Maelstrom: Added Steam game recording timeline features

From 19a10d2495a5eb5ac00f8d8fbb65b1dba7c302c2 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 11 Mar 2026 20:21:24 -0700
Subject: [PATCH] Added Steam game recording timeline features

---
 TODO            |   1 -
 game/game.cpp   |   4 ++
 game/main.cpp   |   2 +
 game/make.cpp   |   8 +++-
 game/player.cpp |   1 +
 game/steam.cpp  | 110 ++++++++++++++++++++++++++++++++++++++++++++++++
 game/steam.h    |  20 +++++++++
 7 files changed, 143 insertions(+), 3 deletions(-)

diff --git a/TODO b/TODO
index a995f1e6..35279a22 100644
--- a/TODO
+++ b/TODO
@@ -1,5 +1,4 @@
 * Add Steam achievements
-* Add Steam game recording highlights
 * Add Steam Remote Play touch control layout
 * Improve touch control (automatically show controls on tap)
 * Add Android build to GitHub actions
diff --git a/game/game.cpp b/game/game.cpp
index 8d42445d..55ad52bd 100644
--- a/game/game.cpp
+++ b/game/game.cpp
@@ -190,6 +190,8 @@ GamePanelDelegate::OnShow()
 {
 	int i;
 
+	SetSteamTimelineMode(STEAM_TIMELINE_PLAYING);
+
 	/* Initialize some game variables */
 	gGameOn = 1;
 	gPaused = 0;
@@ -1050,6 +1052,8 @@ GamePanelDelegate::NextWave()
 			MakeLargeRock(x, y);
 	}
 
+	SetSteamTimelineLevelStarted(gWave);
+
 }	/* -- NextWave */
 
 /* ----------------------------------------------------------------- */
diff --git a/game/main.cpp b/game/main.cpp
index 3557dac6..e508bd1c 100644
--- a/game/main.cpp
+++ b/game/main.cpp
@@ -275,6 +275,8 @@ MainPanelDelegate::OnLoad()
 void
 MainPanelDelegate::OnShow()
 {
+	SetSteamTimelineMode(STEAM_TIMELINE_MENUS);
+
 	gUpdateBuffer = true;
 }
 
diff --git a/game/make.cpp b/game/make.cpp
index 1601bac4..1006ecc1 100644
--- a/game/make.cpp
+++ b/game/make.cpp
@@ -45,6 +45,8 @@ void MakeEnemy(void)
 		gSprites[newsprite] = new LittleShinobi(x, y);
 	else
 		gSprites[newsprite] = new BigShinobi(x, y);
+
+	SetSteamTimelineEvent(STEAM_TIMELINE_EVENT_ENEMY);
 }	/* -- MakeEnemy */
 
 
@@ -88,7 +90,6 @@ void MakePrize(void)
 		newsprite = gNumSprites;
 		gSprites[newsprite] = new Prize(x, y, xVel, yVel);
 	}
-	return;
 }	/* -- MakePrize */
 
 
@@ -107,7 +108,6 @@ void MakeMultiplier(void)
 
 	newsprite = gNumSprites;
 	gSprites[newsprite] = new Multiplier(x, y, FastRandom(4)+2);
-	return;
 }	/* -- MakeMultiplier */
 
 
@@ -153,6 +153,8 @@ void MakeNova(void)
 
 	newsprite = gNumSprites;
 	gSprites[newsprite] = new Nova(x, y);
+
+	SetSteamTimelineEvent(STEAM_TIMELINE_EVENT_NOVA);
 }	/* -- MakeNova */
 
 
@@ -284,6 +286,7 @@ void MakeGravity(void)
 		newsprite = gNumSprites;
 		gSprites[newsprite] = new Gravity(x, y);
 	}
+	SetSteamTimelineEvent(STEAM_TIMELINE_EVENT_GRAVITY);
 }	/* -- MakeGravity */
 
 
