Jump to content

Modding

From Wall Street Raider Wiki
Revision as of 12:25, 15 May 2026 by 74.83.75.1 (talk) (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...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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 the PowerBASIC game engine itself (the simulation logic), because that requires recompiling wsr.exe from source with a PowerBASIC 9.80 compiler. Everything below is doable with nothing more than a text editor.

How the game is built (why modding is easy)

WSR is three layers:

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

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 asar: false, which means after installation every file is still a normal editable file on disk - nothing is archived or compressed.

That means: edit a file, restart the game, see your change.

Where the files live

During development (running from source)

electron/
├── index.html            # app entry point
├── main.js               # Electron main process
├── js/                   # all frontend code
├── css/                  # all styling
├── assets/               # images, videos, icons, help
└── ...

In an installed / Steam copy

After packing, everything lands under the install directory in:

<install>/resources/app/
├── 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)
└── ...

resources/app/js, resources/app/css, and resources/app/assets are copied verbatim from the electron/ source tree at pack time (see the extraFiles block in electron/package.json). Editing them in an installed copy works exactly the same as editing them in source.

Template:Note

What you can mod

Frontend code - resources/app/js/

The entire UI is here, as ES modules. Highlights:

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

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

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 freely:

  • Change layout, text, colors, behavior of any existing component.
  • Add brand-new components and wire them into app.js or any parent.
  • Add new hotkeys (search hotkey across js/ - the handling lives in app.js and HotkeyButtonBar.js).
  • Add new derived/computed displays from gameState (see §6).
  • Add new buttons that call any API endpoint (see §5).

Libraries are vendored in js/lib/ (preact.standalone.module.js, zustand.module.js, tailwind.module.js, fast-json-patch.module.js) - no npm install needed to develop against them.

Styling - resources/app/css/

CSS is modular and loaded at runtime. The structure:

css/
├── style.css             # the single file index.html links - imports the rest
├── base.css              # resets / base element styles
├── variables.css         # CSS custom properties (THE place for theme colors)
├── 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 utility classes are also available (loaded via js/lib/tailwind.module.js), so most components use Tailwind utilities inline plus the modular CSS for anything custom.

To re-theme the game, start with css/variables.css - 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.

Assets - resources/app/assets/

All images, videos, icons, and the loading animation:

Asset type Files
Background videos The many *.mp4 files (Wall Street footage, industry b-roll, etc.)
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 to reskin the game. (Keep filenames identical - components reference them by name.)

Help content - resources/app/assets/help/

The in-game manual is wsrbook.htm - a single ~1 MB HTML file. It is plain HTML; edit it directly to rewrite, correct, or extend the manual. The *.jpg files in the same folder are diagrams embedded by the manual (advanopt.jpg, alerts.jpg, automate.jpg, buyloans.jpg, capcont.jpg, treasury.jpg, rlogotxt.jpg).

electron/UI_HELP_CATALOG.txt is a developer reference catalog of help sections - useful to read when navigating the manual.

Translations / locales - resources/app/js/locale/

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.

How it works (js/locale/localeManager.js):

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

To add or improve a language:

  1. Create js/locale/<your-locale>.js exporting a dictionary object, e.g.
export const frFR = {
    "New Game": "Nouvelle partie",
    "Buy Stock": "Acheter des actions",
    // … one entry per source string you want translated
};
  1. In js/locale/localeManager.js, import it and add an entry to LANGUAGE_OPTIONS:
import { frFR } from './fr-FR.js';
// …
'fr-FR': { name: 'Français', dictionary: frFR },
  1. The chosen locale is persisted by the engine (in CFIG.WSR) and exposed as gameState.locale; the UI switches via the /set_locale endpoint.

Template:Note

In-game hints - resources/app/js/data/hints/

The contextual hint system is data-driven. Hint files are grouped by view:

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

Each file exports an array of hint objects:

