Compare commits

..
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 a950de4b2b Release-ready: README for players, third-party notices, issue template
- README.md: what the mod is (a VR mod for Halo: Combat Evolved), what is
  needed (a disc image of one's own, purchased copy), simple installation
  for the Quest 3 / 3S (SideQuest) and the Steam Frame (Desktop Mode, the
  installer), playing, reporting problems, network use, credits (OpenCE,
  the decompilation, HaloCE-Quest-VR, HaloCE-VR, extract-xiso) and the
  legal notes (unofficial, not affiliated with Microsoft and others).
- THIRD_PARTY_NOTICES.md: the libraries and fonts and their licenses.
- Releases: the APK itself (RingfallMjolnirVR.apk) for sideloading, notes
  that say which file is for which headset, the project's name as title.
- An issue template (headset, build, log; no game files).
- docs/OPENCE_README.md marked as the base port's; the disc images'
  wording everywhere: made from one's own copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:05:29 +02:00
Stefan MaierandClaude Opus 5.5 1984996d04 VR roadmap: comfort, buttons, import, installer and the new name done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:55:46 +02:00
Stefan MaierandClaude Opus 5.5 93b96cf5b8 Merge feature/rename-ringfall: Ringfall: Mjolnir VR, comfort and buttons, Frame installer, release signing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:55:25 +02:00
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
53 changed files with 3561 additions and 471 deletions

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
name: Problem report
description: Something does not work as it should
body:
- type: markdown
attributes:
value: |
Thanks for reporting! Please do **not** ask for, link to or attach game files or disc images; such issues are closed and deleted.
- type: dropdown
id: headset
attributes:
label: Headset
options:
- Meta Quest 3
- Meta Quest 3S
- Steam Frame
- Other
validations:
required: true
- type: input
id: build
attributes:
label: Build number
description: The release you installed, e.g. "build 180".
validations:
required: true
- type: textarea
id: what
attributes:
label: What happened
description: What you did, what you expected and what happened instead.
validations:
required: true
- type: textarea
id: log
attributes:
label: Log
description: |
Attach `debug.txt`. Quest: `Android/data/io.github.cesarus85.ringfall/files/debug.txt` (SideQuest's file manager). Steam Frame: `Documents/RingfallMjolnirVR/debug.txt`, and `logcat.txt` and `install.log` beside it.
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: How to install
url: https://github.com/Cesarus85/ringfall-mjolnir-vr#install-on-meta-quest-3--3s
about: Step-by-step installation for the Quest 3 / 3S and the Steam Frame.
+31 -8
View File
@@ -237,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-*
@@ -245,10 +252,18 @@ 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"
# the APK itself, for sideloading on the Quest (SideQuest, adb)
cp artifacts/halo-android-release/app-release.apk release/RingfallMjolnirVR.apk
ls -l release
- name: Publish
@@ -260,10 +275,18 @@ jobs:
run: |
tag="build-$GITHUB_RUN_NUMBER"
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 \
--title "Build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
readme="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
{
printf '%s\n\n' "$subject"
printf '**Ringfall: Mjolnir VR** is an unofficial VR mod for Halo: Combat Evolved. It needs a disc image of your own, purchased Xbox copy of the game; no game files are included.\n\n'
printf '| Headset | Download | How to install |\n| --- | --- | --- |\n'
printf '| Meta Quest 3 / 3S | `RingfallMjolnirVR.apk` | [Install on Quest](%s#install-on-meta-quest-3--3s) |\n' "$readme"
printf '| Steam Frame | `Install-Ringfall-Mjolnir-VR.desktop` (open it in Desktop Mode) | [Install on Steam Frame](%s#install-on-steam-frame) |\n\n' "$readme"
printf 'The other files are for the built-in updater and the Steam Frame installer.\n\n'
printf 'Commit: %s\nBuild: %s\n' "$COMMIT_URL" "$RUN_URL"
} > notes.md
gh release create "$tag" release/* --target "$GITHUB_SHA" --latest \
--title "Ringfall: Mjolnir VR, build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: Keep the last five
@@ -318,7 +341,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
+145 -104
View File
@@ -1,140 +1,181 @@
# 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`).
**A VR mod for Halo: Combat Evolved** (the original Xbox game) on the
**Meta Quest 3 / 3S** and the **Steam Frame**. Play the campaign and
multiplayer in full 6DoF virtual reality, with your hands in the game.
<img width="1289" height="995" alt="The game on Linux" src="https://github.com/user-attachments/assets/0d3ad50f-f8b8-46cf-aef8-e3661da2a7d7" />
Ringfall: Mjolnir VR runs on your headset by itself, without a PC. It
brings no game files: it uses a disc image of **your own, purchased copy**
of the game.
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).
> **Unofficial fan project.** Ringfall: Mjolnir VR is a free,
> non-commercial fan project. It is not affiliated with, endorsed,
> sponsored or approved by Microsoft, Xbox Game Studios, Halo Studios or
> Bungie. See [Legal](#legal).
## Download
## Contents
GitHub Actions builds the game for each commit. These links download the
builds of the latest release:
- [What you get](#what-you-get)
- [What you need](#what-you-need)
- [Install on Meta Quest 3 / 3S](#install-on-meta-quest-3--3s)
- [Install on Steam Frame](#install-on-steam-frame)
- [Playing](#playing)
- [Problems and questions](#problems-and-questions)
- [Credits](#credits)
- [Legal](#legal)
| 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) |
## What you get
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.
- **Full VR**: stereo 6DoF at your headset's refresh rate (72 to 120 Hz);
the HUD, menus, scope and crosshair float as layers of their own.
- **Your hands in the game**: the weapon sits in your hand and fires
where you point it; arms and fingers follow your controllers;
two-handed weapons, melee by swinging, grenades thrown with your hand,
crouching and leaning with your body.
- **Vehicles**: steer the Warthog by turning its wheel with both hands;
turrets with handles.
- **Comfort**: snap or smooth turning in fine steps, comfort vignettes,
left-handed mode, buttons remapped freely, graphics presets, refresh
rate and resolution, all in the in-game VR options (**Y**).
- **Multiplayer**: the PC version's menus and server browser, join codes,
password lobbies, co-op campaign and bots, up to 128 players;
cross-play with the PC and phone builds of
[OpenCE](https://github.com/OpenCommunityEdition/OpenCE).
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).
## What you need
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.
- A **Meta Quest 3 / 3S** or a **Steam Frame**.
- A **disc image** (`.iso` or `.xiso`) of **Halo: Combat Evolved for the
original Xbox**, made from **a disc you own**. Any region and version
works.
## Game data
This project does not include, link to or help find game files. Please
do not ask for or share disc images in issues or discussions; such posts
are deleted.
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.
## Install on Meta Quest 3 / 3S
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.
You install the game once with a computer (Windows, macOS or Linux); after
that it updates itself.
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).
1. **Turn on Developer Mode** for your Quest, once: in the Meta Horizon
app on your phone, open your headset's settings, then *Developer
Mode* (Meta asks you to create a free developer account first).
2. **Install [SideQuest](https://sidequestvr.com/setup-howto)** (the
"Advanced Installer") on your computer and connect the Quest with a
USB cable. In the headset, allow USB debugging for this computer.
3. **Download `RingfallMjolnirVR.apk`** from the
[latest release](../../releases/latest) and drag it onto the SideQuest
window. It installs the game.
4. **Copy your disc image** to the Quest's `Download` folder: in
SideQuest, open the file manager (the folder icon), go to `Download`
and drag the `.iso` file in.
5. **In the headset**, open your *Library*, choose *Unknown Sources* and
start **Ringfall: Mjolnir VR**. Select **Choose disc image** and pick
your file in `Download`. The game copies what it needs (about 1.8 GB,
a minute or two) and starts.
## Platforms
You can delete the disc image from `Download` afterwards. When a new
version comes out, the game asks you at start whether to update.
Each platform has its own instructions:
## Install on Steam Frame
| 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) |
No computer and no keyboard needed.
The Linux README also gives the controls, the settings and the multiplayer
functions. These are almost the same on all platforms.
1. **Switch to Desktop Mode**: press the Steam button, then *Power* >
*Switch to Desktop*.
2. **Put your disc image** in the `Downloads` folder (for example from a
USB stick or memory card with the file manager, Dolphin).
3. **Download `Install-Ringfall-Mjolnir-VR.desktop`** from the
[latest release](../../releases/latest) with the web browser.
4. **Open it**: in Dolphin, open `Downloads` and double-click the file.
If you are asked whether to run it, choose to continue.
5. **Answer the dialogs.** The installer finds your disc image, copies
what the game needs, adds **Ringfall: Mjolnir VR** to your Steam
library and offers to switch back to Gaming Mode.
6. Start **Ringfall: Mjolnir VR** from your library (under *Non-Steam*).
## Multiplayer
To update, open the same file in Desktop Mode again; your settings,
saves and game data stay. More about the Steam Frame:
[port/android/STEAM_FRAME.md](port/android/STEAM_FRAME.md).
The game can play system link games on a local network and on the internet:
## Playing
- 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).
- **Y** opens the VR options in the main and pause menus: turning,
comfort, weapon hand, buttons, graphics, refresh rate and resolution.
*Quit the game* is on its last page.
- In the menus, point with a controller and pull the trigger to click.
- The controller layout is on the VR options' *Controller buttons* page.
## Build the game
## Problems and questions
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).
Please open an [issue](../../issues) and include:
To build the game:
- your headset and the build number of the release you installed;
- what you did and what happened;
- the game's log, `debug.txt`:
- Quest: `Android/data/io.github.cesarus85.ringfall/files/debug.txt`
(SideQuest's file manager);
- Steam Frame: `Documents/RingfallMjolnirVR/debug.txt` (and
`logcat.txt` and `install.log` beside it).
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:
What is done and what comes next: [port/android/VR_ROADMAP.md](port/android/VR_ROADMAP.md).
| 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` |
**Network use.** The game connects to the internet only for what you
use: the update check (GitHub), online games (to other players
directly), and join codes and lobby lists through public MQTT brokers
(listed in [brokers.txt](port/assets/network/brokers.txt)). There is no
telemetry.
If you enter `ninja` without a target, ninja builds the game for the
computer that you use.
## Credits
`tools/ci_build.py` makes the same builds as GitHub Actions. For example,
enter `python tools/ci_build.py linux release`.
Ringfall: Mjolnir VR stands on the work of others:
### Build options
- **[OpenCE](https://github.com/OpenCommunityEdition/OpenCE)** (Open
Community Edition): the port of the decompiled game to Linux, Windows
and Android, with its renderer, netcode, PC menus and multiplayer, that
this project builds on and follows.
- **The decompilation**: [bnunu/halo-1](https://github.com/bnunu/halo-1),
a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
- **[HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR)**
by moistman42069 (CC0): the arm IK, the forearm's twist and the
fingers' poses are taken from it (`port/linux/game/vr_arms.c`); the
left-handed mode and the graphics presets follow its ideas.
- **[HaloCE-VR](https://github.com/astromaddie/HaloCE-VR)** by astromaddie
(CC0): the Steam Frame work, whose findings (such as quieting Mesa's
trace markers) are used here.
- **[extract-xiso](https://github.com/XboxDev/extract-xiso)**: the disc
image readers follow its format handling.
- The libraries and fonts in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
Give these options to `configure.py`:
Thank you all.
| 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". |
## Building it
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.
`python3 configure.py --release`, then `ninja android_apk` (Android SDK
and NDK needed). See [port/android/README.md](port/android/README.md) for
the details and [docs/OPENCE_README.md](docs/OPENCE_README.md) for the
port it is built on.
### Optimization profiles
## Legal
The builds use profiles of the game to optimize the code:
Ringfall: Mjolnir VR is an unofficial, free, non-commercial fan project.
It is not affiliated with, endorsed, sponsored or approved by Microsoft
Corporation, Xbox Game Studios, Halo Studios or Bungie. Halo, Halo: Combat
Evolved and Xbox are trademarks of Microsoft Corporation; they are named
here only to say which game this mod works with. Meta Quest is a trademark
of Meta Platforms, Inc.; Steam and Steam Frame are trademarks of Valve
Corporation.
- `pgo/halo_linux.profdata` for Linux and Android.
- `pgo/halo_windows.profdata` for Windows.
No game data, art, music or other content of the game is included or
distributed. You need your own, legally purchased copy of the game. If
you enjoy this mod, please support the creators of the original by buying
their games.
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/`.
This project's own code is released under [CC0 1.0](LICENSE.md), as the
decompilation it builds on. Third-party code and fonts keep their own
licenses: see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
+46
View File
@@ -0,0 +1,46 @@
# Third-party notices
Ringfall: Mjolnir VR's own code is released under [CC0 1.0](LICENSE.md).
It includes or builds on the following, under their own licenses; each
license's full text is in the file named.
## Code
| Component | Used for | License | Text |
| --- | --- | --- | --- |
| [OpenCE](https://github.com/OpenCommunityEdition/OpenCE) and the decompilation ([bnunu/halo-1](https://github.com/bnunu/halo-1), [punpckhdq/halo](https://github.com/punpckhdq/halo)) | the game's code, the port | CC0 1.0 | [LICENSE.md](LICENSE.md) |
| [HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR) | the arm IK, forearm twist and finger poses (`port/linux/game/vr_arms.c`) | CC0 1.0 | (its repository) |
| [HaloCE-VR](https://github.com/astromaddie/HaloCE-VR) | Steam Frame findings | CC0 1.0 | (its repository) |
| [SDL 3](https://libsdl.org) | windows, input, sound | zlib | (in SDL's source, `LICENSE.txt`) |
| [Khronos OpenXR loader](https://github.com/KhronosGroup/OpenXR-SDK) | VR runtime access | Apache 2.0 | (in the loader's package) |
| [musl](https://musl.libc.org) | the C library of the game's code on Android, its maths | MIT | [port/third_party/musl-math](port/third_party/musl-math/README.md) |
| [Expat](https://libexpat.github.io) | the PC menus' definitions | MIT | [port/third_party/expat/COPYING](port/third_party/expat/COPYING) |
| [Mbed TLS](https://www.trustedfirmware.org/projects/mbed-tls/) | HTTPS for the desktop builds' updater (not in the Android app) | Apache 2.0 | [port/third_party/mbedtls/LICENSE](port/third_party/mbedtls/LICENSE) |
| [miniupnpc](https://miniupnp.tuxfamily.org) | router port forwarding for online games | BSD 3-Clause | [port/third_party/miniupnpc/LICENSE](port/third_party/miniupnpc/LICENSE) |
| [KCP](https://github.com/skywind3000/kcp) | reliable transport for online games | MIT | [port/third_party/kcp/LICENSE](port/third_party/kcp/LICENSE) |
| [Monocypher](https://monocypher.org) | signing public games' listings (Ed25519); online play's other cryptography is the port's own | CC0 1.0 or BSD 2-Clause | [port/third_party/monocypher/LICENCE.md](port/third_party/monocypher/LICENCE.md) |
| [tomlc17](https://github.com/cktan/tomlc17) | reading `config.toml` | MIT | [port/third_party/tomlc17/LICENSE](port/third_party/tomlc17/LICENSE) |
| [stb](https://github.com/nothings/stb) | drawing the game's text sharply (stb_truetype) | MIT or public domain | [port/third_party/stb/LICENSE](port/third_party/stb/LICENSE) |
| [SMAA](https://www.iryoku.com/smaa/) | anti-aliasing (flat screens; off in VR) | MIT | [port/third_party/smaa/LICENSE](port/third_party/smaa/LICENSE) |
| [extract-xiso](https://github.com/XboxDev/extract-xiso) | reading disc images (followed by `XisoExtractor.java`, `steam_frame_xiso.py`, `xiso.c`) | BSD-style (in@fishtank.com) | [port/third_party/extract-xiso/LICENSE.TXT](port/third_party/extract-xiso/LICENSE.TXT) |
The disc image readers include this acknowledgement, as extract-xiso's
license asks: *This product includes software developed by in
&lt;in@fishtank.com&gt;.*
## Fonts
| Font | Used for | License | Text |
| --- | --- | --- | --- |
| Overpass (Red Hat, Delve Withrington) | the game's text, the library pictures | SIL Open Font License 1.1 | [port/assets/fonts/Overpass-OFL.txt](port/assets/fonts/Overpass-OFL.txt) |
| OpenCE (after Newtown) | the menus' titles | SIL Open Font License 1.1 | [port/assets/fonts/OpenCE-OFL.txt](port/assets/fonts/OpenCE-OFL.txt) |
| Newtown (Roger White) | OpenCE's source | public domain | [port/assets/fonts/Newtown-LICENSE.txt](port/assets/fonts/Newtown-LICENSE.txt) |
See [port/assets/fonts/README.md](port/assets/fonts/README.md) for where
each font is used.
## Not included
No file of Halo: Combat Evolved (maps, sounds, art, music, fonts or
executables) is part of this repository or its releases. The game reads
them from the player's own disc image.
+145
View File
@@ -0,0 +1,145 @@
> This is the README of [OpenCE](https://github.com/OpenCommunityEdition/OpenCE),
> the port Ringfall: Mjolnir VR is built on, kept for its build and
> development notes. Its download links are OpenCE's. For the VR mod, see
> the [main README](../README.md).
# 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. It needs an Xbox disc image
(`.xiso` or `.iso`) made from your own copy 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/`.
+21 -11
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`.
@@ -41,8 +43,8 @@ ninja) and these items:
## Game data
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`.
`.iso`) made from your own copy of the game, any version. The app extracts `maps/` from the disc
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
+182 -8
View File
@@ -1,6 +1,6 @@
# VR roadmap
What the VR port (Quest 3, Steam Frame) has, and what comes next, in the
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.
@@ -16,7 +16,7 @@ named where code is taken over.
| Area | What |
| --- | --- |
| Headsets | Quest 3; Steam Frame (Lepton): its controllers' layout, a Steam library tile that installs, starts, updates and quits the game, data kept in `~/Documents/HaloCE` |
| 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 |
@@ -25,6 +25,8 @@ named where code is taken over.
| 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
@@ -33,11 +35,182 @@ named where code is taken over.
| --- | --- | --- |
| 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 | |
| 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 **done** (in `main` 2026-10-06; Quest 3 good): 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) | |
| 4 | Later: disc image import in the launcher, sprint by arm swing (offline) | import **done** (in `main` 2026-10-06): the app's import (tried on the Quest 3: all 24 maps; on the Frame kept in `~/Documents/RingfallMjolnirVR`), a Steam Frame installer from Desktop Mode (`STEAM_FRAME.md`; tried on the Frame: the old folder renamed, the shortcut added by Steam), a release signing key (CI and local builds the same). Sprint by arm swing still open |
| 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 | **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good) |
## 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 (done)
In `main` 2026-10-06; tried on the Quest 3 (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)
@@ -97,10 +270,11 @@ the left hand animated through reloads.
- Multiview on the Steam Frame: why the eyes do not fuse there (suspected
Mesa zink/Turnip); tools in branch `frame/doubled-images` (86335828),
not merged.
- Transparent geometry groups: the Xbox's 384 a window overflow in games
of 30 and more players.
- The VR on-screen keyboard shows a password in the clear while it is
typed.
- 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`)
+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 */
+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");
+58 -25
View File
@@ -120,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,
@@ -946,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,
@@ -1048,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
@@ -1088,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);
@@ -1227,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;
@@ -1555,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;
@@ -2047,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 };
@@ -2134,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
+8 -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 */
@@ -155,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())
+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).
+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
+84 -19
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,9 +105,18 @@ 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):
@@ -114,16 +124,16 @@ int halo_vr_ready_laser(short local_player_index, float start[3], float directio
and the torso's flat heading (which follows the head beyond a few
degrees, and the hands; the stick's turns turn it at once), from which
the shoulders hang;
- the left hand's pose;
- the left hand on the weapon: given where the model's left hand holds it
- 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 neck[3], float forward[3]);
int halo_vr_left_hand(short local_player_index, float position[3], float forward[3], float up[3]);
int halo_vr_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 (0 the left, 1 the right), from the controller's trigger,
grip and touch: the thumb, index, middle finger and the last two, each
/* - 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
@@ -156,13 +166,14 @@ int halo_vr_melee(void);
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
@@ -193,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 */
@@ -211,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
{
@@ -237,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
@@ -266,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]);
+27 -4
View File
@@ -281,6 +281,10 @@ static long shadow_scale_choose(void)
long resolution = config_integer("display.shadow_resolution");
long scale = 1;
/* (VR's "max" graphics: 512 at least) */
if (halo_vr_graphics() == HALO_VR_GRAPHICS_MAX && resolution < 512)
resolution = 512;
while (scale < SHADOW_MAP_MAXIMUM_SCALE && SHADOW_MAP_SIZE * scale * 2 <= resolution)
scale *= 2;
return scale;
@@ -3170,6 +3174,22 @@ static GLenum blend_equation(DWORD operation)
}
}
/* the model being drawn mirrors: its node matrices are left-handed (the
left hand's first-person weapon in VR, first_person_weapons.c), and its
triangles' winding is turned over (set by rasterizer_xbox.c as it sets them,
after HaloCE-Quest-VR, CC0) */
static int mirrored_skinning;
void d3d8_set_mirrored_skinning(int mirrored)
{
mirrored_skinning = mirrored != 0;
}
int d3d8_mirrored_skinning(void)
{
return mirrored_skinning;
}
static void apply_raster_state(BOOL has_depth)
{
DWORD *rs = D3D__RenderState;
@@ -3305,6 +3325,8 @@ static void apply_raster_state(BOOL has_depth)
#endif
GLenum cull_mode = rs[D3DRS_CULLMODE] == rs[D3DRS_FRONTFACE] ? GL_FRONT : GL_BACK;
if (mirrored_skinning)
front_face = front_face == GL_CW ? GL_CCW : GL_CW;
if (gl_state.front_face != front_face)
{
gl_state.front_face = front_face;
@@ -3416,9 +3438,10 @@ static void draw_flush(void)
#endif
}
/* display.per_pixel_lighting: the model lighting programs' draws are lit for
each pixel (nv2a_psh.c model_lighting), from shaders of their own; read
again when a setting changes */
/* display.per_pixel_lighting (or VR's "max" graphics, vr.graphics): the
model lighting programs' draws are lit for each pixel (nv2a_psh.c
model_lighting), from shaders of their own; read again when a setting
changes */
static BOOL per_pixel_lighting(void)
{
static unsigned long read_at = (unsigned long)-1;
@@ -3427,7 +3450,7 @@ static BOOL per_pixel_lighting(void)
if (read_at != config_changes())
{
read_at = config_changes();
enabled = config_boolean("display.per_pixel_lighting") != 0;
enabled = config_boolean("display.per_pixel_lighting") != 0 || halo_vr_graphics() == HALO_VR_GRAPHICS_MAX;
}
return enabled;
}
+83 -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>
@@ -164,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"
@@ -180,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,
@@ -196,7 +237,7 @@ 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"
@@ -212,17 +253,17 @@ static const struct config_setting config_settings[] =
"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,
@@ -285,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"
@@ -305,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)." },
@@ -554,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"
@@ -695,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
+514 -76
View File
@@ -10,12 +10,35 @@ Frames of reference. The headset reports poses in its LOCAL space: metres,
+x right, +y up, -z forward, the origin and heading where the user last
recentred. The game's player stands in that space's place: its origin is
the game's camera position, and its forward is the "body" yaw kept here.
The right stick turns the body; the head turns on top of it, so the facing
the game uses (halo_vr_aim) is the body's yaw plus the head's.
The right stick (left-handed, mirrored: the left) turns the body; the head
turns on top of it, so the facing the game uses (halo_vr_aim) is the body's
yaw plus the head's.
*/
#include "halo_vr.h"
/* the actions' settings and the buttons' values in them (halo_vr.h's
HALO_VR_ACTION_* and HALO_VR_INPUT_*), by number */
static const char *const vr_action_settings[HALO_VR_ACTION_COUNT] =
{
"vr.button_jump", "vr.button_light", "vr.button_action", "vr.button_weapon", "vr.button_crouch",
"vr.button_zoom", "vr.button_melee",
};
static const char *const vr_input_values[HALO_VR_INPUT_COUNT] =
{
"a", "b", "x", "y", "move_stick", "turn_stick", "grip",
};
const char *halo_vr_action_setting(int action)
{
return action >= 0 && action < HALO_VR_ACTION_COUNT ? vr_action_settings[action] : "";
}
const char *halo_vr_input_value(int input)
{
return input >= 0 && input < HALO_VR_INPUT_COUNT ? vr_input_values[input] : "";
}
#ifdef HALO_ANDROID
#include "platform.h"
@@ -35,6 +58,14 @@ the game uses (halo_vr_aim) is the body's yaw plus the head's.
/* the facing the game changes itself by more than this (a respawn, a
teleport, a vehicle's seat) turns the body with it */
#define FACING_JUMP (10.0f * PI / 180.0f)
/* vr.foot_vignette: none on foot, while the stick turns smoothly, and
while a stick turns smoothly or moves */
enum
{
FOOT_VIGNETTE_OFF = 0,
FOOT_VIGNETTE_TURNING,
FOOT_VIGNETTE_MOVING,
};
static struct
{
@@ -132,9 +163,13 @@ static struct
float vehicle_steer_smoothing; /* seconds */
float steer[2]; /* the steering as smoothed: aside, up (-1..1) */
unsigned long long steer_last_us;
/* the comfort vignette: how strong at most, now, and the heading it
last saw */
/* the comfort vignette: how strong at most in a vehicle, on foot
(vr.foot_vignette: FOOT_VIGNETTE_*) and how strong there, now, and the
heading it last saw */
float comfort_vignette;
int foot_vignette;
float foot_vignette_strength;
int seated; /* in a vehicle's seat, whatever the view */
float vignette;
float vignette_last_yaw;
int vignette_yaw_valid;
@@ -187,13 +222,13 @@ static struct
int laser;
float weapon_offset[3]; /* forward, right, down, metres: the weapon model's camera from the hand */
float muzzle_distance;
/* the left hand on the weapon (halo_vr_two_hand) */
/* the free hand on the weapon (halo_vr_two_hand) */
int two_hand_gripping;
int two_hand_latched; /* the grip is held (on the weapon or not) */
int two_hand_valid;
float two_hand_reach; /* metres from the foregrip a closing grip takes the weapon */
/* the grenade in the left hand (grenade_update, vr.grenade_throw_speed):
the left trigger held, the hand's last positions (LOCAL), and the throw
/* the grenade in the free hand (grenade_update, vr.grenade_throw_speed):
its trigger held, the hand's last positions (LOCAL), and the throw
as it let go */
float grenade_throw_speed;
float grenade_scale;
@@ -219,9 +254,9 @@ static struct
/* until when a melee attack aims where the head looks: its damage is
dealt some ticks into the attack, along the unit's aim */
unsigned long long melee_aim_until_us;
int melee_grip_down;
int melee_button_down;
float melee_strength; /* 0..1, the last attack's swing */
unsigned long long melee_jab_start_us; /* the right grip's attack: the weapon jabs */
unsigned long long melee_jab_start_us; /* the melee button's attack: the weapon jabs */
/* the laser along the barrel, for the frame it was set in */
unsigned long frame_number;
unsigned long laser_frame;
@@ -245,6 +280,17 @@ static struct
int torso_valid;
unsigned long long torso_last_us;
float head_level_yaw;
/* the hands (vr.handedness): the weapon in the left one, and with
vr.mirror_controls the sticks and face buttons mirrored too */
int left_handed;
int mirror_controls;
/* the button each action is on (vr.button_*: HALO_VR_INPUT_* by
HALO_VR_ACTION_*) */
int action_inputs[HALO_VR_ACTION_COUNT];
/* vr.refresh_rate as last asked for (the VR options change it), and
vr.resolution_scale as the app started with it */
float refresh_rate_wanted;
float resolution_scale;
} vr;
/* d3d8_gl.c */
@@ -298,6 +344,24 @@ static int name_pose(const float point[3], struct halo_xr_pose *pose, float *wid
static int weapon_hand_tracked(short local_player_index);
static void controller_axes(float forward[3], float up[3], float right[3]);
static void hand_offset(float out[3]);
static void controls_in_play(struct halo_xr_hand hands[HALO_XR_HAND_COUNT]);
/* the hand that holds the weapon, the free one (the grenade, the
foregrip), and the one whose stick turns the body and steers */
static int weapon_hand(void)
{
return vr.left_handed ? HALO_XR_HAND_LEFT : HALO_XR_HAND_RIGHT;
}
static int free_hand(void)
{
return vr.left_handed ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT;
}
static int turn_hand(void)
{
return vr.left_handed && vr.mirror_controls ? HALO_XR_HAND_LEFT : HALO_XR_HAND_RIGHT;
}
static void quaternion_rotate(const float q[4], const float v[3], float out[3])
{
@@ -468,6 +532,13 @@ static void vr_read_live_settings(void)
vr.laser = config_boolean("vr.laser");
vr.vehicle_wheel = config_boolean("vr.vehicle_wheel");
vr.comfort_vignette = (float)config_real("vr.comfort_vignette");
{
const char *foot = config_string("vr.foot_vignette");
vr.foot_vignette = !strcmp(foot, "moving") ? FOOT_VIGNETTE_MOVING :
!strcmp(foot, "turning") ? FOOT_VIGNETTE_TURNING : FOOT_VIGNETTE_OFF;
}
vr.foot_vignette_strength = (float)config_real("vr.foot_vignette_strength");
vr.fingers = config_boolean("vr.fingers");
vr.arm_ik = config_boolean("vr.arm_ik");
{
@@ -476,6 +547,38 @@ static void vr_read_live_settings(void)
vr.body_mode = !strcmp(body, "hands") ? HALO_VR_BODY_HANDS :
!strcmp(body, "off") ? HALO_VR_BODY_OFF : HALO_VR_BODY_ARMS;
}
{
int left_handed = !strcmp(config_string("vr.handedness"), "left");
if (left_handed != vr.left_handed)
{
/* (the hands change places: what the free hand held lets go) */
vr.two_hand_latched = vr.two_hand_gripping = 0;
vr.grenade_held = vr.grenade_thrown = 0;
vr.grenade_sample_count = 0;
vr.barrel_frame = 0;
platform_log("VR: the weapon in the %s hand", left_handed ? "left" : "right");
}
vr.left_handed = left_handed;
vr.mirror_controls = config_boolean("vr.mirror_controls");
}
{
int action;
for (action = 0; action < HALO_VR_ACTION_COUNT; action++)
vr.action_inputs[action] = halo_vr_action_input(action);
}
{
/* (the refresh rate changes at once where the runtime lets it) */
float refresh_rate = (float)config_real("vr.refresh_rate");
if (vr.active && refresh_rate != vr.refresh_rate_wanted)
{
platform_log("VR: refresh rate %.0f Hz asked for", refresh_rate);
host_xr_set_refresh_rate(refresh_rate);
}
vr.refresh_rate_wanted = refresh_rate;
}
}
void halo_vr_initialize(void)
@@ -498,6 +601,7 @@ void halo_vr_initialize(void)
vr.crouch_limit = (float)config_real("vr.crouch_limit");
vr.rise_limit = 0.25f;
vr.log_performance = config_boolean("vr.log_performance");
vr.resolution_scale = (float)config_real("vr.resolution_scale");
vr.aim_with_controller = strcmp(config_string("vr.aim_with"), "head") != 0;
vr.weapon_offset[0] = (float)config_real("vr.weapon_forward");
vr.weapon_offset[1] = (float)config_real("vr.weapon_right");
@@ -561,6 +665,59 @@ int halo_vr_controller_layout(void)
return vr.active ? vr.frame.controller_layout : HALO_XR_LAYOUT_TOUCH;
}
int halo_vr_weapon_hand(void)
{
return weapon_hand();
}
int halo_vr_action_input(int action)
{
const char *value;
int input;
if (action < 0 || action >= HALO_VR_ACTION_COUNT)
return HALO_VR_INPUT_A;
value = config_string(vr_action_settings[action]);
for (input = 0; input < HALO_VR_INPUT_COUNT; input++)
{
if (!strcmp(value, vr_input_values[input]))
return input;
}
return action;
}
int halo_vr_refresh_rates(float *rates, int maximum, float *current)
{
*current = 0.0f;
return vr.active ? host_xr_refresh_rates(rates, maximum, current) : 0;
}
float halo_vr_started_resolution_scale(void)
{
return vr.resolution_scale;
}
int halo_vr_graphics(void)
{
/* (read as it changes: d3d8_gl.c may ask before vr.c reads the live
settings again) */
static unsigned long read_at = (unsigned long)-1;
static int graphics = HALO_VR_GRAPHICS_HIGH;
if (!vr.active)
return HALO_VR_GRAPHICS_HIGH;
if (read_at != config_changes())
{
const char *name = config_string("vr.graphics");
read_at = config_changes();
graphics = !strcmp(name, "low") ? HALO_VR_GRAPHICS_LOW :
!strcmp(name, "medium") ? HALO_VR_GRAPHICS_MEDIUM :
!strcmp(name, "max") ? HALO_VR_GRAPHICS_MAX : HALO_VR_GRAPHICS_HIGH;
}
return graphics;
}
int halo_vr_eye_size(int *width, int *height)
{
const char *eye_width = getenv("HALO_VR_EYE_WIDTH");
@@ -650,6 +807,14 @@ static void performance_report(int stereo)
vr.report_start_us = now;
if (now - vr.report_start_us < 5000000)
return;
{
/* (the rate the headset runs at now: the VR options change it) */
float rates[1], current = 0.0f;
host_xr_refresh_rates(rates, 0, &current);
if (current > 1.0f)
vr.info.refresh_rate = current;
}
{
float frames = (float)vr.report_frames;
float seconds = (float)(now - vr.report_start_us) * 1e-6f;
@@ -714,15 +879,16 @@ static void look_update(void)
vr.look_valid = 1;
}
/* The grenade in the left hand: while the left trigger is held the game
keeps it in the hand (halo_vr_grenade_held); as the trigger lets go, the
fastest the hand moved in its last moments is the throw (halo_vr_grenade_throw). */
/* The grenade in the free hand (the left one, or left-handed the right):
while its trigger is held the game keeps it in the hand
(halo_vr_grenade_held); as the trigger lets go, the fastest the hand moved
in its last moments is the throw (halo_vr_grenade_throw). */
static void grenade_update(void)
{
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_LEFT];
const struct halo_xr_hand *hand = &vr.frame.hands[free_hand()];
unsigned long long now = now_us();
int valid = hand->active && (hand->flags & HALO_XR_POSE_VALID);
/* (at a wheel the left trigger brakes: xinput_sdl.c) */
/* (at a wheel the free hand's trigger brakes: xinput_sdl.c) */
int held = valid && vr.last_overlay_mode != HALO_XR_OVERLAY_SCREEN && !vr.wheel_hands &&
hand->trigger > (vr.grenade_held ? 0.2f : 0.3f);
int count = vr.grenade_sample_count;
@@ -796,7 +962,7 @@ static void grenade_update(void)
vr.grenade_strength = 0.25f;
if (vr.grenade_strength > 1.0f)
vr.grenade_strength = 1.0f;
host_xr_haptic(HALO_XR_HAND_LEFT, 0.6f, 0.06f);
host_xr_haptic(free_hand(), 0.6f, 0.06f);
platform_log("VR: grenade thrown at %.1f m/s", best_speed);
}
vr.grenade_sample_count = 0;
@@ -814,8 +980,8 @@ int halo_vr_grenade_in_hand(short local_player_index, float position[3], float f
{
static const float local_forward[3] = { 0.0f, 0.0f, -1.0f };
static const float local_up[3] = { 0.0f, 1.0f, 0.0f };
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_LEFT];
float held[3], offset[3], direction[3];
const struct halo_xr_hand *hand = &vr.frame.hands[free_hand()];
float held[3], offset[3], direction[3], grip_offset[3];
int axis;
if (!vr.aim_with_controller || local_player_index != 0 || !halo_vr_stereo_frame() || !hand->active ||
@@ -823,13 +989,16 @@ int halo_vr_grenade_in_hand(short local_player_index, float position[3], float f
{
return 0;
}
/* the grip's +x faces out of the left palm; the drawn hand's knuckles
run along the aim's y, and its fingers along the aim's -z
(first_person_weapons.c, vr_left_hand_free) */
/* the grip's +x faces out of the left palm, into the right one; the
drawn hand's knuckles run along the aim's y, and its fingers along the
aim's -z (first_person_weapons.c, vr_left_hand_free) */
memcpy(grip_offset, vr.grenade_offset, sizeof(grip_offset));
if (free_hand() == HALO_XR_HAND_RIGHT)
grip_offset[0] = -grip_offset[0];
{
float knuckles[3], fingers[3];
quaternion_rotate(hand->grip.orientation, vr.grenade_offset, held);
quaternion_rotate(hand->grip.orientation, grip_offset, held);
quaternion_rotate(hand->aim.orientation, local_up, knuckles);
quaternion_rotate(hand->aim.orientation, local_forward, fingers);
for (axis = 0; axis < 3; axis++)
@@ -878,17 +1047,21 @@ static void melee_update(void)
vr.hand_last_us = now;
{
/* the right grip's melee attack (xinput_sdl.c) aims the same way */
int grip_down = vr.frame.hands[HALO_XR_HAND_RIGHT].active &&
(vr.frame.hands[HALO_XR_HAND_RIGHT].buttons & HALO_XR_BUTTON_SQUEEZE);
/* the melee button's attack (xinput_sdl.c; the weapon hand's grip
unless vr.button_melee moves it) aims the same way */
struct halo_xr_hand hands[HALO_XR_HAND_COUNT];
int button_down;
if (grip_down && !vr.melee_grip_down)
memcpy(hands, vr.frame.hands, sizeof(hands));
controls_in_play(hands);
button_down = (hands[HALO_XR_HAND_RIGHT].buttons & HALO_XR_BUTTON_SQUEEZE) != 0;
if (button_down && !vr.melee_button_down)
{
vr.melee_aim_until_us = now + 700000;
vr.melee_strength = 0.75f;
vr.melee_jab_start_us = now;
}
vr.melee_grip_down = grip_down;
vr.melee_button_down = button_down;
}
quaternion_rotate(vr.frame.head.orientation, look, facing);
facing[1] = 0.0f;
@@ -907,7 +1080,7 @@ static void melee_update(void)
if (vr.hand_last_valid[hand] && seconds > 0.0f && seconds < 0.1f && vr.melee_swing_speed > 0.0f &&
!vr.wheel_held[hand] &&
vr.last_overlay_mode != HALO_XR_OVERLAY_SCREEN && now >= vr.melee_ready_us &&
!(hand == HALO_XR_HAND_LEFT && (vr.grenade_held || now - vr.grenade_released_us < 500000)))
!(hand == free_hand() && (vr.grenade_held || now - vr.grenade_released_us < 500000)))
{
float velocity[3], forward_speed;
int axis;
@@ -963,7 +1136,7 @@ static void fingers_update(void)
target[0] = (state->buttons & HALO_XR_BUTTON_THUMB_TOUCH) ? 0.75f : 0.0f;
target[1] = fmaxf(state->trigger, (state->buttons & HALO_XR_BUTTON_INDEX_TOUCH) ? 0.22f : 0.0f);
target[2] = target[3] = state->squeeze;
if (hand == HALO_XR_HAND_LEFT && vr.grenade_held)
if (hand == free_hand() && vr.grenade_held)
{
for (finger = 0; finger < 4; finger++)
target[finger] = 0.8f;
@@ -1076,6 +1249,9 @@ int halo_vr_finger_curls(short local_player_index, int hand, float curls[4])
if (!vr.active || !vr.fingers || local_player_index != 0 || hand < 0 || hand >= HALO_XR_HAND_COUNT)
return 0;
/* (the model's hands: left-handed, its left is the right controller's) */
if (vr.left_handed)
hand = HALO_XR_HAND_COUNT - 1 - hand;
state = &vr.frame.hands[hand];
if (!state->active || !(state->flags & HALO_XR_POSE_VALID))
return 0;
@@ -1095,7 +1271,7 @@ float halo_vr_melee_strength(void)
static void turn_update(void)
{
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_RIGHT];
const struct halo_xr_hand *hand = &vr.frame.hands[turn_hand()];
unsigned long long now = SDL_GetTicks();
float seconds = vr.last_frame_ms ? (float)(now - vr.last_frame_ms) * 0.001f : 0.0f;
float x = hand->active ? hand->thumbstick[0] : 0.0f;
@@ -1144,7 +1320,7 @@ grips near it takes hold of it, and the hands turn it about its column: both
by the line between them, one about the wheel's middle. Where it is, the
driver's hands as drawn hold it (halo_vr_set_wheel); its column runs from
there back towards the driver, tipped up WHEEL_COLUMN_TILT. Held, its turn
steers (halo_vr_aim); let go, the right stick does. */
steers (halo_vr_aim); let go, the turning stick does. */
#define WHEEL_REACH 0.35f /* metres from its middle */
#define WHEEL_COLUMN_TILT (45.0f * PI / 180.0f) /* from the horizontal */
@@ -1272,13 +1448,45 @@ int halo_vr_wheel_held(void)
}
/* The comfort vignette: while a vehicle turns the player (from 15 degrees a
second, fully at 60), the eye images darken from their edges; it comes in
quickly and goes slowly. */
second, fully at 60), the eye images darken from their edges; on foot
(vr.foot_vignette), while the turning stick turns smoothly and, with
"moving", the move stick moves, as far as they are pushed (fully at three
quarters). It comes in quickly and goes slowly. */
static float foot_vignette_target(void)
{
const struct halo_xr_hand *turn = &vr.frame.hands[turn_hand()];
const struct halo_xr_hand *move = &vr.frame.hands[turn_hand() == HALO_XR_HAND_RIGHT ? HALO_XR_HAND_LEFT :
HALO_XR_HAND_RIGHT];
float push = 0.0f;
if (vr.foot_vignette == FOOT_VIGNETTE_OFF || vr.foot_vignette_strength <= 0.0f || vr.seated ||
!vr.frame.focused || vr.last_overlay_mode == HALO_XR_OVERLAY_SCREEN)
{
return 0.0f;
}
if (vr.smooth_turn && turn->active)
push = fabsf(turn->thumbstick[0]);
if (vr.foot_vignette == FOOT_VIGNETTE_MOVING && move->active)
{
float moving = sqrtf(move->thumbstick[0] * move->thumbstick[0] + move->thumbstick[1] * move->thumbstick[1]);
if (moving > push)
push = moving;
}
/* (beyond the sticks' dead zone, 0.15) */
push = (push - 0.15f) / 0.6f;
if (push < 0.0f)
push = 0.0f;
if (push > 1.0f)
push = 1.0f;
return push * (vr.foot_vignette_strength > 1.0f ? 1.0f : vr.foot_vignette_strength);
}
static void vignette_update(void)
{
unsigned long long now = now_us();
float seconds = vr.vignette_last_us ? (float)(now - vr.vignette_last_us) * 1e-6f : 0.0f;
float target = 0.0f, rate;
float target = foot_vignette_target(), rate;
vr.vignette_last_us = now;
if (seconds > 0.1f)
@@ -1290,13 +1498,15 @@ static void vignette_update(void)
if (vr.vignette_yaw_valid && seconds > 0.0f)
{
float turning = fabsf(wrap_angle(yaw - vr.vignette_last_yaw)) / seconds;
float vehicle = (turning - 15.0f * PI / 180.0f) / (45.0f * PI / 180.0f);
target = (turning - 15.0f * PI / 180.0f) / (45.0f * PI / 180.0f);
if (target < 0.0f)
target = 0.0f;
if (target > 1.0f)
target = 1.0f;
target *= vr.comfort_vignette;
if (vehicle < 0.0f)
vehicle = 0.0f;
if (vehicle > 1.0f)
vehicle = 1.0f;
vehicle *= vr.comfort_vignette;
if (vehicle > target)
target = vehicle;
}
vr.vignette_last_yaw = yaw;
vr.vignette_yaw_valid = 1;
@@ -1317,6 +1527,7 @@ void halo_vr_set_seat(short local_player_index, int seat, int armed, float vehic
{
if (!vr.active || local_player_index != 0)
return;
vr.seated = seat != HALO_VR_SEAT_NONE;
if (!vr.vehicle_first_person)
seat = HALO_VR_SEAT_NONE;
vr.seat_yaw = vehicle_yaw;
@@ -1404,8 +1615,9 @@ float halo_vr_gunner_camera_back(void)
return vr.gunner_camera_back / METRES_PER_WORLD_UNIT;
}
/* seated at a gun or a window, where the right controller aims it: at a
gun's handles (vr.turret_handles), the barrel rises as the hand tips down */
/* seated at a gun or a window, where the weapon hand's controller aims it:
at a gun's handles (vr.turret_handles), the barrel rises as the hand tips
down */
static void seat_aim_direction(float direction[3])
{
float up[3], right[3];
@@ -1416,7 +1628,8 @@ static void seat_aim_direction(float direction[3])
}
/* seated, where the aim runs from (world units) and along: from the eyes,
where the right controller points, or driving, the way the stick steers */
where the weapon hand's controller points, or driving, the way the stick
steers */
static int seat_aim(float start[3], float direction[3])
{
int axis;
@@ -1541,6 +1754,7 @@ void halo_vr_present(unsigned int back_buffer_texture)
submit.wrist_turn = vr.motion_tracker_turn;
submit.wrist_right = vr.motion_tracker_right;
submit.wrist_down = vr.motion_tracker_down;
submit.wrist_hand = free_hand();
d3d8_clear_back_buffer_region(WRIST_REGION_LEFT, WRIST_REGION_TOP, WRIST_REGION_RIGHT, WRIST_REGION_BOTTOM);
}
/* the crosshair, drawn apart (halo_vr_capture_crosshair), to where the
@@ -1831,9 +2045,10 @@ int halo_vr_aim(short local_player_index, float current_yaw, float current_pitch
head_angles(&head_yaw, &head_pitch);
if (vr.seat_mode == HALO_VR_SEAT_STEER)
{
/* driving: the right stick steers off the vehicle's heading, and
aims above or below the horizon (the body turns with the vehicle) */
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_RIGHT];
/* driving: the right stick (mirrored, the left) steers off the
vehicle's heading, and aims above or below the horizon (the body
turns with the vehicle) */
const struct halo_xr_hand *hand = &vr.frame.hands[turn_hand()];
float stick[2];
unsigned long long now = now_us();
float seconds = vr.steer_last_us ? (float)(now - vr.steer_last_us) * 1e-6f : 0.0f;
@@ -1872,7 +2087,7 @@ int halo_vr_aim(short local_player_index, float current_yaw, float current_pitch
}
else if (vr.seat_mode == HALO_VR_SEAT_AIM && weapon_hand_tracked(local_player_index))
{
/* a gunner or passenger: where the right controller points */
/* a gunner or passenger: where the weapon hand's controller points */
float forward[3];
seat_aim_direction(forward);
@@ -1949,42 +2164,125 @@ static void rotate_between(const float from[3], const float to[3], float v[3])
v[index] = v[index] * cosine + turned[index] * sine + axis[index] * along * (1.0f - cosine);
}
/* ---------- the weapon in the right hand (vr.aim_with = "controller") */
/* ---------- the weapon in the hand (vr.aim_with = "controller"): the right
one, or left-handed (vr.handedness) the left */
static int weapon_hand_tracked(short local_player_index)
{
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_RIGHT];
const struct halo_xr_hand *hand = &vr.frame.hands[weapon_hand()];
return vr.aim_with_controller && local_player_index == 0 && halo_vr_stereo_frame() &&
hand->active && (hand->flags & HALO_XR_POSE_VALID);
}
/* the weapon's axes in the game's: where the right controller points */
/* Left-handed, the first-person model (first_person_weapons.c) is posed as
the right hand's is, in a mirror image of the room, "model space", and
drawn mirrored back across the same mirror (as HaloCE-Quest-VR mirrors the
whole model across the weapon, CC0): its right hand takes the left
controller, its left hand the right one, its arms the other shoulders. The
mirror is the upright plane through the weapon hand's grip, square to
LOCAL's x: any upright one would do (the model, posed about the vertical
the same, mirrors back the same), and this one leaves the weapon hand
where it is. Right-handed, model space is the room. */
static void model_local_point(float point[3])
{
if (vr.left_handed)
point[0] = 2.0f * vr.frame.hands[weapon_hand()].grip.position[0] - point[0];
}
static void model_local_direction(float direction[3])
{
if (vr.left_handed)
direction[0] = -direction[0];
}
/* the mirror in the game's world: a point on it (world units) and its
normal, LOCAL's x */
static void mirror_plane(float point[3], float normal[3])
{
static const float local_x[3] = { 1.0f, 0.0f, 0.0f };
float offset[3];
int axis;
tracked_offset(vr.frame.hands[weapon_hand()].grip.position, offset);
for (axis = 0; axis < 3; axis++)
point[axis] = vr.game_camera[axis] + offset[axis] / METRES_PER_WORLD_UNIT;
local_to_game(local_x, vr.body_yaw, normal);
}
/* a point or a direction in the game's world, between the room and model
space (either way: the mirror is its own inverse) */
static void mirror_game_point(float point[3])
{
float on[3], normal[3], along;
int axis;
if (!vr.left_handed)
return;
mirror_plane(on, normal);
along = (point[0] - on[0]) * normal[0] + (point[1] - on[1]) * normal[1] + (point[2] - on[2]) * normal[2];
for (axis = 0; axis < 3; axis++)
point[axis] -= 2.0f * along * normal[axis];
}
static void mirror_game_direction(float direction[3])
{
float on[3], normal[3], along;
int axis;
if (!vr.left_handed)
return;
mirror_plane(on, normal);
along = direction[0] * normal[0] + direction[1] * normal[1] + direction[2] * normal[2];
for (axis = 0; axis < 3; axis++)
direction[axis] -= 2.0f * along * normal[axis];
}
int halo_vr_first_person_mirror(short local_player_index, float point[3], float normal[3])
{
if (!vr.left_handed || !weapon_hand_tracked(local_player_index))
return 0;
mirror_plane(point, normal);
return 1;
}
/* The weapon's frames, all in the game's axes:
- the controller's: where it points;
- the model's: the controller's turned so that the first-person model's
barrel, pointed in towards the screen's centre, points where the
controller does (barrel_direction);
- with both hands on it, both turned alike so that the line from the
model's right hand to its left, in the model's frame, runs from the right
controller to the left one.
model's right hand to its left, in the model's frame, runs from the
weapon hand's controller to the free one's.
The weapon aims along the controller's frame (so turned); the model hangs
from the model's frame. */
static void controller_axes(float forward[3], float up[3], float right[3])
from the model's frame. Turned in model space, both; the aim is then
mirrored back. */
static void hand_axes(int hand, int model, float forward[3], float up[3], float right[3])
{
static const float local_forward[3] = { 0.0f, 0.0f, -1.0f };
static const float local_up[3] = { 0.0f, 1.0f, 0.0f };
const float *orientation = vr.frame.hands[HALO_XR_HAND_RIGHT].aim.orientation;
const float *orientation = vr.frame.hands[hand].aim.orientation;
float direction[3];
quaternion_rotate(orientation, local_forward, direction);
if (model)
model_local_direction(direction);
local_to_game(direction, vr.body_yaw, forward);
quaternion_rotate(orientation, local_up, direction);
if (model)
model_local_direction(direction);
local_to_game(direction, vr.body_yaw, up);
/* the game's axes are right-handed with +z up: forward x up is right */
/* the game's axes are right-handed with +z up: forward x up is right
(in model space too: the mirrored controller's frame is a right hand's) */
cross3(forward, up, right);
}
/* where the weapon hand's controller points */
static void controller_axes(float forward[3], float up[3], float right[3])
{
hand_axes(weapon_hand(), 0, forward, up, right);
}
static void model_axes(const float forward[3], const float up[3], const float right[3],
float model_forward[3], float model_up[3], float model_right[3])
{
@@ -2010,11 +2308,11 @@ static void model_axes(const float forward[3], const float up[3], const float ri
}
/* with both hands on the weapon, the turn that brings the model's grips in
line with the hands (from, to); 0 without */
line with the hands (from, to; model space); 0 without */
static int two_hand_turn(const float model_forward[3], const float model_up[3], const float model_right[3],
float from[3], float to[3])
{
float left[3], right_hand[3];
float free_grip[3], free_offset[3], weapon_offset[3];
int axis;
if (!vr.two_hand_gripping || !vr.two_hand_valid)
@@ -2024,10 +2322,12 @@ static int two_hand_turn(const float model_forward[3], const float model_up[3],
from[axis] = model_forward[axis] * vr.two_hand_reference[0] + model_right[axis] * vr.two_hand_reference[1] +
model_up[axis] * vr.two_hand_reference[2];
}
tracked_offset(vr.frame.hands[HALO_XR_HAND_LEFT].grip.position, left);
tracked_offset(vr.frame.hands[HALO_XR_HAND_RIGHT].grip.position, right_hand);
memcpy(free_grip, vr.frame.hands[free_hand()].grip.position, sizeof(free_grip));
model_local_point(free_grip);
tracked_offset(free_grip, free_offset);
tracked_offset(vr.frame.hands[weapon_hand()].grip.position, weapon_offset);
for (axis = 0; axis < 3; axis++)
to[axis] = left[axis] - right_hand[axis];
to[axis] = free_offset[axis] - weapon_offset[axis];
return normalize3(from) > 0.0f && normalize3(to) > 0.0f;
}
@@ -2038,34 +2338,41 @@ static void turn_axes(const float from[3], const float to[3], float forward[3],
cross3(forward, up, right);
}
/* where the weapon aims */
/* where the weapon aims (in the room) */
static void weapon_axes(float forward[3], float up[3], float right[3])
{
float model_forward[3], model_up[3], model_right[3], from[3], to[3];
controller_axes(forward, up, right);
hand_axes(weapon_hand(), 1, forward, up, right);
model_axes(forward, up, right, model_forward, model_up, model_right);
if (two_hand_turn(model_forward, model_up, model_right, from, to))
turn_axes(from, to, forward, up, right);
if (vr.left_handed)
{
mirror_game_direction(forward);
mirror_game_direction(up);
cross3(forward, up, right);
}
}
/* how the weapon's model hangs */
/* how the weapon's model hangs (model space) */
static void weapon_model_axes(float forward[3], float up[3], float right[3])
{
float controller_forward[3], controller_up[3], controller_right[3], from[3], to[3];
controller_axes(controller_forward, controller_up, controller_right);
hand_axes(weapon_hand(), 1, controller_forward, controller_up, controller_right);
model_axes(controller_forward, controller_up, controller_right, forward, up, right);
if (two_hand_turn(forward, up, right, from, to))
turn_axes(from, to, forward, up, right);
}
/* the hand's offset from the game's camera, in world units */
/* the weapon hand's offset from the game's camera, in world units (the
same in model space) */
static void hand_offset(float out[3])
{
int axis;
tracked_offset(vr.frame.hands[HALO_XR_HAND_RIGHT].grip.position, out);
tracked_offset(vr.frame.hands[weapon_hand()].grip.position, out);
for (axis = 0; axis < 3; axis++)
out[axis] /= METRES_PER_WORLD_UNIT;
}
@@ -2087,7 +2394,10 @@ void halo_vr_set_barrel(short local_player_index, const float world[3], const fl
{
if (local_player_index != 0)
return;
/* (posed in model space: the barrel as drawn, in the room; its
direction stays the model's, which model_axes turns) */
memcpy(vr.barrel, world, sizeof(vr.barrel));
mirror_game_point(vr.barrel);
vr.barrel_frame = vr.frame_number;
if (!vr.barrel_direction_valid || memcmp(vr.barrel_direction, direction, sizeof(vr.barrel_direction)))
{
@@ -2152,7 +2462,7 @@ int halo_vr_weapon_camera(short local_player_index, float position[3], float for
weapon_model_axes(forward, up, right);
hand_offset(hand);
{
/* the right grip's melee attack, which the hand does not swing: the
/* the weapon hand's grip's melee attack, which it does not swing: the
weapon jabs forward and back (the game's own animation is held
off, first_person_weapons.c) */
float seconds = vr.melee_jab_start_us ? (float)(now_us() - vr.melee_jab_start_us) * 1e-6f : 1.0f;
@@ -2264,12 +2574,12 @@ int halo_vr_body_frame(short local_player_index, float head[3], float neck[3], f
return 1;
}
int halo_vr_left_hand(short local_player_index, float position[3], float forward[3], float up[3])
int halo_vr_free_hand(short local_player_index, float position[3], float forward[3], float up[3])
{
static const float local_forward[3] = { 0.0f, 0.0f, -1.0f };
static const float local_up[3] = { 0.0f, 1.0f, 0.0f };
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_LEFT];
float offset[3], direction[3];
const struct halo_xr_hand *hand = &vr.frame.hands[free_hand()];
float grip[3], offset[3], direction[3];
int axis;
if (!vr.aim_with_controller || local_player_index != 0 || !halo_vr_stereo_frame() || !hand->active ||
@@ -2277,25 +2587,30 @@ int halo_vr_left_hand(short local_player_index, float position[3], float forward
{
return 0;
}
tracked_offset(hand->grip.position, offset);
/* (in model space: left-handed, the right controller as a left one) */
memcpy(grip, hand->grip.position, sizeof(grip));
model_local_point(grip);
tracked_offset(grip, offset);
for (axis = 0; axis < 3; axis++)
position[axis] = vr.game_camera[axis] + offset[axis] / METRES_PER_WORLD_UNIT;
quaternion_rotate(hand->aim.orientation, local_forward, direction);
model_local_direction(direction);
local_to_game(direction, vr.body_yaw, forward);
quaternion_rotate(hand->aim.orientation, local_up, direction);
model_local_direction(direction);
local_to_game(direction, vr.body_yaw, up);
return 1;
}
int halo_vr_two_hand(short local_player_index, const float foregrip[3], const float reference[3])
{
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_LEFT];
const struct halo_xr_hand *hand = &vr.frame.hands[free_hand()];
float position[3], forward[3], up[3];
int squeezed = hand->squeeze > (vr.two_hand_latched ? 0.35f : 0.6f);
memcpy(vr.two_hand_reference, reference, sizeof(vr.two_hand_reference));
vr.two_hand_valid = 1;
if (!squeezed || !halo_vr_left_hand(local_player_index, position, forward, up))
if (!squeezed || !halo_vr_free_hand(local_player_index, position, forward, up))
{
vr.two_hand_latched = vr.two_hand_gripping = 0;
return 0;
@@ -2310,7 +2625,7 @@ int halo_vr_two_hand(short local_player_index, const float foregrip[3], const fl
vr.two_hand_latched = 1;
vr.two_hand_gripping = distance < vr.two_hand_reach;
if (vr.two_hand_gripping)
host_xr_haptic(HALO_XR_HAND_LEFT, 0.4f, 0.04f);
host_xr_haptic(free_hand(), 0.4f, 0.04f);
}
return vr.two_hand_gripping;
}
@@ -2338,8 +2653,11 @@ void halo_vr_set_laser(short local_player_index, const float start[3], const flo
{
if (local_player_index != 0)
return;
/* (from model space) */
memcpy(vr.laser_start, start, sizeof(vr.laser_start));
memcpy(vr.laser_direction, direction, sizeof(vr.laser_direction));
mirror_game_point(vr.laser_start);
mirror_game_direction(vr.laser_direction);
vr.laser_frame = vr.frame_number;
}
@@ -2694,7 +3012,7 @@ void halo_vr_set_scope_raise(float metres)
int halo_vr_scope(short local_player_index, float magnification, float position[3], float forward[3], float up[3],
float *vertical_field_of_view)
{
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_RIGHT];
const struct halo_xr_hand *hand = &vr.frame.hands[weapon_hand()];
float right[3], game_offset[3], local_forward[3], local_up[3], local_right[3], back[3];
float eye_distance = 0.0f;
int axis;
@@ -2774,6 +3092,110 @@ void halo_vr_capture_crosshair(void)
d3d8_clear_back_buffer(0.0f, 0.0f, 0.0f, 0.0f);
}
/* Left-handed, in play, the controllers as the gamepad's sides: the weapon
hand's trigger and grip are the right side's (fire, melee), the free hand's
the left side's (the grenade, both hands); with vr.mirror_controls the
sticks, their clicks, the bumpers and the face buttons change sides too,
the Quest's X and Y for A and B (after HaloCE-Quest-VR, CC0). The Steam
Frame has A, B, X and Y on the right controller only, a D-pad on the left:
the D-pad becomes them, as their mirror image (down A, left B, right X, up
Y), and they the D-pad. The menus keep the controllers as they are (A
accepts and B goes back, as the screens show). */
static void controllers_mirrored(struct halo_xr_hand hands[HALO_XR_HAND_COUNT])
{
struct halo_xr_hand *left = &hands[HALO_XR_HAND_LEFT];
struct halo_xr_hand *right = &hands[HALO_XR_HAND_RIGHT];
int left_buttons = left->buttons, right_buttons = right->buttons;
int swapped = HALO_XR_BUTTON_TRIGGER | HALO_XR_BUTTON_SQUEEZE;
int frame = vr.frame.controller_layout == HALO_XR_LAYOUT_FRAME;
float value;
int axis;
value = left->trigger;
left->trigger = right->trigger;
right->trigger = value;
value = left->squeeze;
left->squeeze = right->squeeze;
right->squeeze = value;
if (vr.mirror_controls)
{
for (axis = 0; axis < 2; axis++)
{
value = left->thumbstick[axis];
left->thumbstick[axis] = right->thumbstick[axis];
right->thumbstick[axis] = value;
}
swapped |= HALO_XR_BUTTON_THUMBSTICK | HALO_XR_BUTTON_BUMPER;
if (!frame)
swapped |= HALO_XR_BUTTON_A | HALO_XR_BUTTON_B;
}
left->buttons = (left_buttons & ~swapped) | (right_buttons & swapped);
right->buttons = (right_buttons & ~swapped) | (left_buttons & swapped);
if (vr.mirror_controls && frame)
{
const int face = HALO_XR_BUTTON_A | HALO_XR_BUTTON_B;
const int dpad = HALO_XR_BUTTON_DPAD_UP | HALO_XR_BUTTON_DPAD_DOWN | HALO_XR_BUTTON_DPAD_LEFT |
HALO_XR_BUTTON_DPAD_RIGHT;
/* (the right one's X and Y come as the left's A and B: host_xr.c) */
left->buttons &= ~(face | dpad);
right->buttons &= ~face;
right->buttons |= ((left_buttons & HALO_XR_BUTTON_DPAD_DOWN) ? HALO_XR_BUTTON_A : 0) |
((left_buttons & HALO_XR_BUTTON_DPAD_LEFT) ? HALO_XR_BUTTON_B : 0);
left->buttons |= ((left_buttons & HALO_XR_BUTTON_DPAD_RIGHT) ? HALO_XR_BUTTON_A : 0) |
((left_buttons & HALO_XR_BUTTON_DPAD_UP) ? HALO_XR_BUTTON_B : 0) |
((right_buttons & HALO_XR_BUTTON_A) ? HALO_XR_BUTTON_DPAD_DOWN : 0) |
((right_buttons & HALO_XR_BUTTON_B) ? HALO_XR_BUTTON_DPAD_LEFT : 0) |
((left_buttons & HALO_XR_BUTTON_A) ? HALO_XR_BUTTON_DPAD_RIGHT : 0) |
((left_buttons & HALO_XR_BUTTON_B) ? HALO_XR_BUTTON_DPAD_UP : 0);
}
}
/* The buttons the actions are on (vr.button_*), as the gamepad's sides
have them once mirrored: A and B the right hand's, X and Y the left's
A and B, the move stick's click the left's, the turning stick's the
right's, the grip the right's (the weapon hand's). Each action's button
is read and handed on as the button the action has by default, which
xinput_sdl.c turns into it; with the defaults nothing changes. The grips
holding a Warthog's wheel press nothing. */
static const int input_hands[HALO_VR_INPUT_COUNT] =
{
HALO_XR_HAND_RIGHT, HALO_XR_HAND_RIGHT, HALO_XR_HAND_LEFT, HALO_XR_HAND_LEFT, HALO_XR_HAND_LEFT,
HALO_XR_HAND_RIGHT, HALO_XR_HAND_RIGHT,
};
static const int input_buttons[HALO_VR_INPUT_COUNT] =
{
HALO_XR_BUTTON_A, HALO_XR_BUTTON_B, HALO_XR_BUTTON_A, HALO_XR_BUTTON_B, HALO_XR_BUTTON_THUMBSTICK,
HALO_XR_BUTTON_THUMBSTICK, HALO_XR_BUTTON_SQUEEZE,
};
static void controls_remapped(struct halo_xr_hand hands[HALO_XR_HAND_COUNT])
{
int down[HALO_VR_INPUT_COUNT];
int input, action;
for (input = 0; input < HALO_VR_INPUT_COUNT; input++)
down[input] = (hands[input_hands[input]].buttons & input_buttons[input]) != 0;
if (vr.wheel_hands)
down[HALO_VR_INPUT_GRIP] = 0;
for (input = 0; input < HALO_VR_INPUT_COUNT; input++)
hands[input_hands[input]].buttons &= ~input_buttons[input];
for (action = 0; action < HALO_VR_ACTION_COUNT; action++)
{
if (down[vr.action_inputs[action]])
hands[input_hands[action]].buttons |= input_buttons[action];
}
}
/* the controllers as the game is played with them: mirrored (left-handed)
and the actions on their buttons */
static void controls_in_play(struct halo_xr_hand hands[HALO_XR_HAND_COUNT])
{
if (vr.left_handed)
controllers_mirrored(hands);
controls_remapped(hands);
}
/* the controllers, for the gamepad the game reads (xinput_sdl.c) */
int halo_vr_controllers(struct halo_xr_hand hands[HALO_XR_HAND_COUNT], int *menus)
{
@@ -2781,12 +3203,23 @@ int halo_vr_controllers(struct halo_xr_hand hands[HALO_XR_HAND_COUNT], int *menu
return 0;
memcpy(hands, vr.frame.hands, sizeof(vr.frame.hands));
*menus = vr.last_overlay_mode == HALO_XR_OVERLAY_SCREEN;
if (!*menus)
controls_in_play(hands);
return 1;
}
#else
int halo_vr_controller_layout(void) { return 0; }
int halo_vr_weapon_hand(void) { return 1; }
int halo_vr_action_input(int action) { return action; }
int halo_vr_graphics(void) { return HALO_VR_GRAPHICS_HIGH; }
int halo_vr_refresh_rates(float *rates, int maximum, float *current)
{
(void)rates; (void)maximum; *current = 0.0f;
return 0;
}
float halo_vr_started_resolution_scale(void) { return 1.0f; }
int halo_vr_active(void) { return 0; }
unsigned long long halo_vr_timing_start(void) { return 0; }
unsigned long halo_vr_frame_serial(void) { return 0; }
@@ -2887,11 +3320,16 @@ int halo_vr_body_frame(short local_player_index, float head[3], float neck[3], f
(void)local_player_index; (void)head; (void)neck; (void)forward;
return 0;
}
int halo_vr_left_hand(short local_player_index, float position[3], float forward[3], float up[3])
int halo_vr_free_hand(short local_player_index, float position[3], float forward[3], float up[3])
{
(void)local_player_index; (void)position; (void)forward; (void)up;
return 0;
}
int halo_vr_first_person_mirror(short local_player_index, float point[3], float normal[3])
{
(void)local_player_index; (void)point; (void)normal;
return 0;
}
int halo_vr_two_hand(short local_player_index, const float foregrip[3], const float reference[3])
{
(void)local_player_index; (void)foregrip; (void)reference;
+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);
+10
View File
@@ -118,6 +118,10 @@ void cheats_dispose_from_old_map(
/* network_game_globals.c's */
boolean network_game_distributed_client(void);
#ifdef HALO_ANDROID
/* port/linux/game/vr_graphics.c */
boolean vr_graphics_reduces(boolean const *game_switch);
#endif
/* bipeds.c's, player_control.c's */
extern boolean rider_ejection;
extern boolean player_autoaim_flag;
@@ -155,6 +159,12 @@ static boolean cheats_network_client_rasterizer_enforce(
environment_part <= &rasterizer_debug_options.draw_environment_fog_screen;
environment_part++)
{
#ifdef HALO_ANDROID
/* (VR: a part the graphics preset keeps off, as it may in another's
game: shadows, specular light, reflections; vr_graphics.c) */
if (vr_graphics_reduces(environment_part))
continue;
#endif
if (!*environment_part)
changed = TRUE;
*environment_part = TRUE;
+162 -15
View File
@@ -488,10 +488,11 @@ static boolean first_person_weapon_vr_pose_kept(
first_person_weapon_vr_pose_weapon[local_player_index]==first_person_weapon_get(local_player_index)->weapon_index;
}
/* VR: the grenade the player holds (the left trigger, units.c) hangs from
the body, which the first-person view leaves out, and with it the grenade:
drawn instead in the left hand, as it is this frame, with the hands (which
are drawn over the world, and would hide it wherever they overlap) */
/* VR: the grenade the player holds (the free hand's trigger, units.c)
hangs from the body, which the first-person view leaves out, and with it
the grenade: drawn instead in the free hand, as it is this frame, with the
hands (which are drawn over the world, and would hide it wherever they
overlap; in the room, not mirrored) */
static void first_person_weapon_draw_held_grenade(
long unit_index,
struct render_lighting *lighting,
@@ -1217,17 +1218,132 @@ static void first_person_weapon_set_state(
#ifdef HALO_ANDROID
/* ---------- VR arms
The first-person skeleton hangs from the camera at the right controller
(halo_vr_weapon_camera), so the weapon and the right hand lie where they
should; its arms, though, reach up to shoulders that float with the weapon.
Here each arm's upper arm and forearm are turned (two-bone inverse
kinematics, port/linux/game/vr_arms.c) to run from the player's shoulders,
below the neck and square to the torso, to its wrist, the controller's roll
shared along the forearm. The left wrist, unless the hand holds the
weapon's foregrip, goes to the left controller, its fingers with it. */
The first-person skeleton hangs from the camera at the weapon hand's
controller (halo_vr_weapon_camera), so the weapon and the right hand lie
where they should; its arms, though, reach up to shoulders that float with
the weapon. Here each arm's upper arm and forearm are turned (two-bone
inverse kinematics, port/linux/game/vr_arms.c) to run from the player's
shoulders, below the neck and square to the torso, to its wrist, the
controller's roll shared along the forearm. The left wrist, unless the hand
holds the weapon's foregrip, goes to the free controller, its fingers with
it.
Left-handed (vr.handedness) all this is done in model space (halo_vr.h): a
mirror image of the room, where the left controller holds the weapon as a
right hand would; the body is mirrored into it, and the posed skeleton
mirrored back (vr_mirror_pose), so that the weapon lies in the left hand,
the model's left arm reaches to the right controller. */
#define VR_METRES_PER_WORLD_UNIT 3.048f
/* a point or a vector across the mirror (a point on it, its normal) */
static void vr_mirror_point(
float const on[3],
float const normal[3],
float point[3])
{
float along = (point[0] - on[0]) * normal[0] + (point[1] - on[1]) * normal[1] + (point[2] - on[2]) * normal[2];
point[0] -= 2.f * along * normal[0];
point[1] -= 2.f * along * normal[1];
point[2] -= 2.f * along * normal[2];
}
static void vr_mirror_vector(
float const normal[3],
float vector[3])
{
float along = vector[0] * normal[0] + vector[1] * normal[1] + vector[2] * normal[2];
vector[0] -= 2.f * along * normal[0];
vector[1] -= 2.f * along * normal[1];
vector[2] -= 2.f * along * normal[2];
}
/* the posed skeleton across a mirror: every node mirrored, its axes too
(a left-handed frame: rasterizer_xbox.c turns its triangles' winding over).
Numeric displays drawn so, the assault rifle's ammo counter and compass,
are read the right way round by rasterizer_xbox_transparent_geometry.c. */
static void vr_mirror_nodes(
float const on[3],
float const normal[3],
real_matrix4x3 *node_matrices,
short node_count)
{
short node_index;
for (node_index = 0; node_index < node_count; node_index++)
{
real_matrix4x3 *node = &node_matrices[node_index];
real_vector3d *axes[3];
float vector[3];
short axis;
vector[0] = node->position.x;
vector[1] = node->position.y;
vector[2] = node->position.z;
vr_mirror_point(on, normal, vector);
node->position.x = vector[0];
node->position.y = vector[1];
node->position.z = vector[2];
axes[0] = &node->forward;
axes[1] = &node->left;
axes[2] = &node->up;
for (axis = 0; axis < 3; axis++)
{
vector[0] = axes[axis]->i;
vector[1] = axes[axis]->j;
vector[2] = axes[axis]->k;
vr_mirror_vector(normal, vector);
axes[axis]->i = vector[0];
axes[axis]->j = vector[1];
axes[axis]->k = vector[2];
}
}
}
/* the posed skeleton out of model space (left-handed) */
static void vr_mirror_pose(
short local_player_index,
real_matrix4x3 *node_matrices,
short node_count)
{
float on[3], normal[3];
if (halo_vr_first_person_mirror(local_player_index, on, normal))
vr_mirror_nodes(on, normal, node_matrices, node_count);
}
/* debug.mirror_first_person (a test aid, flat): the first-person weapon
mirrored across the camera's own forward and up, as the left hand's is in
VR; its numeric displays can so be checked off the headset */
int config_boolean(char const *name);
static void debug_mirror_first_person(
struct render_camera const *camera,
real_matrix4x3 *node_matrices,
short node_count)
{
static int wanted = -1;
real_vector3d left;
float on[3], normal[3];
if (wanted < 0)
wanted = config_boolean("debug.mirror_first_person") ? 1 : 0;
if (!wanted)
return;
cross_product3d(&camera->up, &camera->forward, &left);
if (normalize3d(&left) == 0.0f)
return;
on[0] = camera->position.x;
on[1] = camera->position.y;
on[2] = camera->position.z;
normal[0] = left.i;
normal[1] = left.j;
normal[2] = left.k;
vr_mirror_nodes(on, normal, node_matrices, node_count);
}
struct vr_arm_nodes
{
long graph_index;
@@ -1470,6 +1586,16 @@ static void vr_laser_follows_barrel(
{
return;
}
{
/* (the laser at the ready is the room's: into model space) */
float on[3], normal[3];
if (halo_vr_first_person_mirror(local_player_index, on, normal))
{
vr_mirror_point(on, normal, laser_start);
vr_mirror_vector(normal, laser_direction);
}
}
matrix4x3_from_point_and_vectors(&camera_matrix, &weapon_camera->position, &weapon_camera->forward,
&weapon_camera->up);
matrix4x3_multiply(&camera_matrix, &vr_arm_nodes.idle_pose[vr_arm_nodes.gun], &ready_gun);
@@ -1656,6 +1782,18 @@ static void first_person_weapon_vr_arms(
{
return;
}
{
/* (the body is the room's: into model space, where the shoulders
change sides) */
float on[3], normal[3];
if (halo_vr_first_person_mirror(local_player_index, on, normal))
{
vr_mirror_point(on, normal, head);
vr_mirror_point(on, normal, neck);
vr_mirror_vector(normal, body_forward);
}
}
/* (the shoulders hang from the neck, which the head turns about) */
eye.x = neck[0];
eye.y = neck[1];
@@ -1705,7 +1843,7 @@ static void first_person_weapon_vr_arms(
return;
if (!gripping && vr_arm_nodes.wrist[0] != NONE)
{
if (halo_vr_left_hand(local_player_index, hand_position, hand_forward, hand_up))
if (halo_vr_free_hand(local_player_index, hand_position, hand_forward, hand_up))
{
/* (rigid: the hand and its arm carried to the controller now;
bent, the arm is solved to it first, below, and the hand turned
@@ -1834,8 +1972,8 @@ static void first_person_weapon_build_node_matrices(
struct first_person_weapon *first_person_weapon= first_person_weapon_get(local_player_index);
struct render_camera const *camera= &render.camera;
#ifdef HALO_ANDROID
/* VR: posed from the right controller, so the weapon lies in the hand
(port/linux/src/vr.c) */
/* VR: posed from the weapon hand's controller, so the weapon lies in the
hand (port/linux/src/vr.c) */
struct render_camera weapon_camera;
float weapon_position[3], weapon_forward[3], weapon_up[3];
@@ -2202,8 +2340,17 @@ static void first_person_weapon_build_node_matrices(
(short)animation_graph->nodes.count,
camera,
first_person_weapon->state);
/* (left-handed: out of model space; what the frame takes from
the pose after, markers, effects, is then the room's) */
vr_mirror_pose(local_player_index, first_person_weapon->node_matrices,
(short)MIN(animation_graph->nodes.count, MAXIMUM_NODES_PER_ANIMATION));
halo_vr_timing_end(HALO_VR_TIMING_ARMS, timing);
}
else
{
debug_mirror_first_person(camera, first_person_weapon->node_matrices,
(short)MIN(animation_graph->nodes.count, MAXIMUM_NODES_PER_ANIMATION));
}
#endif
}
File diff suppressed because it is too large. Load diff
+8 -1
View File
@@ -31,7 +31,14 @@ enum
RASTERIZER_MAXIMUM_TRIANGLES_PER_TRIANGLE_BUFFER = 24576,
RASTERIZER_MAXIMUM_DEBUG_PRIMITIVES = 131072,
RASTERIZER_MAXIMUM_DEBUG_VERTICES = 393216,
RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS = 384,
/* port: the Xbox's 384 groups a window (each a model's transparent part,
a particle system, a contrail, a light volume, a window's or water's
surface) are spent in games of 30 and more players, and whatever is
submitted after the last is dropped, the structure's glass and water
and the particles first (render_window submits the objects before
them). Their indices are shorts, the pending flags a bit vector of
this size: 2048 groups take 320 KB. */
RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS = 2048,
RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS2 = 32,
RASTERIZER_MAXIMUM_DYNAMIC_TRIANGLES = 32768,
RASTERIZER_MAXIMUM_DYNAMIC_TRIANGLE_BUFFERS = 1024,
@@ -150,6 +150,7 @@ typedef char transparent_geometry_group_cortana_hack_offset_assert[
/* ---------- prototypes */
static void rasterizer_sort_external(
void);
void platform_log(char const *format, ...);
/* ---------- globals */
@@ -162,6 +163,9 @@ static long transparent_geometry_group_count = 0;
static long transparent_geometry_group_count2 = 0;
static short *transparent_geometry_group_sorted_indices = NULL;
static short transparent_geometry_next_group_sorted_index = 0;
/* port: the most groups a window has had, logged as it passes the Xbox's
384 and every 128 beyond (rasterizer.h) */
static long transparent_geometry_group_count_logged = 384;
/* ---------- public code */
@@ -200,6 +204,12 @@ boolean rasterizer_transparent_geometry_initialize(
void rasterizer_transparent_geometry_begin(
void)
{
if (transparent_geometry_group_count>transparent_geometry_group_count_logged)
{
platform_log("rasterizer: %ld transparent geometry groups in a window (the Xbox had room for 384, the port %d)",
transparent_geometry_group_count, RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS);
transparent_geometry_group_count_logged = (transparent_geometry_group_count/128 + 1)*128;
}
transparent_geometry_group_count = 0;
transparent_geometry_next_group_sorted_index = 0;
memset(transparent_geometry_group_pending_flags, 0,
+20
View File
@@ -1227,6 +1227,11 @@ void rasterizer_set_model_lighting(
return;
}
#ifdef HALO_ANDROID
/* port/linux/src/d3d8_gl.c */
void d3d8_set_mirrored_skinning(int mirrored);
#endif
void rasterizer_set_model_skinning(
struct render_skinning const *skinning)
{
@@ -1263,6 +1268,21 @@ void rasterizer_set_model_skinning(
constants[2][2] = scale * matrix->up.k;
constants[2][3] = matrix->position.z;
}
#ifdef HALO_ANDROID
/* port: node matrices that mirror (VR, the left hand's first-person
weapon: first_person_weapons.c) turn the triangles' winding over while
they are drawn (after HaloCE-Quest-VR, CC0); _rasterizer_model_end and
rasterizer_transparent_geometry_group_draw end it */
{
real_matrix4x3 const *m = &skinning->node_matrices[0];
real determinant =
m->forward.i * (m->left.j * m->up.k - m->left.k * m->up.j) -
m->forward.j * (m->left.i * m->up.k - m->left.k * m->up.i) +
m->forward.k * (m->left.i * m->up.j - m->left.j * m->up.i);
d3d8_set_mirrored_skinning(determinant < 0.0f);
}
#endif
D3DDevice_SetVertexShaderConstant(
-36,
vsh_constants__nodematrices,
@@ -596,9 +596,18 @@ void _rasterizer_models_end(
return;
}
#ifdef HALO_ANDROID
/* port/linux/src/d3d8_gl.c: the model's mirroring (rasterizer_xbox.c) ends
with it */
void d3d8_set_mirrored_skinning(int mirrored);
#endif
void _rasterizer_model_end(
void)
{
#ifdef HALO_ANDROID
d3d8_set_mirrored_skinning(0);
#endif
if (rasterizer_debug_options.draw_models)
{
match_assert(
@@ -970,9 +970,83 @@ real_vector4d *offset_vector4d(
return result;
}
#ifdef HALO_ANDROID
/* port: a group's draw, its mirroring (rasterizer_set_model_skinning) put
back after it to what it was: a group drawn in the middle of a model's
(not queued) leaves the model's own */
int d3d8_mirrored_skinning(void);
void d3d8_set_mirrored_skinning(int mirrored);
/* port: a numeric display drawn mirrored (rasterizer_set_model_skinning:
the left hand's first-person weapon in VR; the assault rifle's ammo counter
and compass) reads the right way round. Its geometry stays mirrored with the
weapon (its vertices hang from the gun's nodes as well as the display's);
instead each digit's quad shows its bitmap turned over in u
(rasterizer_numeric_mirror_u), and as the quads' order across the display is
turned over too, the quad of the n-th digit from the right shows the n-th
from the left. The digits: as many as the counter's largest value needs,
that value itself wrapping to 0 (the compass's whole turn). FALSE, the
digit untouched, when not mirrored. Only for a first map in the model's own
u and v (2d, not in screen space), which rasterizer_numeric_mirror_u turns. */
static boolean rasterizer_numeric_mirrored_digit(
real value,
short counter_limit,
short frame_count,
short digit_index,
short *bitmap_sequence_index)
{
short counter_value;
short digit_count;
short place;
long power;
if (!d3d8_mirrored_skinning() || frame_count < 2)
return FALSE;
counter_value = PIN(fast_ftol(floor(counter_limit*value + 0.5f)), 0, counter_limit);
digit_count = 1;
for (power = frame_count; power < counter_limit; power *= frame_count)
digit_count++;
place = digit_count - 1 - digit_index;
if (place < 0)
place = digit_index;
while (place-- > 0)
counter_value /= frame_count;
*bitmap_sequence_index = counter_value%frame_count;
return TRUE;
}
/* the first map's u turned over (1-u: each digit's quad spans its bitmap) */
static void rasterizer_numeric_mirror_u(
real u_transform[4])
{
u_transform[0] = -u_transform[0];
u_transform[1] = -u_transform[1];
u_transform[2] = -u_transform[2];
u_transform[3] = 1.0f - u_transform[3];
}
static void rasterizer_transparent_geometry_group_draw_unmirrored(
struct transparent_geometry_group *group,
boolean dirty);
void rasterizer_transparent_geometry_group_draw(
struct transparent_geometry_group *group,
boolean dirty)
{
int mirrored = d3d8_mirrored_skinning();
rasterizer_transparent_geometry_group_draw_unmirrored(group, dirty);
d3d8_set_mirrored_skinning(mirrored);
}
static void rasterizer_transparent_geometry_group_draw_unmirrored(
#else
void rasterizer_transparent_geometry_group_draw(
#endif
struct transparent_geometry_group *group,
boolean dirty)
{
boolean success = TRUE;
boolean draw_active_camouflage_groups2 = FALSE;
@@ -1813,6 +1887,9 @@ void rasterizer_transparent_geometry_group_draw(
short map_index;
short stage_index;
long result;
#ifdef HALO_ANDROID
boolean numeric_mirrored = FALSE;
#endif
for (layer_index = 0;
layer_index < shader_transparent_generic->generic.extra_layers.count;
@@ -1872,6 +1949,18 @@ void rasterizer_transparent_geometry_group_draw(
counter_value /= frame_count;
bitmap_sequence_index = counter_value%frame_count;
#ifdef HALO_ANDROID
numeric_mirrored =
shader_transparent_generic->generic.type==_shader_transparent_generic_type_2d_map &&
!TEST_FLAG(shader_transparent_generic->generic.flags,
_shader_transparent_flag_first_map_is_in_screenspace_bit) &&
rasterizer_numeric_mirrored_digit(
group->animation->values[function_index],
counter_limit,
frame_count,
group->shader_permutation_index,
&bitmap_sequence_index);
#endif
}
if (shader_transparent_generic->generic.maps.count > 0)
@@ -2034,6 +2123,10 @@ void rasterizer_transparent_geometry_group_draw(
}
}
#ifdef HALO_ANDROID
if (numeric_mirrored)
rasterizer_numeric_mirror_u(vsh_constants__texanim[0]);
#endif
result = IDirect3DDevice8_SetVertexShaderConstant(
global_d3d_device,
VSH_CONSTANTS__TEXANIM_OFFSET,
@@ -2300,6 +2393,9 @@ void rasterizer_transparent_geometry_group_draw(
short layer_index;
short map_index;
long result;
#ifdef HALO_ANDROID
boolean numeric_mirrored = FALSE;
#endif
/* BUG (preserved for exact matching): January never advances layer_index, so the
* loop redraws extra layer 0 for as long as the block is non-empty (the bytes push
@@ -2374,6 +2470,18 @@ void rasterizer_transparent_geometry_group_draw(
counter_value /= frame_count;
bitmap_sequence_index = counter_value%frame_count;
#ifdef HALO_ANDROID
numeric_mirrored =
shader_transparent_chicago->chicago.type==_shader_transparent_chicago_type_2d_map &&
!TEST_FLAG(shader_transparent_chicago->chicago.flags,
_shader_transparent_flag_first_map_is_in_screenspace_bit) &&
rasterizer_numeric_mirrored_digit(
group->animation->values[function_index],
counter_limit,
frame_count,
group->shader_permutation_index,
&bitmap_sequence_index);
#endif
}
}
@@ -2548,6 +2656,10 @@ void rasterizer_transparent_geometry_group_draw(
}
}
#ifdef HALO_ANDROID
if (numeric_mirrored)
rasterizer_numeric_mirror_u(vsh_constants__texanim[0]);
#endif
result = IDirect3DDevice8_SetVertexShaderConstant(
global_d3d_device,
VSH_CONSTANTS__TEXANIM_OFFSET,
+8
View File
@@ -92,6 +92,9 @@ symbols in this file:
#include "physics/collisions.h"
#include "units/units.h"
#include "halo_vr.h"
/* port/linux/game/vr_graphics.c */
void vr_graphics_update(void);
#endif
#include "main/main.h"
#include "structures/structures.h"
@@ -948,6 +951,11 @@ void render_frame(
render.frame_index++;
render.time_delta_since_tick_sec = time_delta_since_tick_sec;
#ifdef HALO_ANDROID
/* VR: the graphics preset's effects (port/linux/game/vr_graphics.c) */
if (halo_vr_active())
vr_graphics_update();
#endif
memset(&parameters, 0, sizeof(parameters));
/* continuous between ticks (render_interpolation.c) */
parameters.game_time_sec = render_interpolation_game_time_sec(game_time_get());
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""
Ringfall: Mjolnir VR's pictures, drawn from nothing but shapes and the
port's Overpass font (port/assets/fonts, SIL Open Font License):
python3 tools/ringfall_art.py
writes
- port/android/art/android-icon.png, the launcher icon's artwork (then run
tools/android_icon.py for the app's icon);
- port/android/steam_frame_grid/*.png, the Steam library's pictures of the
Steam Frame's shortcut (port/android/STEAM_FRAME.md): library.png
(920x430), libraryp.png (600x900), library_hero.png (1920x620) and
library_logo.png (800x300, transparent).
The emblem: a tilted ring, broken where a streak falls through it.
"""
import math
from pathlib import Path
from PIL import Image, ImageDraw, ImageFilter, ImageFont
ROOT = Path(__file__).resolve().parent.parent
FONT = ROOT / "port/assets/fonts/Overpass-900.ttf"
ICON = ROOT / "port/android/art/android-icon.png"
GRID = ROOT / "port/android/steam_frame_grid"
# the launcher icon's background (tools/android_icon.py keeps it)
ICON_BACKGROUND = (30, 83, 138)
LIGHT = (240, 246, 252)
ACCENT = (255, 176, 64)
TOP = (10, 34, 58)
BOTTOM = (6, 12, 24)
SUPERSAMPLE = 4
def gradient(size, top, bottom):
width, height = size
image = Image.new("RGBA", size)
draw = ImageDraw.Draw(image)
for y in range(height):
t = y / max(1, height - 1)
draw.line([(0, y), (width, y)], fill=tuple(int(a + (b - a) * t) for a, b in zip(top, bottom)) + (255,))
return image
def emblem(size, ring=LIGHT, streak=ACCENT):
"""the emblem on a transparent square of the size given"""
scale = size * SUPERSAMPLE
layer = Image.new("RGBA", (scale, scale), (0, 0, 0, 0))
draw = ImageDraw.Draw(layer)
center = scale / 2
radius_x, radius_y = scale * 0.40, scale * 0.17
thickness = scale * 0.055
tilt = math.radians(-18)
# the ring as a band of quads along a tilted ellipse, with a gap at the
# top right where the streak passes
steps = 360
gap = (math.radians(35), math.radians(75))
def point(angle, grow):
x = (radius_x + grow) * math.cos(angle)
y = (radius_y + grow * radius_y / radius_x) * math.sin(angle)
return (center + x * math.cos(tilt) - y * math.sin(tilt), center + x * math.sin(tilt) + y * math.cos(tilt))
for step in range(steps):
a0 = 2 * math.pi * step / steps
a1 = 2 * math.pi * (step + 1.2) / steps
# (angles measured clockwise from the right, screen y down: the gap
# at the upper right is at negative angles)
if gap[0] <= (2 * math.pi - a0) % (2 * math.pi) <= gap[1]:
continue
draw.polygon([point(a0, -thickness / 2), point(a1, -thickness / 2), point(a1, thickness / 2),
point(a0, thickness / 2)], fill=ring)
# the streak: a tapering bar falling from the upper right through the gap
start = (center + scale * 0.40, center - scale * 0.42)
end = (center + scale * 0.02, center + scale * 0.12)
dx, dy = end[0] - start[0], end[1] - start[1]
length = math.hypot(dx, dy)
nx, ny = -dy / length, dx / length
head = scale * 0.050
draw.polygon([start, (end[0] + nx * head, end[1] + ny * head), (end[0] - nx * head, end[1] - ny * head)],
fill=streak)
draw.ellipse([end[0] - head, end[1] - head, end[0] + head, end[1] + head], fill=streak)
return layer.resize((size, size), Image.LANCZOS)
def italic_text(text, height, fill):
"""the text in Overpass Black, slanted, cropped to itself"""
font = ImageFont.truetype(str(FONT), int(height * SUPERSAMPLE))
left, top, right, bottom = font.getbbox(text)
width = right - left + int(height * SUPERSAMPLE * 0.4)
layer = Image.new("RGBA", (width, bottom - top + 8), (0, 0, 0, 0))
ImageDraw.Draw(layer).text((-left + int(height * SUPERSAMPLE * 0.2), -top), text, font=font, fill=fill)
slant = 0.18
layer = layer.transform((layer.width + int(layer.height * slant), layer.height), Image.AFFINE,
(1, slant, -layer.height * slant, 0, 1, 0), Image.BICUBIC)
layer = layer.crop(layer.getbbox())
return layer.resize((max(1, layer.width // SUPERSAMPLE), max(1, layer.height // SUPERSAMPLE)), Image.LANCZOS)
def logo(width):
""""RINGFALL" over "MJOLNIR VR", as wide as asked, transparent"""
title = italic_text("RINGFALL", 200, LIGHT)
title = title.resize((width, int(title.height * width / title.width)), Image.LANCZOS)
subtitle = italic_text("MJOLNIR VR", 200, ACCENT)
sub_width = int(width * 0.62)
subtitle = subtitle.resize((sub_width, int(subtitle.height * sub_width / subtitle.width)), Image.LANCZOS)
gap = int(title.height * 0.16)
image = Image.new("RGBA", (width, title.height + gap + subtitle.height), (0, 0, 0, 0))
image.alpha_composite(title, (0, 0))
image.alpha_composite(subtitle, (width - subtitle.width, title.height + gap))
return image
def shadowed(base, layer, position, blur=12):
shadow = Image.new("RGBA", base.size, (0, 0, 0, 0))
mask = Image.new("RGBA", layer.size, (0, 0, 0, 170))
mask.putalpha(layer.getchannel("A").point(lambda a: a * 2 // 3))
shadow.alpha_composite(mask, position)
base.alpha_composite(shadow.filter(ImageFilter.GaussianBlur(blur)))
base.alpha_composite(layer, position)
def faded(layer, opacity):
layer = layer.copy()
layer.putalpha(layer.getchannel("A").point(lambda a: int(a * opacity)))
return layer
def main():
# the launcher icon: the light emblem on its solid background
icon = Image.new("RGBA", (512, 512), ICON_BACKGROUND + (255,))
icon.alpha_composite(emblem(400, LIGHT, LIGHT), (56, 56))
ICON.parent.mkdir(parents=True, exist_ok=True)
icon.convert("RGB").save(ICON, optimize=True)
print(ICON.relative_to(ROOT))
GRID.mkdir(parents=True, exist_ok=True)
# the wide capsule
image = gradient((920, 430), TOP, BOTTOM)
image.alpha_composite(faded(emblem(520), 0.35), (500, -40))
shadowed(image, logo(600), (70, 120))
image.save(GRID / "library.png", optimize=True)
# the tall capsule
image = gradient((600, 900), TOP, BOTTOM)
image.alpha_composite(emblem(420), (90, 130))
shadowed(image, logo(480), (60, 560))
image.save(GRID / "libraryp.png", optimize=True)
# the hero, under the logo: the emblem large and faint at the right
image = gradient((1920, 620), TOP, BOTTOM)
image.alpha_composite(faded(emblem(900), 0.45), (1080, -140))
image.save(GRID / "library_hero.png", optimize=True)
# the logo, over the hero
image = Image.new("RGBA", (800, 300), (0, 0, 0, 0))
mark = logo(560)
image.alpha_composite(mark, ((800 - mark.width) // 2, (300 - mark.height) // 2))
image.save(GRID / "library_logo.png", optimize=True)
for name in ("library.png", "libraryp.png", "library_hero.png", "library_logo.png"):
print((GRID / name).relative_to(ROOT))
if __name__ == "__main__":
main()
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""
The Steam Frame's installer as release assets (port/android/STEAM_FRAME.md):
python3 tools/steam_frame_bundle.py <apk> <output folder> <owner/repository>
writes into the output folder
- ringfall-mjolnir-vr-steam-frame.zip: the installer (port/android/steam_frame_install.sh,
as install.sh), the APK (RingfallMjolnirVR.apk), the launcher
(steam_frame_launch.sh, as ringfall-mjolnir-vr.sh), the disc image reader (steam_frame_xiso.py, with
extract-xiso's license) and the library pictures (grid/);
- Install-Ringfall-Mjolnir-VR.desktop (port/android/steam_frame_installer.desktop): what
the player downloads and opens in Desktop Mode; it fetches the latest
release's zip from the repository named.
"""
import sys
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
ANDROID = ROOT / "port/android"
def main():
if len(sys.argv) != 4 or "/" not in sys.argv[3]:
print(__doc__.strip(), file=sys.stderr)
return 2
apk, output, repository = Path(sys.argv[1]), Path(sys.argv[2]), sys.argv[3]
output.mkdir(parents=True, exist_ok=True)
files = [
(ANDROID / "steam_frame_install.sh", "install.sh", 0o755),
(ANDROID / "steam_frame_launch.sh", "ringfall-mjolnir-vr.sh", 0o755),
(ANDROID / "steam_frame_xiso.py", "steam_frame_xiso.py", 0o755),
(ROOT / "port/third_party/extract-xiso/LICENSE.TXT", "extract-xiso-LICENSE.txt", 0o644),
(apk, "RingfallMjolnirVR.apk", 0o644),
]
files += [(picture, "grid/" + picture.name, 0o644)
for picture in sorted((ANDROID / "steam_frame_grid").glob("*.png"))]
bundle = output / "ringfall-mjolnir-vr-steam-frame.zip"
with zipfile.ZipFile(bundle, "w", zipfile.ZIP_DEFLATED) as archive:
for path, name, mode in files:
info = zipfile.ZipInfo.from_file(path, name)
info.external_attr = (0o100000 | mode) << 16
info.compress_type = zipfile.ZIP_STORED if name.endswith((".apk", ".png")) else zipfile.ZIP_DEFLATED
archive.writestr(info, path.read_bytes())
print(f"{bundle.relative_to(Path.cwd()) if bundle.is_relative_to(Path.cwd()) else bundle}")
desktop = (ANDROID / "steam_frame_installer.desktop").read_text().replace("@REPOSITORY@", repository)
installer = output / "Install-Ringfall-Mjolnir-VR.desktop"
installer.write_text(desktop)
installer.chmod(0o755)
print(installer)
return 0
if __name__ == "__main__":
sys.exit(main())