Jump to content

Modding: Difference between revisions

From Wall Street Raider Wiki
Created page with "= Wall Street Raider - Modding Guide = This document describes '''everything you can mod today''' in Wall Street Raider, and exactly how to do it. It covers the practical, supported surfaces: the Electron frontend (code, styling, assets, help, translations, hints), the plain-text company-name database, the REST + WebSocket API, and the '''CustomData API''' - the one supported path for writing your own persistent data into a save file. It does '''not''' cover modifying..."
 
No edit summary
 
(4 intermediate revisions by the same user not shown)
Line 1: Line 1:
= Wall Street Raider - Modding Guide =
= Wall Street Raider - Modding Guide =


This document describes '''everything you can mod today''' in Wall Street Raider, and exactly how to do it. It covers the practical, supported surfaces: the Electron frontend (code, styling, assets, help, translations, hints), the plain-text company-name database, the REST + WebSocket API, and the '''CustomData API''' - the one supported path for writing your own persistent data into a save file.
A reference for everything you can mod in WSR with nothing but a text editor. The Electron frontend has '''no build step''' and the install ships with <code>asar: false</code>, so every file under <code>resources/app/</code> is a normal editable file. '''Edit, CTRL+R, see your change.'''


It does '''not''' cover modifying the PowerBASIC game engine itself (the simulation logic), because that requires recompiling <code>wsr.exe</code> from source with a PowerBASIC 9.80 compiler. Everything below is doable with nothing more than a text editor.
What you '''can't''' mod without recompiling: the PowerBASIC engine (<code>wsr.exe</code>), the C++ bridge (<code>ui.dll</code>), and the binary <code>.DAT</code>/<code>.PRM</code> files.


== How the game is built (why modding is easy) ==
{{Note|Back up before you edit. Steam's "verify integrity of game files" reverts changes - that's also your safety net.}}


WSR is three layers:
== TL;DR - what's moddable ==