{
    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>`,
}

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

Company names - CORPNAME.DAT (plain text, directly editable)

resources/app/CORPNAME.DAT is the company-name database, and unlike most .DAT files it is 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 symbol | company name.

It is read by the engine's GetCorpNames 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 | delimiters and spacing. CORPNAME.ORI is the pristine original - keep it as your reference/backup.

Other game-data files (limited / not practical)

These ship in resources/app/ but are not practically moddable without the original tooling:

File(s) Content Why limited
QUOTEWSR.DAT "Quote of the day" text Encrypted/encoded; plaintext source is quotehs.prm but the engine reads the encoded .DAT
MSCNEWS.DAT News & scenario headline text (hundreds of paragraphs) Encoded; MSCNEWS.PRM is a near-plaintext source but the engine reads the encoded .DAT
scenwin1-4.prm Scenario window text Engine-internal format
GAME01.DAT … GAME50.DAT Save slots / scenario data Proprietary binary save format
WSR101.DAT, REGINFO.DAT, etc. Engine data Proprietary binary

Treat these as read-only. (Company names are the one genuinely editable game-data file - see above.)

The runtime: how the UI talks to the engine

When wsr.exe starts, ui.dll binds a REST server and a WebSocket server on OS-assigned ephemeral ports, and writes them to:

%LOCALAPPDATA%\Wall Street Raider\runtime.json
{ "pid": 12345, "rest_port": 54321, "ws_port": 54322 }
  • Electron's main process reads that file and hands the ports to the renderer via the bridge-ports IPC message.
  • js/api.js builds http://127.0.0.1:<rest_port> and ws://127.0.0.1:<ws_port> from it.
  • The WebSocket pushes game-state updates (full snapshot on connect, then JSON-patch diffs on /game_state_patch). When WS is connected, HTTP polling is suspended; if WS drops, app.js falls back to polling GET /gamestate (200 ms on the menu, 50 ms in-game).

For a mod, you almost never touch this plumbing directly - you call the functions in api.js and read from the gameStore.

The API - everything you can make the game do

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.

Calling the API from a mod

js/api.js already wraps every endpoint in a named async function. Import and call:

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

await api.buyStock(companyId);            // buy
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

Under the hood there are four request shapes (also exported, if you need to hit an endpoint that has no named wrapper yet):

Helper Body sent 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; actingAsId runs the action as a company you control
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

The actingAsId 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. 0 means "as the player."

Endpoint reference

Read (GET):

Endpoint Returns
/status health check
/gamestate full game-state JSON (see §6)
/quote quote of the day
/asset_chart price history for an asset
/database_data database/search dataset
/ownership_tree corporate ownership tree
/subsidiaries_tree subsidiary tree

Game / session control: /newgame, /loadgame, /load_specific_save (str), /savegame, /savegameas (str), /exit_game, /check_scoreboard, /start_ticker, /run_ticker, /stop_ticker, /set_ticker_speed (id), /ticker_advance, /clear_event_string, /splash_screen_played.

Trading - stocks & bonds: /buy_stock, /sell_stock, /short_stock, /cover_short_stock, /buy_corporate_bond, /sell_corporate_bond, /buy_long_govt_bonds, /sell_long_govt_bonds, /buy_short_govt_bonds, /sell_short_govt_bonds.

Trading - commodities & crypto: /buy_commodity_futures, /sell_commodity_futures, /close_long_commodity_futures_by_slot, /short_commodity_futures, /cover_short_commodity_futures, /cover_short_commodity_futures_by_slot, /buy_physical_commodity, /sell_physical_commodity, /buy_physical_crypto, /sell_physical_crypto, /buy_crypto_futures, /sell_crypto_futures.

Trading - options: /buy_calls, /sell_calls, /buy_puts, /sell_puts, /advanced_options_trading, /exercise_call_options_early, /exercise_put_options_early.

Corporate management: /prepay_taxes, /elect_ceo, /resign_as_ceo, /change_managers, /set_dividend, /set_productivity, /set_growth_rate, /restructure, /buy_corporate_assets, /sell_corporate_assets, /offer_corporate_assets_for_sale, /view_for_sale_items, /sell_subsidiary_stock, /rebrand, /toggle_company_autopilot, /toggle_global_autopilot, /become_etf_advisor, /set_advisory_fee, /decrease_earnings, /increase_earnings.

M&A / corporate finance: /merger, /greenmail, /lbo, /startup, /capital_contribution, /public_stock_offering, /private_stock_offering, /issue_new_corp_bonds, /redeem_corp_bonds, /extraordinary_dividend, /tax_free_liquidation, /taxable_liquidation, /spin_off, /split_stock, /reverse_split_stock.

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

Legal / dirty tricks: /change_law_firm, /antitrust_lawsuit, /harrassing_lawsuit, /spread_rumors.

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

Settings (toggles): /supp_earn_select, /currency_select, /supp_warn_select, /suppress_select, /autosave_select, /exercise_select, /sweep_select, /makedelivery_select, /takedelivery_select, /tooltips_select, /shareholdergraph_select, /disablehotkeys_select, /autoadd_select, /set_chart_type (id), /set_locale (str).

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

Modals: /close_modal, /modal_result.

Tutorial: /set_tutorial_step (id), /set_tutorial_enabled (id).

Price alerts: /create_price_alert, /show_price_alerts, /delete_price_alert.

CustomData: /set_custom_data - see §7.

Template:Note

The gameState - everything you can read

GET /gamestate (or api.getGameState(), or the live gameStore) 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:

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

Major fields include:

  • 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 (a deep object: cash, assets, debt, equity, EPS, cash-flow, etc.), 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, and customData (see §7).

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

The CustomData API - your persistent storage

customData 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 .WSR/.DAT save on save, and restores it on load. It is exposed back to you on every gameState as gameState.customData.

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

Reading

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

Writing

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

Important semantics:

  • Shallow merge. POST /set_custom_data merges your object into the existing customData at the top level - incoming top-level keys overwrite, other keys are preserved. So namespace your data under one top-level key (e.g. myModKey) and write that whole sub-object each time.
  • It round-trips. After you write, the new value comes back on the next gameState broadcast as gameState.customData.
  • 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 §5.
  • It is per-save. It is saved and loaded with the game; a fresh "New Game" starts with empty customData.

Keys already in use by the base game

The stock UI already stores a few things in customData - do not reuse these top-level keys for your own data:

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

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

What you cannot do (today)

So expectations are clear:

  • 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 customData, and it is inert (§7).
  • 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 (src/main/wsr/*.inc, ~178K lines). Adding a genuinely new game mechanic, a new event type, or a new endpoint means editing that source and rebuilding wsr.exe (PowerBASIC 9.80) and usually ui.dll (Visual Studio C++). That is engine development, not modding.
  • No editing binary game data. Saves and most .DAT/.PRM files are proprietary binary or encoded formats. CORPNAME.DAT (plain text) is the one exception you can safely edit (§3.7).

Workflow & sharing

Editing and testing

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

Packaging a mod 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 resources/app/ structure, e.g.:

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

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

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.
  • 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.

Quick reference

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/feature New file in js/components/, import it in 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 Edit CORPNAME.DAT (plain text), starts on next New Game
Make the game do something Call an api.js function (§5) - ~150 endpoints
Read game state useGameStore(s => s.gameState.<field>) or GET /gamestate (§6)
Persist my mod's own data in saves setCustomData({ myKey: … }) (§7)
Change a core simulation rule Not moddable - requires engine source + recompile