mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-17 16:00:26 +02:00
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7da699b035 | ||
|
|
c5c604bb95 | ||
|
|
8662777f11 | ||
|
|
e24df8ead9 | ||
|
|
7a10a413c8 | ||
|
|
5ce39c27a7 | ||
|
|
67d738451c | ||
|
|
91bdb7e67c | ||
|
|
bf778b6d60 | ||
|
|
d8a786f5a1 | ||
|
|
7c294c9c39 | ||
|
|
f90d955527 | ||
|
|
085676d827 | ||
|
|
4301ca88d6 | ||
|
|
b5cdd9268c | ||
|
|
34b61fba31 | ||
|
|
87fe9fc1ad | ||
|
|
8102e711c3 | ||
|
|
3e0ba55111 | ||
|
|
37ded3287c | ||
|
|
9c70f5f5e3 | ||
|
|
d9669f6eef | ||
|
|
238f3e2391 | ||
|
|
6d64abdb42 | ||
|
|
52e33a391e |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,16 +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) |
|
||||
@@ -42,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)
|
||||
@@ -518,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
|
||||
@@ -526,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 <in@fishtank.com>.
|
||||
*
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
@@ -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. |
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)." },
|
||||
@@ -780,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." },
|
||||
|
||||
@@ -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)
|
||||
|
||||
+119
-14
@@ -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) */
|
||||
@@ -298,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 */
|
||||
@@ -585,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,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");
|
||||
@@ -658,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)
|
||||
@@ -889,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, ¤t);
|
||||
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();
|
||||
@@ -1886,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);
|
||||
}
|
||||
@@ -2053,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Vendored
+38
@@ -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
@@ -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);
|
||||
@@ -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"),
|
||||
|
||||
Vendored
+5
-4
@@ -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
|
||||
|
||||
Vendored
+3
-1
@@ -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
@@ -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
@@ -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 "
|
||||
|
||||
@@ -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,
|
||||
@@ -7521,8 +7522,8 @@ static short const vr_option_pages[NUMBER_OF_VR_OPTION_PAGES][VR_OPTIONS_PER_PAG
|
||||
_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_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_resolution, _vr_option_quit,
|
||||
NONE, NONE, NONE, NONE, NONE, 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 */
|
||||
@@ -7942,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;
|
||||
@@ -8651,7 +8655,10 @@ static void ui_vr_options_render(
|
||||
short count = ui_vr_options_refresh_rates(rates, &chosen, ¤t);
|
||||
|
||||
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);
|
||||
@@ -8660,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, ¤t) <= 1;
|
||||
break;
|
||||
}
|
||||
case _vr_option_resolution:
|
||||
{
|
||||
real scale = vr_resolution_scales[ui_vr_options_nearest((real)config_real("vr.resolution_scale"),
|
||||
@@ -8717,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)
|
||||
{
|
||||
@@ -8739,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
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,10 +223,7 @@ class Converter:
|
||||
if op == ".align":
|
||||
self.emit(f"\t.p2align {args}")
|
||||
return
|
||||
if op in (".ascii", ".asciz", ".string"):
|
||||
# string data, kept as it is: no symbol names in it (renamed, a
|
||||
# "_" at the start of a word was dropped: a string with \" in it
|
||||
# went wrong, Opus's tables among them)
|
||||
if op in STRING_DIRECTIVES:
|
||||
self.emit(f"\t{op} {args}")
|
||||
return
|
||||
if op.startswith("."):
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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([
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,32 +1,76 @@
|
||||
"""Tests of tools/android_asm_convert.py, the Mach-O to ELF assembly
|
||||
conversion of the Android build's guest code."""
|
||||
"""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
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import pytest
|
||||
|
||||
from android_asm_convert import Converter # noqa: E402
|
||||
from tools.android_asm_convert import Converter
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def convert(*lines):
|
||||
return Converter(list(lines)).run().split("\n")
|
||||
return Converter(list(lines)).run().splitlines()
|
||||
|
||||
|
||||
def test_symbols_lose_their_underscore():
|
||||
out = convert("_main:", "\tbl\t_foo", "\t.long\t_bar")
|
||||
assert out[:3] == ["main:", "\tbl foo", "\t.long bar"]
|
||||
@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_strings_are_kept():
|
||||
# (a string starting with "_", or one with \" in it, lost a "_": Opus's
|
||||
# pulse cache was one byte short, and its encoder crashed)
|
||||
out = convert(
|
||||
'\t.asciz\t"_lan_"',
|
||||
'\t.ascii\t"\\"$&\'\\0063_\\206"',
|
||||
'\t.string\t"l_.str _foo"',
|
||||
)
|
||||
assert out[:3] == [
|
||||
'\t.asciz "_lan_"',
|
||||
'\t.ascii "\\"$&\'\\0063_\\206"',
|
||||
'\t.string "l_.str _foo"',
|
||||
]
|
||||
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
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in new issue
Block a user