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..66c057929 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; @@ -44,7 +45,10 @@ import net.minecraftforge.common.util.Constants; import noppes.npcs.controllers.data.AbilityScript; import noppes.npcs.controllers.data.ChainedAbilityScript; import net.minecraft.entity.EntityLivingBase; +import kamkeel.npcs.controllers.sync.handlers.PlayerAbilitySyncHelper; +import kamkeel.npcs.entity.EntityEnergyBarrier; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; @@ -393,7 +397,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); @@ -469,16 +473,32 @@ public class AbilityController implements IAbilityHandler { pData.abilityData.lockAbility(name); changed = true; } + + // An active toggle holds the resolved Ability directly and keeps ticking it, + // so drop it without firing onToggle against an unregistered ability. + if (uuid != null && !uuid.isEmpty()) { + pData.abilityData.setToggleEntryDirect(uuid, 0); + } + pData.abilityData.setToggleEntryDirect(name, 0); + + if (pData.hotbarData != null) { + pData.hotbarData.validateSlots(); + } + if (changed) { pData.save(); } + + if (pData.player instanceof EntityPlayerMP) { + PlayerAbilitySyncHelper.syncAbilities((EntityPlayerMP) pData.player); + } } } } customAbilityRevision++; LogWriter.script("Deleted custom ability: " + name); - SyncController.syncAllCustomAbilities(); + SyncController.syncAll(SyncType.CUSTOM_ABILITY); return true; } @@ -765,7 +785,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 +865,7 @@ public class AbilityController implements IAbilityHandler { chainedAbilityRevision++; LogWriter.script("Deleted chained ability: " + name); - SyncController.syncAllChainedAbilities(); + SyncController.syncAll(SyncType.CHAINED_ABILITY); return true; } @@ -1362,6 +1382,17 @@ public class AbilityController implements IAbilityHandler { return health; } + /** + * Fire modifyBarrierMeleeDamage on all extenders. Cumulative — each extender's output feeds the next. + */ + public float fireModifyBarrierMeleeDamage(EntityEnergyBarrier barrier, EntityLivingBase attacker, float baseDamage) { + float damage = baseDamage; + for (IAbilityExtender ext : extenders) { + damage = ext.modifyBarrierMeleeDamage(barrier, attacker, damage); + } + return damage; + } + /** * Fire onAbilityHeal on all extenders. Chain of responsibility — first true wins. */ diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index 57b111404..56dc377f9 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -5,185 +5,125 @@ 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.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import kamkeel.npcs.network.packets.data.LoginPacket; import kamkeel.npcs.network.packets.data.ProfileSharedQuestPacket; -import kamkeel.npcs.network.packets.data.ability.AbilityCooldownSyncPacket; -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.LogWriter; 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.client.gui.util.IGuiData; 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.EnumMap; -import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; +/** + * Central sync backend for CNPC+. + * + *

This controller manages two distinct sync lanes:

+ * + * + *

This class retains ownership of:

