Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.5 888dbbc62f Optimize OrganizeTroopsCommandSelector for better responsiveness
Performance improvements:
- Remove redundant Update() calls in PlusClickedImpl methods - the caller
  (UpdateTable or MaxClickedImpl) calls Update() when needed
- Remove unused Update() call in MinusClicked (result was never used)
- Cache extraTroops counts by type to avoid repeated LINQ queries on each
  battalion row
- Fix somethingChanged check to inspect fields directly instead of calling
  expensive Update() method inside Exists()
- Remove duplicate maxAllButton.SetActive(false) call

These changes reduce the number of object allocations and iterations
performed on each button click, improving UI responsiveness.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 09:27:30 -08:00
adminandClaude Opus 4.5 68cdf625f4 Optimize MainQueue: skip Stopwatch when queue is empty
Added a fast path to avoid Stopwatch creation when the action queue
is empty, reducing per-frame overhead during normal gameplay. Also
removed unused actionsProcessed variable.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 09:20:11 -08:00
adminandClaude Opus 4.5 80ea98dbce Fix Shardok resync flag cleared before updates received
The resync flag was being cleared immediately after subscription
acknowledgment, but BEFORE the Shardok updates actually arrived.
If the connection dropped between acknowledgment and update delivery,
the flag would already be cleared, so the next reconnect wouldn't
request a resync, leaving the client with stale Shardok state.

