Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.5 67eca9d740 Track and display last played time for games in lobby
Server-side:
- Add last_played_by_user map to RunningGame proto for persistence
- Track last played time in ControllerInfo when processing commands
- Update postCommand and postShardokCommand to record timestamps
- Persist and load last played times across server restarts
- Include lastPlayedTimestampMillis in GameInfo lobby response

Client-side:
- Add lastPlayedField to RunningGameItem for displaying time
- Format time as relative (e.g., "5m ago", "2h ago", "3d ago")
- Display in user's local timezone

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 08:16:20 -08:00
6 changed files with 56 additions and 9 deletions
@@ -714,7 +714,10 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
true);
listItem.transform.localScale = new Vector3 { x = 1.0f, y = 1.0f, z = 1.0f };
var runningGameItem = listItem.GetComponent<RunningGameItem>();
runningGameItem.SetRunningGame(runningGame.GameId, runningGame.Leader);
runningGameItem.SetRunningGame(
runningGame.GameId,
runningGame.Leader,
runningGame.LastPlayedTimestampMillis);
runningGameItem.GoCallback = this.SelectEagleGame;
runningGameItem.DropCallback = this.DropGame;
}
@@ -9,6 +9,7 @@ public class RunningGameItem : MonoBehaviour {
public GameObject item;
public TextMeshProUGUI gameIdField;
public TextMeshProUGUI leaderField;
public TextMeshProUGUI lastPlayedField;
public Button goButton;
public Button dropButton;
@@ -24,7 +25,8 @@ public class RunningGameItem : MonoBehaviour {
public void DropClicked() { DropCallback?.Invoke(gameId); }
public void SetRunningGame(long gameId, AvailableLeader leader) {
public void
SetRunningGame(long gameId, AvailableLeader leader, long lastPlayedTimestampMillis) {
this.gameId = gameId;
gameIdField.text = string.Format("{0:X}", gameId);
@@ -34,5 +36,31 @@ public class RunningGameItem : MonoBehaviour {
"{0} ({1})",
leaderName,
DisplayNames.ProfessionNames[leader.Profession]);
// Display last played time in user's local timezone
if (lastPlayedField != null) {
if (lastPlayedTimestampMillis > 0) {
var lastPlayed = DateTimeOffset.FromUnixTimeMilliseconds(lastPlayedTimestampMillis)
.LocalDateTime;
var now = DateTime.Now;
var diff = now - lastPlayed;
string timeText;
if (diff.TotalMinutes < 1) {
timeText = "Just now";
} else if (diff.TotalHours < 1) {
timeText = $"{(int)diff.TotalMinutes}m ago";
} else if (diff.TotalDays < 1) {
timeText = $"{(int)diff.TotalHours}h ago";
} else if (diff.TotalDays < 7) {
timeText = $"{(int)diff.TotalDays}d ago";
} else {
timeText = lastPlayed.ToString("MMM d");
}
lastPlayedField.text = timeText;
} else {
lastPlayedField.text = "";
}
}
}
}
@@ -265,6 +265,8 @@ message GameInfo {
int64 game_id = 1;
.google.protobuf.Int32Value faction_id = 2;
AvailableLeader leader = 3;
// Timestamp (millis since epoch) of when this user last took a turn
int64 last_played_timestamp_millis = 4;
}
message EagleCommand {
@@ -15,6 +15,8 @@ message RunningGame {
int64 game_id = 1;
map<string, int32> user_to_pid = 2;
bool ongoing = 3;
// Timestamp (millis since epoch) of when each user last took a turn
map<string, int64> last_played_by_user = 4;
}
message RunningGames {
@@ -598,7 +598,8 @@ class EagleServiceImpl(
GameInfo(
gameId = gamePlayerInfos.gameId,
factionId = gamePlayerInfos.factionId,
leader = Some(gamePlayerInfos.leader)
leader = Some(gamePlayerInfos.leader),
lastPlayedTimestampMillis = gamePlayerInfos.lastPlayedMillis.getOrElse(0L)
)
),
availableGames = gamesManager.availableGamesFor(userName),
@@ -46,7 +46,8 @@ import net.eagle0.shardok.common.hex_map.HexMap
case class GamePlayerInfo(
gameId: GameId,
factionId: Option[FactionId],
leader: AvailableLeader
leader: AvailableLeader,
lastPlayedMillis: Option[Long]
)
case class WaitingGamePlayerInfo(
@@ -84,7 +85,8 @@ case class GameAwaitingPlayers(
case class ControllerInfo(
var controller: GameController,
isAiGame: Boolean,
maxPlayers: Int
maxPlayers: Int,
var lastPlayedByUser: Map[String, Long] = Map.empty
)
object GamesManager {
@@ -462,7 +464,8 @@ class GamesManager(
gameControllerInfos += gameId -> ControllerInfo(
controller = initializedController,
isAiGame = false,
maxPlayers = 2
maxPlayers = 2,
lastPlayedByUser = runningGame.lastPlayedByUser.toMap
)
// Resume any outstanding battles
@@ -606,7 +609,8 @@ class GamesManager(
.factions(fid)
.factionHeadId
)
)
),
lastPlayedMillis = c.lastPlayedByUser.get(name)
)
)
}.toVector
@@ -697,7 +701,8 @@ class GamesManager(
RunningGame(
gameId = gameId,
userToPid = gc.controller.userNameToFactionId,
ongoing = true
ongoing = true,
lastPlayedByUser = gc.lastPlayedByUser
)
}.toVector
@@ -752,7 +757,7 @@ class GamesManager(
token: Long
): Unit =
this.synchronized {
val _ = synchronizedHandlePostResults(
val _ = synchronizedHandlePostResults(
gameControllerInfos(gameId).controller
.postHumanCommand(
userName = name,
@@ -761,6 +766,9 @@ class GamesManager(
token = token
)
)
// Record last played time for this user
val ci = gameControllerInfos(gameId)
ci.lastPlayedByUser = ci.lastPlayedByUser.updated(name, System.currentTimeMillis())
}
def postShardokCommand(
@@ -787,6 +795,9 @@ class GamesManager(
.userNameToFactionId(name)
)
}
// Record last played time for this user
val ci = gameControllerInfos(eagleGameId)
ci.lastPlayedByUser = ci.lastPlayedByUser.updated(name, System.currentTimeMillis())
}
def postPlacementCommands(