mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-25 18:05:19 -04:00
Hold every docs file to the fifty-line limit it claims for itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkgiSVEPTbsVZ1RpARujF6
This commit is contained in:
+26
-43
@@ -1,9 +1,8 @@
|
||||
# Shipping a release
|
||||
|
||||
Players update themselves — but not entirely. What `make deploy` does, what
|
||||
reaches the Windows folder with no human involved, and what does not. The
|
||||
contents of that folder are [`WINDOWS.md`](WINDOWS.md). Your own master rather
|
||||
than ours is [`MASTER.md`](MASTER.md).
|
||||
What `make deploy` does, what reaches the Windows folder with no human involved,
|
||||
and what does not. That folder is [`WINDOWS.md`](WINDOWS.md); your own master
|
||||
rather than ours is [`MASTER.md`](MASTER.md).
|
||||
|
||||
```bash
|
||||
make setup-windows # rustup target + cargo-xwin, once
|
||||
@@ -16,52 +15,36 @@ make logs prod|dev SINCE=2h
|
||||
```
|
||||
|
||||
Each recipe is one `cargo xtask` command — `windows`, `release`, `publish`,
|
||||
`provision`, `logs`, `certs` — and `make` only forwards `PROFILE` and
|
||||
`RELEASE`. `cargo xtask` on its own lists them.
|
||||
|
||||
`prod`/`dev` is the publication channel, not the compilation profile. Address
|
||||
and root are `IW4L_DEPLOY_HOST` / `IW4L_DEPLOY_ROOT` in `.env`. prod: udp/4433
|
||||
and `https://<host>:8443/prod/`; dev: 4434 and `/dev/`. Git, protocol and SHA
|
||||
are frozen into `deployment.json` at `release`, not at `publish`.
|
||||
`provision`, `logs`, `certs` — and `make` forwards only `PROFILE` and `RELEASE`;
|
||||
`cargo xtask` alone lists them. `prod`/`dev` is the publication channel, not the
|
||||
compilation profile; address and root are `IW4L_DEPLOY_HOST` /
|
||||
`IW4L_DEPLOY_ROOT` in `.env`. prod: udp/4433 and `https://<host>:8443/prod/`;
|
||||
dev: 4434 and `/dev/`. Git, protocol and SHA freeze into `deployment.json` at
|
||||
`release`, not at `publish`.
|
||||
|
||||
An ordinary publish compiles nothing, calls no zstd, rewrites nothing under
|
||||
`/etc` and does not restart the master if its SHA matched. The uncompressed
|
||||
`iw4l.exe` stays local. The archive is named `iw4l-<sha>.exe.zst`; the old
|
||||
`iw4l.exe.zst` on the VPS is not deleted. The manifest is swapped in by rename.
|
||||
`/etc` and leaves the master running if its SHA matched. The uncompressed
|
||||
`iw4l.exe` stays local, the archive is `iw4l-<sha>.exe.zst`, the old one on the
|
||||
VPS survives, and the manifest is swapped in by rename.
|
||||
|
||||
`iw4launcher.exe update` reads `IW4L_UPDATE_URL` from the sidecar `.env`,
|
||||
fetches `manifest.json`, checks size/sha256 and downloads `download`. Without
|
||||
that verb it starts the `iw4l.exe` already in the folder and makes no request at
|
||||
all — the archive ships one, so updating is a thing the player asks for rather
|
||||
than a thing that happens to them. The launcher never updates itself. The
|
||||
player's `.env` is created once and never overwritten.
|
||||
|
||||
Changing `master_protocol::PROTOCOL_VERSION` is also a change of `ALPN`: an old
|
||||
client is refused at the handshake instead of silently decoding a drifted
|
||||
format. The master and the clients of such a release ship together; `publish`
|
||||
restarts the master, because its SHA changed.
|
||||
Changing `master_protocol::PROTOCOL_VERSION` also changes `ALPN`: an old client
|
||||
is refused at the handshake instead of silently decoding a drifted format. The
|
||||
master and the clients of such a release ship together, and `publish` restarts
|
||||
the master because its SHA changed.
|
||||
|
||||
`make launcher windows` builds the portable ZIP in `dist/windows/`; it is not a
|
||||
deploy. A fix to the updater arrives only by copying `iw4launcher.exe` by hand.
|
||||
deploy, and a fix to the updater arrives only by copying `iw4launcher.exe` by
|
||||
hand. What the player's launcher does with `IW4L_UPDATE_URL`: [`WINDOWS.md`](WINDOWS.md).
|
||||
|
||||
## Cutting a public release
|
||||
|
||||
Tag `v0.1.0-demo.N`, titled `IW4L Technical Demo N`, and mark it a
|
||||
**pre-release** on GitHub — the runtime is experimental and nothing about it is
|
||||
production-ready. The release notes carry four things and are not worth writing
|
||||
without them:
|
||||
|
||||
* the commit it was built from;
|
||||
* the platforms the archive was actually **run** on. A target that compiled is
|
||||
not a verified platform, and a Linux release does not wait for a Windows
|
||||
archive that has not been started yet — nor the other way round;
|
||||
* the demo scenario: which game, which version, which map, what to do;
|
||||
* the known limitations.
|
||||
**Cutting a public release.** Tag `v0.1.0-demo.N`, titled `IW4L Technical
|
||||
Demo N`, and mark it a **pre-release**. The notes carry four things and are not worth writing without
|
||||
them: the commit it was built from; the platforms the archive was actually
|
||||
**run** on; the demo scenario (which game, version and map, and what to do); the
|
||||
known limitations. A target that merely compiled is not a verified platform, and
|
||||
a Linux release waits for no unstarted Windows archive, nor the reverse.
|
||||
|
||||
Every archive carries `LICENSE`, `NOTICE` and the two font licences
|
||||
(`release.rs::LEGAL_FILES`) and no game data. Check the archive, not just
|
||||
`git ls-files`: `make publish-check` reads the tracked tree and cannot see what
|
||||
was staged into a ZIP.
|
||||
|
||||
The published tags are not moved afterwards, and the history is not recreated
|
||||
again once anything has been pushed.
|
||||
was staged into a ZIP. Published tags are never moved, and history is never
|
||||
recreated once anything has been pushed.
|
||||
|
||||
+24
-28
@@ -3,24 +3,23 @@
|
||||
## There is one funnel
|
||||
|
||||
```text
|
||||
human ─┐
|
||||
bot ├─→ TickInput ─→ sim::step ─→ Snapshot ─→ client / theater / demo
|
||||
replay ─┘
|
||||
```
|
||||
human ─┐
|
||||
bot ├─→ TickInput ─→ sim::step ─→ Snapshot ─→ client / theater / demo
|
||||
replay ┘
|
||||
|
||||
```rust
|
||||
pub fn step(world: &mut SimWorld, tick: Tick, input: &TickInput, msec: i32) -> Snapshot;
|
||||
pub fn step(world: &mut SimWorld, tick: Tick, input: &TickInput,
|
||||
msec: i32, reason: StepReason) -> Snapshot;
|
||||
```
|
||||
|
||||
Headless, prediction, replay and the determinism test call exactly this one.
|
||||
`msec` is the length of the step; time enters **only** as an argument. Details
|
||||
in `crates/sim/src/lib.rs`. One funnel in, one snapshot out.
|
||||
`msec` is the step length; time enters **only** as an argument. The shape of
|
||||
the funnel is [`SIM-STEP.md`](SIM-STEP.md), the code `crates/sim/src/lib.rs`.
|
||||
|
||||
## What can be an entity at all — fixed by the data (`entity_iw4`)
|
||||
|
||||
The wiring is ours, the taxonomy comes with the data. The crate neither
|
||||
serializes nor orders fields "for the network": it pins the layout it reads and
|
||||
leaves transport to `net`.
|
||||
serializes nor orders fields "for the network": it pins the layout it reads,
|
||||
and leaves transport to `net`.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -28,27 +27,24 @@ leaves transport to `net`.
|
||||
| `Centity` | client-side slot, stride `0x204` (pose plus snapshot anchors) |
|
||||
| `ClientState` | per-client slot, stride `0x7c` (command, `name[16]`) |
|
||||
| `CorpseInfo` | server-side corpse slot, stride `0x53c`, 8 of them |
|
||||
| `ET_PLAYER` / `ET_PLAYER_CORPSE` / `ET_ITEM` / `ET_MISSILE` | |
|
||||
| `Trajectory` | `BG_EvaluateTrajectory`: gravity 400, delta 800 |
|
||||
| events | the `CG_EntityEvent` ring, an 11-bit wrapper |
|
||||
| `ET_PLAYER` / `ET_PLAYER_CORPSE` / `ET_ITEM` / `ET_MISSILE` | |
|
||||
| `Trajectory` | `bg_evaluate_trajectory`: gravity 400, delta 800 |
|
||||
| events | the entity-event ring, an 11-bit wrapper |
|
||||
| `ENTITYNUM_NONE` | `0x7FF` |
|
||||
|
||||
The player and the input are `playerstate_iw4` (`playerState_t`, `usercmd_s`):
|
||||
the two ends of the funnel.
|
||||
| `playerstate_iw4` | player and input: the two ends of the funnel |
|
||||
|
||||
## Where everything else lives
|
||||
|
||||
* `sim/` — `gentity.rs`, `spawn.rs`, `world.rs`, `snapshot.rs`, `damage.rs`,
|
||||
`bullet*.rs`, `missile.rs`, `item.rs`, `corpse.rs`, `match_state.rs`,
|
||||
`score.rs`, `rules.rs`, `world_objects.rs`, `identities.rs` (`ScriptModelId`
|
||||
is the one identity of a script model; a raw `u32` does not get to be a second);
|
||||
* `frame` — frame and session markers: `AppScreen`, `HasWorld`, `RuntimeRole`,
|
||||
`LaunchIdentity`. No plugins, no systems;
|
||||
* `sim/` — `gentity spawn world snapshot damage bullet* missile item corpse
|
||||
match_state score rules world_objects identities` (`ScriptModelId` is the one
|
||||
identity of a script model; a raw `u32` is not a second one);
|
||||
* `frame` — `AppScreen`, `HasWorld`, `RuntimeRole`, `LaunchIdentity`; no
|
||||
plugins, no systems;
|
||||
* `session` — standing the match up and tearing it down (`MatchInstalled` /
|
||||
`MatchTornDown`); `net` — the wire, deltas, prediction; `bots` — host-only
|
||||
controllers that observe through a sensor adapter, walk a ClipMap-baked
|
||||
graph, and enter the same `TickInput` funnel. Geometric `sim::step` scenes
|
||||
in `bots` tests are the mapless check that a `UserCmd` actually moves.
|
||||
`MatchTornDown`); `net` — wire, deltas, prediction; `bots` — host-only
|
||||
controllers that observe through a sensor adapter, walk a ClipMap-baked graph
|
||||
and enter the same `TickInput`. Geometric `sim::step` scenes in `bots` tests
|
||||
check maplessly that a `UserCmd` moves.
|
||||
|
||||
The snapshot publishes the **semantics** of DObj composition, not runtime trees
|
||||
and not posed vertices — see [`ANIM.md`](ANIM.md).
|
||||
Snapshots publish the **semantics** of DObj composition, never runtime trees or
|
||||
posed vertices ([`ANIM.md`](ANIM.md)).
|
||||
|
||||
+33
-53
@@ -2,68 +2,48 @@
|
||||
|
||||
Three floors: **disk** (`asset_transport`), **format** (`fastfile_*` → the
|
||||
`asset_iw4` IR), **products** (`asset_*` + `assets`). `assets` knows the disk
|
||||
through the transport and knows nothing about the format; `fastfile_*` knows
|
||||
the format and opens no files. Installing the match is not here — it is in
|
||||
`session`.
|
||||
|
||||
## The transaction: who calls whom
|
||||
through the transport and nothing of the format; `fastfile_*` knows the format
|
||||
and opens no files. Installing the match is `session`, not here.
|
||||
|
||||
```text
|
||||
console/ui `map` → session::lifecycle SessionSwapRequest → tear down the previous
|
||||
occupancy, then MapLoadApproved{request_id}
|
||||
→ assets::match_load approve_map_load find_zone_file / find_runtime_common_mp
|
||||
start_match_load spawn onto load_pool() (MatchLoadBusy)
|
||||
poll_match_load → PreparedMatchReady
|
||||
→ session::match_apply apply_prepared_match → scene + SimWorld + MatchInstalled
|
||||
console/ui `map` → session::lifecycle SessionSwapRequest, tear the previous
|
||||
occupancy down, then MapLoadApproved{request_id}
|
||||
→ assets::match_load approve_map_load / start_match_load onto load_pool()
|
||||
(MatchLoadBusy) / poll_match_load → PreparedMatchReady
|
||||
→ session::match_apply apply_prepared_match → scene + SimWorld + MatchInstalled
|
||||
```
|
||||
|
||||
Cancellation is `MatchLoadAbort(request_id)`: the walk returns
|
||||
`MatchLoadOutcome::Canceled` at its next checkpoint and there is no prepared
|
||||
match to throw away. Failure is `MapLoadFailed` — from discovery *and* from
|
||||
install; `LoadingScreen` holds the failed install until teardown and cannot
|
||||
turn it into a success. Every accepted request ends once: `MatchInstalled` or
|
||||
`MapLoadFailed`, never both.
|
||||
`MatchLoadOutcome::Canceled` at its next checkpoint, with no prepared match to
|
||||
throw away. Failure is `MapLoadFailed`, from discovery *and* from install, and
|
||||
every accepted request ends once — `MatchInstalled` or `MapLoadFailed`.
|
||||
|
||||
## The walk itself: `assets::session_load::load_prepared_match`
|
||||
## The walk: `assets::session_load::load_prepared_match`
|
||||
|
||||
The single entry point; the composition root sees neither `fastfile_*` nor
|
||||
`AssetSink`. Inside are several parallel walks on `load_pool()`: opening the
|
||||
archive, startup materials, `common_mp`, the IW5/T5 weapon bundle, language
|
||||
zones. `common_mp` goes **before** the map: its techset tables are needed
|
||||
before `build_world_draw`. The output is `MatchLoadOutcome::Ready` with a
|
||||
`PreparedMatch` (world, one `MatchMaterials` population, clip, weapons,
|
||||
catalogs, `PreparedMap` — the single owner of spawns and map facts, `report`).
|
||||
|
||||
Ownership rules the walk keeps: the zone arenas die with the walk that reads
|
||||
them (only their size travels), a consumed donor catalog is moved row by row
|
||||
rather than cloned, and the merged material population is finalized once —
|
||||
whether or not the map has a `GfxWorld` — and never lives inside the geometry.
|
||||
The single entry point; the composition root sees no `fastfile_*` and no
|
||||
`AssetSink`. Inside are parallel walks on `load_pool()`: the archive, startup
|
||||
materials, `common_mp`, the IW5/T5 weapon bundle, language zones. `common_mp`
|
||||
goes **before** the map — its techset tables are needed before
|
||||
`build_world_draw`. Output is `MatchLoadOutcome::Ready` with a `PreparedMatch`
|
||||
(world, one `MatchMaterials` population, clip, weapons, catalogs and
|
||||
`PreparedMap`, the single owner of spawns and map facts).
|
||||
|
||||
The per-game adapter is `assets::lane` (`ZoneGame` → iw4/iw5/t5), the only
|
||||
exhaustive `match ZoneGame` in the project; the runtime stays game-blind. A gap
|
||||
in the lane is a typed `LaneGap`, not silence.
|
||||
exhaustive `match ZoneGame` in the project; the runtime stays game-blind and a
|
||||
gap in a lane is a typed `LaneGap`, not silence.
|
||||
|
||||
## Installing it: `session::match_apply`
|
||||
Installing it is `session::match_apply`, in two halves. Preflight builds a
|
||||
`MatchInstallPlan` — mode, doors, objectives, scene conversion, drawable world —
|
||||
and publishes nothing until it hands one over, so a refusal leaves no
|
||||
half-installed resources. Commit publishes it, boots the sim, writes
|
||||
`MatchInstalled`.
|
||||
|
||||
Two halves. Preflight builds a `MatchInstallPlan`: mode, doors, objectives,
|
||||
scene conversion, drawable world. Nothing of the match is published until it
|
||||
hands one over, so an ordinary refusal leaves no half-installed resources
|
||||
behind and completes the swap with `MapLoadFailed`. The commit half then
|
||||
publishes the plan, boots the sim and writes `MatchInstalled`.
|
||||
## Cache, and poking it: `iw4l-artifacts/cache/<kind>/<key>`, content-addressed
|
||||
|
||||
## Cache
|
||||
|
||||
`iw4l-artifacts/cache/<kind>/<key>`, content-addressed, leaf in
|
||||
`asset_transport::artifact_cache` (`cache_get` / `cache_put`, `fnv1a64`). A
|
||||
miss is silent: the caller computes the value anyway, and a hit is obliged to
|
||||
be the **same bytes** a miss would have written. The key names every
|
||||
input; if the encoder changed, bump the format word at the caller. Live kinds:
|
||||
`mips` (`asset_material/material_images.rs`), `wgsl`
|
||||
(`render_frontend/.../wgsl_disk_cache.rs`).
|
||||
|
||||
## How to poke it
|
||||
|
||||
`IW4L_GAMES` is the root holding the game trees; no folder name is hardcoded
|
||||
anywhere (T1); the container version picks the decoder, not the folder name
|
||||
(T2). Live, it is `make map mp_boneyard` ([`RUN.md`](RUN.md)); the stages of the
|
||||
walk are visible in `LoadProgress` and in the `.pftrace` ([`PERF.md`](PERF.md)).
|
||||
Leaf in `asset_transport::artifact_cache` (`cache_get` / `cache_put`, `fnv1a64`).
|
||||
A miss is silent — the caller computes the value anyway — and a hit must be the
|
||||
**same bytes** a miss would have written. The key names every input; if the
|
||||
encoder changed, bump the format word. Live kinds: `mips`, `wgsl`. `IW4L_GAMES`
|
||||
is the root holding the game trees; no folder name is hardcoded and the container
|
||||
version picks the decoder. Live it is `make map mp_boneyard` ([`RUN.md`](RUN.md)),
|
||||
with stages in `LoadProgress` and the `.pftrace`.
|
||||
|
||||
+29
-59
@@ -1,80 +1,50 @@
|
||||
# Your own master
|
||||
|
||||
`iw4l-master` is a relay and a server browser, not a game server: the host's
|
||||
client simulates the match. No dedicated server ships in this repository. The
|
||||
crate pulls in no engine dependency, so a VPS needs neither assets nor a GPU.
|
||||
Publishing *our* releases is [`DEPLOY.md`](DEPLOY.md).
|
||||
`iw4l-master` is a relay and server browser, not a game server: the host's client
|
||||
simulates the match. It pulls in no engine dependency, so a VPS needs no assets
|
||||
and no GPU. Publishing our own releases: [`DEPLOY.md`](DEPLOY.md).
|
||||
|
||||
## Install
|
||||
|
||||
Everything runs from the machine holding the clone; the VPS needs only ssh.
|
||||
## Install — from the machine with the clone; the VPS needs only ssh
|
||||
|
||||
```bash
|
||||
cargo xtask master install root@1.2.3.4
|
||||
cargo xtask master logs root@1.2.3.4 --since 10min
|
||||
```
|
||||
|
||||
`install` mints a CA and a server certificate under `~/.iw4l/ca`, builds the
|
||||
static binary (`crt-static`, so the VPS glibc version does not matter), places
|
||||
it in `/usr/local/lib/iw4l/` and the certificates in `/etc/iw4l/` (key
|
||||
`0640 root:iw4l`), writes the unit through `iw4l-master print-unit`, opens the
|
||||
channel port and runs `enable --now`. The CA private key never leaves the
|
||||
local machine.
|
||||
`install` mints a CA and server certificate under `~/.iw4l/ca`, builds a static
|
||||
binary, installs it with the certificates under `/usr/local/lib/iw4l/` and
|
||||
`/etc/iw4l/`, writes the unit through `iw4l-master print-unit` and runs
|
||||
`enable --now`. The CA key never leaves the local machine. `master update`,
|
||||
`status` and `uninstall` follow, each taking `--channel dev` and `--ca DIR`;
|
||||
`uninstall` keeps the certificates and CA, so a reinstall stays trusted.
|
||||
|
||||
The binary prints its **own** unit, so `ExecStart` cannot drift from what
|
||||
`serve` parses; port and unit name come from `master_protocol::Channel`. Then
|
||||
`master update` (rebuild, upload, restart), `status`, `uninstall`. Every verb
|
||||
takes `--channel dev` for the second port and `--ca DIR` for another CA;
|
||||
`uninstall` keeps both certificates and CA, so a reinstall stays trusted by
|
||||
everyone who already has your `iw4l-ca.pem`.
|
||||
## No domain required — the certificate is not tied to one
|
||||
|
||||
## No domain required
|
||||
The client connects by address but checks the certificate against a **separate**
|
||||
name — `IW4L_MASTER_SERVER_NAME`, a fixed label — so nothing in the SAN depends on
|
||||
the host; with `IW4L_MASTER_CA_CERT` set it loads only that PEM into an empty
|
||||
`RootCertStore` (`net/src/transport/master.rs:3107`), bypassing the platform
|
||||
verifier. `install` signs `San::Labels` alone: the certificate is minted **once
|
||||
per user, not per server**, and a new IP or VPS keeps it.
|
||||
|
||||
The client connects by address but verifies the certificate against a
|
||||
**separate** name, and with `IW4L_MASTER_CA_CERT` set it puts only that PEM
|
||||
into an empty `RootCertStore` (`net/src/transport/master.rs:3107`, `:3150`) —
|
||||
the platform verifier is bypassed entirely.
|
||||
## What it records — nothing on disk
|
||||
|
||||
| path | certificate checked against | host in SAN |
|
||||
|---|---|---|
|
||||
| client → master, QUIC | `IW4L_MASTER_SERVER_NAME`, a fixed label | not needed |
|
||||
| updater → Caddy, HTTPS | host from `IW4L_UPDATE_URL` (`updater:267`) | needed |
|
||||
Rooms, the advertised match name, peers and room membership live in `ServiceState`
|
||||
in memory, gone when the room closes or the process restarts. No account, history,
|
||||
analytics or telemetry. What survives is the systemd journal —
|
||||
startup and failures, not matches or players — under the VPS's own `journald`
|
||||
retention. Peer IPs are visible to the kernel and to any packet capture on that
|
||||
host while a connection is open, as with any server. Run it for others and that is
|
||||
the honest description: it forwards packets and vouches for nobody.
|
||||
|
||||
`San::WithHost` / `cert_covers_host` in `xtask/src/certs.rs` serve the second
|
||||
row, our CDN. Your own master runs no Caddy, so `install` signs `San::Labels`
|
||||
alone and nothing in the SAN depends on the host: the certificate is minted
|
||||
**once per user, not per server** — a new IP or a new VPS keeps the same one.
|
||||
## Hand out to players — this block plus `iw4l-ca.pem`
|
||||
|
||||
## What it records
|
||||
|
||||
Nothing on disk. `iw4l-master` opens no file, no database and no log of its own:
|
||||
the state it holds — rooms, the advertised match name the host typed, peers and
|
||||
who is in which room — lives in `ServiceState` in memory and is gone when the
|
||||
room closes or the process restarts. There is no account, no history and no
|
||||
analytics; the runtime sends no telemetry to it.
|
||||
|
||||
What survives is the systemd journal, and only what the process writes to
|
||||
stderr — startup and failures, not matches or players. Its retention is the
|
||||
VPS's `journald` configuration, not ours; `cargo xtask master logs --since` just
|
||||
reads it. The transport is QUIC, so peer IP addresses are visible to the kernel
|
||||
and to any packet capture on that host for as long as a connection is open, the
|
||||
same as for any server you connect to.
|
||||
|
||||
If you run the relay for other people, that is the honest description to give
|
||||
them: it forwards packets between clients who are already talking to each other,
|
||||
and it vouches for nobody.
|
||||
|
||||
## Hand out to players
|
||||
|
||||
`install` prints the block below; give it and `iw4l-ca.pem` to your friends:
|
||||
The host opens a match with `IW4L_MASTER_HOST_NAME='name' make map mp_boneyard`,
|
||||
the rest join from `make menu`, 2..=18 (`IW4L_MASTER_MAX_PLAYERS`). Trust rests
|
||||
entirely on pinning `ca.pem`, so hand it over a trusted channel.
|
||||
|
||||
```dotenv
|
||||
IW4L_MASTER_ADDR=1.2.3.4:4433
|
||||
IW4L_MASTER_SERVER_NAME=iw4l-prod
|
||||
IW4L_MASTER_CA_CERT=/path/to/iw4l-ca.pem
|
||||
```
|
||||
|
||||
The host opens a match with `IW4L_MASTER_HOST_NAME='name' make map mp_boneyard`;
|
||||
everyone else joins from `make menu`, 2..=18 (`IW4L_MASTER_MAX_PLAYERS`). The
|
||||
label `iw4l-prod` is identical for everyone, so trust rests entirely on pinning
|
||||
`ca.pem` — hand it over a channel the players trust.
|
||||
|
||||
+36
-73
@@ -1,87 +1,50 @@
|
||||
# Render: what lives where
|
||||
|
||||
An island of nine crates: `render` (composition and diagnostics only),
|
||||
`render_frontend`, `render_gpu`, `render_frame`,
|
||||
`render_backend`, `render_material`, `render_anim`, `render_fx`,
|
||||
`render_scene`. `render_frontend` reads the presented snapshot, `render_gpu`
|
||||
writes the pixels. The island decides no game logic and never writes
|
||||
`AuthorityWorld`. No asset decoding happens here.
|
||||
Nine crates. `render_frontend` reads the presented snapshot, `render_gpu` writes
|
||||
the pixels and solely owns `wgpu`. The island decides no game logic, never
|
||||
writes `AuthorityWorld`, decodes no assets.
|
||||
|
||||
## What lives where: `render_frontend` (three seams) + neighbours
|
||||
|
||||
* `prepare/` — scene/view/cull (`scene/world.rs`, `scene/spawn.rs`,
|
||||
`scene/cull.rs`, `scene/camera.rs`, `scene/gfx_scene.rs` anim-submission
|
||||
bridge, `worker_cmds.rs`); occupancy Resources, lighting request/result, frustum planes, and camera
|
||||
pose — `render_scene`;
|
||||
* `adapters/` — occupancy out of animation and FX (`adapters/anim/*`,
|
||||
`adapters/fx/*`);
|
||||
* `assemble/` — the immutable products of a frame (`drawsurf/retained_list.rs`,
|
||||
`drawsurf/frame_products.rs`, `drawsurf/material_runtime.rs`,
|
||||
`drawsurf/tess/`);
|
||||
* `render_anim` / `render_fx` — producers of poses and FX; `render_scene` —
|
||||
occupancy storage; `render_frame` — the immutable snapshot of frame products
|
||||
(`SourceRevisions`, `PackedSegments`, pure packing) at the frontend ↔ GPU border;
|
||||
`render_material` — administrator of the material tables; `render_backend` —
|
||||
the list views; `render_gpu` — the sole owner of `wgpu`;
|
||||
`render::diag/` — observation.
|
||||
| crate | owns |
|
||||
|---|---|
|
||||
| `render_frontend` | `prepare/` scene, view, cull (`scene/{world,spawn,cull,camera}.rs`, `scene/gfx_scene.rs` anim-submission bridge, `worker_cmds.rs`); `adapters/` occupancy out of anim and FX; `assemble/` frame products (`drawsurf/{retained_list,frame_products,material_runtime}.rs`, `drawsurf/tess/`) |
|
||||
| `render_scene` | occupancy storage: lighting request/result, frustum planes, camera pose. `render_anim` / `render_fx` produce the poses and FX |
|
||||
| `render_frame` | the immutable frame-product snapshot at the frontend ↔ GPU border (`SourceRevisions`, `PackedSegments`, pure packing) |
|
||||
| `render_material`, `render_backend` | the material tables; the list views |
|
||||
| `render` | composition, `extract.rs`, `diag/` |
|
||||
|
||||
## The frame path
|
||||
|
||||
```text
|
||||
zone → asset_iw4 IR → prepare/scene/world.rs the world in our own types
|
||||
→ prepare/scene/spawn.rs GPU spawn
|
||||
prepare/scene/camera.rs (look/WASD), frustum.rs + cull.rs + dpvs_iw4
|
||||
→ assemble/drawsurf/list.rs ONE sorted GfxDrawSurf[]
|
||||
→ setup.rs R_SetupMaterial — techType → technique
|
||||
→ state.rs R_ChangeState — GfxPassState / blend
|
||||
zone → asset_iw4 IR → prepare/scene/world.rs (our own types) → spawn.rs (GPU)
|
||||
camera.rs (look/WASD), frustum.rs + cull.rs + dpvs_iw4
|
||||
→ assemble/drawsurf/list.rs ONE sorted GfxDrawSurf[]
|
||||
→ setup.rs techType → technique → state.rs GfxPassState / blend
|
||||
→ tess/{world,smodel,xmodel,fx,mark,glass,particle_cloud}.rs
|
||||
→ material_runtime.rs N-pass execution or a typed refusal
|
||||
→ sm3*.rs SM3 tokens (`d3d9_sm3`) → WGSL → gpu_contract.rs → wgpu
|
||||
→ material_runtime.rs N-pass execution or a typed refusal
|
||||
→ sm3*.rs SM3 tokens (`d3d9_sm3`) → WGSL → gpu_contract.rs → wgpu
|
||||
```
|
||||
|
||||
The key point: **one** drawsurf machine, with only the tess emitters forking
|
||||
per surface type. World, static models and the viewmodel are not three
|
||||
renderers; they are three emitters into one list. Lighting: `lighting_iw4` (light grid) plus
|
||||
`prepare/scene/smodel_lighting.rs` and the `model_lighting_atlas.rs` atlas;
|
||||
shadows are `assemble/drawsurf/sun_shadow.rs`.
|
||||
**One** drawsurf machine; only the tess emitters fork per surface type. World,
|
||||
static models and the viewmodel are three emitters into one list, not three
|
||||
renderers. Lighting is `lighting_iw4` (light grid) with
|
||||
`prepare/scene/smodel_lighting.rs` and `model_lighting_atlas.rs`, shadows
|
||||
`assemble/drawsurf/sun_shadow.rs`.
|
||||
|
||||
## GPU-side ownership
|
||||
## GPU-side ownership: `render_gpu/src/drawsurf/colour_submit/`, three owners
|
||||
|
||||
`render_gpu/src/drawsurf/colour_submit/` — one list, three owners:
|
||||
|
||||
* `geometry.rs` + `residency.rs` — stream residency. Allocation capacity, live
|
||||
length, allocation generation and the uploaded producer revision are four
|
||||
different quantities: a frame that shrank does not recreate the buffer, and a
|
||||
producer segment that changed (`render_frame::PackedSegments`) is uploaded
|
||||
alone rather than the whole stream;
|
||||
* `geometry.rs` + `residency.rs` — residency. Capacity, live length, allocation
|
||||
generation and uploaded revision are four quantities: a shrunk frame keeps its
|
||||
buffer and a changed `PackedSegments` segment uploads alone;
|
||||
* `prepare_camera.rs` — shared installation resets generations and publishes
|
||||
shadow views, then sun, spot and camera prepare in order. Executors and arenas
|
||||
are pass-local; binding slots and the shadow texture table still share mutable
|
||||
ownership. The camera's pretess cache is `CameraWorldPretess`; shadows do not
|
||||
hold it. There is exactly one camera: a second `Camera3d` is the typed
|
||||
refusal `MultipleCameraViews`;
|
||||
* `record.rs` + `indirect.rs` — recording. The direct path is `draw_indexed`;
|
||||
with `IW4L_MULTI_DRAW` plus `INDIRECT_FIRST_INSTANCE` and
|
||||
`DownlevelFlags::INDIRECT_EXECUTION`, adjacent compatible runs of the same
|
||||
plan are written as a single `multi_draw_indexed_indirect` (the `multi_draw` /
|
||||
`multi_draw_cmds` counters). Order and per-command `first_instance` do not
|
||||
change.
|
||||
shadow views; sun, spot and camera then prepare in order, on pass-local
|
||||
executors. A second `Camera3d` is the typed refusal `MultipleCameraViews`;
|
||||
* `record.rs` + `indirect.rs` — recording. Direct path `draw_indexed`; with
|
||||
`IW4L_MULTI_DRAW` and the indirect downlevel flags, adjacent compatible runs
|
||||
merge into one `multi_draw_indexed_indirect`, order preserved.
|
||||
|
||||
The `render/src/extract.rs` bridge publishes `InstalledRenderWorld` when static
|
||||
geometry, ports or SMC maps change, and `PublishedRenderFrame` every frame,
|
||||
checking frame/world/material identity.
|
||||
CPU image handles share the `assets::image_handles` Arc with the upload registry.
|
||||
The loader's decisions are made in
|
||||
`render_frontend/src/prepare/scene/world_gpu.rs::consume_gpu_load_progress`.
|
||||
|
||||
## Neighbours
|
||||
|
||||
`d3d9_decl` / `d3d9_sm3` / `d3d9_state` carry D3D9 semantics without a single
|
||||
word about IW4; the only D3D9 → `wgpu` crossing is the adapter inside `render`.
|
||||
`session` stands the match up and tears it down, `assets` owns the bytes.
|
||||
|
||||
## How to poke it
|
||||
|
||||
There are no live `r_*` dvars. Live, it is env
|
||||
vars (`IW4L_SINGLE_CELL`, `IW4L_SUN_SHADOW_*`, …) and native Perfetto
|
||||
(`IW4L_PERF=1`, [`PERF.md`](PERF.md)).
|
||||
`extract.rs` publishes `InstalledRenderWorld` when static geometry, ports or SMC
|
||||
maps change, and `PublishedRenderFrame` every frame. `d3d9_decl` / `d3d9_sm3` /
|
||||
`d3d9_state` carry D3D9 semantics with no word about IW4; the only D3D9 → `wgpu`
|
||||
crossing is the adapter inside `render`, while `session` stands the match up and
|
||||
`assets` owns the bytes. No live `r_*` dvars: env vars (`IW4L_SINGLE_CELL`,
|
||||
`IW4L_SUN_SHADOW_*`, …) and Perfetto (`IW4L_PERF=1`, [`PERF.md`](PERF.md)).
|
||||
|
||||
+32
-49
@@ -2,66 +2,49 @@
|
||||
|
||||
How to touch the live process. Observation is [`PERF.md`](PERF.md).
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
set -a; . ./.env; set +a # IW4L_GAMES; DISPLAY=:0 if the session has none
|
||||
make map mp_boneyard CMDS='spawn assault; wait 2s; quit'
|
||||
cargo run --profile play -p launcher -- map iw5:mp_overwatch --cmds '…'
|
||||
```
|
||||
|
||||
`make` does not pass foreign flags through: in the `Makefile` it is the `CMDS`
|
||||
variable, on the binary it is `--cmds`. The colon is a GNU make pattern:
|
||||
`make map iw5:…` fails; write `make map ZONE=iw5:mp_overwatch` or use
|
||||
`cargo run`. Recipes: `make scenario` / `chaos` / `bench-demo` / `bench-live` /
|
||||
`lifecycle-*` (`*_CMDS` in the `Makefile`). Live recipes use `[profile.play]`;
|
||||
for LTO, `PROFILE=release`.
|
||||
`make` passes no foreign flags through: in the `Makefile` it is `CMDS`, on the
|
||||
binary `--cmds`. The colon is a GNU make pattern, so `make map iw5:…` fails —
|
||||
write `make map ZONE=iw5:mp_overwatch` or use `cargo run`. Recipes: `make
|
||||
scenario`, `chaos`, `bench-demo`, `bench-live`, `lifecycle-*` (`*_CMDS` in the
|
||||
`Makefile`). Live recipes use `[profile.play]`; for LTO, `PROFILE=release`.
|
||||
|
||||
## The match hasn't started — controls are frozen
|
||||
**Controls are frozen until the match starts.** `sim::step` allows movement only
|
||||
in `MatchPhase::Playing`; `hold +attack`, `+forward` and `move` do nothing before
|
||||
then. You wait out `PLAYER_WAIT_MS` 15 s + `MATCH_START_MS` 5 s
|
||||
(`crates/gamemode_iw4/src/prematch.rs`) — bots (≥ 2 live players) cut the first
|
||||
timer short, a single-player script waits the full 20 s. The verb is shorter:
|
||||
`wait world; spawn assault; force_match_start; wait 1s; …`, and a script
|
||||
involving firing, hits or decals **lies with a zero without it**.
|
||||
|
||||
`sim::step` allows movement only in `MatchPhase::Playing`. Before that,
|
||||
`hold +attack`, `+forward` and `move` do nothing. You have to wait out
|
||||
`PLAYER_WAIT_MS` 15 s + `MATCH_START_MS` 5 s
|
||||
(`crates/gamemode_iw4/src/prematch.rs`); the first timer is cut short by bots
|
||||
(≥ 2 live players), a single-player script waits the full 20 s. The verb is
|
||||
shorter: `wait world; spawn assault; force_match_start; wait 1s; …` — a script
|
||||
involving firing / hits / decals **lies with a zero without
|
||||
`force_match_start`**.
|
||||
|
||||
## The script waits on its own (sync-by-default)
|
||||
|
||||
Ritual `wait`s are unnecessary: a command holds the FIFO until its own fact —
|
||||
`map`/`demo`/`play` until the swap plus GPU-ready `WorldScene::spawned`,
|
||||
`spawn` until `AppScreen::InGame`, `move`/`look`/`tp` until a matching
|
||||
presented pose, `force_match_start` until `Playing`, `disconnect` until the
|
||||
hold is torn down. `wait Ns` remains, for pacing and observation windows.
|
||||
The `&` suffix is a loud refusal to wait (`map mp_rust &`), `!` jumps the queue
|
||||
(in bash write `disconnect !`, otherwise history expansion bites). The 120 s
|
||||
gate timeout aborts the rest of the script with an echo. Queue parsing is in
|
||||
`crates/console/src/`.
|
||||
Hand-typed `dump`/`clip`/`screenshot` pass through any wait while preserving
|
||||
the queue; inside `--cmds` that takes an explicit `!`.
|
||||
**Sync by default.** Ritual `wait`s are unnecessary: a command holds the FIFO
|
||||
until its own fact — `map`/`demo`/`play` until the swap plus GPU-ready
|
||||
`WorldScene::spawned`, `spawn` until `AppScreen::InGame`, `move`/`look`/`tp`
|
||||
until a matching presented pose, `force_match_start` until `Playing`,
|
||||
`disconnect` until the hold is torn down. `wait Ns` remains, for pacing and
|
||||
observation windows. The `&` suffix is a loud refusal to wait (`map mp_rust &`),
|
||||
`!` jumps the queue (in bash write `disconnect !`, or history expansion bites).
|
||||
The 120 s gate timeout aborts the rest with an echo; parsing is
|
||||
`crates/console/src/`. Hand-typed `dump`/`clip`/`screenshot` pass through any
|
||||
wait while preserving the queue, which inside `--cmds` takes an explicit `!`.
|
||||
|
||||
## Verbs and traps
|
||||
|
||||
`map spawn class give attach name kill damage move tp look nudge press hold
|
||||
release bind bot wait mark record stoprecord clip demo dump screenshot ui disconnect quit`
|
||||
plus the debug `force_match_start` / `showpos`.
|
||||
`bot` is `add | hold | tp | give | fire`; `dump [name]` writes the current
|
||||
snapshot into `dumps/`.
|
||||
`clip` writes the last available 45 s into `clips/<ULID>/` (demo + dump) on the
|
||||
host and on the clients; the host records authority, a client records the
|
||||
snapshots it received plus its own presented state.
|
||||
The source is named in the manifest; `demo LATEST` plays the clip back.
|
||||
* ADS is `hold +speed_throw`, not `+speed`;
|
||||
* bolt action (`fire_type=1`): `hold +attack` is **one** shot; a full magazine
|
||||
will not reload itself (`press +attack` ×N, then `press +reload`);
|
||||
* `look` without `LookState` only writes `ps.viewangles` — the script is not
|
||||
aiming;
|
||||
* `give` takes a namespace: `give t5:weapon/psg1_acog`, `give iw5:weapon/msr`.
|
||||
plus the debug `force_match_start` / `showpos`. `bot` is
|
||||
`add | hold | tp | give | fire`; `dump [name]` writes the current snapshot into
|
||||
`dumps/`. `clip` writes the last available 45 s into `clips/<ULID>/` (demo + dump)
|
||||
on host and clients — the host records authority, a client the snapshots it
|
||||
received plus its own presented state; `demo LATEST` plays it back.
|
||||
|
||||
`make bench-load-session` runs `make menu` with the script
|
||||
`mark in_menu; map mp_hanoi; spawn; mark in_hanoi; map mp_underpass; spawn; mark in_underpass; quit`.
|
||||
`python3 scripts/bench-marks.py <log>` computes the intervals between engine
|
||||
marks; `--json` gives machine-readable output. Build and menu startup are not
|
||||
part of the intervals.
|
||||
* ADS is `hold +speed_throw`, not `+speed`;
|
||||
* bolt action (`fire_type=1`): `hold +attack` is **one** shot, and a full
|
||||
magazine will not reload itself (`press +attack` ×N, then `press +reload`);
|
||||
* `look` without `LookState` only writes `ps.viewangles` — no aiming;
|
||||
* `give` takes a namespace: `give t5:weapon/psg1_acog`, `give iw5:weapon/msr`.
|
||||
|
||||
+31
-61
@@ -1,51 +1,15 @@
|
||||
# Portable Windows folder
|
||||
|
||||
`make launcher windows` creates `dist/windows/iw4l-windows-dev.zip` and
|
||||
`dist/windows/iw4l-windows-prod.zip`. Both archives use the password
|
||||
`contextrot` and are self-contained — the game binary is in the archive, so the
|
||||
folder runs with no network at all:
|
||||
|
||||
```text
|
||||
.env
|
||||
iw4l-ca.pem
|
||||
iw4launcher.exe
|
||||
iw4l.exe
|
||||
LICENSE
|
||||
NOTICE
|
||||
OFL-Oxanium.txt
|
||||
COPYING-FreeFont.txt
|
||||
```
|
||||
|
||||
The four licence files are not decoration: `iw4l.exe` has both fonts compiled
|
||||
into it with `include_bytes!`, so an archive carrying the binary carries their
|
||||
licences too. `release.rs::LEGAL_FILES` is the list, and a build that ships
|
||||
without them is the bug.
|
||||
|
||||
Extract the selected archive into any dedicated folder and add ordinary Windows
|
||||
`.lnk` shortcuts to installed Call of Duty title folders or title executables.
|
||||
MW2 **Multiplayer** files are required for the menu (`zone/**/common_mp.ff`);
|
||||
a folder containing only `zone/dlc/*.ff` is insufficient. BO1 and MW3 are optional.
|
||||
Run the launcher; it starts the game without modifying those installations.
|
||||
|
||||
**Updating is something you ask for.** Plain `iw4launcher.exe` starts the
|
||||
`iw4l.exe` that is already in the folder and touches nothing else. Fetching a
|
||||
newer build is `iw4launcher.exe update` (or `IW4L_UPDATE=1` for a shortcut that
|
||||
cannot pass an argument), which needs `IW4L_UPDATE_URL` in the portable `.env`.
|
||||
No build replaces its own binary on its own. How a release gets published is
|
||||
[`DEPLOY.md`](DEPLOY.md).
|
||||
|
||||
Steam shipped MW2 and MW3 re-releases whose FastFiles are serialized with
|
||||
64-bit pointers. MW2 is read in both serializations; **MW3 x64 zones are
|
||||
detected and refused**, so an MW3 shortcut to such an install contributes no
|
||||
weapons and no maps, and says so in the log
|
||||
(`IW5 zone is serialized x64 (Steam re-release)`).
|
||||
`make launcher windows` creates `dist/windows/iw4l-windows-{dev,prod}.zip`. Both
|
||||
archives use the password `contextrot` and are self-contained — the game binary
|
||||
is inside, so the folder runs with no network at all.
|
||||
|
||||
```text
|
||||
iw4l-portable/
|
||||
├── iw4launcher.exe from the archive
|
||||
├── iw4l.exe from the archive; replaced only by `iw4launcher update`
|
||||
├── iw4l-ca.pem public trust anchor
|
||||
├── LICENSE NOTICE … the four licence files, kept with the binary
|
||||
├── LICENSE NOTICE OFL-Oxanium.txt COPYING-FreeFont.txt
|
||||
├── .env from the archive; player edits win forever
|
||||
├── Modern Warfare 2.lnk target: game folder or title .exe
|
||||
├── Black Ops.lnk optional
|
||||
@@ -53,28 +17,34 @@ iw4l-portable/
|
||||
└── iw4l-artifacts/ saves, caches, demos, logs and captures
|
||||
```
|
||||
|
||||
The folder containing the two executables is the process working directory even
|
||||
when Explorer supplies another one. `IW4L_GAMES` defaults to this folder on
|
||||
Windows; `.lnk` targets directly inside it are additional read-only search
|
||||
roots. An explicit `IW4L_GAMES` still works and may itself contain shortcuts.
|
||||
The four licence files are not decoration: `iw4l.exe` has both fonts compiled in
|
||||
with `include_bytes!`, so an archive carrying the binary carries their licences
|
||||
too. `release.rs::LEGAL_FILES` is the list; a build shipping without them is a
|
||||
bug.
|
||||
|
||||
`.env` is loaded before release-manifest defaults. The launcher creates it only
|
||||
when missing and never overwrites it. `IW4L_UPDATE_URL` is read only by
|
||||
`iw4launcher.exe update`, never by a plain start; the same `iw4launcher.exe`
|
||||
serves both channels.
|
||||
Extract into any dedicated folder and add ordinary Windows `.lnk` shortcuts to
|
||||
installed Call of Duty title folders or executables. MW2 **Multiplayer** files
|
||||
are required for the menu (`zone/**/common_mp.ff`); `zone/dlc/*.ff` alone is
|
||||
insufficient. BO1 and MW3 are optional. The launcher starts the game without
|
||||
modifying those installations.
|
||||
|
||||
```dotenv
|
||||
IW4L_UPDATE_URL=https://host:8443/prod
|
||||
IW4L_MASTER_ADDR=master.example.net:4433
|
||||
IW4L_MASTER_SERVER_NAME=iw4l-prod
|
||||
IW4L_MASTER_CA_CERT=iw4l-ca.pem
|
||||
```
|
||||
**Updating is something you ask for.** Plain `iw4launcher.exe` starts the
|
||||
`iw4l.exe` already in the folder and touches nothing else. Fetching a newer
|
||||
build is `iw4launcher.exe update` (or `IW4L_UPDATE=1` for a shortcut that cannot
|
||||
pass an argument), which needs `IW4L_UPDATE_URL` in the portable `.env`. No
|
||||
build replaces its own binary on its own. Publishing is [`DEPLOY.md`](DEPLOY.md).
|
||||
|
||||
An unreachable master is not a launch error: the browser keeps retrying and
|
||||
shows the connection error. All writable game state stays below
|
||||
`iw4l-artifacts/` (including Windows `settings.cfg`, unless
|
||||
Steam shipped MW2 and MW3 re-releases whose FastFiles are serialized with 64-bit
|
||||
pointers. MW2 is read in both serializations; **MW3 x64 zones are detected and
|
||||
refused**, so an MW3 shortcut to such an install contributes no weapons and no
|
||||
maps, and says so in the log (`IW5 zone is serialized x64 (Steam re-release)`).
|
||||
|
||||
The folder holding the two executables is the process working directory even
|
||||
when Explorer supplies another; `IW4L_GAMES` defaults to it, and `.lnk` targets
|
||||
inside it are additional read-only search roots. `.env` is loaded before
|
||||
release-manifest defaults, created only when missing and never overwritten.
|
||||
|
||||
`.env` holds `IW4L_UPDATE_URL=https://host:8443/prod` plus the master keys
|
||||
from [`MASTER.md`](MASTER.md). An unreachable master is not a launch error: the browser keeps retrying. All
|
||||
writable state stays below `iw4l-artifacts/` (including `settings.cfg`, unless
|
||||
`IW4L_SETTINGS_PATH` overrides it); shortcut targets are never output paths.
|
||||
|
||||
Startup errors print the detected FastFile counts, missing base assets and log
|
||||
path in the console. On Windows, an interactive console waits for Enter before
|
||||
closing; redirected input exits immediately with code 2. No extra dialog opens.
|
||||
|
||||
Reference in New Issue
Block a user