Licar/mods/tas.diff
2025-07-03 00:35:59 +02:00

509 lines
15 KiB
Diff

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