Demo / attract mode (shareware)¶
This page documents the classic shareware demo/attract loop implemented in
crimsonland.exe. The same code exists in the v1.9.93 codebase but is normally
gated by game_is_full_version() (the DRM-free builds don’t enter the loop on
their own).
The demo loop is useful for reimplementation because it exercises:
- Gameplay state
9(creatures/projectiles/players) without needing menu work. - A deterministic set of setup variants (
demo_setup_variant_*). - A self-contained “upsell” overlay that drives a timer and transitions back to
menu state
0(classic behavior; out of scope for our rewrite, since the storefront is defunct).
Entry points¶
Boot handoff: logos → demo¶
In the logo sequence, once the logo timer passes the theme trigger (~14s), the shareware build:
- mutes the intro track
- plays
music_track_crimsonquest_id - calls
demo_mode_start()(0x00403390)
Full builds instead play music_track_crimson_theme_id and proceed to the menu.
See: docs/boot-sequence.md (handoff section).
Runtime entry: trial/attract trigger¶
The main loop (console_hotkey_update) contains a shareware-only path that
starts demo_mode_start() and switches music to shortie_monk (exact trigger
still TBD; see decompile around 0x0040cf06).
Core loop model¶
At a high level:
- Demo mode runs in gameplay state
9. demo_mode_activegates HUD and input behavior.demo_mode_start()resets the session and picks a setup variant.- A per-frame overlay (
demo_purchase_screen_update) is rendered on top and can transition to menu state0.
Main loop integration (state 9)¶
In the per-frame dispatcher:
- If
game_state_id == 9: - If
demo_purchase_screen_activeis0: rungameplay_update_and_render(). - Else: skip gameplay and clear the screen.
- If
demo_mode_active != 0: always rundemo_purchase_screen_update()on top.
This is why the demo cycle includes a “purchase interstitial” variant: it flips
demo_purchase_screen_active to suppress gameplay and show only the upsell
screen for a fixed time. In the rewrite, the interstitial + upsell overlay
are intentionally skipped; the loop cycles gameplay variants and returns to
menu.
Timing: quest_spawn_timeline + demo_time_limit_ms¶
Two globals define the cycle timing:
quest_spawn_timeline(0x00486fd0): a generic “mode timeline” counter (ms). Reset to0bydemo_mode_start().demo_time_limit_ms(0x004712f0): demo timer limit used as:- ~4–5s per gameplay variant (
4000/5000) - ~10s for the interstitial (
10000) - ~16s when the user triggers the purchase screen (
16000)
Mode-specific update hooks use these:
survival_update()andrush_mode_update():- if
demo_mode_activeandquest_spawn_timeline > demo_time_limit_ms: calldemo_mode_start(). demo_purchase_screen_update():- when the purchase screen is active, it increments
quest_spawn_timelineitself and restarts viademo_mode_start()when it exceedsdemo_time_limit_ms.
demo_mode_start (0x00403390)¶
High-confidence pseudocode (decompile + callsite xrefs):
if (game_state_id != 9) game_state_set(9);
demo_purchase_screen_active = 0;
demo_mode_active = 1;
gameplay_reset_state();
config.game_mode = 1; // survival
switch (demo_variant_index) {
case 0: demo_setup_variant_0(); break;
case 1: demo_setup_variant_1(); break;
case 2: demo_setup_variant_2(); break;
case 3: demo_setup_variant_3(); break;
case 4: demo_setup_variant_0(); break;
case 5: demo_purchase_interstitial_begin(); break; // 0x00403370
}
quest_spawn_timeline = 0;
screen_fade_ramp_flag = 0; // DAT_0048702c
demo_variant_index = (demo_variant_index + 1) % 6;
Setup variants (demo_setup_variant_*)¶
The variants are small, deterministic setup functions that:
- set
config.player_count - optionally call
terrain_generate(desc) - spawn a fixed set of creatures via
creature_spawn_template(spawn_id, pos_xy, heading) - position players and assign starting weapons
- set
demo_time_limit_ms
Variant 0 — demo_setup_variant_0 (0x00402ed0)¶
player_count = 2demo_time_limit_ms = 4000- Spawns template
0x38in two columns (x≈128/192 and x≈798/862), y=256..1632 step 80. - Player positions: P1
(448,384), P2(546,654). - Weapon:
0x0b(Rocket Launcher) for both. - Uses
heading = -100.0for spawns (likely a sentinel; exact semantics TBD).
Variant 1 — demo_setup_variant_1 (0x004030f0)¶
player_count = 2terrain_generate(&DAT_00484914)(this points into the quest metadata table; seedocs/crimsonland-exe/terrain.md).demo_time_limit_ms = 5000- Spawns 20× template
0x34plus ~13× template0x35at random positions: x = rand()%200 + 32(or%30 + 32for template0x35)y = rand()%899 + 64heading = -100.0- Player positions: P1
(490,448), P2(480,576). - Weapon:
0x05(Gauss Gun) for both. - Forces
bonus_weapon_power_up_timer = 15.0.
Variant 2 — demo_setup_variant_2 (0x00402fe0)¶
player_count = 1demo_time_limit_ms = 5000- Spawns template
0x41in columns at y=128..788 step 60, with x offsets: x = 32,128,-64,768(alternating by row parity)heading = -100.0- Weapon:
0x15(Ion Minigun).
Variant 3 — demo_setup_variant_3 (0x00403250)¶
player_count = 1terrain_generate(&quest_selected_meta)(uses the currently selected quest descriptor).demo_time_limit_ms = 4000- Spawns random templates
0x24and0x25at positions similar to variant 1. - Player position:
(512,512). - Weapon:
0x12(Pulse Gun). - Uses
heading = 0.0for spawns.
Variant 5 — purchase interstitial — demo_purchase_interstitial_begin (0x00403370)¶
demo_time_limit_ms = 10000demo_purchase_screen_active = 1(so the main loop skips gameplay and only renders the upsell overlay)
Rewrite note: Out of scope (storefront defunct). The rewrite omits this variant and the upsell overlay entirely.
Upsell overlay (demo_purchase_screen_update / 0x0040b740)¶
Rewrite note: Out of scope (storefront defunct). We only implement the demo loop; no purchase screen or "Buy Now" flow.
This runs whenever demo_mode_active != 0:
- When
demo_purchase_screen_active == 0 - Shows a rotating “Want more …” message (
demo_upsell_message_index). -
On user input, activates the full purchase screen:
demo_purchase_screen_active = 1demo_time_limit_ms = 16000
-
When
demo_purchase_screen_active != 0 - Renders the full-screen purchase UI (backplasma + mockup + logo, feature list).
- Buttons:
Purchase: setsshareware_offer_seen_latchand opens the purchase URL (then requests quit).Maybe later: starts a transition back to menu state0and resumescrimson_theme.
- Increments
quest_spawn_timelineitself and restarts the demo viademo_mode_start()oncequest_spawn_timeline > demo_time_limit_ms.
Exiting demo mode (returning to menu)¶
When the upsell overlay triggers a transition to state 0, the UI transition
manager (ui_elements_update_and_render @ 0x0041a530) does two demo-specific
things:
- If
demo_mode_activeandgame_state_pending == 0, it callsterrain_generate_random()right beforegame_state_set(0)(so the menu background changes). - Once the menu transition finishes, it clears
demo_mode_activeand reloads presets (config_load_presets()).
Player behavior in demo mode (autoplay)¶
player_update @ 0x004136b0 treats demo_mode_active as a control-scheme
override and routes through the same logic as the “auto-aim” control mode:
- Maintains
player_auto_target(player+0x2fc) as the nearest living creature with a 64-unit hysteresis. - Aiming is biased by arena center:
- if no valid target: aim away from
(512,512) - if within 300 units of center: aim at the target; otherwise aim relative to center
The exact firing behavior in this mode still needs confirmation (movement/aim are clearly autonomous; the fire gating should be verified with a runtime probe).