Maelstrom: Added avatars for Steam Remote Play Together players

From 911dd0fc1d118d00010cb567c43172c7aa85efa1 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 15 Dec 2025 22:25:40 -0800
Subject: [PATCH] Added avatars for Steam Remote Play Together players

---
 external/SteamworksSDK       |  2 +-
 game/MaelstromUI.cpp         |  8 +++++
 game/MaelstromUI.h           |  1 +
 game/gameinfo.cpp            | 24 ++++++++-----
 game/steam.cpp               | 70 ++++++++++++++++++++++++++++++++++++
 game/steam.h                 |  1 +
 screenlib/UIElement.cpp      |  8 +++++
 screenlib/UIElement.h        |  1 +
 screenlib/UIImageInterface.h |  1 +
 9 files changed, 107 insertions(+), 9 deletions(-)

diff --git a/external/SteamworksSDK b/external/SteamworksSDK
index b70e485f..56ceee16 160000
--- a/external/SteamworksSDK
+++ b/external/SteamworksSDK
@@ -1 +1 @@
-Subproject commit b70e485fc8a6f6d2f135f0a2aee5074d9e8f18e9
+Subproject commit 56ceee1634d78aabd33461d1dcd549f64ce37a49
diff --git a/game/MaelstromUI.cpp b/game/MaelstromUI.cpp
index 5a8d9c65..0840a130 100644
--- a/game/MaelstromUI.cpp
+++ b/game/MaelstromUI.cpp
@@ -315,6 +315,14 @@ MaelstromUI::CreateImage(const char *name)
 	return Load_Image(screen, name);
 }
 
+UITexture *
+MaelstromUI::CreateImage(SDL_Surface *surface)
+{
+	SDL_Texture *texture = screen->LoadImage(surface);
+	SDL_DestroySurface(surface);
+	return new UITexture(texture, 1.0f);
+}
+
 void
 MaelstromUI::FreeImage(UITexture *texture)
 {
diff --git a/game/MaelstromUI.h b/game/MaelstromUI.h
index 92598572..b451bf79 100644
--- a/game/MaelstromUI.h
+++ b/game/MaelstromUI.h
@@ -44,6 +44,7 @@ class MaelstromUI : public UIManager
 	// UIImageInterface
 	//
 	virtual UITexture *CreateImage(const char *name);
+	virtual UITexture *CreateImage(SDL_Surface *surface);
 	virtual void FreeImage(UITexture *texture);
 	virtual UITexture *CreateBackground(const char *name);
 	virtual void FreeBackground(UITexture *texture);
diff --git a/game/gameinfo.cpp b/game/gameinfo.cpp
index bde5aa6f..2546d2f8 100644
--- a/game/gameinfo.cpp
+++ b/game/gameinfo.cpp
@@ -548,9 +548,17 @@ GameInfo::UpdateUI(GameInfoPlayer *player)
 	}
 
 	if (player->UI.control) {
-		char name[128];
-		SDL_snprintf(name, sizeof(name), "control%d", player->controlMask);
-		player->UI.control->SetImage(name);
+		SDL_Surface *surface = nullptr;
+		if (IS_REMOTE_CONTROL(player->controlMask)) {
+			surface = GetRemotePlayerAvatar(player->controlMask);
+		}
+		if (surface) {
+			player->UI.control->SetImage(surface);
+		} else {
+			char name[128];
+			SDL_snprintf(name, sizeof(name), "control%d", player->controlMask);
+			player->UI.control->SetImage(name);
+		}
 	}
 
 	if (player->UI.desc) {
@@ -673,12 +681,12 @@ printf("Game 0x%8.8x: node 0x%8.8x since last ping %d (TIMEDOUT)\n",
 #endif
 			node->ping.status = PING_TIMEDOUT;
 		}
-	}
 
