Licar mod for making TAS (tool assisted speedruns). This is a simple mod that mainly adds one feature: instead of completely restarting a map, the restart key rather rewinds time a little back. This allows to retry any part of a map as many times as one desires. Loading a replay for viewing also loads the inputs from the replay so that it's possible to reuse a start portion of another run. Additionally there is more information shown in HUD and the possibility to set slow motion levels in main menu. diff --git a/assets.h b/assets.h index 06b853c..a8edf18 100644 --- a/assets.h +++ b/assets.h @@ -36,23 +36,24 @@ static const char *LCR_texts[] = { #define LCR_TEXTS_VERSION 0 - LCR_VERSION, // version string + LCR_VERSION " TAS mod", // version string #define LCR_TEXTS_TABS 1 "main menu", "play map", - "view repl", + "load repl", "race repl", #define LCR_TEXTS_MAIN_MENU 5 "camera", "music", "sound", "save repl", + "slowmo", "exit", -#define LCR_TEXTS_LOADING 10 +#define LCR_TEXTS_LOADING 11 "loading", -#define LCR_TEXTS_SAVED 11 +#define LCR_TEXTS_SAVED 12 "saved", -#define LCR_TEXTS_FAIL 12 +#define LCR_TEXTS_FAIL 13 "failed" }; diff --git a/game.h b/game.h index 1ad5131..f407ef4 100644 --- a/game.h +++ b/game.h @@ -344,9 +344,16 @@ struct uint32_t runTime; ///< Current time of the run, in ticks. uint8_t mapBeaten; + uint8_t tasInputs[LCR_SETTING_TAS_MAX_INPUTS / 2 + 1]; + unsigned int tasInputCount; + char popupStr[LCR_POPUP_STR_SIZE]; uint8_t popupCountdown; + uint16_t racingTickRT; + + int carSpeeds[2]; ///< Current and previous tick speed. + struct { uint8_t selectedTab; @@ -441,8 +448,10 @@ void LCR_gamePopupMessage(const char *str) void LCR_gamePopupNumber(uint8_t num) { - LCR_game.popupStr[0] = '0' + num; - LCR_game.popupStr[1] = 0; + LCR_game.popupStr[3] = 0; + LCR_game.popupStr[2] = '0' + num % 10; + LCR_game.popupStr[1] = num > 9 ? '0' + (num / 10) % 10 : ' '; + LCR_game.popupStr[0] = num > 99 ? '0' + num / 100 : ' '; LCR_gamePopupMessage(LCR_game.popupStr); } @@ -489,6 +498,9 @@ void LCR_gameResetRun(uint8_t replay, uint8_t ghost) LCR_LOG0("resetting run"); LCR_mapReset(); + LCR_game.carSpeeds[0] = 0; + LCR_game.carSpeeds[1] = 0; + LCR_racingRestart(replay); LCR_rendererUnmarkCPs(); LCR_racingGetCarTransform(carTransform,carTransform + 3,0); @@ -504,8 +516,36 @@ void LCR_gameResetRun(uint8_t replay, uint8_t ghost) LCR_game.ghost.active = ghost; #endif - LCR_gameSetState(LCR_GAME_STATE_RUN_STARTING); LCR_game.runTime = 0; + + if (!LCR_racing.replay.on) + { + LCR_game.tasInputCount = LCR_game.tasInputCount >= LCR_SETTING_TAS_REWIND ? + LCR_game.tasInputCount - LCR_SETTING_TAS_REWIND : 0; + + for (unsigned int i = 0; i < LCR_game.tasInputCount; ++i) + { + if (LCR_racingStep((LCR_game.tasInputs[i / 2] >> ((i % 2) * 4)) & 0x0f) + == LCR_RACING_EVENT_CP_TAKEN) + { + int carBlock[3]; + LCR_racingGetCarBlockCoords(carBlock); + LCR_rendererMarkTakenCP(carBlock[0],carBlock[1],carBlock[2]); + } + + LCR_game.runTime = LCR_racing.tick; + } + + LCR_gameSetState(LCR_GAME_STATE_RUN); + LCR_racingGetCarTransform(carTransform,carTransform + 3,0); + LCR_rendererSetCarTransform(carTransform,carTransform + 3); + LCR_rendererCameraReset(); + LCR_rendererLoadMapChunks(); + LCR_game.nextRenderFrameTime = LCR_game.time + 1000 / LCR_SETTING_FPS; + LCR_game.nextRacingTickTime = LCR_game.time + LCR_game.racingTickRT; + } + else + LCR_gameSetState(LCR_GAME_STATE_RUN_STARTING); } void LCR_gameGetNthGhostSample(unsigned int n, @@ -872,6 +912,7 @@ uint8_t LCR_gameLoadMap(unsigned int mapIndex) result = LCR_mapLoadFromStr(LCR_gameGetNextDataStrChar,name); LCR_game.mapBeaten = LCR_mapIsBeaten(LCR_currentMap.name); + LCR_game.tasInputCount = 0; return result; } @@ -967,14 +1008,14 @@ void LCR_gameEraseMenuItemNames(void) void LCR_gameLoadMainMenuItems(void) { - for (int j = 0; j < 5; ++j) + for (int j = 0; j < 6; ++j) for (int i = 0; i < LCR_MENU_STRING_SIZE - 1; ++i) { LCR_game.menu.itemNames[j][i] = LCR_texts[LCR_TEXTS_MAIN_MENU + j][i]; LCR_game.menu.itemNames[j][i + 1] = 0; } - LCR_game.menu.itemCount = 5; + LCR_game.menu.itemCount = 6; } #define LCR_GAME_DATA_FILE_BUFFER_SIZE 32 @@ -1094,6 +1135,8 @@ void LCR_gameInit(int argc, const char **argv) LCR_game.dataFile.state = 0; + LCR_game.racingTickRT = LCR_RACING_TICK_MS_RT; + for (int i = 0; i < LCR_MENU_MAX_ITEMS; ++i) LCR_game.menu.itemNamePtrs[i] = LCR_game.menu.itemNames[i]; @@ -1231,22 +1274,30 @@ void LCR_gameEnd(void) void LCR_gameTimeToStr(uint32_t timeMS, char *str) { - str[9] = 0; - str[8] = '0' + timeMS % 10; // milliseconds + uint32_t ticks = timeMS / LCR_RACING_TICK_MS; + + str[12] = 0; + str[11] = '0' + timeMS % 10; // milliseconds timeMS /= 10; - str[7] = '0' + timeMS % 10; + str[10] = '0' + timeMS % 10; timeMS /= 10; - str[6] = '0' + timeMS % 10; + str[9] = '0' + timeMS % 10; timeMS /= 10; - str[5] = '\''; - str[4] = '0' + timeMS % 10; // seconds + str[8] = '0' + timeMS % 10; // seconds timeMS /= 10; - str[3] = '0' + timeMS % 6; - str[2] = '\''; - timeMS /= 6; - str[1] = '0' + timeMS % 10; // minutes + str[7] = '0' + timeMS % 10; timeMS /= 10; - str[0] = '0' + timeMS % 10; + str[6] = '0' + timeMS % 10; + str[5] = '\''; + str[4] = '0' + ticks % 10; + ticks /= 10; + str[3] = '0' + ticks % 10; + ticks /= 10; + str[2] = '0' + ticks % 10; + ticks /= 10; + str[1] = '0' + ticks % 10; + ticks /= 10; + str[0] = '0' + ticks % 10; } void LCR_gameDrawPopupMessage(void) @@ -1280,7 +1331,7 @@ void LCR_gameDraw3DView(void) #endif ? LCR_GAME_UNIT - (((int) (LCR_game.nextRacingTickTime - LCR_game.time)) * LCR_GAME_UNIT) - / LCR_RACING_TICK_MS_RT // 32: magic constant + / LCR_game.racingTickRT // 32: magic constant : _LCR_min(LCR_GAME_UNIT,32 * ((int) (LCR_game.time - LCR_game.stateStartTime))); @@ -1331,25 +1382,67 @@ void LCR_gameDraw3DView(void) #if LCR_SETTING_DISPLAY_HUD // GUI/HUD: + + char str[16]; - char str[10]; - int val = LCR_carSpeedKMH(); + for (int i = 0; i < 3; ++i) + { + int val = i == 2 ? (100 * LCR_racing.driftFriction) / + LCR_CAR_DRIFT_THRESHOLD_1 : LCR_game.carSpeeds[0]; - if (val < 5) // don't show tiny oscillations when still - val = 0; + if (i == 1) + val -= LCR_game.carSpeeds[1]; + else if (i == 0 && val < 5) // don't show tiny speed oscillations when still + val = 0; - str[0] = val >= 100 ? '0' + (val / 100) % 10 : ' '; - str[1] = val >= 10 ? '0' + (val / 10) % 10 : ' '; - str[2] = '0' + val % 10; - str[3] = 0; + str[0] = ' '; - #define _FONT_SIZE (1 + (LCR_EFFECTIVE_RESOLUTION_Y > 96)) + if (val < 0) + { + str[0] = '-'; + val *= -1; + } - LCR_rendererDrawText(str,LCR_EFFECTIVE_RESOLUTION_X - // speed (bot., right) - LCR_rendererComputeTextWidth(str,_FONT_SIZE) - LCR_GUI_GAP, - LCR_EFFECTIVE_RESOLUTION_Y - LCR_rendererComputeTextHeight(_FONT_SIZE) - - LCR_GUI_GAP,0,_FONT_SIZE); + str[1] = val >= 100 ? '0' + (val / 100) % 10 : ' '; + str[2] = val >= 10 ? '0' + (val / 10) % 10 : ' '; + str[3] = '0' + val % 10; + str[4] = 0; + + #define _FONT_SIZE (1 + (LCR_EFFECTIVE_RESOLUTION_Y > 96)) + + LCR_rendererDrawText(str,LCR_EFFECTIVE_RESOLUTION_X - + LCR_rendererComputeTextWidth(str,_FONT_SIZE) - LCR_GUI_GAP, + LCR_EFFECTIVE_RESOLUTION_Y - (i + 1) * + (LCR_rendererComputeTextHeight(_FONT_SIZE) + LCR_GUI_GAP), + (i == 2 && LCR_racing.carDrifting) ? 0xf800 : 0,_FONT_SIZE); + if (i == 0) + { + str[0] = (LCR_racing.currentInputs & LCR_RACING_INPUT_LEFT) ? 'L' : '.'; + str[1] = (LCR_racing.currentInputs & LCR_RACING_INPUT_BACK) ? 'D' : '.'; + str[2] = (LCR_racing.currentInputs & LCR_RACING_INPUT_FORW) ? 'U' : '.'; + str[3] = (LCR_racing.currentInputs & LCR_RACING_INPUT_RIGHT) ? 'R' : '.'; + str[4] = 0; + } + else if (i == 1) + { + str[0] = '0' + LCR_racingCurrentGroundMaterial(); + str[1] = (LCR_racing.wheelCollisions & 0x02) ? 'f' : '.'; + str[2] = (LCR_racing.wheelCollisions & 0x01) ? 'f' : '.'; + str[3] = (LCR_racing.wheelCollisions & 0x08) ? 'r' : '.'; + str[4] = (LCR_racing.wheelCollisions & 0x04) ? 'r' : '.'; + str[5] = 0; + } + + if (i < 2) + LCR_rendererDrawText(str,LCR_EFFECTIVE_RESOLUTION_X - + LCR_rendererComputeTextWidth(str,_FONT_SIZE) - LCR_GUI_GAP, + LCR_EFFECTIVE_RESOLUTION_Y - (4 + i) * + (LCR_rendererComputeTextHeight(_FONT_SIZE) + LCR_GUI_GAP),0,_FONT_SIZE); + } + + +/* str[0] = (LCR_racing.currentInputs & LCR_RACING_INPUT_LEFT) ? 'L' : '.'; str[1] = (LCR_racing.currentInputs & LCR_RACING_INPUT_BACK) ? 'D' : '.'; str[2] = (LCR_racing.currentInputs & LCR_RACING_INPUT_FORW) ? 'U' : '.'; @@ -1358,8 +1451,9 @@ void LCR_gameDraw3DView(void) LCR_rendererDrawText(str,LCR_EFFECTIVE_RESOLUTION_X - LCR_rendererComputeTextWidth(str,_FONT_SIZE) - LCR_GUI_GAP, - LCR_EFFECTIVE_RESOLUTION_Y - 2 * + LCR_EFFECTIVE_RESOLUTION_Y - 3 * (LCR_rendererComputeTextHeight(_FONT_SIZE) + LCR_GUI_GAP),0,_FONT_SIZE); +*/ LCR_gameTimeToStr(LCR_game.runTime * LCR_RACING_TICK_MS,str); @@ -1384,7 +1478,7 @@ void LCR_gameDraw3DView(void) void LCR_gameSaveReplay(void) { - char str[10]; + char str[16]; LCR_LOG0("saving replay"); @@ -1581,7 +1675,7 @@ void LCR_gameHandleInput(void) if (LCR_game.menu.selectedTab == 0) { - if (LCR_game.menu.selectedItem < 4) + if (LCR_game.menu.selectedItem < 5) { LCR_game.menu.selectedItem++; LCR_audioPlaySound(LCR_SOUND_CLICK); @@ -1647,6 +1741,17 @@ void LCR_gameHandleInput(void) break; case 4: + LCR_game.racingTickRT += LCR_RACING_TICK_MS / 2; + + if (LCR_game.racingTickRT > LCR_RACING_TICK_MS * 10) + LCR_game.racingTickRT = LCR_RACING_TICK_MS; + + LCR_gamePopupNumber( + (100 * LCR_RACING_TICK_MS ) / LCR_game.racingTickRT); + + break; + + case 5: LCR_gameSetState(LCR_GAME_STATE_END); break; @@ -1713,6 +1818,23 @@ void LCR_gameHandleInput(void) } } +void LCR_gameAddTASInput(uint8_t input) +{ + if (LCR_game.tasInputCount >= LCR_SETTING_TAS_MAX_INPUTS) + { + LCR_LOG1("maximum TAS inputs reached!"); + } + else + { + uint8_t *inputRec = LCR_game.tasInputs + LCR_game.tasInputCount / 2; + + *inputRec = (LCR_game.tasInputCount % 2) ? + *inputRec | ((input & 0x0f) << 4) : (input & 0x0f); + + LCR_game.tasInputCount++; + } +} + uint8_t LCR_gameStep(uint32_t time) { LCR_LOG2("game step (start)"); @@ -1747,9 +1869,21 @@ uint8_t LCR_gameStep(uint32_t time) LCR_gameSetState(LCR_GAME_STATE_LOADED); } else if (LCR_game.state == LCR_GAME_STATE_LOADED) - LCR_gameResetRun( // start countdown now, loading the map lost many frames - LCR_game.menu.selectedTab == 2, - LCR_game.menu.selectedTab == 3); + { + if (LCR_game.menu.selectedTab == 2) // view replay? => load TAS inputs + { + LCR_replayInitPlaying(); + LCR_game.tasInputCount = 0; + + while (!LCR_replayHasFinished()) + LCR_gameAddTASInput(LCR_replayGetNextInput()); + + LCR_game.menu.selectedTab = 3; + _LCR_gamePrepareGhost(); + } + + LCR_gameResetRun(0,LCR_game.menu.selectedTab == 3); + } else { LCR_gameHandleInput(); @@ -1770,12 +1904,19 @@ uint8_t LCR_gameStep(uint32_t time) (LCR_game.keyStates[LCR_KEY_DOWN] ? LCR_RACING_INPUT_BACK : 0) | (LCR_game.keyStates[LCR_KEY_LEFT] ? LCR_RACING_INPUT_LEFT : 0)); + if (!paused && !LCR_racing.replay.on) + LCR_gameAddTASInput(input); + #ifdef LCR_FPS_GET_MS frameTime = LCR_FPS_GET_MS; #endif + LCR_game.carSpeeds[1] = LCR_game.carSpeeds[0]; + uint32_t events = paused ? 0 : LCR_racingStep(input); + LCR_game.carSpeeds[0] = LCR_carSpeedKMH(); + #if LCR_SETTING_PARTICLES LCR_rendererSetParticles(0); @@ -1801,7 +1942,7 @@ uint8_t LCR_gameStep(uint32_t time) if (events & LCR_RACING_EVENT_CP_TAKEN) { int carBlock[3]; - char str[10]; + char str[16]; LCR_LOG1("CP taken"); LCR_racingGetCarBlockCoords(carBlock); @@ -1890,7 +2031,7 @@ uint8_t LCR_gameStep(uint32_t time) LCR_game.state == LCR_GAME_STATE_RUN_STARTING) LCR_game.runTime = LCR_racing.tick; - LCR_game.nextRacingTickTime += LCR_RACING_TICK_MS_RT; + LCR_game.nextRacingTickTime += LCR_game.racingTickRT; } // handle rendering: diff --git a/racing.h b/racing.h index 0824354..cee1c23 100644 --- a/racing.h +++ b/racing.h @@ -174,6 +174,8 @@ struct uint16_t crashState; + TPE_Unit driftFriction; + #if LCR_SETTING_REPLAY_MAX_SIZE != 0 struct { @@ -1022,6 +1024,7 @@ void LCR_racingRestart(uint8_t replay) LCR_racing.tick = 0; LCR_racing.fanForce = 0; + LCR_racing.driftFriction = 0; #if LCR_SETTING_REPLAY_MAX_SIZE > 0 LCR_racing.replay.on = replay; @@ -1258,7 +1261,7 @@ uint32_t LCR_racingStep(unsigned int input) TPE_Vec3 carForw, carRight, carUp, carVel; uint8_t onAccel = 0; // standing on accelerator? int groundBlockIndex = -1; - TPE_Unit driftFriction = 0; // average wheel friction (absolute value) + LCR_racing.driftFriction = 0; // average wheel friction (absolute value) LCR_racing.groundMaterial = LCR_BLOCK_MATERIAL_CONCRETE; @@ -1484,7 +1487,7 @@ uint32_t LCR_racingStep(unsigned int input) (LCR_CAR_STEER_FRICTION * LCR_CAR_DRIFT_FACTOR) / 8 : LCR_CAR_STEER_FRICTION,LCR_racing.groundMaterial)) / TPE_F); - driftFriction += TPE_vec3Len(fric); + LCR_racing.driftFriction += TPE_vec3Len(fric); jv = TPE_vec3Minus(jv,fric); // subtract the friction @@ -1493,7 +1496,7 @@ uint32_t LCR_racingStep(unsigned int input) LCR_racing.carBody.joints[i].velocity[2] = jv.z; } - driftFriction /= 4; // divide by 4 wheels + LCR_racing.driftFriction /= 4; // divide by 4 wheels if (steering && (input & (LCR_RACING_INPUT_FORW | LCR_RACING_INPUT_BACK)) && @@ -1510,7 +1513,7 @@ uint32_t LCR_racingStep(unsigned int input) } if ((!LCR_racing.carDrifting) && - driftFriction > + LCR_racing.driftFriction > (LCR_CAR_DRIFT_THRESHOLD_1 >> (( // back key initiates drift easily ((input & (LCR_RACING_INPUT_FORW | LCR_RACING_INPUT_BACK)) == (LCR_RACING_INPUT_FORW | LCR_RACING_INPUT_BACK))) << 2))) @@ -1519,7 +1522,7 @@ uint32_t LCR_racingStep(unsigned int input) LCR_racing.carDrifting = 1; } else if (LCR_racing.carDrifting && - (driftFriction < LCR_CAR_DRIFT_THRESHOLD_0 || + (LCR_racing.driftFriction < LCR_CAR_DRIFT_THRESHOLD_0 || LCR_racingGetCarSpeedUnsigned() < 5)) { LCR_LOG1("drift end"); diff --git a/settings.h b/settings.h index 332df45..b9b3aba 100644 --- a/settings.h +++ b/settings.h @@ -296,4 +296,14 @@ #define LCR_SETTING_ONLY_SMALL_MAPS 0 #endif +#ifndef LCR_SETTING_TAS_MAX_INPUTS + /** Maximum possible length of a run in the TAS mod, in physics ticks. */ + #define LCR_SETTING_TAS_MAX_INPUTS 4096 +#endif + +#ifndef LCR_SETTING_TAS_REWIND + /** Length of TAS rewind, in physics ticks. */ + #define LCR_SETTING_TAS_REWIND 20 +#endif + #endif // guard