Compare commits

...
Author SHA1 Message Date
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
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
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
126 changed files with 10971 additions and 998 deletions

No files matched your search

+6 -1
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
+13
View File
@@ -123,3 +123,16 @@ page of buttons shows this layout on the Frame.
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.
+133
View File
@@ -0,0 +1,133 @@
# 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 |
| 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 | |
| 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 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).
+3 -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 */
+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;
}
+20
View File
@@ -326,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)
+406 -33
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,6 +105,10 @@ 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;
@@ -119,6 +126,33 @@ static struct
int left_grip_valid;
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;
@@ -555,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)
@@ -597,6 +660,8 @@ 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);
@@ -702,13 +767,62 @@ static int actions_initialize(void)
{ &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("/interaction_profiles/valve/frame_controller_valve", frame_controller,
sizeof(frame_controller) / sizeof(frame_controller[0]));
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("/interaction_profiles/oculus/touch_controller", touch, sizeof(touch) / sizeof(touch[0]));
suggest("/interaction_profiles/valve/index_controller", index, sizeof(index) / sizeof(index[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]));
}
@@ -751,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 };
@@ -851,6 +980,17 @@ static void hands_read(struct halo_xr_frame *frame)
(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)
@@ -1080,6 +1220,9 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
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;
@@ -1113,6 +1256,7 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
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)
@@ -1226,6 +1370,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
@@ -1242,6 +1409,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;
@@ -1405,8 +1582,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 };
@@ -1418,17 +1617,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);
@@ -1493,13 +1702,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 */
@@ -1513,13 +1731,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 };
@@ -1537,6 +1837,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)
@@ -1662,11 +1963,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)
+6
View File
@@ -63,6 +63,12 @@ enum
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 */
+3
View File
@@ -315,6 +315,9 @@
<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>
@@ -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">
@@ -9,25 +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="358" 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="101"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_invite" x="54" y="129"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_listing" x="54" y="157"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_friendly_fire" x="54" y="185"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_extra_enemies" x="54" y="213"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_enemies_per_player" x="54" y="241"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_enemies_multiplier" x="54" y="241"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_game_type" x="54" y="185"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_player_options" x="54" y="213"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_item_options" x="54" y="241"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_vehicle_options" x="54" y="269"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_indicator_options" x="54" y="297"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_team_options" x="54" y="325"/>
<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">
@@ -78,6 +80,19 @@
<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"/>
@@ -122,6 +137,17 @@
<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"/>
@@ -211,6 +237,8 @@
<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=""/>
@@ -234,5 +262,8 @@
<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>
@@ -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,20 +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_resolution" x="54" y="99" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_window_size" x="54" y="99" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution_scaling" x="54" y="125" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_vsync" x="54" y="151"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_max_fps" x="54" y="177" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_interpolation" x="54" y="203"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_direct_camera" x="54" y="229" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_hud" x="54" y="255"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_text" x="54" y="281"/>
<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">
@@ -120,6 +124,42 @@
<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"/>
@@ -141,6 +181,10 @@
<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=""/>
@@ -154,5 +198,9 @@
<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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

+1
View File
@@ -54,6 +54,7 @@
"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",
@@ -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

