Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.6 6375c6772c Sort running games by creation date in lobby
Add created_timestamp_millis field through the full stack (internal
storage proto, API proto, Scala server, C# client) so lobby games
display in consistent chronological order. Games with no timestamp
(existing games) sort first, with game ID as tiebreaker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 16:52:29 -08:00
5 changed files with 36 additions and 15 deletions
@@ -705,7 +705,9 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
// Set up running games table
foreach (Transform row in runningGamesListArea.transform) { Destroy(row.gameObject); }
foreach (var runningGame in lobbyResponse.RunningGames) {
foreach (var runningGame in lobbyResponse.RunningGames
.OrderBy(g => g.CreatedTimestampMillis)
.ThenBy(g => g.GameId)) {
var listItem = Instantiate(
runningGamesListItemPrefab,
runningGamesListArea.transform,
@@ -247,6 +247,8 @@ message GameInfo {
AvailableLeader leader = 3;
// Timestamp (millis since epoch) of when this user last took a turn
int64 last_played_timestamp_millis = 4;
// Timestamp (millis since epoch) of when this game was created
int64 created_timestamp_millis = 5;
}
message EagleCommand {
@@ -39,6 +39,9 @@ message RunningGame {
// Cached faction leader metadata (keyed by faction ID) to avoid loading full game state
// for lobby display. Populated by save() from in-memory state.
map<int32, CachedLeaderInfo> faction_leader_cache = 7;
// Timestamp (millis since epoch) of when this game was created
int64 created_timestamp_millis = 8;
}
message RunningGames {
@@ -589,7 +589,8 @@ class EagleServiceImpl(
gameId = gamePlayerInfos.gameId,
factionId = gamePlayerInfos.factionId,
leader = Some(gamePlayerInfos.leader),
lastPlayedTimestampMillis = gamePlayerInfos.lastPlayedMillis.getOrElse(0L)
lastPlayedTimestampMillis = gamePlayerInfos.lastPlayedMillis.getOrElse(0L),
createdTimestampMillis = gamePlayerInfos.createdTimestampMillis
)
),
availableGames = gamesManager.availableGamesFor(userId),
@@ -64,7 +64,8 @@ case class GamePlayerInfo(
gameId: GameId,
factionId: Option[FactionId],
leader: AvailableLeader,
lastPlayedMillis: Option[Long]
lastPlayedMillis: Option[Long],
createdTimestampMillis: Long
)
case class WaitingGamePlayerInfo(
@@ -103,7 +104,8 @@ case class ControllerInfo(
var controller: GameController,
isAiGame: Boolean,
maxPlayers: Int,
var lastPlayedByUserId: Map[UserId, Long] = Map.empty
var lastPlayedByUserId: Map[UserId, Long] = Map.empty,
createdTimestampMillis: Long = 0L
)
object GamesManager {
@@ -603,7 +605,8 @@ class GamesManager(
controller = controller,
isAiGame = false,
maxPlayers = 2,
lastPlayedByUserId = lastPlayedByUserId
lastPlayedByUserId = lastPlayedByUserId,
createdTimestampMillis = runningGame.createdTimestampMillis
)
}
@@ -631,7 +634,8 @@ class GamesManager(
controller = initializedController,
isAiGame = false,
maxPlayers = 2,
lastPlayedByUserId = lastPlayedByUserId
lastPlayedByUserId = lastPlayedByUserId,
createdTimestampMillis = runningGame.createdTimestampMillis
)
}
@@ -804,7 +808,8 @@ class GamesManager(
imagePath = cached.imagePath,
name = cached.resolvedName
),
lastPlayedMillis = lastPlayedMillis
lastPlayedMillis = lastPlayedMillis,
createdTimestampMillis = rg.createdTimestampMillis
)
)
@@ -831,7 +836,8 @@ class GamesManager(
),
resolveName
),
lastPlayedMillis = c.lastPlayedByUserId.get(userId)
lastPlayedMillis = c.lastPlayedByUserId.get(userId),
createdTimestampMillis = rg.createdTimestampMillis
)
}
}
@@ -872,7 +878,8 @@ class GamesManager(
),
resolveName
),
lastPlayedMillis = c.lastPlayedByUserId.get(userId).orElse(rg.lastPlayedByUserId.get(userId.value))
lastPlayedMillis = c.lastPlayedByUserId.get(userId).orElse(rg.lastPlayedByUserId.get(userId.value)),
createdTimestampMillis = rg.createdTimestampMillis
)
case None =>
// Game not loaded - return placeholder info
@@ -885,7 +892,8 @@ class GamesManager(
imagePath = "",
name = "[Loading...]"
),
lastPlayedMillis = rg.lastPlayedByUserId.get(userId.value)
lastPlayedMillis = rg.lastPlayedByUserId.get(userId.value),
createdTimestampMillis = rg.createdTimestampMillis
)
}
}
@@ -1035,7 +1043,8 @@ class GamesManager(
// Use new userId-based format only
userIdToPid = userIdToFid,
lastPlayedByUserId = lastPlayedByUserId,
factionLeaderCache = leaderCache
factionLeaderCache = leaderCache,
createdTimestampMillis = gc.createdTimestampMillis
)
}.toVector
@@ -1051,12 +1060,14 @@ class GamesManager(
private def addController(
controller: GameController,
isAiGame: Boolean,
maxPlayers: Int
maxPlayers: Int,
createdTimestampMillis: Long = 0L
): GameController = this.synchronized {
gameControllerInfos += controller.gameId -> ControllerInfo(
controller = controller,
isAiGame = isAiGame,
maxPlayers = maxPlayers
maxPlayers = maxPlayers,
createdTimestampMillis = createdTimestampMillis
)
controller
}
@@ -1405,7 +1416,8 @@ class GamesManager(
addController(
controller = withAiCommands.gameController,
isAiGame = false,
maxPlayers = 2
maxPlayers = 2,
createdTimestampMillis = System.currentTimeMillis()
): Unit
synchronizedHandlePostResults(withAiCommands): Unit
@@ -1534,7 +1546,8 @@ class GamesManager(
val updated = addController(
controller = withAiCommands.gameController,
isAiGame = humanPlayerCount == 0,
maxPlayers = humanPlayerCount + aiPlayerCount
maxPlayers = humanPlayerCount + aiPlayerCount,
createdTimestampMillis = System.currentTimeMillis()
)
val result = logger.withStopwatch(s"[CREATE:$gameIdHex] synchronizedHandlePostResults") {