Compare commits

..
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 88354e5c6c VR roadmap: left-handed mode and graphics presets done on both headsets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:21:35 +02:00
Stefan MaierandClaude Opus 5.5 8cdce233e4 Android settings: config.toml written beside itself and renamed over
A config.toml pushed by adb belongs to the shell and the app cannot open it
for writing, so the VR options' changes (handedness, graphics, refresh rate,
resolution) lasted only until a restart. Writing a temporary file and
renaming it over the old one replaces it all the same (the folder is the
app's), and a write cut short leaves the old file whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:55:22 +02:00
Stefan MaierandClaude Opus 5.5 81137e3af3 VR roadmap: left-handed mode and graphics presets good on the Steam Frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:40:01 +02:00
Stefan MaierandClaude Opus 5.5 fb670e79b9 VR roadmap: the left hand's ammo counter reads the right way round
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:33:11 +02:00
Stefan MaierandClaude Opus 5.5 8022a0b4a1 VR left-handed: the assault rifle's ammo counter reads the right way round
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>
2026-10-06 20:33:11 +02:00
Stefan MaierandClaude Opus 5.5 696383f191 VR roadmap: the left hand's mirrored ammo display, an open point
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:01:39 +02:00
Stefan Maier 9a33ecd2d3 Revert "VR left-handed: the assault rifle's ammo display reads the right way round"
This reverts commit 530a32b64889c24e64c5e805949e31df1c68fb73.
2026-10-06 20:01:19 +02:00
Stefan Maier 9447bc2ae4 Revert "VR left-handed: the ammo display's subtree found without hanging"
This reverts commit 8e40d6ecacd92f3938e329e528a27c3eddd8dbe1.
2026-10-06 20:01:18 +02:00
Stefan Maier f04c8c4045 Revert "VR left-handed: a part is drawn mirrored only where all its nodes are"
This reverts commit 1a6061b932e7f293cc40351f90f4d0d68edb2bd2.
2026-10-06 20:01:18 +02:00
Stefan MaierandClaude Opus 5.5 b026590824 VR left-handed: a part is drawn mirrored only where all its nodes are
The assault rifle's ammo display, mirrored back, vanished: its part's
winding was judged by its first node alone, which may be the mirrored gun,
so its triangles were turned over and culled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:54:01 +02:00
Stefan MaierandClaude Opus 5.5 2a2b2cdbf3 VR left-handed: the ammo display's subtree found without hanging
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>
2026-10-06 19:50:52 +02:00
Stefan MaierandClaude Opus 5.5 5861e57d99 VR left-handed: the assault rifle's ammo display reads the right way round
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>
2026-10-06 19:47:53 +02:00
Stefan MaierandClaude Opus 5.5 50d685e465 VR roadmap: step 2 in part built (left-handed mode, graphics presets, refresh rate)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:33:27 +02:00
Stefan MaierandClaude Opus 5.5 0e6b006878 VR options: graphics, refresh rate and resolution rows
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>
2026-10-06 19:30:32 +02:00
Stefan MaierandClaude Opus 5.5 ca73ba7909 VR: graphics presets (vr.graphics)
port/linux/game/vr_graphics.c (after HaloCE-Quest-VR's, CC0) sets the
game's own effect switches each frame before drawing: "high" (the
default) draws everything as before; "medium" no object shadows,
environment reflections or lens flares; "low" nor specular light, bump
maps, detail objects (grass), the screen's fog layer, contrails, weather
or camouflage's second pass; "max" is "high" with per-pixel model
lighting and 512x512 shadow maps (d3d8_gl.c asks halo_vr_graphics). A
switch the game changes (scripts, console) is taken as its wish and
combined with the preset's. A client in another's game keeps grass, lens
flares and the fog layer as the host's rules have them; cheats.c leaves
the environment parts the preset may keep off there (shadows, specular,
reflections).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:28:48 +02:00
Stefan MaierandClaude Opus 5.5 c254d6df05 VR: left-handed, the rumble's motors follow the hands
The game's light (right) motor rumbles the weapon hand, the heavy one the
free hand (halo_vr_weapon_hand), as right-handed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:26:40 +02:00
Stefan MaierandClaude Opus 5.5 9910c4a9dc VR options: weapon hand and mirrored controls, two pages
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>
2026-10-06 19:26:03 +02:00
Stefan MaierandClaude Opus 5.5 b1d59e654d VR: left-handed, the motion tracker on the right wrist
The wrist layer goes on the free hand's wrist: halo_xr_submit.wrist_hand
(new, after wrist_down); host_xr.c keeps both grips and places the layer
on the right wrist as the mirror image of the left one's, read the right
way round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:22:25 +02:00
Stefan MaierandClaude Opus 5.5 1f3d1be207 VR: left-handed controls, mirrored with vr.mirror_controls
Left-handed, in play, vr.c hands xinput_sdl.c the controllers with the
weapon hand's trigger and grip as the right side's (fire, melee, the
wheel's throttle) and the free hand's as the left's (grenade, foregrip,
brake). vr.mirror_controls (default true) mirrors the sticks (move on
the right, turn and steer on the left), their clicks, the bumpers and
the face buttons too: on the Quest X/Y for A/B; on the Steam Frame, whose
A, B, X and Y are all on the right controller, the left D-pad takes them
as their mirror image (down A, left B, right X, up Y) and they take the
D-pad, so every function stays reachable. The menus keep the buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:21:36 +02:00
Stefan MaierandClaude Opus 5.5 d97de6d499 VR: left-handed mode, the weapon in the left hand
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>
2026-10-06 19:20:59 +02:00
Stefan MaierandClaude Opus 5.5 fbe48a869f VR roadmap: steps 1 and 3a done, in main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:53:21 +02:00
Stefan MaierandClaude Opus 5.5 206eb0b52b VR roadmap: the Steam Frame's tests done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:46:55 +02:00
Stefan MaierandClaude Opus 5.5 1c66715a19 VR roadmap: the Quest's tests done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:34:15 +02:00
Stefan MaierandClaude Opus 5.5 87f336df78 VR roadmap: 3a tried on the Quest
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:30:49 +02:00
Stefan MaierandClaude Opus 5.5 0a86e406c8 VR arms: the hand's roll leads the elbow; the free arm bent by default
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>
2026-10-06 17:56:47 +02:00
Stefan MaierandClaude Opus 5.5 c09b40a2ef VR arms: the free left arm solved first, its hand turned after
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>
2026-10-06 17:54:15 +02:00
Stefan MaierandClaude Opus 5.5 1e2006d06c VR arms: the elbow bends in the arm's plane, the shoulders higher
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>
2026-10-06 16:47:45 +02:00
Stefan MaierandClaude Opus 5.5 e69e087602 VR arms: the free arm rigid by default, the shoulders further back
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>
2026-10-06 15:59:41 +02:00
Stefan MaierandClaude Opus 5.5 87a38e2895 VR roadmap: the pending headset tests, performance among them
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:45:44 +02:00
Stefan MaierandClaude Opus 5.5 788ab74a6b VR roadmap: step 3a built in feature/vr-arms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:37:07 +02:00
Stefan MaierandClaude Opus 5.5 6d2dcfaaec VR arms 3a.6: vr.body (arms, hands, off) and vr.fingers in the VR options
- 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>
2026-10-06 11:35:07 +02:00
Stefan MaierandClaude Opus 5.5 98e848de39 VR arms 3a.5: the fingers posed
- 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>
2026-10-06 11:33:12 +02:00
Stefan MaierandClaude Opus 5.5 7754532683 VR arms 3a.4: the IK core from HaloCE-Quest-VR, the free left arm bent
- 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>
2026-10-06 11:31:16 +02:00
Stefan MaierandClaude Opus 5.5 19b0bcb42e VR arms 3a.3: the shoulders follow the torso, not the head
- 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>
2026-10-06 11:26:24 +02:00
Stefan MaierandClaude Opus 5.5 f30dd50317 VR arms 3a.2: the fingers' input (touch, trigger, grip)
- host_xr: two actions, the thumb's touch (stick, buttons, thumb rest,
  the Frame's D-pad and View) and the index finger's (trigger), bound
  for the Touch, Index and Steam Frame profiles (paths after
  HaloCE-Quest-VR, CC0, and Valve's "Steam Frame Input" page) as optional
  bindings: a profile refusing them is suggested again without them.
- ABI: HALO_XR_BUTTON_THUMB_TOUCH, _INDEX_TOUCH and _TOUCH_KNOWN (the
  runtime gives the thumb's touch at all) in the buttons' free bits; the
  structures keep their size (android_abi_check: none unsafe).
- vr.c: each hand's thumb, index, middle and last two fingers, curled
  from those (the thumb half curled where touch is unknown, the hand
  round a held grenade), eased by HaloCE-Quest-VR's critically damped
  spring; halo_vr_finger_curls() hands them to the game; vr.fingers
  (default on) turns it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:24:58 +02:00
Stefan MaierandClaude Opus 5.5 07a15190b4 host_xr: suggest() takes up to 64 bindings, says when it cannot, and retries
suggest() copied a profile's bindings into an array of 32 and silently
dropped the rest; the Steam Frame's profile already has 26, so a few
more would have lost bindings without a word. It takes 64 now and logs
an error past that, logs the runtime's result when a profile is
refused, and suggest_bindings() takes optional bindings besides: should
the runtime refuse the profile with them (one unknown path refuses the
whole suggestion), it is suggested again without them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:23:11 +02:00
Stefan MaierandClaude Opus 5.5 f254be4b45 VR arms 3a.1: the first-person pose built once a frame, not per eye
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>
2026-10-06 11:22:45 +02:00
Stefan MaierandClaude Opus 5.5 a6350d207a VR arms 3a.0: time the first-person pose, log the hands' skin
- 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>
2026-10-06 11:22:08 +02:00
Stefan MaierandClaude Opus 5.5 1929702f5b Steam Frame CPU: Mesa's trace markers quiet, no glGet in the counters' copies; VR roadmap
- Valve's Mesa in Lepton writes a kernel trace marker around every flush
  and context switch; its descriptors on trace_marker are pointed at
  /dev/null once the GL context and the OpenXR session exist (after
  astromaddie/HaloCE-VR, CC0)
- host_gl_counters_snapshot/_read no longer ask GL for the copy bindings
  (each glGet waits for Mesa's GL thread): they are left unbound
- port/android/VR_ROADMAP.md: what is done and the agreed next steps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:58:04 +02:00
Stefan MaierandClaude Opus 5.5 9d6b980ba6 Merge upstream (OpenCE 9e4f8a2d): performance with many enemies, co-op fixes, anti-aliasing, per-pixel lighting, shadow resolution, reverb, password lobbies, network version 20
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>
2026-10-06 09:29:14 +02:00
MrBruh 9e4f8a2dcb Merge pull request #115 from eltanschauung/fix/attached-light-storage
Fix missing dynamic lights and attached glows on Windows
2026-10-06 19:09:18 +13:00
eltanschauung 53c268b8a6 Preserve existing workflow line endings 2026-10-06 01:18:49 -04:00
eltanschauung 592be6270a Run light storage regressions in Windows and Linux CI 2026-10-06 01:17:37 -04:00
eltanschauung 8817c37470 Test native light storage with ordinary and LTO links 2026-10-06 01:17:23 -04:00
eltanschauung d635d83725 Fix dynamic lighting storage corruption on Windows 2026-10-06 01:16:42 -04:00
MrBruh 2a6dc2b738 Show the server browser's lock icon 2026-10-06 18:08:45 +13:00
MrBruh 6112dcfc5a Password-protected public lobbies 2026-10-06 17:51:54 +13:00
MrBruh 62fa7e13ef Co-op player collisions option 2026-10-06 17:03:38 +13:00
MrBruh 9a12828762 Play MP maps alone 2026-10-06 16:27:27 +13:00
MrBruh 7e82e7128e New settings in the settings menus
Video Setup has Anti-Aliasing (Android's choices its own), Shadow Resolution and Per-Pixel Lighting, its rows closer and the help lower for them to fit; Audio Setup has Reverb.
2026-10-06 15:36:22 +13:00
MrBruh 94882796b7 Anti-aliasing
display.anti_aliasing, off by default: "fxaa" or "smaa" (a pass over the
3D view before the HUD and menus; SMAA vendored from port/third_party/smaa,
MIT), "ssaa2x" (the screen's targets at twice the resolution, at most the
GPU's largest target) and "msaa2x" to "msaa8x". Multisampled framebuffers
have every attachment multisampled, so the mirror's view with the back
buffer's depth buffer is too, and a target's texture and renderbuffer stay
coherent. The setting is read again between frames. Android has FXAA for
SMAA and no supersampling.

Based on #38 and #39 by Tyberious.
2026-10-06 15:24:59 +13:00
MrBruh 3dba558e4f Per-pixel model lighting
display.per_pixel_lighting (off by default) lights the models for each
pixel by the same ambient, distant and point lights the model lighting
programs (9, 10, 17 and 27) sum for each vertex. The normal and position
are found in each program's instructions and the program is checked to
light as the pixel shader does; the lights come in a small uniform of
their own, and a shader that fails falls back to per-vertex lighting.

Based on #48 by Tyberious.
2026-10-06 15:08:32 +13:00
MrBruh 1dc533fe18 Shadow map resolution setting
display.shadow_resolution (128 by default, as on the Xbox; 256, 512 or
1024) draws the two shadow maps larger. The blur's taps move to half a
texel of the larger map and two passes more for each doubling widen it
to the Xbox's tent, so the shadows keep their softness.

Based on #47 by Tyberious.
2026-10-06 14:59:21 +13:00
MrBruh 8a8e70599f I3DL2 reverb and muffled sounds
Based on #44 and #46 by Tyberious. 3D voices send to an I3DL2 reverb of the camera's sound environment, and obstruction and occlusion muffle their direct path above the HF reference (occlusion their send too). Both use the fixed distance; a maximum distance under the minimum is held at it. A change of environment crossfades the delays and gains over 85 ms, the filters' coefficients ramp over each mix, the diffusers, lines and filters flush denormals, and the reverb stops while it is quiet. audio.reverb (default true) is read again when the settings change.
2026-10-06 14:39:41 +13:00
MrBruh b115a41263 Resample with a table per phase
Fixup for #42: a voice at the output rate or slower blends two rows of weights by its phase instead of working out each tap's (the same low pass, a third of the work), and a voice turned all the way down only moves on.
2026-10-06 13:58:46 +13:00
MrBruh 320464258a Merge pull request #42 from Tyberious/audio-resampling 2026-10-06 13:57:04 +13:00
MrBruh f5bb75c27c Widen the constant checkpoint serial
Fixup for #52: the checkpoint serial main added since is 64-bit too, as the serials it is compared with are.
2026-10-06 13:56:58 +13:00
MrBruh 5cd1daff77 Merge pull request #52 from Tyberious/constant-serial-wrap 2026-10-06 13:56:35 +13:00
MrBruh 6329be8aa1 Merge pull request #53 from Tyberious/interpolation-snaps 2026-10-06 13:56:35 +13:00
MrBruh 7a2f4caa50 Merge pull request #43 from Tyberious/audio-limiter 2026-10-06 13:56:35 +13:00
MrBruh 938c37a669 Merge pull request #45 from Tyberious/audio-distance 2026-10-06 13:56:35 +13:00
MrBruh fae3fbad69 Merge pull request #41 from Tyberious/adpcm-decoding 2026-10-06 13:56:35 +13:00
MrBruh 5a35ea2b20 Network version 18
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.
2026-10-06 13:39:26 +13:00
MrBruh 2e179ea90f Merge pull request #112 from innerhat-dev/networked-break-and-frame-scratch 2026-10-06 13:34:53 +13:00
MrBruh 9b517e554c Merge pull request #99 from xshxdex98/coop-shader-permutation 2026-10-06 13:29:02 +13:00
MrBruh 595bbc5611 Merge pull request #113 from xshxdex98/quiet-garbage-collection 2026-10-06 13:29:02 +13:00
MrBruh 761210d405 Harden map and network input 2026-10-06 13:26:43 +13:00
xshxdex98 7ba780ef59 Garbage collection's notices are chatter: on screen only with console_log = "all"
With many enemies (co-op's extra enemies) object memory runs low and the
garbage collector prints "garbage collection critical" and "removing
objects" at every pass, filling the screen for the host and every client.
They are the game's own chatter, so they show as game.console_log says, as
the sound and texture caches' already do; the log keeps every line.
2026-10-05 18:55:53 -05:00
justinfilip d9c43e3478 Keep a networked glass break from reading the scenario as a damage effect.
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.
2026-10-05 17:19:35 -05:00
Stefan MaierandClaude Opus 5.5 c04ce7fd00 Steam Frame: each eye drawn apart (no multiview on SteamVR)
With both eyes drawn at once (multiview) the eyes' pictures did not fuse
on the Steam Frame, now and then: each eye alone sharp, both together
doubled as the head turned, in the menus too. With vr.multiview = false
the user saw no doubling, at the same frame time (the Frame's time goes
to the GL driver either way). debug.multiview_steamvr brings multiview
back there for finding the cause; the Quest keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:54:53 +02:00
Stefan MaierandClaude Opus 5.5 3f06b1b9d8 Revert "Steam Frame: the layers' images acquired before the frame waits"
This reverts commit 24e583c0. With the images acquired before xrWaitFrame
the frames came fast (about 8 ms of work) but the picture doubled again
on the Steam Frame as the head turned (181 of 3753 frames reprojected);
without it (673b32b1) it did not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:40:35 +02:00
Stefan MaierandClaude Opus 5.5 1b2d058a04 VR multiview: lens flares placed by their depth in each eye
A lens flare is projected on the CPU (w 1, its depth in z) and drawn
without the depth test, which multiview took for something at infinity,
as the sun's flares are. A weapon's own lights have flares too, at its
markers half a metre from the eyes (the plasma pistol's at its prongs,
the plasma rifle's at its muzzle): each eye saw its flare where infinity
puts it, a green ball at the barrel the eyes could not fuse. While flares
are drawn (rasterizer_lens_flares_draw), each eye's is now placed by the
flare's depth, as its occlusion test already was; the sun's are far
enough for infinity either way. Whether a flare shows at all depends on
its occlusion test, which is why the balls came and went, and showed on
the Frame more than on the Quest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:34:56 +02:00
Stefan MaierandClaude Opus 5.5 b5fa53ed95 Steam Frame: the layers' images acquired before the frame waits
With the copies deferred the wait only moved: acquiring the images took
8-10 ms a frame, the left eye's 6-8 of it. SteamVR's acquire (or its wait
for the image) waits for the GL work queued before it, the eyes' whole
drawing when the left eye's copy asked, the HUD's for the overlay. The
images the last frame filled are now acquired before xrWaitFrame, when
little is queued and the frame would wait for the headset anyway; one a
frame needs that was not acquired ahead is acquired as before. The
performance log shows the time acquiring ahead apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:34:56 +02:00
Stefan MaierandClaude Opus 5.5 1f2c10a3ca Menus' art: a texture outside the art's range is no menu bitmap
Every texture the renderer binds asks whether it is a menu bitmap
standing for its PNG (menu_art_texture), in a game too, where the PC
menus' bitmaps are loaded for the pause menu's settings: a walk over the
150-odd registered bitmaps for each texture of each draw. Their textures
lie together, so one lowest/highest check answers for the map's own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:11:05 +02:00
Stefan MaierandClaude Opus 5.5 12f1155752 Steam Frame: the headset's layer copies without waits in the frame
The Frame's game thread spent most of each 13.9 ms frame waiting, not
working: the left eye's copy into its swapchain took 4.5-7 ms, the right
one's right after it 0.7, each later copy 0.7-3. Every copy read the GL
state back to restore it (glGet*, glIsEnabled, the source's swizzle) and
released its image at once; with Mesa's GL thread (zink) each read waits
for the thread to run everything queued, each release for the GPU, so the
thread stood still for the eyes' whole drawing mid-frame, and frames came
late enough for SteamVR to reproject 2-8% of them (the doubled picture
when turning the head).

Deferred copies (SteamVR by default; debug.xr_deferred_copies 0 or 1 says
otherwise; the Quest's path is unchanged):
- nothing read back: a copy leaves GL as it drew and returns 2; vr.c has
  the renderer set its state again (d3d8_host_state_changed: the state
  cache invalidated, its vertex arrays bound again)
- no swizzle round trip: copies read render targets, which have none
- the images kept until the frame ends and released together before
  xrEndFrame; the eyes' work flushed to the GPU once they are copied
- the performance log times acquiring and releasing apart

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:11:05 +02:00
MrBruh 2b0327bc80 Network version 17
Fixups for #102, #103 and #105-#107: the version raised for the glass,
scenery, cutscene cluster and revert messages, NETCODE.md brought up to
date, co-op's followers' object lists freed as soon as they are used, and
extra enemies that find no free ground placed on the rings they used before
rather than left out (at 32x a quarter of them were), with the places found
taken not tried again for the rest of the squad, and the way to a place
looked along level.
2026-10-06 08:59:22 +13:00
MrBruh 58aa352705 Merge pull request #107 from xshxdex98/sync-glass-and-scenery 2026-10-06 08:40:24 +13:00
MrBruh 59c4b2160e Merge pull request #106 from xshxdex98/coop-scripts-for-everyone 2026-10-06 08:40:24 +13:00
MrBruh 1cf336f0ff Merge pull request #105 from xshxdex98/coop-enemy-spread 2026-10-06 08:40:24 +13:00
MrBruh d10969dc83 Merge pull request #103 from xshxdex98/coop-backtrack-team 2026-10-06 08:40:24 +13:00
MrBruh fe1d51dd58 Merge pull request #102 from xshxdex98/coop-bsp-wait 2026-10-06 08:40:24 +13:00
Stefan MaierandClaude Opus 5.5 24e4123882 VR performance diagnostics: the frame's parts, the layer copies, Mesa's GL thread switch
- vr.log_performance: frames with game ticks and without, how many late,
  and their time by part (input, ticks, camera, draw, present)
- with XR_META_performance_metrics: per frame, each layer copy's time, the
  waits for swapchain images and xrEndFrame
- debug.gl_thread: Mesa's GL thread on or off (mesa_glthread) for trying

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:33:00 +02:00
MrBruh 197c199473 Improve performance by ~80% with many enemies
The Silent Cartographer with 32x co-op enemies (484 actors) went from 53
to about 97 fps on the Linux PC, with the same game and the same picture:
cluster list removals start where the reference was put, full data arrays
are not walked again, object lighting keeps each cluster's drawn lights
and passes over far ones without their distance, the per-texture profile
timer only runs while something reads it, and the renderer binds streams
once per draw, caches its texture, target and shader lookups, and no
longer makes Mesa's GL thread wait on large uploads or visibility tests.
2026-10-06 08:13:44 +13:00
xshxdex98 0acc3b1381 Network damage: one place fills a damage event
damage_event_new fills the fields every damage event has, for scenery's
and units' alike.
2026-10-05 13:11:01 -05:00
xshxdex98 1e74e9b03e Co-op scripts: follower actions named, the skip's revert plainer
What the followers of player0 do (teleport, suspend, get out, board) is a
function each in coop_scripts.c, so every engine hook is one call. The
cutscene skip reverts only in a local game or on a co-op host, written as
two plain branches; a non-co-op network host no longer reverts alone.
2026-10-05 13:10:29 -05:00
xshxdex98 5d108579ed Co-op extra enemies: one function judges a spot
coop_enemies_spot_free makes every check of a spot (way, floor, headroom,
nobody there) in one place, each constant has its own comment, and the
file comment's wrapping is fixed.
2026-10-05 13:09:58 -05:00
xshxdex98 7a1ffca28b Network: glass and destructible scenery break the same for everyone
Breakable surfaces (glass) are broken by each machine's own projectiles
with its own random damage, so a pane could shatter on the host and stay
whole, still blocking, on a client. In co-op the host now sends each break
(and a few again every refresh, for late joiners), and a client breaks the
same surface with its shards.

Destructible scenery and devices (lights, panels) are the map's, at the
same index on every machine, but only damage to units was synced: a
client's shots never damaged them, and what the host broke didn't break
or play its effects on the clients. A client now reports its hits on them
like any other, the host checks and deals them, and every client deals the
host's damage to its own copy, which breaks, plays its effects and is
destroyed as the host's was.

Both are new message kinds that a client without them ignores, so the
network version is unchanged.
2026-10-05 13:00:01 -05:00
xshxdex98 b843156fe8 Network: a client never reverts its game on its own
The pause menu's revert (main_revert_map) reverted a network client's own
game state, leaving it out of step with the host. A client now ignores it,
and the cutscene skip's local revert, which a client only reaches outside
the co-op vote, is the host's or a local game's alone.
2026-10-05 12:59:00 -05:00
xshxdex98 99a31ca82d Co-op: a cutscene waiting until it's safe waits for one safe player
The scripts check game_safe_to_save before some cutscenes (the Maw's
bridge). The game's test fails while any enemy sees any player, so with
players spread out a fight elsewhere held up the cutscene a player had
reached. For the scripts in co-op, one player alive, on the ground and
unseen is enough; checkpoints keep the game's own test.
2026-10-05 12:58:59 -05:00
xshxdex98 9666575b39 Co-op extra enemies: same floor, clear of crates, room to stand
A spot's ground was looked for up to 2.6 m below it, so an extra enemy
could land under a ledge or catwalk, and only walls were in the way, so
it could stand inside a crate or a machine.

A spot now needs a floor within 0.5 m of the starting location's, nothing
in the way from beside the starting location (level, scenery, machines,
vehicles), and 1.6 m of room above its floor.
2026-10-05 12:58:21 -05:00
Stefan MaierandClaude Opus 5.5 8466846d45 Steam Frame: the frames' thread kept on the fast cores
Without XR_EXT_performance_settings (SteamVR in Lepton) the scheduler may
put the game's busy thread on the 2.3 GHz cores; frames then come late to
the compositor and the picture doubles as the head turns, now and then,
at a steady 72 fps. On SteamVR the thread that ends the frames is kept on
the cores no slower than 90% of the fastest (HALO_XR_ANY_CORE: not); the
metrics line says which core it is on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:53:47 +02:00
Stefan MaierandClaude Opus 5.5 bc580cf1cf VR: the compositor's performance counters in the log (XR_META_performance_metrics)
Every five seconds, where the runtime offers them (SteamVR on the Steam
Frame): to tell frames the compositor showed late or reprojected from the
game's own pace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:46:54 +02:00
xshxdex98 8e6b9a211e Co-op: triggers, cutscenes and failed missions work for every player
The campaign scripts only run on the host, against every player's unit,
but some of their habits assumed split-screen:

- volume_test_objects_all on the players waited for every player in the
  volume at once: the Maw's bridge cutscene, a30's Pelican pickups. In
  co-op one player arriving is enough now.
- Scripts name players as player0 and player1, so everyone past the
  second was left out of what they did: Pelican loads (a30's pickups,
  insertions), getting out, cutscene teleports and freezes, starting
  profiles. Those players now follow player0 (coop_scripts.c).
- A cutscene filmed far from every player (b30's sword Elite at the
  shaft door) wasn't active on clients: the cluster the scripts force
  active (object_pvs_activate) now rides in the presentation's spare
  bytes, and what the host's camera films is sent to clients as often as
  what is next to their players.
- A mission the scripts fail (d40's timer, a50's Keyes) only respawned the
  players, leaving them in the failure cutscene for good. It now reverts
  like the cutscene skip does; everyone dying still respawns at the last
  checkpoint. The scripts' game_revert reverts the same way.

The network version is unchanged: the presentation keeps its size, and a
host without the active cluster sends zeroes, which read as none.
2026-10-05 12:43:48 -05:00
xshxdex98 d0016bca89 Co-op extra enemies: spread onto free ground, never stacked
An extra enemy was placed on a ring around its starting location, trying
one spot per ring, and checked only against walls. When the 8 tries failed
(tight corridors, the Maw's interiors) it went on the starting location
itself, on top of the squad's own actor, and nothing stopped it landing on
another starting location's actor a metre away.

Now every spot of each ring is tried (a ring holds more spots the further
out it is), and a spot needs ground under it, no wall in the way and no
biped within 0.6 m or vehicle around it. A starting location with no room
passes its enemy to the squad's next one, and with no room anywhere the
enemy isn't placed. Dropship riders released beside the ones who jumped
out are spread the same way.
2026-10-05 12:41:08 -05:00
Stefan MaierandClaude Opus 5.5 3d575f32ed Bots on foot: ramps, rails, door frames and things out of reach
- a way is clear only at the knees and the chest (a rail with room under
  it, a slit in a wall), and for a shortcut along a path at the shoulders
  too (a door frame)
- stuck below a near point (a step, a ramp's lip): a jump on towards it
- a thing to pick up it comes no nearer to for 2.5 s is given up
- stuck three times in five seconds: the goal given up, somewhere away
- at the point it walks to: arrived, not stuck

Emulator, Blood Gulch, 15 bots: CTF stuck events 264 -> 153, mid-field
26% -> 32%, kills 26 -> 33; team slayer stuck events 860 -> 560.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:33:16 +02:00
Stefan MaierandClaude Opus 5.5 727b9ca80d Bots on foot: no crowding at things out of reach, spread over the enemies, hold spots they can reach
- a thing to pick up that it stands by and is not offered (on a ledge,
  out of reach) is given up for 20 s; up to four remembered, in the
  fight too (the crowds jumping at the base's weapons)
- stuck: the side step goes to the open side
- goals: each bot one of the three nearest enemies, and beside them while
  far, so a team spreads out
- holding a spot: stops near it, and one it cannot reach falls back to
  the place's centre

Emulator, Blood Gulch, 15 bots: CTF stuck events 1793 -> 264; team slayer
stuck windows 32% -> 9-15%, time by the bases 85% -> 68-73%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 18:52:30 +02:00
Stefan MaierandClaude Opus 5.5 99d7c924db Bots: a person joining a lobby the bots fill takes a bot's place
The bots fill the game up to its maximum; a machine joining the lobby
keeps its place (no bot added for it) and its player takes the last
bot's; the game lists as open while a bot could leave.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 18:00:31 +02:00
Stefan MaierandClaude Opus 5.5 57d96e4681 VR PC menus: fixes from the first Frame test
- 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>
2026-10-05 17:45:41 +02:00
Stefan MaierandClaude Opus 5.5 177ad4d40d VR: the PC version's menus (vr.menus, on by default)
- 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>
2026-10-05 17:17:16 +02:00
Stefan MaierandClaude Opus 5.5 bfb668d498 Merge branch 'merge/upstream-2026-10-05'
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:00:26 +02:00
Stefan MaierandClaude Opus 5.5 fc2b6551ca Merge upstream (OpenCE 13c14df9): online co-op campaign, online split screen, network version 16
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>
2026-10-05 16:22:05 +02:00
Stefan Maier 5316a6b145 Merge branch 'frame/launcher' 2026-10-05 16:19:02 +02:00
Stefan MaierandClaude Opus 5.5 59b1fb422b Steam Frame: a library button that starts, updates and quits the game
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>
2026-10-05 16:17:35 +02:00
xshxdex98 dc383d10da Co-op: clients draw AI units with their variant's shader permutation
An actor variant sets its unit's forced shader permutation after the unit
is made, and only the host runs the AI, so on clients Elite majors and
commanders drew the minor's armor (bitmap 0 of the cube maps instead of 1
and 2). The object creation message carries it in its spare byte, which
builds without this send as 0; a client applies only one that isn't 0, so
the message keeps its size and games hosted on builds without this keep
each tag's own.
2026-10-05 09:04:54 -05:00
xshxdex98 a143d9490d Co-op: tell a player held back at a loading zone what it waits for
Going back to a BSP the team has been in waits for two thirds of the team,
and a player on the trigger was never told so. They now see "Waiting for
your team to go back: 1 of 2 here", at once and every three seconds while
they wait. Each machine works it out for its own players with the count the
host decides by, now one function, so nothing new goes over the network.
2026-10-05 08:51:49 -05:00
xshxdex98 e55c29a97e Co-op: going back counts the team within 45 metres, not 12
Two thirds of the players had to be in the loading zone or within 4 world
units of whoever was in it. A team walking back through a doorway in a line
is longer than that, so going back almost never switched: the first player
walked on past the loaded level, fell and was brought back to the team.
15 world units fits a team moving together and still not a player who ran
back alone.
2026-10-05 08:51:49 -05:00
xshxdex98 16361bdef1 Co-op: a client's player waits on its floor for the host's BSP
The host's switch and its players' new positions travel separately, so a
client could hear where the host moved its player, in the new BSP, before
the switch itself, and drop through the floor until the BSP loaded. A
client now keeps its player on the floor of the BSP it has until the
host's position is somewhere it has loaded. Nothing changes on the wire.
2026-10-05 08:29:25 -05:00
Stefan MaierandClaude Opus 5.5 5ecd3075a0 Steam Frame: a launcher that starts the game as Steam starts Android titles
Lepton runs the APK in a container of its own, kept between starts, and
installs it anew whenever the APK is newer; the script starts it a second
time after a new APK's first start, which only installs it. With the APK in
~/Documents/HaloCE, updating the game is replacing that file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:17:54 +02:00
MrBruh 13c14df95c Merge pull request #101 from xshxdex98/coop-bsp-sync
Co-op: every machine stays on the host's BSP (network version 16)
2026-10-06 02:13:10 +13:00
xshxdex98 b525bf8514 Network version 16
A client's input says which structure BSP it has loaded (co-op).
2026-10-05 08:09:47 -05:00
xshxdex98 087ca8a875 Co-op: every machine stays on the host's BSP
Players in big sessions ended up on different BSPs:

- The host told clients about a switch only every half second, and not
  reliably, so a client could predict its player for a while in a BSP it
  hadn't loaded. A switch is now sent at once, reliably, ahead of the
  players' new positions.
- A client still loading reports so: its input carries the BSP it has
  loaded. Until it has the host's, the host takes none of its movement
  (which falls there with no floor) and none of its loading zones.
- After a switch, by a trigger, a script or a skipped cutscene, no trigger
  can switch again until every machine has loaded the new BSP (at most
  ten seconds), so no client falls two switches behind.
- A player the host can't place beside whoever crossed, when a big team
  runs out of room there, is placed beside another teammate instead of
  being left where they were.
2026-10-05 08:09:46 -05:00
MrBruh 24957ff6a0 Network version 15 2026-10-06 01:28:04 +13:00
MrBruh 402d4b8351 Merge pull request #97 from xshxdex98/coop-dead-vehicles 2026-10-06 01:24:23 +13:00
MrBruh d1825ab08d Merge pull request #96 from xshxdex98/coop-backtrack-gate 2026-10-06 01:24:22 +13:00
MrBruh 268ec84b74 Merge pull request #95 from xshxdex98/coop-allegiances 2026-10-06 01:24:21 +13:00
xshxdex98 8dc796028e Co-op: going back to a visited BSP needs two thirds of the team
One player who stayed behind or went AFK no longer blocks the team.
2026-10-05 07:14:28 -05:00
xshxdex98 8aa381ed14 Co-op: vehicles the host destroys explode on clients
Clients only had a kill path for bipeds, so a Ghost, Banshee or Wraith the
host destroyed stayed intact on clients. A vehicle the host's state says
is dead is now destroyed there too, with its explosion.
2026-10-05 07:01:37 -05:00
xshxdex98 c3f82fa9b2 Co-op: going back to a BSP already visited needs the whole team there
A loading zone brings the whole team to whoever crosses it. Into a new part
of the level that's what you want, but it let one player drag everyone
back. A switch back to a BSP the team has been in now waits until every
living player is in the trigger or near the player in it.
2026-10-05 07:01:32 -05:00
xshxdex98 5ff4b6ee8d Co-op: clients take the host's allegiances
The scripts make allegiances (ai_allegiance player human) and only the host
runs them, so on clients marines stayed enemies of the players: red reticles
and the wrong team. The host sends its ally and friend tables with the
presentation each tick.
2026-10-05 07:01:26 -05:00
MrBruh 91e838e2f6 Network version 14 2026-10-06 00:54:11 +13:00
MrBruh 520b8401db Merge pull request #93 from xshxdex98/test-death-timing-coop 2026-10-06 00:51:37 +13:00
MrBruh 2ed4a7b49d Merge pull request #92 from xshxdex98/coop-flood-dropships 2026-10-06 00:51:36 +13:00
MrBruh 503992d544 Merge pull request #91 from xshxdex98/coop-device-states 2026-10-06 00:51:35 +13:00
MrBruh ccc7df6611 Merge pull request #90 from xshxdex98/coop-loading-zones 2026-10-06 00:51:35 +13:00
xshxdex98 ddcbe79283 test_death_timing.py: stubs for the network co-op paths
The test compiles main.c's and players.c's death handlers against a fake
world; their network co-op branches call functions it had none of, so it
no longer compiled. Stubbed off, it covers the local game it was written
for, and passes.
2026-10-05 06:41:27 -05:00
xshxdex98 862d88737e Co-op: Flood bodies get up on clients; dropship doors open and close there
- A body the host's unit got back up from (a Flood combat form feigning
  death) gets up on the clients too, instead of its ragdoll being dragged
  about. The resurrection is one function both use (unit_port_resurrect).
- The scripts' unit_open and unit_close go to the clients, so dropships'
  doors aren't left open there.
2026-10-05 06:36:27 -05:00
xshxdex98 8610b28376 Co-op: doors and elevators are where the host has them
The host sends each device's position and power when they change (and with
the resent state), and a client puts its device there when the host's is at
rest and they differ, or when they drift apart while it moves. Clients only
had the device groups' values, which aim a device: a door could stop half
open, and an elevator be somewhere else on a client, so its players fell.
2026-10-05 06:36:27 -05:00
xshxdex98 9e6247e0cb Co-op: a loading zone brings the team along; only someone falling is rescued
- Crossing a BSP loading zone moves every other player to whoever crossed,
  as split screen does. Moving only some players left the rest where the
  next switch threw them back to a hallway, or under a floor.
- The rescue for a player outside the loaded BSP waits two seconds and
  only takes someone falling: a player riding an elevator through its
  shaft is left alone.
2026-10-05 06:34:00 -05:00
MrBruh dea5fe3b1e Fix crash past 256 actors 2026-10-06 00:29:42 +13:00
MrBruh f0804aa587 Extra enemies: per player or static multiplier 2026-10-06 00:08:26 +13:00
MrBruh 16ad4cc900 25% extra enemies option 2026-10-05 23:45:31 +13:00
MrBruh a38ede078a Co-op extra enemies per player 2026-10-05 23:25:15 +13:00
MrBruh fa85aa53f5 Kick command; co-op friendly fire on by default 2026-10-05 22:51:05 +13:00
MrBruh 8be9a038a1 Co-op friendly fire setting 2026-10-05 22:40:34 +13:00
MrBruh 783b591a6a Co-op defaults to 16 players and private 2026-10-05 22:27:36 +13:00
MrBruh e82d880060 Singleplayer and multiplayer map choice 2026-10-05 22:17:22 +13:00
MrBruh 7d0e587c51 Co-op review fixes 2026-10-05 21:02:04 +13:00
MrBruh 01586cfebf Merge pull request #89 from xshxdex98/coop-campaign 2026-10-05 20:29:19 +13:00
MrBruh de52060644 Host takes grenade damage only from grenades thrown 2026-10-05 19:54:33 +13:00
MrBruh ab7c507a29 Leaving the lobby after a game goes to the main menu 2026-10-05 19:39:57 +13:00
xshxdex98 ba6fe0a826 AI sync: any team the host's unit has goes with it, not only the campaign's 2026-10-05 00:59:33 -05:00
MrBruh 3f6c0c37a4 Friendly fire stops teammates' instant kills 2026-10-05 18:47:39 +13:00
xshxdex98 606109551d Co-op: friendly reticles, skipped cutscenes' sound, the end of a level, tutorial prompts
- AI units' teams go with their states, so friendlies aren't red on
  clients' reticles.
- A skipped cutscene's music and dialogue stop on the clients.
- Winning a level returns every machine to the lobby with the next level
  set up, instead of leaving them on its last screen.
- Help text carries whether the host shows it, so no prompt is dropped.
2026-10-05 00:46:51 -05:00
xshxdex98 8194c5a00b Online co-op: play the campaign together over LAN and the internet
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.
2026-10-05 00:31:30 -05:00
MrBruh 737551fd51 Resolution, scaling and window size settings 2026-10-05 18:28:58 +13:00
xshxdex98 7a56c624f5 Merge upstream main 2026-10-05 00:07:13 -05:00
MrBruh 3ae09c3d2c Fade the radar wedge out fully 2026-10-05 17:23:47 +13:00
MrBruh 3a22816c2f Merge pull request #81 from FDervish/fix/antenna-stepped-per-tick 2026-10-05 17:15:38 +13:00
MrBruh 2853a42253 Clear an antenna's shape record when it is created 2026-10-05 17:13:27 +13:00
Fadi DerouicheandClaude Opus 5 39ece781ee Step the antenna chain in the tick and draw the frames between the ticks
An antenna's chain is a simulation of the Xbox's own step, an update a tick,
and the port was stepping it once a frame with the frame's own time. Its
springs, its carry and the points' physics then acted against the frames'
movement of it, so how far the chain moved per update depended on the frame
rate, and a vehicle at speed swung it far wider than a tick's step does.

- game.c steps the chain in the tick, beside the effects, which are already
  stepped there.
- antenna.c records the shape each tick leaves, relative to the point the
  vehicle's marker carries the base by. A frame drawn between two ticks is the
  part of the way between those two shapes that it is, carried to where the
  marker is now, so the chain stays on the vehicle it hangs from instead of
  lagging a tick behind it. The gate that left a chain that had not been drawn
  for five updates unstepped counted frames; it counts ticks now, which is
  what it counted on the Xbox.
- widgets.c no longer steps the chain from the widget update, which ran once a
  frame.
- game_time.c draws the per-frame effects and widgets that hang off objects
  (game_frame) with the pose between the last two ticks, the pose the renderer
  draws with, so what they place on an object is drawn where they put it.
- objects.c hands markers the matrices the renderer draws with, so an effect,
  a particle system or a contrail attached to a moving object follows it
  between the ticks too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 17:13:27 +13:00
MrBruh cf17201ec8 Split screen divider spans wide screens 2026-10-05 17:13:27 +13:00
MrBruh 45de17116b Merge pull request #82 from addisonbair/gamescope-fullscreen
Count a fullscreen window that gamescope never confirms as fullscreen
2026-10-05 17:01:51 +13:00
xshxdex98 7d56f34b83 Map screen: choose co-op campaign or multiplayer, then vanilla or custom maps
Hosting over LAN or the internet, the Map screen's list first offers CO-OP
CAMPAIGN or MULTIPLAYER. CO-OP CAMPAIGN offers CAMPAIGN or CUSTOM CAMPAIGN,
then a level and its difficulty. MULTIPLAYER offers VANILLA MAPS or CUSTOM
MAPS, then the maps, and goes on to the gametypes as before. B steps back
through the choices and leaves the screen from the first. Split screen
starts at VANILLA MAPS / CUSTOM MAPS.

The custom lists stay empty and a co-op level cannot be hosted yet: network
co-op and Custom Edition maps come in their own pull requests, which fill
them in.
2026-10-04 22:54:24 -05:00
MrBruh 3c7ba95ff5 Merge pull request #70 from startupfoundry/windows-no-console
Release builds of Windows without a console window
2026-10-05 16:51:22 +13:00
MrBruh 092505224a Merge pull request #25 from garfbargle/macos-android-build
Build the Android port on macOS hosts
2026-10-05 16:48:39 +13:00
MrBruh 079883d640 Merge pull request #59 from saulob/fix/startup-color-flash
Fix incorrect Colors on the initial menu frame
2026-10-05 16:39:03 +13:00
MrBruh 54faa1dab6 Merge pull request #87 from eltanschauung/fix/death-camera-timing-pr
Fix death camera and co-op respawn timing at high frame rates
2026-10-05 16:33:14 +13:00
MrBruh 273152cc25 Merge pull request #80 from FDervish/fix/vertex-explosion
Fix vertices exploding to the screen centre near the camera plane
2026-10-05 16:07:31 +13:00
MrBruh 9bdd7f30c0 Merge pull request #86 from thelinkin3000/fix/host-again-after-backing-out
Make a new game each time Create is chosen
2026-10-05 15:53:59 +13:00
MrBruh 685bf260a0 Either co-op player picks the level 2026-10-05 14:47:11 +13:00
MrBruh 933ed2b7b8 Point updates at OpenCommunityEdition/OpenCE 2026-10-05 14:21:03 +13:00
MrBruh c115f5292d Split screen joins of games under way 2026-10-05 14:06:51 +13:00
eltanschauung 0f22c1d592 Add standalone death and co-op regression tests 2026-10-04 20:33:17 -04:00
eltanschauung fb1abedd8b Keep dead camera target when no living player exists 2026-10-04 20:32:59 -04:00
eltanschauung cd345aac7c Count death and co-op respawn delays in game ticks 2026-10-04 20:32:31 -04:00
MrBruh 7e00135d31 Fix menu crash with no network client 2026-10-05 12:59:15 +13:00
thelinkin3000andClaude Opus 5.5 95416b2cfc Make a new game each time Create is chosen
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>
2026-10-04 23:37:36 +00:00
MrBruh ffc1512dcb Read brokers.txt without SDL on Android 2026-10-05 11:27:05 +13:00
MrBruh 88a7c07ec7 Brokers in brokers.txt 2026-10-05 11:15:11 +13:00
Stefan Maier 8e8713681d Merge remote-tracking branch 'origin/fix/multiview-desktop-compile' 2026-10-04 22:59:55 +02:00
Stefan Maier d7099f3e40 Fix desktop link: bot machine check and glClearDepthf were Android-only
network_game_server_is_bot_machine (cea7abfc, Bots as players of the
host) was written into the HALO_ANDROID block of network_server_manager.c
while bots.c calls it on every platform, and so was the array it reads;
both move out of the guard (on the desktop nothing marks a bot machine,
so the check is always false there, the writes stay Android-only).

glClearDepthf (dc21f4b4, Play in VR on OpenXR headsets) is ES-only:
GL_FUNCTIONS declares it for Android's loader and the define at the top
maps glClearDepth to it, but the desktop context links against GL's
glClearDepth. The clear now calls glClearDepth, which the Android define
turns into glClearDepthf as before.
2026-10-04 22:37:17 +02:00
Stefan Maier 4ba36d7305 VR: fix desktop build, targets_multiview was Android-only
6c3f4ce2 (VR: draw both eyes in one pass) changed prepare_draw to pass
targets_multiview to vertex_shader_get, but the variable is declared and
kept inside the HALO_ANDROID guard (L887): Linux and Windows failed to
compile with 'use of undeclared identifier targets_multiview', Android
was fine. On the desktop there are no multiview layers to stand in for
the back buffer, so the desktop call passes FALSE.
2026-10-04 22:25:21 +02:00
MrBruh e3389991ac Online split screen 2026-10-05 09:18:40 +13:00
Stefan Maier 855f083117 Merge branch 'frame/performance' 2026-10-04 22:04:33 +02:00
Stefan Maier 6f3786de91 Merge branch 'fix/vr-shot-origin' 2026-10-04 22:04:33 +02:00
Stefan MaierandClaude Opus 5.5 493713b0a6 Steam Frame: the refresh rates offered in the log; its rate and speed noted
The runtime offers only the rate SteamOS is set to. At 72 Hz the game holds
it with multiview (about 11.6 ms a frame); at 90 Hz it cannot, and the
frames SteamVR makes up showed as a doubled picture when the head turned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:47:56 +02:00
Stefan Maier bf356fc58a Merge branch 'fix/vr-shot-origin' into frame/controllers 2026-10-04 21:42:21 +02:00
Stefan MaierandClaude Opus 5.5 6a7b919522 VR: shots fly along the aim, not above it
A shot fired from the muzzle of the weapon in the hand was kept inside the
level by the game's test of the line from the eyes to it, which in VR runs
down through the player's own body: the test hit it and moved the shot up
towards the eyes, from where it flew parallel to the aim, a few centimetres
above the crosshair and the laser (measured: up to 7 cm at 3 m). In VR the
line is tested against the level alone (a hand through a wall still does
not fire through it); measured after: the impacts on their rays, on
average within a millimetre.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:42:07 +02:00
Addison Bair 20861c06d2 Count a fullscreen window that gamescope never confirms as fullscreen
gamescope (the Steam Deck's Game Mode) makes the window the size of the
display but never confirms the fullscreen request, so SDL leaves
SDL_WINDOW_FULLSCREEN unset. platform_screen_mode then reported a window,
the game drew the Xbox's 640x480, and gamescope stretched it to the
screen, soft and 4:3. A window that was asked to be fullscreen and covers
its display now counts as fullscreen too, so the game draws at the
display's resolution and shape (1280x800 on the Deck).
2026-10-04 19:30:07 +00:00
Fadi DerouicheandClaude Opus 5 fc7271fd52 Step the antenna chain in the tick and draw the frames between the ticks
An antenna's chain is a simulation of the Xbox's own step, an update a tick,
and the port was stepping it once a frame with the frame's own time. Its
springs, its carry and the points' physics then acted against the frames'
movement of it, so how far the chain moved per update depended on the frame
rate, and a vehicle at speed swung it far wider than a tick's step does.

- game.c steps the chain in the tick, beside the effects, which are already
  stepped there.
- antenna.c records the shape each tick leaves, relative to the point the
  vehicle's marker carries the base by. A frame drawn between two ticks is the
  part of the way between those two shapes that it is, carried to where the
  marker is now, so the chain stays on the vehicle it hangs from instead of
  lagging a tick behind it. The gate that left a chain that had not been drawn
  for five updates unstepped counted frames; it counts ticks now, which is
  what it counted on the Xbox.
- widgets.c no longer steps the chain from the widget update, which ran once a
  frame.
- game_time.c draws the per-frame effects and widgets that hang off objects
  (game_frame) with the pose between the last two ticks, the pose the renderer
  draws with, so what they place on an object is drawn where they put it.
- objects.c hands markers the matrices the renderer draws with, so an effect,
  a particle system or a contrail attached to a moving object follows it
  between the ticks too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:45:28 +01:00
Stefan MaierandClaude Opus 5.5 c1305fa94f Steam Frame: its controllers as themselves, with the Xbox's layout
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>
2026-10-04 20:28:13 +02:00
Fadi DerouicheandClaude Opus 5 3d2c04d675 Fix vertices exploding to the screen centre near the camera plane
The screen-space conversion that the Xbox's vertex shaders do is undone by
multiplying the screen position by w again, which is lossy near the camera
plane: the conversion takes the reciprocal of w (rcc), which clamps there, and
1/w rounds differently on each GPU.

Worse, where w is zero or not a number the reconstruction leaves the vertex at
the clip-space origin. That point is inside the frustum, so nothing clips the
triangle away, and OpenGL's zero divided by zero puts the vertex in the middle
of the screen: the triangle is then drawn from wherever its other vertices are
out to there. On the first-person weapon, whose pose follows the camera and so
reaches the camera plane, that is a streak across the whole view. The Xbox's
own divide sends such a vertex to infinity and its clipper discards the
triangle.

Compute the final position from the clip-space position that the shader keeps
whenever it takes the reciprocal of w, so that nothing is reconstructed by
dividing; and where w is zero or not a number, put the vertex behind the
camera (w = -1), which the clipper also discards, as the Xbox did.

Both paths were behind the Android build's #ifdef, so desktop GL only ever
used the lossy reconstruction. The y flip and depth range that follow remain
per platform.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:27:35 +01:00
Stefan MaierandClaude Opus 5.5 0fc85d6c67 Steam Frame: the overlays' alpha from their colour on SteamVR
SteamVR draws a quad's texels by their alpha alone, and the game's menus,
HUD and text have none in their additive parts: on the Frame nothing showed.
On SteamVR (the runtime's name says) the copy into the quads' images takes
their alpha from their colour where it is higher; HALO_XR_QUAD_ALPHA
(colour, keep) overrides, for trying. The Frame's notes updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:17:43 +02:00
Stefan MaierandClaude Opus 5.5 c0a08f395e Steam Frame: the game's data in the SteamOS user's Documents when it is there
Lepton empties the app's own folders (and uninstalls the app) whenever it
starts again; Documents is the SteamOS user's and kept. When
/home/steamos/Documents/HaloCE holds the maps and is writable, the launcher
and the game take it as the data folder (maps, config.toml, saves, logs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:51:08 +02:00
Stefan MaierandClaude Opus 5.5 06cb70fb58 Steam Frame: notes, and no variable the Android loader never reads
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:27:28 +02:00
Stefan MaierandClaude Opus 5.5 1709698053 Start on the Steam Frame (Lepton)
Lepton has no clipboard service, and SDL3's activity died taking one for
granted: the build now patches SDL's clipboard handler to do without.
The Frame lists an implicit OpenXR layer for Vulkan apps whose library an
app cannot load, which failed xrCreateInstance (and the Android loader
reads no variable to turn it off): the app now carries a pass-through
layer under that library's name. With both, the game runs on the Frame
and its OpenXR session starts (SteamVR, 1728x1728 an eye, 72 Hz).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:26:39 +02:00
Stefan Maier cb72dca40f Merge branch 'merge/upstream-2026-10-04' 2026-10-04 14:00:26 +02:00
Stefan MaierandClaude Opus 5.5 bbea2a2bff The online panel's game by its row past the fixed rows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:47:56 +02:00
Stefan MaierandClaude Opus 5.5 57cdbd8f80 Merge upstream (cybersecurity/halo-ce-universal 193cbf59): server browser, split-screen co-op
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>
2026-10-04 13:47:29 +02:00
Stefan Maier c5c5a8eaeb Merge branch 'feature/bots-vehicles' 2026-10-04 13:33:07 +02:00
Stefan Maier a3ee36b5ef Merge branch 'feature/vr-hud-fixes' 2026-10-04 13:33:07 +02:00
Stefan Maier 30a1e0f40e Merge branch 'feature/vr-menu-lists' 2026-10-04 13:33:07 +02:00
Stefan Maier 327cf96d6f Merge branch 'feature/vr-options' 2026-10-04 13:33:07 +02:00
Stefan MaierandClaude Opus 5.5 4af570bc08 VR: the online games' hint further left and shorter, clear of Y = Create Game
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:13:27 +02:00
Stefan MaierandClaude Opus 5.5 7a1cf79396 VR: teammates' triangles over their heads, a click joins on the profile screen
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>
2026-10-04 12:00:41 +02:00
Stefan MaierandClaude Opus 5.5 6754403d68 VR: the menu pointer leaves the stick's choice be; clicks beside a list step it
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>
2026-10-04 11:41:09 +02:00
MrBruh 193cbf59c7 Split-screen co-op 2026-10-04 22:27:13 +13:00
MrBruhandClaude Opus 5.5 d578f88bd0 Validate server browser names as player names
The server browser cleans each game's name as a host cleans the names
of the players and machines that join it, and leaves out a game whose
name has nothing left that names it. A host lists a Latin letter with
a mark as its plain letter, as player names are read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 20:53:00 +13:00
MrBruhandClaude Opus 5.5 d3fc997fdc Ready walked-over weapons, swap out the selected one
In multiplayer a second weapon is picked up into the empty slot on
walking over it and readied, as in campaign; a client readies the
weapon the host says it picked up once it has it. A swap drops the
weapon the player selected, not the one still in hand mid-switch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 20:47:06 +13:00
MrBruh a227872ccb Merge pull request #73 from thelinkin3000/fix/unarmed-melee-campaign
Leave an unarmed melee alone where the map has no multiplayer weapons
2026-10-04 20:28:01 +13:00
MrBruh 812ffeea33 Outline the focused server browser row 2026-10-04 20:19:12 +13:00
MrBruh c05864b3fc Leave quit players off the scoreboard 2026-10-04 20:06:28 +13:00
thelinkin3000andClaude Opus 5.5 ce77db8404 Leave an unarmed melee alone where the map has no multiplayer weapons
A player with no weapon, whose biped has no blow of its own, borrows the
melee of the globals' first multiplayer weapon. A campaign map's globals
list no multiplayer weapons, and a10 starts the player unarmed, so a melee
in the cryo tube's tutorial asked for element 0 of an empty block and
halted in tag_groups.c. With no multiplayer weapon there is no blow to
borrow, and the melee does what the game's always did there: nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 06:22:42 +00:00
MrBruh 39350fb81f Enemy names to the motion sensor's range and setting 2026-10-04 18:57:11 +13:00
MrBruh 1a15a171ac No unarmed grenades or melee from vehicle seats 2026-10-04 17:54:34 +13:00
MrBruh 2e3841ff7d Left-align the shotgun's ammo meter 2026-10-04 17:47:55 +13:00
MrBruh 46edf53657 Settings and End Game in the pause menu 2026-10-04 17:20:32 +13:00
Paul Hontz cd747c23c5 Log of the port to halo.log without a console
The release build no longer has a console, and the platform layer's log
(platform_log to stderr: invite links, the data root, crash reports)
went only there. When stderr goes nowhere, halo.log next to halo.exe
gets it instead.
2026-10-03 22:43:14 -04:00
Paul Hontz f148dbd6d9 Release builds of Windows without a console window
The release halo.exe was a console-subsystem PE, so Windows and Wine
open an empty console window next to the game. Under Proton in
gamescope the console can take focus and leave a black screen. Nothing
reads the console (the log is debug.txt), so release builds now link as
GUI with mainCRTStartup; debug builds keep the console.
2026-10-03 22:26:20 -04:00
MrBruh c3adcfe5bf Deflate the release zips again 2026-10-04 14:22:11 +13:00
MrBruh 3304965653 Lobby check test past the compiler launcher 2026-10-04 13:43:13 +13:00
MrBruh c04765d7f4 Server browser of public internet games 2026-10-04 13:34:23 +13:00
MrBruh 933aac6175 No map weapons option, grenades with no weapon 2026-10-04 10:44:37 +13:00
MrBruh 809408c663 Survive kills and joins in departed players' slots 2026-10-04 10:12:32 +13:00
MrBruh 601a4c1f50 Unload the menu map before a client's next game 2026-10-04 09:51:47 +13:00
MrBruh 133d6a5dc1 Free quit players' slots 2026-10-04 09:07:47 +13:00
saulo benigno 61623e68d5 Fix texture bindings after first upload 2026-10-03 11:41:54 -03:00
MrBruh 23b542601f Unarmed melee and infinite grenades for any player count 2026-10-04 03:22:16 +13:00
MrBruh 62b630a261 Remove the flamethrower and gravity rifle 2026-10-04 03:05:59 +13:00
Stefan MaierandClaude Opus 5.5 0e6327cadf The controls page lists switching grenades (B held) on a line of its own
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:41:56 +02:00
Stefan MaierandClaude Opus 5.5 5f82df71f0 VR options: Y on the main menu or the pause menu
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>
2026-10-03 15:37:10 +02:00
MrBruh e8e0c2215a Find the port's headers in Windows game units 2026-10-04 02:08:45 +13:00
MrBruh 383355381c Kills to win up to 500 in the PC menus 2026-10-04 02:04:14 +13:00
Milenko 2b0103a0ef Slayer's kills to win up to 500, for big games
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.
2026-10-04 01:58:26 +13:00
Jeff ClarkandClaude Opus 5.5 e44619818b Snap poses that changed too much in a tick
Frame interpolation blends each node between its last two tick snapshots.
Blending two snapshots of different poses node by node stretches vertices
across the screen for a tick, then they snap back:
- the first-person pose blended from whatever was drawn last, which after
  being zoomed, in a vehicle, dead or in a cinematic is seconds old, and
  after a weapon switch may be another skeleton with as many nodes;
- an object's pose was only checked by its root node's move (10 units), so
  the game changing a pose at once (an actor waking from dormancy, a model
  swapped) was swept through poses it never had.
Now the first-person pose blends only from the tick just before, and not
when a node moved more than 0.25 units in the camera's frame; and an object
snaps when a node moved more than 0.4 units in its root node's frame (in
that frame, so the whole object turning or moving counts for nothing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:11:46 -04:00
Jeff ClarkandClaude Opus 5.5 b449c43e22 Keep vertex constant serials from wrapping
Each changed vertex constant register takes the next serial, and a program
uploads the registers changed since the serial it last saw. The serials
were 32-bit; a skinned model's draw changes up to 132 registers, so at a
high frame rate they wrapped within minutes to an hour. After a wrap, the
log walk (serial <= constants_serial) and the full scan (register serial >
program's serial) both found nothing changed, and every program's next
draw used the registers it last uploaded: often another object's node
matrices, so its vertices flew across the screen for a frame. The serials
are 64-bit now and never wrap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:11:35 -04:00
Jeff ClarkandClaude Opus 5.5 991ab41016 Fix the distance of 3D sounds
The game gives positions and each sound's minimum and maximum distances in
world units, and sets DirectSound's distance factor to 3.048, the meters in
a unit (10 feet). The mixer multiplied the distance by that factor before
the rolloff, but not the minimum distance, so every 3D sound was heard 3
times as far away as it was: up to 10 dB quieter against the 2D sounds,
and panned as if farther. The distance factor is DirectSound's for Doppler;
the rolloff is in the units the distances are given in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:04:57 -04:00
Jeff ClarkandClaude Opus 5.5 5f9748d736 Resample voices with a windowed sinc
The mixer resampled each voice to 48 kHz by linear interpolation, which
left the images of a 22 kHz voice's band only 3 to 21 dB down (an 8 kHz
tone's image at 14 kHz was 10 dB under it) and dulled the band itself
(-4 dB at 8 kHz): voices sounded gritty and low-fi. It also held a packet's
last frame instead of reading on into the next packet. Now each output
sample is band-limited interpolation of the frames around it (a Kaiser-
windowed sinc over 16 zero crossings, J. O. Smith's method): flat to 8 kHz
at 22 kHz, the images 66 to 98 dB down, the low pass narrowed for voices
played faster than the output rate, and the frames taken from the packets
in turn, so it reads across their boundaries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:50:39 -04:00
Jeff ClarkandClaude Opus 5.5 56b2a0e8e3 Fix Xbox ADPCM decoding
An Xbox ADPCM block's header holds its first sample, and its nibbles code
the 63 after it; the 64th nibble only pads the block. The decoder dropped
the header's sample and decoded the padding in its place, so every 64th
sample was wrong: a buzz at 344 Hz under every 22 kHz sound, voices most
of all. In the maps' sounds the last nibble is always 0, and the next
block's header follows the 63rd nibble's sample by one ordinary step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:47:20 -04:00
Jeff ClarkandClaude Opus 5.5 5f388f7818 Replace the soft clipper with a limiter
The voices sum at their full level, as the Xbox's mix bins did, and the
mixer bent every sample over 0.8 with tanh. When loud voices piled up, as
dialogue over gunfire does, that distorted the sound: a 2x full scale sine
came out with 23% harmonic distortion. A look-ahead limiter now turns the
whole mix down for as long as it would go over -1 dBFS, both channels
alike, and brings it back over 100 ms. The output is 1.3 ms later, so the
gain comes down before each peak; below full scale nothing changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:29:45 -04:00
codi 557c0b486c Build the Android port on macOS hosts
configure.py hardcoded the NDK's linux-x86_64 toolchain folder, so the
Android build could not find the NDK on a Mac. Pick the host's folder
(darwin-x86_64, windows-x86_64 or linux-x86_64), or the only one the NDK
has.

On a case-insensitive file system, the game's <StdDef.h> shim found itself
again through its own <stddef.h> inclusion and recursed. Use include_next,
as the port's other header shims do.
2026-09-29 20:51:24 -06:00
252 changed files with 32950 additions and 1891 deletions

No files matched your search

+7 -5
View File
@@ -67,7 +67,9 @@ jobs:
run: python tools/ci_build.py linux release
- name: Test
run: python -m pytest -q tools/test_linux_port.py
run: |
python -m pytest -q tools/test_linux_port.py
python tools/test_light_storage.py
- name: Compile cache
run: ccache --show-stats
@@ -200,6 +202,9 @@ jobs:
- name: Release
run: python tools/ci_build.py windows release
- name: Test light storage
run: python tools/test_light_storage.py
- name: Compile cache
run: ccache --show-stats
@@ -242,10 +247,7 @@ jobs:
mkdir release
for build in artifacts/*/; do
name="$(basename "$build")"
# stored, not deflated: the updater in builds up to 57 waits for
# an end of stream the game's zlib 1.1 does not always report,
# and fails on a deflated entry that unpacked correctly
(cd "$build" && zip -0 -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
(cd "$build" && zip -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
done
ls -l release
+4 -4
View File
@@ -19,9 +19,9 @@ builds of the latest release:
| Platform | Release | Debug |
| --- | --- | --- |
| Linux | [halo-linux-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-linux-debug.zip) |
| Windows | [halo-windows-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-windows-debug.zip) |
| Android | [halo-android-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-android-debug.zip) |
| Linux | [halo-linux-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-debug.zip) |
| Windows | [halo-windows-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-debug.zip) |
| Android | [halo-android-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-debug.zip) |
Use the release build to play. The debug build stops at the first failed
assertion and writes it to the log. Use the debug build to find and report
@@ -32,7 +32,7 @@ if you want to install it. Refer to "Updates" in
[port/linux/README.md](port/linux/README.md#updates).
Each build of the `main` branch that passes on all three platforms is a new
release. The [Releases](https://github.com/cybersecurity/halo-ce-universal/releases)
release. The [Releases](https://github.com/OpenCommunityEdition/OpenCE/releases)
page keeps the last five releases. If the latest build has a problem, get
an older build from that page.
+138
View File
@@ -0,0 +1,138 @@
# Steam Frame
The same APK as for the Quest runs on the Steam Frame, in Lepton (SteamOS's
Android, a Waydroid fork: Android 11, arm64), with SteamVR's OpenXR runtime.
What differs is found at run time, not by a build of its own.
## The device
- Steam Settings > System > Enable Developer Mode; then start "Lepton
Development" from the library. While the headset sleeps Lepton stops, and
adb with it.
- adb over the network: `adb connect <frame-ip>:5555`; over USB:
`adb forward tcp:5555 tcp:5555` then `adb connect localhost:5555`.
Always name the device (`adb -s <frame-ip>:5555 ...`): a Quest may be
connected as well.
- Install with `adb install -r --no-streaming`. It prints "Lepton post
installation failed due to APP_PATH being unset", but installs. Start the
game with `am start -n com.halo.decomp/.LauncherActivity`.
- Lepton empties `/sdcard/Android/data` whenever it starts again: the game's
data must live elsewhere (stage 2). `/sdcard/Documents` and
`/sdcard/Download` are the SteamOS user's folders and are kept.
## What the port does for it
- SDL3's clipboard handler takes a clipboard service for granted, which
Lepton has not: `tools/android_build.py` patches it to do without.
- SteamVR lists an implicit OpenXR layer for Vulkan apps whose library an
app cannot load: the APK carries a pass-through layer under its name
(`port/android/xr_layer/fdm_injection_stub.c`).
## Stages
| # | Stage | State |
| --- | --- | --- |
| 1 | Base: the fixes above on the current main | done |
| 2 | The game's data where Lepton keeps it | done: `/home/steamos/Documents/HaloCE` |
| 3 | Menus, HUD and text visible (quad layers) | done: their alpha from their colour |
| 4 | The freeze at the cryo bay's calibration | not seen again (2026-10-04) |
| 5 | The Frame's controllers | done: their own profile, the Xbox's layout |
| 6 | Frame rate, resolution | done: 72 Hz steady, multiview on |
| 7 | Multiplayer, voice, cross-play with the Quest | |
| 8 | A button in the Steam library | done: `steam_frame_launch.sh` |
## Starting it on the Frame
A button in the Steam library ("Halo VR", a non-Steam game) runs
`~/Documents/HaloCE/halo-vr.sh` (`port/android/steam_frame_launch.sh`),
which starts the game as Steam starts an Android title of its own:
`lepton waitforexitandrun -- ~/Documents/HaloCE/HaloVR.apk`, in a container
of its own (`steamlaunch-4290000001`) that Lepton installs the APK into
anew whenever the file is newer. Updating the game is replacing that APK.
What the script does that Steam would for an Android title:
- a compat data path (`steamapps/compatdata/4290000001/internal`): after an
install Lepton links the app's data to it, a dangling link without it;
and the shader path, which Lepton reads Steam's shader dump path against;
- a container id of its own: with the shortcut's id, SteamVR's client in the
container stalled in xrCreateInstance;
- a few starts: a new APK's first start only installs it, and a start that
ends within 30 s is tried again (up to three times), unless the game was
quit (it writes `quit_requested.txt`).
Steam's "Exit game" does not reach the game (its process is the container's,
not Steam's): quit it with the VR options' "Quit Halo" (Y in the main or
pause menu, twice). A container ended within 30 s of its start makes
Lepton drop the app's install, so the next start installs it again.
The shortcut is in `userdata/<id>/config/shortcuts.vdf` (Steam reads it when
it starts; appid 3151454348 for this name and path), its pictures in
`userdata/<id>/config/grid/` (from `port/android/steam_frame_grid/`). From
the Mac, over USB the Frame's SteamOS is an adb device of its own, `frame`,
beside Lepton's `<frame-ip>:5555`.
## The game's data
Lepton uninstalls the app and empties its folders whenever it starts again.
The game's data lives in the SteamOS user's Documents instead: when
`/home/steamos/Documents/HaloCE` holds `maps/ui.map` and is writable, the
launcher and the game take it as their data folder (config.toml, saves and
debug.txt go there too). Once:
adb -s <frame-ip>:5555 shell mkdir -p /home/steamos/Documents/HaloCE/maps
adb -s <frame-ip>:5555 push assets/maps/. /home/steamos/Documents/HaloCE/maps/
adb -s <frame-ip>:5555 shell chmod -R 777 /home/steamos/Documents/HaloCE
After a Lepton restart only the APK is installed again. (Use the path under
/home, not /sdcard/Documents: Android 11's scoped storage stands between the
app and the latter.)
## The controllers
SteamVR offers the Frame's controllers as themselves
(`XR_VALVE_frame_controller_interaction`,
`/interaction_profiles/valve/frame_controller_valve`; Valve's "Steam Frame
Input" page). Without bindings for it, SteamVR maps the Quest's onto them,
and the left Touch controller's X and Y land on the D-pad. With them the
game gets the Xbox's layout, as the right controller has it: A jump, B
flashlight (held: switch grenades), X action and reload, Y switch weapon,
menu pause; the left one's View is Back (scores), its D-pad the D-pad, the
bumpers switch grenades (left) and the flashlight (right). The VR options'
page of buttons shows this layout on the Frame.
## Findings
- 2026-10-02: runs flat and in VR (GLES 3.2 by Mesa zink on Turnip, eyes
1728x1728, 72 Hz, XR_FB_display_refresh_rate absent). Quad layers
(menus, HUD, instructions) are not shown; the projection layer is. Froze
at the cryo bay's look calibration; its log was lost to Lepton's wipe.
- 2026-10-04: SteamVR draws a quad's texels by their alpha alone; the
game's menus and HUD have none in their additive parts, so nothing showed.
On SteamVR (by the runtime's name) the overlays' alpha now comes from their
colour (host_xr.c; `HALO_XR_QUAD_ALPHA=colour|keep` overrides). With it,
menus, HUD and the campaign ran well (the user: "lief 1a"); no freeze.
- 2026-10-04: no online games found: the public MQTT brokers had banned the
home connection for a while (too many connections from tests that day);
broker.emqx.io came back within minutes. Lepton's network is the Frame's
own (same IP), DNS works.
- 2026-10-04: the refresh rate is SteamOS's setting; the runtime offers
only that one (the game asks for the highest at or below
`vr.refresh_rate`, and logs the rates offered). At 72 Hz the game holds
72 fps (about 11.6 ms a frame with multiview, eyes 7.6 ms; without it
eyes about 9 ms). At 90 Hz it cannot (11.1 ms a frame), and SteamVR's
making up of the missed frames showed as a doubled picture when turning
the head. Keep the Frame at 72 Hz, or lower `vr.resolution_scale` (about
0.85) for 90.
- 2026-10-05: doubled pictures when turning at 72 Hz too, the game at the
edge of its frame (12.5 of 13.9 ms; SteamVR reprojecting 2-8% of the
frames). The time was not the drawing's but waits: the layer copies read
the GL state back (glGet*) and released each image at once, and with
Mesa's GL thread (zink) every read waits for the thread to run all that
is queued, every release for the GPU (the left eye's copy took 4.5-7 ms
of the frame, the right one's, just after it, 0.7). On SteamVR the copies
are now deferred (host_xr.c): no state read back (the game sets its own
again), the images released together as the frame ends, and the eyes'
work flushed to the GPU as soon as they are copied.
`debug.xr_deferred_copies` = 0 brings back the old copies for comparing,
1 defers them on any runtime; `vr.log_performance` logs the frame's parts
and, with the compositor's counters, each copy's time.
+230
View File
@@ -0,0 +1,230 @@
# VR roadmap
What the VR port (Quest 3, Steam Frame) has, and what comes next, in the
agreed order. Updated with every step: a step is **done** once the user has
tried it in the headset and it is merged into `main`; **built** means in a
branch, not yet tried in the headset.
Several of the next steps follow
[HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR) and the
Steam Frame work it grew from,
[astromaddie/HaloCE-VR](https://github.com/astromaddie/HaloCE-VR) (both
CC0): their ideas, rebuilt into this port's own structure, with the source
named where code is taken over.
## Done (in `main`)
| Area | What |
| --- | --- |
| Headsets | Quest 3; Steam Frame (Lepton): its controllers' layout, a Steam library tile that installs, starts, updates and quits the game, data kept in `~/Documents/HaloCE` |
| Rendering | Both eyes in one pass (multiview) on the Quest; one by one on SteamVR, where multiview's eyes did not fuse; lens flares at their depth in stereo; HUD, menus, scope, crosshair and names as layers of their own |
| Aiming | The weapon in the hand, its shots from the muzzle, laser and crosshair options |
| Body | Physical crouch (`vr.crouch_limit`) beside the crouch button, leaning (`vr.lean_limit`), melee by swinging, grenades thrown with the left hand, two-handed weapons |
| Vehicles | The Warthog's wheel turned with the hands, right trigger throttle, left brake; first-person seats; turrets with handles |
| Comfort | Snap or smooth turning, a comfort vignette in vehicles, VR options in the main and pause menus (Y), a page of every controller button |
| Menus | The PC version's menus in VR (`vr.menus`), clicked with a laser; text typed on the on-screen keyboard |
| Multiplayer | Up to date with OpenCE (network version 20): cross-play, its server browser, co-op campaign, password lobbies; join codes and the VR online list |
| Bots | Bots in multiplayer: on foot (paths, items, objectives), in vehicles and at turrets; they make room for a joining person |
| Controls | Left-handed mode (`vr.handedness`): the weapon in the left hand, the arms and grips mirrored, sticks and buttons mirrored on both controllers (`vr.mirror_controls`), the assault rifle's counter read the right way round (2026-10-06) |
| Graphics | Presets low / medium / high / max (`vr.graphics`), refresh rate and resolution in the VR options, all kept over a restart (2026-10-06) |
| Arms and hands | Both arms by IK from shoulders that follow the torso, the elbow led by the hand's roll; fingers from trigger, grip and touch; `vr.body` arms / hands / off, `vr.fingers` (2026-10-06) |
## Next, in order
| # | Step | State |
| --- | --- | --- |
| 1 | Steam Frame CPU time: Mesa's trace markers off (after astromaddie/HaloCE-VR), no `glGet` stalls in the visibility counters' copies | **done** (in `main` 2026-10-06): markers quieted, no problems; the frame's work about unchanged (11-12.5 ms of 13.9 in a bot game) |
| 3a | Visible arms and hands: the arms by IK to the controllers, fingers by trigger and grip | **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good; 0.02 ms a frame) |
| 2 | Controls and comfort: left-handed mode, buttons remapped, flashlight by tapping the head, finer turning and vignette choices, graphics presets and refresh rate in the VR options | first part **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good): left-handed mode, graphics presets, refresh rate and resolution in the VR options. Left: buttons remapped, flashlight by tapping the head, finer turning and vignette choices |
| 3b | The whole body: torso from head and hands, legs and feet following in the room; full / arms only / hands only | |
| 3c | Weapons held physically, holsters at shoulder and hip (the buttons stay) | |
| 3d | Other VR players shown with their heads, hands and bodies (today their bodies move as flat players' do: only their aim and throws come from their hands) | |
| 4 | Later: disc image import in the launcher, sprint by arm swing (offline) | |
## Step 2, first part (done)
Left-handed mode and graphics presets, after HaloCE-Quest-VR (CC0): its
mirrored first-person model, its graphics switches.
Left-handed (`vr.handedness = "left"`):
- The weapon in the left hand: aim, shots, muzzle, laser, crosshair,
scope disc, turret aim. The first-person model is posed as the right
hand's in a mirror image of the room ("model space", `halo_vr.h`) and
mirrored back; a model whose node matrices mirror has its triangles'
winding turned over (`rasterizer_set_model_skinning`, `d3d8_gl.c`). The
arms reach to the other shoulders, the foregrip is gripped with the
right hand, the right hand's fingers follow its controller.
- Grenades held and thrown with the right trigger and hand; melee by the
left grip (a swing of either hand, as before); the motion tracker on the
right wrist (`halo_xr_submit.wrist_hand`); the rumble's light motor in
the weapon hand.
- The left trigger fires, the right one throws; at the Warthog's wheel the
left trigger accelerates, the right one brakes.
- With `vr.mirror_controls` (default): the right stick moves, the left
turns and steers; their clicks, the bumpers and the face buttons change
sides (Quest: X jumps, Y light / held grenades, A action and reload, B
switch weapon; Steam Frame: the left D-pad does A, B, X, Y as their
mirror image, down jump, left light, right action, up switch weapon,
and A, B, X, Y do the D-pad's). The menus keep their buttons; the
laser pointer works from either hand as before.
- Other machines see the shots leave the left hand (the network message
is unchanged: aim and muzzle from the camera).
- The assault rifle's ammo counter and compass read the right way round
on the mirrored model (2026-10-06): their geometry stays mirrored, each
digit's bitmap is drawn turned over in u and the digits' order across
the display swapped (numeric shaders drawn mirrored,
`rasterizer_xbox_transparent_geometry.c`). Checked flat in the emulator
with `debug.mirror_first_person` (the first-person weapon mirrored off
the headset): 60, 48, 36 ... 06 read correctly. Steam Frame good
(2026-10-06).
- Kept as they are (accepted 2026-10-06): the small static icons under the
counter (round and "x") stay mirror images (a texture atlas, not
numeric). The game's prompts still name the Xbox's buttons.
- Settings kept: on Android `config.toml` is written beside itself and
renamed over (`port_config.c`), so one pushed by adb (the shell's, which
the app could not open for writing) no longer loses the VR options'
changes at a restart.
- Headset tests 2026-10-06: Quest 3 and Steam Frame good (left hand,
controls mirrored and not, counter, presets, refresh rates, resolution,
settings kept over a restart).
Graphics (`vr.graphics`, `port/linux/game/vr_graphics.c`), set each frame
on the game's own switches:
| Preset | Leaves out |
| --- | --- |
| `"high"` (default) | nothing: as before |
| `"medium"` | object shadows, environment reflections, lens flares |
| `"low"` | also specular light, bump maps, grass and other detail objects, the screen's fog layer, contrails, weather, camouflage's second pass |
| `"max"` | nothing; models lit for each pixel, shadows at 512x512 |
A client in another's game keeps grass, lens flares and the fog layer as
the host's rules have them (`cheats.c`). Shadows, specular passes,
reflections and detail objects are draws (CPU, which the Frame's GL
driver is short of); bump maps, fog, camouflage and per-pixel lighting
the GPU's fill.
VR options (Y): two pages, PLAY (the comfort rows, "Weapon hand",
"Mirrored controls") and MORE ("Graphics", "Refresh rate", "Resolution",
the controller buttons page, Quit). The refresh rate steps through what
the runtime offers and changes at once; on the Steam Frame the row shows
SteamOS's rate. The resolution (`vr.resolution_scale`, 70-120%) takes a
restart. The controller buttons page shows the layout as set.
| Setting | Values |
| --- | --- |
| `vr.handedness` | `"right"` (default), `"left"` |
| `vr.mirror_controls` | `true` (default): sticks and buttons mirrored when left-handed; `false`: as right-handed (triggers and grips still follow the hands) |
| `vr.graphics` | `"low"`, `"medium"`, `"high"` (default), `"max"` |
| `vr.refresh_rate` | Hz, as offered (Quest 3: 72, 80, 90, 120) |
| `vr.resolution_scale` | 0.7 to 1.2, on restart |
To try in the headset (Quest 3 and Steam Frame):
1. Left-handed: aim, laser, crosshair, scope of sniper and rocket
launcher; weapon held right in the left hand, arms from the right
shoulders, nothing inside out (faces culled the wrong way would show
the inside of the gun or the glove).
2. Two-handed grip with the right hand; grenades in the right hand (held
and thrown); melee by grip and by swing; motion tracker on the right
wrist.
3. Buttons and sticks mirrored on the Touch and the Frame controllers
(every function reachable), then `Mirrored controls: Off`.
4. Vehicles: wheel, throttle and brake, steering stick, a gunner's aim;
menus and the laser pointer; a network game (shots from the left hand
on the other machine).
5. Graphics: each preset's look and smoothness (`vr.log_performance`),
in a bot game; Max on the Quest at 90 Hz; refresh rates 72/80/90/120
on the Quest; the Frame's row.
## Step 3a (done)
Tried on the Quest 3 and the Steam Frame, in `main`. The arm IK,
the forearm's twist and the fingers' poses follow HaloCE-Quest-VR (CC0),
in `port/linux/game/vr_arms.c`.
- The first-person pose (weapon, arms) is built once a frame instead of
per eye and scope view (SteamVR: two to three times before).
- `vr.log_performance` reports its cost (`VR: first person: ... ms a
frame in ... builds (arms ... ms), ... kept`) and, once, how the hands'
skin hangs from its bones (none from the root: no triangle filter
needed).
- Both arms bend from shoulders below the neck, square to the torso. The
torso follows the head beyond 15 degrees and turns towards the hands;
snap and smooth turns carry it at once.
- The free left arm bends at the elbow to the controller (before: carried
rigidly). The hand's roll is shared along forearm and upper arm.
- The free hand's fingers follow the controller: index from the trigger,
the others from the grip, the thumb from where it rests (touch on the
Quest's, Index and Steam Frame controllers; half curled where unknown),
closed round a held grenade. The weapon hand keeps its grip; its index
finger lifts off the trigger when the controller says it is off.
- host_xr: touch actions for thumb and index; `suggest()` takes 64
bindings (was 32, silently cut) and retries a profile without the
touch paths should the runtime refuse them.
Settings (`[vr]` in config.toml; the first two also in the VR options,
Y on the main or pause menu):
| Setting | Values |
| --- | --- |
| `vr.body` | `"arms"` (default), `"hands"` (hands alone, the arms shrunk behind the wrists), `"off"` (the weapon alone) |
| `vr.fingers` | `true` (default): fingers follow the controller; `false`: as the weapon's animation has them |
| `vr.arm_ik` | `true` (default): the free arm bends at the elbow; `false`: rigid as before, should its skin tear |
After the first Quest tries (2026-10-06):
- The shoulders 13 cm behind the eyes and 20 cm below the neck: the open
end of the sleeve was in view. Looking hard down to the side one still
sees into the arm (the model ends at the shoulder): left as it is.
- The elbow bends in the arm's plane (`vr_align_elbow`): it had turned in
with the elbow out to the side; the upper arm takes none of the hand's
roll.
- The free arm is solved first and its hand turned after (as
HaloCE-Quest-VR does): turned first, its skin stretched like a rubber
band.
- The elbow follows the hand's roll (lifted out, the controller rolls in):
before, it bent only towards a fixed direction.
- Arm length, shoulder width, fingers, grenade grip, body modes, scope,
vehicles: fine.
Not in it (3a.7): the glove fitted to each weapon's grip (gun anchor) and
the left hand animated through reloads.
## Open points outside the order
- Multiview on the Steam Frame: why the eyes do not fuse there (suspected
Mesa zink/Turnip); tools in branch `frame/doubled-images` (86335828),
not merged.
- Transparent geometry groups: the Xbox's 384 a window overflow in games
of 30 and more players.
- The VR on-screen keyboard shows a password in the clear while it is
typed.
## Headset tests (2026-10-06, all three in `main`)
Quest 3, 2026-10-06: menus, laser, VR options, head turning, arms and
hands, fingers, body modes, scope, vehicles, a bot game (bots spread and
leave their bases) and a large online game: all good.
Steam Frame, 2026-10-06: buttons, picture (no doubled image), arms, hands,
fingers, wheel: all good; SteamVR reprojected 18% of the frames, unseen.
What follows is left for the two headsets together.
Upstream merge (`merge/upstream-2026-10-06`):
- Menus, a bot game, head turning (no doubled image), co-op briefly.
- A password game hosted on the Frame, joined from the Quest.
- A bot making room for a person joining a full lobby.
Performance (`frame/performance-2`, and the merge's upstream speed-ups),
with `vr.log_performance = true`:
- Frame: the log says `trace markers: N descriptors to /dev/null`; frame
time ("working") against before (about 12.5 ms in a bot game on Blood
Gulch); SteamVR's reprojected share (vrcompositor.txt) against before
(2 to 17 %); no doubled image.
- Lens flares still appear and hide as before (their visibility counters'
copies changed).
- The pose cache (3a.1): `first person: ... builds` about one a frame;
scope, crosshair, laser and the weapon in the hand as before.
- Quest: 90 Hz held as before in a bot game and in a large online game;
nothing slower than the last build.
Arms and hands (3a): on the Steam Frame, as on the Quest (fingers on its
controllers, every button still working).
@@ -35,6 +35,7 @@ import java.nio.channels.FileChannel;
*/
public class LauncherActivity extends Activity {
private static final int PICK_IMAGE = 1;
private static final String SHARED_DATA_ROOT = "/home/steamos/Documents/HaloCE";
private File dataRoot;
private TextView status;
@@ -46,6 +47,12 @@ public class LauncherActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dataRoot = getExternalFilesDir(null);
// the Steam Frame: Lepton empties the app's own folders whenever it
// starts again; the game's data lives in the SteamOS user's
// Documents instead, when it is there (port/android/STEAM_FRAME.md;
// the same choice as host_main.c's)
if (new File(SHARED_DATA_ROOT, "maps/ui.map").isFile() && new File(SHARED_DATA_ROOT).canWrite())
dataRoot = new File(SHARED_DATA_ROOT);
// created by the app, so that files pushed into it with adb stay
// readable (a directory adb creates there belongs to the shell user)
if (dataRoot != null)
+8 -1
View File
@@ -119,7 +119,9 @@ headset before the game started, and set HALO_VR); 0 on failure */
int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate);
/* waits for the next frame and reports the predicted poses */
int host_xr_begin_frame(struct halo_xr_frame *frame);
/* copies a render target texture (0: clears) into a layer's image */
/* copies a render target texture (0: clears) into a layer's image: 0 not
copied, 1 copied with the GL state put back, 2 copied with the copy's GL
state left (the deferred copies, host_xr.c), which the game sets again */
int host_xr_copy_layer(int layer, unsigned int texture, int clear_alpha);
/* the same for part of the texture: left, top, right, bottom, 0..1 from its
top left */
@@ -133,6 +135,11 @@ into a layer's image, opaque */
int host_xr_copy_layer_array(int layer, unsigned int texture, int array_layer);
/* darkens the eye images from their edges, strength 0..1 (0: off) */
void host_xr_set_vignette(float strength);
/* the refresh rates the headset offers (Hz, at most maximum of them; 0 when
it lets none be chosen), and the one it runs at (0: unknown); and asking
for the highest offered at or below wanted (0: the highest) */
int host_xr_refresh_rates(float *rates, int maximum, float *current);
void host_xr_set_refresh_rate(float wanted);
/* ---------- Android */
+1
View File
@@ -92,6 +92,7 @@ void *host_resolve_import(const char *name);
/* ---------- SDL / GL (host_sdl.c, host_gl.c) */
void *host_gl_resolve(const char *name);
int host_gl_quiet_trace_markers(void);
/* ---------- VR (host_xr.c) */
+50 -9
View File
@@ -11,8 +11,52 @@ already have host types by then. Only strings need copying back.
#include <EGL/egl.h>
#include <GLES3/gl32.h>
#include <GLES2/gl2ext.h>
#include <dirent.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* Valve's Mesa (the Steam Frame's Lepton) writes a marker to the kernel's
trace_marker around every flush and context switch, a couple of hundred a
frame, whether or not anything traces. The descriptors it holds open on that
file are pointed at /dev/null instead (not closed: a reused descriptor would
take the markers elsewhere). After astromaddie/HaloCE-VR (CC0). Returns how
many were. */
int host_gl_quiet_trace_markers(void)
{
DIR *directory = opendir("/proc/self/fd");
struct dirent *entry;
int quieted = 0, null_fd;
if (!directory)
return 0;
null_fd = open("/dev/null", O_WRONLY | O_CLOEXEC);
while (null_fd >= 0 && (entry = readdir(directory)) != NULL)
{
char link[64], target[256];
ssize_t length;
int fd = atoi(entry->d_name);
if (entry->d_name[0] < '0' || entry->d_name[0] > '9' || fd == null_fd)
continue;
snprintf(link, sizeof(link), "/proc/self/fd/%d", fd);
length = readlink(link, target, sizeof(target) - 1);
if (length <= 0)
continue;
target[length] = 0;
if (strstr(target, "tracing/trace_marker") && dup3(null_fd, fd, O_CLOEXEC) == fd)
quieted++;
}
if (null_fd >= 0)
close(null_fd);
closedir(directory);
if (quieted)
host_logf(HOST_LOG_INFO, "trace markers: %d descriptors to /dev/null", quieted);
return quieted;
}
void *host_gl_resolve(const char *name)
{
@@ -88,14 +132,13 @@ half-drawn targets out of tile memory, for every lens flare. */
static GLuint counter_snapshots[COUNTER_SNAPSHOTS];
static uint32_t counter_snapshot_sizes[COUNTER_SNAPSHOTS];
/* (the copy bindings are left unbound after: the renderer binds them itself
for each copy. Asking GL what they were would wait for Mesa's GL thread, on
the Frame, to run everything queued) */
void host_gl_counters_snapshot(uint32_t buffer, uint32_t size, uint32_t slot)
{
GLint read_binding = 0, write_binding = 0;
if (slot >= COUNTER_SNAPSHOTS || !buffer || !size)
return;
glGetIntegerv(GL_COPY_READ_BUFFER_BINDING, &read_binding);
glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, &write_binding);
if (!counter_snapshots[slot])
glGenBuffers(1, &counter_snapshots[slot]);
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
@@ -108,19 +151,17 @@ void host_gl_counters_snapshot(uint32_t buffer, uint32_t size, uint32_t slot)
glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
glBindBuffer(GL_COPY_READ_BUFFER, buffer);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, size);
glBindBuffer(GL_COPY_READ_BUFFER, (GLuint)read_binding);
glBindBuffer(GL_COPY_WRITE_BUFFER, (GLuint)write_binding);
glBindBuffer(GL_COPY_READ_BUFFER, 0);
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
}
/* copies a snapshot out; its frame must be finished (host_gl_wait_frame) */
int host_gl_counters_read(uint32_t slot, void *out, uint32_t size)
{
GLint write_binding = 0;
const void *mapping;
if (slot >= COUNTER_SNAPSHOTS || !counter_snapshots[slot] || size > counter_snapshot_sizes[slot])
return 0;
glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, &write_binding);
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
mapping = glMapBufferRange(GL_COPY_WRITE_BUFFER, 0, size, GL_MAP_READ_BIT);
if (mapping)
@@ -128,7 +169,7 @@ int host_gl_counters_read(uint32_t slot, void *out, uint32_t size)
memcpy(out, mapping, size);
glUnmapBuffer(GL_COPY_WRITE_BUFFER);
}
glBindBuffer(GL_COPY_WRITE_BUFFER, (GLuint)write_binding);
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
return mapping != NULL;
}
+41
View File
@@ -90,6 +90,8 @@ int host_errno(void)
static char data_root[512];
static char save_root[512];
/* where a device that empties the app's folders keeps the game's data */
#define SHARED_DATA_ROOT "/home/steamos/Documents/HaloCE"
void host_android_path(int which, char *buffer, uint32_t size)
{
@@ -269,6 +271,12 @@ static void *game_main(void *unused)
external = SDL_GetAndroidExternalStoragePath();
if (!external)
host_fatal("Android storage is unavailable: %s", SDL_GetError());
/* the Steam Frame: Lepton empties the app's own folders whenever it
starts again; the game's data lives in the SteamOS user's Documents
instead, when it is there (port/android/STEAM_FRAME.md; the same choice
as LauncherActivity's) */
if (directory_has_maps(SHARED_DATA_ROOT) && access(SHARED_DATA_ROOT, W_OK) == 0)
external = SHARED_DATA_ROOT;
snprintf(data_root, sizeof(data_root), "%s", external);
snprintf(save_root, sizeof(save_root), "%s/save", external);
/* readable by adb (the shell user), for managing saves */
@@ -293,6 +301,19 @@ static void *game_main(void *unused)
environment_set(&environment, "HALO_SAVE_ROOT", save_root);
time_zone(zone, sizeof(zone));
environment_set(&environment, "TZ", zone);
/* internet play's MQTT brokers (network.brokers_file): the APK's list,
written beside config.toml at each start, as a desktop update replaces
the file beside its game */
{
size_t brokers_size = 0;
void *brokers = SDL_LoadFile("brokers.txt", &brokers_size);
snprintf(path, sizeof(path), "%s/brokers.txt", data_root);
if (!brokers || !SDL_SaveFile(path, brokers, brokers_size))
host_logf(HOST_LOG_ERROR, "cannot write %s: %s", path, SDL_GetError());
SDL_free(brokers);
}
snprintf(path, sizeof(path), "%s/config.toml", data_root);
image = SDL_LoadFile("halo_guest.elf", &image_size);
if (!image)
@@ -305,6 +326,26 @@ static void *game_main(void *unused)
ranges: the headset's runtime, loaded by the probe, maps its libraries
wherever there is room, the Xbox window at 0x80000000 included */
snprintf(path, sizeof(path), "%s/config.toml", data_root);
{
/* (debug.gl_thread: Mesa's GL thread on or off, as the driver's
mesa_glthread, before the GL context is made; -1 leaves it be) */
double gl_thread = config_number(path, "debug.gl_thread", -1.0);
if (gl_thread >= 0.0)
{
setenv("mesa_glthread", gl_thread != 0.0 ? "true" : "false", 1);
host_logf(HOST_LOG_INFO, "Mesa's GL thread %s (debug.gl_thread)", gl_thread != 0.0 ? "on" : "off");
}
}
{
/* (debug.xr_deferred_copies: the headset's layer copies deferred,
host_xr.c, or one by one; -1 as the runtime has it, deferred on
SteamVR) */
double deferred = config_number(path, "debug.xr_deferred_copies", -1.0);
if (deferred >= 0.0)
setenv("HALO_XR_DEFERRED", deferred != 0.0 ? "1" : "0", 1);
}
{
int eye_width, eye_height;
+5 -1
View File
@@ -151,7 +151,11 @@ uint32_t host_sdl_gl_create_context(uint32_t window)
{
SDL_Window *object = handle_get(window, _handle_window);
return object ? handle_new(_handle_context, SDL_GL_CreateContext(object)) : 0;
uint32_t context = object ? handle_new(_handle_context, SDL_GL_CreateContext(object)) : 0;
/* (the driver opens its trace markers with the context) */
host_gl_quiet_trace_markers();
return context;
}
int host_sdl_gl_make_current(uint32_t window, uint32_t context)
+623 -55
View File
@@ -38,6 +38,8 @@ picture; the copy flips them to GL's bottom-up images.
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sched.h>
#include <stdio.h>
#define XR_USE_PLATFORM_ANDROID
#define XR_USE_GRAPHICS_API_OPENGL_ES
@@ -56,6 +58,7 @@ struct xr_swapchain
GLuint framebuffers[MAXIMUM_SWAPCHAIN_IMAGES];
uint32_t acquired_index;
int filled; /* this frame */
int held; /* acquired, released only as the frame ends (xr.deferred) */
};
struct xr_hand_actions
@@ -102,15 +105,54 @@ static struct
XrAction button_b; /* B on the right controller, Y on the left */
XrAction menu;
XrAction thumbrest;
/* the fingers' touch, for the hands drawn (where the runtime has it):
the thumb on the stick, a button or the rest, the index on the trigger */
XrAction thumb_touch;
XrAction index_touch;
/* the Steam Frame's controllers' own (XR_VALVE_frame_controller_interaction) */
int has_frame_controller;
int frame_layout;
XrAction button_x;
XrAction button_y;
XrAction view;
XrAction bumper;
XrAction dpad[4];
XrAction haptic;
struct xr_hand_actions hands[HALO_XR_HAND_COUNT];
/* where the head and the left hand are this frame (the wrist layer) */
/* where the head and the hands are this frame (the wrist layer) */
XrPosef head;
XrPosef left_grip;
int left_grip_valid;
XrPosef grips[HALO_XR_HAND_COUNT];
int grips_valid[HALO_XR_HAND_COUNT];
int has_performance_settings;
/* (XR_META_performance_metrics: the compositor's counts, in the log,
for telling frames shown late from the game's own pace) */
int has_metrics;
PFN_xrQueryPerformanceMetricsCounterMETA query_metric;
XrPath metric_paths[16];
uint32_t metric_count;
struct timespec metrics_logged;
/* (SteamVR: the frames' thread kept on the fast cores, xr_fast_cores) */
int steamvr;
int cores_pinned;
/* (with the metrics: the time the copies into the layers' images and
xrEndFrame took, since the last line, and the frames) */
double copy_seconds[HALO_XR_LAYER_COUNT];
double end_seconds;
double acquire_seconds;
double release_seconds;
int timed_frames;
/* The copies without waits in the frame (SteamVR: HALO_XR_DEFERRED,
debug.xr_deferred_copies). Each copy otherwise reads the GL state back
to restore it (glGet*), and releases its image at once: with Mesa's GL
thread (the Steam Frame's zink) each read waits for the thread to run
everything queued, and each release for the GPU to finish it, so the
game's one thread stood still for the eyes' whole drawing in the middle
of every frame. Deferred, a copy leaves its GL state for the game to set
again (host_xr_copy_layer's 2) and keeps its image until the frame ends,
where all are released at once. */
int deferred;
int has_refresh_rate;
PFN_xrRequestDisplayRefreshRateFB request_refresh_rate;
PFN_xrGetDisplayRefreshRateFB get_refresh_rate;
@@ -122,6 +164,11 @@ static struct
GLint rect_uniform;
GLint cut_disc_uniform;
GLint vignette_uniform;
GLint alpha_from_colour_uniform;
/* the runtime draws a quad's texels by their alpha alone, ignoring the
colour of those with none (SteamVR on the Steam Frame): the game's
overlays, whose additive parts have none, get alpha from their colour */
int quad_alpha_from_colour;
GLint tangents_uniform;
float vignette;
/* the same copy from a layer of a texture array (the eyes drawn at
@@ -223,6 +270,7 @@ static const char copy_fragment_shader[] =
"precision highp float;\n"
"uniform sampler2D source;\n"
"uniform float cut_disc;\n"
"uniform float alpha_from_colour; /* 1: as opaque as it is bright (a quad's, SteamVR) */\n"
"uniform float vignette; /* 0..1: the eye images darken from their edges */\n"
"uniform vec4 tangents; /* the eye's edges: left, right, down, up */\n"
"in vec2 coordinate;\n"
@@ -236,6 +284,8 @@ static const char copy_fragment_shader[] =
"{\n"
" vec4 texel = texture(source, coordinate);\n"
" color = vec4(to_linear(texel.rgb), texel.a);\n"
" if (alpha_from_colour > 0.5)\n"
" color.a = max(color.a, max(color.r, max(color.g, color.b)));\n"
" if (cut_disc > 0.5)\n"
" {\n"
" float inside = 1.0 - smoothstep(0.95, 1.0, length(disc));\n"
@@ -326,6 +376,7 @@ static int copy_initialize(void)
xr.rect_uniform = glGetUniformLocation(xr.program, "rect");
xr.cut_disc_uniform = glGetUniformLocation(xr.program, "cut_disc");
xr.vignette_uniform = glGetUniformLocation(xr.program, "vignette");
xr.alpha_from_colour_uniform = glGetUniformLocation(xr.program, "alpha_from_colour");
xr.tangents_uniform = glGetUniformLocation(xr.program, "tangents");
xr.copy_array_layer = -1;
{
@@ -538,22 +589,51 @@ struct binding
const char *path;
};
static void suggest(const char *profile, const struct binding *bindings, int count)
{
XrActionSuggestedBinding suggested[32];
XrInteractionProfileSuggestedBinding information = { XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING };
int index;
/* a profile's bindings: those it has to have, and those it may (a path the
runtime does not know refuses the whole suggestion, which leaves the
profile with none: then the first alone, as before). Nonzero if taken. */
#define MAXIMUM_SUGGESTED_BINDINGS 64
for (index = 0; index < count && index < 32; index++)
static int suggest_bindings(const char *profile, const struct binding *bindings, int count,
const struct binding *optional, int optional_count)
{
XrActionSuggestedBinding suggested[MAXIMUM_SUGGESTED_BINDINGS];
XrInteractionProfileSuggestedBinding information = { XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING };
XrResult result;
int index, total = 0;
if (count + optional_count > MAXIMUM_SUGGESTED_BINDINGS)
{
suggested[index].action = *bindings[index].action;
suggested[index].binding = path_from_string(bindings[index].path);
host_logf(HOST_LOG_ERROR, "xr: %d bindings for %s, room for %d: the rest left out", count + optional_count,
profile, MAXIMUM_SUGGESTED_BINDINGS);
}
for (index = 0; index < count + optional_count && total < MAXIMUM_SUGGESTED_BINDINGS; index++)
{
const struct binding *binding = index < count ? &bindings[index] : &optional[index - count];
suggested[total].action = *binding->action;
suggested[total].binding = path_from_string(binding->path);
total++;
}
information.interactionProfile = path_from_string(profile);
information.suggestedBindings = suggested;
information.countSuggestedBindings = (uint32_t)index;
if (!XR_SUCCEEDED(xrSuggestInteractionProfileBindings(xr.instance, &information)))
host_logf(HOST_LOG_WARN, "xr: no bindings for %s", profile);
information.countSuggestedBindings = (uint32_t)total;
result = xrSuggestInteractionProfileBindings(xr.instance, &information);
if (XR_SUCCEEDED(result))
return 1;
if (optional_count > 0)
{
host_logf(HOST_LOG_WARN, "xr: %s refused with its optional bindings (%d); suggested without them", profile,
(int)result);
return suggest_bindings(profile, bindings, count, NULL, 0);
}
host_logf(HOST_LOG_WARN, "xr: no bindings for %s (%d)", profile, (int)result);
return 0;
}
static void suggest(const char *profile, const struct binding *bindings, int count)
{
suggest_bindings(profile, bindings, count, NULL, 0);
}
static int actions_initialize(void)
@@ -580,6 +660,16 @@ static int actions_initialize(void)
xr.button_b = action_create("button_upper", "B / Y", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.menu = action_create("menu", "Menu", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.thumbrest = action_create("thumbrest", "Thumb rest", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.thumb_touch = action_create("thumb_touch", "Thumb resting", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.index_touch = action_create("index_touch", "Finger on the trigger", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.button_x = action_create("button_x", "X", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.button_y = action_create("button_y", "Y", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.view = action_create("view", "View", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.bumper = action_create("bumper", "Bumper", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.dpad[0] = action_create("dpad_up", "D-pad up", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.dpad[1] = action_create("dpad_down", "D-pad down", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.dpad[2] = action_create("dpad_left", "D-pad left", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.dpad[3] = action_create("dpad_right", "D-pad right", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.haptic = action_create("haptic", "Rumble", XR_ACTION_TYPE_VIBRATION_OUTPUT);
{
@@ -644,8 +734,95 @@ static int actions_initialize(void)
{ &xr.haptic, "/user/hand/right/output/haptic" },
};
suggest("/interaction_profiles/oculus/touch_controller", touch, sizeof(touch) / sizeof(touch[0]));
suggest("/interaction_profiles/valve/index_controller", index, sizeof(index) / sizeof(index[0]));
/* the Steam Frame's controllers, as themselves: the right one has
A, B, X, Y and a menu button, the left one a D-pad and a View button
(port/android/STEAM_FRAME.md) */
static const struct binding frame_controller[] =
{
{ &xr.grip_pose, "/user/hand/left/input/grip/pose" },
{ &xr.grip_pose, "/user/hand/right/input/grip/pose" },
{ &xr.aim_pose, "/user/hand/left/input/aim/pose" },
{ &xr.aim_pose, "/user/hand/right/input/aim/pose" },
{ &xr.trigger, "/user/hand/left/input/trigger/value" },
{ &xr.trigger, "/user/hand/right/input/trigger/value" },
{ &xr.squeeze, "/user/hand/left/input/squeeze/value" },
{ &xr.squeeze, "/user/hand/right/input/squeeze/value" },
{ &xr.thumbstick, "/user/hand/left/input/thumbstick" },
{ &xr.thumbstick, "/user/hand/right/input/thumbstick" },
{ &xr.thumbstick_click, "/user/hand/left/input/thumbstick/click" },
{ &xr.thumbstick_click, "/user/hand/right/input/thumbstick/click" },
{ &xr.button_a, "/user/hand/right/input/a/click" },
{ &xr.button_b, "/user/hand/right/input/b/click" },
{ &xr.button_x, "/user/hand/right/input/x/click" },
{ &xr.button_y, "/user/hand/right/input/y/click" },
{ &xr.menu, "/user/hand/right/input/menu/click" },
{ &xr.view, "/user/hand/left/input/view/click" },
{ &xr.bumper, "/user/hand/left/input/bumper/click" },
{ &xr.bumper, "/user/hand/right/input/bumper/click" },
{ &xr.dpad[0], "/user/hand/left/input/dpad_up/click" },
{ &xr.dpad[1], "/user/hand/left/input/dpad_down/click" },
{ &xr.dpad[2], "/user/hand/left/input/dpad_left/click" },
{ &xr.dpad[3], "/user/hand/left/input/dpad_right/click" },
{ &xr.haptic, "/user/hand/left/output/haptic" },
{ &xr.haptic, "/user/hand/right/output/haptic" },
};
/* the fingers' touch, as each profile has it (after HaloCE-Quest-VR,
CC0; the Frame's from Valve's "Steam Frame Input" page): optional, a
profile refusing them keeps its buttons */
static const struct binding touch_fingers[] =
{
{ &xr.thumb_touch, "/user/hand/left/input/thumbstick/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/thumbstick/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/thumbrest/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/thumbrest/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/x/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/y/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/a/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/b/touch" },
{ &xr.index_touch, "/user/hand/left/input/trigger/touch" },
{ &xr.index_touch, "/user/hand/right/input/trigger/touch" },
};
static const struct binding index_fingers[] =
{
{ &xr.thumb_touch, "/user/hand/left/input/thumbstick/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/thumbstick/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/trackpad/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/trackpad/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/a/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/b/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/a/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/b/touch" },
{ &xr.index_touch, "/user/hand/left/input/trigger/touch" },
{ &xr.index_touch, "/user/hand/right/input/trigger/touch" },
};
static const struct binding frame_fingers[] =
{
{ &xr.thumb_touch, "/user/hand/left/input/thumbstick/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/thumbstick/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/a/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/b/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/x/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/y/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/dpad_up/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/dpad_down/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/dpad_left/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/dpad_right/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/view/touch" },
{ &xr.index_touch, "/user/hand/left/input/trigger/touch" },
{ &xr.index_touch, "/user/hand/right/input/trigger/touch" },
};
if (xr.has_frame_controller)
{
suggest_bindings("/interaction_profiles/valve/frame_controller_valve", frame_controller,
sizeof(frame_controller) / sizeof(frame_controller[0]), frame_fingers,
sizeof(frame_fingers) / sizeof(frame_fingers[0]));
}
suggest_bindings("/interaction_profiles/oculus/touch_controller", touch, sizeof(touch) / sizeof(touch[0]),
touch_fingers, sizeof(touch_fingers) / sizeof(touch_fingers[0]));
suggest_bindings("/interaction_profiles/valve/index_controller", index, sizeof(index) / sizeof(index[0]),
index_fingers, sizeof(index_fingers) / sizeof(index_fingers[0]));
suggest("/interaction_profiles/khr/simple_controller", simple, sizeof(simple) / sizeof(simple[0]));
}
@@ -688,6 +865,21 @@ static int action_boolean(XrAction action, int hand)
return state.currentState != XR_FALSE;
}
/* (and whether the runtime gives the action for the hand at all) */
static int action_boolean_active(XrAction action, int hand, int *active)
{
XrActionStateGetInfo information = { XR_TYPE_ACTION_STATE_GET_INFO };
XrActionStateBoolean state = { XR_TYPE_ACTION_STATE_BOOLEAN };
*active = 0;
information.action = action;
information.subactionPath = xr.hands[hand].path;
if (!XR_SUCCEEDED(xrGetActionStateBoolean(xr.session, &information, &state)) || !state.isActive)
return 0;
*active = 1;
return state.currentState != XR_FALSE;
}
static int action_vector(XrAction action, int hand, float vector[2])
{
XrActionStateGetInfo information = { XR_TYPE_ACTION_STATE_GET_INFO };
@@ -713,6 +905,30 @@ static void hands_read(struct halo_xr_frame *frame)
sync.activeActionSets = &active;
if (!XR_SUCCEEDED(xrSyncActions(xr.session, &sync)))
return;
{
/* (the controllers' profile the runtime chose, in the log as it
changes) */
static XrPath logged[HALO_XR_HAND_COUNT];
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
{
XrInteractionProfileState state = { XR_TYPE_INTERACTION_PROFILE_STATE };
if (XR_SUCCEEDED(xrGetCurrentInteractionProfile(xr.session, xr.hands[hand].path, &state)) &&
state.interactionProfile != logged[hand])
{
char name[XR_MAX_PATH_LENGTH] = "none";
uint32_t length = 0;
logged[hand] = state.interactionProfile;
if (state.interactionProfile != XR_NULL_PATH)
xrPathToString(xr.instance, state.interactionProfile, sizeof(name), &length, name);
if (hand == HALO_XR_HAND_RIGHT)
xr.frame_layout = strstr(name, "frame_controller") != NULL;
host_logf(HOST_LOG_INFO, "xr: the %s hand's controller: %s", hand ? "right" : "left", name);
}
}
}
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
{
struct halo_xr_hand *out = &frame->hands[hand];
@@ -730,11 +946,8 @@ static void hands_read(struct halo_xr_frame *frame)
{
out->flags = flags_from_location(location.locationFlags);
pose_from_xr(&out->grip, &location.pose);
if (hand == HALO_XR_HAND_LEFT)
{
xr.left_grip = location.pose;
xr.left_grip_valid = (out->flags & HALO_XR_POSE_VALID) != 0;
}
xr.grips[hand] = location.pose;
xr.grips_valid[hand] = (out->flags & HALO_XR_POSE_VALID) != 0;
}
location.locationFlags = 0;
if (XR_SUCCEEDED(xrLocateSpace(xr.hands[hand].aim_space, xr.local_space,
@@ -757,7 +970,42 @@ static void hands_read(struct halo_xr_frame *frame)
(action_boolean(xr.menu, hand) ? HALO_XR_BUTTON_MENU : 0) |
(action_boolean(xr.thumbrest, hand) ? HALO_XR_BUTTON_THUMBREST : 0) |
(out->trigger > 0.5f ? HALO_XR_BUTTON_TRIGGER : 0) |
(out->squeeze > 0.5f ? HALO_XR_BUTTON_SQUEEZE : 0);
(out->squeeze > 0.5f ? HALO_XR_BUTTON_SQUEEZE : 0) |
(action_boolean(xr.view, hand) ? HALO_XR_BUTTON_VIEW : 0) |
(action_boolean(xr.bumper, hand) ? HALO_XR_BUTTON_BUMPER : 0) |
(action_boolean(xr.dpad[0], hand) ? HALO_XR_BUTTON_DPAD_UP : 0) |
(action_boolean(xr.dpad[1], hand) ? HALO_XR_BUTTON_DPAD_DOWN : 0) |
(action_boolean(xr.dpad[2], hand) ? HALO_XR_BUTTON_DPAD_LEFT : 0) |
(action_boolean(xr.dpad[3], hand) ? HALO_XR_BUTTON_DPAD_RIGHT : 0);
{
/* the fingers' touch, and whether there is any */
int thumb_active = 0, index_active = 0;
if (action_boolean_active(xr.thumb_touch, hand, &thumb_active))
out->buttons |= HALO_XR_BUTTON_THUMB_TOUCH;
if (action_boolean_active(xr.index_touch, hand, &index_active))
out->buttons |= HALO_XR_BUTTON_INDEX_TOUCH;
if (thumb_active)
out->buttons |= HALO_XR_BUTTON_TOUCH_KNOWN;
}
}
frame->controller_layout = xr.frame_layout ? HALO_XR_LAYOUT_FRAME : HALO_XR_LAYOUT_TOUCH;
if (xr.frame_layout)
{
/* (the Frame's right X, Y and menu button: the game's X, Y and Start,
which the Quest has on the left) */
struct halo_xr_hand *left = &frame->hands[HALO_XR_HAND_LEFT];
struct halo_xr_hand *right = &frame->hands[HALO_XR_HAND_RIGHT];
if (action_boolean(xr.button_x, HALO_XR_HAND_RIGHT))
left->buttons |= HALO_XR_BUTTON_A;
if (action_boolean(xr.button_y, HALO_XR_HAND_RIGHT))
left->buttons |= HALO_XR_BUTTON_B;
if (right->buttons & HALO_XR_BUTTON_MENU)
{
right->buttons &= ~HALO_XR_BUTTON_MENU;
left->buttons |= HALO_XR_BUTTON_MENU;
}
}
}
@@ -774,6 +1022,13 @@ static void refresh_rate_choose(float wanted)
{
return;
}
{
char line[128] = "";
for (index = 0; index < count; index++)
snprintf(line + strlen(line), sizeof(line) - strlen(line), " %.0f", rates[index]);
host_logf(HOST_LOG_INFO, "xr: refresh rates offered:%s Hz", line);
}
/* the highest rate not above the one wanted (0: the highest) */
for (index = 0; index < count; index++)
{
@@ -790,7 +1045,9 @@ On the back of the left wrist, like a watch: behind the palm, where the
grip pose is, and out of the back of the hand. In the left grip pose -Z
runs to the fingers, +Y up the handle and -X out of the back of the hand;
the layer faces -X with its top towards the elbow, as the wrist turned to
the eyes reads it. Shown only while it is. */
the eyes reads it. Shown only while it is. Left-handed (wrist_hand), on the
right wrist, as the mirror image: there +X runs out of the back of the
hand. */
#define WRIST_IMAGE_SIZE 512
#define CROSSHAIR_IMAGE_SIZE 256
@@ -830,26 +1087,32 @@ static void quaternion_rotate_vector(const XrQuaternionf *q, const XrVector3f *v
static int wrist_pose(const struct halo_xr_submit *submit, XrPosef *pose)
{
/* the layer's axes in the grip's: x = -Y, y = +Z, z = -X, so the
layer's right and down, as it is read, are the grip's -Y and -Z */
static const XrQuaternionf layer_in_grip = { 0.5f, -0.5f, -0.5f, 0.5f };
const XrVector3f offset = { -WRIST_ABOVE_HAND, -WRIST_DOWN_HANDLE - submit->wrist_right,
WRIST_BEHIND_PALM - submit->wrist_down };
/* the layer's axes in the left grip's: x = -Y, y = +Z, z = -X, so the
layer's right and down, as it is read, are the grip's -Y and -Z; in the
right grip's x = +Y, y = +Z, z = +X (the mirror image, read the right
way round: its x turned back) */
static const XrQuaternionf layer_in_left_grip = { 0.5f, -0.5f, -0.5f, 0.5f };
static const XrQuaternionf layer_in_right_grip = { 0.5f, 0.5f, 0.5f, 0.5f };
int right = submit->wrist_hand == HALO_XR_HAND_RIGHT;
const XrPosef *grip = &xr.grips[right ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT];
const XrVector3f offset = { right ? WRIST_ABOVE_HAND : -WRIST_ABOVE_HAND,
-WRIST_DOWN_HANDLE - submit->wrist_right, WRIST_BEHIND_PALM - submit->wrist_down };
const XrVector3f layer_normal = { 0.0f, 0.0f, 1.0f };
/* clockwise as read: about the layer's normal, negatively */
float half_turn = -submit->wrist_turn * WRIST_PI / 360.0f;
/* clockwise as read: about the layer's normal, negatively (the mirror
image's the other way) */
float half_turn = (right ? 1.0f : -1.0f) * submit->wrist_turn * WRIST_PI / 360.0f;
XrQuaternionf turn = { 0.0f, 0.0f, sinf(half_turn), cosf(half_turn) };
XrQuaternionf unturned;
XrVector3f moved, normal, to_eyes;
float distance;
if (!xr.left_grip_valid)
if (!xr.grips_valid[right ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT])
return 0;
quaternion_rotate_vector(&xr.left_grip.orientation, &offset, &moved);
pose->position.x = xr.left_grip.position.x + moved.x;
pose->position.y = xr.left_grip.position.y + moved.y;
pose->position.z = xr.left_grip.position.z + moved.z;
quaternion_multiply(&xr.left_grip.orientation, &layer_in_grip, &unturned);
quaternion_rotate_vector(&grip->orientation, &offset, &moved);
pose->position.x = grip->position.x + moved.x;
pose->position.y = grip->position.y + moved.y;
pose->position.z = grip->position.z + moved.z;
quaternion_multiply(&grip->orientation, right ? &layer_in_right_grip : &layer_in_left_grip, &unturned);
quaternion_multiply(&unturned, &turn, &pose->orientation);
quaternion_rotate_vector(&pose->orientation, &layer_normal, &normal);
@@ -895,6 +1158,12 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
/* the loader and the runtime keep the activity */
activity = (*environment)->NewGlobalRef(environment, activity);
/* (The Steam Frame lists an implicit API layer of its own for Vulkan
apps, XR_APILAYER_VALVE_fdm_injection, whose library an app cannot load
from its namespace, failing xrCreateInstance; the Android loader reads
no variable to turn it off, so the app carries a pass-through layer
under that library's name: port/android/xr_layer/fdm_injection_stub.c) */
if (!XR_SUCCEEDED(xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR",
(PFN_xrVoidFunction *)&initialize_loader)) || !initialize_loader)
{
@@ -922,6 +1191,23 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
extensions[index].next = NULL;
}
xrEnumerateInstanceExtensionProperties(NULL, extension_count, &extension_count, extensions);
{
/* (the runtime's extensions, in the log: what a headset offers) */
char line[512] = "";
for (index = 0; index < extension_count; index++)
{
if (strlen(line) + strlen(extensions[index].extensionName) + 2 >= sizeof(line))
{
host_logf(HOST_LOG_INFO, "xr: extensions %s", line);
line[0] = 0;
}
strcat(line, " ");
strcat(line, extensions[index].extensionName);
}
if (line[0])
host_logf(HOST_LOG_INFO, "xr: extensions %s", line);
}
if (!has_extension(extensions, extension_count, XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME))
{
host_logf(HOST_LOG_ERROR, "xr: the runtime has no OpenGL ES support");
@@ -933,9 +1219,15 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
xr.has_performance_settings = has_extension(extensions, extension_count, XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME);
if (xr.has_performance_settings)
enabled[enabled_count++] = XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME;
xr.has_frame_controller = has_extension(extensions, extension_count, "XR_VALVE_frame_controller_interaction");
if (xr.has_frame_controller)
enabled[enabled_count++] = "XR_VALVE_frame_controller_interaction";
xr.has_refresh_rate = has_extension(extensions, extension_count, XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME);
if (xr.has_refresh_rate)
enabled[enabled_count++] = XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME;
xr.has_metrics = has_extension(extensions, extension_count, XR_META_PERFORMANCE_METRICS_EXTENSION_NAME);
if (xr.has_metrics)
enabled[enabled_count++] = XR_META_PERFORMANCE_METRICS_EXTENSION_NAME;
android.applicationVM = machine;
android.applicationActivity = activity;
@@ -960,6 +1252,23 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
}
xrGetSystemProperties(xr.instance, xr.system, &properties);
strncpy(xr.system_name, properties.systemName, sizeof(xr.system_name) - 1);
{
/* (SteamVR: the quads' alpha from their colour; HALO_XR_QUAD_ALPHA
"colour" or "keep" says otherwise, for trying) */
XrInstanceProperties instance = { XR_TYPE_INSTANCE_PROPERTIES };
const char *choice = getenv("HALO_XR_QUAD_ALPHA");
if (XR_SUCCEEDED(xrGetInstanceProperties(xr.instance, &instance)))
{
xr.quad_alpha_from_colour = strstr(instance.runtimeName, "SteamVR") != NULL;
xr.steamvr = xr.quad_alpha_from_colour;
host_logf(HOST_LOG_INFO, "xr: runtime %s", instance.runtimeName);
}
if (choice)
xr.quad_alpha_from_colour = !strcmp(choice, "colour");
if (xr.quad_alpha_from_colour)
host_logf(HOST_LOG_INFO, "xr: the overlays' alpha from their colour");
}
views[0].type = views[1].type = XR_TYPE_VIEW_CONFIGURATION_VIEW;
views[0].next = views[1].next = NULL;
@@ -1066,6 +1375,29 @@ int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate)
xr.get_refresh_rate(xr.session, &info->refresh_rate);
}
/* (the runtime's GL binding brings contexts of its own, with their own
trace markers: host_gl.c) */
host_gl_quiet_trace_markers();
if (xr.has_metrics)
{
PFN_xrEnumeratePerformanceMetricsCounterPathsMETA enumerate = NULL;
PFN_xrSetPerformanceMetricsStateMETA set_state = NULL;
XrPerformanceMetricsStateMETA state = { XR_TYPE_PERFORMANCE_METRICS_STATE_META };
xrGetInstanceProcAddr(xr.instance, "xrEnumeratePerformanceMetricsCounterPathsMETA",
(PFN_xrVoidFunction *)&enumerate);
xrGetInstanceProcAddr(xr.instance, "xrSetPerformanceMetricsStateMETA", (PFN_xrVoidFunction *)&set_state);
xrGetInstanceProcAddr(xr.instance, "xrQueryPerformanceMetricsCounterMETA",
(PFN_xrVoidFunction *)&xr.query_metric);
state.enabled = XR_TRUE;
xr.has_metrics = enumerate && set_state && xr.query_metric &&
XR_SUCCEEDED(enumerate(xr.instance, 16, &xr.metric_count, xr.metric_paths)) &&
XR_SUCCEEDED(set_state(xr.session, &state));
host_logf(HOST_LOG_INFO, "xr: performance metrics %s (%u counters)", xr.has_metrics ? "on" : "off",
xr.has_metrics ? xr.metric_count : 0);
}
if (xr.has_performance_settings)
{
/* the game's one busy thread and its GPU work both need the clocks
@@ -1082,6 +1414,16 @@ int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate)
}
}
{
/* (deferred on SteamVR; HALO_XR_DEFERRED "1" or "0" says otherwise:
debug.xr_deferred_copies, host_main.c) */
const char *choice = getenv("HALO_XR_DEFERRED");
xr.deferred = choice && *choice ? atoi(choice) != 0 : xr.steamvr;
host_logf(HOST_LOG_INFO, "xr: layer copies %s", xr.deferred ?
"deferred (no waits for the GL thread or the GPU in the frame)" : "restored and released one by one");
}
info->eye_width = xr.eye_width;
info->eye_height = xr.eye_height;
info->overlay_width = xr.eye_width;
@@ -1218,7 +1560,7 @@ int host_xr_begin_frame(struct halo_xr_frame *frame)
pose_from_xr(&frame->head, &head.pose);
xr.head = head.pose;
}
xr.left_grip_valid = 0;
xr.grips_valid[HALO_XR_HAND_LEFT] = xr.grips_valid[HALO_XR_HAND_RIGHT] = 0;
if (xr.focused)
hands_read(frame);
return 1;
@@ -1245,8 +1587,30 @@ int host_xr_copy_layer_array(int layer, uint32_t texture, int array_layer)
/* copies part of a render target (left, top, right, bottom, 0..1 from its
top left) into a layer's image */
static double seconds_now(void)
{
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (double)now.tv_sec + (double)now.tv_nsec * 1e-9;
}
static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
float bottom);
int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
float bottom)
{
double start = xr.has_metrics ? seconds_now() : 0.0;
int copied = copy_layer_region(layer, texture, clear_alpha, left, top, right, bottom);
if (xr.has_metrics && layer >= 0 && layer < HALO_XR_LAYER_COUNT)
xr.copy_seconds[layer] += seconds_now() - start;
return copied;
}
static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
float bottom)
{
struct xr_swapchain *swapchain;
XrSwapchainImageAcquireInfo acquire = { XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO };
@@ -1258,17 +1622,27 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
return 0;
swapchain = &xr.swapchains[layer];
if (swapchain->filled)
return 1;
if (!xr_ok(xrAcquireSwapchainImage(swapchain->handle, &acquire, &swapchain->acquired_index),
"xrAcquireSwapchainImage"))
{
return 0;
}
return xr.deferred ? 2 : 1;
wait.timeout = XR_INFINITE_DURATION;
if (!xr_ok(xrWaitSwapchainImage(swapchain->handle, &wait), "xrWaitSwapchainImage"))
return 0;
{
double start = xr.has_metrics ? seconds_now() : 0.0;
gl_save(&saved);
if (!xr_ok(xrAcquireSwapchainImage(swapchain->handle, &acquire, &swapchain->acquired_index),
"xrAcquireSwapchainImage"))
{
return 0;
}
if (!xr_ok(xrWaitSwapchainImage(swapchain->handle, &wait), "xrWaitSwapchainImage"))
return 0;
if (xr.has_metrics)
xr.acquire_seconds += seconds_now() - start;
}
/* (deferred: nothing read back; the game sets its state again) */
if (xr.deferred)
glActiveTexture(GL_TEXTURE0);
else
gl_save(&saved);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, swapchain->framebuffers[swapchain->acquired_index]);
glViewport(0, 0, swapchain->width, swapchain->height);
glDisable(GL_SCISSOR_TEST);
@@ -1316,6 +1690,8 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
glUniform1i(xr.source_uniform, 0);
glUniform4f(xr.rect_uniform, left, top, right, bottom);
glUniform1f(xr.cut_disc_uniform, clear_alpha == HALO_XR_COPY_DISC ? 1.0f : 0.0f);
glUniform1f(xr.alpha_from_colour_uniform, xr.quad_alpha_from_colour && clear_alpha == HALO_XR_COPY_ALPHA &&
layer != HALO_XR_LAYER_EYE_LEFT && layer != HALO_XR_LAYER_EYE_RIGHT ? 1.0f : 0.0f);
if ((layer == HALO_XR_LAYER_EYE_LEFT || layer == HALO_XR_LAYER_EYE_RIGHT) && xr.vignette > 0.001f)
{
const XrFovf *fov = &xr.views[layer - HALO_XR_LAYER_EYE_LEFT].fov;
@@ -1331,13 +1707,22 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
glBindVertexArray(xr.vertex_array);
glBindTexture(GL_TEXTURE_2D, texture);
glBindSampler(0, xr.sampler);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, &swizzle_r);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, &swizzle_b);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, swizzle_r);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, swizzle_b);
if (xr.deferred)
{
/* (the game's render targets, all a copy reads, have no
swizzle: only its uploaded textures get one, xbox_textures.c) */
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
else
{
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, &swizzle_r);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, &swizzle_b);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, swizzle_r);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, swizzle_b);
}
if (clear_alpha == HALO_XR_COPY_OPAQUE)
{
/* an opaque layer: the game's alpha channel holds other things */
@@ -1351,13 +1736,95 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
glClearColor(0.0f, 0.0f, 0.0f, clear_alpha ? 1.0f : 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
swapchain->filled = 1;
if (xr.deferred)
{
swapchain->held = 1;
/* (the eyes, most of the frame's work, on to the GPU while the
game draws the rest) */
if (layer == HALO_XR_LAYER_EYE_RIGHT)
glFlush();
return 2;
}
gl_restore(&saved);
xrReleaseSwapchainImage(swapchain->handle, &release);
swapchain->filled = 1;
return 1;
}
/* deferred: the images the frame's copies kept, released before it ends */
static void release_held(void)
{
double start = xr.has_metrics ? seconds_now() : 0.0;
int layer;
for (layer = 0; layer < HALO_XR_LAYER_COUNT; layer++)
{
XrSwapchainImageReleaseInfo release = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO };
struct xr_swapchain *swapchain = &xr.swapchains[layer];
if (!swapchain->held)
continue;
swapchain->held = 0;
xr_ok(xrReleaseSwapchainImage(swapchain->handle, &release), "xrReleaseSwapchainImage");
}
if (xr.has_metrics)
xr.release_seconds += seconds_now() - start;
}
/* The Steam Frame: Android in a container, without XR_EXT_performance_settings
to ask for the clocks, and the scheduler free to put the game's one busy
thread, which simulates and draws each frame, on a slow core (its two
2.3 GHz ones) now and then: frames late to the compositor, shown twice,
the picture doubled as the head turns. The thread that ends the frames is
kept on the cores no slower than nine tenths of the fastest. */
static void xr_fast_cores(void)
{
cpu_set_t set;
long fastest = 0, speeds[CPU_SETSIZE];
int cpu, count = 0;
char line[128] = "";
for (cpu = 0; cpu < CPU_SETSIZE && cpu < 64; cpu++)
{
char path[96];
FILE *file;
speeds[cpu] = 0;
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", cpu);
file = fopen(path, "r");
if (!file)
continue;
if (fscanf(file, "%ld", &speeds[cpu]) != 1)
speeds[cpu] = 0;
fclose(file);
if (speeds[cpu] > fastest)
fastest = speeds[cpu];
}
if (!fastest)
{
host_logf(HOST_LOG_INFO, "xr: the cores' speeds unknown: the frames' thread left where it runs");
return;
}
CPU_ZERO(&set);
for (cpu = 0; cpu < CPU_SETSIZE && cpu < 64; cpu++)
{
if (speeds[cpu] && speeds[cpu] * 10 >= fastest * 9)
{
char item[8];
CPU_SET(cpu, &set);
count++;
snprintf(item, sizeof(item), " %d", cpu);
strcat(line, item);
}
}
if (count && sched_setaffinity(0, sizeof(set), &set) == 0)
host_logf(HOST_LOG_INFO, "xr: the frames' thread on the fast cores:%s (fastest %ld MHz)", line, fastest / 1000);
else
host_logf(HOST_LOG_INFO, "xr: the frames' thread could not be kept on the fast cores");
}
int host_xr_end_frame(const struct halo_xr_submit *submit)
{
XrFrameEndInfo end = { XR_TYPE_FRAME_END_INFO };
@@ -1375,6 +1842,7 @@ int host_xr_end_frame(const struct halo_xr_submit *submit)
if (!xr.frame_begun)
return 0;
xr.frame_begun = 0;
release_held();
if (xr.frame_state.shouldRender && submit->eyes && xr.views_valid &&
xr.swapchains[HALO_XR_LAYER_EYE_LEFT].filled && xr.swapchains[HALO_XR_LAYER_EYE_RIGHT].filled)
@@ -1500,11 +1968,83 @@ int host_xr_end_frame(const struct halo_xr_submit *submit)
}
}
if (xr.steamvr && !xr.cores_pinned)
{
xr.cores_pinned = 1;
if (!getenv("HALO_XR_ANY_CORE"))
xr_fast_cores();
}
/* (every five seconds, the compositor's counters, and the core the
frames' thread is on) */
if (xr.has_metrics)
{
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (now.tv_sec - xr.metrics_logged.tv_sec >= 5)
{
char line[768] = "";
uint32_t counter;
xr.metrics_logged = now;
for (counter = 0; counter < xr.metric_count && counter < 16; counter++)
{
XrPerformanceMetricsCounterMETA value = { XR_TYPE_PERFORMANCE_METRICS_COUNTER_META };
char name[XR_MAX_PATH_LENGTH];
uint32_t length = 0;
char item[96];
if (!XR_SUCCEEDED(xr.query_metric(xr.session, xr.metric_paths[counter], &value)) ||
!XR_SUCCEEDED(xrPathToString(xr.instance, xr.metric_paths[counter], sizeof(name), &length, name)))
{
continue;
}
snprintf(item, sizeof(item), " %s=%g", strrchr(name, '/') ? strrchr(name, '/') + 1 : name,
(value.counterFlags & XR_PERFORMANCE_METRICS_COUNTER_FLOAT_VALUE_VALID_BIT_META) ?
(double)value.floatValue : (double)value.uintValue);
if (strlen(line) + strlen(item) < sizeof(line))
strcat(line, item);
}
host_logf(HOST_LOG_INFO, "xr: metrics%s, on core %d", line, sched_getcpu());
if (xr.timed_frames)
{
double frames = (double)xr.timed_frames;
char copies[256] = "";
int timed;
for (timed = 0; timed < HALO_XR_LAYER_COUNT; timed++)
{
char item[24];
snprintf(item, sizeof(item), " %d:%.2f", timed, xr.copy_seconds[timed] * 1e3 / frames);
strcat(copies, item);
xr.copy_seconds[timed] = 0.0;
}
host_logf(HOST_LOG_INFO, "xr: per frame ms: copies (layer:ms)%s; of them acquiring images %.2f; "
"releasing them at the end %.2f; xrEndFrame %.2f (copies %s)", copies,
xr.acquire_seconds * 1e3 / frames, xr.release_seconds * 1e3 / frames, xr.end_seconds * 1e3 / frames,
xr.deferred ? "deferred" : "one by one");
xr.acquire_seconds = xr.release_seconds = xr.end_seconds = 0.0;
xr.timed_frames = 0;
}
}
}
end.displayTime = xr.frame_state.predictedDisplayTime;
end.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
end.layerCount = layer_count;
end.layers = layers;
return xr_ok(xrEndFrame(xr.session, &end), "xrEndFrame");
{
double start = xr.has_metrics ? seconds_now() : 0.0;
int ended = xr_ok(xrEndFrame(xr.session, &end), "xrEndFrame");
if (xr.has_metrics)
{
xr.end_seconds += seconds_now() - start;
xr.timed_frames++;
}
return ended;
}
}
void host_xr_set_vignette(float strength)
@@ -1512,6 +2052,34 @@ void host_xr_set_vignette(float strength)
xr.vignette = strength < 0.0f ? 0.0f : strength > 1.0f ? 1.0f : strength;
}
/* the VR options' refresh rates: those the runtime offers (none where it
lets none be chosen, as SteamVR, whose rate is SteamOS's), and the one it
runs at, by XR_FB_display_refresh_rate or else by its frames' period */
int host_xr_refresh_rates(float *rates, int maximum, float *current)
{
float offered[16];
uint32_t count = 0, index;
*current = 0.0f;
if (!xr.initialized)
return 0;
if (xr.has_refresh_rate)
xr.get_refresh_rate(xr.session, current);
if (*current <= 0.0f && xr.frame_state.predictedDisplayPeriod > 0)
*current = 1e9f / (float)xr.frame_state.predictedDisplayPeriod;
if (!xr.has_refresh_rate || !XR_SUCCEEDED(xr.enumerate_refresh_rates(xr.session, 16, &count, offered)))
return 0;
for (index = 0; index < count && (int)index < maximum; index++)
rates[index] = offered[index];
return (int)index;
}
void host_xr_set_refresh_rate(float wanted)
{
if (xr.initialized)
refresh_rate_choose(wanted);
}
void host_xr_haptic(int hand, float amplitude, float seconds)
{
XrHapticActionInfo information = { XR_TYPE_HAPTIC_ACTION_INFO };
+2
View File
@@ -61,6 +61,8 @@ host_xr_end_frame
host_xr_haptic
host_xr_set_vignette
host_xr_copy_layer_array
host_xr_refresh_rates
host_xr_set_refresh_rate
host_android_path
host_errno
+32 -5
View File
@@ -24,7 +24,7 @@ enum
HALO_XR_LAYER_EYE_LEFT = 0,
HALO_XR_LAYER_EYE_RIGHT = 1,
HALO_XR_LAYER_OVERLAY = 2, /* HUD, menus, cinematics: a flat quad */
HALO_XR_LAYER_WRIST = 3, /* the motion tracker, on the left wrist */
HALO_XR_LAYER_WRIST = 3, /* the motion tracker, on the free hand's wrist */
HALO_XR_LAYER_SCOPE = 4, /* the zoomed view, a disc over the weapon */
HALO_XR_LAYER_CROSSHAIR = 5, /* the weapon's crosshair, where it aims */
HALO_XR_LAYER_NAMES = 6, /* players' names, each a quad over a head */
@@ -53,6 +53,29 @@ enum
HALO_XR_BUTTON_TRIGGER = 1 << 4, /* trigger past its click point */
HALO_XR_BUTTON_SQUEEZE = 1 << 5, /* grip past its click point */
HALO_XR_BUTTON_THUMBREST = 1 << 6, /* thumb resting (touch) */
/* the Steam Frame's controllers' (HALO_XR_LAYOUT_FRAME): the left one's
View button and D-pad, either one's bumper (their right one's X, Y and
menu button come as the left hand's A, B and MENU: the game's X, Y and
Start, as on the Quest) */
HALO_XR_BUTTON_VIEW = 1 << 7,
HALO_XR_BUTTON_BUMPER = 1 << 8,
HALO_XR_BUTTON_DPAD_UP = 1 << 9,
HALO_XR_BUTTON_DPAD_DOWN = 1 << 10,
HALO_XR_BUTTON_DPAD_LEFT = 1 << 11,
HALO_XR_BUTTON_DPAD_RIGHT = 1 << 12,
/* the fingers' touch (the hands drawn: vr.c): the thumb on the stick, a
button or the rest; the index finger on the trigger; and whether the
controller tells the thumb's at all */
HALO_XR_BUTTON_THUMB_TOUCH = 1 << 13,
HALO_XR_BUTTON_INDEX_TOUCH = 1 << 14,
HALO_XR_BUTTON_TOUCH_KNOWN = 1 << 15,
};
/* the controllers' layout, for what the game tells the player */
enum
{
HALO_XR_LAYOUT_TOUCH = 0, /* the Quest's: X, Y and menu on the left */
HALO_XR_LAYOUT_FRAME = 1, /* the Steam Frame's: A, B, X, Y and menu on the right, a D-pad on the left */
};
/* halo_xr_hand.flags, halo_xr_view.flags, halo_xr_frame.head_flags */
@@ -112,6 +135,7 @@ struct halo_xr_frame
struct halo_xr_pose head;
struct halo_xr_view views[HALO_XR_EYE_COUNT];
struct halo_xr_hand hands[HALO_XR_HAND_COUNT];
int controller_layout; /* HALO_XR_LAYOUT_* */
};
/* halo_xr_submit.overlay_mode */
@@ -131,15 +155,18 @@ struct halo_xr_submit
float overlay_width; /* metres; the height follows from 4:3 */
/* the screen's placement in LOCAL space (HALO_XR_OVERLAY_SCREEN) */
struct halo_xr_pose screen;
/* the wrist layer was filled: shown on the back of the left wrist,
this wide (metres), while the wrist is turned to the eyes */
/* the wrist layer was filled: shown on the back of the left wrist (of
the right one, left-handed: wrist_hand, a HALO_XR_HAND_*), this wide
(metres), while the wrist is turned to the eyes */
int wrist;
float wrist_size;
/* its placement as the wrist turned to the eyes reads it: turned this
many degrees clockwise, and moved this far right and down (metres) */
/* its placement as the left wrist turned to the eyes reads it: turned
this many degrees clockwise, and moved this far right and down (metres;
on the right wrist, the mirror image) */
float wrist_turn;
float wrist_right;
float wrist_down;
int wrist_hand;
/* the scope layer was filled: a disc this wide (metres), here in LOCAL
space, facing back along the weapon */
int scope;
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+85
View File
@@ -0,0 +1,85 @@
#!/bin/bash
# Halo VR on the Steam Frame: Lepton runs the APK in a container of its own,
# and installs it anew whenever the APK is newer than the installed one
# (port/android/STEAM_FRAME.md). The game's data: ~/Documents/HaloCE.
# (a container of its own, not the shortcut's id: with that, SteamVR's
# client in the container waited on xrCreateInstance)
export SteamAppId="${HALO_LEPTON_APP_ID:-4290000001}"
# (what Steam gives an Android title of its own, and a non-Steam game not:
# the app's data in the container links to the compat data path's
# internal/, a dangling link without it; and the shader paths: Lepton reads
# the dump path against the other)
export STEAM_COMPAT_DATA_PATH="$HOME/.local/share/Steam/steamapps/compatdata/$SteamAppId"
export STEAM_COMPAT_SHADER_PATH="$HOME/.local/share/Steam/steamapps/shadercache/$SteamAppId"
mkdir -p "$STEAM_COMPAT_DATA_PATH/internal" "$STEAM_COMPAT_SHADER_PATH"
unset STEAM_FOSSILIZE_DUMP_PATH LD_PRELOAD
LEPTON="$HOME/.local/share/Steam/steamapps/common/Lepton/lepton"
APK="$HOME/Documents/HaloCE/HaloVR.apk"
LOG="$HOME/Documents/HaloCE/launcher.log"
# (written by the game when the player quits it: no start again)
QUIT="$HOME/Documents/HaloCE/quit_requested.txt"
rm -f "$QUIT"
CONTEXT="steamlaunch-$SteamAppId"
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
log "start (pid $$, parent $PPID)"
# Steam's "Exit game" ends the game's processes as Steam knows them (this
# script, under Steam's reaper), not the container, which podman runs: a
# signal here is passed on to Lepton, and a watcher of its own takes the
# container down once this script is gone (killed)
stopping=0
child=0
stop()
{
log "signal $1"
stopping=1
[ "$child" -gt 0 ] && kill -TERM "$child" 2>/dev/null
}
trap 'stop TERM' TERM
trap 'stop INT' INT
trap 'stop HUP' HUP
script=$$
setsid bash -c '
while kill -0 '"$script"' 2>/dev/null; do sleep 1; done
echo "$(date "+%F %T") watcher: the game was ended, so its container" >> "'"$LOG"'"
"'"$LEPTON"'" kill "'"$CONTEXT"'" >/dev/null 2>&1
' </dev/null >/dev/null 2>&1 &
# the game's log while it runs (Lepton's logcat of its container), for
# looking into problems: logcat.txt, anew each start
LOGCAT="$HOME/Documents/HaloCE/logcat.txt"
: > "$LOGCAT"
setsid bash -c '
while kill -0 '"$script"' 2>/dev/null; do
"'"$LEPTON"'" logcat "'"$CONTEXT"'" 2>/dev/null | grep -v "^ERROR: \|^steamlaunch-\|^$" >> "'"$LOGCAT"'"
sleep 3
done
' </dev/null >/dev/null 2>&1 &
run()
{
"$LEPTON" waitforexitandrun -- "$APK" &
child=$!
# (wait returns early when a signal comes: until Lepton is gone)
while kill -0 "$child" 2>/dev/null; do
wait "$child"
done
child=0
}
# (a new APK is only installed on its first start, not started; and a start
# right after a game that was ended hard can fail in SteamVR, which then
# needs a moment: again, up to three times, while it ends within 30 s)
for attempt in 1 2 3; do
started=$SECONDS
run
if (( stopping || SECONDS - started >= 30 )) || [ -e "$QUIT" ]; then
break
fi
log "ended within $((SECONDS - started)) s: again"
sleep 5
done
rm -f "$QUIT"
log "end"
@@ -0,0 +1,62 @@
/*
FDM_INJECTION_STUB.C
An OpenXR API layer that does nothing, named as the Steam Frame's
XR_APILAYER_VALVE_fdm_injection names its library
(libVkLayer_VALVE_fdm_injection.so). The Frame lists that layer as implicit
for every OpenXR app (/vendor/etc/openxr/1/api_layers/implicit.d), but an
Android app (in Lepton) cannot load the system's library from its own
namespace, and the Khronos loader then fails xrCreateInstance altogether;
nor does the Android loader read the variable that would turn the layer
off. With this one in the app's libraries the loader finds a layer of that
name, which hands every call on to the next. (The real layer adds fragment
density maps to Vulkan apps; this game draws with OpenGL ES.) On a Quest no
manifest names it and it is never loaded.
*/
#include <openxr/openxr.h>
#include <openxr/openxr_loader_negotiation.h>
#include <stddef.h>
static PFN_xrGetInstanceProcAddr next_get_instance_proc_addr;
static XRAPI_ATTR XrResult XRAPI_CALL layer_get_instance_proc_addr(XrInstance instance, const char *name,
PFN_xrVoidFunction *function)
{
if (!next_get_instance_proc_addr)
return XR_ERROR_HANDLE_INVALID;
return next_get_instance_proc_addr(instance, name, function);
}
static XRAPI_ATTR XrResult XRAPI_CALL layer_create_api_layer_instance(const XrInstanceCreateInfo *info,
const XrApiLayerCreateInfo *layer_info, XrInstance *instance)
{
XrApiLayerCreateInfo next_layer_info;
if (!layer_info || !layer_info->nextInfo)
return XR_ERROR_INITIALIZATION_FAILED;
next_get_instance_proc_addr = layer_info->nextInfo->nextGetInstanceProcAddr;
next_layer_info = *layer_info;
next_layer_info.nextInfo = layer_info->nextInfo->next;
return layer_info->nextInfo->nextCreateApiLayerInstance(info, &next_layer_info, instance);
}
__attribute__((visibility("default")))
XRAPI_ATTR XrResult XRAPI_CALL xrNegotiateLoaderApiLayerInterface(const XrNegotiateLoaderInfo *loader_info,
const char *layer_name, XrNegotiateApiLayerRequest *request)
{
(void)layer_name;
if (!loader_info || !request || loader_info->structType != XR_LOADER_INTERFACE_STRUCT_LOADER_INFO ||
request->structType != XR_LOADER_INTERFACE_STRUCT_API_LAYER_REQUEST ||
loader_info->minInterfaceVersion > XR_CURRENT_LOADER_API_LAYER_VERSION ||
loader_info->maxInterfaceVersion < XR_CURRENT_LOADER_API_LAYER_VERSION)
{
return XR_ERROR_INITIALIZATION_FAILED;
}
request->layerInterfaceVersion = XR_CURRENT_LOADER_API_LAYER_VERSION;
request->layerApiVersion = loader_info->maxApiVersion < XR_CURRENT_API_VERSION ? loader_info->maxApiVersion :
XR_CURRENT_API_VERSION;
request->getInstanceProcAddr = layer_get_instance_proc_addr;
request->createApiLayerInstance = layer_create_api_layer_instance;
return XR_SUCCESS;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 KiB

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+2 -2
View File
@@ -314,7 +314,7 @@
"svg": "hud_xbox_order__bitmaps__combined__hud_ammo_meters.svg",
"source_scale": 4,
"source": [
-34,
-2,
108
],
"clip": [
@@ -380,7 +380,7 @@
"svg": "hud_xbox_order__bitmaps__combined__hud_ammo_meters.svg",
"source_scale": 4,
"source": [
-25,
-5,
312
],
"clip": [
@@ -2,7 +2,7 @@
<defs>
<radialGradient id="cone" gradientUnits="userSpaceOnUse" cx="167.994" cy="346.437" r="165.922"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#000"/></radialGradient><radialGradient id="coned" gradientUnits="userSpaceOnUse" cx="167.994" cy="346.437" r="165.922"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#878787"/></radialGradient>
<g id="art">
<path d="M167.994,346.437 L69.695,202.598 L266.292,202.598 Z" fill="url(#cone)"/>
<path d="M167.994,346.437 L54.604,180.515 L281.384,180.515 Z" fill="url(#cone)"/>
<circle cx="167.994" cy="345.987" r="53.201" fill="#878787"/>
<path d="M167.994,346.437 L137.767,302.207 A53.201,53.201 0 0 1 198.220,302.207 Z" fill="url(#coned)"/>
<!-- "15m" pixel label -->

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -2,7 +2,7 @@
<defs>
<radialGradient id="cone" gradientUnits="userSpaceOnUse" cx="126.000" cy="130.342" r="128.840"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#000"/></radialGradient><radialGradient id="coned" gradientUnits="userSpaceOnUse" cx="126.000" cy="130.342" r="128.840"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#878787"/></radialGradient>
<g id="art">
<path d="M126.000,130.342 L49.632,18.677 L202.368,18.677 Z" fill="url(#cone)"/>
<path d="M126.000,130.342 L37.886,1.502 L214.114,1.502 Z" fill="url(#cone)"/>
<circle cx="126.000" cy="129.985" r="41.776" fill="#878787"/>
<path d="M126.000,130.342 L102.251,95.616 A41.776,41.776 0 0 1 149.749,95.616 Z" fill="url(#coned)"/>
<!-- "15m" pixel label -->

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

+1 -1
View File
@@ -51,7 +51,7 @@ until it is redrawn.
| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` |
| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` |
13 of 147 frames are placeholders.
13 of 156 frames are placeholders.
The PC version's `ui\gamespy` and `ui\ticker` fonts are drawn with `ui\small_ui`, which
the Xbox's map has.
+3 -4
View File
@@ -10,11 +10,10 @@ do (save a setting, join a game) does not happen; the screens open and close as
| Function | Uses | |
| --- | --- | --- |
| `common button bar update` | 24 | PC function |
| `common button bar update` | 25 | PC function |
| `direct ip connect init` | 1 | PC function |
| `direct ip connect update` | 1 | PC function |
| `direct ip edit field` | 4 | PC function |
| `gamespy screen dispose` | 1 | PC function |
| `gamespy screen update` | 1 | PC function |
| `gamespy select button` | 8 | PC function |
| `gamespy select header` | 22 | PC function |
@@ -22,7 +21,7 @@ do (save a setting, join a game) does not happen; the screens open and close as
| `gamespy update filter settings` | 2 | PC function |
| `gt edit list update` | 1 | PC function |
| `gt select list update` | 1 | PC function |
| `mouse spinner 1wide click` | 64 | PC function |
| `mouse spinner 1wide click` | 66 | PC function |
| `mp map list update` | 1 | PC function |
| `mp prof init teamplay options` | 1 | PC function |
| `mp prof init vehicle options` | 1 | PC function |
@@ -34,4 +33,4 @@ do (save a setting, join a game) does not happen; the screens open and close as
| `ss edit server name` | 2 | PC function |
| `ss start game` | 2 | PC function |
Wired: `campaign menu continue`, `campaign menu init`, `controls back handler`, `controls begin binding`, `controls screen change set`, `controls screen defaults`, `controls screen init`, `controls update menu`, `difficulty item select`, `direct ip connect go`, `emit custom activation event`, `gamespy back handler`, `gamespy dismiss error`, `gamespy dismiss filters`, `gamespy screen init`, `load game list update`, `load game menu activated`, `load game menu delete finish`, `load game menu delete request`, `load game menu dispose`, `load game menu init`, `main menu quit game`, `mouse emit accept event`, `mouse emit back event`, `mouse emit x event`, `mp type set mode`, `profile manager select`, `profile set edit begin`, `single prev cl item activated`, `solo map list update`.
Wired: `campaign menu continue`, `campaign menu init`, `controls back handler`, `controls begin binding`, `controls screen change set`, `controls screen defaults`, `controls screen init`, `controls update menu`, `difficulty item select`, `direct ip connect go`, `emit custom activation event`, `gamespy back handler`, `gamespy dismiss error`, `gamespy dismiss filters`, `gamespy screen dispose`, `gamespy screen init`, `load game list update`, `load game menu activated`, `load game menu delete finish`, `load game menu delete request`, `load game menu dispose`, `load game menu init`, `main menu quit game`, `mouse emit accept event`, `mouse emit back event`, `mouse emit x event`, `mp type set mode`, `profile manager select`, `profile set edit begin`, `single prev cl item activated`, `solo map list update`.
+24
View File
@@ -309,13 +309,37 @@
<bitmap name="main_menu/solo_level_select/header_load_level">
<frame png="ce/shell/main_menu/solo_level_select/header_load_level__0.png" width="512" height="64"/>
</bitmap>
<bitmap name="main_menu/multiplayer_type_select/coop/header_player_2">
<frame png="ce/port/main_menu/multiplayer_type_select/coop/header_player_2.png" width="512" height="64"/>
</bitmap>
<bitmap name="main_menu/multiplayer_type_select/join_game/header_direct_link">
<frame png="ce/port/main_menu/multiplayer_type_select/join_game/header_direct_link.png" width="512" height="64"/>
</bitmap>
<bitmap name="main_menu/multiplayer_type_select/join_game/header_password">
<frame png="ce/port/main_menu/multiplayer_type_select/join_game/header_password.png" width="512" height="64"/>
</bitmap>
<bitmap name="main_menu/multiplayer_type_select/join_game/header_server_browser">
<frame png="ce/port/main_menu/multiplayer_type_select/join_game/header_server_browser.png" width="512" height="64"/>
</bitmap>
<bitmap name="main_menu/multiplayer_type_select/lobby/header_add_player">
<frame png="ce/port/main_menu/multiplayer_type_select/lobby/header_add_player.png" width="512" height="64"/>
</bitmap>
<bitmap name="main_menu/multiplayer_type_select/lobby/header_lobby">
<frame png="ce/port/main_menu/multiplayer_type_select/lobby/header_lobby.png" width="512" height="64"/>
</bitmap>
<bitmap name="pause/pausebox_left">
<frame png="ce/port/pause/pausebox_left__0.png" width="16" height="256"/>
<frame png="ce/port/pause/pausebox_left__1.png" width="16" height="256"/>
</bitmap>
<bitmap name="pause/pausebox_center">
<frame png="ce/port/pause/pausebox_center__0.png" width="4" height="256"/>
<frame png="ce/port/pause/pausebox_center__1.png" width="4" height="256"/>
</bitmap>
<bitmap name="pause/pausebox_right">
<frame png="ce/port/pause/pausebox_right__0.png" width="16" height="256"/>
<frame png="ce/port/pause/pausebox_right__1.png" width="16" height="256"/>
</bitmap>
<bitmap name="blank">
<frame png="ce/port/blank__0.png" width="4" height="4"/>
</bitmap>
</menus>
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The port's multiplayer screens, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/multiplayer_type_select/multiplayer_type_coop_item" type="text" left="51" width="232" height="32" bitmap="bitmaps/list_item_bkd" string_list="main_menu/multiplayer_type_select/multiplayer_options" string_index="7" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="5">
<on event="a" run="port coop begin" open="main_menu/multiplayer_type_select/coop/player_2_profile_screen" branch="true"/>
<on event="start" run="port coop begin" open="main_menu/multiplayer_type_select/coop/player_2_profile_screen" branch="true"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/player_2_profile_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
<child widget="main_menu/new_select/sel_list_desc_bkd"/>
<child widget="main_menu/multiplayer_type_select/coop/player_2_profile_list"/>
<child widget="main_menu/multiplayer_type_select/coop/header_player_2"/>
<child widget="main_menu/multiplayer_type_select/coop/player_2_help" x="20" y="416"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/header_player_2" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/coop/header_player_2">
</widget>
<widget name="main_menu/multiplayer_type_select/coop/player_2_profile_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/profile_manager/player_profile_extended_desc">
<data input="3wide player profile list update"/>
<on event="created" run="port coop player 2 list initialize"/>
<on event="deleted" run="player profile list dispose"/>
<on event="custom_activation" run="port coop player 2" open="main_menu/solo_level_select/solo_level_select_screen" branch="true"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_0" x="20" y="73"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_1" x="20" y="103"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_2" x="20" y="133"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_3" x="20" y="163"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_4" x="20" y="193"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_5" x="20" y="223"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_6" x="20" y="253"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_7" x="20" y="283"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_8" x="20" y="313"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_9" x="20" y="343"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_10" x="20" y="373"/>
<child widget="main_menu/multiplayer_type_select/coop/player_2_button_bar" y="414"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_0" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd_top" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_1" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_2" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_3" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_4" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_5" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_6" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_7" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_8" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_9" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_10" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/player_2_help" type="text" width="350" height="24" text="Player 2: choose with your controller." font="ui\small_ui" color="#FF2896FF" text_y="5" text_flags="no_focus_test">
</widget>
<widget name="main_menu/multiplayer_type_select/coop/player_2_button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<data input="common button bar update"/>
<child widget="main_menu/profile_manager/profile_manager_button_ok" x="380" y="1"/>
<child widget="common_button_back" x="510" y="1"/>
</widget>
</menus>
@@ -1,6 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The port's multiplayer screens, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/multiplayer_type_select/join_game/password/password_screen" width="640" height="480" flags="pass_unhandled_to_focused_child pause_game" bitmap="bitmaps/gradient">
<on event="b" run="port password back"/>
<on event="back" run="port password back"/>
<child widget="main_menu/multiplayer_type_select/join_game/password/header_password"/>
<child widget="main_menu/multiplayer_type_select/join_game/password/options_menu"/>
</widget>
<widget name="main_menu/multiplayer_type_select/join_game/password/header_password" controller="1" left="35" top="11" width="605" height="59" bitmap="main_menu/multiplayer_type_select/join_game/header_password">
</widget>
<widget name="main_menu/multiplayer_type_select/join_game/password/help" type="text" controller="1" left="68" top="120" width="482" height="60" string_list="main_menu/multiplayer_type_select/join_game/password/help_strings" font="ui\large_ui" color="#FFFFFFFF">
</widget>
<widget name="main_menu/multiplayer_type_select/join_game/password/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/multiplayer_type_select/join_game/password/help">
<data input="port password update"/>
<on event="created" run="port password init"/>
<child widget="main_menu/multiplayer_type_select/join_game/password/op_password" x="54" y="73"/>
<child widget="main_menu/multiplayer_type_select/join_game/password/button_bar" y="414"/>
</widget>
<widget name="main_menu/multiplayer_type_select/join_game/password/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<child widget="main_menu/multiplayer_type_select/join_game/password/button_ok" x="380" y="1"/>
<child widget="common_button_cancel" x="510" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/join_game/password/op_password" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<on event="a" run="port password edit"/>
<on event="start" run="port password edit"/>
<on event="b"/>
<on event="back"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/multiplayer_type_select/join_game/password/password_label"/>
<child widget="main_menu/multiplayer_type_select/join_game/password/password_value" x="230" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/join_game/password/password_label" type="text" controller="1" width="230" height="22" string_list="main_menu/multiplayer_type_select/join_game/password/labels" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/join_game/password/password_value" type="text" controller="1" width="280" height="22" font="ui\small_ui" color="#FF2896FF" align="center" text_y="5" text_flags="no_focus_test">
</widget>
<widget name="main_menu/multiplayer_type_select/join_game/password/button_defaults" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="3" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/multiplayer_type_select/join_game/password/button_ok" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="JOIN GAME" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port password join"/>
<on event="start" run="port password join"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<strings name="main_menu/multiplayer_type_select/join_game/password/labels">
<string text="PASSWORD:"/>
</strings>
<strings name="main_menu/multiplayer_type_select/join_game/password/help_strings">
<string text=""/>
<string text="This game has a password. Type it, then press\nEnter to join."/>
<string text="That is not the game's password. Try again."/>
<string text="Could not join the game."/>
</strings>
<widget name="main_menu/multiplayer_type_select/join_game/header_server_browser" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/join_game/header_server_browser">
</widget>
<widget name="main_menu/multiplayer_type_select/join_game/header_direct_link" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/join_game/header_direct_link">
@@ -39,7 +39,7 @@
<child widget="main_menu/multiplayer_type_select/join_game/ticker_rules_info" x="80"/>
</widget>
<widget name="main_menu/multiplayer_type_select/join_game/join_game_button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<child widget="main_menu/multiplayer_type_select/join_game/join_game_button_update" y="1"/>
<child widget="main_menu/multiplayer_type_select/join_game/join_game_button_refresh" y="1"/>
<child widget="main_menu/multiplayer_type_select/join_game/button_clipboard" x="128" y="1"/>
<child widget="main_menu/multiplayer_type_select/join_game/join_game_button_filters" x="256" y="1"/>
<child widget="main_menu/multiplayer_type_select/join_game/join_game_button_join" x="384" y="1"/>
@@ -1,34 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The port's multiplayer screens, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
<widget name="main_menu/multiplayer_type_select/lobby/lobby_screen" width="640" height="480" flags="pass_unhandled_to_focused_child main_menu_if_no_history" bitmap="bitmaps/gradient">
<on event="created" run="port lobby open"/>
<on event="created" run="net server accept conx"/>
<on event="created" run="net server allow start"/>
<on event="b" run="net game unjoin player" back="true"/>
<on event="back" run="net game unjoin player" back="true"/>
<on event="b" run="port lobby leave" back="true"/>
<on event="back" run="port lobby leave" back="true"/>
<on event="start" run="port lobby join" open="main_menu/multiplayer_type_select/lobby/player_profile_screen" branch="true"/>
<child widget="main_menu/new_select/sel_list_desc_bkd"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_list"/>
<child widget="main_menu/multiplayer_type_select/lobby/header_lobby"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_join_help"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/header_lobby" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/lobby/header_lobby">
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_join_help" type="text" controller="1" left="355" top="446" width="275" height="24" font="ui\small_ui" color="#FF2896FF" align="right" text_y="5" text_flags="no_focus_test">
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/multiplayer_type_select/lobby/lobby_desc">
<data input="net splitscreen prejoin players"/>
<data input="port lobby update"/>
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_0" x="20" y="73"/>
<child widget="main_menu/new_select/list_item_1" x="20" y="103"/>
<child widget="main_menu/new_select/list_item_2" x="20" y="133"/>
<child widget="main_menu/new_select/list_item_3" x="20" y="163"/>
<child widget="main_menu/new_select/list_item_4" x="20" y="193"/>
<child widget="main_menu/new_select/list_item_5" x="20" y="223"/>
<child widget="main_menu/new_select/list_item_6" x="20" y="253"/>
<child widget="main_menu/new_select/list_item_7" x="20" y="283"/>
<child widget="main_menu/new_select/list_item_8" x="20" y="313"/>
<child widget="main_menu/new_select/list_item_9" x="20" y="343"/>
<child widget="main_menu/new_select/list_item_10" x="20" y="373"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_0" x="20" y="73"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_1" x="20" y="103"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_2" x="20" y="133"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_3" x="20" y="163"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_4" x="20" y="193"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_5" x="20" y="223"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_6" x="20" y="253"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_7" x="20" y="283"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_8" x="20" y="313"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_9" x="20" y="343"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_10" x="20" y="373"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_bar" y="414"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_0" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd_top" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_1" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_2" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_3" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_4" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_5" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_6" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_7" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_8" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_9" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_10" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_desc" width="640" height="480">
<child widget="main_menu/current_profile_name"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_right_item" x="22" y="2"/>
@@ -36,6 +95,8 @@
<widget name="main_menu/multiplayer_type_select/lobby/lobby_right_item" controller="1" left="406" top="75" width="162" height="326" bitmap="bitmaps/spinner_list_3_wide_item_background">
<child widget="main_menu/multiplayer_type_select/lobby/lobby_map_pic"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_map_name"/>
<child widget="main_menu/solo_level_select/replay_level_right_pic"/>
<child widget="main_menu/solo_level_select/replay_level_right_name"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_game_data"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_map_pic" controller="1" left="419" top="87" width="140" height="114" bitmap="ui\shell\bitmaps\mp_map_grafix">
@@ -45,10 +106,16 @@
<widget name="main_menu/multiplayer_type_select/lobby/lobby_game_data" type="text" controller="1" left="417" top="250" width="146" height="144" font="ui\small_ui" color="#FF2896FF">
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_team" x="250" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_start" x="380" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_team" x="120" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_start" x="250" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_add" x="380" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_leave" x="510" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_button_add" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="ADD PLAYER" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port lobby add player"/>
<on event="start" run="port lobby add player"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_button_team" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="SWITCH TEAM" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="swap player team"/>
<on event="start" run="swap player team"/>
@@ -64,14 +131,46 @@
<on event="start" run="mouse emit back event"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/player_profile_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
<on event="created" run="net server defer start"/>
<child widget="main_menu/new_select/sel_list_desc_bkd"/>
<child widget="main_menu/multiplayer_type_select/lobby/player_profile_list"/>
<child widget="main_menu/multiplayer_type_select/lobby/header_add_player"/>
<child widget="main_menu/multiplayer_type_select/lobby/player_profile_help" x="20" y="416"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/header_add_player" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/lobby/header_add_player">
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/player_profile_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/profile_manager/player_profile_extended_desc">
<data input="3wide player profile list update"/>
<on event="created" run="port lobby player list initialize"/>
<on event="deleted" run="player profile list dispose"/>
<on event="custom_activation" run="port lobby player choose" back="true" branch="true"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_0" x="20" y="73"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_1" x="20" y="103"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_2" x="20" y="133"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_3" x="20" y="163"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_4" x="20" y="193"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_5" x="20" y="223"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_6" x="20" y="253"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_7" x="20" y="283"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_8" x="20" y="313"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_9" x="20" y="343"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_10" x="20" y="373"/>
<child widget="main_menu/multiplayer_type_select/coop/player_2_button_bar" y="414"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/player_profile_help" type="text" width="350" height="24" text="The new player's profile." font="ui\small_ui" color="#FF2896FF" text_y="5" text_flags="no_focus_test">
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/preview_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
<on event="b" back="true"/>
<on event="back" back="true"/>
<on event="created" run="port lobby open"/>
<on event="b" run="port lobby preview leave" back="true"/>
<on event="back" run="port lobby preview leave" back="true"/>
<on event="start" run="port lobby preview add" open="main_menu/multiplayer_type_select/lobby/player_profile_screen" branch="true"/>
<child widget="main_menu/new_select/sel_list_desc_bkd"/>
<child widget="main_menu/multiplayer_type_select/lobby/preview_list"/>
<child widget="main_menu/multiplayer_type_select/lobby/header_lobby"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_join_help"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/preview_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child left_right_tabs_items" description="main_menu/multiplayer_type_select/lobby/lobby_desc">
<widget name="main_menu/multiplayer_type_select/lobby/preview_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/multiplayer_type_select/lobby/lobby_desc">
<data input="port lobby preview update"/>
<child widget="main_menu/multiplayer_type_select/lobby/preview_status" x="30" y="75"/>
<child widget="main_menu/multiplayer_type_select/lobby/preview_button_bar" y="414"/>
@@ -79,7 +178,8 @@
<widget name="main_menu/multiplayer_type_select/lobby/preview_status" type="text" controller="1" width="360" height="300" font="ui\large_ui" color="#FF2896FF">
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/preview_button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<child widget="main_menu/multiplayer_type_select/lobby/preview_button_join" x="380" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/preview_button_join" x="250" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/preview_button_add" x="380" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/preview_button_back" x="510" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/preview_button_join" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="JOIN GAME" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
@@ -87,6 +187,11 @@
<on event="start" run="port lobby preview join"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/preview_button_add" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="ADD PLAYER" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port lobby add player"/>
<on event="start" run="port lobby add player"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/preview_button_back" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="BACK" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="mouse emit back event"/>
<on event="start" run="mouse emit back event"/>
@@ -13,6 +13,9 @@
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_name"/>
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_pic"/>
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_data"/>
<child widget="main_menu/solo_level_select/replay_level_right_name"/>
<child widget="main_menu/solo_level_select/replay_level_right_pic"/>
<child widget="main_menu/solo_level_select/replay_level_right_data"/>
</widget>
<widget name="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_name" type="text" controller="1" left="417" top="204" width="146" height="43" string_list="main_menu/mp_map_list" string_index="2" font="ui\large_ui" color="#FF2896FF">
</widget>
@@ -33,7 +36,9 @@
<on event="created" run="mp level list initialize"/>
<on event="deleted" run="mp level list dispose"/>
<on event="custom_activation" run="mp level select" open="main_menu/multiplayer_type_select/connected/gametype_select_screen_wrapper"/>
<child widget="main_menu/new_select/list_item_0" x="20" y="73"/>
<on event="b" run="port map list back"/>
<on event="back" run="port map list back"/>
<child widget="main_menu/new_select/list_item_0_map_kind" x="82" y="73"/>
<child widget="main_menu/new_select/list_item_1" x="20" y="103"/>
<child widget="main_menu/new_select/list_item_2" x="20" y="133"/>
<child widget="main_menu/new_select/list_item_3" x="20" y="163"/>
@@ -9,20 +9,27 @@
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/header_server_settings" controller="1" left="35" top="11" width="605" height="59" bitmap="main_menu/multiplayer_type_select/server_settings/header_server_settings">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/help" type="text" controller="1" left="68" top="350" width="482" height="60" string_list="main_menu/multiplayer_type_select/server_settings/help_strings" font="ui\large_ui" color="#FFFFFFFF">
<widget name="main_menu/multiplayer_type_select/server_settings/help" type="text" controller="1" left="68" top="366" width="482" height="60" string_list="main_menu/multiplayer_type_select/server_settings/help_strings" font="ui\large_ui" color="#FFFFFFFF">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/multiplayer_type_select/server_settings/help">
<data input="server settings update"/>
<on event="created" run="server settings init"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_server_name" x="54" y="73"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_max_players" x="54" y="103"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_invite" x="54" y="133"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_game_type" x="54" y="163"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_player_options" x="54" y="193"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_item_options" x="54" y="223"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_vehicle_options" x="54" y="253"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_indicator_options" x="54" y="283"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_team_options" x="54" y="313"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_max_players" x="54" y="99"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_invite" x="54" y="125"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_listing" x="54" y="151"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_password" x="54" y="177"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_friendly_fire" x="54" y="203"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_extra_enemies" x="54" y="229"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_enemies_per_player" x="54" y="255"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_enemies_multiplier" x="54" y="255"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_player_collisions" x="54" y="281"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_game_type" x="54" y="203"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_player_options" x="54" y="229"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_item_options" x="54" y="255"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_vehicle_options" x="54" y="281"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_indicator_options" x="54" y="307"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_team_options" x="54" y="333"/>
<child widget="main_menu/multiplayer_type_select/server_settings/button_bar" y="414"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
@@ -64,6 +71,83 @@
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/invite_value" type="text" controller="1" width="280" height="22" font="ui\small_ui" color="#FF2896FF" align="center" text_y="5" text_flags="no_focus_test">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_listing" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/listing_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/listing_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/listing_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="9" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/listing_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="PUBLIC|PRIVATE" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_password" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<on event="a" run="ss edit server password"/>
<on event="start" run="ss edit server password"/>
<on event="b"/>
<on event="back"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/multiplayer_type_select/server_settings/password_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/password_value" x="230" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/password_label" type="text" controller="1" width="230" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="15" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/password_value" type="text" controller="1" width="280" height="22" font="ui\small_ui" color="#FF2896FF" align="center" text_y="5" text_flags="no_focus_test">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_friendly_fire" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/friendly_fire_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/friendly_fire_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/friendly_fire_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="10" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/friendly_fire_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|ON|SHIELD ONLY|EXPLOSIVES ONLY" setting="network.coop_friendly_fire" values="off|on|shields_only|explosives_only" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
<on event="deleted" run="port setting save"/>
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_extra_enemies" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/extra_enemies_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/extra_enemies_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/extra_enemies_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="11" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/extra_enemies_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="NONE|PER PLAYER|STATIC MULTIPLIER" setting="network.coop_enemies_mode" values="none|per_player|multiplier" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
<on event="deleted" run="port setting save"/>
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_enemies_per_player" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/enemies_per_player_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/enemies_per_player_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_per_player_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="12" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_per_player_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="25%|50%|100%|150%|200%" setting="network.coop_enemies" values="25|50|100|150|200" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
<on event="deleted" run="port setting save"/>
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_enemies_multiplier" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/enemies_multiplier_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/enemies_multiplier_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_multiplier_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="13" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_multiplier_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="2X|4X|8X|16X|32X" setting="network.coop_enemies_multiplier" values="2|4|8|16|32" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
<on event="deleted" run="port setting save"/>
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_player_collisions" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/player_collisions_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/player_collisions_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/player_collisions_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="14" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/player_collisions_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="network.coop_player_collisions" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
<on event="deleted" run="port setting save"/>
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_game_type" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<on event="a" run="port setup edit" open="main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/gametype_select_screen"/>
<on event="start" run="port setup edit" open="main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/gametype_select_screen"/>
@@ -148,6 +232,13 @@
<string text="VEHICLE OPTIONS:"/>
<string text="INDICATOR OPTIONS:"/>
<string text="TEAMPLAY OPTIONS:"/>
<string text="LISTING:"/>
<string text="FRIENDLY FIRE:"/>
<string text="EXTRA ENEMIES:"/>
<string text="PER PLAYER:"/>
<string text="MULTIPLIER:"/>
<string text="PLAYER COLLISIONS:"/>
<string text="PASSWORD:"/>
</strings>
<strings name="main_menu/multiplayer_type_select/server_settings/help_strings">
<string text=""/>
@@ -160,5 +251,19 @@
<string text="Each team's vehicles and their respawn time, for\nthis game."/>
<string text="The motion tracker and nav points, for this game."/>
<string text="Friendly fire and team balance, for this game."/>
<string text="Anyone can see and join your game: it is listed\nin everyone's Server Browser."/>
<string text="Only players with your invite link can join."/>
<string text="Players can not be hurt by weapons and explosives\nfired by the other players."/>
<string text="Players can be hurt by weapons or explosives\nfired by the other players."/>
<string text="Damage from the other players will only reduce\nshields. Health will be unaffected."/>
<string text="Players can be hurt by damage from explosives\nfired by the other players."/>
<string text="Enemy squads are as the campaign has them."/>
<string text="Enemy squads grow with the players: by the amount\nbelow for each player past the first."/>
<string text="Enemy squads are the amount below times as large,\nhowever many players there are."/>
<string text="For each player past the first, enemy squads get\nthis much more of themselves (100%: as many again)."/>
<string text="Each enemy squad is this many times as large."/>
<string text="Players bump into each other, as in the campaign."/>
<string text="Players walk through each other, so that no one\nblocks a doorway. The AI's characters still block."/>
<string text="Players from the Server Browser must type it to\njoin (an invite link needs none). Enter sets it."/>
</strings>
</menus>
@@ -59,13 +59,14 @@
<child widget="main_menu/multiplayer_type_select/multiplayer_type_create_item" x="-32" y="210"/>
<child widget="main_menu/multiplayer_type_select/multiplayer_type_create_internet_item" y="243"/>
<child widget="main_menu/multiplayer_type_select/multiplayer_type_create_lan_item" y="276"/>
<child widget="main_menu/multiplayer_type_select/multiplayer_type_gametypes_item" y="342"/>
<child widget="main_menu/multiplayer_type_select/multiplayer_type_coop_item" y="309"/>
<child widget="main_menu/multiplayer_type_select/multiplayer_type_gametypes_item" y="375"/>
<child widget="main_menu/multiplayer_type_select/multiplayer_type_button_bar" y="414"/>
</widget>
<widget name="main_menu/multiplayer_type_select/multiplayer_type_select_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
<on event="created" run="mp type menu initialize"/>
<child widget="main_menu/multiplayer_type_select/header_multiplayer"/>
<child widget="main_menu/multiplayer_type_select/multiplayer_type_select_list"/>
<child widget="main_menu/blueline" x="64" y="325"/>
<child widget="main_menu/blueline" x="64" y="358"/>
</widget>
</menus>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The port's multiplayer screens, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/new_select/list_item_0_map_kind" width="256" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds_sm" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<child widget="main_menu/new_select/list_item_0_map_kind_spinner" x="15" y="2"/>
</widget>
<widget name="main_menu/new_select/list_item_0_map_kind_spinner" type="spinner" width="226" height="22" flags="left_right_tabs_items" string_list="main_menu/new_select/var_map_kinds" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 226 19 232">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/new_select/var_map_kinds">
<string text="SINGLEPLAYER"/>
<string text="MULTIPLAYER"/>
</strings>
</menus>
@@ -1,6 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The port's multiplayer screens, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/op_map_weapons" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/map_weapons_label"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/map_weapons_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/map_weapons_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/multiplayer_setup/item_options_edit/item_options_labels" string_index="8" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/map_weapons_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/settings_select/multiplayer_setup/item_options_edit/var_map_weapons" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/op_loadout" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/loadout_label"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/loadout_spinner" x="286" y="1"/>
@@ -28,6 +37,10 @@
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/secondary_weapon_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/settings_select/multiplayer_setup/item_options_edit/var_loadout_weapon" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/settings_select/multiplayer_setup/item_options_edit/var_map_weapons">
<string text="YES"/>
<string text="NO"/>
</strings>
<strings name="main_menu/settings_select/multiplayer_setup/item_options_edit/var_loadout">
<string text="CATEGORY"/>
<string text="CUSTOM"/>
@@ -43,7 +56,5 @@
<string text="PLASMA PISTOL"/>
<string text="PLASMA RIFLE"/>
<string text="NEEDLER"/>
<string text="FUEL ROD"/>
<string text="FLAMETHROWER"/>
</strings>
</menus>
@@ -26,9 +26,10 @@
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_infinite_grenades" x="54" y="73"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_weapon_set" x="54" y="103"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_starting_equipment" x="54" y="133"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_loadout" x="54" y="163"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_primary_weapon" x="54" y="193"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_secondary_weapon" x="54" y="223"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_map_weapons" x="54" y="163"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_loadout" x="54" y="193"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_primary_weapon" x="54" y="223"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_secondary_weapon" x="54" y="253"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/item_button_bar" y="414"/>
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/item_options_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
@@ -16,7 +16,8 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_volume" x="54" y="73"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_music_volume" x="54" y="103"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_effects_volume" x="54" y="133"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_enabled" x="54" y="163"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_reverb" x="54" y="163"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_enabled" x="54" y="193"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/button_bar" y="414"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
@@ -51,11 +52,20 @@
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/effects_volume_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="0|1|2|3|4|5|6|7|8|9|10" setting="audio.effects_volume" values="0|0.1|0.2|0.3|0.4|0.5|0.6|0.7|0.8|0.9|1" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_reverb" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/reverb_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/reverb_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/reverb_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/reverb_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="audio.reverb" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_enabled" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="4" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="audio.enabled" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -74,6 +84,7 @@
<string text="MASTER VOLUME:"/>
<string text="MUSIC VOLUME:"/>
<string text="EFFECTS VOLUME:"/>
<string text="REVERB:"/>
<string text="SOUND:"/>
</strings>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/help_strings">
@@ -81,6 +92,7 @@
<string text="The volume of everything."/>
<string text="The music's volume."/>
<string text="The volume of every other sound: effects and\nspeech."/>
<string text="Echo sounds as the place you are in does, and\nmuffle those behind walls, as the Xbox did."/>
<string text="Play sound at all; from the next time the game\nstarts."/>
</strings>
</menus>
@@ -9,18 +9,24 @@
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/header_profile_video_settings" controller="1" left="35" top="11" width="605" height="59" bitmap="main_menu/settings_select/player_setup/player_profile_edit/video_settings/header_profile_video_settings">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help" type="text" controller="1" left="68" top="350" width="482" height="60" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help_strings" font="ui\large_ui" color="#FFFFFFFF">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help" type="text" controller="1" left="68" top="364" width="482" height="60" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help_strings" font="ui\large_ui" color="#FFFFFFFF">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help">
<data input="port settings help"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_mode" x="54" y="73" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_window_scale" x="54" y="99" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_vsync" x="54" y="125"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_max_fps" x="54" y="151" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_interpolation" x="54" y="177"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_direct_camera" x="54" y="203" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_hud" x="54" y="229"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_text" x="54" y="255"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution" x="54" y="97" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_window_size" x="54" y="97" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution_scaling" x="54" y="121" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_vsync" x="54" y="145"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_max_fps" x="54" y="169" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_interpolation" x="54" y="193"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_direct_camera" x="54" y="217" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_hud" x="54" y="241"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_text" x="54" y="265"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_anti_aliasing" x="54" y="289" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_anti_aliasing_android" x="54" y="289" platform="android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_shadow_resolution" x="54" y="313"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_per_pixel_lighting" x="54" y="337"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/button_bar" y="414"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
@@ -37,20 +43,38 @@
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/mode_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="FULLSCREEN|BORDERLESS|WINDOWED" setting="display.mode" values="fullscreen|borderless|windowed" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_window_scale" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_scale_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_scale_spinner" x="320" y="1"/>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_scale_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="1" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="1" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_scale_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="640 x 480|1280 x 960|1920 x 1440|2560 x 1920" setting="display.window_scale" values="1|2|3|4" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="NATIVE" setting="display.resolution" values="native" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_window_size" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_size_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_size_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_size_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="2" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_size_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="1280 x 960" setting="display.window_size" values="1280x960" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution_scaling" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_scaling_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_scaling_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_scaling_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_scaling_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="NATIVE|ORIGINAL" setting="display.resolution_scaling" values="native|original" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_vsync" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/vsync_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/vsync_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/vsync_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="2" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/vsync_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="4" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/vsync_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.vsync" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -59,7 +83,7 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/max_fps_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/max_fps_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/max_fps_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/max_fps_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="5" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/max_fps_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="AUTO|30|60|120|144|165|240|NONE" setting="display.max_fps" values="0|30|60|120|144|165|240|-1" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -68,7 +92,7 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/interpolation_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/interpolation_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/interpolation_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="4" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/interpolation_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="6" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/interpolation_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.interpolation" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -77,7 +101,7 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/direct_camera_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/direct_camera_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/direct_camera_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="5" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/direct_camera_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="7" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/direct_camera_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.direct_camera" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -86,7 +110,7 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="6" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="8" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.high_res_hud" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -95,11 +119,47 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="7" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="9" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.high_res_text" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_anti_aliasing" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="10" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|FXAA|SMAA|SSAA 2X|MSAA 2X|MSAA 4X|MSAA 8X" setting="display.anti_aliasing" values="off|fxaa|smaa|ssaa2x|msaa2x|msaa4x|msaa8x" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_anti_aliasing_android" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="android">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_android_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_android_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_android_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="11" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_android_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|FXAA|MSAA 2X|MSAA 4X" setting="display.anti_aliasing" values="off|fxaa|msaa2x|msaa4x" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_shadow_resolution" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/shadow_resolution_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/shadow_resolution_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/shadow_resolution_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="12" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/shadow_resolution_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="128|256|512|1024" setting="display.shadow_resolution" values="128|256|512|1024" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_per_pixel_lighting" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/per_pixel_lighting_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/per_pixel_lighting_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/per_pixel_lighting_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="13" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/per_pixel_lighting_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.per_pixel_lighting" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/button_defaults" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="3" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port settings defaults"/>
<on event="start" run="port settings defaults"/>
@@ -112,23 +172,35 @@
</widget>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels">
<string text="DISPLAY MODE:"/>
<string text="RESOLUTION:"/>
<string text="WINDOW SIZE:"/>
<string text="RESOLUTION SCALING:"/>
<string text="V-SYNC:"/>
<string text="FRAME RATE LIMIT:"/>
<string text="SMOOTH MOTION:"/>
<string text="INSTANT AIM:"/>
<string text="HIGH-RES HUD:"/>
<string text="HIGH-RES TEXT:"/>
<string text="ANTI-ALIASING:"/>
<string text="ANTI-ALIASING:"/>
<string text="SHADOW RESOLUTION:"/>
<string text="PER-PIXEL LIGHTING:"/>
</strings>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help_strings">
<string text=""/>
<string text="Fullscreen and borderless draw at the display's\nresolution; windowed, 640x480 scaled. F11: window."/>
<string text="The window's size when windowed (its edges can\nalso be dragged)."/>
<string text="Fullscreen takes the display; borderless covers it\nwith a window. F11 switches to the window."/>
<string text="What fullscreen and borderless draw at. Fullscreen\nsets the display to it; borderless scales it."/>
<string text="The window's size: 4:3, then 16:10, 16:9 and 21:9\n(its edges can also be dragged)."/>
<string text="Native draws at the resolution; Original draws\nthe Xbox's 640x480 and scales it up."/>
<string text="Wait for the display between frames, so that the\npicture never tears."/>
<string text="With V-Sync off, the most frames a second. Auto:\ntwice the display's refresh rate."/>
<string text="Draw a frame for every display refresh, blending\nbetween the game's 30 ticks a second."/>
<string text="In first person, turn the view the moment the\nmouse moves, not up to two ticks later."/>
<string text="Draw the HUD from the high-res redraws; off\ndraws the game's own pictures."/>
<string text="Draw text and titles with high-res fonts; off\ndraws the game's own."/>
<string text="Smooth jagged edges, which the Xbox did not. FXAA\nand SMAA are cheap; SSAA and MSAA are sharper."/>
<string text="Smooth jagged edges, which the Xbox did not. FXAA\nis cheap; MSAA is sharper."/>
<string text="The size objects' shadows are drawn at: 128 as on\nthe Xbox; larger for smoother, as soft, edges."/>
<string text="Light models for each pixel, without the facets\nof the Xbox's lighting for each vertex."/>
</strings>
</menus>
@@ -14,6 +14,9 @@
<child widget="main_menu/solo_level_select/difficulty_indicator" x="416" y="343"/>
<child widget="main_menu/solo_level_select/difficulty_indicator" x="466" y="343"/>
<child widget="main_menu/solo_level_select/difficulty_indicator" x="516" y="343"/>
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_name"/>
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_pic"/>
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_data"/>
</widget>
<widget name="main_menu/solo_level_select/replay_level_right_name" type="text" controller="1" left="417" top="204" width="146" height="43" string_list="main_menu/map_list" string_index="2" font="ui\large_ui" color="#FF2896FF">
</widget>
@@ -38,7 +41,7 @@
<on event="created" run="initialize sp level list solo"/>
<on event="deleted" run="dispose sp level list"/>
<on event="custom_activation" run="solo level set map" open="main_menu/difficulty_select/difficulty_select_list_screen"/>
<child widget="main_menu/new_select/list_item_0" x="20" y="73"/>
<child widget="main_menu/new_select/list_item_0_map_kind" x="82" y="73"/>
<child widget="main_menu/new_select/list_item_1" x="20" y="103"/>
<child widget="main_menu/new_select/list_item_2" x="20" y="133"/>
<child widget="main_menu/new_select/list_item_3" x="20" y="163"/>
Binary file not shown.

After

Width:  |  Height:  |  Size: 75 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

+17 -4
View File
@@ -374,6 +374,7 @@
<string text="Join a game by its invite link,\nor one a Discord invite\nreached."/>
<string text="Host a game on the Internet:\nplayers join by its invite\nlink or Discord."/>
<string text="Host a game on your LAN\nonly."/>
<string text="Play the campaign with a\nfriend in split screen:\nplayer 2 on a gamepad."/>
<string text="Set all the attributes for your \nmultiplayer gametypes and\nkeep them for future use."/>
</strings>
<strings name="main_menu/multiplayer_type_select/multiplayer_options">
@@ -384,6 +385,7 @@
<string text="DIRECT LINK"/>
<string text="EDIT GAMETYPES"/>
<string text="SERVER BROWSER"/>
<string text="CO-OP CAMPAIGN"/>
</strings>
<strings name="main_menu/multiplayer_type_select/server_settings/cap_server_settings_options">
<string text="Enter a name for your server."/>
@@ -498,6 +500,8 @@
<string text="The map's weapons, without any grenades."/>
<string text="You will start the game with the weapon set that the \nmap designers custom tailored for it."/>
<string text="Use the generic starting weapons across all maps."/>
<string text="The map's weapons appear where its designers\nplaced them."/>
<string text="No weapons appear on the map: with a loadout of\nNONE and NONE, melee and grenades only."/>
<string text="The weapons follow the weapon set above."/>
<string text="Everyone starts with the primary and secondary\nweapons below; the map's weapons are its own."/>
<string text="No primary weapon."/>
@@ -510,8 +514,6 @@
<string text="Everyone spawns with a plasma pistol as their primary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a plasma rifle as their primary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a needler as their primary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a fuel rod as their primary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a flamethrower as their primary\nweapon (where the map has one)."/>
<string text="No secondary weapon."/>
<string text="A random secondary weapon, of those the map has,\neach time you spawn."/>
<string text="Everyone spawns with a assault rifle as their secondary\nweapon (where the map has one)."/>
@@ -522,8 +524,6 @@
<string text="Everyone spawns with a plasma pistol as their secondary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a plasma rifle as their secondary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a needler as their secondary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a fuel rod as their secondary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a flamethrower as their secondary\nweapon (where the map has one)."/>
</strings>
<strings name="main_menu/settings_select/multiplayer_setup/item_options_edit/item_options_labels">
<string text="INFINITE GRENADES:"/>
@@ -534,6 +534,7 @@
<string text="LOADOUT:"/>
<string text="PRIMARY WEAPON:"/>
<string text="SECONDARY WEAPON:"/>
<string text="MAP WEAPONS:"/>
</strings>
<strings name="main_menu/settings_select/multiplayer_setup/item_options_edit/var_infinite_grenades">
<string text="YES"/>
@@ -963,6 +964,12 @@
<string text="At the end of this game there will be fifteen angry\nmen. Angry because the winning player killed them."/>
<string text="You'll need to secure twenty-five kills in order to\nwin the game."/>
<string text="We hope you've got plenty of time, because you'll\nneed to make a full fifty kills to win this one."/>
<string text="Seventy-five kills to win. Settle in for a long\nfight."/>
<string text="A hundred kills to win. Made for big games."/>
<string text="A hundred and fifty kills to win. Only a crowded\nserver gets there."/>
<string text="Two hundred kills to win. Bring friends. Lots of\nthem."/>
<string text="Two hundred and fifty kills to win."/>
<string text="Five hundred kills to win. You'll be here a while."/>
<string text="This is a team game."/>
<string text="It's every man for himself."/>
<string text="The game will have no time limit. It will only end\nwhen someone wins."/>
@@ -994,6 +1001,12 @@
<string text="15"/>
<string text="25"/>
<string text="50"/>
<string text="75"/>
<string text="100"/>
<string text="150"/>
<string text="200"/>
<string text="250"/>
<string text="500"/>
</strings>
<strings name="main_menu/settings_select/multiplayer_setup/playlist_edit/slayer_edit/var_team_play">
<string text="YES"/>
+12
View File
@@ -8,6 +8,7 @@
"ce/main_menu.gametype_select.xml",
"ce/main_menu.multiplayer_type_select.connected.4way_profile_select.xml",
"ce/main_menu.multiplayer_type_select.connected.xml",
"ce/main_menu.multiplayer_type_select.coop.xml",
"ce/main_menu.multiplayer_type_select.direct_ip.xml",
"ce/main_menu.multiplayer_type_select.join_game.filters.xml",
"ce/main_menu.multiplayer_type_select.join_game.port.xml",
@@ -16,6 +17,7 @@
"ce/main_menu.multiplayer_type_select.mp_map_select.xml",
"ce/main_menu.multiplayer_type_select.server_settings.xml",
"ce/main_menu.multiplayer_type_select.xml",
"ce/main_menu.new_select.port.xml",
"ce/main_menu.new_select.xml",
"ce/main_menu.player_profiles_select.xml",
"ce/main_menu.profile_manager.xml",
@@ -49,9 +51,19 @@
"ce/main_menu.xml",
"ce/shell.xml",
"ce/strings.xml",
"ce/port/blank__0.png",
"ce/port/main_menu/multiplayer_type_select/coop/header_player_2.png",
"ce/port/main_menu/multiplayer_type_select/join_game/header_direct_link.png",
"ce/port/main_menu/multiplayer_type_select/join_game/header_password.png",
"ce/port/main_menu/multiplayer_type_select/join_game/header_server_browser.png",
"ce/port/main_menu/multiplayer_type_select/lobby/header_add_player.png",
"ce/port/main_menu/multiplayer_type_select/lobby/header_lobby.png",
"ce/port/pause/pausebox_center__0.png",
"ce/port/pause/pausebox_center__1.png",
"ce/port/pause/pausebox_left__0.png",
"ce/port/pause/pausebox_left__1.png",
"ce/port/pause/pausebox_right__0.png",
"ce/port/pause/pausebox_right__1.png",
"ce/shell/bitmaps/alert_bkd__0.png",
"ce/shell/bitmaps/arrow_sm_down__0.png",
"ce/shell/bitmaps/arrow_sm_down__1.png",
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="4" viewBox="0 0 4 4"/>

After

Width:  |  Height:  |  Size: 81 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="64" viewBox="0 0 512 64">
<defs>
<filter id="soft" filterUnits="userSpaceOnUse" x="-60" y="-60" width="632" height="184">
<feGaussianBlur stdDeviation="14.23 13.06"/>
</filter>
<mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="64">
<rect width="512" height="59" fill="#fff"/>
</mask>
</defs>
<g mask="url(#cut)">
<rect x="29.08" y="28.05" width="323.93" height="40.95" fill="#021931" fill-opacity="0.893" filter="url(#soft)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 563 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="64" viewBox="0 0 512 64">
<defs>
<filter id="soft" filterUnits="userSpaceOnUse" x="-60" y="-60" width="632" height="184">
<feGaussianBlur stdDeviation="14.23 13.06"/>
</filter>
<mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="64">
<rect width="512" height="59" fill="#fff"/>
</mask>
</defs>
<g mask="url(#cut)">
<rect x="29.08" y="28.05" width="216.45" height="40.95" fill="#021931" fill-opacity="0.893" filter="url(#soft)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 563 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="64" viewBox="0 0 512 64">
<defs>
<filter id="soft" filterUnits="userSpaceOnUse" x="-60" y="-60" width="632" height="184">
<feGaussianBlur stdDeviation="14.23 13.06"/>
</filter>
<mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="64">
<rect width="512" height="59" fill="#fff"/>
</mask>
</defs>
<g mask="url(#cut)">
<rect x="29.08" y="28.05" width="226.48" height="40.95" fill="#021931" fill-opacity="0.893" filter="url(#soft)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 563 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="256" viewBox="0 0 4 256">
<rect x="0" y="2" width="4" height="190" fill="#0a2649" fill-opacity="0.75"/>
<rect x="0" y="0" width="4" height="2" fill="#2896ff"/>
<rect x="0" y="192" width="4" height="2" fill="#2896ff"/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="256" viewBox="0 0 4 256">
<rect x="0" y="2" width="4" height="225" fill="#0a2649" fill-opacity="0.75"/>
<rect x="0" y="0" width="4" height="2" fill="#2896ff"/>
<rect x="0" y="227" width="4" height="2" fill="#2896ff"/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="256" viewBox="0 0 16 256">
<path d="M9.5,1 H17 V193 H9.5 A8.5,8.5 0 0 1 1,184.5 V9.5 A8.5,8.5 0 0 1 9.5,1 Z" fill="#0a2649" fill-opacity="0.75" fill-rule="evenodd"/>
<path d="M17,1 H9.5 A8.5,8.5 0 0 0 1,9.5 V184.5 A8.5,8.5 0 0 0 9.5,193 H17" fill="none" stroke="#2896ff" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 360 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="256" viewBox="0 0 16 256">
<path d="M9.5,1 H17 V228 H9.5 A8.5,8.5 0 0 1 1,219.5 V9.5 A8.5,8.5 0 0 1 9.5,1 Z" fill="#0a2649" fill-opacity="0.75" fill-rule="evenodd"/>
<path d="M17,1 H9.5 A8.5,8.5 0 0 0 1,9.5 V219.5 A8.5,8.5 0 0 0 9.5,228 H17" fill="none" stroke="#2896ff" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 360 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="256" viewBox="0 0 16 256">
<g transform="matrix(-1 0 0 1 16 0)">
<path d="M9.5,1 H17 V193 H9.5 A8.5,8.5 0 0 1 1,184.5 V9.5 A8.5,8.5 0 0 1 9.5,1 Z" fill="#0a2649" fill-opacity="0.75" fill-rule="evenodd"/>
<path d="M17,1 H9.5 A8.5,8.5 0 0 0 1,9.5 V184.5 A8.5,8.5 0 0 0 9.5,193 H17" fill="none" stroke="#2896ff" stroke-width="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 411 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="256" viewBox="0 0 16 256">
<g transform="matrix(-1 0 0 1 16 0)">
<path d="M9.5,1 H17 V228 H9.5 A8.5,8.5 0 0 1 1,219.5 V9.5 A8.5,8.5 0 0 1 9.5,1 Z" fill="#0a2649" fill-opacity="0.75" fill-rule="evenodd"/>
<path d="M17,1 H9.5 A8.5,8.5 0 0 0 1,9.5 V219.5 A8.5,8.5 0 0 0 9.5,228 H17" fill="none" stroke="#2896ff" stroke-width="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 411 B

+10
View File
@@ -0,0 +1,10 @@
# The public MQTT brokers of internet play: the machines of an invite find
# each other through them, and the server browser's listings go through
# them. The game uses every one at once (up to 4), so any one working is
# enough. One host:port on each line; "#" starts a comment.
# (The game reads this file beside config.toml: network.brokers_file. An
# update replaces it; keep a list of your own under another name.)
opence.milenko.org:1883
broker.emqx.io:1883
broker.hivemq.com:1883
test.mosquitto.org:1883
+97 -8
View File
@@ -21,6 +21,69 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick:
vehicle it drives) from its local input at once. Remote players are
driven by the inputs the host relays every tick, the latest one held
until a newer arrives.
- **Actors driven.** Only the host runs the AI. Each tick it sends its
clients the control its actors gave their units (how they move, where
they face, aim and look, their trigger and buttons, their animation
impulses) with the units' state, those near a client's players every
tick and others less often; a client drives each unit with the latest
it has, as it drives a remote player's, until it hears nothing of it for
two seconds (`port/linux/game/network_actors.c`).
- **Co-op.** A network game on a campaign level with no game engine
(Create Game's Map screen, its SINGLEPLAYER maps, over LAN and the
internet) is co-op. Only the host runs the
level's scripts and spawns players. `network_coop.c` sends the clients
everything the scripts do that they would otherwise miss:
- every tick: the cinematic, camera, screen fade, the HUD settings the
scripts control (what is shown, the mission timer), the skip vote, and
which teams are allies and friends (the scripts' allegiances, so the
marines are the players' allies on every machine);
- once each, numbered so nothing is applied twice: script sounds,
chapter titles, help and objective text, "Checkpoint" messages, screen
shake, nav points, custom animations on units and scenery, and units
opening and closing (dropships' doors);
- device groups (doors, elevators, switches; a client sets none itself,
and its player's use of one is relayed to the host), and each device's
position and power as it changes: a client puts its device where the
host's is once that stops, or if they drift apart while it moves;
- which named objects exist, so scripted creates and deletes match.
Every machine follows the host's structure BSP: a switch is sent at once
and reliably, and each client's input says which BSP it has loaded.
Until a client has the host's, the host takes none of its players'
movement and none of its loading zones, and after any switch no loading
zone switches again until every machine has the new BSP (ten seconds at
most). A loading zone into a
BSP the team hasn't been in brings every player to whoever crossed it,
as split screen does; one back into a BSP it has been in switches only
with two thirds of the living players at it (in the trigger, or within
15 world units of the player in it, about 45 metres), so one player can't
drag the team back through the level; a player held back is told how
many are there and how many it needs. A player outside the loaded BSP
and falling for two seconds is brought back beside a teammate. A dead
player watches a living teammate (`coop_spectate.c`) and comes back
beside one once it is safe. With everyone dead they come back where they
were at the last checkpoint, without a revert. A mission the scripts fail
with players still alive reverts to the last checkpoint on the host, as a
skipped cutscene does (below); a client never reverts on its own. A level
won ends the round as in multiplayer, and the next round is the
campaign's next level.
Cutscenes are skipped by vote: more than half the machines must press
skip. The host then reverts as single player does, but keeps its clock
moving forward (the netcode depends on that) and moves the script
threads' wake times along with it. The object, device and name syncs
bring the clients up to date.
The host's EXTRA ENEMIES (`coop_enemies.c`, `network.coop_enemies_mode`)
give each squad of enemies a level places more of itself: PER PLAYER, a
percentage of itself for each player past the first; STATIC MULTIPLIER,
that many times itself for any number of players. They stand around its
starting locations on free ground (the same floor, clear of crates and
other actors, with room to stand), or where none is left on rings about
them as before, and never take the actors a level needs for its own (the
actor pool, `halo_port_capacity.h`, holds 1024). Riders a dropship has no
seats for are kept, and placed beside its riders once they get out. Only
the host runs the AI, so the clients see them as the host's other actors.
- **Host authoritative.** The host alone decides damage, deaths, spawns,
pickups, scores and the game's objects; clients do not decide them but
apply what the host sends.
@@ -93,7 +156,24 @@ is dead; version 8 is the first whose clients play by the host's rules
(below), so a build without them joins no host of it; version 9 tells
every machine of a player the host dropped for cheating, each client
tells the host its Discord user, and a machine's join request carries its
hardware id; version 10 sends every player's ping for the scoreboard.
hardware id; version 10 sends every player's ping for the scoreboard;
version 11 sends with the game's settings its gametype's PC options;
version 12 plays the campaign together (co-op, above), drives the host's
actors on its clients and sends the flinches and deaths the host picked;
version 13 drives up to 1056 of the host's AI units on its clients (co-op's
extra enemies), where 12 drove 288; version 14 sends co-op's device positions
and its units opening and closing; version 15 sends co-op's allegiances with
its presentation, a message of another size; version 16 has a client's input
say which structure BSP it has loaded (co-op); version 17 breaks the host's
glass and destructible scenery on every machine (and takes a client's hits on
scenery), sends the cluster a co-op cutscene keeps active, and leaves a
failed co-op mission's revert to the host; version 18 sends with an object
the bitmap of its shaders it draws with when its actor variant set one (co-op:
the Elite major's and commander's armor); version 19 sends with the game's
settings whether co-op's players collide with each other (Server Setup's PLAYER
COLLISIONS: each machine's players then pass through the others'); version 20
lists a public game with a password with its invite's token sealed with the
password's key (`p2p_lobby.c`), a listing of another layout.
A client plays by its host's rules: in another's game (searching for it,
in its lobby, or playing it) the developer console, the telnet console
@@ -115,9 +195,13 @@ only ever jumps forward to it when behind, so an honest one is never ahead
while going faster (one that caught up, or a host that stalled, is one or
the other, not both). One more than a tenth faster and half a second ahead
has its players' predictions refused at once (the host's copies go as its
own ticks have them), and after ten seconds of it is dropped, its address
kept out of the host's games while the host runs, and every machine is
told who, in red on its console and in its `debug.txt`
own ticks have them), and after ten seconds of it is dropped, and every
machine is told who, in red on its console and in its `debug.txt`. Its
address is kept out of the host's games while the host runs only when a
message that came over its connection's stream was that far ahead too: a
datagram is known to be the machine's only by the address it came from,
which another machine can send one as, so on its datagrams alone it is
dropped, logged, and may join again
(`distributed_note_client_clock`, `network_game_server_kick_machine`,
`_distributed_message_notice`). The host also adds a line to
`cheaters.txt` beside its `debug.txt`: when, the player's address (an
@@ -132,13 +216,16 @@ A joining machine tells the host its hardware id: a keyed hash (HMAC-SHA-256,
16 bytes as hex) of what its machine is known by (Windows' SMBIOS UUID, else
its MachineGuid; Linux's `/etc/machine-id`; Android's `ANDROID_ID`, which the
launcher writes to `hardware_id.txt`: `p2p_hardware_id`), kept by the host
as hex only. A player dropped for cheating, and one the host bans with the
as hex only. A player dropped for cheating (on its stream's word, as
above), and one the host bans with the
console's `ban <player name>` (Tab completes the name; the host's alone), is
added to `bans.txt` beside `debug.txt` (a line each, as in `cheaters.txt`,
with `ip=` and `hwid=`): the host refuses a machine joining whose address or
hardware id is in it (a line taken out unbans). Both are as the player's
machine tells them: anyone with administrator or root access can change
them, and players behind one address share it.
them, and players behind one address share it. The console's `kick <player
name>` drops a player as `ban` does (every machine told), but adds no line
and keeps no address out: the player may join again at once.
A speed hack of less than a tenth is let be: the host's bounds on how far
and how fast a client's player moves and fires hold it to the host's time
anyway.
@@ -282,8 +369,10 @@ a pregame keep-alive every five seconds from the host
host's latest tick it had heard of when it made the report.
- The host deals a report once it has checked it: from that machine's
player; damage one of their weapons (a vehicle's a driver's or
gunner's; now or in the last ten seconds), their grenades (while they
have them, and for a while after) or the vehicle they drove (in the
gunner's; now or in the last ten seconds), a grenade the host's own game
saw them throw in the last ten seconds that has not gone off (each
throw's explosion is taken once, its other hits that tick with it:
holding grenades deals nothing) or the vehicle they drove (in the
last ten seconds: its collisions) can deal (its projectiles' impacts
and detonations, followed through the tags), no harder than it can be
(all of it, but an airborne melee blow's half again); of the shape the
+153 -22
View File
@@ -83,7 +83,8 @@ These files are in the data root:
| `init.txt` | Console commands that the game does at start-up. For example, `map_name levels\a10\a10` starts the first campaign level. |
The settings are in `config.toml` next to the executable. Refer to
"Settings".
"Settings". Internet play's MQTT brokers are in `brokers.txt` next to it
(`network.brokers_file`).
If the game stops because of a fatal signal, it writes the address and a
backtrace to the standard error. To find the function at the address, enter
@@ -95,8 +96,12 @@ The keyboard and the mouse are a control scheme of their own for the player
of controller 1: each action has up to two keys or mouse buttons, which
Settings > Controls Setup (or `[controls]` in `config.toml`) changes. The
game adds the input of the first gamepad to controller 1. The other
gamepads operate controllers 2 to 4. The profile's button layout (Settings >
Gamepads) is the gamepads' only.
gamepads operate controllers 2 to 4. With two or more players on this
computer (co-op, or split screen in a network game) and only one gamepad,
that gamepad is controller 2 (player 2) and the keyboard and mouse stay
controller 1. The gamepad changes controller only when none of its buttons
is held. The profile's button layout (Settings > Gamepads) is the
gamepads' only.
| Action | Keys and buttons (default) |
| --- | --- |
@@ -157,6 +162,37 @@ change. The screens open, close and move between each other as the PC
version's. `display.menus = "xbox"` gives the Xbox's menus, which start every
kind of game.
Multiplayer > CO-OP CAMPAIGN is the Xbox's cooperative play, which the PC
version does not have: two players on this computer play the campaign in
split screen. Player 1 is the player who chose it, on the current profile.
Player 2 then chooses a profile with their own controller (a gamepad), and
New Game's levels are those either profile has reached. Either player's
controller chooses the level and the difficulty. A co-op game does
not continue a saved game of one player.
Network games have split screen too: up to 4 players on each computer. In
the game lobby, another controller presses START to join, and the new
player's profile is chosen on the ADD PLAYER screen that opens (with any
controller). With one gamepad, choose the lobby's ADD PLAYER button first:
until then that gamepad shares controller 1 with the keyboard. Two players on one profile get different names from the host. A
player's B in the lobby leaves the game alone, and the last player of the
computer leaves it for all of them. In the game, each player's pause menu
opens on their part of the screen, and its LEAVE GAME is theirs: their part
of the screen stays until the game ends. A game under way shows its own
screen before JOIN GAME: players join there the same way, START or ADD
PLAYER then START, and JOIN GAME brings them all into the game.
In a multiplayer game, the pause menu (escape) has SETTINGS, which opens
the profile's settings while the game goes on, and for the host END GAME.
END GAME ends the game as its time limit does: the players stay, and after
the carnage report the host picks the next map and gametype (PICK GAME).
LEAVE GAME of the host still ends the game for everyone. The buttons go into
the map's own pause menu, before LEAVE GAME, so the buttons of a custom map
stay. The Xbox's pause box is drawn taller to hold them (a redraw,
`port/assets/menus/port_svg/pause`); a custom map's box keeps its size, and
what is below its list moves down. The few pictures of the settings that
come from the main menu's map are not drawn there.
The menus are XML files in `port/assets/menus` (`tools/ce_menus.py` writes
them from the PC version's tags), which the game contains. To change them,
put files in a `menus` folder next to `config.toml`: a file with the same
@@ -184,26 +220,33 @@ the setting for one start of the game. It has priority over the file.
| Setting | Default | Environment variable | Function |
| --- | --- | --- | --- |
| `display.mode` | `""` | `HALO_DISPLAY_MODE` | `"fullscreen"`: the display, taken at its desktop resolution. `"borderless"`: a window over the whole desktop. Both draw at the resolution of the display, the picture 480 lines of the game and the width of the display. `"windowed"`: a window with the 640x480 picture of the Xbox, scaled. Empty: `display.fullscreen` decides (`true`: borderless). F11 changes between the window and the fullscreen mode. Video Setup sets it. |
| `display.fullscreen` | `true` | `HALO_FULLSCREEN` | `true`: fullscreen at the resolution of the display. The picture has 480 lines of the game and the width of the display. `false`: a window with the 640x480 picture of the Xbox. Used when `display.mode` is empty. |
| `display.window_scale` | `2` | `HALO_WINDOW_SCALE` | The size of the window, as a multiple of 640x480. You can change the size of the window. |
| `display.mode` | `""` | `HALO_DISPLAY_MODE` | `"fullscreen"`: the display, taken at the mode of `display.resolution` (the nearest the display has), or at its desktop mode. `"borderless"`: a window over the whole desktop, whose mode does not change. `"windowed"`: a window of `display.window_size`. Empty: `display.fullscreen` decides (`true`: borderless). F11 changes between the window and the fullscreen mode. Video Setup sets it. |
| `display.fullscreen` | `true` | `HALO_FULLSCREEN` | `true`: borderless, as `display.mode = "borderless"`. `false`: a window, as `display.mode = "windowed"`. Used when `display.mode` is empty. |
| `display.resolution` | `"native"` | `HALO_RESOLUTION` | What fullscreen and borderless draw at: `"native"`, the display's own resolution, or `"<width>x<height>"`, such as `"1920x1080"`, 640x480 or more. Fullscreen sets the display to it. Borderless draws at it and scales the picture to the display, where the display has room for it. The picture has 480 lines of the game and the width of the resolution's shape. A window draws at its own size instead. Video Setup's Resolution sets it, from the display's modes; it shows with Fullscreen and Borderless. |
| `display.resolution_scaling` | `"native"` | `HALO_RESOLUTION_SCALING` | `"native"`: the game draws at the resolution of the window, or of the display (or `display.resolution`) fullscreen. `"original"`: the game draws the 640x480 picture of the Xbox and scales it up to the window or the display, whatever `display.mode` is. Video Setup sets it. |
| `display.window_size` | `""` | `HALO_WINDOW_SIZE` | The size of the window, as `"<width>x<height>"`, such as `"1920x1080"`, 640x480 or more. You can change the size of the window; the game's picture takes its shape. Empty: `display.window_scale` decides. Video Setup's Window Size sets it, from sizes of each shape (4:3, 16:10, 16:9 and 21:9) that fit the desktop; it shows with Windowed. |
| `display.window_scale` | `2` | `HALO_WINDOW_SCALE` | Used when `display.window_size` is empty: the size of the window, as a multiple of 640x480. |
| `display.vsync` | `true` | `HALO_NO_VSYNC=1` sets `false` | `true`: each frame waits for the display. |
| `display.max_fps` | `0` | `HALO_MAX_FPS` | With vsync off, the most frames each second. `0`: twice the display's refresh rate. `-1`: no limit, which can hang some Intel graphics (Raptor Lake), resetting the desktop's graphics too. |
| `display.anti_aliasing` | `"off"` | `HALO_ANTI_ALIASING` | The smoothing of jagged edges, which the Xbox did not have. `"off"`: none, as on the Xbox. `"fxaa"` or `"smaa"`: a pass over the 3D view after the game draws it. The HUD and the menus stay sharp. SMAA is the sharper and costs more. `"ssaa2x"`: the game draws at two times the resolution in each direction (at most the GPU's largest texture), and the picture is scaled down. The GPU does four times the work. Not with `display.resolution_scaling = "original"`. `"msaa2x"`, `"msaa4x"` or `"msaa8x"`: each pixel of the 3D view has that many samples (at most the GPU's). On Android, `"smaa"` gives FXAA and `"ssaa2x"` none. A change applies from the next frame. Refer to "Anti-aliasing" in "What operates". |
| `debug.gpu_flush_draws` | `-1` | `HALO_GPU_FLUSH_DRAWS` | Flush the GPU's pipeline every this many draws. `-1`: every 3 on Intel graphics with Mesa's driver, which can otherwise hang in the game's long runs of small draws and reset the desktop's graphics too. `0`: never. |
| `display.interpolation` | `true` | `HALO_INTERPOLATION` | `true`: one frame for each refresh of the display. `false`: 30 frames each second, as on the Xbox. Refer to "Frame rate". |
| `display.direct_camera` | `true` | `HALO_DIRECT_CAMERA` | `true`: in first person, on foot, the view points where the player aims in each frame, not where the last tick left it. Refer to "Frame rate". |
| `display.high_res_hud` | `true` | `HALO_HIGH_RES_HUD` | `true`: the HUD (meters, counters, panels and their outlines, the motion sensor, reticles, waypoints, scopes) is drawn from the high-res assets in `port/assets/hud`, 8x the size of the maps' bitmaps. The bitmaps with English text keep the maps' own. `false`: the maps' own bitmaps. |
| `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the display's resolution, laid out as before, and the menus' titles are drawn from the high-res pictures in `port/assets/titles`. `false`: the maps' bitmap fonts and titles. |
| `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the resolution the game draws at, laid out as before, and the menus' titles are drawn from the high-res pictures in `port/assets/titles`. `false`: the maps' bitmap fonts and titles. |
| `display.shadow_resolution` | `128` | `HALO_SHADOW_RESOLUTION` | The size of the maps that the shadows of the objects are drawn in, in pixels each way: `128`, `256`, `512` or `1024` (other values go down to one of these). The game draws the shadow of each object into a map of 128x128 pixels, blurs it and projects it onto the ground. On a large screen, the edges of these shadows show steps that move when the object moves. A larger map makes the edges smooth; the blur is made wider to match, so the shadows are as soft as on the Xbox. Each doubling adds two passes of the blur. `128`: as on the Xbox. |
| `display.menus` | `"pc"` | `HALO_MENUS` | `"pc"`: the PC version's menus, from the files in `port/assets/menus` and a `menus` folder next to `config.toml`. Refer to "Menus". `"xbox"`: the Xbox's menus. |
| `display.player_names` | `"all"` | `HALO_PLAYER_NAMES` | In multiplayer, whose names are drawn above their heads: `"all"`, `"allies"`, `"enemies"` or `"none"`. An ally's name is drawn above the triangle the game shows over teammates. An enemy's name shows only while the enemy is in sight and not camouflaged, so it never shows where an enemy hides, and only as far away as the weapon in hand turns its reticle red over an enemy (at least 20 world units, the motion sensor's reach, and at most 70). |
| `display.player_names` | `"all"` | `HALO_PLAYER_NAMES` | In multiplayer, whose names are drawn above their heads: `"all"`, `"allies"`, `"enemies"` or `"none"`. An ally's name is drawn above the triangle the game shows over teammates. An enemy's name shows only within the motion sensor's reach, while the enemy is in sight and not camouflaged, so it never shows where an enemy hides. The gametype's motion tracker setting also applies: no names if it shows no players, only allies' if it shows only friends. |
| `display.player_name_scale` | `1.0` | `HALO_PLAYER_NAME_SCALE` | How large the players' names are drawn: `1.0` is three quarters of the size of the HUD's text, from `0.25` to `4`. With high-res text, larger names are rasterized at their size, so they stay sharp. |
| `display.scoreboard_team_layout` | `"teams"` | `HALO_SCOREBOARD_TEAM_LAYOUT` | How the multiplayer scoreboard (hold BACK, or tab) lists a team game's players. `"teams"`: a column for each team, red on the left and blue on the right. `"score"`: all the players in order of score. With more players than fit, the mouse wheel and Page Up / Page Down scroll the scoreboard. |
| `display.scoreboard_background` | `true` | `HALO_SCOREBOARD_BACKGROUND` | `true`: the multiplayer scoreboard (hold BACK, or tab) has a panel behind its text, for clearer text. |
| `display.scoreboard_background_color` | `"16, 16, 16, 150"` | `HALO_SCOREBOARD_BACKGROUND_COLOR` | The colour of the scoreboard's panel: `"red, green, blue, alpha"`, each from `0` to `255`. Alpha `0` is see-through, `255` is solid. |
| `display.per_pixel_lighting` | `false` | `HALO_PER_PIXEL_LIGHTING` | `false`: the models (characters, weapons, vehicles, scenery) are lit at each vertex and the light is blended between them, as on the Xbox. The light across a curved surface then shows facets, and a point light that passes close lights only the vertices it reaches. `true`: the models are lit at each pixel by the same lights (the ambient light, two distant lights and two point lights), which changes their look. |
| `audio.enabled` | `true` | `HALO_NO_AUDIO=1` sets `false` | `false`: no audio device. The sound continues without output. |
| `audio.volume` | `1.0` | `HALO_VOLUME` | The master volume. |
| `audio.music_volume` | `1.0` | `HALO_MUSIC_VOLUME` | The music's volume, of the master volume. |
| `audio.effects_volume` | `1.0` | `HALO_EFFECTS_VOLUME` | The volume of the other sounds (effects and speech), of the master volume. |
| `audio.reverb` | `true` | `HALO_REVERB` | `true`: the sounds of the world reverberate as the place the player is in does: the sound environments of the maps (a corridor, a cave, a large hall, outdoors) set the reverberation, as the I3DL2 reverb of the Xbox did. A sound behind a wall or a door is muffled in it too. `false`: no reverberation (sounds behind a wall are still muffled). |
| `input.mouse_sensitivity` | `1.0` | `HALO_MOUSE_SENSITIVITY` | The multiplier for the mouse aim. |
| `input.mouse_vertical_sensitivity` | `0.0` | `HALO_MOUSE_VERTICAL_SENSITIVITY` | The multiplier for the vertical mouse aim. `0`: the same as `input.mouse_sensitivity`. |
| `input.invert_mouse` | `false` | `HALO_MOUSE_INVERT=1` sets `true` | `true`: the vertical mouse aim is inverted. |
@@ -219,7 +262,15 @@ the setting for one start of the game. It has priority over the file.
| `network.join_from_clipboard` | `true` | `HALO_NET_JOIN_FROM_CLIPBOARD` | `true`: when the game comes to the front, it joins the game of an invite link on the clipboard. |
| `network.tunnel_port` | `0` | `HALO_NET_TUNNEL_PORT` | The UDP port for internet play. `0`: the game selects a port. Refer to "Internet play". |
| `network.allow_upnp` | `true` | `HALO_NET_ALLOW_UPNP` | `true`: internet play can ask the router to forward its port (UPnP). `false`: the game does not ask. Refer to "Internet play". |
| `network.signalling_brokers` | three public brokers | `HALO_NET_BROKERS` | The public MQTT brokers (`host:port`, with commas between them) that let the machines of an invite find each other. |
| `network.public_lobby` | `true` | `HALO_NET_PUBLIC_LOBBY` | `true`: the server browser. Public games are listed, and Join Game > Server Browser shows them. `false`: no games are listed or shown. Refer to "Server browser". |
| `network.host_public` | `true` | `HALO_NET_HOST_PUBLIC` | `true`: a new game of Create Game > Internet starts as PUBLIC. `false`: it starts as PRIVATE. LISTING in Server Setup changes it for each game. Refer to "Server browser". |
| `network.coop_friendly_fire` | `"on"` | `HALO_NET_COOP_FRIENDLY_FIRE` | Whether the players of an online co-op game hurt each other: `"off"`, `"on"`, `"shields_only"` or `"explosives_only"`. FRIENDLY FIRE in co-op's Server Setup writes its choice here. Their AI allies they always can, as in the campaign. |
| `network.coop_player_collisions` | `true` | `HALO_NET_COOP_PLAYER_COLLISIONS` | Whether the players of an online co-op game bump into each other. `false`: they walk through each other, so that one cannot block a doorway or stand on another; they still bump into the AI's characters. PLAYER COLLISIONS in co-op's Server Setup writes its choice here. |
| `network.coop_enemies_mode` | `"per_player"` | `HALO_NET_COOP_ENEMIES_MODE` | Online co-op's extra enemies: `"none"`; `"per_player"`, each squad of enemies that a level places grows by `network.coop_enemies` for each player past the first; or `"multiplier"`, each squad is `network.coop_enemies_multiplier` times as large, for any number of players. The extra enemies stand around the squad's places, and those that a dropship has no seats for drop out of it after its passengers. EXTRA ENEMIES in co-op's Server Setup writes its choice here. |
| `network.coop_enemies` | `50` | `HALO_NET_COOP_ENEMIES` | The extra enemies per player, a percentage from `25` to `200`: for each player past the first, each squad of enemies gets this much of itself more (`100`: as many again, so four players meet four times the squad). PER PLAYER in co-op's Server Setup writes its choice here. |
| `network.coop_enemies_multiplier` | `2` | `HALO_NET_COOP_ENEMIES_MULTIPLIER` | The static multiplier of the enemies, `2` to `32`: each squad of enemies is this many times as large. MULTIPLIER in co-op's Server Setup writes its choice here. |
| `network.coop_public` | `false` | `HALO_NET_COOP_PUBLIC` | `true`: an online co-op game (Create Game > Internet, a SINGLEPLAYER map) starts as PUBLIC. `false`: it starts as PRIVATE. LISTING in co-op's Server Setup writes its choice here. Refer to "Server browser". |
| `network.brokers_file` | `"brokers.txt"` | `HALO_NET_BROKERS_FILE` | The file of the public MQTT brokers that let the machines of an invite find each other, and that carry the listings of the server browser: next to `config.toml`, unless a full path. One `host:port` on each line, up to 4; `#` starts a comment. |
| `network.stun_servers` | Google and Cloudflare | `HALO_NET_STUN` | The public STUN servers (`host:port`, with commas between them) that give the internet address of a machine. |
| `discord.application_id` | the application of the project | `HALO_DISCORD_APPLICATION` | The Discord application for invites. Empty: no Discord. |
| `update.auto` | `true` | `HALO_UPDATE_AUTO` | `true`: at start-up, the game looks for a new version. Refer to "Updates". `false`: the game does not look. |
@@ -402,17 +453,89 @@ When the machines connect, the game of the host shows in Multiplayer,
System Link. Join the game as on a local network. System link on a local
network does not need an invite.
### Server browser
Server Setup in Create Game > Internet has a LISTING row:
- PUBLIC (the default): the game also shows in Join Game > Server Browser
on every machine. Anyone can see and join the game.
- PRIVATE: only players with the invite link can join.
Each new game starts as PUBLIC (`network.host_public = false` makes new
games start as PRIVATE). An online co-op game starts as PRIVATE, and keeps
the last choice of its LISTING (`network.coop_public`). A LAN game is never listed. `network.public_lobby = false` turns the server browser off.
A PUBLIC game can also have a PASSWORD (a row of Server Setup, below
LISTING). The Server Browser shows a lock at the left of a game with a
password. A player who selects that game must type the password, and JOIN
GAME joins only with the correct password. The invite link of the game joins
it without the password. The host keeps the password only while the game
runs.
In the Server Browser, select a game to join it. The game joins the invite
of the game, as for a link. When it reaches the host, it opens the lobby.
If it cannot reach the host in 30 seconds, it marks the game FAILED. A game
that is full or starting shows CLOSED. REFRESH asks the hosts for their
listings again.
How it operates (`src/p2p_lobby.c`):
- The host of a public game publishes a listing: the invite, and the name,
map, gametype and player counts of the game. The listing goes to the
same MQTT brokers as the invites (`network.brokers_file`), retained,
to a topic of the host (`hceu/3/lobby/s/<hash of its key>`).
- The key of the host signs the listing (Ed25519). The key is the key of the
invite, so no other machine can list the invite of the host, change its
listing, or list a game with false details.
- The host publishes the listing again every 30 seconds, when the game
changes, and when a browser asks. If the topic of the host is empty or
holds another listing, the host publishes again in 5 seconds or less. Thus
a broker that deletes the listing does not remove the game.
- When the host stops (it stops hosting, the game becomes private, or the
game quits), it publishes a closed listing, and then empties its topic.
If the host loses its connection, the broker empties its topic (the will
of the connection). A browser removes a game that it does not hear for 90
seconds.
- When a public game becomes private, the host makes a new invite. Thus a
player who saw the listing cannot join with the old invite.
- The listing of a game with a password does not hold the invite in clear
text. The secret part of the invite (its token) is encrypted with a key
from the password (Argon2id, salted with the key of the host, then
XChaCha20-Poly1305). The browser makes the key from the password that the
player types, and opens the invite only if the password is correct. When
the host sets or changes the password, it makes a new invite. A player who
has the listing can try passwords on their own machine without the host,
so use a long password.
A public game does not publish the address of the host. But any machine
with the invite can ask the host to connect, and the host then sends its
addresses. Thus anyone can learn the address of the host of a public game,
as for any public server.
The brokers are in `brokers.txt` next to the executable (from
`port/assets/network/brokers.txt`; on Android, the app writes it next to
`config.toml` at each start), one `host:port` on each line. The game uses
all of them at once (up to 4), so one that works is enough. An update
replaces `brokers.txt`: to use brokers of your own, put them in another
file and name it in `network.brokers_file`. All the players must use the
same broker to see each other's games. The game uses
MQTT 5 if the broker has it, else MQTT 3.1.1. A broker that does not keep
retained messages, or does not let clients subscribe with wildcards, carries
only invites, not listings.
### Security
Only machines with the invite can find the game:
- Each copy of the game makes an X25519 key pair when it starts. Its
identifier is from the hash of its public key.
- Each copy of the game makes an Ed25519 key pair when it starts, and from
it an X25519 key pair (Monocypher, `port/third_party/monocypher`). Its
identifier is from the hash of its X25519 public key. The Ed25519 key
signs the listing of a public game.
- The link contains a 16-byte hash of the public key of the host and a
random 16-byte token. The identifier of the host is from the first 6
bytes of the hash.
- The machines exchange their public keys and addresses through public MQTT
brokers (`network.signalling_brokers`). The topics are HMACs of the token.
brokers (`network.brokers_file`). The topics are HMACs of the token.
A key from the token encrypts and authenticates the messages
(`src/p2p_signal.c`, `src/p2p_crypto.c`). The host authenticates its answer
with a key that only it and the player can calculate. Its public key must
@@ -444,15 +567,20 @@ Only machines with the invite can find the game:
flood of requests can make players join more slowly. A player asks again
for 90 seconds.
- The host drops a player whose game runs faster than time (a speed hack)
for ten seconds, and keeps that address out of its games. Each player
sees who in red on the console. The host adds a line to `cheaters.txt`
(beside `debug.txt`) with the address and hardware id of the player, and
the Discord name and id that the game of the player told it (a player can
change these). The host also bans the player: it adds the line to
`bans.txt`, and refuses a machine whose address or hardware id is in it.
for ten seconds. Each player sees who in red on the console. The host
adds a line to `cheaters.txt` (beside `debug.txt`) with the address and
hardware id of the player, and the Discord name and id that the game of
the player told it (a player can change these). If the messages on the
player's connection were also ahead, the host keeps that address out of
its games and bans the player: it adds the line to `bans.txt`, and refuses
a machine whose address or hardware id is in it. If only the player's
datagrams were ahead, the player can join again: another machine can send
datagrams with the player's address.
- The host can ban a player with `ban <player name>` in the developer
console (Tab completes the name). Remove a line from `bans.txt` to unban.
Refer to `NETCODE.md`. So that every player can be named, the host trims
Refer to `NETCODE.md`. `kick <player name>` drops the player the same
way, but keeps nothing: no line in `bans.txt`, and the player can join
again at once. So that every player can be named, the host trims
the spaces around a name and removes characters that draw as nothing. A
letter with a mark is typed as the plain letter (`ban jose` for "José").
A name with nothing left to type becomes "Player", and a name that another
@@ -522,7 +650,8 @@ The game sends the activity only to a Discord client of the same user.
| Game code | All 466 C files of the game. The changes are in "Game source changes". |
| Graphics | Direct3D 8 on OpenGL 4.5 core through SDL3 (`src/d3d8_gl.c`). The port translates the NV2A vertex shaders and register combiners to GLSL. It decodes all the Xbox texture formats. The vertex and index buffers come from a GL copy of the Xbox memory. |
| High-res HUD | The HUD is drawn from high-res assets: redraws at 8x the size of the maps' bitmaps (4x for the largest), in `port/assets/hud`. They cover the meters, counters, panels and their outlines, the motion sensor, reticles, waypoints and scopes, but no bitmap with English text. `tools/hud_assets.py` makes them from the SVG redraws, and the build embeds them in the executable. When the game uploads one of those bitmaps, `src/hud_hires.c` gives the high-res texture in its place, if the bitmap's pixels are those of the English maps: another language's maps keep their own. The game sizes and places the HUD from its tags as before. `display.high_res_hud = false` turns this off. |
| High-res text | The menus' and HUD's text is drawn with Overpass (`port/assets/fonts`, SIL Open Font License) in place of the maps' bitmap fonts, which are Interstate. `src/text_hires.c` rasterizes each glyph with stb_truetype (`port/third_party/stb`) at the display's resolution, into an atlas that a placeholder bitmap of the game stands for. The game lays the text out from its font tags as before. The menus' titles (the screens' headers and the main menu's items) are pictures of text in the maps, so they are drawn as the high-res HUD is: `tools/title_assets.py` sets each one again in OpenCE, Roger White's public-domain Newtown respaced to match the maps' commercial title typeface (`tools/title_font.py`), at 4x the bitmap's size over its own plate or glow, each letter placed where the map's letter is, in `port/assets/titles`. The postgame carnage report's title is set over a hand-made SVG redraw of its panel (`port/assets/titles/svg`) instead. `display.high_res_text = false` turns it off. |
| High-res text | The menus' and HUD's text is drawn with Overpass (`port/assets/fonts`, SIL Open Font License) in place of the maps' bitmap fonts, which are Interstate. `src/text_hires.c` rasterizes each glyph with stb_truetype (`port/third_party/stb`) at the resolution the game draws at, into an atlas that a placeholder bitmap of the game stands for. The game lays the text out from its font tags as before. The menus' titles (the screens' headers and the main menu's items) are pictures of text in the maps, so they are drawn as the high-res HUD is: `tools/title_assets.py` sets each one again in OpenCE, Roger White's public-domain Newtown respaced to match the maps' commercial title typeface (`tools/title_font.py`), at 4x the bitmap's size over its own plate or glow, each letter placed where the map's letter is, in `port/assets/titles`. The postgame carnage report's title is set over a hand-made SVG redraw of its panel (`port/assets/titles/svg`) instead. `display.high_res_text = false` turns it off. |
| Anti-aliasing | Off unless `display.anti_aliasing` is set (`src/d3d8_gl.c`, `src/xgpu_post.c`). FXAA (written in the port) and SMAA (`port/third_party/smaa`, MIT licensed, at its HIGH preset, compiled as GLSL) are passes over the 3D view of each window, after the lens flares and before the HUD and the menus (`render/render.c`). Their programs are built when the setting is chosen. Supersampling draws the render targets the size of the screen at two times the resolution in each direction, and the display blit scales them down. Multisampling draws the back buffer and its depth buffer into multisampled renderbuffers, and with them any target that is drawn together with one of them (a mirror's view, in the secondary target with the back buffer's depth buffer), so that the attachments of a framebuffer are all multisampled or none is. A target's pixels are resolved into its texture before something reads the texture (as a texture, or at the display blit). Visibility tests count samples, divided by the samples of a pixel. An alpha-tested surface (foliage, grates) covers the samples of a pixel in proportion to its alpha past the reference (`gl_SampleMask`, not on Android). |
| Sound | Xbox DirectSound on SDL3 audio (`src/dsound_sdl.c`): PCM and Xbox ADPCM, mixed at 48 kHz, with volume, pitch, mix bins, distance, stereo pan, occlusion and obstruction. There is no Doppler effect, no cones and no reverb. |
| Input | XInput on SDL3 (`src/xinput_sdl.c`): keyboard, mouse, gamepads with rumble, and the debug keyboard for the console. |
| Files | The Win32 file functions and the MSVC file functions on POSIX, with the translation of Xbox paths. |
@@ -608,11 +737,13 @@ Other changes:
| `networking/`, `game/`, `interface/`, `bungie_net/network/` and the pools of objects, effects and sounds | The system link limits and the memory for them. |
| `game/`, `objects/`, `units/`, `networking/` | The distributed netcode. Refer to `NETCODE.md`. |
| `cache/cache_files.c` | When a map's tags load and unload, the port finds the bitmaps that the high-res HUD replaces (`game/hud_hires_tags.c`), and adds the tags of the menus to the menus' map (`game/menu_tags.c`). |
| `interface/ui_widget.c`, `interface/ui_widget_event_handler_functions.c`, `interface/ui_widget_game_data_input_functions.c` | The main menu is the PC version's from `port/assets/menus` (`display.menus`); the menus' widgets can call the port's functions (`game/menu_functions.c`) and send the PC version's custom activation event; the widgets' memory is 256 KB, not 16 KB. |
| `interface/ui_widget.c`, `interface/ui_widget_event_handler_functions.c`, `interface/ui_widget_game_data_input_functions.c` | The main menu is the PC version's from `port/assets/menus` (`display.menus`); the menus' widgets can call the port's functions (`game/menu_functions.c`) and send the PC version's custom activation event; the widgets' memory is 256 KB, not 16 KB; the main menu and Multiplayer clear co-op's controllers, so that a gamepad going back to controller 1 is not a controller unplugged; the lobby's split screen players leave alone, and one who quits in game is not joined to the next game (`interface/player_ui.c`). |
| `input/input_abstraction.c`, `game/player_control.c`, `game/players.c`, `game/player_queues_new.c`, `units/units.h` | The keyboard and mouse's actions (`src/xinput_sdl.c`, `include/halo_keyboard.h`) join controller 1's game controls; their reload key reloads on its own, and their action key only acts (a control flag of the port's, sent with the player's action, stops the reload the controller's X falls back to). |
| `sound/sound_manager.c`, `interface/hud.c`, `game/game_engine.c` | The music's and the other sounds' volumes; the HUD's and the scoreboard's settings are read again when Settings changes them. |
| `rasterizer/xbox/rasterizer_xbox_shadows.c` | With larger shadow maps (`display.shadow_resolution`), the blur's taps stay half a texel apart, and more passes widen it to cover the same part of the map as on the Xbox. |
| `render/render.c` | The 3D view of each window is antialiased before the HUD is drawn (`display.anti_aliasing`). |
| `interface/hud.c` | In multiplayer, players' names are drawn above their heads (`display.player_names`, `display.player_name_scale`). |
| `rasterizer/rasterizer_text.c`, `text/draw_string.c` | Text is drawn from an atlas of the fonts' glyphs, rasterized at the display's resolution (`src/text_hires.c`), when the font has every character of the string. Text can be drawn scaled about a point (`rasterizer_text_set_scale`), as the players' names are. Each glyph's advance is centred on the font tag character's, so the layout is the same, and a glyph is cut at a text box only where the font tag's character visibly was. |
| `rasterizer/rasterizer_text.c`, `text/draw_string.c` | Text is drawn from an atlas of the fonts' glyphs, rasterized at the resolution the game draws at (`src/text_hires.c`), when the font has every character of the string. Text can be drawn scaled about a point (`rasterizer_text_set_scale`), as the players' names are. Each glyph's advance is centred on the font tag character's, so the layout is the same, and a glyph is cut at a text box only where the font tag's character visibly was. |
The x86 inline assembly of the game is replaced by C. Thus the compiler
can optimize that code for each processor:
+231 -32
View File
@@ -117,6 +117,10 @@ enum
/* ---------- structures */
/* (how many things to pick up it gives up at once, and for how long) */
#define BOT_IGNORED_ITEMS 4
#define BOT_IGNORE_ITEM_TICKS 600
struct bot
{
long player_index;
@@ -141,8 +145,20 @@ struct bot
while on that account: following a person, a thing to pick up */
short path_failures;
long follow_again_time;
long ignored_item_index;
long ignored_item_time;
/* things to pick up given up for a while (no way to them, or there and
not taken: on a ledge, out of reach), and how long it has stood by the
one it goes for */
long ignored_items[BOT_IGNORED_ITEMS];
long ignored_item_times[BOT_IGNORED_ITEMS];
short ignored_item_next;
/* (the nearest it has come to the thing it goes for, and when) */
long item_progress_index;
real item_best_distance;
long item_best_time;
/* stuck again and again about the same place (as at the foot of a ramp
or behind its rail): how often since when, to give the goal up */
short stuck_count;
long stuck_count_time;
real wander_yaw;
short weave_phase;
/* where it is going, and the way there */
@@ -231,6 +247,7 @@ static struct
long switches;
long stucks;
long long_stucks;
long gave_up;
long fail_start;
long fail_goal;
long fail_search;
@@ -540,18 +557,40 @@ the level between them at its knees */
static boolean bot_clear_walk(
long unit_index,
real_point3d const *from,
real_point3d const *to)
real_point3d const *to,
boolean wide)
{
struct collision_result collision;
real_point3d start = *from;
real_vector3d vector;
real length;
short line;
start.z += 0.3f;
vector.i = to->x - from->x;
vector.j = to->y - from->y;
vector.k = to->z - from->z;
return !collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
&start, &vector, unit_index, &collision);
length = (real)sqrt(vector.i * vector.i + vector.j * vector.j);
/* (at its knees and at its chest: a rail with room under it, a slit in
a wall, are not ways through; wide, along its shoulders too: a door it
would pass the middle of but catch its frame) */
for (line = 0; line < (wide && length > 0.001f ? 4 : 2); line++)
{
real_point3d start = *from;
start.z += line == 1 ? 1.0f : line ? 0.6f : 0.3f;
if (line >= 2)
{
real side = line == 2 ? 0.35f : -0.35f;
start.x += -vector.j / length * side;
start.y += vector.i / length * side;
}
if (collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
&start, &vector, unit_index, &collision))
{
return FALSE;
}
}
return TRUE;
}
static boolean bot_next_point_now(
@@ -592,7 +631,7 @@ static boolean bot_next_point_now(
real dy = bot->goal.y - position->y;
if (dx * dx + dy * dy < 3.0f * 3.0f && fabsf(bot->goal.z - position->z) < 0.6f &&
bot_clear_walk(unit_index, position, &bot->goal))
bot_clear_walk(unit_index, position, &bot->goal, FALSE))
{
*point = bot->goal;
return TRUE;
@@ -620,7 +659,7 @@ static boolean bot_next_point_now(
/* (on its own level only: one above may be in view over a
ledge that a ramp leads round to) */
if (fabsf(next->z - position->z) < 0.6f && bot_clear_walk(unit_index, position, next))
if (fabsf(next->z - position->z) < 0.6f && bot_clear_walk(unit_index, position, next, TRUE))
{
bot->path.step_index++;
if (bot->path.step_index >= bot->path.step_count)
@@ -642,17 +681,20 @@ static boolean bot_next_point_now(
return TRUE;
}
/* a goal when none is in sight: the nearest enemy, or somewhere near
someone at random */
/* a goal when none is in sight: an enemy, or somewhere near someone at
random. Not all the nearest: each bot one of the three nearest (by its
number), and while it is far, a few metres beside them, so that a team
spreads out over the ways there instead of going as one crowd */
static void bot_choose_goal(
struct bot *bot,
struct player_datum const *player,
real_point3d const *position)
{
boolean teams = game_engine_has_teams();
real best_distance = 1000000.0f;
real best_distance[3] = { 1000000.0f, 1000000.0f, 1000000.0f };
real_point3d best_origin[3];
short found = 0;
short index;
boolean found = FALSE;
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
{
@@ -660,6 +702,7 @@ static void bot_choose_goal(
long unit_index;
real_point3d origin;
real distance;
short rank;
if (!other || other == player || (teams && other->team_index == player->team_index))
continue;
@@ -668,14 +711,38 @@ static void bot_choose_goal(
continue;
object_get_origin(unit_index, &origin);
distance = distance3d(position, &origin);
if (distance < best_distance)
/* (kept in order, nearest first) */
for (rank = 0; rank < 3 && distance >= best_distance[rank]; rank++)
;
if (rank == 3)
continue;
found = (short)MIN(found + 1, 3);
{
best_distance = distance;
bot->goal = origin;
found = TRUE;
short move;
for (move = 2; move > rank; move--)
{
best_distance[move] = best_distance[move - 1];
best_origin[move] = best_origin[move - 1];
}
}
best_distance[rank] = distance;
best_origin[rank] = origin;
}
if (found)
{
short pick = (short)(DATUM_INDEX_TO_ABSOLUTE_INDEX(bot->player_index) % found);
bot->goal = best_origin[pick];
if (best_distance[pick] > 25.0f)
{
real angle = (real)DATUM_INDEX_TO_ABSOLUTE_INDEX(bot->player_index) * 2.4f;
bot->goal.x += (real)cos(angle) * 6.0f;
bot->goal.y += (real)sin(angle) * 6.0f;
}
}
if (!found)
else
{
/* (no enemy alive: a few metres off, somewhere) */
real angle = bot_random() * 2.0f * _pi;
@@ -817,7 +884,59 @@ static real bot_item_worth(
}
/* the thing most worth going for near the bot, or NONE */
static boolean bot_item_ignored(
struct bot const *bot,
long item_index)
{
short index;
for (index = 0; index < BOT_IGNORED_ITEMS; index++)
{
if (bot->ignored_items[index] == item_index &&
game_time_get() - bot->ignored_item_times[index] < BOT_IGNORE_ITEM_TICKS &&
game_time_get() >= bot->ignored_item_times[index])
{
return TRUE;
}
}
return FALSE;
}
static void bot_ignore_item(
struct bot *bot,
long item_index)
{
bot->ignored_items[bot->ignored_item_next] = item_index;
bot->ignored_item_times[bot->ignored_item_next] = game_time_get();
bot->ignored_item_next = (short)((bot->ignored_item_next + 1) % BOT_IGNORED_ITEMS);
bot->item_progress_index = NONE;
}
/* going for a thing to pick up: whether to give it up, coming no nearer
(half a metre) for two and a half seconds: out of reach, past a rail, up
where no way it finds goes, or in a crowd */
static boolean bot_item_out_of_reach(
struct bot *bot,
real distance)
{
if (bot->item_progress_index != bot->item_index || game_time_get() < bot->item_best_time)
{
bot->item_progress_index = bot->item_index;
bot->item_best_distance = distance;
bot->item_best_time = game_time_get();
return FALSE;
}
if (distance < bot->item_best_distance - 0.5f)
{
bot->item_best_distance = distance;
bot->item_best_time = game_time_get();
return FALSE;
}
return game_time_get() - bot->item_best_time > 75;
}
static long bot_find_item(
struct bot const *bot,
struct unit_datum const *unit,
real_point3d const *position,
real range,
@@ -840,7 +959,7 @@ static long bot_find_item(
if (distance > range)
continue;
worth = bot_item_worth(unit, iterator.index);
if (worth <= 0.0f || worth < least_worth)
if (worth <= 0.0f || worth < least_worth || bot_item_ignored(bot, iterator.index))
continue;
/* (the strong ones worth a longer way) */
score = worth * worth / (1.0f + distance / 15.0f);
@@ -2393,7 +2512,7 @@ static void bot_think(
game_time_get() < bot->item_look_time))
{
bot->item_look_time = game_time_get();
bot->item_index = bot_find_item(unit, &position, 10.0f, 4.0f);
bot->item_index = bot_find_item(bot, unit, &position, 10.0f, 4.0f);
}
if (bot->item_index != NONE && !bot->carrying && !bot->has_objective && object_try_and_get(bot->item_index) &&
bot_item_worth(unit, bot->item_index) > 0.0f)
@@ -2408,6 +2527,11 @@ static void bot_think(
bot_walk_to(action, bot->yaw, &position, &point);
if (distance3d(&position, &item_position) < 2.0f)
bot_take_item(bot, player, unit, action);
if (bot_item_out_of_reach(bot, distance3d(&position, &item_position)))
{
bot_ignore_item(bot, bot->item_index);
bot->item_index = NONE;
}
}
/* closes on it along its way, weaving, until near */
else if (bot->has_objective && bot->objective_unit_index != bot->target_unit_index)
@@ -2497,11 +2621,9 @@ static void bot_think(
/* something worth having near: there first */
else if (game_time_get() - bot->item_look_time >= ITEM_LOOK_TICKS || game_time_get() < bot->item_look_time)
{
long item = bot_find_item(unit, &position, BOT_ITEM_RANGE, 0.0f);
long item = bot_find_item(bot, unit, &position, BOT_ITEM_RANGE, 0.0f);
bot->item_look_time = game_time_get();
if (item == bot->ignored_item_index && game_time_get() - bot->ignored_item_time < 600)
item = NONE;
if (item != bot->item_index)
{
bot->item_index = item;
@@ -2519,8 +2641,7 @@ static void bot_think(
/* (taken or gone, or no way to it: on) */
if (bot->path_failures >= 3)
{
bot->ignored_item_index = bot->item_index;
bot->ignored_item_time = game_time_get();
bot_ignore_item(bot, bot->item_index);
bot->path_failures = 0;
bot->item_index = NONE;
bot->has_goal = FALSE;
@@ -2536,6 +2657,14 @@ static void bot_think(
bot->goal_time = game_time_get();
if (distance3d(&position, &bot->goal) < 2.0f)
bot_take_item(bot, player, unit, action);
/* (no nearer a while: out of reach, as on a ledge, or not
offered: given up for a while) */
if (bot_item_out_of_reach(bot, distance3d(&position, &bot->goal)))
{
bot_ignore_item(bot, bot->item_index);
bot->item_index = NONE;
bot->has_goal = FALSE;
}
}
}
/* (in a team, every third bot with nothing to do keeps near the
@@ -2590,7 +2719,15 @@ static void bot_think(
bot->path_failures = 0;
}
wanted_yaw = bot->wander_yaw;
if (bot_next_point(bot, player->unit_index, &position, &point))
/* (holding a spot and near it: there, not pressing on into what may
stand on it, a pillar, a wall) */
if (bot->holding && bot->has_objective &&
(bot->goal.x - position.x) * (bot->goal.x - position.x) +
(bot->goal.y - position.y) * (bot->goal.y - position.y) < 1.5f * 1.5f)
{
bot->walking = FALSE;
}
else if (bot_next_point(bot, player->unit_index, &position, &point))
{
real dx = point.x - position.x;
real dy = point.y - position.y;
@@ -2680,7 +2817,8 @@ static void bot_think(
boolean progress = distance < bot->walk_distance - 0.015f || moved > 0.06f;
bot->walk_distance = MIN(bot->walk_distance, distance);
if (progress && moved > 0.02f)
/* (at the point, as on a thing to pick up: there, not stuck) */
if ((progress && moved > 0.02f) || distance < 0.5f)
{
bot->stuck_ticks = 0;
}
@@ -2689,8 +2827,27 @@ static void bot_think(
bot->stuck_ticks++;
if (bot->stuck_ticks == 6)
{
bot->sidestep = bot_random() < 0.5f ? 1.0f : -1.0f;
/* (to the side that is open, as round a pillar; either, at
random: left, the strafe's positive, bot_walk_to) */
real_point3d left = position, right = position;
boolean left_open, right_open;
left.x += -(real)sin(bot->yaw) * 1.2f;
left.y += (real)cos(bot->yaw) * 1.2f;
right.x -= -(real)sin(bot->yaw) * 1.2f;
right.y -= (real)cos(bot->yaw) * 1.2f;
left_open = bot_clear_walk(player->unit_index, &position, &left, FALSE);
right_open = bot_clear_walk(player->unit_index, &position, &right, FALSE);
bot->sidestep = left_open != right_open ? (left_open ? 1.0f : -1.0f) :
bot_random() < 0.5f ? 1.0f : -1.0f;
bot->sidestep_ticks = 10;
/* (the point a little above and near, up a step or the lip of
a ramp: a jump on towards it, not a step aside) */
if (bot->walk_point.z - position.z > 0.3f && distance < 3.0f)
{
bot->sidestep_ticks = 0;
SET_FLAG(action->control_flags, _unit_control_jump_bit, TRUE);
}
/* (near a point of its path it cannot reach, at a corner: the
next one) */
if (distance < 1.5f && bot->path.valid && bot->path.step_index < bot->path.step_count)
@@ -2699,6 +2856,38 @@ static void bot_think(
bot->sidestep_ticks = 4;
}
bots.stucks++;
/* (the third time in five seconds: the goal given up, the
thing to pick up for a while, and somewhere else, away from
where it could not get) */
if (game_time_get() - bot->stuck_count_time > 150 || game_time_get() < bot->stuck_count_time)
{
bot->stuck_count = 0;
bot->stuck_count_time = game_time_get();
}
if (++bot->stuck_count >= 3)
{
real away = (real)atan2(position.y - bot->walk_point.y, position.x - bot->walk_point.x) +
(bot_random() - 0.5f) * 1.5f;
bot->stuck_count = 0;
if (bot->item_index != NONE)
{
bot_ignore_item(bot, bot->item_index);
bot->item_index = NONE;
}
if (bot->holding)
bot->path_failures = 3;
else if (!bot->has_objective)
{
bot->goal = position;
bot->goal.x += (real)cos(away) * 10.0f;
bot->goal.y += (real)sin(away) * 10.0f;
bot->has_goal = TRUE;
bot->goal_time = game_time_get();
bot->follow_again_time = game_time_get() + 150;
}
bots.gave_up++;
}
bot->path_time = game_time_get() - PATH_REFRESH_TICKS;
bot->walk_distance = 1000000.0f;
@@ -2707,6 +2896,10 @@ static void bot_think(
SET_FLAG(action->control_flags, _unit_control_jump_bit, TRUE);
if (bot->stuck_ticks == 24)
bots.long_stucks++;
/* (a spot to hold it cannot get to: the place's centre instead,
bot_hold_spot) */
if (bot->stuck_ticks == 18 && bot->holding)
bot->path_failures = 3;
}
}
else
@@ -2735,7 +2928,13 @@ static struct bot *bot_get(
free_bot->player_index = player_index;
free_bot->target_unit_index = NONE;
free_bot->item_index = NONE;
free_bot->ignored_item_index = NONE;
free_bot->item_progress_index = NONE;
{
short ignored;
for (ignored = 0; ignored < BOT_IGNORED_ITEMS; ignored++)
free_bot->ignored_items[ignored] = NONE;
}
free_bot->vehicle_index = NONE;
free_bot->ignored_vehicle_index = NONE;
free_bot->step_radius = BOT_STEP_RADIUS;
@@ -2823,12 +3022,12 @@ void bots_tick(
}
}
bots.logged_time = game_time_get();
platform_log("bots: driving %d; paths found %ld, not %ld; weapon swaps asked %ld; grenades %ld; changes %ld; stuck %ld, long %ld (no start %ld, no goal %ld, no way %ld); vehicles: in %ld, out %ld",
(int)count, bots.paths_found, bots.paths_failed, bots.pickups, bots.grenades, bots.switches, bots.stucks, bots.long_stucks, bots.fail_start, bots.fail_goal, bots.fail_search,
platform_log("bots: driving %d; paths found %ld, not %ld; weapon swaps asked %ld; grenades %ld; changes %ld; stuck %ld, long %ld, gave up %ld (no start %ld, no goal %ld, no way %ld); vehicles: in %ld, out %ld",
(int)count, bots.paths_found, bots.paths_failed, bots.pickups, bots.grenades, bots.switches, bots.stucks, bots.long_stucks, bots.gave_up, bots.fail_start, bots.fail_goal, bots.fail_search,
bots.vehicle_entries, bots.vehicle_exits);
bots.vehicle_entries = bots.vehicle_exits = 0;
bots.fail_start = bots.fail_goal = bots.fail_search = 0;
bots.paths_found = bots.paths_failed = bots.pickups = bots.grenades = bots.switches = bots.stucks = bots.long_stucks = 0;
bots.paths_found = bots.paths_failed = bots.pickups = bots.grenades = bots.switches = bots.stucks = bots.long_stucks = bots.gave_up = 0;
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
{
struct player_datum *player = distributed_player(index);
+677
View File
@@ -0,0 +1,677 @@
/*
COOP_ENEMIES.C
Network co-op's extra enemies: Server Setup's EXTRA ENEMIES
(network.coop_enemies_mode). PER PLAYER (network.coop_enemies, a
percentage): for each player past the first, each squad of the players'
enemies that a level places (encounters.c's encounter_create) gets that
much of its count more; at 100%, two players meet twice the squad, four
players four times it. STATIC MULTIPLIER (network.coop_enemies_multiplier):
each squad is that many times as large, for any number of players.
Extra enemies are placed in widening rings around the squad's starting
locations, on a spot on the same floor, reachable without passing through
a wall, crate or machine, with room to stand and nobody already there. A
starting location with no room left passes its enemy to the squad's next
one; if none has room, the enemy goes where extra enemies went before free
ground was looked for (rings around the starting location, whoever stands
there), so a squad still gets every enemy the setting asks for.
A squad a script loads into a dropship (vehicle_load_magic) can be larger
than the dropship's seats. The riders left without a seat are erased, and
kept: once the dropship's own riders get out (vehicle_unload, or any other
way), they are placed beside those riders, a few ticks apart, and drop and
fight as they do. A dropship gone first takes them with it.
Only the host runs the AI, so all of this is the host's.
*/
#include "cseries.h"
#include "ai/actor_placement.h"
#include "ai/actors.h"
#include "ai/ai_scenario_definitions.h"
#include "ai/encounters.h"
#include "game/game.h"
#include "game/game_allegiance.h"
#include "game/players.h"
#include "networking/network_game_globals.h"
#include "networking/network_game_manager.h"
#include "objects/objects.h"
#include "physics/collisions.h"
#include "scenario/scenario.h"
#include "scenario/scenario_definitions.h"
#include "units/units.h"
#include "units/unit_definitions.h"
#include "coop_enemies.h"
#include "network_coop.h"
#include <math.h>
#include <string.h>
/* port_config.c's */
long config_integer(char const *name);
char const *config_string(char const *name);
/* ---------- constants */
enum
{
/* the actors a level keeps for its own placements, which the extra
enemies never take: as many as the Xbox's whole pool */
COOP_ENEMIES_LEVEL_ACTORS = 256,
/* EXTRA ENEMIES' amounts, as Server Setup has them: 25 to 200% per
player, or 2 to 32 times (none is its NONE) */
COOP_ENEMIES_MINIMUM_PERCENT = 25,
COOP_ENEMIES_MAXIMUM_PERCENT = 200,
COOP_ENEMIES_MINIMUM_MULTIPLIER = 2,
COOP_ENEMIES_MAXIMUM_MULTIPLIER = 32,
/* the rings tried around a starting location, the first holding
SPREAD_PLACES_PER_RING places and each further one that many more */
SPREAD_RINGS = 5,
SPREAD_PLACES_PER_RING = 6,
/* the rings coop_enemies_fallback_position tries, a place on each */
FALLBACK_RINGS = 8,
MAXIMUM_RIDING_VEHICLES = 32,
MAXIMUM_SEATED_RIDERS = 16,
MAXIMUM_KEPT_RIDERS = 512,
/* a kept rider placed every this many ticks, as the seated ones
jumped out */
RIDER_RELEASE_TICKS = 4,
};
/* (coop_enemies.h's count of the places on all the rings) */
typedef char coop_enemies_spread_spots_assert[
COOP_ENEMIES_SPREAD_SPOTS == SPREAD_PLACES_PER_RING * SPREAD_RINGS * (SPREAD_RINGS + 1) / 2 ? 1 : -1];
/* distance between rings */
#define SPREAD_SPACING 0.8f
/* height above the floor at which the way to a spot is checked */
#define SPREAD_STEP_HEIGHT 0.6f
/* how far a spot's floor may be above or below the starting location's:
a step, not a ledge */
#define SPREAD_FLOOR_DIFFERENCE 0.5f
/* room a spot needs above its floor */
#define SPREAD_HEADROOM 1.6f
/* how close across the ground a spot may be to a biped already there
(about two bipeds' collision radius), and how far around it to look */
#define SPREAD_CLEARANCE 0.6f
#define SPREAD_SEARCH_RADIUS 1.0f
/* how far below a fallback place the ground is looked for */
#define FALLBACK_GROUND_DEPTH 2.0f
/* a kept rider's place beside a rider who got out, each a little apart */
#define RIDER_SPACING 0.5f
/* EXTRA ENEMIES' choices (network.coop_enemies_mode's values) */
enum
{
_coop_enemies_none,
_coop_enemies_per_player,
_coop_enemies_multiplier,
};
/* ---------- structures */
/* a vehicle a script loaded riders into: its seated ones, and whether they
have begun to get out */
struct coop_riding_vehicle
{
long vehicle_index;
long seated_unit_indices[MAXIMUM_SEATED_RIDERS];
short seat_indices[MAXIMUM_SEATED_RIDERS];
short seated_count;
boolean releasing;
short released_count;
long next_release_time;
};
/* a rider left without a seat, to place once its vehicle's get out */
struct coop_kept_rider
{
long vehicle_index;
long variant_definition_index;
long encounter_index;
short squad_index;
};
/* ---------- globals */
static struct
{
/* EXTRA ENEMIES, as the game began with it: its choice, and the amount
of PER PLAYER (a percentage) and of STATIC MULTIPLIER */
short mode;
short percent;
short multiplier;
struct coop_riding_vehicle vehicles[MAXIMUM_RIDING_VEHICLES];
short vehicle_count;
struct coop_kept_rider riders[MAXIMUM_KEPT_RIDERS];
short rider_count;
} coop_enemies;
/* ---------- private code */
static boolean coop_enemies_host(
void)
{
return game_connection() == _game_connection_network_server && network_coop_active();
}
/* whether this game has extra enemies (it is a co-op host's, with some
chosen) */
static boolean coop_enemies_on(
void)
{
return coop_enemies.mode != _coop_enemies_none && coop_enemies_host();
}
/* the players in the game, every machine's: the network game's (a level
places its first enemies before the players' units are made, as it loads:
game_initialize_for_new_map) */
static short coop_enemies_player_count(
void)
{
struct network_game *game = network_game_get_game();
short count = 0;
short index;
for (index = 0; game && index < HALO_PORT_MAXIMUM_NETWORK_PLAYERS; index++)
{
if (network_player_is_valid(&game->players[index]))
count++;
}
return count;
}
/* what a spot is tested against: the level, and the objects that block
the way (crates and other scenery, machines, vehicles) */
#define SPREAD_COLLISION_FLAGS (FLAG(_collision_test_structure_bit) | FLAG(_collision_test_objects_bit) | \
_collision_test_objects_sight_blocking_flags | FLAG(_collision_test_front_facing_surfaces_bit) | \
FLAG(_collision_test_back_facing_surfaces_bit))
/* whether anything is in the way from `from`, along `vector` */
static boolean coop_enemies_blocked(
real_point3d const *from,
real_vector3d const *vector)
{
struct collision_result collision;
return collision_test_vector(SPREAD_COLLISION_FLAGS, from, vector, NONE, &collision);
}
/* the floor under a point, looked for from SPREAD_STEP_HEIGHT above it to
SPREAD_FLOOR_DIFFERENCE below it */
static boolean coop_enemies_floor(
real_point3d const *point,
real_point3d *floor)
{
struct collision_result collision;
real_point3d from = *point;
real_vector3d down = { 0.0f, 0.0f, -(2.0f * SPREAD_STEP_HEIGHT + SPREAD_FLOOR_DIFFERENCE) };
from.z += SPREAD_STEP_HEIGHT;
if (!collision_test_vector(SPREAD_COLLISION_FLAGS, &from, &down, NONE, &collision))
return FALSE;
*floor = collision.point;
return TRUE;
}
/* whether a biped stands within SPREAD_CLEARANCE of a spot on the floor,
or the spot is inside a vehicle */
static boolean coop_enemies_occupied(
real_point3d const *floor)
{
real_point3d body = { floor->x, floor->y, floor->z + SPREAD_STEP_HEIGHT };
struct location location;
long object_indices[32];
short object_count;
short index;
scenario_location_from_point(&location, &body);
if (location.cluster_index == NONE)
return TRUE;
object_count = objects_in_sphere(0, _object_mask_biped | _object_mask_vehicle, &location, &body,
SPREAD_SEARCH_RADIUS, object_indices, NUMBEROF(object_indices));
for (index = 0; index < object_count; index++)
{
struct object_datum *object = object_get(object_indices[index]);
if (object->object.type == _object_type_vehicle)
{
if (point_in_sphere(&body, &object->object.bounding_sphere_center, object->object.bounding_sphere_radius))
return TRUE;
}
else
{
real dx = object->object.position.x - floor->x;
real dy = object->object.position.y - floor->y;
if (dx * dx + dy * dy < SPREAD_CLEARANCE * SPREAD_CLEARANCE)
return TRUE;
}
}
return FALSE;
}
/* Whether an enemy can stand at `spot`: reachable from `from` (beside the
starting location) with nothing in the way, a floor within a step of
`floor_height` (the starting location's), room above it, and nobody there.
The floor goes in `position`. */
static boolean coop_enemies_spot_free(
real_point3d const *from,
real_point3d const *spot,
real floor_height,
real_point3d *position)
{
/* (level, at step height: down to the starting location's floor, a spot
whose floor is a little higher would end the look inside it) */
real_vector3d way = { spot->x - from->x, spot->y - from->y, 0.0f };
real_vector3d up = { 0.0f, 0.0f, SPREAD_HEADROOM };
real_point3d floor;
real_point3d feet;
if (coop_enemies_blocked(from, &way) || !coop_enemies_floor(spot, &floor) ||
fabsf(floor.z - floor_height) > SPREAD_FLOOR_DIFFERENCE)
{
return FALSE;
}
/* (headroom is checked from just above the floor, so the floor itself
doesn't count as in the way) */
feet = floor;
feet.z += 0.1f;
if (coop_enemies_blocked(&feet, &up) || coop_enemies_occupied(&floor))
return FALSE;
*position = floor;
return TRUE;
}
static struct coop_riding_vehicle *coop_enemies_riding_vehicle(
long vehicle_index,
boolean create)
{
short index;
for (index = 0; index < coop_enemies.vehicle_count; index++)
{
if (coop_enemies.vehicles[index].vehicle_index == vehicle_index)
return &coop_enemies.vehicles[index];
}
if (!create || coop_enemies.vehicle_count >= MAXIMUM_RIDING_VEHICLES)
return NULL;
csmemset(&coop_enemies.vehicles[coop_enemies.vehicle_count], 0, sizeof(coop_enemies.vehicles[0]));
coop_enemies.vehicles[coop_enemies.vehicle_count].vehicle_index = vehicle_index;
return &coop_enemies.vehicles[coop_enemies.vehicle_count++];
}
/* the riders kept for a vehicle forgotten, and the vehicle with them */
static void coop_enemies_forget_vehicle(
short vehicle_number)
{
long vehicle_index = coop_enemies.vehicles[vehicle_number].vehicle_index;
short index, kept = 0;
for (index = 0; index < coop_enemies.rider_count; index++)
{
if (coop_enemies.riders[index].vehicle_index != vehicle_index)
coop_enemies.riders[kept++] = coop_enemies.riders[index];
}
coop_enemies.rider_count = kept;
coop_enemies.vehicles[vehicle_number] = coop_enemies.vehicles[--coop_enemies.vehicle_count];
}
/* whether a vehicle's seated riders have begun to get out: one alive and
out of it (one killed in its seat is not getting out) */
static boolean coop_enemies_riders_getting_out(
struct coop_riding_vehicle const *vehicle)
{
short index;
for (index = 0; index < vehicle->seated_count; index++)
{
struct object_datum *unit = object_try_and_get_and_verify_type(vehicle->seated_unit_indices[index],
_object_mask_unit);
if (unit && !TEST_FLAG(unit->object.damage_flags, _object_dead_bit) &&
unit->object.parent_object_index != vehicle->vehicle_index)
{
return TRUE;
}
}
return FALSE;
}
/* where a vehicle's `number`th kept rider is placed: beside one of the
riders who got out of it (where it is now: out of the vehicle, in the
open), a ring further about them each round; else at a seat's marker, else
the vehicle's place */
static void coop_enemies_rider_place(
struct coop_riding_vehicle const *vehicle,
short number,
real_point3d *position,
real *facing)
{
struct object_datum *object = object_get(vehicle->vehicle_index);
struct unit_definition *definition = unit_definition_get(object->definition_index);
long out_unit_indices[MAXIMUM_SEATED_RIDERS];
short out_count = 0;
short index, round;
real angle;
real_point3d spread;
*position = object->object.position;
*facing = (real)atan2(object->object.forward.j, object->object.forward.i);
for (index = 0; index < vehicle->seated_count; index++)
{
struct object_datum *unit = object_try_and_get_and_verify_type(vehicle->seated_unit_indices[index],
_object_mask_unit);
if (unit && !TEST_FLAG(unit->object.damage_flags, _object_dead_bit) &&
unit->object.parent_object_index != vehicle->vehicle_index)
{
out_unit_indices[out_count++] = vehicle->seated_unit_indices[index];
}
}
if (out_count > 0)
{
*position = object_get(out_unit_indices[number % out_count])->object.position;
round = (short)(number / out_count);
}
else
{
struct object_marker marker;
round = number;
if (vehicle->seated_count > 0 && VALID_INDEX(vehicle->seat_indices[0], definition->unit.seats.count))
{
struct unit_seat *seat = TAG_BLOCK_GET_ELEMENT(&definition->unit.seats, vehicle->seat_indices[0],
struct unit_seat);
if (seat->marker_name[0] &&
object_get_marker_by_name(vehicle->vehicle_index, seat->marker_name, &marker, 1) > 0)
{
*position = marker.matrix.position;
}
}
}
/* free ground around that spot, as the squad's own extra enemies get;
failing that, a ring around it, further out each round */
if (coop_enemies_spread_position(position, (short)(number + 1), NULL, &spread))
{
*position = spread;
return;
}
angle = (real)number * (_pi * 2.0f / SPREAD_PLACES_PER_RING);
position->x += (real)cos(angle) * RIDER_SPACING * (real)(round / SPREAD_PLACES_PER_RING + 1);
position->y += (real)sin(angle) * RIDER_SPACING * (real)(round / SPREAD_PLACES_PER_RING + 1);
}
/* ---------- public code */
void coop_enemies_new_game(
void)
{
char const *mode = config_string("network.coop_enemies_mode");
csmemset(&coop_enemies, 0, sizeof(coop_enemies));
/* (an amount past Server Setup's, as it shows it: its nearest choice's
end) */
coop_enemies.percent = (short)PIN(config_integer("network.coop_enemies"), COOP_ENEMIES_MINIMUM_PERCENT,
COOP_ENEMIES_MAXIMUM_PERCENT);
coop_enemies.multiplier = (short)PIN(config_integer("network.coop_enemies_multiplier"),
COOP_ENEMIES_MINIMUM_MULTIPLIER, COOP_ENEMIES_MAXIMUM_MULTIPLIER);
coop_enemies.mode = !strcmp(mode, "per_player") ? _coop_enemies_per_player :
!strcmp(mode, "multiplier") ? _coop_enemies_multiplier : _coop_enemies_none;
}
void coop_enemies_reset(
void)
{
coop_enemies.vehicle_count = 0;
coop_enemies.rider_count = 0;
}
short coop_enemies_extra_count(
long encounter_index,
short count)
{
struct encounter_datum *encounter;
long extra, room;
short players;
if (count <= 0 || !coop_enemies_on())
return 0;
encounter = encounter_get(encounter_index);
if (!game_team_is_enemy(_game_team_player, encounter->team_index))
return 0;
if (coop_enemies.mode == _coop_enemies_multiplier)
extra = (long)count * (coop_enemies.multiplier - 1);
else
{
players = coop_enemies_player_count();
if (players <= 1)
return 0;
extra = ((long)count * coop_enemies.percent * (players - 1) + 50) / 100;
}
room = MAXIMUM_ACTORS - COOP_ENEMIES_LEVEL_ACTORS - actor_data->actual_count;
return (short)PIN(extra, 0, MAX(room, 0));
}
boolean coop_enemies_spread_position(
real_point3d const *origin,
short number,
long *taken,
real_point3d *position)
{
real_point3d from = *origin;
real_point3d floor;
real floor_height = coop_enemies_floor(origin, &floor) ? floor.z : origin->z;
short ring, first_spot = 0;
from.z = floor_height + SPREAD_STEP_HEIGHT;
for (ring = 0; ring < SPREAD_RINGS; ring++)
{
short place_count = (short)(SPREAD_PLACES_PER_RING * (ring + 1));
real radius = SPREAD_SPACING * (real)(ring + 1);
short place;
/* (each enemy starts at a different place on the ring, so a squad
spreads all the way round rather than filling one side first) */
for (place = 0; place < place_count; place++)
{
short around = (short)((number + place) % place_count);
short spot_index = (short)(first_spot + around);
real angle = (real)around * (_pi * 2.0f / (real)place_count);
real_point3d spot = from;
if (taken && BIT_VECTOR_TEST_FLAG(taken, spot_index))
continue;
spot.x += (real)cos(angle) * radius;
spot.y += (real)sin(angle) * radius;
spot.z = floor_height;
if (coop_enemies_spot_free(&from, &spot, floor_height, position))
return TRUE;
if (taken)
BIT_VECTOR_SET_FLAG(taken, spot_index, TRUE);
}
first_spot = (short)(first_spot + place_count);
}
return FALSE;
}
boolean coop_enemies_fallback_position(
real_point3d const *origin,
short number,
real_point3d *position)
{
real_point3d from = *origin;
short place = (short)((number - 1) % SPREAD_PLACES_PER_RING);
short ring = (short)((number - 1) / SPREAD_PLACES_PER_RING);
short attempt;
from.z += SPREAD_STEP_HEIGHT;
for (attempt = 0; attempt < FALLBACK_RINGS; attempt++, ring++)
{
/* (each ring turned half a place from the last, so its places fall
between the last's) */
real angle = ((real)place + 0.5f * (real)(ring % 2)) * (_pi * 2.0f / SPREAD_PLACES_PER_RING);
real radius = SPREAD_SPACING * (real)(ring + 1);
real_point3d to = from;
real_vector3d way = { 0.0f, 0.0f, 0.0f };
real_vector3d down = { 0.0f, 0.0f, -(SPREAD_STEP_HEIGHT + FALLBACK_GROUND_DEPTH) };
struct collision_result collision;
way.i = (real)cos(angle) * radius;
way.j = (real)sin(angle) * radius;
to.x += way.i;
to.y += way.j;
/* (the level only: the way open, and ground below) */
if (!collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit) |
FLAG(_collision_test_back_facing_surfaces_bit), &from, &way, NONE, &collision) &&
collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
&to, &down, NONE, &collision))
{
*position = collision.point;
return TRUE;
}
}
return FALSE;
}
void coop_enemies_rider_seated(
long vehicle_index,
long unit_index)
{
struct coop_riding_vehicle *vehicle;
struct unit_datum *unit;
if (!coop_enemies_on())
return;
vehicle = coop_enemies_riding_vehicle(vehicle_index, TRUE);
unit = unit_get(unit_index);
if (!vehicle)
return;
/* (a vehicle loaded again: the riders that got out of it before are not
this load's) */
{
short index, kept = 0;
for (index = 0; index < vehicle->seated_count; index++)
{
struct object_datum *seated = object_try_and_get_and_verify_type(vehicle->seated_unit_indices[index],
_object_mask_unit);
if (seated && seated->object.parent_object_index == vehicle_index)
{
vehicle->seated_unit_indices[kept] = vehicle->seated_unit_indices[index];
vehicle->seat_indices[kept++] = vehicle->seat_indices[index];
}
}
vehicle->seated_count = kept;
}
if (vehicle->seated_count >= MAXIMUM_SEATED_RIDERS)
return;
vehicle->seated_unit_indices[vehicle->seated_count] = unit_index;
vehicle->seat_indices[vehicle->seated_count] = unit->unit.parent_seat_index;
vehicle->seated_count++;
/* (a vehicle loaded again, after its riders got out: they get out again) */
vehicle->releasing = FALSE;
}
boolean coop_enemies_rider_unseated(
long vehicle_index,
long unit_index)
{
struct unit_datum *unit;
struct actor_datum *actor;
struct coop_kept_rider *rider;
struct coop_riding_vehicle *vehicle;
if (!coop_enemies_on() || coop_enemies.rider_count >= MAXIMUM_KEPT_RIDERS)
return FALSE;
unit = unit_get(unit_index);
vehicle = coop_enemies_riding_vehicle(vehicle_index, FALSE);
/* (kept only for a vehicle with riders seated, whose getting out places
it: one of a load that seated nobody is left where it is) */
if (unit->object.type != _object_type_biped || unit->unit.actor_index == NONE || !vehicle ||
vehicle->seated_count == 0)
{
return FALSE;
}
actor = actor_get(unit->unit.actor_index);
if (actor->meta.swarm || actor->meta.encounter_index == NONE ||
!game_team_is_enemy(_game_team_player, encounter_get(actor->meta.encounter_index)->team_index))
{
return FALSE;
}
rider = &coop_enemies.riders[coop_enemies.rider_count++];
rider->vehicle_index = vehicle_index;
rider->variant_definition_index = actor->meta.variant_definition_index;
rider->encounter_index = actor->meta.encounter_index;
rider->squad_index = actor->meta.squad_index;
return TRUE;
}
void coop_enemies_update(
void)
{
long now = game_time_get();
short vehicle_number;
if (!coop_enemies_host())
return;
for (vehicle_number = coop_enemies.vehicle_count - 1; vehicle_number >= 0; vehicle_number--)
{
struct coop_riding_vehicle *vehicle = &coop_enemies.vehicles[vehicle_number];
struct object_datum *object = object_try_and_get_and_verify_type(vehicle->vehicle_index, _object_mask_unit);
short index;
/* (the vehicle's next kept rider) */
for (index = 0; index < coop_enemies.rider_count; index++)
{
if (coop_enemies.riders[index].vehicle_index == vehicle->vehicle_index)
break;
}
/* (a vehicle gone, or destroyed, takes its kept riders with it; one
with none kept, every one loaded seated or all placed, is done with) */
if (!object || TEST_FLAG(object->object.damage_flags, _object_dead_bit) || index >= coop_enemies.rider_count)
{
coop_enemies_forget_vehicle(vehicle_number);
continue;
}
if (!vehicle->releasing)
{
if (!coop_enemies_riders_getting_out(vehicle))
continue;
vehicle->releasing = TRUE;
vehicle->released_count = 0;
vehicle->next_release_time = now;
}
if (now < vehicle->next_release_time)
continue;
{
struct coop_kept_rider rider = coop_enemies.riders[index];
struct encounter_definition *encounter_definition;
struct squad_definition *squad_definition;
struct actor_starting_location location;
coop_enemies.riders[index] = coop_enemies.riders[--coop_enemies.rider_count];
encounter_definition = TAG_BLOCK_GET_ELEMENT(&global_scenario_get()->ai_encounters,
DATUM_INDEX_TO_ABSOLUTE_INDEX(rider.encounter_index), struct encounter_definition);
squad_definition = TAG_BLOCK_GET_ELEMENT(&encounter_definition->squads, rider.squad_index,
struct squad_definition);
/* (as the squad's: its states, no command list of a starting
location's) */
csmemset(&location, 0, sizeof(location));
coop_enemies_rider_place(vehicle, vehicle->released_count++, &location.position, &location.facing);
location.cluster_index = NONE;
location.noncombat_sequence_id = NONE;
location.default_state = squad_definition->default_state;
location.initial_state = squad_definition->initial_state;
location.actor_variant_index = NONE;
location.command_list_index = NONE;
actor_place(rider.variant_definition_index, rider.encounter_index, rider.squad_index, &location, FALSE, 0);
encounter_update_status(rider.encounter_index);
}
vehicle->next_release_time = now + RIDER_RELEASE_TICKS;
}
}
+65
View File
@@ -0,0 +1,65 @@
/*
COOP_ENEMIES.H
Network co-op's extra enemies, for its players (coop_enemies.c).
*/
#ifndef __COOP_ENEMIES_H
#define __COOP_ENEMIES_H
/* network_coop.c: a game begins (its EXTRA ENEMIES read), or the host's
game state went back (a cutscene skipped): the dropships' riders kept are
forgotten */
void coop_enemies_new_game(
void);
void coop_enemies_reset(
void);
/* encounters.c: how many enemies more a squad placed with `count` gets
(none but on a co-op host, for an encounter of the players' enemies) */
short coop_enemies_extra_count(
long encounter_index,
short count);
/* the places coop_enemies_spread_position tries around a starting location */
#define COOP_ENEMIES_SPREAD_SPOTS 90
/* encounters.c: finds a spot for a squad's `number`th extra enemy
(counting from 1) around the starting location `origin`, on its floor,
clear of walls and objects, with room to stand and nobody there. FALSE if
there's no room. `taken` (COOP_ENEMIES_SPREAD_SPOTS bits, or NULL) marks the
places found taken, which stay so while the squad's enemies are placed: they
aren't tried again. */
boolean coop_enemies_spread_position(
real_point3d const *origin,
short number,
long *taken,
real_point3d *position);
/* encounters.c: where the `number`th extra enemy (counting from 1) goes
when no starting location has room: a place on rings around `origin` with
the way open and ground below, whoever stands there (as extra enemies were
placed before free ground was looked for), so the squad still gets every
enemy the setting asks for. FALSE if none (the starting location itself
then) */
boolean coop_enemies_fallback_position(
real_point3d const *origin,
short number,
real_point3d *position);
/* units.c's vehicle_scripting_load_magic: a rider seated, and one left
without a seat (the vehicle's seats taken: an extra enemy), which is kept,
off the map, until the vehicle's riders get out (TRUE: kept, the caller
erases it) */
void coop_enemies_rider_seated(
long vehicle_index,
long unit_index);
boolean coop_enemies_rider_unseated(
long vehicle_index,
long unit_index);
/* network_coop.c, after each of the host's ticks: the riders kept placed
at their vehicle's seats once its own riders get out */
void coop_enemies_update(
void);
#endif
+234
View File
@@ -0,0 +1,234 @@
/*
COOP_SCRIPTS.C
Campaign scripts made to work for every player in network co-op.
The campaign's scripts were written for one or two players. Only the host
runs them (game.c), against every player's unit, so most of what they do
already happens for everyone: a trigger volume tested against (players)
fires for whoever walks into it, and what it starts is sent to the clients
(network_coop.c). Three habits of the scripts don't carry over:
- Waiting for every player at once (volume_test_objects_all on the
players): the Maw's run to the Pillar of Autumn's bridge, a30's Pelican
pickups. Split-screen players stand together; network players don't, so
the wait would never end. In co-op one player arriving is enough.
- Waiting until it's safe (game_safe_to_save) before a cutscene: the
game's test fails while any enemy sees any player. In co-op one player
out of danger is enough.
- Naming players one at a time: the scripts reach the players through
player0 and player1 (the first two in the players list), to put them in
a Pelican, take them out of it, stage them for a cutscene or give them a
starting profile. Everyone past the second would be left out, so they
follow player0: wherever the scripts put player0, they go too.
*/
/* ---------- headers */
#include "cseries.h"
#include "ai/ai.h"
#include "game/game.h"
#include "game/players.h"
#include "hs/object_lists.h"
#include "objects/objects.h"
#include "objects/object_types.h"
#include "units/bipeds.h"
#include "units/units.h"
#include "coop_scripts.h"
#include "network_coop.h"
#include <string.h>
/* ---------- constants */
/* players the scripts name: player0 and player1 */
#define NAMED_PLAYERS 2
/* ---------- private code */
static boolean coop_scripts_host(
void)
{
return game_connection() == _game_connection_network_server && network_coop_active();
}
/* the seat label to look for when boarding a follower: player0's own up to
and including "rider" (Pelicans have "p-riderlf", "p-riderrf", "p-riderlb"
and so on), else up to the '-' ("b-driver" gives "b-") */
static void follower_seat_name(
char const *seat_name,
char *follower_seat,
size_t size)
{
char *rider;
char *dash;
/* (lower case: vehicle_scripting_find_available_seats compares it with
lowered seat labels) */
snprintf(follower_seat, size, "%s", seat_name);
strlwr(follower_seat);
rider = strstr(follower_seat, "rider");
dash = strchr(follower_seat, '-');
if (rider)
rider[strlen("rider")] = 0;
else if (dash)
dash[1] = 0;
}
/* ---------- public code */
boolean coop_scripts_any_player_will_do(
long object_list_index)
{
long reference_index;
long object_index;
boolean any = FALSE;
if (!coop_scripts_host())
return FALSE;
for (object_index = object_list_get_first(object_list_index, &reference_index);
object_index != NONE;
object_index = object_list_get_next(object_list_index, &reference_index))
{
if (player_index_from_unit_index(object_index) == NONE)
return FALSE;
any = TRUE;
}
return any;
}
/* the units of the players following player0, if `unit_index` is
player0's; how many */
static short players_following(
long unit_index,
long *unit_indices,
short maximum_count)
{
struct data_iterator iterator;
struct player_datum *player;
short position = 0;
short count = 0;
if (unit_index == NONE || !coop_scripts_host())
return 0;
/* (in the order (players) lists them: player0 is the first with a unit) */
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
if (player->unit_index == NONE)
continue;
if (position == 0 && player->unit_index != unit_index)
return 0;
if (position >= NAMED_PLAYERS && count < maximum_count)
unit_indices[count++] = player->unit_index;
position++;
}
return count;
}
void coop_scripts_teleport_followers(
long unit_index)
{
long followers[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
short count = players_following(unit_index, followers, NUMBEROF(followers));
short index;
for (index = 0; index < count; index++)
{
player_teleport(player_index_from_unit_index(followers[index]), unit_index,
&object_get(unit_index)->object.position);
}
}
void coop_scripts_suspend_followers(
long unit_index,
boolean suspended)
{
long followers[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
short count = players_following(unit_index, followers, NUMBEROF(followers));
short index;
for (index = 0; index < count; index++)
unit_scripting_suspended(followers[index], suspended);
}
void coop_scripts_exit_followers(
long unit_index,
long vehicle_index)
{
long followers[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
short count = players_following(unit_index, followers, NUMBEROF(followers));
short index;
for (index = 0; index < count; index++)
{
if (object_get(followers[index])->object.parent_object_index == vehicle_index)
unit_scripting_exit_vehicle(followers[index]);
}
}
void coop_scripts_board_followers(
long unit_index,
long vehicle_index,
char const *seat_name)
{
long followers[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
short count = players_following(unit_index, followers, NUMBEROF(followers));
char follower_seat[64];
short index;
if (count == 0 || vehicle_index == NONE)
return;
follower_seat_name(seat_name, follower_seat, sizeof(follower_seat));
/* (each list freed at once: there are few, and the scripts' own are only
collected after the tick's threads have run) */
for (index = 0; index < count; index++)
{
long list_index = object_list_new();
if (list_index == NONE)
break;
object_list_add(list_index, followers[index]);
vehicle_scripting_load_magic(vehicle_index, follower_seat, list_index);
object_list_delete(list_index);
}
}
boolean coop_scripts_safe_to_save(
void)
{
struct data_iterator iterator;
struct player_datum *player;
if (game_safe_to_save())
return TRUE;
if (!coop_scripts_host())
return FALSE;
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
struct biped_datum *biped = biped_try_and_get(player->unit_index);
if (biped && !TEST_FLAG(biped->object.damage_flags, _object_dead_bit) &&
!TEST_FLAG(biped->biped.flags, _biped_airborne_bit) && !ai_port_enemies_can_see_unit(player->unit_index))
{
return TRUE;
}
}
return FALSE;
}
void coop_scripts_player_add_equipment(
long unit_index,
short starting_profile_index,
boolean reset_equipment)
{
long followers[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
short count = players_following(unit_index, followers, NUMBEROF(followers));
short index;
player_add_equipment(unit_index, starting_profile_index, reset_equipment);
for (index = 0; index < count; index++)
player_add_equipment(followers[index], starting_profile_index, reset_equipment);
}
+55
View File
@@ -0,0 +1,55 @@
/*
COOP_SCRIPTS.H
Campaign scripts made to work for every player in network co-op
(coop_scripts.c).
*/
#ifndef __COOP_SCRIPTS_H
#define __COOP_SCRIPTS_H
/* ---------- prototypes */
/* hs_library_external.c's volume_test_objects_all: whether to count the
list as inside once any of it is, because every object in it is a player */
boolean coop_scripts_any_player_will_do(
long object_list_index);
/* What the scripts do to player0, done to the players they can't name too
(the "followers"). Each does nothing unless `unit_index` is player0's in a
co-op host's game. */
/* hs_library_external.c's object_teleport: around player0 */
void coop_scripts_teleport_followers(
long unit_index);
/* units.c's unit_suspended */
void coop_scripts_suspend_followers(
long unit_index,
boolean suspended);
/* units.c's unit_exit_vehicle: those riding player0's vehicle get out */
void coop_scripts_exit_followers(
long unit_index,
long vehicle_index);
/* units.c's vehicle_load_magic and unit_enter_vehicle: into the vehicle,
each in a free seat of the same kind as player0's (`seat_name`) */
void coop_scripts_board_followers(
long unit_index,
long vehicle_index,
char const *seat_name);
/* hs.c's game_safe_to_save, for the scripts: the game's own test, or in
co-op, any one player alive, on the ground and unseen by enemies. The
scripts wait on it before a cutscene, and with players spread out a fight
somewhere else would hold up the one a player has reached. Checkpoints
keep the game's own test. */
boolean coop_scripts_safe_to_save(
void);
/* hs.c's player_add_equipment, for the scripts: gives the starting profile
to player0's followers too */
void coop_scripts_player_add_equipment(
long unit_index,
short starting_profile_index,
boolean reset_equipment);
#endif
+285
View File
@@ -0,0 +1,285 @@
/*
COOP_SPECTATE.C
Spectating in network co-op. A dead player watches a living teammate
until they respawn (players.c respawns them beside a teammate once it is
safe). The campaign's dead camera would watch the body; instead this
follows a teammate from behind, and A (jump on the keyboard) switches to
the next one. director.c and hud.c call in here.
It also draws the line telling players about the vote to skip a cutscene
(network_coop.c counts the votes).
*/
#include "cseries.h"
#include "camera/dead_camera.h"
#include "game/game.h"
#include "game/game_engine.h"
#include "game/players.h"
#include "input/input.h"
#include "interface/hud_messaging.h"
#include "objects/objects.h"
#include "rasterizer/rasterizer.h"
#include "render/render.h"
#include "text/draw_string.h"
#include "text/font_group.h"
#include "text/unicode.h"
#include "units/units.h"
#include "coop_spectate.h"
#include "network_coop.h"
#include <math.h>
/* ---------- constants */
/* camera distance behind the unit, pitch (radians), and how much of the
turn toward the unit's facing it makes each frame */
#define SPECTATE_DISTANCE 3.5f
#define SPECTATE_PITCH -0.3f
#define SPECTATE_TURN 0.15f
/* a passenger (riding a Pelican) is watched from in front of them, above
and looking down, so the camera stays inside the vehicle; a wide view (the
observer allows up to 90 degrees) takes in all of them from that close */
#define SPECTATE_PASSENGER_DISTANCE 1.0f
#define SPECTATE_PASSENGER_PITCH -0.5f
#define SPECTATE_PASSENGER_FIELD_OF_VIEW DEGREES_TO_RADIANS(85.0f)
/* field of view for every other spectate view (dead_camera.c's default) */
#define SPECTATE_FIELD_OF_VIEW DEGREES_TO_RADIANS(70.0f)
/* a teammate the scripts hold (Pillar of Autumn's cryo tube) is watched from
in front, far enough back to see all of what holds them */
#define SPECTATE_HELD_DISTANCE 2.0f
#define SPECTATE_HELD_PITCH -0.1f
/* ---------- globals */
/* the player each local player is watching, or NONE */
static long coop_spectate_watched[MAXIMUM_LOCAL_PLAYERS] = { NONE, NONE, NONE, NONE };
/* input_abstraction.c: how many ticks the keyboard's jump key has been held */
byte input_abstraction_port_accept(short controller_index);
/* ---------- private code */
/* The next living player after `after` (or the first, if `after` is NONE),
skipping `self` and wrapping around. NONE if nobody else is alive. */
static long next_living_player(long self, long after)
{
struct data_iterator iterator;
struct player_datum *player;
long first = NONE;
boolean passed = after == NONE;
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
if (iterator.datum_index == after)
{
passed = TRUE;
continue;
}
if (iterator.datum_index == self || player->unit_index == NONE)
continue;
if (passed)
return iterator.datum_index;
if (first == NONE)
first = iterator.datum_index;
}
return first;
}
/* Returns TRUE once per press of A (or jump on the keyboard), however long
it is held. The buttons count ticks, and one tick can span several frames,
so testing "held" alone would switch more than once. */
static boolean next_pressed(short controller_index)
{
static boolean held[MAXIMUM_GAMEPADS];
struct gamepad_state const *gamepad;
boolean down;
boolean pressed;
if (controller_index < 0 || controller_index >= MAXIMUM_GAMEPADS)
return FALSE;
gamepad = input_get_gamepad_state(controller_index);
down = (gamepad && gamepad->buttons[FIRST_GAMEPAD_ANALOG_BUTTON + _gamepad_analog_button_a] > 0) ||
input_abstraction_port_accept(controller_index) > 0;
pressed = down && !held[controller_index];
held[controller_index] = down;
return pressed;
}
/* whether the unit rides in a vehicle it isn't driving */
static boolean unit_is_passenger(
long unit_index)
{
long vehicle_index = object_get(unit_index)->object.parent_object_index;
return vehicle_index != NONE && object_try_and_get_and_verify_type(vehicle_index, _object_mask_unit) &&
unit_get(vehicle_index)->unit.driver_object_index != unit_index;
}
/* ---------- public code */
boolean coop_spectating(
void)
{
return network_coop_active();
}
long coop_spectate_unit(
short local_player_index)
{
long self = local_player_get_player_index(local_player_index);
long *watched = &coop_spectate_watched[local_player_index];
struct player_datum *player;
boolean next;
if (self == NONE)
return NONE;
player = player_get(self);
/* checked every frame so a press is never missed or counted twice */
next = next_pressed(player->network_player_data.controller_index);
/* alive: keep `watched`, so the next death starts on the same teammate */
if (player->unit_index != NONE)
return NONE;
if (*watched == NONE || !player_try_and_get(*watched) || player_get(*watched)->unit_index == NONE || next)
{
*watched = next_living_player(self, *watched);
}
return *watched != NONE ? player_get(*watched)->unit_index : NONE;
}
boolean coop_spectate_watching_rider(
short local_player_index)
{
long self = local_player_get_player_index(local_player_index);
long watched = coop_spectate_watched[local_player_index];
long unit_index;
long driver_index;
if (self == NONE || player_get(self)->unit_index != NONE)
return FALSE;
/* the scripted camera may have been running since before this player
died, so nobody has been picked to watch yet */
if (watched == NONE || !player_try_and_get(watched) || player_get(watched)->unit_index == NONE)
watched = next_living_player(self, NONE);
unit_index = watched != NONE ? player_get(watched)->unit_index : NONE;
if (unit_index == NONE || !unit_is_passenger(unit_index))
return FALSE;
driver_index = unit_get(object_get(unit_index)->object.parent_object_index)->unit.driver_object_index;
return driver_index == NONE || unit_get(driver_index)->unit.player_index == NONE;
}
boolean coop_spectate_nothing_to_watch(
short local_player_index)
{
long self = local_player_get_player_index(local_player_index);
return self != NONE && player_get(self)->unit_index == NONE && next_living_player(self, NONE) == NONE;
}
void coop_spectate_camera(
struct dead_camera *camera)
{
struct object_datum *unit = camera->unit_index != NONE ? object_try_and_get(camera->unit_index) : NULL;
real yaw, turn;
if (!unit)
return;
yaw = (real)atan2(unit->object.forward.j, unit->object.forward.i);
if (unit_is_passenger(camera->unit_index))
{
/* fixed in front of the seat, turning with the vehicle */
camera->facing.yaw = yaw + _pi;
camera->facing.pitch = SPECTATE_PASSENGER_PITCH;
camera->distance = SPECTATE_PASSENGER_DISTANCE;
camera->field_of_view = SPECTATE_PASSENGER_FIELD_OF_VIEW;
return;
}
camera->field_of_view = SPECTATE_FIELD_OF_VIEW;
if (!player_input_enabled())
{
camera->facing.yaw = yaw + _pi;
camera->facing.pitch = SPECTATE_HELD_PITCH;
camera->distance = SPECTATE_HELD_DISTANCE;
return;
}
/* ease the camera round behind the unit */
turn = yaw - camera->facing.yaw;
while (turn > _pi)
turn -= 2.0f * _pi;
while (turn < -_pi)
turn += 2.0f * _pi;
camera->facing.yaw += turn * SPECTATE_TURN;
camera->facing.pitch = SPECTATE_PITCH;
camera->distance = SPECTATE_DISTANCE;
}
/* centred text near the bottom of the screen, in the HUD's font */
static void draw_bottom_text(
wchar_t const *text)
{
long font_index = hud_get_font_index();
struct font_header *font;
real_argb_color color = *global_real_argb_white;
rectangle2d bounds;
short height;
if (font_index == NONE)
return;
font = font_definition_get(font_index);
height = (short)(font->ascending_height + font->descending_height);
bounds = render.camera.window_bounds;
bounds.y0 = (short)(bounds.y1 - 3 * height - 24);
bounds.y1 = (short)(bounds.y1 - 24);
/* draw mode 2: centered */
draw_string_set_draw_mode(font_index, NONE, 2, 0, &color);
rasterizer_draw_unicode_string(&bounds, NULL, NULL, 0, text);
}
void coop_spectate_draw(
short local_player_index)
{
long watched = coop_spectate_watched[local_player_index];
wchar_t text[96];
wchar_t const *hint;
/* nobody to watch yet (still joining): network_coop.c shows the host's
view from behind instead */
if (watched == NONE || !player_try_and_get(watched))
{
if (coop_spectate_nothing_to_watch(local_player_index))
draw_bottom_text(L"JOINING THE GAME\r\nYou spawn beside a teammate once it is safe");
return;
}
if (!players_coop_waiting_to_start(local_player_get_player_index(local_player_index)))
hint = L"You come back beside them once it is safe";
else
hint = L"You join them when they're on foot";
usnprintf(text, NUMBEROF(text) - 1, L"SPECTATING %.12s (A: NEXT)\r\n%s", player_get(watched)->name, hint);
text[NUMBEROF(text) - 1] = 0;
draw_bottom_text(text);
}
void coop_skip_vote_draw(
short local_player_index)
{
short votes, voters;
boolean voted;
wchar_t text[96];
/* once, for the first local player */
if (local_player_index != local_player_get_next(NONE) || !network_coop_skip_vote_status(&votes, &voters, &voted))
return;
if (voted)
usnprintf(text, NUMBEROF(text) - 1, L"VOTED TO SKIP %d OF %d", votes, voters);
else
usnprintf(text, NUMBEROF(text) - 1, L"PRESS SPACE OR A TO VOTE TO SKIP %d OF %d", votes, voters);
text[NUMBEROF(text) - 1] = 0;
draw_bottom_text(text);
}
+45
View File
@@ -0,0 +1,45 @@
/*
COOP_SPECTATE.H
Spectating in network co-op, and the cutscene skip vote's line
(coop_spectate.c).
*/
#ifndef __COOP_SPECTATE_H
#define __COOP_SPECTATE_H
struct dead_camera;
/* whether this is network co-op: a network game with no game engine (a campaign map) */
boolean coop_spectating(
void);
/* the unit a dead local player watches (A switches teammate), or NONE if
the player is alive or nobody else is */
long coop_spectate_unit(
short local_player_index);
/* TRUE if the teammate this local player is spectating (while dead or not
yet spawned) rides in an AI-driven vehicle, such as an insertion Pelican.
network_coop.c then shows that seat instead of the scripted camera. */
boolean coop_spectate_watching_rider(
short local_player_index);
/* whether a local player has nothing to look at: no unit of its own and
no living teammate to watch (network_coop.c shows it the host's view) */
boolean coop_spectate_nothing_to_watch(
short local_player_index);
/* keeps the dead camera behind the watched unit as it turns */
void coop_spectate_camera(
struct dead_camera *camera);
/* the HUD line saying who a dead local player is watching */
void coop_spectate_draw(
short local_player_index);
/* during a skippable cutscene in network co-op, the vote count and how to vote */
void coop_skip_vote_draw(
short local_player_index);
#endif
File diff suppressed because it is too large. Load diff
+400 -16
View File
@@ -23,6 +23,11 @@ game keeps its own menus.
The tag table cannot grow where it is (the tags' names follow it), so it
is copied, with ours after it; every existing tag keeps its index. All of it
is let go in scenario_tags_unload, before the next map's tags load.
A multiplayer map gets them too, for its in-game pause menu's SETTINGS
(pause_patch): there the map's tags they name that it has not (ui.map's
pictures, sounds and screens) are none, a missing picture drawn blank, and
no screen pauses the game (a network game's clock runs on).
*/
#include "cseries.h"
@@ -40,6 +45,11 @@ is let go in scenario_tags_unload, before the next map's tags load.
/* the platform layer's (port/linux/src) */
void platform_log(char const *format, ...);
char const *config_string(char const *name);
int platform_display_resolutions(long *widths, long *heights, int maximum);
int platform_window_sizes(long *widths, long *heights, int maximum);
/* the game's network server (this machine hosts: network_game_globals.c) */
void *global_network_game_server_get(void);
/* cache_files.c's (port) */
void *cache_files_tag_instances(long *count);
@@ -59,6 +69,13 @@ boolean pc_menu_tag(long tag_index);
/* ---------- constants */
#define PC_MENU_TAG_PREFIX "pc\\"
/* the multiplayer maps' widget collection, whose pause screens pause_patch
adds to */
#define MULTIPLAYER_COLLECTION "ui\\shell\\multiplayer"
/* the tags pause_patch adds: two buttons and their texts */
#define PAUSE_PATCH_TAGS 4
/* what a frame of ui.map's is drawn as in a game map, which has not got it */
#define BLANK_PNG "ce/port/blank__0.png"
#define UI_WIDGET_DEFINITION_TAG 'DeLa'
#define UNICODE_STRING_LIST_TAG 'ustr'
#define FONT_TAG 'font'
@@ -326,6 +343,12 @@ static char const *const port_function_names[] =
"mp level select", "mp profiles list initialize", "mp profiles list dispose", "mp profile set for game",
"port lobby preview join",
"port setup edit",
"port map list back",
"port pause end game",
"port coop begin", "port coop player 2 list initialize", "port coop player 2",
"port lobby open", "port lobby add player", "port lobby join", "port lobby leave",
"port lobby player list initialize", "port lobby player choose",
"port lobby preview add", "port lobby preview leave",
/* (the gametype editor's: the Xbox's walk their rows by place, which the
PC version's screens changed) */
"mp profile begin editing", "mp profile save changes", "request del playlist profile", "final del playlist profile",
@@ -335,6 +358,11 @@ static char const *const port_function_names[] =
"mp profile init indicator opts", "mp profile set ctf rules", "mp profile set koth rules",
"mp profile set slayer rules", "mp profile set oddball rules", "mp profile set racing rules",
"mp profile set player options", "mp profile set item options", "mp profile set indicator opts",
/* (Settings' OK: the Xbox's fails when the profile has no changes, the
settings' screens having written theirs to config.toml) */
"player profile save changes",
/* (the server browser's password screen) */
"port password init", "port password edit", "port password join", "port password back",
};
/* the PC version's game data functions that the Xbox's have not, from
@@ -352,6 +380,7 @@ static char const *const port_game_data_input_names[] =
/* (the gametype editor's: the Xbox's stops the game on the buttons'
row; the Xbox's read only player_ui's gametype, not Server Setup's) */
"game settings lists text update", "get edit game settings name", "mp edit profile set rule text",
"port password update",
};
static struct
@@ -384,6 +413,9 @@ static struct
long *text_tags;
long *spinner_tags;
boolean failed;
/* a multiplayer map's build (the pause menu's SETTINGS): what the map
has not got is none, not a failure */
boolean game_map;
} build;
/* ---------- private code */
@@ -497,7 +529,8 @@ static long map_tag(long group_tag, char const *name, char const *file, long lin
{
long index = tag_loaded(group_tag, name);
if (index == NONE)
/* (a game map has few of ui.map's: those are none) */
if (index == NONE && !build.game_map)
problem(file, line, "the map has no tag", name);
return index;
}
@@ -674,6 +707,41 @@ static long split(char const *text, char const **pieces)
return count;
}
/* a spinner's strings, or its values, split: its own, but Video Setup's
sizes are the display's (sdl_platform.c), shown "1920 x 1080" and set
"1920x1080": its resolutions after Resolution's own (NATIVE), and the
window sizes that fit it in place of Window Size's own */
static long spinner_split(struct halo_menu_widget const *widget, boolean values, char const **pieces)
{
long count = split(values ? widget->values : widget->strings, pieces);
long widths[MAXIMUM_STRINGS], heights[MAXIMUM_STRINGS];
long added, index;
if (widget->setting && !strcmp(widget->setting, "display.resolution"))
added = platform_display_resolutions(widths, heights, (int)(MAXIMUM_STRINGS - count));
else if (widget->setting && !strcmp(widget->setting, "display.window_size"))
{
added = platform_window_sizes(widths, heights, MAXIMUM_STRINGS);
if (added > 0)
count = 0;
}
else
return count;
for (index = 0; index < added; index++)
{
char text[32];
char *piece;
snprintf(text, sizeof(text), values ? "%ldx%ld" : "%ld x %ld", widths[index], heights[index]);
piece = allocate((long)strlen(text) + 1);
if (!piece)
break;
strcpy(piece, text);
pieces[count++] = piece;
}
return count;
}
static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index)
{
struct bitmap_group *group = allocate(sizeof(struct bitmap_group));
@@ -698,10 +766,18 @@ static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index)
{
struct halo_menu_frame const *data = &build.menus->frames[source->first_frame + frame];
struct bitmap_data *bitmap = &bitmaps[frame];
char const *png = data->png;
long width = data->width, height = data->height;
/* (a frame of the map's: a copy of its bitmap, which the texture cache
loads from the map as its own) */
if (data->map)
loads from the map as its own; in a game map that has not got it,
nothing) */
if (data->map && build.game_map && tag_loaded(BITMAP_GROUP_TAG, data->map) == NONE)
{
png = BLANK_PNG;
width = height = PLACEHOLDER_SIZE;
}
else if (data->map)
{
long group_index = map_tag(BITMAP_GROUP_TAG, data->map, source->file, source->line);
struct bitmap_group *group_source;
@@ -720,9 +796,9 @@ static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index)
continue;
}
if (data->width <= 0 || data->height <= 0 || data->width > 2048 || data->height > 2048)
if (width <= 0 || height <= 0 || width > 2048 || height > 2048)
{
problem(source->file, source->line, "a frame is 1 to 2048 wide and high:", data->png);
problem(source->file, source->line, "a frame is 1 to 2048 wide and high:", png);
return group;
}
/* (its D3D texture is a small one, which its PNG is drawn for: the
@@ -741,13 +817,13 @@ static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index)
problem(source->file, source->line, "could not make the texture of bitmap", source->name);
return group;
}
bitmap->width = (short)data->width;
bitmap->height = (short)data->height;
bitmap->width = (short)width;
bitmap->height = (short)height;
menu_tags.bitmaps = realloc(menu_tags.bitmaps, (menu_tags.bitmap_count + 1) * sizeof(*menu_tags.bitmaps));
menu_tags.bitmaps[menu_tags.bitmap_count++] = bitmap;
/* (none without a renderer: debug.null_renderer) */
if (bitmap->hardware_format)
halo_menus_art_register(bitmap->hardware_format, data->png);
halo_menus_art_register(bitmap->hardware_format, png);
}
return group;
}
@@ -880,7 +956,7 @@ static void setting_add(struct halo_menu_widget const *source, long definition_i
setting->definition_index = definition_index;
setting->setting = source->setting;
setting->loaded_index = NONE;
setting->value_count = split(source->values, setting->values);
setting->value_count = spinner_split(source, TRUE, setting->values);
}
/* a font: large, small, terminal, or the map's by its path */
@@ -928,6 +1004,9 @@ static void *widget_build(long widget_index)
definition->bounds.x1 = (short)(source->left + (source->has_width ? source->width : 640));
definition->flags = flags_parse(source->flags, widget_flag_names, NUMBEROF(widget_flag_names),
source->file, source->line);
/* (in a network game the clock runs on) */
if (build.game_map)
definition->flags &= ~FLAG(1); /* pause_game */
definition->milliseconds_to_auto_close = source->auto_close;
definition->auto_close_fade_time = source->auto_close_fade;
reference_clear(&definition->background_bitmap, BITMAP_GROUP_TAG);
@@ -1170,6 +1249,14 @@ static struct cache_file_tag_instance *instances_grow(long count, long *first_in
if (!instances)
return NULL;
/* (every tag's absolute index fits a tag index's 16 bits, short of
NONE's, and the table's size cannot wrap: the map's count was checked
as it loaded, cache_files.c, and is again) */
if (existing < 0 || count < 0 || existing > UNSIGNED_SHORT_MAX - count)
{
platform_log("menus: %ld tags and the map's %ld are too many for a tag table", count, existing);
return NULL;
}
grown = allocate((existing + count) * sizeof(*grown));
if (!grown)
return NULL;
@@ -1250,6 +1337,281 @@ static void menu_tags_release(void)
/* ---------- public code */
/* ---------- the in-game pause menu (a multiplayer map's)
Its list (RESUME GAME, LEAVE GAME; a custom map's own buttons too) gets
SETTINGS, the menus' settings (as the main menu's SETTINGS opens them), and
for the host END GAME, which ends the game as its time limit would: its
players stay, and the host picks the next map after the carnage report.
They are copies of the map's LEAVE GAME button (its look), put before it.
The map's pause screens move to fit them: on the Xbox's own (its box of
pausebox2 pieces), the box is the port's taller one (pause/pausebox_*:
tools/port_settings.py), centred where the old one was; on a custom map's,
what is below the list moves down. */
#define PAUSE_SETTINGS_SCREEN "main_menu/settings_select/player_setup/player_profile_edit/player_profile_edit_screen"
#define PAUSE_BUTTON_SPACING 35
#define PAUSE_BOX_FIRST_BUTTONS 3
static long ui_function_named(char const *name)
{
long index;
for (index = 0; index < 512; index++)
{
char const *function = ui_widget_event_handler_function_name(index);
if (!function)
break;
if (!strcmp(function, name))
return index;
}
return NONE;
}
static boolean tag_name_ends(long tag_index, char const *end)
{
char const *name = tag_index == NONE ? NULL : tag_get_name(tag_index);
size_t length = name ? strlen(name) : 0, end_length = strlen(end);
return name && length >= end_length && !strcmp(name + length - end_length, end);
}
/* the index of the list's button that runs the function (LEAVE GAME's),
else NONE */
static long pause_quit_button(struct ui_widget_definition const *list, long quit_function)
{
struct ui_widget_child_reference const *children = list->child_widgets.address;
long child;
for (child = 0; child < list->child_widgets.count; child++)
{
struct ui_widget_definition const *button;
struct ui_widget_event_handler_reference const *handlers;
long handler;
if (children[child].widget_tag.index == NONE)
continue;
button = tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index);
handlers = button->event_handlers.address;
for (handler = 0; handler < button->event_handlers.count; handler++)
{
if (handlers[handler].function == quit_function &&
TEST_FLAG(handlers[handler].flags, _event_handler_run_function_bit))
{
return child;
}
}
}
return NONE;
}
/* a button like the model (its look), its own text, each of the model's
events (its A and Start) running the function and opening the widget (if
not NONE) */
static void *pause_button(struct cache_file_tag_instance *instances, struct ui_widget_definition const *model,
char const *name, char const *text, long function, long widget_tag)
{
struct ui_widget_definition *button = allocate(sizeof(*button));
struct ui_widget_event_handler_reference *handlers =
allocate(model->event_handlers.count * sizeof(struct ui_widget_event_handler_reference));
long text_tag = next_tag(), handler;
if (!button || !handlers)
return NULL;
memcpy(button, model, sizeof(*button));
instance_set(instances, UNICODE_STRING_LIST_TAG, text_tag, name, " text", string_list_build(&text, 1));
reference_set(&button->text_label_string_list, UNICODE_STRING_LIST_TAG, text_tag);
button->string_list_index = 0;
button->game_data_inputs.count = 0;
button->game_data_inputs.address = NULL;
/* (the game's memcpy asserts on a NULL source, even for nothing) */
if (model->event_handlers.count)
memcpy(handlers, model->event_handlers.address, model->event_handlers.count * sizeof(*handlers));
for (handler = 0; handler < model->event_handlers.count; handler++)
{
handlers[handler].flags = FLAG(_event_handler_run_function_bit);
handlers[handler].function = (short)function;
handlers[handler].script[0] = 0;
reference_set(&handlers[handler].widget_tag, UI_WIDGET_DEFINITION_TAG, widget_tag);
if (widget_tag != NONE)
{
handlers[handler].flags |= FLAG(_event_handler_open_widget_bit) |
FLAG(_event_handler_try_to_branch_on_failure_bit);
}
}
button->event_handlers.address = handlers;
return button;
}
/* the list's buttons: SETTINGS (and the host's END GAME) put before LEAVE
GAME (quit); returns how many were added */
static long pause_list_patch(struct cache_file_tag_instance *instances, struct ui_widget_definition *list, long quit,
boolean host)
{
struct ui_widget_child_reference *children = list->child_widgets.address;
long count = list->child_widgets.count, added = host ? 2 : 1, child;
struct ui_widget_child_reference *grown = allocate((count + added) * sizeof(struct ui_widget_child_reference));
struct ui_widget_definition const *model = tag_get(UI_WIDGET_DEFINITION_TAG, children[quit].widget_tag.index);
long settings_widget = widget_named(PAUSE_SETTINGS_SCREEN);
short spacing = count >= 2 ? (short)(children[count - 1].vertical_offset - children[count - 2].vertical_offset) :
PAUSE_BUTTON_SPACING;
void *buttons[2];
long tags[2];
if (!grown || settings_widget == NONE || spacing <= 0)
return 0;
tags[0] = next_tag();
buttons[0] = pause_button(instances, model, "pause/settings_button", "SETTINGS",
function_index("profile set edit begin", "pause", 0), build.widget_tags[settings_widget]);
tags[1] = host ? next_tag() : NONE;
buttons[1] = host ? pause_button(instances, model, "pause/end_game_button", "END GAME",
function_index("port pause end game", "pause", 0), NONE) : NULL;
if (!buttons[0] || (host && !buttons[1]))
return 0;
for (child = 0; child < added; child++)
{
instance_set(instances, UI_WIDGET_DEFINITION_TAG, tags[child],
child ? "pause/end_game_button" : "pause/settings_button", "", buttons[child]);
}
if (quit)
memcpy(grown, children, quit * sizeof(*grown));
for (child = 0; child < added; child++)
{
grown[quit + child] = children[quit];
reference_set(&grown[quit + child].widget_tag, UI_WIDGET_DEFINITION_TAG, tags[child]);
grown[quit + child].vertical_offset = (short)(children[quit].vertical_offset + child * spacing);
}
for (child = quit; child < count; child++)
{
grown[child + added] = children[child];
grown[child + added].vertical_offset = (short)(children[child].vertical_offset + added * spacing);
}
list->child_widgets.address = grown;
list->child_widgets.count = count + added;
/* (the list draws within its bounds) */
list->bounds.y1 = (short)(list->bounds.y1 + added * spacing);
return added;
}
/* the Xbox's own box: a widget of the three pausebox2 pieces */
static boolean pause_box_stock(struct ui_widget_definition const *box)
{
struct ui_widget_child_reference const *pieces = box->child_widgets.address;
return box->child_widgets.count == 3 && tag_name_ends(pieces[0].widget_tag.index, "\\pausebox2_left") &&
tag_name_ends(pieces[1].widget_tag.index, "\\pausebox2_left_center") &&
tag_name_ends(pieces[2].widget_tag.index, "\\pausebox2_right");
}
/* the stock box's pieces drawn with the port's taller box: its frame for
the number of buttons */
static void pause_box_redraw(struct ui_widget_definition const *box, long buttons)
{
static char const *const names[] = { "pause/pausebox_left", "pause/pausebox_center", "pause/pausebox_right" };
struct ui_widget_child_reference const *pieces = box->child_widgets.address;
long piece;
for (piece = 0; piece < 3; piece++)
{
struct ui_widget_definition *definition = tag_get(UI_WIDGET_DEFINITION_TAG, pieces[piece].widget_tag.index);
long bitmap = find(names[piece], build.menus->bitmap_count, bitmap_name);
struct bitmap_group *group;
struct bitmap_group_sequence *sequence;
if (bitmap == NONE)
continue;
group = bitmap_group_get(build.bitmap_tags[bitmap]);
sequence = group->sequences.address;
/* (one frame: the one for the number of buttons) */
sequence->first_bitmap_index = (short)PIN(buttons - PAUSE_BOX_FIRST_BUTTONS, 0, group->bitmaps.count - 1);
sequence->bitmap_count = 1;
reference_set(&definition->background_bitmap, BITMAP_GROUP_TAG, build.bitmap_tags[bitmap]);
definition->bounds.y1 = (short)(definition->bounds.y0 + 256);
}
}
static void pause_patch(struct cache_file_tag_instance *instances)
{
long collection = tag_loaded('Soul', MULTIPLAYER_COLLECTION);
long quit_function = ui_function_named("mp game player quit");
boolean host = global_network_game_server_get() != NULL;
struct tag_block const *screens;
long patched_list = NONE, added = 0, buttons = 0, screen;
boolean box_redrawn = FALSE;
if (collection == NONE || quit_function == NONE)
return;
screens = tag_get('Soul', collection);
for (screen = 0; screen < screens->count; screen++)
{
long screen_tag = ((struct tag_reference const *)screens->address)[screen].index;
struct ui_widget_definition *definition;
struct ui_widget_child_reference *children;
long child, list_child = NONE, box_child = NONE;
short grow, list_top;
if (screen_tag == NONE)
continue;
definition = tag_get(UI_WIDGET_DEFINITION_TAG, screen_tag);
children = definition->child_widgets.address;
for (child = 0; child < definition->child_widgets.count && list_child == NONE; child++)
{
struct ui_widget_definition *list;
long quit;
if (children[child].widget_tag.index == NONE)
continue;
list = tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index);
if (list->type != _widget_type_column_list)
continue;
/* (one list, which the screens share: patched once) */
if (children[child].widget_tag.index == patched_list)
{
list_child = child;
continue;
}
quit = pause_quit_button(list, quit_function);
if (quit == NONE || patched_list != NONE)
continue;
added = pause_list_patch(instances, list, quit, host);
if (!added)
return;
buttons = list->child_widgets.count;
patched_list = children[child].widget_tag.index;
list_child = child;
}
if (list_child == NONE)
continue;
for (child = 0; child < definition->child_widgets.count; child++)
{
if (child != list_child && children[child].widget_tag.index != NONE &&
pause_box_stock(tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index)))
{
box_child = child;
}
}
/* the stock box: taller, centred where it was, the list with it, and
what is below it moved down; else only what is below the list */
grow = (short)(added * PAUSE_BUTTON_SPACING);
list_top = children[list_child].vertical_offset;
for (child = 0; child < definition->child_widgets.count; child++)
{
if (box_child != NONE && (child == box_child || child == list_child))
children[child].vertical_offset -= grow / 2;
else if (children[child].vertical_offset > list_top)
children[child].vertical_offset += box_child != NONE ? grow - grow / 2 : grow;
}
if (box_child != NONE && !box_redrawn)
{
pause_box_redraw(tag_get(UI_WIDGET_DEFINITION_TAG, children[box_child].widget_tag.index), buttons);
box_redrawn = TRUE;
}
}
if (patched_list != NONE)
platform_log("menus: the pause menu has SETTINGS%s", host ? " and END GAME" : "");
}
void menu_tags_loaded(
char const *map_name)
{
@@ -1257,17 +1619,23 @@ void menu_tags_loaded(
struct cache_file_tag_instance *instances;
long widget_count, own_lists = 0, total, index;
if (strcmp(map_name, "ui") || strcmp(config_string("display.menus"), "pc"))
boolean game_map = strcmp(map_name, "ui") != 0;
/* (ui.map, and a multiplayer map: its pause menu's SETTINGS) */
if ((game_map && tag_loaded('Soul', MULTIPLAYER_COLLECTION) == NONE) ||
strcmp(config_string("display.menus"), "pc"))
{
return;
/* (in VR, the Xbox's: the VR port's join codes, game list, host options
and keyboard pointer are built into its screens) */
if (halo_vr_active())
}
/* (in VR, the Xbox's unless vr.menus asks for the PC version's) */
if (halo_vr_active() && strcmp(config_string("vr.menus"), "pc"))
return;
menus = halo_menus_load();
if (!menus)
return;
memset(&build, 0, sizeof(build));
build.menus = menus;
build.game_map = game_map;
widget_count = menus->widget_count;
build.widget_tags = malloc((widget_count + 1) * sizeof(long));
build.text_tags = malloc((widget_count + 1) * sizeof(long));
@@ -1282,7 +1650,8 @@ void menu_tags_loaded(
problem(menus->widgets[index].file, menus->widgets[index].line, "two widgets are named",
menus->widgets[index].name);
}
total = widget_count + own_lists + menus->string_list_count + menus->bitmap_count;
total = widget_count + own_lists + menus->string_list_count + menus->bitmap_count +
(game_map ? PAUSE_PATCH_TAGS : 0);
instances = build.failed ? NULL : instances_grow(total, &build.first_index, &build.first_salt);
if (!instances)
goto failed;
@@ -1323,7 +1692,7 @@ void menu_tags_loaded(
if (widget->strings)
{
char const *pieces[MAXIMUM_STRINGS];
long count = split(widget->strings, pieces);
long count = spinner_split(widget, FALSE, pieces);
instance_set(instances, UNICODE_STRING_LIST_TAG, build.spinner_tags[index], widget->name, " strings",
string_list_build(pieces, count));
@@ -1332,12 +1701,27 @@ void menu_tags_loaded(
}
if (build.failed)
goto failed;
/* (pause_patch's, until it makes them: nothing a name finds) */
for (index = build.first_index + build.next; index < build.first_index + total; index++)
{
instances[index].group_tag = NONE;
instances[index].tag_index = NONE;
instances[index].name = "";
}
cache_files_set_tag_instances(instances, build.first_index + total);
for (index = 0; index < widget_count && !build.failed; index++)
instances[DATUM_INDEX_TO_ABSOLUTE_INDEX(build.widget_tags[index])].base_address = widget_build(index);
if (build.failed)
goto failed;
if (widget_named(menus->root) != NONE)
if (game_map)
{
pause_patch(instances);
if (build.failed)
goto failed;
/* (those it made) */
cache_files_set_tag_instances(instances, build.first_index + build.next);
}
else if (widget_named(menus->root) != NONE)
{
snprintf(menu_tags.root, sizeof(menu_tags.root), "%s%s", PC_MENU_TAG_PREFIX, menus->root);
for (index = 0; menu_tags.root[index]; index++)
+901
View File
@@ -0,0 +1,901 @@
/*
NETWORK_ACTORS.C
AI sync (port/linux/NETCODE.md).
Only the host runs the AI (game.c skips ai_update on clients). An actor
drives its unit the way a player does, through unit_control. The clients
have the units (network_objects.c); this file makes them move like the
host's.
Each tick the host records the control every actor gave its unit (a hook
in unit_control, and the bursts it holds the trigger for) and sends it to
each client with the unit's state: position, seat, shields and health,
camouflage, and whether it flees in a panic. Units near a client's players
are sent every tick, others less often, but a unit starting a one-tick
action (a jump, a grenade) goes to everyone that tick. Each tick a client
feeds the latest control into unit_control, the same path that drives a
remote player's unit, so the unit walks, aims, fires and throws as it does
on the host. The state corrects any drift.
What the AI starts once is numbered, so a client plays each once however
many times it receives it: animation impulses (dives, vaults), melee
attacks and leaps (which the AI starts directly, not through the control),
speech, and custom animations (outside co-op, whose events carry them).
Pain and death sounds aren't sent: a client plays those itself as it
replays the host's damage.
A client creates no actors of its own (actors.c): the host's are the only
ones. Deaths need nothing here: network_damage.c kills a client's copy of
an AI unit the same way it kills a player's.
*/
/* ---------- headers */
#include "cseries.h"
#include "game/game.h"
#include "networking/network_game_globals.h"
#include "objects/objects.h"
#include "objects/damage.h"
#include "sound/game_sound.h"
#include "sound/sound_definitions.h"
#include "units/units.h"
#include "units/unit_control_data.h"
#include "network_coop.h"
#include "network_distributed.h"
#include <math.h>
/* ---------- constants */
enum
{
/* AI units tracked at once: every actor (halo_port_capacity.h's
HALO_PORT_MAXIMUM_ACTORS) and the units the cutscenes' recorded
animations drive */
MAXIMUM_NETWORK_ACTORS = HALO_PORT_MAXIMUM_ACTORS + 32,
MAXIMUM_ENTRIES_PER_MESSAGE = 64,
/* A client keeps applying a control for this long after the host last
sent one. After that the unit is left alone: the actor let go of it, or
the host went quiet. */
CONTROL_HELD_TICKS = 2 * TICKS_PER_SECOND,
/* an impulse is repeated in this many entries, in case one is lost */
IMPULSE_REPEAT_TICKS = 3,
/* the number of unit_start_animation_impulse impulses (private to units.c) */
NUMBER_OF_UNIT_ANIMATION_IMPULSES = 14,
NO_IMPULSE = 0xFF,
NO_TEAM = 0xFF,
/* An entry's impulse is one of the animation impulses or, past them, an
action the AI starts directly: a melee attack (unit_melee_attack_begin)
or a leap (unit_leap_begin). */
_actor_action_melee = NUMBER_OF_UNIT_ANIMATION_IMPULSES,
_actor_action_leap,
NUMBER_OF_ACTOR_ACTIONS,
/* control flags a unit acts on in one tick: a unit that sets one goes to
every client that tick, however far away, so the action isn't missed */
ONE_SHOT_CONTROL_FLAGS = FLAG(_unit_control_jump_bit) | FLAG(_unit_control_action_bit) |
FLAG(_unit_control_weapon_reload_bit) | FLAG(_unit_control_throw_grenade_bit) |
FLAG(_unit_control_swap_weapons_bit),
};
/* struct distributed_actor_state flags */
enum
{
_distributed_actor_rides_bit = 0,
_distributed_actor_shield_depleted_bit,
_distributed_actor_shield_charging_bit,
_distributed_actor_shield_over_charging_bit,
_distributed_actor_camouflaged_bit,
_distributed_actor_super_camouflaged_bit,
/* the impulse turns the unit to impulse_alignment */
_distributed_actor_impulse_aligned_bit,
/* fleeing in a panic, toward run_blindly_angle (unit_start_running_blindly) */
_distributed_actor_running_blindly_bit,
};
/* facing, aiming and looking vectors are sent as yaw and pitch */
enum
{
_angle_yaw,
_angle_pitch,
NUMBER_OF_ANGLES
};
/* ---------- structures */
/* an AI unit's control and state at the end of the host's tick (like
struct distributed_unit_state for players) */
struct distributed_actor_state
{
long unit_index;
long vehicle_index;
short seat_index;
byte flags;
/* the impulse's sequence number; impulse is NO_IMPULSE when there is none */
byte impulse_number;
byte impulse;
byte animation_state;
byte aiming_speed;
byte primary_trigger;
word control_flags;
signed char throttle[3];
byte active_camouflage;
/* the speech's sequence number; 0 when there is none */
byte speech_number;
/* the unit's team (NO_TEAM for none): the AI sets it after the unit is
made, so a client's copy would keep the team it was made with */
byte team;
short facing[NUMBER_OF_ANGLES];
short aiming[NUMBER_OF_ANGLES];
short looking[NUMBER_OF_ANGLES];
short impulse_alignment;
short run_blindly_angle;
word body_vitality;
word shield_vitality;
/* the sound the unit started saying (a sound tag) */
long speech_sound;
/* the custom animation it started (unit_start_user_animation), and its
sequence number; the graph is NONE when there is none */
long user_animation_graph;
short user_animation;
byte user_animation_number;
byte user_animation_interpolate;
real_point3d position;
struct distributed_vector velocity;
struct distributed_vector forward;
struct distributed_vector up;
};
/* the damage an AI unit is taking: its shield's flare and its body's wounds
are drawn by it */
struct distributed_actor_damage
{
long unit_index;
word current_shield_damage;
word recent_shield_damage;
word current_body_damage;
word recent_body_damage;
};
struct distributed_actor_damage_message
{
struct distributed_message_header header;
struct distributed_actor_damage units[MAXIMUM_ENTRIES_PER_MESSAGE];
};
struct distributed_actor_state_message
{
struct distributed_message_header header;
struct distributed_actor_state states[MAXIMUM_ENTRIES_PER_MESSAGE];
};
/* host: what an actor gave its unit this tick */
struct host_actor
{
long unit_index;
/* got a control since the last send (noted during game_tick, sent after it) */
boolean noted;
/* its damage was sent last tick (and once more when it ends) */
boolean damaged;
struct unit_control_data control;
/* the control flags of the last tick's entry */
word sent_control_flags;
/* the latest impulse, how many more entries carry it, and its number */
short impulse;
short impulse_sends;
byte impulse_number;
boolean impulse_aligned;
real_vector2d impulse_alignment;
/* the latest speech, how many more entries carry it, and its number */
long speech_sound;
short speech_sends;
byte speech_number;
/* the latest custom animation, the same way */
long user_animation_graph;
short user_animation;
boolean user_animation_interpolate;
short user_animation_sends;
byte user_animation_number;
};
/* client: the latest the host sent about an AI unit */
struct client_actor
{
long unit_index;
long received_time;
struct unit_control_data control;
/* the impulse to play, and the number of the last one played */
short impulse;
byte impulse_number;
byte played_impulse_number;
boolean impulse_aligned;
real_vector2d impulse_alignment;
/* the speech to play (NONE when there is none), and the number of the last played */
long speech_sound;
byte speech_number;
byte played_speech_number;
/* the custom animation to play (graph NONE when there is none), and the
number of the last played */
long user_animation_graph;
short user_animation;
boolean user_animation_interpolate;
byte user_animation_number;
byte played_user_animation_number;
boolean driven;
};
/* ---------- globals */
static struct host_actor host_actors[MAXIMUM_NETWORK_ACTORS];
static short host_actor_count;
/* host: the numbers of every unit's impulses, speech and animations, from
one counter, so a unit that drops out of the table and comes back can't
repeat the number a client last played */
static byte host_event_number;
static struct client_actor client_actors[MAXIMUM_NETWORK_ACTORS];
static short client_actor_count;
/* ---------- private code */
static void angles_pack(
real_vector3d const *vector,
short *angles)
{
angles[_angle_yaw] = distributed_angle_pack((real)atan2(vector->j, vector->i));
angles[_angle_pitch] = distributed_angle_pack((real)asin(PIN(vector->k, -1.0f, 1.0f)));
}
static void angles_unpack(
short const *angles,
real_vector3d *vector)
{
real yaw = distributed_angle_unpack(angles[_angle_yaw], FALSE);
real pitch = distributed_angle_unpack(angles[_angle_pitch], TRUE);
vector->i = (real)(cos(pitch) * cos(yaw));
vector->j = (real)(cos(pitch) * sin(yaw));
vector->k = (real)sin(pitch);
}
/* host: the next event's number (never 0, which a client's new entry has
as played) */
static byte next_event_number(
void)
{
if (++host_event_number == 0)
host_event_number = 1;
return host_event_number;
}
/* the unit's entry, adding one if needed; NULL if the table is full */
static struct host_actor *host_actor_for(
long unit_index)
{
short index;
for (index = 0; index < host_actor_count; index++)
{
if (host_actors[index].unit_index == unit_index)
return &host_actors[index];
}
if (host_actor_count >= MAXIMUM_NETWORK_ACTORS)
return NULL;
csmemset(&host_actors[host_actor_count], 0, sizeof(host_actors[0]));
host_actors[host_actor_count].unit_index = unit_index;
host_actors[host_actor_count].impulse = NONE;
host_actors[host_actor_count].speech_sound = NONE;
host_actors[host_actor_count].user_animation_graph = NONE;
return &host_actors[host_actor_count++];
}
static struct client_actor *client_actor_for(
long unit_index)
{
short index;
for (index = 0; index < client_actor_count; index++)
{
if (client_actors[index].unit_index == unit_index)
return &client_actors[index];
}
if (client_actor_count >= MAXIMUM_NETWORK_ACTORS)
return NULL;
csmemset(&client_actors[client_actor_count], 0, sizeof(client_actors[0]));
client_actors[client_actor_count].unit_index = unit_index;
client_actors[client_actor_count].impulse = NONE;
client_actors[client_actor_count].speech_sound = NONE;
client_actors[client_actor_count].user_animation_graph = NONE;
return &client_actors[client_actor_count++];
}
/* whether the unit exists, is alive, and isn't a player's */
static boolean actor_unit_valid(
long unit_index)
{
struct unit_datum *unit;
if (!object_try_and_get_and_verify_type(unit_index, _object_mask_unit))
return FALSE;
unit = unit_get(unit_index);
return !TEST_FLAG(unit->object.damage_flags, _object_dead_bit) && unit->unit.player_index == NONE;
}
/* host: builds the entry to send for an AI unit */
static void actor_state_from_unit(
struct host_actor const *actor,
struct distributed_actor_state *state)
{
struct unit_datum *unit = unit_get(actor->unit_index);
struct damage_network_state damage;
real camouflage = PIN(unit->unit.active_camouflage, 0.0f, 1.0f);
csmemset(state, 0, sizeof(*state));
state->unit_index = actor->unit_index;
state->vehicle_index = NONE;
state->seat_index = NONE;
if (unit->object.parent_object_index != NONE && unit->unit.parent_seat_index != NONE)
{
SET_FLAG(state->flags, _distributed_actor_rides_bit, TRUE);
state->vehicle_index = unit->object.parent_object_index;
state->seat_index = unit->unit.parent_seat_index;
}
state->animation_state = (byte)actor->control.animation_state;
state->aiming_speed = (byte)actor->control.aiming_speed;
state->primary_trigger = (byte)(long)floor(PIN(actor->control.primary_trigger, 0.0f, 1.0f) * 255.0f + 0.5f);
state->control_flags = actor->control.control_flags;
/* for a burst the AI holds the trigger for (unit_persistent_control),
send the flags and trigger the unit's update produced this tick */
if (unit->unit.persistent_control_timer > 0)
{
state->control_flags = (word)(unit->unit.control_flags & (FLAG(NUMBER_OF_UNIT_CONTROL_FLAGS) - 1));
state->primary_trigger = (byte)(long)floor(PIN(unit->unit.primary_trigger, 0.0f, 1.0f) * 255.0f + 0.5f);
}
state->throttle[0] = (signed char)(long)floor(PIN(actor->control.throttle.i, -1.0f, 1.0f) * 127.0f + 0.5f);
state->throttle[1] = (signed char)(long)floor(PIN(actor->control.throttle.j, -1.0f, 1.0f) * 127.0f + 0.5f);
state->throttle[2] = (signed char)(long)floor(PIN(actor->control.throttle.k, -1.0f, 1.0f) * 127.0f + 0.5f);
angles_pack(&actor->control.facing_vector, state->facing);
angles_pack(&actor->control.aiming_vector, state->aiming);
angles_pack(&actor->control.looking_vector, state->looking);
state->impulse = NO_IMPULSE;
if (actor->impulse != NONE && actor->impulse_sends > 0)
{
state->impulse = (byte)actor->impulse;
state->impulse_number = actor->impulse_number;
SET_FLAG(state->flags, _distributed_actor_impulse_aligned_bit, actor->impulse_aligned);
if (actor->impulse_aligned)
{
state->impulse_alignment = distributed_angle_pack(
(real)atan2(actor->impulse_alignment.j, actor->impulse_alignment.i));
}
}
state->speech_sound = NONE;
if (actor->speech_sound != NONE && actor->speech_sends > 0)
{
state->speech_sound = actor->speech_sound;
state->speech_number = actor->speech_number;
}
state->user_animation_graph = NONE;
if (actor->user_animation_graph != NONE && actor->user_animation_sends > 0)
{
state->user_animation_graph = actor->user_animation_graph;
state->user_animation = actor->user_animation;
state->user_animation_interpolate = (byte)actor->user_animation_interpolate;
state->user_animation_number = actor->user_animation_number;
}
if (TEST_FLAG(unit->unit.flags, _unit_running_blindly_bit))
{
SET_FLAG(state->flags, _distributed_actor_running_blindly_bit, TRUE);
state->run_blindly_angle = distributed_angle_pack(unit->unit.run_blindly_angle);
}
damage_get_network_state(actor->unit_index, &damage);
SET_FLAG(state->flags, _distributed_actor_shield_depleted_bit, damage.shield_depleted);
SET_FLAG(state->flags, _distributed_actor_shield_charging_bit, damage.shield_charging);
SET_FLAG(state->flags, _distributed_actor_shield_over_charging_bit, damage.shield_over_charging);
state->body_vitality = distributed_vitality_pack(damage.body_vitality);
state->shield_vitality = distributed_vitality_pack(damage.shield_vitality);
SET_FLAG(state->flags, _distributed_actor_camouflaged_bit,
TEST_FLAG(unit->unit.flags, _unit_active_camouflaged_bit));
SET_FLAG(state->flags, _distributed_actor_super_camouflaged_bit,
TEST_FLAG(unit->unit.flags, _unit_super_camouflaged_bit));
state->active_camouflage = (byte)(long)floor(camouflage * 255.0f + 0.5f);
state->team = unit->object.owner_team_index >= 0 && unit->object.owner_team_index < NO_TEAM ?
(byte)unit->object.owner_team_index : NO_TEAM;
state->position = unit->object.position;
distributed_vector_pack(&unit->object.translational_velocity, DISTRIBUTED_VELOCITY_SCALE, &state->velocity);
distributed_vector_pack(&unit->object.forward, DISTRIBUTED_UNIT_SCALE, &state->forward);
distributed_vector_pack(&unit->object.up, DISTRIBUTED_UNIT_SCALE, &state->up);
}
/* Client: stores the entry's control and applies its state to the unit.
Returns FALSE for an entry with invalid values. */
static boolean actor_state_apply(
struct distributed_actor_state const *state,
long now)
{
struct client_actor *actor;
struct unit_datum *unit;
struct damage_network_state damage;
real_vector3d velocity;
real_vector3d forward;
real_vector3d up;
if (!distributed_object_index_valid(state->unit_index) || !network_objects_client_has(state->unit_index))
return FALSE;
/* (a body the host's unit got back up from: a Flood combat form that
feigned death, which it does only once its body is at rest. Left dead,
the ragdoll was dragged about by the host's positions. The host sends
only living units.) */
if (object_try_and_get_and_verify_type(state->unit_index, _object_mask_unit) &&
TEST_FLAG(unit_get(state->unit_index)->object.damage_flags, _object_dead_bit) &&
TEST_FLAG(unit_get(state->unit_index)->object.flags, _object_at_rest_bit) &&
unit_get(state->unit_index)->unit.player_index == NONE && state->body_vitality > 0)
{
unit_port_resurrect(state->unit_index);
}
if (!actor_unit_valid(state->unit_index) ||
state->animation_state >= NUMBER_OF_UNIT_ANIMATION_STATES ||
state->aiming_speed >= NUMBER_OF_UNIT_AIMING_SPEEDS ||
!VALID_FLAGS(state->control_flags, NUMBER_OF_UNIT_CONTROL_FLAGS) ||
(state->impulse != NO_IMPULSE && state->impulse >= NUMBER_OF_ACTOR_ACTIONS) ||
(state->speech_sound != NONE && !distributed_tag_of_group(state->speech_sound, SOUND_DEFINITION_TAG)) ||
(state->user_animation_graph != NONE &&
!distributed_graph_animation(state->user_animation_graph, state->user_animation)) ||
!distributed_point_valid(&state->position, UNIT_WORLD_BOUND))
{
return FALSE;
}
distributed_vector_unpack(&state->velocity, DISTRIBUTED_VELOCITY_SCALE, &velocity);
distributed_vector_unpack(&state->forward, DISTRIBUTED_UNIT_SCALE, &forward);
distributed_vector_unpack(&state->up, DISTRIBUTED_UNIT_SCALE, &up);
if (!distributed_axes_make_valid(&forward, &up))
return FALSE;
actor = client_actor_for(state->unit_index);
if (!actor)
return TRUE;
/* the control, applied every tick until the next entry */
actor->received_time = now;
actor->control.animation_state = (char)state->animation_state;
actor->control.aiming_speed = (char)state->aiming_speed;
actor->control.control_flags = state->control_flags;
actor->control.weapon_index = NONE;
actor->control.grenade_index = NONE;
actor->control.zoom_level = NONE;
actor->control.throttle.i = (real)state->throttle[0] / 127.0f;
actor->control.throttle.j = (real)state->throttle[1] / 127.0f;
actor->control.throttle.k = (real)state->throttle[2] / 127.0f;
actor->control.primary_trigger = (real)state->primary_trigger / 255.0f;
angles_unpack(state->facing, &actor->control.facing_vector);
angles_unpack(state->aiming, &actor->control.aiming_vector);
angles_unpack(state->looking, &actor->control.looking_vector);
if (state->impulse != NO_IMPULSE && state->impulse_number != actor->played_impulse_number)
{
real yaw = distributed_angle_unpack(state->impulse_alignment, FALSE);
actor->impulse = state->impulse;
actor->impulse_number = state->impulse_number;
actor->impulse_aligned = TEST_FLAG(state->flags, _distributed_actor_impulse_aligned_bit);
actor->impulse_alignment.i = (real)cos(yaw);
actor->impulse_alignment.j = (real)sin(yaw);
}
if (state->speech_sound != NONE && state->speech_number != actor->played_speech_number)
{
actor->speech_sound = state->speech_sound;
actor->speech_number = state->speech_number;
}
if (state->user_animation_graph != NONE && state->user_animation_number != actor->played_user_animation_number)
{
actor->user_animation_graph = state->user_animation_graph;
actor->user_animation = state->user_animation;
actor->user_animation_interpolate = state->user_animation_interpolate != 0;
actor->user_animation_number = state->user_animation_number;
}
/* the state: seat, shields and health, camouflage, running blindly, and
position (a unit in a vehicle goes where the vehicle goes) */
unit = unit_get(state->unit_index);
if (TEST_FLAG(state->flags, _distributed_actor_rides_bit))
{
if (unit->object.parent_object_index != state->vehicle_index ||
unit->unit.parent_seat_index != state->seat_index)
{
network_objects_set_seat(state->unit_index, state->vehicle_index, state->seat_index);
}
}
else if (unit->object.parent_object_index != NONE)
{
network_objects_set_seat(state->unit_index, NONE, NONE);
}
damage_get_network_state(state->unit_index, &damage);
damage.shield_depleted = TEST_FLAG(state->flags, _distributed_actor_shield_depleted_bit);
damage.shield_charging = TEST_FLAG(state->flags, _distributed_actor_shield_charging_bit);
damage.shield_over_charging = TEST_FLAG(state->flags, _distributed_actor_shield_over_charging_bit);
damage.body_vitality = distributed_vitality_unpack(state->body_vitality);
damage.shield_vitality = distributed_vitality_unpack(state->shield_vitality);
damage_set_network_state(state->unit_index, &damage);
SET_FLAG(unit->unit.flags, _unit_active_camouflaged_bit,
TEST_FLAG(state->flags, _distributed_actor_camouflaged_bit));
SET_FLAG(unit->unit.flags, _unit_super_camouflaged_bit,
TEST_FLAG(state->flags, _distributed_actor_super_camouflaged_bit));
unit->unit.active_camouflage = (real)state->active_camouflage / 255.0f;
if (state->team != NO_TEAM)
unit->object.owner_team_index = state->team;
SET_FLAG(unit->unit.flags, _unit_running_blindly_bit,
TEST_FLAG(state->flags, _distributed_actor_running_blindly_bit));
if (TEST_FLAG(state->flags, _distributed_actor_running_blindly_bit))
unit->unit.run_blindly_angle = distributed_angle_unpack(state->run_blindly_angle, FALSE);
/* vehicles are placed only by the host's object states (network_objects.c) */
if (unit->object.parent_object_index == NONE && unit->object.type != _object_type_vehicle)
{
real dx = state->position.x - unit->object.position.x;
real dy = state->position.y - unit->object.position.y;
real dz = state->position.z - unit->object.position.z;
if (dx * dx + dy * dy + dz * dz > REMOTE_CORRECTION_TOLERANCE * REMOTE_CORRECTION_TOLERANCE &&
network_objects_reconcile(state->unit_index, &state->position, &forward, &up, &velocity, NULL,
REMOTE_BLEND_DISTANCE))
{
distributed_count_correction();
}
}
return TRUE;
}
/* Client: plays a sound at the unit's head, as unit_dialogue_update starts
a unit's speech. */
static void speech_play(
long unit_index,
long sound_definition_index)
{
struct object_marker marker;
real_point3d position = *global_origin3d;
real_vector3d forward = *global_forward3d;
short node_index = 0;
if (object_get_marker_by_name(unit_index, "head", &marker, 1))
{
node_index = marker.node_index;
position = marker.node_matrix.position;
forward = marker.node_matrix.forward;
}
object_impulse_sound_new(unit_index, sound_definition_index, node_index, &position, &forward, 1.0f);
}
/* ---------- public code */
void network_actors_new_game(
void)
{
host_actor_count = 0;
client_actor_count = 0;
}
/* host: unit_control calls this when an actor drives its unit */
void network_actors_note_control(
long unit_index,
struct unit_control_data const *control_data)
{
struct host_actor *actor;
if (game_connection() != _game_connection_network_server)
return;
actor = host_actor_for(unit_index);
if (!actor)
return;
actor->noted = TRUE;
actor->control = *control_data;
}
/* host: an impulse or action for an AI unit, sent in the next few entries */
static void note_action(
long unit_index,
short action,
real_vector2d const *alignment_vector)
{
struct host_actor *actor;
if (game_connection() != _game_connection_network_server)
return;
actor = host_actor_for(unit_index);
if (!actor)
return;
actor->impulse = action;
actor->impulse_sends = IMPULSE_REPEAT_TICKS;
actor->impulse_number = next_event_number();
actor->impulse_aligned = alignment_vector != NULL;
if (alignment_vector)
actor->impulse_alignment = *alignment_vector;
}
/* host: unit_start_animation_impulse calls this for an AI unit */
void network_actors_note_impulse(
long unit_index,
short animation_impulse,
real_vector2d const *alignment_vector)
{
note_action(unit_index, animation_impulse, alignment_vector);
}
/* host: unit_melee_attack_begin calls this when an AI unit starts a melee attack */
void network_actors_note_melee(
long unit_index,
real_vector2d const *alignment_vector)
{
note_action(unit_index, _actor_action_melee, alignment_vector);
}
/* host: unit_leap_begin calls this when an AI unit leaps */
void network_actors_note_leap(
long unit_index,
real_vector2d const *alignment_vector)
{
note_action(unit_index, _actor_action_leap, alignment_vector);
}
/* host: unit_dialogue_update calls this when an AI unit starts saying
something (not a pain or death sound, which clients play themselves) */
void network_actors_note_speech(
long unit_index,
long sound_definition_index)
{
struct host_actor *actor;
if (game_connection() != _game_connection_network_server)
return;
actor = host_actor_for(unit_index);
if (!actor)
return;
actor->speech_sound = sound_definition_index;
actor->speech_sends = IMPULSE_REPEAT_TICKS;
actor->speech_number = next_event_number();
}
/* host: unit_start_user_animation calls this when an AI unit starts a
custom animation (co-op's events carry them in co-op) */
void network_actors_note_user_animation(
long unit_index,
long animation_graph_index,
short animation_index,
boolean interpolate)
{
struct host_actor *actor;
if (game_connection() != _game_connection_network_server || network_coop_active())
return;
actor = host_actor_for(unit_index);
if (!actor)
return;
actor->user_animation_graph = animation_graph_index;
actor->user_animation = animation_index;
actor->user_animation_interpolate = interpolate;
actor->user_animation_sends = IMPULSE_REPEAT_TICKS;
actor->user_animation_number = next_event_number();
}
/* host: the damage of the AI units taking some, to every client (and once
more when it ends, so a flare goes) */
static void host_send_damage(
void)
{
struct distributed_actor_damage_message message;
short count = 0;
short index;
for (index = 0; index < host_actor_count; index++)
{
struct host_actor *actor = &host_actors[index];
struct damage_network_state damage;
boolean damaged;
if (!actor_unit_valid(actor->unit_index))
continue;
damage_get_network_state(actor->unit_index, &damage);
damaged = damage.current_shield_damage > 0.0f || damage.recent_shield_damage > 0.0f ||
damage.current_body_damage > 0.0f || damage.recent_body_damage > 0.0f;
if (!damaged && !actor->damaged)
continue;
actor->damaged = damaged;
message.units[count].unit_index = actor->unit_index;
message.units[count].current_shield_damage = distributed_vitality_pack(damage.current_shield_damage);
message.units[count].recent_shield_damage = distributed_vitality_pack(damage.recent_shield_damage);
message.units[count].current_body_damage = distributed_vitality_pack(damage.current_body_damage);
message.units[count].recent_body_damage = distributed_vitality_pack(damage.recent_body_damage);
if (++count == MAXIMUM_ENTRIES_PER_MESSAGE)
{
distributed_send(&message, _distributed_message_actor_damage, count,
(word)(sizeof(message.header) + count * sizeof(message.units[0])), _distributed_to_clients);
count = 0;
}
}
if (count)
{
distributed_send(&message, _distributed_message_actor_damage, count,
(word)(sizeof(message.header) + count * sizeof(message.units[0])), _distributed_to_clients);
}
}
/* Host, after each tick: sends each client the AI units driven this tick.
Units near the client's players go every tick, others less often. A unit
with a fresh impulse, action, speech or animation goes to everyone in each
of the next IMPULSE_REPEAT_TICKS ticks, and one starting a one-tick action
goes that tick. A unit no actor drove this tick is dropped from the table,
unless it is fleeing: a unit running blindly moves by itself, without its
actor's control, and dropped it stood running in place on the clients. */
void network_actors_host_tick(
void)
{
static struct distributed_actor_state states[MAXIMUM_NETWORK_ACTORS];
/* sent to every client this tick, whatever its period */
static boolean urgent[MAXIMUM_NETWORK_ACTORS];
struct distributed_actor_state_message message;
long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
short machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES);
long now = game_time_get();
short state_count = 0;
short index;
short machine_number;
for (index = 0; index < host_actor_count; index++)
{
struct host_actor *actor = &host_actors[index];
if (!actor_unit_valid(actor->unit_index) ||
(!actor->noted && !TEST_FLAG(unit_get(actor->unit_index)->unit.flags, _unit_running_blindly_bit)))
{
host_actors[index--] = host_actors[--host_actor_count];
continue;
}
actor_state_from_unit(actor, &states[state_count]);
urgent[state_count] = states[state_count].impulse != NO_IMPULSE || states[state_count].speech_sound != NONE ||
states[state_count].user_animation_graph != NONE ||
(states[state_count].control_flags & ~actor->sent_control_flags & ONE_SHOT_CONTROL_FLAGS) != 0;
actor->sent_control_flags = states[state_count].control_flags;
state_count++;
actor->noted = FALSE;
if (actor->impulse_sends > 0)
actor->impulse_sends--;
if (actor->speech_sends > 0)
actor->speech_sends--;
if (actor->user_animation_sends > 0)
actor->user_animation_sends--;
}
host_send_damage();
for (machine_number = 0; machine_number < machine_count; machine_number++)
{
long machine_index = machine_indices[machine_number];
short count = 0;
for (index = 0; index < state_count; index++)
{
struct distributed_actor_state const *state = &states[index];
short period = network_objects_send_period(machine_index, &state->position);
if (!urgent[index] && (now + DATUM_INDEX_TO_ABSOLUTE_INDEX(state->unit_index)) % period != 0)
continue;
message.states[count++] = *state;
if (count == MAXIMUM_ENTRIES_PER_MESSAGE || count == DATAGRAM_ENTRIES(struct distributed_actor_state))
{
distributed_send_to_machine(machine_index, &message, _distributed_message_actor_states, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_actor_state)));
count = 0;
}
}
if (count)
{
distributed_send_to_machine(machine_index, &message, _distributed_message_actor_states, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_actor_state)));
}
}
}
word network_actors_damage_entry_size(
void)
{
return sizeof(struct distributed_actor_damage);
}
/* client: the damage the host's AI units are taking */
void network_actors_handle_damage(
void const *entries,
short count)
{
struct distributed_actor_damage const *units = entries;
short index;
for (index = 0; index < count; index++)
{
struct damage_network_state damage;
if (!distributed_object_index_valid(units[index].unit_index) ||
!network_objects_client_has(units[index].unit_index) || !actor_unit_valid(units[index].unit_index))
{
continue;
}
damage_get_network_state(units[index].unit_index, &damage);
damage.current_shield_damage = distributed_vitality_unpack(units[index].current_shield_damage);
damage.recent_shield_damage = distributed_vitality_unpack(units[index].recent_shield_damage);
damage.current_body_damage = distributed_vitality_unpack(units[index].current_body_damage);
damage.recent_body_damage = distributed_vitality_unpack(units[index].recent_body_damage);
damage_set_network_state(units[index].unit_index, &damage);
}
}
word network_actors_entry_size(
void)
{
return sizeof(struct distributed_actor_state);
}
/* client: entries from the host */
void network_actors_handle_states(
void const *entries,
short count)
{
struct distributed_actor_state const *states = entries;
long now = game_time_get();
short index;
for (index = 0; index < count; index++)
actor_state_apply(&states[index], now);
}
/* Client: called at the point in the tick where the host runs the AI.
Drives each AI unit with the last control the host sent. */
void network_actors_drive(
void)
{
long now = game_time_get();
short index;
for (index = 0; index < client_actor_count; index++)
{
struct client_actor *actor = &client_actors[index];
if (!actor_unit_valid(actor->unit_index) || now - actor->received_time > CONTROL_HELD_TICKS)
{
if (actor->driven && object_try_and_get_and_verify_type(actor->unit_index, _object_mask_unit))
unit_set_actively_controlled(actor->unit_index, FALSE);
client_actors[index--] = client_actors[--client_actor_count];
continue;
}
/* vehicles follow the host's transform instead (network_objects.c) */
if (object_get(actor->unit_index)->object.type == _object_type_vehicle)
continue;
if (!actor->driven)
{
unit_set_actively_controlled(actor->unit_index, TRUE);
actor->driven = TRUE;
}
unit_control(actor->unit_index, &actor->control);
if (actor->impulse != NONE && actor->impulse_number != actor->played_impulse_number)
{
real_vector2d *alignment = actor->impulse_aligned ? &actor->impulse_alignment : NULL;
if (actor->impulse == _actor_action_melee)
unit_melee_attack_begin(actor->unit_index, FALSE, alignment);
else if (actor->impulse == _actor_action_leap)
unit_leap_begin(actor->unit_index, alignment);
else
unit_start_animation_impulse(actor->unit_index, actor->impulse, alignment);
actor->played_impulse_number = actor->impulse_number;
actor->impulse = NONE;
}
if (actor->speech_sound != NONE)
{
speech_play(actor->unit_index, actor->speech_sound);
actor->played_speech_number = actor->speech_number;
actor->speech_sound = NONE;
}
if (actor->user_animation_graph != NONE)
{
unit_port_play_user_animation(actor->unit_index, actor->user_animation_graph, actor->user_animation,
actor->user_animation_interpolate, 0);
actor->played_user_animation_number = actor->user_animation_number;
actor->user_animation_graph = NONE;
}
}
}
File diff suppressed because it is too large. Load diff
+115
View File
@@ -0,0 +1,115 @@
/*
NETWORK_COOP.H
The calls Halo's own code (source/) makes into network_coop.c. On a co-op
host the note_ functions send what the scripts just did to the clients; on
any other machine they do nothing, so the clients can call the same engine
functions to apply what they receive.
*/
#ifndef __NETWORK_COOP_H
#define __NETWORK_COOP_H
/* ---------- constants */
/* network_coop_note_hud */
enum
{
_coop_hud_help_text,
_coop_hud_objective,
_coop_hud_help_flash,
_coop_hud_messages_clear,
_coop_hud_checkpoint,
};
/* network_coop_note_player_effect */
enum
{
_coop_player_effect_translation,
_coop_player_effect_rotation,
_coop_player_effect_start,
_coop_player_effect_stop,
};
/* network_coop_note_nav_point */
enum
{
_coop_nav_point_team_flag,
_coop_nav_point_team_object,
_coop_nav_point_unit_flag,
_coop_nav_point_unit_object,
};
/* ---------- prototypes/NETWORK_COOP.C */
/* whether this is a network co-op game (not its lobby, whose menu scene also
has no game engine) */
boolean network_coop_active(void);
/* whether the players collide with each other: FALSE in a co-op game whose
host set Server Setup's PLAYER COLLISIONS off (bipeds.c, collisions.c) */
boolean network_coop_player_collisions(void);
/* whether this machine is a co-op client, whose devices only the host moves */
boolean network_coop_devices_remote(void);
/* devices.c: a group's devices were set straight to its value */
void network_coop_note_device_snap(short group_index);
/* unit_scripting_commands.c: a script set a unit's maximum or current
vitality; TRUE if it was a co-op player's, now set on every player's */
boolean network_coop_set_players_vitality(long unit_index, boolean maximum, real body, real shield);
/* host: whether the player's machine has the host's structure BSP loaded
(always outside co-op, and for the host's own players). Until a client has,
the host takes none of its players' movement, which falls there with no
floor, and lets them trigger no BSP switch (players.c). */
boolean network_coop_player_has_structure_bsp(long player_index);
/* network_distributed.c: the structure BSP a client's input says it has */
void network_coop_note_player_structure_bsp(short player_index, short structure_bsp_index);
/* units.c: a vehicle left the vehicle carrying it (a Pelican's Warthog drop) */
void network_coop_vehicle_dropped(long vehicle_index, long carrier_index);
/* cinematics.c: a chapter title */
void network_coop_note_title(short title_index, real delay);
/* hud_messaging.c, hud.c: value is a message index, or TRUE/FALSE */
void network_coop_note_hud(short kind, short value);
/* player_effects.c: the script screen shake (a, b, c as the function takes them) */
void network_coop_note_player_effect(short kind, real a, real b, real c);
/* hud_nav_points.c: activate (or, with nav_index NONE, deactivate) a nav point.
target is the team or the unit; marker is the cutscene flag or the object. */
void network_coop_note_nav_point(short kind, short nav_index, long target, long marker, real vertical_offset);
/* units.c: a unit started a custom animation (animation_index NONE: stopped) */
void network_coop_note_unit_animation(long unit_index, long animation_graph_index, short animation_index,
boolean interpolate);
/* units.c: a unit (a dropship) opened or closed (unit_open, unit_close) */
void network_coop_note_unit_open(long unit_index, boolean open);
/* units.c: unit_custom_animation_at_frame moved it to a frame */
void network_coop_note_unit_animation_frame(long unit_index, short frame_index);
/* scenery.c: a scenery animation started */
/* the scripts' effects (hs_library_external.c): at a cutscene flag, or on an
object's marker */
void network_coop_note_effect(long effect_definition_index, short cutscene_flag_index);
/* the scripts' objects_attach and objects_detach (objects.c) */
void network_coop_note_attach(long parent_index, char const *parent_marker_name, long child_index,
char const *child_marker_name);
void network_coop_note_detach(long parent_index, long child_index);
void network_coop_note_object_effect(long effect_definition_index, long object_index, char const *marker_name);
/* breakable_surfaces.c: a breakable surface (glass) broke, from damage at
`epicenter`. Each machine's own shots break glass too, but with their own
random damage, so the host's breaks are the ones everyone keeps. */
void network_coop_note_surface_broken(short breakable_surface_index, real_point3d const *epicenter);
void network_coop_note_scenery_animation(long object_index, long animation_graph_index, short animation_index,
short frame_index);
/* player_control.c: whether the skip key should do anything, and the press.
network_coop_vote_skip returns FALSE outside network co-op, where the
cinematic is skipped as usual. */
boolean network_coop_skip_offered(void);
boolean network_coop_vote_skip(void);
/* main.c: after a co-op host reverts to its last saved state (to skip a
cinematic, or because the mission failed); now is the game time before the
revert */
void network_coop_reverted(long now);
/* main.c: the skip vote is over once the skip has reverted */
void network_coop_skip_done(void);
/* the vote count to show, if a skippable cinematic is playing */
boolean network_coop_skip_vote_status(short *votes, short *voters, boolean *voted);
#endif
+263 -50
View File
@@ -16,8 +16,9 @@ being hit looks and feels like on the clients).
- A client deals no damage itself. What its own players' shots, grenades,
melee and vehicles hit it reports to the host, which deals it once it has
checked it: the report is of that machine's player, the damage one that
player's weapons (now or lately), grenades or the vehicle they drove
deal, of the shape the game gives it (at a point, a melee blow from the
player's weapons (now or lately), a grenade the host saw them throw
(each throw's explosion once) or the vehicle they drove deal, of the
shape the game gives it (at a point, a melee blow from the
striker, a collision from the vehicle), the target about where the host
had it when the shooter saw it (the host keeps a second of where
players' units and vehicles were, and looks back as far as the report
@@ -33,9 +34,14 @@ being hit looks and feels like on the clients).
- The damage to a unit goes to the machines of the players it concerns
(the unit's, its riders', the damage's owner's) and those sent the
unit's player this tick, which see it; a killing blow to all.
- Scenery and devices that can be destroyed (lights, panels) are the
map's, at the same index on every machine. A client reports its hits on
them like any other, and every client deals the host's damage to its own
copy, which breaks and plays its effects as the host's did.
*/
#include "cseries.h"
#include "cseries/errors.h"
#include "game/game.h"
#include "game/game_globals.h"
#include "game/players.h"
@@ -57,6 +63,8 @@ being hit looks and feels like on the clients).
void damage_replay_player_effect(long player_index, struct damage_data *damage, real total_damage);
void damage_replay_aftermath(long object_index, struct damage_data *damage, unsigned long being_damaged_flags,
real shield_damage, real body_damage, real body_damage_multiplier, short body_part);
void damage_replay_static(long object_index, struct damage_data *damage, short node_index, short region_index,
short material_index);
void damage_replay_kill(long object_index, struct damage_data *damage, short node_index, short region_index,
short material_index);
/* network_distributed.c's */
@@ -81,6 +89,13 @@ enum
died or dropped the launcher) */
MAXIMUM_RECENT_WEAPONS = 8,
RECENT_WEAPON_TICKS = 10 * TICKS_PER_SECOND,
/* the grenades a player has thrown lately, as the host's own game threw
them, whose damage the host takes from them: each goes off once, within
RECENT_WEAPON_TICKS of its throw. Each player has their own (all of
MAXIMUM_TRACKED_PLAYERS); one that hits nothing is kept the whole time,
so this is more than a player throws in it, infinite grenades and all
(a throw takes most of a second); past it the oldest gives way */
MAXIMUM_GRENADE_THROWS = 32,
/* a player's hits: a bucket of this many seconds of fire, filling at a
second a second, a hit taking what one of its weapon's projectiles
takes to fire (at twice the weapon's fastest rate: a shotgun's
@@ -133,8 +148,14 @@ enum
_damage_event_player_effect,
_damage_event_aftermath,
_damage_event_kill,
/* damage to destructible scenery or a device, to every machine */
_damage_event_static,
};
/* the objects every machine has from the map, at the same index, which
damage can break (static_target) */
#define STATIC_TARGET_TYPES (_object_mask_scenery | _object_mask_device)
/* what a player's damage is, by how the game deals it (the shape of their
report of it) */
enum
@@ -341,8 +362,14 @@ static struct
{
long definition_indices[MAXIMUM_RECENT_WEAPONS];
long times[MAXIMUM_RECENT_WEAPONS];
long grenade_times[NUMBER_OF_UNIT_GRENADE_TYPES];
/* (the grenades thrown, not yet gone off: NONE for none) */
long throw_times[MAXIMUM_GRENADE_THROWS];
short throw_types[MAXIMUM_GRENADE_THROWS];
long driven_time;
/* (the blow of the player's unit with no weapon: units.c's
unit_unarmed_melee_damage) */
long unarmed_melee_damage_index;
long unarmed_time;
real hit_seconds;
long hit_seconds_time;
} damage_players[MAXIMUM_TRACKED_PLAYERS];
@@ -452,7 +479,8 @@ static boolean distributed_damage_to_data(
if (!tag_index_is_group(damage->definition_index, DAMAGE_EFFECT_DEFINITION_TAG))
return FALSE;
damage_data_new(result, damage->definition_index);
result->flags = damage->flags;
/* (the flags the game has) */
result->flags = damage->flags & (FLAG(NUMBER_OF_DAMAGE_DATA_FLAGS) - 1);
result->owner_player_index = distributed_player_from_byte(damage->owner_player_index);
result->owner_team_index = damage->owner_team_index;
result->owner_object_index = distributed_object_index_valid(damage->owner_object_index) &&
@@ -463,11 +491,22 @@ static boolean distributed_damage_to_data(
result->scale = damage->scale;
result->multiplier = damage->multiplier;
result->material_effect_scale = damage->material_effect_scale;
result->material_type = damage->material_type;
/* (a material the game has, else none, damage_data_new's: it indexes
the materials' modifiers) */
if (damage->material_type >= 0 && damage->material_type < NUMBER_OF_MATERIAL_TYPES)
result->material_type = damage->material_type;
scenario_location_from_point(&result->location, &result->epicenter);
return TRUE;
}
/* whether the object is scenery or a device that damage can destroy */
static boolean static_target(
long object_index)
{
return object_try_and_get_and_verify_type(object_index, STATIC_TARGET_TYPES) &&
object_get_maximum_body_vitality(object_index, TRUE) > 0.0f;
}
/* ---------- object_cause_damage (damage.c) */
/* whether the damage is a weapon's own at the unit that fires it (weapons.c:
@@ -614,7 +653,8 @@ boolean network_damage_deals(
damage_replay_player_effect(unit->unit.player_index, &effect, 0.0f);
}
/* a hit of this machine's own player's, on the host's object */
if (distributed_player_is_local(damage->owner_player_index) && network_objects_client_has(object_index) &&
if (distributed_player_is_local(damage->owner_player_index) &&
(network_objects_client_has(object_index) || static_target(object_index)) &&
damage_report_count < MAXIMUM_HIT_REPORTS_PER_TICK)
{
struct distributed_hit_report *report = &damage_reports[damage_report_count++];
@@ -702,6 +742,33 @@ void network_damage_player_effect(
/* the host: an object damaged, and what that did (object_damage_aftermath
done, so that a kill's killer is known) */
/* a damage event of this tick for the object, or NULL once the tick's are
spent (the last RESERVED_KILL_EVENTS are kept for killing blows) */
static struct distributed_damage_event *damage_event_new(
byte kind,
long object_index,
struct damage_data const *damage,
short node_index,
short region_index,
short material_index,
boolean kill)
{
struct distributed_damage_event *event;
if (damage_event_count >= MAXIMUM_DAMAGE_EVENTS_PER_TICK - (kill ? 0 : RESERVED_KILL_EVENTS))
return NULL;
event = &damage_events[damage_event_count++];
csmemset(event, 0, sizeof(*event));
event->kind = kind;
event->player_index = NO_PLAYER;
event->object_index = object_index;
distributed_damage_from_data(damage, &event->damage);
event->node_index = node_index;
event->region_index = region_index;
event->material_index = material_index;
return event;
}
void network_damage_aftermath(
long object_index,
struct damage_data const *damage,
@@ -721,6 +788,11 @@ void network_damage_aftermath(
if (game_connection() != _game_connection_network_server)
return;
if (static_target(object_index))
{
damage_event_new(_damage_event_static, object_index, damage, node_index, region_index, material_index, FALSE);
return;
}
/* (units only: items and the like the objects' states place) */
unit = (struct unit_datum *)object_try_and_get_and_verify_type(object_index, _object_mask_unit);
if (!unit)
@@ -734,22 +806,15 @@ void network_damage_aftermath(
(victim_player_index != NONE ||
(TEST_FLAG(_object_mask_biped, unit->object.type) &&
!TEST_FLAG(being_damaged_flags, _object_being_damaged_body_destroyed_bit)));
if (damage_event_count >= MAXIMUM_DAMAGE_EVENTS_PER_TICK - (kill ? 0 : RESERVED_KILL_EVENTS))
event = damage_event_new(_damage_event_aftermath, object_index, damage, node_index, region_index, material_index,
kill);
if (!event)
return;
event = &damage_events[damage_event_count++];
csmemset(event, 0, sizeof(*event));
event->kind = _damage_event_aftermath;
event->player_index = NO_PLAYER;
event->object_index = object_index;
distributed_damage_from_data(damage, &event->damage);
event->being_damaged_flags = being_damaged_flags;
event->shield_damage = shield_damage;
event->body_damage = body_damage;
event->body_damage_multiplier = body_damage_multiplier;
event->body_part = body_part;
event->node_index = node_index;
event->region_index = region_index;
event->material_index = material_index;
/* a killing blow, a player's with who the host says dealt it */
if (kill)
{
@@ -1074,21 +1139,74 @@ static real distributed_source_deals(
return distributed_source_deals_uncached(source_index, damage_index, grenade, kinds, reach);
}
/* the oldest grenade of the type the host saw the player throw within
RECENT_WEAPON_TICKS that has not gone off yet (network_damage_note_grenade),
NONE for none */
static short distributed_grenade_throw(
short player_index,
short grenade_type)
{
short index;
short oldest = NONE;
for (index = 0; index < MAXIMUM_GRENADE_THROWS; index++)
{
long time = damage_players[player_index].throw_times[index];
if (time != NONE && damage_players[player_index].throw_types[index] == grenade_type &&
game_time_get() - time <= RECENT_WEAPON_TICKS &&
(oldest == NONE || time < damage_players[player_index].throw_times[oldest]))
{
oldest = index;
}
}
return oldest;
}
/* whether an explosion of the damage is among the player's this tick
(distributed_report_paid): at the epicenter, or (NULL) anywhere */
static boolean distributed_explosion_this_tick(
short player_index,
long damage_index,
real_point3d const *epicenter)
{
short index;
if (damage_explosions[player_index].time != game_time_get())
return FALSE;
for (index = 0; index < damage_explosions[player_index].count; index++)
{
if (damage_explosions[player_index].explosions[index].definition_index == damage_index &&
(!epicenter || (damage_explosions[player_index].explosions[index].epicenter.x == epicenter->x &&
damage_explosions[player_index].explosions[index].epicenter.y == epicenter->y &&
damage_explosions[player_index].explosions[index].epicenter.z == epicenter->z)))
{
return TRUE;
}
}
return FALSE;
}
/* how many hits a second the player could deal of the damage: their
weapons, lately, their grenades, the vehicle they drove lately; 0 when none
of them deals it; how they deal it (the _damage_source flags); and how far
from them what they fire deals it */
weapons, lately, a grenade the host saw them throw that has not gone off
(or one going off this tick, hitting more than one object), the vehicle
they drove lately; 0 when none of them deals it; how they deal it (the
_damage_source flags); how far from them what they fire deals it; and the
type of grenade when nothing but a grenade of theirs deals it, else NONE */
static real distributed_player_deals(
short player_index,
long damage_index,
byte *kinds,
struct damage_reach *reach)
struct damage_reach *reach,
short *grenade_type)
{
struct game_globals *globals = scenario_get_game_globals();
short index;
real rate = 0.0f;
real grenade_rate = 0.0f;
*kinds = 0;
*grenade_type = NONE;
reach->distance = 0.0f;
reach->ticks = 0.0f;
reach->carried_ticks = 0.0f;
@@ -1108,24 +1226,12 @@ static real distributed_player_deals(
*kinds |= weapon_kinds;
}
}
for (index = 0; index < globals->grenades.count && index < NUMBER_OF_UNIT_GRENADE_TYPES; index++)
/* (a blow with no weapon: the unit's own, units.c) */
if (damage_players[player_index].unarmed_melee_damage_index == damage_index && damage_index != NONE &&
game_time_get() - damage_players[player_index].unarmed_time <= RECENT_WEAPON_TICKS)
{
struct game_globals_grenade *grenade = TAG_BLOCK_GET_ELEMENT(&globals->grenades, index,
struct game_globals_grenade);
if (game_time_get() - damage_players[player_index].grenade_times[index] <= RECENT_WEAPON_TICKS &&
grenade->projectile.index != NONE)
{
byte grenade_kinds;
struct damage_reach grenade_reach;
real grenade_rate = distributed_source_deals(grenade->projectile.index, damage_index, TRUE, &grenade_kinds,
&grenade_reach);
if (grenade_rate > 0.0f)
distributed_reach_combine(reach, &grenade_reach);
rate = MAX(rate, grenade_rate);
*kinds |= grenade_kinds;
}
rate = MAX(rate, MELEE_HITS_PER_SECOND);
SET_FLAG(*kinds, _damage_source_melee_bit, TRUE);
}
/* (a vehicle's collision is its driver's, physics.c; the damage it deals
a unit it kills is no one's) */
@@ -1141,6 +1247,36 @@ static real distributed_player_deals(
SET_FLAG(*kinds, _damage_source_collision_bit, TRUE);
}
}
/* (a grenade's damage only from one thrown: holding grenades deals
nothing) */
for (index = 0; index < globals->grenades.count && index < NUMBER_OF_UNIT_GRENADE_TYPES; index++)
{
struct game_globals_grenade *grenade = TAG_BLOCK_GET_ELEMENT(&globals->grenades, index,
struct game_globals_grenade);
boolean thrown = distributed_grenade_throw(player_index, index) != NONE;
if ((thrown || distributed_explosion_this_tick(player_index, damage_index, NULL)) &&
grenade->projectile.index != NONE)
{
byte this_kinds;
struct damage_reach this_reach;
real this_rate = distributed_source_deals(grenade->projectile.index, damage_index, TRUE, &this_kinds,
&this_reach);
if (this_rate > 0.0f)
{
distributed_reach_combine(reach, &this_reach);
/* (one with a throw left before one only going off now) */
if (*grenade_type == NONE || thrown)
*grenade_type = index;
}
grenade_rate = MAX(grenade_rate, this_rate);
*kinds |= this_kinds;
}
}
if (rate > 0.0f)
*grenade_type = NONE;
rate = MAX(rate, grenade_rate);
return rate * HIT_REPORT_RATE_MARGIN;
}
@@ -1409,6 +1545,8 @@ static void distributed_note_weapons(
if (player_index >= MAXIMUM_TRACKED_PLAYERS || unit_index == NONE)
continue;
damage_players[player_index].unarmed_melee_damage_index = unit_unarmed_melee_damage(unit_index);
damage_players[player_index].unarmed_time = game_time_get();
units[0] = unit_index;
units[1] = object_get(unit_index)->object.parent_object_index;
/* (a vehicle's collisions are its driver's, physics.c) */
@@ -1419,16 +1557,6 @@ static void distributed_note_weapons(
if (vehicle && vehicle->unit.driver_object_index == unit_index)
damage_players[player_index].driven_time = game_time_get();
}
{
struct unit_datum *unit = unit_get(unit_index);
short grenade_index;
for (grenade_index = 0; grenade_index < NUMBER_OF_UNIT_GRENADE_TYPES; grenade_index++)
{
if (unit->unit.grenade_counts[grenade_index] > 0)
damage_players[player_index].grenade_times[grenade_index] = game_time_get();
}
}
for (unit_number = 0; unit_number < 2; unit_number++)
{
struct unit_datum *unit = units[unit_number] != NONE ?
@@ -1626,6 +1754,8 @@ static boolean distributed_report_valid(
real reach;
struct damage_reach fired_reach;
short seen;
short grenade_type;
short throw_index = NONE;
/* that machine's player */
if (player_index == NO_PLAYER || player_index >= MAXIMUM_TRACKED_PLAYERS ||
@@ -1639,6 +1769,8 @@ static boolean distributed_report_valid(
return FALSE;
target = (struct object_datum *)object_try_and_get_and_verify_type(report->object_index,
_object_mask_biped | _object_mask_vehicle | _object_mask_weapon | _object_mask_equipment);
if (!target && static_target(report->object_index))
target = object_get(report->object_index);
if (!target || !tag_index_is_group(report->damage.definition_index, DAMAGE_EFFECT_DEFINITION_TAG))
return FALSE;
/* numbers the game can take */
@@ -1658,7 +1790,8 @@ static boolean distributed_report_valid(
return FALSE;
ticks = MIN(now - report->host_time + TARGET_HISTORY_SLACK_TICKS, TARGET_HISTORY_TICKS - 1);
/* damage that player could deal, and how they deal it */
rate = distributed_player_deals(player_index, report->damage.definition_index, &kinds, &fired_reach);
rate = distributed_player_deals(player_index, report->damage.definition_index, &kinds, &fired_reach,
&grenade_type);
if (rate <= 0.0f)
return FALSE;
/* ... of its shape: at a point, else a melee blow, else a collision; and
@@ -1700,12 +1833,26 @@ static boolean distributed_report_valid(
reach += definition->cutoff_radius;
if (!(distributed_distance_squared(&report->damage.origin, &report->target_position) <= reach * reach))
return FALSE;
/* what only a grenade of theirs deals, from one the host saw them throw
that has not gone off: its explosion once (the explosion's other objects
that tick come free, distributed_report_paid), what it hits at a point
while it is about */
if (grenade_type != NONE &&
!distributed_explosion_this_tick(player_index, report->damage.definition_index, &report->damage.epicenter))
{
throw_index = distributed_grenade_throw(player_index, grenade_type);
if (throw_index == NONE)
return FALSE;
}
/* no more than the weapon fires */
if (!distributed_report_paid(player_index, report, rate, area && !melee && !collision,
definition->cutoff_radius > 0.0f))
{
return FALSE;
}
/* (the grenade gone off: its throw spent) */
if (throw_index != NONE && area && !melee && !collision)
damage_players[player_index].throw_times[throw_index] = NONE;
/* a melee blow from where the host had the player (its origin their
head, its epicenter their body); a collision from the vehicle they
drove */
@@ -1926,6 +2073,26 @@ static boolean distributed_event_goes_to(
return FALSE;
}
/* How often the host logs how many client hits it accepted and refused
(only when it refused some). Tells "my shots don't register" apart from
"I missed". */
#define REPORT_LOG_TICKS (30 * TICKS_PER_SECOND)
static void distributed_log_reports(
void)
{
static long logged_dealt;
static long logged_rejected;
if (game_time_get() % REPORT_LOG_TICKS != 0 || damage_rejected_reports == logged_rejected)
return;
error(_error_silent, "damage: clients' hits in the last %d seconds: %ld dealt, %ld refused",
REPORT_LOG_TICKS / TICKS_PER_SECOND, damage_dealt_reports - logged_dealt,
damage_rejected_reports - logged_rejected);
logged_dealt = damage_dealt_reports;
logged_rejected = damage_rejected_reports;
}
void network_damage_host_tick(
void)
{
@@ -1936,6 +2103,7 @@ void network_damage_host_tick(
distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES) : 0;
short machine_number;
distributed_log_reports();
distributed_note_weapons();
distributed_note_targets();
distributed_note_event_destinations();
@@ -2044,6 +2212,15 @@ void network_damage_handle_events(
players_show_telefragged(victim_player_index);
}
break;
case _damage_event_static:
if (distributed_object_index_valid(event->object_index) && static_target(event->object_index) &&
distributed_damage_indices_valid(event->object_index, event->node_index, event->region_index,
event->material_index))
{
damage_replay_static(event->object_index, &damage, event->node_index, event->region_index,
event->material_index);
}
break;
}
}
}
@@ -2089,6 +2266,41 @@ void network_damage_statistics(
*replayed_events = damage_replayed_events;
}
/* the host: a grenade a player's unit threw (units.c), as the host's own
game threw it, which that player's machine reports the damage of
(distributed_report_valid) */
void network_damage_note_grenade(
long unit_index,
short grenade_type)
{
struct unit_datum *unit = unit_get(unit_index);
long player_index;
short index;
short slot = 0;
if (game_connection() != _game_connection_network_server || unit->unit.player_index == NONE ||
grenade_type < 0 || grenade_type >= NUMBER_OF_UNIT_GRENADE_TYPES)
{
return;
}
player_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(unit->unit.player_index);
if (player_index >= MAXIMUM_TRACKED_PLAYERS)
return;
/* (in a free place, else the oldest's) */
for (index = 0; index < MAXIMUM_GRENADE_THROWS; index++)
{
if (damage_players[player_index].throw_times[index] == NONE)
{
slot = index;
break;
}
if (damage_players[player_index].throw_times[index] < damage_players[player_index].throw_times[slot])
slot = index;
}
damage_players[player_index].throw_times[slot] = game_time_get();
damage_players[player_index].throw_types[slot] = grenade_type;
}
void network_damage_new_game(
void)
{
@@ -2107,8 +2319,9 @@ void network_damage_new_game(
for (index = 0; index < MAXIMUM_RECENT_WEAPONS; index++)
damage_players[player_index].definition_indices[index] = NONE;
for (index = 0; index < NUMBER_OF_UNIT_GRENADE_TYPES; index++)
damage_players[player_index].grenade_times[index] = -RECENT_WEAPON_TICKS - 1;
damage_players[player_index].unarmed_melee_damage_index = NONE;
for (index = 0; index < MAXIMUM_GRENADE_THROWS; index++)
damage_players[player_index].throw_times[index] = NONE;
damage_players[player_index].driven_time = -RECENT_WEAPON_TICKS - 1;
damage_players[player_index].hit_seconds = HIT_REPORT_BURST_SECONDS;
damage_explosions[player_index].time = NONE;
+304 -19
View File
@@ -53,18 +53,24 @@ machine (their datum identifiers need not be).
#include "cseries.h"
#include "cseries/errors.h"
#include "cache/cache_files.h"
#include "models/model_animation_definitions.h"
#include "game/game.h"
#include "game/game_globals.h"
#include "game/players.h"
#include "game/game_engine.h"
#include "main/main.h"
#include "game/player_queues_new.h"
#include "networking/network_game_globals.h"
#include "objects/objects.h"
#include "objects/damage.h"
#include "scenario/scenario.h"
#include "scenario/scenario_definitions.h"
#include "structures/structure_bsp_definitions.h"
#include "units/units.h"
#include "units/biped_definitions.h"
#include "units/bipeds.h"
#include "network_coop.h"
#include "network_distributed.h"
#include <limits.h>
@@ -89,7 +95,7 @@ void game_engine_read_network_state(byte const *buffer, long size);
/* physics.c's (world units a tick, each tick) */
extern real global_gravity;
/* network_server_manager.c's, cseries_windows.c's, console.c's, p2p.c's */
void network_game_server_kick_machine(long machine_index);
void network_game_server_kick_machine(long machine_index, boolean kept_out);
unsigned long network_game_server_machine_address(long machine_index);
char const *network_game_server_machine_hardware_id(long machine_index);
void p2p_hardware_id_sanitize(char *destination, int size, const char *source);
@@ -238,11 +244,8 @@ before the host takes it no longer, and before the client is put where the
host has it (no further than that: between the two they would disagree for
good, the host's player somewhere its own is not) */
#define HOST_ACCEPT_TOLERANCE 3.5f
#define REMOTE_CORRECTION_TOLERANCE 0.05f
#define LOCAL_CORRECTION_TOLERANCE 3.0f
/* how far from the origin a unit is (world units), and how fast a client's
own player's unit moves at most (world units a tick) */
#define UNIT_WORLD_BOUND 32768.0f
/* how fast a client's own player's unit moves at most (world units a tick) */
#define MAXIMUM_PREDICTED_SPEED 2.0f
/* ... on foot: this many times as fast as a player runs and jumps, or as
fast as the host's ticks threw its copy lately, whichever is more; and how
@@ -317,7 +320,9 @@ ticks before it */
struct distributed_player_input
{
byte player_index;
byte pad[3];
/* co-op: the structure BSP the client has loaded */
byte structure_bsp_index;
byte pad[2];
/* the client's tick */
long tick;
/* the latest host tick the client has had a message of (the host
@@ -583,7 +588,10 @@ static long distributed_received_times[MAXIMUM_SENDERS][NUMBER_OF_DISTRIBUTED_ME
/* the host: each client machine's clock, measured (CLIENT_CLOCK_WINDOW_MILLISECONDS):
its latest tick, and its tick and the host's time as the window began
(NONE: none begun); how many windows in a row it went fast, and whether
the last did */
the last did; and whether, since they began, a message that came over its
stream was that far ahead too (a datagram's source address is all that
says which machine sent it, and anyone can send one as from another's: a
machine is banned only on its stream's word, distributed_note_client_clock) */
/* the host: each client machine's Discord user, as it told it, the text
kept only of what is allowed (p2p_discord_sanitize) */
static struct distributed_client_identity distributed_client_identities[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
@@ -597,7 +605,11 @@ static struct distributed_client_clock
unsigned long window_time;
short fast_windows;
boolean fast;
boolean ahead_on_stream;
} distributed_client_clocks[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
/* the host: whether the message being handled came over its machine's
stream (network_distributed_handle_stream_message) */
static boolean distributed_handling_stream_message;
/* a client: the host's latest tick it has had a message of */
static long distributed_host_time = NONE;
/* the host: each client's round trip, in ticks, and its jitter */
@@ -666,6 +678,40 @@ boolean distributed_point_valid(
return fabsf(point->x) <= bound && fabsf(point->y) <= bound && fabsf(point->z) <= bound;
}
/* whether a tag index from the host really is a tag of that group */
boolean distributed_tag_of_group(
long tag_index,
unsigned long group_tag)
{
struct tag_iterator iterator;
long index;
tag_iterator_new(&iterator, group_tag);
while ((index = tag_iterator_next(&iterator)) != NONE)
{
if (index == tag_index)
return TRUE;
}
return FALSE;
}
/* the graph's animation, if the graph tag and index are valid */
struct animation *distributed_graph_animation(
long animation_graph_index,
short animation_index)
{
struct animation_graph *graph;
if (!distributed_tag_of_group(animation_graph_index, ANIMATION_GRAPH_TAG))
return NULL;
graph = animation_graph_definition_get(animation_graph_index);
if (animation_index < 0 || animation_index >= graph->animations.count)
return NULL;
return TAG_BLOCK_GET_ELEMENT(&graph->animations, animation_index, struct animation);
}
boolean distributed_object_index_valid(
long object_index)
{
@@ -749,7 +795,7 @@ void distributed_unit_vector_unpack(
}
}
static word distributed_vitality_pack(
word distributed_vitality_pack(
real value)
{
value *= VITALITY_SCALE;
@@ -757,15 +803,13 @@ static word distributed_vitality_pack(
return (word)(long)floor(value + 0.5f);
}
static real distributed_vitality_unpack(
real distributed_vitality_unpack(
word value)
{
return (real)value / VITALITY_SCALE;
}
/* an angle as a 16-bit fraction of a turn, and back (yaw from 0 to 2 pi,
pitch from -pi to pi) */
static short distributed_angle_pack(
short distributed_angle_pack(
real angle)
{
real turns = angle / (2.0f * _pi);
@@ -774,7 +818,7 @@ static short distributed_angle_pack(
return (short)(word)((long)floor(turns * 65536.0f + 0.5f) & 0xFFFF);
}
static real distributed_angle_unpack(
real distributed_angle_unpack(
short value,
boolean signed_angle)
{
@@ -1585,6 +1629,14 @@ static void distributed_correct_own_unit(
short local_player_index = player->local_player_index;
real_point3d position = state->position;
/* (co-op: the host has its player in a BSP this machine hasn't loaded
yet, the switch still on its way. Moved there, it would fall with no
floor under it, so it stays on the floor it has until the BSP loads.) */
if (network_coop_active() && scenario_leaf_index_from_point(&state->position) == NONE &&
scenario_leaf_index_from_point(&object_get(unit_index)->object.position) != NONE)
{
return;
}
if (TEST_FLAG(state->flags, _distributed_unit_predicted_bit) &&
local_player_index >= 0 && local_player_index < MAXIMUM_LOCAL_PLAYERS)
{
@@ -2048,9 +2100,15 @@ static void distributed_apply_predictions(
{
distributed_predictions[player_index].valid = FALSE;
distributed_predictions[player_index].taken_host_time = NONE;
/* (of the unit it has now: not one of a life before) */
if (unit_index != NONE && unit_index == state->unit_index)
/* (of the unit it has now: not one of a life before; and not
from a co-op client still loading the host's BSP, whose player
falls there with no floor under it) */
if (unit_index != NONE && unit_index == state->unit_index &&
unit_get(unit_index)->unit.player_index != NONE &&
network_coop_player_has_structure_bsp(unit_get(unit_index)->unit.player_index))
{
distributed_take_prediction(player_index, unit_index, &bound);
}
}
/* (what the host's next tick starts from) */
if (unit_index != NONE)
@@ -2222,6 +2280,7 @@ static void distributed_client_send_inputs(
if (!update_client_distributed_input(local_player_index, &input->tick, &input->action, input->control_flags))
continue;
input->player_index = distributed_player_to_byte(player_index);
input->structure_bsp_index = (byte)global_structure_bsp_index_get();
input->host_time = distributed_host_time;
count++;
}
@@ -2232,6 +2291,37 @@ static void distributed_client_send_inputs(
}
}
/* (the host, in co-op) where each client player's input last faced, by
absolute index: how far it turns between inputs is its looking around */
static struct
{
boolean valid;
real_euler_angles2d facing;
} distributed_input_facings[MAXIMUM_TRACKED_PLAYERS];
/* (the host, in co-op) a client player's input counted in the scripts'
action tests, as a local player's is (player_control_action_test_note) */
static void distributed_note_input_actions(
short player_index,
struct player_action const *action)
{
real_euler_angles2d turn = { 0.0f, 0.0f };
if (distributed_input_facings[player_index].valid)
{
turn.yaw = action->desired_facing.yaw - distributed_input_facings[player_index].facing.yaw;
turn.pitch = action->desired_facing.pitch - distributed_input_facings[player_index].facing.pitch;
/* (the short way round) */
if (turn.yaw > _pi)
turn.yaw -= 2.0f * _pi;
else if (turn.yaw < -_pi)
turn.yaw += 2.0f * _pi;
}
distributed_input_facings[player_index].valid = TRUE;
distributed_input_facings[player_index].facing = action->desired_facing;
player_control_action_test_note(action->control_flags, &turn, &action->throttle, action->primary_trigger);
}
/* (the host) a client's players' input, and from it (once a message) how
long a message takes that client and back */
static void distributed_handle_inputs(
@@ -2261,6 +2351,10 @@ static void distributed_handle_inputs(
action.throttle.i = PIN(action.throttle.i, -1.0f, 1.0f);
action.throttle.j = PIN(action.throttle.j, -1.0f, 1.0f);
action.primary_trigger = PIN(action.primary_trigger, 0.0f, 1.0f);
/* (a spectator's buttons pick whom it watches: not counted) */
if (network_coop_active() && player->unit_index != NONE)
distributed_note_input_actions(input->player_index, &action);
network_coop_note_player_structure_bsp(input->player_index, input->structure_bsp_index);
update_server_handle_distributed_input(DATUM_INDEX_NEW(input->player_index, player->identifier), input->tick,
&action, input->control_flags, DISTRIBUTED_INPUT_HISTORY);
if (input->host_time != NONE && (host_time == NONE || input->host_time > host_time))
@@ -2931,6 +3025,52 @@ static void distributed_send_pings(
}
}
/* Co-op: the host's structure BSP. Clients switch BSP only when told
(their own trigger volumes don't, players.c). A switch is sent at once and
reliably; it is also resent regularly so a client that joined late catches
up. */
#define STRUCTURE_BSP_INTERVAL_TICKS (TICKS_PER_SECOND / 2)
static short distributed_sent_structure_bsp_index = NONE;
struct distributed_structure_bsp
{
short structure_bsp_index;
short pad;
};
struct distributed_structure_bsp_message
{
struct distributed_message_header header;
struct distributed_structure_bsp structure_bsp;
};
static void distributed_send_structure_bsp(
void)
{
struct distributed_structure_bsp_message message;
short index = global_structure_bsp_index_get();
boolean switched = index != distributed_sent_structure_bsp_index;
message.structure_bsp.structure_bsp_index = index;
message.structure_bsp.pad = 0;
distributed_send(&message, _distributed_message_structure_bsp, 1, (word)sizeof(message),
switched ? _distributed_to_clients_reliably : _distributed_to_clients);
distributed_sent_structure_bsp_index = index;
}
static void distributed_handle_structure_bsp(
struct distributed_structure_bsp const *structure_bsp)
{
short index = structure_bsp->structure_bsp_index;
if (network_coop_active() && index >= 0 && index < global_scenario_get()->structure_bsp_references.count &&
index != global_structure_bsp_index_get())
{
main_switch_structure_bsp(index);
}
}
/* the players' statistics that changed since they were last sent, and when
refreshing, STATISTICS_REFRESH_PLAYERS more whatever they are, round them
all (a client that lost a change has it again within eight seconds) */
@@ -3076,6 +3216,7 @@ void network_distributed_new_game(
csmemset(distributed_accepted, 0, sizeof(distributed_accepted));
csmemset(distributed_host_speeds, 0, sizeof(distributed_host_speeds));
csmemset(distributed_round_trips, 0, sizeof(distributed_round_trips));
csmemset(distributed_input_facings, 0, sizeof(distributed_input_facings));
csmemset(distributed_client_clocks, 0, sizeof(distributed_client_clocks));
csmemset(distributed_client_identities, 0, sizeof(distributed_client_identities));
distributed_identity_sent = FALSE;
@@ -3136,6 +3277,8 @@ void network_distributed_new_game(
network_damage_new_game();
network_vr_new_game();
bots_new_game();
network_actors_new_game();
network_coop_new_game();
}
/* after each tick (game_time.c) */
@@ -3178,9 +3321,18 @@ void network_distributed_tick(
distributed_statistics_due = FALSE;
if (game_time_get() % PING_INTERVAL_TICKS == 0)
distributed_send_pings();
/* (before the players, so clients load a new BSP before they hear
where the host moved everyone into it) */
if (network_coop_active() && (global_structure_bsp_index_get() != distributed_sent_structure_bsp_index ||
game_time_get() % STRUCTURE_BSP_INTERVAL_TICKS == 0))
{
distributed_send_structure_bsp();
}
distributed_host_send_players();
network_vr_tick();
bots_tick();
network_actors_host_tick();
network_coop_host_tick();
distributed_send_pickups();
if (game_time_get() % GAME_STATE_INTERVAL_TICKS == 0)
distributed_send_game_state(NONE);
@@ -3193,6 +3345,7 @@ void network_distributed_tick(
distributed_client_send_predictions();
network_objects_client_tick();
network_damage_client_tick();
network_coop_client_tick();
}
distributed_batches_flush();
distributed_machines.in_tick = FALSE;
@@ -3221,6 +3374,19 @@ static boolean distributed_message_stale(
case _distributed_message_damage_events:
case _distributed_message_vr_poses:
case _distributed_message_pings:
case _distributed_message_actor_states:
case _distributed_message_structure_bsp:
case _distributed_message_coop_presentation:
case _distributed_message_coop_device_groups:
case _distributed_message_coop_object_names:
case _distributed_message_coop_skip_vote:
case _distributed_message_coop_events:
case _distributed_message_coop_object_transforms:
case _distributed_message_actor_damage:
case _distributed_message_coop_object_looks:
case _distributed_message_damage_animations:
case _distributed_message_coop_screen_effect:
case _distributed_message_coop_device_states:
break;
default:
return FALSE;
@@ -3523,6 +3689,27 @@ void network_distributed_ban(
}
}
/* (the host: network_server_manager.c, its kick command) players kicked by
the host, which may join again: every machine told, nothing kept (no line
in BANS_FILE) */
void network_distributed_kick(
char const *names)
{
char kept_names[64];
char notice[MAXIMUM_NOTICE_LENGTH];
distributed_printable(kept_names, sizeof(kept_names), names);
snprintf(notice, sizeof(notice), "%s kicked by the host", kept_names);
/* (to every client in the game: in the lobby, the host's own) */
if (game_in_progress())
distributed_send_notice(notice);
else
{
console_warning("%s", notice);
error(_error_log, "%s", notice);
}
}
/* (the host) a client machine's tick, which one of its messages is
stamped with: its clock measured, each window, against the host's; one
whose game runs fast (distributed_client_clock) has its players'
@@ -3538,6 +3725,8 @@ static void distributed_note_client_clock(
if (machine_index < 0 || machine_index >= HALO_PORT_MAXIMUM_NETWORK_MACHINES)
return;
clock = &distributed_client_clocks[machine_index];
if (distributed_handling_stream_message && tick - game_time_get() > CLIENT_CLOCK_AHEAD_TICKS)
clock->ahead_on_stream = TRUE;
if (clock->window_tick == NONE)
{
clock->latest_tick = tick;
@@ -3559,6 +3748,7 @@ static void distributed_note_client_clock(
if (!clock->fast)
{
clock->fast_windows = 0;
clock->ahead_on_stream = FALSE;
}
else if (++clock->fast_windows == 1)
{
@@ -3575,7 +3765,7 @@ static void distributed_note_client_clock(
char discord_id[DISCORD_ID_SIZE];
char discord_name[DISCORD_NAME_SIZE];
char discord[DISCORD_ID_SIZE + DISCORD_NAME_SIZE + 16] = "";
char reason[64];
char reason[96];
char text[MAXIMUM_NOTICE_LENGTH];
distributed_machine_player_names(machine_index, names, sizeof(names));
@@ -3585,14 +3775,30 @@ static void distributed_note_client_clock(
distributed_client_identities[machine_index].discord_name, 1);
if (discord_id[0] || discord_name[0])
snprintf(discord, sizeof(discord), " (Discord: %s, %s)", discord_name, discord_id);
snprintf(reason, sizeof(reason), "speed hack (game ran %.2f times as fast)", rate);
snprintf(reason, sizeof(reason), "speed hack (game ran %.2f times as fast)%s", rate,
clock->ahead_on_stream ? "" : " (its datagrams only: not banned)");
snprintf(text, sizeof(text), "%s%s kicked by the host: their game ran %.2f times as fast (a speed hack)",
names, discord, rate);
distributed_send_notice(text);
distributed_log_cheater(machine_index, names, reason);
/* banned, and kept out, only when its stream said so
too; on its datagrams' word alone (which another machine
could have sent as from its address), logged and dropped,
and it may join again */
if (clock->ahead_on_stream)
{
distributed_log_cheater(machine_index, names, reason);
}
else
{
char address[32];
distributed_machine_address_text(machine_index, address, sizeof(address));
distributed_write_player_record(CHEATERS_FILE, address, machine_index, names, reason);
}
}
network_game_server_kick_machine(machine_index);
network_game_server_kick_machine(machine_index, clock->ahead_on_stream);
clock->fast_windows = 0;
clock->ahead_on_stream = FALSE;
}
}
clock->window_tick = clock->latest_tick;
@@ -3658,6 +3864,19 @@ void network_distributed_handle_message(
case _distributed_message_unit_states: entry_size = DISTRIBUTED_UNIT_STATE_MINIMUM_SIZE; break;
case _distributed_message_player_statistics: entry_size = sizeof(struct distributed_player_statistics); break;
case _distributed_message_pings: entry_size = sizeof(struct distributed_player_ping); break;
case _distributed_message_actor_states: entry_size = network_actors_entry_size(); break;
case _distributed_message_structure_bsp: entry_size = sizeof(struct distributed_structure_bsp); break;
case _distributed_message_coop_presentation: entry_size = network_coop_presentation_entry_size(); break;
case _distributed_message_coop_events: entry_size = network_coop_event_entry_size(); break;
case _distributed_message_coop_skip_vote: entry_size = network_coop_skip_vote_entry_size(); break;
case _distributed_message_coop_device_groups: entry_size = network_coop_device_group_entry_size(); break;
case _distributed_message_coop_object_names: entry_size = network_coop_object_names_entry_size(); break;
case _distributed_message_coop_object_transforms: entry_size = network_coop_object_transform_entry_size(); break;
case _distributed_message_actor_damage: entry_size = network_actors_damage_entry_size(); break;
case _distributed_message_coop_object_looks: entry_size = network_coop_object_look_entry_size(); break;
case _distributed_message_damage_animations: entry_size = network_objects_damage_animation_entry_size(); break;
case _distributed_message_coop_screen_effect: entry_size = network_coop_screen_effect_entry_size(); break;
case _distributed_message_coop_device_states: entry_size = network_coop_device_state_entry_size(); break;
case _distributed_message_pickups: entry_size = sizeof(struct distributed_pickup); break;
case _distributed_message_player_inputs: entry_size = sizeof(struct distributed_player_input); break;
case _distributed_message_relayed_actions: entry_size = DISTRIBUTED_RELAYED_ACTION_MINIMUM_SIZE; break;
@@ -3676,6 +3895,19 @@ void network_distributed_handle_message(
{
return;
}
/* (the kinds whose handlers read one entry: not without it) */
switch (header.type)
{
case _distributed_message_structure_bsp:
case _distributed_message_coop_presentation:
case _distributed_message_coop_object_names:
case _distributed_message_coop_skip_vote:
if (header.count < 1)
return;
break;
default:
break;
}
distributed_statistics.received++;
/* (each kind from the host, or from a client; a VR player's hands
@@ -3699,6 +3931,7 @@ void network_distributed_handle_message(
case _distributed_message_hit_reports:
case _distributed_message_vehicle_prediction:
case _distributed_message_player_inputs:
case _distributed_message_coop_skip_vote:
if (machine_index == NONE || game_connection() != _game_connection_network_server)
return;
break;
@@ -3735,6 +3968,45 @@ void network_distributed_handle_message(
case _distributed_message_unit_states:
distributed_handle_unit_states((byte const *)entries, (byte const *)message + size, header.count);
break;
case _distributed_message_actor_states:
network_actors_handle_states(entries, header.count);
break;
case _distributed_message_structure_bsp:
distributed_handle_structure_bsp((struct distributed_structure_bsp const *)entries);
break;
case _distributed_message_coop_presentation:
network_coop_handle_presentation(entries, header.game_time);
break;
case _distributed_message_coop_events:
network_coop_handle_events(entries, header.count);
break;
case _distributed_message_coop_skip_vote:
network_coop_handle_skip_vote(machine_index, entries);
break;
case _distributed_message_coop_device_groups:
network_coop_handle_device_groups(entries, header.count);
break;
case _distributed_message_coop_object_names:
network_coop_handle_object_names(entries);
break;
case _distributed_message_coop_object_transforms:
network_coop_handle_object_transforms(entries, header.count);
break;
case _distributed_message_actor_damage:
network_actors_handle_damage(entries, header.count);
break;
case _distributed_message_coop_object_looks:
network_coop_handle_object_looks(entries, header.count);
break;
case _distributed_message_damage_animations:
network_objects_handle_damage_animations(entries, header.count);
break;
case _distributed_message_coop_screen_effect:
network_coop_handle_screen_effect(entries, header.count);
break;
case _distributed_message_coop_device_states:
network_coop_handle_device_states(entries, header.count);
break;
case _distributed_message_player_statistics:
{
/* the host's count of kills, deaths, ... */
@@ -3864,3 +4136,16 @@ void network_distributed_handle_message(
}
}
}
/* (the host: network_server_message_handler.c) a client machine's message
of the distributed kind that came over its stream, which no other machine
can send as it: as network_distributed_handle_message */
void network_distributed_handle_stream_message(
long machine_index,
word const *message,
word size)
{
distributed_handling_stream_message = TRUE;
network_distributed_handle_message(machine_index, message, size);
distributed_handling_stream_message = FALSE;
}
+115
View File
@@ -65,6 +65,47 @@ enum
/* every player's ping as the host measures it, every two seconds, for
the scoreboard (unreliable) */
_distributed_message_pings,
/* the host's actors' units: their control and state (network_actors.c).
A number of its own, clear of the kinds upstream adds: a build without
it drops the message as a kind it does not know, so the network version
stays upstream's and its players join as before */
_distributed_message_actor_states = 64,
/* co-op (a campaign map, no game engine): the host's structure BSP,
twice a second, for a client to switch to; numbered as actor_states */
_distributed_message_structure_bsp,
/* co-op: the host's cinematic, camera and screen fade, every tick
(network_coop.c) */
_distributed_message_coop_presentation,
/* (0.2.0's co-op script sounds; now part of coop_events. The number
stays taken so a 0.2.0 build never misreads a newer message.) */
_distributed_message_coop_sounds_retired,
/* co-op: device group state (network_coop.c) */
_distributed_message_coop_device_groups,
/* co-op: which named objects exist on the host (network_coop.c) */
_distributed_message_coop_object_names,
/* co-op: a client's vote to skip the cutscene, to the host (network_coop.c) */
_distributed_message_coop_skip_vote,
/* co-op: what the host's scripts did once: sounds, titles, HUD text,
animations (network_coop.c) */
_distributed_message_coop_events,
/* co-op: where the host's scenery and machines are, when they move
(network_coop.c) */
_distributed_message_coop_object_transforms,
/* the damage the host's AI units are taking, which their shields'
flares are drawn by (network_actors.c); 73 in upstream's AI sync too */
_distributed_message_actor_damage = 73,
/* co-op: how the host's objects look (their permutations, scale), when
that changes (network_coop.c) */
_distributed_message_coop_object_looks,
/* the flinch and death animations the host picked for its units, which a
client plays rather than its own picks (network_objects.c) */
_distributed_message_damage_animations = 75,
/* co-op: the host's cinematic screen effect (blur, filters, video), when
it changes and every two seconds (network_coop.c) */
_distributed_message_coop_screen_effect = 76,
/* co-op: where the host's devices are (doors, elevators), when they move
(network_coop.c) */
_distributed_message_coop_device_states = 77,
/* The VR port's own messages, numbered far past the game's (240 on),
so that messages upstream adds never take their numbers: a build
@@ -93,6 +134,10 @@ tick, drawn as it goes; further, it is put there, drawn gliding
#define HOST_BLEND_DISTANCE 0.25f
#define HOST_VEHICLE_BLEND_DISTANCE 0.5f
#define REMOTE_BLEND_DISTANCE 1.0f
/* world units: how far a remote unit may be from where the host says
before it is put there, and how far from the origin a unit can be */
#define REMOTE_CORRECTION_TOLERANCE 0.05f
#define UNIT_WORLD_BOUND 32768.0f
#define REMOTE_VEHICLE_BLEND_DISTANCE 2.0f
enum
@@ -190,6 +235,10 @@ never 0, which any object at the index matches) */
boolean distributed_real_valid(real value);
boolean distributed_point_valid(real_point3d const *point, real bound);
boolean distributed_object_index_valid(long object_index);
/* whether a tag index from the host really is a tag of that group */
boolean distributed_tag_of_group(long tag_index, unsigned long group_tag);
/* the graph's animation, if a graph tag and animation index from the host are valid */
struct animation *distributed_graph_animation(long animation_graph_index, short animation_index);
/* ... an orientation's two axes (unpacked): TRUE when they are one long
and about square, then made exactly so */
boolean distributed_axes_make_valid(real_vector3d *forward, real_vector3d *up);
@@ -200,12 +249,70 @@ void distributed_unit_vector_unpack(struct distributed_vector const *vector, rea
#define DISTRIBUTED_UNIT_SCALE 32767.0f
#define DISTRIBUTED_VELOCITY_SCALE 1024.0f
#define DISTRIBUTED_ANGULAR_VELOCITY_SCALE 4096.0f
/* an angle as a 16-bit fraction of a turn, and back (yaw from 0 to 2 pi,
pitch from -pi to pi) */
short distributed_angle_pack(real angle);
real distributed_angle_unpack(short value, boolean signed_angle);
/* shields and health in 16 bits */
word distributed_vitality_pack(real value);
real distributed_vitality_unpack(word value);
/* ---------- prototypes/NETWORK_ACTORS.C */
void network_actors_new_game(void);
/* (the host, after each tick) the units its actors drove this tick, to
each client */
void network_actors_host_tick(void);
/* (a client) the host's word on its actors' units */
void network_actors_handle_states(void const *entries, short count);
word network_actors_entry_size(void);
/* (a client, in its tick where the host runs its actors) each actor's unit
given the control the host last sent for it */
void network_actors_drive(void);
/* ---------- prototypes/NETWORK_COOP.C */
void network_coop_new_game(void);
/* after each tick, on the host and on a client */
void network_coop_host_tick(void);
void network_coop_client_tick(void);
/* the message handlers, and their entry sizes */
word network_coop_presentation_entry_size(void);
void network_coop_handle_presentation(void const *entries, long host_time);
word network_coop_event_entry_size(void);
void network_coop_handle_events(void const *entries, short count);
word network_coop_device_group_entry_size(void);
void network_coop_handle_device_groups(void const *entries, short count);
word network_coop_object_names_entry_size(void);
void network_coop_handle_object_names(void const *entries);
word network_coop_object_transform_entry_size(void);
word network_coop_object_look_entry_size(void);
void network_coop_handle_object_looks(void const *entries, short count);
word network_coop_screen_effect_entry_size(void);
word network_coop_device_state_entry_size(void);
void network_coop_handle_device_states(void const *entries, short count);
void network_coop_handle_screen_effect(void const *entries, short count);
word network_actors_damage_entry_size(void);
void network_actors_handle_damage(void const *entries, short count);
void network_coop_handle_object_transforms(void const *entries, short count);
word network_coop_skip_vote_entry_size(void);
void network_coop_handle_skip_vote(long machine_index, void const *entries);
/* game_sound.c: a sound the host's scripts played */
enum
{
_coop_sound_impulse,
_coop_sound_looping_start,
_coop_sound_looping_stop,
};
void network_coop_note_sound(short kind, long definition_index, long object_index, real scale);
/* ---------- prototypes/NETWORK_OBJECTS.C */
void network_objects_new_game(void);
/* after each tick */
void network_objects_host_tick(void);
word network_objects_damage_animation_entry_size(void);
void network_objects_handle_damage_animations(void const *entries, short count);
void network_objects_client_tick(void);
/* (the host) a client has loaded the game and asks for the host's objects:
again, having failed to make one of them */
@@ -214,6 +321,8 @@ void network_objects_handle_changes(void const *entries, short count);
void network_objects_handle_synchronized(void);
void network_objects_handle_states(void const *entries, short count);
void network_objects_handle_inventories(void const *entries, short count);
/* (a client) its own player picked up the weapon, to ready once its unit has it */
void network_objects_client_picked_up_weapon(short local_player_index, long unit_index, long definition_index);
void network_objects_handle_vehicle_prediction(long machine_index, void const *entries, short count);
/* (the host) the vehicle predictions come in since the last tick, taken */
void network_objects_apply_vehicle_predictions(void);
@@ -230,10 +339,16 @@ boolean network_objects_reconcile(long object_index, real_point3d const *positio
real blend_distance);
/* a unit in the vehicle's seat as the host has it (NONE: in none) */
void network_objects_set_seat(long unit_index, long vehicle_index, short seat_index);
/* (the host) the ticks between sends to a machine of a unit at position, by
how near that machine's players are (network_actors.c) */
short network_objects_send_period(long machine_index, real_point3d const *position);
/* ---------- prototypes/NETWORK_DAMAGE.C */
void network_damage_new_game(void);
/* the host: a grenade a player's unit threw (units.c), whose damage that
player's machine reports */
void network_damage_note_grenade(long unit_index, short grenade_type);
void network_damage_host_tick(void);
void network_damage_client_tick(void);
void network_damage_handle_events(void const *entries, short count);
+503 -21
View File
@@ -50,6 +50,7 @@ same datum index (identifier and all), so that any message can name one:
#include "objects/objects.h"
#include "objects/damage.h"
#include "objects/object_definitions.h"
#include "models/model_animation_definitions.h"
#include "models/model_definitions.h"
#include "units/units.h"
#include "units/biped_definitions.h"
@@ -59,6 +60,9 @@ same datum index (identifier and all), so that any message can name one:
#include "items/weapons.h"
#include "items/weapon_definitions.h"
#include "items/equipment_definitions.h"
#include "camera/observer.h"
#include "cutscene/cinematics.h"
#include "network_coop.h"
#include "network_distributed.h"
#include <math.h>
@@ -175,6 +179,9 @@ enum
/* ... the round trip without one measured, and the most */
DEFAULT_OWN_ROUND_TRIP_TICKS = 6,
MAXIMUM_OWN_ROUND_TRIP_TICKS = 60,
/* ... how long the weapon the host says it picked up may take to reach
its unit, to be readied (the host's messages are not in step) */
PICKED_UP_WEAPON_TICKS = 3 * TICKS_PER_SECOND,
/* the most grenades of a kind a unit carries, as the host says (a dead
unit drops each) */
MAXIMUM_INVENTORY_GRENADES = 16,
@@ -238,7 +245,11 @@ struct distributed_object_change
byte change;
byte flags;
byte owner_player_index;
byte pad;
/* which bitmap of its shaders it draws with, when not its tag's (0). An
AI unit's actor variant sets it after the unit is made
(actor_customize_unit): the Elite major's and commander's armor are 1
and 2. Builds before it sent 0 here. */
byte forced_shader_permutation_index;
long object_index;
long definition_index;
short owner_team_index;
@@ -326,11 +337,14 @@ static struct
} objects_host_inventories[MAXIMUM_TRACKED_OBJECTS];
/* ... each client machine, found once a tick: where its players' living
units are, those units, and the vehicles they drive (with the player's
absolute index) */
absolute index); and where every player's living unit is, for a machine
with none (dead or joining, it watches one of them) */
static struct
{
short count;
long indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
short player_unit_count;
real_point3d player_unit_origins[MAXIMUM_TRACKED_PLAYERS];
struct
{
short unit_count;
@@ -422,6 +436,14 @@ static struct distributed_own_inventory
long fired_times[MAXIMUM_WEAPONS_PER_UNIT];
long weapon_times[MAXIMUM_WEAPONS_PER_UNIT];
} objects_client_own_inventories[MAXIMUM_LOCAL_PLAYERS];
/* ... the weapon each of them last picked up (the host said so), its unit
and when, until it is readied (definition NONE: none) */
static struct distributed_picked_up_weapon
{
long unit_index;
long definition_index;
long time;
} objects_client_picked_up_weapons[MAXIMUM_LOCAL_PLAYERS];
/* ... whether it failed to make one of the host's objects since it last
asked for them (it says so when it asks again) */
static boolean objects_client_ask_again;
@@ -626,6 +648,32 @@ static void distributed_vector_clamp(
}
}
/* Whether the object is a vehicle no player drives in co-op (a Pelican on
its flight path): nothing on a client steers it, so its copy is kept at rest
there (no physics of its own) and moved only to where the host has it. In
multiplayer an empty vehicle (one bailed out of, or tumbling) keeps its own
physics between updates. */
static boolean distributed_vehicle_unsteered(
long object_index)
{
struct unit_datum *vehicle;
if (!network_coop_active() || object_get(object_index)->object.type != _object_type_vehicle)
return FALSE;
vehicle = unit_get(object_index);
return vehicle->unit.driver_object_index == NONE ||
unit_get(vehicle->unit.driver_object_index)->unit.player_index == NONE;
}
/* Whether a client puts its copy exactly where the host has it, rather than
closing on it: an unsteered vehicle, and anything in a co-op cutscene
(where it keeps up with the host's camera). */
static boolean distributed_object_follows_host(
long object_index)
{
return (network_coop_active() && cinematic_in_progress()) || distributed_vehicle_unsteered(object_index);
}
/* (the transform checked) */
static void distributed_object_move(
long object_index,
@@ -647,7 +695,10 @@ static void distributed_object_move(
object->object.translational_velocity = *velocity;
if (angular_velocity)
object->object.angular_velocity = *angular_velocity;
render_interpolation_correct_object(object_index, &offset);
if (!distributed_object_follows_host(object_index))
render_interpolation_correct_object(object_index, &offset);
if (distributed_vehicle_unsteered(object_index))
SET_FLAG(object->object.flags, _object_at_rest_bit, TRUE);
}
void network_objects_correct(
@@ -692,15 +743,33 @@ boolean network_objects_reconcile(
distributed_object_move(object_index, position, &valid_forward, &valid_up, velocity, angular_velocity);
return TRUE;
}
/* (half of the way: the tick's snapshots draw it moving, no jump) */
blended.x = object->object.position.x + dx * 0.5f;
blended.y = object->object.position.y + dy * 0.5f;
blended.z = object->object.position.z + dz * 0.5f;
object_set_position(object_index, &blended, &valid_forward, &valid_up);
/* Move halfway, and draw the move as a glide over the next few frames,
as larger corrections are. Drawn as one step, a unit corrected every tick
or two (a distant enemy) visibly hitched each time. */
if (distributed_object_follows_host(object_index))
blended = *position;
else
{
blended.x = object->object.position.x + dx * 0.5f;
blended.y = object->object.position.y + dy * 0.5f;
blended.z = object->object.position.z + dz * 0.5f;
}
{
real_vector3d offset;
offset.i = object->object.position.x - blended.x;
offset.j = object->object.position.y - blended.y;
offset.k = object->object.position.z - blended.z;
object_set_position(object_index, &blended, &valid_forward, &valid_up);
if (!distributed_object_follows_host(object_index))
render_interpolation_correct_object(object_index, &offset);
}
if (velocity)
object->object.translational_velocity = *velocity;
if (angular_velocity)
object->object.angular_velocity = *angular_velocity;
if (distributed_vehicle_unsteered(object_index))
SET_FLAG(object->object.flags, _object_at_rest_bit, TRUE);
return FALSE;
}
@@ -755,6 +824,175 @@ static long distributed_driven_vehicle(
object_get(unit_index)->object.parent_object_index : NONE;
}
/* ---------- damage animations
A unit's flinches and deaths are animations picked by where the damage came
from and at random among their permutations, so a client replaying the
host's damage could pick others. The host sends its picks (each in three
ticks' messages, in case one is lost). A client plays the host's pick when
it gets there first, or switches to it if its own has only just begun
(unit_port_correct_damage_animation). */
enum
{
DAMAGE_ANIMATION_SENDS = 3,
MAXIMUM_QUEUED_DAMAGE_ANIMATIONS = 32,
/* how long a client keeps the host's pick for a hit it hasn't replayed yet */
DAMAGE_ANIMATION_KEPT_TICKS = 2 * TICKS_PER_SECOND,
};
/* type: units.h's _unit_damage_animation_soft_ping and the rest; number:
the pick's, so a client tells a pick sent again from a new one */
struct distributed_damage_animation
{
long unit_index;
short animation_index;
byte type;
byte number;
};
struct distributed_damage_animations_message
{
struct distributed_message_header header;
struct distributed_damage_animation animations[MAXIMUM_QUEUED_DAMAGE_ANIMATIONS];
};
/* host: the picks still to be sent */
static struct
{
struct distributed_damage_animation animation;
short sends;
} host_damage_animations[MAXIMUM_QUEUED_DAMAGE_ANIMATIONS];
static short host_damage_animation_count;
static byte host_damage_animation_number;
/* client: the host's latest pick of each type for each unit, by absolute
index: its number, and when it came (NONE once played) */
static struct
{
long unit_index;
short animation_index[NUMBER_OF_UNIT_DAMAGE_ANIMATIONS];
byte number[NUMBER_OF_UNIT_DAMAGE_ANIMATIONS];
long time[NUMBER_OF_UNIT_DAMAGE_ANIMATIONS];
} client_damage_animations[MAXIMUM_TRACKED_OBJECTS];
/* whether the animation is one of the unit's graph */
static boolean distributed_unit_has_animation(
long unit_index,
short animation_index)
{
long graph_index = unit_definition_get(unit_get(unit_index)->definition_index)->object.animation_graph.index;
return graph_index != NONE && animation_index >= 0 &&
animation_index < animation_graph_definition_get(graph_index)->animations.count;
}
/* units.c, as a unit flinches or dies: the host notes the animation it
picked; a client takes the host's pick when it has one */
short network_objects_damage_animation(
long unit_index,
short type,
short animation_index)
{
short connection = game_connection();
long absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(unit_index);
if (type < 0 || type >= NUMBER_OF_UNIT_DAMAGE_ANIMATIONS)
return animation_index;
if (connection == _game_connection_network_server && animation_index != NONE &&
host_damage_animation_count < MAXIMUM_QUEUED_DAMAGE_ANIMATIONS)
{
struct distributed_damage_animation *animation =
&host_damage_animations[host_damage_animation_count].animation;
animation->unit_index = unit_index;
animation->animation_index = animation_index;
animation->type = (byte)type;
animation->number = ++host_damage_animation_number;
host_damage_animations[host_damage_animation_count++].sends = DAMAGE_ANIMATION_SENDS;
}
else if (connection == _game_connection_network_client && absolute_index >= 0 &&
absolute_index < MAXIMUM_TRACKED_OBJECTS && client_damage_animations[absolute_index].unit_index == unit_index)
{
long *time = &client_damage_animations[absolute_index].time[type];
if (*time != NONE && game_time_get() - *time < DAMAGE_ANIMATION_KEPT_TICKS)
{
*time = NONE;
return client_damage_animations[absolute_index].animation_index[type];
}
}
return animation_index;
}
/* host, each tick: the picks still to be sent, to every client */
static void distributed_host_send_damage_animations(
void)
{
struct distributed_damage_animations_message message;
short count = 0;
short index;
for (index = 0; index < host_damage_animation_count; index++)
{
message.animations[count++] = host_damage_animations[index].animation;
if (--host_damage_animations[index].sends <= 0)
host_damage_animations[index--] = host_damage_animations[--host_damage_animation_count];
}
if (count > 0)
{
distributed_send(&message, _distributed_message_damage_animations, count,
(word)(sizeof(message.header) + count * sizeof(message.animations[0])), _distributed_to_clients);
}
}
word network_objects_damage_animation_entry_size(
void)
{
return sizeof(struct distributed_damage_animation);
}
void network_objects_handle_damage_animations(
void const *entries,
short count)
{
struct distributed_damage_animation const *animations = entries;
short index;
for (index = 0; index < count; index++)
{
struct distributed_damage_animation const *animation = &animations[index];
long absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(animation->unit_index);
short type;
if (!distributed_object_index_valid(animation->unit_index) || absolute_index >= MAXIMUM_TRACKED_OBJECTS ||
animation->type >= NUMBER_OF_UNIT_DAMAGE_ANIMATIONS || !network_objects_client_has(animation->unit_index) ||
!object_try_and_get_and_verify_type(animation->unit_index, _object_mask_unit) ||
!distributed_unit_has_animation(animation->unit_index, animation->animation_index))
{
continue;
}
if (client_damage_animations[absolute_index].unit_index != animation->unit_index)
{
csmemset(&client_damage_animations[absolute_index], 0, sizeof(client_damage_animations[absolute_index]));
client_damage_animations[absolute_index].unit_index = animation->unit_index;
for (type = 0; type < NUMBER_OF_UNIT_DAMAGE_ANIMATIONS; type++)
client_damage_animations[absolute_index].time[type] = NONE;
}
/* (a pick sent again) */
else if (client_damage_animations[absolute_index].number[animation->type] == animation->number)
{
continue;
}
client_damage_animations[absolute_index].number[animation->type] = animation->number;
client_damage_animations[absolute_index].animation_index[animation->type] = animation->animation_index;
/* (replayed here already: switched to the host's if it has only just begun) */
client_damage_animations[absolute_index].time[animation->type] =
unit_port_correct_damage_animation(animation->unit_index, animation->type, animation->animation_index) ?
NONE : game_time_get();
}
}
/* ---------- the host */
static void distributed_change_from_object(
@@ -770,6 +1008,7 @@ static void distributed_change_from_object(
change->owner_player_index = distributed_player_to_byte(object->object.owner_player_index);
change->owner_team_index = object->object.owner_team_index;
change->variant_number = object->object.variant_number;
change->forced_shader_permutation_index = (byte)PIN(object->object.forced_shader_permutation_index, 0, 0xFF);
change->position = object->object.position;
change->forward = object->object.forward;
change->up = object->object.up;
@@ -866,6 +1105,30 @@ static void distributed_host_update_objects(
}
}
/* The order a newly loaded client receives the host's objects in, most
needed first: players' units (it may spectate them), the vehicles players
and AI ride, other units, then everything else. */
enum
{
_send_rank_player_unit,
_send_rank_vehicle,
_send_rank_unit,
_send_rank_other,
NUMBER_OF_SEND_RANKS
};
static short distributed_object_send_rank(
long object_index)
{
struct object_datum *object = object_get(object_index);
if (object->object.type == _object_type_vehicle)
return _send_rank_vehicle;
if (object_try_and_get_and_verify_type(object_index, _object_mask_unit))
return unit_get(object_index)->unit.player_index != NONE ? _send_rank_player_unit : _send_rank_unit;
return _send_rank_other;
}
/* a client has loaded the game: every object the host has, to it alone,
and word that that is all of them; asked again, only the word (they are on
their way ahead of it), unless it failed to make one since it last asked
@@ -878,6 +1141,7 @@ void network_objects_client_asked(
short count = 0;
short limit = MIN(MAXIMUM_ENTRIES_PER_MESSAGE, RELIABLE_ENTRIES(struct distributed_object_change));
long absolute_index;
short rank;
long *player_list;
if (machine_index < 0 || machine_index >= HALO_PORT_MAXIMUM_NETWORK_MACHINES)
@@ -905,14 +1169,22 @@ void network_objects_client_asked(
/* (what every unit carries, with the next of them) */
objects_host_inventories[absolute_index].checksum = 0;
objects_host_inventories[absolute_index].weapons_checksum = 0;
if (objects_host_told[absolute_index] == NONE)
continue;
distributed_change_from_object(objects_host_told[absolute_index], &message.changes[count]);
if (++count == limit)
}
for (rank = 0; rank < NUMBER_OF_SEND_RANKS; rank++)
{
for (absolute_index = 0; absolute_index < objects_host_told_count; absolute_index++)
{
distributed_send_to_machine_reliably(machine_index, &message, _distributed_message_object_changes, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_object_change)));
count = 0;
long object_index = objects_host_told[absolute_index];
if (object_index == NONE || distributed_object_send_rank(object_index) != rank)
continue;
distributed_change_from_object(object_index, &message.changes[count]);
if (++count == limit)
{
distributed_send_to_machine_reliably(machine_index, &message, _distributed_message_object_changes, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_object_change)));
count = 0;
}
}
}
if (count)
@@ -957,8 +1229,24 @@ static void distributed_host_find_viewers(
{
short machine_number;
struct data_iterator iterator;
struct player_datum *player;
objects_host_viewers.count = distributed_client_machines(objects_host_viewers.indices,
HALO_PORT_MAXIMUM_NETWORK_MACHINES);
objects_host_viewers.player_unit_count = 0;
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL &&
objects_host_viewers.player_unit_count < MAXIMUM_TRACKED_PLAYERS)
{
long unit_index = distributed_living_unit(player);
if (unit_index != NONE)
{
object_get_origin(unit_index,
&objects_host_viewers.player_unit_origins[objects_host_viewers.player_unit_count++]);
}
}
for (machine_number = 0; machine_number < objects_host_viewers.count; machine_number++)
{
long *player_list = machine_get_player_list(objects_host_viewers.indices[machine_number]);
@@ -990,19 +1278,44 @@ static void distributed_host_find_viewers(
}
}
/* how often (ticks) the host sends that client machine an object where it
is, moving: by its nearest player; every tick to one with none in the
world (dead, it watches anyone) */
/* How often, in ticks, the host sends a moving object's position to a
client machine, based on its distance from that machine's nearest player.
A machine with no player in the world (dead or joining) spectates someone,
so the distance is measured from every player instead. Sending such a
machine every moving object every tick flooded it on campaign levels and
threw everything it saw out of sync. */
static short distributed_host_object_period(
short machine_number,
real_point3d const *position)
{
short count = objects_host_viewers.machines[machine_number].unit_count;
real_point3d const *origins = objects_host_viewers.machines[machine_number].origins;
real nearest = -1.0f;
short index;
for (index = 0; index < objects_host_viewers.machines[machine_number].unit_count; index++)
if (!count)
{
real_point3d const *origin = &objects_host_viewers.machines[machine_number].origins[index];
count = objects_host_viewers.player_unit_count;
origins = objects_host_viewers.player_unit_origins;
}
/* in a co-op cutscene every client looks through the host's camera, so
what it films is sent as often as what's next to the client's players
(b30's sword Elite walking out of the door) */
if (network_coop_active() && cinematic_in_progress())
{
real_point3d const *camera = &observer_get_camera(0)->position;
real dx = position->x - camera->x;
real dy = position->y - camera->y;
real dz = position->z - camera->z;
nearest = dx * dx + dy * dy + dz * dz;
}
/* nobody is alive anywhere, so nothing needs to be sent often */
if (!count && nearest < 0.0f)
return MAXIMUM_OBJECT_PERIOD_TICKS;
for (index = 0; index < count; index++)
{
real_point3d const *origin = &origins[index];
real dx = position->x - origin->x;
real dy = position->y - origin->y;
real dz = position->z - origin->z;
@@ -1016,6 +1329,21 @@ static short distributed_host_object_period(
nearest < FAR_OBJECT_DISTANCE * FAR_OBJECT_DISTANCE ? 3 : MAXIMUM_OBJECT_PERIOD_TICKS;
}
/* ... for other units sent by a machine's index (network_actors.c) */
short network_objects_send_period(
long machine_index,
real_point3d const *position)
{
short machine_number;
for (machine_number = 0; machine_number < objects_host_viewers.count; machine_number++)
{
if (objects_host_viewers.indices[machine_number] == machine_index)
return distributed_host_object_period(machine_number, position);
}
return 1;
}
/* the kinds of states sent this tick */
enum
{
@@ -1485,6 +1813,10 @@ void network_objects_apply_vehicle_predictions(
{
continue;
}
/* (not from a co-op client still loading the host's BSP: the
vehicle falls there with no ground under it) */
if (!network_coop_player_has_structure_bsp(driver->unit.player_index))
continue;
dx = state->position.x - vehicle->object.position.x;
dy = state->position.y - vehicle->object.position.y;
dz = state->position.z - vehicle->object.position.z;
@@ -1633,6 +1965,7 @@ void network_objects_host_tick(
distributed_host_send_states();
if (game_time_get() % INVENTORY_INTERVAL_TICKS == 0)
distributed_host_send_inventories();
distributed_host_send_damage_animations();
}
/* ---------- a client */
@@ -1827,6 +2160,21 @@ static void distributed_client_apply_change(
struct object_datum *object = object_get(object_index);
csmemcpy(object->object.base_change_colors, change->change_colors, sizeof(object->object.base_change_colors));
if (change->forced_shader_permutation_index)
object->object.forced_shader_permutation_index = change->forced_shader_permutation_index;
/* (and the colors drawn, which object_new chose from the tag: an AI unit's
are its variant's, set after it was made, actors.c) */
{
short color_index;
for (color_index = 0; color_index < NUMBER_OF_OBJECT_CHANGE_COLORS; color_index++)
{
object->object.outgoing_change_colors[color_index].red = PIN(change->change_colors[color_index].red, 0.0f, 1.0f);
object->object.outgoing_change_colors[color_index].green =
PIN(change->change_colors[color_index].green, 0.0f, 1.0f);
object->object.outgoing_change_colors[color_index].blue = PIN(change->change_colors[color_index].blue, 0.0f, 1.0f);
}
}
/* (a permutation the model has, or none: the model's renderer takes it
as it is) */
{
@@ -2081,6 +2429,20 @@ static void distributed_client_dead_biped(
}
}
/* client: a vehicle the host says is dead is destroyed here too, with its
explosion. No killing blow comes for vehicles, only for bipeds. */
static void distributed_client_dead_vehicle(
long object_index)
{
struct unit_datum *vehicle = (struct unit_datum *)object_try_and_get_and_verify_type(object_index,
_object_mask_vehicle);
if (!vehicle || TEST_FLAG(vehicle->object.damage_flags, _object_dead_bit))
return;
unit_kill_no_statistics(object_index);
object_damage_update(object_index);
}
void network_objects_handle_states(
void const *entries,
short count)
@@ -2104,6 +2466,7 @@ void network_objects_handle_states(
if (TEST_FLAG(state->flags, _distributed_object_dead_bit))
{
distributed_client_dead_biped(state->object_index);
distributed_client_dead_vehicle(state->object_index);
/* (a body its tag destroys at once is gone) */
if (!object_try_and_get_and_verify_type(state->object_index, _object_mask_all))
continue;
@@ -2136,6 +2499,12 @@ void network_objects_handle_states(
if (blend_distance > 0.0f)
blend_distance = REMOTE_VEHICLE_BLEND_DISTANCE;
/* (one no player drives follows every move the host sends, however small) */
if (distributed_vehicle_unsteered(state->object_index))
{
tolerance = 0.0f;
angle_tolerance = 2.0f;
}
if (vehicle->unit.driver_object_index != NONE &&
distributed_player_is_local(unit_get(vehicle->unit.driver_object_index)->unit.player_index))
{
@@ -2166,7 +2535,8 @@ void network_objects_handle_states(
{
distributed_count_correction();
}
SET_FLAG(object->object.flags, _object_at_rest_bit, TEST_FLAG(state->flags, _distributed_object_at_rest_bit));
SET_FLAG(object->object.flags, _object_at_rest_bit, TEST_FLAG(state->flags, _distributed_object_at_rest_bit) ||
distributed_vehicle_unsteered(state->object_index));
}
}
@@ -2377,6 +2747,68 @@ static void distributed_client_apply_inventory(
distributed_client_note_own_inventory(own, inventory->unit_index, FALSE);
}
/* a client's own player picked up a weapon (network_player_show_pickup) */
void network_objects_client_picked_up_weapon(
short local_player_index,
long unit_index,
long definition_index)
{
struct distributed_picked_up_weapon *picked_up;
if (local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS)
return;
picked_up = &objects_client_picked_up_weapons[local_player_index];
picked_up->unit_index = unit_index;
picked_up->definition_index = definition_index;
picked_up->time = game_time_get();
}
/* the weapons its own players picked up readied once they have them, as
unit_add_weapon_to_inventory readies a pickup where it decides it (not
while firing): the client chooses its players' weapons in hand
(distributed_client_apply_inventory), and the host follows */
static void distributed_client_ready_picked_up_weapons(
void)
{
short local_player_index;
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
{
struct distributed_picked_up_weapon *picked_up = &objects_client_picked_up_weapons[local_player_index];
long player_index = local_player_get_player_index(local_player_index);
struct player_datum *player = player_index != NONE ? player_try_and_get(player_index) : NULL;
long unit_index = distributed_living_unit(player);
struct unit_datum *unit;
short weapon_slot;
if (picked_up->definition_index == NONE)
continue;
if (unit_index == NONE || unit_index != picked_up->unit_index ||
game_time_get() - picked_up->time > PICKED_UP_WEAPON_TICKS)
{
picked_up->definition_index = NONE;
continue;
}
unit = unit_get(unit_index);
for (weapon_slot = 0; weapon_slot < MAXIMUM_WEAPONS_PER_UNIT; weapon_slot++)
{
long weapon_index = unit->unit.weapon_object_indices[weapon_slot];
struct weapon_datum *weapon = weapon_index != NONE ? weapon_try_and_get(weapon_index) : NULL;
if (weapon && weapon->definition_index == picked_up->definition_index)
break;
}
if (weapon_slot == MAXIMUM_WEAPONS_PER_UNIT)
continue;
if (!TEST_FLAG(unit->unit.control_flags, _unit_control_weapon_primary_trigger_bit))
{
player_control_set_desired_weapon(unit_index, weapon_slot);
unit->unit.desired_weapon_index = weapon_slot;
}
picked_up->definition_index = NONE;
}
}
void network_objects_handle_inventories(
void const *entries,
short count)
@@ -2525,6 +2957,51 @@ static void distributed_client_remove_own_objects(
objects_client_new_object_count = 0;
}
/* Client: keeps each AI-driven vehicle (such as a Pelican on its flight
path) moving at the velocity the host last sent. These vehicles are held at
rest here, and a distant one is only updated every few ticks, so otherwise
it would stop between updates and jump on each one. */
static void distributed_client_carry_unsteered_vehicles(
void)
{
struct object_iterator iterator;
struct object_datum *object;
object_iterator_new(&iterator, _object_mask_vehicle, 0);
while ((object = object_iterator_next(&iterator)) != NULL)
{
real_vector3d const *velocity = &object->object.translational_velocity;
real_vector3d axis = object->object.angular_velocity;
real_vector3d forward = object->object.forward;
real_vector3d up = object->object.up;
real_point3d position;
real angle;
if (object->object.parent_object_index != NONE || unit_get(iterator.index)->unit.driver_object_index == NONE ||
!distributed_vehicle_unsteered(iterator.index))
{
continue;
}
angle = normalize3d(&axis);
if (velocity->i == 0.0f && velocity->j == 0.0f && velocity->k == 0.0f && angle == 0.0f)
continue;
position.x = object->object.position.x + velocity->i;
position.y = object->object.position.y + velocity->j;
position.z = object->object.position.z + velocity->k;
/* rotate the same way physics.c applies angular velocity */
if (angle != 0.0f)
{
real_matrix4x3 rotation;
matrix4x3_rotation_from_axis_and_angle(&rotation, &axis, (real)sin(angle), (real)cos(angle));
matrix4x3_transform_vector(&rotation, &object->object.forward, &forward);
matrix4x3_transform_vector(&rotation, &object->object.up, &up);
}
if (distributed_transform_valid(&position, &forward, &up, NULL, NULL, &forward, &up))
object_set_position(iterator.index, &position, &forward, &up);
}
}
void network_objects_client_tick(
void)
{
@@ -2549,8 +3026,10 @@ void network_objects_client_tick(
/* (too many to look at one by one later: all of them) */
else if (objects_client_new_object_count >= MAXIMUM_CLIENT_NEW_OBJECTS)
objects_client_check_all = TRUE;
distributed_client_ready_picked_up_weapons();
distributed_client_note_own_inventories();
distributed_client_send_vehicles();
distributed_client_carry_unsteered_vehicles();
/* (who it is, as its Discord told it: once its ready went, which makes
it a machine the host takes messages of) */
if (objects_client_ready_time != NONE)
@@ -2568,6 +3047,8 @@ void network_objects_new_game(
short index;
csmemset(objects_host_inventories, 0, sizeof(objects_host_inventories));
host_damage_animation_count = 0;
csmemset(client_damage_animations, 0, sizeof(client_damage_animations));
for (absolute_index = 0; absolute_index < MAXIMUM_TRACKED_OBJECTS; absolute_index++)
{
objects_host_told[absolute_index] = NONE;
@@ -2606,6 +3087,7 @@ void network_objects_new_game(
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
{
objects_client_own_inventories[local_player_index].unit_index = NONE;
objects_client_picked_up_weapons[local_player_index].definition_index = NONE;
for (index = 0; index < OWN_VEHICLE_POSITION_TICKS; index++)
{
objects_client_own_vehicles[local_player_index][index].time = NONE;
+13 -10
View File
@@ -3,19 +3,22 @@ NETWORK_VR.C
VR players' hands for the other machines: an extension of the distributed
netcode (port/linux/NETCODE.md, network_distributed.c). A VR player's
weapon aims where the right controller points and fires from the hand, and
the left hand holds a grenade and throws it its own way (port/linux/src/
vr.c); the input the netcode relays has only where the head looks, so the
other machines would show that player aiming and throwing where they look.
weapon aims where the weapon hand's controller points and fires from the
hand, and the free hand holds a grenade and throws it its own way
(port/linux/src/vr.c; the right hand and the left, or left-handed the other
way round); the input the netcode relays has only where the head looks, so
the other machines would show that player aiming and throwing where they
look.
Every tick, a machine with a VR player sends the host that player's hands
(_distributed_message_vr_poses, unreliable): where the weapon aims, where
its muzzle is from the unit's camera, whether the left hand holds a
grenade, and the last throw (from where, which way, how hard) for a second
after it. The host keeps them, its own VR players' too, and sends each
client those of the players of other machines. A machine drives a VR
player of another (players.c, weapons.c, units.c) as long as their hands
keep coming, else as before.
its muzzle is from the unit's camera (whichever hand holds it: the others
see shots leave the left hand of a left-handed player), whether the free
hand holds a grenade, and the last throw (from where, which way, how hard)
for a second after it. The host keeps them, its own VR players' too, and
sends each client those of the players of other machines. A machine drives
a VR player of another (players.c, weapons.c, units.c) as long as their
hands keep coming, else as before.
What the hands decide is only how the game looks: damage is the shooter's
report, which the host checks as any other (network_damage.c).
+56 -19
View File
@@ -16,7 +16,10 @@ also need the tick after the pair it spans: two ticks behind.)
Rotations are blended as quaternions (normalised lerp, taking the shorter
way round), positions and scales linearly. Anything that moves further than
a tick of motion plausibly allows (teleports, respawns, camera cuts) snaps
instead of sweeping across the world.
instead of sweeping across the world, and so does a pose whose nodes moved
too far for one tick: two snapshots of different poses (a model swapped,
another weapon's skeleton, a pose left from seconds before) blended node by
node stretch vertices across the screen.
Particles, contrails and other effects already move every frame
(game_frame), so they need nothing here.
@@ -56,6 +59,17 @@ unsigned long config_changes(void);
/* world units (10 feet each) a node may move in one tick before it snaps:
well beyond any vehicle, short of any teleport */
#define OBJECT_SNAP_DISTANCE 10.0f
/* ... a node may move in the object's root node's frame in one tick before
the pose is taken for a new one, not blended to: further than any limb or
part of a model moves in 33 ms (37 m/s), short of the game changing a pose
at once (an actor waking from dormancy, a model swapped), which blended
sweeps the vertices through poses it never had. (In the root's frame, so
that the whole object turning moves nothing: measured in the world, a fast
turn swung the head and weapon far enough to snap, and characters moved
like robots.) */
#define NODE_SNAP_DISTANCE 0.4f
/* ... a first-person node may move relative to the camera */
#define FIRST_PERSON_SNAP_DISTANCE 0.25f
/* a correction's difference left drawn after each tick (of 1) */
#define CORRECTION_DECAY 0.6f
/* ... and small enough to be none */
@@ -311,16 +325,6 @@ static void matrix_blend_rotations(
result->up.k = lerp(a->up.k, b->up.k, t);
}
/* a matrix a fraction t of the way from a to b */
static void matrix_blend(real_matrix4x3 const *a, real_matrix4x3 const *b, real t, real_matrix4x3 *result)
{
struct interpolation_rotation rotation_a, rotation_b;
rotation_from_matrix(a, &rotation_a);
rotation_from_matrix(b, &rotation_b);
matrix_blend_rotations(a, b, &rotation_a, &rotation_b, t, result);
}
/* the vector's parts' sum, large enough to be a correction (so written that
one not a number is none) */
static boolean correction_significant(real_vector3d const *correction)
@@ -530,14 +534,22 @@ real_matrix4x3 *render_interpolation_object_node_matrices(long object_index)
real_matrix4x3 const *latest = record->nodes + record->latest * record->node_capacity;
real_matrix4x3 *blended = record->nodes + 2 * record->node_capacity;
short node_index;
/* (so written that a position not a number snaps) */
if (!(distance_squared(&previous[0].position, &latest[0].position) <=
OBJECT_SNAP_DISTANCE * OBJECT_SNAP_DISTANCE))
boolean snap = !(distance_squared(&previous[0].position, &latest[0].position) <=
OBJECT_SNAP_DISTANCE * OBJECT_SNAP_DISTANCE);
/* a node moved further in the root's frame than a tick allows: the
two snapshots are different poses, not one moving (so written that
a position not a number snaps) */
for (node_index = 1; !snap && node_index < record->node_count; node_index++)
{
memcpy(blended, latest, record->node_count * sizeof(real_matrix4x3));
real_point3d previous_local, latest_local;
matrix4x3_inverse_transform_point(&previous[0], &previous[node_index].position, &previous_local);
matrix4x3_inverse_transform_point(&latest[0], &latest[node_index].position, &latest_local);
snap = !(distance_squared(&previous_local, &latest_local) <= NODE_SNAP_DISTANCE * NODE_SNAP_DISTANCE);
}
else
if (!snap)
{
struct interpolation_rotation *previous_rotations =
record->rotations + (record->latest ^ 1) * record->node_capacity;
@@ -566,6 +578,8 @@ real_matrix4x3 *render_interpolation_object_node_matrices(long object_index)
&latest_rotations[node_index], interpolation_fraction, &blended[node_index]);
}
}
if (snap)
memcpy(blended, latest, record->node_count * sizeof(real_matrix4x3));
if (correction_significant(&record->correction) || correction_significant(&record->correction_pending))
{
real_vector3d drawn;
@@ -804,6 +818,8 @@ void render_interpolation_first_person(
struct interpolated_first_person *first_person;
real_matrix4x3 camera_matrix;
real_matrix4x3 inverse_camera;
struct interpolation_rotation previous_rotations[MAXIMUM_INTERPOLATED_NODES];
struct interpolation_rotation latest_rotations[MAXIMUM_INTERPOLATED_NODES];
short node_index;
if (!interpolation_rendering ||
@@ -817,8 +833,11 @@ void render_interpolation_first_person(
matrix4x3_inverse(&camera_matrix, &inverse_camera);
if (first_person->tick != interpolation_tick)
{
/* the pose drawn last, at the end of the previous tick */
first_person->has_previous = first_person->node_count == node_count;
/* the pose drawn last, if that was at the end of the tick just
before: not one left from before the view was last away from first
person (zoomed, in a vehicle, dead, in a cinematic), seconds old */
first_person->has_previous = first_person->tick == interpolation_tick - 1 &&
first_person->node_count == node_count;
memcpy(first_person->previous, first_person->latest, sizeof(first_person->previous));
first_person->tick = interpolation_tick;
}
@@ -827,13 +846,31 @@ void render_interpolation_first_person(
first_person->node_count = node_count;
if (!first_person->has_previous)
return;
/* a node that jumped further in the camera's frame than a tick allows:
the last pose was another weapon's skeleton (of as many nodes), not this
one moving (so written that a position not a number snaps) */
for (node_index = 0; node_index < node_count; node_index++)
{
real_matrix4x3 const *previous = &first_person->previous[node_index];
real_matrix4x3 const *latest = &first_person->latest[node_index];
if (!(distance_squared(&previous->position, &latest->position) <=
FIRST_PERSON_SNAP_DISTANCE * FIRST_PERSON_SNAP_DISTANCE))
{
return;
}
rotation_from_matrix(previous, &previous_rotations[node_index]);
rotation_from_matrix(latest, &latest_rotations[node_index]);
}
for (node_index = 0; node_index < node_count; node_index++)
{
real_matrix4x3 blended;
matrix_blend(
matrix_blend_rotations(
&first_person->previous[node_index],
&first_person->latest[node_index],
&previous_rotations[node_index],
&latest_rotations[node_index],
interpolation_fraction,
&blended);
matrix4x3_multiply(&camera_matrix, &blended, &node_matrices[node_index]);
+875
View File
@@ -0,0 +1,875 @@
/*
VR_ARMS.C
The VR arms' bones (vr_arms.h): the first-person hands' arms bent from the
shoulders to the hands (interface/first_person_weapons.c poses them).
The solver, the forearm's twist and their helpers are after
moistman42069/HaloCE-Quest-VR (CC0), port/linux/game/vr_render.c, nearly
as they are there: the skeleton is read through its animation graph's
index (the nodes under each node found once a frame, not per node), and
the time is the headset's frame's (halo_vr_pose_time).
*/
#include "cseries.h"
#include "math/real_math.h"
#include "models/model_animation_definitions.h"
#include "halo_vr.h"
#include "vr_arms.h"
#include <math.h>
#include <string.h>
#ifdef HALO_ANDROID
#define VR_UNITS_PER_METRE (1.0f / 3.048f)
/* ---------- the skeleton */
boolean vr_skeleton_build(
struct vr_skeleton *skeleton,
long graph_index,
short node_count)
{
short node_index;
skeleton->graph_index = NONE;
if (graph_index == NONE || node_count <= 0 || node_count > MAXIMUM_NODES_PER_ANIMATION ||
node_count != animation_graph_node_count(graph_index))
{
return FALSE;
}
for (node_index = 0; node_index < node_count; node_index++)
{
short parent = animation_graph_node_parent(graph_index, node_index);
/* (parents before their children: else no parent) */
skeleton->parents[node_index] = parent >= 0 && parent < node_index ? parent : NONE;
skeleton->under[node_index] = 0;
}
/* each node under itself and all its ancestors */
for (node_index = 0; node_index < node_count; node_index++)
{
short ancestor = node_index;
while (ancestor != NONE)
{
skeleton->under[ancestor] |= 1ULL << node_index;
ancestor = skeleton->parents[ancestor];
}
}
skeleton->graph_index = graph_index;
skeleton->node_count = node_count;
return TRUE;
}
boolean vr_skeleton_under(
struct vr_skeleton const *skeleton,
short node_index,
short root)
{
return root >= 0 && root < skeleton->node_count && node_index >= 0 && node_index < skeleton->node_count &&
(skeleton->under[root] & (1ULL << node_index)) != 0;
}
/* ---------- rotations (column vectors) */
/* v += a * scale */
static void vr_vector_scale_add_local(
real_vector3d *v,
real_vector3d const *a,
real scale)
{
v->i += a->i * scale;
v->j += a->j * scale;
v->k += a->k * scale;
}
static real vr_length(
real_vector3d const *v)
{
return (real)sqrt(v->i * v->i + v->j * v->j + v->k * v->k);
}
static void vr_point_minus(
real_point3d const *a,
real_point3d const *b,
real_vector3d *out)
{
out->i = a->x - b->x;
out->j = a->y - b->y;
out->k = a->z - b->z;
}
static boolean vr_unit_vector(
real_vector3d *v)
{
real length = vr_length(v);
if (!(length > 1e-5f && length < 1e8f))
return FALSE;
v->i /= length;
v->j /= length;
v->k /= length;
return TRUE;
}
/* the rotation taking direction a to direction b, the shortest way */
static void vr_rotation_between(
real_vector3d const *a,
real_vector3d const *b,
real rotation[3][3])
{
real_vector3d u = *a, v = *b, axis;
real c, s, t;
normalize3d(&u);
normalize3d(&v);
cross_product3d(&u, &v, &axis);
s = vr_length(&axis);
c = u.i * v.i + u.j * v.j + u.k * v.k;
if (s < 1e-6f)
{
memset(rotation, 0, sizeof(real) * 9);
if (c >= 0.0f)
{
rotation[0][0] = rotation[1][1] = rotation[2][2] = 1.0f;
}
else
{
/* half a turn about an axis across */
real_vector3d basis;
int row, column;
basis.i = 1.0f;
basis.j = basis.k = 0.0f;
if (fabs(u.i) > 0.8f)
{
basis.i = 0.0f;
basis.j = 1.0f;
}
cross_product3d(&u, &basis, &axis);
normalize3d(&axis);
for (row = 0; row < 3; row++)
{
for (column = 0; column < 3; column++)
rotation[row][column] = 2.0f * axis.n[row] * axis.n[column] - (row == column ? 1.0f : 0.0f);
}
}
return;
}
axis.i /= s;
axis.j /= s;
axis.k /= s;
t = 1.0f - c;
rotation[0][0] = c + axis.i * axis.i * t;
rotation[0][1] = axis.i * axis.j * t - axis.k * s;
rotation[0][2] = axis.i * axis.k * t + axis.j * s;
rotation[1][0] = axis.j * axis.i * t + axis.k * s;
rotation[1][1] = c + axis.j * axis.j * t;
rotation[1][2] = axis.j * axis.k * t - axis.i * s;
rotation[2][0] = axis.k * axis.i * t - axis.j * s;
rotation[2][1] = axis.k * axis.j * t + axis.i * s;
rotation[2][2] = c + axis.k * axis.k * t;
}
/* a rotation of angle radians about the unit axis */
static void vr_axis_rotation(
real_vector3d const *axis,
real angle,
real rotation[3][3])
{
real c = (real)cos(angle), s = (real)sin(angle), t = 1.0f - c;
real x = axis->i, y = axis->j, z = axis->k;
rotation[0][0] = c + x * x * t;
rotation[0][1] = x * y * t - z * s;
rotation[0][2] = x * z * t + y * s;
rotation[1][0] = y * x * t + z * s;
rotation[1][1] = c + y * y * t;
rotation[1][2] = y * z * t - x * s;
rotation[2][0] = z * x * t - y * s;
rotation[2][1] = z * y * t + x * s;
rotation[2][2] = c + z * z * t;
}
static void vr_rotate_vector(
real rotation[3][3],
real_vector3d const *in,
real_vector3d *out)
{
real_vector3d v = *in;
out->i = rotation[0][0] * v.i + rotation[0][1] * v.j + rotation[0][2] * v.k;
out->j = rotation[1][0] * v.i + rotation[1][1] * v.j + rotation[1][2] * v.k;
out->k = rotation[2][0] * v.i + rotation[2][1] * v.j + rotation[2][2] * v.k;
}
static void vr_multiply_rotations(
real a[3][3],
real b[3][3],
real out[3][3])
{
real r[3][3];
int row, column;
for (row = 0; row < 3; row++)
{
for (column = 0; column < 3; column++)
r[row][column] = a[row][0] * b[0][column] + a[row][1] * b[1][column] + a[row][2] * b[2][column];
}
memcpy(out, r, sizeof(r));
}
/* a node moved from `from` to `to` and turned by rotation about itself: its
matrix, or that of a node it carries (whose old matrix is node) */
static void vr_carry(
real_matrix4x3 *node,
real_point3d const *from,
real_point3d const *to,
real rotation[3][3])
{
real_vector3d offset;
vr_point_minus(&node->position, from, &offset);
vr_rotate_vector(rotation, &offset, &offset);
node->position.x = to->x + offset.i;
node->position.y = to->y + offset.j;
node->position.z = to->z + offset.k;
vr_rotate_vector(rotation, &node->forward, &node->forward);
vr_rotate_vector(rotation, &node->left, &node->left);
vr_rotate_vector(rotation, &node->up, &node->up);
}
/* ---------- the arm */
void vr_solve_arm(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES],
real_point3d shoulder,
real_point3d const *target,
real_vector3d const *pole,
boolean hand_stays,
struct vr_limb_history *history)
{
real_matrix4x3 old[NUMBER_OF_VR_ARM_BONES];
real rotation[NUMBER_OF_VR_ARM_BONES][3][3];
real_point3d moved_to[NUMBER_OF_VR_ARM_BONES];
real_vector3d upper, fore, reach, direction, bend, old_direction;
real upper_length, fore_length, distance, along, across;
real_point3d solved_target = *target;
short moved[MAXIMUM_NODES_PER_ANIMATION];
short index, bone;
for (bone = 0; bone < NUMBER_OF_VR_ARM_BONES; bone++)
{
if (bones[bone] < 0 || bones[bone] >= skeleton->node_count)
return;
old[bone] = matrices[bones[bone]];
}
vr_point_minus(&old[_vr_arm_fore].position, &old[_vr_arm_upper].position, &upper);
vr_point_minus(&old[_vr_arm_hand].position, &old[_vr_arm_fore].position, &fore);
upper_length = vr_length(&upper);
fore_length = vr_length(&fore);
if (!isfinite(upper_length) || !isfinite(fore_length) || upper_length < 1e-4f || fore_length < 1e-4f)
return;
vr_point_minus(target, &shoulder, &reach);
distance = vr_length(&reach);
if (!isfinite(distance) || distance < 1e-4f)
return;
direction = reach;
normalize3d(&direction);
/* out of reach: the shoulder reaches a little (8 cm), then the arm
stretches a little (18%); the hand stays where it is held */
if (distance > (upper_length + fore_length) * (history ? 0.996f : 0.999f))
{
real maximum = (upper_length + fore_length) * (history ? 0.996f : 0.999f);
real travel = distance - maximum;
travel = MIN(travel, 0.08f * VR_UNITS_PER_METRE);
shoulder.x += direction.i * travel;
shoulder.y += direction.j * travel;
shoulder.z += direction.k * travel;
distance -= travel;
if (history && distance > maximum)
{
real stretch = MIN(distance / maximum, 1.18f);
upper_length *= stretch;
fore_length *= stretch;
maximum *= stretch;
}
if (hand_stays && distance > maximum)
{
/* (further still, as this port had it: the shoulder follows the
hand, and the arm stays whole) */
shoulder.x += direction.i * (distance - maximum);
shoulder.y += direction.j * (distance - maximum);
shoulder.z += direction.k * (distance - maximum);
}
distance = MIN(distance, maximum);
}
if (history)
{
/* bent no further than 145 degrees: nearer, the bones shorten (to
65% at most) */
real minimum = (real)sqrt(upper_length * upper_length + fore_length * fore_length -
1.638304f * upper_length * fore_length);
if (distance < minimum)
{
real compress = MAX(0.65f, distance / minimum);
upper_length *= compress;
fore_length *= compress;
}
}
if (distance < (real)fabs(upper_length - fore_length) + 1e-3f)
distance = (real)fabs(upper_length - fore_length) + 1e-3f;
solved_target.x = shoulder.x + direction.i * distance;
solved_target.y = shoulder.y + direction.j * distance;
solved_target.z = shoulder.z + direction.k * distance;
/* the elbow: in the plane of the reach and the pole */
{
real dot = pole->i * direction.i + pole->j * direction.j + pole->k * direction.k;
bend.i = pole->i - direction.i * dot;
bend.j = pole->j - direction.j * dot;
bend.k = pole->k - direction.k * dot;
if (vr_length(&bend) < 1e-4f)
{
/* reaching along the pole: the old bend, else a steady side */
dot = dot_product3d(&upper, &direction);
bend = upper;
bend.i -= dot * direction.i;
bend.j -= dot * direction.j;
bend.k -= dot * direction.k;
if (vr_length(&bend) < 1e-4f)
{
real_vector3d basis;
basis.i = basis.j = 0.0f;
basis.k = 1.0f;
if (fabs(direction.k) > 0.9f)
{
basis.j = 1.0f;
basis.k = 0.0f;
}
cross_product3d(&direction, &basis, &bend);
}
}
if (history)
{
/* the elbow's swing about the reach, eased (18 a second, at most 8
radians a second) unless the arm was away or jumped */
double now = halo_vr_pose_time();
double dt = now - history->time;
real_vector3d previous = history->bend, travel;
real confidence = PIN(vr_length(&bend) / 0.35f, 0.0f, 1.0f);
vr_point_minus(&shoulder, &history->shoulder, &travel);
dot = dot_product3d(&previous, &direction);
previous.i -= dot * direction.i;
previous.j -= dot * direction.j;
previous.k -= dot * direction.k;
if (history->valid && history->graph_index == skeleton->graph_index && dt >= 0.0 && dt < 0.25 &&
vr_length(&travel) < 0.5f * VR_UNITS_PER_METRE && vr_unit_vector(&previous) && vr_unit_vector(&bend))
{
real_vector3d tangent;
real angle, change;
cross_product3d(&previous, &bend, &tangent);
angle = (real)atan2(dot_product3d(&tangent, &direction), dot_product3d(&previous, &bend));
change = angle * (1.0f - (real)exp(-18.0 * MIN(dt, 0.05))) * confidence;
change = PIN(change, (real)(-8.0 * dt), (real)(8.0 * dt));
cross_product3d(&direction, &previous, &tangent);
bend.i = previous.i * (real)cos(change) + tangent.i * (real)sin(change);
bend.j = previous.j * (real)cos(change) + tangent.j * (real)sin(change);
bend.k = previous.k * (real)cos(change) + tangent.k * (real)sin(change);
}
else
{
history->roll_valid = FALSE;
}
normalize3d(&bend);
history->valid = TRUE;
history->graph_index = skeleton->graph_index;
history->time = now;
history->shoulder = shoulder;
history->bend = bend;
}
else
{
normalize3d(&bend);
}
}
along = (upper_length * upper_length + distance * distance - fore_length * fore_length) / (2.0f * distance);
across = upper_length * upper_length - along * along;
across = across > 0.0f ? (real)sqrt(across) : 0.0f;
moved_to[_vr_arm_upper] = shoulder;
moved_to[_vr_arm_fore].x = shoulder.x + direction.i * along + bend.i * across;
moved_to[_vr_arm_fore].y = shoulder.y + direction.j * along + bend.j * across;
moved_to[_vr_arm_fore].z = shoulder.z + direction.k * along + bend.k * across;
moved_to[_vr_arm_hand] = solved_target;
/* each bone turned to point at the next joint, and the hand carried by
the forearm's turn */
vr_point_minus(&moved_to[_vr_arm_fore], &moved_to[_vr_arm_upper], &reach);
vr_rotation_between(&upper, &reach, rotation[_vr_arm_upper]);
vr_rotate_vector(rotation[_vr_arm_upper], &fore, &old_direction);
vr_point_minus(&moved_to[_vr_arm_hand], &moved_to[_vr_arm_fore], &reach);
vr_rotation_between(&old_direction, &reach, rotation[_vr_arm_fore]);
vr_multiply_rotations(rotation[_vr_arm_fore], rotation[_vr_arm_upper], rotation[_vr_arm_fore]);
if (hand_stays)
{
/* the hand stays as it was put, and what it holds */
memset(rotation[_vr_arm_hand], 0, sizeof(rotation[_vr_arm_hand]));
rotation[_vr_arm_hand][0][0] = rotation[_vr_arm_hand][1][1] = rotation[_vr_arm_hand][2][2] = 1.0f;
moved_to[_vr_arm_hand] = old[_vr_arm_hand].position;
}
else
{
memcpy(rotation[_vr_arm_hand], rotation[_vr_arm_fore], sizeof(rotation[_vr_arm_hand]));
}
/* every node under a moved bone goes with the nearest one above it
(parents come first) */
for (index = 0; index < skeleton->node_count; index++)
{
short parent = skeleton->parents[index];
moved[index] = NONE;
for (bone = 0; bone < NUMBER_OF_VR_ARM_BONES; bone++)
{
if (bones[bone] == index)
moved[index] = bone;
}
if (moved[index] == NONE && parent != NONE)
moved[index] = moved[parent];
if (moved[index] != NONE)
{
bone = moved[index];
vr_carry(&matrices[index], &old[bone].position, &moved_to[bone], rotation[bone]);
}
}
}
/* The controller's roll shared along the arm rather than all of it at the
wrist's skin: the hand's turn about the forearm from where the animation
has it against its forearm (old_fore, old_hand), 85% of it on the forearm
(none on the upper arm: vr_align_elbow rolls it), within 110 degrees, running on across half a
turn (history). The bones' places and the hand stay. */
void vr_distribute_arm_twist(
real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES],
real_matrix4x3 const *old_fore,
real_matrix4x3 const *old_hand,
struct vr_limb_history *history)
{
real_matrix4x3 *fore = &matrices[bones[_vr_arm_fore]];
real_matrix4x3 *hand = &matrices[bones[_vr_arm_hand]];
real_matrix3x3 delta;
real_vector3d expected[3], desired[3], axis;
real_quaternion q;
real angle, rotation[3][3];
int a, b, row, column;
vr_point_minus(&hand->position, &fore->position, &axis);
if (!vr_unit_vector(&axis))
return;
for (a = 0; a < 3; a++)
{
expected[a].i = expected[a].j = expected[a].k = 0.0f;
desired[a].i = hand->n[a][0];
desired[a].j = hand->n[a][1];
desired[a].k = hand->n[a][2];
for (b = 0; b < 3; b++)
{
real amount = old_fore->n[b][0] * old_hand->n[a][0] + old_fore->n[b][1] * old_hand->n[a][1] +
old_fore->n[b][2] * old_hand->n[a][2];
for (row = 0; row < 3; row++)
expected[a].n[row] += fore->n[b][row] * amount;
}
if (!vr_unit_vector(&expected[a]) || !vr_unit_vector(&desired[a]))
return;
}
for (row = 0; row < 3; row++)
{
for (column = 0; column < 3; column++)
{
delta.n[row][column] = 0.0f;
for (a = 0; a < 3; a++)
delta.n[row][column] += desired[a].n[row] * expected[a].n[column];
}
}
matrix3x3_rotation_to_quaternion(&delta, &q);
angle = 2.0f * (real)atan2(dot_product3d(&q.v, &axis), q.w);
if (!isfinite(angle))
return;
angle = (real)atan2(sin(angle), cos(angle));
/* (continuous across half a turn before the limit) */
if (history && history->roll_valid)
angle = history->roll + (real)atan2(sin(angle - history->roll), cos(angle - history->roll));
angle = PIN(angle, -1.92f, 1.92f);
if (history)
{
history->roll = angle;
history->roll_valid = TRUE;
}
vr_axis_rotation(&axis, angle * 0.85f, rotation);
vr_rotate_vector(rotation, &fore->forward, &fore->forward);
vr_rotate_vector(rotation, &fore->left, &fore->left);
vr_rotate_vector(rotation, &fore->up, &fore->up);
}
/* The upper arm rolled about its length so that the elbow bends in the
plane the arm now bends in, as the animation has it bend: the shortest
turn that took the upper arm to its new direction leaves its hinge where
it was, and with the elbow out to the side the elbow's skin turned in
(Quest, 2026-10-06). The hinge is the animation's bend (upper arm by
forearm, old_upper, old_fore, old_hand) in the upper arm's own axes. A
straight arm, then or now, has no bend: left as it is. */
void vr_align_elbow(
real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES],
real_matrix4x3 const *old_upper,
real_matrix4x3 const *old_fore,
real_matrix4x3 const *old_hand)
{
real_matrix4x3 *upper = &matrices[bones[_vr_arm_upper]];
real_matrix4x3 const *fore = &matrices[bones[_vr_arm_fore]];
real_matrix4x3 const *hand = &matrices[bones[_vr_arm_hand]];
real_vector3d old_along, old_below, old_bend, along, below, bend, hinge;
real c_forward, c_left, c_up, angle, rotation[3][3];
vr_point_minus(&old_fore->position, &old_upper->position, &old_along);
vr_point_minus(&old_hand->position, &old_fore->position, &old_below);
cross_product3d(&old_along, &old_below, &old_bend);
vr_point_minus(&fore->position, &upper->position, &along);
vr_point_minus(&hand->position, &fore->position, &below);
cross_product3d(&along, &below, &bend);
if (vr_length(&old_bend) < 1e-4f * vr_length(&old_along) * vr_length(&old_below) ||
vr_length(&bend) < 0.05f * vr_length(&along) * vr_length(&below) ||
!vr_unit_vector(&old_bend) || !vr_unit_vector(&bend) || !vr_unit_vector(&along))
{
return;
}
/* (the hinge in the upper arm's axes, then where those axes are now) */
c_forward = dot_product3d(&old_bend, &old_upper->forward);
c_left = dot_product3d(&old_bend, &old_upper->left);
c_up = dot_product3d(&old_bend, &old_upper->up);
hinge.i = upper->forward.i * c_forward + upper->left.i * c_left + upper->up.i * c_up;
hinge.j = upper->forward.j * c_forward + upper->left.j * c_left + upper->up.j * c_up;
hinge.k = upper->forward.k * c_forward + upper->left.k * c_left + upper->up.k * c_up;
/* (both across the arm: the turn about it between them) */
vr_vector_scale_add_local(&hinge, &along, -dot_product3d(&hinge, &along));
vr_vector_scale_add_local(&bend, &along, -dot_product3d(&bend, &along));
if (!vr_unit_vector(&hinge) || !vr_unit_vector(&bend))
return;
{
real_vector3d cross;
cross_product3d(&hinge, &bend, &cross);
angle = (real)atan2(dot_product3d(&cross, &along), dot_product3d(&hinge, &bend));
}
if (!isfinite(angle))
return;
vr_axis_rotation(&along, angle, rotation);
vr_rotate_vector(rotation, &upper->forward, &upper->forward);
vr_rotate_vector(rotation, &upper->left, &upper->left);
vr_rotate_vector(rotation, &upper->up, &upper->up);
}
/* ---------- the fingers
Each finger's joints are found by name: "finger" and its number (0 the
thumb to 4 the little finger), or "thumb", "index", "middle", "ring",
"pinky", on the hand's side ("l ", "r "). They are posed by absolute
directions in the controller's palm frame, each segment's subtree turned,
its length kept (after HaloCE-Quest-VR, CC0, nearly verbatim; its touch of
the world, a few rays a finger, is left out: it costs the Steam Frame's
processor too much). */
/* the finger (0 the thumb .. 4 the little finger) a node's name says it is
a joint of, on that side; NONE when not a finger's */
static short vr_finger_of(
char const *name,
char const *side)
{
static char const *const names[] = { "thumb", "index", "middle", "ring", "pinky" };
char const *finger;
short index;
if (!name || !strstr(name, side))
return NONE;
finger = strstr(name, "finger");
if (finger && finger[6] >= '0' && finger[6] <= '4')
return (short)(finger[6] - '0');
for (index = 0; index < 5; index++)
{
if (strstr(name, names[index]))
return index;
}
return NONE;
}
void vr_finger_nodes(
struct vr_skeleton const *skeleton,
short hand,
char const *side,
short joints[5][3])
{
short node_index, finger, depth;
for (finger = 0; finger < 5; finger++)
{
for (depth = 0; depth < 3; depth++)
joints[finger][depth] = NONE;
}
for (node_index = 0; node_index < skeleton->node_count; node_index++)
{
short at;
finger = vr_finger_of(animation_graph_node_name(skeleton->graph_index, node_index), side);
if (finger == NONE || !vr_skeleton_under(skeleton, node_index, hand))
continue;
/* its depth: how many of the same finger's joints above it */
depth = 0;
for (at = skeleton->parents[node_index]; at != NONE; at = skeleton->parents[at])
{
if (vr_finger_of(animation_graph_node_name(skeleton->graph_index, at), side) == finger)
depth++;
}
if (depth < 3)
joints[finger][depth] = node_index;
}
}
/* a node and all under it turned about the node */
static void vr_turn_subtree(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short root,
real rotation[3][3])
{
real_point3d pivot = matrices[root].position;
unsigned long long under = skeleton->under[root];
short node_index;
for (node_index = 0; node_index < skeleton->node_count; node_index++)
{
if (under & (1ULL << node_index))
vr_carry(&matrices[node_index], &pivot, &pivot, rotation);
}
}
/* a finger's joints (low, middle, tip) curled 0 (straight, a little bent
at rest) to 1 (a fist), in the palm's frame: forward along the fingers,
up the knuckles towards the index, palm out of the palm */
static boolean vr_pose_finger(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short hand,
short const joints[3],
int finger,
real curl,
real_vector3d const *palm,
real_vector3d const *forward,
real_vector3d const *up)
{
static real const rest[3] = { 0.12f, 0.15f, 0.10f };
static real const bend[3] = { 1.25f, 1.6f, 1.1f };
real_vector3d base, axis, direction, wanted;
real angle = 0.0f, dot, rotation[3][3];
int depth;
if (joints[0] == NONE || joints[1] == NONE || joints[2] == NONE)
return FALSE;
vr_point_minus(&matrices[joints[0]].position, &matrices[hand].position, &base);
dot = dot_product3d(&base, palm);
base.i -= dot * palm->i;
base.j -= dot * palm->j;
base.k -= dot * palm->k;
if (finger == 0)
{
/* the thumb: up the handle, or across the palm */
base.i = (up->i * 0.9f + forward->i * 0.3f) * (1.0f - curl) +
(forward->i * 0.75f + palm->i * 0.55f + up->i * 0.1f) * curl;
base.j = (up->j * 0.9f + forward->j * 0.3f) * (1.0f - curl) +
(forward->j * 0.75f + palm->j * 0.55f + up->j * 0.1f) * curl;
base.k = (up->k * 0.9f + forward->k * 0.3f) * (1.0f - curl) +
(forward->k * 0.75f + palm->k * 0.55f + up->k * 0.1f) * curl;
}
if (!vr_unit_vector(&base))
return FALSE;
cross_product3d(&base, palm, &axis);
if (!vr_unit_vector(&axis))
return FALSE;
for (depth = 0; depth < 3; depth++)
{
short joint = joints[depth];
if (depth < 2)
{
vr_point_minus(&matrices[joints[depth + 1]].position, &matrices[joint].position, &direction);
}
else
{
/* the tip: its own axis that the middle joint's bone runs along */
real_vector3d previous;
real_vector3d *axes[3];
real_vector3d *last[3];
real best = 0.8f, sign = 1.0f;
int a, found = -1;
axes[0] = &matrices[joints[1]].forward;
axes[1] = &matrices[joints[1]].left;
axes[2] = &matrices[joints[1]].up;
last[0] = &matrices[joint].forward;
last[1] = &matrices[joint].left;
last[2] = &matrices[joint].up;
vr_point_minus(&matrices[joint].position, &matrices[joints[1]].position, &previous);
if (!vr_unit_vector(&previous))
return FALSE;
for (a = 0; a < 3; a++)
{
real_vector3d unit = *axes[a];
if (!vr_unit_vector(&unit))
continue;
dot = dot_product3d(&unit, &previous);
if (fabs(dot) > best)
{
best = (real)fabs(dot);
found = a;
sign = dot < 0.0f ? -1.0f : 1.0f;
}
}
if (found < 0)
return TRUE;
direction.i = last[found]->i * sign;
direction.j = last[found]->j * sign;
direction.k = last[found]->k * sign;
}
if (!vr_unit_vector(&direction))
return FALSE;
angle += finger == 0 ? (depth == 0 ? 0.0f : (depth == 1 ? 0.35f : 0.3f) * curl) :
rest[depth] + bend[depth] * curl;
vr_axis_rotation(&axis, angle, rotation);
vr_rotate_vector(rotation, &base, &wanted);
vr_rotation_between(&direction, &wanted, rotation);
vr_turn_subtree(skeleton, matrices, joint, rotation);
}
return TRUE;
}
void vr_pose_fingers(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short hand,
short const joints[5][3],
float const curls[4],
real_vector3d const *palm,
real_vector3d const *forward,
real_vector3d const *up)
{
int finger;
for (finger = 0; finger < 5; finger++)
vr_pose_finger(skeleton, matrices, hand, joints[finger], finger, curls[finger < 3 ? finger : 3], palm, forward, up);
}
/* the finger's middle and tip turned this much (0..1) towards the line of
its low joint: a finger lifted off the trigger (the weapon hand's index,
whose low joint, and the rest of the hand, keep the weapon's grip) */
void vr_straighten_finger(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short const joints[3],
real amount)
{
real_vector3d low, middle, axis;
real rotation[3][3];
real angle;
if (amount <= 0.0f || joints[0] == NONE || joints[1] == NONE || joints[2] == NONE)
return;
vr_point_minus(&matrices[joints[1]].position, &matrices[joints[0]].position, &low);
vr_point_minus(&matrices[joints[2]].position, &matrices[joints[1]].position, &middle);
if (!vr_unit_vector(&low) || !vr_unit_vector(&middle))
return;
cross_product3d(&middle, &low, &axis);
if (!vr_unit_vector(&axis))
return;
angle = (real)acos(PIN(dot_product3d(&middle, &low), -1.0f, 1.0f)) * PIN(amount, 0.0f, 1.0f);
vr_axis_rotation(&axis, angle, rotation);
vr_turn_subtree(skeleton, matrices, joints[1], rotation);
}
/* ---------- hands alone (vr.body "hands")
The arm's bones (not the hand's, its fingers' or the weapon's) shrunk to
nothing at a point 3.5 cm back from the wrist along the hand's line,
knuckles to wrist: the cuff closes just behind the hand. Shrunk onto their
own places, the forearm drew the wrist's skin to the elbow and the hand
looked cut off (after HaloCE-Quest-VR, CC0, its vr_hide_forearms and
vr_hand_back_axis). */
void vr_hide_arm(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES],
short const fingers[5][3],
short gun)
{
real_point3d centre = matrices[bones[_vr_arm_hand]].position;
real_point3d knuckles;
real_vector3d back;
boolean along = FALSE;
short finger, node_index, found = 0;
knuckles.x = knuckles.y = knuckles.z = 0.0f;
/* (the four fingers' bases: the thumb's sits off the line) */
for (finger = 1; finger < 5; finger++)
{
if (fingers[finger][0] == NONE)
continue;
knuckles.x += matrices[fingers[finger][0]].position.x;
knuckles.y += matrices[fingers[finger][0]].position.y;
knuckles.z += matrices[fingers[finger][0]].position.z;
found++;
}
if (found)
{
knuckles.x /= found;
knuckles.y /= found;
knuckles.z /= found;
vr_point_minus(&centre, &knuckles, &back);
along = vr_unit_vector(&back);
}
if (!along)
{
vr_point_minus(&matrices[bones[_vr_arm_fore]].position, &centre, &back);
along = vr_unit_vector(&back);
}
if (along)
{
centre.x += back.i * 0.035f * VR_UNITS_PER_METRE;
centre.y += back.j * 0.035f * VR_UNITS_PER_METRE;
centre.z += back.k * 0.035f * VR_UNITS_PER_METRE;
}
for (node_index = 0; node_index < skeleton->node_count; node_index++)
{
if (!vr_skeleton_under(skeleton, node_index, bones[_vr_arm_upper]) ||
vr_skeleton_under(skeleton, node_index, bones[_vr_arm_hand]) ||
(gun != NONE && vr_skeleton_under(skeleton, node_index, gun)))
{
continue;
}
matrices[node_index].scale = 0.0f;
matrices[node_index].position = centre;
}
}
#endif
+83
View File
@@ -0,0 +1,83 @@
/*
VR_ARMS.H
The VR arms' bones (vr_arms.c), for the first-person hands
(interface/first_person_weapons.c): the arms bent from the shoulders to the
hands. Include after the game's math and animation headers.
*/
#ifndef __VR_ARMS_H
#define __VR_ARMS_H
#ifdef HALO_ANDROID
enum
{
_vr_arm_upper,
_vr_arm_fore,
_vr_arm_hand,
NUMBER_OF_VR_ARM_BONES
};
/* a skeleton's parents, and the nodes under each node (it among them), from
its animation graph: parents come before their children */
struct vr_skeleton
{
long graph_index;
short node_count;
short parents[MAXIMUM_NODES_PER_ANIMATION];
unsigned long long under[MAXIMUM_NODES_PER_ANIMATION];
};
/* an arm's last frame, for its elbow's swing and its forearm's twist to run
on smoothly (one each arm) */
struct vr_limb_history
{
long graph_index;
double time;
real_point3d shoulder;
real_vector3d bend;
boolean valid;
boolean roll_valid;
real roll;
};
boolean vr_skeleton_build(struct vr_skeleton *skeleton, long graph_index, short node_count);
boolean vr_skeleton_under(struct vr_skeleton const *skeleton, short node_index, short root);
/* one arm (bones: upper arm, forearm, hand) bent from the shoulder for the
hand to reach target, its elbow towards the pole; the hand (and what it
holds) stays where it is, or turns with the forearm */
void vr_solve_arm(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES], real_point3d shoulder, real_point3d const *target,
real_vector3d const *pole, boolean hand_stays, struct vr_limb_history *history);
/* the hand's roll from the animation (old_fore, old_hand: the arm as
animated) shared by the forearm and the upper arm */
void vr_align_elbow(real_matrix4x3 *matrices, short const bones[NUMBER_OF_VR_ARM_BONES],
real_matrix4x3 const *old_upper, real_matrix4x3 const *old_fore, real_matrix4x3 const *old_hand);
void vr_distribute_arm_twist(real_matrix4x3 *matrices, short const bones[NUMBER_OF_VR_ARM_BONES],
real_matrix4x3 const *old_fore, real_matrix4x3 const *old_hand, struct vr_limb_history *history);
/* a hand's fingers' joints (low, middle, tip; NONE where not found), by
their names on that side ("l ", "r "): the thumb, index, middle, ring and
little finger */
void vr_finger_nodes(struct vr_skeleton const *skeleton, short hand, char const *side, short joints[5][3]);
/* the fingers curled (halo_vr_finger_curls: the thumb, index, middle, the
last two) in the palm's frame: forward along the fingers, up the knuckles
towards the index, palm out of the palm */
void vr_pose_fingers(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices, short hand,
short const joints[5][3], float const curls[4], real_vector3d const *palm, real_vector3d const *forward,
real_vector3d const *up);
/* a finger's middle and tip turned this much (0..1) into line with its low
joint */
void vr_straighten_finger(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices, short const joints[3],
real amount);
/* the arm's bones shrunk to nothing just behind the wrist, the hand (with
its fingers) and the weapon left: the hand alone */
void vr_hide_arm(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES], short const fingers[5][3], short gun);
#endif
#endif
+183
View File
@@ -0,0 +1,183 @@
/*
VR_GRAPHICS.C
The VR graphics presets (vr.graphics, the VR options' "Graphics" row): which
of the game's costlier effects are drawn, for headsets with little time to
spare (the Steam Frame spends most of its frame in its GL driver: fewer
draws help it most; the Quest 3 holds 90 Hz with all of them).
- "high", the default: everything, as before;
- "medium": no object shadows, environment reflections or lens flares;
- "low": nor specular light, bump maps, grass and other detail objects,
the screen's fog layer, contrails, weather or camouflage's second pass;
- "max": "high", the models lit for each pixel and the shadows drawn at
512x512 (d3d8_gl.c asks halo_vr_graphics).
Each effect is a switch the game has already: the rasterizer's console
variables (rasterizer_debug_options) and render.c's effect switches, after
HaloCE-Quest-VR's vr_graphics.c (CC0). Scripts and the console set some of
them (render_effects turns particles off in some cutscenes): a switch is
drawn only while both the game and the preset want it. A change the game
made since the last frame is taken as its wish, kept and combined with the
preset's. Playing in another's game, a client draws grass, lens flares and
the screen's fog as the host's rules have it (cheats.c), whatever the
preset; the reductions it does keep there, cheats.c leaves alone
(vr_graphics_reduces).
What each costs: shadows (each object's map, blur and projection: draws),
specular light and reflections (passes of the level's geometry: draws and
fill), detail objects (many small draws), lens flares (their visibility
tests), contrails and weather (particles), bump maps, the fog layer and
camouflage's pass (the GPU's fill).
*/
#ifdef HALO_ANDROID
#include "cseries.h"
#include "rasterizer/rasterizer_console_vars.h"
#include "networking/network_game_globals.h"
#include "halo_vr.h"
/* source/render/render.c, render_objects.c; source/effects/
weather_particle_systems.c */
extern boolean render_shadows;
extern boolean render_contrails_enabled;
extern boolean render_weather_particle_systems_enabled;
extern boolean weather;
void platform_log(char const *format, ...);
/* an effect: whether each preset draws it (low, medium; high and max draw
everything), whether a client in another's game draws it anyway, and the
game's switches it governs */
struct vr_graphics_effect
{
char const *name;
boolean preset[2];
boolean host_rules;
boolean *switches[2];
};
static struct vr_graphics_effect vr_graphics_effects[] =
{
{ "shadows", { FALSE, FALSE }, FALSE,
{ &render_shadows, &rasterizer_debug_options.draw_environment_shadows } },
{ "specular", { FALSE, TRUE }, FALSE,
{ &rasterizer_debug_options.draw_environment_specular_lights,
&rasterizer_debug_options.draw_environment_specular_lightmaps } },
{ "reflections", { FALSE, FALSE }, FALSE,
{ &rasterizer_debug_options.draw_environment_reflections, NULL } },
{ "bump maps", { FALSE, TRUE }, FALSE,
{ &rasterizer_debug_options.bump_mapping_enabled, NULL } },
{ "detail objects", { FALSE, TRUE }, TRUE,
{ &rasterizer_debug_options.draw_detail_objects, NULL } },
{ "lens flares", { FALSE, FALSE }, TRUE,
{ &rasterizer_debug_options.draw_lens_flares, NULL } },
{ "screen fog", { FALSE, TRUE }, TRUE,
{ &rasterizer_debug_options.draw_environment_fog_screen, NULL } },
{ "contrails", { FALSE, TRUE }, FALSE,
{ &render_contrails_enabled, NULL } },
{ "weather", { FALSE, TRUE }, FALSE,
{ &weather, &render_weather_particle_systems_enabled } },
{ "camouflage pass", { FALSE, TRUE }, FALSE,
{ &rasterizer_debug_options.active_camouflage_multipass_enabled, NULL } },
};
#define NUMBER_OF_VR_GRAPHICS_EFFECTS (sizeof(vr_graphics_effects) / sizeof(vr_graphics_effects[0]))
#define VR_GRAPHICS_SWITCHES (sizeof(vr_graphics_effects[0].switches) / sizeof(vr_graphics_effects[0].switches[0]))
static struct
{
boolean started;
int preset;
boolean client;
/* whether the preset allows each effect; what the game wants of each
switch, and what was written last frame */
boolean allowed[NUMBER_OF_VR_GRAPHICS_EFFECTS];
boolean wanted[NUMBER_OF_VR_GRAPHICS_EFFECTS][VR_GRAPHICS_SWITCHES];
boolean written[NUMBER_OF_VR_GRAPHICS_EFFECTS][VR_GRAPHICS_SWITCHES];
} vr_graphics;
static void vr_graphics_choose(
int preset,
boolean client)
{
static char const *const names[] = { "low", "medium", "high", "max" };
unsigned int index;
vr_graphics.preset = preset;
vr_graphics.client = client;
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
{
struct vr_graphics_effect const *effect = &vr_graphics_effects[index];
vr_graphics.allowed[index] = preset >= HALO_VR_GRAPHICS_HIGH || effect->preset[preset] ||
(client && effect->host_rules);
}
platform_log("VR: graphics %s%s", names[preset], client ? " (in another's game)" : "");
}
/* each frame, before it is drawn (render.c) */
void vr_graphics_update(
void)
{
int preset = halo_vr_graphics();
boolean client = network_game_distributed_client();
unsigned int index, which;
if (!vr_graphics.started)
{
/* the game's switches as it starts are its wishes */
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
{
for (which = 0; which < VR_GRAPHICS_SWITCHES && vr_graphics_effects[index].switches[which]; which++)
{
vr_graphics.wanted[index][which] = *vr_graphics_effects[index].switches[which];
vr_graphics.written[index][which] = *vr_graphics_effects[index].switches[which];
}
}
vr_graphics.started = TRUE;
vr_graphics_choose(preset, client);
}
else if (preset != vr_graphics.preset || client != vr_graphics.client)
{
vr_graphics_choose(preset, client);
}
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
{
for (which = 0; which < VR_GRAPHICS_SWITCHES && vr_graphics_effects[index].switches[which]; which++)
{
boolean *game_switch = vr_graphics_effects[index].switches[which];
boolean value;
/* (the game, a script, the console changed it since: its wish) */
if (*game_switch != vr_graphics.written[index][which])
vr_graphics.wanted[index][which] = *game_switch;
value = vr_graphics.wanted[index][which] && vr_graphics.allowed[index];
*game_switch = value;
vr_graphics.written[index][which] = value;
}
}
}
/* whether the preset keeps this switch of the game's off now (cheats.c, a
client's rules: those it may keep off) */
boolean vr_graphics_reduces(
boolean const *game_switch)
{
unsigned int index, which;
if (!vr_graphics.started)
return FALSE;
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
{
for (which = 0; which < VR_GRAPHICS_SWITCHES && vr_graphics_effects[index].switches[which]; which++)
{
if (vr_graphics_effects[index].switches[which] == game_switch)
return !vr_graphics.allowed[index];
}
}
return FALSE;
}
#endif
+3 -3
View File
@@ -1,4 +1,4 @@
/* the game includes <StdDef.h>; Linux file names are case sensitive.
#include_next, since on a case-insensitive file system (macOS) this file
is also what <stddef.h> finds */
/* the game includes <StdDef.h>; Linux file names are case sensitive. On a
case-insensitive file system (macOS) a plain <stddef.h> would find this file
again; include_next goes to the real one, as the other shims do. */
#include_next <stddef.h>
@@ -41,8 +41,17 @@ the display's shape, or 640 (port/linux/src/d3d8_gl.c) */
long halo_screen_width(void);
/* takes up a new width between frames (F11); returns the width */
long halo_screen_commit(void);
/* the shadow maps' pixels for each of their 128 texels each way, a power of
two (display.shadow_resolution) */
long halo_shadow_map_scale(void);
/* while TRUE, drawing shifts right to center 640-column layouts */
void halo_screen_ui_offset(unsigned char centered);
/* names a model lighting vertex program, whose draws can be lit for each
pixel (port/linux/src/d3d8_gl.c, display.per_pixel_lighting) */
void halo_vertex_shader_lighting(unsigned long handle);
/* display.anti_aliasing's pass over a window's 3D view, before the HUD and
menus (source/render/render.c): the window's bounds on the screen */
void halo_screen_anti_alias(short x0, short y0, short x1, short y1);
/* the mouse in the menus (source/interface/ui_widget.c) */
#include "halo_ui_pointer.h"
+15 -3
View File
@@ -24,15 +24,27 @@ state above the tag cache (which ends at 0x819A6000), inside the Xbox memory
window (0x80000000-0x88000000, port/linux/src/platform.h) and below everything
the window hands out top-down (texture and sound caches, Direct3D resources).
The CPU part holds about 13.6 MB of pools at the sizes below (the Xbox pools
The CPU part holds about 17.2 MB of pools at the sizes below (the Xbox pools
fill 3,165,260 of its 0x305000 bytes); the GPU part holds only the decal
vertices, as on the Xbox. */
vertices, as on the Xbox. A change to a pool's size changes the game state's
layout: saved games of builds before it no longer load. */
#define HALO_PORT_GAME_STATE_BASE_ADDRESS 0x81A00000 /* (0x80061000) */
#define HALO_PORT_GAME_STATE_CPU_SIZE 0xFC0000 /* (0x305000) */
#define HALO_PORT_GAME_STATE_CPU_SIZE 0x13C0000 /* (0x305000) */
#define HALO_PORT_GAME_STATE_GPU_SIZE 0x40000 /* (0x40000) */
#define HALO_PORT_GAME_STATE_SIZE (HALO_PORT_GAME_STATE_CPU_SIZE+HALO_PORT_GAME_STATE_GPU_SIZE)
/* ---------- AI
Network co-op adds enemies for its players (port/linux/game/coop_enemies.c):
the actors, and their knowledge of the units about them (props: with many
players, more each), have room for several times a level's own. */
#define HALO_PORT_MAXIMUM_ACTORS 1024 /* (256) */
#define HALO_PORT_MAXIMUM_PROPS 8192 /* (768) */
#define HALO_PORT_MAXIMUM_SWARMS 128 /* (32) */
#define HALO_PORT_MAXIMUM_SWARM_COMPONENTS 1024 /* (256) */
/* ---------- objects */
#define HALO_PORT_MAXIMUM_OBJECTS_PER_MAP 8192 /* (2048) */
+1 -1
View File
@@ -66,7 +66,7 @@ is newer (network_client_manager.c). A host advertises it, with its netcode,
in its game's advertisement's reserved bytes (network_server_message_handler.c),
which hosts built before there was a version send as zeros: version 0.
Raise it with any change to what the machines send each other. */
#define HALO_PORT_NETWORK_VERSION 11
#define HALO_PORT_NETWORK_VERSION 20
/* ... the advertisement's reserved bytes: the version (a little-endian word),
then flags */
#define HALO_PORT_ADVERTISED_VERSION_OFFSET 0
+117 -20
View File
@@ -63,6 +63,9 @@ void halo_vr_multiview_sky(int sky);
/* the shared camera's projection's [2][2] and [3][2], for what the game
projects itself (sprites, lens flare tests) */
void halo_vr_multiview_projection(float projection_22, float projection_32);
/* lens flares are being drawn: placed by their depth, though not depth
tested */
void halo_vr_multiview_lens_flares(int drawing);
/* clears the back buffer to transparent black for the overlay, after the
eyes; menus_active chooses where the overlay is shown */
@@ -85,13 +88,14 @@ degrees on an axis (the other axis 0); nonzero if it has. The game counts
it as looking around (player_control.c's action tests). */
int halo_vr_look_delta(float *yaw, float *pitch);
/* The weapon in the right hand (vr.aim_with = "controller"; each returns 0,
and the game does as before, when the hand is not tracked):
/* The weapon in the right hand, or the left one (vr.handedness; vr.aim_with
= "controller"; each returns 0, and the game does as before, when the hand
is not tracked):
- the direction the player's weapon aims, where the controller points (the
body and the view keep facing where the head looks);
- where its shots start, as an offset from the unit's camera (world units);
- the camera the first-person weapon model is posed from, so that it lies
in the hand;
in the hand (in model space, below);
- the aiming laser's start and direction; */
int halo_vr_aim_vector(short local_player_index, float vector[3]);
int halo_vr_weapon_origin_offset(short local_player_index, float offset[3]);
@@ -101,35 +105,75 @@ int halo_vr_weapon_origin_offset(short local_player_index, float offset[3]);
int halo_vr_weapon_origin(short local_player_index, float origin[3]);
int halo_vr_weapon_camera(short local_player_index, float position[3], float forward[3], float up[3]);
int halo_vr_laser(short local_player_index, float start[3], float direction[3]);
/* Model space: the first-person model (first_person_weapons.c) is always
posed as the right hand's. Left-handed, it is posed in a mirror image of
the room and drawn mirrored back (its triangles' winding turned over); the
mirror: a point on it (world units) and its normal, or 0 when it is not
mirrored. What first_person_weapons.c and vr.c exchange about the model
is in model space: the weapon camera, the free hand, the foregrip, the
fingers (0 the model's left hand, the free one), the barrel and the laser
along it. */
int halo_vr_first_person_mirror(short local_player_index, float point[3], float normal[3]);
/* the laser for this frame, along the barrel (first_person_weapons.c: while
an animation turns the weapon, as in a reload), and the laser at the ready,
where the controller aims */
an animation turns the weapon, as in a reload; model space), and the laser
at the ready, where the controller aims (in the room) */
void halo_vr_set_laser(short local_player_index, const float start[3], const float direction[3]);
int halo_vr_ready_laser(short local_player_index, float start[3], float direction[3]);
/* The arms (interface/first_person_weapons.c):
- the body: the head's position (world units) and flat heading, from
which the shoulders hang;
- the left hand's pose;
- the left hand on the weapon: given where the model's left hand holds it
- the body: the eyes' position (world units), the neck they turn about,
and the torso's flat heading (which follows the head beyond a few
degrees, and the hands; the stick's turns turn it at once), from which
the shoulders hang;
- the free hand's pose (the left one, left-handed the right; model space);
- the free hand on the weapon: given where the model's left hand holds it
(world units) and the line from its right hand to its left in the
weapon's forward, right and up, whether the left hand grips it; then the
weapon's forward, right and up, whether the free hand grips it; then the
weapon aims along both hands; */
int halo_vr_body_frame(short local_player_index, float head[3], float forward[3]);
int halo_vr_left_hand(short local_player_index, float position[3], float forward[3], float up[3]);
int halo_vr_body_frame(short local_player_index, float head[3], float neck[3], float forward[3]);
int halo_vr_free_hand(short local_player_index, float position[3], float forward[3], float up[3]);
int halo_vr_two_hand(short local_player_index, const float foregrip[3], const float reference[3]);
/* - the fingers of a hand of the model (0 the left, the free hand; 1 the
right, the weapon's), from the controller's trigger, grip and touch: the thumb, index, middle finger and the last two, each
curled 0 (straight) to 1; 0 when the hand is not tracked or vr.fingers
is off, else HALO_VR_FINGERS_CURLED, and HALO_VR_FINGERS_TOUCH too when
the controller tells where the fingers rest (else a finger off the
trigger reads as one on it, unpressed); */
enum
{
HALO_VR_FINGERS_CURLED = 1,
HALO_VR_FINGERS_TOUCH = 2,
};
int halo_vr_finger_curls(short local_player_index, int hand, float curls[4]);
/* - whether the free hand's arm bends at the elbow to reach it from the
shoulder (vr.arm_ik); else it keeps the shape it has on the foregrip,
pointed at the shoulder;
- the time of the headset's frame (seconds, steady through the frame), by
which the arms' bending is eased */
int halo_vr_free_arm_bends(void);
double halo_vr_pose_time(void);
/* - what of the body is drawn (vr.body): the arms and hands, the hands
alone, or neither (the weapon alone) */
enum
{
HALO_VR_BODY_ARMS = 0,
HALO_VR_BODY_HANDS,
HALO_VR_BODY_OFF,
};
int halo_vr_body_mode(void);
/* a hand has just been thrust forward: a melee attack (xinput_sdl.c) */
int halo_vr_melee(void);
/* the weight of the melee attack under way, from the swing's speed (0..1;
0 with none): the game scales melee damage by the player's run instead,
and in VR a player punches standing (units.c) */
float halo_vr_melee_strength(void);
/* The grenade (units.c): whether the left trigger holds it in the hand; and
/* The grenade (units.c): whether the free hand's trigger holds it in the
hand; and
as it let go, whether the hand threw it: from where (world units), which
way (the game's axes) and how hard (0..1 of the game's throw) */
int halo_vr_grenade_held(short local_player_index);
int halo_vr_grenade_throw(short local_player_index, float origin[3], float direction[3], float *strength);
/* where the held grenade is drawn (render_objects.c): in the left palm, and
at what size */
/* where the held grenade is drawn (first_person_weapons.c): in the free
hand's palm, and at what size */
int halo_vr_grenade_in_hand(short local_player_index, float position[3], float forward[3], float up[3],
float *scale);
/* The scope (render.c): zoomed in by magnification, the camera for the
@@ -160,9 +204,9 @@ void halo_vr_capture_names(void);
void halo_vr_set_scope_raise(float metres);
void halo_vr_capture_scope(float left, float top, float right, float bottom);
/* where the weapon's gun node is as drawn, at the ready (world units;
first_person_weapons.c, each frame): shots, the crosshair and the laser
start there; and the direction its barrel points in the weapon camera's
frame (forward, left, up), which the camera is turned to aim */
first_person_weapons.c, each frame; model space): shots, the crosshair and
the laser start there; and the direction its barrel points in the weapon
camera's frame (forward, left, up), which the camera is turned to aim */
void halo_vr_set_barrel(short local_player_index, const float world[3], const float direction[3]);
/* how far, in world units, the weapon's aim runs before it hits something
(render.c, each frame); the crosshair floats there */
@@ -178,7 +222,8 @@ void halo_vr_capture_crosshair(void);
/* Vehicles (player_control.c, each tick): the seat the player sits in and
its vehicle's heading (yaw, radians). Seated, the body turns with the
vehicle; a driver who steers by facing (HALO_VR_SEAT_STEER) steers with the
right stick, else the right controller aims (HALO_VR_SEAT_AIM). And each
turning stick, else the weapon hand's controller aims (HALO_VR_SEAT_AIM).
And each
frame (render.c) the heading as drawn, between ticks. */
enum
{
@@ -201,6 +246,29 @@ driver's hands on it as drawn (world units, from the game's camera), or
NULL; the player's own hands turn it. And whether they hold it. */
void halo_vr_set_wheel(short local_player_index, const float left[3], const float right[3]);
int halo_vr_wheel_held(void);
/* the controllers' layout (HALO_XR_LAYOUT_*: 0 the Quest's, 1 the Steam
Frame's), for what the game tells the player */
int halo_vr_controller_layout(void);
/* the hand that holds the weapon (HALO_XR_HAND_*: 1 the right, 0 the left,
vr.handedness) */
int halo_vr_weapon_hand(void);
/* the graphics preset (vr.graphics; HALO_VR_GRAPHICS_HIGH outside VR): which
effects are drawn (port/linux/game/vr_graphics.c), and with "max" the
models lit for each pixel and larger shadows (d3d8_gl.c) */
enum
{
HALO_VR_GRAPHICS_LOW = 0,
HALO_VR_GRAPHICS_MEDIUM,
HALO_VR_GRAPHICS_HIGH,
HALO_VR_GRAPHICS_MAX,
};
int halo_vr_graphics(void);
/* the VR options: the refresh rates the headset offers (Hz, at most maximum;
0 when it lets none be chosen, as SteamVR), and the one it runs at (0:
unknown; vr.refresh_rate asks for one); and vr.resolution_scale as the app
started (it takes a restart) */
int halo_vr_refresh_rates(float *rates, int maximum, float *current);
float halo_vr_started_resolution_scale(void);
/* whether a seated player's view stays in first person (director.c) */
int halo_vr_vehicle_first_person(void);
/* VR test aid (debug.vehicle_spawn): both sticks held in for a moment asks
@@ -230,7 +298,7 @@ coming (each returns 0 otherwise, and for this machine's own players, and
the game does as before):
- where the weapon aims, on foot;
- where its shots start (world units), from the unit's camera;
- whether the left hand holds the grenade;
- whether the free hand holds the grenade;
- the hand's throw, once, as the grenade goes (as halo_vr_grenade_throw);
and this machine's own VR player's throw, to send (units.c). */
int halo_vr_network_aim(long player_index, float vector[3]);
@@ -244,6 +312,35 @@ void halo_vr_network_grenade_thrown(short local_player_index, long unit_index, c
/* the platform layer's side (vr.c) */
void halo_vr_initialize(void);
void halo_vr_frame_begin(void);
/* vr.log_performance: the end of a part of the frame, for the report: the
input and network, the game's ticks (how many), the camera and director,
the drawing; the rest is presenting */
enum
{
HALO_VR_PHASE_INPUT,
HALO_VR_PHASE_TICKS,
HALO_VR_PHASE_CAMERA,
HALO_VR_PHASE_DRAW,
HALO_VR_PHASE_COUNT
};
void halo_vr_phase(int phase, int ticks);
/* vr.log_performance: time taken by a part of the drawing, for the report
(first_person_weapons.c): its start (0 when not logged), and its end. The
first-person pose (its animation and the arms), the arms alone, and the
pose taken as built already this frame (counted) */
enum
{
HALO_VR_TIMING_FIRST_PERSON,
HALO_VR_TIMING_ARMS,
HALO_VR_TIMING_FIRST_PERSON_KEPT,
HALO_VR_TIMING_COUNT
};
unsigned long long halo_vr_timing_start(void);
void halo_vr_timing_end(int part, unsigned long long start);
/* the headset's frames, counted (0 before the first): what is drawn the same
for every eye, as the first-person pose posed from the controller, is built
once a frame */
unsigned long halo_vr_frame_serial(void);
void halo_vr_present(unsigned int back_buffer_texture);
/* the eye image size: the back buffer's size in pixels in VR */
int halo_vr_eye_size(int *width, int *height);
+905 -110
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+26
View File
@@ -86,6 +86,10 @@ this list to generate the guest's entry points */
X(glCheckFramebufferStatus) \
X(glBlitFramebuffer) \
X(glDrawBuffers) \
X(glGenRenderbuffers) \
X(glBindRenderbuffer) \
X(glRenderbufferStorageMultisample) \
X(glFramebufferRenderbuffer) \
X(glReadBuffer) \
X(glInvalidateFramebuffer) \
X(glGenBuffers) \
@@ -195,6 +199,10 @@ this list to generate the guest's entry points */
X(glCheckFramebufferStatus) \
X(glBlitFramebuffer) \
X(glDrawBuffers) \
X(glGenRenderbuffers) \
X(glBindRenderbuffer) \
X(glRenderbufferStorageMultisample) \
X(glFramebufferRenderbuffer) \
X(glGenBuffers) \
X(glDeleteBuffers) \
X(glBindBuffer) \
@@ -209,6 +217,11 @@ this list to generate the guest's entry points */
X(glDisableVertexAttribArray) \
X(glVertexAttribPointer) \
X(glVertexAttribIPointer) \
X(glVertexAttribFormat) \
X(glVertexAttribIFormat) \
X(glVertexAttribBinding) \
X(glBindVertexBuffer) \
X(glGetQueryBufferObjectuiv) \
X(glVertexAttrib4fv) \
X(glVertexAttribI4ui) \
X(glDrawArrays) \
@@ -306,6 +319,10 @@ pointers, sees the declarations without these aliases */
#define glCheckFramebufferStatus halo_glCheckFramebufferStatus
#define glBlitFramebuffer halo_glBlitFramebuffer
#define glDrawBuffers halo_glDrawBuffers
#define glGenRenderbuffers halo_glGenRenderbuffers
#define glBindRenderbuffer halo_glBindRenderbuffer
#define glRenderbufferStorageMultisample halo_glRenderbufferStorageMultisample
#define glFramebufferRenderbuffer halo_glFramebufferRenderbuffer
#define glReadBuffer halo_glReadBuffer
#define glInvalidateFramebuffer halo_glInvalidateFramebuffer
#define glGenBuffers halo_glGenBuffers
@@ -413,6 +430,10 @@ pointers, sees the declarations without these aliases */
#define glCheckFramebufferStatus halo_glCheckFramebufferStatus
#define glBlitFramebuffer halo_glBlitFramebuffer
#define glDrawBuffers halo_glDrawBuffers
#define glGenRenderbuffers halo_glGenRenderbuffers
#define glBindRenderbuffer halo_glBindRenderbuffer
#define glRenderbufferStorageMultisample halo_glRenderbufferStorageMultisample
#define glFramebufferRenderbuffer halo_glFramebufferRenderbuffer
#define glGenBuffers halo_glGenBuffers
#define glDeleteBuffers halo_glDeleteBuffers
#define glBindBuffer halo_glBindBuffer
@@ -427,6 +448,11 @@ pointers, sees the declarations without these aliases */
#define glDisableVertexAttribArray halo_glDisableVertexAttribArray
#define glVertexAttribPointer halo_glVertexAttribPointer
#define glVertexAttribIPointer halo_glVertexAttribIPointer
#define glVertexAttribFormat halo_glVertexAttribFormat
#define glVertexAttribIFormat halo_glVertexAttribIFormat
#define glVertexAttribBinding halo_glVertexAttribBinding
#define glBindVertexBuffer halo_glBindVertexBuffer
#define glGetQueryBufferObjectuiv halo_glGetQueryBufferObjectuiv
#define glVertexAttrib4fv halo_glVertexAttrib4fv
#define glVertexAttribI4ui halo_glVertexAttribI4ui
#define glDrawArrays halo_glDrawArrays
+3 -1
View File
@@ -125,7 +125,9 @@ HALO_COMMON(hs_global_data, 4); /* struct data_array *hs_global_data */
HALO_COMMON(hs_thread_data, 4); /* struct data_array *hs_thread_data */
HALO_COMMON(hud_msg_def, 4); /* struct hud_messaging_parameters_definition *hud_msg_def */
HALO_COMMON(interrupt_result, 24); /* HRESULT interrupt_result */
HALO_COMMON(light_cluster_partition, 12); /* struct cluster_partition light_cluster_partition */
/* object_lights.c owns light_cluster_partition. On COFF a weak fallback
can override its tentative COMMON definition; let the owner allocate the
native struct cluster_partition, including its reference-cache fields. */
HALO_COMMON(light_data, 4); /* struct data_array *light_data */
HALO_COMMON(local_player_index_for_draw_string_and_hack_in_icons, 4); /* short local_player_index_for_draw_string_and_hack_in_icons */
HALO_COMMON(looping_sound_data, 4); /* struct data_array *looping_sound_data */
+12
View File
@@ -911,6 +911,11 @@ static struct
int failed;
} art[MAXIMUM_ART];
static long art_count;
/* the lowest and highest texture the art stands for: every texture the
renderer binds asks (xbox_textures.c), every frame of a game too, and the
menus' lie together, made as the map loaded, mostly apart from the map's
(in its texture cache) */
static unsigned long art_lowest, art_highest;
void halo_menus_art_register(void const *texture, char const *png)
{
@@ -944,6 +949,10 @@ void halo_menus_art_register(void const *texture, char const *png)
memset(&art[index], 0, sizeof(art[index]));
art[index].data = data;
art[index].png = strdup(png);
if (art_count == 1 || data < art_lowest)
art_lowest = data;
if (art_count == 1 || data > art_highest)
art_highest = data;
}
void halo_menus_art_forget(void)
@@ -957,12 +966,15 @@ void halo_menus_art_forget(void)
glDeleteTextures(1, &art[index].texture);
}
art_count = 0;
art_lowest = art_highest = 0;
}
unsigned int menu_art_texture(unsigned long data, unsigned long *levels)
{
long index;
if (!art_count || data < art_lowest || data > art_highest)
return 0;
for (index = 0; index < art_count; index++)
{
if (art[index].data == data)
+75
View File
@@ -518,6 +518,59 @@ static const char *comparison_operator(unsigned long function)
}
}
/* ---------- per-pixel model lighting
The game lights its models for each vertex: its model lighting programs
(nv2a_vsh.c nv2a_vertex_shader_lighting) sum the ambient light, two distant
lights and two point lights into the diffuse color v0. Halo's models have
few vertices, so the light across a curved surface comes out in facets, and
a point light that passes close lights only the vertices it reaches.
model_lighting computes the same sum for each pixel, from the normal and
world position the vertex program hands on and the same constants
(XGPU_MODEL_LIGHT_COUNT): the same falloff, facing, cone and translucency
terms, clamped where the programs clamp them. The diffuse alpha, which the
programs write apart (0, or planar fog's), is kept. */
static void model_lighting(struct xgpu_text *text, BOOL point_lights)
{
int light;
xgpu_text_append(text,
"uniform vec4 model_lights[%d];\n"
"in vec4 xWorldNormal;\n"
"%s"
"vec3 model_lighting()\n"
"{\n"
/* the normal's direction between the vertices, at the length the
vertices' normals have, as the programs light by it unnormalized
(shorter than 1 on some models: the first-person weapons) */
"\tvec3 n = xWorldNormal.xyz * (xWorldNormal.w * inversesqrt(max(dot(xWorldNormal.xyz, xWorldNormal.xyz), 1.0e-24)));\n"
/* the ambient light, the first distant light, which also lights the
back by the translucency, and the second */
"\tfloat facing = dot(n, -model_lights[7].xyz);\n"
"\tvec3 light = model_lights[11].xyz + max(max(facing, -facing * model_lights[0].z), 0.0) * model_lights[8].xyz +\n"
"\t\tmax(dot(n, -model_lights[9].xyz), 0.0) * model_lights[10].xyz;\n",
XGPU_MODEL_LIGHT_COUNT, point_lights ? "in vec3 xWorldPosition;\n" : "");
/* each point light: its position and 1 / radius squared, its cone's axis
and falloff scale, its color and falloff offset */
for (light = 0; point_lights && light < 2; light++)
{
int first = 1 + light * 3;
xgpu_text_append(text,
"\t{\n"
"\t\tvec3 to_light = model_lights[%d].xyz - xWorldPosition;\n"
"\t\tfloat distance_squared = dot(to_light, to_light);\n"
"\t\tvec3 l = to_light * inversesqrt(max(distance_squared, 1.0e-12));\n"
"\t\tlight += max(1.0 - distance_squared * model_lights[%d].w, 0.0) * clamp(dot(l, n), 0.0, 1.0) *\n"
"\t\t\tclamp(dot(l, -model_lights[%d].xyz) * model_lights[%d].w + model_lights[%d].w, 0.0, 1.0) *\n"
"\t\t\tmodel_lights[%d].xyz;\n"
"\t}\n",
first, first, first + 1, first + 1, first + 2, first + 2);
}
xgpu_text_append(text, "\treturn clamp(light, 0.0, 1.0);\n}\n");
}
char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key)
{
const DWORD *state = key->combiner_state;
@@ -556,6 +609,8 @@ char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key)
XGPU_PIXEL_UNIFORMS);
for (stage = 0; stage < 4; stage++)
xgpu_text_append(&text, "uniform %s tex%d;\n", sampler_declaration(key->sampler_type[stage]), stage);
if (key->per_pixel_lighting)
model_lighting(&text, key->per_pixel_lighting == 2);
xgpu_text_append(&text,
"float signed_byte(float x)\n"
"{\n"
@@ -572,6 +627,8 @@ char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key)
"\tvec4 v1 = xD1;\n"
"\tvec4 t0 = vec4(0.0), t1 = vec4(0.0), t2 = vec4(0.0), t3 = vec4(0.0);\n"
"\tfloat dot0 = 0.0, dot1 = 0.0, dot2 = 0.0, dot3 = 0.0;\n");
if (key->per_pixel_lighting)
xgpu_text_append(&text, "\tv0.rgb = model_lighting();\n");
for (stage = 0; stage < 4; stage++)
texture_stage(&text, key, stage);
@@ -645,9 +702,27 @@ char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key)
const char *comparison = comparison_operator(key->alpha_test_function);
if (!comparison)
{
xgpu_text_append(&text, "\tdiscard;\n");
}
else if (*comparison && key->alpha_test_samples && comparison[0] != '=' && comparison[0] != '!')
{
/* multisampled: the samples covered as alpha passes the reference
over the pixel (its change across the pixel from fwidth), the rest
left as they are; alpha itself is kept, the game keeping values of
its own in destination alpha */
xgpu_text_append(&text,
"\tfloat test_alpha = clamp(result.a, 0.0, 1.0) * 255.0;\n"
"\tfloat test_coverage = clamp(%s(test_alpha - alpha_reference) / max(fwidth(test_alpha), 1.0) + 0.5, 0.0, 1.0);\n"
"\tint test_samples = int(test_coverage * %d.0 + 0.5);\n"
"\tif (test_samples == 0) discard;\n"
"\tgl_SampleMask[0] = (1 << test_samples) - 1;\n",
comparison[0] == '<' ? "-" : "", (int)key->alpha_test_samples);
}
else if (*comparison)
{
xgpu_text_append(&text, "\tif (!(floor(clamp(result.a, 0.0, 1.0) * 255.0 + 0.5) %s alpha_reference)) discard;\n", comparison);
}
}
if (*config_string("debug.gpu_debug_expression"))
xgpu_text_append(&text, "\tresult = vec4(vec3(%s), 1.0);\n", config_string("debug.gpu_debug_expression"));
+250 -40
View File
@@ -7,7 +7,7 @@ GLSL.
Each instruction is four little-endian words. Word 1 to 3 hold a MAC
(vector) operation and an ILU (scalar) operation that execute in parallel
on the same three operands A, B and C, and the destinations of both; the
bit positions of every field are spelled out in operand() and in
bit positions of every field are spelled out in operand_fields() and in
nv2a_vertex_shader_to_glsl(). When both units run, the ILU result goes to
temporary r1, whatever the instruction's temporary register field says.
@@ -76,43 +76,55 @@ static void write_mask(unsigned long mask, char *out)
out[count] = 0;
}
static void operand(struct xgpu_text *text, const DWORD *instruction, char which, int relative)
struct operand_fields
{
static const char swizzle_names[] = "xyzw";
unsigned long negate, swizzle[4], index, mux;
};
static void operand_fields(const DWORD *instruction, char which, struct operand_fields *fields)
{
switch (which)
{
case 'A':
negate = field(instruction, 1, 8, 1);
swizzle[0] = field(instruction, 1, 6, 2);
swizzle[1] = field(instruction, 1, 4, 2);
swizzle[2] = field(instruction, 1, 2, 2);
swizzle[3] = field(instruction, 1, 0, 2);
index = field(instruction, 2, 28, 4);
mux = field(instruction, 2, 26, 2);
fields->negate = field(instruction, 1, 8, 1);
fields->swizzle[0] = field(instruction, 1, 6, 2);
fields->swizzle[1] = field(instruction, 1, 4, 2);
fields->swizzle[2] = field(instruction, 1, 2, 2);
fields->swizzle[3] = field(instruction, 1, 0, 2);
fields->index = field(instruction, 2, 28, 4);
fields->mux = field(instruction, 2, 26, 2);
break;
case 'B':
negate = field(instruction, 2, 25, 1);
swizzle[0] = field(instruction, 2, 23, 2);
swizzle[1] = field(instruction, 2, 21, 2);
swizzle[2] = field(instruction, 2, 19, 2);
swizzle[3] = field(instruction, 2, 17, 2);
index = field(instruction, 2, 13, 4);
mux = field(instruction, 2, 11, 2);
fields->negate = field(instruction, 2, 25, 1);
fields->swizzle[0] = field(instruction, 2, 23, 2);
fields->swizzle[1] = field(instruction, 2, 21, 2);
fields->swizzle[2] = field(instruction, 2, 19, 2);
fields->swizzle[3] = field(instruction, 2, 17, 2);
fields->index = field(instruction, 2, 13, 4);
fields->mux = field(instruction, 2, 11, 2);
break;
default:
negate = field(instruction, 2, 10, 1);
swizzle[0] = field(instruction, 2, 8, 2);
swizzle[1] = field(instruction, 2, 6, 2);
swizzle[2] = field(instruction, 2, 4, 2);
swizzle[3] = field(instruction, 2, 2, 2);
index = (field(instruction, 2, 0, 2) << 2) | field(instruction, 3, 30, 2);
mux = field(instruction, 3, 28, 2);
fields->negate = field(instruction, 2, 10, 1);
fields->swizzle[0] = field(instruction, 2, 8, 2);
fields->swizzle[1] = field(instruction, 2, 6, 2);
fields->swizzle[2] = field(instruction, 2, 4, 2);
fields->swizzle[3] = field(instruction, 2, 2, 2);
fields->index = (field(instruction, 2, 0, 2) << 2) | field(instruction, 3, 30, 2);
fields->mux = field(instruction, 3, 28, 2);
break;
}
}
xgpu_text_append(text, "%s", negate ? "-" : "");
static void operand(struct xgpu_text *text, const DWORD *instruction, char which, int relative)
{
static const char swizzle_names[] = "xyzw";
struct operand_fields fields;
unsigned long *swizzle = fields.swizzle, index, mux;
operand_fields(instruction, which, &fields);
index = fields.index;
mux = fields.mux;
xgpu_text_append(text, "%s", fields.negate ? "-" : "");
switch (mux)
{
case _mux_temporary:
@@ -139,6 +151,183 @@ static void operand(struct xgpu_text *text, const DWORD *instruction, char which
swizzle_names[swizzle[0]], swizzle_names[swizzle[1]], swizzle_names[swizzle[2]], swizzle_names[swizzle[3]]);
}
/* ---------- model lighting
The game's model lighting programs (rasterizer_xbox_vertex_shaders_data.inc
9, 10, 17 and 27) sum into the diffuse color oD0.xyz the ambient light
c[-69], two distant lights (direction c[-73] and c[-71], color c[-72] and
c[-70]; the first also lights the back by the translucency c[-82].z) and, in
10 and 17, two point lights (c[-79] to c[-77] and c[-76] to c[-74]: position
and 1 / radius squared, the cone's axis and falloff scale, the color and
falloff offset). The normal and world position they light by are
temporaries that the programs reuse (10 and 17 overwrite both after the
sum). In 9, 10 and 17 the normal is the skinned normal turned by c[-84].w
for the back faces' pass, r0 (written by instruction 17) and the position
the skinned position, r10 (by 12); in 27, of a single node, the normal is
r3 (by 5). They are taken as the first instruction that lights by them
reads them: in 9 the normal before instruction 19, in 10 and 17 the normal
before 24 and the position before 19, in 27 the normal before 7. A program
is taken only where its lighting is the sum nv2a_psh.c computes for each
pixel (the diffuse color written once, by the sum; both distant lights by
one normal; the translucency; each point light by one position, its cone
by its axis reversed) and the registers keep the same normal and position
through it. Nothing else of the programs goes into oD0.xyz; oD0.w is 0, or
in 17 the planar fog's. */
static BOOL xyz_unswizzled(const struct operand_fields *operand)
{
return operand->swizzle[0] == 0 && operand->swizzle[1] == 1 && operand->swizzle[2] == 2;
}
/* the operand is c[reg] (as the game numbers them: -73), its xyz unswizzled */
static BOOL light_constant(const DWORD *instruction, const struct operand_fields *operand, int reg, BOOL negated)
{
return operand->mux == _mux_constant && !field(instruction, 3, 1, 1) &&
field(instruction, 1, 13, 8) == (unsigned long)(XGPU_VERTEX_CONSTANT_BIAS + reg) &&
operand->negate == (negated ? 1UL : 0UL) && xyz_unswizzled(operand);
}
/* the operand is a temporary (not the position output), its xyz unswizzled */
static BOOL light_temporary(const struct operand_fields *operand, BOOL negated)
{
return operand->mux == _mux_temporary && operand->index < 12 &&
operand->negate == (negated ? 1UL : 0UL) && xyz_unswizzled(operand);
}
/* any of x, y and z of temporary reg written by the instructions in [first, last) */
static BOOL temporary_written(const DWORD *instructions, unsigned long first, unsigned long last, unsigned long reg)
{
unsigned long index;
for (index = first; index < last; index++)
{
const DWORD *instruction = instructions + index * 4;
unsigned long mac = field(instruction, 1, 21, 4);
unsigned long ilu = field(instruction, 1, 25, 3);
unsigned long temporary = field(instruction, 3, 20, 4);
if (mac != _mac_nop && mac != _mac_arl && temporary == reg && (field(instruction, 3, 24, 4) & 0xe))
return TRUE;
/* (the ILU writes r1 when both units run) */
if (ilu != _ilu_nop && (mac != _mac_nop ? 1 : temporary) == reg && (field(instruction, 3, 16, 4) & 0xe))
return TRUE;
}
return FALSE;
}
BOOL nv2a_vertex_shader_lighting(const DWORD *instructions, unsigned long instruction_count,
struct nv2a_vertex_lighting *lighting)
{
unsigned long count, index, diffuse;
unsigned long distant[2], distant_register[2] = { 0, 0 }, point[2], point_register[2] = { 0, 0 };
BOOL cone[2] = { FALSE, FALSE }, translucency = FALSE, point_lights = FALSE;
int light;
/* to the instruction that ends the program */
for (count = 0; count < instruction_count;)
{
if (field(instructions + count++ * 4, 3, 0, 1))
break;
}
diffuse = distant[0] = distant[1] = point[0] = point[1] = count;
for (index = 0; index < count; index++)
{
const DWORD *instruction = instructions + index * 4;
unsigned long mac = field(instruction, 1, 21, 4);
unsigned long output_mask = field(instruction, 3, 12, 4);
unsigned long constant = field(instruction, 1, 13, 8);
struct operand_fields a, b, c;
operand_fields(instruction, 'A', &a);
operand_fields(instruction, 'B', &b);
operand_fields(instruction, 'C', &c);
/* the diffuse color's x, y and z, written once and last by the sum:
the second distant light's term times its color, plus the rest */
if ((output_mask & 0xe) && field(instruction, 3, 11, 1) && field(instruction, 3, 3, 8) == 3)
{
if (diffuse != count || (output_mask & 0xe) != 0xe || field(instruction, 3, 2, 1) ||
mac != _mac_mad || !light_constant(instruction, &b, -70, FALSE) || c.mux != _mux_temporary)
{
return FALSE;
}
diffuse = index;
}
for (light = 0; light < 2; light++)
{
/* each distant light's facing: the normal's dot product with the
light's direction reversed */
if (mac == _mac_dp3 && light_constant(instruction, &b, light ? -71 : -73, TRUE))
{
if (distant[light] != count || !light_temporary(&a, FALSE))
return FALSE;
distant[light] = index;
distant_register[light] = a.index;
}
/* each point light's direction, from the world position */
if (mac == _mac_add && light_constant(instruction, &a, light ? -76 : -79, FALSE) &&
light_temporary(&c, TRUE))
{
if (point[light] != count)
return FALSE;
point[light] = index;
point_register[light] = c.index;
}
/* and its cone: the direction's dot product with the light's axis
reversed */
if (mac == _mac_dp3 && b.mux == _mux_constant && !field(instruction, 3, 1, 1) &&
constant == (unsigned long)(XGPU_VERTEX_CONSTANT_BIAS + (light ? -75 : -78)))
{
if (!light_constant(instruction, &b, light ? -75 : -78, TRUE))
return FALSE;
cone[light] = TRUE;
}
}
/* the back lit by the first distant light: its facing reversed, times
the translucency */
if (mac == _mac_mul && a.mux == _mux_temporary && a.negate && b.mux == _mux_constant &&
!field(instruction, 3, 1, 1) && constant == XGPU_VERTEX_CONSTANT_BIAS - 82 && b.swizzle[0] == 2)
{
translucency = TRUE;
}
if ((a.mux == _mux_constant || b.mux == _mux_constant || c.mux == _mux_constant) &&
!field(instruction, 3, 1, 1) && constant >= XGPU_VERTEX_CONSTANT_BIAS - 79 &&
constant <= XGPU_VERTEX_CONSTANT_BIAS - 74)
{
point_lights = TRUE;
}
}
if (diffuse == count || distant[0] >= diffuse || distant[1] >= diffuse ||
distant_register[0] != distant_register[1] || !translucency)
{
return FALSE;
}
lighting->lights = 1;
lighting->normal_register = distant_register[0];
lighting->normal_instruction = distant[0] < distant[1] ? distant[0] : distant[1];
if (temporary_written(instructions, lighting->normal_instruction, diffuse, lighting->normal_register))
return FALSE;
lighting->position_instruction = lighting->position_register = 0;
if (point_lights)
{
unsigned long last = point[0] > point[1] ? point[0] : point[1];
if (point[0] >= diffuse || point[1] >= diffuse || point_register[0] != point_register[1] ||
!cone[0] || !cone[1])
{
return FALSE;
}
lighting->lights = 2;
lighting->position_register = point_register[0];
lighting->position_instruction = point[0] < point[1] ? point[0] : point[1];
if (temporary_written(instructions, lighting->position_instruction, last, lighting->position_register))
return FALSE;
}
return TRUE;
}
/* ---------- translation */
static const char shader_prologue[] =
#ifdef HALO_ANDROID
/* the #version line comes first, from the context's capabilities */
@@ -195,7 +384,7 @@ static const char shader_prologue[] =
"}\n";
char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instruction_count,
unsigned long packed_attribute_mask, int multiview)
unsigned long packed_attribute_mask, const struct nv2a_vertex_lighting *lighting, int multiview)
{
struct xgpu_text text = { 0 };
unsigned long index;
@@ -221,6 +410,11 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
(void)multiview;
#endif
xgpu_text_append(&text, "%s", shader_prologue);
/* (the pixel shader's model_lighting; the normal's length in w) */
if (lighting)
xgpu_text_append(&text, "out vec4 xWorldNormal;\n");
if (lighting && lighting->lights == 2)
xgpu_text_append(&text, "out vec3 xWorldPosition;\n");
for (index = 0; index < XGPU_VERTEX_ATTRIBUTE_COUNT; index++)
{
if (packed_attribute_mask & (1UL << index))
@@ -249,9 +443,7 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
"\tvec4 oFog = vec4(1.0), oPts = vec4(point_size), oUnused = vec4(0.0);\n"
"\tint a0 = 0;\n"
"\tvec4 A, B, C, mac, ilu;\n");
#ifdef HALO_ANDROID
xgpu_text_append(&text, "\tvec4 clip_position = vec4(0.0);\n\tbool clip_captured = false;\n");
#endif
for (index = 0; index < instruction_count; index++)
{
@@ -269,6 +461,14 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
char mask[5];
xgpu_text_append(&text, "\t/* %lu */\n", index);
/* the lighting's normal and position, before the program reuses them */
if (lighting && index == lighting->normal_instruction)
{
xgpu_text_append(&text, "\txWorldNormal = vec4(r%lu.xyz, length(r%lu.xyz));\n",
lighting->normal_register, lighting->normal_register);
}
if (lighting && lighting->lights == 2 && index == lighting->position_instruction)
xgpu_text_append(&text, "\txWorldPosition = r%lu.xyz;\n", lighting->position_register);
xgpu_text_append(&text, "\tA = "); operand(&text, instruction, 'A', relative); xgpu_text_append(&text, ";\n");
xgpu_text_append(&text, "\tB = "); operand(&text, instruction, 'B', relative); xgpu_text_append(&text, ";\n");
xgpu_text_append(&text, "\tC = "); operand(&text, instruction, 'C', relative); xgpu_text_append(&text, ";\n");
@@ -303,7 +503,6 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
case _ilu_lit: xgpu_text_append(&text, "\tilu = nv2a_lit(C);\n"); break;
default: xgpu_text_append(&text, "\tilu = vec4(0.0);\n"); break;
}
#ifdef HALO_ANDROID
/* the screen-space conversion takes the reciprocal of the clip-space
position's w (rcc of r12.w); keep the position it converts */
if (ilu == _ilu_rcc && field(instruction, 3, 28, 2) == _mux_temporary &&
@@ -311,7 +510,6 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
{
xgpu_text_append(&text, "\tclip_position = oPos;\n\tclip_captured = true;\n");
}
#endif
/* results are written only after both units have read their inputs */
if (mac == _mac_arl)
@@ -357,21 +555,33 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
/* Direct3D 8 puts pixel centres on integer screen coordinates (the
game offsets its screen-space quads by -0.5 to match), OpenGL on
half-integers */
#ifdef HALO_ANDROID
/* The conversion is screen = clip * c[-38] * rcc(w) + c[-37]; undoing
it by multiplying by w again is lossy near the camera plane, where
rcc clamps and 1/w rounds differently on each GPU (Mali put vertices
of the first-person weapon at the vanishing point). Where the clip
position was kept, the same result is computed without dividing. */
"\tvec4 position;\n"
"\tif (clip_captured)\n"
"\t\tgl_Position = vec4((clip_position.xyz * c[%d].xyz + (c[%d].xyz + vec3(0.5 + screen_offset, 0.5, 0.0)\n"
"\t\tposition = vec4((clip_position.xyz * c[%d].xyz + (c[%d].xyz + vec3(0.5 + screen_offset, 0.5, 0.0)\n"
"\t\t\t- viewport_offset.xyz) * clip_position.w) / scale, clip_position.w);\n"
"\telse\n"
"\t\tgl_Position = vec4((vec3(oPos.xy + vec2(0.5 + screen_offset, 0.5), oPos.z) - viewport_offset.xyz) / scale * oPos.w, oPos.w);\n"
#else
"\tvec3 ndc = (vec3(oPos.xy + vec2(0.5 + screen_offset, 0.5), oPos.z) - viewport_offset.xyz) / scale;\n"
"\tgl_Position = vec4(ndc * oPos.w, oPos.w);\n"
#endif
"\t{\n"
"\t\tvec3 ndc = (vec3(oPos.xy + vec2(0.5 + screen_offset, 0.5), oPos.z) - viewport_offset.xyz) / scale;\n"
"\t\tposition = vec4(ndc * oPos.w, oPos.w);\n"
"\t}\n"
/* A position whose w is zero, or is not a number, is the clip-space
origin: the screen conversion's reciprocal is clamped rather than
infinite, so a large position times a w of zero is exactly zero, and
the origin is inside the frustum. Nothing then clips the triangle
away and OpenGL divides zero by zero there: the vertex lands on the
middle of the screen and the triangle is drawn out to it from the
first-person weapon, whose pose follows the camera and so reaches the
camera plane. The divide on the Xbox sends such a vertex to infinity
and the clipper takes the triangle; put it behind the camera instead,
which the clipper also takes. */
"\tif (!(abs(position.w) > 0.0))\n"
"\t\tposition = vec4(0.0, 0.0, 0.0, -1.0);\n"
"\tgl_Position = position;\n"
#ifdef HALO_ANDROID
/* what glClipControl(GL_UPPER_LEFT, GL_ZERO_TO_ONE) does on desktop
GL: rows from the top, depth 0..1 */
@@ -389,16 +599,16 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
"\txT2 = oT2;\n"
"\txT3 = oT3;\n"
"\txFog = oFog.x;\n"
"}\n"
"}\n",
XGPU_VERTEX_CONSTANT_BIAS - 38, XGPU_VERTEX_CONSTANT_BIAS - 37
#ifdef HALO_ANDROID
, XGPU_VERTEX_CONSTANT_BIAS - 38, XGPU_VERTEX_CONSTANT_BIAS - 37,
/* (what is drawn in screen space, which kept no clip position, is
taken to each view as though infinitely far: a lens flare's sun
lands where the sky's does) */
/* (what the game projects itself, a sprite or a lens flare's
test, comes as a screen position with w 1 and its depth in z: its
eye's offset is by that depth, where it is depth tested) */
multiview ?
, multiview ?
"\t{\n"
"\t\tint row = int(gl_ViewID_OVR) * 2;\n"
"\t\tfloat near = 0.0;\n"
+142 -21
View File
@@ -274,7 +274,7 @@ struct stun_server
struct p2p_candidate mapped;
};
static pthread_mutex_t p2p_lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t p2p_lock = PTHREAD_MUTEX_INITIALIZER;
static struct
{
@@ -316,6 +316,11 @@ static struct
int hosting_lan_only;
int has_token;
unsigned char token[P2P_TOKEN_SIZE];
/* whether the server browser's exit hook is set (lobby_quit) */
int lobby_quit_registered;
/* whether the server browser listed this token (p2p_lobby.c): going
private makes a new one */
int token_listed;
char invite[P2P_LINK_SIZE];
int invite_copied;
/* the game's players and its most (p2p_set_game_player_counts; 0: not
@@ -355,7 +360,11 @@ p2p.local_address) */
static unsigned short proxy_by_port[65536];
static unsigned char identifier[P2P_IDENTIFIER_SIZE];
/* this run's X25519 keys, which the identifier comes from */
/* this run's X25519 keys, which the identifier comes from: from an Ed25519
seed, whose key signs the listing of a public game (p2p_lobby.c), so that a
listing's key is also the invite's */
static unsigned char seed[P2P_SEED_SIZE];
static unsigned char signing_key[P2P_KEY_SIZE];
static unsigned char secret_key[P2P_KEY_SIZE];
static unsigned char public_key[P2P_KEY_SIZE];
static int has_identifier;
@@ -503,7 +512,8 @@ const unsigned char *p2p_identifier(void)
pthread_mutex_lock(&identifier_lock);
if (!has_identifier)
{
posix_random_bytes(secret_key, sizeof(secret_key));
posix_random_bytes(seed, sizeof(seed));
p2p_ed25519_public(seed, signing_key, secret_key);
p2p_x25519(public_key, secret_key, NULL);
p2p_identifier_for(public_key, identifier);
has_identifier = 1;
@@ -542,6 +552,18 @@ const unsigned char *p2p_public_key(void)
return public_key;
}
const unsigned char *p2p_signing_key(void)
{
p2p_identifier();
return signing_key;
}
void p2p_sign(const void *message, int size, unsigned char *signature)
{
p2p_identifier();
p2p_ed25519_sign(seed, signing_key, message, size, signature);
}
int p2p_shared_secret(const unsigned char *key, unsigned char *shared)
{
static const unsigned char zero[P2P_KEY_SIZE];
@@ -2362,6 +2384,8 @@ void p2p_set_public_game(int public_game)
pthread_mutex_lock(&p2p_lock);
p2p_list_set_public(public_game);
pthread_mutex_unlock(&p2p_lock);
/* (and in the server browser of every build: p2p_lobby.c) */
p2p_set_hosting_public(public_game);
}
int p2p_public_game(void)
@@ -2382,13 +2406,53 @@ void p2p_set_game_description(const char *name, const char *map, const char *mod
pthread_mutex_unlock(&p2p_lock);
}
/* the server browser's games first (every build lists its public games
there: PC, phones, VR), then the VR port's own list's that it has not */
int p2p_public_games(struct p2p_public_game *games, int maximum)
{
int count;
struct p2p_listing listings[32];
struct p2p_public_game own[32];
int listing_count;
int own_count;
int count = 0;
int index;
p2p_lobby_browse(1);
listing_count = p2p_lobby_games(listings, 32);
for (index = 0; index < listing_count && count < maximum; index++)
{
struct p2p_public_game *game;
/* (one with a password: joined from the server browser, which asks
for it, not from this list, which has no way to) */
if (listings[index].locked)
continue;
game = &games[count++];
memset(game, 0, sizeof(*game));
snprintf(game->name, sizeof(game->name), "%s", listings[index].name);
snprintf(game->map, sizeof(game->map), "%s", listings[index].map);
snprintf(game->mode, sizeof(game->mode), "%s", listings[index].gametype);
snprintf(game->invite, sizeof(game->invite), "%s", listings[index].invite);
game->players = listings[index].player_count;
game->maximum_players = listings[index].maximum_player_count;
}
pthread_mutex_lock(&p2p_lock);
count = p2p.running ? p2p_list_games(games, maximum) : 0;
own_count = p2p.running ? p2p_list_games(own, 32) : 0;
pthread_mutex_unlock(&p2p_lock);
for (index = 0; index < own_count && count < maximum; index++)
{
int other;
int listed = 0;
for (other = 0; other < listing_count; other++)
{
if (!strcmp(own[index].name, listings[other].name) && !strcmp(own[index].map, listings[other].map))
listed = 1;
}
if (!listed)
games[count++] = own[index];
}
return count;
}
@@ -2455,27 +2519,56 @@ static int connected_player_count(void)
return count;
}
/* a new invite (a new token) */
static void make_invite(void)
{
unsigned char bytes[P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE];
char text[2 * (P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE) + 1];
posix_random_bytes(p2p.token, sizeof(p2p.token));
p2p.has_token = 1;
p2p.token_listed = 0;
p2p_key_hash(p2p_public_key(), bytes);
memcpy(bytes + P2P_KEY_HASH_SIZE, p2p.token, P2P_TOKEN_SIZE);
p2p_hex(bytes, sizeof(bytes), text);
snprintf(p2p.invite, sizeof(p2p.invite), "halo://join/%s", text);
}
void p2p_new_invite_if_listed(void)
{
if (!p2p.has_token || !p2p.token_listed)
return;
make_invite();
platform_log("Internet play: a new invite, so that the one listed lets no one in (private, or a new password): %s",
p2p.invite);
if (p2p.hosting)
{
p2p_signal_host(p2p.token);
memcpy(p2p.clipboard, p2p.invite, sizeof(p2p.clipboard));
p2p.has_clipboard = 1;
/* (Discord is told the new one) */
p2p.reported_player_count = -1;
}
}
/* the hosted game's players, as the game says; else the host and the
machines the tunnel reaches */
static void hosted_player_counts(int *count, int *maximum)
{
*count = p2p.game_player_maximum > 0 ? p2p.game_player_count : connected_player_count() + 1;
*maximum = p2p.game_player_maximum > 0 ? p2p.game_player_maximum : P2P_MAXIMUM_PEERS + 1;
}
static void update_hosting(void)
{
int want = p2p.hosting_socket >= 0 && !p2p.hosting_lan_only;
if (want && !p2p.hosting)
{
char text[2 * (P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE) + 1];
/* one invite for the whole run, so a link keeps working from game
to game */
if (!p2p.has_token)
{
unsigned char bytes[P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE];
posix_random_bytes(p2p.token, sizeof(p2p.token));
p2p.has_token = 1;
p2p_key_hash(p2p_public_key(), bytes);
memcpy(bytes + P2P_KEY_HASH_SIZE, p2p.token, P2P_TOKEN_SIZE);
p2p_hex(bytes, sizeof(bytes), text);
snprintf(p2p.invite, sizeof(p2p.invite), "halo://join/%s", text);
}
make_invite();
p2p.hosting = 1;
p2p.stun_started = 1;
p2p_signal_start();
@@ -2507,11 +2600,9 @@ static void update_hosting(void)
}
if (p2p.hosting)
{
/* the game's players, as the game says; else the host and the
machines the tunnel reaches */
int count = p2p.game_player_maximum > 0 ? p2p.game_player_count : connected_player_count() + 1;
int maximum = p2p.game_player_maximum > 0 ? p2p.game_player_maximum : P2P_MAXIMUM_PEERS + 1;
int count, maximum;
hosted_player_counts(&count, &maximum);
if (count != p2p.reported_player_count || maximum != p2p.reported_player_maximum)
{
p2p.reported_player_count = count;
@@ -2601,6 +2692,15 @@ static void *upnp_thread(void *unused)
return NULL;
}
/* quitting: a game listed in the server browser is taken out of it at once
(not when its listing lapses), as when it stops being hosted */
static void lobby_quit(void)
{
pthread_mutex_lock(&p2p_lock);
p2p_lobby_quit();
pthread_mutex_unlock(&p2p_lock);
}
/* the game exits: the router forwards the port no longer (after a request
under way, which may forward one, if it ends soon) */
static void upnp_release(void)
@@ -2637,6 +2737,9 @@ static int upnp_needed(void)
if (p2p.joining && elapsed(p2p.join_time, UPNP_JOIN_DELAY))
return 1;
/* a game in the server browser: at once (more joiners get through) */
if (p2p_lobby_listed())
return 1;
for (index = 0; index < P2P_MAXIMUM_PEERS; index++)
{
struct peer const *peer = &p2p.peers[index];
@@ -3013,6 +3116,24 @@ static void *p2p_thread(void *unused)
expire_proxies();
stun_update();
update_hosting();
/* the server browser (p2p_lobby.c): signalling while browsing too */
if (p2p_lobby_browsing())
p2p_signal_start();
{
int count, maximum;
hosted_player_counts(&count, &maximum);
p2p_lobby_update(p2p.hosting && p2p.has_token ? p2p.token : NULL, count, maximum);
if (p2p_lobby_listed())
{
p2p.token_listed = 1;
if (!p2p.lobby_quit_registered)
{
p2p.lobby_quit_registered = 1;
atexit(lobby_quit);
}
}
}
update_joining();
update_upnp();
p2p_discord_update();
Loaded 100 of 252 files, more files were not shown because too many files have changed in this diff. Show more