<pre>
{| class="wikitable"
PowerBASIC engine  →  C++ bridge DLL (REST + WebSocket server)  →  Electron UI
! I want to… !! Do this
  wsr.exe              ui.dll                                      everything you see
|-
</pre>
| Re-theme the game || Edit <code>css/variables.css</code>, then <code>css/theme.css</code> / <code>css/components/</code>
 
|-
The crucial fact for modders: '''the Electron frontend has no build step.''' The UI is plain ES modules, plain CSS, and plain asset files loaded directly from disk at runtime. There is no webpack, no transpile, no bundling. The app is packaged with <code>asar: false</code>, which means after installation every file is still a normal editable file on disk - nothing is archived or compressed.
| Change UI text / layout / behavior || Edit the relevant file in <code>js/components/</code>
 
|-
That means: '''edit a file, restart the game, see your change.'''
| Add a new panel or feature || New file in <code>js/components/</code>, import it from a parent or <code>app.js</code>
 
|-
== Where the files live ==
| Replace a background video or image || Drop a same-named file into <code>assets/</code>
|-
| Rewrite the manual || Edit <code>assets/help/wsrbook.htm</code>
|-
| Add a language || New <code>js/locale/&lt;code&gt;.js</code> + register in <code>localeManager.js</code>
|-
| Change / add contextual hints || Edit files in <code>js/data/hints/</code>
|-
| Rename companies / tickers / nations || Edit <code>CORPNAME.DAT</code> (plain text). Effective on '''next New Game'''.
|-
| Drive the game from JS || Call an <code>api.js</code> function ([[#Calling the API from a frontend mod|§api.js]])
|-
| Drive the game from a third-party app || Hit the REST API directly ([[#REST API reference|§REST API]])
|-
| Read game state || <code>useGameStore(s =&gt; s.gameState.&lt;field&gt;)</code> or <code>GET /gamestate</code> ([[#gameState reference|§gameState]])
|-
| Persist your mod's data in saves || <code>setCustomData({ myKey: … })</code> ([[#CustomData - your persistent storage|§CustomData]])
|-
| Share a mod with other players || Steam Workshop ([[#Steam Workshop|§Workshop]])
|-
| Change a core simulation rule || '''Not moddable.''' Requires engine source + recompile.
|}


=== During development (running from source) ===
== Architecture in one breath ==


<pre>
<pre>
electron/
PowerBASIC engine  →  C++ bridge DLL (REST + WebSocket)  →  Electron UI
├── index.html            # app entry point
  wsr.exe              ui.dll                                everything you see
├── main.js              # Electron main process
├── js/                  # all frontend code
├── css/                  # all styling
├── assets/              # images, videos, icons, help
└── ...
</pre>
</pre>


=== In an installed / Steam copy ===
At launch, <code>ui.dll</code> picks ephemeral REST + WS ports and writes them to <code>%LOCALAPPDATA%\Wall Street Raider\runtime.json</code>:
 
After packing, everything lands under the install directory in:


<pre>
<pre>
&lt;install&gt;/resources/app/
{ "pid": 12345, "rest_port": 54321, "ws_port": 54322 }
├── index.html
├── main.js
├── js/                  # ← editable
├── css/                  # ← editable
├── assets/              # ← editable
├── wsr.exe              # game engine (binary, not moddable without source)
├── ui.dll                # bridge (binary, not moddable without source)
├── CORPNAME.DAT          # ← editable plain-text company database
├── *.DAT / *.PRM        # other game data (mostly binary/encoded)
└── ...
</pre>
</pre>


<code>resources/app/js</code>, <code>resources/app/css</code>, and <code>resources/app/assets</code> are copied verbatim from the <code>electron/</code> source tree at pack time (see the <code>extraFiles</code> block in <code>electron/package.json</code>). Editing them in an installed copy works exactly the same as editing them in source.
Electron reads that file and hands the ports to the renderer; <code>js/api.js</code> builds <code>http://127.0.0.1:&lt;rest&gt;</code> and <code>ws://127.0.0.1:&lt;ws&gt;</code>. WS pushes a full snapshot on connect, then JSON-patch diffs; if WS drops, the UI polls <code>GET /gamestate</code>.


{{Note|Back up any file before you edit it. A Steam "verify integrity of game files" will revert your changes - that is also your safety net.}}
'''Third-party apps:''' read <code>runtime.json</code> the same way. Ports change every launch.


== What you can mod ==
== File layout ==


=== Frontend code - <code>resources/app/js/</code> ===
In source (<code>electron/</code>) and in an installed copy (<code>&lt;install&gt;/resources/app/</code>) the layout is identical - the install is a verbatim copy of the source tree (see <code>extraFiles</code> in <code>electron/package.json</code>):


The entire UI is here, as ES modules. Highlights:
<pre>
resources/app/
├── index.html              # entry point
├── main.js                # Electron main process
├── js/                    # ← all frontend code (editable)
│  ├── app.js              #  Preact root, WS wiring, hotkeys
│  ├── api.js              #  REST/WS client, gameStore, ID constants, endpoint wrappers
│  ├── components/        #  100+ Preact panels/modals/tables/tabs
│  ├── hooks/              #  useCookie, useInterval, usePanelSelection, …
│  ├── services/          #  hintMatcher.js,
│  ├── utils/              #  misc
│  ├── icons.js            #  inline SVG
│  ├── debug-log.js        #  frontend logging
│  ├── lib/                #  vendored: preact, zustand, tailwind, fast-json-patch
│  ├── locale/            #  ← translations
│  └── data/hints/        #  ← contextual hints
├── css/                    # ← all styling (editable)
├── assets/                # ← images / videos / icons / help (editable)
├── CORPNAME.DAT            # ← plain-text company database (editable)
├── wsr.exe                # engine (binary)
├── ui.dll                  # bridge (binary)
└── *.DAT / *.PRM          # other game data (binary/encoded - leave alone)
</pre>


{| class="wikitable"
== Frontend code (<code>js/</code>) ==
! Path !! What it is
|-
| <code>js/app.js</code> || Preact root component, WebSocket wiring, gameState polling, hotkey handling
|-
| <code>js/api.js</code> || REST/WebSocket client, the Zustand <code>gameStore</code>, all constants, the command map
|-
| <code>js/components/</code> || 100+ Preact components - every panel, modal, table, tab
|-
| <code>js/hooks/</code> || Reusable Preact hooks (<code>useCookie</code>, <code>useInterval</code>, <code>usePanelSelection</code>, …)
|-
| <code>js/services/</code> || Logic helpers (e.g. <code>hintMatcher.js</code>)
|-
| <code>js/utils/</code> || Misc utilities
|-
| <code>js/icons.js</code> || Inline SVG icon definitions
|-
| <code>js/debug-log.js</code> || Frontend logging
|}


Components use '''Preact + htm''' (JSX-like template literals, no compile step) and '''Zustand''' for state. A minimal component looks like:
Components use '''Preact + htm''' (JSX-like template literals, no compile step) and '''Zustand''' for state. Minimal example:


<pre>
<pre>
Line 87: Line 95:


export function MyPanel() {
export function MyPanel() {
     const cash = useGameStore(s => s.gameState.cash);
     const cash = useGameStore(s =&gt; s.gameState.cash);
     return html`&lt;div class="p-4"&gt;Cash: ${cash}&lt;/div&gt;`;
     return html`&lt;div class="p-4"&gt;Cash: ${cash}&lt;/div&gt;`;
}
}
</pre>
</pre>


You can freely:
You can:
* Change layout, text, colors, behavior of any existing component.
* Edit any existing component (layout, text, colors, behavior).
* Add brand-new components and wire them into <code>app.js</code> or any parent.
* Add new components and import them into <code>app.js</code> or any parent.
* Add new hotkeys (search <code>hotkey</code> across <code>js/</code> - the handling lives in <code>app.js</code> and <code>HotkeyButtonBar.js</code>).
* Add new hotkeys (grep <code>hotkey</code>; handling lives in <code>app.js</code> + <code>HotkeyButtonBar.js</code>).
* Add new derived/computed displays from <code>gameState</code> (see [[#The gameState - everything you can read|§6]]).
* Add new buttons that call any API endpoint ([[#Calling the API from a frontend mod|§api.js]]).
* Add new buttons that call any API endpoint (see [[#The API - everything you can make the game do|§5]]).
* Add new derived displays from <code>gameState</code> ([[#gameState reference|§gameState]]).


Libraries are vendored in <code>js/lib/</code> (<code>preact.standalone.module.js</code>, <code>zustand.module.js</code>, <code>tailwind.module.js</code>, <code>fast-json-patch.module.js</code>) - no npm install needed to develop against them.
== Styling (<code>css/</code>) ==
 
=== Styling - <code>resources/app/css/</code> ===
 
CSS is modular and loaded at runtime. The structure:


<pre>
<pre>
css/
css/
├── style.css             # the single file index.html links - imports the rest
├── style.css               # the file index.html links - imports the rest
├── base.css             # resets / base element styles
├── base.css               # resets / base element styles
├── variables.css         # CSS custom properties (THE place for theme colors)
├── variables.css           # CSS custom properties (theme palette lives here)
├── theme.css             # theme layer
├── theme.css               # theme layer
├── layout.css           # app layout
├── layout.css             # app layout
├── chart-styles.js       # chart styling injected as JS
├── chart-styles.js         # chart styling injected as JS
├── components/           # buttons, dropdown, forms, modals, panels, tables, tabs
├── components/             # buttons, dropdown, forms, modals, panels, tables, tabs
├── features/             # assets, calculator, help, market, menu, notes, ticker, tutorial
├── features/               # assets, calculator, help, market, menu, notes, ticker, tutorial
└── pages/               # game, main-menu, quotes
└── pages/                 # game, main-menu, quotes
</pre>
</pre>


Tailwind utility classes are also available (loaded via <code>js/lib/tailwind.module.js</code>), so most components use Tailwind utilities inline plus the modular CSS for anything custom.
Tailwind utilities are also available via <code>js/lib/tailwind.module.js</code>, so most components use Tailwind inline + the modular CSS for anything custom.


'''To re-theme the game''', start with <code>css/variables.css</code> - it defines the color palette as CSS custom properties. Override those and the whole app follows. For finer control, edit the per-component and per-feature files.
'''To re-theme:''' start with <code>variables.css</code> - override the custom properties and the whole app follows.


=== Assets - <code>resources/app/assets/</code> ===
== Assets (<code>assets/</code>) ==
 
All images, videos, icons, and the loading animation:


{| class="wikitable"
{| class="wikitable"
! Asset type !! Files
! What !! Files
|-
|-
| Background videos || The many <code>*.mp4</code> files (Wall Street footage, industry b-roll, etc.)
| Background videos || the many <code>*.mp4</code> (Wall Street footage, industry b-roll, )
|-
|-
| Event images || <code>blackswan.png</code>, <code>ponzi.jpg</code>, <code>soupline.jpg</code>, <code>zimbabwe.jpg</code>, <code>helicopter.jpg</code>, <code>yacht.mp4</code>
| Event images || <code>blackswan.png</code>, <code>ponzi.jpg</code>, <code>soupline.jpg</code>, <code>zimbabwe.jpg</code>, <code>helicopter.jpg</code>, <code>yacht.mp4</code>
Line 142: Line 144:
|}
|}


Replace any of these with a same-named file of the same type to reskin the game. (Keep filenames identical - components reference them by name.)
Replace any of these with a same-named file of the same type. '''Keep filenames identical''' - components reference them by name.
 
=== Help content - <code>resources/app/assets/help/</code> ===
 
The in-game manual is '''<code>wsrbook.htm</code>''' - a single ~1 MB HTML file. It is plain HTML; edit it directly to rewrite, correct, or extend the manual. The <code>*.jpg</code> files in the same folder are diagrams embedded by the manual (<code>advanopt.jpg</code>, <code>alerts.jpg</code>, <code>automate.jpg</code>, <code>buyloans.jpg</code>, <code>capcont.jpg</code>, <code>treasury.jpg</code>, <code>rlogotxt.jpg</code>).
 
<code>electron/UI_HELP_CATALOG.txt</code> is a developer reference catalog of help sections - useful to read when navigating the manual.


=== Translations / locales - <code>resources/app/js/locale/</code> ===
== Help content (<code>assets/help/</code>) ==


WSR ships an English base plus full translation dictionaries. You can add a new language with '''no code changes to the engine''' - it is pure data.
<code>wsrbook.htm</code> is the in-game manual - one ~1 MB plain HTML file. Edit it directly. The <code>*.jpg</code> files in the same folder are diagrams it embeds. <code>electron/UI_HELP_CATALOG.txt</code> is a developer catalog of help sections.


How it works (<code>js/locale/localeManager.js</code>):
== Translations (<code>js/locale/</code>) ==


* <code>LANGUAGE_OPTIONS</code> maps a locale code → <code>{ name, dictionary, warning? }</code>.
Pure data - '''no engine changes needed to add a language.'''
* Each <code>dictionary</code> is a flat object: <code>{ "English source string": "translated string", … }</code>.
* A <code>translator</code> does runtime lookup/replacement on UI strings.
* Existing locale files: <code>zh-CN.js</code>, <code>es-419.js</code>, <code>ja-JP.js</code>, <code>pt-BR.js</code>, <code>ru-RU.js</code>.


To add or improve a language:
How it works (<code>localeManager.js</code>):
* <code>LANGUAGE_OPTIONS</code> maps locale code → <code>{ name, dictionary, warning? }</code>.
* Each <code>dictionary</code> is a flat <code>{ "English source": "translation", … }</code>.
* The <code>translator</code> does runtime lookup/replacement on UI strings.
* Existing locales: <code>zh-CN.js</code>, <code>es-419.js</code>, <code>ja-JP.js</code>, <code>pt-BR.js</code>, <code>ru-RU.js</code>.


# Create <code>js/locale/&lt;your-locale&gt;.js</code> exporting a dictionary object, e.g.
To add a language:


<pre>
<pre>
// js/locale/fr-FR.js
export const frFR = {
export const frFR = {
     "New Game": "Nouvelle partie",
     "New Game": "Nouvelle partie",
Line 171: Line 169:
     // … one entry per source string you want translated
     // … one entry per source string you want translated
};
};
</pre>


# In <code>js/locale/localeManager.js</code>, import it and add an entry to <code>LANGUAGE_OPTIONS</code>:
// js/locale/localeManager.js
 
<pre>
import { frFR } from './fr-FR.js';
import { frFR } from './fr-FR.js';
// …
// …
'fr-FR': { name: 'Français', dictionary: frFR },
'fr-FR': { name: 'Français', dictionary: frFR },
</pre>
</pre>
# The chosen locale is persisted by the engine (in <code>CFIG.WSR</code>) and exposed as <code>gameState.locale</code>; the UI switches via the <code>/set_locale</code> endpoint.


{{Note|Strings missing from the dictionary simply fall through to English, so a partial translation is perfectly valid.
The chosen locale persists in <code>CFIG.WSR</code>, surfaces as <code>gameState.locale</code>, and switches via <code>POST /set_locale</code>.


The <code>*-backup-*</code> files in the locale folder are timestamped editing backups and are git-ignored - ignore them.}}
{{Note|Missing strings fall through to English, so a partial translation is fine. The <code>*-backup-*</code> files are timestamped editing backups (git-ignored) - ignore them.}}


=== In-game hints - <code>resources/app/js/data/hints/</code> ===
== Contextual hints (<code>js/data/hints/</code>) ==


The contextual hint system is data-driven. Hint files are grouped by view:
Hint files are grouped by view and aggregated by <code>index.js</code>:


<pre>
<pre>
js/data/hints/
hints/
├── index.js       # aggregates all hint sets
├── index.js         # aggregates all sets
├── global.js       # fallback hints
├── global.js         # fallback hints
├── company.js     # hints while viewing a company
├── company.js       # while viewing a company
├── industry.js
├── industry.js
├── market.js
├── market.js
Line 203: Line 197:
</pre>
</pre>


Each file exports an array of hint objects:
Each entry:


<pre>
<pre>
Line 214: Line 208:
</pre>
</pre>


The matcher (<code>js/services/hintMatcher.js</code>) picks the most specific matching hint for the current screen. You can rewrite hint text, add new hints, or retarget <code>match</code> conditions - all without touching engine code.
<code>js/services/hintMatcher.js</code> picks the most specific match for the current screen. Rewrite text, add new hints, retarget <code>match</code> conditions - no engine changes.


=== Company names - <code>CORPNAME.DAT</code> (plain text, directly editable) ===
== Company names (<code>CORPNAME.DAT</code>) ==


<code>resources/app/CORPNAME.DAT</code> is the company-name database, and unlike most <code>.DAT</code> files it is '''plain pipe-delimited text''':
Plain pipe-delimited text:


<pre>
<pre>
Line 226: Line 220:
</pre>
</pre>


Columns: <code>company ID | nation code | ticker symbol | company name</code>.
Columns: <code>company ID | nation code | ticker | name</code>. Read by the engine's <code>GetCorpNames</code> at '''new-game''' time, so edits take effect on the '''next''' new game (not on already-saved games). Rename companies, change tickers, change a company's home nation.
 
It is read by the engine's <code>GetCorpNames</code> routine at '''new-game''' time, so edits take effect on the next new game (not on already-saved games). You can rename companies, change ticker symbols, or change a company's home nation.
 
Keep the format exact: 4-digit zero-padded ID, 2-digit nation code, the fixed-width symbol field, and the surrounding <code>|</code> delimiters and spacing. <code>CORPNAME.ORI</code> is the pristine original - keep it as your reference/backup.


=== Other game-data files (limited / not practical) ===
'''Keep the format exact:''' 4-digit zero-padded ID, 2-digit nation code, fixed-width symbol field, and the surrounding <code>|</code> + spacing. <code>CORPNAME.ORI</code> is the pristine original - keep as your backup.


These ship in <code>resources/app/</code> but are '''not practically moddable''' without the original tooling:
== Other game data (don't bother) ==


{| class="wikitable"
{| class="wikitable"
! File(s) !! Content !! Why limited
! File(s) !! Content !! Why limited
|-
|-
| <code>QUOTEWSR.DAT</code> || "Quote of the day" text || Encrypted/encoded; plaintext source is <code>quotehs.prm</code> but the engine reads the encoded <code>.DAT</code>
| <code>QUOTEWSR.DAT</code> || quote of the day || encoded; <code>quotehs.prm</code> is a near-plaintext source the engine doesn't read
|-
|-
| <code>MSCNEWS.DAT</code> || News & scenario headline text (hundreds of paragraphs) || Encoded; <code>MSCNEWS.PRM</code> is a near-plaintext source but the engine reads the encoded <code>.DAT</code>
| <code>MSCNEWS.DAT</code> || news / scenario headlines || encoded; same story with <code>MSCNEWS.PRM</code>
|-
|-
| <code>scenwin1-4.prm</code> || Scenario window text || Engine-internal format
| <code>scenwin1-4.prm</code> || scenario window text || engine-internal format
|-
|-
| <code>GAME01.DAT … GAME50.DAT</code> || Save slots / scenario data || Proprietary binary save format
| <code>GAME01-50.DAT</code> || save slots / scenario data || proprietary binary
|-
|-
| <code>WSR101.DAT</code>, <code>REGINFO.DAT</code>, etc. || Engine data || Proprietary binary
| <code>WSR101.DAT</code>, <code>REGINFO.DAT</code>, || engine data || proprietary binary
|}
|}


Treat these as read-only. (Company names are the one genuinely editable game-data file - see [[#Company names - CORPNAME.DAT (plain text, directly editable)|above]].)
Treat as read-only. <code>CORPNAME.DAT</code> is the one truly editable game-data file.


== The runtime: how the UI talks to the engine ==
== Calling the API from a frontend mod ==


When <code>wsr.exe</code> starts, <code>ui.dll</code> binds a '''REST server''' and a '''WebSocket server''' on OS-assigned ephemeral ports, and writes them to:
<code>js/api.js</code> is the '''wrapper layer''' for the REST API: each endpoint has a named async function with semantic parameters, plus the live <code>gameStore</code>, plus exported '''ID constants''' (entity IDs, industry indices, special asset IDs, <code>UI_*</code> report IDs). '''Frontend mods should call <code>api.js</code> functions, not raw HTTP.'''


<pre>
<pre>
%LOCALAPPDATA%\Wall Street Raider\runtime.json
import * as api from '../api.js';
{ "pid": 12345, "rest_port": 54321, "ws_port": 54322 }
 
await api.buyStock(companyId);                    // /buy_stock
await api.setDividend(actingAsId);                // /set_dividend
await api.merger(targetId, actingAsId);          // /merger
await api.cheatAddCash();                        // /cheat_add_cash
const state = await api.getGameState();          // GET /gamestate
</pre>
</pre>


* Electron's main process reads that file and hands the ports to the renderer via the <code>bridge-ports</code> IPC message.
For endpoints that don't have a named wrapper yet, four request shapes are also exported:
* <code>js/api.js</code> builds <code>http://127.0.0.1:&lt;rest_port&gt;</code> and <code>ws://127.0.0.1:&lt;ws_port&gt;</code> from it.
 
* The '''WebSocket''' pushes game-state updates (full snapshot on connect, then JSON-patch diffs on <code>/game_state_patch</code>). When WS is connected, HTTP polling is suspended; if WS drops, <code>app.js</code> falls back to polling <code>GET /gamestate</code> (200 ms on the menu, 50 ms in-game).
{| class="wikitable"
! Helper !! Body shape !! Use
|-
| <code>postNoArg(path)</code> || <code>{}</code> || actions with no parameter
|-
| <code>postIdArg(path, id)</code> || <code>{ id }</code> || actions on a specific entity / slot
|-
| <code>postIdArgWithActingAs(path, id, actingAsId)</code> || <code>{ id, intParam2 }</code> || most corporate / trade actions
|-
| <code>postOptionsTradeWithActingAs(path, id, actingAsId, underlyingId)</code> || <code>{ id, intParam2, underlyingId }</code> || options trades
|-
| <code>postStringArg(path, str)</code> || <code>{ str }</code> || actions taking a string (e.g. save-as filename)
|-
| <code>getJSON(path)</code> || - || GET endpoints
|}
 
For wire-level details (the underlying routes, the JSON body the bridge actually receives, what each generic field means per endpoint), see [[#REST API reference|§REST API reference]].
 
== REST API reference ==
 
For third-party apps and direct integrations. Frontend mods should prefer <code>api.js</code> ([[#Calling the API from a frontend mod|§api.js]]).
 
=== Conventions ===
 
* '''Base URL:''' <code>http://127.0.0.1:&lt;rest_port&gt;</code>. Read <code>rest_port</code> from <code>%LOCALAPPDATA%\Wall Street Raider\runtime.json</code>; ports change every launch.
* '''Content-Type:''' <code>application/json</code> on all POSTs.
* '''Generic body schema:''' the bridge accepts a single envelope <code>{ id?, intParam2?, underlyingId?, str?, answer?, value?, filename? }</code>. Each endpoint uses 0-3 of these fields with '''endpoint-specific meanings''' - the per-endpoint tables below spell out which fields are read and what they mean in context.
* '''Omitted fields default to 0 / empty.''' Most action endpoints can be called as <code>{}</code> if they don't strictly need params (the engine often falls back to "use the currently active entity" or "show the picker modal").
* '''Most actions open a modal''' rather than executing immediately. After POSTing, the engine pushes a modal state via WS / <code>gameState.modalType</code>; respond with <code>POST /modal_result</code>.
 
==== The <code>actingAsId</code> pattern ====
 
Most corporate / trade endpoints accept an '''<code>actingAsId</code>''' (sent as the JSON field <code>intParam2</code>): the entity that '''executes''' the action.
 
* <code>0</code> (or omitted) = act as the player.
* Any other ID = act as that controlled company. The engine momentarily switches context, runs the action ''as that company'', then restores.
 
When the per-endpoint tables below show <code>intParam2</code>, it means actingAsId unless noted otherwise.
 
==== Response shape ====
 
GET endpoints return endpoint-specific JSON (documented in the GET table). POST endpoints return <code>{ status: "ok" }</code> on success and a <code>{ error: "…" }</code> with HTTP 4xx/5xx on failure. Side effects show up via the next WS broadcast.
 
=== GET endpoints ===
 
{| class="wikitable"
! Method !! Endpoint !! Returns
|-
| GET || <code>/status</code> || health check
|-
| GET || <code>/gamestate</code> || full game-state JSON ([[#gameState reference|§gameState]])
|-
| GET || <code>/quote</code> || <code>{ quote: string }</code> - quote of the day
|-
| POST || <code>/asset_chart</code> || body <code>{ id }</code>; returns 60 months of price history for an asset (<code>{ prices, highs, lows, xAxisTitle, yAxisTitle, baseMonth, baseYear }</code>)
|-
| GET || <code>/database_data</code> || searchable database of all companies (industry, financials, ratings, …)
|-
| GET || <code>/ownership_tree</code> || ownership hierarchy of the active entity (who owns it, at what %)
|-
| GET || <code>/subsidiaries_tree</code> || what the active entity owns (its subsidiaries, at what %)
|}
 
=== Session / clock ===


For a mod, you almost never touch this plumbing directly - you call the functions in <code>api.js</code> and read from the <code>gameStore</code>.
{| class="wikitable"
! Endpoint !! Body !! Description
|-
| POST <code>/newgame</code> || <code>{}</code> || Start a new game.
|-
| POST <code>/loadgame</code> || <code>{}</code> || Open the load-game flow.
|-
| POST <code>/load_specific_save</code> || <code>{ filename }</code> || Load a specific save. <code>filename</code> = save basename; must not contain <code>..</code>, <code>/</code>, <code>\</code>, <code>:</code>.
|-
| POST <code>/savegame</code> || <code>{}</code> || Save to the default slot.
|-
| POST <code>/savegameas</code> || <code>{ filename }</code> || Save under a name. Empty <code>filename</code> shows the Save-As dialog.
|-
| POST <code>/exit_game</code> || <code>{}</code> || Exit to main menu.
|-
| POST <code>/check_scoreboard</code> || <code>{}</code> || Open the scoreboard view.
|-
| POST <code>/start_ticker</code> || <code>{}</code> || Start the market ticker animations.
|-
| POST <code>/run_ticker</code> || <code>{}</code> || Advance the ticker by one queue item.
|-
| POST <code>/stop_ticker</code> || <code>{}</code> || Stop the ticker.
|-
| POST <code>/set_ticker_speed</code> || <code>{ id }</code> || Set ticker speed. <code>id</code> = speed enum.
|-
| POST <code>/ticker_advance</code> || <code>{}</code> || Pop the front of the ticker queue, append a new item.
|-
| POST <code>/clear_event_string</code> || <code>{}</code> || Clear the event message log.
|-
| POST <code>/splash_screen_played</code> || <code>{}</code> || Mark splash as shown so it's skipped next time.
|}


== The API - everything you can make the game do ==
=== Trading - stocks & bonds ===


All state-changing actions go through a fixed set of named REST endpoints, each of which the bridge turns into a typed engine event. '''There is no generic "run arbitrary command" endpoint''' - you can only do what an endpoint exists for. The good news: there are ~150 of them, covering every player action in the game.
{| class="wikitable"
! Endpoint !! Body !! Description
|-
| POST <code>/buy_stock</code> || <code>{ id, intParam2? }</code> || Buy stock. <code>id</code> = company to buy. <code>intParam2</code> = actingAsId.
|-
| POST <code>/sell_stock</code> || <code>{ id, intParam2? }</code> || Sell stock. <code>id</code> = company to sell. <code>intParam2</code> = actingAsId.
|-
| POST <code>/short_stock</code> || <code>{ id, intParam2? }</code> || Open short. <code>id</code> = company to short. <code>intParam2</code> = actingAsId.
|-
| POST <code>/cover_short_stock</code> || <code>{ id, intParam2? }</code> || Cover an existing short. <code>id</code> = company. <code>intParam2</code> = actingAsId.
|-
| POST <code>/buy_corporate_bond</code> || <code>{ id, intParam2? }</code> || Buy a corporate bond. <code>id</code> = issuing company. <code>intParam2</code> = actingAsId.
|-
| POST <code>/sell_corporate_bond</code> || <code>{ id, intParam2? }</code> || Sell a corporate bond. <code>id</code> = issuing company. <code>intParam2</code> = actingAsId.
|-
| POST <code>/buy_long_govt_bonds</code> || <code>{ intParam2 }</code> || Buy long-term govt bonds. <code>intParam2</code> = actingAsId. <code>id</code> ignored.
|-
| POST <code>/sell_long_govt_bonds</code> || <code>{ intParam2 }</code> || Sell long-term govt bonds. <code>intParam2</code> = actingAsId.
|-
| POST <code>/buy_short_govt_bonds</code> || <code>{ intParam2 }</code> || Buy short-term govt bonds. <code>intParam2</code> = actingAsId.
|-
| POST <code>/sell_short_govt_bonds</code> || <code>{ intParam2 }</code> || Sell short-term govt bonds. <code>intParam2</code> = actingAsId.
|}


=== Calling the API from a mod ===
=== Trading - commodities & crypto ===


<code>js/api.js</code> already wraps every endpoint in a named async function. Import and call:
{| class="wikitable"
! Endpoint !! Body !! Description
|-
| POST <code>/buy_commodity_futures</code> || <code>{ id, intParam2? }</code> || Buy commodity futures. <code>id</code> = commodity ID (e.g. <code>OIL_ID</code>, <code>GOLD_ID</code>). <code>intParam2</code> = actingAsId.
|-
| POST <code>/sell_commodity_futures</code> || <code>{ id, intParam2? }</code> || Sell commodity futures. <code>id</code> = commodity ID. <code>intParam2</code> = actingAsId.
|-
| POST <code>/close_long_commodity_futures_by_slot</code> || <code>{ id, intParam2? }</code> || Close a specific long position. <code>id</code> = '''portfolio slot index''' (not a commodity ID). <code>intParam2</code> = actingAsId.
|-
| POST <code>/short_commodity_futures</code> || <code>{ id, intParam2? }</code> || Short commodity futures. <code>id</code> = commodity ID. <code>intParam2</code> = actingAsId.
|-
| POST <code>/cover_short_commodity_futures</code> || <code>{ id, intParam2? }</code> || Cover commodity short. <code>id</code> = commodity ID. <code>intParam2</code> = actingAsId.
|-
| POST <code>/cover_short_commodity_futures_by_slot</code> || <code>{ id, intParam2? }</code> || Cover a specific short position. <code>id</code> = '''portfolio slot index'''. <code>intParam2</code> = actingAsId.
|-
| POST <code>/buy_physical_commodity</code> || <code>{ id, intParam2? }</code> || Buy physical commodity. <code>id</code> = commodity ID. <code>intParam2</code> = actingAsId.
|-
| POST <code>/sell_physical_commodity</code> || <code>{ id, intParam2? }</code> || Sell physical commodity. <code>id</code> = commodity ID. <code>intParam2</code> = actingAsId.
|-
| POST <code>/buy_physical_crypto</code> || <code>{ id, intParam2? }</code> || Buy physical crypto. <code>id</code> = crypto ID (e.g. <code>BITCOIN_ID</code>). <code>intParam2</code> = actingAsId.
|-
| POST <code>/sell_physical_crypto</code> || <code>{ id }</code> || Sell physical crypto. <code>id</code> = crypto ID. (No actingAs param on this one.)
|-
| POST <code>/buy_crypto_futures</code> || <code>{ id, intParam2? }</code> || Buy crypto futures. <code>id</code> = crypto ID. <code>intParam2</code> = actingAsId.
|-
| POST <code>/sell_crypto_futures</code> || <code>{ id, intParam2? }</code> || Sell crypto futures. <code>id</code> = crypto ID. <code>intParam2</code> = actingAsId.
|}


<pre>
=== Trading - options ===
import * as api from '../api.js';
 
{| class="wikitable"
! Endpoint !! Body !! Description
|-
| POST <code>/buy_calls</code> || <code>{ id, intParam2?, underlyingId? }</code> || Buy calls. <code>id</code> = strike-price ID (or 0 to show the strike picker). <code>intParam2</code> = actingAsId. <code>underlyingId</code> = company being optioned (when <code>id&gt;0</code> and <code>underlyingId&gt;0</code>, modal is skipped — used by CLI flows like "CALL ABC").
|-
| POST <code>/sell_calls</code> || <code>{ id, intParam2?, underlyingId? }</code> || Sell calls. Same param semantics as <code>/buy_calls</code>.
|-
| POST <code>/buy_puts</code> || <code>{ id, intParam2?, underlyingId? }</code> || Buy puts. Same param semantics as <code>/buy_calls</code>.
|-
| POST <code>/sell_puts</code> || <code>{ id, intParam2?, underlyingId? }</code> || Sell puts. Same param semantics as <code>/buy_calls</code>.
|-
| POST <code>/advanced_options_trading</code> || <code>{ intParam2 }</code> || Open the advanced options panel (spreads, collars). <code>intParam2</code> = actingAsId.
|-
| POST <code>/exercise_call_options_early</code> || <code>{ id, intParam2? }</code> || Exercise a call early. <code>id</code> = call contract ID. <code>intParam2</code> = actingAsId.
|-
| POST <code>/exercise_put_options_early</code> || <code>{ id, intParam2? }</code> || Exercise a put early. <code>id</code> = put contract ID. <code>intParam2</code> = actingAsId.
|}


await api.buyStock(companyId);            // buy
=== Corporate management ===
await api.setDividend(actingAsId);        // open the set-dividend flow
await api.merger(targetId, actingAsId);  // start a merger
await api.cheatAddCash();                // cheat menu: add cash
const state = await api.getGameState();  // full state snapshot
</pre>


Under the hood there are four request shapes (also exported, if you need to hit an endpoint that has no named wrapper yet):
All of these operate on the acting-as entity. <code>id</code> is unused unless noted; pass <code>0</code> or omit.


{| class="wikitable"
{| class="wikitable"
! Helper !! Body sent !! Use
! Endpoint !! Body !! Description
|-
| POST <code>/prepay_taxes</code> || <code>{ intParam2 }</code> || Prepay taxes. <code>intParam2</code> = actingAsId.
|-
| POST <code>/elect_ceo</code> || <code>{ intParam2 }</code> || Open CEO-election picker for the entity. <code>intParam2</code> = actingAsId.
|-
| POST <code>/resign_as_ceo</code> || <code>{ intParam2 }</code> || Acting-as entity resigns as CEO. <code>intParam2</code> = actingAsId.
|-
| POST <code>/change_managers</code> || <code>{ intParam2 }</code> || Hire / fire managers. <code>intParam2</code> = actingAsId.
|-
| POST <code>/set_dividend</code> || <code>{ intParam2 }</code> || Set the company's dividend. <code>intParam2</code> = actingAsId (dividend issuer).
|-
| POST <code>/set_productivity</code> || <code>{ intParam2 }</code> || Set productivity. <code>intParam2</code> = actingAsId.
|-
| POST <code>/set_growth_rate</code> || <code>{ intParam2 }</code> || Set growth rate. <code>intParam2</code> = actingAsId.
|-
| POST <code>/restructure</code> || <code>{ intParam2 }</code> || Open restructuring modal. <code>intParam2</code> = actingAsId.
|-
| POST <code>/buy_corporate_assets</code> || <code>{ intParam2 }</code> || Buy plant / equipment. <code>intParam2</code> = actingAsId.
|-
| POST <code>/sell_corporate_assets</code> || <code>{ intParam2 }</code> || Sell plant / equipment. <code>intParam2</code> = actingAsId.
|-
| POST <code>/offer_corporate_assets_for_sale</code> || <code>{ intParam2 }</code> || List assets for other entities to buy. <code>intParam2</code> = actingAsId.
|-
| POST <code>/view_for_sale_items</code> || <code>{ intParam2 }</code> || Browse assets currently for sale. <code>intParam2</code> = actingAsId.
|-
| POST <code>/sell_subsidiary_stock</code> || <code>{ id, intParam2? }</code> || Sell stock the entity holds in a subsidiary. <code>id</code> = subsidiary company ID. <code>intParam2</code> = actingAsId (seller).
|-
| POST <code>/rebrand</code> || <code>{ intParam2 }</code> || Rename the company. <code>intParam2</code> = actingAsId.
|-
|-
| <code>postNoArg(path)</code> || <code>{}</code> || actions with no parameter
| POST <code>/toggle_company_autopilot</code> || <code>{ id }</code> || Toggle one company's autopilot. <code>id</code> = company.
|-
|-
| <code>postIdArg(path, id)</code> || <code>{ id }</code> || actions on a specific entity/slot
| POST <code>/toggle_global_autopilot</code> || <code>{ intParam2 }</code> || Toggle autopilot across all of the entity's holdings. <code>intParam2</code> = actingAsId.
|-
|-
| <code>postIdArgWithActingAs(path, id, actingAsId)</code> || <code>{ id, intParam2? }</code> || most corporate/trade actions; <code>actingAsId</code> runs the action as a company you control
| POST <code>/become_etf_advisor</code> || <code>{ intParam2 }</code> || Become ETF advisor. <code>intParam2</code> = actingAsId.
|-
|-
| <code>postOptionsTradeWithActingAs(path, id, actingAsId, underlyingId)</code> || <code>{ id, intParam2?, underlyingId? }</code> || options trades
| POST <code>/set_advisory_fee</code> || <code>{ intParam2 }</code> || Set the ETF advisory fee. <code>intParam2</code> = actingAsId.
|-
|-
| <code>postStringArg(path, str)</code> || <code>{ str }</code> || actions taking a string (e.g. save-as filename)
| POST <code>/decrease_earnings</code> || <code>{ intParam2 }</code> || Accounting adjustment to lower reported earnings. <code>intParam2</code> = actingAsId.
|-
|-
| <code>getJSON(path)</code> || - || <code>GET</code> endpoints
| POST <code>/increase_earnings</code> || <code>{ intParam2 }</code> || Accounting adjustment to raise reported earnings. <code>intParam2</code> = actingAsId.
|}
|}


The <code>actingAsId</code> parameter is important: passing the ID of a company you control makes the engine execute the action ''as that company'' (e.g. have a subsidiary buy stock), then restore. <code>0</code> means "as the player."
=== M&A / corporate finance ===


=== Endpoint reference ===
{| class="wikitable"
! Endpoint !! Body !! Description
|-
| POST <code>/merger</code> || <code>{ id, intParam2? }</code> || Start a merger. <code>id</code> = target company. <code>intParam2</code> = actingAsId (acquirer).
|-
| POST <code>/greenmail</code> || <code>{ id, intParam2? }</code> || Greenmail (coercive buyback). <code>id</code> = target. <code>intParam2</code> = actingAsId.
|-
| POST <code>/lbo</code> || <code>{ id, intParam2? }</code> || Leveraged buyout. <code>id</code> = target. <code>intParam2</code> = actingAsId.
|-
| POST <code>/startup</code> || <code>{ intParam2 }</code> || Found a new startup. <code>intParam2</code> = actingAsId (founder).
|-
| POST <code>/capital_contribution</code> || <code>{ intParam2 }</code> || Inject cash into a subsidiary. <code>intParam2</code> = actingAsId.
|-
| POST <code>/public_stock_offering</code> || <code>{ intParam2 }</code> || IPO. <code>intParam2</code> = actingAsId (company going public).
|-
| POST <code>/private_stock_offering</code> || <code>{ intParam2 }</code> || Private placement. <code>intParam2</code> = actingAsId.
|-
| POST <code>/issue_new_corp_bonds</code> || <code>{ intParam2 }</code> || Issue new corporate bonds. <code>intParam2</code> = actingAsId (issuer).
|-
| POST <code>/redeem_corp_bonds</code> || <code>{ intParam2 }</code> || Retire outstanding bonds. <code>intParam2</code> = actingAsId.
|-
| POST <code>/extraordinary_dividend</code> || <code>{ intParam2 }</code> || One-time special dividend. <code>intParam2</code> = actingAsId.
|-
| POST <code>/tax_free_liquidation</code> || <code>{ intParam2 }</code> || Liquidate with tax deferral. <code>intParam2</code> = actingAsId.
|-
| POST <code>/taxable_liquidation</code> || <code>{ intParam2 }</code> || Liquidate; pay tax on gains. <code>intParam2</code> = actingAsId.
|-
| POST <code>/spin_off</code> || <code>{ id, intParam2? }</code> || Spin off a subsidiary. <code>id</code> = subsidiary to spin off. <code>intParam2</code> = actingAsId (parent).
|-
| POST <code>/split_stock</code> || <code>{ intParam2 }</code> || Forward stock split. <code>intParam2</code> = actingAsId.
|-
| POST <code>/reverse_split_stock</code> || <code>{ intParam2 }</code> || Reverse stock split. <code>intParam2</code> = actingAsId.
|}


'''Read (GET):'''
=== Banking / loans / swaps ===


{| class="wikitable"
{| class="wikitable"
! Endpoint !! Returns
! Endpoint !! Body !! Description
|-
| POST <code>/borrow_money</code> || <code>{ intParam2 }</code> || Take out a loan. <code>intParam2</code> = actingAsId (borrower).
|-
| POST <code>/repay_loan</code> || <code>{ intParam2 }</code> || Pay down / repay. <code>intParam2</code> = actingAsId.
|-
| POST <code>/advance_funds</code> || <code>{ intParam2 }</code> || Lender advances cash against securities. <code>intParam2</code> = actingAsId (lender).
|-
|-
| <code>/status</code> || health check
| POST <code>/call_in_advance</code> || <code>{ id }</code> || Terminate an advance. <code>id</code> = advance ID.
|-
|-
| <code>/gamestate</code> || full game-state JSON (see [[#The gameState - everything you can read|§6]])
| POST <code>/interest_rate_swaps</code> || <code>{ id, intParam2? }</code> || Open the swaps modal. <code>id</code> = asset ID. <code>intParam2</code> = actingAsId.
|-
|-
| <code>/quote</code> || quote of the day
| POST <code>/view_swap_details</code> || <code>{ id, intParam2? }</code> || View one swap's details. <code>id</code> = swap ID. <code>intParam2</code> = actingAsId.
|-
|-
| <code>/asset_chart</code> || price history for an asset
| POST <code>/terminate_swap</code> || <code>{ id, intParam2? }</code> || Terminate a swap early. <code>id</code> = swap ID. <code>intParam2</code> = actingAsId.
|-
|-
| <code>/database_data</code> || database/search dataset
| POST <code>/set_bank_allocation</code> || <code>{ intParam2 }</code> || Distribute cash across banks. <code>intParam2</code> = actingAsId.
|-
|-
| <code>/ownership_tree</code> || corporate ownership tree
| POST <code>/trade_tbills</code> || <code>{ intParam2 }</code> || Trade T-bills. <code>intParam2</code> = actingAsId.
|-
|-
| <code>/subsidiaries_tree</code> || subsidiary tree
| POST <code>/list_bank_loans</code> || <code>{ intParam2 }</code> || List the entity's outstanding bank loans (when acting as a bank). <code>intParam2</code> = actingAsId.
|-
| POST <code>/change_bank</code> || <code>{ intParam2 }</code> || Change primary bank. <code>intParam2</code> = actingAsId.
|-
| POST <code>/call_in_loan</code> || <code>{ id }</code> || Demand immediate repayment. <code>id</code> = loan ID.
|-
| POST <code>/buy_bank_loans</code> || <code>{}</code> || Open the bank-loans investment picker.
|-
| POST <code>/buy_business_loans</code> || <code>{ intParam2 }</code> || Buy business-loan portfolio. <code>intParam2</code> = actingAsId (investor).
|-
| POST <code>/sell_business_loan</code> || <code>{ id }</code> || Sell one business-loan holding. <code>id</code> = loan ID.
|-
| POST <code>/buy_consumer_loans</code> || <code>{ intParam2 }</code> || Buy consumer-loan portfolio. <code>intParam2</code> = actingAsId.
|-
| POST <code>/sell_consumer_loans</code> || <code>{ intParam2 }</code> || Sell consumer-loan holdings. <code>intParam2</code> = actingAsId.
|-
| POST <code>/buy_prime_mortgages</code> || <code>{ intParam2 }</code> || Buy prime mortgages. <code>intParam2</code> = actingAsId.
|-
| POST <code>/sell_prime_mortgages</code> || <code>{ intParam2 }</code> || Sell prime mortgages. <code>intParam2</code> = actingAsId.
|-
| POST <code>/buy_subprime_mortgages</code> || <code>{ intParam2 }</code> || Buy subprime mortgages. <code>intParam2</code> = actingAsId.
|-
| POST <code>/sell_subprime_mortgages</code> || <code>{ intParam2 }</code> || Sell subprime mortgages. <code>intParam2</code> = actingAsId.
|-
| POST <code>/list_etfs</code> || <code>{}</code> || Show available ETFs.
|-
| POST <code>/freeze_all_loans</code> || <code>{ intParam2 }</code> || Freeze all loan repayments across the lender's portfolio. <code>intParam2</code> = actingAsId.
|-
| POST <code>/freeze_loan</code> || <code>{ id }</code> || Freeze one specific loan. <code>id</code> = loan ID.
|}
|}


'''Game / session control:''' <code>/newgame</code>, <code>/loadgame</code>, <code>/load_specific_save</code> (str), <code>/savegame</code>, <code>/savegameas</code> (str), <code>/exit_game</code>, <code>/check_scoreboard</code>, <code>/start_ticker</code>, <code>/run_ticker</code>, <code>/stop_ticker</code>, <code>/set_ticker_speed</code> (id), <code>/ticker_advance</code>, <code>/clear_event_string</code>, <code>/splash_screen_played</code>.
=== Legal / dirty tricks ===


'''Trading - stocks & bonds:''' <code>/buy_stock</code>, <code>/sell_stock</code>, <code>/short_stock</code>, <code>/cover_short_stock</code>, <code>/buy_corporate_bond</code>, <code>/sell_corporate_bond</code>, <code>/buy_long_govt_bonds</code>, <code>/sell_long_govt_bonds</code>, <code>/buy_short_govt_bonds</code>, <code>/sell_short_govt_bonds</code>.
{| class="wikitable"
! Endpoint !! Body !! Description
|-
| POST <code>/change_law_firm</code> || <code>{ intParam2 }</code> || Hire a law firm. <code>intParam2</code> = actingAsId.
|-
| POST <code>/credit_info</code> || <code>{ intParam2 }</code> || Show credit info (rating, history). <code>intParam2</code> = actingAsId.
|-
| POST <code>/antitrust_lawsuit</code> || <code>{ id, intParam2? }</code> || Sue for antitrust. <code>id</code> = target company. <code>intParam2</code> = actingAsId (plaintiff).
|-
| POST <code>/harrassing_lawsuit</code> || <code>{ id, intParam2? }</code> || File a frivolous lawsuit. <code>id</code> = target. <code>intParam2</code> = actingAsId.
|-
| POST <code>/spread_rumors</code> || <code>{ id, intParam2? }</code> || Spread rumors to damage stock / reputation. <code>id</code> = target company. <code>intParam2</code> = actingAsId.
|}


'''Trading - commodities & crypto:''' <code>/buy_commodity_futures</code>, <code>/sell_commodity_futures</code>, <code>/close_long_commodity_futures_by_slot</code>, <code>/short_commodity_futures</code>, <code>/cover_short_commodity_futures</code>, <code>/cover_short_commodity_futures_by_slot</code>, <code>/buy_physical_commodity</code>, <code>/sell_physical_commodity</code>, <code>/buy_physical_crypto</code>, <code>/sell_physical_crypto</code>, <code>/buy_crypto_futures</code>, <code>/sell_crypto_futures</code>.
=== Reports / views / navigation ===


'''Trading - options:''' <code>/buy_calls</code>, <code>/sell_calls</code>, <code>/buy_puts</code>, <code>/sell_puts</code>, <code>/advanced_options_trading</code>, <code>/exercise_call_options_early</code>, <code>/exercise_put_options_early</code>.
{| class="wikitable"
! Endpoint !! Body !! Description
|-
| POST <code>/set_active_ui_report</code> || <code>{ id }</code> || Switch active report panel. <code>id</code> = a <code>UI_*</code> enum (see <code>api.js</code> constants).
|-
| POST <code>/set_view_asset</code> || <code>{ id }</code> || Navigate to a company / player. <code>id</code> = entity ID. Updates nav history.
|-
| POST <code>/set_view_industry</code> || <code>{ id }</code> || Navigate to an industry overview. <code>id</code> = industry index, or <code>-2</code> = trigger DB Search.
|-
| POST <code>/database_search</code> || <code>{}</code> || Open the database search interface.
|-
| POST <code>/clear_chart</code> || <code>{}</code> || Clear the active price chart.
|-
| POST <code>/growth_throttle</code> || <code>{}</code> || Open growth-throttle settings.
|-
| POST <code>/clear_stream_list</code> || <code>{}</code> || Clear the streaming-quotes watchlist.
|-
| POST <code>/fill_stream_list</code> || <code>{}</code> || Populate the streaming-quotes watchlist with defaults.
|-
| POST <code>/toggle_streaming_quote</code> || <code>{ id }</code> || Toggle live updates for one asset. <code>id</code> = company / asset.
|-
| POST <code>/nav_back</code> || <code>{}</code> || Nav history: back.
|-
| POST <code>/nav_forward</code> || <code>{}</code> || Nav history: forward.
|-
| POST <code>/nav_clear</code> || <code>{}</code> || Clear nav history.
|-
| POST <code>/nav_goto</code> || <code>{ id }</code> || Jump to a nav-history index. <code>id</code> = position in history stack.
|-
| POST <code>/nav_set_history</code> || JSON array || Restore nav history from saved state. Body = array of <code>{ id, type }</code> where <code>type</code> is <code>"asset"</code> or <code>"industry"</code>, most-recent-first.
|-
| POST <code>/set_who_owns_filter</code> || <code>{ value }</code> || Set the "who owns what" filter. <code>value</code> = filter enum.
|-
| POST <code>/view_current_interest_rates</code> || <code>{}</code> || Show current interest rates.
|-
| POST <code>/whos_ahead</code> || <code>{}</code> || Show the leaderboard.
|-
| POST <code>/db_research_tool</code> || <code>{}</code> || Open the research database tool.
|-
| POST <code>/economic_stats</code> || <code>{}</code> || Show macro stats.
|-
| POST <code>/most_cash_report</code> || <code>{}</code> || Most-cash leaderboard.
|-
| POST <code>/largest_market_cap</code> || <code>{}</code> || Market-cap leaderboard.
|-
| POST <code>/largest_tax_losses</code> || <code>{}</code> || Largest carried-forward tax losses.
|-
| POST <code>/industry_summary</code> || <code>{}</code> || Industry summary stats.
|-
| POST <code>/industry_projections</code> || <code>{}</code> || Industry growth / decline projections.
|-
| POST <code>/view_corp_assets_for_sale</code> || <code>{}</code> || All corporate assets currently for sale.
|}
 
=== Settings / toggles ===


'''Corporate management:''' <code>/prepay_taxes</code>, <code>/elect_ceo</code>, <code>/resign_as_ceo</code>, <code>/change_managers</code>, <code>/set_dividend</code>, <code>/set_productivity</code>, <code>/set_growth_rate</code>, <code>/restructure</code>, <code>/buy_corporate_assets</code>, <code>/sell_corporate_assets</code>, <code>/offer_corporate_assets_for_sale</code>, <code>/view_for_sale_items</code>, <code>/sell_subsidiary_stock</code>, <code>/rebrand</code>, <code>/toggle_company_autopilot</code>, <code>/toggle_global_autopilot</code>, <code>/become_etf_advisor</code>, <code>/set_advisory_fee</code>, <code>/decrease_earnings</code>, <code>/increase_earnings</code>.
These all open a settings menu / cycle the toggle. No params except where noted.


'''M&A / corporate finance:''' <code>/merger</code>, <code>/greenmail</code>, <code>/lbo</code>, <code>/startup</code>, <code>/capital_contribution</code>, <code>/public_stock_offering</code>, <code>/private_stock_offering</code>, <code>/issue_new_corp_bonds</code>, <code>/redeem_corp_bonds</code>, <code>/extraordinary_dividend</code>, <code>/tax_free_liquidation</code>, <code>/taxable_liquidation</code>, <code>/spin_off</code>, <code>/split_stock</code>, <code>/reverse_split_stock</code>.
{| class="wikitable"
! Endpoint !! Body !! Description
|-
| POST <code>/supp_earn_select</code> || <code>{}</code> || Suppress earnings-warnings menu.
|-
| POST <code>/currency_select</code> || <code>{}</code> || Currency selection.
|-
| POST <code>/supp_warn_select</code> || <code>{}</code> || Suppress-warnings menu.
|-
| POST <code>/suppress_select</code> || <code>{}</code> || Suppress messages / alerts menu.
|-
| POST <code>/autosave_select</code> || <code>{}</code> || Autosave settings.
|-
| POST <code>/exercise_select</code> || <code>{}</code> || Auto-exercise options settings.
|-
| POST <code>/sweep_select</code> || <code>{}</code> || Cash-sweep settings.
|-
| POST <code>/makedelivery_select</code> || <code>{}</code> || Make-delivery settings (commodity/crypto).
|-
| POST <code>/takedelivery_select</code> || <code>{}</code> || Take-delivery settings.
|-
| POST <code>/tooltips_select</code> || <code>{}</code> || Tooltip enable / disable.
|-
| POST <code>/shareholdergraph_select</code> || <code>{}</code> || Shareholder-graph display settings.
|-
| POST <code>/disablehotkeys_select</code> || <code>{}</code> || Hotkey enable / disable.
|-
| POST <code>/autoadd_select</code> || <code>{}</code> || Auto-add-to-watchlist settings.
|-
| POST <code>/set_chart_type</code> || <code>{ id }</code> || Set chart style. <code>id</code> = chart type enum.
|-
| POST <code>/set_locale</code> || <code>{ str }</code> || Set UI locale. <code>str</code> = locale code (e.g. <code>"en-US"</code>, <code>"ja-JP"</code>).
|}


'''Banking / loans / swaps:''' <code>/borrow_money</code>, <code>/repay_loan</code>, <code>/advance_funds</code>, <code>/call_in_advance</code>, <code>/interest_rate_swaps</code>, <code>/view_swap_details</code>, <code>/terminate_swap</code>, <code>/set_bank_allocation</code>, <code>/trade_tbills</code>, <code>/list_bank_loans</code>, <code>/change_bank</code>, <code>/call_in_loan</code>, <code>/buy_business_loans</code>, <code>/sell_business_loan</code>, <code>/buy_consumer_loans</code>, <code>/sell_consumer_loans</code>, <code>/buy_prime_mortgages</code>, <code>/sell_prime_mortgages</code>, <code>/buy_subprime_mortgages</code>, <code>/sell_subprime_mortgages</code>, <code>/list_etfs</code>, <code>/freeze_all_loans</code>, <code>/freeze_loan</code>.
=== Cheat menu ===


'''Legal / dirty tricks:''' <code>/change_law_firm</code>, <code>/antitrust_lawsuit</code>, <code>/harrassing_lawsuit</code>, <code>/spread_rumors</code>.
{| class="wikitable"
! Endpoint !! Body !! Description
|-
| POST <code>/cheat_disable</code> || <code>{}</code> || Lock cheats off.
|-
| POST <code>/cheat_disable_lawsuits</code> || <code>{}</code> || Disable lawsuits entirely.
|-
| POST <code>/cheat_merger_info</code> || <code>{}</code> || Reveal hidden merger info.
|-
| POST <code>/cheat_earnings_info</code> || <code>{}</code> || Reveal earnings forecasts and hidden financials.
|-
| POST <code>/cheat_add_cash</code> || <code>{}</code> || Add a fixed amount of cash. (No "set cash to N" - this is fixed.)
|}


'''Reports / views / navigation:''' <code>/set_active_ui_report</code> (id), <code>/set_view_asset</code> (id), <code>/set_view_industry</code> (id), <code>/database_search</code>, <code>/clear_chart</code>, <code>/growth_throttle</code>, <code>/clear_stream_list</code>, <code>/fill_stream_list</code>, <code>/toggle_streaming_quote</code> (id), <code>/nav_back</code>, <code>/nav_forward</code>, <code>/nav_clear</code>, <code>/nav_goto</code> (id), plus the market-report triggers <code>/view_current_interest_rates</code>, <code>/whos_ahead</code>, <code>/db_research_tool</code>, <code>/economic_stats</code>, <code>/most_cash_report</code>, <code>/largest_market_cap</code>, <code>/largest_tax_losses</code>, <code>/industry_summary</code>, <code>/industry_projections</code>, <code>/view_corp_assets_for_sale</code>.
=== Modals ===


'''Settings (toggles):''' <code>/supp_earn_select</code>, <code>/currency_select</code>, <code>/supp_warn_select</code>, <code>/suppress_select</code>, <code>/autosave_select</code>, <code>/exercise_select</code>, <code>/sweep_select</code>, <code>/makedelivery_select</code>, <code>/takedelivery_select</code>, <code>/tooltips_select</code>, <code>/shareholdergraph_select</code>, <code>/disablehotkeys_select</code>, <code>/autoadd_select</code>, <code>/set_chart_type</code> (id), <code>/set_locale</code> (str).
{| class="wikitable"
! Endpoint !! Body !! Description
|-
| POST <code>/close_modal</code> || <code>{}</code> || Close the current modal.
|-
| POST <code>/modal_result</code> || <code>{ answer }</code> or <code>{ str }</code> || Submit the player's modal response. Use <code>answer</code> for numeric / choice modals; <code>str</code> for text-entry modals.
|}


'''Cheat menu:''' <code>/cheat_disable</code>, <code>/cheat_disable_lawsuits</code>, <code>/cheat_merger_info</code>, <code>/cheat_earnings_info</code>, <code>/cheat_add_cash</code>. (These are fixed-function - e.g. <code>/cheat_add_cash</code> adds a preset amount; there is no "set cash to N" endpoint.)
=== Tutorial ===


'''Modals:''' <code>/close_modal</code>, <code>/modal_result</code>.
{| class="wikitable"
! Endpoint !! Body !! Description
|-
| POST <code>/set_tutorial_step</code> || <code>{ id }</code> || Jump tutorial to a step. <code>id</code> = step number.
|-
| POST <code>/set_tutorial_enabled</code> || <code>{ id }</code> || Enable / disable tutorial. <code>id</code> = <code>1</code> on, <code>0</code> off.
|}


'''Tutorial:''' <code>/set_tutorial_step</code> (id), <code>/set_tutorial_enabled</code> (id).
=== Price alerts ===


'''Price alerts:''' <code>/create_price_alert</code>, <code>/show_price_alerts</code>, <code>/delete_price_alert</code>.
{| class="wikitable"
! Endpoint !! Body !! Description
|-
| POST <code>/show_price_alerts</code> || <code>{}</code> || Open the alerts management dialog.
|-
| POST <code>/create_price_alert</code> || <code>{ str }</code> || Create an alert. <code>str</code> = pipe-delimited <code>"entityId|direction|targetPrice"</code>; direction is <code>"up"</code> or <code>"down"</code>.
|-
| POST <code>/delete_price_alert</code> || <code>{ id }</code> || Delete one alert. <code>id</code> = alert slot index.
|}


'''CustomData:''' <code>/set_custom_data</code> - see [[#The CustomData API - your persistent storage|§7]].
=== CustomData ===


{{Note|<code>js/api.js</code> also exports useful '''ID constants''' so you don't hardcode magic numbers: entity IDs (<code>HUMAN1_ID</code>, <code>COMPUTER1_ID</code>, …), industry indices (<code>BANK_IND</code>, <code>INSURANCE_IND</code>, <code>ETF_IND</code>, …), special asset IDs (<code>OIL_ID</code>, <code>GOLD_ID</code>, <code>BITCOIN_ID</code>, <code>PRIME_RATE_ID</code>, …), and the <code>UI_*</code> report IDs used with <code>/set_active_ui_report</code>.}}
{| class="wikitable"
! Endpoint !! Body !! Description
|-
| POST <code>/set_custom_data</code> || JSON object || Shallow-merge mod data into <code>customData</code>. See [[#CustomData - your persistent storage|§CustomData]].
|}


== The gameState - everything you can read ==
== gameState reference ==


<code>GET /gamestate</code> (or <code>api.getGameState()</code>, or the live <code>gameStore</code>) returns a single JSON object - roughly 90 top-level fields - that is the complete read model of the game. In a component, subscribe to just the fields you need so you only re-render on those:
<code>GET /gamestate</code> returns one JSON object - ~90 top-level fields - that is the complete read model. In a frontend mod, subscribe to only the fields you need so you only re-render on those:


<pre>
<pre>
import { useGameStore } from '../api.js';
const cash = useGameStore(s =&gt; s.gameState.cash);
const cash = useGameStore(s => s.gameState.cash);
const year = useGameStore(s =&gt; s.gameState.currentYear);
const year = useGameStore(s => s.gameState.currentYear);
</pre>
</pre>


Major fields include:
{| class="wikitable"
! Group !! Fields
|-
| '''Player''' || <code>cash</code>, <code>otherAssets</code>, <code>totalAssets</code>, <code>totalDebt</code>, <code>netWorth</code>, <code>playerId</code>, <code>playerName</code>, <code>chairedCompanyId</code>
|-
| '''Clock / session''' || <code>currentYear</code>, <code>currentQuarter</code>, <code>currentMonth</code>, <code>currentDay</code>, <code>currentTime</code>, <code>nextEarningsDate</code>, <code>gameLoaded</code>, <code>gameOver</code>, <code>readyToRestart</code>, <code>isTickerRunning</code>, <code>tickSpeed</code>, <code>splashScreenPlayed</code>
|-
| '''Active selection''' || <code>activeEntityNum</code>, <code>activeEntityName</code>, <code>activeEntitySymbol</code>, <code>activeIndustryNum</code>, <code>activeIndustryId</code>, <code>actingAsId</code>, <code>actingAsName</code>, <code>actingAsSymbol</code>, <code>actingAsIndustryId</code>, <code>actingAs</code>
|-
| '''Modal state''' || <code>modalType</code>, <code>modalText</code>, <code>modalTitle</code>, <code>modalDefault</code>, <code>modalFilter</code>
|-
| '''Collections''' || <code>allCompanies</code>, <code>allPlayers</code>, <code>allSecurities</code>, <code>allIndustries</code>, <code>controlledCompanies</code>, <code>tickerQueue</code>, <code>streamingQuotesList</code>, <code>newsHeadlines</code>, <code>trendingNews</code>, <code>navHistory</code>, <code>navPointerIndex</code>
|-
| '''Active entity detail''' || <code>activeEntityData</code>, <code>activeEntityFinancials</code> (deep: cash/assets/debt/equity/EPS/cash-flow/…), <code>activeEntityPlayerFinancials</code>, <code>financialProfile</code>, <code>cashflowProjection</code>, <code>portfolio</code>, <code>portHoldings</code>, <code>swapsPortfolio</code>, <code>optionsList</code>, <code>commodityList</code>, <code>shareholdersList</code>, <code>researchReport</code>, <code>advisorySummary</code>, <code>earningsReport</code>, <code>advances</code>, <code>loansReport</code>, <code>myCorporationsReport</code>
|-
| '''Market reports''' || <code>whoOwns*Report</code> (futures, physical commodities, options, swaps, stocks, investment contracts), <code>industrySummaryReport</code>, <code>industryProjectionReport</code>, <code>industryGrowthRatesReport</code>, <code>mostMarketShareReport</code>, <code>mostTaxLossReport</code>, <code>mostCashReport</code>, <code>mostMarketCapReport</code>, <code>economicDataReport</code>, <code>interestRatesReport</code>, <code>whosAheadReport</code>
|-
| '''Settings''' || <code>suppEarnSetting</code>, <code>suppWarnSetting</code>, <code>supPopupSetting</code>, <code>autosaveSetting</code>, <code>exerciseItSetting</code>, <code>sweepSetting</code>, <code>makeDeliverySetting</code>, <code>takeDeliverySetting</code>, <code>tooltipsSetting</code>, <code>shareholderGraphSetting</code>, <code>unethicalSetting</code>, <code>disableHotkeysSetting</code>, <code>chartType</code>, <code>locale</code>
|-
| '''Tutorial / misc''' || <code>tutorialEnabled</code>, <code>tutorialStep</code>, <code>alerts</code>, <code>frozenAllLoans</code>, <code>dlrSign</code>, <code>euro</code>, '''<code>customData</code>''' ([[#CustomData - your persistent storage|§CustomData]])
|}


* '''Player:''' <code>cash</code>, <code>otherAssets</code>, <code>totalAssets</code>, <code>totalDebt</code>, <code>netWorth</code>, <code>playerId</code>, <code>playerName</code>, <code>chairedCompanyId</code>.
The exact shape can change between versions - the authoritative move is to <code>GET /gamestate</code> from a running game. <code>Ctrl+Shift+I</code> opens DevTools; <code>gameStore.getState().gameState</code> in the console dumps the live object.
* '''Clock / session:''' <code>currentYear</code>, <code>currentQuarter</code>, <code>currentMonth</code>, <code>currentDay</code>, <code>currentTime</code>, <code>nextEarningsDate</code>, <code>gameLoaded</code>, <code>gameOver</code>, <code>readyToRestart</code>, <code>isTickerRunning</code>, <code>tickSpeed</code>, <code>splashScreenPlayed</code>.
* '''Active selection:''' <code>activeEntityNum</code>, <code>activeEntityName</code>, <code>activeEntitySymbol</code>, <code>activeIndustryNum</code>, <code>activeIndustryId</code>, <code>actingAsId</code>, <code>actingAsName</code>, <code>actingAsSymbol</code>, <code>actingAsIndustryId</code>, <code>actingAs</code>.
* '''Modal state:''' <code>modalType</code>, <code>modalText</code>, <code>modalTitle</code>, <code>modalDefault</code>, <code>modalFilter</code>.
* '''Collections:''' <code>allCompanies</code>, <code>allPlayers</code>, <code>allSecurities</code>, <code>allIndustries</code>, <code>controlledCompanies</code>, <code>tickerQueue</code>, <code>streamingQuotesList</code>, <code>newsHeadlines</code>, <code>trendingNews</code>, <code>navHistory</code>, <code>navPointerIndex</code>.
* '''Active entity detail:''' <code>activeEntityData</code>, <code>activeEntityFinancials</code> (a deep object: cash, assets, debt, equity, EPS, cash-flow, etc.), <code>activeEntityPlayerFinancials</code>, <code>financialProfile</code>, <code>cashflowProjection</code>, <code>portfolio</code>, <code>portHoldings</code>, <code>swapsPortfolio</code>, <code>optionsList</code>, <code>commodityList</code>, <code>shareholdersList</code>, <code>researchReport</code>, <code>advisorySummary</code>, <code>earningsReport</code>, <code>advances</code>, <code>loansReport</code>, <code>myCorporationsReport</code>.
* '''Market reports:''' <code>whoOwns*Report</code> (futures, physical commodities, options, swaps, stocks, investment contracts), <code>industrySummaryReport</code>, <code>industryProjectionReport</code>, <code>industryGrowthRatesReport</code>, <code>mostMarketShareReport</code>, <code>mostTaxLossReport</code>, <code>mostCashReport</code>, <code>mostMarketCapReport</code>, <code>economicDataReport</code>, <code>interestRatesReport</code>, <code>whosAheadReport</code>.
* '''Settings:''' <code>suppEarnSetting</code>, <code>suppWarnSetting</code>, <code>supPopupSetting</code>, <code>autosaveSetting</code>, <code>exerciseItSetting</code>, <code>sweepSetting</code>, <code>makeDeliverySetting</code>, <code>takeDeliverySetting</code>, <code>tooltipsSetting</code>, <code>shareholderGraphSetting</code>, <code>unethicalSetting</code>, <code>disableHotkeysSetting</code>, <code>chartType</code>, <code>locale</code>.
* '''Tutorial / misc:''' <code>tutorialEnabled</code>, <code>tutorialStep</code>, <code>alerts</code>, <code>frozenAllLoans</code>, <code>dlrSign</code>, <code>euro</code>, and '''<code>customData</code>''' (see [[#The CustomData API - your persistent storage|§7]]).


The exact shape can change between versions - the authoritative move is to <code>GET /gamestate</code> from a running game and inspect it. (<code>Ctrl+Shift+I</code> opens DevTools; <code>gameStore.getState().gameState</code> in the console dumps the live object.)
== CustomData - your persistent storage ==


== The CustomData API - your persistent storage ==
<code>customData</code> is a free-form JSON object that '''rides along inside the save file'''. The engine never reads it for simulation - it stores it verbatim, writes it into the <code>.WSR</code>/<code>.DAT</code> save, restores it on load, and exposes it on every <code>gameState</code> as <code>gameState.customData</code>.
 
<code>customData</code> is a free-form JSON object that '''rides along inside the save file'''. The engine never reads it for simulation - it just stores it verbatim, writes it into the <code>.WSR</code>/<code>.DAT</code> save on save, and restores it on load. It is exposed back to you on every <code>gameState</code> as <code>gameState.customData</code>.


This is the supported way for a mod to persist its own state per-save.
This is the supported way for a mod to persist its own state per-save.


=== Reading ===
=== Read ===


<pre>
<pre>
import { useGameStore } from '../api.js';
const customData = useGameStore(s =&gt; s.gameState?.customData ?? {});
const customData = useGameStore(s => s.gameState?.customData ?? {});
const myStuff = customData.myModKey;
const myStuff = customData.myModKey;
</pre>
</pre>


=== Writing ===
=== Write ===


<pre>
<pre>
Line 404: Line 788:
</pre>
</pre>


Important semantics:
=== Semantics ===


* '''Shallow merge.''' <code>POST /set_custom_data</code> merges your object into the existing <code>customData</code> at the '''top level''' - incoming top-level keys overwrite, other keys are preserved. So namespace your data under one top-level key (e.g. <code>myModKey</code>) and write that whole sub-object each time.
* '''Shallow merge.''' <code>POST /set_custom_data</code> merges into the existing <code>customData</code> at the '''top level''' - incoming top-level keys overwrite, others are preserved. '''Namespace your data under one top-level key''' (e.g. <code>myModKey</code>) and write that whole sub-object each time.
* '''It round-trips.''' After you write, the new value comes back on the next <code>gameState</code> broadcast as <code>gameState.customData</code>.
* '''Round-trips.''' New value comes back on the next <code>gameState</code> broadcast.
* '''It is inert.''' The engine does not validate, interpret, or simulate on it. It is pure storage. It cannot be used to change cash, prices, or any engine value - for that you must use the action endpoints in [[#The API - everything you can make the game do|§5]].
* '''Inert.''' Cannot change cash, prices, or any engine value - only action endpoints can ([[#REST API reference|§REST API]]).
* '''It is per-save.''' It is saved and loaded with the game; a fresh "New Game" starts with empty <code>customData</code>.
* '''Per-save.''' Saved/loaded with the game; a fresh New Game starts empty.


=== Keys already in use by the base game ===
=== Reserved top-level keys ===


The stock UI already stores a few things in <code>customData</code> - '''do not reuse these top-level keys''' for your own data:
The base UI already stores a few things in <code>customData</code>. '''Don't reuse these keys''':


{| class="wikitable"
{| class="wikitable"
! Key !! Used by !! Contents
! Key !! Used by !! Contents
|-
|-
| <code>notes</code> || Notes modal || the player's free-text notes
| <code>notes</code> || Notes modal || free-text notes
|-
|-
| <code>navHistory</code> || Navigation system || saved navigation history
| <code>navHistory</code> || Navigation system || saved nav history
|-
|-
| (DB search criteria) || Database Search view || saved search filters/sort
| (DB search criteria) || Database Search view || saved filters / sort
|-
|-
| (holdings prefs) || Portfolio Holdings table || saved filters/sort/hide-subsidiaries
| (holdings prefs) || Portfolio Holdings table || saved filters / sort / hide-subsidiaries
|}
|}


Pick a unique top-level key for your mod (e.g. <code>customData["mymod:state"]</code>).
Pick a unique key for your mod (e.g. <code>customData["mymod:state"]</code>).


== What you cannot do (today) ==
== Limits - what you '''can't''' do ==


So expectations are clear:
* '''No raw memory / pointer writes.''' Only curated endpoints. The only free-form write is <code>customData</code>, and it's inert.
* '''No intercepting the update loop.''' WS delivers a snapshot. Read and react; you cannot mutate values mid-update or inject into serialization.
* '''No new engine behavior without recompiling.''' New mechanic / event type / endpoint = edit PowerBASIC source (<code>src/main/wsr/*.inc</code>, ~178K lines), rebuild <code>wsr.exe</code> (PB 9.80) and usually <code>ui.dll</code> (VS C++). That's engine development, not modding.
* '''No editing binary game data.''' Saves and most <code>.DAT</code>/<code>.PRM</code> are proprietary binary or encoded. <code>CORPNAME.DAT</code> is the one safe exception.


* '''No raw memory / pointer writes.''' The bridge exposes the engine's state through curated endpoints only. You cannot poke an arbitrary game variable from JavaScript - there is no generic "set value X" endpoint. The only free-form write is <code>customData</code>, and it is inert ([[#The CustomData API - your persistent storage|§7]]).
== Workflow ==
* '''No intercepting the update loop.''' The WebSocket delivers a ''snapshot''. You can read it and react, but you cannot mutate values mid-update or inject into the serialization.
* '''No new engine behavior without recompiling.''' The simulation lives in PowerBASIC (<code>src/main/wsr/*.inc</code>, ~178K lines). Adding a genuinely new game mechanic, a new event type, or a new endpoint means editing that source and rebuilding <code>wsr.exe</code> (PowerBASIC 9.80) and usually <code>ui.dll</code> (Visual Studio C++). That is engine development, not modding.
* '''No editing binary game data.''' Saves and most <code>.DAT</code>/<code>.PRM</code> files are proprietary binary or encoded formats. <code>CORPNAME.DAT</code> (plain text) is the one exception you can safely edit ([[#Company names - CORPNAME.DAT (plain text, directly editable)|§3.7]]).


== Workflow & sharing ==
=== Edit / test ===
 
=== Editing and testing ===


# Edit a file under <code>js/</code>, <code>css/</code>, <code>assets/</code>, or <code>CORPNAME.DAT</code>.
# Edit a file under <code>js/</code>, <code>css/</code>, <code>assets/</code>, or <code>CORPNAME.DAT</code>.
# Restart the game (frontend has no hot reload).
# Restart the game (no hot reload).
# Use <code>Ctrl+Shift+I</code> for DevTools - console errors, the <code>gameStore</code>, network tab against the REST API.
# <code>Ctrl+Shift+I</code> for DevTools - console errors, the <code>gameStore</code>, network tab against the REST API.


=== Packaging a mod for others ===
=== Packaging for others ===


Because there is no build step, a mod is just '''a set of replacement files'''. The simplest distribution is a zip that mirrors the <code>resources/app/</code> structure, e.g.:
A mod is '''a set of replacement files'''. Simplest distribution: a zip mirroring the <code>resources/app/</code> structure:


<pre>
<pre>
Line 456: Line 838:
</pre>
</pre>


with install instructions: "drop these into <code>resources/app/</code>, overwriting."
Install instructions: "drop these into <code>resources/app/</code>, overwriting."


Good practice:
Good practice:
* Keep mods '''additive''' where possible - new component files wired in with one import line, rather than rewriting core files, so two mods are less likely to collide.
* Keep mods '''additive''' where possible - new component files wired in with one import line, not rewrites of core files. Two additive mods are less likely to collide.
* Document which base files you replace.
* Document which base files you replace.
* Tell users that Steam's "verify integrity of game files" reverts changes (their uninstall path) and that they should back up originals.
* Tell users that Steam's "verify integrity of game files" reverts changes (their uninstall path) and to back up originals.


== Quick reference ==
== Steam Workshop ==
 
WSR ships with Steam Workshop support for sharing mods. Subscribed Workshop items are downloaded by Steam, copied into a per-user staging directory at startup, and overlaid on top of the install directory at the file-protocol layer. '''The install directory is never modified''' - Steam's "verify integrity of game files" stays a no-op for Workshop content, and mods survive a full reinstall.
 
=== How it works for players ===
 
# Open the game's Workshop page in Steam, subscribe to a mod.
# Steam downloads the mod content to its UGC folder.
# Next time you launch Wall Street Raider, the game mirrors that folder into <code>%LOCALAPPDATA%\Wall Street Raider\workshop\installed\&lt;id&gt;\</code> and builds a merged overlay at <code>%LOCALAPPDATA%\Wall Street Raider\workshop\overlay\</code>.
# When the renderer asks for a file (<code>js/locale/de.js</code>, <code>css/variables.css</code>, etc.), Electron's file-protocol interceptor checks the overlay first and falls through to the install dir if the file isn't there.
 
In-game: '''Main Menu &rarr; Workshop''' opens the subscribed-mods list. Each row shows the mod's current Steam state with color coding:
* '''Installed''' (green) - downloaded, will apply on next game restart
* '''Downloading…''' / '''Queued''' (cyan) - Steam is fetching the content; wait, then restart
* '''Subscribed (not downloaded)''' (yellow) - Steam hasn't started downloading; click the '''Force Download''' button on that row to prompt it
* '''Update available''' (yellow) - newer version exists on Workshop; click Force Download
The list auto-refreshes every 3 seconds while the modal is open, so status changes are visible without closing and reopening.
 
Other modal buttons:
* '''Browse Workshop in Steam''' - opens the WSR Workshop page in the Steam overlay (or your default browser if the overlay is off)
* '''Publish a Mod…''' - launches the bundled Mod Uploader (see [[#Publishing a mod|§Publishing a mod]])
* '''Unsubscribe''' (per row) - removes the subscription; effective on next restart
 
{{Note|Mod changes take effect on the '''next''' game launch. Subscribed mods aren't hot-swapped into a running session - JS modules cache after first fetch, so overlay rebuilds mid-session wouldn't reach the running renderer.}}
 
=== What can be in a Workshop mod ===
 
A Workshop mod is '''a zip of replacement files mirroring the <code>resources/app/</code> tree'''. Everything listed earlier in this guide is fair game:
 
* <code>css/variables.css</code>, <code>css/theme.css</code>, <code>css/components/*.css</code> (themes)
* <code>js/locale/&lt;code&gt;.js</code> plus an updated <code>js/locale/localeManager.js</code> (locales)
* <code>js/data/hints/*.js</code> (contextual hints)
* <code>assets/*.png</code> / <code>*.jpg</code> / <code>*.mp4</code> / <code>*.ico</code> (asset reskins, same filenames)
* <code>assets/help/wsrbook.htm</code> (rewritten manual)
* <code>CORPNAME.DAT</code> (renamed companies / tickers)
* '''New''' Preact component files plus a one-line import in <code>js/app.js</code> or a parent component (additive UI mods are the cleanest way to avoid collisions with other mods)
 
You '''cannot''' mod through Workshop:
* <code>wsr.exe</code> or <code>ui.dll</code> (binaries, blocked from the overlay for safety)
* Engine simulation rules (engine doesn't read mods)
* Save format
 
=== Conflict resolution ===
 
If two subscribed mods both ship a <code>css/variables.css</code>, '''last-loaded wins''' and a warning is logged to <code>wsr-stdio.log</code> listing the conflict. There is no in-game enable/disable toggle in v1 - if two mods collide and you don't like the result, unsubscribe from the one whose changes you don't want.
 
=== Publishing a mod ===
 
The '''WSR Mod Uploader''' is bundled with the game. Two ways to launch it:
 
* '''From inside WSR:''' Main Menu &rarr; Workshop &rarr; '''Publish a Mod…'''
* '''Direct from your install dir:''' <code>&lt;install&gt;\mod-uploader\wsr-mod-uploader.exe</code>
 
The uploader is a thin GUI around <code>ISteamUGC::CreateItem</code> / <code>SubmitItemUpdate</code>. Steam must be running.
 
==== Workflow ====
 
# '''Mod folder''' - pick the folder containing your mod's files. The picker defaults to <code>&lt;Documents&gt;\WSR Mods\</code>, which is auto-created on first launch. Anything under there shows up as a quick pick. Your folder should mirror the <code>resources/app/</code> structure (see [[#What can be in a Workshop mod|§What can be in a Workshop mod]]).
# '''Preview image''' - pick a PNG or JPG '''under 1 MB''' (Workshop limit, enforced client-side). Required - Workshop rejects items without a preview.
# '''Title''' - 5 to 128 characters. Steam rejects very short titles for public items.
# '''Description''' - at least 30 characters. Same reason.
# '''Tags''' - optional. Pick from the preset checkboxes, type your own comma-separated, or both. See [[#Standard tags|§Standard tags]].
# '''Visibility''' - Public / Friends only / Private. Start with Private while you iterate.
# '''Changelog''' - shown in the Workshop item's revision history. Useful on updates.
# Click '''Publish'''.
 
The Publish button stays disabled until folder, preview, title, and description are all valid. The status panel at the bottom streams JSON status lines as Steam processes the upload; success ends with <code>Published! Workshop ID: &lt;number&gt;</code> and an '''Open in browser''' button.
 
==== .wsrmod-id (updates vs new items) ====
 
On the first successful publish from a folder, the uploader writes <code>.wsrmod-id</code> into that folder containing the assigned <code>PublishedFileId_t</code>. Subsequent publishes from the same folder detect this file and '''update''' the existing Workshop item rather than creating a duplicate. To fork a mod (publish a copy as a new item), delete or move <code>.wsrmod-id</code> out of the folder before publishing.
 
==== SteamCMD alternative ====
 
You can also use SteamCMD's <code>+workshop_build_item</code> with a <code>.vdf</code> if you prefer scripting. Same metadata is required. See [https://partner.steamgames.com/doc/features/workshop/implementation#SteamCmd Valve's docs].
 
==== Standard tags ====
 
The Wall Street Raider Workshop accepts '''freeform tags by default''' - you can type any tag in the uploader's "Custom tags" field. The convention below is what the in-game Workshop browser groups by, so following it makes your mod easier to discover:


{| class="wikitable"
{| class="wikitable"
! I want to… !! Do this
! Tag !! What it means
|-
| Re-theme the game || Edit <code>css/variables.css</code>, then <code>css/theme.css</code> / <code>css/components/</code>
|-
| Change UI text/layout/behavior || Edit the relevant file in <code>js/components/</code>
|-
| Add a new panel/feature || New file in <code>js/components/</code>, import it in a parent or <code>app.js</code>
|-
| Replace a background video or image || Drop a same-named file into <code>assets/</code>
|-
|-
| Rewrite the manual || Edit <code>assets/help/wsrbook.htm</code>
| <code>Theme</code> || CSS theme / re-skin (palette, fonts, layout tweaks)
|-
|-
| Add a language || New <code>js/locale/&lt;code&gt;.js</code> + register in <code>localeManager.js</code>
| <code>Locale</code> || Translation pack (full or partial)
|-
|-
| Change/add contextual hints || Edit files in <code>js/data/hints/</code>
| <code>Hints</code> || New / rewritten contextual hints
|-
|-
| Rename companies / tickers || Edit <code>CORPNAME.DAT</code> (plain text), starts on next New Game
| <code>Assets</code> || Replacement images, videos, icons
|-
|-
| Make the game ''do'' something || Call an <code>api.js</code> function ([[#The API - everything you can make the game do|§5]]) - ~150 endpoints
| <code>Companies</code> || Alternate company-name database (<code>CORPNAME.DAT</code>)
|-
|-
| Read game state || <code>useGameStore(s => s.gameState.&lt;field&gt;)</code> or <code>GET /gamestate</code> ([[#The gameState - everything you can read|§6]])
| <code>UI Extension</code> || New Preact components or feature additions
|-
|-
| Persist my mod's own data in saves || <code>setCustomData({ myKey: … })</code> ([[#The CustomData API - your persistent storage|§7]])
| <code>Other</code> || Anything that doesn't fit the categories above
|-
| Change a core simulation rule || Not moddable - requires engine source + recompile
|}
|}
Tags are optional. Multiple tags are fine when a mod spans categories (e.g. a "1929 era" mod might tag <code>Theme</code> + <code>Companies</code> + <code>Assets</code>).
=== Troubleshooting ===
==== For subscribers (using mods) ====
* '''Subscribed mod isn't visible.''' Re-launch the game - mod changes apply at startup only.
* '''Workshop menu says "not available."''' The game must be launched via Steam. Workshop calls require a logged-in Steam client.
* '''Mod is stuck on "Subscribed (not downloaded)" yellow status.''' Click the '''Force Download''' button on that row. Calls <code>ISteamUGC::DownloadItem(id, high_priority=true)</code> to prompt Steam to fetch the content. Watch the status flip to '''Queued''' then '''Downloading…''' then '''Installed'''. If the status doesn't change within ~10 seconds, fully restart the Steam client (Exit, wait, reopen) - subscriptions can fail to sync until Steam reconnects.
* '''Mod's files aren't loading.''' Check <code>%LOCALAPPDATA%\Wall Street Raider\workshop\manifest.json</code> to see which mods were detected and which file paths the overlay owns. If your mod isn't in the <code>mods</code> list, the loader couldn't see its install folder (rare - usually means the Steam content download is still in progress; wait and re-launch).
* '''Two mods conflict.''' Check the same <code>manifest.json</code> for the <code>conflicts</code> array. Last-wins means whichever mod sorted later in the loader's iteration order replaced the conflicting file. Unsubscribe from one of the two.
==== For mod authors (using the uploader) ====
* '''Uploader's Publish button is greyed out.''' One or more required fields aren't satisfied. Yellow hint text under each field shows what's missing (folder picked, preview image, title at least 5 chars, description at least 30 chars).
* '''Upload returns <code>AccessDenied (15)</code> after content reaches 100%.''' Almost always: the Steamworks Workshop config has unpublished changes. Go to the top-nav '''Publish''' tab and push any pending config changes to Steam. If everything's already published, check that '''Steam Cloud quota''' is non-zero for this app and that '''Enable ISteamUGC for file transfer''' is checked.
* '''Upload returns <code>Busy (10)</code> repeatedly.''' The uploader auto-retries 3 times. If still failing: Workshop config for this app likely isn't fully provisioned (the per-app Workshop service hasn't initialized in Steam's backend yet). Wait 10 minutes after publishing any config changes, then retry.
* '''Upload returns <code>InvalidParam (8)</code>.''' One of the metadata fields is rejected. Check the diagnostic <code>{"kind":"params"}</code> line in the uploader's status panel - it shows everything that was sent. Most common cause: title or description too short.
* '''Friend's Steam chat opens whenever I publish.''' Steam shows you as "In-Game: Wall Street Raider" while the uploader's native helper is running, which fires friend-presence notifications. Set your Steam status to '''Invisible''' while testing, OR have the friend disable "Open chat when friend signs in" in their Steam notification settings. There's no clean code-level fix.
* '''Cleanup: empty / placeholder Workshop items from failed publishes.''' Each <code>CreateItem</code> succeeds even when <code>SubmitItemUpdate</code> later fails, leaving an empty Workshop item that auto-subscribes you. Visit [https://steamcommunity.com/my/myworkshopfiles/?appid=3525620 your Workshop submissions page], click into each empty/no-title item, and delete it (Edit &rarr; Delete). Also unsubscribe from them via Main Menu &rarr; Workshop in-game.


[[Category:Modding]]
[[Category:Modding]]
[[Category:Wall Street Raider]]
[[Category:Wall Street Raider]]

Latest revision as of 14:13, 17 May 2026

Wall Street Raider - Modding Guide

[edit]

A reference for everything you can mod in WSR with nothing but a text editor. The Electron frontend has no build step and the install ships with asar: false, so every file under resources/app/ is a normal editable file. Edit, CTRL+R, see your change.

What you can't mod without recompiling: the PowerBASIC engine (wsr.exe), the C++ bridge (ui.dll), and the binary .DAT/.PRM files.

Template:Note

TL;DR - what's moddable

[edit]
I want to… Do this
Re-theme the game Edit css/variables.css, then css/theme.css / css/components/
Change UI text / layout / behavior Edit the relevant file in js/components/
Add a new panel or feature New file in js/components/, import it from a parent or app.js
Replace a background video or image Drop a same-named file into assets/
Rewrite the manual Edit assets/help/wsrbook.htm
Add a language New js/locale/<code>.js + register in localeManager.js
Change / add contextual hints Edit files in js/data/hints/
Rename companies / tickers / nations Edit CORPNAME.DAT (plain text). Effective on next New Game.
Drive the game from JS Call an api.js function (§api.js)
Drive the game from a third-party app Hit the REST API directly (§REST API)
Read game state useGameStore(s => s.gameState.<field>) or GET /gamestate (§gameState)
Persist your mod's data in saves setCustomData({ myKey: … }) (§CustomData)
Share a mod with other players Steam Workshop (§Workshop)
Change a core simulation rule Not moddable. Requires engine source + recompile.

Architecture in one breath

[edit]
PowerBASIC engine  →  C++ bridge DLL (REST + WebSocket)  →  Electron UI
   wsr.exe              ui.dll                                everything you see

At launch, ui.dll picks ephemeral REST + WS ports and writes them to %LOCALAPPDATA%\Wall Street Raider\runtime.json:

{ "pid": 12345, "rest_port": 54321, "ws_port": 54322 }

Electron reads that file and hands the ports to the renderer; js/api.js builds http://127.0.0.1:<rest> and ws://127.0.0.1:<ws>. WS pushes a full snapshot on connect, then JSON-patch diffs; if WS drops, the UI polls GET /gamestate.

Third-party apps: read runtime.json the same way. Ports change every launch.

File layout

[edit]

In source (electron/) and in an installed copy (<install>/resources/app/) the layout is identical - the install is a verbatim copy of the source tree (see extraFiles in electron/package.json):

resources/app/
├── index.html              # entry point
├── main.js                 # Electron main process
├── js/                     # ← all frontend code (editable)
│   ├── app.js              #   Preact root, WS wiring, hotkeys
│   ├── api.js              #   REST/WS client, gameStore, ID constants, endpoint wrappers
│   ├── components/         #   100+ Preact panels/modals/tables/tabs
│   ├── hooks/              #   useCookie, useInterval, usePanelSelection, …
│   ├── services/           #   hintMatcher.js, …
│   ├── utils/              #   misc
│   ├── icons.js            #   inline SVG
│   ├── debug-log.js        #   frontend logging
│   ├── lib/                #   vendored: preact, zustand, tailwind, fast-json-patch
│   ├── locale/             #   ← translations
│   └── data/hints/         #   ← contextual hints
├── css/                    # ← all styling (editable)
├── assets/                 # ← images / videos / icons / help (editable)
├── CORPNAME.DAT            # ← plain-text company database (editable)
├── wsr.exe                 # engine (binary)
├── ui.dll                  # bridge (binary)
└── *.DAT / *.PRM           # other game data (binary/encoded - leave alone)

Frontend code (js/)

[edit]

Components use Preact + htm (JSX-like template literals, no compile step) and Zustand for state. Minimal example:

import { html, Component } from '../lib/preact.standalone.module.js';
import { useGameStore } from '../api.js';

export function MyPanel() {
    const cash = useGameStore(s => s.gameState.cash);
    return html`<div class="p-4">Cash: ${cash}</div>`;
}

You can:

  • Edit any existing component (layout, text, colors, behavior).
  • Add new components and import them into app.js or any parent.
  • Add new hotkeys (grep hotkey; handling lives in app.js + HotkeyButtonBar.js).
  • Add new buttons that call any API endpoint (§api.js).
  • Add new derived displays from gameState (§gameState).

Styling (css/)

[edit]
css/
├── style.css               # the file index.html links - imports the rest
├── base.css                # resets / base element styles
├── variables.css           # ← CSS custom properties (theme palette lives here)
├── theme.css               # theme layer
├── layout.css              # app layout
├── chart-styles.js         # chart styling injected as JS
├── components/             # buttons, dropdown, forms, modals, panels, tables, tabs
├── features/               # assets, calculator, help, market, menu, notes, ticker, tutorial
└── pages/                  # game, main-menu, quotes

Tailwind utilities are also available via js/lib/tailwind.module.js, so most components use Tailwind inline + the modular CSS for anything custom.

To re-theme: start with variables.css - override the custom properties and the whole app follows.

Assets (assets/)

[edit]
What Files
Background videos the many *.mp4 (Wall Street footage, industry b-roll, …)
Event images blackswan.png, ponzi.jpg, soupline.jpg, zimbabwe.jpg, helicopter.jpg, yacht.mp4
Branding / logos wallstreetraider_logo.png, hackjackgames_logo.png, roninsoft_logo.png
Social widgets discord-widget.png, reddit-widget.png
Loading spinner loading.gif
App icon wsr.ico

Replace any of these with a same-named file of the same type. Keep filenames identical - components reference them by name.

Help content (assets/help/)

[edit]

wsrbook.htm is the in-game manual - one ~1 MB plain HTML file. Edit it directly. The *.jpg files in the same folder are diagrams it embeds. electron/UI_HELP_CATALOG.txt is a developer catalog of help sections.

Translations (js/locale/)

[edit]

Pure data - no engine changes needed to add a language.

How it works (localeManager.js):

  • LANGUAGE_OPTIONS maps locale code → { name, dictionary, warning? }.
  • Each dictionary is a flat { "English source": "translation", … }.
  • The translator does runtime lookup/replacement on UI strings.
  • Existing locales: zh-CN.js, es-419.js, ja-JP.js, pt-BR.js, ru-RU.js.

To add a language:

// js/locale/fr-FR.js
export const frFR = {
    "New Game": "Nouvelle partie",
    "Buy Stock": "Acheter des actions",
    // … one entry per source string you want translated
};

// js/locale/localeManager.js
import { frFR } from './fr-FR.js';
// …
'fr-FR': { name: 'Français', dictionary: frFR },

The chosen locale persists in CFIG.WSR, surfaces as gameState.locale, and switches via POST /set_locale.

Template:Note

Contextual hints (js/data/hints/)

[edit]

Hint files are grouped by view and aggregated by index.js:

hints/
├── index.js          # aggregates all sets
├── global.js         # fallback hints
├── company.js        # while viewing a company
├── industry.js
├── market.js
├── modals.js
├── player.js
├── portfolio.js
└── swap-info-text.js

Each entry:

{
    id: 'company-overview-controlled-etf',
    match: { view: 'company', tab: 'overview', controls: true, entityKind: 'mutual-fund' },
    title: 'Advising an ETF',
    content: `<p>HTML body shown to the player…</p>`,
}

js/services/hintMatcher.js picks the most specific match for the current screen. Rewrite text, add new hints, retarget match conditions - no engine changes.

Company names (CORPNAME.DAT)

[edit]

Plain pipe-delimited text:

0011|00|JPB  |  J.P. MULLINS WALL ST. BANK |
0012|00|UBC  |  URBAN BANCORP |
0015|26|KYOB |  KYOTO BANK |

Columns: company ID | nation code | ticker | name. Read by the engine's GetCorpNames at new-game time, so edits take effect on the next new game (not on already-saved games). Rename companies, change tickers, change a company's home nation.

Keep the format exact: 4-digit zero-padded ID, 2-digit nation code, fixed-width symbol field, and the surrounding | + spacing. CORPNAME.ORI is the pristine original - keep as your backup.

Other game data (don't bother)

[edit]
File(s) Content Why limited
QUOTEWSR.DAT quote of the day encoded; quotehs.prm is a near-plaintext source the engine doesn't read
MSCNEWS.DAT news / scenario headlines encoded; same story with MSCNEWS.PRM
scenwin1-4.prm scenario window text engine-internal format
GAME01-50.DAT save slots / scenario data proprietary binary
WSR101.DAT, REGINFO.DAT, … engine data proprietary binary

Treat as read-only. CORPNAME.DAT is the one truly editable game-data file.

Calling the API from a frontend mod

[edit]

js/api.js is the wrapper layer for the REST API: each endpoint has a named async function with semantic parameters, plus the live gameStore, plus exported ID constants (entity IDs, industry indices, special asset IDs, UI_* report IDs). Frontend mods should call api.js functions, not raw HTTP.

import * as api from '../api.js';

await api.buyStock(companyId);                    // /buy_stock
await api.setDividend(actingAsId);                // /set_dividend
await api.merger(targetId, actingAsId);           // /merger
await api.cheatAddCash();                         // /cheat_add_cash
const state = await api.getGameState();           // GET /gamestate

For endpoints that don't have a named wrapper yet, four request shapes are also exported:

Helper Body shape Use
postNoArg(path) {} actions with no parameter
postIdArg(path, id) { id } actions on a specific entity / slot
postIdArgWithActingAs(path, id, actingAsId) { id, intParam2 } most corporate / trade actions
postOptionsTradeWithActingAs(path, id, actingAsId, underlyingId) { id, intParam2, underlyingId } options trades
postStringArg(path, str) { str } actions taking a string (e.g. save-as filename)
getJSON(path) - GET endpoints

For wire-level details (the underlying routes, the JSON body the bridge actually receives, what each generic field means per endpoint), see §REST API reference.

REST API reference

[edit]

For third-party apps and direct integrations. Frontend mods should prefer api.js (§api.js).

Conventions

[edit]
  • Base URL: http://127.0.0.1:<rest_port>. Read rest_port from %LOCALAPPDATA%\Wall Street Raider\runtime.json; ports change every launch.
  • Content-Type: application/json on all POSTs.
  • Generic body schema: the bridge accepts a single envelope { id?, intParam2?, underlyingId?, str?, answer?, value?, filename? }. Each endpoint uses 0-3 of these fields with endpoint-specific meanings - the per-endpoint tables below spell out which fields are read and what they mean in context.
  • Omitted fields default to 0 / empty. Most action endpoints can be called as {} if they don't strictly need params (the engine often falls back to "use the currently active entity" or "show the picker modal").
  • Most actions open a modal rather than executing immediately. After POSTing, the engine pushes a modal state via WS / gameState.modalType; respond with POST /modal_result.

The actingAsId pattern

[edit]

Most corporate / trade endpoints accept an actingAsId (sent as the JSON field intParam2): the entity that executes the action.

  • 0 (or omitted) = act as the player.
  • Any other ID = act as that controlled company. The engine momentarily switches context, runs the action as that company, then restores.

When the per-endpoint tables below show intParam2, it means actingAsId unless noted otherwise.

Response shape

[edit]

GET endpoints return endpoint-specific JSON (documented in the GET table). POST endpoints return { status: "ok" } on success and a { error: "…" } with HTTP 4xx/5xx on failure. Side effects show up via the next WS broadcast.

GET endpoints

[edit]
Method Endpoint Returns
GET /status health check
GET /gamestate full game-state JSON (§gameState)
GET /quote { quote: string } - quote of the day
POST /asset_chart body { id }; returns 60 months of price history for an asset ({ prices, highs, lows, xAxisTitle, yAxisTitle, baseMonth, baseYear })
GET /database_data searchable database of all companies (industry, financials, ratings, …)
GET /ownership_tree ownership hierarchy of the active entity (who owns it, at what %)
GET /subsidiaries_tree what the active entity owns (its subsidiaries, at what %)

Session / clock

[edit]
Endpoint Body Description
POST /newgame {} Start a new game.
POST /loadgame {} Open the load-game flow.
POST /load_specific_save { filename } Load a specific save. filename = save basename; must not contain .., /, \, :.
POST /savegame {} Save to the default slot.
POST /savegameas { filename } Save under a name. Empty filename shows the Save-As dialog.
POST /exit_game {} Exit to main menu.
POST /check_scoreboard {} Open the scoreboard view.
POST /start_ticker {} Start the market ticker animations.
POST /run_ticker {} Advance the ticker by one queue item.
POST /stop_ticker {} Stop the ticker.
POST /set_ticker_speed { id } Set ticker speed. id = speed enum.
POST /ticker_advance {} Pop the front of the ticker queue, append a new item.
POST /clear_event_string {} Clear the event message log.
POST /splash_screen_played {} Mark splash as shown so it's skipped next time.

Trading - stocks & bonds

[edit]
Endpoint Body Description
POST /buy_stock { id, intParam2? } Buy stock. id = company to buy. intParam2 = actingAsId.
POST /sell_stock { id, intParam2? } Sell stock. id = company to sell. intParam2 = actingAsId.
POST /short_stock { id, intParam2? } Open short. id = company to short. intParam2 = actingAsId.
POST /cover_short_stock { id, intParam2? } Cover an existing short. id = company. intParam2 = actingAsId.
POST /buy_corporate_bond { id, intParam2? } Buy a corporate bond. id = issuing company. intParam2 = actingAsId.
POST /sell_corporate_bond { id, intParam2? } Sell a corporate bond. id = issuing company. intParam2 = actingAsId.
POST /buy_long_govt_bonds { intParam2 } Buy long-term govt bonds. intParam2 = actingAsId. id ignored.
POST /sell_long_govt_bonds { intParam2 } Sell long-term govt bonds. intParam2 = actingAsId.
POST /buy_short_govt_bonds { intParam2 } Buy short-term govt bonds. intParam2 = actingAsId.
POST /sell_short_govt_bonds { intParam2 } Sell short-term govt bonds. intParam2 = actingAsId.

Trading - commodities & crypto

[edit]
Endpoint Body Description
POST /buy_commodity_futures { id, intParam2? } Buy commodity futures. id = commodity ID (e.g. OIL_ID, GOLD_ID). intParam2 = actingAsId.
POST /sell_commodity_futures { id, intParam2? } Sell commodity futures. id = commodity ID. intParam2 = actingAsId.
POST /close_long_commodity_futures_by_slot { id, intParam2? } Close a specific long position. id = portfolio slot index (not a commodity ID). intParam2 = actingAsId.
POST /short_commodity_futures { id, intParam2? } Short commodity futures. id = commodity ID. intParam2 = actingAsId.
POST /cover_short_commodity_futures { id, intParam2? } Cover commodity short. id = commodity ID. intParam2 = actingAsId.
POST /cover_short_commodity_futures_by_slot { id, intParam2? } Cover a specific short position. id = portfolio slot index. intParam2 = actingAsId.
POST /buy_physical_commodity { id, intParam2? } Buy physical commodity. id = commodity ID. intParam2 = actingAsId.
POST /sell_physical_commodity { id, intParam2? } Sell physical commodity. id = commodity ID. intParam2 = actingAsId.
POST /buy_physical_crypto { id, intParam2? } Buy physical crypto. id = crypto ID (e.g. BITCOIN_ID). intParam2 = actingAsId.
POST /sell_physical_crypto { id } Sell physical crypto. id = crypto ID. (No actingAs param on this one.)
POST /buy_crypto_futures { id, intParam2? } Buy crypto futures. id = crypto ID. intParam2 = actingAsId.
POST /sell_crypto_futures { id, intParam2? } Sell crypto futures. id = crypto ID. intParam2 = actingAsId.

Trading - options

[edit]
Endpoint Body Description
POST /buy_calls { id, intParam2?, underlyingId? } Buy calls. id = strike-price ID (or 0 to show the strike picker). intParam2 = actingAsId. underlyingId = company being optioned (when id>0 and underlyingId>0, modal is skipped — used by CLI flows like "CALL ABC").
POST /sell_calls { id, intParam2?, underlyingId? } Sell calls. Same param semantics as /buy_calls.
POST /buy_puts { id, intParam2?, underlyingId? } Buy puts. Same param semantics as /buy_calls.
POST /sell_puts { id, intParam2?, underlyingId? } Sell puts. Same param semantics as /buy_calls.
POST /advanced_options_trading { intParam2 } Open the advanced options panel (spreads, collars). intParam2 = actingAsId.
POST /exercise_call_options_early { id, intParam2? } Exercise a call early. id = call contract ID. intParam2 = actingAsId.
POST /exercise_put_options_early { id, intParam2? } Exercise a put early. id = put contract ID. intParam2 = actingAsId.

Corporate management

[edit]

All of these operate on the acting-as entity. id is unused unless noted; pass 0 or omit.

Endpoint Body Description
POST /prepay_taxes { intParam2 } Prepay taxes. intParam2 = actingAsId.
POST /elect_ceo { intParam2 } Open CEO-election picker for the entity. intParam2 = actingAsId.
POST /resign_as_ceo { intParam2 } Acting-as entity resigns as CEO. intParam2 = actingAsId.
POST /change_managers { intParam2 } Hire / fire managers. intParam2 = actingAsId.
POST /set_dividend { intParam2 } Set the company's dividend. intParam2 = actingAsId (dividend issuer).
POST /set_productivity { intParam2 } Set productivity. intParam2 = actingAsId.
POST /set_growth_rate { intParam2 } Set growth rate. intParam2 = actingAsId.
POST /restructure { intParam2 } Open restructuring modal. intParam2 = actingAsId.
POST /buy_corporate_assets { intParam2 } Buy plant / equipment. intParam2 = actingAsId.
POST /sell_corporate_assets { intParam2 } Sell plant / equipment. intParam2 = actingAsId.
POST /offer_corporate_assets_for_sale { intParam2 } List assets for other entities to buy. intParam2 = actingAsId.
POST /view_for_sale_items { intParam2 } Browse assets currently for sale. intParam2 = actingAsId.
POST /sell_subsidiary_stock { id, intParam2? } Sell stock the entity holds in a subsidiary. id = subsidiary company ID. intParam2 = actingAsId (seller).
POST /rebrand { intParam2 } Rename the company. intParam2 = actingAsId.
POST /toggle_company_autopilot { id } Toggle one company's autopilot. id = company.
POST /toggle_global_autopilot { intParam2 } Toggle autopilot across all of the entity's holdings. intParam2 = actingAsId.
POST /become_etf_advisor { intParam2 } Become ETF advisor. intParam2 = actingAsId.
POST /set_advisory_fee { intParam2 } Set the ETF advisory fee. intParam2 = actingAsId.
POST /decrease_earnings { intParam2 } Accounting adjustment to lower reported earnings. intParam2 = actingAsId.
POST /increase_earnings { intParam2 } Accounting adjustment to raise reported earnings. intParam2 = actingAsId.

M&A / corporate finance

[edit]
Endpoint Body Description
POST /merger { id, intParam2? } Start a merger. id = target company. intParam2 = actingAsId (acquirer).
POST /greenmail { id, intParam2? } Greenmail (coercive buyback). id = target. intParam2 = actingAsId.
POST /lbo { id, intParam2? } Leveraged buyout. id = target. intParam2 = actingAsId.
POST /startup { intParam2 } Found a new startup. intParam2 = actingAsId (founder).
POST /capital_contribution { intParam2 } Inject cash into a subsidiary. intParam2 = actingAsId.
POST /public_stock_offering { intParam2 } IPO. intParam2 = actingAsId (company going public).
POST /private_stock_offering { intParam2 } Private placement. intParam2 = actingAsId.
POST /issue_new_corp_bonds { intParam2 } Issue new corporate bonds. intParam2 = actingAsId (issuer).
POST /redeem_corp_bonds { intParam2 } Retire outstanding bonds. intParam2 = actingAsId.
POST /extraordinary_dividend { intParam2 } One-time special dividend. intParam2 = actingAsId.
POST /tax_free_liquidation { intParam2 } Liquidate with tax deferral. intParam2 = actingAsId.
POST /taxable_liquidation { intParam2 } Liquidate; pay tax on gains. intParam2 = actingAsId.
POST /spin_off { id, intParam2? } Spin off a subsidiary. id = subsidiary to spin off. intParam2 = actingAsId (parent).
POST /split_stock { intParam2 } Forward stock split. intParam2 = actingAsId.
POST /reverse_split_stock { intParam2 } Reverse stock split. intParam2 = actingAsId.

Banking / loans / swaps

[edit]
Endpoint Body Description
POST /borrow_money { intParam2 } Take out a loan. intParam2 = actingAsId (borrower).
POST /repay_loan { intParam2 } Pay down / repay. intParam2 = actingAsId.
POST /advance_funds { intParam2 } Lender advances cash against securities. intParam2 = actingAsId (lender).
POST /call_in_advance { id } Terminate an advance. id = advance ID.
POST /interest_rate_swaps { id, intParam2? } Open the swaps modal. id = asset ID. intParam2 = actingAsId.
POST /view_swap_details { id, intParam2? } View one swap's details. id = swap ID. intParam2 = actingAsId.
POST /terminate_swap { id, intParam2? } Terminate a swap early. id = swap ID. intParam2 = actingAsId.
POST /set_bank_allocation { intParam2 } Distribute cash across banks. intParam2 = actingAsId.
POST /trade_tbills { intParam2 } Trade T-bills. intParam2 = actingAsId.
POST /list_bank_loans { intParam2 } List the entity's outstanding bank loans (when acting as a bank). intParam2 = actingAsId.
POST /change_bank { intParam2 } Change primary bank. intParam2 = actingAsId.
POST /call_in_loan { id } Demand immediate repayment. id = loan ID.
POST /buy_bank_loans {} Open the bank-loans investment picker.
POST /buy_business_loans { intParam2 } Buy business-loan portfolio. intParam2 = actingAsId (investor).
POST /sell_business_loan { id } Sell one business-loan holding. id = loan ID.
POST /buy_consumer_loans { intParam2 } Buy consumer-loan portfolio. intParam2 = actingAsId.
POST /sell_consumer_loans { intParam2 } Sell consumer-loan holdings. intParam2 = actingAsId.
POST /buy_prime_mortgages { intParam2 } Buy prime mortgages. intParam2 = actingAsId.
POST /sell_prime_mortgages { intParam2 } Sell prime mortgages. intParam2 = actingAsId.
POST /buy_subprime_mortgages { intParam2 } Buy subprime mortgages. intParam2 = actingAsId.
POST /sell_subprime_mortgages { intParam2 } Sell subprime mortgages. intParam2 = actingAsId.
POST /list_etfs {} Show available ETFs.
POST /freeze_all_loans { intParam2 } Freeze all loan repayments across the lender's portfolio. intParam2 = actingAsId.
POST /freeze_loan { id } Freeze one specific loan. id = loan ID.
[edit]
Endpoint Body Description
POST /change_law_firm { intParam2 } Hire a law firm. intParam2 = actingAsId.
POST /credit_info { intParam2 } Show credit info (rating, history). intParam2 = actingAsId.
POST /antitrust_lawsuit { id, intParam2? } Sue for antitrust. id = target company. intParam2 = actingAsId (plaintiff).
POST /harrassing_lawsuit { id, intParam2? } File a frivolous lawsuit. id = target. intParam2 = actingAsId.
POST /spread_rumors { id, intParam2? } Spread rumors to damage stock / reputation. id = target company. intParam2 = actingAsId.

Reports / views / navigation

[edit]
Endpoint Body Description
POST /set_active_ui_report { id } Switch active report panel. id = a UI_* enum (see api.js constants).
POST /set_view_asset { id } Navigate to a company / player. id = entity ID. Updates nav history.
POST /set_view_industry { id } Navigate to an industry overview. id = industry index, or -2 = trigger DB Search.
POST /database_search {} Open the database search interface.
POST /clear_chart {} Clear the active price chart.
POST /growth_throttle {} Open growth-throttle settings.
POST /clear_stream_list {} Clear the streaming-quotes watchlist.
POST /fill_stream_list {} Populate the streaming-quotes watchlist with defaults.
POST /toggle_streaming_quote { id } Toggle live updates for one asset. id = company / asset.
POST /nav_back {} Nav history: back.
POST /nav_forward {} Nav history: forward.
POST /nav_clear {} Clear nav history.
POST /nav_goto { id } Jump to a nav-history index. id = position in history stack.
POST /nav_set_history JSON array Restore nav history from saved state. Body = array of { id, type } where type is "asset" or "industry", most-recent-first.
POST /set_who_owns_filter { value } Set the "who owns what" filter. value = filter enum.
POST /view_current_interest_rates {} Show current interest rates.
POST /whos_ahead {} Show the leaderboard.
POST /db_research_tool {} Open the research database tool.
POST /economic_stats {} Show macro stats.
POST /most_cash_report {} Most-cash leaderboard.
POST /largest_market_cap {} Market-cap leaderboard.
POST /largest_tax_losses {} Largest carried-forward tax losses.
POST /industry_summary {} Industry summary stats.
POST /industry_projections {} Industry growth / decline projections.
POST /view_corp_assets_for_sale {} All corporate assets currently for sale.

Settings / toggles

[edit]

These all open a settings menu / cycle the toggle. No params except where noted.

Endpoint Body Description
POST /supp_earn_select {} Suppress earnings-warnings menu.
POST /currency_select {} Currency selection.
POST /supp_warn_select {} Suppress-warnings menu.
POST /suppress_select {} Suppress messages / alerts menu.
POST /autosave_select {} Autosave settings.
POST /exercise_select {} Auto-exercise options settings.
POST /sweep_select {} Cash-sweep settings.
POST /makedelivery_select {} Make-delivery settings (commodity/crypto).
POST /takedelivery_select {} Take-delivery settings.
POST /tooltips_select {} Tooltip enable / disable.
POST /shareholdergraph_select {} Shareholder-graph display settings.
POST /disablehotkeys_select {} Hotkey enable / disable.
POST /autoadd_select {} Auto-add-to-watchlist settings.
POST /set_chart_type { id } Set chart style. id = chart type enum.
POST /set_locale { str } Set UI locale. str = locale code (e.g. "en-US", "ja-JP").

Cheat menu

[edit]
Endpoint Body Description
POST /cheat_disable {} Lock cheats off.
POST /cheat_disable_lawsuits {} Disable lawsuits entirely.
POST /cheat_merger_info {} Reveal hidden merger info.
POST /cheat_earnings_info {} Reveal earnings forecasts and hidden financials.
POST /cheat_add_cash {} Add a fixed amount of cash. (No "set cash to N" - this is fixed.)

Modals

[edit]
Endpoint Body Description
POST /close_modal {} Close the current modal.
POST /modal_result { answer } or { str } Submit the player's modal response. Use answer for numeric / choice modals; str for text-entry modals.

Tutorial

[edit]
Endpoint Body Description
POST /set_tutorial_step { id } Jump tutorial to a step. id = step number.
POST /set_tutorial_enabled { id } Enable / disable tutorial. id = 1 on, 0 off.

Price alerts

[edit]
Endpoint Body Description
POST /show_price_alerts {} Open the alerts management dialog.
POST /create_price_alert { str } direction|targetPrice"; direction is "up" or "down".
POST /delete_price_alert { id } Delete one alert. id = alert slot index.

CustomData

[edit]
Endpoint Body Description
POST /set_custom_data JSON object Shallow-merge mod data into customData. See §CustomData.

gameState reference

[edit]

GET /gamestate returns one JSON object - ~90 top-level fields - that is the complete read model. In a frontend mod, subscribe to only the fields you need so you only re-render on those:

const cash = useGameStore(s => s.gameState.cash);
const year = useGameStore(s => s.gameState.currentYear);
Group Fields
Player cash, otherAssets, totalAssets, totalDebt, netWorth, playerId, playerName, chairedCompanyId
Clock / session currentYear, currentQuarter, currentMonth, currentDay, currentTime, nextEarningsDate, gameLoaded, gameOver, readyToRestart, isTickerRunning, tickSpeed, splashScreenPlayed
Active selection activeEntityNum, activeEntityName, activeEntitySymbol, activeIndustryNum, activeIndustryId, actingAsId, actingAsName, actingAsSymbol, actingAsIndustryId, actingAs
Modal state modalType, modalText, modalTitle, modalDefault, modalFilter
Collections allCompanies, allPlayers, allSecurities, allIndustries, controlledCompanies, tickerQueue, streamingQuotesList, newsHeadlines, trendingNews, navHistory, navPointerIndex
Active entity detail activeEntityData, activeEntityFinancials (deep: cash/assets/debt/equity/EPS/cash-flow/…), activeEntityPlayerFinancials, financialProfile, cashflowProjection, portfolio, portHoldings, swapsPortfolio, optionsList, commodityList, shareholdersList, researchReport, advisorySummary, earningsReport, advances, loansReport, myCorporationsReport
Market reports whoOwns*Report (futures, physical commodities, options, swaps, stocks, investment contracts), industrySummaryReport, industryProjectionReport, industryGrowthRatesReport, mostMarketShareReport, mostTaxLossReport, mostCashReport, mostMarketCapReport, economicDataReport, interestRatesReport, whosAheadReport
Settings suppEarnSetting, suppWarnSetting, supPopupSetting, autosaveSetting, exerciseItSetting, sweepSetting, makeDeliverySetting, takeDeliverySetting, tooltipsSetting, shareholderGraphSetting, unethicalSetting, disableHotkeysSetting, chartType, locale
Tutorial / misc tutorialEnabled, tutorialStep, alerts, frozenAllLoans, dlrSign, euro, customData (§CustomData)

The exact shape can change between versions - the authoritative move is to GET /gamestate from a running game. Ctrl+Shift+I opens DevTools; gameStore.getState().gameState in the console dumps the live object.

CustomData - your persistent storage

[edit]

customData is a free-form JSON object that rides along inside the save file. The engine never reads it for simulation - it stores it verbatim, writes it into the .WSR/.DAT save, restores it on load, and exposes it on every gameState as gameState.customData.

This is the supported way for a mod to persist its own state per-save.

Read

[edit]
const customData = useGameStore(s => s.gameState?.customData ?? {});
const myStuff = customData.myModKey;

Write

[edit]
import { setCustomData } from '../api.js';
await setCustomData({ myModKey: { highScore: 42, notes: 'hello' } });

Semantics

[edit]
  • Shallow merge. POST /set_custom_data merges into the existing customData at the top level - incoming top-level keys overwrite, others are preserved. Namespace your data under one top-level key (e.g. myModKey) and write that whole sub-object each time.
  • Round-trips. New value comes back on the next gameState broadcast.
  • Inert. Cannot change cash, prices, or any engine value - only action endpoints can (§REST API).
  • Per-save. Saved/loaded with the game; a fresh New Game starts empty.

Reserved top-level keys

[edit]

The base UI already stores a few things in customData. Don't reuse these keys:

Key Used by Contents
notes Notes modal free-text notes
navHistory Navigation system saved nav history
(DB search criteria) Database Search view saved filters / sort
(holdings prefs) Portfolio Holdings table saved filters / sort / hide-subsidiaries

Pick a unique key for your mod (e.g. customData["mymod:state"]).

Limits - what you can't do

[edit]
  • No raw memory / pointer writes. Only curated endpoints. The only free-form write is customData, and it's inert.
  • No intercepting the update loop. WS delivers a snapshot. Read and react; you cannot mutate values mid-update or inject into serialization.
  • No new engine behavior without recompiling. New mechanic / event type / endpoint = edit PowerBASIC source (src/main/wsr/*.inc, ~178K lines), rebuild wsr.exe (PB 9.80) and usually ui.dll (VS C++). That's engine development, not modding.
  • No editing binary game data. Saves and most .DAT/.PRM are proprietary binary or encoded. CORPNAME.DAT is the one safe exception.

Workflow

[edit]

Edit / test

[edit]
  1. Edit a file under js/, css/, assets/, or CORPNAME.DAT.
  2. Restart the game (no hot reload).
  3. Ctrl+Shift+I for DevTools - console errors, the gameStore, network tab against the REST API.

Packaging for others

[edit]

A mod is a set of replacement files. Simplest distribution: a zip mirroring the resources/app/ structure:

my-dark-theme/
├── css/variables.css
└── css/theme.css

Install instructions: "drop these into resources/app/, overwriting."

Good practice:

  • Keep mods additive where possible - new component files wired in with one import line, not rewrites of core files. Two additive mods are less likely to collide.
  • Document which base files you replace.
  • Tell users that Steam's "verify integrity of game files" reverts changes (their uninstall path) and to back up originals.

Steam Workshop

[edit]

WSR ships with Steam Workshop support for sharing mods. Subscribed Workshop items are downloaded by Steam, copied into a per-user staging directory at startup, and overlaid on top of the install directory at the file-protocol layer. The install directory is never modified - Steam's "verify integrity of game files" stays a no-op for Workshop content, and mods survive a full reinstall.

How it works for players

[edit]
  1. Open the game's Workshop page in Steam, subscribe to a mod.
  2. Steam downloads the mod content to its UGC folder.
  3. Next time you launch Wall Street Raider, the game mirrors that folder into %LOCALAPPDATA%\Wall Street Raider\workshop\installed\<id>\ and builds a merged overlay at %LOCALAPPDATA%\Wall Street Raider\workshop\overlay\.
  4. When the renderer asks for a file (js/locale/de.js, css/variables.css, etc.), Electron's file-protocol interceptor checks the overlay first and falls through to the install dir if the file isn't there.

In-game: Main Menu → Workshop opens the subscribed-mods list. Each row shows the mod's current Steam state with color coding:

  • Installed (green) - downloaded, will apply on next game restart
  • Downloading… / Queued (cyan) - Steam is fetching the content; wait, then restart
  • Subscribed (not downloaded) (yellow) - Steam hasn't started downloading; click the Force Download button on that row to prompt it
  • Update available (yellow) - newer version exists on Workshop; click Force Download

The list auto-refreshes every 3 seconds while the modal is open, so status changes are visible without closing and reopening.

Other modal buttons:

  • Browse Workshop in Steam - opens the WSR Workshop page in the Steam overlay (or your default browser if the overlay is off)
  • Publish a Mod… - launches the bundled Mod Uploader (see §Publishing a mod)
  • Unsubscribe (per row) - removes the subscription; effective on next restart

Template:Note

What can be in a Workshop mod

[edit]

A Workshop mod is a zip of replacement files mirroring the resources/app/ tree. Everything listed earlier in this guide is fair game:

  • css/variables.css, css/theme.css, css/components/*.css (themes)
  • js/locale/<code>.js plus an updated js/locale/localeManager.js (locales)
  • js/data/hints/*.js (contextual hints)
  • assets/*.png / *.jpg / *.mp4 / *.ico (asset reskins, same filenames)
  • assets/help/wsrbook.htm (rewritten manual)
  • CORPNAME.DAT (renamed companies / tickers)
  • New Preact component files plus a one-line import in js/app.js or a parent component (additive UI mods are the cleanest way to avoid collisions with other mods)

You cannot mod through Workshop:

  • wsr.exe or ui.dll (binaries, blocked from the overlay for safety)
  • Engine simulation rules (engine doesn't read mods)
  • Save format

Conflict resolution

[edit]

If two subscribed mods both ship a css/variables.css, last-loaded wins and a warning is logged to wsr-stdio.log listing the conflict. There is no in-game enable/disable toggle in v1 - if two mods collide and you don't like the result, unsubscribe from the one whose changes you don't want.

Publishing a mod

[edit]

The WSR Mod Uploader is bundled with the game. Two ways to launch it:

  • From inside WSR: Main Menu → Workshop → Publish a Mod…
  • Direct from your install dir: <install>\mod-uploader\wsr-mod-uploader.exe

The uploader is a thin GUI around ISteamUGC::CreateItem / SubmitItemUpdate. Steam must be running.

Workflow

[edit]
  1. Mod folder - pick the folder containing your mod's files. The picker defaults to <Documents>\WSR Mods\, which is auto-created on first launch. Anything under there shows up as a quick pick. Your folder should mirror the resources/app/ structure (see §What can be in a Workshop mod).
  2. Preview image - pick a PNG or JPG under 1 MB (Workshop limit, enforced client-side). Required - Workshop rejects items without a preview.
  3. Title - 5 to 128 characters. Steam rejects very short titles for public items.
  4. Description - at least 30 characters. Same reason.
  5. Tags - optional. Pick from the preset checkboxes, type your own comma-separated, or both. See §Standard tags.
  6. Visibility - Public / Friends only / Private. Start with Private while you iterate.
  7. Changelog - shown in the Workshop item's revision history. Useful on updates.
  8. Click Publish.

The Publish button stays disabled until folder, preview, title, and description are all valid. The status panel at the bottom streams JSON status lines as Steam processes the upload; success ends with Published! Workshop ID: <number> and an Open in browser button.

.wsrmod-id (updates vs new items)

[edit]

On the first successful publish from a folder, the uploader writes .wsrmod-id into that folder containing the assigned PublishedFileId_t. Subsequent publishes from the same folder detect this file and update the existing Workshop item rather than creating a duplicate. To fork a mod (publish a copy as a new item), delete or move .wsrmod-id out of the folder before publishing.

SteamCMD alternative

[edit]

You can also use SteamCMD's +workshop_build_item with a .vdf if you prefer scripting. Same metadata is required. See Valve's docs.

Standard tags

[edit]

The Wall Street Raider Workshop accepts freeform tags by default - you can type any tag in the uploader's "Custom tags" field. The convention below is what the in-game Workshop browser groups by, so following it makes your mod easier to discover:

Tag What it means
Theme CSS theme / re-skin (palette, fonts, layout tweaks)
Locale Translation pack (full or partial)
Hints New / rewritten contextual hints
Assets Replacement images, videos, icons
Companies Alternate company-name database (CORPNAME.DAT)
UI Extension New Preact components or feature additions
Other Anything that doesn't fit the categories above

Tags are optional. Multiple tags are fine when a mod spans categories (e.g. a "1929 era" mod might tag Theme + Companies + Assets).

Troubleshooting

[edit]

For subscribers (using mods)

[edit]
  • Subscribed mod isn't visible. Re-launch the game - mod changes apply at startup only.
  • Workshop menu says "not available." The game must be launched via Steam. Workshop calls require a logged-in Steam client.
  • Mod is stuck on "Subscribed (not downloaded)" yellow status. Click the Force Download button on that row. Calls ISteamUGC::DownloadItem(id, high_priority=true) to prompt Steam to fetch the content. Watch the status flip to Queued then Downloading… then Installed. If the status doesn't change within ~10 seconds, fully restart the Steam client (Exit, wait, reopen) - subscriptions can fail to sync until Steam reconnects.
  • Mod's files aren't loading. Check %LOCALAPPDATA%\Wall Street Raider\workshop\manifest.json to see which mods were detected and which file paths the overlay owns. If your mod isn't in the mods list, the loader couldn't see its install folder (rare - usually means the Steam content download is still in progress; wait and re-launch).
  • Two mods conflict. Check the same manifest.json for the conflicts array. Last-wins means whichever mod sorted later in the loader's iteration order replaced the conflicting file. Unsubscribe from one of the two.

For mod authors (using the uploader)

[edit]
  • Uploader's Publish button is greyed out. One or more required fields aren't satisfied. Yellow hint text under each field shows what's missing (folder picked, preview image, title at least 5 chars, description at least 30 chars).
  • Upload returns AccessDenied (15) after content reaches 100%. Almost always: the Steamworks Workshop config has unpublished changes. Go to the top-nav Publish tab and push any pending config changes to Steam. If everything's already published, check that Steam Cloud quota is non-zero for this app and that Enable ISteamUGC for file transfer is checked.
  • Upload returns Busy (10) repeatedly. The uploader auto-retries 3 times. If still failing: Workshop config for this app likely isn't fully provisioned (the per-app Workshop service hasn't initialized in Steam's backend yet). Wait 10 minutes after publishing any config changes, then retry.
  • Upload returns InvalidParam (8). One of the metadata fields is rejected. Check the diagnostic {"kind":"params"} line in the uploader's status panel - it shows everything that was sent. Most common cause: title or description too short.
  • Friend's Steam chat opens whenever I publish. Steam shows you as "In-Game: Wall Street Raider" while the uploader's native helper is running, which fires friend-presence notifications. Set your Steam status to Invisible while testing, OR have the friend disable "Open chat when friend signs in" in their Steam notification settings. There's no clean code-level fix.
  • Cleanup: empty / placeholder Workshop items from failed publishes. Each CreateItem succeeds even when SubmitItemUpdate later fails, leaving an empty Workshop item that auto-subscribes you. Visit your Workshop submissions page, click into each empty/no-title item, and delete it (Edit → Delete). Also unsubscribe from them via Main Menu → Workshop in-game.