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:
+ *
+ * - Deserialize from the compound into a sync buffer
+ * - Atomically swap the sync buffer to the primary map
+ * - Reset the sync buffer for the next reload
+ *
+ *
+ * @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);
}
}