From ffa44366f88a32c48b8046bd5426413c0ef62775 Mon Sep 17 00:00:00 2001 From: Walnut <39544927+Walnut356@users.noreply.github.com> Date: Sun, 27 Aug 2023 19:20:10 -0500 Subject: [PATCH 1/5] added lastAttackChanged and damage threshold to conversion check --- src/stats/conversions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stats/conversions.ts b/src/stats/conversions.ts index fddd80bb..ac922801 100644 --- a/src/stats/conversions.ts +++ b/src/stats/conversions.ts @@ -161,9 +161,11 @@ function handleConversionCompute( state.lastHitAnimation = null; } + const lastAttackChanged = prevPlayerFrame?.lastAttackLanded != playerFrame.lastAttackLanded; + // If opponent took damage and was put in some kind of stun this frame, either // start a conversion or - if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed) { + if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed || lastAttackChanged || opntDamageTaken > 3.1) { if (!state.conversion) { state.conversion = { playerIndex: indices.opponentIndex, From f15ee261795c0f584c4b051e6e085560f6536fb4 Mon Sep 17 00:00:00 2001 From: Walnut <39544927+Walnut356@users.noreply.github.com> Date: Sun, 27 Aug 2023 19:41:10 -0500 Subject: [PATCH 2/5] included new checks in counter reset --- src/stats/conversions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stats/conversions.ts b/src/stats/conversions.ts index ac922801..ade85b98 100644 --- a/src/stats/conversions.ts +++ b/src/stats/conversions.ts @@ -223,7 +223,7 @@ function handleConversionCompute( state.conversion.currentPercent = opponentFrame.percent ?? 0; } - if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed) { + if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed || lastAttackChanged || opntDamageTaken > 3.1) { // If opponent got grabbed or damaged, reset the reset counter state.resetCounter = 0; } From 5b7e149d4e909e68ce01e31982c70aca710b95a5 Mon Sep 17 00:00:00 2001 From: Walnut <39544927+Walnut356@users.noreply.github.com> Date: Sun, 27 Aug 2023 20:00:42 -0500 Subject: [PATCH 3/5] optional chain -> ternary --- src/stats/conversions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stats/conversions.ts b/src/stats/conversions.ts index ade85b98..9741a8be 100644 --- a/src/stats/conversions.ts +++ b/src/stats/conversions.ts @@ -161,7 +161,7 @@ function handleConversionCompute( state.lastHitAnimation = null; } - const lastAttackChanged = prevPlayerFrame?.lastAttackLanded != playerFrame.lastAttackLanded; + const lastAttackChanged = prevPlayerFrame ? prevPlayerFrame.lastAttackLanded != playerFrame.lastAttackLanded : false; // If opponent took damage and was put in some kind of stun this frame, either // start a conversion or From b098a6ac67525a0c3e9dacf22551a2cd3d595512 Mon Sep 17 00:00:00 2001 From: Walnut <39544927+Walnut356@users.noreply.github.com> Date: Sun, 27 Aug 2023 20:27:23 -0500 Subject: [PATCH 4/5] equality -> strict equality --- src/stats/conversions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stats/conversions.ts b/src/stats/conversions.ts index 9741a8be..c73c1574 100644 --- a/src/stats/conversions.ts +++ b/src/stats/conversions.ts @@ -161,7 +161,7 @@ function handleConversionCompute( state.lastHitAnimation = null; } - const lastAttackChanged = prevPlayerFrame ? prevPlayerFrame.lastAttackLanded != playerFrame.lastAttackLanded : false; + const lastAttackChanged = prevPlayerFrame ? prevPlayerFrame.lastAttackLanded !== playerFrame.lastAttackLanded : false; // If opponent took damage and was put in some kind of stun this frame, either // start a conversion or From 5d316911cf61d68ab9730b389ed7420665dbbe6b Mon Sep 17 00:00:00 2001 From: Walnut <39544927+Walnut356@users.noreply.github.com> Date: Mon, 20 Nov 2023 01:12:58 -0600 Subject: [PATCH 5/5] replaced new checks with hitstun check --- src/stats/common.ts | 77 ++++++++++++++++++++++++++++++++++++++++ src/stats/conversions.ts | 8 ++--- src/types.ts | 1 + src/utils/slpReader.ts | 12 +++++++ 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/stats/common.ts b/src/stats/common.ts index 750f98ec..7170b62f 100644 --- a/src/stats/common.ts +++ b/src/stats/common.ts @@ -261,6 +261,79 @@ export enum State { COMMAND_GRAB_RANGE2_END = 0x152, } +export enum Flags { + BIT_1_1 = 1 << 0, + // Active when any absorber hitbox is active (ness down b) + ABSORB_BUBBLE = 1 << 1, + BIT_1_3 = 1 << 2, + // Active when REFLECT_BUBBLE is active, but the reflected projectile does not change ownership + // (e.g. Mewtwo side b) + REFLECT_NO_STEAL = 1 << 3, + // Active when any projectile reflect bubble is active + REFLECT_BUBBLE = 1 << 4, + BIT_1_6 = 1 << 5, + BIT_1_7 = 1 << 6, + BIT_1_8 = 1 << 7, + BIT_2_1 = 1 << 8, + BIT_2_2 = 1 << 9, + // "Active when a character recieves intangibility or invulnerability due to a subaction that + // is removed when the subaction ends" - per UnclePunch. Little else is known besides this + // description. + SUBACTION_INVULN = 1 << 10, + // Active when the character is fastfalling + FASTFALL = 1 << 11, + // Active when the character is in hitlag, and is the one being hit. Can be thought of as + // `CAN_SDI` + DEFENDER_HITLAG = 1 << 12, + // Active when the character is in hitlag + HITLAG = 1 << 13, + BIT_2_7 = 1 << 14, + BIT_2_8 = 1 << 15, + BIT_3_1 = 1 << 16, + BIT_3_2 = 1 << 17, + // Active when the character has grabbed another character and is holding them + GRAB_HOLD = 1 << 18, + BIT_3_4 = 1 << 19, + BIT_3_5 = 1 << 20, + BIT_3_6 = 1 << 21, + BIT_3_7 = 1 << 22, + // Active when the character is shielding + SHIELDING = 1 << 23, + BIT_4_1 = 1 << 24, + // Active when character is in hitstun + HITSTUN = 1 << 25, + // Dubious meaning, likely related to subframe events (per UnclePunch). Very little is known + // besides offhand remarks + HITBOX_TOUCHING_SHIELD = 1 << 26, + BIT_4_4 = 1 << 27, + BIT_4_5 = 1 << 28, + // Active when character's physical OR projectile Powershield bubble is active + POWERSHIELD_BUBBLE = 1 << 29, + BIT_4_7 = 1 << 30, + BIT_4_8 = 1 << 31, + BIT_5_1 = 1 << 32, + // Active when character is invisible due to cloaking device item/special mode toggle + CLOAKING_DEVICE = 1 << 33, + BIT_5_3 = 1 << 34, + // Active when character is follower-type (e.g. Nana) + FOLLOWER = 1 << 35, + // Character is not processed. Corresponds to Action State `Sleep` (not to be confused with + // `FURA_SLEEP` and `DAMAGE_SLEEP`) + // + // This is typically only relevant for shiek/zelda, and in doubles. When shiek is active, zelda + // will have this flag active (and vice versa). When a doubles teammate has 0 stocks, this flag + // is active as well. + // + // IMPORTANT: If this flag is active in a replay, something has gone horribly wrong. This is + // the bit checked to determine whether or not slippi records a frame event for the character + INACTIVE = 1 << 36, + BIT_5_6 = 1 << 37, + // Active when character is dead + DEAD = 1 << 38, + // Active when character is in the magnifying glass + OFFSCREEN = 1 << 39, +} + export const Timers = { PUNISH_RESET_FRAMES: 45, RECOVERY_RESET_FRAMES: 45, @@ -342,3 +415,7 @@ export function calcDamageTaken(frame: PostFrameUpdateType, prevFrame: PostFrame return percent - prevPercent; } + +export function isInHitstun(flags: bigint): boolean { + return (flags & BigInt(Flags.HITSTUN)) !== BigInt(0); +} diff --git a/src/stats/conversions.ts b/src/stats/conversions.ts index 0aca56df..8decc5d3 100644 --- a/src/stats/conversions.ts +++ b/src/stats/conversions.ts @@ -15,6 +15,7 @@ import { isDamaged, isGrabbed, isInControl, + isInHitstun, Timers, } from "./common"; import type { StatComputer } from "./stats"; @@ -146,6 +147,7 @@ function handleConversionCompute( const opntIsGrabbed = isGrabbed(oppActionStateId); const opntIsCommandGrabbed = isCommandGrabbed(oppActionStateId); const opntDamageTaken = prevOpponentFrame ? calcDamageTaken(opponentFrame, prevOpponentFrame) : 0; + const opntInHitstun = isInHitstun(opponentFrame.flags ?? BigInt(0)); // Keep track of whether actionState changes after a hit. Used to compute move count // When purely using action state there was a bug where if you did two of the same @@ -161,11 +163,9 @@ function handleConversionCompute( state.lastHitAnimation = null; } - const lastAttackChanged = prevPlayerFrame ? prevPlayerFrame.lastAttackLanded !== playerFrame.lastAttackLanded : false; - // If opponent took damage and was put in some kind of stun this frame, either // start a conversion or - if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed || lastAttackChanged || opntDamageTaken > 3.1) { + if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed || opntInHitstun) { if (!state.conversion) { state.conversion = { playerIndex: indices.opponentIndex, @@ -223,7 +223,7 @@ function handleConversionCompute( state.conversion.currentPercent = opponentFrame.percent ?? 0; } - if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed || lastAttackChanged || opntDamageTaken > 3.1) { + if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed || opntInHitstun) { // If opponent got grabbed or damaged, reset the reset counter state.resetCounter = 0; } diff --git a/src/types.ts b/src/types.ts index 486ca488..98ee0d39 100644 --- a/src/types.ts +++ b/src/types.ts @@ -197,6 +197,7 @@ export type PostFrameUpdateType = { lastHitBy: number | null; stocksRemaining: number | null; actionStateCounter: number | null; + flags: bigint | null; miscActionState: number | null; isAirborne: boolean | null; lastGroundId: number | null; diff --git a/src/utils/slpReader.ts b/src/utils/slpReader.ts index 2db26dc5..0828ad97 100644 --- a/src/utils/slpReader.ts +++ b/src/utils/slpReader.ts @@ -503,6 +503,7 @@ export function parseMessage(command: Command, payload: Uint8Array): EventPayloa lastHitBy: readUint8(view, 0x20), stocksRemaining: readUint8(view, 0x21), actionStateCounter: readFloat(view, 0x22), + flags: readFlags(view, 0x26), miscActionState: readFloat(view, 0x2b), isAirborne: readBool(view, 0x2f), lastGroundId: readUint16(view, 0x30), @@ -649,6 +650,17 @@ function readBool(view: DataView, offset: number): boolean | null { return !!view.getUint8(offset); } +function readFlags(view: DataView, offset: number): bigint | null { + if (!canReadFromView(view, offset, 8)) { + return null; + } + + // this overreads by 3 bytes, but those 3 bytes will always exist in any replay that has Flags, + // and we just mask off the extra that we don't need. We're essentially reading in a byte array + // so it needs to be read as little endian. + return view.getBigUint64(offset, true) & BigInt(0x0000_00ff_ffff_ffff); +} + export function getMetadata(slpFile: SlpFileType): MetadataType | null { if (slpFile.metadataLength <= 0) { // This will happen on a severed incomplete file