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:
+ *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, - MapEach 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: + *
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 SetNote: 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) { + ConcurrentHashMapEach {@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
+ * 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
+ * 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
+ * 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
+ * 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
+ * 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> extends ScriptHandle
public void readFromNBT(NBTTagCompound compound) {
// JaninoScriptable's helper generics assume JaninoScript 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