Compare commits

...
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 f68db3b03e VR: the flashlight by tapping the head taken out again
Revert of c2aceb64: on the Quest 3 the tap did not toggle the flashlight.
The flashlight's button stays; the on-foot vignette that came after it is
kept. The roadmap and README no longer name it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:53:48 +02:00
Stefan MaierandClaude Opus 5.5 bd08baacac Steam Frame installer: the shortcut added by Steam itself
On the Frame Desktop Mode runs inside Gaming Mode's session: closing Steam
to write shortcuts.vdf ended the session, and the installer with it, before
it wrote the shortcut (the library kept "Halo VR"). Steam now adds the
game itself (steam://addnonsteamgame, from a desktop file beside the game,
as SteamOS's "Add to Steam" does) and picks its id, which the installer
reads back for the library pictures. An old "Halo VR" shortcut is named
for the player to remove.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:32:49 +02:00
Stefan MaierandClaude Opus 5.5 f39126e019 Steam Frame installer: Steam found by its pid file, an install log
Steam's client on SteamOS is not always a process named steam: the
installer may have written the shortcut while Steam ran, and Steam wrote its
own list back when it quit (the library kept "Halo VR"). Steam is now
found by ~/.steam/steam.pid or its ubuntu12_32/steam process, given time
after it ends, and the shortcut checked once written; what the installer
does goes to install.log beside the game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:28:50 +02:00
Stefan MaierandClaude Opus 5.5 f649bd1a3e Release as Ringfall: Mjolnir VR
The product no longer carries Halo in its name; the game it plays is named
only to say which disc image it needs.

- The app: "Ringfall: Mjolnir VR", application id
  io.github.cesarus85.ringfall (the Java classes keep the decompilation's
  package), its update provider's authority from the id; the launcher's,
  updater's, host's and VR options' texts; the OpenXR application name.
- New icon and Steam library pictures, a tilted, broken ring with a falling
  streak (tools/ringfall_art.py, from shapes and Overpass; the helmet and
  the HALO logo gone).
- The Steam Frame: ~/Documents/RingfallMjolnirVR, RingfallMjolnirVR.apk,
  ringfall-mjolnir-vr.sh, Install-Ringfall-Mjolnir-VR.desktop and
  ringfall-mjolnir-vr-steam-frame.zip; the installer renames the folder of
  the builds before (~/Documents/HaloCE) and replaces their "Halo VR"
  shortcut and pictures.
- Releases: only the Android builds, as ringfall-mjolnir-vr-android-*.zip
  (the updater's names), and the Frame's installer; the repository
  Cesarus85/ringfall-mjolnir-vr.
- The signing key's default alias ringfall.
- README.md is the project's own; the port's moved to docs/OPENCE_README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:16:31 +02:00
Stefan Maier 719563a48c Merge branch 'feature/release-install' into test/comfort-release 2026-10-06 22:08:41 +02:00
Stefan MaierandClaude Opus 5.5 32cc1cb206 VR roadmap: step 2's second part built on feature/vr-comfort
Buttons remapped, the flashlight by tapping the head, finer turning and
vignette steps, the transparent geometry groups raised: what they do, the
settings, what the emulator showed and the headset tests for the Quest 3
and the Steam Frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:07:28 +02:00
Stefan MaierandClaude Opus 5.5 d4c1fbe9a7 Release: Steam Frame installer, release signing, Frame import kept
- A Steam Frame installer for players, opened in Desktop Mode
  (Install-Halo-VR.desktop, a release asset): it downloads the latest
  release's bundle (tools/steam_frame_bundle.py) and runs
  steam_frame_install.sh, with zenity dialogs and no keyboard: the APK and
  launcher in ~/Documents/HaloCE, the maps copied out of a disc image it
  finds or is given (steam_frame_xiso.py, the app's XisoExtractor in
  Python), and Halo VR in the Steam library with its pictures and OpenVR
  set. Running it again updates the game.
- The release job adds the bundle and the desktop file to each release.
- build.gradle signs with port/android/signing.properties (ignored) when
  the workflow's key is not given, so local builds and releases install over
  each other.
- On the Frame the launcher and host take ~/Documents/HaloCE whenever it can
  be written, not only once it holds maps: a disc image imported in the app
  is no longer emptied with Lepton's folders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:06:21 +02:00
Stefan MaierandClaude Opus 5.5 adf220d344 VR: finer turning steps, an on-foot vignette, a COMFORT page
The VR options get a fourth page, COMFORT (PLAY, COMFORT, BUTTONS, MORE;
the tabs beside the title share its width): Turning, Snap turn, Smooth
turn speed and the vehicle vignette move there from PLAY, joined by the
on-foot vignette and its strength. PLAY keeps the hands, laser,
crosshair, motion tracker, body, fingers and the Warthog's wheel.

- Snap turns step through 10 to 45 degrees by 5, then 60, 75 and 90 (were
  30, 45, 60, 90); smooth turning through 30 to 150 degrees a second by
  15, then to 300 by 30 (were 60 to 180 by 30); the vehicle vignette
  through Off, 15, 25, 35, 50, 60, 70, 85 and 100% (were Off, Light 35%,
  Medium 70%, Strong). The old values are among the steps; the defaults
  (snap 30, 120 a second, vignette 70%) are unchanged.
- vr.foot_vignette ("off" default, "turning", "moving") and
  vr.foot_vignette_strength (0.7): on foot the eye images darken from
  their edges while the turning stick turns smoothly and, with "moving",
  while the move stick moves, as far as the sticks are pushed beyond
  their dead zone (fully at three quarters), eased in and out as the
  vehicle vignette is. Not in a vehicle's seat (whether or not its view
  is first person), not in the menus; snap turns have none. The stronger
  of the two vignettes is shown. Off by default: nothing changes.

Checked in the emulator (debug.vr_options): the four tabs and the rows
without overlaps, each row's steps, the values kept over a restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:05:10 +02:00
Stefan MaierandClaude Opus 5.5 32ad9ed6b0 VR: the flashlight by tapping the side or top of the head
Either hand brought to the side or the top of the head and away again, as
a tap, toggles the flashlight (vr.flashlight_gesture, default true; a row
on the VR options' BUTTONS page); the flashlight's button keeps working.
After HaloCE-Quest-VR (CC0), whose off hand near the head's middle
toggles it, made stricter against false toggles:

- The zone is in the head's axes: beside the head (7 to 24 cm out, from
  10 cm below the eyes to 18 cm above) or over it (within 12 cm of its
  middle line, 8 to 30 cm up), never more than 6 cm in front of the eyes,
  so a scope held to the eye, a two-handed weapon, a punch and the wrist's
  motion tracker, all in front of the face, are outside it.
- A tap: the hand comes from 35 cm or more from the head's middle, enters
  slower than 3 m/s (a throw past the ear is faster), stays 40 to 600 ms
  (adjusting the headset takes longer), alone (both hands at the head is
  the headset adjusted) and leaves 4 cm past the zone's edges; toggles are
  at least a second apart.
- None while the menus are up, while recentring, while a grenade is held
  or for 0.6 s after one is thrown, within 0.7 s of a melee swing, with
  both hands on the weapon, with the weapon hand while the scope is drawn,
  or with a hand on a Warthog's wheel; such a hand must leave the head
  before it taps.
- A toggle presses the flashlight's button (white) for 100 ms, as B's tap
  does, and gives the tapping hand a short rumble.

The tap's state machine was run off the headset in a small harness over
taps beside and over the head, a turned head, a second tap within the
cooldown, a long dwell, both hands, a scope at the eye, the scope flag, a
fast throw past the ear, a held grenade and a two-handed grip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:00:53 +02:00
Stefan MaierandClaude Opus 5.5 cf19bcfdfa VR: the game's actions moved between the controllers' buttons
A BUTTONS page in the VR options (between PLAY and MORE) puts jump, the
flashlight (held: switch grenades), action/reload, switch weapon, crouch,
zoom and melee on any of seven buttons: A, B, X, Y, the move and turning
sticks' clicks and the weapon hand's grip. Moving an action to a button
swaps it with the action there, so every action always has a button;
"Reset buttons to defaults" puts them back. The triggers, the free hand's
grip (both hands on the weapon), the menu button, the Steam Frame's
bumpers, D-pad and View button keep what they do, and the menus keep
their buttons.

Settings vr.button_jump, _light, _action, _weapon, _crouch, _zoom and
_melee ("a", "b", "x", "y", "move_stick", "turn_stick", "grip"; the
defaults are today's layout), named as right-handed. vr.c reads them with
the live settings and, after the left-handed mirroring
(controllers_mirrored), reads each action's button and hands it on as the
button that action has by default (controls_remapped), so xinput_sdl.c's
table is unchanged and the defaults change nothing. Because the mapping
sits after the mirroring it works the same on the Quest's Touch and the
Steam Frame's controllers (mirrored on the Frame the left D-pad is A, B,
X, Y), and a mirrored player's swaps are the mirror image of the
right-handed ones. The guest/host ABI is unchanged.

- The melee's aim and the weapon's jab (melee_update) follow the melee's
  button, not the grip as such; the grips holding a Warthog's wheel press
  nothing whatever action the grip has (moved from xinput_sdl.c).
- The controller buttons page (now on the BUTTONS page) shows each action
  under the controller and name of its button, for both layouts and the
  mirrored hands; its ON FOOT tab no longer draws the MENUS tab's notes
  over its own.
- debug.vr_options (Android, false): the VR options panel off the
  headset, for checking its pages in the emulator.

Checked in the emulator (debug.vr_options): the rows, swaps, reset, the
controls page right- and left-handed, the settings kept over a restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:57:15 +02:00
Stefan MaierandClaude Opus 5.5 3b5796f1a0 VR roadmap: the keyboard's password in the clear is no open point
Nobody else sees the headset's view (decided 2026-10-06): no masking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:47:26 +02:00
Stefan MaierandClaude Opus 5.5 68135a712d Transparent geometry: room for 2048 groups a window (was the Xbox's 384)
Every transparent thing a window draws is a group of its own: a model's
transparent part (shields, plasma, glass), each particle system, contrail
and light volume, a window's or water's surface. The Xbox had room for 384
(rasterizer_transparent_geometry_new_group); past that new_group returns
NULL and the submitter drops the geometry, logging "### ERROR too many
transparent geometry groups" once (rasterizer_xbox_models.c,
rasterizer_xbox_widgets.c; environment and dynavobgeom drop it silently).
render_window submits the objects first, then the structure's transparent
geometry, weather, particles, particle systems and contrails, so in games
of 30 and more players the late ones vanish: water and glass surfaces,
explosions, muzzle flashes, smoke, plasma glows, contrails.

The groups are a heap array (0xA0 each), a short array of sorted indices
and a bit vector of pending flags, all sized by the constant; indices are
shorts (sorted_index, presorted indices, the draw loop), good to 32767.
Nothing else depends on the number (the debug view's "all groups" index
1000 only colours groups in a debug mode). Upstream (halo-ce-universal)
still has 384.

2048 groups take 320 KB (61 KB before). A one-line log tells when a window
passes 384 and every 128 beyond, for checking big games in the headset.
In the emulator, a 36-player bot game on Blood Gulch had up to 134 groups
a window from a standing player, using under 8 KB of the rasterizer's
768 KB memory pool (the models' queued matrices and lighting), so the
pool is not the next limit.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:55:22 +02:00
Stefan MaierandClaude Opus 5.5 81137e3af3 VR roadmap: left-handed mode and graphics presets good on the Steam Frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:40:01 +02:00
Stefan MaierandClaude Opus 5.5 fb670e79b9 VR roadmap: the left hand's ammo counter reads the right way round
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:33:11 +02:00
Stefan MaierandClaude Opus 5.5 8022a0b4a1 VR left-handed: the assault rifle's ammo counter reads the right way round
The mirrored first-person model leaves its geometry mirrored; a numeric
display drawn mirrored (rasterizer_set_model_skinning's winding flag) now
shows each digit's bitmap turned over in u (1-u on the first map's texture
transform) and swaps the digits' order across the display (the quad of the
n-th digit from the right shows the n-th from the left; digit count from
the counter limit, the compass's single digit unchanged). Only for 2d,
non-screen-space first maps of numeric chicago/generic shaders, only when
mirrored: right-handed and flat output unchanged.

debug.mirror_first_person (Android, default false): off the headset the
first-person weapon is mirrored across the camera's forward/up plane as
the left hand's is in VR, for checking such things in the emulator.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:54:01 +02:00
Stefan MaierandClaude Opus 5.5 2a2b2cdbf3 VR left-handed: the ammo display's subtree found without hanging
Walking up from a node to the display through parent_node_index never
ended where the root names itself as its parent: the game hung loading a
level left-handed (Steam Frame). Each step now goes only to a parent
before its child, as vr_skeleton_build does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:50:52 +02:00
Stefan MaierandClaude Opus 5.5 5861e57d99 VR left-handed: the assault rifle's ammo display reads the right way round
Its node ("... display") and what hangs from it are mirrored back about
the display's own centre after the pose is mirrored: it stays where the
mirrored gun has it, reads as in the right hand, and its triangles keep
their winding (rasterizer_xbox.c decides per skinned part). After
HaloCE-Quest-VR (CC0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:47:53 +02:00
Stefan MaierandClaude Opus 5.5 50d685e465 VR roadmap: step 2 in part built (left-handed mode, graphics presets, refresh rate)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:33:27 +02:00
Stefan MaierandClaude Opus 5.5 0e6b006878 VR options: graphics, refresh rate and resolution rows
The panel's second page (MORE) gets "Graphics" (vr.graphics: Low,
Medium, High, Max), "Refresh rate" and "Resolution" rows, with a note
of what they do. The refresh rate steps through the rates the runtime
offers (XR_FB_display_refresh_rate: host_xr_refresh_rates, new host
imports with host_xr_set_refresh_rate) and changes at once (vr.c asks
for it when vr.refresh_rate changes); where the runtime offers none
(SteamVR on the Steam Frame, whose rate is SteamOS's) the row shows the
rate it runs at, from its frames' period, and stays as it is. The
resolution (vr.resolution_scale, 70 to 120%) is read as the app starts:
the row says when a restart is needed. vr.log_performance's late frames
follow the rate as it changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:30:32 +02:00
Stefan MaierandClaude Opus 5.5 ca73ba7909 VR: graphics presets (vr.graphics)
port/linux/game/vr_graphics.c (after HaloCE-Quest-VR's, CC0) sets the
game's own effect switches each frame before drawing: "high" (the
default) draws everything as before; "medium" no object shadows,
environment reflections or lens flares; "low" nor specular light, bump
maps, detail objects (grass), the screen's fog layer, contrails, weather
or camouflage's second pass; "max" is "high" with per-pixel model
lighting and 512x512 shadow maps (d3d8_gl.c asks halo_vr_graphics). A
switch the game changes (scripts, console) is taken as its wish and
combined with the preset's. A client in another's game keeps grass, lens
flares and the fog layer as the host's rules have them; cheats.c leaves
the environment parts the preset may keep off there (shadows, specular,
reflections).

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:26:40 +02:00
Stefan MaierandClaude Opus 5.5 9910c4a9dc VR options: weapon hand and mirrored controls, two pages
The VR options panel gets a "Weapon hand" row (vr.handedness) and a
"Mirrored controls" row (vr.mirror_controls, dim when right-handed). Its
rows now go on two pages with tabs beside the title (PLAY, MORE; up and
down run on across them, a tab clicked shows its page). The controller
buttons page is built from the hands' jobs: the weapon hand's trigger
and grip, the free hand's, the move and turn sticks and the face
buttons (Quest X/Y for A/B; Steam Frame D-pad for A, B, X, Y) on the
sides the settings put them; the wrist, wheel and aiming notes name the
hands. network_vr.c's notes name the weapon and the free hand.

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:21:36 +02:00
Stefan MaierandClaude Opus 5.5 d97de6d499 VR: left-handed mode, the weapon in the left hand
vr.handedness = "left" puts the weapon in the left hand: aim, muzzle,
laser, crosshair, scope and turret aim follow the left controller; the
grenade, the foregrip and the fingers' closing round a grenade move to
the right hand, the grip's melee to the left. The first-person model is
posed as the right hand's in a mirror image of the room ("model space":
vr.c mirrors the controllers into it, first_person_weapons.c the body)
and the posed skeleton mirrored back, after HaloCE-Quest-VR (CC0); a
model whose node matrices mirror has its triangles' winding turned over
(rasterizer_set_model_skinning -> d3d8_gl.c, ended with the model and
each transparent group). halo_vr_left_hand is now halo_vr_free_hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:20:59 +02:00
Stefan MaierandClaude Opus 5.5 fbe48a869f VR roadmap: steps 1 and 3a done, in main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:53:21 +02:00
Stefan MaierandClaude Opus 5.5 206eb0b52b VR roadmap: the Steam Frame's tests done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:46:55 +02:00
Stefan MaierandClaude Opus 5.5 1c66715a19 VR roadmap: the Quest's tests done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:34:15 +02:00
Stefan MaierandClaude Opus 5.5 87f336df78 VR roadmap: 3a tried on the Quest
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:30:49 +02:00
Stefan MaierandClaude Opus 5.5 0a86e406c8 VR arms: the hand's roll leads the elbow; the free arm bent by default
The elbow could not be lifted out to the side: it bent only towards the
fixed pole (down, out, back). An elbow lifted out rolls the controller's
top in, so the pole now leans towards the hand's underside (the weapon's
for the right arm, the free left controller's for the left; the hand on
the foregrip keeps the fixed pole). With the free arm solved before its
hand is turned it looked right on the Quest: vr.arm_ik defaults to true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:56:47 +02:00
Stefan MaierandClaude Opus 5.5 c09b40a2ef VR arms: the free left arm solved first, its hand turned after
As HaloCE-Quest-VR does it: the free arm is bent to the controller's grip
from the animated pose (the hand free to swing with it), and only then is
the hand turned to the controller and the roll shared along the forearm.
Turning the hand first left the solve to swing it on, which stretched the
arm's skin like a rubber band (Quest, 2026-10-06). vr.arm_ik stays off by
default until it is seen right.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:54:15 +02:00
Stefan MaierandClaude Opus 5.5 1e2006d06c VR arms: the elbow bends in the arm's plane, the shoulders higher
Second Quest test: with the elbow out to the side the right elbow's skin
turned inwards: the shortest turn of the solve left the upper arm's hinge
where it was. vr_align_elbow now rolls the upper arm about its length so
the animation's bend lies in the plane the arm bends in, and the upper
arm takes no share of the hand's roll any more. The open sleeve end could
still be seen: the shoulders are 5 cm higher (20 cm below the neck).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:47:45 +02:00
Stefan MaierandClaude Opus 5.5 e69e087602 VR arms: the free arm rigid by default, the shoulders further back
First Quest test: the free left arm's skin stretched like a rubber band
with the new elbow IK, so vr.arm_ik now defaults to false (the arm's shape
as before, pointed at the shoulder); and the open end of the right
sleeve came into view looking down to the side: the shoulders now stand
4 cm behind the neck instead of 3 cm in front of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:59:41 +02:00
Stefan MaierandClaude Opus 5.5 87a38e2895 VR roadmap: the pending headset tests, performance among them
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:45:44 +02:00
Stefan MaierandClaude Opus 5.5 788ab74a6b VR roadmap: step 3a built in feature/vr-arms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:37:07 +02:00
Stefan MaierandClaude Opus 5.5 6d2dcfaaec VR arms 3a.6: vr.body (arms, hands, off) and vr.fingers in the VR options
- vr.body = "arms" (default): arms and hands as before; "hands": the arms'
  bones shrunk to a point 3.5 cm behind each wrist along the hand's line
  so the cuff closes behind the hand (vr_hide_arm, after HaloCE-Quest-VR's
  vr_hide_forearms, CC0), no arm IK solved; "off": the hands model is not
  drawn (one model draw less an eye) and nothing of the arms posed; the
  weapon, its aim with both hands, barrel, laser and crosshair stay.
- vr.c reads vr.body with the live settings (halo_vr_body_mode).
- The VR options panel gets "Body" (Arms and hands / Hands only / Weapon
  only) and "Fingers" (Tracked / Off) rows; its twelve rows are 22
  pixels apart (were 26) so the panel keeps its box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:35:07 +02:00
Stefan MaierandClaude Opus 5.5 98e848de39 VR arms 3a.5: the fingers posed
- vr_arms.c: vr_finger_nodes, vr_pose_finger (after HaloCE-Quest-VR, CC0,
  nearly verbatim): each finger's three joints found by name under the
  hand, posed by absolute directions in the palm's frame (a little bent
  at rest, a fist at full curl; the thumb up the handle or across the
  palm), each segment's subtree turned with its length kept. Its touch
  of the world (rays per finger) is left out for the Frame's processor.
- The free left hand takes its controller's curls (halo_vr_finger_curls:
  index from the trigger, the others from the grip, the thumb from where
  it rests; round a held grenade). On the foregrip the hand keeps the
  weapon's pose.
- The hand on the weapon keeps its grip; only its index finger is lifted
  part way off the trigger while the controller says it is (touch known
  and the finger off it), never where touch is unknown.
- Joints found once per skeleton; the cost is a few dozen node turns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:33:12 +02:00
Stefan MaierandClaude Opus 5.5 7754532683 VR arms 3a.4: the IK core from HaloCE-Quest-VR, the free left arm bent
- port/linux/game/vr_arms.c: vr_solve_arm and vr_distribute_arm_twist,
  after HaloCE-Quest-VR (CC0), nearly verbatim: the shoulder reaches 8 cm
  and the arm stretches up to 18% out of reach (further, the shoulder
  follows the held hand, as this port had it, so the arm stays whole),
  bends no further than 145 degrees, the elbow's swing eased per arm
  (vr_limb_history), the hand's roll shared by forearm (85%) and upper
  arm (25%) within 110 degrees, continuous across half a turn. The
  skeleton is read through the graph's index; the nodes under each node
  come from one pass a frame (vr_skeleton_build) rather than walking
  ancestors per node; time is the headset's frame's (halo_vr_pose_time).
- Both arms use it; the right hand and a left hand on the foregrip stay
  where the weapon puts them (hand_stays), so shots, barrel and laser do
  not move.
- The free left hand: its wrist and fingers are set on the controller as
  before, and the arm now bends to it from the shoulder instead of being
  carried rigidly. The ribbons of e377e4dc did not come from skin on the
  root (3a.0: none hangs there), so no triangle filter is needed; the
  rigid arm stays behind vr.arm_ik = false, read live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:31:16 +02:00
