- Extend SharedTileRing to include a shared dirtyFlags buffer alongside header and data
- Pass shared dirty buffer through WorkerClient/WorkerMessages and initialize views in Worker.worker
- In SAB mode, mark tiles dirty via Atomics.compareExchange before enqueuing to ensure each tile is queued at most once until processed
- On the main thread, clear dirty flags when draining the ring and build packedTileUpdates from distinct tile refs
- Keep non-SAB behaviour unchanged while reducing ring pressure and making overflows reflect true backlog, not duplicate updates
- Share GameMapImpl tile state between worker and main via SharedArrayBuffer
- Add SAB-backed tile update ring buffer to stream tile changes instead of postMessage payloads
- Wire shared state/ring through WorkerClient, Worker.worker, GameRunner, and ClientGameRunner
- Update GameView to skip updateTile when shared state is enabled and consume tile refs from the ring
Added src/core/worker/SharedTileRing.ts, which defines a SharedArrayBuffer-backed ring buffer (SharedTileRingBuffers/SharedTileRingViews) and helpers pushTileUpdate (worker-side writer) and drainTileUpdates (main-thread reader) using Atomics.
Extended GameRunner (src/core/GameRunner.ts) with an optional tileUpdateSink?: (update: bigint) => void; when provided, tile updates are sent to the sink instead of being packed into GameUpdateViewData.packedTileUpdates (those become an empty BigUint64Array in this mode).
Extended the worker protocol (src/core/worker/WorkerMessages.ts) so the init message can optionally carry sharedTileRingHeader and sharedTileRingData (the two SABs for the ring).
Updated WorkerClient (src/core/worker/WorkerClient.ts) to accept optional SharedTileRingBuffers in its constructor and, during initialize(), include those SABs in the init message sent to Worker.worker.ts.
Updated the worker implementation (src/core/worker/Worker.worker.ts) to build sharedTileRing views from the SABs on "init" and pass a (update: bigint) => pushTileUpdate(...) sink into createGameRunner, so every tile update from the simulation goes into the shared ring instead of the message payload.
Updated the client side (src/client/ClientGameRunner.ts):
In createClientGame, when SharedArrayBuffer/Atomics are available and crossOriginIsolated is true, it allocates the ring (createSharedTileRingBuffers with capacity 262144), creates views, passes buffers into WorkerClient, and keeps the views in ClientGameRunner.
ClientGameRunner now has a tileRingViews field and, in mergeGameUpdates, if the ring is present it ignores gu.packedTileUpdates and instead drains up to MAX_TILE_UPDATES_PER_RENDER (100 000) tile updates from the SAB ring into combinedPackedTileUpdates before constructing the merged GameUpdateViewData passed to GameView.update.
If SABs are unavailable, it falls back to the original behaviour (packing gu.packedTileUpdates from messages), so the feature is opt-in and backward compatible.
Net effect: when the page is cross-origin isolated, the worker no longer sends per-tick BigUint64Array tile payloads via postMessage; it streams tile updates into a shared ring buffer, and the main thread pulls and applies them at its own pace while still batching non-tile updates and rendering exactly as before. If you’d like, I can next run the existing tests/build in your environment to sanity-check everything.
- Introduced new metrics in ClientGameRunner to track worker simulation ticks and render tick calls per second.
- Updated TickMetricsEvent to include these new metrics.
- Enhanced PerformanceOverlay to display worker and render ticks per second, improving performance monitoring capabilities.
- Adjusted minimum FPS in GameRenderer
- Refactor rendering and metrics emission in ClientGameRunner to ensure updates occur only after all processing is complete
- Throttle renderGame() based on the current backlog
## Description:
Make more clear who's responsible to set the milestone on a PR. The
current error message raises questions about who's responsible for this,
especially when the PR author doesn't have the access rights to set the
Milestone.
**Old message:**
"Pull request must have a milestone assigned before merging."
**New message:**
"Reviewer must assign a Milestone to this Pull request before merging."
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
tryout33
## Description:
In commit
https://github.com/openfrontio/OpenFrontIO/commit/bbf72bd14f7f31146c687523aea8fc0aff31bbe1#diff-ee2fcbca50d87cc09d2c7d2667210defe2e3e111239820c89c40283be5385b64
it was added that startManpower in DefaultConfig used
playerInfo.nation.strength to set the starting troops for a Nation. In
ExecutionManager, param nation of PlayerInfo was set during the
instantiation of a new FakeHumanExecution.
However in commit
https://github.com/openfrontio/OpenFrontIO/commit/d6a412aa50dd86d474d80c216fd9ba36e7426ef9#diff-2d0a5d8b171d8b504f934891025e42742e142ef0964d6e17712bfdcd30bf050c
the changes made it so that **param nation of PlayerInfo was never
set**. While startManpower in DefaultConfig still checked for
playerinfo.nation.strength. Since it was always undefined, it would use
a multiplier of 1 instead of the actual nation strength.
This PR fixes it by passing the nation strength to param nationStrength
in PlayerInfo. Removing param strength from class Nation. Strength isn't
used anywhere else so this isn't a problem and it also consolidates
human player info and nation player info even more. We could have also
used the Nation.strength directly, but that would have required more
code in addPlayers and addPlayer in GameImpl, especially for Teams
games. So this PR has the simplest solution.
- I did add a config setting useNationStrengthForStartManpower with a
comment that explains its reason for being. Namely that in the months
that startManpower didn't get to use nation strength because of the bug,
FakeHumans have become much harder to fight. Re-enabling higher starting
troops from this fix would make them even harder to fight, and i think
rebalancing is needed before that.
- Or we could decide to scrap Nation strength altogether, as it is only
ever used to set starting troops anyway. This would make map making a
little easier as a bycatch.
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
tryout33
## Description:
Fixes incorrect RTL rendering where the percentage symbol (%) appeared
before the label in Persian (fa-IR).
The UI now correctly displays values as `20%`.
**Changes:**
- Updated `controlpanel.ts` to fix percentage position.
- 16 additions, 7 deletions.
**Testing:**
1. Switch UI language to Persian (fa-IR).
2. Open control panel with percentage display.
3. Confirm `%` appears after numeric value.
4. Verified no changes in LTR languages.
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
nobodyiran
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves#495
## Description:
Adds a button to quickly retaliate against a incoming attack
<img width="464" height="212" alt="image"
src="https://github.com/user-attachments/assets/4764d261-a408-4d61-a2d2-2685018aa698"
/>
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
notifxy (1379678982676676639)
src/client/ClientGameRunner.ts now drains pending game updates in small chunks (max 100 updates or ~8ms per slice) via requestAnimationFrame, merging and rendering per slice, and only clears the processing flag when the queue is empty.
removed:
- catchUpMode and its CATCH_UP_ENTER/EXIT thresholds in ClientGameRunner
- tick metrics fields and overlay UI for inCatchUpMode and beatsPerFrame
- leftover worker heartbeat plumbing (message type + WorkerClient.sendHeartbeat) that was no longer used after self-clocking
changed:
- backlog tracking: keep serverTurnHighWater / lastProcessedTick / backlogTurns, but simplify it to just compute backlog and a backlogGrowing flag instead of driving a dedicated catch-up mode
- frame skip: adaptRenderFrequency now only increases renderEveryN when backlog > 0 and still growing; when backlog is stable/shrinking or zero, it decays renderEveryN back toward 1
- render loop: uses the backlog-aware renderEveryN unconditionally (no catch-up flag), and resets skipping completely when backlog reaches 0
- metrics/overlay: TickMetricsEvent now carries backlogTurns and renderEveryN; the performance overlay displays backlog and current “render every N frames” but no longer mentions catch-up or heartbeats
Learnings during branch development leading to this
Once the worker self-clocks, a separate “catch-up mode” and beats-per-frame knob don’t add real control; they just complicate the model.
Backlog is still a valuable signal, but it’s more effective as a quantitative input (backlog size and whether it’s growing) than as a boolean mode toggle.
Frame skipping should be driven by actual backlog pressure plus frame cost: throttle only while backlog is growing and frames are heavy, and automatically relax back to full-rate rendering once the simulation catches up.
GameRunner exposes pending work via a new hasPendingTurns() so the worker can check whether more ticks need to be processed.
Worker auto-runs ticks: as soon as it initializes or receives a new turn, it calls processPendingTurns() and loops executeNextTick() while hasPendingTurns() is true. No more "heartbeat" message type; the worker no longer depends on the main thread’s RAF loop to advance the simulation.
Client main thread simplified:
Removed CATCH_UP_HEARTBEATS_PER_FRAME, the heartbeat loop, and the lastBeatsPerFrame tracking.
keepWorkerAlive now just manages frame skipping + draining. When it decides to render (based on renderEveryN), it drains pendingUpdates, merges them, updates GameView, and runs renderer.tick().
Because rendering a batch always implies draining, we restored the invariant that every GameView.update is paired with a layer tick() (no more lost incremental updates).
MAX_RENDER_EVERY_N is now 5 to keep the queue from growing too large while the worker sprints.
- Refactor worker update handling into processPendingUpdates so multiple GameUpdateViewData objects are batched per frame.
- Combine all tick updates in a batch into a single GameUpdateViewData before applying it to GameView, while still running per-tick side effects (turnComplete, hashes, backlog metrics, win saving).
- Ensure layers using updatesSinceLastTick and recentlyUpdatedTiles see all events in a batch, fixing visual artifacts during fast-forward resync.
Increase worker heartbeats per frame when far behind server to fast-forward simulation.
Track backlog and expose catch-up status via TickMetricsEvent.
Extend performance overlay to display backlog turns and indicate active catch-up mode.
## Description:
* Update copyright notice to "OpenFront and Contributors"
* remove the "how to play" on the footer, since that will be moved to
the death screen.
* Removed css and used tailwind instead for better mobile support
Describe the PR.
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
evan
## Description:
This PR optimizes how the rail network looks up railroads connecting two
stations by introducing an O(1) neighbor→railroad map on `TrainStation`.
It also updates `getOrientedRailroad` and railroad deletion to use this
new API, avoiding repeated linear scans over all railroads attached to a
station.
### What changed
- **TrainStation neighbor→railroad index**
- Added `railroadByNeighbor: Map<TrainStation, Railroad>` to
`TrainStation` for quick edge lookup.
- Kept `railroads: Set<Railroad>` for iteration and existing APIs.
- Updated lifecycle methods to keep both data structures in sync:
- `addRailroad(railRoad: Railroad)` now:
- Adds to `railroads`.
- Computes the neighbor station (`railRoad.from === this ? railRoad.to :
railRoad.from`).
- Stores the mapping in `railroadByNeighbor`.
- `removeRailroad(railRoad: Railroad)` now:
- Removes from `railroads`.
- Removes the corresponding entry from `railroadByNeighbor`.
- `clearRailroads()` now clears both `railroads` and
`railroadByNeighbor`.
- Added `getRailroadTo(station: TrainStation): Railroad | null` to
retrieve the connecting railroad in O(1).
- **Use the new API in `TrainStation` and `Railroad`**
- `TrainStation.removeNeighboringRails(station)` now calls
`removeRailroad(toRemove)` instead of manually deleting from the set,
ensuring the map stays in sync.
- `Railroad.delete(game)` now calls `from.removeRailroad(this)` and
`to.removeRailroad(this)` instead of mutating the sets directly.
- **Refactor `getOrientedRailroad` to use O(1) lookup**
- Replaced a linear scan over `from.getRailroads()` with a direct
lookup:
```ts
export function getOrientedRailroad(
from: TrainStation,
to: TrainStation,
): OrientedRailroad | null {
const railroad = from.getRailroadTo(to);
if (!railroad) return null;
// If tiles are stored from -> to, we go forward when railroad.to === to
const forward = railroad.to === to;
return new OrientedRailroad(railroad, forward);
}
```
- Behavior is preserved:
- `getRailroadTo` returns the same `Railroad` instance that was
previously found by scanning `getRailroads()`.
- Direction (`forward` vs reversed) is still derived from the
`Railroad.from` / `.to` fields in the same way as before.
### Motivation
- `getOrientedRailroad` and upcoming logic both need to resolve “the
railroad between station A and station B” frequently.
- The old pattern (`for (const railroad of from.getRailroads()) { ...
}`) was:
- O(degree) per lookup,
- Repeated in multiple places,
- Harder to maintain as more features (like fare-based costs) touch this
code.
- Centralizing edge lookup in a dedicated `railroadByNeighbor` map makes
this:
- **O(1)** per lookup,
- Less error-prone (one source of truth),
- Easier to reuse from new systems (e.g. train pathfinding, fare-aware
logic).
### Impact / Risk
- **Public behavior:** No functional change in how railroads are
created, deleted, or oriented; only the lookup mechanism changed.
- **Internal invariants:** Correctness relies on:
- All railroad creations using `addRailroad` on both endpoints (already
true via `RailNetworkImpl.connect`).
- All removals (`Railroad.delete`,
`TrainStation.removeNeighboringRails`, `disconnectFromNetwork`) using
`removeRailroad` / `clearRailroads`, which this PR updates.
- **Tests:** Existing `TrainStation` tests still pass; they exercise
`addRailroad`, `removeNeighboringRails`, and `getRailroads()`, which
continue to behave the same from the outside.
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
DISCORD_USERNAME
Resolves#2448
Hi team,
I've implemented and locally tested the alliance-related changes
(including unit tests and some manual simulation with multiple browser
profiles).
Unfortunately I wasn't able to perform full end-to-end testing on the
live game server with two separate machines/accounts.
If someone on the team (or another contributor) can verify the alliance
flow with two real players, that would be greatly appreciated before
merging. Happy to hop on a call or provide any clarification needed.
Thanks!
## Description:
Fixed a race condition bug where donations (troops/gold) between human
players failed after forming an alliance. The issue was caused by a
one-tick delay in `AllianceRequestReplyExecution`: the alliance
acceptance logic ran in `tick()` instead of `init()`, meaning the
alliance wasn't created until the tick after the execution was added. If
a donation execution was added in the same turn as the alliance
acceptance, it would fail the `isFriendly()` check because the alliance
didn't exist yet.
**Root cause:** When human players formed alliances via reply, the
execution model delayed alliance creation by one tick, while bots called
`accept()` directly without this delay.
**Solution:** Moved alliance acceptance logic from `tick()` to `init()`
in `AllianceRequestReplyExecution.ts`, ensuring immediate alliance
creation and eliminating race conditions with donations.
**Changes:**
- Modified
`src/core/execution/alliance/AllianceRequestReplyExecution.ts` to
process alliance replies in `init()` instead of `tick()`
- Added comprehensive test suite `tests/AllianceDonation.test.ts` with 5
test cases covering donation scenarios after alliance formation (reply
and mutual request flows)
- All existing tests pass (323 total)
## Please complete the following:
- [x] I have added screenshots for all UI updates (N/A - backend logic
fix only)
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file (N/A - no user-facing text
changes)
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
Discord: loacky
GitHub: @LoackyBit
---------
Co-authored-by: Evan <evanpelle@gmail.com>
Nginx was stripping query params when routing requests to workers, so the creatorClientID param was stripped when creating a private game. This caused the game server to not know who the lobby owner was, so it rejected the kick requests.
## Description:
mls for v27
Version identifier within MLS: v4.9
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
aotumuri