Repository navigation
Custom Edition maps a host does not send downloaded from the community's archives - #335
Open
FractumSeraph wants to merge 4 commits into
Open
FractumSeraph wants to merge 4 commits into
FractumSeraph wants to merge 4 commits into
Conversation
This was referenced Oct 13, 2026
A Custom Edition map's level is named for its hash (custom_maps\<name>_<8 digits>), so that two versions of a map have two names and a game always loads the host's. Until now the map file in custom_maps was renamed to that name. A player's map files are theirs, and other programs use them by name (Halo Custom Edition, map managers, a server's playlists), so the file now keeps its name, and the game remembers its hash instead: - map_share_files.c remembers the digits of each map it hashed by its folder, name, size and time, and keeps them in the save root (z:\map_hashes.txt), so that a map is hashed once until its file changes, as when it was renamed. map_share_name_digits gives a file's digits, hashing it then if it was not hashed yet. - A host's level is named custom_maps\<name>_<digits> as before (map_share_level_name_hash): nothing on the wire changes, and network version 28's builds (build-201) play with it either way. - custom_edition_map_path finds <name>.map for <name>_<digits> when that file's digits are those. Maps renamed by earlier builds keep working. A name too long to keep whole with the digits is cut short to fit, as the renaming did, and finds the file whose name starts so and whose digits they are. - The maps of a Custom Edition install's folder are hashed in the background too (they are still not shared), so that a game on one does not hash it while joining. The digits are kept only if the file did not change while it was hashed, map_hashes.txt is written once the hashing is done, and it holds as many maps as the map lists. - The menus sort the maps by their names without the hash, so that two versions of a map are always told apart. Checked: the Linux CI steps in Docker; the web build and its tests; the Windows release build. End to end on Linux in Docker, on the loopback: this build hosts chaosgulch (its file not renamed) and build-201's client downloads it by map sharing and plays; build-201 hosts and this build's client does the same, and plays the downloaded copy on a second join; a ChupathingyCE-based dedicated server (version 28, hash names) sends it to this build's client; a second run of a host hashes nothing again (map_hashes.txt). No map file of this build's changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loading screen (interface/progress_bar.c: a light sweeping from left to right over a picture, with a glowing trail) drew nothing: it uses the fixed-function pipeline (SetVertexShader with 0), which d3d8_gl.c did not have, and its picture, loading.tga, is on no retail disc. - d3d8_gl.c draws fixed-function immediate-mode draws with a vertex shader of its own (nv2a_fixed_function_glsl), positions transformed by the world, view and projection matrices; and keeps a front buffer (a 640x480 render target taking the middle of the frame at BlockUntilVerticalBlank) for GetBackBuffer with -1, the frame before, whose blur makes the light's trail. The screen is drawn in the middle 640 columns, as the menus are on a wide screen. - After it, the rasterizer's caches of the device's states and of its vertex shader are made the device's again (rasterizer_forget_hardware_state, rasterizer_vertex_shader_runtime_forget): it sets them itself; and SelectVertexShader (a packed program) always leaves the fixed-function pipeline. - On OpenGL ES (Android, the web) the fixed-function shader turns Direct3D's clip space as the others do (HALO_GLES); without OpenGL (debug.null_renderer) it is not compiled. - The picture is the Xbox's own: the retail default.xbe holds it (320 by 240, runs of 4-bit greys after its width and height), and importing a disc image now writes it into the maps folder as loading.tga: port/linux/src/ xiso.c, the Android app's XisoExtractor.java and the web page's xiso-worker.js. Without one (a maps folder copied by hand) the light sweeps over clouds the game makes. No game data is in the repository. - progress_bar.c also has a mode a download screen over the menus can use (a share given, from 0, drawn over the menus, and let go of). - A picture that cannot be written whole is not left cut short (the loading screen would read past it). Checked: port/web/tests/xiso.test.js takes the picture out of made-up executables (and an image's default.xbe); the Java decoder gives the same bytes as the JavaScript one; xiso.c's picture from a Rev 2 disc image matched a separate decoder's byte for byte; the Linux CI steps and the web build and its tests in Docker; the Windows release build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y's archives
Map sharing downloads a missing map from the game's players. When the host
does not send it (SHARE MAPS off, a build that names its maps without
their hash, or a game seen in the games list before joining), the game now
downloads it from the zips its authors published, on HaloNet's map
repository and HaloMaps, else from a web seed, and checks every byte
against an index of the maps before it uses it. Design and details in
docs/map_downloads.md.
- The index (port/assets/network/map_index.txt, tools/map_index.py):
5,829 maps and the three resource maps, by file name and header checksum
(the map version the host already sends), with each one's size and the
info hash of its single-file torrent, and for 5,826 of them the
archives' zips that hold it. The builds put it next to the game.
- The archives (port/linux/src/map_archives.c, maps.archives): HaloNet's
zip by its URL, HaloMaps's as its Download button fetches it, then the
web seeds (maps.web_seeds), over HTTPS with the self-updater's download
(update_request in update.h: a POST, a user agent and a stop flag added
to posix_update.c and win32_update.c). The map is taken out of its zip
(map_archives_zip.c: every offset checked, and its CRC), and a file is
used only once its info hash is the index's (map_archives_verify, with
map_sha1.c); one that is not is thrown away and the next source tried.
- The downloads (port/linux/src/map_downloads.c, maps.downloads): one file
at a time, polled each frame from main.c's loop. A map goes into
custom_maps as <name>_<digits>.map, the name map sharing gives that
version, beside any other version; a resource map keeps its name and is
written only when none is there. No file is ever replaced or renamed
(the name is taken with O_EXCL before the rename on POSIX).
- The hooks: a host declining map sharing's request (map_share.c,
download_offered) and a level map sharing cannot download
(cache_files_map_present) start a download, and the player joins again
once it is there; joining a game from the games list whose map is
missing waits for its download and joins by itself (menu_functions.c).
custom_edition_cache_files_missing says which files are missing.
- A host's level named custom_maps\<name> alone, whose version is not the
player's <name>.map, plays the version beside it whose header checksum is
the host's (custom_edition_cache.c; custom_edition_map_version_forget
once the player leaves).
- The download screen (port/linux/game/download_screen.c,
maps.download_screen): Halo's loading screen over the menus at the
download's share, the map's name over it, B to stop it; the menus go on
behind it. With it off, the games list's status line shows the download.
- tools/map_download_check.c runs the unzip and the check alone (ninja
linux builds it): tools/test_map_download_check.py.
- The web build leaves the downloads out (port/web/src/web_map_downloads.c),
and Android has no HTTPS of its platform layer: neither downloads.
Nothing goes over the game's connections: no network version of its own.
Checked:
- The Linux CI steps in Docker (tools/ci_build.py linux release): the
test step's pytest list, with tools/test_map_index.py and the new
tools/test_map_download_check.py (335 passed), and test_light_storage.
- The web CI steps in Docker (emsdk 6.0.10): ci_build.py web release and
tools/test_web_build.py; node --test port/web/tests.
- The Windows release built with clang-cl and MSVC 2019's libraries
(ci_build.py windows release), and run on Windows: a host with map
sharing off and a client without the map on the loopback; the map came
from the web seed (WinHTTP), and again from HaloMaps alone, was the
index's, went in as ChaosGulch_7cac43f9.map and was played on the second
join; the host's chaosgulch.map unchanged.
- End to end on Linux in Docker, two or three machines on the loopback,
with OpenCE build-201 and a ChupathingyCE-based dedicated server
(version 28, plain level names when set to):
- this build hosts (hash name, its file not renamed) and build-201's
client downloads the map by map sharing and plays; build-201 hosts
and this build's client does the same, and plays it again on a second
join;
- a host with map sharing off: the client's map came from the web seed
(chaosgulch), and from HaloNet's zip beside the client's own other
version of battlecity_lvl1; each the host's byte for byte, played on
the second join, the client's own files unchanged;
- a server naming its level custom_maps\chaosgulch, the client having
another version: the host's came from HaloMaps's zip as
ChaosGulch_7cac43f9.map, and the second join played it (chosen by its
header checksum);
- an index whose info hash is not the file's: the web seed's download
refused, nothing written;
- map_hashes.txt: a second run of a host hashes nothing again;
- the download screen recorded on a virtual display: the light sweeping
over the Xbox's picture as paradis_perdu (126 MB) came from the web
seed, "from the web seed", the share counting up, B=STOP.
- No map file was renamed or changed by this build in any run (build-201's
host renames its own for its hash, as it does).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When map sharing downloads a map from the game's players, a web seed (maps.web_seeds, by default the seed box's folder of every map of the index) now sends pieces of it too, so that a host whose upload is slow does not hold its players' downloads up. Nothing goes over the game's connections: no network version. - A file of the download that the index has (the map by its name and the host's header checksum, a resource map by its name), at the size the host offered, is asked of the web seed 16 pieces (4 MiB) at a time, pieces no player has been asked for, with an HTTP range request on a thread of its own (map_archives_range_*, map_downloads_range_*). Each piece is checked against its hash as a player's piece is; one that is not the file's ends the web seed for that file, and three failed asks end it for the download, with a pause after each. - update_request_range (update.h) on Linux (Mbed TLS) and Windows (WinHTTP): a GET with a Range header whose answer must be 206, for that range (Content-Range) and of its length; a server that sends the whole file instead is refused unread. No range is asked while a map comes from the archives (one HTTPS request at a time on Linux). - The download cap of update_request is 1 GiB (the largest map of the index is about 760 MB). - Android and the web have no HTTPS of their platform layer: their builds ask nothing (the stubs in map_archives.c and web_map_downloads.c). - docs/map_downloads.md, port/linux/README.md and NETCODE.md say so; the setting's comment too. Checked: the Linux CI steps in Docker (335 passed), the web build and its tests, and the Windows release build. End to end on Linux in Docker, a host sharing chaosgulch at 16 KB/s: 13.4 of the 13.9 MB came from the web seed, the map the host's byte for byte, and the client played; a web seed answering 404: three failed asks, then the host sent the map; maps.downloads off: no web seed asked. On Windows, the same host at 16 KB/s: 13.4 MB came from the web seed in WinHTTP ranges, and the client played the map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FractumSeraph
force-pushed
the
map-downloads
branch
from
October 13, 2026 14:17
f743723 to
7e77a45
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Custom Edition maps that a host does not send are now downloaded from the community's archives (HaloNet, then HaloMaps, then a web seed). Each file is checked against an index of the maps before the game uses it. Map sharing's downloads from the game's players also take pieces from the web seed, so a host with a slow upload doesn't hold its players up.
This builds on #333 and #334, and its branch contains their commits until they are merged; after that I'll rebase it so only its own two commits remain: "Custom Edition maps a host does not send downloaded from the community's archives" and "Map sharing takes pieces from the web seeds too". #333 gives a downloaded map its name (
<name>_<hash>.map, which the game finds without renaming), and #334 gives the download screen its drawing.This replaces #315, which added a BitTorrent client and reverted map sharing, and no longer merged with
main(the web build among it). This one is built onmainas it is, network version 28, and keeps map sharing as it is. It has no BitTorrent client and changes nothing on the wire.When it downloads
Map sharing downloads a missing map from the game's players. When it cannot, the game downloads the map instead:
A map comes from the zip on HaloNet's repository or on HaloMaps (
maps.archives), taken out of the zip, and otherwise from a web seed (maps.web_seeds).Every file is checked against the index (
port/assets/network/map_index.txt: 5,829 maps plus the three resource maps) by the SHA-1 info hash of its single-file torrent. A file that fails is thrown away and the next source is tried.Halo's loading screen covers the menus while a map downloads, and B stops the download. From the games list, the join goes ahead by itself once the map is there. When the host declined, the player is told to join again.
Details are in
docs/map_downloads.md.Web seeds for map sharing
When map sharing downloads a map from the game's players, the web seed sends pieces of it too:
update_request_rangeon Linux and Windows).206for exactly that range (Content-Range) and its full length. A server that sends the whole file instead is refused unread.maps.downloads = falseor an emptymaps.web_seeds.Nothing of this goes over the game's connections, and players of other builds are unaffected.
No map file is ever replaced or renamed
custom_mapsas<name>_<digits>.map, the same name map sharing gives that version, beside any other version.O_EXCLbefore the rename.<name>.mapis another version, the client plays the version beside it whose header checksum is the host's.Platforms
update_requestadds a POST, a user agent and a stop flag toposix_update.candwin32_update.c, plusupdate_request_range; its download cap is 1 GiB (the largest map in the index is about 760 MB).port/web/src/web_map_downloads.c).Network version
Unchanged (28). Nothing new goes over the game's connections.
The web seed and torrents
The default web seed,
https://halomaps.fractumseraph.net/maps/, is my seed box's folder of every map in the index. The index's info hashes are those of the torrents it seeds, so anyone can seed the same files with any BitTorrent client. A BitTorrent source could be added later as another entry inmaps.archives, without changing the index.Checked
tools/ci_build.py linux release): the test step's pytest list withtools/test_map_index.pyand the newtools/test_map_download_check.py(335 passed; each commit also builds on its own), andtest_light_storage.py.ci_build.py web release,tools/test_web_build.py,node --test port/web/tests(16 passed).ci_build.py windows releasebuilt with clang-cl and MSVC 2019's libraries. Then run on Windows:ChaosGulch_7cac43f9.mapand was played on the second join; the host'schaosgulch.mapwas unchanged.chaosgulch.mapkeeps its name and bytescustom_maps\chaosgulch(no hash); the client has another versionChaosGulch_7cac43f9.map; the second join plays it, chosen by header checksum; the client's own copy is unchangedcustom_maps\chaosgulch; the client has two other versions named for their hashes and nochaosgulch.mapChaosGulch_7cac43f9.map; the second join plays it, chosen by header checksum; the client's two files are unchangedmaps.downloadsoffmap_hashes.txtis used and nothing is hashed againNo map file was renamed or changed by this build in any run.
Found along the way and left alone (they are in build-201 too): with
debug.network_test, a client that joins a lobby game whose host starts it mid-download stops responding (so the two map sharing rows above usedebug.network_test_startto give the download time); and the Windows build does not exit atdebug.exit_after.🤖 Generated with Claude Code