mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 02:15:43 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
823e56fd2d | ||
|
|
8a619a9e9e | ||
|
|
6002f1b483 | ||
|
|
a6c67db11c |
@@ -1,8 +1,22 @@
|
||||
using System;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Provides game state information for the connection status UI.
|
||||
/// Implement this interface to show game-specific status when connected.
|
||||
/// </summary>
|
||||
public interface IGameStateProvider {
|
||||
/// <summary>Server-reported game status. Null if no status received yet.</summary>
|
||||
ServerGameStatus ServerStatus { get; }
|
||||
|
||||
/// <summary>True if a command was submitted and we're awaiting response (for >
|
||||
/// 500ms).</summary>
|
||||
bool IsProcessingCommand { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple UI component to display connection status and reconnection countdown.
|
||||
/// Attach to a TextMeshProUGUI component to display status.
|
||||
@@ -10,6 +24,7 @@ namespace eagle {
|
||||
public class ConnectionStatusUI : MonoBehaviour {
|
||||
private TextMeshProUGUI _textComponent;
|
||||
private PersistentClientConnection _connection;
|
||||
private IGameStateProvider _gameStateProvider;
|
||||
|
||||
// Update interval in seconds
|
||||
private const float UpdateInterval = 0.5f;
|
||||
@@ -36,6 +51,14 @@ namespace eagle {
|
||||
_connection = connection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the game state provider for showing game-specific status.
|
||||
/// Call this when entering a game, clear it when leaving.
|
||||
/// </summary>
|
||||
public void SetGameStateProvider(IGameStateProvider provider) {
|
||||
_gameStateProvider = provider;
|
||||
}
|
||||
|
||||
void Update() {
|
||||
if (_connection == null || _textComponent == null) { return; }
|
||||
|
||||
@@ -72,7 +95,7 @@ namespace eagle {
|
||||
var nextAttempt = _connection.NextReconnectAttempt;
|
||||
|
||||
string statusText = state switch {
|
||||
ConnectionState.Connected => "<color=green>●</color> Connected",
|
||||
ConnectionState.Connected => GetConnectedStatusText(),
|
||||
ConnectionState.Connecting => "<color=yellow>●</color> Connecting...",
|
||||
ConnectionState.Disconnected => "<color=red>●</color> Disconnected",
|
||||
ConnectionState.Reconnecting => GetReconnectingText(nextAttempt),
|
||||
@@ -83,6 +106,34 @@ namespace eagle {
|
||||
_textComponent.text = statusText;
|
||||
}
|
||||
|
||||
private string GetConnectedStatusText() {
|
||||
// If no game state provider, just show "Connected"
|
||||
if (_gameStateProvider == null) { return "<color=green>●</color> Connected"; }
|
||||
|
||||
// Processing takes priority (client knows it submitted a command)
|
||||
if (_gameStateProvider.IsProcessingCommand) {
|
||||
return "<color=green>●</color> Processing...";
|
||||
}
|
||||
|
||||
// Use server-reported status
|
||||
var serverStatus = _gameStateProvider.ServerStatus;
|
||||
if (serverStatus == null) {
|
||||
// No server status yet - waiting for first response
|
||||
return "<color=green>●</color> Connected";
|
||||
}
|
||||
|
||||
return serverStatus.Status switch {
|
||||
ServerGameStatus.Types.Status.YourTurn => "<color=green>●</color> Your turn",
|
||||
ServerGameStatus.Types.Status.WaitingForPlayers =>
|
||||
"<color=green>●</color> Waiting for other players",
|
||||
ServerGameStatus.Types.Status.GeneratingText =>
|
||||
"<color=green>●</color> Generating...",
|
||||
ServerGameStatus.Types.Status.ProcessingAction =>
|
||||
"<color=green>●</color> Processing...",
|
||||
_ => "<color=green>●</color> Connected"
|
||||
};
|
||||
}
|
||||
|
||||
private string GetReconnectingText(DateTime? nextAttempt) {
|
||||
if (!nextAttempt.HasValue) { return "<color=yellow>●</color> Reconnecting..."; }
|
||||
|
||||
|
||||
@@ -165,6 +165,10 @@ namespace eagle {
|
||||
Model = null;
|
||||
chronicleCanvasController.Entries = new List<ChronicleEntry>();
|
||||
SetMusic();
|
||||
|
||||
// Clear game state provider when leaving game
|
||||
var statusUI = connectionStatusLabel.GetComponent<ConnectionStatusUI>();
|
||||
if (statusUI != null) { statusUI.SetGameStateProvider(null); }
|
||||
}
|
||||
|
||||
void MapControllerChangedTarget(List<ProvinceId> newTarget) {
|
||||
@@ -263,6 +267,10 @@ namespace eagle {
|
||||
|
||||
ModelUpdater.ErrorHandler = errorHandler;
|
||||
|
||||
// Set up game state provider for connection status UI
|
||||
var statusUI = connectionStatusLabel.GetComponent<ConnectionStatusUI>();
|
||||
if (statusUI != null) { statusUI.SetGameStateProvider(ModelUpdater); }
|
||||
|
||||
// Fire-and-forget - subscription is awaited internally and failures are logged
|
||||
MainQueue.Q.EnqueueForNextUpdate(
|
||||
() => { _ = ModelUpdater.StartListeningForUpdates(); });
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace eagle {
|
||||
RollFetcher RollFetcher { get; }
|
||||
}
|
||||
|
||||
public class GameModelUpdater : IClientConnectionSubscriber {
|
||||
public class GameModelUpdater : IClientConnectionSubscriber, IGameStateProvider {
|
||||
private FactionId? PlayerId => _currentModel.PlayerId;
|
||||
public Int64? CurrentEagleToken => _currentModel.CommandToken;
|
||||
public Int64? CurrentShardokToken(string shardokGameId) {
|
||||
@@ -126,6 +126,11 @@ namespace eagle {
|
||||
|
||||
private readonly Logger _connectionLogger = Logger.GetLogger("ConnectionLogger");
|
||||
|
||||
// Track when a command was submitted for "Processing..." display
|
||||
// Only show "Processing..." if command has been pending for > 500ms
|
||||
private DateTime? _commandSubmittedTime;
|
||||
private const double ProcessingDisplayDelayMs = 500.0;
|
||||
|
||||
// Track which Shardok games need full state resync after connection drop
|
||||
// Thread-safe: accessed from both connection thread and Unity main thread
|
||||
private readonly ConcurrentDictionary<string, bool> _shardokNeedsResync =
|
||||
@@ -284,6 +289,14 @@ namespace eagle {
|
||||
|
||||
switch (updateItem.GameUpdateDetailsCase) {
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ActionResultResponse:
|
||||
// Clear processing state - we received a response from the server
|
||||
_commandSubmittedTime = null;
|
||||
|
||||
// Store server-reported game status for UI
|
||||
if (updateItem.ActionResultResponse.ServerGameStatus != null) {
|
||||
ServerStatus = updateItem.ActionResultResponse.ServerGameStatus;
|
||||
}
|
||||
|
||||
_lastUnfilteredResultCount =
|
||||
updateItem.ActionResultResponse.UnfilteredResultCountAfter;
|
||||
if (updateItem.ActionResultResponse.ActionResultViews.Any() ||
|
||||
@@ -405,6 +418,7 @@ namespace eagle {
|
||||
_currentModel.AvailableCommandsByProvince.Clear();
|
||||
_currentModel.CommandToken = null;
|
||||
_currentModel.LastPostedToken = token;
|
||||
_commandSubmittedTime = DateTime.UtcNow;
|
||||
return PersistentConnection.PostEagleCommand(
|
||||
gameId: GameId,
|
||||
token: token,
|
||||
@@ -766,5 +780,21 @@ namespace eagle {
|
||||
Notify(result);
|
||||
}
|
||||
}
|
||||
|
||||
#region IGameStateProvider implementation
|
||||
|
||||
/// <summary>Server-reported game status from the last ActionResultResponse.</summary>
|
||||
public ServerGameStatus ServerStatus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if a command was submitted and we're awaiting response.
|
||||
/// Only returns true if processing for > 500ms to avoid flashing.
|
||||
/// </summary>
|
||||
public bool IsProcessingCommand =>
|
||||
_commandSubmittedTime.HasValue &&
|
||||
(DateTime.UtcNow - _commandSubmittedTime.Value).TotalMilliseconds >
|
||||
ProcessingDisplayDelayMs;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -289,6 +289,25 @@ message ActionResultResponse {
|
||||
int32 unfiltered_result_count_after = 1;
|
||||
repeated .net.eagle0.eagle.views.ActionResultView action_result_views = 2;
|
||||
AvailableCommands available_commands = 3;
|
||||
// Server-reported game status for connection status UI
|
||||
ServerGameStatus server_game_status = 4;
|
||||
}
|
||||
|
||||
// Server-reported game status for the connection status indicator.
|
||||
// Tells the client what the server is doing/waiting for.
|
||||
message ServerGameStatus {
|
||||
enum Status {
|
||||
UNKNOWN = 0;
|
||||
YOUR_TURN = 1; // Player has commands available
|
||||
WAITING_FOR_PLAYERS = 2; // Waiting for other player(s) to act
|
||||
GENERATING_TEXT = 3; // LLM text generation in progress
|
||||
PROCESSING_ACTION = 4; // Server is processing an action
|
||||
}
|
||||
Status status = 1;
|
||||
// For WAITING_FOR_PLAYERS: which faction(s) we're waiting for
|
||||
repeated int32 waiting_for_faction_ids = 2;
|
||||
// For GENERATING_TEXT: which LLM stream is being generated
|
||||
string generating_llm_id = 3;
|
||||
}
|
||||
|
||||
message ShardokActionResultResponse {
|
||||
|
||||
+17
-2
@@ -7,7 +7,13 @@ import scala.annotation.tailrec
|
||||
|
||||
import net.eagle0.eagle.{FactionId, GameId, ShardokGameId}
|
||||
import net.eagle0.eagle.api.command.AvailableCommands
|
||||
import net.eagle0.eagle.api.eagle.{ActionResultResponse, GameUpdate, ShardokActionResultResponse, UpdateStreamResponse}
|
||||
import net.eagle0.eagle.api.eagle.{
|
||||
ActionResultResponse,
|
||||
GameUpdate,
|
||||
ServerGameStatus,
|
||||
ShardokActionResultResponse,
|
||||
UpdateStreamResponse
|
||||
}
|
||||
import net.eagle0.eagle.api.eagle.GameUpdate.GameUpdateDetails
|
||||
import net.eagle0.eagle.api.eagle.ShardokActionResultResponse.SingleShardokGameResultResponse
|
||||
import net.eagle0.eagle.api.eagle.UpdateStreamRequest.StreamGameRequest.{ShardokViewStatus, StreamingTextStatus}
|
||||
@@ -358,6 +364,14 @@ case class HumanPlayerClientConnectionState(
|
||||
unfilteredCountAfter: Int,
|
||||
availableCommands: Option[AvailableCommands]
|
||||
): HumanPlayerClientConnectionState = {
|
||||
// Determine server game status based on available commands
|
||||
val serverGameStatus = availableCommands match {
|
||||
case Some(cmds) if cmds.commandsByProvince.nonEmpty =>
|
||||
Some(ServerGameStatus(status = ServerGameStatus.Status.YOUR_TURN))
|
||||
case _ =>
|
||||
Some(ServerGameStatus(status = ServerGameStatus.Status.WAITING_FOR_PLAYERS))
|
||||
}
|
||||
|
||||
send(
|
||||
UpdateStreamResponse(
|
||||
responseDetails = UpdateStreamResponse.ResponseDetails.GameUpdate(
|
||||
@@ -368,7 +382,8 @@ case class HumanPlayerClientConnectionState(
|
||||
ActionResultResponse(
|
||||
actionResultViews = filteredResults,
|
||||
unfilteredResultCountAfter = unfilteredCountAfter,
|
||||
availableCommands = availableCommands
|
||||
availableCommands = availableCommands,
|
||||
serverGameStatus = serverGameStatus
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user