@@ -328,6 +331,7 @@ void MakeHoming(void)
 		newsprite = gNumSprites;
 		gSprites[newsprite] = new Homing(x, y, xVel, yVel);
 	}
+	SetSteamTimelineEvent(STEAM_TIMELINE_EVENT_MINE);
 }	/* -- MakeHoming */
 
 
diff --git a/game/player.cpp b/game/player.cpp
index 66914dbc..b9235792 100644
--- a/game/player.cpp
+++ b/game/player.cpp
@@ -352,6 +352,7 @@ Player::Explode(void)
 	Set_Blit(gShipExplosion);
 	Set_TTL(myblit->numFrames*phasetime);
 	ExplodeSound();
+	SetSteamTimelineEvent(STEAM_TIMELINE_EVENT_DEATH);
 	return(0);
 }
 
diff --git a/game/steam.cpp b/game/steam.cpp
index 0da74810..71adfddb 100644
--- a/game/steam.cpp
+++ b/game/steam.cpp
@@ -44,6 +44,10 @@ class SteamInterface
 	bool Init();
 	void Quit();
 
+	void SetSteamTimelineMode(STEAM_TIMELINE_MODE mode);
+	void SetSteamTimelineLevelStarted(int level);
+	void SetSteamTimelineEvent(STEAM_TIMELINE_EVENT event);
+
 	void Update();
 
 	Uint8 GetControlForSession(RemotePlaySessionID_t sessionID);
@@ -74,6 +78,7 @@ class SteamInterface
 
 private:
 	bool m_initialized = false;
+	STEAM_TIMELINE_MODE m_gameMode = STEAM_TIMELINE_NONE;
 	CSteamID m_steamID;
 
 	array<RemoteSession_t *> m_sessions;
@@ -99,6 +104,8 @@ bool SteamInterface::Init()
 	m_steamID = SteamUser()->GetSteamID();
 	SteamController()->Init();
 
+	SetSteamTimelineMode(STEAM_TIMELINE_LOADING);
+
 	return true;
 }
 
@@ -120,6 +127,82 @@ void SteamInterface::Quit()
 	m_initialized = false;
 }
 
