Commit Graph

200 Commits

Author SHA1 Message Date
Gh4zi 025ea1259d Boot: zone discovery skips cargo output and the artifacts folder
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>
2026-10-11 17:13:50 +03:00
Gh4zi a690ea55c0 Sun shadows: trees cast through their shadow-caster mesh again
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>
2026-10-11 17:13:49 +03:00
Gh4zi d9f497322e Sun shadows: script models out of view still cast into it
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>
2026-10-11 17:13:49 +03:00
vc 891edead2e Merge pull request #94 from vladtrc/fix/pipe-crest-mr
Prevent uncommanded lift at slope transitions
2026-10-11 15:59:51 +03:00
vtroc7 3ce719abee Keep snapped movement tangent to the landing surface 2026-10-11 13:41:33 +03:00
vtroc7 b8ded62c23 Restore T5 character animation, killfeed colors and killcam audio continuity v0.1.0-demo.17 2026-10-11 00:10:03 +03:00
vtroc7 a2f189df10 Scope match lifetimes and restore gameplay and menu presentation
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>
v0.1.0-demo.16
2026-10-10 23:01:34 +03:00
vtroc7 5273074e72 Integrate contributor rendering, gameplay and development fixes 2026-10-10 01:33:23 +03:00
JohnM c94e2197d1 Let pipelined rendering run on macOS by creating the surface during extraction
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>
2026-10-10 05:23:47 +08:00
JohnM 3039cf4943 Draw effect model pieces such as cash register bills
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>
2026-10-10 05:20:19 +08:00
JohnM a2f3f830e6 Draw spark fountains where they were sprayed, at their own size
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>
2026-10-10 05:01:01 +08:00
JohnM 2036c3653e Stop models vanishing for two frames when the model lighting cache runs dry
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>
2026-10-10 05:01:01 +08:00
JohnM 4fbae4ba4c Walk single-player zones to the end by reading path data at its real layout
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>
2026-10-10 04:55:11 +08:00
JohnM 71e6e18c1c Draw props whose materials a map borrows from common_mp
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>
2026-10-10 04:55:08 +08:00
JohnM 9b0c1a9459 End a private match from End Game and return to the lobby
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>
2026-10-10 03:45:37 +08:00
JohnM e4159bb901 Reopen the private lobby when its match fails to load
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>
2026-10-10 03:45:37 +08:00
JohnM e6a658bf33 Keep the baked bot navigation when only weapon or class data changes
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>
2026-10-10 03:40:04 +08:00
JohnM d934e6ea33 Keep bots inside the 18 player slots instead of colliding with corpses
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>
2026-10-10 03:40:04 +08:00
JohnM d3bd8e23fc Fill screens wider than 16:9 with the main menu soldier art
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>
2026-10-10 03:35:30 +08:00
JohnM 9f21620750 Never strand a return to the menu behind a late disconnect
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>
2026-10-10 02:19:00 +08:00
JohnM da8ec04bb7 Return the host to the private lobby when the match ends
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>
2026-10-10 02:19:00 +08:00
JohnM 7157feb9bd Bind the script glass functions so Strike loads
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>
2026-10-10 01:18:51 +08:00
JohnM 0a0d598a22 Lay a dropped riot shield flat on the ground
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>
2026-10-10 00:14:22 +08:00
JohnM d04ea2af59 Leave a dropped gun on the ground when only its ammo is taken
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>
2026-10-10 00:13:38 +08:00
JohnM 10c5c84c6c Slide dropped weapons down walls instead of hanging in the air
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>
2026-10-10 00:12:47 +08:00
JohnM 7ac8c28f8d Rebuild the xmodel merge when effect pieces or dynents start or stop drawing
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>
2026-10-10 00:12:09 +08:00
JohnM e95b40a0c3 Admit the sky model to the xmodel merge only when it has draws
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>
2026-10-10 00:11:45 +08:00
JohnM 21e2bd00f7 Remove the oldest dropped weapon at the limit, not the lowest-numbered one
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>
2026-10-10 00:11:35 +08:00
JohnM 05e5f4e695 Move the killstreak map cursor with the controller sticks
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>
2026-10-10 00:10:43 +08:00
JohnM 72cd946147 Free a projectile's entity slot when it ends without exploding
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>
2026-10-10 00:10:14 +08:00
JohnM f5981b1245 Steer the remote missile with the controller sticks
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>
2026-10-10 00:08:54 +08:00
JohnM 3953364247 Free the last match's body animations before the next map loads
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>
2026-10-10 00:08:48 +08:00
JohnM 9c4d4320f4 Treat a deleted entity as undefined for the rest of its frame
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>
2026-10-10 00:08:34 +08:00
JohnM 09bae0b5ae Keep the Camera view row off the Controller and Game Folders pages
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>
2026-10-10 00:08:20 +08:00
Gh4zi 61b7c43e28 Fix flower pots floating up when woken; walking past moves clutter only
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>
2026-10-09 07:40:52 +03:00
Gh4zi 65a7ce4963 Fix animated models vanishing when the first cell queued for their bounds is not theirs
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>
2026-10-09 07:22:07 +03:00
Gh4zi b65454ab88 Fix models disappearing as the player moves; fix red tint on breakable glass
- 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>
2026-10-09 03:47:41 +03:00
vtroc7 91217cd0ac Restore native effects and objectives, scope zoom, camouflage and lighting v0.1.0-demo.15 2026-10-08 22:04:32 +03:00
Matthew Wilding 1b8c4df703 Add Nix flake and direnv
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.
2026-10-08 12:33:49 +08:00
vtroc7 a2f4e0b857 Restore cross-game class previews and loadout customization v0.1.0-demo.14 2026-10-08 00:19:44 +03:00
vtroc7 983fd97853 Publish prepared presentation contracts and predict local bullet impacts v0.1.0-demo.13 2026-10-07 22:44:14 +03:00
vtroc7 a1fc0f902c Align hosted match epochs for first-person weapons v0.1.0-demo.12 2026-10-07 21:11:35 +03:00
vtroc7 25c008b391 Prepare coherent match assets, native camouflages and server releases
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.1.0-demo.11
2026-10-07 20:30:21 +03:00
vtroc7 7eff9406fb Connect native soldier hands and finalize match rule integration v0.1.0-demo.10 2026-10-07 12:52:50 +03:00
vtroc7 19b9438bab Merge pull requests #71 #72 from NeptuneTheGoat 2026-10-07 12:52:25 +03:00
vtroc7 ea45cf7dcc Preserve native families, soldier hands and T6 world materials v0.1.0-demo.9 2026-10-07 00:13:36 +03:00
neptune 682f714148 Add Constant Radar, Enemy Bots and Friendly Bots game rules
Constant Radar (Gameplay Options) keeps every player's radar constant
unless an EMP blocks it, and the compass shows every living enemy each
frame, Cold-Blooded or not. Enemy Bots (0-9) and Friendly Bots (0-8, team
modes only) under Team Options add that many bots when the match starts;
a sided bot waits for the local player's team, then answers the team menu
with it or the other one. Free-for-all ignores the friendly count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:36:47 -04:00
neptune 078ed7fbda Stop rehashing FX element random draws every frame
Each FX random draw is a BLAKE3 hash. Velocity graph walks hashed the
same three per-element draws for every sample they touched, and the
element seed was rehashed several times per element per frame across
update, sort and draw. Take the velocity draws once per walk and keep
the element seed on the slot, set at spawn from the same inputs, so
every value is unchanged.

12 bots on mp_rust: fx_update 0.57 -> 0.25 ms per frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:36:47 -04:00
neptune 96e9f701d5 Speed up the GSC scheduler and instruction loop
One pass over the waiters instead of a quadratic dead-thread scan; a
validated serial-to-entity cache for find_thread; heap collection every
ten ticks walking ids by reference; execute no longer clones each
instruction's location, and keeps the runaway-loop budget local, syncing
it with the runtime only around instructions that can run other threads.
With 12 bots the authority step falls from about 7.1 to 3.6 ms a tick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:36:47 -04:00
vtroc7 f1f4c07feb Bind prepared runtime assets to their owners and correct T6 rendering v0.1.0-demo.8 2026-10-06 21:37:31 +03:00