Merge branch 'dev' into pr-278

This commit is contained in:
KAMKEEL committed 2026-08-11 05:51:40 -04:00
commit 1532ef722c
90 files changed
+3783 -1261

No files matched your search

+2 -1
View File
@@ -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;
}
+2 -1
View File
@@ -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;
}
}
@@ -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");
}
@@ -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");
}
}
@@ -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.
*/
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -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<Integer, MagicEntry> 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;
@@ -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<FieldDef> 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<Integer, CustomEffect> map = CustomEffectController.getInstance().getEffectMap(effectIndex);
return map != null ? map.get(effectId) : null;
}
@@ -38,20 +38,7 @@ public class ConditionQuestCompleted extends AbilityCondition {
@SideOnly(Side.CLIENT)
@Override
public void getConditionDefinitions(List<FieldDef> 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)
@@ -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()).
@@ -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;
@@ -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
// ═══════════════════════════════════════════════════════════════════
@@ -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
@@ -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"),
@@ -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);
@@ -300,14 +300,7 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
@SideOnly(Side.CLIENT)
@Override
protected void addPresetFieldDef(List<FieldDef> 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)
@@ -690,14 +690,7 @@ public abstract class AbilityZone extends Ability {
@SideOnly(Side.CLIENT)
protected void addPresetFieldDef(List<FieldDef> 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)
@@ -248,7 +248,12 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
* 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;
}
/**
@@ -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
@@ -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.
*
* <p>Each handler is registered with {@link SyncRegistry} and covers
* all three sync actions: RELOAD (full), UPDATE (single entity),
* and REMOVE (delete entity).</p>
*
* <p>Handlers that don't support UPDATE or REMOVE should use the
* default no-op implementations.</p>
*/
public interface SyncHandler {
// ========== SERVER-SIDE ==========
/**
* Serialize ALL objects of this type into an NBTTagCompound.
* This is the supplier for the cache system.
*
* <p>Called lazily when the cache is dirty and a player needs data.
* The returned compound is cached and chunked for transmission.</p>
*
* @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.
*
* <p>Implementations should:
* <ol>
* <li>Deserialize from the compound into a sync buffer</li>
* <li>Atomically swap the sync buffer to the primary map</li>
* <li>Reset the sync buffer for the next reload</li>
* </ol>
*
* @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.
*
* <p>Default: no-op. Types that don't support individual updates
* use {@code syncAll()} for full reload instead.</p>
*
* @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.
*
* <p>Default: no-op. Types that don't support individual updates
* use {@code syncAll()} for full reload instead.</p>
*
* @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.
*
* <p>Default: no-op. Types that don't support individual updates
* use {@code syncAll()} for full reload instead.</p>
*
* @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.
*
* <p>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.</p>
*/
default boolean isCachedType() {
return true;
}
/**
* Get the set of cache types that must be invalidated when
* this type changes. Used for cascade invalidation.
*
* <p>Most types return just themselves. Parent-child types
* (e.g. DIALOG maps to DIALOG_CATEGORY) point children
* to the parent's cache.</p>
*
* @param self the SyncType this handler is registered under
* @return the set of types to invalidate
*/
default Set<SyncType> getInvalidationTargets(SyncType self) {
Set<SyncType> set = new HashSet<>();
set.add(self);
return set;
}
}
@@ -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<SyncType, SyncHandler> 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<SyncType> list = new ArrayList<>();
for (Map.Entry<SyncType, SyncHandler> e : handlers.entrySet())
if (e.getValue().isCachedType())
list.add(e.getKey());
loginTypesCache = list.toArray(new SyncType[0]);
}
return loginTypesCache;
}
public static Set<SyncType> 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<SyncType> getRegisteredTypes() {
return Collections.unmodifiableCollection(handlers.keySet());
}
}
@@ -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)
}
@@ -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)
}
@@ -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<String, ChainedAbility> 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<String, ChainedAbility> 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)
}
@@ -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<String, Ability> 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<String, Ability> 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)
}
@@ -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;
}
}
@@ -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<Integer, Dialog> 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;
}
}
@@ -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<SyncType> getInvalidationTargets(SyncType self) {
// Child invalidates the parent cache
Set<SyncType> set = new HashSet<>();
set.add(SyncType.DIALOG_CATEGORY);
return set;
}
}
@@ -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<Integer, Faction>();
}
@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;
}
}
@@ -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;
}
}
@@ -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.
*
* <p>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.</p>
*/
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.
* <p>Bug fix: The original code incorrectly removed from {@code cycles}
* instead of {@code magics} after dissociating cycle associations.</p>
*/
@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;
}
}
@@ -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.
*
* <p>Abilities are synced per-player on demand (e.g. ability unlock,
* entity reconstruction, periodic cooldown ticks).</p>
*/
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);
}
}
@@ -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;
}
}
@@ -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.
*
* <p>Effects are synced per-player on demand (e.g. effect application,
* periodic ticks). The data mutates {@code playerData.effectData}
* before sending the packet.</p>
*/
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<EffectKey, PlayerEffect> 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);
}
}
}
@@ -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<Integer, Quest> 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;
}
}
@@ -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<SyncType> getInvalidationTargets(SyncType self) {
// Child invalidates the parent cache
Set<SyncType> set = new HashSet<>();
set.add(SyncType.QUEST_CATEGORY);
return set;
}
}
@@ -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<SyncType> getInvalidationTargets(SyncType self) {
Set<SyncType> set = new HashSet<>();
set.add(SyncType.WORKBENCH_RECIPES);
return set;
}
// RELOAD-only: supportsUpdate() and supportsRemove() remain false (default)
}
@@ -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);
@@ -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; }
@@ -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");
@@ -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
}
@@ -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.
*
* <p>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.</p>
*
* <p>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).</p>
*
* <p>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.</p>
*/
public final class SyncType {
// ──────────────────── Registry ────────────────────
private static final Map<String, SyncType> 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<SyncType> 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;
}
}
@@ -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<EnumSyncType, Integer> revisionSnapshot = new EnumMap<>(EnumSyncType.class);
private final Map<SyncType, Integer> revisionSnapshot = new LinkedHashMap<>();
public LoginPacket() {
}
public LoginPacket(String serverCacheKey, EnumMap<EnumSyncType, Integer> revisionSnapshot) {
public LoginPacket(String serverCacheKey, Map<SyncType, Integer> 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<EnumSyncType, Integer> entry : revisionSnapshot.entrySet()) {
for (Map.Entry<SyncType, Integer> 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<EnumSyncType, Integer> serverRevisions = new EnumMap<>(EnumSyncType.class);
Map<SyncType, Integer> 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);
@@ -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));
}
}
@@ -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"));
}
@@ -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;
}
}
@@ -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<EnumSyncType, Integer> revisions = new EnumMap<>(EnumSyncType.class);
private final Map<SyncType, Integer> revisions = new LinkedHashMap<>();
public SyncRevisionInfoPacket() {
}
public SyncRevisionInfoPacket(String serverKey, String previousServerKey, EnumMap<EnumSyncType, Integer> revisions) {
public SyncRevisionInfoPacket(String serverKey, String previousServerKey, Map<SyncType, Integer> 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<EnumSyncType, Integer> entry : revisions.entrySet()) {
for (Map.Entry<SyncType, Integer> 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<EnumSyncType, Integer> incomingRevisions = new EnumMap<>(EnumSyncType.class);
Map<SyncType, Integer> 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);
}
}
@@ -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();
}
@@ -73,7 +73,7 @@ public final class CategoryItemsRequestPacket extends AbstractPacket {
// Send tag map for the items
HashMap<String, HashSet<UUID>> tagMap = getTagMapByCat(type, categoryId);
if (tagMap != null && !tagMap.isEmpty()) {
if (tagMap != null) {
TagController.sendCategoryTagMap((EntityPlayerMP) player, tagMap);
}
}
@@ -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;
}
}
@@ -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);
}
@@ -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,
@@ -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/");
}
+14 -9
View File
@@ -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());
}
}
@@ -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);
}
}
}
@@ -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();
}
@@ -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) {
@@ -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) {
@@ -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
@@ -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) {
@@ -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);
@@ -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<String, EnumMap<EnumSyncType, Integer>> clientRevisionCache = new HashMap<>();
private static final Map<String, Map<SyncType, Integer>> clientRevisionCache = new HashMap<>();
public static Party party;
public static boolean allowProfiles = true;
public static boolean allowParties = true;
public static void setActiveServer(String serverKey, EnumMap<EnumSyncType, Integer> serverRevisions) {
public static void setActiveServer(String serverKey, Map<SyncType, Integer> serverRevisions) {
String normalizedKey = serverKey == null ? "" : serverKey;
String previousKey = activeServerKey == null ? "" : activeServerKey;
lastServerKey = previousKey;
@@ -54,9 +54,9 @@ public class ClientCacheHandler {
return;
}
EnumMap<EnumSyncType, Integer> cached = clientRevisionCache.computeIfAbsent(
Map<SyncType, Integer> 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<EnumSyncType, Integer> getCachedRevisionsForServer(String serverKey) {
public static Map<SyncType, Integer> getCachedRevisionsForServer(String serverKey) {
String key = serverKey == null ? "" : serverKey;
if (key.isEmpty()) {
return new EnumMap<>(EnumSyncType.class);
return new LinkedHashMap<>();
}
EnumMap<EnumSyncType, Integer> revisions = clientRevisionCache.get(key);
Map<SyncType, Integer> 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<EnumSyncType, Integer> revisions = clientRevisionCache.computeIfAbsent(
Map<SyncType, Integer> 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();
}
@@ -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;
@@ -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.
* <p>
* 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<String> 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",
@@ -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.
* <p>
* 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<String> 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",
@@ -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.
* <p>
* 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<Integer, MagicEntry> 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");
@@ -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.
* <p>
* 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<String, Integer> displayNameToId = new HashMap<>();
private final HashMap<String, Integer> allDisplayNameToId = new HashMap<>();
@@ -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<String> lines, int startY, int align, int color) {
int y = startY;
FontRenderer font = mc.fontRenderer;
@@ -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;
@@ -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;
@@ -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.
* <p>
* 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 = "";
@@ -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);
@@ -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;
@@ -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);
@@ -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;
}
}
@@ -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<TypeVariable<?>>());
}
/**
* @param resolving type variables whose bounds are currently being expanded, so a
* self-referential bound such as {@code E extends Enum<E>} terminates
*/
private static TypeInfo fromGenericType(Type type, Set<TypeVariable<?>> 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<String>
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<TypeInfo> 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<String>[]
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;
}
@@ -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);
@@ -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);
}
@@ -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() {
@@ -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 {
@@ -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();
}
}
@@ -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();
}
@@ -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;
@@ -56,11 +56,17 @@ public class JaninoScriptHandler<S extends JaninoScript<?>> extends ScriptHandle
public void readFromNBT(NBTTagCompound compound) {
// JaninoScriptable's helper generics assume JaninoScript<T>, 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 --------------------
@@ -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<String, Long> 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"));
}
}
}
/**
@@ -10,7 +10,7 @@ import java.util.List;
public abstract class ScriptHandler implements IScriptHandler {
public List<IScriptUnit> scripts = new ArrayList<>();
protected String scriptLanguage = "ECMAScript";
protected boolean enabled = false;
protected boolean enabled = true;
protected long lastInited = -1;
protected boolean canRunScripts() {
@@ -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
@@ -544,15 +544,13 @@ public abstract class JaninoScript<T> implements IScriptUnit {
}
public static <T, S extends JaninoScript<T>> S readFromNBT(NBTTagCompound compound, S script, Supplier<S> 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 <T> NBTTagCompound writeToNBT(NBTTagCompound compound, JaninoScript<T> script) {
if (script != null) compound.setTag("Script", script.writeToNBT(new NBTTagCompound()));
if (script != null) script.writeToNBT(compound);
return compound;
}
}
@@ -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) {