+37 -18
View File
@@ -56,13 +56,17 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick:
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
4 world units of the player in it), so one player can't drag the team
back through the level. 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, since only the host could revert. A level won ends the round
as in multiplayer, and the next round is the campaign's next level.
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
@@ -73,12 +77,13 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick:
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 in rings
about its starting locations where the ground is open, 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.
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.
@@ -159,7 +164,16 @@ 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).
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
@@ -181,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
@@ -198,7 +216,8 @@ 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
+35 -8
View File
@@ -228,21 +228,25 @@ the setting for one start of the game. It has priority over the file.
| `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 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 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. |
@@ -261,6 +265,7 @@ the setting for one start of the game. It has priority over the file.
| `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. |
@@ -460,10 +465,18 @@ 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. REFRESH
asks the hosts for their listings again.
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`):
@@ -485,6 +498,14 @@ How it operates (`src/p2p_lobby.c`):
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
@@ -546,12 +567,15 @@ 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`. `kick <player name>` drops the player the same
@@ -627,6 +651,7 @@ The game sends the activity only to a Discord client of the same user.
| 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 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. |
@@ -715,6 +740,8 @@ Other changes:
| `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 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. |
+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);
+184 -30
View File
@@ -7,9 +7,15 @@ 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. The
extra enemies stand in rings about the squad's starting locations, on
open ground, so they do not start inside each other.
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
@@ -60,9 +66,12 @@ enum
COOP_ENEMIES_MAXIMUM_PERCENT = 200,
COOP_ENEMIES_MINIMUM_MULTIPLIER = 2,
COOP_ENEMIES_MAXIMUM_MULTIPLIER = 32,
/* the places tried for an extra enemy, a ring further out each */
SPREAD_ATTEMPTS = 8,
/* 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,
@@ -71,11 +80,25 @@ enum
RIDER_RELEASE_TICKS = 4,
};
/* the rings' spacing, and the height over the ground the way to a place is
looked along, and the ground looked for below it */
/* (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
#define SPREAD_GROUND_DEPTH 2.0f
/* 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
@@ -160,35 +183,105 @@ static short coop_enemies_player_count(
return count;
}
/* whether the way from a to b is open (no structure between them) */
static boolean coop_enemies_open(
real_point3d const *a,
real_point3d const *b)
/* 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;
real_vector3d vector;
vector.i = b->x - a->x;
vector.j = b->y - a->y;
vector.k = b->z - a->z;
return !collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit) |
FLAG(_collision_test_back_facing_surfaces_bit), a, &vector, NONE, &collision);
return collision_test_vector(SPREAD_COLLISION_FLAGS, from, vector, NONE, &collision);
}
/* the ground below a point, within SPREAD_GROUND_DEPTH of it */
static boolean coop_enemies_ground(
/* 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 *ground)
real_point3d *floor)
{
struct collision_result collision;
real_vector3d vector = { 0.0f, 0.0f, -(SPREAD_STEP_HEIGHT + SPREAD_GROUND_DEPTH) };
real_point3d from = *point;
real_vector3d down = { 0.0f, 0.0f, -(2.0f * SPREAD_STEP_HEIGHT + SPREAD_FLOOR_DIFFERENCE) };
if (!collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
point, &vector, NONE, &collision))
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;
}
*ground = collision.point;
/* (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;
}
@@ -263,6 +356,7 @@ static void coop_enemies_rider_place(
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);
@@ -299,7 +393,13 @@ static void coop_enemies_rider_place(
}
}
}
/* (never on the rider's own place: a ring about it, further each round) */
/* 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);
@@ -357,6 +457,48 @@ short coop_enemies_extra_count(
}
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)
@@ -367,18 +509,30 @@ boolean coop_enemies_spread_position(
short attempt;
from.z += SPREAD_STEP_HEIGHT;
for (attempt = 0; attempt < SPREAD_ATTEMPTS; attempt++, ring++)
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;
to.x += (real)cos(angle) * radius;
to.y += (real)sin(angle) * radius;
if (coop_enemies_open(&from, &to) && coop_enemies_ground(&to, position))
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;
}
+20 -3
View File
@@ -21,10 +21,27 @@ short coop_enemies_extra_count(
long encounter_index,
short count);
/* encounters.c: the place of a squad's `number`th extra enemy (from 1) at
its starting location `origin`: rings about it, on open ground; FALSE if
none is (the starting location itself then) */
/* 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);
+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
+250 -16
View File
@@ -1970,6 +1970,10 @@ Xbox's networking, run by the engine's port entry points
#define KEY_MODIFIER_CONTROL_BIT 1
#define LOBBY_NAME "pc\\main_menu\\multiplayer_type_select\\lobby\\lobby_screen"
#define PREVIEW_NAME "pc\\main_menu\\multiplayer_type_select\\lobby\\preview_screen"
/* the server browser's password screen (tools/port_settings.py), and the
longest password */
#define PASSWORD_NAME "pc\\main_menu\\multiplayer_type_select\\join_game\\password\\password_screen"
#define PASSWORD_LENGTH 32
/* the lobby's panel's lines (the gametype, the players, the countdown, the
invite) */
#define LOBBY_TEXT_LENGTH (ROW_TEXT_LENGTH * 4)
@@ -2036,6 +2040,7 @@ boolean ui_widget_port_unjoin_player(struct widget_instance *widget, struct even
boolean *widget_deleted);
void network_game_server_port_set_settings(wchar_t const *name, long maximum_players);
void network_game_server_port_set_cooperative_friendly_fire(short friendly_fire);
void network_game_server_port_set_cooperative_player_collisions(boolean player_collisions);
void *global_network_game_client_get(void);
void *global_network_game_server_get(void);
boolean network_game_is_splitscreen_local(void);
@@ -2092,6 +2097,8 @@ static struct
boolean cooperative_maximum_players_set;
/* co-op's FRIENDLY FIRE shown (network.coop_friendly_fire's) */
short cooperative_friendly_fire;
/* co-op's PLAYER COLLISIONS shown, OFF (network.coop_player_collisions's) */
boolean cooperative_no_player_collisions;
/* the browser's games */
struct advertised_game *games[MAXIMUM_ADVERTISED_GAMES];
short game_count, game_chosen;
@@ -2104,6 +2111,9 @@ static struct
/* Server Setup's LISTING: PRIVATE (each new game starts with
network.host_public's choice: PUBLIC unless set otherwise) */
boolean game_private;
/* Server Setup's PASSWORD (a public internet game's; empty: none), kept
while the game runs, never written down */
char game_password[PASSWORD_LENGTH + 1];
} multiplayer = { 0, 0, 0, { 0 }, 0, { 0 }, 0, 0, 0, 0, { 0 }, NUMBEROF(maximum_players) - 1 };
/* ---- a text field (Direct Link's link, the game's name): the keyboard
@@ -2116,6 +2126,8 @@ static struct
char before[TEXT_FIELD_LENGTH];
short maximum;
void (*done)(char const *text);
/* a password's: shown as stars (text_field_begin_masked) */
boolean masked;
} text_field;
/* when the field was last shown (a field not shown for a while is let go
@@ -2161,6 +2173,7 @@ static void text_field_begin(struct widget_instance *row, char const *text, shor
snprintf(text_field.before, sizeof(text_field.before), "%s", text);
text_field.maximum = (short)MIN(maximum, TEXT_FIELD_LENGTH - 1);
text_field.done = done;
text_field.masked = FALSE;
text_field_shown_time = system_milliseconds();
while (input_get_key(&key))
;
@@ -2171,6 +2184,14 @@ static void text_field_begin(struct widget_instance *row, char const *text, shor
#endif
}
/* a password's field: as text_field_begin, its text shown as stars */
static void text_field_begin_masked(struct widget_instance *row, char const *text, short maximum,
void (*done)(char const *text))
{
text_field_begin(row, text, maximum, done);
text_field.masked = TRUE;
}
static void text_field_end(boolean keep)
{
void (*done)(char const *text) = text_field.done;
@@ -2259,7 +2280,7 @@ static void text_field_show(struct widget_instance *value, char const *text, boo
text = text_field.text;
}
for (index = 0; text[index] && index < TEXT_FIELD_LENGTH; index++)
shown[index] = (wchar_t)(unsigned char)text[index];
shown[index] = editing && text_field.masked ? L'*' : (wchar_t)(unsigned char)text[index];
if (editing && (system_milliseconds() / 500) % 2)
shown[index++] = L'_';
shown[index] = 0;
@@ -2643,6 +2664,8 @@ help_strings, tools/port_settings.py) */
#define COOPERATIVE_EXTRA_ENEMIES_HELP 16
#define COOPERATIVE_ENEMIES_PER_PLAYER_HELP 19
#define COOPERATIVE_ENEMIES_MULTIPLIER_HELP 20
#define COOPERATIVE_PLAYER_COLLISIONS_HELP 21
#define SERVER_PASSWORD_HELP 23
/* co-op's EXTRA ENEMIES' choices (port_settings.COOP_ENEMIES_MODES, in this
order): its amount's row is the choice's */
@@ -2806,6 +2829,18 @@ static void server_settings_update(struct widget_instance *list)
help->parameters.text_box.string_list_index = COOPERATIVE_ENEMIES_MULTIPLIER_HELP;
}
}
/* ... and its PLAYER COLLISIONS, below them: ON or OFF, its help its
choice's */
visible_set(named(list, "op_player_collisions", 0), cooperative);
if (cooperative && (spinner = named(list, "player_collisions_spinner", 0)) != NULL)
{
short choice = (short)PIN(spinner->parameters.list.selected_index, 0, 1);
struct widget_instance *help = list->parameters.list.extended_description;
multiplayer.cooperative_no_player_collisions = choice == 1;
if (help && list->focused_child == named(list, "op_player_collisions", 0))
help->parameters.text_box.string_list_index = (short)(COOPERATIVE_PLAYER_COLLISIONS_HELP + choice);
}
}
/* LISTING (an internet game's): PUBLIC, listed in everyone's server
browser, or PRIVATE, for this game. Its help is its choice's */
@@ -2821,6 +2856,41 @@ static void server_settings_update(struct widget_instance *list)
if (help && list->focused_child == named(list, "op_listing", 0))
help->parameters.text_box.string_list_index = (short)(10 + spinner->parameters.list.selected_index);
}
/* PASSWORD (a PUBLIC internet game's): its stars, NONE if it has none */
row = named(list, "op_password", 0);
visible_set(row, named(list, "op_listing", 0) && named(list, "op_listing", 0)->visible &&
!server_settings_private());
if (row && !row->visible && text_field_editing(row))
text_field_end(FALSE);
if (row && row->visible)
{
struct widget_instance *help = list->parameters.list.extended_description;
short index;
for (index = 0; multiplayer.game_password[index] && index < (short)sizeof(text) - 1; index++)
text[index] = '*';
text[index] = 0;
text_field_show(named(list, "password_value", 0), index ? text : "NONE", text_field_editing(row));
if (help && list->focused_child == row)
help->parameters.text_box.string_list_index = SERVER_PASSWORD_HELP;
}
}
static void server_password_done(char const *text)
{
snprintf(multiplayer.game_password, sizeof(multiplayer.game_password), "%s", text);
}
/* "ss edit server password" (its row's A: begins, or ends) */
static boolean server_password_edit(struct widget_instance *row)
{
if (text_field_editing(row))
{
text_field_end(TRUE);
return TRUE;
}
text_field_begin_masked(row, multiplayer.game_password, PASSWORD_LENGTH, server_password_done);
return TRUE;
}
/* "ss edit server name" (its row's A: begins, or ends) */
@@ -2857,12 +2927,16 @@ static boolean server_start(void)
text_field_end(TRUE);
network_game_server_port_set_settings(multiplayer.game_name,
maximum_players[PIN(*server_settings_maximum_players_index(), 0, NUMBEROF(maximum_players) - 1)]);
/* (its listing's password: a PUBLIC internet game's, else none) */
p2p_set_hosting_password(multiplayer.mode == _multiplayer_mode_host_internet && !server_settings_private() ?
multiplayer.game_password : NULL);
/* the gametype as Server Setup's options left it (co-op keeps its own,
and its FRIENDLY FIRE) */
if (hosting_cooperative())
{
gametype_setup_end();
network_game_server_port_set_cooperative_friendly_fire(multiplayer.cooperative_friendly_fire);
network_game_server_port_set_cooperative_player_collisions(!multiplayer.cooperative_no_player_collisions);
}
else if (!gametype_setup_apply())
return campaign_fail();
@@ -3123,6 +3197,11 @@ static struct
/* the last that failed, and when */
char failed_name[P2P_LISTING_NAME_SIZE + 1];
unsigned long failed_time;
/* a game with a password chosen: the password screen's (its own copy:
the rows' are read again each frame), and a join it began, which the
browser takes up again as it comes back */
struct p2p_listing password_game;
boolean password_joined;
} lobby_browser;
static void lobby_browser_begin(void)
@@ -3131,7 +3210,10 @@ static void lobby_browser_begin(void)
lobby_browser.first = 0;
lobby_browser.chosen = 0;
lobby_browser.shown = FALSE;
lobby_browser.joining = lobby_browser.ready = FALSE;
/* (going on with the join the password screen began) */
lobby_browser.joining = lobby_browser.password_joined;
lobby_browser.ready = FALSE;
lobby_browser.password_joined = FALSE;
lobby_browser.begin_time = system_milliseconds();
p2p_lobby_browse(TRUE);
}
@@ -3279,8 +3361,18 @@ static void lobby_browser_update(struct widget_instance *list)
usnprintf(text, ROW_TEXT_LENGTH - 1, L"%d/%d", game->player_count, game->maximum_player_count);
text_set(named(row, "server_item_players", 0), text);
/* (no ping yet: its host is reached only on joining) */
text_set(named(row, "server_item_ping", 0), game->failed ? L"FAILED" : game->in_progress ? L"LIVE" : L"-");
visible_set(named(row, "server_item_locked", 0), !game->open);
text_set(named(row, "server_item_ping", 0), game->failed ? L"FAILED" : !game->open ? L"CLOSED" :
game->in_progress ? L"LIVE" : L"-");
/* (the lock: a game with a password. Its bitmap's first frame is
empty, the PC version's for no lock; the next is the lock, in the
menus' blue) */
{
struct widget_instance *lock = named(row, "server_item_locked", 0);
visible_set(lock, game->locked);
if (lock)
lock->animation.current_frame_index = 1;
}
visible_set(named(row, "server_item_dedicated", 0), FALSE);
visible_set(named(row, "server_item_classic", 0), FALSE);
}
@@ -3355,9 +3447,10 @@ static void lobby_browser_update(struct widget_instance *list)
wchar_t gametype[P2P_LISTING_GAMETYPE_SIZE + 1];
text_to_wide(game->gametype, gametype, NUMBEROF(gametype));
usnprintf(text, NUMBEROF(text) - 1, L"%s: %d %s of %d%s", gametype, game->player_count,
usnprintf(text, NUMBEROF(text) - 1, L"%s: %d %s of %d%s%s", gametype, game->player_count,
game->player_count == 1 ? L"player" : L"players", game->maximum_player_count,
!game->open ? L", full or starting" : game->in_progress ? L", under way" : L"");
!game->open ? L", full or starting" : game->in_progress ? L", under way" : L"",
game->locked ? L", password" : L"");
}
else
text[0] = 0;
@@ -3387,9 +3480,26 @@ static void lobby_browser_update(struct widget_instance *list)
browser_focus(list);
}
/* the game's host reached by its invite (joined, once it is reached:
lobby_browser_update) */
static boolean lobby_browser_join(struct p2p_listing const *game, short controller)
{
if (!p2p_join_invite(game->invite))
return FALSE;
csmemcpy(lobby_browser.identifier, game->identifier, sizeof(lobby_browser.identifier));
csmemcpy(lobby_browser.name, game->name, sizeof(lobby_browser.name));
lobby_browser.joining = TRUE;
lobby_browser.ready = FALSE;
lobby_browser.join_time = system_milliseconds();
lobby_browser.controller = controller;
ui_play_audio_feedback_sound(SOUND_FORWARD);
return TRUE;
}
/* the server browser's rows and buttons: Refresh asks the hosts again; a
row (or Join) joins its game's invite; the A press posted once its host is
reached joins its game (or shows its lobby, if under way) */
row (or Join) joins its game's invite, a game with a password once its
screen has it; the A press posted once its host is reached joins its game
(or shows its lobby, if under way) */
static boolean lobby_browser_select(struct widget_instance *widget, short controller, short row,
boolean *widget_deleted)
{
@@ -3434,15 +3544,117 @@ static boolean lobby_browser_select(struct widget_instance *widget, short contro
if (index >= lobby_browser.count)
return campaign_fail();
game = &lobby_browser.games[index];
if (!game->open || !config_boolean("network.online") || !p2p_join_invite(game->invite))
if (!game->open || !config_boolean("network.online"))
return campaign_fail();
csmemcpy(lobby_browser.identifier, game->identifier, sizeof(lobby_browser.identifier));
csmemcpy(lobby_browser.name, game->name, sizeof(lobby_browser.name));
lobby_browser.joining = TRUE;
lobby_browser.ready = FALSE;
lobby_browser.join_time = system_milliseconds();
lobby_browser.controller = controller;
ui_play_audio_feedback_sound(SOUND_FORWARD);
/* (a game with a password: its screen asks for it, then joins) */
if (game->locked)
{
lobby_browser.password_game = *game;
lobby_browser.controller = controller;
return ui_widget_port_open(widget, PASSWORD_NAME, widget_deleted);
}
return lobby_browser_join(game, controller) || campaign_fail();
}
/* ---- the server browser's password screen (a game with a password
chosen: lobby_browser_select, tools/port_settings.py): its password typed at
once, shown as stars; JOIN GAME, or Enter, joins the game with it if it is
the game's (the browser, back again, goes on with the join), else its help
says it is not and the typing begins again */
/* its help's lines (tools/port_settings.py's) */
enum
{
_password_help_ask = 1,
_password_help_wrong,
_password_help_failed,
};
static struct
{
char text[PASSWORD_LENGTH + 1];
short help;
} password_screen;
static void password_screen_done(char const *text)
{
snprintf(password_screen.text, sizeof(password_screen.text), "%s", text);
}
/* "port password init" */
static boolean password_screen_initialize(struct widget_instance *list)
{
struct widget_instance *row = named(list, "op_password", 0);
password_screen.text[0] = 0;
password_screen.help = _password_help_ask;
if (row)
text_field_begin_masked(row, "", PASSWORD_LENGTH, password_screen_done);
return TRUE;
}
/* "port password update" */
static void password_screen_update(struct widget_instance *list)
{
struct widget_instance *help = list->parameters.list.extended_description;
char stars[PASSWORD_LENGTH + 1];
short index;
for (index = 0; password_screen.text[index] && index < PASSWORD_LENGTH; index++)
stars[index] = '*';
stars[index] = 0;
text_field_show(named(list, "password_value", 0), stars, text_field_editing(named(list, "op_password", 0)));
if (help)
help->parameters.text_box.string_list_index = password_screen.help;
}
/* "port password join" (JOIN GAME, and Enter: password_screen_edit) */
static boolean password_screen_join(struct widget_instance *widget, boolean *widget_deleted)
{
struct p2p_listing game = lobby_browser.password_game;
struct widget_instance *row = named(screen_of(widget), "op_password", 0);
if (text_field_editing(NULL))
text_field_end(TRUE);
if (!p2p_listing_unlock(&game, password_screen.text))
{
password_screen.help = _password_help_wrong;
password_screen.text[0] = 0;
if (row)
text_field_begin_masked(row, "", PASSWORD_LENGTH, password_screen_done);
return campaign_fail();
}
if (!lobby_browser_join(&game, lobby_browser.controller))
{
password_screen.help = _password_help_failed;
return campaign_fail();
}
password_screen.text[0] = 0;
lobby_browser.password_joined = TRUE;
ui_widget_port_go_back(widget);
*widget_deleted = TRUE;
return TRUE;
}
/* "port password back" (B, Escape): back to the browser at once, though the
password is being typed */
static boolean password_screen_back(struct widget_instance *widget, boolean *widget_deleted)
{
if (text_field_editing(NULL))
text_field_end(FALSE);
password_screen.text[0] = 0;
ui_widget_port_go_back(widget);
*widget_deleted = TRUE;
return TRUE;
}
/* "port password edit" (the field's row's A: the typing begins; Enter, its
end, joins) */
static boolean password_screen_edit(struct widget_instance *row, boolean *widget_deleted)
{
if (text_field_editing(row))
return password_screen_join(row, widget_deleted);
text_field_begin_masked(row, password_screen.text, PASSWORD_LENGTH, password_screen_done);
return TRUE;
}
@@ -4950,6 +5162,26 @@ boolean pc_menu_event_function_invoke(
{
return server_name_edit(widget);
}
else if (!strcmp(name, "ss edit server password"))
{
return server_password_edit(widget);
}
else if (!strcmp(name, "port password init"))
{
return password_screen_initialize(widget);
}
else if (!strcmp(name, "port password edit"))
{
return password_screen_edit(widget, widget_deleted);
}
else if (!strcmp(name, "port password join"))
{
return password_screen_join(widget, widget_deleted);
}
else if (!strcmp(name, "port password back"))
{
return password_screen_back(widget, widget_deleted);
}
else if (!strcmp(name, "ss copy invite"))
{
return invite_copy();
@@ -5129,6 +5361,8 @@ void pc_menu_game_data_function_invoke(
gametype_list_update(widget);
else if (!strcmp(name, "server settings update"))
server_settings_update(widget);
else if (!strcmp(name, "port password update"))
password_screen_update(widget);
else if (!strcmp(name, "gamespy screen update"))
browser_update(widget);
else if (!strcmp(name, "gt edit list update"))
+11
View File
@@ -361,6 +361,8 @@ static char const *const port_function_names[] =
/* (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
@@ -378,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
@@ -1246,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;
+111 -10
View File
@@ -29,6 +29,8 @@ that clients need to see is sent from this file:
every tick in rotation, those that have moved since the map loaded more
often. A scenario group is identified by its index, which is the same on
every machine; a device's own group by the device.
- Breakable surfaces (glass) the host breaks, each once as it breaks and
a few at a time again in rotation (for a late joiner or a lost message).
- Named objects (scenery and devices) the scripts create or destroy.
network_objects.c already handles units, vehicles, weapons and equipment.
Twice a second the host sends which object names currently exist. A
@@ -53,7 +55,9 @@ host skips once more than half the machines have voted. The skip itself is
the campaign's: the host reverts to the state saved when the cutscene began
and the script carries on past it. Clients catch up through the object,
device and name syncs. The host keeps its clock running through the revert,
because the netcode needs it to only go forward (network_coop_skip_reverted).
because the netcode needs it to only go forward (network_coop_reverted).
A failed mission (game_lost, or the scripts' game_revert) reverts the same
way, rather than leaving the players stuck in the failure's cutscene.
Objects (scenery and devices) that aren't synced by network_objects.c are
found on a client by their scenario name, or, unnamed, by their object
@@ -88,11 +92,15 @@ index and tag, since the map placed them at the same index everywhere.
#include "models/model_animation_definitions.h"
#include "models/model_definitions.h"
#include "models/models.h"
#include "networking/network_game_globals.h"
#include "networking/network_game_manager.h"
#include "objects/object_definitions.h"
#include "objects/objects.h"
#include "objects/object_types.h"
#include "objects/damage.h"
#include "objects/scenery.h"
#include "physics/breakable_surfaces.h"
#include "structures/structure_bsp_definitions.h"
#include "physics/collisions.h"
#include "rasterizer/rasterizer_cinematics.h"
#include "saved games/game_state.h"
@@ -184,9 +192,13 @@ enum
_coop_event_effect,
_coop_event_attach,
/* the host skipped the cutscene: a client stops its dialogue */
_coop_event_cutscene_skipped,
/* the host reverted to its last saved state */
_coop_event_reverted,
/* a unit opened (value TRUE) or closed: a dropship's doors */
_coop_event_unit_open,
/* a breakable surface broke: value its index, frame its BSP, reals the
damage's epicenter */
_coop_event_surface_broken,
};
/* distributed_coop_event.type of an effect: at a cutscene flag (value), or on
@@ -270,7 +282,11 @@ struct distributed_coop_presentation
/* whether the scripts have set the players' maximum vitality, and to
what (network_coop_set_players_vitality) */
byte players_vitality_set;
byte pad[2];
/* the cluster the scripts keep active (object_pvs_activate) plus one,
0 for none: a cutscene filmed far from every player (b30's Elite at the
shaft door) is active on the clients too. Plus one so the zeroed padding
of a host without it reads as none. */
short activating_cluster;
real players_maximum_body_vitality;
real players_maximum_shield_vitality;
/* which teams are allies and friends (game_allegiance.c): the scripts
@@ -1429,6 +1445,7 @@ static void host_presentation(
presentation->input_disabled = (byte)!player_input_enabled();
presentation->scripted_shake = (byte)player_effect_port_scripted_active();
presentation->players_vitality_set = (byte)players_vitality.set;
presentation->activating_cluster = (short)(objects_get_activating_cluster_index() + 1);
presentation->players_maximum_body_vitality = players_vitality.maximum_body;
presentation->players_maximum_shield_vitality = players_vitality.maximum_shield;
game_allegiance_get_teams(presentation->ally_teams, presentation->friendly_teams);
@@ -1544,7 +1561,10 @@ static void client_apply_player_effect(
switch (event->type)
{
case _coop_player_effect_translation:
scripted_player_effect_set_translation(reals[0], reals[1], reals[2]);
/* (the camera's shake, which moves it no further than an observer
accepts) */
scripted_player_effect_set_translation(PIN(reals[0], -CAMERA_WORLD_BOUND, CAMERA_WORLD_BOUND),
PIN(reals[1], -CAMERA_WORLD_BOUND, CAMERA_WORLD_BOUND), PIN(reals[2], -CAMERA_WORLD_BOUND, CAMERA_WORLD_BOUND));
break;
case _coop_player_effect_rotation:
scripted_player_effect_set_rotation(reals[0], reals[1], reals[2]);
@@ -1802,9 +1822,17 @@ static void client_apply_event(
case _coop_event_attach:
client_apply_attach(event);
break;
case _coop_event_cutscene_skipped:
case _coop_event_reverted:
client_stop_script_sounds();
break;
case _coop_event_surface_broken:
if (event->frame == global_structure_bsp_index)
{
real_point3d epicenter = { event->reals[0], event->reals[1], event->reals[2] };
breakable_surface_port_break(event->value, &epicenter);
}
break;
case _coop_event_unit_open:
if (distributed_object_index_valid(event->object_index) && network_objects_client_has(event->object_index) &&
object_try_and_get_and_verify_type(event->object_index, _object_mask_unit))
@@ -1935,6 +1963,16 @@ boolean network_coop_active(
global_scenario && global_scenario->type == _scenario_type_solo && !game_engine_running();
}
boolean network_coop_player_collisions(
void)
{
struct network_game *game;
if (!network_coop_active() || !(game = network_game_get_game()))
return TRUE;
return !TEST_FLAG(game->cooperative_flags, _network_game_cooperative_no_player_collisions_bit);
}
boolean network_coop_devices_remote(
void)
{
@@ -2430,6 +2468,51 @@ void network_coop_note_effect(
event->value = cutscene_flag_index;
}
void network_coop_note_surface_broken(
short breakable_surface_index,
real_point3d const *epicenter)
{
struct distributed_coop_event *event = event_new(_coop_event_surface_broken);
if (!event)
return;
event->value = breakable_surface_index;
event->frame = global_structure_bsp_index;
event->reals[0] = epicenter->x;
event->reals[1] = epicenter->y;
event->reals[2] = epicenter->z;
}
/* host: a few broken surfaces again each refresh, round them all, for a
machine that joined since or lost the message (broken from their middle) */
static void host_send_broken_surfaces(
void)
{
enum { SURFACES_PER_REFRESH = 8 };
static short cursor;
struct structure_bsp *structure_bsp = global_structure_bsp_get();
short count = (short)structure_bsp->breakable_surfaces.count;
short sent = 0;
short step;
if (!host_resend.refresh || count <= 0)
return;
for (step = 0; step < count && sent < SURFACES_PER_REFRESH; step++)
{
short index = (short)((cursor + step) % count);
if (!breakable_surface_extant(index))
{
struct structure_breakable_surface *surface = TAG_BLOCK_GET_ELEMENT(&structure_bsp->breakable_surfaces,
index, struct structure_breakable_surface);
network_coop_note_surface_broken(index, &surface->centroid);
sent++;
}
}
cursor = (short)((cursor + step) % count);
}
void network_coop_note_object_effect(
long effect_definition_index,
long object_index,
@@ -2465,7 +2548,7 @@ boolean network_coop_vote_skip(
return TRUE;
}
/* The skip reverted the host's game state, clock included. The clock is
/* The host reverted its game state, clock included. The clock is
put back, since clients and the netcode expect it to only go forward, and
the script threads' wake times move by the same amount so they still wake
when they would have. The revert also renumbered the input queues from the
@@ -2474,7 +2557,7 @@ from the restored one, or no tick would run until they caught up. And the
revert is stamped with the restored time, since the script's game_reverted
compares the stamp with the clock: otherwise it would play the cinematic
it was meant to skip. */
void network_coop_skip_reverted(
void network_coop_reverted(
long now)
{
long ticks;
@@ -2491,8 +2574,15 @@ void network_coop_skip_reverted(
}
/* (the dropships' riders kept are of the game state reverted from) */
coop_enemies_reset();
error(_error_silent, "co-op: cutscene skipped; reverted %ld ticks, clock kept at %ld", ticks, now);
event_new(_coop_event_cutscene_skipped);
error(_error_silent, "co-op: reverted %ld ticks, clock kept at %ld", ticks, now);
event_new(_coop_event_reverted);
}
void network_coop_skip_done(
void)
{
if (!coop_host())
return;
skip_vote_clear();
skip_vote.requested = FALSE;
skip_vote.cooldown_until = game_time_get() + SKIP_COOLDOWN_TICKS;
@@ -2537,6 +2627,7 @@ void network_coop_host_tick(
host_send_attachments();
host_send_looping_sounds();
host_send_hud_state();
host_send_broken_surfaces();
host_send_events();
}
@@ -2598,7 +2689,16 @@ void network_coop_note_player_structure_bsp(
boolean network_coop_player_has_structure_bsp(
long player_index)
{
return !network_coop_active() || player_get(player_index)->local_player_index != NONE ||
struct player_datum *player;
if (!network_coop_active())
return TRUE;
/* (no player, or none of the tracked: whose BSP is not known, which
does not keep their predictions out) */
player = player_index != NONE ? player_try_and_get(player_index) : NULL;
if (!player || DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) >= MAXIMUM_TRACKED_PLAYERS)
return TRUE;
return player->local_player_index != NONE ||
host_player_structure_bsps[DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index)] == global_structure_bsp_index_get();
}
@@ -2904,6 +3004,7 @@ static void client_presentation_apply(
!coop_spectate_watching_rider(0)) || coop_spectate_nothing_to_watch(0));
if (coop_presentation.cinematic_started)
cinematic_show_letterbox(TEST_FLAG(presentation->flags, _presentation_letterbox_bit));
objects_port_set_activating_cluster((short)(presentation->activating_cluster - 1));
if (coop_presentation.host_camera)
{
real_vector3d forward, up;
+13 -3
View File
@@ -45,6 +45,9 @@ enum
/* 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 */
@@ -88,6 +91,10 @@ void network_coop_note_attach(long parent_index, char const *parent_marker_name,
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);
@@ -96,9 +103,12 @@ 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 skip a cinematic; now is the game
time before the revert */
void network_coop_skip_reverted(long now);
/* 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);
+74 -13
View File
@@ -34,6 +34,10 @@ 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"
@@ -59,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 */
@@ -142,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
@@ -467,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) &&
@@ -478,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:
@@ -629,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++];
@@ -717,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,
@@ -736,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)
@@ -749,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)
{
@@ -1719,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 */
@@ -2160,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;
}
}
}
+54 -6
View File
@@ -95,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);
@@ -588,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];
@@ -602,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 */
@@ -1622,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)
{
@@ -2089,6 +2104,7 @@ static void distributed_apply_predictions(
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);
@@ -3709,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;
@@ -3730,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)
{
@@ -3746,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));
@@ -3756,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;
@@ -4101,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;
}
+22 -2
View File
@@ -60,6 +60,7 @@ 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"
@@ -244,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;
@@ -1003,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;
@@ -1292,8 +1298,20 @@ static short distributed_host_object_period(
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)
if (!count && nearest < 0.0f)
return MAXIMUM_OBJECT_PERIOD_TICKS;
for (index = 0; index < count; index++)
{
@@ -2142,6 +2160,8 @@ 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) */
{
+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
@@ -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"
+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 16
#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
+65 -3
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 */
@@ -107,16 +110,46 @@ where the controller aims */
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 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 left hand's pose;
- the left 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 aims along both hands; */
int halo_vr_body_frame(short local_player_index, float head[3], float forward[3]);
int halo_vr_body_frame(short local_player_index, float head[3], float neck[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_two_hand(short local_player_index, const float foregrip[3], const float reference[3]);
/* - the fingers of a hand (0 the left, 1 the right), 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;
@@ -247,6 +280,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);
+814 -84
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"));
+228 -26
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))
@@ -267,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");
+9 -2
View File
@@ -2421,7 +2421,13 @@ int p2p_public_games(struct p2p_public_game *games, int maximum)
listing_count = p2p_lobby_games(listings, 32);
for (index = 0; index < listing_count && count < maximum; index++)
{
struct p2p_public_game *game = &games[count++];
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);
@@ -2533,7 +2539,8 @@ void p2p_new_invite_if_listed(void)
if (!p2p.has_token || !p2p.token_listed)
return;
make_invite();
platform_log("Internet play: the game is private now, with a new invite: %s", p2p.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);
+20
View File
@@ -149,6 +149,12 @@ in everyone's server browser: Create Game's PUBLIC; a private game is
reached only by its invite. Going private makes a new invite, if the old one
was listed */
void p2p_set_hosting_public(int public);
/* the password of the game hosted for the internet (Server Setup's
PASSWORD; NULL or empty: none): its listing lets only those who know it
join from the server browser (its invite link still joins it). Setting or
changing it makes a new invite, if the old one was listed. It takes a few
milliseconds (the password's key) */
void p2p_set_hosting_password(const char *password);
/* the hosted game's details as listed (printable ASCII is kept; NULL leaves
one as it was): the game's server calls it as they change (calling it with
the same again costs little) */
@@ -167,10 +173,16 @@ enum
/* an invite link's text (P2P_LINK_SIZE) */
P2P_LISTING_INVITE_SIZE = 77,
/* (struct p2p_public_game's invite is as large) */
/* a password's game's: its host's key and its key's hash, and its token
sealed (P2P_KEY_SIZE, P2P_KEY_HASH_SIZE, P2P_SEALED_TOKEN_SIZE) */
P2P_LISTING_SIGNING_KEY_SIZE = 32,
P2P_LISTING_KEY_HASH_SIZE = 16,
P2P_LISTING_SEALED_TOKEN_SIZE = 56,
};
struct p2p_listing
{
/* (empty for a locked one until p2p_listing_unlock opens it) */
char invite[P2P_LISTING_INVITE_SIZE];
/* the host's (its XNADDR's abEnet once reached) */
unsigned char identifier[6];
@@ -183,12 +195,20 @@ struct p2p_listing
unsigned char failed;
/* milliseconds, -1 if not known */
short ping;
/* it has a password: its invite is sealed with it (p2p_listing_unlock) */
unsigned char locked;
unsigned char key_hash[P2P_LISTING_KEY_HASH_SIZE];
unsigned char signing_key[P2P_LISTING_SIGNING_KEY_SIZE];
unsigned char sealed_token[P2P_LISTING_SEALED_TOKEN_SIZE];
};
/* a copy of the public games found, in the order shown (the most players
first; then those not failed, the open ones, by name); returns their count */
int p2p_lobby_games(struct p2p_listing *games, int maximum_count);
/* a game the browser could not join: kept, marked failed, for this run */
void p2p_lobby_mark_failed(const unsigned char *identifier);
/* a locked game's invite (into its invite), if the password is its own:
FALSE if not; TRUE for a game with none. It takes a few milliseconds */
int p2p_listing_unlock(struct p2p_listing *listing, const char *password);
/* the sizes of a Discord user's id and name as kept (with their end), and
the text kept of either as told: only the characters allowed (digits in an
+45
View File
@@ -20,6 +20,7 @@ tunnel's packets are sealed with keys only its two machines have.
#include "monocypher.h"
#include "monocypher-ed25519.h"
#include <stdlib.h>
#include <string.h>
/* ---------- SHA-256 */
@@ -649,6 +650,50 @@ int p2p_ed25519_to_x25519(const unsigned char *public_key, unsigned char *x25519
return !p2p_equal(product, zero, P2P_KEY_SIZE);
}
/* ---------- passwords (a password-protected public game's listing holds
its invite's token sealed with the password's key: p2p_lobby.c) */
/* (Argon2id of 4 MiB and three passes: tens of milliseconds, once for the
host and once a guess for a joiner) */
#define PASSWORD_KEY_BLOCKS 4096
#define PASSWORD_KEY_PASSES 3
void p2p_password_key(const char *password, const unsigned char *salt, unsigned char *key)
{
crypto_argon2_config config = { CRYPTO_ARGON2_ID, PASSWORD_KEY_BLOCKS, PASSWORD_KEY_PASSES, 1 };
crypto_argon2_inputs inputs;
void *work_area = malloc((size_t)PASSWORD_KEY_BLOCKS * 1024);
inputs.pass = (const unsigned char *)password;
inputs.pass_size = (unsigned int)strlen(password);
inputs.salt = salt;
inputs.salt_size = P2P_KEY_SIZE;
if (!work_area)
{
/* (no key anyone could guess: nothing opens with it) */
posix_random_bytes(key, P2P_PASSWORD_KEY_SIZE);
return;
}
crypto_argon2(key, P2P_PASSWORD_KEY_SIZE, work_area, config, inputs, crypto_argon2_no_extras);
crypto_wipe(work_area, (size_t)PASSWORD_KEY_BLOCKS * 1024);
free(work_area);
}
void p2p_seal_token(const unsigned char *key, const unsigned char *signing_key, const unsigned char *token,
unsigned char *sealed)
{
/* (a nonce of its own each time: 24 random bytes never repeat) */
posix_random_bytes(sealed, 24);
crypto_aead_lock(sealed + 24 + 16, sealed + 24, key, sealed, signing_key, P2P_KEY_SIZE, token, P2P_TOKEN_SIZE);
}
int p2p_unseal_token(const unsigned char *key, const unsigned char *signing_key, const unsigned char *sealed,
unsigned char *token)
{
return crypto_aead_unlock(token, sealed + 24, key, sealed, signing_key, P2P_KEY_SIZE, sealed + 24 + 16,
P2P_TOKEN_SIZE) == 0;
}
int p2p_ed25519_verify(const unsigned char *public_key, const void *message, int size,
const unsigned char *signature)
{
+20
View File
@@ -246,6 +246,26 @@ int p2p_ed25519_verify(const unsigned char *public_key, const void *message, int
/* the X25519 public key of an Ed25519 one; 0 if it has a small order */
int p2p_ed25519_to_x25519(const unsigned char *public_key, unsigned char *x25519_public);
enum
{
/* a password-protected listing's token, sealed (p2p_seal_token): its
nonce (24), its tag (16), then the token sealed */
P2P_PASSWORD_KEY_SIZE = 32,
P2P_SEALED_TOKEN_SIZE = 24 + 16 + 16,
};
/* the key of a password (Argon2id: P2P_PASSWORD_KEY_SIZE bytes), for the
host whose Ed25519 key salt is (P2P_KEY_SIZE bytes): it takes a few
milliseconds, so that guessing passwords at a listing takes long */
void p2p_password_key(const char *password, const unsigned char *salt, unsigned char *key);
/* a token (P2P_TOKEN_SIZE bytes) sealed with a password's key, bound to the
host's Ed25519 key (P2P_SEALED_TOKEN_SIZE bytes); and opened: 0 if the key
is not the one it was sealed with (a wrong password), or it was altered */
void p2p_seal_token(const unsigned char *key, const unsigned char *signing_key, const unsigned char *token,
unsigned char *sealed);
int p2p_unseal_token(const unsigned char *key, const unsigned char *signing_key, const unsigned char *sealed,
unsigned char *token);
/* ---------- p2p_lobby.c: public games' listings */
/* the p2p thread's pass: the token of the game hosted for the internet
+119 -13
View File
@@ -12,9 +12,11 @@ the host can list its invite, alter its listing, or list another's under
false details:
"HL", format 1, the lobby version (2: HALO_PORT_NETWORK_VERSION; browsers
hide others), flags (open, under way, teams, closed), sequence (4: newest
wins), Unix time (4), the Ed25519 key (32), the invite's token (16),
players, most players, the gametype's engine (1 each), the game's name,
hide others), flags (open, under way, teams, closed, password), sequence
(4: newest wins), Unix time (4), the Ed25519 key (32), the invite's token
(16; a password's game's sealed with the password's key, 56:
p2p_seal_token; a tombstone's zero), players, most players, the
gametype's engine (1 each), the game's name,
map and gametype (a length byte, then up to 32, 32 and 24 characters of
printable ASCII), a stamp (8, reserved: zero), and the signature (64) of
"hceu-lobby-1" and all before it.
@@ -38,6 +40,14 @@ one deletes or keeps is no authority, so
A host that stops publishes a tombstone, then clears its slot. Going private
makes a new invite (p2p.c), so a listing seen before lets no one in.
A game with a password (Server Setup's PASSWORD: p2p_set_hosting_password)
is listed with its token sealed with the password's key (Argon2id of the
password, salted with the host's key: p2p_password_key), so that only who
knows the password can join it from the server browser
(p2p_listing_unlock); its invite link, which holds the token, still joins it
as any invite does. Setting or changing the password makes a new invite, as
going private does.
Checking signatures takes work, and anyone can send listings: at most
VERIFY_BUDGET milliseconds of it each pass of the p2p thread, from a queue
of MAXIMUM_QUEUED; the rest is dropped (hosts publish again).
@@ -62,11 +72,11 @@ enum
_listing_has_teams = 4,
/* a tombstone: the host stopped */
_listing_closed = 8,
/* (reserved) */
/* its token sealed with a password's key */
_listing_password = 16,
STAMP_SIZE = 8,
/* the signed part's end: everything but the signature */
MAXIMUM_LISTING_SIZE = 2 + 1 + 2 + 1 + 4 + 4 + P2P_KEY_SIZE + P2P_TOKEN_SIZE + 3 +
MAXIMUM_LISTING_SIZE = 2 + 1 + 2 + 1 + 4 + 4 + P2P_KEY_SIZE + P2P_SEALED_TOKEN_SIZE + 3 +
(1 + P2P_LISTING_NAME_SIZE) + (1 + P2P_LISTING_MAP_SIZE) + (1 + P2P_LISTING_GAMETYPE_SIZE) + STAMP_SIZE +
P2P_SIGNATURE_SIZE,
MINIMUM_LISTING_SIZE = 2 + 1 + 2 + 1 + 4 + 4 + P2P_KEY_SIZE + P2P_TOKEN_SIZE + 3 + 3 + STAMP_SIZE +
@@ -88,6 +98,10 @@ enum
static const char signature_label[] = "hceu-lobby-1";
/* (p2p.h's sizes of a locked listing's are p2p_internal.h's) */
typedef char check_listing_sizes[P2P_LISTING_SIGNING_KEY_SIZE == P2P_KEY_SIZE &&
P2P_LISTING_KEY_HASH_SIZE == P2P_KEY_HASH_SIZE && P2P_LISTING_SEALED_TOKEN_SIZE == P2P_SEALED_TOKEN_SIZE ? 1 : -1];
/* a listing, read */
struct listing
{
@@ -97,6 +111,8 @@ struct listing
unsigned long time;
unsigned char key[P2P_KEY_SIZE];
unsigned char token[P2P_TOKEN_SIZE];
/* (_listing_password: its token, sealed) */
unsigned char sealed_token[P2P_SEALED_TOKEN_SIZE];
int player_count, maximum_player_count, engine_type;
char name[P2P_LISTING_NAME_SIZE + 1];
char map[P2P_LISTING_MAP_SIZE + 1];
@@ -143,6 +159,9 @@ static struct
int engine_type;
int flags;
int player_count, maximum_player_count;
/* the password's key (p2p_set_hosting_password), if it has one */
int has_password;
unsigned char password_key[P2P_PASSWORD_KEY_SIZE];
/* listed: with this token; its listing as last published */
int listed;
unsigned char token[P2P_TOKEN_SIZE];
@@ -240,6 +259,8 @@ static int listing_make(unsigned char *bytes, int flags)
unsigned char data[sizeof(signature_label) - 1 + MAXIMUM_LISTING_SIZE];
int size = 0;
if (lobby.has_password && !(flags & _listing_closed))
flags |= _listing_password;
bytes[size++] = 'H';
bytes[size++] = 'L';
bytes[size++] = LISTING_FORMAT;
@@ -252,8 +273,20 @@ static int listing_make(unsigned char *bytes, int flags)
size += 4;
memcpy(bytes + size, p2p_signing_key(), P2P_KEY_SIZE);
size += P2P_KEY_SIZE;
memcpy(bytes + size, lobby.token, P2P_TOKEN_SIZE);
size += P2P_TOKEN_SIZE;
if (flags & _listing_password)
{
p2p_seal_token(lobby.password_key, p2p_signing_key(), lobby.token, bytes + size);
size += P2P_SEALED_TOKEN_SIZE;
}
else
{
/* (a tombstone's none: it may end a game with a password) */
if (flags & _listing_closed)
memset(bytes + size, 0, P2P_TOKEN_SIZE);
else
memcpy(bytes + size, lobby.token, P2P_TOKEN_SIZE);
size += P2P_TOKEN_SIZE;
}
bytes[size++] = (unsigned char)(lobby.player_count > 255 ? 255 : lobby.player_count);
bytes[size++] = (unsigned char)(lobby.maximum_player_count > 255 ? 255 : lobby.maximum_player_count);
bytes[size++] = (unsigned char)lobby.engine_type;
@@ -302,8 +335,20 @@ static int listing_read(const unsigned char *bytes, int size, struct listing *li
offset += 4;
memcpy(listing->key, bytes + offset, P2P_KEY_SIZE);
offset += P2P_KEY_SIZE;
memcpy(listing->token, bytes + offset, P2P_TOKEN_SIZE);
offset += P2P_TOKEN_SIZE;
if (listing->flags & _listing_password)
{
/* (its token sealed, which is longer: the counts after it too) */
if (offset + P2P_SEALED_TOKEN_SIZE + 3 > size)
return 0;
memset(listing->token, 0, P2P_TOKEN_SIZE);
memcpy(listing->sealed_token, bytes + offset, P2P_SEALED_TOKEN_SIZE);
offset += P2P_SEALED_TOKEN_SIZE;
}
else
{
memcpy(listing->token, bytes + offset, P2P_TOKEN_SIZE);
offset += P2P_TOKEN_SIZE;
}
listing->player_count = bytes[offset++];
listing->maximum_player_count = bytes[offset++];
listing->engine_type = bytes[offset++];
@@ -569,10 +614,22 @@ static void listing_take(const struct queued *queued, const struct listing *list
memcpy(game->payload, queued->payload, (size_t)queued->size);
game->payload_size = queued->size;
shown = &game->listing;
memcpy(bytes, queued->key_hash, P2P_KEY_HASH_SIZE);
memcpy(bytes + P2P_KEY_HASH_SIZE, listing->token, P2P_TOKEN_SIZE);
p2p_hex(bytes, sizeof(bytes), text);
snprintf(shown->invite, sizeof(shown->invite), "halo://join/%s", text);
shown->locked = (listing->flags & _listing_password) != 0;
if (shown->locked)
{
/* (its invite once the password opens its token: p2p_listing_unlock) */
shown->invite[0] = 0;
memcpy(shown->key_hash, queued->key_hash, P2P_KEY_HASH_SIZE);
memcpy(shown->signing_key, listing->key, P2P_KEY_SIZE);
memcpy(shown->sealed_token, listing->sealed_token, P2P_SEALED_TOKEN_SIZE);
}
else
{
memcpy(bytes, queued->key_hash, P2P_KEY_HASH_SIZE);
memcpy(bytes + P2P_KEY_HASH_SIZE, listing->token, P2P_TOKEN_SIZE);
p2p_hex(bytes, sizeof(bytes), text);
snprintf(shown->invite, sizeof(shown->invite), "halo://join/%s", text);
}
p2p_identifier_from_hash(queued->key_hash, shown->identifier);
memcpy(shown->name, listing->name, sizeof(shown->name));
memcpy(shown->map, listing->map, sizeof(shown->map));
@@ -669,6 +726,33 @@ void p2p_set_hosting_public(int public)
pthread_mutex_unlock(&p2p_lock);
}
void p2p_set_hosting_password(const char *password)
{
unsigned char key[P2P_PASSWORD_KEY_SIZE];
int has_password = password && *password;
/* (the key takes a while: not under the lock) */
if (has_password)
p2p_password_key(password, p2p_signing_key(), key);
pthread_mutex_lock(&p2p_lock);
if (has_password != lobby.has_password ||
(has_password && !p2p_equal(key, lobby.password_key, P2P_PASSWORD_KEY_SIZE)))
{
/* (a new invite, if one was listed: who saw it, with no password or
another, cannot join with it) */
p2p_new_invite_if_listed();
lobby.has_password = has_password;
if (has_password)
memcpy(lobby.password_key, key, sizeof(key));
else
memset(lobby.password_key, 0, sizeof(lobby.password_key));
if (lobby.listed)
republish_soon(0);
}
pthread_mutex_unlock(&p2p_lock);
memset(key, 0, sizeof(key));
}
void p2p_set_game_listing(const char *name, const char *map, const char *gametype, int engine_type, int open,
int in_progress, int has_teams)
{
@@ -765,6 +849,28 @@ int p2p_lobby_games(struct p2p_listing *games, int maximum_count)
return count;
}
int p2p_listing_unlock(struct p2p_listing *listing, const char *password)
{
unsigned char key[P2P_PASSWORD_KEY_SIZE];
unsigned char bytes[P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE];
char text[2 * (P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE) + 1];
int opened;
if (!listing->locked)
return 1;
p2p_password_key(password ? password : "", listing->signing_key, key);
memcpy(bytes, listing->key_hash, P2P_KEY_HASH_SIZE);
opened = p2p_unseal_token(key, listing->signing_key, listing->sealed_token, bytes + P2P_KEY_HASH_SIZE);
if (opened)
{
p2p_hex(bytes, sizeof(bytes), text);
snprintf(listing->invite, sizeof(listing->invite), "halo://join/%s", text);
}
memset(key, 0, sizeof(key));
memset(bytes, 0, sizeof(bytes));
return opened;
}
void p2p_lobby_mark_failed(const unsigned char *identifier)
{
pthread_mutex_lock(&p2p_lock);
+3 -2
View File
@@ -104,8 +104,9 @@ enum
PROOF_SIZE = NONCE_SIZE + TAG_SIZE,
BUFFER_SIZE = 4096,
MAXIMUM_MESSAGE_SIZE = 512,
/* a listing's most (p2p_lobby.c's MAXIMUM_LISTING_SIZE, and some) */
MAXIMUM_LISTING_SIZE = 256,
/* a listing's most (p2p_lobby.c's MAXIMUM_LISTING_SIZE, 268 with a
password's sealed token, and some) */
MAXIMUM_LISTING_SIZE = 320,
/* the publishes awaiting acknowledgement on a broker */
MAXIMUM_IN_FLIGHT = 4,
/* the topics a broker is subscribed to (_topic_*) */
+49
View File
@@ -96,6 +96,12 @@ static const struct config_setting config_settings[] =
{ "display.max_fps", _config_integer, "0", "HALO_MAX_FPS", _environment_value, _platform_desktop,
"With vsync off, the most frames a second: 0 for twice the display's\n"
"refresh rate, -1 for no limit (which can hang some Intel graphics)." },
{ "display.anti_aliasing", _config_string, "\"off\"", "HALO_ANTI_ALIASING", _environment_value, _platform_all,
"Smoothing of jagged edges, which the Xbox did not have: \"off\"; \"fxaa\"\n"
"or \"smaa\" smooth the 3D view once it is drawn (the HUD and menus stay\n"
"sharp); \"ssaa2x\" draws at twice the resolution each way (four times\n"
"the work); \"msaa2x\", \"msaa4x\" or \"msaa8x\" draw with that many samples\n"
"a pixel. Android has \"fxaa\" for \"smaa\", and no \"ssaa2x\"." },
{ "display.interpolation", _config_boolean, "true", "HALO_INTERPOLATION", _environment_value, _platform_all,
"Draw a frame for every display refresh, blending between the game's 30\n"
"ticks a second; false keeps the original 30 frames a second." },
@@ -111,6 +117,10 @@ static const struct config_setting config_settings[] =
"Draw the menus' and HUD's text with the fonts in port/assets/fonts\n"
"(Overpass) at the resolution the game draws at, and the menus' titles\n"
"from port/assets/titles; false draws the maps' bitmap fonts and titles." },
{ "display.shadow_resolution", _config_integer, "128", "HALO_SHADOW_RESOLUTION", _environment_value,
_platform_all,
"The size the objects' shadows are drawn at, in pixels each way: 128 as\n"
"on the Xbox, or 256, 512 or 1024 for smoother edges, as soft." },
{ "display.menus", _config_string, "\"pc\"", "HALO_MENUS", _environment_value, _platform_all,
"The menus: \"pc\" for the PC version's main menu (port/assets/menus,\n"
"and a menus folder here for your own), \"xbox\" for the Xbox's." },
@@ -135,6 +145,12 @@ static const struct config_setting config_settings[] =
_environment_value, _platform_all,
"The scoreboard panel's colour: \"red, green, blue, alpha\", each 0 to 255\n"
"(alpha 0 is see-through, 255 solid)." },
{ "display.per_pixel_lighting", _config_boolean, "false", "HALO_PER_PIXEL_LIGHTING", _environment_value,
_platform_all,
"Light the models (characters, weapons, vehicles, scenery) for each\n"
"pixel by the lights the game gives them, without the facets the light\n"
"of each vertex shows across curved surfaces; false lights each vertex,\n"
"as the Xbox does." },
{ "vr.enabled", _config_boolean, "true", "HALO_VR_ENABLED", _environment_value, _platform_android,
"On a VR headset (Quest, Steam Frame), play in VR; false shows the game\n"
@@ -182,6 +198,18 @@ static const struct config_setting config_settings[] =
{ "vr.two_hand_reach", _config_real, "0.2", "HALO_VR_TWO_HAND_REACH", _environment_value, _platform_android,
"How near, in metres, the left hand must be to the weapon's foregrip for\n"
"its grip button to take hold of it with both hands." },
{ "vr.body", _config_string, "\"arms\"", "HALO_VR_BODY", _environment_value, _platform_android,
"What of your body is drawn with the weapon: \"arms\", the arms and hands\n"
"reaching from the shoulders to the controllers; \"hands\", the hands\n"
"alone; \"off\", neither (the weapon alone)." },
{ "vr.fingers", _config_boolean, "true", "HALO_VR_FINGERS", _environment_value, _platform_android,
"The free hand's fingers follow the controller: the index finger the\n"
"trigger, the others the grip, the thumb where it rests (on the Quest's\n"
"and the Steam Frame's controllers); false leaves the hand as the\n"
"weapon's animation has it." },
{ "vr.arm_ik", _config_boolean, "true", "HALO_VR_ARM_IK", _environment_value, _platform_android,
"The free hand's arm bends at the elbow to reach it from the shoulder;\n"
"false keeps the arm's shape as on the weapon, pointed at the shoulder." },
{ "vr.melee_swing_speed", _config_real, "2.0", "HALO_VR_MELEE_SWING", _environment_value, _platform_android,
"Thrusting a hand forward faster than this, in metres per second, is a\n"
"melee attack (the right grip is too); 0 turns it off." },
@@ -301,6 +329,10 @@ static const struct config_setting config_settings[] =
{ "audio.effects_volume", _config_real, "1.0", "HALO_EFFECTS_VOLUME", _environment_value, _platform_all,
"The volume of every other sound (effects and speech), 0.0 to 1.0 (of\n"
"audio.volume)." },
{ "audio.reverb", _config_boolean, "true", "HALO_REVERB", _environment_value, _platform_all,
"Reverberate the world's sounds as the place the player is in does (the\n"
"maps' sound environments, as the Xbox's I3DL2 reverb did); false keeps\n"
"them dry." },
{ "input.mouse_sensitivity", _config_real, "1.0", "HALO_MOUSE_SENSITIVITY", _environment_value, _platform_desktop,
"How far the view turns for the mouse's movement." },
@@ -425,6 +457,12 @@ static const struct config_setting config_settings[] =
"\"on\", \"shields_only\" or \"explosives_only\" (Server Setup's FRIENDLY\n"
"FIRE in co-op, which writes its choice here). Their AI allies they\n"
"always can, as in the campaign." },
{ "network.coop_player_collisions", _config_boolean, "true", "HALO_NET_COOP_PLAYER_COLLISIONS", _environment_value,
_platform_all,
"Whether the players of an online co-op game bump into each other;\n"
"false, they walk through each other (the AI's characters they still\n"
"bump into). Server Setup's PLAYER COLLISIONS in co-op writes its\n"
"choice here." },
{ "network.coop_enemies_mode", _config_string, "\"per_player\"", "HALO_NET_COOP_ENEMIES_MODE", _environment_value,
_platform_all,
"Online co-op's extra enemies: \"none\", \"per_player\" (each squad of\n"
@@ -505,6 +543,17 @@ static const struct config_setting config_settings[] =
"machines of twice it), to test the netcode as over the internet; 0 none." },
{ "debug.network_loss", _config_real, "0.0", "HALO_NETWORK_LOSS", _environment_value, _platform_all,
"Percent of datagrams received that are dropped, for the same; 0 none." },
{ "debug.multiview_steamvr", _config_boolean, "false", "HALO_MULTIVIEW_STEAMVR", _environment_value,
_platform_android,
"On SteamVR (the Steam Frame) the eyes are drawn one by one, whatever\n"
"vr.multiview says: true draws them at once there too, for testing." },
{ "debug.gl_thread", _config_integer, "-1", "HALO_GL_THREAD", _environment_value, _platform_android,
"Mesa's GL thread (its drivers: the Steam Frame's): 1 on, 0 off, -1 as the\n"
"driver chooses. Read when the app starts." },
{ "debug.xr_deferred_copies", _config_integer, "-1", "HALO_XR_DEFERRED", _environment_value, _platform_android,
"The headset's layer copies: 1 deferred (no waits for the GL thread or the\n"
"GPU in the middle of the frame), 0 each one restored and released at once,\n"
"-1 as the runtime needs (deferred on SteamVR). Read when the app starts." },
{ "debug.test_input", _config_string, "\"\"", "HALO_TEST_INPUT", _environment_value, _platform_all,
"\"bot:<seed>\" plays controller 1 with a scripted pattern (automated\n"
"network tests); \"look:<seed>\" stands still, only turning and looking\n"
+343 -29
View File
@@ -69,6 +69,17 @@ static struct
unsigned long long wait_us;
int report_frames;
int report_stereo_frames;
/* (the frame's parts, halo_vr_phase: when each ended, and the game's
ticks in it; summed apart for frames with ticks and without, with how
many took longer than the headset's frame) */
unsigned long long phase_end_us[HALO_VR_PHASE_COUNT];
int frame_ticks;
unsigned long long phase_us[2][HALO_VR_PHASE_COUNT + 1];
int phase_frames[2];
int phase_late[2];
/* (parts of the drawing, halo_vr_timing_end: their time and count) */
unsigned long long timing_us[HALO_VR_TIMING_COUNT];
int timing_count[HALO_VR_TIMING_COUNT];
/* the view's turning, gathered for halo_vr_look_delta */
float look_yaw;
@@ -220,6 +231,20 @@ static struct
float lean_limit;
float crouch_limit;
float rise_limit;
/* the fingers (fingers_update, vr.fingers): each hand's thumb, index,
middle and the last two, curled 0 (open) to 1, and how fast */
int fingers;
int arm_ik; /* vr.arm_ik: the free arm bends at the elbow */
int body_mode; /* vr.body: HALO_VR_BODY_* */
float finger_curl[HALO_XR_HAND_COUNT][4];
float finger_velocity[HALO_XR_HAND_COUNT][4];
unsigned long long fingers_last_us;
/* the torso's heading (torso_update): in LOCAL space, so that turning
the body by the stick turns it too; and the head's, level */
float torso_yaw;
int torso_valid;
unsigned long long torso_last_us;
float head_level_yaw;
} vr;
/* d3d8_gl.c */
@@ -228,9 +253,20 @@ int d3d8_multiview_supported(void);
int d3d8_multiview_begin(const float rows[4][4], const float resolve_rect[2][4]);
void d3d8_multiview_rows(const float rows[4][4]);
void d3d8_multiview_depth(float a, float b);
void d3d8_multiview_projected_depth(int projected);
unsigned int d3d8_multiview_end(void);
void d3d8_clear_back_buffer(float red, float green, float blue, float alpha);
void d3d8_clear_back_buffer_region(float left, float top, float right, float bottom);
void d3d8_host_state_changed(void);
/* a layer copied (host_xr_copy_layer): 2 is a deferred copy, which left GL
as it drew, for the renderer to set again */
static int vr_copied(int result)
{
if (result == 2)
d3d8_host_state_changed();
return result != 0;
}
/* The motion tracker, the HUD's one element at its bottom left, moves from
the HUD to the left wrist (host_xr.c draws it there): this part of the
@@ -432,6 +468,14 @@ static void vr_read_live_settings(void)
vr.laser = config_boolean("vr.laser");
vr.vehicle_wheel = config_boolean("vr.vehicle_wheel");
vr.comfort_vignette = (float)config_real("vr.comfort_vignette");
vr.fingers = config_boolean("vr.fingers");
vr.arm_ik = config_boolean("vr.arm_ik");
{
const char *body = config_string("vr.body");
vr.body_mode = !strcmp(body, "hands") ? HALO_VR_BODY_HANDS :
!strcmp(body, "off") ? HALO_VR_BODY_OFF : HALO_VR_BODY_ARMS;
}
}
void halo_vr_initialize(void)
@@ -495,6 +539,16 @@ void halo_vr_initialize(void)
vr.active = 1;
platform_log("VR: %s, eyes %dx%d at %.0f Hz", vr.info.system_name, vr.info.eye_width, vr.info.eye_height,
vr.info.refresh_rate);
/* SteamVR (the Steam Frame): each eye drawn apart. With both drawn at
once the eyes' pictures did not fuse, now and then: each alone was
sharp, the two together doubled as the head turned (2026-10-05), and
one by one costs it no more (its time goes to the driver either way).
debug.multiview_steamvr draws them at once there again, for finding why */
if (vr.multiview && strstr(vr.info.system_name, "SteamVR") && !config_boolean("debug.multiview_steamvr"))
{
vr.multiview = 0;
platform_log("VR: SteamVR: the eyes drawn one by one (debug.multiview_steamvr)");
}
}
int halo_vr_active(void)
@@ -530,10 +584,65 @@ static unsigned long long now_us(void)
/* vr.log_performance: every five seconds, the frames the game gave the
headset, and where their time went: waiting for the headset (xrWaitFrame),
drawing the eyes, and the rest (the game's update, the overlay, the copies) */
void halo_vr_phase(int phase, int ticks)
{
if (!vr.active || !vr.log_performance || phase < 0 || phase >= HALO_VR_PHASE_COUNT)
return;
vr.phase_end_us[phase] = now_us();
if (phase == HALO_VR_PHASE_TICKS)
vr.frame_ticks = ticks;
}
unsigned long long halo_vr_timing_start(void)
{
return vr.active && vr.log_performance ? now_us() : 0;
}
unsigned long halo_vr_frame_serial(void)
{
return vr.active ? vr.frame_number : 0;
}
void halo_vr_timing_end(int part, unsigned long long start)
{
if (!start || part < 0 || part >= HALO_VR_TIMING_COUNT)
return;
vr.timing_us[part] += now_us() - start;
vr.timing_count[part]++;
}
/* the frame's parts, from the marks halo_vr_phase left (a part not marked
this frame: none) */
static void performance_phases(unsigned long long now)
{
int kind = vr.frame_ticks > 0;
unsigned long long last = vr.frame_start_us;
int phase;
for (phase = 0; phase < HALO_VR_PHASE_COUNT; phase++)
{
unsigned long long end = vr.phase_end_us[phase];
if (end >= last && end <= now)
{
vr.phase_us[kind][phase] += end - last;
last = end;
}
}
vr.phase_us[kind][HALO_VR_PHASE_COUNT] += now - last;
vr.phase_frames[kind]++;
/* (late: longer than the headset's frame, less a millisecond) */
if (vr.info.refresh_rate > 1.0f && (float)(now - vr.frame_start_us + 1000) > 1e6f / vr.info.refresh_rate)
vr.phase_late[kind]++;
memset(vr.phase_end_us, 0, sizeof(vr.phase_end_us));
vr.frame_ticks = 0;
}
static void performance_report(int stereo)
{
unsigned long long now = now_us();
performance_phases(now);
vr.work_us += now - vr.frame_start_us;
vr.report_frames++;
vr.report_stereo_frames += stereo != 0;
@@ -548,6 +657,34 @@ static void performance_report(int stereo)
platform_log("VR: %.1f fps (%d%% stereo); per frame %.1f ms working (eyes %.1f ms), %.1f ms waiting "
"for the headset", frames / seconds, (int)(100.0f * vr.report_stereo_frames / frames),
vr.work_us * 1e-3f / frames, vr.eyes_us * 1e-3f / frames, vr.wait_us * 1e-3f / frames);
{
int kind;
for (kind = 1; kind >= 0; kind--)
{
float count = (float)(vr.phase_frames[kind] ? vr.phase_frames[kind] : 1);
platform_log("VR: %s: %d frames, %d late; ms input %.1f, ticks %.1f, camera %.1f, draw %.1f, "
"present %.1f", kind ? "with ticks" : "no ticks", vr.phase_frames[kind], vr.phase_late[kind],
vr.phase_us[kind][HALO_VR_PHASE_INPUT] * 1e-3f / count,
vr.phase_us[kind][HALO_VR_PHASE_TICKS] * 1e-3f / count,
vr.phase_us[kind][HALO_VR_PHASE_CAMERA] * 1e-3f / count,
vr.phase_us[kind][HALO_VR_PHASE_DRAW] * 1e-3f / count,
vr.phase_us[kind][HALO_VR_PHASE_COUNT] * 1e-3f / count);
}
}
memset(vr.phase_us, 0, sizeof(vr.phase_us));
memset(vr.phase_frames, 0, sizeof(vr.phase_frames));
memset(vr.phase_late, 0, sizeof(vr.phase_late));
/* (the first-person pose: built how often a frame and in how long,
the arms' part of it, and how often taken as built) */
platform_log("VR: first person: %.2f ms a frame in %.2f builds (arms %.3f ms), %.2f kept",
vr.timing_us[HALO_VR_TIMING_FIRST_PERSON] * 1e-3f / frames,
vr.timing_count[HALO_VR_TIMING_FIRST_PERSON] / frames,
vr.timing_us[HALO_VR_TIMING_ARMS] * 1e-3f / frames,
vr.timing_count[HALO_VR_TIMING_FIRST_PERSON_KEPT] / frames);
memset(vr.timing_us, 0, sizeof(vr.timing_us));
memset(vr.timing_count, 0, sizeof(vr.timing_count));
}
vr.report_start_us = now;
vr.report_frames = vr.report_stereo_frames = 0;
@@ -798,6 +935,154 @@ static void melee_update(void)
}
}
/* The fingers, from where they rest on the controller (after
HaloCE-Quest-VR, CC0, its vr_frame.c): the thumb curled while it touches
the stick, a button or the rest, half curled where the controller does not
tell; the index finger with the trigger, a little curled on it; the others
with the grip; holding a grenade, the hand closes round it. Each eased
towards that by a critically damped spring (no overshoot however the
frames' lengths vary). */
static void fingers_update(void)
{
unsigned long long now = now_us();
float seconds = vr.fingers_last_us ? (float)(now - vr.fingers_last_us) * 1e-6f : 0.0f;
int hand;
vr.fingers_last_us = now;
if (seconds > 0.05f)
seconds = 0.05f;
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
{
const struct halo_xr_hand *state = &vr.frame.hands[hand];
float target[4];
int finger;
if (!(state->buttons & HALO_XR_BUTTON_TOUCH_KNOWN))
target[0] = 0.4f;
else
target[0] = (state->buttons & HALO_XR_BUTTON_THUMB_TOUCH) ? 0.75f : 0.0f;
target[1] = fmaxf(state->trigger, (state->buttons & HALO_XR_BUTTON_INDEX_TOUCH) ? 0.22f : 0.0f);
target[2] = target[3] = state->squeeze;
if (hand == HALO_XR_HAND_LEFT && vr.grenade_held)
{
for (finger = 0; finger < 4; finger++)
target[finger] = 0.8f;
}
for (finger = 0; finger < 4; finger++)
{
float omega = finger == 0 ? 20.0f : 26.0f;
float x = vr.finger_curl[hand][finger] - target[finger];
float v = vr.finger_velocity[hand][finger];
float j = v + omega * x, decay = expf(-omega * seconds);
vr.finger_curl[hand][finger] = fmaxf(0.0f, fminf(1.0f, target[finger] + (x + j * seconds) * decay));
vr.finger_velocity[hand][finger] = (v - omega * j * seconds) * decay;
}
}
}
/* The torso's heading (after HaloCE-Quest-VR, CC0, its vr_body_heading):
the head's, steady at any pitch (looking down, the top of the head points
forward; up, the back of it), drawn halfway towards the hands when both
are ahead of it, by at most 45 degrees (turning the body turns both,
looking about only the head). The torso follows it beyond a 15 degree cone,
the turn eased (10 a second, whatever the frame rate). Kept in LOCAL
space, which the stick's turns carry (body_yaw): the shoulders turn with
them at once. */
#define TORSO_CONE (15.0f * PI / 180.0f)
static void torso_update(void)
{
static const float local_forward[3] = { 0.0f, 0.0f, -1.0f };
static const float local_up[3] = { 0.0f, 1.0f, 0.0f };
const struct halo_xr_hand *hands = vr.frame.hands;
unsigned long long now = now_us();
float seconds = vr.torso_valid ? (float)(now - vr.torso_last_us) * 1e-6f : -1.0f;
float forward[3], up[3], level[3], wanted;
int axis;
vr.torso_last_us = now;
if (!(vr.frame.head_flags & HALO_XR_POSE_VALID))
{
vr.torso_valid = 0;
return;
}
quaternion_rotate(vr.frame.head.orientation, local_forward, forward);
quaternion_rotate(vr.frame.head.orientation, local_up, up);
for (axis = 0; axis < 3; axis++)
level[axis] = forward[axis] - forward[1] * up[axis];
level[1] = 0.0f;
if (normalize3(level) > 1e-3f)
{
wanted = atan2f(-level[0], -level[2]);
}
else
{
float pitch;
head_angles(&wanted, &pitch);
}
vr.head_level_yaw = wanted;
if (hands[0].active && (hands[0].flags & HALO_XR_POSE_VALID) && hands[1].active &&
(hands[1].flags & HALO_XR_POSE_VALID))
{
float x = (hands[0].grip.position[0] + hands[1].grip.position[0]) * 0.5f - vr.frame.head.position[0];
float z = (hands[0].grip.position[2] + hands[1].grip.position[2]) * 0.5f - vr.frame.head.position[2];
if (sqrtf(x * x + z * z) > 0.12f)
{
float difference = wrap_angle(atan2f(-x, -z) - wanted);
if (fabsf(difference) < 1.745f)
wanted += 0.5f * fmaxf(-0.785f, fminf(0.785f, difference));
}
}
if (!vr.torso_valid || seconds < 0.0f || seconds > 0.25f || vr.recentre_frames > 0)
{
vr.torso_yaw = wrap_angle(wanted);
}
else
{
float difference = wrap_angle(wanted - vr.torso_yaw);
if (fabsf(difference) > TORSO_CONE)
{
float turn = difference - copysignf(TORSO_CONE, difference);
vr.torso_yaw = wrap_angle(vr.torso_yaw + turn * (1.0f - expf(-10.0f * fminf(seconds, 0.05f))));
}
}
vr.torso_valid = 1;
}
int halo_vr_free_arm_bends(void)
{
return vr.active && vr.arm_ik;
}
int halo_vr_body_mode(void)
{
return vr.active ? vr.body_mode : HALO_VR_BODY_ARMS;
}
double halo_vr_pose_time(void)
{
return (double)vr.frame_start_us * 1e-6;
}
int halo_vr_finger_curls(short local_player_index, int hand, float curls[4])
{
const struct halo_xr_hand *state;
if (!vr.active || !vr.fingers || local_player_index != 0 || hand < 0 || hand >= HALO_XR_HAND_COUNT)
return 0;
state = &vr.frame.hands[hand];
if (!state->active || !(state->flags & HALO_XR_POSE_VALID))
return 0;
memcpy(curls, vr.finger_curl[hand], sizeof(vr.finger_curl[hand]));
return HALO_VR_FINGERS_CURLED | ((state->buttons & HALO_XR_BUTTON_TOUCH_KNOWN) ? HALO_VR_FINGERS_TOUCH : 0);
}
int halo_vr_melee(void)
{
return vr.active && now_us() < vr.melee_until_us;
@@ -1214,6 +1499,8 @@ void halo_vr_frame_begin(void)
look_update();
grenade_update();
melee_update();
fingers_update();
torso_update();
}
void halo_vr_present(unsigned int back_buffer_texture)
@@ -1248,9 +1535,8 @@ void halo_vr_present(unsigned int back_buffer_texture)
if (vr.overlay_mode == HALO_XR_OVERLAY_HEAD_LOCKED && vr.motion_tracker_on_wrist && vr.aim_with_controller)
{
/* the motion tracker to the wrist, and out of the HUD */
submit.wrist = host_xr_copy_layer_region(HALO_XR_LAYER_WRIST, back_buffer_texture, HALO_XR_COPY_ALPHA,
WRIST_REGION_LEFT,
WRIST_REGION_TOP, WRIST_REGION_RIGHT, WRIST_REGION_BOTTOM);
submit.wrist = vr_copied(host_xr_copy_layer_region(HALO_XR_LAYER_WRIST, back_buffer_texture,
HALO_XR_COPY_ALPHA, WRIST_REGION_LEFT, WRIST_REGION_TOP, WRIST_REGION_RIGHT, WRIST_REGION_BOTTOM));
submit.wrist_size = vr.motion_tracker_size;
submit.wrist_turn = vr.motion_tracker_turn;
submit.wrist_right = vr.motion_tracker_right;
@@ -1281,8 +1567,8 @@ void halo_vr_present(unsigned int back_buffer_texture)
submit.scope_size = vr.scope_size;
submit.scope_pose = vr.scope_pose;
}
host_xr_copy_layer(HALO_XR_LAYER_OVERLAY, back_buffer_texture,
vr.overlay_blend ? HALO_XR_COPY_ALPHA : HALO_XR_COPY_OPAQUE);
vr_copied(host_xr_copy_layer(HALO_XR_LAYER_OVERLAY, back_buffer_texture,
vr.overlay_blend ? HALO_XR_COPY_ALPHA : HALO_XR_COPY_OPAQUE));
host_xr_end_frame(&submit);
vr.last_overlay_mode = vr.overlay_mode;
vr.last_frame_stereo = submit.eyes;
@@ -1480,6 +1766,15 @@ void halo_vr_multiview_projection(float projection_22, float projection_32)
d3d8_multiview_depth(-projection_22, projection_32);
}
/* lens flares are being drawn (rasterizer_lights.c): each eye's by the
flare's depth, as the flare's test was, not at infinity (a weapon's own,
the plasma pistol's at its prongs, half a metre away: a ball each eye saw
elsewhere); the sun's are far enough for infinity either way */
void halo_vr_multiview_lens_flares(int drawing)
{
d3d8_multiview_projected_depth(drawing && multiview_drawing);
}
/* the sky is being drawn (render.c): as though infinitely far */
void halo_vr_multiview_sky(int sky)
{
@@ -1495,9 +1790,9 @@ void halo_vr_capture_multiview(void)
if (!multiview_drawing)
return;
multiview_drawing = 0;
if (host_xr_copy_layer_array(HALO_XR_LAYER_EYE_LEFT, layers, 0))
if (vr_copied(host_xr_copy_layer_array(HALO_XR_LAYER_EYE_LEFT, layers, 0)))
vr.eyes_captured |= 1;
if (host_xr_copy_layer_array(HALO_XR_LAYER_EYE_RIGHT, layers, 1))
if (vr_copied(host_xr_copy_layer_array(HALO_XR_LAYER_EYE_RIGHT, layers, 1)))
vr.eyes_captured |= 2;
if (vr.eyes_start_us)
vr.eyes_us += now_us() - vr.eyes_start_us;
@@ -1507,7 +1802,7 @@ void halo_vr_capture_eye(int eye)
{
if (!vr.active || eye < 0 || eye >= HALO_XR_EYE_COUNT)
return;
if (host_xr_copy_layer(HALO_XR_LAYER_EYE_LEFT + eye, d3d8_back_buffer_texture(), 1))
if (vr_copied(host_xr_copy_layer(HALO_XR_LAYER_EYE_LEFT + eye, d3d8_back_buffer_texture(), 1)))
vr.eyes_captured |= 1 << eye;
if (eye == HALO_XR_EYE_COUNT - 1 && vr.eyes_start_us)
vr.eyes_us += now_us() - vr.eyes_start_us;
@@ -1938,27 +2233,34 @@ void halo_vr_debug_look(short local_player_index, const float unit_looking[3], i
head[0], head[1], head[2], unit_looking[0], unit_looking[1], unit_looking[2], in_vehicle ? ", in a vehicle" : "");
}
/* the body: where the head is (world units) and its heading, flat */
int halo_vr_body_frame(short local_player_index, float head[3], float forward[3])
/* the body: where the head is (world units), the neck it turns about (a
little behind the eyes, at their height) and the torso's heading, flat */
#define NECK_BEHIND_EYES 0.09f
int halo_vr_body_frame(short local_player_index, float head[3], float neck[3], float forward[3])
{
float offset[3], look[3];
float offset[3], local[3], back[3];
int axis;
if (local_player_index != 0 || !halo_vr_stereo_frame() || !(vr.frame.head_flags & HALO_XR_POSE_VALID))
if (local_player_index != 0 || !halo_vr_stereo_frame() || !(vr.frame.head_flags & HALO_XR_POSE_VALID) ||
!vr.torso_valid)
{
return 0;
}
tracked_offset(vr.frame.head.position, offset);
for (axis = 0; axis < 3; axis++)
head[axis] = vr.game_camera[axis] + offset[axis] / METRES_PER_WORLD_UNIT;
if (!halo_vr_look_vector(local_player_index, look))
return 0;
forward[0] = look[0];
forward[1] = look[1];
local[0] = sinf(vr.head_level_yaw) * NECK_BEHIND_EYES;
local[1] = 0.0f;
local[2] = cosf(vr.head_level_yaw) * NECK_BEHIND_EYES;
local_to_game(local, vr.body_yaw, back);
for (axis = 0; axis < 3; axis++)
neck[axis] = head[axis] + back[axis] / METRES_PER_WORLD_UNIT;
local[0] = -sinf(vr.torso_yaw);
local[2] = -cosf(vr.torso_yaw);
local_to_game(local, vr.body_yaw, forward);
forward[2] = 0.0f;
if (normalize3(forward) < 1e-3f)
{
forward[0] = cosf(vr.body_yaw);
forward[1] = sinf(vr.body_yaw);
}
normalize3(forward);
return 1;
}
@@ -2338,9 +2640,9 @@ void halo_vr_capture_names(void)
{
if (vr.name_count > 0)
{
vr.names_captured = host_xr_copy_layer_region(HALO_XR_LAYER_NAMES, d3d8_back_buffer_texture(),
vr.names_captured = vr_copied(host_xr_copy_layer_region(HALO_XR_LAYER_NAMES, d3d8_back_buffer_texture(),
HALO_XR_COPY_ALPHA, NAME_SLOT_LEFT / 640.0f, 0.0f, (NAME_SLOT_LEFT + NAME_SLOT_WIDTH) / 640.0f,
HALO_XR_MAXIMUM_NAMES * NAME_SLOT_HEIGHT / 480.0f);
HALO_XR_MAXIMUM_NAMES * NAME_SLOT_HEIGHT / 480.0f));
}
d3d8_clear_back_buffer(0.0f, 0.0f, 0.0f, 0.0f);
}
@@ -2448,8 +2750,8 @@ int halo_vr_scope_pass(void)
void halo_vr_capture_scope(float left, float top, float right, float bottom)
{
vr.scope_pass = 0;
vr.scope_captured = host_xr_copy_layer_region(HALO_XR_LAYER_SCOPE, d3d8_back_buffer_texture(), HALO_XR_COPY_DISC,
left, top, right, bottom);
vr.scope_captured = vr_copied(host_xr_copy_layer_region(HALO_XR_LAYER_SCOPE, d3d8_back_buffer_texture(),
HALO_XR_COPY_DISC, left, top, right, bottom));
}
int halo_vr_hide_crosshair(void)
@@ -2466,9 +2768,9 @@ int halo_vr_crosshair_apart(void)
void halo_vr_capture_crosshair(void)
{
vr.crosshair_captured = host_xr_copy_layer_region(HALO_XR_LAYER_CROSSHAIR, d3d8_back_buffer_texture(),
vr.crosshair_captured = vr_copied(host_xr_copy_layer_region(HALO_XR_LAYER_CROSSHAIR, d3d8_back_buffer_texture(),
HALO_XR_COPY_ALPHA, CROSSHAIR_REGION_LEFT, CROSSHAIR_REGION_TOP, CROSSHAIR_REGION_RIGHT,
CROSSHAIR_REGION_BOTTOM);
CROSSHAIR_REGION_BOTTOM));
d3d8_clear_back_buffer(0.0f, 0.0f, 0.0f, 0.0f);
}
@@ -2486,6 +2788,9 @@ int halo_vr_controllers(struct halo_xr_hand hands[HALO_XR_HAND_COUNT], int *menu
int halo_vr_controller_layout(void) { return 0; }
int halo_vr_active(void) { return 0; }
unsigned long long halo_vr_timing_start(void) { return 0; }
unsigned long halo_vr_frame_serial(void) { return 0; }
void halo_vr_timing_end(int part, unsigned long long start) { (void)part; (void)start; }
int halo_vr_stereo_frame(void) { return 0; }
int halo_vr_stereo_player(short local_player_index, int cinematic) { (void)local_player_index; (void)cinematic; return 0; }
void halo_vr_eye_camera(int eye, const float game_position[3], float position[3], float forward[3], float up[3],
@@ -2503,6 +2808,7 @@ void halo_vr_center_camera(const float game_position[3], float position[3], floa
}
void halo_vr_capture_multiview(void) {}
void halo_vr_multiview_sky(int sky) { (void)sky; }
void halo_vr_multiview_lens_flares(int drawing) { (void)drawing; }
void halo_vr_multiview_projection(float projection_22, float projection_32)
{
(void)projection_22; (void)projection_32;
@@ -2540,6 +2846,14 @@ void halo_vr_debug_look(short local_player_index, const float unit_looking[3], i
(void)local_player_index; (void)unit_looking; (void)in_vehicle;
}
int halo_vr_melee(void) { return 0; }
int halo_vr_finger_curls(short local_player_index, int hand, float curls[4])
{
(void)local_player_index; (void)hand; (void)curls;
return 0;
}
int halo_vr_free_arm_bends(void) { return 0; }
int halo_vr_body_mode(void) { return HALO_VR_BODY_ARMS; }
double halo_vr_pose_time(void) { return 0.0; }
void halo_vr_set_seat(short local_player_index, int seat, int armed, float vehicle_yaw)
{
(void)local_player_index; (void)seat; (void)armed; (void)vehicle_yaw;
@@ -2568,9 +2882,9 @@ int halo_vr_grenade_throw(short local_player_index, float origin[3], float direc
}
float halo_vr_melee_strength(void) { return 0.0f; }
int halo_vr_look_delta(float *yaw, float *pitch) { *yaw = *pitch = 0.0f; return 0; }
int halo_vr_body_frame(short local_player_index, float head[3], float forward[3])
int halo_vr_body_frame(short local_player_index, float head[3], float neck[3], float forward[3])
{
(void)local_player_index; (void)head; (void)forward;
(void)local_player_index; (void)head; (void)neck; (void)forward;
return 0;
}
int halo_vr_left_hand(short local_player_index, float position[3], float forward[3], float up[3])
+40 -13
View File
@@ -656,6 +656,10 @@ struct texture_entry
unsigned long last_used_frame;
/* the high-res HUD texture drawn in its place (hud_hires.h), or -1 */
long override;
/* the newest generation of its pages (memory_watch_generation) as of the
memory watch serial read before it was found: the same while no watched
page has been written since (0: never found) */
unsigned long watched_serial, watched_generation;
};
#define TEXTURE_BUCKET_COUNT 4096
@@ -668,7 +672,7 @@ static struct texture_entry *texture_buckets[TEXTURE_BUCKET_COUNT];
texture that is not palettized is remembered with the memory watch serial it
started at: while no watched page has been written since, and no texture
has been dropped, the same lookup finds the same current texture. */
#define RECENT_TEXTURE_COUNT 64
#define RECENT_TEXTURE_COUNT 512
static struct
{
@@ -680,9 +684,14 @@ static struct
static unsigned long texture_drop_serial = 1;
static unsigned long texture_frame = 0;
/* (every bit of the three mixed into the top ones: textures are aligned,
and few sizes and formats are common) */
static unsigned long bucket_index(DWORD data, DWORD format_word, DWORD size_word)
{
return ((data >> 7) ^ (format_word * 2654435761UL) ^ size_word) % TEXTURE_BUCKET_COUNT;
unsigned long hash = (unsigned long)data * 2654435761UL ^ (unsigned long)format_word * 2246822519UL ^
(unsigned long)size_word * 3266489917UL;
return ((hash & 0xffffffffUL) >> 20) % TEXTURE_BUCKET_COUNT;
}
/* palettized textures are cached per palette contents: the game rewrites
@@ -757,25 +766,33 @@ GLuint xgpu_texture_get(const DWORD *resource, const D3DCOLOR *palette, GLenum *
unsigned long recent = bucket_index(data, format_word, size_word) % RECENT_TEXTURE_COUNT;
unsigned long watch_serial = memory_watch_serial();
/* (a texture that is not palettized has the one entry, until one is
dropped: remembered, a page written since only means checking it) */
entry = NULL;
if (!palettized && recent_textures[recent].entry && recent_textures[recent].data == data &&
recent_textures[recent].format_word == format_word && recent_textures[recent].size_word == size_word &&
recent_textures[recent].watch_serial == watch_serial &&
recent_textures[recent].drop_serial == texture_drop_serial)
{
entry = recent_textures[recent].entry;
entry->last_used_frame = texture_frame;
return texture_entry_result(entry, target, description);
if (recent_textures[recent].watch_serial == watch_serial)
{
entry->last_used_frame = texture_frame;
return texture_entry_result(entry, target, description);
}
}
for (entry = *bucket; entry; entry = entry->next)
if (!entry)
{
if (entry->data == data && entry->format_word == format_word && entry->size_word == size_word)
for (entry = *bucket; entry; entry = entry->next)
{
if (entry->palette_hash == hash)
break;
variant_count++;
if (!oldest_variant || entry->last_used_frame < oldest_variant->last_used_frame)
oldest_variant = entry;
if (entry->data == data && entry->format_word == format_word && entry->size_word == size_word)
{
if (entry->palette_hash == hash)
break;
variant_count++;
if (!oldest_variant || entry->last_used_frame < oldest_variant->last_used_frame)
oldest_variant = entry;
}
}
}
if (!entry && variant_count >= MAXIMUM_PALETTE_VARIANTS)
@@ -806,7 +823,17 @@ GLuint xgpu_texture_get(const DWORD *resource, const D3DCOLOR *palette, GLenum *
if (no_cache < 0)
no_cache = config_boolean("debug.texture_no_cache");
generation = memory_watch_generation(entry->address, entry->size);
/* (its pages' newest generation, found again only once a watched page
has been written: a large texture's pages, scanned for every draw that
bound it, were much of a frame with many objects) */
if (entry->watched_serial && entry->watched_serial == watch_serial)
generation = entry->watched_generation;
else
{
generation = memory_watch_generation(entry->address, entry->size);
entry->watched_serial = watch_serial;
entry->watched_generation = generation;
}
if (!entry->generation || generation > entry->generation || no_cache)
{
/* protect first, so a write racing with the upload is noticed */
+68 -3
View File
@@ -53,6 +53,11 @@ must call this afterwards. */
void xgpu_gl_state_invalidate(void);
/* a compiled shader, or a linked program of two, or 0 with the log
written */
GLuint xgpu_compile_shader(GLenum type, const char *code, const char *what);
GLuint xgpu_link_program(GLuint vertex_shader, GLuint fragment_shader, const char *what);
/* ---------- generated source text */
struct xgpu_text
@@ -71,14 +76,34 @@ void xgpu_text_append(struct xgpu_text *text, const char *format, ...) __attribu
/* D3D constant register -96 is hardware register 0 */
#define XGPU_VERTEX_CONSTANT_BIAS 96
/* where one of the game's model lighting programs (d3d8_gl.c
halo_vertex_shader_lighting) has the normal, and the world position, that it
lights the diffuse color by: the temporary register that holds each before
the instruction given */
struct nv2a_vertex_lighting
{
/* 1 by the ambient and distant lights, 2 by the point lights too */
int lights;
unsigned long normal_instruction, normal_register;
/* (with the point lights only) */
unsigned long position_instruction, position_register;
};
/* finds them in a model lighting program, checking that it lights its
diffuse color as nv2a_psh.c does for each pixel; FALSE if it does not */
BOOL nv2a_vertex_shader_lighting(const DWORD *instructions, unsigned long instruction_count,
struct nv2a_vertex_lighting *lighting);
/* GLSL for an NV2A vertex program (the instruction words after the program
header). Attributes whose bit is set in packed_attribute_mask are fed as
NORMPACKED3 32-bit integers and unpacked in the shader. Returns a malloc'd
string. */
NORMPACKED3 32-bit integers and unpacked in the shader. With lighting (else
NULL), the normal and world position go to the pixel shader too, which
lights the diffuse color for each pixel (nv2a_pixel_shader_key
per_pixel_lighting). Returns a malloc'd string. */
/* multiview: for a two-view (GL_OVR_multiview2) target, each view's
position corrected from the shared one by mv_rows (d3d8_gl.c, VR) */
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);
/* ---------- pixel shaders */
@@ -114,6 +139,15 @@ struct nv2a_pixel_shader_key
behind it only where it covers it (the Xbox's point-sampled meters stop
at their texels' edges; filtered ones have a fringe of faint texels) */
unsigned char coverage_alpha;
/* a model lighting program's draw lit for each pixel
(display.per_pixel_lighting): nv2a_vertex_lighting's lights, or 0 for
the diffuse color the vertex shader computed */
unsigned char per_pixel_lighting;
/* drawn into a multisampled target (display.anti_aliasing's
multisampling), its samples a pixel: the alpha test covers samples in
proportion to how far alpha is past the reference, not all of the pixel
or none of it, so that cut-out edges (foliage, grates) are smoothed too */
unsigned char alpha_test_samples;
};
char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key);
@@ -140,6 +174,13 @@ C0/C1 of each stage and the final combiner, and texture constants */
"uniform vec4 texture_scale[4];\n" \
XGPU_PIXEL_UNIFORMS_ES
/* the vertex constants the per-pixel model lighting reads, in a uniform of
their own (the vertex shader's 192 would pass OpenGL ES's least fragment
uniform space): [0] c[-82] (the translucency in z), [1] to [11] c[-79] to
c[-69] (rasterizer_set_model_lighting's two point lights, two distant
lights and the ambient light) */
#define XGPU_MODEL_LIGHT_COUNT 12
/* ---------- textures */
struct xgpu_texture_description
@@ -178,9 +219,33 @@ struct xgpu_render_target
targets when the game draws at the display's resolution (d3d8_gl.c) */
float scale[2];
unsigned long gl_width, gl_height;
/* with multisampling, the multisampled renderbuffer draws go to, its
samples a pixel (0 when it has none), and whether it has been drawn into
since the texture last had its pixels (d3d8_gl.c,
render_target_multisample) */
GLuint multisample;
int samples;
BOOL unresolved;
};
/* the GL texture holding a render target with this physical address, or 0 */
struct xgpu_render_target *xgpu_render_target_find(unsigned long data);
/* ---------- anti-aliasing
display.anti_aliasing's passes (xgpu_post.c): FXAA or SMAA antialias each
window's 3D view in place before the HUD and menus are drawn over it, so
that their text stays sharp. Supersampling and multisampling are the
device's (d3d8_gl.c). */
/* the programs and textures of FXAA, or of SMAA, made now; FALSE if they
cannot be (once FALSE, it stays so) */
BOOL xgpu_post_prepare(BOOL smaa);
/* FXAA, or SMAA, on the corners x0, y0 to x1, y1 (from row 0) of a render
target's framebuffer, width by height, GL_RGBA8; FALSE if its programs do
not build */
BOOL xgpu_post_anti_alias(BOOL smaa, GLuint framebuffer, unsigned long width, unsigned long height,
const GLint corners[4]);
#endif
+524
View File
@@ -0,0 +1,524 @@
/*
XGPU_POST.C
The post-process passes of display.anti_aliasing (xgpu.h): FXAA and SMAA,
run on a render target in place, over the rectangle of a window's 3D view.
Each pass draws one triangle covering the rectangle (its corners from
gl_VertexID, with no vertex data) and finds its texels from gl_FragCoord: a
target's rows are the picture's from the top, as the passes' own textures'
are, so no coordinate is turned over. The target's pixels are copied first,
so that no pass reads what it writes, and only colour is written back: the
game keeps values of its own in destination alpha. The programs are built
when the setting is chosen (xgpu_post_prepare), not in the middle of a frame.
FXAA is written here, after Timothy Lottes' description: the direction of an
edge from the luma around a pixel, the edge's ends found by stepping along
it, and the pixel blended across it by where it lies between them, or by how
much it stands out from its neighbours. SMAA is port/third_party/smaa's,
compiled as GLSL at its HIGH preset: edges from luma, blending weights from
its lookup textures, then the blend. Android has FXAA only (d3d8_gl.c's
anti_aliasing_values).
*/
#include "xgpu.h"
#include <stdlib.h>
#ifndef HALO_ANDROID
#include "memory/zlib/zlib.h"
/* port/third_party/smaa, embedded by tools/embed_assets.py: the shader's
text (ending in a NUL), and the lookup textures' bytes compressed with
zlib; each size 0 where the build had no such file */
extern const unsigned int xgpu_smaa_shader[];
extern const unsigned long xgpu_smaa_shader_size;
extern const unsigned int xgpu_smaa_area_texture[];
extern const unsigned long xgpu_smaa_area_texture_size;
extern const unsigned int xgpu_smaa_search_texture[];
extern const unsigned long xgpu_smaa_search_texture_size;
#endif
enum
{
_post_program_fxaa,
_post_program_smaa_edges,
_post_program_smaa_weights,
_post_program_smaa_blend,
NUMBER_OF_POST_PROGRAMS
};
static struct
{
GLuint vertex_shader;
GLuint programs[NUMBER_OF_POST_PROGRAMS];
/* each program's metrics uniform, and FXAA's bounds */
GLint metrics[NUMBER_OF_POST_PROGRAMS];
GLint bounds;
/* FXAA's, and SMAA's, programs or textures could not be made */
BOOL failed[2];
/* the size of the textures below */
unsigned long width, height;
/* the target's pixels before the pass, and SMAA's edges and blending
weights */
GLuint color, edges, weights;
GLuint color_framebuffer, edges_framebuffer, weights_framebuffer;
/* SMAA's lookup textures */
GLuint area, search;
/* linear and clamped, as FXAA and SMAA sample */
GLuint sampler;
/* no attributes: the triangle comes from gl_VertexID */
GLuint vertex_array;
} post;
/* ---------- programs */
static void shader_header(struct xgpu_text *text)
{
#ifdef HALO_ANDROID
xgpu_text_append(text,
"#version %s\n"
"precision highp float;\n"
"precision highp int;\n"
"precision highp sampler2D;\n",
xgpu_capabilities.shading_language);
#else
xgpu_text_append(text, "#version 450 core\n");
#endif
}
/* one triangle over the whole viewport */
static const char vertex_source[] =
"void main()\n"
"{\n"
"\tgl_Position = vec4(float((gl_VertexID & 1) << 2) - 1.0, float((gl_VertexID & 2) << 1) - 1.0, 0.0, 1.0);\n"
"}\n";
static const char fxaa_source[] =
"uniform sampler2D color_texture;\n"
"/* 1 / width, 1 / height, width, height */\n"
"uniform vec4 metrics;\n"
"/* the rectangle's outermost texel centres, as texture coordinates */\n"
"uniform vec4 bounds;\n"
"out vec4 result;\n"
"\n"
"const float EDGE_THRESHOLD = 0.125;\n"
"const float EDGE_THRESHOLD_MINIMUM = 0.0312;\n"
"const float SUBPIXEL_QUALITY = 0.75;\n"
"const int STEPS = 12;\n"
"const float STEP_LENGTHS[12] = float[12](1.0, 1.0, 1.0, 1.0, 1.0, 1.5, 2.0, 2.0, 2.0, 2.0, 4.0, 8.0);\n"
"\n"
"vec3 color_at(vec2 position)\n"
"{\n"
"\treturn textureLod(color_texture, clamp(position, bounds.xy, bounds.zw), 0.0).rgb;\n"
"}\n"
"\n"
"float luma_at(vec2 position)\n"
"{\n"
"\treturn dot(color_at(position), vec3(0.299, 0.587, 0.114));\n"
"}\n"
"\n"
"void main()\n"
"{\n"
"\tvec2 position = gl_FragCoord.xy * metrics.xy;\n"
"\tfloat luma = luma_at(position);\n"
"\tfloat up = luma_at(position + vec2(0.0, -metrics.y));\n"
"\tfloat down = luma_at(position + vec2(0.0, metrics.y));\n"
"\tfloat left = luma_at(position + vec2(-metrics.x, 0.0));\n"
"\tfloat right = luma_at(position + vec2(metrics.x, 0.0));\n"
"\tfloat highest = max(luma, max(max(up, down), max(left, right)));\n"
"\tfloat range = highest - min(luma, min(min(up, down), min(left, right)));\n"
"\n"
"\t/* no edge: the target has the pixel already */\n"
"\tif (range < max(EDGE_THRESHOLD_MINIMUM, highest * EDGE_THRESHOLD))\n"
"\t\tdiscard;\n"
"\tfloat up_left = luma_at(position + vec2(-metrics.x, -metrics.y));\n"
"\tfloat up_right = luma_at(position + vec2(metrics.x, -metrics.y));\n"
"\tfloat down_left = luma_at(position + vec2(-metrics.x, metrics.y));\n"
"\tfloat down_right = luma_at(position + vec2(metrics.x, metrics.y));\n"
"\n"
"\t/* how much the pixel stands out from its neighbourhood (a feature\n"
"\tthinner than a pixel) */\n"
"\tfloat average = (2.0 * (up + down + left + right) + up_left + up_right + down_left + down_right) / 12.0;\n"
"\tfloat subpixel = smoothstep(0.0, 1.0, clamp(abs(average - luma) / range, 0.0, 1.0));\n"
"\tsubpixel = subpixel * subpixel * SUBPIXEL_QUALITY;\n"
"\n"
"\t/* an edge along the rows where the luma changes more from row to row\n"
"\tthan from column to column; across it, the side it changes more to */\n"
"\tbool horizontal = abs(up_left + down_left - 2.0 * left) + 2.0 * abs(up + down - 2.0 * luma) +\n"
"\t\tabs(up_right + down_right - 2.0 * right) >=\n"
"\t\tabs(up_left + up_right - 2.0 * up) + 2.0 * abs(left + right - 2.0 * luma) +\n"
"\t\tabs(down_left + down_right - 2.0 * down);\n"
"\tfloat before = horizontal ? up : left;\n"
"\tfloat after = horizontal ? down : right;\n"
"\tbool before_steeper = abs(before - luma) >= abs(after - luma);\n"
"\tfloat gradient = 0.25 * max(abs(before - luma), abs(after - luma));\n"
"\tfloat edge_luma = 0.5 * ((before_steeper ? before : after) + luma);\n"
"\tvec2 across = (horizontal ? vec2(0.0, metrics.y) : vec2(metrics.x, 0.0)) * (before_steeper ? -1.0 : 1.0);\n"
"\tvec2 along = horizontal ? vec2(metrics.x, 0.0) : vec2(0.0, metrics.y);\n"
"\n"
"\t/* along the edge, half a pixel across, to where its luma ends either way */\n"
"\tvec2 end_before = position + 0.5 * across - along;\n"
"\tvec2 end_after = position + 0.5 * across + along;\n"
"\tfloat luma_before = luma_at(end_before) - edge_luma;\n"
"\tfloat luma_after = luma_at(end_after) - edge_luma;\n"
"\tbool reached_before = abs(luma_before) >= gradient;\n"
"\tbool reached_after = abs(luma_after) >= gradient;\n"
"\tfor (int index = 1; index < STEPS && !(reached_before && reached_after); index++)\n"
"\t{\n"
"\t\tif (!reached_before)\n"
"\t\t{\n"
"\t\t\tend_before -= along * STEP_LENGTHS[index];\n"
"\t\t\tluma_before = luma_at(end_before) - edge_luma;\n"
"\t\t\treached_before = abs(luma_before) >= gradient;\n"
"\t\t}\n"
"\t\tif (!reached_after)\n"
"\t\t{\n"
"\t\t\tend_after += along * STEP_LENGTHS[index];\n"
"\t\t\tluma_after = luma_at(end_after) - edge_luma;\n"
"\t\t\treached_after = abs(luma_after) >= gradient;\n"
"\t\t}\n"
"\t}\n"
"\n"
"\t/* blended across the edge the more, the nearer the pixel is to an end;\n"
"\tonly where the luma at that end changes the way the pixel's does */\n"
"\tfloat distance_before = horizontal ? position.x - end_before.x : position.y - end_before.y;\n"
"\tfloat distance_after = horizontal ? end_after.x - position.x : end_after.y - position.y;\n"
"\tfloat pixel_offset = 0.5 - min(distance_before, distance_after) / (distance_before + distance_after);\n"
"\tbool varies = ((distance_before < distance_after ? luma_before : luma_after) < 0.0) != (luma < edge_luma);\n"
"\tresult = vec4(color_at(position + max(varies ? pixel_offset : 0.0, subpixel) * across), 1.0);\n"
"}\n";
#ifndef HALO_ANDROID
/* the main functions of SMAA's passes, after SMAA.hlsl; its vertex
functions' offsets are found for each pixel */
static const char *const smaa_mains[] =
{
/* edges */
"uniform sampler2D color_texture;\n"
"out vec4 result;\n"
"\n"
"void main()\n"
"{\n"
"\tvec2 texcoord = gl_FragCoord.xy * metrics.xy;\n"
"\tvec4 offset[3];\n"
"\tSMAAEdgeDetectionVS(texcoord, offset);\n"
"\tresult = vec4(SMAALumaEdgeDetectionPS(texcoord, offset, color_texture), 0.0, 0.0);\n"
"}\n",
/* weights */
"uniform sampler2D edges_texture;\n"
"uniform sampler2D area_texture;\n"
"uniform sampler2D search_texture;\n"
"out vec4 result;\n"
"\n"
"void main()\n"
"{\n"
"\tvec2 texcoord = gl_FragCoord.xy * metrics.xy;\n"
"\tvec2 pixcoord;\n"
"\tvec4 offset[3];\n"
"\tSMAABlendingWeightCalculationVS(texcoord, pixcoord, offset);\n"
"\tresult = SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, edges_texture, area_texture,\n"
"\t\tsearch_texture, vec4(0.0));\n"
"}\n",
/* blend */
"uniform sampler2D color_texture;\n"
"uniform sampler2D weights_texture;\n"
"out vec4 result;\n"
"\n"
"void main()\n"
"{\n"
"\tvec2 texcoord = gl_FragCoord.xy * metrics.xy;\n"
"\tvec4 offset;\n"
"\tSMAANeighborhoodBlendingVS(texcoord, offset);\n"
"\tresult = SMAANeighborhoodBlendingPS(texcoord, offset, color_texture, weights_texture);\n"
"}\n",
};
#endif
/* a program, its samplers given their texture units; 0 if it does not
build */
static GLuint program_build(int which)
{
static const char *const names[NUMBER_OF_POST_PROGRAMS] =
{
"FXAA", "SMAA edge detection", "SMAA blending weight", "SMAA neighborhood blending"
};
static const struct
{
const char *name;
GLint unit;
} units[] =
{
{ "color_texture", 0 }, { "edges_texture", 0 }, { "area_texture", 1 },
{ "search_texture", 2 }, { "weights_texture", 1 },
};
struct xgpu_text text = { 0 };
GLuint fragment_shader, program;
unsigned long index;
if (!post.vertex_shader)
{
shader_header(&text);
xgpu_text_append(&text, "%s", vertex_source);
post.vertex_shader = xgpu_compile_shader(GL_VERTEX_SHADER, text.buffer, "post-process");
text.length = 0;
}
shader_header(&text);
if (which == _post_program_fxaa)
{
xgpu_text_append(&text, "%s", fxaa_source);
}
#ifndef HALO_ANDROID
else if (xgpu_smaa_shader_size)
{
xgpu_text_append(&text,
"#define SMAA_GLSL_4 1\n"
"uniform vec4 metrics;\n"
"#define SMAA_RT_METRICS metrics\n"
"#define SMAA_PRESET_HIGH 1\n"
"%s\n%s",
(const char *)xgpu_smaa_shader, smaa_mains[which - _post_program_smaa_edges]);
}
#endif
else
{
platform_log("anti-aliasing: the build has no SMAA");
free(text.buffer);
return 0;
}
fragment_shader = post.vertex_shader ?
xgpu_compile_shader(GL_FRAGMENT_SHADER, text.buffer, names[which]) :
0;
free(text.buffer);
if (!fragment_shader)
return 0;
program = xgpu_link_program(post.vertex_shader, fragment_shader, names[which]);
glDeleteShader(fragment_shader);
if (!program)
return 0;
glUseProgram(program);
/* (a name the program does not have is location -1, which is ignored) */
for (index = 0; index < sizeof(units) / sizeof(units[0]); index++)
glUniform1i(glGetUniformLocation(program, units[index].name), units[index].unit);
post.metrics[which] = glGetUniformLocation(program, "metrics");
if (which == _post_program_fxaa)
post.bounds = glGetUniformLocation(program, "bounds");
return program;
}
/* ---------- textures */
static GLuint texture_new(GLenum format, unsigned long width, unsigned long height, GLenum data_format,
const void *data)
{
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
glTexImage2D(GL_TEXTURE_2D, 0, (GLint)format, (GLsizei)width, (GLsizei)height, 0, data_format,
GL_UNSIGNED_BYTE, data);
return texture;
}
static GLuint framebuffer_new(GLuint texture)
{
GLuint framebuffer;
GLenum draw_buffer = GL_COLOR_ATTACHMENT0;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
glDrawBuffers(1, &draw_buffer);
return framebuffer;
}
/* the passes' textures, of the target's size (SMAA's only for SMAA); color
is the target's format, so that the copy is exact */
static void textures_fit(BOOL smaa, unsigned long width, unsigned long height)
{
if (post.width != width || post.height != height)
{
GLuint textures[3] = { post.color, post.edges, post.weights };
GLuint framebuffers[3] = { post.color_framebuffer, post.edges_framebuffer, post.weights_framebuffer };
glDeleteFramebuffers(3, framebuffers);
glDeleteTextures(3, textures);
post.color = post.edges = post.weights = 0;
post.width = width;
post.height = height;
post.color = texture_new(GL_RGBA8, width, height, GL_RGBA, NULL);
post.color_framebuffer = framebuffer_new(post.color);
}
if (smaa && !post.edges)
{
post.edges = texture_new(GL_RG8, width, height, GL_RG, NULL);
post.weights = texture_new(GL_RGBA8, width, height, GL_RGBA, NULL);
post.edges_framebuffer = framebuffer_new(post.edges);
post.weights_framebuffer = framebuffer_new(post.weights);
}
}
#ifndef HALO_ANDROID
/* a lookup texture of SMAA's from its zlib stream; 0 if it does not
inflate */
static GLuint lookup_texture(const unsigned int *stream, unsigned long stream_size, GLenum format,
GLenum data_format, unsigned long width, unsigned long height, unsigned long texel_size)
{
uLongf size = (uLongf)(width * height * texel_size);
unsigned char *texels = malloc(size);
GLuint texture = 0;
int result;
if (!texels)
return 0;
result = uncompress(texels, &size, (const Bytef *)stream, (uLong)stream_size);
/* (the game's zlib is 1.1, which can stop short of saying the stream
has ended: the size says whether it is whole) */
if ((result == Z_OK || result == Z_BUF_ERROR) && size == (uLongf)(width * height * texel_size))
{
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
texture = texture_new(format, width, height, data_format, texels);
}
free(texels);
return texture;
}
#endif
/* ---------- the passes */
static void texture_bind(GLuint unit, GLuint texture)
{
glActiveTexture(GL_TEXTURE0 + unit);
glBindTexture(GL_TEXTURE_2D, texture);
glBindSampler(unit, post.sampler);
}
/* a program's triangle drawn into a framebuffer over the rectangle;
color_only keeps the framebuffer's alpha */
static void pass_draw(int which, GLuint framebuffer, const GLint corners[4], BOOL color_only)
{
float metrics[4];
metrics[0] = 1.0f / (float)post.width;
metrics[1] = 1.0f / (float)post.height;
metrics[2] = (float)post.width;
metrics[3] = (float)post.height;
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glViewport(corners[0], corners[1], corners[2] - corners[0], corners[3] - corners[1]);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, color_only ? GL_FALSE : GL_TRUE);
glUseProgram(post.programs[which]);
glUniform4fv(post.metrics[which], 1, metrics);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
/* an intermediate emptied, all of it, so that no edge of another window's
pass is found: SMAA's passes write only where there is an edge */
static void intermediate_clear(GLuint framebuffer)
{
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
BOOL xgpu_post_prepare(BOOL smaa)
{
int first = smaa ? _post_program_smaa_edges : _post_program_fxaa;
int last = smaa ? _post_program_smaa_blend : _post_program_fxaa;
int which;
if (post.failed[smaa != FALSE])
return FALSE;
for (which = first; which <= last; which++)
{
if (!post.programs[which] && !(post.programs[which] = program_build(which)))
{
post.failed[smaa != FALSE] = TRUE;
return FALSE;
}
}
#ifndef HALO_ANDROID
if (smaa && !post.area)
{
post.area = lookup_texture(xgpu_smaa_area_texture, xgpu_smaa_area_texture_size, GL_RG8, GL_RG,
160, 560, 2);
post.search = lookup_texture(xgpu_smaa_search_texture, xgpu_smaa_search_texture_size, GL_R8, GL_RED,
64, 16, 1);
if (!post.area || !post.search)
{
platform_log("anti-aliasing: cannot inflate SMAA's lookup textures");
post.failed[1] = TRUE;
return FALSE;
}
}
#endif
if (!post.sampler)
{
glGenSamplers(1, &post.sampler);
glSamplerParameteri(post.sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glSamplerParameteri(post.sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glSamplerParameteri(post.sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glSamplerParameteri(post.sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glGenVertexArrays(1, &post.vertex_array);
}
return TRUE;
}
BOOL xgpu_post_anti_alias(BOOL smaa, GLuint framebuffer, unsigned long width, unsigned long height,
const GLint corners[4])
{
/* (made already, as the setting was chosen) */
if (!xgpu_post_prepare(smaa))
return FALSE;
textures_fit(smaa, width, height);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glDisable(GL_BLEND);
glDisable(GL_CULL_FACE);
glDisable(GL_POLYGON_OFFSET_FILL);
#ifndef HALO_ANDROID
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
#endif
glBindVertexArray(post.vertex_array);
/* the target's pixels, read while the target is drawn into */
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, post.color_framebuffer);
glBlitFramebuffer(corners[0], corners[1], corners[2], corners[3], corners[0], corners[1], corners[2], corners[3],
GL_COLOR_BUFFER_BIT, GL_NEAREST);
if (!smaa)
{
float bounds[4];
/* (split screen: no texel of the next window is read) */
bounds[0] = ((float)corners[0] + 0.5f) / (float)width;
bounds[1] = ((float)corners[1] + 0.5f) / (float)height;
bounds[2] = ((float)corners[2] - 0.5f) / (float)width;
bounds[3] = ((float)corners[3] - 0.5f) / (float)height;
texture_bind(0, post.color);
glUseProgram(post.programs[_post_program_fxaa]);
glUniform4fv(post.bounds, 1, bounds);
pass_draw(_post_program_fxaa, framebuffer, corners, TRUE);
}
else
{
intermediate_clear(post.edges_framebuffer);
texture_bind(0, post.color);
pass_draw(_post_program_smaa_edges, post.edges_framebuffer, corners, FALSE);
intermediate_clear(post.weights_framebuffer);
texture_bind(0, post.edges);
texture_bind(1, post.area);
texture_bind(2, post.search);
pass_draw(_post_program_smaa_weights, post.weights_framebuffer, corners, FALSE);
texture_bind(0, post.color);
texture_bind(1, post.weights);
pass_draw(_post_program_smaa_blend, framebuffer, corners, TRUE);
}
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
return TRUE;
}
+24
View File
@@ -0,0 +1,24 @@
Copyright (C) 2013 Jorge Jimenez (jorge@iryoku.com)
Copyright (C) 2013 Jose I. Echevarria (joseignacioechevarria@gmail.com)
Copyright (C) 2013 Belen Masia (bmasia@unizar.es)
Copyright (C) 2013 Fernando Navarro (fernandn@microsoft.com)
Copyright (C) 2013 Diego Gutierrez (diegog@unizar.es)
Permission is hereby granted, free of charge, to any person obtaining a copy
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software. As clarification, there is no
requirement that the copyright notice and permission be included in binary
distributions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+20
View File
@@ -0,0 +1,20 @@
# SMAA
Enhanced Subpixel Morphological Antialiasing, by Jorge Jimenez, Jose I.
Echevarria, Belen Masia, Fernando Navarro and Diego Gutierrez, MIT licensed
(see `LICENSE`).
Upstream: https://github.com/iryoku/smaa, commit
71c806a838bdd7d517df19192a20f0c61b3ca29d. `SMAA.hlsl` is copied
unchanged. `area_tex.zlib` and `search_tex.zlib` are the bytes of
`Textures/AreaTex.h` (160x560, two channels) and `Textures/SearchTex.h`
(64x16, one channel), compressed with zlib:
python -c "import re, sys, zlib; t = open(sys.argv[1]).read(); \
open(sys.argv[2], 'wb').write(zlib.compress(bytes(int(h, 16) for h in \
re.findall(r'0x([0-9a-fA-F]{2})', t[t.index('{'):])), 9))" AreaTex.h area_tex.zlib
`display.anti_aliasing = "smaa"` antialiases the 3D view with it, compiled
as GLSL (`port/linux/src/xgpu_post.c`); the files are embedded in the Linux
and Windows platform layers by `tools/embed_assets.py`. Android has FXAA in
its place.
+1361
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
Binary file not shown.
+7
View File
@@ -2082,6 +2082,13 @@ boolean ai_enemies_attacking_player(
return ai_enemies_endanger_player(TRUE, NONE);
}
/* port: whether enemies can see this player's unit */
boolean ai_port_enemies_can_see_unit(
long unit_index)
{
return ai_enemies_endanger_player(FALSE, unit_index);
}
/* port: whether enemies are attacking this player's unit */
boolean ai_port_enemies_attacking_unit(
long unit_index)
+3
View File
@@ -143,6 +143,9 @@ boolean ai_consider_major_upgrade(
real chance);
/* port: network co-op's respawn (players.c) */
/* port: whether enemies can see this player's unit */
boolean ai_port_enemies_can_see_unit(
long unit_index);
boolean ai_port_enemies_attacking_unit(
long unit_index);
boolean ai_enemies_attacking_player(
+2 -1
View File
@@ -3255,7 +3255,8 @@ void ai_scripting_attach_unit(
squad_index,
struct squad_definition);
if (squad_definition->actor_palette_index != NONE)
/* port: an entry the palette has (a map's index), as the others here */
if (VALID_INDEX(squad_definition->actor_palette_index, scenario->ai_actor_palette.count))
{
struct tag_reference *actor_palette_entry = TAG_BLOCK_GET_ELEMENT(
&scenario->ai_actor_palette,
+52 -9
View File
@@ -3006,9 +3006,9 @@ static void encounter_post_combat(
return;
}
/* port: extra_number, from 1, places one of network co-op's extra enemies
(coop_enemies.c): at the squad's starting locations in turn, each the
place a ring about its own (0: the squad's own actor, as the Xbox's) */
/* port: extra_number, counting from 1, places one of network co-op's extra
enemies (coop_enemies.c), spread around the squad's starting locations in
turn; 0 places the squad's own actor, as on the Xbox */
static boolean encounter_place_actor(
long encounter_index,
short squad_index,
@@ -3033,16 +3033,59 @@ static boolean encounter_place_actor(
short actor_palette_index = squad_definition->actor_palette_index;
struct scenario *scenario = global_scenario_get();
/* port: an extra enemy a ring about the starting location */
/* port: an extra enemy goes on free ground around its starting
location, else around the squad's others in turn */
if (extra_number > 0)
{
spread_location = *starting_location;
if (coop_enemies_spread_position(&starting_location->position,
(short)((extra_number - 1) / squad_definition->starting_locations.count + 1),
&spread_location.position))
/* (what was found taken around the starting locations while this
squad's extra enemies are placed, not tried again for the rest of
them: placing actors only takes room, and each enemy would try
every taken place again, and every place of a full location) */
static struct
{
starting_location = &spread_location;
long encounter_index;
long time;
short squad_index;
unsigned long full;
long taken[32][BIT_VECTOR_SIZE_IN_LONGS(COOP_ENEMIES_SPREAD_SPOTS)];
} full_locations = { NONE, NONE, NONE, 0 };
short count = squad_definition->starting_locations.count;
short tried;
if (extra_number == 1 || full_locations.encounter_index != encounter_index ||
full_locations.squad_index != squad_index || full_locations.time != game_time_get())
{
full_locations.encounter_index = encounter_index;
full_locations.squad_index = squad_index;
full_locations.time = game_time_get();
full_locations.full = 0;
csmemset(full_locations.taken, 0, sizeof(full_locations.taken));
}
for (tried = 0; tried < count; tried++)
{
short candidate_index = (short)((starting_location_index + tried) % count);
struct actor_starting_location *candidate = TAG_BLOCK_GET_ELEMENT(&squad_definition->starting_locations,
candidate_index, struct actor_starting_location);
if (candidate_index < 32 && TEST_FLAG(full_locations.full, candidate_index))
continue;
spread_location = *candidate;
if (coop_enemies_spread_position(&candidate->position, extra_number,
candidate_index < 32 ? full_locations.taken[candidate_index] : NULL, &spread_location.position))
break;
if (candidate_index < 32)
SET_FLAG(full_locations.full, candidate_index, TRUE);
}
/* (with no room anywhere, as extra enemies were placed before:
on rings around the squad's starting location in turn, else on
it) */
if (tried == count)
{
spread_location = *starting_location;
coop_enemies_fallback_position(&starting_location->position,
(short)((extra_number - 1) / MAX(count, 1) + 1), &spread_location.position);
}
starting_location = &spread_location;
}
if (starting_location->actor_variant_index != NONE)
+268 -2
View File
@@ -137,6 +137,16 @@ symbols in this file:
/* ---------- constants */
enum
{
/* port: a vertex or index buffer in a cache file (a D3DResource: Common,
Data, Lock), as cache_files_disable_writes counts them */
CACHE_FILE_BUFFER_SIZE = 12,
/* port: what cache_file_read rounds a read's size up to a multiple of
(cache_files_windows.c) */
CACHE_FILE_SECTOR_SIZE = 512,
};
/* ---------- macros */
#define STRUCTURE_BSP_TAG 'sbsp'
@@ -221,6 +231,18 @@ typedef char verify_cache_file_header_size[
static struct cache_file_tag_instance *cache_get_tag_instance(
long tag_index);
static boolean cache_file_region_contains(
void const *region,
unsigned long region_size,
void const *address,
long count,
long element_size);
static boolean cache_file_tag_header_verify(
struct cache_file_tag_header *tag_header,
long tag_data_size,
char const *scenario_name);
static boolean cache_file_structure_bsp_reference_verify(
struct scenario_structure_bsp_reference *reference);
/* ---------- globals */
@@ -270,6 +292,156 @@ static struct cache_file_tag_instance *cache_get_tag_instance(
return tag_instance;
}
/* port: whether count elements of element_size bytes at address all lie in
the region_size bytes at region (no elements always do): a map's pointers
and counts are checked so before anything follows them */
static boolean cache_file_region_contains(
void const *region,
unsigned long region_size,
void const *address,
long count,
long element_size)
{
unsigned long offset = (unsigned long)address - (unsigned long)region;
if (count == 0)
return TRUE;
return count > 0 &&
(unsigned long)address >= (unsigned long)region &&
offset <= region_size &&
(unsigned long)count <= (region_size - offset) / (unsigned long)element_size;
}
/* port: whether the tag header of the tags just read (tag_data_size bytes
at the tag cache's base) can be trusted, as everything after trusts it:
its tag table and vertex and index buffers lie in the tag data, its tags'
count fits a tag index's absolute index, and it names a scenario tag */
static boolean cache_file_tag_header_verify(
struct cache_file_tag_header *tag_header,
long tag_data_size,
char const *scenario_name)
{
char const *problem = NULL;
if (tag_header->signature != CACHE_FILE_TAG_HEADER_SIGNATURE)
{
problem = "signature";
}
else if (tag_header->tag_count <= 0 || tag_header->tag_count > UNSIGNED_SHORT_MAX)
{
problem = "tag count";
}
else if (!cache_file_region_contains(
tag_header,
tag_data_size,
tag_header->tag_instances,
tag_header->tag_count,
sizeof(struct cache_file_tag_instance)))
{
problem = "tag table";
}
else if (!cache_file_region_contains(
tag_header,
tag_data_size,
tag_header->vertex_buffers,
tag_header->vertex_buffer_count,
CACHE_FILE_BUFFER_SIZE))
{
problem = "vertex buffers";
}
else if (!cache_file_region_contains(
tag_header,
tag_data_size,
tag_header->index_buffers,
tag_header->index_buffer_count,
CACHE_FILE_BUFFER_SIZE))
{
problem = "index buffers";
}
else
{
long scenario_absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(tag_header->scenario_tag_index);
if (scenario_absolute_index >= tag_header->tag_count ||
tag_header->tag_instances[scenario_absolute_index].tag_index != tag_header->scenario_tag_index ||
tag_header->tag_instances[scenario_absolute_index].group_tag != SCENARIO_TAG)
{
problem = "scenario tag";
}
}
if (problem)
{
error(_error_silent, "the cache file '%s' is damaged: its tag header's %s is wrong", scenario_name, problem);
return FALSE;
}
return TRUE;
}
/* port: whether a structure bsp reference (the scenario's) may be loaded:
its bytes lie in the map and fit the tag cache after the tag data, where
they are read to (rounded up to whole sectors, as the read is), and it
names a structure bsp tag */
static boolean cache_file_structure_bsp_reference_verify(
struct scenario_structure_bsp_reference *reference)
{
byte *tag_cache_base_address = physical_memory_get_tag_cache_base_address();
long tag_data_size = cache_file_globals.header.tag_data_size;
long absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(reference->structure_bsp.index);
long read_size;
if (reference->file_offset < 0 ||
reference->file_size < (long)sizeof(struct cache_file_structure_bsp_header) ||
reference->file_size > TAG_CACHE_SIZE ||
reference->file_offset > cache_file_globals.header.file_length - reference->file_size)
{
error(
_error_silent,
"a structure bsp is damaged: %08x bytes at %08x, in %08x bytes",
reference->file_size,
reference->file_offset,
cache_file_globals.header.file_length);
return FALSE;
}
read_size = (reference->file_size + CACHE_FILE_SECTOR_SIZE - 1) & ~(CACHE_FILE_SECTOR_SIZE - 1);
if (!cache_file_region_contains(
tag_cache_base_address + tag_data_size,
TAG_CACHE_SIZE - tag_data_size,
reference->base_address,
read_size,
1))
{
error(
_error_silent,
"a structure bsp is damaged: its %08x bytes at %08x would load to %08lx, outside the tag cache",
reference->file_size,
reference->file_offset,
(unsigned long)reference->base_address);
return FALSE;
}
if (reference->structure_bsp.index == NONE ||
absolute_index >= cache_file_globals.tag_header->tag_count ||
global_tag_instances[absolute_index].tag_index != reference->structure_bsp.index ||
global_tag_instances[absolute_index].group_tag != STRUCTURE_BSP_TAG)
{
error(
_error_silent,
"a structure bsp is damaged: %08x is not a structure bsp tag",
reference->structure_bsp.index);
return FALSE;
}
return TRUE;
}
/* ---------- public code */
char const *cache_files_map_directory(
@@ -444,7 +616,9 @@ long tag_loaded(
long group_tag,
char const *name)
{
short absolute_index;
/* port: a long, as the tags' count is (a short wrapped on a count past
0x7FFF, and the walk never ended) */
long absolute_index;
long result = NONE;
if (cache_file_globals.tags_loaded)
@@ -651,6 +825,27 @@ boolean cache_file_header_verify(
return FALSE;
}
/* port: the map holds at least its header, and its tag data lies in it
and fits the tag cache, which it is read into whole (a size rounded up
to whole sectors still fits, the cache being whole sectors). Checked
without overflow: the offset and size are each checked first */
if (header->file_length < (long)sizeof(struct cache_file_header) ||
header->tag_data_offset < 0 ||
header->tag_data_size < 0 ||
header->tag_data_size > TAG_CACHE_SIZE ||
header->tag_data_offset > header->file_length - header->tag_data_size)
{
error(
_error_silent,
"the cache file '%s' is damaged: %08x bytes of tag data at %08x, in %08x bytes",
scenario_name,
header->tag_data_size,
header->tag_data_offset,
header->file_length);
return FALSE;
}
return TRUE;
}
@@ -846,6 +1041,26 @@ long scenario_tags_load(
SwitchToThread();
}
/* port: tags that did not all read, or whose header cannot be
trusted, are not loaded: the map is refused, as one whose header
is wrong is, and closed for the next to open */
if (read_complete != TRUE)
{
error(_error_silent, "the cache file '%s' could not be read", scenario_name);
cache_file_close();
return NONE;
}
if (!cache_file_tag_header_verify(
tag_cache_base_address,
cache_file_globals.header.tag_data_size,
scenario_name))
{
cache_file_close();
return NONE;
}
cache_file_globals.tag_header = tag_cache_base_address;
match_vassert(
"c:\\halo\\SOURCE\\cache\\cache_files.c",
@@ -885,6 +1100,11 @@ long scenario_tags_load(
}
result = cache_file_globals.tag_header->scenario_tag_index;
}
/* port: a map refused is closed for the next to open */
else
{
cache_file_close();
}
}
return result;
@@ -895,6 +1115,18 @@ boolean scenario_structure_bsp_load(
{
struct cache_file_tag_instance *tag_instance;
byte *tag_cache_base_address;
/* port: the bsp's header, once read and checked */
struct cache_file_structure_bsp_header *structure_bsp_header;
/* port: the tag data's size was checked as the map loaded
(cache_file_header_verify); the bsp's reference is the map's, and is
checked before anything is read where it says */
if (cache_file_globals.header.tag_data_size < 0 ||
cache_file_globals.header.tag_data_size > TAG_CACHE_SIZE ||
!cache_file_structure_bsp_reference_verify(reference))
{
return FALSE;
}
tag_cache_base_address = physical_memory_get_tag_cache_base_address();
csmemset(
@@ -919,9 +1151,43 @@ boolean scenario_structure_bsp_load(
sound_idle();
}
}
/* port: a bsp that did not all read, or whose header's pointers
leave what was read, is not loaded */
structure_bsp_header = reference->base_address;
if (read_complete != TRUE ||
structure_bsp_header->signature != CACHE_FILE_STRUCTURE_BSP_HEADER_SIGNATURE ||
!cache_file_region_contains(
reference->base_address,
reference->file_size,
structure_bsp_header->base_address,
1,
1) ||
!cache_file_region_contains(
reference->base_address,
reference->file_size,
structure_bsp_header->vertex_buffers,
structure_bsp_header->vertex_buffer_count,
CACHE_FILE_BUFFER_SIZE) ||
!cache_file_region_contains(
reference->base_address,
reference->file_size,
structure_bsp_header->index_buffers,
structure_bsp_header->index_buffer_count,
CACHE_FILE_BUFFER_SIZE))
{
error(
_error_silent,
"a structure bsp is damaged: its %08x bytes at %08x %s",
reference->file_size,
reference->file_offset,
read_complete != TRUE ? "could not be read" : "have a wrong header");
return FALSE;
}
}
cache_file_globals.structure_bsp_header = reference->base_address;
cache_file_globals.structure_bsp_header = structure_bsp_header;
match_assert(
"c:\\halo\\SOURCE\\cache\\cache_files.c",
0xE0,
+12 -2
View File
@@ -22,6 +22,14 @@ enum cached_map_file_precache_state
NUMBER_OF_CACHED_MAP_FILE_PRECACHE_STATES,
};
/* port: what a read's completion flag is set to when the read failed or came
up short (cache_file_read): not FALSE, so whatever waits on it stops
waiting, but not TRUE either, so a caller that checks can tell */
enum
{
_cache_file_read_failed = 2,
};
/* ---------- macros */
/* ---------- structures */
@@ -37,8 +45,10 @@ struct cache_file_structure_bsp_header;
struct tag_iterator
{
byte reserved0[4];
short absolute_index;
byte reserved6[10];
/* port: a long, as the tags' count is (a short wrapped on a count past
0x7FFF, and the walk never ended); the struct keeps its size */
long absolute_index;
byte reserved8[8];
long group_tag;
};
+37 -2
View File
@@ -755,6 +755,14 @@ static void cache_copy_initialize_file_data(
"c:\\halo\\SOURCE\\cache\\cache_files_decompress_windows.c",
964,
self->read_bytes_left>=sizeof(self->header));
/* port: a map that did not open, or is too short to hold a header, is a
bad file, not copied (the assertion goes on in a release build, and an
unopened file's size is -1, which it compared unsigned) */
if (self->source_file == INVALID_HANDLE_VALUE ||
self->read_bytes_left < (long)sizeof(self->header))
{
cache_copy_set_flag(_copy_bad_file_bit);
}
csmemset(self->overlapped_in_use_flags, 0, sizeof(self->overlapped_in_use_flags));
csmemset(self->overlapped_completed_flags, 0, sizeof(self->overlapped_completed_flags));
@@ -1610,6 +1618,25 @@ static void cache_copy_run_decompression(
if (self->write_requests_pending > 1)
decompressor_timer_stop(_decompressor_timer_zlib_during_write_file);
/* port: a stream that ends before the size the header gives the map
is a bad file: the rest of the cache file would be taken for it */
if (zlib_result == Z_STREAM_END &&
zlib_stream->total_out != (uLong)(self->header.size - sizeof(self->header)))
{
match_vassert(
"c:\\halo\\SOURCE\\cache\\cache_files_decompress_windows.c",
1248,
FALSE,
csprintf(
decompressor_globals.message,
"decompression ended after %lu of %ld bytes",
(unsigned long)zlib_stream->total_out,
self->header.size - (long)sizeof(self->header)));
cache_copy_set_flag(_copy_bad_file_bit);
break;
}
if (zlib_result == Z_OK || zlib_result == Z_STREAM_END)
{
if (!zlib_stream->avail_in)
@@ -1661,14 +1688,22 @@ static unsigned long __stdcall simple_cache_copy_thread(
cache_copy_initialize_read_buffers(self);
cache_copy_initialize_file_data(self);
if (!cache_copy_stop_requested())
/* port: nor is a bad file (cache_copy_initialize_file_data) read */
if (!cache_copy_stop_requested() && !(self->flags & ALL_COPY_FAILURE_FLAGS))
{
decompressor_timer_start(_decompressor_timer_setup);
cache_copy_initialize_read_data(self);
cache_copy_initialize_zlib(self);
decompressor_timer_stop(_decompressor_timer_setup);
if (cache_file_header_verify(&self->header, "cache decompressed", TRUE))
/* port: a header that is not a map's is a bad file, so the copy
fails (it ended as if it had worked, and the map was precached
again, or, of no size, seemed never to end) */
if (!cache_file_header_verify(&self->header, "cache decompressed", TRUE))
{
cache_copy_set_flag(_copy_bad_file_bit);
}
else
{
boolean keep_going = TRUE;
+59 -8
View File
@@ -427,7 +427,9 @@ static struct cache_file_runtime_globals cache_file_globals;
void tags_header_register_vertex_and_index_buffers(
struct cache_file_tag_header *header)
{
short index;
/* port: a long, as the buffers' counts are (a short wrapped on a count
past 0x7FFF, and the loop never ended) */
long index;
for (index = 0; index < header->vertex_buffer_count; index++)
{
@@ -450,7 +452,9 @@ void tags_header_register_vertex_and_index_buffers(
void tags_header_deregister_vertex_and_index_buffers(
struct cache_file_tag_header *header)
{
short index;
/* port: a long, as the buffers' counts are (a short wrapped on a count
past 0x7FFF, and the loop never ended) */
long index;
for (index = 0; index < header->vertex_buffer_count; index++)
{
@@ -480,7 +484,9 @@ void tags_header_deregister_vertex_and_index_buffers(
void structure_bsp_header_register_vertex_buffers(
struct cache_file_structure_bsp_header *header)
{
short index;
/* port: a long, as the buffers' counts are (a short wrapped on a count
past 0x7FFF, and the loop never ended) */
long index;
for (index = 0; index < header->vertex_buffer_count; index++)
{
@@ -504,7 +510,9 @@ void structure_bsp_header_register_vertex_buffers(
void structure_bsp_header_deregister_vertex_buffers(
struct cache_file_structure_bsp_header *header)
{
short index;
/* port: a long, as the buffers' counts are (a short wrapped on a count
past 0x7FFF, and the loop never ended) */
long index;
rasterizer_globals.current_lock_operation = _rasterizer_lock_bsp_switch;
@@ -590,11 +598,28 @@ boolean cache_files_precache_map_begin(
if (cache_file_read_header_from_dvd(cache_map_name, &header))
{
long buffer_size = cache_copy_buffer_size(copy_map);
void *buffer = texture_cache_steal_memory(buffer_size);
short map_file_index = cached_map_files_find_free_map(
header.file_length,
header.scenario_type);
struct cached_map_file *map_file = cached_map_file_get(map_file_index);
void *buffer;
struct cached_map_file *map_file;
/* port: a map no cache file holds (of no type the cache files are
for, or too big for its type's) is not precached; the texture
cache's memory is taken only once one does */
if (map_file_index == NONE)
{
error(_error_silent, "no cache file can hold map '%s' (%08x bytes, type %d)",
cache_map_name, header.file_length, header.scenario_type);
if (copy_map)
{
display_error_damaged_media();
}
return FALSE;
}
buffer = texture_cache_steal_memory(buffer_size);
map_file = cached_map_file_get(map_file_index);
memset(
&map_file->header,
@@ -757,6 +782,14 @@ boolean cache_file_open(
"c:\\halo\\SOURCE\\cache\\cache_files_windows.c",
224,
map_file_index!=NONE);
/* port: a map in no cache file is not opened (its index would be
NONE's, before the cache files) */
if (map_file_index == NONE)
{
error(_error_silent, "the map '%s' is in no cache file", scenario_name);
return FALSE;
}
memset(
cache_file_globals.requests,
0,
@@ -797,6 +830,16 @@ short cache_file_read(
"c:\\halo\\SOURCE\\cache\\cache_files_windows.c",
276,
offset>=0);
/* port: the offset and size are the map's (a texture's or sound's,
which nothing checks): a negative one is no read, failed at once (a
negative size read the file to its end over what follows the buffer) */
if (offset < 0 || size < 0)
{
error(_error_silent, "cache file read of %08x bytes at %08x refused", size, offset);
*completion_flag_reference = _cache_file_read_failed;
return NONE;
}
if (size & (CACHE_FILE_SECTOR_SIZE - 1))
{
size = (size | (CACHE_FILE_SECTOR_SIZE - 1)) + 1;
@@ -1056,7 +1099,13 @@ static void CALLBACK cache_file_read_io_completion_routine(
"c:\\halo\\SOURCE\\cache\\cache_files_windows.c",
1389,
finished_request->overlapped.hEvent);
*(volatile boolean *)finished_request->overlapped.hEvent = TRUE;
/* port: a read that failed or came up short completes as failed: what
waits on it stops waiting, and a caller that checks (the map's tags and
bsps) sees it, instead of taking what is in its buffer for the map */
*(volatile boolean *)finished_request->overlapped.hEvent =
error_code == ERROR_SUCCESS && bytes_transferred == (unsigned long)finished_request->size ?
TRUE :
_cache_file_read_failed;
finished_request->pending = FALSE;
finished_request->running = FALSE;
@@ -1222,9 +1271,11 @@ static short cached_map_files_find_free_map(
* arm's assertion failure calls system_exit, which does not return in January
* (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit).
* Source-policy approval pending (2026-09-27 audit). */
/* port: the type is the map's header's, and a release build's assertion
goes on: a type no cache file is for finds none */
default:
match_vassert("c:\\halo\\SOURCE\\cache\\cache_files_windows.c", 1172, FALSE, NULL);
break;
return NONE;
}
for (map_file_index = first_map_file_index;
-1
View File
@@ -46,7 +46,6 @@ symbols in this file:
#define GAME_STATE_SIZE HALO_PORT_GAME_STATE_SIZE
#define GAME_STATE_VERIFY_SIZE HALO_PORT_GAME_STATE_CPU_SIZE
#define TAG_CACHE_BASE_ADDRESS 0x803A6000
#define TAG_CACHE_SIZE 0x1600000
#define TEXTURE_CACHE_SIZE 0x1600000
#define SOUND_CACHE_SIZE 0x400000
+4
View File
@@ -10,6 +10,10 @@ header included in hcex build.
/* ---------- constants */
/* port: the tag cache's size, which the loader checks a map's tag data and
structure bsps against (cache_files.c) */
#define TAG_CACHE_SIZE 0x1600000
/* ---------- macros */
/* ---------- structures */
+5 -1
View File
@@ -277,6 +277,11 @@ void error(
va_end(argument_list);
csstrcat(string, "\r\n");
/* (port: the file first: printing used to halt on a long line
while this call still held the lock, and the reason never reached
debug.txt) */
write_to_error_file(string, TRUE);
/* (port: on screen as config.toml's game.console_log says: an
assert that stops the game, and what a command someone typed
logs (its answer), always; the rest, the game's chatter.
@@ -287,7 +292,6 @@ void error(
{
terminal_printf(global_real_argb_white, "%s", string);
}
write_to_error_file(string, TRUE);
new_size = csstrlen(string);
if (error_globals.message_buffer_size+new_size >= ERROR_MESSAGE_BUFFER_MAXIMUM_SIZE)
+13 -2
View File
@@ -1189,10 +1189,18 @@ void profile_render_end(
return;
}
/* port: the texture timer is read only by the profile graph and the frame
dumps; a texture set per draw, it is not timed while nothing reads it */
#define PROFILE_TEXTURE_TIMED() \
(profile_global_enable || profile_graph || profile_dump_frames || profile_dump_lost_frames)
void profile_texture_start(
void)
{
profile_timesection_begin_now(&profile_globals.current_frame.texture);
if (PROFILE_TEXTURE_TIMED())
{
profile_timesection_begin_now(&profile_globals.current_frame.texture);
}
return;
}
@@ -1200,7 +1208,10 @@ void profile_texture_start(
void profile_texture_end(
void)
{
profile_timesection_end_now(&profile_globals.current_frame.texture);
if (PROFILE_TEXTURE_TIMED())
{
profile_timesection_end_now(&profile_globals.current_frame.texture);
}
return;
}
+2 -2
View File
@@ -577,10 +577,10 @@ long player_aim_projectile(
if (halo_vr_active() && player->local_player_index==0)
{
struct unit_datum *vr_unit= unit_get(player->unit_index);
float vr_eyes[3], vr_heading[3];
float vr_eyes[3], vr_neck[3], vr_heading[3];
if (vr_unit->object.parent_object_index!=NONE &&
halo_vr_body_frame(player->local_player_index, vr_eyes, vr_heading))
halo_vr_body_frame(player->local_player_index, vr_eyes, vr_neck, vr_heading))
{
/* VR, seated: as the game aims a vehicle's gun, at what lies
along the player's aim, from the eyes (vr.c draws the crosshair
+96 -22
View File
@@ -479,6 +479,10 @@ static struct
long bsp_switch_time;
/* the structure BSPs the players have been in this map, by bit */
word visited_structure_bsps;
/* when each local player was last told they wait for the team to go
back, and the count they were told (players_coop_show_backtrack_wait) */
long backtrack_message_times[MAXIMUM_LOCAL_PLAYERS];
short backtrack_message_counts[MAXIMUM_LOCAL_PLAYERS];
/* where each player last stood on the ground, and on which BSP: where
everyone comes back when the last checkpoint was on another one */
short ground_structure_bsp_index;
@@ -1310,6 +1314,16 @@ long find_best_starting_location_index(
}
}
/* port: a multiplayer map played alone (New Game's MULTIPLAYER maps:
no game engine) has no starting location for no game type, every one
being for its game types: the player starts at any of them, not at none
(outside the map, with no pause menu) */
if (best_starting_location_index == NONE && !game_engine_running() && starting_location_count > 0)
{
best_starting_location_index = (short)PIN(
(short)(real_random_range(0.0f, 1.0f) * starting_location_count), 0, starting_location_count - 1);
}
return best_starting_location_index;
}
@@ -2781,9 +2795,13 @@ waits for every machine to have loaded it */
#define COOP_BSP_SWITCH_WAIT_TICKS (10 * TICKS_PER_SECOND)
/* Going back to a BSP the team has already been in needs two thirds of the
living players at the trigger: inside it, or this near the player in it
(world units) */
#define COOP_BACKTRACK_GATHER_DISTANCE 4.0f
living players with the player on the trigger: inside it, or this near
them (world units, about 45 metres). Wide enough for a team walking through
a doorway in a line, not for one player who ran back alone. */
#define COOP_BACKTRACK_GATHER_DISTANCE 15.0f
/* how often a player waiting for the team to go back is reminded */
#define COOP_BACKTRACK_MESSAGE_TICKS (3 * TICKS_PER_SECOND)
/* Co-op host, each tick: a player stranded for COOP_STRANDED_TICKS is moved
beside a grounded teammate inside the BSP, else any teammate inside it, else
@@ -2970,9 +2988,9 @@ void players_respawn_at_checkpoint(
return;
}
/* port: whether the unit stands in a trigger that switches away from the
loaded structure BSP */
static boolean players_coop_in_bsp_switch_trigger(
/* port: the trigger that switches away from the loaded structure BSP the
unit stands in, or NONE */
static short players_coop_bsp_switch_trigger(
long unit_index)
{
struct scenario *scenario = global_scenario_get();
@@ -2986,11 +3004,11 @@ static boolean players_coop_in_bsp_switch_trigger(
if (volume->source_structure_bsp_index == global_structure_bsp_index &&
scenario_trigger_volume_test_object(volume->trigger_volume_index, unit_index))
{
return TRUE;
return index;
}
}
return FALSE;
return NONE;
}
/* port: whether a network co-op host lets any trigger switch the BSP yet.
@@ -3019,14 +3037,16 @@ static boolean players_coop_bsp_switch_ready(
return TRUE;
}
/* port: whether a network co-op host lets the unit on this trigger switch
the BSP, which brings the whole team along. Not while the unit's machine
is still loading the BSP the host has. Anyone can lead the team into a BSP
it hasn't been in; going back to one needs two thirds of the team there,
so one player can't drag everyone back through the level. */
static boolean players_coop_bsp_switch_allowed(
/* port: whether this trigger leads back to a BSP the team has been in and
the unit on it must wait for more of the team: two thirds of the living
players must be with it (in the trigger, or within
COOP_BACKTRACK_GATHER_DISTANCE), so one player can't drag everyone back
through the level. Also how many are there and how many it needs. */
static boolean players_coop_backtrack_wait(
short bsp_switch_trigger_volume_index,
long unit_index)
long unit_index,
short *gathered_count,
short *needed_count)
{
struct scenario_bsp_switch_trigger_volume *volume = TAG_BLOCK_GET_ELEMENT(
&global_scenario_get()->bsp_switch_trigger_volumes, bsp_switch_trigger_volume_index,
@@ -3035,14 +3055,13 @@ static boolean players_coop_bsp_switch_allowed(
struct data_iterator iterator;
struct player_datum *player;
short living_count = 0;
short gathered_count = 0;
if (!network_coop_player_has_structure_bsp(unit_get(unit_index)->unit.player_index))
return FALSE;
*gathered_count = 0;
*needed_count = 0;
if (!VALID_INDEX(volume->destination_structure_bsp_index, MAXIMUM_STRUCTURE_BSPS_PER_SCENARIO) ||
!TEST_FLAG(players_coop_state.visited_structure_bsps, volume->destination_structure_bsp_index))
{
return TRUE;
return FALSE;
}
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
@@ -3056,11 +3075,64 @@ static boolean players_coop_bsp_switch_allowed(
distance_squared3d(center, &teammate->object.bounding_sphere_center) <=
COOP_BACKTRACK_GATHER_DISTANCE * COOP_BACKTRACK_GATHER_DISTANCE)
{
gathered_count++;
(*gathered_count)++;
}
}
/* (two thirds, rounded up) */
*needed_count = (short)((living_count * 2 + 2) / 3);
return gathered_count * 3 >= living_count * 2;
return *gathered_count < *needed_count;
}
/* port: whether a network co-op host lets the unit on this trigger switch
the BSP, which brings the whole team along: not while the unit's machine is
still loading the host's BSP, nor back to one the team has been in until
enough of it is there (players_coop_backtrack_wait) */
static boolean players_coop_bsp_switch_allowed(
short bsp_switch_trigger_volume_index,
long unit_index)
{
short gathered_count;
short needed_count;
return network_coop_player_has_structure_bsp(unit_get(unit_index)->unit.player_index) &&
!players_coop_backtrack_wait(bsp_switch_trigger_volume_index, unit_index, &gathered_count, &needed_count);
}
/* port: tells each of this machine's co-op players held back on a trigger
how many of the team are there and how many it needs, at once and every
COOP_BACKTRACK_MESSAGE_TICKS while they wait. Every machine works this out
for its own players, as the host does for its decision. */
static void players_coop_show_backtrack_wait(
void)
{
short local_player_index;
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
{
long player_index = local_player_get_player_index(local_player_index);
long unit_index = player_index != NONE ? player_get(player_index)->unit_index : NONE;
short trigger_index = unit_index != NONE ? players_coop_bsp_switch_trigger(unit_index) : NONE;
long *time = &players_coop_state.backtrack_message_times[local_player_index];
short *count = &players_coop_state.backtrack_message_counts[local_player_index];
short gathered_count;
short needed_count;
wchar_t text[64];
if (trigger_index == NONE ||
!players_coop_backtrack_wait(trigger_index, unit_index, &gathered_count, &needed_count))
{
*time = 0;
continue;
}
if (*time != 0 && *count == gathered_count && game_time_get() - *time < COOP_BACKTRACK_MESSAGE_TICKS)
continue;
*time = game_time_get();
*count = gathered_count;
usnprintf(text, NUMBEROF(text), L"Waiting for your team to go back: %d of %d here",
gathered_count, needed_count);
hud_print_message(local_player_index, text);
}
}
static void player_teleport_on_bsp_switch(
@@ -3253,7 +3325,7 @@ void players_reconnect_to_structure_bsp(
struct object_datum *unit = object_try_and_get(player->unit_index);
if (unit && scenario_leaf_index_from_point(&unit->object.bounding_sphere_center) != NONE &&
!players_coop_in_bsp_switch_trigger(player->unit_index))
players_coop_bsp_switch_trigger(player->unit_index) == NONE)
{
source_unit_index = player->unit_index;
teleport_position = unit->object.position;
@@ -4633,6 +4705,8 @@ void players_update_after_game(
players_coop_state.structure_bsp_index = global_structure_bsp_index;
players_coop_state.bsp_switch_time = game_time_get();
}
if (network_coop_active())
players_coop_show_backtrack_wait();
data_iterator_new(&iterator, player_data);
while (player = data_iterator_next(&iterator))
+95 -16
View File
@@ -2787,6 +2787,7 @@ symbols in this file:
#include "ai/ai.h"
#include "ai/ai_debug.h"
#include "cache/cache_files.h"
#include "cache/physical_memory_map.h" /* port: hs_scenario_syntax_data_valid */
#include "cache/texture_cache.h"
#include "camera/camera_scripting.h"
#include "cutscene/cinematics.h"
@@ -2837,6 +2838,7 @@ symbols in this file:
#include "main/main.h"
#include "units/units.h"
#include "units/vehicles.h"
#include "coop_scripts.h" /* port: port/linux/game/coop_scripts.c */
/* ---------- constants */
@@ -3606,6 +3608,10 @@ boolean hs_scenario_merge(
struct scenario *source_scenario);
static void hs_allocate(
void);
static boolean hs_scenario_syntax_data_valid(
struct scenario const *scenario);
static void hs_scenario_scripts_disable(
struct scenario *scenario);
static boolean hs_rebuild_source(
void);
static boolean hs_compile_source(
@@ -12093,19 +12099,81 @@ boolean hs_scenario_merge(
return success;
}
/* port: the scenario's script data as the map holds it, the header of its
data array too (which hs_scenario_postprocess and the data array code go by,
the nodes' count above all): inside the tag cache, and the data array of
script nodes it has room for. The shipped maps' all are, of 19001 nodes
(their names differ: a30's is "static script node"). */
static boolean hs_scenario_syntax_data_valid(
struct scenario const *scenario)
{
long const syntax_data_size =
sizeof(struct data_array)+MAXIMUM_HS_SYNTAX_NODES_PER_SCENARIO*sizeof(struct hs_syntax_node);
byte const *tag_cache = (byte const *)physical_memory_get_tag_cache_base_address();
byte const *address = (byte const *)scenario->hs_syntax_data.address;
struct data_array const *data = (struct data_array const *)address;
if (scenario->hs_syntax_data.size != syntax_data_size ||
!tag_cache ||
address < tag_cache ||
address > tag_cache+TAG_CACHE_SIZE-syntax_data_size ||
((unsigned long)address & 3))
{
return FALSE;
}
return data->signature == 'd@t@' &&
data->maximum_count == MAXIMUM_HS_SYNTAX_NODES_PER_SCENARIO &&
data->size == sizeof(struct hs_syntax_node) &&
data->valid &&
data->count >= 0 &&
data->count <= data->maximum_count &&
data->actual_count >= 0 &&
data->actual_count <= data->count &&
data->first_free_absolute_index >= 0 &&
data->first_free_absolute_index <= data->maximum_count;
}
/* port: the scenario runs no scripts, its script data not being sound: a
cache file's blocks can't be resized (tag_block_resize), so the counts are
let go of in place, and no global is initialized or script thread started
(hs_runtime_initialize_for_new_map) against nodes that aren't there */
static void hs_scenario_scripts_disable(
struct scenario *scenario)
{
scenario->hs_scripts.count = 0;
scenario->hs_globals.count = 0;
return;
}
static void hs_allocate(
void)
{
struct scenario *scenario;
scenario = global_scenario_index != NONE ? global_scenario_get() : NULL;
/* port: as the map holds it only when it is sound */
if (scenario &&
scenario->hs_syntax_data.size ==
sizeof(struct data_array)+MAXIMUM_HS_SYNTAX_NODES_PER_SCENARIO*sizeof(struct hs_syntax_node))
sizeof(struct data_array)+MAXIMUM_HS_SYNTAX_NODES_PER_SCENARIO*sizeof(struct hs_syntax_node) &&
hs_scenario_syntax_data_valid(scenario))
{
return;
}
/* port: script data a map holds that isn't sound is the map's, in the
tag cache: it isn't freed or replaced (what the Xbox did here, freeing
tag memory), the map runs no scripts, and an array of the port's own,
made once, stands in for it */
if (scenario)
{
hs_scenario_scripts_disable(scenario);
if (hs_syntax_data && hs_syntax_data_allocated)
return;
error(0, "the scenario's script data is missing or damaged; its scripts won't run");
}
hs_syntax_data = data_new(
"script node",
MAXIMUM_HS_SYNTAX_NODES_PER_SCENARIO,
@@ -12113,19 +12181,7 @@ static void hs_allocate(
if (hs_syntax_data)
{
data_make_valid(hs_syntax_data);
if (scenario)
{
match_free("c:\\halo\\SOURCE\\hs\\hs.c", 336, scenario->hs_syntax_data.address);
scenario->hs_syntax_data.address = hs_syntax_data;
scenario->hs_syntax_data.size =
sizeof(struct data_array)+MAXIMUM_HS_SYNTAX_NODES_PER_SCENARIO*sizeof(struct hs_syntax_node);
tag_data_resize(&scenario->hs_string_constants, 0x400);
tag_block_resize(&scenario->hs_scripts, 0);
}
else
{
hs_syntax_data_allocated = TRUE;
}
hs_syntax_data_allocated = TRUE;
}
else
{
@@ -12542,8 +12598,17 @@ struct hs_external_global_definition *hs_global_external_get(
short hs_global_get_type(
short global_index)
{
/* port: a global that isn't there (a map's syntax node names it) has
no type */
if (global_index & 0x8000)
{
if ((global_index & 0x7FFF) >= hs_external_global_count)
return _hs_unparsed;
return hs_global_external_get(global_index & 0x7FFF)->type;
}
if ((global_index & 0x7FFF) >= global_scenario_get()->hs_globals.count)
return _hs_unparsed;
return TAG_BLOCK_GET_ELEMENT(
&global_scenario_get()->hs_globals,
@@ -12964,7 +13029,8 @@ static long alphabetize(
return _stricmp(*left, *right);
}
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(game_safe_to_save_evaluate, game_safe_to_save)
/* port: in network co-op, once any player is safe (coop_scripts.c) */
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(game_safe_to_save_evaluate, coop_scripts_safe_to_save)
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(game_all_quiet_evaluate, game_all_quiet)
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(game_safe_to_speak_evaluate, game_safe_to_speak)
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(game_is_cooperative_evaluate, game_is_cooperative)
@@ -13782,7 +13848,8 @@ HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(player_control_action_test_look_relative
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(player_control_action_test_look_relative_right_evaluate, player_control_action_test_look_relative_right)
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(player_control_action_test_look_relative_all_directions_evaluate, player_control_action_test_look_relative_all_directions)
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(player_control_action_test_move_relative_all_directions_evaluate, player_control_action_test_move_relative_all_directions)
HS_EVALUATE_VOID_FROM_ARGUMENTS(player_add_equipment_evaluate, struct hs_arguments_long_word_boolean, (player_add_equipment(arguments->value0, arguments->value1, arguments->value2)))
/* port: in network co-op, also for the players the scripts can't name (coop_scripts.c) */
HS_EVALUATE_VOID_FROM_ARGUMENTS(player_add_equipment_evaluate, struct hs_arguments_long_word_boolean, (coop_scripts_player_add_equipment(arguments->value0, arguments->value1, arguments->value2)))
HS_EVALUATE_VOID_FROM_ARGUMENTS(debug_player_teleport_evaluate, struct hs_arguments_short_word, (debug_player_teleport(arguments->value0, arguments->value1)))
HS_EVALUATE_VOID_STRING(main_set_map_name_evaluate, main_set_map_name)
HS_EVALUATE_VOID_STRING(main_set_multiplayer_map_name_evaluate, main_set_multiplayer_map_name)
@@ -13961,6 +14028,15 @@ boolean hs_scenario_postprocess(
scenario = global_scenario_get();
saved_syntax_data = hs_syntax_data;
hs_allocate();
/* port: script data that isn't sound isn't gone through (hs_allocate
has an empty array stand in for it, and the map runs no scripts) */
if (!hs_scenario_syntax_data_valid(scenario))
{
if (restore_syntax_data)
hs_syntax_data = saved_syntax_data;
return FALSE;
}
recompile = scenario->hs_scripts.count == 0 && scenario->hs_source_files.count>0;
hs_syntax_data = (struct data_array *)scenario->hs_syntax_data.address;
hs_syntax_data->data = (char *)hs_syntax_data+sizeof(struct data_array);
@@ -13996,6 +14072,9 @@ boolean hs_scenario_postprocess(
!tag_data_resize(&global_scenario_get()->hs_string_constants, 0x400))
{
error(0, "couldn't reset scripts.");
/* port: a cache file's can't be resized: none run against the
nodes just deleted */
hs_scenario_scripts_disable(scenario);
}
success = FALSE;
}
+5 -1
View File
@@ -2486,8 +2486,12 @@ boolean hs_compile_postprocess(
long predicate_index = hs_syntax_get(expression_index)->data;
struct hs_syntax_node *predicate;
/* port: a predicate that isn't a node (a map's) is as corrupt
as one that isn't a function name, where hs_syntax_get's NULL
was gone through */
if (predicate_index == NONE ||
(predicate = hs_syntax_get(predicate_index))->type != _hs_function_name)
!(predicate = datum_try_and_get(hs_syntax_data, predicate_index)) ||
predicate->type != _hs_function_name)
{
hs_compile_globals.error = "corrupt syntax tree (you need to recompile scripts.)";
success = FALSE;
+6 -1
View File
@@ -105,6 +105,7 @@ symbols in this file:
#include "units/units.h"
#include "object_lists.h"
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
#include "coop_scripts.h" /* port: port/linux/game/coop_scripts.c */
/* ---------- constants */
@@ -844,10 +845,12 @@ boolean hs_trigger_volume_test_objects_all(
short trigger_volume_index,
long object_list_index)
{
/* port: in network co-op, waiting for every player means waiting for
any one of them (coop_scripts.c) */
return hs_trigger_volume_test_objects(
trigger_volume_index,
object_list_index,
TRUE);
!coop_scripts_any_player_will_do(object_list_index));
}
boolean hs_trigger_volume_test_objects_any(
@@ -986,6 +989,8 @@ void hs_object_teleport(
short cutscene_flag_index)
{
hs_object_orient(object_index, cutscene_flag_index, TRUE, TRUE);
/* port: co-op players the scripts can't name go with player0 */
coop_scripts_teleport_followers(object_index);
return;
}
+119 -8
View File
@@ -315,6 +315,9 @@ enum
{
_hs_thread_in_function_call_bit = 0,
_hs_thread_sleeping_bit,
/* port: the thread's stack overflowed (hs_stack_overflow); hs_thread_main
ends it */
_hs_thread_stack_overflow_bit,
_hs_global_external_bit = 15,
};
@@ -494,7 +497,9 @@ static void hs_thread_delete(
static long hs_thread_new(
short type,
long script_index);
static void hs_stack_push(
static boolean hs_stack_push(
long thread_index);
static void hs_stack_overflow(
long thread_index);
static void *hs_stack_allocate(
long thread_index,
@@ -1304,7 +1309,7 @@ static void hs_thread_delete(
return;
}
static void hs_stack_push(
static boolean hs_stack_push(
long thread_index)
{
struct hs_thread_datum *thread = hs_thread_get(thread_index);
@@ -1315,10 +1320,39 @@ static void hs_stack_push(
(byte *) (new_frame+1)<thread->stack_data+HS_THREAD_STACK_SIZE,
"stack overflow.");
/* port: a frame past the thread's stack isn't pushed (it would be
written over the next thread's datum): the thread is ended instead */
if ((byte *)(new_frame+1)>=thread->stack_data+HS_THREAD_STACK_SIZE ||
TEST_FLAG(thread->flags, _hs_thread_stack_overflow_bit))
{
hs_stack_overflow(thread_index);
return FALSE;
}
new_frame->previous = thread->stack;
thread->stack = new_frame;
new_frame->size = 0;
return TRUE;
}
/* port: a script whose stack overflows is ended rather than let write past
its thread's stack: the push or allocation is refused (its caller returns),
and hs_thread_main lets the thread's frames go once the evaluation in hand
returns */
static void hs_stack_overflow(
long thread_index)
{
struct hs_thread_datum *thread = hs_thread_get(thread_index);
if (!TEST_FLAG(thread->flags, _hs_thread_stack_overflow_bit))
{
error(_error_silent, "a problem occurred while executing the script %s: stack overflow. (it was stopped)",
hs_thread_format(thread_index));
SET_FLAG(thread->flags, _hs_thread_stack_overflow_bit, TRUE);
}
return;
}
@@ -1836,6 +1870,10 @@ void hs_evaluate_begin(
match_assert("c:\\halo\\source\\hs\\hs_library_internal_runtime.h", 0x15,
function_index==_hs_function_begin);
/* port: none on a stack overflow (hs_stack_overflow) */
if (!expression_index || !result)
return;
if (initialize)
{
*expression_index = hs_syntax_get(hs_syntax_get(
@@ -1963,6 +2001,10 @@ void hs_evaluate_logical(
match_assert("c:\\halo\\source\\hs\\hs_library_internal_runtime.h", 0xcf,
function_index==_hs_function_and || function_index==_hs_function_or);
/* port: none on a stack overflow (hs_stack_overflow) */
if (!expression_index || !value || !result)
return;
if (initialize)
{
*expression_index = hs_syntax_get(hs_syntax_get(
@@ -2014,6 +2056,10 @@ void hs_evaluate_if(
match_assert("c:\\halo\\source\\hs\\hs_library_internal_runtime.h", 0x77,
function_index==_hs_function_if);
/* port: none on a stack overflow (hs_stack_overflow) */
if (!condition || !expression_index || !result)
return;
if (initialize)
{
*condition = 0;
@@ -2064,7 +2110,9 @@ void hs_evaluate_set(
short type;
long global_index;
hs_stack_allocate(thread_index, sizeof(long));
/* port: none on a stack overflow (hs_stack_overflow) */
if (!hs_stack_allocate(thread_index, sizeof(long)))
return;
type = hs_global_get_type((short)variable->data);
if (initialize)
@@ -2107,6 +2155,10 @@ void hs_evaluate_inspect(
match_assert("c:\\halo\\source\\hs\\hs_library_internal_runtime.h", 0x2bc,
function_index==_hs_function_inspect);
/* port: none on a stack overflow (hs_stack_overflow) */
if (!value)
return;
if (initialize)
{
hs_evaluate(thread_index, hs_syntax_get(hs_syntax_get(
@@ -2146,6 +2198,10 @@ void hs_evaluate_arithmetic(
real *result = hs_stack_allocate(thread_index, sizeof(real));
long result_long;
/* port: none on a stack overflow (hs_stack_overflow) */
if (!argument_index || !expression_index || !value || !result)
return;
if (initialize)
{
*argument_index = 0;
@@ -2219,6 +2275,10 @@ void hs_evaluate_object_cast_up(
function_index>=_hs_function_object_to_unit &&
function_index<=_hs_function_object_to_unit);
/* port: none on a stack overflow (hs_stack_overflow) */
if (!object_index)
return;
if (initialize)
{
hs_evaluate(thread_index, hs_syntax_get(hs_syntax_get(
@@ -2264,6 +2324,10 @@ void hs_evaluate_begin_random(
match_assert("c:\\halo\\source\\hs\\hs_library_internal_runtime.h", 0x45,
function_index==_hs_function_begin_random);
/* port: none on a stack overflow (hs_stack_overflow) */
if (!argument_count || !evaluated || !result)
return;
if (initialize)
{
long expression_index = hs_syntax_get(hs_syntax_get(
@@ -2318,6 +2382,10 @@ void hs_evaluate_debug_string(
(function_index>=_hs_function_debug_string__first) &&
(function_index<=_hs_function_debug_string__last));
/* port: none on a stack overflow (hs_stack_overflow) */
if (!expression_index || !argument_count || !arguments)
return;
if (initialize)
{
*expression_index = hs_syntax_get(hs_syntax_get(
@@ -2384,6 +2452,10 @@ void hs_evaluate_sleep_until(
match_assert("c:\\halo\\source\\hs\\hs_library_internal_runtime.h", 0x1e5,
function_index==_hs_function_sleep_until);
/* port: none on a stack overflow (hs_stack_overflow) */
if (!condition || !ticks || !timeout || !start_time || !argument_index)
return;
if (initialize)
{
*(boolean *)condition = FALSE;
@@ -2455,6 +2527,10 @@ void hs_evaluate_sleep(
match_assert("c:\\halo\\source\\hs\\hs_library_internal_runtime.h", 0x189,
function_index==_hs_function_sleep);
/* port: none on a stack overflow (hs_stack_overflow) */
if (!ticks || !script_index || !argument_index)
return;
if (initialize)
{
hs_evaluate(thread_index, hs_syntax_get(hs_syntax_get(
@@ -2524,6 +2600,7 @@ static void hs_thread_main(
{
struct hs_thread_datum *thread = hs_thread_get(thread_index);
struct hs_script *script = NULL;
long *root_result;
hs_runtime_globals.executing_thread_index = (short)thread_index;
if (thread->type==_hs_thread_type_script)
@@ -2547,8 +2624,10 @@ static void hs_thread_main(
match_assert("c:\\halo\\SOURCE\\hs\\hs_runtime.c", 0x2c3, script);
thread->stack->size = 0;
hs_evaluate(thread_index, script->root_expression_index,
hs_stack_allocate(thread_index, sizeof(long)));
/* port: (the base frame, just emptied, has room) */
root_result = hs_stack_allocate(thread_index, sizeof(long));
if (root_result)
hs_evaluate(thread_index, script->root_expression_index, root_result);
}
while (thread->stack!=(struct hs_stack_frame *)thread->stack_data &&
@@ -2577,6 +2656,15 @@ static void hs_thread_main(
{
hs_script_evaluate(expression->index, thread_index, initialize);
}
/* port: a stack overflow (hs_stack_overflow) ends the thread: its
frames are let go, and a script's thread doesn't run again */
if (TEST_FLAG(thread->flags, _hs_thread_stack_overflow_bit))
{
thread->stack = (struct hs_stack_frame *)thread->stack_data;
thread->flags = 0;
thread->sleep_until = thread->type==_hs_thread_type_script ? NONE : 0;
}
}
if (thread->stack==(struct hs_stack_frame *)thread->stack_data)
@@ -2612,6 +2700,10 @@ static void hs_script_evaluate(
struct hs_thread_datum *thread = hs_thread_get(thread_index);
long *result = hs_stack_allocate(thread_index, sizeof(long));
/* port: none on a stack overflow (hs_stack_overflow) */
if (!result)
return;
if (initialize)
hs_evaluate(thread_index, script->root_expression_index, result);
else
@@ -2636,6 +2728,17 @@ static void *hs_stack_allocate(
frame->data+frame->size+size<=thread->stack_data+HS_THREAD_STACK_SIZE,
"stack overflow.");
/* port: none past the thread's stack (hs_stack_overflow); the caller
returns on NULL */
if (size<0 ||
size>thread->stack_data+HS_THREAD_STACK_SIZE-(frame->data+frame->size) ||
TEST_FLAG(thread->flags, _hs_thread_stack_overflow_bit))
{
hs_stack_overflow(thread_index);
return NULL;
}
result = frame->data+frame->size;
frame->size += (short)size;
@@ -2676,9 +2779,12 @@ static void hs_evaluate(
else
{
thread->stack->result = destination;
hs_stack_push(thread_index);
SET_FLAG(thread->flags, _hs_thread_in_function_call_bit, TRUE);
thread->stack->expression_index = expression_index;
/* port: not pushed on a stack overflow (hs_stack_overflow) */
if (hs_stack_push(thread_index))
{
SET_FLAG(thread->flags, _hs_thread_in_function_call_bit, TRUE);
thread->stack->expression_index = expression_index;
}
}
return;
@@ -3132,6 +3238,11 @@ static long *hs_arguments_evaluate(
short *argument_index = hs_stack_allocate(thread_index, sizeof(short));
long *expression_index = hs_stack_allocate(thread_index, sizeof(long));
/* port: none on a stack overflow (hs_stack_overflow): as for arguments
still being evaluated, the caller returns on NULL */
if (!values || !argument_index || !expression_index)
return NULL;
if (initialize)
{
*argument_index = 0;
+248 -176
View File
@@ -126,6 +126,7 @@ symbols in this file:
#include "units/units.h"
#ifdef HALO_ANDROID
#include "halo_vr.h"
#include "vr_arms.h"
#endif
/* ---------- constants */
@@ -467,6 +468,26 @@ long first_person_weapon_get_local_index(
}
#ifdef HALO_ANDROID
int config_boolean(char const *name);
void model_vr_log_skin(long model_index);
/* VR: the headset's frame each local player's pose was last built for
from the controller (halo_vr_frame_serial; 0 when not), and its weapon */
static unsigned long first_person_weapon_vr_pose_serial[MAXIMUM_NUMBER_OF_LOCAL_PLAYERS];
static long first_person_weapon_vr_pose_weapon[MAXIMUM_NUMBER_OF_LOCAL_PLAYERS];
/* the pose was built for this frame already, from the controller, which
the eyes do not move (from the eye's own camera, each eye builds its own) */
static boolean first_person_weapon_vr_pose_kept(
short local_player_index)
{
unsigned long serial= halo_vr_frame_serial();
return serial!=0 &&
first_person_weapon_vr_pose_serial[local_player_index]==serial &&
first_person_weapon_vr_pose_weapon[local_player_index]==first_person_weapon_get(local_player_index)->weapon_index;
}
/* VR: the grenade the player holds (the left trigger, units.c) hangs from
the body, which the first-person view leaves out, and with it the grenade:
drawn instead in the left hand, as it is this frame, with the hands (which
@@ -636,6 +657,22 @@ void first_person_weapon_draw(
weapon_definition->weapon.interface_definition.first_person_animations.index,
first_person_weapon->node_matrices,
first_person_weapon->hands_node_remapping_table);
#ifdef HALO_ANDROID
/* (vr.log_performance: once, how its skin hangs from its
bones, for the VR arms) */
{
static int log_skin= -1;
if (log_skin<0)
log_skin= config_boolean("vr.log_performance")!=0;
if (log_skin)
model_vr_log_skin(first_person_interface->hands.index);
}
#endif
#ifdef HALO_ANDROID
/* VR: the weapon alone (vr.body "off") */
if (halo_vr_body_mode()!=HALO_VR_BODY_OFF)
#endif
render_model(
first_person_interface->hands.index,
0.0f,
@@ -725,7 +762,23 @@ void first_person_weapon_render_update(
first_person_weapon_set_visibility(render.local_player_index, visible);
if (first_person_weapon->visible)
{
#ifdef HALO_ANDROID
unsigned long long timing= halo_vr_timing_start();
/* VR: posed from the controller, the pose is the same for
every eye (and the scope's view): built once a frame */
if (first_person_weapon_vr_pose_kept(render.local_player_index))
{
halo_vr_timing_end(HALO_VR_TIMING_FIRST_PERSON_KEPT, timing);
}
else
{
first_person_weapon_build_node_matrices(render.local_player_index);
halo_vr_timing_end(HALO_VR_TIMING_FIRST_PERSON, timing);
}
#else
first_person_weapon_build_node_matrices(render.local_player_index);
#endif
}
}
}
@@ -1168,9 +1221,10 @@ The first-person skeleton hangs from the camera at the right controller
(halo_vr_weapon_camera), so the weapon and the right hand lie where they
should; its arms, though, reach up to shoulders that float with the weapon.
Here each arm's upper arm and forearm are turned (two-bone inverse
kinematics) to run from the player's shoulders, below the head, to its
wrist. The left wrist, unless the hand holds the weapon's foregrip, goes to
the left controller, its fingers with it. */
kinematics, port/linux/game/vr_arms.c) to run from the player's shoulders,
below the neck and square to the torso, to its wrist, the controller's roll
shared along the forearm. The left wrist, unless the hand holds the
weapon's foregrip, goes to the left controller, its fingers with it. */
#define VR_METRES_PER_WORLD_UNIT 3.048f
@@ -1186,6 +1240,10 @@ struct vr_arm_nodes
short left_middle;
short left_pinky;
long barrel_logged_graph_index;
/* the fingers' joints (vr_finger_nodes) of the skeleton they were
found in: each hand's thumb, index, middle, ring, little finger */
long fingers_graph_index;
short fingers[2][5][3];
/* the whole pose in the weapon camera's frame, outside a melee attack */
boolean idle_pose_valid;
short idle_pose_count;
@@ -1193,7 +1251,11 @@ struct vr_arm_nodes
};
static struct vr_arm_nodes vr_arm_nodes = { NONE, { NONE, NONE }, { NONE, NONE }, { NONE, NONE }, NONE, NONE, NONE, NONE,
NONE };
NONE, NONE };
/* the skeleton this frame, and each arm's last (its elbow's swing and its
forearm's twist run on from them) */
static struct vr_skeleton vr_arm_skeleton;
static struct vr_limb_history vr_arm_history[2];
static void vr_vector_scale_add(
real_vector3d *out,
@@ -1227,69 +1289,6 @@ static void vr_rotate_between(
v->k = v->k * cosine + turned.k * sine + axis.k * along * (1.f - cosine);
}
/* turns v about the unit axis by angle (radians) */
static void vr_rotate_about(
real_vector3d const *axis,
real angle,
real_vector3d *v)
{
real_vector3d turned;
real sin_angle = sine(angle);
real cos_angle = cosine(angle);
real along = dot_product3d(axis, v);
cross_product3d(axis, v, &turned);
v->i = v->i * cos_angle + turned.i * sin_angle + axis->i * along * (1.f - cos_angle);
v->j = v->j * cos_angle + turned.j * sin_angle + axis->j * along * (1.f - cos_angle);
v->k = v->k * cos_angle + turned.k * sin_angle + axis->k * along * (1.f - cos_angle);
}
/* A forearm turned only to point at the wrist keeps the animation's twist,
while the hand may have rolled half a turn from it: the skin between them
collapses. Twists the forearm about its bone by most of the hand's roll
from the animation (the hand's turn from old_wrist to new_wrist, applied to
the animated forearm old_forearm), as a real forearm twists along its
length. */
static void vr_twist_forearm(
real_matrix4x3 *forearm,
real_matrix4x3 const *old_forearm,
real_matrix4x3 const *old_wrist,
real_matrix4x3 const *new_wrist,
real_point3d const *wrist_position)
{
real_vector3d const *old_axes[3];
real_vector3d const *new_axes[3];
real_vector3d bone, turned_up, up, across;
real along, angle;
short axis;
vector_from_points3d(&forearm->position, wrist_position, &bone);
if (normalize3d(&bone) < 1e-6f)
return;
/* the animated forearm's up, turned as the hand turned */
old_axes[0] = &old_wrist->forward;
old_axes[1] = &old_wrist->left;
old_axes[2] = &old_wrist->up;
new_axes[0] = &new_wrist->forward;
new_axes[1] = &new_wrist->left;
new_axes[2] = &new_wrist->up;
turned_up.i = turned_up.j = turned_up.k = 0.f;
for (axis = 0; axis < 3; axis++)
vr_vector_scale_add(&turned_up, new_axes[axis], dot_product3d(&old_forearm->up, old_axes[axis]));
/* both ups across the bone, and the angle between them about it */
up = forearm->up;
vr_vector_scale_add(&up, &bone, -dot_product3d(&up, &bone));
vr_vector_scale_add(&turned_up, &bone, -dot_product3d(&turned_up, &bone));
if (normalize3d(&up) < 1e-4f || normalize3d(&turned_up) < 1e-4f)
return;
cross_product3d(&up, &turned_up, &across);
along = dot_product3d(&up, &turned_up);
angle = arctangent(dot_product3d(&across, &bone), along) * 0.6f;
vr_rotate_about(&bone, angle, &forearm->forward);
vr_rotate_about(&bone, angle, &forearm->left);
vr_rotate_about(&bone, angle, &forearm->up);
}
/* a node turned so that the bone to its child, from old to new direction */
static void vr_turn_bone(
real_matrix4x3 *node,
@@ -1308,39 +1307,6 @@ static void vr_turn_bone(
node->position = *position;
}
/* moves the descendants of a node that moved from old to new with it */
static void vr_carry_descendants(
long graph_index,
real_matrix4x3 *node_matrices,
short node_count,
short root,
real_matrix4x3 const *old_root,
real_matrix4x3 const *new_root)
{
real_matrix4x3 inverse;
real_matrix4x3 delta;
short node_index;
matrix4x3_inverse(old_root, &inverse);
matrix4x3_multiply(new_root, &inverse, &delta);
for (node_index = 0; node_index < node_count; node_index++)
{
short ancestor = animation_graph_node_parent(graph_index, node_index);
short steps;
/* parents come first; bounded in case of a malformed graph */
for (steps = 0; ancestor > 0 && ancestor != root && steps < node_count; steps++)
ancestor = animation_graph_node_parent(graph_index, ancestor);
if (ancestor == root && node_index != root)
{
real_matrix4x3 moved;
matrix4x3_multiply(&delta, &node_matrices[node_index], &moved);
node_matrices[node_index] = moved;
}
}
}
/* the upper arm turned about the elbow to point at the shoulder, keeping
its length (the free left arm: vr_left_hand_free) */
static void vr_point_upper_arm(
@@ -1362,50 +1328,6 @@ static void vr_point_upper_arm(
vr_turn_bone(&node_matrices[upper], &old_direction, &new_direction, &position);
}
/* one arm: upper arm and forearm from the shoulder to the wrist at target */
static void vr_arm(
real_matrix4x3 *node_matrices,
short upper,
short fore,
short wrist,
real_point3d shoulder,
real_point3d const *target,
real_vector3d const *pole)
{
real_vector3d old_upper, old_fore, reach, bend, new_upper, new_fore;
real upper_length, fore_length, distance, along, height;
real_point3d elbow;
vector_from_points3d(&node_matrices[upper].position, &node_matrices[fore].position, &old_upper);
vector_from_points3d(&node_matrices[fore].position, &node_matrices[wrist].position, &old_fore);
upper_length = magnitude3d(&old_upper);
fore_length = magnitude3d(&old_fore);
vector_from_points3d(&shoulder, target, &reach);
distance = normalize3d(&reach);
if (upper_length < 1e-4f || fore_length < 1e-4f || distance < 1e-4f)
return;
/* out of reach: the shoulder follows the hand (the arm stays whole) */
if (distance > (upper_length + fore_length) * 0.999f)
{
distance = (upper_length + fore_length) * 0.999f;
shoulder = *target;
point_from_line3d(target, &reach, -distance, &shoulder);
}
along = (upper_length * upper_length - fore_length * fore_length + distance * distance) / (2.f * distance);
height = upper_length * upper_length - along * along;
height = height > 0.f ? square_root(height) : 0.f;
bend = *pole;
vr_vector_scale_add(&bend, &reach, -dot_product3d(pole, &reach));
normalize3d(&bend);
point_from_line3d(&shoulder, &reach, along, &elbow);
point_from_line3d(&elbow, &bend, height, &elbow);
vector_from_points3d(&shoulder, &elbow, &new_upper);
vector_from_points3d(&elbow, target, &new_fore);
vr_turn_bone(&node_matrices[upper], &old_upper, &new_upper, &shoulder);
vr_turn_bone(&node_matrices[fore], &old_fore, &new_fore, &elbow);
}
/* an orthonormal frame from a direction and a second one across it */
static void vr_frame_from(
real_vector3d const *direction,
@@ -1422,14 +1344,14 @@ static void vr_frame_from(
/* the free left hand: the wrist behind the controller's grip, the fingers
along the controller and the knuckles, little finger to index, up its
handle, as a hand holds it. The whole arm follows the hand as it is shaped
on the foregrip: bent to the shoulder instead (vr_arm), the arm's skin,
bound partly to the skeleton's root at the other hand, tears into ribbons
wherever the hand is far from where the animation has it. */
handle, as a hand holds it; its fingers with it. The arm is bent to it
from the shoulder afterwards (vr_solve_arm), or, rigid (vr.arm_ik off),
follows the hand as it is shaped on the foregrip. (The arm's skin hangs
from the arm's bones alone, none from the skeleton's root: 3a.0's log.) */
static void vr_left_hand_free(
long graph_index,
struct vr_skeleton const *skeleton,
real_matrix4x3 *node_matrices,
short node_count,
boolean rigid,
float const hand_position[3],
float const hand_forward[3],
float const hand_up[3])
@@ -1473,23 +1395,24 @@ static void vr_left_hand_free(
wrist->position.y = hand_position[1];
wrist->position.z = hand_position[2];
point_from_line3d(&wrist->position, &to[0], -0.07f / VR_METRES_PER_WORLD_UNIT, &wrist->position);
vr_carry_descendants(graph_index, node_matrices, node_count, wrist_index, &old_wrist, wrist);
/* the forearm and the upper arm with it, as they are on the foregrip */
/* what the wrist carries (its fingers) with it, and rigid, the forearm
and the upper arm too, as they are on the foregrip */
{
real_matrix4x3 inverse, delta, moved;
short chain[2];
short link;
short node_index;
chain[0] = vr_arm_nodes.fore[0];
chain[1] = vr_arm_nodes.upper[0];
matrix4x3_inverse(&old_wrist, &inverse);
matrix4x3_multiply(wrist, &inverse, &delta);
for (link = 0; link < 2; link++)
for (node_index = 0; node_index < skeleton->node_count; node_index++)
{
if (chain[link] == NONE)
if (node_index == wrist_index ||
!(vr_skeleton_under(skeleton, node_index, wrist_index) ||
(rigid && (node_index == vr_arm_nodes.fore[0] || node_index == vr_arm_nodes.upper[0]))))
{
continue;
matrix4x3_multiply(&delta, &node_matrices[chain[link]], &moved);
node_matrices[chain[link]] = moved;
}
matrix4x3_multiply(&delta, &node_matrices[node_index], &moved);
node_matrices[node_index] = moved;
}
}
}
@@ -1570,6 +1493,58 @@ static void vr_laser_follows_barrel(
halo_vr_set_laser(local_player_index, laser_start, laser_direction);
}
/* The fingers (vr.fingers): the free left hand's from its controller's
trigger, grip and touch (vr_pose_fingers); of the hand on the weapon, only
the index finger, lifted a little off the trigger when the controller
says it is (the rest keeps the weapon's grip). */
static void first_person_weapon_vr_fingers(
short local_player_index,
real_matrix4x3 *node_matrices,
boolean left_free,
float const hand_forward[3],
float const hand_up[3])
{
float curls[4];
int fingers;
if (vr_arm_nodes.fingers_graph_index != vr_arm_skeleton.graph_index)
{
vr_arm_nodes.fingers_graph_index = vr_arm_skeleton.graph_index;
vr_finger_nodes(&vr_arm_skeleton, vr_arm_nodes.wrist[0], "l ", vr_arm_nodes.fingers[0]);
vr_finger_nodes(&vr_arm_skeleton, vr_arm_nodes.wrist[1], "r ", vr_arm_nodes.fingers[1]);
}
if (left_free && vr_arm_nodes.wrist[0] != NONE &&
halo_vr_finger_curls(local_player_index, 0, curls))
{
/* the palm's frame: along the fingers, up the knuckles, and out of
the left palm (inwards, to the right) */
real_vector3d forward, up, palm;
real along;
forward.i = hand_forward[0];
forward.j = hand_forward[1];
forward.k = hand_forward[2];
up.i = hand_up[0];
up.j = hand_up[1];
up.k = hand_up[2];
along = dot_product3d(&forward, &up);
vr_vector_scale_add(&up, &forward, -along);
if (normalize3d(&forward) > 1e-4f && normalize3d(&up) > 1e-4f)
{
cross_product3d(&forward, &up, &palm);
vr_pose_fingers(&vr_arm_skeleton, node_matrices, vr_arm_nodes.wrist[0], vr_arm_nodes.fingers[0], curls,
&palm, &forward, &up);
}
}
fingers = halo_vr_finger_curls(local_player_index, 1, curls);
if (vr_arm_nodes.wrist[1] != NONE && (fingers & HALO_VR_FINGERS_TOUCH))
{
/* (resting on the trigger the index curls 0.22 or more) */
vr_straighten_finger(&vr_arm_skeleton, node_matrices, vr_arm_nodes.fingers[1][1],
0.6f * PIN(1.f - curls[1] / 0.22f, 0.f, 1.f));
}
}
static void first_person_weapon_vr_arms(
short local_player_index,
long graph_index,
@@ -1591,14 +1566,18 @@ static void first_person_weapon_vr_arms(
static char const *const upper_names[2] = { "l upperarm", "r upperarm" };
static char const *const fore_names[2] = { "l forearm", "r forearm" };
static char const *const wrist_names[2] = { "l wriste", "r wriste" };
float head[3], body_forward[3];
float head[3], neck[3], body_forward[3];
real_vector3d forward, left, weapon_left, weapon_right;
real_point3d eye;
real_matrix4x3 animated_upper[2];
real_matrix4x3 animated_forearm[2];
real_matrix4x3 animated_wrist[2];
short side;
boolean gripping;
boolean left_free;
boolean rigid = !halo_vr_free_arm_bends();
int body = halo_vr_body_mode();
float hand_position[3], hand_forward[3], hand_up[3];
if (vr_arm_nodes.graph_index != graph_index)
{
@@ -1672,11 +1651,15 @@ static void first_person_weapon_vr_arms(
}
if (!ready && !melee)
vr_laser_follows_barrel(local_player_index, node_matrices, weapon_camera);
if (!halo_vr_body_frame(local_player_index, head, body_forward))
if (!halo_vr_body_frame(local_player_index, head, neck, body_forward) ||
!vr_skeleton_build(&vr_arm_skeleton, graph_index, node_count))
{
return;
eye.x = head[0];
eye.y = head[1];
eye.z = head[2];
}
/* (the shoulders hang from the neck, which the head turns about) */
eye.x = neck[0];
eye.y = neck[1];
eye.z = neck[2];
forward.i = body_forward[0];
forward.j = body_forward[1];
forward.k = 0.f;
@@ -1693,6 +1676,8 @@ static void first_person_weapon_vr_arms(
{
animated_forearm[side] = node_matrices[vr_arm_nodes.fore[side]];
animated_wrist[side] = node_matrices[vr_arm_nodes.wrist[side]];
if (vr_arm_nodes.upper[side] != NONE)
animated_upper[side] = node_matrices[vr_arm_nodes.upper[side]];
}
}
@@ -1715,13 +1700,19 @@ static void first_person_weapon_vr_arms(
reference[2] = dot_product3d(&between, &weapon_camera->up);
gripping = halo_vr_two_hand(local_player_index, foregrip, reference);
}
/* (the weapon alone: no hands to pose) */
if (body == HALO_VR_BODY_OFF)
return;
if (!gripping && vr_arm_nodes.wrist[0] != NONE)
{
float hand_position[3], hand_forward[3], hand_up[3];
if (halo_vr_left_hand(local_player_index, hand_position, hand_forward, hand_up))
{
vr_left_hand_free(graph_index, node_matrices, node_count, hand_position, hand_forward, hand_up);
/* (rigid: the hand and its arm carried to the controller now;
bent, the arm is solved to it first, below, and the hand turned
after, as HaloCE-Quest-VR does: turned first, the solve's swing
turned it on and the arm's skin stretched) */
if (rigid)
vr_left_hand_free(&vr_arm_skeleton, node_matrices, rigid, hand_position, hand_forward, hand_up);
left_free = TRUE;
}
}
@@ -1730,15 +1721,18 @@ static void first_person_weapon_vr_arms(
{
real_point3d shoulder;
real_vector3d pole;
short bones[NUMBER_OF_VR_ARM_BONES];
real outward = side ? -1.f : 1.f; /* left is +left */
if (vr_arm_nodes.upper[side] == NONE || vr_arm_nodes.fore[side] == NONE || vr_arm_nodes.wrist[side] == NONE)
continue;
/* the shoulder: below the eyes, a little behind them, to the side */
/* the shoulder: below the eyes, behind them (a little behind the
neck: in front of it, the open end of the sleeve came into view
looking down to the side), to the side, square to the torso */
shoulder = eye;
point_from_line3d(&shoulder, global_up3d, -0.25f / VR_METRES_PER_WORLD_UNIT, &shoulder);
point_from_line3d(&shoulder, &forward, -0.06f / VR_METRES_PER_WORLD_UNIT, &shoulder);
point_from_line3d(&shoulder, global_up3d, -0.20f / VR_METRES_PER_WORLD_UNIT, &shoulder);
point_from_line3d(&shoulder, &forward, -0.04f / VR_METRES_PER_WORLD_UNIT, &shoulder);
point_from_line3d(&shoulder, &left, outward * 0.19f / VR_METRES_PER_WORLD_UNIT, &shoulder);
/* the elbow bends down, out and back */
pole.i = -global_up3d->i;
@@ -1746,17 +1740,90 @@ static void first_person_weapon_vr_arms(
pole.k = -global_up3d->k;
vr_vector_scale_add(&pole, &left, outward * 0.6f);
vr_vector_scale_add(&pole, &forward, -0.3f);
if (!side && left_free)
/* and as the hand rolls: an elbow lifted out to the side rolls the
controller's top in, an elbow tucked in rolls it out. Its underside
(away from the thumb) leads the elbow: the right hand's the weapon's,
the free left hand's its controller's (the hand on the foregrip
keeps the default) */
{
real_vector3d below;
boolean rolls = FALSE;
if (side)
{
below.i = -weapon_camera->up.i;
below.j = -weapon_camera->up.j;
below.k = -weapon_camera->up.k;
rolls = TRUE;
}
else if (left_free)
{
below.i = -hand_up[0];
below.j = -hand_up[1];
below.k = -hand_up[2];
rolls = TRUE;
}
if (rolls && normalize3d(&pole) > 0.f)
{
vr_vector_scale_add(&pole, &below, 1.5f);
if (normalize3d(&pole) <= 0.f)
{
pole.i = -global_up3d->i;
pole.j = -global_up3d->j;
pole.k = -global_up3d->k;
}
}
}
bones[_vr_arm_upper] = vr_arm_nodes.upper[side];
bones[_vr_arm_fore] = vr_arm_nodes.fore[side];
bones[_vr_arm_hand] = vr_arm_nodes.wrist[side];
/* (the hands alone: the arms are shrunk away below) */
if (body == HALO_VR_BODY_HANDS)
continue;
if (!side && left_free && rigid)
{
/* the free left arm keeps its shape (vr_left_hand_free), but
for the upper arm, which points back at the shoulder */
vr_point_upper_arm(node_matrices, vr_arm_nodes.upper[side], vr_arm_nodes.fore[side], &shoulder);
continue;
}
vr_arm(node_matrices, vr_arm_nodes.upper[side], vr_arm_nodes.fore[side], vr_arm_nodes.wrist[side],
shoulder, &node_matrices[vr_arm_nodes.wrist[side]].position, &pole);
vr_twist_forearm(&node_matrices[vr_arm_nodes.fore[side]], &animated_forearm[side], &animated_wrist[side],
&node_matrices[vr_arm_nodes.wrist[side]], &node_matrices[vr_arm_nodes.wrist[side]].position);
if (!side && left_free)
{
/* the free left arm bent to the controller's grip (the wrist
7 cm behind it), the hand turned to the controller after */
real_point3d target;
target.x = hand_position[0] - hand_forward[0] * 0.07f / VR_METRES_PER_WORLD_UNIT;
target.y = hand_position[1] - hand_forward[1] * 0.07f / VR_METRES_PER_WORLD_UNIT;
target.z = hand_position[2] - hand_forward[2] * 0.07f / VR_METRES_PER_WORLD_UNIT;
vr_solve_arm(&vr_arm_skeleton, node_matrices, bones, shoulder, &target, &pole, FALSE,
&vr_arm_history[side]);
vr_left_hand_free(&vr_arm_skeleton, node_matrices, FALSE, hand_position, hand_forward, hand_up);
}
else
{
/* the hand stays where it is (on the weapon): the arm bends to
it */
vr_solve_arm(&vr_arm_skeleton, node_matrices, bones, shoulder,
&node_matrices[bones[_vr_arm_hand]].position, &pole, TRUE, &vr_arm_history[side]);
}
vr_align_elbow(node_matrices, bones, &animated_upper[side], &animated_forearm[side], &animated_wrist[side]);
vr_distribute_arm_twist(node_matrices, bones, &animated_forearm[side], &animated_wrist[side],
&vr_arm_history[side]);
}
first_person_weapon_vr_fingers(local_player_index, node_matrices, left_free, hand_forward, hand_up);
if (body == HALO_VR_BODY_HANDS)
{
for (side = 0; side < 2; side++)
{
short bones[NUMBER_OF_VR_ARM_BONES];
bones[_vr_arm_upper] = vr_arm_nodes.upper[side];
bones[_vr_arm_fore] = vr_arm_nodes.fore[side];
bones[_vr_arm_hand] = vr_arm_nodes.wrist[side];
if (bones[_vr_arm_upper] != NONE && bones[_vr_arm_fore] != NONE && bones[_vr_arm_hand] != NONE)
vr_hide_arm(&vr_arm_skeleton, node_matrices, bones, vr_arm_nodes.fingers[side], vr_arm_nodes.gun);
}
}
}
@@ -2122,8 +2189,12 @@ static void first_person_weapon_build_node_matrices(
(short)animation_graph->nodes.count,
camera);
#ifdef HALO_ANDROID
first_person_weapon_vr_pose_serial[local_player_index]= camera!=&render.camera ? halo_vr_frame_serial() : 0;
first_person_weapon_vr_pose_weapon[local_player_index]= first_person_weapon->weapon_index;
if (camera!=&render.camera)
{
unsigned long long timing= halo_vr_timing_start();
first_person_weapon_vr_arms(
local_player_index,
weapon_definition->weapon.interface_definition.first_person_animations.index,
@@ -2131,6 +2202,7 @@ static void first_person_weapon_build_node_matrices(
(short)animation_graph->nodes.count,
camera,
first_person_weapon->state);
halo_vr_timing_end(HALO_VR_TIMING_ARMS, timing);
}
#endif
}
+7 -13
View File
@@ -373,22 +373,16 @@ void terminal_printf(
if (line_index!=NONE)
{
short count;
int count;
struct output_line_datum *line = output_line_get(line_index);
line->timer = 0;
line->color = !color ? default_terminal_printf_color : *color;
count = _vsnprintf(line->buffer, NUMBEROF(line->buffer)-2, format, arglist) - 1;
match_vassert(
"c:\\halo\\SOURCE\\interface\\terminal.c",
413,
count < (short)(NUMBEROF(line->buffer)-1),
csprintf(
temporary,
"terminal_printf call generated %d characters; the buffer size is %d characters.",
count,
NUMBEROF(line->buffer)-1
)
);
/* port: a line longer than the on-screen buffer is cut to fit (it
halted here, from inside error(), so the real halt was never
written down) */
count = _vsnprintf(line->buffer, NUMBEROF(line->buffer)-2, format, arglist);
if (count < 0 || count >= (int)(NUMBEROF(line->buffer)-2))
line->buffer[NUMBEROF(line->buffer)-2] = 0;
line->tabstop = strstr(line->buffer, "|t") != 0;
telnet_console_print(line->buffer);
}
+61 -2
View File
@@ -6645,6 +6645,7 @@ they change), and from it a page of what the controllers' buttons do. */
int config_boolean(char const *name);
double config_real(char const *name);
char const *config_string(char const *name);
int config_write(char const *name, char const *value);
int config_write_boolean(char const *name, int value);
@@ -6658,12 +6659,15 @@ enum
_vr_option_laser,
_vr_option_crosshair,
_vr_option_motion_tracker,
_vr_option_body,
_vr_option_fingers,
_vr_option_controls,
_vr_option_quit,
NUMBER_OF_VR_OPTIONS,
VR_OPTIONS_TOP = 98,
VR_OPTIONS_ROW_HEIGHT = 26,
/* (twelve rows between the title and the footer, in the box) */
VR_OPTIONS_TOP = 94,
VR_OPTIONS_ROW_HEIGHT = 22,
VR_OPTIONS_FOOTER = 362,
_vr_controls_on_foot = 0,
@@ -6688,6 +6692,23 @@ static real const vr_snap_angles[] = { 30.0f, 45.0f, 60.0f, 90.0f };
static real const vr_smooth_speeds[] = { 60.0f, 90.0f, 120.0f, 150.0f, 180.0f };
static real const vr_vignettes[] = { 0.0f, 0.35f, 0.7f, 1.0f };
static char const *const vr_vignette_names[] = { "Off", "Light", "Medium", "Strong" };
/* vr.body's values, and as the panel shows them */
static char const *const vr_bodies[] = { "arms", "hands", "off" };
static char const *const vr_body_names[] = { "Arms and hands", "Hands only", "Weapon only" };
static short ui_vr_options_body(
void)
{
char const *body = config_string("vr.body");
short index;
for (index = 0; index < (short)NUMBEROF(vr_bodies); index++)
{
if (body && strcmp(body, vr_bodies[index]) == 0)
return index;
}
return 0;
}
static boolean ui_vr_options_screen(
void)
@@ -6753,6 +6774,11 @@ static void ui_vr_options_change(
case _vr_option_motion_tracker:
config_write_boolean("vr.motion_tracker_on_wrist", !config_boolean("vr.motion_tracker_on_wrist"));
break;
case _vr_option_body:
config_write("vr.body", vr_bodies[(ui_vr_options_body() + change + (short)NUMBEROF(vr_bodies)) %
(short)NUMBEROF(vr_bodies)]);
break;
case _vr_option_fingers: config_write_boolean("vr.fingers", !config_boolean("vr.fingers")); break;
case _vr_option_controls:
ui_vr_options_globals.controls = TRUE;
ui_vr_options_globals.page = _vr_controls_on_foot;
@@ -7179,6 +7205,15 @@ static void ui_vr_options_render(
label = "Motion tracker";
snprintf(value, sizeof(value), "%s", config_boolean("vr.motion_tracker_on_wrist") ? "On the wrist" : "In the HUD");
break;
case _vr_option_body:
label = "Body";
snprintf(value, sizeof(value), "%s", vr_body_names[ui_vr_options_body()]);
break;
case _vr_option_fingers:
label = "Fingers";
snprintf(value, sizeof(value), "%s", config_boolean("vr.fingers") ? "Tracked" : "Off");
unused = ui_vr_options_body() == 2;
break;
case _vr_option_controls:
label = "Controller buttons...";
break;
@@ -8629,6 +8664,30 @@ static boolean ui_check_for_pause_game(
{
if (game_time_get_paused() == TRUE)
ui_widgets_close_all();
/* port: (and a multiplayer map's own, below, which pauses
nothing, closes as in a multiplayer game) */
else if (tag_loaded(UI_WIDGET_DEFINITION_TAG, "ui\\shell\\solo_game\\pause_game\\pause_game") == NONE)
ui_widget_delete(widget_globals.active_widgets[controller_index]);
}
/* port: a multiplayer map played alone (New Game's MULTIPLAYER
maps) has no campaign pause screen, but its own (LEAVE GAME
goes to the main menu: network_game_remove_local_player) */
else if (tag_loaded(UI_WIDGET_DEFINITION_TAG, "ui\\shell\\solo_game\\pause_game\\pause_game") == NONE &&
tag_loaded(UI_WIDGET_DEFINITION_TAG, "ui\\shell\\multiplayer_game\\pause_game\\1p_pause_game") != NONE)
{
if (!ui_widget_load_by_name_or_tag(
"ui\\shell\\multiplayer_game\\pause_game\\1p_pause_game",
NONE,
NULL,
controller_index,
NONE,
NONE,
NONE))
{
error(
_error_silent,
"failed to load multiplayer pause game window");
}
}
else if (!ui_widget_load_by_name_or_tag(
"ui\\shell\\solo_game\\pause_game\\pause_game",
@@ -2431,6 +2431,14 @@ static boolean network_game_remove_local_player(
match_vassert("c:\\halo\\SOURCE\\interface\\ui_widget_event_handler_functions.c", 4073,
event && event->controller_index >= 0 && event->controller_index < 4,
"valid controller index required to remove player from network game");
/* port: a multiplayer map played alone (its pause screen: ui_widget.c's
ui_check_for_pause_game) has no network game to leave: the main menu,
the map not saved (as the campaign's pause screen quits it) */
if (!global_network_game_client_get())
{
main_goto_main_menu();
return TRUE;
}
network_game_client_local_player_quit(event->controller_index);
/* port: a split screen player who quit, the others staying, is not
joined to the next game */
+69 -19
View File
@@ -1852,11 +1852,40 @@ void main_pregame_render(
return;
}
/* port: a network co-op host reverts to its last saved state with its
clock kept going forward, and the clients follow through the co-op syncs
(network_coop.c). FALSE without a saved state, which would reset the map
on the host alone. */
static boolean main_coop_host_revert(
void)
{
long now = game_time_get();
if (!game_state_port_saved_game_valid())
return FALSE;
game_state_revert();
network_coop_reverted(now);
ui_widgets_disable_pause_game(30);
return TRUE;
}
static boolean main_coop_host(
void)
{
return game_connection() == _game_connection_network_server && network_coop_active();
}
static void main_revert_map_private(
void)
{
game_state_revert();
ui_widgets_disable_pause_game(30);
/* (a network client never reverts on its own: its game is the host's) */
if (main_coop_host())
main_coop_host_revert();
else if (game_connection() != _game_connection_network_client)
{
game_state_revert();
ui_widgets_disable_pause_game(30);
}
main_globals.revert_map = FALSE;
return;
}
@@ -1864,22 +1893,29 @@ static void main_revert_map_private(
static void main_skip_cinematic_private(
void)
{
/* port: a network host never reverts without a saved state, which
would reset the map on the host alone */
if (cinematic_can_be_skipped() &&
(game_connection() != _game_connection_network_server || game_state_port_saved_game_valid()))
{
long now = game_time_get();
/* port: only a local game or a network co-op host reverts to skip; a
network game's other machines would be left out of step */
boolean skippable = cinematic_can_be_skipped();
boolean skipped = FALSE;
game_state_revert();
network_coop_skip_reverted(now);
ui_widgets_disable_pause_game(30);
main_globals.revert_map = FALSE;
if (skippable && main_coop_host())
{
skipped = main_coop_host_revert();
if (skipped)
network_coop_skip_done();
}
else if (game_connection() == _game_connection_network_server)
else if (skippable && game_connection() == _game_connection_local)
{
game_state_revert();
ui_widgets_disable_pause_game(30);
skipped = TRUE;
}
if (skipped)
main_globals.revert_map = FALSE;
else if (main_coop_host())
{
error(_error_silent, "co-op: cutscene not skipped (skippable %d, saved state %d)",
cinematic_can_be_skipped(), game_state_port_saved_game_valid());
skippable, game_state_port_saved_game_valid());
}
main_globals.skip_cinematic = FALSE;
return;
@@ -2018,12 +2054,14 @@ static void main_lost_map_private(
{
main_globals.lost_map = FALSE;
main_globals.loss_timer = 0;
/* port: network co-op can't revert to a checkpoint (every machine would
have to), so the players respawn where they were at the last one */
if (game_connection() == _game_connection_network_server)
players_respawn_at_checkpoint();
else
/* port: in network co-op, everyone dying respawns the players where
they were at the last checkpoint, without a revert. A mission the
scripts failed (game_lost, with players still alive: d40's timer,
a50's Keyes) does revert, or its failure cutscene would never end. */
if (game_connection() != _game_connection_network_server)
game_state_revert();
else if (players_are_all_dead() || !main_coop_host_revert())
players_respawn_at_checkpoint();
}
return;
}
@@ -3338,7 +3376,13 @@ void main_loop(
network_game_abort();
}
#ifdef HALO_ANDROID
halo_vr_phase(HALO_VR_PHASE_INPUT, 0);
#endif
game_time_update((real)main_globals.halt_time_scale*main_globals.seconds_elapsed);
#ifdef HALO_ANDROID
halo_vr_phase(HALO_VR_PHASE_TICKS, game_time_get_elapsed());
#endif
render_frame = main_globals.main_menu_scenario_loaded ||
(main_globals.halt_time_scale &&
@@ -3353,6 +3397,9 @@ void main_loop(
observer_update((real)main_globals.halt_time_scale*main_globals.seconds_elapsed);
collision_log_end_period();
game_engine_update_non_deterministic((real)main_globals.halt_time_scale*main_globals.seconds_elapsed);
#ifdef HALO_ANDROID
halo_vr_phase(HALO_VR_PHASE_CAMERA, 0);
#endif
}
if (main_globals.saving_map)
@@ -3367,6 +3414,9 @@ void main_loop(
main_game_render((double)main_globals.seconds_elapsed);
render_interpolation_frame_end();
profile_render_end();
#ifdef HALO_ANDROID
halo_vr_phase(HALO_VR_PHASE_DRAW, 0);
#endif
}
}
else
+6
View File
@@ -322,6 +322,12 @@ long datum_new(
absolute_index++;
header = (struct datum_header *)((byte *)header+size);
}
/* port: none free from first_free_absolute_index on (and none before it
is, as ever): the next search starts at the end, until a datum is deleted
(datum_delete lowers it), rather than walking a full array again. A full
array (the AI's props, with many actors) was walked on every new datum. */
if (result == NONE)
data->first_free_absolute_index = data->maximum_count;
return result;
}
+181
View File
@@ -258,6 +258,187 @@ static struct profile_section render_model_section = { "render_model", NONE, TRU
/* ---------- private code */
#ifdef HALO_ANDROID
/* ---------- VR: the first-person hands' skin
The VR arms bend the first-person hands model's arms far from where its
animations have them; a triangle whose corners hang from bones that move
apart (an arm's, the skeleton's root's) stretches across. Read from the
part's own buffers, as drawn. */
void platform_log(char const *format, ...);
char *tag_get_name(long tag_index);
boolean rasterizer_model_buffer_data(struct vertex_buffer const *vertices, struct triangle_buffer const *triangles,
void const **vertex_data, void const **index_data);
/* the part's vertices and indices, if they are of the kinds read here */
static boolean model_vr_part_buffers(
struct model_geometry_part const *part,
struct model_vertex_compressed const **vertices,
word const **indices)
{
if (!rasterizer_model_buffer_data(&part->vertex_buffer, &part->triangle_buffer,
(void const **)vertices, (void const **)indices))
{
return FALSE;
}
return *vertices && *indices && part->vertex_buffer.count > 0 && part->vertex_buffer.count <= 65535 &&
part->triangle_buffer.count > 0 && part->triangle_buffer.count <= 65535 &&
part->vertex_buffer.type == _rasterizer_vertex_type_model_compressed &&
(part->triangle_buffer.type == _triangle_buffer_type_precompiled_strip ||
part->triangle_buffer.type == _triangle_buffer_type_triangles);
}
/* a triangle's corners (from the strip, or the list) and the nodes they
hang from (bits, a node more than a trace of weight); FALSE if malformed */
static boolean model_vr_triangle(
struct model_geometry_part const *part,
struct model_vertex_compressed const *vertices,
word const *indices,
long triangle_index,
short node_count,
word corners[3],
unsigned long long *nodes)
{
short corner;
*nodes = 0;
for (corner = 0; corner < 3; corner++)
{
long at = part->triangle_buffer.type == _triangle_buffer_type_triangles ?
triangle_index * 3 + corner : triangle_index + corner;
struct model_vertex_compressed const *vertex;
real weight;
short influence;
corners[corner] = indices[at];
if (corners[corner] >= part->vertex_buffer.count)
return FALSE;
vertex = &vertices[corners[corner]];
/* (the weight is a signed short's fraction, as rasterizer_debug_model_vertices reads it) */
weight = (real)vertex->node_weight / 32767.0f;
if (weight < 0.0f || weight > 1.0f)
return FALSE;
for (influence = 0; influence < 2; influence++)
{
short node = (short)(vertex->nodes[influence] / 3);
real amount = influence ? 1.0f - weight : weight;
if (amount <= 0.001f)
continue;
if (vertex->nodes[influence] % 3 || node >= node_count || node >= 64)
return FALSE;
*nodes |= 1ULL << node;
}
}
return TRUE;
}
/* once a model: how many triangles hang from each node, and how many of
those from its root too (debug's logcat; first_person_weapons.c, for the
hands) */
void model_vr_log_skin(
long model_index)
{
static long logged[8] = { NONE, NONE, NONE, NONE, NONE, NONE, NONE, NONE };
struct model *model;
long triangles[64];
long with_root[64];
long total = 0;
short slot;
short node_index;
short region_index;
short geometry_seen[MAXIMUM_REGIONS_PER_MODEL];
short geometry_seen_count = 0;
boolean readable = TRUE;
for (slot = 0; slot < NUMBEROF(logged); slot++)
{
if (logged[slot] == model_index)
return;
if (logged[slot] == NONE)
break;
}
if (slot == NUMBEROF(logged) || model_index == NONE)
return;
logged[slot] = model_index;
model = model_definition_get(model_index);
if (model->nodes.count <= 0 || model->nodes.count > 64)
return;
csmemset(triangles, 0, sizeof(triangles));
csmemset(with_root, 0, sizeof(with_root));
/* (each region's first permutation, in the finest detail, as drawn) */
for (region_index = 0; region_index < model->regions.count && region_index < MAXIMUM_REGIONS_PER_MODEL;
region_index++)
{
struct model_region *region = TAG_BLOCK_GET_ELEMENT(&model->regions, region_index, struct model_region);
struct model_region_permutation *permutation;
short geometry_index;
short seen;
struct model_geometry *geometry;
short part_index;
if (region->permutations.count <= 0)
continue;
permutation = TAG_BLOCK_GET_ELEMENT(&region->permutations, 0, struct model_region_permutation);
geometry_index = permutation->geometry_indices[NUMBER_OF_DETAIL_LEVELS_PER_MODEL - 1];
for (seen = 0; seen < geometry_seen_count && geometry_seen[seen] != geometry_index; seen++);
if (geometry_index == NONE || seen < geometry_seen_count)
continue;
geometry_seen[geometry_seen_count++] = geometry_index;
geometry = TAG_BLOCK_GET_ELEMENT(&model->geometries, geometry_index, struct model_geometry);
for (part_index = 0; part_index < geometry->parts.count; part_index++)
{
struct model_geometry_part *part = TAG_BLOCK_GET_ELEMENT(&geometry->parts, part_index,
struct model_geometry_part);
struct model_vertex_compressed const *vertices;
word const *indices;
long triangle_index;
if (!model_vr_part_buffers(part, &vertices, &indices))
{
readable = FALSE;
continue;
}
for (triangle_index = 0; triangle_index < part->triangle_buffer.count; triangle_index++)
{
word corners[3];
unsigned long long nodes;
if (!model_vr_triangle(part, vertices, indices, triangle_index, model->nodes.count, corners, &nodes))
{
readable = FALSE;
break;
}
if (corners[0] == corners[1] || corners[1] == corners[2] || corners[0] == corners[2])
continue;
total++;
for (node_index = 0; node_index < model->nodes.count; node_index++)
{
if (!(nodes & (1ULL << node_index)))
continue;
triangles[node_index]++;
if (nodes & 1)
with_root[node_index]++;
}
}
}
}
platform_log("VR skin of %s: %ld triangles%s; %ld hang from its root '%s'", tag_get_name(model_index), total,
readable ? "" : " (some parts unreadable)", triangles[0],
TAG_BLOCK_GET_ELEMENT(&model->nodes, 0, struct model_node)->name);
for (node_index = 1; node_index < model->nodes.count; node_index++)
{
if (triangles[node_index])
{
platform_log("VR skin: node %d '%s': %ld triangles, %ld with the root", node_index,
TAG_BLOCK_GET_ELEMENT(&model->nodes, node_index, struct model_node)->name, triangles[node_index],
with_root[node_index]);
}
}
}
#endif
static void render_model_parts(
struct model const *model,
char const *region_permutation_indices,
+23 -1
View File
@@ -411,6 +411,12 @@ enum
long as the host waits for a silent machine, network_server_manager.c):
network_game_client_network_lost */
NETWORK_GAME_CLIENT_LINK_DOWN_TIMEOUT = 15000,
/* port: how long a frame the client spends on the messages that came
(a host that floods it with them would hold each frame for as long as
they took): those left wait in the queue, in order, for the next
frame's, and the stream what the queue has no room for. One is always
handled (network_game_client_process_messages) */
MAXIMUM_MESSAGE_MILLISECONDS_PER_IDLE = 50,
};
enum network_game_client_state
@@ -745,6 +751,9 @@ static boolean add_advertised_game(
struct message_server_game_advertise *advertisement);
static boolean network_game_client_process_incoming_messages(
struct network_game_client *client);
static boolean network_game_client_process_messages(
struct network_game_client *client,
boolean budgeted);
static boolean network_game_client_process_last_messages(
struct network_game_client *client);
static void network_game_client_update_precache_status(
@@ -2450,11 +2459,21 @@ static boolean add_advertised_game(
static boolean network_game_client_process_incoming_messages(
struct network_game_client *client)
{
return network_game_client_process_messages(client, TRUE);
}
/* (budgeted: no longer than MAXIMUM_MESSAGE_MILLISECONDS_PER_IDLE, else
every message queued) */
static boolean network_game_client_process_messages(
struct network_game_client *client,
boolean budgeted)
{
boolean success = TRUE;
word message_packet_size;
struct transport_address source_address;
word message_packet[MAXIMUM_NETWORK_MESSAGE_SIZE / sizeof(word)];
unsigned long start_time = system_milliseconds();
message_packet_size = sizeof(message_packet);
@@ -2474,6 +2493,8 @@ static boolean network_game_client_process_incoming_messages(
}
message_packet_size = sizeof(message_packet);
if (budgeted && system_milliseconds() - start_time >= MAXIMUM_MESSAGE_MILLISECONDS_PER_IDLE)
break;
}
return success;
@@ -2490,7 +2511,8 @@ static boolean network_game_client_process_last_messages(
{
short state = client->state;
if (!network_game_client_process_incoming_messages(client))
/* (all of them: the connection goes) */
if (!network_game_client_process_messages(client, FALSE))
{
network_event("network_game_client_process_incoming_messages() failed after the connection failed");
}
+11 -1
View File
@@ -662,6 +662,14 @@ boolean network_game_remove_player(
game->players[player_index].machine_index == player->machine_index &&
game->players[player_index].controller_index == player->controller_index)
{
/* port: a client's count is the host's (its settings), and
need not be as many as the players it lists: none taken
below none */
if (game->player_count <= 0)
{
error(2, "tried to remove a player from a game of %d players", game->player_count);
break;
}
network_game_invalidate_player(&game->players[player_index]);
game->player_count--;
result = TRUE;
@@ -707,7 +715,9 @@ boolean network_game_remove_machine(
}
network_game_invalidate_machine(game, machine->machine_index);
game->machine_count--;
/* (port: and none below none, as for its players) */
if (game->machine_count > 0)
game->machine_count--;
result = TRUE;
break;
}
+11 -1
View File
@@ -42,12 +42,22 @@ struct network_game_local_data
byte __padding431[3];
};
/* port: network_game's cooperative_flags */
enum
{
/* the players pass through each other (Server Setup's PLAYER COLLISIONS:
OFF, co-op's): network_coop_player_collisions */
_network_game_cooperative_no_player_collisions_bit = 0,
};
struct network_game
{
wchar_t name[16];
struct network_game_map map;
struct game_variant variant;
byte __padding10C;
/* port: co-op's switches (_network_game_cooperative_*_bit), in a byte the
Xbox's game left as padding, which builds before them send as 0 */
byte cooperative_flags;
char minimum_players;
/* port: 128 does not fit a signed char */
byte maximum_players;
+27 -3
View File
@@ -1116,9 +1116,12 @@ unsigned long network_game_server_machine_address(
/* port: the distributed netcode asks that a client machine be dropped (its
game ran faster than this one's: network_distributed.c); it is, once this
server next looks at its machines, not while its messages are read */
server next looks at its machines, not while its messages are read. Its
address kept out of this server's games (kept_out), else it may join
again, as a kick's */
void network_game_server_kick_machine(
long machine_index)
long machine_index,
boolean kept_out)
{
struct network_game_server *server = global_network_game_server_get();
@@ -1128,7 +1131,7 @@ void network_game_server_kick_machine(
{
return;
}
network_game_server_kick_pending[machine_index] = _kick_kept_out;
network_game_server_kick_pending[machine_index] = kept_out ? _kick_kept_out : _kick_rejoinable;
}
/* (a client machine's player queued to add in game: one refused is as one
@@ -3142,6 +3145,11 @@ struct network_machine *network_game_server_get_client_machine(
if (machine_index)
*machine_index = NONE;
/* port: none for a connection without a slot (NONE: the assert is not
checked in release builds) */
if (client_machine->machine_index < 0 || client_machine->machine_index >= MAXIMUM_NETWORK_MACHINE_COUNT)
return NULL;
machine = &server->game.machines[client_machine->machine_index];
if (machine_index)
*machine_index = machine->machine_index;
@@ -3178,6 +3186,10 @@ struct network_game_server_client_machine *network_game_server_get_client_machin
match_assert(NETWORK_SERVER_MANAGER_FILE, 0x741,
server && (index<MAXIMUM_NETWORK_MACHINE_COUNT));
/* (port: and in release builds) */
if (index < 0 || index >= MAXIMUM_NETWORK_MACHINE_COUNT)
return NULL;
return &server->client_machines[index];
}
@@ -4032,6 +4044,18 @@ void network_game_server_port_set_cooperative_friendly_fire(
network_event("network_game_server_port_set_cooperative_friendly_fire() failed to send updated game settings to clients");
}
void network_game_server_port_set_cooperative_player_collisions(
boolean player_collisions)
{
struct network_game_server *server = global_network_game_server_get();
if (!server)
return;
SET_FLAG(server->game.cooperative_flags, _network_game_cooperative_no_player_collisions_bit, !player_collisions);
if (server->state == _network_game_server_state_pregame && !network_game_server_send_game_data_pregame(server))
network_event("network_game_server_port_set_cooperative_player_collisions() failed to send updated game settings to clients");
}
/* port: a gametype's PC options: the menus' (player_ui_set_game_variant_options)
when it is the menus' gametype, else its defaults */
static void network_game_server_variant_options(
+6 -1
View File
@@ -47,7 +47,8 @@ void network_game_server_pause_countdown(
void network_game_generate_join_game_token(
byte *join_token);
void network_game_server_kick_machine(
long machine_index);
long machine_index,
boolean kept_out);
/* the host's ban command (console.c, hs.c) */
enum
{
@@ -72,6 +73,10 @@ void network_game_server_port_set_cooperative(
Server Setup's FRIENDLY FIRE), kept for the levels after */
void network_game_server_port_set_cooperative_friendly_fire(
short friendly_fire);
/* port: whether co-op's players collide with each other (Server Setup's
PLAYER COLLISIONS), kept for the levels after */
void network_game_server_port_set_cooperative_player_collisions(
boolean player_collisions);
boolean network_game_server_ban_player(
char const *text);
/* port: the host's kick command: as the ban command, but nothing kept (no
@@ -267,6 +267,7 @@ symbols in this file:
/* port/linux/game/network_distributed.c's */
void network_distributed_handle_message(long machine_index, word const *message, word size);
void network_distributed_handle_stream_message(long machine_index, word const *message, word size);
/* ---------- constants */
@@ -289,6 +290,11 @@ enum
/* port: the least time between two advertisements of the game
(milliseconds: network_game_server_handle_message_client_broadcast_game_search) */
GAME_ADVERTISEMENT_INTERVAL = 250,
/* port: the least time between two answers to one address's pings, and
the addresses kept to tell, no more of them answered in that time
(network_game_server_handle_message_client_ping) */
PING_REPLY_INTERVAL = 250,
MAXIMUM_PING_REPLY_ADDRESSES = 32,
};
enum
@@ -447,6 +453,11 @@ struct message_client_switch_to_pregame
long unused;
};
struct message_client_graceful_game_exit_postgame
{
long unused;
};
struct message_server_machine_accepted
{
long random_seed;
@@ -551,6 +562,11 @@ static boolean network_game_server_handle_message_client_switch_to_pregame(
struct network_game_server_client_machine *client_machine,
word *message,
short message_size);
static boolean network_game_server_handle_message_client_graceful_game_exit_postgame(
struct network_game_server *server,
struct network_game_server_client_machine *client_machine,
word *message,
short message_size);
/* ---------- globals */
@@ -793,7 +809,12 @@ short network_distributed_server_machines(
connection = network_game_server_get_client_connection(machine);
if (!connection || !network_connection_active(connection))
continue;
network_game_server_get_client_machine(server, machine, &game_machine_index);
/* (port: and not a machine without a slot) */
if (!network_game_server_get_client_machine(server, machine, &game_machine_index) ||
game_machine_index == NONE)
{
continue;
}
machine_indices[count++] = game_machine_index;
}
return count;
@@ -1292,15 +1313,19 @@ boolean network_game_server_handle_client_message(
}
break;
/* (port: its own handler, which decodes it as the
postgame message it is: the pregame's, which this
called, took none out of the pregame, nor could decode
one) */
case _message_client_graceful_game_exit_postgame:
result = network_game_server_handle_message_client_graceful_game_exit_pregame(
result = network_game_server_handle_message_client_graceful_game_exit_postgame(
server,
machine,
message,
message_buffer_size);
if (!result)
{
network_event("network_game_server_handle_message_client_graceful_game_exit_pregame() failed");
network_event("network_game_server_handle_message_client_graceful_game_exit_postgame() failed");
}
break;
@@ -1328,7 +1353,8 @@ boolean network_game_server_handle_client_message(
long machine_index;
network_game_server_get_client_machine(server, machine, &machine_index);
network_distributed_handle_message(machine_index, message, message_buffer_size);
/* (port: from its stream, which only it can send on) */
network_distributed_handle_stream_message(machine_index, message, message_buffer_size);
}
break;
@@ -1726,12 +1752,44 @@ static boolean network_game_server_handle_message_client_ping(
struct message_server_pong pong;
struct network_message *reply;
boolean result = FALSE;
/* port: an address answered no more often than PING_REPLY_INTERVAL, and
no more than MAXIMUM_PING_REPLY_ADDRESSES in that time (the answer goes
to the port the ping names, at the address it came from, which anyone
can send one as: a flood of pings would be a flood of answers, at
another's machine). A searching client pings once a second */
static struct
{
unsigned long address;
unsigned long time;
} replied[MAXIMUM_PING_REPLY_ADDRESSES];
unsigned long now = system_milliseconds();
short index;
short slot = NONE;
match_assert(
"c:\\halo\\SOURCE\\networking\\network_server_message_handler.c",
0x26A,
server && source_address && client_message);
for (index = 0; index < MAXIMUM_PING_REPLY_ADDRESSES; index++)
{
boolean recent = replied[index].address && now - replied[index].time < PING_REPLY_INTERVAL;
if (replied[index].address == source_address->address.long_words[0])
{
if (recent)
return TRUE;
slot = index;
break;
}
if (!recent && slot == NONE)
slot = index;
}
if (slot == NONE)
return TRUE;
replied[slot].address = source_address->address.long_words[0];
replied[slot].time = now;
pong.timestamp = client_message->timestamp;
reply = create_network_game_message(
_message_server_pong,
@@ -1858,7 +1916,7 @@ static boolean network_game_server_handle_message_client_join_game_request(
/* port: a machine joining the game in progress is named
by its join (its settings request, which names a machine
in the lobby, is refused in game) */
if (late_join)
if (late_join && client_machine)
csmemcpy(client_machine->name, machine_name, sizeof(client_machine->name));
acceptance.machine_index = (short)machine_index;
@@ -2404,9 +2462,10 @@ static boolean network_game_server_handle_message_client_graceful_game_exit_preg
&packet_version,
_network_game_packet_class_client_pregame))
{
if (network_game_server_remove_machine_from_game(
server,
network_game_server_get_client_machine(server, client_machine, NULL)))
struct network_machine *machine = network_game_server_get_client_machine(server, client_machine, NULL);
/* (port: none for a machine without a slot) */
if (machine && network_game_server_remove_machine_from_game(server, machine))
{
/* (not to machines loading the game) */
if (network_game_server_lobby_is_open(server) &&
@@ -2705,3 +2764,48 @@ static boolean network_game_server_handle_message_client_switch_to_pregame(
return result;
}
/* port: a machine leaving the game's scores, as one leaving the lobby does
(network_game_server_handle_message_client_graceful_game_exit_pregame) */
static boolean network_game_server_handle_message_client_graceful_game_exit_postgame(
struct network_game_server *server,
struct network_game_server_client_machine *client_machine,
word *message,
short message_size)
{
if (network_game_server_get_state(server, NULL) == _network_game_server_state_postgame)
{
struct message_client_graceful_game_exit_postgame graceful_game_exit;
short packet_type = _message_client_graceful_game_exit_postgame;
short packet_version = NETWORK_GAME_MESSAGE_VERSION;
message_size -= sizeof(word);
if (decode_network_game_message(
&graceful_game_exit,
message + 1,
&message_size,
&packet_type,
&packet_version,
_network_game_packet_class_client_postgame))
{
struct network_machine *machine = network_game_server_get_client_machine(server, client_machine, NULL);
if (!machine || !network_game_server_remove_machine_from_game(server, machine))
{
network_event(
"network_game_server_remove_machine_from_game() failed in network_game_server_handle_message_client_graceful_game_exit_postgame()");
}
}
else
{
network_event("server failed to decode a message_client_graceful_game_exit_postgame packet");
}
}
else
{
network_event(
"failed to handle a message_client_graceful_game_exit_postgame message because the server is not in post-game");
}
return TRUE;
}
+14
View File
@@ -2654,6 +2654,20 @@ void damage_replay_kill(
distributed_damage_authorized = FALSE;
}
/* ... damage to scenery or a device, dealt as the host dealt it: the copy
breaks, plays its effects and is destroyed as the host's was */
void damage_replay_static(
long object_index,
struct damage_data *damage,
short node_index,
short region_index,
short material_index)
{
distributed_damage_authorized = TRUE;
object_cause_damage(damage, object_index, node_index, region_index, material_index, NULL);
distributed_damage_authorized = FALSE;
}
/* the host's: what damage_set_network_state takes */
void damage_get_network_state(
long object_index,
+195 -51
View File
@@ -496,6 +496,31 @@ boolean debug_lights;
boolean debug_object_lights;
struct data_array *light_data;
struct cluster_partition light_cluster_partition;
/* port: the clusters' lights kept for lighting objects (lights_port_cluster):
as many as the light partition's lists can hold */
enum
{
LIGHTS_PORT_MAXIMUM_ENTRIES = HALO_PORT_MAXIMUM_CLUSTER_REFERENCES,
};
struct lights_port_entry
{
long light_index;
real_point3d position;
real radius;
};
static struct
{
/* each light's radius or rasterizer light written (lights_preprocess_scene) */
unsigned long radius_writes;
/* what the entries were kept for; each cluster's, and where they are */
unsigned long generation;
short entry_count;
unsigned long cluster_generations[MAXIMUM_CLUSTERS_PER_STRUCTURE];
short cluster_first_entries[MAXIMUM_CLUSTERS_PER_STRUCTURE];
short cluster_entry_counts[MAXIMUM_CLUSTERS_PER_STRUCTURE];
struct lights_port_entry entries[LIGHTS_PORT_MAXIMUM_ENTRIES];
} lights_port;
struct lights_game_globals *lights_game_globals = NULL;
short debug_rasterizer_light_count;
struct lights_globals lights_globals;
@@ -812,6 +837,7 @@ void lights_preprocess_scene(
_point_light_attached_to_first_person_weapon_bit,
FALSE);
light->rasterizer_light_index = NONE;
lights_port.radius_writes++;
if (light->parent_light_index != NONE)
{
struct point_light_definition *definition = light_definition_get(
@@ -967,6 +993,7 @@ void lights_preprocess_scene(
light->radius = (definition->radius_modifier_lower_bound * inverse_intensity
+ definition->radius_modifier_upper_bound * intensity)
* definition->radius;
lights_port.radius_writes++;
if (light->radius != 0.0f)
{
struct rasterizer_light_submit_parameters light_parameters;
@@ -1035,6 +1062,7 @@ void lights_preprocess_scene(
else
{
light->radius = definition->radius;
lights_port.radius_writes++;
}
if (definition->lens_flare.index != NONE)
@@ -1796,6 +1824,136 @@ void light_reconnect_to_map(
return;
}
/* a light of a cluster, for find_point_lights_for_object_in_cluster: as
the Xbox's loop does each, marking it (so its other clusters pass it over) */
static void find_point_light_for_object(
long object_index,
long light_index,
real_point3d const *center,
real radius,
long *light_indices,
real *light_intensities,
real *light_attenuations,
short *light_count,
short maximum_light_count)
{
if (light_unmarked(light_index))
{
struct light_datum *light = light_get(light_index);
if (light->rasterizer_light_index != NONE
&& (light->object_index != object_index
|| !TEST_FLAG(light_definition_get(light->definition_index)->flags,
_light_definition_dont_light_own_object_bit)))
{
real distance = distance3d(&light->position, center);
if (distance < radius + light->radius)
{
real attenuation = light_attenuation(light->radius, distance);
real intensity = real_rgb_color_brightness(&light->color) * attenuation;
short index;
if (*light_count < maximum_light_count)
{
index = (*light_count)++;
}
else
{
real minimum_intensity = REAL_MAX;
short dimmest_index = NONE;
for (index = 0; index < *light_count; index++)
{
if (minimum_intensity > light_intensities[index])
{
minimum_intensity = light_intensities[index];
dimmest_index = index;
}
}
if (minimum_intensity < intensity)
{
index = dimmest_index;
}
}
if (index < maximum_light_count)
{
light_indices[index] = light_index;
light_intensities[index] = intensity;
light_attenuations[index] = attenuation;
}
}
}
light_mark(light_index);
}
}
/* port: a cluster's lights in its list's order, with where each is and how
far it reaches, kept while none of that changes: while the light partition's
lists (cluster_partitions.c's port_modification_count) and the lights' radii
and rasterizer lights (lights_port.radius_writes, written as a frame's lights
are prepared) are as they were; a light only moves as it joins its clusters
again. An object lit (each object drawn, each shadow) then passes over the
lights too far to reach it without their datums or the list's: with hundreds
of lights in a cluster (network co-op's extra enemies), reading them was
most of a frame's lighting. FALSE: not kept (the list walked as before). */
static boolean lights_port_cluster(
short cluster_index,
short *first_entry,
short *entry_count)
{
unsigned long generation = 1 + light_cluster_partition.port_modification_count + lights_port.radius_writes;
long reference_index;
long light_index;
short first;
if (cluster_index < 0 || cluster_index >= MAXIMUM_CLUSTERS_PER_STRUCTURE)
return FALSE;
if (lights_port.generation != generation)
{
lights_port.generation = generation;
lights_port.entry_count = 0;
}
if (lights_port.cluster_generations[cluster_index] != generation)
{
first = lights_port.entry_count;
for (light_index = cluster_partition_get_first_datum(&light_cluster_partition, &reference_index, cluster_index);
light_index != NONE;
light_index = cluster_partition_get_next_datum(&light_cluster_partition, &reference_index))
{
struct light_datum *light = light_get(light_index);
struct lights_port_entry *entry;
/* (one not drawn this frame lights nothing: reaching an object,
it would only be marked, for that object's other clusters to pass
over) */
if (light->rasterizer_light_index == NONE)
continue;
if (lights_port.entry_count >= LIGHTS_PORT_MAXIMUM_ENTRIES)
{
/* (more than are kept: this cluster's walked) */
lights_port.entry_count = first;
first = NONE;
break;
}
entry = &lights_port.entries[lights_port.entry_count++];
entry->light_index = light_index;
entry->position = light->position;
entry->radius = light->radius;
}
lights_port.cluster_generations[cluster_index] = generation;
lights_port.cluster_first_entries[cluster_index] = first;
lights_port.cluster_entry_counts[cluster_index] = first == NONE ? 0 : (short)(lights_port.entry_count - first);
}
if (lights_port.cluster_first_entries[cluster_index] == NONE)
return FALSE;
*first_entry = lights_port.cluster_first_entries[cluster_index];
*entry_count = lights_port.cluster_entry_counts[cluster_index];
return TRUE;
}
static void find_point_lights_for_object_in_cluster(
long object_index,
short cluster_index,
@@ -1809,66 +1967,52 @@ static void find_point_lights_for_object_in_cluster(
{
long light_index;
long reference_index;
short first_entry, entry_count;
match_assert(
"c:\\halo\\SOURCE\\objects\\object_lights.c",
0x544,
lights_globals.marker_initialized);
/* port: the cluster's lights as kept (lights_port_cluster). A light too
far to reach the object is passed over: the Xbox's loop tested it the
same, on the same position and radius, and rejected it. Left unmarked, it
is tested again in the object's other clusters, and rejected again.
Most are far along one axis, which says so without the distance: a
distance is never less than one axis of it but for rounding, a few parts
in 2^24, well inside the margin here. */
if (lights_port_cluster(cluster_index, &first_entry, &entry_count))
{
short entry_index;
for (entry_index = first_entry; entry_index < first_entry + entry_count; entry_index++)
{
struct lights_port_entry const *entry = &lights_port.entries[entry_index];
real reach = radius + entry->radius;
real bound = reach * 1.000001f;
real offset;
offset = entry->position.x - center->x;
if (offset > bound || -offset > bound)
continue;
offset = entry->position.y - center->y;
if (offset > bound || -offset > bound)
continue;
offset = entry->position.z - center->z;
if (offset > bound || -offset > bound)
continue;
if (!(distance3d(&entry->position, center) < reach))
continue;
find_point_light_for_object(object_index, entry->light_index, center, radius, light_indices,
light_intensities, light_attenuations, light_count, maximum_light_count);
}
return;
}
for (light_index = cluster_partition_get_first_datum(&light_cluster_partition, &reference_index, cluster_index);
light_index != NONE;
light_index = cluster_partition_get_next_datum(&light_cluster_partition, &reference_index))
{
if (light_unmarked(light_index))
{
struct light_datum *light = light_get(light_index);
if (light->rasterizer_light_index != NONE
&& (light->object_index != object_index
|| !TEST_FLAG(light_definition_get(light->definition_index)->flags,
_light_definition_dont_light_own_object_bit)))
{
real distance = distance3d(&light->position, center);
if (distance < radius + light->radius)
{
real attenuation = light_attenuation(light->radius, distance);
real intensity = real_rgb_color_brightness(&light->color) * attenuation;
short index;
if (*light_count < maximum_light_count)
{
index = (*light_count)++;
}
else
{
real minimum_intensity = REAL_MAX;
short dimmest_index = NONE;
for (index = 0; index < *light_count; index++)
{
if (minimum_intensity > light_intensities[index])
{
minimum_intensity = light_intensities[index];
dimmest_index = index;
}
}
if (minimum_intensity < intensity)
{
index = dimmest_index;
}
}
if (index < maximum_light_count)
{
light_indices[index] = light_index;
light_intensities[index] = intensity;
light_attenuations[index] = attenuation;
}
}
}
light_mark(light_index);
}
find_point_light_for_object(object_index, light_index, center, radius, light_indices, light_intensities,
light_attenuations, light_count, maximum_light_count);
}
return;
+20 -2
View File
@@ -31,6 +31,7 @@ OBJECTS.C
#include "game/players.h"
#include "items/weapons.h"
#include "main/console.h"
#include "interface/terminal.h"
#include "math/periodic_functions.h"
#include "memory/memory_pool.h"
#include "models/model_animation_definitions.h"
@@ -525,6 +526,18 @@ void object_pvs_set_camera_point(
return;
}
void objects_port_set_activating_cluster(
short cluster_index)
{
if (cluster_index == NONE || cluster_index >= global_structure_bsp_get()->clusters.count)
{
object_globals->pvs_activation_type = _pvs_activation_normal;
return;
}
object_globals->pvs_activation_type = _pvs_activation_cluster;
object_globals->pvs_activation.cluster_index = cluster_index;
}
void object_pvs_clear(void)
{
object_globals->pvs_activation_type = _pvs_activation_normal;
@@ -4090,7 +4103,10 @@ void objects_garbage_collection(
}
sprintf(tempbuffer, "garbage collection %scritical (%s)", status, warningbuf);
console_printf(FALSE, "%s", tempbuffer);
/* (port: chatter, shown as config.toml's game.console_log says:
many enemies keep it coming, over the whole screen) */
if (terminal_shows(_terminal_message_chatter))
console_printf(FALSE, "%s", tempbuffer);
error(_error_log, "%s", tempbuffer);
update_time = TRUE;
}
@@ -4129,7 +4145,9 @@ void objects_garbage_collection(
char tempbuffer[512];
sprintf(tempbuffer, "removing objects: %s", released_resultbuf);
console_printf(FALSE, "%s", tempbuffer);
/* (port: chatter, shown as config.toml's game.console_log says) */
if (terminal_shows(_terminal_message_chatter))
console_printf(FALSE, "%s", tempbuffer);
error(_error_log, "%s", tempbuffer);
}
+4
View File
@@ -386,6 +386,10 @@ void object_pvs_set_object(long object_index);
void object_pvs_set_camera_point(short camera_point_index);
void object_pvs_clear(void);
short objects_get_activating_cluster_index(void);
/* port: makes a cluster active as object_pvs_activate would (NONE: back to
the players' own), for a network co-op client following the host's
cutscene (port/linux/game/network_coop.c) */
void objects_port_set_activating_cluster(short cluster_index);
void object_definition_predict(long definition_index);
void object_predict(long object_index);
void object_beautify(long object_index, boolean beautiful);
+53 -3
View File
@@ -25,6 +25,7 @@ BREAKABLE_SURFACES.C
#include "sound/game_sound.h"
#include "structures/structure_bsp_definitions.h"
#include "tag_files/tag_groups.h"
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
/* ---------- structures */
@@ -37,6 +38,21 @@ struct breakable_surface_globals
/* ---------- prototypes */
/* port: cache_files.c's */
boolean tag_index_is_group(long tag_index, long group_tag);
/* port: a networked break carries no damage effect (the host already dealt
it). Tag 0 is the scenario, and reading it as a damage effect stops the game */
static struct damage_breaking_effect_definition const *breakable_surface_breaking_effect(
struct damage_data const *damage_data)
{
static struct damage_breaking_effect_definition const none;
if (damage_data && tag_index_is_group(damage_data->definition_index, DAMAGE_EFFECT_DEFINITION_TAG))
return &damage_effect_definition_get(damage_data->definition_index)->breaking_effect;
return &none;
}
/* January evaluates the j/k terms as one group in this translation unit.
The shared helper must remain flat for its other exact consumers. */
static __inline real breakable_surface_plane_distance(
@@ -160,7 +176,7 @@ void breakable_surface_damage(
if (globals->enabled)
{
if (breakable_surface_index != NONE &&
damage_data->definition_index != NONE &&
tag_index_is_group(damage_data->definition_index, DAMAGE_EFFECT_DEFINITION_TAG) &&
damage_data->material_type != NONE)
{
struct breakable_surface_datum *surface = breakable_surface_get(breakable_surface_index);
@@ -189,6 +205,8 @@ void breakable_surface_damage(
BIT_VECTOR_SET_FLAG((long *)breakable_surface_flags_get(), breakable_surface_index, FALSE);
breakable_surface_effect(breakable_surface_index, damage_data, seed_surface_index);
/* port: and on network co-op's clients */
network_coop_note_surface_broken(breakable_surface_index, &damage_data->epicenter);
}
}
}
@@ -204,7 +222,11 @@ void breakable_surface_damage_area_of_effect(
const struct damage_data *damage_data)
{
struct structure_bsp *structure_bsp = global_structure_bsp_get();
struct damage_effect_definition *damage_effect_definition = damage_effect_definition_get(damage_data->definition_index);
struct damage_effect_definition *damage_effect_definition;
if (!tag_index_is_group(damage_data->definition_index, DAMAGE_EFFECT_DEFINITION_TAG))
return;
damage_effect_definition = damage_effect_definition_get(damage_data->definition_index);
if (globals->enabled &&
(damage_effect_definition->damage.damage_lower_bound != 0.0f || damage_effect_definition->damage.damage_upper_bound != 0.0f))
@@ -236,6 +258,8 @@ void breakable_surface_damage_area_of_effect(
breakable_surface_get(breakable_surface_index)->vitality = 0.0f;
BIT_VECTOR_SET_FLAG((long *)breakable_surface_flags_get(), breakable_surface_index, FALSE);
breakable_surface_effect(breakable_surface_index, damage_data, breakable_surface->collision_surface_index);
/* port: and on network co-op's clients */
network_coop_note_surface_broken(breakable_surface_index, &damage_data->epicenter);
}
}
}
@@ -244,6 +268,32 @@ void breakable_surface_damage_area_of_effect(
return;
}
void breakable_surface_port_break(
short breakable_surface_index,
real_point3d const *epicenter)
{
struct structure_bsp *structure_bsp = global_structure_bsp_get();
struct structure_breakable_surface *breakable_surface;
struct damage_data damage;
if (breakable_surface_index < 0 || breakable_surface_index >= structure_bsp->breakable_surfaces.count ||
!breakable_surface_extant(breakable_surface_index))
{
return;
}
breakable_surface = TAG_BLOCK_GET_ELEMENT(&structure_bsp->breakable_surfaces, breakable_surface_index,
struct structure_breakable_surface);
/* (the shards fly away from the epicenter, all the effect reads) */
csmemset(&damage, 0, sizeof(damage));
damage.epicenter = *epicenter;
/* (and where it is, which the break's sound plays in: zeros were leaf
and cluster 0) */
scenario_location_from_point(&damage.location, epicenter);
breakable_surface_get(breakable_surface_index)->vitality = 0.0f;
BIT_VECTOR_SET_FLAG((long *)breakable_surface_flags_get(), breakable_surface_index, FALSE);
breakable_surface_effect(breakable_surface_index, &damage, breakable_surface->collision_surface_index);
}
/* ---------- private code */
static void breakable_surface_effect(
@@ -473,7 +523,7 @@ static void breakable_surface_effect(
struct new_particle_data particle;
velocity = *global_zero_vector3d;
breaking_effect = &damage_effect_definition_get(damage_data->definition_index)->breaking_effect;
breaking_effect = breakable_surface_breaking_effect(damage_data);
vector_from_points3d(&damage_data->epicenter, &position, &outward_vector);
distance = normalize3d(&outward_vector);
+6
View File
@@ -74,6 +74,12 @@ struct breakable_surface_particle_effect
struct breakable_surface_datum *breakable_surface_get(
short breakable_surface_index);
/* port: breaks a surface as damage at `epicenter` would, shards and all,
for a network co-op client the host told (port/linux/game/network_coop.c);
nothing if it is already broken */
void breakable_surface_port_break(
short breakable_surface_index,
real_point3d const *epicenter);
void breakable_surfaces_initialize(
void);
void breakable_surfaces_dispose(
+4
View File
@@ -1687,6 +1687,10 @@ static void object_get_features_in_sphere(
if ((!TEST_FLAG(flags, _collision_test_skip_passthrough_bipeds_bit) ||
!TEST_FLAG(biped->biped.flags, _biped_movement_passes_through_bipeds_bit)) &&
/* port: (nor a player's, to a player in a co-op
game without player collisions) */
(!TEST_FLAG(flags, _collision_test_skip_player_bipeds_bit) ||
biped->unit.player_index == NONE) &&
(object->object.parent_object_index == NONE ||
biped->unit.parent_seat_index == NONE))
{
+3
View File
@@ -51,6 +51,9 @@ enum
_collision_test_try_to_keep_location_valid_bit,
_collision_test_skip_passthrough_bipeds_bit,
_collision_test_use_vehicle_physics_bit,
/* port: no players' bipeds (a player's own movement in a co-op game
without player collisions: network_coop_player_collisions) */
_collision_test_skip_player_bipeds_bit,
NUMBER_OF_COLLISION_TEST_FLAGS,
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff. Show more