-	// Update the UI for matching players
-	for (int i = 0; i < MAX_PLAYERS; ++i) {
-		if (players[i].nodeID == node->nodeID) {
-			UpdateUI(&players[i]);
+		// Update the UI for matching players
+		for (int i = 0; i < MAX_PLAYERS; ++i) {
+			if (players[i].nodeID == node->nodeID) {
+				UpdateUI(&players[i]);
+			}
 		}
 	}
 }
diff --git a/game/steam.cpp b/game/steam.cpp
index f083d155..9330563f 100644
--- a/game/steam.cpp
+++ b/game/steam.cpp
@@ -52,6 +52,7 @@ class SteamInterface
 	RemotePlaySessionID_t GetRemoteSessionForGamepad(SDL_Gamepad *gamepad);
 
 	const char *GetRemotePlayerName(Uint8 controlType);
+	SDL_Surface *GetRemotePlayerAvatar(Uint8 controlType);
 	const bool *GetRemotePlayerKeyboardState(Uint8 controlType);
 
 	void EnableRemoteInput();
@@ -80,6 +81,7 @@ class SteamInterface
 	RemoteSession_t *m_players[MAX_PLAYERS - 1] = { };
 
 	STEAM_CALLBACK(SteamInterface, OnRemotePlaySessionConnected, SteamRemotePlaySessionConnected_t);
+	STEAM_CALLBACK(SteamInterface, OnRemotePlaySessionAvatarLoaded, SteamRemotePlaySessionAvatarLoaded_t);
 	STEAM_CALLBACK(SteamInterface, OnRemotePlaySessionDisconnected, SteamRemotePlaySessionDisconnected_t);
 };
 static SteamInterface steam;
@@ -246,6 +248,59 @@ const char *SteamInterface::GetRemotePlayerName(Uint8 controlType)
 	return session->name;
 }
 
+SDL_Surface *SteamInterface::GetRemotePlayerAvatar(Uint8 controlType)
+{
+	RemoteSession_t *session = GetSessionForControl(controlType);
+	if (!session) {
+		return nullptr;
+	}
+
+	SDL_Surface *surface = nullptr;
+	int image = SteamRemotePlay()->GetSmallSessionAvatar(session->id);
+	if (image > 0) {
+		uint32 width, height;
+		if (!SteamUtils()->GetImageSize(image, &width, &height)) {
+			SDL_Log("Couldn't get image size");
+			return nullptr;
+		}
+
+		SDL_Surface *avatar = SDL_CreateSurface(width, height, SDL_PIXELFORMAT_RGBA32);
+		if (!avatar) {
+			return nullptr;
+		}
+		// Steam returns tightly packed images
+		SDL_assert(avatar->pitch == width * 4);
+
+		if (!SteamUtils()->GetImageRGBA(image, (uint8 *)avatar->pixels, avatar->h * avatar->pitch)) {
+			SDL_Log("Couldn't get image pixels");
+			SDL_DestroySurface(surface);
+			return nullptr;
+		}
+
+		const int AVATAR_SIZE = 36;
+		surface = SDL_CreateSurface(AVATAR_SIZE, AVATAR_SIZE, SDL_PIXELFORMAT_RGBA32);
+		if (surface) {
+			SDL_Rect rect;
+
+			// Add a black border
+			rect.w = avatar->w + 2;
+			rect.h = avatar->h + 2;
+			rect.x = (surface->w - rect.w) / 2;
+			rect.y = (surface->h - rect.h) / 2;
+			SDL_FillSurfaceRect(surface, &rect, SDL_MapSurfaceRGB(surface, 0, 0, 0));
+
+			// Center the avatar
+			rect.w = avatar->w;
+			rect.h = avatar->h;
+			rect.x = (surface->w - rect.w) / 2;
+			rect.y = (surface->h - rect.h) / 2;
+			SDL_BlitSurface(avatar, NULL, surface, &rect);
+		}
+		SDL_DestroySurface(avatar);
+	}
+	return surface;
+}
+
 const bool *SteamInterface::GetRemotePlayerKeyboardState(Uint8 controlType)
 {
 	RemoteSession_t *session = GetSessionForControl(controlType);
@@ -273,6 +328,11 @@ void SteamInterface::DisableRemoteInput()
 	SteamRemotePlay()->DisableRemotePlayTogetherDirectInput();
 }
 