+ * + */ public class SyncController { - public static boolean DEBUG_SYNC_LOGGING = false; + /** Cached serialized reload payloads and revision state for each global sync family. */ + private static final Map cacheEntries = new LinkedHashMap<>(); - private static final EnumMap cacheEntries = new EnumMap<>(EnumSyncType.class); + /** Last known synced revision map for each connected player. */ private static final ConcurrentHashMap playerSyncState = new ConcurrentHashMap<>(); + /** Stable identity token for the current dedicated-server process, used by the revision handshake. */ + private static final String SERVER_IDENTITY_KEY = UUID.randomUUID().toString(); - private static void debug(String message, Object... args) { - if (DEBUG_SYNC_LOGGING) { - System.out.println("[SyncController] " + String.format(message, args)); - } + // ═══════════════════════════════════════════════════════════════════════ + // 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 register() { + 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()); } - - private static final EnumSyncType[] LOGIN_SYNC_TYPES = new EnumSyncType[]{ - EnumSyncType.FACTION, - EnumSyncType.DIALOG_CATEGORY, - EnumSyncType.QUEST_CATEGORY, - EnumSyncType.WORKBENCH_RECIPES, - EnumSyncType.CARPENTRY_RECIPES, - EnumSyncType.ANVIL_RECIPES, - EnumSyncType.CUSTOM_EFFECTS, - EnumSyncType.MAGIC, - EnumSyncType.MAGIC_CYCLE, - EnumSyncType.CUSTOM_ABILITY, - EnumSyncType.CHAINED_ABILITY - }; - + public static void load() { cacheEntries.clear(); playerSyncState.clear(); - registerCache(EnumSyncType.FACTION, SyncController::factionsNBT); - registerCache(EnumSyncType.DIALOG_CATEGORY, SyncController::dialogCategoriesNBT); - registerCache(EnumSyncType.QUEST_CATEGORY, SyncController::questCategoriesNBT); - registerCache(EnumSyncType.WORKBENCH_RECIPES, SyncController::workbenchNBT); - registerCache(EnumSyncType.CARPENTRY_RECIPES, SyncController::carpentryNBT); - registerCache(EnumSyncType.ANVIL_RECIPES, SyncController::anvilNBT); - registerCache(EnumSyncType.CUSTOM_EFFECTS, SyncController::customEffectsNBT); - registerCache(EnumSyncType.MAGIC, SyncController::magicsNBT); - registerCache(EnumSyncType.MAGIC_CYCLE, SyncController::magicCyclesNBT); - registerCache(EnumSyncType.CUSTOM_ABILITY, SyncController::customAbilitiesNBT); - registerCache(EnumSyncType.CHAINED_ABILITY, SyncController::chainedAbilitiesNBT); - } - - public static void syncPlayer(EntityPlayerMP player) { - SyncController.syncPlayerInternal(player, true); - } - - private static void syncPlayerInternal(EntityPlayerMP player, boolean includePostPackets) { - PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); - - for (EnumSyncType 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); + // 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); } } + + // ═══════════════════════════════════════════════════════════════════════ + // Other Login syncers + // ═══════════════════════════════════════════════════════════════════════ + public static void beginLogin(EntityPlayerMP player) { playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); - PacketHandler.Instance.sendToPlayer( - new LoginPacket(getServerCacheKey(), getServerRevisionSnapshot()), - player - ); - } - - public static void handleClientRevisionReport( - EntityPlayerMP player, - String serverKey, - String previousServerKey, - Map clientRevisions - ) { - String currentServerKey = getServerCacheKey(); - PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); - - if (!currentServerKey.equals(serverKey)) { - state.reset(); - syncPlayer(player); - return; - } - - if (!currentServerKey.isEmpty()) { - if (previousServerKey == null || !currentServerKey.equals(previousServerKey)) { - state.reset(); - syncPlayer(player); - return; - } - } - - if (clientRevisions == null || clientRevisions.isEmpty()) { - debug( - "Client %s reported no cached revisions; forcing full sync", - player.getCommandSenderName() - ); - state.reset(); - } else { - state.applyHandshake(clientRevisions); - } - - syncPlayer(player); + PacketHandler.Instance.sendToPlayer(new LoginPacket(getServerCacheKey(), getServerRevisionSnapshot()), player); } private static void sendPostLoginPackets(EntityPlayerMP player) { DBCAddon.instance.syncPlayer(player); - syncPlayerData(player, false); + PlayerDataSyncHandler.syncPlayerData(player, false); PartyInfoPacket.sendPartyData(player); ProfileSharedQuestPacket.sendToPlayer(player); @@ -191,744 +131,213 @@ public class SyncController { // Overlays are not part of getSyncNBTFull(), and packets sent during // PlayerLoggedInEvent can arrive before the client entity exists. PlayerData data = PlayerData.get(player); - if (data != null) { - data.skinOverlays.updateClient(); - } + if (data != null) data.skinOverlays.updateClient(); } - private static EnumMap getServerRevisionSnapshot() { - EnumMap snapshot = new EnumMap<>(EnumSyncType.class); - for (EnumSyncType type : LOGIN_SYNC_TYPES) { + // ═══════════════════════════════════════════════════════════════════════ + // MAIN PIPELINE — completion of login handshake and full catalog sync + // ═══════════════════════════════════════════════════════════════════════ + + /** + * Runs the cached-family sync pass for one player using the currently tracked + * revision state. Used after the login handshake and by server-side callers + * that need to refresh a player's global cached sync families. + */ + public static void dispatchSyncForAllTypes(EntityPlayerMP player) { + PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); + + // Registry-driven login iteration: iterate only cached types + for (SyncType type : SyncRegistry.getLoginTypes()) { SyncCacheEntry entry = cacheEntries.get(type); - if (entry != null) { - snapshot.put(type, entry.getRevisionValue()); - } - } - return snapshot; - } + if (entry == null) continue; - private static final String SERVER_IDENTITY_KEY = UUID.randomUUID().toString(); + int currentRevision = entry.getRevisionValue(); + int lastRevision = state.getRevision(type); + if (lastRevision == currentRevision) continue; + + CachedSyncPayload payload = entry.getPayload(type); + if (payload == null) continue; - private static String getServerCacheKey() { - MinecraftServer server = MinecraftServer.getServer(); - if (server == null) { - return ""; - } - - if (!server.isDedicatedServer()) { - return ""; - } - - return SERVER_IDENTITY_KEY; - } - - public static int getCurrentRevision(EnumSyncType 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(EnumSyncType.CUSTOM_ABILITY); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CUSTOM_ABILITY, payload)); - updateAllPlayerRevisions(EnumSyncType.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(EnumSyncType.CHAINED_ABILITY); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CHAINED_ABILITY, payload)); - updateAllPlayerRevisions(EnumSyncType.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( - EnumSyncType.PLAYERDATA, - EnumSyncAction.UPDATE, - -1, - data.getSyncNBT()) - , player); - } else { - PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.PLAYERDATA, - EnumSyncAction.RELOAD, - -1, - data.getSyncNBTFull()), player); - } - } - } - - public static void syncRemove(EnumSyncType enumSyncType, int id) { - Map revisions = invalidateCaches(enumSyncType); - int revision = revisions.getOrDefault(enumSyncType, getCurrentRevision(enumSyncType)); - PacketHandler.Instance.sendToAll(new SyncPacket( - enumSyncType, - EnumSyncAction.REMOVE, - id, - revision, - new NBTTagCompound() - )); - updateAllPlayerRevisions(revisions); - } - - public static void syncAllDialogs() { - CachedSyncPayload payload = rebuildNow(EnumSyncType.DIALOG_CATEGORY); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.DIALOG_CATEGORY, payload)); - updateAllPlayerRevisions(EnumSyncType.DIALOG_CATEGORY, payload.getRevision()); - } - - public static void syncAllQuests() { - CachedSyncPayload payload = rebuildNow(EnumSyncType.QUEST_CATEGORY); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.QUEST_CATEGORY, payload)); - updateAllPlayerRevisions(EnumSyncType.QUEST_CATEGORY, payload.getRevision()); - } - - public static void syncAllWorkbenchRecipes() { - CachedSyncPayload payload = rebuildNow(EnumSyncType.WORKBENCH_RECIPES); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.WORKBENCH_RECIPES, payload)); - updateAllPlayerRevisions(EnumSyncType.WORKBENCH_RECIPES, payload.getRevision()); - } - - public static void syncAllCarpentryRecipes() { - CachedSyncPayload payload = rebuildNow(EnumSyncType.CARPENTRY_RECIPES); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CARPENTRY_RECIPES, payload)); - updateAllPlayerRevisions(EnumSyncType.CARPENTRY_RECIPES, payload.getRevision()); - } - - public static void syncAllAnvilRecipes() { - CachedSyncPayload payload = rebuildNow(EnumSyncType.ANVIL_RECIPES); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.ANVIL_RECIPES, payload)); - updateAllPlayerRevisions(EnumSyncType.ANVIL_RECIPES, payload.getRevision()); - } - - public static void syncAllCustomEffects() { - CachedSyncPayload payload = rebuildNow(EnumSyncType.CUSTOM_EFFECTS); - if (payload == null) { - return; - } - PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CUSTOM_EFFECTS, payload)); - updateAllPlayerRevisions(EnumSyncType.CUSTOM_EFFECTS, payload.getRevision()); - } - - @SideOnly(Side.CLIENT) - public static void clientSync(EnumSyncType enumSyncType, int revision, NBTTagCompound fullCompound) { - switch (enumSyncType) { - case FACTION: { - NBTTagList list = fullCompound.getTagList("Factions", 10); - for (int i = 0; i < list.tagCount(); i++) { - Faction faction = new Faction(); - faction.readNBT(list.getCompoundTagAt(i)); - FactionController.getInstance().factionsSync.put(faction.id, faction); - } - FactionController.getInstance().factions = FactionController.getInstance().factionsSync; - FactionController.getInstance().factionsSync = new HashMap(); - break; - } - case DIALOG_CATEGORY: { - if (!fullCompound.hasNoTags()) { - NBTTagList categories = fullCompound.getTagList("DialogCategories", 10); - for (int j = 0; j < fullCompound.getTagList("DialogCategories", 10).tagCount(); j++) { - NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); - if (categoryCompound.hasNoTags()) - continue; - - DialogCategory category = new DialogCategory(); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - NBTTagList dialogList = categoryCompound.getTagList("Data", 10); - if (DialogController.Instance.categoriesSync.containsKey(category.id)) { - category = DialogController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < dialogList.tagCount(); i++) { - Dialog dialog = new Dialog(); - dialog.readNBT(dialogList.getCompoundTagAt(i)); - dialog.category = category; - category.dialogs.put(dialog.id, dialog); - } - DialogController.Instance.categoriesSync.put(category.id, category); - } - } - - HashMap dialogs = new HashMap(); - for (DialogCategory category : DialogController.Instance.categoriesSync.values()) { - for (Dialog dialog : category.dialogs.values()) { - dialogs.put(dialog.id, dialog); - } - } - - DialogController.Instance.categories = DialogController.Instance.categoriesSync; - DialogController.Instance.dialogs = dialogs; - DialogController.Instance.categoriesSync = new HashMap(); - break; - } - case QUEST_CATEGORY: { - if (!fullCompound.hasNoTags()) { - NBTTagList categories = fullCompound.getTagList("QuestCategories", 10); - for (int j = 0; j < fullCompound.getTagList("QuestCategories", 10).tagCount(); j++) { - NBTTagCompound categoryCompound = categories.getCompoundTagAt(j); - if (categoryCompound.hasNoTags()) - continue; - - QuestCategory category = new QuestCategory(); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - NBTTagList questList = categoryCompound.getTagList("Data", 10); - if (QuestController.Instance.categoriesSync.containsKey(category.id)) { - category = QuestController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(categoryCompound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < questList.tagCount(); i++) { - Quest quest = new Quest(); - quest.readNBT(questList.getCompoundTagAt(i)); - quest.category = category; - category.quests.put(quest.id, quest); - } - QuestController.Instance.categoriesSync.put(category.id, category); - } - } - - HashMap quests = new HashMap(); - for (QuestCategory category : QuestController.Instance.categoriesSync.values()) { - for (Quest quest : category.quests.values()) { - quests.put(quest.id, quest); - } - } - - QuestController.Instance.categories = QuestController.Instance.categoriesSync; - QuestController.Instance.quests = quests; - QuestController.Instance.categoriesSync = new HashMap(); - break; - } - case PLAYERDATA: { - ClientCacheHandler.playerData.setSyncNBTFull(fullCompound); - break; - } - case MAGIC: { - NBTTagList list = fullCompound.getTagList("Data", 10); - MagicController mc = MagicController.getInstance(); - mc.magicSync.clear(); - for (int i = 0; i < list.tagCount(); i++) { - Magic magic = new Magic(); - magic.readNBT(list.getCompoundTagAt(i)); - mc.magicSync.put(magic.id, magic); - } - - mc.magics.clear(); - mc.magics.putAll(mc.magicSync); - mc.magicSync.clear(); - break; - } - case MAGIC_CYCLE: { - NBTTagList list = fullCompound.getTagList("Data", 10); - MagicController mc = MagicController.getInstance(); - mc.cyclesSync.clear(); - for (int i = 0; i < list.tagCount(); i++) { - MagicCycle cycle = new MagicCycle(); - cycle.readNBT(list.getCompoundTagAt(i)); - mc.cyclesSync.put(cycle.id, cycle); - } - - mc.cycles.clear(); - mc.cycles.putAll(mc.cyclesSync); - mc.cyclesSync.clear(); - break; - } - case WORKBENCH_RECIPES: { - NBTTagList list = fullCompound.getTagList("recipes", 10); - if (list == null) - return; - - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound recipeCompound = list.getCompoundTagAt(i); - RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); - recipe.readNBT(recipeCompound); - RecipeController.syncRecipes.put(recipe.id, recipe); - } - - RecipeController.reloadGlobalRecipes(RecipeController.syncRecipes); - RecipeController.syncRecipes = new HashMap(); - break; - } - case CARPENTRY_RECIPES: { - NBTTagList list = fullCompound.getTagList("recipes", 10); - if (list == null) - return; - - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound recipeCompound = list.getCompoundTagAt(i); - RecipeCarpentry recipe = RecipeCarpentry.create(recipeCompound); - recipe.readNBT(recipeCompound); - RecipeController.syncRecipes.put(recipe.id, recipe); - } - - RecipeController.Instance.carpentryRecipes = RecipeController.syncRecipes; - RecipeController.syncRecipes = new HashMap(); - break; - } - case ANVIL_RECIPES: { - NBTTagList list = fullCompound.getTagList("recipes", 10); - if (list == null) - return; - - for (int i = 0; i < list.tagCount(); i++) { - RecipeAnvil recipe = new RecipeAnvil(); - recipe.readNBT(list.getCompoundTagAt(i)); - RecipeController.syncAnvilRecipes.put(recipe.id, recipe); - } - - RecipeController.Instance.anvilRecipes = RecipeController.syncAnvilRecipes; - RecipeController.syncAnvilRecipes = new HashMap(); - break; - } - case CUSTOM_EFFECTS: { - NBTTagList list = fullCompound.getTagList("Data", 10); - CustomEffectController ce = CustomEffectController.getInstance(); - ce.customEffectsSync.clear(); - for (int i = 0; i < list.tagCount(); i++) { - CustomEffect effect = new CustomEffect(); - effect.readFromNBT(list.getCompoundTagAt(i)); - ClientCacheHandler.getImageData(effect.icon); - ce.customEffectsSync.put(effect.id, effect); - } - - ce.getCustomEffects().clear(); - ce.getCustomEffects().putAll(ce.customEffectsSync); - ce.customEffectsSync.clear(); - break; - } - case CUSTOM_ABILITY: { - NBTTagList list = fullCompound.getTagList("Data", 10); - LinkedHashMap sync = new LinkedHashMap<>(); - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound nbt = list.getCompoundTagAt(i); - String name = nbt.getString("CustomAbilityId"); - Ability ability = AbilityController.Instance.fromNBT(nbt); - if (ability != null) { - ability.setName(name); - sync.put(name, ability); - } - } - AbilityController.Instance.setCustomAbilities(sync); - break; - } - case CHAINED_ABILITY: { - NBTTagList list = fullCompound.getTagList("Data", 10); - LinkedHashMap sync = new LinkedHashMap<>(); - for (int i = 0; i < list.tagCount(); i++) { - NBTTagCompound nbt = list.getCompoundTagAt(i); - String id = nbt.getString("ChainedAbilityId"); - ChainedAbility chain = new ChainedAbility(); - chain.readNBT(nbt); - chain.setName(id); - sync.put(id, chain); - } - AbilityController.Instance.setChainedAbilities(sync); - break; + if (lastRevision != payload.getRevision()) { + PacketHandler.Instance.sendToPlayer(new SyncPacket(type, payload), player); + state.updateRevision(type, payload.getRevision()); } } - ClientCacheHandler.updateClientRevision(enumSyncType, revision); + sendPostLoginPackets(player); } /** - * Update Related Tasks + * Completes the server-side revision handshake that follows {@link LoginPacket}. + * + * The client reports which cached revisions it believes are still valid for the + * current server identity. If the reported server identity is stale or missing, + * we reset the stored state and force a full cached resync. Otherwise we seed the + * player's tracked revisions from the client report and only send outdated families. */ - @SideOnly(Side.CLIENT) - public static void clientUpdate(EnumSyncType enumSyncType, int category_id, int revision, NBTTagCompound compound) { - switch (enumSyncType) { - case FACTION: { - Faction faction = new Faction(); - faction.readNBT(compound); - FactionController.getInstance().factions.put(faction.id, faction); - break; - } - case DIALOG: { - DialogCategory category = DialogController.Instance.categories.get(category_id); - Dialog dialog = new Dialog(); - dialog.category = category; - dialog.readNBT(compound); - DialogController.Instance.dialogs.put(dialog.id, dialog); - category.dialogs.put(dialog.id, dialog); - break; - } - case QUEST: { - QuestCategory category = QuestController.Instance.categories.get(category_id); - Quest quest = new Quest(); - quest.category = category; - quest.readNBT(compound); - QuestController.Instance.quests.put(quest.id, quest); - category.quests.put(quest.id, quest); - break; - } - case QUEST_CATEGORY: { - QuestCategory category = new QuestCategory(); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - NBTTagList list = compound.getTagList("Data", 10); - if (QuestController.Instance.categoriesSync.containsKey(category.id)) { - category = QuestController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < list.tagCount(); i++) { - Quest quest = new Quest(); - quest.readNBT(list.getCompoundTagAt(i)); - quest.category = category; - category.quests.put(quest.id, quest); - } - QuestController.Instance.categories.put(category.id, category); - break; - } - case DIALOG_CATEGORY: { - DialogCategory category = new DialogCategory(); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - NBTTagList list = compound.getTagList("Data", 10); - if (DialogController.Instance.categoriesSync.containsKey(category.id)) { - category = DialogController.Instance.categoriesSync.get(category.id); - category.readSmallNBT(compound.getCompoundTag("CatNBT")); - } - for (int i = 0; i < list.tagCount(); i++) { - Dialog dialog = new Dialog(); - dialog.readNBT(list.getCompoundTagAt(i)); - dialog.category = category; - category.dialogs.put(dialog.id, dialog); - } - DialogController.Instance.categories.put(category.id, category); - break; - } - case MAGIC_CYCLE: { - MagicCycle cycle = new MagicCycle(); - cycle.readNBT(compound); - MagicController.getInstance().cycles.put(cycle.id, cycle); - break; - } - case MAGIC: { - Magic magic = new Magic(); - magic.readNBT(compound); - MagicController.getInstance().magics.put(magic.id, magic); - break; - } - case PLAYERDATA: { - ClientCacheHandler.playerData.setSyncNBT(compound); - break; - } - case CUSTOM_EFFECTS: { - CustomEffect effect = new CustomEffect(); - effect.readFromNBT(compound); - ClientCacheHandler.getImageData(effect.icon); + public static void completeLoginRevisionHandshake(EntityPlayerMP player, String serverKey, String previousServerKey, + Map clientRevisions) { + String currentServerKey = getServerCacheKey(); + PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); - CustomEffectController.Instance.getCustomEffects().put(effect.id, effect); - break; - } - } + boolean serverIdentityMismatch = !currentServerKey.equals(serverKey); + boolean previousServerMismatch = !currentServerKey.isEmpty() && (previousServerKey == null || !currentServerKey.equals( + previousServerKey)); + boolean canReuseClientRevisions = clientRevisions != null && !clientRevisions.isEmpty(); - ClientCacheHandler.updateClientRevision(enumSyncType, revision); + // Any server identity mismatch means the client's cached revision map is unsafe to reuse. + if (serverIdentityMismatch || previousServerMismatch || !canReuseClientRevisions) + state.reset(); + else // The client and server agree on identity, so seed the stored state from the report. + state.applyHandshake(clientRevisions); + + dispatchSyncForAllTypes(player); } - public static void syncUpdate(EnumSyncType type, int cat, NBTTagCompound compound) { - Map revisions = invalidateCaches(type); - int revision = revisions.getOrDefault(type, getCurrentRevision(type)); - PacketHandler.Instance.sendToAll(new SyncPacket( - type, - EnumSyncAction.UPDATE, - cat, - revision, - compound - )); + + // ═══════════════════════════════════════════════════════════════════════ + // Generic dispatcher — called from outside SyncController + // ═══════════════════════════════════════════════════════════════════════ + + /** + * 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 = rebuildCache(type); + if (payload == null) return; + + PacketHandler.Instance.sendToAll(new SyncPacket(type, payload)); + updateAllPlayerRevisions(type, payload.getRevision()); + } + + // ═══════════════════════════════════════════════════════════════════════ + // Player data sync + // ═══════════════════════════════════════════════════════════════════════ + + public static void syncPlayerData(SyncType type, NBTTagCompound compound, boolean fullData, EntityPlayerMP player) { + Map revisions = invalidateCaches(type); + int revision = getRequestedInvalidationRevision(type, revisions); + PacketHandler.Instance.sendToPlayer( + new SyncPacket(type, EnumSyncAction.UPDATE, fullData ? 1 : 0, null, revision, compound), player); + updateAllPlayerRevisions(revisions); + } + + // ═══════════════════════════════════════════════════════════════════════ + // Server-side dispatchers — registry-driven + // ═══════════════════════════════════════════════════════════════════════ + public static void syncUpdate(SyncType type, NBTTagCompound compound) { + Map revisions = invalidateCaches(type); + int revision = getRequestedInvalidationRevision(type, revisions); + PacketHandler.Instance.sendToAll(new SyncPacket(type, EnumSyncAction.UPDATE, -1, null, revision, compound)); + updateAllPlayerRevisions(revisions); + } + + public static void syncUpdate(SyncType type, String key, NBTTagCompound compound) { + Map revisions = invalidateCaches(type); + int revision = getRequestedInvalidationRevision(type, revisions); + PacketHandler.Instance.sendToAll(new SyncPacket(type, EnumSyncAction.UPDATE, -1, key, revision, compound)); + updateAllPlayerRevisions(revisions); + } + + public static void syncUpdate(SyncType type, int cat, NBTTagCompound compound) { + Map revisions = invalidateCaches(type); + int revision = getRequestedInvalidationRevision(type, revisions); + PacketHandler.Instance.sendToAll(new SyncPacket(type, EnumSyncAction.UPDATE, cat, null, revision, compound)); + updateAllPlayerRevisions(revisions); + } + + public static void syncRemove(SyncType syncType, String key) { + Map revisions = invalidateCaches(syncType); + int revision = getRequestedInvalidationRevision(syncType, revisions); + PacketHandler.Instance.sendToAll(new SyncPacket(syncType, EnumSyncAction.REMOVE, -1, key, revision, null)); + updateAllPlayerRevisions(revisions); + } + + public static void syncRemove(SyncType syncType, int id) { + Map revisions = invalidateCaches(syncType); + int revision = getRequestedInvalidationRevision(syncType, revisions); + PacketHandler.Instance.sendToAll(new SyncPacket(syncType, EnumSyncAction.REMOVE, id, null, revision, null)); updateAllPlayerRevisions(revisions); } - 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; - } + // ═══════════════════════════════════════════════════════════════════════ + // Client-side handlers — registry-driven handler delegation + // ═══════════════════════════════════════════════════════════════════════ - 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())); + @SideOnly(Side.CLIENT) + public static void clientHandleAll(SyncType syncType, int revision, NBTTagCompound fullCompound) { + SyncHandler handler = SyncRegistry.getHandler(syncType); + if (handler == null) return; + try { + handler.clientHandleReload(fullCompound); + } catch (Exception e) { + LogWriter.error("[SyncController] Failed to handle RELOAD for sync type " + syncType, e); } - dialogCompound.setTag("Data", dialogList); - dialogCompound.setTag("CatNBT", dialogCategory.writeSmallNBT(new NBTTagCompound())); - return dialogCompound; + ClientCacheHandler.updateClientRevision(syncType, revision); + } @SideOnly(Side.CLIENT) - public static void clientSyncRemove(EnumSyncType enumSyncType, int id, int revision) { - switch (enumSyncType) { - case FACTION: - FactionController.getInstance().factions.remove(id); - break; - case DIALOG: - Dialog dialog = DialogController.Instance.dialogs.remove(id); - if (dialog != null) { - dialog.category.dialogs.remove(id); - } - break; - case DIALOG_CATEGORY: - DialogCategory dialogCategory = DialogController.Instance.categories.remove(id); - if (dialogCategory != null) { - DialogController.Instance.dialogs.keySet().removeAll(dialogCategory.dialogs.keySet()); - } - break; - case QUEST: - Quest quest = QuestController.Instance.quests.remove(id); - if (quest != null) { - quest.category.quests.remove(id); - } - break; - case QUEST_CATEGORY: - QuestCategory questCategory = QuestController.Instance.categories.remove(id); - if (questCategory != null) { - QuestController.Instance.quests.keySet().removeAll(questCategory.quests.keySet()); - } - break; - case MAGIC: - for (MagicCycle cycle : MagicController.getInstance().cycles.values()) { - cycle.associations.remove(id); - } - MagicController.getInstance().cycles.remove(id); - break; - case MAGIC_CYCLE: - MagicController.getInstance().cycles.remove(id); - break; - case CUSTOM_EFFECTS: - CustomEffectController.Instance.getCustomEffects().remove(id); - break; + public static void clientHandleUpdate(SyncType syncType, int id, String key, int revision, NBTTagCompound compound) { + SyncHandler handler = SyncRegistry.getHandler(syncType); + if (handler == null) return; + try { + handler.clientHandleUpdate(compound); + handler.clientHandleUpdate(compound, id); + handler.clientHandleUpdate(compound, key); + } catch (Exception e) { + LogWriter.error("[SyncController] Failed to handle UPDATE for sync type " + syncType, e); } - - ClientCacheHandler.updateClientRevision(enumSyncType, 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); + + ClientCacheHandler.updateClientRevision(syncType, revision); + + // New GUI data handling approach + IGuiData.notifyUpdate(compound, key, id); } @SideOnly(Side.CLIENT) - public static void clientSyncEffects(NBTTagCompound compound) { - PlayerData playerData = PlayerData.get(Minecraft.getMinecraft().thePlayer); - if (playerData != null) { - playerData.setPlayerEffects(compound); + public static void clientHandleRemove(SyncType syncType, int id, String key, int revision) { + SyncHandler handler = SyncRegistry.getHandler(syncType); + if (handler == null) return; + try { + handler.clientHandleRemove(id); + handler.clientHandleRemove(key); + } catch (Exception e) { + LogWriter.error("[SyncController] Failed to handle REMOVE for sync type " + syncType, e); } + + ClientCacheHandler.updateClientRevision(syncType, revision); + + // New GUI data handling approach + IGuiData.notifyRemove(key, id); } - private static void registerCache(EnumSyncType type, Supplier supplier) { - cacheEntries.put(type, new SyncCacheEntry(type, supplier)); + // ═══════════════════════════════════════════════════════════════════════ + // Cache and revision internals + // ═══════════════════════════════════════════════════════════════════════ + + private static void registerCache(SyncType type, Supplier supplier) { + cacheEntries.put(type, new SyncCacheEntry(supplier)); } - private static CachedSyncPayload rebuildNow(EnumSyncType type) { + private static CachedSyncPayload rebuildCache(SyncType type) { SyncCacheEntry entry = cacheEntries.get(type); - if (entry == null) { - return null; - } + if (entry == null) return null; + entry.invalidate(); return entry.getPayload(type); } - public static Map invalidateCaches(EnumSyncType type) { - EnumSet targets = getInvalidationTargets(type); - EnumMap revisions = new EnumMap<>(EnumSyncType.class); - for (EnumSyncType target : targets) { + /** + * Registry-driven invalidation: delegates to handler-owned + * invalidation targets via {@link SyncRegistry#getInvalidationTargets}. + */ + private static Map invalidateCaches(SyncType type) { + Set targets = SyncRegistry.getInvalidationTargets(type); + Map revisions = new LinkedHashMap<>(); + for (SyncType target : targets) { SyncCacheEntry entry = cacheEntries.get(target); if (entry != null) { int newRevision = entry.invalidate(); @@ -938,57 +347,55 @@ public class SyncController { return revisions; } - private static EnumSet getInvalidationTargets(EnumSyncType type) { - switch (type) { - case DIALOG: - case DIALOG_CATEGORY: - return EnumSet.of(EnumSyncType.DIALOG_CATEGORY); - case QUEST: - case QUEST_CATEGORY: - return EnumSet.of(EnumSyncType.QUEST_CATEGORY); - case FACTION: - return EnumSet.of(EnumSyncType.FACTION); - case MAGIC: - return EnumSet.of(EnumSyncType.MAGIC); - case MAGIC_CYCLE: - return EnumSet.of(EnumSyncType.MAGIC_CYCLE); - case WORKBENCH_RECIPES: - return EnumSet.of(EnumSyncType.WORKBENCH_RECIPES); - case CARPENTRY_RECIPES: - return EnumSet.of(EnumSyncType.CARPENTRY_RECIPES); - case ANVIL_RECIPES: - return EnumSet.of(EnumSyncType.ANVIL_RECIPES); - case CUSTOM_EFFECTS: - return EnumSet.of(EnumSyncType.CUSTOM_EFFECTS); - case CUSTOM_ABILITY: - return EnumSet.of(EnumSyncType.CUSTOM_ABILITY); - case CHAINED_ABILITY: - return EnumSet.of(EnumSyncType.CHAINED_ABILITY); - default: - return EnumSet.noneOf(EnumSyncType.class); - } + private static int getRequestedInvalidationRevision(SyncType type, Map revisions) { + Integer revision = revisions.get(type); + return revision != null ? revision : getCurrentServerRevision(type); } - private static void updateAllPlayerRevisions(EnumSyncType type, int revision) { - if (revision < 0) { - return; + public static int getCurrentServerRevision(SyncType type) { + SyncCacheEntry entry = cacheEntries.get(type); + return entry == null ? -1 : entry.getRevisionValue(); + } + + private static Map getServerRevisionSnapshot() { + // Registry-driven: iterate login types from registry + Map snapshot = new LinkedHashMap<>(); + for (SyncType type : SyncRegistry.getLoginTypes()) { + SyncCacheEntry entry = cacheEntries.get(type); + if (entry != null) + snapshot.put(type, entry.getRevisionValue()); } - for (PlayerSyncState state : playerSyncState.values()) { + return snapshot; + } + + 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()); } } - 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()); - } - } + private static String getServerCacheKey() { + MinecraftServer server = MinecraftServer.getServer(); + if (server == null || !server.isDedicatedServer()) return ""; + + return SERVER_IDENTITY_KEY; } + + // ═══════════════════════════════════════════════════════════════════════ + // Inner classes — cache payload and player revision state + // ═══════════════════════════════════════════════════════════════════════ + public static final class CachedSyncPayload { private final int revision; private final byte[] payload; @@ -1019,20 +426,19 @@ public class SyncController { private int revision = 0; private boolean dirty = true; - private SyncCacheEntry(EnumSyncType type, Supplier supplier) { + private SyncCacheEntry(Supplier supplier) { this.supplier = supplier; } - private synchronized CachedSyncPayload getPayload(EnumSyncType requestedType) { - if (!dirty && payload != null) { - return payload; - } + 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 { + NBTTagCompound data = supplier.get(); + buffer.writeInt(requestedType.ordinal()); buffer.writeInt(EnumSyncAction.RELOAD.ordinal()); buffer.writeInt(-1); @@ -1041,8 +447,9 @@ public class SyncController { bytes = new byte[buffer.readableBytes()]; buffer.readBytes(bytes); - } catch (IOException e) { - throw new RuntimeException("Failed to serialize sync payload for " + requestedType, e); + } catch (Exception e) { + LogWriter.error("Failed to serialize sync payload for " + requestedType, e); + return null; } finally { buffer.release(); } @@ -1053,6 +460,23 @@ public class SyncController { return payload; } + private static byte[][] splitIntoChunks(byte[] payload) { + int totalSize = payload.length; + int chunkCount = (totalSize + LargeAbstractPacket.CHUNK_SIZE - 1) / LargeAbstractPacket.CHUNK_SIZE; + if (chunkCount <= 0) + chunkCount = 1; + + byte[][] chunks = new byte[chunkCount][]; + for (int i = 0; i < chunkCount; i++) { + int offset = i * LargeAbstractPacket.CHUNK_SIZE; + int length = Math.min(LargeAbstractPacket.CHUNK_SIZE, totalSize - offset); + byte[] chunk = new byte[length]; + System.arraycopy(payload, offset, chunk, 0, length); + chunks[i] = chunk; + } + return chunks; + } + private synchronized int invalidate() { dirty = true; payload = null; @@ -1069,50 +493,30 @@ public class SyncController { payload = null; revision = 0; } - - private static byte[][] splitIntoChunks(byte[] payload) { - int totalSize = payload.length; - int chunkCount = (totalSize + LargeAbstractPacket.CHUNK_SIZE - 1) / LargeAbstractPacket.CHUNK_SIZE; - if (chunkCount <= 0) { - chunkCount = 1; - } - byte[][] chunks = new byte[chunkCount][]; - for (int i = 0; i < chunkCount; i++) { - int offset = i * LargeAbstractPacket.CHUNK_SIZE; - int length = Math.min(LargeAbstractPacket.CHUNK_SIZE, totalSize - offset); - byte[] chunk = new byte[length]; - System.arraycopy(payload, offset, chunk, 0, length); - chunks[i] = chunk; - } - return chunks; - } } private static final class PlayerSyncState { - private final EnumMap revisions = new EnumMap<>(EnumSyncType.class); + private final Map revisions = new HashMap<>(); private PlayerSyncState(UUID ignored) { } - private synchronized int getRevision(EnumSyncType type) { + private synchronized int getRevision(SyncType type) { Integer value = revisions.get(type); return value == null ? -1 : value; } - private synchronized void updateRevision(EnumSyncType type, int revision) { + private synchronized void updateRevision(SyncType type, int revision) { revisions.put(type, revision); } - private synchronized void applyHandshake(Map incoming) { - if (incoming == null || incoming.isEmpty()) { - return; - } + 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/SyncControllerOld.java b/src/main/java/kamkeel/npcs/controllers/SyncControllerOld.java new file mode 100644 index 000000000..05f37e8e9 --- /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/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 05049d8bb..d944f7535 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -840,19 +840,7 @@ public abstract class Ability implements IAbility, IAbilityAction { return sb.toString(); }).tab("General")); - defs.add(FieldDef.subGuiField("ability.magic.editor", - () -> new SubGuiAbilityMagic(magicData.copy()), - gui -> { - MagicData result = ((SubGuiAbilityMagic) gui).magicData; - magicData.getMagics().clear(); - for (Map.Entry e : result.getMagics().entrySet()) { - magicData.getMagics().put(e.getKey(), e.getValue()); - } - }) - .buttonLabel(() -> magicData.isEmpty() - ? StatCollector.translateToLocal("ability.magic.usesCaster") - : magicData.getMagics().size() + " Magic(s)") - .tab("General")); + defs.add(SubGuiAbilityMagic.createMagicField(magicData)); } // ── Target tab ─────────────────────────────────────────────── @@ -1062,10 +1050,11 @@ public abstract class Ability implements IAbility, IAbilityAction { return null; } - telegraph.setDurationTicks(windUpTicks); + int windUp = getWindUpTicks(); + telegraph.setDurationTicks(windUp); telegraph.setColor(windUpColor); telegraph.setWarningColor(activeColor); - telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setWarningStartTick(Math.max(5, windUp / 4)); telegraph.setHeightOffset(telegraphHeightOffset); TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); @@ -1183,7 +1172,9 @@ public abstract class Ability implements IAbility, IAbilityAction { this.burstIndex = 0; this.burstEntities.clear(); - if (windUpTicks <= 0) { + // Use the effective windup, not the raw field: when the windup is synced to an + // animation the raw field is not the duration that actually applies. + if (getWindUpTicks() <= 0) { this.phase = AbilityPhase.ACTIVE; } else { this.phase = AbilityPhase.WINDUP; @@ -1205,7 +1196,7 @@ public abstract class Ability implements IAbility, IAbilityAction { switch (phase) { case WINDUP: - if (currentTick >= windUpTicks) { + if (currentTick >= getWindUpTicks()) { phase = AbilityPhase.ACTIVE; currentTick = 0; return true; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHasEffect.java b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHasEffect.java index eca7aded0..a608a92e6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHasEffect.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHasEffect.java @@ -34,27 +34,15 @@ public class ConditionHasEffect extends AbilityCondition { return CustomEffectController.getInstance().hasEffect(player, effectId, effectIndex); } + public void setEffect(int effectId, int effectIndex) { + this.effectId = effectId; + this.effectIndex = effectIndex; + } + @SideOnly(Side.CLIENT) @Override public void getConditionDefinitions(List defs) { - defs.add(FieldDef.subGuiField("condition.select_effect", - () -> new SubGuiCustomEffectSelect(effectId, effectIndex), - gui -> { - SubGuiCustomEffectSelect sel = (SubGuiCustomEffectSelect) gui; - if (sel.getSelectedEffectId() >= 0) { - effectId = sel.getSelectedEffectId(); - effectIndex = sel.getSelectedIndex(); - } - }) - .buttonLabel(() -> { - if (effectId < 0) return "None"; - CustomEffect effect = getEffect(); - return effect != null ? effect.getName() : "ID:" + effectId; - }) - .clearable(() -> { - effectId = -1; - effectIndex = 0; - })); + defs.add(SubGuiCustomEffectSelect.createSelectField(this)); } @SideOnly(Side.CLIENT) @@ -74,7 +62,7 @@ public class ConditionHasEffect extends AbilityCondition { return effectId >= 0; } - private CustomEffect getEffect() { + public CustomEffect getEffect() { HashMap map = CustomEffectController.getInstance().getEffectMap(effectIndex); return map != null ? map.get(effectId) : null; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java index 2955b7610..ac8a05272 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java @@ -38,20 +38,7 @@ public class ConditionQuestCompleted extends AbilityCondition { @SideOnly(Side.CLIENT) @Override public void getConditionDefinitions(List defs) { - defs.add(FieldDef.subGuiField("condition.select_quest", - () -> new GuiQuestSelection(questId), - gui -> { - GuiQuestSelection sel = (GuiQuestSelection) gui; - if (sel.selectedQuest != null) { - questId = sel.selectedQuest.id; - } - }) - .buttonLabel(() -> { - if (questId < 0) return "None"; - Quest q = QuestController.Instance.quests.get(questId); - return q != null ? q.title : "ID:" + questId; - }) - .clearable(() -> questId = -1)); + defs.add(GuiQuestSelection.createConditionField(this)); } @SideOnly(Side.CLIENT) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java b/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java index d691a6d18..2a0b90344 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.extender; import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition; import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.entity.EntityEnergyBarrier; import net.minecraft.entity.EntityLivingBase; /** @@ -108,6 +109,21 @@ public interface IAbilityExtender { return baseHealth; } + /** + * Called when a barrier entity takes a melee hit, allowing extenders to scale the damage. + * A barrier is a plain Entity, so no LivingHurtEvent fires for it and an extender has no + * other opportunity to scale melee against it the way it scales {@link #modifyBarrierHealth}. + * Cumulative: all extenders apply in sequence, each receiving the previous output. + * + * @param barrier The barrier being hit + * @param attacker The entity dealing the hit + * @param baseDamage The damage after the barrier's own melee multiplier + * @return The modified damage value + */ + default float modifyBarrierMeleeDamage(EntityEnergyBarrier barrier, EntityLivingBase attacker, float baseDamage) { + return baseDamage; + } + /** * Called when an ability is about to heal an entity. * Return true to handle the healing (skip default entity.heal()). diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index c88d63c30..d5c293c64 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -43,7 +43,7 @@ public class AbilityCharge extends AbilityMovement implements IAbilityCharge { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 20.0f; this.minRange = 4.0f; - this.lockMovement = LockMode.WINDUP; + this.lockMovement = LockMode.WINDUP_AND_ACTIVE; this.cooldownTicks = 0; this.windUpTicks = 20; // LINE telegraph showing charge path @@ -58,6 +58,11 @@ public class AbilityCharge extends AbilityMovement implements IAbilityCharge { }; } + @Override + public boolean hasAbilityMovement() { + return true; // This ability drives the NPC's motion itself + } + @Override public boolean isTargetingModeLocked() { return true; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java index f03543028..64fb10743 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java @@ -3,14 +3,21 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.addon.DBCAddon; +import noppes.npcs.AbstractDataAbilities; import noppes.npcs.controllers.AnimationController; +import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.enums.UserType; import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.DamageSource; import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.api.ability.type.IAbilityCounter; import noppes.npcs.client.gui.builder.FieldDef; +import noppes.npcs.entity.EntityNPCInterface; import java.util.Arrays; import java.util.List; @@ -25,6 +32,11 @@ public class AbilityCounter extends AbilityDefend implements IAbilityCounter { private int counterAnimationId = -1; private String counterAnimationName = "Ability_Guard_Counter"; + // True only while this counter is synchronously dispatching its retaliation damage. + // Another counter that is hit by that retaliation inspects this flag on the source + // entity's active counter to avoid reacting and creating infinite mutual retaliation. + private transient boolean retaliating; + public AbilityCounter() { this.typeId = "ability.cnpc.counter"; this.name = "Counter"; @@ -60,6 +72,35 @@ public class AbilityCounter extends AbilityDefend implements IAbilityCounter { // DEFEND HOOKS // ═══════════════════════════════════════════════════════════════════ + @Override + protected boolean isValidDamageSource(DamageSource source) { + if (!super.isValidDamageSource(source)) { + return false; + } + // Break counter-vs-counter recursion: if the hit is itself a retaliation + // from another counter that is mid-dispatch, don't counter it back. + Entity sourceEntity = source.getEntity(); + AbstractDataAbilities abilities = getAbilitiesOf(sourceEntity); + if (abilities != null) { + Ability current = abilities.getCurrentAbility(); + if (current instanceof AbilityCounter && ((AbilityCounter) current).retaliating) { + return false; + } + } + return true; + } + + private static AbstractDataAbilities getAbilitiesOf(Entity entity) { + if (entity instanceof EntityPlayer) { + PlayerData pData = PlayerData.get((EntityPlayer) entity); + return pData != null ? pData.abilityData : null; + } + if (entity instanceof EntityNPCInterface) { + return ((EntityNPCInterface) entity).abilities; + } + return null; + } + @Override protected float performDefend(EntityLivingBase attacker, float amount) { // For PERCENT mode with DBC, use the full DBC-calculated damage, not vanilla base @@ -78,13 +119,24 @@ public class AbilityCounter extends AbilityDefend implements IAbilityCounter { // Counter-attack through the standard ability damage pipeline. // DBCAbilityExtender handles DBC stat routing (ignore flags, scaling, etc.) automatically. - if (caster != null && attacker.isEntityAlive() && damage > 0) { - applyAbilityDamage(caster, attacker, damage, 0); + if (caster != null && attacker != null && attacker.isEntityAlive() && damage > 0) { + retaliating = true; + try { + applyAbilityDamage(caster, attacker, damage, 0); + } finally { + retaliating = false; + } } return 0; } + @Override + public void reset() { + super.reset(); + retaliating = false; + } + // ═══════════════════════════════════════════════════════════════════ // OVERRIDES // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java index 376c4ad4c..4826daee4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java @@ -74,6 +74,12 @@ public abstract class AbilityDefend extends Ability implements IAbilityDefend { hitCount = 0; lastAttacker = null; lastDamageTaken = 0.0f; + // NPCs reuse one ability instance, so every transient field has to be cleared here. + // A leftover pendingCompletion would end the next activation on its first tick. + pendingDefendAnimation = null; + defendAnimEndTick = -1; + lastDefendTick = -1; + pendingCompletion = false; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 47f9156a8..2086d4e8f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -107,7 +107,7 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { readZoneNBT(nbt, 300); this.radius = nbt.getFloat("radius"); this.damagePerSecond = nbt.getFloat("damagePerSecond"); - this.damageInterval = nbt.getInteger("damageInterval"); + setDamageInterval(nbt.getInteger("damageInterval")); this.affectsCaster = nbt.getBoolean("affectsCaster"); } @@ -138,7 +138,7 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { } public void setDamageInterval(int damageInterval) { - this.damageInterval = damageInterval; + this.damageInterval = Math.max(EntityAbilityZone.MIN_DAMAGE_INTERVAL, damageInterval); } public boolean isAffectsCaster() { @@ -171,6 +171,7 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { FieldDef.row( FieldDef.floatField("gui.dps", this::getDamagePerSecond, this::setDamagePerSecond), FieldDef.intField("gui.interval", this::getDamageInterval, this::setDamageInterval) + .range(EntityAbilityZone.MIN_DAMAGE_INTERVAL, 2000) ), FieldDef.boolField("ability.affectsCaster", this::isAffectsCaster, this::setAffectsCaster) .hover("ability.hover.affectsCaster"), diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 104209eeb..3caabc678 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -97,6 +97,15 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { dz /= len; } + // A rotation-locked NPC cannot turn to follow the target, so it can only hit what its + // frozen heading still covers - the same cone the player hit scan uses. + if (isRotationLockedForCurrentPhase()) { + Vec3 look = caster.getLookVec(); + if (look != null && (dx * look.xCoord + dy * look.yCoord + dz * look.zCoord) < 0.95) { + return; + } + } + applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz); world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 5e508069a..1c40b202e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -300,14 +300,7 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap { @SideOnly(Side.CLIENT) @Override protected void addPresetFieldDef(List defs) { - defs.add(FieldDef.subGuiField("gui.applyPreset", - SubGuiTrapPresetSelector::new, - gui -> { - SubGuiTrapPresetSelector selector = (SubGuiTrapPresetSelector) gui; - if (selector.selectedPreset != null) { - applyTrapPresetDefaults(selector.selectedPreset); - } - })); + defs.add(SubGuiTrapPresetSelector.createPresetField(this::applyTrapPresetDefaults)); } @SideOnly(Side.CLIENT) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java index 968ff03ba..a2f9fdeb7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java @@ -690,14 +690,7 @@ public abstract class AbilityZone extends Ability { @SideOnly(Side.CLIENT) protected void addPresetFieldDef(List defs) { - defs.add(FieldDef.subGuiField("gui.applyPreset", - SubGuiZonePresetSelector::new, - gui -> { - SubGuiZonePresetSelector selector = (SubGuiZonePresetSelector) gui; - if (selector.selectedPreset != null) { - applyPresetDefaults(selector.selectedPreset); - } - })); + defs.add(SubGuiZonePresetSelector.createPresetField(this::applyPresetDefaults)); } @SideOnly(Side.CLIENT) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java index 7e7986d20..723781d36 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java @@ -248,7 +248,12 @@ public abstract class AbilityEnergyProjectile * NPCs use their resolved target so launch math can track the target immediately. */ protected EntityLivingBase getLaunchTarget(EntityLivingBase caster, EntityLivingBase target) { - return isPlayerCaster(caster) ? null : target; + if (isPlayerCaster(caster)) return null; + // A rotation-locked NPC cannot turn, so launching from the target's position would let + // it fire a perfectly aimed shot regardless of where it is facing. Fall back to the + // look vector, which reflects the frozen heading. + if (isRotationLockedForCurrentPhase()) return null; + return target; } /** diff --git a/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java b/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java index f7d6ded84..ccf6ecac7 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.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..35f863c35 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/SyncHandler.java @@ -0,0 +1,167 @@ +package kamkeel.npcs.controllers.sync; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.nbt.NBTTagCompound; + +import java.util.HashSet; +import java.util.Set; + +/** + * Contract for a synchronized data type. Implementors handle both + * server-side serialization and client-side deserialization for their + * specific data type. + * + *

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

+ * + *

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

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

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

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

Implementations should: + *

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

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

+ * + * @param compound the serialized single object + */ + @SideOnly(Side.CLIENT) + default void clientHandleUpdate(NBTTagCompound compound) { + // No-op: type does not support individual updates + } + + /** + * Handle an UPDATE action on the client. + * Deserialize a single object and insert/replace it in the client-side data. + * + *

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

+ * + * @param compound the serialized single object + * @param categoryKey the parent category key (null if not applicable) + */ + @SideOnly(Side.CLIENT) + default void clientHandleUpdate(NBTTagCompound compound, String categoryKey) { + // No-op: type does not support individual updates + } + + + /** + * Handle an UPDATE action on the client. Deserialize a single + * entity and insert/replace it in the client-side data. + * + *

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(String key) { + } + + /** + * 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..1260d0ab1 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/SyncRegistry.java @@ -0,0 +1,69 @@ +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 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 (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; + } + + + 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..b92277c31 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/sync/handlers/PlayerDataSyncHandler.java @@ -0,0 +1,55 @@ +package kamkeel.npcs.controllers.sync.handlers; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.sync.SyncHandler; +import kamkeel.npcs.network.enums.SyncType; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.client.ClientCacheHandler; +import noppes.npcs.controllers.data.PlayerData; + +/** + * 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 { + + public static void syncPlayerData(EntityPlayerMP player, boolean update) { + PlayerData data = PlayerData.get(player); + if (data != null) + SyncController.syncPlayerData(SyncType.PLAYERDATA, update ? data.getSyncNBTFull() : data.getSyncNBT(), update, player); + } + + // ========== SERVER-SIDE ========== + + /** + * PLAYERDATA is not a global cached type — it is per-player. + */ + @Override + public NBTTagCompound serializeAll() { + return null; + } + + @SideOnly(Side.CLIENT) + @Override + public void clientHandleUpdate(NBTTagCompound compound, int categoryId) { + if (categoryId == 1) ClientCacheHandler.playerData.setSyncNBTFull(compound); + else 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/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 546457f4d..f8f5d44f5 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -264,18 +264,21 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { Vec3 anchorPos = AnchorPointHelper.calculateAnchorPosition(livingOwner, anchorData); setPosition(anchorPos.xCoord, anchorPos.yCoord, anchorPos.zCoord); - Entity targetEntity = getTargetEntity(); - if (targetEntity instanceof EntityLivingBase && targetEntity.isEntityAlive()) { - EntityLivingBase target = (EntityLivingBase) targetEntity; - double dx = target.posX - anchorPos.xCoord; - double dy = (target.posY + target.getEyeHeight() - 0.4) - anchorPos.yCoord; - double dz = target.posZ - anchorPos.zCoord; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - if (len > 0.0001) { - dirX = dx / len; - dirY = dy / len; - dirZ = dz / len; - } + // Converge on the point the owner is aiming at, not on the target itself. Aiming + // straight at the target would re-derive the direction every tick and ignore the + // rotation the owner is actually allowed to reach, which is what track speed and + // rotation lock constrain. + double aimX = livingOwner.posX + dirX * maxLength; + double aimY = livingOwner.posY + livingOwner.getEyeHeight() + dirY * maxLength; + double aimZ = livingOwner.posZ + dirZ * maxLength; + double dx = aimX - anchorPos.xCoord; + double dy = aimY - anchorPos.yCoord; + double dz = aimZ - anchorPos.zCoord; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0.0001) { + dirX = dx / len; + dirY = dy / len; + dirZ = dz / len; } } else { Vec3 direction = Vec3.createVectorHelper(dirX, dirY, dirZ); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java index 7913386c6..529d59d74 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java @@ -155,6 +155,9 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD // HAZARD-SPECIFIC PROPERTIES // ═══════════════════════════════════════════════════════════════════ + /** Floor on how often a hazard pulses, so a small interval cannot turn into a per-tick scan. */ + public static final int MIN_DAMAGE_INTERVAL = 5; + private float damagePerSecond = 1.0f; private int damageInterval = 20; private boolean ignoreIFrames = false; @@ -291,7 +294,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD zone.shape = shape; zone.radius = radius; zone.damagePerSecond = damagePerSecond; - zone.damageInterval = damageInterval; + zone.damageInterval = Math.max(MIN_DAMAGE_INTERVAL, damageInterval); zone.ignoreIFrames = ignoreIFrames; zone.affectsCaster = affectsCaster; zone.durationTicks = durationTicks; @@ -309,7 +312,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD zone.effects.add(e.copy()); } } - zone.ticksSinceDamage = damageInterval; return zone; } @@ -518,7 +520,9 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD if (!isInZone(entity)) continue; if (damagePerSecond > 0) { - applyDamage(entity, owner, damagePerSecond); + // The configured value is damage per second, so a pulse deals the share of a + // second it covers. Applying it whole made the real rate depend on the interval. + applyDamage(entity, owner, damagePerSecond * (damageInterval / 20.0f)); } applyEffects(entity); @@ -815,7 +819,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD public float getDamagePerSecond() { return damagePerSecond; } public void setDamagePerSecond(float dps) { this.damagePerSecond = dps; } public int getDamageInterval() { return damageInterval; } - public void setDamageInterval(int ticks) { this.damageInterval = ticks; } + public void setDamageInterval(int ticks) { this.damageInterval = Math.max(MIN_DAMAGE_INTERVAL, ticks); } public boolean isAffectsCaster() { return affectsCaster; } public void setAffectsCaster(boolean affects) { this.affectsCaster = affects; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java index 4ccb01269..da9edb676 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java @@ -1,5 +1,7 @@ package kamkeel.npcs.entity; +import kamkeel.npcs.addon.DBCAddon; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData; import kamkeel.npcs.network.packets.data.energy.BarrierClientSyncPacket; @@ -15,6 +17,7 @@ import net.minecraft.util.DamageSource; import net.minecraft.world.World; import noppes.npcs.CustomNpcs; import noppes.npcs.EventHooks; +import noppes.npcs.config.ConfigMain; import noppes.npcs.controllers.data.MagicData; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; @@ -114,6 +117,8 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { protected EnergyBarrierData barrierData = new EnergyBarrierData(); protected float currentHealth; protected int ticksAlive = 0; + /** Absolute world time the barrier expires at, so ticks it misses cannot extend it. */ + protected long barrierDeathTime = -1; // ==================== DATA WATCHER INDICES ==================== protected static final int DW_HEALTH_PERCENT = 21; @@ -436,8 +441,19 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { float damage = amount * barrierData.meleeDamageMultiplier; - // Apply magic interactions from attacker's magic vs barrier's magic Entity attacker = source.getEntity(); + + // A barrier is a plain Entity, so no LivingHurtEvent fires for it and nothing scales the + // hit the way it would against a living target - vanilla hands us the bare attackDamage + // attribute. Run the scaling here instead, or the barrier only ever takes a point of it. + if (attacker instanceof EntityLivingBase) { + damage = AbilityController.Instance.fireModifyBarrierMeleeDamage(this, (EntityLivingBase) attacker, damage); + } + if (attacker instanceof EntityPlayer && ConfigMain.AttributesEnabled && !DBCAddon.IsAvailable()) { + damage = AttributeAttackUtil.calculateOutgoing((EntityPlayer) attacker, damage); + } + + // Apply magic interactions from attacker's magic vs barrier's magic if (attacker != null) { MagicData attackerMagic = null; if (attacker instanceof EntityPlayer) { @@ -510,15 +526,16 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { } } - // Duration check - if (barrierData.useDuration && ticksAlive >= barrierData.durationTicks) { - onBarrierDestroyed(); - this.setDead(); - return true; + // Set the deadline on the first tick after charging, so lifetime starts when the + // barrier is actually up. Counting elapsed ticks instead would let any tick the + // entity misses - chunk unload, for one - extend it past its configured duration. + if (barrierDeathTime < 0 && !isCharging()) { + barrierDeathTime = worldObj.getTotalWorldTime() + + (barrierData.useDuration ? barrierData.durationTicks : BARRIER_HARD_LIFETIME_CAP); } - // Absolute hard lifetime cap (safety net for barriers with no duration) - if (ticksAlive > BARRIER_HARD_LIFETIME_CAP) { + // Duration check, and the hard lifetime cap for barriers with no duration + if (barrierDeathTime >= 0 && worldObj.getTotalWorldTime() >= barrierDeathTime) { onBarrierDestroyed(); this.setDead(); return true; @@ -865,6 +882,7 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { protected void writeBarrierBaseNBT(NBTTagCompound nbt) { writeEnergyBaseNBT(nbt); nbt.setInteger("TicksAlive", ticksAlive); + nbt.setLong("BarrierDeathTime", barrierDeathTime); nbt.setFloat("CurrentHealth", currentHealth); nbt.setBoolean("Charging", charging); nbt.setInteger("ChargeTick", chargeTick); @@ -879,6 +897,7 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { protected void readBarrierBaseNBT(NBTTagCompound nbt) { readEnergyBaseNBT(nbt); this.ticksAlive = nbt.getInteger("TicksAlive"); + this.barrierDeathTime = nbt.hasKey("BarrierDeathTime") ? nbt.getLong("BarrierDeathTime") : -1; this.currentHealth = nbt.getFloat("CurrentHealth"); if (Float.isNaN(currentHealth) || Float.isInfinite(currentHealth) || currentHealth < 0) currentHealth = barrierData.maxHealth; this.charging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java b/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java deleted file mode 100644 index ee6cf352f..000000000 --- a/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java +++ /dev/null @@ -1,18 +0,0 @@ -package kamkeel.npcs.network.enums; - -public enum EnumSyncType { - FACTION, - DIALOG, - DIALOG_CATEGORY, - QUEST, - QUEST_CATEGORY, - PLAYERDATA, - MAGIC, - MAGIC_CYCLE, - WORKBENCH_RECIPES, - CARPENTRY_RECIPES, - ANVIL_RECIPES, - CUSTOM_EFFECTS, - CUSTOM_ABILITY, - CHAINED_ABILITY -} diff --git a/src/main/java/kamkeel/npcs/network/enums/SyncType.java b/src/main/java/kamkeel/npcs/network/enums/SyncType.java new file mode 100644 index 000000000..82d5639ae --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/enums/SyncType.java @@ -0,0 +1,142 @@ +package kamkeel.npcs.network.enums; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Immutable, object-based sync-type identity for the CNPC+ sync backend. + * + *

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

+ * + *

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

+ * + *

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

+ */ +public final class SyncType { + + // ──────────────────── Registry ──────────────────── + private static final Map REGISTRY = new LinkedHashMap<>(); + private static int nextOrdinal = 0; + + // ──────────────────── CNPC+ families ──────────────────── + public static final SyncType FACTION = register("cnpc:faction"); + public static final SyncType DIALOG = register("cnpc:dialog"); + public static final SyncType DIALOG_CATEGORY = register("cnpc:dialog_category"); + public static final SyncType QUEST = register("cnpc:quest"); + public static final SyncType QUEST_CATEGORY = register("cnpc:quest_category"); + public static final SyncType PLAYERDATA = register("cnpc:playerdata"); + public static final SyncType MAGIC = register("cnpc:magic"); + public static final SyncType MAGIC_CYCLE = register("cnpc:magic_cycle"); + public static final SyncType WORKBENCH_RECIPES = register("cnpc:workbench_recipes"); + public static final SyncType CARPENTRY_RECIPES = register("cnpc:carpentry_recipes"); + public static final SyncType ANVIL_RECIPES = register("cnpc:anvil_recipes"); + public static final SyncType CUSTOM_EFFECTS = register("cnpc:custom_effects"); + public static final SyncType CUSTOM_ABILITY = register("cnpc:custom_ability"); + public static final SyncType CHAINED_ABILITY = register("cnpc:chained_ability"); + + // ──────────────────── Instance fields ──────────────────── + private final String id; + private final int ordinal; + + private SyncType(String id, int ordinal) { + this.id = id; + this.ordinal = ordinal; + } + + // ──────────────────── Public API ──────────────────── + + /** + * The canonical namespaced identifier, e.g. {@code "cnpc:faction"}. + */ + public String getId() { + return id; + } + + /** + * Stable integer ordinal assigned at registration time. + * Used for wire encoding during the migration period only. + */ + public int ordinal() { + return ordinal; + } + + // ──────────────────── Registration ──────────────────── + + /** + * Register a new {@code SyncType} with the given canonical ID. + * + * @param id namespaced identifier ({@code modid:path}) + * @return the newly created and registered {@code SyncType} + * @throws IllegalArgumentException if the ID is null, empty, or lacks a colon + * @throws IllegalStateException if a type with the same ID is already registered + * @throws IllegalStateException if the registry has been frozen + */ + public static SyncType register(String id) { + if (id == null || id.isEmpty()) throw new IllegalArgumentException("SyncType id must not be null or empty"); + if (!id.contains(":")) throw new IllegalArgumentException("SyncType id must be namespaced (modid:path): " + id); + if (REGISTRY.containsKey(id)) throw new IllegalStateException("Duplicate SyncType registration: " + id); + + SyncType type = new SyncType(id, nextOrdinal++); + REGISTRY.put(id, type); + return type; + } + + /** + * Look up a registered SyncType by its canonical string ID. + * + * @return the registered type, or {@code null} if not found + */ + public static SyncType byId(String id) { + return id == null ? null : REGISTRY.get(id); + } + + /** + * Look up a registered SyncType by its ordinal. + * Used during migration for decoding ordinal-based wire traffic. + * + * @return the registered type, or {@code null} if ordinal is out of range + */ + public static SyncType byOrdinal(int ordinal) { + if (ordinal < 0) return null; + + for (SyncType type : REGISTRY.values()) + if (type.ordinal == ordinal) return type; + + return null; + } + + /** + * Returns all registered SyncType instances in registration order. + */ + public static Collection values() { + return Collections.unmodifiableCollection(REGISTRY.values()); + } + + // ──────────────────── Object overrides ──────────────────── + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SyncType)) return false; + return id.equals(((SyncType) o).id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public String toString() { + return id; + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java index da9eb9aa2..103c2e5fa 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java @@ -8,7 +8,7 @@ import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumDataPacket; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import kamkeel.npcs.network.packets.player.SyncRevisionInfoPacket; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; @@ -21,19 +21,20 @@ import noppes.npcs.controllers.AuctionConfigSync; import noppes.npcs.controllers.ScriptConfigSync; import java.io.IOException; -import java.util.EnumMap; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; public final class LoginPacket extends AbstractPacket { public static final String packetName = "CNPC+|Login"; private String serverCacheKey = ""; - private final EnumMap revisionSnapshot = new EnumMap<>(EnumSyncType.class); + private final Map revisionSnapshot = new LinkedHashMap<>(); public LoginPacket() { } - public LoginPacket(String serverCacheKey, EnumMap revisionSnapshot) { + public LoginPacket(String serverCacheKey, Map revisionSnapshot) { if (serverCacheKey != null) { this.serverCacheKey = serverCacheKey; } @@ -58,17 +59,15 @@ public final class LoginPacket extends AbstractPacket { out.writeBoolean(ConfigMain.ProfilesEnabled); ByteBufUtils.writeString(out, serverCacheKey); out.writeShort(revisionSnapshot.size()); - for (Map.Entry entry : revisionSnapshot.entrySet()) { + for (Map.Entry entry : revisionSnapshot.entrySet()) { out.writeInt(entry.getKey().ordinal()); out.writeInt(entry.getValue()); } - // Send auction config (server-side sync) NBTTagCompound auctionConfig = new NBTTagCompound(); AuctionConfigSync.writeToNBT(auctionConfig); ByteBufUtils.writeNBT(out, auctionConfig); - // Send script config (server-side sync) NBTTagCompound scriptConfig = new NBTTagCompound(); ScriptConfigSync.writeToNBT(scriptConfig); ByteBufUtils.writeNBT(out, scriptConfig); @@ -82,20 +81,20 @@ public final class LoginPacket extends AbstractPacket { String serverKey = ByteBufUtils.readString(in); int revisionCount = in.readUnsignedShort(); - EnumMap serverRevisions = new EnumMap<>(EnumSyncType.class); + Map serverRevisions = new LinkedHashMap<>(); for (int i = 0; i < revisionCount; i++) { - EnumSyncType type = EnumSyncType.values()[in.readInt()]; + SyncType type = SyncType.byOrdinal(in.readInt()); int revision = in.readInt(); - serverRevisions.put(type, revision); + if (type != null) { + serverRevisions.put(type, revision); + } } ClientCacheHandler.setActiveServer(serverKey, serverRevisions); - // Read auction config NBTTagCompound auctionConfig = ByteBufUtils.readNBT(in); AuctionClientConfig.readFromNBT(auctionConfig); - // Read script config NBTTagCompound scriptConfig = ByteBufUtils.readNBT(in); ScriptClientConfig.readFromNBT(scriptConfig); diff --git a/src/main/java/kamkeel/npcs/network/packets/data/large/GuiDataPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/large/GuiDataPacket.java index 631e32bf2..784d0fe1c 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/large/GuiDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/large/GuiDataPacket.java @@ -9,13 +9,9 @@ import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumDataPacket; import kamkeel.npcs.util.ByteBufUtils; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiScreen; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.client.gui.util.GuiContainerNPCInterface; -import noppes.npcs.client.gui.util.GuiNPCInterface; import noppes.npcs.client.gui.util.IGuiData; import java.io.IOException; @@ -59,15 +55,6 @@ public final class GuiDataPacket extends LargeAbstractPacket { @SideOnly(Side.CLIENT) @Override protected void handleCompleteData(ByteBuf data, EntityPlayer player) throws IOException { - GuiScreen gui = Minecraft.getMinecraft().currentScreen; - if (gui instanceof GuiNPCInterface && ((GuiNPCInterface) gui).hasSubGui()) { - gui = ((GuiNPCInterface) gui).getSubGui(); - } else if (gui instanceof GuiContainerNPCInterface && ((GuiContainerNPCInterface) gui).hasSubGui()) { - gui = ((GuiContainerNPCInterface) gui).getSubGui(); - } - if (gui instanceof IGuiData) { - NBTTagCompound nbt = ByteBufUtils.readBigNBT(data); - ((IGuiData) gui).setGuiData(nbt); - } + IGuiData.legacySetGuiData(ByteBufUtils.readBigNBT(data)); } } diff --git a/src/main/java/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/kamkeel/npcs/network/packets/data/large/SyncPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/large/SyncPacket.java index 77b2122b4..dcd940a50 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/large/SyncPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/large/SyncPacket.java @@ -10,7 +10,7 @@ import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumDataPacket; import kamkeel.npcs.network.enums.EnumSyncAction; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; @@ -22,16 +22,13 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; -/** - * A large sync packet that sends chunked data to the client for a given SyncType - * along with a SyncAction (RELOAD, UPDATE, REMOVE). - */ public final class SyncPacket extends LargeAbstractPacket { - private EnumSyncType enumSyncType; + private SyncType syncType; private EnumSyncAction enumSyncAction; private NBTTagCompound syncData; private int operationID; + private String operationKey; private int revision = -1; private byte[] cachedPayload; private byte[][] cachedChunks; @@ -39,23 +36,21 @@ public final class SyncPacket extends LargeAbstractPacket { public SyncPacket() { } - /** - * Constructs a new LargeSyncPacket. - */ - public SyncPacket(EnumSyncType enumSyncType, EnumSyncAction enumSyncAction, int catId, NBTTagCompound syncData) { - this(enumSyncType, enumSyncAction, catId, -1, syncData); + public SyncPacket(SyncType syncType, EnumSyncAction enumSyncAction, int catId, NBTTagCompound syncData) { + this(syncType, enumSyncAction, catId, null, -1, syncData); } - public SyncPacket(EnumSyncType enumSyncType, EnumSyncAction enumSyncAction, int catId, int revision, NBTTagCompound syncData) { - this.enumSyncType = enumSyncType; + public SyncPacket(SyncType syncType, EnumSyncAction enumSyncAction, int operationId, String operationKey, int revision, NBTTagCompound syncData) { + this.syncType = syncType; this.enumSyncAction = enumSyncAction; this.syncData = syncData; - this.operationID = catId; + this.operationID = operationId; + this.operationKey = operationKey; this.revision = revision; } - public SyncPacket(EnumSyncType enumSyncType, SyncController.CachedSyncPayload payload) { - this.enumSyncType = enumSyncType; + public SyncPacket(SyncType syncType, SyncController.CachedSyncPayload payload) { + this.syncType = syncType; this.enumSyncAction = EnumSyncAction.RELOAD; this.operationID = -1; this.revision = payload.getRevision(); @@ -79,13 +74,21 @@ public final class SyncPacket extends LargeAbstractPacket { return cachedPayload; } + boolean hasKey = operationKey != null; + boolean hasNbt = syncData != null; + ByteBuf buffer = Unpooled.buffer(); try { - buffer.writeInt(enumSyncType.ordinal()); + buffer.writeInt(syncType.ordinal()); buffer.writeInt(enumSyncAction.ordinal()); buffer.writeInt(operationID); buffer.writeInt(revision); - ByteBufUtils.writeBigNBT(buffer, syncData); + + // UPDATE/REMOVE UNIQUE FIELDS + buffer.writeBoolean(hasKey); + if (hasKey) ByteBufUtils.writeString(buffer, operationKey); + buffer.writeBoolean(hasNbt); + if (hasNbt) ByteBufUtils.writeBigNBT(buffer, syncData); byte[] bytes = new byte[buffer.readableBytes()]; buffer.readBytes(bytes); @@ -131,39 +134,40 @@ public final class SyncPacket extends LargeAbstractPacket { if (CustomNpcs.side() != Side.CLIENT) return; - // Reconstruct everything from the ByteBuf int syncTypeOrdinal = data.readInt(); int syncActionOrdinal = data.readInt(); - int categoryID = data.readInt(); - int incomingRevision = data.readInt(); + operationID = data.readInt(); + revision = data.readInt(); - EnumSyncType type = EnumSyncType.values()[syncTypeOrdinal]; EnumSyncAction action = EnumSyncAction.values()[syncActionOrdinal]; - try { - NBTTagCompound tag = ByteBufUtils.readBigNBT(data); - // Now do your client-side logic (similar to your old clientSync() or clientSyncUpdate() approach) - handleSyncPacketClient(type, action, categoryID, incomingRevision, tag); - } catch (RuntimeException e) { - LogWriter.error(String.format("Attempted to Sync %s but it was too big", type.toString())); + + // These fields only exist for UPDATE/REMOVE + if (action != EnumSyncAction.RELOAD) { + if (data.readBoolean()) operationKey = ByteBufUtils.readString(data); + if (data.readBoolean()) syncData = ByteBufUtils.readBigNBT(data); + } else + syncData = ByteBufUtils.readBigNBT(data); + + + syncType = SyncType.byOrdinal(syncTypeOrdinal); + if (syncType == null) { + LogWriter.error("[SyncPacket] Unknown sync type ordinal: " + syncTypeOrdinal + "; skipping"); + return; } + + handleSync(syncType, action, operationID, operationKey, revision, syncData); } - private void handleSyncPacketClient( - EnumSyncType enumSyncType, - EnumSyncAction enumSyncAction, - int id, - int incomingRevision, - NBTTagCompound data - ) { - switch (enumSyncAction) { + private void handleSync(SyncType type, EnumSyncAction action, int id, String key, int revision, NBTTagCompound data) { + switch (action) { case RELOAD: - SyncController.clientSync(enumSyncType, incomingRevision, data); + SyncController.clientHandleAll(type, revision, data); break; case UPDATE: - SyncController.clientUpdate(enumSyncType, id, incomingRevision, data); + SyncController.clientHandleUpdate(type, id, key, revision, data); break; case REMOVE: - SyncController.clientSyncRemove(enumSyncType, id, incomingRevision); + SyncController.clientHandleRemove(type, id, key, revision); break; } } diff --git a/src/main/java/kamkeel/npcs/network/packets/player/SyncRevisionInfoPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/SyncRevisionInfoPacket.java index 769b086b7..b6becdaab 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/SyncRevisionInfoPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/SyncRevisionInfoPacket.java @@ -8,13 +8,13 @@ import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumPlayerPacket; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import java.io.IOException; -import java.util.EnumMap; +import java.util.LinkedHashMap; import java.util.Map; public class SyncRevisionInfoPacket extends AbstractPacket { @@ -23,12 +23,12 @@ public class SyncRevisionInfoPacket extends AbstractPacket { private String serverKey = ""; private String previousServerKey = ""; - private final EnumMap revisions = new EnumMap<>(EnumSyncType.class); + private final Map revisions = new LinkedHashMap<>(); public SyncRevisionInfoPacket() { } - public SyncRevisionInfoPacket(String serverKey, String previousServerKey, EnumMap revisions) { + public SyncRevisionInfoPacket(String serverKey, String previousServerKey, Map revisions) { if (serverKey != null) { this.serverKey = serverKey; } @@ -56,7 +56,7 @@ public class SyncRevisionInfoPacket extends AbstractPacket { ByteBufUtils.writeString(out, serverKey); ByteBufUtils.writeString(out, previousServerKey); out.writeShort(revisions.size()); - for (Map.Entry entry : revisions.entrySet()) { + for (Map.Entry entry : revisions.entrySet()) { out.writeInt(entry.getKey().ordinal()); out.writeInt(entry.getValue()); } @@ -71,13 +71,15 @@ public class SyncRevisionInfoPacket extends AbstractPacket { String incomingServerKey = ByteBufUtils.readString(in); String incomingPreviousKey = ByteBufUtils.readString(in); int revisionCount = in.readUnsignedShort(); - EnumMap incomingRevisions = new EnumMap<>(EnumSyncType.class); + Map incomingRevisions = new LinkedHashMap<>(); for (int i = 0; i < revisionCount; i++) { - EnumSyncType type = EnumSyncType.values()[in.readInt()]; + SyncType type = SyncType.byOrdinal(in.readInt()); int revision = in.readInt(); - incomingRevisions.put(type, revision); + if (type != null) { + incomingRevisions.put(type, revision); + } } - SyncController.handleClientRevisionReport((EntityPlayerMP) player, incomingServerKey, incomingPreviousKey, incomingRevisions); + SyncController.completeLoginRevisionHandshake((EntityPlayerMP) player, incomingServerKey, incomingPreviousKey, incomingRevisions); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSelectPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSelectPacket.java index ba97769a0..87affdc1b 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSelectPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSelectPacket.java @@ -50,10 +50,9 @@ public final class AbilityHotbarSelectPacket extends AbstractPacket { // Deselect data.abilityData.setSelectedIndex(-1); } else { - int index = data.abilityData.getUnlockedAbilityList().indexOf(key); - if (index >= 0) { - data.abilityData.setSelectedIndex(index); - } + // A key the player no longer has deselects rather than leaving the previous + // selection in place, which would fire a different ability than the one shown. + data.abilityData.setSelectedIndex(data.abilityData.getUnlockedAbilityList().indexOf(key)); } data.save(); } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryItemsRequestPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryItemsRequestPacket.java index 9e06ab938..fef2a0f2e 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryItemsRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryItemsRequestPacket.java @@ -73,7 +73,7 @@ public final class CategoryItemsRequestPacket extends AbstractPacket { // Send tag map for the items HashMap> tagMap = getTagMapByCat(type, categoryId); - if (tagMap != null && !tagMap.isEmpty()) { + if (tagMap != null) { TagController.sendCategoryTagMap((EntityPlayerMP) player, tagMap); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/role/RoleSavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/role/RoleSavePacket.java index d63f2603c..21d0f2667 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/role/RoleSavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/role/RoleSavePacket.java @@ -14,6 +14,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.roles.RoleTrader; import java.io.IOException; @@ -63,7 +64,13 @@ public final class RoleSavePacket extends AbstractPacket { if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) return; - npc.roleInterface.readFromNBT(ByteBufUtils.readNBT(in)); + NBTTagCompound roleCompound = ByteBufUtils.readNBT(in); + if (npc.roleInterface instanceof RoleTrader) { + // An admin edit wins over the linked market; the market is written from it afterwards. + ((RoleTrader) npc.roleInterface).readFromNBT(roleCompound, false); + } else { + npc.roleInterface.readFromNBT(roleCompound); + } npc.updateClient = true; } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/tags/TagsGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/tags/TagsGetPacket.java index 4414131f1..85fe8e5c3 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/tags/TagsGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/tags/TagsGetPacket.java @@ -6,8 +6,6 @@ import io.netty.buffer.ByteBuf; import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; -import kamkeel.npcs.network.PacketUtil; -import kamkeel.npcs.network.enums.EnumItemPacketType; import kamkeel.npcs.network.enums.EnumRequestPacket; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; @@ -38,9 +36,6 @@ public final class TagsGetPacket extends AbstractPacket { @Override public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { - if (!PacketUtil.verifyItemPacket(packetName, player, EnumItemPacketType.WAND, EnumItemPacketType.CLONER)) - return; - NoppesUtilServer.sendTagDataAll((EntityPlayerMP) player); } diff --git a/src/main/java/noppes/npcs/AbstractDataAbilities.java b/src/main/java/noppes/npcs/AbstractDataAbilities.java index 244ed91d1..12f1fd793 100644 --- a/src/main/java/noppes/npcs/AbstractDataAbilities.java +++ b/src/main/java/noppes/npcs/AbstractDataAbilities.java @@ -786,7 +786,15 @@ public abstract class AbstractDataAbilities { fireTickEvent(currentAbility, target); // Script may have completed/cancelled the ability during the tick event - if (currentAbility == null || !currentAbility.isExecuting()) return; + if (currentAbility == null) return; + if (!currentAbility.isExecuting()) { + // tick() itself can end the ability (DAZED -> IDLE). Run its completion here, + // otherwise it stays the current ability forever and nothing else can start. + if (phaseChanged && currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(target); + } + return; + } // Fire extender tick hook (e.g., per-tick resource drain) if (!AbilityController.Instance.fireOnAbilityTick(currentAbility, entity, target, diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index d72bdf23f..480ee4f12 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -317,6 +317,9 @@ public class CustomNpcs { new AddonManager(); new AttributeController(); new MagicController(); + + SyncController.register(); + APIRegistry.Instance.register("CNPC+ API", "https://kamkeel.github.io/CustomNPC-Plus/"); } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 7772f6e22..3888df2d5 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -188,29 +188,34 @@ public class DataAbilities extends AbstractDataAbilities { if (maxCooldown > minCooldown) { baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1); } - cooldownEndTime = npc.worldObj.getTotalWorldTime() + baseCooldown + ability.getCooldownTicks(); + long now = npc.worldObj.getTotalWorldTime(); - // Additionally set per-ability cooldown if enabled if (ability.isPerAbilityCooldown() && ability.getCooldownTicks() > 0) { - long endTime = npc.worldObj.getTotalWorldTime() + ability.getCooldownTicks(); - setPerAbilityCooldown(ability.getName(), endTime, ability.getCooldownTicks()); + // The ability's own cooldown is tracked against the ability, so the global timer + // only paces how often the NPC acts. Folding the ability's cooldown into it as + // well would always outlast the per-ability entry and make it unreachable. + cooldownEndTime = now + baseCooldown; + setPerAbilityCooldown(ability.getName(), now + ability.getCooldownTicks(), ability.getCooldownTicks()); + } else { + cooldownEndTime = now + baseCooldown + ability.getCooldownTicks(); } } @Override protected void rollChainCooldown(ChainedAbility chain) { + long now = npc.worldObj.getTotalWorldTime(); + boolean perAbility = chain.isPerAbilityCooldown() && chain.getCooldownTicks() > 0; + if (!chain.isIgnoreCooldown()) { int baseCooldown = minCooldown; if (maxCooldown > minCooldown) { baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1); } - cooldownEndTime = npc.worldObj.getTotalWorldTime() + baseCooldown + chain.getCooldownTicks(); + cooldownEndTime = perAbility ? now + baseCooldown : now + baseCooldown + chain.getCooldownTicks(); } - // Additionally set per-ability cooldown if enabled - if (chain.isPerAbilityCooldown() && chain.getCooldownTicks() > 0) { - long endTime = npc.worldObj.getTotalWorldTime() + chain.getCooldownTicks(); - setPerAbilityCooldown(chain.getName(), endTime, chain.getCooldownTicks()); + if (perAbility) { + setPerAbilityCooldown(chain.getName(), now + chain.getCooldownTicks(), chain.getCooldownTicks()); } } diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index 53dbfb239..dba0c27c1 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -640,7 +640,7 @@ public class NoppesUtilServer { playerdata.save(); } if (pl != null) { - SyncController.syncPlayer((EntityPlayerMP) pl); + SyncController.dispatchSyncForAllTypes((EntityPlayerMP) pl); } sendPlayerData(type, player, name); } @@ -1229,7 +1229,7 @@ public class NoppesUtilServer { playerdata.save(); if (pl != null) { - SyncController.syncPlayer((EntityPlayerMP) pl); + SyncController.dispatchSyncForAllTypes((EntityPlayerMP) pl); } } @@ -1261,7 +1261,7 @@ public class NoppesUtilServer { playerdata.save(); if (pl != null) { - SyncController.syncPlayer((EntityPlayerMP) pl); + SyncController.dispatchSyncForAllTypes((EntityPlayerMP) pl); } } } diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index 3133fb43c..3e7fa07d9 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.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 04e0406d8..d36c95eb5 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -4,13 +4,15 @@ 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; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumSoundOperation; import kamkeel.npcs.network.enums.EnumSyncAction; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import kamkeel.npcs.network.packets.data.ClonerPacket; import kamkeel.npcs.network.packets.data.MarkDataPacket; import kamkeel.npcs.network.packets.data.SoundManagementPacket; @@ -255,20 +257,22 @@ public class ServerEventsHandler { if (block == Blocks.crafting_table && event.action == Action.RIGHT_CLICK_BLOCK && !player.worldObj.isRemote) { PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.WORKBENCH_RECIPES, + SyncType.WORKBENCH_RECIPES, EnumSyncAction.RELOAD, -1, - SyncController.getCurrentRevision(EnumSyncType.WORKBENCH_RECIPES), - SyncController.workbenchNBT() + null, + SyncController.getCurrentServerRevision(SyncType.WORKBENCH_RECIPES), + WorkbenchRecipeSyncHandler.getInstance().serializeAll() ), (EntityPlayerMP) player); } if (block == CustomItems.carpentyBench && event.action == Action.RIGHT_CLICK_BLOCK && !player.worldObj.isRemote) { PacketHandler.Instance.sendToPlayer(new SyncPacket( - EnumSyncType.CARPENTRY_RECIPES, + SyncType.CARPENTRY_RECIPES, EnumSyncAction.RELOAD, -1, - SyncController.getCurrentRevision(EnumSyncType.CARPENTRY_RECIPES), - SyncController.carpentryNBT() + null, + SyncController.getCurrentServerRevision(SyncType.CARPENTRY_RECIPES), + 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/ai/EntityAITransform.java b/src/main/java/noppes/npcs/ai/EntityAITransform.java index 9f565bccc..b0c3fa30e 100644 --- a/src/main/java/noppes/npcs/ai/EntityAITransform.java +++ b/src/main/java/noppes/npcs/ai/EntityAITransform.java @@ -10,7 +10,10 @@ public class EntityAITransform extends EntityAIBase { public EntityAITransform(EntityNPCInterface npc) { this.npc = npc; - setMutexBits(AiMutex.PASSIVE); + // No mutex: this only polls the world time and flips the transform, it drives nothing. + // Claiming PASSIVE let any running movement or look task starve it, so the day/night + // swap only fired in the gaps where those yielded - such as while saying a line. + setMutexBits(0); } @Override diff --git a/src/main/java/noppes/npcs/blocks/BlockBorder.java b/src/main/java/noppes/npcs/blocks/BlockBorder.java index 3781b9c72..db7761b1c 100644 --- a/src/main/java/noppes/npcs/blocks/BlockBorder.java +++ b/src/main/java/noppes/npcs/blocks/BlockBorder.java @@ -67,10 +67,6 @@ public class BlockBorder extends BlockContainer { } tile.rotation = l; - - if (par5EntityLivingBase instanceof EntityPlayer && par1World.isRemote) { - CustomNpcs.proxy.openGui(x, y, z, EnumGuiType.Border, (EntityPlayer) par5EntityLivingBase); - } } private TileBorder getTile(World world, int x, int y, int z) { diff --git a/src/main/java/noppes/npcs/blocks/BlockNpcRedstone.java b/src/main/java/noppes/npcs/blocks/BlockNpcRedstone.java index e97096c72..6f89b442a 100644 --- a/src/main/java/noppes/npcs/blocks/BlockNpcRedstone.java +++ b/src/main/java/noppes/npcs/blocks/BlockNpcRedstone.java @@ -49,13 +49,6 @@ public class BlockNpcRedstone extends BlockContainer { par1World.notifyBlocksOfNeighborChange(par2, par3, par4 + 1, this); } - @Override - public void onBlockPlacedBy(World world, int i, int j, int k, EntityLivingBase entityliving, ItemStack item) { - if (entityliving instanceof EntityPlayer && world.isRemote) { - CustomNpcs.proxy.openGui(i, j, k, EnumGuiType.RedstoneBlock, (EntityPlayer) entityliving); - } - } - @Override public void onBlockDestroyedByPlayer(World par1World, int par2, int par3, int par4, int par5) { onBlockAdded(par1World, par2, par3, par4); diff --git a/src/main/java/noppes/npcs/client/ClientCacheHandler.java b/src/main/java/noppes/npcs/client/ClientCacheHandler.java index 3386ffb43..53dfb9959 100644 --- a/src/main/java/noppes/npcs/client/ClientCacheHandler.java +++ b/src/main/java/noppes/npcs/client/ClientCacheHandler.java @@ -1,6 +1,6 @@ package noppes.npcs.client; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; import noppes.npcs.client.controllers.MusicController; @@ -18,8 +18,8 @@ import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.controllers.data.SkinOverlay; import noppes.npcs.util.CacheHashMap; -import java.util.EnumMap; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; @@ -32,14 +32,14 @@ public class ClientCacheHandler { private static String activeServerKey = ""; private static String lastServerKey = ""; - private static final Map> clientRevisionCache = new HashMap<>(); + private static final Map> clientRevisionCache = new HashMap<>(); public static Party party; public static boolean allowProfiles = true; public static boolean allowParties = true; - public static void setActiveServer(String serverKey, EnumMap serverRevisions) { + public static void setActiveServer(String serverKey, Map serverRevisions) { String normalizedKey = serverKey == null ? "" : serverKey; String previousKey = activeServerKey == null ? "" : activeServerKey; lastServerKey = previousKey; @@ -54,9 +54,9 @@ public class ClientCacheHandler { return; } - EnumMap cached = clientRevisionCache.computeIfAbsent( + Map cached = clientRevisionCache.computeIfAbsent( activeServerKey, - ignored -> new EnumMap<>(EnumSyncType.class) + ignored -> new LinkedHashMap<>() ); if (serverRevisions != null && !serverRevisions.isEmpty()) { cached.keySet().retainAll(serverRevisions.keySet()); @@ -67,19 +67,19 @@ public class ClientCacheHandler { return lastServerKey == null ? "" : lastServerKey; } - public static EnumMap getCachedRevisionsForServer(String serverKey) { + public static Map getCachedRevisionsForServer(String serverKey) { String key = serverKey == null ? "" : serverKey; if (key.isEmpty()) { - return new EnumMap<>(EnumSyncType.class); + return new LinkedHashMap<>(); } - EnumMap revisions = clientRevisionCache.get(key); + Map revisions = clientRevisionCache.get(key); if (revisions == null) { - return new EnumMap<>(EnumSyncType.class); + return new LinkedHashMap<>(); } - return new EnumMap<>(revisions); + return new LinkedHashMap<>(revisions); } - public static void updateClientRevision(EnumSyncType type, int revision) { + public static void updateClientRevision(SyncType type, int revision) { if (revision < 0) { return; } @@ -87,9 +87,9 @@ public class ClientCacheHandler { if (key.isEmpty()) { return; } - EnumMap revisions = clientRevisionCache.computeIfAbsent( + Map revisions = clientRevisionCache.computeIfAbsent( key, - ignored -> new EnumMap<>(EnumSyncType.class) + ignored -> new LinkedHashMap<>() ); revisions.put(type, revision); } @@ -126,14 +126,11 @@ public class ClientCacheHandler { ClientCacheHandler.playerAnimations.clear(); ProfileClientConfig.reset(); - // Clear Texture Caches GuiSoundSelection.cachedDomains.clear(); GuiTextureSelection.cachedTextures.clear(); - // Clear music/bard sounds MusicController.Instance.stopAllSounds(); - // Clear Quest Tracker HudComponent component = ClientHudManager.getInstance().getHudComponents().get(EnumHudComponent.QuestTracker); if (component != null) { component.loadData(new NBTTagCompound()); @@ -146,7 +143,6 @@ public class ClientCacheHandler { ClientCacheHandler.customOverlays.clear(); ClientCacheHandler.skinOverlays.clear(); - // Clear Texture Caches GuiSoundSelection.cachedDomains.clear(); GuiTextureSelection.cachedTextures.clear(); } diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcAvailabilityDialog.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcAvailabilityDialog.java index 802ede067..d3d93daa2 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcAvailabilityDialog.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcAvailabilityDialog.java @@ -112,19 +112,19 @@ public class SubGuiNpcAvailabilityDialog extends SubGuiInterface implements GuiS } if (button.id == 10) { slot = 1; - setSubGui(new GuiDialogSelection(availabitily.questId)); + setSubGui(new GuiDialogSelection(availabitily.dialogId)); } if (button.id == 11) { slot = 2; - setSubGui(new GuiDialogSelection(availabitily.quest2Id)); + setSubGui(new GuiDialogSelection(availabitily.dialog2Id)); } if (button.id == 12) { slot = 3; - setSubGui(new GuiDialogSelection(availabitily.quest3Id)); + setSubGui(new GuiDialogSelection(availabitily.dialog3Id)); } if (button.id == 13) { slot = 4; - setSubGui(new GuiDialogSelection(availabitily.quest4Id)); + setSubGui(new GuiDialogSelection(availabitily.dialog4Id)); } if (button.id == 20) { availabitily.dialogId = -1; diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiTrapPresetSelector.java b/src/main/java/noppes/npcs/client/gui/SubGuiTrapPresetSelector.java index b8fb9e376..086e27569 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiTrapPresetSelector.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiTrapPresetSelector.java @@ -4,6 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import net.minecraft.client.gui.GuiButton; import net.minecraft.util.StatCollector; +import noppes.npcs.client.gui.builder.FieldDef; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.SubGuiInterface; @@ -12,6 +13,25 @@ public class SubGuiTrapPresetSelector extends SubGuiInterface { public String selectedPreset = null; + /** + * Builds the trap editor's preset field. + *

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

+ * This lives here rather than in AbilityZone because the lambda below carries this class in + * its method descriptor. AbilityZone is a both-side class, and @SideOnly does not strip the + * synthetic lambda methods it generates - so anything reflecting over its declared methods + * on a server would resolve that descriptor and fail to find this client-only type. + */ + public static FieldDef createPresetField(java.util.function.Consumer applyPreset) { + return FieldDef.subGuiField("gui.applyPreset", + SubGuiZonePresetSelector::new, + gui -> { + SubGuiZonePresetSelector selector = (SubGuiZonePresetSelector) gui; + if (selector.selectedPreset != null) { + applyPreset.accept(selector.selectedPreset); + } + }); + } + private static final String[] PRESET_NAMES = {"DEFAULT", "TOXIC", "INFERNO", "ARCANE", "ELECTRIC", "FROST"}; private static final String[] PRESET_LABELS = { "ability.preset.default", "ability.preset.toxic", "ability.preset.inferno", diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityMagic.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityMagic.java index 8543ad208..8f555017a 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityMagic.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityMagic.java @@ -1,6 +1,8 @@ package noppes.npcs.client.gui.advanced; import net.minecraft.client.gui.GuiButton; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.gui.builder.FieldDef; import noppes.npcs.client.gui.util.GuiCustomScroll; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -11,6 +13,7 @@ import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.controllers.MagicController; import noppes.npcs.controllers.data.Magic; import noppes.npcs.controllers.data.MagicData; +import noppes.npcs.controllers.data.MagicEntry; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -36,6 +39,30 @@ public class SubGuiAbilityMagic extends SubGuiInterface implements ICustomScroll private GuiNpcTextField splitField; + /** + * Builds the ability editor's magic field. + *

+ * This lives here rather than in Ability because the lambdas below carry this class in + * their method descriptors. Ability is a both-side class, and anything reflecting over its + * declared methods on a server - Nashorn's adapter generator, for one - would resolve those + * descriptors and fail to find this client-only type. + */ + public static FieldDef createMagicField(MagicData magicData) { + return FieldDef.subGuiField("ability.magic.editor", + () -> new SubGuiAbilityMagic(magicData.copy()), + gui -> { + MagicData result = ((SubGuiAbilityMagic) gui).magicData; + magicData.getMagics().clear(); + for (Map.Entry e : result.getMagics().entrySet()) { + magicData.getMagics().put(e.getKey(), e.getValue()); + } + }) + .buttonLabel(() -> magicData.isEmpty() + ? StatCollector.translateToLocal("ability.magic.usesCaster") + : magicData.getMagics().size() + " Magic(s)") + .tab("General"); + } + public SubGuiAbilityMagic(MagicData data) { this.magicData = data; setBackground("menubg.png"); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiCustomEffectSelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiCustomEffectSelect.java index 411d03351..233ad8ece 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiCustomEffectSelect.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiCustomEffectSelect.java @@ -3,6 +3,8 @@ package noppes.npcs.client.gui.advanced; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import net.minecraft.client.gui.GuiButton; +import kamkeel.npcs.controllers.data.ability.conditions.ConditionHasEffect; +import noppes.npcs.client.gui.builder.FieldDef; import noppes.npcs.client.gui.util.GuiCustomScroll; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcTextField; @@ -25,6 +27,31 @@ import java.util.Map; @SideOnly(Side.CLIENT) public class SubGuiCustomEffectSelect extends SubGuiInterface implements ICustomScrollListener, ITextfieldListener { + /** + * Builds the condition editor's effect-select field. + *

+ * This lives here rather than in ConditionHasEffect because the lambdas below carry this + * class in their method descriptors. The condition is a both-side class, and @SideOnly does + * not strip the synthetic lambda methods it generates - so anything reflecting over its + * declared methods on a server would resolve those descriptors and fail to find this type. + */ + public static FieldDef createSelectField(ConditionHasEffect condition) { + return FieldDef.subGuiField("condition.select_effect", + () -> new SubGuiCustomEffectSelect(condition.getEffectId(), condition.getEffectIndex()), + gui -> { + SubGuiCustomEffectSelect sel = (SubGuiCustomEffectSelect) gui; + if (sel.getSelectedEffectId() >= 0) { + condition.setEffect(sel.getSelectedEffectId(), sel.getSelectedIndex()); + } + }) + .buttonLabel(() -> { + if (condition.getEffectId() < 0) return "None"; + CustomEffect effect = condition.getEffect(); + return effect != null ? effect.getName() : "ID:" + condition.getEffectId(); + }) + .clearable(() -> condition.setEffect(-1, 0)); + } + private GuiCustomScroll scroll; private final HashMap displayNameToId = new HashMap<>(); private final HashMap allDisplayNameToId = new HashMap<>(); diff --git a/src/main/java/noppes/npcs/client/gui/hud/QuestTrackingComponent.java b/src/main/java/noppes/npcs/client/gui/hud/QuestTrackingComponent.java index ee6a6fe51..dc32caa16 100644 --- a/src/main/java/noppes/npcs/client/gui/hud/QuestTrackingComponent.java +++ b/src/main/java/noppes/npcs/client/gui/hud/QuestTrackingComponent.java @@ -39,6 +39,7 @@ public class QuestTrackingComponent extends HudComponent { if (turnIn != null && !turnIn.isEmpty()) { turnInLines.add(convertColorCodes(turnIn)); } + updateOverlayHeight(); } /** @@ -239,6 +240,19 @@ public class QuestTrackingComponent extends HudComponent { } // ---------- Helper rendering methods ----------- (methods unchanged) ... + private void updateOverlayHeight() { + int lineHeight = mc.fontRenderer.FONT_HEIGHT + 4; + int contentHeight = 5; + contentHeight += questTitleLines.size() * lineHeight + 8; + contentHeight += questCategoryLines.size() * lineHeight + 8; + contentHeight += objectiveLines.size() * lineHeight; + if (!turnInLines.isEmpty()) { + contentHeight += 8; + } + contentHeight += turnInLines.size() * lineHeight; + overlayHeight = contentHeight; + } + private int renderTextBlock(List lines, int startY, int align, int color) { int y = startY; FontRenderer font = mc.fontRenderer; diff --git a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java index f9e627fe7..56777ca6f 100644 --- a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java +++ b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java @@ -429,6 +429,9 @@ public class GuiProfiles extends GuiCNPCInventory implements ISubGuiListener, IC } private boolean isMouseOverScrollbar(int mouseX, int mouseY) { + if (maxScrollY <= 0) + return false; + int scrollbarX = guiLeft + xSize + 30; int scrollbarY = guiTop + 27; int scrollbarHeight = ySize - 35; diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java index 7b54518e3..1e5aab8d1 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java +++ b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java @@ -21,6 +21,7 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex private final ResourceLocation slot = new ResourceLocation("customnpcs", "textures/gui/slot.png"); private RoleTrader role; + private boolean marketRelinked = false; // Currency cost textfield IDs start at 100 (100-117 for slots 0-17) private static final int CURRENCY_FIELD_ID_START = 100; @@ -116,8 +117,14 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex @Override public void save() { - PacketClient.sendClient(new TraderMarketSavePacket(role.marketName, false)); + // Linking to a different market already replaced the server's role with that market's + // data. Pushing this GUI's pre-link state back would overwrite the market being linked to. + if (marketRelinked) + return; + + // Save the role first so the market is written from the edited data, not the stale data. PacketClient.sendClient(new RoleSavePacket(role.writeToNBT(new NBTTagCompound()))); + PacketClient.sendClient(new TraderMarketSavePacket(role.marketName, false)); } @Override @@ -129,6 +136,7 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex String name = guiNpcTextField.getText(); if (!name.equalsIgnoreCase(role.marketName)) { role.marketName = name; + marketRelinked = true; PacketClient.sendClient(new TraderMarketSavePacket(role.marketName, true)); } return; diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiQuestSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiQuestSelection.java index 288964545..cf805fe44 100644 --- a/src/main/java/noppes/npcs/client/gui/select/GuiQuestSelection.java +++ b/src/main/java/noppes/npcs/client/gui/select/GuiQuestSelection.java @@ -2,6 +2,8 @@ package noppes.npcs.client.gui.select; import com.google.common.collect.Lists; import net.minecraft.client.gui.GuiButton; +import kamkeel.npcs.controllers.data.ability.conditions.ConditionQuestCompleted; +import noppes.npcs.client.gui.builder.FieldDef; import noppes.npcs.client.gui.util.GuiCustomScroll; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -27,6 +29,31 @@ public class GuiQuestSelection extends SubGuiInterface implements ICustomScrollL private QuestCategory selectedCategory; public Quest selectedQuest; + /** + * Builds the condition editor's quest-select field. + *

+ * This lives here rather than in ConditionQuestCompleted because the lambdas below carry + * this class in their method descriptors. The condition is a both-side class, and @SideOnly + * does not strip the synthetic lambda methods it generates - so anything reflecting over its + * declared methods on a server would resolve those descriptors and fail to find this type. + */ + public static FieldDef createConditionField(ConditionQuestCompleted condition) { + return FieldDef.subGuiField("condition.select_quest", + () -> new GuiQuestSelection(condition.getQuestId()), + gui -> { + GuiQuestSelection sel = (GuiQuestSelection) gui; + if (sel.selectedQuest != null) { + condition.setQuestId(sel.selectedQuest.id); + } + }) + .buttonLabel(() -> { + if (condition.getQuestId() < 0) return "None"; + Quest q = QuestController.Instance.quests.get(condition.getQuestId()); + return q != null ? q.title : "ID:" + condition.getQuestId(); + }) + .clearable(() -> condition.setQuestId(-1)); + } + private GuiSelectionListener listener; private String catSearch = ""; private String questSearch = ""; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 08a5dff00..24d08cff2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -515,15 +515,28 @@ public abstract class GuiNPCInterface extends GuiScreen { } boolean subGui = hasSubGui(); + + // Mouse.getDWheel() clears the accumulated delta, so only the first caller in a frame + // ever sees it. Read it once here and let each widget take it in turn; anything that + // consumes it zeroes it so later widgets - the script editor, for one - are not left + // reading an already-drained wheel. + mouseScroll = Mouse.getDWheel(); + drawCenteredString(fontRendererObj, title, width / 2, guiTop + 4, 0xffffff); for (GuiNpcLabel label : labels.values()) label.drawLabel(this, fontRendererObj); for (GuiCustomScroll scroll : scrolls.values()) { scroll.updateSubGUI(subGui); - scroll.drawScreen(drawX, drawY, f, !subGui && scroll.isMouseOver(drawX, drawY) ? Mouse.getDWheel() : 0); + int delta = !subGui && scroll.isMouseOver(drawX, drawY) ? mouseScroll : 0; + if (delta != 0) + mouseScroll = 0; + scroll.drawScreen(drawX, drawY, f, delta); } for (GuiScrollWindow guiScrollableComponent : scrollWindows.values()) { - guiScrollableComponent.drawScreen(drawX, drawY, f, !subGui && guiScrollableComponent.isMouseOver(drawX, drawY) ? Mouse.getDWheel() : 0); + int delta = !subGui && guiScrollableComponent.isMouseOver(drawX, drawY) ? mouseScroll : 0; + if (delta != 0) + mouseScroll = 0; + guiScrollableComponent.drawScreen(drawX, drawY, f, delta); } for (GuiDiagram diagram : diagrams.values()) { diagram.drawDiagram(drawX, drawY, subGui); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 647621117..70283b7e0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -624,9 +624,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { int maxScroll = Math.max(0, getPaddedLineCount() - container.visibleLines); // Handle mouse wheel scroll - int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); + // Take whatever the parent screen has left for this frame rather than polling the wheel + // again - the poll is destructive and the parent has already drained it. + int wheelDelta = listener instanceof GuiNPCInterface ? ((GuiNPCInterface) listener).mouseScroll : Mouse.getDWheel(); if (listener instanceof GuiNPCInterface) { - ((GuiNPCInterface) listener).mouseScroll = wheelDelta; + ((GuiNPCInterface) listener).mouseScroll = 0; // Intercept wheel scroll for tooltip (highest priority) // Only consume the wheel event when the tooltip actually has content to scroll; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java index 60e5a38a0..a5a555514 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java @@ -486,9 +486,11 @@ public class GuiScriptTextArea1 extends GuiNpcTextField { int maxScroll = Math.max(0, getPaddedLineCount() - container.visibleLines); // Handle mouse wheel scroll - int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); + // Take whatever the parent screen has left for this frame rather than polling the wheel + // again - the poll is destructive and the parent has already drained it. + int wheelDelta = listener instanceof GuiNPCInterface ? ((GuiNPCInterface) listener).mouseScroll : Mouse.getDWheel(); if (listener instanceof GuiNPCInterface) { - ((GuiNPCInterface) listener).mouseScroll = wheelDelta; + ((GuiNPCInterface) listener).mouseScroll = 0; boolean canScroll = !KEYS_OVERLAY.isVisible() || KEYS_OVERLAY.isVisible() && !KEYS_OVERLAY.aboveOverlay; if (wheelDelta != 0 && canScroll) scroll.applyWheelScroll(wheelDelta, maxScroll); diff --git a/src/main/java/noppes/npcs/client/gui/util/IGuiData.java b/src/main/java/noppes/npcs/client/gui/util/IGuiData.java index 36ba683ac..9f1796c8a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/IGuiData.java +++ b/src/main/java/noppes/npcs/client/gui/util/IGuiData.java @@ -1,7 +1,108 @@ package noppes.npcs.client.gui.util; +import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.sync.SyncHandler; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; import net.minecraft.nbt.NBTTagCompound; +/** + * OVERHAULED IGuiData FRAMEWORK! + * Based on the new {@link SyncController} and {@link SyncHandler} infrastructure + */ public interface IGuiData { - public void setGuiData(NBTTagCompound compound); + + /** + * Legacy data setter + * @Deprecated Use {@link #onDataUpdated(NBTTagCompound)} or {@link #onDataRemoved(String)} instead + */ + default void setGuiData(NBTTagCompound compound) { + + } + + // ═══════════════════════════════════════════════════════════════════════ + // USE THESE !!! + // ═══════════════════════════════════════════════════════════════════════ + + /** NOTE: + * No need for full object dataset onDataReload on GUI open, as these datasets are + * always freshly synced to their client's controllers thanks to {@link SyncController} + */ + + /** + * When a controller's object gets saved on server side, the updated object is synced to the client + * @param compound the compound of the updated object + */ + default void onDataUpdated(NBTTagCompound compound) { + } + + /** + * When a controller's object gets saved on server side, the updated object is synced to the client + * @param key key of updated object + * @param compound compound of updated object + */ + default void onDataUpdated(NBTTagCompound compound, String key) { + } + + /** + * When a controller's object gets saved on server side, the updated object is synced to the client + * @param id id of updated object + * @param compound compound of updated object + */ + default void onDataUpdated(NBTTagCompound compound, int id) { + } + + /** + * When a controller's object gets deleted on server side, the deleted object is removed from the client + * @param key key of removed object + */ + default void onDataRemoved(String key) { + } + + /** + * When a controller's object gets deleted on server side, the deleted object is removed from the client + * @param id id of removed object + */ + default void onDataRemoved(int id) { + } + + // ═══════════════════════════════════════════════════════════════════════ + // Static dispatchers + // ═══════════════════════════════════════════════════════════════════════ + + static void legacySetGuiData(NBTTagCompound nbt) { + GuiScreen gui = activeGui(); + if (gui instanceof IGuiData) + ((IGuiData) gui).setGuiData(nbt); + } + + static void notifyUpdate(NBTTagCompound nbt, String key, int id) { + GuiScreen gui = activeGui(); + if (gui instanceof IGuiData) { + ((IGuiData) gui).onDataUpdated(nbt); + ((IGuiData) gui).onDataUpdated(nbt, key); + ((IGuiData) gui).onDataUpdated(nbt, id); + } + } + + static void notifyRemove(String key, int id) { + GuiScreen gui = activeGui(); + if (gui instanceof IGuiData) { + ((IGuiData) gui).onDataRemoved(key); + ((IGuiData) gui).onDataRemoved(id); + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // Active Gui fetcher + // ═══════════════════════════════════════════════════════════════════════ + + static GuiScreen activeGui() { + GuiScreen gui = Minecraft.getMinecraft().currentScreen; + if (gui instanceof GuiNPCInterface && ((GuiNPCInterface) gui).hasSubGui()) + return ((GuiNPCInterface) gui).getSubGui(); + if (gui instanceof GuiContainerNPCInterface && ((GuiContainerNPCInterface) gui).hasSubGui()) + return ((GuiContainerNPCInterface) gui).getSubGui(); + return gui; + } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index dcac25c99..df406ebba 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -325,78 +325,94 @@ public class TypeInfo { * @return A TypeInfo preserving the generic structure, or null if type is null */ public static TypeInfo fromGenericType(Type type) { + return fromGenericType(type, new HashSet>()); + } + + /** + * @param resolving type variables whose bounds are currently being expanded, so a + * self-referential bound such as {@code E extends Enum} terminates + */ + private static TypeInfo fromGenericType(Type type, Set> resolving) { if (type == null) return null; - + if (type instanceof Class) { // Plain class - no generic info return fromClass((Class) type); } - + if (type instanceof ParameterizedType) { // Generic type like List ParameterizedType paramType = (ParameterizedType) type; Type rawType = paramType.getRawType(); - + if (!(rawType instanceof Class)) { // Edge case - should not happen normally - return rawType != null ? fromGenericType(rawType) : null; + return rawType != null ? fromGenericType(rawType, resolving) : null; } - + // Get the raw type as TypeInfo TypeInfo rawTypeInfo = fromClass((Class) rawType); - + // Recursively convert type arguments Type[] typeArgs = paramType.getActualTypeArguments(); List appliedArgs = new ArrayList<>(typeArgs.length); for (Type arg : typeArgs) { - TypeInfo argInfo = fromGenericType(arg); + TypeInfo argInfo = fromGenericType(arg, resolving); if (argInfo != null) { appliedArgs.add(argInfo); } } - + // Return a parameterized TypeInfo if (!appliedArgs.isEmpty()) { return rawTypeInfo.parameterize(appliedArgs); } return rawTypeInfo; } - + if (type instanceof GenericArrayType) { // Array of generic type, e.g. T[] or List[] GenericArrayType arrayType = (GenericArrayType) type; - TypeInfo elementType = fromGenericType(arrayType.getGenericComponentType()); + TypeInfo elementType = fromGenericType(arrayType.getGenericComponentType(), resolving); if (elementType != null) { return arrayOf(elementType); } return fromClass(Object[].class); } - + if (type instanceof TypeVariable) { TypeVariable typeVar = (TypeVariable) type; String varName = typeVar.getName(); - // Check for bounds - if bounded, create typeParameter with bound - Type[] bounds = typeVar.getBounds(); - if (bounds != null && bounds.length > 0 && bounds[0] != Object.class) { - return TypeInfo.typeParameter(varName, fromGenericType(bounds[0])); + // Already expanding this variable - emit it unbounded rather than recursing forever + if (!resolving.add(typeVar)) { + return TypeInfo.typeParameter(varName); + } + try { + // Check for bounds - if bounded, create typeParameter with bound + Type[] bounds = typeVar.getBounds(); + if (bounds != null && bounds.length > 0 && bounds[0] != Object.class) { + return TypeInfo.typeParameter(varName, fromGenericType(bounds[0], resolving)); + } + return TypeInfo.typeParameter(varName); + } finally { + resolving.remove(typeVar); } - return TypeInfo.typeParameter(varName); } - + if (type instanceof WildcardType) { // Wildcard like ? or ? extends Number or ? super Integer WildcardType wildcard = (WildcardType) type; Type[] upperBounds = wildcard.getUpperBounds(); - + // Use the upper bound if available (most useful for ? extends T) if (upperBounds.length > 0 && upperBounds[0] != Object.class) { - return fromGenericType(upperBounds[0]); + return fromGenericType(upperBounds[0], resolving); } - + // For ? super T or plain ?, just use Object return fromClass(Object.class); } - + // Unknown type - shouldn't happen in practice return null; } diff --git a/src/main/java/noppes/npcs/client/model/part/tails/ModelMonkeyTail.java b/src/main/java/noppes/npcs/client/model/part/tails/ModelMonkeyTail.java index fae556349..347b5ebfa 100644 --- a/src/main/java/noppes/npcs/client/model/part/tails/ModelMonkeyTail.java +++ b/src/main/java/noppes/npcs/client/model/part/tails/ModelMonkeyTail.java @@ -34,7 +34,9 @@ public class ModelMonkeyTail extends ModelRenderer { public ModelMonkeyTail(ModelBiped base) { super(base); - float heightFactor = -9.0f; + // Cancels ModelTail's rotationPointY of 11 so the boxes land where JBRA puts them + // on players: plain tail at 10, wrapped tail at 0. + float heightFactor = -11.0f; (this.monkey_large = new ModelRenderer(base, 38, 54)).addBox(-2.0f, -2.0f, 0.0f, 4, 4, 6); this.setRotation(this.monkey_large, -0.5235988f, 0.0f, 0.0f); diff --git a/src/main/java/noppes/npcs/controllers/CustomEffectController.java b/src/main/java/noppes/npcs/controllers/CustomEffectController.java index 640f544c4..6a465b0ee 100644 --- a/src/main/java/noppes/npcs/controllers/CustomEffectController.java +++ b/src/main/java/noppes/npcs/controllers/CustomEffectController.java @@ -1,7 +1,7 @@ package noppes.npcs.controllers; import kamkeel.npcs.controllers.SyncController; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; @@ -128,7 +128,7 @@ public class CustomEffectController implements ICustomEffectHandler { file.delete(); } categoryManager.removeItem(id); - SyncController.syncRemove(EnumSyncType.CUSTOM_EFFECTS, foundEffect.getID()); + SyncController.syncRemove(SyncType.CUSTOM_EFFECTS, foundEffect.getID()); saveEffectLoadMap(); } } @@ -700,7 +700,7 @@ public class CustomEffectController implements ICustomEffectHandler { file2.delete(); file.renameTo(file2); nbtTagCompound.removeTag("ScriptData"); - SyncController.syncUpdate(EnumSyncType.CUSTOM_EFFECTS, -1, nbtTagCompound); + SyncController.syncUpdate(SyncType.CUSTOM_EFFECTS, -1, nbtTagCompound); } catch (Exception e) { LogWriter.except(e); } diff --git a/src/main/java/noppes/npcs/controllers/DialogController.java b/src/main/java/noppes/npcs/controllers/DialogController.java index 3490735f3..df652ac89 100644 --- a/src/main/java/noppes/npcs/controllers/DialogController.java +++ b/src/main/java/noppes/npcs/controllers/DialogController.java @@ -1,7 +1,8 @@ package noppes.npcs.controllers; import kamkeel.npcs.controllers.SyncController; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.controllers.sync.handlers.DialogCategorySyncHandler; +import kamkeel.npcs.network.enums.SyncType; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -239,7 +240,7 @@ public class DialogController implements IDialogHandler { dir.mkdirs(); } categories.put(category.id, category); - SyncController.syncUpdate(EnumSyncType.DIALOG_CATEGORY, -1, SyncController.updateDialogCat(category)); + SyncController.syncUpdate(SyncType.DIALOG_CATEGORY, -1, DialogCategorySyncHandler.serializeCategory(category)); } public void removeCategory(int category) { @@ -252,7 +253,7 @@ public class DialogController implements IDialogHandler { for (int dia : cat.dialogs.keySet()) dialogs.remove(dia); categories.remove(category); - SyncController.syncRemove(EnumSyncType.DIALOG_CATEGORY, category); + SyncController.syncRemove(SyncType.DIALOG_CATEGORY, category); } private boolean containsCategoryName(String name) { @@ -302,7 +303,7 @@ public class DialogController implements IDialogHandler { if (file2.exists()) file2.delete(); file.renameTo(file2); - SyncController.syncUpdate(EnumSyncType.DIALOG, category.id, dialog.writeToNBT(new NBTTagCompound())); + SyncController.syncUpdate(SyncType.DIALOG, category.id, dialog.writeToNBT(new NBTTagCompound())); } catch (Exception e) { LogWriter.except(e); } @@ -316,7 +317,7 @@ public class DialogController implements IDialogHandler { return; category.dialogs.remove(dialog.id); dialogs.remove(dialog.id); - SyncController.syncRemove(EnumSyncType.DIALOG, dialog.id); + SyncController.syncRemove(SyncType.DIALOG, dialog.id); } private File getDir() { diff --git a/src/main/java/noppes/npcs/controllers/FactionController.java b/src/main/java/noppes/npcs/controllers/FactionController.java index 9ddf860ea..7da2ebe67 100644 --- a/src/main/java/noppes/npcs/controllers/FactionController.java +++ b/src/main/java/noppes/npcs/controllers/FactionController.java @@ -1,7 +1,7 @@ package noppes.npcs.controllers; import kamkeel.npcs.controllers.SyncController; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -159,7 +159,7 @@ public class FactionController implements IFactionHandler { NBTTagCompound facCompound = new NBTTagCompound(); faction.writeNBT(facCompound); - SyncController.syncUpdate(EnumSyncType.FACTION, -1, facCompound); + SyncController.syncUpdate(SyncType.FACTION, -1, facCompound); saveFactions(); } @@ -195,7 +195,7 @@ public class FactionController implements IFactionHandler { } else { this.saveFactions(); faction.id = -1; - SyncController.syncRemove(EnumSyncType.FACTION, id); + SyncController.syncRemove(SyncType.FACTION, id); return faction; } } else { diff --git a/src/main/java/noppes/npcs/controllers/MagicController.java b/src/main/java/noppes/npcs/controllers/MagicController.java index 40febf172..dff89d015 100644 --- a/src/main/java/noppes/npcs/controllers/MagicController.java +++ b/src/main/java/noppes/npcs/controllers/MagicController.java @@ -1,7 +1,7 @@ package noppes.npcs.controllers; import kamkeel.npcs.controllers.SyncController; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.network.enums.SyncType; import net.minecraft.item.ItemStack; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; @@ -275,7 +275,7 @@ public class MagicController implements IMagicHandler { NBTTagCompound magicCompound = new NBTTagCompound(); mag.writeNBT(magicCompound); - SyncController.syncUpdate(EnumSyncType.MAGIC, -1, magicCompound); + SyncController.syncUpdate(SyncType.MAGIC, -1, magicCompound); saveMagicData(); } @@ -283,7 +283,7 @@ public class MagicController implements IMagicHandler { public void removeMagic(int magicID) { if (magics.containsKey(magicID)) { magics.remove(magicID); - SyncController.syncRemove(EnumSyncType.MAGIC, magicID); + SyncController.syncRemove(SyncType.MAGIC, magicID); saveMagicData(); } } @@ -345,7 +345,7 @@ public class MagicController implements IMagicHandler { NBTTagCompound cycleCompound = new NBTTagCompound(); cycle.writeNBT(cycleCompound); - SyncController.syncUpdate(EnumSyncType.MAGIC_CYCLE, -1, cycleCompound); + SyncController.syncUpdate(SyncType.MAGIC_CYCLE, -1, cycleCompound); saveMagicData(); } @@ -353,7 +353,7 @@ public class MagicController implements IMagicHandler { public void removeCycle(int categoryId) { if (cycles.containsKey(categoryId)) { cycles.remove(categoryId); - SyncController.syncRemove(EnumSyncType.MAGIC_CYCLE, categoryId); + SyncController.syncRemove(SyncType.MAGIC_CYCLE, categoryId); saveMagicData(); } } diff --git a/src/main/java/noppes/npcs/controllers/QuestController.java b/src/main/java/noppes/npcs/controllers/QuestController.java index e9b403ee7..69bbe88d0 100644 --- a/src/main/java/noppes/npcs/controllers/QuestController.java +++ b/src/main/java/noppes/npcs/controllers/QuestController.java @@ -1,7 +1,8 @@ package noppes.npcs.controllers; import kamkeel.npcs.controllers.SyncController; -import kamkeel.npcs.network.enums.EnumSyncType; +import kamkeel.npcs.controllers.sync.handlers.QuestCategorySyncHandler; +import kamkeel.npcs.network.enums.SyncType; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -158,7 +159,7 @@ public class QuestController implements IQuestHandler { for (int dia : cat.quests.keySet()) quests.remove(dia); categories.remove(category); - SyncController.syncRemove(EnumSyncType.QUEST_CATEGORY, category); + SyncController.syncRemove(SyncType.QUEST_CATEGORY, category); } public void saveCategory(QuestCategory category) { @@ -188,7 +189,7 @@ public class QuestController implements IQuestHandler { dir.mkdirs(); } categories.put(category.id, category); - SyncController.syncUpdate(EnumSyncType.QUEST_CATEGORY, -1, SyncController.updateQuestCat(category)); + SyncController.syncUpdate(SyncType.QUEST_CATEGORY, -1, QuestCategorySyncHandler.serializeCategory(category)); } private boolean containsCategoryName(String name) { @@ -245,7 +246,7 @@ public class QuestController implements IQuestHandler { if (file2.exists()) file2.delete(); file.renameTo(file2); - SyncController.syncUpdate(EnumSyncType.QUEST, category.id, quest.writeToNBT(new NBTTagCompound())); + SyncController.syncUpdate(SyncType.QUEST, category.id, quest.writeToNBT(new NBTTagCompound())); } catch (Exception e) { e.printStackTrace(); } 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/JaninoScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java index cbe1230e0..4c9731880 100644 --- a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java @@ -56,11 +56,17 @@ public class JaninoScriptHandler> extends ScriptHandle public void readFromNBT(NBTTagCompound compound) { // JaninoScriptable's helper generics assume JaninoScript, but most of our scripts are JaninoScript. // Keep it simple and safe: treat it as raw. - script = (S) JaninoScript.readFromNBT(compound, (JaninoScript) script, (Supplier) factory); + NBTTagCompound tag = compound.getCompoundTag("Script"); + enabled = tag.getBoolean("enabled"); + script = (S) JaninoScript.readFromNBT(tag, (JaninoScript) script, (Supplier) factory); } public void writeToNBT(NBTTagCompound compound) { - JaninoScript.writeToNBT(compound, (JaninoScript) script); + NBTTagCompound tag = new NBTTagCompound(); + tag.setBoolean("enabled", enabled); + JaninoScript.writeToNBT(tag, (JaninoScript) script); + compound.setTag("Script", tag); + } // -------------------- IScriptHandler -------------------- diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 916f2949a..1da16a3af 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); } } @@ -773,21 +773,21 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA } /** - * Fully reset ability state when changing dimensions. - * Clears cooldowns so the player can immediately use abilities in the new dimension. + * Fully reset transient ability execution state when changing dimensions. + * Cooldowns are preserved so dimension changes cannot bypass them. * Keeps active toggles (dimension change shouldn't reset ongoing effects). */ public void resetOnDimensionChange() { trackingAbilityPosition = false; - onEntityReconstructed(true, false); + onEntityReconstructed(false, false); } /** * Reset ability state when a large-distance teleport is detected during execution. - * Same behavior as dimension change — silent cleanup, clear cooldowns, keep toggles. + * Same behavior as dimension change: silent cleanup, preserve cooldowns, keep toggles. */ public void resetOnTeleport() { - onEntityReconstructed(true, false); + onEntityReconstructed(false, false); } /** @@ -832,9 +832,8 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA currentTarget = null; lastAbilityActivationTime = -1; - // Clear cooldowns (fresh start after relog) - cooldownEndTime = 0; - resetAllPerAbilityCooldowns(); + // Cooldowns are absolute world-time deadlines and are restored from NBT, so relogging + // must not clear them or it becomes a way to bypass them. interruptCooldownRolled = false; // Reset sync tracking so next sync sends fresh state @@ -852,9 +851,10 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA if (currentAbility != null && currentAbility.isExecuting()) { interruptCurrentAbility(null, 0); } - // Clear any remaining transient state (cooldowns, chains, locks) + // Clear any remaining transient state (chains, locks) // Don't clear toggles — they were already saved to NBT by this point - onEntityReconstructed(true, false); + // Don't clear cooldowns — they persist across sessions + onEntityReconstructed(false, false); } // ═══════════════════════════════════════════════════════════════════ @@ -1048,6 +1048,22 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA toggleList.appendTag(toggleNbt); } compound.setTag("ActiveToggles", toggleList); + + // Cooldowns are absolute world-time deadlines, so they stay meaningful across sessions + NBTTagCompound cooldowns = new NBTTagCompound(); + cooldowns.setLong("GlobalEnd", cooldownEndTime); + cooldowns.setInteger("GlobalDuration", globalCooldownDuration); + NBTTagList perAbilityList = new NBTTagList(); + for (Map.Entry entry : perAbilityCooldownEndTimes.entrySet()) { + NBTTagCompound cooldownNbt = new NBTTagCompound(); + cooldownNbt.setString("Key", entry.getKey()); + cooldownNbt.setLong("End", entry.getValue()); + Integer duration = perAbilityCooldownDurations.get(entry.getKey()); + cooldownNbt.setInteger("Duration", duration == null ? 0 : duration); + perAbilityList.appendTag(cooldownNbt); + } + cooldowns.setTag("PerAbility", perAbilityList); + compound.setTag("AbilityCooldowns", cooldowns); } public void readFromNBT(NBTTagCompound compound) { @@ -1082,6 +1098,24 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA setToggleEntryDirect(key, state); } } + + if (compound.hasKey("AbilityCooldowns")) { + cooldownEndTime = 0; + globalCooldownDuration = 0; + resetAllPerAbilityCooldowns(); + + NBTTagCompound cooldowns = compound.getCompoundTag("AbilityCooldowns"); + cooldownEndTime = cooldowns.getLong("GlobalEnd"); + globalCooldownDuration = cooldowns.getInteger("GlobalDuration"); + NBTTagList perAbilityList = cooldowns.getTagList("PerAbility", 10); // 10 = TAG_COMPOUND + for (int i = 0; i < perAbilityList.tagCount(); i++) { + NBTTagCompound entry = perAbilityList.getCompoundTagAt(i); + String key = entry.getString("Key"); + if (key == null || key.isEmpty()) continue; + perAbilityCooldownEndTimes.put(key, entry.getLong("End")); + perAbilityCooldownDurations.put(key, entry.getInteger("Duration")); + } + } } /** diff --git a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java index 096f8f381..d35c6bc9c 100644 --- a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java @@ -10,7 +10,7 @@ import java.util.List; public abstract class ScriptHandler implements IScriptHandler { public List scripts = new ArrayList<>(); protected String scriptLanguage = "ECMAScript"; - protected boolean enabled = false; + protected boolean enabled = true; protected long lastInited = -1; protected boolean canRunScripts() { diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 77927d4b1..ae4aba781 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -301,7 +301,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti @Override public boolean getLeashed() { - return false; //Prevents npcs from being leashed + return false; // Prevents npcs from being leashed } @Override diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index f94be8acc..afa55033c 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -544,15 +544,13 @@ public abstract class JaninoScript implements IScriptUnit { } public static > S readFromNBT(NBTTagCompound compound, S script, Supplier factory) { - if (compound.hasKey("Script")) { - if (script == null) script = factory.get(); - script.readFromNBT(compound.getCompoundTag("Script")); - } + if (script == null) script = factory.get(); + script.readFromNBT(compound); return script; } public static NBTTagCompound writeToNBT(NBTTagCompound compound, JaninoScript script) { - if (script != null) compound.setTag("Script", script.writeToNBT(new NBTTagCompound())); + if (script != null) script.writeToNBT(compound); return compound; } } diff --git a/src/main/java/noppes/npcs/roles/RoleTrader.java b/src/main/java/noppes/npcs/roles/RoleTrader.java index 6aa69d357..717754abf 100644 --- a/src/main/java/noppes/npcs/roles/RoleTrader.java +++ b/src/main/java/noppes/npcs/roles/RoleTrader.java @@ -94,6 +94,15 @@ public class RoleTrader extends RoleInterface { @Override public void readFromNBT(NBTTagCompound nbttagcompound) { + readFromNBT(nbttagcompound, true); + } + + /** + * @param loadFromMarket pull the linked market's stored data over the compound just read. + * False when the compound is an admin edit, which is authoritative and + * would otherwise be overwritten by the market it is about to be saved to. + */ + public void readFromNBT(NBTTagCompound nbttagcompound, boolean loadFromMarket) { String oldMarket = marketName; marketName = nbttagcompound.getString("TraderMarket"); readNBT(nbttagcompound); @@ -101,6 +110,9 @@ public class RoleTrader extends RoleInterface { // Register with MarketRegistry for linked market sync MarketRegistry.updateTraderMarket(oldMarket, marketName, this); + if (!loadFromMarket) + return; + try { Market.getMarket(this, marketName); } catch (Exception ex) {