The fix removes the premature flag clearing - flags are now only
cleared in EagleGameModel.HandleOneGameUpdate after updates are
actually received.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 08:31:42 -08:00
3 changed files with 47 additions and 32 deletions
@@ -224,13 +224,13 @@ namespace eagle {
tp => tp.TypeId == battalionTypeId && tp.MeetsRequirements);
}
private void MaybeActivateRow(EventBasedTable table, BattalionTypeId battalionTypeId) {
private void MaybeActivateRow(
EventBasedTable table,
BattalionTypeId battalionTypeId,
Dictionary<BattalionTypeId, int> extraTroopsByType) {
var parent = table.gameObject.transform.parent;
var allowed = TypeIsAllowed(battalionTypeId) ||
extraTroops.Where(tfb => tfb.type == battalionTypeId)
.Select(tfb => tfb.count)
.Sum() > 0;
var allowed = TypeIsAllowed(battalionTypeId) || extraTroopsByType[battalionTypeId] > 0;
table.gameObject.GetComponent<OrganizeExtrasTable>().Set(
allowed,
@@ -251,12 +251,12 @@ namespace eagle {
parent.GetComponentInChildren<RawImage>().color = color;
}
private void MaybeActivateRows() {
MaybeActivateRow(lightInfantryTable, BattalionTypeId.LightInfantry);
MaybeActivateRow(heavyInfantryTable, BattalionTypeId.HeavyInfantry);
MaybeActivateRow(longbowmenTable, BattalionTypeId.Longbowmen);
MaybeActivateRow(dragoonsTable, BattalionTypeId.LightCavalry);
MaybeActivateRow(knightsTable, BattalionTypeId.HeavyCavalry);
private void MaybeActivateRows(Dictionary<BattalionTypeId, int> extraTroopsByType) {
MaybeActivateRow(lightInfantryTable, BattalionTypeId.LightInfantry, extraTroopsByType);
MaybeActivateRow(heavyInfantryTable, BattalionTypeId.HeavyInfantry, extraTroopsByType);
MaybeActivateRow(longbowmenTable, BattalionTypeId.Longbowmen, extraTroopsByType);
MaybeActivateRow(dragoonsTable, BattalionTypeId.LightCavalry, extraTroopsByType);
MaybeActivateRow(knightsTable, BattalionTypeId.HeavyCavalry, extraTroopsByType);
}
protected override void SetUpUI() {
@@ -330,7 +330,8 @@ namespace eagle {
} else
return false;
eb.Update(existingBattalions);
// Note: We don't call Update() here - the caller (UpdateTable or MaxClickedImpl)
// will call it when needed. This avoids redundant recalculations.
return true;
}
@@ -367,7 +368,6 @@ namespace eagle {
}
// Remove original troops
else {
var updated = eb.Update(existingBattalions);
var availableToRemove = eb.Original.Size - eb.troopsRemoved;
var newlyRemovedCount = Math.Min(KeyModifiedAmount.Amount(), availableToRemove);
@@ -464,7 +464,8 @@ namespace eagle {
} else
return false;
newB.Update(existingBattalions);
// Note: We don't call Update() here - the caller (UpdateTable or MaxClickedImpl)
// will call it when needed. This avoids redundant recalculations.
return true;
}
@@ -653,7 +654,16 @@ namespace eagle {
{ BattalionTypeId.Longbowmen, 0 }
};
maxAllButton.gameObject.SetActive(false);
// Cache extra troop counts by type to avoid repeated LINQ queries
var extraTroopsByType = new Dictionary<BattalionTypeId, int> {
{ BattalionTypeId.LightInfantry, 0 },
{ BattalionTypeId.HeavyInfantry, 0 },
{ BattalionTypeId.LightCavalry, 0 },
{ BattalionTypeId.HeavyCavalry, 0 },
{ BattalionTypeId.Longbowmen, 0 }
};
foreach (var et in extraTroops) { extraTroopsByType[et.type] += et.count; }
foreach (var eb in existingBattalions) {
if (eb.dismissed) continue;
@@ -666,9 +676,7 @@ namespace eagle {
newRow.MaxButtonClickedCallback = () => MaxClicked(eb);
newRow.DismissButtonClickedCallback = () => DismissClicked(eb);
bool canAugment =
TypeIsAllowed(eb.TypeId) ||
extraTroops.Where(tfb => tfb.type == eb.TypeId).Sum(tfb => tfb.count) > 0;
bool canAugment = TypeIsAllowed(eb.TypeId) || extraTroopsByType[eb.TypeId] > 0;
newRow.CanAugment = canAugment;
if (eb.Count < eb.Capacity) {
@@ -739,15 +747,18 @@ namespace eagle {
if (!sufficient) { _disabledReason = "Not enough gold"; }
// Also check that something has changed
// Check newBattalion fields directly instead of calling Update() which is expensive
bool somethingChanged =
(newBattalions.Exists(b => b.Update(existingBattalions).Size > 0) ||
(newBattalions.Exists(
b => b.newBattalion.NewTroops > 0 ||
b.newBattalion.TroopsFromOtherBattalion.Count > 0) ||
existingBattalions.Exists(eb => eb.changed != null || eb.troopsRemoved > 0));
if (!somethingChanged) { _disabledReason = "No battalions have changed"; }
_enableCommit = sufficient && somethingChanged;
resetAllButton.gameObject.SetActive(somethingChanged);
MaybeActivateRows();
MaybeActivateRows(extraTroopsByType);
}
public override AvailableCommand.SealedValueOneofCase CommandType =>
@@ -226,12 +226,10 @@ namespace eagle {
$"[SUBSCRIBE] Subscription confirmed for game {gameId}, " +
$"confirmedResultCount={ack.ConfirmedResultCount}");
// Clear resync flags ONLY after successful acknowledgment
if (subscriber is GameModelUpdater updater) {
foreach (var status in shardokStatuses.Where(s => s.requestFullResync)) {
updater.ClearShardokResyncFlag(status.shardokGameId);
}
}
// Note: Shardok resync flags are cleared in EagleGameModel.HandleOneGameUpdate
// AFTER updates are actually received, not here. This ensures that if the
// connection drops between acknowledgment and update delivery, the resync
// will be requested again on the next reconnect.
return true;
} else {
@@ -24,11 +24,20 @@ public class MainQueue : MonoBehaviour {
// Update is called once per frame
void Update() {
int actionsProcessed = 0;
int queueDepthBefore;
lock (_actionQueue) { queueDepthBefore = _actionQueue.Count; }
// Fast path: skip processing if queue is empty
if (queueDepthBefore == 0) {
lock (_nextUpdateQueue) {
if (_nextUpdateQueue.Count > 0) {
foreach (Action action in _nextUpdateQueue) { Enqueue(action); }
_nextUpdateQueue.Clear();
}
}
return;
}
// Log when queue has built up (e.g., after resuming from background)
if (queueDepthBefore > 100 && queueDepthBefore != _lastLoggedQueueDepth) {
Debug.Log($"[MainQueue] Processing backlog: {queueDepthBefore} actions queued");
@@ -45,10 +54,7 @@ public class MainQueue : MonoBehaviour {
if (_actionQueue.Count > 0) { possibleAction = _actionQueue.Dequeue(); }
}
if (possibleAction != null) {
possibleAction.Invoke();
actionsProcessed++;
}
if (possibleAction != null) { possibleAction.Invoke(); }
} while (possibleAction != null && stopwatch.ElapsedMilliseconds < MaxMillisecondsPerFrame);
lock (_nextUpdateQueue) {