Decode Black Ops audio natively and load Black Ops II worlds, faction
models, equipment, materials, lighting and weapon camouflage.
Rework capsule collision, solid recovery, penetration, recoil, effects
and sentry placement; synchronize late joins and player event delivery.
Keep local skill ratings and signed player accounts.
Show the selected community and master ping on the main menu. Add twenty
asset-filtered class presets across IW4, IW5, T5 and T6, plus fifty-two
unique player-style names shared by AI bots and dummies. Honor the chosen
dev descriptor in the two-client approved scenario and credit contributors
and weapon, map and attachment QA testers.
Validated with the approved gameplay lifecycle and two-client dev-master
scenarios, installed class-asset audit, bot-name uniqueness checks and
Linux IW4/IW5/T5/T6 gameplay smoke runs. Foreign-weapon match-stat schema
coverage remains incomplete.
Tactical insertion: under IW4 rules offhands are bridged by name only, so
the upstream slot stand-ins no longer hand the scripts a flash for it (it
plants and sets the spawn again); hidden entities stay usable, so its
owner picks it back up.
Equipment stats: read T6 detonate delay/time, damage cone, forward and
view-relative up speed, the detonator flag (C4 detonates) and the
retrievable flag (owner pickup only where T6 allows it).
Equipment behaviour: iw4l_t6/equipment ports T6's bouncing betty, shock
charge, trophy system, sensor and EMP grenades onto IW4's scripts, plus
T6's one-of-each counts, claymore grace and respawn cleanup. The engine
reports those items under their own names, tags thrown grenades with
nativename/weaponmodel, keeps sensors planted, drops the claymore laser
from betty and trophy, and lets objective_team address one player (sensor
pings, drawn in FFA too; protocol 103).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The match bootstrap set allow_debug_actions to true for every match.
The host therefore accepted the debug actions from any peer.
Add the --cheats launch switch for map, serve and menu.
The switch sets the HostCheats resource. The match install reads it.
Cheats are off when the switch is absent.
Pass --cheats in make map, the live recipes and the scripted harnesses.
Scoped weapons hold breath with recovery feedback and sound. Riot shields
block and deflect bullets in hand and on the back; explosive crossbow bolts,
destructible map cars with round resets, and T5 map model collision work.
Remote players show last stand, weapon change, jump, mantle, landing,
ladder and stun animations, and server hitboxes follow the shared torso tree.
GSC gains spectator follow cameras, chat, kick, vehicle paths and flight
controls, physics spheres, slow motion, slide and mantle movers, damage
triggers, music, ambience, rumble and live audio fades over a shared mixer.
IW5 and T5 maps get sun shadows with per-stage sun selection; post-fx
pipelines compile before a vision blend needs them. The lobby browser keeps
focus and selection, and rooms can be password protected. Console supply
and give commands search every item category. Frames and replays decode
only the current wire protocol.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Controls list Aim Down the Sight and Hold Aim Down the Sight; pressing hold
clears a toggle latch so release always leaves ADS. MWHEELUP and MWHEELDOWN
bind from the menu, the console and saved settings, and wheel scroll reaches
the game as discrete presses, so weapon cycling works on the wheel.
Closes#6Closes#7
- Pipelined rendering is off by default on macOS. The single-threaded Render
schedule runs `create_surfaces` on the render thread, and AppKit only lets
the main thread touch the NSView behind the Metal surface, so the first
frame panicked in raw-window-metal. IW4L_PIPELINED_RENDERING still
overrides it.
- docs/BUILD.md: macOS needs only the Xcode command line tools; game data is
the Windows depot of a Steam copy fetched with steamcmd.
- README and docs index list macOS (Metal) among the platforms.
- One common set per content profile, shared by every match and the loadout
menu; a restart over the same map keeps its images and static geometry.
- Weapons resolve as families: attachments are chosen from each game's own
tables, checked, and composed into prepared configurations with their
first-person skeleton assembled once.
- Sessions end through one teardown and one process exit; retired resources
are dropped on a worker, not the frame.
- Sound is captured by the walks that already read each zone.
- A loading screen that reports what the load is doing.
- Tests live only in crates/approved_tests (heavy_gameplay_lifecycle);
publish-check refuses them anywhere else.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Destructibles walk their stage staircase on the host: each stage carries
its model, the parts it throws off, the effects it fires on the way out
and the health it bleeds afterwards, and the loops a prop is speaking are
published as sound aliases the client resolves through the configstring.
The scoreboard, overhead names and the compass draw against the live
roster -- team identity survives player-supplied color escapes, ranks and
prestige come out of the menu tables, and the final snapshot is kept when
it lands ahead of the next scores command.
Film vision presets load from the map and common fastfiles; visionSetNaked
and visionReset transition between them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Host bots bake a navigation graph off the clip map and walk it — drop and
break-glass edges, routes that resume across ticks under a shared per-tick
query budget, firing positions chosen from baked supports rather than a ring
around the bot. They pick a weapon, reload or switch when the magazine runs
out, and plant, defuse and claim flags on their own.
A broken pane now builds a planar crack graph and emits shards carrying their
holes and open cracks, so tessellation and repeated hits read the stored
geometry instead of assuming a convex fan from vertex zero.
Sun cascades are clipped to the view. Class select and the options menu match
the layout players expect. Weapon definitions load from IW5 and T5 fastfiles.
The test suite is removed: it had grown into scaffolding for the dig rather
than a description of the runtime, and new tests will be written against the
behaviour that stayed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The split of an image variant into `payload` and `usage` is now what sharing
keys on. A registry holds the texels an archive entry decodes to — the mip
chain in one allocation, laid out as an upload wants it — and the `Image` is
built over them where the row is placed. Two materials that want the same file
under a different colour space or sampler cost one read, one inflate and one
decode, and the second of them a copy of the chain; the last asker copies
nothing, because the decode's own buffer becomes the image's. Asking whether a
name is a cubemap reads the thirty-two byte IWI header out of the entry instead
of inflating the megabyte behind it.
The resident-bytes ceiling is charged per payload rather than per batch, so two
batches pointing at one buffer hold one allocation between them. Load rows
split `produced_bytes` into `prepared_bytes` and `reused_bytes`: only the first
is work, and their sum is what the plan served. The merge census names who won
a disputed claim by plan id, not only that somebody did.
The frame clock closes the row and opens the next at the top of `First`, before
anything else runs, so every main-phase interval falls wholly inside one frame;
the frame the process exits from is closed at exit and its row carries
`partial`. Which spans are a frame's coverage roots is declared by
`Span::coverage_root` rather than read off a thread's stack — an executor is
free to run a schedule's `begin` and its `end` on two workers — and spans a
migration left behind are pruned. A counter measured on another thread is
charged to the frame it ran in through `emit_at`, and a sample that finds no
row is counted rather than moved to whichever frame is open.
`IW4L_PIPELINED_RENDERING=1` runs the render world a frame behind on its own
thread, and it is the only thing the switch moves; the manifest records which
branch a run took. The report gains `main/render overlap`, read off the
intervals rather than off the frame a render stage names as its origin. A
`bevy-trace` build writes bevy's own spans as a Chrome trace, which is the only
place the render graph's submit can be timed from, and
`scheduling.bevy_tracing` says whether a run was one.
Glass keeps its shattered material while damaged, scales the authored UV to it,
picks its shatter effect by whether the piece is landing, and carries the
surface normal on the presentation event.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An image variant's identity was one digest of the archive entries plus a bag
of option bits, which mixed two unlike things. The map type chooses between
the 2D and the cubemap decode, so it changes the bytes; the colour space and
the sampler state change no byte of the payload, only the `Image` built
around it.
The identity now splits along that line: `payload` digests the resolved
entries and the map type, `usage` carries the colour space and the sampler.
Sharing still keys on the whole variant, because the wrapping lives inside
the `Image` whose buffer would be shared.
The merge census reads the split. A discarded claim whose winner held the
same payload under a different wrapping was counted as overridden by another
source, which reported nothing to share where the same archive entry had in
fact been read into the same bytes twice. It is its own bucket now,
`discarded_same_payload`, on the discard line and in the image demand report.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A 47.78 ms frame reported a 36.35 ms `Load` inside an `Update` its own row
implied was 11.87 ms, and 364 frames summed their root spans to more than their
own wall. Three mistakes stacked: a span was credited to whichever frame was
open when it *closed*, its parent was whichever span had opened most recently
anywhere in the process, and the remainder was a sum of overlapping intervals
clamped at zero.
Now a span is charged by intersection with the frame's wall, and what it ran
before the frame opened is `carried_in_ns` rather than nothing. Nesting comes
from a thread-local stack, so the render thread no longer adopts the main
thread's spans, and a close out of order is counted. `unclassified` is wall
minus the union of the root intervals, which cannot go negative and counts two
threads inside one nanosecond once. The report prints how many frames still
hold a span longer than its parent.
`present_hud_surfaces` and `present_hud_stage_max` measured wall *between*
systems, which `.chain()` leaves the executor free to fill. They are renamed
`*_schedule_interval`, tabled apart, and given a number that is the HUD's:
`hud_tess_body`, taken inside the tess flush bodies, with `hud_tess_jobs`
beside it. The screenshot flag now stays up through the readback, the encode
and the write instead of only while the request queues.
An image decode gains an identity — the resolved IWD candidate list plus the
recipe bits that change the payload — so two plans that would prepare the same
bytes share one decode through a registry of weak handles, and a claim the
merge drops is reported as the same variant twice or as another game's
different image under the same name. Merge precedence and the final content are
untouched. The load job row splits budget wait from decode, renames
`join_wait_ms` to what it measures, and carries `produced_bytes`; the budget
wait is bounded so it cannot outlive a join order that stops guaranteeing
progress. The manifest walks the nested artifact cache instead of reading zero
files from its top directory, and prints the viewport the passes were sized by
beside the surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`make bench` printed two reports and left nothing behind but a text file, so a
number could be compared with another number from the same afternoon and not
much else. It now prints three and writes the run's own directory: `report.txt`,
`manifest.json` and `summary.json` beside the `trace.pftrace`.
Section [3/3] is the counters — the render stages, the GPU passes, and the
per-frame work census. A counter nobody sampled prints MISS rather than
`0.00`, and the counters and spans this build declares but this run never
reached are named at the end, so a hole in the instrumentation cannot read as a
fact about the workload. GPU passes overlap and resolve late, so they are never
summed.
`manifest.json` says what produced the number: the revision, the dirty flag,
the toolchain, the profile cargo actually built under, the locked `bevy` and
`wgpu`, the adapter and whether its device can time a pass, the window, the
pools, the env toggles and the cache state. The build half is baked in at
compile time, because asking `git` at runtime names the tree the process is
standing in rather than the build. A fact nobody could read is `null`, never a
plausible default.
Reading that report, `preparing match audio` was the largest stage of every
load — one row holding one number, which cannot tell a thousand cheap byte
loops from five hundred external processes. It is the second. Section [1/3] now
splits the stage by the decoder each clip took, in worker time, and counts what
each one prepared, failed, held and left resident.
The T5 XWMA decode goes through the artifact cache under `xwma_pcm`, keyed on
every input the decode reads, so a clip is decoded once per machine rather than
once per launch. A cold load still had to pay the first time: 515 `ffmpeg`
processes, 18.9 s of worker time, almost none of it decoding. A worker now
takes the jobs already queued and answers up to 64 clips through one process —
every clip its own input and its own output, nothing concatenated — which is
1.3 s for the same bytes. Failure stays per clip: a batch `ffmpeg` refuses is
re-asked one clip at a time, leaving the bad clip as the only failure.
`make bench-overhead` can point at either recorder, `VAR=IW4L_PERF` or
`VAR=IW4L_BENCH`; they are separate costs and each needs its own pairs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`make bench` is one recipe over one run now. `IW4L_BENCH=1` arms an in-process
recorder on the spans that already feed Perfetto, and at exit the run prints
two independent reports and writes them to `iw4l-artifacts/bench/<stamp>.txt`:
the waterfall from the shell command to a playable map, carrying the time each
load stage held the pool alone, and frame time as a span tree whose nesting is
recorded rather than declared. Neither needs a trace file or Trace Processor,
and either can be MISS while the other stands. `bench-demo` and `bench-load`
are gone — a goal after `bench` is a demo, `ZONE=` is a live map.
A bot is no longer minted onto an id a real client already owns. The roster
steps over the ids the authority and the local player hold, and a slot that
still lands on the local player is retired, because `is_bot` would otherwise
claim the player and nothing could drive the slot anyway.
A stretch with no authority ack re-adopts the next snapshot and counts one
stall; three of them still fail the link. Failing on the first turned every
long frame into a permanent disconnect.
The loading overlay comes down once the world is installed rather than waiting
on the class-select hop, which a replay never makes — it was playing the whole
demo behind `UiLayer::Loading`, with map ambience held back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bHwyKwUjaYmtVG1F3Ttku
The zone walk and the runtime were the same types. A catalog carried the
walk's `Ptr → row` link maps, its half-captured names and its scratch into
the published result, so nothing said the linking was over except a
convention — and the runtime, which has no zone open any more, could not
have used them anyway.
Build and published types are now separate for materials, effects, tracers
and reticles: `MaterialDefinitions`, `FxDefinitions`, `TracerDefinitions`
are what a finished build hands on, `publish` is the only way to get one,
and the pointer lookups and absorbs stay behind on the builder. The
presence those pointers stood in for is one shared type, `AuthoredRef {
slot, alias }`. Every published asset type is free of zone pointers except
`WeaponCombatFx`, which waits on splitting `WeaponRegistry` the same way.
That split also turned up dead code the old types had hidden:
`resolve_after_absorb` looked weapon icons up by pointer after `finalize`
had emptied the map, so every lookup there returned `None`.
Map load is a transaction with one terminal outcome. Preflight assembles a
`MatchInstallPlan` — nothing to publish exists until every check that can
refuse has passed — and any refusal writes `MapLoadFailed` for the request,
including "no drawable world", which used to be a `MatchInstalled` the
lifecycle read as success and the UI read as failure. A canceled walk
returns `MatchLoadOutcome` instead of orchestrating on with empty products.
The match carries one material population (`PreparedMatch.materials`)
rather than reaching for it through geometry that may not exist, and the
transport schema around the boundary — the six `LoadCapture` wrappers,
`PreparedSpawn`, the doubly-owned map facts, `ZoneMemory` held to the end
of install — is gone.
Copies that had no reason to exist: the absorbs that already consume their
donor now move its rows instead of cloning every `ModelSkel` and
`RawXAnimParts`; sim and the dyn-ent physics share one `ClipMeshTables`
behind an Arc instead of each holding a deep copy of the collision.
Ownership on the live side follows the same cut. `SessionCore` takes a full
room identity and revision before any transport effect, so a stale
teardown cannot override a different match key. `HostMatchEvent::Applied`
carries no match key, so the peer's phase is its whole guard — it now
passes before anything is written, instead of landing a retired match's
connection id on the live peer on the way to being refused. `ClipStore` owns every PCM
conversion and every terminal decode failure, with Play/Stop on one ordered
channel scoped by namespace, entity and epoch. `SimContent` holds the
immutable definitions behind a consumed builder, so authority and
prediction share one backing instead of deep-copying per tick. Direct UDP
launch is removed; relay, listen and replay stay.
In the renderer each producer publishes whether its rows actually moved,
and the xmodel merge stops re-hashing seven producers' draws, materials and
vertex counts every frame to find out. Payload fields are `pub(crate)`
behind read-only accessors, so a consumer that wanted to finish a
producer's work no longer compiles. Colour extract is split from the
installed world, scene and shadow get their own bind caches, and record
census stays off the hot refusal path.
Docs: `CONTEXT.md` gains "Before shipping: the scaffolding goes back out",
and `make mr` and `cargo xtask mr` point at it. MAP-LOAD describes the
transaction as it now ends.
Scaffolding: the share and census probes that only proved the split are
out. What stays is five tests, each holding several independent systems to
one claim and each verified to fail when that claim is broken:
- one alias carried from two-zone capture through absorb, edge binding,
the alias-chain walk, the prep workers' decode and finally cancel —
every stage has to mean the same clip by it;
- a replacement match driven through all three cores that decide whether
a player is in one (host admission, session view, local load), then
every event the retired match still had in flight, replayed against a
live peer, moving nothing;
- a rejected room view leaving no bridge identity, membership, bootstrap
or admission effect behind;
- the seven draw producers published through their own API and asked of
the consumer that replaced the merge's per-frame hashing: the stamp
moves for exactly the producers whose rows moved;
- authority and prediction proved to share one immutable world by
tracing it, and to keep tracing their own after the other replaces it.