diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index f27428807..2d6384c27 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -12,6 +12,9 @@ import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumSyncAction; import kamkeel.npcs.network.enums.EnumSyncType; import kamkeel.npcs.network.packets.data.LoginPacket; +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; @@ -865,6 +868,32 @@ public class SyncController { 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); diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index 31a15551a..05d6b0889 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -10,6 +10,7 @@ import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.SyncController; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; import kamkeel.npcs.util.AttributeAttackUtil; import net.minecraft.entity.Entity; @@ -142,8 +143,16 @@ public class ScriptPlayerEventHandler { playerData.timers.update(); } - if (player.ticksExisted % 10 == 0) - SyncController.syncEffects((EntityPlayerMP) player); + if (player.ticksExisted % 10 == 0) { + EntityPlayerMP mp = (EntityPlayerMP) player; + SyncController.syncEffects(mp); + SyncController.syncAbilityCooldowns(mp); + if (playerData.abilityData.needsSync) { + PlayerAbilitySyncPacket.sendToPlayer(mp); + playerData.abilityData.needsSync = false; + } + playerData.abilityData.ensureLockStateClear(); + } if (player.ticksExisted % 10 == 0) CustomEffectController.Instance.runEffects(player); diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index fa439578e..30876a76a 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -1,6 +1,7 @@ package noppes.npcs.controllers.data; import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.SyncController; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; @@ -90,6 +91,14 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA */ private transient byte lastSyncedFlags = 0; + /** + * Dirty flag: set when ability data changes (unlock/lock/select/toggle). + * Checked by the periodic 10-tick sync handler, which sends an ability + * data packet and clears the flag. Avoids scattering sync calls across + * every mutation method. + */ + public transient boolean needsSync = false; + /** * World time when the last ability was activated (for double-press cancel detection). */ @@ -251,7 +260,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA cooldownEndTime = endTime; globalCooldownDuration = duration; } - syncCooldownToClient(); } @Override @@ -259,7 +267,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA int duration = chain.getCooldownTicks(); cooldownEndTime = getWorldTime() + duration; globalCooldownDuration = duration; - syncCooldownToClient(); } @Override @@ -450,6 +457,8 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA playAbilityAnimation(ability.getWindUpAnimation()); } + // Immediate sync on ability use + syncToClient(); return true; } @@ -458,31 +467,30 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA // ═══════════════════════════════════════════════════════════════════ /** - * Sync ability data to the client. - * Called when abilities change (unlock/lock/select) and on login. + * Full sync of all ability state to the client. + * Delegates to {@link SyncController#syncAbilities(EntityPlayerMP)}. */ public void syncToClient() { EntityPlayer player = playerData.player; if (player instanceof EntityPlayerMP) { - PlayerAbilitySyncPacket.sendToPlayer((EntityPlayerMP) player); - syncCooldownToClient(); + SyncController.syncAbilities((EntityPlayerMP) player); } } /** - * Sync cooldown state (global + per-ability) to the client. - * Sent as a lightweight packet separate from full ability sync. + * Lightweight sync of cooldown state only to the client. + * Delegates to {@link SyncController#syncAbilityCooldowns(EntityPlayerMP)}. */ public void syncCooldownToClient() { EntityPlayer player = playerData.player; if (player instanceof EntityPlayerMP) { - AbilityCooldownSyncPacket.sendToPlayer((EntityPlayerMP) player); + SyncController.syncAbilityCooldowns((EntityPlayerMP) player); } } @Override protected void onToggleStateChanged(String key, boolean active, int state) { - syncToClient(); + needsSync = true; } @Override @@ -519,20 +527,20 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA public void setSelectedIndex(int index) { if (index == -1 || (index >= 0 && index < unlockedAbilities.size())) { this.selectedIndex = index; - syncToClient(); + needsSync = true; } } public void selectNext() { if (unlockedAbilities.isEmpty()) return; selectedIndex = (selectedIndex + 1) % unlockedAbilities.size(); - syncToClient(); + needsSync = true; } public void selectPrevious() { if (unlockedAbilities.isEmpty()) return; selectedIndex = (selectedIndex - 1 + unlockedAbilities.size()) % unlockedAbilities.size(); - syncToClient(); + needsSync = true; } public String getSelectedAbilityKey() { @@ -561,13 +569,13 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA if (selectedIndex >= unlockedAbilities.size()) { selectedIndex = Math.max(0, unlockedAbilities.size() - 1); } - syncToClient(); + needsSync = true; } public void unlockAbility(String key) { if (key != null && !key.isEmpty() && !unlockedAbilities.contains(key)) { unlockedAbilities.add(key); - syncToClient(); + needsSync = true; } } @@ -576,7 +584,7 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA if (selectedIndex >= unlockedAbilities.size()) { selectedIndex = Math.max(0, unlockedAbilities.size() - 1); } - syncToClient(); + needsSync = true; } public boolean hasUnlockedAbility(String key) { @@ -620,7 +628,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA public void resetCooldown(String key) { resetPerAbilityCooldown(key); cooldownEndTime = 0; - syncCooldownToClient(); } /** @@ -629,7 +636,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA public void resetAllCooldowns() { cooldownEndTime = 0; resetAllPerAbilityCooldowns(); - syncCooldownToClient(); } // ═══════════════════════════════════════════════════════════════════ @@ -664,7 +670,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA public void resetCooldown() { cooldownEndTime = 0; resetAllPerAbilityCooldowns(); - syncCooldownToClient(); } // ═══════════════════════════════════════════════════════════════════ @@ -969,6 +974,20 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA } } + /** + * Periodic safety check: if no ability is executing but the client may have + * stale lock flags, send a clear. Prevents stuck clients from missed packets. + */ + public void ensureLockStateClear() { + if (isExecutingAbility() || isExecutingChain()) return; + EntityPlayer player = playerData.player; + if (!(player instanceof EntityPlayerMP)) return; + if (lastSyncedFlags != 0) { + lastSyncedFlags = 0; + PlayerAbilityStatePacket.sendToPlayer((EntityPlayerMP) player, (byte) 0, 0, 0); + } + } + // ═══════════════════════════════════════════════════════════════════ // SCRIPT EVENTS (Player-specific: start event only, others via abstract) // ═══════════════════════════════════════════════════════════════════ @@ -1051,6 +1070,10 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA */ private void validateUnlockedAbilities() { if (AbilityController.Instance == null) return; + // Only validate on the server - the client trusts server-sent data. + // Custom ability definitions may not have been synced to the client yet, + // which would cause valid abilities to be incorrectly pruned. + if (playerData == null || !(playerData.player instanceof EntityPlayerMP)) return; Iterator it = unlockedAbilities.iterator(); while (it.hasNext()) { diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityHotbarData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityHotbarData.java index abfea96e3..6ff49c4b6 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityHotbarData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityHotbarData.java @@ -1,5 +1,6 @@ package noppes.npcs.controllers.data; +import cpw.mods.fml.common.FMLCommonHandler; import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.network.packets.data.ability.AbilityHotbarSyncPacket; @@ -54,6 +55,8 @@ public class PlayerAbilityHotbarData { public void validateSlots() { if (AbilityController.Instance == null) return; + // Only validate on the server - the client trusts server-sent data. + if (FMLCommonHandler.instance().getEffectiveSide().isClient()) return; for (AbilityHotbarData slot : slots) { if (slot.isEmpty()) continue; boolean valid; @@ -75,6 +78,10 @@ public class PlayerAbilityHotbarData { } } + /** + * Sync hotbar data only to the client. + * Used after server confirms a client hotbar edit. + */ public void syncToClient(EntityPlayer player) { if (player instanceof EntityPlayerMP) { AbilityHotbarSyncPacket.sendToPlayer((EntityPlayerMP) player); diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 8283d4f6c..f37def637 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -117,12 +117,6 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { if (!controller.playerEffects.containsKey(playerID)) { controller.playerEffects.put(playerID, effectData.getEffects()); } - - // Sync player ability data to client - abilityData.syncToClient(); - - // Sync hotbar data to client - hotbarData.syncToClient(player); } public void onLogout() { @@ -254,6 +248,8 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { factionData.saveNBTData(compound); mailData.saveNBTData(compound); tradeData.writeToNBT(compound); + abilityData.writeToNBT(compound); + hotbarData.writeToNBT(compound); compound.setString("PlayerName", playername); compound.setString("UUID", uuid); DBCAddon.instance.writeToNBT(this, compound); @@ -268,6 +264,8 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { factionData.loadNBTData(data); mailData.loadNBTData(data); tradeData.readFromNBT(data); + abilityData.readFromNBT(data); + hotbarData.readFromNBT(data); if (player != null) { playername = player.getCommandSenderName(); uuid = player.getPersistentID().toString();