Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed conversion detection for mario/doc's Cape #130

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions src/stats/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
6 changes: 4 additions & 2 deletions src/stats/conversions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
isDamaged,
isGrabbed,
isInControl,
isInHitstun,
Timers,
} from "./common";
import type { StatComputer } from "./stats";
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions src/utils/slpReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand Down
Loading