Compare commits

...
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 c5c604bb95 Import the European disc in Spanish and Italian, in the headset's language
That disc has no maps folder but one for each language, maps_es and
maps_it, and the disc image setup stopped: "The disc image has no maps
folder: it is not a Halo disc." The Quest's (XisoExtractor.java) and
the Steam Frame's (steam_frame_xiso.py, RINGFALL_LANGUAGE or the
system's) now take the folder of the device's language, else English's,
else the first, and write its language into maps/language.txt. The game
takes that as the Xbox's language (XGetLanguage) while game.language is
unset, so the menus speak it too. (Reported for ChupathingyCE, another
OpenCE fork, in its issue 145; written anew here.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:39:30 +02:00
Stefan MaierandClaude Opus 5.5 8662777f11 Merge feature/frame-updater: the Steam Frame's launcher updates the game
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:53:11 +02:00
Stefan MaierandClaude Opus 5.5 e24df8ead9 The Steam Frame's launcher updates the game
The game's own updater (the Quest's) cannot on the Steam Frame: Lepton
installs the APK in the game's folder there at every start. So the
launcher does it: while the game runs it asks GitHub for the latest
release, fetches a newer one's Steam Frame bundle into update/, checks
it (its build.txt is the release's number), and puts the APK and the
launcher in place as the next start begins, before Lepton runs the APK.
update.auto = false in config.toml stops it, as on the Quest; a build
with no number (build.txt, which the bundle now carries and the
installer copies; 0 for local builds) never looks; without the internet,
or when anything fails, nothing changes. The in-game updater no longer
starts there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:40:26 +02:00
Stefan MaierandClaude Opus 5.5 7a10a413c8 Merge feature/vr-refresh-auto: the eyes drawn one by one (multiview's doubled pictures online), the refresh rate stepped down while frames come late
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:57:18 +02:00
Stefan MaierandClaude Opus 5.5 5ce39c27a7 Roadmap: multiview to look into later; refresh rate steps in main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:57:17 +02:00
Stefan MaierandClaude Opus 5.5 67d738451c Draw the eyes one by one by default; multiview only with debug.multiview
On the Quest 3, in big online games (26-35 players), the picture was
doubled and shifted from the start as the head turned, nauseating; with
48 bots it was not, and with the eyes drawn one by one it was not online
either. Not the frame rate: one by one, online at 72 Hz, the compositor
repeated as many frames and nothing was seen. The Steam Frame had the
same doubling with multiview (2026-10-05) and draws them one by one
since. So both now do by default; debug.multiview (was vr.multiview, and
debug.multiview_steamvr) draws them at once, for finding what the shared
pass draws wrongly. The refresh rate's steps take up the extra cost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:48:27 +02:00
Stefan MaierandClaude Opus 5.5 91bdb7e67c Refresh rate: step down at one late frame in 32, show the rate it runs at
The first try stepped 90 to 80 Hz a few seconds into a 30-player game,
and no doubled picture was seen, but 2-5 stale frames a second were left
at 80: one late frame in 32 (was 16) takes it on to 72. The VR options
show the rate chosen and the one it runs at while lowered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:44:18 +02:00
Stefan MaierandClaude Opus 5.5 bf778b6d60 Step the refresh rate down while frames come late (vr.refresh_rate_auto)
In big online games a client's ticks cost 3 to 5 ms (twice what bots or
smaller games do), and the frames with a tick missed 90 Hz's 11.1 ms:
the headset showed them twice, a doubled picture when turning, with the
GPU at 60-80 %. While more than one frame in sixteen comes late, two
two-second windows running, the rate now steps down to the next the
headset offers (90 to 72 Hz); after ten windows in which hardly any
would be late at the rate above, it steps back up, never above
vr.refresh_rate. A load's long frames count for neither. On by default,
"Lower it when frames are late" on the VR options' MORE page; nothing
where the headset offers one rate (SteamVR).

Also the BUTTONS page's notes below its ten rows (the push to talk row
pushed the last one onto them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:38:59 +02:00
Stefan MaierandClaude Opus 5.5 d8a786f5a1 Merge merge/upstream-oct16: OpenCE's string conversion fix, voice packets checked, the postgame host screen's menu
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:34:03 +02:00
Stefan MaierandClaude Opus 5.5 7c294c9c39 Merge OpenCE upstream: its fix of the string conversion (taken over ours), voice packets checked, the postgame host screen's menu
Upstream fixed the Android asm conversion's string bug independently
(d9669f6ee, also from a Steam Frame crash); its converter and tests
replace ours (c85cf8ece), which they cover and extend (quoted names
honour escapes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:05:24 +02:00
bnunu f90d955527 Merge pull request #374 from chestahh/opence-multiplayer-menu-fix
Restore the main menu when leaving the postgame host screen
2026-10-16 00:13:42 -04:00
bnunu 085676d827 Merge pull request #346 from startupfoundry/arm64-guest-strings
arm64 and Android guests: string literals kept byte for byte (fixes a voice chat crash)
2026-10-15 23:37:14 -04:00
chestahh 4301ca88d6 Restore the main menu when leaving the postgame host screen
The postgame host map picker opens without menu history. Give it the
existing main_menu_if_no_history flag used by the lobby so leaving the
last screen restores the main menu instead of leaving only the background.
Update the generator and generated XML together. Preserve OpenCE's
existing handlers; no MCC code or regression-test files are included.
This local menu change does not require a network version bump.

Resolved the fork cherry-pick against OpenCE main 8102e711 by adding only
the flag override. Windows portable release build passed. Existing menu
teardown, touch and web checks returned 28 passes and 6 failures: missing
m.lib/pthread.h and Windows path separators in POSIX-oriented tests.
Android and web build targets were unavailable locally. Checked generator
round-trip stability and git diff --check. The two simple wrappers were
regenerated through ce_menus.widget_xml from their exported XML fields;
the original PC tag inputs were unavailable. No upstream runtime game
session or Linux build was run. Earlier scripted runtime verification
was performed on the GulchCE implementation, not this upstream revision.

(cherry picked from commit c811c7f527d70d5476578ed88ea0edcef2269967)
2026-10-16 02:51:54 +01:00
Stefan MaierandClaude Opus 5.5 b5cdd9268c Merge merge/upstream-net29: OpenCE network version 29
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:26:16 +02:00
Stefan MaierandClaude Opus 5.5 34b61fba31 Roadmap: OpenCE network version 29
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:06:41 +02:00
Stefan MaierandClaude Opus 5.5 87fe9fc1ad Merge OpenCE upstream: network version 29, Custom Edition maps keep their names, map sharing hardened
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:04:54 +02:00
MrBruh 8102e711c3 Merge pull request #353 from OpenCommunityEdition/testing
Merge testing into main
2026-10-14 23:21:30 +13:00
MrBruh 3e0ba55111 Keep Custom Edition maps' own names: the version goes in the game record
A game's level name is the map's own (custom_maps\hugeass), so no screen or
server browser listing shows a hash, and the game record's map version is
the map's hash's first 32 bits, which a client finds its file by. Maps are
no longer renamed; a download beside another version of its name is named
<name>_<hash>.map and listed as "(2)"; maps the last build renamed get
their names back. Network version 29: the level name and the map version
mean something else.
2026-10-14 23:08:39 +13:00
MrBruh 37ded3287c Fix harness tests for clang 23 2026-10-14 21:17:03 +13:00
Paul Hontz 9c70f5f5e3 Voice chat: a packet that is not one 20 ms frame is dropped before decoding
Every build sends one 20 ms frame per packet (voice_audio_encode). A packet
whose table of contents says otherwise, or that Opus cannot parse, is
damaged: voice_audio_play now drops it before the decoder sees it
(opus_packet_get_nb_samples), and the next frame makes up for it as lost.
Cheap defence in depth: Opus's decoder already refuses such packets, and the
crash in decode_pulses came from a valid packet and the guest's garbled
pulse cache, fixed in the assembly conversion.
2026-10-13 22:13:44 -04:00
Paul Hontz d9669f6eef The guest's assembly conversion keeps string literals byte for byte
The arm64 and Android guests are compiled for arm64_32-apple-watchos and
their Darwin assembly converted to ELF (tools/android_asm_convert.py). The
converter took a quoted .ascii/.asciz operand for a quoted symbol name and
dropped a leading "_" from it, as it does from symbols; and its quoted-name
pattern ended at an escaped quote (\"), so the "_" of a word after one was
dropped too. Clang writes byte tables as .ascii strings, so a table holding
95 ("_") lost that byte and everything after it moved down by one.

In Opus that table was CELT's pulse cache (modes.c, cache_bits50): the bits
a band may have are read off it, and shifted by a byte it lets a band have
more pulses than the PVQ tables hold. Decoding a frame from another player
then read past CELT_PVQ_U_ROW in decode_pulses (n 32, k 240) and crashed: a
client on the Steam Frame, in a CTF game on the internet. The guest's own
encoder could crash likewise in encode_pulses. The same build of Opus,
driven with the game's settings and with clean, corrupt, truncated and
random packets and voice_audio_play's loss handling, crashed within a few
thousand frames (and identically at -O0); with the fix it ran 4.8 million
decodes clean, and its encoded bytes and decoded samples are identical to a
native 64-bit build's.

The same loss hit other strings and tables of the guest: musl's iswalpha,
iswpunct and towctrans tables and iconv's charmaps; tomlc17's "_0123..."
digit sets (TOML numbers with "_"); the attract mode's "_de"/"_fr" names,
"_lan_", "_lit" and "_%s"; the progress bar's compressed monk tone and the
DirectSound effects image; and assertion and error texts. All are now
assembled as compiled.

String directives now go through unchanged, and quoted names honour
escapes. tools/test_android_asm_convert.py checks both, and a table of
every byte value through clang, the conversion and the assembler.
2026-10-13 22:13:44 -04:00
Stefan MaierandClaude Opus 5.5 238f3e2391 Merge fix/frame-voice-crash: the Android asm conversion keeps string data (the Steam Frame's voice chat crash), push to talk on any button
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:45:46 +02:00
Stefan MaierandClaude Opus 5.5 e18c2b37d6 Push to talk on any button, never sent by a grip reaching for the weapon
Push to talk becomes an action of the BUTTONS page (vr.button_talk),
on an eighth button there, the free hand's grip (its default): it can
trade places with any other action (the move stick's click, say, so
that a player holding a weapon in both hands can talk).

The free hand's grip as a button is pressed once held 0.15 s, at the
threshold halo_vr_two_hand takes the foregrip at, and not at all while
it holds the weapon's foregrip or the Warthog's wheel: taken in that
time, the press is theirs until the grip opens. Before, the runtime's
grip button (which closes before the foregrip's threshold) could open
the microphone for a frame or two as the hand took the weapon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:45:40 +02:00
Stefan MaierandClaude Opus 5.5 c85cf8ece1 Keep string data as it is in the Android asm conversion
android_asm_convert.py renamed identifiers in every directive's operands,
string data included: a string starting with "_" was taken for a quoted
symbol and lost the "_", and one with \" in it was split wrongly, so a
"_" starting a word in it was dropped too. 26 of the guest's objects had
strings or tables cut short. Among them Opus's pulse cache (cache_bits50,
one byte short from entry 338): its encoder picked more pulses than its
tables hold for some bands and crashed (the Steam Frame's crashes in
online games, with push to talk). Also musl's wide character tables,
map sharing's hashed names ("_%s"), the "_lan_" menu check, libtiff's
and the sound code's tables.

.ascii, .asciz and .string are now passed on unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:06:59 +02:00
Stefan MaierandClaude Opus 5.5 01076509dd Merge fix/vr-frame-hitches: OpenCE network version 28, a shader worker and cache, graphics "medium" by default
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:04:19 +02:00
MrBruh 6d64abdb42 Fix map sharing restart overflow and harden map validation 2026-10-13 07:43:41 +13:00
MrBruh 52e33a391e Harden map sharing and Custom Edition map loading 2026-10-12 22:26:25 +13:00
65 changed files with 3417 additions and 482 deletions

No files matched your search

+1 -1
View File
@@ -99,7 +99,7 @@ jobs:
- name: Test
run: |
python -m pytest -q tools/test_linux_port.py tools/test_host_watch_hash.py tools/test_touch_menu.py tools/harness tools/test_profile.py
python -m pytest -q tools/test_linux_port.py tools/test_android_asm_convert.py tools/test_host_watch_hash.py tools/test_touch_menu.py tools/harness tools/test_profile.py
python tools/test_light_storage.py
- name: Compile cache
+8 -3
View File
@@ -68,7 +68,8 @@ of the game.
- 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.
works. The game plays in the disc's language; on the European disc in
Spanish and Italian it takes your headset's language (else the first).
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
@@ -116,8 +117,9 @@ No computer and no keyboard needed.
library and offers to switch back to Gaming Mode.
6. Start **Ringfall: Mjolnir VR** from your library (under *Non-Steam*).
To update, open the same file in Desktop Mode again; your settings,
saves and game data stay. More about the Steam Frame:
The game updates itself: when a new version is out, it is downloaded
while you play and used from the next start on. Your settings, saves and
game data stay. More about the Steam Frame:
[port/android/STEAM_FRAME.md](port/android/STEAM_FRAME.md).
## Playing
@@ -127,6 +129,9 @@ saves and game data stay. More about the Steam Frame:
*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.
- **Voice chat** in network games: hold the grip of the hand without the
weapon to talk (not while it holds the weapon or the Warthog's wheel).
The *BUTTONS* page can put push to talk on another button.
## Problems and questions
+27 -22
View File
@@ -33,17 +33,18 @@ network games, and is never taken for one of the campaign's levels (saved
games, progress, the next level). The game engine keeps 63 characters of a
level name, so a map whose file name is longer than 51 is not listed.
A map in `custom_maps` is named for its contents once the map lists (or a
host picking it) find it: `hugeass.map` becomes `hugeass_0e2b3ee4.map`, the
first 8 hexadecimal digits of its hash after `_`, its `.bmp` and `.txt`
renamed with it (`port/linux/game/map_share_files.c`; the name is cut short
to keep the level name within 63 characters). A network game's level name
then says which version of the map it is on, a player without it downloads
it under that name (`port/linux/NETCODE.md`, "Map sharing"), and two
versions of a map keep apart. A level name without the hash (a saved last
map, a test's) finds the one map of that name in `custom_maps`. The menus
show a map without its hash, unless two versions of it are listed. Maps in
a Custom Edition install's folder keep their names.
Maps keep their names. A network game's level name is the map's own
(`custom_maps\hugeass`), and its game record's map version is the first 32
bits of the map's hash (`port/linux/game/map_share_files.c`), so a client
plays its file of that name and version, whatever else it has of the name
(`custom_edition_map_find_version`); a player without it downloads it
(`port/linux/NETCODE.md`, "Map sharing"). A download beside another version
of its name is named `hugeass_0e2b3ee4.map`, the first 8 hexadecimal digits
of its hash after `_`, and the menus show it as "Hugeass (2)". A level name
without the hash (a saved last map, a test's) finds the one map of that
name in `custom_maps`, and one with it (an earlier build's, which named
every map so) the map given its name back. Maps in a Custom Edition
install's folder are found and shared as `custom_maps`' are.
A map needs only the resource maps that hold its tags: its tag index says
which (`cache_file_resource_maps_needed`, `cache_file_formats.c`: a tag
@@ -96,7 +97,7 @@ than the damaged disc error precaching a missing map gives
the map cannot run there (Custom Edition maps turned off, the file not a
Custom Edition cache, or `bitmaps.map`, `sounds.map` and `loc.map` missing),
when its copy of a Custom Edition map is another version than the host's
(the host sends its file's header checksum as the game record's map
(the host sends its map's hash's first 32 bits as the game record's map
version, which the Xbox left 0: `cache_files_map_version`; a host that sends
0 is not checked), and when the host names a Custom Edition map as other
versions of this port do (`levels\test\<name>\<name>`) and the file is in
@@ -191,9 +192,11 @@ Loading, step by step (`custom_edition_cache_load`):
its lightmaps and their materials: every block within the BSP and aligned,
every material's uncompressed environment vertices (56 bytes) and
lightmap vertices (20 bytes) within the BSP and of the size their counts
give, lightmap vertices exactly when the lightmap has a bitmap, and every
normal, binormal, tangent and incident radiosity within ±1.005 (the game's
vertex compressor asserts on anything further out).
give, at most 64000 of each (the game's limit, to which the validator
cuts a material's counts), lightmap vertices exactly when the lightmap
has a bitmap, and every normal, binormal, tangent and incident radiosity
within ±1.005 (the game's vertex compressor asserts on anything further
out).
5. The header checksum is computed as OpenSauce computes it, before anything
changes the tag data; a mismatch is reported, not refused.
6. The tags held by resource maps are read into the tag cache after the tag
@@ -384,9 +387,11 @@ every tag as it checks an Xbox map's (`tag_validate_custom_edition_tags`,
called by `custom_edition_cache_tags_load`): every block and data within
the loaded tags and overlapping no other's, counts cut to the game's
capacities, indices, enums and tag references corrected, runtime values
reset, then the graph checks (collision BSPs, node trees, scripts' syntax).
A map whose pointers cannot be trusted is refused; what can be corrected is,
and logged. What differs from an Xbox map:
reset, then the graph checks (collision BSPs, node trees, scripts' syntax),
and a tag name that lies in any tag's block or data is emptied (what the
game writes there could take away its end). A map whose pointers cannot be
trusted is refused; what can be corrected is, and logged. What differs
from an Xbox map:
- the tag header has no vertex or index buffers, and the tag cache is the
Custom Edition one (23 MB);
@@ -563,10 +568,10 @@ every layout used was then checked against the sample maps.
- **What the validator corrects is lost.** A community map's data that the
validator corrects (an index past its block, an enum past its values) is
what the game then runs; Halo PC read past such data unchecked.
- **Ogg Vorbis sounds** are decoded when the map loads (stb_vorbis, public
domain) and encoded again as Xbox ADPCM in memory
(`port/linux/game/custom_edition_sounds.c`); one that cannot be decoded is
silenced, and still logs when played.
- **Ogg Vorbis sounds** are decoded when the map loads (stb_vorbis, with
added bounds checks: `port/third_party/stb_vorbis`) and encoded again as
Xbox ADPCM in memory (`port/linux/game/custom_edition_sounds.c`); one that
cannot be decoded is silenced, and still logs when played.
- **Models of 44 nodes or more** are drawn a part's nodes at a time, given
local nodes when they have none; one whose part's vertices name more
nodes than a part holds is refused.
+3 -1
View File
@@ -388,7 +388,9 @@ The guest is the game, the platform layer and a small runtime:
AArch64 target of clang. The options `-U__APPLE__` and
`-fno-define-target-os-macros` hide the Darwin environment.
2. `tools/android_asm_convert.py` changes the Mach-O assembly to ELF
assembly.
assembly. It renames symbols, but keeps string literals (`.ascii`,
`.asciz`) byte for byte: they hold tables as well as text
(`tools/test_android_asm_convert.py`).
3. The AArch64 assembler makes the objects.
4. `ld.lld` links the objects with `guest/guest.ld` to a static image,
`build/android/halo_guest.elf`, at a fixed address above the Xbox memory
+5 -2
View File
@@ -16,8 +16,11 @@ On the Frame, in Desktop Mode (Steam menu > Power > Switch to Desktop):
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 game updates itself: while it runs, its launcher looks for a newer
release, fetches it and puts it in place as the next start begins
(`steam_frame_launch.sh`; `update.auto = false` in `config.toml` stops it;
`launcher.log` says what it did). Opening the same file again updates it
too (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
+45 -7
View File
@@ -17,14 +17,17 @@ 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/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 |
| Rendering | The eyes drawn one by one (both at once, multiview, only with `debug.multiview`: its eyes did not always fuse, see the open points); lens flares at their depth in stereo; HUD, menus, scope, crosshair and names as layers of their own |
| Aiming | The weapon in the hand, its shots from the muzzle, laser and crosshair options |
| Body | Physical crouch (`vr.crouch_limit`) beside the crouch button, leaning (`vr.lean_limit`), melee by swinging, grenades thrown with the left hand, two-handed weapons |
| Vehicles | The Warthog's wheel turned with the hands, right trigger throttle, left brake; first-person seats; turrets with handles |
| Comfort | Snap or smooth turning, a comfort vignette in vehicles, VR options in the main and pause menus (Y), a page of every controller button |
| Menus | The PC version's menus in VR (`vr.menus`), clicked with a laser; text typed on the on-screen keyboard |
| Multiplayer | Up to date with OpenCE (network version 28, 2026-10-12): cross-play, its server browser, co-op campaign, password lobbies; join codes and the VR online list |
| Multiplayer | Up to date with OpenCE (network version 29, 2026-10-14): 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 |
| Voice chat | Push to talk on any button (`vr.button_talk`, BUTTONS page; the free hand's grip by default, sending nothing while it reaches for the foregrip or the wheel), open mic, off (2026-10-13) |
| Builds | The Android build's assembly conversion (`tools/android_asm_convert.py`) keeps string data as it is: strings and tables starting with "_" or holding \" lost a "_" (Opus's pulse cache among them: the Steam Frame's crashes in online games while talking), 2026-10-13 |
| Languages | The European disc in Spanish and Italian (a maps folder for each, `maps_es` and `maps_it`, none called `maps`) imported on the Quest and the Steam Frame: the headset's language's, its language the game's and the menus' (`maps/language.txt`, unless `game.language` is set), 2026-10-17; not yet tried with that disc |
| 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) |
@@ -40,6 +43,7 @@ named where code is taken over.
| 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; moving by swinging the arms (in place of the stick, never faster than it: online too), and a sprint by swinging them harder (faster than the game allows: offline only) | 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). Arm swinging and its offline sprint still open |
| 5 | Later: multiview again (both eyes in one pass, half the drawing's CPU cost): find why its eyes do not fuse in big online games on the Quest 3 (and now and then on the Steam Frame), then turn it back on | open: off by default since 2026-10-16 (`debug.multiview`); findings in the open points |
| 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)
@@ -150,11 +154,15 @@ COMFORT (turning, the vignettes), BUTTONS (the actions' buttons,
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
weapon, crouch, zoom, melee and push to talk each go on one of eight
buttons: A, B, X, Y, the move stick's click, the turning stick's, the
weapon hand's grip, the free hand's grip. Moving an action to a button
swaps it with the action there, so none is ever left without one.
- The free hand's grip as a button (`free_grip_down`): pressed once held
0.15 s at the foregrip's threshold, and not at all while it holds the
weapon's foregrip or the Warthog's wheel (taken in that time, the press
is theirs until the grip opens).
- The triggers, 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
@@ -512,6 +520,34 @@ see that nothing is missing or slower.
## Open points outside the order
- Multiview's doubled pictures (2026-10-16): on the developer's Quest 3,
doubled and shifted pictures from the start of big online games (26-35
players) as the head turned, nauseating; none in a local game with 48
bots; none online either with the eyes drawn one by one. Not the frame
rate: one by one, online at 72 Hz, the compositor repeated as many
frames (about 4 a second) and nothing was seen. Not the facing jumps
(respawns only) nor the camera (it moves every frame). Before: the
Steam Frame's (2026-10-05), each eye alone sharp. So the eyes are drawn
one by one by default now (`debug.multiview` for both at once). To do:
find what the shared pass draws wrongly online, e.g. by saving both
eyes' pictures in a big online game and comparing what is doubled
(sprites and effects projected by the game itself, `mv_depth`; programs
made on the shader worker; the sky rows), then turn it back on: it
halves the drawing's CPU cost.
- Big online games (2026-10-16): doubled pictures on a 32-player server,
none in a local game with 48 bots. Not the GPU (60–80 %): the ticks cost
3.3–4.6 ms as a client there (1.3–1.9 ms with bots or 16 players), so
47–70 of 150 frames with a tick missed 90 Hz's 11.1 ms (the doubling
itself was multiview's, above). In `main` 2026-10-16 (Quest 3 good):
`vr.refresh_rate_auto`
(VR options MORE, "Lower it when frames are late", on by default) steps
the rate down a step (90, 80, 72 Hz) while more than one frame in 32
comes late, and back up once they would keep up; "VR: refresh rate" in
the log. First try: 90 to 80 Hz a few seconds into a 30-player game, no
doubled picture seen; 2-5 stale frames a second left at 80, hence one
in 32 (was 16). Later:
why a client's ticks cost that much per remote player.
- Doubled pictures when turning on the developer's Quest 3 (2026-10-12):
with every build tried, back to 2026-10-06's, at 90 and at 72 Hz, even
in seconds the compositor reported no stale frame. That Quest runs
@@ -520,6 +556,8 @@ see that nothing is missing or slower.
default (smooth there). To do: try this and later builds with "high" on
a Quest 3 outside PTC (the developer's wife's), and set the default
back to "high" or "auto" if it holds.
Resolved 2026-10-16: multiview (above), not the Horizon OS PTC.
- 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.
@@ -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/RingfallMjolnirVR";
static final String SHARED_DATA_ROOT = "/home/steamos/Documents/RingfallMjolnirVR";
private File dataRoot;
private TextView status;
@@ -36,7 +36,8 @@ import java.util.zip.ZipInputStream;
* the latest release when it starts, on a thread of its own, and if it is
* newer asks the player whether to update:
*
* - Yes: the release's app (halo-android-release.zip or -debug.zip) is
* - Yes: the release's app (ringfall-mjolnir-vr-android-release.zip or
* -debug.zip) is
* downloaded and handed to Android's package installer, which replaces the
* game (closing it) and offers to open the new version.
* - No: nothing, until the next start.
@@ -45,6 +46,9 @@ import java.util.zip.ZipInputStream;
*
* Every build of main is signed with the same key (the workflow's), which an
* app must keep for Android to install a new version over it.
*
* Not on the Steam Frame: Lepton installs the APK in the game's folder there
* at every start, so its launcher updates that (steam_frame_launch.sh).
*/
final class Updater {
/* the VR port's own releases: the original's, made without VR, must
@@ -63,7 +67,8 @@ final class Updater {
static void start(Activity activity) {
File config = configFile(activity);
if (BuildConfig.HALO_BUILD_NUMBER <= 0 || config == null || !autoUpdate(config))
if (BuildConfig.HALO_BUILD_NUMBER <= 0 || config == null || !autoUpdate(config) ||
new File(LauncherActivity.SHARED_DATA_ROOT).isDirectory())
return;
new Thread(() -> {
int latest = latestRelease();
@@ -9,6 +9,7 @@ import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Copies the maps folder out of an Xbox disc image (an "xiso", or a whole
@@ -29,6 +30,12 @@ import java.util.List;
* The files are written to maps.partial first and moved into maps once they
* all are, so an interrupted extraction never leaves maps/ui.map behind.
*
* A European disc in two languages (Spanish and Italian) has no maps folder
* but one for each, maps_es and maps_it: the device's language's is taken,
* else English's, else the first, and its language written to
* maps/language.txt, which the game asks the Xbox for (game.language unset,
* port/linux/src/xbox_xapi.c): the menus in it too.
*
* Some parts of this code are copyright in@fishtank.com. This product
* includes software developed by in &lt;in@fishtank.com&gt;.
*
@@ -225,6 +232,28 @@ final class XisoExtractor {
if (entry.name.equalsIgnoreCase("maps"))
maps = entry;
}
String language = null;
if (maps == null) {
String wanted = Locale.getDefault().getLanguage();
Entry english = null, first = null;
for (Entry entry : directories) {
String code = mapsLanguage(entry.name);
if (code == null)
continue;
if (code.equals(wanted))
maps = entry;
if (code.equals("en"))
english = entry;
if (first == null)
first = entry;
}
if (maps == null)
maps = english != null ? english : first;
if (maps != null)
language = mapsLanguage(maps.name);
}
if (maps == null)
throw new ExtractException("The disc image has no maps folder: it is not a Halo disc.");
@@ -286,5 +315,22 @@ final class XisoExtractor {
throw new ExtractException("Could not move " + file.name + " into " + finished + ".");
}
partial.delete();
File languageFile = new File(finished, "language.txt");
languageFile.delete();
if (language != null) {
try (FileOutputStream out = new FileOutputStream(languageFile)) {
out.write((language + "\n").getBytes(StandardCharsets.US_ASCII));
} catch (IOException e) {
throw new ExtractException("Could not write " + languageFile + " (is the storage full?).");
}
}
}
/** a language's maps folder's language ("maps_it": "it"), else null */
static String mapsLanguage(String name) {
if (name.length() != 7 || !name.regionMatches(true, 0, "maps_", 0, 5))
return null;
String code = name.substring(5).toLowerCase(Locale.ROOT);
return Character.isLetter(code.charAt(0)) && Character.isLetter(code.charAt(1)) ? code : null;
}
}
+4
View File
@@ -237,6 +237,10 @@ cp "$BUNDLE/$LAUNCHER" "$GAME_DIR/$LAUNCHER.tmp" && chmod 755 "$GAME_DIR/$LAUNCH
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 build's number: the launcher updates from it, steam_frame_launch.sh;
# an update fetched for an older build is dropped)
cp "$BUNDLE/build.txt" "$GAME_DIR/build.txt" 2>/dev/null || rm -f "$GAME_DIR/build.txt"
rm -rf "$GAME_DIR/update"
# --- the game data
+72
View File
@@ -70,6 +70,75 @@ run()
child=0
}
# Updates (update.auto in config.toml; the game's own updater, the Quest's,
# does nothing here: Lepton installs the APK in this folder each start).
# While the game runs, the latest release is looked for; a newer one's
# bundle is fetched into update/, checked, and put in place as the next
# start begins, before Lepton runs the APK (it installs the newer one).
# A build that knows no number of its own (build.txt: a local one) never
# looks. Nothing changes without the internet, or when anything fails.
REPOSITORY="@REPOSITORY@"
UPDATE="$DATA/update"
BUILD_FILE="$DATA/build.txt"
SELF="$DATA/ringfall-mjolnir-vr.sh"
update_apply()
{
local new="$UPDATE/bundle" build
[ -f "$UPDATE/ready" ] || { rm -rf "$UPDATE"; return 0; }
build=$(cat "$new/build.txt" 2>/dev/null)
# (the launcher renamed over this one: bash goes on reading its own copy)
if cp "$new/ringfall-mjolnir-vr.sh" "$SELF.tmp" && chmod 755 "$SELF.tmp" &&
cp "$new/RingfallMjolnirVR.apk" "$APK.tmp" && chmod 666 "$APK.tmp" &&
mv -f "$SELF.tmp" "$SELF" && mv -f "$APK.tmp" "$APK" && touch "$APK" &&
cp "$new/build.txt" "$BUILD_FILE"
then
log "update: build $build put in place"
else
log "update: build $build could not be put in place"
rm -f "$SELF.tmp" "$APK.tmp"
fi
rm -rf "$UPDATE"
}
update_wanted()
{
# (update.auto = false in config.toml's [update]: never)
! awk '/^\[/ { section = $0 } section == "[update]" && /^[ \t]*auto[ \t]*=/ { print $3 }' \
"$DATA/config.toml" 2>/dev/null | grep -q false
}
update_check()
{
local installed latest
installed=$(cat "$BUILD_FILE" 2>/dev/null)
[[ "$installed" =~ ^[0-9]+$ ]] && (( installed > 0 )) || return 0
update_wanted || return 0
latest=$(curl -fsSL --max-time 20 -H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$REPOSITORY/releases/latest" |
python3 -I -c 'import json, sys; print(json.load(sys.stdin)["tag_name"].rpartition("-")[2])' 2>/dev/null)
[[ "$latest" =~ ^[0-9]+$ ]] && (( latest > installed )) || return 0
log "update: build $latest found (this is $installed), fetching it"
rm -rf "$UPDATE" && mkdir -p "$UPDATE" || return 0
if curl -fsSL --retry 2 --max-time 900 -o "$UPDATE/bundle.zip" \
"https://github.com/$REPOSITORY/releases/download/build-$latest/ringfall-mjolnir-vr-steam-frame.zip" &&
python3 -I -m zipfile -e "$UPDATE/bundle.zip" "$UPDATE/bundle" &&
[ "$(cat "$UPDATE/bundle/build.txt" 2>/dev/null)" = "$latest" ] &&
[ -s "$UPDATE/bundle/RingfallMjolnirVR.apk" ] && [ -s "$UPDATE/bundle/ringfall-mjolnir-vr.sh" ]
then
rm -f "$UPDATE/bundle.zip"
touch "$UPDATE/ready"
log "update: build $latest fetched, put in place at the next start"
else
log "update: build $latest could not be fetched"
rm -rf "$UPDATE"
fi
}
update_apply
update_check </dev/null >/dev/null 2>&1 &
updater=$!
# (a new APK is only installed on its first start, not started; and a start
# right after a game that was ended hard can fail in SteamVR, which then
# needs a moment: again, up to three times, while it ends within 30 s)
@@ -83,4 +152,7 @@ for attempt in 1 2 3; do
sleep 5
done
rm -f "$QUIT"
# (a fetch still under way is given up: the next start looks again)
pkill -P "$updater" 2>/dev/null
kill "$updater" 2>/dev/null
log "end"
+31
View File
@@ -138,6 +138,13 @@ def walk(table, offset, depth, directories, out, visited):
walk(table, right, depth + 1, directories, out, visited)
def maps_language(name):
"""a language's maps folder's language ("maps_it": "it"), else None"""
if len(name) == 7 and name[:5].lower() == "maps_" and name[5:].isalpha():
return name[5:].lower()
return None
def extract(image_path, destination):
with open(image_path, "rb") as image:
partition, root_sector, root_size = find_volume(image)
@@ -148,6 +155,24 @@ def extract(image_path, destination):
directories = []
walk(table, 0, 0, True, directories, [0])
maps = [entry for entry in directories if entry[0].lower() == "maps"]
# (a European disc in two languages, Spanish and Italian, has a maps
# folder for each, maps_es and maps_it: the one for the language
# asked for (RINGFALL_LANGUAGE, else the system's), else English's,
# else the first; its language goes into maps/language.txt, which the
# game takes as game.language's when that is unset)
language = None
if not maps:
languages = [(maps_language(entry[0]), entry) for entry in directories if maps_language(entry[0])]
wanted = (os.environ.get("RINGFALL_LANGUAGE") or os.environ.get("LC_ALL") or
os.environ.get("LC_MESSAGES") or os.environ.get("LANG") or "")[:2].lower()
for preferred in (wanted, "en"):
chosen = [pair for pair in languages if pair[0] == preferred]
if chosen:
break
chosen = chosen or languages[:1]
if chosen:
language, entry = chosen[0]
maps = [entry]
if not maps:
raise ExtractError("The disc image has no maps folder: it is not a Halo disc.")
@@ -197,6 +222,12 @@ def extract(image_path, destination):
for name, _, _ in files:
os.replace(os.path.join(partial, name), os.path.join(finished, name))
os.rmdir(partial)
language_file = os.path.join(finished, "language.txt")
if os.path.exists(language_file):
os.remove(language_file)
if language:
with open(language_file, "w") as out:
out.write(language + "\n")
except OSError as error:
raise ExtractError("Could not move the maps into %s (%s)." % (finished, error.strerror))
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The PC version's menus: ui\shell\main_menu\multiplayer_type_select\connected (tools/ce_menus.py) -->
<menus>
<widget name="main_menu/multiplayer_type_select/connected/connected_map_select_wrapper" width="640" height="480" flags="pass_unhandled_to_focused_child pause_game tag_controller_index force_handle_mouse">
<widget name="main_menu/multiplayer_type_select/connected/connected_map_select_wrapper" width="640" height="480" flags="pass_unhandled_to_focused_child pause_game main_menu_if_no_history tag_controller_index force_handle_mouse">
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_select_screen"/>
</widget>
<widget name="main_menu/multiplayer_type_select/connected/gametype_select_screen_wrapper" width="640" height="480" flags="pass_unhandled_to_focused_child pause_game">
+62 -27
View File
@@ -197,7 +197,11 @@ a map it lacks, and the game's players reach each other to send it; version
28 holds the players of a machine still downloading the map out of the game
until it has the map, keeps a multiplayer map's first scripted BSP, leaves
dynamic lights and the flashlight on past the Xbox game's player and vehicle
limits, and readies a second weapon picked up while firing.
limits, and readies a second weapon picked up while firing; version 29 names
a Custom Edition map by its own name (`custom_maps\hugeass`, which every
screen shows) and says which version it is by the map version of the game
record, its hash's first 32 bits (the header checksum before), which a
client finds its file by (below, "Map sharing").
`port/linux/network_format.txt` records the wire format of the current
version (`tools/network_format.py`): `test_linux_port.py` fails when the
@@ -339,21 +343,25 @@ a pregame keep-alive every five seconds from the host
## Map sharing
A client may join a game on a Custom Edition map it does not have
(`port/linux/game/map_share.c`, `map_share_files.c`). Every map in the
custom_maps folder is named for its contents: once the map lists (or a host
picking it) find it, `hugeass.map` is renamed `hugeass_<8 hex digits>.map`,
the first digits of its hash, with its picture and description, so a game's
level name (`custom_maps\hugeass_0e2b3ee4`) says which version of the map it
is, and two versions never meet. A file's hash is BLAKE2b's (32 bytes) of a
label, its size and the BLAKE2b hashes of its pieces (256 KiB each). The
menus show a map without its hash, unless two versions are listed.
(`port/linux/game/map_share.c`, `map_share_files.c`). A game's level name is
the map's own (`custom_maps\hugeass`), which every screen and the server
browser show, and the game record's map version is the first 32 bits of the
map's hash (`map_share_hash_version`), which says which version of the map it
is: a client plays its file of that name and version, so two versions never
meet. A file's hash is BLAKE2b's (32 bytes) of a label, its size and the
BLAKE2b hashes of its pieces (256 KiB each); a map is hashed when it is
needed (a host's game is put on it, a client's settings name its name) and
kept for the run. No map is renamed. A map downloaded beside another version
of its name is named for its hash (`hugeass_0e2b3ee4.map`), and the menus
show it as `Hugeass (2)`; a map an earlier build named so, with no map of
its name alone beside it, has its name given back as the map lists find it.
- **Asking.** A client whose settings name a map it lacks
(`cache_files_map_present`), or one whose resource maps it lacks, asks the
host over its connection (`_distributed_message_map_share_request`: the
level, and which of the map, bitmaps.map, sounds.map and loc.map it
lacks) instead of leaving, if `network.map_download` lets it; the map must
be named for its hash. Its players wait to join until the host's settings
- **Asking.** A client whose settings name a map it lacks, or lacks of
their version (`cache_files_map_present`), or one whose resource maps it
lacks, asks the host over its connection
(`_distributed_message_map_share_request`: the level, and which of the
map, bitmaps.map, sounds.map and loc.map it lacks) instead of leaving, if
`network.map_download` lets it. Its players wait to join until the host's settings
have named the map, and for a game in progress until it is downloaded; in
the lobby they join at once, and the host's start waits for the machine to
precache the map, as it waits for any machine.
@@ -365,24 +373,38 @@ menus show a map without its hash, unless two versions are listed.
answers with each file's size and hash (`_offer`, or why it does not:
sharing off, or not the game's map), and which of its other machines may
have them (`_sources`: an address on a LAN, internet play's key hash on the
internet), again every ten seconds. A map whose hash is not its name's is
not offered. The client checks that the map's hash is its name's, and
that no file is larger than the cache loader takes.
internet), again every ten seconds. A map that is not of the game's version is
not offered. The client checks that the map is of the game's version, and
that no file is larger than the cache loader takes, and refuses a map
name Windows takes for a device (`CON`, `NUL`, `COM1` and the like,
before the first dot). It connects to a LAN machine the host names only
at an address of the host's kind (a private network's if the host's is,
loopback only if the host's is; never 0.0.0.0, multicast or internet
play's), so that a host cannot send its players anywhere.
- **The swarm.** The client connects to the host and up to eight of the
others on port 5153 (of two downloading, the one of the lower machine
connects). Each end says which files it has, whole or by piece, and tells
the other of each piece it gets; the client asks for the rarest pieces
first (from a random place, so that clients ask for different pieces), two
at a time from each connection, sixteen in all, and the last few again of
whoever is quicker. The pieces' hashes come from any end that has them,
checked against the file's hash; each piece is checked against its own as
it arrives, and an end that sends a wrong one is not asked again. A machine
whoever is quicker. The pieces' hashes come from the host (from the
others it named after ten seconds without them, and from the connections
other machines made after thirty), all of them within a minute, checked
against the file's hash; a frame of none is refused. Each piece is
checked against its own as it arrives, and an end that sends a wrong one
is not asked again. A machine
sends of the game's files (its map, the resource maps the map needs, a
download's pieces) no faster than `network.map_share_upload` (the host,
1024 KB/s) or `network.map_seed_upload` (a player, 256 KB/s), round all
its connections a 16 KiB block at a time, so that the game's own traffic
keeps the rest of the upload; `network.map_seed = false` sends none. A
player that has downloaded the map sends it on at once.
- **Connections.** A machine takes up to 24 connections, of which 15 others
made (the rest are kept for its own download's), four from one address,
eight at once that have not said hello (the oldest makes way); a
connection says hello within ten seconds, and one that sends no whole
frame for 30 seconds is closed. The host takes connections only from its
game's machines' addresses.
- **Internet play.** The players of an internet game reach its host alone.
A player that shares answers the holders of the invite it joined with as
its host does (`p2p_set_answering_players`), and a downloading client asks
@@ -391,13 +413,26 @@ menus show a map without its hash, unless two versions are listed.
the host, in one topic of the invite's token). Its map sharing connection
then goes through their tunnel, as the game's connections go.
- **Writing.** A download is written to `<file>.part` beside its file in
custom_maps, and renamed when every piece is had. A part file found again
is checked piece by piece on the hashing thread and taken up where it was.
custom_maps, each piece at its place as it comes (the file is not made its
size at once), and renamed when every piece is had. A part file found
again is checked piece by piece on the hashing thread and taken up where
it was. A download that stops deletes a part file that holds none of its
file, and keeps one map's part file at most (the one kept before is
deleted), so that a host cannot fill a disk with offers.
The client checks the map again once it has it (`custom_edition_cache_present`),
and tells the host its progress every two seconds (`_progress`); the host
does not drop a machine downloading (and for 20 seconds after) for adding
no players or loading slowly. A download that brings nothing for a minute,
or a host that does not answer in 30 seconds, leaves the game, saying why.
and deletes the files the download put in custom_maps if it cannot be
played (or if one of them could not be put there), so that none is taken
for present; it tells the host its progress every two seconds
(`_progress`). The host does not drop a machine downloading (and for 20
seconds after) for adding no players or loading slowly, while what it says
it has grew by a piece (or to the whole, and no more than the files
offered) in the last 80 seconds, counted from the first offer of the
game's map however often, and for whatever level, the machine asks again.
It takes a machine's requests five seconds apart at most: one that comes
sooner waits its turn (the last of them kept). A connection forgets which
pieces the other end has of the download's files when the download ends.
A download that brings nothing for a minute, or a host that does not
answer in 30 seconds, leaves the game, saying why.
- The download's progress is drawn at the top of the screen.
## Stages
+16 -12
View File
@@ -778,7 +778,9 @@ on the internet. It also downloads the resource maps that the map needs
the top of the screen shows the progress. In the lobby, the game starts when
every player has the map. If the game already operates, you join it when
the download is complete. If no player sends any of the map for a minute,
you leave the game, and the main menu tells you why.
you leave the game, and the main menu tells you why. If the downloaded map
cannot be played, the game deletes the files that it downloaded, and you
leave the game.
All the players help (HELP SEND MAPS in Settings > Network Setup > Map
Sharing, `network.map_seed`), as the peers of a torrent do:
@@ -792,16 +794,18 @@ UPLOAD LIMIT in Settings > Network Setup > Map Sharing). Set these lower on
a slow connection. DOWNLOAD MAPS there (`network.map_download`) turns
downloading off: you then leave a game whose map you do not have.
The name of each map in `custom_maps` ends with the start of its hash: when
the map lists find `hugeass.map`, the game renames it `hugeass_0e2b3ee4.map`
(and its picture and description). Thus two versions of a map have different
names, and a game always loads the version of the host. The menus show the
map without the hash, and with it only when you have two versions of the
map. The downloaded map has the same name. A download that stops leaves a
`.part` file in `custom_maps`; the next download of that map continues from
it. Maps in the folder of a Halo Custom Edition install
(`paths.custom_edition`) keep their names, and the host does not share them.
Copy a map to `custom_maps` to share it.
Maps keep their names. A game is on a map by its name (`hugeass`), and says
which version of it by the start of the map's hash, so a game always loads
the version of the host, whatever your copy is called. A downloaded map
takes its own name (`hugeass.map`); if you have another version of that
name, the download is named with the start of its hash
(`hugeass_0e2b3ee4.map`), and the menus show it as "Hugeass (2)". A map that
an earlier build renamed this way gets its name back, unless you have a map
of that name. A download that stops leaves a `.part` file in `custom_maps`
(only of the last map whose download stopped, and only if it holds part of
the map); the next download of that map continues from it. The host shares
maps in the folder of a Halo Custom Edition install (`paths.custom_edition`)
too; downloads go to `custom_maps`.
Over the internet, the players of a game connect only to the host. To send
each other a map, a player answers the other players of the game (who have
@@ -1153,7 +1157,7 @@ Other changes:
| `rasterizer/xbox/rasterizer_xbox_widgets.c`, `objects/widgets/light_volumes.c` | A light volume's sprites (a plasma bolt's glow, a couple of hundred) are one draw. |
| `rasterizer/rasterizer.h` | A frame holds 2048 transparent geometry groups, not 384 (`include/halo_port_capacity.h`): a co-op game with many added enemies asked for more, and the Xbox's pool drew the frame without the rest. |
| `structures/structure_render.c` | The structure's visible surfaces are put in order by a radix sort, not `qsort_4byte`: the same order. |
| `cache/cache_files.c`, `networking/network_client_manager.c`, `networking/network_server_manager.c`, `networking/network_server_message_handler.c`, `main/main.c`, `interface/interface.c` | Map sharing (`game/map_share.c`): a client that does not have the game's Custom Edition map asks the host for it instead of leaving, is not given time to precache it until it has it, and adds its players to a game in progress only then; the host names the game's map for its hash, takes the requests from any machine in the game, lobby too, and does not drop a machine that downloads; the transfers run each frame, and their progress is drawn over the screen. |
| `cache/cache_files.c`, `networking/network_client_manager.c`, `networking/network_server_manager.c`, `networking/network_server_message_handler.c`, `main/main.c`, `interface/interface.c` | Map sharing (`game/map_share.c`): a client that does not have the game's Custom Edition map asks the host for it instead of leaving, is not given time to precache it until it has it, and adds its players to a game in progress only then; the host gives the game's map its own name and its version (its hash's start), takes the requests from any machine in the game, lobby too, and does not drop a machine that downloads; the transfers run each frame, and their progress is drawn over the screen. |
| `cutscene/cinematics.c` | A scenario's titles (some Custom Edition maps show their name for the whole game) are not drawn over a menu, as the letterbox is not: the screen's overlays are drawn after the players' menus. |
| `tag_files/tag_groups.h` | `TAG_BLOCK_GET_ELEMENT` returns an element in the block without a call, and calls `tag_block_get_element_with_size` for anything else. |
| `input/input_xbox.c`, `physics/collision_usage.c` | The clock is read only for a button held, and for collision tests only when their times are shown: a read of the clock is slow in a browser. |
+6 -1
View File
@@ -159,6 +159,11 @@ rasterizer_geometry.h */
#define TRIANGLE_BUFFER_TYPE_PRECOMPILED_STRIP 1
/* strips index vertices with 16 bits */
#define MAXIMUM_VERTICES_PER_BUFFER 0xFFFF
/* the most vertices a structure BSP material has (the game's
MAXIMUM_VERTICES_PER_STRUCTURE_MATERIAL, structures.h): the validator cuts
a count past it, which would move where the lightmap vertices that follow
are read from. The most of the maps examined is 9956 */
#define MAXIMUM_VERTICES_PER_STRUCTURE_MATERIAL 64000
/* gbxmodel (OpenSauce model_definitions.hpp: gbxmodel_definition 0xE8, its
markers 0x40 and their instances 0x20, nodes 0x9C, regions 0x4C,
@@ -1817,7 +1822,7 @@ static enum cache_file_status structure_bsp_geometry_verify(
/* the counts bound the size, which bounds the counts */
if (read_s16(material + STRUCTURE_BSP_MATERIAL_VERTEX_TYPE_OFFSET) != VERTEX_TYPE_ENVIRONMENT_UNCOMPRESSED ||
vertex_count < 0 ||
vertex_count > MAXIMUM_VERTICES_PER_BUFFER ||
vertex_count > MAXIMUM_VERTICES_PER_STRUCTURE_MATERIAL ||
lightmap_vertex_count != (has_bitmap ? vertex_count : 0) ||
vertices_size != vertex_count * STRUCTURE_BSP_ENVIRONMENT_VERTEX_BYTES +
lightmap_vertex_count * STRUCTURE_BSP_LIGHTMAP_VERTEX_BYTES ||
+165 -35
View File
@@ -211,9 +211,9 @@ static boolean maps_folder_has(
/* The custom_maps folder's one map named <name>_<hash>.map, for a name
without the hash its file has: its file name (without the extension) in
`hashed` (MAP_PATH_SIZE characters). Maps there are named for their hash as
the map lists find them (map_share_name_map), so a level name of before (a
saved last map, a test's) still finds its map; FALSE if there are more. */
`hashed` (MAP_PATH_SIZE characters). A download beside another version of
its name is named so (map_share.c), and a build of before named every map so
(map_share_map_found gives the names back); FALSE if there are more. */
static boolean maps_folder_hashed_name(
char const *name,
char *hashed)
@@ -258,10 +258,9 @@ static boolean maps_folder_hashed_name(
return count == 1;
}
/* the file that holds the map `map_name` names (a level name or a file
name): <custom maps folder>\<name>.map (custom_maps' <name>_<hash>.map for
a name without the hash, if it has one map of that name) */
static boolean custom_edition_map_path(
/* the file the map `map_name` names (a level name or a file name),
whatever version a network game is on (custom_edition_map_file_named) */
static boolean custom_edition_map_named(
char const *map_name,
char *path)
{
@@ -276,9 +275,83 @@ static boolean custom_edition_map_path(
return TRUE;
}
}
if (maps_folder_hashed_name(name, hashed) && maps_folder_has(maps_folder(0), hashed, MAP_FILE_EXTENSION, path))
{
return TRUE;
}
/* (a level name of a build of before, the map named for its hash: the
map given its name back, if the hash is still its) */
if (map_share_name_hashed(name) && strlen(name) < MAP_PATH_SIZE)
{
char base[MAP_PATH_SIZE];
char digits[MAP_SHARE_NAME_DIGITS + 1];
byte hash[MAP_SHARE_HASH_BYTES];
short length = map_share_name_base_length(name);
return maps_folder_hashed_name(name, hashed) &&
maps_folder_has(maps_folder(0), hashed, MAP_FILE_EXTENSION, path);
memcpy(base, name, (size_t)length);
base[length] = 0;
if (maps_folder_has(maps_folder(0), base, MAP_FILE_EXTENSION, path) && map_share_map_hash(path, hash))
{
map_share_hash_digits(hash, digits);
if (!strcmp(name + length + 1, digits))
return TRUE;
}
}
return FALSE;
}
/* the file of a network game's map of its version, as last looked for: the
game asks for the map's file many times a frame, so it is looked for again
a second later, or once the maps have changed (custom_edition_map_paths_forget) */
static struct
{
char name[MAP_PATH_SIZE];
unsigned long version;
boolean found;
char path[MAP_PATH_SIZE];
unsigned long looked_at;
} custom_edition_version_lookup;
void custom_edition_map_paths_forget(
void)
{
custom_edition_version_lookup.looked_at = 0;
}
/* the file that holds the map `map_name` names (a level name or a file
name): in a network game on that map, the file of its version, and none
if this machine has none (a download's, or another version's, is not
loaded in its place); else custom_edition_map_named's */
static boolean custom_edition_map_path(
char const *map_name,
char *path)
{
unsigned long version;
if (map_share_game_map_version(map_name, &version) && version)
{
char const *name = tag_name_strip_path(map_name);
unsigned long now = GetTickCount() | 1;
if (custom_edition_version_lookup.looked_at && now - custom_edition_version_lookup.looked_at < 1000 &&
custom_edition_version_lookup.version == version && !csstrcasecmp(custom_edition_version_lookup.name, name))
{
if (custom_edition_version_lookup.found)
strcpy(path, custom_edition_version_lookup.path);
return custom_edition_version_lookup.found;
}
if (strlen(name) >= MAP_PATH_SIZE)
return FALSE;
custom_edition_version_lookup.found = custom_edition_map_find_version(map_name, version, path);
strcpy(custom_edition_version_lookup.name, name);
strcpy(custom_edition_version_lookup.path, custom_edition_version_lookup.found ? path : "");
custom_edition_version_lookup.version = version;
custom_edition_version_lookup.looked_at = now;
return custom_edition_version_lookup.found;
}
return custom_edition_map_named(map_name, path);
}
/* A cache's resource map of `type`, bitmaps.map and so on, in the first maps
@@ -633,6 +706,69 @@ boolean custom_edition_map_file_path(
return custom_edition_map_path(map_name, path);
}
boolean custom_edition_map_file_named(
char const *map_name,
char *path)
{
return custom_edition_map_named(map_name, path);
}
boolean custom_edition_map_find_version(
char const *level_name,
unsigned long version,
char *path)
{
char const *name = tag_name_strip_path(level_name);
char pattern[MAP_PATH_SIZE];
char digits[MAP_SHARE_NAME_DIGITS + 1];
WIN32_FIND_DATAA data;
HANDLE find;
unsigned long found_version;
boolean found = FALSE;
short folder;
for (folder = 0; folder < NUMBER_OF_MAPS_FOLDERS; folder++)
{
if (maps_folder_has(maps_folder(folder), name, MAP_FILE_EXTENSION, path) &&
map_share_map_version(path, &found_version) && found_version == version)
{
return TRUE;
}
}
/* (custom_maps' other versions of the name: the one whose name ends in
the version's digits, if its hash is that) */
if (strlen(maps_folder(0)) + strlen(name) + 1 + MAP_SHARE_NAME_DIGITS + strlen(MAP_FILE_EXTENSION) >= MAP_PATH_SIZE)
return FALSE;
sprintf(digits, "%08lx", version);
sprintf(pattern, "%s%s_*%s", maps_folder(0), name, MAP_FILE_EXTENSION);
find = FindFirstFileA(pattern, &data);
if (find == INVALID_HANDLE_VALUE)
return FALSE;
do
{
char file_name[sizeof(data.cFileName) + 1];
size_t length;
memcpy(file_name, data.cFileName, sizeof(data.cFileName));
file_name[sizeof(data.cFileName)] = 0;
length = strlen(file_name);
if ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) || length <= strlen(MAP_FILE_EXTENSION))
continue;
file_name[length - strlen(MAP_FILE_EXTENSION)] = 0;
/* (version 1 is also a hash's of digits 00000000: map_share_hash_version) */
if (!map_share_name_hashed(file_name) || (size_t)map_share_name_base_length(file_name) != strlen(name) ||
_strnicmp(file_name, name, strlen(name)) || (version != 1 && strcmp(file_name + strlen(name) + 1, digits)))
{
continue;
}
sprintf(path, "%s%s%s", maps_folder(0), file_name, MAP_FILE_EXTENSION);
found = map_share_map_version(path, &found_version) && found_version == version;
} while (!found && FindNextFileA(find, &data));
CloseHandle(find);
return found;
}
boolean custom_edition_resource_map_file_path(
short type,
char *path)
@@ -682,30 +818,23 @@ boolean custom_edition_map_file_present(
return custom_edition_map_path(map_name, path);
}
unsigned long custom_edition_map_checksum(
unsigned long custom_edition_map_version(
char const *level_name)
{
char path[MAP_PATH_SIZE];
struct custom_edition_file file;
struct cache_file_identity identity;
unsigned long checksum = 0;
unsigned long version = 0;
if (custom_edition_map_path(level_name, path) && custom_edition_file_open(&file, path))
{
if (cache_file_identify(&file.source, &identity) == _cache_file_status_ok &&
identity.format == _cache_file_format_custom_edition_cache)
{
checksum = identity.checksum;
}
custom_edition_file_close(&file);
}
if (map_share_level_name_version(level_name, &version))
return version;
if (!custom_edition_map_path(level_name, path) || !map_share_map_version(path, &version))
return 0;
return checksum;
return version;
}
boolean custom_edition_cache_present(
char const *level_name,
unsigned long checksum,
unsigned long version,
char *message,
long message_size)
{
@@ -718,8 +847,19 @@ boolean custom_edition_cache_present(
short folder;
short type;
if (!custom_edition_map_path(level_name, path))
/* (another version's tags are not the host's: the objects the host sends
would be other things here) */
if (version ? !custom_edition_map_find_version(level_name, version, path) :
!custom_edition_map_path(level_name, path))
{
if (version && custom_edition_map_named(level_name, path))
{
error(_error_silent, "custom edition: '%s' is another version of the host's map (the host's %08lx)", path,
version);
snprintf(message, message_size,
"Your %.64s.map is a different version from the host's. Copy the host's into custom_maps.", name);
return FALSE;
}
snprintf(message, message_size, "You don't have the map %.*s.map. If you have it, copy it into custom_maps.",
(int)MIN(64, map_share_name_base_length(name)), name);
return FALSE;
@@ -752,16 +892,6 @@ boolean custom_edition_cache_present(
snprintf(message, message_size, "Your %.64s.map can't be played (debug.txt says why).", name);
return FALSE;
}
/* (another version's tags are not the host's: the objects the host sends
would be other things here) */
if (checksum && identity.checksum != checksum)
{
error(_error_silent, "custom edition: '%s' is another version of the host's map (checksum %08lX, the host's %08lX)",
path, (unsigned long)identity.checksum, checksum);
snprintf(message, message_size,
"Your %.64s.map is a different version from the host's. Copy the host's into custom_maps.", name);
return FALSE;
}
for (type = _resource_map_bitmaps; type < NUMBER_OF_RESOURCE_MAP_TYPES; type++)
{
char const *resource_name = resource_map_type_describe((enum resource_map_type)type);
+32 -13
View File
@@ -80,18 +80,37 @@ boolean custom_edition_cache_playable(
Custom Edition maps folders, whatever it holds. */
boolean custom_edition_map_file_present(
char const *map_name);
/* The checksum in the header of the level's map file, which differs between
versions of a map; 0 if it has no file or the file is not a Custom Edition
cache. */
unsigned long custom_edition_map_checksum(
/* The version of the level's map, a network game record's
(map_share_hash_version): of the map a host's game was given
(map_share_level_name_choose), else of the level's file; 0 if it has none. */
unsigned long custom_edition_map_version(
char const *level_name);
/* The file of the map `map_name` (a level name or a file name) in the
Custom Edition maps folders, into `path` (256 characters): the custom_maps
folder's map named for its hash (<name>_<hash>.map, map_share.h) for a name
without it, if there is one map of that name. FALSE if none. */
Custom Edition maps folders, into `path` (256 characters): in a network game
on that level, the file of the game's version
(custom_edition_map_find_version), else custom_edition_map_file_named's.
FALSE if none. */
boolean custom_edition_map_file_path(
char const *map_name,
char *path);
/* ... a network game's map's file looked for again at once: the maps
changed (custom_edition_maps_look_again) */
void custom_edition_map_paths_forget(
void);
/* ... the file the name names, whatever version a network game is on:
<name>.map; for a name without a hash, the custom_maps folder's map of that
name and a hash (<name>_<hash>.map, another version's) if it is the one;
for a name with one (a build of before's level name), the map of the name
without it, if the hash is its. */
boolean custom_edition_map_file_named(
char const *map_name,
char *path);
/* ... the file of the level's name and `version` (map_share_hash_version's):
<name>.map in a maps folder, or the custom_maps folder's <name>_<hash>.map. */
boolean custom_edition_map_find_version(
char const *level_name,
unsigned long version,
char *path);
/* The resource map of `type` (enum resource_map_type: bitmaps.map,
sounds.map, loc.map) in the first maps folder that has it, into `path` (256
characters); FALSE if none has it. */
@@ -107,14 +126,14 @@ unsigned long custom_edition_map_resource_maps_needed(
char const *custom_edition_cache_loaded_path(
void);
/* Whether this machine can play the level `level_name` (custom_maps\<name>),
as a network game's client must: its file present and a Custom Edition
cache of the host's version (`checksum`, custom_edition_map_checksum's on
the host; 0 for any), Custom Edition maps able to run, and the resource maps
it needs present. When not, `message` (`message_size` characters) says why, for the
player, and what to do. */
as a network game's client must: a file of it of the host's version
(`version`, custom_edition_map_version's on the host; 0 for any), a Custom
Edition cache, Custom Edition maps able to run, and the resource maps it
needs present. When not, `message` (`message_size` characters) says why, for
the player, and what to do. */
boolean custom_edition_cache_present(
char const *level_name,
unsigned long checksum,
unsigned long version,
char *message,
long message_size);
+74 -7
View File
@@ -65,6 +65,14 @@ component this far past 1 stays within that after the packing's rounding
(every vector of the maps examined is within 1.0001) */
#define MAXIMUM_COMPRESSIBLE_COMPONENT 1.005f
/* the most room a map's models, or a structure BSP, are given for their
compressed vertices and strip indices: several times what the largest maps
take (some 470000 model vertices, 15 MB), and far less than the 4 GB past
which the sizes would wrap. Parts and materials may name the same vertices,
each checked alone (cache_file_formats.c), so the file's size does not bound
what they add up to */
#define MAXIMUM_GEOMETRY_BYTES 0x8000000
/* ---------- structures */
/* this build's model geometry and part, as models.c and rasterizer.c each
@@ -162,6 +170,8 @@ struct model_geometry_totals
long largest_part_vertex_count;
/* the parts of models of more nodes than the renderer skins at once */
long many_node_part_count;
/* the room the parts' compressed vertices and strips take */
unsigned long geometry_bytes;
};
struct custom_edition_geometry_globals
@@ -414,6 +424,32 @@ static void custom_edition_model_part_shaders_bound(
return;
}
/* Adds to `bytes` the room for `vertex_count` vertices of `vertex_size`
bytes and `index_count` strip indices, unless the total would pass
MAXIMUM_GEOMETRY_BYTES. */
static boolean geometry_bytes_add(
unsigned long *bytes,
unsigned long vertex_count,
unsigned long vertex_size,
unsigned long index_count)
{
unsigned long added;
if (vertex_count > MAXIMUM_GEOMETRY_BYTES / vertex_size ||
index_count > MAXIMUM_GEOMETRY_BYTES / sizeof(word))
{
return FALSE;
}
added = vertex_count * vertex_size + index_count * sizeof(word);
if (added > MAXIMUM_GEOMETRY_BYTES - *bytes)
{
return FALSE;
}
*bytes += added;
return TRUE;
}
/* Whether this build can draw `model`: every part must pass
custom_edition_model_part_verify, and a model of more nodes than the
renderer skins at once must have local nodes (each part few enough). Adds
@@ -469,6 +505,21 @@ static boolean custom_edition_model_verify(
name);
return FALSE;
}
if (!geometry_bytes_add(
&totals->geometry_bytes,
(unsigned long)part->vertex_count,
(unsigned long)rasterizer_geometry_get_vertex_size(_rasterizer_vertex_type_model_compressed),
(unsigned long)part->strip_triangle_count + 2))
{
error(
_error_silent,
"custom edition: the models' parts, to part %ld of geometry %ld of the model '%s', take more than 0x%X bytes of geometry",
part_index,
geometry_index,
name,
MAXIMUM_GEOMETRY_BYTES);
return FALSE;
}
totals->part_count++;
if (model_has_many_nodes(model))
totals->many_node_part_count++;
@@ -766,7 +817,7 @@ boolean custom_edition_models_convert(
byte *model_data)
{
struct custom_edition_geometry_globals *globals = &custom_edition_geometry_globals;
struct model_geometry_totals totals = { 0, 0, 0, 0 };
struct model_geometry_totals totals = { 0, 0, 0, 0, 0, 0 };
struct model_vertex_uncompressed *scratch;
struct model_vertex_compressed *vertices;
struct model *model;
@@ -805,10 +856,9 @@ boolean custom_edition_models_convert(
globals->model_parts = malloc((totals.part_count + 1) * sizeof(*globals->model_parts));
globals->palettes = malloc((totals.many_node_part_count + 1) * sizeof(*globals->palettes));
globals->model_geometry = geometry_allocate(
totals.vertex_count * rasterizer_geometry_get_vertex_size(_rasterizer_vertex_type_model_compressed) +
totals.strip_index_count * sizeof(*strips) + 1,
&globals->model_geometry_contiguous);
/* (totals.geometry_bytes is the vertices' and strips' room, at most
MAXIMUM_GEOMETRY_BYTES) */
globals->model_geometry = geometry_allocate(totals.geometry_bytes + 1, &globals->model_geometry_contiguous);
scratch = malloc(totals.largest_part_vertex_count * sizeof(*scratch) + 1);
if (!globals->model_parts || !globals->model_geometry || !globals->palettes || !scratch)
{
@@ -910,6 +960,7 @@ boolean custom_edition_structure_bsp_load(
unsigned long vertices_size = 0;
unsigned long vertices_offset = 0;
boolean success = TRUE;
boolean too_large = FALSE;
long lightmap_index;
assert(!globals->structure_bsp);
@@ -939,11 +990,25 @@ boolean custom_edition_structure_bsp_load(
material->vertices.count = vertex_count;
material->lightmap_vertices.type = _rasterizer_vertex_type_environment_lightmap_compressed;
material->lightmap_vertices.count = lightmap_vertex_count;
vertices_size += vertex_count * vertex_size + lightmap_vertex_count * lightmap_vertex_size;
/* (every material is still emptied when the sizes are past the
most there is room for, so that the failure releases nothing) */
if (!too_large &&
(!geometry_bytes_add(&vertices_size, (unsigned long)vertex_count, (unsigned long)vertex_size, 0) ||
!geometry_bytes_add(&vertices_size, (unsigned long)lightmap_vertex_count, (unsigned long)lightmap_vertex_size, 0)))
{
too_large = TRUE;
}
}
}
globals->structure_bsp = structure_bsp;
if (too_large)
{
error(_error_silent, "custom edition: the structure BSP's materials take more than 0x%X bytes of vertices",
MAXIMUM_GEOMETRY_BYTES);
custom_edition_structure_bsp_unload();
return FALSE;
}
/* (in Xbox memory either way: the material points at them by Xbox address) */
globals->structure_bsp_vertices = geometry_allocate(vertices_size + 1,
&globals->structure_bsp_vertices_contiguous);
@@ -997,7 +1062,9 @@ void custom_edition_structure_bsp_unload(
if (globals->structure_bsp)
{
structure_bsp_buffers_release(globals->structure_bsp);
geometry_free(globals->structure_bsp_vertices, globals->structure_bsp_vertices_contiguous);
/* (none when the load failed before they were made) */
if (globals->structure_bsp_vertices)
geometry_free(globals->structure_bsp_vertices, globals->structure_bsp_vertices_contiguous);
globals->structure_bsp = NULL;
globals->structure_bsp_vertices = NULL;
}
+75 -22
View File
@@ -15,11 +15,12 @@ A map's level name is custom_maps\<name> (CUSTOM_EDITION_LEVEL_NAME_PREFIX),
which the cache loader reads from the custom maps folders alone: a map named
as one of the game's own levels (a30.map, bloodgulch.map) is listed and
played as itself. The game engine keeps 63 characters of a level name, so a
map whose name is longer than 51 is skipped. A map in the custom_maps folder
is named for its hash (<name>_<hash>.map) once it has been found: a network
game's level name then says which version of the map it is on, and a player
without it can download it (map_share.h). The menus show the name without
the hash, unless two versions of a map are listed.
map whose name is longer than 51 is skipped. A map keeps its name: a network
game names it so, and says which version it is on by its hash (map_share.h).
A downloaded map of a name another version has is named <name>_<hash>.map;
the menus show it without the hash, with (2), (3) and so on after the first
of the name. A map a build of before named for its hash has its name given
back (map_share_map_found).
Each map can have, beside it in its folder:
- <name>.bmp, its picture (bmp_files.c; for a map named for its hash, a
@@ -124,7 +125,8 @@ struct custom_edition_map
/* saved_game_file_remember_last_used_multiplayer_map writes
MAXIMUM_FILENAME_LENGTH+1 characters of a level name */
char level_name[MAXIMUM_FILENAME_LENGTH + 1];
wchar_t display_name[MAXIMUM_MAP_NAME_LENGTH + 1];
/* (and room for a version's number, " (2)") */
wchar_t display_name[MAXIMUM_MAP_NAME_LENGTH + 8];
wchar_t description[MAXIMUM_DESCRIPTION_LENGTH + 1];
boolean picture_read;
struct bitmap_data *picture;
@@ -202,16 +204,15 @@ static short stock_map_index(
}
/* A map's menu name: Halo PC's name for its own maps, otherwise the file
name (without the hash it ends in, unless `hash`) with underscores as spaces
and words capitalized ("the_bay_of_pigs" becomes "The Bay Of Pigs"). */
name (without a hash it ends in) with underscores as spaces and words
capitalized ("the_bay_of_pigs" becomes "The Bay Of Pigs"). */
static void display_name_make(
char const *name,
boolean hash,
wchar_t *display_name)
{
boolean word_start = TRUE;
short index = hash ? NONE : stock_map_index(name);
short length = hash ? (short)csstrlen(name) : map_share_name_base_length(name);
short index = stock_map_index(name);
short length = map_share_name_base_length(name);
if (index != NONE)
{
@@ -387,11 +388,11 @@ static void custom_edition_map_add(
csstrcpy(map->name, name);
csstrncpy(map->folder, folder, sizeof(map->folder) - 1);
csprintf(map->level_name, "%s%s", CUSTOM_EDITION_LEVEL_NAME_PREFIX, name);
display_name_make(name, FALSE, map->display_name);
display_name_make(name, map->display_name);
custom_edition_map_description_read(map);
/* (named for its hash, renamed: map_share_files.c) */
if (!map_share_name_hashed(name))
map_share_name_map(folder, name);
/* (named for its hash by a build of before: its name given back,
map_share_files.c) */
map_share_map_found(folder, name);
return;
}
@@ -405,24 +406,49 @@ static int custom_edition_map_compare(
((struct custom_edition_map const *)second)->name);
}
/* Two versions of a map (its name the same but for its hash) listed: each
shown with its hash, to tell them apart. The maps are sorted by name. */
/* Versions of a map listed (its name the same but for the hash a version's
file name ends in, a download's beside another of its name): the first in
the order the maps are sorted by name (the one of the name alone, which
sorts before them) shown by its name, the others with (2), (3) and so on. */
static void custom_edition_maps_tell_versions_apart(
struct custom_edition_map *maps,
short count)
{
short index;
for (index = 0; index + 1 < count; index++)
for (index = 0; index < count; index++)
{
struct custom_edition_map *map = &maps[index];
struct custom_edition_map *next = &maps[index + 1];
short length = map_share_name_base_length(map->name);
short number = 1;
short other;
if (length == map_share_name_base_length(next->name) && !_strnicmp(map->name, next->name, length))
if (!map_share_name_hashed(map->name))
continue;
for (other = 0; other < count; other++)
{
display_name_make(map->name, TRUE, map->display_name);
display_name_make(next->name, TRUE, next->display_name);
struct custom_edition_map const *version = &maps[other];
if (other == index || map_share_name_base_length(version->name) != length ||
_strnicmp(version->name, map->name, length))
{
continue;
}
if (other < index)
number++;
}
if (number > 1)
{
char suffix[8];
short end = 0;
short character;
while (map->display_name[end])
end++;
csprintf(suffix, " (%d)", (int)MIN(number, 999));
for (character = 0; suffix[character] && end + 1 < NUMBEROF(map->display_name); character++)
map->display_name[end++] = (wchar_t)suffix[character];
map->display_name[end] = 0;
}
}
}
@@ -641,6 +667,8 @@ void custom_edition_maps_look_again(
void)
{
custom_edition_maps_globals.looked_for = FALSE;
/* (a network game's map's file, found again) */
custom_edition_map_paths_forget();
return;
}
@@ -659,6 +687,7 @@ short custom_edition_maps_display_index(
{
struct custom_edition_maps_globals *globals = &custom_edition_maps_globals;
char const *name = tag_name_strip_path(level_name);
char path[MAP_SHARE_PATH_SIZE];
short map_index;
/* (only a custom_maps\ name is a CE map's; any other is the game's own,
@@ -673,6 +702,30 @@ short custom_edition_maps_display_index(
{
custom_edition_maps_look_for();
}
/* (the map by its file: a network game's of its version, which may be
one of another name, <name>_<hash>.map, shown with its number) */
if (custom_edition_map_file_path(level_name, path))
{
char const *file_name = tag_name_strip_path(path);
short length = (short)csstrlen(file_name) - (short)(sizeof(".map") - 1);
for (map_index = 0; length > 0 && map_index < globals->map_count; map_index++)
{
if ((short)csstrlen(globals->maps[map_index].name) == length &&
!_strnicmp(globals->maps[map_index].name, file_name, length))
{
return FIRST_DISPLAY_INDEX + map_index;
}
}
for (map_index = 0; length > 0 && map_index < globals->campaign_count; map_index++)
{
if ((short)csstrlen(globals->campaigns[map_index].name) == length &&
!_strnicmp(globals->campaigns[map_index].name, file_name, length))
{
return FIRST_CUSTOM_CAMPAIGN_DISPLAY_INDEX + map_index;
}
}
}
for (map_index = 0; map_index < globals->map_count; map_index++)
{
if (!csstrcasecmp(globals->maps[map_index].name, name))
+9 -3
View File
@@ -8,7 +8,7 @@ This build plays Xbox ADPCM only, mono at 22 kHz or stereo at 22 or 44 kHz
(sound_manager.c), and refuses any other sound each time it is played.
Halo PC also has Ogg Vorbis (music, dialogue, the announcer), uncompressed
samples and mono sounds at 44 kHz. When a map is loaded, each such
permutation is decoded here (Ogg Vorbis with stb_vorbis.c, public domain),
permutation is decoded here (Ogg Vorbis with port/third_party/stb_vorbis),
brought to the sound's channel count at a rate this build plays, and
encoded again as Xbox ADPCM into one buffer kept for as long as the map is
loaded. The permutation is then an Xbox ADPCM one whose samples lie in a
@@ -32,7 +32,7 @@ the channels (port/linux/src/dsound_sdl.c decodes it).
#include "custom_edition_cache.h"
#define STB_VORBIS_HEADER_ONLY
#include "stb_vorbis.c"
#include "../../third_party/stb_vorbis/stb_vorbis.c"
#include <stdlib.h>
#include <string.h>
@@ -60,6 +60,11 @@ stereo samples. Every buffer of frames is sized from a count within it
size_t of this build: a stream that said it was at 1 Hz once made a count
whose bytes wrapped to a few KB, which the resampler then wrote 4 GB into */
#define MAXIMUM_FRAMES 0x1000000L
/* The most bytes a permutation's samples are read in: as many frames as
that, stereo, uncompressed. Any longer decode to more frames than that, so
are not read at all (Xbox ADPCM and Ogg Vorbis are smaller than the
uncompressed samples they hold, and no stream has more than two channels). */
#define MAXIMUM_SAMPLES_BYTES (MAXIMUM_FRAMES * 2 * (long)sizeof(short))
/* the highest rate a stream is taken to be at (its own rate is read from
it, and the resampler scales by it) */
#define MAXIMUM_SAMPLE_RATE 192000
@@ -526,7 +531,8 @@ static boolean permutation_convert(
long offset = NONE;
/* (the loader checked the samples lie in the file; none at all is no sound) */
if (permutation->samples.size <= 0 || !(data = malloc(permutation->samples.size)))
if (permutation->samples.size <= 0 || permutation->samples.size > MAXIMUM_SAMPLES_BYTES ||
!(data = malloc(permutation->samples.size)))
{
return FALSE;
}
+415 -84
View File
@@ -31,11 +31,14 @@ session as it asked the host (p2p.c: a player answers holders of the invite
it joined with while it may share). Each downloaded file is written beside
the others as <name>.part and renamed when whole (a .part found again is
checked piece by piece and taken up where it was), into custom_maps under
the map's own name, which is its hash's.
the map's own name, or, beside another version of that name, the name and
its hash's first digits (the game's map version is checked against the
offered map's hash).
A machine still downloading does not hold the lobby: the machines that
have the map start, and it joins the game in progress once the download is
done. The host does not drop a machine that says it is downloading.
done. The host does not drop a machine that says it is downloading, while
what it says it has grows.
*/
/* ---------- headers */
@@ -99,9 +102,12 @@ enum
MAXIMUM_SOURCES = 32,
MAXIMUM_OUTGOING = 8,
/* connections at once, those other machines made included, and of
those, from one address */
those, from one address; those other machines made (the rest kept for
the download's own), and of those, ones that have not said hello yet */
MAXIMUM_CONNECTIONS = 24,
MAXIMUM_CONNECTIONS_PER_ADDRESS = 4,
MAXIMUM_INCOMING = MAXIMUM_CONNECTIONS - MAXIMUM_OUTGOING - 1,
MAXIMUM_INCOMING_WITHOUT_HELLO = 8,
/* a connection's buffers: a whole frame in (the largest a block), and
a few blocks out */
INPUT_BYTES = 0x6000,
@@ -122,16 +128,31 @@ enum
STALL_TIMEOUT = 60000,
PROGRESS_INTERVAL = 2000,
SOURCES_INTERVAL = 10000,
/* the host takes a machine for downloading this long after it said so */
/* the host takes a machine for downloading this long after it said so,
and while what it says it has grew by a piece (or to the whole) this
long ago at most (a download that brings nothing for STALL_TIMEOUT
fails) */
DOWNLOADING_TIMEOUT = 20000,
DOWNLOADING_STALL_TIMEOUT = STALL_TIMEOUT + DOWNLOADING_TIMEOUT,
CONNECTION_IDLE_TIMEOUT = 30000,
CONNECT_TIMEOUT = 10000,
/* a connection open this long without the other end's hello is closed */
HELLO_TIMEOUT = 10000,
KEEP_ALIVE_INTERVAL = 10000,
REQUEST_TIMEOUT = 30000,
/* the pieces' hashes are asked of the host first: of the others the host
named once this long has passed since its offer without them, and of the
connections other machines made after REQUEST_TIMEOUT; one connection
sends them all in this long */
HASHES_SOURCES_DELAY = 10000,
HASHES_TIMEOUT = 60000,
SOURCE_RETRY_INTERVAL = 15000,
LISTEN_RETRY_INTERVAL = 10000,
RATE_WINDOW = 2000,
LOG_INTERVAL = 10000,
/* (the host) a machine's requests for a map taken this often at most:
one that comes sooner waits its turn, the last of them kept */
MAP_REQUEST_INTERVAL = 5000,
/* the largest file of each kind the cache loader takes
(custom_edition_cache.c's combined offset space), so that a host cannot
@@ -193,7 +214,7 @@ enum
_offer_ok,
/* it does not share maps (network.map_share) */
_offer_off,
/* not the game's map, not named for its hash, or not readable */
/* not the game's map, not of the game's version, or not readable */
_offer_unavailable,
};
@@ -319,6 +340,8 @@ struct download_file
byte *hashes_coming;
long hashes_received;
short hashes_connection;
/* when they were asked for, and when the last of them came */
unsigned long hashes_started_at;
unsigned long hashes_asked_at;
/* the pieces had (a bit each), and asked for (a count each) */
byte *have;
@@ -363,6 +386,7 @@ struct download
short source_count;
struct source sources[MAXIMUM_SOURCES + 1];
unsigned long asked_at;
unsigned long offered_at;
unsigned long progress_at;
unsigned long reported_at;
/* the bytes of the pieces had, of all of them; and for the rate shown,
@@ -405,6 +429,9 @@ struct connection
unsigned long address;
struct transport_endpoint *endpoint;
unsigned long created_at;
/* connected or taken (its hello due by HELLO_TIMEOUT), and when it last
sent a whole frame */
unsigned long opened_at;
unsigned long heard_at;
unsigned long sent_at;
/* the bytes of pieces it brought and took, for the log */
@@ -413,14 +440,16 @@ struct connection
boolean hello_heard;
/* the files it is about, and for each: the download's file, the
catalog's file this machine sends (map_share_files.c), what the other
end has, whether this end said what it has, and the next of the pieces'
hashes to send it (NONE: none asked) */
end has (by piece: a bit each of the pieces it covers, as many as the
download's file had when it was made), whether this end said what it
has, and the next of the pieces' hashes to send it (NONE: none asked) */
short file_count;
byte file_hashes[MAXIMUM_FILES][MAP_SHARE_HASH_BYTES];
short download_file[MAXIMUM_FILES];
short catalog_file[MAXIMUM_FILES];
short remote_has[MAXIMUM_FILES];
byte *remote_pieces[MAXIMUM_FILES];
long remote_piece_count[MAXIMUM_FILES];
boolean state_heard[MAXIMUM_FILES];
boolean state_told[MAXIMUM_FILES];
long hashes_next[MAXIMUM_FILES];
@@ -454,6 +483,19 @@ struct host_machine
unsigned long done;
unsigned long total;
unsigned long address;
/* the bytes offered, and when what it said it has last grew by a piece
(or to the whole), and to what */
unsigned long offered_total;
unsigned long advanced_at;
unsigned long advanced_done;
/* the game's map that progress is counted for (another request does
not start it again, whatever level it names) */
char advanced_level[LEVEL_NAME_BYTES];
/* when its last request was taken, and one that came sooner after it
than MAP_REQUEST_INTERVAL, taken then (host_asked) */
unsigned long requested_at;
boolean request_waiting;
struct distributed_map_share_request waiting_request;
};
struct map_share_globals
@@ -472,6 +514,9 @@ struct map_share_globals
this machine sends (NONE: none), and the level they are of */
char current_level[LEVEL_NAME_BYTES];
short current_files[MAXIMUM_FILES];
/* the part file of a map a download stopped in, kept to be taken up
again (one alone: the one before is deleted) */
char kept_part[MAP_SHARE_PATH_SIZE];
};
/* ---------- globals */
@@ -536,6 +581,32 @@ static boolean address_internet(
return (address & 0xFFC00000UL) == 0x64400000UL;
}
/* whether an address (host byte order) is a private network's (RFC 1918,
link-local) or this machine's (loopback) */
static boolean address_private(
unsigned long address)
{
return (address >> 24) == 10 || (address >> 24) == 127 || (address & 0xFFF00000UL) == 0xAC100000UL ||
(address & 0xFFFF0000UL) == 0xC0A80000UL || (address & 0xFFFF0000UL) == 0xA9FE0000UL;
}
/* whether a client connects to an address the host named (host byte
order) for a machine of a LAN game: one of the host's own kind, a private
network's if the host's is, this machine's (loopback) only if the host's is,
and never 0.0.0.0/8, multicast or reserved; so that a host on a LAN cannot
have its players connect anywhere on the internet */
static boolean source_address_allowed(
unsigned long address,
unsigned long host_address)
{
if ((address >> 24) == 0 || (address >> 28) >= 14 || address_internet(address) ||
((address >> 24) == 127) != ((host_address >> 24) == 127))
{
return FALSE;
}
return !address_private(host_address) || address_private(address);
}
/* ---------- this machine's network game */
static struct network_game *map_share_network_game(
@@ -546,6 +617,22 @@ static struct network_game *map_share_network_game(
return client ? network_game_client_get_game(client) : NULL;
}
boolean map_share_game_map_version(
char const *level_name,
unsigned long *version)
{
struct network_game *game = map_share_network_game();
/* (by the map's name: the loader asks by its file's, hugeass) */
if (!game || !level_name || !custom_edition_level_name(game->map.name) ||
csstrcasecmp(tag_name_strip_path(game->map.name), tag_name_strip_path(level_name)))
{
return FALSE;
}
*version = (unsigned long)game->map.version;
return TRUE;
}
static boolean map_share_host(
void)
{
@@ -834,6 +921,31 @@ static void connection_match_files(
}
}
/* whether the other end of a connection has a piece of its file: all of
it, or the piece among those it said it has (a piece beyond them is not
had: they are of the download's file as it was when they were said) */
static boolean remote_piece_had(
struct connection const *connection,
short index,
long piece)
{
if (connection->remote_has[index] == _have_all)
return TRUE;
return connection->remote_has[index] == _have_pieces && connection->remote_pieces[index] && piece >= 0 &&
piece < connection->remote_piece_count[index] &&
(connection->remote_pieces[index][piece >> 3] & (1 << (piece & 7))) != 0;
}
/* the pieces the other end said it has of a connection's file forgotten */
static void remote_pieces_forget(
struct connection *connection,
short index)
{
free(connection->remote_pieces[index]);
connection->remote_pieces[index] = NULL;
connection->remote_piece_count[index] = 0;
}
/* ---------- connections */
static struct connection *connection_new(
@@ -1014,37 +1126,80 @@ static boolean connection_connected(
return TRUE;
}
/* (the host) whether an address (host byte order) is one of its game's
machines', which alone fetch the game's files from it */
static boolean host_machine_address(
unsigned long address)
{
long index;
for (index = 0; index < MAP_SHARE_MACHINES; index++)
{
if (address && network_game_server_machine_address(index) == address)
return TRUE;
}
return FALSE;
}
static void connection_accept(
void)
{
struct map_share_globals *globals = &map_share_globals;
struct transport_endpoint *endpoint;
struct connection *connection;
struct connection *oldest;
struct transport_address address;
short count;
short incoming;
short without_hello;
short index;
while (globals->listener && endpoint_readable(globals->listener, 0))
{
count = 0;
incoming = 0;
without_hello = 0;
oldest = NULL;
endpoint = accept_endpoint(globals->listener);
if (!endpoint)
return;
csmemset(&address, 0, sizeof(address));
if (set_endpoint_blocking(endpoint, FALSE) != _transport_error_none ||
get_endpoint_address(endpoint, &address) != _transport_error_none ||
(!map_share_sending() && map_share_globals.download.state != _download_transferring))
(!map_share_sending() && map_share_globals.download.state != _download_transferring) ||
(map_share_host() && !host_machine_address(address.address.long_words[0])))
{
delete_transport_endpoint(endpoint);
continue;
}
/* (a few from one address: anyone can connect) */
/* (a few from one address, and a few at once that have not said
hello: anyone can connect. The download's own connections keep slots
of their own) */
for (index = 0; index < MAXIMUM_CONNECTIONS; index++)
{
count += globals->connections[index].used &&
globals->connections[index].address == address.address.long_words[0];
struct connection *other = &globals->connections[index];
if (!other->used)
continue;
count += other->address == address.address.long_words[0];
if (other->outgoing)
continue;
incoming++;
if (!other->hello_heard)
{
without_hello++;
if (!oldest || (long)(other->opened_at - oldest->opened_at) < 0)
oldest = other;
}
}
connection = count < MAXIMUM_CONNECTIONS_PER_ADDRESS ? connection_new() : NULL;
/* (the longest waiting for its hello makes way: one that holds its
slot without a word holds it HELLO_TIMEOUT at most) */
if (without_hello >= MAXIMUM_INCOMING_WITHOUT_HELLO && count < MAXIMUM_CONNECTIONS_PER_ADDRESS)
{
connection_close(oldest, "no hello while others waited");
incoming--;
}
connection = count < MAXIMUM_CONNECTIONS_PER_ADDRESS && incoming < MAXIMUM_INCOMING ? connection_new() : NULL;
if (!connection)
{
delete_transport_endpoint(endpoint);
@@ -1054,6 +1209,7 @@ static void connection_accept(
connection->address = address.address.long_words[0];
connection->state = _connection_open;
connection->outgoing = FALSE;
connection->opened_at = connection->created_at;
}
}
@@ -1110,9 +1266,45 @@ static void request_remove(
struct connection *connection,
short index);
/* the download over: its files closed (a part file is kept, to be taken
up again), and the connections it made closed, unless they are kept for
what this machine sends now that it has the files (keep_connections) */
/* a download's file closed, not had whole: its part file deleted if it
holds none of the file (made for an offer alone, it would be a host's to
fill the disk with, a new name each time), else kept to be taken up again, a
map's alone of them all (the one before deleted: resource maps' part files
have names of their own) */
static void download_file_close(
struct download_file *file)
{
struct map_share_globals *globals = &map_share_globals;
if (file->stream)
{
fclose(file->stream);
file->stream = NULL;
}
/* (one being checked, or not opened yet, is not this download's to
delete: it may hold what an earlier one had) */
if (file->state != _file_transferring || !file->part_path[0])
return;
if (!file->have_count)
{
DeleteFileA(file->part_path);
if (!csstrcasecmp(globals->kept_part, file->part_path))
globals->kept_part[0] = 0;
return;
}
if (file->kind != _map_share_file_map)
return;
if (globals->kept_part[0] && csstrcasecmp(globals->kept_part, file->part_path))
{
error(_error_silent, "map sharing: deleted %s (another download stopped since)", globals->kept_part);
DeleteFileA(globals->kept_part);
}
csstrncpy(globals->kept_part, file->part_path, sizeof(globals->kept_part) - 1);
}
/* the download over: its files closed (download_file_close), and the
connections it made closed, unless they are kept for what this machine
sends now that it has the files (keep_connections) */
static void download_end(
boolean keep_connections)
{
@@ -1134,18 +1326,23 @@ static void download_end(
continue;
}
/* (another's connection, which may still fetch what this machine
sends: what it asked of the download let go) */
sends: what it asked of the download let go, and which pieces of the
download's files the other end has, a bit each of pieces counted by
the offer: another download's file of the same hash may be offered
of another size) */
while (connection->request_count)
request_remove(connection, (short)(connection->request_count - 1));
for (file = 0; file < MAXIMUM_FILES; file++)
{
connection->download_file[file] = NONE;
remote_pieces_forget(connection, file);
}
}
for (index = 0; index < download->file_count; index++)
{
struct download_file *file = &download->files[index];
if (file->stream)
fclose(file->stream);
download_file_close(file);
free(file->piece_hashes);
free(file->hashes_coming);
free(file->have);
@@ -1215,14 +1412,18 @@ static void download_ask(
/* the files this machine lacks for the level: a bit each */
static byte files_wanted(
char const *level_name)
char const *level_name,
unsigned long version)
{
char path[MAP_SHARE_PATH_SIZE];
byte wanted = 0;
short kind;
if (!custom_edition_map_file_path(level_name, path))
if (version ? !custom_edition_map_find_version(level_name, version, path) :
!custom_edition_map_file_named(level_name, path))
{
wanted |= FLAG(_map_share_file_map);
}
for (kind = _map_share_file_bitmaps; kind < MAXIMUM_FILES; kind++)
{
if (!custom_edition_resource_map_file_path(kind, path))
@@ -1253,17 +1454,12 @@ boolean map_share_download_begin(
download_end(FALSE);
}
name = tag_name_strip_path(level_name);
wanted = files_wanted(level_name);
/* (the map named for its hash, so that the download is the host's
version under its own name, a file of custom_maps alone, whatever the
host's level name holds; a map this machine has of another version is
not replaced) */
if (!wanted || !map_share_name_safe(name) ||
(TEST_FLAG(wanted, _map_share_file_map) && !map_share_name_hashed(name)) ||
(!TEST_FLAG(wanted, _map_share_file_map) && custom_edition_map_checksum(level_name) != version && version))
{
wanted = files_wanted(level_name, version);
/* (the map of the host's version, which the offer's hash is checked
against, a file of custom_maps alone, whatever the host's level name
holds; a map this machine has of another version is not replaced) */
if (!wanted || !map_share_name_safe(name) || (TEST_FLAG(wanted, _map_share_file_map) && !version))
return FALSE;
}
{
static long serial;
@@ -1307,7 +1503,9 @@ boolean map_share_holds_players(
(download->state == _download_transferring && download->in_progress);
}
/* the download's file made anew for writing: its part file the file's size */
/* the download's file made anew for writing: its part file empty, each
piece written at its place as it comes (not the file's size at once: the
offer alone, the host's word, would take the disk) */
static boolean download_file_create(
struct download_file *file)
{
@@ -1315,11 +1513,6 @@ static boolean download_file_create(
if (!stream)
return FALSE;
if (file->size && (fseek(stream, (long)file->size - 1, SEEK_SET) != 0 || fputc(0, stream) == EOF))
{
fclose(stream);
return FALSE;
}
fclose(stream);
file->stream = fopen(file->part_path, "r+b");
if (!file->stream)
@@ -1328,9 +1521,9 @@ static boolean download_file_create(
return TRUE;
}
/* the download's file opened for writing: a part file of its size there
already checked piece by piece first (on the hashing thread, asked again
while it is busy: download_update), else one made anew */
/* the download's file opened for writing: a part file there already (no
larger than the file) checked piece by piece first (on the hashing thread,
asked again while it is busy: download_update), else one made anew */
static boolean download_file_open(
struct download_file *file,
short index)
@@ -1344,7 +1537,7 @@ static boolean download_file_open(
size = ftell(stream);
fclose(stream);
}
if (size == (long)file->size && file->size)
if (size > 0 && size <= (long)file->size)
{
file->state = _file_checking;
map_share_pieces_hash(file->part_path, map_share_globals.download.serial << 3 | index);
@@ -1390,21 +1583,28 @@ static void download_offered(
file->piece_count = map_share_piece_count(file->size);
map_share_hash_digits(file->hash, digits);
/* (a file this machine did not ask for, or offered twice, or a map
whose hash is not its name's) */
not of the game's version) */
if (file->kind >= MAXIMUM_FILES || !TEST_FLAG(download->wanted, file->kind) ||
TEST_FLAG(offered, file->kind) || !file->size ||
file->size > (unsigned long)(file->kind == _map_share_file_map ? MAXIMUM_MAP_BYTES :
MAXIMUM_RESOURCE_MAP_BYTES) ||
(file->kind == _map_share_file_map && csstrcmp(name + map_share_name_base_length(name) + 1, digits)))
(file->kind == _map_share_file_map && map_share_hash_version(file->hash) != download->version))
{
download->file_count = index;
download_fail("The host's offer was not of this map.");
return;
}
offered |= (byte)FLAG(file->kind);
/* (the map by its own name, or beside another version of it, by the
name and its hash's first digits) */
if (file->kind == _map_share_file_map)
{
snprintf(file->final_path, sizeof(file->final_path), "%s%s.map", CUSTOM_EDITION_MAP_DIRECTORY, name);
if (GetFileAttributesA(file->final_path) != (unsigned long)-1)
{
snprintf(file->final_path, sizeof(file->final_path), "%s%s_%s.map", CUSTOM_EDITION_MAP_DIRECTORY, name,
digits);
}
}
else
{
@@ -1427,6 +1627,7 @@ static void download_offered(
download->files[index].state = _file_awaiting_hashes;
download->state = _download_transferring;
download->progress_at = system_milliseconds();
download->offered_at = download->progress_at;
error(_error_silent, "map sharing: downloading %s, %d files, %lu bytes", download->level_name,
(int)download->file_count, download->total_bytes);
}
@@ -1457,8 +1658,12 @@ static void download_sources(
struct source *source = NULL;
short known;
if (entry->machine_index == network_game_client_get_local_machine_index())
if (entry->machine_index == network_game_client_get_local_machine_index() ||
(!TEST_FLAG(entry->flags, _source_internet_bit) &&
!source_address_allowed(entry->address, download->sources[0].address)))
{
continue;
}
for (known = 1; known < download->source_count; known++)
{
if (download->sources[known].machine_index == entry->machine_index &&
@@ -1604,7 +1809,27 @@ static void download_piece_had(
}
}
/* the download's files, all had: renamed, the map checked */
/* the first `count` of the download's files, which download_finish put in
custom_maps, deleted again: a download that cannot be played leaves none of
its files there, for the map lists to take for present (none of them was
there before it: MoveFileA does not replace a file) */
static void download_unfinish(
short count)
{
struct download *download = &map_share_globals.download;
short index;
for (index = 0; index < count; index++)
{
error(_error_silent, "map sharing: deleted %s (the download cannot be played)",
download->files[index].final_path);
DeleteFileA(download->files[index].final_path);
}
custom_edition_maps_look_again();
}
/* the download's files, all had: renamed, the map checked (each deleted
again if it cannot be played) */
static void download_finish(
void)
{
@@ -1623,14 +1848,20 @@ static void download_finish(
}
if (!MoveFileA(file->part_path, file->final_path))
{
download_unfinish(index);
download_fail("The downloaded map could not be put in custom_maps (a file of its name is there).");
return;
}
if (!csstrcasecmp(map_share_globals.kept_part, file->part_path))
map_share_globals.kept_part[0] = 0;
/* (its hash known: its version is not read again) */
map_share_map_hash_remember(file->final_path, file->hash);
}
custom_edition_maps_look_again();
if (!custom_edition_cache_present(download->level_name, download->version, message, sizeof(message)))
{
csstrncpy(download->message, message, sizeof(download->message) - 1);
download_unfinish(download->file_count);
download_fail("The map was downloaded, but cannot be played.");
return;
}
@@ -1682,12 +1913,8 @@ static long piece_choose(
{
continue;
}
if (connection->remote_has[connection_file] != _have_all &&
!(connection->remote_has[connection_file] == _have_pieces && connection->remote_pieces[connection_file] &&
(connection->remote_pieces[connection_file][piece >> 3] & (1 << (piece & 7)))))
{
if (!remote_piece_had(connection, connection_file, piece))
continue;
}
for (index = 0; index < connection->request_count; index++)
{
if (connection->requests[index].file == connection->download_file[connection_file] &&
@@ -1712,13 +1939,8 @@ static long piece_choose(
if (other->download_file[other_file] == connection->download_file[connection_file])
break;
}
if (other_file < other->file_count &&
(other->remote_has[other_file] == _have_all ||
(other->remote_has[other_file] == _have_pieces && other->remote_pieces[other_file] &&
(other->remote_pieces[other_file][piece >> 3] & (1 << (piece & 7))))))
{
if (other_file < other->file_count && remote_piece_had(other, other_file, piece))
count++;
}
}
if (count < best_count)
{
@@ -1886,8 +2108,7 @@ static void frame_state(
if (size < 2 || body[0] >= connection->file_count)
return;
index = body[0];
free(connection->remote_pieces[index]);
connection->remote_pieces[index] = NULL;
remote_pieces_forget(connection, index);
connection->remote_has[index] = _have_none;
connection->state_heard[index] = TRUE;
if (body[1] == _have_all)
@@ -1901,11 +2122,15 @@ static void frame_state(
connection->remote_has[index] = _have_pieces;
if (connection->download_file[index] != NONE)
{
long bytes = (download->files[connection->download_file[index]].piece_count + 7) / 8;
long piece_count = download->files[connection->download_file[index]].piece_count;
long bytes = (piece_count + 7) / 8;
connection->remote_pieces[index] = size == 2 + bytes ? malloc(bytes) : NULL;
if (connection->remote_pieces[index])
{
csmemcpy(connection->remote_pieces[index], body + 2, bytes);
connection->remote_piece_count[index] = piece_count;
}
}
}
}
@@ -1927,11 +2152,15 @@ static void frame_have(
piece = (long)get_long(body + 1);
if (piece < 0 || piece >= file->piece_count || connection->remote_has[index] == _have_all)
return;
/* (said of another file of this hash, of another size: forgotten) */
if (connection->remote_pieces[index] && connection->remote_piece_count[index] != file->piece_count)
remote_pieces_forget(connection, index);
if (!connection->remote_pieces[index])
{
connection->remote_pieces[index] = calloc((unsigned long)(file->piece_count + 7) / 8, 1);
if (!connection->remote_pieces[index])
return;
connection->remote_piece_count[index] = file->piece_count;
}
connection->remote_pieces[index][piece >> 3] |= (byte)(1 << (piece & 7));
connection->remote_has[index] = _have_pieces;
@@ -2057,7 +2286,7 @@ static void frame_refuse(
says next) */
if (connection->remote_has[body[0]] == _have_all)
connection->remote_has[body[0]] = _have_none;
else if (connection->remote_pieces[body[0]])
else if (remote_piece_had(connection, body[0], piece))
connection->remote_pieces[body[0]][piece >> 3] &= (byte)~(1 << (piece & 7));
return;
}
@@ -2091,7 +2320,8 @@ static void frame_hashes(
file = &download->files[connection->download_file[body[0]]];
first = get_long(body + 1);
count = body[5] | body[6] << 8;
if (file->piece_hashes || file->hashes_connection != connection_index || !file->hashes_coming ||
/* (none: a frame that brings nothing does not keep the request alive) */
if (file->piece_hashes || file->hashes_connection != connection_index || !file->hashes_coming || count < 1 ||
size != 7 + count * MAP_SHARE_HASH_BYTES || first != (unsigned long)file->hashes_received ||
count > file->piece_count - file->hashes_received)
{
@@ -2142,8 +2372,6 @@ static void connection_read(
connection->input_size += result;
if (!result && connection->input_size < 5)
return;
if (result)
connection->heard_at = system_milliseconds();
}
while (connection->used && connection->input_size - offset >= 5)
{
@@ -2163,6 +2391,8 @@ static void connection_read(
body = connection->input + offset + 5;
size = (long)length - 1;
offset += 4 + (long)length;
/* (heard from: a whole frame, not a byte at a time) */
connection->heard_at = system_milliseconds();
if (type != _frame_hello && !connection->hello_heard)
{
connection_close(connection, "no hello");
@@ -2383,6 +2613,24 @@ static void serve_blocks(
}
}
/* whether a download's file's pieces' hashes may be asked of the
connection: of the host's, the host's word; of another the host named once
HASHES_SOURCES_DELAY has passed since the offer, and of one another machine
made after REQUEST_TIMEOUT (anyone can connect, and hold the request) */
static boolean hashes_source(
struct connection const *connection,
unsigned long now)
{
struct download const *download = &map_share_globals.download;
if (connection->outgoing && connection->source != NONE && connection->source < download->source_count &&
download->sources[connection->source].host)
{
return TRUE;
}
return elapsed(download->offered_at, now, connection->outgoing ? HASHES_SOURCES_DELAY : REQUEST_TIMEOUT);
}
/* each connection: connected, read, its states said, its pieces and
hashes asked for, written */
static void connections_update(
@@ -2416,6 +2664,7 @@ static void connections_update(
continue;
}
connection->state = _connection_open;
connection->opened_at = now;
connection->heard_at = now;
send_hello(connection);
}
@@ -2432,6 +2681,11 @@ static void connections_update(
}
if (!connection->hello_heard)
{
if (elapsed(connection->opened_at, now, HELLO_TIMEOUT))
{
connection_close(connection, "no hello");
continue;
}
connection_flush(connection);
continue;
}
@@ -2444,7 +2698,8 @@ static void connections_update(
/* (the pieces' hashes asked of one that has the file whole) */
if (download_file != NONE && download->files[download_file].state == _file_awaiting_hashes &&
download->files[download_file].hashes_connection == NONE &&
connection->remote_has[file_index] == _have_all && frame_room(connection, 1))
connection->remote_has[file_index] == _have_all && hashes_source(connection, now) &&
frame_room(connection, 1))
{
struct download_file *file = &download->files[download_file];
byte *body;
@@ -2455,6 +2710,7 @@ static void connections_update(
continue;
file->hashes_received = 0;
file->hashes_connection = index;
file->hashes_started_at = now;
file->hashes_asked_at = now;
body = frame_begin(connection, _frame_hashes_request, 1);
body[0] = (byte)file_index;
@@ -2500,7 +2756,8 @@ static void connections_update(
for (file_index = 0; file_index < download->file_count; file_index++)
{
if (download->files[file_index].hashes_connection == index &&
elapsed(download->files[file_index].hashes_asked_at, now, REQUEST_TIMEOUT))
(elapsed(download->files[file_index].hashes_asked_at, now, REQUEST_TIMEOUT) ||
elapsed(download->files[file_index].hashes_started_at, now, HASHES_TIMEOUT)))
{
break;
}
@@ -2557,9 +2814,11 @@ static void download_update(
{
long piece;
file->stream = piece_hashes && piece_count == file->piece_count && size == file->size ?
/* (a part file as long as the furthest piece written: the pieces
it reaches, the last of them perhaps cut short and not taken) */
file->stream = piece_hashes && piece_count <= file->piece_count && size <= file->size ?
fopen(file->part_path, "r+b") : NULL;
for (piece = 0; file->stream && piece < file->piece_count; piece++)
for (piece = 0; file->stream && piece < piece_count; piece++)
{
if (!csmemcmp(piece_hashes + piece * MAP_SHARE_HASH_BYTES, file->piece_hashes + piece * MAP_SHARE_HASH_BYTES,
MAP_SHARE_HASH_BYTES))
@@ -2638,13 +2897,26 @@ static boolean host_machine_valid(
return machine_index >= 0 && machine_index < MAP_SHARE_MACHINES;
}
/* whether a machine says it is downloading, lately, and what it says it
has grew lately (once offered the files): one that only says so, its
download going nowhere, holds its slot no longer than an honest one's
download would last before it failed */
static boolean host_machine_downloading(
struct host_machine const *machine)
{
unsigned long now = system_milliseconds();
return machine->downloading && !elapsed(machine->heard_at, now, DOWNLOADING_TIMEOUT) &&
(!machine->offered || !elapsed(machine->advanced_at, now, DOWNLOADING_STALL_TIMEOUT));
}
boolean map_share_machine_downloading(
long machine_index)
{
struct host_machine const *machine = host_machine_valid(machine_index) ?
&map_share_globals.machines[machine_index] : NULL;
return machine && machine->downloading && !elapsed(machine->heard_at, system_milliseconds(), DOWNLOADING_TIMEOUT);
return machine && host_machine_downloading(machine);
}
boolean map_share_machine_needs_map(
@@ -2653,8 +2925,7 @@ boolean map_share_machine_needs_map(
struct host_machine const *machine = host_machine_valid(machine_index) ?
&map_share_globals.machines[machine_index] : NULL;
return machine && machine->downloading && !machine->downloaded &&
!elapsed(machine->heard_at, system_milliseconds(), DOWNLOADING_TIMEOUT);
return machine && !machine->downloaded && host_machine_downloading(machine);
}
static boolean host_in_game(
@@ -2696,6 +2967,16 @@ static void host_offer(
csmemcpy(message.files[index].hash, file->hash, MAP_SHARE_HASH_BYTES);
}
network_distributed_server_send_to_machine_reliably(machine_index, &message, size);
/* (the files' bytes, and the time its progress is counted from: a
request asked again does not start it again, host_request) */
machine->offered_total = 0;
for (index = 0; index < count; index++)
machine->offered_total += message.files[index].size;
if (!machine->advanced_at && result == _offer_ok)
{
machine->advanced_at = system_milliseconds() | 1;
level_name_copy(machine->advanced_level, machine->request.level_name);
}
machine->offered = TRUE;
machine->asked = FALSE;
error(_error_silent, "map sharing: offered machine #%ld %s (%d files)%s", machine_index,
@@ -2755,27 +3036,42 @@ static void host_request(
{
struct host_machine *machine = &map_share_globals.machines[machine_index];
struct network_game *game = map_share_network_game();
char const *name;
char path[MAP_SHARE_PATH_SIZE];
unsigned long needed;
short kind;
/* (the same machine asking again while the game is on the map its
progress is counted for, its download failed or not, whatever level it
names: its progress counted from the first offer still, so that asking
again and again, or for another level between, does not keep it
downloading. One that leaves is forgotten, host_update; a game on
another map starts it again) */
boolean again = machine->advanced_at && machine->address == network_game_server_machine_address(machine_index) &&
game && !csstrncmp(machine->advanced_level, game->map.name, LEVEL_NAME_BYTES - 1);
unsigned long advanced_at = again ? machine->advanced_at : 0;
unsigned long advanced_done = again ? machine->advanced_done : 0;
char advanced_level[LEVEL_NAME_BYTES];
csmemcpy(advanced_level, machine->advanced_level, LEVEL_NAME_BYTES);
csmemset(machine, 0, sizeof(*machine));
machine->request = *request;
machine->asked_at = system_milliseconds();
machine->heard_at = machine->asked_at;
machine->requested_at = machine->asked_at | 1;
machine->address = network_game_server_machine_address(machine_index);
machine->advanced_at = advanced_at;
machine->advanced_done = advanced_done;
if (again)
csmemcpy(machine->advanced_level, advanced_level, LEVEL_NAME_BYTES);
if (!config_boolean("network.map_share"))
{
host_offer(machine_index, _offer_off);
return;
}
/* (the game's map alone, a Custom Edition map's, named for its hash if
it is asked for) */
name = tag_name_strip_path(request->level_name);
/* (the game's map alone, a Custom Edition map's, of the game's version
if it is asked for) */
if (!game || csstrcmp(game->map.name, request->level_name) || !custom_edition_level_name(request->level_name) ||
!custom_edition_map_file_path(request->level_name, path) ||
(TEST_FLAG(request->wanted, _map_share_file_map) && !map_share_name_hashed(name)))
(TEST_FLAG(request->wanted, _map_share_file_map) && !game->map.version))
{
host_offer(machine_index, _offer_unavailable);
return;
@@ -2805,12 +3101,34 @@ static void host_request(
(unsigned)request->wanted);
}
/* a machine's request, as it comes: taken at once if the last it made was
taken MAP_REQUEST_INTERVAL ago, else kept (the last of them) and taken then
(host_update), so that one asking again and again has the host answer it,
and write to its log, that often at most; one asking again honestly (its
download failed) waits a moment at most */
static void host_asked(
long machine_index,
struct distributed_map_share_request const *request)
{
struct host_machine *machine = &map_share_globals.machines[machine_index];
if (machine->requested_at && machine->address == network_game_server_machine_address(machine_index) &&
!elapsed(machine->requested_at, system_milliseconds(), MAP_REQUEST_INTERVAL))
{
machine->waiting_request = *request;
machine->request_waiting = TRUE;
return;
}
host_request(machine_index, request);
}
/* the host's machines: answered once their files are hashed, told the
sources again now and then, forgotten once gone */
static void host_update(
void)
{
struct map_share_globals *globals = &map_share_globals;
struct network_game *game = map_share_network_game();
unsigned long now = system_milliseconds();
long index;
@@ -2820,7 +3138,7 @@ static void host_update(
unsigned long address = network_game_server_machine_address(index);
short file_index;
if (!machine->asked && !machine->downloading)
if (!machine->asked && !machine->downloading && !machine->advanced_at && !machine->request_waiting)
continue;
/* (left: another machine may take its slot) */
if (!address || address != machine->address)
@@ -2828,6 +3146,13 @@ static void host_update(
csmemset(machine, 0, sizeof(*machine));
continue;
}
/* (a request that came too soon after the last, its turn come) */
if (machine->request_waiting && elapsed(machine->requested_at, now, MAP_REQUEST_INTERVAL))
{
struct distributed_map_share_request request = machine->waiting_request;
host_request(index, &request);
}
if (machine->asked)
{
boolean ready = TRUE;
@@ -2852,19 +3177,16 @@ static void host_update(
}
if (!ready)
continue;
/* (the map's hash its name's, as the client checks) */
/* (the map of the game's version, as the client checks) */
for (file_index = 0; file_index < machine->file_count; file_index++)
{
struct map_share_file *file = map_share_file_get(machine->catalog_files[file_index]);
char const *name = tag_name_strip_path(machine->request.level_name);
char digits[MAP_SHARE_NAME_DIGITS + 1];
map_share_hash_digits(file->hash, digits);
if (machine->file_kinds[file_index] == _map_share_file_map &&
csstrcmp(name + map_share_name_base_length(name) + 1, digits))
(!game || map_share_hash_version(file->hash) != (unsigned long)game->map.version))
{
error(_error_silent, "map sharing: %s is not named for its hash (%s): not shared", file->path,
digits);
error(_error_silent, "map sharing: %s is not the game's version of its map (%08lx): not shared",
file->path, game ? (unsigned long)game->map.version : 0UL);
failed = TRUE;
}
}
@@ -2895,8 +3217,17 @@ static void host_progress(
if (!machine->offered)
return;
machine->heard_at = system_milliseconds();
machine->done = progress->done;
machine->done = MIN(progress->done, machine->offered_total);
machine->total = progress->total;
/* (grown by a piece, or to the whole: a client's own download fails
when no piece comes for STALL_TIMEOUT) */
if (machine->done >= machine->advanced_done + MAP_SHARE_PIECE_BYTES ||
(machine->done > machine->advanced_done && machine->done == machine->offered_total) ||
progress->state == _progress_done)
{
machine->advanced_done = machine->done;
machine->advanced_at = machine->heard_at | 1;
}
machine->downloading = progress->state != _progress_failed;
machine->downloaded = progress->state == _progress_done;
if (progress->state != _progress_downloading)
@@ -2950,7 +3281,7 @@ void map_share_handle_message(
csmemcpy(&request, entries, sizeof(request));
if (!level_name_valid(request.level_name))
return;
host_request(machine_index, &request);
host_asked(machine_index, &request);
break;
}
case _distributed_message_map_share_progress:
+66 -30
View File
@@ -4,16 +4,18 @@ MAP_SHARE.H
Custom Edition maps shared over network games (map_share.c, map_share_files.c;
port/linux/NETCODE.md, "Map sharing").
Every map in the custom_maps folder is named for its contents: its file name
ends in _ and the first 8 hexadecimal digits of its hash (hugeass.map becomes
hugeass_1a2b3c4d.map, renamed as the map lists find it), so the level name a
host's game is on (custom_maps\hugeass_1a2b3c4d) says which version of the map
it is. A client joining without that file asks the host for it. If the host
shares maps (network.map_share), it says which files make up the map (the
map, and the resource maps it needs that the client lacks) and which other
players have them, and the client downloads them in pieces from all of them
at once, as a torrent's peers do, each piece checked against its hash. The
downloaded map keeps its name, so two versions of a map never meet.
A host's game is on a map by its own name (custom_maps\hugeass, as every
screen shows it), and its game record's map version is the first 32 bits of
the map's hash (map_share_hash_version), which says which version of the map
it is. A client takes its file of that name and version (custom_edition_cache.c),
and one without it asks the host for it. If the host shares maps
(network.map_share), it says which files make up the map (the map, and the
resource maps it needs that the client lacks) and which other players have
them, and the client downloads them in pieces from all of them at once, as a
torrent's peers do, each piece checked against its hash. The downloaded map
takes its own name (hugeass.map), or, beside another version of that name,
the name and its hash's first digits (hugeass_1a2b3c4d.map). No map is
renamed: one a build of before named for its hash has its name given back.
*/
#ifndef __MAP_SHARE_H
@@ -25,7 +27,8 @@ enum
{
/* a file's hash, of its pieces' (BLAKE2b, 32 bytes) */
MAP_SHARE_HASH_BYTES = 32,
/* the hexadecimal digits of the hash a map file's name ends in, after _ */
/* the hexadecimal digits of the hash the file name of a second version
of a map ends in, after _ */
MAP_SHARE_NAME_DIGITS = 8,
/* a file is hashed and sent in pieces of this size (the last shorter),
each piece in blocks of this */
@@ -51,7 +54,8 @@ boolean map_share_name_hashed(
/* Whether a map's name (from another machine's level name) is only a file
name of the custom_maps folder: printable ASCII, no separator or character
a file system refuses, no dot or space at either end (so never "." or
".."). */
".."), and not a name Windows takes for a device (CON, NUL, COM1 and the
like, before the first dot). */
boolean map_share_name_safe(
char const *name);
/* the length of a map's file name without the hash it ends in (all of it,
@@ -60,37 +64,68 @@ short map_share_name_base_length(
char const *name);
/* The map lists found the map file <folder><name>.map, a Custom Edition
map, whose name is not its hash's: in the custom_maps folder it is hashed,
on a thread of its own, and renamed <name>_<hash>.map, with its picture and
description beside it (<name>.bmp, <name>.txt); the map lists are scanned
again once it has been (custom_edition_maps_look_again). Not while the map
is loaded (a file in use is not renamed on Windows): it is asked for again. */
void map_share_name_map(
map, whose name ends in _ and a hash's first digits, as a build of before
named every map of the custom_maps folder: if no map there has its name
without them, it is hashed, on a thread of its own, and if the hash is its
name's, given its name back (<name>.map, with its picture and description,
<name>.bmp and <name>.txt); the map lists are scanned again once it has been
(custom_edition_maps_look_again). Not while the map is loaded: it is asked
for again. */
void map_share_map_found(
char const *folder,
char const *name);
/* A host's game is on the level `level_name` (a buffer of `size`): a
Custom Edition map's named without its hash (custom_maps\hugeass, a saved
last map's or a test's) is named with it (custom_maps\hugeass_1a2b3c4d), the
map's file renamed for its hash first if it has not been yet, so that the
players can download it (network_server_manager.c). */
void map_share_level_name_hash(
/* A host's game is on the level `level_name` (a buffer of `size`), as the
menus name it: a Custom Edition map's is made its file's own name
(custom_maps\hugeass, for hugeass.map, or for hugeass_1a2b3c4d.map, another
version's, whose hash that is), which every screen shows, and its version
kept for map_share_level_name_version (network_server_manager.c). */
void map_share_level_name_choose(
char *level_name,
long size);
/* the version of the map that level name was last made for, if it was */
boolean map_share_level_name_version(
char const *level_name,
unsigned long *version);
/* A map file's hash, read here once (a fraction of a second) and kept for
the rest of the run, by its path, size and the time it was written. FALSE
if it cannot be read. */
boolean map_share_map_hash(
char const *path,
byte *hash);
/* ... a hash known otherwise (a download's) kept */
void map_share_map_hash_remember(
char const *path,
byte const *hash);
/* the version of a map of this hash, a game record's: its first 32 bits
(the digits a file name of another version ends in), never 0 */
unsigned long map_share_hash_version(
byte const *hash);
/* the version of the map file at `path` */
boolean map_share_map_version(
char const *path,
unsigned long *version);
/* ---------- prototypes/MAP_SHARE.C */
/* A client joining a network game on the level `level_name` (with the
host's `version`, its header checksum) that this machine cannot play
host's `version`, map_share_hash_version's) that this machine cannot play
(custom_edition_cache_present said why in `message`): TRUE if it asks the
host for it instead (a Custom Edition map named for its hash, or one whose
resource maps are missing, with network.map_download on). The download
host for it instead (a Custom Edition map of a version it lacks, or one
whose resource maps are missing, with network.map_download on). The download
runs in the lobby (the host waits for it to precache), or, for a game in
progress, before this machine's players join. */
boolean map_share_download_begin(
char const *level_name,
unsigned long version,
char const *message);
/* the version of the map the network game this machine is in is on, if it
is on the level `level_name` (or a map of its name: custom_edition_cache.c
takes the file of that version) */
boolean map_share_game_map_version(
char const *level_name,
unsigned long *version);
/* whether the level's files are being downloaded (cache_files.c gives the
map no time to precache until they are) */
boolean map_share_downloading(
@@ -101,8 +136,8 @@ for a game in progress until the map is downloaded (network_client_manager.c) */
boolean map_share_holds_players(
void);
/* (the host) whether a client machine is downloading the game's map, as it
said lately: it is not dropped for adding no players or loading slowly
(network_server_manager.c) */
said lately, and what it says it has grew lately: it is not dropped for
adding no players or loading slowly (network_server_manager.c) */
boolean map_share_machine_downloading(
long machine_index);
/* (the host) whether that download is not finished: the machines that have
@@ -176,7 +211,8 @@ void map_share_file_hash_of_pieces(
byte const *piece_hashes,
long piece_count,
byte *hash);
/* the hash's first MAP_SHARE_NAME_DIGITS digits, as a name ends in */
/* the hash's first MAP_SHARE_NAME_DIGITS digits, as the name of another
version ends in */
void map_share_hash_digits(
byte const *hash,
char *digits);
+264 -62
View File
@@ -1,10 +1,11 @@
/*
MAP_SHARE_FILES.C
The files map sharing hashes (map_share.h): a map's name made from its
hash, and the hashes of the files this machine shares, worked out on a
The files map sharing hashes (map_share.h): a map's version (its hash's
first digits), the hashes of the files this machine shares, worked out on a
thread of their own a file at a time, so that a map of hundreds of
megabytes costs the game no frames.
megabytes costs the game no frames, and the names a build of before gave
maps (<name>_<hash>.map) given back.
A file's hash is BLAKE2b's (32 bytes) of a label, its size and its pieces'
hashes (each piece MAP_SHARE_PIECE_BYTES of it, the last shorter, hashed with
@@ -48,6 +49,8 @@ enum
run (not looked at again) */
MAXIMUM_NAMING_QUEUE = 64,
MAXIMUM_NAMED = 1024,
/* the maps' hashes kept this run (map_share_map_hash) */
MAXIMUM_KNOWN_HASHES = 64,
/* the longest map file name the map lists take (custom_edition_maps.c:
the level name, custom_maps\ and it, kept whole by the game engine) */
MAXIMUM_MAP_NAME_LENGTH = CUSTOM_EDITION_MAXIMUM_LEVEL_NAME_LENGTH - (sizeof(CUSTOM_EDITION_LEVEL_NAME_PREFIX) - 1),
@@ -66,7 +69,7 @@ enum
_hash_job_none,
/* a catalog file's (map_share_file_hash) */
_hash_job_file,
/* a map to name for its hash (map_share_name_map) */
/* a map named for its hash, to give its name back (map_share_map_found) */
_hash_job_name,
/* a download's pieces (map_share_pieces_hash) */
_hash_job_pieces,
@@ -92,6 +95,17 @@ struct naming
char name[MAP_SHARE_PATH_SIZE];
};
/* a map's hash, found this run: its file by its path, size and time
written, so that it is read once */
struct known_hash
{
boolean used;
char path[MAP_SHARE_PATH_SIZE];
unsigned long size;
unsigned long modified;
byte hash[MAP_SHARE_HASH_BYTES];
};
struct map_share_files_globals
{
struct map_share_file files[MAXIMUM_SHARE_FILES];
@@ -106,7 +120,16 @@ struct map_share_files_globals
long job_owner;
struct naming job_naming;
/* the maps to name, and those looked at already (folder and name) */
/* the maps' hashes found, and the next of them to replace */
struct known_hash known[MAXIMUM_KNOWN_HASHES];
short next_known;
/* the level name a host's game was last given, and its map's version
(map_share_level_name_choose) */
char chosen_level[MAP_SHARE_PATH_SIZE];
unsigned long chosen_version;
/* the maps to give their names back, and those looked at already
(folder and name) */
short naming_count;
struct naming naming_queue[MAXIMUM_NAMING_QUEUE];
long named_count;
@@ -151,14 +174,42 @@ boolean map_share_name_hashed(
return TRUE;
}
/* whether Windows takes a file of this name for a device: its name before
the first dot, spaces after it aside, is CON, PRN, AUX, NUL, CONIN$, CONOUT$,
or COM or LPT and a digit, in any case (CON.x_1a2b3c4d.map.part is the
console) */
static boolean name_device(
char const *name)
{
static char const *const devices[] = { "con", "prn", "aux", "nul", "conin$", "conout$" };
long length = 0;
short index;
while (name[length] && name[length] != '.')
length++;
while (length && name[length - 1] == ' ')
length--;
if (length == 4 && (!_strnicmp(name, "com", 3) || !_strnicmp(name, "lpt", 3)) && name[3] >= '0' && name[3] <= '9')
return TRUE;
for (index = 0; index < (short)(sizeof(devices) / sizeof(devices[0])); index++)
{
if ((long)csstrlen(devices[index]) == length && !_strnicmp(name, devices[index], length))
return TRUE;
}
return FALSE;
}
boolean map_share_name_safe(
char const *name)
{
long length = (long)csstrlen(name);
long index;
if (!length || name[0] == '.' || name[0] == ' ' || name[length - 1] == '.' || name[length - 1] == ' ')
if (!length || name[0] == '.' || name[0] == ' ' || name[length - 1] == '.' || name[length - 1] == ' ' ||
name_device(name))
{
return FALSE;
}
for (index = 0; index < length; index++)
{
unsigned char character = (unsigned char)name[index];
@@ -440,7 +491,129 @@ boolean map_share_pieces_hashed(
return TRUE;
}
/* ---------- naming */
/* ---------- maps' hashes */
static struct known_hash *known_hash_find(
char const *path,
unsigned long size,
unsigned long modified)
{
struct map_share_files_globals *globals = &map_share_files_globals;
short index;
for (index = 0; index < MAXIMUM_KNOWN_HASHES; index++)
{
struct known_hash *known = &globals->known[index];
if (known->used && known->size == size && known->modified == modified && !csstrcasecmp(known->path, path))
return known;
}
return NULL;
}
void map_share_map_hash_remember(
char const *path,
byte const *hash)
{
struct map_share_files_globals *globals = &map_share_files_globals;
struct known_hash *known;
unsigned long size;
unsigned long modified;
short index;
if (!file_stamp(path, &size, &modified))
return;
known = known_hash_find(path, size, modified);
if (!known)
{
/* (the same path's of before, else the next in turn) */
for (index = 0; index < MAXIMUM_KNOWN_HASHES; index++)
{
if (globals->known[index].used && !csstrcasecmp(globals->known[index].path, path))
break;
}
if (index == MAXIMUM_KNOWN_HASHES)
{
index = globals->next_known;
globals->next_known = (short)((index + 1) % MAXIMUM_KNOWN_HASHES);
}
known = &globals->known[index];
csmemset(known, 0, sizeof(*known));
known->used = TRUE;
csstrncpy(known->path, path, sizeof(known->path) - 1);
known->size = size;
known->modified = modified;
}
csmemcpy(known->hash, hash, MAP_SHARE_HASH_BYTES);
}
boolean map_share_map_hash(
char const *path,
byte *hash)
{
struct map_share_files_globals *globals = &map_share_files_globals;
struct known_hash const *known;
struct hash_job job;
unsigned long size;
unsigned long modified;
short index;
if (!file_stamp(path, &size, &modified) || size > MAXIMUM_FILE_BYTES)
return FALSE;
known = known_hash_find(path, size, modified);
if (known)
{
csmemcpy(hash, known->hash, MAP_SHARE_HASH_BYTES);
return TRUE;
}
/* (one the catalog hashed for sharing) */
for (index = 0; index < MAXIMUM_SHARE_FILES; index++)
{
struct map_share_file const *file = &globals->files[index];
if (file->used && file->ready && file->size == size && file->modified == modified &&
!csstrcasecmp(file->path, path))
{
csmemcpy(hash, file->hash, MAP_SHARE_HASH_BYTES);
map_share_map_hash_remember(path, hash);
return TRUE;
}
}
/* (read here: a map takes a fraction of a second) */
csmemset(&job, 0, sizeof(job));
csstrncpy(job.path, path, sizeof(job.path) - 1);
hash_job_run(&job);
free(job.piece_hashes);
if (job.failed || job.size != size)
return FALSE;
csmemcpy(hash, job.hash, MAP_SHARE_HASH_BYTES);
map_share_map_hash_remember(path, hash);
return TRUE;
}
unsigned long map_share_hash_version(
byte const *hash)
{
unsigned long version = (unsigned long)hash[0] << 24 | (unsigned long)hash[1] << 16 |
(unsigned long)hash[2] << 8 | (unsigned long)hash[3];
/* (0 is a version a client checks nothing for) */
return version ? version : 1;
}
boolean map_share_map_version(
char const *path,
unsigned long *version)
{
byte hash[MAP_SHARE_HASH_BYTES];
if (!map_share_map_hash(path, hash))
return FALSE;
*version = map_share_hash_version(hash);
return TRUE;
}
/* ---------- names given back */
/* a folder and name's own number, to know it again */
static unsigned long naming_key(
@@ -480,18 +653,29 @@ static void naming_remember(
globals->named_hashes[globals->named_count++] = key;
}
void map_share_name_map(
void map_share_map_found(
char const *folder,
char const *name)
{
struct map_share_files_globals *globals = &map_share_files_globals;
unsigned long key = naming_key(folder, name);
char path[MAP_SHARE_PATH_SIZE];
short base_length = map_share_name_base_length(name);
short index;
/* (the custom_maps folder's alone: an install of Custom Edition is
left as it is, for Custom Edition) */
if (csstrcasecmp(folder, CUSTOM_EDITION_MAP_DIRECTORY) || map_share_name_hashed(name) || naming_known(key))
/* (the custom_maps folder's alone, a name ending in a hash, and none of
its name without it there: one of that name is another version) */
if (csstrcasecmp(folder, CUSTOM_EDITION_MAP_DIRECTORY) || !map_share_name_hashed(name) || naming_known(key) ||
csstrlen(folder) + base_length + 4 >= MAP_SHARE_PATH_SIZE)
{
return;
}
csprintf(path, "%s%.*s.map", folder, (int)base_length, name);
if (GetFileAttributesA(path) != (unsigned long)-1)
{
naming_remember(key);
return;
}
for (index = 0; index < globals->naming_count; index++)
{
if (!csstrcasecmp(globals->naming_queue[index].name, name) &&
@@ -533,90 +717,106 @@ static boolean rename_beside(
return MoveFileA(from_path, to_path) != 0;
}
/* a map hashed: renamed <name>_<hash>.map (the name cut short to fit the
map lists), with its picture and description */
/* a map named for its hash hashed: given its own name back, with its
picture and description, if the hash is its name's (a name this game gave
it) */
static void naming_finish(
struct naming const *naming,
struct hash_job const *job)
{
char digits[MAP_SHARE_NAME_DIGITS + 1];
char hashed[MAP_SHARE_PATH_SIZE];
char base[MAP_SHARE_PATH_SIZE];
char path[MAP_SHARE_PATH_SIZE];
short base_length = (short)csstrlen(naming->name);
short base_length = map_share_name_base_length(naming->name);
char const *loaded = custom_edition_cache_loaded_path();
short index;
csprintf(path, "%s%s.map", naming->folder, naming->name);
/* (in use: named another time) */
/* (in use, loaded or being sent: named another time) */
if (loaded && !csstrcasecmp(loaded, path))
return;
for (index = 0; index < MAXIMUM_SHARE_FILES; index++)
{
if (map_share_files_globals.files[index].used && !csstrcasecmp(map_share_files_globals.files[index].path, path))
return;
}
naming_remember(naming_key(naming->folder, naming->name));
if (job->failed)
{
error(_error_silent, "map sharing: '%s' could not be read to name it for its hash", path);
error(_error_silent, "map sharing: '%s' could not be read to give it its name back", path);
return;
}
map_share_hash_digits(job->hash, digits);
base_length = (short)MIN(base_length, MAXIMUM_MAP_NAME_LENGTH - MAP_SHARE_NAME_DIGITS - 1);
csmemcpy(hashed, naming->name, base_length);
csprintf(hashed + base_length, "_%s", digits);
if (!rename_beside(naming->folder, naming->name, hashed, ".map"))
if (csstrcmp(naming->name + base_length + 1, digits))
return;
csmemcpy(base, naming->name, base_length);
base[base_length] = 0;
if (!rename_beside(naming->folder, naming->name, base, ".map"))
{
error(_error_silent, "map sharing: '%s' was not renamed %s.map (that file exists, or this one is in use)",
path, hashed);
path, base);
return;
}
rename_beside(naming->folder, naming->name, hashed, ".bmp");
rename_beside(naming->folder, naming->name, hashed, ".txt");
error(_error_silent, "map sharing: %s.map is now %s.map, named for its hash", naming->name, hashed);
rename_beside(naming->folder, naming->name, base, ".bmp");
rename_beside(naming->folder, naming->name, base, ".txt");
csprintf(path, "%s%s.map", naming->folder, base);
map_share_map_hash_remember(path, job->hash);
error(_error_silent, "map sharing: %s.map has its name back, %s.map", naming->name, base);
custom_edition_maps_look_again();
}
void map_share_level_name_hash(
/* ---------- a host's level name */
void map_share_level_name_choose(
char *level_name,
long size)
{
struct naming naming;
struct hash_job job;
struct map_share_files_globals *globals = &map_share_files_globals;
char path[MAP_SHARE_PATH_SIZE];
char const *name;
char const *hashed;
char name[MAP_SHARE_PATH_SIZE];
char digits[MAP_SHARE_NAME_DIGITS + 1];
byte hash[MAP_SHARE_HASH_BYTES];
long length;
if (!level_name || !custom_edition_level_name(level_name))
return;
name = tag_name_strip_path(level_name);
if (map_share_name_hashed(name) || !custom_edition_map_file_path(level_name, path))
return;
/* (the file's own name, which may have its hash already) */
hashed = tag_name_strip_path(path);
if (_strnicmp(path, CUSTOM_EDITION_MAP_DIRECTORY, csstrlen(CUSTOM_EDITION_MAP_DIRECTORY)) ||
strchr(path + csstrlen(CUSTOM_EDITION_MAP_DIRECTORY), '\\'))
globals->chosen_level[0] = 0;
if (!level_name || !custom_edition_level_name(level_name) || !custom_edition_map_file_named(level_name, path) ||
!map_share_map_hash(path, hash))
{
return;
}
if (!map_share_name_hashed(name) && csstrlen(hashed) > 4 && csstrlen(hashed) - 4 == csstrlen(name) &&
!_strnicmp(hashed, name, csstrlen(name)))
/* the map's own name: its file's, without a hash it ends in (another
version's, or one of before) if that is its hash's */
csstrncpy(name, tag_name_strip_path(path), sizeof(name) - 1);
name[sizeof(name) - 1] = 0;
length = (long)csstrlen(name) - 4;
if (length <= 0)
return;
name[length] = 0;
map_share_hash_digits(hash, digits);
if (map_share_name_hashed(name) && !csstrcmp(name + length - MAP_SHARE_NAME_DIGITS, digits))
length -= MAP_SHARE_NAME_DIGITS + 1;
if (length <= 0 || csstrlen(CUSTOM_EDITION_LEVEL_NAME_PREFIX) + length >= (unsigned long)size ||
csstrlen(CUSTOM_EDITION_LEVEL_NAME_PREFIX) + length >= sizeof(globals->chosen_level))
{
/* named now: hashed here (a map's hash takes a fraction of a second
to work out) and renamed, unless it is loaded */
csmemset(&naming, 0, sizeof(naming));
csmemset(&job, 0, sizeof(job));
csstrncpy(naming.folder, CUSTOM_EDITION_MAP_DIRECTORY, sizeof(naming.folder) - 1);
csstrncpy(naming.name, name, sizeof(naming.name) - 1);
csstrncpy(job.path, path, sizeof(job.path) - 1);
hash_job_run(&job);
naming_finish(&naming, &job);
free(job.piece_hashes);
if (!custom_edition_map_file_path(level_name, path))
return;
hashed = tag_name_strip_path(path);
return;
}
/* the level name of the file found: custom_maps\<name>_<hash> */
if (csstrlen(hashed) <= 4 || csstrlen(CUSTOM_EDITION_LEVEL_NAME_PREFIX) + csstrlen(hashed) - 4 >= (unsigned long)size)
return;
csprintf(level_name, "%s%.*s", CUSTOM_EDITION_LEVEL_NAME_PREFIX, (int)(csstrlen(hashed) - 4), hashed);
if (!map_share_name_hashed(tag_name_strip_path(level_name)))
return;
error(_error_silent, "map sharing: the game's map is %s", level_name);
csprintf(level_name, "%s%.*s", CUSTOM_EDITION_LEVEL_NAME_PREFIX, (int)length, name);
csstrncpy(globals->chosen_level, level_name, sizeof(globals->chosen_level) - 1);
globals->chosen_version = map_share_hash_version(hash);
error(_error_silent, "map sharing: the game's map is %s (%s, version %08lx)", level_name, path,
globals->chosen_version);
}
boolean map_share_level_name_version(
char const *level_name,
unsigned long *version)
{
struct map_share_files_globals *globals = &map_share_files_globals;
if (!level_name || !globals->chosen_level[0] || csstrcasecmp(globals->chosen_level, level_name))
return FALSE;
*version = globals->chosen_version;
return TRUE;
}
/* ---------- the hashing thread's work */
@@ -654,6 +854,7 @@ void map_share_files_update(
file->piece_hashes = globals->job.piece_hashes;
csmemcpy(file->hash, globals->job.hash, MAP_SHARE_HASH_BYTES);
file->ready = TRUE;
map_share_map_hash_remember(file->path, file->hash);
}
}
else
@@ -679,7 +880,8 @@ void map_share_files_update(
globals->job_type = _hash_job_none;
}
/* the next: a download's pieces, a file to share, then a map to name */
/* the next: a download's pieces, a file to share, then a map to give
its name back */
if (globals->pieces_wanted)
{
globals->pieces_wanted = FALSE;
+6 -3
View File
@@ -1379,9 +1379,12 @@ static void structure_material_check(
{
long fit_count = PIN(material->vertices.count, 0,
MIN(vertex_data_size / vertex_size, MAXIMUM_VERTICES_PER_STRUCTURE_MATERIAL));
long fit_lightmap_count = PIN(material->lightmap_vertices.count, 0,
MIN((vertex_data_size - fit_count * vertex_size) / lightmap_vertex_size,
MAXIMUM_VERTICES_PER_STRUCTURE_MATERIAL));
/* (its lightmap vertices follow its vertices: with fewer of those,
they are not where they would be read, and there are none) */
long fit_lightmap_count = fit_count != material->vertices.count ? 0 :
PIN(material->lightmap_vertices.count, 0,
MIN((vertex_data_size - fit_count * vertex_size) / lightmap_vertex_size,
MAXIMUM_VERTICES_PER_STRUCTURE_MATERIAL));
tag_validate_correct(validation, "lightmap %d's material %d's %ld vertices and %ld lightmap vertices are not in"
" its %ld bytes: %ld and %ld", lightmap_index, material_index, material->vertices.count,
+13 -2
View File
@@ -2962,8 +2962,14 @@ static boolean game_globals_interface_tag_references_check(
/* the blocks whose first element is read through its address, or asked
for and used unchecked: a player's look function (player_control.c,
evaluate_piecewise_linear_function, from its address with its count),
the rasterizer's data (rasterizer_common.c) and the interface's tags
(interface.c, interface_tag_references_get) */
the rasterizer's data (rasterizer_common.c), the interface's tags
(interface.c, interface_tag_references_get), the camera (following_camera.c),
the player's information (bipeds.c, units.c, players.c, player_control.c)
and the first person interface (first_person_weapons.c, units.c): every map
examined has each. (An element past a block's end reads as zeros,
tag_empty_data: the falling damage and the multiplayer information, also
read unchecked, are left to that, as the game's own maps leave them empty,
ui.map the one and the campaign's the other) */
static boolean game_globals_check(
struct tag_validation *validation,
void *base)
@@ -2981,6 +2987,11 @@ static boolean game_globals_check(
tag_validate_refuse(validation, "has no rasterizer data or interface tags");
return FALSE;
}
if (!globals->camera.count || !globals->player_information.count || !globals->first_person_interface.count)
{
tag_validate_refuse(validation, "has no camera, player information or first person interface");
return FALSE;
}
return TRUE;
}
+32 -4
View File
@@ -20,7 +20,9 @@ walked once through its group's schema:
- counts are cut to what the game has room for, indices, enums and tags
past what they name are corrected, runtime values reset (tag_schema.h);
- then, once every tag has been through it, the checks that look at other
tags or at graphs (a bsp's nodes, a model's) run.
tags or at graphs (a bsp's nodes, a model's) run, and a tag's name that
lies in any tag's bytes, where what is written could end it elsewhere,
is none.
A map is refused if any pointer is wrong; anything else is corrected and
logged, and the map plays.
@@ -966,7 +968,8 @@ static boolean definition_fits(
return TRUE;
}
/* whether every group's schema fits (definition_fits), found once */
/* whether every group's schema fits (definition_fits), Custom Edition's
too, found once */
static boolean schemas_fit(
void)
{
@@ -975,12 +978,16 @@ static boolean schemas_fit(
if (fit == NONE)
{
struct tag_schema_group const *const *list;
struct tag_schema_group const *group;
fit = TRUE;
for (group = tag_schema_custom_edition_groups; group->group_tag && fit; group++)
{
if (group->definition && !definition_fits(group->definition, 0))
fit = FALSE;
}
for (list = tag_schema_group_lists; *list && fit; list++)
{
struct tag_schema_group const *group;
for (group = *list; group->group_tag && fit; group++)
{
if (group->definition && !definition_fits(group->definition, 0))
@@ -1136,6 +1143,27 @@ static boolean validate_tag_table(
for (absolute_index = 0; absolute_index < tag_count && !validation->refused; absolute_index++)
validate_instance(validation, &instances[absolute_index], _pass_checks);
/* each tag's name, now that every root, block and data is claimed: a
name in any of their bytes could lose its terminator to what is written
there (the corrections above, the game's runtime values), and is none.
(Tags may share a name: names are only read) */
for (absolute_index = 0; absolute_index < tag_count && !validation->refused; absolute_index++)
{
struct tag_validate_instance *instance = &instances[absolute_index];
if (instance->name == tag_validate_empty_name)
continue;
validation->tag_index = instance->tag_index;
validation->depth = 0;
validation->field_name = NULL;
if (!string_valid(validation, instance->name) ||
tag_validate_any_claimed(instance->name, (unsigned long)strlen(instance->name) + 1))
{
instance->name = (char *)tag_validate_empty_name;
tag_validate_correct(validation, "has its name in a tag's data: none");
}
}
tag_validate_globals.corrections = validation->corrections;
return !validation->refused;
+1 -1
View File
@@ -66,7 +66,7 @@ is newer (network_client_manager.c). A host advertises it, with its netcode,
in its game's advertisement's reserved bytes (network_server_message_handler.c),
which hosts built before there was a version send as zeros: version 0.
Raise it with any change to what the machines send each other. */
#define HALO_PORT_NETWORK_VERSION 28
#define HALO_PORT_NETWORK_VERSION 29
/* ... the advertisement's reserved bytes: the version (a little-endian word),
then flags */
#define HALO_PORT_ADVERTISED_VERSION_OFFSET 0
+12 -7
View File
@@ -50,7 +50,7 @@ void halo_vr_eye_camera(int eye, const float game_position[3],
/* copies the back buffer, just drawn for an eye, to that eye's image */
void halo_vr_capture_eye(int eye);
/* Both eyes in one draw (vr.multiview, where GL_OVR_multiview2 is): whether
/* Both eyes in one draw (debug.multiview, where GL_OVR_multiview2 is): whether
this frame draws so; the camera between the eyes whose view takes in both
(as halo_vr_eye_camera), which also starts drawing into both eyes' layers;
and after the draw, the layers taken for the eyes' images. */
@@ -169,7 +169,8 @@ enum
int halo_vr_body_mode(void);
/* a hand has just been thrust forward: a melee attack (xinput_sdl.c) */
int halo_vr_melee(void);
/* the free hand's grip as voice chat's push to talk (vr.c) */
/* voice chat's push to talk held: its button (vr.button_talk; the free
hand's grip by default) */
int halo_vr_push_to_talk_held(void);
/* the weight of the melee attack under way, from the swing's speed (0..1;
0 with none): the game scales melee damage by the player's run instead,
@@ -262,13 +263,13 @@ int halo_vr_controller_layout(void);
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
options' BUTTONS page): each action on one of eight 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. */
grip, the free hand's); left-handed with vr.mirror_controls, their mirror
images. The actions are numbered by the button each has by default. The
triggers, 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,
@@ -278,6 +279,9 @@ enum
HALO_VR_INPUT_MOVE_STICK,
HALO_VR_INPUT_TURN_STICK,
HALO_VR_INPUT_GRIP,
/* (pressed held a moment, if it took neither the weapon's foregrip nor
the Warthog's wheel: vr.c's free_grip_down) */
HALO_VR_INPUT_FREE_GRIP,
HALO_VR_INPUT_COUNT,
HALO_VR_ACTION_JUMP = HALO_VR_INPUT_A,
@@ -287,6 +291,7 @@ enum
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_TALK = HALO_VR_INPUT_FREE_GRIP, /* voice chat's push to talk */
HALO_VR_ACTION_COUNT = HALO_VR_INPUT_COUNT,
};
/* an action's setting ("vr.button_jump"), a button's value in it ("a"),
+1 -1
View File
@@ -1,4 +1,4 @@
version 28
version 29
bound DISCORD_ID_SIZE 24
bound DISCORD_NAME_SIZE 40
bound DISTRIBUTED_INPUT_HISTORY 4
+23 -2
View File
@@ -243,6 +243,8 @@ struct retired_session
{
unsigned char secret[P2P_SHA256_SIZE];
unsigned long virtual_address;
/* the machine whose it was */
unsigned char identifier[P2P_IDENTIFIER_SIZE];
};
/* a TCP stand-in for one port of a peer: takes the game's connections */
@@ -785,7 +787,24 @@ static int is_virtual_address(unsigned long address)
return (network_long(address) & 0xFFC00000) == 0x64400000;
}
/* an address in 100.64.0.0/10 from the identifier, not one another peer has */
/* whether address was another machine's than the identifier's, recently: the
game may still have traffic for that machine, which a new peer at the
address would take (and whose traffic would seem to come from it) */
static int address_retired_by_another(unsigned long address, const unsigned char *peer_identifier)
{
int index;
for (index = 0; index < MAXIMUM_RETIRED_SESSIONS; index++)
{
if (p2p.retired[index].virtual_address == address &&
memcmp(p2p.retired[index].identifier, peer_identifier, P2P_IDENTIFIER_SIZE))
return 1;
}
return 0;
}
/* an address in 100.64.0.0/10 from the identifier, not one another peer has
or had lately (the same machine reached again has its own back) */
static unsigned long virtual_address_for(const unsigned char *peer_identifier)
{
unsigned char digest[P2P_SHA256_SIZE];
@@ -798,7 +817,8 @@ static unsigned long virtual_address_for(const unsigned char *peer_identifier)
unsigned long address = 0x64400000 | (value & 0x3FFFFF);
/* no .0 or .255, which look like network and broadcast addresses */
if ((address & 255) != 0 && (address & 255) != 255 && !find_peer_by_address(network_long(address)))
if ((address & 255) != 0 && (address & 255) != 255 && !find_peer_by_address(network_long(address)) &&
!address_retired_by_another(network_long(address), peer_identifier))
return network_long(address);
value++;
}
@@ -948,6 +968,7 @@ static void drop_peer(struct peer *peer, const char *reason)
again */
memcpy(retired->secret, peer->secret, P2P_SHA256_SIZE);
retired->virtual_address = peer->virtual_address;
memcpy(retired->identifier, peer->identifier, P2P_IDENTIFIER_SIZE);
memset(peer, 0, sizeof(*peer));
/* still joining: a new request, with a nonce of its own (the host
makes no second session from one request, which anyone who saw it could
+20 -10
View File
@@ -200,6 +200,12 @@ 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.refresh_rate_auto", _config_boolean, "true", "HALO_VR_REFRESH_RATE_AUTO", _environment_value,
_platform_android,
"While frames come late at that rate (big online games), step down to the\n"
"next the headset has (90 to 72 Hz), and back up once they would keep up\n"
"again; the headset shows a late frame twice, a doubled picture when\n"
"turning. Only where the headset lets the game choose (not SteamVR)." },
{ "vr.graphics", _config_string, "\"medium\"", "HALO_VR_GRAPHICS", _environment_value, _platform_android,
"Which of the costlier effects are drawn (\"medium\" by default): \"auto\" all of them while the\n"
"GPU keeps up, leaving out one after another (the shield's glow, object\n"
@@ -227,9 +233,6 @@ static const struct config_setting config_settings[] =
"away from the player's body (walls are not checked)." },
{ "vr.crouch_limit", _config_real, "0.9", "HALO_VR_CROUCH_LIMIT", _environment_value, _platform_android,
"How far, in metres, the view follows your head down." },
{ "vr.multiview", _config_boolean, "true", "HALO_VR_MULTIVIEW", _environment_value, _platform_android,
"Draw both eyes in one pass (GL_OVR_multiview2), from between them: about\n"
"half the drawing's cost on the processor. false: one eye after the other." },
{ "vr.log_performance", _config_boolean, "false", "HALO_VR_LOG_PERFORMANCE", _environment_value, _platform_android,
"Log the frame rate and where the frame time goes every five seconds\n"
"(adb logcat -s halo)." },
@@ -252,7 +255,9 @@ static const struct config_setting config_settings[] =
"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"
"\"grip\" (the weapon hand's), \"free_grip\" (the free hand's, held a\n"
"moment, when it took neither the weapon's foregrip nor the Warthog's\n"
"wheel), 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)," },
@@ -267,8 +272,11 @@ static const struct config_setting config_settings[] =
_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." },
"melee" },
{ "vr.button_talk", _config_string, "\"free_grip\"", "HALO_VR_BUTTON_TALK", _environment_value,
_platform_android,
"and voice chat's push to talk (audio.voice_chat). The triggers and the\n"
"menu button keep 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,
@@ -775,10 +783,12 @@ static const struct config_setting config_settings[] =
"machines of twice it), to test the netcode as over the internet; 0 none." },
{ "debug.network_loss", _config_real, "0.0", "HALO_NETWORK_LOSS", _environment_value, _platform_all,
"Percent of datagrams received that are dropped, for the same; 0 none." },
{ "debug.multiview_steamvr", _config_boolean, "false", "HALO_MULTIVIEW_STEAMVR", _environment_value,
_platform_android,
"On SteamVR (the Steam Frame) the eyes are drawn one by one, whatever\n"
"vr.multiview says: true draws them at once there too, for testing." },
{ "debug.multiview", _config_boolean, "false", "HALO_MULTIVIEW", _environment_value, _platform_android,
"Draw both eyes in one pass (GL_OVR_multiview2), from between them: about\n"
"half the drawing's cost on the processor, but the eyes' pictures did not\n"
"always fuse (doubled as the head turned: on the Steam Frame now and then,\n"
"on the Quest 3 in big online games). Off, one eye after the other; on,\n"
"for finding why." },
{ "debug.gl_thread", _config_integer, "-1", "HALO_GL_THREAD", _environment_value, _platform_android,
"Mesa's GL thread (its drivers: the Steam Frame's): 1 on, 0 off, -1 as the\n"
"driver chooses. Read when the app starts." },
+4
View File
@@ -395,6 +395,10 @@ void voice_audio_play(int speaker, unsigned short sequence, const unsigned char
if (length <= 0 || length > VOICE_MAXIMUM_PACKET)
return;
/* (every build sends a frame of 20 ms: anything else, damaged, is
dropped before the decoder sees it, and made up for as lost) */
if (opus_packet_get_nb_samples(packet, length, VOICE_RATE) != VOICE_FRAME_SAMPLES)
return;
pthread_mutex_lock(&voice_lock);
slot = voice_slot_for(speaker, TRUE);
if (!slot)
+183 -36
View File
@@ -22,11 +22,11 @@ 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",
"vr.button_zoom", "vr.button_melee", "vr.button_talk",
};
static const char *const vr_input_values[HALO_VR_INPUT_COUNT] =
{
"a", "b", "x", "y", "move_stick", "turn_stick", "grip",
"a", "b", "x", "y", "move_stick", "turn_stick", "grip", "free_grip",
};
const char *halo_vr_action_setting(int action)
@@ -138,7 +138,7 @@ static struct
float seat_frame_yaw;
int seat_frame_valid;
float seat_turn;
int multiview; /* vr.multiview: both eyes in one draw */
int multiview; /* debug.multiview: both eyes in one draw */
int multiview_logged;
int multiview_logged_views;
/* vr.foveation's level last logged (+1; 0: none yet) */
@@ -230,6 +230,10 @@ static struct
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 free hand's grip as a button (free_grip_down): closed since (0
open), and whether it took the weapon or the wheel since */
unsigned long long free_grip_since_us;
int free_grip_taken;
/* 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 */
@@ -294,6 +298,15 @@ static struct
vr.resolution_scale as the app started with it */
float refresh_rate_wanted;
float resolution_scale;
/* vr.refresh_rate_auto (refresh_governor): the rate asked for now (the
one wanted, or lower while frames come late), the rates below and above
it, and the window's frames: all, late at this rate, and those that
would be late at the one above */
int refresh_auto;
float refresh_rate_now, refresh_lower, refresh_higher, refresh_period_ms;
unsigned long long refresh_window_us, refresh_changed_us;
unsigned long refresh_frames, refresh_late, refresh_tight;
int refresh_bad_windows, refresh_good_windows;
} vr;
/* d3d8_gl.c */
@@ -581,12 +594,23 @@ static void vr_read_live_settings(void)
/* (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)
int automatic = config_boolean("vr.refresh_rate_auto");
if (vr.active && (refresh_rate != vr.refresh_rate_wanted ||
(!automatic && vr.refresh_rate_now != refresh_rate)))
{
platform_log("VR: refresh rate %.0f Hz asked for", refresh_rate);
host_xr_set_refresh_rate(refresh_rate);
}
if (refresh_rate != vr.refresh_rate_wanted || automatic != vr.refresh_auto)
{
/* (the governor starts over from the rate chosen) */
vr.refresh_rate_now = refresh_rate;
vr.refresh_window_us = 0;
vr.refresh_bad_windows = vr.refresh_good_windows = 0;
}
vr.refresh_rate_wanted = refresh_rate;
vr.refresh_auto = automatic;
}
}
@@ -621,7 +645,7 @@ void halo_vr_initialize(void)
vr.two_hand_reach = (float)config_real("vr.two_hand_reach");
vr.melee_swing_speed = (float)config_real("vr.melee_swing_speed");
vr.grenade_throw_speed = (float)config_real("vr.grenade_throw_speed");
vr.multiview = config_boolean("vr.multiview");
vr.multiview = config_boolean("debug.multiview");
vr.vehicle_first_person = config_boolean("vr.vehicle_first_person");
vr.vehicle_spawn = config_boolean("debug.vehicle_spawn");
vr.turret_handles = config_boolean("vr.turret_handles");
@@ -654,16 +678,13 @@ void halo_vr_initialize(void)
vr.active = 1;
platform_log("VR: %s, eyes %dx%d at %.0f Hz", vr.info.system_name, vr.info.eye_width, vr.info.eye_height,
vr.info.refresh_rate);
/* SteamVR (the Steam Frame): each eye drawn apart. With both drawn at
once the eyes' pictures did not fuse, now and then: each alone was
sharp, the two together doubled as the head turned (2026-10-05), and
one by one costs it no more (its time goes to the driver either way).
debug.multiview_steamvr draws them at once there again, for finding why */
if (vr.multiview && strstr(vr.info.system_name, "SteamVR") && !config_boolean("debug.multiview_steamvr"))
{
vr.multiview = 0;
platform_log("VR: SteamVR: the eyes drawn one by one (debug.multiview_steamvr)");
}
/* Each eye drawn apart (debug.multiview draws both at once). At once,
the eyes' pictures did not always fuse: on the Steam Frame now and then
(each alone sharp, the two together doubled as the head turned,
2026-10-05; one by one costs it no more there), and on the Quest 3 in
big online games (doubled from the start as the head turned, none with
48 bots; one by one, none online either, 2026-10-16) */
platform_log("VR: the eyes drawn %s (debug.multiview)", vr.multiview ? "at once" : "one by one");
}
int halo_vr_active(void)
@@ -885,6 +906,93 @@ static void performance_phases(unsigned long long now)
vr.frame_ticks = 0;
}
/* vr.refresh_rate_auto: frames late at the headset's rate (a game's big
online games: the ticks cost as a client in step with the players) are
shown twice by the headset, a doubled picture when turning. While more
than one in 32 of them come late, two windows of REFRESH_WINDOW_US
running, the rate steps down to the next the headset offers (the Quest
3's: 90, 80, 72 Hz); while hardly any would be late at the next one up (one in a
hundred), ten windows running, it steps back up, never above
vr.refresh_rate. Frames of a load (REFRESH_LOAD_MS and more) count for
neither; a step waits REFRESH_SETTLE_US (down) or REFRESH_RAISE_US (up)
after the last. */
#define REFRESH_WINDOW_US 2000000ULL
#define REFRESH_SETTLE_US 10000000ULL
#define REFRESH_RAISE_US 30000000ULL
#define REFRESH_LOAD_MS 50.0f
static void refresh_rates_around(void)
{
float rates[16], current = 0.0f;
int count = host_xr_refresh_rates(rates, 16, &current);
float ceiling = vr.refresh_rate_wanted > 1.0f ? vr.refresh_rate_wanted + 0.5f : 1e6f;
int index;
vr.refresh_lower = vr.refresh_higher = 0.0f;
if (current <= 1.0f)
current = vr.refresh_rate_now;
vr.refresh_period_ms = current > 1.0f ? 1000.0f / current : 0.0f;
for (index = 0; index < count; index++)
{
if (rates[index] < current - 0.5f && rates[index] > vr.refresh_lower)
vr.refresh_lower = rates[index];
if (rates[index] > current + 0.5f && rates[index] <= ceiling &&
(!vr.refresh_higher || rates[index] < vr.refresh_higher))
vr.refresh_higher = rates[index];
}
}
static void refresh_step(float rate, unsigned long long now, const char *why)
{
platform_log("VR: refresh rate %.0f Hz (from %.0f): %s (%lu of %lu frames late, %lu too slow for the rate above)",
rate, 1000.0f / vr.refresh_period_ms, why, vr.refresh_late, vr.refresh_frames, vr.refresh_tight);
host_xr_set_refresh_rate(rate);
vr.refresh_rate_now = rate;
vr.refresh_changed_us = now;
vr.refresh_bad_windows = vr.refresh_good_windows = 0;
}
static void refresh_governor(void)
{
unsigned long long now = now_us();
float work_ms = (float)(now - vr.frame_start_us) * 1e-3f;
if (!vr.refresh_auto)
return;
if (!vr.refresh_window_us)
{
refresh_rates_around();
vr.refresh_window_us = now;
vr.refresh_frames = vr.refresh_late = vr.refresh_tight = 0;
return;
}
if (work_ms < REFRESH_LOAD_MS && vr.refresh_period_ms > 0.0f)
{
vr.refresh_frames++;
if (work_ms + 1.0f > vr.refresh_period_ms)
vr.refresh_late++;
if (vr.refresh_higher > 1.0f && work_ms + 1.0f > 1000.0f / vr.refresh_higher)
vr.refresh_tight++;
}
if (now - vr.refresh_window_us < REFRESH_WINDOW_US)
return;
if (vr.refresh_frames)
{
vr.refresh_bad_windows = vr.refresh_late * 32 > vr.refresh_frames ? vr.refresh_bad_windows + 1 : 0;
vr.refresh_good_windows = vr.refresh_tight * 100 <= vr.refresh_frames ? vr.refresh_good_windows + 1 : 0;
if (vr.refresh_lower > 1.0f && vr.refresh_bad_windows >= 2 && now - vr.refresh_changed_us >= REFRESH_SETTLE_US)
refresh_step(vr.refresh_lower, now, "frames come late");
else if (vr.refresh_higher > 1.0f && vr.refresh_good_windows >= 10 &&
now - vr.refresh_changed_us >= REFRESH_RAISE_US)
{
refresh_step(vr.refresh_higher, now, "frames would keep up again");
}
}
refresh_rates_around();
vr.refresh_window_us = now;
vr.refresh_frames = vr.refresh_late = vr.refresh_tight = 0;
}
static void performance_report(int stereo)
{
unsigned long long now = now_us();
@@ -1354,19 +1462,6 @@ int halo_vr_finger_curls(short local_player_index, int hand, float curls[4])
return HALO_VR_FINGERS_CURLED | ((state->buttons & HALO_XR_BUTTON_TOUCH_KNOWN) ? HALO_VR_FINGERS_TOUCH : 0);
}
/* Voice chat's push to talk in VR (network_voice.c, through
halo_push_to_talk_held): the free hand's grip, while it holds neither the
weapon's foregrip nor the Warthog's wheel; the grip has no button of the
game's of its own. */
int halo_vr_push_to_talk_held(void)
{
int hand = free_hand();
if (!vr.active || !vr.frame.focused || !(vr.frame.hands[hand].buttons & HALO_XR_BUTTON_SQUEEZE))
return 0;
return !vr.two_hand_gripping && !vr.wheel_held[hand];
}
int halo_vr_melee(void)
{
return vr.active && now_us() < vr.melee_until_us;
@@ -1895,6 +1990,7 @@ void halo_vr_present(unsigned int back_buffer_texture)
vr.last_overlay_mode = vr.overlay_mode;
vr.last_frame_stereo = submit.eyes;
vr.frame_begun = 0;
refresh_governor();
if (vr.log_performance)
performance_report(submit.eyes);
}
@@ -2062,7 +2158,7 @@ float halo_vr_refresh_rate(void)
return vr.active ? vr.info.refresh_rate : 0.0f;
}
/* ---------- both eyes at once (vr.multiview)
/* ---------- both eyes at once (debug.multiview)
The scene is drawn once, from a camera between the eyes whose view takes in
both eyes' (with a margin, for what is near: the eyes stand apart), into two
@@ -3406,28 +3502,63 @@ 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,
HALO_XR_HAND_RIGHT, HALO_XR_HAND_RIGHT, HALO_XR_HAND_LEFT,
};
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,
HALO_XR_BUTTON_THUMBSTICK, HALO_XR_BUTTON_SQUEEZE, HALO_XR_BUTTON_SQUEEZE,
};
/* The free hand's grip as a button (HALO_VR_INPUT_FREE_GRIP; push to talk
by default), the hands mirrored: pressed once closed for
FREE_GRIP_DELAY_US, at halo_vr_two_hand's threshold, unless it took the
weapon's foregrip or the Warthog's wheel (which both decide sooner) since
it closed; that press then is theirs until it opens. So a grip reaching
for the weapon presses nothing, not even for a moment. */
#define FREE_GRIP_DELAY_US 150000ULL
static int free_grip_down(const struct halo_xr_hand *free)
{
unsigned long long now = now_us();
if (free->squeeze <= (vr.free_grip_since_us ? 0.35f : 0.6f))
{
vr.free_grip_since_us = 0;
vr.free_grip_taken = 0;
return 0;
}
if (!vr.free_grip_since_us)
vr.free_grip_since_us = now;
if (vr.two_hand_gripping || vr.wheel_held[free_hand()])
vr.free_grip_taken = 1;
return !vr.free_grip_taken && now - vr.free_grip_since_us >= FREE_GRIP_DELAY_US;
}
/* the buttons' presses (HALO_VR_INPUT_*), the hands mirrored */
static void controls_down(const struct halo_xr_hand hands[HALO_XR_HAND_COUNT], int down[HALO_VR_INPUT_COUNT])
{
int input;
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;
down[HALO_VR_INPUT_FREE_GRIP] = free_grip_down(&hands[HALO_XR_HAND_LEFT]);
}
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;
controls_down(hands, down);
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]])
/* (talking is no button of the game's: halo_vr_push_to_talk_held) */
if (action != HALO_VR_ACTION_TALK && down[vr.action_inputs[action]])
hands[input_hands[action]].buttons |= input_buttons[action];
}
}
@@ -3441,6 +3572,22 @@ static void controls_in_play(struct halo_xr_hand hands[HALO_XR_HAND_COUNT])
controls_remapped(hands);
}
/* Voice chat's push to talk in VR (network_voice.c, through
halo_push_to_talk_held): its button (vr.button_talk), in the menus too */
int halo_vr_push_to_talk_held(void)
{
struct halo_xr_hand hands[HALO_XR_HAND_COUNT];
int down[HALO_VR_INPUT_COUNT];
if (!vr.active || !vr.frame.focused)
return 0;
memcpy(hands, vr.frame.hands, sizeof(hands));
if (vr.left_handed)
controllers_mirrored(hands);
controls_down(hands, down);
return down[vr.action_inputs[HALO_VR_ACTION_TALK]];
}
/* 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)
{
+30
View File
@@ -42,10 +42,40 @@ DWORD WINAPI XLaunchNewImageA(LPCSTR image_path, PLAUNCH_DATA launch_data)
exit(EXIT_SUCCESS);
}
/* port: a European disc in two languages has a maps folder for each, and
the Android and Steam Frame disc image setups (XisoExtractor.java,
steam_frame_xiso.py) write the language of the one they took into
maps/language.txt: the Xbox's language, unless game.language says another */
static const char *maps_language(void)
{
static char language[4];
static BOOL read;
if (!read)
{
char path[MAX_PATH];
FILE *file;
read = TRUE;
snprintf(path, sizeof(path), "%s/maps/language.txt", platform_data_root());
if ((file = fopen(path, "r")) != NULL)
{
if (!fgets(language, sizeof(language), file))
language[0] = '\0';
language[2] = '\0';
fclose(file);
}
}
return language;
}
DWORD WINAPI XGetLanguage(void)
{
const char *language = config_string("game.language");
if (!*language)
language = maps_language();
if (*language)
{
if (!strncmp(language, "ja", 2)) return XC_LANGUAGE_JAPANESE;
+1 -1
View File
@@ -605,7 +605,7 @@ int halo_push_to_talk_held(void)
int slot;
#ifdef HALO_ANDROID
/* (port: in VR the free hand's grip too, halo_vr_push_to_talk_held) */
/* (port: in VR its button too, vr.button_talk: halo_vr_push_to_talk_held) */
if (halo_vr_active() && halo_vr_push_to_talk_held())
return 1;
#endif
+38
View File
@@ -0,0 +1,38 @@
This software is available under 2 licenses -- choose whichever you prefer.
------------------------------------------------------------------------------
ALTERNATIVE A - MIT License
Copyright (c) 2017 Sean Barrett
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
ALTERNATIVE B - Public Domain (www.unlicense.org)
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
------------------------------------------------------------------------------
+39
View File
@@ -0,0 +1,39 @@
# stb_vorbis
An Ogg Vorbis decoder in one C file, by Sean Barrett and its other authors,
public domain or MIT licensed, as the user chooses (see `LICENSE`, the
notice at the end of `stb_vorbis.c`).
Upstream: https://github.com/nothings/stb, commit
2c980bb59875b0d32144a71867fbdebb2f77cd20 (`stb_vorbis.c` v1.22, SHA-256
`4c7cb2ff1f7011e9d67950446b7eb9ca044f2e464d76bfbb0b84dd2e23e65636`).
Unlike the other libraries here, `stb_vorbis.c` is **not** unchanged: a Custom
Edition map is untrusted input, and the decoder reads the map's streams, so
it carries checks upstream does not have, each marked with a `port:`
comment. As changed, its SHA-256 is
`16ceceac06245040d7ec5965d150a08aa53d663a4cecbe3302f447a12a03b6a3`.
- `alloca.h` included on macOS too.
- `submap_floor` and `submap_residue` hold 16 entries, as many as the
mapping's 4-bit submap count (plus one) allows (upstream has 15).
- `setup_malloc` and `setup_temp_malloc` refuse a size whose rounding to 8
would overflow.
- `lookup1_values` refuses a code book of no dimensions or entries (it
divided by the dimensions), and compares the powers as floats rather than
casting values past an int's range to int.
- `DECODE` looks up a sparse code book's value only for a code that was
found (the scalar decode gives -1 when none is).
- The comment header's vendor string and comment lengths, and its comment
count, are no larger than the stream holds; the comment list is zeroed
when allocated, and its length 0 when it is not.
- A code book's entries times its dimensions is at most 2^24.
- `do_floor` masks the table index of the flat run after a floor's last
point to 255, as `draw_line` masks its own (upstream reads past
`inverse_db_table` for a floor value over 255).
`LICENSE` is a copy of that notice. The Custom Edition maps' Ogg Vorbis
sounds are decoded with it when a map loads
(`port/linux/game/custom_edition_sounds.c`, which includes it for its
declarations); it is compiled as the port's game units are and builds into
the Linux, Windows, Android and web builds.
@@ -1760,6 +1760,8 @@ static int codebook_decode_scalar(vorb *f, Codebook *c)
#endif
// port: a sparse code book's value looked up only for a code that was
// found (the scalar decode gives -1 when none is)
#define DECODE(var,f,c) \
DECODE_RAW(var,f,c) \
if (c->sparse && var >= 0) var = c->sorted_values[var];
@@ -3108,8 +3110,10 @@ static int do_floor(vorb *f, Mapping *map, int i, int n, float *target, YTYPE *f
}
if (lx < n2) {
// optimization of: draw_line(target, lx,ly, n,ly, n2);
// port: the index masked as draw_line masks it (a floor value is
// the stream's, and the table holds 256)
for (j=lx; j < n2; ++j)
LINE_OP(target[j], inverse_db_table[ly]);
LINE_OP(target[j], inverse_db_table[ly&255]);
CHECK(f);
}
}
@@ -3687,6 +3691,7 @@ static int start_decoder(vorb *f)
for(i=0; i < f->comment_list_length; ++i) {
len = get32_packet(f);
// port: as the vendor's length
if (len < 0 || (USE_MEMORY(f) && len > f->stream_end - f->stream)) return error(f, VORBIS_invalid_setup);
f->comment_list[i] = (char*)setup_malloc(f, sizeof(char) * (len+1));
if (f->comment_list[i] == NULL) return error(f, VORBIS_outofmem);
+1 -1
View File
@@ -20,7 +20,7 @@ NOTICES = [
("Monocypher (internet play's signatures)", "port/third_party/monocypher/LICENCE.md"),
("tomlc17 (config.toml)", "port/third_party/tomlc17/LICENSE"),
("musl (the maths functions)", "port/third_party/musl-math/COPYRIGHT"),
("stb_vorbis (loose sounds)", "port/third_party/stb/LICENSE"),
("stb_vorbis (loose sounds)", "port/third_party/stb_vorbis/LICENSE"),
("extract-xiso (how the disc image is read)", "port/third_party/extract-xiso/LICENSE.TXT"),
("Overpass (the menus' text and the page's)", "port/assets/fonts/Overpass-OFL.txt"),
("OpenCE (the title font, from Newtown)", "port/assets/fonts/OpenCE-OFL.txt"),
+5 -4
View File
@@ -1086,13 +1086,14 @@ void cache_files_show_multiplayer_unavailable(
/* port: the version of the map a network game is on, which the host sends
in its game record (network_game_map's version, which the Xbox's left 0):
a Custom Edition map's header checksum, which differs between versions of
it. 0, which a client checks nothing for, for the game's own maps, whose
builds of other regions play together. */
a Custom Edition map's hash's first 32 bits (port/linux/game/map_share.h),
which differ between versions of it, a client playing its file of that
version whatever its name. 0, which a client checks nothing for, for the
game's own maps, whose builds of other regions play together. */
unsigned long cache_files_map_version(
char const *map_name)
{
return custom_edition_level_name(map_name) ? custom_edition_map_checksum(map_name) : 0;
return custom_edition_level_name(map_name) ? custom_edition_map_version(map_name) : 0;
}
/* port: whether this machine has the map a network game is on (a client
+3 -1
View File
@@ -543,8 +543,10 @@ static const char *cache_block_get_sound_permutation_name(
struct xbox_cache_sound_datum *cache_sound;
cache_sound = datum_get(xbox_sound_cache_globals.cache_sounds, block_index);
sprintf(
/* port: snprintf (a map's tag's name may be any length) */
snprintf(
xbox_sound_cache_globals.debug_block_name,
sizeof(xbox_sound_cache_globals.debug_block_name),
"%s (%s)",
tag_get_name(cache_sound->sound->runtime_tag_index),
cache_sound->sound);
+5 -2
View File
@@ -13485,10 +13485,13 @@ static boolean hs_rebuild_source(
struct file_reference source_files[8];
short source_file_count;
short source_file_index;
char *scripts_name;
tag_block_resize(&global_scenario_get()->hs_source_files, 0);
sprintf(scenario_path, "data\\%s", tag_get_name(global_scenario_index));
sprintf(strrchr(scenario_path, '\\') + 1, "scripts");
/* port: snprintf (a map's tag's name may be any length) */
snprintf(scenario_path, sizeof(scenario_path), "data\\%s", tag_get_name(global_scenario_index));
scripts_name = strrchr(scenario_path, '\\') + 1;
snprintf(scripts_name, sizeof(scenario_path) - (scripts_name - scenario_path), "scripts");
file_reference_create_from_path(
&global_scripts,
"data\\global_scripts.hsc",
+16 -8
View File
@@ -1367,10 +1367,14 @@ static void temporary_hud_draw(
if (equipment_index != NONE)
{
struct object_datum *equipment = object_get(equipment_index);
sprintf(
/* port: snprintf, here and below, and the names' paths stripped as
the grenade's is (a map's tag's name may be any length, and need
not have a path) */
snprintf(
temporary,
sizeof(temporary),
"%s (press WHITE to use)|n",
strrchr(tag_get_name(equipment->definition_index), '\\') + 1);
tag_name_strip_path(tag_get_name(equipment->definition_index)));
}
else
{
@@ -1383,8 +1387,9 @@ static void temporary_hud_draw(
&scenario_get_game_globals()->grenades,
unit->unit.current_grenade_index,
struct game_globals_grenade);
sprintf(
snprintf(
temporary + csstrlen(temporary),
sizeof(temporary) - csstrlen(temporary),
"%d %s|n",
unit->unit.grenade_counts[unit->unit.current_grenade_index],
tag_name_strip_path(tag_get_name(grenade->projectile.index)));
@@ -1403,10 +1408,11 @@ static void temporary_hud_draw(
&weapon_definition->weapon.magazines,
0,
struct weapon_magazine_definition);
sprintf(
snprintf(
temporary + csstrlen(temporary),
sizeof(temporary) - csstrlen(temporary),
"%s|ntotal %d/%d|nloaded %d/%d|nheat %3.2f|nage %3.2f|n",
strrchr(tag_get_name(weapon->definition_index), '\\') + 1,
tag_name_strip_path(tag_get_name(weapon->definition_index)),
weapon->weapon.magazines[0].rounds_total,
magazine_definition->rounds_total_maximum,
weapon->weapon.magazines[0].rounds_loaded,
@@ -1416,14 +1422,16 @@ static void temporary_hud_draw(
}
else
{
sprintf(
snprintf(
temporary + csstrlen(temporary),
sizeof(temporary) - csstrlen(temporary),
"%s|n",
strrchr(tag_get_name(weapon->definition_index), '\\') + 1);
tag_name_strip_path(tag_get_name(weapon->definition_index)));
}
sprintf(
snprintf(
temporary + csstrlen(temporary),
sizeof(temporary) - csstrlen(temporary),
"%s%s",
(real)player->powerup_durations[_player_powerup_active_camouflage] > 0.0f
? "ACTIVE-CAMOUFLAGE "
+55 -18
View File
@@ -7468,6 +7468,7 @@ enum
_vr_option_graphics,
_vr_option_foveation,
_vr_option_refresh_rate,
_vr_option_refresh_auto,
_vr_option_resolution,
_vr_option_controls,
_vr_option_quit,
@@ -7519,10 +7520,10 @@ static short const vr_option_pages[NUMBER_OF_VR_OPTION_PAGES][VR_OPTIONS_PER_PAG
{ _vr_option_button_first + HALO_VR_ACTION_JUMP, _vr_option_button_first + HALO_VR_ACTION_LIGHT,
_vr_option_button_first + HALO_VR_ACTION_ACTION, _vr_option_button_first + HALO_VR_ACTION_WEAPON,
_vr_option_button_first + HALO_VR_ACTION_CROUCH, _vr_option_button_first + HALO_VR_ACTION_ZOOM,
_vr_option_button_first + HALO_VR_ACTION_MELEE, _vr_option_buttons_reset, _vr_option_controls,
NONE, NONE, NONE },
{ _vr_option_graphics, _vr_option_foveation, _vr_option_refresh_rate, _vr_option_resolution, _vr_option_quit,
NONE, NONE, NONE, NONE, NONE, NONE, NONE },
_vr_option_button_first + HALO_VR_ACTION_MELEE, _vr_option_button_first + HALO_VR_ACTION_TALK,
_vr_option_buttons_reset, _vr_option_controls, NONE, NONE },
{ _vr_option_graphics, _vr_option_foveation, _vr_option_refresh_rate, _vr_option_refresh_auto,
_vr_option_resolution, _vr_option_quit, NONE, NONE, NONE, NONE, NONE, NONE },
};
/* the page a row is on, and its place there */
@@ -7582,7 +7583,7 @@ options' rows. On the Quest A and B are on the right, X and Y on the
left, and mirrored the other way round; on the Steam Frame all four are
on the right, and mirrored the left D-pad takes them (down, left, right,
up). The sticks' clicks change sides with them; the grip is the weapon
hand's. */
hand's, the free grip the other hand's. */
static char const *ui_vr_input_name(
short input,
boolean full,
@@ -7604,7 +7605,8 @@ static char const *ui_vr_input_name(
name = full ? (side ? "Right stick click" : "Left stick click") : "Stick click";
break;
case HALO_VR_INPUT_GRIP:
side = (short)(ui_vr_left_handed() ? 0 : 1);
case HALO_VR_INPUT_FREE_GRIP:
side = (short)((input == HALO_VR_INPUT_FREE_GRIP) != (ui_vr_left_handed() == 0));
name = full ? (side ? "Right grip" : "Left grip") : "Grip";
break;
default:
@@ -7631,10 +7633,11 @@ options' rows name them, and in short (the Steam Frame's stick lines) */
static char const *const vr_action_names[HALO_VR_ACTION_COUNT] =
{
"Jump", "Light; held: grenades", "Action, reload", "Switch weapon", "Crouch", "Zoom", "Melee",
"Push to talk",
};
static char const *const vr_action_short_names[HALO_VR_ACTION_COUNT] =
{
"jump", "light", "action", "weapon", "crouch", "zoom", "melee",
"jump", "light", "action", "weapon", "crouch", "zoom", "melee", "talk",
};
/* an action moved to the next button or the one before: the action that
@@ -7723,9 +7726,10 @@ static void ui_vr_options_vignette_text(
/* vr.body's values, and as the panel shows them */
static char const *const vr_bodies[] = { "full", "arms", "hands", "off" };
static char const *const vr_body_names[] = { "Full body", "Arms and hands", "Hands only", "Weapon only" };
/* audio.voice_chat's (network_voice.c); push to talk is the free hand's grip */
/* audio.voice_chat's (network_voice.c); push to talk's button is on the
BUTTONS page (vr.button_talk) */
static char const *const vr_voice_chats[] = { "push_to_talk", "open_mic", "off" };
static char const *const vr_voice_chat_names[] = { "Push to talk (grip)", "Open mic", "Off" };
static char const *const vr_voice_chat_names[] = { "Push to talk", "Open mic", "Off" };
static short ui_vr_options_voice_chat(
void)
@@ -7939,6 +7943,9 @@ static void ui_vr_options_change(
}
break;
}
case _vr_option_refresh_auto:
config_write_boolean("vr.refresh_rate_auto", !config_boolean("vr.refresh_rate_auto"));
break;
case _vr_option_resolution:
ui_vr_options_step("vr.resolution_scale", vr_resolution_scales, NUMBEROF(vr_resolution_scales), change);
break;
@@ -8291,6 +8298,23 @@ static void ui_vr_controls_add_action(
}
}
/* the free hand's grip: the weapon in both hands at its foregrip, else the
action on it (vr.button_talk's push to talk by default) */
static void ui_vr_controls_add_free_grip(
struct ui_vr_controls_column columns[2],
short free_column)
{
static char line[32];
short action = ui_vr_input_action(HALO_VR_INPUT_FREE_GRIP);
if (action != HALO_VR_ACTION_TALK)
snprintf(line, sizeof(line), "Both hands; else %s", vr_action_short_names[action]);
else
snprintf(line, sizeof(line), "%s", ui_vr_options_voice_chat() == 0 ? "Both hands; else talk" :
"Weapon in both hands");
ui_vr_controls_add(&columns[free_column], "Grip", line);
}
static void ui_vr_controls_column_render(
short x,
short y,
@@ -8375,8 +8399,7 @@ static void ui_vr_controls_render(
ui_vr_controls_add(&columns[gun], "Trigger", "Fire");
ui_vr_controls_add(&columns[free_column], "Trigger", "Grenade: hold, throw");
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_GRIP), TRUE);
ui_vr_controls_add(&columns[free_column], "Grip", ui_vr_options_voice_chat() == 0 ?
"Both hands; else talk" : "Weapon in both hands");
ui_vr_controls_add_free_grip(columns, free_column);
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_A), TRUE);
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_B), TRUE);
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_X), TRUE);
@@ -8429,8 +8452,7 @@ static void ui_vr_controls_render(
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_GRIP), FALSE);
ui_vr_controls_add(&columns[free_column], "Trigger", "Grenade: hold it,");
ui_vr_controls_add(&columns[free_column], "", "let go as you throw");
ui_vr_controls_add(&columns[free_column], "Grip", ui_vr_options_voice_chat() == 0 ?
"Both hands; else talk" : "Weapon in both hands");
ui_vr_controls_add_free_grip(columns, free_column);
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_A), FALSE);
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_B), FALSE);
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_X), FALSE);
@@ -8633,7 +8655,10 @@ static void ui_vr_options_render(
short count = ui_vr_options_refresh_rates(rates, &chosen, &current);
label = "Refresh rate";
if (count > 0)
/* (lowered while frames come late: vr.refresh_rate_auto) */
if (count > 0 && current > 1.0f && current < rates[chosen] - 0.5f)
snprintf(value, sizeof(value), "%.0f Hz (now %.0f)", rates[chosen], current);
else if (count > 0)
snprintf(value, sizeof(value), "%.0f Hz", rates[chosen]);
else if (current > 1.0f)
snprintf(value, sizeof(value), "%.0f Hz (system)", current);
@@ -8642,6 +8667,17 @@ static void ui_vr_options_render(
unused = count <= 1;
break;
}
case _vr_option_refresh_auto:
{
/* (dim where the headset offers one rate: SteamVR's) */
real rates[16], current;
short chosen;
label = "Lower it when frames are late";
snprintf(value, sizeof(value), "%s", config_boolean("vr.refresh_rate_auto") ? "On" : "Off");
unused = ui_vr_options_refresh_rates(rates, &chosen, &current) <= 1;
break;
}
case _vr_option_resolution:
{
real scale = vr_resolution_scales[ui_vr_options_nearest((real)config_real("vr.resolution_scale"),
@@ -8699,12 +8735,11 @@ static void ui_vr_options_render(
}
if (page == _vr_options_page_buttons)
{
/* (what the buttons' rows do) */
ui_join_code_draw_text(128, 512, 302, "An action moved to a button swaps with the one there;", FALSE, 0,
/* (what the buttons' rows do: below the page's ten rows) */
ui_join_code_draw_text(128, 512, 320, "An action moved to a button swaps with the one there;", FALSE, 0,
&dim_color);
ui_join_code_draw_text(128, 512, 320, "left-handed and mirrored, the buttons are mirrored too.", FALSE, 0,
ui_join_code_draw_text(128, 512, 338, "mirrored, the buttons are too. The menus keep theirs.", FALSE, 0,
&dim_color);
ui_join_code_draw_text(128, 512, 338, "The menus keep their buttons.", FALSE, 0, &dim_color);
}
if (page == _vr_options_page_more)
{
@@ -8721,6 +8756,8 @@ static void ui_vr_options_render(
&dim_color);
ui_join_code_draw_text(128, 512, 320, "Foveation draws the view's edges coarser (Quest only).", FALSE, 0,
&dim_color);
ui_join_code_draw_text(128, 512, 338, "Late frames: the rate steps down (90, 80, 72) while they last.", FALSE, 0,
&dim_color);
}
ui_join_code_draw_text(126, 514, VR_OPTIONS_FOOTER + 8, "%a-button Change %b-button Done", TRUE, 0, &text_color);
}
+5 -2
View File
@@ -2307,8 +2307,11 @@ static void main_frame_rate_debug(
FILE *file;
GetSystemTime(&system_time);
sprintf(
/* port: snprintf, here and below (a map's tag's name may be any
length) */
snprintf(
core_name,
sizeof(core_name),
"%s_slow_%d_%d_%d_%d_%d_%d.bin",
scenario_name,
system_time.wMonth,
@@ -2318,7 +2321,7 @@ static void main_frame_rate_debug(
system_time.wMinute,
system_time.wSecond);
game_state_save_core(core_name);
sprintf(init_file_name, "d:\\%s_init.txt", scenario_name);
snprintf(init_file_name, sizeof(init_file_name), "d:\\%s_init.txt", scenario_name);
file = fopen(init_file_name, "r");
if (!file)
+7 -7
View File
@@ -3603,9 +3603,9 @@ void network_game_server_change_map_name(
map_name,
NETWORK_GAME_MAP_NAME_LENGTH - 1);
server->game.map.name[NETWORK_GAME_MAP_NAME_LENGTH - 1] = 0;
/* port: a Custom Edition map by the name its hash makes, which the
players download it by (port/linux/game/map_share.c) */
map_share_level_name_hash(server->game.map.name, NETWORK_GAME_MAP_NAME_LENGTH);
/* port: a Custom Edition map by its own name, its version (its hash's)
telling which one it is (port/linux/game/map_share.c) */
map_share_level_name_choose(server->game.map.name, NETWORK_GAME_MAP_NAME_LENGTH);
server->game.map.version = (long)cache_files_map_version(server->game.map.name);
if (!network_game_server_send_game_data_pregame(server))
@@ -4359,8 +4359,8 @@ static void network_game_server_cooperative_round(
server->game.variant_options.friendly_fire = friendly_fire;
csstrncpy(server->game.map.name, network_game_server_cooperative_next_map, sizeof(server->game.map.name) - 1);
server->game.map.name[sizeof(server->game.map.name) - 1] = 0;
/* port: by the name its hash makes (port/linux/game/map_share.c) */
map_share_level_name_hash(server->game.map.name, sizeof(server->game.map.name));
/* port: by its own name, and its version (port/linux/game/map_share.c) */
map_share_level_name_choose(server->game.map.name, sizeof(server->game.map.name));
server->game.map.version = (long)cache_files_map_version(server->game.map.name);
main_set_multiplayer_map_name(server->game.map.name);
server->game.maximum_teams = 1;
@@ -4436,8 +4436,8 @@ static boolean network_game_server_setup_game_from_playlist(
network_game_generate_local_machine_name(machine_name);
ustrncpy(server->game.name, machine_name, NETWORK_GAME_NAME_LENGTH - 1);
server->game.name[NETWORK_GAME_NAME_LENGTH - 1] = L'\0';
/* port: by the name its hash makes (port/linux/game/map_share.c) */
map_share_level_name_hash(server->game.map.name, sizeof(server->game.map.name));
/* port: by its own name, and its version (port/linux/game/map_share.c) */
map_share_level_name_choose(server->game.map.name, sizeof(server->game.map.name));
server->game.map.version = (long)cache_files_map_version(server->game.map.name);
server->game.minimum_players = 2;
server->game.maximum_players = MAXIMUM_NETWORK_PLAYER_COUNT;
+10 -1
View File
@@ -15,7 +15,13 @@ Usage: android_asm_convert.py input.s output.s
import re
import sys
IDENT = re.compile(r'"[^"]*"|[A-Za-z_.$][\w.$]*')
# a quoted name (its escapes kept: \" does not end it) or a plain one
IDENT = re.compile(r'"(?:[^"\\]|\\.)*"|[A-Za-z_.$][\w.$]*')
# directives whose operands are string literals, the same in both
# syntaxes: they go through as they are, never renamed as symbols (data
# starting with "_" would lose that byte)
STRING_DIRECTIVES = {".ascii", ".asciz", ".string"}
DROP_DIRECTIVES = {
".subsections_via_symbols", ".build_version", ".watchos_version_min",
@@ -217,6 +223,9 @@ class Converter:
if op == ".align":
self.emit(f"\t.p2align {args}")
return
if op in STRING_DIRECTIVES:
self.emit(f"\t{op} {args}")
return
if op.startswith("."):
self.emit(f"\t{op} {self.operands(args)}" if args else f"\t{op}")
return
+2 -1
View File
@@ -25,7 +25,7 @@ import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
from .linux_build import (LINUX_PROFILE, MINIUPNPC_DEFINES, MINIUPNPC_DIR, MUSL_MATH_DIR, XDK_INCLUDE,
from .linux_build import (LINUX_PROFILE, MINIUPNPC_DEFINES, MINIUPNPC_DIR, MUSL_MATH_DIR, STB_VORBIS_SOURCE, XDK_INCLUDE,
compile_launcher, configuration_defines, game_defines_and_includes, game_sources,
miniupnpc_sources, musl_math_sources, opus_cflags, opus_sources, pgo_mode, pgo_profile,
profile_use_flags, updater_defines, xdk_headers)
@@ -518,6 +518,7 @@ def generate_android_build(n: Writer, sln: Any) -> None:
objects.append(guest_object(source, cflags))
for source in sorted(Path(config["game_sources"]).glob("*.c")):
objects.append(guest_object(source, game_cflags))
objects.append(guest_object(STB_VORBIS_SOURCE, game_cflags))
# the platform layer shared with Linux, and the guest runtime
platform_cflags = " ".join([
+1 -1
View File
@@ -22,7 +22,7 @@ no files beside its guest image.
The data are 32-bit words, not bytes: the Android build passes the guest's
assembly through tools/android_asm_convert.py, which rewrites identifiers in
operands and would garble a long .ascii string of binary data.
operands (it keeps .ascii strings as they are, but once garbled them).
"""
import json
+68
View File
@@ -0,0 +1,68 @@
/*
GEOMETRY_BYTES.C (test)
The real geometry_bytes_add (custom_edition_geometry.c), which counts the room
a Custom Edition map's model parts and structure BSP materials take before it
is allocated. Parts may all name the same vertices, so a hostile map can name
far more than its file holds: the count must stop at MAXIMUM_GEOMETRY_BYTES
rather than wrap. test_geometry_bytes.py takes the limit (config.inc) and the
code (under_test.inc).
*/
#include "harness.h"
#include "config.inc"
typedef unsigned short word;
#include "under_test.inc"
/* the compressed model vertex (rasterizer_geometry_get_vertex_size) */
enum { MODEL_VERTEX_BYTES = 32 };
int main(int argc, char **argv)
{
const char *case_name = argc > 1 ? argv[1] : "";
unsigned long bytes = 0;
long part;
/* a real map's parts: counted exactly */
CASE("counts-real-parts")
{
for (part = 0; part < 1000; part++)
CHECK(geometry_bytes_add(&bytes, 300, MODEL_VERTEX_BYTES, 600), "part %ld of a real map refused", part);
CHECK(bytes == 1000UL * (300 * MODEL_VERTEX_BYTES + 600 * sizeof(word)), "counted 0x%lX bytes", bytes);
return 0;
}
/* parts of 65535 vertices each, all naming one block: refused before the
count passes the limit, never wrapping to a small size */
CASE("refuses-before-wrapping")
{
for (part = 0; part < 4096 && geometry_bytes_add(&bytes, 65535, MODEL_VERTEX_BYTES, 65537); part++)
;
CHECK(part < 4096, "4096 parts of 65535 vertices were all counted (0x%lX bytes)", bytes);
CHECK(bytes <= MAXIMUM_GEOMETRY_BYTES, "counted 0x%lX bytes, past the most 0x%X", bytes, MAXIMUM_GEOMETRY_BYTES);
CHECK(bytes > MAXIMUM_GEOMETRY_BYTES - 65535UL * MODEL_VERTEX_BYTES - 65537UL * sizeof(word),
"stopped at 0x%lX bytes, well short of the most", bytes);
CHECK(!geometry_bytes_add(&bytes, 1, MODEL_VERTEX_BYTES, 0) || bytes <= MAXIMUM_GEOMETRY_BYTES,
"counted past the most");
return 0;
}
/* counts that alone would wrap the multiplication are refused */
CASE("refuses-huge-counts")
{
CHECK(!geometry_bytes_add(&bytes, 0x08000001UL, MODEL_VERTEX_BYTES, 0), "a vertex count that wraps was counted");
CHECK(!geometry_bytes_add(&bytes, 0, MODEL_VERTEX_BYTES, 0x80000001UL), "an index count that wraps was counted");
CHECK(bytes == 0, "a refused part changed the count to 0x%lX", bytes);
return 0;
}
/* exactly the most fits */
CASE("takes-exactly-the-most")
{
CHECK(geometry_bytes_add(&bytes, MAXIMUM_GEOMETRY_BYTES / MODEL_VERTEX_BYTES, MODEL_VERTEX_BYTES, 0),
"exactly the most was refused");
CHECK(!geometry_bytes_add(&bytes, 0, MODEL_VERTEX_BYTES, 1), "one index past the most was counted");
return 0;
}
fprintf(stderr, "unknown case '%s'\n", case_name);
return 2;
}
+111 -19
View File
@@ -1,10 +1,12 @@
/*
MAP_SHARE_NAMES.C (test)
The real names of map sharing (port/linux/game/map_share_files.c): a map's
file name ends in _ and the first digits of its hash, which the menus leave
out and a download is checked against. test_map_share_names.py takes the
constants (config.inc) and the code (under_test.inc).
The real names of map sharing (port/linux/game/map_share_files.c): the file
name of a second version of a map ends in _ and the first digits of its
hash, which the menus leave out, numbering the versions instead
(custom_edition_maps.c), and a map's version is those digits' number.
test_map_share_names.py takes the constants (config.inc) and the code
(under_test.inc).
*/
#include "harness.h"
@@ -18,23 +20,54 @@ constants (config.inc) and the code (under_test.inc).
#define NUMBEROF(array) ((short)(sizeof(array) / sizeof((array)[0])))
#define ustrcpy(destination, source) wcscpy(destination, source)
#define _strnicmp(first, second, count) strncasecmp(first, second, count)
#define csprintf sprintf
/* a map listed (custom_edition_maps.c's, its names alone) */
struct custom_edition_map
{
char name[64];
wchar_t display_name[64 + 8];
};
#include "under_test.inc"
/* the menus' name of a map file's name (custom_edition_maps.c) */
static char const *menu_name(char const *name, boolean hash)
static char const *wide_text(wchar_t const *display_name)
{
static wchar_t display_name[64];
static char text[64];
static char text[80];
int index;
display_name_make(name, hash, display_name);
for (index = 0; display_name[index] && index < 63; index++)
for (index = 0; display_name[index] && index < 79; index++)
text[index] = (char)display_name[index];
text[index] = 0;
return text;
}
static char const *menu_name(char const *name)
{
static wchar_t display_name[64];
display_name_make(name, display_name);
return wide_text(display_name);
}
/* the maps listed, sorted by name as the map lists sort them, their menu
names made and the versions told apart */
static struct custom_edition_map fake_maps[8];
static short fake_list(char const *const *names, short count)
{
short index;
for (index = 0; index < count; index++)
{
strcpy(fake_maps[index].name, names[index]);
display_name_make(fake_maps[index].name, fake_maps[index].display_name);
}
custom_edition_maps_tell_versions_apart(fake_maps, count);
return count;
}
int main(int argc, char **argv)
{
char const *case_name = argc > 1 ? argv[1] : "";
@@ -60,6 +93,13 @@ int main(int argc, char **argv)
{
"", ".", "..", ".x_0e2b3ee4", "../x_0e2b3ee4", "..\\x", "a/b", "c:x", "x.", " x", "x ", "a*b", "a?b", "a\"b", "a<b",
"a>b", "a|b", "a\tb", "caf\xE9",
/* Windows' devices, before the first dot, in any case */
"CON.x_0e2b3ee4", "con.x_0e2b3ee4", "Nul.map", "PRN", "aux .x_0e2b3ee4", "COM1.x_0e2b3ee4", "lpt9.x", "com0.x",
"CONIN$.x", "conout$",
};
static char const *const safe[] =
{
"con_0e2b3ee4", "console.x_0e2b3ee4", "com10.x", "com.x", "lptx.x", "a.con", "nul_0e2b3ee4", "auxiliary",
};
int index;
@@ -67,6 +107,8 @@ int main(int argc, char **argv)
CHECK(map_share_name_safe("Elite_Alpha_Siege (v2).final"), "a name with spaces and dots inside was refused");
for (index = 0; index < (int)(sizeof(unsafe) / sizeof(unsafe[0])); index++)
CHECK(!map_share_name_safe(unsafe[index]), "the name \"%s\" was taken", unsafe[index]);
for (index = 0; index < (int)(sizeof(safe) / sizeof(safe[0])); index++)
CHECK(map_share_name_safe(safe[index]), "the name \"%s\" was refused", safe[index]);
return 0;
}
/* the menus' name leaves the hash out */
@@ -101,18 +143,68 @@ int main(int argc, char **argv)
CHECK(map_share_piece_count(59221172UL) == 226, "hugeass.map's pieces: %ld", map_share_piece_count(59221172UL));
return 0;
}
/* the menus show a map without its hash, unless asked to tell versions
apart (Halo PC's own maps by their names) */
/* the menus show a map without its hash (Halo PC's own maps by their
names) */
CASE("display-names")
{
CHECK(!strcmp(menu_name("the_bay_of_pigs_0c7e7075", FALSE), "The Bay Of Pigs"), "made %s",
menu_name("the_bay_of_pigs_0c7e7075", FALSE));
CHECK(!strcmp(menu_name("the_bay_of_pigs", FALSE), "The Bay Of Pigs"), "a name without a hash: %s",
menu_name("the_bay_of_pigs", FALSE));
CHECK(!strcmp(menu_name("icefields_0e2b3ee4", FALSE), "Ice Fields"), "Halo PC's map: %s",
menu_name("icefields_0e2b3ee4", FALSE));
CHECK(!strcmp(menu_name("hugeass_0e2b3ee4", TRUE), "Hugeass 0e2b3ee4"), "two versions: %s",
menu_name("hugeass_0e2b3ee4", TRUE));
CHECK(!strcmp(menu_name("the_bay_of_pigs_0c7e7075"), "The Bay Of Pigs"), "made %s",
menu_name("the_bay_of_pigs_0c7e7075"));
CHECK(!strcmp(menu_name("the_bay_of_pigs"), "The Bay Of Pigs"), "a name without a hash: %s",
menu_name("the_bay_of_pigs"));
CHECK(!strcmp(menu_name("icefields_0e2b3ee4"), "Ice Fields"), "Halo PC's map: %s",
menu_name("icefields_0e2b3ee4"));
return 0;
}
/* versions of a map: the one of the name alone first, the others (2),
(3) after it; a map of a name of its own, or a longer name, is not one */
CASE("versions-numbered")
{
static char const *const names[] =
{
"hugeass", "hugeass2", "hugeass_0e2b3ee4", "hugeass_ffff0000", "ratrace_12345678", "wizard",
};
short count = fake_list(names, NUMBEROF(names));
CHECK(!strcmp(wide_text(fake_maps[0].display_name), "Hugeass"), "the first version: %s",
wide_text(fake_maps[0].display_name));
CHECK(!strcmp(wide_text(fake_maps[1].display_name), "Hugeass2"), "another map: %s",
wide_text(fake_maps[1].display_name));
CHECK(!strcmp(wide_text(fake_maps[2].display_name), "Hugeass (2)"), "the second version: %s",
wide_text(fake_maps[2].display_name));
CHECK(!strcmp(wide_text(fake_maps[3].display_name), "Hugeass (3)"), "the third version: %s",
wide_text(fake_maps[3].display_name));
CHECK(!strcmp(wide_text(fake_maps[4].display_name), "Ratrace"), "a map's one version: %s",
wide_text(fake_maps[4].display_name));
CHECK(!strcmp(wide_text(fake_maps[5].display_name), "Wizard"), "a map of its own: %s",
wide_text(fake_maps[5].display_name));
CHECK(count == 6, "listed %d", count);
return 0;
}
/* versions without the one of the name alone: the first of them by its
name, the next (2) */
CASE("versions-without-the-first")
{
static char const *const names[] = { "hugeass_0e2b3ee4", "hugeass_ffff0000" };
fake_list(names, NUMBEROF(names));
CHECK(!strcmp(wide_text(fake_maps[0].display_name), "Hugeass"), "the first: %s",
wide_text(fake_maps[0].display_name));
CHECK(!strcmp(wide_text(fake_maps[1].display_name), "Hugeass (2)"), "the second: %s",
wide_text(fake_maps[1].display_name));
return 0;
}
/* a map's version, a game record's, is the number its digits are (the
name of a second version ends in them), never 0 */
CASE("versions")
{
byte hash[MAP_SHARE_HASH_BYTES] = { 0x0e, 0x2b, 0x3e, 0xe4, 0x55 };
byte zero[MAP_SHARE_HASH_BYTES] = { 0 };
char digits[MAP_SHARE_NAME_DIGITS + 1];
map_share_hash_digits(hash, digits);
CHECK(map_share_hash_version(hash) == strtoul(digits, NULL, 16), "version %08lx for the digits %s",
map_share_hash_version(hash), digits);
CHECK(map_share_hash_version(zero) != 0, "a version of 0, which a client checks nothing for");
return 0;
}
fprintf(stderr, "unknown case: %s\n", case_name);
+457
View File
@@ -0,0 +1,457 @@
/*
MAP_SHARE_RESTART.C (test)
What a map sharing download (port/linux/game/map_share.c) keeps of another
and leaves behind: the pieces another machine said it has, a bit each of the
download's file as the offer counted them, forgotten when the download ends
(another download's file of the same hash may be offered of another size),
and never read or written beyond them; and the files of a download that
cannot be played, deleted again from custom_maps (none that was there
before). test_map_share_restart.py takes the constants and structures
(config.inc) and the code (under_test.inc).
*/
#include "harness.h"
typedef unsigned short word;
/* the allocator: each block followed by SLACK bytes of 0xFF, so that a bit
read beyond a bitmap is had, and one cleared beyond it shows */
enum
{
SLACK = 1024,
HEAD = 8,
};
static void *fake_malloc(size_t size)
{
byte *block = malloc(HEAD + size + SLACK);
memcpy(block, &size, sizeof(size));
memset(block + HEAD, 0xA5, size);
memset(block + HEAD + size, 0xFF, SLACK);
return block + HEAD;
}
static void *fake_calloc(size_t count, size_t size)
{
byte *block = fake_malloc(count * size);
memset(block, 0, count * size);
return block;
}
static void fake_free(void *pointer)
{
if (pointer)
free((byte *)pointer - HEAD);
}
static size_t fake_size(void const *pointer)
{
size_t size;
memcpy(&size, (byte const *)pointer - HEAD, sizeof(size));
return size;
}
/* whether the slack after a block is as it was made */
static boolean fake_slack_intact(void const *pointer)
{
byte const *slack = (byte const *)pointer + fake_size(pointer);
long index;
for (index = 0; index < SLACK; index++)
{
if (slack[index] != 0xFF)
return FALSE;
}
return TRUE;
}
#define malloc(size) fake_malloc(size)
#define calloc(count, size) fake_calloc(count, size)
#define free(pointer) fake_free(pointer)
#include "config.inc"
#define csmemset(destination, value, size) memset(destination, value, size)
#define csmemcpy(destination, source, size) memcpy(destination, source, size)
#define csmemmove(destination, source, size) memmove(destination, source, size)
#define csmemcmp(first, second, size) memcmp(first, second, size)
#define csstrcmp(first, second) strcmp(first, second)
#define csstrcasecmp(first, second) strcasecmp(first, second)
#define csstrncpy(destination, source, count) strncpy(destination, source, count)
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define _error_silent 0
#define error(level, ...) ((void)0)
#define _progress_done 1
/* the fake world: a clock, a folder of a few files, whether the map can be
played, and what the code under test did */
enum
{
FAKE_FILES = 8,
};
static unsigned long fake_now = 1000;
static char fake_paths[FAKE_FILES][MAP_SHARE_PATH_SIZE];
static boolean fake_playable;
static int fake_failed;
static int fake_looked_again;
static struct map_share_globals map_share_globals;
static unsigned long system_milliseconds(void)
{
return fake_now;
}
static short fake_find(char const *path)
{
short index;
for (index = 0; index < FAKE_FILES; index++)
{
if (fake_paths[index][0] && !strcmp(fake_paths[index], path))
return index;
}
return NONE;
}
static boolean fake_exists(char const *path)
{
return fake_find(path) != NONE;
}
static void fake_create(char const *path)
{
short index;
for (index = 0; index < FAKE_FILES && fake_paths[index][0]; index++)
;
if (index < FAKE_FILES)
strcpy(fake_paths[index], path);
}
/* (MoveFileA does not replace a file: xbox_files.c) */
static boolean MoveFileA(char const *from, char const *to)
{
short index = fake_find(from);
if (index == NONE || fake_exists(to))
return FALSE;
strcpy(fake_paths[index], to);
return TRUE;
}
static boolean DeleteFileA(char const *path)
{
short index = fake_find(path);
if (index == NONE)
return FALSE;
fake_paths[index][0] = 0;
return TRUE;
}
/* (map_share_files.c's: a finished download's hash kept) */
static void map_share_map_hash_remember(char const *path, byte const *hash)
{
(void)path;
(void)hash;
}
static void custom_edition_maps_look_again(void)
{
fake_looked_again++;
}
static boolean custom_edition_cache_present(char const *level_name, unsigned long version, char *message, long size)
{
(void)level_name;
(void)version;
snprintf(message, (size_t)size, "%s", fake_playable ? "" : "Your map can't be played.");
return fake_playable;
}
static void download_fail(char const *reason)
{
(void)reason;
fake_failed++;
map_share_globals.download.state = _download_failed;
}
static void download_report(byte state)
{
(void)state;
}
static short map_share_file_hashed(char const *path, short kind, unsigned long size, byte const *piece_hashes,
long piece_count, byte const *hash)
{
(void)path;
(void)kind;
(void)size;
(void)piece_hashes;
(void)piece_count;
(void)hash;
return NONE;
}
static boolean map_share_sending(void)
{
return FALSE;
}
static void current_files_update(boolean hash)
{
(void)hash;
}
static void connection_close(struct connection *connection, char const *reason)
{
(void)reason;
connection->used = FALSE;
}
#include "under_test.inc"
/* the download's file 0 of `piece_count` pieces, being fetched */
static void download_setup(long piece_count)
{
struct download *download = &map_share_globals.download;
struct download_file *file = &download->files[0];
download->state = _download_transferring;
download->file_count = 1;
memset(file, 0, sizeof(*file));
file->state = _file_transferring;
file->size = (unsigned long)piece_count * MAP_SHARE_PIECE_BYTES;
file->piece_count = piece_count;
file->hashes_connection = NONE;
file->have = calloc((size_t)(piece_count + 7) / 8, 1);
file->asked = calloc((size_t)piece_count, 1);
}
/* connection 0, another machine's, open, about the download's file */
static struct connection *connection_setup(void)
{
struct connection *connection = &map_share_globals.connections[0];
short index;
memset(connection, 0, sizeof(*connection));
connection->used = TRUE;
connection->state = _connection_open;
connection->outgoing = FALSE;
connection->source = NONE;
connection->hello_heard = TRUE;
connection->file_count = 1;
for (index = 0; index < MAXIMUM_FILES; index++)
{
connection->download_file[index] = NONE;
connection->catalog_file[index] = NONE;
connection->hashes_next[index] = NONE;
}
connection->download_file[0] = 0;
return connection;
}
/* a frame of a file and a piece (have, refuse) */
static void piece_frame(void (*handle)(struct connection *, byte const *, long), struct connection *connection,
long piece)
{
byte body[5];
body[0] = 0;
body[1] = (byte)piece;
body[2] = (byte)(piece >> 8);
body[3] = (byte)(piece >> 16);
body[4] = (byte)(piece >> 24);
handle(connection, body, 5);
}
/* a state frame of the pieces the other end has: the first `count` of them,
in a bitmap of `bytes` */
static void state_frame(struct connection *connection, long count, long bytes)
{
static byte body[2 + 0x1000];
long piece;
body[0] = 0;
body[1] = _have_pieces;
memset(body + 2, 0, (size_t)bytes);
for (piece = 0; piece < count; piece++)
body[2 + (piece >> 3)] |= (byte)(1 << (piece & 7));
frame_state(connection, body, 2 + bytes);
}
/* the download's files: the map and bitmaps.map, whole in their part files */
static void finish_setup(void)
{
struct download *download = &map_share_globals.download;
short index;
memset(download, 0, sizeof(*download));
memset(fake_paths, 0, sizeof(fake_paths));
download->state = _download_transferring;
strcpy(download->level_name, "custom_maps\\hugeass_0e2b3ee4");
download->file_count = 2;
strcpy(download->files[0].final_path, "d:\\custom_maps\\hugeass_0e2b3ee4.map");
strcpy(download->files[1].final_path, "d:\\custom_maps\\bitmaps.map");
for (index = 0; index < download->file_count; index++)
{
struct download_file *file = &download->files[index];
file->kind = index;
file->state = _file_complete;
file->piece_count = 1;
file->size = 1;
file->have_count = 1;
file->hashes_connection = NONE;
snprintf(file->part_path, sizeof(file->part_path), "%s.part", file->final_path);
fake_create(file->part_path);
}
/* (a file of custom_maps the download did not bring) */
fake_create("d:\\custom_maps\\sounds.map");
}
int main(int argc, char **argv)
{
char const *case_name = argc > 1 ? argv[1] : "";
srand(1);
/* a download of a file of one piece ends, the connection kept; the next
is of a file of the same hash, 3072 pieces long: what the other end says
it has of it is a bitmap of 3072 pieces, not of the one */
CASE("restart-forgets")
{
struct connection *connection;
download_setup(1);
connection = connection_setup();
piece_frame(frame_have, connection, 0);
CHECK(connection->remote_pieces[0] && connection->remote_piece_count[0] == 1, "a piece had was not kept");
download_end(FALSE);
CHECK(connection->used, "another machine's connection was not kept");
CHECK(!connection->remote_pieces[0] && !connection->remote_piece_count[0],
"the pieces the other end had of the last download's file were kept");
download_setup(3072);
connection->download_file[0] = 0;
piece_frame(frame_have, connection, 3071);
CHECK(fake_size(connection->remote_pieces[0]) * 8 > 3071, "a bit was set beyond a bitmap of %lu bytes",
(unsigned long)fake_size(connection->remote_pieces[0]));
CHECK(remote_piece_had(connection, 0, 3071), "the piece had was not taken");
return 0;
}
/* a bitmap of fewer pieces than the download's file has (said of a file
of the same hash of another size) is made again of the file's */
CASE("have-of-another-size")
{
struct connection *connection;
download_setup(1);
connection = connection_setup();
piece_frame(frame_have, connection, 0);
map_share_globals.download.files[0].piece_count = 3072;
piece_frame(frame_have, connection, 3071);
CHECK(fake_size(connection->remote_pieces[0]) * 8 > 3071, "a bit was set beyond a bitmap of %lu bytes",
(unsigned long)fake_size(connection->remote_pieces[0]));
CHECK(connection->remote_piece_count[0] == 3072, "the bitmap's pieces were not counted: %ld",
connection->remote_piece_count[0]);
CHECK(remote_piece_had(connection, 0, 3071), "the piece had was not taken");
CHECK(!remote_piece_had(connection, 0, 3070), "a piece not had was taken");
return 0;
}
/* the pieces chosen of a connection whose bitmap covers fewer pieces than
the file has are among those it covers */
CASE("choose-within")
{
struct download_file *file = &map_share_globals.download.files[0];
struct connection *connection;
int attempt;
download_setup(8);
connection = connection_setup();
state_frame(connection, 8, 1);
CHECK(connection->remote_piece_count[0] == 8, "the state's pieces were not counted");
file->piece_count = 3072;
free(file->have);
free(file->asked);
file->have = calloc(3072 / 8, 1);
file->asked = calloc(3072, 1);
for (attempt = 0; attempt < 200; attempt++)
{
long piece = piece_choose(connection, 0, FALSE);
CHECK(piece >= 0 && piece < 8, "a piece beyond the other end's bitmap was chosen: %ld", piece);
}
CHECK(fake_slack_intact(connection->remote_pieces[0]), "the bitmap was written beyond");
return 0;
}
/* a piece refused beyond the connection's bitmap clears nothing beyond it */
CASE("refuse-within")
{
struct connection *connection;
download_setup(8);
connection = connection_setup();
state_frame(connection, 8, 1);
map_share_globals.download.files[0].piece_count = 3072;
free(map_share_globals.download.files[0].asked);
map_share_globals.download.files[0].asked = calloc(3072, 1);
connection->requests[0].file = 0;
connection->requests[0].piece = 100;
connection->requests[0].buffer = malloc(MAP_SHARE_PIECE_BYTES);
connection->request_count = 1;
piece_frame(frame_refuse, connection, 100);
CHECK(!connection->request_count, "the refused piece is still asked for");
CHECK(fake_slack_intact(connection->remote_pieces[0]), "a bit was cleared beyond the bitmap");
return 0;
}
/* a download that can be played is left in custom_maps */
CASE("finish-playable")
{
finish_setup();
fake_playable = TRUE;
download_finish();
CHECK(!fake_failed, "a download that can be played failed");
CHECK(fake_exists("d:\\custom_maps\\hugeass_0e2b3ee4.map") && fake_exists("d:\\custom_maps\\bitmaps.map"),
"a download that can be played was not left in custom_maps");
CHECK(!fake_exists("d:\\custom_maps\\bitmaps.map.part"), "a part file was left");
CHECK(fake_looked_again == 1, "the map lists looked again %d times", fake_looked_again);
return 0;
}
/* one that cannot is deleted again, and nothing it did not bring */
CASE("finish-unplayable")
{
finish_setup();
fake_playable = FALSE;
download_finish();
CHECK(fake_failed == 1, "a download that cannot be played did not fail");
CHECK(!fake_exists("d:\\custom_maps\\hugeass_0e2b3ee4.map"), "a map that cannot be played was left");
CHECK(!fake_exists("d:\\custom_maps\\bitmaps.map"), "a resource map of a map that cannot be played was left");
CHECK(fake_exists("d:\\custom_maps\\sounds.map"), "a file the download did not bring was deleted");
CHECK(fake_looked_again == 2, "the map lists did not look again once the files were deleted (%d times)",
fake_looked_again);
return 0;
}
/* a file not put in custom_maps (one of its name there): those that were
put there deleted again, and not the one that was there */
CASE("finish-refused")
{
finish_setup();
fake_create("d:\\custom_maps\\bitmaps.map");
fake_playable = TRUE;
download_finish();
CHECK(fake_failed == 1, "a download whose file could not be put in custom_maps did not fail");
CHECK(!fake_exists("d:\\custom_maps\\hugeass_0e2b3ee4.map"), "the files put in custom_maps were left");
CHECK(fake_exists("d:\\custom_maps\\bitmaps.map"), "the file that was there was deleted");
CHECK(fake_exists("d:\\custom_maps\\bitmaps.map.part"), "the part file not put there was deleted");
CHECK(fake_looked_again == 1, "the map lists did not look again once the files were deleted (%d times)",
fake_looked_again);
return 0;
}
fprintf(stderr, "unknown case: %s\n", case_name);
return 2;
}
+474
View File
@@ -0,0 +1,474 @@
/*
MAP_SHARE_TRUST.C (test)
What map sharing (port/linux/game/map_share.c) takes of another machine's
word: the LAN addresses a host names, the pieces' hashes (asked of the host
first, none in a frame refused), a client's progress, which keeps it in the
host's game only while it grows, and its requests for the map, taken a few
seconds apart at most. test_map_share_trust.py takes the constants and
structures (config.inc) and the code (under_test.inc).
*/
#include "harness.h"
typedef unsigned short word;
#include "config.inc"
#define csmemset(destination, value, size) memset(destination, value, size)
#define csmemcpy(destination, source, size) memcpy(destination, source, size)
#define csmemcmp(first, second, size) memcmp(first, second, size)
#define csstrcmp(first, second) strcmp(first, second)
#define csstrncmp(first, second, count) strncmp(first, second, count)
#define csstrncpy(destination, source, count) strncpy(destination, source, count)
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define _error_silent 0
#define error(level, ...) ((void)0)
#define _distributed_message_map_share_offer 0x40
/* the fake world: a clock, the host's game and its machines, a catalog of one
file, and what the code under test did */
struct distributed_message_header
{
word header;
byte type;
byte count;
long game_time;
};
struct network_game
{
struct
{
char name[128];
long version;
} map;
};
static unsigned long fake_now = 1000;
static struct network_game fake_game;
static unsigned long fake_machine_addresses[MAP_SHARE_MACHINES];
static struct map_share_file fake_file;
static int fake_offers;
static byte fake_offer_result;
static int fake_opened;
static struct map_share_globals map_share_globals;
static unsigned long system_milliseconds(void)
{
return fake_now;
}
static struct network_game *map_share_network_game(void)
{
return &fake_game;
}
static unsigned long network_game_server_machine_address(long machine_index)
{
return machine_index >= 0 && machine_index < MAP_SHARE_MACHINES ? fake_machine_addresses[machine_index] : 0;
}
static boolean host_in_game(void)
{
return FALSE;
}
static void map_share_fill_header(struct distributed_message_header *header, byte type, word size)
{
(void)size;
header->type = type;
}
/* (an offer's result, after its header and level name) */
static boolean network_distributed_server_send_to_machine_reliably(long machine_index, void *message, word size)
{
(void)machine_index;
(void)size;
fake_offer_result = ((byte const *)message)[sizeof(struct distributed_message_header) + LEVEL_NAME_BYTES];
fake_offers++;
return TRUE;
}
static void host_sources(long machine_index)
{
(void)machine_index;
}
static struct map_share_file *map_share_file_get(short index)
{
return index == 0 ? &fake_file : NULL;
}
static short map_share_file_hash(char const *path, short kind)
{
(void)path;
(void)kind;
return 0;
}
static char const *tag_name_strip_path(char const *name)
{
char const *slash = strrchr(name, '\\');
return slash ? slash + 1 : name;
}
#define config_boolean(name) 1
#define custom_edition_level_name(level_name) (!strncmp(level_name, "custom_maps\\", 12))
#define custom_edition_map_file_path(level_name, path) (strcpy(path, "d:\\custom_maps\\hugeass.map"), 1)
#define custom_edition_resource_map_file_path(kind, path) 0
#define custom_edition_map_resource_maps_needed(level_name) 0UL
/* (map_share_files.c's: the hash's first 32 bits) */
#define map_share_hash_version(hash) \
((unsigned long)(hash)[0] << 24 | (unsigned long)(hash)[1] << 16 | (unsigned long)(hash)[2] << 8 | (hash)[3])
/* the pieces' hashes: a file's hash the first piece's here, which is enough to
tell a whole list from none */
static void map_share_file_hash_of_pieces(unsigned long size, byte const *piece_hashes, long piece_count, byte *hash)
{
(void)size;
(void)piece_count;
memcpy(hash, piece_hashes, MAP_SHARE_HASH_BYTES);
}
static boolean download_file_open(struct download_file *file, short index)
{
(void)file;
(void)index;
fake_opened++;
return TRUE;
}
static void download_fail(char const *reason)
{
(void)reason;
}
static void connection_close(struct connection *connection, char const *reason)
{
(void)reason;
connection->used = FALSE;
}
#include "under_test.inc"
#define ADDRESS(a, b, c, d) \
((unsigned long)(a) << 24 | (unsigned long)(b) << 16 | (unsigned long)(c) << 8 | (unsigned long)(d))
/* a hashes frame (the file, the first, the count, then its hashes, each one's
first byte its number) fed to the connection */
static void hashes_frame(struct connection *connection, long first, long count)
{
static byte body[7 + 0x10000];
long index;
body[0] = 0;
body[1] = (byte)first;
body[2] = (byte)(first >> 8);
body[3] = 0;
body[4] = 0;
body[5] = (byte)count;
body[6] = (byte)(count >> 8);
memset(body + 7, 0, (size_t)count * MAP_SHARE_HASH_BYTES);
for (index = 0; index < count; index++)
body[7 + index * MAP_SHARE_HASH_BYTES] = (byte)(first + index);
frame_hashes(connection, body, 7 + count * MAP_SHARE_HASH_BYTES);
}
/* the host's game on a map whose file is 3 MB, machine 3 in it */
static void host_setup(void)
{
strcpy(fake_game.map.name, "custom_maps\\hugeass");
fake_game.map.version = 0x0e2b3ee4;
fake_machine_addresses[3] = ADDRESS(10, 0, 0, 3);
memset(&fake_file, 0, sizeof(fake_file));
fake_file.hash[0] = 0x0e;
fake_file.hash[1] = 0x2b;
fake_file.hash[2] = 0x3e;
fake_file.hash[3] = 0xe4;
fake_file.used = TRUE;
fake_file.ready = TRUE;
fake_file.size = 3 * 1024 * 1024;
fake_file.piece_count = 12;
}
/* machine 3 asks for the map, and is offered it (as host_update does once
the file is hashed) */
static void host_ask(void)
{
struct distributed_map_share_request request;
memset(&request, 0, sizeof(request));
strcpy(request.level_name, fake_game.map.name);
request.wanted = FLAG(_map_share_file_map);
host_request(3, &request);
host_offer(3, _offer_ok);
}
/* machine 3's request for a level, as it comes over its connection */
static void host_asked_for(char const *level_name)
{
struct distributed_map_share_request request;
memset(&request, 0, sizeof(request));
strcpy(request.level_name, level_name);
request.wanted = FLAG(_map_share_file_map);
host_asked(3, &request);
}
/* machine 3 says it has `done` bytes */
static void host_told(unsigned long done)
{
struct distributed_map_share_progress progress;
memset(&progress, 0, sizeof(progress));
progress.state = _progress_downloading;
progress.done = done;
progress.total = fake_file.size;
host_progress(3, &progress);
}
/* `time` milliseconds pass, machine 3 saying it has `done` bytes every two
seconds */
static void host_wait(unsigned long time, unsigned long done)
{
unsigned long end = fake_now + time;
while (fake_now < end)
{
fake_now += 2000;
host_told(done);
}
}
int main(int argc, char **argv)
{
char const *case_name = argc > 1 ? argv[1] : "";
/* a LAN host names its players' private addresses alone, and never
this machine's, nor 0.0.0.0, multicast or internet play's */
CASE("source-addresses")
{
unsigned long lan_host = ADDRESS(192, 168, 1, 5);
CHECK(source_address_allowed(ADDRESS(192, 168, 1, 9), lan_host), "a player beside the host was refused");
CHECK(source_address_allowed(ADDRESS(10, 1, 2, 3), lan_host), "a private network's player was refused");
CHECK(source_address_allowed(ADDRESS(172, 20, 0, 1), lan_host), "a 172.16/12 player was refused");
CHECK(source_address_allowed(ADDRESS(169, 254, 7, 7), lan_host), "a link-local player was refused");
CHECK(!source_address_allowed(ADDRESS(8, 8, 8, 8), lan_host), "a LAN host sent its players to the internet");
CHECK(!source_address_allowed(ADDRESS(172, 32, 0, 1), lan_host), "172.32 was taken for a private network's");
CHECK(!source_address_allowed(ADDRESS(127, 0, 0, 1), lan_host), "a LAN host sent its players to themselves");
CHECK(!source_address_allowed(0, lan_host), "0.0.0.0 was taken");
CHECK(!source_address_allowed(ADDRESS(224, 0, 0, 1), lan_host), "a multicast address was taken");
CHECK(!source_address_allowed(ADDRESS(255, 255, 255, 255), lan_host), "the broadcast address was taken");
CHECK(!source_address_allowed(ADDRESS(100, 64, 0, 9), lan_host), "internet play's address was taken");
/* (copies of the game on one machine, and a VPN of public addresses) */
CHECK(source_address_allowed(ADDRESS(127, 0, 0, 2), ADDRESS(127, 0, 0, 1)),
"a copy beside a loopback host was refused");
CHECK(source_address_allowed(ADDRESS(25, 4, 5, 6), ADDRESS(25, 1, 2, 3)), "a VPN's player was refused");
return 0;
}
/* the pieces' hashes: the host's at once, the others it named a little
later, connections other machines made last */
CASE("hashes-sources")
{
struct download *download = &map_share_globals.download;
struct connection *connections = map_share_globals.connections;
download->offered_at = fake_now;
download->source_count = 2;
download->sources[0].host = TRUE;
connections[0].outgoing = TRUE;
connections[0].source = 0;
connections[1].outgoing = TRUE;
connections[1].source = 1;
connections[2].outgoing = FALSE;
connections[2].source = NONE;
CHECK(hashes_source(&connections[0], fake_now), "the host was not asked at once");
CHECK(!hashes_source(&connections[1], fake_now), "another player was asked before the host had a chance");
CHECK(!hashes_source(&connections[2], fake_now), "a connection anyone could make was asked at once");
CHECK(hashes_source(&connections[1], fake_now + HASHES_SOURCES_DELAY), "the players the host named were never asked");
CHECK(!hashes_source(&connections[2], fake_now + HASHES_SOURCES_DELAY),
"a connection anyone could make was asked as soon as the host's players");
CHECK(hashes_source(&connections[2], fake_now + REQUEST_TIMEOUT), "the other connections were never asked");
return 0;
}
/* a hashes frame that brings none keeps nothing alive; the rest are
taken in order, and the file opened once they all came */
CASE("hashes-frames")
{
struct download *download = &map_share_globals.download;
struct download_file *file = &download->files[0];
struct connection *connection = &map_share_globals.connections[0];
unsigned long asked_at = fake_now;
download->file_count = 1;
file->piece_count = 300;
file->hashes_coming = calloc(300, MAP_SHARE_HASH_BYTES);
file->hashes_connection = 0;
file->hashes_started_at = asked_at;
file->hashes_asked_at = asked_at;
connection->used = TRUE;
connection->file_count = 1;
connection->download_file[0] = 0;
fake_now += REQUEST_TIMEOUT / 2;
hashes_frame(connection, 0, 0);
CHECK(file->hashes_asked_at == asked_at, "a frame of no hashes kept the request alive");
CHECK(file->hashes_received == 0, "a frame of no hashes counted");
hashes_frame(connection, 0, 256);
CHECK(file->hashes_received == 256, "the first hashes were not taken: %ld", file->hashes_received);
CHECK(file->hashes_asked_at == fake_now, "hashes that came did not keep the request alive");
hashes_frame(connection, 0, 44);
CHECK(file->hashes_received == 256, "hashes out of order were taken");
hashes_frame(connection, 256, 45);
CHECK(file->hashes_received == 256, "more hashes than the file's pieces were taken");
hashes_frame(connection, 256, 44);
CHECK(file->piece_hashes && fake_opened == 1, "the whole list was not taken");
return 0;
}
/* a client whose progress stops is not taken for downloading once an
honest one would have given up, however often it says it is */
CASE("progress-advances")
{
host_setup();
host_ask();
CHECK(fake_offers == 1, "no offer was made");
CHECK(map_share_machine_downloading(3) && map_share_machine_needs_map(3), "an offered machine is not downloading");
host_wait(DOWNLOADING_STALL_TIMEOUT - 2000, 0);
CHECK(map_share_machine_downloading(3), "a machine was given up on before an honest one would give up");
host_wait(4000, 0);
CHECK(!map_share_machine_downloading(3), "a machine that has had nothing for long is still downloading");
CHECK(!map_share_machine_needs_map(3), "a machine that has had nothing for long still holds the others");
host_told(MAP_SHARE_PIECE_BYTES);
CHECK(map_share_machine_downloading(3), "a piece that came did not count");
/* (a byte at a time is not a piece) */
host_wait(DOWNLOADING_STALL_TIMEOUT + 2000, MAP_SHARE_PIECE_BYTES + 1000);
CHECK(!map_share_machine_downloading(3), "a byte more each time kept a machine downloading");
/* (the whole, said once, counts once; more than the whole is the whole) */
host_told(0xFFFFFFFFUL);
CHECK(map_share_machine_downloading(3), "the last pieces did not count");
CHECK(map_share_globals.machines[3].done == fake_file.size, "more than was offered was taken: %lu",
map_share_globals.machines[3].done);
host_wait(DOWNLOADING_STALL_TIMEOUT + 2000, 0xFFFFFFFFUL);
CHECK(!map_share_machine_downloading(3), "saying it has it all kept a machine downloading");
return 0;
}
/* asking again does not start the count again; a machine that left is
forgotten */
CASE("progress-asked-again")
{
host_setup();
host_ask();
host_wait(DOWNLOADING_STALL_TIMEOUT + 2000, 0);
CHECK(!map_share_machine_downloading(3), "a machine that has had nothing for long is still downloading");
host_ask();
CHECK(fake_offers == 2, "the request asked again was not answered");
CHECK(!map_share_machine_downloading(3), "asking again made a machine downloading again");
host_ask();
host_ask();
CHECK(!map_share_machine_downloading(3), "asking again and again made a machine downloading again");
/* (another machine in its slot) */
fake_machine_addresses[3] = ADDRESS(10, 0, 0, 4);
host_ask();
CHECK(map_share_machine_downloading(3), "another machine in the slot was taken for the last");
return 0;
}
/* nor does asking for another level between, which is answered that it
is not the game's; a game on another map starts the count again */
CASE("progress-other-level-between")
{
struct distributed_map_share_request request;
host_setup();
host_ask();
host_wait(DOWNLOADING_STALL_TIMEOUT + 2000, 0);
CHECK(!map_share_machine_downloading(3), "a machine that has had nothing for long is still downloading");
memset(&request, 0, sizeof(request));
strcpy(request.level_name, "custom_maps\\bloodgulch");
request.wanted = FLAG(_map_share_file_map);
host_request(3, &request);
CHECK(fake_offers == 2 && fake_offer_result == _offer_unavailable, "another level was offered");
host_ask();
CHECK(!map_share_machine_downloading(3), "asking for another level between made a machine downloading again");
host_request(3, &request);
host_ask();
CHECK(!map_share_machine_downloading(3), "asking for another level between, again, made it downloading");
/* (the host's game on another map) */
strcpy(fake_game.map.name, "custom_maps\\hugeass2");
host_ask();
CHECK(map_share_machine_downloading(3), "a game on another map kept the last map's count");
return 0;
}
/* a machine's requests are taken MAP_REQUEST_INTERVAL apart at most: one
sooner waits, the last of them taken in its turn (a millisecond more: the
time a request was taken is kept odd, never 0) */
CASE("offer-of-the-version")
{
host_setup();
host_asked_for(fake_game.map.name);
host_update();
CHECK(fake_offers == 1 && fake_offer_result == _offer_ok, "the game's version was not offered");
/* (the host's file another version than the game is on) */
host_setup();
fake_file.hash[3] = 0xe5;
fake_now += MAP_REQUEST_INTERVAL + 1;
host_asked_for(fake_game.map.name);
host_update();
CHECK(fake_offers == 2 && fake_offer_result == _offer_unavailable,
"a map of another version than the game's was offered");
return 0;
}
CASE("requests-apart")
{
int asked;
host_setup();
host_asked_for(fake_game.map.name);
host_update();
CHECK(fake_offers == 1 && fake_offer_result == _offer_ok, "the first request was not answered");
for (asked = 0; asked < 50; asked++)
{
fake_now += 10;
host_asked_for(asked & 1 ? fake_game.map.name : "custom_maps\\bloodgulch");
host_update();
}
CHECK(fake_offers == 1, "requests one after another were all answered: %d", fake_offers);
fake_now += MAP_REQUEST_INTERVAL + 1;
host_update();
CHECK(fake_offers == 2 && fake_offer_result == _offer_ok, "the last request was not answered in its turn");
CHECK(!strcmp(map_share_globals.machines[3].request.level_name, fake_game.map.name),
"a request before the last was taken");
host_update();
CHECK(fake_offers == 2, "a request was answered twice");
/* (asked again later, its download failed: answered at once) */
fake_now += MAP_REQUEST_INTERVAL + 1;
host_asked_for(fake_game.map.name);
host_update();
CHECK(fake_offers == 3, "a request a while after the last was not answered at once");
return 0;
}
/* a request waiting its turn after one answered that the level is not the
game's, which leaves nothing else to look at */
CASE("requests-apart-after-another-level")
{
host_setup();
host_asked_for("custom_maps\\bloodgulch");
CHECK(fake_offers == 1 && fake_offer_result == _offer_unavailable, "another level was offered");
host_asked_for(fake_game.map.name);
host_update();
CHECK(fake_offers == 1, "a request just after another was answered at once");
fake_now += MAP_REQUEST_INTERVAL + 1;
host_update();
CHECK(fake_offers == 2 && fake_offer_result == _offer_ok, "the request that waited was never answered");
/* (another machine in the slot: not held for the last's requests) */
fake_machine_addresses[3] = ADDRESS(10, 0, 0, 4);
host_asked_for(fake_game.map.name);
CHECK(map_share_globals.machines[3].address == ADDRESS(10, 0, 0, 4),
"another machine in the slot waited for the last's turn");
return 0;
}
fprintf(stderr, "unknown case: %s\n", case_name);
return 2;
}
@@ -0,0 +1,41 @@
"""The real geometry_bytes_add (custom_edition_geometry.c): a hostile Custom Edition map's parts, all naming one block
of vertices, are refused at MAXIMUM_GEOMETRY_BYTES instead of wrapping the geometry's size to a small allocation."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from harness import CHECK_FAILED, build, constant, function, mutated, read, run # noqa: E402
CASES = ["counts-real-parts", "refuses-before-wrapping", "refuses-huge-counts", "takes-exactly-the-most"]
# a fault in geometry_bytes_add and the case that must catch it
NEGATIVE_CONTROLS = {
"no-limit": (("if (added > MAXIMUM_GEOMETRY_BYTES - *bytes)", "if (0)"), "refuses-before-wrapping"),
"no-count-check": (("if (vertex_count > MAXIMUM_GEOMETRY_BYTES / vertex_size ||", "if (0 &&"),
"refuses-huge-counts"),
}
def generated(fault=None):
geometry = read("port/linux/game/custom_edition_geometry.c")
config = f"#define MAXIMUM_GEOMETRY_BYTES {constant(geometry, 'MAXIMUM_GEOMETRY_BYTES')}\n"
code = function(geometry, "geometry_bytes_add")
if fault:
code = mutated(code, *fault)
return (("config.inc", config), ("under_test.inc", code + "\n"))
@pytest.mark.parametrize("case", CASES)
def test_case(case):
status, output = run(build("geometry_bytes", generated()), case)
assert status == 0, output
@pytest.mark.parametrize("control", NEGATIVE_CONTROLS)
def test_negative_control(control):
fault, case = NEGATIVE_CONTROLS[control]
status, output = run(build("geometry_bytes", generated(fault)), case)
assert status == CHECK_FAILED, f"geometry_bytes_add with a fault ({control}) passed '{case}': the test cannot see it"
+23 -7
View File
@@ -1,5 +1,5 @@
"""The real names of map sharing (map_share_files.c): a map named for its hash is known by its name's end, the menus
leave the hash out, and a download is checked against the digits the hash gives."""
"""The real names of map sharing (map_share_files.c): a second version of a map is known by its name's end, the menus
leave the hash out and number the versions (custom_edition_maps.c), and a map's version is its digits' number."""
import sys
from pathlib import Path
@@ -9,7 +9,8 @@ import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from harness import CHECK_FAILED, build, constant, function, mutated, read, run # noqa: E402
CASES = ["hashed-names", "safe-names", "base-length", "digits", "pieces", "display-names"]
CASES = ["hashed-names", "safe-names", "base-length", "digits", "pieces", "display-names", "versions-numbered",
"versions-without-the-first", "versions"]
# a fault in map_share_files.c, the function it is in, and the case that must catch it
NEGATIVE_CONTROLS = {
@@ -17,6 +18,10 @@ NEGATIVE_CONTROLS = {
"hex_digit", "hashed-names"),
"lets-slashes-through": (("strchr(\"/\\\\:", "strchr(\"\\\\:"), "map_share_name_safe", "safe-names"),
"lets-dots-through": (("name[0] == '.' || ", ""), "map_share_name_safe", "safe-names"),
"lets-devices-through": ((" ||\n\t\tname_device(name)", ""), "map_share_name_safe", "safe-names"),
"com-ports-not-devices": (("(!_strnicmp(name, \"com\", 3) || ", "("), "name_device", "safe-names"),
"spaces-make-a-name": (("while (length && name[length - 1] == ' ')", "while (0)"), "name_device", "safe-names"),
"every-con-a-device": (("(long)csstrlen(devices[index]) == length && ", ""), "name_device", "safe-names"),
"no-underscore": (("|| name[length - MAP_SHARE_NAME_DIGITS - 1] != '_'", ""), "map_share_name_hashed",
"hashed-names"),
"keeps-the-underscore": (("length - MAP_SHARE_NAME_DIGITS - 1)", "length - MAP_SHARE_NAME_DIGITS)"),
@@ -27,6 +32,16 @@ NEGATIVE_CONTROLS = {
"display-names"),
"stock-by-whole-name": (("map_share_name_base_length(name)", "(short)csstrlen(name)"), "stock_map_index",
"display-names"),
"the-first-numbered-too": (("short number = 1;", "short number = 2;"), "custom_edition_maps_tell_versions_apart",
"versions-numbered"),
"longer-name-a-version": (("map_share_name_base_length(version->name) != length ||", "0 ||"),
"custom_edition_maps_tell_versions_apart", "versions-numbered"),
"versions-not-numbered": (("if (number > 1)", "if (0)"), "custom_edition_maps_tell_versions_apart",
"versions-without-the-first"),
"version-bytes-swapped": (("(unsigned long)hash[0] << 24 | (unsigned long)hash[1] << 16",
"(unsigned long)hash[1] << 24 | (unsigned long)hash[0] << 16"),
"map_share_hash_version", "versions"),
"version-zero": (("return version ? version : 1;", "return version;"), "map_share_hash_version", "versions"),
}
@@ -34,11 +49,12 @@ def generated(fault=None, faulty_function=None):
header = read("port/linux/game/map_share.h")
source = read("port/linux/game/map_share_files.c")
config = ""
for name in ("MAP_SHARE_NAME_DIGITS", "MAP_SHARE_PIECE_BYTES"):
for name in ("MAP_SHARE_NAME_DIGITS", "MAP_SHARE_PIECE_BYTES", "MAP_SHARE_HASH_BYTES"):
config += f"#define {name} {constant(header, name)}\n"
text = ""
for name in ("hex_digit", "map_share_name_hashed", "map_share_name_safe", "map_share_name_base_length",
"map_share_hash_digits", "map_share_piece_count"):
for name in ("hex_digit", "map_share_name_hashed", "name_device", "map_share_name_safe",
"map_share_name_base_length", "map_share_hash_digits", "map_share_piece_count",
"map_share_hash_version"):
code = function(source, name)
if fault and name == faulty_function:
code = mutated(code, *fault)
@@ -47,7 +63,7 @@ def generated(fault=None, faulty_function=None):
maps = read("port/linux/game/custom_edition_maps.c")
start = maps.index("static struct\n{\n\tchar const *file;")
text += maps[start:maps.index("};", start) + 2] + "\n"
for name in ("stock_map_index", "display_name_make"):
for name in ("stock_map_index", "display_name_make", "custom_edition_maps_tell_versions_apart"):
code = function(maps, name)
if fault and name == faulty_function:
code = mutated(code, *fault)
@@ -0,0 +1,76 @@
"""What a map sharing download (map_share.c) keeps of another and leaves behind: the pieces another machine said it
has, a bitmap of the download's file as the offer counted its pieces, forgotten when the download ends and never read
or written beyond; and the files of a download that cannot be played, deleted again (none that was there before)."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from harness import CHECK_FAILED, build, constant, enum_with, function, mutated, read, run, structure # noqa: E402
CASES = ["restart-forgets", "have-of-another-size", "choose-within", "refuse-within", "finish-playable",
"finish-unplayable", "finish-refused"]
# a fault in map_share.c, the function it is in, and the case that must catch it
NEGATIVE_CONTROLS = {
"kept-across-downloads": (("\t\t\tremote_pieces_forget(connection, file);\n", ""), "download_end",
"restart-forgets"),
"stale-bitmap-written": (("\tif (connection->remote_pieces[index] && connection->remote_piece_count[index] != "
"file->piece_count)\n\t\tremote_pieces_forget(connection, index);\n", ""),
"frame_have", "have-of-another-size"),
"bitmap-not-counted": (("\t\tconnection->remote_piece_count[index] = file->piece_count;\n", ""), "frame_have",
"have-of-another-size"),
"read-beyond-bitmap": (("piece < connection->remote_piece_count[index] &&", ""), "remote_piece_had",
"choose-within"),
"state-not-counted": (("\t\t\t\tconnection->remote_piece_count[index] = piece_count;\n", ""), "frame_state",
"choose-within"),
"cleared-beyond-bitmap": (("else if (remote_piece_had(connection, body[0], piece))",
"else if (connection->remote_pieces[body[0]])"), "frame_refuse", "refuse-within"),
"unplayable-left": (("\t\tdownload_unfinish(download->file_count);\n", ""), "download_finish",
"finish-unplayable"),
"put-there-left": (("\t\t\tdownload_unfinish(index);\n", ""), "download_finish", "finish-refused"),
"deleted-what-was-there": (("index < count", "index <= count"), "download_unfinish", "finish-refused"),
}
FUNCTIONS = ("get_long", "remote_piece_had", "remote_pieces_forget", "download_file_close", "request_remove",
"download_end", "download_unfinish", "download_finish", "piece_choose", "frame_state", "frame_have",
"frame_refuse")
def generated(fault=None, faulty_function=None):
header = read("port/linux/game/map_share.h")
source = read("port/linux/game/map_share.c")
config = "#define P2P_MAP_SHARE_PORT %d\n" % constant(read("port/linux/src/p2p.h"), "P2P_MAP_SHARE_PORT")
config += "#define P2P_PLAYER_KEY_HASH_SIZE %d\n" % constant(read("port/linux/src/p2p.h"),
"P2P_PLAYER_KEY_HASH_SIZE")
config += "#define HALO_PORT_MAXIMUM_NETWORK_MACHINES %d\n" % constant(
read("port/linux/include/halo_port_limits.h"), "HALO_PORT_MAXIMUM_NETWORK_MACHINES")
config += enum_with(header, "MAP_SHARE_HASH_BYTES") + "\n"
for member in ("OFFER_TIMEOUT", "_have_none", "_download_none", "_file_awaiting_hashes",
"_connection_connecting"):
config += enum_with(source, member) + "\n"
for name in ("distributed_map_share_request", "download_file", "source", "download", "request", "serving",
"connection", "host_machine", "map_share_globals"):
config += structure(source, name) + "\n"
text = ""
for name in FUNCTIONS:
code = function(source, name)
if fault and name == faulty_function:
code = mutated(code, *fault)
text += code + "\n"
return (("config.inc", config), ("under_test.inc", text))
@pytest.mark.parametrize("case", CASES)
def test_case(case):
status, output = run(build("map_share_restart", generated()), case)
assert status == 0, output
@pytest.mark.parametrize("control", NEGATIVE_CONTROLS)
def test_negative_control(control):
fault, faulty_function, case = NEGATIVE_CONTROLS[control]
status, output = run(build("map_share_restart", generated(fault, faulty_function)), case)
assert status == CHECK_FAILED, f"map_share.c with a fault ({control}) passed '{case}': the test cannot see it"
@@ -0,0 +1,94 @@
"""What map sharing (map_share.c) takes of another machine's word: the LAN addresses a host names, the pieces' hashes
(the host's first, a frame of none refused), a client's progress, which keeps it in the host's game only while it
grows, and its requests for the map, taken a few seconds apart at most."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from harness import CHECK_FAILED, build, constant, enum_with, function, mutated, read, run, structure # noqa: E402
CASES = ["source-addresses", "hashes-sources", "hashes-frames", "progress-advances", "progress-asked-again",
"progress-other-level-between", "offer-of-the-version", "requests-apart", "requests-apart-after-another-level"]
# a fault in map_share.c, the function it is in, and the case that must catch it
NEGATIVE_CONTROLS = {
"any-version-offered": (("(!game || map_share_hash_version(file->hash) != (unsigned long)game->map.version)", "0"),
"host_update", "offer-of-the-version"),
"lan-host-sends-anywhere": (("return !address_private(host_address) || address_private(address);", "return TRUE;"),
"source_address_allowed", "source-addresses"),
"loopback-from-a-lan": ((" ||\n\t\t((address >> 24) == 127) != ((host_address >> 24) == 127)", ""),
"source_address_allowed", "source-addresses"),
"any-player-first": (("download->sources[connection->source].host", "TRUE"), "hashes_source", "hashes-sources"),
"anyone-as-soon": (("connection->outgoing ? HASHES_SOURCES_DELAY : REQUEST_TIMEOUT", "HASHES_SOURCES_DELAY"),
"hashes_source", "hashes-sources"),
"empty-frames-taken": (("count < 1 ||", ""), "frame_hashes", "hashes-frames"),
"saying-is-enough": ((" &&\n\t\t(!machine->offered || "
"!elapsed(machine->advanced_at, now, DOWNLOADING_STALL_TIMEOUT))", ""),
"host_machine_downloading", "progress-advances"),
"a-byte-is-a-piece": (("machine->advanced_done + MAP_SHARE_PIECE_BYTES", "machine->advanced_done + 1"),
"host_progress", "progress-advances"),
"more-than-offered": (("MIN(progress->done, machine->offered_total)", "progress->done"), "host_progress",
"progress-advances"),
"asking-starts-over": (("unsigned long advanced_at = again ? machine->advanced_at : 0;",
"unsigned long advanced_at = 0;"), "host_request", "progress-asked-again"),
"slot-kept-for-the-last": (("machine->advanced_at && machine->address", "machine->advanced_at"),
"host_request", "progress-asked-again"),
"the-level-asked-for-counts": (("game && !csstrncmp(machine->advanced_level, game->map.name, LEVEL_NAME_BYTES - 1)",
"!csstrncmp(machine->request.level_name, request->level_name, LEVEL_NAME_BYTES)"),
"host_request", "progress-other-level-between"),
"another-map-counts-on": (("game && !csstrncmp(machine->advanced_level, game->map.name, LEVEL_NAME_BYTES - 1)",
"game"), "host_request", "progress-other-level-between"),
"every-request-taken": (("machine->requested_at && machine->address", "FALSE && machine->address"),
"host_asked", "requests-apart"),
"waiting-request-lost": (("machine->request_waiting && elapsed(", "FALSE && elapsed("), "host_update",
"requests-apart"),
"waiting-request-overlooked": ((" && !machine->request_waiting)", ")"), "host_update",
"requests-apart-after-another-level"),
}
FUNCTIONS = ("get_long", "elapsed", "address_internet", "address_private", "source_address_allowed", "hashes_source",
"frame_hashes", "host_machine_valid", "host_machine_downloading", "map_share_machine_downloading",
"map_share_machine_needs_map", "level_name_copy", "host_offer", "host_request", "host_asked",
"host_update", "host_progress")
def generated(fault=None, faulty_function=None):
header = read("port/linux/game/map_share.h")
source = read("port/linux/game/map_share.c")
config = "#define P2P_MAP_SHARE_PORT %d\n" % constant(read("port/linux/src/p2p.h"), "P2P_MAP_SHARE_PORT")
config += "#define P2P_PLAYER_KEY_HASH_SIZE %d\n" % constant(read("port/linux/src/p2p.h"),
"P2P_PLAYER_KEY_HASH_SIZE")
config += "#define HALO_PORT_MAXIMUM_NETWORK_MACHINES %d\n" % constant(
read("port/linux/include/halo_port_limits.h"), "HALO_PORT_MAXIMUM_NETWORK_MACHINES")
config += enum_with(header, "MAP_SHARE_HASH_BYTES") + "\n"
config += structure(header, "map_share_file") + "\n"
for member in ("OFFER_TIMEOUT", "_offer_ok", "_offer_in_progress_bit", "_progress_downloading",
"_file_awaiting_hashes"):
config += enum_with(source, member) + "\n"
for name in ("distributed_map_share_request", "distributed_map_share_offer", "distributed_map_share_offer_file",
"distributed_map_share_progress", "download_file", "source", "download", "request", "serving",
"connection", "host_machine", "map_share_globals"):
config += structure(source, name) + "\n"
text = ""
for name in FUNCTIONS:
code = function(source, name)
if fault and name == faulty_function:
code = mutated(code, *fault)
text += code + "\n"
return (("config.inc", config), ("under_test.inc", text))
@pytest.mark.parametrize("case", CASES)
def test_case(case):
status, output = run(build("map_share_trust", generated()), case)
assert status == 0, output
@pytest.mark.parametrize("control", NEGATIVE_CONTROLS)
def test_negative_control(control):
fault, faulty_function, case = NEGATIVE_CONTROLS[control]
status, output = run(build("map_share_trust", generated(fault, faulty_function)), case)
assert status == CHECK_FAILED, f"map_share.c with a fault ({control}) passed '{case}': the test cannot see it"
+4
View File
@@ -132,6 +132,9 @@ ZLIB_DEFINES = ("-DZ_PREFIX", "-Dz_errmsg=z_port_errmsg")
MBEDTLS_DIR = Path("port/third_party/mbedtls")
# internet play's UPnP (port/linux/src/posix_upnp.c)
MINIUPNPC_DIR = Path("port/third_party/miniupnpc")
# the Custom Edition maps' Ogg Vorbis decoder (custom_edition_sounds.c),
# compiled as the port's game units are
STB_VORBIS_SOURCE = Path("port/third_party/stb_vorbis/stb_vorbis.c")
# miniupnpc's own build's definitions (its Makefile), and a static library
MINIUPNPC_DEFINES = ["-DMINIUPNP_STATICLIB", "-DMINIUPNPC_SET_SOCKET_TIMEOUT", "-DMINIUPNPC_GET_SRC_ADDR",
"-D_BSD_SOURCE", "-D_DEFAULT_SOURCE"]
@@ -557,6 +560,7 @@ def generate_linux_build(n: Writer, sln: Any) -> None:
# sources do (port/linux/game).
for source in sorted(Path(config["game_sources"]).glob("*.c")):
add_object(source, game_cflags)
add_object(STB_VORBIS_SOURCE, game_cflags)
platform_dir = Path(config["platform_sources"])
platform_cflags = " ".join([
+5
View File
@@ -618,6 +618,11 @@ MAP_KIND_CHOOSER = "main_menu/new_select/list_item_0_map_kind"
# changes to the PC version's widgets (by our names): attributes set, all
# their handlers replaced, children added, game data inputs added
WIDGET_PATCHES = {
# The host returns here after a round with no menu history. Back must
# reopen the main menu when it closes the last screen, as the lobby does.
f"{MT}/connected/connected_map_select_wrapper": {"set": {
"flags": "pass_unhandled_to_focused_child pause_game main_menu_if_no_history tag_controller_index force_handle_mouse",
}},
# (the profile settings' picture: on Gamepad Setup's row, the profile's
# button settings, BITMAP_FRAMES; menu_functions.c's
# profile_gamepad_layout)
+15 -2
View File
@@ -7,13 +7,17 @@ The Steam Frame's installer as release assets (port/android/STEAM_FRAME.md):
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
(steam_frame_launch.sh, as ringfall-mjolnir-vr.sh, which updates the game
from the repository named), the build's number (build.txt: the workflow's
run number, HALO_BUILD_NUMBER or GITHUB_RUN_NUMBER; 0, which never
updates, without one), 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 os
import sys
import zipfile
from pathlib import Path
@@ -38,13 +42,22 @@ def main():
]
files += [(picture, "grid/" + picture.name, 0o644)
for picture in sorted((ANDROID / "steam_frame_grid").glob("*.png"))]
build = os.environ.get("HALO_BUILD_NUMBER") or os.environ.get("GITHUB_RUN_NUMBER") or "0"
if not build.isdigit():
build = "0"
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())
data = path.read_bytes()
if name == "ringfall-mjolnir-vr.sh":
data = data.replace(b"@REPOSITORY@", repository.encode())
archive.writestr(info, data)
info = zipfile.ZipInfo("build.txt", date_time=(2026, 1, 1, 0, 0, 0))
info.external_attr = (0o100000 | 0o644) << 16
archive.writestr(info, build + "\n")
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)
+76
View File
@@ -0,0 +1,76 @@
"""The Darwin to ELF assembly conversion of the ILP32 guest
(tools/android_asm_convert.py): string literals keep every byte. It once took
a quoted .ascii operand for a quoted symbol name and dropped its leading "_"
(and, after an escaped quote, the "_" of any word after it), which shifted
tables such as Opus's pulse cache by a byte: voice chat crashed in
decode_pulses on the arm64 and Android builds."""
import shutil
import struct
import subprocess
import sys
from pathlib import Path
import pytest
from tools.android_asm_convert import Converter
ROOT = Path(__file__).resolve().parent.parent
def convert(*lines):
return Converter(list(lines)).run().splitlines()
@pytest.mark.parametrize("directive", [
'.ascii\t"_c[}]L{s{"',
'.asciz\t"_transport_error_none"',
'.ascii\t"v\\177\\"O7v\\177_\\037\\374"',
'.ascii\t"=`\'\\\\_\\026"',
'.asciz\t"ltmp0 _x @PAGE ; not a comment"',
])
def test_string_literals_are_kept(directive):
op, args = directive.split("\t", 1)
assert convert("l_.str:", directive) == [".Ll_.str:", f"\t{op} {args}"]
def test_symbols_are_still_renamed():
assert convert("\t.globl\t_table", "_table:", "\tadrp\tx8, _table@PAGE",
"\tadd\tx8, x8, _table@PAGEOFF", '\t.long\t"_odd name"') == [
"\t.globl table", "table:", "\tadrp x8, table", "\tadd x8, x8, :lo12:table", '\t.long "odd name"']
def rodata(elf: bytes) -> bytes:
"""the .rodata section of a 64-bit little-endian ELF object"""
shoff, = struct.unpack_from("<Q", elf, 0x28)
shentsize, shnum, shstrndx = struct.unpack_from("<HHH", elf, 0x3a)
sections = [struct.unpack_from("<IIQQQQ", elf, shoff + index * shentsize) for index in range(shnum)]
names = sections[shstrndx][4]
for name, _, _, _, offset, size in sections:
if elf[names + name:elf.index(b"\0", names + name)] == b".rodata":
return elf[offset:offset + size]
raise AssertionError("no .rodata")
def test_tables_survive_the_build(tmp_path):
"""every byte value, and the patterns that were lost, through the
guest's pipeline (clang for arm64_32, the conversion, the assembler)"""
clang = shutil.which("clang")
if not clang:
pytest.skip("no clang")
table = bytes(range(256)) + b'_a"_b\\_c' + bytes(reversed(range(256)))
source = tmp_path / "table.c"
source.write_text(
"const unsigned char table[] = {" + ",".join(str(b) for b in table) + "};\n"
'const char text[] = "_starts with an underscore";\n')
darwin, converted, obj = tmp_path / "table.darwin.s", tmp_path / "table.s", tmp_path / "table.o"
try:
subprocess.run([clang, "--target=arm64_32-apple-watchos", "-O2", "-S", str(source), "-o", str(darwin)],
check=True, capture_output=True)
except subprocess.CalledProcessError:
pytest.skip("clang has no arm64_32 target")
subprocess.run([sys.executable, str(ROOT / "tools" / "android_asm_convert.py"), str(darwin), str(converted)],
check=True)
subprocess.run([clang, "--target=aarch64-linux-gnu", "-c", str(converted), "-o", str(obj)], check=True)
data = rodata(obj.read_bytes())
assert table in data
assert b"_starts with an underscore\0" in data
+13
View File
@@ -880,6 +880,19 @@ def test_malformed_structure_bsp_materials_are_rejected(report_tool, tmp_path, c
assert report["load"] == BAD_BSP_GEOMETRY
@pytest.mark.parametrize("vertex_count, loads", [(64000, True), (64001, False)])
def test_structure_bsp_materials_hold_at_most_the_games_vertices(report_tool, tmp_path, vertex_count, loads):
"""the game's MAXIMUM_VERTICES_PER_STRUCTURE_MATERIAL, to which the
validator cuts a material's counts (which would move its lightmap
vertices): a material of more, all within the BSP, is refused"""
material = {"vertex_count": vertex_count, "lightmap_vertex_count": vertex_count}
returncode, report, _ = converted(report_tool, Map(bsp_sizes=(0x500000,), bsp_material=material), tmp_path)
if loads:
assert returncode == 0 and report["structure_bsp_materials_checked"] == "1"
else:
assert returncode == 1 and report["load"] == BAD_BSP_GEOMETRY
def test_structure_bsp_lightmaps_outside_the_bsp_are_rejected(report_tool, tmp_path):
cache = Map(bsp_material={})
path = patched(cache.write(tmp_path), cache.where["bsp"] + 0x18 + 0x108, "<I", BASE)
+3 -2
View File
@@ -43,8 +43,8 @@ from typing import Any, Dict, List, Optional
from .android_build import VARIADIC_PROTOTYPE_FILES
from .embed_assets import hud_assets_build, hud_configure_inputs
from .linux_build import (EXPAT_DIR, EXPAT_SOURCES, KCP_DIR, MONOCYPHER_DIR, MUSL_MATH_DIR, TOML_DIR, XDK_INCLUDE,
ZLIB_DEFINES, ZLIB_DIR, ZLIB_SOURCES, compile_launcher, configuration_defines,
from .linux_build import (EXPAT_DIR, EXPAT_SOURCES, KCP_DIR, MONOCYPHER_DIR, MUSL_MATH_DIR, STB_VORBIS_SOURCE, TOML_DIR,
XDK_INCLUDE, ZLIB_DEFINES, ZLIB_DIR, ZLIB_SOURCES, compile_launcher, configuration_defines,
game_defines_and_includes, game_sources, musl_math_cflags, musl_math_sources,
opus_cflags, opus_sources, updater_defines, xdk_headers)
from .ninja_syntax import Writer
@@ -245,6 +245,7 @@ def generate_web_build(n: Writer, sln: Any) -> None:
add_object(source, cflags)
for source in sorted(Path(config["game_sources"]).glob("*.c")):
add_object(source, game_cflags)
add_object(STB_VORBIS_SOURCE, game_cflags)
# ---------- the platform layer and the web runtime
+3 -2
View File
@@ -18,8 +18,8 @@ import zipfile
from pathlib import Path
from typing import Any, Dict, List, Optional
from .linux_build import (LINUX_PROFILE, MINIUPNPC_DIR, OPTIMISATION, WINDOWS_PROFILE, XDK_INCLUDE, lto_mode,
march_flag, miniupnpc_sources, pgo_mode, compile_launcher, configuration_defines,
from .linux_build import (LINUX_PROFILE, MINIUPNPC_DIR, OPTIMISATION, STB_VORBIS_SOURCE, WINDOWS_PROFILE, XDK_INCLUDE,
lto_mode, march_flag, miniupnpc_sources, pgo_mode, compile_launcher, configuration_defines,
game_defines_and_includes, game_sources, musl_math_cflags, musl_math_sources, opus_cflags,
opus_sources, pgo_profile, profile_use_flags, xdk_headers)
from .embed_assets import hud_assets_build, hud_configure_inputs
@@ -395,6 +395,7 @@ def generate_windows_build(n: Writer, sln: Any) -> None:
add_object(source, flags)
for source in sorted(Path(linux_config["game_sources"]).glob("*.c")):
add_object(source, game_cflags)
add_object(STB_VORBIS_SOURCE, game_cflags)
linux_platform = Path(linux_config["platform_sources"])
platform_cflags = " ".join([