## Description:
If the time on the local device differs from the server time, users may
see the message “You did not join the lobby on time.”
Resolve this by accounting for the time difference, reusing the logic in
`JoinLobbyModal` that was previously in `GameModeSelector`, and
centralizing it into `ServerTime.ts`.
Bug reports:
https://github.com/openfrontio/OpenFrontIO/issues/3428https://discord.com/channels/1284581928254701718/1482511096597315815https://discord.com/channels/1284581928254701718/1482382264011591781Resolves#3428
## 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:
An inverse annexation could happen where the small player (even with
0,01% tiles owned) could fully annex the large player.
**TL;DR:** basically wrong use of calculateBoundingBox in
surroundedBySamePlayer, feeding it all bordertiles, making enemyBox far
bigger than it actually was in some cases. Which resulted in enemyBox of
small player with two small clusters at some distance from each other,
being seen as inscribing the largest cluster of the bigger player. While
that largest cluster is actually the border tiles of the bigger player
surrounding the main cluster of the small player. Instead of an
annexation of small by bigger, small would incorrectly annex bigger
completely.
**Situation:** bigger player fully surrounds main cluster of smaller
player. Those border tiles are also the largest cluster of the bigger
player, for which surroundedBySamePlayer is called.
SurroundedBySamePlayer finds the small player as the only bordering
enemy of this cluster. Then it needs to check which of the two players
is surrounded by the other one. EnemyBox uses calculateBoundingBox with
all border tiles of the small player as argument. The small player also
has at least one seperate cluster elsewhere, could be on another island,
which count as border tiles too. The enemyBox from the main cluster of
the small player to the seperate cluster elsewhere, can be huge. Now
inscribed() is called and it determines that largest cluster box of the
bigger player (which was in fact calculated correctly, also making use
of calculateBoundingBox) is surrounded by the bigger enemyBox. And so
the small surrounded player fully annexes the bigger player.
**Fix:** instead of a global enemyBox, we only need the localEnemyBox
that touches the largest cluster of the bigger player. With that,
inscribed() can correctly conclude that largest cluster box surrounds
the localEnemyBox. As a matter of fact isSurrounded() already used the
same method to calculate its enemyBox as introduced by @scamiv for v30:
https://github.com/openfrontio/OpenFrontIO/pull/3127/changes#diff-fb1101a2b50dd7c353d082ff7a3351cff5469b8249b3ebca91c10573a3dfaaf1
- Change in PlayerExecution
- Added test NoInverseAnnexation.test.ts, which fails before and passes
after the fix
The bug was introduced in this commit 10 months ago:
https://github.com/openfrontio/OpenFrontIO/commit/c4381a9ad3828b06764ab1a21fc1514e37aacfd7
It has probably led to some weird annexations happening since then. The
bug could seemingly happen on any map. But was noted recently a few
times on square islands (Sierpinski) or maps (The Box/The Alps), where
the circumstances probably highten the chances of the bug occuring.
**Bug reports:**
https://discord.com/channels/1359946986937258015/1481916231689703477/1481916231689703477https://discord.com/channels/1359946986937258015/1481916231689703477/1481963273367851030https://discord.com/channels/1284581928254701718/1479993924432171008/1479995658302652496https://discord.com/channels/1284581928254701718/1479993924432171008/1481865495492956182https://discord.com/channels/1284581928254701718/1483047153571201034
**BEFORE:**
https://github.com/user-attachments/assets/4440182b-f696-45cf-bb01-b10159df8763
**AFTER**, on the same replay but with the bugfix:
https://github.com/user-attachments/assets/5f461ab2-eb62-4cc3-ae07-e2224adbbc6a
## 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:
Can't tell you how many times I've been playing solo, I try to go change
the speed from `Max` to `x1` and before I've opened the speed controls
and clicked on one the AI completely wipes me. But not to worry, we now
have a pause and speed toggle up/down keybinds!
https://github.com/user-attachments/assets/48692c27-888f-40fb-837a-45e26f262441
Keybinds were added to "Menu Shortcuts" submenu:
<img width="1750" height="1099" alt="image"
src="https://github.com/user-attachments/assets/8c4500d5-f43e-4a1c-9940-04db75bf18cf"
/>
Tested on Solo, custom match, and public lobbies. Pause intent works
correctly on solo and private match (only if you are host), and neither
feature works in public matches, as expected.
## 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:
bijx
## Description:
Reduces `numSpawnPhaseTurns` from 300 to 100 (matching singleplayer
duration) when random spawn is active. Since players can't choose their
spawn location in random spawn mode, there's no need for the full
300-tick spawn phase.
## 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:
FloPinguin
## Description:
For the next v30 fix version
<img width="868" height="364" alt="imaege"
src="https://github.com/user-attachments/assets/520a999c-67e7-4c57-8651-895ad9eeb73a"
/>
HvN balancing for the revamped difficulty steps of v30 sadly doesn't
really work out...
In medium difficulty games humans nearly always win (boring)
In hard difficulty games humans usually lose
It was intended differently...
So lets get rid of medium difficulty HvN, always use hard difficulty and
disable the donation-capability for public game nations.
That will tune the human winrate towards a middle ground at about 65% I
think. Which should be nice.
Easier than in v29 (was frustrating sometimes) but not as easy as it's
now.
We can only test this in prod lol
## 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:
FloPinguin
## Description:
mls for v30
Version identifier within MLS: 4.16
## 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
## Description:
* Adds ClientMsgRateLimiter — a per-client token-bucket rate limiter
that gates all incoming WebSocket messages. Returns "ok", "limit"
(drop), or "kick" based on the violation type.
* Intent messages are capped at 500 bytes each (they are stored in turn
history for the game duration, so oversized intents
accumulate in server RAM). Violations kick the client.
* Winner messages bypass the byte rate limit (they include stats for all
players and can be 100s of KB) but are strictly capped at one per client
— a second winner message kicks the client.
* All other messages go through the standard per-second (10/s) and
per-minute (150/min) rate limits. Violations drop the message; byte
budget exhaustion kicks the client.
* WebSocket maxPayload set to 2 MB on game workers.
Invalid (unparseable) messages now immediately kick the client rather
than being silently dropped.
Unit tests added for all rate limiting behaviors.
## 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:
- Prevent `BaseModal.open()` from firing `onOpen()` twice when
Navigation.showPage() re-calls `open()` on inline modals
- Adds an `isModalOpen` early-return guard, matching existing behavior
in `OModal.open()`
## Please complete the following:
- [ ] I have added screenshots for all UI updates
- [ ] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [ ] I have added relevant tests to the test directory
- [ ] 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:
Jish
## Description:
**Homepage layout:** Reorder the game mode cards so FFA is the left
(large) box, Teams is the upper-right box, and Special Games is the
lower-right box. Mobile order updated to match (FFA → Teams → Special).
**Special game modifiers:**
- Adjusted modifier count roll to 40%/40%/15%/5% for 1/2/3/4 modifiers
(was 30%/40%/20%/10%) because having so many special games with 3/4
modifiers while we only have 8 modifiers in the pool is a bit dumb (from
the 8 modifiers two are mutually exclusive and 4 should be quite rare).
- Changed ticket counts in `SPECIAL_MODIFIER_POOL` so
isAlliancesDisabled and isHardNations are more rare.
## 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:
FloPinguin
## Description:
**Homepage layout:** Reorder the game mode cards so FFA is the left
(large) box, Teams is the upper-right box, and Special Games is the
lower-right box. Mobile order updated to match (FFA → Teams → Special).
**Special game modifiers:**
- Adjusted modifier count roll to 40%/40%/15%/5% for 1/2/3/4 modifiers
(was 30%/40%/20%/10%) because having so many special games with 3/4
modifiers while we only have 8 modifiers in the pool is a bit dumb (from
the 8 modifiers two are mutually exclusive and 4 should be quite rare).
- Changed ticket counts in `SPECIAL_MODIFIER_POOL` so
isAlliancesDisabled and isHardNations are more rare.
## 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:
FloPinguin
## Description:
The French Flag has its real colours now.
Source: https://fr.wikipedia.org/wiki/Drapeau_de_la_France
## 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:
canonulldreinull_
## Description:
To give more space in the center of the screen remove the top & bottom
margin. Also make the control panel & player info panel slightly shorter
& longer.
<img width="1167" height="961" alt="Screenshot 2026-03-12 at 7 20 40 PM"
src="https://github.com/user-attachments/assets/15baf640-daf0-4394-9107-d175130e7754"
/>
## 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
After a player left, the server would fall back to Lobby phase if player
count dropped below the max — even if the game had already started.
Now checks hasStarted() before reporting Lobby phase.
## Description:
In v30 we have the following change to prevent teammates from destroying
your structures:
**Block nuking teammate structures** - Nukes blocked if they'd hit a
teammate's structure (that was possible by nuking oceans / rivers) (by
@FloPinguin)
Original idea was from Wonder.
I think it makes sense, but it has a side effect: The aftergame, which
many players love, will be dead because of this change.
<img width="835" height="103" alt="image"
src="https://github.com/user-attachments/assets/521b7915-be28-4d83-8d45-65835e7385ab"
/>
<img width="1101" height="105" alt="image"
src="https://github.com/user-attachments/assets/db74a9c6-da12-44a2-aa06-f042b8e58b8a"
/>
I think a lot of complaints will follow after v30 is live.
So why not add a little bit of logic for the aftergame?
After a team wins/loses, players can nuke their teammates. No longer
need to aim for water. SAMs also intercept teammate nukes in this phase.
## 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:
FloPinguin
## Description:
Calling `lobbySocket.stop()` closes the WebSocket, but the async `close`
event fires afterward and triggers `handleClose()` →
`scheduleReconnect()`, reopening the connection. In singleplayer, both
`onPrestart` and `onJoin` callbacks fire synchronously in a single
stack, so the second `stop()` call has nothing left to clean up - the
reconnect timeout gets set after both calls complete.
Adds a `stopped` flag that skips reconnection in `handleClose()` when
the socket was intentionally closed.
Surely this PR description is not AI generated.
Should reduce network load of the server because all the singleplayer
gamers no longer stay connected to the lobbies websocket forever. They
also no longer load all the map mainfest.jsons and thumbnails while they
are playing alone (rotation running in the background).
- [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:
FloPinguin
## Description:
Before:
<img width="215" height="239" alt="Screenshot 2026-03-10 at 10 29 12 PM"
src="https://github.com/user-attachments/assets/bb044425-eb2f-427c-afd6-6c9dd5d075aa"
/>
After:
<img width="240" height="207" alt="Screenshot 2026-03-10 at 10 27 33 PM"
src="https://github.com/user-attachments/assets/21ce4c3b-ab24-4af0-b608-6be5603320fb"
/>
## 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:
Now that cities snap to existing rails, it's possible to line up dozens
of cities in a row, producing way too much gold. This PR reduces the
gold after each stop to prevent that. Gold only starts decreasing after
the 3rd city.
## 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:
Show outgoing attacks on the PlayerInfoPanel. Also update the
AttacksDisplay so it uses the same icon for incoming & outgoing attacks
<img width="502" height="97" alt="Screenshot 2026-03-10 at 2 02 49 PM"
src="https://github.com/user-attachments/assets/de83bf4d-b1b9-4fec-a8c6-e25bbf82c942"
/>
<img width="483" height="167" alt="Screenshot 2026-03-10 at 2 03 02 PM"
src="https://github.com/user-attachments/assets/cf43106f-5f3a-4a78-abae-dff019de4fb4"
/>
<img width="509" height="203" alt="Screenshot 2026-03-10 at 2 03 28 PM"
src="https://github.com/user-attachments/assets/79db2081-dd93-4ff4-aaa7-75281c16c037"
/>
## 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:
When the game was on dark mode, seeing the coordinate text on oceans
became impossible. Now we check for dark mode in the layer to change the
text color accordingly.
Light Mode:
<img width="3430" height="1669" alt="image"
src="https://github.com/user-attachments/assets/181d53f5-b218-4f85-9d06-7e80f5d20004"
/>
Dark Mode:
<img width="3488" height="1706" alt="image"
src="https://github.com/user-attachments/assets/8a8e3951-31b4-448d-bc87-7fabd105e5fb"
/>
## 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:
bijx
## Description:
During the playtests people mentioned the UI was too small. Also change
the troop color because white & green was difficult to read.
<img width="559" height="196" alt="Screenshot 2026-03-10 at 12 42 58 PM"
src="https://github.com/user-attachments/assets/9ba5ccaa-3113-4998-a016-e9936269f5de"
/>
<img width="491" height="96" alt="Screenshot 2026-03-10 at 12 43 15 PM"
src="https://github.com/user-attachments/assets/54f7886f-b830-451e-9a7a-3a764efdfbcc"
/>
<img width="574" height="208" alt="Screenshot 2026-03-10 at 12 43 25 PM"
src="https://github.com/user-attachments/assets/2583b492-1844-4814-8fd8-b7216a1dad60"
/>
## 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