347 lines
13 KiB
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);
|
|
}
|
|
}
|
|
}
|