present_pose drew a body between its previous and current snapshot only
when both were TR_INTERPOLATE. A falling or sliding corpse (TR_GRAVITY)
was drawn at its snapshot position for a whole 50 ms and then jumped, and
so was every switch between resting and falling.
Each snapshot position of a corpse is already where the body was at that
snapshot, so corpses are now drawn between the two whatever their
trajectory type. Players keep the old rule.
A corpse slot is reused for later deaths, so an entity number can hold a
new body. A corpse whose death time (time2) or victim changes is now a new
entity and is not drawn sliding over from where the old body lay.
Seen on the Mac: a flying body's drawn position changes on every frame
while the server moves it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client kept a corpse's animation tree only while the corpse's tr_time
stayed the same. The server changes tr_time whenever a body's fall changes:
when it goes over an edge, when it slides down a wall, and when it lands.
Each change copied the tree again from the victim, whose tree is already
gone because a dead player is not posed, so the corpse got a fresh tree
with the death clip back at its first frame. Bodies flew off standing
upright in the clip's first 50 ms, stood against walls, and then played
the whole fall again where they stopped.
The server now stamps each corpse slot with the level time of the death
(spawn_time_ms, sent with the corpse pool), and the client keys the tree
on that time and the victim. Both stay the same for one body's life and
change when the slot holds another body. When there is no victim tree to
copy (a body first seen late, or an entity number that now holds another
body), the fresh tree starts as far into the death clip as the body is
old, so an old body shows the pose it already lies in instead of dying
again.
The corpse pool's wire format changes, so PROTOCOL_VERSION is 117.
Seen on the Mac (mp_favela, point-blank SPAS-12 kills of a bot on an open
terrace, with temporary logging): every body copies its tree once and its
death clip plays once, from the victim's pose to the end. Before, the same
kill copied it 18 times and drew the body upright in the clip's first
50 ms for about 0.8 s. On an iPhone build, point-blank shotgun kills on
mp_favela no longer glided off upright, stood pinned to walls or replayed
their fall.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A body whose death animation carried it past an edge stayed in the air at
its death height, until newer deaths recycled the slot. The open side of
the cliff stairs on mp_estate showed it often. phase_run_corpse_move set a
body resting (TR_INTERPOLATE, not falling) after every unblocked animation
step and never looked for ground there. Its only downward probe ran for
ragdolls, which never start, and its test was reversed.
One body's server tick is now step_corpse:
- on the ground only the animation step's x/y moves the body. It follows
the ground down by up to a stair step (18 units), so it goes down slopes
and stairs without hopping. With nothing within a step below it falls
from rest, so it goes over an edge instead of floating;
- in the air the trajectory keeps its clock and speed, so gravity builds,
and the animation step is added to its base once;
- a falling body that meets a wall slides down it with the vertical speed
it had and lands. Before, it restarted its fall from rest every tick and
stood pinned against the wall for up to a second;
- a body stopped part way on the ground moves its trajectory base with it,
so clients draw it where it stopped.
The clone velocity clamp now holds x and y to 80 in both directions.
Before, only +x and +y were held, so a body thrown toward -x or -y flew
further.
Seen on the Mac (mp_favela, seven point-blank shotgun kills of a bot on an
open terrace, pb_shotgun_death_back2): the body travels 106-111 units (the
clip's root motion is 120), drops over the terrace edge, lands after
0.35 s and slides down the slope without falling again. Against a wall it
slides down and lands in 0.2 s. A player killed 300 units up lands in
0.8 s. On an iPhone build, bodies killed on the mp_estate cliff stairs
dropped onto the slope, and point-blank shotgun kills on mp_favela flew
back a little and dropped.
There is still no ragdoll, so the death clips' root motion still moves the
body, their end poses (legs up, headstands) remain, and limbs can still
reach into walls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the games root is the build's own `target/play`, discovery walked every
file of cargo's output (incremental, deps, build, .fingerprint, examples) and
the artifacts folder, several times at boot: the window sat white and "Not
Responding" for seconds before the map loaded. Those folders never hold
zones, so the walk skips them (retail MW2, MW3 and Black Ops installs have no
folders by these names).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MW2 trees (Favela's oaks: mc/mtl_tree_oak_*) cast their sun shadow only
through a separate, never-drawn mesh (mc/mtl_tree_shadow_caster). Its
shadow-only material was kept out of the static model colour material keys,
and the sun shadow pass looked its material up in those same keys, so the
surface was dropped and the trees cast nothing. What stayed was the
lightmap's sun mask: MW2's shader forces shadow where the mask is 0, which
showed as blocky dark rectangles on otherwise sunlit ground under the trees.
Shadow-only materials are now listed in their own key, read by the sun
shadow pass only; the colour pass still never sees them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Favela's hanging laundry is animated script models. They were posed only
when the camera's scene-entity walk reached them, so once the clothes left
the screen their shadows on the wall below vanished (only the static rope's
stayed). IW4 culls scene entities for each sun partition apart from the
camera; here a script model the walk hid is kept for the sun shadow only
(colour refused, no lighting request) while its bound is inside one of the
sun partitions.
The partitions are published before the models are posed, from one shared
builder the command context now uses as well, so both see the same planes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Prepare progression and loading through Bevy scheduling, preserve round HUD and bombsite props, restore grenade and T5 shadow presentation, and keep menus usable while classes load. Report update progress and continue startup after update failures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IW4L_PIPELINED_RENDERING=1 panicked on the first frame on macOS ("can only
access NSView on the main thread" in raw-window-metal), which is why the
default is off there. Bevy's create_surfaces takes a NonSendMarker so that
the multi-threaded executor hands it back to the main thread, but the render
app's Render schedule runs on a single-threaded executor. With pipelining on
that schedule runs on the render thread, so create_surfaces built the
CAMetalLayer there and touched the NSView off the main thread.
With pipelining on, macOS now also runs create_surfaces in the render app's
ExtractSchedule, after extract_cameras and so after the windows are
extracted. Extraction runs on the main thread, so the surface is created and
configured there. The Render schedule's instance then finds it configured
and skips, or after a resize reconfigures it, which only sets CAMetalLayer
properties and is allowed off the main thread.
The macOS default stays off, so nothing changes unless
IW4L_PIPELINED_RENDERING=1 is set. docs/BUILD.md now says how to turn it on
instead of pointing at the plugins.rs explanation this removes.
Measured on an Apple M3 (mp_terminal, 9 bots, uncapped present, bench
recorder), pipelining off -> on: 87 -> 105 fps average, frame p50 10.8 ->
8.3 ms, p99 19.4 -> 18.4 ms, worst frames about the same (36 -> 34 ms),
main/render overlap 5.4 ms per frame, presented state one frame older.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Effect model pieces (the bills and halves of a shot cash register, food,
luggage contents, plant pots, glass pieces) have not drawn since 69cf9ff
moved the effect model geometry into the Plan phase of the world spawn.
That phase runs before world_occupancy::place builds the model lighting
atlas, so prepare_fx_model_geometry found no atlas and quietly returned an
empty plan, and every model elem that passed its LOD and cull checks was
dropped for want of geometry.
The geometry is now built in spawn_world_finish straight after
world_occupancy::place. The Plan phase still installs an empty plan so its
readers find the resource. A missing atlas is now a warning with the model
count, and one line reports the models and LODs prepared and refused.
The bills had a second fault. mp_terminal stores `,fx_cash01` as a
placeholder for common_mp's fx_cash01, and the effect catalog kept the
comma in the model hint, so the edge never bound. Model hints now use the
bare name (AssetRef::bare_name), so fx_cash01 resolves against common_mp.
One model whose packed rows do not match its decoded vertices would make
the packed payload of all effect model geometry Unavailable, and the merged
xmodel lane then drops every xmodel draw (viewmodel, bodies, script models)
for the frame. Such a LOD is now refused and logged when the geometry is
built, and fill_fx_model_plan only draws effect models from geometry whose
Iw4 packed rows cover every decoded vertex.
The player's own first-person shell casings are still skipped by the
render-flag check in fill_fx_model_plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shooting a vending machine on mp_terminal drew large flat yellow, white
and red sheets next to it and made the game stutter. A spark fountain that
runs relative to its effect was sprayed from the effect-local spawn origin,
and the custom draw then placed the cells with the cloud matrix T(pos)
R(quat) S(vis scale). Prop sparks (props/credit_card_touchpad_exp on the
card touchpads, and explosions/powerlines_* from the vending machine's
tv_explosion) carry a vis scale of 150, so every spark was drawn 150 times
too big; barrelexp and small_vehicle_explosion (scale 10) were 10 times
too big. New cells also had every sample time at 0, which ends the ribbon at
origins[3]: about 1,780 untraced cells stacked one 150-unit quad on the
effect point until the tracer, 16 cells per update, reached them.
The spray now gets the world spawn point (spawn_origin_world), untraced cells
start as an unobstructed arc (spark_fountain_integrate_miss_cell), and the
custom draw is submitted with a unit placement (spark_fountain_draw_cloud).
The cells are already in world space and the fountain shader only applies
view and projection, so the vis scale stays a draw gate and no longer scales
the vertices. Bounce traces now run in the same space as world collision.
The elem keeps drifting on its velocity graph (0.4 units/ms on most
fountains) while the sparks stay where they were sprayed, so the fountain is
now culled around the spray point, with the radius grown by how far a spark
can have flown by then (max speed * t + |gravity| * t^2 / 2, times the bounce
fraction when it is above 1). Sparks no longer follow an effect that moves
after spraying them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The model lighting cache has 1024 tile slots and keeps a slot for two more
frames after its owner leaves it. Only glass could rewrite its own slot when
it moved. Every other moving owner (awake physics props, players, corpses,
the viewer's own sample) took a fresh slot each frame and so held three.
When the pool ran dry, as it does when many models need a slot at once, an
owner that missed got handle 0 and was not drawn that frame (props, players,
corpses, effect and world models, the viewmodel); a glass pane was drawn
with default lighting instead. One mp_highrise match (2412 dynents) on an
iPhone logged "model light cache alloc failed — remote body skipped" on 14
frames.
WorldModelLightingCache now records which owner each slot was given to, and
an owner that still holds its slot rewrites that tile in place when it
moves. The tile write is queued after every earlier frame's draws, so those
draws still read the old tile. An owner that still misses, one with no slot
of its own, is drawn with the viewer's tile for that frame, and the log line
names it. The viewer's own sample has no tile to borrow, so a miss there
still hides the viewmodel for that frame. A failed light-grid sample still
returns no lighting, as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 32-bit layout of the single-player game world left out the path
data's chain-node count, which sits between the base nodes and the two
chain-node arrays. Both chain-node arrays, the node visibility bits and
the node tree were read four bytes early, so the visibility bits were
walked as tree nodes and trainer stopped at its game world with an
invalid pointer and one unsettled offset. The glass pointer was read from
the vehicle track's segment count the same way. The reader already took
the visibility byte count and the tree count from the positions that only
fit with the count in place, and the 64-bit layout was already right.
load_path_data now reads the arrays and the tree after the count, and
load_gameworld_sp reads the glass pointer from the last field of the
game world.
trainer, roadkill and so_killspree_trainer now walk completely; common,
code_pre_gfx, code_post_gfx, patch_code_pre_gfx, patch and ui walk
completely as before. Each lands exactly on the end of its image, and
every non-temp block ends at the size its header declares. mp_terminal,
mp_rust and common_mp walk unchanged; multiplayer zones carry no
single-player game world.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A map zone streams the materials it shares with common_mp as ",name"
references: stubs with no textures and no technique set. Since
capture_material refuses a material without a technique set, the slot of
such a reference was never bound and every model surface on it was
dropped: 97 of the 14551 map model surfaces on mp_terminal and 28 of the
14125 on mp_favela, among them a surface of the wall fan cs_wallfan1 and
one of the air conditioner com_ex_airconditioner.
When the capture refuses a reference, the slot is now bound to the real
material of the same name and namespace, which the common pool already
holds. A reference with no real counterpart stays unbound as before.
",mc/shadowcaster" references bind now too, so scene_lit_pass_material
skips shadow-only materials for script models and dyn ents, as the static
model path already does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Yes button of the pause menu's End Game popup is rewritten to
`disconnect` (03016c48). The host of a private match therefore quits
instead of ending the match: nobody sees the "Host ended game"
end-of-match flow, and the host lands on the main menu with the lobby
gone.
The button now sends the "endround" menu response to the game scripts,
which end the match as host-ended, run the end-of-game display and then
end the level, so the host returns to the private lobby. The hud still
turns that response into `disconnect` unless the local player hosts a
private lobby (ui_lobby_host set, ui_lobby_public not set), so public
lobbies and clients leave as they do today.
This reverses the end-game leave from 03016c48 for private hosts only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a private match fails to load, the session returns to the menu
without a world and the frontend only handled that case for public
lobbies. The host was left on the main menu with in_lobby and is_host
still set but game_lobby closed by the loading screen, which also kept
the Community Servers switch locked.
A host of a private lobby whose map fails to load now gets game_lobby
reopened under the map error, the same way as after a match end.
Quitting during the load now resets the party, and the host stays on the
main menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The baked walk graph is reused in memory and cached on disk under the
world's content_digest, which also folds in the weapon tables (combat,
penetration, admission, camouflage, equipment) and the class catalog.
The walk never reads those: it traces clip geometry and script-model
collision from the spawns and objectives. So any weapon or class change,
including installing or removing a game that donates weapons, sent every
map back through a cold bake on its next load, one of the longest steps
of a first map load.
The graph is now keyed on the `map` (spawns and clip brushes) and
`models` (script-model collision) content components only, and
NAV_SCHEMA goes to 9 so graphs cached under the old key are not
reused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bot took the next unused client id with no upper bound, and the roster
only capped bots at MAX_HOST_BOTS (20). The lobby's Enemy and Friendly
Bots rules plus a `bot add`, or two `bot add` commands, could therefore
make more than 17 bots beside the host. Client ids are also entity
numbers, and the corpse ring starts at entity 18
(PLAYER_CORPSE_ENTITY_BASE), so bots 18 and up shared an entity number
with a corpse: the net layer rejected the snapshot entity as a
centity-number gap, and the end-of-match client data overran its 18 rows
(setclientmatchdata IndexOutOfRange).
Bots now claim ids 1..17 only, wrapping around to reuse freed ids, and
adding stops when every slot is taken.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The soldier picture on the main menu and Find Lobbies (rect -427 -240
854 480, centre aligned) is exactly 16:9 wide. On a wider screen it
stops short of the edges: about 319 px per side at 2560x1080, 439 px at
3440x1440, and 87 pt per side on a 2.17:1 phone. Past its right edge
only the stretched background and glow show, with a faint vertical edge
where the art ends.
Add an opt-in MenuItem.cover flag. When a stretch item with cover set
is narrower than the screen, push_stretch scales it up uniformly about
its centre, keeping the top edge, until it fills the width. The factor
comes from the item's un-animated rect, so the open/close zoom still
plays. Set the flag on the soldier item of both menus. Nothing changes
at 16:9 or narrower, including the default 1280x720 window and 16:10
displays. On very wide screens the bottom of the art is cropped. The
clouds item starts at the left edge of the 16:9 area, so its edge still
shows on the left of a wider screen; it is left as it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the match scripts end a match, the session swap requests a
MatchEnded teardown. A disconnect that arrives before the teardown fact
is read turns the pending Menu target into a leave, and the swap then
waits for a Disconnect teardown that never comes. No ReturnedToMenu is
written, and every later map or Start Game fails with "session swap
already in progress" until a restart.
A swap back to the menu now finishes on whichever teardown fact arrives,
but still waits for one. The reason reported with the return still
follows the target, so a late disconnect lands on the main menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the match scripts end a private match, the session swap already
tears the world down as MatchEnded, but ReturnedToMenu did not carry the
reason. The frontend treated every return from a world like a disconnect:
it reset the party, so the host landed on the bare main menu and Start
Game refused with "Only the lobby host can start a match" until a new
lobby was created.
ReturnedToMenu now carries the teardown reason (None for a failed load).
After a MatchEnded return the host of a private lobby keeps the party,
and once the main menu is open again the frontend reopens game_lobby over
it with map, mode and rules kept. If the main menu does not come up
within five seconds, the party is reset as before. Disconnects, failed
loads, public lobbies and clients still go to the main menu. Creating a
lobby or starting a match clears a pending reopen, so it cannot reset
the party later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mp_strike's map script breaks its "brokenglass01" panes in main() with
GetGlassArray and DestroyGlass. Neither native was bound, and an unbound
native refuses the whole program at install ("2 unbound natives:
destroyglass getglassarray"), so the map never loaded.
The glass reset now also reads the map's named glass table (a targetname
and its piece indices), and the session installs it next to the glass
panes as a GlassNames resource. getglassarray, getglass, destroyglass
(the existing script shatter), isglassdestroyed and getglassorigin are
bound on top of it. The map script only runs at the next scheduler pass,
after the glass is installed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The riot shield's world model stands upright, as it is carried, with its
origin at the grip. A dropped item gets only the dropper's yaw, so a
dropped shield stood on edge with half of it in the floor: a thin upright
sliver beside the body.
A dropped shield, recognised by its weapon type, is now pitched onto its
back. Guns keep pitch 0 as before. A gun dropped by a swap now keeps only
the yaw of the item it replaces, so a gun swapped for a shield does not
inherit the shield's tilt.
To see it: on Rust, die holding a riot shield after the start grace period.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Walking over a dropped gun of a type you already carry took its ammo and
deleted the whole item. add_ammo_on_ps caps the total at max_ammo, so every
round that did not fit was destroyed with the gun. A touch only fires for a
gun you carry, so every touch pickup is ammo-only, and a victim's gun at the
killer's feet vanished at once when both used the same weapon. The game's
pickup script treats a trigger that swapped nothing as an ammo pickup and
keeps waiting on the item, so the item is meant to stay.
grab_number now takes only the rounds there is room for (stock first, then
the right clip, then the left) and leaves the gun on the ground with the
rest. A gun with nothing left is removed as before. The pickup record
carries only the rounds taken.
With the item staying, one player's ammo touch and another player's swap of
the same gun can land in the same tick. settle_items retired the item's
script entity right after its first trigger, so the death notify ended the
script thread waiting on it and the swap that followed was lost. It now
raises every trigger first and then retires only the items whose row is
gone, so a gun still on the ground keeps its script entity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
think_item_move ended a fall only on a floor. A falling drop whose trace
hit a wall or a ceiling kept its old falling trajectory but was held at the
contact point on the server, so it hung there for good, while clients, who
draw the trajectory, showed it flying on through the wall and under the
floor.
A blocked fall now rests on a floor, when the trace starts in solid, or on
a hit with no usable normal. A wall or a ceiling restarts the fall from one
unit off the contact point with the velocity into the surface removed. The
push off is traced, so in a tight gap the item moves only as far as is
clear, and rests at the contact point if nothing is. When the push meets a
second surface, as in an acute crease between two walls, the velocity is
clipped against that one too, and if it still points into the first wall
the horizontal part is dropped. Without that the gun bounced from wall to
wall every tick for up to about 1.8 s before it fell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merged xmodel lane patches draws in place (the concat path) while its
topology key holds. That key hashed which body, script, missile and item
draws the scene admits, but not whether effect pieces or dynents had any
draws, and a full merge leaves a source with no draws out. So when dynents
went from no draws to some while the merge was in its concat layout, their
draws were spliced into a layout with no room for them: they indexed the
wrong surfaces and shifted the sky ranges behind them. The same happens to
the first effect piece after a frame with none. Whether each of the two has
draws is now part of the occupancy hash, so either transition forces a full
merge.
Effect model geometry is also appended whole instead of compacted to the
surfaces drawn that frame. It holds every prepared model at every LOD and
does not change once built; compacting it dropped the concat layout, so
every frame with a live piece would re-merge and re-upload the whole
vertex buffer.
Effect pieces do not draw yet, so for now only the dynent transition is
reachable; the effect piece half prepares for drawing them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sky model whose surfaces were all refused still publishes a plan with no
draws. The full merge leaves a source without draws out, but admitted_mask
marked the sky admitted whenever the plan existed. The next concat recopy
then added sky rows the merge never laid out, the packed payload came out
the wrong length, and every xmodel draw (viewmodel, bodies, props) was
dropped for the frame. This repeats on every frame whose vertices change.
The sky now counts as admitted only when it has draws, like the effect
model and dynent sources. Only the T5 and T6 lanes produce a sky model.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The map keeps at most 16 dropped items, scavenger bags included. When it
was full, push_dropped_item removed the item with the lowest entity number.
Numbers are reused oldest-freed-first once the reuse quarantine is over, so
a fresh drop often took a low number and was removed at the next death
anywhere on the map. Replaying a 10-bot FFA, about half of all drops lived
under 5 s, and a killed soldier's gun often vanished within a second or two.
Each item now carries its drop order (drop_seq, one past the newest live
item) and the cap removes the oldest. Only the authority evicts and the
next value comes from the live items, so drop_seq needs no snapshot field;
items rebuilt from a snapshot carry 0.
Script delete() and the map_restart cleanup removed item rows without
freeing their entity slots, so every drop the game script deleted leaked an
Item slot for the rest of the match. Both now go through
despawn_dropped_item, which removes the row and frees the slot. This has to
land with the cap change: drops that live longer are far more often deleted
by script.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LocationCursor::step read only mouse motion, so with a controller the
map cursor of Precision Airstrike, Stealth Bomber and Harrier stayed at
the map centre and the attack button called the strike in there. The
sample_client_input closure for location selection cleared only the
mouse, so the sticks meanwhile still turned and walked the hidden
player.
The move stick now moves the cursor at LOCATION_PAD_SPEED map widths
per second, or either stick when there is no direction to choose. The
look stick points the direction once pushed past half deflection, and
aim plus the move stick drags the direction as holding aim does with
the mouse. Look inversion is undone for the map, so stick up is always
map up. While the map is up the closure clears the pad look delta, move
axes, turn rate, lock-on and auto-aim target, so the sticks no longer
reach the player. Mouse and keyboard behaviour is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A projectile that reached its cleanup time without exploding (both cleanup
paths in think_projectile), one removed because its script entity was
deleted (settle_projectiles), and every projectile cleared by map_restart
lost its row through remove_projectile_by_number but kept its Missile slot
for the rest of the match. Only splash detonations, whose slot is held for
the dying-missile event and freed when it expires, and pickups gave their
slots back. The leaked slots shrink the dynamic entity pool over a long match;
once it is full the 500 ms reuse wait is skipped and freed numbers are
handed out again in the same frame.
Add FrameWorld::despawn_projectile, which removes the row and frees its
slot, and use it on those paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
remote_control_axes filled the Predator's remote_control axes from mouse
motion only, so with a controller the missile flew straight down, while
the look stick still turned the hidden player's view through
pad_look_delta.
Either stick now steers the missile, whichever is pushed further. Stick
up is nose up and stick right turns right, following the sim's
remote_control sign rather than the mouse's, so the pad terms are added
outside the m_pitch/m_yaw factors and do not depend on the mouse sign
convention. pad_look already carries the look inversion; the new
pad_invert field gives the move stick the same pitch sign, so both
sticks agree when look is inverted. pad_look_delta is cleared while the
missile is linked so the hidden player's view no longer turns. Mouse
steering is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every PersistentRemoteTree holds an Arc<SoldierBodyAnimation> (since
25c008b), which owns the match's player XAnimCatalog and BodyMeshCatalog.
reset_anim_for_match clears the other per-match animation state on
MatchInstalled and MatchTornDown, but not RemoteBodyTrees, and the trees'
only pruning (retain_live in pose_remote_bodies) runs once the next match
poses remote bodies. When a different map loads, load_prepared_match drops
the resident copy of the last match before walking the new one, and from
then until the first remote pose the trees alone keep the old match's
catalogs alive, through the peak of the next load.
Reset RemoteBodyTrees with the rest of the per-match state. The trees are
rebuilt when remote bodies are posed in the new match.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
delete() only queues an entity; it is retired at the scheduler's frame
boundary so scripts can still read its fields for the rest of the frame.
isdefined() looked only at retired entities, though, so an entity deleted
earlier in the same frame still tested defined.
The game's scripts expect the opposite. common_scripts/_pipes deletes the
sound of a finished pipe burst and immediately runs array_removeUndefined
on its list. The dead sound stayed in the list, and the next hit on that
pipe faulted in pipe_logic (DistanceSquared on the sound's now-undefined
origin), took the "return false" path, and the pipe never burst again for
the rest of the match (mp_highrise).
isdefined() now also treats an entity with a pending delete as gone.
Field reads are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Controller and Game Folders pages are built from pc_options_video
and hide the Video rows they do not use one by one. The Camera view
selector (ui_third_person) on the Video page was not in either list, so
it stayed visible. On Controller it sat on the same row as ADS
Sensitivity (y=108), drawn over its label and slider and able to take
focus and clicks. On Game Folders it overlapped the Modern Warfare 3
row.
Hide video_third_person and its label on both pages, as Advanced Video
and Community Servers already do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A physics prop's hull is its model's whole bounds, a potted plant's leaves included, so a pot
where the map placed it can already overlap a pillar or shelf. Once woken, every physics step
found it starting in solid and lifted it a unit, so it rose until the hull cleared the
obstacle and fell asleep in the air; each later wake lifted it again. A prop starting in solid
now stays where it is and settles.
Walking past woke any physics prop whose bounding sphere touched the player's box and gave it
the player's velocity: a large pot's sphere (75 units, the leaves) reached the player well
before the pot did, so walking near one pushed it along. Only clutter (cans, bottles, luggage)
is moved by walking now; bullets and explosions still move breakable props.
Checked on mp_terminal: pots floated after walking past them on this branch before the change;
with it, walking past the bookshop pot (com_potted_plant_large at 1177 5453) leaves it on the
floor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scene DObj seen through several cells gets a bounds command per cell visit. The first one
to run bounded the DObj and tested its box against that command's cell; every later command
saw the gate already past idle and returned, so a DObj whose box did not reach the first
cell was dropped even when it reached the next. Terminal's atrium pine
(foliage_cod5_tree_pine05_large_animated) was queued first for the eye's cell from some spots
in the hall and disappeared there.
Later commands now test the bounded box against their own cell until the DObj is visible.
Checked on mp_terminal: the pine shows at 1682 6115 192 (missing before) and 1554 6135 192;
the rooftop tank at 2187 4290 304 still shows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Map models (Terminal's rooftop tank, boxes and barrels; Favela's laundry) disappeared and
came back as the player moved, out in the open as well. The DPVS walk visits a cell once per
visibility portal it is seen through, but only the first visit's clip planes were kept, and
the cell entity commands culled against those alone, so anything visible only through a
later portal was dropped. The walk now reports every visit and the cell entity commands run
once per visit, as IW4 queues them. More visits than the cell queue holds falls back to the
camera frustum, never to one visit's planes.
- Separately, breakable glass skipped model lighting for a pane whose centre was more than 64
units off screen, then drew it anyway with lighting handle 0 and reflection probe 0:
Terminal's wide glass panes showed a red tint with a hard edge. The test now uses the pane's
own radius.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dev shell for x86_64-linux, aarch64-linux and aarch64-darwin. The toolchain
comes from rust-toolchain.toml with rust-src and rust-analyzer, and .envrc
loads the make-style .env.