ProjectAres/Lobby/src/main/java/tc/oc/lobby/bukkit/SignUpdater.java

347 lines
13 KiB
Java

package tc.oc.lobby.bukkit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.function.Consumer;
import java.util.logging.Logger;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.inject.Inject;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableSet;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.block.Sign;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.material.MaterialData;
import org.bukkit.util.RayBlockIntersection;
import tc.oc.api.docs.Arena;
import tc.oc.api.docs.Game;
import tc.oc.api.docs.Server;
import tc.oc.api.games.GameStore;
import tc.oc.commons.bukkit.chat.ComponentRenderContext;
import tc.oc.commons.bukkit.event.BlockPunchEvent;
import tc.oc.commons.bukkit.format.GameFormatter;
import tc.oc.commons.bukkit.format.ServerFormatter;
import tc.oc.commons.bukkit.teleport.Navigator;
import tc.oc.commons.bukkit.util.BlockUtils;
import tc.oc.commons.bukkit.util.ChunkLocation;
import tc.oc.commons.bukkit.util.NMSHacks;
import tc.oc.commons.bukkit.util.Vectors;
import tc.oc.commons.core.chat.ChatUtils;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.chat.Components;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.plugin.PluginFacet;
import tc.oc.commons.core.util.CacheUtils;
public class SignUpdater implements Listener, PluginFacet {
private static final int SIGN_WIDTH = 90;
private static final ImmutableSet<Material> PORTAL_MATERIALS = ImmutableSet.of(Material.PORTAL);
private final Logger logger;
private final Collection<World> initialWorlds;
private final ComponentRenderContext renderer;
private final GameStore games;
private final GameFormatter.Dark gameFormatter;
private final Navigator navigator;
private final Set<ChunkLocation> chunks = new HashSet<>();
private final Map<Location, SignHandle> signs = new HashMap<>();
private final Map<Player, SignHandle> hovering = new WeakHashMap<>();
@Inject SignUpdater(Loggers loggers,
Collection<World> initialWorlds,
ComponentRenderContext renderer,
GameStore games,
GameFormatter.Dark gameFormatter,
Navigator navigator) {
this.logger = loggers.get(getClass());
this.initialWorlds = initialWorlds;
this.renderer = renderer;
this.games = games;
this.gameFormatter = gameFormatter;
this.navigator = navigator;
}
@Override
public void enable() {
initialWorlds.forEach(world -> Stream.of(world.getLoadedChunks()).forEach(this::load));
}
@Override
public void disable() {
hovering.forEach((player, sign) -> sign.hover(player, false));
hovering.clear();
}
private @Nullable SignHandle createSign(Sign blockState) {
String prev = "";
final List<Navigator.Connector> connectors = new ArrayList<>();
for(BaseComponent component : blockState.lines()) {
String line = component.toPlainText();
if(line.endsWith("\\")) {
prev += line.substring(0, line.length() - 1);
} else {
final Navigator.Connector connector = navigator.parseConnector(prev + line);
if(connector != null) {
connectors.add(connector);
}
}
}
return connectors.isEmpty() ? null : new SignHandle(blockState, navigator.combineConnectors(connectors));
}
void load(Chunk chunk) {
if(chunks.add(ChunkLocation.of(chunk))) {
for(BlockState blockState : chunk.getTileEntities()) {
if(blockState instanceof Sign) {
final SignHandle sign = createSign((Sign) blockState);
if(sign != null) {
signs.put(blockState.getLocation(), sign);
}
}
}
logger.fine(() -> "Loaded chunk " + chunk.getX() + "," + chunk.getZ() +" with " + signs.size());
}
}
@EventHandler
private void onChunkLoad(ChunkLoadEvent event) {
load(event.getChunk());
}
private Optional<SignHandle> sign(Location location) {
return Optional.ofNullable(signs.get(location));
}
public Optional<SignHandle> nearestSign(Location location) {
return signs.values().stream()
.filter(sign -> sign.connector().isVisible())
.min(Comparator.comparing(sign -> sign.distanceSquared(location)));
}
@EventHandler(priority = EventPriority.MONITOR)
public void move(final PlayerMoveEvent event) {
Player player = event.getPlayer();
Location to = event.getTo();
Location from = event.getFrom();
Block toBlock = to.getBlock();
Block fromBlock = from.getBlock();
handleLook(player, to);
if(PORTAL_MATERIALS.contains(toBlock.getType()) && !PORTAL_MATERIALS.contains(fromBlock.getType())) {
nearestSign(event.getTo()).ifPresent(
sign -> sign.connector().teleport(player)
);
}
}
private void handleLook(Player player, Location loc) {
final RayBlockIntersection hit = player.getWorld().rayTraceBlock(loc.clone().add(0, player.getEyeHeight(), 0), player.getBlockReach(), true, false);
final SignHandle newSign = hit == null ? null : sign(hit.getBlock().getLocation()).orElse(null);
final SignHandle oldSign = hovering.get(player);
if(!Objects.equals(oldSign, newSign)) {
if(oldSign != null) {
hovering.remove(player);
oldSign.hover(player, false);
}
if(newSign != null) {
hovering.put(player, newSign);
newSign.hover(player, true);
}
}
}
private void handleBlockClick(Player player, Block block) {
if(block != null && block.getState() instanceof Sign) {
sign(block.getLocation()).ifPresent(
sign -> sign.connector().teleport(player)
);
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void rightClick(final PlayerInteractEvent event) {
handleBlockClick(event.getPlayer(), event.getClickedBlock());
}
@EventHandler(priority = EventPriority.MONITOR)
public void leftClick(final BlockPunchEvent event) {
handleBlockClick(event.getPlayer(), event.getBlock());
}
class SignHandle {
private final Location location;
private final MaterialData material;
private final Navigator.Connector connector;
private final LoadingCache<BaseComponent, NMSHacks.FakeArmorStand> hoverEntities;
private final Map<Player, NMSHacks.FakeArmorStand> hovering = new WeakHashMap<>();
private final Consumer<Navigator.Connector> observer = c -> paint();
public SignHandle(Sign sign, Navigator.Connector connector) {
this.location = sign.getLocation();
this.material = sign.getMaterialData();
this.connector = connector;
hoverEntities = CacheUtils.newCache(
description -> new NMSHacks.FakeArmorStand(this.location.getWorld(), description.toLegacyText())
);
connector.startObserving(observer);
paint();
logger.fine("Created " + this);
}
@Override
public String toString() {
return getClass().getSimpleName() +
"{location=(" + Vectors.format(location.position(), "%.0f") +
") connector=" + connector +
"}";
}
public double distanceSquared(Location to) {
return this.location.getWorldId().equals(to.getWorldId()) ? this.location.distanceSquared(to) : Double.POSITIVE_INFINITY;
}
public Navigator.Connector connector() {
return connector;
}
public void hover(Player player, boolean over) {
if(over) {
final BaseComponent description = connector.description();
if(description != null) {
final NMSHacks.FakeArmorStand entity = hoverEntities.getUnchecked(renderer.render(new Component(description, ChatColor.DARK_AQUA), player));
hovering.put(player, entity);
entity.spawn(player, BlockUtils.center(location));
}
} else {
final NMSHacks.FakeArmorStand entity = hovering.remove(player);
if(entity != null) {
entity.destroy(player);
}
}
}
private void paint() {
final BaseComponent[] lines = new BaseComponent[4];
if(connector.isVisible()) {
final Object mapped = connector.mappedTo();
if(Navigator.DEFAULT_MAPPING.equals(mapped)) {
renderDefault(lines);
} else if(mapped instanceof Server) {
renderServer(lines, (Server) mapped);
} else if(mapped instanceof Arena) {
renderArena(lines, (Arena) mapped);
}
}
final BlockState block = location.getBlock().getState();
if(lines[0] != null) {
final Sign sign;
if(block instanceof Sign) {
sign = (Sign) block;
} else {
block.setMaterialData(material);
block.update(true, false);
sign = (Sign) location.getBlock().getState();
}
for(int i = 0; i < 4; i++) {
sign.setLine(i, renderer.render(lines[i], Bukkit.getConsoleSender()));
}
block.update(true, false);
} else if(block.getMaterial() != Material.AIR) {
block.setMaterial(Material.AIR);
block.update(true, false);
}
}
void renderDefault(BaseComponent[] lines) {
lines[0] = Components.blank();
lines[1] = new Component(new TranslatableComponent("servers.backToLobby"), ChatColor.DARK_BLUE);
lines[2] = Components.blank();
lines[3] = Components.blank();
}
void renderServer(BaseComponent[] lines, Server server) {
final ServerFormatter formatter = ServerFormatter.dark;
lines[0] = formatter.name(server);
if(formatter.isRestarting(server)) {
lines[1] = Components.blank();
lines[2] = Components.blank();
lines[3] = formatter.onlineStatus(server);
} else {
lines[1] = formatter.playerCounts(server, false);
if(server.current_match() != null && server.current_match().map() != null) {
final ChatColor color = formatter.matchStatusColor(server);
final List<String> matchText = ChatUtils.wordWrap(server.current_match().map().name(), SIGN_WIDTH);
if(matchText.size() == 1) {
lines[2] = Components.blank();
lines[3] = new Component(matchText.get(0), color);
} else {
lines[2] = new Component(matchText.get(0), color);
lines[3] = new Component(matchText.get(1), color);
}
} else {
lines[2] = Components.blank();
lines[3] = Components.blank();
}
}
}
void renderArena(BaseComponent[] lines, Arena arena) {
final Game game = games.byId(arena.game_id());
lines[0] = new Component(game.name(), ChatColor.BLACK, ChatColor.BOLD);
lines[1] = Components.blank();
lines[2] = gameFormatter.playingCount(arena);
lines[3] = gameFormatter.waitingCount(arena);
}
}
}