From 852f506d9b502c969f32ecbfbb32ef6173ecbb2f Mon Sep 17 00:00:00 2001 From: Franck Michea Date: Wed, 31 Jan 2024 23:20:01 +0100 Subject: [PATCH] Add better support for stage RTA mode. (#21) * 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 --- README.md | 35 +++----- sm64-livesplit-autosplitter.asl | 146 ++++++++++++++++++++++++-------- 2 files changed, 124 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 2da1802..d09c1cd 100644 --- a/README.md +++ b/README.md @@ -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 -------- @@ -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. @@ -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. diff --git a/sm64-livesplit-autosplitter.asl b/sm64-livesplit-autosplitter.asl index 9119c9c..8629e34 100644 --- a/sm64-livesplit-autosplitter.asl +++ b/sm64-livesplit-autosplitter.asl @@ -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 @@ -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 @@ -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 { @@ -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 @@ -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; @@ -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 create3DBox = delegate(byte stageIndex, float x1, float y1, float z1, float x2, float y2, float z2) { dynamic box = new ExpandoObject(); @@ -396,6 +410,9 @@ startup { data.wantToReset = false; data.wantToResetTiming = 0; + data.starSelectTimerUpdateCount = 0; + data.starSelectLastTimerUpdateGT = 0; + return data; }; @@ -524,10 +541,6 @@ startup { return varsD.data.runConfig.isJapaneseVersion ? state.starCountJP : state.starCountUS; }; - Func getMusicTrack = delegate(dynamic varsD, dynamic state) { - return varsD.data.runConfig.isJapaneseVersion ? state.musicJP : state.musicUS; - }; - Func getHUDCameraMode = delegate(dynamic varsD, dynamic state) { return varsD.data.runConfig.isJapaneseVersion ? state.hudCameraModeJP : state.hudCameraModeUS; }; @@ -552,6 +565,28 @@ startup { return varsD.data.runConfig.isJapaneseVersion ? state.nonStopInteractionOverwriteJP : state.nonStopInteractionOverwriteUS; }; + Func getBehaviorSegmentInfo = delegate(dynamic varsD, dynamic state) { + return varsD.data.runConfig.isJapaneseVersion ? state.behaviorSegmentInfoJP : state.behaviorSegmentInfoUS; + }; + + Func 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 getBhvActSelector = delegate(dynamic varsD) { + return varsD.data.runConfig.isJapaneseVersion ? BHV_ACT_SELECTOR_JP : BHV_ACT_SELECTOR_US; + }; + + Func getObject0Timer = delegate(dynamic varsD, dynamic state) { + return varsD.data.runConfig.isJapaneseVersion ? state.object0TimerJP : state.object0TimerUS; + }; + + Func getController0Buttons = delegate(dynamic varsD, dynamic state) { + return varsD.data.runConfig.isJapaneseVersion ? state.controller0ButtonsJP : state.controller0ButtonsUS; + }; + Func getFileFlags = delegate(dynamic varsD, dynamic state) { if (varsD.data.runLiveData.selectedFileID == 0) { return varsD.data.runConfig.isJapaneseVersion ? state.fileAFlagsJP : state.fileAFlagsUS; @@ -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; }; @@ -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); @@ -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); @@ -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 && @@ -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 ) ) ) { @@ -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; @@ -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 && @@ -1248,7 +1312,7 @@ startup { return varsD; }; - Func mockStateBuilder = delegate(bool isJP, uint gameRuntime, uint globalTimer, byte stageIndex, uint animation, ushort starCount, uint music) { + Func mockStateBuilder = delegate(bool isJP, uint gameRuntime, uint globalTimer, byte stageIndex, uint animation, ushort starCount) { dynamic state = new ExpandoObject(); uint defaultDebugFunctionValue = 0xf1f1f1f1; @@ -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 */; @@ -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; }; @@ -1317,8 +1393,8 @@ startup { Action 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"); @@ -1333,8 +1409,8 @@ startup { Action 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"); @@ -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"); @@ -1374,9 +1450,9 @@ startup { Action 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"); @@ -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");