From 48281c8d34e025222d3cd5407bd6bee27690caec Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 31 Mar 2026 19:59:53 +0200 Subject: [PATCH 01/34] SyncController OVERHAUL PHASE #1: - Refactored EnumSyncType into object based SyncType registry, so other mods can register their own types --- .../npcs/controllers/SyncController.java | 871 +++++++++--------- .../npcs/network/enums/EnumSyncType.java | 18 - .../kamkeel/npcs/network/enums/SyncType.java | 178 ++++ .../network/packets/data/LoginPacket.java | 23 +- .../packets/data/large/SyncPacket.java | 41 +- .../player/SyncRevisionInfoPacket.java | 18 +- .../java/noppes/npcs/ServerEventsHandler.java | 10 +- .../npcs/client/ClientCacheHandler.java | 32 +- .../controllers/CustomEffectController.java | 6 +- .../npcs/controllers/DialogController.java | 10 +- .../npcs/controllers/FactionController.java | 6 +- .../npcs/controllers/MagicController.java | 10 +- .../npcs/controllers/QuestController.java | 8 +- 13 files changed, 673 insertions(+), 558 deletions(-) delete mode 100644 src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java create mode 100644 src/main/java/kamkeel/npcs/network/enums/SyncType.java diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index 57b111404..ca2171461 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -10,7 +10,7 @@ import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; import kamkeel.npcs.network.LargeAbstractPacket; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumSyncAction; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import kamkeel.npcs.network.packets.data.LoginPacket; import kamkeel.npcs.network.packets.data.ProfileSharedQuestPacket; import kamkeel.npcs.network.packets.data.ability.AbilityCooldownSyncPacket; @@ -47,11 +47,12 @@ import noppes.npcs.controllers.data.RecipeAnvil; import noppes.npcs.controllers.data.RecipeCarpentry; import java.io.IOException; -import java.util.EnumMap; -import java.util.EnumSet; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; @@ -60,7 +61,7 @@ public class SyncController { public static boolean DEBUG_SYNC_LOGGING = false; - private static final EnumMap cacheEntries = new EnumMap<>(EnumSyncType.class); + private static final Map cacheEntries = new LinkedHashMap<>(); private static final ConcurrentHashMap playerSyncState = new ConcurrentHashMap<>(); @@ -70,35 +71,35 @@ public class SyncController { } } - 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, - EnumSyncType.CUSTOM_ABILITY, - EnumSyncType.CHAINED_ABILITY + private static final SyncType[] LOGIN_SYNC_TYPES = new SyncType[]{ + SyncType.FACTION, + SyncType.DIALOG_CATEGORY, + SyncType.QUEST_CATEGORY, + SyncType.WORKBENCH_RECIPES, + SyncType.CARPENTRY_RECIPES, + SyncType.ANVIL_RECIPES, + SyncType.CUSTOM_EFFECTS, + SyncType.MAGIC, + SyncType.MAGIC_CYCLE, + SyncType.CUSTOM_ABILITY, + SyncType.CHAINED_ABILITY }; public static void load() { cacheEntries.clear(); playerSyncState.clear(); - 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); - registerCache(EnumSyncType.CUSTOM_ABILITY, SyncController::customAbilitiesNBT); - registerCache(EnumSyncType.CHAINED_ABILITY, SyncController::chainedAbilitiesNBT); + registerCache(SyncType.FACTION, SyncController::factionsNBT); + registerCache(SyncType.DIALOG_CATEGORY, SyncController::dialogCategoriesNBT); + registerCache(SyncType.QUEST_CATEGORY, SyncController::questCategoriesNBT); + registerCache(SyncType.WORKBENCH_RECIPES, SyncController::workbenchNBT); + registerCache(SyncType.CARPENTRY_RECIPES, SyncController::carpentryNBT); + registerCache(SyncType.ANVIL_RECIPES, SyncController::anvilNBT); + registerCache(SyncType.CUSTOM_EFFECTS, SyncController::customEffectsNBT); + registerCache(SyncType.MAGIC, SyncController::magicsNBT); + registerCache(SyncType.MAGIC_CYCLE, SyncController::magicCyclesNBT); + registerCache(SyncType.CUSTOM_ABILITY, SyncController::customAbilitiesNBT); + registerCache(SyncType.CHAINED_ABILITY, SyncController::chainedAbilitiesNBT); } public static void syncPlayer(EntityPlayerMP player) { @@ -108,7 +109,7 @@ public class SyncController { private static void syncPlayerInternal(EntityPlayerMP player, boolean includePostPackets) { PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); - for (EnumSyncType type : LOGIN_SYNC_TYPES) { + for (SyncType type : LOGIN_SYNC_TYPES) { SyncCacheEntry entry = cacheEntries.get(type); if (entry == null) { continue; @@ -149,7 +150,7 @@ public class SyncController { EntityPlayerMP player, String serverKey, String previousServerKey, - Map clientRevisions + Map clientRevisions ) { String currentServerKey = getServerCacheKey(); PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); @@ -196,9 +197,9 @@ public class SyncController { } } - private static EnumMap getServerRevisionSnapshot() { - EnumMap snapshot = new EnumMap<>(EnumSyncType.class); - for (EnumSyncType type : LOGIN_SYNC_TYPES) { + private static Map getServerRevisionSnapshot() { + Map snapshot = new LinkedHashMap<>(); + for (SyncType type : LOGIN_SYNC_TYPES) { SyncCacheEntry entry = cacheEntries.get(type); if (entry != null) { snapshot.put(type, entry.getRevisionValue()); @@ -222,7 +223,7 @@ public class SyncController { return SERVER_IDENTITY_KEY; } - public static int getCurrentRevision(EnumSyncType type) { + public static int getCurrentRevision(SyncType type) { SyncCacheEntry entry = cacheEntries.get(type); return entry == null ? -1 : entry.getRevisionValue(); } @@ -356,12 +357,12 @@ public class SyncController { } public static void syncAllCustomAbilities() { - CachedSyncPayload payload = rebuildNow(EnumSyncType.CUSTOM_ABILITY); + CachedSyncPayload payload = rebuildNow(SyncType.CUSTOM_ABILITY); if (payload == null) { return; } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CUSTOM_ABILITY, payload)); - updateAllPlayerRevisions(EnumSyncType.CUSTOM_ABILITY, payload.getRevision()); + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CUSTOM_ABILITY, payload)); + updateAllPlayerRevisions(SyncType.CUSTOM_ABILITY, payload.getRevision()); } public static NBTTagCompound chainedAbilitiesNBT() { @@ -377,12 +378,12 @@ public class SyncController { } public static void syncAllChainedAbilities() { - CachedSyncPayload payload = rebuildNow(EnumSyncType.CHAINED_ABILITY); + CachedSyncPayload payload = rebuildNow(SyncType.CHAINED_ABILITY); if (payload == null) { return; } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CHAINED_ABILITY, payload)); - updateAllPlayerRevisions(EnumSyncType.CHAINED_ABILITY, payload.getRevision()); + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CHAINED_ABILITY, payload)); + updateAllPlayerRevisions(SyncType.CHAINED_ABILITY, payload.getRevision()); } public static void syncPlayerData(EntityPlayerMP player, boolean update) { @@ -390,14 +391,14 @@ public class SyncController { if (data != null) { if (update) { PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.PLAYERDATA, + SyncType.PLAYERDATA, EnumSyncAction.UPDATE, -1, data.getSyncNBT()) , player); } else { PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.PLAYERDATA, + SyncType.PLAYERDATA, EnumSyncAction.RELOAD, -1, data.getSyncNBTFull()), player); @@ -405,11 +406,11 @@ public class SyncController { } } - public static void syncRemove(EnumSyncType enumSyncType, int id) { - Map revisions = invalidateCaches(enumSyncType); - int revision = revisions.getOrDefault(enumSyncType, getCurrentRevision(enumSyncType)); + public static void syncRemove(SyncType syncType, int id) { + Map revisions = invalidateCaches(syncType); + int revision = revisions.getOrDefault(syncType, getCurrentRevision(syncType)); PacketHandler.Instance.sendToAll(new SyncPacket( - enumSyncType, + syncType, EnumSyncAction.REMOVE, id, revision, @@ -419,373 +420,326 @@ public class SyncController { } public static void syncAllDialogs() { - CachedSyncPayload payload = rebuildNow(EnumSyncType.DIALOG_CATEGORY); + CachedSyncPayload payload = rebuildNow(SyncType.DIALOG_CATEGORY); if (payload == null) { return; } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.DIALOG_CATEGORY, payload)); - updateAllPlayerRevisions(EnumSyncType.DIALOG_CATEGORY, payload.getRevision()); + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.DIALOG_CATEGORY, payload)); + updateAllPlayerRevisions(SyncType.DIALOG_CATEGORY, payload.getRevision()); } public static void syncAllQuests() { - CachedSyncPayload payload = rebuildNow(EnumSyncType.QUEST_CATEGORY); + CachedSyncPayload payload = rebuildNow(SyncType.QUEST_CATEGORY); if (payload == null) { return; } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.QUEST_CATEGORY, payload)); - updateAllPlayerRevisions(EnumSyncType.QUEST_CATEGORY, payload.getRevision()); + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.QUEST_CATEGORY, payload)); + updateAllPlayerRevisions(SyncType.QUEST_CATEGORY, payload.getRevision()); } public static void syncAllWorkbenchRecipes() { - CachedSyncPayload payload = rebuildNow(EnumSyncType.WORKBENCH_RECIPES); + CachedSyncPayload payload = rebuildNow(SyncType.WORKBENCH_RECIPES); if (payload == null) { return; } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.WORKBENCH_RECIPES, payload)); - updateAllPlayerRevisions(EnumSyncType.WORKBENCH_RECIPES, payload.getRevision()); + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.WORKBENCH_RECIPES, payload)); + updateAllPlayerRevisions(SyncType.WORKBENCH_RECIPES, payload.getRevision()); } public static void syncAllCarpentryRecipes() { - CachedSyncPayload payload = rebuildNow(EnumSyncType.CARPENTRY_RECIPES); + CachedSyncPayload payload = rebuildNow(SyncType.CARPENTRY_RECIPES); if (payload == null) { return; } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CARPENTRY_RECIPES, payload)); - updateAllPlayerRevisions(EnumSyncType.CARPENTRY_RECIPES, payload.getRevision()); + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CARPENTRY_RECIPES, payload)); + updateAllPlayerRevisions(SyncType.CARPENTRY_RECIPES, payload.getRevision()); } public static void syncAllAnvilRecipes() { - CachedSyncPayload payload = rebuildNow(EnumSyncType.ANVIL_RECIPES); + CachedSyncPayload payload = rebuildNow(SyncType.ANVIL_RECIPES); if (payload == null) { return; } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.ANVIL_RECIPES, payload)); - updateAllPlayerRevisions(EnumSyncType.ANVIL_RECIPES, payload.getRevision()); + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.ANVIL_RECIPES, payload)); + updateAllPlayerRevisions(SyncType.ANVIL_RECIPES, payload.getRevision()); } public static void syncAllCustomEffects() { - CachedSyncPayload payload = rebuildNow(EnumSyncType.CUSTOM_EFFECTS); + CachedSyncPayload payload = rebuildNow(SyncType.CUSTOM_EFFECTS); if (payload == null) { return; } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CUSTOM_EFFECTS, payload)); - updateAllPlayerRevisions(EnumSyncType.CUSTOM_EFFECTS, payload.getRevision()); + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CUSTOM_EFFECTS, payload)); + updateAllPlayerRevisions(SyncType.CUSTOM_EFFECTS, payload.getRevision()); } @SideOnly(Side.CLIENT) - public static void clientSync(EnumSyncType enumSyncType, int revision, NBTTagCompound fullCompound) { - switch (enumSyncType) { - case FACTION: { - NBTTagList list = fullCompound.getTagList("Factions", 10); - for (int i = 0; i < list.tagCount(); i++) { - Faction faction = new Faction(); - faction.readNBT(list.getCompoundTagAt(i)); - FactionController.getInstance().factionsSync.put(faction.id, faction); - } - FactionController.getInstance().factions = FactionController.getInstance().factionsSync; - FactionController.getInstance().factionsSync = new HashMap(); - break; - } - case DIALOG_CATEGORY: { - if (!fullCompound.hasNoTags()) { - NBTTagList categories = fullCompound.getTagList("DialogCategories", 10); - for (int j = 0; j < fullCompound.getTagList("DialogCategories", 10).tagCount(); j++) { - NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); - if (categoryCompound.hasNoTags()) - continue; - - DialogCategory category = new DialogCategory(); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - NBTTagList dialogList = categoryCompound.getTagList("Data", 10); - if (DialogController.Instance.categoriesSync.containsKey(category.id)) { - category = DialogController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < dialogList.tagCount(); i++) { - Dialog dialog = new Dialog(); - dialog.readNBT(dialogList.getCompoundTagAt(i)); - dialog.category = category; - category.dialogs.put(dialog.id, dialog); - } - DialogController.Instance.categoriesSync.put(category.id, category); - } - } - - HashMap dialogs = new HashMap(); - for (DialogCategory category : DialogController.Instance.categoriesSync.values()) { - for (Dialog dialog : category.dialogs.values()) { - dialogs.put(dialog.id, dialog); - } - } - - DialogController.Instance.categories = DialogController.Instance.categoriesSync; - DialogController.Instance.dialogs = dialogs; - DialogController.Instance.categoriesSync = new HashMap(); - break; - } - case QUEST_CATEGORY: { - if (!fullCompound.hasNoTags()) { - NBTTagList categories = fullCompound.getTagList("QuestCategories", 10); - for (int j = 0; j < fullCompound.getTagList("QuestCategories", 10).tagCount(); j++) { - NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); - if (categoryCompound.hasNoTags()) - continue; - - QuestCategory category = new QuestCategory(); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - NBTTagList questList = categoryCompound.getTagList("Data", 10); - if (QuestController.Instance.categoriesSync.containsKey(category.id)) { - category = QuestController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < questList.tagCount(); i++) { - Quest quest = new Quest(); - quest.readNBT(questList.getCompoundTagAt(i)); - quest.category = category; - category.quests.put(quest.id, quest); - } - QuestController.Instance.categoriesSync.put(category.id, category); - } - } - - HashMap quests = new HashMap(); - for (QuestCategory category : QuestController.Instance.categoriesSync.values()) { - for (Quest quest : category.quests.values()) { - quests.put(quest.id, quest); - } - } - - QuestController.Instance.categories = QuestController.Instance.categoriesSync; - QuestController.Instance.quests = quests; - QuestController.Instance.categoriesSync = new HashMap(); - break; - } - case PLAYERDATA: { - ClientCacheHandler.playerData.setSyncNBTFull(fullCompound); - break; - } - case MAGIC: { - NBTTagList list = fullCompound.getTagList("Data", 10); - MagicController mc = MagicController.getInstance(); - mc.magicSync.clear(); - for (int i = 0; i < list.tagCount(); i++) { - Magic magic = new Magic(); - magic.readNBT(list.getCompoundTagAt(i)); - mc.magicSync.put(magic.id, magic); - } - - mc.magics.clear(); - mc.magics.putAll(mc.magicSync); - mc.magicSync.clear(); - break; - } - case MAGIC_CYCLE: { - NBTTagList list = fullCompound.getTagList("Data", 10); - MagicController mc = MagicController.getInstance(); - mc.cyclesSync.clear(); - for (int i = 0; i < list.tagCount(); i++) { - MagicCycle cycle = new MagicCycle(); - cycle.readNBT(list.getCompoundTagAt(i)); - mc.cyclesSync.put(cycle.id, cycle); - } - - mc.cycles.clear(); - mc.cycles.putAll(mc.cyclesSync); - mc.cyclesSync.clear(); - break; - } - case WORKBENCH_RECIPES: { - NBTTagList list = fullCompound.getTagList("recipes", 10); - if (list == null) - return; - - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound recipeCompound = list.getCompoundTagAt(i); - RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); - recipe.readNBT(recipeCompound); - RecipeController.syncRecipes.put(recipe.id, recipe); - } - - RecipeController.reloadGlobalRecipes(RecipeController.syncRecipes); - RecipeController.syncRecipes = new HashMap(); - break; - } - case CARPENTRY_RECIPES: { - NBTTagList list = fullCompound.getTagList("recipes", 10); - if (list == null) - return; - - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound recipeCompound = list.getCompoundTagAt(i); - RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); - recipe.readNBT(recipeCompound); - RecipeController.syncRecipes.put(recipe.id, recipe); - } - - RecipeController.Instance.carpentryRecipes = RecipeController.syncRecipes; - RecipeController.syncRecipes = new HashMap(); - break; - } - case ANVIL_RECIPES: { - NBTTagList list = fullCompound.getTagList("recipes", 10); - if (list == null) - return; - - for (int i = 0; i < list.tagCount(); i++) { - RecipeAnvil recipe = new RecipeAnvil(); - recipe.readNBT(list.getCompoundTagAt(i)); - RecipeController.syncAnvilRecipes.put(recipe.id, recipe); - } - - RecipeController.Instance.anvilRecipes = RecipeController.syncAnvilRecipes; - RecipeController.syncAnvilRecipes = new HashMap(); - break; - } - case CUSTOM_EFFECTS: { - NBTTagList list = fullCompound.getTagList("Data", 10); - CustomEffectController ce = CustomEffectController.getInstance(); - ce.customEffectsSync.clear(); - for (int i = 0; i < list.tagCount(); i++) { - CustomEffect effect = new CustomEffect(); - effect.readFromNBT(list.getCompoundTagAt(i)); - ClientCacheHandler.getImageData(effect.icon); - ce.customEffectsSync.put(effect.id, effect); - } - - ce.getCustomEffects().clear(); - ce.getCustomEffects().putAll(ce.customEffectsSync); - ce.customEffectsSync.clear(); - break; - } - case CUSTOM_ABILITY: { - NBTTagList list = fullCompound.getTagList("Data", 10); - LinkedHashMap sync = new LinkedHashMap<>(); - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound nbt = list.getCompoundTagAt(i); - String name = nbt.getString("CustomAbilityId"); - Ability ability = AbilityController.Instance.fromNBT(nbt); - if (ability != null) { - ability.setName(name); - sync.put(name, ability); - } - } - AbilityController.Instance.setCustomAbilities(sync); - break; - } - case CHAINED_ABILITY: { - NBTTagList list = fullCompound.getTagList("Data", 10); - LinkedHashMap sync = new LinkedHashMap<>(); - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound nbt = list.getCompoundTagAt(i); - String id = nbt.getString("ChainedAbilityId"); - ChainedAbility chain = new ChainedAbility(); - chain.readNBT(nbt); - chain.setName(id); - sync.put(id, chain); - } - AbilityController.Instance.setChainedAbilities(sync); - break; - } - } - - ClientCacheHandler.updateClientRevision(enumSyncType, revision); - } - - /** - * Update Related Tasks - */ - @SideOnly(Side.CLIENT) - public static void clientUpdate(EnumSyncType enumSyncType, int category_id, int revision, NBTTagCompound compound) { - switch (enumSyncType) { - case FACTION: { + public static void clientSync(SyncType syncType, int revision, NBTTagCompound fullCompound) { + if (syncType == SyncType.FACTION) { + NBTTagList list = fullCompound.getTagList("Factions", 10); + for (int i = 0; i < list.tagCount(); i++) { Faction faction = new Faction(); - faction.readNBT(compound); - FactionController.getInstance().factions.put(faction.id, faction); - break; + faction.readNBT(list.getCompoundTagAt(i)); + FactionController.getInstance().factionsSync.put(faction.id, faction); } - case DIALOG: { - DialogCategory category = DialogController.Instance.categories.get(category_id); - Dialog dialog = new Dialog(); - dialog.category = category; - dialog.readNBT(compound); - DialogController.Instance.dialogs.put(dialog.id, dialog); - category.dialogs.put(dialog.id, dialog); - break; - } - case QUEST: { - QuestCategory category = QuestController.Instance.categories.get(category_id); - Quest quest = new Quest(); - quest.category = category; - quest.readNBT(compound); - QuestController.Instance.quests.put(quest.id, quest); - category.quests.put(quest.id, quest); - break; - } - case QUEST_CATEGORY: { - QuestCategory category = new QuestCategory(); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - NBTTagList list = compound.getTagList("Data", 10); - if (QuestController.Instance.categoriesSync.containsKey(category.id)) { - category = QuestController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < list.tagCount(); i++) { - Quest quest = new Quest(); - quest.readNBT(list.getCompoundTagAt(i)); - quest.category = category; - category.quests.put(quest.id, quest); - } - QuestController.Instance.categories.put(category.id, category); - break; - } - case DIALOG_CATEGORY: { - DialogCategory category = new DialogCategory(); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - NBTTagList list = compound.getTagList("Data", 10); - if (DialogController.Instance.categoriesSync.containsKey(category.id)) { - category = DialogController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < list.tagCount(); i++) { - Dialog dialog = new Dialog(); - dialog.readNBT(list.getCompoundTagAt(i)); - dialog.category = category; - category.dialogs.put(dialog.id, dialog); - } - DialogController.Instance.categories.put(category.id, category); - break; - } - case MAGIC_CYCLE: { - MagicCycle cycle = new MagicCycle(); - cycle.readNBT(compound); - MagicController.getInstance().cycles.put(cycle.id, cycle); - break; - } - case MAGIC: { - Magic magic = new Magic(); - magic.readNBT(compound); - MagicController.getInstance().magics.put(magic.id, magic); - break; - } - case PLAYERDATA: { - ClientCacheHandler.playerData.setSyncNBT(compound); - break; - } - case CUSTOM_EFFECTS: { - CustomEffect effect = new CustomEffect(); - effect.readFromNBT(compound); - ClientCacheHandler.getImageData(effect.icon); + FactionController.getInstance().factions = FactionController.getInstance().factionsSync; + FactionController.getInstance().factionsSync = new HashMap(); + } else if (syncType == SyncType.DIALOG_CATEGORY) { + if (!fullCompound.hasNoTags()) { + NBTTagList categories = fullCompound.getTagList("DialogCategories", 10); + for (int j = 0; j < fullCompound.getTagList("DialogCategories", 10).tagCount(); j++) { + NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); + if (categoryCompound.hasNoTags()) + continue; - CustomEffectController.Instance.getCustomEffects().put(effect.id, effect); - break; + DialogCategory category = new DialogCategory(); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + NBTTagList dialogList = categoryCompound.getTagList("Data", 10); + if (DialogController.Instance.categoriesSync.containsKey(category.id)) { + category = DialogController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < dialogList.tagCount(); i++) { + Dialog dialog = new Dialog(); + dialog.readNBT(dialogList.getCompoundTagAt(i)); + dialog.category = category; + category.dialogs.put(dialog.id, dialog); + } + DialogController.Instance.categoriesSync.put(category.id, category); + } } + + HashMap dialogs = new HashMap(); + for (DialogCategory category : DialogController.Instance.categoriesSync.values()) { + for (Dialog dialog : category.dialogs.values()) { + dialogs.put(dialog.id, dialog); + } + } + + DialogController.Instance.categories = DialogController.Instance.categoriesSync; + DialogController.Instance.dialogs = dialogs; + DialogController.Instance.categoriesSync = new HashMap(); + } else if (syncType == SyncType.QUEST_CATEGORY) { + if (!fullCompound.hasNoTags()) { + NBTTagList categories = fullCompound.getTagList("QuestCategories", 10); + for (int j = 0; j < fullCompound.getTagList("QuestCategories", 10).tagCount(); j++) { + NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); + if (categoryCompound.hasNoTags()) + continue; + + QuestCategory category = new QuestCategory(); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + NBTTagList questList = categoryCompound.getTagList("Data", 10); + if (QuestController.Instance.categoriesSync.containsKey(category.id)) { + category = QuestController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < questList.tagCount(); i++) { + Quest quest = new Quest(); + quest.readNBT(questList.getCompoundTagAt(i)); + quest.category = category; + category.quests.put(quest.id, quest); + } + QuestController.Instance.categoriesSync.put(category.id, category); + } + } + + HashMap quests = new HashMap(); + for (QuestCategory category : QuestController.Instance.categoriesSync.values()) { + for (Quest quest : category.quests.values()) { + quests.put(quest.id, quest); + } + } + + QuestController.Instance.categories = QuestController.Instance.categoriesSync; + QuestController.Instance.quests = quests; + QuestController.Instance.categoriesSync = new HashMap(); + } else if (syncType == SyncType.PLAYERDATA) { + ClientCacheHandler.playerData.setSyncNBTFull(fullCompound); + } else if (syncType == SyncType.MAGIC) { + NBTTagList list = fullCompound.getTagList("Data", 10); + MagicController mc = MagicController.getInstance(); + mc.magicSync.clear(); + for (int i = 0; i < list.tagCount(); i++) { + Magic magic = new Magic(); + magic.readNBT(list.getCompoundTagAt(i)); + mc.magicSync.put(magic.id, magic); + } + + mc.magics.clear(); + mc.magics.putAll(mc.magicSync); + mc.magicSync.clear(); + } else if (syncType == SyncType.MAGIC_CYCLE) { + NBTTagList list = fullCompound.getTagList("Data", 10); + MagicController mc = MagicController.getInstance(); + mc.cyclesSync.clear(); + for (int i = 0; i < list.tagCount(); i++) { + MagicCycle cycle = new MagicCycle(); + cycle.readNBT(list.getCompoundTagAt(i)); + mc.cyclesSync.put(cycle.id, cycle); + } + + mc.cycles.clear(); + mc.cycles.putAll(mc.cyclesSync); + mc.cyclesSync.clear(); + } else if (syncType == SyncType.WORKBENCH_RECIPES) { + NBTTagList list = fullCompound.getTagList("recipes", 10); + if (list == null) + return; + + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound recipeCompound = list.getCompoundTagAt(i); + RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); + recipe.readNBT(recipeCompound); + RecipeController.syncRecipes.put(recipe.id, recipe); + } + + RecipeController.reloadGlobalRecipes(RecipeController.syncRecipes); + RecipeController.syncRecipes = new HashMap(); + } else if (syncType == SyncType.CARPENTRY_RECIPES) { + NBTTagList list = fullCompound.getTagList("recipes", 10); + if (list == null) + return; + + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound recipeCompound = list.getCompoundTagAt(i); + RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); + recipe.readNBT(recipeCompound); + RecipeController.syncRecipes.put(recipe.id, recipe); + } + + RecipeController.Instance.carpentryRecipes = RecipeController.syncRecipes; + RecipeController.syncRecipes = new HashMap(); + } else if (syncType == SyncType.ANVIL_RECIPES) { + NBTTagList list = fullCompound.getTagList("recipes", 10); + if (list == null) + return; + + for (int i = 0; i < list.tagCount(); i++) { + RecipeAnvil recipe = new RecipeAnvil(); + recipe.readNBT(list.getCompoundTagAt(i)); + RecipeController.syncAnvilRecipes.put(recipe.id, recipe); + } + + RecipeController.Instance.anvilRecipes = RecipeController.syncAnvilRecipes; + RecipeController.syncAnvilRecipes = new HashMap(); + } else if (syncType == SyncType.CUSTOM_EFFECTS) { + NBTTagList list = fullCompound.getTagList("Data", 10); + CustomEffectController ce = CustomEffectController.getInstance(); + ce.customEffectsSync.clear(); + for (int i = 0; i < list.tagCount(); i++) { + CustomEffect effect = new CustomEffect(); + effect.readFromNBT(list.getCompoundTagAt(i)); + ClientCacheHandler.getImageData(effect.icon); + ce.customEffectsSync.put(effect.id, effect); + } + + ce.getCustomEffects().clear(); + ce.getCustomEffects().putAll(ce.customEffectsSync); + ce.customEffectsSync.clear(); + } else if (syncType == SyncType.CUSTOM_ABILITY) { + NBTTagList list = fullCompound.getTagList("Data", 10); + LinkedHashMap sync = new LinkedHashMap<>(); + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound nbt = list.getCompoundTagAt(i); + String name = nbt.getString("CustomAbilityId"); + Ability ability = AbilityController.Instance.fromNBT(nbt); + if (ability != null) { + ability.setName(name); + sync.put(name, ability); + } + } + AbilityController.Instance.setCustomAbilities(sync); + } else if (syncType == SyncType.CHAINED_ABILITY) { + NBTTagList list = fullCompound.getTagList("Data", 10); + LinkedHashMap sync = new LinkedHashMap<>(); + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound nbt = list.getCompoundTagAt(i); + String id = nbt.getString("ChainedAbilityId"); + ChainedAbility chain = new ChainedAbility(); + chain.readNBT(nbt); + chain.setName(id); + sync.put(id, chain); + } + AbilityController.Instance.setChainedAbilities(sync); } - ClientCacheHandler.updateClientRevision(enumSyncType, revision); + ClientCacheHandler.updateClientRevision(syncType, revision); } - public static void syncUpdate(EnumSyncType type, int cat, NBTTagCompound compound) { - Map revisions = invalidateCaches(type); + @SideOnly(Side.CLIENT) + public static void clientUpdate(SyncType syncType, int category_id, int revision, NBTTagCompound compound) { + if (syncType == SyncType.FACTION) { + Faction faction = new Faction(); + faction.readNBT(compound); + FactionController.getInstance().factions.put(faction.id, faction); + } else if (syncType == SyncType.DIALOG) { + DialogCategory category = DialogController.Instance.categories.get(category_id); + Dialog dialog = new Dialog(); + dialog.category = category; + dialog.readNBT(compound); + DialogController.Instance.dialogs.put(dialog.id, dialog); + category.dialogs.put(dialog.id, dialog); + } else if (syncType == SyncType.QUEST) { + QuestCategory category = QuestController.Instance.categories.get(category_id); + Quest quest = new Quest(); + quest.category = category; + quest.readNBT(compound); + QuestController.Instance.quests.put(quest.id, quest); + category.quests.put(quest.id, quest); + } else if (syncType == SyncType.QUEST_CATEGORY) { + QuestCategory category = new QuestCategory(); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + NBTTagList list = compound.getTagList("Data", 10); + if (QuestController.Instance.categoriesSync.containsKey(category.id)) { + category = QuestController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < list.tagCount(); i++) { + Quest quest = new Quest(); + quest.readNBT(list.getCompoundTagAt(i)); + quest.category = category; + category.quests.put(quest.id, quest); + } + QuestController.Instance.categories.put(category.id, category); + } else if (syncType == SyncType.DIALOG_CATEGORY) { + DialogCategory category = new DialogCategory(); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + NBTTagList list = compound.getTagList("Data", 10); + if (DialogController.Instance.categoriesSync.containsKey(category.id)) { + category = DialogController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < list.tagCount(); i++) { + Dialog dialog = new Dialog(); + dialog.readNBT(list.getCompoundTagAt(i)); + dialog.category = category; + category.dialogs.put(dialog.id, dialog); + } + DialogController.Instance.categories.put(category.id, category); + } else if (syncType == SyncType.MAGIC_CYCLE) { + MagicCycle cycle = new MagicCycle(); + cycle.readNBT(compound); + MagicController.getInstance().cycles.put(cycle.id, cycle); + } else if (syncType == SyncType.MAGIC) { + Magic magic = new Magic(); + magic.readNBT(compound); + MagicController.getInstance().magics.put(magic.id, magic); + } else if (syncType == SyncType.PLAYERDATA) { + ClientCacheHandler.playerData.setSyncNBT(compound); + } else if (syncType == SyncType.CUSTOM_EFFECTS) { + CustomEffect effect = new CustomEffect(); + effect.readFromNBT(compound); + ClientCacheHandler.getImageData(effect.icon); + + CustomEffectController.Instance.getCustomEffects().put(effect.id, effect); + } + + ClientCacheHandler.updateClientRevision(syncType, revision); + } + + public static void syncUpdate(SyncType type, int cat, NBTTagCompound compound) { + Map revisions = invalidateCaches(type); int revision = revisions.getOrDefault(type, getCurrentRevision(type)); PacketHandler.Instance.sendToAll(new SyncPacket( type, @@ -823,50 +777,41 @@ public class SyncController { } @SideOnly(Side.CLIENT) - public static void clientSyncRemove(EnumSyncType enumSyncType, int id, int revision) { - switch (enumSyncType) { - case FACTION: - FactionController.getInstance().factions.remove(id); - break; - case DIALOG: - Dialog dialog = DialogController.Instance.dialogs.remove(id); - if (dialog != null) { - dialog.category.dialogs.remove(id); - } - break; - case DIALOG_CATEGORY: - DialogCategory dialogCategory = DialogController.Instance.categories.remove(id); - if (dialogCategory != null) { - DialogController.Instance.dialogs.keySet().removeAll(dialogCategory.dialogs.keySet()); - } - break; - case QUEST: - Quest quest = QuestController.Instance.quests.remove(id); - if (quest != null) { - quest.category.quests.remove(id); - } - break; - case QUEST_CATEGORY: - QuestCategory questCategory = QuestController.Instance.categories.remove(id); - if (questCategory != null) { - QuestController.Instance.quests.keySet().removeAll(questCategory.quests.keySet()); - } - break; - case MAGIC: - for (MagicCycle cycle : MagicController.getInstance().cycles.values()) { - cycle.associations.remove(id); - } - MagicController.getInstance().cycles.remove(id); - break; - case MAGIC_CYCLE: - MagicController.getInstance().cycles.remove(id); - break; - case CUSTOM_EFFECTS: - CustomEffectController.Instance.getCustomEffects().remove(id); - break; + public static void clientSyncRemove(SyncType syncType, int id, int revision) { + if (syncType == SyncType.FACTION) { + FactionController.getInstance().factions.remove(id); + } else if (syncType == SyncType.DIALOG) { + Dialog dialog = DialogController.Instance.dialogs.remove(id); + if (dialog != null) { + dialog.category.dialogs.remove(id); + } + } else if (syncType == SyncType.DIALOG_CATEGORY) { + DialogCategory dialogCategory = DialogController.Instance.categories.remove(id); + if (dialogCategory != null) { + DialogController.Instance.dialogs.keySet().removeAll(dialogCategory.dialogs.keySet()); + } + } else if (syncType == SyncType.QUEST) { + Quest quest = QuestController.Instance.quests.remove(id); + if (quest != null) { + quest.category.quests.remove(id); + } + } else if (syncType == SyncType.QUEST_CATEGORY) { + QuestCategory questCategory = QuestController.Instance.categories.remove(id); + if (questCategory != null) { + QuestController.Instance.quests.keySet().removeAll(questCategory.quests.keySet()); + } + } else if (syncType == SyncType.MAGIC) { + for (MagicCycle cycle : MagicController.getInstance().cycles.values()) { + cycle.associations.remove(id); + } + MagicController.getInstance().cycles.remove(id); + } else if (syncType == SyncType.MAGIC_CYCLE) { + MagicController.getInstance().cycles.remove(id); + } else if (syncType == SyncType.CUSTOM_EFFECTS) { + CustomEffectController.Instance.getCustomEffects().remove(id); } - ClientCacheHandler.updateClientRevision(enumSyncType, revision); + ClientCacheHandler.updateClientRevision(syncType, revision); } public static void syncEffects(EntityPlayerMP playerMP) { @@ -912,11 +857,11 @@ public class SyncController { } } - private static void registerCache(EnumSyncType type, Supplier supplier) { - cacheEntries.put(type, new SyncCacheEntry(type, supplier)); + private static void registerCache(SyncType type, Supplier supplier) { + cacheEntries.put(type, new SyncCacheEntry(supplier)); } - private static CachedSyncPayload rebuildNow(EnumSyncType type) { + private static CachedSyncPayload rebuildNow(SyncType type) { SyncCacheEntry entry = cacheEntries.get(type); if (entry == null) { return null; @@ -925,10 +870,10 @@ public class SyncController { return entry.getPayload(type); } - public static Map invalidateCaches(EnumSyncType type) { - EnumSet targets = getInvalidationTargets(type); - EnumMap revisions = new EnumMap<>(EnumSyncType.class); - for (EnumSyncType target : targets) { + public static Map invalidateCaches(SyncType type) { + Set targets = getInvalidationTargets(type); + Map revisions = new LinkedHashMap<>(); + for (SyncType target : targets) { SyncCacheEntry entry = cacheEntries.get(target); if (entry != null) { int newRevision = entry.invalidate(); @@ -938,38 +883,56 @@ public class SyncController { 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); - case CUSTOM_ABILITY: - return EnumSet.of(EnumSyncType.CUSTOM_ABILITY); - case CHAINED_ABILITY: - return EnumSet.of(EnumSyncType.CHAINED_ABILITY); - default: - return EnumSet.noneOf(EnumSyncType.class); + private static Set getInvalidationTargets(SyncType type) { + if (type == SyncType.DIALOG || type == SyncType.DIALOG_CATEGORY) { + Set set = new HashSet<>(); + set.add(SyncType.DIALOG_CATEGORY); + return set; + } else if (type == SyncType.QUEST || type == SyncType.QUEST_CATEGORY) { + Set set = new HashSet<>(); + set.add(SyncType.QUEST_CATEGORY); + return set; + } else if (type == SyncType.FACTION) { + Set set = new HashSet<>(); + set.add(SyncType.FACTION); + return set; + } else if (type == SyncType.MAGIC) { + Set set = new HashSet<>(); + set.add(SyncType.MAGIC); + return set; + } else if (type == SyncType.MAGIC_CYCLE) { + Set set = new HashSet<>(); + set.add(SyncType.MAGIC_CYCLE); + return set; + } else if (type == SyncType.WORKBENCH_RECIPES) { + Set set = new HashSet<>(); + set.add(SyncType.WORKBENCH_RECIPES); + return set; + } else if (type == SyncType.CARPENTRY_RECIPES) { + Set set = new HashSet<>(); + set.add(SyncType.CARPENTRY_RECIPES); + return set; + } else if (type == SyncType.ANVIL_RECIPES) { + Set set = new HashSet<>(); + set.add(SyncType.ANVIL_RECIPES); + return set; + } else if (type == SyncType.CUSTOM_EFFECTS) { + Set set = new HashSet<>(); + set.add(SyncType.CUSTOM_EFFECTS); + return set; + } else if (type == SyncType.CUSTOM_ABILITY) { + Set set = new HashSet<>(); + set.add(SyncType.CUSTOM_ABILITY); + return set; + } else if (type == SyncType.CHAINED_ABILITY) { + Set set = new HashSet<>(); + set.add(SyncType.CHAINED_ABILITY); + return set; } + return Collections.emptySet(); } - private static void updateAllPlayerRevisions(EnumSyncType type, int revision) { + private static void updateAllPlayerRevisions(SyncType type, int revision) { if (revision < 0) { return; } @@ -978,12 +941,12 @@ public class SyncController { } } - private static void updateAllPlayerRevisions(Map revisions) { + private static void updateAllPlayerRevisions(Map revisions) { if (revisions.isEmpty()) { return; } for (PlayerSyncState state : playerSyncState.values()) { - for (Map.Entry entry : revisions.entrySet()) { + for (Map.Entry entry : revisions.entrySet()) { state.updateRevision(entry.getKey(), entry.getValue()); } } @@ -1019,11 +982,11 @@ public class SyncController { private int revision = 0; private boolean dirty = true; - private SyncCacheEntry(EnumSyncType type, Supplier supplier) { + private SyncCacheEntry(Supplier supplier) { this.supplier = supplier; } - private synchronized CachedSyncPayload getPayload(EnumSyncType requestedType) { + private synchronized CachedSyncPayload getPayload(SyncType requestedType) { if (!dirty && payload != null) { return payload; } @@ -1089,21 +1052,21 @@ public class SyncController { } private static final class PlayerSyncState { - private final EnumMap revisions = new EnumMap<>(EnumSyncType.class); + private final Map revisions = new HashMap<>(); private PlayerSyncState(UUID ignored) { } - private synchronized int getRevision(EnumSyncType type) { + private synchronized int getRevision(SyncType type) { Integer value = revisions.get(type); return value == null ? -1 : value; } - private synchronized void updateRevision(EnumSyncType type, int revision) { + private synchronized void updateRevision(SyncType type, int revision) { revisions.put(type, revision); } - private synchronized void applyHandshake(Map incoming) { + private synchronized void applyHandshake(Map incoming) { if (incoming == null || incoming.isEmpty()) { return; } diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java b/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java deleted file mode 100644 index ee6cf352f..000000000 --- a/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java +++ /dev/null @@ -1,18 +0,0 @@ -package kamkeel.npcs.network.enums; - -public enum EnumSyncType { - FACTION, - DIALOG, - DIALOG_CATEGORY, - QUEST, - QUEST_CATEGORY, - PLAYERDATA, - MAGIC, - MAGIC_CYCLE, - WORKBENCH_RECIPES, - CARPENTRY_RECIPES, - ANVIL_RECIPES, - CUSTOM_EFFECTS, - CUSTOM_ABILITY, - CHAINED_ABILITY -} diff --git a/src/main/java/kamkeel/npcs/network/enums/SyncType.java b/src/main/java/kamkeel/npcs/network/enums/SyncType.java new file mode 100644 index 000000000..2623038ac --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/enums/SyncType.java @@ -0,0 +1,178 @@ +package kamkeel.npcs.network.enums; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Immutable, object-based sync-type identity for the CNPC+ sync backend. + * + *

Each {@code SyncType} has a canonical namespaced string ID ({@code modid:path}) + * and is registered via the static {@link #register} factory. Built-in CNPC+ + * families are exposed as {@code public static final} constants.

+ * + *

Identity is based on the canonical {@link #id} string, which is unique + * across all registered types. Two references with the same {@code id} + * are always the same object (identity == equality for registered types).

+ * + *

The {@code ordinal} field provides a stable integer identity for + * wire encoding during the migration period. It is assigned at registration + * time in declaration order and must NOT be used as a protocol identifier + * once string-on-wire migration is complete.

+ */ +public final class SyncType { + + // ──────────────────── Registry ──────────────────── + private static final Map REGISTRY = new LinkedHashMap<>(); + private static int nextOrdinal = 0; + private static boolean frozen = false; + + // ──────────────────── CNPC+ families ──────────────────── + public static final SyncType FACTION = register("cnpc:faction"); + public static final SyncType DIALOG = register("cnpc:dialog"); + public static final SyncType DIALOG_CATEGORY = register("cnpc:dialog_category"); + public static final SyncType QUEST = register("cnpc:quest"); + public static final SyncType QUEST_CATEGORY = register("cnpc:quest_category"); + public static final SyncType PLAYERDATA = register("cnpc:playerdata"); + public static final SyncType MAGIC = register("cnpc:magic"); + public static final SyncType MAGIC_CYCLE = register("cnpc:magic_cycle"); + public static final SyncType WORKBENCH_RECIPES = register("cnpc:workbench_recipes"); + public static final SyncType CARPENTRY_RECIPES = register("cnpc:carpentry_recipes"); + public static final SyncType ANVIL_RECIPES = register("cnpc:anvil_recipes"); + public static final SyncType CUSTOM_EFFECTS = register("cnpc:custom_effects"); + public static final SyncType CUSTOM_ABILITY = register("cnpc:custom_ability"); + public static final SyncType CHAINED_ABILITY = register("cnpc:chained_ability"); + + // ──────────────────── Instance fields ──────────────────── + private final String id; + private final int ordinal; + + private SyncType(String id, int ordinal) { + this.id = id; + this.ordinal = ordinal; + } + + // ──────────────────── Public API ──────────────────── + + /** + * The canonical namespaced identifier, e.g. {@code "cnpc:faction"}. + */ + public String getId() { + return id; + } + + /** + * Stable integer ordinal assigned at registration time. + * Used for wire encoding during the migration period only. + */ + public int ordinal() { + return ordinal; + } + + // ──────────────────── Registration ──────────────────── + + /** + * Register a new {@code SyncType} with the given canonical ID. + * + * @param id namespaced identifier ({@code modid:path}) + * @return the newly created and registered {@code SyncType} + * @throws IllegalArgumentException if the ID is null, empty, or lacks a colon + * @throws IllegalStateException if a type with the same ID is already registered + * @throws IllegalStateException if the registry has been frozen + */ + public static SyncType register(String id) { + if (id == null || id.isEmpty()) { + throw new IllegalArgumentException("SyncType id must not be null or empty"); + } + if (!id.contains(":")) { + throw new IllegalArgumentException("SyncType id must be namespaced (modid:path): " + id); + } + if (frozen) { + throw new IllegalStateException("SyncType registry is frozen; cannot register: " + id); + } + if (REGISTRY.containsKey(id)) { + throw new IllegalStateException("Duplicate SyncType registration: " + id); + } + SyncType type = new SyncType(id, nextOrdinal++); + REGISTRY.put(id, type); + return type; + } + + /** + * Look up a registered SyncType by its canonical string ID. + * + * @return the registered type, or {@code null} if not found + */ + public static SyncType byId(String id) { + return id == null ? null : REGISTRY.get(id); + } + + /** + * Look up a registered SyncType by its ordinal. + * Used during migration for decoding ordinal-based wire traffic. + * + * @return the registered type, or {@code null} if ordinal is out of range + */ + public static SyncType byOrdinal(int ordinal) { + if (ordinal < 0) { + return null; + } + for (SyncType type : REGISTRY.values()) { + if (type.ordinal == ordinal) { + return type; + } + } + return null; + } + + /** + * Returns all registered SyncType instances in registration order. + */ + public static Collection values() { + return Collections.unmodifiableCollection(REGISTRY.values()); + } + + /** + * Freeze the registry. After freezing, no new types can be registered. + * Should be called after all mods have had a chance to register types. + */ + public static void freeze() { + frozen = true; + } + + /** + * Whether the registry has been frozen. + */ + public static boolean isFrozen() { + return frozen; + } + + /** + * Unfreeze the registry. Called during world reload to allow addon + * re-registration. Does NOT clear existing registrations — built-in + * constants stay stable. + */ + public static void unfreeze() { + frozen = false; + } + + // ──────────────────── Object overrides ──────────────────── + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SyncType)) return false; + return id.equals(((SyncType) o).id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public String toString() { + return id; + } +} 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 da9eb9aa2..103c2e5fa 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java @@ -8,7 +8,7 @@ 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.enums.SyncType; import kamkeel.npcs.network.packets.player.SyncRevisionInfoPacket; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; @@ -21,19 +21,20 @@ import noppes.npcs.controllers.AuctionConfigSync; import noppes.npcs.controllers.ScriptConfigSync; import java.io.IOException; -import java.util.EnumMap; +import java.util.HashMap; +import java.util.LinkedHashMap; 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); + private final Map revisionSnapshot = new LinkedHashMap<>(); public LoginPacket() { } - public LoginPacket(String serverCacheKey, EnumMap revisionSnapshot) { + public LoginPacket(String serverCacheKey, Map revisionSnapshot) { if (serverCacheKey != null) { this.serverCacheKey = serverCacheKey; } @@ -58,17 +59,15 @@ public final class LoginPacket extends AbstractPacket { out.writeBoolean(ConfigMain.ProfilesEnabled); ByteBufUtils.writeString(out, serverCacheKey); out.writeShort(revisionSnapshot.size()); - for (Map.Entry entry : revisionSnapshot.entrySet()) { + for (Map.Entry entry : revisionSnapshot.entrySet()) { out.writeInt(entry.getKey().ordinal()); out.writeInt(entry.getValue()); } - // Send auction config (server-side sync) NBTTagCompound auctionConfig = new NBTTagCompound(); AuctionConfigSync.writeToNBT(auctionConfig); ByteBufUtils.writeNBT(out, auctionConfig); - // Send script config (server-side sync) NBTTagCompound scriptConfig = new NBTTagCompound(); ScriptConfigSync.writeToNBT(scriptConfig); ByteBufUtils.writeNBT(out, scriptConfig); @@ -82,20 +81,20 @@ public final class LoginPacket extends AbstractPacket { String serverKey = ByteBufUtils.readString(in); int revisionCount = in.readUnsignedShort(); - EnumMap serverRevisions = new EnumMap<>(EnumSyncType.class); + Map serverRevisions = new LinkedHashMap<>(); for (int i = 0; i < revisionCount; i++) { - EnumSyncType type = EnumSyncType.values()[in.readInt()]; + SyncType type = SyncType.byOrdinal(in.readInt()); int revision = in.readInt(); - serverRevisions.put(type, revision); + if (type != null) { + serverRevisions.put(type, revision); + } } ClientCacheHandler.setActiveServer(serverKey, serverRevisions); - // Read auction config NBTTagCompound auctionConfig = ByteBufUtils.readNBT(in); AuctionClientConfig.readFromNBT(auctionConfig); - // Read script config NBTTagCompound scriptConfig = ByteBufUtils.readNBT(in); ScriptClientConfig.readFromNBT(scriptConfig); 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 77b2122b4..322f6c976 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 @@ -10,7 +10,7 @@ import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumDataPacket; import kamkeel.npcs.network.enums.EnumSyncAction; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; @@ -22,13 +22,9 @@ 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 - * along with a SyncAction (RELOAD, UPDATE, REMOVE). - */ public final class SyncPacket extends LargeAbstractPacket { - private EnumSyncType enumSyncType; + private SyncType syncType; private EnumSyncAction enumSyncAction; private NBTTagCompound syncData; private int operationID; @@ -39,23 +35,20 @@ public final class SyncPacket extends LargeAbstractPacket { public SyncPacket() { } - /** - * Constructs a new LargeSyncPacket. - */ - public SyncPacket(EnumSyncType enumSyncType, EnumSyncAction enumSyncAction, int catId, NBTTagCompound syncData) { - this(enumSyncType, enumSyncAction, catId, -1, syncData); + public SyncPacket(SyncType syncType, EnumSyncAction enumSyncAction, int catId, NBTTagCompound syncData) { + this(syncType, enumSyncAction, catId, -1, syncData); } - public SyncPacket(EnumSyncType enumSyncType, EnumSyncAction enumSyncAction, int catId, int revision, NBTTagCompound syncData) { - this.enumSyncType = enumSyncType; + public SyncPacket(SyncType syncType, EnumSyncAction enumSyncAction, int catId, int revision, NBTTagCompound syncData) { + this.syncType = syncType; this.enumSyncAction = enumSyncAction; this.syncData = syncData; this.operationID = catId; this.revision = revision; } - public SyncPacket(EnumSyncType enumSyncType, SyncController.CachedSyncPayload payload) { - this.enumSyncType = enumSyncType; + public SyncPacket(SyncType syncType, SyncController.CachedSyncPayload payload) { + this.syncType = syncType; this.enumSyncAction = EnumSyncAction.RELOAD; this.operationID = -1; this.revision = payload.getRevision(); @@ -81,7 +74,7 @@ public final class SyncPacket extends LargeAbstractPacket { ByteBuf buffer = Unpooled.buffer(); try { - buffer.writeInt(enumSyncType.ordinal()); + buffer.writeInt(syncType.ordinal()); buffer.writeInt(enumSyncAction.ordinal()); buffer.writeInt(operationID); buffer.writeInt(revision); @@ -131,17 +124,19 @@ public final class SyncPacket extends LargeAbstractPacket { if (CustomNpcs.side() != Side.CLIENT) return; - // Reconstruct everything from the ByteBuf int syncTypeOrdinal = data.readInt(); int syncActionOrdinal = data.readInt(); int categoryID = data.readInt(); int incomingRevision = data.readInt(); - EnumSyncType type = EnumSyncType.values()[syncTypeOrdinal]; + SyncType type = SyncType.byOrdinal(syncTypeOrdinal); + if (type == null) { + LogWriter.error("[SyncPacket] Unknown sync type ordinal: " + syncTypeOrdinal + "; skipping"); + return; + } 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, incomingRevision, tag); } catch (RuntimeException e) { LogWriter.error(String.format("Attempted to Sync %s but it was too big", type.toString())); @@ -149,7 +144,7 @@ public final class SyncPacket extends LargeAbstractPacket { } private void handleSyncPacketClient( - EnumSyncType enumSyncType, + SyncType syncType, EnumSyncAction enumSyncAction, int id, int incomingRevision, @@ -157,13 +152,13 @@ public final class SyncPacket extends LargeAbstractPacket { ) { switch (enumSyncAction) { case RELOAD: - SyncController.clientSync(enumSyncType, incomingRevision, data); + SyncController.clientSync(syncType, incomingRevision, data); break; case UPDATE: - SyncController.clientUpdate(enumSyncType, id, incomingRevision, data); + SyncController.clientUpdate(syncType, id, incomingRevision, data); break; case REMOVE: - SyncController.clientSyncRemove(enumSyncType, id, incomingRevision); + SyncController.clientSyncRemove(syncType, 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 index 769b086b7..25824e15a 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/SyncRevisionInfoPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/SyncRevisionInfoPacket.java @@ -8,13 +8,13 @@ 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.network.enums.SyncType; 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.LinkedHashMap; import java.util.Map; public class SyncRevisionInfoPacket extends AbstractPacket { @@ -23,12 +23,12 @@ public class SyncRevisionInfoPacket extends AbstractPacket { private String serverKey = ""; private String previousServerKey = ""; - private final EnumMap revisions = new EnumMap<>(EnumSyncType.class); + private final Map revisions = new LinkedHashMap<>(); public SyncRevisionInfoPacket() { } - public SyncRevisionInfoPacket(String serverKey, String previousServerKey, EnumMap revisions) { + public SyncRevisionInfoPacket(String serverKey, String previousServerKey, Map revisions) { if (serverKey != null) { this.serverKey = serverKey; } @@ -56,7 +56,7 @@ public class SyncRevisionInfoPacket extends AbstractPacket { ByteBufUtils.writeString(out, serverKey); ByteBufUtils.writeString(out, previousServerKey); out.writeShort(revisions.size()); - for (Map.Entry entry : revisions.entrySet()) { + for (Map.Entry entry : revisions.entrySet()) { out.writeInt(entry.getKey().ordinal()); out.writeInt(entry.getValue()); } @@ -71,11 +71,13 @@ public class SyncRevisionInfoPacket extends AbstractPacket { String incomingServerKey = ByteBufUtils.readString(in); String incomingPreviousKey = ByteBufUtils.readString(in); int revisionCount = in.readUnsignedShort(); - EnumMap incomingRevisions = new EnumMap<>(EnumSyncType.class); + Map incomingRevisions = new LinkedHashMap<>(); for (int i = 0; i < revisionCount; i++) { - EnumSyncType type = EnumSyncType.values()[in.readInt()]; + SyncType type = SyncType.byOrdinal(in.readInt()); int revision = in.readInt(); - incomingRevisions.put(type, revision); + if (type != null) { + incomingRevisions.put(type, revision); + } } SyncController.handleClientRevisionReport((EntityPlayerMP) player, incomingServerKey, incomingPreviousKey, incomingRevisions); diff --git a/src/main/java/noppes/npcs/ServerEventsHandler.java b/src/main/java/noppes/npcs/ServerEventsHandler.java index 04e0406d8..a8c5f2b4a 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -10,7 +10,7 @@ import kamkeel.npcs.entity.EntityEnergyDome; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumSoundOperation; import kamkeel.npcs.network.enums.EnumSyncAction; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import kamkeel.npcs.network.packets.data.ClonerPacket; import kamkeel.npcs.network.packets.data.MarkDataPacket; import kamkeel.npcs.network.packets.data.SoundManagementPacket; @@ -255,19 +255,19 @@ public class ServerEventsHandler { if (block == Blocks.crafting_table && event.action == Action.RIGHT_CLICK_BLOCK && !player.worldObj.isRemote) { PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.WORKBENCH_RECIPES, + SyncType.WORKBENCH_RECIPES, EnumSyncAction.RELOAD, -1, - SyncController.getCurrentRevision(EnumSyncType.WORKBENCH_RECIPES), + SyncController.getCurrentRevision(SyncType.WORKBENCH_RECIPES), SyncController.workbenchNBT() ), (EntityPlayerMP) player); } if (block == CustomItems.carpentyBench && event.action == Action.RIGHT_CLICK_BLOCK && !player.worldObj.isRemote) { PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.CARPENTRY_RECIPES, + SyncType.CARPENTRY_RECIPES, EnumSyncAction.RELOAD, -1, - SyncController.getCurrentRevision(EnumSyncType.CARPENTRY_RECIPES), + SyncController.getCurrentRevision(SyncType.CARPENTRY_RECIPES), SyncController.carpentryNBT() ), (EntityPlayerMP) player); } diff --git a/src/main/java/noppes/npcs/client/ClientCacheHandler.java b/src/main/java/noppes/npcs/client/ClientCacheHandler.java index 3386ffb43..53dfb9959 100644 --- a/src/main/java/noppes/npcs/client/ClientCacheHandler.java +++ b/src/main/java/noppes/npcs/client/ClientCacheHandler.java @@ -1,6 +1,6 @@ package noppes.npcs.client; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; import noppes.npcs.client.controllers.MusicController; @@ -18,8 +18,8 @@ import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.controllers.data.SkinOverlay; import noppes.npcs.util.CacheHashMap; -import java.util.EnumMap; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; @@ -32,14 +32,14 @@ public class ClientCacheHandler { private static String activeServerKey = ""; private static String lastServerKey = ""; - private static final Map> clientRevisionCache = new HashMap<>(); + 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) { + public static void setActiveServer(String serverKey, Map serverRevisions) { String normalizedKey = serverKey == null ? "" : serverKey; String previousKey = activeServerKey == null ? "" : activeServerKey; lastServerKey = previousKey; @@ -54,9 +54,9 @@ public class ClientCacheHandler { return; } - EnumMap cached = clientRevisionCache.computeIfAbsent( + Map cached = clientRevisionCache.computeIfAbsent( activeServerKey, - ignored -> new EnumMap<>(EnumSyncType.class) + ignored -> new LinkedHashMap<>() ); if (serverRevisions != null && !serverRevisions.isEmpty()) { cached.keySet().retainAll(serverRevisions.keySet()); @@ -67,19 +67,19 @@ public class ClientCacheHandler { return lastServerKey == null ? "" : lastServerKey; } - public static EnumMap getCachedRevisionsForServer(String serverKey) { + public static Map getCachedRevisionsForServer(String serverKey) { String key = serverKey == null ? "" : serverKey; if (key.isEmpty()) { - return new EnumMap<>(EnumSyncType.class); + return new LinkedHashMap<>(); } - EnumMap revisions = clientRevisionCache.get(key); + Map revisions = clientRevisionCache.get(key); if (revisions == null) { - return new EnumMap<>(EnumSyncType.class); + return new LinkedHashMap<>(); } - return new EnumMap<>(revisions); + return new LinkedHashMap<>(revisions); } - public static void updateClientRevision(EnumSyncType type, int revision) { + public static void updateClientRevision(SyncType type, int revision) { if (revision < 0) { return; } @@ -87,9 +87,9 @@ public class ClientCacheHandler { if (key.isEmpty()) { return; } - EnumMap revisions = clientRevisionCache.computeIfAbsent( + Map revisions = clientRevisionCache.computeIfAbsent( key, - ignored -> new EnumMap<>(EnumSyncType.class) + ignored -> new LinkedHashMap<>() ); revisions.put(type, revision); } @@ -126,14 +126,11 @@ public class ClientCacheHandler { ClientCacheHandler.playerAnimations.clear(); ProfileClientConfig.reset(); - // Clear Texture Caches GuiSoundSelection.cachedDomains.clear(); GuiTextureSelection.cachedTextures.clear(); - // Clear music/bard sounds MusicController.Instance.stopAllSounds(); - // Clear Quest Tracker HudComponent component = ClientHudManager.getInstance().getHudComponents().get(EnumHudComponent.QuestTracker); if (component != null) { component.loadData(new NBTTagCompound()); @@ -146,7 +143,6 @@ public class ClientCacheHandler { ClientCacheHandler.customOverlays.clear(); ClientCacheHandler.skinOverlays.clear(); - // Clear Texture Caches GuiSoundSelection.cachedDomains.clear(); GuiTextureSelection.cachedTextures.clear(); } diff --git a/src/main/java/noppes/npcs/controllers/CustomEffectController.java b/src/main/java/noppes/npcs/controllers/CustomEffectController.java index 640f544c4..6a465b0ee 100644 --- a/src/main/java/noppes/npcs/controllers/CustomEffectController.java +++ b/src/main/java/noppes/npcs/controllers/CustomEffectController.java @@ -1,7 +1,7 @@ package noppes.npcs.controllers; import kamkeel.npcs.controllers.SyncController; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; @@ -128,7 +128,7 @@ public class CustomEffectController implements ICustomEffectHandler { file.delete(); } categoryManager.removeItem(id); - SyncController.syncRemove(EnumSyncType.CUSTOM_EFFECTS, foundEffect.getID()); + SyncController.syncRemove(SyncType.CUSTOM_EFFECTS, foundEffect.getID()); saveEffectLoadMap(); } } @@ -700,7 +700,7 @@ public class CustomEffectController implements ICustomEffectHandler { file2.delete(); file.renameTo(file2); nbtTagCompound.removeTag("ScriptData"); - SyncController.syncUpdate(EnumSyncType.CUSTOM_EFFECTS, -1, nbtTagCompound); + SyncController.syncUpdate(SyncType.CUSTOM_EFFECTS, -1, nbtTagCompound); } catch (Exception e) { LogWriter.except(e); } diff --git a/src/main/java/noppes/npcs/controllers/DialogController.java b/src/main/java/noppes/npcs/controllers/DialogController.java index 3490735f3..0e2df3d84 100644 --- a/src/main/java/noppes/npcs/controllers/DialogController.java +++ b/src/main/java/noppes/npcs/controllers/DialogController.java @@ -1,7 +1,7 @@ package noppes.npcs.controllers; import kamkeel.npcs.controllers.SyncController; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -239,7 +239,7 @@ public class DialogController implements IDialogHandler { dir.mkdirs(); } categories.put(category.id, category); - SyncController.syncUpdate(EnumSyncType.DIALOG_CATEGORY, -1, SyncController.updateDialogCat(category)); + SyncController.syncUpdate(SyncType.DIALOG_CATEGORY, -1, SyncController.updateDialogCat(category)); } public void removeCategory(int category) { @@ -252,7 +252,7 @@ public class DialogController implements IDialogHandler { for (int dia : cat.dialogs.keySet()) dialogs.remove(dia); categories.remove(category); - SyncController.syncRemove(EnumSyncType.DIALOG_CATEGORY, category); + SyncController.syncRemove(SyncType.DIALOG_CATEGORY, category); } private boolean containsCategoryName(String name) { @@ -302,7 +302,7 @@ public class DialogController implements IDialogHandler { if (file2.exists()) file2.delete(); file.renameTo(file2); - SyncController.syncUpdate(EnumSyncType.DIALOG, category.id, dialog.writeToNBT(new NBTTagCompound())); + SyncController.syncUpdate(SyncType.DIALOG, category.id, dialog.writeToNBT(new NBTTagCompound())); } catch (Exception e) { LogWriter.except(e); } @@ -316,7 +316,7 @@ public class DialogController implements IDialogHandler { return; category.dialogs.remove(dialog.id); dialogs.remove(dialog.id); - SyncController.syncRemove(EnumSyncType.DIALOG, dialog.id); + SyncController.syncRemove(SyncType.DIALOG, dialog.id); } private File getDir() { diff --git a/src/main/java/noppes/npcs/controllers/FactionController.java b/src/main/java/noppes/npcs/controllers/FactionController.java index 9ddf860ea..7da2ebe67 100644 --- a/src/main/java/noppes/npcs/controllers/FactionController.java +++ b/src/main/java/noppes/npcs/controllers/FactionController.java @@ -1,7 +1,7 @@ package noppes.npcs.controllers; import kamkeel.npcs.controllers.SyncController; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -159,7 +159,7 @@ public class FactionController implements IFactionHandler { NBTTagCompound facCompound = new NBTTagCompound(); faction.writeNBT(facCompound); - SyncController.syncUpdate(EnumSyncType.FACTION, -1, facCompound); + SyncController.syncUpdate(SyncType.FACTION, -1, facCompound); saveFactions(); } @@ -195,7 +195,7 @@ public class FactionController implements IFactionHandler { } else { this.saveFactions(); faction.id = -1; - SyncController.syncRemove(EnumSyncType.FACTION, id); + SyncController.syncRemove(SyncType.FACTION, id); return faction; } } else { diff --git a/src/main/java/noppes/npcs/controllers/MagicController.java b/src/main/java/noppes/npcs/controllers/MagicController.java index 40febf172..dff89d015 100644 --- a/src/main/java/noppes/npcs/controllers/MagicController.java +++ b/src/main/java/noppes/npcs/controllers/MagicController.java @@ -1,7 +1,7 @@ package noppes.npcs.controllers; import kamkeel.npcs.controllers.SyncController; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import net.minecraft.item.ItemStack; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; @@ -275,7 +275,7 @@ public class MagicController implements IMagicHandler { NBTTagCompound magicCompound = new NBTTagCompound(); mag.writeNBT(magicCompound); - SyncController.syncUpdate(EnumSyncType.MAGIC, -1, magicCompound); + SyncController.syncUpdate(SyncType.MAGIC, -1, magicCompound); saveMagicData(); } @@ -283,7 +283,7 @@ public class MagicController implements IMagicHandler { public void removeMagic(int magicID) { if (magics.containsKey(magicID)) { magics.remove(magicID); - SyncController.syncRemove(EnumSyncType.MAGIC, magicID); + SyncController.syncRemove(SyncType.MAGIC, magicID); saveMagicData(); } } @@ -345,7 +345,7 @@ public class MagicController implements IMagicHandler { NBTTagCompound cycleCompound = new NBTTagCompound(); cycle.writeNBT(cycleCompound); - SyncController.syncUpdate(EnumSyncType.MAGIC_CYCLE, -1, cycleCompound); + SyncController.syncUpdate(SyncType.MAGIC_CYCLE, -1, cycleCompound); saveMagicData(); } @@ -353,7 +353,7 @@ public class MagicController implements IMagicHandler { public void removeCycle(int categoryId) { if (cycles.containsKey(categoryId)) { cycles.remove(categoryId); - SyncController.syncRemove(EnumSyncType.MAGIC_CYCLE, categoryId); + SyncController.syncRemove(SyncType.MAGIC_CYCLE, categoryId); saveMagicData(); } } diff --git a/src/main/java/noppes/npcs/controllers/QuestController.java b/src/main/java/noppes/npcs/controllers/QuestController.java index e9b403ee7..59136b2aa 100644 --- a/src/main/java/noppes/npcs/controllers/QuestController.java +++ b/src/main/java/noppes/npcs/controllers/QuestController.java @@ -1,7 +1,7 @@ package noppes.npcs.controllers; import kamkeel.npcs.controllers.SyncController; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -158,7 +158,7 @@ public class QuestController implements IQuestHandler { for (int dia : cat.quests.keySet()) quests.remove(dia); categories.remove(category); - SyncController.syncRemove(EnumSyncType.QUEST_CATEGORY, category); + SyncController.syncRemove(SyncType.QUEST_CATEGORY, category); } public void saveCategory(QuestCategory category) { @@ -188,7 +188,7 @@ public class QuestController implements IQuestHandler { dir.mkdirs(); } categories.put(category.id, category); - SyncController.syncUpdate(EnumSyncType.QUEST_CATEGORY, -1, SyncController.updateQuestCat(category)); + SyncController.syncUpdate(SyncType.QUEST_CATEGORY, -1, SyncController.updateQuestCat(category)); } private boolean containsCategoryName(String name) { @@ -245,7 +245,7 @@ public class QuestController implements IQuestHandler { if (file2.exists()) file2.delete(); file.renameTo(file2); - SyncController.syncUpdate(EnumSyncType.QUEST, category.id, quest.writeToNBT(new NBTTagCompound())); + SyncController.syncUpdate(SyncType.QUEST, category.id, quest.writeToNBT(new NBTTagCompound())); } catch (Exception e) { e.printStackTrace(); } From 6d0655ad7da727db7726154ed11f84733663ae95 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 31 Mar 2026 21:44:23 +0200 Subject: [PATCH 02/34] SyncController OVERHAUL PHASE #2: - Created SyncRegistry and SyncHandler - Extracted all 14 SyncType family handling to their own SyncHandler - Rewritten SyncController to accomodate the new framework --- src/main/java/foxz/command/CmdDialog.java | 3 +- src/main/java/foxz/command/CmdQuest.java | 3 +- .../kamkeel/npcs/command/DialogCommand.java | 3 +- .../kamkeel/npcs/command/QuestCommand.java | 3 +- .../npcs/controllers/AbilityController.java | 9 +- .../npcs/controllers/SyncController.java | 849 +++---------- .../npcs/controllers/SyncControllerOld.java | 1058 +++++++++++++++++ .../controllers/data/profile/CNPCData.java | 4 +- .../npcs/controllers/sync/SyncHandler.java | 135 +++ .../npcs/controllers/sync/SyncRegistry.java | 98 ++ .../sync/handlers/AnvilRecipeSyncHandler.java | 56 + .../handlers/CarpentryRecipeSyncHandler.java | 63 + .../handlers/ChainedAbilitySyncHandler.java | 58 + .../handlers/CustomAbilitySyncHandler.java | 59 + .../handlers/CustomEffectSyncHandler.java | 82 ++ .../handlers/DialogCategorySyncHandler.java | 140 +++ .../sync/handlers/DialogSyncHandler.java | 89 ++ .../sync/handlers/FactionSyncHandler.java | 76 ++ .../sync/handlers/MagicCycleSyncHandler.java | 79 ++ .../sync/handlers/MagicSyncHandler.java | 95 ++ .../handlers/PlayerAbilitySyncHelper.java | 47 + .../sync/handlers/PlayerDataSyncHandler.java | 75 ++ .../sync/handlers/PlayerEffectSyncHelper.java | 54 + .../handlers/QuestCategorySyncHandler.java | 141 +++ .../sync/handlers/QuestSyncHandler.java | 90 ++ .../handlers/WorkbenchRecipeSyncHandler.java | 70 ++ .../packets/data/large/SyncEffectPacket.java | 4 +- .../noppes/npcs/ScriptPlayerEventHandler.java | 10 +- .../java/noppes/npcs/ServerEventsHandler.java | 6 +- .../java/noppes/npcs/ServerTickHandler.java | 7 +- .../npcs/controllers/DialogController.java | 3 +- .../npcs/controllers/QuestController.java | 3 +- .../npcs/controllers/RecipeController.java | 7 +- .../controllers/data/PlayerAbilityData.java | 10 +- 34 files changed, 2743 insertions(+), 746 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/SyncControllerOld.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/SyncHandler.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/SyncRegistry.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/AnvilRecipeSyncHandler.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/CarpentryRecipeSyncHandler.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/ChainedAbilitySyncHandler.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/CustomAbilitySyncHandler.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/CustomEffectSyncHandler.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/DialogCategorySyncHandler.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/DialogSyncHandler.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/FactionSyncHandler.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/MagicCycleSyncHandler.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/MagicSyncHandler.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerAbilitySyncHelper.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerDataSyncHandler.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerEffectSyncHelper.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/QuestCategorySyncHandler.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/QuestSyncHandler.java create mode 100644 src/main/java/kamkeel/npcs/controllers/sync/handlers/WorkbenchRecipeSyncHandler.java diff --git a/src/main/java/foxz/command/CmdDialog.java b/src/main/java/foxz/command/CmdDialog.java index 2bb2d1da3..74ec807fc 100644 --- a/src/main/java/foxz/command/CmdDialog.java +++ b/src/main/java/foxz/command/CmdDialog.java @@ -6,6 +6,7 @@ import foxz.commandhelper.annotations.SubCommand; import foxz.commandhelper.permissions.OpOnly; import foxz.commandhelper.permissions.ParamCheck; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.network.enums.SyncType; import net.minecraft.command.CommandBase; import net.minecraft.entity.player.EntityPlayer; import noppes.npcs.NoppesUtilServer; @@ -90,7 +91,7 @@ public class CmdDialog extends ChMcLogger { ) public boolean reload(String args[]) { new DialogController().load(); - SyncController.syncAllDialogs(); + SyncController.syncAll(SyncType.DIALOG_CATEGORY); return true; } diff --git a/src/main/java/foxz/command/CmdQuest.java b/src/main/java/foxz/command/CmdQuest.java index 5f7a7f1f4..95e4b75f0 100644 --- a/src/main/java/foxz/command/CmdQuest.java +++ b/src/main/java/foxz/command/CmdQuest.java @@ -6,6 +6,7 @@ import foxz.commandhelper.annotations.SubCommand; import foxz.commandhelper.permissions.OpOnly; import foxz.commandhelper.permissions.ParamCheck; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.network.enums.SyncType; import kamkeel.npcs.network.packets.data.AchievementPacket; import kamkeel.npcs.network.packets.data.ChatAlertPacket; import net.minecraft.entity.player.EntityPlayerMP; @@ -170,7 +171,7 @@ public class CmdQuest extends ChMcLogger { ) public boolean reload(String args[]) { new QuestController().load(); - SyncController.syncAllQuests(); + SyncController.syncAll(SyncType.QUEST_CATEGORY); return true; } } diff --git a/src/main/java/kamkeel/npcs/command/DialogCommand.java b/src/main/java/kamkeel/npcs/command/DialogCommand.java index 2392859a4..d9df94c13 100644 --- a/src/main/java/kamkeel/npcs/command/DialogCommand.java +++ b/src/main/java/kamkeel/npcs/command/DialogCommand.java @@ -1,6 +1,7 @@ package kamkeel.npcs.command; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.network.enums.SyncType; import net.minecraft.command.CommandBase; import net.minecraft.command.CommandException; import net.minecraft.command.ICommandSender; @@ -93,7 +94,7 @@ public class DialogCommand extends CommandKamkeelBase { ) public void reload(ICommandSender sender, String args[]) { new DialogController().load(); - SyncController.syncAllDialogs(); + SyncController.syncAll(SyncType.DIALOG_CATEGORY); sendResult(sender, "Dialogs Reloaded"); } diff --git a/src/main/java/kamkeel/npcs/command/QuestCommand.java b/src/main/java/kamkeel/npcs/command/QuestCommand.java index 5ccbe5319..7cfd6ebe4 100644 --- a/src/main/java/kamkeel/npcs/command/QuestCommand.java +++ b/src/main/java/kamkeel/npcs/command/QuestCommand.java @@ -2,6 +2,7 @@ package kamkeel.npcs.command; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.network.enums.SyncType; import kamkeel.npcs.network.packets.data.AchievementPacket; import kamkeel.npcs.network.packets.data.ChatAlertPacket; import net.minecraft.command.CommandException; @@ -393,7 +394,7 @@ public class QuestCommand extends CommandKamkeelBase { ) public void reload(ICommandSender sender, String args[]) { new QuestController().load(); - SyncController.syncAllQuests(); + SyncController.syncAll(SyncType.QUEST_CATEGORY); sendResult(sender, "Quests Reloaded"); } } diff --git a/src/main/java/kamkeel/npcs/controllers/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/AbilityController.java index c47c395a6..c352119dd 100644 --- a/src/main/java/kamkeel/npcs/controllers/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/AbilityController.java @@ -32,6 +32,7 @@ import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport; import kamkeel.npcs.controllers.data.ability.type.AbilityTrap; import kamkeel.npcs.controllers.data.ability.type.AbilityVortex; +import kamkeel.npcs.network.enums.SyncType; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityBeam; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityDisc; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityDome; @@ -393,7 +394,7 @@ public class AbilityController implements IAbilityHandler { customAbilitiesById.put(uuid, ability); customAbilityRevision++; LogWriter.script("Saved custom ability: " + name + " [" + uuid + "]"); - SyncController.syncAllCustomAbilities(); + SyncController.syncAll(SyncType.CUSTOM_ABILITY); return true; } catch (Exception e) { LogWriter.error("Error saving custom ability: " + name, e); @@ -478,7 +479,7 @@ public class AbilityController implements IAbilityHandler { customAbilityRevision++; LogWriter.script("Deleted custom ability: " + name); - SyncController.syncAllCustomAbilities(); + SyncController.syncAll(SyncType.CUSTOM_ABILITY); return true; } @@ -765,7 +766,7 @@ public class AbilityController implements IAbilityHandler { chainedAbilitiesById.put(uuid, chain); chainedAbilityRevision++; LogWriter.script("Saved chained ability: " + name + " [" + uuid + "]"); - SyncController.syncAllChainedAbilities(); + SyncController.syncAll(SyncType.CHAINED_ABILITY); return true; } catch (Exception e) { LogWriter.error("Error saving chained ability: " + name, e); @@ -845,7 +846,7 @@ public class AbilityController implements IAbilityHandler { chainedAbilityRevision++; LogWriter.script("Deleted chained ability: " + name); - SyncController.syncAllChainedAbilities(); + SyncController.syncAll(SyncType.CHAINED_ABILITY); return true; } diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index ca2171461..7bf1231ea 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -5,51 +5,26 @@ import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import kamkeel.npcs.addon.DBCAddon; -import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.controllers.sync.SyncRegistry; +import kamkeel.npcs.controllers.sync.handlers.*; import kamkeel.npcs.network.LargeAbstractPacket; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumSyncAction; import kamkeel.npcs.network.enums.SyncType; import kamkeel.npcs.network.packets.data.LoginPacket; import kamkeel.npcs.network.packets.data.ProfileSharedQuestPacket; -import kamkeel.npcs.network.packets.data.ability.AbilityCooldownSyncPacket; -import kamkeel.npcs.network.packets.data.ability.AbilityHotbarSyncPacket; -import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; -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; -import noppes.npcs.controllers.FactionController; -import noppes.npcs.controllers.MagicController; -import noppes.npcs.controllers.QuestController; -import noppes.npcs.controllers.RecipeController; -import noppes.npcs.controllers.data.CustomEffect; -import noppes.npcs.controllers.data.Dialog; -import noppes.npcs.controllers.data.DialogCategory; -import noppes.npcs.controllers.data.EffectKey; -import noppes.npcs.controllers.data.Faction; -import noppes.npcs.controllers.data.Magic; -import noppes.npcs.controllers.data.MagicCycle; import noppes.npcs.controllers.data.PlayerData; -import noppes.npcs.controllers.data.PlayerEffect; -import noppes.npcs.controllers.data.Quest; -import noppes.npcs.controllers.data.QuestCategory; -import noppes.npcs.controllers.data.RecipeAnvil; -import noppes.npcs.controllers.data.RecipeCarpentry; import java.io.IOException; -import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -57,6 +32,31 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; +/** + * Central sync backend for CNPC+. + * + *

This controller manages two distinct sync lanes:

+ *
    + *
  • Global cached registry families — registered via {@link SyncRegistry} + * with concrete {@link SyncHandler} implementations. These families use + * cached serialized payloads, revision-based delta sync on reconnect, + * and chunked delivery. All family-specific RELOAD/UPDATE/REMOVE logic + * lives in the handler implementations, not here.
  • + *
  • Direct per-player sync helpers — for transient per-player state + * like effects, ability hotbars, and cooldowns. These bypass the cached + * registry system and are sent as direct packets. Owned by + * {@link PlayerEffectSyncHelper} and {@link PlayerAbilitySyncHelper}.
  • + *
+ * + *

This class retains ownership of:

+ *
    + *
  • Cache entry storage and lazy rebuild ({@link SyncCacheEntry})
  • + *
  • Cached payload byte[] + precomputed chunks ({@link CachedSyncPayload})
  • + *
  • Per-player revision tracking ({@link PlayerSyncState})
  • + *
  • Login handshake and post-login packet sequencing
  • + *
  • Generic sync dispatch helpers (syncAll, syncUpdate, syncRemove)
  • + *
+ */ public class SyncController { public static boolean DEBUG_SYNC_LOGGING = false; @@ -71,45 +71,65 @@ public class SyncController { } } - private static final SyncType[] LOGIN_SYNC_TYPES = new SyncType[]{ - SyncType.FACTION, - SyncType.DIALOG_CATEGORY, - SyncType.QUEST_CATEGORY, - SyncType.WORKBENCH_RECIPES, - SyncType.CARPENTRY_RECIPES, - SyncType.ANVIL_RECIPES, - SyncType.CUSTOM_EFFECTS, - SyncType.MAGIC, - SyncType.MAGIC_CYCLE, - SyncType.CUSTOM_ABILITY, - SyncType.CHAINED_ABILITY - }; + // ═══════════════════════════════════════════════════════════════════════ + // Lifecycle — handler registration and cache initialization + // ═══════════════════════════════════════════════════════════════════════ + /** + * Registers all core CNPC+ sync handlers in deterministic order. + * Called on server start. The registration order determines login + * sync ordering via {@link SyncRegistry#getLoginTypes()}. + * + *

Handler registration order matches the original hard-coded + * LOGIN_SYNC_TYPES array for behavioral parity.

+ */ public static void load() { cacheEntries.clear(); playerSyncState.clear(); + SyncRegistry.clear(); - registerCache(SyncType.FACTION, SyncController::factionsNBT); - registerCache(SyncType.DIALOG_CATEGORY, SyncController::dialogCategoriesNBT); - registerCache(SyncType.QUEST_CATEGORY, SyncController::questCategoriesNBT); - registerCache(SyncType.WORKBENCH_RECIPES, SyncController::workbenchNBT); - registerCache(SyncType.CARPENTRY_RECIPES, SyncController::carpentryNBT); - registerCache(SyncType.ANVIL_RECIPES, SyncController::anvilNBT); - registerCache(SyncType.CUSTOM_EFFECTS, SyncController::customEffectsNBT); - registerCache(SyncType.MAGIC, SyncController::magicsNBT); - registerCache(SyncType.MAGIC_CYCLE, SyncController::magicCyclesNBT); - registerCache(SyncType.CUSTOM_ABILITY, SyncController::customAbilitiesNBT); - registerCache(SyncType.CHAINED_ABILITY, SyncController::chainedAbilitiesNBT); + // Register all core handlers in deterministic order + // Order matches original LOGIN_SYNC_TYPES for login sync ordering + SyncRegistry.register(SyncType.FACTION, new FactionSyncHandler()); + SyncRegistry.register(SyncType.DIALOG, new DialogSyncHandler()); + SyncRegistry.register(SyncType.DIALOG_CATEGORY, new DialogCategorySyncHandler()); + SyncRegistry.register(SyncType.QUEST, new QuestSyncHandler()); + SyncRegistry.register(SyncType.QUEST_CATEGORY, new QuestCategorySyncHandler()); + SyncRegistry.register(SyncType.PLAYERDATA, new PlayerDataSyncHandler()); + SyncRegistry.register(SyncType.WORKBENCH_RECIPES, new WorkbenchRecipeSyncHandler()); + SyncRegistry.register(SyncType.CARPENTRY_RECIPES, new CarpentryRecipeSyncHandler()); + SyncRegistry.register(SyncType.ANVIL_RECIPES, new AnvilRecipeSyncHandler()); + SyncRegistry.register(SyncType.CUSTOM_EFFECTS, new CustomEffectSyncHandler()); + SyncRegistry.register(SyncType.MAGIC, new MagicSyncHandler()); + SyncRegistry.register(SyncType.MAGIC_CYCLE, new MagicCycleSyncHandler()); + SyncRegistry.register(SyncType.CUSTOM_ABILITY, new CustomAbilitySyncHandler()); + SyncRegistry.register(SyncType.CHAINED_ABILITY, new ChainedAbilitySyncHandler()); + + // Register caches for all cached (login-sync) types + // The cache supplier delegates to the handler's serializeAll() + for (SyncType type : SyncRegistry.getLoginTypes()) { + SyncHandler handler = SyncRegistry.getHandler(type); + if (handler != null) + registerCache(type, handler::serializeAll); + } + + SyncRegistry.validateRegistrations(); + SyncRegistry.freeze(); } + // ═══════════════════════════════════════════════════════════════════════ + // Login sync and revision handshake + // ═══════════════════════════════════════════════════════════════════════ + public static void syncPlayer(EntityPlayerMP player) { - SyncController.syncPlayerInternal(player, true); + SyncController.syncPlayer(player, true); } - private static void syncPlayerInternal(EntityPlayerMP player, boolean includePostPackets) { + private static void syncPlayer(EntityPlayerMP player, boolean includePostPackets) { PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); - for (SyncType type : LOGIN_SYNC_TYPES) { + // Registry-driven login iteration: iterate only cached types + for (SyncType type : SyncRegistry.getLoginTypes()) { SyncCacheEntry entry = cacheEntries.get(type); if (entry == null) { continue; @@ -117,14 +137,12 @@ public class SyncController { int currentRevision = entry.getRevisionValue(); int lastRevision = state.getRevision(type); - if (lastRevision == currentRevision) { + if (lastRevision == currentRevision) continue; - } - + CachedSyncPayload payload = entry.getPayload(type); - if (payload == null) { + if (payload == null) continue; - } if (lastRevision != payload.getRevision()) { debug("Sending %s data to %s", type, player.getCommandSenderName()); @@ -184,7 +202,7 @@ public class SyncController { private static void sendPostLoginPackets(EntityPlayerMP player) { DBCAddon.instance.syncPlayer(player); - syncPlayerData(player, false); + PlayerDataSyncHandler.getInstance().syncPlayerData(player, false); PartyInfoPacket.sendPartyData(player); ProfileSharedQuestPacket.sendToPlayer(player); @@ -198,12 +216,13 @@ public class SyncController { } private static Map getServerRevisionSnapshot() { + // Registry-driven: iterate login types from registry Map snapshot = new LinkedHashMap<>(); - for (SyncType type : LOGIN_SYNC_TYPES) { + for (SyncType type : SyncRegistry.getLoginTypes()) { SyncCacheEntry entry = cacheEntries.get(type); - if (entry != null) { + if (entry != null) snapshot.put(type, entry.getRevisionValue()); - } + } return snapshot; } @@ -228,634 +247,75 @@ public class SyncController { return entry == null ? -1 : entry.getRevisionValue(); } - public static NBTTagCompound workbenchNBT() { - RecipeController controller = RecipeController.Instance; - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (RecipeCarpentry recipe : controller.globalRecipes.values()) { - list.appendTag(recipe.writeNBT(false)); - } - compound.setTag("recipes", list); - return compound; - } + // ═══════════════════════════════════════════════════════════════════════ + // Generic sync dispatch — registry-driven, no family-specific logic + // ═══════════════════════════════════════════════════════════════════════ - public static NBTTagCompound carpentryNBT() { - RecipeController controller = RecipeController.Instance; - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (RecipeCarpentry recipe : controller.carpentryRecipes.values()) { - list.appendTag(recipe.writeNBT(false)); - } - compound.setTag("recipes", list); - return compound; - } - - public static NBTTagCompound anvilNBT() { - RecipeController controller = RecipeController.Instance; - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (RecipeAnvil recipe : controller.anvilRecipes.values()) { - list.appendTag(recipe.writeNBT(false)); - } - compound.setTag("recipes", list); - return compound; - } - - public static NBTTagCompound factionsNBT() { - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (Faction faction : FactionController.getInstance().factions.values()) { - NBTTagCompound factioNBT = new NBTTagCompound(); - faction.writeNBT(factioNBT); - list.appendTag(factioNBT); - } - compound.setTag("Factions", list); - return compound; - } - - public static NBTTagCompound dialogCategoriesNBT() { - NBTTagCompound compound = new NBTTagCompound(); - NBTTagList categoryList = new NBTTagList(); - for (DialogCategory category : DialogController.Instance.categories.values()) { - NBTTagCompound questCompound = new NBTTagCompound(); - NBTTagList dialogList = new NBTTagList(); - for (int dialogID : category.dialogs.keySet()) { - Dialog quest = category.dialogs.get(dialogID); - dialogList.appendTag(quest.writeToNBT(new NBTTagCompound())); - } - questCompound.setTag("Data", dialogList); - questCompound.setTag("CatNBT", category.writeSmallNBT(new NBTTagCompound())); - categoryList.appendTag(questCompound); - } - compound.setTag("DialogCategories", categoryList); - return compound; - } - - public static NBTTagCompound questCategoriesNBT() { - NBTTagCompound compound = new NBTTagCompound(); - NBTTagList categoryList = new NBTTagList(); - for (QuestCategory category : QuestController.Instance.categories.values()) { - NBTTagCompound questCompound = new NBTTagCompound(); - ; - NBTTagList questList = new NBTTagList(); - for (int questID : category.quests.keySet()) { - Quest quest = category.quests.get(questID); - questList.appendTag(quest.writeToNBT(new NBTTagCompound())); - } - questCompound.setTag("Data", questList); - questCompound.setTag("CatNBT", category.writeSmallNBT(new NBTTagCompound())); - categoryList.appendTag(questCompound); - } - compound.setTag("QuestCategories", categoryList); - return compound; - } - - public static NBTTagCompound customEffectsNBT() { - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (CustomEffect effect : CustomEffectController.getInstance().getCustomEffects().values()) { - list.appendTag(effect.writeToNBT(false)); - } - compound.setTag("Data", list); - return compound; - } - - public static NBTTagCompound magicsNBT() { - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (Magic magic : MagicController.getInstance().magics.values()) { - NBTTagCompound magicCompound = new NBTTagCompound(); - magic.writeNBT(magicCompound); - list.appendTag(magicCompound); - } - compound.setTag("Data", list); - return compound; - } - - public static NBTTagCompound magicCyclesNBT() { - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (MagicCycle cycle : MagicController.getInstance().cycles.values()) { - NBTTagCompound cycleCompound = new NBTTagCompound(); - cycle.writeNBT(cycleCompound); - list.appendTag(cycleCompound); - } - compound.setTag("Data", list); - return compound; - } - - public static NBTTagCompound customAbilitiesNBT() { - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (Map.Entry entry : AbilityController.Instance.getCustomAbilities().entrySet()) { - NBTTagCompound abilityNBT = entry.getValue().writeNBT(false); - abilityNBT.setString("CustomAbilityId", entry.getKey()); - list.appendTag(abilityNBT); - } - compound.setTag("Data", list); - return compound; - } - - public static void syncAllCustomAbilities() { - CachedSyncPayload payload = rebuildNow(SyncType.CUSTOM_ABILITY); + /** + * Generic full-broadcast helper. Rebuilds the cache for the given type + * and sends to all connected players. + */ + public static void syncAll(SyncType type) { + CachedSyncPayload payload = rebuildNow(type); if (payload == null) { return; } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CUSTOM_ABILITY, payload)); - updateAllPlayerRevisions(SyncType.CUSTOM_ABILITY, payload.getRevision()); + PacketHandler.Instance.sendToAll(new SyncPacket(type, payload)); + updateAllPlayerRevisions(type, payload.getRevision()); } - public static NBTTagCompound chainedAbilitiesNBT() { - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (Map.Entry entry : AbilityController.Instance.getChainedAbilities().entrySet()) { - NBTTagCompound chainNBT = entry.getValue().writeNBT(false); - chainNBT.setString("ChainedAbilityId", entry.getKey()); - list.appendTag(chainNBT); - } - compound.setTag("Data", list); - return compound; - } - - public static void syncAllChainedAbilities() { - CachedSyncPayload payload = rebuildNow(SyncType.CHAINED_ABILITY); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CHAINED_ABILITY, payload)); - updateAllPlayerRevisions(SyncType.CHAINED_ABILITY, payload.getRevision()); - } - - public static void syncPlayerData(EntityPlayerMP player, boolean update) { - PlayerData data = PlayerData.get(player); - if (data != null) { - if (update) { - PacketHandler.Instance.sendToPlayer(new SyncPacket( - SyncType.PLAYERDATA, - EnumSyncAction.UPDATE, - -1, - data.getSyncNBT()) - , player); - } else { - PacketHandler.Instance.sendToPlayer(new SyncPacket( - SyncType.PLAYERDATA, - EnumSyncAction.RELOAD, - -1, - data.getSyncNBTFull()), player); - } - } - } + // ═══════════════════════════════════════════════════════════════════════ + // UPDATE and REMOVE dispatch — generic, registry-driven + // ═══════════════════════════════════════════════════════════════════════ public static void syncRemove(SyncType syncType, int id) { Map revisions = invalidateCaches(syncType); int revision = revisions.getOrDefault(syncType, getCurrentRevision(syncType)); - PacketHandler.Instance.sendToAll(new SyncPacket( - syncType, - EnumSyncAction.REMOVE, - id, - revision, - new NBTTagCompound() - )); + PacketHandler.Instance.sendToAll(new SyncPacket(syncType, EnumSyncAction.REMOVE, id, revision, new NBTTagCompound())); updateAllPlayerRevisions(revisions); } - public static void syncAllDialogs() { - CachedSyncPayload payload = rebuildNow(SyncType.DIALOG_CATEGORY); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.DIALOG_CATEGORY, payload)); - updateAllPlayerRevisions(SyncType.DIALOG_CATEGORY, payload.getRevision()); - } - - public static void syncAllQuests() { - CachedSyncPayload payload = rebuildNow(SyncType.QUEST_CATEGORY); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.QUEST_CATEGORY, payload)); - updateAllPlayerRevisions(SyncType.QUEST_CATEGORY, payload.getRevision()); - } - - public static void syncAllWorkbenchRecipes() { - CachedSyncPayload payload = rebuildNow(SyncType.WORKBENCH_RECIPES); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.WORKBENCH_RECIPES, payload)); - updateAllPlayerRevisions(SyncType.WORKBENCH_RECIPES, payload.getRevision()); - } - - public static void syncAllCarpentryRecipes() { - CachedSyncPayload payload = rebuildNow(SyncType.CARPENTRY_RECIPES); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CARPENTRY_RECIPES, payload)); - updateAllPlayerRevisions(SyncType.CARPENTRY_RECIPES, payload.getRevision()); - } - - public static void syncAllAnvilRecipes() { - CachedSyncPayload payload = rebuildNow(SyncType.ANVIL_RECIPES); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.ANVIL_RECIPES, payload)); - updateAllPlayerRevisions(SyncType.ANVIL_RECIPES, payload.getRevision()); - } - - public static void syncAllCustomEffects() { - CachedSyncPayload payload = rebuildNow(SyncType.CUSTOM_EFFECTS); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CUSTOM_EFFECTS, payload)); - updateAllPlayerRevisions(SyncType.CUSTOM_EFFECTS, payload.getRevision()); - } - - @SideOnly(Side.CLIENT) - public static void clientSync(SyncType syncType, int revision, NBTTagCompound fullCompound) { - if (syncType == SyncType.FACTION) { - NBTTagList list = fullCompound.getTagList("Factions", 10); - for (int i = 0; i < list.tagCount(); i++) { - Faction faction = new Faction(); - faction.readNBT(list.getCompoundTagAt(i)); - FactionController.getInstance().factionsSync.put(faction.id, faction); - } - FactionController.getInstance().factions = FactionController.getInstance().factionsSync; - FactionController.getInstance().factionsSync = new HashMap(); - } else if (syncType == SyncType.DIALOG_CATEGORY) { - if (!fullCompound.hasNoTags()) { - NBTTagList categories = fullCompound.getTagList("DialogCategories", 10); - for (int j = 0; j < fullCompound.getTagList("DialogCategories", 10).tagCount(); j++) { - NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); - if (categoryCompound.hasNoTags()) - continue; - - DialogCategory category = new DialogCategory(); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - NBTTagList dialogList = categoryCompound.getTagList("Data", 10); - if (DialogController.Instance.categoriesSync.containsKey(category.id)) { - category = DialogController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < dialogList.tagCount(); i++) { - Dialog dialog = new Dialog(); - dialog.readNBT(dialogList.getCompoundTagAt(i)); - dialog.category = category; - category.dialogs.put(dialog.id, dialog); - } - DialogController.Instance.categoriesSync.put(category.id, category); - } - } - - HashMap dialogs = new HashMap(); - for (DialogCategory category : DialogController.Instance.categoriesSync.values()) { - for (Dialog dialog : category.dialogs.values()) { - dialogs.put(dialog.id, dialog); - } - } - - DialogController.Instance.categories = DialogController.Instance.categoriesSync; - DialogController.Instance.dialogs = dialogs; - DialogController.Instance.categoriesSync = new HashMap(); - } else if (syncType == SyncType.QUEST_CATEGORY) { - if (!fullCompound.hasNoTags()) { - NBTTagList categories = fullCompound.getTagList("QuestCategories", 10); - for (int j = 0; j < fullCompound.getTagList("QuestCategories", 10).tagCount(); j++) { - NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); - if (categoryCompound.hasNoTags()) - continue; - - QuestCategory category = new QuestCategory(); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - NBTTagList questList = categoryCompound.getTagList("Data", 10); - if (QuestController.Instance.categoriesSync.containsKey(category.id)) { - category = QuestController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < questList.tagCount(); i++) { - Quest quest = new Quest(); - quest.readNBT(questList.getCompoundTagAt(i)); - quest.category = category; - category.quests.put(quest.id, quest); - } - QuestController.Instance.categoriesSync.put(category.id, category); - } - } - - HashMap quests = new HashMap(); - for (QuestCategory category : QuestController.Instance.categoriesSync.values()) { - for (Quest quest : category.quests.values()) { - quests.put(quest.id, quest); - } - } - - QuestController.Instance.categories = QuestController.Instance.categoriesSync; - QuestController.Instance.quests = quests; - QuestController.Instance.categoriesSync = new HashMap(); - } else if (syncType == SyncType.PLAYERDATA) { - ClientCacheHandler.playerData.setSyncNBTFull(fullCompound); - } else if (syncType == SyncType.MAGIC) { - NBTTagList list = fullCompound.getTagList("Data", 10); - MagicController mc = MagicController.getInstance(); - mc.magicSync.clear(); - for (int i = 0; i < list.tagCount(); i++) { - Magic magic = new Magic(); - magic.readNBT(list.getCompoundTagAt(i)); - mc.magicSync.put(magic.id, magic); - } - - mc.magics.clear(); - mc.magics.putAll(mc.magicSync); - mc.magicSync.clear(); - } else if (syncType == SyncType.MAGIC_CYCLE) { - NBTTagList list = fullCompound.getTagList("Data", 10); - MagicController mc = MagicController.getInstance(); - mc.cyclesSync.clear(); - for (int i = 0; i < list.tagCount(); i++) { - MagicCycle cycle = new MagicCycle(); - cycle.readNBT(list.getCompoundTagAt(i)); - mc.cyclesSync.put(cycle.id, cycle); - } - - mc.cycles.clear(); - mc.cycles.putAll(mc.cyclesSync); - mc.cyclesSync.clear(); - } else if (syncType == SyncType.WORKBENCH_RECIPES) { - NBTTagList list = fullCompound.getTagList("recipes", 10); - if (list == null) - return; - - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound recipeCompound = list.getCompoundTagAt(i); - RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); - recipe.readNBT(recipeCompound); - RecipeController.syncRecipes.put(recipe.id, recipe); - } - - RecipeController.reloadGlobalRecipes(RecipeController.syncRecipes); - RecipeController.syncRecipes = new HashMap(); - } else if (syncType == SyncType.CARPENTRY_RECIPES) { - NBTTagList list = fullCompound.getTagList("recipes", 10); - if (list == null) - return; - - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound recipeCompound = list.getCompoundTagAt(i); - RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); - recipe.readNBT(recipeCompound); - RecipeController.syncRecipes.put(recipe.id, recipe); - } - - RecipeController.Instance.carpentryRecipes = RecipeController.syncRecipes; - RecipeController.syncRecipes = new HashMap(); - } else if (syncType == SyncType.ANVIL_RECIPES) { - NBTTagList list = fullCompound.getTagList("recipes", 10); - if (list == null) - return; - - for (int i = 0; i < list.tagCount(); i++) { - RecipeAnvil recipe = new RecipeAnvil(); - recipe.readNBT(list.getCompoundTagAt(i)); - RecipeController.syncAnvilRecipes.put(recipe.id, recipe); - } - - RecipeController.Instance.anvilRecipes = RecipeController.syncAnvilRecipes; - RecipeController.syncAnvilRecipes = new HashMap(); - } else if (syncType == SyncType.CUSTOM_EFFECTS) { - NBTTagList list = fullCompound.getTagList("Data", 10); - CustomEffectController ce = CustomEffectController.getInstance(); - ce.customEffectsSync.clear(); - for (int i = 0; i < list.tagCount(); i++) { - CustomEffect effect = new CustomEffect(); - effect.readFromNBT(list.getCompoundTagAt(i)); - ClientCacheHandler.getImageData(effect.icon); - ce.customEffectsSync.put(effect.id, effect); - } - - ce.getCustomEffects().clear(); - ce.getCustomEffects().putAll(ce.customEffectsSync); - ce.customEffectsSync.clear(); - } else if (syncType == SyncType.CUSTOM_ABILITY) { - NBTTagList list = fullCompound.getTagList("Data", 10); - LinkedHashMap sync = new LinkedHashMap<>(); - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound nbt = list.getCompoundTagAt(i); - String name = nbt.getString("CustomAbilityId"); - Ability ability = AbilityController.Instance.fromNBT(nbt); - if (ability != null) { - ability.setName(name); - sync.put(name, ability); - } - } - AbilityController.Instance.setCustomAbilities(sync); - } else if (syncType == SyncType.CHAINED_ABILITY) { - NBTTagList list = fullCompound.getTagList("Data", 10); - LinkedHashMap sync = new LinkedHashMap<>(); - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound nbt = list.getCompoundTagAt(i); - String id = nbt.getString("ChainedAbilityId"); - ChainedAbility chain = new ChainedAbility(); - chain.readNBT(nbt); - chain.setName(id); - sync.put(id, chain); - } - AbilityController.Instance.setChainedAbilities(sync); - } - - ClientCacheHandler.updateClientRevision(syncType, revision); - } - - @SideOnly(Side.CLIENT) - public static void clientUpdate(SyncType syncType, int category_id, int revision, NBTTagCompound compound) { - if (syncType == SyncType.FACTION) { - Faction faction = new Faction(); - faction.readNBT(compound); - FactionController.getInstance().factions.put(faction.id, faction); - } else if (syncType == SyncType.DIALOG) { - DialogCategory category = DialogController.Instance.categories.get(category_id); - Dialog dialog = new Dialog(); - dialog.category = category; - dialog.readNBT(compound); - DialogController.Instance.dialogs.put(dialog.id, dialog); - category.dialogs.put(dialog.id, dialog); - } else if (syncType == SyncType.QUEST) { - QuestCategory category = QuestController.Instance.categories.get(category_id); - Quest quest = new Quest(); - quest.category = category; - quest.readNBT(compound); - QuestController.Instance.quests.put(quest.id, quest); - category.quests.put(quest.id, quest); - } else if (syncType == SyncType.QUEST_CATEGORY) { - QuestCategory category = new QuestCategory(); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - NBTTagList list = compound.getTagList("Data", 10); - if (QuestController.Instance.categoriesSync.containsKey(category.id)) { - category = QuestController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < list.tagCount(); i++) { - Quest quest = new Quest(); - quest.readNBT(list.getCompoundTagAt(i)); - quest.category = category; - category.quests.put(quest.id, quest); - } - QuestController.Instance.categories.put(category.id, category); - } else if (syncType == SyncType.DIALOG_CATEGORY) { - DialogCategory category = new DialogCategory(); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - NBTTagList list = compound.getTagList("Data", 10); - if (DialogController.Instance.categoriesSync.containsKey(category.id)) { - category = DialogController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < list.tagCount(); i++) { - Dialog dialog = new Dialog(); - dialog.readNBT(list.getCompoundTagAt(i)); - dialog.category = category; - category.dialogs.put(dialog.id, dialog); - } - DialogController.Instance.categories.put(category.id, category); - } else if (syncType == SyncType.MAGIC_CYCLE) { - MagicCycle cycle = new MagicCycle(); - cycle.readNBT(compound); - MagicController.getInstance().cycles.put(cycle.id, cycle); - } else if (syncType == SyncType.MAGIC) { - Magic magic = new Magic(); - magic.readNBT(compound); - MagicController.getInstance().magics.put(magic.id, magic); - } else if (syncType == SyncType.PLAYERDATA) { - ClientCacheHandler.playerData.setSyncNBT(compound); - } else if (syncType == SyncType.CUSTOM_EFFECTS) { - CustomEffect effect = new CustomEffect(); - effect.readFromNBT(compound); - ClientCacheHandler.getImageData(effect.icon); - - CustomEffectController.Instance.getCustomEffects().put(effect.id, effect); - } - - ClientCacheHandler.updateClientRevision(syncType, revision); - } - public static void syncUpdate(SyncType 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 - )); + PacketHandler.Instance.sendToAll(new SyncPacket(type, EnumSyncAction.UPDATE, cat, revision, compound)); updateAllPlayerRevisions(revisions); } + // ═══════════════════════════════════════════════════════════════════════ + // Client-side dispatch — registry-driven handler delegation + // ═══════════════════════════════════════════════════════════════════════ - public static NBTTagCompound updateQuestCat(QuestCategory questCategory) { - NBTTagCompound questCompound = new NBTTagCompound(); - NBTTagList questList = new NBTTagList(); - for (int questID : questCategory.quests.keySet()) { - Quest quest = questCategory.quests.get(questID); - questList.appendTag(quest.writeToNBT(new NBTTagCompound())); - } - questCompound.setTag("Data", questList); - questCompound.setTag("CatNBT", questCategory.writeSmallNBT(new NBTTagCompound())); - return questCompound; + @SideOnly(Side.CLIENT) + public static void clientSync(SyncType syncType, int revision, NBTTagCompound fullCompound) { + SyncHandler handler = SyncRegistry.getHandler(syncType); + if (handler != null) + handler.clientHandleReload(fullCompound); + + ClientCacheHandler.updateClientRevision(syncType, revision); } - public static NBTTagCompound updateDialogCat(DialogCategory dialogCategory) { - NBTTagCompound dialogCompound = new NBTTagCompound(); - NBTTagList dialogList = new NBTTagList(); - for (int questID : dialogCategory.dialogs.keySet()) { - Dialog dialog = dialogCategory.dialogs.get(questID); - dialogList.appendTag(dialog.writeToNBT(new NBTTagCompound())); - } - dialogCompound.setTag("Data", dialogList); - dialogCompound.setTag("CatNBT", dialogCategory.writeSmallNBT(new NBTTagCompound())); - return dialogCompound; + @SideOnly(Side.CLIENT) + public static void clientUpdate(SyncType syncType, int category_id, int revision, NBTTagCompound compound) { + SyncHandler handler = SyncRegistry.getHandler(syncType); + if (handler != null) + handler.clientHandleUpdate(compound, category_id); + + ClientCacheHandler.updateClientRevision(syncType, revision); } @SideOnly(Side.CLIENT) public static void clientSyncRemove(SyncType syncType, int id, int revision) { - if (syncType == SyncType.FACTION) { - FactionController.getInstance().factions.remove(id); - } else if (syncType == SyncType.DIALOG) { - Dialog dialog = DialogController.Instance.dialogs.remove(id); - if (dialog != null) { - dialog.category.dialogs.remove(id); - } - } else if (syncType == SyncType.DIALOG_CATEGORY) { - DialogCategory dialogCategory = DialogController.Instance.categories.remove(id); - if (dialogCategory != null) { - DialogController.Instance.dialogs.keySet().removeAll(dialogCategory.dialogs.keySet()); - } - } else if (syncType == SyncType.QUEST) { - Quest quest = QuestController.Instance.quests.remove(id); - if (quest != null) { - quest.category.quests.remove(id); - } - } else if (syncType == SyncType.QUEST_CATEGORY) { - QuestCategory questCategory = QuestController.Instance.categories.remove(id); - if (questCategory != null) { - QuestController.Instance.quests.keySet().removeAll(questCategory.quests.keySet()); - } - } else if (syncType == SyncType.MAGIC) { - for (MagicCycle cycle : MagicController.getInstance().cycles.values()) { - cycle.associations.remove(id); - } - MagicController.getInstance().cycles.remove(id); - } else if (syncType == SyncType.MAGIC_CYCLE) { - MagicController.getInstance().cycles.remove(id); - } else if (syncType == SyncType.CUSTOM_EFFECTS) { - CustomEffectController.Instance.getCustomEffects().remove(id); - } - + SyncHandler handler = SyncRegistry.getHandler(syncType); + if (handler != null) + handler.clientHandleRemove(id); + ClientCacheHandler.updateClientRevision(syncType, revision); } - public static void syncEffects(EntityPlayerMP playerMP) { - ConcurrentHashMap playerEffects = CustomEffectController.getInstance().getPlayerEffects(playerMP); - PlayerData playerData = PlayerData.get(playerMP); - playerData.effectData.setEffects(playerEffects); - - NBTTagCompound compound = playerData.getPlayerEffects(); - PacketHandler.Instance.sendToPlayer(new SyncEffectPacket(compound), playerMP); - } - - /** - * Full sync of all player ability state to the client: unlocked abilities, - * selection, toggles, hotbar layout, and cooldowns. - * Used for entity reconstruction and other full-refresh scenarios. - */ - public static void syncAbilities(EntityPlayerMP playerMP) { - PlayerData playerData = PlayerData.get(playerMP); - if (playerData == null) return; - - PlayerAbilitySyncPacket.sendToPlayer(playerMP); - AbilityHotbarSyncPacket.sendToPlayer(playerMP); - AbilityCooldownSyncPacket.sendToPlayer(playerMP); - } - - /** - * Lightweight sync of cooldown state only (global + per-ability). - * Called periodically (every 10 ticks) to keep the client's cooldown - * display accurate. Cooldowns are transient and not part of NBT sync. - */ - public static void syncAbilityCooldowns(EntityPlayerMP playerMP) { - PlayerData playerData = PlayerData.get(playerMP); - if (playerData == null) return; - - AbilityCooldownSyncPacket.sendToPlayer(playerMP); - } - - @SideOnly(Side.CLIENT) - public static void clientSyncEffects(NBTTagCompound compound) { - PlayerData playerData = PlayerData.get(Minecraft.getMinecraft().thePlayer); - if (playerData != null) { - playerData.setPlayerEffects(compound); - } - } + // ═══════════════════════════════════════════════════════════════════════ + // Cache and revision internals + // ═══════════════════════════════════════════════════════════════════════ private static void registerCache(SyncType type, Supplier supplier) { cacheEntries.put(type, new SyncCacheEntry(supplier)); @@ -863,15 +323,19 @@ public class SyncController { private static CachedSyncPayload rebuildNow(SyncType type) { SyncCacheEntry entry = cacheEntries.get(type); - if (entry == null) { + if (entry == null) return null; - } + entry.invalidate(); return entry.getPayload(type); } + /** + * Registry-driven invalidation: delegates to handler-owned + * invalidation targets via {@link SyncRegistry#getInvalidationTargets}. + */ public static Map invalidateCaches(SyncType type) { - Set targets = getInvalidationTargets(type); + Set targets = SyncRegistry.getInvalidationTargets(type); Map revisions = new LinkedHashMap<>(); for (SyncType target : targets) { SyncCacheEntry entry = cacheEntries.get(target); @@ -883,55 +347,6 @@ public class SyncController { return revisions; } - private static Set getInvalidationTargets(SyncType type) { - if (type == SyncType.DIALOG || type == SyncType.DIALOG_CATEGORY) { - Set set = new HashSet<>(); - set.add(SyncType.DIALOG_CATEGORY); - return set; - } else if (type == SyncType.QUEST || type == SyncType.QUEST_CATEGORY) { - Set set = new HashSet<>(); - set.add(SyncType.QUEST_CATEGORY); - return set; - } else if (type == SyncType.FACTION) { - Set set = new HashSet<>(); - set.add(SyncType.FACTION); - return set; - } else if (type == SyncType.MAGIC) { - Set set = new HashSet<>(); - set.add(SyncType.MAGIC); - return set; - } else if (type == SyncType.MAGIC_CYCLE) { - Set set = new HashSet<>(); - set.add(SyncType.MAGIC_CYCLE); - return set; - } else if (type == SyncType.WORKBENCH_RECIPES) { - Set set = new HashSet<>(); - set.add(SyncType.WORKBENCH_RECIPES); - return set; - } else if (type == SyncType.CARPENTRY_RECIPES) { - Set set = new HashSet<>(); - set.add(SyncType.CARPENTRY_RECIPES); - return set; - } else if (type == SyncType.ANVIL_RECIPES) { - Set set = new HashSet<>(); - set.add(SyncType.ANVIL_RECIPES); - return set; - } else if (type == SyncType.CUSTOM_EFFECTS) { - Set set = new HashSet<>(); - set.add(SyncType.CUSTOM_EFFECTS); - return set; - } else if (type == SyncType.CUSTOM_ABILITY) { - Set set = new HashSet<>(); - set.add(SyncType.CUSTOM_ABILITY); - return set; - } else if (type == SyncType.CHAINED_ABILITY) { - Set set = new HashSet<>(); - set.add(SyncType.CHAINED_ABILITY); - return set; - } - return Collections.emptySet(); - } - private static void updateAllPlayerRevisions(SyncType type, int revision) { if (revision < 0) { return; @@ -952,6 +367,10 @@ public class SyncController { } } + // ═══════════════════════════════════════════════════════════════════════ + // Inner classes — cache payload and player revision state + // ═══════════════════════════════════════════════════════════════════════ + public static final class CachedSyncPayload { private final int revision; private final byte[] payload; diff --git a/src/main/java/kamkeel/npcs/controllers/SyncControllerOld.java b/src/main/java/kamkeel/npcs/controllers/SyncControllerOld.java new file mode 100644 index 000000000..987059c80 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/SyncControllerOld.java @@ -0,0 +1,1058 @@ +package kamkeel.npcs.controllers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import kamkeel.npcs.addon.DBCAddon; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; +import kamkeel.npcs.network.LargeAbstractPacket; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumSyncAction; +import kamkeel.npcs.network.enums.SyncType; +import kamkeel.npcs.network.packets.data.LoginPacket; +import kamkeel.npcs.network.packets.data.ProfileSharedQuestPacket; +import kamkeel.npcs.network.packets.data.ability.AbilityCooldownSyncPacket; +import kamkeel.npcs.network.packets.data.ability.AbilityHotbarSyncPacket; +import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; +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.*; +import noppes.npcs.controllers.data.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +public class SyncControllerOld { + + public static boolean DEBUG_SYNC_LOGGING = false; + + private static final Map cacheEntries = new LinkedHashMap<>(); + private static final ConcurrentHashMap playerSyncState = new ConcurrentHashMap<>(); + + + private static void debug(String message, Object... args) { + if (DEBUG_SYNC_LOGGING) { + System.out.println("[SyncController] " + String.format(message, args)); + } + } + + private static final SyncType[] LOGIN_SYNC_TYPES = new SyncType[]{ + SyncType.FACTION, + SyncType.DIALOG_CATEGORY, + SyncType.QUEST_CATEGORY, + SyncType.WORKBENCH_RECIPES, + SyncType.CARPENTRY_RECIPES, + SyncType.ANVIL_RECIPES, + SyncType.CUSTOM_EFFECTS, + SyncType.MAGIC, + SyncType.MAGIC_CYCLE, + SyncType.CUSTOM_ABILITY, + SyncType.CHAINED_ABILITY + }; + + public static void load() { + cacheEntries.clear(); + playerSyncState.clear(); + + registerCache(SyncType.FACTION, SyncControllerOld::factionsNBT); + registerCache(SyncType.DIALOG_CATEGORY, SyncControllerOld::dialogCategoriesNBT); + registerCache(SyncType.QUEST_CATEGORY, SyncControllerOld::questCategoriesNBT); + registerCache(SyncType.WORKBENCH_RECIPES, SyncControllerOld::workbenchNBT); + registerCache(SyncType.CARPENTRY_RECIPES, SyncControllerOld::carpentryNBT); + registerCache(SyncType.ANVIL_RECIPES, SyncControllerOld::anvilNBT); + registerCache(SyncType.CUSTOM_EFFECTS, SyncControllerOld::customEffectsNBT); + registerCache(SyncType.MAGIC, SyncControllerOld::magicsNBT); + registerCache(SyncType.MAGIC_CYCLE, SyncControllerOld::magicCyclesNBT); + registerCache(SyncType.CUSTOM_ABILITY, SyncControllerOld::customAbilitiesNBT); + registerCache(SyncType.CHAINED_ABILITY, SyncControllerOld::chainedAbilitiesNBT); + } + + public static void syncPlayer(EntityPlayerMP player) { + SyncControllerOld.syncPlayerInternal(player, true); + } + + private static void syncPlayerInternal(EntityPlayerMP player, boolean includePostPackets) { + PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); + + for (SyncType type : LOGIN_SYNC_TYPES) { + SyncCacheEntry entry = cacheEntries.get(type); + if (entry == null) { + continue; + } + + int currentRevision = entry.getRevisionValue(); + int lastRevision = state.getRevision(type); + if (lastRevision == currentRevision) { + continue; + } + + CachedSyncPayload payload = entry.getPayload(type); + if (payload == null) { + continue; + } + + if (lastRevision != payload.getRevision()) { + debug("Sending %s data to %s", type, player.getCommandSenderName()); + PacketHandler.Instance.sendToPlayer(new SyncPacket(type, payload), player); + state.updateRevision(type, payload.getRevision()); + } + } + + if (includePostPackets) { + sendPostLoginPackets(player); + } + } + + public static void beginLogin(EntityPlayerMP player) { + playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); + PacketHandler.Instance.sendToPlayer( + new LoginPacket(getServerCacheKey(), getServerRevisionSnapshot()), + player + ); + } + + public static void handleClientRevisionReport( + EntityPlayerMP player, + String serverKey, + String previousServerKey, + Map clientRevisions + ) { + String currentServerKey = getServerCacheKey(); + PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); + + if (!currentServerKey.equals(serverKey)) { + state.reset(); + syncPlayer(player); + return; + } + + if (!currentServerKey.isEmpty()) { + if (previousServerKey == null || !currentServerKey.equals(previousServerKey)) { + state.reset(); + syncPlayer(player); + return; + } + } + + if (clientRevisions == null || clientRevisions.isEmpty()) { + debug( + "Client %s reported no cached revisions; forcing full sync", + player.getCommandSenderName() + ); + state.reset(); + } else { + state.applyHandshake(clientRevisions); + } + + syncPlayer(player); + } + + private static void sendPostLoginPackets(EntityPlayerMP player) { + DBCAddon.instance.syncPlayer(player); + syncPlayerData(player, false); + PartyInfoPacket.sendPartyData(player); + ProfileSharedQuestPacket.sendToPlayer(player); + + // Sync skin overlays after full handshake to ensure the client is ready. + // Overlays are not part of getSyncNBTFull(), and packets sent during + // PlayerLoggedInEvent can arrive before the client entity exists. + PlayerData data = PlayerData.get(player); + if (data != null) { + data.skinOverlays.updateClient(); + } + } + + private static Map getServerRevisionSnapshot() { + Map snapshot = new LinkedHashMap<>(); + for (SyncType type : LOGIN_SYNC_TYPES) { + SyncCacheEntry entry = cacheEntries.get(type); + if (entry != null) { + snapshot.put(type, entry.getRevisionValue()); + } + } + return snapshot; + } + + private static final String SERVER_IDENTITY_KEY = UUID.randomUUID().toString(); + + private static String getServerCacheKey() { + MinecraftServer server = MinecraftServer.getServer(); + if (server == null) { + return ""; + } + + if (!server.isDedicatedServer()) { + return ""; + } + + return SERVER_IDENTITY_KEY; + } + + public static int getCurrentRevision(SyncType type) { + SyncCacheEntry entry = cacheEntries.get(type); + return entry == null ? -1 : entry.getRevisionValue(); + } + + public static NBTTagCompound workbenchNBT() { + RecipeController controller = RecipeController.Instance; + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (RecipeCarpentry recipe : controller.globalRecipes.values()) { + list.appendTag(recipe.writeNBT(false)); + } + compound.setTag("recipes", list); + return compound; + } + + public static NBTTagCompound carpentryNBT() { + RecipeController controller = RecipeController.Instance; + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (RecipeCarpentry recipe : controller.carpentryRecipes.values()) { + list.appendTag(recipe.writeNBT(false)); + } + compound.setTag("recipes", list); + return compound; + } + + public static NBTTagCompound anvilNBT() { + RecipeController controller = RecipeController.Instance; + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (RecipeAnvil recipe : controller.anvilRecipes.values()) { + list.appendTag(recipe.writeNBT(false)); + } + compound.setTag("recipes", list); + return compound; + } + + public static NBTTagCompound factionsNBT() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (Faction faction : FactionController.getInstance().factions.values()) { + NBTTagCompound factioNBT = new NBTTagCompound(); + faction.writeNBT(factioNBT); + list.appendTag(factioNBT); + } + compound.setTag("Factions", list); + return compound; + } + + public static NBTTagCompound dialogCategoriesNBT() { + NBTTagCompound compound = new NBTTagCompound(); + NBTTagList categoryList = new NBTTagList(); + for (DialogCategory category : DialogController.Instance.categories.values()) { + NBTTagCompound questCompound = new NBTTagCompound(); + NBTTagList dialogList = new NBTTagList(); + for (int dialogID : category.dialogs.keySet()) { + Dialog quest = category.dialogs.get(dialogID); + dialogList.appendTag(quest.writeToNBT(new NBTTagCompound())); + } + questCompound.setTag("Data", dialogList); + questCompound.setTag("CatNBT", category.writeSmallNBT(new NBTTagCompound())); + categoryList.appendTag(questCompound); + } + compound.setTag("DialogCategories", categoryList); + return compound; + } + + public static NBTTagCompound questCategoriesNBT() { + NBTTagCompound compound = new NBTTagCompound(); + NBTTagList categoryList = new NBTTagList(); + for (QuestCategory category : QuestController.Instance.categories.values()) { + NBTTagCompound questCompound = new NBTTagCompound(); + ; + NBTTagList questList = new NBTTagList(); + for (int questID : category.quests.keySet()) { + Quest quest = category.quests.get(questID); + questList.appendTag(quest.writeToNBT(new NBTTagCompound())); + } + questCompound.setTag("Data", questList); + questCompound.setTag("CatNBT", category.writeSmallNBT(new NBTTagCompound())); + categoryList.appendTag(questCompound); + } + compound.setTag("QuestCategories", categoryList); + return compound; + } + + public static NBTTagCompound customEffectsNBT() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (CustomEffect effect : CustomEffectController.getInstance().getCustomEffects().values()) { + list.appendTag(effect.writeToNBT(false)); + } + compound.setTag("Data", list); + return compound; + } + + public static NBTTagCompound magicsNBT() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (Magic magic : MagicController.getInstance().magics.values()) { + NBTTagCompound magicCompound = new NBTTagCompound(); + magic.writeNBT(magicCompound); + list.appendTag(magicCompound); + } + compound.setTag("Data", list); + return compound; + } + + public static NBTTagCompound magicCyclesNBT() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (MagicCycle cycle : MagicController.getInstance().cycles.values()) { + NBTTagCompound cycleCompound = new NBTTagCompound(); + cycle.writeNBT(cycleCompound); + list.appendTag(cycleCompound); + } + compound.setTag("Data", list); + return compound; + } + + public static NBTTagCompound customAbilitiesNBT() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (Map.Entry entry : AbilityController.Instance.getCustomAbilities().entrySet()) { + NBTTagCompound abilityNBT = entry.getValue().writeNBT(false); + abilityNBT.setString("CustomAbilityId", entry.getKey()); + list.appendTag(abilityNBT); + } + compound.setTag("Data", list); + return compound; + } + + public static void syncAllCustomAbilities() { + CachedSyncPayload payload = rebuildNow(SyncType.CUSTOM_ABILITY); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CUSTOM_ABILITY, payload)); + updateAllPlayerRevisions(SyncType.CUSTOM_ABILITY, payload.getRevision()); + } + + public static NBTTagCompound chainedAbilitiesNBT() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (Map.Entry entry : AbilityController.Instance.getChainedAbilities().entrySet()) { + NBTTagCompound chainNBT = entry.getValue().writeNBT(false); + chainNBT.setString("ChainedAbilityId", entry.getKey()); + list.appendTag(chainNBT); + } + compound.setTag("Data", list); + return compound; + } + + public static void syncAllChainedAbilities() { + CachedSyncPayload payload = rebuildNow(SyncType.CHAINED_ABILITY); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CHAINED_ABILITY, payload)); + updateAllPlayerRevisions(SyncType.CHAINED_ABILITY, payload.getRevision()); + } + + public static void syncPlayerData(EntityPlayerMP player, boolean update) { + PlayerData data = PlayerData.get(player); + if (data != null) { + if (update) { + PacketHandler.Instance.sendToPlayer(new SyncPacket( + SyncType.PLAYERDATA, + EnumSyncAction.UPDATE, + -1, + data.getSyncNBT()) + , player); + } else { + PacketHandler.Instance.sendToPlayer(new SyncPacket( + SyncType.PLAYERDATA, + EnumSyncAction.RELOAD, + -1, + data.getSyncNBTFull()), player); + } + } + } + + public static void syncRemove(SyncType syncType, int id) { + Map revisions = invalidateCaches(syncType); + int revision = revisions.getOrDefault(syncType, getCurrentRevision(syncType)); + PacketHandler.Instance.sendToAll(new SyncPacket( + syncType, + EnumSyncAction.REMOVE, + id, + revision, + new NBTTagCompound() + )); + updateAllPlayerRevisions(revisions); + } + + public static void syncAllDialogs() { + CachedSyncPayload payload = rebuildNow(SyncType.DIALOG_CATEGORY); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.DIALOG_CATEGORY, payload)); + updateAllPlayerRevisions(SyncType.DIALOG_CATEGORY, payload.getRevision()); + } + + public static void syncAllQuests() { + CachedSyncPayload payload = rebuildNow(SyncType.QUEST_CATEGORY); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.QUEST_CATEGORY, payload)); + updateAllPlayerRevisions(SyncType.QUEST_CATEGORY, payload.getRevision()); + } + + public static void syncAllWorkbenchRecipes() { + CachedSyncPayload payload = rebuildNow(SyncType.WORKBENCH_RECIPES); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.WORKBENCH_RECIPES, payload)); + updateAllPlayerRevisions(SyncType.WORKBENCH_RECIPES, payload.getRevision()); + } + + public static void syncAllCarpentryRecipes() { + CachedSyncPayload payload = rebuildNow(SyncType.CARPENTRY_RECIPES); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CARPENTRY_RECIPES, payload)); + updateAllPlayerRevisions(SyncType.CARPENTRY_RECIPES, payload.getRevision()); + } + + public static void syncAllAnvilRecipes() { + CachedSyncPayload payload = rebuildNow(SyncType.ANVIL_RECIPES); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.ANVIL_RECIPES, payload)); + updateAllPlayerRevisions(SyncType.ANVIL_RECIPES, payload.getRevision()); + } + + public static void syncAllCustomEffects() { + CachedSyncPayload payload = rebuildNow(SyncType.CUSTOM_EFFECTS); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CUSTOM_EFFECTS, payload)); + updateAllPlayerRevisions(SyncType.CUSTOM_EFFECTS, payload.getRevision()); + } + + @SideOnly(Side.CLIENT) + public static void clientSync(SyncType syncType, int revision, NBTTagCompound fullCompound) { + if (syncType == SyncType.FACTION) { + NBTTagList list = fullCompound.getTagList("Factions", 10); + for (int i = 0; i < list.tagCount(); i++) { + Faction faction = new Faction(); + faction.readNBT(list.getCompoundTagAt(i)); + FactionController.getInstance().factionsSync.put(faction.id, faction); + } + FactionController.getInstance().factions = FactionController.getInstance().factionsSync; + FactionController.getInstance().factionsSync = new HashMap(); + } else if (syncType == SyncType.DIALOG_CATEGORY) { + if (!fullCompound.hasNoTags()) { + NBTTagList categories = fullCompound.getTagList("DialogCategories", 10); + for (int j = 0; j < fullCompound.getTagList("DialogCategories", 10).tagCount(); j++) { + NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); + if (categoryCompound.hasNoTags()) + continue; + + DialogCategory category = new DialogCategory(); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + NBTTagList dialogList = categoryCompound.getTagList("Data", 10); + if (DialogController.Instance.categoriesSync.containsKey(category.id)) { + category = DialogController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < dialogList.tagCount(); i++) { + Dialog dialog = new Dialog(); + dialog.readNBT(dialogList.getCompoundTagAt(i)); + dialog.category = category; + category.dialogs.put(dialog.id, dialog); + } + DialogController.Instance.categoriesSync.put(category.id, category); + } + } + + HashMap dialogs = new HashMap(); + for (DialogCategory category : DialogController.Instance.categoriesSync.values()) { + for (Dialog dialog : category.dialogs.values()) { + dialogs.put(dialog.id, dialog); + } + } + + DialogController.Instance.categories = DialogController.Instance.categoriesSync; + DialogController.Instance.dialogs = dialogs; + DialogController.Instance.categoriesSync = new HashMap(); + } else if (syncType == SyncType.QUEST_CATEGORY) { + if (!fullCompound.hasNoTags()) { + NBTTagList categories = fullCompound.getTagList("QuestCategories", 10); + for (int j = 0; j < fullCompound.getTagList("QuestCategories", 10).tagCount(); j++) { + NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); + if (categoryCompound.hasNoTags()) + continue; + + QuestCategory category = new QuestCategory(); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + NBTTagList questList = categoryCompound.getTagList("Data", 10); + if (QuestController.Instance.categoriesSync.containsKey(category.id)) { + category = QuestController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < questList.tagCount(); i++) { + Quest quest = new Quest(); + quest.readNBT(questList.getCompoundTagAt(i)); + quest.category = category; + category.quests.put(quest.id, quest); + } + QuestController.Instance.categoriesSync.put(category.id, category); + } + } + + HashMap quests = new HashMap(); + for (QuestCategory category : QuestController.Instance.categoriesSync.values()) { + for (Quest quest : category.quests.values()) { + quests.put(quest.id, quest); + } + } + + QuestController.Instance.categories = QuestController.Instance.categoriesSync; + QuestController.Instance.quests = quests; + QuestController.Instance.categoriesSync = new HashMap(); + } else if (syncType == SyncType.PLAYERDATA) { + ClientCacheHandler.playerData.setSyncNBTFull(fullCompound); + } else if (syncType == SyncType.MAGIC) { + NBTTagList list = fullCompound.getTagList("Data", 10); + MagicController mc = MagicController.getInstance(); + mc.magicSync.clear(); + for (int i = 0; i < list.tagCount(); i++) { + Magic magic = new Magic(); + magic.readNBT(list.getCompoundTagAt(i)); + mc.magicSync.put(magic.id, magic); + } + + mc.magics.clear(); + mc.magics.putAll(mc.magicSync); + mc.magicSync.clear(); + } else if (syncType == SyncType.MAGIC_CYCLE) { + NBTTagList list = fullCompound.getTagList("Data", 10); + MagicController mc = MagicController.getInstance(); + mc.cyclesSync.clear(); + for (int i = 0; i < list.tagCount(); i++) { + MagicCycle cycle = new MagicCycle(); + cycle.readNBT(list.getCompoundTagAt(i)); + mc.cyclesSync.put(cycle.id, cycle); + } + + mc.cycles.clear(); + mc.cycles.putAll(mc.cyclesSync); + mc.cyclesSync.clear(); + } else if (syncType == SyncType.WORKBENCH_RECIPES) { + NBTTagList list = fullCompound.getTagList("recipes", 10); + if (list == null) + return; + + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound recipeCompound = list.getCompoundTagAt(i); + RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); + recipe.readNBT(recipeCompound); + RecipeController.syncRecipes.put(recipe.id, recipe); + } + + RecipeController.reloadGlobalRecipes(RecipeController.syncRecipes); + RecipeController.syncRecipes = new HashMap(); + } else if (syncType == SyncType.CARPENTRY_RECIPES) { + NBTTagList list = fullCompound.getTagList("recipes", 10); + if (list == null) + return; + + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound recipeCompound = list.getCompoundTagAt(i); + RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); + recipe.readNBT(recipeCompound); + RecipeController.syncRecipes.put(recipe.id, recipe); + } + + RecipeController.Instance.carpentryRecipes = RecipeController.syncRecipes; + RecipeController.syncRecipes = new HashMap(); + } else if (syncType == SyncType.ANVIL_RECIPES) { + NBTTagList list = fullCompound.getTagList("recipes", 10); + if (list == null) + return; + + for (int i = 0; i < list.tagCount(); i++) { + RecipeAnvil recipe = new RecipeAnvil(); + recipe.readNBT(list.getCompoundTagAt(i)); + RecipeController.syncAnvilRecipes.put(recipe.id, recipe); + } + + RecipeController.Instance.anvilRecipes = RecipeController.syncAnvilRecipes; + RecipeController.syncAnvilRecipes = new HashMap(); + } else if (syncType == SyncType.CUSTOM_EFFECTS) { + NBTTagList list = fullCompound.getTagList("Data", 10); + CustomEffectController ce = CustomEffectController.getInstance(); + ce.customEffectsSync.clear(); + for (int i = 0; i < list.tagCount(); i++) { + CustomEffect effect = new CustomEffect(); + effect.readFromNBT(list.getCompoundTagAt(i)); + ClientCacheHandler.getImageData(effect.icon); + ce.customEffectsSync.put(effect.id, effect); + } + + ce.getCustomEffects().clear(); + ce.getCustomEffects().putAll(ce.customEffectsSync); + ce.customEffectsSync.clear(); + } else if (syncType == SyncType.CUSTOM_ABILITY) { + NBTTagList list = fullCompound.getTagList("Data", 10); + LinkedHashMap sync = new LinkedHashMap<>(); + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound nbt = list.getCompoundTagAt(i); + String name = nbt.getString("CustomAbilityId"); + Ability ability = AbilityController.Instance.fromNBT(nbt); + if (ability != null) { + ability.setName(name); + sync.put(name, ability); + } + } + AbilityController.Instance.setCustomAbilities(sync); + } else if (syncType == SyncType.CHAINED_ABILITY) { + NBTTagList list = fullCompound.getTagList("Data", 10); + LinkedHashMap sync = new LinkedHashMap<>(); + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound nbt = list.getCompoundTagAt(i); + String id = nbt.getString("ChainedAbilityId"); + ChainedAbility chain = new ChainedAbility(); + chain.readNBT(nbt); + chain.setName(id); + sync.put(id, chain); + } + AbilityController.Instance.setChainedAbilities(sync); + } + + ClientCacheHandler.updateClientRevision(syncType, revision); + } + + @SideOnly(Side.CLIENT) + public static void clientUpdate(SyncType syncType, int category_id, int revision, NBTTagCompound compound) { + if (syncType == SyncType.FACTION) { + Faction faction = new Faction(); + faction.readNBT(compound); + FactionController.getInstance().factions.put(faction.id, faction); + } else if (syncType == SyncType.DIALOG) { + DialogCategory category = DialogController.Instance.categories.get(category_id); + Dialog dialog = new Dialog(); + dialog.category = category; + dialog.readNBT(compound); + DialogController.Instance.dialogs.put(dialog.id, dialog); + category.dialogs.put(dialog.id, dialog); + } else if (syncType == SyncType.QUEST) { + QuestCategory category = QuestController.Instance.categories.get(category_id); + Quest quest = new Quest(); + quest.category = category; + quest.readNBT(compound); + QuestController.Instance.quests.put(quest.id, quest); + category.quests.put(quest.id, quest); + } else if (syncType == SyncType.QUEST_CATEGORY) { + QuestCategory category = new QuestCategory(); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + NBTTagList list = compound.getTagList("Data", 10); + if (QuestController.Instance.categoriesSync.containsKey(category.id)) { + category = QuestController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < list.tagCount(); i++) { + Quest quest = new Quest(); + quest.readNBT(list.getCompoundTagAt(i)); + quest.category = category; + category.quests.put(quest.id, quest); + } + QuestController.Instance.categories.put(category.id, category); + } else if (syncType == SyncType.DIALOG_CATEGORY) { + DialogCategory category = new DialogCategory(); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + NBTTagList list = compound.getTagList("Data", 10); + if (DialogController.Instance.categoriesSync.containsKey(category.id)) { + category = DialogController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < list.tagCount(); i++) { + Dialog dialog = new Dialog(); + dialog.readNBT(list.getCompoundTagAt(i)); + dialog.category = category; + category.dialogs.put(dialog.id, dialog); + } + DialogController.Instance.categories.put(category.id, category); + } else if (syncType == SyncType.MAGIC_CYCLE) { + MagicCycle cycle = new MagicCycle(); + cycle.readNBT(compound); + MagicController.getInstance().cycles.put(cycle.id, cycle); + } else if (syncType == SyncType.MAGIC) { + Magic magic = new Magic(); + magic.readNBT(compound); + MagicController.getInstance().magics.put(magic.id, magic); + } else if (syncType == SyncType.PLAYERDATA) { + ClientCacheHandler.playerData.setSyncNBT(compound); + } else if (syncType == SyncType.CUSTOM_EFFECTS) { + CustomEffect effect = new CustomEffect(); + effect.readFromNBT(compound); + ClientCacheHandler.getImageData(effect.icon); + + CustomEffectController.Instance.getCustomEffects().put(effect.id, effect); + } + + ClientCacheHandler.updateClientRevision(syncType, revision); + } + + public static void syncUpdate(SyncType 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); + } + + + public static NBTTagCompound updateQuestCat(QuestCategory questCategory) { + NBTTagCompound questCompound = new NBTTagCompound(); + NBTTagList questList = new NBTTagList(); + for (int questID : questCategory.quests.keySet()) { + Quest quest = questCategory.quests.get(questID); + questList.appendTag(quest.writeToNBT(new NBTTagCompound())); + } + questCompound.setTag("Data", questList); + questCompound.setTag("CatNBT", questCategory.writeSmallNBT(new NBTTagCompound())); + return questCompound; + } + + public static NBTTagCompound updateDialogCat(DialogCategory dialogCategory) { + NBTTagCompound dialogCompound = new NBTTagCompound(); + NBTTagList dialogList = new NBTTagList(); + for (int questID : dialogCategory.dialogs.keySet()) { + Dialog dialog = dialogCategory.dialogs.get(questID); + dialogList.appendTag(dialog.writeToNBT(new NBTTagCompound())); + } + dialogCompound.setTag("Data", dialogList); + dialogCompound.setTag("CatNBT", dialogCategory.writeSmallNBT(new NBTTagCompound())); + return dialogCompound; + } + + @SideOnly(Side.CLIENT) + public static void clientSyncRemove(SyncType syncType, int id, int revision) { + if (syncType == SyncType.FACTION) { + FactionController.getInstance().factions.remove(id); + } else if (syncType == SyncType.DIALOG) { + Dialog dialog = DialogController.Instance.dialogs.remove(id); + if (dialog != null) { + dialog.category.dialogs.remove(id); + } + } else if (syncType == SyncType.DIALOG_CATEGORY) { + DialogCategory dialogCategory = DialogController.Instance.categories.remove(id); + if (dialogCategory != null) { + DialogController.Instance.dialogs.keySet().removeAll(dialogCategory.dialogs.keySet()); + } + } else if (syncType == SyncType.QUEST) { + Quest quest = QuestController.Instance.quests.remove(id); + if (quest != null) { + quest.category.quests.remove(id); + } + } else if (syncType == SyncType.QUEST_CATEGORY) { + QuestCategory questCategory = QuestController.Instance.categories.remove(id); + if (questCategory != null) { + QuestController.Instance.quests.keySet().removeAll(questCategory.quests.keySet()); + } + } else if (syncType == SyncType.MAGIC) { + for (MagicCycle cycle : MagicController.getInstance().cycles.values()) { + cycle.associations.remove(id); + } + MagicController.getInstance().cycles.remove(id); + } else if (syncType == SyncType.MAGIC_CYCLE) { + MagicController.getInstance().cycles.remove(id); + } else if (syncType == SyncType.CUSTOM_EFFECTS) { + CustomEffectController.Instance.getCustomEffects().remove(id); + } + + ClientCacheHandler.updateClientRevision(syncType, revision); + } + + public static void syncEffects(EntityPlayerMP playerMP) { + ConcurrentHashMap playerEffects = CustomEffectController.getInstance().getPlayerEffects(playerMP); + PlayerData playerData = PlayerData.get(playerMP); + playerData.effectData.setEffects(playerEffects); + + NBTTagCompound compound = playerData.getPlayerEffects(); + PacketHandler.Instance.sendToPlayer(new SyncEffectPacket(compound), playerMP); + } + + /** + * Full sync of all player ability state to the client: unlocked abilities, + * selection, toggles, hotbar layout, and cooldowns. + * Used for entity reconstruction and other full-refresh scenarios. + */ + public static void syncAbilities(EntityPlayerMP playerMP) { + PlayerData playerData = PlayerData.get(playerMP); + if (playerData == null) return; + + PlayerAbilitySyncPacket.sendToPlayer(playerMP); + AbilityHotbarSyncPacket.sendToPlayer(playerMP); + AbilityCooldownSyncPacket.sendToPlayer(playerMP); + } + + /** + * Lightweight sync of cooldown state only (global + per-ability). + * Called periodically (every 10 ticks) to keep the client's cooldown + * display accurate. Cooldowns are transient and not part of NBT sync. + */ + public static void syncAbilityCooldowns(EntityPlayerMP playerMP) { + PlayerData playerData = PlayerData.get(playerMP); + if (playerData == null) return; + + AbilityCooldownSyncPacket.sendToPlayer(playerMP); + } + + @SideOnly(Side.CLIENT) + public static void clientSyncEffects(NBTTagCompound compound) { + PlayerData playerData = PlayerData.get(Minecraft.getMinecraft().thePlayer); + if (playerData != null) { + playerData.setPlayerEffects(compound); + } + } + + private static void registerCache(SyncType type, Supplier supplier) { + cacheEntries.put(type, new SyncCacheEntry(supplier)); + } + + private static CachedSyncPayload rebuildNow(SyncType type) { + SyncCacheEntry entry = cacheEntries.get(type); + if (entry == null) { + return null; + } + entry.invalidate(); + return entry.getPayload(type); + } + + public static Map invalidateCaches(SyncType type) { + Set targets = getInvalidationTargets(type); + Map revisions = new LinkedHashMap<>(); + for (SyncType target : targets) { + SyncCacheEntry entry = cacheEntries.get(target); + if (entry != null) { + int newRevision = entry.invalidate(); + revisions.put(target, newRevision); + } + } + return revisions; + } + + private static Set getInvalidationTargets(SyncType type) { + if (type == SyncType.DIALOG || type == SyncType.DIALOG_CATEGORY) { + Set set = new HashSet<>(); + set.add(SyncType.DIALOG_CATEGORY); + return set; + } else if (type == SyncType.QUEST || type == SyncType.QUEST_CATEGORY) { + Set set = new HashSet<>(); + set.add(SyncType.QUEST_CATEGORY); + return set; + } else if (type == SyncType.FACTION) { + Set set = new HashSet<>(); + set.add(SyncType.FACTION); + return set; + } else if (type == SyncType.MAGIC) { + Set set = new HashSet<>(); + set.add(SyncType.MAGIC); + return set; + } else if (type == SyncType.MAGIC_CYCLE) { + Set set = new HashSet<>(); + set.add(SyncType.MAGIC_CYCLE); + return set; + } else if (type == SyncType.WORKBENCH_RECIPES) { + Set set = new HashSet<>(); + set.add(SyncType.WORKBENCH_RECIPES); + return set; + } else if (type == SyncType.CARPENTRY_RECIPES) { + Set set = new HashSet<>(); + set.add(SyncType.CARPENTRY_RECIPES); + return set; + } else if (type == SyncType.ANVIL_RECIPES) { + Set set = new HashSet<>(); + set.add(SyncType.ANVIL_RECIPES); + return set; + } else if (type == SyncType.CUSTOM_EFFECTS) { + Set set = new HashSet<>(); + set.add(SyncType.CUSTOM_EFFECTS); + return set; + } else if (type == SyncType.CUSTOM_ABILITY) { + Set set = new HashSet<>(); + set.add(SyncType.CUSTOM_ABILITY); + return set; + } else if (type == SyncType.CHAINED_ABILITY) { + Set set = new HashSet<>(); + set.add(SyncType.CHAINED_ABILITY); + return set; + } + return Collections.emptySet(); + } + + private static void updateAllPlayerRevisions(SyncType type, int revision) { + if (revision < 0) { + return; + } + for (PlayerSyncState state : playerSyncState.values()) { + state.updateRevision(type, revision); + } + } + + private static void updateAllPlayerRevisions(Map revisions) { + if (revisions.isEmpty()) { + return; + } + for (PlayerSyncState state : playerSyncState.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(Supplier supplier) { + this.supplier = supplier; + } + + private synchronized CachedSyncPayload getPayload(SyncType requestedType) { + if (!dirty && payload != null) { + return payload; + } + + int payloadRevision = revision; + NBTTagCompound data = supplier.get(); + 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; + return payload; + } + + private synchronized int invalidate() { + dirty = true; + payload = null; + revision++; + return revision; + } + + private synchronized int getRevisionValue() { + return revision; + } + + private synchronized void reset() { + dirty = true; + payload = null; + revision = 0; + } + + 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; + } + 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 Map revisions = new HashMap<>(); + + private PlayerSyncState(UUID ignored) { + } + + private synchronized int getRevision(SyncType type) { + Integer value = revisions.get(type); + return value == null ? -1 : value; + } + + private synchronized void updateRevision(SyncType type, int revision) { + revisions.put(type, revision); + } + + private synchronized void applyHandshake(Map incoming) { + if (incoming == null || incoming.isEmpty()) { + return; + } + revisions.putAll(incoming); + } + + private synchronized void reset() { + revisions.clear(); + } + + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java b/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java index f7d6ded84..7d77317a0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java @@ -1,6 +1,6 @@ package kamkeel.npcs.controllers.data.profile; -import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.sync.handlers.PlayerDataSyncHandler; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; @@ -30,7 +30,7 @@ public class CNPCData implements IProfileData { public void save(EntityPlayer player) { PlayerData customNPCData = PlayerData.get(player); customNPCData.save(); - SyncController.syncPlayerData((EntityPlayerMP) player, false); + PlayerDataSyncHandler.getInstance().syncPlayerData((EntityPlayerMP) player, false); } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/sync/SyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/SyncHandler.java new file mode 100644 index 000000000..5cfe5e7a6 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/SyncHandler.java @@ -0,0 +1,135 @@ +package kamkeel.npcs.controllers.sync; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; + +import java.util.HashSet; +import java.util.Set; + +/** + * Contract for a synchronized data type. Implementors handle both + * server-side serialization and client-side deserialization for their + * specific data type. + * + *

Each handler is registered with {@link SyncRegistry} and covers + * all three sync actions: RELOAD (full), UPDATE (single entity), + * and REMOVE (delete entity).

+ * + *

Handlers that don't support UPDATE or REMOVE should use the + * default no-op implementations.

+ */ +public interface SyncHandler { + + // ========== SERVER-SIDE ========== + + /** + * Serialize ALL objects of this type into an NBTTagCompound. + * This is the supplier for the cache system. + * + *

Called lazily when the cache is dirty and a player needs data. + * The returned compound is cached and chunked for transmission.

+ * + * @return the serialized compound, or null if this type has no independent cache + */ + NBTTagCompound serializeAll(); + + + // ========== CLIENT-SIDE ========== + + /** + * Handle a RELOAD action on the client. Deserialize the full + * dataset and replace the client-side data. + * + *

Implementations should: + *

    + *
  1. Deserialize from the compound into a sync buffer
  2. + *
  3. Atomically swap the sync buffer to the primary map
  4. + *
  5. Reset the sync buffer for the next reload
  6. + *
+ * + * @param fullCompound the full serialized dataset from {@link #serializeAll()} + */ + @SideOnly(Side.CLIENT) + void clientHandleReload(NBTTagCompound fullCompound); + + /** + * Handle an UPDATE action on the client. Deserialize a single + * entity and insert/replace it in the client-side data. + * + *

Default: no-op. Types that don't support individual updates + * use {@code syncAll()} for full reload instead.

+ * + * @param compound the serialized single entity + * @param categoryId the parent category ID (-1 if not applicable) + */ + @SideOnly(Side.CLIENT) + default void clientHandleUpdate(NBTTagCompound compound, int categoryId) { + // No-op: type does not support individual updates + } + + /** + * Removes a single object from client-side data. + * Full object NBTTagCompound sent finer query control + * Int-keyed handlers should override {@link #clientHandleRemove(int)} instead. + */ + @SideOnly(Side.CLIENT) + default void clientHandleRemove(NBTTagCompound compound) { + } + + /** + * Removes a single object with the given ID from client-side data. + * + * @param id the ID of the object to remove + */ + @SideOnly(Side.CLIENT) + default void clientHandleRemove(int id) { + } + + /** + * Whether this handler supports the UPDATE action. + * Used for validation and self-documentation. + */ + default boolean supportsUpdate() { + return false; + } + + /** + * Whether this handler supports the REMOVE action. + * Used for validation and self-documentation. + */ + default boolean supportsRemove() { + return false; + } + + /** + * Whether this type should be included in login sync. + * Types that return true will be sent to players on login + * and checked for revision deltas on reconnect. + * + *

Default: true. Override to false for types like PLAYERDATA + * that are synced through separate mechanisms, or child types + * (DIALOG, QUEST) that share their parent's cache.

+ */ + default boolean isCachedType() { + return true; + } + + /** + * Get the set of cache types that must be invalidated when + * this type changes. Used for cascade invalidation. + * + *

Most types return just themselves. Parent-child types + * (e.g. DIALOG maps to DIALOG_CATEGORY) point children + * to the parent's cache.

+ * + * @param self the SyncType this handler is registered under + * @return the set of types to invalidate + */ + default Set getInvalidationTargets(SyncType self) { + Set set = new HashSet<>(); + set.add(self); + return set; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/SyncRegistry.java b/src/main/java/kamkeel/npcs/controllers/sync/SyncRegistry.java new file mode 100644 index 000000000..a13568797 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/SyncRegistry.java @@ -0,0 +1,98 @@ +package kamkeel.npcs.controllers.sync; + +import kamkeel.npcs.network.enums.SyncType; +import noppes.npcs.LogWriter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public final class SyncRegistry { + + private static final LinkedHashMap handlers = new LinkedHashMap<>(); + + private static SyncType[] loginTypesCache = null; + private static boolean frozen = false; + + private SyncRegistry() { + } + + public static void register(SyncType type, SyncHandler handler) { + if (type == null) { + throw new IllegalArgumentException("SyncType must not be null"); + } + if (handler == null) { + throw new IllegalArgumentException("SyncHandler must not be null for " + type); + } + if (frozen) { + throw new IllegalStateException( + "SyncRegistry is frozen; cannot register handler for " + type); + } + if (handlers.containsKey(type)) { + throw new IllegalStateException( + "SyncHandler already registered for " + type); + } + handlers.put(type, handler); + loginTypesCache = null; + } + + public static SyncHandler getHandler(SyncType type) { + return handlers.get(type); + } + + public static SyncType[] getLoginTypes() { + if (loginTypesCache == null) { + List list = new ArrayList<>(); + for (Map.Entry e : handlers.entrySet()) { + if (e.getValue().isCachedType()) { + list.add(e.getKey()); + } + } + loginTypesCache = list.toArray(new SyncType[0]); + } + return loginTypesCache; + } + + public static Set getInvalidationTargets(SyncType type) { + SyncHandler handler = handlers.get(type); + if (handler == null) { + return Collections.emptySet(); + } + return handler.getInvalidationTargets(type); + } + + public static void clear() { + handlers.clear(); + loginTypesCache = null; + frozen = false; + } + + public static void freeze() { + frozen = true; + } + + public static boolean isFrozen() { + return frozen; + } + + public static void validateRegistrations() { + for (SyncType type : SyncType.values()) { + if (!handlers.containsKey(type)) { + LogWriter.info("[SyncRegistry] No SyncHandler registered for " + type); + } + } + } + + public static boolean isRegistered(SyncType type) { + return handlers.containsKey(type); + } + + public static Collection getRegisteredTypes() { + return Collections.unmodifiableCollection(handlers.keySet()); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/AnvilRecipeSyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/AnvilRecipeSyncHandler.java new file mode 100644 index 000000000..caaad94d9 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/AnvilRecipeSyncHandler.java @@ -0,0 +1,56 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.controllers.RecipeController; +import noppes.npcs.controllers.data.RecipeAnvil; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +/** + * Sync handler for {@link SyncType#ANVIL_RECIPES}. + * RELOAD-only family — does not support individual UPDATE or REMOVE. + */ +public class AnvilRecipeSyncHandler implements SyncHandler { + + // ========== SERVER-SIDE ========== + + @Override + public NBTTagCompound serializeAll() { + RecipeController controller = RecipeController.Instance; + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (RecipeAnvil recipe : controller.anvilRecipes.values()) { + list.appendTag(recipe.writeNBT(false)); + } + compound.setTag("recipes", list); + return compound; + } + + // ========== CLIENT-SIDE ========== + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleReload(NBTTagCompound fullCompound) { + NBTTagList list = fullCompound.getTagList("recipes", 10); + if (list == null) + return; + + for (int i = 0; i < list.tagCount(); i++) { + RecipeAnvil recipe = new RecipeAnvil(); + recipe.readNBT(list.getCompoundTagAt(i)); + RecipeController.syncAnvilRecipes.put(recipe.id, recipe); + } + + RecipeController.Instance.anvilRecipes = RecipeController.syncAnvilRecipes; + RecipeController.syncAnvilRecipes = new HashMap<>(); + } + + // RELOAD-only: supportsUpdate() and supportsRemove() remain false (default) +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/CarpentryRecipeSyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/CarpentryRecipeSyncHandler.java new file mode 100644 index 000000000..5213dc227 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/CarpentryRecipeSyncHandler.java @@ -0,0 +1,63 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.controllers.RecipeController; +import noppes.npcs.controllers.data.RecipeCarpentry; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +/** + * Sync handler for {@link SyncType#CARPENTRY_RECIPES}. + * RELOAD-only family — does not support individual UPDATE or REMOVE. + */ +public class CarpentryRecipeSyncHandler implements SyncHandler { + + private static final CarpentryRecipeSyncHandler INSTANCE = new CarpentryRecipeSyncHandler(); + + public static CarpentryRecipeSyncHandler getInstance() { + return INSTANCE; + } + + // ========== SERVER-SIDE ========== + + @Override + public NBTTagCompound serializeAll() { + RecipeController controller = RecipeController.Instance; + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (RecipeCarpentry recipe : controller.carpentryRecipes.values()) { + list.appendTag(recipe.writeNBT(false)); + } + compound.setTag("recipes", list); + return compound; + } + + // ========== CLIENT-SIDE ========== + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleReload(NBTTagCompound fullCompound) { + NBTTagList list = fullCompound.getTagList("recipes", 10); + if (list == null) + return; + + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound recipeCompound = list.getCompoundTagAt(i); + RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); + recipe.readNBT(recipeCompound); + RecipeController.syncRecipes.put(recipe.id, recipe); + } + + RecipeController.Instance.carpentryRecipes = RecipeController.syncRecipes; + RecipeController.syncRecipes = new HashMap<>(); + } + + // RELOAD-only: supportsUpdate() and supportsRemove() remain false (default) +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/ChainedAbilitySyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/ChainedAbilitySyncHandler.java new file mode 100644 index 000000000..59600770d --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/ChainedAbilitySyncHandler.java @@ -0,0 +1,58 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * Sync handler for {@link SyncType#CHAINED_ABILITY}. + * String-keyed family using ordered {@link LinkedHashMap} to preserve + * registration/reload order. Supports RELOAD only for global sync. + */ +public class ChainedAbilitySyncHandler implements SyncHandler { + + // ========== SERVER-SIDE ========== + + @Override + public NBTTagCompound serializeAll() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (Map.Entry entry : AbilityController.Instance.getChainedAbilities().entrySet()) { + NBTTagCompound chainNBT = entry.getValue().writeNBT(false); + chainNBT.setString("ChainedAbilityId", entry.getKey()); + list.appendTag(chainNBT); + } + compound.setTag("Data", list); + return compound; + } + + // ========== CLIENT-SIDE ========== + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleReload(NBTTagCompound fullCompound) { + NBTTagList list = fullCompound.getTagList("Data", 10); + LinkedHashMap sync = new LinkedHashMap<>(); + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound nbt = list.getCompoundTagAt(i); + String id = nbt.getString("ChainedAbilityId"); + ChainedAbility chain = new ChainedAbility(); + chain.readNBT(nbt); + chain.setName(id); + sync.put(id, chain); + } + AbilityController.Instance.setChainedAbilities(sync); + } + + // RELOAD-only for global sync: supportsUpdate() and supportsRemove() remain false (default) +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/CustomAbilitySyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/CustomAbilitySyncHandler.java new file mode 100644 index 000000000..16a808797 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/CustomAbilitySyncHandler.java @@ -0,0 +1,59 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * Sync handler for {@link SyncType#CUSTOM_ABILITY}. + * String-keyed family using ordered {@link LinkedHashMap} to preserve + * registration/reload order. Supports RELOAD only for global sync. + */ +public class CustomAbilitySyncHandler implements SyncHandler { + + // ========== SERVER-SIDE ========== + + @Override + public NBTTagCompound serializeAll() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (Map.Entry entry : AbilityController.Instance.getCustomAbilities().entrySet()) { + NBTTagCompound abilityNBT = entry.getValue().writeNBT(false); + abilityNBT.setString("CustomAbilityId", entry.getKey()); + list.appendTag(abilityNBT); + } + compound.setTag("Data", list); + return compound; + } + + // ========== CLIENT-SIDE ========== + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleReload(NBTTagCompound fullCompound) { + NBTTagList list = fullCompound.getTagList("Data", 10); + LinkedHashMap sync = new LinkedHashMap<>(); + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound nbt = list.getCompoundTagAt(i); + String name = nbt.getString("CustomAbilityId"); + Ability ability = AbilityController.Instance.fromNBT(nbt); + if (ability != null) { + ability.setName(name); + sync.put(name, ability); + } + } + AbilityController.Instance.setCustomAbilities(sync); + } + + // RELOAD-only for global sync: supportsUpdate() and supportsRemove() remain false (default) +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/CustomEffectSyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/CustomEffectSyncHandler.java new file mode 100644 index 000000000..ed1d8012e --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/CustomEffectSyncHandler.java @@ -0,0 +1,82 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.client.ClientCacheHandler; +import noppes.npcs.controllers.CustomEffectController; +import noppes.npcs.controllers.data.CustomEffect; + +import java.util.HashSet; +import java.util.Set; + +/** + * Sync handler for {@link SyncType#CUSTOM_EFFECTS}. + * Simple int-keyed map family with RELOAD, UPDATE, and REMOVE support. + * Client-side RELOAD precaches effect icons via {@link ClientCacheHandler#getImageData}. + */ +public class CustomEffectSyncHandler implements SyncHandler { + + // ========== SERVER-SIDE ========== + + @Override + public NBTTagCompound serializeAll() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (CustomEffect effect : CustomEffectController.getInstance().getCustomEffects().values()) { + list.appendTag(effect.writeToNBT(false)); + } + compound.setTag("Data", list); + return compound; + } + + // ========== CLIENT-SIDE ========== + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleReload(NBTTagCompound fullCompound) { + NBTTagList list = fullCompound.getTagList("Data", 10); + CustomEffectController ce = CustomEffectController.getInstance(); + ce.customEffectsSync.clear(); + for (int i = 0; i < list.tagCount(); i++) { + CustomEffect effect = new CustomEffect(); + effect.readFromNBT(list.getCompoundTagAt(i)); + ClientCacheHandler.getImageData(effect.icon); + ce.customEffectsSync.put(effect.id, effect); + } + + ce.getCustomEffects().clear(); + ce.getCustomEffects().putAll(ce.customEffectsSync); + ce.customEffectsSync.clear(); + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleUpdate(NBTTagCompound compound, int categoryId) { + CustomEffect effect = new CustomEffect(); + effect.readFromNBT(compound); + ClientCacheHandler.getImageData(effect.icon); + + CustomEffectController.Instance.getCustomEffects().put(effect.id, effect); + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleRemove(int id) { + CustomEffectController.Instance.getCustomEffects().remove(id); + } + + @Override + public boolean supportsUpdate() { + return true; + } + + @Override + public boolean supportsRemove() { + return true; + } + +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/DialogCategorySyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/DialogCategorySyncHandler.java new file mode 100644 index 000000000..45d8dcce8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/DialogCategorySyncHandler.java @@ -0,0 +1,140 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.controllers.DialogController; +import noppes.npcs.controllers.data.Dialog; +import noppes.npcs.controllers.data.DialogCategory; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +/** + * Sync handler for {@link SyncType#DIALOG_CATEGORY}. + * Parent-child category family. DIALOG children share this cache. + * Invalidation from DIALOG or DIALOG_CATEGORY both target this cache. + */ +public class DialogCategorySyncHandler implements SyncHandler { + + /** + * Serialize a single dialog category for UPDATE packets. + */ + public static NBTTagCompound serializeCategory(DialogCategory dialogCategory) { + NBTTagCompound dialogCompound = new NBTTagCompound(); + NBTTagList dialogList = new NBTTagList(); + for (int questID : dialogCategory.dialogs.keySet()) { + Dialog dialog = dialogCategory.dialogs.get(questID); + dialogList.appendTag(dialog.writeToNBT(new NBTTagCompound())); + } + dialogCompound.setTag("Data", dialogList); + dialogCompound.setTag("CatNBT", dialogCategory.writeSmallNBT(new NBTTagCompound())); + return dialogCompound; + } + + // ========== SERVER-SIDE ========== + + @Override + public NBTTagCompound serializeAll() { + NBTTagCompound compound = new NBTTagCompound(); + NBTTagList categoryList = new NBTTagList(); + for (DialogCategory category : DialogController.Instance.categories.values()) { + NBTTagCompound questCompound = new NBTTagCompound(); + NBTTagList dialogList = new NBTTagList(); + for (int dialogID : category.dialogs.keySet()) { + Dialog quest = category.dialogs.get(dialogID); + dialogList.appendTag(quest.writeToNBT(new NBTTagCompound())); + } + questCompound.setTag("Data", dialogList); + questCompound.setTag("CatNBT", category.writeSmallNBT(new NBTTagCompound())); + categoryList.appendTag(questCompound); + } + compound.setTag("DialogCategories", categoryList); + return compound; + } + + + // ========== CLIENT-SIDE ========== + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleReload(NBTTagCompound fullCompound) { + if (!fullCompound.hasNoTags()) { + NBTTagList categories = fullCompound.getTagList("DialogCategories", 10); + for (int j = 0; j < fullCompound.getTagList("DialogCategories", 10).tagCount(); j++) { + NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); + if (categoryCompound.hasNoTags()) + continue; + + DialogCategory category = new DialogCategory(); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + NBTTagList dialogList = categoryCompound.getTagList("Data", 10); + if (DialogController.Instance.categoriesSync.containsKey(category.id)) { + category = DialogController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < dialogList.tagCount(); i++) { + Dialog dialog = new Dialog(); + dialog.readNBT(dialogList.getCompoundTagAt(i)); + dialog.category = category; + category.dialogs.put(dialog.id, dialog); + } + DialogController.Instance.categoriesSync.put(category.id, category); + } + } + + HashMap dialogs = new HashMap<>(); + for (DialogCategory category : DialogController.Instance.categoriesSync.values()) { + for (Dialog dialog : category.dialogs.values()) { + dialogs.put(dialog.id, dialog); + } + } + + DialogController.Instance.categories = DialogController.Instance.categoriesSync; + DialogController.Instance.dialogs = dialogs; + DialogController.Instance.categoriesSync = new HashMap<>(); + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleUpdate(NBTTagCompound compound, int categoryId) { + DialogCategory category = new DialogCategory(); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + NBTTagList list = compound.getTagList("Data", 10); + if (DialogController.Instance.categoriesSync.containsKey(category.id)) { + category = DialogController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < list.tagCount(); i++) { + Dialog dialog = new Dialog(); + dialog.readNBT(list.getCompoundTagAt(i)); + dialog.category = category; + category.dialogs.put(dialog.id, dialog); + } + DialogController.Instance.categories.put(category.id, category); + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleRemove(int id) { + DialogCategory dialogCategory = DialogController.Instance.categories.remove(id); + if (dialogCategory != null) { + DialogController.Instance.dialogs.keySet().removeAll(dialogCategory.dialogs.keySet()); + } + } + + @Override + public boolean supportsUpdate() { + return true; + } + + @Override + public boolean supportsRemove() { + return true; + } + +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/DialogSyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/DialogSyncHandler.java new file mode 100644 index 000000000..df0b82259 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/DialogSyncHandler.java @@ -0,0 +1,89 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.controllers.DialogController; +import noppes.npcs.controllers.data.Dialog; +import noppes.npcs.controllers.data.DialogCategory; + +import java.util.HashSet; +import java.util.Set; + +/** + * Sync handler for {@link SyncType#DIALOG}. + * Child family — does not have its own cache. UPDATE and REMOVE only. + * Invalidation targets the parent {@link SyncType#DIALOG_CATEGORY} cache. + */ +public class DialogSyncHandler implements SyncHandler { + + // ========== SERVER-SIDE ========== + + /** + * DIALOG does not have its own independent cache; it shares + * the DIALOG_CATEGORY parent cache. + */ + @Override + public NBTTagCompound serializeAll() { + return null; + } + + // ========== CLIENT-SIDE ========== + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleReload(NBTTagCompound fullCompound) { + // DIALOG does not handle standalone RELOAD; it's part of DIALOG_CATEGORY + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleUpdate(NBTTagCompound compound, int categoryId) { + DialogCategory category = DialogController.Instance.categories.get(categoryId); + if (category == null) { + return; // Defensive: category may have been removed + } + Dialog dialog = new Dialog(); + dialog.category = category; + dialog.readNBT(compound); + DialogController.Instance.dialogs.put(dialog.id, dialog); + category.dialogs.put(dialog.id, dialog); + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleRemove(int id) { + Dialog dialog = DialogController.Instance.dialogs.remove(id); + if (dialog != null) { + dialog.category.dialogs.remove(id); + } + } + + @Override + public boolean supportsUpdate() { + return true; + } + + @Override + public boolean supportsRemove() { + return true; + } + + /** + * Not a cached type — DIALOG shares the DIALOG_CATEGORY cache. + */ + @Override + public boolean isCachedType() { + return false; + } + + @Override + public Set getInvalidationTargets(SyncType self) { + // Child invalidates the parent cache + Set set = new HashSet<>(); + set.add(SyncType.DIALOG_CATEGORY); + return set; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/FactionSyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/FactionSyncHandler.java new file mode 100644 index 000000000..2639567db --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/FactionSyncHandler.java @@ -0,0 +1,76 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.controllers.FactionController; +import noppes.npcs.controllers.data.Faction; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +/** + * Sync handler for {@link SyncType#FACTION}. + * Simple int-keyed map family with RELOAD, UPDATE, and REMOVE support. + */ +public class FactionSyncHandler implements SyncHandler { + + // ========== SERVER-SIDE ========== + + @Override + public NBTTagCompound serializeAll() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (Faction faction : FactionController.getInstance().factions.values()) { + NBTTagCompound factionNBT = new NBTTagCompound(); + faction.writeNBT(factionNBT); + list.appendTag(factionNBT); + } + compound.setTag("Factions", list); + return compound; + } + + // ========== CLIENT-SIDE ========== + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleReload(NBTTagCompound fullCompound) { + NBTTagList list = fullCompound.getTagList("Factions", 10); + for (int i = 0; i < list.tagCount(); i++) { + Faction faction = new Faction(); + faction.readNBT(list.getCompoundTagAt(i)); + FactionController.getInstance().factionsSync.put(faction.id, faction); + } + FactionController.getInstance().factions = FactionController.getInstance().factionsSync; + FactionController.getInstance().factionsSync = new HashMap(); + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleUpdate(NBTTagCompound compound, int categoryId) { + Faction faction = new Faction(); + faction.readNBT(compound); + FactionController.getInstance().factions.put(faction.id, faction); + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleRemove(int id) { + FactionController.getInstance().factions.remove(id); + } + + @Override + public boolean supportsUpdate() { + return true; + } + + @Override + public boolean supportsRemove() { + return true; + } + +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/MagicCycleSyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/MagicCycleSyncHandler.java new file mode 100644 index 000000000..37cbb3091 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/MagicCycleSyncHandler.java @@ -0,0 +1,79 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.controllers.MagicController; +import noppes.npcs.controllers.data.MagicCycle; + +import java.util.HashSet; +import java.util.Set; + +/** + * Sync handler for {@link SyncType#MAGIC_CYCLE}. + * Simple int-keyed map family with RELOAD, UPDATE, and REMOVE support. + */ +public class MagicCycleSyncHandler implements SyncHandler { + + // ========== SERVER-SIDE ========== + + @Override + public NBTTagCompound serializeAll() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (MagicCycle cycle : MagicController.getInstance().cycles.values()) { + NBTTagCompound cycleCompound = new NBTTagCompound(); + cycle.writeNBT(cycleCompound); + list.appendTag(cycleCompound); + } + compound.setTag("Data", list); + return compound; + } + + // ========== CLIENT-SIDE ========== + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleReload(NBTTagCompound fullCompound) { + NBTTagList list = fullCompound.getTagList("Data", 10); + MagicController mc = MagicController.getInstance(); + mc.cyclesSync.clear(); + for (int i = 0; i < list.tagCount(); i++) { + MagicCycle cycle = new MagicCycle(); + cycle.readNBT(list.getCompoundTagAt(i)); + mc.cyclesSync.put(cycle.id, cycle); + } + + mc.cycles.clear(); + mc.cycles.putAll(mc.cyclesSync); + mc.cyclesSync.clear(); + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleUpdate(NBTTagCompound compound, int categoryId) { + MagicCycle cycle = new MagicCycle(); + cycle.readNBT(compound); + MagicController.getInstance().cycles.put(cycle.id, cycle); + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleRemove(int id) { + MagicController.getInstance().cycles.remove(id); + } + + @Override + public boolean supportsUpdate() { + return true; + } + + @Override + public boolean supportsRemove() { + return true; + } + +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/MagicSyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/MagicSyncHandler.java new file mode 100644 index 000000000..86d965cc4 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/MagicSyncHandler.java @@ -0,0 +1,95 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.controllers.MagicController; +import noppes.npcs.controllers.data.Magic; +import noppes.npcs.controllers.data.MagicCycle; + +import java.util.HashSet; +import java.util.Set; + +/** + * Sync handler for {@link SyncType#MAGIC}. + * Simple int-keyed map family. + * REMOVE dissociates cycle associations before removing the magic entry. + * + *

Note: The original code had a bug where MAGIC REMOVE incorrectly + * removed from {@code cycles} instead of {@code magics}. This handler + * fixes that by properly removing from {@code magics} after dissociating.

+ */ +public class MagicSyncHandler implements SyncHandler { + + // ========== SERVER-SIDE ========== + + @Override + public NBTTagCompound serializeAll() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (Magic magic : MagicController.getInstance().magics.values()) { + NBTTagCompound magicCompound = new NBTTagCompound(); + magic.writeNBT(magicCompound); + list.appendTag(magicCompound); + } + compound.setTag("Data", list); + return compound; + } + + // ========== CLIENT-SIDE ========== + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleReload(NBTTagCompound fullCompound) { + NBTTagList list = fullCompound.getTagList("Data", 10); + MagicController mc = MagicController.getInstance(); + mc.magicSync.clear(); + for (int i = 0; i < list.tagCount(); i++) { + Magic magic = new Magic(); + magic.readNBT(list.getCompoundTagAt(i)); + mc.magicSync.put(magic.id, magic); + } + + mc.magics.clear(); + mc.magics.putAll(mc.magicSync); + mc.magicSync.clear(); + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleUpdate(NBTTagCompound compound, int categoryId) { + Magic magic = new Magic(); + magic.readNBT(compound); + MagicController.getInstance().magics.put(magic.id, magic); + } + + /** + * Removes a magic entry from the client. First dissociates cycle + * references, then removes from the magics map. + *

Bug fix: The original code incorrectly removed from {@code cycles} + * instead of {@code magics} after dissociating cycle associations.

+ */ + @SideOnly(Side.CLIENT) + @Override + public void clientHandleRemove(int id) { + // First remove this magic's association from all cycles + for (MagicCycle cycle : MagicController.getInstance().cycles.values()) { + cycle.associations.remove(id); + } + // Then remove the magic itself (fixed: was cycles.remove(id)) + MagicController.getInstance().magics.remove(id); + } + + @Override + public boolean supportsUpdate() { + return true; + } + + @Override + public boolean supportsRemove() { + return true; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerAbilitySyncHelper.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerAbilitySyncHelper.java new file mode 100644 index 000000000..258b25edb --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerAbilitySyncHelper.java @@ -0,0 +1,47 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import kamkeel.npcs.network.packets.data.ability.AbilityCooldownSyncPacket; +import kamkeel.npcs.network.packets.data.ability.AbilityHotbarSyncPacket; +import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; +import net.minecraft.entity.player.EntityPlayerMP; +import noppes.npcs.controllers.data.PlayerData; + +/** + * Direct per-player sync helper for player ability state. + * This is NOT a cached global sync family — it operates outside the + * registry-driven reload/revision system. + * + *

Abilities are synced per-player on demand (e.g. ability unlock, + * entity reconstruction, periodic cooldown ticks).

+ */ +public final class PlayerAbilitySyncHelper { + + private PlayerAbilitySyncHelper() { + } + + /** + * Full sync of all player ability state to the client: unlocked abilities, + * selection, toggles, hotbar layout, and cooldowns. + * Used for entity reconstruction and other full-refresh scenarios. + */ + public static void syncAbilities(EntityPlayerMP playerMP) { + PlayerData playerData = PlayerData.get(playerMP); + if (playerData == null) return; + + PlayerAbilitySyncPacket.sendToPlayer(playerMP); + AbilityHotbarSyncPacket.sendToPlayer(playerMP); + AbilityCooldownSyncPacket.sendToPlayer(playerMP); + } + + /** + * Lightweight sync of cooldown state only (global + per-ability). + * Called periodically (every 10 ticks) to keep the client's cooldown + * display accurate. Cooldowns are transient and not part of NBT sync. + */ + public static void syncAbilityCooldowns(EntityPlayerMP playerMP) { + PlayerData playerData = PlayerData.get(playerMP); + if (playerData == null) return; + + AbilityCooldownSyncPacket.sendToPlayer(playerMP); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerDataSyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerDataSyncHandler.java new file mode 100644 index 000000000..2842b5cee --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerDataSyncHandler.java @@ -0,0 +1,75 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumSyncAction; +import kamkeel.npcs.network.packets.data.large.SyncPacket; +import net.minecraft.entity.player.EntityPlayerMP; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.client.ClientCacheHandler; +import noppes.npcs.controllers.data.PlayerData; + +/** + * Sync handler for {@link SyncType#PLAYERDATA}. + * Special non-cached per-player path. Not included in login cache iteration. + * RELOAD and UPDATE are sent as direct per-player packets. + */ +public class PlayerDataSyncHandler implements SyncHandler { + + private static final PlayerDataSyncHandler INSTANCE = new PlayerDataSyncHandler(); + + public static PlayerDataSyncHandler getInstance() { + return INSTANCE; + } + + public void syncPlayerData(EntityPlayerMP player, boolean update) { + PlayerData data = PlayerData.get(player); + if (data != null) { + if (update) { + PacketHandler.Instance.sendToPlayer(new SyncPacket(SyncType.PLAYERDATA, EnumSyncAction.UPDATE, -1, data.getSyncNBT()), player); + } else { + PacketHandler.Instance.sendToPlayer(new SyncPacket(SyncType.PLAYERDATA, EnumSyncAction.RELOAD, -1, data.getSyncNBTFull()), player); + } + } + } + + // ========== SERVER-SIDE ========== + + /** + * PLAYERDATA is not a global cached type — it is per-player. + */ + @Override + public NBTTagCompound serializeAll() { + return null; + } + + // ========== CLIENT-SIDE ========== + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleReload(NBTTagCompound fullCompound) { + ClientCacheHandler.playerData.setSyncNBTFull(fullCompound); + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleUpdate(NBTTagCompound compound, int categoryId) { + ClientCacheHandler.playerData.setSyncNBT(compound); + } + + @Override + public boolean supportsUpdate() { + return true; + } + + /** + * Not a cached type — PLAYERDATA uses direct per-player sync. + */ + @Override + public boolean isCachedType() { + return false; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerEffectSyncHelper.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerEffectSyncHelper.java new file mode 100644 index 000000000..52244de44 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerEffectSyncHelper.java @@ -0,0 +1,54 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.packets.data.large.SyncEffectPacket; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.controllers.CustomEffectController; +import noppes.npcs.controllers.data.EffectKey; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerEffect; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * Direct per-player sync helper for player effects. + * This is NOT a cached global sync family — it operates outside the + * registry-driven reload/revision system. + * + *

Effects are synced per-player on demand (e.g. effect application, + * periodic ticks). The data mutates {@code playerData.effectData} + * before sending the packet.

+ */ +public final class PlayerEffectSyncHelper { + + private PlayerEffectSyncHelper() { + } + + /** + * Server-side: sync the player's active effects to their client. + * Mutates {@code playerData.effectData} before sending. + */ + public static void syncEffects(EntityPlayerMP playerMP) { + ConcurrentHashMap playerEffects = CustomEffectController.getInstance().getPlayerEffects(playerMP); + PlayerData playerData = PlayerData.get(playerMP); + playerData.effectData.setEffects(playerEffects); + + NBTTagCompound compound = playerData.getPlayerEffects(); + PacketHandler.Instance.sendToPlayer(new SyncEffectPacket(compound), playerMP); + } + + /** + * Client-side: apply incoming effect data to the local player. + */ + @SideOnly(Side.CLIENT) + public static void clientSyncEffects(NBTTagCompound compound) { + PlayerData playerData = PlayerData.get(Minecraft.getMinecraft().thePlayer); + if (playerData != null) { + playerData.setPlayerEffects(compound); + } + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/QuestCategorySyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/QuestCategorySyncHandler.java new file mode 100644 index 000000000..91d1fc893 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/QuestCategorySyncHandler.java @@ -0,0 +1,141 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.controllers.QuestController; +import noppes.npcs.controllers.data.Quest; +import noppes.npcs.controllers.data.QuestCategory; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +/** + * Sync handler for {@link SyncType#QUEST_CATEGORY}. + * Parent-child category family. QUEST children share this cache. + * Invalidation from QUEST or QUEST_CATEGORY both target this cache. + */ +public class QuestCategorySyncHandler implements SyncHandler { + + // ========== SERVER-SIDE ========== + + @Override + public NBTTagCompound serializeAll() { + NBTTagCompound compound = new NBTTagCompound(); + NBTTagList categoryList = new NBTTagList(); + for (QuestCategory category : QuestController.Instance.categories.values()) { + NBTTagCompound questCompound = new NBTTagCompound(); + ; + NBTTagList questList = new NBTTagList(); + for (int questID : category.quests.keySet()) { + Quest quest = category.quests.get(questID); + questList.appendTag(quest.writeToNBT(new NBTTagCompound())); + } + questCompound.setTag("Data", questList); + questCompound.setTag("CatNBT", category.writeSmallNBT(new NBTTagCompound())); + categoryList.appendTag(questCompound); + } + compound.setTag("QuestCategories", categoryList); + return compound; + } + + /** + * Serialize a single quest category for UPDATE packets. + */ + public static NBTTagCompound serializeCategory(QuestCategory questCategory) { + NBTTagCompound questCompound = new NBTTagCompound(); + NBTTagList questList = new NBTTagList(); + for (int questID : questCategory.quests.keySet()) { + Quest quest = questCategory.quests.get(questID); + questList.appendTag(quest.writeToNBT(new NBTTagCompound())); + } + questCompound.setTag("Data", questList); + questCompound.setTag("CatNBT", questCategory.writeSmallNBT(new NBTTagCompound())); + return questCompound; + } + + + // ========== CLIENT-SIDE ========== + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleReload(NBTTagCompound fullCompound) { + if (!fullCompound.hasNoTags()) { + NBTTagList categories = fullCompound.getTagList("QuestCategories", 10); + for (int j = 0; j < fullCompound.getTagList("QuestCategories", 10).tagCount(); j++) { + NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); + if (categoryCompound.hasNoTags()) + continue; + + QuestCategory category = new QuestCategory(); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + NBTTagList questList = categoryCompound.getTagList("Data", 10); + if (QuestController.Instance.categoriesSync.containsKey(category.id)) { + category = QuestController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < questList.tagCount(); i++) { + Quest quest = new Quest(); + quest.readNBT(questList.getCompoundTagAt(i)); + quest.category = category; + category.quests.put(quest.id, quest); + } + QuestController.Instance.categoriesSync.put(category.id, category); + } + } + + HashMap quests = new HashMap<>(); + for (QuestCategory category : QuestController.Instance.categoriesSync.values()) { + for (Quest quest : category.quests.values()) { + quests.put(quest.id, quest); + } + } + + QuestController.Instance.categories = QuestController.Instance.categoriesSync; + QuestController.Instance.quests = quests; + QuestController.Instance.categoriesSync = new HashMap<>(); + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleUpdate(NBTTagCompound compound, int categoryId) { + QuestCategory category = new QuestCategory(); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + NBTTagList list = compound.getTagList("Data", 10); + if (QuestController.Instance.categoriesSync.containsKey(category.id)) { + category = QuestController.Instance.categoriesSync.get(category.id); + category.readSmallNBT(compound.getCompoundTag("CatNBT")); + } + for (int i = 0; i < list.tagCount(); i++) { + Quest quest = new Quest(); + quest.readNBT(list.getCompoundTagAt(i)); + quest.category = category; + category.quests.put(quest.id, quest); + } + QuestController.Instance.categories.put(category.id, category); + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleRemove(int id) { + QuestCategory questCategory = QuestController.Instance.categories.remove(id); + if (questCategory != null) { + QuestController.Instance.quests.keySet().removeAll(questCategory.quests.keySet()); + } + } + + @Override + public boolean supportsUpdate() { + return true; + } + + @Override + public boolean supportsRemove() { + return true; + } + +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/QuestSyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/QuestSyncHandler.java new file mode 100644 index 000000000..172bf5535 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/QuestSyncHandler.java @@ -0,0 +1,90 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.controllers.QuestController; +import noppes.npcs.controllers.data.Quest; +import noppes.npcs.controllers.data.QuestCategory; + +import java.util.HashSet; +import java.util.Set; + +/** + * Sync handler for {@link SyncType#QUEST}. + * Child family — does not have its own cache. UPDATE and REMOVE only. + * Invalidation targets the parent {@link SyncType#QUEST_CATEGORY} cache. + */ +public class QuestSyncHandler implements SyncHandler { + + // ========== SERVER-SIDE ========== + + /** + * QUEST does not have its own independent cache; it shares + * the QUEST_CATEGORY parent cache. + */ + @Override + public NBTTagCompound serializeAll() { + return null; + } + + + // ========== CLIENT-SIDE ========== + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleReload(NBTTagCompound fullCompound) { + // QUEST does not handle standalone RELOAD; it's part of QUEST_CATEGORY + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleUpdate(NBTTagCompound compound, int categoryId) { + QuestCategory category = QuestController.Instance.categories.get(categoryId); + if (category == null) + return; // Defensive: category may have been removed + + Quest quest = new Quest(); + quest.category = category; + quest.readNBT(compound); + QuestController.Instance.quests.put(quest.id, quest); + category.quests.put(quest.id, quest); + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleRemove(int id) { + Quest quest = QuestController.Instance.quests.remove(id); + if (quest != null) + quest.category.quests.remove(id); + + } + + @Override + public boolean supportsUpdate() { + return true; + } + + @Override + public boolean supportsRemove() { + return true; + } + + /** + * Not a cached type — QUEST shares the QUEST_CATEGORY cache. + */ + @Override + public boolean isCachedType() { + return false; + } + + @Override + public Set getInvalidationTargets(SyncType self) { + // Child invalidates the parent cache + Set set = new HashSet<>(); + set.add(SyncType.QUEST_CATEGORY); + return set; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/WorkbenchRecipeSyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/WorkbenchRecipeSyncHandler.java new file mode 100644 index 000000000..02d52f25a --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/WorkbenchRecipeSyncHandler.java @@ -0,0 +1,70 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.controllers.RecipeController; +import noppes.npcs.controllers.data.RecipeCarpentry; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +/** + * Sync handler for {@link SyncType#WORKBENCH_RECIPES}. + * RELOAD-only family — does not support individual UPDATE or REMOVE. + * Client-side RELOAD routes through {@link RecipeController#reloadGlobalRecipes}. + */ +public class WorkbenchRecipeSyncHandler implements SyncHandler { + + private static final WorkbenchRecipeSyncHandler INSTANCE = new WorkbenchRecipeSyncHandler(); + + public static WorkbenchRecipeSyncHandler getInstance() { + return INSTANCE; + } + // ========== SERVER-SIDE ========== + + @Override + public NBTTagCompound serializeAll() { + RecipeController controller = RecipeController.Instance; + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (RecipeCarpentry recipe : controller.globalRecipes.values()) { + list.appendTag(recipe.writeNBT(false)); + } + compound.setTag("recipes", list); + return compound; + } + + // ========== CLIENT-SIDE ========== + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleReload(NBTTagCompound fullCompound) { + NBTTagList list = fullCompound.getTagList("recipes", 10); + if (list == null) + return; + + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound recipeCompound = list.getCompoundTagAt(i); + RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); + recipe.readNBT(recipeCompound); + RecipeController.syncRecipes.put(recipe.id, recipe); + } + + RecipeController.reloadGlobalRecipes(RecipeController.syncRecipes); + RecipeController.syncRecipes = new HashMap<>(); + } + + @Override + public Set getInvalidationTargets(SyncType self) { + Set set = new HashSet<>(); + set.add(SyncType.WORKBENCH_RECIPES); + return set; + } + + // RELOAD-only: supportsUpdate() and supportsRemove() remain false (default) +} diff --git a/src/main/java/kamkeel/npcs/network/packets/data/large/SyncEffectPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/large/SyncEffectPacket.java index 66d7a5756..026ec44bf 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/large/SyncEffectPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/large/SyncEffectPacket.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.sync.handlers.PlayerEffectSyncHelper; import kamkeel.npcs.network.LargeAbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; @@ -54,7 +54,7 @@ public final class SyncEffectPacket extends LargeAbstractPacket { return; try { NBTTagCompound tag = ByteBufUtils.readBigNBT(data); - SyncController.clientSyncEffects(tag); + PlayerEffectSyncHelper.clientSyncEffects(tag); } catch (RuntimeException e) { LogWriter.error(String.format("Attempted to Sync Effects but it was too big")); } diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index 3133fb43c..8c1ef02c9 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -9,7 +9,9 @@ import kamkeel.npcs.addon.DBCAddon; import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.data.attribute.tracker.PlayerAttributeTracker; -import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.sync.handlers.PlayerAbilitySyncHelper; +import kamkeel.npcs.controllers.sync.handlers.PlayerDataSyncHandler; +import kamkeel.npcs.controllers.sync.handlers.PlayerEffectSyncHelper; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.type.AbilityCounter; import kamkeel.npcs.controllers.data.ability.type.AbilityDefend; @@ -141,7 +143,7 @@ public class ScriptPlayerEventHandler { } if (playerData.updateClient) { - SyncController.syncPlayerData((EntityPlayerMP) player, true); + PlayerDataSyncHandler.getInstance().syncPlayerData((EntityPlayerMP) player, true); playerData.updateClient = false; } @@ -151,8 +153,8 @@ public class ScriptPlayerEventHandler { if (player.ticksExisted % 10 == 0) { EntityPlayerMP mp = (EntityPlayerMP) player; - SyncController.syncEffects(mp); - SyncController.syncAbilityCooldowns(mp); + PlayerEffectSyncHelper.syncEffects(mp); + PlayerAbilitySyncHelper.syncAbilityCooldowns(mp); PlayerAbilitySyncPacket.sendToPlayer(mp); playerData.abilityData.ensureLockStateClear(); } diff --git a/src/main/java/noppes/npcs/ServerEventsHandler.java b/src/main/java/noppes/npcs/ServerEventsHandler.java index a8c5f2b4a..1ad41c657 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -4,6 +4,8 @@ import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.relauncher.Side; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.sync.handlers.CarpentryRecipeSyncHandler; +import kamkeel.npcs.controllers.sync.handlers.WorkbenchRecipeSyncHandler; import kamkeel.npcs.controllers.data.energycharge.EnergyChargeTracker; import kamkeel.npcs.entity.EntityEnergyBarrier; import kamkeel.npcs.entity.EntityEnergyDome; @@ -259,7 +261,7 @@ public class ServerEventsHandler { EnumSyncAction.RELOAD, -1, SyncController.getCurrentRevision(SyncType.WORKBENCH_RECIPES), - SyncController.workbenchNBT() + WorkbenchRecipeSyncHandler.getInstance().serializeAll() ), (EntityPlayerMP) player); } if (block == CustomItems.carpentyBench && event.action == Action.RIGHT_CLICK_BLOCK && !player.worldObj.isRemote) { @@ -268,7 +270,7 @@ public class ServerEventsHandler { EnumSyncAction.RELOAD, -1, SyncController.getCurrentRevision(SyncType.CARPENTRY_RECIPES), - SyncController.carpentryNBT() + CarpentryRecipeSyncHandler.getInstance().serializeAll() ), (EntityPlayerMP) player); } if ((block == CustomItems.banner || block == CustomItems.wallBanner || block == CustomItems.sign) && event.action == Action.RIGHT_CLICK_BLOCK) { diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index 12a0f29ce..52bebe1ef 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -6,6 +6,8 @@ import cpw.mods.fml.common.gameevent.TickEvent; import cpw.mods.fml.common.gameevent.TickEvent.Phase; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.sync.handlers.PlayerAbilitySyncHelper; +import kamkeel.npcs.controllers.sync.handlers.PlayerEffectSyncHelper; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.world.WorldServer; import noppes.npcs.controllers.AuctionController; @@ -45,13 +47,12 @@ public class ServerTickHandler { playerData.onLogin(); } - ProfileController.Instance.login(player); SyncController.beginLogin(player); - SyncController.syncEffects(player); + PlayerEffectSyncHelper.syncEffects(player); ScriptController.Instance.syncClientScripts(player); // Sync clean ability state to the new client (after beginLogin initializes sync state) - SyncController.syncAbilities(player); + PlayerAbilitySyncHelper.syncAbilities(player); // Send auction notifications on login if (AuctionController.Instance != null) { diff --git a/src/main/java/noppes/npcs/controllers/DialogController.java b/src/main/java/noppes/npcs/controllers/DialogController.java index 0e2df3d84..df652ac89 100644 --- a/src/main/java/noppes/npcs/controllers/DialogController.java +++ b/src/main/java/noppes/npcs/controllers/DialogController.java @@ -1,6 +1,7 @@ package noppes.npcs.controllers; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.sync.handlers.DialogCategorySyncHandler; import kamkeel.npcs.network.enums.SyncType; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; @@ -239,7 +240,7 @@ public class DialogController implements IDialogHandler { dir.mkdirs(); } categories.put(category.id, category); - SyncController.syncUpdate(SyncType.DIALOG_CATEGORY, -1, SyncController.updateDialogCat(category)); + SyncController.syncUpdate(SyncType.DIALOG_CATEGORY, -1, DialogCategorySyncHandler.serializeCategory(category)); } public void removeCategory(int category) { diff --git a/src/main/java/noppes/npcs/controllers/QuestController.java b/src/main/java/noppes/npcs/controllers/QuestController.java index 59136b2aa..69bbe88d0 100644 --- a/src/main/java/noppes/npcs/controllers/QuestController.java +++ b/src/main/java/noppes/npcs/controllers/QuestController.java @@ -1,6 +1,7 @@ package noppes.npcs.controllers; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.sync.handlers.QuestCategorySyncHandler; import kamkeel.npcs.network.enums.SyncType; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; @@ -188,7 +189,7 @@ public class QuestController implements IQuestHandler { dir.mkdirs(); } categories.put(category.id, category); - SyncController.syncUpdate(SyncType.QUEST_CATEGORY, -1, SyncController.updateQuestCat(category)); + SyncController.syncUpdate(SyncType.QUEST_CATEGORY, -1, QuestCategorySyncHandler.serializeCategory(category)); } private boolean containsCategoryName(String name) { diff --git a/src/main/java/noppes/npcs/controllers/RecipeController.java b/src/main/java/noppes/npcs/controllers/RecipeController.java index 7fccdec93..621726927 100644 --- a/src/main/java/noppes/npcs/controllers/RecipeController.java +++ b/src/main/java/noppes/npcs/controllers/RecipeController.java @@ -1,6 +1,7 @@ package noppes.npcs.controllers; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.network.enums.SyncType; import net.minecraft.inventory.InventoryCrafting; import net.minecraft.item.ItemStack; import net.minecraft.item.crafting.CraftingManager; @@ -282,12 +283,12 @@ public class RecipeController implements IRecipeHandler { return null; RecipeCarpentry globalRecipe = globalRecipes.remove(recipe.id); if (globalRecipe != null) - SyncController.syncAllWorkbenchRecipes(); + SyncController.syncAll(SyncType.WORKBENCH_RECIPES); RecipeCarpentry carpentry = carpentryRecipes.remove(recipe.id); if (carpentry != null) { carpentryScripts.remove(recipe.id); - SyncController.syncAllCarpentryRecipes(); + SyncController.syncAll(SyncType.CARPENTRY_RECIPES); } saveCategories(); @@ -302,7 +303,7 @@ public class RecipeController implements IRecipeHandler { RecipeAnvil anvilRecipe = anvilRecipes.remove(recipe.id); if (anvilRecipe != null) - SyncController.syncAllAnvilRecipes(); + SyncController.syncAll(SyncType.ANVIL_RECIPES); saveCategories(); return recipe; diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 916f2949a..8b444cbf2 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -1,7 +1,7 @@ package noppes.npcs.controllers.data; import kamkeel.npcs.controllers.AbilityController; -import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.sync.handlers.PlayerAbilitySyncHelper; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; @@ -496,23 +496,23 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA /** * Full sync of all ability state to the client. - * Delegates to {@link SyncController#syncAbilities(EntityPlayerMP)}. + * Delegates to {@link PlayerAbilitySyncHelper#syncAbilities(EntityPlayerMP)}. */ public void syncToClient() { EntityPlayer player = playerData.player; if (player instanceof EntityPlayerMP) { - SyncController.syncAbilities((EntityPlayerMP) player); + PlayerAbilitySyncHelper.syncAbilities((EntityPlayerMP) player); } } /** * Lightweight sync of cooldown state only to the client. - * Delegates to {@link SyncController#syncAbilityCooldowns(EntityPlayerMP)}. + * Delegates to {@link PlayerAbilitySyncHelper#syncAbilityCooldowns(EntityPlayerMP)}. */ public void syncCooldownToClient() { EntityPlayer player = playerData.player; if (player instanceof EntityPlayerMP) { - SyncController.syncAbilityCooldowns((EntityPlayerMP) player); + PlayerAbilitySyncHelper.syncAbilityCooldowns((EntityPlayerMP) player); } } From b28ea7226f1c4aa08ab3da5524899dde577aba06 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 31 Mar 2026 22:55:42 +0200 Subject: [PATCH 03/34] SyncController OVERHAUL PHASE #3: - Clean up, reorganization and documentation --- .../npcs/controllers/SyncController.java | 342 +++++++++--------- .../npcs/controllers/sync/SyncRegistry.java | 47 +-- .../kamkeel/npcs/network/enums/SyncType.java | 56 +-- .../packets/data/large/SyncPacket.java | 6 +- .../player/SyncRevisionInfoPacket.java | 2 +- .../java/noppes/npcs/ServerEventsHandler.java | 4 +- 6 files changed, 188 insertions(+), 269 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index 7bf1231ea..b4119ec8d 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -4,6 +4,8 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import kamkeel.npcdbc.network.DBCPacketHandler; +import kamkeel.npcdbc.network.packets.get.DBCInfoSyncPacket; import kamkeel.npcs.addon.DBCAddon; import kamkeel.npcs.controllers.sync.SyncHandler; import kamkeel.npcs.controllers.sync.SyncRegistry; @@ -20,6 +22,7 @@ import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.server.MinecraftServer; +import noppes.npcs.LogWriter; import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.controllers.data.PlayerData; @@ -59,17 +62,14 @@ import java.util.function.Supplier; */ public class SyncController { - public static boolean DEBUG_SYNC_LOGGING = false; - + /** Cached serialized reload payloads and revision state for each global sync family. */ private static final Map cacheEntries = new LinkedHashMap<>(); + + /** Last known synced revision map for each connected player. */ private static final ConcurrentHashMap playerSyncState = new ConcurrentHashMap<>(); - - private static void debug(String message, Object... args) { - if (DEBUG_SYNC_LOGGING) { - System.out.println("[SyncController] " + String.format(message, args)); - } - } + /** Stable identity token for the current dedicated-server process, used by the revision handshake. */ + private static final String SERVER_IDENTITY_KEY = UUID.randomUUID().toString(); // ═══════════════════════════════════════════════════════════════════════ // Lifecycle — handler registration and cache initialization @@ -112,92 +112,16 @@ public class SyncController { if (handler != null) registerCache(type, handler::serializeAll); } - - SyncRegistry.validateRegistrations(); - SyncRegistry.freeze(); } + // ═══════════════════════════════════════════════════════════════════════ - // Login sync and revision handshake + // Other Login syncers // ═══════════════════════════════════════════════════════════════════════ - public static void syncPlayer(EntityPlayerMP player) { - SyncController.syncPlayer(player, true); - } - - private static void syncPlayer(EntityPlayerMP player, boolean includePostPackets) { - PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); - - // Registry-driven login iteration: iterate only cached types - for (SyncType type : SyncRegistry.getLoginTypes()) { - SyncCacheEntry entry = cacheEntries.get(type); - if (entry == null) { - continue; - } - - int currentRevision = entry.getRevisionValue(); - int lastRevision = state.getRevision(type); - if (lastRevision == currentRevision) - continue; - - CachedSyncPayload payload = entry.getPayload(type); - if (payload == null) - continue; - - if (lastRevision != payload.getRevision()) { - debug("Sending %s data to %s", type, player.getCommandSenderName()); - PacketHandler.Instance.sendToPlayer(new SyncPacket(type, payload), player); - state.updateRevision(type, payload.getRevision()); - } - } - - if (includePostPackets) { - sendPostLoginPackets(player); - } - } - public static void beginLogin(EntityPlayerMP player) { playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); - PacketHandler.Instance.sendToPlayer( - new LoginPacket(getServerCacheKey(), getServerRevisionSnapshot()), - player - ); - } - - public static void handleClientRevisionReport( - EntityPlayerMP player, - String serverKey, - String previousServerKey, - Map clientRevisions - ) { - String currentServerKey = getServerCacheKey(); - PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); - - if (!currentServerKey.equals(serverKey)) { - state.reset(); - syncPlayer(player); - return; - } - - if (!currentServerKey.isEmpty()) { - if (previousServerKey == null || !currentServerKey.equals(previousServerKey)) { - state.reset(); - syncPlayer(player); - return; - } - } - - if (clientRevisions == null || clientRevisions.isEmpty()) { - debug( - "Client %s reported no cached revisions; forcing full sync", - player.getCommandSenderName() - ); - state.reset(); - } else { - state.applyHandshake(clientRevisions); - } - - syncPlayer(player); + PacketHandler.Instance.sendToPlayer(new LoginPacket(getServerCacheKey(), getServerRevisionSnapshot()), player); } private static void sendPostLoginPackets(EntityPlayerMP player) { @@ -210,45 +134,72 @@ public class SyncController { // Overlays are not part of getSyncNBTFull(), and packets sent during // PlayerLoggedInEvent can arrive before the client entity exists. PlayerData data = PlayerData.get(player); - if (data != null) { - data.skinOverlays.updateClient(); - } - } - - private static Map getServerRevisionSnapshot() { - // Registry-driven: iterate login types from registry - Map snapshot = new LinkedHashMap<>(); - for (SyncType type : SyncRegistry.getLoginTypes()) { - SyncCacheEntry entry = cacheEntries.get(type); - if (entry != null) - snapshot.put(type, entry.getRevisionValue()); - - } - return snapshot; - } - - private static final String SERVER_IDENTITY_KEY = UUID.randomUUID().toString(); - - private static String getServerCacheKey() { - MinecraftServer server = MinecraftServer.getServer(); - if (server == null) { - return ""; - } - - if (!server.isDedicatedServer()) { - return ""; - } - - return SERVER_IDENTITY_KEY; - } - - public static int getCurrentRevision(SyncType type) { - SyncCacheEntry entry = cacheEntries.get(type); - return entry == null ? -1 : entry.getRevisionValue(); + if (data != null) data.skinOverlays.updateClient(); } // ═══════════════════════════════════════════════════════════════════════ - // Generic sync dispatch — registry-driven, no family-specific logic + // MAIN PIPELINE — completion of login handshake and full catalog sync + // ═══════════════════════════════════════════════════════════════════════ + + /** + * Runs the cached-family sync pass for one player using the currently tracked + * revision state. Used after the login handshake and by server-side callers + * that need to refresh a player's global cached sync families. + */ + public static void syncPlayer(EntityPlayerMP player) { + PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); + + // Registry-driven login iteration: iterate only cached types + for (SyncType type : SyncRegistry.getLoginTypes()) { + SyncCacheEntry entry = cacheEntries.get(type); + if (entry == null) continue; + + int currentRevision = entry.getRevisionValue(); + int lastRevision = state.getRevision(type); + if (lastRevision == currentRevision) continue; + + CachedSyncPayload payload = entry.getPayload(type); + if (payload == null) continue; + + if (lastRevision != payload.getRevision()) { + PacketHandler.Instance.sendToPlayer(new SyncPacket(type, payload), player); + state.updateRevision(type, payload.getRevision()); + } + } + + sendPostLoginPackets(player); + } + + /** + * Completes the server-side revision handshake that follows {@link LoginPacket}. + * + * The client reports which cached revisions it believes are still valid for the + * current server identity. If the reported server identity is stale or missing, + * we reset the stored state and force a full cached resync. Otherwise we seed the + * player's tracked revisions from the client report and only send outdated families. + */ + public static void completeLoginRevisionHandshake(EntityPlayerMP player, String serverKey, String previousServerKey, + Map clientRevisions) { + String currentServerKey = getServerCacheKey(); + PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); + + boolean serverIdentityMismatch = !currentServerKey.equals(serverKey); + boolean previousServerMismatch = !currentServerKey.isEmpty() && (previousServerKey == null || !currentServerKey.equals( + previousServerKey)); + boolean canReuseClientRevisions = clientRevisions != null && !clientRevisions.isEmpty(); + + // Any server identity mismatch means the client's cached revision map is unsafe to reuse. + if (serverIdentityMismatch || previousServerMismatch || !canReuseClientRevisions) + state.reset(); + else // The client and server agree on identity, so seed the stored state from the report. + state.applyHandshake(clientRevisions); + + syncPlayer(player); + } + + + // ═══════════════════════════════════════════════════════════════════════ + // Generic dispatcher — called from outside SyncController // ═══════════════════════════════════════════════════════════════════════ /** @@ -256,59 +207,71 @@ public class SyncController { * and sends to all connected players. */ public static void syncAll(SyncType type) { - CachedSyncPayload payload = rebuildNow(type); - if (payload == null) { - return; - } + CachedSyncPayload payload = rebuildCache(type); + if (payload == null) return; + PacketHandler.Instance.sendToAll(new SyncPacket(type, payload)); updateAllPlayerRevisions(type, payload.getRevision()); } // ═══════════════════════════════════════════════════════════════════════ - // UPDATE and REMOVE dispatch — generic, registry-driven + // Server-side dispatchers — registry-driven // ═══════════════════════════════════════════════════════════════════════ public static void syncRemove(SyncType syncType, int id) { Map revisions = invalidateCaches(syncType); - int revision = revisions.getOrDefault(syncType, getCurrentRevision(syncType)); + int revision = getRequestedInvalidationRevision(syncType, revisions); PacketHandler.Instance.sendToAll(new SyncPacket(syncType, EnumSyncAction.REMOVE, id, revision, new NBTTagCompound())); updateAllPlayerRevisions(revisions); } public static void syncUpdate(SyncType type, int cat, NBTTagCompound compound) { Map revisions = invalidateCaches(type); - int revision = revisions.getOrDefault(type, getCurrentRevision(type)); + int revision = getRequestedInvalidationRevision(type, revisions); PacketHandler.Instance.sendToAll(new SyncPacket(type, EnumSyncAction.UPDATE, cat, revision, compound)); updateAllPlayerRevisions(revisions); } // ═══════════════════════════════════════════════════════════════════════ - // Client-side dispatch — registry-driven handler delegation + // Client-side handlers — registry-driven handler delegation // ═══════════════════════════════════════════════════════════════════════ @SideOnly(Side.CLIENT) - public static void clientSync(SyncType syncType, int revision, NBTTagCompound fullCompound) { + public static void clientHandleAll(SyncType syncType, int revision, NBTTagCompound fullCompound) { SyncHandler handler = SyncRegistry.getHandler(syncType); - if (handler != null) + if (handler == null) return; + try { handler.clientHandleReload(fullCompound); - + } catch (Exception e) { + LogWriter.error("[SyncController] Failed to handle RELOAD for sync type " + syncType, e); + } ClientCacheHandler.updateClientRevision(syncType, revision); + } @SideOnly(Side.CLIENT) - public static void clientUpdate(SyncType syncType, int category_id, int revision, NBTTagCompound compound) { + public static void clientHandleUpdate(SyncType syncType, int category_id, int revision, NBTTagCompound compound) { SyncHandler handler = SyncRegistry.getHandler(syncType); - if (handler != null) + if (handler == null) return; + try { handler.clientHandleUpdate(compound, category_id); + } catch (Exception e) { + LogWriter.error("[SyncController] Failed to handle UPDATE for sync type " + syncType, e); + } ClientCacheHandler.updateClientRevision(syncType, revision); } @SideOnly(Side.CLIENT) - public static void clientSyncRemove(SyncType syncType, int id, int revision) { + public static void clientHandleRemove(SyncType syncType, int id, int revision, NBTTagCompound compound) { SyncHandler handler = SyncRegistry.getHandler(syncType); - if (handler != null) + if (handler == null) return; + try { handler.clientHandleRemove(id); + handler.clientHandleRemove(compound); + } catch (Exception e) { + LogWriter.error("[SyncController] Failed to handle REMOVE for sync type " + syncType, e); + } ClientCacheHandler.updateClientRevision(syncType, revision); } @@ -321,10 +284,9 @@ public class SyncController { cacheEntries.put(type, new SyncCacheEntry(supplier)); } - private static CachedSyncPayload rebuildNow(SyncType type) { + private static CachedSyncPayload rebuildCache(SyncType type) { SyncCacheEntry entry = cacheEntries.get(type); - if (entry == null) - return null; + if (entry == null) return null; entry.invalidate(); return entry.getPayload(type); @@ -334,7 +296,7 @@ public class SyncController { * Registry-driven invalidation: delegates to handler-owned * invalidation targets via {@link SyncRegistry#getInvalidationTargets}. */ - public static Map invalidateCaches(SyncType type) { + private static Map invalidateCaches(SyncType type) { Set targets = SyncRegistry.getInvalidationTargets(type); Map revisions = new LinkedHashMap<>(); for (SyncType target : targets) { @@ -347,26 +309,51 @@ public class SyncController { return revisions; } + private static int getRequestedInvalidationRevision(SyncType type, Map revisions) { + Integer revision = revisions.get(type); + return revision != null ? revision : getCurrentServerRevision(type); + } + + public static int getCurrentServerRevision(SyncType type) { + SyncCacheEntry entry = cacheEntries.get(type); + return entry == null ? -1 : entry.getRevisionValue(); + } + + private static Map getServerRevisionSnapshot() { + // Registry-driven: iterate login types from registry + Map snapshot = new LinkedHashMap<>(); + for (SyncType type : SyncRegistry.getLoginTypes()) { + SyncCacheEntry entry = cacheEntries.get(type); + if (entry != null) + snapshot.put(type, entry.getRevisionValue()); + } + return snapshot; + } + private static void updateAllPlayerRevisions(SyncType type, int revision) { - if (revision < 0) { - return; - } - for (PlayerSyncState state : playerSyncState.values()) { + if (revision < 0) return; + + for (PlayerSyncState state : playerSyncState.values()) state.updateRevision(type, revision); - } } private static void updateAllPlayerRevisions(Map revisions) { - if (revisions.isEmpty()) { - return; - } + if (revisions.isEmpty()) return; + for (PlayerSyncState state : playerSyncState.values()) { - for (Map.Entry entry : revisions.entrySet()) { + for (Map.Entry entry : revisions.entrySet()) state.updateRevision(entry.getKey(), entry.getValue()); - } } } + private static String getServerCacheKey() { + MinecraftServer server = MinecraftServer.getServer(); + if (server == null || !server.isDedicatedServer()) return ""; + + return SERVER_IDENTITY_KEY; + } + + // ═══════════════════════════════════════════════════════════════════════ // Inner classes — cache payload and player revision state // ═══════════════════════════════════════════════════════════════════════ @@ -406,15 +393,14 @@ public class SyncController { } private synchronized CachedSyncPayload getPayload(SyncType requestedType) { - if (!dirty && payload != null) { - return payload; - } + if (!dirty && payload != null) return payload; int payloadRevision = revision; - NBTTagCompound data = supplier.get(); ByteBuf buffer = Unpooled.buffer(); byte[] bytes; try { + NBTTagCompound data = supplier.get(); + buffer.writeInt(requestedType.ordinal()); buffer.writeInt(EnumSyncAction.RELOAD.ordinal()); buffer.writeInt(-1); @@ -423,8 +409,9 @@ public class SyncController { bytes = new byte[buffer.readableBytes()]; buffer.readBytes(bytes); - } catch (IOException e) { - throw new RuntimeException("Failed to serialize sync payload for " + requestedType, e); + } catch (Exception e) { + LogWriter.error("Failed to serialize sync payload for " + requestedType, e); + return null; } finally { buffer.release(); } @@ -435,6 +422,23 @@ public class SyncController { return payload; } + 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; + + 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 synchronized int invalidate() { dirty = true; payload = null; @@ -451,23 +455,6 @@ public class SyncController { payload = null; revision = 0; } - - 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; - } - 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 { @@ -486,15 +473,12 @@ public class SyncController { } private synchronized void applyHandshake(Map incoming) { - if (incoming == null || incoming.isEmpty()) { - return; - } + if (incoming == null || incoming.isEmpty()) return; revisions.putAll(incoming); } private synchronized void reset() { revisions.clear(); } - } } diff --git a/src/main/java/kamkeel/npcs/controllers/sync/SyncRegistry.java b/src/main/java/kamkeel/npcs/controllers/sync/SyncRegistry.java index a13568797..1260d0ab1 100644 --- a/src/main/java/kamkeel/npcs/controllers/sync/SyncRegistry.java +++ b/src/main/java/kamkeel/npcs/controllers/sync/SyncRegistry.java @@ -17,26 +17,14 @@ public final class SyncRegistry { private static final LinkedHashMap handlers = new LinkedHashMap<>(); private static SyncType[] loginTypesCache = null; - private static boolean frozen = false; - private SyncRegistry() { - } + private SyncRegistry() {} public static void register(SyncType type, SyncHandler handler) { - if (type == null) { - throw new IllegalArgumentException("SyncType must not be null"); - } - if (handler == null) { - throw new IllegalArgumentException("SyncHandler must not be null for " + type); - } - if (frozen) { - throw new IllegalStateException( - "SyncRegistry is frozen; cannot register handler for " + type); - } - if (handlers.containsKey(type)) { - throw new IllegalStateException( - "SyncHandler already registered for " + type); - } + if (type == null) throw new IllegalArgumentException("SyncType must not be null"); + if (handler == null) throw new IllegalArgumentException("SyncHandler must not be null for " + type); + if (handlers.containsKey(type)) throw new IllegalStateException("SyncHandler already registered for " + type); + handlers.put(type, handler); loginTypesCache = null; } @@ -48,11 +36,10 @@ public final class SyncRegistry { public static SyncType[] getLoginTypes() { if (loginTypesCache == null) { List list = new ArrayList<>(); - for (Map.Entry e : handlers.entrySet()) { - if (e.getValue().isCachedType()) { + for (Map.Entry e : handlers.entrySet()) + if (e.getValue().isCachedType()) list.add(e.getKey()); - } - } + loginTypesCache = list.toArray(new SyncType[0]); } return loginTypesCache; @@ -69,24 +56,8 @@ public final class SyncRegistry { public static void clear() { handlers.clear(); loginTypesCache = null; - frozen = false; - } - - public static void freeze() { - frozen = true; - } - - public static boolean isFrozen() { - return frozen; - } - - public static void validateRegistrations() { - for (SyncType type : SyncType.values()) { - if (!handlers.containsKey(type)) { - LogWriter.info("[SyncRegistry] No SyncHandler registered for " + type); - } - } } + public static boolean isRegistered(SyncType type) { return handlers.containsKey(type); diff --git a/src/main/java/kamkeel/npcs/network/enums/SyncType.java b/src/main/java/kamkeel/npcs/network/enums/SyncType.java index 2623038ac..82d5639ae 100644 --- a/src/main/java/kamkeel/npcs/network/enums/SyncType.java +++ b/src/main/java/kamkeel/npcs/network/enums/SyncType.java @@ -26,7 +26,6 @@ public final class SyncType { // ──────────────────── Registry ──────────────────── private static final Map REGISTRY = new LinkedHashMap<>(); private static int nextOrdinal = 0; - private static boolean frozen = false; // ──────────────────── CNPC+ families ──────────────────── public static final SyncType FACTION = register("cnpc:faction"); @@ -69,7 +68,7 @@ public final class SyncType { public int ordinal() { return ordinal; } - + // ──────────────────── Registration ──────────────────── /** @@ -82,18 +81,10 @@ public final class SyncType { * @throws IllegalStateException if the registry has been frozen */ public static SyncType register(String id) { - if (id == null || id.isEmpty()) { - throw new IllegalArgumentException("SyncType id must not be null or empty"); - } - if (!id.contains(":")) { - throw new IllegalArgumentException("SyncType id must be namespaced (modid:path): " + id); - } - if (frozen) { - throw new IllegalStateException("SyncType registry is frozen; cannot register: " + id); - } - if (REGISTRY.containsKey(id)) { - throw new IllegalStateException("Duplicate SyncType registration: " + id); - } + if (id == null || id.isEmpty()) throw new IllegalArgumentException("SyncType id must not be null or empty"); + if (!id.contains(":")) throw new IllegalArgumentException("SyncType id must be namespaced (modid:path): " + id); + if (REGISTRY.containsKey(id)) throw new IllegalStateException("Duplicate SyncType registration: " + id); + SyncType type = new SyncType(id, nextOrdinal++); REGISTRY.put(id, type); return type; @@ -115,14 +106,11 @@ public final class SyncType { * @return the registered type, or {@code null} if ordinal is out of range */ public static SyncType byOrdinal(int ordinal) { - if (ordinal < 0) { - return null; - } - for (SyncType type : REGISTRY.values()) { - if (type.ordinal == ordinal) { - return type; - } - } + if (ordinal < 0) return null; + + for (SyncType type : REGISTRY.values()) + if (type.ordinal == ordinal) return type; + return null; } @@ -133,30 +121,6 @@ public final class SyncType { return Collections.unmodifiableCollection(REGISTRY.values()); } - /** - * Freeze the registry. After freezing, no new types can be registered. - * Should be called after all mods have had a chance to register types. - */ - public static void freeze() { - frozen = true; - } - - /** - * Whether the registry has been frozen. - */ - public static boolean isFrozen() { - return frozen; - } - - /** - * Unfreeze the registry. Called during world reload to allow addon - * re-registration. Does NOT clear existing registrations — built-in - * constants stay stable. - */ - public static void unfreeze() { - frozen = false; - } - // ──────────────────── Object overrides ──────────────────── @Override 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 322f6c976..3ebe78712 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 @@ -152,13 +152,13 @@ public final class SyncPacket extends LargeAbstractPacket { ) { switch (enumSyncAction) { case RELOAD: - SyncController.clientSync(syncType, incomingRevision, data); + SyncController.clientHandleAll(syncType, incomingRevision, data); break; case UPDATE: - SyncController.clientUpdate(syncType, id, incomingRevision, data); + SyncController.clientHandleUpdate(syncType, id, incomingRevision, data); break; case REMOVE: - SyncController.clientSyncRemove(syncType, id, incomingRevision); + SyncController.clientHandleRemove(syncType, id, incomingRevision, data); 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 index 25824e15a..b6becdaab 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/SyncRevisionInfoPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/SyncRevisionInfoPacket.java @@ -80,6 +80,6 @@ public class SyncRevisionInfoPacket extends AbstractPacket { } } - SyncController.handleClientRevisionReport((EntityPlayerMP) player, incomingServerKey, incomingPreviousKey, incomingRevisions); + SyncController.completeLoginRevisionHandshake((EntityPlayerMP) player, incomingServerKey, incomingPreviousKey, incomingRevisions); } } diff --git a/src/main/java/noppes/npcs/ServerEventsHandler.java b/src/main/java/noppes/npcs/ServerEventsHandler.java index 1ad41c657..9664933be 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -260,7 +260,7 @@ public class ServerEventsHandler { SyncType.WORKBENCH_RECIPES, EnumSyncAction.RELOAD, -1, - SyncController.getCurrentRevision(SyncType.WORKBENCH_RECIPES), + SyncController.getCurrentServerRevision(SyncType.WORKBENCH_RECIPES), WorkbenchRecipeSyncHandler.getInstance().serializeAll() ), (EntityPlayerMP) player); } @@ -269,7 +269,7 @@ public class ServerEventsHandler { SyncType.CARPENTRY_RECIPES, EnumSyncAction.RELOAD, -1, - SyncController.getCurrentRevision(SyncType.CARPENTRY_RECIPES), + SyncController.getCurrentServerRevision(SyncType.CARPENTRY_RECIPES), CarpentryRecipeSyncHandler.getInstance().serializeAll() ), (EntityPlayerMP) player); } From 22de352dd908ba039df3ded7a8ad1f45258fc19e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 31 Mar 2026 22:57:09 +0200 Subject: [PATCH 04/34] FAAAAAH --- .../npcs/controllers/SyncControllerOld.java | 2116 ++++++++--------- 1 file changed, 1058 insertions(+), 1058 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/SyncControllerOld.java b/src/main/java/kamkeel/npcs/controllers/SyncControllerOld.java index 987059c80..05f37e8e9 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncControllerOld.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncControllerOld.java @@ -1,1058 +1,1058 @@ -package kamkeel.npcs.controllers; - -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import kamkeel.npcs.addon.DBCAddon; -import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; -import kamkeel.npcs.network.LargeAbstractPacket; -import kamkeel.npcs.network.PacketHandler; -import kamkeel.npcs.network.enums.EnumSyncAction; -import kamkeel.npcs.network.enums.SyncType; -import kamkeel.npcs.network.packets.data.LoginPacket; -import kamkeel.npcs.network.packets.data.ProfileSharedQuestPacket; -import kamkeel.npcs.network.packets.data.ability.AbilityCooldownSyncPacket; -import kamkeel.npcs.network.packets.data.ability.AbilityHotbarSyncPacket; -import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; -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.*; -import noppes.npcs.controllers.data.*; - -import java.io.IOException; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Supplier; - -public class SyncControllerOld { - - public static boolean DEBUG_SYNC_LOGGING = false; - - private static final Map cacheEntries = new LinkedHashMap<>(); - private static final ConcurrentHashMap playerSyncState = new ConcurrentHashMap<>(); - - - private static void debug(String message, Object... args) { - if (DEBUG_SYNC_LOGGING) { - System.out.println("[SyncController] " + String.format(message, args)); - } - } - - private static final SyncType[] LOGIN_SYNC_TYPES = new SyncType[]{ - SyncType.FACTION, - SyncType.DIALOG_CATEGORY, - SyncType.QUEST_CATEGORY, - SyncType.WORKBENCH_RECIPES, - SyncType.CARPENTRY_RECIPES, - SyncType.ANVIL_RECIPES, - SyncType.CUSTOM_EFFECTS, - SyncType.MAGIC, - SyncType.MAGIC_CYCLE, - SyncType.CUSTOM_ABILITY, - SyncType.CHAINED_ABILITY - }; - - public static void load() { - cacheEntries.clear(); - playerSyncState.clear(); - - registerCache(SyncType.FACTION, SyncControllerOld::factionsNBT); - registerCache(SyncType.DIALOG_CATEGORY, SyncControllerOld::dialogCategoriesNBT); - registerCache(SyncType.QUEST_CATEGORY, SyncControllerOld::questCategoriesNBT); - registerCache(SyncType.WORKBENCH_RECIPES, SyncControllerOld::workbenchNBT); - registerCache(SyncType.CARPENTRY_RECIPES, SyncControllerOld::carpentryNBT); - registerCache(SyncType.ANVIL_RECIPES, SyncControllerOld::anvilNBT); - registerCache(SyncType.CUSTOM_EFFECTS, SyncControllerOld::customEffectsNBT); - registerCache(SyncType.MAGIC, SyncControllerOld::magicsNBT); - registerCache(SyncType.MAGIC_CYCLE, SyncControllerOld::magicCyclesNBT); - registerCache(SyncType.CUSTOM_ABILITY, SyncControllerOld::customAbilitiesNBT); - registerCache(SyncType.CHAINED_ABILITY, SyncControllerOld::chainedAbilitiesNBT); - } - - public static void syncPlayer(EntityPlayerMP player) { - SyncControllerOld.syncPlayerInternal(player, true); - } - - private static void syncPlayerInternal(EntityPlayerMP player, boolean includePostPackets) { - PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); - - for (SyncType type : LOGIN_SYNC_TYPES) { - SyncCacheEntry entry = cacheEntries.get(type); - if (entry == null) { - continue; - } - - int currentRevision = entry.getRevisionValue(); - int lastRevision = state.getRevision(type); - if (lastRevision == currentRevision) { - continue; - } - - CachedSyncPayload payload = entry.getPayload(type); - if (payload == null) { - continue; - } - - if (lastRevision != payload.getRevision()) { - debug("Sending %s data to %s", type, player.getCommandSenderName()); - PacketHandler.Instance.sendToPlayer(new SyncPacket(type, payload), player); - state.updateRevision(type, payload.getRevision()); - } - } - - if (includePostPackets) { - sendPostLoginPackets(player); - } - } - - public static void beginLogin(EntityPlayerMP player) { - playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); - PacketHandler.Instance.sendToPlayer( - new LoginPacket(getServerCacheKey(), getServerRevisionSnapshot()), - player - ); - } - - public static void handleClientRevisionReport( - EntityPlayerMP player, - String serverKey, - String previousServerKey, - Map clientRevisions - ) { - String currentServerKey = getServerCacheKey(); - PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); - - if (!currentServerKey.equals(serverKey)) { - state.reset(); - syncPlayer(player); - return; - } - - if (!currentServerKey.isEmpty()) { - if (previousServerKey == null || !currentServerKey.equals(previousServerKey)) { - state.reset(); - syncPlayer(player); - return; - } - } - - if (clientRevisions == null || clientRevisions.isEmpty()) { - debug( - "Client %s reported no cached revisions; forcing full sync", - player.getCommandSenderName() - ); - state.reset(); - } else { - state.applyHandshake(clientRevisions); - } - - syncPlayer(player); - } - - private static void sendPostLoginPackets(EntityPlayerMP player) { - DBCAddon.instance.syncPlayer(player); - syncPlayerData(player, false); - PartyInfoPacket.sendPartyData(player); - ProfileSharedQuestPacket.sendToPlayer(player); - - // Sync skin overlays after full handshake to ensure the client is ready. - // Overlays are not part of getSyncNBTFull(), and packets sent during - // PlayerLoggedInEvent can arrive before the client entity exists. - PlayerData data = PlayerData.get(player); - if (data != null) { - data.skinOverlays.updateClient(); - } - } - - private static Map getServerRevisionSnapshot() { - Map snapshot = new LinkedHashMap<>(); - for (SyncType type : LOGIN_SYNC_TYPES) { - SyncCacheEntry entry = cacheEntries.get(type); - if (entry != null) { - snapshot.put(type, entry.getRevisionValue()); - } - } - return snapshot; - } - - private static final String SERVER_IDENTITY_KEY = UUID.randomUUID().toString(); - - private static String getServerCacheKey() { - MinecraftServer server = MinecraftServer.getServer(); - if (server == null) { - return ""; - } - - if (!server.isDedicatedServer()) { - return ""; - } - - return SERVER_IDENTITY_KEY; - } - - public static int getCurrentRevision(SyncType type) { - SyncCacheEntry entry = cacheEntries.get(type); - return entry == null ? -1 : entry.getRevisionValue(); - } - - public static NBTTagCompound workbenchNBT() { - RecipeController controller = RecipeController.Instance; - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (RecipeCarpentry recipe : controller.globalRecipes.values()) { - list.appendTag(recipe.writeNBT(false)); - } - compound.setTag("recipes", list); - return compound; - } - - public static NBTTagCompound carpentryNBT() { - RecipeController controller = RecipeController.Instance; - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (RecipeCarpentry recipe : controller.carpentryRecipes.values()) { - list.appendTag(recipe.writeNBT(false)); - } - compound.setTag("recipes", list); - return compound; - } - - public static NBTTagCompound anvilNBT() { - RecipeController controller = RecipeController.Instance; - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (RecipeAnvil recipe : controller.anvilRecipes.values()) { - list.appendTag(recipe.writeNBT(false)); - } - compound.setTag("recipes", list); - return compound; - } - - public static NBTTagCompound factionsNBT() { - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (Faction faction : FactionController.getInstance().factions.values()) { - NBTTagCompound factioNBT = new NBTTagCompound(); - faction.writeNBT(factioNBT); - list.appendTag(factioNBT); - } - compound.setTag("Factions", list); - return compound; - } - - public static NBTTagCompound dialogCategoriesNBT() { - NBTTagCompound compound = new NBTTagCompound(); - NBTTagList categoryList = new NBTTagList(); - for (DialogCategory category : DialogController.Instance.categories.values()) { - NBTTagCompound questCompound = new NBTTagCompound(); - NBTTagList dialogList = new NBTTagList(); - for (int dialogID : category.dialogs.keySet()) { - Dialog quest = category.dialogs.get(dialogID); - dialogList.appendTag(quest.writeToNBT(new NBTTagCompound())); - } - questCompound.setTag("Data", dialogList); - questCompound.setTag("CatNBT", category.writeSmallNBT(new NBTTagCompound())); - categoryList.appendTag(questCompound); - } - compound.setTag("DialogCategories", categoryList); - return compound; - } - - public static NBTTagCompound questCategoriesNBT() { - NBTTagCompound compound = new NBTTagCompound(); - NBTTagList categoryList = new NBTTagList(); - for (QuestCategory category : QuestController.Instance.categories.values()) { - NBTTagCompound questCompound = new NBTTagCompound(); - ; - NBTTagList questList = new NBTTagList(); - for (int questID : category.quests.keySet()) { - Quest quest = category.quests.get(questID); - questList.appendTag(quest.writeToNBT(new NBTTagCompound())); - } - questCompound.setTag("Data", questList); - questCompound.setTag("CatNBT", category.writeSmallNBT(new NBTTagCompound())); - categoryList.appendTag(questCompound); - } - compound.setTag("QuestCategories", categoryList); - return compound; - } - - public static NBTTagCompound customEffectsNBT() { - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (CustomEffect effect : CustomEffectController.getInstance().getCustomEffects().values()) { - list.appendTag(effect.writeToNBT(false)); - } - compound.setTag("Data", list); - return compound; - } - - public static NBTTagCompound magicsNBT() { - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (Magic magic : MagicController.getInstance().magics.values()) { - NBTTagCompound magicCompound = new NBTTagCompound(); - magic.writeNBT(magicCompound); - list.appendTag(magicCompound); - } - compound.setTag("Data", list); - return compound; - } - - public static NBTTagCompound magicCyclesNBT() { - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (MagicCycle cycle : MagicController.getInstance().cycles.values()) { - NBTTagCompound cycleCompound = new NBTTagCompound(); - cycle.writeNBT(cycleCompound); - list.appendTag(cycleCompound); - } - compound.setTag("Data", list); - return compound; - } - - public static NBTTagCompound customAbilitiesNBT() { - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (Map.Entry entry : AbilityController.Instance.getCustomAbilities().entrySet()) { - NBTTagCompound abilityNBT = entry.getValue().writeNBT(false); - abilityNBT.setString("CustomAbilityId", entry.getKey()); - list.appendTag(abilityNBT); - } - compound.setTag("Data", list); - return compound; - } - - public static void syncAllCustomAbilities() { - CachedSyncPayload payload = rebuildNow(SyncType.CUSTOM_ABILITY); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CUSTOM_ABILITY, payload)); - updateAllPlayerRevisions(SyncType.CUSTOM_ABILITY, payload.getRevision()); - } - - public static NBTTagCompound chainedAbilitiesNBT() { - NBTTagList list = new NBTTagList(); - NBTTagCompound compound = new NBTTagCompound(); - for (Map.Entry entry : AbilityController.Instance.getChainedAbilities().entrySet()) { - NBTTagCompound chainNBT = entry.getValue().writeNBT(false); - chainNBT.setString("ChainedAbilityId", entry.getKey()); - list.appendTag(chainNBT); - } - compound.setTag("Data", list); - return compound; - } - - public static void syncAllChainedAbilities() { - CachedSyncPayload payload = rebuildNow(SyncType.CHAINED_ABILITY); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CHAINED_ABILITY, payload)); - updateAllPlayerRevisions(SyncType.CHAINED_ABILITY, payload.getRevision()); - } - - public static void syncPlayerData(EntityPlayerMP player, boolean update) { - PlayerData data = PlayerData.get(player); - if (data != null) { - if (update) { - PacketHandler.Instance.sendToPlayer(new SyncPacket( - SyncType.PLAYERDATA, - EnumSyncAction.UPDATE, - -1, - data.getSyncNBT()) - , player); - } else { - PacketHandler.Instance.sendToPlayer(new SyncPacket( - SyncType.PLAYERDATA, - EnumSyncAction.RELOAD, - -1, - data.getSyncNBTFull()), player); - } - } - } - - public static void syncRemove(SyncType syncType, int id) { - Map revisions = invalidateCaches(syncType); - int revision = revisions.getOrDefault(syncType, getCurrentRevision(syncType)); - PacketHandler.Instance.sendToAll(new SyncPacket( - syncType, - EnumSyncAction.REMOVE, - id, - revision, - new NBTTagCompound() - )); - updateAllPlayerRevisions(revisions); - } - - public static void syncAllDialogs() { - CachedSyncPayload payload = rebuildNow(SyncType.DIALOG_CATEGORY); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.DIALOG_CATEGORY, payload)); - updateAllPlayerRevisions(SyncType.DIALOG_CATEGORY, payload.getRevision()); - } - - public static void syncAllQuests() { - CachedSyncPayload payload = rebuildNow(SyncType.QUEST_CATEGORY); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.QUEST_CATEGORY, payload)); - updateAllPlayerRevisions(SyncType.QUEST_CATEGORY, payload.getRevision()); - } - - public static void syncAllWorkbenchRecipes() { - CachedSyncPayload payload = rebuildNow(SyncType.WORKBENCH_RECIPES); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.WORKBENCH_RECIPES, payload)); - updateAllPlayerRevisions(SyncType.WORKBENCH_RECIPES, payload.getRevision()); - } - - public static void syncAllCarpentryRecipes() { - CachedSyncPayload payload = rebuildNow(SyncType.CARPENTRY_RECIPES); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CARPENTRY_RECIPES, payload)); - updateAllPlayerRevisions(SyncType.CARPENTRY_RECIPES, payload.getRevision()); - } - - public static void syncAllAnvilRecipes() { - CachedSyncPayload payload = rebuildNow(SyncType.ANVIL_RECIPES); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.ANVIL_RECIPES, payload)); - updateAllPlayerRevisions(SyncType.ANVIL_RECIPES, payload.getRevision()); - } - - public static void syncAllCustomEffects() { - CachedSyncPayload payload = rebuildNow(SyncType.CUSTOM_EFFECTS); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CUSTOM_EFFECTS, payload)); - updateAllPlayerRevisions(SyncType.CUSTOM_EFFECTS, payload.getRevision()); - } - - @SideOnly(Side.CLIENT) - public static void clientSync(SyncType syncType, int revision, NBTTagCompound fullCompound) { - if (syncType == SyncType.FACTION) { - NBTTagList list = fullCompound.getTagList("Factions", 10); - for (int i = 0; i < list.tagCount(); i++) { - Faction faction = new Faction(); - faction.readNBT(list.getCompoundTagAt(i)); - FactionController.getInstance().factionsSync.put(faction.id, faction); - } - FactionController.getInstance().factions = FactionController.getInstance().factionsSync; - FactionController.getInstance().factionsSync = new HashMap(); - } else if (syncType == SyncType.DIALOG_CATEGORY) { - if (!fullCompound.hasNoTags()) { - NBTTagList categories = fullCompound.getTagList("DialogCategories", 10); - for (int j = 0; j < fullCompound.getTagList("DialogCategories", 10).tagCount(); j++) { - NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); - if (categoryCompound.hasNoTags()) - continue; - - DialogCategory category = new DialogCategory(); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - NBTTagList dialogList = categoryCompound.getTagList("Data", 10); - if (DialogController.Instance.categoriesSync.containsKey(category.id)) { - category = DialogController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < dialogList.tagCount(); i++) { - Dialog dialog = new Dialog(); - dialog.readNBT(dialogList.getCompoundTagAt(i)); - dialog.category = category; - category.dialogs.put(dialog.id, dialog); - } - DialogController.Instance.categoriesSync.put(category.id, category); - } - } - - HashMap dialogs = new HashMap(); - for (DialogCategory category : DialogController.Instance.categoriesSync.values()) { - for (Dialog dialog : category.dialogs.values()) { - dialogs.put(dialog.id, dialog); - } - } - - DialogController.Instance.categories = DialogController.Instance.categoriesSync; - DialogController.Instance.dialogs = dialogs; - DialogController.Instance.categoriesSync = new HashMap(); - } else if (syncType == SyncType.QUEST_CATEGORY) { - if (!fullCompound.hasNoTags()) { - NBTTagList categories = fullCompound.getTagList("QuestCategories", 10); - for (int j = 0; j < fullCompound.getTagList("QuestCategories", 10).tagCount(); j++) { - NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); - if (categoryCompound.hasNoTags()) - continue; - - QuestCategory category = new QuestCategory(); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - NBTTagList questList = categoryCompound.getTagList("Data", 10); - if (QuestController.Instance.categoriesSync.containsKey(category.id)) { - category = QuestController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < questList.tagCount(); i++) { - Quest quest = new Quest(); - quest.readNBT(questList.getCompoundTagAt(i)); - quest.category = category; - category.quests.put(quest.id, quest); - } - QuestController.Instance.categoriesSync.put(category.id, category); - } - } - - HashMap quests = new HashMap(); - for (QuestCategory category : QuestController.Instance.categoriesSync.values()) { - for (Quest quest : category.quests.values()) { - quests.put(quest.id, quest); - } - } - - QuestController.Instance.categories = QuestController.Instance.categoriesSync; - QuestController.Instance.quests = quests; - QuestController.Instance.categoriesSync = new HashMap(); - } else if (syncType == SyncType.PLAYERDATA) { - ClientCacheHandler.playerData.setSyncNBTFull(fullCompound); - } else if (syncType == SyncType.MAGIC) { - NBTTagList list = fullCompound.getTagList("Data", 10); - MagicController mc = MagicController.getInstance(); - mc.magicSync.clear(); - for (int i = 0; i < list.tagCount(); i++) { - Magic magic = new Magic(); - magic.readNBT(list.getCompoundTagAt(i)); - mc.magicSync.put(magic.id, magic); - } - - mc.magics.clear(); - mc.magics.putAll(mc.magicSync); - mc.magicSync.clear(); - } else if (syncType == SyncType.MAGIC_CYCLE) { - NBTTagList list = fullCompound.getTagList("Data", 10); - MagicController mc = MagicController.getInstance(); - mc.cyclesSync.clear(); - for (int i = 0; i < list.tagCount(); i++) { - MagicCycle cycle = new MagicCycle(); - cycle.readNBT(list.getCompoundTagAt(i)); - mc.cyclesSync.put(cycle.id, cycle); - } - - mc.cycles.clear(); - mc.cycles.putAll(mc.cyclesSync); - mc.cyclesSync.clear(); - } else if (syncType == SyncType.WORKBENCH_RECIPES) { - NBTTagList list = fullCompound.getTagList("recipes", 10); - if (list == null) - return; - - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound recipeCompound = list.getCompoundTagAt(i); - RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); - recipe.readNBT(recipeCompound); - RecipeController.syncRecipes.put(recipe.id, recipe); - } - - RecipeController.reloadGlobalRecipes(RecipeController.syncRecipes); - RecipeController.syncRecipes = new HashMap(); - } else if (syncType == SyncType.CARPENTRY_RECIPES) { - NBTTagList list = fullCompound.getTagList("recipes", 10); - if (list == null) - return; - - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound recipeCompound = list.getCompoundTagAt(i); - RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); - recipe.readNBT(recipeCompound); - RecipeController.syncRecipes.put(recipe.id, recipe); - } - - RecipeController.Instance.carpentryRecipes = RecipeController.syncRecipes; - RecipeController.syncRecipes = new HashMap(); - } else if (syncType == SyncType.ANVIL_RECIPES) { - NBTTagList list = fullCompound.getTagList("recipes", 10); - if (list == null) - return; - - for (int i = 0; i < list.tagCount(); i++) { - RecipeAnvil recipe = new RecipeAnvil(); - recipe.readNBT(list.getCompoundTagAt(i)); - RecipeController.syncAnvilRecipes.put(recipe.id, recipe); - } - - RecipeController.Instance.anvilRecipes = RecipeController.syncAnvilRecipes; - RecipeController.syncAnvilRecipes = new HashMap(); - } else if (syncType == SyncType.CUSTOM_EFFECTS) { - NBTTagList list = fullCompound.getTagList("Data", 10); - CustomEffectController ce = CustomEffectController.getInstance(); - ce.customEffectsSync.clear(); - for (int i = 0; i < list.tagCount(); i++) { - CustomEffect effect = new CustomEffect(); - effect.readFromNBT(list.getCompoundTagAt(i)); - ClientCacheHandler.getImageData(effect.icon); - ce.customEffectsSync.put(effect.id, effect); - } - - ce.getCustomEffects().clear(); - ce.getCustomEffects().putAll(ce.customEffectsSync); - ce.customEffectsSync.clear(); - } else if (syncType == SyncType.CUSTOM_ABILITY) { - NBTTagList list = fullCompound.getTagList("Data", 10); - LinkedHashMap sync = new LinkedHashMap<>(); - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound nbt = list.getCompoundTagAt(i); - String name = nbt.getString("CustomAbilityId"); - Ability ability = AbilityController.Instance.fromNBT(nbt); - if (ability != null) { - ability.setName(name); - sync.put(name, ability); - } - } - AbilityController.Instance.setCustomAbilities(sync); - } else if (syncType == SyncType.CHAINED_ABILITY) { - NBTTagList list = fullCompound.getTagList("Data", 10); - LinkedHashMap sync = new LinkedHashMap<>(); - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound nbt = list.getCompoundTagAt(i); - String id = nbt.getString("ChainedAbilityId"); - ChainedAbility chain = new ChainedAbility(); - chain.readNBT(nbt); - chain.setName(id); - sync.put(id, chain); - } - AbilityController.Instance.setChainedAbilities(sync); - } - - ClientCacheHandler.updateClientRevision(syncType, revision); - } - - @SideOnly(Side.CLIENT) - public static void clientUpdate(SyncType syncType, int category_id, int revision, NBTTagCompound compound) { - if (syncType == SyncType.FACTION) { - Faction faction = new Faction(); - faction.readNBT(compound); - FactionController.getInstance().factions.put(faction.id, faction); - } else if (syncType == SyncType.DIALOG) { - DialogCategory category = DialogController.Instance.categories.get(category_id); - Dialog dialog = new Dialog(); - dialog.category = category; - dialog.readNBT(compound); - DialogController.Instance.dialogs.put(dialog.id, dialog); - category.dialogs.put(dialog.id, dialog); - } else if (syncType == SyncType.QUEST) { - QuestCategory category = QuestController.Instance.categories.get(category_id); - Quest quest = new Quest(); - quest.category = category; - quest.readNBT(compound); - QuestController.Instance.quests.put(quest.id, quest); - category.quests.put(quest.id, quest); - } else if (syncType == SyncType.QUEST_CATEGORY) { - QuestCategory category = new QuestCategory(); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - NBTTagList list = compound.getTagList("Data", 10); - if (QuestController.Instance.categoriesSync.containsKey(category.id)) { - category = QuestController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < list.tagCount(); i++) { - Quest quest = new Quest(); - quest.readNBT(list.getCompoundTagAt(i)); - quest.category = category; - category.quests.put(quest.id, quest); - } - QuestController.Instance.categories.put(category.id, category); - } else if (syncType == SyncType.DIALOG_CATEGORY) { - DialogCategory category = new DialogCategory(); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - NBTTagList list = compound.getTagList("Data", 10); - if (DialogController.Instance.categoriesSync.containsKey(category.id)) { - category = DialogController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < list.tagCount(); i++) { - Dialog dialog = new Dialog(); - dialog.readNBT(list.getCompoundTagAt(i)); - dialog.category = category; - category.dialogs.put(dialog.id, dialog); - } - DialogController.Instance.categories.put(category.id, category); - } else if (syncType == SyncType.MAGIC_CYCLE) { - MagicCycle cycle = new MagicCycle(); - cycle.readNBT(compound); - MagicController.getInstance().cycles.put(cycle.id, cycle); - } else if (syncType == SyncType.MAGIC) { - Magic magic = new Magic(); - magic.readNBT(compound); - MagicController.getInstance().magics.put(magic.id, magic); - } else if (syncType == SyncType.PLAYERDATA) { - ClientCacheHandler.playerData.setSyncNBT(compound); - } else if (syncType == SyncType.CUSTOM_EFFECTS) { - CustomEffect effect = new CustomEffect(); - effect.readFromNBT(compound); - ClientCacheHandler.getImageData(effect.icon); - - CustomEffectController.Instance.getCustomEffects().put(effect.id, effect); - } - - ClientCacheHandler.updateClientRevision(syncType, revision); - } - - public static void syncUpdate(SyncType 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); - } - - - public static NBTTagCompound updateQuestCat(QuestCategory questCategory) { - NBTTagCompound questCompound = new NBTTagCompound(); - NBTTagList questList = new NBTTagList(); - for (int questID : questCategory.quests.keySet()) { - Quest quest = questCategory.quests.get(questID); - questList.appendTag(quest.writeToNBT(new NBTTagCompound())); - } - questCompound.setTag("Data", questList); - questCompound.setTag("CatNBT", questCategory.writeSmallNBT(new NBTTagCompound())); - return questCompound; - } - - public static NBTTagCompound updateDialogCat(DialogCategory dialogCategory) { - NBTTagCompound dialogCompound = new NBTTagCompound(); - NBTTagList dialogList = new NBTTagList(); - for (int questID : dialogCategory.dialogs.keySet()) { - Dialog dialog = dialogCategory.dialogs.get(questID); - dialogList.appendTag(dialog.writeToNBT(new NBTTagCompound())); - } - dialogCompound.setTag("Data", dialogList); - dialogCompound.setTag("CatNBT", dialogCategory.writeSmallNBT(new NBTTagCompound())); - return dialogCompound; - } - - @SideOnly(Side.CLIENT) - public static void clientSyncRemove(SyncType syncType, int id, int revision) { - if (syncType == SyncType.FACTION) { - FactionController.getInstance().factions.remove(id); - } else if (syncType == SyncType.DIALOG) { - Dialog dialog = DialogController.Instance.dialogs.remove(id); - if (dialog != null) { - dialog.category.dialogs.remove(id); - } - } else if (syncType == SyncType.DIALOG_CATEGORY) { - DialogCategory dialogCategory = DialogController.Instance.categories.remove(id); - if (dialogCategory != null) { - DialogController.Instance.dialogs.keySet().removeAll(dialogCategory.dialogs.keySet()); - } - } else if (syncType == SyncType.QUEST) { - Quest quest = QuestController.Instance.quests.remove(id); - if (quest != null) { - quest.category.quests.remove(id); - } - } else if (syncType == SyncType.QUEST_CATEGORY) { - QuestCategory questCategory = QuestController.Instance.categories.remove(id); - if (questCategory != null) { - QuestController.Instance.quests.keySet().removeAll(questCategory.quests.keySet()); - } - } else if (syncType == SyncType.MAGIC) { - for (MagicCycle cycle : MagicController.getInstance().cycles.values()) { - cycle.associations.remove(id); - } - MagicController.getInstance().cycles.remove(id); - } else if (syncType == SyncType.MAGIC_CYCLE) { - MagicController.getInstance().cycles.remove(id); - } else if (syncType == SyncType.CUSTOM_EFFECTS) { - CustomEffectController.Instance.getCustomEffects().remove(id); - } - - ClientCacheHandler.updateClientRevision(syncType, revision); - } - - public static void syncEffects(EntityPlayerMP playerMP) { - ConcurrentHashMap playerEffects = CustomEffectController.getInstance().getPlayerEffects(playerMP); - PlayerData playerData = PlayerData.get(playerMP); - playerData.effectData.setEffects(playerEffects); - - NBTTagCompound compound = playerData.getPlayerEffects(); - PacketHandler.Instance.sendToPlayer(new SyncEffectPacket(compound), playerMP); - } - - /** - * Full sync of all player ability state to the client: unlocked abilities, - * selection, toggles, hotbar layout, and cooldowns. - * Used for entity reconstruction and other full-refresh scenarios. - */ - public static void syncAbilities(EntityPlayerMP playerMP) { - PlayerData playerData = PlayerData.get(playerMP); - if (playerData == null) return; - - PlayerAbilitySyncPacket.sendToPlayer(playerMP); - AbilityHotbarSyncPacket.sendToPlayer(playerMP); - AbilityCooldownSyncPacket.sendToPlayer(playerMP); - } - - /** - * Lightweight sync of cooldown state only (global + per-ability). - * Called periodically (every 10 ticks) to keep the client's cooldown - * display accurate. Cooldowns are transient and not part of NBT sync. - */ - public static void syncAbilityCooldowns(EntityPlayerMP playerMP) { - PlayerData playerData = PlayerData.get(playerMP); - if (playerData == null) return; - - AbilityCooldownSyncPacket.sendToPlayer(playerMP); - } - - @SideOnly(Side.CLIENT) - public static void clientSyncEffects(NBTTagCompound compound) { - PlayerData playerData = PlayerData.get(Minecraft.getMinecraft().thePlayer); - if (playerData != null) { - playerData.setPlayerEffects(compound); - } - } - - private static void registerCache(SyncType type, Supplier supplier) { - cacheEntries.put(type, new SyncCacheEntry(supplier)); - } - - private static CachedSyncPayload rebuildNow(SyncType type) { - SyncCacheEntry entry = cacheEntries.get(type); - if (entry == null) { - return null; - } - entry.invalidate(); - return entry.getPayload(type); - } - - public static Map invalidateCaches(SyncType type) { - Set targets = getInvalidationTargets(type); - Map revisions = new LinkedHashMap<>(); - for (SyncType target : targets) { - SyncCacheEntry entry = cacheEntries.get(target); - if (entry != null) { - int newRevision = entry.invalidate(); - revisions.put(target, newRevision); - } - } - return revisions; - } - - private static Set getInvalidationTargets(SyncType type) { - if (type == SyncType.DIALOG || type == SyncType.DIALOG_CATEGORY) { - Set set = new HashSet<>(); - set.add(SyncType.DIALOG_CATEGORY); - return set; - } else if (type == SyncType.QUEST || type == SyncType.QUEST_CATEGORY) { - Set set = new HashSet<>(); - set.add(SyncType.QUEST_CATEGORY); - return set; - } else if (type == SyncType.FACTION) { - Set set = new HashSet<>(); - set.add(SyncType.FACTION); - return set; - } else if (type == SyncType.MAGIC) { - Set set = new HashSet<>(); - set.add(SyncType.MAGIC); - return set; - } else if (type == SyncType.MAGIC_CYCLE) { - Set set = new HashSet<>(); - set.add(SyncType.MAGIC_CYCLE); - return set; - } else if (type == SyncType.WORKBENCH_RECIPES) { - Set set = new HashSet<>(); - set.add(SyncType.WORKBENCH_RECIPES); - return set; - } else if (type == SyncType.CARPENTRY_RECIPES) { - Set set = new HashSet<>(); - set.add(SyncType.CARPENTRY_RECIPES); - return set; - } else if (type == SyncType.ANVIL_RECIPES) { - Set set = new HashSet<>(); - set.add(SyncType.ANVIL_RECIPES); - return set; - } else if (type == SyncType.CUSTOM_EFFECTS) { - Set set = new HashSet<>(); - set.add(SyncType.CUSTOM_EFFECTS); - return set; - } else if (type == SyncType.CUSTOM_ABILITY) { - Set set = new HashSet<>(); - set.add(SyncType.CUSTOM_ABILITY); - return set; - } else if (type == SyncType.CHAINED_ABILITY) { - Set set = new HashSet<>(); - set.add(SyncType.CHAINED_ABILITY); - return set; - } - return Collections.emptySet(); - } - - private static void updateAllPlayerRevisions(SyncType type, int revision) { - if (revision < 0) { - return; - } - for (PlayerSyncState state : playerSyncState.values()) { - state.updateRevision(type, revision); - } - } - - private static void updateAllPlayerRevisions(Map revisions) { - if (revisions.isEmpty()) { - return; - } - for (PlayerSyncState state : playerSyncState.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(Supplier supplier) { - this.supplier = supplier; - } - - private synchronized CachedSyncPayload getPayload(SyncType requestedType) { - if (!dirty && payload != null) { - return payload; - } - - int payloadRevision = revision; - NBTTagCompound data = supplier.get(); - 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; - return payload; - } - - private synchronized int invalidate() { - dirty = true; - payload = null; - revision++; - return revision; - } - - private synchronized int getRevisionValue() { - return revision; - } - - private synchronized void reset() { - dirty = true; - payload = null; - revision = 0; - } - - 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; - } - 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 Map revisions = new HashMap<>(); - - private PlayerSyncState(UUID ignored) { - } - - private synchronized int getRevision(SyncType type) { - Integer value = revisions.get(type); - return value == null ? -1 : value; - } - - private synchronized void updateRevision(SyncType type, int revision) { - revisions.put(type, revision); - } - - private synchronized void applyHandshake(Map incoming) { - if (incoming == null || incoming.isEmpty()) { - return; - } - revisions.putAll(incoming); - } - - private synchronized void reset() { - revisions.clear(); - } - - } -} +//package kamkeel.npcs.controllers; +// +//import cpw.mods.fml.relauncher.Side; +//import cpw.mods.fml.relauncher.SideOnly; +//import io.netty.buffer.ByteBuf; +//import io.netty.buffer.Unpooled; +//import kamkeel.npcs.addon.DBCAddon; +//import kamkeel.npcs.controllers.data.ability.Ability; +//import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; +//import kamkeel.npcs.network.LargeAbstractPacket; +//import kamkeel.npcs.network.PacketHandler; +//import kamkeel.npcs.network.enums.EnumSyncAction; +//import kamkeel.npcs.network.enums.SyncType; +//import kamkeel.npcs.network.packets.data.LoginPacket; +//import kamkeel.npcs.network.packets.data.ProfileSharedQuestPacket; +//import kamkeel.npcs.network.packets.data.ability.AbilityCooldownSyncPacket; +//import kamkeel.npcs.network.packets.data.ability.AbilityHotbarSyncPacket; +//import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; +//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.*; +//import noppes.npcs.controllers.data.*; +// +//import java.io.IOException; +//import java.util.*; +//import java.util.concurrent.ConcurrentHashMap; +//import java.util.function.Supplier; +// +//public class SyncControllerOld { +// +// public static boolean DEBUG_SYNC_LOGGING = false; +// +// private static final Map cacheEntries = new LinkedHashMap<>(); +// private static final ConcurrentHashMap playerSyncState = new ConcurrentHashMap<>(); +// +// +// private static void debug(String message, Object... args) { +// if (DEBUG_SYNC_LOGGING) { +// System.out.println("[SyncController] " + String.format(message, args)); +// } +// } +// +// private static final SyncType[] LOGIN_SYNC_TYPES = new SyncType[]{ +// SyncType.FACTION, +// SyncType.DIALOG_CATEGORY, +// SyncType.QUEST_CATEGORY, +// SyncType.WORKBENCH_RECIPES, +// SyncType.CARPENTRY_RECIPES, +// SyncType.ANVIL_RECIPES, +// SyncType.CUSTOM_EFFECTS, +// SyncType.MAGIC, +// SyncType.MAGIC_CYCLE, +// SyncType.CUSTOM_ABILITY, +// SyncType.CHAINED_ABILITY +// }; +// +// public static void load() { +// cacheEntries.clear(); +// playerSyncState.clear(); +// +// registerCache(SyncType.FACTION, SyncControllerOld::factionsNBT); +// registerCache(SyncType.DIALOG_CATEGORY, SyncControllerOld::dialogCategoriesNBT); +// registerCache(SyncType.QUEST_CATEGORY, SyncControllerOld::questCategoriesNBT); +// registerCache(SyncType.WORKBENCH_RECIPES, SyncControllerOld::workbenchNBT); +// registerCache(SyncType.CARPENTRY_RECIPES, SyncControllerOld::carpentryNBT); +// registerCache(SyncType.ANVIL_RECIPES, SyncControllerOld::anvilNBT); +// registerCache(SyncType.CUSTOM_EFFECTS, SyncControllerOld::customEffectsNBT); +// registerCache(SyncType.MAGIC, SyncControllerOld::magicsNBT); +// registerCache(SyncType.MAGIC_CYCLE, SyncControllerOld::magicCyclesNBT); +// registerCache(SyncType.CUSTOM_ABILITY, SyncControllerOld::customAbilitiesNBT); +// registerCache(SyncType.CHAINED_ABILITY, SyncControllerOld::chainedAbilitiesNBT); +// } +// +// public static void syncPlayer(EntityPlayerMP player) { +// SyncControllerOld.syncPlayerInternal(player, true); +// } +// +// private static void syncPlayerInternal(EntityPlayerMP player, boolean includePostPackets) { +// PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); +// +// for (SyncType type : LOGIN_SYNC_TYPES) { +// SyncCacheEntry entry = cacheEntries.get(type); +// if (entry == null) { +// continue; +// } +// +// int currentRevision = entry.getRevisionValue(); +// int lastRevision = state.getRevision(type); +// if (lastRevision == currentRevision) { +// continue; +// } +// +// CachedSyncPayload payload = entry.getPayload(type); +// if (payload == null) { +// continue; +// } +// +// if (lastRevision != payload.getRevision()) { +// debug("Sending %s data to %s", type, player.getCommandSenderName()); +// PacketHandler.Instance.sendToPlayer(new SyncPacket(type, payload), player); +// state.updateRevision(type, payload.getRevision()); +// } +// } +// +// if (includePostPackets) { +// sendPostLoginPackets(player); +// } +// } +// +// public static void beginLogin(EntityPlayerMP player) { +// playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); +// PacketHandler.Instance.sendToPlayer( +// new LoginPacket(getServerCacheKey(), getServerRevisionSnapshot()), +// player +// ); +// } +// +// public static void handleClientRevisionReport( +// EntityPlayerMP player, +// String serverKey, +// String previousServerKey, +// Map clientRevisions +// ) { +// String currentServerKey = getServerCacheKey(); +// PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); +// +// if (!currentServerKey.equals(serverKey)) { +// state.reset(); +// syncPlayer(player); +// return; +// } +// +// if (!currentServerKey.isEmpty()) { +// if (previousServerKey == null || !currentServerKey.equals(previousServerKey)) { +// state.reset(); +// syncPlayer(player); +// return; +// } +// } +// +// if (clientRevisions == null || clientRevisions.isEmpty()) { +// debug( +// "Client %s reported no cached revisions; forcing full sync", +// player.getCommandSenderName() +// ); +// state.reset(); +// } else { +// state.applyHandshake(clientRevisions); +// } +// +// syncPlayer(player); +// } +// +// private static void sendPostLoginPackets(EntityPlayerMP player) { +// DBCAddon.instance.syncPlayer(player); +// syncPlayerData(player, false); +// PartyInfoPacket.sendPartyData(player); +// ProfileSharedQuestPacket.sendToPlayer(player); +// +// // Sync skin overlays after full handshake to ensure the client is ready. +// // Overlays are not part of getSyncNBTFull(), and packets sent during +// // PlayerLoggedInEvent can arrive before the client entity exists. +// PlayerData data = PlayerData.get(player); +// if (data != null) { +// data.skinOverlays.updateClient(); +// } +// } +// +// private static Map getServerRevisionSnapshot() { +// Map snapshot = new LinkedHashMap<>(); +// for (SyncType type : LOGIN_SYNC_TYPES) { +// SyncCacheEntry entry = cacheEntries.get(type); +// if (entry != null) { +// snapshot.put(type, entry.getRevisionValue()); +// } +// } +// return snapshot; +// } +// +// private static final String SERVER_IDENTITY_KEY = UUID.randomUUID().toString(); +// +// private static String getServerCacheKey() { +// MinecraftServer server = MinecraftServer.getServer(); +// if (server == null) { +// return ""; +// } +// +// if (!server.isDedicatedServer()) { +// return ""; +// } +// +// return SERVER_IDENTITY_KEY; +// } +// +// public static int getCurrentRevision(SyncType type) { +// SyncCacheEntry entry = cacheEntries.get(type); +// return entry == null ? -1 : entry.getRevisionValue(); +// } +// +// public static NBTTagCompound workbenchNBT() { +// RecipeController controller = RecipeController.Instance; +// NBTTagList list = new NBTTagList(); +// NBTTagCompound compound = new NBTTagCompound(); +// for (RecipeCarpentry recipe : controller.globalRecipes.values()) { +// list.appendTag(recipe.writeNBT(false)); +// } +// compound.setTag("recipes", list); +// return compound; +// } +// +// public static NBTTagCompound carpentryNBT() { +// RecipeController controller = RecipeController.Instance; +// NBTTagList list = new NBTTagList(); +// NBTTagCompound compound = new NBTTagCompound(); +// for (RecipeCarpentry recipe : controller.carpentryRecipes.values()) { +// list.appendTag(recipe.writeNBT(false)); +// } +// compound.setTag("recipes", list); +// return compound; +// } +// +// public static NBTTagCompound anvilNBT() { +// RecipeController controller = RecipeController.Instance; +// NBTTagList list = new NBTTagList(); +// NBTTagCompound compound = new NBTTagCompound(); +// for (RecipeAnvil recipe : controller.anvilRecipes.values()) { +// list.appendTag(recipe.writeNBT(false)); +// } +// compound.setTag("recipes", list); +// return compound; +// } +// +// public static NBTTagCompound factionsNBT() { +// NBTTagList list = new NBTTagList(); +// NBTTagCompound compound = new NBTTagCompound(); +// for (Faction faction : FactionController.getInstance().factions.values()) { +// NBTTagCompound factioNBT = new NBTTagCompound(); +// faction.writeNBT(factioNBT); +// list.appendTag(factioNBT); +// } +// compound.setTag("Factions", list); +// return compound; +// } +// +// public static NBTTagCompound dialogCategoriesNBT() { +// NBTTagCompound compound = new NBTTagCompound(); +// NBTTagList categoryList = new NBTTagList(); +// for (DialogCategory category : DialogController.Instance.categories.values()) { +// NBTTagCompound questCompound = new NBTTagCompound(); +// NBTTagList dialogList = new NBTTagList(); +// for (int dialogID : category.dialogs.keySet()) { +// Dialog quest = category.dialogs.get(dialogID); +// dialogList.appendTag(quest.writeToNBT(new NBTTagCompound())); +// } +// questCompound.setTag("Data", dialogList); +// questCompound.setTag("CatNBT", category.writeSmallNBT(new NBTTagCompound())); +// categoryList.appendTag(questCompound); +// } +// compound.setTag("DialogCategories", categoryList); +// return compound; +// } +// +// public static NBTTagCompound questCategoriesNBT() { +// NBTTagCompound compound = new NBTTagCompound(); +// NBTTagList categoryList = new NBTTagList(); +// for (QuestCategory category : QuestController.Instance.categories.values()) { +// NBTTagCompound questCompound = new NBTTagCompound(); +// ; +// NBTTagList questList = new NBTTagList(); +// for (int questID : category.quests.keySet()) { +// Quest quest = category.quests.get(questID); +// questList.appendTag(quest.writeToNBT(new NBTTagCompound())); +// } +// questCompound.setTag("Data", questList); +// questCompound.setTag("CatNBT", category.writeSmallNBT(new NBTTagCompound())); +// categoryList.appendTag(questCompound); +// } +// compound.setTag("QuestCategories", categoryList); +// return compound; +// } +// +// public static NBTTagCompound customEffectsNBT() { +// NBTTagList list = new NBTTagList(); +// NBTTagCompound compound = new NBTTagCompound(); +// for (CustomEffect effect : CustomEffectController.getInstance().getCustomEffects().values()) { +// list.appendTag(effect.writeToNBT(false)); +// } +// compound.setTag("Data", list); +// return compound; +// } +// +// public static NBTTagCompound magicsNBT() { +// NBTTagList list = new NBTTagList(); +// NBTTagCompound compound = new NBTTagCompound(); +// for (Magic magic : MagicController.getInstance().magics.values()) { +// NBTTagCompound magicCompound = new NBTTagCompound(); +// magic.writeNBT(magicCompound); +// list.appendTag(magicCompound); +// } +// compound.setTag("Data", list); +// return compound; +// } +// +// public static NBTTagCompound magicCyclesNBT() { +// NBTTagList list = new NBTTagList(); +// NBTTagCompound compound = new NBTTagCompound(); +// for (MagicCycle cycle : MagicController.getInstance().cycles.values()) { +// NBTTagCompound cycleCompound = new NBTTagCompound(); +// cycle.writeNBT(cycleCompound); +// list.appendTag(cycleCompound); +// } +// compound.setTag("Data", list); +// return compound; +// } +// +// public static NBTTagCompound customAbilitiesNBT() { +// NBTTagList list = new NBTTagList(); +// NBTTagCompound compound = new NBTTagCompound(); +// for (Map.Entry entry : AbilityController.Instance.getCustomAbilities().entrySet()) { +// NBTTagCompound abilityNBT = entry.getValue().writeNBT(false); +// abilityNBT.setString("CustomAbilityId", entry.getKey()); +// list.appendTag(abilityNBT); +// } +// compound.setTag("Data", list); +// return compound; +// } +// +// public static void syncAllCustomAbilities() { +// CachedSyncPayload payload = rebuildNow(SyncType.CUSTOM_ABILITY); +// if (payload == null) { +// return; +// } +// PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CUSTOM_ABILITY, payload)); +// updateAllPlayerRevisions(SyncType.CUSTOM_ABILITY, payload.getRevision()); +// } +// +// public static NBTTagCompound chainedAbilitiesNBT() { +// NBTTagList list = new NBTTagList(); +// NBTTagCompound compound = new NBTTagCompound(); +// for (Map.Entry entry : AbilityController.Instance.getChainedAbilities().entrySet()) { +// NBTTagCompound chainNBT = entry.getValue().writeNBT(false); +// chainNBT.setString("ChainedAbilityId", entry.getKey()); +// list.appendTag(chainNBT); +// } +// compound.setTag("Data", list); +// return compound; +// } +// +// public static void syncAllChainedAbilities() { +// CachedSyncPayload payload = rebuildNow(SyncType.CHAINED_ABILITY); +// if (payload == null) { +// return; +// } +// PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CHAINED_ABILITY, payload)); +// updateAllPlayerRevisions(SyncType.CHAINED_ABILITY, payload.getRevision()); +// } +// +// public static void syncPlayerData(EntityPlayerMP player, boolean update) { +// PlayerData data = PlayerData.get(player); +// if (data != null) { +// if (update) { +// PacketHandler.Instance.sendToPlayer(new SyncPacket( +// SyncType.PLAYERDATA, +// EnumSyncAction.UPDATE, +// -1, +// data.getSyncNBT()) +// , player); +// } else { +// PacketHandler.Instance.sendToPlayer(new SyncPacket( +// SyncType.PLAYERDATA, +// EnumSyncAction.RELOAD, +// -1, +// data.getSyncNBTFull()), player); +// } +// } +// } +// +// public static void syncRemove(SyncType syncType, int id) { +// Map revisions = invalidateCaches(syncType); +// int revision = revisions.getOrDefault(syncType, getCurrentRevision(syncType)); +// PacketHandler.Instance.sendToAll(new SyncPacket( +// syncType, +// EnumSyncAction.REMOVE, +// id, +// revision, +// new NBTTagCompound() +// )); +// updateAllPlayerRevisions(revisions); +// } +// +// public static void syncAllDialogs() { +// CachedSyncPayload payload = rebuildNow(SyncType.DIALOG_CATEGORY); +// if (payload == null) { +// return; +// } +// PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.DIALOG_CATEGORY, payload)); +// updateAllPlayerRevisions(SyncType.DIALOG_CATEGORY, payload.getRevision()); +// } +// +// public static void syncAllQuests() { +// CachedSyncPayload payload = rebuildNow(SyncType.QUEST_CATEGORY); +// if (payload == null) { +// return; +// } +// PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.QUEST_CATEGORY, payload)); +// updateAllPlayerRevisions(SyncType.QUEST_CATEGORY, payload.getRevision()); +// } +// +// public static void syncAllWorkbenchRecipes() { +// CachedSyncPayload payload = rebuildNow(SyncType.WORKBENCH_RECIPES); +// if (payload == null) { +// return; +// } +// PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.WORKBENCH_RECIPES, payload)); +// updateAllPlayerRevisions(SyncType.WORKBENCH_RECIPES, payload.getRevision()); +// } +// +// public static void syncAllCarpentryRecipes() { +// CachedSyncPayload payload = rebuildNow(SyncType.CARPENTRY_RECIPES); +// if (payload == null) { +// return; +// } +// PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CARPENTRY_RECIPES, payload)); +// updateAllPlayerRevisions(SyncType.CARPENTRY_RECIPES, payload.getRevision()); +// } +// +// public static void syncAllAnvilRecipes() { +// CachedSyncPayload payload = rebuildNow(SyncType.ANVIL_RECIPES); +// if (payload == null) { +// return; +// } +// PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.ANVIL_RECIPES, payload)); +// updateAllPlayerRevisions(SyncType.ANVIL_RECIPES, payload.getRevision()); +// } +// +// public static void syncAllCustomEffects() { +// CachedSyncPayload payload = rebuildNow(SyncType.CUSTOM_EFFECTS); +// if (payload == null) { +// return; +// } +// PacketHandler.Instance.sendToAll(new SyncPacket(SyncType.CUSTOM_EFFECTS, payload)); +// updateAllPlayerRevisions(SyncType.CUSTOM_EFFECTS, payload.getRevision()); +// } +// +// @SideOnly(Side.CLIENT) +// public static void clientSync(SyncType syncType, int revision, NBTTagCompound fullCompound) { +// if (syncType == SyncType.FACTION) { +// NBTTagList list = fullCompound.getTagList("Factions", 10); +// for (int i = 0; i < list.tagCount(); i++) { +// Faction faction = new Faction(); +// faction.readNBT(list.getCompoundTagAt(i)); +// FactionController.getInstance().factionsSync.put(faction.id, faction); +// } +// FactionController.getInstance().factions = FactionController.getInstance().factionsSync; +// FactionController.getInstance().factionsSync = new HashMap(); +// } else if (syncType == SyncType.DIALOG_CATEGORY) { +// if (!fullCompound.hasNoTags()) { +// NBTTagList categories = fullCompound.getTagList("DialogCategories", 10); +// for (int j = 0; j < fullCompound.getTagList("DialogCategories", 10).tagCount(); j++) { +// NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); +// if (categoryCompound.hasNoTags()) +// continue; +// +// DialogCategory category = new DialogCategory(); +// category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); +// NBTTagList dialogList = categoryCompound.getTagList("Data", 10); +// if (DialogController.Instance.categoriesSync.containsKey(category.id)) { +// category = DialogController.Instance.categoriesSync.get(category.id); +// category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); +// } +// for (int i = 0; i < dialogList.tagCount(); i++) { +// Dialog dialog = new Dialog(); +// dialog.readNBT(dialogList.getCompoundTagAt(i)); +// dialog.category = category; +// category.dialogs.put(dialog.id, dialog); +// } +// DialogController.Instance.categoriesSync.put(category.id, category); +// } +// } +// +// HashMap dialogs = new HashMap(); +// for (DialogCategory category : DialogController.Instance.categoriesSync.values()) { +// for (Dialog dialog : category.dialogs.values()) { +// dialogs.put(dialog.id, dialog); +// } +// } +// +// DialogController.Instance.categories = DialogController.Instance.categoriesSync; +// DialogController.Instance.dialogs = dialogs; +// DialogController.Instance.categoriesSync = new HashMap(); +// } else if (syncType == SyncType.QUEST_CATEGORY) { +// if (!fullCompound.hasNoTags()) { +// NBTTagList categories = fullCompound.getTagList("QuestCategories", 10); +// for (int j = 0; j < fullCompound.getTagList("QuestCategories", 10).tagCount(); j++) { +// NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); +// if (categoryCompound.hasNoTags()) +// continue; +// +// QuestCategory category = new QuestCategory(); +// category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); +// NBTTagList questList = categoryCompound.getTagList("Data", 10); +// if (QuestController.Instance.categoriesSync.containsKey(category.id)) { +// category = QuestController.Instance.categoriesSync.get(category.id); +// category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); +// } +// for (int i = 0; i < questList.tagCount(); i++) { +// Quest quest = new Quest(); +// quest.readNBT(questList.getCompoundTagAt(i)); +// quest.category = category; +// category.quests.put(quest.id, quest); +// } +// QuestController.Instance.categoriesSync.put(category.id, category); +// } +// } +// +// HashMap quests = new HashMap(); +// for (QuestCategory category : QuestController.Instance.categoriesSync.values()) { +// for (Quest quest : category.quests.values()) { +// quests.put(quest.id, quest); +// } +// } +// +// QuestController.Instance.categories = QuestController.Instance.categoriesSync; +// QuestController.Instance.quests = quests; +// QuestController.Instance.categoriesSync = new HashMap(); +// } else if (syncType == SyncType.PLAYERDATA) { +// ClientCacheHandler.playerData.setSyncNBTFull(fullCompound); +// } else if (syncType == SyncType.MAGIC) { +// NBTTagList list = fullCompound.getTagList("Data", 10); +// MagicController mc = MagicController.getInstance(); +// mc.magicSync.clear(); +// for (int i = 0; i < list.tagCount(); i++) { +// Magic magic = new Magic(); +// magic.readNBT(list.getCompoundTagAt(i)); +// mc.magicSync.put(magic.id, magic); +// } +// +// mc.magics.clear(); +// mc.magics.putAll(mc.magicSync); +// mc.magicSync.clear(); +// } else if (syncType == SyncType.MAGIC_CYCLE) { +// NBTTagList list = fullCompound.getTagList("Data", 10); +// MagicController mc = MagicController.getInstance(); +// mc.cyclesSync.clear(); +// for (int i = 0; i < list.tagCount(); i++) { +// MagicCycle cycle = new MagicCycle(); +// cycle.readNBT(list.getCompoundTagAt(i)); +// mc.cyclesSync.put(cycle.id, cycle); +// } +// +// mc.cycles.clear(); +// mc.cycles.putAll(mc.cyclesSync); +// mc.cyclesSync.clear(); +// } else if (syncType == SyncType.WORKBENCH_RECIPES) { +// NBTTagList list = fullCompound.getTagList("recipes", 10); +// if (list == null) +// return; +// +// for (int i = 0; i < list.tagCount(); i++) { +// NBTTagCompound recipeCompound = list.getCompoundTagAt(i); +// RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); +// recipe.readNBT(recipeCompound); +// RecipeController.syncRecipes.put(recipe.id, recipe); +// } +// +// RecipeController.reloadGlobalRecipes(RecipeController.syncRecipes); +// RecipeController.syncRecipes = new HashMap(); +// } else if (syncType == SyncType.CARPENTRY_RECIPES) { +// NBTTagList list = fullCompound.getTagList("recipes", 10); +// if (list == null) +// return; +// +// for (int i = 0; i < list.tagCount(); i++) { +// NBTTagCompound recipeCompound = list.getCompoundTagAt(i); +// RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); +// recipe.readNBT(recipeCompound); +// RecipeController.syncRecipes.put(recipe.id, recipe); +// } +// +// RecipeController.Instance.carpentryRecipes = RecipeController.syncRecipes; +// RecipeController.syncRecipes = new HashMap(); +// } else if (syncType == SyncType.ANVIL_RECIPES) { +// NBTTagList list = fullCompound.getTagList("recipes", 10); +// if (list == null) +// return; +// +// for (int i = 0; i < list.tagCount(); i++) { +// RecipeAnvil recipe = new RecipeAnvil(); +// recipe.readNBT(list.getCompoundTagAt(i)); +// RecipeController.syncAnvilRecipes.put(recipe.id, recipe); +// } +// +// RecipeController.Instance.anvilRecipes = RecipeController.syncAnvilRecipes; +// RecipeController.syncAnvilRecipes = new HashMap(); +// } else if (syncType == SyncType.CUSTOM_EFFECTS) { +// NBTTagList list = fullCompound.getTagList("Data", 10); +// CustomEffectController ce = CustomEffectController.getInstance(); +// ce.customEffectsSync.clear(); +// for (int i = 0; i < list.tagCount(); i++) { +// CustomEffect effect = new CustomEffect(); +// effect.readFromNBT(list.getCompoundTagAt(i)); +// ClientCacheHandler.getImageData(effect.icon); +// ce.customEffectsSync.put(effect.id, effect); +// } +// +// ce.getCustomEffects().clear(); +// ce.getCustomEffects().putAll(ce.customEffectsSync); +// ce.customEffectsSync.clear(); +// } else if (syncType == SyncType.CUSTOM_ABILITY) { +// NBTTagList list = fullCompound.getTagList("Data", 10); +// LinkedHashMap sync = new LinkedHashMap<>(); +// for (int i = 0; i < list.tagCount(); i++) { +// NBTTagCompound nbt = list.getCompoundTagAt(i); +// String name = nbt.getString("CustomAbilityId"); +// Ability ability = AbilityController.Instance.fromNBT(nbt); +// if (ability != null) { +// ability.setName(name); +// sync.put(name, ability); +// } +// } +// AbilityController.Instance.setCustomAbilities(sync); +// } else if (syncType == SyncType.CHAINED_ABILITY) { +// NBTTagList list = fullCompound.getTagList("Data", 10); +// LinkedHashMap sync = new LinkedHashMap<>(); +// for (int i = 0; i < list.tagCount(); i++) { +// NBTTagCompound nbt = list.getCompoundTagAt(i); +// String id = nbt.getString("ChainedAbilityId"); +// ChainedAbility chain = new ChainedAbility(); +// chain.readNBT(nbt); +// chain.setName(id); +// sync.put(id, chain); +// } +// AbilityController.Instance.setChainedAbilities(sync); +// } +// +// ClientCacheHandler.updateClientRevision(syncType, revision); +// } +// +// @SideOnly(Side.CLIENT) +// public static void clientUpdate(SyncType syncType, int category_id, int revision, NBTTagCompound compound) { +// if (syncType == SyncType.FACTION) { +// Faction faction = new Faction(); +// faction.readNBT(compound); +// FactionController.getInstance().factions.put(faction.id, faction); +// } else if (syncType == SyncType.DIALOG) { +// DialogCategory category = DialogController.Instance.categories.get(category_id); +// Dialog dialog = new Dialog(); +// dialog.category = category; +// dialog.readNBT(compound); +// DialogController.Instance.dialogs.put(dialog.id, dialog); +// category.dialogs.put(dialog.id, dialog); +// } else if (syncType == SyncType.QUEST) { +// QuestCategory category = QuestController.Instance.categories.get(category_id); +// Quest quest = new Quest(); +// quest.category = category; +// quest.readNBT(compound); +// QuestController.Instance.quests.put(quest.id, quest); +// category.quests.put(quest.id, quest); +// } else if (syncType == SyncType.QUEST_CATEGORY) { +// QuestCategory category = new QuestCategory(); +// category.readSmallNBT(compound.getCompoundTag("CatNBT")); +// NBTTagList list = compound.getTagList("Data", 10); +// if (QuestController.Instance.categoriesSync.containsKey(category.id)) { +// category = QuestController.Instance.categoriesSync.get(category.id); +// category.readSmallNBT(compound.getCompoundTag("CatNBT")); +// } +// for (int i = 0; i < list.tagCount(); i++) { +// Quest quest = new Quest(); +// quest.readNBT(list.getCompoundTagAt(i)); +// quest.category = category; +// category.quests.put(quest.id, quest); +// } +// QuestController.Instance.categories.put(category.id, category); +// } else if (syncType == SyncType.DIALOG_CATEGORY) { +// DialogCategory category = new DialogCategory(); +// category.readSmallNBT(compound.getCompoundTag("CatNBT")); +// NBTTagList list = compound.getTagList("Data", 10); +// if (DialogController.Instance.categoriesSync.containsKey(category.id)) { +// category = DialogController.Instance.categoriesSync.get(category.id); +// category.readSmallNBT(compound.getCompoundTag("CatNBT")); +// } +// for (int i = 0; i < list.tagCount(); i++) { +// Dialog dialog = new Dialog(); +// dialog.readNBT(list.getCompoundTagAt(i)); +// dialog.category = category; +// category.dialogs.put(dialog.id, dialog); +// } +// DialogController.Instance.categories.put(category.id, category); +// } else if (syncType == SyncType.MAGIC_CYCLE) { +// MagicCycle cycle = new MagicCycle(); +// cycle.readNBT(compound); +// MagicController.getInstance().cycles.put(cycle.id, cycle); +// } else if (syncType == SyncType.MAGIC) { +// Magic magic = new Magic(); +// magic.readNBT(compound); +// MagicController.getInstance().magics.put(magic.id, magic); +// } else if (syncType == SyncType.PLAYERDATA) { +// ClientCacheHandler.playerData.setSyncNBT(compound); +// } else if (syncType == SyncType.CUSTOM_EFFECTS) { +// CustomEffect effect = new CustomEffect(); +// effect.readFromNBT(compound); +// ClientCacheHandler.getImageData(effect.icon); +// +// CustomEffectController.Instance.getCustomEffects().put(effect.id, effect); +// } +// +// ClientCacheHandler.updateClientRevision(syncType, revision); +// } +// +// public static void syncUpdate(SyncType 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); +// } +// +// +// public static NBTTagCompound updateQuestCat(QuestCategory questCategory) { +// NBTTagCompound questCompound = new NBTTagCompound(); +// NBTTagList questList = new NBTTagList(); +// for (int questID : questCategory.quests.keySet()) { +// Quest quest = questCategory.quests.get(questID); +// questList.appendTag(quest.writeToNBT(new NBTTagCompound())); +// } +// questCompound.setTag("Data", questList); +// questCompound.setTag("CatNBT", questCategory.writeSmallNBT(new NBTTagCompound())); +// return questCompound; +// } +// +// public static NBTTagCompound updateDialogCat(DialogCategory dialogCategory) { +// NBTTagCompound dialogCompound = new NBTTagCompound(); +// NBTTagList dialogList = new NBTTagList(); +// for (int questID : dialogCategory.dialogs.keySet()) { +// Dialog dialog = dialogCategory.dialogs.get(questID); +// dialogList.appendTag(dialog.writeToNBT(new NBTTagCompound())); +// } +// dialogCompound.setTag("Data", dialogList); +// dialogCompound.setTag("CatNBT", dialogCategory.writeSmallNBT(new NBTTagCompound())); +// return dialogCompound; +// } +// +// @SideOnly(Side.CLIENT) +// public static void clientSyncRemove(SyncType syncType, int id, int revision) { +// if (syncType == SyncType.FACTION) { +// FactionController.getInstance().factions.remove(id); +// } else if (syncType == SyncType.DIALOG) { +// Dialog dialog = DialogController.Instance.dialogs.remove(id); +// if (dialog != null) { +// dialog.category.dialogs.remove(id); +// } +// } else if (syncType == SyncType.DIALOG_CATEGORY) { +// DialogCategory dialogCategory = DialogController.Instance.categories.remove(id); +// if (dialogCategory != null) { +// DialogController.Instance.dialogs.keySet().removeAll(dialogCategory.dialogs.keySet()); +// } +// } else if (syncType == SyncType.QUEST) { +// Quest quest = QuestController.Instance.quests.remove(id); +// if (quest != null) { +// quest.category.quests.remove(id); +// } +// } else if (syncType == SyncType.QUEST_CATEGORY) { +// QuestCategory questCategory = QuestController.Instance.categories.remove(id); +// if (questCategory != null) { +// QuestController.Instance.quests.keySet().removeAll(questCategory.quests.keySet()); +// } +// } else if (syncType == SyncType.MAGIC) { +// for (MagicCycle cycle : MagicController.getInstance().cycles.values()) { +// cycle.associations.remove(id); +// } +// MagicController.getInstance().cycles.remove(id); +// } else if (syncType == SyncType.MAGIC_CYCLE) { +// MagicController.getInstance().cycles.remove(id); +// } else if (syncType == SyncType.CUSTOM_EFFECTS) { +// CustomEffectController.Instance.getCustomEffects().remove(id); +// } +// +// ClientCacheHandler.updateClientRevision(syncType, revision); +// } +// +// public static void syncEffects(EntityPlayerMP playerMP) { +// ConcurrentHashMap playerEffects = CustomEffectController.getInstance().getPlayerEffects(playerMP); +// PlayerData playerData = PlayerData.get(playerMP); +// playerData.effectData.setEffects(playerEffects); +// +// NBTTagCompound compound = playerData.getPlayerEffects(); +// PacketHandler.Instance.sendToPlayer(new SyncEffectPacket(compound), playerMP); +// } +// +// /** +// * Full sync of all player ability state to the client: unlocked abilities, +// * selection, toggles, hotbar layout, and cooldowns. +// * Used for entity reconstruction and other full-refresh scenarios. +// */ +// public static void syncAbilities(EntityPlayerMP playerMP) { +// PlayerData playerData = PlayerData.get(playerMP); +// if (playerData == null) return; +// +// PlayerAbilitySyncPacket.sendToPlayer(playerMP); +// AbilityHotbarSyncPacket.sendToPlayer(playerMP); +// AbilityCooldownSyncPacket.sendToPlayer(playerMP); +// } +// +// /** +// * Lightweight sync of cooldown state only (global + per-ability). +// * Called periodically (every 10 ticks) to keep the client's cooldown +// * display accurate. Cooldowns are transient and not part of NBT sync. +// */ +// public static void syncAbilityCooldowns(EntityPlayerMP playerMP) { +// PlayerData playerData = PlayerData.get(playerMP); +// if (playerData == null) return; +// +// AbilityCooldownSyncPacket.sendToPlayer(playerMP); +// } +// +// @SideOnly(Side.CLIENT) +// public static void clientSyncEffects(NBTTagCompound compound) { +// PlayerData playerData = PlayerData.get(Minecraft.getMinecraft().thePlayer); +// if (playerData != null) { +// playerData.setPlayerEffects(compound); +// } +// } +// +// private static void registerCache(SyncType type, Supplier supplier) { +// cacheEntries.put(type, new SyncCacheEntry(supplier)); +// } +// +// private static CachedSyncPayload rebuildNow(SyncType type) { +// SyncCacheEntry entry = cacheEntries.get(type); +// if (entry == null) { +// return null; +// } +// entry.invalidate(); +// return entry.getPayload(type); +// } +// +// public static Map invalidateCaches(SyncType type) { +// Set targets = getInvalidationTargets(type); +// Map revisions = new LinkedHashMap<>(); +// for (SyncType target : targets) { +// SyncCacheEntry entry = cacheEntries.get(target); +// if (entry != null) { +// int newRevision = entry.invalidate(); +// revisions.put(target, newRevision); +// } +// } +// return revisions; +// } +// +// private static Set getInvalidationTargets(SyncType type) { +// if (type == SyncType.DIALOG || type == SyncType.DIALOG_CATEGORY) { +// Set set = new HashSet<>(); +// set.add(SyncType.DIALOG_CATEGORY); +// return set; +// } else if (type == SyncType.QUEST || type == SyncType.QUEST_CATEGORY) { +// Set set = new HashSet<>(); +// set.add(SyncType.QUEST_CATEGORY); +// return set; +// } else if (type == SyncType.FACTION) { +// Set set = new HashSet<>(); +// set.add(SyncType.FACTION); +// return set; +// } else if (type == SyncType.MAGIC) { +// Set set = new HashSet<>(); +// set.add(SyncType.MAGIC); +// return set; +// } else if (type == SyncType.MAGIC_CYCLE) { +// Set set = new HashSet<>(); +// set.add(SyncType.MAGIC_CYCLE); +// return set; +// } else if (type == SyncType.WORKBENCH_RECIPES) { +// Set set = new HashSet<>(); +// set.add(SyncType.WORKBENCH_RECIPES); +// return set; +// } else if (type == SyncType.CARPENTRY_RECIPES) { +// Set set = new HashSet<>(); +// set.add(SyncType.CARPENTRY_RECIPES); +// return set; +// } else if (type == SyncType.ANVIL_RECIPES) { +// Set set = new HashSet<>(); +// set.add(SyncType.ANVIL_RECIPES); +// return set; +// } else if (type == SyncType.CUSTOM_EFFECTS) { +// Set set = new HashSet<>(); +// set.add(SyncType.CUSTOM_EFFECTS); +// return set; +// } else if (type == SyncType.CUSTOM_ABILITY) { +// Set set = new HashSet<>(); +// set.add(SyncType.CUSTOM_ABILITY); +// return set; +// } else if (type == SyncType.CHAINED_ABILITY) { +// Set set = new HashSet<>(); +// set.add(SyncType.CHAINED_ABILITY); +// return set; +// } +// return Collections.emptySet(); +// } +// +// private static void updateAllPlayerRevisions(SyncType type, int revision) { +// if (revision < 0) { +// return; +// } +// for (PlayerSyncState state : playerSyncState.values()) { +// state.updateRevision(type, revision); +// } +// } +// +// private static void updateAllPlayerRevisions(Map revisions) { +// if (revisions.isEmpty()) { +// return; +// } +// for (PlayerSyncState state : playerSyncState.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(Supplier supplier) { +// this.supplier = supplier; +// } +// +// private synchronized CachedSyncPayload getPayload(SyncType requestedType) { +// if (!dirty && payload != null) { +// return payload; +// } +// +// int payloadRevision = revision; +// NBTTagCompound data = supplier.get(); +// 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; +// return payload; +// } +// +// private synchronized int invalidate() { +// dirty = true; +// payload = null; +// revision++; +// return revision; +// } +// +// private synchronized int getRevisionValue() { +// return revision; +// } +// +// private synchronized void reset() { +// dirty = true; +// payload = null; +// revision = 0; +// } +// +// 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; +// } +// 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 Map revisions = new HashMap<>(); +// +// private PlayerSyncState(UUID ignored) { +// } +// +// private synchronized int getRevision(SyncType type) { +// Integer value = revisions.get(type); +// return value == null ? -1 : value; +// } +// +// private synchronized void updateRevision(SyncType type, int revision) { +// revisions.put(type, revision); +// } +// +// private synchronized void applyHandshake(Map incoming) { +// if (incoming == null || incoming.isEmpty()) { +// return; +// } +// revisions.putAll(incoming); +// } +// +// private synchronized void reset() { +// revisions.clear(); +// } +// +// } +//} From facee7c388d40bfbd7c6f5a79b4fbaf3070616ce Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 2 Apr 2026 00:24:15 +0200 Subject: [PATCH 05/34] Expanded SyncController scope to allow syncing based on string operation keys instead of just int ids (i.e ability keys) --- .../npcs/controllers/SyncController.java | 47 ++++++++++++----- .../npcs/controllers/sync/SyncHandler.java | 22 +++++++- .../packets/data/large/SyncPacket.java | 51 ++++++++++--------- .../java/noppes/npcs/NoppesUtilServer.java | 6 +-- .../java/noppes/npcs/ServerEventsHandler.java | 2 + 5 files changed, 84 insertions(+), 44 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index b4119ec8d..eed423ea1 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -4,8 +4,6 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -import kamkeel.npcdbc.network.DBCPacketHandler; -import kamkeel.npcdbc.network.packets.get.DBCInfoSyncPacket; import kamkeel.npcs.addon.DBCAddon; import kamkeel.npcs.controllers.sync.SyncHandler; import kamkeel.npcs.controllers.sync.SyncRegistry; @@ -26,7 +24,6 @@ import noppes.npcs.LogWriter; import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.controllers.data.PlayerData; -import java.io.IOException; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; @@ -146,7 +143,7 @@ public class SyncController { * revision state. Used after the login handshake and by server-side callers * that need to refresh a player's global cached sync families. */ - public static void syncPlayer(EntityPlayerMP player) { + public static void dispatchSyncForAllTypes(EntityPlayerMP player) { PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); // Registry-driven login iteration: iterate only cached types @@ -194,7 +191,7 @@ public class SyncController { else // The client and server agree on identity, so seed the stored state from the report. state.applyHandshake(clientRevisions); - syncPlayer(player); + dispatchSyncForAllTypes(player); } @@ -217,21 +214,42 @@ public class SyncController { // ═══════════════════════════════════════════════════════════════════════ // Server-side dispatchers — registry-driven // ═══════════════════════════════════════════════════════════════════════ + public static void syncUpdate(SyncType type, NBTTagCompound compound) { + Map revisions = invalidateCaches(type); + int revision = getRequestedInvalidationRevision(type, revisions); + PacketHandler.Instance.sendToAll(new SyncPacket(type, EnumSyncAction.UPDATE, -1, null, revision, compound)); + updateAllPlayerRevisions(revisions); + } - public static void syncRemove(SyncType syncType, int id) { - Map revisions = invalidateCaches(syncType); - int revision = getRequestedInvalidationRevision(syncType, revisions); - PacketHandler.Instance.sendToAll(new SyncPacket(syncType, EnumSyncAction.REMOVE, id, revision, new NBTTagCompound())); + public static void syncUpdate(SyncType type, String key, NBTTagCompound compound) { + Map revisions = invalidateCaches(type); + int revision = getRequestedInvalidationRevision(type, revisions); + PacketHandler.Instance.sendToAll(new SyncPacket(type, EnumSyncAction.UPDATE, -1, key, revision, compound)); updateAllPlayerRevisions(revisions); } public static void syncUpdate(SyncType type, int cat, NBTTagCompound compound) { Map revisions = invalidateCaches(type); int revision = getRequestedInvalidationRevision(type, revisions); - PacketHandler.Instance.sendToAll(new SyncPacket(type, EnumSyncAction.UPDATE, cat, revision, compound)); + PacketHandler.Instance.sendToAll(new SyncPacket(type, EnumSyncAction.UPDATE, cat, null, revision, compound)); updateAllPlayerRevisions(revisions); } + public static void syncRemove(SyncType syncType, String key) { + Map revisions = invalidateCaches(syncType); + int revision = getRequestedInvalidationRevision(syncType, revisions); + PacketHandler.Instance.sendToAll(new SyncPacket(syncType, EnumSyncAction.REMOVE, -1, key, revision, null)); + updateAllPlayerRevisions(revisions); + } + + public static void syncRemove(SyncType syncType, int id) { + Map revisions = invalidateCaches(syncType); + int revision = getRequestedInvalidationRevision(syncType, revisions); + PacketHandler.Instance.sendToAll(new SyncPacket(syncType, EnumSyncAction.REMOVE, id, null, revision, null)); + updateAllPlayerRevisions(revisions); + } + + // ═══════════════════════════════════════════════════════════════════════ // Client-side handlers — registry-driven handler delegation // ═══════════════════════════════════════════════════════════════════════ @@ -250,11 +268,12 @@ public class SyncController { } @SideOnly(Side.CLIENT) - public static void clientHandleUpdate(SyncType syncType, int category_id, int revision, NBTTagCompound compound) { + public static void clientHandleUpdate(SyncType syncType, int id, String key, int revision, NBTTagCompound compound) { SyncHandler handler = SyncRegistry.getHandler(syncType); if (handler == null) return; try { - handler.clientHandleUpdate(compound, category_id); + handler.clientHandleUpdate(compound, id); + handler.clientHandleUpdate(compound, key); } catch (Exception e) { LogWriter.error("[SyncController] Failed to handle UPDATE for sync type " + syncType, e); } @@ -263,12 +282,12 @@ public class SyncController { } @SideOnly(Side.CLIENT) - public static void clientHandleRemove(SyncType syncType, int id, int revision, NBTTagCompound compound) { + public static void clientHandleRemove(SyncType syncType, int id, String key, int revision) { SyncHandler handler = SyncRegistry.getHandler(syncType); if (handler == null) return; try { handler.clientHandleRemove(id); - handler.clientHandleRemove(compound); + handler.clientHandleRemove(key); } catch (Exception e) { LogWriter.error("[SyncController] Failed to handle REMOVE for sync type " + syncType, e); } diff --git a/src/main/java/kamkeel/npcs/controllers/sync/SyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/SyncHandler.java index 5cfe5e7a6..ef0ce0bda 100644 --- a/src/main/java/kamkeel/npcs/controllers/sync/SyncHandler.java +++ b/src/main/java/kamkeel/npcs/controllers/sync/SyncHandler.java @@ -52,8 +52,26 @@ public interface SyncHandler { * @param fullCompound the full serialized dataset from {@link #serializeAll()} */ @SideOnly(Side.CLIENT) - void clientHandleReload(NBTTagCompound fullCompound); + default void clientHandleReload(NBTTagCompound fullCompound){ + // No-op: type does not support individual updates + } + /** + * Handle an UPDATE action on the client. + * Deserialize a single object and insert/replace it in the client-side data. + * + *

Default: no-op. Types that don't support individual updates + * use {@code syncAll()} for full reload instead.

+ * + * @param compound the serialized single object + * @param categoryKey the parent category key (null if not applicable) + */ + @SideOnly(Side.CLIENT) + default void clientHandleUpdate(NBTTagCompound compound, String categoryKey) { + // No-op: type does not support individual updates + } + + /** * Handle an UPDATE action on the client. Deserialize a single * entity and insert/replace it in the client-side data. @@ -75,7 +93,7 @@ public interface SyncHandler { * Int-keyed handlers should override {@link #clientHandleRemove(int)} instead. */ @SideOnly(Side.CLIENT) - default void clientHandleRemove(NBTTagCompound compound) { + default void clientHandleRemove(String key) { } /** 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 3ebe78712..8ef77a69a 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 @@ -28,6 +28,7 @@ public final class SyncPacket extends LargeAbstractPacket { private EnumSyncAction enumSyncAction; private NBTTagCompound syncData; private int operationID; + private String operationKey; private int revision = -1; private byte[] cachedPayload; private byte[][] cachedChunks; @@ -36,14 +37,15 @@ public final class SyncPacket extends LargeAbstractPacket { } public SyncPacket(SyncType syncType, EnumSyncAction enumSyncAction, int catId, NBTTagCompound syncData) { - this(syncType, enumSyncAction, catId, -1, syncData); + this(syncType, enumSyncAction, catId, null, -1, syncData); } - public SyncPacket(SyncType syncType, EnumSyncAction enumSyncAction, int catId, int revision, NBTTagCompound syncData) { + public SyncPacket(SyncType syncType, EnumSyncAction enumSyncAction, int operationId, String operationKey, int revision, NBTTagCompound syncData) { this.syncType = syncType; this.enumSyncAction = enumSyncAction; this.syncData = syncData; - this.operationID = catId; + this.operationID = operationId; + this.operationKey = operationKey; this.revision = revision; } @@ -72,13 +74,19 @@ public final class SyncPacket extends LargeAbstractPacket { return cachedPayload; } + boolean hasKey = operationKey != null; + boolean hasNbt = syncData != null; + ByteBuf buffer = Unpooled.buffer(); try { buffer.writeInt(syncType.ordinal()); buffer.writeInt(enumSyncAction.ordinal()); buffer.writeInt(operationID); + buffer.writeBoolean(hasKey); + if (hasKey) ByteBufUtils.writeString(buffer, operationKey); buffer.writeInt(revision); - ByteBufUtils.writeBigNBT(buffer, syncData); + buffer.writeBoolean(hasNbt); + if (hasNbt) ByteBufUtils.writeBigNBT(buffer, syncData); byte[] bytes = new byte[buffer.readableBytes()]; buffer.readBytes(bytes); @@ -126,39 +134,32 @@ public final class SyncPacket extends LargeAbstractPacket { int syncTypeOrdinal = data.readInt(); int syncActionOrdinal = data.readInt(); - int categoryID = data.readInt(); - int incomingRevision = data.readInt(); + operationID = data.readInt(); + if (data.readBoolean()) operationKey = ByteBufUtils.readString(data); + revision = data.readInt(); + if (data.readBoolean()) syncData = ByteBufUtils.readBigNBT(data); - SyncType type = SyncType.byOrdinal(syncTypeOrdinal); - if (type == null) { + + syncType = SyncType.byOrdinal(syncTypeOrdinal); + if (syncType == null) { LogWriter.error("[SyncPacket] Unknown sync type ordinal: " + syncTypeOrdinal + "; skipping"); return; } + EnumSyncAction action = EnumSyncAction.values()[syncActionOrdinal]; - try { - NBTTagCompound tag = ByteBufUtils.readBigNBT(data); - handleSyncPacketClient(type, action, categoryID, incomingRevision, tag); - } catch (RuntimeException e) { - LogWriter.error(String.format("Attempted to Sync %s but it was too big", type.toString())); - } + handleSync(syncType, action, operationID, operationKey, revision, syncData); } - private void handleSyncPacketClient( - SyncType syncType, - EnumSyncAction enumSyncAction, - int id, - int incomingRevision, - NBTTagCompound data - ) { - switch (enumSyncAction) { + private void handleSync(SyncType type, EnumSyncAction action, int id, String key, int revision, NBTTagCompound data) { + switch (action) { case RELOAD: - SyncController.clientHandleAll(syncType, incomingRevision, data); + SyncController.clientHandleAll(type, revision, data); break; case UPDATE: - SyncController.clientHandleUpdate(syncType, id, incomingRevision, data); + SyncController.clientHandleUpdate(type, id, key, revision, data); break; case REMOVE: - SyncController.clientHandleRemove(syncType, id, incomingRevision, data); + SyncController.clientHandleRemove(type, id, key, revision); break; } } diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index 53dbfb239..dba0c27c1 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -640,7 +640,7 @@ public class NoppesUtilServer { playerdata.save(); } if (pl != null) { - SyncController.syncPlayer((EntityPlayerMP) pl); + SyncController.dispatchSyncForAllTypes((EntityPlayerMP) pl); } sendPlayerData(type, player, name); } @@ -1229,7 +1229,7 @@ public class NoppesUtilServer { playerdata.save(); if (pl != null) { - SyncController.syncPlayer((EntityPlayerMP) pl); + SyncController.dispatchSyncForAllTypes((EntityPlayerMP) pl); } } @@ -1261,7 +1261,7 @@ public class NoppesUtilServer { playerdata.save(); if (pl != null) { - SyncController.syncPlayer((EntityPlayerMP) pl); + SyncController.dispatchSyncForAllTypes((EntityPlayerMP) pl); } } } diff --git a/src/main/java/noppes/npcs/ServerEventsHandler.java b/src/main/java/noppes/npcs/ServerEventsHandler.java index 9664933be..d36c95eb5 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -260,6 +260,7 @@ public class ServerEventsHandler { SyncType.WORKBENCH_RECIPES, EnumSyncAction.RELOAD, -1, + null, SyncController.getCurrentServerRevision(SyncType.WORKBENCH_RECIPES), WorkbenchRecipeSyncHandler.getInstance().serializeAll() ), (EntityPlayerMP) player); @@ -269,6 +270,7 @@ public class ServerEventsHandler { SyncType.CARPENTRY_RECIPES, EnumSyncAction.RELOAD, -1, + null, SyncController.getCurrentServerRevision(SyncType.CARPENTRY_RECIPES), CarpentryRecipeSyncHandler.getInstance().serializeAll() ), (EntityPlayerMP) player); From 332f1bfd57a90912596bdec24f7d698ad39b00ce Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 2 Apr 2026 00:30:36 +0200 Subject: [PATCH 06/34] juhhh in case --- .../kamkeel/npcs/controllers/SyncController.java | 1 + .../kamkeel/npcs/controllers/sync/SyncHandler.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index eed423ea1..f38625d15 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -272,6 +272,7 @@ public class SyncController { SyncHandler handler = SyncRegistry.getHandler(syncType); if (handler == null) return; try { + handler.clientHandleUpdate(compound); handler.clientHandleUpdate(compound, id); handler.clientHandleUpdate(compound, key); } catch (Exception e) { diff --git a/src/main/java/kamkeel/npcs/controllers/sync/SyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/SyncHandler.java index ef0ce0bda..35f863c35 100644 --- a/src/main/java/kamkeel/npcs/controllers/sync/SyncHandler.java +++ b/src/main/java/kamkeel/npcs/controllers/sync/SyncHandler.java @@ -56,6 +56,20 @@ public interface SyncHandler { // No-op: type does not support individual updates } + /** + * Handle an UPDATE action on the client. + * Deserialize a single object and insert/replace it in the client-side data. + * + *

Default: no-op. Types that don't support individual updates + * use {@code syncAll()} for full reload instead.

+ * + * @param compound the serialized single object + */ + @SideOnly(Side.CLIENT) + default void clientHandleUpdate(NBTTagCompound compound) { + // No-op: type does not support individual updates + } + /** * Handle an UPDATE action on the client. * Deserialize a single object and insert/replace it in the client-side data. From b8d68f771fffa0437db13e5ba7ea45ba14ddd535 Mon Sep 17 00:00:00 2001 From: zeng-github01 <95841646+zeng-github01@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:38:04 +0800 Subject: [PATCH 07/34] refactor: disable auto-open GUI for BlockBorder and BlockNpcRedstone - Removed auto-open GUI logic upon placing BlockBorder and BlockNpcRedstone. - Resolved a race condition where the GUI would open before NBT/Permission data was fully synced from the server, causing "Permission Denied" or empty states. - Fixed a logic bug in SubGuiNpcAvailabilityDialog where 'questId' was incorrectly used instead of 'dialogId' when opening the selection picker. - This change ensures better UX as manual interaction guarantees all data is properly initialized. --- src/main/java/noppes/npcs/blocks/BlockBorder.java | 4 ---- src/main/java/noppes/npcs/blocks/BlockNpcRedstone.java | 7 ------- .../npcs/client/gui/SubGuiNpcAvailabilityDialog.java | 8 ++++---- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/main/java/noppes/npcs/blocks/BlockBorder.java b/src/main/java/noppes/npcs/blocks/BlockBorder.java index 3781b9c72..db7761b1c 100644 --- a/src/main/java/noppes/npcs/blocks/BlockBorder.java +++ b/src/main/java/noppes/npcs/blocks/BlockBorder.java @@ -67,10 +67,6 @@ public class BlockBorder extends BlockContainer { } tile.rotation = l; - - if (par5EntityLivingBase instanceof EntityPlayer && par1World.isRemote) { - CustomNpcs.proxy.openGui(x, y, z, EnumGuiType.Border, (EntityPlayer) par5EntityLivingBase); - } } private TileBorder getTile(World world, int x, int y, int z) { diff --git a/src/main/java/noppes/npcs/blocks/BlockNpcRedstone.java b/src/main/java/noppes/npcs/blocks/BlockNpcRedstone.java index e97096c72..6f89b442a 100644 --- a/src/main/java/noppes/npcs/blocks/BlockNpcRedstone.java +++ b/src/main/java/noppes/npcs/blocks/BlockNpcRedstone.java @@ -49,13 +49,6 @@ public class BlockNpcRedstone extends BlockContainer { par1World.notifyBlocksOfNeighborChange(par2, par3, par4 + 1, this); } - @Override - public void onBlockPlacedBy(World world, int i, int j, int k, EntityLivingBase entityliving, ItemStack item) { - if (entityliving instanceof EntityPlayer && world.isRemote) { - CustomNpcs.proxy.openGui(i, j, k, EnumGuiType.RedstoneBlock, (EntityPlayer) entityliving); - } - } - @Override public void onBlockDestroyedByPlayer(World par1World, int par2, int par3, int par4, int par5) { onBlockAdded(par1World, par2, par3, par4); diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcAvailabilityDialog.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcAvailabilityDialog.java index 802ede067..d3d93daa2 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcAvailabilityDialog.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcAvailabilityDialog.java @@ -112,19 +112,19 @@ public class SubGuiNpcAvailabilityDialog extends SubGuiInterface implements GuiS } if (button.id == 10) { slot = 1; - setSubGui(new GuiDialogSelection(availabitily.questId)); + setSubGui(new GuiDialogSelection(availabitily.dialogId)); } if (button.id == 11) { slot = 2; - setSubGui(new GuiDialogSelection(availabitily.quest2Id)); + setSubGui(new GuiDialogSelection(availabitily.dialog2Id)); } if (button.id == 12) { slot = 3; - setSubGui(new GuiDialogSelection(availabitily.quest3Id)); + setSubGui(new GuiDialogSelection(availabitily.dialog3Id)); } if (button.id == 13) { slot = 4; - setSubGui(new GuiDialogSelection(availabitily.quest4Id)); + setSubGui(new GuiDialogSelection(availabitily.dialog4Id)); } if (button.id == 20) { availabitily.dialogId = -1; From 0a4bc4126fee3fa6f7dc61b3fa652221965cba25 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 2 Apr 2026 04:09:20 +0200 Subject: [PATCH 08/34] Fixed massive SyncPacket failure --- .../network/packets/data/large/SyncPacket.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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 8ef77a69a..dcd940a50 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 @@ -82,9 +82,11 @@ public final class SyncPacket extends LargeAbstractPacket { buffer.writeInt(syncType.ordinal()); buffer.writeInt(enumSyncAction.ordinal()); buffer.writeInt(operationID); + buffer.writeInt(revision); + + // UPDATE/REMOVE UNIQUE FIELDS buffer.writeBoolean(hasKey); if (hasKey) ByteBufUtils.writeString(buffer, operationKey); - buffer.writeInt(revision); buffer.writeBoolean(hasNbt); if (hasNbt) ByteBufUtils.writeBigNBT(buffer, syncData); @@ -135,9 +137,16 @@ public final class SyncPacket extends LargeAbstractPacket { int syncTypeOrdinal = data.readInt(); int syncActionOrdinal = data.readInt(); operationID = data.readInt(); - if (data.readBoolean()) operationKey = ByteBufUtils.readString(data); revision = data.readInt(); - if (data.readBoolean()) syncData = ByteBufUtils.readBigNBT(data); + + EnumSyncAction action = EnumSyncAction.values()[syncActionOrdinal]; + + // These fields only exist for UPDATE/REMOVE + if (action != EnumSyncAction.RELOAD) { + if (data.readBoolean()) operationKey = ByteBufUtils.readString(data); + if (data.readBoolean()) syncData = ByteBufUtils.readBigNBT(data); + } else + syncData = ByteBufUtils.readBigNBT(data); syncType = SyncType.byOrdinal(syncTypeOrdinal); @@ -146,7 +155,6 @@ public final class SyncPacket extends LargeAbstractPacket { return; } - EnumSyncAction action = EnumSyncAction.values()[syncActionOrdinal]; handleSync(syncType, action, operationID, operationKey, revision, syncData); } From 67c6cf14d3405073ea567594ce7bba4b2963f082 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 2 Apr 2026 04:26:49 +0200 Subject: [PATCH 09/34] SyncController common SyncRegistry registration (SyncTypes must be on client & server) --- .../kamkeel/npcs/controllers/SyncController.java | 13 ++++++------- src/main/java/noppes/npcs/CustomNpcs.java | 3 +++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index f38625d15..ca6231ca7 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -80,13 +80,7 @@ public class SyncController { *

Handler registration order matches the original hard-coded * LOGIN_SYNC_TYPES array for behavioral parity.

*/ - public static void load() { - cacheEntries.clear(); - playerSyncState.clear(); - SyncRegistry.clear(); - - // Register all core handlers in deterministic order - // Order matches original LOGIN_SYNC_TYPES for login sync ordering + public static void register() { SyncRegistry.register(SyncType.FACTION, new FactionSyncHandler()); SyncRegistry.register(SyncType.DIALOG, new DialogSyncHandler()); SyncRegistry.register(SyncType.DIALOG_CATEGORY, new DialogCategorySyncHandler()); @@ -101,6 +95,11 @@ public class SyncController { SyncRegistry.register(SyncType.MAGIC_CYCLE, new MagicCycleSyncHandler()); SyncRegistry.register(SyncType.CUSTOM_ABILITY, new CustomAbilitySyncHandler()); SyncRegistry.register(SyncType.CHAINED_ABILITY, new ChainedAbilitySyncHandler()); + } + + public static void load() { + cacheEntries.clear(); + playerSyncState.clear(); // Register caches for all cached (login-sync) types // The cache supplier delegates to the handler's serializeAll() diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index d72bdf23f..480ee4f12 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -317,6 +317,9 @@ public class CustomNpcs { new AddonManager(); new AttributeController(); new MagicController(); + + SyncController.register(); + APIRegistry.Instance.register("CNPC+ API", "https://kamkeel.github.io/CustomNPC-Plus/"); } From 4c301b3c1cc9fb8865d1ad213d0d0f8c660f77c1 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 2 Apr 2026 04:29:29 +0200 Subject: [PATCH 10/34] Improved PlayerDataSyncHandler --- .../npcs/controllers/SyncController.java | 14 +++++++- .../controllers/data/profile/CNPCData.java | 2 +- .../sync/handlers/PlayerDataSyncHandler.java | 34 ++++--------------- .../noppes/npcs/ScriptPlayerEventHandler.java | 2 +- 4 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index ca6231ca7..100486761 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -122,7 +122,7 @@ public class SyncController { private static void sendPostLoginPackets(EntityPlayerMP player) { DBCAddon.instance.syncPlayer(player); - PlayerDataSyncHandler.getInstance().syncPlayerData(player, false); + PlayerDataSyncHandler.syncPlayerData(player, false); PartyInfoPacket.sendPartyData(player); ProfileSharedQuestPacket.sendToPlayer(player); @@ -210,6 +210,18 @@ public class SyncController { updateAllPlayerRevisions(type, payload.getRevision()); } + // ═══════════════════════════════════════════════════════════════════════ + // Player data sync + // ═══════════════════════════════════════════════════════════════════════ + + public static void syncPlayerData(SyncType type, NBTTagCompound compound, boolean fullData, EntityPlayerMP player) { + Map revisions = invalidateCaches(type); + int revision = getRequestedInvalidationRevision(type, revisions); + PacketHandler.Instance.sendToPlayer( + new SyncPacket(type, EnumSyncAction.UPDATE, fullData ? 1 : 0, null, revision, compound), player); + updateAllPlayerRevisions(revisions); + } + // ═══════════════════════════════════════════════════════════════════════ // Server-side dispatchers — registry-driven // ═══════════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java b/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java index 7d77317a0..ccf6ecac7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java @@ -30,7 +30,7 @@ public class CNPCData implements IProfileData { public void save(EntityPlayer player) { PlayerData customNPCData = PlayerData.get(player); customNPCData.save(); - PlayerDataSyncHandler.getInstance().syncPlayerData((EntityPlayerMP) player, false); + PlayerDataSyncHandler.syncPlayerData((EntityPlayerMP) player, false); } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerDataSyncHandler.java b/src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerDataSyncHandler.java index 2842b5cee..b92277c31 100644 --- a/src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerDataSyncHandler.java +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerDataSyncHandler.java @@ -2,12 +2,10 @@ package kamkeel.npcs.controllers.sync.handlers; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.network.PacketHandler; -import kamkeel.npcs.network.enums.EnumSyncAction; -import kamkeel.npcs.network.packets.data.large.SyncPacket; -import net.minecraft.entity.player.EntityPlayerMP; +import kamkeel.npcs.controllers.SyncController; import kamkeel.npcs.controllers.sync.SyncHandler; import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.controllers.data.PlayerData; @@ -19,21 +17,10 @@ import noppes.npcs.controllers.data.PlayerData; */ public class PlayerDataSyncHandler implements SyncHandler { - private static final PlayerDataSyncHandler INSTANCE = new PlayerDataSyncHandler(); - - public static PlayerDataSyncHandler getInstance() { - return INSTANCE; - } - - public void syncPlayerData(EntityPlayerMP player, boolean update) { + public static void syncPlayerData(EntityPlayerMP player, boolean update) { PlayerData data = PlayerData.get(player); - if (data != null) { - if (update) { - PacketHandler.Instance.sendToPlayer(new SyncPacket(SyncType.PLAYERDATA, EnumSyncAction.UPDATE, -1, data.getSyncNBT()), player); - } else { - PacketHandler.Instance.sendToPlayer(new SyncPacket(SyncType.PLAYERDATA, EnumSyncAction.RELOAD, -1, data.getSyncNBTFull()), player); - } - } + if (data != null) + SyncController.syncPlayerData(SyncType.PLAYERDATA, update ? data.getSyncNBTFull() : data.getSyncNBT(), update, player); } // ========== SERVER-SIDE ========== @@ -46,18 +33,11 @@ public class PlayerDataSyncHandler implements SyncHandler { return null; } - // ========== CLIENT-SIDE ========== - - @SideOnly(Side.CLIENT) - @Override - public void clientHandleReload(NBTTagCompound fullCompound) { - ClientCacheHandler.playerData.setSyncNBTFull(fullCompound); - } - @SideOnly(Side.CLIENT) @Override public void clientHandleUpdate(NBTTagCompound compound, int categoryId) { - ClientCacheHandler.playerData.setSyncNBT(compound); + if (categoryId == 1) ClientCacheHandler.playerData.setSyncNBTFull(compound); + else ClientCacheHandler.playerData.setSyncNBT(compound); } @Override diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index 8c1ef02c9..3e7fa07d9 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -143,7 +143,7 @@ public class ScriptPlayerEventHandler { } if (playerData.updateClient) { - PlayerDataSyncHandler.getInstance().syncPlayerData((EntityPlayerMP) player, true); + PlayerDataSyncHandler.syncPlayerData((EntityPlayerMP) player, true); playerData.updateClient = false; } From 8e8ab065074c23e48513fef171fe123c4bded30d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 3 Apr 2026 16:32:34 +0200 Subject: [PATCH 11/34] Fixed janino script handler serialization inconsistencies --- .../npcs/controllers/data/JaninoScriptHandler.java | 10 ++++++++-- src/main/java/noppes/npcs/janino/JaninoScript.java | 8 +++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java index cbe1230e0..4c9731880 100644 --- a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java @@ -56,11 +56,17 @@ public class JaninoScriptHandler> extends ScriptHandle public void readFromNBT(NBTTagCompound compound) { // JaninoScriptable's helper generics assume JaninoScript, but most of our scripts are JaninoScript. // Keep it simple and safe: treat it as raw. - script = (S) JaninoScript.readFromNBT(compound, (JaninoScript) script, (Supplier) factory); + NBTTagCompound tag = compound.getCompoundTag("Script"); + enabled = tag.getBoolean("enabled"); + script = (S) JaninoScript.readFromNBT(tag, (JaninoScript) script, (Supplier) factory); } public void writeToNBT(NBTTagCompound compound) { - JaninoScript.writeToNBT(compound, (JaninoScript) script); + NBTTagCompound tag = new NBTTagCompound(); + tag.setBoolean("enabled", enabled); + JaninoScript.writeToNBT(tag, (JaninoScript) script); + compound.setTag("Script", tag); + } // -------------------- IScriptHandler -------------------- diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index f94be8acc..afa55033c 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -544,15 +544,13 @@ public abstract class JaninoScript implements IScriptUnit { } public static > S readFromNBT(NBTTagCompound compound, S script, Supplier factory) { - if (compound.hasKey("Script")) { - if (script == null) script = factory.get(); - script.readFromNBT(compound.getCompoundTag("Script")); - } + if (script == null) script = factory.get(); + script.readFromNBT(compound); return script; } public static NBTTagCompound writeToNBT(NBTTagCompound compound, JaninoScript script) { - if (script != null) compound.setTag("Script", script.writeToNBT(new NBTTagCompound())); + if (script != null) script.writeToNBT(compound); return compound; } } From 7f63b64a66ed59734b4994891586e1d2ba1cd555 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 3 Apr 2026 16:32:56 +0200 Subject: [PATCH 12/34] Set ScriptHandler default to enabled = true --- src/main/java/noppes/npcs/controllers/data/ScriptHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java index 096f8f381..d35c6bc9c 100644 --- a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java @@ -10,7 +10,7 @@ import java.util.List; public abstract class ScriptHandler implements IScriptHandler { public List scripts = new ArrayList<>(); protected String scriptLanguage = "ECMAScript"; - protected boolean enabled = false; + protected boolean enabled = true; protected long lastInited = -1; protected boolean canRunScripts() { From 1a77728bcd9e4fe207ee2e60b46c377e743b6c3f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 3 Apr 2026 18:00:27 +0200 Subject: [PATCH 13/34] OVERHAULED IGuiData FRAMEWORK SO ALL UPDATES WIRE THROUGH SyncController (deprecated legacy setGuiData) --- .../npcs/controllers/SyncController.java | 7 ++ .../packets/data/large/GuiDataPacket.java | 15 +-- .../noppes/npcs/client/gui/util/IGuiData.java | 103 +++++++++++++++++- 3 files changed, 110 insertions(+), 15 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index 100486761..56dc377f9 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -22,6 +22,7 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.server.MinecraftServer; import noppes.npcs.LogWriter; import noppes.npcs.client.ClientCacheHandler; +import noppes.npcs.client.gui.util.IGuiData; import noppes.npcs.controllers.data.PlayerData; import java.util.HashMap; @@ -291,6 +292,9 @@ public class SyncController { } ClientCacheHandler.updateClientRevision(syncType, revision); + + // New GUI data handling approach + IGuiData.notifyUpdate(compound, key, id); } @SideOnly(Side.CLIENT) @@ -305,6 +309,9 @@ public class SyncController { } ClientCacheHandler.updateClientRevision(syncType, revision); + + // New GUI data handling approach + IGuiData.notifyRemove(key, id); } // ═══════════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/network/packets/data/large/GuiDataPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/large/GuiDataPacket.java index 631e32bf2..784d0fe1c 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/large/GuiDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/large/GuiDataPacket.java @@ -9,13 +9,9 @@ import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumDataPacket; import kamkeel.npcs.util.ByteBufUtils; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiScreen; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.client.gui.util.GuiContainerNPCInterface; -import noppes.npcs.client.gui.util.GuiNPCInterface; import noppes.npcs.client.gui.util.IGuiData; import java.io.IOException; @@ -59,15 +55,6 @@ public final class GuiDataPacket extends LargeAbstractPacket { @SideOnly(Side.CLIENT) @Override protected void handleCompleteData(ByteBuf data, EntityPlayer player) throws IOException { - GuiScreen gui = Minecraft.getMinecraft().currentScreen; - if (gui instanceof GuiNPCInterface && ((GuiNPCInterface) gui).hasSubGui()) { - gui = ((GuiNPCInterface) gui).getSubGui(); - } else if (gui instanceof GuiContainerNPCInterface && ((GuiContainerNPCInterface) gui).hasSubGui()) { - gui = ((GuiContainerNPCInterface) gui).getSubGui(); - } - if (gui instanceof IGuiData) { - NBTTagCompound nbt = ByteBufUtils.readBigNBT(data); - ((IGuiData) gui).setGuiData(nbt); - } + IGuiData.legacySetGuiData(ByteBufUtils.readBigNBT(data)); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/IGuiData.java b/src/main/java/noppes/npcs/client/gui/util/IGuiData.java index 36ba683ac..9f1796c8a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/IGuiData.java +++ b/src/main/java/noppes/npcs/client/gui/util/IGuiData.java @@ -1,7 +1,108 @@ package noppes.npcs.client.gui.util; +import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.sync.SyncHandler; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; import net.minecraft.nbt.NBTTagCompound; +/** + * OVERHAULED IGuiData FRAMEWORK! + * Based on the new {@link SyncController} and {@link SyncHandler} infrastructure + */ public interface IGuiData { - public void setGuiData(NBTTagCompound compound); + + /** + * Legacy data setter + * @Deprecated Use {@link #onDataUpdated(NBTTagCompound)} or {@link #onDataRemoved(String)} instead + */ + default void setGuiData(NBTTagCompound compound) { + + } + + // ═══════════════════════════════════════════════════════════════════════ + // USE THESE !!! + // ═══════════════════════════════════════════════════════════════════════ + + /** NOTE: + * No need for full object dataset onDataReload on GUI open, as these datasets are + * always freshly synced to their client's controllers thanks to {@link SyncController} + */ + + /** + * When a controller's object gets saved on server side, the updated object is synced to the client + * @param compound the compound of the updated object + */ + default void onDataUpdated(NBTTagCompound compound) { + } + + /** + * When a controller's object gets saved on server side, the updated object is synced to the client + * @param key key of updated object + * @param compound compound of updated object + */ + default void onDataUpdated(NBTTagCompound compound, String key) { + } + + /** + * When a controller's object gets saved on server side, the updated object is synced to the client + * @param id id of updated object + * @param compound compound of updated object + */ + default void onDataUpdated(NBTTagCompound compound, int id) { + } + + /** + * When a controller's object gets deleted on server side, the deleted object is removed from the client + * @param key key of removed object + */ + default void onDataRemoved(String key) { + } + + /** + * When a controller's object gets deleted on server side, the deleted object is removed from the client + * @param id id of removed object + */ + default void onDataRemoved(int id) { + } + + // ═══════════════════════════════════════════════════════════════════════ + // Static dispatchers + // ═══════════════════════════════════════════════════════════════════════ + + static void legacySetGuiData(NBTTagCompound nbt) { + GuiScreen gui = activeGui(); + if (gui instanceof IGuiData) + ((IGuiData) gui).setGuiData(nbt); + } + + static void notifyUpdate(NBTTagCompound nbt, String key, int id) { + GuiScreen gui = activeGui(); + if (gui instanceof IGuiData) { + ((IGuiData) gui).onDataUpdated(nbt); + ((IGuiData) gui).onDataUpdated(nbt, key); + ((IGuiData) gui).onDataUpdated(nbt, id); + } + } + + static void notifyRemove(String key, int id) { + GuiScreen gui = activeGui(); + if (gui instanceof IGuiData) { + ((IGuiData) gui).onDataRemoved(key); + ((IGuiData) gui).onDataRemoved(id); + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // Active Gui fetcher + // ═══════════════════════════════════════════════════════════════════════ + + static GuiScreen activeGui() { + GuiScreen gui = Minecraft.getMinecraft().currentScreen; + if (gui instanceof GuiNPCInterface && ((GuiNPCInterface) gui).hasSubGui()) + return ((GuiNPCInterface) gui).getSubGui(); + if (gui instanceof GuiContainerNPCInterface && ((GuiContainerNPCInterface) gui).hasSubGui()) + return ((GuiContainerNPCInterface) gui).getSubGui(); + return gui; + } } From 4a7c7b09c7be8fbf61fc22c96d6734f3b0c68f74 Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:00:54 -0400 Subject: [PATCH 14/34] - Fix Counter infinite recursion - Tags Get Packet - Cooldowns Resetting on Dimension Change / Teleport --- .../data/ability/type/AbilityCounter.java | 56 ++++++++++++++++++- .../category/CategoryItemsRequestPacket.java | 2 +- .../packets/request/tags/TagsGetPacket.java | 5 -- .../controllers/data/PlayerAbilityData.java | 10 ++-- 4 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java index f03543028..64fb10743 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java @@ -3,14 +3,21 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.addon.DBCAddon; +import noppes.npcs.AbstractDataAbilities; import noppes.npcs.controllers.AnimationController; +import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.enums.UserType; import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.DamageSource; import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.api.ability.type.IAbilityCounter; import noppes.npcs.client.gui.builder.FieldDef; +import noppes.npcs.entity.EntityNPCInterface; import java.util.Arrays; import java.util.List; @@ -25,6 +32,11 @@ public class AbilityCounter extends AbilityDefend implements IAbilityCounter { private int counterAnimationId = -1; private String counterAnimationName = "Ability_Guard_Counter"; + // True only while this counter is synchronously dispatching its retaliation damage. + // Another counter that is hit by that retaliation inspects this flag on the source + // entity's active counter to avoid reacting and creating infinite mutual retaliation. + private transient boolean retaliating; + public AbilityCounter() { this.typeId = "ability.cnpc.counter"; this.name = "Counter"; @@ -60,6 +72,35 @@ public class AbilityCounter extends AbilityDefend implements IAbilityCounter { // DEFEND HOOKS // ═══════════════════════════════════════════════════════════════════ + @Override + protected boolean isValidDamageSource(DamageSource source) { + if (!super.isValidDamageSource(source)) { + return false; + } + // Break counter-vs-counter recursion: if the hit is itself a retaliation + // from another counter that is mid-dispatch, don't counter it back. + Entity sourceEntity = source.getEntity(); + AbstractDataAbilities abilities = getAbilitiesOf(sourceEntity); + if (abilities != null) { + Ability current = abilities.getCurrentAbility(); + if (current instanceof AbilityCounter && ((AbilityCounter) current).retaliating) { + return false; + } + } + return true; + } + + private static AbstractDataAbilities getAbilitiesOf(Entity entity) { + if (entity instanceof EntityPlayer) { + PlayerData pData = PlayerData.get((EntityPlayer) entity); + return pData != null ? pData.abilityData : null; + } + if (entity instanceof EntityNPCInterface) { + return ((EntityNPCInterface) entity).abilities; + } + return null; + } + @Override protected float performDefend(EntityLivingBase attacker, float amount) { // For PERCENT mode with DBC, use the full DBC-calculated damage, not vanilla base @@ -78,13 +119,24 @@ public class AbilityCounter extends AbilityDefend implements IAbilityCounter { // Counter-attack through the standard ability damage pipeline. // DBCAbilityExtender handles DBC stat routing (ignore flags, scaling, etc.) automatically. - if (caster != null && attacker.isEntityAlive() && damage > 0) { - applyAbilityDamage(caster, attacker, damage, 0); + if (caster != null && attacker != null && attacker.isEntityAlive() && damage > 0) { + retaliating = true; + try { + applyAbilityDamage(caster, attacker, damage, 0); + } finally { + retaliating = false; + } } return 0; } + @Override + public void reset() { + super.reset(); + retaliating = false; + } + // ═══════════════════════════════════════════════════════════════════ // OVERRIDES // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryItemsRequestPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryItemsRequestPacket.java index 9e06ab938..fef2a0f2e 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryItemsRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryItemsRequestPacket.java @@ -73,7 +73,7 @@ public final class CategoryItemsRequestPacket extends AbstractPacket { // Send tag map for the items HashMap> tagMap = getTagMapByCat(type, categoryId); - if (tagMap != null && !tagMap.isEmpty()) { + if (tagMap != null) { TagController.sendCategoryTagMap((EntityPlayerMP) player, tagMap); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/tags/TagsGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/tags/TagsGetPacket.java index 4414131f1..85fe8e5c3 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/tags/TagsGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/tags/TagsGetPacket.java @@ -6,8 +6,6 @@ import io.netty.buffer.ByteBuf; import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; -import kamkeel.npcs.network.PacketUtil; -import kamkeel.npcs.network.enums.EnumItemPacketType; import kamkeel.npcs.network.enums.EnumRequestPacket; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; @@ -38,9 +36,6 @@ public final class TagsGetPacket extends AbstractPacket { @Override public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { - if (!PacketUtil.verifyItemPacket(packetName, player, EnumItemPacketType.WAND, EnumItemPacketType.CLONER)) - return; - NoppesUtilServer.sendTagDataAll((EntityPlayerMP) player); } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 8b444cbf2..c979d5641 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -773,21 +773,21 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA } /** - * Fully reset ability state when changing dimensions. - * Clears cooldowns so the player can immediately use abilities in the new dimension. + * Fully reset transient ability execution state when changing dimensions. + * Cooldowns are preserved so dimension changes cannot bypass them. * Keeps active toggles (dimension change shouldn't reset ongoing effects). */ public void resetOnDimensionChange() { trackingAbilityPosition = false; - onEntityReconstructed(true, false); + onEntityReconstructed(false, false); } /** * Reset ability state when a large-distance teleport is detected during execution. - * Same behavior as dimension change — silent cleanup, clear cooldowns, keep toggles. + * Same behavior as dimension change: silent cleanup, preserve cooldowns, keep toggles. */ public void resetOnTeleport() { - onEntityReconstructed(true, false); + onEntityReconstructed(false, false); } /** From 322626015d309689daaac9c1c1053bb9ef346a33 Mon Sep 17 00:00:00 2001 From: postyizhan <185839426@qq.com> Date: Sun, 2 Aug 2026 22:32:00 +0800 Subject: [PATCH 15/34] fix: adapt quest tracker height to content --- .../client/gui/hud/QuestTrackingComponent.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/hud/QuestTrackingComponent.java b/src/main/java/noppes/npcs/client/gui/hud/QuestTrackingComponent.java index ee6a6fe51..dc32caa16 100644 --- a/src/main/java/noppes/npcs/client/gui/hud/QuestTrackingComponent.java +++ b/src/main/java/noppes/npcs/client/gui/hud/QuestTrackingComponent.java @@ -39,6 +39,7 @@ public class QuestTrackingComponent extends HudComponent { if (turnIn != null && !turnIn.isEmpty()) { turnInLines.add(convertColorCodes(turnIn)); } + updateOverlayHeight(); } /** @@ -239,6 +240,19 @@ public class QuestTrackingComponent extends HudComponent { } // ---------- Helper rendering methods ----------- (methods unchanged) ... + private void updateOverlayHeight() { + int lineHeight = mc.fontRenderer.FONT_HEIGHT + 4; + int contentHeight = 5; + contentHeight += questTitleLines.size() * lineHeight + 8; + contentHeight += questCategoryLines.size() * lineHeight + 8; + contentHeight += objectiveLines.size() * lineHeight; + if (!turnInLines.isEmpty()) { + contentHeight += 8; + } + contentHeight += turnInLines.size() * lineHeight; + overlayHeight = contentHeight; + } + private int renderTextBlock(List lines, int startY, int align, int color) { int y = startY; FontRenderer font = mc.fontRenderer; From 0829b6285f2f89a097e0834964511d28e1135ae0 Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 02:17:28 -0400 Subject: [PATCH 16/34] Update EntityNPCInterface.java --- src/main/java/noppes/npcs/entity/EntityNPCInterface.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 77927d4b1..ae4aba781 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -301,7 +301,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti @Override public boolean getLeashed() { - return false; //Prevents npcs from being leashed + return false; // Prevents npcs from being leashed } @Override From 5aa843966ac1350718dd820a85f62efac3bac002 Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:19:52 -0400 Subject: [PATCH 17/34] Fix divide by zero clicking the profiles scrollbar The scrollbar hit test was pure geometry, so clicking its column before the profile list had loaded - or on any list short enough not to scroll - divided by a maxScrollY of zero. The draw path already guarded on the same value. --- .../noppes/npcs/client/gui/player/inventory/GuiProfiles.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java index f9e627fe7..56777ca6f 100644 --- a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java +++ b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java @@ -429,6 +429,9 @@ public class GuiProfiles extends GuiCNPCInventory implements ISubGuiListener, IC } private boolean isMouseOverScrollbar(int mouseX, int mouseY) { + if (maxScrollY <= 0) + return false; + int scrollbarX = guiLeft + xSize + 30; int scrollbarY = guiTop + 27; int scrollbarHeight = ySize - 35; From 17f61de57a631cce06843e8185d021120d24d68e Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:20:34 -0400 Subject: [PATCH 18/34] Fix editor StackOverflow on self-referential generic bounds fromGenericType expanded a type variable bound, then expanded the bound''s type arguments, which led straight back to the same variable. Any enum reaches it through Enum>, so typing ability.getAllowedBy() crashed the client. Track the variables being expanded and emit repeats unbounded. --- .../script/interpreter/type/TypeInfo.java | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index dcac25c99..df406ebba 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -325,78 +325,94 @@ public class TypeInfo { * @return A TypeInfo preserving the generic structure, or null if type is null */ public static TypeInfo fromGenericType(Type type) { + return fromGenericType(type, new HashSet>()); + } + + /** + * @param resolving type variables whose bounds are currently being expanded, so a + * self-referential bound such as {@code E extends Enum} terminates + */ + private static TypeInfo fromGenericType(Type type, Set> resolving) { if (type == null) return null; - + if (type instanceof Class) { // Plain class - no generic info return fromClass((Class) type); } - + if (type instanceof ParameterizedType) { // Generic type like List ParameterizedType paramType = (ParameterizedType) type; Type rawType = paramType.getRawType(); - + if (!(rawType instanceof Class)) { // Edge case - should not happen normally - return rawType != null ? fromGenericType(rawType) : null; + return rawType != null ? fromGenericType(rawType, resolving) : null; } - + // Get the raw type as TypeInfo TypeInfo rawTypeInfo = fromClass((Class) rawType); - + // Recursively convert type arguments Type[] typeArgs = paramType.getActualTypeArguments(); List appliedArgs = new ArrayList<>(typeArgs.length); for (Type arg : typeArgs) { - TypeInfo argInfo = fromGenericType(arg); + TypeInfo argInfo = fromGenericType(arg, resolving); if (argInfo != null) { appliedArgs.add(argInfo); } } - + // Return a parameterized TypeInfo if (!appliedArgs.isEmpty()) { return rawTypeInfo.parameterize(appliedArgs); } return rawTypeInfo; } - + if (type instanceof GenericArrayType) { // Array of generic type, e.g. T[] or List[] GenericArrayType arrayType = (GenericArrayType) type; - TypeInfo elementType = fromGenericType(arrayType.getGenericComponentType()); + TypeInfo elementType = fromGenericType(arrayType.getGenericComponentType(), resolving); if (elementType != null) { return arrayOf(elementType); } return fromClass(Object[].class); } - + if (type instanceof TypeVariable) { TypeVariable typeVar = (TypeVariable) type; String varName = typeVar.getName(); - // Check for bounds - if bounded, create typeParameter with bound - Type[] bounds = typeVar.getBounds(); - if (bounds != null && bounds.length > 0 && bounds[0] != Object.class) { - return TypeInfo.typeParameter(varName, fromGenericType(bounds[0])); + // Already expanding this variable - emit it unbounded rather than recursing forever + if (!resolving.add(typeVar)) { + return TypeInfo.typeParameter(varName); + } + try { + // Check for bounds - if bounded, create typeParameter with bound + Type[] bounds = typeVar.getBounds(); + if (bounds != null && bounds.length > 0 && bounds[0] != Object.class) { + return TypeInfo.typeParameter(varName, fromGenericType(bounds[0], resolving)); + } + return TypeInfo.typeParameter(varName); + } finally { + resolving.remove(typeVar); } - return TypeInfo.typeParameter(varName); } - + if (type instanceof WildcardType) { // Wildcard like ? or ? extends Number or ? super Integer WildcardType wildcard = (WildcardType) type; Type[] upperBounds = wildcard.getUpperBounds(); - + // Use the upper bound if available (most useful for ? extends T) if (upperBounds.length > 0 && upperBounds[0] != Object.class) { - return fromGenericType(upperBounds[0]); + return fromGenericType(upperBounds[0], resolving); } - + // For ? super T or plain ?, just use Object return fromClass(Object.class); } - + // Unknown type - shouldn't happen in practice return null; } From 68b5191db6790651ebeeaabdbc5761d9e37ff0e0 Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:21:46 -0400 Subject: [PATCH 19/34] Fix trader edits being lost on an existing linked market Closing the setup GUI saved the market from the server''s stale role and then re-read that market over the edits it had just applied, so stock and currency cost changes never survived. The role now saves first and the market is written from it, and relinking to another market no longer pushes pre-link data over it. --- .../network/packets/request/role/RoleSavePacket.java | 9 ++++++++- .../npcs/client/gui/roles/GuiNpcTraderSetup.java | 10 +++++++++- src/main/java/noppes/npcs/roles/RoleTrader.java | 12 ++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/main/java/kamkeel/npcs/network/packets/request/role/RoleSavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/role/RoleSavePacket.java index d63f2603c..21d0f2667 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/role/RoleSavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/role/RoleSavePacket.java @@ -14,6 +14,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.roles.RoleTrader; import java.io.IOException; @@ -63,7 +64,13 @@ public final class RoleSavePacket extends AbstractPacket { if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) return; - npc.roleInterface.readFromNBT(ByteBufUtils.readNBT(in)); + NBTTagCompound roleCompound = ByteBufUtils.readNBT(in); + if (npc.roleInterface instanceof RoleTrader) { + // An admin edit wins over the linked market; the market is written from it afterwards. + ((RoleTrader) npc.roleInterface).readFromNBT(roleCompound, false); + } else { + npc.roleInterface.readFromNBT(roleCompound); + } npc.updateClient = true; } } diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java index 7b54518e3..1e5aab8d1 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java +++ b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java @@ -21,6 +21,7 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex private final ResourceLocation slot = new ResourceLocation("customnpcs", "textures/gui/slot.png"); private RoleTrader role; + private boolean marketRelinked = false; // Currency cost textfield IDs start at 100 (100-117 for slots 0-17) private static final int CURRENCY_FIELD_ID_START = 100; @@ -116,8 +117,14 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex @Override public void save() { - PacketClient.sendClient(new TraderMarketSavePacket(role.marketName, false)); + // Linking to a different market already replaced the server's role with that market's + // data. Pushing this GUI's pre-link state back would overwrite the market being linked to. + if (marketRelinked) + return; + + // Save the role first so the market is written from the edited data, not the stale data. PacketClient.sendClient(new RoleSavePacket(role.writeToNBT(new NBTTagCompound()))); + PacketClient.sendClient(new TraderMarketSavePacket(role.marketName, false)); } @Override @@ -129,6 +136,7 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex String name = guiNpcTextField.getText(); if (!name.equalsIgnoreCase(role.marketName)) { role.marketName = name; + marketRelinked = true; PacketClient.sendClient(new TraderMarketSavePacket(role.marketName, true)); } return; diff --git a/src/main/java/noppes/npcs/roles/RoleTrader.java b/src/main/java/noppes/npcs/roles/RoleTrader.java index 6aa69d357..717754abf 100644 --- a/src/main/java/noppes/npcs/roles/RoleTrader.java +++ b/src/main/java/noppes/npcs/roles/RoleTrader.java @@ -94,6 +94,15 @@ public class RoleTrader extends RoleInterface { @Override public void readFromNBT(NBTTagCompound nbttagcompound) { + readFromNBT(nbttagcompound, true); + } + + /** + * @param loadFromMarket pull the linked market's stored data over the compound just read. + * False when the compound is an admin edit, which is authoritative and + * would otherwise be overwritten by the market it is about to be saved to. + */ + public void readFromNBT(NBTTagCompound nbttagcompound, boolean loadFromMarket) { String oldMarket = marketName; marketName = nbttagcompound.getString("TraderMarket"); readNBT(nbttagcompound); @@ -101,6 +110,9 @@ public class RoleTrader extends RoleInterface { // Register with MarketRegistry for linked market sync MarketRegistry.updateTraderMarket(oldMarket, marketName, this); + if (!loadFromMarket) + return; + try { Market.getMarket(this, marketName); } catch (Exception ex) { From aaf6734bd99146c17f208e63a10c0605fde29336 Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:22:56 -0400 Subject: [PATCH 20/34] Take deleted abilities away from players properly Deleting a custom ability only cleared the unlocked list. An active toggle kept a direct reference and went on ticking, stale hotbar slots survived, and the client was never resynced so it still showed and could select the ability. Selecting a key the player no longer has now deselects instead of firing whatever was previously selected. --- .../npcs/controllers/AbilityController.java | 18 ++++++++++++++++++ .../ability/AbilityHotbarSelectPacket.java | 7 +++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/AbilityController.java index c352119dd..6c5810188 100644 --- a/src/main/java/kamkeel/npcs/controllers/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/AbilityController.java @@ -45,7 +45,9 @@ import net.minecraftforge.common.util.Constants; import noppes.npcs.controllers.data.AbilityScript; import noppes.npcs.controllers.data.ChainedAbilityScript; import net.minecraft.entity.EntityLivingBase; +import kamkeel.npcs.controllers.sync.handlers.PlayerAbilitySyncHelper; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; @@ -470,9 +472,25 @@ public class AbilityController implements IAbilityHandler { pData.abilityData.lockAbility(name); changed = true; } + + // An active toggle holds the resolved Ability directly and keeps ticking it, + // so drop it without firing onToggle against an unregistered ability. + if (uuid != null && !uuid.isEmpty()) { + pData.abilityData.setToggleEntryDirect(uuid, 0); + } + pData.abilityData.setToggleEntryDirect(name, 0); + + if (pData.hotbarData != null) { + pData.hotbarData.validateSlots(); + } + if (changed) { pData.save(); } + + if (pData.player instanceof EntityPlayerMP) { + PlayerAbilitySyncHelper.syncAbilities((EntityPlayerMP) pData.player); + } } } } diff --git a/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSelectPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSelectPacket.java index ba97769a0..87affdc1b 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSelectPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSelectPacket.java @@ -50,10 +50,9 @@ public final class AbilityHotbarSelectPacket extends AbstractPacket { // Deselect data.abilityData.setSelectedIndex(-1); } else { - int index = data.abilityData.getUnlockedAbilityList().indexOf(key); - if (index >= 0) { - data.abilityData.setSelectedIndex(index); - } + // A key the player no longer has deselects rather than leaving the previous + // selection in place, which would fire a different ability than the one shown. + data.abilityData.setSelectedIndex(data.abilityData.getUnlockedAbilityList().indexOf(key)); } data.save(); } From 3565c66cf89ab3f604cff9f5ea783c52aec4dd83 Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:24:19 -0400 Subject: [PATCH 21/34] Keep ability cooldowns across logout Dimension change and teleport were already fixed, but disconnect still cleared cooldowns and login cleared them again, and they were never written to NBT - so relogging reset every cooldown. They are absolute world-time deadlines, so they now persist and are restored on load. --- .../controllers/data/PlayerAbilityData.java | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index c979d5641..1da16a3af 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -832,9 +832,8 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA currentTarget = null; lastAbilityActivationTime = -1; - // Clear cooldowns (fresh start after relog) - cooldownEndTime = 0; - resetAllPerAbilityCooldowns(); + // Cooldowns are absolute world-time deadlines and are restored from NBT, so relogging + // must not clear them or it becomes a way to bypass them. interruptCooldownRolled = false; // Reset sync tracking so next sync sends fresh state @@ -852,9 +851,10 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA if (currentAbility != null && currentAbility.isExecuting()) { interruptCurrentAbility(null, 0); } - // Clear any remaining transient state (cooldowns, chains, locks) + // Clear any remaining transient state (chains, locks) // Don't clear toggles — they were already saved to NBT by this point - onEntityReconstructed(true, false); + // Don't clear cooldowns — they persist across sessions + onEntityReconstructed(false, false); } // ═══════════════════════════════════════════════════════════════════ @@ -1048,6 +1048,22 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA toggleList.appendTag(toggleNbt); } compound.setTag("ActiveToggles", toggleList); + + // Cooldowns are absolute world-time deadlines, so they stay meaningful across sessions + NBTTagCompound cooldowns = new NBTTagCompound(); + cooldowns.setLong("GlobalEnd", cooldownEndTime); + cooldowns.setInteger("GlobalDuration", globalCooldownDuration); + NBTTagList perAbilityList = new NBTTagList(); + for (Map.Entry entry : perAbilityCooldownEndTimes.entrySet()) { + NBTTagCompound cooldownNbt = new NBTTagCompound(); + cooldownNbt.setString("Key", entry.getKey()); + cooldownNbt.setLong("End", entry.getValue()); + Integer duration = perAbilityCooldownDurations.get(entry.getKey()); + cooldownNbt.setInteger("Duration", duration == null ? 0 : duration); + perAbilityList.appendTag(cooldownNbt); + } + cooldowns.setTag("PerAbility", perAbilityList); + compound.setTag("AbilityCooldowns", cooldowns); } public void readFromNBT(NBTTagCompound compound) { @@ -1082,6 +1098,24 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA setToggleEntryDirect(key, state); } } + + if (compound.hasKey("AbilityCooldowns")) { + cooldownEndTime = 0; + globalCooldownDuration = 0; + resetAllPerAbilityCooldowns(); + + NBTTagCompound cooldowns = compound.getCompoundTag("AbilityCooldowns"); + cooldownEndTime = cooldowns.getLong("GlobalEnd"); + globalCooldownDuration = cooldowns.getInteger("GlobalDuration"); + NBTTagList perAbilityList = cooldowns.getTagList("PerAbility", 10); // 10 = TAG_COMPOUND + for (int i = 0; i < perAbilityList.tagCount(); i++) { + NBTTagCompound entry = perAbilityList.getCompoundTagAt(i); + String key = entry.getString("Key"); + if (key == null || key.isEmpty()) continue; + perAbilityCooldownEndTimes.put(key, entry.getLong("End")); + perAbilityCooldownDurations.put(key, entry.getInteger("Duration")); + } + } } /** From abd1c7cbebb0961fc11d2b1c1e6f76ad0fb35383 Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:24:59 -0400 Subject: [PATCH 22/34] Make per-ability cooldowns work on NPCs An NPC rolled both clocks and folded the ability''s cooldown into the global one as well, so the global timer always outlasted the per-ability entry and the setting could never reject anything. The global timer now only paces the NPC when the ability tracks its own cooldown. --- src/main/java/noppes/npcs/DataAbilities.java | 23 ++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 7772f6e22..3888df2d5 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -188,29 +188,34 @@ public class DataAbilities extends AbstractDataAbilities { if (maxCooldown > minCooldown) { baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1); } - cooldownEndTime = npc.worldObj.getTotalWorldTime() + baseCooldown + ability.getCooldownTicks(); + long now = npc.worldObj.getTotalWorldTime(); - // Additionally set per-ability cooldown if enabled if (ability.isPerAbilityCooldown() && ability.getCooldownTicks() > 0) { - long endTime = npc.worldObj.getTotalWorldTime() + ability.getCooldownTicks(); - setPerAbilityCooldown(ability.getName(), endTime, ability.getCooldownTicks()); + // The ability's own cooldown is tracked against the ability, so the global timer + // only paces how often the NPC acts. Folding the ability's cooldown into it as + // well would always outlast the per-ability entry and make it unreachable. + cooldownEndTime = now + baseCooldown; + setPerAbilityCooldown(ability.getName(), now + ability.getCooldownTicks(), ability.getCooldownTicks()); + } else { + cooldownEndTime = now + baseCooldown + ability.getCooldownTicks(); } } @Override protected void rollChainCooldown(ChainedAbility chain) { + long now = npc.worldObj.getTotalWorldTime(); + boolean perAbility = chain.isPerAbilityCooldown() && chain.getCooldownTicks() > 0; + if (!chain.isIgnoreCooldown()) { int baseCooldown = minCooldown; if (maxCooldown > minCooldown) { baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1); } - cooldownEndTime = npc.worldObj.getTotalWorldTime() + baseCooldown + chain.getCooldownTicks(); + cooldownEndTime = perAbility ? now + baseCooldown : now + baseCooldown + chain.getCooldownTicks(); } - // Additionally set per-ability cooldown if enabled - if (chain.isPerAbilityCooldown() && chain.getCooldownTicks() > 0) { - long endTime = npc.worldObj.getTotalWorldTime() + chain.getCooldownTicks(); - setPerAbilityCooldown(chain.getName(), endTime, chain.getCooldownTicks()); + if (perAbility) { + setPerAbilityCooldown(chain.getName(), now + chain.getCooldownTicks(), chain.getCooldownTicks()); } } From 6af66b8883d10b6a74f51be9e63925c76c6ea7a1 Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:25:51 -0400 Subject: [PATCH 23/34] Fix NPCs getting stuck after using one ability The terminal DAZED to IDLE transition was swallowed before the completion switch ran, so the finished ability stayed set as the current one and the NPC never started another until it died and reset. Guard abilities compounded it: NPCs reuse a single ability instance and onExecute left pendingCompletion set, ending every later activation on its first tick. --- .../controllers/data/ability/type/AbilityDefend.java | 6 ++++++ src/main/java/noppes/npcs/AbstractDataAbilities.java | 10 +++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java index 376c4ad4c..4826daee4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java @@ -74,6 +74,12 @@ public abstract class AbilityDefend extends Ability implements IAbilityDefend { hitCount = 0; lastAttacker = null; lastDamageTaken = 0.0f; + // NPCs reuse one ability instance, so every transient field has to be cleared here. + // A leftover pendingCompletion would end the next activation on its first tick. + pendingDefendAnimation = null; + defendAnimEndTick = -1; + lastDefendTick = -1; + pendingCompletion = false; } @Override diff --git a/src/main/java/noppes/npcs/AbstractDataAbilities.java b/src/main/java/noppes/npcs/AbstractDataAbilities.java index 244ed91d1..12f1fd793 100644 --- a/src/main/java/noppes/npcs/AbstractDataAbilities.java +++ b/src/main/java/noppes/npcs/AbstractDataAbilities.java @@ -786,7 +786,15 @@ public abstract class AbstractDataAbilities { fireTickEvent(currentAbility, target); // Script may have completed/cancelled the ability during the tick event - if (currentAbility == null || !currentAbility.isExecuting()) return; + if (currentAbility == null) return; + if (!currentAbility.isExecuting()) { + // tick() itself can end the ability (DAZED -> IDLE). Run its completion here, + // otherwise it stays the current ability forever and nothing else can start. + if (phaseChanged && currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(target); + } + return; + } // Fire extender tick hook (e.g., per-tick resource drain) if (!AbilityController.Instance.fireOnAbilityTick(currentAbility, entity, target, From 9f4334a8031d3051111d4d2158d78a3652b163d1 Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:26:32 -0400 Subject: [PATCH 24/34] Line the monkey tail up with the player model heightFactor did not fully cancel ModelTail''s anchor, leaving both the wrapped and plain monkey tails two pixels below where JBRA renders them on players. --- .../noppes/npcs/client/model/part/tails/ModelMonkeyTail.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/model/part/tails/ModelMonkeyTail.java b/src/main/java/noppes/npcs/client/model/part/tails/ModelMonkeyTail.java index fae556349..347b5ebfa 100644 --- a/src/main/java/noppes/npcs/client/model/part/tails/ModelMonkeyTail.java +++ b/src/main/java/noppes/npcs/client/model/part/tails/ModelMonkeyTail.java @@ -34,7 +34,9 @@ public class ModelMonkeyTail extends ModelRenderer { public ModelMonkeyTail(ModelBiped base) { super(base); - float heightFactor = -9.0f; + // Cancels ModelTail's rotationPointY of 11 so the boxes land where JBRA puts them + // on players: plain tail at 10, wrapped tail at 0. + float heightFactor = -11.0f; (this.monkey_large = new ModelRenderer(base, 38, 54)).addBox(-2.0f, -2.0f, 0.0f, 4, 4, 6); this.setRotation(this.monkey_large, -0.5235988f, 0.0f, 0.0f); From 7cafa4f67720621f8418942be01823f876ffb12b Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:28:49 -0400 Subject: [PATCH 25/34] Make anchored lasers and rotation lock affect ability aim - Anchored lasers re-derived their direction straight at the target every tick, discarding the rate-limited look vector, so track speed did nothing and the beam was undodgeable. They now converge on the point the owner is aiming at. - Rotation-locked NPCs aimed energy projectiles and hit-scan projectiles from the target''s position, so the lock was purely visual. Both now respect the frozen heading. --- .../data/ability/type/AbilityProjectile.java | 9 +++++++ .../type/energy/AbilityEnergyProjectile.java | 7 ++++- .../npcs/entity/EntityAbilityLaser.java | 27 ++++++++++--------- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 104209eeb..3caabc678 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -97,6 +97,15 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { dz /= len; } + // A rotation-locked NPC cannot turn to follow the target, so it can only hit what its + // frozen heading still covers - the same cone the player hit scan uses. + if (isRotationLockedForCurrentPhase()) { + Vec3 look = caster.getLookVec(); + if (look != null && (dx * look.xCoord + dy * look.yCoord + dz * look.zCoord) < 0.95) { + return; + } + } + applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz); world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java index 7e7986d20..723781d36 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java @@ -248,7 +248,12 @@ public abstract class AbilityEnergyProjectile * NPCs use their resolved target so launch math can track the target immediately. */ protected EntityLivingBase getLaunchTarget(EntityLivingBase caster, EntityLivingBase target) { - return isPlayerCaster(caster) ? null : target; + if (isPlayerCaster(caster)) return null; + // A rotation-locked NPC cannot turn, so launching from the target's position would let + // it fire a perfectly aimed shot regardless of where it is facing. Fall back to the + // look vector, which reflects the frozen heading. + if (isRotationLockedForCurrentPhase()) return null; + return target; } /** diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 546457f4d..f8f5d44f5 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -264,18 +264,21 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { Vec3 anchorPos = AnchorPointHelper.calculateAnchorPosition(livingOwner, anchorData); setPosition(anchorPos.xCoord, anchorPos.yCoord, anchorPos.zCoord); - Entity targetEntity = getTargetEntity(); - if (targetEntity instanceof EntityLivingBase && targetEntity.isEntityAlive()) { - EntityLivingBase target = (EntityLivingBase) targetEntity; - double dx = target.posX - anchorPos.xCoord; - double dy = (target.posY + target.getEyeHeight() - 0.4) - anchorPos.yCoord; - double dz = target.posZ - anchorPos.zCoord; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - if (len > 0.0001) { - dirX = dx / len; - dirY = dy / len; - dirZ = dz / len; - } + // Converge on the point the owner is aiming at, not on the target itself. Aiming + // straight at the target would re-derive the direction every tick and ignore the + // rotation the owner is actually allowed to reach, which is what track speed and + // rotation lock constrain. + double aimX = livingOwner.posX + dirX * maxLength; + double aimY = livingOwner.posY + livingOwner.getEyeHeight() + dirY * maxLength; + double aimZ = livingOwner.posZ + dirZ * maxLength; + double dx = aimX - anchorPos.xCoord; + double dy = aimY - anchorPos.yCoord; + double dz = aimZ - anchorPos.zCoord; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0.0001) { + dirX = dx / len; + dirY = dy / len; + dirZ = dz / len; } } else { Vec3 direction = Vec3.createVectorHelper(dirX, dirY, dirZ); From 6477ede022abccc21e432d1e0aba0b3150c31848 Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:31:38 -0400 Subject: [PATCH 26/34] Make hazard radius and barrier duration accurate - Hazards only checked who was inside once per damage interval, so the boundary where damage started and stopped drifted by however far an entity moved in between - several blocks at sprint speed. Membership is now checked every tick and damage accrues per entity from the time it actually spends inside. - Barriers counted elapsed ticks instead of holding a deadline, so every tick the entity missed extended it past its configured duration. They now expire on an absolute world time like the other energy entities. --- .../npcs/entity/EntityAbilityZone.java | 34 +++++++++++++------ .../npcs/entity/EntityEnergyBarrier.java | 19 +++++++---- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java index 7913386c6..2a4632e77 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java @@ -23,8 +23,10 @@ import noppes.npcs.NpcDamageSource; import noppes.npcs.entity.EntityNPCInterface; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -170,8 +172,9 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD private transient Set triggeredEntities = new HashSet<>(); private transient int triggerFlashTick = -1; - private transient int ticksSinceDamage = 0; private transient Set damagedThisTick = new HashSet<>(); + /** Ticks each entity has spent inside the zone since it was last damaged, by entity id. */ + private transient Map ticksInZone = new HashMap<>(); private transient Ability sourceAbility = null; @@ -216,7 +219,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD this.ownerEntityId = owner != null ? owner.getEntityId() : -1; double groundY = Ability.findGroundLevel(worldObj, x, y, z) + GROUND_OFFSET; this.setPosition(x, groundY, z); - this.ticksSinceDamage = damageInterval; } /** @@ -309,7 +311,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD zone.effects.add(e.copy()); } } - zone.ticksSinceDamage = damageInterval; return zone; } @@ -495,13 +496,10 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD private void tickHazard() { damagedThisTick.clear(); - ticksSinceDamage++; - - if (ticksSinceDamage < damageInterval) { - return; - } - ticksSinceDamage = 0; + // Membership is checked every tick, not once per damage interval. Sampling on the + // interval alone made the effective boundary drift by however far an entity moved in + // between, which on a large radius reads as the radius itself being wrong. AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox( posX - radius, posY - 0.5, posZ - radius, posX + radius, posY + zoneHeight, posZ + radius @@ -511,19 +509,35 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD @SuppressWarnings("unchecked") List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); + Set insideThisTick = new HashSet<>(); for (EntityLivingBase entity : entities) { if (owner != null && entity == owner && !affectsCaster) continue; if (damagedThisTick.contains(entity.getEntityId())) continue; if (entity != owner && owner instanceof EntityLivingBase && !AbilityTargetHelper.shouldAffect((EntityLivingBase) owner, entity, TargetFilter.ENEMIES, false)) continue; if (!isInZone(entity)) continue; + int entityId = entity.getEntityId(); + insideThisTick.add(entityId); + + // Entities that just entered are hit immediately, then once per interval of time + // actually spent inside. + Integer elapsed = ticksInZone.get(entityId); + int ticks = elapsed == null ? damageInterval : elapsed + 1; + if (ticks < damageInterval) { + ticksInZone.put(entityId, ticks); + continue; + } + ticksInZone.put(entityId, 0); + if (damagePerSecond > 0) { applyDamage(entity, owner, damagePerSecond); } applyEffects(entity); - damagedThisTick.add(entity.getEntityId()); + damagedThisTick.add(entityId); } + + ticksInZone.keySet().retainAll(insideThisTick); } private boolean isInZone(EntityLivingBase entity) { diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java index 4ccb01269..8b5da0ad5 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java @@ -114,6 +114,8 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { protected EnergyBarrierData barrierData = new EnergyBarrierData(); protected float currentHealth; protected int ticksAlive = 0; + /** Absolute world time the barrier expires at, so ticks it misses cannot extend it. */ + protected long barrierDeathTime = -1; // ==================== DATA WATCHER INDICES ==================== protected static final int DW_HEALTH_PERCENT = 21; @@ -510,15 +512,16 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { } } - // Duration check - if (barrierData.useDuration && ticksAlive >= barrierData.durationTicks) { - onBarrierDestroyed(); - this.setDead(); - return true; + // Set the deadline on the first tick after charging, so lifetime starts when the + // barrier is actually up. Counting elapsed ticks instead would let any tick the + // entity misses - chunk unload, for one - extend it past its configured duration. + if (barrierDeathTime < 0 && !isCharging()) { + barrierDeathTime = worldObj.getTotalWorldTime() + + (barrierData.useDuration ? barrierData.durationTicks : BARRIER_HARD_LIFETIME_CAP); } - // Absolute hard lifetime cap (safety net for barriers with no duration) - if (ticksAlive > BARRIER_HARD_LIFETIME_CAP) { + // Duration check, and the hard lifetime cap for barriers with no duration + if (barrierDeathTime >= 0 && worldObj.getTotalWorldTime() >= barrierDeathTime) { onBarrierDestroyed(); this.setDead(); return true; @@ -865,6 +868,7 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { protected void writeBarrierBaseNBT(NBTTagCompound nbt) { writeEnergyBaseNBT(nbt); nbt.setInteger("TicksAlive", ticksAlive); + nbt.setLong("BarrierDeathTime", barrierDeathTime); nbt.setFloat("CurrentHealth", currentHealth); nbt.setBoolean("Charging", charging); nbt.setInteger("ChargeTick", chargeTick); @@ -879,6 +883,7 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { protected void readBarrierBaseNBT(NBTTagCompound nbt) { readEnergyBaseNBT(nbt); this.ticksAlive = nbt.getInteger("TicksAlive"); + this.barrierDeathTime = nbt.hasKey("BarrierDeathTime") ? nbt.getLong("BarrierDeathTime") : -1; this.currentHealth = nbt.getFloat("CurrentHealth"); if (Float.isNaN(currentHealth) || Float.isInfinite(currentHealth) || currentHealth < 0) currentHealth = barrierData.maxHealth; this.charging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); From 4341c15cf4daf53e599a9f3aff51fa94214da1ce Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:33:04 -0400 Subject: [PATCH 27/34] Scale melee damage dealt to barriers Barriers are plain entities, so no LivingHurtEvent fires for them and nothing scaled an incoming melee hit - vanilla handed over the bare attackDamage attribute and a dome took one point per swing however strong the attacker was. Attributes now apply, and modifyBarrierMeleeDamage gives addons the same seam they already have for barrier health. --- .../npcs/controllers/AbilityController.java | 12 ++++++++++++ .../data/ability/extender/IAbilityExtender.java | 16 ++++++++++++++++ .../kamkeel/npcs/entity/EntityEnergyBarrier.java | 16 +++++++++++++++- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/controllers/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/AbilityController.java index 6c5810188..66c057929 100644 --- a/src/main/java/kamkeel/npcs/controllers/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/AbilityController.java @@ -46,6 +46,7 @@ import noppes.npcs.controllers.data.AbilityScript; import noppes.npcs.controllers.data.ChainedAbilityScript; import net.minecraft.entity.EntityLivingBase; import kamkeel.npcs.controllers.sync.handlers.PlayerAbilitySyncHelper; +import kamkeel.npcs.entity.EntityEnergyBarrier; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; @@ -1381,6 +1382,17 @@ public class AbilityController implements IAbilityHandler { return health; } + /** + * Fire modifyBarrierMeleeDamage on all extenders. Cumulative — each extender's output feeds the next. + */ + public float fireModifyBarrierMeleeDamage(EntityEnergyBarrier barrier, EntityLivingBase attacker, float baseDamage) { + float damage = baseDamage; + for (IAbilityExtender ext : extenders) { + damage = ext.modifyBarrierMeleeDamage(barrier, attacker, damage); + } + return damage; + } + /** * Fire onAbilityHeal on all extenders. Chain of responsibility — first true wins. */ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java b/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java index d691a6d18..2a0b90344 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.extender; import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition; import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.entity.EntityEnergyBarrier; import net.minecraft.entity.EntityLivingBase; /** @@ -108,6 +109,21 @@ public interface IAbilityExtender { return baseHealth; } + /** + * Called when a barrier entity takes a melee hit, allowing extenders to scale the damage. + * A barrier is a plain Entity, so no LivingHurtEvent fires for it and an extender has no + * other opportunity to scale melee against it the way it scales {@link #modifyBarrierHealth}. + * Cumulative: all extenders apply in sequence, each receiving the previous output. + * + * @param barrier The barrier being hit + * @param attacker The entity dealing the hit + * @param baseDamage The damage after the barrier's own melee multiplier + * @return The modified damage value + */ + default float modifyBarrierMeleeDamage(EntityEnergyBarrier barrier, EntityLivingBase attacker, float baseDamage) { + return baseDamage; + } + /** * Called when an ability is about to heal an entity. * Return true to handle the healing (skip default entity.heal()). diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java index 8b5da0ad5..da9edb676 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java @@ -1,5 +1,7 @@ package kamkeel.npcs.entity; +import kamkeel.npcs.addon.DBCAddon; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData; import kamkeel.npcs.network.packets.data.energy.BarrierClientSyncPacket; @@ -15,6 +17,7 @@ import net.minecraft.util.DamageSource; import net.minecraft.world.World; import noppes.npcs.CustomNpcs; import noppes.npcs.EventHooks; +import noppes.npcs.config.ConfigMain; import noppes.npcs.controllers.data.MagicData; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; @@ -438,8 +441,19 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { float damage = amount * barrierData.meleeDamageMultiplier; - // Apply magic interactions from attacker's magic vs barrier's magic Entity attacker = source.getEntity(); + + // A barrier is a plain Entity, so no LivingHurtEvent fires for it and nothing scales the + // hit the way it would against a living target - vanilla hands us the bare attackDamage + // attribute. Run the scaling here instead, or the barrier only ever takes a point of it. + if (attacker instanceof EntityLivingBase) { + damage = AbilityController.Instance.fireModifyBarrierMeleeDamage(this, (EntityLivingBase) attacker, damage); + } + if (attacker instanceof EntityPlayer && ConfigMain.AttributesEnabled && !DBCAddon.IsAvailable()) { + damage = AttributeAttackUtil.calculateOutgoing((EntityPlayer) attacker, damage); + } + + // Apply magic interactions from attacker's magic vs barrier's magic if (attacker != null) { MagicData attackerMagic = null; if (attacker instanceof EntityPlayer) { From 1acda0e6b088ff16ca855eafba23e94c2198869c Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 04:23:59 -0400 Subject: [PATCH 28/34] Scale hazard damage to its interval and floor the interval at 5 ticks The value is configured, stored and labelled as damage per second, but a pulse applied it whole, so the real rate was damage per interval - it only matched at the default interval of 20. A pulse now deals the share of a second it covers. The interval is floored at 5 ticks, which also bounds how often a zone scans; this replaces the per-tick membership check, which cost 20x the entity queries. --- .../data/ability/type/AbilityHazard.java | 5 ++- .../npcs/entity/EntityAbilityZone.java | 44 +++++++------------ 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 47f9156a8..2086d4e8f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -107,7 +107,7 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { readZoneNBT(nbt, 300); this.radius = nbt.getFloat("radius"); this.damagePerSecond = nbt.getFloat("damagePerSecond"); - this.damageInterval = nbt.getInteger("damageInterval"); + setDamageInterval(nbt.getInteger("damageInterval")); this.affectsCaster = nbt.getBoolean("affectsCaster"); } @@ -138,7 +138,7 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { } public void setDamageInterval(int damageInterval) { - this.damageInterval = damageInterval; + this.damageInterval = Math.max(EntityAbilityZone.MIN_DAMAGE_INTERVAL, damageInterval); } public boolean isAffectsCaster() { @@ -171,6 +171,7 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { FieldDef.row( FieldDef.floatField("gui.dps", this::getDamagePerSecond, this::setDamagePerSecond), FieldDef.intField("gui.interval", this::getDamageInterval, this::setDamageInterval) + .range(EntityAbilityZone.MIN_DAMAGE_INTERVAL, 2000) ), FieldDef.boolField("ability.affectsCaster", this::isAffectsCaster, this::setAffectsCaster) .hover("ability.hover.affectsCaster"), diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java index 2a4632e77..529d59d74 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java @@ -23,10 +23,8 @@ import noppes.npcs.NpcDamageSource; import noppes.npcs.entity.EntityNPCInterface; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; /** @@ -157,6 +155,9 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD // HAZARD-SPECIFIC PROPERTIES // ═══════════════════════════════════════════════════════════════════ + /** Floor on how often a hazard pulses, so a small interval cannot turn into a per-tick scan. */ + public static final int MIN_DAMAGE_INTERVAL = 5; + private float damagePerSecond = 1.0f; private int damageInterval = 20; private boolean ignoreIFrames = false; @@ -172,9 +173,8 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD private transient Set triggeredEntities = new HashSet<>(); private transient int triggerFlashTick = -1; + private transient int ticksSinceDamage = 0; private transient Set damagedThisTick = new HashSet<>(); - /** Ticks each entity has spent inside the zone since it was last damaged, by entity id. */ - private transient Map ticksInZone = new HashMap<>(); private transient Ability sourceAbility = null; @@ -219,6 +219,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD this.ownerEntityId = owner != null ? owner.getEntityId() : -1; double groundY = Ability.findGroundLevel(worldObj, x, y, z) + GROUND_OFFSET; this.setPosition(x, groundY, z); + this.ticksSinceDamage = damageInterval; } /** @@ -293,7 +294,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD zone.shape = shape; zone.radius = radius; zone.damagePerSecond = damagePerSecond; - zone.damageInterval = damageInterval; + zone.damageInterval = Math.max(MIN_DAMAGE_INTERVAL, damageInterval); zone.ignoreIFrames = ignoreIFrames; zone.affectsCaster = affectsCaster; zone.durationTicks = durationTicks; @@ -496,10 +497,13 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD private void tickHazard() { damagedThisTick.clear(); + ticksSinceDamage++; + + if (ticksSinceDamage < damageInterval) { + return; + } + ticksSinceDamage = 0; - // Membership is checked every tick, not once per damage interval. Sampling on the - // interval alone made the effective boundary drift by however far an entity moved in - // between, which on a large radius reads as the radius itself being wrong. AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox( posX - radius, posY - 0.5, posZ - radius, posX + radius, posY + zoneHeight, posZ + radius @@ -509,35 +513,21 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD @SuppressWarnings("unchecked") List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); - Set insideThisTick = new HashSet<>(); for (EntityLivingBase entity : entities) { if (owner != null && entity == owner && !affectsCaster) continue; if (damagedThisTick.contains(entity.getEntityId())) continue; if (entity != owner && owner instanceof EntityLivingBase && !AbilityTargetHelper.shouldAffect((EntityLivingBase) owner, entity, TargetFilter.ENEMIES, false)) continue; if (!isInZone(entity)) continue; - int entityId = entity.getEntityId(); - insideThisTick.add(entityId); - - // Entities that just entered are hit immediately, then once per interval of time - // actually spent inside. - Integer elapsed = ticksInZone.get(entityId); - int ticks = elapsed == null ? damageInterval : elapsed + 1; - if (ticks < damageInterval) { - ticksInZone.put(entityId, ticks); - continue; - } - ticksInZone.put(entityId, 0); - if (damagePerSecond > 0) { - applyDamage(entity, owner, damagePerSecond); + // The configured value is damage per second, so a pulse deals the share of a + // second it covers. Applying it whole made the real rate depend on the interval. + applyDamage(entity, owner, damagePerSecond * (damageInterval / 20.0f)); } applyEffects(entity); - damagedThisTick.add(entityId); + damagedThisTick.add(entity.getEntityId()); } - - ticksInZone.keySet().retainAll(insideThisTick); } private boolean isInZone(EntityLivingBase entity) { @@ -829,7 +819,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD public float getDamagePerSecond() { return damagePerSecond; } public void setDamagePerSecond(float dps) { this.damagePerSecond = dps; } public int getDamageInterval() { return damageInterval; } - public void setDamageInterval(int ticks) { this.damageInterval = ticks; } + public void setDamageInterval(int ticks) { this.damageInterval = Math.max(MIN_DAMAGE_INTERVAL, ticks); } public boolean isAffectsCaster() { return affectsCaster; } public void setAffectsCaster(boolean affects) { this.affectsCaster = affects; } From c79f5998d621fae8b90c7415d0b2353caaa61b80 Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 04:23:59 -0400 Subject: [PATCH 29/34] Stop NPC pathfinding fighting Charge's own movement Charge drives the NPC with applyVelocityFlat but never declared that it does, so locking movement during the active phase zeroed its motion outright while not locking left the navigator running against it - no setting worked. It now declares its own movement and locks both phases, so the navigator stays clear. --- .../npcs/controllers/data/ability/type/AbilityCharge.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index c88d63c30..d5c293c64 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -43,7 +43,7 @@ public class AbilityCharge extends AbilityMovement implements IAbilityCharge { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 20.0f; this.minRange = 4.0f; - this.lockMovement = LockMode.WINDUP; + this.lockMovement = LockMode.WINDUP_AND_ACTIVE; this.cooldownTicks = 0; this.windUpTicks = 20; // LINE telegraph showing charge path @@ -58,6 +58,11 @@ public class AbilityCharge extends AbilityMovement implements IAbilityCharge { }; } + @Override + public boolean hasAbilityMovement() { + return true; // This ability drives the NPC's motion itself + } + @Override public boolean isTargetingModeLocked() { return true; From dedbe94cd8e2a9551ee808d7a2b24c678c0819ce Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 04:44:14 -0400 Subject: [PATCH 30/34] Keep client-only sub-GUI types out of Ability's method descriptors The magic editor field was built with lambdas inside Ability.getAllDefinitions. subGuiField is generic, so those lambdas took SubGuiAbilityMagic as their descriptor type. @SideOnly strips the annotated method on a server but not the synthetic lambdas it generates, so anything reflecting over Ability's declared methods - Nashorn's adapter generator when a script touches an ability - hit NoClassDefFoundError. Building the field in the client class keeps the type out of Ability entirely. --- .../controllers/data/ability/Ability.java | 14 +--------- .../gui/advanced/SubGuiAbilityMagic.java | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 05049d8bb..a73a8a0a2 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -840,19 +840,7 @@ public abstract class Ability implements IAbility, IAbilityAction { return sb.toString(); }).tab("General")); - defs.add(FieldDef.subGuiField("ability.magic.editor", - () -> new SubGuiAbilityMagic(magicData.copy()), - gui -> { - MagicData result = ((SubGuiAbilityMagic) gui).magicData; - magicData.getMagics().clear(); - for (Map.Entry e : result.getMagics().entrySet()) { - magicData.getMagics().put(e.getKey(), e.getValue()); - } - }) - .buttonLabel(() -> magicData.isEmpty() - ? StatCollector.translateToLocal("ability.magic.usesCaster") - : magicData.getMagics().size() + " Magic(s)") - .tab("General")); + defs.add(SubGuiAbilityMagic.createMagicField(magicData)); } // ── Target tab ─────────────────────────────────────────────── diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityMagic.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityMagic.java index 8543ad208..8f555017a 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityMagic.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityMagic.java @@ -1,6 +1,8 @@ package noppes.npcs.client.gui.advanced; import net.minecraft.client.gui.GuiButton; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.gui.builder.FieldDef; import noppes.npcs.client.gui.util.GuiCustomScroll; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -11,6 +13,7 @@ import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.controllers.MagicController; import noppes.npcs.controllers.data.Magic; import noppes.npcs.controllers.data.MagicData; +import noppes.npcs.controllers.data.MagicEntry; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -36,6 +39,30 @@ public class SubGuiAbilityMagic extends SubGuiInterface implements ICustomScroll private GuiNpcTextField splitField; + /** + * Builds the ability editor's magic field. + *

+ * This lives here rather than in Ability because the lambdas below carry this class in + * their method descriptors. Ability is a both-side class, and anything reflecting over its + * declared methods on a server - Nashorn's adapter generator, for one - would resolve those + * descriptors and fail to find this client-only type. + */ + public static FieldDef createMagicField(MagicData magicData) { + return FieldDef.subGuiField("ability.magic.editor", + () -> new SubGuiAbilityMagic(magicData.copy()), + gui -> { + MagicData result = ((SubGuiAbilityMagic) gui).magicData; + magicData.getMagics().clear(); + for (Map.Entry e : result.getMagics().entrySet()) { + magicData.getMagics().put(e.getKey(), e.getValue()); + } + }) + .buttonLabel(() -> magicData.isEmpty() + ? StatCollector.translateToLocal("ability.magic.usesCaster") + : magicData.getMagics().size() + " Magic(s)") + .tab("General"); + } + public SubGuiAbilityMagic(MagicData data) { this.magicData = data; setBackground("menubg.png"); From 466b49db5ee5648932e44de0d7a7e2f6abab0eb3 Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 04:52:50 -0400 Subject: [PATCH 31/34] Let the day/night transform actually fire EntityAITransform claimed the PASSIVE mutex, so any running movement or look task blocked it from ever starting. The swap only happened in the gaps where those yielded - which is why it appeared to trigger when an NPC said a line and for a moment afterwards. The task only polls world time and flips the state, so it needs no mutex at all. --- src/main/java/noppes/npcs/ai/EntityAITransform.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/ai/EntityAITransform.java b/src/main/java/noppes/npcs/ai/EntityAITransform.java index 9f565bccc..b0c3fa30e 100644 --- a/src/main/java/noppes/npcs/ai/EntityAITransform.java +++ b/src/main/java/noppes/npcs/ai/EntityAITransform.java @@ -10,7 +10,10 @@ public class EntityAITransform extends EntityAIBase { public EntityAITransform(EntityNPCInterface npc) { this.npc = npc; - setMutexBits(AiMutex.PASSIVE); + // No mutex: this only polls the world time and flips the transform, it drives nothing. + // Claiming PASSIVE let any running movement or look task starve it, so the day/night + // swap only fired in the gaps where those yielded - such as while saying a line. + setMutexBits(0); } @Override From 677d50fa03fae36155f76b0d3998a92a425eae9a Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 05:00:02 -0400 Subject: [PATCH 32/34] Keep client-only sub-GUI types out of condition and zone descriptors Same leak as the ability magic editor: @SideOnly strips the annotated method on a server but not the synthetic lambdas it generates, so ConditionHasEffect, ConditionQuestCompleted, AbilityTrap and AbilityZone all shipped methods whose descriptors named a client-only GUI class. Each field is now built in the GUI class it belongs to. --- .../conditions/ConditionHasEffect.java | 26 +++++------------- .../conditions/ConditionQuestCompleted.java | 15 +---------- .../data/ability/type/AbilityTrap.java | 9 +------ .../data/ability/type/AbilityZone.java | 9 +------ .../client/gui/SubGuiTrapPresetSelector.java | 20 ++++++++++++++ .../client/gui/SubGuiZonePresetSelector.java | 20 ++++++++++++++ .../advanced/SubGuiCustomEffectSelect.java | 27 +++++++++++++++++++ .../client/gui/select/GuiQuestSelection.java | 27 +++++++++++++++++++ 8 files changed, 104 insertions(+), 49 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHasEffect.java b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHasEffect.java index eca7aded0..a608a92e6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHasEffect.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHasEffect.java @@ -34,27 +34,15 @@ public class ConditionHasEffect extends AbilityCondition { return CustomEffectController.getInstance().hasEffect(player, effectId, effectIndex); } + public void setEffect(int effectId, int effectIndex) { + this.effectId = effectId; + this.effectIndex = effectIndex; + } + @SideOnly(Side.CLIENT) @Override public void getConditionDefinitions(List defs) { - defs.add(FieldDef.subGuiField("condition.select_effect", - () -> new SubGuiCustomEffectSelect(effectId, effectIndex), - gui -> { - SubGuiCustomEffectSelect sel = (SubGuiCustomEffectSelect) gui; - if (sel.getSelectedEffectId() >= 0) { - effectId = sel.getSelectedEffectId(); - effectIndex = sel.getSelectedIndex(); - } - }) - .buttonLabel(() -> { - if (effectId < 0) return "None"; - CustomEffect effect = getEffect(); - return effect != null ? effect.getName() : "ID:" + effectId; - }) - .clearable(() -> { - effectId = -1; - effectIndex = 0; - })); + defs.add(SubGuiCustomEffectSelect.createSelectField(this)); } @SideOnly(Side.CLIENT) @@ -74,7 +62,7 @@ public class ConditionHasEffect extends AbilityCondition { return effectId >= 0; } - private CustomEffect getEffect() { + public CustomEffect getEffect() { HashMap map = CustomEffectController.getInstance().getEffectMap(effectIndex); return map != null ? map.get(effectId) : null; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java index 2955b7610..ac8a05272 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java @@ -38,20 +38,7 @@ public class ConditionQuestCompleted extends AbilityCondition { @SideOnly(Side.CLIENT) @Override public void getConditionDefinitions(List defs) { - defs.add(FieldDef.subGuiField("condition.select_quest", - () -> new GuiQuestSelection(questId), - gui -> { - GuiQuestSelection sel = (GuiQuestSelection) gui; - if (sel.selectedQuest != null) { - questId = sel.selectedQuest.id; - } - }) - .buttonLabel(() -> { - if (questId < 0) return "None"; - Quest q = QuestController.Instance.quests.get(questId); - return q != null ? q.title : "ID:" + questId; - }) - .clearable(() -> questId = -1)); + defs.add(GuiQuestSelection.createConditionField(this)); } @SideOnly(Side.CLIENT) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 5e508069a..1c40b202e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -300,14 +300,7 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap { @SideOnly(Side.CLIENT) @Override protected void addPresetFieldDef(List defs) { - defs.add(FieldDef.subGuiField("gui.applyPreset", - SubGuiTrapPresetSelector::new, - gui -> { - SubGuiTrapPresetSelector selector = (SubGuiTrapPresetSelector) gui; - if (selector.selectedPreset != null) { - applyTrapPresetDefaults(selector.selectedPreset); - } - })); + defs.add(SubGuiTrapPresetSelector.createPresetField(this::applyTrapPresetDefaults)); } @SideOnly(Side.CLIENT) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java index 968ff03ba..a2f9fdeb7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java @@ -690,14 +690,7 @@ public abstract class AbilityZone extends Ability { @SideOnly(Side.CLIENT) protected void addPresetFieldDef(List defs) { - defs.add(FieldDef.subGuiField("gui.applyPreset", - SubGuiZonePresetSelector::new, - gui -> { - SubGuiZonePresetSelector selector = (SubGuiZonePresetSelector) gui; - if (selector.selectedPreset != null) { - applyPresetDefaults(selector.selectedPreset); - } - })); + defs.add(SubGuiZonePresetSelector.createPresetField(this::applyPresetDefaults)); } @SideOnly(Side.CLIENT) diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiTrapPresetSelector.java b/src/main/java/noppes/npcs/client/gui/SubGuiTrapPresetSelector.java index b8fb9e376..086e27569 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiTrapPresetSelector.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiTrapPresetSelector.java @@ -4,6 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import net.minecraft.client.gui.GuiButton; import net.minecraft.util.StatCollector; +import noppes.npcs.client.gui.builder.FieldDef; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.SubGuiInterface; @@ -12,6 +13,25 @@ public class SubGuiTrapPresetSelector extends SubGuiInterface { public String selectedPreset = null; + /** + * Builds the trap editor's preset field. + *

+ * This lives here rather than in AbilityTrap because the lambda below carries this class in + * its method descriptor. AbilityTrap is a both-side class, and @SideOnly does not strip the + * synthetic lambda methods it generates - so anything reflecting over its declared methods + * on a server would resolve that descriptor and fail to find this client-only type. + */ + public static FieldDef createPresetField(java.util.function.Consumer applyPreset) { + return FieldDef.subGuiField("gui.applyPreset", + SubGuiTrapPresetSelector::new, + gui -> { + SubGuiTrapPresetSelector selector = (SubGuiTrapPresetSelector) gui; + if (selector.selectedPreset != null) { + applyPreset.accept(selector.selectedPreset); + } + }); + } + private static final String[] PRESET_NAMES = {"HIDDEN", "VENOM", "EXPLOSIVE", "CURSED", "SHOCK", "SNARE"}; private static final String[] PRESET_LABELS = { "ability.preset.hidden", "ability.preset.venom", "ability.preset.explosive", diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiZonePresetSelector.java b/src/main/java/noppes/npcs/client/gui/SubGuiZonePresetSelector.java index f67ef3db1..5bdc9c291 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiZonePresetSelector.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiZonePresetSelector.java @@ -4,6 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import net.minecraft.client.gui.GuiButton; import net.minecraft.util.StatCollector; +import noppes.npcs.client.gui.builder.FieldDef; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.SubGuiInterface; @@ -12,6 +13,25 @@ public class SubGuiZonePresetSelector extends SubGuiInterface { public String selectedPreset = null; + /** + * Builds the zone editor's preset field. + *

+ * This lives here rather than in AbilityZone because the lambda below carries this class in + * its method descriptor. AbilityZone is a both-side class, and @SideOnly does not strip the + * synthetic lambda methods it generates - so anything reflecting over its declared methods + * on a server would resolve that descriptor and fail to find this client-only type. + */ + public static FieldDef createPresetField(java.util.function.Consumer applyPreset) { + return FieldDef.subGuiField("gui.applyPreset", + SubGuiZonePresetSelector::new, + gui -> { + SubGuiZonePresetSelector selector = (SubGuiZonePresetSelector) gui; + if (selector.selectedPreset != null) { + applyPreset.accept(selector.selectedPreset); + } + }); + } + private static final String[] PRESET_NAMES = {"DEFAULT", "TOXIC", "INFERNO", "ARCANE", "ELECTRIC", "FROST"}; private static final String[] PRESET_LABELS = { "ability.preset.default", "ability.preset.toxic", "ability.preset.inferno", diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiCustomEffectSelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiCustomEffectSelect.java index 411d03351..233ad8ece 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiCustomEffectSelect.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiCustomEffectSelect.java @@ -3,6 +3,8 @@ package noppes.npcs.client.gui.advanced; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import net.minecraft.client.gui.GuiButton; +import kamkeel.npcs.controllers.data.ability.conditions.ConditionHasEffect; +import noppes.npcs.client.gui.builder.FieldDef; import noppes.npcs.client.gui.util.GuiCustomScroll; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcTextField; @@ -25,6 +27,31 @@ import java.util.Map; @SideOnly(Side.CLIENT) public class SubGuiCustomEffectSelect extends SubGuiInterface implements ICustomScrollListener, ITextfieldListener { + /** + * Builds the condition editor's effect-select field. + *

+ * This lives here rather than in ConditionHasEffect because the lambdas below carry this + * class in their method descriptors. The condition is a both-side class, and @SideOnly does + * not strip the synthetic lambda methods it generates - so anything reflecting over its + * declared methods on a server would resolve those descriptors and fail to find this type. + */ + public static FieldDef createSelectField(ConditionHasEffect condition) { + return FieldDef.subGuiField("condition.select_effect", + () -> new SubGuiCustomEffectSelect(condition.getEffectId(), condition.getEffectIndex()), + gui -> { + SubGuiCustomEffectSelect sel = (SubGuiCustomEffectSelect) gui; + if (sel.getSelectedEffectId() >= 0) { + condition.setEffect(sel.getSelectedEffectId(), sel.getSelectedIndex()); + } + }) + .buttonLabel(() -> { + if (condition.getEffectId() < 0) return "None"; + CustomEffect effect = condition.getEffect(); + return effect != null ? effect.getName() : "ID:" + condition.getEffectId(); + }) + .clearable(() -> condition.setEffect(-1, 0)); + } + private GuiCustomScroll scroll; private final HashMap displayNameToId = new HashMap<>(); private final HashMap allDisplayNameToId = new HashMap<>(); 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 288964545..cf805fe44 100644 --- a/src/main/java/noppes/npcs/client/gui/select/GuiQuestSelection.java +++ b/src/main/java/noppes/npcs/client/gui/select/GuiQuestSelection.java @@ -2,6 +2,8 @@ package noppes.npcs.client.gui.select; import com.google.common.collect.Lists; import net.minecraft.client.gui.GuiButton; +import kamkeel.npcs.controllers.data.ability.conditions.ConditionQuestCompleted; +import noppes.npcs.client.gui.builder.FieldDef; import noppes.npcs.client.gui.util.GuiCustomScroll; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -27,6 +29,31 @@ public class GuiQuestSelection extends SubGuiInterface implements ICustomScrollL private QuestCategory selectedCategory; public Quest selectedQuest; + /** + * Builds the condition editor's quest-select field. + *

+ * This lives here rather than in ConditionQuestCompleted because the lambdas below carry + * this class in their method descriptors. The condition is a both-side class, and @SideOnly + * does not strip the synthetic lambda methods it generates - so anything reflecting over its + * declared methods on a server would resolve those descriptors and fail to find this type. + */ + public static FieldDef createConditionField(ConditionQuestCompleted condition) { + return FieldDef.subGuiField("condition.select_quest", + () -> new GuiQuestSelection(condition.getQuestId()), + gui -> { + GuiQuestSelection sel = (GuiQuestSelection) gui; + if (sel.selectedQuest != null) { + condition.setQuestId(sel.selectedQuest.id); + } + }) + .buttonLabel(() -> { + if (condition.getQuestId() < 0) return "None"; + Quest q = QuestController.Instance.quests.get(condition.getQuestId()); + return q != null ? q.title : "ID:" + condition.getQuestId(); + }) + .clearable(() -> condition.setQuestId(-1)); + } + private GuiSelectionListener listener; private String catSearch = ""; private String questSearch = ""; From 71bb4b451ea67fedbcb2542ff8e7e51c30f9ed81 Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 05:01:50 -0400 Subject: [PATCH 33/34] Honour the effective windup instead of the raw field start() and tick() compared against the raw windUpTicks, but when the windup is synced to an animation the value that applies comes from getWindUpTicks(). An ability configured that way skipped its windup entirely and fired on the first tick. The telegraph duration used the raw field too, so it disagreed with the phase it was meant to cover. --- .../npcs/controllers/data/ability/Ability.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index a73a8a0a2..d944f7535 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -1050,10 +1050,11 @@ public abstract class Ability implements IAbility, IAbilityAction { return null; } - telegraph.setDurationTicks(windUpTicks); + int windUp = getWindUpTicks(); + telegraph.setDurationTicks(windUp); telegraph.setColor(windUpColor); telegraph.setWarningColor(activeColor); - telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setWarningStartTick(Math.max(5, windUp / 4)); telegraph.setHeightOffset(telegraphHeightOffset); TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); @@ -1171,7 +1172,9 @@ public abstract class Ability implements IAbility, IAbilityAction { this.burstIndex = 0; this.burstEntities.clear(); - if (windUpTicks <= 0) { + // Use the effective windup, not the raw field: when the windup is synced to an + // animation the raw field is not the duration that actually applies. + if (getWindUpTicks() <= 0) { this.phase = AbilityPhase.ACTIVE; } else { this.phase = AbilityPhase.WINDUP; @@ -1193,7 +1196,7 @@ public abstract class Ability implements IAbility, IAbilityAction { switch (phase) { case WINDUP: - if (currentTick >= windUpTicks) { + if (currentTick >= getWindUpTicks()) { phase = AbilityPhase.ACTIVE; currentTick = 0; return true; From a028142dda9e2e362e1a144e126b0bab44e4fb43 Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 05:08:15 -0400 Subject: [PATCH 34/34] Fix mouse wheel scrolling in the script editors Mouse.getDWheel() clears the accumulated delta, so whichever widget polled it first in a frame took it and every later reader saw zero. The script text areas polled it themselves, after the parent screen had already drained it for its scroll lists, so the wheel did nothing in the player, NPC and event script GUIs. The screen now reads the wheel once per frame into the mouseScroll field it already declared for this, and each widget zeroes it when it consumes it. --- .../npcs/client/gui/util/GuiNPCInterface.java | 17 +++++++++++++++-- .../npcs/client/gui/util/GuiScriptTextArea.java | 6 ++++-- .../client/gui/util/GuiScriptTextArea1.java | 6 ++++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 08a5dff00..24d08cff2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -515,15 +515,28 @@ public abstract class GuiNPCInterface extends GuiScreen { } boolean subGui = hasSubGui(); + + // Mouse.getDWheel() clears the accumulated delta, so only the first caller in a frame + // ever sees it. Read it once here and let each widget take it in turn; anything that + // consumes it zeroes it so later widgets - the script editor, for one - are not left + // reading an already-drained wheel. + mouseScroll = Mouse.getDWheel(); + drawCenteredString(fontRendererObj, title, width / 2, guiTop + 4, 0xffffff); for (GuiNpcLabel label : labels.values()) label.drawLabel(this, fontRendererObj); for (GuiCustomScroll scroll : scrolls.values()) { scroll.updateSubGUI(subGui); - scroll.drawScreen(drawX, drawY, f, !subGui && scroll.isMouseOver(drawX, drawY) ? Mouse.getDWheel() : 0); + int delta = !subGui && scroll.isMouseOver(drawX, drawY) ? mouseScroll : 0; + if (delta != 0) + mouseScroll = 0; + scroll.drawScreen(drawX, drawY, f, delta); } for (GuiScrollWindow guiScrollableComponent : scrollWindows.values()) { - guiScrollableComponent.drawScreen(drawX, drawY, f, !subGui && guiScrollableComponent.isMouseOver(drawX, drawY) ? Mouse.getDWheel() : 0); + int delta = !subGui && guiScrollableComponent.isMouseOver(drawX, drawY) ? mouseScroll : 0; + if (delta != 0) + mouseScroll = 0; + guiScrollableComponent.drawScreen(drawX, drawY, f, delta); } for (GuiDiagram diagram : diagrams.values()) { diagram.drawDiagram(drawX, drawY, subGui); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 647621117..70283b7e0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -624,9 +624,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { int maxScroll = Math.max(0, getPaddedLineCount() - container.visibleLines); // Handle mouse wheel scroll - int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); + // Take whatever the parent screen has left for this frame rather than polling the wheel + // again - the poll is destructive and the parent has already drained it. + int wheelDelta = listener instanceof GuiNPCInterface ? ((GuiNPCInterface) listener).mouseScroll : Mouse.getDWheel(); if (listener instanceof GuiNPCInterface) { - ((GuiNPCInterface) listener).mouseScroll = wheelDelta; + ((GuiNPCInterface) listener).mouseScroll = 0; // Intercept wheel scroll for tooltip (highest priority) // Only consume the wheel event when the tooltip actually has content to scroll; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java index 60e5a38a0..a5a555514 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java @@ -486,9 +486,11 @@ public class GuiScriptTextArea1 extends GuiNpcTextField { int maxScroll = Math.max(0, getPaddedLineCount() - container.visibleLines); // Handle mouse wheel scroll - int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); + // Take whatever the parent screen has left for this frame rather than polling the wheel + // again - the poll is destructive and the parent has already drained it. + int wheelDelta = listener instanceof GuiNPCInterface ? ((GuiNPCInterface) listener).mouseScroll : Mouse.getDWheel(); if (listener instanceof GuiNPCInterface) { - ((GuiNPCInterface) listener).mouseScroll = wheelDelta; + ((GuiNPCInterface) listener).mouseScroll = 0; boolean canScroll = !KEYS_OVERLAY.isVisible() || KEYS_OVERLAY.isVisible() && !KEYS_OVERLAY.aboveOverlay; if (wheelDelta != 0 && canScroll) scroll.applyWheelScroll(wheelDelta, maxScroll);