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 6e823e97..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 @@ -163,7 +165,7 @@ function handleConversionCompute( // 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 || opntInHitstun) { if (!state.conversion) { state.conversion = { playerIndex: indices.opponentIndex, @@ -221,7 +223,7 @@ function handleConversionCompute( state.conversion.currentPercent = opponentFrame.percent ?? 0; } - if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed) { + 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