DOOM running entirely on the ServiceNow platform.
Every game asset, every byte of engine code, and every line of backend logic is stored in and served from native ServiceNow objects on a Personal Developer Instance. No external servers. No CDNs. Just ServiceNow doing things it was never designed to do.
Browser ServiceNow PDI
+-----------------------+ +--------------------------------+
| Processor (sndoom.do)|<-------->| sys_processor (raw HTML) |
| +------------------+ | | |
| | HTML5 Canvas | | | Attachments on sys_ui_page: |
| | DOOM @ 60+ FPS | | XHR/ | - dwasm_index.wasm (1.9MB) |
| +------------------+ | fetch() | - dwasm_index.data (4.5MB) |
| +------------------+ |<-------->| - dwasm_index.js (198KB) |
| | OPL2 Audio | | | - bootstrap.js (15KB) |
| | WebAudio API | | | |
| +------------------+ | | Scripted REST API: |
| +------------------+ | fetch() | GET /health |
| | Leaderboard (F1) | |<-------->| GET /leaderboard |
| +------------------+ | | POST /leaderboard |
| Session Keepalive | | |
| (15 min interval) | | Script Include: |
+-----------------------+ | SNDoom_Leaderboard |
| |
| Table: u_x_sndoom_score |
+--------------------------------+
- Full DOOM gameplay -- shareware episode (Knee-Deep in the Dead), all nine levels
- OPL2 synthesized music and sound effects via Dwasm/PrBoomX and SDL2_mixer
- Leaderboard with server-side persistence (Scripted REST API + custom table)
- One-command deployment to any ServiceNow PDI via
tools/install.sh - Processor-based serving -- works for all users, not just admins
- Session keepalive -- 15-minute heartbeat prevents the 60-90 minute session timeout
- Save/load via Emscripten IDBFS (browser IndexedDB persistence)
- Mouse capture with pointer lock, widescreen support, 60+ FPS capable
- ServiceNow-themed Easter eggs in the browser console
| Tool | Purpose | Install |
|---|---|---|
| ServiceNow PDI | Target platform | Free at developer.servicenow.com |
| Emscripten | WASM compilation | brew install emscripten |
| CMake | Build system | brew install cmake |
| curl | HTTP requests | Pre-installed on macOS/Linux |
| python3 | JSON processing in scripts | Pre-installed on macOS/Linux |
The build is a two-phase process: first generate the engine resource WAD natively, then compile the full engine to WebAssembly.
1. Download the shareware WAD (if not already present):
curl -o src/engine/dwasm-src/wasm/fs/doom1.wad \
https://distro.ibiblio.org/slitaz/sources/packages/d/doom1.wad2. Native pre-build (generates prboomx.wad, the engine resource file):
cd src/engine/dwasm-src
mkdir -p build-native && cd build-native
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5
make prboomwad -j$(sysctl -n hw.ncpu)
cp prboomx.wad ../wasm/fs/3. Emscripten build (compiles PrBoomX to WebAssembly):
cd src/engine/dwasm-src
mkdir -p build && cd build
emcmake cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5
emmake make -j$(sysctl -n hw.ncpu)This produces three artifacts in build/: index.js (198KB), index.wasm (1.9MB), and index.data (4.5MB).
./tools/install.sh https://<instance>.service-now.com admin 'your_password'Then open https://<instance>.service-now.com/sndoom.do in your browser (logged in).
The installer is idempotent -- run it again to update an existing deployment.
| Key | Action |
|---|---|
| Arrow keys | Move / turn |
| Ctrl | Fire |
| Space | Use / open doors |
| Shift | Run |
| 1-7 | Select weapon |
| Tab | Automap |
| Escape | Menu |
| F1 | Leaderboard (SNDoom addition) |
SNDoom exploits a specific combination of ServiceNow platform features to create a browser environment clean enough to run a WASM game engine:
Non-admin users can't navigate to sn_doom.do (a direct=true UI Page) without ServiceNow's Polaris/UI16 navigator intercepting and redirecting them. The solution is a Processor at sndoom.do that serves raw text/html via g_processor.writeOutput(), bypassing the framework entirely.
The processor also injects the user's CSRF token (gs.getSessionToken()) into the page as window.g_ck, so the bootstrap JS can authenticate all REST API calls (asset downloads, leaderboard) via the X-UserToken header. This eliminates the double-login problem.
Even with direct=true, ServiceNow XML-parses the HTML field of UI Pages before serving them. Any inline JavaScript containing <, &&, or template literals causes a silent blank white page with zero console output. This is the single most disorienting failure mode on the platform.
The fix: never inline JavaScript. All JS is loaded from attachments via <script src>. Styles use CDATA blocks.
Browser hits sndoom.do
|
+-- Processor serves raw HTML with embedded g_ck token
|
+-- Inline XHR loads bootstrap JS from attachment (with X-UserToken)
| |
| +-- Bootstrap defines window.Module with locateFile()
| +-- Sets up leaderboard UI, keepalive timer, Easter eggs
| +-- Patches XMLHttpRequest to inject X-UserToken on all /api/ calls
| +-- Creates <script> to load Emscripten runtime from attachment
| |
| +-- Emscripten runtime (index.js) executes:
| +-- Calls Module.locateFile('index.wasm')
| | -> fetch('/api/now/attachment/<id>/file')
| | -> WebAssembly.instantiate()
| +-- Calls Module.locateFile('index.data')
| | -> fetch('/api/now/attachment/<id>/file')
| | -> Unpack to virtual filesystem
| +-- Mount IDBFS at /dwasm/ for save persistence
| +-- Start DOOM main loop
|
+-- Game running, all assets served from SN Attachment API
Scores are stored in a custom ServiceNow table (u_x_sndoom_score) with seven columns: player name, level, kills, secrets, difficulty, completion time, and computed score. The score formula is kills*100 + secrets*500 - time*2.
The leaderboard UI is built in the bootstrap JS and toggled with F1. It fetches scores from and submits scores to a Scripted REST API at /api/sndoom/sndoom_api/leaderboard. The backend is a Script Include (SNDoom_Leaderboard) that performs GlideRecord CRUD.
The installer creates these platform objects:
| Object | SN Table | Purpose |
|---|---|---|
Processor (sndoom.do) |
sys_processor |
Serves game HTML with injected CSRF token |
UI Page (sn_doom, direct=true) |
sys_ui_page |
HTML shell + attachment host (legacy entry point) |
| Bootstrap JS | sys_attachment |
Module setup, leaderboard UI, XHR patch, Easter eggs |
| Emscripten Runtime | sys_attachment |
WASM loader + preload manifest |
| DOOM Engine | sys_attachment |
PrBoomX compiled to WebAssembly |
| Game Data | sys_attachment |
doom1.wad + prboomx.wad + config |
| SNDoom API | sys_ws_definition |
Scripted REST API at /api/sndoom/sndoom_api |
| GET /health | sys_ws_operation |
Session keepalive endpoint |
| GET /leaderboard | sys_ws_operation |
Fetch top scores |
| POST /leaderboard | sys_ws_operation |
Submit a score |
| SNDoom_Leaderboard | sys_script_include |
Server-side score CRUD |
| SNDoom_Ajax | sys_script_include |
CSRF token provider |
| Score Table | sys_db_object |
u_x_sndoom_score -- 7 custom columns |
Base URL: https://<instance>/api/sndoom/sndoom_api
Session keepalive and health check. Returns:
{"result": {"status": "ok", "game": "SNDoom", "engine": "Dwasm/PrBoomX"}}Query parameters: ?level=E1M1&limit=10 (both optional)
Returns top scores sorted by score descending:
{
"result": {
"status": "ok",
"count": 3,
"scores": [
{
"player_name": "Doomguy",
"level": "E1M1",
"kills": 42,
"secrets": 3,
"difficulty": "Hurt Me Plenty",
"completion_time": 180,
"score": 5700,
"created": "2026-03-23 20:15:00"
}
]
}
}Submit a score. Request body:
{
"player_name": "Doomguy",
"level": "E1M1",
"kills": 42,
"secrets": 3,
"difficulty": "Hurt Me Plenty",
"completion_time": 180,
"score": 5700
}Returns 201 on success with the new record's sys_id.
SNDoom/
+-- README.md # This file
+-- CLAUDE.md # AI agent context
+-- .env # PDI credentials (gitignored)
+-- .gitignore
+-- RFI/ # Cross-agent research requests
| +-- 001-platform-constraints.md # Platform research questions
| +-- 001-response.md # Answers from Principal Researcher
+-- docs/
| +-- architecture.md # Detailed technical design
| +-- development.md # Build guide and dev workflow
| +-- deployment.md # Deployment reference
| +-- api.md # REST API documentation
+-- src/
| +-- servicenow/
| | +-- ui_pages/
| | +-- sn_doom.html # UI Page HTML shell
| | +-- sndoom_dwasm_bootstrap.js # Bootstrap + leaderboard + Easter eggs
| | +-- sndoom_engine.js # v1 engine (retired, no audio)
| +-- engine/
| +-- doom.wasm # v1 WASM binary (retired)
| +-- doom-wasm-src/ # v1 engine source (jacobenget/doom.wasm)
| +-- dwasm-src/ # v2 engine source (GMH-Code/Dwasm)
| +-- build/ # Emscripten output (index.js/wasm/data)
| +-- build-native/ # Native build (rdatawad -> prboomx.wad)
| +-- wasm/fs/ # Preloaded filesystem (WADs + config)
| +-- src/ # PrBoomX C source code
+-- tools/
+-- install.sh # One-command PDI deployment
+-- deploy.sh # Granular deployment helpers (legacy)
SNDoom went through two engine iterations:
A minimal WASM port of DOOM with a clean 14-function interface (10 imports, 4 exports). Provided the initial proof of concept but had no audio support -- DOOM without the E1M1 music felt wrong. The v1 engine and its HTML shell (sn_doom.html + sndoom_engine.js) are preserved in the repo for reference.
The current engine. A full-featured PrBoomX port compiled to WebAssembly via Emscripten. Adds OPL2 synthesized music, sound effects via SDL2_mixer, mouse capture with pointer lock, widescreen support, and 60+ FPS capability. Emscripten handles the virtual filesystem, audio subsystem, and browser integration.
The tradeoff: the Emscripten build produces three artifacts that must be uploaded together (the JS contains a preload manifest with byte offsets into the data file), vs. v1's single self-contained WASM binary. The install script handles this automatically.
SNDoom was built as a creative engineering challenge: can a 1993 first-person shooter run entirely within an enterprise IT service management platform? The answer is yes, with caveats.
The project was developed collaboratively by a human engineer and Claude Code AI agents. Platform constraints were researched via a formal Request for Information (RFI) process informed by prior hands-on ServiceNow platform experience.
Key platform discoveries during development:
direct=trueUI Pages bypass Jelly but NOT XML parsing -- the most counterintuitive gotcha- Processors bypass the framework entirely but can't be created via REST API (ACL-locked) -- solved with a temporary server-side REST endpoint
- ServiceNow injects no JavaScript libraries on
direct=truepages -- a clean browser environment - PDI session timeout is 60-90 minutes of inactivity, but "inactivity" means no server calls -- a client-side game needs its own keepalive
- CSP is instance-global, not per-page, but wasn't enforced on our Zurich Patch 6 PDI
- The Attachment API serves binary files with correct Content-Types -- no workarounds needed
Tested on ServiceNow Zurich Patch 6. The architecture should work on any recent release (Xanadu, Yokohama, Zurich) since it relies on stable platform features (UI Pages, Processors, Attachments, Scripted REST APIs, Script Includes, custom tables). CSP enforcement varies by release and instance configuration -- if WASM is blocked, the Scripted REST API response-header override is the fallback.
DOOM source code is GPL-2.0 (id Software). PrBoomX/Dwasm is GPL-2.0 (GMH-Code). doom.wasm is GPL-2.0 (jacobenget). The shareware DOOM1.WAD is freely distributable per id Software's original terms.