+void SteamInterface::OnRemotePlaySessionAvatarLoaded(SteamRemotePlaySessionAvatarLoaded_t *pParam)
+{
+	UpdatePlayers();
+}
+
 void SteamInterface::OnRemotePlaySessionConnected(SteamRemotePlaySessionConnected_t *pParam)
 {
 	RemotePlaySessionID_t sessionID = pParam->m_unSessionID;
@@ -346,6 +406,11 @@ const char *GetRemotePlayerName(Uint8 controlType)
 	return steam.GetRemotePlayerName(controlType);
 }
 
+SDL_Surface *GetRemotePlayerAvatar(Uint8 controlType)
+{
+	return steam.GetRemotePlayerAvatar(controlType);
+}
+
 const bool *GetRemotePlayerKeyboardState(Uint8 controlType)
 {
 	return steam.GetRemotePlayerKeyboardState(controlType);
@@ -393,6 +458,11 @@ const char *GetRemotePlayerName(Uint8 controlType)
 	return nullptr;
 }
 
+SDL_Surface *GetRemotePlayerAvatar(Uint8 controlType)
+{
+	return nullptr;
+}
+
 const bool *GetRemotePlayerKeyboardState(Uint8 controlType)
 {
 	return nullptr;
diff --git a/game/steam.h b/game/steam.h
index 6ea3a99b..881e10ad 100644
--- a/game/steam.h
+++ b/game/steam.h
@@ -32,6 +32,7 @@ extern bool InitSteam();
 extern RemotePlaySessionID_t GetRemoteSessionForGamepad(SDL_Gamepad *gamepad);
 extern Uint8 GetRemoteSessionControl(RemotePlaySessionID_t sessionID);
 extern const char *GetRemotePlayerName(Uint8 controlType);
+extern SDL_Surface* GetRemotePlayerAvatar(Uint8 controlType);
 extern const bool *GetRemotePlayerKeyboardState(Uint8 controlType);
 extern void EnableRemoteInput();
 extern void DisableRemoteInput();
diff --git a/screenlib/UIElement.cpp b/screenlib/UIElement.cpp
index c84124dd..a88463e4 100644
--- a/screenlib/UIElement.cpp
+++ b/screenlib/UIElement.cpp
@@ -537,6 +537,14 @@ UIElement::SetImage(const char *name)
 	return true;
 }
 
+void
+UIElement::SetImage(SDL_Surface *surface)
+{
+	UITexture *image = m_ui->CreateImage(surface);
+
+	SetImage(image);
+}
+
 void
 UIElement::SetImage(UITexture *image)
 {
diff --git a/screenlib/UIElement.h b/screenlib/UIElement.h
index 94cbad83..2a29203f 100644
--- a/screenlib/UIElement.h
+++ b/screenlib/UIElement.h
@@ -221,6 +221,7 @@ DECLARE_TYPESAFE_CLASS(UIBaseElement)
 
 	// Image information
 	bool SetImage(const char *name);
+	void SetImage(SDL_Surface *surface);
 	void SetImage(UITexture *image);
 	UITexture *GetImage() const {
 		return m_image;
diff --git a/screenlib/UIImageInterface.h b/screenlib/UIImageInterface.h
index 75430aae..82108451 100644
--- a/screenlib/UIImageInterface.h
+++ b/screenlib/UIImageInterface.h
@@ -28,6 +28,7 @@ class UIImageInterface
 {
 public:
 	virtual UITexture *CreateImage(const char *name) = 0;
+	virtual UITexture* CreateImage(SDL_Surface* surface) = 0;
 	virtual void FreeImage(UITexture *texture) = 0;
 
 	virtual UITexture *CreateBackground(const char *name) = 0;