Files
iw4L/docs/SIM-STEP.md
T
2026-09-13 13:52:33 +03:00

4.4 KiB

How sim::step is grounded in retail — and how it is not

An analogue of sim::step in the IW3 lineage did exist, and a fairly direct one: G_RunFrame. The single TickInput → step → Snapshot funnel is ours. Retail has several entrances into one global mutable world.

The IW3-lineage sources read here are a hint about the shape of the algorithm, not about IW4 offsets.

World step ≈ G_RunFrame

SV_RunFrame()
  → G_RunFrame(svs.time)
       → G_RunFrameForEntity(ent)
            → G_RunMissile / G_RunItem / G_RunCorpse
            → G_RunMover / G_RunClient / G_RunThink
       → ClientEndFrame(...)

SV_RunFrame calls G_RunFrame(svs.time) (sv_main_mp.cpp). Inside, level.time / frametime, XAnim and scripts are updated; then the entities, and the clients at the end. The dispatcher on eType is literally G_RunFrameForEntity (g_main_mp.cpp).

sim::step(world, tick, …) is a normalized G_RunFrame, not an idea from scratch.

A single input → step → snapshot funnel did not exist

The ordinary input path in COD4 is separate from G_RunFrame:

network usercmd → ClientThink(clientNum)
                    → SV_GetUsercmd(...)
                    → ClientThink_real(ent, &cmd)

Only under g_synchronousClients does G_RunClient() inside G_RunFrame call ClientThink_real() exactly once per server frame (g_active_mp.cpp). The dvar's own description:

Call 'client think' exactly once for each server frame to make smooth demos

This is Quake 3 heritage: separate VM entrypoints GAME_CLIENT_THINK → ClientThink and GAME_RUN_FRAME → G_RunFrame. The Q3 comment: normally ClientThink_real runs several times per server frame; with g_synchronousClients, exactly once (g_active.c, g_public.h).

The snapshot is a separate stage too: SV_BuildClientSnapshot / SV_WriteSnapshotToClient live in sv_snapshot_mp.cpp, and are not the return value of G_RunFrame.

retail:                         here:

     usercmd                         human ─┐
        |                            bot   ─┼→ TickInput → sim::step → Snapshot
        v                            replay─┘
  ClientThink_real
        |
        v
  global game state
        ^
        |
SV_RunFrame → G_RunFrame
        |
        v
  global game state
        |
        v
  BuildClientSnapshot
        |
        v
     network

Server, prediction, replay and the determinism test are all obliged to go through one function here (crates/sim/src/lib.rs).

Where the border runs

IW3 lineage here
World step G_RunFrame(levelTime) sim::step(...)
State global level, g_entities, g_clients an explicit &mut SimWorld
Time written into global state the tick / msec argument
Usercmd a separate ClientThink_real TickInput
Bot input a separate game path the same TickInput
Replay/demo special synchronous behavior the same sim::step
Snapshot built separately after the game step the result of step
Determinism not an architectural border held by the funnel

sim::step is retail-like in itself. What is modern is not the existence of a step function, but:

everything capable of changing the authoritative simulation must enter through one typed input and leave through one snapshot boundary.

No such strict funnel exists in the IW3 lineage: several entrances into one global mutable world. g_synchronousClients shows that IW themselves had already run into the consequences of that split, and for demos temporarily shifted the old model toward what is always in place here.

IW4

There is no directly reconstructed MW2 tree. The public MW2 projects are patch DLLs over the retail binary, and the bodies of G_RunFrame / ClientThink are not in them (a catalogue of names, not an architecture). By the IW3 lineage, IW4 is expected to be a development of the same G_RunFrame / ClientThink pair rather than a different scheme. This is a hypothesis.

The funnel cheat sheet is ENTITIES.md.