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). Whenwindow.location.origin === 'file://', the app setsApplicationProfile.ENV = ApplicationEnvironment.ELECTRON. - Web: Pages are served over HTTP/HTTPS (e.g.
https://play.swkotor.net/launcher/index.html). When the origin is notfile://, the app setsApplicationProfile.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.jsfrom the project root (e.g. vianpm run start). main.jsdoes:require('./dist/electron').- That loads the compiled Electron main process (built from
src/electron/). There is no separate “web entry”; the samemain.jsis the single desktop entry.
2. Main process
src/electron/index.tsruns: it callsMain.setApplicationPath(app.getAppPath())andMain.main(app).Main.main(app)(inMain.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), thenWindowManager.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.
- Subscribes to
3. Launcher window
WindowManager.createLauncherWindow()creates aLauncherWindow(seeLauncherWindow.ts).- That opens a
BrowserWindowwhich:- Loads
file://${ApplicationPath}/dist/launcher/index.html. - Injects the preload script:
dist/electron/preload.js(fromsrc/electron/preload.ts).
- Loads
- The preload script uses
contextBridge.exposeInMainWorldso the renderer can call into the main process without direct Node access. It exposes:window.dialog—locateDirectoryDialog,showOpenDialog,showSaveDialog(implemented viaipcRenderer.invoketo main process).window.fs— Node.jsfs(e.g. readFile, writeFile, readdir, stat, createReadStream, etc.).window.electron—isMac,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(inWindowManager.ts) hasipcMain.on('launch_profile', ...)which:- Creates a new
ApplicationWindowwith that profile. - Hides the launcher window (and optionally shows it again when the app window closes).
- Creates a new
ApplicationWindow(inApplicationWindow.ts) creates another BrowserWindow that loads:file://${ApplicationPath}/dist/${profile.launch.path}?key=...- e.g.
dist/game/index.html?key=kotorordist/forge/index.html?key=forge.
- That window also gets the same preload script, so
window.dialog,window.fs, andwindow.electronare available. The game or Forge again seefile://and use ELECTRON behavior (Nodefs, 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. viachild_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.jsand 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 fromdist/(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.originis notfile://, soApplicationProfile.ENV = ApplicationEnvironment.BROWSER. - The launcher hides the top-right window controls (minimize, maximize, close), since the browser owns the window.
window.electronis undefined (no preload in the browser). Any call towindow.electronwould throw; the launcher only uses it whenApplicationProfile.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
showDirectoryPickercould 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=kotoror/forge/index.html?key=forgein the same tab (or a new one, depending onwindow.openbehavior). No IPC; normal browser navigation.
4. Game or Forge in the browser
- The game or Forge loads with a normal
https://origin, so againApplicationProfile.ENV = BROWSER(and in the game,AppState.env = BROWSER). - File system:
- Electron uses Node
fsand 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 aFileSystemDirectoryHandle(and possibly file handles), which is stored and used for reading/writing game files. No Nodefs; no path strings.
- Electron uses Node
- 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.js → require('./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 localfile://pages with a preload that exposesdialog,fs, andelectron. File access uses paths and Nodefs; 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.