VariableVince 341f344ce5 Perf/Refactor(UserSettings): caching makes it 10-20x faster (#3481)
## Description:

Skip slow and blocking LocalStorage reads, replace by a Map. Also some
refactoring.

### Contains

- No out-of-sync issue between main and worker thread: Earlier PRs got a
comment from evan about main & worker.worker thread having their own
version of usersettings and possibly getting out-of-sync (see
https://github.com/openfrontio/OpenFrontIO/pull/760#pullrequestreview-2845155737,
https://github.com/openfrontio/OpenFrontIO/pull/896#pullrequestreview-2871836979
and https://github.com/openfrontio/OpenFrontIO/pull/1266.
But userSettings is not used in files ran by worker.worker, not even 10
months after evan's first comment about it. In GameRunner,
createGameRunner sends NULL to getConfig as argument for userSettings.
And DefaultConfig guards against userSettings being null by throwing an
error, but it has never been thrown which points to worker.worker thread
not using userSettings. So we do not need to worry about syncing between
the threads currently.
(If needed in the future after all, we could quite easily sync it, by
loading the userSettings cache on worker.worker and listening to the
"user-settings-changed" event @scamiv to keep it synced (changes in
WorkerMessages and WorkerClient etc would be needed to handle this).

- Went with cache in UserSettings, not with listening to
"user-settings-changed" event: "user-settings-changed" was added by
@scamiv and is used in PerformanceOverlay. Which is great for single
files that need the very best performance. But having to add that same
system to any file reading settings, scales poorly and would lead to
messy code. Also, a developer could make the mistake of not listening to
the event and it would end up just reading LocalStorage again just like
now. Also a developer might forget removing the listener or so etc. The
cache is a central solution and fast, without changes to other files
needed and future-proof.

- Make sure each setting is cached: UserSettingsModal was using
LocalStorage directly by itself for some things. Made it use the central
UserSettings methods instead so we avoid LocalStorage reads as much as
possible. For this, changed get() and set() in UserSettings to getBool()
and setBool(), to introduce a getString() and setString() for use in
UserSettingsModal while keeping getCached() and setCached() private
within UserSettings.

- Remove unused 'focusLocked' and 'toggleFocusLocked' from UserSettings:
was last changed 11 months ago to just return false. Since then we've
moved to different ways of highlighting and this setting isn't used
anymore. No existing references or callers are left.

- Other files:
-- Have callers call the renamed functions (see point above)
-- Remove userSettings from UILayer and Territorylayer: the variable is
unused in those files. Also remove from GameRenderer when it calls
TerritoryLayer.
-- Cache calls to defaultconfig Theme (which in turn calls dark mode
setting)/Config better in: GameView and Terrainlayer.

### Update on Contents later on
It wasn't really in scope of this PR but further consolidation was
called for. These changes could also pave the way for UserSettingsModal
(main menu) perhaps being partly mergable with SettingsModal (in-game)
one day as it begins to look more like it. Even though UserSettingsModal
still does things its own way, and does console.log where SettingsModal
doesn't, etc. They both have partially different content and settings
but also have a large overlap.

- UserSettings: Removed localStorage call from clearFlag() and setFlag()
which were added after creation of this PR, and were neatly merged in
silence without merge conflicts so i wasn't aware of them yet until now.

- UserSettings: added key constants, exported to use both inside
UserSettings and in files that listen to its events.

- UserSettings 'emitChange': now done from setCached, removed from
setBool, setFlag etc. Also removed from the new setFlag. And from
setPattern even though it emitted "pattern" instead of key name
"territoryPattern"; now it emits the default "territoryPattern" from
PATTERN_KEY which is re-used in Store, TerritoryPatternsModal and
PatternInput.

- UserSettingsModal: made UserSettingsModal call existing toggle
functions in UserSettings, or new or existing getter or setter. We do
not need CustomEvent: checked anymore. In UserSettingsModal, its toggle
functions did not all actually toggle, some like
toggleLeftClickOpensMenu actually just set a value. Based on the
'checked' value of the CustomEvent. But we don't need that 'checked'
value anymore and none of the checks for it inside the toggle functions
in UserSettingsModal, now that we just directly call
toggleLeftClickOpensMenu and others in UserSettings.

- SettingToggle: continuing about not needing CustomEvent anymore: the
old way actually fired two events. The native change event from <input>
and our own CustomEvent from handleChange in SettingToggle. It prevented
handling both events by checking e.detail?.checked === undefined. But
now, the native <input> event is all we need to show the visual toggle
change and trigger @changed in UserSettingsModal which calls the toggle
function.

- Use the toggle functions too from CopyButton and
PerformanceOverlay.ts. In PerformanceOverlay, change in
onUserSettingsChanged was needed because of how setBool works.

- UserSettingsModal 'toggleDarkMode': in UserSettingsModal, removed the
event from toggleDarkMode in UserSettingsModal; nothing is listening to
this event anymore after DarkModeButton.ts was removed some time ago.
Also both UserSettingsModal an UserSettings added/removed "dark" from
the document element. Now that UserSettingsModal calls toggleDarkMode in
UserSettings, we could centralize that. But UserSettings is in core, not
in client like UserSettingsModal. But now that we emit
"user-settings-changed", we could handle it even more centralized and
not have UserSettingsModal or UserSettings touch the element directly.
Instead have Main.ts listen to the event and change it dark mode from
there.

- UserSettings: added claryfing comment to attackRatioIncrement and the
new attackRatio setters/getters, to explain their difference. Noticed a
small omitment in its description and fixed that right away in en.json:
you can change attack ratio increment by shift+mouse wheel scroll or by
hotkey. So made "How much the attack ratio keybinds change per press"
also mention "/scroll."



**BEFORE** (with getDisplayName added back to NameLayer as a fix i will
do soon)
get > getItem in UserSettings
![BEFORE get
getItem](https://github.com/user-attachments/assets/5d2bf8b2-9e68-4c58-9b1f-d5636ee5d7e9)

renderLayer in NameLayer (with getDisplayName added back to NameLayer as
a fix i will do soon)
![BEFORE renderLayer NameLayer
ea](https://github.com/user-attachments/assets/ea12d9a4-2ff3-421b-844c-dbc39e5c3193)

**AFTER** (with getDisplayName added back to NameLayer as a fix i will
do soon)
getCached in UserSettings
![AFTER
getCached](https://github.com/user-attachments/assets/7fb1151f-d289-4420-a257-9fe1f9fbcb8f)

renderLayer in NameLayer (with getDisplayName added back to NameLayer as
a fix i will do soon)
![AFTER renderLayer NameLayer
ea](https://github.com/user-attachments/assets/f844e3d4-d6e5-4774-ba18-ba541f066c76)

## 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
2026-04-06 20:41:57 -07:00
2025-06-22 08:14:08 -07:00
2026-03-22 15:10:43 -07:00
2025-09-30 11:13:32 -07:00
2026-04-06 20:38:22 -07:00
2025-05-15 23:09:39 -04:00
2026-04-06 13:40:09 -07:00
2025-03-06 15:50:29 -08:00
2025-05-15 23:09:39 -04:00
2026-03-23 13:59:34 -07:00
2026-01-21 10:00:55 -08:00
2026-03-26 19:18:30 -07:00
2026-04-01 19:33:55 -07:00
2026-04-01 19:33:55 -07:00
2026-01-08 13:34:18 -08:00
2026-04-01 19:33:55 -07:00
2026-04-06 20:38:22 -07:00
2026-04-01 20:03:39 -07:00
2026-04-01 19:33:55 -07:00

OpenFrontIO Logo

OpenFront.io is an online real-time strategy game focused on territorial control and alliance building. Players compete to expand their territory, build structures, and form strategic alliances in various maps based on real-world geography.

This is a fork/rewrite of WarFront.io. Credit to https://github.com/WarFrontIO.

CI Crowdin CLA assistant License: AGPL v3 Assets: CC BY-SA 4.0

License

OpenFront source code is licensed under the GNU Affero General Public License v3.0

Current copyright notices appear in:

  • Footer: "© OpenFront and Contributors"
  • Loading screen: "© OpenFront and Contributors"

Modified versions must preserve these notices in reasonably visible locations.

See the LICENSE for complete requirements.

For asset licensing, see LICENSE-ASSETS.
For license history, see LICENSING.md.

🌟 Features

  • Real-time Strategy Gameplay: Expand your territory and engage in strategic battles
  • Alliance System: Form alliances with other players for mutual defense
  • Multiple Maps: Play across various geographical regions including Europe, Asia, Africa, and more
  • Resource Management: Balance your expansion with defensive capabilities
  • Cross-platform: Play in any modern web browser

📋 Prerequisites

  • npm (v10.9.2 or higher)
  • A modern web browser (Chrome, Firefox, Edge, etc.)

🚀 Installation

  1. Clone the repository

    git clone https://github.com/openfrontio/OpenFrontIO.git
    cd OpenFrontIO
    
  2. Install dependencies

    npm run inst
    

    Do NOT use npm install nor npm i but instead use our npm run inst. It runs the safer npm ci --ignore-scripts to install dependencies exactly according to the versions in package-lock.json and doesn't run scripts. This can prevent being hit by a supply chain attack.

🎮 Running the Game

Development Mode

Run both the client and server in development mode with live reloading:

npm run dev

This will:

  • Start the webpack dev server for the client
  • Launch the game server with development settings
  • Open the game in your default browser (to disable this behavior, set SKIP_BROWSER_OPEN=true in your environment)

Client Only

To run just the client with hot reloading:

npm run start:client

Server Only

To run just the server with development settings:

npm run start:server-dev

Connecting to staging or production backends

Sometimes it's useful to connect to production servers when replaying a game, testing user profiles, purchases, or login flow.

To replay a production game, make sure you're on the same commit that the game you want to replay was executed on, you can find the gitCommit value via https://api.openfront.io/game/[gameId]. Unfinished games cannot be replayed on localhost.

To connect to staging api servers:

npm run dev:staging

To connect to production api servers:

npm run dev:prod

🛠️ Development Tools

  • Format code:

    npm run format
    
  • Lint code:

    npm run lint
    
  • Lint and fix code:

    npm run lint:fix
    
  • Testing

    npm test
    

🏗️ Project Structure

  • /src/client - Frontend game client
  • /src/core - Shared game logic
  • /src/server - Backend game server
  • /resources - Static assets (images, maps, etc.)

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Request to join the development Discord.
  2. Fork the repository
  3. Create your feature branch (git checkout -b amazing-feature)
  4. Commit your changes (git commit -m 'Add some amazing feature')
  5. Push to the branch (git push origin amazing-feature)
  6. Open a Pull Request

🌐 Translation

Translators are welcome! Please feel free to help translate into your language. How to help?

  1. Join the translation Discord
  2. Go to the project's Crowdin translation page: https://crowdin.com/project/openfront-mls
  3. Login if you already have an account / Sign up if you don't have one
  4. Join the project
  5. Select the language you want to translate in. If your language isn't on the list, click the "Request New Language" button and enter the language you want added there.
  6. Translate the strings

Feel free to ask questions in the translation Discord server!

Project Governance

  • The project maintainer (evan) has final authority on all code changes and design decisions
  • All pull requests require maintainer approval before merging
  • The maintainer reserves the right to reject contributions that don't align with the project's vision or quality standards

Contribution Path for New Contributors

To ensure code quality and project stability, we use a progressive contribution system:

  1. New Contributors: Limited to UI improvements and small bug fixes only

    • This helps you become familiar with the codebase
    • UI changes are easier to review and less likely to break core functionality
    • Small, focused PRs have a higher chance of being accepted
  2. Established Contributors: After several successful PRs and demonstrating understanding of the codebase, you may work on more complex features

  3. Core Contributors: Only those with extensive experience with the project may modify critical game systems

How to Contribute Successfully

  1. Before Starting Work:

    • Open an issue describing what you want to contribute
    • Wait for maintainer feedback before investing significant time
    • Small improvements can proceed directly to PR stage
  2. Code Quality Requirements:

    • All code must be well-commented and follow existing style patterns
    • New features should not break existing functionality
    • Code should be thoroughly tested before submission
    • All code changes in src/core MUST be tested.
  3. Pull Request Process:

    • Keep PRs focused on a single feature or bug fix
    • Include screenshots for UI changes
    • Describe what testing you've performed
    • Be responsive to feedback and requested changes
  4. Testing Requirements:

    • Verify your changes work as expected
    • Test on multiple systems/browsers if applicable
    • Document your testing process in the PR

Communication

  • Be respectful and constructive in all project interactions
  • Questions are welcome, but please search existing issues first
  • For major changes, discuss in an issue before starting work

Final Notes

Remember that maintaining this project requires significant effort. The maintainer appreciates your contributions but must prioritize long-term project health and stability. Not all contributions will be accepted, and that's okay.

Thank you for helping make OpenFront better!

S
Description
Languages
TypeScript 91.3%
GLSL 2.6%
JavaScript 1.9%
HTML 1.7%
CSS 1%
Other 1.5%