diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/Movecraft.java b/Movecraft/src/main/java/net/countercraft/movecraft/Movecraft.java index fe69859c1..0a72e101b 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/Movecraft.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/Movecraft.java @@ -23,7 +23,6 @@ import net.countercraft.movecraft.config.Settings; import net.countercraft.movecraft.craft.ChunkManager; import net.countercraft.movecraft.craft.CraftManager; -import net.countercraft.movecraft.craft.datatag.CraftDataTagRegistry; import net.countercraft.movecraft.features.contacts.ContactsCommand; import net.countercraft.movecraft.features.contacts.ContactsManager; import net.countercraft.movecraft.features.contacts.ContactsSign; @@ -59,6 +58,7 @@ public class Movecraft extends JavaPlugin { private SmoothTeleport smoothTeleport; private AsyncManager asyncManager; private WreckManager wreckManager; + private SignListener abstractSignListener; public static synchronized Movecraft getInstance() { return instance; @@ -213,7 +213,6 @@ public void onEnable() { getServer().getPluginManager().registerEvents(new PlayerListener(), this); getServer().getPluginManager().registerEvents(new ChunkManager(), this); getServer().getPluginManager().registerEvents(new AscendSign(), this); - getServer().getPluginManager().registerEvents(new CraftSign(), this); getServer().getPluginManager().registerEvents(new CruiseSign(), this); getServer().getPluginManager().registerEvents(new DescendSign(), this); getServer().getPluginManager().registerEvents(new HelmSign(), this); @@ -221,7 +220,6 @@ public void onEnable() { getServer().getPluginManager().registerEvents(new NameSign(), this); getServer().getPluginManager().registerEvents(new PilotSign(), this); getServer().getPluginManager().registerEvents(new RelativeMoveSign(), this); - getServer().getPluginManager().registerEvents(new ReleaseSign(), this); getServer().getPluginManager().registerEvents(new RemoteSign(), this); getServer().getPluginManager().registerEvents(new SpeedSign(), this); getServer().getPluginManager().registerEvents(new SubcraftRotateSign(), this); @@ -229,6 +227,11 @@ public void onEnable() { getServer().getPluginManager().registerEvents(new ScuttleSign(), this); getServer().getPluginManager().registerEvents(new CraftPilotListener(), this); getServer().getPluginManager().registerEvents(new CraftReleaseListener(), this); + getServer().getPluginManager().registerEvents(new CraftTypeListener(), this); + getServer().getPluginManager().registerEvents(new SignListener(), this); + + // Signs + AbstractMovecraftSign.register("Release", new ReleaseSign()); var contactsManager = new ContactsManager(); contactsManager.runTaskTimerAsynchronously(this, 0, 20); @@ -340,4 +343,8 @@ public AsyncManager getAsyncManager() { public @NotNull WreckManager getWreckManager(){ return wreckManager; } + + public SignListener getAbstractSignListener() { + return abstractSignListener; + } } diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/craft/CraftManager.java b/Movecraft/src/main/java/net/countercraft/movecraft/craft/CraftManager.java index 1db3db008..dd8001831 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/craft/CraftManager.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/craft/CraftManager.java @@ -31,6 +31,8 @@ import net.countercraft.movecraft.processing.effects.Effect; import net.countercraft.movecraft.processing.functions.CraftSupplier; import net.countercraft.movecraft.processing.tasks.detection.DetectionTask; +import net.countercraft.movecraft.sign.AbstractMovecraftSign; +import net.countercraft.movecraft.sign.CraftPilotSign; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; @@ -96,6 +98,9 @@ private CraftManager(boolean loadCraftTypes) { craftTypes = loadCraftTypes(); else craftTypes = new HashSet<>(); + + // Since the event is only created in reload cases... + AbstractMovecraftSign.registerCraftPilotSigns(CraftManager.getInstance().getCraftTypes(), CraftPilotSign::new); } @NotNull diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/listener/CraftTypeListener.java b/Movecraft/src/main/java/net/countercraft/movecraft/listener/CraftTypeListener.java new file mode 100644 index 000000000..6b3748b89 --- /dev/null +++ b/Movecraft/src/main/java/net/countercraft/movecraft/listener/CraftTypeListener.java @@ -0,0 +1,17 @@ +package net.countercraft.movecraft.listener; + +import net.countercraft.movecraft.craft.CraftManager; +import net.countercraft.movecraft.events.TypesReloadedEvent; +import net.countercraft.movecraft.sign.AbstractMovecraftSign; +import net.countercraft.movecraft.sign.CraftPilotSign; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; + +public class CraftTypeListener implements Listener { + + @EventHandler + public void onReload(TypesReloadedEvent event) { + AbstractMovecraftSign.registerCraftPilotSigns(CraftManager.getInstance().getCraftTypes(), CraftPilotSign::new); + } + +} diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/mapUpdater/update/CraftRotateCommand.java b/Movecraft/src/main/java/net/countercraft/movecraft/mapUpdater/update/CraftRotateCommand.java index 01fbef041..a42e68287 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/mapUpdater/update/CraftRotateCommand.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/mapUpdater/update/CraftRotateCommand.java @@ -14,7 +14,7 @@ import net.countercraft.movecraft.craft.SinkingCraft; import net.countercraft.movecraft.craft.type.CraftType; import net.countercraft.movecraft.events.CraftReleaseEvent; -import net.countercraft.movecraft.events.SignTranslateEvent; +import net.countercraft.movecraft.sign.SignListener; import net.countercraft.movecraft.util.CollectionUtils; import net.countercraft.movecraft.util.MathUtils; import net.countercraft.movecraft.util.Tags; @@ -201,55 +201,7 @@ public void doUpdate() { } private void sendSignEvents() { - Object2ObjectMap> signs = new Object2ObjectOpenCustomHashMap<>(new Hash.Strategy() { - @Override - public int hashCode(String[] strings) { - return Arrays.hashCode(strings); - } - - @Override - public boolean equals(String[] a, String[] b) { - return Arrays.equals(a, b); - } - }); - Map signStates = new HashMap<>(); - - for (MovecraftLocation location : craft.getHitBox()) { - Block block = location.toBukkit(craft.getWorld()).getBlock(); - BlockState state = block.getState(); - if (state instanceof Sign) { - Sign sign = (Sign) block.getState(); - if (!signs.containsKey(sign.getLines())) - signs.put(sign.getLines(), new ArrayList<>()); - signs.get(sign.getLines()).add(location); - signStates.put(location, sign); - } - } - for (Map.Entry> entry : signs.entrySet()) { - SignTranslateEvent event = new SignTranslateEvent(craft, entry.getKey(), entry.getValue()); - Bukkit.getServer().getPluginManager().callEvent(event); - // if(!event.isUpdated()){ - // continue; - // } - // TODO: This is implemented only to fix client caching - // ideally we wouldn't do the update and would instead fake it out to the player - for (MovecraftLocation location : entry.getValue()) { - Block block = location.toBukkit(craft.getWorld()).getBlock(); - BlockState state = block.getState(); - BlockData data = block.getBlockData(); - if (!(state instanceof Sign)) { - continue; - } - Sign sign = signStates.get(location); - if (event.isUpdated()) { - for (int i = 0; i < 4; i++) { - sign.setLine(i, entry.getKey()[i]); - } - } - sign.update(false, false); - block.setBlockData(data); - } - } + SignListener.INSTANCE.processSignTranslation(craft, true); } @NotNull diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/mapUpdater/update/CraftTranslateCommand.java b/Movecraft/src/main/java/net/countercraft/movecraft/mapUpdater/update/CraftTranslateCommand.java index 7fa64e43a..6351a968f 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/mapUpdater/update/CraftTranslateCommand.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/mapUpdater/update/CraftTranslateCommand.java @@ -14,7 +14,7 @@ import net.countercraft.movecraft.craft.SinkingCraft; import net.countercraft.movecraft.craft.type.CraftType; import net.countercraft.movecraft.events.CraftReleaseEvent; -import net.countercraft.movecraft.events.SignTranslateEvent; +import net.countercraft.movecraft.sign.SignListener; import net.countercraft.movecraft.util.MathUtils; import net.countercraft.movecraft.util.Tags; import net.countercraft.movecraft.util.hitboxes.HitBox; @@ -303,54 +303,7 @@ private LinkedList hullSearch(SetHitBox validExterior) { } private void sendSignEvents(){ - Object2ObjectMap> signs = new Object2ObjectOpenCustomHashMap<>(new Hash.Strategy() { - @Override - public int hashCode(String[] strings) { - return Arrays.hashCode(strings); - } - - @Override - public boolean equals(String[] a, String[] b) { - return Arrays.equals(a, b); - } - }); - Map signStates = new HashMap<>(); - - for (MovecraftLocation location : craft.getHitBox()) { - Block block = location.toBukkit(craft.getWorld()).getBlock(); - if(!Tag.SIGNS.isTagged(block.getType())){ - continue; - } - BlockState state = block.getState(); - if (state instanceof Sign) { - Sign sign = (Sign) state; - if(!signs.containsKey(sign.getLines())) - signs.put(sign.getLines(), new ArrayList<>()); - signs.get(sign.getLines()).add(location); - signStates.put(location, sign); - } - } - for(Map.Entry> entry : signs.entrySet()){ - SignTranslateEvent event = new SignTranslateEvent(craft, entry.getKey(), entry.getValue()); - Bukkit.getServer().getPluginManager().callEvent(event); - // if(!event.isUpdated()){ - // continue; - // } - // TODO: This is implemented only to fix client caching - // ideally we wouldn't do the update and would instead fake it out to the player - for(MovecraftLocation location : entry.getValue()){ - Block block = location.toBukkit(craft.getWorld()).getBlock(); - BlockState state = block.getState(); - if (!(state instanceof Sign)) { - continue; - } - Sign sign = signStates.get(location); - for(int i = 0; i<4; i++){ - sign.setLine(i, entry.getKey()[i]); - } - sign.update(false, false); - } - } + SignListener.INSTANCE.processSignTranslation(craft, false); } @NotNull diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/sign/CraftSign.java b/Movecraft/src/main/java/net/countercraft/movecraft/sign/CraftPilotSign.java similarity index 62% rename from Movecraft/src/main/java/net/countercraft/movecraft/sign/CraftSign.java rename to Movecraft/src/main/java/net/countercraft/movecraft/sign/CraftPilotSign.java index ec5a977c5..3e5800b59 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/sign/CraftSign.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/sign/CraftPilotSign.java @@ -4,12 +4,7 @@ import net.countercraft.movecraft.Movecraft; import net.countercraft.movecraft.MovecraftLocation; import net.countercraft.movecraft.config.Settings; -import net.countercraft.movecraft.craft.Craft; -import net.countercraft.movecraft.craft.CraftManager; -import net.countercraft.movecraft.craft.CruiseOnPilotCraft; -import net.countercraft.movecraft.craft.CruiseOnPilotSubCraft; -import net.countercraft.movecraft.craft.PlayerCraftImpl; -import net.countercraft.movecraft.craft.SubCraft; +import net.countercraft.movecraft.craft.*; import net.countercraft.movecraft.craft.type.CraftType; import net.countercraft.movecraft.events.CraftPilotEvent; import net.countercraft.movecraft.events.CraftReleaseEvent; @@ -17,76 +12,70 @@ import net.countercraft.movecraft.processing.functions.Result; import net.countercraft.movecraft.util.Pair; import org.bukkit.Bukkit; -import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.World; -import org.bukkit.block.BlockState; -import org.bukkit.block.Sign; -import org.bukkit.block.data.Directional; +import org.bukkit.block.BlockFace; import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; import org.bukkit.event.block.Action; import org.bukkit.event.block.SignChangeEvent; -import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.scheduler.BukkitRunnable; -import org.jetbrains.annotations.NotNull; +import java.util.Collections; import java.util.HashSet; import java.util.Set; -public final class CraftSign implements Listener { - private final Set piloting = new HashSet<>(); +//TODO: This is not very pretty... +public class CraftPilotSign extends AbstractCraftPilotSign { - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onSignChange(@NotNull SignChangeEvent event) { - if (CraftManager.getInstance().getCraftTypeFromString(event.getLine(0)) == null) - return; + static final Set PILOTING = Collections.synchronizedSet(new HashSet<>()); - if (!Settings.RequireCreatePerm) - return; - - if (!event.getPlayer().hasPermission("movecraft." + ChatColor.stripColor(event.getLine(0)) + ".create")) { - event.getPlayer().sendMessage(I18nSupport.getInternationalisedString("Insufficient Permissions")); - event.setCancelled(true); - } + public CraftPilotSign(CraftType craftType) { + super(craftType); } - @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) - public void onSignClick(@NotNull PlayerInteractEvent event) { - if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getClickedBlock() == null) - return; - - BlockState state = event.getClickedBlock().getState(); - if (!(state instanceof Sign)) - return; - - Sign sign = (Sign) state; - CraftType craftType = CraftManager.getInstance().getCraftTypeFromString(ChatColor.stripColor(sign.getLine(0))); - if (craftType == null) - return; - - // Valid sign prompt for ship command. - event.setCancelled(true); - Player player = event.getPlayer(); - if (!player.hasPermission("movecraft." + ChatColor.stripColor(sign.getLine(0)) + ".pilot")) { + @Override + protected boolean isSignValid(Action clickType, SignListener.SignWrapper sign, Player player) { + String header = sign.getRaw(0).trim(); + CraftType craftType = CraftManager.getInstance().getCraftTypeFromString(header); + if (craftType != this.craftType) { + return false; + } + if (!player.hasPermission("movecraft." + header + ".pilot")) { player.sendMessage(I18nSupport.getInternationalisedString("Insufficient Permissions")); - return; + return false; + } else { + return true; } + } - Location loc = event.getClickedBlock().getLocation(); + @Override + protected boolean internalProcessSign(Action clickType, SignListener.SignWrapper sign, Player player, @javax.annotation.Nullable Craft craft) { + if (this.craftType.getBoolProperty(CraftType.MUST_BE_SUBCRAFT) && craft == null) { + return false; + } + World world = sign.block().getWorld(); + if (craft != null) { + world = craft.getWorld(); + } + Location loc = sign.block().getLocation(); MovecraftLocation startPoint = new MovecraftLocation(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); - if (piloting.contains(startPoint)) { - return; + + if (PILOTING.contains(startPoint)) { + // Always return true + return true; } - // Attempt to run detection - World world = event.getClickedBlock().getWorld(); + runDetectTask(startPoint, player, sign, craft, world); + + return true; + } + protected void runDetectTask(MovecraftLocation startPoint, Player player, SignListener.SignWrapper signWrapper, Craft parentCraft, World world) { + PILOTING.add(startPoint); CraftManager.getInstance().detect( startPoint, craftType, (type, w, p, parents) -> { + // Assert instructions are not available normally, also this is checked in beforehand sort of assert p != null; // Note: This only passes in a non-null player. if (type.getBoolProperty(CraftType.CRUISE_ON_PILOT)) { if (parents.size() > 1) @@ -122,10 +111,8 @@ public void onSignClick(@NotNull PlayerInteractEvent event) { if (craft.getType().getBoolProperty(CraftType.CRUISE_ON_PILOT)) { // Setup cruise direction - if (sign.getBlockData() instanceof Directional) - craft.setCruiseDirection(CruiseDirection.fromBlockFace(((Directional) sign.getBlockData()).getFacing())); - else - craft.setCruiseDirection(CruiseDirection.NONE); + BlockFace facing = signWrapper.facing(); + craft.setCruiseDirection(CruiseDirection.fromBlockFace(facing)); // Start craft cruising craft.setLastCruiseUpdate(System.currentTimeMillis()); @@ -148,11 +135,34 @@ public void run() { } } ); + // TODO: Move this to be directly called by the craftmanager post detection... + // Or use the event handler or something new BukkitRunnable() { @Override public void run() { - piloting.remove(startPoint); + PILOTING.remove(startPoint); } }.runTaskLater(Movecraft.getInstance(), 4); + + } + + @Override + public boolean processSignChange(SignChangeEvent event, SignListener.SignWrapper sign) { + String header = sign.getRaw(0).trim(); + CraftType craftType = CraftManager.getInstance().getCraftTypeFromString(header); + if (craftType != this.craftType) { + return false; + } + if (Settings.RequireCreatePerm) { + Player player = event.getPlayer(); + if (!player.hasPermission("movecraft." + header + ".create")) { + player.sendMessage(I18nSupport.getInternationalisedString("Insufficient Permissions")); + return false; + } else { + return true; + } + } else { + return true; + } } } diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/sign/ReleaseSign.java b/Movecraft/src/main/java/net/countercraft/movecraft/sign/ReleaseSign.java index 91e9aa544..d7b7bfa3e 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/sign/ReleaseSign.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/sign/ReleaseSign.java @@ -2,38 +2,38 @@ import net.countercraft.movecraft.craft.Craft; import net.countercraft.movecraft.craft.CraftManager; +import net.countercraft.movecraft.craft.PilotedCraft; import net.countercraft.movecraft.events.CraftReleaseEvent; -import org.bukkit.ChatColor; -import org.bukkit.block.BlockState; -import org.bukkit.block.Sign; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; +import org.bukkit.entity.Player; import org.bukkit.event.block.Action; -import org.bukkit.event.player.PlayerInteractEvent; -import org.jetbrains.annotations.NotNull; +import org.bukkit.event.block.SignChangeEvent; +import org.jetbrains.annotations.Nullable; -public final class ReleaseSign implements Listener{ - private static final String HEADER = "Release"; +public class ReleaseSign extends AbstractMovecraftSign { - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onSignClick(@NotNull PlayerInteractEvent event) { - if (event.getAction() != Action.RIGHT_CLICK_BLOCK) { - return; - } - BlockState state = event.getClickedBlock().getState(); - if (!(state instanceof Sign)) { - return; - } - Sign sign = (Sign) state; - if (!ChatColor.stripColor(sign.getLine(0)).equalsIgnoreCase(HEADER)) { - return; + public ReleaseSign() { + super(null); + } + + @Override + protected boolean isSignValid(Action clickType, SignListener.SignWrapper sign, Player player) { + return true; + } + + @Override + protected boolean internalProcessSign(Action clickType, SignListener.SignWrapper sign, Player player, @Nullable Craft craft) { + if (craft == null || (craft instanceof PilotedCraft pc && pc.getPilot() != player)) { + craft = CraftManager.getInstance().getCraftByPlayer(player); } - event.setCancelled(true); - Craft craft = CraftManager.getInstance().getCraftByPlayer(event.getPlayer()); - if (craft == null) { - return; + if (craft != null) { + CraftManager.getInstance().release(craft, CraftReleaseEvent.Reason.PLAYER, false); } - CraftManager.getInstance().release(craft, CraftReleaseEvent.Reason.PLAYER, false); + return true; + } + + @Override + public boolean processSignChange(SignChangeEvent event, SignListener.SignWrapper sign) { + return false; } + } diff --git a/api/src/main/java/net/countercraft/movecraft/MovecraftRotation.java b/api/src/main/java/net/countercraft/movecraft/MovecraftRotation.java index f9b580ee7..1e9362bbe 100644 --- a/api/src/main/java/net/countercraft/movecraft/MovecraftRotation.java +++ b/api/src/main/java/net/countercraft/movecraft/MovecraftRotation.java @@ -17,6 +17,21 @@ package net.countercraft.movecraft; +import org.bukkit.event.block.Action; + public enum MovecraftRotation { - CLOCKWISE, NONE, ANTICLOCKWISE + CLOCKWISE, NONE, ANTICLOCKWISE; + + public static MovecraftRotation fromAction(Action clickType) { + switch (clickType) { + case LEFT_CLICK_AIR: + case LEFT_CLICK_BLOCK: + return ANTICLOCKWISE; + case RIGHT_CLICK_AIR: + case RIGHT_CLICK_BLOCK: + return CLOCKWISE; + default: + return NONE; + } + } } diff --git a/api/src/main/java/net/countercraft/movecraft/craft/datatag/CraftDataTagRegistry.java b/api/src/main/java/net/countercraft/movecraft/craft/datatag/CraftDataTagRegistry.java index d542f8405..96207624e 100644 --- a/api/src/main/java/net/countercraft/movecraft/craft/datatag/CraftDataTagRegistry.java +++ b/api/src/main/java/net/countercraft/movecraft/craft/datatag/CraftDataTagRegistry.java @@ -10,6 +10,9 @@ import java.util.concurrent.ConcurrentMap; import java.util.function.Function; +/** + * TODO: Change to extend @link SimpleRegistry in the future + */ public class CraftDataTagRegistry { public static final @NotNull CraftDataTagRegistry INSTANCE = new CraftDataTagRegistry(); diff --git a/api/src/main/java/net/countercraft/movecraft/events/SignTranslateEvent.java b/api/src/main/java/net/countercraft/movecraft/events/SignTranslateEvent.java index 5439cfdad..75a10abff 100644 --- a/api/src/main/java/net/countercraft/movecraft/events/SignTranslateEvent.java +++ b/api/src/main/java/net/countercraft/movecraft/events/SignTranslateEvent.java @@ -2,46 +2,97 @@ import net.countercraft.movecraft.MovecraftLocation; import net.countercraft.movecraft.craft.Craft; +import net.countercraft.movecraft.sign.SignListener; +import net.kyori.adventure.text.Component; +import org.bukkit.block.BlockFace; import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +/** + * Obsolete, functionality is covered by the new sign system + */ +@Deprecated(forRemoval = true) public class SignTranslateEvent extends CraftEvent{ private static final HandlerList HANDLERS = new HandlerList(); @NotNull private final List locations; - @NotNull private final String[] lines; + @NotNull private final SignListener.SignWrapper backing; private boolean updated = false; + @Deprecated(forRemoval = true) public SignTranslateEvent(@NotNull Craft craft, @NotNull String[] lines, @NotNull List locations) throws IndexOutOfBoundsException{ super(craft); this.locations = locations; - if(lines.length!=4) - throw new IndexOutOfBoundsException(); - this.lines=lines; + List components = new ArrayList<>(); + for (String s : lines) { + components.add(Component.text(s)); + } + this.backing = new SignListener.SignWrapper(null, components::get, components, components::set, BlockFace.SELF); + } + + public SignTranslateEvent(@NotNull Craft craft, @NotNull SignListener.SignWrapper backing, @NotNull List locations) throws IndexOutOfBoundsException{ + super(craft); + this.locations = locations; + this.backing = backing; + } + + public @NotNull SignListener.SignWrapper getBacking() { + return this.backing; } @NotNull - @Deprecated + @Deprecated(forRemoval = true) public String[] getLines() { + // Why does this set it to updated? This is just reading... + // => Lines can be updated externally. We need to mark all signs as updated so it displays properly on clients this.updated = true; - return lines; + return backing.rawLines(); } + @Deprecated(forRemoval = true) public String getLine(int index) throws IndexOutOfBoundsException{ if(index > 3 || index < 0) throw new IndexOutOfBoundsException(); - return lines[index]; + return backing.getRaw(index); } + @Deprecated(forRemoval = true) public void setLine(int index, String line){ if(index > 3 || index < 0) throw new IndexOutOfBoundsException(); this.updated = true; - lines[index]=line; + backing.line(index, Component.text(line)); + } + + public Component line(int index) { + return backing.line(index); + } + + public void line(int index, Component component) { + this.updated = true; + backing.line(index, component); + } + + public String getRaw(int index) { + return backing.getRaw(index); + } + + public String[] rawLines() { + return backing.rawLines(); + } + + public BlockFace facing() { + return backing.facing(); + } + + public List lines() { + return backing.lines(); } + // Bukkit @Override public HandlerList getHandlers() { return HANDLERS; diff --git a/api/src/main/java/net/countercraft/movecraft/sign/AbstractCraftPilotSign.java b/api/src/main/java/net/countercraft/movecraft/sign/AbstractCraftPilotSign.java new file mode 100644 index 000000000..501cf4743 --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/sign/AbstractCraftPilotSign.java @@ -0,0 +1,21 @@ +package net.countercraft.movecraft.sign; + +import net.countercraft.movecraft.craft.type.CraftType; + +/* + * Base implementation for all craft pilot signs, does nothing but has the relevant CraftType instance backed + */ +public abstract class AbstractCraftPilotSign extends AbstractMovecraftSign { + + protected final CraftType craftType; + + public AbstractCraftPilotSign(final CraftType craftType) { + super(); + this.craftType = craftType; + } + + public CraftType getCraftType() { + return this.craftType; + } + +} diff --git a/api/src/main/java/net/countercraft/movecraft/sign/AbstractCraftSign.java b/api/src/main/java/net/countercraft/movecraft/sign/AbstractCraftSign.java new file mode 100644 index 000000000..2dbfde88d --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/sign/AbstractCraftSign.java @@ -0,0 +1,125 @@ +package net.countercraft.movecraft.sign; + +import net.countercraft.movecraft.MovecraftLocation; +import net.countercraft.movecraft.craft.Craft; +import net.countercraft.movecraft.craft.PilotedCraft; +import net.countercraft.movecraft.craft.PlayerCraft; +import net.countercraft.movecraft.events.CraftDetectEvent; +import net.countercraft.movecraft.events.SignTranslateEvent; +import net.kyori.adventure.text.Component; +import org.bukkit.entity.Player; +import org.bukkit.event.block.Action; + +import javax.annotation.Nullable; +import java.util.List; + +/* + * Extension of @AbstractMovecraftSign + * The difference is, that for this sign to work, it must exist on a craft instance + * + * Also this will react to the SignTranslate event + */ +public abstract class AbstractCraftSign extends AbstractMovecraftSign { + + // Helper method for the listener + // Use the methods in MovecraftSignRegistry instead + @Deprecated(forRemoval = true) + public static @Nullable AbstractCraftSign getCraftSign(final Component ident) { + return MovecraftSignRegistry.INSTANCE.getCraftSign(ident); + } + + // Use the methods in MovecraftSignRegistry instead + @Deprecated(forRemoval = true) + public static @Nullable AbstractCraftSign getCraftSign(final String ident) { + return MovecraftSignRegistry.INSTANCE.getCraftSign(ident); + } + + protected final boolean ignoreCraftIsBusy; + + public AbstractCraftSign(boolean ignoreCraftIsBusy) { + this(null, ignoreCraftIsBusy); + } + + public AbstractCraftSign(final String permission, boolean ignoreCraftIsBusy) { + super(permission); + this.ignoreCraftIsBusy = ignoreCraftIsBusy; + } + + // Similar to the super class variant + // In addition a check for a existing craft is being made + // If the craft is a player craft that is currently processing and ignoreCraftIsBusy is set to false, this will quit early and call onCraftIsBusy() + // If no craft is found, onCraftNotFound() is called + // Return true to cancel the event + @Override + public boolean processSignClick(Action clickType, SignListener.SignWrapper sign, Player player) { + if (!this.isSignValid(clickType, sign, player)) { + return false; + } + if (!this.canPlayerUseSign(clickType, sign, player)) { + return false; + } + Craft craft = this.getCraft(sign); + if (craft == null) { + this.onCraftNotFound(player, sign); + return false; + } + + if (craft instanceof PlayerCraft pc) { + if (!pc.isNotProcessing() && !this.ignoreCraftIsBusy) { + this.onCraftIsBusy(player, craft); + return false; + } + } + + return internalProcessSign(clickType, sign, player, craft); + } + + // Implementation of the standard method. + // The craft instance is required here and it's existance is being confirmed in processSignClick() in beforehand + // After that, canPlayerUseSignOn() is being called. If that is successful, the result of internalProcessSignWithCraft() is returned + @Override + protected boolean internalProcessSign(Action clickType, SignListener.SignWrapper sign, Player player, @Nullable Craft craft) { + if (craft == null) { + throw new IllegalStateException("Somehow craft is not set here. It should always be present here!"); + } + if (this.canPlayerUseSignOn(player, craft)) { + return this.internalProcessSignWithCraft(clickType, sign, craft, player); + } + return false; + } + + // Called when the craft is a player craft and is processing and ignoreCraftIsBusy is set to false + protected abstract void onCraftIsBusy(Player player, Craft craft); + + // Validation method, intended to indicate if a player is allowed to execute a sign action on a mounted craft + // By default, this returns wether or not the player is the pilot of the craft + protected boolean canPlayerUseSignOn(Player player, @Nullable Craft craft) { + if (craft instanceof PilotedCraft pc) { + return pc.getPilot() == player; + } + return true; + } + + // Called when there is no craft instance for this sign + protected abstract void onCraftNotFound(Player player, SignListener.SignWrapper sign); + + // By default we don't react to CraftDetectEvent here + public void onCraftDetect(CraftDetectEvent event, SignListener.SignWrapper sign) { + // Do nothing by default + } + + // Return true if you modified anything + public boolean processSignTranslation(final Craft translatingCraft, SignListener.SignWrapper movingData, @Nullable List signLocations) { + // DO nothing by default + return false; + } + + public void onSignMovedByCraft(SignTranslateEvent event) { + this.processSignTranslation(event.getCraft(), event.getBacking(), event.getLocations()); + } + + // Gets called by internalProcessSign if a craft is found + // Always override this as the validation has been made already when this is being called + protected abstract boolean internalProcessSignWithCraft(Action clickType, SignListener.SignWrapper sign, Craft craft, Player player); + +} diff --git a/api/src/main/java/net/countercraft/movecraft/sign/AbstractCruiseSign.java b/api/src/main/java/net/countercraft/movecraft/sign/AbstractCruiseSign.java new file mode 100644 index 000000000..24df56341 --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/sign/AbstractCruiseSign.java @@ -0,0 +1,58 @@ +package net.countercraft.movecraft.sign; + +import net.countercraft.movecraft.CruiseDirection; +import net.countercraft.movecraft.craft.Craft; +import org.bukkit.entity.Player; + +/* + * Base class for all cruise signs + * + * Has the relevant logic for the "state" suffix (on / off) as well as calling the relevant methods and setting the craft to cruising + * + */ +public abstract class AbstractCruiseSign extends AbstractToggleSign { + + public AbstractCruiseSign(boolean ignoreCraftIsBusy, String ident, String suffixOn, String suffixOff) { + super(ignoreCraftIsBusy, ident, suffixOn, suffixOff); + } + + public AbstractCruiseSign(final String permission, boolean ignoreCraftIsBusy, String ident, String suffixOn, String suffixOff) { + super(permission, ignoreCraftIsBusy, ident, suffixOn, suffixOff); + } + + // Hook to do stuff that run after stopping to cruise + protected void onAfterStoppingCruise(Craft craft, SignListener.SignWrapper signWrapper, Player player) { + + } + + // Hook to do stuff that run after starting to cruise + protected void onAfterStartingCruise(Craft craft, SignListener.SignWrapper signWrapper, Player player) { + + } + + @Override + protected void onAfterToggle(Craft craft, SignListener.SignWrapper signWrapper, Player player, boolean toggledToOn) { + if (toggledToOn) { + this.onAfterStartingCruise(craft, signWrapper, player); + } else { + this.onAfterStoppingCruise(craft, signWrapper, player); + } + } + + @Override + protected void onBeforeToggle(Craft craft, SignListener.SignWrapper signWrapper, Player player, boolean willBeOn) { + if (willBeOn) { + CruiseDirection cruiseDirection = this.getCruiseDirection(signWrapper); + this.setCraftCruising(player, cruiseDirection, craft); + } else { + craft.setCruising(false); + } + } + + // Should call the craft's relevant methods to start cruising + protected abstract void setCraftCruising(Player player, CruiseDirection direction, Craft craft); + + // TODO: Rework cruise direction to vectors => Vector defines the skip distance and the direction + // Returns the direction in which the craft should cruise + protected abstract CruiseDirection getCruiseDirection(SignListener.SignWrapper sign); +} diff --git a/api/src/main/java/net/countercraft/movecraft/sign/AbstractInformationSign.java b/api/src/main/java/net/countercraft/movecraft/sign/AbstractInformationSign.java new file mode 100644 index 000000000..f3c97ac31 --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/sign/AbstractInformationSign.java @@ -0,0 +1,160 @@ +package net.countercraft.movecraft.sign; + +import net.countercraft.movecraft.MovecraftLocation; +import net.countercraft.movecraft.craft.Craft; +import net.countercraft.movecraft.events.CraftDetectEvent; +import net.countercraft.movecraft.events.SignTranslateEvent; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.Sign; +import org.bukkit.entity.Player; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.SignChangeEvent; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public abstract class AbstractInformationSign extends AbstractCraftSign { + + public static final Component EMPTY = Component.text(""); + + protected static final Style STYLE_COLOR_GREEN = Style.style(TextColor.color(0, 255, 0)); + protected static final Style STYLE_COLOR_YELLOW = Style.style(TextColor.color(255, 255, 0)); + protected static final Style STYLE_COLOR_RED = Style.style(TextColor.color(255, 0, 0)); + protected static final Style STYLE_COLOR_WHITE = Style.style(TextColor.color(255, 255, 255)); + + public enum REFRESH_CAUSE { + SIGN_CREATION, + CRAFT_DETECT, + SIGN_MOVED_BY_CRAFT, + SIGN_CLICK + } + + public AbstractInformationSign() { + // Info signs only display things, that should not require permissions, also it doesn't matter if the craft is busy or not + super(null, true); + } + + @Override + protected boolean canPlayerUseSignOn(Player player, Craft craft) { + // Permcheck related, no perms required, return true + return true; + } + + @Override + public void onCraftDetect(CraftDetectEvent event, SignListener.SignWrapper sign) { + // TODO: Check if the craft supports this sign? If no, cancel + super.onCraftDetect(event, sign); + this.refreshSign(event.getCraft(), sign, true, REFRESH_CAUSE.CRAFT_DETECT); + sign.block().update(); + } + + @Override + public boolean processSignTranslation(Craft translatingCraft, SignListener.SignWrapper movingData, @Nullable List signLocations) { + //SignListener.SignWrapper wrapperTmp = new SignListener.SignWrapper(null, movingData::line, movingData.lines(), movingData::line, BlockFace.SELF); + if (this.refreshSign(translatingCraft, movingData, false, REFRESH_CAUSE.SIGN_MOVED_BY_CRAFT)) { + for (MovecraftLocation movecraftLocation : signLocations) { + Block block = movecraftLocation.toBukkit(translatingCraft.getWorld()).getBlock(); + if (block instanceof Sign sign) { + SignListener.SignWrapper wrapperTmpTmp = new SignListener.SignWrapper(sign, movingData::line, movingData.lines(), movingData::line, movingData.facing()); + this.sendUpdatePacket(translatingCraft, wrapperTmpTmp, REFRESH_CAUSE.SIGN_MOVED_BY_CRAFT); + } + } + } + // We looped over the lines, so we HAVE to return true here + return true; + } + + @Override + protected void onCraftNotFound(Player player, SignListener.SignWrapper sign) { + // Nothing to do + } + + @Override + protected boolean isSignValid(Action clickType, SignListener.SignWrapper sign, Player player) { + return true; + } + + @Override + protected boolean internalProcessSignWithCraft(Action clickType, SignListener.SignWrapper sign, Craft craft, Player player) { + if (this.refreshSign(craft, sign, false, REFRESH_CAUSE.SIGN_CLICK)) { + this.sendUpdatePacket(craft, sign, REFRESH_CAUSE.SIGN_CLICK); + } + return true; + } + + // Called whenever the info needs to be refreshed + // That happens on CraftDetect, sign right click (new), Sign Translate + // The new and old values are gathered here and compared + // If nothing has changed, no update happens + // If something has changed, performUpdate() and sendUpdatePacket() are called + // Returns wether or not something has changed + protected boolean refreshSign(@Nullable Craft craft, SignListener.SignWrapper sign, boolean fillDefault, REFRESH_CAUSE refreshCause) { + boolean changedSome = false; + Component[] updatePayload = new Component[sign.lines().size()]; + for(int i = 1; i < sign.lines().size(); i++) { + Component oldComponent = sign.line(i); + Component potentiallyNew; + if (craft == null || fillDefault) { + potentiallyNew = this.getDefaultString(i, oldComponent); + } else { + potentiallyNew = this.getUpdateString(i, oldComponent, craft); + } + if (potentiallyNew != null && !potentiallyNew.equals(oldComponent)) { + String oldValue = PlainTextComponentSerializer.plainText().serialize(oldComponent); + String newValue = PlainTextComponentSerializer.plainText().serialize(potentiallyNew); + if (!oldValue.equals(newValue)) { + changedSome = true; + updatePayload[i] = potentiallyNew; + } + } + } + if (changedSome) { + this.performUpdate(updatePayload, sign, refreshCause); + } + return changedSome; + } + + @Override + public boolean processSignChange(SignChangeEvent event, SignListener.SignWrapper sign) { + if (this.refreshSign(null, sign, true, REFRESH_CAUSE.SIGN_CREATION)) { + this.sendUpdatePacket(null, sign, REFRESH_CAUSE.SIGN_CREATION); + } + return true; + } + + /* + Data to set on the sign. Return null if no update should happen! + Attention: A update will only be performed, if the new and old component are different! + */ + @Nullable + protected abstract Component getUpdateString(int lineIndex, Component oldData, Craft craft); + + // Returns the default value for this info sign per line + // Used on CraftDetect and on sign change + @Nullable + protected abstract Component getDefaultString(int lineIndex, Component oldComponent); + + /* + * @param newComponents: Array of nullable values. The index represents the index on the sign. Only contains the updated components + * + * Only gets called if at least one line has changed + */ + protected abstract void performUpdate(Component[] newComponents, SignListener.SignWrapper sign, REFRESH_CAUSE refreshCause); + + /* + Gets called after performUpdate has been called + */ + protected void sendUpdatePacket(Craft craft, SignListener.SignWrapper sign, REFRESH_CAUSE refreshCause) { + if (sign.block() == null) { + return; + } + for (Player player : sign.block().getLocation().getNearbyPlayers(16)) { + player.sendSignChange(sign.block().getLocation(), sign.lines()); + } + } +} diff --git a/api/src/main/java/net/countercraft/movecraft/sign/AbstractMovecraftSign.java b/api/src/main/java/net/countercraft/movecraft/sign/AbstractMovecraftSign.java new file mode 100644 index 000000000..d643fa7e8 --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/sign/AbstractMovecraftSign.java @@ -0,0 +1,180 @@ +package net.countercraft.movecraft.sign; + +import net.countercraft.movecraft.craft.Craft; +import net.countercraft.movecraft.craft.type.CraftType; +import net.countercraft.movecraft.util.MathUtils; +import net.kyori.adventure.text.Component; +import org.bukkit.entity.Player; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.SignChangeEvent; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.function.Function; + +// DONE: In 1.21 signs can have multiple sides! This requires us to pass the clicked side through or well the relevant lines and the set method for the clicked side => Resolved using the SignWrapper +/* + * Base class for all signs + * + * A instance of a sign needs to be registered using the register function. + * Signs react to the following events: + * - SignChangeEvent + * - PlayerInteractEvent, if the clicked block is a sign + * - CraftDetectEvent + * - SignTranslateEvent (if the sign is a subclass of AbstractCraftSign) + * + * Whenenver one of those events are cought by the AbstractSignListener instance, it is attempted to retrieve the relevant AbstractMovecraftSign instance. + * For that, the first line of the sign's clicked side is extracted and formatting removed. If it matches the format "foo: bar", only "foo:" will be used. + * With that ident, the sign is attempted to be retrieved vy tryGet(). If that returns something, the object's relevant method is called. + */ +public abstract class AbstractMovecraftSign { + + private static final Map SIGNS = Collections.synchronizedMap(new HashMap<>()); + + public static boolean hasBeenRegistered(final String ident) { + return SIGNS.containsKey(ident); + } + + // Special case for pilot signs, they are registered via the crafttypes name + // Use the methods in MovecraftSignRegistry instead + @Deprecated(forRemoval = true) + public static void registerCraftPilotSigns(Set loadedTypes, Function signFactory) { + MovecraftSignRegistry.INSTANCE.registerCraftPilotSigns(loadedTypes, signFactory); + } + + // Use the methods in MovecraftSignRegistry instead + @Deprecated(forRemoval = true) + public static @Nullable AbstractMovecraftSign get(final Component ident) { + return MovecraftSignRegistry.INSTANCE.get(ident); + } + + // Attempts to find a AbstractMovecraftSign instance, if something has been registered + // If the ident follows the format "foo: bar", only "foo:" is used as ident to search for + // Use the methods in MovecraftSignRegistry instead + @Deprecated(forRemoval = true) + public static @Nullable AbstractMovecraftSign get(final String ident) { + return MovecraftSignRegistry.INSTANCE.get(ident); + } + + // Registers a sign in all cases + // Use the methods in MovecraftSignRegistry instead + @Deprecated(forRemoval = true) + public static void register(final String ident, final @Nonnull AbstractMovecraftSign instance) { + MovecraftSignRegistry.INSTANCE.register(ident, instance); + } + + // Registers a sign + // If @param overrideIfAlreadyRegistered is set to false, it won't be registered if something has elready been registered using that name + // Use the methods in MovecraftSignRegistry instead + @Deprecated(forRemoval = true) + public static void register(final String ident, final @Nonnull AbstractMovecraftSign instance, boolean override) { + MovecraftSignRegistry.INSTANCE.register(ident, instance, override); + } + + // Optional permission for this sign + // Note that this is only checked against in normal processSignClick by default + // When using the default constructor, the permission will not be set + @Nullable + protected final String permissionString; + + public AbstractMovecraftSign() { + this(null); + } + + public AbstractMovecraftSign(String permissionNode) { + this.permissionString = permissionNode; + } + + // Utility function to retrieve the ident of a a given sign instance + // DO NOT call this for unregistered instances! + // It is a good idea to cache the return value of this function cause otherwise a loop over all registered sign instances will be necessary + public static String findIdent(AbstractMovecraftSign instance) { + if (!SIGNS.containsValue(instance)) { + throw new IllegalArgumentException("MovecraftSign instance must be registered!"); + } + for (Map.Entry entry : SIGNS.entrySet()) { + if (entry.getValue() == instance) { + return entry.getKey(); + } + } + throw new IllegalStateException("Somehow didn't find a key for a value that is in the map!"); + } + + // Called whenever a player clicks the sign + // SignWrapper wraps the relevant clicked side of the sign and the sign block itself + // If true is returned, the event will be cancelled + public boolean processSignClick(Action clickType, SignListener.SignWrapper sign, Player player) { + if (!this.isSignValid(clickType, sign, player)) { + return false; + } + if (!this.canPlayerUseSign(clickType, sign, player)) { + return false; + } + + return internalProcessSign(clickType, sign, player, getCraft(sign)); + } + + // Validation method + // By default this checks if the player has the set permission + protected boolean canPlayerUseSign(Action clickType, SignListener.SignWrapper sign, Player player) { + if (this.permissionString == null || this.permissionString.isBlank()) { + return true; + } + return player.hasPermission(this.permissionString); + } + + // Helper method, simply calls the existing methods + @Nullable + protected Craft getCraft(SignListener.SignWrapper sign) { + return MathUtils.getCraftByPersistentBlockData(sign.block().getLocation()); + } + + public enum EventType { + SIGN_CREATION, + SIGN_EDIT, + SIGN_EDIT_ON_CRAFT(true), + SIGN_CLICK, + SIGN_CLICK_ON_CRAFT(true); + + private boolean onCraft; + + EventType() { + this(false); + } + + EventType(boolean onCraft) { + this.onCraft = onCraft; + } + + public boolean isOnCraft() { + return this.onCraft; + } + } + + // Used by the event handler to determine if the event should be cancelled + // processingSuccessful is the output of processSignClick() or processSignChange() + // This is only called for the PlayerInteractEvent and the SignChangeEvent + public boolean shouldCancelEvent(boolean processingSuccessful, @Nullable Action type, boolean sneaking, EventType eventType) { + if (eventType.isOnCraft()) { + return true; + } + if (processingSuccessful && !sneaking) { + return eventType == EventType.SIGN_CLICK; + } + return false; + } + + // Validation method, called by default in processSignClick + // If false is returned, nothing will be processed + protected abstract boolean isSignValid(Action clickType, SignListener.SignWrapper sign, Player player); + + // Called by processSignClick after validation. At this point, isSignValid() and canPlayerUseSign() have been called already + // If the sign belongs to a craft, that craft is given in the @param craft argument + // Return true, if everything was ok + protected abstract boolean internalProcessSign(Action clickType, SignListener.SignWrapper sign, Player player, @Nullable Craft craft); + + // Called by the event handler when SignChangeEvent is being cought + // Return true, if everything was ok + public abstract boolean processSignChange(SignChangeEvent event, SignListener.SignWrapper sign); +} diff --git a/api/src/main/java/net/countercraft/movecraft/sign/AbstractSubcraftSign.java b/api/src/main/java/net/countercraft/movecraft/sign/AbstractSubcraftSign.java new file mode 100644 index 000000000..a2b846366 --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/sign/AbstractSubcraftSign.java @@ -0,0 +1,188 @@ +package net.countercraft.movecraft.sign; + +import net.countercraft.movecraft.MovecraftLocation; +import net.countercraft.movecraft.craft.Craft; +import net.countercraft.movecraft.craft.PlayerCraft; +import net.countercraft.movecraft.craft.type.CraftType; +import net.kyori.adventure.text.Component; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.SignChangeEvent; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +public abstract class AbstractSubcraftSign extends AbstractCraftSign { + + // TODO: Replace by writing to the signs nbt data + protected static final Set IN_USE = Collections.synchronizedSet(new HashSet<>()); + + protected final Function craftTypeRetrievalFunction; + + protected final Supplier pluginInstance; + + public AbstractSubcraftSign(Function craftTypeRetrievalFunction, final Supplier plugin) { + this(null, craftTypeRetrievalFunction, plugin); + } + + public AbstractSubcraftSign(final String permission, Function craftTypeRetrievalFunction, final Supplier plugin) { + super(permission, false); + this.craftTypeRetrievalFunction = craftTypeRetrievalFunction; + this.pluginInstance = plugin; + } + + @Override + public boolean processSignClick(Action clickType, SignListener.SignWrapper sign, Player player) { + if (!this.isSignValid(clickType, sign, player)) { + return false; + } + if (!this.canPlayerUseSign(clickType, sign, player)) { + return false; + } + Craft craft = this.getCraft(sign); + + if (craft instanceof PlayerCraft pc) { + if (!pc.isNotProcessing() && !this.ignoreCraftIsBusy) { + this.onCraftIsBusy(player, craft); + return false; + } + } + + return internalProcessSign(clickType, sign, player, craft); + } + + @Override + protected boolean internalProcessSign(Action clickType, SignListener.SignWrapper sign, Player player, Craft craft) { + if (craft != null) { + // TODO: Add property to crafts that they can use subcrafts? + if (!this.canPlayerUseSignOn(player, craft)) { + return false; + } + } + return this.internalProcessSignWithCraft(clickType, sign, craft, player); + } + + @Override + public boolean processSignChange(SignChangeEvent event, SignListener.SignWrapper sign) { + if (!this.isSignValid(Action.PHYSICAL, sign, event.getPlayer())) { + for (int i = 0; i < sign.lines().size(); i++) { + sign.line(i, Component.empty()); + } + return false; + } + this.applyDefaultText(sign); + return true; + } + + @Override + protected boolean isSignValid(Action clickType, SignListener.SignWrapper sign, Player player) { + String[] headerSplit = sign.getRaw(0).split(" "); + if (headerSplit.length != 2) { + return false; + } + // TODO: Change to enums? + String action = headerSplit[headerSplit.length - 1].toUpperCase(); + if (!this.isActionAllowed(action)) { + return false; + } + return this.getCraftType(sign) != null; + } + + @Override + protected boolean canPlayerUseSign(Action clickType, SignListener.SignWrapper sign, Player player) { + if (!super.canPlayerUseSign(clickType, sign, player)) { + return false; + } + CraftType craftType = this.getCraftType(sign); + if (craftType != null) { + return player.hasPermission("movecraft." + craftType.getStringProperty(CraftType.NAME) + ".pilot") && this.canPlayerUseSignForCraftType(clickType, sign, player, craftType); + } + return false; + } + + @Override + protected boolean internalProcessSignWithCraft(Action clickType, SignListener.SignWrapper sign, @Nullable Craft craft, Player player) { + CraftType subcraftType = this.getCraftType(sign); + + final Location signLoc = sign.block().getLocation(); + final MovecraftLocation startPoint = new MovecraftLocation(signLoc.getBlockX(), signLoc.getBlockY(), signLoc.getBlockZ()); + + if (craft != null) { + craft.setProcessing(true); + // TODO: SOlve this more elegantly... + new BukkitRunnable() { + @Override + public void run() { + craft.setProcessing(false); + } + }.runTaskLater(this.pluginInstance.get(), (10)); + } + + if (!IN_USE.add(startPoint)) { + this.onActionAlreadyInProgress(player); + return true; + } + + this.applyDefaultText(sign); + + final World world = sign.block().getWorld(); + + this.runDetectTask(clickType, subcraftType, craft, world, player, startPoint); + + // TODO: Change this, it is ugly, should be done by the detect task itself + new BukkitRunnable() { + @Override + public void run() { + IN_USE.remove(startPoint); + } + }.runTaskLater(this.pluginInstance.get(), 4); + + return true; + } + + protected void applyDefaultText(SignListener.SignWrapper sign) { + if (sign.getRaw(2).isBlank() && sign.getRaw(3).isBlank()) { + Component l3 = this.getDefaultTextFor(2); + Component l4 = this.getDefaultTextFor(3); + if (l3 != null) { + sign.line(2, l3); + } + if (l4 != null) { + sign.line(3, l4); + } + } + } + + @Nullable + protected CraftType getCraftType(SignListener.SignWrapper wrapper) { + String ident = wrapper.getRaw(1); + if (ident.trim().isBlank()) { + return null; + } + return this.craftTypeRetrievalFunction.apply(ident); + } + + @Override + public boolean shouldCancelEvent(boolean processingSuccessful, @Nullable Action type, boolean sneaking, EventType eventType) { + boolean resultSuper = super.shouldCancelEvent(processingSuccessful, type, sneaking, eventType); + if (!resultSuper) { + return eventType == EventType.SIGN_CLICK_ON_CRAFT || eventType == EventType.SIGN_CLICK; + } + return resultSuper; + } + + protected abstract void runDetectTask(Action clickType, CraftType subcraftType, Craft parentCraft, World world, Player player, MovecraftLocation startPoint); + protected abstract boolean isActionAllowed(final String action); + protected abstract void onActionAlreadyInProgress(Player player); + protected abstract Component getDefaultTextFor(int line); + protected abstract boolean canPlayerUseSignForCraftType(Action clickType, SignListener.SignWrapper sign, Player player, CraftType subCraftType); + +} diff --git a/api/src/main/java/net/countercraft/movecraft/sign/AbstractToggleSign.java b/api/src/main/java/net/countercraft/movecraft/sign/AbstractToggleSign.java new file mode 100644 index 000000000..7a8702c72 --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/sign/AbstractToggleSign.java @@ -0,0 +1,141 @@ +package net.countercraft.movecraft.sign; + +import net.countercraft.movecraft.CruiseDirection; +import net.countercraft.movecraft.craft.Craft; +import net.countercraft.movecraft.craft.PilotedCraft; +import net.countercraft.movecraft.events.CraftDetectEvent; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.entity.Player; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.SignChangeEvent; +import org.jetbrains.annotations.Nullable; + +public abstract class AbstractToggleSign extends AbstractCraftSign { + + private final String suffixOn; + private final String suffixOff; + private final String ident; + private final Component headerOn; + private final Component headerOff; + + public AbstractToggleSign(boolean ignoreCraftIsBusy, final String ident, final String suffixOn, final String suffixOff) { + this(null, ignoreCraftIsBusy, ident, suffixOn, suffixOff); + } + + public AbstractToggleSign(final String permission, boolean ignoreCraftIsBusy, final String ident, final String suffixOn, final String suffixOff) { + super(permission, ignoreCraftIsBusy); + this.suffixOn = suffixOn; + this.suffixOff = suffixOff; + this.ident = ident; + + this.headerOn = this.buildHeaderOn(); + this.headerOff = this.buildHeaderOff(); + } + + // Checks if the header is empty, if yes, it quits early (unnecessary actually as if it was empty this would never be called) + // Afterwards the header is validated, if it's splitted variant doesn't have exactly 2 entries it is invalid + // Finally, the "state" (second part of the header) isn't matching suffixOn or suffixOff, it is invalid + @Override + protected boolean isSignValid(Action clickType, SignListener.SignWrapper sign, Player player) { + if (PlainTextComponentSerializer.plainText().serialize(sign.line(0)).isBlank()) { + return false; + } + String[] headerSplit = getSplitHeader(sign); + if (headerSplit.length != 2) { + return false; + } + String suffix = headerSplit[1].trim(); + return suffix.equalsIgnoreCase(this.suffixOff) || suffix.equalsIgnoreCase(this.suffixOn); + } + + // Returns the raw header, which should consist of the ident and either the suffixOn or suffixOff value + // Returns null if the header is blank + @Nullable + protected static String[] getSplitHeader(final SignListener.SignWrapper sign) { + String header = PlainTextComponentSerializer.plainText().serialize(sign.line(0)); + if (header.isBlank()) { + return null; + } + return header.split(":"); + } + + // If the suffix matches the suffixOn field it will returnt true + // calls getSplitHeader() to retrieve the raw header string + protected boolean isOnOrOff(SignListener.SignWrapper sign) { + String[] headerSplit = getSplitHeader(sign); + if (headerSplit == null || headerSplit.length != 2) { + return false; + } + String suffix = headerSplit[1].trim(); + return suffix.equalsIgnoreCase(this.suffixOn); + } + + protected abstract void onAfterToggle(Craft craft, SignListener.SignWrapper signWrapper, Player player, boolean toggledToOn); + protected abstract void onBeforeToggle(Craft craft, SignListener.SignWrapper signWrapper, Player player, boolean willBeOn); + + // Actual processing, determines wether the sign will switch to on or off + // If it will be on, the CruiseDirection is retrieved and then setCraftCruising() is called + // Otherwise, the craft will stop cruising + // Then the sign is updated and the block resetted + // Finally, the relevant hooks are called + // This always returns true + @Override + protected boolean internalProcessSignWithCraft(Action clickType, SignListener.SignWrapper sign, Craft craft, Player player) { + boolean isOn = this.isOnOrOff(sign); + boolean willBeOn = !isOn; + + this.onBeforeToggle(craft, sign, player, willBeOn); + + // Update sign + sign.line(0, buildHeader(willBeOn)); + sign.block().update(true); + craft.resetSigns(sign.block()); + + this.onAfterToggle(craft, sign, player, willBeOn); + + return true; + } + + // On sign placement, if the entered header is the same as our ident, it will append the off-suffix automatically + @Override + public boolean processSignChange(SignChangeEvent event, SignListener.SignWrapper sign) { + String header = sign.getRaw(0).trim(); + if (header.equalsIgnoreCase(this.ident)) { + sign.line(0, buildHeaderOff()); + } + return true; + } + + // On craft detection, we set all the headers to the "off" header + @Override + public void onCraftDetect(CraftDetectEvent event, SignListener.SignWrapper sign) { + Player p = null; + if (event.getCraft() instanceof PilotedCraft pc) { + p = pc.getPilot(); + } + + if (this.isSignValid(Action.PHYSICAL, sign, p)) { + sign.line(0, buildHeader(false)); + } else { + // TODO: Error? React in any way? + sign.line(0, buildHeader(false)); + } + } + + // Helper method to build the headline for on or off state + protected Component buildHeader(boolean on) { + return on ? this.headerOn : this.headerOff; + } + + protected Component buildHeaderOn() { + return Component.text(this.ident).append(this.ident.endsWith(":") ? Component.text(" ") : Component.text(": ")).append(Component.text(this.suffixOn, Style.style(TextColor.color(0, 255, 0)))); + } + + protected Component buildHeaderOff() { + return Component.text(this.ident).append(this.ident.endsWith(":") ? Component.text(" ") : Component.text(": ")).append(Component.text(this.suffixOff, Style.style(TextColor.color(255, 0, 0)))); + } + +} diff --git a/api/src/main/java/net/countercraft/movecraft/sign/MovecraftSignRegistry.java b/api/src/main/java/net/countercraft/movecraft/sign/MovecraftSignRegistry.java new file mode 100644 index 000000000..a068f94ba --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/sign/MovecraftSignRegistry.java @@ -0,0 +1,80 @@ +package net.countercraft.movecraft.sign; + +import net.countercraft.movecraft.craft.datatag.CraftDataTagRegistry; +import net.countercraft.movecraft.craft.type.CraftType; +import net.countercraft.movecraft.util.SimpleRegistry; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; +import java.util.function.Function; + +public class MovecraftSignRegistry extends SimpleRegistry { + + public static final @NotNull MovecraftSignRegistry INSTANCE = new MovecraftSignRegistry(); + + public void registerCraftPilotSigns(Set loadedTypes, Function signFactory) { + _register.entrySet().removeIf(entry -> entry.getValue() instanceof AbstractCraftPilotSign); + // Now, add all types... + for (CraftType type : loadedTypes) { + AbstractCraftPilotSign sign = signFactory.apply(type); + register(type.getStringProperty(CraftType.NAME), sign, true); + } + } + + public @NotNull AbstractMovecraftSign register(@NotNull String key, @NotNull AbstractMovecraftSign value, String... aliases) throws IllegalArgumentException { + return register(key, value, false, aliases); + } + + public @NotNull AbstractMovecraftSign register(@NotNull String key, @NotNull AbstractMovecraftSign value, boolean override, String... aliases) throws IllegalArgumentException { + AbstractMovecraftSign result = this.register(key, value, override); + for (String alias : aliases) { + this.register(alias, result, override); + } + return result; + } + + public @Nullable AbstractMovecraftSign get(Component key) { + if (key == null) { + return null; + } + final String identStr = PlainTextComponentSerializer.plainText().serialize(key); + return get(identStr); + } + + @Override + public @Nullable AbstractMovecraftSign get(@NotNull String key) { + String identToUse = key.toUpperCase(); + if (identToUse.contains(":")) { + identToUse = identToUse.split(":")[0]; + // Re-add the : cause things should be registered with : at the end + identToUse = identToUse + ":"; + } + return super.get(identToUse); + } + + // Helper method for the listener + public @Nullable AbstractCraftSign getCraftSign(final Component ident) { + if (ident == null) { + return null; + } + final String identStr = PlainTextComponentSerializer.plainText().serialize(ident); + return this.getCraftSign(identStr); + } + + public @Nullable AbstractCraftSign getCraftSign(final String ident) { + AbstractMovecraftSign tmp = this.get(ident); + if (tmp != null && tmp instanceof AbstractCraftSign acs) { + return acs; + } + return null; + } + + + @Override + public @NotNull AbstractMovecraftSign register(@NotNull String key, @NotNull AbstractMovecraftSign value, boolean override) throws IllegalArgumentException { + return super.register(key.toUpperCase(), value, override); + } +} diff --git a/api/src/main/java/net/countercraft/movecraft/sign/SignListener.java b/api/src/main/java/net/countercraft/movecraft/sign/SignListener.java new file mode 100644 index 000000000..76936a426 --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/sign/SignListener.java @@ -0,0 +1,497 @@ +package net.countercraft.movecraft.sign; + +import it.unimi.dsi.fastutil.Hash; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap; +import net.countercraft.movecraft.MovecraftLocation; +import net.countercraft.movecraft.craft.Craft; +import net.countercraft.movecraft.events.CraftDetectEvent; +import net.countercraft.movecraft.events.SignTranslateEvent; +import net.countercraft.movecraft.util.MathUtils; +import net.countercraft.movecraft.util.Tags; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.Tag; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.Sign; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Directional; +import org.bukkit.block.data.Rotatable; +import org.bukkit.block.sign.Side; +import org.bukkit.block.sign.SignSide; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.SignChangeEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Function; + +public class SignListener implements Listener { + + public static SignListener INSTANCE; + + public SignListener() { + INSTANCE = this; + } + + // Keep this, it is good to abstract away the sign + public record SignWrapper( + @Nullable Sign block, + Function getLine, + List lines, + BiConsumer setLine, + BlockFace facing + ) { + public Component line(int index) { + if (index >= lines.size() || index < 0) { + throw new IndexOutOfBoundsException(); + } + return getLine().apply(index); + } + + public void line(int index, Component component) { + setLine.accept(index, component); + } + + public String getRaw(int index) { + return PlainTextComponentSerializer.plainText().serialize(line(index)); + } + + public String[] rawLines() { + String[] result = new String[this.lines.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = this.getRaw(i); + } + return result; + } + + public boolean isEmpty() { + for(String s : this.rawLines()) { + if (s.trim().isEmpty() || s.trim().isBlank()) { + continue; + } + else { + return false; + } + } + return true; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (obj instanceof SignWrapper other) { + return areSignsEqual(other); + } + return false; + } + + public boolean areSignsEqual(SignWrapper other) { + return areSignsEqual(this, other); + } + + public static boolean areSignsEqualIgnoreFace(SignWrapper a, SignWrapper b) { + return areSignsEqual(a, b, true); + } + + public static boolean areSignsEqual(SignWrapper a, SignWrapper b) { + return areSignsEqual(a, b, false); + } + + public static boolean areSignsEqual(SignWrapper a, SignWrapper b, boolean ignoreFace) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + String[] aLines = a.rawLines(); + String[] bLines = b.rawLines(); + + if (aLines.length != bLines.length) { + return false; + } + + for (int i = 0; i < aLines.length; i++) { + String aLine = aLines[i].trim(); + String bLine = bLines[i].trim(); + + if (!aLine.equalsIgnoreCase(bLine)) { + return false; + } + } + + // Now check the facing too! + return ignoreFace || a.facing().equals(b.facing()); + } + + public static boolean areSignsEqual(SignWrapper[] a, SignWrapper[] b) { + if (a == null || b == null) { + return false; + } + if (a.length != b.length) { + return false; + } + + for (int i = 0; i < a.length; i++) { + SignWrapper aWrap = a[i]; + SignWrapper bWrap = b[i]; + if (!areSignsEqual(aWrap, bWrap)) { + return false; + } + } + return true; + } + + public void copyContent(SignWrapper other) { + this.copyContent(other::line, (i) -> i < other.lines().size()); + } + + public void copyContent(Function retrievalFunction, Function indexValidator) { + for (int i = 0; i < this.lines().size() && indexValidator.apply(i); i++) { + this.line(i, retrievalFunction.apply(i)); + } + } + + } + + public SignWrapper[] getSignWrappers(Sign sign) { + return getSignWrappers(sign, false); + }; + + protected SignWrapper getSignWrapper(Sign sign, PlayerInteractEvent interactEvent) { + return this.getSignWrapper(sign, interactEvent.getPlayer()); + } + + protected final SignWrapper createFromSide(final Sign sign, final Side side) { + SignSide signSide = sign.getSide(side); + return createFromSide(sign, signSide, side); + } + + protected final SignWrapper createFromSide(final Sign sign, final SignSide signSide, Side side) { + BlockData blockData = sign.getBlock().getBlockData(); + BlockFace face; + if (blockData instanceof Directional directional) { + face = directional.getFacing(); + } else if (blockData instanceof Rotatable rotatable) { + face = rotatable.getRotation(); + } + else { + face = BlockFace.SELF; + } + + if (side == Side.BACK) { + face = face.getOppositeFace(); + } + SignWrapper wrapper = new SignWrapper( + sign, + signSide::line, + signSide.lines(), + signSide::line, + face + ); + return wrapper; + } + + public SignWrapper[] getSignWrappers(Sign sign, boolean ignoreEmpty) { + List wrappers = new ArrayList<>(); + for (Side side : Side.values()) { + SignSide signSide = sign.getSide(side); + SignWrapper wrapper = this.createFromSide(sign, signSide, side); + wrappers.add(wrapper); + } + if (ignoreEmpty) + wrappers.removeIf(SignWrapper::isEmpty); + return wrappers.toArray(new SignWrapper[wrappers.size()]); + } + + protected SignWrapper getSignWrapper(Sign sign, SignChangeEvent signChangeEvent) { + @NotNull Side side = signChangeEvent.getSide(); + + BlockData blockData = sign.getBlock().getBlockData(); + BlockFace face; + if (blockData instanceof Directional directional) { + face = directional.getFacing(); + } else if (blockData instanceof Rotatable rotatable) { + face = rotatable.getRotation(); + } + else { + face = BlockFace.SELF; + } + + if (side == Side.BACK) { + face = face.getOppositeFace(); + } + SignWrapper wrapper = new SignWrapper( + sign, + signChangeEvent::line, + signChangeEvent.lines(), + signChangeEvent::line, + face + ); + return wrapper; + } + + protected SignWrapper getSignWrapper(Sign sign, Player player) { + @NotNull SignSide side = sign.getTargetSide(player); + return this.createFromSide(sign, side, sign.getInteractableSideFor(player)); + } + + public void processSignTranslation(final Craft craft, boolean checkEventIsUpdated) { + // Ignore facing value here and directly store the associated wrappers in the list + Object2ObjectMap> signs = new Object2ObjectOpenCustomHashMap<>(new Hash.Strategy() { + @Override + public int hashCode(SignWrapper strings) { + return Arrays.hashCode(strings.rawLines()); + } + + @Override + public boolean equals(SignWrapper a, SignWrapper b) { + return SignWrapper.areSignsEqualIgnoreFace(a, b); + } + }); + // Remember the locations for the event! + Map> wrapperToLocs = new HashMap<>(); + + for (MovecraftLocation location : craft.getHitBox()) { + Block block = location.toBukkit(craft.getWorld()).getBlock(); + if(!Tag.SIGNS.isTagged(block.getType())){ + continue; + } + BlockState state = block.getState(); + if (state instanceof Sign) { + Sign sign = (Sign) state; + SignWrapper[] wrappersAtLoc = this.getSignWrappers(sign, true); + if (wrappersAtLoc == null || wrappersAtLoc.length == 0) { + continue; + } + for (SignWrapper wrapper : wrappersAtLoc) { + List values = signs.computeIfAbsent(wrapper, (w) -> new ArrayList<>()); + values.add(wrapper); + wrapperToLocs.computeIfAbsent(wrapper, (w) -> new ArrayList<>()).add(location); + } + } + } + Set signsToUpdate = new HashSet<>(); + for(Map.Entry> entry : signs.entrySet()){ + final List components = new ArrayList<>(entry.getKey().lines()); + SignWrapper backingForEvent = new SignWrapper(null, components::get, components, components::set, entry.getKey().facing()); + // if(!event.isUpdated()){ + // continue; + // } + // TODO: This is implemented only to fix client caching + // ideally we wouldn't do the update and would instead fake it out to the player + + /*System.out.println("New lines: "); + for (String s : event.rawLines()) { + System.out.println(" - " + s); + } + System.out.println("Old lines: "); + for (String s : entry.getKey().rawLines()) { + System.out.println(" - " + s); + }*/ + AbstractCraftSign acs = MovecraftSignRegistry.INSTANCE.getCraftSign(backingForEvent.line(0)); + if (acs != null) { + if (acs.processSignTranslation(craft, backingForEvent, wrapperToLocs.get(entry.getKey()))) { + // Values get changed definitely, but perhaps it does not get applied to the sign after all? + for(SignWrapper wrapperTmp : entry.getValue()){ + if (!checkEventIsUpdated) { + wrapperTmp.copyContent(backingForEvent::line, (i) -> i < backingForEvent.lines().size()); + if (wrapperTmp.block() != null) { + signsToUpdate.add(wrapperTmp.block()); + } + } + } + } + } + + } + + for (Sign sign : signsToUpdate) { + sign.update(false, false); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) + public void onCraftDetect(CraftDetectEvent event) { + final World world = event.getCraft().getWorld(); + event.getCraft().getHitBox().forEach( + (mloc) -> { + Block block = mloc.toBukkit(world).getBlock(); + BlockState state = block.getState(); + if (state instanceof Sign sign) { + for (SignWrapper wrapper : this.getSignWrappers(sign)) { + // Would be one more readable line if using Optionals but Nullables were wanted + AbstractCraftSign acs = MovecraftSignRegistry.INSTANCE.getCraftSign(wrapper.line(0)); + if (acs != null) { + acs.onCraftDetect(event, wrapper); + } + } + } + } + ); + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) + public void onSignTranslate(SignTranslateEvent event) { + // Would be one more readable line if using Optionals but Nullables were wanted + AbstractCraftSign acs = MovecraftSignRegistry.INSTANCE.getCraftSign(event.line(0)); + if (acs != null) { + acs.onSignMovedByCraft(event); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) + public void onSignChange(SignChangeEvent event) { + Block block = event.getBlock(); + BlockState state = block.getState(); + if (state instanceof Sign sign) { + SignWrapper signWrapper = this.getSignWrapper(sign, event.getPlayer()); + SignWrapper wrapper = this.getSignWrapper(sign, event); + + boolean onCraft = MathUtils.getCraftByPersistentBlockData(sign.getLocation()) != null; + + AbstractMovecraftSign.EventType eventType = signWrapper.isEmpty() ? AbstractMovecraftSign.EventType.SIGN_CREATION : AbstractMovecraftSign.EventType.SIGN_EDIT; + if (eventType == AbstractMovecraftSign.EventType.SIGN_EDIT && onCraft) { + eventType = AbstractMovecraftSign.EventType.SIGN_EDIT_ON_CRAFT; + } + final AbstractMovecraftSign.EventType eventTypeTmp = eventType; + AbstractMovecraftSign ams = null; + + // If the side is empty, we should try a different side, like, the next side that is not empty and which has a signHandler + if (wrapper.isEmpty()) { + SignWrapper[] wrapps = this.getSignWrappers(sign, true); + if (wrapps == null || wrapps.length == 0) { + // Nothing found + if (onCraft) { + event.setCancelled(true); + } + return; + } else { + for (SignWrapper swTmp : wrapps) { + AbstractMovecraftSign amsTmp = MovecraftSignRegistry.INSTANCE.get(swTmp.line(0)); + if (amsTmp != null) { + wrapper = swTmp; + ams = amsTmp; + break; + } + } + } + } + + if (ams == null) { + ams = MovecraftSignRegistry.INSTANCE.get(wrapper.line(0)); + } + + // Would be one more readable line if using Optionals but Nullables were wanted + if (ams != null) { + if (!eventType.isOnCraft()) { + ItemStack heldItem = event.getPlayer().getActiveItem(); + // Allow the usage of the colors and wax for things => Cleaner solution: Use NMS to check if the item is an instanceof of SignApplicator... + if (Tags.SIGN_EDIT_MATERIALS.contains(heldItem.getType())) { + return; + } + + if (Tags.SIGN_BYPASS_RIGHT_CLICK.contains(heldItem.getType())) { + return; + } + } + + boolean success = ams.processSignChange(event, wrapper); + if (ams.shouldCancelEvent(success, null, event.getPlayer().isSneaking(), eventTypeTmp)) { + event.setCancelled(true); + } + } + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) + public void onSignClick(PlayerInteractEvent event) { + Block block = event.getClickedBlock(); + if (block == null) { + return; + } + BlockState state = block.getState(); + if (state instanceof Sign sign) { + SignWrapper wrapper = this.getSignWrapper(sign, event); + ItemStack heldItem = event.getItem(); + boolean onCraft = MathUtils.getCraftByPersistentBlockData(sign.getLocation()) != null; + + AbstractMovecraftSign ams = null; + + // If the side is empty, we should try a different side, like, the next side that is not empty and which has a signHandler + if (wrapper.isEmpty()) { + SignWrapper[] wrapps = this.getSignWrappers(sign, true); + if (wrapps == null || wrapps.length == 0) { + // Nothing found + if (onCraft) { + event.setCancelled(true); + } + return; + } else { + for (SignWrapper swTmp : wrapps) { + AbstractMovecraftSign amsTmp = MovecraftSignRegistry.INSTANCE.get(swTmp.line(0)); + if (amsTmp != null) { + wrapper = swTmp; + ams = amsTmp; + break; + } + } + } + } + + // Always cancel if on craft => Avoid clicking empty sides and entering edit mode + if (onCraft && wrapper.isEmpty()) { + event.setCancelled(true); + } + + boolean sneaking = event.getPlayer().isSneaking(); + // Allow editing and breaking signs with tools + if (heldItem != null && !onCraft) { + // Allow the usage of the colors and wax for things => Cleaner solution: Use NMS to check if the item is an instanceof of SignApplicator... + if (Tags.SIGN_EDIT_MATERIALS.contains(heldItem.getType()) && event.getAction().isRightClick()) { + return; + } + if (sneaking) { + if (Tags.SIGN_BYPASS_RIGHT_CLICK.contains(heldItem.getType()) && (event.getAction().isRightClick())) { + return; + } + + if (Tags.SIGN_BYPASS_LEFT_CLICK.contains(heldItem.getType()) && (event.getAction().isLeftClick())) { + return; + } + } + } + + if (ams == null) { + ams = MovecraftSignRegistry.INSTANCE.get(wrapper.line(0)); + } + + // Would be one more readable line if using Optionals but Nullables were wanted + if (ams != null) { + boolean success = ams.processSignClick(event.getAction(), wrapper, event.getPlayer()); + // Always cancel, regardless of the success + event.setCancelled(true); + if (ams.shouldCancelEvent(success, event.getAction(), sneaking, onCraft ? AbstractMovecraftSign.EventType.SIGN_CLICK_ON_CRAFT : AbstractMovecraftSign.EventType.SIGN_CLICK)) { + event.setCancelled(true); + } + } + } + } + +} diff --git a/api/src/main/java/net/countercraft/movecraft/util/SimpleRegistry.java b/api/src/main/java/net/countercraft/movecraft/util/SimpleRegistry.java new file mode 100644 index 000000000..04f2548c9 --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/util/SimpleRegistry.java @@ -0,0 +1,46 @@ +package net.countercraft.movecraft.util; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class SimpleRegistry { + + protected final @NotNull ConcurrentMap<@NotNull K, @NotNull T> _register; + + public SimpleRegistry(){ + _register = new ConcurrentHashMap<>(); + } + + public @NotNull T register(final @NotNull K key, final @NotNull T value) throws IllegalArgumentException { + return this.register(key, value, false); + } + + public @NotNull T register(final @NotNull K key, final @NotNull T value, boolean override) throws IllegalArgumentException { + T previous = _register.put(key, value); + if (previous != null && !override) { + _register.put(key, value); + throw new IllegalArgumentException(String.format("Key %s is already registered.", key)); + } + return value; + } + + public @Nullable T get(final @NotNull K key) { + return _register.getOrDefault(key, null); + } + + public boolean isRegistered(final @NotNull K key){ + return _register.containsKey(key); + } + + /** + * Get an iterable over all keys currently registered. + * @return An immutable iterable over the registry keys + */ + public @NotNull Iterable<@NotNull K> getAllKeys(){ + return _register.keySet().stream().toList(); + } + +} diff --git a/api/src/main/java/net/countercraft/movecraft/util/Tags.java b/api/src/main/java/net/countercraft/movecraft/util/Tags.java index b2d2dc5a5..b8a9f3ee5 100644 --- a/api/src/main/java/net/countercraft/movecraft/util/Tags.java +++ b/api/src/main/java/net/countercraft/movecraft/util/Tags.java @@ -21,6 +21,10 @@ public class Tags { public static final EnumSet BUCKETS = EnumSet.of(Material.LAVA_BUCKET, Material.WATER_BUCKET, Material.MILK_BUCKET, Material.COD_BUCKET, Material.PUFFERFISH_BUCKET, Material.SALMON_BUCKET, Material.TROPICAL_FISH_BUCKET); public static final EnumSet WALL_TORCHES = EnumSet.of(Material.WALL_TORCH, Material.SOUL_WALL_TORCH, Material.REDSTONE_WALL_TORCH); public static final EnumSet LANTERNS = EnumSet.of(Material.LANTERN, Material.SOUL_LANTERN); + // TODO: Move to tags + public static final EnumSet SIGN_BYPASS_RIGHT_CLICK = EnumSet.of(Material.FEATHER); + public static final EnumSet SIGN_BYPASS_LEFT_CLICK = EnumSet.copyOf(Tag.ITEMS_AXES.getValues()); + public static final EnumSet SIGN_EDIT_MATERIALS = EnumSet.copyOf(Tag.ITEMS_AXES.getValues()); static { FRAGILE_MATERIALS.add(Material.PISTON_HEAD); @@ -54,6 +58,27 @@ public class Tags { FALL_THROUGH_BLOCKS.add(Material.POTATO); FALL_THROUGH_BLOCKS.addAll(Tag.FENCES.getValues()); FALL_THROUGH_BLOCKS.addAll(FLUID); + + SIGN_EDIT_MATERIALS.add(Material.HONEYCOMB); + SIGN_EDIT_MATERIALS.add(Material.INK_SAC); + SIGN_EDIT_MATERIALS.add(Material.GLOW_INK_SAC); + + SIGN_EDIT_MATERIALS.add(Material.WHITE_DYE); + SIGN_EDIT_MATERIALS.add(Material.LIGHT_GRAY_DYE); + SIGN_EDIT_MATERIALS.add(Material.GRAY_DYE); + SIGN_EDIT_MATERIALS.add(Material.BLACK_DYE); + SIGN_EDIT_MATERIALS.add(Material.BROWN_DYE); + SIGN_EDIT_MATERIALS.add(Material.RED_DYE); + SIGN_EDIT_MATERIALS.add(Material.ORANGE_DYE); + SIGN_EDIT_MATERIALS.add(Material.YELLOW_DYE); + SIGN_EDIT_MATERIALS.add(Material.LIME_DYE); + SIGN_EDIT_MATERIALS.add(Material.GREEN_DYE); + SIGN_EDIT_MATERIALS.add(Material.CYAN_DYE); + SIGN_EDIT_MATERIALS.add(Material.LIGHT_BLUE_DYE); + SIGN_EDIT_MATERIALS.add(Material.BLUE_DYE); + SIGN_EDIT_MATERIALS.add(Material.PURPLE_DYE); + SIGN_EDIT_MATERIALS.add(Material.MAGENTA_DYE); + SIGN_EDIT_MATERIALS.add(Material.PINK_DYE); } @Nullable