Skip to content

Commit

Permalink
Add better support for stage RTA mode. (#21)
Browse files Browse the repository at this point in the history
* Change support for stage rta to better match leaderboard method.

* Update README.md and restore some code that should not have been removed.

* Add timer offset to readme.

* Update sm64-livesplit-autosplitter.asl
  • Loading branch information
fmichea authored Jan 31, 2024
1 parent 884b5c0 commit 852f506
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 57 deletions.
35 changes: 13 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,18 +168,20 @@ to port these changes every time you upgrade the script.
RTA Mode
--------

RTA mode is enabled when keyword ``run-mode=rta`` is present in first segment name for the splits. It adds reset and start conditions,
with usamune ROM in mind.
RTA mode is enabled when keyword ``[run-mode=rta]`` is present in the first split. It adds reset and start conditions,
with usamune ROM in mind. It aims to time similarly to how leaderboard runs are expected to be timed.

Reset: If star count decreases, the timer is reset.
Reset: If either of the dpad down or L button is pressed, the timer is reset.

Additional start conditions:

- Star select screen is displayed (either through painting entry or usamune ROM menu)
- A star is selected on the mission select scene. (timer offset: ``0.20``)
- Stage is exited (fade-out)
- A key door is touched.

Splitting conditions (incl. last split) are identical.
Additional end condition: immediate split on last star grab (can be delayed with ``[fade]`` keyword).

Splitting conditions (incl. last split) are identical. Note that for RTA done on the 120 file, you will have to specify ``[120]`` for each split. This will split immediately on 100 coin stars. To avoid this issue, use ``[120,fade]``.

Non-Stop
--------
Expand Down Expand Up @@ -220,23 +222,6 @@ To reset:
- Under "MENU > DATA > For 70 Star" select "34". To reset your splits, flip between "OFF" and "34".
- Touch the upstairs door, timer will star with door unlocking animation.

### Single Stage RTA

![WF 70/120 Stage RTA](./images/stage_rta.jpg)

In the single stage RTA case, star count requirements should be starting from 0 stars. Usamune interaction looks like this:

Once in a while:

- Under "MENU > STAGE > STARTXT", select "1 Star", press A, increase CNT to 255 (each digit can be increased independently) and
press A again. This removes the textboxes when certain number of stars are collected. Otherwise too hard to manage correctly.

To reset:

- If painting entry is desired, get in position. Otherwise usamune ROM stage select can be used.
- Under "MENU > DATA > For 0 Star", flip between OFF and DWEND. DWEND should be selected at the start of stage RTA. This resets all stars in stage.
- Enter painting or select stage in menu, this will start the timer.

# Settings

> :warning: Please leave all boxes unchecked unless you know what you are doing.
Expand Down Expand Up @@ -321,3 +306,9 @@ For entry and exit splits, here are all of the shortcuts that can be used to ref
| Wet Dry World | ``wdw`` |
| Whomp's Fortress | ``wf`` |
| Wing Mario Over the Rainbow | ``wmotr`` |

## Developing / Debugging

The program called "DbgView" can be used to see all printed log statenments by the auto splitter, as well as log made
from livesplit when it loads the auto splitter, which can be used to find syntax errors and the like. The process ID
can be filtered once it is known in the program directly to avoid the other spam.
146 changes: 111 additions & 35 deletions sm64-livesplit-autosplitter.asl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Version: 3.0.1
// Version: 4.0.0-beta

// Code: https://github.com/n64decomp/sm64/
// Address map: https://github.com/SM64-TAS-ABC/STROOP/tree/Development/STROOP/Mappings
Expand All @@ -25,9 +25,6 @@ state("Project64") {
ushort starCountJP : "Project64.exe", 0xD6A1C, 0x339EA8; // N64 addr: 0x80339E00 + 0xAA (struct field)
ushort starCountUS : "Project64.exe", 0xD6A1C, 0x33B218; // N64 addr: 0x8033B170 + 0xAA (struct field)

uint musicJP : "Project64.exe", 0xD6A1C, 0x222A1C;
uint musicUS : "Project64.exe", 0xD6A1C, 0x22261C;

ushort hudCameraModeJP : "Project64.exe", 0xD6A1C, 0x3314FA; // N64 addr: 0x803314F8
ushort hudCameraModeUS : "Project64.exe", 0xD6A1C, 0x33260A; // N64 addr: 0x80332608

Expand Down Expand Up @@ -65,6 +62,18 @@ state("Project64") {

short menuClickPosJP : "Project64.exe", 0xD6A1C, 0x1A7BE8;
short menuClickPosUS : "Project64.exe", 0xD6A1C, 0x1A7D28;

uint behaviorSegmentInfoJP : "Project64.exe", 0xD6A1C, 0x33A0DC; // N64 addr: sSegmentTable[0x13] = 0x8033A090 + 4 * 0x13 = 0x8033A0DC
uint behaviorSegmentInfoUS : "Project64.exe", 0xD6A1C, 0x33B44C; // N64 addr: sSegmentTable[0x13] = 0x8033B400 + 4 * 0x13 = 0x8033B44C

uint object0TimerJP : "Project64.exe", 0xD6A1C, 0x33C26C; // N64 addr: 0x8033C118 + 0x154
uint object0TimerUS : "Project64.exe", 0xD6A1C, 0x33D5DC; // N64 addr: 0x8033D488 + 0x154

uint object0BehaviorJP : "Project64.exe", 0xD6A1C, 0x33C324; // N64 addr: 0x8033C118 + 0x20C
uint object0BehaviorUS : "Project64.exe", 0xD6A1C, 0x33D694; // N64 addr: 0x8033D488 + 0x20C

ushort controller0ButtonsJP : "Project64.exe", 0xD6A1C, 0x339C30; // N64 addr: 0x80339C20 + 0x10 (gControllers[0].buttonDown)
ushort controller0ButtonsUS : "Project64.exe", 0xD6A1C, 0x33AFA0; // N64 addr: 0x8033AF90 + 0x10 (gControllers[0].buttonDown)
}

startup {
Expand Down Expand Up @@ -261,6 +270,7 @@ startup {
uint ACT_ENTERING_STAR_DOOR = 0x1331;

uint ACT_JUMBO_STAR_CUTSCENE = 0x1909;
uint ACT_FALL_AFTER_STAR_GRAB = 0x1904;

uint[] DOOR_XCAM_COUNT_ACTIONS = new uint[]{
0x1320, // ACT_PULLING_DOOR
Expand All @@ -271,8 +281,6 @@ startup {

uint DEBUG_FUNCTION_VALUE = 0x27bdffd8;

uint STAR_SELECT_MUSIC = 0x800d1600;

ushort FIXED_CAMERA_HUD = 0x4;
ushort FIXED_CAMERA_CDOWN_HUD = 0xC;

Expand All @@ -281,6 +289,12 @@ startup {

uint KEY_FLAGS = 0x10 | 0x20;

uint BHV_ACT_SELECTOR_JP = 0x13003028;
uint BHV_ACT_SELECTOR_US = 0x13003048;

ushort BUTTON_L_TRIG = 0x0020;
ushort BUTTON_DPAD_DOWN = 0x0400;

// Allows defining a 3D rectangular box to checkpoint mario's position for various splitting conditions.
Func<byte, float, float, float, float, float, float, dynamic> create3DBox = delegate(byte stageIndex, float x1, float y1, float z1, float x2, float y2, float z2) {
dynamic box = new ExpandoObject();
Expand Down Expand Up @@ -396,6 +410,9 @@ startup {
data.wantToReset = false;
data.wantToResetTiming = 0;

data.starSelectTimerUpdateCount = 0;
data.starSelectLastTimerUpdateGT = 0;

return data;
};

Expand Down Expand Up @@ -524,10 +541,6 @@ startup {
return varsD.data.runConfig.isJapaneseVersion ? state.starCountJP : state.starCountUS;
};

Func<dynamic, dynamic, uint> getMusicTrack = delegate(dynamic varsD, dynamic state) {
return varsD.data.runConfig.isJapaneseVersion ? state.musicJP : state.musicUS;
};

Func<dynamic, dynamic, ushort> getHUDCameraMode = delegate(dynamic varsD, dynamic state) {
return varsD.data.runConfig.isJapaneseVersion ? state.hudCameraModeJP : state.hudCameraModeUS;
};
Expand All @@ -552,6 +565,28 @@ startup {
return varsD.data.runConfig.isJapaneseVersion ? state.nonStopInteractionOverwriteJP : state.nonStopInteractionOverwriteUS;
};

Func<dynamic, dynamic, uint> getBehaviorSegmentInfo = delegate(dynamic varsD, dynamic state) {
return varsD.data.runConfig.isJapaneseVersion ? state.behaviorSegmentInfoJP : state.behaviorSegmentInfoUS;
};

Func<dynamic, dynamic, uint> getObject0Behavior = delegate(dynamic varsD, dynamic state) {
uint segmentOffset = getBehaviorSegmentInfo(varsD, state);
uint virt = varsD.data.runConfig.isJapaneseVersion ? state.object0BehaviorJP : state.object0BehaviorUS;
return 0x13000000 + ((virt & 0x1FFFFFFF) - segmentOffset);
};

Func<dynamic, uint> getBhvActSelector = delegate(dynamic varsD) {
return varsD.data.runConfig.isJapaneseVersion ? BHV_ACT_SELECTOR_JP : BHV_ACT_SELECTOR_US;
};

Func<dynamic, dynamic, uint> getObject0Timer = delegate(dynamic varsD, dynamic state) {
return varsD.data.runConfig.isJapaneseVersion ? state.object0TimerJP : state.object0TimerUS;
};

Func<dynamic, dynamic, ushort> getController0Buttons = delegate(dynamic varsD, dynamic state) {
return varsD.data.runConfig.isJapaneseVersion ? state.controller0ButtonsJP : state.controller0ButtonsUS;
};

Func<dynamic, dynamic, uint> getFileFlags = delegate(dynamic varsD, dynamic state) {
if (varsD.data.runLiveData.selectedFileID == 0) {
return varsD.data.runConfig.isJapaneseVersion ? state.fileAFlagsJP : state.fileAFlagsUS;
Expand Down Expand Up @@ -826,6 +861,21 @@ startup {
varsD.timerModel.Reset();
}

// If we are in a star select menu, we keep track of when the timer was last updated to find when it stops
// (a star was selected) so stage rta mode can start the timer.
uint bhvActSelector = getBhvActSelector(varsD);
uint object0Behavior_current = getObject0Behavior(varsD, currentD);

uint object0Timer_old = getObject0Timer(varsD, oldD);
uint object0Timer_current = getObject0Timer(varsD, currentD);

uint globalTimer_current = getGlobalTimer(varsD, currentD);

if (object0Behavior_current == bhvActSelector && object0Timer_old != object0Timer_current) {
varsD.data.runLiveData.starSelectTimerUpdateCount += 1;
varsD.data.runLiveData.starSelectLastTimerUpdateGT = globalTimer_current;
}

return true;
};

Expand Down Expand Up @@ -876,6 +926,7 @@ startup {
uint gameRuntime_old = getGameRuntime(varsD, oldD);
uint gameRuntime_current = getGameRuntime(varsD, currentD);

uint globalTimer_old = getGlobalTimer(varsD, oldD);
uint globalTimer_current = getGlobalTimer(varsD, currentD);

byte stageIndex_old = getStageIndex(varsD, oldD);
Expand All @@ -884,9 +935,6 @@ startup {
uint animation_old = getAnimation(varsD, oldD);
uint animation_current = getAnimation(varsD, currentD);

uint music_old = getMusicTrack(varsD, oldD);
uint music_current = getMusicTrack(varsD, currentD);

byte menuSelectedButtonID_old = getMenuSelectedButtonID(varsD, oldD);
byte menuSelectedButtonID_current = getMenuSelectedButtonID(varsD, currentD);

Expand Down Expand Up @@ -915,6 +963,9 @@ startup {
}

// RTA mode, timer starts when we see star select screen, we leave a stage (fade-out) or we touch a door.
uint bhvActSelector = getBhvActSelector(varsD);
uint object0Behavior_current = getObject0Behavior(varsD, currentD);

if (
!varsD.settings.disableRTAMode &&
varsD.data.runConfig.isRTAMode &&
Expand All @@ -925,8 +976,9 @@ startup {
animation_current == ACT_UNLOCKING_KEY_DOOR
) ||
(
music_old != music_current &&
music_current == STAR_SELECT_MUSIC
object0Behavior_current == bhvActSelector &&
2 <= varsD.data.runLiveData.starSelectTimerUpdateCount &&
varsD.data.runLiveData.starSelectLastTimerUpdateGT <= globalTimer_current - 5
)
)
) {
Expand Down Expand Up @@ -961,10 +1013,12 @@ startup {
varsD.data.runLiveData.wantToResetTiming = gameRuntime_old;
}

ushort controller0Buttons = getController0Buttons(varsD, currentD);

bool isResetRTA = (
!vars.settings.disableRTAMode &&
varsD.data.runConfig.isRTAMode &&
starCount_current < starCount_old
(controller0Buttons & (BUTTON_L_TRIG | BUTTON_DPAD_DOWN)) != 0
);

bool isReset = isResetRTA;
Expand Down Expand Up @@ -1127,15 +1181,25 @@ startup {
);

// This is the last split and we are getting the last star, split immediately.
bool isLastSplit = varsD.settings.currentSplitIndex == varsD.settings.splitCount - 1;

addImmediateSplittingCondition(
(
splitConfig.type == SPLIT_TYPE_FINAL_STAR_GRAB ||
varsD.settings.currentSplitIndex == varsD.settings.splitCount - 1
splitConfig.type == SPLIT_TYPE_FINAL_STAR_GRAB || isLastSplit
) &&
animation_old != animation_current &&
animation_current == ACT_JUMBO_STAR_CUTSCENE
);

addImmediateSplittingCondition(
!splitConfig.isForcedFade &&
!vars.settings.disableRTAMode &&
varsD.data.runConfig.isRTAMode &&
isLastSplit &&
animation_old != animation_current &&
animation_current == ACT_FALL_AFTER_STAR_GRAB
);

// When we are doing castle movement split and we enter a stage.
addImmediateSplittingCondition(
splitConfig.type == SPLIT_TYPE_CASTLE_MOVEMENT &&
Expand Down Expand Up @@ -1248,7 +1312,7 @@ startup {
return varsD;
};

Func<bool, uint, uint, byte, uint, ushort, uint, dynamic> mockStateBuilder = delegate(bool isJP, uint gameRuntime, uint globalTimer, byte stageIndex, uint animation, ushort starCount, uint music) {
Func<bool, uint, uint, byte, uint, ushort, dynamic> mockStateBuilder = delegate(bool isJP, uint gameRuntime, uint globalTimer, byte stageIndex, uint animation, ushort starCount) {
dynamic state = new ExpandoObject();

uint defaultDebugFunctionValue = 0xf1f1f1f1;
Expand All @@ -1275,10 +1339,6 @@ startup {
state.starCountJP = isJP ? starCount : defaultStarCount;
state.starCountUS = isJP ? defaultStarCount : starCount;

uint defaultMusic = 0xf7f7f7f7;
state.musicJP = isJP ? music : defaultMusic;
state.musicUS = isJP ? defaultMusic : music;

ushort defaultNonStopValue = 0xf8f8;
state.nonStopInteractionOverwriteJP = /* isJP ? nonStopValue : */ defaultNonStopValue;
state.nonStopInteractionOverwriteUS = /* isJP ? */ defaultNonStopValue /* : nonStopValue */;
Expand All @@ -1295,6 +1355,22 @@ startup {
state.fileAFlagsJP = defaultFileAMagic;
state.fileAFlagsUS = defaultFileAMagic;

uint defaultObject0Timer = 0;
state.object0TimerJP = defaultObject0Timer;
state.object0TimerUS = defaultObject0Timer;

uint defaultObject0Behavior = 0;
state.object0BehaviorJP = defaultObject0Behavior;
state.object0BehaviorUS = defaultObject0Behavior;

uint defaultBehaviorSegmentInfo = 0;
state.behaviorSegmentInfoJP = defaultBehaviorSegmentInfo;
state.behaviorSegmentInfoUS = defaultBehaviorSegmentInfo;

ushort defaultController0Buttons = 0;
state.controller0ButtonsJP = defaultController0Buttons;
state.controller0ButtonsUS = defaultController0Buttons;

return state;
};

Expand All @@ -1317,8 +1393,8 @@ startup {
Action<bool> testDoesNotResetNormalConditions = delegate(bool isJP) {
dynamic varsD = mockVarsBuilder();

dynamic oldD = mockStateBuilder(isJP, 0, 0, 1, 0, 0, 0);
dynamic currentD = mockStateBuilder(isJP, 1, 0, 1, 0, 0, 0);
dynamic oldD = mockStateBuilder(isJP, 0, 0, 1, 0, 0);
dynamic currentD = mockStateBuilder(isJP, 1, 0, 1, 0, 0);

bool isUpdate = updateRunConditionInner(varsD, oldD, currentD);
assertCondition(isJP, isUpdate, "testDoesNotResetNormalConditions: update returned false");
Expand All @@ -1333,8 +1409,8 @@ startup {
Action<bool> testResetWhenGameRestarted = delegate(bool isJP) {
dynamic varsD = mockVarsBuilder();

dynamic oldD = mockStateBuilder(isJP, 1, 0, 1, 0, 0, 0);
dynamic currentD = mockStateBuilder(isJP, 0, 0, 1, 0, 0, 0);
dynamic oldD = mockStateBuilder(isJP, 1, 0, 1, 0, 0);
dynamic currentD = mockStateBuilder(isJP, 0, 0, 1, 0, 0);

bool isUpdate = updateRunConditionInner(varsD, oldD, currentD);
assertCondition(isJP, isUpdate, "testResetWhenGameRestarted: update returned false");
Expand All @@ -1354,8 +1430,8 @@ startup {
dynamic varsD = mockVarsBuilder();
varsD.data.runConfig.isRTAMode = true;

dynamic oldD = mockStateBuilder(isJP, 0, 0, 1, 0, 60, 0);
dynamic currentD = mockStateBuilder(isJP, 1, 0, 1, 0, 58, 0);
dynamic oldD = mockStateBuilder(isJP, 0, 0, 1, 0, 60);
dynamic currentD = mockStateBuilder(isJP, 1, 0, 1, 0, 58);

bool isUpdate = updateRunConditionInner(varsD, oldD, currentD);
assertCondition(isJP, isUpdate, "testResetWhenRTAModeAndStarsReduction: update returned false");
Expand All @@ -1374,9 +1450,9 @@ startup {
Action<bool> testStartRunOnFrame4 = delegate(bool isJP) {
dynamic varsD = mockVarsBuilder();

dynamic state1 = mockStateBuilder(isJP, 1, 0, 1, 0, 0, 0);
dynamic state2 = mockStateBuilder(isJP, 0, 1, 1, 0, 0, 0);
dynamic state3 = mockStateBuilder(isJP, 1, 4, 1, 0, 0, 0);
dynamic state1 = mockStateBuilder(isJP, 1, 0, 1, 0, 0);
dynamic state2 = mockStateBuilder(isJP, 0, 1, 1, 0, 0);
dynamic state3 = mockStateBuilder(isJP, 1, 4, 1, 0, 0);

bool isUpdate = updateRunConditionInner(varsD, state1, state2);
assertCondition(isJP, isUpdate, "testStartRunOnFrame4: update returned false");
Expand All @@ -1395,9 +1471,9 @@ startup {
dynamic varsD = mockVarsBuilder();
varsD.settings.forceLaunchOnStart = true;

dynamic state1 = mockStateBuilder(isJP, 1, 0, 1, 0, 0, 0);
dynamic state2 = mockStateBuilder(isJP, 0, 1, 1, 0, 0, 0);
dynamic state3 = mockStateBuilder(isJP, 1, 4, 1, 0, 0, 0);
dynamic state1 = mockStateBuilder(isJP, 1, 0, 1, 0, 0);
dynamic state2 = mockStateBuilder(isJP, 0, 1, 1, 0, 0);
dynamic state3 = mockStateBuilder(isJP, 1, 4, 1, 0, 0);

bool isUpdate = updateRunConditionInner(varsD, state1, state2);
assertCondition(isJP, isUpdate, "testStartForceOnLaunch: update returned false");
Expand Down

0 comments on commit 852f506

Please sign in to comment.