Application Flow - KobaltBlu/KotOR.js GitHub Wiki

Application Flow

This page describes how KotOR.js starts and how the Electron (desktop) and Web (browser) builds differ. The same launcher, game, and Forge bundles run in both environments; the app detects which it is in and adjusts behavior accordingly.


How the app detects Electron vs Web

The environment is set once at load time:

  • Electron: Pages are loaded from disk via file:// URLs (e.g. file:///path/to/project/dist/launcher/index.html). When window.location.origin === 'file://', the app sets ApplicationProfile.ENV = ApplicationEnvironment.ELECTRON.
  • Web: Pages are served over HTTP/HTTPS (e.g. https://play.swkotor.net/launcher/index.html). When the origin is not file://, the app sets ApplicationProfile.ENV = ApplicationEnvironment.BROWSER.

This check is done in the launcher (src/apps/launcher/index.tsx), in the game (src/apps/game/states/AppState.ts), and in ApplicationProfile.InitEnvironment(). All subsequent code branches on ApplicationProfile.ENV or AppState.env (e.g. file system access, dialogs, window controls).


Electron (desktop) flow

1. Entry point

  • You run main.js from the project root (e.g. via npm run start).
  • main.js does: require('./dist/electron').
  • That loads the compiled Electron main process (built from src/electron/). There is no separate “web entry”; the same main.js is the single desktop entry.

2. Main process

  • src/electron/index.ts runs: it calls Main.setApplicationPath(app.getAppPath()) and Main.main(app).
  • Main.main(app) (in Main.ts):
    • Subscribes to app.on('ready'), app.on('window-all-closed'), app.on('activate').
    • Calls WindowManager.initIPC(ipcMain) to register IPC handlers (see below).
    • On ready: creates a system tray (with Exit), then WindowManager.createLauncherWindow(). On some platforms, if the tray fails, it falls back to only creating the launcher window. Also registers a global shortcut (e.g. Alt+`) to show the launcher.

3. Launcher window

  • WindowManager.createLauncherWindow() creates a LauncherWindow (see LauncherWindow.ts).
  • That opens a BrowserWindow which:
    • Loads file://${ApplicationPath}/dist/launcher/index.html.
    • Injects the preload script: dist/electron/preload.js (from src/electron/preload.ts).
  • The preload script uses contextBridge.exposeInMainWorld so the renderer can call into the main process without direct Node access. It exposes:
    • window.dialoglocateDirectoryDialog, showOpenDialog, showSaveDialog (implemented via ipcRenderer.invoke to main process).
    • window.fs — Node.js fs (e.g. readFile, writeFile, readdir, stat, createReadStream, etc.).
    • window.electronisMac, minimize, maximize, locate_game_directory, launchProfile(profile), openExternal.

Inside the launcher page, React runs and window.location.origin === 'file://' so ApplicationProfile.ENV = ELECTRON. The launcher shows window controls (minimize, maximize, close) and uses window.electron for them and for locating the game directory.

4. Launching a profile (game, Forge, or debugger)

  • When the user clicks Play or Open, the launcher calls window.electron.launchProfile(profile).
  • That sends an IPC message 'launch_profile' to the main process with the selected profile (e.g. game key, Forge, debugger).
  • WindowManager (in WindowManager.ts) has ipcMain.on('launch_profile', ...) which:
    • Creates a new ApplicationWindow with that profile.
    • Hides the launcher window (and optionally shows it again when the app window closes).
  • ApplicationWindow (in ApplicationWindow.ts) creates another BrowserWindow that loads:
    • file://${ApplicationPath}/dist/${profile.launch.path}?key=...
    • e.g. dist/game/index.html?key=kotor or dist/forge/index.html?key=forge.
  • That window also gets the same preload script, so window.dialog, window.fs, and window.electron are available. The game or Forge again see file:// and use ELECTRON behavior (Node fs, IPC dialogs, etc.).

5. IPC handlers (main process)

  • config-changed — Broadcast to all app windows and the launcher.
  • win-minimize / win-maximize — Minimize or maximize the focused window.
  • locate-game-directory — Show a native “Open folder” dialog; return the selected path.
  • open-file-dialog / save-file-dialog — Native open/save file dialogs.
  • launch_profile — Create a new ApplicationWindow for the given profile (see above).
  • launch_executable — Run the retail game executable (e.g. via child_process.execFile), used when the user chooses “Retail” in the launcher.

When the last ApplicationWindow is closed, the main process shows the launcher again (see ApplicationWindow closed handler). The app quits when all windows are closed (except on macOS, where it typically stays in the dock until the user quits).


Web (browser) flow

1. Entry point

  • There is no main.js and no Node/Electron process.
  • The user opens the site in a browser (e.g. https://play.swkotor.net/). The server serves the same built files from dist/ (e.g. dist/launcher/index.html, dist/game/index.html, dist/forge/index.html).

2. Launcher

  • The user navigates to the launcher (or the site’s default route points there).
  • The launcher bundle loads; window.location.origin is not file://, so ApplicationProfile.ENV = ApplicationEnvironment.BROWSER.
  • The launcher hides the top-right window controls (minimize, maximize, close), since the browser owns the window.
  • window.electron is undefined (no preload in the browser). Any call to window.electron would throw; the launcher only uses it when ApplicationProfile.ENV === ELECTRON.
  • “Locate game directory” in the launcher does not use the Electron dialog; the web path for that is currently commented out (File System Access API showDirectoryPicker could be used in the future).

3. Launching a profile

  • When the user clicks Play or Open, the launcher does not call window.electron.launchProfile. Instead it does:
    • window.open(\/${profile.launch.path}?key=${profile.key}`)`
  • So the user is taken to e.g. /game/index.html?key=kotor or /forge/index.html?key=forge in the same tab (or a new one, depending on window.open behavior). No IPC; normal browser navigation.

4. Game or Forge in the browser

  • The game or Forge loads with a normal https:// origin, so again ApplicationProfile.ENV = BROWSER (and in the game, AppState.env = BROWSER).
  • File system:
    • Electron uses Node fs and a string path (from “Locate game directory” via IPC).
    • Web uses the File System Access API: the user clicks “Grant Access” in the Grant Access modal, and the app calls window.showDirectoryPicker({ mode: 'readwrite' }). The result is a FileSystemDirectoryHandle (and possibly file handles), which is stored and used for reading/writing game files. No Node fs; no path strings.
  • Dialogs:
    • Electron: Open/save/locate use IPC to the main process, which shows native dialogs.
    • Web: Only the browser’s directory picker is used for the game directory; other dialogs may be limited or use browser/HTML behavior.
  • Window controls: In the browser there are no custom min/max/close; the browser tab/window is controlled by the user.

So: same codebase, same bundles; only the origin and presence of window.electron change, and the app branches on ELECTRON vs BROWSER for file system, dialogs, and UI.


Differences at a glance

Aspect Electron (desktop) Web (browser)
Entry main.jsrequire('./dist/electron') → Main process User opens site URL; no main process
Launcher URL file://.../dist/launcher/index.html https://.../launcher/index.html (or root)
Environment window.location.origin === 'file://'ELECTRON Any other origin → BROWSER
Window controls Custom min/max/close; tray; global shortcut None; browser chrome only
Launching app window.electron.launchProfile(profile) → IPC → new BrowserWindow with `file://.../dist/game forge
Game directory User clicks “Locate” → IPC → native folder dialog → string path → Node fs User clicks “Grant Access” → showDirectoryPicker()FileSystemDirectoryHandle; no Node fs
Open/save dialogs IPC to main process → native dialogs Not available (or limited); Forge/game rely on directory handle in browser
Preload / window.electron Preload script exposes dialog, fs, electron No preload; window.electron is undefined; code must check ENV before using it
File paths Path strings and Node fs (e.g. GameFileSystem, Forge) Handles only (e.g. ApplicationProfile.directoryHandle); no path strings for game data
Multi-window Launcher + multiple app windows (game, Forge, debugger) Typically one tab; “launch” = navigate to another page

Summary

  • Electron: One process started by main.js; it opens the launcher and every app window as local file:// pages with a preload that exposes dialog, fs, and electron. File access uses paths and Node fs; dialogs are native via IPC.
  • Web: No main process; the user loads the same HTML/JS from a web server. Environment is BROWSER; no window.electron; launch = navigation; file access uses the File System Access API and directory/file handles.

The Project Directory Structure and Browser Support wiki pages give more context on the repo layout and required browser features for the web build.