Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.5 1d0d2f02f6 Register ErrorHandler in Awake to catch startup exceptions
Move Application.logMessageReceivedThreaded registration from Start()
to Awake() so exceptions during initialization are captured.

Also add fallback for when MainQueue isn't ready yet - errors are
queued and displayed in Update() once the UI is available.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 18:54:49 -08:00
adminandClaude Opus 4.5 189bc067e9 Remove unnecessary MainQueue enqueues in ShardokGameController
ModelUpdated() and SetModifiers() were wrapping their work in
MainQueue.Q.Enqueue(), but they're already called from the MainQueue
via the update processing chain:

  MainQueue → ReceiveGameUpdate → HandleUpdates → UpdateAction → ModelUpdated

This double/triple-enqueuing caused UI updates to be pushed to the end
of the queue during rapid updates (like AI turns), making moves appear
delayed or batched instead of in real-time.

By removing the unnecessary enqueues, UI updates now happen immediately
when the update is processed, restoring real-time display of moves.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 18:46:16 -08:00
2 changed files with 127 additions and 102 deletions
@@ -385,98 +385,99 @@ namespace Shardok {
Model.MyUncommittedUnits.Where(uv => uv.Location.Row == -1).ToList();
}
/// <summary>
/// Called when the ShardokGameModel is updated. This is invoked from UpdateAction,
/// which is called from ShardokGameModel.HandleUpdates, which runs on MainQueue.
/// No need to re-enqueue - we're already on the main thread.
/// </summary>
void ModelUpdated() {
MainQueue.Q.Enqueue(() => {
if (Model == null) { return; }
SetHeroLabels();
SetModifiers();
UpdateReserves();
if (Model == null) { return; }
SetHeroLabels();
SetModifiers();
UpdateReserves();
HandleEnemyStartingPositionOverlays();
HandleEnemyStartingPositionOverlays();
endTurnButton.interactable = false;
endTurnButton.interactable = false;
if (Model.GameStatus != null &&
(Model.GameStatus.State == GameStatus.Types.State.Victory)) {
turnStatusLabel.text = "Game Over!";
if (Model.GameStatus != null &&
(Model.GameStatus.State == GameStatus.Types.State.Victory)) {
turnStatusLabel.text = "Game Over!";
gameOverText.text = Model.GameStatus.Description;
gameOverCanvas.gameObject.SetActive(true);
gameOverText.text = Model.GameStatus.Description;
gameOverCanvas.gameObject.SetActive(true);
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Back to Eagle";
endTurnButton.interactable = true;
} else if (Model.GameStatus != null && Model.MyTurn) {
gameOverCanvas.gameObject.SetActive(false);
turnStatusLabel.text = "Your Turn";
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Back to Eagle";
endTurnButton.interactable = true;
} else if (Model.GameStatus != null && Model.MyTurn) {
gameOverCanvas.gameObject.SetActive(false);
turnStatusLabel.text = "Your Turn";
if (Model.InSetUp) {
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Commit";
if (Model.InSetUp) {
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Commit";
var unplacedUnitsWithLocations =
Model.MyUncommittedUnits
.Where(u => u.Location.Row >= 0 && u.Location.Column >= 0)
.ToList();
var unplacedUnitsWithLocations =
Model.MyUncommittedUnits
.Where(u => u.Location.Row >= 0 && u.Location.Column >= 0)
.ToList();
if (!Model.MyTurn) {
endTurnButton.interactable = false;
} else if (unplacedUnitsWithLocations.Count() < 1) {
endTurnButton.GetComponentInChildren<TMP_Text>().text =
"No units placed";
endTurnButton.interactable = false;
} else if (unplacedUnitsWithLocations.Count() > 10) {
endTurnButton.GetComponentInChildren<TMP_Text>().text =
"Too many units";
endTurnButton.interactable = false;
} else {
endTurnButton.interactable = true;
}
SetDisplayedCommandGroup(0);
if (!Model.MyTurn) {
endTurnButton.interactable = false;
} else if (unplacedUnitsWithLocations.Count() < 1) {
endTurnButton.GetComponentInChildren<TMP_Text>().text = "No units placed";
endTurnButton.interactable = false;
} else if (unplacedUnitsWithLocations.Count() > 10) {
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Too many units";
endTurnButton.interactable = false;
} else {
endTurnButton.GetComponentInChildren<TMP_Text>().text = "End Turn";
}
if (Model.HasAvailableCommandWhere(
command => commandTypeUIManager.CommandGroupForType(command.Type) ==
CommandTypeUIManager.EndTurnCommandGroup)) {
endTurnButton.interactable = true;
}
SelectAppropriateDefaultCommand();
SetDisplayedCommandGroup(0);
} else {
turnStatusLabel.text = $"{Model.CurrentPlayerName}'s Turn";
endTurnButton.GetComponentInChildren<TMP_Text>().text = "End Turn";
}
locationNameText.text = Model.LocationName;
if (Model.History.Count > 0) {
string monthString = new DateTime(777, Model.Month, 1)
.ToString("MMMM", CultureInfo.InvariantCulture);
roundInfoText.text = $"{monthString} {Model.CurrentRound}";
Weather weather = Model.Weather;
if (weather != null) {
roundInfoText.text += ", " + ProtoExtensions.WeatherToString(weather);
}
if (Model.HasAvailableCommandWhere(
command => commandTypeUIManager.CommandGroupForType(command.Type) ==
CommandTypeUIManager.EndTurnCommandGroup)) {
endTurnButton.interactable = true;
}
if (Model.History.Count == 0) {
turnHistoryButtonText.text = NoHistoryText;
} else if (Model.History.Count > _lastRetrievedHistoryCount) {
for (int i = _lastRetrievedHistoryCount; i < Model.History.Count; i++) {
var historyEntry = Model.History[i];
turnHistoryPanel.AddLine(GetActionResultDescription(historyEntry));
SelectAppropriateDefaultCommand();
} else {
turnStatusLabel.text = $"{Model.CurrentPlayerName}'s Turn";
}
ActionType type = historyEntry.Type;
var thisSound = soundManager.SoundForType(type);
if (thisSound != null) { audioClipSource.PlayOneShot(thisSound, 1.0f); }
}
_lastRetrievedHistoryCount = Model.History.Count;
turnHistoryButtonText.text = GetActionResultDescription(Model.History.Last());
SetModifiers();
locationNameText.text = Model.LocationName;
if (Model.History.Count > 0) {
string monthString = new DateTime(777, Model.Month, 1)
.ToString("MMMM", CultureInfo.InvariantCulture);
roundInfoText.text = $"{monthString} {Model.CurrentRound}";
Weather weather = Model.Weather;
if (weather != null) {
roundInfoText.text += ", " + ProtoExtensions.WeatherToString(weather);
}
}
SetupArmiesTable();
});
if (Model.History.Count == 0) {
turnHistoryButtonText.text = NoHistoryText;
} else if (Model.History.Count > _lastRetrievedHistoryCount) {
for (int i = _lastRetrievedHistoryCount; i < Model.History.Count; i++) {
var historyEntry = Model.History[i];
turnHistoryPanel.AddLine(GetActionResultDescription(historyEntry));
ActionType type = historyEntry.Type;
var thisSound = soundManager.SoundForType(type);
if (thisSound != null) { audioClipSource.PlayOneShot(thisSound, 1.0f); }
}
_lastRetrievedHistoryCount = Model.History.Count;
turnHistoryButtonText.text = GetActionResultDescription(Model.History.Last());
SetModifiers();
}
SetupArmiesTable();
}
private void SetupArmiesTable() {
@@ -707,34 +708,33 @@ namespace Shardok {
}
void SetModifiers() {
MainQueue.Q.Enqueue(() => {
hexGrid.ClearCellModifierImages();
// Called from ModelUpdated which runs on MainQueue - no need to re-enqueue
hexGrid.ClearCellModifierImages();
for (byte row = 0; row < Model.Map.RowCount; row++) {
for (byte column = 0; column < Model.Map.ColumnCount; column++) {
Coords coords = new Coords();
coords.Row = row;
coords.Column = column;
for (byte row = 0; row < Model.Map.RowCount; row++) {
for (byte column = 0; column < Model.Map.ColumnCount; column++) {
Coords coords = new Coords();
coords.Row = row;
coords.Column = column;
var terrain = Model.Map.TerrainAt(coords);
int cellIndex = MapCoordsToGridIndex(coords);
int numberForCell = _randomNumberForCellIndex[cellIndex];
hexGrid.SetCellTerrainImage(
cellIndex,
_imageForTerrainTracker
.GetImageForTerrain(terrain, numberForCell, Model.Month));
if (terrain.Modifier?.Fire != null) {
hexGrid.SetCellModifierEffect(cellIndex, fireEffectPrefab);
} else {
hexGrid.SetCellModifierEffect(cellIndex, null);
}
var terrain = Model.Map.TerrainAt(coords);
int cellIndex = MapCoordsToGridIndex(coords);
int numberForCell = _randomNumberForCellIndex[cellIndex];
hexGrid.SetCellTerrainImage(
cellIndex,
_imageForTerrainTracker
.GetImageForTerrain(terrain, numberForCell, Model.Month));
if (terrain.Modifier?.Fire != null) {
hexGrid.SetCellModifierEffect(cellIndex, fireEffectPrefab);
} else {
hexGrid.SetCellModifierEffect(cellIndex, null);
}
if (terrain.Modifier?.Bridge != null) {
hexGrid.SetCellModifierImage(cellIndex, bridgeImage);
}
if (terrain.Modifier?.Bridge != null) {
hexGrid.SetCellModifierImage(cellIndex, bridgeImage);
}
}
});
}
}
void HandleButton() {
@@ -54,18 +54,43 @@ namespace common.GUIUtils {
Type = type,
Time = DateTime.UtcNow
});
MainQueue.Q.Enqueue(() => {
errorTextField.text = AllMessageText;
panel.gameObject.SetActive(true);
});
// Try to display immediately if possible, otherwise queue for later
try {
if (MainQueue.Q != null) {
MainQueue.Q.Enqueue(ShowErrorPanel);
} else {
_pendingShow = true;
}
} catch {
// MainQueue not ready yet - will show in Update
_pendingShow = true;
}
}
}
private bool _pendingShow = false;
private void ShowErrorPanel() {
if (errorTextField != null && panel != null) {
errorTextField.text = AllMessageText;
panel.gameObject.SetActive(true);
}
}
// Register for log messages as early as possible
void Awake() { Application.logMessageReceivedThreaded += HandleLog; }
void Update() {
// Handle errors that occurred before MainQueue was ready
if (_pendingShow) {
_pendingShow = false;
ShowErrorPanel();
}
}
// Use this for initialization
void Start() {
panel.gameObject.SetActive(false);
Application.logMessageReceivedThreaded += HandleLog;
}
void Start() { panel.gameObject.SetActive(false); }
public void DismissClicked() {
lock (_messages) {