+void SteamInterface::SetSteamTimelineMode(STEAM_TIMELINE_MODE mode)
+{
+	if (!m_initialized) {
+		return;
+	}
+
+	if (mode != m_gameMode) {
+		switch (mode) {
+		case STEAM_TIMELINE_LOADING:
+			SteamTimeline()->SetTimelineGameMode(k_ETimelineGameMode_LoadingScreen);
+			break;
+		case STEAM_TIMELINE_MENUS:
+			SteamTimeline()->ClearTimelineTooltip(0.0f);
+			SteamTimeline()->SetTimelineGameMode(k_ETimelineGameMode_Menus);
+			break;
+		case STEAM_TIMELINE_PLAYING:
+			SteamTimeline()->SetTimelineGameMode(k_ETimelineGameMode_Playing);
+			break;
+		default:
+			break;
+		}
+		m_gameMode = mode;
+	}
+}
+
+void SteamInterface::SetSteamTimelineLevelStarted(int level)
+{
+	if (!m_initialized) {
+		return;
+	}
+
+	char icon[32];
+	SDL_snprintf(icon, sizeof(icon), "steam_%d", level);
+
+	char wave[32];
+	SDL_snprintf(wave, sizeof(wave), "Wave %d", level);
+
+	SteamTimeline()->AddInstantaneousTimelineEvent("Next Wave", nullptr, icon, 0, 0.0f, k_ETimelineEventClipPriority_None);
+	SteamTimeline()->SetTimelineTooltip(wave, 0.0f);
+}
+
+void SteamInterface::SetSteamTimelineEvent(STEAM_TIMELINE_EVENT event)
+{
+	if (!m_initialized) {
+		return;
+	}
+
+	const char *title = nullptr;
+	const char *icon = nullptr;
+	switch (event) {
+	case STEAM_TIMELINE_EVENT_DEATH:
+		title = "Death";
+		icon = "steam_death";
+		break;
+	case STEAM_TIMELINE_EVENT_ENEMY:
+		title = "Aliens";
+		icon = "steam_caution";
+		break;
+	case STEAM_TIMELINE_EVENT_MINE:
+		title = "Homing Mine";
+		icon = "steam_caution";
+		break;
+	case STEAM_TIMELINE_EVENT_GRAVITY:
+		title = "Gravity Well";
+		icon = "steam_caution";
+		break;
+	case STEAM_TIMELINE_EVENT_NOVA:
+		title = "Supernova";
+		icon = "steam_explosion";
+		break;
+	default:
+		break;
+	}
+	SteamTimeline()->AddInstantaneousTimelineEvent(title, nullptr, icon, 0, 0.0f, k_ETimelineEventClipPriority_Standard);
+}
+
 void SteamInterface::Update()
 {
 	if (!m_initialized) {
@@ -425,6 +508,21 @@ void DisableRemoteInput()
 	steam.DisableRemoteInput();
 }
 
+void SetSteamTimelineMode(STEAM_TIMELINE_MODE mode)
+{
+	steam.SetSteamTimelineMode(mode);
+}
+
+void SetSteamTimelineLevelStarted(int level)
+{
+	steam.SetSteamTimelineLevelStarted(level);
+}
+
+void SetSteamTimelineEvent(STEAM_TIMELINE_EVENT event)
+{
+	steam.SetSteamTimelineEvent(event);
+}
+
 void UpdateSteam()
 {
 	steam.Update();
@@ -475,6 +573,18 @@ void DisableRemoteInput()
 {
 }
 
+void SetSteamTimelineMode(STEAM_TIMELINE_MODE mode)
+{
+}
+
+void SetSteamTimelineLevelStarted(int level)
+{
+}
+
+void SetSteamTimelineEvent(STEAM_TIMELINE_EVENT event)
+{
+}
+
 void UpdateSteam()
 {
 }
diff --git a/game/steam.h b/game/steam.h
index 47d8e79d..966ad9f6 100644
--- a/game/steam.h
+++ b/game/steam.h
@@ -25,6 +25,23 @@
 #define SDL_EVENT_REMOTE_PLAYERS_CHANGED	(SDL_EVENT_USER + 0)
 #define SDL_EVENT_REMOTE_INPUT				(SDL_EVENT_USER + 1)
 
+enum STEAM_TIMELINE_MODE
+{
+	STEAM_TIMELINE_NONE,
+	STEAM_TIMELINE_LOADING,
+	STEAM_TIMELINE_MENUS,
+	STEAM_TIMELINE_PLAYING
+};
+
+enum STEAM_TIMELINE_EVENT
+{
+	STEAM_TIMELINE_EVENT_DEATH,
+	STEAM_TIMELINE_EVENT_ENEMY,
+	STEAM_TIMELINE_EVENT_MINE,
+	STEAM_TIMELINE_EVENT_GRAVITY,
+	STEAM_TIMELINE_EVENT_NOVA,
+};
+
 typedef Uint32 RemotePlaySessionID_t;
 
 extern bool InitSteam();
@@ -35,6 +52,9 @@ extern SDL_Surface* GetRemotePlayerAvatar(Uint8 controlType);
 extern const bool *GetRemotePlayerKeyboardState(Uint8 controlType);
 extern void EnableRemoteInput();
 extern void DisableRemoteInput();
+extern void SetSteamTimelineMode(STEAM_TIMELINE_MODE mode);
+extern void SetSteamTimelineLevelStarted(int level);
+extern void SetSteamTimelineEvent(STEAM_TIMELINE_EVENT event);
 extern void UpdateSteam();
 extern void QuitSteam();