Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Kam committed 2026-02-24 00:31:36 -05:00
commit 083d38b9d3
5 files changed
+99 -8

No files matched your search

@@ -1169,8 +1169,8 @@ public abstract class Ability implements IAbility, IAbilityAction {
public void interrupt() {
cleanupBurstEntities();
cleanup();
if (interruptible && (phase == AbilityPhase.WINDUP || phase == AbilityPhase.BURST_DELAY)) {
// Interrupted during windup or burst delay - go to dazed state
if (interruptible && phase == AbilityPhase.WINDUP) {
// Interrupted during windup - go to dazed state
phase = AbilityPhase.DAZED;
currentTick = 0;
} else {
@@ -1207,13 +1207,15 @@ public abstract class Ability implements IAbility, IAbilityAction {
/**
* Check if can be interrupted by this damage source.
* Only direct physical hits (not magic, fire, or other indirect damage) can interrupt.
* Interruption only occurs during WINDUP phase - if ability is already active, it completes.
* Interruption only occurs during WINDUP phase (including burst replay windups).
* BURST_DELAY is not interruptible - only the windup animation itself can be interrupted.
* For chained abilities, windUpAll controls whether subsequent entries even have a windup.
*
* @param source The damage source
* @return true if the ability should be interrupted
*/
public boolean canInterrupt(DamageSource source) {
if (!interruptible || (phase != AbilityPhase.WINDUP && phase != AbilityPhase.BURST_DELAY)) {
if (!interruptible || phase != AbilityPhase.WINDUP) {
return false;
}
if (isInvulnerableForCurrentPhase()) {
@@ -35,6 +35,13 @@ import java.util.Set;
*/
public abstract class AbstractDataAbilities {
/**
* Safety timeout (ticks) for abilities stuck in ACTIVE phase.
* If an ability stays in ACTIVE for longer than this without calling signalCompletion(),
* it is force-completed to prevent permanent stuck state. 30 seconds at 20 TPS.
*/
protected static final int MAX_ACTIVE_PHASE_TICKS = 600;
// ═══════════════════════════════════════════════════════════════════
// SHARED RUNTIME STATE (not saved to NBT)
// ═══════════════════════════════════════════════════════════════════
@@ -864,6 +871,16 @@ public abstract class AbstractDataAbilities {
if (positionLocked) releaseLockedPosition();
onBurstDelayReleaseLocks();
}
// Safety timeout: force-complete if stuck in ACTIVE too long.
// Individual ability types should have their own timeouts well below this;
// this is a last-resort failsafe to prevent permanent stuck state.
if (currentAbility != null && currentAbility.getPhase() == AbilityPhase.ACTIVE
&& currentAbility.getCurrentTick() > MAX_ACTIVE_PHASE_TICKS) {
currentAbility.cancel();
handleAbilityCompletion(target);
return;
}
break;
case BURST_DELAY:
@@ -50,6 +50,9 @@ public class ServerTickHandler {
SyncController.syncEffects(player);
ScriptController.Instance.syncClientScripts(player);
// Sync clean ability state to the new client (after beginLogin initializes sync state)
SyncController.syncAbilities(player);
// Send auction notifications on login
if (AuctionController.Instance != null) {
AuctionController.Instance.onPlayerLogin(player);
@@ -285,6 +285,13 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
@Override
protected void onInterruptComplete() {
// Player clears state immediately (unlike NPC which ticks through DAZED)
// Roll cooldown before clearing state — NPC rolls during handleAbilityCompletion
// after ticking through DAZED, but the player skips DAZED entirely.
if (!interruptCooldownRolled && currentAbility != null) {
rollCooldown(currentAbility);
}
interruptCooldownRolled = false;
// Stop the dazed animation that was started in interruptCurrentAbility()
stopAbilityAnimation();
currentAbility = null;
@@ -787,13 +794,70 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
/**
* Reset ability state after death/respawn.
* Preserves cooldowns (interrupt on death already rolled them) but clears
* active toggles (death should end ongoing effects).
* Preserves cooldowns (the death handler interrupted the ability which rolled cooldown
* via onInterruptComplete) but clears active toggles (death should end ongoing effects).
*/
public void resetOnRespawn() {
onEntityReconstructed(false, true);
}
/**
* Reset ability state on login. Clears all transient execution state (current ability,
* locks, chains, cooldowns) that may have leaked from a previous session via the
* PlayerData cache. Toggles are NOT cleared — they are restored from NBT by readFromNBT.
* <p>
* Does NOT send packets — called before the client's sync state is initialized.
* The caller must trigger a full ability sync afterwards (e.g. via syncToClient).
*/
public void resetOnLogin() {
// Silently clean up any stale executing ability
if (currentAbility != null && currentAbility.isExecuting()) {
removeTelegraph(currentAbility);
currentAbility.cleanup();
currentAbility.interrupt();
stopAbilityAnimation();
releaseRotationControl();
releaseLockedPosition();
}
// Clear chain and concurrent state
currentChain = null;
chainEntryIndex = -1;
chainDelayRemaining = -1;
interruptConcurrentSlots();
// Clear all transient state
currentAbility = null;
currentAbilityKey = null;
currentTarget = null;
lastAbilityActivationTime = -1;
// Clear cooldowns (fresh start after relog)
cooldownEndTime = 0;
resetAllPerAbilityCooldowns();
interruptCooldownRolled = false;
// Reset sync tracking so next sync sends fresh state
lastSyncedFlags = 0;
needsSync = true;
}
/**
* Clean up ability state on disconnect. Interrupts any executing ability (fires events,
* rolls cooldown) before the player entity goes away, then clears all transient state.
* This ensures scripts receive proper interrupt/complete events and prevents stale state
* from leaking into the next session via the PlayerData cache.
*/
public void resetOnDisconnect() {
// Interrupt with events so scripts know the ability ended
if (currentAbility != null && currentAbility.isExecuting()) {
interruptCurrentAbility(null, 0);
}
// Clear any remaining transient state (cooldowns, chains, locks)
// Don't clear toggles — they were already saved to NBT by this point
onEntityReconstructed(true, false);
}
// ═══════════════════════════════════════════════════════════════════
// DAMAGE HANDLING
// ═══════════════════════════════════════════════════════════════════
@@ -94,6 +94,10 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData {
private boolean specialKeyDown = false;
public void onLogin() {
// Clear any stale transient ability state that leaked via the PlayerData cache.
// Must happen before the animation check since it may clear ability animation state.
abilityData.resetOnLogin();
// Handle animation state from previous session
AnimationData animationData = this.animationData;
if (animationData != null && animationData.isClientAnimating()) {
@@ -120,8 +124,9 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData {
}
public void onLogout() {
// Cancel any executing ability and release all locks
abilityData.interruptCurrentAbility();
// Interrupt executing ability (fires events, rolls cooldown), then clear
// all transient state so nothing leaks via the PlayerData cache.
abilityData.resetOnDisconnect();
this.partyInvites.clear();
this.actionManager.clear();
}