Stefan MaierandClaude Opus 5.5 19b0bcb42e VR arms 3a.3: the shoulders follow the torso, not the head
- vr.c keeps the torso's heading in LOCAL space (torso_update, after
  HaloCE-Quest-VR's vr_body_heading, CC0): the head's heading, steady at
  any pitch, drawn halfway towards the hands when both are ahead (at most
  45 degrees), followed beyond a 15 degree cone, eased at 10 a second;
  reset on a recentre or a gap. In LOCAL space the stick's snap and
  smooth turns carry it at once (HaloCE-Quest-VR's "arm IK lags as the
  body turns").
- halo_vr_body_frame gives the torso's heading and the neck the head
  turns about (9 cm behind the eyes); the shoulders hang from the neck,
  square to the torso, so turning the head no longer swings them (as the
  head faces the torso, where they were before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:26:24 +02:00
Stefan MaierandClaude Opus 5.5 f30dd50317 VR arms 3a.2: the fingers' input (touch, trigger, grip)
- host_xr: two actions, the thumb's touch (stick, buttons, thumb rest,
  the Frame's D-pad and View) and the index finger's (trigger), bound
  for the Touch, Index and Steam Frame profiles (paths after
  HaloCE-Quest-VR, CC0, and Valve's "Steam Frame Input" page) as optional
  bindings: a profile refusing them is suggested again without them.
- ABI: HALO_XR_BUTTON_THUMB_TOUCH, _INDEX_TOUCH and _TOUCH_KNOWN (the
  runtime gives the thumb's touch at all) in the buttons' free bits; the
  structures keep their size (android_abi_check: none unsafe).
- vr.c: each hand's thumb, index, middle and last two fingers, curled
  from those (the thumb half curled where touch is unknown, the hand
  round a held grenade), eased by HaloCE-Quest-VR's critically damped
  spring; halo_vr_finger_curls() hands them to the game; vr.fingers
  (default on) turns it off.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:23:11 +02:00
Stefan MaierandClaude Opus 5.5 f254be4b45 VR arms 3a.1: the first-person pose built once a frame, not per eye
Posed from the camera at the right controller, the first-person weapon
and the arms are the same for both eyes and the scope's view, yet were
built (animation, interpolation, arm IK) for each: twice a frame on
SteamVR, three times zoomed in. The pose is kept for the rest of the
headset's frame (halo_vr_frame_serial) once built from the controller,
for the same weapon; posed from an eye's own camera (the hand not
tracked), each eye still builds its own. vr.log_performance counts the
builds and the poses kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:22:45 +02:00
Stefan MaierandClaude Opus 5.5 a6350d207a VR arms 3a.0: time the first-person pose, log the hands' skin
- vr.log_performance reports how long the first-person pose takes a frame
  (its animation and the arms), how often it is built, and the arms' part
  of it (halo_vr_timing_start/_end).
- Once per hands model it logs how many triangles hang from each node and
  how many of those from the skeleton's root ('frame bone24'), read from
  the part's own buffers (rasterizer_model_buffer_data, after
  HaloCE-Quest-VR, CC0). In the emulator the cyborg's hands have none on
  the root: the ribbons of e377e4dc did not come from root skin.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:58:04 +02:00
Stefan MaierandClaude Opus 5.5 9d6b980ba6 Merge upstream (OpenCE 9e4f8a2d): performance with many enemies, co-op fixes, anti-aliasing, per-pixel lighting, shadow resolution, reverb, password lobbies, network version 20
Conflicts: d3d8_gl.c (multiview variants kept beside the per-pixel lit
shaders: shader[4] and lit_shader[4], vertex_shader_get takes both;
multiview targets before multisampling; the picture read resolves both),
nv2a_vsh.c/xgpu.h (lighting and multiview parameters), render.c (no
anti-aliasing pass on VR eye passes), dsound_sdl.c, p2p.h.
VR: anti-aliasing off (the eyes go to the headset from their textures);
the VR online list skips password games (the server browser asks).

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

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

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

Based on #47 by Tyberious.
2026-10-06 14:59:21 +13:00
MrBruh 8a8e70599f I3DL2 reverb and muffled sounds
Based on #44 and #46 by Tyberious. 3D voices send to an I3DL2 reverb of the camera's sound environment, and obstruction and occlusion muffle their direct path above the HF reference (occlusion their send too). Both use the fixed distance; a maximum distance under the minimum is held at it. A change of environment crossfades the delays and gains over 85 ms, the filters' coefficients ramp over each mix, the diffusers, lines and filters flush denormals, and the reverb stops while it is quiet. audio.reverb (default true) is read again when the settings change.
2026-10-06 14:39:41 +13:00
MrBruh b115a41263 Resample with a table per phase
Fixup for #42: a voice at the output rate or slower blends two rows of weights by its phase instead of working out each tap's (the same low pass, a third of the work), and a voice turned all the way down only moves on.
2026-10-06 13:58:46 +13:00
MrBruh 320464258a Merge pull request #42 from Tyberious/audio-resampling 2026-10-06 13:57:04 +13:00
MrBruh f5bb75c27c Widen the constant checkpoint serial
Fixup for #52: the checkpoint serial main added since is 64-bit too, as the serials it is compared with are.
2026-10-06 13:56:58 +13:00
MrBruh 5cd1daff77 Merge pull request #52 from Tyberious/constant-serial-wrap 2026-10-06 13:56:35 +13:00
MrBruh 6329be8aa1 Merge pull request #53 from Tyberious/interpolation-snaps 2026-10-06 13:56:35 +13:00
MrBruh 7a2f4caa50 Merge pull request #43 from Tyberious/audio-limiter 2026-10-06 13:56:35 +13:00
MrBruh 938c37a669 Merge pull request #45 from Tyberious/audio-distance 2026-10-06 13:56:35 +13:00
MrBruh fae3fbad69 Merge pull request #41 from Tyberious/adpcm-decoding 2026-10-06 13:56:35 +13:00
MrBruh 5a35ea2b20 Network version 18
Fixups for #99 and #112: the version raised for the object message's
shader permutation, NETCODE.md brought up to date, a networked glass
break's sound placed where it broke, a frame scratch pool that runs out
logged once, and the new comments marked as the port's.
2026-10-06 13:39:26 +13:00
MrBruh 2e179ea90f Merge pull request #112 from innerhat-dev/networked-break-and-frame-scratch 2026-10-06 13:34:53 +13:00
MrBruh 9b517e554c Merge pull request #99 from xshxdex98/coop-shader-permutation 2026-10-06 13:29:02 +13:00
MrBruh 595bbc5611 Merge pull request #113 from xshxdex98/quiet-garbage-collection 2026-10-06 13:29:02 +13:00
MrBruh 761210d405 Harden map and network input 2026-10-06 13:26:43 +13:00
xshxdex98 7ba780ef59 Garbage collection's notices are chatter: on screen only with console_log = "all"
With many enemies (co-op's extra enemies) object memory runs low and the
garbage collector prints "garbage collection critical" and "removing
objects" at every pass, filling the screen for the host and every client.
They are the game's own chatter, so they show as game.console_log says, as
the sound and texture caches' already do; the log keeps every line.
2026-10-05 18:55:53 -05:00
justinfilip d9c43e3478 Keep a networked glass break from reading the scenario as a damage effect.
The host's notice of a broken surface carries no damage effect. The client filled a damage record with zeros and then read tag 0, which is the scenario, and halted. Shards still spawn. They skip the kick from an effect this machine never received.

The frame scratch pool was still the Xbox size after the particle pool grew eight times, so a busy level spent it every frame. It now grows with that particle pool, and a frame that still runs out is logged once.

A console line longer than the buffer halted inside the error printer, before the reason was written to the log. Long lines are cut to fit, and the reason is written first.
2026-10-05 17:19:35 -05:00
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
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
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
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
158 changed files with 13603 additions and 1395 deletions

No files matched your search

+24 -6
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
@@ -232,6 +237,13 @@ jobs:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
port/android
port/third_party/extract-xiso
tools
- uses: actions/download-artifact@v4
with:
pattern: halo-*
@@ -240,10 +252,16 @@ jobs:
- name: Package
run: |
mkdir release
for build in artifacts/*/; do
name="$(basename "$build")"
(cd "$build" && zip -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
# (Ringfall: Mjolnir VR publishes its Android builds only, under
# its own name: Updater.java looks for them; the desktop ports are
# built to keep them building)
for config in debug release; do
(cd "artifacts/halo-android-$config" &&
zip -qr "$GITHUB_WORKSPACE/release/ringfall-mjolnir-vr-android-$config.zip" .)
done
# the Steam Frame's installer (port/android/STEAM_FRAME.md): its
# bundle with the release APK, and the desktop file that fetches it
python3 tools/steam_frame_bundle.py artifacts/halo-android-release/app-release.apk release "$GH_REPO"
ls -l release
- name: Publish
@@ -257,7 +275,7 @@ jobs:
subject="$(printf '%s' "${MESSAGE:-manual run}" | head -n 1)"
printf '%s\n\nCommit: %s\nBuild: %s\n\nThe release builds are for playing; the debug builds stop at the first failed assertion and log it.\n' \
"$subject" "$COMMIT_URL" "$RUN_URL" > notes.md
gh release create "$tag" release/*.zip --target "$GITHUB_SHA" --latest \
gh release create "$tag" release/* --target "$GITHUB_SHA" --latest \
--title "Build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
echo "tag=$tag" >> "$GITHUB_OUTPUT"
@@ -313,7 +331,7 @@ jobs:
title="Build failed"; color=15548997
fi
latest="$SERVER/$REPOSITORY/releases/latest"
downloads="[Linux]($latest/download/halo-linux-release.zip) · [Windows]($latest/download/halo-windows-release.zip) · [Android]($latest/download/halo-android-release.zip) ([all builds]($latest))"
downloads="[Quest]($latest/download/ringfall-mjolnir-vr-android-release.zip) · [Steam Frame]($latest/download/Install-Ringfall-Mjolnir-VR.desktop) ([all builds]($latest))"
if [ -n "$RELEASE_TAG" ]; then
release="[$RELEASE_TAG]($SERVER/$REPOSITORY/releases/tag/$RELEASE_TAG)"
else
+5
View File
@@ -30,3 +30,8 @@ original/
# CI build outputs (tools/ci_build.py)
dist/
# The app signing key's local settings and keys (port/android/app/build.gradle; never commit)
port/android/signing.properties
*.jks
*.keystore
+73 -120
View File
@@ -1,140 +1,93 @@
# Halo: Combat Evolved for Linux, Windows and Android
<p align="center">
<img src="port/android/steam_frame_grid/library.png" width="640" alt="Ringfall: Mjolnir VR">
</p>
[![Join our Discord](https://invidget.switchblade.xyz/9gqcHyr5km)](https://discord.gg/9gqcHyr5km)
# Ringfall: Mjolnir VR
This project is a port of the Halo: Combat Evolved decompilation to Linux,
Windows and Android. The decompilation is of the Xbox build 2342
(`cachebeta.exe`, SHA-256
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`).
Ringfall: Mjolnir VR plays **Halo: Combat Evolved** for the original Xbox in
virtual reality, on the **Meta Quest 3 / 3S** and the **Steam Frame**. You
play your own copy: the game data comes from your own disc image of the
game, and this project includes none of it.
<img width="1289" height="995" alt="The game on Linux" src="https://github.com/user-attachments/assets/0d3ad50f-f8b8-46cf-aef8-e3661da2a7d7" />
> **Unofficial fan project.** Ringfall: Mjolnir VR is not affiliated with,
> endorsed or sponsored by Microsoft, Xbox Game Studios, Bungie or Halo
> Studios. Halo is a trademark of Microsoft Corporation; it is named here
> only to say which game the project works with. No game data, art or
> music of the game is distributed.
The port starts from the decompilation of [bnunu/halo-1](https://github.com/bnunu/halo-1).
That project is a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
## What it does
## Download
- **Stereo 6DoF VR** at the headset's refresh rate (72 to 120 Hz), the HUD,
menus, scope and crosshair as layers of their own.
- **Your hands and arms**: the weapon in the hand, its shots from the
muzzle, arms by inverse kinematics, fingers from the controllers;
two-handed weapons, melee by swinging, grenades thrown with the hand,
physical crouch and lean.
- **Vehicles**: the Warthog's wheel turned with your hands, turrets with
handles, first-person seats.
- **Comfort**: snap or smooth turning in fine steps, comfort vignettes,
left-handed mode with mirrored controls, buttons remapped freely,
graphics presets, refresh rate and resolution in the VR options.
- **Multiplayer**: the PC version's menus and server browser, join codes,
password lobbies, co-op campaign, bots that make room for people, up to
128 players; cross-play with the desktop and phone builds of
[OpenCE](https://github.com/OpenCommunityEdition/OpenCE).
GitHub Actions builds the game for each commit. These links download the
builds of the latest release:
## Install
| Platform | Release | Debug |
| --- | --- | --- |
| Linux | [halo-linux-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-debug.zip) |
| Windows | [halo-windows-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-debug.zip) |
| Android | [halo-android-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-debug.zip) |
You need your own Xbox disc image of Halo: Combat Evolved (`.iso` or
`.xiso`, any version).
Use the release build to play. The debug build stops at the first failed
assertion and writes it to the log. Use the debug build to find and report
problems.
### Meta Quest 3 / 3S
The game updates itself. At start-up it looks for a newer release, and asks
if you want to install it. Refer to "Updates" in
[port/linux/README.md](port/linux/README.md#updates).
1. Download `ringfall-mjolnir-vr-android-release.zip` from the
[latest release](../../releases/latest) and unpack it.
2. Install the APK with [SideQuest](https://sidequestvr.com/) or
`adb install`.
3. Copy your disc image to the Quest's `Download` folder (USB cable, then
the Quest's storage on your computer).
4. Start **Ringfall: Mjolnir VR** (Library > Unknown Sources), select
**Choose disc image** and pick it. The game copies its maps out of the
image (about 1.8 GB) and starts. You can delete the image afterwards.
Each build of the `main` branch that passes on all three platforms is a new
release. The [Releases](https://github.com/OpenCommunityEdition/OpenCE/releases)
page keeps the last five releases. If the latest build has a problem, get
an older build from that page.
The game looks for new releases when it starts and offers to update itself.
## Game data
### Steam Frame
The port does not include the game data. Download an Xbox disc image
(`.xiso` or `.iso`) of Halo: Combat Evolved. All versions of the game
operate. The maps of the European (PAL) version were made for a slower
console. The port changes them to play as the North American (NTSC) maps do,
so players of the two versions can play together.
1. In Desktop Mode, put your disc image in `Downloads`.
2. Download `Install-Ringfall-Mjolnir-VR.desktop` from the
[latest release](../../releases/latest) and open it.
3. Answer the dialogs: the installer finds the disc image, copies the game
data out of it, adds **Ringfall: Mjolnir VR** to your Steam library and
switches back to Gaming Mode.
1. Start the game.
2. At the first start, the game asks for the disc image. Select it.
3. The game extracts the `maps/` folder. Then the game starts.
Open the same file again to update. Details:
[port/android/STEAM_FRAME.md](port/android/STEAM_FRAME.md).
On Linux and Windows, the game puts `maps/` next to the executable. On
Android, copy the disc image to the phone first. The app puts `maps/` in its
data folder. Refer to [port/android/README.md](port/android/README.md).
## Documentation
## Platforms
Each platform has its own instructions:
| Platform | Instructions |
| Topic | Where |
| --- | --- |
| Linux (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/linux/README.md](port/linux/README.md) |
| Windows (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/windows/README.md](port/windows/README.md) |
| Android (arm64 app, OpenGL ES 3, SDL3) | [port/android/README.md](port/android/README.md) |
| The Android/VR app: build, controls, settings | [port/android/README.md](port/android/README.md) |
| The Steam Frame | [port/android/STEAM_FRAME.md](port/android/STEAM_FRAME.md) |
| What is done and what comes next | [port/android/VR_ROADMAP.md](port/android/VR_ROADMAP.md) |
| The port it is built on (desktop builds, netcode, build options) | [docs/OPENCE_README.md](docs/OPENCE_README.md) |
The Linux README also gives the controls, the settings and the multiplayer
functions. These are almost the same on all platforms.
To build it: `python3 configure.py --release`, then `ninja android_apk`
(Android SDK and NDK needed; see the Android README).
## Multiplayer
## Credits
The game can play system link games on a local network and on the internet:
- [OpenCE](https://github.com/OpenCommunityEdition/OpenCE), the port of the
decompilation to Linux, Windows and Android that this project follows.
- The decompilation: [bnunu/halo-1](https://github.com/bnunu/halo-1), a fork
of [punpckhdq/halo](https://github.com/punpckhdq/halo).
- Ideas for the arms, hands and left-handed mode from
[HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR) and
[astromaddie/HaloCE-VR](https://github.com/astromaddie/HaloCE-VR) (CC0).
- The disc image readers follow [extract-xiso](https://github.com/XboxDev/extract-xiso)
(its license: [port/third_party/extract-xiso](port/third_party/extract-xiso/LICENSE.TXT)).
- Fonts: Overpass (SIL Open Font License).
- A system link game can have up to 128 players on up to 128 machines.
- Linux, Windows and Android machines can play in the same game.
- An invite link lets a machine join a game on the internet. No server of
this project is necessary.
- The netcode is new. Each machine moves its own player at once,
and the host makes the decisions for the game. Refer to
[port/linux/NETCODE.md](port/linux/NETCODE.md).
## Build the game
You do not need the Xbox SDK. The port supplies the SDK declarations that
the game uses. Refer to [port/include/xdk](port/include/xdk/README.md).
To build the game:
1. Install Python and [ninja](https://ninja-build.org/).
2. Install the tools for your platform. Refer to the README for the
platform.
3. In the root folder of the repository, enter `python configure.py`.
4. Enter `ninja` with the target for the platform:
| Target | Result |
| --- | --- |
| `ninja linux` | `build/linux/halo` |
| `ninja windows` (on Windows) | `build/windows/halo.exe` and `SDL3.dll` |
| `ninja android_apk` | `port/android/app/build/outputs/apk/debug/app-debug.apk` |
If you enter `ninja` without a target, ninja builds the game for the
computer that you use.
`tools/ci_build.py` makes the same builds as GitHub Actions. For example,
enter `python tools/ci_build.py linux release`.
### Build options
Give these options to `configure.py`:
| Option | Result |
| --- | --- |
| (none) | A debug build. A failed assertion stops the game. |
| `--release` | A release build. The game does not examine assertions, as in the retail game. |
| `--portable` | The Linux and Windows builds operate on all x86-64 processors. Use this option for builds that you give to other persons. |
| `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. |
| `--pgo=off` | No profile-guided optimization. |
| `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". |
Without `--portable`, the Linux and Windows builds use all the instructions
of the processor that builds them (`-march=native`). Such a build does not
always start on a different computer.
### Optimization profiles
The builds use profiles of the game to optimize the code:
- `pgo/halo_linux.profdata` for Linux and Android.
- `pgo/halo_windows.profdata` for Windows.
The profiles need clang 22 or later. With an older clang, the builds do not
use the profiles.
To record a new profile:
1. Delete the profile.
2. Enter `python configure.py --pgo=train`.
3. Enter `ninja linux` or `ninja windows`.
The build then plays the main menu and the first minute of each campaign
level. This procedure continues for approximately 15 minutes. The game
data must be in `assets/`.
The project's own code is released under [CC0](LICENSE.md), as the
decompilation it builds on.
+140
View File
@@ -0,0 +1,140 @@
# Halo: Combat Evolved for Linux, Windows and Android
[![Join our Discord](https://invidget.switchblade.xyz/9gqcHyr5km)](https://discord.gg/9gqcHyr5km)
This project is a port of the Halo: Combat Evolved decompilation to Linux,
Windows and Android. The decompilation is of the Xbox build 2342
(`cachebeta.exe`, SHA-256
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`).
<img width="1289" height="995" alt="The game on Linux" src="https://github.com/user-attachments/assets/0d3ad50f-f8b8-46cf-aef8-e3661da2a7d7" />
The port starts from the decompilation of [bnunu/halo-1](https://github.com/bnunu/halo-1).
That project is a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
## Download
GitHub Actions builds the game for each commit. These links download the
builds of the latest release:
| Platform | Release | Debug |
| --- | --- | --- |
| Linux | [halo-linux-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-debug.zip) |
| Windows | [halo-windows-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-debug.zip) |
| Android | [halo-android-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-debug.zip) |
Use the release build to play. The debug build stops at the first failed
assertion and writes it to the log. Use the debug build to find and report
problems.
The game updates itself. At start-up it looks for a newer release, and asks
if you want to install it. Refer to "Updates" in
[port/linux/README.md](../port/linux/README.md#updates).
Each build of the `main` branch that passes on all three platforms is a new
release. The [Releases](https://github.com/OpenCommunityEdition/OpenCE/releases)
page keeps the last five releases. If the latest build has a problem, get
an older build from that page.
## Game data
The port does not include the game data. Download an Xbox disc image
(`.xiso` or `.iso`) of Halo: Combat Evolved. All versions of the game
operate. The maps of the European (PAL) version were made for a slower
console. The port changes them to play as the North American (NTSC) maps do,
so players of the two versions can play together.
1. Start the game.
2. At the first start, the game asks for the disc image. Select it.
3. The game extracts the `maps/` folder. Then the game starts.
On Linux and Windows, the game puts `maps/` next to the executable. On
Android, copy the disc image to the phone first. The app puts `maps/` in its
data folder. Refer to [port/android/README.md](../port/android/README.md).
## Platforms
Each platform has its own instructions:
| Platform | Instructions |
| --- | --- |
| Linux (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/linux/README.md](../port/linux/README.md) |
| Windows (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/windows/README.md](../port/windows/README.md) |
| Android (arm64 app, OpenGL ES 3, SDL3) | [port/android/README.md](../port/android/README.md) |
The Linux README also gives the controls, the settings and the multiplayer
functions. These are almost the same on all platforms.
## Multiplayer
The game can play system link games on a local network and on the internet:
- A system link game can have up to 128 players on up to 128 machines.
- Linux, Windows and Android machines can play in the same game.
- An invite link lets a machine join a game on the internet. No server of
this project is necessary.
- The netcode is new. Each machine moves its own player at once,
and the host makes the decisions for the game. Refer to
[port/linux/NETCODE.md](../port/linux/NETCODE.md).
## Build the game
You do not need the Xbox SDK. The port supplies the SDK declarations that
the game uses. Refer to [port/include/xdk](../port/include/xdk/README.md).
To build the game:
1. Install Python and [ninja](https://ninja-build.org/).
2. Install the tools for your platform. Refer to the README for the
platform.
3. In the root folder of the repository, enter `python configure.py`.
4. Enter `ninja` with the target for the platform:
| Target | Result |
| --- | --- |
| `ninja linux` | `build/linux/halo` |
| `ninja windows` (on Windows) | `build/windows/halo.exe` and `SDL3.dll` |
| `ninja android_apk` | `port/android/app/build/outputs/apk/debug/app-debug.apk` |
If you enter `ninja` without a target, ninja builds the game for the
computer that you use.
`tools/ci_build.py` makes the same builds as GitHub Actions. For example,
enter `python tools/ci_build.py linux release`.
### Build options
Give these options to `configure.py`:
| Option | Result |
| --- | --- |
| (none) | A debug build. A failed assertion stops the game. |
| `--release` | A release build. The game does not examine assertions, as in the retail game. |
| `--portable` | The Linux and Windows builds operate on all x86-64 processors. Use this option for builds that you give to other persons. |
| `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. |
| `--pgo=off` | No profile-guided optimization. |
| `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". |
Without `--portable`, the Linux and Windows builds use all the instructions
of the processor that builds them (`-march=native`). Such a build does not
always start on a different computer.
### Optimization profiles
The builds use profiles of the game to optimize the code:
- `pgo/halo_linux.profdata` for Linux and Android.
- `pgo/halo_windows.profdata` for Windows.
The profiles need clang 22 or later. With an older clang, the builds do not
use the profiles.
To record a new profile:
1. Delete the profile.
2. Enter `python configure.py --pgo=train`.
3. Enter `ninja linux` or `ninja windows`.
The build then plays the main menu and the first minute of each campaign
level. This procedure continues for approximately 15 minutes. The game
data must be in `assets/`.
+20 -10
View File
@@ -1,5 +1,7 @@
# Android
The app of Ringfall: Mjolnir VR (application id `io.github.cesarus85.ringfall`;
its Java classes keep the decompilation's package, `com.halo.decomp`).
`ninja android` builds the game for 64-bit ARM Android (arm64-v8a).
`ninja android_apk` makes an app from it:
`port/android/app/build/outputs/apk/debug/app-debug.apk`.
@@ -42,7 +44,7 @@ ninja) and these items:
The game needs the `maps/` folder from an Xbox disc image (`.xiso` or
`.iso`) of any version of the game. The app extracts `maps/` from the disc
image. The app keeps the data in `/sdcard/Android/data/com.halo.decomp/files`.
image. The app keeps the data in `/sdcard/Android/data/io.github.cesarus85.ringfall/files`.
To install the data with the app:
@@ -57,16 +59,16 @@ To install the data with the app:
To install the data from a computer:
1. Start the app one time. The app makes its folders.
2. Enter `adb push <folder>/. /sdcard/Android/data/com.halo.decomp/files/`.
2. Enter `adb push <folder>/. /sdcard/Android/data/io.github.cesarus85.ringfall/files/`.
| Item | Location in `/sdcard/Android/data/com.halo.decomp/files` |
| Item | Location in `/sdcard/Android/data/io.github.cesarus85.ringfall/files` |
| --- | --- |
| Saved games (`z:\` and `u:\`) | `save` |
| Log | `debug.txt` |
| Settings | `config.toml` |
To make a copy of the saved games, enter
`adb pull /sdcard/Android/data/com.halo.decomp/files/save`.
`adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/save`.
## Controls
@@ -100,9 +102,9 @@ not accept touch input.
The settings are in `config.toml` in the data folder of the app. To change
them:
1. Enter `adb pull /sdcard/Android/data/com.halo.decomp/files/config.toml`.
1. Enter `adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/config.toml`.
2. Change the file.
3. Enter `adb push config.toml /sdcard/Android/data/com.halo.decomp/files/`.
3. Enter `adb push config.toml /sdcard/Android/data/io.github.cesarus85.ringfall/files/`.
At the first start, the game writes the file with the default values. To
get the default values again, delete the file.
@@ -148,14 +150,22 @@ select "Yes":
1. The app downloads the new version.
2. The package installer of Android opens. At the first update, Android asks
you to let Halo install apps. Allow it.
you to let the game install apps. Allow it.
3. Select "Update". Android replaces the app.
4. Select "Open" to start the new version.
To install over the previous version, each build must have the same
signature. GitHub Actions signs each build with the key in the
`ANDROID_KEYSTORE_BASE64` and `ANDROID_KEYSTORE_PASSWORD` secrets of the
repository. If you installed a build that has a different signature, remove
repository. A computer that builds for its own devices signs with the same
key when `port/android/signing.properties` (not in the repository) names it:
storeFile=/path/to/ringfall-release.jks
password=<the keystore's password>
keyAlias=ringfall
Keep the keystore and its password safe: a lost key means every player must
remove the app before the next version. If you installed a build that has a different signature, remove
that build before you install a new build. Removing the app deletes its data
folder: first make a copy of `maps/` and `save/`.
@@ -182,7 +192,7 @@ instead.
Controls in play (in the menus the buttons are the Xbox's by position):
| Controller | Halo |
| Controller | Game |
| --- | --- |
| right trigger / left trigger | fire / throw grenade |
| right grip / left grip | melee / switch weapon |
@@ -230,7 +240,7 @@ through a shader that decodes to linear, and the stored bytes are the
game's. Camera shake is off in VR.
A file named `xr_test` in the data folder
(`adb shell touch /sdcard/Android/data/com.halo.decomp/files/xr_test`)
(`adb shell touch /sdcard/Android/data/io.github.cesarus85.ringfall/files/xr_test`)
starts a self-test instead of the game, which needs no game data: each eye
in its own colour (red while the right trigger is held), a checkerboard
screen in the room (its top-left square red), and the controllers' buttons
+51 -12
View File
@@ -4,6 +4,43 @@ The same APK as for the Quest runs on the Steam Frame, in Lepton (SteamOS's
Android, a Waydroid fork: Android 11, arm64), with SteamVR's OpenXR runtime.
What differs is found at run time, not by a build of its own.
## Installing it (players)
On the Frame, in Desktop Mode (Steam menu > Power > Switch to Desktop):
1. Put your own disc image of Halo: Combat Evolved for the original Xbox (`.iso` or
`.xiso`, any version) in Downloads (or Documents, the Desktop, a memory
card).
2. Download `Install-Ringfall-Mjolnir-VR.desktop` from the latest release and open it
(double-click; confirm running it).
3. Answer the dialogs: the installer finds the disc image (or asks for it),
copies the game data out of it, adds "Ringfall: Mjolnir VR" to the Steam library
and switches back to Gaming Mode. Start it from the library there.
Opening the same file again updates the game (settings, saves and game data
stay).
The desktop file (`steam_frame_installer.desktop`) downloads the latest
release's `ringfall-mjolnir-vr-steam-frame.zip` (`tools/steam_frame_bundle.py`, made by
the release job of `.github/workflows/build.yml`) and runs its
`install.sh` (`steam_frame_install.sh`), with zenity dialogs:
- `~/Documents/RingfallMjolnirVR`: the launcher (`ringfall-mjolnir-vr.sh`)
and the release APK (`RingfallMjolnirVR.apk`); the folder of the builds
before the name, `~/Documents/HaloCE`, renamed to it, and their "Halo VR"
shortcut replaced;
- the maps folder out of the disc image (`steam_frame_xiso.py`, the app's
`XisoExtractor.java` in Python), unless `maps/ui.map` is there;
- the Steam shortcut, unless it is there: added by Steam itself
(`steam://addnonsteamgame`, as SteamOS's "Add to Steam" does) from
`Ringfall Mjolnir VR.desktop` beside the game, then the library pictures
under the id Steam gave it. Steam is not closed: on the Frame, Desktop
Mode runs inside Gaming Mode's session, and closing Steam ends it (and the
installer with it). An old "Halo VR" shortcut is left for the player to
remove (Steam would write it back);
- what it did in `install.log` beside the game.
`HALO_VR_UNATTENDED=1` (and `HALO_VR_IMAGE=<image>`) runs it without
dialogs, for testing over adb; it leaves Steam and its shortcuts alone.
## The device
- Steam Settings > System > Enable Developer Mode; then start "Lepton
@@ -15,7 +52,7 @@ What differs is found at run time, not by a build of its own.
connected as well.
- Install with `adb install -r --no-streaming`. It prints "Lepton post
installation failed due to APP_PATH being unset", but installs. Start the
game with `am start -n com.halo.decomp/.LauncherActivity`.
game with `am start -n io.github.cesarus85.ringfall/com.halo.decomp.LauncherActivity`.
- Lepton empties `/sdcard/Android/data` whenever it starts again: the game's
data must live elsewhere (stage 2). `/sdcard/Documents` and
`/sdcard/Download` are the SteamOS user's folders and are kept.
@@ -33,7 +70,7 @@ What differs is found at run time, not by a build of its own.
| # | Stage | State |
| --- | --- | --- |
| 1 | Base: the fixes above on the current main | done |
| 2 | The game's data where Lepton keeps it | done: `/home/steamos/Documents/HaloCE` |
| 2 | The game's data where Lepton keeps it | done: `/home/steamos/Documents/RingfallMjolnirVR` |
| 3 | Menus, HUD and text visible (quad layers) | done: their alpha from their colour |
| 4 | The freeze at the cryo bay's calibration | not seen again (2026-10-04) |
| 5 | The Frame's controllers | done: their own profile, the Xbox's layout |
@@ -43,10 +80,10 @@ What differs is found at run time, not by a build of its own.
## Starting it on the Frame
A button in the Steam library ("Halo VR", a non-Steam game) runs
`~/Documents/HaloCE/halo-vr.sh` (`port/android/steam_frame_launch.sh`),
A button in the Steam library ("Ringfall: Mjolnir VR", a non-Steam game)
runs `~/Documents/RingfallMjolnirVR/ringfall-mjolnir-vr.sh` (`port/android/steam_frame_launch.sh`),
which starts the game as Steam starts an Android title of its own:
`lepton waitforexitandrun -- ~/Documents/HaloCE/HaloVR.apk`, in a container
`lepton waitforexitandrun -- ~/Documents/RingfallMjolnirVR/RingfallMjolnirVR.apk`, in a container
of its own (`steamlaunch-4290000001`) that Lepton installs the APK into
anew whenever the file is newer. Updating the game is replacing that APK.
@@ -61,7 +98,7 @@ What the script does that Steam would for an Android title:
quit (it writes `quit_requested.txt`).
Steam's "Exit game" does not reach the game (its process is the container's,
not Steam's): quit it with the VR options' "Quit Halo" (Y in the main or
not Steam's): quit it with the VR options' "Quit the game" (Y in the main or
pause menu, twice). A container ended within 30 s of its start makes
Lepton drop the app's install, so the next start installs it again.
@@ -75,13 +112,15 @@ beside Lepton's `<frame-ip>:5555`.
Lepton uninstalls the app and empties its folders whenever it starts again.
The game's data lives in the SteamOS user's Documents instead: when
`/home/steamos/Documents/HaloCE` holds `maps/ui.map` and is writable, the
launcher and the game take it as their data folder (config.toml, saves and
debug.txt go there too). Once:
`/home/steamos/Documents` can be written, the launcher makes
`/home/steamos/Documents/RingfallMjolnirVR` and the launcher and the game take it as
their data folder (config.toml, saves and debug.txt go there too), so a
disc image imported in the app stays as well. The installer above fills it;
by hand, once:
adb -s <frame-ip>:5555 shell mkdir -p /home/steamos/Documents/HaloCE/maps
adb -s <frame-ip>:5555 push assets/maps/. /home/steamos/Documents/HaloCE/maps/
adb -s <frame-ip>:5555 shell chmod -R 777 /home/steamos/Documents/HaloCE
adb -s <frame-ip>:5555 shell mkdir -p /home/steamos/Documents/RingfallMjolnirVR/maps
adb -s <frame-ip>:5555 push assets/maps/. /home/steamos/Documents/RingfallMjolnirVR/maps/
adb -s <frame-ip>:5555 shell chmod -R 777 /home/steamos/Documents/RingfallMjolnirVR
After a Lepton restart only the APK is installed again. (Use the path under
/home, not /sdcard/Documents: Android 11's scoped storage stands between the
+308
View File
@@ -0,0 +1,308 @@
# VR roadmap
What Ringfall: Mjolnir VR (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/RingfallMjolnirVR` |
| Rendering | Both eyes in one pass (multiview) on the Quest; one by one on SteamVR, where multiview's eyes did not fuse; lens flares at their depth in stereo; HUD, menus, scope, crosshair and names as layers of their own |
| Aiming | The weapon in the hand, its shots from the muzzle, laser and crosshair options |
| Body | Physical crouch (`vr.crouch_limit`) beside the crouch button, leaning (`vr.lean_limit`), melee by swinging, grenades thrown with the left hand, two-handed weapons |
| Vehicles | The Warthog's wheel turned with the hands, right trigger throttle, left brake; first-person seats; turrets with handles |
| Comfort | Snap or smooth turning, a comfort vignette in vehicles, VR options in the main and pause menus (Y), a page of every controller button |
| Menus | The PC version's menus in VR (`vr.menus`), clicked with a laser; text typed on the on-screen keyboard |
| Multiplayer | Up to date with OpenCE (network version 20): cross-play, its server browser, co-op campaign, password lobbies; join codes and the VR online list |
| Bots | Bots in multiplayer: on foot (paths, items, objectives), in vehicles and at turrets; they make room for a joining person |
| Controls | Left-handed mode (`vr.handedness`): the weapon in the left hand, the arms and grips mirrored, sticks and buttons mirrored on both controllers (`vr.mirror_controls`), the assault rifle's counter read the right way round (2026-10-06) |
| Graphics | Presets low / medium / high / max (`vr.graphics`), refresh rate and resolution in the VR options, all kept over a restart (2026-10-06) |
| Arms and hands | Both arms by IK from shoulders that follow the torso, the elbow led by the hand's roll; fingers from trigger, grip and touch; `vr.body` arms / hands / off, `vr.fingers` (2026-10-06) |
## Next, in order
| # | Step | State |
| --- | --- | --- |
| 1 | Steam Frame CPU time: Mesa's trace markers off (after astromaddie/HaloCE-VR), no `glGet` stalls in the visibility counters' copies | **done** (in `main` 2026-10-06): markers quieted, no problems; the frame's work about unchanged (11-12.5 ms of 13.9 in a bot game) |
| 3a | Visible arms and hands: the arms by IK to the controllers, fingers by trigger and grip | **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good; 0.02 ms a frame) |
| 2 | Controls and comfort: left-handed mode, buttons remapped, flashlight by tapping the head, finer turning and vignette choices, graphics presets and refresh rate in the VR options | first part **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good): left-handed mode, graphics presets, refresh rate and resolution in the VR options. Second part **built** (branch `feature/vr-comfort`; Quest 3 good 2026-10-06): buttons remapped, finer turning and vignette choices; the flashlight by tapping the head taken out (did not work) |
| 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) | import **built** (branches `feature/release-install`, `feature/rename-ringfall`): the app's import (Quest; on the Frame kept in `~/Documents/RingfallMjolnirVR`), a Steam Frame installer from Desktop Mode (`STEAM_FRAME.md`), a release signing key; test pending |
| R | Release as **Ringfall: Mjolnir VR** (no "Halo" in the product's name): app name, id `io.github.cesarus85.ringfall`, icon and library pictures (`tools/ringfall_art.py`), Frame folder and installer, release assets, README; the repository renamed | **built** (branch `feature/rename-ringfall`); test pending |
## Step 2, first part (done)
Left-handed mode and graphics presets, after HaloCE-Quest-VR (CC0): its
mirrored first-person model, its graphics switches.
Left-handed (`vr.handedness = "left"`):
- The weapon in the left hand: aim, shots, muzzle, laser, crosshair,
scope disc, turret aim. The first-person model is posed as the right
hand's in a mirror image of the room ("model space", `halo_vr.h`) and
mirrored back; a model whose node matrices mirror has its triangles'
winding turned over (`rasterizer_set_model_skinning`, `d3d8_gl.c`). The
arms reach to the other shoulders, the foregrip is gripped with the
right hand, the right hand's fingers follow its controller.
- Grenades held and thrown with the right trigger and hand; melee by the
left grip (a swing of either hand, as before); the motion tracker on the
right wrist (`halo_xr_submit.wrist_hand`); the rumble's light motor in
the weapon hand.
- The left trigger fires, the right one throws; at the Warthog's wheel the
left trigger accelerates, the right one brakes.
- With `vr.mirror_controls` (default): the right stick moves, the left
turns and steers; their clicks, the bumpers and the face buttons change
sides (Quest: X jumps, Y light / held grenades, A action and reload, B
switch weapon; Steam Frame: the left D-pad does A, B, X, Y as their
mirror image, down jump, left light, right action, up switch weapon,
and A, B, X, Y do the D-pad's). The menus keep their buttons; the
laser pointer works from either hand as before.
- Other machines see the shots leave the left hand (the network message
is unchanged: aim and muzzle from the camera).
- The assault rifle's ammo counter and compass read the right way round
on the mirrored model (2026-10-06): their geometry stays mirrored, each
digit's bitmap is drawn turned over in u and the digits' order across
the display swapped (numeric shaders drawn mirrored,
`rasterizer_xbox_transparent_geometry.c`). Checked flat in the emulator
with `debug.mirror_first_person` (the first-person weapon mirrored off
the headset): 60, 48, 36 ... 06 read correctly. Steam Frame good
(2026-10-06).
- Kept as they are (accepted 2026-10-06): the small static icons under the
counter (round and "x") stay mirror images (a texture atlas, not
numeric). The game's prompts still name the Xbox's buttons.
- Settings kept: on Android `config.toml` is written beside itself and
renamed over (`port_config.c`), so one pushed by adb (the shell's, which
the app could not open for writing) no longer loses the VR options'
changes at a restart.
- Headset tests 2026-10-06: Quest 3 and Steam Frame good (left hand,
controls mirrored and not, counter, presets, refresh rates, resolution,
settings kept over a restart).
Graphics (`vr.graphics`, `port/linux/game/vr_graphics.c`), set each frame
on the game's own switches:
| Preset | Leaves out |
| --- | --- |
| `"high"` (default) | nothing: as before |
| `"medium"` | object shadows, environment reflections, lens flares |
| `"low"` | also specular light, bump maps, grass and other detail objects, the screen's fog layer, contrails, weather, camouflage's second pass |
| `"max"` | nothing; models lit for each pixel, shadows at 512x512 |
A client in another's game keeps grass, lens flares and the fog layer as
the host's rules have them (`cheats.c`). Shadows, specular passes,
reflections and detail objects are draws (CPU, which the Frame's GL
driver is short of); bump maps, fog, camouflage and per-pixel lighting
the GPU's fill.
VR options (Y): two pages, PLAY (the comfort rows, "Weapon hand",
"Mirrored controls") and MORE ("Graphics", "Refresh rate", "Resolution",
the controller buttons page, Quit); four since the second part (below). The refresh rate steps through what
the runtime offers and changes at once; on the Steam Frame the row shows
SteamOS's rate. The resolution (`vr.resolution_scale`, 70-120%) takes a
restart. The controller buttons page shows the layout as set.
| Setting | Values |
| --- | --- |
| `vr.handedness` | `"right"` (default), `"left"` |
| `vr.mirror_controls` | `true` (default): sticks and buttons mirrored when left-handed; `false`: as right-handed (triggers and grips still follow the hands) |
| `vr.graphics` | `"low"`, `"medium"`, `"high"` (default), `"max"` |
| `vr.refresh_rate` | Hz, as offered (Quest 3: 72, 80, 90, 120) |
| `vr.resolution_scale` | 0.7 to 1.2, on restart |
To try in the headset (Quest 3 and Steam Frame):
1. Left-handed: aim, laser, crosshair, scope of sniper and rocket
launcher; weapon held right in the left hand, arms from the right
shoulders, nothing inside out (faces culled the wrong way would show
the inside of the gun or the glove).
2. Two-handed grip with the right hand; grenades in the right hand (held
and thrown); melee by grip and by swing; motion tracker on the right
wrist.
3. Buttons and sticks mirrored on the Touch and the Frame controllers
(every function reachable), then `Mirrored controls: Off`.
4. Vehicles: wheel, throttle and brake, steering stick, a gunner's aim;
menus and the laser pointer; a network game (shots from the left hand
on the other machine).
5. Graphics: each preset's look and smoothness (`vr.log_performance`),
in a bot game; Max on the Quest at 90 Hz; refresh rates 72/80/90/120
on the Quest; the Frame's row.
## Step 2, second part (built)
On branch `feature/vr-comfort`; tried on the Quest 3 2026-10-06 (with the
rename): good. The flashlight by tapping the head (after HaloCE-Quest-VR)
did not work in the headset and was taken out again; the button stays.
The VR
options (Y) have four pages: PLAY ("Weapon hand", "Mirrored controls",
laser, crosshair, motion tracker, body, fingers, the Warthog's wheel),
COMFORT (turning, the vignettes), BUTTONS (the actions' buttons,
"Reset buttons to defaults", the controller buttons page) and MORE (graphics, refresh rate, resolution, Quit).
Buttons remapped (BUTTONS page; `vr.c` `controls_remapped`):
- Jump, the flashlight (held: switch grenades), action/reload, switch
weapon, crouch, zoom and melee each go on one of seven buttons: A, B,
X, Y, the move stick's click, the turning stick's, the weapon hand's
grip. Moving an action to a button swaps it with the action there, so
none is ever left without one.
- The triggers, the free hand's grip, the menu button, the Steam Frame's
bumpers, D-pad and View button keep theirs; the menus keep the buttons
as they are.
- Applied after the left-handed mirroring, on the gamepad's sides: the
same on the Touch and the Frame controllers, and a left-handed player's
buttons are the mirror image of the right-handed ones (on the Frame,
mirrored, the left D-pad's). The defaults are the old layout, the
host's bindings and the ABI unchanged.
- The melee's aim and jab follow its button; the grips on a Warthog's
wheel press nothing, whatever the grip does.
- The controller buttons page shows each action where its button is.
Comfort (COMFORT page): snap turns 10 to 45 degrees by 5, 60, 75, 90;
smooth turning 30 to 150 degrees a second by 15, then to 300 by 30; the
vehicle vignette Off, 15 to 100%; the on-foot vignette (off by default)
while the stick turns smoothly, or also while the move stick moves, as
far as the sticks are pushed, with its own strength. The old values are
among the steps; defaults unchanged.
Transparent geometry: room for 2048 groups a window (the Xbox's 384,
below).
| Setting | Values |
| --- | --- |
| `vr.button_jump` | `"a"` (default), `"b"`, `"x"`, `"y"`, `"move_stick"`, `"turn_stick"`, `"grip"`: as right-handed, mirrored with `vr.mirror_controls` |
| `vr.button_light` | the flashlight, held switch grenades; `"b"` (default) |
| `vr.button_action` | action, reload; `"x"` (default) |
| `vr.button_weapon` | switch weapon; `"y"` (default) |
| `vr.button_crouch` | `"move_stick"` (default) |
| `vr.button_zoom` | `"turn_stick"` (default) |
| `vr.button_melee` | `"grip"` (default) |
| `vr.foot_vignette` | `"off"` (default), `"turning"` (smooth turns), `"moving"` (and moving) |
| `vr.foot_vignette_strength` | 0.15 to 1, `0.7` (default) |
| `debug.vr_options` | `false` (default); `true` shows the VR options off the headset (the emulator) |
Checked in the emulator (`debug.vr_options`): the four pages without
overlaps, each row's values, swaps and reset, the controller buttons page
right- and left-handed after swaps, the settings kept over a restart, a
15-bot game as before.
To try in the headset (Quest 3 and Steam Frame):
1. Defaults: every button as before (jump, light/held grenades, action,
switch weapon, crouch, zoom, melee by grip and by swing), right- and
left-handed, mirrored and not; the menus' A and B.
2. BUTTONS: swap jump and melee (jump on the grip, melee on A): both work,
the melee jabs and aims where the head looks; light on a stick click
(tap: light, hold: grenades); the controller buttons page says so.
At the Warthog's wheel with jump on the grip: no jump while holding
the wheel. Reset: all back.
3. The same left-handed with mirrored controls (on the Frame the left
D-pad), and with `Mirrored controls: Off`.
4. COMFORT: snap 10 and 90, smooth 30 and 300; vehicle vignette 15% and
100%; on-foot vignette "Smooth turns" (nothing with snap turns),
"Moving, turning", strength 15% and 100%; no vignette in a vehicle
from the on-foot one, none in the menus.
5. A game of 30 and more players: effects, glass and water stay; the log
(`adb logcat -s halo`) says `rasterizer: N transparent geometry groups
in a window` once a window passes 384; frame time as before.
## 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 overflowed in
games of 30 and more players (the last submitted, glass, water,
particles, contrails, vanished). Raised to 2048 (320 KB) on
`feature/vr-comfort`, to check in a large game (step 2, second part,
test 6); upstream still has 384.
## 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).
+19 -5
View File
@@ -13,16 +13,30 @@ def buildNumberText = System.getenv("HALO_BUILD_NUMBER") ?: "0"
def buildNumber = buildNumberText.isInteger() ? buildNumberText.toInteger() : 0
// the workflow's signing key (.github/workflows/build.yml): every build of
// main signed with it can install over the one before; without it, the
// main signed with it can install over the one before; on a computer that
// builds for its own devices, the same key from port/android/signing.properties
// (not in the repository: storeFile, password, keyAlias), so that its
// builds and the releases install over each other; without either, the
// debug key of the computer that builds
def signingKey = System.getenv("HALO_ANDROID_KEYSTORE")
def signingPassword = System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
def signingAlias = System.getenv("HALO_ANDROID_KEY_ALIAS") ?: "ringfall"
def signingProperties = new File(rootDir, "signing.properties")
if (!signingKey && signingProperties.isFile()) {
def properties = new Properties()
signingProperties.withInputStream { properties.load(it) }
signingKey = properties.getProperty("storeFile")
signingPassword = properties.getProperty("password")
signingAlias = properties.getProperty("keyAlias", "ringfall")
}
android {
namespace "com.halo.decomp"
compileSdk 35
defaultConfig {
applicationId "com.halo.decomp"
// (the app's own id; the Java classes keep the decompilation's package)
applicationId "io.github.cesarus85.ringfall"
minSdk 28
targetSdk 35
versionCode Math.max(1, buildNumber)
@@ -60,9 +74,9 @@ android {
if (signingKey) {
halo {
storeFile file(signingKey)
storePassword System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
keyAlias System.getenv("HALO_ANDROID_KEY_ALIAS") ?: "halo"
keyPassword System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
storePassword signingPassword
keyAlias signingAlias
keyPassword signingPassword
}
}
}
@@ -93,7 +93,7 @@
<!-- the downloaded new version, for the package installer (Updater) -->
<provider
android:name=".UpdateProvider"
android:authorities="com.halo.decomp.update"
android:authorities="${applicationId}.update"
android:exported="false"
android:grantUriPermissions="true" />
@@ -27,7 +27,7 @@ import java.nio.channels.FileChannel;
* Starts the game once its data is in place.
*
* The game reads the Xbox game data (the folder holding maps/) from the
* app's external files directory, /sdcard/Android/data/com.halo.decomp/files.
* app's external files directory, /sdcard/Android/data/<application id>/files.
* If it is missing, this screen lets the player pick an Xbox disc image of
* the game (.xiso or .iso, any version) with the system file picker, and
* copies its maps folder there (XisoExtractor), as the desktop games do; or
@@ -35,7 +35,7 @@ import java.nio.channels.FileChannel;
*/
public class LauncherActivity extends Activity {
private static final int PICK_IMAGE = 1;
private static final String SHARED_DATA_ROOT = "/home/steamos/Documents/HaloCE";
private static final String SHARED_DATA_ROOT = "/home/steamos/Documents/RingfallMjolnirVR";
private File dataRoot;
private TextView status;
@@ -49,10 +49,15 @@ public class LauncherActivity extends Activity {
dataRoot = getExternalFilesDir(null);
// the Steam Frame: Lepton empties the app's own folders whenever it
// starts again; the game's data lives in the SteamOS user's
// Documents instead, when it is there (port/android/STEAM_FRAME.md;
// the same choice as host_main.c's)
if (new File(SHARED_DATA_ROOT, "maps/ui.map").isFile() && new File(SHARED_DATA_ROOT).canWrite())
dataRoot = new File(SHARED_DATA_ROOT);
// Documents instead, whenever they can be written, so that a disc
// image imported here stays too (port/android/STEAM_FRAME.md; the
// same choice as host_main.c's)
File shared = new File(SHARED_DATA_ROOT);
if (shared.getParentFile().canWrite() || shared.canWrite()) {
shared.mkdirs();
if (shared.canWrite())
dataRoot = shared;
}
// created by the app, so that files pushed into it with adb stay
// readable (a directory adb creates there belongs to the shell user)
if (dataRoot != null)
@@ -142,15 +147,15 @@ public class LauncherActivity extends Activity {
layout.setBackgroundColor(Color.rgb(12, 16, 20));
TextView title = new TextView(this);
title.setText("Halo needs its game data");
title.setText("Ringfall: Mjolnir VR needs game data");
title.setTextColor(Color.WHITE);
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24);
title.setGravity(Gravity.CENTER);
layout.addView(title);
TextView message = new TextView(this);
message.setText("Choose an Xbox disc image of Halo: Combat Evolved (an .iso or .xiso file, any "
+ "version) on this device. Its maps folder is copied into the app's storage (about 1.8 GB), "
message.setText("Choose your own disc image of Halo: Combat Evolved for the original Xbox (an .iso or "
+ ".xiso file, any version) on this device. Its maps folder is copied into the app's storage (about 1.8 GB), "
+ "and you can delete the image afterwards.\n\n"
+ "You can also copy a maps folder from a computer:\n"
+ "adb push <folder with maps>/. " + (dataRoot != null ? dataRoot.getAbsolutePath() : "") + "/");
@@ -13,13 +13,13 @@ import java.io.FileNotFoundException;
/**
* Hands Android's package installer the downloaded new version of the app
* (Updater), read-only, the one file content://com.halo.decomp.update/halo.apk.
* (Updater), read-only, the one file content://<application id>.update/update.apk.
* (The app has no AndroidX, whose FileProvider does the same.)
*/
public class UpdateProvider extends ContentProvider {
static final String AUTHORITY = "com.halo.decomp.update";
static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".update";
static final String DIRECTORY = "update";
static final String APK = "halo.apk";
static final String APK = "update.apk";
private File apk() {
return new File(new File(getContext().getCacheDir(), DIRECTORY), APK);
@@ -49,7 +49,8 @@ import java.util.zip.ZipInputStream;
final class Updater {
/* the VR port's own releases: the original's, made without VR, must
never replace it (they share the package name) */
private static final String REPOSITORY = "Cesarus85/halo-ce-vr";
private static final String APP_NAME = "Ringfall: Mjolnir VR";
private static final String REPOSITORY = "Cesarus85/ringfall-mjolnir-vr";
private static final String USER_AGENT = "halo-ce-universal-updater";
private static final int TIMEOUT_MILLISECONDS = 20000;
@@ -202,8 +203,8 @@ final class Updater {
if (activity.isFinishing())
return;
new AlertDialog.Builder(activity)
.setTitle("Halo: new version")
.setMessage("A new version of Halo was detected (build " + latest + "; this is build "
.setTitle(APP_NAME + ": new version")
.setMessage("A new version of " + APP_NAME + " was detected (build " + latest + "; this is build "
+ BuildConfig.HALO_BUILD_NUMBER + ").\n\nDo you want to update? The game will close and start "
+ "the new version.")
.setCancelable(false)
@@ -215,7 +216,7 @@ final class Updater {
private static void confirmNever(Activity activity) {
new AlertDialog.Builder(activity)
.setTitle("Halo: new version")
.setTitle(APP_NAME + ": new version")
.setMessage("Stop asking about new versions?\n\nTo ask again, set auto = true in the [update] section "
+ "of config.toml.")
.setCancelable(false)
@@ -232,7 +233,7 @@ final class Updater {
/* ---------- updating */
private static void update(Activity activity, int latest) {
String asset = "halo-android-" + (BuildConfig.DEBUG ? "debug" : "release") + ".zip";
String asset = "ringfall-mjolnir-vr-android-" + (BuildConfig.DEBUG ? "debug" : "release") + ".zip";
File directory = new File(activity.getCacheDir(), UpdateProvider.DIRECTORY);
LinearLayout layout = new LinearLayout(activity);
TextView status = new TextView(activity);
@@ -247,7 +248,7 @@ final class Updater {
layout.addView(status);
layout.addView(bar);
AlertDialog progress = new AlertDialog.Builder(activity)
.setTitle("Halo: new version")
.setTitle(APP_NAME + ": new version")
.setView(layout)
.setCancelable(false)
.show();
@@ -275,7 +276,7 @@ final class Updater {
activity.runOnUiThread(() -> {
progress.dismiss();
new AlertDialog.Builder(activity)
.setTitle("Halo: new version")
.setTitle(APP_NAME + ": new version")
.setMessage("The update failed:\n\n" + e.getMessage())
.setPositiveButton("OK", null)
.show();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Halo</string>
<string name="app_name">Ringfall: Mjolnir VR</string>
</resources>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 21 KiB

+5
View File
@@ -135,6 +135,11 @@ into a layer's image, opaque */
int host_xr_copy_layer_array(int layer, unsigned int texture, int array_layer);
/* darkens the eye images from their edges, strength 0..1 (0: off) */
void host_xr_set_vignette(float strength);
/* the refresh rates the headset offers (Hz, at most maximum of them; 0 when
it lets none be chosen), and the one it runs at (0: unknown); and asking
for the highest offered at or below wanted (0: the highest) */
int host_xr_refresh_rates(float *rates, int maximum, float *current);
void host_xr_set_refresh_rate(float wanted);
/* ---------- Android */
+1
View File
@@ -92,6 +92,7 @@ void *host_resolve_import(const char *name);
/* ---------- SDL / GL (host_sdl.c, host_gl.c) */
void *host_gl_resolve(const char *name);
int host_gl_quiet_trace_markers(void);
/* ---------- VR (host_xr.c) */
+50 -9
View File
@@ -11,8 +11,52 @@ already have host types by then. Only strings need copying back.
#include <EGL/egl.h>
#include <GLES3/gl32.h>
#include <GLES2/gl2ext.h>
#include <dirent.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* Valve's Mesa (the Steam Frame's Lepton) writes a marker to the kernel's
trace_marker around every flush and context switch, a couple of hundred a
frame, whether or not anything traces. The descriptors it holds open on that
file are pointed at /dev/null instead (not closed: a reused descriptor would
take the markers elsewhere). After astromaddie/HaloCE-VR (CC0). Returns how
many were. */
int host_gl_quiet_trace_markers(void)
{
DIR *directory = opendir("/proc/self/fd");
struct dirent *entry;
int quieted = 0, null_fd;
if (!directory)
return 0;
null_fd = open("/dev/null", O_WRONLY | O_CLOEXEC);
while (null_fd >= 0 && (entry = readdir(directory)) != NULL)
{
char link[64], target[256];
ssize_t length;
int fd = atoi(entry->d_name);
if (entry->d_name[0] < '0' || entry->d_name[0] > '9' || fd == null_fd)
continue;
snprintf(link, sizeof(link), "/proc/self/fd/%d", fd);
length = readlink(link, target, sizeof(target) - 1);
if (length <= 0)
continue;
target[length] = 0;
if (strstr(target, "tracing/trace_marker") && dup3(null_fd, fd, O_CLOEXEC) == fd)
quieted++;
}
if (null_fd >= 0)
close(null_fd);
closedir(directory);
if (quieted)
host_logf(HOST_LOG_INFO, "trace markers: %d descriptors to /dev/null", quieted);
return quieted;
}
void *host_gl_resolve(const char *name)
{
@@ -88,14 +132,13 @@ half-drawn targets out of tile memory, for every lens flare. */
static GLuint counter_snapshots[COUNTER_SNAPSHOTS];
static uint32_t counter_snapshot_sizes[COUNTER_SNAPSHOTS];
/* (the copy bindings are left unbound after: the renderer binds them itself
for each copy. Asking GL what they were would wait for Mesa's GL thread, on
the Frame, to run everything queued) */
void host_gl_counters_snapshot(uint32_t buffer, uint32_t size, uint32_t slot)
{
GLint read_binding = 0, write_binding = 0;
if (slot >= COUNTER_SNAPSHOTS || !buffer || !size)
return;
glGetIntegerv(GL_COPY_READ_BUFFER_BINDING, &read_binding);
glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, &write_binding);
if (!counter_snapshots[slot])
glGenBuffers(1, &counter_snapshots[slot]);
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
@@ -108,19 +151,17 @@ void host_gl_counters_snapshot(uint32_t buffer, uint32_t size, uint32_t slot)
glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
glBindBuffer(GL_COPY_READ_BUFFER, buffer);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, size);
glBindBuffer(GL_COPY_READ_BUFFER, (GLuint)read_binding);
glBindBuffer(GL_COPY_WRITE_BUFFER, (GLuint)write_binding);
glBindBuffer(GL_COPY_READ_BUFFER, 0);
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
}
/* copies a snapshot out; its frame must be finished (host_gl_wait_frame) */
int host_gl_counters_read(uint32_t slot, void *out, uint32_t size)
{
GLint write_binding = 0;
const void *mapping;
if (slot >= COUNTER_SNAPSHOTS || !counter_snapshots[slot] || size > counter_snapshot_sizes[slot])
return 0;
glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, &write_binding);
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
mapping = glMapBufferRange(GL_COPY_WRITE_BUFFER, 0, size, GL_MAP_READ_BIT);
if (mapping)
@@ -128,7 +169,7 @@ int host_gl_counters_read(uint32_t slot, void *out, uint32_t size)
memcpy(out, mapping, size);
glUnmapBuffer(GL_COPY_WRITE_BUFFER);
}
glBindBuffer(GL_COPY_WRITE_BUFFER, (GLuint)write_binding);
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
return mapping != NULL;
}
+7 -7
View File
@@ -63,7 +63,7 @@ void host_fatal(const char *format, ...)
vsnprintf(message, sizeof(message), format, arguments);
va_end(arguments);
__android_log_write(ANDROID_LOG_FATAL, "halo", message);
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Halo", message, NULL);
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Ringfall: Mjolnir VR", message, NULL);
_exit(1);
}
@@ -91,7 +91,7 @@ int host_errno(void)
static char data_root[512];
static char save_root[512];
/* where a device that empties the app's folders keeps the game's data */
#define SHARED_DATA_ROOT "/home/steamos/Documents/HaloCE"
#define SHARED_DATA_ROOT "/home/steamos/Documents/RingfallMjolnirVR"
void host_android_path(int which, char *buffer, uint32_t size)
{
@@ -273,9 +273,9 @@ static void *game_main(void *unused)
host_fatal("Android storage is unavailable: %s", SDL_GetError());
/* the Steam Frame: Lepton empties the app's own folders whenever it
starts again; the game's data lives in the SteamOS user's Documents
instead, when it is there (port/android/STEAM_FRAME.md; the same choice
as LauncherActivity's) */
if (directory_has_maps(SHARED_DATA_ROOT) && access(SHARED_DATA_ROOT, W_OK) == 0)
instead, whenever they can be written (port/android/STEAM_FRAME.md; the
same choice as LauncherActivity's, which makes the folder) */
if (access(SHARED_DATA_ROOT, W_OK) == 0)
external = SHARED_DATA_ROOT;
snprintf(data_root, sizeof(data_root), "%s", external);
snprintf(save_root, sizeof(save_root), "%s/save", external);
@@ -292,7 +292,7 @@ static void *game_main(void *unused)
}
if (!directory_has_maps(data_root))
{
host_fatal("The Halo game data was not found.\n\nCopy the PAL game data (build 01.01.14.2342), "
host_fatal("The game data was not found.\n\nCopy the PAL game data (build 01.01.14.2342), "
"the folder that contains maps, into\n%s\nor import it from the launcher screen.", data_root);
}
@@ -400,7 +400,7 @@ int main(int argc, char *argv[])
{
(void)argc;
(void)argv;
host_logf(HOST_LOG_INFO, "Halo for Android starting");
host_logf(HOST_LOG_INFO, "Ringfall: Mjolnir VR starting");
host_install_signal_handlers();
if (host_native_thread_create(game_main, NULL, MAIN_STACK_SIZE) != 0)
host_fatal("cannot start the game thread");
+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)
+187 -40
View File
@@ -105,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;
@@ -116,10 +120,10 @@ static struct
XrAction haptic;
struct xr_hand_actions hands[HALO_XR_HAND_COUNT];
/* where the head and the left hand are this frame (the wrist layer) */
/* where the head and the hands are this frame (the wrist layer) */
XrPosef head;
XrPosef left_grip;
int left_grip_valid;
XrPosef grips[HALO_XR_HAND_COUNT];
int grips_valid[HALO_XR_HAND_COUNT];
int has_performance_settings;
/* (XR_META_performance_metrics: the compositor's counts, in the log,
@@ -585,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)
@@ -627,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);
@@ -732,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]));
}
@@ -781,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 };
@@ -847,11 +946,8 @@ static void hands_read(struct halo_xr_frame *frame)
{
out->flags = flags_from_location(location.locationFlags);
pose_from_xr(&out->grip, &location.pose);
if (hand == HALO_XR_HAND_LEFT)
{
xr.left_grip = location.pose;
xr.left_grip_valid = (out->flags & HALO_XR_POSE_VALID) != 0;
}
xr.grips[hand] = location.pose;
xr.grips_valid[hand] = (out->flags & HALO_XR_POSE_VALID) != 0;
}
location.locationFlags = 0;
if (XR_SUCCEEDED(xrLocateSpace(xr.hands[hand].aim_space, xr.local_space,
@@ -881,6 +977,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)
@@ -938,7 +1045,9 @@ On the back of the left wrist, like a watch: behind the palm, where the
grip pose is, and out of the back of the hand. In the left grip pose -Z
runs to the fingers, +Y up the handle and -X out of the back of the hand;
the layer faces -X with its top towards the elbow, as the wrist turned to
the eyes reads it. Shown only while it is. */
the eyes reads it. Shown only while it is. Left-handed (wrist_hand), on the
right wrist, as the mirror image: there +X runs out of the back of the
hand. */
#define WRIST_IMAGE_SIZE 512
#define CROSSHAIR_IMAGE_SIZE 256
@@ -978,26 +1087,32 @@ static void quaternion_rotate_vector(const XrQuaternionf *q, const XrVector3f *v
static int wrist_pose(const struct halo_xr_submit *submit, XrPosef *pose)
{
/* the layer's axes in the grip's: x = -Y, y = +Z, z = -X, so the
layer's right and down, as it is read, are the grip's -Y and -Z */
static const XrQuaternionf layer_in_grip = { 0.5f, -0.5f, -0.5f, 0.5f };
const XrVector3f offset = { -WRIST_ABOVE_HAND, -WRIST_DOWN_HANDLE - submit->wrist_right,
WRIST_BEHIND_PALM - submit->wrist_down };
/* the layer's axes in the left grip's: x = -Y, y = +Z, z = -X, so the
layer's right and down, as it is read, are the grip's -Y and -Z; in the
right grip's x = +Y, y = +Z, z = +X (the mirror image, read the right
way round: its x turned back) */
static const XrQuaternionf layer_in_left_grip = { 0.5f, -0.5f, -0.5f, 0.5f };
static const XrQuaternionf layer_in_right_grip = { 0.5f, 0.5f, 0.5f, 0.5f };
int right = submit->wrist_hand == HALO_XR_HAND_RIGHT;
const XrPosef *grip = &xr.grips[right ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT];
const XrVector3f offset = { right ? WRIST_ABOVE_HAND : -WRIST_ABOVE_HAND,
-WRIST_DOWN_HANDLE - submit->wrist_right, WRIST_BEHIND_PALM - submit->wrist_down };
const XrVector3f layer_normal = { 0.0f, 0.0f, 1.0f };
/* clockwise as read: about the layer's normal, negatively */
float half_turn = -submit->wrist_turn * WRIST_PI / 360.0f;
/* clockwise as read: about the layer's normal, negatively (the mirror
image's the other way) */
float half_turn = (right ? 1.0f : -1.0f) * submit->wrist_turn * WRIST_PI / 360.0f;
XrQuaternionf turn = { 0.0f, 0.0f, sinf(half_turn), cosf(half_turn) };
XrQuaternionf unturned;
XrVector3f moved, normal, to_eyes;
float distance;
if (!xr.left_grip_valid)
if (!xr.grips_valid[right ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT])
return 0;
quaternion_rotate_vector(&xr.left_grip.orientation, &offset, &moved);
pose->position.x = xr.left_grip.position.x + moved.x;
pose->position.y = xr.left_grip.position.y + moved.y;
pose->position.z = xr.left_grip.position.z + moved.z;
quaternion_multiply(&xr.left_grip.orientation, &layer_in_grip, &unturned);
quaternion_rotate_vector(&grip->orientation, &offset, &moved);
pose->position.x = grip->position.x + moved.x;
pose->position.y = grip->position.y + moved.y;
pose->position.z = grip->position.z + moved.z;
quaternion_multiply(&grip->orientation, right ? &layer_in_right_grip : &layer_in_left_grip, &unturned);
quaternion_multiply(&unturned, &turn, &pose->orientation);
quaternion_rotate_vector(&pose->orientation, &layer_normal, &normal);
@@ -1117,7 +1232,7 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
android.applicationVM = machine;
android.applicationActivity = activity;
instance.next = &android;
strcpy(instance.applicationInfo.applicationName, "Halo VR");
strcpy(instance.applicationInfo.applicationName, "Ringfall Mjolnir VR");
instance.applicationInfo.applicationVersion = 1;
strcpy(instance.applicationInfo.engineName, "halo-ce-universal");
instance.applicationInfo.engineVersion = 1;
@@ -1260,6 +1375,10 @@ 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;
@@ -1441,7 +1560,7 @@ int host_xr_begin_frame(struct halo_xr_frame *frame)
pose_from_xr(&frame->head, &head.pose);
xr.head = head.pose;
}
xr.left_grip_valid = 0;
xr.grips_valid[HALO_XR_HAND_LEFT] = xr.grips_valid[HALO_XR_HAND_RIGHT] = 0;
if (xr.focused)
hands_read(frame);
return 1;
@@ -1933,6 +2052,34 @@ void host_xr_set_vignette(float strength)
xr.vignette = strength < 0.0f ? 0.0f : strength > 1.0f ? 1.0f : strength;
}
/* the VR options' refresh rates: those the runtime offers (none where it
lets none be chosen, as SteamVR, whose rate is SteamOS's), and the one it
runs at, by XR_FB_display_refresh_rate or else by its frames' period */
int host_xr_refresh_rates(float *rates, int maximum, float *current)
{
float offered[16];
uint32_t count = 0, index;
*current = 0.0f;
if (!xr.initialized)
return 0;
if (xr.has_refresh_rate)
xr.get_refresh_rate(xr.session, current);
if (*current <= 0.0f && xr.frame_state.predictedDisplayPeriod > 0)
*current = 1e9f / (float)xr.frame_state.predictedDisplayPeriod;
if (!xr.has_refresh_rate || !XR_SUCCEEDED(xr.enumerate_refresh_rates(xr.session, 16, &count, offered)))
return 0;
for (index = 0; index < count && (int)index < maximum; index++)
rates[index] = offered[index];
return (int)index;
}
void host_xr_set_refresh_rate(float wanted)
{
if (xr.initialized)
refresh_rate_choose(wanted);
}
void host_xr_haptic(int hand, float amplitude, float seconds)
{
XrHapticActionInfo information = { XR_TYPE_HAPTIC_ACTION_INFO };
@@ -2020,7 +2167,7 @@ void host_xr_test(void)
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
window = SDL_CreateWindow("Halo", 640, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN);
window = SDL_CreateWindow("Ringfall: Mjolnir VR", 640, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN);
context = window ? SDL_GL_CreateContext(window) : NULL;
if (!context || !SDL_GL_MakeCurrent(window, context))
host_fatal("VR test: no OpenGL ES context: %s", SDL_GetError());
+2
View File
@@ -61,6 +61,8 @@ host_xr_end_frame
host_xr_haptic
host_xr_set_vignette
host_xr_copy_layer_array
host_xr_refresh_rates
host_xr_set_refresh_rate
host_android_path
host_errno
+14 -5
View File
@@ -24,7 +24,7 @@ enum
HALO_XR_LAYER_EYE_LEFT = 0,
HALO_XR_LAYER_EYE_RIGHT = 1,
HALO_XR_LAYER_OVERLAY = 2, /* HUD, menus, cinematics: a flat quad */
HALO_XR_LAYER_WRIST = 3, /* the motion tracker, on the left wrist */
HALO_XR_LAYER_WRIST = 3, /* the motion tracker, on the free hand's wrist */
HALO_XR_LAYER_SCOPE = 4, /* the zoomed view, a disc over the weapon */
HALO_XR_LAYER_CROSSHAIR = 5, /* the weapon's crosshair, where it aims */
HALO_XR_LAYER_NAMES = 6, /* players' names, each a quad over a head */
@@ -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 */
@@ -149,15 +155,18 @@ struct halo_xr_submit
float overlay_width; /* metres; the height follows from 4:3 */
/* the screen's placement in LOCAL space (HALO_XR_OVERLAY_SCREEN) */
struct halo_xr_pose screen;
/* the wrist layer was filled: shown on the back of the left wrist,
this wide (metres), while the wrist is turned to the eyes */
/* the wrist layer was filled: shown on the back of the left wrist (of
the right one, left-handed: wrist_hand, a HALO_XR_HAND_*), this wide
(metres), while the wrist is turned to the eyes */
int wrist;
float wrist_size;
/* its placement as the wrist turned to the eyes reads it: turned this
many degrees clockwise, and moved this far right and down (metres) */
/* its placement as the left wrist turned to the eyes reads it: turned
this many degrees clockwise, and moved this far right and down (metres;
on the right wrist, the mirror image) */
float wrist_turn;
float wrist_right;
float wrist_down;
int wrist_hand;
/* the scope layer was filled: a disc this wide (metres), here in LOCAL
space, facing back along the weapon */
int scope;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 71 KiB

+288
View File
@@ -0,0 +1,288 @@
#!/bin/bash
# Ringfall: Mjolnir VR's installer for the Steam Frame, run in its Desktop
# Mode from the bundle the release's "Install Ringfall: Mjolnir VR" desktop
# file downloads (steam_frame_installer.desktop; the bundle: this script,
# RingfallMjolnirVR.apk, ringfall-mjolnir-vr.sh = steam_frame_launch.sh,
# steam_frame_xiso.py and the library pictures in grid/). Everything is
# answered with dialogs, no keyboard:
# - the game in ~/Documents/RingfallMjolnirVR (port/android/STEAM_FRAME.md),
# its APK replaced (Lepton installs a newer one at the next start), so
# running the installer again updates the game; the folder of the builds
# before the name, ~/Documents/HaloCE, becomes it;
# - the game data: the maps folder copied out of a disc image of the
# original Xbox game (.iso or .xiso) found in Downloads, Documents, the
# Desktop or on a memory card, or chosen, unless it is there already;
# - the game in the Steam library, with its pictures, unless it is there
# already: added by Steam itself (steam://addnonsteamgame, as SteamOS's
# "Add to Steam" does), from a desktop file beside the game. Steam is not
# closed: it keeps its shortcuts in memory and writes them back, and on the
# Steam Frame Desktop Mode runs inside Gaming Mode's session, which ends
# with Steam. The shortcut of the builds before the name ("Halo VR") is
# left for the player to remove (Steam would write it back).
# RINGFALL_UNATTENDED=1 answers every question with its default and prints
# instead of showing dialogs (RINGFALL_IMAGE: the disc image to use), for
# testing over adb; it never closes Steam.
BUNDLE="$(cd "$(dirname "$0")" && pwd)"
GAME_DIR="$HOME/Documents/RingfallMjolnirVR"
STEAM_DIR="$HOME/.local/share/Steam"
LEPTON="$STEAM_DIR/steamapps/common/Lepton/lepton"
TITLE="Ringfall: Mjolnir VR"
NAME="Ringfall: Mjolnir VR"
APK_NAME="RingfallMjolnirVR.apk"
LAUNCHER="ringfall-mjolnir-vr.sh"
EXE="\"$GAME_DIR/$LAUNCHER\""
# (the desktop file Steam adds the game from)
NAME_FILE="Ringfall Mjolnir VR"
# the builds before the name: their folder and Steam shortcut
OLD_DIR="$HOME/Documents/HaloCE"
OLD_NAME="Halo VR"
# what the installer did, for looking into problems: written to /tmp while
# it runs, then kept beside the game as install.log
LOG="/tmp/ringfall-mjolnir-vr-install.log"
: > "$LOG"
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
keep_log() { [ -d "$GAME_DIR" ] && cp -f "$LOG" "$GAME_DIR/install.log" 2>/dev/null; }
trap keep_log EXIT
log "installer from $BUNDLE"
# whether Steam runs: its pid file (~/.steam/steam.pid), or its client's
# process (ubuntu12_32/steam: the process is not always named "steam")
steam_running()
{
local pid
pid="$(cat "$HOME/.steam/steam.pid" 2>/dev/null)"
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
return 0
fi
pgrep -f 'ubuntu12_32/steam( |$)' >/dev/null || pgrep -x steam >/dev/null
}
unattended() { [ "${RINGFALL_UNATTENDED:-0}" = 1 ] || ! command -v zenity >/dev/null; }
info()
{
if unattended; then printf "info: %b\n" "$1"; else zenity --info --title="$TITLE" --width=460 --text="$1"; fi
}
fail()
{
log "error: $1"
if unattended; then printf "error: %b\n" "$1" >&2; else zenity --error --title="$TITLE" --width=460 --text="$1"; fi
exit 1
}
# yes/no; unattended: the default given ($2, "yes" or "no")
ask()
{
if unattended; then [ "$2" = yes ]; return; fi
zenity --question --title="$TITLE" --width=460 --text="$1"
}
# the disc image the player chooses
choose_image()
{
if unattended; then return 1; fi
zenity --file-selection --title="Choose a disc image of Halo: Combat Evolved for the Xbox (.iso or .xiso)" \
--filename="$HOME/Downloads/" --file-filter="Disc images | *.iso *.ISO *.xiso *.XISO" \
--file-filter="All files | *"
}
# the disc images near at hand, largest first (the game's image is a few GB;
# anything under 500 MB is not one)
find_images()
{
find "$HOME/Downloads" "$HOME/Documents" "$HOME/Desktop" /run/media -maxdepth 3 -type f \
\( -iname '*.iso' -o -iname '*.xiso' \) -size +500M -printf '%s\t%p\n' 2>/dev/null |
sort -rn | cut -f2-
}
extract_maps()
{
local image="$1" log
log="$(mktemp)"
if unattended; then
python3 "$BUNDLE/steam_frame_xiso.py" "$image" "$GAME_DIR" 2>"$log" | grep '^#' | tail -n 1
status=${PIPESTATUS[0]}
else
python3 "$BUNDLE/steam_frame_xiso.py" "$image" "$GAME_DIR" 2>"$log" |
zenity --progress --title="$TITLE" --width=460 --auto-close --no-cancel \
--text="Copying the game data out of the disc image..."
status=${PIPESTATUS[0]}
fi
if [ "$status" != 0 ]; then
reason="$(sed -n 's/^error: //p' "$log" | head -n 1)"
rm -f "$log"
fail "The game data could not be copied out of\n$image\n\n${reason:-Unknown error.}"
fi
rm -f "$log"
}
# the Steam users' shortcut files (userdata/0 is no user)
shortcut_files()
{
local config
for config in "$STEAM_DIR"/userdata/*/config; do
case "$config" in */userdata/0/*) continue ;; esac
[ -f "$config/shortcuts.vdf" ] && echo "$config/shortcuts.vdf"
done
}
# whether a Steam user's library holds the launcher given
have_shortcut_to()
{
local file
while read -r file; do
grep -qaF "$1" "$file" && return 0
done < <(shortcut_files)
return 1
}
have_shortcut() { have_shortcut_to "$GAME_DIR/$LAUNCHER"; }
# the id Steam gave the shortcut whose Exe is $2, in shortcuts.vdf $1 (Steam
# picks it; its library pictures in config/grid are named after it)
shortcut_id()
{
python3 - "$1" "$2" <<'PYTHON'
import struct, sys
path, exe = sys.argv[1], sys.argv[2].encode()
# Steam's binary VDF: 0x00 a map, 0x01 a string, 0x02 an int32, 0x08 a map's end
def read_map(data, at):
items = []
while True:
kind = data[at]; at += 1
if kind == 0x08:
return items, at
end = data.index(b"\0", at); key = data[at:end].lower(); at = end + 1
if kind == 0x00:
value, at = read_map(data, at)
elif kind == 0x01:
end = data.index(b"\0", at); value = data[at:end]; at = end + 1
elif kind == 0x02:
value = struct.unpack_from("<I", data, at)[0]; at += 4
else:
raise ValueError("unknown VDF type %d" % kind)
items.append((key, value))
with open(path, "rb") as file:
top, _ = read_map(file.read(), 0)
for key, shortcuts in top:
if key == b"shortcuts":
for _, entry in shortcuts:
fields = dict(entry)
if fields.get(b"exe") == exe and b"appid" in fields:
print(fields[b"appid"])
sys.exit(0)
sys.exit(1)
PYTHON
}
# the game added to the library of the Steam user logged in, by Steam (it
# must run), from a desktop file beside the game; then its pictures
add_shortcut()
{
local desktop="$GAME_DIR/$NAME_FILE.desktop" url file id
printf '[Desktop Entry]\nType=Application\nName=%s\nExec=%s\nPath=%s\nIcon=steam\nTerminal=false\n' \
"$NAME" "$GAME_DIR/$LAUNCHER" "$GAME_DIR" > "$desktop" && chmod 755 "$desktop" || return 1
url="steam://addnonsteamgame/$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$desktop")"
log "asking Steam: $url"
# (Steam's own "Add to Steam" touches this first: steamos-add-to-steam)
touch /tmp/addnonsteamgamefile
steam "$url" >/dev/null 2>&1 &
for _ in $(seq 30); do have_shortcut && break; sleep 1; done
have_shortcut || return 1
while read -r file; do
id="$(shortcut_id "$file" "$EXE")" || continue
log "shortcut $id in $file"
mkdir -p "$(dirname "$file")/grid"
cp "$BUNDLE/grid/library.png" "$(dirname "$file")/grid/${id}.png"
cp "$BUNDLE/grid/libraryp.png" "$(dirname "$file")/grid/${id}p.png"
cp "$BUNDLE/grid/library_hero.png" "$(dirname "$file")/grid/${id}_hero.png"
cp "$BUNDLE/grid/library_logo.png" "$(dirname "$file")/grid/${id}_logo.png"
done < <(shortcut_files)
return 0
}
# --- the game
[ -x "$LEPTON" ] || fail "Lepton, which runs Android games on the Steam Frame, was not found.\n\nStart any Android game from the Steam store once, then run this installer again."
for file in "$APK_NAME" "$LAUNCHER" steam_frame_xiso.py grid/library.png; do
[ -f "$BUNDLE/$file" ] || fail "The installer is incomplete ($file is missing). Download it again."
done
# the builds before the name: their folder becomes the game's (settings,
# saves and game data kept), without their APK and launcher
if [ -d "$OLD_DIR" ] && [ ! -e "$GAME_DIR" ]; then
log "renaming $OLD_DIR to $GAME_DIR"
mv "$OLD_DIR" "$GAME_DIR" || fail "Could not rename $OLD_DIR to $GAME_DIR."
rm -f "$GAME_DIR/HaloVR.apk" "$GAME_DIR/halo-vr.sh"
fi
if [ -f "$GAME_DIR/$APK_NAME" ] || [ -f "$GAME_DIR/maps/ui.map" ]; then
ask "$NAME is installed already. Update it to this version?\n\n(Your settings, saves and game data stay.)" yes || exit 0
else
ask "This installs $NAME on your Steam Frame.\n\nYou need your own disc image of Halo: Combat Evolved for the original Xbox (an .iso or .xiso file, any version), for example in Downloads.\n\nContinue?" yes || exit 0
fi
mkdir -p "$GAME_DIR/maps" || fail "Could not create $GAME_DIR."
# (the game runs in Lepton's container as a user of its own)
chmod 777 "$GAME_DIR" "$GAME_DIR/maps"
cp "$BUNDLE/$LAUNCHER" "$GAME_DIR/$LAUNCHER.tmp" && chmod 755 "$GAME_DIR/$LAUNCHER.tmp" &&
mv -f "$GAME_DIR/$LAUNCHER.tmp" "$GAME_DIR/$LAUNCHER" || fail "Could not write $GAME_DIR/$LAUNCHER."
# (newer than the installed one: Lepton installs it at the next start)
cp "$BUNDLE/$APK_NAME" "$GAME_DIR/$APK_NAME.tmp" && chmod 666 "$GAME_DIR/$APK_NAME.tmp" &&
mv -f "$GAME_DIR/$APK_NAME.tmp" "$GAME_DIR/$APK_NAME" && touch "$GAME_DIR/$APK_NAME" ||
fail "Could not write $GAME_DIR/$APK_NAME (is the storage full?)."
# --- the game data
if [ ! -f "$GAME_DIR/maps/ui.map" ]; then
image="${RINGFALL_IMAGE:-}"
if [ -z "$image" ]; then
mapfile -t images < <(find_images)
for candidate in "${images[@]}"; do
if ask "Use this disc image?\n\n$candidate" yes; then
image="$candidate"
break
fi
done
fi
if [ -z "$image" ]; then
if [ ${#images[@]} = 0 ]; then
info "No disc image was found in Downloads, Documents, on the Desktop or a memory card.\n\nChoose it in the next window."
fi
image="$(choose_image)" || fail "No disc image was chosen. Run the installer again once it is on the Frame (for example in Downloads)."
fi
log "extracting $image"
extract_maps "$image"
chmod -R a+rwX "$GAME_DIR/maps"
image_note="\n\nThe disc image itself is no longer needed:\n$image"
fi
# --- Steam
if have_shortcut; then
log "the Steam shortcut is there"
elif unattended; then
echo "info: the Steam shortcut is missing (not added unattended)"
elif ! steam_running; then
info "Steam is not running, so $NAME could not be added to your library.\n\nStart Steam and run the installer again."
else
add_shortcut || fail "Steam did not add $NAME to your library. Run the installer again; if it happens again, add\n$GAME_DIR/$NAME_FILE.desktop\nwith Steam's \"Add a Non-Steam Game\"."
fi
if unattended; then
echo "info: installed in $GAME_DIR"
exit 0
fi
if have_shortcut_to "$OLD_DIR/halo-vr.sh"; then
info "The library still shows the old \"$OLD_NAME\" from before the game's new name. It no longer starts: remove it in Steam (its gear or Manage > Remove non-Steam game)."
fi
if ask "$NAME is installed.${image_note:-}\n\nSwitch to Gaming Mode now? Start $NAME from your library there (its pictures may show once Steam starts again)." yes; then
steamos-session-select gamescope >/dev/null 2>&1 ||
info "Switch to Gaming Mode yourself (the \"Return to Gaming Mode\" icon or the power menu)."
fi
@@ -0,0 +1,14 @@
[Desktop Entry]
# Ringfall: Mjolnir VR's installer for the Steam Frame
# (port/android/STEAM_FRAME.md): a release asset,
# "Install-Ringfall-Mjolnir-VR.desktop", opened in Desktop Mode. It downloads
# the latest release's bundle (tools/steam_frame_bundle.py, which fills in
# the repository) and runs its installer, steam_frame_install.sh.
# (no "$", "\" or "%" in Exec: the desktop entry format would need them
# escaped)
Type=Application
Name=Install Ringfall: Mjolnir VR
Comment=Installs Ringfall: Mjolnir VR for the Steam Frame, or updates it
Icon=steam
Terminal=false
Exec=bash -c "if curl -fL --retry 3 -o /tmp/ringfall-mjolnir-vr-steam-frame.zip https://github.com/@REPOSITORY@/releases/latest/download/ringfall-mjolnir-vr-steam-frame.zip && rm -rf /tmp/ringfall-mjolnir-vr-install && python3 -m zipfile -e /tmp/ringfall-mjolnir-vr-steam-frame.zip /tmp/ringfall-mjolnir-vr-install; then exec bash /tmp/ringfall-mjolnir-vr-install/install.sh; else zenity --error --width=460 --title='Ringfall: Mjolnir VR' --text='Ringfall: Mjolnir VR could not be downloaded. Is the Steam Frame online?'; fi"
+7 -6
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# Halo VR on the Steam Frame: Lepton runs the APK in a container of its own,
# Ringfall: Mjolnir VR on the Steam Frame: Lepton runs the APK in a container of its own,
# and installs it anew whenever the APK is newer than the installed one
# (port/android/STEAM_FRAME.md). The game's data: ~/Documents/HaloCE.
# (port/android/STEAM_FRAME.md). The game's data: ~/Documents/RingfallMjolnirVR.
# (a container of its own, not the shortcut's id: with that, SteamVR's
# client in the container waited on xrCreateInstance)
export SteamAppId="${HALO_LEPTON_APP_ID:-4290000001}"
@@ -14,10 +14,11 @@ export STEAM_COMPAT_SHADER_PATH="$HOME/.local/share/Steam/steamapps/shadercache/
mkdir -p "$STEAM_COMPAT_DATA_PATH/internal" "$STEAM_COMPAT_SHADER_PATH"
unset STEAM_FOSSILIZE_DUMP_PATH LD_PRELOAD
LEPTON="$HOME/.local/share/Steam/steamapps/common/Lepton/lepton"
APK="$HOME/Documents/HaloCE/HaloVR.apk"
LOG="$HOME/Documents/HaloCE/launcher.log"
DATA="$HOME/Documents/RingfallMjolnirVR"
APK="$DATA/RingfallMjolnirVR.apk"
LOG="$DATA/launcher.log"
# (written by the game when the player quits it: no start again)
QUIT="$HOME/Documents/HaloCE/quit_requested.txt"
QUIT="$DATA/quit_requested.txt"
rm -f "$QUIT"
CONTEXT="steamlaunch-$SteamAppId"
@@ -49,7 +50,7 @@ setsid bash -c '
# the game's log while it runs (Lepton's logcat of its container), for
# looking into problems: logcat.txt, anew each start
LOGCAT="$HOME/Documents/HaloCE/logcat.txt"
LOGCAT="$DATA/logcat.txt"
: > "$LOGCAT"
setsid bash -c '
while kill -0 '"$script"' 2>/dev/null; do
+220
View File
@@ -0,0 +1,220 @@
#!/usr/bin/env python3
"""
Copies the maps folder out of an Xbox disc image (an "xiso", or a whole
disc's ".iso") on the Steam Frame's SteamOS side, for its installer
(steam_frame_install.sh). It reads the image as the app's own import does
(app/src/main/java/com/halo/decomp/XisoExtractor.java, which this follows;
the desktop games' is port/linux/src/xiso.c).
steam_frame_xiso.py <image> <destination>
writes <destination>/maps (through maps.partial, so an interrupted copy
never leaves maps/ui.map behind) and prints its progress for zenity
--progress: a percentage on a line of its own, and "# <text>" lines. On
failure it prints "error: <reason the player can act on>" to stderr and
exits 1.
The image's file system is XDVDFS, read as extract-xiso does
(https://github.com/XboxDev/extract-xiso, extract-xiso.c, whose format
handling this follows; its license is below and in
port/third_party/extract-xiso/LICENSE.TXT): 2048-byte sectors; a volume
descriptor at 0x10000 that starts and ends with "MICROSOFT*XBOX*MEDIA" and
gives the root directory's sector and size; and directories whose entries
form a binary tree (each entry: left and right subtree offsets in 4-byte
units, the start sector, the size, attributes, the name's length and the
name). Images made from a whole disc put the game partition further in, at
one of the offsets below.
Some parts of this code are copyright in@fishtank.com. This product
includes software developed by in <in@fishtank.com>.
Copyright (c) 2003 in <in@fishtank.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software
must display the following acknowledgement:
This product includes software developed by in <in@fishtank.com>.
4. Neither the name of "in" nor the email address "in@fishtank.com"
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED `AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
AUTHOR OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
import os
import struct
import sys
SECTOR_SIZE = 2048
VOLUME_DESCRIPTOR_OFFSET = 0x10000
ENTRY_HEADER_SIZE = 14
ATTRIBUTE_DIRECTORY = 0x10
# directory tables are a few sectors; anything much larger is not one
MAXIMUM_DIRECTORY_SIZE = 4 << 20
MAXIMUM_FILES = 256
COPY_BUFFER_SIZE = 1 << 20
VOLUME_MAGIC = b"MICROSOFT*XBOX*MEDIA"
# where the game partition starts: a plain image, and whole-disc images
# (extract-xiso's GLOBAL, XGD3 and XGD1 offsets)
PARTITION_OFFSETS = (0, 0x0FD90000, 0x02080000, 0x18300000)
class ExtractError(Exception):
"""a reason the player can act on"""
def read_at(image, offset, size):
image.seek(offset)
data = image.read(size)
if len(data) != size:
raise ExtractError("Could not read the disc image (is it complete?).")
return data
def find_volume(image):
"""the game partition's offset, and the root directory's sector and size"""
for offset in PARTITION_OFFSETS:
try:
descriptor = read_at(image, offset + VOLUME_DESCRIPTOR_OFFSET, SECTOR_SIZE)
except ExtractError:
continue
if descriptor[:20] != VOLUME_MAGIC or descriptor[0x7EC:0x7EC + 20] != VOLUME_MAGIC:
continue
sector, size = struct.unpack_from("<II", descriptor, 20)
return offset, sector, size
raise ExtractError("This is not an Xbox disc image.")
def read_directory(image, partition, sector, size, what):
if size <= 0 or size > MAXIMUM_DIRECTORY_SIZE:
raise ExtractError(what)
return read_at(image, partition + sector * SECTOR_SIZE, size)
def walk(table, offset, depth, directories, out, visited):
"""collects the entries (name, sector, size) of the subtree at offset
(4-byte units), files or directories as asked"""
at = offset * 4
# a malformed table must not loop or run off its end
visited[0] += 1
if depth > 64 or visited[0] > 4096 or at + ENTRY_HEADER_SIZE > len(table):
return
left, right, sector, size, attributes, name_length = struct.unpack_from("<HHIIBB", table, at)
# 0xFFFF: padding, an empty directory
if left == 0xFFFF:
return
if left:
walk(table, left, depth + 1, directories, out, visited)
is_directory = bool(attributes & ATTRIBUTE_DIRECTORY)
if (at + ENTRY_HEADER_SIZE + name_length <= len(table) and name_length > 0
and is_directory == directories and len(out) < MAXIMUM_FILES):
name = table[at + ENTRY_HEADER_SIZE:at + ENTRY_HEADER_SIZE + name_length].decode("latin-1")
# (as extract-xiso refuses them: no name may leave the folder)
if name not in (".", "..") and "/" not in name and "\\" not in name:
out.append((name, sector, size))
if right:
walk(table, right, depth + 1, directories, out, visited)
def extract(image_path, destination):
with open(image_path, "rb") as image:
partition, root_sector, root_size = find_volume(image)
# the root's maps folder
table = read_directory(image, partition, root_sector, root_size,
"The disc image's file system is damaged.")
directories = []
walk(table, 0, 0, True, directories, [0])
maps = [entry for entry in directories if entry[0].lower() == "maps"]
if not maps:
raise ExtractError("The disc image has no maps folder: it is not a Halo disc.")
# its files
table = read_directory(image, partition, maps[-1][1], maps[-1][2],
"The disc image's maps folder is damaged.")
files = []
walk(table, 0, 0, False, files, [0])
if not any(name.lower() == "ui.map" for name, _, _ in files):
raise ExtractError("The disc image's maps folder has no ui.map: it is not a Halo disc.")
total = sum(size for _, _, size in files) or 1
partial = os.path.join(destination, "maps.partial")
finished = os.path.join(destination, "maps")
try:
os.makedirs(partial, exist_ok=True)
except OSError:
raise ExtractError("Could not create %s." % partial)
done = 0
shown = -1
for name, sector, size in files:
path = os.path.join(partial, name)
offset = partition + sector * SECTOR_SIZE
remaining = size
print("# Copying maps/%s" % name, flush=True)
try:
with open(path, "wb") as out:
while remaining > 0:
count = min(remaining, COPY_BUFFER_SIZE)
out.write(read_at(image, offset, count))
offset += count
remaining -= count
done += count
# (zenity closes at 100: that is left to the caller)
percent = min(99, done * 100 // total)
if percent != shown:
print(percent, flush=True)
shown = percent
except ExtractError:
raise ExtractError("Could not read %s from the disc image (is it complete?)." % name)
except OSError:
raise ExtractError("Could not write %s (is the storage full?)." % path)
# (the maps folder may be there, empty)
try:
os.makedirs(finished, exist_ok=True)
for name, _, _ in files:
os.replace(os.path.join(partial, name), os.path.join(finished, name))
os.rmdir(partial)
except OSError as error:
raise ExtractError("Could not move the maps into %s (%s)." % (finished, error.strerror))
def main():
if len(sys.argv) != 3:
print("usage: steam_frame_xiso.py <image> <destination>", file=sys.stderr)
return 2
try:
extract(sys.argv[1], sys.argv[2])
except ExtractError as error:
print("error: %s" % error, file=sys.stderr)
return 1
except OSError as error:
print("error: Could not open the disc image (%s)." % error.strerror, file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
+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. |
+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) */
{
+13 -10
View File
@@ -3,19 +3,22 @@ NETWORK_VR.C
VR players' hands for the other machines: an extension of the distributed
netcode (port/linux/NETCODE.md, network_distributed.c). A VR player's
weapon aims where the right controller points and fires from the hand, and
the left hand holds a grenade and throws it its own way (port/linux/src/
vr.c); the input the netcode relays has only where the head looks, so the
other machines would show that player aiming and throwing where they look.
weapon aims where the weapon hand's controller points and fires from the
hand, and the free hand holds a grenade and throws it its own way
(port/linux/src/vr.c; the right hand and the left, or left-handed the other
way round); the input the netcode relays has only where the head looks, so
the other machines would show that player aiming and throwing where they
look.
Every tick, a machine with a VR player sends the host that player's hands
(_distributed_message_vr_poses, unreliable): where the weapon aims, where
its muzzle is from the unit's camera, whether the left hand holds a
grenade, and the last throw (from where, which way, how hard) for a second
after it. The host keeps them, its own VR players' too, and sends each
client those of the players of other machines. A machine drives a VR
player of another (players.c, weapons.c, units.c) as long as their hands
keep coming, else as before.
its muzzle is from the unit's camera (whichever hand holds it: the others
see shots leave the left hand of a left-handed player), whether the free
hand holds a grenade, and the last throw (from where, which way, how hard)
for a second after it. The host keeps them, its own VR players' too, and
sends each client those of the players of other machines. A machine drives
a VR player of another (players.c, weapons.c, units.c) as long as their
hands keep coming, else as before.
What the hands decide is only how the game looks: damage is the shooter's
report, which the host checks as any other (network_damage.c).
+56 -19
View File
@@ -16,7 +16,10 @@ also need the tick after the pair it spans: two ticks behind.)
Rotations are blended as quaternions (normalised lerp, taking the shorter
way round), positions and scales linearly. Anything that moves further than
a tick of motion plausibly allows (teleports, respawns, camera cuts) snaps
instead of sweeping across the world.
instead of sweeping across the world, and so does a pose whose nodes moved
too far for one tick: two snapshots of different poses (a model swapped,
another weapon's skeleton, a pose left from seconds before) blended node by
node stretch vertices across the screen.
Particles, contrails and other effects already move every frame
(game_frame), so they need nothing here.
@@ -56,6 +59,17 @@ unsigned long config_changes(void);
/* world units (10 feet each) a node may move in one tick before it snaps:
well beyond any vehicle, short of any teleport */
#define OBJECT_SNAP_DISTANCE 10.0f
/* ... a node may move in the object's root node's frame in one tick before
the pose is taken for a new one, not blended to: further than any limb or
part of a model moves in 33 ms (37 m/s), short of the game changing a pose
at once (an actor waking from dormancy, a model swapped), which blended
sweeps the vertices through poses it never had. (In the root's frame, so
that the whole object turning moves nothing: measured in the world, a fast
turn swung the head and weapon far enough to snap, and characters moved
like robots.) */
#define NODE_SNAP_DISTANCE 0.4f
/* ... a first-person node may move relative to the camera */
#define FIRST_PERSON_SNAP_DISTANCE 0.25f
/* a correction's difference left drawn after each tick (of 1) */
#define CORRECTION_DECAY 0.6f
/* ... and small enough to be none */
@@ -311,16 +325,6 @@ static void matrix_blend_rotations(
result->up.k = lerp(a->up.k, b->up.k, t);
}
/* a matrix a fraction t of the way from a to b */
static void matrix_blend(real_matrix4x3 const *a, real_matrix4x3 const *b, real t, real_matrix4x3 *result)
{
struct interpolation_rotation rotation_a, rotation_b;
rotation_from_matrix(a, &rotation_a);
rotation_from_matrix(b, &rotation_b);
matrix_blend_rotations(a, b, &rotation_a, &rotation_b, t, result);
}
/* the vector's parts' sum, large enough to be a correction (so written that
one not a number is none) */
static boolean correction_significant(real_vector3d const *correction)
@@ -530,14 +534,22 @@ real_matrix4x3 *render_interpolation_object_node_matrices(long object_index)
real_matrix4x3 const *latest = record->nodes + record->latest * record->node_capacity;
real_matrix4x3 *blended = record->nodes + 2 * record->node_capacity;
short node_index;
/* (so written that a position not a number snaps) */
if (!(distance_squared(&previous[0].position, &latest[0].position) <=
OBJECT_SNAP_DISTANCE * OBJECT_SNAP_DISTANCE))
boolean snap = !(distance_squared(&previous[0].position, &latest[0].position) <=
OBJECT_SNAP_DISTANCE * OBJECT_SNAP_DISTANCE);
/* a node moved further in the root's frame than a tick allows: the
two snapshots are different poses, not one moving (so written that
a position not a number snaps) */
for (node_index = 1; !snap && node_index < record->node_count; node_index++)
{
memcpy(blended, latest, record->node_count * sizeof(real_matrix4x3));
real_point3d previous_local, latest_local;
matrix4x3_inverse_transform_point(&previous[0], &previous[node_index].position, &previous_local);
matrix4x3_inverse_transform_point(&latest[0], &latest[node_index].position, &latest_local);
snap = !(distance_squared(&previous_local, &latest_local) <= NODE_SNAP_DISTANCE * NODE_SNAP_DISTANCE);
}
else
if (!snap)
{
struct interpolation_rotation *previous_rotations =
record->rotations + (record->latest ^ 1) * record->node_capacity;
@@ -566,6 +578,8 @@ real_matrix4x3 *render_interpolation_object_node_matrices(long object_index)
&latest_rotations[node_index], interpolation_fraction, &blended[node_index]);
}
}
if (snap)
memcpy(blended, latest, record->node_count * sizeof(real_matrix4x3));
if (correction_significant(&record->correction) || correction_significant(&record->correction_pending))
{
real_vector3d drawn;
@@ -804,6 +818,8 @@ void render_interpolation_first_person(
struct interpolated_first_person *first_person;
real_matrix4x3 camera_matrix;
real_matrix4x3 inverse_camera;
struct interpolation_rotation previous_rotations[MAXIMUM_INTERPOLATED_NODES];
struct interpolation_rotation latest_rotations[MAXIMUM_INTERPOLATED_NODES];
short node_index;
if (!interpolation_rendering ||
@@ -817,8 +833,11 @@ void render_interpolation_first_person(
matrix4x3_inverse(&camera_matrix, &inverse_camera);
if (first_person->tick != interpolation_tick)
{
/* the pose drawn last, at the end of the previous tick */
first_person->has_previous = first_person->node_count == node_count;
/* the pose drawn last, if that was at the end of the tick just
before: not one left from before the view was last away from first
person (zoomed, in a vehicle, dead, in a cinematic), seconds old */
first_person->has_previous = first_person->tick == interpolation_tick - 1 &&
first_person->node_count == node_count;
memcpy(first_person->previous, first_person->latest, sizeof(first_person->previous));
first_person->tick = interpolation_tick;
}
@@ -827,13 +846,31 @@ void render_interpolation_first_person(
first_person->node_count = node_count;
if (!first_person->has_previous)
return;
/* a node that jumped further in the camera's frame than a tick allows:
the last pose was another weapon's skeleton (of as many nodes), not this
one moving (so written that a position not a number snaps) */
for (node_index = 0; node_index < node_count; node_index++)
{
real_matrix4x3 const *previous = &first_person->previous[node_index];
real_matrix4x3 const *latest = &first_person->latest[node_index];
if (!(distance_squared(&previous->position, &latest->position) <=
FIRST_PERSON_SNAP_DISTANCE * FIRST_PERSON_SNAP_DISTANCE))
{
return;
}
rotation_from_matrix(previous, &previous_rotations[node_index]);
rotation_from_matrix(latest, &latest_rotations[node_index]);
}
for (node_index = 0; node_index < node_count; node_index++)
{
real_matrix4x3 blended;
matrix_blend(
matrix_blend_rotations(
&first_person->previous[node_index],
&first_person->latest[node_index],
&previous_rotations[node_index],
&latest_rotations[node_index],
interpolation_fraction,
&blended);
matrix4x3_multiply(&camera_matrix, &blended, &node_matrices[node_index]);
+875
View File
@@ -0,0 +1,875 @@
/*
VR_ARMS.C
The VR arms' bones (vr_arms.h): the first-person hands' arms bent from the
shoulders to the hands (interface/first_person_weapons.c poses them).
The solver, the forearm's twist and their helpers are after
moistman42069/HaloCE-Quest-VR (CC0), port/linux/game/vr_render.c, nearly
as they are there: the skeleton is read through its animation graph's
index (the nodes under each node found once a frame, not per node), and
the time is the headset's frame's (halo_vr_pose_time).
*/
#include "cseries.h"
#include "math/real_math.h"
#include "models/model_animation_definitions.h"
#include "halo_vr.h"
#include "vr_arms.h"
#include <math.h>
#include <string.h>
#ifdef HALO_ANDROID
#define VR_UNITS_PER_METRE (1.0f / 3.048f)
/* ---------- the skeleton */
boolean vr_skeleton_build(
struct vr_skeleton *skeleton,
long graph_index,
short node_count)
{
short node_index;
skeleton->graph_index = NONE;
if (graph_index == NONE || node_count <= 0 || node_count > MAXIMUM_NODES_PER_ANIMATION ||
node_count != animation_graph_node_count(graph_index))
{
return FALSE;
}
for (node_index = 0; node_index < node_count; node_index++)
{
short parent = animation_graph_node_parent(graph_index, node_index);
/* (parents before their children: else no parent) */
skeleton->parents[node_index] = parent >= 0 && parent < node_index ? parent : NONE;
skeleton->under[node_index] = 0;
}
/* each node under itself and all its ancestors */
for (node_index = 0; node_index < node_count; node_index++)
{
short ancestor = node_index;
while (ancestor != NONE)
{
skeleton->under[ancestor] |= 1ULL << node_index;
ancestor = skeleton->parents[ancestor];
}
}
skeleton->graph_index = graph_index;
skeleton->node_count = node_count;
return TRUE;
}
boolean vr_skeleton_under(
struct vr_skeleton const *skeleton,
short node_index,
short root)
{
return root >= 0 && root < skeleton->node_count && node_index >= 0 && node_index < skeleton->node_count &&
(skeleton->under[root] & (1ULL << node_index)) != 0;
}
/* ---------- rotations (column vectors) */
/* v += a * scale */
static void vr_vector_scale_add_local(
real_vector3d *v,
real_vector3d const *a,
real scale)
{
v->i += a->i * scale;
v->j += a->j * scale;
v->k += a->k * scale;
}
static real vr_length(
real_vector3d const *v)
{
return (real)sqrt(v->i * v->i + v->j * v->j + v->k * v->k);
}
static void vr_point_minus(
real_point3d const *a,
real_point3d const *b,
real_vector3d *out)
{
out->i = a->x - b->x;
out->j = a->y - b->y;
out->k = a->z - b->z;
}
static boolean vr_unit_vector(
real_vector3d *v)
{
real length = vr_length(v);
if (!(length > 1e-5f && length < 1e8f))
return FALSE;
v->i /= length;
v->j /= length;
v->k /= length;
return TRUE;
}
/* the rotation taking direction a to direction b, the shortest way */
static void vr_rotation_between(
real_vector3d const *a,
real_vector3d const *b,
real rotation[3][3])
{
real_vector3d u = *a, v = *b, axis;
real c, s, t;
normalize3d(&u);
normalize3d(&v);
cross_product3d(&u, &v, &axis);
s = vr_length(&axis);
c = u.i * v.i + u.j * v.j + u.k * v.k;
if (s < 1e-6f)
{
memset(rotation, 0, sizeof(real) * 9);
if (c >= 0.0f)
{
rotation[0][0] = rotation[1][1] = rotation[2][2] = 1.0f;
}
else
{
/* half a turn about an axis across */
real_vector3d basis;
int row, column;
basis.i = 1.0f;
basis.j = basis.k = 0.0f;
if (fabs(u.i) > 0.8f)
{
basis.i = 0.0f;
basis.j = 1.0f;
}
cross_product3d(&u, &basis, &axis);
normalize3d(&axis);
for (row = 0; row < 3; row++)
{
for (column = 0; column < 3; column++)
rotation[row][column] = 2.0f * axis.n[row] * axis.n[column] - (row == column ? 1.0f : 0.0f);
}
}
return;
}
axis.i /= s;
axis.j /= s;
axis.k /= s;
t = 1.0f - c;
rotation[0][0] = c + axis.i * axis.i * t;
rotation[0][1] = axis.i * axis.j * t - axis.k * s;
rotation[0][2] = axis.i * axis.k * t + axis.j * s;
rotation[1][0] = axis.j * axis.i * t + axis.k * s;
rotation[1][1] = c + axis.j * axis.j * t;
rotation[1][2] = axis.j * axis.k * t - axis.i * s;
rotation[2][0] = axis.k * axis.i * t - axis.j * s;
rotation[2][1] = axis.k * axis.j * t + axis.i * s;
rotation[2][2] = c + axis.k * axis.k * t;
}
/* a rotation of angle radians about the unit axis */
static void vr_axis_rotation(
real_vector3d const *axis,
real angle,
real rotation[3][3])
{
real c = (real)cos(angle), s = (real)sin(angle), t = 1.0f - c;
real x = axis->i, y = axis->j, z = axis->k;
rotation[0][0] = c + x * x * t;
rotation[0][1] = x * y * t - z * s;
rotation[0][2] = x * z * t + y * s;
rotation[1][0] = y * x * t + z * s;
rotation[1][1] = c + y * y * t;
rotation[1][2] = y * z * t - x * s;
rotation[2][0] = z * x * t - y * s;
rotation[2][1] = z * y * t + x * s;
rotation[2][2] = c + z * z * t;
}
static void vr_rotate_vector(
real rotation[3][3],
real_vector3d const *in,
real_vector3d *out)
{
real_vector3d v = *in;
out->i = rotation[0][0] * v.i + rotation[0][1] * v.j + rotation[0][2] * v.k;
out->j = rotation[1][0] * v.i + rotation[1][1] * v.j + rotation[1][2] * v.k;
out->k = rotation[2][0] * v.i + rotation[2][1] * v.j + rotation[2][2] * v.k;
}
static void vr_multiply_rotations(
real a[3][3],
real b[3][3],
real out[3][3])
{
real r[3][3];
int row, column;
for (row = 0; row < 3; row++)
{
for (column = 0; column < 3; column++)
r[row][column] = a[row][0] * b[0][column] + a[row][1] * b[1][column] + a[row][2] * b[2][column];
}
memcpy(out, r, sizeof(r));
}
/* a node moved from `from` to `to` and turned by rotation about itself: its
matrix, or that of a node it carries (whose old matrix is node) */
static void vr_carry(
real_matrix4x3 *node,
real_point3d const *from,
real_point3d const *to,
real rotation[3][3])
{
real_vector3d offset;
vr_point_minus(&node->position, from, &offset);
vr_rotate_vector(rotation, &offset, &offset);
node->position.x = to->x + offset.i;
node->position.y = to->y + offset.j;
node->position.z = to->z + offset.k;
vr_rotate_vector(rotation, &node->forward, &node->forward);
vr_rotate_vector(rotation, &node->left, &node->left);
vr_rotate_vector(rotation, &node->up, &node->up);
}
/* ---------- the arm */
void vr_solve_arm(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES],
real_point3d shoulder,
real_point3d const *target,
real_vector3d const *pole,
boolean hand_stays,
struct vr_limb_history *history)
{
real_matrix4x3 old[NUMBER_OF_VR_ARM_BONES];
real rotation[NUMBER_OF_VR_ARM_BONES][3][3];
real_point3d moved_to[NUMBER_OF_VR_ARM_BONES];
real_vector3d upper, fore, reach, direction, bend, old_direction;
real upper_length, fore_length, distance, along, across;
real_point3d solved_target = *target;
short moved[MAXIMUM_NODES_PER_ANIMATION];
short index, bone;
for (bone = 0; bone < NUMBER_OF_VR_ARM_BONES; bone++)
{
if (bones[bone] < 0 || bones[bone] >= skeleton->node_count)
return;
old[bone] = matrices[bones[bone]];
}
vr_point_minus(&old[_vr_arm_fore].position, &old[_vr_arm_upper].position, &upper);
vr_point_minus(&old[_vr_arm_hand].position, &old[_vr_arm_fore].position, &fore);
upper_length = vr_length(&upper);
fore_length = vr_length(&fore);
if (!isfinite(upper_length) || !isfinite(fore_length) || upper_length < 1e-4f || fore_length < 1e-4f)
return;
vr_point_minus(target, &shoulder, &reach);
distance = vr_length(&reach);
if (!isfinite(distance) || distance < 1e-4f)
return;
direction = reach;
normalize3d(&direction);
/* out of reach: the shoulder reaches a little (8 cm), then the arm
stretches a little (18%); the hand stays where it is held */
if (distance > (upper_length + fore_length) * (history ? 0.996f : 0.999f))
{
real maximum = (upper_length + fore_length) * (history ? 0.996f : 0.999f);
real travel = distance - maximum;
travel = MIN(travel, 0.08f * VR_UNITS_PER_METRE);
shoulder.x += direction.i * travel;
shoulder.y += direction.j * travel;
shoulder.z += direction.k * travel;
distance -= travel;
if (history && distance > maximum)
{
real stretch = MIN(distance / maximum, 1.18f);
upper_length *= stretch;
fore_length *= stretch;
maximum *= stretch;
}
if (hand_stays && distance > maximum)
{
/* (further still, as this port had it: the shoulder follows the
hand, and the arm stays whole) */
shoulder.x += direction.i * (distance - maximum);
shoulder.y += direction.j * (distance - maximum);
shoulder.z += direction.k * (distance - maximum);
}
distance = MIN(distance, maximum);
}
if (history)
{
/* bent no further than 145 degrees: nearer, the bones shorten (to
65% at most) */
real minimum = (real)sqrt(upper_length * upper_length + fore_length * fore_length -
1.638304f * upper_length * fore_length);
if (distance < minimum)
{
real compress = MAX(0.65f, distance / minimum);
upper_length *= compress;
fore_length *= compress;
}
}
if (distance < (real)fabs(upper_length - fore_length) + 1e-3f)
distance = (real)fabs(upper_length - fore_length) + 1e-3f;
solved_target.x = shoulder.x + direction.i * distance;
solved_target.y = shoulder.y + direction.j * distance;
solved_target.z = shoulder.z + direction.k * distance;
/* the elbow: in the plane of the reach and the pole */
{
real dot = pole->i * direction.i + pole->j * direction.j + pole->k * direction.k;
bend.i = pole->i - direction.i * dot;
bend.j = pole->j - direction.j * dot;
bend.k = pole->k - direction.k * dot;
if (vr_length(&bend) < 1e-4f)
{
/* reaching along the pole: the old bend, else a steady side */
dot = dot_product3d(&upper, &direction);
bend = upper;
bend.i -= dot * direction.i;
bend.j -= dot * direction.j;
bend.k -= dot * direction.k;
if (vr_length(&bend) < 1e-4f)
{
real_vector3d basis;
basis.i = basis.j = 0.0f;
basis.k = 1.0f;
if (fabs(direction.k) > 0.9f)
{
basis.j = 1.0f;
basis.k = 0.0f;
}
cross_product3d(&direction, &basis, &bend);
}
}
if (history)
{
/* the elbow's swing about the reach, eased (18 a second, at most 8
radians a second) unless the arm was away or jumped */
double now = halo_vr_pose_time();
double dt = now - history->time;
real_vector3d previous = history->bend, travel;
real confidence = PIN(vr_length(&bend) / 0.35f, 0.0f, 1.0f);
vr_point_minus(&shoulder, &history->shoulder, &travel);
dot = dot_product3d(&previous, &direction);
previous.i -= dot * direction.i;
previous.j -= dot * direction.j;
previous.k -= dot * direction.k;
if (history->valid && history->graph_index == skeleton->graph_index && dt >= 0.0 && dt < 0.25 &&
vr_length(&travel) < 0.5f * VR_UNITS_PER_METRE && vr_unit_vector(&previous) && vr_unit_vector(&bend))
{
real_vector3d tangent;
real angle, change;
cross_product3d(&previous, &bend, &tangent);
angle = (real)atan2(dot_product3d(&tangent, &direction), dot_product3d(&previous, &bend));
change = angle * (1.0f - (real)exp(-18.0 * MIN(dt, 0.05))) * confidence;
change = PIN(change, (real)(-8.0 * dt), (real)(8.0 * dt));
cross_product3d(&direction, &previous, &tangent);
bend.i = previous.i * (real)cos(change) + tangent.i * (real)sin(change);
bend.j = previous.j * (real)cos(change) + tangent.j * (real)sin(change);
bend.k = previous.k * (real)cos(change) + tangent.k * (real)sin(change);
}
else
{
history->roll_valid = FALSE;
}
normalize3d(&bend);
history->valid = TRUE;
history->graph_index = skeleton->graph_index;
history->time = now;
history->shoulder = shoulder;
history->bend = bend;
}
else
{
normalize3d(&bend);
}
}
along = (upper_length * upper_length + distance * distance - fore_length * fore_length) / (2.0f * distance);
across = upper_length * upper_length - along * along;
across = across > 0.0f ? (real)sqrt(across) : 0.0f;
moved_to[_vr_arm_upper] = shoulder;
moved_to[_vr_arm_fore].x = shoulder.x + direction.i * along + bend.i * across;
moved_to[_vr_arm_fore].y = shoulder.y + direction.j * along + bend.j * across;
moved_to[_vr_arm_fore].z = shoulder.z + direction.k * along + bend.k * across;
moved_to[_vr_arm_hand] = solved_target;
/* each bone turned to point at the next joint, and the hand carried by
the forearm's turn */
vr_point_minus(&moved_to[_vr_arm_fore], &moved_to[_vr_arm_upper], &reach);
vr_rotation_between(&upper, &reach, rotation[_vr_arm_upper]);
vr_rotate_vector(rotation[_vr_arm_upper], &fore, &old_direction);
vr_point_minus(&moved_to[_vr_arm_hand], &moved_to[_vr_arm_fore], &reach);
vr_rotation_between(&old_direction, &reach, rotation[_vr_arm_fore]);
vr_multiply_rotations(rotation[_vr_arm_fore], rotation[_vr_arm_upper], rotation[_vr_arm_fore]);
if (hand_stays)
{
/* the hand stays as it was put, and what it holds */
memset(rotation[_vr_arm_hand], 0, sizeof(rotation[_vr_arm_hand]));
rotation[_vr_arm_hand][0][0] = rotation[_vr_arm_hand][1][1] = rotation[_vr_arm_hand][2][2] = 1.0f;
moved_to[_vr_arm_hand] = old[_vr_arm_hand].position;
}
else
{
memcpy(rotation[_vr_arm_hand], rotation[_vr_arm_fore], sizeof(rotation[_vr_arm_hand]));
}
/* every node under a moved bone goes with the nearest one above it
(parents come first) */
for (index = 0; index < skeleton->node_count; index++)
{
short parent = skeleton->parents[index];
moved[index] = NONE;
for (bone = 0; bone < NUMBER_OF_VR_ARM_BONES; bone++)
{
if (bones[bone] == index)
moved[index] = bone;
}
if (moved[index] == NONE && parent != NONE)
moved[index] = moved[parent];
if (moved[index] != NONE)
{
bone = moved[index];
vr_carry(&matrices[index], &old[bone].position, &moved_to[bone], rotation[bone]);
}
}
}
/* The controller's roll shared along the arm rather than all of it at the
wrist's skin: the hand's turn about the forearm from where the animation
has it against its forearm (old_fore, old_hand), 85% of it on the forearm
(none on the upper arm: vr_align_elbow rolls it), within 110 degrees, running on across half a
turn (history). The bones' places and the hand stay. */
void vr_distribute_arm_twist(
real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES],
real_matrix4x3 const *old_fore,
real_matrix4x3 const *old_hand,
struct vr_limb_history *history)
{
real_matrix4x3 *fore = &matrices[bones[_vr_arm_fore]];
real_matrix4x3 *hand = &matrices[bones[_vr_arm_hand]];
real_matrix3x3 delta;
real_vector3d expected[3], desired[3], axis;
real_quaternion q;
real angle, rotation[3][3];
int a, b, row, column;
vr_point_minus(&hand->position, &fore->position, &axis);
if (!vr_unit_vector(&axis))
return;
for (a = 0; a < 3; a++)
{
expected[a].i = expected[a].j = expected[a].k = 0.0f;
desired[a].i = hand->n[a][0];
desired[a].j = hand->n[a][1];
desired[a].k = hand->n[a][2];
for (b = 0; b < 3; b++)
{
real amount = old_fore->n[b][0] * old_hand->n[a][0] + old_fore->n[b][1] * old_hand->n[a][1] +
old_fore->n[b][2] * old_hand->n[a][2];
for (row = 0; row < 3; row++)
expected[a].n[row] += fore->n[b][row] * amount;
}
if (!vr_unit_vector(&expected[a]) || !vr_unit_vector(&desired[a]))
return;
}
for (row = 0; row < 3; row++)
{
for (column = 0; column < 3; column++)
{
delta.n[row][column] = 0.0f;
for (a = 0; a < 3; a++)
delta.n[row][column] += desired[a].n[row] * expected[a].n[column];
}
}
matrix3x3_rotation_to_quaternion(&delta, &q);
angle = 2.0f * (real)atan2(dot_product3d(&q.v, &axis), q.w);
if (!isfinite(angle))
return;
angle = (real)atan2(sin(angle), cos(angle));
/* (continuous across half a turn before the limit) */
if (history && history->roll_valid)
angle = history->roll + (real)atan2(sin(angle - history->roll), cos(angle - history->roll));
angle = PIN(angle, -1.92f, 1.92f);
if (history)
{
history->roll = angle;
history->roll_valid = TRUE;
}
vr_axis_rotation(&axis, angle * 0.85f, rotation);
vr_rotate_vector(rotation, &fore->forward, &fore->forward);
vr_rotate_vector(rotation, &fore->left, &fore->left);
vr_rotate_vector(rotation, &fore->up, &fore->up);
}
/* The upper arm rolled about its length so that the elbow bends in the
plane the arm now bends in, as the animation has it bend: the shortest
turn that took the upper arm to its new direction leaves its hinge where
it was, and with the elbow out to the side the elbow's skin turned in
(Quest, 2026-10-06). The hinge is the animation's bend (upper arm by
forearm, old_upper, old_fore, old_hand) in the upper arm's own axes. A
straight arm, then or now, has no bend: left as it is. */
void vr_align_elbow(
real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES],
real_matrix4x3 const *old_upper,
real_matrix4x3 const *old_fore,
real_matrix4x3 const *old_hand)
{
real_matrix4x3 *upper = &matrices[bones[_vr_arm_upper]];
real_matrix4x3 const *fore = &matrices[bones[_vr_arm_fore]];
real_matrix4x3 const *hand = &matrices[bones[_vr_arm_hand]];
real_vector3d old_along, old_below, old_bend, along, below, bend, hinge;
real c_forward, c_left, c_up, angle, rotation[3][3];
vr_point_minus(&old_fore->position, &old_upper->position, &old_along);
vr_point_minus(&old_hand->position, &old_fore->position, &old_below);
cross_product3d(&old_along, &old_below, &old_bend);
vr_point_minus(&fore->position, &upper->position, &along);
vr_point_minus(&hand->position, &fore->position, &below);
cross_product3d(&along, &below, &bend);
if (vr_length(&old_bend) < 1e-4f * vr_length(&old_along) * vr_length(&old_below) ||
vr_length(&bend) < 0.05f * vr_length(&along) * vr_length(&below) ||
!vr_unit_vector(&old_bend) || !vr_unit_vector(&bend) || !vr_unit_vector(&along))
{
return;
}
/* (the hinge in the upper arm's axes, then where those axes are now) */
c_forward = dot_product3d(&old_bend, &old_upper->forward);
c_left = dot_product3d(&old_bend, &old_upper->left);
c_up = dot_product3d(&old_bend, &old_upper->up);
hinge.i = upper->forward.i * c_forward + upper->left.i * c_left + upper->up.i * c_up;
hinge.j = upper->forward.j * c_forward + upper->left.j * c_left + upper->up.j * c_up;
hinge.k = upper->forward.k * c_forward + upper->left.k * c_left + upper->up.k * c_up;
/* (both across the arm: the turn about it between them) */
vr_vector_scale_add_local(&hinge, &along, -dot_product3d(&hinge, &along));
vr_vector_scale_add_local(&bend, &along, -dot_product3d(&bend, &along));
if (!vr_unit_vector(&hinge) || !vr_unit_vector(&bend))
return;
{
real_vector3d cross;
cross_product3d(&hinge, &bend, &cross);
angle = (real)atan2(dot_product3d(&cross, &along), dot_product3d(&hinge, &bend));
}
if (!isfinite(angle))
return;
vr_axis_rotation(&along, angle, rotation);
vr_rotate_vector(rotation, &upper->forward, &upper->forward);
vr_rotate_vector(rotation, &upper->left, &upper->left);
vr_rotate_vector(rotation, &upper->up, &upper->up);
}
/* ---------- the fingers
Each finger's joints are found by name: "finger" and its number (0 the
thumb to 4 the little finger), or "thumb", "index", "middle", "ring",
"pinky", on the hand's side ("l ", "r "). They are posed by absolute
directions in the controller's palm frame, each segment's subtree turned,
its length kept (after HaloCE-Quest-VR, CC0, nearly verbatim; its touch of
the world, a few rays a finger, is left out: it costs the Steam Frame's
processor too much). */
/* the finger (0 the thumb .. 4 the little finger) a node's name says it is
a joint of, on that side; NONE when not a finger's */
static short vr_finger_of(
char const *name,
char const *side)
{
static char const *const names[] = { "thumb", "index", "middle", "ring", "pinky" };
char const *finger;
short index;
if (!name || !strstr(name, side))
return NONE;
finger = strstr(name, "finger");
if (finger && finger[6] >= '0' && finger[6] <= '4')
return (short)(finger[6] - '0');
for (index = 0; index < 5; index++)
{
if (strstr(name, names[index]))
return index;
}
return NONE;
}
void vr_finger_nodes(
struct vr_skeleton const *skeleton,
short hand,
char const *side,
short joints[5][3])
{
short node_index, finger, depth;
for (finger = 0; finger < 5; finger++)
{
for (depth = 0; depth < 3; depth++)
joints[finger][depth] = NONE;
}
for (node_index = 0; node_index < skeleton->node_count; node_index++)
{
short at;
finger = vr_finger_of(animation_graph_node_name(skeleton->graph_index, node_index), side);
if (finger == NONE || !vr_skeleton_under(skeleton, node_index, hand))
continue;
/* its depth: how many of the same finger's joints above it */
depth = 0;
for (at = skeleton->parents[node_index]; at != NONE; at = skeleton->parents[at])
{
if (vr_finger_of(animation_graph_node_name(skeleton->graph_index, at), side) == finger)
depth++;
}
if (depth < 3)
joints[finger][depth] = node_index;
}
}
/* a node and all under it turned about the node */
static void vr_turn_subtree(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short root,
real rotation[3][3])
{
real_point3d pivot = matrices[root].position;
unsigned long long under = skeleton->under[root];
short node_index;
for (node_index = 0; node_index < skeleton->node_count; node_index++)
{
if (under & (1ULL << node_index))
vr_carry(&matrices[node_index], &pivot, &pivot, rotation);
}
}
/* a finger's joints (low, middle, tip) curled 0 (straight, a little bent
at rest) to 1 (a fist), in the palm's frame: forward along the fingers,
up the knuckles towards the index, palm out of the palm */
static boolean vr_pose_finger(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short hand,
short const joints[3],
int finger,
real curl,
real_vector3d const *palm,
real_vector3d const *forward,
real_vector3d const *up)
{
static real const rest[3] = { 0.12f, 0.15f, 0.10f };
static real const bend[3] = { 1.25f, 1.6f, 1.1f };
real_vector3d base, axis, direction, wanted;
real angle = 0.0f, dot, rotation[3][3];
int depth;
if (joints[0] == NONE || joints[1] == NONE || joints[2] == NONE)
return FALSE;
vr_point_minus(&matrices[joints[0]].position, &matrices[hand].position, &base);
dot = dot_product3d(&base, palm);
base.i -= dot * palm->i;
base.j -= dot * palm->j;
base.k -= dot * palm->k;
if (finger == 0)
{
/* the thumb: up the handle, or across the palm */
base.i = (up->i * 0.9f + forward->i * 0.3f) * (1.0f - curl) +
(forward->i * 0.75f + palm->i * 0.55f + up->i * 0.1f) * curl;
base.j = (up->j * 0.9f + forward->j * 0.3f) * (1.0f - curl) +
(forward->j * 0.75f + palm->j * 0.55f + up->j * 0.1f) * curl;
base.k = (up->k * 0.9f + forward->k * 0.3f) * (1.0f - curl) +
(forward->k * 0.75f + palm->k * 0.55f + up->k * 0.1f) * curl;
}
if (!vr_unit_vector(&base))
return FALSE;
cross_product3d(&base, palm, &axis);
if (!vr_unit_vector(&axis))
return FALSE;
for (depth = 0; depth < 3; depth++)
{
short joint = joints[depth];
if (depth < 2)
{
vr_point_minus(&matrices[joints[depth + 1]].position, &matrices[joint].position, &direction);
}
else
{
/* the tip: its own axis that the middle joint's bone runs along */
real_vector3d previous;
real_vector3d *axes[3];
real_vector3d *last[3];
real best = 0.8f, sign = 1.0f;
int a, found = -1;
axes[0] = &matrices[joints[1]].forward;
axes[1] = &matrices[joints[1]].left;
axes[2] = &matrices[joints[1]].up;
last[0] = &matrices[joint].forward;
last[1] = &matrices[joint].left;
last[2] = &matrices[joint].up;
vr_point_minus(&matrices[joint].position, &matrices[joints[1]].position, &previous);
if (!vr_unit_vector(&previous))
return FALSE;
for (a = 0; a < 3; a++)
{
real_vector3d unit = *axes[a];
if (!vr_unit_vector(&unit))
continue;
dot = dot_product3d(&unit, &previous);
if (fabs(dot) > best)
{
best = (real)fabs(dot);
found = a;
sign = dot < 0.0f ? -1.0f : 1.0f;
}
}
if (found < 0)
return TRUE;
direction.i = last[found]->i * sign;
direction.j = last[found]->j * sign;
direction.k = last[found]->k * sign;
}
if (!vr_unit_vector(&direction))
return FALSE;
angle += finger == 0 ? (depth == 0 ? 0.0f : (depth == 1 ? 0.35f : 0.3f) * curl) :
rest[depth] + bend[depth] * curl;
vr_axis_rotation(&axis, angle, rotation);
vr_rotate_vector(rotation, &base, &wanted);
vr_rotation_between(&direction, &wanted, rotation);
vr_turn_subtree(skeleton, matrices, joint, rotation);
}
return TRUE;
}
void vr_pose_fingers(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short hand,
short const joints[5][3],
float const curls[4],
real_vector3d const *palm,
real_vector3d const *forward,
real_vector3d const *up)
{
int finger;
for (finger = 0; finger < 5; finger++)
vr_pose_finger(skeleton, matrices, hand, joints[finger], finger, curls[finger < 3 ? finger : 3], palm, forward, up);
}
/* the finger's middle and tip turned this much (0..1) towards the line of
its low joint: a finger lifted off the trigger (the weapon hand's index,
whose low joint, and the rest of the hand, keep the weapon's grip) */
void vr_straighten_finger(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short const joints[3],
real amount)
{
real_vector3d low, middle, axis;
real rotation[3][3];
real angle;
if (amount <= 0.0f || joints[0] == NONE || joints[1] == NONE || joints[2] == NONE)
return;
vr_point_minus(&matrices[joints[1]].position, &matrices[joints[0]].position, &low);
vr_point_minus(&matrices[joints[2]].position, &matrices[joints[1]].position, &middle);
if (!vr_unit_vector(&low) || !vr_unit_vector(&middle))
return;
cross_product3d(&middle, &low, &axis);
if (!vr_unit_vector(&axis))
return;
angle = (real)acos(PIN(dot_product3d(&middle, &low), -1.0f, 1.0f)) * PIN(amount, 0.0f, 1.0f);
vr_axis_rotation(&axis, angle, rotation);
vr_turn_subtree(skeleton, matrices, joints[1], rotation);
}
/* ---------- hands alone (vr.body "hands")
The arm's bones (not the hand's, its fingers' or the weapon's) shrunk to
nothing at a point 3.5 cm back from the wrist along the hand's line,
knuckles to wrist: the cuff closes just behind the hand. Shrunk onto their
own places, the forearm drew the wrist's skin to the elbow and the hand
looked cut off (after HaloCE-Quest-VR, CC0, its vr_hide_forearms and
vr_hand_back_axis). */
void vr_hide_arm(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES],
short const fingers[5][3],
short gun)
{
real_point3d centre = matrices[bones[_vr_arm_hand]].position;
real_point3d knuckles;
real_vector3d back;
boolean along = FALSE;
short finger, node_index, found = 0;
knuckles.x = knuckles.y = knuckles.z = 0.0f;
/* (the four fingers' bases: the thumb's sits off the line) */
for (finger = 1; finger < 5; finger++)
{
if (fingers[finger][0] == NONE)
continue;
knuckles.x += matrices[fingers[finger][0]].position.x;
knuckles.y += matrices[fingers[finger][0]].position.y;
knuckles.z += matrices[fingers[finger][0]].position.z;
found++;
}
if (found)
{
knuckles.x /= found;
knuckles.y /= found;
knuckles.z /= found;
vr_point_minus(&centre, &knuckles, &back);
along = vr_unit_vector(&back);
}
if (!along)
{
vr_point_minus(&matrices[bones[_vr_arm_fore]].position, &centre, &back);
along = vr_unit_vector(&back);
}
if (along)
{
centre.x += back.i * 0.035f * VR_UNITS_PER_METRE;
centre.y += back.j * 0.035f * VR_UNITS_PER_METRE;
centre.z += back.k * 0.035f * VR_UNITS_PER_METRE;
}
for (node_index = 0; node_index < skeleton->node_count; node_index++)
{
if (!vr_skeleton_under(skeleton, node_index, bones[_vr_arm_upper]) ||
vr_skeleton_under(skeleton, node_index, bones[_vr_arm_hand]) ||
(gun != NONE && vr_skeleton_under(skeleton, node_index, gun)))
{
continue;
}
matrices[node_index].scale = 0.0f;
matrices[node_index].position = centre;
}
}
#endif
+83
View File
@@ -0,0 +1,83 @@
/*
VR_ARMS.H
The VR arms' bones (vr_arms.c), for the first-person hands
(interface/first_person_weapons.c): the arms bent from the shoulders to the
hands. Include after the game's math and animation headers.
*/
#ifndef __VR_ARMS_H
#define __VR_ARMS_H
#ifdef HALO_ANDROID
enum
{
_vr_arm_upper,
_vr_arm_fore,
_vr_arm_hand,
NUMBER_OF_VR_ARM_BONES
};
/* a skeleton's parents, and the nodes under each node (it among them), from
its animation graph: parents come before their children */
struct vr_skeleton
{
long graph_index;
short node_count;
short parents[MAXIMUM_NODES_PER_ANIMATION];
unsigned long long under[MAXIMUM_NODES_PER_ANIMATION];
};
/* an arm's last frame, for its elbow's swing and its forearm's twist to run
on smoothly (one each arm) */
struct vr_limb_history
{
long graph_index;
double time;
real_point3d shoulder;
real_vector3d bend;
boolean valid;
boolean roll_valid;
real roll;
};
boolean vr_skeleton_build(struct vr_skeleton *skeleton, long graph_index, short node_count);
boolean vr_skeleton_under(struct vr_skeleton const *skeleton, short node_index, short root);
/* one arm (bones: upper arm, forearm, hand) bent from the shoulder for the
hand to reach target, its elbow towards the pole; the hand (and what it
holds) stays where it is, or turns with the forearm */
void vr_solve_arm(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES], real_point3d shoulder, real_point3d const *target,
real_vector3d const *pole, boolean hand_stays, struct vr_limb_history *history);
/* the hand's roll from the animation (old_fore, old_hand: the arm as
animated) shared by the forearm and the upper arm */
void vr_align_elbow(real_matrix4x3 *matrices, short const bones[NUMBER_OF_VR_ARM_BONES],
real_matrix4x3 const *old_upper, real_matrix4x3 const *old_fore, real_matrix4x3 const *old_hand);
void vr_distribute_arm_twist(real_matrix4x3 *matrices, short const bones[NUMBER_OF_VR_ARM_BONES],
real_matrix4x3 const *old_fore, real_matrix4x3 const *old_hand, struct vr_limb_history *history);
/* a hand's fingers' joints (low, middle, tip; NONE where not found), by
their names on that side ("l ", "r "): the thumb, index, middle, ring and
little finger */
void vr_finger_nodes(struct vr_skeleton const *skeleton, short hand, char const *side, short joints[5][3]);
/* the fingers curled (halo_vr_finger_curls: the thumb, index, middle, the
last two) in the palm's frame: forward along the fingers, up the knuckles
towards the index, palm out of the palm */
void vr_pose_fingers(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices, short hand,
short const joints[5][3], float const curls[4], real_vector3d const *palm, real_vector3d const *forward,
real_vector3d const *up);
/* a finger's middle and tip turned this much (0..1) into line with its low
joint */
void vr_straighten_finger(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices, short const joints[3],
real amount);
/* the arm's bones shrunk to nothing just behind the wrist, the hand (with
its fingers) and the weapon left: the hand alone */
void vr_hide_arm(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES], short const fingers[5][3], short gun);
#endif
#endif
+183
View File
@@ -0,0 +1,183 @@
/*
VR_GRAPHICS.C
The VR graphics presets (vr.graphics, the VR options' "Graphics" row): which
of the game's costlier effects are drawn, for headsets with little time to
spare (the Steam Frame spends most of its frame in its GL driver: fewer
draws help it most; the Quest 3 holds 90 Hz with all of them).
- "high", the default: everything, as before;
- "medium": no object shadows, environment reflections or lens flares;
- "low": nor specular light, bump maps, grass and other detail objects,
the screen's fog layer, contrails, weather or camouflage's second pass;
- "max": "high", the models lit for each pixel and the shadows drawn at
512x512 (d3d8_gl.c asks halo_vr_graphics).
Each effect is a switch the game has already: the rasterizer's console
variables (rasterizer_debug_options) and render.c's effect switches, after
HaloCE-Quest-VR's vr_graphics.c (CC0). Scripts and the console set some of
them (render_effects turns particles off in some cutscenes): a switch is
drawn only while both the game and the preset want it. A change the game
made since the last frame is taken as its wish, kept and combined with the
preset's. Playing in another's game, a client draws grass, lens flares and
the screen's fog as the host's rules have it (cheats.c), whatever the
preset; the reductions it does keep there, cheats.c leaves alone
(vr_graphics_reduces).
What each costs: shadows (each object's map, blur and projection: draws),
specular light and reflections (passes of the level's geometry: draws and
fill), detail objects (many small draws), lens flares (their visibility
tests), contrails and weather (particles), bump maps, the fog layer and
camouflage's pass (the GPU's fill).
*/
#ifdef HALO_ANDROID
#include "cseries.h"
#include "rasterizer/rasterizer_console_vars.h"
#include "networking/network_game_globals.h"
#include "halo_vr.h"
/* source/render/render.c, render_objects.c; source/effects/
weather_particle_systems.c */
extern boolean render_shadows;
extern boolean render_contrails_enabled;
extern boolean render_weather_particle_systems_enabled;
extern boolean weather;
void platform_log(char const *format, ...);
/* an effect: whether each preset draws it (low, medium; high and max draw
everything), whether a client in another's game draws it anyway, and the
game's switches it governs */
struct vr_graphics_effect
{
char const *name;
boolean preset[2];
boolean host_rules;
boolean *switches[2];
};
static struct vr_graphics_effect vr_graphics_effects[] =
{
{ "shadows", { FALSE, FALSE }, FALSE,
{ &render_shadows, &rasterizer_debug_options.draw_environment_shadows } },
{ "specular", { FALSE, TRUE }, FALSE,
{ &rasterizer_debug_options.draw_environment_specular_lights,
&rasterizer_debug_options.draw_environment_specular_lightmaps } },
{ "reflections", { FALSE, FALSE }, FALSE,
{ &rasterizer_debug_options.draw_environment_reflections, NULL } },
{ "bump maps", { FALSE, TRUE }, FALSE,
{ &rasterizer_debug_options.bump_mapping_enabled, NULL } },
{ "detail objects", { FALSE, TRUE }, TRUE,
{ &rasterizer_debug_options.draw_detail_objects, NULL } },
{ "lens flares", { FALSE, FALSE }, TRUE,
{ &rasterizer_debug_options.draw_lens_flares, NULL } },
{ "screen fog", { FALSE, TRUE }, TRUE,
{ &rasterizer_debug_options.draw_environment_fog_screen, NULL } },
{ "contrails", { FALSE, TRUE }, FALSE,
{ &render_contrails_enabled, NULL } },
{ "weather", { FALSE, TRUE }, FALSE,
{ &weather, &render_weather_particle_systems_enabled } },
{ "camouflage pass", { FALSE, TRUE }, FALSE,
{ &rasterizer_debug_options.active_camouflage_multipass_enabled, NULL } },
};
#define NUMBER_OF_VR_GRAPHICS_EFFECTS (sizeof(vr_graphics_effects) / sizeof(vr_graphics_effects[0]))
#define VR_GRAPHICS_SWITCHES (sizeof(vr_graphics_effects[0].switches) / sizeof(vr_graphics_effects[0].switches[0]))
static struct
{
boolean started;
int preset;
boolean client;
/* whether the preset allows each effect; what the game wants of each
switch, and what was written last frame */
boolean allowed[NUMBER_OF_VR_GRAPHICS_EFFECTS];
boolean wanted[NUMBER_OF_VR_GRAPHICS_EFFECTS][VR_GRAPHICS_SWITCHES];
boolean written[NUMBER_OF_VR_GRAPHICS_EFFECTS][VR_GRAPHICS_SWITCHES];
} vr_graphics;
static void vr_graphics_choose(
int preset,
boolean client)
{
static char const *const names[] = { "low", "medium", "high", "max" };
unsigned int index;
vr_graphics.preset = preset;
vr_graphics.client = client;
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
{
struct vr_graphics_effect const *effect = &vr_graphics_effects[index];
vr_graphics.allowed[index] = preset >= HALO_VR_GRAPHICS_HIGH || effect->preset[preset] ||
(client && effect->host_rules);
}
platform_log("VR: graphics %s%s", names[preset], client ? " (in another's game)" : "");
}
/* each frame, before it is drawn (render.c) */
void vr_graphics_update(
void)
{
int preset = halo_vr_graphics();
boolean client = network_game_distributed_client();
unsigned int index, which;
if (!vr_graphics.started)
{
/* the game's switches as it starts are its wishes */
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
{
for (which = 0; which < VR_GRAPHICS_SWITCHES && vr_graphics_effects[index].switches[which]; which++)
{
vr_graphics.wanted[index][which] = *vr_graphics_effects[index].switches[which];
vr_graphics.written[index][which] = *vr_graphics_effects[index].switches[which];
}
}
vr_graphics.started = TRUE;
vr_graphics_choose(preset, client);
}
else if (preset != vr_graphics.preset || client != vr_graphics.client)
{
vr_graphics_choose(preset, client);
}
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
{
for (which = 0; which < VR_GRAPHICS_SWITCHES && vr_graphics_effects[index].switches[which]; which++)
{
boolean *game_switch = vr_graphics_effects[index].switches[which];
boolean value;
/* (the game, a script, the console changed it since: its wish) */
if (*game_switch != vr_graphics.written[index][which])
vr_graphics.wanted[index][which] = *game_switch;
value = vr_graphics.wanted[index][which] && vr_graphics.allowed[index];
*game_switch = value;
vr_graphics.written[index][which] = value;
}
}
}
/* whether the preset keeps this switch of the game's off now (cheats.c, a
client's rules: those it may keep off) */
boolean vr_graphics_reduces(
boolean const *game_switch)
{
unsigned int index, which;
if (!vr_graphics.started)
return FALSE;
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
{
for (which = 0; which < VR_GRAPHICS_SWITCHES && vr_graphics_effects[index].switches[which]; which++)
{
if (vr_graphics_effects[index].switches[which] == game_switch)
return !vr_graphics.allowed[index];
}
}
return FALSE;
}
#endif
@@ -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
+132 -20
View File
@@ -88,13 +88,14 @@ degrees on an axis (the other axis 0); nonzero if it has. The game counts
it as looking around (player_control.c's action tests). */
int halo_vr_look_delta(float *yaw, float *pitch);
/* The weapon in the right hand (vr.aim_with = "controller"; each returns 0,
and the game does as before, when the hand is not tracked):
/* The weapon in the right hand, or the left one (vr.handedness; vr.aim_with
= "controller"; each returns 0, and the game does as before, when the hand
is not tracked):
- the direction the player's weapon aims, where the controller points (the
body and the view keep facing where the head looks);
- where its shots start, as an offset from the unit's camera (world units);
- the camera the first-person weapon model is posed from, so that it lies
in the hand;
in the hand (in model space, below);
- the aiming laser's start and direction; */
int halo_vr_aim_vector(short local_player_index, float vector[3]);
int halo_vr_weapon_origin_offset(short local_player_index, float offset[3]);
@@ -104,35 +105,75 @@ int halo_vr_weapon_origin_offset(short local_player_index, float offset[3]);
int halo_vr_weapon_origin(short local_player_index, float origin[3]);
int halo_vr_weapon_camera(short local_player_index, float position[3], float forward[3], float up[3]);
int halo_vr_laser(short local_player_index, float start[3], float direction[3]);
/* Model space: the first-person model (first_person_weapons.c) is always
posed as the right hand's. Left-handed, it is posed in a mirror image of
the room and drawn mirrored back (its triangles' winding turned over); the
mirror: a point on it (world units) and its normal, or 0 when it is not
mirrored. What first_person_weapons.c and vr.c exchange about the model
is in model space: the weapon camera, the free hand, the foregrip, the
fingers (0 the model's left hand, the free one), the barrel and the laser
along it. */
int halo_vr_first_person_mirror(short local_player_index, float point[3], float normal[3]);
/* the laser for this frame, along the barrel (first_person_weapons.c: while
an animation turns the weapon, as in a reload), and the laser at the ready,
where the controller aims */
an animation turns the weapon, as in a reload; model space), and the laser
at the ready, where the controller aims (in the room) */
void halo_vr_set_laser(short local_player_index, const float start[3], const float direction[3]);
int halo_vr_ready_laser(short local_player_index, float start[3], float direction[3]);
/* The arms (interface/first_person_weapons.c):
- the body: the head's position (world units) and flat heading, from
which the shoulders hang;
- the left hand's pose;
- the left hand on the weapon: given where the model's left hand holds it
- the body: the eyes' position (world units), the neck they turn about,
and the torso's flat heading (which follows the head beyond a few
degrees, and the hands; the stick's turns turn it at once), from which
the shoulders hang;
- the free hand's pose (the left one, left-handed the right; model space);
- the free hand on the weapon: given where the model's left hand holds it
(world units) and the line from its right hand to its left in the
weapon's forward, right and up, whether the left hand grips it; then the
weapon's forward, right and up, whether the free hand grips it; then the
weapon aims along both hands; */
int halo_vr_body_frame(short local_player_index, float head[3], float forward[3]);
int halo_vr_left_hand(short local_player_index, float position[3], float forward[3], float up[3]);
int halo_vr_body_frame(short local_player_index, float head[3], float neck[3], float forward[3]);
int halo_vr_free_hand(short local_player_index, float position[3], float forward[3], float up[3]);
int halo_vr_two_hand(short local_player_index, const float foregrip[3], const float reference[3]);
/* - the fingers of a hand of the model (0 the left, the free hand; 1 the
right, the weapon's), from the controller's trigger, grip and touch: the thumb, index, middle finger and the last two, each
curled 0 (straight) to 1; 0 when the hand is not tracked or vr.fingers
is off, else HALO_VR_FINGERS_CURLED, and HALO_VR_FINGERS_TOUCH too when
the controller tells where the fingers rest (else a finger off the
trigger reads as one on it, unpressed); */
enum
{
HALO_VR_FINGERS_CURLED = 1,
HALO_VR_FINGERS_TOUCH = 2,
};
int halo_vr_finger_curls(short local_player_index, int hand, float curls[4]);
/* - whether the free hand's arm bends at the elbow to reach it from the
shoulder (vr.arm_ik); else it keeps the shape it has on the foregrip,
pointed at the shoulder;
- the time of the headset's frame (seconds, steady through the frame), by
which the arms' bending is eased */
int halo_vr_free_arm_bends(void);
double halo_vr_pose_time(void);
/* - what of the body is drawn (vr.body): the arms and hands, the hands
alone, or neither (the weapon alone) */
enum
{
HALO_VR_BODY_ARMS = 0,
HALO_VR_BODY_HANDS,
HALO_VR_BODY_OFF,
};
int halo_vr_body_mode(void);
/* a hand has just been thrust forward: a melee attack (xinput_sdl.c) */
int halo_vr_melee(void);
/* the weight of the melee attack under way, from the swing's speed (0..1;
0 with none): the game scales melee damage by the player's run instead,
and in VR a player punches standing (units.c) */
float halo_vr_melee_strength(void);
/* The grenade (units.c): whether the left trigger holds it in the hand; and
/* The grenade (units.c): whether the free hand's trigger holds it in the
hand; and
as it let go, whether the hand threw it: from where (world units), which
way (the game's axes) and how hard (0..1 of the game's throw) */
int halo_vr_grenade_held(short local_player_index);
int halo_vr_grenade_throw(short local_player_index, float origin[3], float direction[3], float *strength);
/* where the held grenade is drawn (render_objects.c): in the left palm, and
at what size */
/* where the held grenade is drawn (first_person_weapons.c): in the free
hand's palm, and at what size */
int halo_vr_grenade_in_hand(short local_player_index, float position[3], float forward[3], float up[3],
float *scale);
/* The scope (render.c): zoomed in by magnification, the camera for the
@@ -163,9 +204,9 @@ void halo_vr_capture_names(void);
void halo_vr_set_scope_raise(float metres);
void halo_vr_capture_scope(float left, float top, float right, float bottom);
/* where the weapon's gun node is as drawn, at the ready (world units;
first_person_weapons.c, each frame): shots, the crosshair and the laser
start there; and the direction its barrel points in the weapon camera's
frame (forward, left, up), which the camera is turned to aim */
first_person_weapons.c, each frame; model space): shots, the crosshair and
the laser start there; and the direction its barrel points in the weapon
camera's frame (forward, left, up), which the camera is turned to aim */
void halo_vr_set_barrel(short local_player_index, const float world[3], const float direction[3]);
/* how far, in world units, the weapon's aim runs before it hits something
(render.c, each frame); the crosshair floats there */
@@ -181,7 +222,8 @@ void halo_vr_capture_crosshair(void);
/* Vehicles (player_control.c, each tick): the seat the player sits in and
its vehicle's heading (yaw, radians). Seated, the body turns with the
vehicle; a driver who steers by facing (HALO_VR_SEAT_STEER) steers with the
right stick, else the right controller aims (HALO_VR_SEAT_AIM). And each
turning stick, else the weapon hand's controller aims (HALO_VR_SEAT_AIM).
And each
frame (render.c) the heading as drawn, between ticks. */
enum
{
@@ -207,6 +249,59 @@ int halo_vr_wheel_held(void);
/* the controllers' layout (HALO_XR_LAYOUT_*: 0 the Quest's, 1 the Steam
Frame's), for what the game tells the player */
int halo_vr_controller_layout(void);
/* the hand that holds the weapon (HALO_XR_HAND_*: 1 the right, 0 the left,
vr.handedness) */
int halo_vr_weapon_hand(void);
/* The game's actions on the controllers' buttons (vr.button_*; the VR
options' BUTTONS page): each action on one of seven buttons, named as
right-handed (A and B on the right controller, X and Y, the Quest's, on
the left; the move stick's click, the turning stick's, the weapon hand's
grip); left-handed with vr.mirror_controls, their mirror images. The
actions are numbered by the button each has by default. The triggers, the
free hand's grip, the menu button and the Steam Frame's bumpers, D-pad and
View button keep theirs; the menus keep the buttons as they are. */
enum
{
HALO_VR_INPUT_A = 0,
HALO_VR_INPUT_B,
HALO_VR_INPUT_X,
HALO_VR_INPUT_Y,
HALO_VR_INPUT_MOVE_STICK,
HALO_VR_INPUT_TURN_STICK,
HALO_VR_INPUT_GRIP,
HALO_VR_INPUT_COUNT,
HALO_VR_ACTION_JUMP = HALO_VR_INPUT_A,
HALO_VR_ACTION_LIGHT = HALO_VR_INPUT_B, /* a tap the flashlight; held, switch grenades */
HALO_VR_ACTION_ACTION = HALO_VR_INPUT_X, /* action, reload */
HALO_VR_ACTION_WEAPON = HALO_VR_INPUT_Y, /* switch weapon */
HALO_VR_ACTION_CROUCH = HALO_VR_INPUT_MOVE_STICK,
HALO_VR_ACTION_ZOOM = HALO_VR_INPUT_TURN_STICK,
HALO_VR_ACTION_MELEE = HALO_VR_INPUT_GRIP,
HALO_VR_ACTION_COUNT = HALO_VR_INPUT_COUNT,
};
/* an action's setting ("vr.button_jump"), a button's value in it ("a"),
and the button an action is on, as set */
const char *halo_vr_action_setting(int action);
const char *halo_vr_input_value(int input);
int halo_vr_action_input(int action);
/* the graphics preset (vr.graphics; HALO_VR_GRAPHICS_HIGH outside VR): which
effects are drawn (port/linux/game/vr_graphics.c), and with "max" the
models lit for each pixel and larger shadows (d3d8_gl.c) */
enum
{
HALO_VR_GRAPHICS_LOW = 0,
HALO_VR_GRAPHICS_MEDIUM,
HALO_VR_GRAPHICS_HIGH,
HALO_VR_GRAPHICS_MAX,
};
int halo_vr_graphics(void);
/* the VR options: the refresh rates the headset offers (Hz, at most maximum;
0 when it lets none be chosen, as SteamVR), and the one it runs at (0:
unknown; vr.refresh_rate asks for one); and vr.resolution_scale as the app
started (it takes a restart) */
int halo_vr_refresh_rates(float *rates, int maximum, float *current);
float halo_vr_started_resolution_scale(void);
/* whether a seated player's view stays in first person (director.c) */
int halo_vr_vehicle_first_person(void);
/* VR test aid (debug.vehicle_spawn): both sticks held in for a moment asks
@@ -236,7 +331,7 @@ coming (each returns 0 otherwise, and for this machine's own players, and
the game does as before):
- where the weapon aims, on foot;
- where its shots start (world units), from the unit's camera;
- whether the left hand holds the grenade;
- whether the free hand holds the grenade;
- the hand's throw, once, as the grenade goes (as halo_vr_grenade_throw);
and this machine's own VR player's throw, to send (units.c). */
int halo_vr_network_aim(long player_index, float vector[3]);
@@ -262,6 +357,23 @@ enum
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 -83
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 */
+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_*) */
+121 -15
View File
@@ -16,6 +16,7 @@ it exists, so that the player's edits and comments stay.
#include <SDL3/SDL.h>
#include <ctype.h>
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
@@ -96,6 +97,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 +118,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 +146,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"
@@ -148,6 +165,13 @@ static const struct config_setting config_settings[] =
{ "vr.refresh_rate", _config_real, "90.0", "HALO_VR_REFRESH_RATE", _environment_value, _platform_android,
"The display refresh rate to ask the headset for, in Hz (72, 90, 120);\n"
"it picks the nearest it has at or below. 0 for the highest." },
{ "vr.graphics", _config_string, "\"high\"", "HALO_VR_GRAPHICS", _environment_value, _platform_android,
"Which of the costlier effects are drawn: \"high\" all of them; \"medium\"\n"
"no object shadows, environment reflections or lens flares; \"low\" nor\n"
"specular light, bump maps, grass, the screen's fog, contrails, weather\n"
"or camouflage's second pass (a client in another's game keeps grass,\n"
"lens flares and fog); \"max\" all of them, the models lit for each\n"
"pixel and shadows at 512x512." },
{ "vr.positional_tracking", _config_boolean, "true", "HALO_VR_POSITIONAL", _environment_value, _platform_android,
"Moving your head moves the view (leaning, ducking); false only turns it.\n"
"The view recentres when play starts or resumes after a cinematic, and\n"
@@ -164,8 +188,41 @@ static const struct config_setting config_settings[] =
"Log the frame rate and where the frame time goes every five seconds\n"
"(adb logcat -s halo)." },
{ "vr.aim_with", _config_string, "\"controller\"", "HALO_VR_AIM_WITH", _environment_value, _platform_android,
"\"controller\": the weapon is in your right hand and aims where it points,\n"
"while you move and look with your head; \"head\": you aim where you look." },
"\"controller\": the weapon is in your hand (vr.handedness) and aims where\n"
"it points, while you move and look with your head; \"head\": you aim\n"
"where you look." },
{ "vr.handedness", _config_string, "\"right\"", "HALO_VR_HANDEDNESS", _environment_value, _platform_android,
"The hand that holds the weapon: \"right\", or \"left\", which mirrors the\n"
"weapon and the arms, throws grenades and grips the foregrip with the\n"
"right hand, and fires with the left trigger." },
{ "vr.mirror_controls", _config_boolean, "true", "HALO_VR_MIRROR_CONTROLS", _environment_value,
_platform_android,
"Left-handed, the sticks and buttons swap sides too: the right stick moves\n"
"and the left one turns, X jumps and A reloads (on the Steam Frame the\n"
"D-pad takes A, B, X and Y as their mirror image); false keeps them as\n"
"right-handed. The menus keep their buttons either way." },
{ "vr.button_jump", _config_string, "\"a\"", "HALO_VR_BUTTON_JUMP", _environment_value, _platform_android,
"The button each of the game's actions is on (the VR options' BUTTONS\n"
"page swaps them): \"a\", \"b\", \"x\", \"y\" (A and B on the right\n"
"controller, X and Y the Quest's on the left; the Steam Frame has all\n"
"four on the right), \"move_stick\" and \"turn_stick\" (their clicks),\n"
"\"grip\" (the weapon hand's), named as right-handed: left-handed with\n"
"mirror_controls, their mirror images. Jump," },
{ "vr.button_light", _config_string, "\"b\"", "HALO_VR_BUTTON_LIGHT", _environment_value, _platform_android,
"the flashlight (held: switch grenades)," },
{ "vr.button_action", _config_string, "\"x\"", "HALO_VR_BUTTON_ACTION", _environment_value, _platform_android,
"action and reload," },
{ "vr.button_weapon", _config_string, "\"y\"", "HALO_VR_BUTTON_WEAPON", _environment_value, _platform_android,
"switch weapon," },
{ "vr.button_crouch", _config_string, "\"move_stick\"", "HALO_VR_BUTTON_CROUCH", _environment_value,
_platform_android,
"crouch," },
{ "vr.button_zoom", _config_string, "\"turn_stick\"", "HALO_VR_BUTTON_ZOOM", _environment_value,
_platform_android,
"zoom" },
{ "vr.button_melee", _config_string, "\"grip\"", "HALO_VR_BUTTON_MELEE", _environment_value, _platform_android,
"and melee. The triggers, the free hand's grip and the menu button keep\n"
"theirs, as do the menus." },
{ "vr.laser", _config_boolean, "false", "HALO_VR_LASER", _environment_value, _platform_android,
"A laser from the weapon to what it aims at (with aim_with = \"controller\")." },
{ "vr.crosshair", _config_boolean, "true", "HALO_VR_CROSSHAIR", _environment_value, _platform_android,
@@ -180,21 +237,33 @@ static const struct config_setting config_settings[] =
{ "vr.weapon_down", _config_real, "0.14", "HALO_VR_WEAPON_DOWN", _environment_value, _platform_android,
"See weapon_forward." },
{ "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"
"How near, in metres, the free 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." },
"melee attack (the weapon hand's grip is too); 0 turns it off." },
{ "vr.grenade_throw_speed", _config_real, "1.5", "HALO_VR_GRENADE_THROW", _environment_value, _platform_android,
"A grenade is held while the left trigger is, and thrown as it lets go:\n"
"along the hand's throw, when the hand moved faster than this (metres per\n"
"second; full strength at 4), else as the game throws it. 0 turns the\n"
"hand's throw off." },
"A grenade is held while the free hand's trigger is, and thrown as it\n"
"lets go: along the hand's throw, when the hand moved faster than this\n"
"(metres per second; full strength at 4), else as the game throws it. 0\n"
"turns the hand's throw off." },
{ "vr.grenade_scale", _config_real, "0.55", "HALO_VR_GRENADE_SCALE", _environment_value, _platform_android,
"The size the grenade held in the left hand is drawn at (1 as in the game)." },
"The size the grenade held in the free hand is drawn at (1 as in the game)." },
{ "vr.grenade_right", _config_real, "0.055", "HALO_VR_GRENADE_RIGHT", _environment_value, _platform_android,
"Where the held grenade is drawn, in metres from the left controller's\n"
"grip: towards the palm (right),", },
"Where the held grenade is drawn, in metres from the free controller's\n"
"grip: towards the palm (the left hand's right),", },
{ "vr.grenade_forward", _config_real, "0.01", "HALO_VR_GRENADE_FORWARD", _environment_value, _platform_android,
"the grip's forward (through the fist, towards the index finger),", },
{ "vr.grenade_down", _config_real, "0.01", "HALO_VR_GRENADE_DOWN", _environment_value, _platform_android,
@@ -257,6 +326,13 @@ static const struct config_setting config_settings[] =
{ "vr.comfort_vignette", _config_real, "0.7", "HALO_VR_COMFORT_VIGNETTE", _environment_value, _platform_android,
"How far the view darkens from its edges while a vehicle turns you\n"
"(0 never, 1 to a small window)." },
{ "vr.foot_vignette", _config_string, "\"off\"", "HALO_VR_FOOT_VIGNETTE", _environment_value, _platform_android,
"On foot, the view darkens from its edges too: \"turning\" while the stick\n"
"turns you smoothly (smooth_turn), \"moving\" while a stick turns or moves\n"
"you, as far as it is pushed; \"off\" never." },
{ "vr.foot_vignette_strength", _config_real, "0.7", "HALO_VR_FOOT_VIGNETTE_STRENGTH", _environment_value,
_platform_android,
"How far it darkens then (as comfort_vignette)." },
{ "vr.motion_tracker_on_wrist", _config_boolean, "true", "HALO_VR_MOTION_TRACKER_WRIST", _environment_value,
_platform_android,
"Show the motion tracker on the back of the left wrist, like a watch (turn\n"
@@ -277,9 +353,11 @@ static const struct config_setting config_settings[] =
{ "vr.smooth_turn", _config_boolean, "false", "HALO_VR_SMOOTH_TURN", _environment_value, _platform_android,
"The right stick turns smoothly; false turns in steps (snap_turn_degrees)." },
{ "vr.snap_turn_degrees", _config_real, "30.0", "HALO_VR_SNAP_TURN", _environment_value, _platform_android,
"How far a flick of the right stick turns." },
"How far a flick of the right stick turns, in degrees (the VR options step\n"
"through 10 to 45 by 5, 60, 75 and 90)." },
{ "vr.smooth_turn_speed", _config_real, "120.0", "HALO_VR_SMOOTH_TURN_SPEED", _environment_value, _platform_android,
"Degrees per second the right stick turns at, fully pushed, with smooth_turn." },
"Degrees per second the right stick turns at, fully pushed, with smooth_turn\n"
"(the VR options step through 30 to 150 by 15, then to 300 by 30)." },
{ "vr.cinematics_in_3d", _config_boolean, "false", "HALO_VR_CINEMATICS_3D", _environment_value, _platform_android,
"Show cinematics around you, from the cinematic camera; false shows them\n"
"on the screen in front of you (easier on the stomach)." },
@@ -301,6 +379,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 +507,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"
@@ -516,6 +604,13 @@ static const struct config_setting config_settings[] =
"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.vr_options", _config_boolean, "false", "HALO_DEBUG_VR_OPTIONS", _environment_value, _platform_android,
"Off the headset (the emulator), show the VR options panel on the main\n"
"and pause menus all the same, for checking its pages." },
{ "debug.mirror_first_person", _config_boolean, "false", "HALO_MIRROR_FIRST_PERSON", _environment_value,
_platform_android,
"Off the headset (flat), the first-person weapon drawn mirrored, as the\n"
"left hand's is in VR: a test aid for what reads mirrored there. Read once." },
{ "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"
@@ -657,13 +752,24 @@ static char *config_read_file(const char *path, size_t *size)
static int config_write_file(const char *path, const char *text)
{
#ifdef HALO_ANDROID
FILE *file = fopen(path, "wb");
/* written beside it and renamed over it: a file the app may not write
(one pushed by adb belongs to the shell) is replaced all the same, as the
folder is the app's, and a write cut short leaves the old file whole */
char temporary[1100];
FILE *file;
int written;
snprintf(temporary, sizeof(temporary), "%s.tmp", path);
file = fopen(temporary, "wb");
if (!file)
return 0;
written = fwrite(text, 1, strlen(text), file) == strlen(text);
return fclose(file) == 0 && written;
written = fclose(file) == 0 && written;
if (written && rename(temporary, path) == 0)
return 1;
platform_log("settings: cannot replace %s (%s)", path, strerror(errno));
remove(temporary);
return 0;
#else
return SDL_SaveFile(path, text, strlen(text));
#endif
+748 -90
View File
File diff suppressed because it is too large. Load diff
+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;
}
+22 -7
View File
@@ -851,6 +851,19 @@ Holding a Warthog's wheel (vr.c), the right trigger is the throttle and the
left one the brake (backwards, once stopped): the left stick pushed up or
down as far, unless it is pushed further.
Left-handed (vr.handedness), vr.c hands the controllers over with the sides
swapped where the hands' jobs are (halo_vr_controllers): the left trigger
fires and the left grip strikes, the right ones hold and throw the grenade
and grip the foregrip; and with vr.mirror_controls the sticks, bumpers and
face buttons too (the right stick moves, X jumps, A reloads; on the Steam
Frame the left D-pad takes A, B, X and Y). Here "right" and "left" are the
sides so handed over.
The VR options' BUTTONS page (vr.button_*) moves jump, the flashlight,
action, switch weapon, crouch, zoom and melee between A, B, X, Y, the
stick clicks and the weapon hand's grip: vr.c hands each action's button
over as the one above, so "A" here is the jump wherever it is.
In the menus the buttons are the Xbox's by position: A accepts, B goes back. */
#include "guest_host.h"
@@ -929,9 +942,8 @@ static void vr_gamepad_state(XINPUT_GAMEPAD *pad)
}
merge_button(pad, XINPUT_GAMEPAD_A, (right->buttons & HALO_XR_BUTTON_A) != 0);
merge_button(pad, XINPUT_GAMEPAD_X, (left->buttons & HALO_XR_BUTTON_A) != 0);
/* (the grips holding a Warthog's wheel are not a blow) */
merge_button(pad, XINPUT_GAMEPAD_B,
((right->buttons & HALO_XR_BUTTON_SQUEEZE) != 0 && !halo_vr_wheel_held()) || halo_vr_melee());
/* (the grips holding a Warthog's wheel are not a blow: vr.c) */
merge_button(pad, XINPUT_GAMEPAD_B, (right->buttons & HALO_XR_BUTTON_SQUEEZE) != 0 || halo_vr_melee());
merge_button(pad, XINPUT_GAMEPAD_Y, (left->buttons & HALO_XR_BUTTON_B) != 0);
/* (the Steam Frame's bumpers: the left switches grenades, the right is
the flashlight) */
@@ -1038,12 +1050,15 @@ DWORD WINAPI XInputSetState(HANDLE device, PXINPUT_FEEDBACK feedback)
if (port < 0)
return ERROR_DEVICE_NOT_CONNECTED;
#ifdef HALO_ANDROID
/* VR: the heavy (left) motor in the left hand, the light one in the
right, as the game refreshes them every frame */
/* VR: the heavy (left) motor in the free hand, the light one in the
weapon's (left-handed, the other way round), as the game refreshes them
every frame */
if (port == 0 && halo_vr_active())
{
host_xr_haptic(HALO_XR_HAND_LEFT, feedback->Rumble.wLeftMotorSpeed / 65535.0f, 0.1f);
host_xr_haptic(HALO_XR_HAND_RIGHT, feedback->Rumble.wRightMotorSpeed / 65535.0f, 0.1f);
int weapon = halo_vr_weapon_hand();
host_xr_haptic(HALO_XR_HAND_COUNT - 1 - weapon, feedback->Rumble.wLeftMotorSpeed / 65535.0f, 0.1f);
host_xr_haptic(weapon, feedback->Rumble.wRightMotorSpeed / 65535.0f, 0.1f);
}
#endif
count = sdl_gamepads(gamepads);
+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
Loaded 100 of 158 files, more files were not shown because too many files have changed in this diff. Show more