The mirrored first-person model leaves its geometry mirrored; a numeric
display drawn mirrored (rasterizer_set_model_skinning's winding flag) now
shows each digit's bitmap turned over in u (1-u on the first map's texture
transform) and swaps the digits' order across the display (the quad of the
n-th digit from the right shows the n-th from the left; digit count from
the counter limit, the compass's single digit unchanged). Only for 2d,
non-screen-space first maps of numeric chicago/generic shaders, only when
mirrored: right-handed and flat output unchanged.
debug.mirror_first_person (Android, default false): off the headset the
first-person weapon is mirrored across the camera's forward/up plane as
the left hand's is in VR, for checking such things in the emulator.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Walking up from a node to the display through parent_node_index never
ended where the root names itself as its parent: the game hung loading a
level left-handed (Steam Frame). Each step now goes only to a parent
before its child, as vr_skeleton_build does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its node ("... display") and what hangs from it are mirrored back about
the display's own centre after the pose is mirrored: it stays where the
mirrored gun has it, reads as in the right hand, and its triangles keep
their winding (rasterizer_xbox.c decides per skinned part). After
HaloCE-Quest-VR (CC0).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel's second page (MORE) gets "Graphics" (vr.graphics: Low,
Medium, High, Max), "Refresh rate" and "Resolution" rows, with a note
of what they do. The refresh rate steps through the rates the runtime
offers (XR_FB_display_refresh_rate: host_xr_refresh_rates, new host
imports with host_xr_set_refresh_rate) and changes at once (vr.c asks
for it when vr.refresh_rate changes); where the runtime offers none
(SteamVR on the Steam Frame, whose rate is SteamOS's) the row shows the
rate it runs at, from its frames' period, and stays as it is. The
resolution (vr.resolution_scale, 70 to 120%) is read as the app starts:
the row says when a restart is needed. vr.log_performance's late frames
follow the rate as it changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The VR options panel gets a "Weapon hand" row (vr.handedness) and a
"Mirrored controls" row (vr.mirror_controls, dim when right-handed). Its
rows now go on two pages with tabs beside the title (PLAY, MORE; up and
down run on across them, a tab clicked shows its page). The controller
buttons page is built from the hands' jobs: the weapon hand's trigger
and grip, the free hand's, the move and turn sticks and the face
buttons (Quest X/Y for A/B; Steam Frame D-pad for A, B, X, Y) on the
sides the settings put them; the wrist, wheel and aiming notes name the
hands. network_vr.c's notes name the weapon and the free hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vr.handedness = "left" puts the weapon in the left hand: aim, muzzle,
laser, crosshair, scope and turret aim follow the left controller; the
grenade, the foregrip and the fingers' closing round a grenade move to
the right hand, the grip's melee to the left. The first-person model is
posed as the right hand's in a mirror image of the room ("model space":
vr.c mirrors the controllers into it, first_person_weapons.c the body)
and the posed skeleton mirrored back, after HaloCE-Quest-VR (CC0); a
model whose node matrices mirror has its triangles' winding turned over
(rasterizer_set_model_skinning -> d3d8_gl.c, ended with the model and
each transparent group). halo_vr_left_hand is now halo_vr_free_hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The elbow could not be lifted out to the side: it bent only towards the
fixed pole (down, out, back). An elbow lifted out rolls the controller's
top in, so the pole now leans towards the hand's underside (the weapon's
for the right arm, the free left controller's for the left; the hand on
the foregrip keeps the fixed pole). With the free arm solved before its
hand is turned it looked right on the Quest: vr.arm_ik defaults to true.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As HaloCE-Quest-VR does it: the free arm is bent to the controller's grip
from the animated pose (the hand free to swing with it), and only then is
the hand turned to the controller and the roll shared along the forearm.
Turning the hand first left the solve to swing it on, which stretched the
arm's skin like a rubber band (Quest, 2026-10-06). vr.arm_ik stays off by
default until it is seen right.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Second Quest test: with the elbow out to the side the right elbow's skin
turned inwards: the shortest turn of the solve left the upper arm's hinge
where it was. vr_align_elbow now rolls the upper arm about its length so
the animation's bend lies in the plane the arm bends in, and the upper
arm takes no share of the hand's roll any more. The open sleeve end could
still be seen: the shoulders are 5 cm higher (20 cm below the neck).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
First Quest test: the free left arm's skin stretched like a rubber band
with the new elbow IK, so vr.arm_ik now defaults to false (the arm's shape
as before, pointed at the shoulder); and the open end of the right
sleeve came into view looking down to the side: the shoulders now stand
4 cm behind the neck instead of 3 cm in front of it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- vr.body = "arms" (default): arms and hands as before; "hands": the arms'
bones shrunk to a point 3.5 cm behind each wrist along the hand's line
so the cuff closes behind the hand (vr_hide_arm, after HaloCE-Quest-VR's
vr_hide_forearms, CC0), no arm IK solved; "off": the hands model is not
drawn (one model draw less an eye) and nothing of the arms posed; the
weapon, its aim with both hands, barrel, laser and crosshair stay.
- vr.c reads vr.body with the live settings (halo_vr_body_mode).
- The VR options panel gets "Body" (Arms and hands / Hands only / Weapon
only) and "Fingers" (Tracked / Off) rows; its twelve rows are 22
pixels apart (were 26) so the panel keeps its box.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- vr_arms.c: vr_finger_nodes, vr_pose_finger (after HaloCE-Quest-VR, CC0,
nearly verbatim): each finger's three joints found by name under the
hand, posed by absolute directions in the palm's frame (a little bent
at rest, a fist at full curl; the thumb up the handle or across the
palm), each segment's subtree turned with its length kept. Its touch
of the world (rays per finger) is left out for the Frame's processor.
- The free left hand takes its controller's curls (halo_vr_finger_curls:
index from the trigger, the others from the grip, the thumb from where
it rests; round a held grenade). On the foregrip the hand keeps the
weapon's pose.
- The hand on the weapon keeps its grip; only its index finger is lifted
part way off the trigger while the controller says it is (touch known
and the finger off it), never where touch is unknown.
- Joints found once per skeleton; the cost is a few dozen node turns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- port/linux/game/vr_arms.c: vr_solve_arm and vr_distribute_arm_twist,
after HaloCE-Quest-VR (CC0), nearly verbatim: the shoulder reaches 8 cm
and the arm stretches up to 18% out of reach (further, the shoulder
follows the held hand, as this port had it, so the arm stays whole),
bends no further than 145 degrees, the elbow's swing eased per arm
(vr_limb_history), the hand's roll shared by forearm (85%) and upper
arm (25%) within 110 degrees, continuous across half a turn. The
skeleton is read through the graph's index; the nodes under each node
come from one pass a frame (vr_skeleton_build) rather than walking
ancestors per node; time is the headset's frame's (halo_vr_pose_time).
- Both arms use it; the right hand and a left hand on the foregrip stay
where the weapon puts them (hand_stays), so shots, barrel and laser do
not move.
- The free left hand: its wrist and fingers are set on the controller as
before, and the arm now bends to it from the shoulder instead of being
carried rigidly. The ribbons of e377e4dc did not come from skin on the
root (3a.0: none hangs there), so no triangle filter is needed; the
rigid arm stays behind vr.arm_ik = false, read live.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- vr.c keeps the torso's heading in LOCAL space (torso_update, after
HaloCE-Quest-VR's vr_body_heading, CC0): the head's heading, steady at
any pitch, drawn halfway towards the hands when both are ahead (at most
45 degrees), followed beyond a 15 degree cone, eased at 10 a second;
reset on a recentre or a gap. In LOCAL space the stick's snap and
smooth turns carry it at once (HaloCE-Quest-VR's "arm IK lags as the
body turns").
- halo_vr_body_frame gives the torso's heading and the neck the head
turns about (9 cm behind the eyes); the shoulders hang from the neck,
square to the torso, so turning the head no longer swings them (as the
head faces the torso, where they were before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Posed from the camera at the right controller, the first-person weapon
and the arms are the same for both eyes and the scope's view, yet were
built (animation, interpolation, arm IK) for each: twice a frame on
SteamVR, three times zoomed in. The pose is kept for the rest of the
headset's frame (halo_vr_frame_serial) once built from the controller,
for the same weapon; posed from an eye's own camera (the hand not
tracked), each eye still builds its own. vr.log_performance counts the
builds and the poses kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- vr.log_performance reports how long the first-person pose takes a frame
(its animation and the arms), how often it is built, and the arms' part
of it (halo_vr_timing_start/_end).
- Once per hands model it logs how many triangles hang from each node and
how many of those from the skeleton's root ('frame bone24'), read from
the part's own buffers (rasterizer_model_buffer_data, after
HaloCE-Quest-VR, CC0). In the emulator the cyborg's hands have none on
the root: the ribbons of e377e4dc did not come from root skin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: d3d8_gl.c (multiview variants kept beside the per-pixel lit
shaders: shader[4] and lit_shader[4], vertex_shader_get takes both;
multiview targets before multisampling; the picture read resolves both),
nv2a_vsh.c/xgpu.h (lighting and multiview parameters), render.c (no
anti-aliasing pass on VR eye passes), dsound_sdl.c, p2p.h.
VR: anti-aliasing off (the eyes go to the headset from their textures);
the VR online list skips password games (the server browser asks).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixups for #99 and #112: the version raised for the object message's
shader permutation, NETCODE.md brought up to date, a networked glass
break's sound placed where it broke, a frame scratch pool that runs out
logged once, and the new comments marked as the port's.
The host's notice of a broken surface carries no damage effect. The client filled a damage record with zeros and then read tag 0, which is the scenario, and halted. Shards still spawn. They skip the kick from an effect this machine never received.
The frame scratch pool was still the Xbox size after the particle pool grew eight times, so a busy level spent it every frame. It now grows with that particle pool, and a frame that still runs out is logged once.
A console line longer than the buffer halted inside the error printer, before the reason was written to the log. Long lines are cut to fit, and the reason is written first.
- bots: no more than the game's MAXIMUM PLAYERS has room for beside the
people (instead of raising it); the host options show and change the
bots there are
- VR options hint on the PC main menu drawn whole (left-aligned)
- the online and host panels: a click beside them or on their key's B
closes them, on the online panel's X enters a code
- spinners' arrows (drawn just outside them) clickable
- Android flat (the emulator): the mouse drives the menus' pointer, for
testing the menus' clicks with adb shell input mouse
- Frame launcher: Lepton's logcat of the game's container in logcat.txt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- menu_tags.c builds the PC menus in VR too unless vr.menus = "xbox"
- their text fields (Server Setup's game name) typed on the Xbox's
on-screen keyboard in VR
- join code / online panel on the PC Server Browser (X or click its hint),
host options (Y or click) and join code on the PC lobby, VR options on
the PC main menu
- bots: room made for them beside the people (Server Setup's maximum
players), none in co-op
- player_name_valid accepts trailing spaces (Xbox menus pad new profile
names, "New001 ", which the PC lobby refused)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream's online co-op (network_actors.c, network_coop.c) ticks beside the
VR port's VR relay and bots; its message types end at 77, below the VR
port's from 240. The look and move flags the action test reads moved into
player_control_action_test_note: the head's turning goes there too. The
vertex shader's screen-space undo is upstream's on every platform now, with
the multiview eye offset after it on Android. The APK carries upstream's
brokers.txt beside the FDM layer stub; the updaters keep the VR port's own
repository (upstream's now points at OpenCommunityEdition/OpenCE).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The launcher gives Lepton what Steam gives an Android title of its own (a
compat data path, which the app's data links to after an install, and the
shader path), keeps a container id of its own, tries a start that ends at
once again (a new APK's first start only installs it), and stops when the
game was quit. Steam's "Exit game" cannot reach the game in Lepton's
container: the VR options get "Quit Halo" (asked twice), which on Android
now ends the process and leaves quit_requested.txt for the launcher. The
library's pictures are in port/android/steam_frame_grid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hosting a co-op level from the Map screen (CO-OP CAMPAIGN, a level and its
difficulty, then Server Setup) makes a network game on the campaign map
with no game engine, which is what co-op is. Only the host runs the
scripts and the AI; port/linux/game/network_coop.c sends clients what the
scripts do (cutscenes, camera, fades, HUD, sounds, devices, named objects,
animations), and network_actors.c drives the host's AI units on every
machine. Players spawn beside each other, respawn beside a safe teammate,
and spectate while dead or waiting. Skipping a cutscene is a vote.
Players can join a game in progress at any time.
Backing out of a hosted game keeps its server: the lobby's last local
player leaving pauses it (netgame_unjoin_player). Choosing Create again
threw the host's client away, but network_game_start_new_server makes and
joins a server only when there is none, so the new client never joined the
one left over. The lobby then had nobody in it: START added no player, B
could not leave (not in pregame), and the server's map changes failed to
reach the client that was gone.
Create now disposes the client and server first, so it always makes a new
game and joins it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SteamVR offers the Frame's controllers by their own profile
(XR_VALVE_frame_controller_interaction); without bindings for it, it mapped
the Quest's onto them, and X and Y landed on the left one's D-pad. Now: the
right one's A, B, X, Y and menu button are the game's A, B, X, Y and Start
(the Xbox's layout), the left one's View is Back and its D-pad the D-pad,
the bumpers switch grenades and the flashlight. The VR options' page of
buttons shows the Frame's layout there. The runtime's extensions and the
controllers' profile are in the log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream's server browser of public internet games (p2p_lobby.c) and the VR
port's own public list share the brokers: its topics are a table now, and
the VR port's (join codes' and public games') are entries of it; the VR
port's retained publishes carry MQTT 5's empty properties. The VR port's
public toggle lists the game in the server browser too, and the VR online
panel shows the server browser's games (PC, phones, VR) first, joined by
their invites, then its own list's that it has not.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The triangle the game draws over each teammate goes into the name's slot
over the head (shown in the world there, not on the HUD before the eyes,
where it hung distorted), under the name, or alone when names are off. On
the screen where controllers join by pressing A, a click is the pad's A. On
the screen of games to join, the online games' hint goes into the key bar's
free left and the join status under it, clear of the map's picture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A choice the stick moved to (a map among the tiles) is no longer taken back
by the pointer resting on another, until the pointer moves well away; and a
click beside a list's slots or a value's box, along the way it steps (where
its arrows are drawn), steps it back or on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A panel of the VR settings one may change while playing (turning, snap
angle, smooth speed, the vehicles' comfort vignette, the Warthog's wheel,
the weapon laser, the crosshair, the motion tracker on the wrist), written
to config.toml, which vr.c reads again as they change; and a page of what
the controllers' buttons do, on foot, in vehicles and in the menus. The
pointer works both, and its hint opens the panel. The menus' screens are
named in the log as they come up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream's PC menus are kept for the desktop; in VR the Xbox's stay, which
the VR port's join codes, game list, host options and keyboard pointer are
built into. config_write_integer now writes through upstream's config_write.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The game type editor's kills to win (Slayer and Team Slayer) went up to
50, which a game of many players reaches in moments: it now also offers 75,
100, 150, 200, 250 and 500. The spinner's items are its string list's
(ui.map's 5, 10, 15, 25, 50) and then strings of the port's own, each with
its description; the options list's descriptions count only a spinner's
items of the tag's own, so those of the rows after it are where they were.
A spinner with extra items is a little wider, for three digits.