diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index 02830966c..0ea0f15b4 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -3,6 +3,9 @@ package kamkeel.npcs.controllers; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.addon.DBCAddon; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import kamkeel.npcs.network.LargeAbstractPacket; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumSyncAction; import kamkeel.npcs.network.enums.EnumSyncType; @@ -10,10 +13,12 @@ import kamkeel.npcs.network.packets.data.LoginPacket; import kamkeel.npcs.network.packets.data.large.SyncEffectPacket; import kamkeel.npcs.network.packets.data.large.SyncPacket; import kamkeel.npcs.network.packets.request.party.PartyInfoPacket; +import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.client.Minecraft; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; +import net.minecraft.server.MinecraftServer; import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.controllers.CustomEffectController; import noppes.npcs.controllers.DialogController; @@ -35,91 +40,164 @@ import noppes.npcs.controllers.data.QuestCategory; import noppes.npcs.controllers.data.RecipeAnvil; import noppes.npcs.controllers.data.RecipeCarpentry; +import java.io.IOException; +import java.security.KeyPair; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.EnumSet; import java.util.HashMap; +import java.util.Map; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; public class SyncController { + public static boolean DEBUG_SYNC_LOGGING = false; + + private static void debug(String message, Object... args) { + if (DEBUG_SYNC_LOGGING) { + System.out.println("[SyncController] " + String.format(message, args)); + } + } + + private static final EnumSyncType[] LOGIN_SYNC_TYPES = new EnumSyncType[]{ + EnumSyncType.FACTION, + EnumSyncType.DIALOG_CATEGORY, + EnumSyncType.QUEST_CATEGORY, + EnumSyncType.WORKBENCH_RECIPES, + EnumSyncType.CARPENTRY_RECIPES, + EnumSyncType.ANVIL_RECIPES, + EnumSyncType.CUSTOM_EFFECTS, + EnumSyncType.MAGIC, + EnumSyncType.MAGIC_CYCLE + }; + + private static final EnumMap CACHE_ENTRIES = new EnumMap<>(EnumSyncType.class); + private static final ConcurrentHashMap PLAYER_SYNC_STATE = new ConcurrentHashMap<>(); + + static { + registerCache(EnumSyncType.FACTION, SyncController::factionsNBT); + registerCache(EnumSyncType.DIALOG_CATEGORY, SyncController::dialogCategoriesNBT); + registerCache(EnumSyncType.QUEST_CATEGORY, SyncController::questCategoriesNBT); + registerCache(EnumSyncType.WORKBENCH_RECIPES, SyncController::workbenchNBT); + registerCache(EnumSyncType.CARPENTRY_RECIPES, SyncController::carpentryNBT); + registerCache(EnumSyncType.ANVIL_RECIPES, SyncController::anvilNBT); + registerCache(EnumSyncType.CUSTOM_EFFECTS, SyncController::customEffectsNBT); + registerCache(EnumSyncType.MAGIC, SyncController::magicsNBT); + registerCache(EnumSyncType.MAGIC_CYCLE, SyncController::magicCyclesNBT); + } + public static void syncPlayer(EntityPlayerMP player) { + syncPlayer(player, true); + } - // Login Packet - PacketHandler.Instance.sendToPlayer(new LoginPacket(), player); + private static void syncPlayer(EntityPlayerMP player, boolean includePostPackets) { + PlayerSyncState state = PLAYER_SYNC_STATE.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); - // 1) Factions - PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.FACTION, - EnumSyncAction.RELOAD, - -1, - factionsNBT() - ), player); + for (EnumSyncType type : LOGIN_SYNC_TYPES) { + SyncCacheEntry entry = CACHE_ENTRIES.get(type); + if (entry == null) { + debug("Skipping %s for %s - no cache entry", type, player.getCommandSenderName()); + continue; + } - // 2) Dialog Categories - PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.DIALOG_CATEGORY, - EnumSyncAction.RELOAD, - -1, - dialogCategoriesNBT() - ), player); + int currentRevision = entry.getRevisionValue(); + int lastRevision = state.getRevision(type); + debug("Evaluating %s for %s - server=%d client=%d", type, player.getCommandSenderName(), currentRevision, lastRevision); + if (lastRevision == currentRevision) { + debug("Up to date: %s already has revision %d", player.getCommandSenderName(), currentRevision); + continue; + } - // 3) Quest Categories - PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.QUEST_CATEGORY, - EnumSyncAction.RELOAD, - -1, - questCategoriesNBT() - ), player); + CachedSyncPayload payload = entry.getPayload(type); + if (payload == null) { + debug("No payload available for %s during %s sync", type, player.getCommandSenderName()); + continue; + } - // 4) Workbench Recipes - PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.WORKBENCH_RECIPES, - EnumSyncAction.RELOAD, - -1, - workbenchNBT() - ), player); + if (lastRevision != payload.getRevision()) { + debug("Sending %s revision %d to %s", type, payload.getRevision(), player.getCommandSenderName()); + PacketHandler.Instance.sendToPlayer(new SyncPacket(type, payload), player); + state.updateRevision(type, payload.getRevision()); + } + } - // 5) Carpentry Recipes - PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.CARPENTRY_RECIPES, - EnumSyncAction.RELOAD, - -1, - carpentryNBT() - ), player); + if (includePostPackets) { + debug("Dispatching post login packets to %s", player.getCommandSenderName()); + sendPostLoginPackets(player); + } + } - PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.ANVIL_RECIPES, - EnumSyncAction.RELOAD, - -1, - anvilNBT() - ), player); + public static void beginLogin(EntityPlayerMP player) { + PLAYER_SYNC_STATE.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); + debug("Initiating login sync handshake with %s", player.getCommandSenderName()); + PacketHandler.Instance.sendToPlayer( + new LoginPacket(getServerCacheKey(), getServerRevisionSnapshot()), + player + ); + } - PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.CUSTOM_EFFECTS, - EnumSyncAction.RELOAD, - -1, - customEffectsNBT() - ), player); + public static void handleClientRevisionReport( + EntityPlayerMP player, + String serverKey, + Map clientRevisions + ) { + if (!getServerCacheKey().equals(serverKey)) { + debug("Server cache key mismatch for %s (client=%s server=%s) - forcing full sync", player.getCommandSenderName(), serverKey, getServerCacheKey()); + syncPlayer(player); + return; + } - PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.MAGIC, - EnumSyncAction.RELOAD, - -1, - magicsNBT() - ), player); + PlayerSyncState state = PLAYER_SYNC_STATE.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); + state.applyHandshake(clientRevisions); - PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.MAGIC_CYCLE, - EnumSyncAction.RELOAD, - -1, - magicCyclesNBT() - ), player); + debug("Received revision report from %s: %s", player.getCommandSenderName(), clientRevisions); + syncPlayer(player); + } + + private static void sendPostLoginPackets(EntityPlayerMP player) { + debug("Sending addon/player data packets to %s", player.getCommandSenderName()); DBCAddon.instance.syncPlayer(player); syncPlayerData(player, false); - - // Send party information to the player on login PartyInfoPacket.sendPartyData(player); } + private static EnumMap getServerRevisionSnapshot() { + EnumMap snapshot = new EnumMap<>(EnumSyncType.class); + for (EnumSyncType type : LOGIN_SYNC_TYPES) { + SyncCacheEntry entry = CACHE_ENTRIES.get(type); + if (entry != null) { + snapshot.put(type, entry.getRevisionValue()); + } + } + return snapshot; + } + + private static String getServerCacheKey() { + MinecraftServer server = MinecraftServer.getServer(); + if (server == null) { + return "unknown"; + } + + if (!server.isDedicatedServer()) { + return ""; + } + + String base = server.getFolderName(); + KeyPair keyPair = server.getKeyPair(); + if (keyPair != null && keyPair.getPublic() != null) { + base = base + ":" + Integer.toHexString(Arrays.hashCode(keyPair.getPublic().getEncoded())); + } + return base; + } + + public static int getCurrentRevision(EnumSyncType type) { + SyncCacheEntry entry = CACHE_ENTRIES.get(type); + return entry == null ? -1 : entry.getRevisionValue(); + } + public static NBTTagCompound workbenchNBT() { RecipeController controller = RecipeController.Instance; NBTTagList list = new NBTTagList(); @@ -239,6 +317,7 @@ public class SyncController { public static void syncPlayerData(EntityPlayerMP player, boolean update) { PlayerData data = PlayerData.get(player); if (data != null) { + debug("Syncing %s data for %s", update ? "incremental" : "full", player.getCommandSenderName()); if (update) { PacketHandler.Instance.sendToPlayer(new SyncPacket( EnumSyncType.PLAYERDATA, @@ -257,70 +336,81 @@ public class SyncController { } public static void syncRemove(EnumSyncType enumSyncType, int id) { + Map revisions = invalidateCaches(enumSyncType); + int revision = revisions.getOrDefault(enumSyncType, getCurrentRevision(enumSyncType)); + debug("Broadcasting %s removal for id=%d at revision=%d", enumSyncType, id, revision); PacketHandler.Instance.sendToAll(new SyncPacket( enumSyncType, EnumSyncAction.REMOVE, id, + revision, new NBTTagCompound() )); + updateAllPlayerRevisions(revisions); } public static void syncAllDialogs() { - PacketHandler.Instance.sendToAll(new SyncPacket( - EnumSyncType.DIALOG_CATEGORY, - EnumSyncAction.RELOAD, - -1, - dialogCategoriesNBT() - )); + CachedSyncPayload payload = rebuildNow(EnumSyncType.DIALOG_CATEGORY); + debug("Broadcasting full dialog category reload revision=%d", payload == null ? -1 : payload.getRevision()); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.DIALOG_CATEGORY, payload)); + updateAllPlayerRevisions(EnumSyncType.DIALOG_CATEGORY, payload.getRevision()); } public static void syncAllQuests() { - PacketHandler.Instance.sendToAll(new SyncPacket( - EnumSyncType.QUEST_CATEGORY, - EnumSyncAction.RELOAD, - -1, - questCategoriesNBT() - )); + CachedSyncPayload payload = rebuildNow(EnumSyncType.QUEST_CATEGORY); + debug("Broadcasting full quest category reload revision=%d", payload == null ? -1 : payload.getRevision()); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.QUEST_CATEGORY, payload)); + updateAllPlayerRevisions(EnumSyncType.QUEST_CATEGORY, payload.getRevision()); } public static void syncAllWorkbenchRecipes() { - PacketHandler.Instance.sendToAll(new SyncPacket( - EnumSyncType.WORKBENCH_RECIPES, - EnumSyncAction.RELOAD, - -1, - workbenchNBT() - )); + CachedSyncPayload payload = rebuildNow(EnumSyncType.WORKBENCH_RECIPES); + debug("Broadcasting full workbench recipe reload revision=%d", payload == null ? -1 : payload.getRevision()); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.WORKBENCH_RECIPES, payload)); + updateAllPlayerRevisions(EnumSyncType.WORKBENCH_RECIPES, payload.getRevision()); } public static void syncAllCarpentryRecipes() { - PacketHandler.Instance.sendToAll(new SyncPacket( - EnumSyncType.CARPENTRY_RECIPES, - EnumSyncAction.RELOAD, - -1, - carpentryNBT() - )); + CachedSyncPayload payload = rebuildNow(EnumSyncType.CARPENTRY_RECIPES); + debug("Broadcasting full carpentry recipe reload revision=%d", payload == null ? -1 : payload.getRevision()); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CARPENTRY_RECIPES, payload)); + updateAllPlayerRevisions(EnumSyncType.CARPENTRY_RECIPES, payload.getRevision()); } public static void syncAllAnvilRecipes() { - PacketHandler.Instance.sendToAll(new SyncPacket( - EnumSyncType.ANVIL_RECIPES, - EnumSyncAction.RELOAD, - -1, - anvilNBT() - )); + CachedSyncPayload payload = rebuildNow(EnumSyncType.ANVIL_RECIPES); + debug("Broadcasting full anvil recipe reload revision=%d", payload == null ? -1 : payload.getRevision()); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.ANVIL_RECIPES, payload)); + updateAllPlayerRevisions(EnumSyncType.ANVIL_RECIPES, payload.getRevision()); } public static void syncAllCustomEffects() { - PacketHandler.Instance.sendToAll(new SyncPacket( - EnumSyncType.CUSTOM_EFFECTS, - EnumSyncAction.RELOAD, - -1, - customEffectsNBT() - )); + CachedSyncPayload payload = rebuildNow(EnumSyncType.CUSTOM_EFFECTS); + debug("Broadcasting full custom effect reload revision=%d", payload == null ? -1 : payload.getRevision()); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CUSTOM_EFFECTS, payload)); + updateAllPlayerRevisions(EnumSyncType.CUSTOM_EFFECTS, payload.getRevision()); } @SideOnly(Side.CLIENT) - public static void clientSync(EnumSyncType enumSyncType, NBTTagCompound fullCompound) { + public static void clientSync(EnumSyncType enumSyncType, int revision, NBTTagCompound fullCompound) { switch (enumSyncType) { case FACTION: { NBTTagList list = fullCompound.getTagList("Factions", 10); @@ -505,13 +595,15 @@ public class SyncController { break; } } + + ClientCacheHandler.updateClientRevision(enumSyncType, revision); } /** * Update Related Tasks */ @SideOnly(Side.CLIENT) - public static void clientUpdate(EnumSyncType enumSyncType, int category_id, NBTTagCompound compound) { + public static void clientUpdate(EnumSyncType enumSyncType, int category_id, int revision, NBTTagCompound compound) { switch (enumSyncType) { case FACTION: { Faction faction = new Faction(); @@ -596,15 +688,21 @@ public class SyncController { break; } } + + ClientCacheHandler.updateClientRevision(enumSyncType, revision); } public static void syncUpdate(EnumSyncType type, int cat, NBTTagCompound compound) { + Map revisions = invalidateCaches(type); + int revision = revisions.getOrDefault(type, getCurrentRevision(type)); PacketHandler.Instance.sendToAll(new SyncPacket( type, EnumSyncAction.UPDATE, cat, + revision, compound )); + updateAllPlayerRevisions(revisions); } @@ -633,7 +731,7 @@ public class SyncController { } @SideOnly(Side.CLIENT) - public static void clientSyncRemove(EnumSyncType enumSyncType, int id) { + public static void clientSyncRemove(EnumSyncType enumSyncType, int id, int revision) { switch (enumSyncType) { case FACTION: FactionController.getInstance().factions.remove(id); @@ -675,6 +773,8 @@ public class SyncController { CustomEffectController.Instance.getCustomEffects().remove(id); break; } + + ClientCacheHandler.updateClientRevision(enumSyncType, revision); } public static void syncEffects(EntityPlayerMP playerMP) { @@ -683,6 +783,7 @@ public class SyncController { playerData.effectData.setEffects(playerEffects); NBTTagCompound compound = playerData.getPlayerEffects(); + debug("Sending active effect sync to %s", playerMP.getCommandSenderName()); PacketHandler.Instance.sendToPlayer(new SyncEffectPacket(compound), playerMP); } @@ -693,4 +794,205 @@ public class SyncController { playerData.setPlayerEffects(compound); } } + + private static void registerCache(EnumSyncType type, Supplier supplier) { + CACHE_ENTRIES.put(type, new SyncCacheEntry(type, supplier)); + debug("Registered cache entry for %s", type); + } + + private static CachedSyncPayload rebuildNow(EnumSyncType type) { + SyncCacheEntry entry = CACHE_ENTRIES.get(type); + if (entry == null) { + debug("Cannot rebuild cache for %s - no entry registered", type); + return null; + } + debug("Rebuilding cache immediately for %s", type); + entry.invalidate(); + return entry.getPayload(type); + } + + public static Map invalidateCaches(EnumSyncType type) { + EnumSet targets = getInvalidationTargets(type); + EnumMap revisions = new EnumMap<>(EnumSyncType.class); + for (EnumSyncType target : targets) { + SyncCacheEntry entry = CACHE_ENTRIES.get(target); + if (entry != null) { + int newRevision = entry.invalidate(); + debug("Invalidated cache for %s due to %s mutation - next revision=%d", target, type, newRevision); + revisions.put(target, newRevision); + } + } + return revisions; + } + + private static EnumSet getInvalidationTargets(EnumSyncType type) { + switch (type) { + case DIALOG: + case DIALOG_CATEGORY: + return EnumSet.of(EnumSyncType.DIALOG_CATEGORY); + case QUEST: + case QUEST_CATEGORY: + return EnumSet.of(EnumSyncType.QUEST_CATEGORY); + case FACTION: + return EnumSet.of(EnumSyncType.FACTION); + case MAGIC: + return EnumSet.of(EnumSyncType.MAGIC); + case MAGIC_CYCLE: + return EnumSet.of(EnumSyncType.MAGIC_CYCLE); + case WORKBENCH_RECIPES: + return EnumSet.of(EnumSyncType.WORKBENCH_RECIPES); + case CARPENTRY_RECIPES: + return EnumSet.of(EnumSyncType.CARPENTRY_RECIPES); + case ANVIL_RECIPES: + return EnumSet.of(EnumSyncType.ANVIL_RECIPES); + case CUSTOM_EFFECTS: + return EnumSet.of(EnumSyncType.CUSTOM_EFFECTS); + default: + return EnumSet.noneOf(EnumSyncType.class); + } + } + + private static void updateAllPlayerRevisions(EnumSyncType type, int revision) { + if (revision < 0) { + return; + } + debug("Updating cached revision for all players: %s -> %d", type, revision); + for (PlayerSyncState state : PLAYER_SYNC_STATE.values()) { + state.updateRevision(type, revision); + } + } + + private static void updateAllPlayerRevisions(Map revisions) { + if (revisions.isEmpty()) { + return; + } + debug("Updating cached revisions for all players: %s", revisions); + for (PlayerSyncState state : PLAYER_SYNC_STATE.values()) { + for (Map.Entry entry : revisions.entrySet()) { + state.updateRevision(entry.getKey(), entry.getValue()); + } + } + } + + public static final class CachedSyncPayload { + private final int revision; + private final byte[] payload; + private final byte[][] chunks; + + private CachedSyncPayload(int revision, byte[] payload, byte[][] chunks) { + this.revision = revision; + this.payload = payload; + this.chunks = chunks; + } + + public int getRevision() { + return revision; + } + + public byte[] getPayload() { + return payload; + } + + public byte[][] getChunks() { + return chunks; + } + } + + private static final class SyncCacheEntry { + private final Supplier supplier; + private volatile CachedSyncPayload payload; + private int revision = 0; + private boolean dirty = true; + + private SyncCacheEntry(EnumSyncType type, Supplier supplier) { + this.supplier = supplier; + } + + private synchronized CachedSyncPayload getPayload(EnumSyncType requestedType) { + if (!dirty && payload != null) { + debug("Reusing cached payload for %s revision %d", requestedType, payload.getRevision()); + return payload; + } + + int payloadRevision = revision; + NBTTagCompound data = supplier.get(); + debug("Building new payload for %s revision %d", requestedType, payloadRevision); + ByteBuf buffer = Unpooled.buffer(); + byte[] bytes; + try { + buffer.writeInt(requestedType.ordinal()); + buffer.writeInt(EnumSyncAction.RELOAD.ordinal()); + buffer.writeInt(-1); + buffer.writeInt(payloadRevision); + ByteBufUtils.writeBigNBT(buffer, data); + + bytes = new byte[buffer.readableBytes()]; + buffer.readBytes(bytes); + } catch (IOException e) { + throw new RuntimeException("Failed to serialize sync payload for " + requestedType, e); + } finally { + buffer.release(); + } + + byte[][] chunks = splitIntoChunks(bytes); + payload = new CachedSyncPayload(payloadRevision, bytes, chunks); + dirty = false; + debug("Cached payload for %s revision %d with %d chunks", requestedType, payloadRevision, chunks.length); + return payload; + } + + private synchronized int invalidate() { + dirty = true; + payload = null; + revision++; + debug("Marked cache dirty for revision %d", revision); + return revision; + } + + private synchronized int getRevisionValue() { + return revision; + } + + private static byte[][] splitIntoChunks(byte[] payload) { + int totalSize = payload.length; + int chunkCount = (totalSize + LargeAbstractPacket.CHUNK_SIZE - 1) / LargeAbstractPacket.CHUNK_SIZE; + if (chunkCount <= 0) { + chunkCount = 1; + } + debug("Splitting payload of %d bytes into %d chunks", totalSize, chunkCount); + byte[][] chunks = new byte[chunkCount][]; + for (int i = 0; i < chunkCount; i++) { + int offset = i * LargeAbstractPacket.CHUNK_SIZE; + int length = Math.min(LargeAbstractPacket.CHUNK_SIZE, totalSize - offset); + byte[] chunk = new byte[length]; + System.arraycopy(payload, offset, chunk, 0, length); + chunks[i] = chunk; + } + return chunks; + } + } + + private static final class PlayerSyncState { + private final EnumMap revisions = new EnumMap<>(EnumSyncType.class); + + private PlayerSyncState(UUID ignored) { + } + + private synchronized int getRevision(EnumSyncType type) { + Integer value = revisions.get(type); + return value == null ? -1 : value; + } + + private synchronized void updateRevision(EnumSyncType type, int revision) { + revisions.put(type, revision); + } + + private synchronized void applyHandshake(Map incoming) { + if (incoming == null || incoming.isEmpty()) { + return; + } + revisions.putAll(incoming); + } + + } } diff --git a/src/main/java/kamkeel/npcs/network/LargeAbstractPacket.java b/src/main/java/kamkeel/npcs/network/LargeAbstractPacket.java index 5c1e9dcc4..1cb8279cd 100644 --- a/src/main/java/kamkeel/npcs/network/LargeAbstractPacket.java +++ b/src/main/java/kamkeel/npcs/network/LargeAbstractPacket.java @@ -16,7 +16,7 @@ import java.util.concurrent.ConcurrentHashMap; public abstract class LargeAbstractPacket extends AbstractPacket { private static final Map packetChunks = new ConcurrentHashMap<>(); - private static final int CHUNK_SIZE = 10000; + public static final int CHUNK_SIZE = 10000; /** * Create multiple FMLProxyPacket objects, each containing up to CHUNK_SIZE bytes. diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 67164a93a..4dcf50bac 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -71,6 +71,7 @@ import kamkeel.npcs.network.packets.player.item.MagicCyclesPacket; import kamkeel.npcs.network.packets.player.profile.ProfileChangePacket; import kamkeel.npcs.network.packets.player.profile.ProfileCreatePacket; import kamkeel.npcs.network.packets.player.profile.ProfileGetInfoPacket; +import kamkeel.npcs.network.packets.player.SyncRevisionInfoPacket; import kamkeel.npcs.network.packets.player.profile.ProfileGetPacket; import kamkeel.npcs.network.packets.player.profile.ProfileRemovePacket; import kamkeel.npcs.network.packets.player.profile.ProfileRenamePacket; @@ -575,6 +576,8 @@ public class PacketHandler { PLAYER_PACKET.registerPacket(new ScreenSizePacket()); + PLAYER_PACKET.registerPacket(new SyncRevisionInfoPacket()); + // CustomGUI Packets PLAYER_PACKET.registerPacket(new CustomGuiButtonPacket()); PLAYER_PACKET.registerPacket(new CustomGuiClosePacket()); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java index c5c912c79..04d3d3396 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java @@ -28,5 +28,8 @@ public enum EnumPlayerPacket { ProfileCreate, ProfileRemove, ProfileRename, - ProfileChange, ProfileGet, ProfileGetInfo, + ProfileChange, + ProfileGet, + ProfileGetInfo, + SyncRevisionInfo, } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java index a30d4266c..48e682735 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java @@ -5,20 +5,38 @@ import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumDataPacket; +import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.packets.player.SyncRevisionInfoPacket; +import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.config.ConfigMain; import java.io.IOException; +import java.util.EnumMap; +import java.util.Map; public final class LoginPacket extends AbstractPacket { public static final String packetName = "CNPC+|Login"; + private String serverCacheKey = ""; + private final EnumMap revisionSnapshot = new EnumMap<>(EnumSyncType.class); + public LoginPacket() { } + public LoginPacket(String serverCacheKey, EnumMap revisionSnapshot) { + if (serverCacheKey != null) { + this.serverCacheKey = serverCacheKey; + } + if (revisionSnapshot != null) { + this.revisionSnapshot.putAll(revisionSnapshot); + } + } + @Override public Enum getType() { return EnumDataPacket.LOGIN; @@ -33,6 +51,12 @@ public final class LoginPacket extends AbstractPacket { public void sendData(ByteBuf out) throws IOException { out.writeBoolean(ConfigMain.PartiesEnabled); out.writeBoolean(ConfigMain.ProfilesEnabled); + ByteBufUtils.writeString(out, serverCacheKey); + out.writeShort(revisionSnapshot.size()); + for (Map.Entry entry : revisionSnapshot.entrySet()) { + out.writeInt(entry.getKey().ordinal()); + out.writeInt(entry.getValue()); + } } @SideOnly(Side.CLIENT) @@ -40,5 +64,21 @@ public final class LoginPacket extends AbstractPacket { public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { ClientCacheHandler.allowParties = in.readBoolean(); ClientCacheHandler.allowProfiles = in.readBoolean(); + + String serverKey = ByteBufUtils.readString(in); + int revisionCount = in.readUnsignedShort(); + EnumMap serverRevisions = new EnumMap<>(EnumSyncType.class); + for (int i = 0; i < revisionCount; i++) { + EnumSyncType type = EnumSyncType.values()[in.readInt()]; + int revision = in.readInt(); + serverRevisions.put(type, revision); + } + + ClientCacheHandler.setActiveServer(serverKey, serverRevisions); + + PacketClient.sendClient(new SyncRevisionInfoPacket( + serverKey, + ClientCacheHandler.getCachedRevisionsForServer(serverKey) + )); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/large/SyncPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/large/SyncPacket.java index 2b4ca27b9..77b2122b4 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/large/SyncPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/large/SyncPacket.java @@ -1,5 +1,6 @@ package kamkeel.npcs.network.packets.data.large; +import cpw.mods.fml.common.network.internal.FMLProxyPacket; import cpw.mods.fml.relauncher.Side; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -17,6 +18,9 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; /** * A large sync packet that sends chunked data to the client for a given SyncType @@ -28,6 +32,9 @@ public final class SyncPacket extends LargeAbstractPacket { private EnumSyncAction enumSyncAction; private NBTTagCompound syncData; private int operationID; + private int revision = -1; + private byte[] cachedPayload; + private byte[][] cachedChunks; public SyncPacket() { } @@ -36,10 +43,24 @@ public final class SyncPacket extends LargeAbstractPacket { * Constructs a new LargeSyncPacket. */ public SyncPacket(EnumSyncType enumSyncType, EnumSyncAction enumSyncAction, int catId, NBTTagCompound syncData) { + this(enumSyncType, enumSyncAction, catId, -1, syncData); + } + + public SyncPacket(EnumSyncType enumSyncType, EnumSyncAction enumSyncAction, int catId, int revision, NBTTagCompound syncData) { this.enumSyncType = enumSyncType; this.enumSyncAction = enumSyncAction; this.syncData = syncData; this.operationID = catId; + this.revision = revision; + } + + public SyncPacket(EnumSyncType enumSyncType, SyncController.CachedSyncPayload payload) { + this.enumSyncType = enumSyncType; + this.enumSyncAction = EnumSyncAction.RELOAD; + this.operationID = -1; + this.revision = payload.getRevision(); + this.cachedPayload = payload.getPayload(); + this.cachedChunks = payload.getChunks(); } @Override @@ -54,20 +75,55 @@ public final class SyncPacket extends LargeAbstractPacket { @Override protected byte[] getData() throws IOException { - ByteBuf buffer = Unpooled.buffer(); - // 1) Write SyncType - buffer.writeInt(enumSyncType.ordinal()); - // 2) Write SyncAction - buffer.writeInt(enumSyncAction.ordinal()); - // 3) Optional Category ID - buffer.writeInt(operationID); - // 4) Write the NBTTagCompound - ByteBufUtils.writeBigNBT(buffer, syncData); + if (cachedPayload != null) { + return cachedPayload; + } - // Copy the buffer’s readable bytes into a byte[] - byte[] bytes = new byte[buffer.readableBytes()]; - buffer.readBytes(bytes); - return bytes; + ByteBuf buffer = Unpooled.buffer(); + try { + buffer.writeInt(enumSyncType.ordinal()); + buffer.writeInt(enumSyncAction.ordinal()); + buffer.writeInt(operationID); + buffer.writeInt(revision); + ByteBufUtils.writeBigNBT(buffer, syncData); + + byte[] bytes = new byte[buffer.readableBytes()]; + buffer.readBytes(bytes); + return bytes; + } finally { + buffer.release(); + } + } + + @Override + public List generatePackets() { + if (cachedChunks == null) { + return super.generatePackets(); + } + + List packets = new ArrayList<>(cachedChunks.length); + PacketChannel packetChannel = getChannel(); + EnumDataPacket dataPacket = (EnumDataPacket) getType(); + UUID packetId = UUID.randomUUID(); + + int totalSize = cachedPayload.length; + int offset = 0; + + for (byte[] chunk : cachedChunks) { + ByteBuf chunkBuf = Unpooled.buffer(); + chunkBuf.writeInt(packetChannel.getChannelType().ordinal()); + chunkBuf.writeInt(dataPacket.ordinal()); + chunkBuf.writeLong(packetId.getMostSignificantBits()); + chunkBuf.writeLong(packetId.getLeastSignificantBits()); + chunkBuf.writeInt(totalSize); + chunkBuf.writeInt(offset); + chunkBuf.writeInt(cachedChunks.length); + chunkBuf.writeBytes(chunk); + packets.add(new FMLProxyPacket(chunkBuf, packetChannel.getChannelName())); + offset += chunk.length; + } + + return packets; } @Override @@ -79,28 +135,35 @@ public final class SyncPacket extends LargeAbstractPacket { int syncTypeOrdinal = data.readInt(); int syncActionOrdinal = data.readInt(); int categoryID = data.readInt(); + int incomingRevision = data.readInt(); EnumSyncType type = EnumSyncType.values()[syncTypeOrdinal]; EnumSyncAction action = EnumSyncAction.values()[syncActionOrdinal]; try { NBTTagCompound tag = ByteBufUtils.readBigNBT(data); // Now do your client-side logic (similar to your old clientSync() or clientSyncUpdate() approach) - handleSyncPacketClient(type, action, categoryID, tag); + handleSyncPacketClient(type, action, categoryID, incomingRevision, tag); } catch (RuntimeException e) { LogWriter.error(String.format("Attempted to Sync %s but it was too big", type.toString())); } } - private void handleSyncPacketClient(EnumSyncType enumSyncType, EnumSyncAction enumSyncAction, int id, NBTTagCompound data) { + private void handleSyncPacketClient( + EnumSyncType enumSyncType, + EnumSyncAction enumSyncAction, + int id, + int incomingRevision, + NBTTagCompound data + ) { switch (enumSyncAction) { case RELOAD: - SyncController.clientSync(enumSyncType, data); + SyncController.clientSync(enumSyncType, incomingRevision, data); break; case UPDATE: - SyncController.clientUpdate(enumSyncType, id, data); + SyncController.clientUpdate(enumSyncType, id, incomingRevision, data); break; case REMOVE: - SyncController.clientSyncRemove(enumSyncType, id); + SyncController.clientSyncRemove(enumSyncType, id, incomingRevision); break; } } diff --git a/src/main/java/kamkeel/npcs/network/packets/player/SyncRevisionInfoPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/SyncRevisionInfoPacket.java new file mode 100644 index 000000000..7d64f2cf4 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/player/SyncRevisionInfoPacket.java @@ -0,0 +1,77 @@ +package kamkeel.npcs.network.packets.player; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumPlayerPacket; +import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; + +import java.io.IOException; +import java.util.EnumMap; +import java.util.Map; + +public class SyncRevisionInfoPacket extends AbstractPacket { + + public static final String packetName = "Player|SyncRevisionInfo"; + + private String serverKey = ""; + private final EnumMap revisions = new EnumMap<>(EnumSyncType.class); + + public SyncRevisionInfoPacket() { + } + + public SyncRevisionInfoPacket(String serverKey, EnumMap revisions) { + if (serverKey != null) { + this.serverKey = serverKey; + } + if (revisions != null) { + this.revisions.putAll(revisions); + } + } + + @Override + public Enum getType() { + return EnumPlayerPacket.SyncRevisionInfo; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.PLAYER_PACKET; + } + + @Override + @SideOnly(Side.CLIENT) + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, serverKey); + out.writeShort(revisions.size()); + for (Map.Entry entry : revisions.entrySet()) { + out.writeInt(entry.getKey().ordinal()); + out.writeInt(entry.getValue()); + } + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) { + return; + } + + String incomingServerKey = ByteBufUtils.readString(in); + int revisionCount = in.readUnsignedShort(); + EnumMap incomingRevisions = new EnumMap<>(EnumSyncType.class); + for (int i = 0; i < revisionCount; i++) { + EnumSyncType type = EnumSyncType.values()[in.readInt()]; + int revision = in.readInt(); + incomingRevisions.put(type, revision); + } + + SyncController.handleClientRevisionReport((EntityPlayerMP) player, incomingServerKey, incomingRevisions); + } +} diff --git a/src/main/java/noppes/npcs/ServerEventsHandler.java b/src/main/java/noppes/npcs/ServerEventsHandler.java index acc72a56c..a41a828e6 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -209,6 +209,7 @@ public class ServerEventsHandler { EnumSyncType.WORKBENCH_RECIPES, EnumSyncAction.RELOAD, -1, + SyncController.getCurrentRevision(EnumSyncType.WORKBENCH_RECIPES), SyncController.workbenchNBT() ), (EntityPlayerMP) player); } @@ -217,6 +218,7 @@ public class ServerEventsHandler { EnumSyncType.CARPENTRY_RECIPES, EnumSyncAction.RELOAD, -1, + SyncController.getCurrentRevision(EnumSyncType.CARPENTRY_RECIPES), SyncController.carpentryNBT() ), (EntityPlayerMP) player); } diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index ef2ba0847..62f53399b 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -62,7 +62,7 @@ public class ServerTickHandler { } ProfileController.Instance.login(player); - SyncController.syncPlayer(player); + SyncController.beginLogin(player); SyncController.syncEffects(player); } diff --git a/src/main/java/noppes/npcs/client/ClientCacheHandler.java b/src/main/java/noppes/npcs/client/ClientCacheHandler.java index 298bedc06..b90e24e69 100644 --- a/src/main/java/noppes/npcs/client/ClientCacheHandler.java +++ b/src/main/java/noppes/npcs/client/ClientCacheHandler.java @@ -15,9 +15,12 @@ import noppes.npcs.controllers.data.AnimationData; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.controllers.data.SkinOverlay; +import kamkeel.npcs.network.enums.EnumSyncType; import noppes.npcs.util.CacheHashMap; +import java.util.EnumMap; import java.util.HashMap; +import java.util.Map; import java.util.UUID; public class ClientCacheHandler { @@ -27,11 +30,61 @@ public class ClientCacheHandler { public static HashMap> skinOverlays = new HashMap<>(); public static HashMap playerAnimations = new HashMap<>(); + private static String activeServerKey = ""; + private static final Map> clientRevisionCache = new HashMap<>(); + public static Party party; public static boolean allowProfiles = true; public static boolean allowParties = true; + public static void setActiveServer(String serverKey, EnumMap serverRevisions) { + String normalizedKey = serverKey == null ? "" : serverKey; + if (!normalizedKey.equals(activeServerKey)) { + clientRevisionCache.clear(); + } + activeServerKey = normalizedKey; + + if (activeServerKey.isEmpty()) { + return; + } + + EnumMap cached = clientRevisionCache.computeIfAbsent( + activeServerKey, + ignored -> new EnumMap<>(EnumSyncType.class) + ); + if (serverRevisions != null && !serverRevisions.isEmpty()) { + cached.keySet().retainAll(serverRevisions.keySet()); + } + } + + public static EnumMap getCachedRevisionsForServer(String serverKey) { + String key = serverKey == null ? "" : serverKey; + if (key.isEmpty()) { + return new EnumMap<>(EnumSyncType.class); + } + EnumMap revisions = clientRevisionCache.get(key); + if (revisions == null) { + return new EnumMap<>(EnumSyncType.class); + } + return new EnumMap<>(revisions); + } + + public static void updateClientRevision(EnumSyncType type, int revision) { + if (revision < 0) { + return; + } + String key = activeServerKey == null ? "" : activeServerKey; + if (key.isEmpty()) { + return; + } + EnumMap revisions = clientRevisionCache.computeIfAbsent( + key, + ignored -> new EnumMap<>(EnumSyncType.class) + ); + revisions.put(type, revision); + } + public static ImageData getImageData(String directory) { synchronized (imageDataCache) { diff --git a/src/main/java/noppes/npcs/client/gui/GuiMagicCycleMap.java b/src/main/java/noppes/npcs/client/gui/GuiMagicCycleMap.java index 6d0a97b95..7508958ed 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiMagicCycleMap.java +++ b/src/main/java/noppes/npcs/client/gui/GuiMagicCycleMap.java @@ -41,6 +41,8 @@ public class GuiMagicCycleMap extends GuiDiagram { invalidateCache(); return; } + // TODO: Consumes MagicController's full magic registry to resolve associations; visible only through magic editing GUIs + // that require the NPC wand and CustomNpcsPermissions.GLOBAL_MAGIC (or op status) to open. MagicController mc = MagicController.getInstance(); // Get magics from the cycle's associations if associations are not null. if (cycle.associations != null) { @@ -67,6 +69,8 @@ public class GuiMagicCycleMap extends GuiDiagram { * Constructor using a cycle ID. */ public GuiMagicCycleMap(GuiNPCInterface parent, int x, int y, int width, int height, int cycleId) { + // TODO: Looks up cycle data from MagicController; accessed from the same wand/CustomNpcsPermissions.GLOBAL_MAGIC gated + // management screens. this(parent, x, y, width, height, MagicController.getInstance().getCycle(cycleId)); } diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index 3cdde7e62..ea9941b69 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -236,6 +236,8 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall initGui(); } if (guibutton.id == 106) { + // TODO: Uses ScriptController.Instance.dir (shared script repository); this GUI is gated behind the scripter tool + // and CustomNpcsPermissions.TOOL_SCRIPTER so only authorized editors touch the controller-backed scripts. NoppesUtil.openFolder(ScriptController.Instance.dir); } if (guibutton.id == 107) { diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiMagicCycleViewer.java b/src/main/java/noppes/npcs/client/gui/SubGuiMagicCycleViewer.java index dd0954ac9..09b367deb 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiMagicCycleViewer.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiMagicCycleViewer.java @@ -19,6 +19,8 @@ public class SubGuiMagicCycleViewer extends SubGuiInterface { } public SubGuiMagicCycleViewer(int cycleID) { + // TODO: Fetches MagicController cycle definitions (global magic data); only staff with the wand and + // CustomNpcsPermissions.GLOBAL_MAGIC reach this viewer via the magic management GUIs. this(MagicController.getInstance().getCycle(cycleID)); } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageEffects.java b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageEffects.java index a5f32da52..21ecb1b8d 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageEffects.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageEffects.java @@ -124,6 +124,8 @@ public class GuiNPCManageEffects extends GuiNPCInterface2 implements ICustomScro effect.readFromNBT(compound); setSelected(effect.name); if (effect.id != -1) { + // TODO: Writes back into the CustomEffectController cache (global effect dataset); only players with the wand and + // CustomNpcsPermissions.GLOBAL_EFFECT reach this admin editor. CustomEffectController.getInstance().getCustomEffects().replace(effect.id, effect); } initGui(); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java b/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java index 956e30058..683e21798 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java @@ -41,6 +41,8 @@ public class GuiAnvilRecipes extends GuiNPCInterface { setBackground("recipesAnvil.png"); this.closeOnEsc = true; + // TODO: Loads the entire anvil recipe registry from RecipeController; any player can open this reference GUI without + // special permissions. recipes.addAll(RecipeController.Instance.anvilRecipes.values()); } @@ -81,9 +83,12 @@ public class GuiAnvilRecipes extends GuiNPCInterface { private List getSearchList() { if (search.isEmpty()) { + // TODO: Returns every anvil recipe from RecipeController for browsing; no CustomNpcsPermissions gate applies. return new ArrayList(RecipeController.Instance.anvilRecipes.values()); } List list = new ArrayList(); + // TODO: Filters across the RecipeController anvil collection; the bench/anvil GUIs are normal gameplay with no + // permission checks. for (RecipeAnvil recipe : RecipeController.Instance.anvilRecipes.values()) { ItemStack repairItem = recipe.itemToRepair; if (repairItem == null) diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiNpcAnvil.java b/src/main/java/noppes/npcs/client/gui/player/GuiNpcAnvil.java index e98b8d280..c4a21e05e 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiNpcAnvil.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiNpcAnvil.java @@ -39,6 +39,8 @@ public class GuiNpcAnvil extends GuiContainerNPCInterface { @Override protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) { // Enable the button if there are any anvil recipes. + // TODO: Checks the shared RecipeController anvil list (entire dataset) before enabling; this player crafting GUI is + // public, so no CustomNpcsPermissions gate applies here. button.enabled = RecipeController.Instance != null && !RecipeController.Instance.getAnvilList().isEmpty(); GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); mc.renderEngine.bindTexture(resource); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiNpcCarpentryBench.java b/src/main/java/noppes/npcs/client/gui/player/GuiNpcCarpentryBench.java index f3476528e..39d4f5d86 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiNpcCarpentryBench.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiNpcCarpentryBench.java @@ -36,6 +36,8 @@ public class GuiNpcCarpentryBench extends GuiContainerNPCInterface { @Override protected void drawGuiContainerBackgroundLayer(float f, int i, int j) { + // TODO: Reads the global RecipeController carpentry list before enabling; this bench is a public crafting block with no + // additional permission checks. button.enabled = RecipeController.Instance != null && !RecipeController.Instance.carpentryRecipes.isEmpty(); GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); mc.renderEngine.bindTexture(resource); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiRecipes.java b/src/main/java/noppes/npcs/client/gui/player/GuiRecipes.java index 78507813a..e3187c80b 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiRecipes.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiRecipes.java @@ -39,6 +39,8 @@ public class GuiRecipes extends GuiNPCInterface { this.xSize = 256; setBackground("recipes.png"); this.closeOnEsc = true; + // TODO: Preloads every carpentry recipe from RecipeController (full dataset); this book is open to any player, so no + // explicit permission is checked when accessing it. recipes.addAll(RecipeController.Instance.carpentryRecipes.values()); } @@ -74,9 +76,13 @@ public class GuiRecipes extends GuiNPCInterface { private List getSearchList() { if (search.isEmpty()) { + // TODO: Returns the entire carpentry recipe collection from RecipeController; again this is unrestricted player + // content with no CustomNpcsPermissions gating. return new ArrayList(RecipeController.Instance.carpentryRecipes.values()); } List list = new ArrayList(); + // TODO: Iterates over the RecipeController carpentry map to filter recipes; this viewing GUI is available to all + // players without special permissions. for (IRecipe recipe : RecipeController.Instance.carpentryRecipes.values()) { if (recipe.getRecipeOutput() == null) continue; diff --git a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiCNPCInventory.java b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiCNPCInventory.java index 666056506..f27913d89 100644 --- a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiCNPCInventory.java +++ b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiCNPCInventory.java @@ -94,6 +94,8 @@ public class GuiCNPCInventory extends GuiNPCInterface { PlayerData data = PlayerData.get(mc.thePlayer); if (data != null && data.effectData != null) { for (PlayerEffect pe : data.effectData.getEffects().values()) { + // TODO: Pulls specific CustomEffect entries from the global controller; this HUD is part of the base + // player inventory so it must work without extra permissions (no CustomNpcsPermissions required). CustomEffect effect = CustomEffectController.getInstance().get(pe.id, pe.index); if (effect != null) { effectBar.entries.add(new GuiEffectBar.EffectEntry(effect, pe)); diff --git a/src/main/java/noppes/npcs/client/gui/player/modern/GuiModernQuestDialog.java b/src/main/java/noppes/npcs/client/gui/player/modern/GuiModernQuestDialog.java index f0a20373f..ac09ffb43 100644 --- a/src/main/java/noppes/npcs/client/gui/player/modern/GuiModernQuestDialog.java +++ b/src/main/java/noppes/npcs/client/gui/player/modern/GuiModernQuestDialog.java @@ -255,6 +255,8 @@ public class GuiModernQuestDialog extends GuiNPCInterface implements IGuiClose { } int fac1ID = quest.factionOptions.factionId; if (fac1ID != -1) { + // TODO: Looks up faction display data from the global FactionController; this dialog is part of the standard + // player quest flow so no special permissions are needed beyond quest access. String fac1Name = FactionController.getInstance().getFaction(fac1ID).getName(); String fac1Color = (quest.factionOptions.decreaseFactionPoints) ? "§c-" : "§a+"; int fac1Point = quest.factionOptions.factionPoints; @@ -263,6 +265,8 @@ public class GuiModernQuestDialog extends GuiNPCInterface implements IGuiClose { } int fac2ID = quest.factionOptions.faction2Id; if (fac2ID != -1) { + // TODO: Retrieves the second faction's metadata from FactionController as above; normal quest participants reach + // this without CustomNpcsPermissions requirements. String fac2Name = FactionController.getInstance().getFaction(fac2ID).getName(); String fac2Color = (quest.factionOptions.decreaseFaction2Points) ? "§c-" : "§a+"; int fac2Point = quest.factionOptions.faction2Points; diff --git a/src/main/java/noppes/npcs/client/gui/questtypes/GuiNpcQuestTypeItem.java b/src/main/java/noppes/npcs/client/gui/questtypes/GuiNpcQuestTypeItem.java index 0d9ac099f..0d0747175 100644 --- a/src/main/java/noppes/npcs/client/gui/questtypes/GuiNpcQuestTypeItem.java +++ b/src/main/java/noppes/npcs/client/gui/questtypes/GuiNpcQuestTypeItem.java @@ -79,7 +79,9 @@ public class GuiNpcQuestTypeItem extends GuiContainerNPCInterface implements ITe // map.put(i, item.copy()); // } // ((QuestItem)quest.questInterface).items = map; -// QuestController.saveQuest(quest); +// TODO: Saving would route through the global QuestController, which should stay limited to wand users with +// CustomNpcsPermissions.GLOBAL_QUEST or operators when OpsOnly is active. +// QuestController.saveQuest(quest); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index f68d5bfa0..185eb90fd 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -307,6 +307,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } if (guibutton.id == 106) { + // TODO: Opens the ScriptController shared directory; this screen is available only to editors with the scripter + // tool and CustomNpcsPermissions.TOOL_SCRIPTER. NoppesUtil.openFolder(ScriptController.Instance.dir); } diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiDialogSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiDialogSelection.java index bfe59e467..861043151 100644 --- a/src/main/java/noppes/npcs/client/gui/select/GuiDialogSelection.java +++ b/src/main/java/noppes/npcs/client/gui/select/GuiDialogSelection.java @@ -37,6 +37,8 @@ public class GuiDialogSelection extends SubGuiInterface implements ICustomScroll setBackground("menubg.png"); xSize = 366; ySize = 226; + // TODO: Pulls the entire DialogController dialog map (global data); players only reach this via wand-required dialog + // editors that enforce PacketUtil.verifyItemPacket and CustomNpcsPermissions.GLOBAL_DIALOG/ops when editing. this.selectedDialog = DialogController.Instance.dialogs.get(dialog); if (selectedDialog != null) { selectedCategory = selectedDialog.category; @@ -59,6 +61,8 @@ public class GuiDialogSelection extends SubGuiInterface implements ICustomScroll HashMap categoryData = new HashMap(); HashMap dialogData = new HashMap(); + // TODO: Iterates over every dialog category provided by DialogController; reachable through NPC wand workflows that + // require dialog-edit permissions (CustomNpcsPermissions.GLOBAL_DIALOG or op status under OpsOnly). for (DialogCategory category : DialogController.Instance.categories.values()) { categoryData.put(category.title, category); } diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiQuestSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiQuestSelection.java index 72a1da482..288964545 100644 --- a/src/main/java/noppes/npcs/client/gui/select/GuiQuestSelection.java +++ b/src/main/java/noppes/npcs/client/gui/select/GuiQuestSelection.java @@ -37,6 +37,8 @@ public class GuiQuestSelection extends SubGuiInterface implements ICustomScrollL setBackground("menubg.png"); xSize = 366; ySize = 226; + // TODO: Requires the full QuestController quest map (global dataset); access comes from wand-driven quest editors + // that rely on PacketUtil.verifyItemPacket and ultimately CustomNpcsPermissions.GLOBAL_QUEST / server op checks. this.selectedQuest = QuestController.Instance.quests.get(quest); if (selectedQuest != null) { selectedCategory = selectedQuest.category; @@ -59,6 +61,8 @@ public class GuiQuestSelection extends SubGuiInterface implements ICustomScrollL HashMap categoryData = new HashMap(); HashMap questData = new HashMap(); + // TODO: Needs every quest category from QuestController so the selector can populate; opened from NPC wand flows that + // demand the editing player satisfy CustomNpcsPermissions.GLOBAL_QUEST (or be OP when OpsOnly is enabled). for (QuestCategory category : QuestController.Instance.categories.values()) { categoryData.put(category.title, category); }