Compare commits

...
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 59b1fb422b Steam Frame: a library button that starts, updates and quits the game
The launcher gives Lepton what Steam gives an Android title of its own (a
compat data path, which the app's data links to after an install, and the
shader path), keeps a container id of its own, tries a start that ends at
once again (a new APK's first start only installs it), and stops when the
game was quit. Steam's "Exit game" cannot reach the game in Lepton's
container: the VR options get "Quit Halo" (asked twice), which on Android
now ends the process and leaves quit_requested.txt for the launcher. The
library's pictures are in port/android/steam_frame_grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:17:35 +02:00
Stefan MaierandClaude Opus 5.5 5ecd3075a0 Steam Frame: a launcher that starts the game as Steam starts Android titles
Lepton runs the APK in a container of its own, kept between starts, and
installs it anew whenever the APK is newer; the script starts it a second
time after a new APK's first start, which only installs it. With the APK in
~/Documents/HaloCE, updating the game is replacing that file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:17:54 +02:00
Stefan Maier 8e8713681d Merge remote-tracking branch 'origin/fix/multiview-desktop-compile' 2026-10-04 22:59:55 +02:00
Stefan Maier d7099f3e40 Fix desktop link: bot machine check and glClearDepthf were Android-only
network_game_server_is_bot_machine (cea7abfc, Bots as players of the
host) was written into the HALO_ANDROID block of network_server_manager.c
while bots.c calls it on every platform, and so was the array it reads;
both move out of the guard (on the desktop nothing marks a bot machine,
so the check is always false there, the writes stay Android-only).

glClearDepthf (dc21f4b4, Play in VR on OpenXR headsets) is ES-only:
GL_FUNCTIONS declares it for Android's loader and the define at the top
maps glClearDepth to it, but the desktop context links against GL's
glClearDepth. The clear now calls glClearDepth, which the Android define
turns into glClearDepthf as before.
2026-10-04 22:37:17 +02:00
Stefan Maier 4ba36d7305 VR: fix desktop build, targets_multiview was Android-only
6c3f4ce2 (VR: draw both eyes in one pass) changed prepare_draw to pass
targets_multiview to vertex_shader_get, but the variable is declared and
kept inside the HALO_ANDROID guard (L887): Linux and Windows failed to
compile with 'use of undeclared identifier targets_multiview', Android
was fine. On the desktop there are no multiview layers to stand in for
the back buffer, so the desktop call passes FALSE.
2026-10-04 22:25:21 +02:00
Stefan Maier 855f083117 Merge branch 'frame/performance' 2026-10-04 22:04:33 +02:00
Stefan Maier 6f3786de91 Merge branch 'fix/vr-shot-origin' 2026-10-04 22:04:33 +02:00
Stefan MaierandClaude Opus 5.5 493713b0a6 Steam Frame: the refresh rates offered in the log; its rate and speed noted
The runtime offers only the rate SteamOS is set to. At 72 Hz the game holds
it with multiview (about 11.6 ms a frame); at 90 Hz it cannot, and the
frames SteamVR makes up showed as a doubled picture when the head turned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:47:56 +02:00
Stefan Maier bf356fc58a Merge branch 'fix/vr-shot-origin' into frame/controllers 2026-10-04 21:42:21 +02:00
Stefan MaierandClaude Opus 5.5 6a7b919522 VR: shots fly along the aim, not above it
A shot fired from the muzzle of the weapon in the hand was kept inside the
level by the game's test of the line from the eyes to it, which in VR runs
down through the player's own body: the test hit it and moved the shot up
towards the eyes, from where it flew parallel to the aim, a few centimetres
above the crosshair and the laser (measured: up to 7 cm at 3 m). In VR the
line is tested against the level alone (a hand through a wall still does
not fire through it); measured after: the impacts on their rays, on
average within a millimetre.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:42:07 +02:00
Stefan MaierandClaude Opus 5.5 c1305fa94f Steam Frame: its controllers as themselves, with the Xbox's layout
SteamVR offers the Frame's controllers by their own profile
(XR_VALVE_frame_controller_interaction); without bindings for it, it mapped
the Quest's onto them, and X and Y landed on the left one's D-pad. Now: the
right one's A, B, X, Y and menu button are the game's A, B, X, Y and Start
(the Xbox's layout), the left one's View is Back and its D-pad the D-pad,
the bumpers switch grenades and the flashlight. The VR options' page of
buttons shows the Frame's layout there. The runtime's extensions and the
controllers' profile are in the log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:28:13 +02:00
Stefan MaierandClaude Opus 5.5 0fc85d6c67 Steam Frame: the overlays' alpha from their colour on SteamVR
SteamVR draws a quad's texels by their alpha alone, and the game's menus,
HUD and text have none in their additive parts: on the Frame nothing showed.
On SteamVR (the runtime's name says) the copy into the quads' images takes
their alpha from their colour where it is higher; HALO_XR_QUAD_ALPHA
(colour, keep) overrides, for trying. The Frame's notes updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:17:43 +02:00
Stefan MaierandClaude Opus 5.5 c0a08f395e Steam Frame: the game's data in the SteamOS user's Documents when it is there
Lepton empties the app's own folders (and uninstalls the app) whenever it
starts again; Documents is the SteamOS user's and kept. When
/home/steamos/Documents/HaloCE holds the maps and is writable, the launcher
and the game take it as the data folder (maps, config.toml, saves, logs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:51:08 +02:00
Stefan MaierandClaude Opus 5.5 06cb70fb58 Steam Frame: notes, and no variable the Android loader never reads
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:27:28 +02:00
Stefan MaierandClaude Opus 5.5 1709698053 Start on the Steam Frame (Lepton)
Lepton has no clipboard service, and SDL3's activity died taking one for
granted: the build now patches SDL's clipboard handler to do without.
The Frame lists an implicit OpenXR layer for Vulkan apps whose library an
app cannot load, which failed xrCreateInstance (and the Android loader
reads no variable to turn it off): the app now carries a pass-through
layer under that library's name. With both, the game runs on the Frame
and its OpenXR session starts (SteamVR, 1728x1728 an eye, 72 Hz).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:26:39 +02:00
Stefan Maier cb72dca40f Merge branch 'merge/upstream-2026-10-04' 2026-10-04 14:00:26 +02:00
Stefan MaierandClaude Opus 5.5 bbea2a2bff The online panel's game by its row past the fixed rows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:47:56 +02:00
Stefan MaierandClaude Opus 5.5 57cdbd8f80 Merge upstream (cybersecurity/halo-ce-universal 193cbf59): server browser, split-screen co-op
Upstream's server browser of public internet games (p2p_lobby.c) and the VR
port's own public list share the brokers: its topics are a table now, and
the VR port's (join codes' and public games') are entries of it; the VR
port's retained publishes carry MQTT 5's empty properties. The VR port's
public toggle lists the game in the server browser too, and the VR online
panel shows the server browser's games (PC, phones, VR) first, joined by
their invites, then its own list's that it has not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:47:29 +02:00
Stefan Maier c5c5a8eaeb Merge branch 'feature/bots-vehicles' 2026-10-04 13:33:07 +02:00
Stefan Maier a3ee36b5ef Merge branch 'feature/vr-hud-fixes' 2026-10-04 13:33:07 +02:00
Stefan Maier 30a1e0f40e Merge branch 'feature/vr-menu-lists' 2026-10-04 13:33:07 +02:00
Stefan Maier 327cf96d6f Merge branch 'feature/vr-options' 2026-10-04 13:33:07 +02:00
Stefan MaierandClaude Opus 5.5 867855d388 Bot drivers see walls and rocks ahead, back off when stuck, go round teammates
Driving a jeep or a scout, a bot looks ahead (farther the faster) along the
way it turns: a wall or a rock there (not ground it can drive up) is passed
on the most open side nearest its way, slower, and boxed in it backs off.
Stuck is now making no way along its heading for a second and a half, which
spinning wheels no longer hide. A teammate in the way is gone round slowly
on the other side instead of waited for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:25:15 +02:00
Stefan MaierandClaude Opus 5.5 4af570bc08 VR: the online games' hint further left and shorter, clear of Y = Create Game
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:13:27 +02:00
Stefan MaierandClaude Opus 5.5 7a1cf79396 VR: teammates' triangles over their heads, a click joins on the profile screen
The triangle the game draws over each teammate goes into the name's slot
over the head (shown in the world there, not on the HUD before the eyes,
where it hung distorted), under the name, or alone when names are off. On
the screen where controllers join by pressing A, a click is the pad's A. On
the screen of games to join, the online games' hint goes into the key bar's
free left and the join status under it, clear of the map's picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:00:41 +02:00
Stefan MaierandClaude Opus 5.5 9e7fd75f57 Bots: a vehicle left stuck is left alone a while; every third bot follows a person
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:41:40 +02:00
Stefan MaierandClaude Opus 5.5 aedf5ff9bc Bot drivers wait for those coming to get in, and brake for teammates
Just in, a bot driver waits up to six seconds while a bot goes for its
vehicle or a person of its team stands near it with a gun's seat free; it
brakes (backing, if fast) for a teammate on foot in its way. A bot takes a
vehicle only when the nearest enemy (or the objective) is a long way off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:39:24 +02:00
MrBruh 193cbf59c7 Split-screen co-op 2026-10-04 22:27:13 +13:00
Stefan MaierandClaude Opus 5.5 fe160e9dbe Bots man turrets while an enemy is about
A turret near a bot (15 world units) is manned while an enemy is within 35
of it, and left once there has been no one to shoot for ten seconds; at it
the bot aims and fires as at a vehicle's gun. The map's vehicles are named
in the log once a game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:23:49 +02:00
MrBruhandClaude Opus 5.5 d578f88bd0 Validate server browser names as player names
The server browser cleans each game's name as a host cleans the names
of the players and machines that join it, and leaves out a game whose
name has nothing left that names it. A host lists a Latin letter with
a mark as its plain letter, as player names are read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 20:53:00 +13:00
MrBruhandClaude Opus 5.5 d3fc997fdc Ready walked-over weapons, swap out the selected one
In multiplayer a second weapon is picked up into the empty slot on
walking over it and readied, as in campaign; a client readies the
weapon the host says it picked up once it has it. A swap drops the
weapon the player selected, not the one still in hand mid-switch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 20:47:06 +13:00
MrBruh a227872ccb Merge pull request #73 from thelinkin3000/fix/unarmed-melee-campaign
Leave an unarmed melee alone where the map has no multiplayer weapons
2026-10-04 20:28:01 +13:00
MrBruh 812ffeea33 Outline the focused server browser row 2026-10-04 20:19:12 +13:00
MrBruh c05864b3fc Leave quit players off the scoreboard 2026-10-04 20:06:28 +13:00
thelinkin3000andClaude Opus 5.5 ce77db8404 Leave an unarmed melee alone where the map has no multiplayer weapons
A player with no weapon, whose biped has no blow of its own, borrows the
melee of the globals' first multiplayer weapon. A campaign map's globals
list no multiplayer weapons, and a10 starts the player unarmed, so a melee
in the cryo tube's tutorial asked for element 0 of an empty block and
halted in tag_groups.c. With no multiplayer weapon there is no blow to
borrow, and the melee does what the game's always did there: nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 06:22:42 +00:00
MrBruh 39350fb81f Enemy names to the motion sensor's range and setting 2026-10-04 18:57:11 +13:00
MrBruh 1a15a171ac No unarmed grenades or melee from vehicle seats 2026-10-04 17:54:34 +13:00
MrBruh 2e3841ff7d Left-align the shotgun's ammo meter 2026-10-04 17:47:55 +13:00
MrBruh 46edf53657 Settings and End Game in the pause menu 2026-10-04 17:20:32 +13:00
MrBruh c3adcfe5bf Deflate the release zips again 2026-10-04 14:22:11 +13:00
MrBruh 3304965653 Lobby check test past the compiler launcher 2026-10-04 13:43:13 +13:00
MrBruh c04765d7f4 Server browser of public internet games 2026-10-04 13:34:23 +13:00
MrBruh 933aac6175 No map weapons option, grenades with no weapon 2026-10-04 10:44:37 +13:00
MrBruh 809408c663 Survive kills and joins in departed players' slots 2026-10-04 10:12:32 +13:00
MrBruh 601a4c1f50 Unload the menu map before a client's next game 2026-10-04 09:51:47 +13:00
MrBruh 133d6a5dc1 Free quit players' slots 2026-10-04 09:07:47 +13:00
Stefan MaierandClaude Opus 5.5 12c97ffa85 Bots take vehicles: drive, man the gun, ride along; and race
A bot whose way is long takes an empty vehicle it can drive near it (a
Warthog, a Ghost, a Scorpion), walking to the driver's door and standing
there until the game offers the seat. Driving, it follows a vehicle's path
(from the vehicle's surface, wider, its steps further apart), turning a
Warthog or a Ghost by facing where it goes and pushing a Scorpion there from
where its gun faces; it runs an enemy down, circles one with a gunner
aboard, fires a Ghost's and a Scorpion's guns, backs off what it is stuck
on, roams between the map's spawn points, and gets out at the objective,
overturned, stuck again and again, or with an enemy close while it stands.
In a game of teams a bot mans the gun of a teammate's vehicle, a person's
first (or rides along with a person), aims and fires it, and gets out once
no one drives. Overturned vehicles near it are turned upright.

In a race a bot heads for the flag it is to touch next
(game_engine_race_next_flag), in a vehicle if it can have one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 16:48:59 +02:00
MrBruh 23b542601f Unarmed melee and infinite grenades for any player count 2026-10-04 03:22:16 +13:00
MrBruh 62b630a261 Remove the flamethrower and gravity rifle 2026-10-04 03:05:59 +13:00
Stefan MaierandClaude Opus 5.5 0e6327cadf The controls page lists switching grenades (B held) on a line of its own
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:41:56 +02:00
Stefan MaierandClaude Opus 5.5 5f82df71f0 VR options: Y on the main menu or the pause menu
A panel of the VR settings one may change while playing (turning, snap
angle, smooth speed, the vehicles' comfort vignette, the Warthog's wheel,
the weapon laser, the crosshair, the motion tracker on the wrist), written
to config.toml, which vr.c reads again as they change; and a page of what
the controllers' buttons do, on foot, in vehicles and in the menus. The
pointer works both, and its hint opens the panel. The menus' screens are
named in the log as they come up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:37:10 +02:00
MrBruh e8e0c2215a Find the port's headers in Windows game units 2026-10-04 02:08:45 +13:00
MrBruh 383355381c Kills to win up to 500 in the PC menus 2026-10-04 02:04:14 +13:00
Milenko 2b0103a0ef Slayer's kills to win up to 500, for big games
The game type editor's kills to win (Slayer and Team Slayer) went up to
50, which a game of many players reaches in moments: it now also offers 75,
100, 150, 200, 250 and 500. The spinner's items are its string list's
(ui.map's 5, 10, 15, 25, 50) and then strings of the port's own, each with
its description; the options list's descriptions count only a spinner's
items of the tag's own, so those of the rows after it are where they were.
A spinner with extra items is a little wider, for three digits.
2026-10-04 01:58:26 +13:00
97 changed files with 10763 additions and 351 deletions

No files matched your search

+1 -4
View File
@@ -242,10 +242,7 @@ jobs:
mkdir release
for build in artifacts/*/; do
name="$(basename "$build")"
# stored, not deflated: the updater in builds up to 57 waits for
# an end of stream the game's zlib 1.1 does not always report,
# and fails on a deflated entry that unpacked correctly
(cd "$build" && zip -0 -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
(cd "$build" && zip -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
done
ls -l release
+125
View File
@@ -0,0 +1,125 @@
# Steam Frame
The same APK as for the Quest runs on the Steam Frame, in Lepton (SteamOS's
Android, a Waydroid fork: Android 11, arm64), with SteamVR's OpenXR runtime.
What differs is found at run time, not by a build of its own.
## The device
- Steam Settings > System > Enable Developer Mode; then start "Lepton
Development" from the library. While the headset sleeps Lepton stops, and
adb with it.
- adb over the network: `adb connect <frame-ip>:5555`; over USB:
`adb forward tcp:5555 tcp:5555` then `adb connect localhost:5555`.
Always name the device (`adb -s <frame-ip>:5555 ...`): a Quest may be
connected as well.
- Install with `adb install -r --no-streaming`. It prints "Lepton post
installation failed due to APP_PATH being unset", but installs. Start the
game with `am start -n com.halo.decomp/.LauncherActivity`.
- Lepton empties `/sdcard/Android/data` whenever it starts again: the game's
data must live elsewhere (stage 2). `/sdcard/Documents` and
`/sdcard/Download` are the SteamOS user's folders and are kept.
## What the port does for it
- SDL3's clipboard handler takes a clipboard service for granted, which
Lepton has not: `tools/android_build.py` patches it to do without.
- SteamVR lists an implicit OpenXR layer for Vulkan apps whose library an
app cannot load: the APK carries a pass-through layer under its name
(`port/android/xr_layer/fdm_injection_stub.c`).
## Stages
| # | Stage | State |
| --- | --- | --- |
| 1 | Base: the fixes above on the current main | done |
| 2 | The game's data where Lepton keeps it | done: `/home/steamos/Documents/HaloCE` |
| 3 | Menus, HUD and text visible (quad layers) | done: their alpha from their colour |
| 4 | The freeze at the cryo bay's calibration | not seen again (2026-10-04) |
| 5 | The Frame's controllers | done: their own profile, the Xbox's layout |
| 6 | Frame rate, resolution | done: 72 Hz steady, multiview on |
| 7 | Multiplayer, voice, cross-play with the Quest | |
| 8 | A button in the Steam library | done: `steam_frame_launch.sh` |
## Starting it on the Frame
A button in the Steam library ("Halo VR", a non-Steam game) runs
`~/Documents/HaloCE/halo-vr.sh` (`port/android/steam_frame_launch.sh`),
which starts the game as Steam starts an Android title of its own:
`lepton waitforexitandrun -- ~/Documents/HaloCE/HaloVR.apk`, in a container
of its own (`steamlaunch-4290000001`) that Lepton installs the APK into
anew whenever the file is newer. Updating the game is replacing that APK.
What the script does that Steam would for an Android title:
- a compat data path (`steamapps/compatdata/4290000001/internal`): after an
install Lepton links the app's data to it, a dangling link without it;
and the shader path, which Lepton reads Steam's shader dump path against;
- a container id of its own: with the shortcut's id, SteamVR's client in the
container stalled in xrCreateInstance;
- a few starts: a new APK's first start only installs it, and a start that
ends within 30 s is tried again (up to three times), unless the game was
quit (it writes `quit_requested.txt`).
Steam's "Exit game" does not reach the game (its process is the container's,
not Steam's): quit it with the VR options' "Quit Halo" (Y in the main or
pause menu, twice). A container ended within 30 s of its start makes
Lepton drop the app's install, so the next start installs it again.
The shortcut is in `userdata/<id>/config/shortcuts.vdf` (Steam reads it when
it starts; appid 3151454348 for this name and path), its pictures in
`userdata/<id>/config/grid/` (from `port/android/steam_frame_grid/`). From
the Mac, over USB the Frame's SteamOS is an adb device of its own, `frame`,
beside Lepton's `<frame-ip>:5555`.
## The game's data
Lepton uninstalls the app and empties its folders whenever it starts again.
The game's data lives in the SteamOS user's Documents instead: when
`/home/steamos/Documents/HaloCE` holds `maps/ui.map` and is writable, the
launcher and the game take it as their data folder (config.toml, saves and
debug.txt go there too). Once:
adb -s <frame-ip>:5555 shell mkdir -p /home/steamos/Documents/HaloCE/maps
adb -s <frame-ip>:5555 push assets/maps/. /home/steamos/Documents/HaloCE/maps/
adb -s <frame-ip>:5555 shell chmod -R 777 /home/steamos/Documents/HaloCE
After a Lepton restart only the APK is installed again. (Use the path under
/home, not /sdcard/Documents: Android 11's scoped storage stands between the
app and the latter.)
## The controllers
SteamVR offers the Frame's controllers as themselves
(`XR_VALVE_frame_controller_interaction`,
`/interaction_profiles/valve/frame_controller_valve`; Valve's "Steam Frame
Input" page). Without bindings for it, SteamVR maps the Quest's onto them,
and the left Touch controller's X and Y land on the D-pad. With them the
game gets the Xbox's layout, as the right controller has it: A jump, B
flashlight (held: switch grenades), X action and reload, Y switch weapon,
menu pause; the left one's View is Back (scores), its D-pad the D-pad, the
bumpers switch grenades (left) and the flashlight (right). The VR options'
page of buttons shows this layout on the Frame.
## Findings
- 2026-10-02: runs flat and in VR (GLES 3.2 by Mesa zink on Turnip, eyes
1728x1728, 72 Hz, XR_FB_display_refresh_rate absent). Quad layers
(menus, HUD, instructions) are not shown; the projection layer is. Froze
at the cryo bay's look calibration; its log was lost to Lepton's wipe.
- 2026-10-04: SteamVR draws a quad's texels by their alpha alone; the
game's menus and HUD have none in their additive parts, so nothing showed.
On SteamVR (by the runtime's name) the overlays' alpha now comes from their
colour (host_xr.c; `HALO_XR_QUAD_ALPHA=colour|keep` overrides). With it,
menus, HUD and the campaign ran well (the user: "lief 1a"); no freeze.
- 2026-10-04: no online games found: the public MQTT brokers had banned the
home connection for a while (too many connections from tests that day);
broker.emqx.io came back within minutes. Lepton's network is the Frame's
own (same IP), DNS works.
- 2026-10-04: the refresh rate is SteamOS's setting; the runtime offers
only that one (the game asks for the highest at or below
`vr.refresh_rate`, and logs the rates offered). At 72 Hz the game holds
72 fps (about 11.6 ms a frame with multiview, eyes 7.6 ms; without it
eyes about 9 ms). At 90 Hz it cannot (11.1 ms a frame), and SteamVR's
making up of the missed frames showed as a doubled picture when turning
the head. Keep the Frame at 72 Hz, or lower `vr.resolution_scale` (about
0.85) for 90.
@@ -35,6 +35,7 @@ import java.nio.channels.FileChannel;
*/
public class LauncherActivity extends Activity {
private static final int PICK_IMAGE = 1;
private static final String SHARED_DATA_ROOT = "/home/steamos/Documents/HaloCE";
private File dataRoot;
private TextView status;
@@ -46,6 +47,12 @@ public class LauncherActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dataRoot = getExternalFilesDir(null);
// the Steam Frame: Lepton empties the app's own folders whenever it
// starts again; the game's data lives in the SteamOS user's
// Documents instead, when it is there (port/android/STEAM_FRAME.md;
// the same choice as host_main.c's)
if (new File(SHARED_DATA_ROOT, "maps/ui.map").isFile() && new File(SHARED_DATA_ROOT).canWrite())
dataRoot = new File(SHARED_DATA_ROOT);
// created by the app, so that files pushed into it with adb stay
// readable (a directory adb creates there belongs to the shell user)
if (dataRoot != null)
+8
View File
@@ -90,6 +90,8 @@ int host_errno(void)
static char data_root[512];
static char save_root[512];
/* where a device that empties the app's folders keeps the game's data */
#define SHARED_DATA_ROOT "/home/steamos/Documents/HaloCE"
void host_android_path(int which, char *buffer, uint32_t size)
{
@@ -269,6 +271,12 @@ static void *game_main(void *unused)
external = SDL_GetAndroidExternalStoragePath();
if (!external)
host_fatal("Android storage is unavailable: %s", SDL_GetError());
/* the Steam Frame: Lepton empties the app's own folders whenever it
starts again; the game's data lives in the SteamOS user's Documents
instead, when it is there (port/android/STEAM_FRAME.md; the same choice
as LauncherActivity's) */
if (directory_has_maps(SHARED_DATA_ROOT) && access(SHARED_DATA_ROOT, W_OK) == 0)
external = SHARED_DATA_ROOT;
snprintf(data_root, sizeof(data_root), "%s", external);
snprintf(save_root, sizeof(save_root), "%s/save", external);
/* readable by adb (the shell user), for managing saves */
+163 -1
View File
@@ -102,6 +102,14 @@ static struct
XrAction button_b; /* B on the right controller, Y on the left */
XrAction menu;
XrAction thumbrest;
/* the Steam Frame's controllers' own (XR_VALVE_frame_controller_interaction) */
int has_frame_controller;
int frame_layout;
XrAction button_x;
XrAction button_y;
XrAction view;
XrAction bumper;
XrAction dpad[4];
XrAction haptic;
struct xr_hand_actions hands[HALO_XR_HAND_COUNT];
@@ -122,6 +130,11 @@ static struct
GLint rect_uniform;
GLint cut_disc_uniform;
GLint vignette_uniform;
GLint alpha_from_colour_uniform;
/* the runtime draws a quad's texels by their alpha alone, ignoring the
colour of those with none (SteamVR on the Steam Frame): the game's
overlays, whose additive parts have none, get alpha from their colour */
int quad_alpha_from_colour;
GLint tangents_uniform;
float vignette;
/* the same copy from a layer of a texture array (the eyes drawn at
@@ -223,6 +236,7 @@ static const char copy_fragment_shader[] =
"precision highp float;\n"
"uniform sampler2D source;\n"
"uniform float cut_disc;\n"
"uniform float alpha_from_colour; /* 1: as opaque as it is bright (a quad's, SteamVR) */\n"
"uniform float vignette; /* 0..1: the eye images darken from their edges */\n"
"uniform vec4 tangents; /* the eye's edges: left, right, down, up */\n"
"in vec2 coordinate;\n"
@@ -236,6 +250,8 @@ static const char copy_fragment_shader[] =
"{\n"
" vec4 texel = texture(source, coordinate);\n"
" color = vec4(to_linear(texel.rgb), texel.a);\n"
" if (alpha_from_colour > 0.5)\n"
" color.a = max(color.a, max(color.r, max(color.g, color.b)));\n"
" if (cut_disc > 0.5)\n"
" {\n"
" float inside = 1.0 - smoothstep(0.95, 1.0, length(disc));\n"
@@ -326,6 +342,7 @@ static int copy_initialize(void)
xr.rect_uniform = glGetUniformLocation(xr.program, "rect");
xr.cut_disc_uniform = glGetUniformLocation(xr.program, "cut_disc");
xr.vignette_uniform = glGetUniformLocation(xr.program, "vignette");
xr.alpha_from_colour_uniform = glGetUniformLocation(xr.program, "alpha_from_colour");
xr.tangents_uniform = glGetUniformLocation(xr.program, "tangents");
xr.copy_array_layer = -1;
{
@@ -580,6 +597,14 @@ static int actions_initialize(void)
xr.button_b = action_create("button_upper", "B / Y", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.menu = action_create("menu", "Menu", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.thumbrest = action_create("thumbrest", "Thumb rest", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.button_x = action_create("button_x", "X", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.button_y = action_create("button_y", "Y", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.view = action_create("view", "View", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.bumper = action_create("bumper", "Bumper", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.dpad[0] = action_create("dpad_up", "D-pad up", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.dpad[1] = action_create("dpad_down", "D-pad down", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.dpad[2] = action_create("dpad_left", "D-pad left", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.dpad[3] = action_create("dpad_right", "D-pad right", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.haptic = action_create("haptic", "Rumble", XR_ACTION_TYPE_VIBRATION_OUTPUT);
{
@@ -644,6 +669,44 @@ static int actions_initialize(void)
{ &xr.haptic, "/user/hand/right/output/haptic" },
};
/* the Steam Frame's controllers, as themselves: the right one has
A, B, X, Y and a menu button, the left one a D-pad and a View button
(port/android/STEAM_FRAME.md) */
static const struct binding frame_controller[] =
{
{ &xr.grip_pose, "/user/hand/left/input/grip/pose" },
{ &xr.grip_pose, "/user/hand/right/input/grip/pose" },
{ &xr.aim_pose, "/user/hand/left/input/aim/pose" },
{ &xr.aim_pose, "/user/hand/right/input/aim/pose" },
{ &xr.trigger, "/user/hand/left/input/trigger/value" },
{ &xr.trigger, "/user/hand/right/input/trigger/value" },
{ &xr.squeeze, "/user/hand/left/input/squeeze/value" },
{ &xr.squeeze, "/user/hand/right/input/squeeze/value" },
{ &xr.thumbstick, "/user/hand/left/input/thumbstick" },
{ &xr.thumbstick, "/user/hand/right/input/thumbstick" },
{ &xr.thumbstick_click, "/user/hand/left/input/thumbstick/click" },
{ &xr.thumbstick_click, "/user/hand/right/input/thumbstick/click" },
{ &xr.button_a, "/user/hand/right/input/a/click" },
{ &xr.button_b, "/user/hand/right/input/b/click" },
{ &xr.button_x, "/user/hand/right/input/x/click" },
{ &xr.button_y, "/user/hand/right/input/y/click" },
{ &xr.menu, "/user/hand/right/input/menu/click" },
{ &xr.view, "/user/hand/left/input/view/click" },
{ &xr.bumper, "/user/hand/left/input/bumper/click" },
{ &xr.bumper, "/user/hand/right/input/bumper/click" },
{ &xr.dpad[0], "/user/hand/left/input/dpad_up/click" },
{ &xr.dpad[1], "/user/hand/left/input/dpad_down/click" },
{ &xr.dpad[2], "/user/hand/left/input/dpad_left/click" },
{ &xr.dpad[3], "/user/hand/left/input/dpad_right/click" },
{ &xr.haptic, "/user/hand/left/output/haptic" },
{ &xr.haptic, "/user/hand/right/output/haptic" },
};
if (xr.has_frame_controller)
{
suggest("/interaction_profiles/valve/frame_controller_valve", frame_controller,
sizeof(frame_controller) / sizeof(frame_controller[0]));
}
suggest("/interaction_profiles/oculus/touch_controller", touch, sizeof(touch) / sizeof(touch[0]));
suggest("/interaction_profiles/valve/index_controller", index, sizeof(index) / sizeof(index[0]));
suggest("/interaction_profiles/khr/simple_controller", simple, sizeof(simple) / sizeof(simple[0]));
@@ -713,6 +776,30 @@ static void hands_read(struct halo_xr_frame *frame)
sync.activeActionSets = &active;
if (!XR_SUCCEEDED(xrSyncActions(xr.session, &sync)))
return;
{
/* (the controllers' profile the runtime chose, in the log as it
changes) */
static XrPath logged[HALO_XR_HAND_COUNT];
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
{
XrInteractionProfileState state = { XR_TYPE_INTERACTION_PROFILE_STATE };
if (XR_SUCCEEDED(xrGetCurrentInteractionProfile(xr.session, xr.hands[hand].path, &state)) &&
state.interactionProfile != logged[hand])
{
char name[XR_MAX_PATH_LENGTH] = "none";
uint32_t length = 0;
logged[hand] = state.interactionProfile;
if (state.interactionProfile != XR_NULL_PATH)
xrPathToString(xr.instance, state.interactionProfile, sizeof(name), &length, name);
if (hand == HALO_XR_HAND_RIGHT)
xr.frame_layout = strstr(name, "frame_controller") != NULL;
host_logf(HOST_LOG_INFO, "xr: the %s hand's controller: %s", hand ? "right" : "left", name);
}
}
}
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
{
struct halo_xr_hand *out = &frame->hands[hand];
@@ -757,7 +844,31 @@ static void hands_read(struct halo_xr_frame *frame)
(action_boolean(xr.menu, hand) ? HALO_XR_BUTTON_MENU : 0) |
(action_boolean(xr.thumbrest, hand) ? HALO_XR_BUTTON_THUMBREST : 0) |
(out->trigger > 0.5f ? HALO_XR_BUTTON_TRIGGER : 0) |
(out->squeeze > 0.5f ? HALO_XR_BUTTON_SQUEEZE : 0);
(out->squeeze > 0.5f ? HALO_XR_BUTTON_SQUEEZE : 0) |
(action_boolean(xr.view, hand) ? HALO_XR_BUTTON_VIEW : 0) |
(action_boolean(xr.bumper, hand) ? HALO_XR_BUTTON_BUMPER : 0) |
(action_boolean(xr.dpad[0], hand) ? HALO_XR_BUTTON_DPAD_UP : 0) |
(action_boolean(xr.dpad[1], hand) ? HALO_XR_BUTTON_DPAD_DOWN : 0) |
(action_boolean(xr.dpad[2], hand) ? HALO_XR_BUTTON_DPAD_LEFT : 0) |
(action_boolean(xr.dpad[3], hand) ? HALO_XR_BUTTON_DPAD_RIGHT : 0);
}
frame->controller_layout = xr.frame_layout ? HALO_XR_LAYOUT_FRAME : HALO_XR_LAYOUT_TOUCH;
if (xr.frame_layout)
{
/* (the Frame's right X, Y and menu button: the game's X, Y and Start,
which the Quest has on the left) */
struct halo_xr_hand *left = &frame->hands[HALO_XR_HAND_LEFT];
struct halo_xr_hand *right = &frame->hands[HALO_XR_HAND_RIGHT];
if (action_boolean(xr.button_x, HALO_XR_HAND_RIGHT))
left->buttons |= HALO_XR_BUTTON_A;
if (action_boolean(xr.button_y, HALO_XR_HAND_RIGHT))
left->buttons |= HALO_XR_BUTTON_B;
if (right->buttons & HALO_XR_BUTTON_MENU)
{
right->buttons &= ~HALO_XR_BUTTON_MENU;
left->buttons |= HALO_XR_BUTTON_MENU;
}
}
}
@@ -774,6 +885,13 @@ static void refresh_rate_choose(float wanted)
{
return;
}
{
char line[128] = "";
for (index = 0; index < count; index++)
snprintf(line + strlen(line), sizeof(line) - strlen(line), " %.0f", rates[index]);
host_logf(HOST_LOG_INFO, "xr: refresh rates offered:%s Hz", line);
}
/* the highest rate not above the one wanted (0: the highest) */
for (index = 0; index < count; index++)
{
@@ -895,6 +1013,12 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
/* the loader and the runtime keep the activity */
activity = (*environment)->NewGlobalRef(environment, activity);
/* (The Steam Frame lists an implicit API layer of its own for Vulkan
apps, XR_APILAYER_VALVE_fdm_injection, whose library an app cannot load
from its namespace, failing xrCreateInstance; the Android loader reads
no variable to turn it off, so the app carries a pass-through layer
under that library's name: port/android/xr_layer/fdm_injection_stub.c) */
if (!XR_SUCCEEDED(xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR",
(PFN_xrVoidFunction *)&initialize_loader)) || !initialize_loader)
{
@@ -922,6 +1046,23 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
extensions[index].next = NULL;
}
xrEnumerateInstanceExtensionProperties(NULL, extension_count, &extension_count, extensions);
{
/* (the runtime's extensions, in the log: what a headset offers) */
char line[512] = "";
for (index = 0; index < extension_count; index++)
{
if (strlen(line) + strlen(extensions[index].extensionName) + 2 >= sizeof(line))
{
host_logf(HOST_LOG_INFO, "xr: extensions %s", line);
line[0] = 0;
}
strcat(line, " ");
strcat(line, extensions[index].extensionName);
}
if (line[0])
host_logf(HOST_LOG_INFO, "xr: extensions %s", line);
}
if (!has_extension(extensions, extension_count, XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME))
{
host_logf(HOST_LOG_ERROR, "xr: the runtime has no OpenGL ES support");
@@ -933,6 +1074,9 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
xr.has_performance_settings = has_extension(extensions, extension_count, XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME);
if (xr.has_performance_settings)
enabled[enabled_count++] = XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME;
xr.has_frame_controller = has_extension(extensions, extension_count, "XR_VALVE_frame_controller_interaction");
if (xr.has_frame_controller)
enabled[enabled_count++] = "XR_VALVE_frame_controller_interaction";
xr.has_refresh_rate = has_extension(extensions, extension_count, XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME);
if (xr.has_refresh_rate)
enabled[enabled_count++] = XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME;
@@ -960,6 +1104,22 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
}
xrGetSystemProperties(xr.instance, xr.system, &properties);
strncpy(xr.system_name, properties.systemName, sizeof(xr.system_name) - 1);
{
/* (SteamVR: the quads' alpha from their colour; HALO_XR_QUAD_ALPHA
"colour" or "keep" says otherwise, for trying) */
XrInstanceProperties instance = { XR_TYPE_INSTANCE_PROPERTIES };
const char *choice = getenv("HALO_XR_QUAD_ALPHA");
if (XR_SUCCEEDED(xrGetInstanceProperties(xr.instance, &instance)))
{
xr.quad_alpha_from_colour = strstr(instance.runtimeName, "SteamVR") != NULL;
host_logf(HOST_LOG_INFO, "xr: runtime %s", instance.runtimeName);
}
if (choice)
xr.quad_alpha_from_colour = !strcmp(choice, "colour");
if (xr.quad_alpha_from_colour)
host_logf(HOST_LOG_INFO, "xr: the overlays' alpha from their colour");
}
views[0].type = views[1].type = XR_TYPE_VIEW_CONFIGURATION_VIEW;
views[0].next = views[1].next = NULL;
@@ -1316,6 +1476,8 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
glUniform1i(xr.source_uniform, 0);
glUniform4f(xr.rect_uniform, left, top, right, bottom);
glUniform1f(xr.cut_disc_uniform, clear_alpha == HALO_XR_COPY_DISC ? 1.0f : 0.0f);
glUniform1f(xr.alpha_from_colour_uniform, xr.quad_alpha_from_colour && clear_alpha == HALO_XR_COPY_ALPHA &&
layer != HALO_XR_LAYER_EYE_LEFT && layer != HALO_XR_LAYER_EYE_RIGHT ? 1.0f : 0.0f);
if ((layer == HALO_XR_LAYER_EYE_LEFT || layer == HALO_XR_LAYER_EYE_RIGHT) && xr.vignette > 0.001f)
{
const XrFovf *fov = &xr.views[layer - HALO_XR_LAYER_EYE_LEFT].fov;
+18
View File
@@ -53,6 +53,23 @@ enum
HALO_XR_BUTTON_TRIGGER = 1 << 4, /* trigger past its click point */
HALO_XR_BUTTON_SQUEEZE = 1 << 5, /* grip past its click point */
HALO_XR_BUTTON_THUMBREST = 1 << 6, /* thumb resting (touch) */
/* the Steam Frame's controllers' (HALO_XR_LAYOUT_FRAME): the left one's
View button and D-pad, either one's bumper (their right one's X, Y and
menu button come as the left hand's A, B and MENU: the game's X, Y and
Start, as on the Quest) */
HALO_XR_BUTTON_VIEW = 1 << 7,
HALO_XR_BUTTON_BUMPER = 1 << 8,
HALO_XR_BUTTON_DPAD_UP = 1 << 9,
HALO_XR_BUTTON_DPAD_DOWN = 1 << 10,
HALO_XR_BUTTON_DPAD_LEFT = 1 << 11,
HALO_XR_BUTTON_DPAD_RIGHT = 1 << 12,
};
/* the controllers' layout, for what the game tells the player */
enum
{
HALO_XR_LAYOUT_TOUCH = 0, /* the Quest's: X, Y and menu on the left */
HALO_XR_LAYOUT_FRAME = 1, /* the Steam Frame's: A, B, X, Y and menu on the right, a D-pad on the left */
};
/* halo_xr_hand.flags, halo_xr_view.flags, halo_xr_frame.head_flags */
@@ -112,6 +129,7 @@ struct halo_xr_frame
struct halo_xr_pose head;
struct halo_xr_view views[HALO_XR_EYE_COUNT];
struct halo_xr_hand hands[HALO_XR_HAND_COUNT];
int controller_layout; /* HALO_XR_LAYOUT_* */
};
/* halo_xr_submit.overlay_mode */
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
# Halo VR on the Steam Frame: Lepton runs the APK in a container of its own,
# and installs it anew whenever the APK is newer than the installed one
# (port/android/STEAM_FRAME.md). The game's data: ~/Documents/HaloCE.
# (a container of its own, not the shortcut's id: with that, SteamVR's
# client in the container waited on xrCreateInstance)
export SteamAppId="${HALO_LEPTON_APP_ID:-4290000001}"
# (what Steam gives an Android title of its own, and a non-Steam game not:
# the app's data in the container links to the compat data path's
# internal/, a dangling link without it; and the shader paths: Lepton reads
# the dump path against the other)
export STEAM_COMPAT_DATA_PATH="$HOME/.local/share/Steam/steamapps/compatdata/$SteamAppId"
export STEAM_COMPAT_SHADER_PATH="$HOME/.local/share/Steam/steamapps/shadercache/$SteamAppId"
mkdir -p "$STEAM_COMPAT_DATA_PATH/internal" "$STEAM_COMPAT_SHADER_PATH"
unset STEAM_FOSSILIZE_DUMP_PATH LD_PRELOAD
LEPTON="$HOME/.local/share/Steam/steamapps/common/Lepton/lepton"
APK="$HOME/Documents/HaloCE/HaloVR.apk"
LOG="$HOME/Documents/HaloCE/launcher.log"
# (written by the game when the player quits it: no start again)
QUIT="$HOME/Documents/HaloCE/quit_requested.txt"
rm -f "$QUIT"
CONTEXT="steamlaunch-$SteamAppId"
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
log "start (pid $$, parent $PPID)"
# Steam's "Exit game" ends the game's processes as Steam knows them (this
# script, under Steam's reaper), not the container, which podman runs: a
# signal here is passed on to Lepton, and a watcher of its own takes the
# container down once this script is gone (killed)
stopping=0
child=0
stop()
{
log "signal $1"
stopping=1
[ "$child" -gt 0 ] && kill -TERM "$child" 2>/dev/null
}
trap 'stop TERM' TERM
trap 'stop INT' INT
trap 'stop HUP' HUP
script=$$
setsid bash -c '
while kill -0 '"$script"' 2>/dev/null; do sleep 1; done
echo "$(date "+%F %T") watcher: the game was ended, so its container" >> "'"$LOG"'"
"'"$LEPTON"'" kill "'"$CONTEXT"'" >/dev/null 2>&1
' </dev/null >/dev/null 2>&1 &
run()
{
"$LEPTON" waitforexitandrun -- "$APK" &
child=$!
# (wait returns early when a signal comes: until Lepton is gone)
while kill -0 "$child" 2>/dev/null; do
wait "$child"
done
child=0
}
# (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)
for attempt in 1 2 3; do
started=$SECONDS
run
if (( stopping || SECONDS - started >= 30 )) || [ -e "$QUIT" ]; then
break
fi
log "ended within $((SECONDS - started)) s: again"
sleep 5
done
rm -f "$QUIT"
log "end"
@@ -0,0 +1,62 @@
/*
FDM_INJECTION_STUB.C
An OpenXR API layer that does nothing, named as the Steam Frame's
XR_APILAYER_VALVE_fdm_injection names its library
(libVkLayer_VALVE_fdm_injection.so). The Frame lists that layer as implicit
for every OpenXR app (/vendor/etc/openxr/1/api_layers/implicit.d), but an
Android app (in Lepton) cannot load the system's library from its own
namespace, and the Khronos loader then fails xrCreateInstance altogether;
nor does the Android loader read the variable that would turn the layer
off. With this one in the app's libraries the loader finds a layer of that
name, which hands every call on to the next. (The real layer adds fragment
density maps to Vulkan apps; this game draws with OpenGL ES.) On a Quest no
manifest names it and it is never loaded.
*/
#include <openxr/openxr.h>
#include <openxr/openxr_loader_negotiation.h>
#include <stddef.h>
static PFN_xrGetInstanceProcAddr next_get_instance_proc_addr;
static XRAPI_ATTR XrResult XRAPI_CALL layer_get_instance_proc_addr(XrInstance instance, const char *name,
PFN_xrVoidFunction *function)
{
if (!next_get_instance_proc_addr)
return XR_ERROR_HANDLE_INVALID;
return next_get_instance_proc_addr(instance, name, function);
}
static XRAPI_ATTR XrResult XRAPI_CALL layer_create_api_layer_instance(const XrInstanceCreateInfo *info,
const XrApiLayerCreateInfo *layer_info, XrInstance *instance)
{
XrApiLayerCreateInfo next_layer_info;
if (!layer_info || !layer_info->nextInfo)
return XR_ERROR_INITIALIZATION_FAILED;
next_get_instance_proc_addr = layer_info->nextInfo->nextGetInstanceProcAddr;
next_layer_info = *layer_info;
next_layer_info.nextInfo = layer_info->nextInfo->next;
return layer_info->nextInfo->nextCreateApiLayerInstance(info, &next_layer_info, instance);
}
__attribute__((visibility("default")))
XRAPI_ATTR XrResult XRAPI_CALL xrNegotiateLoaderApiLayerInterface(const XrNegotiateLoaderInfo *loader_info,
const char *layer_name, XrNegotiateApiLayerRequest *request)
{
(void)layer_name;
if (!loader_info || !request || loader_info->structType != XR_LOADER_INTERFACE_STRUCT_LOADER_INFO ||
request->structType != XR_LOADER_INTERFACE_STRUCT_API_LAYER_REQUEST ||
loader_info->minInterfaceVersion > XR_CURRENT_LOADER_API_LAYER_VERSION ||
loader_info->maxInterfaceVersion < XR_CURRENT_LOADER_API_LAYER_VERSION)
{
return XR_ERROR_INITIALIZATION_FAILED;
}
request->layerInterfaceVersion = XR_CURRENT_LOADER_API_LAYER_VERSION;
request->layerApiVersion = loader_info->maxApiVersion < XR_CURRENT_API_VERSION ? loader_info->maxApiVersion :
XR_CURRENT_API_VERSION;
request->getInstanceProcAddr = layer_get_instance_proc_addr;
request->createApiLayerInstance = layer_create_api_layer_instance;
return XR_SUCCESS;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 KiB

After

Width:  |  Height:  |  Size: 369 KiB

+2 -2
View File
@@ -314,7 +314,7 @@
"svg": "hud_xbox_order__bitmaps__combined__hud_ammo_meters.svg",
"source_scale": 4,
"source": [
-34,
-2,
108
],
"clip": [
@@ -380,7 +380,7 @@
"svg": "hud_xbox_order__bitmaps__combined__hud_ammo_meters.svg",
"source_scale": 4,
"source": [
-25,
-5,
312
],
"clip": [
+1 -1
View File
@@ -51,7 +51,7 @@ until it is redrawn.
| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` |
| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` |
13 of 147 frames are placeholders.
13 of 155 frames are placeholders.
The PC version's `ui\gamespy` and `ui\ticker` fonts are drawn with `ui\small_ui`, which
the Xbox's map has.
+3 -4
View File
@@ -10,11 +10,10 @@ do (save a setting, join a game) does not happen; the screens open and close as
| Function | Uses | |
| --- | --- | --- |
| `common button bar update` | 24 | PC function |
| `common button bar update` | 25 | PC function |
| `direct ip connect init` | 1 | PC function |
| `direct ip connect update` | 1 | PC function |
| `direct ip edit field` | 4 | PC function |
| `gamespy screen dispose` | 1 | PC function |
| `gamespy screen update` | 1 | PC function |
| `gamespy select button` | 8 | PC function |
| `gamespy select header` | 22 | PC function |
@@ -22,7 +21,7 @@ do (save a setting, join a game) does not happen; the screens open and close as
| `gamespy update filter settings` | 2 | PC function |
| `gt edit list update` | 1 | PC function |
| `gt select list update` | 1 | PC function |
| `mouse spinner 1wide click` | 64 | PC function |
| `mouse spinner 1wide click` | 66 | PC function |
| `mp map list update` | 1 | PC function |
| `mp prof init teamplay options` | 1 | PC function |
| `mp prof init vehicle options` | 1 | PC function |
@@ -34,4 +33,4 @@ do (save a setting, join a game) does not happen; the screens open and close as
| `ss edit server name` | 2 | PC function |
| `ss start game` | 2 | PC function |
Wired: `campaign menu continue`, `campaign menu init`, `controls back handler`, `controls begin binding`, `controls screen change set`, `controls screen defaults`, `controls screen init`, `controls update menu`, `difficulty item select`, `direct ip connect go`, `emit custom activation event`, `gamespy back handler`, `gamespy dismiss error`, `gamespy dismiss filters`, `gamespy screen init`, `load game list update`, `load game menu activated`, `load game menu delete finish`, `load game menu delete request`, `load game menu dispose`, `load game menu init`, `main menu quit game`, `mouse emit accept event`, `mouse emit back event`, `mouse emit x event`, `mp type set mode`, `profile manager select`, `profile set edit begin`, `single prev cl item activated`, `solo map list update`.
Wired: `campaign menu continue`, `campaign menu init`, `controls back handler`, `controls begin binding`, `controls screen change set`, `controls screen defaults`, `controls screen init`, `controls update menu`, `difficulty item select`, `direct ip connect go`, `emit custom activation event`, `gamespy back handler`, `gamespy dismiss error`, `gamespy dismiss filters`, `gamespy screen dispose`, `gamespy screen init`, `load game list update`, `load game menu activated`, `load game menu delete finish`, `load game menu delete request`, `load game menu dispose`, `load game menu init`, `main menu quit game`, `mouse emit accept event`, `mouse emit back event`, `mouse emit x event`, `mp type set mode`, `profile manager select`, `profile set edit begin`, `single prev cl item activated`, `solo map list update`.
+18
View File
@@ -309,6 +309,9 @@
<bitmap name="main_menu/solo_level_select/header_load_level">
<frame png="ce/shell/main_menu/solo_level_select/header_load_level__0.png" width="512" height="64"/>
</bitmap>
<bitmap name="main_menu/multiplayer_type_select/coop/header_player_2">
<frame png="ce/port/main_menu/multiplayer_type_select/coop/header_player_2.png" width="512" height="64"/>
</bitmap>
<bitmap name="main_menu/multiplayer_type_select/join_game/header_direct_link">
<frame png="ce/port/main_menu/multiplayer_type_select/join_game/header_direct_link.png" width="512" height="64"/>
</bitmap>
@@ -318,4 +321,19 @@
<bitmap name="main_menu/multiplayer_type_select/lobby/header_lobby">
<frame png="ce/port/main_menu/multiplayer_type_select/lobby/header_lobby.png" width="512" height="64"/>
</bitmap>
<bitmap name="pause/pausebox_left">
<frame png="ce/port/pause/pausebox_left__0.png" width="16" height="256"/>
<frame png="ce/port/pause/pausebox_left__1.png" width="16" height="256"/>
</bitmap>
<bitmap name="pause/pausebox_center">
<frame png="ce/port/pause/pausebox_center__0.png" width="4" height="256"/>
<frame png="ce/port/pause/pausebox_center__1.png" width="4" height="256"/>
</bitmap>
<bitmap name="pause/pausebox_right">
<frame png="ce/port/pause/pausebox_right__0.png" width="16" height="256"/>
<frame png="ce/port/pause/pausebox_right__1.png" width="16" height="256"/>
</bitmap>
<bitmap name="blank">
<frame png="ce/port/blank__0.png" width="4" height="4"/>
</bitmap>
</menus>
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The port's multiplayer screens, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/multiplayer_type_select/multiplayer_type_coop_item" type="text" left="51" width="232" height="32" bitmap="bitmaps/list_item_bkd" string_list="main_menu/multiplayer_type_select/multiplayer_options" string_index="7" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="5">
<on event="a" run="port coop begin" open="main_menu/multiplayer_type_select/coop/player_2_profile_screen" branch="true"/>
<on event="start" run="port coop begin" open="main_menu/multiplayer_type_select/coop/player_2_profile_screen" branch="true"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/player_2_profile_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
<child widget="main_menu/new_select/sel_list_desc_bkd"/>
<child widget="main_menu/multiplayer_type_select/coop/player_2_profile_list"/>
<child widget="main_menu/multiplayer_type_select/coop/header_player_2"/>
<child widget="main_menu/multiplayer_type_select/coop/player_2_help" x="20" y="416"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/header_player_2" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/coop/header_player_2">
</widget>
<widget name="main_menu/multiplayer_type_select/coop/player_2_profile_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/profile_manager/player_profile_extended_desc">
<data input="3wide player profile list update"/>
<on event="created" run="port coop player 2 list initialize"/>
<on event="deleted" run="player profile list dispose"/>
<on event="custom_activation" run="port coop player 2" open="main_menu/solo_level_select/solo_level_select_screen" branch="true"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_0" x="20" y="73"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_1" x="20" y="103"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_2" x="20" y="133"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_3" x="20" y="163"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_4" x="20" y="193"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_5" x="20" y="223"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_6" x="20" y="253"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_7" x="20" y="283"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_8" x="20" y="313"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_9" x="20" y="343"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_10" x="20" y="373"/>
<child widget="main_menu/multiplayer_type_select/coop/player_2_button_bar" y="414"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_0" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd_top" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_1" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_2" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_3" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_4" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_5" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_6" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_7" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_8" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_9" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_10" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
<on event="start" run="single prev cl item activated"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/coop/player_2_help" type="text" width="350" height="24" text="Player 2: choose with your controller." font="ui\small_ui" color="#FF2896FF" text_y="5" text_flags="no_focus_test">
</widget>
<widget name="main_menu/multiplayer_type_select/coop/player_2_button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<data input="common button bar update"/>
<child widget="main_menu/profile_manager/profile_manager_button_ok" x="380" y="1"/>
<child widget="common_button_back" x="510" y="1"/>
</widget>
</menus>
@@ -39,7 +39,7 @@
<child widget="main_menu/multiplayer_type_select/join_game/ticker_rules_info" x="80"/>
</widget>
<widget name="main_menu/multiplayer_type_select/join_game/join_game_button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<child widget="main_menu/multiplayer_type_select/join_game/join_game_button_update" y="1"/>
<child widget="main_menu/multiplayer_type_select/join_game/join_game_button_refresh" y="1"/>
<child widget="main_menu/multiplayer_type_select/join_game/button_clipboard" x="128" y="1"/>
<child widget="main_menu/multiplayer_type_select/join_game/join_game_button_filters" x="256" y="1"/>
<child widget="main_menu/multiplayer_type_select/join_game/join_game_button_join" x="384" y="1"/>
@@ -9,20 +9,21 @@
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/header_server_settings" controller="1" left="35" top="11" width="605" height="59" bitmap="main_menu/multiplayer_type_select/server_settings/header_server_settings">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/help" type="text" controller="1" left="68" top="350" width="482" height="60" string_list="main_menu/multiplayer_type_select/server_settings/help_strings" font="ui\large_ui" color="#FFFFFFFF">
<widget name="main_menu/multiplayer_type_select/server_settings/help" type="text" controller="1" left="68" top="358" width="482" height="60" string_list="main_menu/multiplayer_type_select/server_settings/help_strings" font="ui\large_ui" color="#FFFFFFFF">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/multiplayer_type_select/server_settings/help">
<data input="server settings update"/>
<on event="created" run="server settings init"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_server_name" x="54" y="73"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_max_players" x="54" y="103"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_invite" x="54" y="133"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_game_type" x="54" y="163"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_player_options" x="54" y="193"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_item_options" x="54" y="223"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_vehicle_options" x="54" y="253"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_indicator_options" x="54" y="283"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_team_options" x="54" y="313"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_max_players" x="54" y="101"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_invite" x="54" y="129"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_listing" x="54" y="157"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_game_type" x="54" y="185"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_player_options" x="54" y="213"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_item_options" x="54" y="241"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_vehicle_options" x="54" y="269"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_indicator_options" x="54" y="297"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_team_options" x="54" y="325"/>
<child widget="main_menu/multiplayer_type_select/server_settings/button_bar" y="414"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
@@ -64,6 +65,15 @@
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/invite_value" type="text" controller="1" width="280" height="22" font="ui\small_ui" color="#FF2896FF" align="center" text_y="5" text_flags="no_focus_test">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_listing" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/listing_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/listing_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/listing_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="9" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/listing_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="PUBLIC|PRIVATE" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_game_type" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<on event="a" run="port setup edit" open="main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/gametype_select_screen"/>
<on event="start" run="port setup edit" open="main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/gametype_select_screen"/>
@@ -148,6 +158,7 @@
<string text="VEHICLE OPTIONS:"/>
<string text="INDICATOR OPTIONS:"/>
<string text="TEAMPLAY OPTIONS:"/>
<string text="LISTING:"/>
</strings>
<strings name="main_menu/multiplayer_type_select/server_settings/help_strings">
<string text=""/>
@@ -160,5 +171,7 @@
<string text="Each team's vehicles and their respawn time, for\nthis game."/>
<string text="The motion tracker and nav points, for this game."/>
<string text="Friendly fire and team balance, for this game."/>
<string text="Anyone can see and join your game: it is listed\nin everyone's Server Browser."/>
<string text="Only players with your invite link can join."/>
</strings>
</menus>
@@ -59,13 +59,14 @@
<child widget="main_menu/multiplayer_type_select/multiplayer_type_create_item" x="-32" y="210"/>
<child widget="main_menu/multiplayer_type_select/multiplayer_type_create_internet_item" y="243"/>
<child widget="main_menu/multiplayer_type_select/multiplayer_type_create_lan_item" y="276"/>
<child widget="main_menu/multiplayer_type_select/multiplayer_type_gametypes_item" y="342"/>
<child widget="main_menu/multiplayer_type_select/multiplayer_type_coop_item" y="309"/>
<child widget="main_menu/multiplayer_type_select/multiplayer_type_gametypes_item" y="375"/>
<child widget="main_menu/multiplayer_type_select/multiplayer_type_button_bar" y="414"/>
</widget>
<widget name="main_menu/multiplayer_type_select/multiplayer_type_select_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
<on event="created" run="mp type menu initialize"/>
<child widget="main_menu/multiplayer_type_select/header_multiplayer"/>
<child widget="main_menu/multiplayer_type_select/multiplayer_type_select_list"/>
<child widget="main_menu/blueline" x="64" y="325"/>
<child widget="main_menu/blueline" x="64" y="358"/>
</widget>
</menus>
@@ -1,6 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The port's multiplayer screens, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/op_map_weapons" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/map_weapons_label"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/map_weapons_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/map_weapons_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/multiplayer_setup/item_options_edit/item_options_labels" string_index="8" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/map_weapons_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/settings_select/multiplayer_setup/item_options_edit/var_map_weapons" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/op_loadout" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/loadout_label"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/loadout_spinner" x="286" y="1"/>
@@ -28,6 +37,10 @@
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/secondary_weapon_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/settings_select/multiplayer_setup/item_options_edit/var_loadout_weapon" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/settings_select/multiplayer_setup/item_options_edit/var_map_weapons">
<string text="YES"/>
<string text="NO"/>
</strings>
<strings name="main_menu/settings_select/multiplayer_setup/item_options_edit/var_loadout">
<string text="CATEGORY"/>
<string text="CUSTOM"/>
@@ -43,7 +56,5 @@
<string text="PLASMA PISTOL"/>
<string text="PLASMA RIFLE"/>
<string text="NEEDLER"/>
<string text="FUEL ROD"/>
<string text="FLAMETHROWER"/>
</strings>
</menus>
@@ -26,9 +26,10 @@
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_infinite_grenades" x="54" y="73"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_weapon_set" x="54" y="103"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_starting_equipment" x="54" y="133"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_loadout" x="54" y="163"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_primary_weapon" x="54" y="193"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_secondary_weapon" x="54" y="223"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_map_weapons" x="54" y="163"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_loadout" x="54" y="193"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_primary_weapon" x="54" y="223"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_secondary_weapon" x="54" y="253"/>
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/item_button_bar" y="414"/>
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/item_options_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
Binary file not shown.

After

Width:  |  Height:  |  Size: 75 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

+17 -4
View File
@@ -374,6 +374,7 @@
<string text="Join a game by its invite link,\nor one a Discord invite\nreached."/>
<string text="Host a game on the Internet:\nplayers join by its invite\nlink or Discord."/>
<string text="Host a game on your LAN\nonly."/>
<string text="Play the campaign with a\nfriend in split screen:\nplayer 2 on a gamepad."/>
<string text="Set all the attributes for your \nmultiplayer gametypes and\nkeep them for future use."/>
</strings>
<strings name="main_menu/multiplayer_type_select/multiplayer_options">
@@ -384,6 +385,7 @@
<string text="DIRECT LINK"/>
<string text="EDIT GAMETYPES"/>
<string text="SERVER BROWSER"/>
<string text="CO-OP CAMPAIGN"/>
</strings>
<strings name="main_menu/multiplayer_type_select/server_settings/cap_server_settings_options">
<string text="Enter a name for your server."/>
@@ -498,6 +500,8 @@
<string text="The map's weapons, without any grenades."/>
<string text="You will start the game with the weapon set that the \nmap designers custom tailored for it."/>
<string text="Use the generic starting weapons across all maps."/>
<string text="The map's weapons appear where its designers\nplaced them."/>
<string text="No weapons appear on the map: with a loadout of\nNONE and NONE, melee and grenades only."/>
<string text="The weapons follow the weapon set above."/>
<string text="Everyone starts with the primary and secondary\nweapons below; the map's weapons are its own."/>
<string text="No primary weapon."/>
@@ -510,8 +514,6 @@
<string text="Everyone spawns with a plasma pistol as their primary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a plasma rifle as their primary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a needler as their primary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a fuel rod as their primary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a flamethrower as their primary\nweapon (where the map has one)."/>
<string text="No secondary weapon."/>
<string text="A random secondary weapon, of those the map has,\neach time you spawn."/>
<string text="Everyone spawns with a assault rifle as their secondary\nweapon (where the map has one)."/>
@@ -522,8 +524,6 @@
<string text="Everyone spawns with a plasma pistol as their secondary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a plasma rifle as their secondary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a needler as their secondary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a fuel rod as their secondary\nweapon (where the map has one)."/>
<string text="Everyone spawns with a flamethrower as their secondary\nweapon (where the map has one)."/>
</strings>
<strings name="main_menu/settings_select/multiplayer_setup/item_options_edit/item_options_labels">
<string text="INFINITE GRENADES:"/>
@@ -534,6 +534,7 @@
<string text="LOADOUT:"/>
<string text="PRIMARY WEAPON:"/>
<string text="SECONDARY WEAPON:"/>
<string text="MAP WEAPONS:"/>
</strings>
<strings name="main_menu/settings_select/multiplayer_setup/item_options_edit/var_infinite_grenades">
<string text="YES"/>
@@ -963,6 +964,12 @@
<string text="At the end of this game there will be fifteen angry\nmen. Angry because the winning player killed them."/>
<string text="You'll need to secure twenty-five kills in order to\nwin the game."/>
<string text="We hope you've got plenty of time, because you'll\nneed to make a full fifty kills to win this one."/>
<string text="Seventy-five kills to win. Settle in for a long\nfight."/>
<string text="A hundred kills to win. Made for big games."/>
<string text="A hundred and fifty kills to win. Only a crowded\nserver gets there."/>
<string text="Two hundred kills to win. Bring friends. Lots of\nthem."/>
<string text="Two hundred and fifty kills to win."/>
<string text="Five hundred kills to win. You'll be here a while."/>
<string text="This is a team game."/>
<string text="It's every man for himself."/>
<string text="The game will have no time limit. It will only end\nwhen someone wins."/>
@@ -994,6 +1001,12 @@
<string text="15"/>
<string text="25"/>
<string text="50"/>
<string text="75"/>
<string text="100"/>
<string text="150"/>
<string text="200"/>
<string text="250"/>
<string text="500"/>
</strings>
<strings name="main_menu/settings_select/multiplayer_setup/playlist_edit/slayer_edit/var_team_play">
<string text="YES"/>
+9
View File
@@ -8,6 +8,7 @@
"ce/main_menu.gametype_select.xml",
"ce/main_menu.multiplayer_type_select.connected.4way_profile_select.xml",
"ce/main_menu.multiplayer_type_select.connected.xml",
"ce/main_menu.multiplayer_type_select.coop.xml",
"ce/main_menu.multiplayer_type_select.direct_ip.xml",
"ce/main_menu.multiplayer_type_select.join_game.filters.xml",
"ce/main_menu.multiplayer_type_select.join_game.port.xml",
@@ -49,9 +50,17 @@
"ce/main_menu.xml",
"ce/shell.xml",
"ce/strings.xml",
"ce/port/blank__0.png",
"ce/port/main_menu/multiplayer_type_select/coop/header_player_2.png",
"ce/port/main_menu/multiplayer_type_select/join_game/header_direct_link.png",
"ce/port/main_menu/multiplayer_type_select/join_game/header_server_browser.png",
"ce/port/main_menu/multiplayer_type_select/lobby/header_lobby.png",
"ce/port/pause/pausebox_center__0.png",
"ce/port/pause/pausebox_center__1.png",
"ce/port/pause/pausebox_left__0.png",
"ce/port/pause/pausebox_left__1.png",
"ce/port/pause/pausebox_right__0.png",
"ce/port/pause/pausebox_right__1.png",
"ce/shell/bitmaps/alert_bkd__0.png",
"ce/shell/bitmaps/arrow_sm_down__0.png",
"ce/shell/bitmaps/arrow_sm_down__1.png",
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="4" viewBox="0 0 4 4"/>

After

Width:  |  Height:  |  Size: 81 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="64" viewBox="0 0 512 64">
<defs>
<filter id="soft" filterUnits="userSpaceOnUse" x="-60" y="-60" width="632" height="184">
<feGaussianBlur stdDeviation="14.23 13.06"/>
</filter>
<mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="64">
<rect width="512" height="59" fill="#fff"/>
</mask>
</defs>
<g mask="url(#cut)">
<rect x="29.08" y="28.05" width="323.93" height="40.95" fill="#021931" fill-opacity="0.893" filter="url(#soft)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 563 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="256" viewBox="0 0 4 256">
<rect x="0" y="2" width="4" height="190" fill="#0a2649" fill-opacity="0.75"/>
<rect x="0" y="0" width="4" height="2" fill="#2896ff"/>
<rect x="0" y="192" width="4" height="2" fill="#2896ff"/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="256" viewBox="0 0 4 256">
<rect x="0" y="2" width="4" height="225" fill="#0a2649" fill-opacity="0.75"/>
<rect x="0" y="0" width="4" height="2" fill="#2896ff"/>
<rect x="0" y="227" width="4" height="2" fill="#2896ff"/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="256" viewBox="0 0 16 256">
<path d="M9.5,1 H17 V193 H9.5 A8.5,8.5 0 0 1 1,184.5 V9.5 A8.5,8.5 0 0 1 9.5,1 Z" fill="#0a2649" fill-opacity="0.75" fill-rule="evenodd"/>
<path d="M17,1 H9.5 A8.5,8.5 0 0 0 1,9.5 V184.5 A8.5,8.5 0 0 0 9.5,193 H17" fill="none" stroke="#2896ff" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 360 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="256" viewBox="0 0 16 256">
<path d="M9.5,1 H17 V228 H9.5 A8.5,8.5 0 0 1 1,219.5 V9.5 A8.5,8.5 0 0 1 9.5,1 Z" fill="#0a2649" fill-opacity="0.75" fill-rule="evenodd"/>
<path d="M17,1 H9.5 A8.5,8.5 0 0 0 1,9.5 V219.5 A8.5,8.5 0 0 0 9.5,228 H17" fill="none" stroke="#2896ff" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 360 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="256" viewBox="0 0 16 256">
<g transform="matrix(-1 0 0 1 16 0)">
<path d="M9.5,1 H17 V193 H9.5 A8.5,8.5 0 0 1 1,184.5 V9.5 A8.5,8.5 0 0 1 9.5,1 Z" fill="#0a2649" fill-opacity="0.75" fill-rule="evenodd"/>
<path d="M17,1 H9.5 A8.5,8.5 0 0 0 1,9.5 V184.5 A8.5,8.5 0 0 0 9.5,193 H17" fill="none" stroke="#2896ff" stroke-width="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 411 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="256" viewBox="0 0 16 256">
<g transform="matrix(-1 0 0 1 16 0)">
<path d="M9.5,1 H17 V228 H9.5 A8.5,8.5 0 0 1 1,219.5 V9.5 A8.5,8.5 0 0 1 9.5,1 Z" fill="#0a2649" fill-opacity="0.75" fill-rule="evenodd"/>
<path d="M17,1 H9.5 A8.5,8.5 0 0 0 1,9.5 V219.5 A8.5,8.5 0 0 0 9.5,228 H17" fill="none" stroke="#2896ff" stroke-width="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 411 B

+79 -7
View File
@@ -95,8 +95,10 @@ The keyboard and the mouse are a control scheme of their own for the player
of controller 1: each action has up to two keys or mouse buttons, which
Settings > Controls Setup (or `[controls]` in `config.toml`) changes. The
game adds the input of the first gamepad to controller 1. The other
gamepads operate controllers 2 to 4. The profile's button layout (Settings >
Gamepads) is the gamepads' only.
gamepads operate controllers 2 to 4. In co-op with only one gamepad, that
gamepad is controller 2 (player 2) and the keyboard and mouse stay
controller 1. The profile's button layout (Settings > Gamepads) is the
gamepads' only.
| Action | Keys and buttons (default) |
| --- | --- |
@@ -157,6 +159,24 @@ change. The screens open, close and move between each other as the PC
version's. `display.menus = "xbox"` gives the Xbox's menus, which start every
kind of game.
Multiplayer > CO-OP CAMPAIGN is the Xbox's cooperative play, which the PC
version does not have: two players on this computer play the campaign in
split screen. Player 1 is the player who chose it, on the current profile.
Player 2 then chooses a profile with their own controller (a gamepad), and
New Game's levels are those either profile has reached. A co-op game does
not continue a saved game of one player.
In a multiplayer game, the pause menu (escape) has SETTINGS, which opens
the profile's settings while the game goes on, and for the host END GAME.
END GAME ends the game as its time limit does: the players stay, and after
the carnage report the host picks the next map and gametype (PICK GAME).
LEAVE GAME of the host still ends the game for everyone. The buttons go into
the map's own pause menu, before LEAVE GAME, so the buttons of a custom map
stay. The Xbox's pause box is drawn taller to hold them (a redraw,
`port/assets/menus/port_svg/pause`); a custom map's box keeps its size, and
what is below its list moves down. The few pictures of the settings that
come from the main menu's map are not drawn there.
The menus are XML files in `port/assets/menus` (`tools/ce_menus.py` writes
them from the PC version's tags), which the game contains. To change them,
put files in a `menus` folder next to `config.toml`: a file with the same
@@ -195,7 +215,7 @@ the setting for one start of the game. It has priority over the file.
| `display.high_res_hud` | `true` | `HALO_HIGH_RES_HUD` | `true`: the HUD (meters, counters, panels and their outlines, the motion sensor, reticles, waypoints, scopes) is drawn from the high-res assets in `port/assets/hud`, 8x the size of the maps' bitmaps. The bitmaps with English text keep the maps' own. `false`: the maps' own bitmaps. |
| `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the display's resolution, laid out as before, and the menus' titles are drawn from the high-res pictures in `port/assets/titles`. `false`: the maps' bitmap fonts and titles. |
| `display.menus` | `"pc"` | `HALO_MENUS` | `"pc"`: the PC version's menus, from the files in `port/assets/menus` and a `menus` folder next to `config.toml`. Refer to "Menus". `"xbox"`: the Xbox's menus. |
| `display.player_names` | `"all"` | `HALO_PLAYER_NAMES` | In multiplayer, whose names are drawn above their heads: `"all"`, `"allies"`, `"enemies"` or `"none"`. An ally's name is drawn above the triangle the game shows over teammates. An enemy's name shows only while the enemy is in sight and not camouflaged, so it never shows where an enemy hides, and only as far away as the weapon in hand turns its reticle red over an enemy (at least 20 world units, the motion sensor's reach, and at most 70). |
| `display.player_names` | `"all"` | `HALO_PLAYER_NAMES` | In multiplayer, whose names are drawn above their heads: `"all"`, `"allies"`, `"enemies"` or `"none"`. An ally's name is drawn above the triangle the game shows over teammates. An enemy's name shows only within the motion sensor's reach, while the enemy is in sight and not camouflaged, so it never shows where an enemy hides. The gametype's motion tracker setting also applies: no names if it shows no players, only allies' if it shows only friends. |
| `display.player_name_scale` | `1.0` | `HALO_PLAYER_NAME_SCALE` | How large the players' names are drawn: `1.0` is three quarters of the size of the HUD's text, from `0.25` to `4`. With high-res text, larger names are rasterized at their size, so they stay sharp. |
| `display.scoreboard_team_layout` | `"teams"` | `HALO_SCOREBOARD_TEAM_LAYOUT` | How the multiplayer scoreboard (hold BACK, or tab) lists a team game's players. `"teams"`: a column for each team, red on the left and blue on the right. `"score"`: all the players in order of score. With more players than fit, the mouse wheel and Page Up / Page Down scroll the scoreboard. |
| `display.scoreboard_background` | `true` | `HALO_SCOREBOARD_BACKGROUND` | `true`: the multiplayer scoreboard (hold BACK, or tab) has a panel behind its text, for clearer text. |
@@ -219,7 +239,9 @@ the setting for one start of the game. It has priority over the file.
| `network.join_from_clipboard` | `true` | `HALO_NET_JOIN_FROM_CLIPBOARD` | `true`: when the game comes to the front, it joins the game of an invite link on the clipboard. |
| `network.tunnel_port` | `0` | `HALO_NET_TUNNEL_PORT` | The UDP port for internet play. `0`: the game selects a port. Refer to "Internet play". |
| `network.allow_upnp` | `true` | `HALO_NET_ALLOW_UPNP` | `true`: internet play can ask the router to forward its port (UPnP). `false`: the game does not ask. Refer to "Internet play". |
| `network.signalling_brokers` | three public brokers | `HALO_NET_BROKERS` | The public MQTT brokers (`host:port`, with commas between them) that let the machines of an invite find each other. |
| `network.public_lobby` | `true` | `HALO_NET_PUBLIC_LOBBY` | `true`: the server browser. Public games are listed, and Join Game > Server Browser shows them. `false`: no games are listed or shown. Refer to "Server browser". |
| `network.host_public` | `true` | `HALO_NET_HOST_PUBLIC` | `true`: a new game of Create Game > Internet starts as PUBLIC. `false`: it starts as PRIVATE. LISTING in Server Setup changes it for each game. Refer to "Server browser". |
| `network.signalling_brokers` | three public brokers | `HALO_NET_BROKERS` | The public MQTT brokers (`host:port`, with commas between them) that let the machines of an invite find each other, and that carry the listings of the server browser. |
| `network.stun_servers` | Google and Cloudflare | `HALO_NET_STUN` | The public STUN servers (`host:port`, with commas between them) that give the internet address of a machine. |
| `discord.application_id` | the application of the project | `HALO_DISCORD_APPLICATION` | The Discord application for invites. Empty: no Discord. |
| `update.auto` | `true` | `HALO_UPDATE_AUTO` | `true`: at start-up, the game looks for a new version. Refer to "Updates". `false`: the game does not look. |
@@ -402,12 +424,62 @@ When the machines connect, the game of the host shows in Multiplayer,
System Link. Join the game as on a local network. System link on a local
network does not need an invite.
### Server browser
Server Setup in Create Game > Internet has a LISTING row:
- PUBLIC (the default): the game also shows in Join Game > Server Browser
on every machine. Anyone can see and join the game.
- PRIVATE: only players with the invite link can join.
Each new game starts as PUBLIC (`network.host_public = false` makes new
games start as PRIVATE). A LAN game is never listed. `network.public_lobby = false` turns the server browser off.
In the Server Browser, select a game to join it. The game joins the invite
of the game, as for a link. When it reaches the host, it opens the lobby.
If it cannot reach the host in 30 seconds, it marks the game FAILED. REFRESH
asks the hosts for their listings again.
How it operates (`src/p2p_lobby.c`):
- The host of a public game publishes a listing: the invite, and the name,
map, gametype and player counts of the game. The listing goes to the
same MQTT brokers as the invites (`network.signalling_brokers`), retained,
to a topic of the host (`hceu/3/lobby/s/<hash of its key>`).
- The key of the host signs the listing (Ed25519). The key is the key of the
invite, so no other machine can list the invite of the host, change its
listing, or list a game with false details.
- The host publishes the listing again every 30 seconds, when the game
changes, and when a browser asks. If the topic of the host is empty or
holds another listing, the host publishes again in 5 seconds or less. Thus
a broker that deletes the listing does not remove the game.
- When the host stops (it stops hosting, the game becomes private, or the
game quits), it publishes a closed listing, and then empties its topic.
If the host loses its connection, the broker empties its topic (the will
of the connection). A browser removes a game that it does not hear for 90
seconds.
- When a public game becomes private, the host makes a new invite. Thus a
player who saw the listing cannot join with the old invite.
A public game does not publish the address of the host. But any machine
with the invite can ask the host to connect, and the host then sends its
addresses. Thus anyone can learn the address of the host of a public game,
as for any public server.
To use a broker of your own, add it to `network.signalling_brokers`. All the
players must use the same broker to see each other's games. The game uses
MQTT 5 if the broker has it, else MQTT 3.1.1. A broker that does not keep
retained messages, or does not let clients subscribe with wildcards, carries
only invites, not listings.
### Security
Only machines with the invite can find the game:
- Each copy of the game makes an X25519 key pair when it starts. Its
identifier is from the hash of its public key.
- Each copy of the game makes an Ed25519 key pair when it starts, and from
it an X25519 key pair (Monocypher, `port/third_party/monocypher`). Its
identifier is from the hash of its X25519 public key. The Ed25519 key
signs the listing of a public game.
- The link contains a 16-byte hash of the public key of the host and a
random 16-byte token. The identifier of the host is from the first 6
bytes of the hash.
@@ -608,7 +680,7 @@ Other changes:
| `networking/`, `game/`, `interface/`, `bungie_net/network/` and the pools of objects, effects and sounds | The system link limits and the memory for them. |
| `game/`, `objects/`, `units/`, `networking/` | The distributed netcode. Refer to `NETCODE.md`. |
| `cache/cache_files.c` | When a map's tags load and unload, the port finds the bitmaps that the high-res HUD replaces (`game/hud_hires_tags.c`), and adds the tags of the menus to the menus' map (`game/menu_tags.c`). |
| `interface/ui_widget.c`, `interface/ui_widget_event_handler_functions.c`, `interface/ui_widget_game_data_input_functions.c` | The main menu is the PC version's from `port/assets/menus` (`display.menus`); the menus' widgets can call the port's functions (`game/menu_functions.c`) and send the PC version's custom activation event; the widgets' memory is 256 KB, not 16 KB. |
| `interface/ui_widget.c`, `interface/ui_widget_event_handler_functions.c`, `interface/ui_widget_game_data_input_functions.c` | The main menu is the PC version's from `port/assets/menus` (`display.menus`); the menus' widgets can call the port's functions (`game/menu_functions.c`) and send the PC version's custom activation event; the widgets' memory is 256 KB, not 16 KB; the main menu and Multiplayer clear co-op's controllers, so that a gamepad going back to controller 1 is not a controller unplugged. |
| `input/input_abstraction.c`, `game/player_control.c`, `game/players.c`, `game/player_queues_new.c`, `units/units.h` | The keyboard and mouse's actions (`src/xinput_sdl.c`, `include/halo_keyboard.h`) join controller 1's game controls; their reload key reloads on its own, and their action key only acts (a control flag of the port's, sent with the player's action, stops the reload the controller's X falls back to). |
| `sound/sound_manager.c`, `interface/hud.c`, `game/game_engine.c` | The music's and the other sounds' volumes; the HUD's and the scoreboard's settings are read again when Settings changes them. |
| `interface/hud.c` | In multiplayer, players' names are drawn above their heads (`display.player_names`, `display.player_name_scale`). |
+1103 -10
View File
File diff suppressed because it is too large. Load diff
+601 -22
View File
@@ -51,6 +51,11 @@ the one last used, else the first) and the game's saved game in it:
The Xbox's functions of those names take the Xbox's widgets (a spinner of
levels, the difficulty list itself), so ours run instead (menu_tags.c).
Co-op (the Xbox's Cooperative Play, Multiplayer's CO-OP CAMPAIGN): "port coop
begin" makes two players, player 1 on its profile; "port coop player 2"
gives player 2 the profile chosen and the controller that chose it; then the
campaign's New Game and difficulty start the game for both.
The rest are the PC version's, which its menus (port/assets/menus/ce)
name and the Xbox's has not: they do nothing yet, and succeed, so that what
their handlers open opens.
@@ -69,6 +74,8 @@ their handlers open opens.
#include "text/unicode.h"
#include "halo_menus.h"
/* (internet play's server browser: the platform layer's) */
#include "../src/p2p.h"
#include <stdlib.h>
#include <string.h>
@@ -97,6 +104,7 @@ void ui_widget_port_go_back(struct widget_instance *widget);
short ui_widget_port_list_index(struct widget_instance *list_widget);
boolean ui_widget_port_saved_game(char const **map_name, short *level, short *difficulty);
short main_get_solo_level_from_name(char const *name);
boolean player_name_clean(wchar_t *name, long count);
boolean pc_menu_event_function_invoke(struct widget_instance *widget, struct event_record *event,
long function_index, boolean *widget_deleted);
@@ -657,7 +665,8 @@ static void rows_update(struct widget_instance *list, short count, void (*row_te
}
/* player 1's profile, read again (the active one, else the one last used,
else the first), on the controller; FALSE if there is none */
else the first), on the controller (in co-op, on the one that chose co-op:
coop_begin); FALSE if there is none */
static boolean campaign_profile(short controller, struct player_profile *profile)
{
long profile_index = player_ui_get_active_player_profile_index(0);
@@ -676,39 +685,52 @@ static boolean campaign_profile(short controller, struct player_profile *profile
}
}
player_ui_set_active_player_profile(0, profile_index, profile);
player_ui_set_single_player_local_player_controller(0, controller);
if (player_spawn_count < 2)
player_ui_set_single_player_local_player_controller(0, controller);
return TRUE;
}
/* the levels the profile has reached (as the Xbox's list has them: those
it has played, the one after the last it finished, the first) and finished,
and its saved game's */
and its saved game's; in co-op, those either player's has reached and
finished, and no saved game (a game of one player's does not go on with
two: game_state.c's game_state_header_valid) */
static void campaign_levels_read(struct player_profile const *profile)
{
char const *map_name;
short highest_level, highest_difficulty, difficulty, level;
struct player_profile player2;
short highest_level, highest_difficulty, difficulty, level, player;
player_profile_get_highest_completed_solo_level((struct player_profile *)profile, &highest_level,
&highest_difficulty);
for (level = 0; level < NUMBER_OF_SINGLE_PLAYER_LEVELS; level++)
memset(&campaign.levels[level], 0, sizeof(campaign.levels[level]));
if (player_spawn_count >= 2)
player_ui_get_active_player_profile(1, &player2);
for (player = 0; player < (player_spawn_count >= 2 ? 2 : 1); player++)
{
byte flags = profile->single_player_map_flags[level];
short marker;
struct player_profile const *reader = player == 0 ? profile : &player2;
campaign.levels[level].available = flags || level == highest_level + 1 || level == 0;
for (marker = 0; marker < 3; marker++)
campaign.levels[level].finished[marker] = (flags >> (marker + 1)) & 1;
player_profile_get_highest_completed_solo_level((struct player_profile *)reader, &highest_level,
&highest_difficulty);
for (level = 0; level < NUMBER_OF_SINGLE_PLAYER_LEVELS; level++)
{
byte flags = reader->single_player_map_flags[level];
short marker;
campaign.levels[level].available |= flags || level == highest_level + 1 || level == 0;
for (marker = 0; marker < 3; marker++)
campaign.levels[level].finished[marker] |= (flags >> (marker + 1)) & 1;
}
}
if (!ui_widget_port_saved_game(&map_name, &campaign.saved_level, &difficulty))
if (player_spawn_count >= 2 || !ui_widget_port_saved_game(&map_name, &campaign.saved_level, &difficulty))
campaign.saved_level = NONE;
(void)map_name;
}
/* plays the map, at the difficulty (a saved game in it goes on: main.c's
main_new_map, if its difficulty is this one) */
static void campaign_start(char const *map_name, short difficulty, short controller)
{
player_ui_set_single_player_local_player_controller(0, controller);
if (player_spawn_count < 2)
player_ui_set_single_player_local_player_controller(0, controller);
main_set_map_name(map_name);
main_set_difficulty(difficulty);
game_connection_set(0);
@@ -1394,6 +1416,82 @@ static boolean profile_delete(void)
return TRUE;
}
/* ---------- Co-op: the campaign for two players on this machine, in split
screen (the Xbox's Cooperative Play, which the PC version has not):
Multiplayer's CO-OP CAMPAIGN ("port coop begin"), player 2's profile, chosen
with player 2's controller ("port coop player 2"), then New Game's levels
(those either has reached) and difficulty. The main menu and Multiplayer
go back to one player (main_menu_initialize, multiplayer_type_menu_initialize) */
/* (xinput_sdl.c) whether two players play on this machine: then one gamepad
is player 2's, not the keyboard's player 1's */
unsigned char pc_menu_coop_players(void)
{
return player_spawn_count >= 2;
}
/* "port coop begin": two players, player 1 on its profile (campaign_profile)
and the controller that chose co-op */
static boolean coop_begin(short controller)
{
struct player_profile profile;
player_spawn_count = 1;
player_ui_reset_single_player_local_player_controllers();
if (!campaign_profile(controller, &profile))
return campaign_fail();
player_spawn_count = 2;
return TRUE;
}
/* "port coop player 2 list initialize": on player 2's profile, else the first
that is not player 1's */
static boolean coop_player2_list_initialize(struct widget_instance *list)
{
long player1 = player_ui_get_active_player_profile_index(0);
long player2 = player_ui_get_active_player_profile_index(1);
short index;
profile_list_read(TRUE);
profile_list.chosen = 0;
for (index = profile_list.count - 1; index >= 0; index--)
{
if (profile_list.indices[index] != player1)
profile_list.chosen = index;
}
for (index = 0; index < profile_list.count && player2 != NONE; index++)
{
if (profile_list.indices[index] == player2)
profile_list.chosen = index;
}
profile_list.first = (short)PIN(profile_list.chosen - PROFILE_ROWS / 2, 0,
MAX(0, profile_list.count + 1 - PROFILE_ROWS));
focus_row(list, (short)(profile_list.chosen - profile_list.first));
return TRUE;
}
/* "port coop player 2": the profile chosen made player 2's, on the controller
that chose it, which must not be player 1's */
static boolean coop_player2_choose(short controller)
{
struct player_profile profile;
if (player_spawn_count < 2 || profile_list.chosen >= profile_list.count ||
!player_profile_get(profile_list.indices[profile_list.chosen], &profile))
{
return campaign_fail();
}
if (controller == player_ui_get_single_player_local_player_controller(0))
{
display_error_text_deferred(L"Player 2 chooses their\r\nprofile with their own\r\ncontroller.",
NONE);
return campaign_fail();
}
player_ui_set_single_player_local_player_controller(1, controller);
player_ui_set_active_player_profile(1, profile_list.indices[profile_list.chosen], &profile);
return TRUE;
}
/* ---------- Controls Setup: the keyboard and mouse's controls
(port/linux/include/halo_keyboard.h), shown a group at a time, two
bindings each */
@@ -1731,6 +1829,8 @@ boolean network_player_is_valid(struct network_player *player);
boolean playlist_profile_get(long index, struct game_variant *variant);
boolean playlist_profile_get_display_name(long index, wchar_t *name);
boolean input_get_key(struct key_stroke *key);
boolean game_engine_running(void);
void game_engine_end_game(void);
/* the platform layer's */
int p2p_join_invite(char const *text);
int p2p_invite_link(char *link, int size);
@@ -1740,6 +1840,7 @@ int platform_clipboard_get(char *text, int size);
void platform_clipboard_set(char const *text);
void platform_text_field(int typing);
int config_boolean(char const *name);
void ui_widget_port_post_button(short controller_index, short button_index);
static wchar_t const *const engine_names[] = { L"", L"CTF", L"SLAYER", L"ODDBALL", L"KING OF THE HILL", L"RACE" };
static short const maximum_players[] = { 2, 4, 8, 12, 16, 24, 32, 48, 64, 96, 128 };
@@ -1766,6 +1867,9 @@ static struct
(the client's slot it is in may come to hold another game) */
byte preview_key_id[8];
byte preview_xnaddr[12];
/* Server Setup's LISTING: PRIVATE (each new game starts with
network.host_public's choice: PUBLIC unless set otherwise) */
boolean game_private;
} multiplayer = { 0, 0, 0, { 0 }, 0, { 0 }, 0, 0, 0, 0, { 0 }, NUMBEROF(maximum_players) - 1 };
/* ---- a text field (Direct Link's link, the game's name): the keyboard
@@ -1911,6 +2015,10 @@ static boolean multiplayer_host(struct widget_instance *widget, struct event_rec
if (!multiplayer_player(controller))
return FALSE;
p2p_set_hosting_allowed(multiplayer.mode == _multiplayer_mode_host_internet);
/* (in the server browser, if PUBLIC: Server Setup's LISTING, which a
new game starts with as network.host_public says) */
multiplayer.game_private = !config_boolean("network.host_public");
p2p_set_hosting_public(multiplayer.mode == _multiplayer_mode_host_internet && !multiplayer.game_private);
return ui_widget_port_host(widget, event, widget_deleted);
}
@@ -2130,6 +2238,10 @@ static boolean server_settings_initialize(struct widget_instance *list)
}
if (spinner)
spinner->parameters.list.selected_index = multiplayer.maximum_players_index;
/* (PUBLIC or PRIVATE: this game's; the screen is made again on coming
back from an option's screen) */
if ((spinner = named(list, "listing_spinner", 0)) != NULL)
spinner->parameters.list.selected_index = multiplayer.game_private ? 1 : 0;
return TRUE;
}
@@ -2168,6 +2280,20 @@ static void server_settings_update(struct widget_instance *list)
text_set(named(list, "game_type_value", 0), type);
}
settings_help(list);
/* LISTING (an internet game's): PUBLIC, listed in everyone's server
browser, or PRIVATE, for this game. Its help is its choice's */
visible_set(named(list, "op_listing", 0), multiplayer.mode == _multiplayer_mode_host_internet &&
config_boolean("network.online") && config_boolean("network.public_lobby"));
if ((spinner = named(list, "listing_spinner", 0)) != NULL && named(list, "op_listing", 0)->visible)
{
boolean public = spinner->parameters.list.selected_index == 0;
struct widget_instance *help = list->parameters.list.extended_description;
multiplayer.game_private = !public;
p2p_set_hosting_public(public);
if (help && list->focused_child == named(list, "op_listing", 0))
help->parameters.text_box.string_list_index = (short)(10 + spinner->parameters.list.selected_index);
}
}
/* "ss edit server name" (its row's A: begins, or ends) */
@@ -2291,8 +2417,17 @@ static void browser_focus(struct widget_instance *list)
{
char const *const choices[] = { "server_item_1", "join_game_button_bar" };
struct widget_instance *focused = list->focused_child;
struct widget_instance *row;
short index;
/* (the rows' backgrounds: the focused one's outlined frame. The engine
shows a list item's focus only on a bitmap of two frames, and theirs has
three: normal, focused, selected) */
for (row = list->child; row; row = row->next)
{
if (browser_row_index(row) != NONE)
row->animation.current_frame_index = row == focused ? 1 : 0;
}
focus_off_hidden(named(list, "join_game_button_bar", 0));
if (focused && focused->visible && !focused->disabled)
return;
@@ -2313,6 +2448,12 @@ static void browser_focus(struct widget_instance *list)
}
}
/* (the server browser's: below) */
static void lobby_browser_begin(void);
static void lobby_browser_update(struct widget_instance *list);
static boolean lobby_browser_select(struct widget_instance *widget, short controller, short row,
boolean *widget_deleted);
/* "gamespy screen init": the mode's title and parts; the games' client */
static boolean browser_initialize(struct widget_instance *screen, struct event_record *event,
boolean *widget_deleted)
@@ -2329,6 +2470,7 @@ static boolean browser_initialize(struct widget_instance *screen, struct event_r
for (index = 0; index < NUMBEROF(unused); index++)
visible_set(named(screen, unused[index], 0), FALSE);
visible_set(named(screen, "button_clipboard", 0), multiplayer.mode == _multiplayer_mode_direct_link);
visible_set(named(screen, "join_game_button_refresh", 0), multiplayer.mode == _multiplayer_mode_server_browser);
{
struct widget_instance *list = named(screen, "join_game_items_list", 0);
struct widget_instance *child;
@@ -2346,12 +2488,24 @@ static boolean browser_initialize(struct widget_instance *screen, struct event_r
for (index = 0; named(screen, "header_sort_arrows", index); index++)
visible_set(named(screen, "header_sort_arrows", index), FALSE);
multiplayer.game_chosen = 0;
/* (the server browser's games are internet play's listings; joining
one reaches its host, whose game the client then finds as Direct Link's) */
if (multiplayer.mode == _multiplayer_mode_server_browser)
return TRUE;
lobby_browser_begin();
return ui_widget_port_browse(screen, event, widget_deleted);
}
static void game_map_name(struct advertised_game const *game, wchar_t *text)
/* a scenario's name without its path */
static char const *scenario_name(char const *path)
{
char const *name = strrchr(path, '\\');
return name ? name + 1 : path;
}
/* a map's name as the menus show it (its scenario's name, if not one of
theirs), from its scenario's path or name */
static void map_display_name(char const *map_name, wchar_t *text)
{
char const *const *names;
short last, index;
@@ -2359,17 +2513,376 @@ static void game_map_name(struct advertised_game const *game, wchar_t *text)
for (index = 0; index < count; index++)
{
if (!_stricmp(names[index], game->map_name))
if (!_stricmp(scenario_name(names[index]), scenario_name(map_name)))
{
string_get("pc\\main_menu\\mp_map_list", index, text);
return;
}
}
for (index = 0; game->map_name[index] && index < ROW_TEXT_LENGTH - 1; index++)
text[index] = (wchar_t)(unsigned char)game->map_name[index];
for (index = 0; map_name[index] && index < ROW_TEXT_LENGTH - 1; index++)
text[index] = (wchar_t)(unsigned char)map_name[index];
text[index] = 0;
}
static void game_map_name(struct advertised_game const *game, wchar_t *text)
{
map_display_name(game->map_name, text);
}
/* ---- Join Game > Server Browser: internet play's public games (their
hosts' listings, p2p_lobby.c). Joining one joins its invite (as Direct
Link's PASTE LINK); once its host is reached its game is among the client's,
and an A press (posted) joins it, or shows its lobby if under way */
#define LOBBY_BROWSER_GAMES 256
/* milliseconds: a host not reached in this long is given up (p2p's own
wait is longer) */
#define LOBBY_BROWSER_JOIN_TIMEOUT 30000
/* how long the browser looks before it says it found none, and shows a
join that failed */
#define LOBBY_BROWSER_LOOK_TIME 6000
#define LOBBY_BROWSER_FAILED_TIME 8000
static struct
{
struct p2p_listing games[LOBBY_BROWSER_GAMES];
/* the games found, the first on the rows, the one last focused (Join's) */
short count, first, chosen;
/* whether it has shown a game (the first has the focus) */
boolean shown;
unsigned long begin_time;
/* a game being joined: its host, its name, since when, the controller
that asked; ready: its game reached (the A press posted) */
boolean joining, ready;
unsigned char identifier[6];
char name[P2P_LISTING_NAME_SIZE + 1];
unsigned long join_time;
short controller;
/* when the press was posted (again if it was lost) */
unsigned long ready_time;
/* the last that failed, and when */
char failed_name[P2P_LISTING_NAME_SIZE + 1];
unsigned long failed_time;
} lobby_browser;
static void lobby_browser_begin(void)
{
lobby_browser.count = 0;
lobby_browser.first = 0;
lobby_browser.chosen = 0;
lobby_browser.shown = FALSE;
lobby_browser.joining = lobby_browser.ready = FALSE;
lobby_browser.begin_time = system_milliseconds();
p2p_lobby_browse(TRUE);
}
/* "gamespy screen dispose" */
static void lobby_browser_end(void)
{
lobby_browser.joining = lobby_browser.ready = FALSE;
p2p_lobby_browse(FALSE);
}
static void text_to_wide(char const *text, wchar_t *wide, short size)
{
short index;
for (index = 0; text[index] && index < size - 1; index++)
wide[index] = (wchar_t)(unsigned char)text[index];
wide[index] = 0;
}
/* the games whose names are valid, as a host keeps the names of the
machines and players that join it (network_game_server_clean_name): each
name cleaned (player_name_clean), and a game whose name has nothing left
that names it left out; returns how many are left */
static short lobby_browser_valid_games(struct p2p_listing *games, short count)
{
short read;
short written = 0;
for (read = 0; read < count; read++)
{
wchar_t name[P2P_LISTING_NAME_SIZE + 1];
short index;
text_to_wide(games[read].name, name, NUMBEROF(name));
if (!player_name_clean(name, NUMBEROF(name)))
continue;
/* (ASCII still: the listing's names are) */
for (index = 0; name[index]; index++)
games[read].name[index] = (char)name[index];
games[read].name[index] = 0;
if (written != read)
games[written] = games[read];
written++;
}
return written;
}
/* the game being joined, once its host is reached (the client's game from
it), else NULL */
static struct advertised_game *lobby_browser_joined_game(void)
{
void *client = global_network_game_client_get();
struct advertised_game *games = client ? network_game_client_get_available_games(client) : NULL;
short index;
for (index = 0; games && index < MAXIMUM_ADVERTISED_GAMES; index++)
{
if (network_game_client_advertised_game_is_valid(&games[index]) &&
!memcmp(games[index].xnaddr + 2, lobby_browser.identifier, sizeof(lobby_browser.identifier)))
{
return &games[index];
}
}
return NULL;
}
/* the focus on a game's row (0 to BROWSER_ROWS - 1) */
static void lobby_browser_focus_row(struct widget_instance *list, short row)
{
struct widget_instance *child;
short child_index = 0;
char name[16];
sprintf(name, "server_item_%d", row + 1);
for (child = list->child; child; child = child->next, child_index++)
{
if (!strcmp(child->name, name))
{
list->focused_child = child;
list->parameters.list.selected_index = child_index;
return;
}
}
}
/* "gamespy screen update" for the server browser: the rows (scrolled on
at their ends), the line below them, the counts over the titles */
static void lobby_browser_update(struct widget_instance *list)
{
struct widget_instance *stats = list->parameters.list.extended_description;
struct widget_instance *row;
short focused = browser_row_index(list->focused_child);
wchar_t text[ROW_TEXT_LENGTH * 2];
unsigned long now = system_milliseconds();
short chosen;
lobby_browser.count = lobby_browser_valid_games(lobby_browser.games,
(short)p2p_lobby_games(lobby_browser.games, LOBBY_BROWSER_GAMES));
if (focused == BROWSER_ROWS - 1 && lobby_browser.first + BROWSER_ROWS < lobby_browser.count)
{
lobby_browser.first++;
lobby_browser_focus_row(list, --focused);
}
else if (focused == 0 && lobby_browser.first > 0)
{
lobby_browser.first--;
lobby_browser_focus_row(list, ++focused);
}
lobby_browser.first = (short)PIN(lobby_browser.first, 0, MAX(0, lobby_browser.count - BROWSER_ROWS));
/* (the first game found takes the focus from the buttons, which had it
while there were none) */
if (lobby_browser.count && !lobby_browser.shown)
{
struct widget_instance *first_row = named(list, "server_item_1", 0);
lobby_browser.shown = TRUE;
if (first_row)
{
first_row->visible = TRUE;
lobby_browser_focus_row(list, 0);
focused = 0;
}
}
if (focused != NONE)
lobby_browser.chosen = (short)(lobby_browser.first + focused);
chosen = lobby_browser.chosen;
for (row = list->child; row; row = row->next)
{
short index = browser_row_index(row);
struct p2p_listing const *game;
if (index == NONE)
continue;
index = (short)(index + lobby_browser.first);
row->visible = index < lobby_browser.count;
if (index >= lobby_browser.count)
continue;
game = &lobby_browser.games[index];
text_to_wide(game->name, text, ROW_TEXT_LENGTH);
text_set(named(row, "server_item_server_name", 0), text);
map_display_name(game->map, text);
text_set(named(row, "server_item_map", 0), text);
text_set(named(row, "server_item_type", 0), engine_names[PIN(game->engine_type, 0, 5)]);
usnprintf(text, ROW_TEXT_LENGTH - 1, L"%d/%d", game->player_count, game->maximum_player_count);
text_set(named(row, "server_item_players", 0), text);
/* (no ping yet: its host is reached only on joining) */
text_set(named(row, "server_item_ping", 0), game->failed ? L"FAILED" : game->in_progress ? L"LIVE" : L"-");
visible_set(named(row, "server_item_locked", 0), !game->open);
visible_set(named(row, "server_item_dedicated", 0), FALSE);
visible_set(named(row, "server_item_classic", 0), FALSE);
}
/* joining: once the host is reached, its game joined (by an A press, on
its row); given up after a while */
if (lobby_browser.ready && now - lobby_browser.ready_time > 3000 &&
now - lobby_browser.join_time <= LOBBY_BROWSER_JOIN_TIMEOUT)
{
lobby_browser.ready_time = now;
ui_widget_port_post_button(lobby_browser.controller, BUTTON_A);
}
if (lobby_browser.joining)
{
if (lobby_browser.ready)
{
/* (its press posted: waiting for it, or for the timeout) */
if (now - lobby_browser.join_time > LOBBY_BROWSER_JOIN_TIMEOUT)
lobby_browser.joining = lobby_browser.ready = FALSE;
}
else if (lobby_browser_joined_game())
{
short index;
lobby_browser.ready = TRUE;
for (index = 0; index < lobby_browser.count; index++)
{
if (!memcmp(lobby_browser.games[index].identifier, lobby_browser.identifier, 6) &&
index >= lobby_browser.first && index < lobby_browser.first + BROWSER_ROWS)
{
lobby_browser_focus_row(list, (short)(index - lobby_browser.first));
}
}
lobby_browser.ready_time = now;
ui_widget_port_post_button(lobby_browser.controller, BUTTON_A);
}
else if (now - lobby_browser.join_time > LOBBY_BROWSER_JOIN_TIMEOUT)
{
p2p_lobby_mark_failed(lobby_browser.identifier);
csmemcpy(lobby_browser.failed_name, lobby_browser.name, sizeof(lobby_browser.failed_name));
lobby_browser.failed_time = now ? now : 1;
lobby_browser.joining = FALSE;
platform_log("menus: could not reach the server browser's game %s", lobby_browser.name);
ui_play_audio_feedback_sound(SOUND_ERROR);
}
}
/* the line below the rows */
{
wchar_t name[P2P_LISTING_NAME_SIZE + 1];
if (!config_boolean("network.online"))
usnprintf(text, NUMBEROF(text) - 1, L"Internet play is off (Settings)");
else if (!config_boolean("network.public_lobby"))
usnprintf(text, NUMBEROF(text) - 1, L"The server browser is off (network.public_lobby)");
else if (lobby_browser.joining)
{
text_to_wide(lobby_browser.name, name, NUMBEROF(name));
usnprintf(text, NUMBEROF(text) - 1, L"Connecting to %s...", name);
}
else if (lobby_browser.failed_time && now - lobby_browser.failed_time < LOBBY_BROWSER_FAILED_TIME)
{
text_to_wide(lobby_browser.failed_name, name, NUMBEROF(name));
usnprintf(text, NUMBEROF(text) - 1, L"Could not reach %s", name);
}
else if (!lobby_browser.count)
{
usnprintf(text, NUMBEROF(text) - 1, L"%s", now - lobby_browser.begin_time < LOBBY_BROWSER_LOOK_TIME ?
L"Looking for public games..." : L"No public games found");
}
else if (chosen < lobby_browser.count)
{
struct p2p_listing const *game = &lobby_browser.games[chosen];
wchar_t gametype[P2P_LISTING_GAMETYPE_SIZE + 1];
text_to_wide(game->gametype, gametype, NUMBEROF(gametype));
usnprintf(text, NUMBEROF(text) - 1, L"%s: %d %s of %d%s", gametype, game->player_count,
game->player_count == 1 ? L"player" : L"players", game->maximum_player_count,
!game->open ? L", full or starting" : game->in_progress ? L", under way" : L"");
}
else
text[0] = 0;
text[NUMBEROF(text) - 1] = 0;
text_set(named(list, "ticker_player_info", 0), text);
text_set(named(list, "ticker_rules_info", 0), L"");
}
if (stats)
{
wchar_t label[ROW_TEXT_LENGTH];
long players = 0;
short index;
for (index = 0; index < lobby_browser.count; index++)
players += lobby_browser.games[index].player_count;
string_get(JOIN_GAME_LABELS, _join_game_label_players, label);
usnprintf(text, ROW_TEXT_LENGTH - 1, L"%s %ld", label, players);
text_set(named(stats, "player_count_label", 0), text);
string_get(JOIN_GAME_LABELS, _join_game_label_page, label);
usnprintf(text, ROW_TEXT_LENGTH - 1, L"%s %d/%d", label, chosen / BROWSER_ROWS + 1,
MAX(1, (lobby_browser.count + BROWSER_ROWS - 1) / BROWSER_ROWS));
text_set(named(stats, "page_count_label", 0), text);
string_get(JOIN_GAME_LABELS, _join_game_label_servers, label);
usnprintf(text, ROW_TEXT_LENGTH - 1, L"%s %d", label, lobby_browser.count);
text_set(named(stats, "server_count_label", 0), text);
}
browser_focus(list);
}
/* the server browser's rows and buttons: Refresh asks the hosts again; a
row (or Join) joins its game's invite; the A press posted once its host is
reached joins its game (or shows its lobby, if under way) */
static boolean lobby_browser_select(struct widget_instance *widget, short controller, short row,
boolean *widget_deleted)
{
struct p2p_listing const *game;
short index;
/* (the press posted: whatever has the focus) */
if (lobby_browser.ready)
{
struct advertised_game *found = lobby_browser_joined_game();
lobby_browser.joining = lobby_browser.ready = FALSE;
if (!found)
return campaign_fail();
if (advertised_in_progress(found))
{
csmemcpy(multiplayer.preview_key_id, found->key_id, sizeof(multiplayer.preview_key_id));
csmemcpy(multiplayer.preview_xnaddr, found->xnaddr, sizeof(multiplayer.preview_xnaddr));
return ui_widget_port_open(widget, PREVIEW_NAME, widget_deleted);
}
if (!found->open)
return campaign_fail();
if (!multiplayer_player(controller))
return FALSE;
return ui_widget_port_join(widget, found, LOBBY_NAME, widget_deleted);
}
if (strstr(widget->name, "button_refresh"))
{
p2p_lobby_refresh();
lobby_browser.begin_time = system_milliseconds();
ui_play_audio_feedback_sound(SOUND_FORWARD);
return TRUE;
}
if (row == NONE && !strstr(widget->name, "button_join"))
return TRUE;
if (lobby_browser.joining)
return TRUE;
index = row != NONE ? (short)(lobby_browser.first + row) : lobby_browser.chosen;
if (index >= lobby_browser.count)
return campaign_fail();
game = &lobby_browser.games[index];
if (!game->open || !config_boolean("network.online") || !p2p_join_invite(game->invite))
return campaign_fail();
csmemcpy(lobby_browser.identifier, game->identifier, sizeof(lobby_browser.identifier));
csmemcpy(lobby_browser.name, game->name, sizeof(lobby_browser.name));
lobby_browser.joining = TRUE;
lobby_browser.ready = FALSE;
lobby_browser.join_time = system_milliseconds();
lobby_browser.controller = controller;
ui_play_audio_feedback_sound(SOUND_FORWARD);
return TRUE;
}
/* "gamespy screen update": the games' rows (name, map, gametype, players),
the one chosen's line, the counts over the titles (the PC version's: its
players, page, servers) */
@@ -2379,6 +2892,11 @@ static void browser_update(struct widget_instance *list)
struct widget_instance *stats = list->parameters.list.extended_description;
short focused = browser_row_index(list->focused_child);
if (multiplayer.mode == _multiplayer_mode_server_browser)
{
lobby_browser_update(list);
return;
}
browser_games_read();
if (focused != NONE && focused < multiplayer.game_count)
multiplayer.game_chosen = focused;
@@ -2491,6 +3009,8 @@ static boolean browser_select(struct widget_instance *widget, struct event_recor
{
short row = browser_row_index(widget);
if (multiplayer.mode == _multiplayer_mode_server_browser)
return lobby_browser_select(widget, controller, row, widget_deleted);
if (row != NONE)
multiplayer.game_chosen = row;
if (row != NONE || strstr(widget->name, "button_join"))
@@ -2510,6 +3030,38 @@ static boolean browser_select(struct widget_instance *widget, struct event_recor
return TRUE;
}
/* "player profile save changes" (Settings' OK, the profile being edited):
saved if it has changes, as the Xbox's (the saving screen follows); if it
has none (the settings' own screens write theirs to config.toml as their OK
is chosen), editing ends and the previous screen comes back, as CANCEL
(the Xbox's called that a failure, and closed every screen) */
static boolean profile_save_changes(struct widget_instance *widget, boolean *widget_deleted)
{
if (player_ui_edit_profile_is_dirty())
{
if (player_ui_save_profile())
return TRUE;
platform_log("menus: could not save the profile's changes");
return campaign_fail();
}
player_ui_end_editing_profile();
ui_play_audio_feedback_sound(SOUND_FORWARD);
ui_widget_port_go_back(widget);
*widget_deleted = TRUE;
return TRUE;
}
/* "port pause end game" (the in-game pause menu's END GAME, the host's:
menu_tags.c's pause_patch): the game ends as its time limit would, its
players staying for the next (the carnage report, then the host's PICK GAME) */
static boolean pause_end_game(void)
{
if (!global_network_game_server_get() || !game_engine_running())
return campaign_fail();
game_engine_end_game();
return TRUE;
}
/* ---- the lobby: the game's players (up to the port's 128), its map and
gametype, the countdown */
@@ -2757,12 +3309,13 @@ static struct gametype_option const gametype_options[] =
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 10 } },
{ "item_options_starting_equipment_spinner", _option_flag, 0, FLAG(_game_variant_generic_starting_equipment_bit),
2, { 0, 1 } },
{ "map_weapons_spinner", _option_option_byte, OPTIONS_FIELD(no_map_weapons), 0, 2, { 0, 1 } },
/* (the loadout: the weapon set's, or each player's two weapons) */
{ "loadout_spinner", _option_option_byte, OPTIONS_FIELD(loadout), 0, 2, { _loadout_category, _loadout_custom } },
{ "primary_weapon_spinner", _option_option_byte, OPTIONS_FIELD(primary_weapon), 0, NUMBER_OF_LOADOUT_WEAPONS,
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 } },
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } },
{ "secondary_weapon_spinner", _option_option_byte, OPTIONS_FIELD(secondary_weapon), 0, NUMBER_OF_LOADOUT_WEAPONS,
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 } },
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } },
/* indicator options */
{ "indicator_options_radar display_spinner", _option_long, VARIANT_FIELD(universal_variant.goal_radar), 0, 3,
{ 0, 1, 2 } },
@@ -2813,7 +3366,9 @@ static struct gametype_option const gametype_options[] =
{ "kill_penalty_spinner", _option_byte, VARIANT_FIELD(game_engine_variant.slayer.kill_in_order), 0, 2, { 1, 0 } },
{ "kill_in_order_spinner", _option_byte, VARIANT_FIELD(game_engine_variant.slayer.no_kill_penalty), 0, 2,
{ 0, 1 } },
{ "kills_to_win_spinner", _option_long, VARIANT_FIELD(universal_variant.score_to_win), 0, 5, { 5, 10, 15, 25, 50 } },
/* (up to 500, for big games: tools/port_settings.py's STRING_INSERTS) */
{ "kills_to_win_spinner", _option_long, VARIANT_FIELD(universal_variant.score_to_win), 0, 11,
{ 5, 10, 15, 25, 50, 75, 100, 150, 200, 250, 500 } },
/* team options */
{ "friendly_fire_spinner", _option_short, OPTIONS_FIELD(friendly_fire), 0, 4,
{ _friendly_fire_off, _friendly_fire_on, _friendly_fire_shields_only, _friendly_fire_explosives_only } },
@@ -3475,6 +4030,18 @@ boolean pc_menu_event_function_invoke(
{
return preview_join(widget, controller, widget_deleted);
}
else if (!strcmp(name, "gamespy screen dispose"))
{
lobby_browser_end();
}
else if (!strcmp(name, "port pause end game"))
{
return pause_end_game();
}
else if (!strcmp(name, "player profile save changes"))
{
return profile_save_changes(widget, widget_deleted);
}
else if (!strcmp(name, "direct ip connect go"))
{
return direct_link_from_clipboard();
@@ -3550,6 +4117,18 @@ boolean pc_menu_event_function_invoke(
{
return profile_choose(controller);
}
else if (!strcmp(name, "port coop begin"))
{
return coop_begin(controller);
}
else if (!strcmp(name, "port coop player 2 list initialize"))
{
return coop_player2_list_initialize(widget);
}
else if (!strcmp(name, "port coop player 2"))
{
return coop_player2_choose(controller);
}
else if (!strcmp(name, "request del player profile"))
{
return profile_delete_request();
+344 -11
View File
@@ -23,6 +23,11 @@ game keeps its own menus.
The tag table cannot grow where it is (the tags' names follow it), so it
is copied, with ours after it; every existing tag keeps its index. All of it
is let go in scenario_tags_unload, before the next map's tags load.
A multiplayer map gets them too, for its in-game pause menu's SETTINGS
(pause_patch): there the map's tags they name that it has not (ui.map's
pictures, sounds and screens) are none, a missing picture drawn blank, and
no screen pauses the game (a network game's clock runs on).
*/
#include "cseries.h"
@@ -41,6 +46,9 @@ is let go in scenario_tags_unload, before the next map's tags load.
void platform_log(char const *format, ...);
char const *config_string(char const *name);
/* the game's network server (this machine hosts: network_game_globals.c) */
void *global_network_game_server_get(void);
/* cache_files.c's (port) */
void *cache_files_tag_instances(long *count);
void cache_files_set_tag_instances(void *instances, long count);
@@ -59,6 +67,13 @@ boolean pc_menu_tag(long tag_index);
/* ---------- constants */
#define PC_MENU_TAG_PREFIX "pc\\"
/* the multiplayer maps' widget collection, whose pause screens pause_patch
adds to */
#define MULTIPLAYER_COLLECTION "ui\\shell\\multiplayer"
/* the tags pause_patch adds: two buttons and their texts */
#define PAUSE_PATCH_TAGS 4
/* what a frame of ui.map's is drawn as in a game map, which has not got it */
#define BLANK_PNG "ce/port/blank__0.png"
#define UI_WIDGET_DEFINITION_TAG 'DeLa'
#define UNICODE_STRING_LIST_TAG 'ustr'
#define FONT_TAG 'font'
@@ -326,6 +341,8 @@ static char const *const port_function_names[] =
"mp level select", "mp profiles list initialize", "mp profiles list dispose", "mp profile set for game",
"port lobby preview join",
"port setup edit",
"port pause end game",
"port coop begin", "port coop player 2 list initialize", "port coop player 2",
/* (the gametype editor's: the Xbox's walk their rows by place, which the
PC version's screens changed) */
"mp profile begin editing", "mp profile save changes", "request del playlist profile", "final del playlist profile",
@@ -335,6 +352,9 @@ static char const *const port_function_names[] =
"mp profile init indicator opts", "mp profile set ctf rules", "mp profile set koth rules",
"mp profile set slayer rules", "mp profile set oddball rules", "mp profile set racing rules",
"mp profile set player options", "mp profile set item options", "mp profile set indicator opts",
/* (Settings' OK: the Xbox's fails when the profile has no changes, the
settings' screens having written theirs to config.toml) */
"player profile save changes",
};
/* the PC version's game data functions that the Xbox's have not, from
@@ -384,6 +404,9 @@ static struct
long *text_tags;
long *spinner_tags;
boolean failed;
/* a multiplayer map's build (the pause menu's SETTINGS): what the map
has not got is none, not a failure */
boolean game_map;
} build;
/* ---------- private code */
@@ -497,7 +520,8 @@ static long map_tag(long group_tag, char const *name, char const *file, long lin
{
long index = tag_loaded(group_tag, name);
if (index == NONE)
/* (a game map has few of ui.map's: those are none) */
if (index == NONE && !build.game_map)
problem(file, line, "the map has no tag", name);
return index;
}
@@ -698,10 +722,18 @@ static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index)
{
struct halo_menu_frame const *data = &build.menus->frames[source->first_frame + frame];
struct bitmap_data *bitmap = &bitmaps[frame];
char const *png = data->png;
long width = data->width, height = data->height;
/* (a frame of the map's: a copy of its bitmap, which the texture cache
loads from the map as its own) */
if (data->map)
loads from the map as its own; in a game map that has not got it,
nothing) */
if (data->map && build.game_map && tag_loaded(BITMAP_GROUP_TAG, data->map) == NONE)
{
png = BLANK_PNG;
width = height = PLACEHOLDER_SIZE;
}
else if (data->map)
{
long group_index = map_tag(BITMAP_GROUP_TAG, data->map, source->file, source->line);
struct bitmap_group *group_source;
@@ -720,9 +752,9 @@ static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index)
continue;
}
if (data->width <= 0 || data->height <= 0 || data->width > 2048 || data->height > 2048)
if (width <= 0 || height <= 0 || width > 2048 || height > 2048)
{
problem(source->file, source->line, "a frame is 1 to 2048 wide and high:", data->png);
problem(source->file, source->line, "a frame is 1 to 2048 wide and high:", png);
return group;
}
/* (its D3D texture is a small one, which its PNG is drawn for: the
@@ -741,13 +773,13 @@ static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index)
problem(source->file, source->line, "could not make the texture of bitmap", source->name);
return group;
}
bitmap->width = (short)data->width;
bitmap->height = (short)data->height;
bitmap->width = (short)width;
bitmap->height = (short)height;
menu_tags.bitmaps = realloc(menu_tags.bitmaps, (menu_tags.bitmap_count + 1) * sizeof(*menu_tags.bitmaps));
menu_tags.bitmaps[menu_tags.bitmap_count++] = bitmap;
/* (none without a renderer: debug.null_renderer) */
if (bitmap->hardware_format)
halo_menus_art_register(bitmap->hardware_format, data->png);
halo_menus_art_register(bitmap->hardware_format, png);
}
return group;
}
@@ -928,6 +960,9 @@ static void *widget_build(long widget_index)
definition->bounds.x1 = (short)(source->left + (source->has_width ? source->width : 640));
definition->flags = flags_parse(source->flags, widget_flag_names, NUMBEROF(widget_flag_names),
source->file, source->line);
/* (in a network game the clock runs on) */
if (build.game_map)
definition->flags &= ~FLAG(1); /* pause_game */
definition->milliseconds_to_auto_close = source->auto_close;
definition->auto_close_fade_time = source->auto_close_fade;
reference_clear(&definition->background_bitmap, BITMAP_GROUP_TAG);
@@ -1250,6 +1285,281 @@ static void menu_tags_release(void)
/* ---------- public code */
/* ---------- the in-game pause menu (a multiplayer map's)
Its list (RESUME GAME, LEAVE GAME; a custom map's own buttons too) gets
SETTINGS, the menus' settings (as the main menu's SETTINGS opens them), and
for the host END GAME, which ends the game as its time limit would: its
players stay, and the host picks the next map after the carnage report.
They are copies of the map's LEAVE GAME button (its look), put before it.
The map's pause screens move to fit them: on the Xbox's own (its box of
pausebox2 pieces), the box is the port's taller one (pause/pausebox_*:
tools/port_settings.py), centred where the old one was; on a custom map's,
what is below the list moves down. */
#define PAUSE_SETTINGS_SCREEN "main_menu/settings_select/player_setup/player_profile_edit/player_profile_edit_screen"
#define PAUSE_BUTTON_SPACING 35
#define PAUSE_BOX_FIRST_BUTTONS 3
static long ui_function_named(char const *name)
{
long index;
for (index = 0; index < 512; index++)
{
char const *function = ui_widget_event_handler_function_name(index);
if (!function)
break;
if (!strcmp(function, name))
return index;
}
return NONE;
}
static boolean tag_name_ends(long tag_index, char const *end)
{
char const *name = tag_index == NONE ? NULL : tag_get_name(tag_index);
size_t length = name ? strlen(name) : 0, end_length = strlen(end);
return name && length >= end_length && !strcmp(name + length - end_length, end);
}
/* the index of the list's button that runs the function (LEAVE GAME's),
else NONE */
static long pause_quit_button(struct ui_widget_definition const *list, long quit_function)
{
struct ui_widget_child_reference const *children = list->child_widgets.address;
long child;
for (child = 0; child < list->child_widgets.count; child++)
{
struct ui_widget_definition const *button;
struct ui_widget_event_handler_reference const *handlers;
long handler;
if (children[child].widget_tag.index == NONE)
continue;
button = tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index);
handlers = button->event_handlers.address;
for (handler = 0; handler < button->event_handlers.count; handler++)
{
if (handlers[handler].function == quit_function &&
TEST_FLAG(handlers[handler].flags, _event_handler_run_function_bit))
{
return child;
}
}
}
return NONE;
}
/* a button like the model (its look), its own text, each of the model's
events (its A and Start) running the function and opening the widget (if
not NONE) */
static void *pause_button(struct cache_file_tag_instance *instances, struct ui_widget_definition const *model,
char const *name, char const *text, long function, long widget_tag)
{
struct ui_widget_definition *button = allocate(sizeof(*button));
struct ui_widget_event_handler_reference *handlers =
allocate(model->event_handlers.count * sizeof(struct ui_widget_event_handler_reference));
long text_tag = next_tag(), handler;
if (!button || !handlers)
return NULL;
memcpy(button, model, sizeof(*button));
instance_set(instances, UNICODE_STRING_LIST_TAG, text_tag, name, " text", string_list_build(&text, 1));
reference_set(&button->text_label_string_list, UNICODE_STRING_LIST_TAG, text_tag);
button->string_list_index = 0;
button->game_data_inputs.count = 0;
button->game_data_inputs.address = NULL;
/* (the game's memcpy asserts on a NULL source, even for nothing) */
if (model->event_handlers.count)
memcpy(handlers, model->event_handlers.address, model->event_handlers.count * sizeof(*handlers));
for (handler = 0; handler < model->event_handlers.count; handler++)
{
handlers[handler].flags = FLAG(_event_handler_run_function_bit);
handlers[handler].function = (short)function;
handlers[handler].script[0] = 0;
reference_set(&handlers[handler].widget_tag, UI_WIDGET_DEFINITION_TAG, widget_tag);
if (widget_tag != NONE)
{
handlers[handler].flags |= FLAG(_event_handler_open_widget_bit) |
FLAG(_event_handler_try_to_branch_on_failure_bit);
}
}
button->event_handlers.address = handlers;
return button;
}
/* the list's buttons: SETTINGS (and the host's END GAME) put before LEAVE
GAME (quit); returns how many were added */
static long pause_list_patch(struct cache_file_tag_instance *instances, struct ui_widget_definition *list, long quit,
boolean host)
{
struct ui_widget_child_reference *children = list->child_widgets.address;
long count = list->child_widgets.count, added = host ? 2 : 1, child;
struct ui_widget_child_reference *grown = allocate((count + added) * sizeof(struct ui_widget_child_reference));
struct ui_widget_definition const *model = tag_get(UI_WIDGET_DEFINITION_TAG, children[quit].widget_tag.index);
long settings_widget = widget_named(PAUSE_SETTINGS_SCREEN);
short spacing = count >= 2 ? (short)(children[count - 1].vertical_offset - children[count - 2].vertical_offset) :
PAUSE_BUTTON_SPACING;
void *buttons[2];
long tags[2];
if (!grown || settings_widget == NONE || spacing <= 0)
return 0;
tags[0] = next_tag();
buttons[0] = pause_button(instances, model, "pause/settings_button", "SETTINGS",
function_index("profile set edit begin", "pause", 0), build.widget_tags[settings_widget]);
tags[1] = host ? next_tag() : NONE;
buttons[1] = host ? pause_button(instances, model, "pause/end_game_button", "END GAME",
function_index("port pause end game", "pause", 0), NONE) : NULL;
if (!buttons[0] || (host && !buttons[1]))
return 0;
for (child = 0; child < added; child++)
{
instance_set(instances, UI_WIDGET_DEFINITION_TAG, tags[child],
child ? "pause/end_game_button" : "pause/settings_button", "", buttons[child]);
}
if (quit)
memcpy(grown, children, quit * sizeof(*grown));
for (child = 0; child < added; child++)
{
grown[quit + child] = children[quit];
reference_set(&grown[quit + child].widget_tag, UI_WIDGET_DEFINITION_TAG, tags[child]);
grown[quit + child].vertical_offset = (short)(children[quit].vertical_offset + child * spacing);
}
for (child = quit; child < count; child++)
{
grown[child + added] = children[child];
grown[child + added].vertical_offset = (short)(children[child].vertical_offset + added * spacing);
}
list->child_widgets.address = grown;
list->child_widgets.count = count + added;
/* (the list draws within its bounds) */
list->bounds.y1 = (short)(list->bounds.y1 + added * spacing);
return added;
}
/* the Xbox's own box: a widget of the three pausebox2 pieces */
static boolean pause_box_stock(struct ui_widget_definition const *box)
{
struct ui_widget_child_reference const *pieces = box->child_widgets.address;
return box->child_widgets.count == 3 && tag_name_ends(pieces[0].widget_tag.index, "\\pausebox2_left") &&
tag_name_ends(pieces[1].widget_tag.index, "\\pausebox2_left_center") &&
tag_name_ends(pieces[2].widget_tag.index, "\\pausebox2_right");
}
/* the stock box's pieces drawn with the port's taller box: its frame for
the number of buttons */
static void pause_box_redraw(struct ui_widget_definition const *box, long buttons)
{
static char const *const names[] = { "pause/pausebox_left", "pause/pausebox_center", "pause/pausebox_right" };
struct ui_widget_child_reference const *pieces = box->child_widgets.address;
long piece;
for (piece = 0; piece < 3; piece++)
{
struct ui_widget_definition *definition = tag_get(UI_WIDGET_DEFINITION_TAG, pieces[piece].widget_tag.index);
long bitmap = find(names[piece], build.menus->bitmap_count, bitmap_name);
struct bitmap_group *group;
struct bitmap_group_sequence *sequence;
if (bitmap == NONE)
continue;
group = bitmap_group_get(build.bitmap_tags[bitmap]);
sequence = group->sequences.address;
/* (one frame: the one for the number of buttons) */
sequence->first_bitmap_index = (short)PIN(buttons - PAUSE_BOX_FIRST_BUTTONS, 0, group->bitmaps.count - 1);
sequence->bitmap_count = 1;
reference_set(&definition->background_bitmap, BITMAP_GROUP_TAG, build.bitmap_tags[bitmap]);
definition->bounds.y1 = (short)(definition->bounds.y0 + 256);
}
}
static void pause_patch(struct cache_file_tag_instance *instances)
{
long collection = tag_loaded('Soul', MULTIPLAYER_COLLECTION);
long quit_function = ui_function_named("mp game player quit");
boolean host = global_network_game_server_get() != NULL;
struct tag_block const *screens;
long patched_list = NONE, added = 0, buttons = 0, screen;
boolean box_redrawn = FALSE;
if (collection == NONE || quit_function == NONE)
return;
screens = tag_get('Soul', collection);
for (screen = 0; screen < screens->count; screen++)
{
long screen_tag = ((struct tag_reference const *)screens->address)[screen].index;
struct ui_widget_definition *definition;
struct ui_widget_child_reference *children;
long child, list_child = NONE, box_child = NONE;
short grow, list_top;
if (screen_tag == NONE)
continue;
definition = tag_get(UI_WIDGET_DEFINITION_TAG, screen_tag);
children = definition->child_widgets.address;
for (child = 0; child < definition->child_widgets.count && list_child == NONE; child++)
{
struct ui_widget_definition *list;
long quit;
if (children[child].widget_tag.index == NONE)
continue;
list = tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index);
if (list->type != _widget_type_column_list)
continue;
/* (one list, which the screens share: patched once) */
if (children[child].widget_tag.index == patched_list)
{
list_child = child;
continue;
}
quit = pause_quit_button(list, quit_function);
if (quit == NONE || patched_list != NONE)
continue;
added = pause_list_patch(instances, list, quit, host);
if (!added)
return;
buttons = list->child_widgets.count;
patched_list = children[child].widget_tag.index;
list_child = child;
}
if (list_child == NONE)
continue;
for (child = 0; child < definition->child_widgets.count; child++)
{
if (child != list_child && children[child].widget_tag.index != NONE &&
pause_box_stock(tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index)))
{
box_child = child;
}
}
/* the stock box: taller, centred where it was, the list with it, and
what is below it moved down; else only what is below the list */
grow = (short)(added * PAUSE_BUTTON_SPACING);
list_top = children[list_child].vertical_offset;
for (child = 0; child < definition->child_widgets.count; child++)
{
if (box_child != NONE && (child == box_child || child == list_child))
children[child].vertical_offset -= grow / 2;
else if (children[child].vertical_offset > list_top)
children[child].vertical_offset += box_child != NONE ? grow - grow / 2 : grow;
}
if (box_child != NONE && !box_redrawn)
{
pause_box_redraw(tag_get(UI_WIDGET_DEFINITION_TAG, children[box_child].widget_tag.index), buttons);
box_redrawn = TRUE;
}
}
if (patched_list != NONE)
platform_log("menus: the pause menu has SETTINGS%s", host ? " and END GAME" : "");
}
void menu_tags_loaded(
char const *map_name)
{
@@ -1257,8 +1567,14 @@ void menu_tags_loaded(
struct cache_file_tag_instance *instances;
long widget_count, own_lists = 0, total, index;
if (strcmp(map_name, "ui") || strcmp(config_string("display.menus"), "pc"))
boolean game_map = strcmp(map_name, "ui") != 0;
/* (ui.map, and a multiplayer map: its pause menu's SETTINGS) */
if ((game_map && tag_loaded('Soul', MULTIPLAYER_COLLECTION) == NONE) ||
strcmp(config_string("display.menus"), "pc"))
{
return;
}
/* (in VR, the Xbox's: the VR port's join codes, game list, host options
and keyboard pointer are built into its screens) */
if (halo_vr_active())
@@ -1268,6 +1584,7 @@ void menu_tags_loaded(
return;
memset(&build, 0, sizeof(build));
build.menus = menus;
build.game_map = game_map;
widget_count = menus->widget_count;
build.widget_tags = malloc((widget_count + 1) * sizeof(long));
build.text_tags = malloc((widget_count + 1) * sizeof(long));
@@ -1282,7 +1599,8 @@ void menu_tags_loaded(
problem(menus->widgets[index].file, menus->widgets[index].line, "two widgets are named",
menus->widgets[index].name);
}
total = widget_count + own_lists + menus->string_list_count + menus->bitmap_count;
total = widget_count + own_lists + menus->string_list_count + menus->bitmap_count +
(game_map ? PAUSE_PATCH_TAGS : 0);
instances = build.failed ? NULL : instances_grow(total, &build.first_index, &build.first_salt);
if (!instances)
goto failed;
@@ -1332,12 +1650,27 @@ void menu_tags_loaded(
}
if (build.failed)
goto failed;
/* (pause_patch's, until it makes them: nothing a name finds) */
for (index = build.first_index + build.next; index < build.first_index + total; index++)
{
instances[index].group_tag = NONE;
instances[index].tag_index = NONE;
instances[index].name = "";
}
cache_files_set_tag_instances(instances, build.first_index + total);
for (index = 0; index < widget_count && !build.failed; index++)
instances[DATUM_INDEX_TO_ABSOLUTE_INDEX(build.widget_tags[index])].base_address = widget_build(index);
if (build.failed)
goto failed;
if (widget_named(menus->root) != NONE)
if (game_map)
{
pause_patch(instances);
if (build.failed)
goto failed;
/* (those it made) */
cache_files_set_tag_instances(instances, build.first_index + build.next);
}
else if (widget_named(menus->root) != NONE)
{
snprintf(menu_tags.root, sizeof(menu_tags.root), "%s%s", PC_MENU_TAG_PREFIX, menus->root);
for (index = 0; menu_tags.root[index]; index++)
+14
View File
@@ -343,6 +343,10 @@ static struct
long times[MAXIMUM_RECENT_WEAPONS];
long grenade_times[NUMBER_OF_UNIT_GRENADE_TYPES];
long driven_time;
/* (the blow of the player's unit with no weapon: units.c's
unit_unarmed_melee_damage) */
long unarmed_melee_damage_index;
long unarmed_time;
real hit_seconds;
long hit_seconds_time;
} damage_players[MAXIMUM_TRACKED_PLAYERS];
@@ -1127,6 +1131,13 @@ static real distributed_player_deals(
*kinds |= grenade_kinds;
}
}
/* (a blow with no weapon: the unit's own, units.c) */
if (damage_players[player_index].unarmed_melee_damage_index == damage_index && damage_index != NONE &&
game_time_get() - damage_players[player_index].unarmed_time <= RECENT_WEAPON_TICKS)
{
rate = MAX(rate, MELEE_HITS_PER_SECOND);
SET_FLAG(*kinds, _damage_source_melee_bit, TRUE);
}
/* (a vehicle's collision is its driver's, physics.c; the damage it deals
a unit it kills is no one's) */
if (game_time_get() - damage_players[player_index].driven_time <= RECENT_WEAPON_TICKS &&
@@ -1409,6 +1420,8 @@ static void distributed_note_weapons(
if (player_index >= MAXIMUM_TRACKED_PLAYERS || unit_index == NONE)
continue;
damage_players[player_index].unarmed_melee_damage_index = unit_unarmed_melee_damage(unit_index);
damage_players[player_index].unarmed_time = game_time_get();
units[0] = unit_index;
units[1] = object_get(unit_index)->object.parent_object_index;
/* (a vehicle's collisions are its driver's, physics.c) */
@@ -2107,6 +2120,7 @@ void network_damage_new_game(
for (index = 0; index < MAXIMUM_RECENT_WEAPONS; index++)
damage_players[player_index].definition_indices[index] = NONE;
damage_players[player_index].unarmed_melee_damage_index = NONE;
for (index = 0; index < NUMBER_OF_UNIT_GRENADE_TYPES; index++)
damage_players[player_index].grenade_times[index] = -RECENT_WEAPON_TICKS - 1;
damage_players[player_index].driven_time = -RECENT_WEAPON_TICKS - 1;
+2
View File
@@ -214,6 +214,8 @@ void network_objects_handle_changes(void const *entries, short count);
void network_objects_handle_synchronized(void);
void network_objects_handle_states(void const *entries, short count);
void network_objects_handle_inventories(void const *entries, short count);
/* (a client) its own player picked up the weapon, to ready once its unit has it */
void network_objects_client_picked_up_weapon(short local_player_index, long unit_index, long definition_index);
void network_objects_handle_vehicle_prediction(long machine_index, void const *entries, short count);
/* (the host) the vehicle predictions come in since the last tick, taken */
void network_objects_apply_vehicle_predictions(void);
+75
View File
@@ -175,6 +175,9 @@ enum
/* ... the round trip without one measured, and the most */
DEFAULT_OWN_ROUND_TRIP_TICKS = 6,
MAXIMUM_OWN_ROUND_TRIP_TICKS = 60,
/* ... how long the weapon the host says it picked up may take to reach
its unit, to be readied (the host's messages are not in step) */
PICKED_UP_WEAPON_TICKS = 3 * TICKS_PER_SECOND,
/* the most grenades of a kind a unit carries, as the host says (a dead
unit drops each) */
MAXIMUM_INVENTORY_GRENADES = 16,
@@ -422,6 +425,14 @@ static struct distributed_own_inventory
long fired_times[MAXIMUM_WEAPONS_PER_UNIT];
long weapon_times[MAXIMUM_WEAPONS_PER_UNIT];
} objects_client_own_inventories[MAXIMUM_LOCAL_PLAYERS];
/* ... the weapon each of them last picked up (the host said so), its unit
and when, until it is readied (definition NONE: none) */
static struct distributed_picked_up_weapon
{
long unit_index;
long definition_index;
long time;
} objects_client_picked_up_weapons[MAXIMUM_LOCAL_PLAYERS];
/* ... whether it failed to make one of the host's objects since it last
asked for them (it says so when it asks again) */
static boolean objects_client_ask_again;
@@ -2377,6 +2388,68 @@ static void distributed_client_apply_inventory(
distributed_client_note_own_inventory(own, inventory->unit_index, FALSE);
}
/* a client's own player picked up a weapon (network_player_show_pickup) */
void network_objects_client_picked_up_weapon(
short local_player_index,
long unit_index,
long definition_index)
{
struct distributed_picked_up_weapon *picked_up;
if (local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS)
return;
picked_up = &objects_client_picked_up_weapons[local_player_index];
picked_up->unit_index = unit_index;
picked_up->definition_index = definition_index;
picked_up->time = game_time_get();
}
/* the weapons its own players picked up readied once they have them, as
unit_add_weapon_to_inventory readies a pickup where it decides it (not
while firing): the client chooses its players' weapons in hand
(distributed_client_apply_inventory), and the host follows */
static void distributed_client_ready_picked_up_weapons(
void)
{
short local_player_index;
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
{
struct distributed_picked_up_weapon *picked_up = &objects_client_picked_up_weapons[local_player_index];
long player_index = local_player_get_player_index(local_player_index);
struct player_datum *player = player_index != NONE ? player_try_and_get(player_index) : NULL;
long unit_index = distributed_living_unit(player);
struct unit_datum *unit;
short weapon_slot;
if (picked_up->definition_index == NONE)
continue;
if (unit_index == NONE || unit_index != picked_up->unit_index ||
game_time_get() - picked_up->time > PICKED_UP_WEAPON_TICKS)
{
picked_up->definition_index = NONE;
continue;
}
unit = unit_get(unit_index);
for (weapon_slot = 0; weapon_slot < MAXIMUM_WEAPONS_PER_UNIT; weapon_slot++)
{
long weapon_index = unit->unit.weapon_object_indices[weapon_slot];
struct weapon_datum *weapon = weapon_index != NONE ? weapon_try_and_get(weapon_index) : NULL;
if (weapon && weapon->definition_index == picked_up->definition_index)
break;
}
if (weapon_slot == MAXIMUM_WEAPONS_PER_UNIT)
continue;
if (!TEST_FLAG(unit->unit.control_flags, _unit_control_weapon_primary_trigger_bit))
{
player_control_set_desired_weapon(unit_index, weapon_slot);
unit->unit.desired_weapon_index = weapon_slot;
}
picked_up->definition_index = NONE;
}
}
void network_objects_handle_inventories(
void const *entries,
short count)
@@ -2549,6 +2622,7 @@ void network_objects_client_tick(
/* (too many to look at one by one later: all of them) */
else if (objects_client_new_object_count >= MAXIMUM_CLIENT_NEW_OBJECTS)
objects_client_check_all = TRUE;
distributed_client_ready_picked_up_weapons();
distributed_client_note_own_inventories();
distributed_client_send_vehicles();
/* (who it is, as its Discord told it: once its ready went, which makes
@@ -2606,6 +2680,7 @@ void network_objects_new_game(
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
{
objects_client_own_inventories[local_player_index].unit_index = NONE;
objects_client_picked_up_weapons[local_player_index].definition_index = NONE;
for (index = 0; index < OWN_VEHICLE_POSITION_TICKS; index++)
{
objects_client_own_vehicles[local_player_index][index].time = NONE;
+3
View File
@@ -201,6 +201,9 @@ driver's hands on it as drawn (world units, from the game's camera), or
NULL; the player's own hands turn it. And whether they hold it. */
void halo_vr_set_wheel(short local_player_index, const float left[3], const float right[3]);
int halo_vr_wheel_held(void);
/* the controllers' layout (HALO_XR_LAYOUT_*: 0 the Quest's, 1 the Steam
Frame's), for what the game tells the player */
int halo_vr_controller_layout(void);
/* whether a seated player's view stays in first person (director.c) */
int halo_vr_vehicle_first_person(void);
/* VR test aid (debug.vehicle_spawn): both sticks held in for a moment asks
+12 -2
View File
@@ -2863,7 +2863,15 @@ static struct program_entry *prepare_draw(BOOL immediate)
key.count_samples = device.visibility_test_active && xgpu_capabilities.atomic_counters;
#endif
entry = program_get(vertex_shader_get(program, immediate, targets_multiview), fragment_shader_get(&key));
entry = program_get(vertex_shader_get(program, immediate,
#ifdef HALO_ANDROID
/* the bound targets are the layers (Android's GL_OVR_multiview2);
on the desktop there are no layers to stand in for the back buffer */
targets_multiview
#else
FALSE
#endif
), fragment_shader_get(&key));
if (!entry)
{
stats.skipped_link++;
@@ -4023,7 +4031,9 @@ void d3d8_clear_back_buffer(float red, float green, float blue, float alpha)
glDepthMask(GL_TRUE);
glStencilMask(0xff);
glClearColor(red, green, blue, alpha);
glClearDepthf(1.0f);
/* (glClearDepth: on Android the define above maps it to glClearDepthf,
ES's float entry point) */
glClearDepth(1.0f);
glClearStencil(0);
glClear(GL_COLOR_BUFFER_BIT | (depth_buffer ? GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT : 0));
xgpu_gl_state_invalidate();
+135 -21
View File
@@ -274,7 +274,7 @@ struct stun_server
struct p2p_candidate mapped;
};
static pthread_mutex_t p2p_lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t p2p_lock = PTHREAD_MUTEX_INITIALIZER;
static struct
{
@@ -316,6 +316,11 @@ static struct
int hosting_lan_only;
int has_token;
unsigned char token[P2P_TOKEN_SIZE];
/* whether the server browser's exit hook is set (lobby_quit) */
int lobby_quit_registered;
/* whether the server browser listed this token (p2p_lobby.c): going
private makes a new one */
int token_listed;
char invite[P2P_LINK_SIZE];
int invite_copied;
/* the game's players and its most (p2p_set_game_player_counts; 0: not
@@ -355,7 +360,11 @@ p2p.local_address) */
static unsigned short proxy_by_port[65536];
static unsigned char identifier[P2P_IDENTIFIER_SIZE];
/* this run's X25519 keys, which the identifier comes from */
/* this run's X25519 keys, which the identifier comes from: from an Ed25519
seed, whose key signs the listing of a public game (p2p_lobby.c), so that a
listing's key is also the invite's */
static unsigned char seed[P2P_SEED_SIZE];
static unsigned char signing_key[P2P_KEY_SIZE];
static unsigned char secret_key[P2P_KEY_SIZE];
static unsigned char public_key[P2P_KEY_SIZE];
static int has_identifier;
@@ -503,7 +512,8 @@ const unsigned char *p2p_identifier(void)
pthread_mutex_lock(&identifier_lock);
if (!has_identifier)
{
posix_random_bytes(secret_key, sizeof(secret_key));
posix_random_bytes(seed, sizeof(seed));
p2p_ed25519_public(seed, signing_key, secret_key);
p2p_x25519(public_key, secret_key, NULL);
p2p_identifier_for(public_key, identifier);
has_identifier = 1;
@@ -542,6 +552,18 @@ const unsigned char *p2p_public_key(void)
return public_key;
}
const unsigned char *p2p_signing_key(void)
{
p2p_identifier();
return signing_key;
}
void p2p_sign(const void *message, int size, unsigned char *signature)
{
p2p_identifier();
p2p_ed25519_sign(seed, signing_key, message, size, signature);
}
int p2p_shared_secret(const unsigned char *key, unsigned char *shared)
{
static const unsigned char zero[P2P_KEY_SIZE];
@@ -2362,6 +2384,8 @@ void p2p_set_public_game(int public_game)
pthread_mutex_lock(&p2p_lock);
p2p_list_set_public(public_game);
pthread_mutex_unlock(&p2p_lock);
/* (and in the server browser of every build: p2p_lobby.c) */
p2p_set_hosting_public(public_game);
}
int p2p_public_game(void)
@@ -2382,13 +2406,47 @@ void p2p_set_game_description(const char *name, const char *map, const char *mod
pthread_mutex_unlock(&p2p_lock);
}
/* the server browser's games first (every build lists its public games
there: PC, phones, VR), then the VR port's own list's that it has not */
int p2p_public_games(struct p2p_public_game *games, int maximum)
{
int count;
struct p2p_listing listings[32];
struct p2p_public_game own[32];
int listing_count;
int own_count;
int count = 0;
int index;
p2p_lobby_browse(1);
listing_count = p2p_lobby_games(listings, 32);
for (index = 0; index < listing_count && count < maximum; index++)
{
struct p2p_public_game *game = &games[count++];
memset(game, 0, sizeof(*game));
snprintf(game->name, sizeof(game->name), "%s", listings[index].name);
snprintf(game->map, sizeof(game->map), "%s", listings[index].map);
snprintf(game->mode, sizeof(game->mode), "%s", listings[index].gametype);
snprintf(game->invite, sizeof(game->invite), "%s", listings[index].invite);
game->players = listings[index].player_count;
game->maximum_players = listings[index].maximum_player_count;
}
pthread_mutex_lock(&p2p_lock);
count = p2p.running ? p2p_list_games(games, maximum) : 0;
own_count = p2p.running ? p2p_list_games(own, 32) : 0;
pthread_mutex_unlock(&p2p_lock);
for (index = 0; index < own_count && count < maximum; index++)
{
int other;
int listed = 0;
for (other = 0; other < listing_count; other++)
{
if (!strcmp(own[index].name, listings[other].name) && !strcmp(own[index].map, listings[other].map))
listed = 1;
}
if (!listed)
games[count++] = own[index];
}
return count;
}
@@ -2455,27 +2513,55 @@ static int connected_player_count(void)
return count;
}
/* a new invite (a new token) */
static void make_invite(void)
{
unsigned char bytes[P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE];
char text[2 * (P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE) + 1];
posix_random_bytes(p2p.token, sizeof(p2p.token));
p2p.has_token = 1;
p2p.token_listed = 0;
p2p_key_hash(p2p_public_key(), bytes);
memcpy(bytes + P2P_KEY_HASH_SIZE, p2p.token, P2P_TOKEN_SIZE);
p2p_hex(bytes, sizeof(bytes), text);
snprintf(p2p.invite, sizeof(p2p.invite), "halo://join/%s", text);
}
void p2p_new_invite_if_listed(void)
{
if (!p2p.has_token || !p2p.token_listed)
return;
make_invite();
platform_log("Internet play: the game is private now, with a new invite: %s", p2p.invite);
if (p2p.hosting)
{
p2p_signal_host(p2p.token);
memcpy(p2p.clipboard, p2p.invite, sizeof(p2p.clipboard));
p2p.has_clipboard = 1;
/* (Discord is told the new one) */
p2p.reported_player_count = -1;
}
}
/* the hosted game's players, as the game says; else the host and the
machines the tunnel reaches */
static void hosted_player_counts(int *count, int *maximum)
{
*count = p2p.game_player_maximum > 0 ? p2p.game_player_count : connected_player_count() + 1;
*maximum = p2p.game_player_maximum > 0 ? p2p.game_player_maximum : P2P_MAXIMUM_PEERS + 1;
}
static void update_hosting(void)
{
int want = p2p.hosting_socket >= 0 && !p2p.hosting_lan_only;
if (want && !p2p.hosting)
{
char text[2 * (P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE) + 1];
/* one invite for the whole run, so a link keeps working from game
to game */
if (!p2p.has_token)
{
unsigned char bytes[P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE];
posix_random_bytes(p2p.token, sizeof(p2p.token));
p2p.has_token = 1;
p2p_key_hash(p2p_public_key(), bytes);
memcpy(bytes + P2P_KEY_HASH_SIZE, p2p.token, P2P_TOKEN_SIZE);
p2p_hex(bytes, sizeof(bytes), text);
snprintf(p2p.invite, sizeof(p2p.invite), "halo://join/%s", text);
}
make_invite();
p2p.hosting = 1;
p2p.stun_started = 1;
p2p_signal_start();
@@ -2507,11 +2593,9 @@ static void update_hosting(void)
}
if (p2p.hosting)
{
/* the game's players, as the game says; else the host and the
machines the tunnel reaches */
int count = p2p.game_player_maximum > 0 ? p2p.game_player_count : connected_player_count() + 1;
int maximum = p2p.game_player_maximum > 0 ? p2p.game_player_maximum : P2P_MAXIMUM_PEERS + 1;
int count, maximum;
hosted_player_counts(&count, &maximum);
if (count != p2p.reported_player_count || maximum != p2p.reported_player_maximum)
{
p2p.reported_player_count = count;
@@ -2601,6 +2685,15 @@ static void *upnp_thread(void *unused)
return NULL;
}
/* quitting: a game listed in the server browser is taken out of it at once
(not when its listing lapses), as when it stops being hosted */
static void lobby_quit(void)
{
pthread_mutex_lock(&p2p_lock);
p2p_lobby_quit();
pthread_mutex_unlock(&p2p_lock);
}
/* the game exits: the router forwards the port no longer (after a request
under way, which may forward one, if it ends soon) */
static void upnp_release(void)
@@ -2637,6 +2730,9 @@ static int upnp_needed(void)
if (p2p.joining && elapsed(p2p.join_time, UPNP_JOIN_DELAY))
return 1;
/* a game in the server browser: at once (more joiners get through) */
if (p2p_lobby_listed())
return 1;
for (index = 0; index < P2P_MAXIMUM_PEERS; index++)
{
struct peer const *peer = &p2p.peers[index];
@@ -3013,6 +3109,24 @@ static void *p2p_thread(void *unused)
expire_proxies();
stun_update();
update_hosting();
/* the server browser (p2p_lobby.c): signalling while browsing too */
if (p2p_lobby_browsing())
p2p_signal_start();
{
int count, maximum;
hosted_player_counts(&count, &maximum);
p2p_lobby_update(p2p.hosting && p2p.has_token ? p2p.token : NULL, count, maximum);
if (p2p_lobby_listed())
{
p2p.token_listed = 1;
if (!p2p.lobby_quit_registered)
{
p2p.lobby_quit_registered = 1;
atexit(lobby_quit);
}
}
}
update_joining();
update_upnp();
p2p_discord_update();
+50
View File
@@ -74,6 +74,8 @@ struct p2p_public_game
char code[12];
int players;
int maximum_players;
/* (a game of the server browser's: no code, its invite) */
char invite[77];
};
void p2p_set_public_game(int public_game);
int p2p_public_game(void);
@@ -140,6 +142,54 @@ are shown). The game's server calls it as they change (calling it with the
same counts again costs little) */
void p2p_set_game_player_counts(int count, int maximum);
/* ---- the server browser's public games (p2p_lobby.c) */
/* whether the game this machine hosts for the internet (not LAN) is listed
in everyone's server browser: Create Game's PUBLIC; a private game is
reached only by its invite. Going private makes a new invite, if the old one
was listed */
void p2p_set_hosting_public(int public);
/* the hosted game's details as listed (printable ASCII is kept; NULL leaves
one as it was): the game's server calls it as they change (calling it with
the same again costs little) */
void p2p_set_game_listing(const char *name, const char *map, const char *gametype, int engine_type, int open,
int in_progress, int has_teams);
/* the server browser: while on, the public games are gathered (asked for
when turned on, and by p2p_lobby_refresh) */
void p2p_lobby_browse(int on);
void p2p_lobby_refresh(void);
enum
{
P2P_LISTING_NAME_SIZE = 32,
P2P_LISTING_MAP_SIZE = 32,
P2P_LISTING_GAMETYPE_SIZE = 24,
/* an invite link's text (P2P_LINK_SIZE) */
P2P_LISTING_INVITE_SIZE = 77,
/* (struct p2p_public_game's invite is as large) */
};
struct p2p_listing
{
char invite[P2P_LISTING_INVITE_SIZE];
/* the host's (its XNADDR's abEnet once reached) */
unsigned char identifier[6];
char name[P2P_LISTING_NAME_SIZE + 1];
char map[P2P_LISTING_MAP_SIZE + 1];
char gametype[P2P_LISTING_GAMETYPE_SIZE + 1];
unsigned char player_count, maximum_player_count, engine_type;
unsigned char open, in_progress, has_teams;
/* joining it failed this run (p2p_lobby_mark_failed) */
unsigned char failed;
/* milliseconds, -1 if not known */
short ping;
};
/* a copy of the public games found, in the order shown (the most players
first; then those not failed, the open ones, by name); returns their count */
int p2p_lobby_games(struct p2p_listing *games, int maximum_count);
/* a game the browser could not join: kept, marked failed, for this run */
void p2p_lobby_mark_failed(const unsigned char *identifier);
/* the sizes of a Discord user's id and name as kept (with their end), and
the text kept of either as told: only the characters allowed (digits in an
id; letters, digits, "_", "." and "-" in a name), no longer than that */
+73 -4
View File
@@ -6,16 +6,20 @@ private (p2p.c): SHA-256 (FIPS 180-4) and HMAC-SHA256 (RFC 2104), from
which an invite's topics and keys are derived; ChaCha20-Poly1305 (RFC 8439)
with additional data, which seals the tunnel's packets (a counter for their
nonce) and, with a random nonce sent ahead of the ciphertext, signalling's
messages; and X25519 (RFC 7748), with which two machines agree on their
tunnel's keys without sending them. The public brokers carry sealed
messages, so only holders of the invite read them; the tunnel's packets are
sealed with keys only its two machines have.
messages; X25519 (RFC 7748), with which two machines agree on their
tunnel's keys without sending them; and Ed25519 (RFC 8032, Monocypher's),
with which a host signs its public game's listing (p2p_lobby.c). The public
brokers carry sealed messages, so only holders of the invite read them; the
tunnel's packets are sealed with keys only its two machines have.
*/
#include "platform.h"
#include "posix.h"
#include "p2p_internal.h"
#include "monocypher.h"
#include "monocypher-ed25519.h"
#include <string.h>
/* ---------- SHA-256 */
@@ -590,3 +594,68 @@ void p2p_x25519(unsigned char *result, const unsigned char *scalar, const unsign
field_multiply(a, a, c);
field_pack(result, a);
}
/* ---------- Ed25519 (RFC 8032, with SHA-512), from Monocypher
(port/third_party/monocypher): a run's key, from which its X25519 key also
comes, signs the listing of a public game (p2p_lobby.c) */
void p2p_sha512(const void *data, int size, unsigned char *digest)
{
crypto_sha512(digest, data, (size_t)size);
}
void p2p_ed25519_public(const unsigned char *seed, unsigned char *public_key, unsigned char *x25519_secret)
{
unsigned char copy[P2P_SEED_SIZE];
unsigned char secret_key[64];
unsigned char hash[64];
/* (the key pair's making wipes the seed it is given) */
memcpy(copy, seed, sizeof(copy));
crypto_ed25519_key_pair(secret_key, public_key, copy);
if (x25519_secret)
{
/* the scalar Ed25519 signs with: X25519 clamps it the same way */
p2p_sha512(seed, P2P_SEED_SIZE, hash);
memcpy(x25519_secret, hash, P2P_KEY_SIZE);
}
crypto_wipe(secret_key, sizeof(secret_key));
crypto_wipe(hash, sizeof(hash));
}
void p2p_ed25519_sign(const unsigned char *seed, const unsigned char *public_key, const void *message, int size,
unsigned char *signature)
{
unsigned char secret_key[64];
/* (Monocypher's secret key: the seed, then the public key) */
memcpy(secret_key, seed, P2P_SEED_SIZE);
memcpy(secret_key + P2P_SEED_SIZE, public_key, P2P_KEY_SIZE);
crypto_ed25519_sign(signature, secret_key, message, (size_t)size);
crypto_wipe(secret_key, sizeof(secret_key));
}
int p2p_ed25519_to_x25519(const unsigned char *public_key, unsigned char *x25519_public)
{
static const unsigned char zero[P2P_KEY_SIZE];
static const unsigned char scalar[P2P_KEY_SIZE] = { 1 };
unsigned char product[P2P_KEY_SIZE];
crypto_eddsa_to_x25519(x25519_public, public_key);
/* a point of small order (whose multiple by a clamped scalar, a
multiple of 8, is 0) has signatures anyone can make, and secrets anyone
knows */
crypto_x25519(product, scalar, x25519_public);
return !p2p_equal(product, zero, P2P_KEY_SIZE);
}
int p2p_ed25519_verify(const unsigned char *public_key, const void *message, int size,
const unsigned char *signature)
{
unsigned char x25519_public[P2P_KEY_SIZE];
/* (Monocypher's check turns away an S past the group's order: one
signature a message) */
return p2p_ed25519_to_x25519(public_key, x25519_public) &&
crypto_ed25519_check(signature, public_key, message, (size_t)size) == 0;
}
+76
View File
@@ -10,6 +10,8 @@ p2p_discord.c; see p2p.c for the design).
#include "p2p.h"
#include <pthread.h>
enum
{
/* a machine's identifier: from the hash of its public key (which is new
@@ -31,6 +33,17 @@ enum
P2P_MAXIMUM_PEERS = 127,
};
typedef char p2p_listing_invite_size_assert[P2P_LISTING_INVITE_SIZE == P2P_LINK_SIZE ? 1 : -1];
/* the prefix of a public game's slot (a key hash in hex follows), and the
topic of queries (p2p_lobby.c) */
#define P2P_LOBBY_SLOT_PREFIX "hceu/3/lobby/s/"
#define P2P_LOBBY_QUERY_TOPIC "hceu/3/lobby/q"
/* internet play's state (p2p.c), which the game's threads and the p2p
thread share */
extern pthread_mutex_t p2p_lock;
struct p2p_candidate
{
/* network byte order */
@@ -57,6 +70,10 @@ int p2p_local_candidates(struct p2p_candidate *candidates, int maximum_count);
/* this run's X25519 public key (P2P_KEY_SIZE bytes), whose hash the
identifier is (p2p_identifier) */
const unsigned char *p2p_public_key(void);
/* this run's Ed25519 public key (the X25519 one's, p2p_ed25519_to_x25519),
and a signature with it */
const unsigned char *p2p_signing_key(void);
void p2p_sign(const void *message, int size, unsigned char *signature);
/* the identifier of the machine with this public key */
void p2p_identifier_for(const unsigned char *public_key, unsigned char *identifier);
/* the hash of a public key an invite holds (P2P_KEY_HASH_SIZE bytes), and
@@ -87,6 +104,10 @@ int p2p_peer_reoffered(const unsigned char *identifier, const unsigned char *sec
/* an invite that arrived on the p2p thread (from Discord, or another copy
of the game) */
void p2p_invite_received(const char *text);
/* a new invite (token) for the game hosted, if its invite was listed in the
server browser (going private: those who saw it must not get in); under
p2p_lock */
void p2p_new_invite_if_listed(void);
/* ---------- p2p_signal.c: signalling through public MQTT brokers */
@@ -154,6 +175,20 @@ const char *p2p_list_topic(void);
int p2p_list_received(const char *topic, const unsigned char *payload, int size);
void p2p_list_update(int hosting);
/* the server browser's topics: the own slot and the queries (a listed
game), and every slot (browsing) */
void p2p_signal_lobby_topics(int listed, int browsing);
/* publishes a listing to the own slot on every broker, retained, and again
on each broker that connects later; closing: a tombstone, after which the
slot is cleared and nothing is published again */
void p2p_signal_lobby_publish(const unsigned char *listing, int size, int closing);
/* asks the hosts to publish again */
void p2p_signal_lobby_query(void);
/* the game is quitting: the tombstone published (p2p_signal_lobby_publish)
and the slot cleared on every ready broker now, whatever their buckets, and
the connections closed cleanly */
void p2p_signal_lobby_quit(void);
/* ---------- p2p_crypto.c */
enum
@@ -189,6 +224,47 @@ int p2p_equal(const void *first, const void *second, int size);
key of the secret key scalar) */
void p2p_x25519(unsigned char *result, const unsigned char *scalar, const unsigned char *point);
enum
{
/* an Ed25519 seed (a run's key comes from one) and signature */
P2P_SEED_SIZE = 32,
P2P_SIGNATURE_SIZE = 64,
P2P_SHA512_SIZE = 64,
};
void p2p_sha512(const void *data, int size, unsigned char *digest);
/* Ed25519 (with SHA-512): a seed's public key (P2P_KEY_SIZE bytes) and,
unless NULL, its X25519 secret key (the scalar it signs with, whose X25519
public key is p2p_ed25519_to_x25519 of the Ed25519 one) */
void p2p_ed25519_public(const unsigned char *seed, unsigned char *public_key, unsigned char *x25519_secret);
void p2p_ed25519_sign(const unsigned char *seed, const unsigned char *public_key, const void *message, int size,
unsigned char *signature);
/* whether the signature is the key's, of the message (never for a key of
small order, which anyone can sign for) */
int p2p_ed25519_verify(const unsigned char *public_key, const void *message, int size,
const unsigned char *signature);
/* the X25519 public key of an Ed25519 one; 0 if it has a small order */
int p2p_ed25519_to_x25519(const unsigned char *public_key, unsigned char *x25519_public);
/* ---------- p2p_lobby.c: public games' listings */
/* the p2p thread's pass: the token of the game hosted for the internet
(NULL if none) and its player counts */
void p2p_lobby_update(const unsigned char *token, int player_count, int maximum_player_count);
/* whether the game hosted is listed now, and whether the browser is open */
int p2p_lobby_listed(void);
int p2p_lobby_browsing(void);
/* a message on a slot (its key hash in hex) through a broker; retained: the
slot's retained copy, sent on subscribing */
void p2p_lobby_slot_heard(const char *hash_text, const unsigned char *payload, int size, int retained);
/* a query was heard */
void p2p_lobby_query_heard(void);
/* the game is quitting: a listed game's tombstone, and its slot cleared,
written to the brokers at once; under p2p_lock */
void p2p_lobby_quit(void);
/* a key hash's slot */
void p2p_lobby_slot_topic(const unsigned char *key_hash, char *topic, int size);
/* ---------- p2p_discord.c: rich presence and invites through the Discord
desktop client */
+774
View File
@@ -0,0 +1,774 @@
/*
P2P_LOBBY.C
Internet play's public games (the server browser, Join Game > Server
Browser): a public game's host publishes a signed listing of it through the
signalling brokers (p2p_signal.c), and browsers gather the listings and join
a game by the invite a listing holds, as an invite link would.
The listing (network byte order) is signed with the host's Ed25519 key,
whose X25519 form's hash is the invite's host part (p2p.c), so no one but
the host can list its invite, alter its listing, or list another's under
false details:
"HL", format 1, the lobby version (2: HALO_PORT_NETWORK_VERSION; browsers
hide others), flags (open, under way, teams, closed), sequence (4: newest
wins), Unix time (4), the Ed25519 key (32), the invite's token (16),
players, most players, the gametype's engine (1 each), the game's name,
map and gametype (a length byte, then up to 32, 32 and 24 characters of
printable ASCII), a stamp (8, reserved: zero), and the signature (64) of
"hceu-lobby-1" and all before it.
Each host has a slot, hceu/3/lobby/s/<its key's hash in hex>: its listing,
retained on every broker (and expiring there after LISTING_EXPIRY seconds
on MQTT 5), and the broker's will (set at the connection, p2p_signal.c)
clears it if the host vanishes. Browsers subscribe to every slot and ask the
hosts to publish again (hceu/3/lobby/q). Brokers are other people's: what
one deletes or keeps is no authority, so
- a host publishes every REPUBLISH_INTERVAL, and sooner (at most once each
TRIGGER_INTERVAL) when its game changes, when asked, and when its slot is
heard to hold anything but its listing (cleared, another's, or an older
one of its own): a deletion is undone in seconds;
- a browser takes only a signed listing whose key's hash is its slot's,
keeps the newest of each host (by sequence), drops one not heard in
GAME_EXPIRY (on its own clock; a retained copy is taken only if its time
is within RETAINED_WINDOW of this machine's, from a host that died and
left it), takes a host's closing listing (a tombstone) as its end, and
ignores an emptied slot (a wipe is not a delete).
A host that stops publishes a tombstone, then clears its slot. Going private
makes a new invite (p2p.c), so a listing seen before lets no one in.
Checking signatures takes work, and anyone can send listings: at most
VERIFY_BUDGET milliseconds of it each pass of the p2p thread, from a queue
of MAXIMUM_QUEUED; the rest is dropped (hosts publish again).
*/
#include "platform.h"
#include "posix.h"
#include "port_config.h"
#include "p2p_internal.h"
#include "halo_port_limits.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
enum
{
LISTING_FORMAT = 1,
_listing_open = 1,
_listing_in_progress = 2,
_listing_has_teams = 4,
/* a tombstone: the host stopped */
_listing_closed = 8,
/* (reserved) */
_listing_password = 16,
STAMP_SIZE = 8,
/* the signed part's end: everything but the signature */
MAXIMUM_LISTING_SIZE = 2 + 1 + 2 + 1 + 4 + 4 + P2P_KEY_SIZE + P2P_TOKEN_SIZE + 3 +
(1 + P2P_LISTING_NAME_SIZE) + (1 + P2P_LISTING_MAP_SIZE) + (1 + P2P_LISTING_GAMETYPE_SIZE) + STAMP_SIZE +
P2P_SIGNATURE_SIZE,
MINIMUM_LISTING_SIZE = 2 + 1 + 2 + 1 + 4 + 4 + P2P_KEY_SIZE + P2P_TOKEN_SIZE + 3 + 3 + STAMP_SIZE +
P2P_SIGNATURE_SIZE,
MAXIMUM_GAMES = 256,
MAXIMUM_TOMBSTONES = 256,
MAXIMUM_QUEUED = 64,
/* milliseconds */
REPUBLISH_INTERVAL = 30000,
TRIGGER_INTERVAL = 5000,
QUERY_JITTER = 2000,
GAME_EXPIRY = 90000,
TOMBSTONE_TIME = 600000,
VERIFY_BUDGET = 2,
/* seconds */
RETAINED_WINDOW = 600,
};
static const char signature_label[] = "hceu-lobby-1";
/* a listing, read */
struct listing
{
int version;
int flags;
unsigned long sequence;
unsigned long time;
unsigned char key[P2P_KEY_SIZE];
unsigned char token[P2P_TOKEN_SIZE];
int player_count, maximum_player_count, engine_type;
char name[P2P_LISTING_NAME_SIZE + 1];
char map[P2P_LISTING_MAP_SIZE + 1];
char gametype[P2P_LISTING_GAMETYPE_SIZE + 1];
/* the signed part's size */
int signed_size;
};
struct game
{
int used;
unsigned char key_hash[P2P_KEY_HASH_SIZE];
unsigned long sequence;
unsigned long heard_time;
/* as it came (one the same again needs no check) */
unsigned char payload[MAXIMUM_LISTING_SIZE];
int payload_size;
struct p2p_listing listing;
};
struct tombstone
{
unsigned char key_hash[P2P_KEY_HASH_SIZE];
unsigned long sequence;
unsigned long time;
};
struct queued
{
unsigned char key_hash[P2P_KEY_HASH_SIZE];
unsigned char payload[MAXIMUM_LISTING_SIZE];
int size;
int retained;
};
static struct
{
/* hosting: the game as the game's server tells it
(p2p_set_game_listing), and whether it is public (p2p_set_hosting_public) */
int public;
char name[P2P_LISTING_NAME_SIZE + 1];
char map[P2P_LISTING_MAP_SIZE + 1];
char gametype[P2P_LISTING_GAMETYPE_SIZE + 1];
int engine_type;
int flags;
int player_count, maximum_player_count;
/* listed: with this token; its listing as last published */
int listed;
unsigned char token[P2P_TOKEN_SIZE];
unsigned long sequence;
unsigned char listing[MAXIMUM_LISTING_SIZE];
int listing_size;
unsigned long published_time;
/* a publish wanted (the game changed, a query, the slot needs mending),
not before this */
int republish_wanted;
unsigned long republish_time;
/* browsing */
int browsing;
int query_wanted;
struct queued queue[MAXIMUM_QUEUED];
int queue_count;
struct game games[MAXIMUM_GAMES];
struct tombstone tombstones[MAXIMUM_TOMBSTONES];
int next_tombstone;
/* the identifiers of games joining failed */
unsigned char failed[MAXIMUM_GAMES][P2P_IDENTIFIER_SIZE];
int failed_count;
} lobby;
static int elapsed(unsigned long since, unsigned long time)
{
return (unsigned int)(p2p_now() - since) >= (unsigned int)time;
}
/* printable ASCII, as the menus' font has it (others: '?'), cut to size */
static void sanitize(char *destination, int size, const char *source, int source_size)
{
int index;
for (index = 0; index < size && index < source_size && source[index]; index++)
{
unsigned char character = (unsigned char)source[index];
destination[index] = character >= 0x20 && character < 0x7F ? (char)character : '?';
}
destination[index] = 0;
}
void p2p_lobby_slot_topic(const unsigned char *key_hash, char *topic, int size)
{
char text[2 * P2P_KEY_HASH_SIZE + 1];
p2p_hex(key_hash, P2P_KEY_HASH_SIZE, text);
snprintf(topic, (size_t)size, "%s%s", P2P_LOBBY_SLOT_PREFIX, text);
}
/* the hash of an Ed25519 key's X25519 form (an invite's host part); 0 if
the key has a small order */
static int signing_key_hash(const unsigned char *key, unsigned char *hash)
{
unsigned char x25519[P2P_KEY_SIZE];
if (!p2p_ed25519_to_x25519(key, x25519))
return 0;
p2p_key_hash(x25519, hash);
return 1;
}
/* ---------- the listing */
static void put_long(unsigned char *bytes, unsigned long value)
{
bytes[0] = (unsigned char)(value >> 24);
bytes[1] = (unsigned char)(value >> 16);
bytes[2] = (unsigned char)(value >> 8);
bytes[3] = (unsigned char)value;
}
static unsigned long get_long(const unsigned char *bytes)
{
return (unsigned long)bytes[0] << 24 | (unsigned long)bytes[1] << 16 | (unsigned long)bytes[2] << 8 | bytes[3];
}
static int put_text(unsigned char *bytes, const char *text, int maximum)
{
int length = (int)strlen(text);
length = length > maximum ? maximum : length;
bytes[0] = (unsigned char)length;
memcpy(bytes + 1, text, (size_t)length);
return 1 + length;
}
/* the listing of the game hosted, signed: flags (_listing_closed for a
tombstone) */
static int listing_make(unsigned char *bytes, int flags)
{
unsigned char *signed_part = bytes;
unsigned char data[sizeof(signature_label) - 1 + MAXIMUM_LISTING_SIZE];
int size = 0;
bytes[size++] = 'H';
bytes[size++] = 'L';
bytes[size++] = LISTING_FORMAT;
bytes[size++] = (unsigned char)(HALO_PORT_NETWORK_VERSION >> 8);
bytes[size++] = (unsigned char)HALO_PORT_NETWORK_VERSION;
bytes[size++] = (unsigned char)flags;
put_long(bytes + size, ++lobby.sequence);
size += 4;
put_long(bytes + size, (unsigned long)time(NULL));
size += 4;
memcpy(bytes + size, p2p_signing_key(), P2P_KEY_SIZE);
size += P2P_KEY_SIZE;
memcpy(bytes + size, lobby.token, P2P_TOKEN_SIZE);
size += P2P_TOKEN_SIZE;
bytes[size++] = (unsigned char)(lobby.player_count > 255 ? 255 : lobby.player_count);
bytes[size++] = (unsigned char)(lobby.maximum_player_count > 255 ? 255 : lobby.maximum_player_count);
bytes[size++] = (unsigned char)lobby.engine_type;
size += put_text(bytes + size, lobby.name, P2P_LISTING_NAME_SIZE);
size += put_text(bytes + size, lobby.map, P2P_LISTING_MAP_SIZE);
size += put_text(bytes + size, lobby.gametype, P2P_LISTING_GAMETYPE_SIZE);
memset(bytes + size, 0, STAMP_SIZE);
size += STAMP_SIZE;
memcpy(data, signature_label, sizeof(signature_label) - 1);
memcpy(data + sizeof(signature_label) - 1, signed_part, (size_t)size);
p2p_sign(data, (int)(sizeof(signature_label) - 1) + size, bytes + size);
return size + P2P_SIGNATURE_SIZE;
}
static int get_text(const unsigned char *bytes, int size, int *offset, char *text, int maximum)
{
int length;
if (*offset >= size)
return 0;
length = bytes[(*offset)++];
if (length > maximum || *offset + length > size)
return 0;
sanitize(text, maximum, (const char *)bytes + *offset, length);
*offset += length;
return 1;
}
/* reads a listing (not its signature); 0 if it is malformed */
static int listing_read(const unsigned char *bytes, int size, struct listing *listing)
{
int offset = 0;
if (size < MINIMUM_LISTING_SIZE || size > MAXIMUM_LISTING_SIZE || bytes[0] != 'H' || bytes[1] != 'L' ||
bytes[2] != LISTING_FORMAT)
{
return 0;
}
offset = 3;
listing->version = bytes[offset] << 8 | bytes[offset + 1];
offset += 2;
listing->flags = bytes[offset++];
listing->sequence = get_long(bytes + offset);
offset += 4;
listing->time = get_long(bytes + offset);
offset += 4;
memcpy(listing->key, bytes + offset, P2P_KEY_SIZE);
offset += P2P_KEY_SIZE;
memcpy(listing->token, bytes + offset, P2P_TOKEN_SIZE);
offset += P2P_TOKEN_SIZE;
listing->player_count = bytes[offset++];
listing->maximum_player_count = bytes[offset++];
listing->engine_type = bytes[offset++];
if (!get_text(bytes, size, &offset, listing->name, P2P_LISTING_NAME_SIZE) ||
!get_text(bytes, size, &offset, listing->map, P2P_LISTING_MAP_SIZE) ||
!get_text(bytes, size, &offset, listing->gametype, P2P_LISTING_GAMETYPE_SIZE))
{
return 0;
}
offset += STAMP_SIZE;
if (offset + P2P_SIGNATURE_SIZE != size)
return 0;
listing->signed_size = offset;
return 1;
}
static int listing_signed(const unsigned char *bytes, const struct listing *listing)
{
unsigned char data[sizeof(signature_label) - 1 + MAXIMUM_LISTING_SIZE];
memcpy(data, signature_label, sizeof(signature_label) - 1);
memcpy(data + sizeof(signature_label) - 1, bytes, (size_t)listing->signed_size);
return p2p_ed25519_verify(listing->key, data, (int)(sizeof(signature_label) - 1) + listing->signed_size,
bytes + listing->signed_size);
}
/* ---------- hosting */
static void publish(void)
{
lobby.listing_size = listing_make(lobby.listing, lobby.flags);
lobby.published_time = p2p_now();
lobby.republish_wanted = 0;
p2p_signal_lobby_publish(lobby.listing, lobby.listing_size, 0);
}
/* a publish soon: at once if none was lately, else when TRIGGER_INTERVAL
has passed (plus jitter) */
static void republish_soon(unsigned long jitter)
{
unsigned long earliest = lobby.published_time + TRIGGER_INTERVAL;
unsigned long when = p2p_now() + jitter;
if ((long)(earliest - when) > 0)
when = earliest;
if (!lobby.republish_wanted || (long)(lobby.republish_time - when) > 0)
lobby.republish_time = when;
lobby.republish_wanted = 1;
}
static void stop_listing(void)
{
unsigned char tombstone[MAXIMUM_LISTING_SIZE];
int size = listing_make(tombstone, _listing_closed);
lobby.listed = 0;
lobby.republish_wanted = 0;
p2p_signal_lobby_publish(tombstone, size, 1);
p2p_signal_lobby_topics(0, lobby.browsing);
platform_log("Internet play: the game is no longer listed in the server browser");
}
static void update_hosting(const unsigned char *token, int player_count, int maximum_player_count)
{
int want = token && lobby.public && config_boolean("network.public_lobby");
if (lobby.listed && (!want || memcmp(token, lobby.token, P2P_TOKEN_SIZE)))
stop_listing();
if (!want)
return;
if (player_count != lobby.player_count || maximum_player_count != lobby.maximum_player_count)
{
lobby.player_count = player_count;
lobby.maximum_player_count = maximum_player_count;
if (lobby.listed)
republish_soon(0);
}
if (!lobby.listed)
{
memcpy(lobby.token, token, P2P_TOKEN_SIZE);
lobby.listed = 1;
p2p_signal_lobby_topics(1, lobby.browsing);
publish();
platform_log("Internet play: the game is listed in everyone's server browser (public)");
return;
}
if ((lobby.republish_wanted && (long)(p2p_now() - lobby.republish_time) >= 0) ||
elapsed(lobby.published_time, REPUBLISH_INTERVAL))
{
publish();
}
}
void p2p_lobby_query_heard(void)
{
if (lobby.listed)
{
unsigned short jitter;
posix_random_bytes(&jitter, sizeof(jitter));
republish_soon(jitter % QUERY_JITTER);
}
}
/* the host's own slot was heard: anything but its listing is mended */
static void own_slot_heard(const unsigned char *payload, int size, int retained)
{
struct listing listing;
if (!lobby.listed || (size == lobby.listing_size && !memcmp(payload, lobby.listing, (size_t)size)))
return;
/* (an older listing of its own, forwarded live, is the echo of a publish
before the last; as the slot's retained copy, the slot holds it) */
if (!retained && listing_read(payload, size, &listing) && !memcmp(listing.key, p2p_signing_key(), P2P_KEY_SIZE) &&
listing.sequence < lobby.sequence)
{
return;
}
republish_soon(0);
}
/* ---------- browsing */
static struct game *find_game(const unsigned char *key_hash)
{
int index;
for (index = 0; index < MAXIMUM_GAMES; index++)
{
if (lobby.games[index].used && !memcmp(lobby.games[index].key_hash, key_hash, P2P_KEY_HASH_SIZE))
return &lobby.games[index];
}
return NULL;
}
static struct tombstone *find_tombstone(const unsigned char *key_hash)
{
int index;
for (index = 0; index < MAXIMUM_TOMBSTONES; index++)
{
struct tombstone *tombstone = &lobby.tombstones[index];
if (tombstone->time && !elapsed(tombstone->time, TOMBSTONE_TIME) &&
!memcmp(tombstone->key_hash, key_hash, P2P_KEY_HASH_SIZE))
{
return tombstone;
}
}
return NULL;
}
static int identifier_failed(const unsigned char *identifier)
{
int index;
for (index = 0; index < lobby.failed_count; index++)
{
if (!memcmp(lobby.failed[index], identifier, P2P_IDENTIFIER_SIZE))
return 1;
}
return 0;
}
void p2p_lobby_slot_heard(const char *hash_text, const unsigned char *payload, int size, int retained)
{
unsigned char key_hash[P2P_KEY_HASH_SIZE];
struct game *game;
struct queued *queued;
int index;
if (strlen(hash_text) != 2 * P2P_KEY_HASH_SIZE || strspn(hash_text, "0123456789abcdef") != 2 * P2P_KEY_HASH_SIZE)
return;
for (index = 0; index < P2P_KEY_HASH_SIZE; index++)
{
unsigned int byte;
sscanf(hash_text + 2 * index, "%2x", &byte);
key_hash[index] = (unsigned char)byte;
}
{
unsigned char own[P2P_KEY_HASH_SIZE];
p2p_key_hash(p2p_public_key(), own);
if (!memcmp(own, key_hash, P2P_KEY_HASH_SIZE))
own_slot_heard(payload, size, retained);
}
/* (an emptied slot is no news: a wipe is not a delete) */
if (!lobby.browsing || size < MINIMUM_LISTING_SIZE || size > MAXIMUM_LISTING_SIZE)
return;
/* the same listing again: heard, no work */
game = find_game(key_hash);
if (game && game->payload_size == size && !memcmp(game->payload, payload, (size_t)size))
{
game->heard_time = p2p_now();
return;
}
for (index = 0; index < lobby.queue_count; index++)
{
if (lobby.queue[index].size == size && !memcmp(lobby.queue[index].payload, payload, (size_t)size))
return;
}
if (lobby.queue_count == MAXIMUM_QUEUED)
return;
queued = &lobby.queue[lobby.queue_count++];
memcpy(queued->key_hash, key_hash, P2P_KEY_HASH_SIZE);
memcpy(queued->payload, payload, (size_t)size);
queued->size = size;
queued->retained = retained;
}
/* a queued listing, its signature checked: taken or not */
static void listing_take(const struct queued *queued, const struct listing *listing)
{
struct tombstone *tombstone = find_tombstone(queued->key_hash);
struct game *game = find_game(queued->key_hash);
struct p2p_listing *shown;
unsigned char bytes[P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE];
char text[2 * (P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE) + 1];
int index;
if (tombstone && listing->sequence <= tombstone->sequence)
return;
if (game && listing->sequence <= game->sequence)
return;
if (listing->flags & _listing_closed)
{
if (!tombstone)
{
tombstone = &lobby.tombstones[lobby.next_tombstone];
lobby.next_tombstone = (lobby.next_tombstone + 1) % MAXIMUM_TOMBSTONES;
memcpy(tombstone->key_hash, queued->key_hash, P2P_KEY_HASH_SIZE);
}
tombstone->sequence = listing->sequence;
tombstone->time = p2p_now() ? p2p_now() : 1;
if (game)
game->used = 0;
return;
}
if (!game)
{
/* the room of one gone longest, if there is none free */
for (index = 0; index < MAXIMUM_GAMES && lobby.games[index].used; index++)
;
if (index == MAXIMUM_GAMES)
{
int oldest = 0;
for (index = 1; index < MAXIMUM_GAMES; index++)
{
if ((long)(lobby.games[index].heard_time - lobby.games[oldest].heard_time) < 0)
oldest = index;
}
index = oldest;
}
game = &lobby.games[index];
memset(game, 0, sizeof(*game));
memcpy(game->key_hash, queued->key_hash, P2P_KEY_HASH_SIZE);
game->used = 1;
}
game->sequence = listing->sequence;
game->heard_time = p2p_now();
memcpy(game->payload, queued->payload, (size_t)queued->size);
game->payload_size = queued->size;
shown = &game->listing;
memcpy(bytes, queued->key_hash, P2P_KEY_HASH_SIZE);
memcpy(bytes + P2P_KEY_HASH_SIZE, listing->token, P2P_TOKEN_SIZE);
p2p_hex(bytes, sizeof(bytes), text);
snprintf(shown->invite, sizeof(shown->invite), "halo://join/%s", text);
p2p_identifier_from_hash(queued->key_hash, shown->identifier);
memcpy(shown->name, listing->name, sizeof(shown->name));
memcpy(shown->map, listing->map, sizeof(shown->map));
memcpy(shown->gametype, listing->gametype, sizeof(shown->gametype));
shown->player_count = (unsigned char)listing->player_count;
shown->maximum_player_count = (unsigned char)listing->maximum_player_count;
shown->engine_type = (unsigned char)listing->engine_type;
shown->open = (listing->flags & _listing_open) != 0;
shown->in_progress = (listing->flags & _listing_in_progress) != 0;
shown->has_teams = (listing->flags & _listing_has_teams) != 0;
shown->ping = -1;
}
static void update_browsing(void)
{
unsigned long start = p2p_now();
int index;
if (lobby.query_wanted)
{
lobby.query_wanted = 0;
p2p_signal_lobby_query();
}
/* the queue, within the budget (one at least) */
while (lobby.queue_count && (p2p_now() == start || !elapsed(start, VERIFY_BUDGET)))
{
struct queued queued = lobby.queue[0];
struct listing listing;
unsigned char key_hash[P2P_KEY_HASH_SIZE];
int good;
memmove(lobby.queue, lobby.queue + 1, sizeof(*lobby.queue) * (size_t)(--lobby.queue_count));
if (!listing_read(queued.payload, queued.size, &listing) || listing.version != HALO_PORT_NETWORK_VERSION ||
!signing_key_hash(listing.key, key_hash) || memcmp(key_hash, queued.key_hash, P2P_KEY_HASH_SIZE))
{
continue;
}
/* a slot's retained copy: only one of about now (a host that died
left it, and nothing cleared it) */
if (queued.retained)
{
long difference = (long)(listing.time - (unsigned long)time(NULL));
if (difference > RETAINED_WINDOW || difference < -RETAINED_WINDOW)
continue;
}
/* (the work, without the lock: the game's threads need not wait) */
pthread_mutex_unlock(&p2p_lock);
good = listing_signed(queued.payload, &listing);
pthread_mutex_lock(&p2p_lock);
if (good && lobby.browsing)
listing_take(&queued, &listing);
}
if (lobby.queue_count && !lobby.browsing)
lobby.queue_count = 0;
for (index = 0; index < MAXIMUM_GAMES; index++)
{
if (lobby.games[index].used && elapsed(lobby.games[index].heard_time, GAME_EXPIRY))
lobby.games[index].used = 0;
}
}
/* ---------- p2p.c's side */
void p2p_lobby_update(const unsigned char *token, int player_count, int maximum_player_count)
{
update_hosting(token, player_count, maximum_player_count);
update_browsing();
}
int p2p_lobby_listed(void)
{
return lobby.listed;
}
void p2p_lobby_quit(void)
{
if (!lobby.listed)
return;
stop_listing();
/* (the game is ending: written now, not on the coming passes) */
p2p_signal_lobby_quit();
}
/* ---------- the game's side */
void p2p_set_hosting_public(int public)
{
pthread_mutex_lock(&p2p_lock);
/* (a new invite going private: p2p.c) */
if (lobby.public && !public)
p2p_new_invite_if_listed();
lobby.public = public ? 1 : 0;
pthread_mutex_unlock(&p2p_lock);
}
void p2p_set_game_listing(const char *name, const char *map, const char *gametype, int engine_type, int open,
int in_progress, int has_teams)
{
char new_name[P2P_LISTING_NAME_SIZE + 1];
char new_map[P2P_LISTING_MAP_SIZE + 1];
char new_gametype[P2P_LISTING_GAMETYPE_SIZE + 1];
int flags = (open ? _listing_open : 0) | (in_progress ? _listing_in_progress : 0) |
(has_teams ? _listing_has_teams : 0);
sanitize(new_name, P2P_LISTING_NAME_SIZE, name ? name : lobby.name, P2P_LISTING_NAME_SIZE);
sanitize(new_map, P2P_LISTING_MAP_SIZE, map ? map : lobby.map, P2P_LISTING_MAP_SIZE);
sanitize(new_gametype, P2P_LISTING_GAMETYPE_SIZE, gametype ? gametype : lobby.gametype,
P2P_LISTING_GAMETYPE_SIZE);
/* (only the game's server writes these: unchanged, it need not wait for
the lock) */
if (!strcmp(new_name, lobby.name) && !strcmp(new_map, lobby.map) && !strcmp(new_gametype, lobby.gametype) &&
engine_type == lobby.engine_type && flags == lobby.flags)
{
return;
}
pthread_mutex_lock(&p2p_lock);
memcpy(lobby.name, new_name, sizeof(lobby.name));
memcpy(lobby.map, new_map, sizeof(lobby.map));
memcpy(lobby.gametype, new_gametype, sizeof(lobby.gametype));
lobby.engine_type = engine_type;
lobby.flags = flags;
if (lobby.listed)
republish_soon(0);
pthread_mutex_unlock(&p2p_lock);
}
void p2p_lobby_browse(int on)
{
pthread_mutex_lock(&p2p_lock);
on = on && config_boolean("network.public_lobby") ? 1 : 0;
if (on != lobby.browsing)
{
lobby.browsing = on;
if (!on)
{
memset(lobby.games, 0, sizeof(lobby.games));
lobby.queue_count = 0;
}
lobby.query_wanted = on;
p2p_signal_lobby_topics(lobby.listed, on);
}
pthread_mutex_unlock(&p2p_lock);
}
void p2p_lobby_refresh(void)
{
pthread_mutex_lock(&p2p_lock);
if (lobby.browsing)
lobby.query_wanted = 1;
pthread_mutex_unlock(&p2p_lock);
}
int p2p_lobby_browsing(void)
{
return lobby.browsing;
}
/* the order shown: the most players first; then those joining has not
failed, the open ones, and by name */
static int listing_order(const void *first, const void *second)
{
const struct p2p_listing *a = first, *b = second;
if (a->player_count != b->player_count)
return b->player_count - a->player_count;
if (a->failed != b->failed)
return a->failed - b->failed;
if (a->open != b->open)
return b->open - a->open;
return strcmp(a->name, b->name);
}
int p2p_lobby_games(struct p2p_listing *games, int maximum_count)
{
int count = 0;
int index;
pthread_mutex_lock(&p2p_lock);
for (index = 0; index < MAXIMUM_GAMES && count < maximum_count; index++)
{
if (!lobby.games[index].used)
continue;
games[count] = lobby.games[index].listing;
games[count].failed = (unsigned char)identifier_failed(games[count].identifier);
count++;
}
pthread_mutex_unlock(&p2p_lock);
qsort(games, (size_t)count, sizeof(*games), listing_order);
return count;
}
void p2p_lobby_mark_failed(const unsigned char *identifier)
{
pthread_mutex_lock(&p2p_lock);
if (!identifier_failed(identifier) && lobby.failed_count < MAXIMUM_GAMES)
memcpy(lobby.failed[lobby.failed_count++], identifier, P2P_IDENTIFIER_SIZE);
pthread_mutex_unlock(&p2p_lock);
}
+600 -83
View File
@@ -3,9 +3,22 @@ P2P_SIGNAL.C
Internet play's signalling (p2p.c): how a joiner and the host of an invite
tell each other where they can be reached, through public MQTT brokers
(network.signalling_brokers; MQTT 3.1.1 over TCP). Every broker is used at
once, so any one of them working is enough (an answer goes back through
each broker a request came through).
(network.signalling_brokers; MQTT 5 over TCP, or 3.1.1 with a broker that
refuses 5). Every broker is used at once, so any one of them working is
enough (an answer goes back through each broker a request came through).
They also carry the server browser's listings of public games (p2p_lobby.c):
retained in each host's slot, published at least once (QoS 1, sent again
until the broker acknowledges them), expiring at the broker after
LISTING_EXPIRY seconds (MQTT 5), and cleared by the broker if the host's
connection dies (the will every connection sets: harmless when not hosting).
A broker may drop a connection's publishes past about 10 a second without a
word (EMQX), and a host's answers to a flood of JOINs would reach that: each
broker's publishes spend from a bucket (PUBLISH_BURST at once, one more each
PUBLISH_INTERVAL). Answers to proven joiners come first, then the listing
(kept to publish later), then a joiner's requests and the browser's queries;
answers to requests not proven yet only while some are left over (the
joiner asks again).
Everything that passes through them is sealed with a key derived from the
invite's token, and goes to topics that are hashes of it, so the brokers
@@ -81,20 +94,42 @@ enum
host takes at most a few new players a minute: this is hours of them;
one is not forgotten while its host nonce lasts: USED_REQUEST_TIME) */
MAXIMUM_USED_REQUESTS = 1024,
TOPIC_SIZE = 7 + 32 + 1,
/* (p2p_code.c's are as long) */
TOPIC_SIZE_CHECK = 1 / (TOPIC_SIZE == P2P_CODE_TOPIC_SIZE),
/* (a slot's: hceu/3/lobby/s/ and a key hash in hex) */
TOPIC_SIZE = 64,
NONCE_SIZE = 8,
/* an ACCEPT's or a proven JOIN's tag: the first half of an HMAC-SHA256 */
TAG_SIZE = 16,
/* a proven JOIN's end: the host's nonce, and the tag */
PROOF_SIZE = NONCE_SIZE + TAG_SIZE,
BUFFER_SIZE = 4096,
MAXIMUM_MESSAGE_SIZE = 256,
MAXIMUM_MESSAGE_SIZE = 512,
/* a listing's most (p2p_lobby.c's MAXIMUM_LISTING_SIZE, and some) */
MAXIMUM_LISTING_SIZE = 256,
/* the publishes awaiting acknowledgement on a broker */
MAXIMUM_IN_FLIGHT = 4,
/* the topics a broker is subscribed to (_topic_*) */
NUMBER_OF_TOPICS = 8,
/* a broker's publishes: at most PUBLISH_BURST at once, one more each
PUBLISH_INTERVAL milliseconds (EMQX drops a connection's publishes past
about 10 a second, without a word) */
PUBLISH_BURST = 8,
PUBLISH_INTERVAL = 125,
/* what is kept back of the bucket for answers to proven joiners and the
listing: a request's answer, and a query, need this many left */
UNPROVEN_RESERVE = 3,
/* seconds a listing lasts at the broker (MQTT 5's Message Expiry
Interval): a dead host's goes even from a broker that kept it through a
restart */
LISTING_EXPIRY = 90,
/* milliseconds */
CONNECT_TIMEOUT = 10000,
/* a broker that failed is tried again after this, twice as long each
failure after, to MAXIMUM_RETRY_INTERVAL */
RETRY_INTERVAL = 15000,
MAXIMUM_RETRY_INTERVAL = 120000,
/* a publish at least once not acknowledged is sent again */
RESEND_INTERVAL = 5000,
KEEP_ALIVE_SECONDS = 60,
PING_INTERVAL = 30000,
SILENCE_TIMEOUT = 90000,
@@ -130,6 +165,31 @@ enum
_broker_ready,
};
enum
{
_topic_host,
_topic_join,
/* the server browser's (p2p_lobby.c): the own slot, queries, all slots */
_topic_own_slot,
_topic_query,
_topic_slots,
/* the VR port's: a hosted join code's, a looked up one's, the public
games' (p2p_code.c, p2p_list.c) */
_topic_code_host,
_topic_code_look,
_topic_list,
};
/* MQTT 5's properties */
enum
{
_property_message_expiry = 0x02,
_property_server_keep_alive = 0x13,
_property_receive_maximum = 0x21,
_property_retain_available = 0x25,
_property_wildcard_available = 0x28,
};
enum
{
_message_join = 'J',
@@ -151,14 +211,41 @@ struct broker
unsigned long heard_time;
int failures;
unsigned short packet_identifier;
/* the topics it has been asked for */
char host_topic[TOPIC_SIZE];
char join_topic[TOPIC_SIZE];
/* (join codes' topics: p2p_code.c) */
char code_host_topic[TOPIC_SIZE];
char code_look_topic[TOPIC_SIZE];
/* (public games': p2p_list.c) */
char list_topic[TOPIC_SIZE];
/* 5 (MQTT 5), or 4 (3.1.1: the broker refused 5) */
int protocol;
/* as its CONNACK said (MQTT 5; 3.1.1 brokers have both): without
either, it carries only signalling, not listings */
int retain_available;
int wildcard_available;
/* seconds; its own if it said */
int keep_alive;
int receive_maximum;
/* the bucket its publishes spend from */
int publish_tokens;
unsigned long publish_time;
/* the topics it has been asked for (_topic_*) */
char topics[NUMBER_OF_TOPICS][TOPIC_SIZE];
/* the listing's version it was given (signalling.lobby_version); and
whether it has yet to have its slot cleared (after a tombstone) */
int lobby_version;
int lobby_clear_pending;
/* a query to send once subscribed to the slots */
int query_pending;
/* publishes at least once, awaiting PUBACK */
struct
{
int used;
unsigned short identifier;
unsigned long sent_time;
/* a listing, which a newer one replaces (not a tombstone or a
clearing) */
int listing;
/* refused (MQTT 5's PUBACK said why): sent again as a new publish
(the refusal ended the old one's identifier) */
int refused;
int size;
unsigned char payload[MAXIMUM_LISTING_SIZE];
} in_flight[MAXIMUM_IN_FLIGHT];
unsigned char input[BUFFER_SIZE];
int input_size;
unsigned char output[BUFFER_SIZE];
@@ -210,6 +297,18 @@ static struct
int broker_count;
char client_identifier[24];
/* the server browser (p2p_lobby.c): this machine's slot (its will
clears it), whether its game is listed and whether it browses; the
listing to publish (none: size 0), its version, and whether it is a
tombstone (the slot is cleared after it) */
char own_slot[TOPIC_SIZE];
int lobby_listed;
int lobby_browsing;
unsigned char lobby_listing[MAXIMUM_LISTING_SIZE];
int lobby_listing_size;
int lobby_version;
int lobby_closing;
/* hosting */
int hosting;
unsigned char host_token[P2P_TOKEN_SIZE];
@@ -326,11 +425,12 @@ static void broker_close(struct broker *broker, int failed)
broker->state_time = p2p_now();
broker->input_size = 0;
broker->output_size = 0;
broker->host_topic[0] = 0;
broker->join_topic[0] = 0;
broker->code_host_topic[0] = 0;
broker->code_look_topic[0] = 0;
broker->list_topic[0] = 0;
memset(broker->topics, 0, sizeof(broker->topics));
memset(broker->in_flight, 0, sizeof(broker->in_flight));
/* (the will cleared the slot: the listing again once connected) */
broker->lobby_version = 0;
broker->lobby_clear_pending = 0;
broker->query_pending = 0;
if (failed)
broker->failures++;
}
@@ -354,23 +454,50 @@ static void broker_flush(struct broker *broker)
}
}
static int put_variable(unsigned char *bytes, int value)
{
int size = 0;
do
{
unsigned char byte = (unsigned char)(value & 127);
value >>= 7;
bytes[size++] = (unsigned char)(value ? byte | 128 : byte);
} while (value);
return size;
}
/* reads a variable byte integer; 0 if it does not fit */
static int get_variable(const unsigned char *bytes, int size, int *offset, int *value)
{
int shift = 0;
*value = 0;
for (;;)
{
unsigned char byte;
if (*offset >= size || shift > 21)
return 0;
byte = bytes[(*offset)++];
*value |= (byte & 127) << shift;
shift += 7;
if (!(byte & 128))
return 1;
}
}
/* queues a packet: its fixed header's first byte, and its body */
static void broker_send(struct broker *broker, unsigned char type, const unsigned char *body, int size)
{
unsigned char header[5];
int header_size = 1;
int remaining = size;
int header_size;
if (broker->socket < 0)
return;
header[0] = type;
do
{
unsigned char byte = (unsigned char)(remaining & 127);
remaining >>= 7;
header[header_size++] = (unsigned char)(remaining ? byte | 128 : byte);
} while (remaining);
header_size = 1 + put_variable(header + 1, size);
if (broker->output_size + header_size + size > BUFFER_SIZE)
{
broker_close(broker, 1);
@@ -393,57 +520,170 @@ static int put_string(unsigned char *body, const char *text)
return size + 2;
}
static void broker_topic(struct broker *broker, const char *topic, int subscribe)
static unsigned short next_packet_identifier(struct broker *broker)
{
unsigned char body[4 + TOPIC_SIZE + 1];
int size = 0;
if (++broker->packet_identifier == 0)
broker->packet_identifier = 1;
body[size++] = (unsigned char)(broker->packet_identifier >> 8);
body[size++] = (unsigned char)broker->packet_identifier;
return broker->packet_identifier;
}
/* whether the broker may publish now, spending from its bucket: reserve is
how many must be left after (UNPROVEN_RESERVE for what may wait) */
static int broker_may_publish(struct broker *broker, int reserve)
{
unsigned long now = p2p_now();
int gained = (int)((now - broker->publish_time) / PUBLISH_INTERVAL);
if (gained > 0)
{
broker->publish_tokens = broker->publish_tokens + gained > PUBLISH_BURST ? PUBLISH_BURST :
broker->publish_tokens + gained;
broker->publish_time += (unsigned long)gained * PUBLISH_INTERVAL;
if (broker->publish_tokens == PUBLISH_BURST)
broker->publish_time = now;
}
if (broker->publish_tokens <= reserve)
return 0;
broker->publish_tokens--;
return 1;
}
static void broker_topic(struct broker *broker, const char *topic, int subscribe, int no_local)
{
unsigned char body[5 + TOPIC_SIZE + 1];
unsigned short identifier = next_packet_identifier(broker);
int size = 0;
body[size++] = (unsigned char)(identifier >> 8);
body[size++] = (unsigned char)identifier;
/* (MQTT 5: no properties) */
if (broker->protocol == 5)
body[size++] = 0;
size += put_string(body + size, topic);
/* at most once; MQTT 5: No Local, not this connection's own publishes */
if (subscribe)
body[size++] = 0; /* at most once */
body[size++] = (unsigned char)(broker->protocol == 5 && no_local ? 0x04 : 0);
broker_send(broker, subscribe ? 0x82 : 0xA2, body, size);
}
/* a publish at most once, not retained: signalling's, and queries */
static void broker_publish(struct broker *broker, const char *topic, const unsigned char *payload, int payload_size)
{
unsigned char body[2 + TOPIC_SIZE + MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD];
unsigned char body[3 + TOPIC_SIZE + MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD];
int size = put_string(body, topic);
if (broker->protocol == 5)
body[size++] = 0;
memcpy(body + size, payload, (size_t)payload_size);
broker_send(broker, 0x30, body, size + payload_size);
}
/* a publish at least once to the own slot, retained: a listing (expiring
at the broker), or an empty one, which clears it; again (duplicate) if the
first was not acknowledged */
static void broker_publish_slot(struct broker *broker, unsigned short identifier, const unsigned char *payload,
int payload_size, int duplicate)
{
unsigned char body[2 + TOPIC_SIZE + 2 + 8 + MAXIMUM_LISTING_SIZE];
int size = put_string(body, signalling.own_slot);
body[size++] = (unsigned char)(identifier >> 8);
body[size++] = (unsigned char)identifier;
if (broker->protocol == 5)
{
if (payload_size)
{
body[size++] = 5;
body[size++] = _property_message_expiry;
body[size++] = 0;
body[size++] = 0;
body[size++] = 0;
body[size++] = LISTING_EXPIRY;
}
else
{
body[size++] = 0;
}
}
if (payload_size)
memcpy(body + size, payload, (size_t)payload_size);
/* PUBLISH, QoS 1, retained */
broker_send(broker, (unsigned char)(0x30 | (duplicate ? 0x08 : 0) | 0x02 | 0x01), body, size + payload_size);
broker->sent_time = p2p_now();
}
/* a listing (or a clearing: size 0) to the own slot, at least once: 0 if
there is no room for it yet */
static int broker_publish_listing(struct broker *broker, const unsigned char *payload, int size, int listing)
{
int maximum = broker->receive_maximum < MAXIMUM_IN_FLIGHT ? broker->receive_maximum : MAXIMUM_IN_FLIGHT;
int count = 0;
int free_index = -1;
int index;
/* (a listing replaces an older one not acknowledged yet) */
for (index = 0; index < MAXIMUM_IN_FLIGHT; index++)
{
if (broker->in_flight[index].used && broker->in_flight[index].listing && listing)
broker->in_flight[index].used = 0;
if (broker->in_flight[index].used)
count++;
else if (free_index < 0)
free_index = index;
}
if (free_index < 0 || count >= maximum || !broker_may_publish(broker, 0))
return 0;
broker->in_flight[free_index].used = 1;
broker->in_flight[free_index].identifier = next_packet_identifier(broker);
broker->in_flight[free_index].sent_time = p2p_now();
broker->in_flight[free_index].listing = listing;
broker->in_flight[free_index].refused = 0;
broker->in_flight[free_index].size = size;
if (size)
memcpy(broker->in_flight[free_index].payload, payload, (size_t)size);
broker_publish_slot(broker, broker->in_flight[free_index].identifier, payload, size, 0);
return 1;
}
/* whether the broker carries the server browser: retained messages and
wildcards */
static int broker_carries_lobby(const struct broker *broker)
{
return broker->retain_available && broker->wildcard_available;
}
/* the topics a ready broker should be subscribed to */
static void broker_sync_topics(struct broker *broker)
{
const char *wanted[5];
char *had[5];
const char *wanted[NUMBER_OF_TOPICS];
int lobby = broker_carries_lobby(broker);
int index;
if (broker->state != _broker_ready)
return;
wanted[0] = signalling.hosting ? signalling.host_topic : "";
wanted[1] = signalling.joining ? signalling.join_topic : "";
p2p_code_topics(&wanted[2], &wanted[3]);
wanted[4] = p2p_list_topic();
had[0] = broker->host_topic;
had[1] = broker->join_topic;
had[2] = broker->code_host_topic;
had[3] = broker->code_look_topic;
had[4] = broker->list_topic;
for (index = 0; index < 5; index++)
wanted[_topic_host] = signalling.hosting ? signalling.host_topic : "";
wanted[_topic_join] = signalling.joining ? signalling.join_topic : "";
wanted[_topic_own_slot] = lobby && signalling.lobby_listed ? signalling.own_slot : "";
wanted[_topic_query] = lobby && signalling.lobby_listed ? P2P_LOBBY_QUERY_TOPIC : "";
wanted[_topic_slots] = lobby && signalling.lobby_browsing ? P2P_LOBBY_SLOT_PREFIX "+" : "";
/* (the VR port's join codes and public games: p2p_code.c, p2p_list.c) */
p2p_code_topics(&wanted[_topic_code_host], &wanted[_topic_code_look]);
wanted[_topic_list] = p2p_list_topic();
for (index = 0; index < NUMBER_OF_TOPICS; index++)
{
if (!strcmp(wanted[index], had[index]))
char *had = broker->topics[index];
if (!strcmp(wanted[index], had))
continue;
if (had[index][0])
broker_topic(broker, had[index], 0);
if (had[0])
broker_topic(broker, had, 0, 0);
if (wanted[index][0])
broker_topic(broker, wanted[index], 1);
strcpy(had[index], wanted[index]);
broker_topic(broker, wanted[index], 1, index == _topic_own_slot);
/* (once subscribed to the slots, the hosts are asked to publish:
retained copies come at once, but may be old) */
if (index == _topic_slots && wanted[index][0])
broker->query_pending = 1;
strcpy(had, wanted[index]);
}
}
@@ -453,22 +693,80 @@ static void publish_everywhere(const char *topic, const unsigned char *payload,
for (index = 0; index < signalling.broker_count; index++)
{
if (signalling.brokers[index].state == _broker_ready)
broker_publish(&signalling.brokers[index], topic, payload, size);
struct broker *broker = &signalling.brokers[index];
if (broker->state == _broker_ready && broker_may_publish(broker, 0))
broker_publish(broker, topic, payload, size);
}
}
/* the listing's publishes due on a broker: the listing, or a tombstone and
then the slot's clearing; acknowledgements not come sent again */
static void broker_update_lobby(struct broker *broker)
{
int index;
if (broker->state != _broker_ready || !broker_carries_lobby(broker))
return;
for (index = 0; index < MAXIMUM_IN_FLIGHT; index++)
{
if (broker->in_flight[index].used && elapsed(broker->in_flight[index].sent_time, RESEND_INTERVAL) &&
broker_may_publish(broker, 0))
{
int duplicate = !broker->in_flight[index].refused;
if (!duplicate)
broker->in_flight[index].identifier = next_packet_identifier(broker);
broker->in_flight[index].refused = 0;
broker->in_flight[index].sent_time = p2p_now();
broker_publish_slot(broker, broker->in_flight[index].identifier, broker->in_flight[index].payload,
broker->in_flight[index].size, duplicate);
}
}
if (broker->lobby_version != signalling.lobby_version && signalling.lobby_listing_size &&
broker_publish_listing(broker, signalling.lobby_listing, signalling.lobby_listing_size,
!signalling.lobby_closing))
{
broker->lobby_version = signalling.lobby_version;
broker->lobby_clear_pending = signalling.lobby_closing;
}
if (broker->lobby_clear_pending && broker->lobby_version == signalling.lobby_version &&
broker_publish_listing(broker, NULL, 0, 0))
{
broker->lobby_clear_pending = 0;
}
if (broker->query_pending && broker->topics[_topic_slots][0] && broker_may_publish(broker, UNPROVEN_RESERVE))
{
unsigned char nonce[NONCE_SIZE];
posix_random_bytes(nonce, sizeof(nonce));
broker_publish(broker, P2P_LOBBY_QUERY_TOPIC, nonce, sizeof(nonce));
broker->query_pending = 0;
}
}
static void broker_connected(struct broker *broker)
{
unsigned char body[64];
unsigned char body[32 + sizeof(signalling.client_identifier) + TOPIC_SIZE];
int size = 0;
if (!broker->protocol)
broker->protocol = 5;
size += put_string(body, "MQTT");
body[size++] = 4; /* 3.1.1 */
body[size++] = 0x02; /* a clean session */
body[size++] = (unsigned char)broker->protocol;
/* a clean session, and a will: an empty retained message to this
machine's slot, which clears its listing if the connection dies */
body[size++] = 0x02 | 0x04 | 0x20;
body[size++] = 0;
body[size++] = KEEP_ALIVE_SECONDS;
if (broker->protocol == 5)
body[size++] = 0;
size += put_string(body + size, signalling.client_identifier);
if (broker->protocol == 5)
body[size++] = 0;
size += put_string(body + size, signalling.own_slot);
body[size++] = 0;
body[size++] = 0;
broker->state = _broker_awaiting_acknowledgement;
broker->state_time = p2p_now();
broker_send(broker, 0x10, body, size);
@@ -726,7 +1024,7 @@ static int request_used(const unsigned char *request)
/* the host's answer to a request, through the broker it came through */
static void send_accept(struct broker *broker, const unsigned char *identifier, const unsigned char *nonce,
const unsigned char *host_nonce, const unsigned char *base)
const unsigned char *host_nonce, const unsigned char *base, int proven)
{
unsigned char answer[MAXIMUM_MESSAGE_SIZE];
unsigned char sealed[MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD];
@@ -746,7 +1044,8 @@ static void send_accept(struct broker *broker, const unsigned char *identifier,
size += TAG_SIZE;
size = p2p_seal(signalling.host_key, answer, size, sealed);
make_topic(signalling.host_token, "joiner", identifier, topic);
if (broker->state == _broker_ready)
/* (a proven joiner's first: its session is made; others ask again) */
if (broker->state == _broker_ready && broker_may_publish(broker, proven ? 0 : UNPROVEN_RESERVE))
broker_publish(broker, topic, sealed, size);
}
@@ -799,7 +1098,7 @@ static void join_received(struct broker *broker, const unsigned char *message, i
}
joiner->answered_time = p2p_now();
joiner->answered_broker_times[broker_index] = joiner->answered_time;
send_accept(broker, identifier, joiner->nonce, joiner->host_nonce, joiner->base);
send_accept(broker, identifier, joiner->nonce, joiner->host_nonce, joiner->base, proven);
return;
}
memcpy(request, identifier, P2P_IDENTIFIER_SIZE);
@@ -860,7 +1159,7 @@ static void join_received(struct broker *broker, const unsigned char *message, i
asker->answered_nonce_brokers[slot] = (signed char)broker_index;
asker->answered_nonce_times[slot] = asker->answered_time;
host_nonce_for(public_key, nonce, p2p_now() / HOST_NONCE_PERIOD, host_nonce);
send_accept(broker, identifier, nonce, host_nonce, asker->base);
send_accept(broker, identifier, nonce, host_nonce, asker->base, 0);
return;
}
/* proven: with a nonce the host answered the request with lately, and a
@@ -897,7 +1196,7 @@ static void join_received(struct broker *broker, const unsigned char *message, i
memset(joiner->answered_broker_times, 0, sizeof(joiner->answered_broker_times));
joiner->answered_broker_times[broker_index] = joiner->answered_time;
joiner->used = 1;
send_accept(broker, identifier, nonce, host_nonce, base);
send_accept(broker, identifier, nonce, host_nonce, base, 1);
}
/* the joiner: the host answered */
@@ -947,7 +1246,8 @@ static void accept_received(const unsigned char *message, int size)
send_join();
}
static void publish_received(struct broker *broker, const char *topic, const unsigned char *payload, int size)
static void publish_received(struct broker *broker, const char *topic, const unsigned char *payload, int size,
int retained)
{
unsigned char message[MAXIMUM_MESSAGE_SIZE];
int message_size;
@@ -956,6 +1256,16 @@ static void publish_received(struct broker *broker, const char *topic, const uns
return;
if (p2p_code_received(topic, payload, size) || p2p_list_received(topic, payload, size))
return;
if (!strncmp(topic, P2P_LOBBY_SLOT_PREFIX, sizeof(P2P_LOBBY_SLOT_PREFIX) - 1))
{
p2p_lobby_slot_heard(topic + sizeof(P2P_LOBBY_SLOT_PREFIX) - 1, payload, size, retained);
return;
}
if (!strcmp(topic, P2P_LOBBY_QUERY_TOPIC))
{
p2p_lobby_query_heard();
return;
}
if (signalling.hosting && !strcmp(topic, signalling.host_topic))
{
message_size = p2p_open(signalling.host_key, payload, size, message);
@@ -970,6 +1280,112 @@ static void publish_received(struct broker *broker, const char *topic, const uns
}
}
/* a CONNACK: 0 if the connection is closed (refused, or to try 3.1.1) */
static int broker_acknowledged(struct broker *broker, const unsigned char *body, int size)
{
int code = body[1];
int offset = 2;
int properties = 0;
broker->retain_available = 1;
broker->wildcard_available = 1;
broker->keep_alive = KEEP_ALIVE_SECONDS;
broker->receive_maximum = MAXIMUM_IN_FLIGHT;
/* a broker that has not MQTT 5 answers 0x84 (5's unsupported protocol
version), or 3.1.1's 1 (unacceptable protocol version) */
if (broker->protocol == 5 && (code == 0x84 || (size == 2 && code == 1)))
{
platform_log("Internet play: the signalling broker %s has not MQTT 5; using 3.1.1", broker->host);
broker->protocol = 4;
broker_close(broker, 0);
/* (again at once) */
broker->state_time = p2p_now() - MAXIMUM_RETRY_INTERVAL;
return 0;
}
if (code != 0)
{
platform_log("Internet play: the signalling broker %s refused the connection", broker->host);
broker_close(broker, 1);
return 0;
}
if (broker->protocol == 5 && get_variable(body, size, &offset, &properties))
{
int end = offset + properties > size ? size : offset + properties;
while (offset < end)
{
int property = body[offset++];
switch (property)
{
/* a byte */
case 0x01: case 0x17: case 0x19: case 0x24: case 0x25: case 0x28: case 0x29: case 0x2A:
if (offset < end)
{
if (property == _property_retain_available)
broker->retain_available = body[offset];
else if (property == _property_wildcard_available)
broker->wildcard_available = body[offset];
}
offset += 1;
break;
/* two */
case 0x13: case 0x21: case 0x22: case 0x23:
if (offset + 2 <= end)
{
int value = body[offset] << 8 | body[offset + 1];
if (property == _property_server_keep_alive && value > 0)
broker->keep_alive = value;
else if (property == _property_receive_maximum && value > 0)
broker->receive_maximum = value;
}
offset += 2;
break;
/* four */
case 0x02: case 0x11: case 0x18: case 0x27:
offset += 4;
break;
/* a variable byte integer */
case 0x0B:
{
int value;
if (!get_variable(body, end, &offset, &value))
offset = end;
break;
}
/* a string or binary data: a length, then that */
case 0x03: case 0x08: case 0x09: case 0x12: case 0x15: case 0x16: case 0x1A: case 0x1C: case 0x1F:
offset += offset + 2 <= end ? 2 + (body[offset] << 8 | body[offset + 1]) : 2;
break;
/* a pair of strings */
case 0x26:
{
int pair;
for (pair = 0; pair < 2; pair++)
offset += offset + 2 <= end ? 2 + (body[offset] << 8 | body[offset + 1]) : 2;
break;
}
default:
/* (unknown: the rest can't be read) */
offset = end;
break;
}
}
}
broker->state = _broker_ready;
broker->failures = 0;
broker->publish_tokens = PUBLISH_BURST;
broker->publish_time = p2p_now();
broker_sync_topics(broker);
/* a joiner's first request need not wait for the next repeat */
if (signalling.joining)
send_join();
return 1;
}
/* the packets that arrived whole */
static void broker_parse(struct broker *broker)
{
@@ -1009,36 +1425,61 @@ static void broker_parse(struct broker *broker)
type = broker->input[0];
if ((type & 0xF0) == 0x20 && remaining >= 2)
{
/* CONNACK */
if (broker->input[header_size + 1] != 0)
{
platform_log("Internet play: the signalling broker %s refused the connection", broker->host);
broker_close(broker, 1);
if (!broker_acknowledged(broker, broker->input + header_size, remaining))
return;
}
broker->state = _broker_ready;
broker->failures = 0;
broker_sync_topics(broker);
/* a joiner's first request need not wait for the next repeat */
if (signalling.joining)
send_join();
}
else if ((type & 0xF0) == 0x30 && remaining >= 2)
{
/* PUBLISH */
const unsigned char *body = broker->input + header_size;
int topic_size = body[0] << 8 | body[1];
int offset = 2 + topic_size + (((type >> 1) & 3) ? 2 : 0);
int quality = (type >> 1) & 3;
int offset = 2 + topic_size;
int properties = 0;
if (quality)
{
/* (subscriptions ask for at most once; acknowledged all the
same) */
if (offset + 2 <= remaining && quality == 1)
broker_send(broker, 0x40, body + offset, 2);
offset += 2;
}
if (broker->protocol == 5 && get_variable(body, remaining, &offset, &properties))
offset += properties;
if (topic_size < TOPIC_SIZE && offset <= remaining)
{
char topic[TOPIC_SIZE];
memcpy(topic, body + 2, (size_t)topic_size);
topic[topic_size] = 0;
publish_received(broker, topic, body + offset, remaining - offset);
publish_received(broker, topic, body + offset, remaining - offset, type & 1);
}
}
else if ((type & 0xF0) == 0x40 && remaining >= 2)
{
/* PUBACK: MQTT 5's reason, if any, of 0x80 or more is a refusal
(a quota, a rate): sent again later */
const unsigned char *body = broker->input + header_size;
unsigned short identifier = (unsigned short)(body[0] << 8 | body[1]);
int refused = remaining >= 3 && body[2] >= 0x80;
int index;
for (index = 0; index < MAXIMUM_IN_FLIGHT; index++)
{
if (broker->in_flight[index].used && broker->in_flight[index].identifier == identifier)
{
broker->in_flight[index].used = refused;
broker->in_flight[index].refused = refused;
}
}
}
else if ((type & 0xF0) == 0xE0)
{
/* DISCONNECT (MQTT 5): the broker's */
broker_close(broker, 1);
return;
}
/* (what it sent in answer may have closed it, emptying input) */
if (broker->socket < 0)
return;
@@ -1092,6 +1533,12 @@ void p2p_signal_start(void)
posix_random_bytes(random, sizeof(random));
p2p_hex(random, sizeof(random), hex);
snprintf(signalling.client_identifier, sizeof(signalling.client_identifier), "hceu-%s", hex);
{
unsigned char hash[P2P_KEY_HASH_SIZE];
p2p_key_hash(p2p_public_key(), hash);
p2p_lobby_slot_topic(hash, signalling.own_slot, sizeof(signalling.own_slot));
}
text = config_string("network.signalling_brokers");
while (*text && signalling.broker_count < MAXIMUM_BROKERS)
{
@@ -1164,7 +1611,8 @@ void p2p_signal_update(const int *read, int read_count, const int *write, int wr
for (index = 0; index < signalling.broker_count; index++)
{
struct broker *broker = &signalling.brokers[index];
int retry = RETRY_INTERVAL * (broker->failures < 4 ? broker->failures + 1 : 4);
int retry = broker->failures < 1 ? RETRY_INTERVAL :
broker->failures >= 4 ? MAXIMUM_RETRY_INTERVAL : RETRY_INTERVAL << (broker->failures - 1);
switch (broker->state)
{
@@ -1191,13 +1639,18 @@ void p2p_signal_update(const int *read, int read_count, const int *write, int wr
broker_readable(broker);
if (broker->state != _broker_ready)
continue;
if (elapsed(broker->heard_time, SILENCE_TIMEOUT))
if (elapsed(broker->heard_time, (unsigned long)broker->keep_alive * 1500 > SILENCE_TIMEOUT ?
(unsigned long)broker->keep_alive * 1500 : SILENCE_TIMEOUT))
{
broker_close(broker, 1);
continue;
}
if (elapsed(broker->sent_time, PING_INTERVAL))
if (elapsed(broker->sent_time, (unsigned long)broker->keep_alive * 500 < PING_INTERVAL ?
(unsigned long)broker->keep_alive * 500 : PING_INTERVAL))
{
broker_send(broker, 0xC0, NULL, 0);
}
broker_update_lobby(broker);
}
if (signalling.joining && !signalling.join_answered && elapsed(signalling.join_nonce_time, UNANSWERED_TIME))
{
@@ -1244,7 +1697,7 @@ void p2p_signal_publish(const char *topic, const unsigned char *payload, int siz
void p2p_signal_publish_retained(const char *topic, const unsigned char *payload, int size)
{
unsigned char body[2 + TOPIC_SIZE + MAXIMUM_MESSAGE_SIZE];
unsigned char body[3 + TOPIC_SIZE + MAXIMUM_MESSAGE_SIZE];
int index;
if (size > MAXIMUM_MESSAGE_SIZE)
@@ -1257,6 +1710,9 @@ void p2p_signal_publish_retained(const char *topic, const unsigned char *payload
if (broker->state != _broker_ready)
continue;
body_size = put_string(body, topic);
/* (MQTT 5: no properties) */
if (broker->protocol == 5)
body[body_size++] = 0;
if (size)
memcpy(body + body_size, payload, (size_t)size);
/* (PUBLISH, at most once, retained: an empty one clears it) */
@@ -1307,3 +1763,64 @@ void p2p_signal_stop_joining(void)
signalling.joining = 0;
sync_all_topics();
}
void p2p_signal_lobby_topics(int listed, int browsing)
{
signalling.lobby_listed = listed;
signalling.lobby_browsing = browsing;
sync_all_topics();
}
void p2p_signal_lobby_publish(const unsigned char *listing, int size, int closing)
{
if (size > MAXIMUM_LISTING_SIZE)
return;
memcpy(signalling.lobby_listing, listing, (size_t)size);
signalling.lobby_listing_size = size;
signalling.lobby_closing = closing;
signalling.lobby_version++;
/* (at once where the bucket allows; the rest in the coming passes) */
{
int index;
for (index = 0; index < signalling.broker_count; index++)
broker_update_lobby(&signalling.brokers[index]);
}
}
void p2p_signal_lobby_query(void)
{
int index;
for (index = 0; index < signalling.broker_count; index++)
{
if (signalling.brokers[index].topics[_topic_slots][0])
signalling.brokers[index].query_pending = 1;
broker_update_lobby(&signalling.brokers[index]);
}
}
void p2p_signal_lobby_quit(void)
{
int index;
for (index = 0; index < signalling.broker_count; index++)
{
struct broker *broker = &signalling.brokers[index];
static const unsigned char disconnect[2] = { 0, 0 };
if (broker->state != _broker_ready)
continue;
/* (unless sent already: the tombstone and the clearing) */
if (broker_carries_lobby(broker) && signalling.lobby_listing_size &&
(broker->lobby_version != signalling.lobby_version || broker->lobby_clear_pending))
{
broker_publish_slot(broker, next_packet_identifier(broker), signalling.lobby_listing,
signalling.lobby_listing_size, 0);
broker_publish_slot(broker, next_packet_identifier(broker), NULL, 0, 0);
}
/* DISCONNECT (MQTT 5: a normal one, its reason and no properties) */
broker_send(broker, 0xE0, disconnect, broker->protocol == 5 ? 2 : 0);
broker_flush(broker);
}
}
+13 -2
View File
@@ -103,8 +103,10 @@ static const struct config_setting config_settings[] =
"and a menus folder here for your own), \"xbox\" for the Xbox's." },
{ "display.player_names", _config_string, "\"all\"", "HALO_PLAYER_NAMES", _environment_value, _platform_all,
"In multiplayer, whose names are drawn above their heads: \"all\",\n"
"\"allies\", \"enemies\" or \"none\". An enemy's shows only while in sight\n"
"and not camouflaged." },
"\"allies\", \"enemies\" or \"none\". An enemy's shows only within the\n"
"motion sensor's reach, in sight and not camouflaged; none show if the\n"
"gametype's motion tracker shows no players, only allies' if it shows\n"
"only friends." },
{ "display.player_name_scale", _config_real, "1.0", "HALO_PLAYER_NAME_SCALE", _environment_value, _platform_all,
"How large the players' names are drawn: 1.0 three quarters of the size of\n"
"the HUD's text, 0.25 to 4." },
@@ -388,6 +390,15 @@ static const struct config_setting config_settings[] =
"networks whose NAT stops connections: when a player joins this\n"
"machine's game, and when joining a game takes too long. False never\n"
"asks." },
{ "network.public_lobby", _config_boolean, "true", "HALO_NET_PUBLIC_LOBBY", _environment_value, _platform_all,
"The server browser: public games are listed through the signalling\n"
"brokers, and Join Game > Server Browser shows them. False lists no\n"
"game of this machine's and shows none." },
{ "network.host_public", _config_boolean, "true", "HALO_NET_HOST_PUBLIC", _environment_value, _platform_all,
"Whether a new game of Create Game > Internet starts as PUBLIC (listed\n"
"in everyone's server browser: anyone can see and join it) or, false,\n"
"PRIVATE (only players with its invite link can join). Server Setup's\n"
"LISTING changes it for each game." },
{ "network.signalling_brokers", _config_string,
"\"broker.emqx.io:1883,broker.hivemq.com:1883,test.mosquitto.org:1883\"",
"HALO_NET_BROKERS", _environment_value, _platform_all,
+18 -2
View File
@@ -925,8 +925,10 @@ static void platform_show_pending_message(void)
/* ---------- events */
/* quits as closing the window does, when the events are next read (the
menus' Quit: port/linux/game/menu_functions.c); Android's menus have none,
as the system closes its apps */
menus' Quit: port/linux/game/menu_functions.c); on Android, where the
system closes its apps, the VR options' Quit (ui_widget.c) ends the process
at once: on the Steam Frame, Steam cannot end the game, which runs in
Lepton's container (port/android/STEAM_FRAME.md) */
void platform_request_quit(void)
{
#ifndef HALO_ANDROID
@@ -935,6 +937,20 @@ void platform_request_quit(void)
memset(&event, 0, sizeof(event));
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
#else
{
/* (and says so beside the game's data: the Steam Frame's launcher
does not then take it for a start that failed and start it again) */
char path[512];
FILE *file;
snprintf(path, sizeof(path), "%s/quit_requested.txt", platform_data_root());
file = fopen(path, "wb");
if (file)
fclose(file);
}
platform_log("quit: the player asked");
exit(EXIT_SUCCESS);
#endif
}
+25 -8
View File
@@ -132,6 +132,7 @@ static struct
unsigned long long last_frame_ms;
/* settings (config.toml, [vr]) */
unsigned long settings_read_at; /* config_changes() when the live ones were read */
int smooth_turn;
float snap_turn_degrees;
float smooth_turn_degrees_per_second;
@@ -418,22 +419,33 @@ static void world_to_local(const float world[3], float local[3])
/* ---------- platform side */
/* the settings the VR options (ui_widget.c) change while the game runs:
read at the start, and again whenever one changes */
static void vr_read_live_settings(void)
{
vr.settings_read_at = config_changes();
vr.smooth_turn = config_boolean("vr.smooth_turn");
vr.snap_turn_degrees = (float)config_real("vr.snap_turn_degrees");
vr.smooth_turn_degrees_per_second = (float)config_real("vr.smooth_turn_speed");
vr.motion_tracker_on_wrist = config_boolean("vr.motion_tracker_on_wrist");
vr.crosshair = config_boolean("vr.crosshair");
vr.laser = config_boolean("vr.laser");
vr.vehicle_wheel = config_boolean("vr.vehicle_wheel");
vr.comfort_vignette = (float)config_real("vr.comfort_vignette");
}
void halo_vr_initialize(void)
{
if (vr.active || !getenv("HALO_VR"))
return;
vr.smooth_turn = config_boolean("vr.smooth_turn");
vr.snap_turn_degrees = (float)config_real("vr.snap_turn_degrees");
vr.smooth_turn_degrees_per_second = (float)config_real("vr.smooth_turn_speed");
vr_read_live_settings();
vr.cinematics_3d = config_boolean("vr.cinematics_in_3d");
vr.hud_distance = (float)config_real("vr.hud_distance");
vr.hud_width = 2.0f * vr.hud_distance * tanf((float)config_real("vr.hud_field_of_view") * PI / 360.0f);
vr.screen_distance = (float)config_real("vr.screen_distance");
vr.screen_width = (float)config_real("vr.screen_width");
vr.positional_tracking = config_boolean("vr.positional_tracking");
vr.motion_tracker_on_wrist = config_boolean("vr.motion_tracker_on_wrist");
vr.scope_size = (float)config_real("vr.scope_size");
vr.crosshair = config_boolean("vr.crosshair");
vr.motion_tracker_size = (float)config_real("vr.motion_tracker_size");
vr.motion_tracker_turn = (float)config_real("vr.motion_tracker_turn");
vr.motion_tracker_right = (float)config_real("vr.motion_tracker_right");
@@ -443,7 +455,6 @@ void halo_vr_initialize(void)
vr.rise_limit = 0.25f;
vr.log_performance = config_boolean("vr.log_performance");
vr.aim_with_controller = strcmp(config_string("vr.aim_with"), "head") != 0;
vr.laser = config_boolean("vr.laser");
vr.weapon_offset[0] = (float)config_real("vr.weapon_forward");
vr.weapon_offset[1] = (float)config_real("vr.weapon_right");
vr.weapon_offset[2] = (float)config_real("vr.weapon_down");
@@ -465,13 +476,11 @@ void halo_vr_initialize(void)
vr.vehicle_steer_angle = (float)config_real("vr.vehicle_steer_degrees") * PI / 180.0f;
vr.vehicle_gun_steer_angle = (float)config_real("vr.vehicle_gun_steer_degrees") * PI / 180.0f;
vr.vehicle_pitch_angle = (float)config_real("vr.vehicle_pitch_degrees") * PI / 180.0f;
vr.vehicle_wheel = config_boolean("vr.vehicle_wheel");
vr.vehicle_wheel_angle = (float)config_real("vr.vehicle_wheel_degrees") * PI / 180.0f;
if (vr.vehicle_wheel_angle < 10.0f * PI / 180.0f)
vr.vehicle_wheel_angle = 10.0f * PI / 180.0f;
vr.vehicle_steer_curve = (float)config_real("vr.vehicle_steer_curve");
vr.vehicle_steer_smoothing = (float)config_real("vr.vehicle_steer_smoothing");
vr.comfort_vignette = (float)config_real("vr.comfort_vignette");
vr.grenade_scale = (float)config_real("vr.grenade_scale");
vr.grenade_offset[0] = (float)config_real("vr.grenade_right");
vr.grenade_offset[1] = 0.0f;
@@ -493,6 +502,11 @@ int halo_vr_active(void)
return vr.active;
}
int halo_vr_controller_layout(void)
{
return vr.active ? vr.frame.controller_layout : HALO_XR_LAYOUT_TOUCH;
}
int halo_vr_eye_size(int *width, int *height)
{
const char *eye_width = getenv("HALO_VR_EYE_WIDTH");
@@ -1191,6 +1205,8 @@ void halo_vr_frame_begin(void)
vr.recentre_frames--;
vr_recentre();
}
if (vr.settings_read_at != config_changes())
vr_read_live_settings();
turn_update();
wheel_update();
vignette_update();
@@ -2468,6 +2484,7 @@ int halo_vr_controllers(struct halo_xr_hand hands[HALO_XR_HAND_COUNT], int *menu
#else
int halo_vr_controller_layout(void) { return 0; }
int halo_vr_active(void) { return 0; }
int halo_vr_stereo_frame(void) { return 0; }
int halo_vr_stereo_player(short local_player_index, int cinematic) { (void)local_player_index; (void)cinematic; return 0; }
+45 -9
View File
@@ -55,6 +55,9 @@ drive the controller.
/* main/console.c */
extern unsigned char console_is_active(void);
/* port/linux/game/menu_functions.c: two players on this machine (the
campaign's co-op) */
extern unsigned char pc_menu_coop_players(void);
/* ---------- device tables */
@@ -635,6 +638,16 @@ static int sdl_gamepads(SDL_Gamepad *gamepads[PORT_COUNT])
return found;
}
/* the gamepad of a port: the first shares port 0 with the keyboard, but for
two players with one gamepad (co-op), port 1 has it (the keyboard's player
is 1, the gamepad's 2) */
static SDL_Gamepad *port_gamepad(SDL_Gamepad *gamepads[PORT_COUNT], int count, int port)
{
if (count == 1 && pc_menu_coop_players())
return port == 1 ? gamepads[0] : NULL;
return port < count ? gamepads[port] : NULL;
}
static SHORT stick(Sint16 value, BOOL flip)
{
int result = flip ? -(int)value - 1 : value;
@@ -718,9 +731,12 @@ static DWORD connected_gamepads(void)
DWORD mask = XDEVICE_PORT0_MASK;
int port;
/* the first pad shares port 0 with the keyboard */
for (port = 1; port < count; port++)
mask |= 1UL << port;
/* the first pad shares port 0 with the keyboard (port_gamepad) */
for (port = 1; port < PORT_COUNT; port++)
{
if (port_gamepad(gamepads, count, port))
mask |= 1UL << port;
}
return mask;
}
@@ -813,6 +829,15 @@ In the menus the buttons are the Xbox's by position: A accepts, B goes back. */
int halo_vr_controllers(struct halo_xr_hand hands[HALO_XR_HAND_COUNT], int *menus);
int halo_vr_wheel_held(void);
/* a click that stands for the pad's A (ui_widget.c: the screen where
controllers join by pressing it), held a moment */
static Uint64 vr_pad_a_until;
void halo_vr_pulse_pad_a(void)
{
vr_pad_a_until = SDL_GetTicks() + 120;
}
static SHORT vr_stick(float value)
{
if (value > 1.0f)
@@ -837,6 +862,12 @@ static void vr_gamepad_state(XINPUT_GAMEPAD *pad)
value = vr_stick(left->thumbstick[1]);
if (abs(value) > abs(pad->sThumbLY)) pad->sThumbLY = value;
if (left->buttons & HALO_XR_BUTTON_MENU) pad->wButtons |= XINPUT_GAMEPAD_START;
/* (the Steam Frame's controllers: the left one's View button and D-pad) */
if (left->buttons & HALO_XR_BUTTON_VIEW) pad->wButtons |= XINPUT_GAMEPAD_BACK;
if (left->buttons & HALO_XR_BUTTON_DPAD_UP) pad->wButtons |= XINPUT_GAMEPAD_DPAD_UP;
if (left->buttons & HALO_XR_BUTTON_DPAD_DOWN) pad->wButtons |= XINPUT_GAMEPAD_DPAD_DOWN;
if (left->buttons & HALO_XR_BUTTON_DPAD_LEFT) pad->wButtons |= XINPUT_GAMEPAD_DPAD_LEFT;
if (left->buttons & HALO_XR_BUTTON_DPAD_RIGHT) pad->wButtons |= XINPUT_GAMEPAD_DPAD_RIGHT;
if (left->buttons & HALO_XR_BUTTON_THUMBSTICK) pad->wButtons |= XINPUT_GAMEPAD_LEFT_THUMB;
if (right->buttons & HALO_XR_BUTTON_THUMBSTICK) pad->wButtons |= XINPUT_GAMEPAD_RIGHT_THUMB;
/* (in the menus the triggers click the pointer: halo_vr_menu_pointer) */
@@ -859,7 +890,8 @@ static void vr_gamepad_state(XINPUT_GAMEPAD *pad)
if (abs(value) > abs(pad->sThumbRX)) pad->sThumbRX = value;
value = vr_stick(right->thumbstick[1]);
if (abs(value) > abs(pad->sThumbRY)) pad->sThumbRY = value;
merge_button(pad, XINPUT_GAMEPAD_A, (right->buttons & HALO_XR_BUTTON_A) != 0);
merge_button(pad, XINPUT_GAMEPAD_A, (right->buttons & HALO_XR_BUTTON_A) != 0 ||
SDL_GetTicks() < vr_pad_a_until);
merge_button(pad, XINPUT_GAMEPAD_B, (right->buttons & HALO_XR_BUTTON_B) != 0);
merge_button(pad, XINPUT_GAMEPAD_X, (left->buttons & HALO_XR_BUTTON_A) != 0);
merge_button(pad, XINPUT_GAMEPAD_Y, (left->buttons & HALO_XR_BUTTON_B) != 0);
@@ -871,6 +903,10 @@ static void vr_gamepad_state(XINPUT_GAMEPAD *pad)
merge_button(pad, XINPUT_GAMEPAD_B,
((right->buttons & HALO_XR_BUTTON_SQUEEZE) != 0 && !halo_vr_wheel_held()) || halo_vr_melee());
merge_button(pad, XINPUT_GAMEPAD_Y, (left->buttons & HALO_XR_BUTTON_B) != 0);
/* (the Steam Frame's bumpers: the left switches grenades, the right is
the flashlight) */
merge_button(pad, XINPUT_GAMEPAD_BLACK, (left->buttons & HALO_XR_BUTTON_BUMPER) != 0);
merge_button(pad, XINPUT_GAMEPAD_WHITE, (right->buttons & HALO_XR_BUTTON_BUMPER) != 0);
{
/* B: a tap is the flashlight, holding it switches grenades; the
game sees a short press of either */
@@ -932,7 +968,7 @@ DWORD WINAPI XInputGetState(HANDLE device, PXINPUT_STATE state)
else
keyboard_controls(&input, &state->Gamepad);
}
if (count > 0)
if (port_gamepad(gamepads, count, 0))
sdl_gamepad_state(gamepads[0], &state->Gamepad);
#ifdef HALO_ANDROID
vr_gamepad_state(&state->Gamepad);
@@ -946,9 +982,9 @@ DWORD WINAPI XInputGetState(HANDLE device, PXINPUT_STATE state)
pthread_mutex_unlock(&mouse_lock);
}
}
else if (port < count)
else if (port_gamepad(gamepads, count, port))
{
sdl_gamepad_state(gamepads[port], &state->Gamepad);
sdl_gamepad_state(port_gamepad(gamepads, count, port), &state->Gamepad);
}
if (memcmp(&state->Gamepad, &controllers[port].previous, sizeof(state->Gamepad)))
@@ -981,11 +1017,11 @@ DWORD WINAPI XInputSetState(HANDLE device, PXINPUT_FEEDBACK feedback)
}
#endif
count = sdl_gamepads(gamepads);
if (port < count)
if (port_gamepad(gamepads, count, port))
{
/* the game refreshes the motors every frame; rumble a little longer
than that so they do not stutter */
SDL_RumbleGamepad(gamepads[port], feedback->Rumble.wLeftMotorSpeed,
SDL_RumbleGamepad(port_gamepad(gamepads, count, port), feedback->Rumble.wLeftMotorSpeed,
feedback->Rumble.wRightMotorSpeed, 100);
}
return ERROR_SUCCESS;
+167
View File
@@ -0,0 +1,167 @@
Monocypher as a whole is dual-licensed. Choose whichever licence you
want from the two licences listed below.
The first licence is a regular 2-clause BSD licence. The second licence
is the CC-0 from Creative Commons. It is intended to release Monocypher
to the public domain. The BSD licence serves as a fallback option.
See the individual files for specific information about who contributed
to what file during which years. See below for special notes.
Licence 1 (2-clause BSD)
------------------------
Copyright (c) 2017-2023, Loup Vaillant
Copyright (c) 2017-2019, Michael Savage
Copyright (c) 2017-2023, Fabio Scotoni
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Licence 2 (CC-0)
----------------
> CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
> LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
> ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
> INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
> REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
> PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
> THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
> HEREUNDER.
### Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work
of authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without
fear of later claims of infringement build upon, modify, incorporate in
other works, reuse and redistribute as freely as possible in any form
whatsoever and for any purposes, including without limitation commercial
purposes. These owners may contribute to the Commons to promote the
ideal of a free culture and the further production of creative, cultural
and scientific works, or to gain reputation or greater distribution for
their Work in part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or
she is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under
its terms, with knowledge of his or her Copyright and Related Rights in
the Work and the meaning and intended legal effect of CC0 on those
rights.
1. **Copyright and Related Rights.** A Work made available under CC0 may
be protected by copyright and related or neighboring rights
("Copyright and Related Rights"). Copyright and Related Rights
include, but are not limited to, the following:
- the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
- moral rights retained by the original author(s) and/or
performer(s); publicity and privacy rights pertaining to a person's
image or likeness depicted in a Work;
- rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
- rights protecting the extraction, dissemination, use and reuse of
data in a Work;
- database rights (such as those arising under Directive 96/9/EC of
the European Parliament and of the Council of 11 March 1996 on the
legal protection of databases, and under any national
implementation thereof, including any amended or successor version
of such directive); and
- other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. **Waiver.** To the greatest extent permitted by, but not in
contravention of, applicable law, Affirmer hereby overtly, fully,
permanently, irrevocably and unconditionally waives, abandons, and
surrenders all of Affirmer's Copyright and Related Rights and
associated claims and causes of action, whether now known or unknown
(including existing as well as future claims and causes of action),
in the Work (i) in all territories worldwide, (ii) for the maximum
duration provided by applicable law or treaty (including future time
extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"Waiver"). Affirmer makes the Waiver for the benefit of each member
of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal
or equitable action to disrupt the quiet enjoyment of the Work by the
public as contemplated by Affirmer's express Statement of Purpose.
3. **Public License Fallback.** Should any part of the Waiver for any
reason be judged legally invalid or ineffective under applicable law,
then the Waiver shall be preserved to the maximum extent permitted
taking into account Affirmer's express Statement of Purpose. In
addition, to the extent the Waiver is so judged Affirmer hereby
grants to each affected person a royalty-free, non transferable, non
sublicensable, non exclusive, irrevocable and unconditional license
to exercise Affirmer's Copyright and Related Rights in the Work (i)
in all territories worldwide, (ii) for the maximum duration provided
by applicable law or treaty (including future time extensions), (iii)
in any current or future medium and for any number of copies, and
(iv) for any purpose whatsoever, including without limitation
commercial, advertising or promotional purposes (the "License"). The
License shall be deemed effective as of the date CC0 was applied by
Affirmer to the Work. Should any part of the License for any reason
be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the
remainder of the License, and in such case Affirmer hereby affirms
that he or she will not (i) exercise any of his or her remaining
Copyright and Related Rights in the Work or (ii) assert any
associated claims and causes of action with respect to the Work, in
either case contrary to Affirmer's express Statement of Purpose.
4. **Limitations and Disclaimers.**
- No trademark or patent rights held by Affirmer are waived,
abandoned, surrendered, licensed or otherwise affected by this
document.
- Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy,
or the present or absence of errors, whether or not discoverable,
all to the greatest extent permissible under applicable law.
- Affirmer disclaims responsibility for clearing rights of other
persons that may apply to the Work or any use thereof, including
without limitation any person's Copyright and Related Rights in the
Work. Further, Affirmer disclaims responsibility for obtaining any
necessary consents, permissions or other rights required for any
use of the Work.
- Affirmer understands and acknowledges that Creative Commons is not
a party to this document and has no duty or obligation with respect
to this CC0 or use of the Work.
+19
View File
@@ -0,0 +1,19 @@
# Monocypher
A small cryptography library in C, by Loup Vaillant, Michael Savage and
Fabio Scotoni, dual-licensed CC0-1.0 or BSD 2-Clause (see `LICENCE.md`).
Upstream: https://monocypher.org (https://github.com/LoupVaillant/Monocypher),
release `4.0.3`, `monocypher-4.0.3.tar.gz`, SHA-256
`8cc9bc341a66249016db9bd70e9142d8d0aef9945973744b1ac05dbc55d8ee66`
(its sources match the repository's `4.0.3` tag but for their first line,
where the release writes its version).
Only `src/monocypher.c`, `src/monocypher.h`, `src/optional/monocypher-ed25519.c`,
`src/optional/monocypher-ed25519.h` and `LICENCE.md` are kept, unchanged.
Internet play (`port/linux/src/p2p_crypto.c`) uses its Ed25519 (with
SHA-512) and its conversion of Ed25519 public keys to X25519: a run's key
signs the listing of a public game in the server browser
(`port/linux/src/p2p_lobby.c`). It builds into the Linux, Windows and
Android platform layers.
+503
View File
@@ -0,0 +1,503 @@
// Monocypher version 4.0.3
//
// This file is dual-licensed. Choose whichever licence you want from
// the two licences listed below.
//
// The first licence is a regular 2-clause BSD licence. The second licence
// is the CC-0 from Creative Commons. It is intended to release Monocypher
// to the public domain. The BSD licence serves as a fallback option.
//
// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0
//
// ------------------------------------------------------------------------
//
// Copyright (c) 2017-2019, Loup Vaillant
// All rights reserved.
//
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the
// distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// ------------------------------------------------------------------------
//
// Written in 2017-2019 by Loup Vaillant
//
// To the extent possible under law, the author(s) have dedicated all copyright
// and related neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
//
// You should have received a copy of the CC0 Public Domain Dedication along
// with this software. If not, see
// <https://creativecommons.org/publicdomain/zero/1.0/>
#include "monocypher-ed25519.h"
#ifdef MONOCYPHER_CPP_NAMESPACE
namespace MONOCYPHER_CPP_NAMESPACE {
#endif
/////////////////
/// Utilities ///
/////////////////
#define FOR(i, min, max) for (size_t i = min; i < max; i++)
#define COPY(dst, src, size) FOR(_i_, 0, size) (dst)[_i_] = (src)[_i_]
#define ZERO(buf, size) FOR(_i_, 0, size) (buf)[_i_] = 0
#define WIPE_CTX(ctx) crypto_wipe(ctx , sizeof(*(ctx)))
#define WIPE_BUFFER(buffer) crypto_wipe(buffer, sizeof(buffer))
#define MIN(a, b) ((a) <= (b) ? (a) : (b))
typedef uint8_t u8;
typedef uint64_t u64;
// Returns the smallest positive integer y such that
// (x + y) % pow_2 == 0
// Basically, it's how many bytes we need to add to "align" x.
// Only works when pow_2 is a power of 2.
// Note: we use ~x+1 instead of -x to avoid compiler warnings
static size_t align(size_t x, size_t pow_2)
{
return (~x + 1) & (pow_2 - 1);
}
static u64 load64_be(const u8 s[8])
{
return((u64)s[0] << 56)
| ((u64)s[1] << 48)
| ((u64)s[2] << 40)
| ((u64)s[3] << 32)
| ((u64)s[4] << 24)
| ((u64)s[5] << 16)
| ((u64)s[6] << 8)
| (u64)s[7];
}
static void store64_be(u8 out[8], u64 in)
{
out[0] = (u8)(in >> 56);
out[1] = (u8)(in >> 48);
out[2] = (u8)(in >> 40);
out[3] = (u8)(in >> 32);
out[4] = (u8)(in >> 24);
out[5] = (u8)(in >> 16);
out[6] = (u8)(in >> 8);
out[7] = (u8) in ;
}
static void load64_be_buf (u64 *dst, const u8 *src, size_t size) {
FOR(i, 0, size) { dst[i] = load64_be(src + i*8); }
}
///////////////
/// SHA 512 ///
///////////////
static u64 rot(u64 x, int c ) { return (x >> c) | (x << (64 - c)); }
static u64 ch (u64 x, u64 y, u64 z) { return (x & y) ^ (~x & z); }
static u64 maj(u64 x, u64 y, u64 z) { return (x & y) ^ ( x & z) ^ (y & z); }
static u64 big_sigma0(u64 x) { return rot(x, 28) ^ rot(x, 34) ^ rot(x, 39); }
static u64 big_sigma1(u64 x) { return rot(x, 14) ^ rot(x, 18) ^ rot(x, 41); }
static u64 lit_sigma0(u64 x) { return rot(x, 1) ^ rot(x, 8) ^ (x >> 7); }
static u64 lit_sigma1(u64 x) { return rot(x, 19) ^ rot(x, 61) ^ (x >> 6); }
static const u64 K[80] = {
0x428a2f98d728ae22,0x7137449123ef65cd,0xb5c0fbcfec4d3b2f,0xe9b5dba58189dbbc,
0x3956c25bf348b538,0x59f111f1b605d019,0x923f82a4af194f9b,0xab1c5ed5da6d8118,
0xd807aa98a3030242,0x12835b0145706fbe,0x243185be4ee4b28c,0x550c7dc3d5ffb4e2,
0x72be5d74f27b896f,0x80deb1fe3b1696b1,0x9bdc06a725c71235,0xc19bf174cf692694,
0xe49b69c19ef14ad2,0xefbe4786384f25e3,0x0fc19dc68b8cd5b5,0x240ca1cc77ac9c65,
0x2de92c6f592b0275,0x4a7484aa6ea6e483,0x5cb0a9dcbd41fbd4,0x76f988da831153b5,
0x983e5152ee66dfab,0xa831c66d2db43210,0xb00327c898fb213f,0xbf597fc7beef0ee4,
0xc6e00bf33da88fc2,0xd5a79147930aa725,0x06ca6351e003826f,0x142929670a0e6e70,
0x27b70a8546d22ffc,0x2e1b21385c26c926,0x4d2c6dfc5ac42aed,0x53380d139d95b3df,
0x650a73548baf63de,0x766a0abb3c77b2a8,0x81c2c92e47edaee6,0x92722c851482353b,
0xa2bfe8a14cf10364,0xa81a664bbc423001,0xc24b8b70d0f89791,0xc76c51a30654be30,
0xd192e819d6ef5218,0xd69906245565a910,0xf40e35855771202a,0x106aa07032bbd1b8,
0x19a4c116b8d2d0c8,0x1e376c085141ab53,0x2748774cdf8eeb99,0x34b0bcb5e19b48a8,
0x391c0cb3c5c95a63,0x4ed8aa4ae3418acb,0x5b9cca4f7763e373,0x682e6ff3d6b2b8a3,
0x748f82ee5defb2fc,0x78a5636f43172f60,0x84c87814a1f0ab72,0x8cc702081a6439ec,
0x90befffa23631e28,0xa4506cebde82bde9,0xbef9a3f7b2c67915,0xc67178f2e372532b,
0xca273eceea26619c,0xd186b8c721c0c207,0xeada7dd6cde0eb1e,0xf57d4f7fee6ed178,
0x06f067aa72176fba,0x0a637dc5a2c898a6,0x113f9804bef90dae,0x1b710b35131c471b,
0x28db77f523047d84,0x32caab7b40c72493,0x3c9ebe0a15c9bebc,0x431d67c49c100d4c,
0x4cc5d4becb3e42b6,0x597f299cfc657e2a,0x5fcb6fab3ad6faec,0x6c44198c4a475817
};
static void sha512_compress(crypto_sha512_ctx *ctx)
{
u64 a = ctx->hash[0]; u64 b = ctx->hash[1];
u64 c = ctx->hash[2]; u64 d = ctx->hash[3];
u64 e = ctx->hash[4]; u64 f = ctx->hash[5];
u64 g = ctx->hash[6]; u64 h = ctx->hash[7];
FOR (j, 0, 16) {
u64 in = K[j] + ctx->input[j];
u64 t1 = big_sigma1(e) + ch (e, f, g) + h + in;
u64 t2 = big_sigma0(a) + maj(a, b, c);
h = g; g = f; f = e; e = d + t1;
d = c; c = b; b = a; a = t1 + t2;
}
size_t i16 = 0;
FOR(i, 1, 5) {
i16 += 16;
FOR (j, 0, 16) {
ctx->input[j] += lit_sigma1(ctx->input[(j- 2) & 15]);
ctx->input[j] += lit_sigma0(ctx->input[(j-15) & 15]);
ctx->input[j] += ctx->input[(j- 7) & 15];
u64 in = K[i16 + j] + ctx->input[j];
u64 t1 = big_sigma1(e) + ch (e, f, g) + h + in;
u64 t2 = big_sigma0(a) + maj(a, b, c);
h = g; g = f; f = e; e = d + t1;
d = c; c = b; b = a; a = t1 + t2;
}
}
ctx->hash[0] += a; ctx->hash[1] += b;
ctx->hash[2] += c; ctx->hash[3] += d;
ctx->hash[4] += e; ctx->hash[5] += f;
ctx->hash[6] += g; ctx->hash[7] += h;
}
// Write 1 input byte
static void sha512_set_input(crypto_sha512_ctx *ctx, u8 input)
{
size_t word = ctx->input_idx >> 3;
size_t byte = ctx->input_idx & 7;
ctx->input[word] |= (u64)input << (8 * (7 - byte));
}
// Increment a 128-bit "word".
static void sha512_incr(u64 x[2], u64 y)
{
x[1] += y;
if (x[1] < y) {
x[0]++;
}
}
void crypto_sha512_init(crypto_sha512_ctx *ctx)
{
ctx->hash[0] = 0x6a09e667f3bcc908;
ctx->hash[1] = 0xbb67ae8584caa73b;
ctx->hash[2] = 0x3c6ef372fe94f82b;
ctx->hash[3] = 0xa54ff53a5f1d36f1;
ctx->hash[4] = 0x510e527fade682d1;
ctx->hash[5] = 0x9b05688c2b3e6c1f;
ctx->hash[6] = 0x1f83d9abfb41bd6b;
ctx->hash[7] = 0x5be0cd19137e2179;
ctx->input_size[0] = 0;
ctx->input_size[1] = 0;
ctx->input_idx = 0;
ZERO(ctx->input, 16);
}
void crypto_sha512_update(crypto_sha512_ctx *ctx,
const u8 *message, size_t message_size)
{
// Avoid undefined NULL pointer increments with empty messages
if (message_size == 0) {
return;
}
// Align ourselves with word boundaries
if ((ctx->input_idx & 7) != 0) {
size_t nb_bytes = MIN(align(ctx->input_idx, 8), message_size);
FOR (i, 0, nb_bytes) {
sha512_set_input(ctx, message[i]);
ctx->input_idx++;
}
message += nb_bytes;
message_size -= nb_bytes;
}
// Align ourselves with block boundaries
if ((ctx->input_idx & 127) != 0) {
size_t nb_words = MIN(align(ctx->input_idx, 128), message_size) >> 3;
load64_be_buf(ctx->input + (ctx->input_idx >> 3), message, nb_words);
ctx->input_idx += nb_words << 3;
message += nb_words << 3;
message_size -= nb_words << 3;
}
// Compress block if needed
if (ctx->input_idx == 128) {
sha512_incr(ctx->input_size, 1024); // size is in bits
sha512_compress(ctx);
ctx->input_idx = 0;
ZERO(ctx->input, 16);
}
// Process the message block by block
FOR (i, 0, message_size >> 7) { // number of blocks
load64_be_buf(ctx->input, message, 16);
sha512_incr(ctx->input_size, 1024); // size is in bits
sha512_compress(ctx);
ctx->input_idx = 0;
ZERO(ctx->input, 16);
message += 128;
}
message_size &= 127;
if (message_size != 0) {
// Remaining words
size_t nb_words = message_size >> 3;
load64_be_buf(ctx->input, message, nb_words);
ctx->input_idx += nb_words << 3;
message += nb_words << 3;
message_size -= nb_words << 3;
// Remaining bytes
FOR (i, 0, message_size) {
sha512_set_input(ctx, message[i]);
ctx->input_idx++;
}
}
}
void crypto_sha512_final(crypto_sha512_ctx *ctx, u8 hash[64])
{
// Add padding bit
if (ctx->input_idx == 0) {
ZERO(ctx->input, 16);
}
sha512_set_input(ctx, 128);
// Update size
sha512_incr(ctx->input_size, ctx->input_idx * 8);
// Compress penultimate block (if any)
if (ctx->input_idx > 111) {
sha512_compress(ctx);
ZERO(ctx->input, 14);
}
// Compress last block
ctx->input[14] = ctx->input_size[0];
ctx->input[15] = ctx->input_size[1];
sha512_compress(ctx);
// Copy hash to output (big endian)
FOR (i, 0, 8) {
store64_be(hash + i*8, ctx->hash[i]);
}
WIPE_CTX(ctx);
}
void crypto_sha512(u8 hash[64], const u8 *message, size_t message_size)
{
crypto_sha512_ctx ctx;
crypto_sha512_init (&ctx);
crypto_sha512_update(&ctx, message, message_size);
crypto_sha512_final (&ctx, hash);
}
////////////////////
/// HMAC SHA 512 ///
////////////////////
void crypto_sha512_hmac_init(crypto_sha512_hmac_ctx *ctx,
const u8 *key, size_t key_size)
{
// hash key if it is too long
if (key_size > 128) {
crypto_sha512(ctx->key, key, key_size);
key = ctx->key;
key_size = 64;
}
// Compute inner key: padded key XOR 0x36
FOR (i, 0, key_size) { ctx->key[i] = key[i] ^ 0x36; }
FOR (i, key_size, 128) { ctx->key[i] = 0x36; }
// Start computing inner hash
crypto_sha512_init (&ctx->ctx);
crypto_sha512_update(&ctx->ctx, ctx->key, 128);
}
void crypto_sha512_hmac_update(crypto_sha512_hmac_ctx *ctx,
const u8 *message, size_t message_size)
{
crypto_sha512_update(&ctx->ctx, message, message_size);
}
void crypto_sha512_hmac_final(crypto_sha512_hmac_ctx *ctx, u8 hmac[64])
{
// Finish computing inner hash
crypto_sha512_final(&ctx->ctx, hmac);
// Compute outer key: padded key XOR 0x5c
FOR (i, 0, 128) {
ctx->key[i] ^= 0x36 ^ 0x5c;
}
// Compute outer hash
crypto_sha512_init (&ctx->ctx);
crypto_sha512_update(&ctx->ctx, ctx->key , 128);
crypto_sha512_update(&ctx->ctx, hmac, 64);
crypto_sha512_final (&ctx->ctx, hmac); // outer hash
WIPE_CTX(ctx);
}
void crypto_sha512_hmac(u8 hmac[64], const u8 *key, size_t key_size,
const u8 *message, size_t message_size)
{
crypto_sha512_hmac_ctx ctx;
crypto_sha512_hmac_init (&ctx, key, key_size);
crypto_sha512_hmac_update(&ctx, message, message_size);
crypto_sha512_hmac_final (&ctx, hmac);
}
////////////////////
/// HKDF SHA 512 ///
////////////////////
void crypto_sha512_hkdf_expand(u8 *okm, size_t okm_size,
const u8 *prk, size_t prk_size,
const u8 *info, size_t info_size)
{
int not_first = 0;
u8 ctr = 1;
u8 blk[64];
while (okm_size > 0) {
size_t out_size = MIN(okm_size, sizeof(blk));
crypto_sha512_hmac_ctx ctx;
crypto_sha512_hmac_init(&ctx, prk , prk_size);
if (not_first) {
// For some reason HKDF uses some kind of CBC mode.
// For some reason CTR mode alone wasn't enough.
// Like what, they didn't trust HMAC in 2010? Really??
crypto_sha512_hmac_update(&ctx, blk , sizeof(blk));
}
crypto_sha512_hmac_update(&ctx, info, info_size);
crypto_sha512_hmac_update(&ctx, &ctr, 1);
crypto_sha512_hmac_final(&ctx, blk);
COPY(okm, blk, out_size);
not_first = 1;
okm += out_size;
okm_size -= out_size;
ctr++;
}
WIPE_BUFFER(blk);
}
void crypto_sha512_hkdf(u8 *okm , size_t okm_size,
const u8 *ikm , size_t ikm_size,
const u8 *salt, size_t salt_size,
const u8 *info, size_t info_size)
{
// Extract
u8 prk[64];
crypto_sha512_hmac(prk, salt, salt_size, ikm, ikm_size);
// Expand
crypto_sha512_hkdf_expand(okm, okm_size, prk, sizeof(prk), info, info_size);
WIPE_BUFFER(prk);
}
///////////////
/// Ed25519 ///
///////////////
void crypto_ed25519_key_pair(u8 secret_key[64], u8 public_key[32], u8 seed[32])
{
u8 a[64];
COPY(a, seed, 32); // a[ 0..31] = seed
crypto_wipe(seed, 32);
COPY(secret_key, a, 32); // secret key = seed
crypto_sha512(a, a, 32); // a[ 0..31] = scalar
crypto_eddsa_trim_scalar(a, a); // a[ 0..31] = trimmed scalar
crypto_eddsa_scalarbase(public_key, a); // public key = [trimmed scalar]B
COPY(secret_key + 32, public_key, 32); // secret key includes public half
WIPE_BUFFER(a);
}
static void hash_reduce(u8 h[32],
const u8 *a, size_t a_size,
const u8 *b, size_t b_size,
const u8 *c, size_t c_size,
const u8 *d, size_t d_size)
{
u8 hash[64];
crypto_sha512_ctx ctx;
crypto_sha512_init (&ctx);
crypto_sha512_update(&ctx, a, a_size);
crypto_sha512_update(&ctx, b, b_size);
crypto_sha512_update(&ctx, c, c_size);
crypto_sha512_update(&ctx, d, d_size);
crypto_sha512_final (&ctx, hash);
crypto_eddsa_reduce(h, hash);
}
static void ed25519_dom_sign(u8 signature[64], const u8 secret_key[64],
const u8 *dom, size_t dom_size,
const u8 *message, size_t message_size)
{
u8 a[64]; // secret scalar and prefix
u8 r[32]; // secret deterministic "random" nonce
u8 h[32]; // publically verifiable hash of the message (not wiped)
u8 R[32]; // first half of the signature (allows overlapping inputs)
const u8 *pk = secret_key + 32;
crypto_sha512(a, secret_key, 32);
crypto_eddsa_trim_scalar(a, a);
hash_reduce(r, dom, dom_size, a + 32, 32, message, message_size, 0, 0);
crypto_eddsa_scalarbase(R, r);
hash_reduce(h, dom, dom_size, R, 32, pk, 32, message, message_size);
COPY(signature, R, 32);
crypto_eddsa_mul_add(signature + 32, h, a, r);
WIPE_BUFFER(a);
WIPE_BUFFER(r);
}
void crypto_ed25519_sign(u8 signature [64], const u8 secret_key[64],
const u8 *message, size_t message_size)
{
ed25519_dom_sign(signature, secret_key, 0, 0, message, message_size);
}
int crypto_ed25519_check(const u8 signature[64], const u8 public_key[32],
const u8 *msg, size_t msg_size)
{
u8 h_ram[32];
hash_reduce(h_ram, signature, 32, public_key, 32, msg, msg_size, 0, 0);
return crypto_eddsa_check_equation(signature, public_key, h_ram);
}
static const u8 domain[34] = "SigEd25519 no Ed25519 collisions\1";
void crypto_ed25519_ph_sign(uint8_t signature[64], const uint8_t secret_key[64],
const uint8_t message_hash[64])
{
ed25519_dom_sign(signature, secret_key, domain, sizeof(domain),
message_hash, 64);
}
int crypto_ed25519_ph_check(const uint8_t sig[64], const uint8_t pk[32],
const uint8_t msg_hash[64])
{
u8 h_ram[32];
hash_reduce(h_ram, domain, sizeof(domain), sig, 32, pk, 32, msg_hash, 64);
return crypto_eddsa_check_equation(sig, pk, h_ram);
}
#ifdef MONOCYPHER_CPP_NAMESPACE
}
#endif
+140
View File
@@ -0,0 +1,140 @@
// Monocypher version 4.0.3
//
// This file is dual-licensed. Choose whichever licence you want from
// the two licences listed below.
//
// The first licence is a regular 2-clause BSD licence. The second licence
// is the CC-0 from Creative Commons. It is intended to release Monocypher
// to the public domain. The BSD licence serves as a fallback option.
//
// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0
//
// ------------------------------------------------------------------------
//
// Copyright (c) 2017-2019, Loup Vaillant
// All rights reserved.
//
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the
// distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// ------------------------------------------------------------------------
//
// Written in 2017-2019 by Loup Vaillant
//
// To the extent possible under law, the author(s) have dedicated all copyright
// and related neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
//
// You should have received a copy of the CC0 Public Domain Dedication along
// with this software. If not, see
// <https://creativecommons.org/publicdomain/zero/1.0/>
#ifndef ED25519_H
#define ED25519_H
#include "monocypher.h"
#ifdef MONOCYPHER_CPP_NAMESPACE
namespace MONOCYPHER_CPP_NAMESPACE {
#elif defined(__cplusplus)
extern "C" {
#endif
////////////////////////
/// Type definitions ///
////////////////////////
// Do not rely on the size or content on any of those types,
// they may change without notice.
typedef struct {
uint64_t hash[8];
uint64_t input[16];
uint64_t input_size[2];
size_t input_idx;
} crypto_sha512_ctx;
typedef struct {
uint8_t key[128];
crypto_sha512_ctx ctx;
} crypto_sha512_hmac_ctx;
// SHA 512
// -------
void crypto_sha512_init (crypto_sha512_ctx *ctx);
void crypto_sha512_update(crypto_sha512_ctx *ctx,
const uint8_t *message, size_t message_size);
void crypto_sha512_final (crypto_sha512_ctx *ctx, uint8_t hash[64]);
void crypto_sha512(uint8_t hash[64],
const uint8_t *message, size_t message_size);
// SHA 512 HMAC
// ------------
void crypto_sha512_hmac_init(crypto_sha512_hmac_ctx *ctx,
const uint8_t *key, size_t key_size);
void crypto_sha512_hmac_update(crypto_sha512_hmac_ctx *ctx,
const uint8_t *message, size_t message_size);
void crypto_sha512_hmac_final(crypto_sha512_hmac_ctx *ctx, uint8_t hmac[64]);
void crypto_sha512_hmac(uint8_t hmac[64],
const uint8_t *key , size_t key_size,
const uint8_t *message, size_t message_size);
// SHA 512 HKDF
// ------------
void crypto_sha512_hkdf_expand(uint8_t *okm, size_t okm_size,
const uint8_t *prk, size_t prk_size,
const uint8_t *info, size_t info_size);
void crypto_sha512_hkdf(uint8_t *okm , size_t okm_size,
const uint8_t *ikm , size_t ikm_size,
const uint8_t *salt, size_t salt_size,
const uint8_t *info, size_t info_size);
// Ed25519
// -------
// Signatures (EdDSA with curve25519 + SHA-512)
// --------------------------------------------
void crypto_ed25519_key_pair(uint8_t secret_key[64],
uint8_t public_key[32],
uint8_t seed[32]);
void crypto_ed25519_sign(uint8_t signature [64],
const uint8_t secret_key[64],
const uint8_t *message, size_t message_size);
int crypto_ed25519_check(const uint8_t signature [64],
const uint8_t public_key[32],
const uint8_t *message, size_t message_size);
// Pre-hash variants
void crypto_ed25519_ph_sign(uint8_t signature [64],
const uint8_t secret_key [64],
const uint8_t message_hash[64]);
int crypto_ed25519_ph_check(const uint8_t signature [64],
const uint8_t public_key [32],
const uint8_t message_hash[64]);
#ifdef __cplusplus
}
#endif
#endif // ED25519_H
File diff suppressed because it is too large. Load diff
+321
View File
@@ -0,0 +1,321 @@
// Monocypher version 4.0.3
//
// This file is dual-licensed. Choose whichever licence you want from
// the two licences listed below.
//
// The first licence is a regular 2-clause BSD licence. The second licence
// is the CC-0 from Creative Commons. It is intended to release Monocypher
// to the public domain. The BSD licence serves as a fallback option.
//
// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0
//
// ------------------------------------------------------------------------
//
// Copyright (c) 2017-2019, Loup Vaillant
// All rights reserved.
//
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the
// distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// ------------------------------------------------------------------------
//
// Written in 2017-2019 by Loup Vaillant
//
// To the extent possible under law, the author(s) have dedicated all copyright
// and related neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
//
// You should have received a copy of the CC0 Public Domain Dedication along
// with this software. If not, see
// <https://creativecommons.org/publicdomain/zero/1.0/>
#ifndef MONOCYPHER_H
#define MONOCYPHER_H
#include <stddef.h>
#include <stdint.h>
#ifdef MONOCYPHER_CPP_NAMESPACE
namespace MONOCYPHER_CPP_NAMESPACE {
#elif defined(__cplusplus)
extern "C" {
#endif
// Constant time comparisons
// -------------------------
// Return 0 if a and b are equal, -1 otherwise
int crypto_verify16(const uint8_t a[16], const uint8_t b[16]);
int crypto_verify32(const uint8_t a[32], const uint8_t b[32]);
int crypto_verify64(const uint8_t a[64], const uint8_t b[64]);
// Erase sensitive data
// --------------------
void crypto_wipe(void *secret, size_t size);
// Authenticated encryption
// ------------------------
void crypto_aead_lock(uint8_t *cipher_text,
uint8_t mac [16],
const uint8_t key [32],
const uint8_t nonce[24],
const uint8_t *ad, size_t ad_size,
const uint8_t *plain_text, size_t text_size);
int crypto_aead_unlock(uint8_t *plain_text,
const uint8_t mac [16],
const uint8_t key [32],
const uint8_t nonce[24],
const uint8_t *ad, size_t ad_size,
const uint8_t *cipher_text, size_t text_size);
// Authenticated stream
// --------------------
typedef struct {
uint64_t counter;
uint8_t key[32];
uint8_t nonce[8];
} crypto_aead_ctx;
void crypto_aead_init_x(crypto_aead_ctx *ctx,
const uint8_t key[32], const uint8_t nonce[24]);
void crypto_aead_init_djb(crypto_aead_ctx *ctx,
const uint8_t key[32], const uint8_t nonce[8]);
void crypto_aead_init_ietf(crypto_aead_ctx *ctx,
const uint8_t key[32], const uint8_t nonce[12]);
void crypto_aead_write(crypto_aead_ctx *ctx,
uint8_t *cipher_text,
uint8_t mac[16],
const uint8_t *ad , size_t ad_size,
const uint8_t *plain_text, size_t text_size);
int crypto_aead_read(crypto_aead_ctx *ctx,
uint8_t *plain_text,
const uint8_t mac[16],
const uint8_t *ad , size_t ad_size,
const uint8_t *cipher_text, size_t text_size);
// General purpose hash (BLAKE2b)
// ------------------------------
// Direct interface
void crypto_blake2b(uint8_t *hash, size_t hash_size,
const uint8_t *message, size_t message_size);
void crypto_blake2b_keyed(uint8_t *hash, size_t hash_size,
const uint8_t *key, size_t key_size,
const uint8_t *message, size_t message_size);
// Incremental interface
typedef struct {
// Do not rely on the size or contents of this type,
// for they may change without notice.
uint64_t hash[8];
uint64_t input_offset[2];
uint64_t input[16];
size_t input_idx;
size_t hash_size;
} crypto_blake2b_ctx;
void crypto_blake2b_init(crypto_blake2b_ctx *ctx, size_t hash_size);
void crypto_blake2b_keyed_init(crypto_blake2b_ctx *ctx, size_t hash_size,
const uint8_t *key, size_t key_size);
void crypto_blake2b_update(crypto_blake2b_ctx *ctx,
const uint8_t *message, size_t message_size);
void crypto_blake2b_final(crypto_blake2b_ctx *ctx, uint8_t *hash);
// Password key derivation (Argon2)
// --------------------------------
#define CRYPTO_ARGON2_D 0
#define CRYPTO_ARGON2_I 1
#define CRYPTO_ARGON2_ID 2
typedef struct {
uint32_t algorithm; // Argon2d, Argon2i, Argon2id
uint32_t nb_blocks; // memory hardness, >= 8 * nb_lanes
uint32_t nb_passes; // CPU hardness, >= 1 (>= 3 recommended for Argon2i)
uint32_t nb_lanes; // parallelism level (single threaded anyway)
} crypto_argon2_config;
typedef struct {
const uint8_t *pass;
const uint8_t *salt;
uint32_t pass_size;
uint32_t salt_size; // 16 bytes recommended
} crypto_argon2_inputs;
typedef struct {
const uint8_t *key; // may be NULL if no key
const uint8_t *ad; // may be NULL if no additional data
uint32_t key_size; // 0 if no key (32 bytes recommended otherwise)
uint32_t ad_size; // 0 if no additional data
} crypto_argon2_extras;
extern const crypto_argon2_extras crypto_argon2_no_extras;
void crypto_argon2(uint8_t *hash, uint32_t hash_size, void *work_area,
crypto_argon2_config config,
crypto_argon2_inputs inputs,
crypto_argon2_extras extras);
// Key exchange (X-25519)
// ----------------------
// Shared secrets are not quite random.
// Hash them to derive an actual shared key.
void crypto_x25519_public_key(uint8_t public_key[32],
const uint8_t secret_key[32]);
void crypto_x25519(uint8_t raw_shared_secret[32],
const uint8_t your_secret_key [32],
const uint8_t their_public_key [32]);
// Conversion to EdDSA
void crypto_x25519_to_eddsa(uint8_t eddsa[32], const uint8_t x25519[32]);
// scalar "division"
// Used for OPRF. Be aware that exponential blinding is less secure
// than Diffie-Hellman key exchange.
void crypto_x25519_inverse(uint8_t blind_salt [32],
const uint8_t private_key[32],
const uint8_t curve_point[32]);
// "Dirty" versions of x25519_public_key().
// Use with crypto_elligator_rev().
// Leaks 3 bits of the private key.
void crypto_x25519_dirty_small(uint8_t pk[32], const uint8_t sk[32]);
void crypto_x25519_dirty_fast (uint8_t pk[32], const uint8_t sk[32]);
// Signatures
// ----------
// EdDSA with curve25519 + BLAKE2b
void crypto_eddsa_key_pair(uint8_t secret_key[64],
uint8_t public_key[32],
uint8_t seed[32]);
void crypto_eddsa_sign(uint8_t signature [64],
const uint8_t secret_key[64],
const uint8_t *message, size_t message_size);
int crypto_eddsa_check(const uint8_t signature [64],
const uint8_t public_key[32],
const uint8_t *message, size_t message_size);
// Conversion to X25519
void crypto_eddsa_to_x25519(uint8_t x25519[32], const uint8_t eddsa[32]);
// EdDSA building blocks
void crypto_eddsa_trim_scalar(uint8_t out[32], const uint8_t in[32]);
void crypto_eddsa_reduce(uint8_t reduced[32], const uint8_t expanded[64]);
void crypto_eddsa_mul_add(uint8_t r[32],
const uint8_t a[32],
const uint8_t b[32],
const uint8_t c[32]);
void crypto_eddsa_scalarbase(uint8_t point[32], const uint8_t scalar[32]);
int crypto_eddsa_check_equation(const uint8_t signature[64],
const uint8_t public_key[32],
const uint8_t h_ram[32]);
// Chacha20
// --------
// Specialised hash.
// Used to hash X25519 shared secrets.
void crypto_chacha20_h(uint8_t out[32],
const uint8_t key[32],
const uint8_t in [16]);
// Unauthenticated stream cipher.
// Don't forget to add authentication.
uint64_t crypto_chacha20_djb(uint8_t *cipher_text,
const uint8_t *plain_text,
size_t text_size,
const uint8_t key[32],
const uint8_t nonce[8],
uint64_t ctr);
uint32_t crypto_chacha20_ietf(uint8_t *cipher_text,
const uint8_t *plain_text,
size_t text_size,
const uint8_t key[32],
const uint8_t nonce[12],
uint32_t ctr);
uint64_t crypto_chacha20_x(uint8_t *cipher_text,
const uint8_t *plain_text,
size_t text_size,
const uint8_t key[32],
const uint8_t nonce[24],
uint64_t ctr);
// Poly 1305
// ---------
// This is a *one time* authenticator.
// Disclosing the mac reveals the key.
// See crypto_lock() on how to use it properly.
// Direct interface
void crypto_poly1305(uint8_t mac[16],
const uint8_t *message, size_t message_size,
const uint8_t key[32]);
// Incremental interface
typedef struct {
// Do not rely on the size or contents of this type,
// for they may change without notice.
uint8_t c[16]; // chunk of the message
size_t c_idx; // How many bytes are there in the chunk.
uint32_t r [4]; // constant multiplier (from the secret key)
uint32_t pad[4]; // random number added at the end (from the secret key)
uint32_t h [5]; // accumulated hash
} crypto_poly1305_ctx;
void crypto_poly1305_init (crypto_poly1305_ctx *ctx, const uint8_t key[32]);
void crypto_poly1305_update(crypto_poly1305_ctx *ctx,
const uint8_t *message, size_t message_size);
void crypto_poly1305_final (crypto_poly1305_ctx *ctx, uint8_t mac[16]);
// Elligator 2
// -----------
// Elligator mappings proper
void crypto_elligator_map(uint8_t curve [32], const uint8_t hidden[32]);
int crypto_elligator_rev(uint8_t hidden[32], const uint8_t curve [32],
uint8_t tweak);
// Easy to use key pair generation
void crypto_elligator_key_pair(uint8_t hidden[32], uint8_t secret_key[32],
uint8_t seed[32]);
#ifdef __cplusplus
}
#endif
#endif // MONOCYPHER_H
+8
View File
@@ -655,6 +655,14 @@ void game_initialize_for_new_map(
return;
}
/* port: whether a map is loaded, the main menu's too: game_in_progress()
is not, once game_time_end() stops its clock */
boolean game_map_loaded(
void)
{
return game_globals->map_loaded;
}
boolean game_map_loading_in_progress(
real *progress)
{
+1
View File
@@ -184,6 +184,7 @@ long local_time_get(void);
short local_time_get_elapsed(void);
boolean game_predicting(void);
boolean game_in_progress(void);
boolean game_map_loaded(void);
boolean game_time_get_paused(void);
/* whether a client's clock waits for the host's first game update */
boolean game_time_held(void);
+40 -30
View File
@@ -1456,6 +1456,11 @@ static void rasterize_in_game_score_draw_line(
return;
}
/* port: the in-game scoreboard's lists (game_engine_rasterize_scoreboard,
game_engine_rasterize_in_game_score): the players in the game, not those who
quit, ranked and placed among themselves */
static boolean statistic_buffer_in_game_only = FALSE;
long populate_statistic_buffer(
struct statistic_buffer *statistic_buffer,
enum postgame_statistic statistic,
@@ -1474,6 +1479,8 @@ long populate_statistic_buffer(
"c:\\halo\\SOURCE\\game\\game_engine.c",
0x2C8,
player_count < MULTIPLAYER_MAXIMUM_PLAYERS);
if (statistic_buffer_in_game_only && player->quit_out_of_game)
continue;
if (player_count < MULTIPLAYER_MAXIMUM_PLAYERS)
{
statistic_buffer[player_count].player_index = player_iterator.datum_index;
@@ -1886,7 +1893,9 @@ static void game_engine_rasterize_scoreboard(
rows = (long)((bounds.y1 - SCOREBOARD_LAYOUT_TOP_ROWS * line_height) / SCOREBOARD_SCALE / line_height) - 2 -
SCOREBOARD_BOTTOM_ROWS;
rows = MAX(rows, 1);
statistic_buffer_in_game_only = TRUE;
ranked_count = populate_statistic_buffer(ranked, _postgame_statistic_ranking, FALSE);
statistic_buffer_in_game_only = FALSE;
team_columns = has_teams && scoreboard_team_columns() && width >= 2 * SCOREBOARD_COLUMN_WIDTH + SCOREBOARD_COLUMN_GAP;
for (index = 0; index < ranked_count; index++)
{
@@ -2109,11 +2118,13 @@ static void game_engine_rasterize_in_game_score(
return;
}
game_engine_generate_title_string(title_string, player_index);
statistic_buffer_in_game_only = TRUE;
entry_count = select_players_to_display(
_postgame_statistic_ranking,
player_index,
entries,
NUMBEROF(entries));
statistic_buffer_in_game_only = FALSE;
color.alpha = alpha;
color.red = 0.7f;
@@ -2655,12 +2666,11 @@ long game_engine_remap_equipment(
break;
}
if (!TEST_FLAG(game_engine_globals.flags, _game_engine_5_or_more_players_bit))
/* (port: whatever the number of players, as
game_engine_infinite_grenades_internal) */
if (game_engine_infinite_grenades_internal())
{
if (game_engine_infinite_grenades_internal())
{
weapon_list_index = NONE;
}
weapon_list_index = NONE;
}
if (TEST_FLAG(game_engine_globals.flags, _game_engine_9_or_more_players_bit))
@@ -3182,12 +3192,9 @@ boolean match_game_type(
static boolean game_engine_infinite_grenades_internal(
void)
{
boolean infinite_grenades = FALSE;
if (!TEST_FLAG(game_engine_globals.flags, _game_engine_5_or_more_players_bit))
infinite_grenades = TEST_FLAG(global_variant.universal_variant.flags, _game_variant_infinite_grenades_bit);
return infinite_grenades;
/* port: whatever the number of players (the Xbox game's had none with
five or more) */
return TEST_FLAG(global_variant.universal_variant.flags, _game_variant_infinite_grenades_bit);
}
static boolean find_closest_player_callback(
@@ -4400,6 +4407,9 @@ void game_engine_player_killed(
killer (port/linux/game/network_distributed.c) */
network_distributed_player_killed(&killing_player_index, &killing_object_index, dead_player_index,
&friendly_fire);
/* port: a killer who has left the game since is no one's kill */
if (killing_player_index != NONE && !player_try_and_get(killing_player_index))
killing_player_index = NONE;
/* the host's kill of a player who quit, ahead of this client's clock
(game_update_quit_players has not come to its time yet) */
if (network_game_distributed_client() && dead_player->quit_out_of_game_time != NONE &&
@@ -7196,12 +7206,14 @@ long game_engine_remap_weapon(
return weapon_definition_index;
}
/* (port: and the gravity rifle, a cut weapon, as the flamethrower,
which the Xbox's maps have unfinished: neither is a multiplayer weapon) */
if (weapon_list_index == _weapon_list_flamethrower || weapon_list_index == _weapon_list_gravity_rifle)
weapon_list_index = _weapon_list_rocket_launcher;
/* port: a custom loadout (game_variant_options) has no weapon set */
if (game_variant_options_get()->loadout == _loadout_custom)
return weapon_definition_index;
if (weapon_list_index == _weapon_list_flamethrower)
weapon_list_index = _weapon_list_rocket_launcher;
return list_index_to_weapon_definition_index(weapon_list_index);
switch (global_variant.universal_variant.weapon_set)
{
@@ -7299,7 +7311,7 @@ long game_engine_remap_weapon(
break;
/* port: the PC version's sets. Covenant: the Covenant's weapons (the
fuel rod, where the map has one, for the rocket launcher) */
needler for the rocket launcher: the Xbox's maps have no fuel rod) */
case _game_engine_weapons_covenant:
switch (weapon_list_index)
{
@@ -7317,26 +7329,19 @@ long game_engine_remap_weapon(
break;
case _weapon_list_rocket_launcher:
weapon_list_index = list_index_to_weapon_definition_index(_weapon_list_gravity_rifle) != NONE ?
_weapon_list_gravity_rifle : _weapon_list_needler;
weapon_list_index = _weapon_list_needler;
break;
}
break;
/* classic: the Xbox game's weapons (no fuel rod: the flamethrower is
already a rocket launcher) */
/* classic: the Xbox game's weapons (the flamethrower and the gravity
rifle are already rocket launchers) */
case _game_engine_weapons_classic:
if (weapon_list_index == _weapon_list_gravity_rifle)
weapon_list_index = _weapon_list_rocket_launcher;
break;
/* heavy weapons: rocket launchers, and fuel rods where the map has them */
/* heavy weapons: rocket launchers */
case _game_engine_weapons_heavy:
if (weapon_list_index != _weapon_list_gravity_rifle ||
list_index_to_weapon_definition_index(_weapon_list_gravity_rifle) == NONE)
{
weapon_list_index = _weapon_list_rocket_launcher;
}
weapon_list_index = _weapon_list_rocket_launcher;
break;
}
@@ -8290,6 +8295,12 @@ static void game_engine_update_item_spawn(
struct object_placement_data placement_data;
long object_index;
/* port: the gametype's no weapons on the map */
if (definition_index != NONE && game_variant_options_get()->no_map_weapons &&
object_definition_get(definition_index)->object.type == _object_type_weapon)
{
continue;
}
object_placement_data_new(
&placement_data,
definition_index,
@@ -8412,8 +8423,7 @@ static long game_engine_loadout_weapon_definition(
static short const list_indices[] =
{
_weapon_list_assault_rifle, _weapon_list_pistol, _weapon_list_shotgun, _weapon_list_sniper_rifle,
_weapon_list_rocket_launcher, _weapon_list_plasma_pistol, _weapon_list_plasma_rifle, _weapon_list_needler,
_weapon_list_gravity_rifle, _weapon_list_flamethrower
_weapon_list_rocket_launcher, _weapon_list_plasma_pistol, _weapon_list_plasma_rifle, _weapon_list_needler
};
short index;
+3 -3
View File
@@ -192,8 +192,6 @@ enum
_loadout_weapon_plasma_pistol,
_loadout_weapon_plasma_rifle,
_loadout_weapon_needler,
_loadout_weapon_fuel_rod,
_loadout_weapon_flamethrower,
NUMBER_OF_LOADOUT_WEAPONS
};
@@ -233,7 +231,9 @@ struct game_variant_options
byte loadout;
byte primary_weapon;
byte secondary_weapon;
byte pad;
/* no weapons spawn on the map (its grenades and powerups do): for
a loadout of none, melee only */
boolean no_map_weapons;
};
typedef char verify_game_variant_options_size[sizeof(struct game_variant_options) == 0x1C ? 1 : -1];
+37
View File
@@ -1670,3 +1670,40 @@ boolean game_engine_race_read_network_state(
csmemcpy(race_events.best_laps, state.best_laps, sizeof(race_events.best_laps));
return TRUE;
}
boolean game_engine_race_next_flag(
long player_index,
real_point3d const *from,
real_point3d *position)
{
struct scenario *scenario = global_scenario_get();
real best_distance = REAL_MAX;
boolean found = FALSE;
long itr;
if (!scenario || DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) >= MULTIPLAYER_MAXIMUM_PLAYERS)
return FALSE;
for (itr = 0; itr < scenario->netgame_flags.count; itr++)
{
struct scenario_netgame_flag *flag = TAG_BLOCK_GET_ELEMENT(
&scenario->netgame_flags,
itr,
struct scenario_netgame_flag);
real distance;
if (flag->type != _netgame_flag_race_track ||
!TEST_FLAG(race_globals.lap_completed_value, flag->team_index) ||
!can_touch_team(player_index, flag->team_index))
{
continue;
}
distance = distance3d(from, &flag->position);
if (distance < best_distance)
{
best_distance = distance;
*position = flag->position;
found = TRUE;
}
}
return found;
}
+7
View File
@@ -18,6 +18,13 @@ struct race_globals;
void race_flags_make_unique(
void);
/* port: the race flag a player is to touch next, the one of those it may
touch nearest the point (the VR port's bots, port/linux/game/bots.c);
FALSE: none */
boolean game_engine_race_next_flag(
long player_index,
real_point3d const *from,
real_point3d *position);
/* ---------- globals */
+10 -3
View File
@@ -94,6 +94,11 @@ void game_statistics_record_kill(
long dead_player_index = player_index_from_unit_index(dead_unit_index);
long statistic_index;
/* port: a unit keeps its attackers' player indices, and a player who
has left the game since has no player (a NULL from player_get) */
if (killing_player_index != NONE && !player_try_and_get(killing_player_index))
killing_player_index = NONE;
/* only player victims are tallied (NONE otherwise); the later HCEA build
still indexes kills[] and assists[] through this NONE-or-zero value */
if (dead_player_index == NONE)
@@ -161,7 +166,9 @@ void game_statistics_record_kill(
if (best_attacker_index == NONE)
best_attacker_index = killer_attacker_index;
if (best_attacker_index != NONE)
if (best_attacker_index != NONE &&
(dead_unit->unit.attackers[best_attacker_index].player_index == NONE ||
player_try_and_get(dead_unit->unit.attackers[best_attacker_index].player_index)))
{
credited_player_index =
dead_unit->unit.attackers[best_attacker_index].player_index;
@@ -206,9 +213,9 @@ void game_statistics_record_kill(
attacker->player_index != NONE &&
attacker->player_index != credited_player_index)
{
struct player_datum *assisting_player = player_get(attacker->player_index);
struct player_datum *assisting_player = player_try_and_get(attacker->player_index);
if (game_team_is_enemy(dead_team_index, (short)assisting_player->team_index))
if (assisting_player && game_team_is_enemy(dead_team_index, (short)assisting_player->team_index))
assisting_player->statistics.assists[statistic_index]++;
}
}
+27
View File
@@ -356,11 +356,36 @@ void update_server_start(
return;
}
/* port: a player's queue is at its player's index, and the slot of a player
who left the game is reused (network_game_spawn_player): that player's queue
goes first, which datum_new_at_index would find in the way. The same player
added again keeps its queue. TRUE when the slot is the player's already */
static boolean update_queue_make_room(
struct data_array *queues,
long player_index)
{
short absolute_index = (short)player_index;
struct datum_header *header;
if (absolute_index < 0 || absolute_index >= queues->maximum_count)
return FALSE;
header = (struct datum_header *)((byte *)queues->data + queues->size * absolute_index);
if (!header->identifier)
return FALSE;
if (header->identifier == (short)(player_index >> 16))
return TRUE;
datum_delete(queues, (long)header->identifier << 16 | (unsigned short)absolute_index);
return FALSE;
}
void update_server_add_player(
long player_index)
{
long queue_index;
if (update_queue_make_room(update_server_globals.queues, player_index))
return;
queue_index = datum_new_at_index(update_server_globals.queues, player_index);
match_assert(
"c:\\halo\\SOURCE\\game\\player_queues_new.c",
@@ -558,6 +583,8 @@ void update_client_add_player(
{
long queue_index;
if (update_queue_make_room(update_client_globals.queues, player_index))
return;
queue_index = datum_new_at_index(update_client_globals.queues, player_index);
match_assert(
"c:\\halo\\SOURCE\\game\\player_queues_new.c",
+10 -3
View File
@@ -283,6 +283,8 @@ symbols in this file:
boolean network_game_distributed_client(void);
/* port/linux/game/network_distributed.c's */
void network_distributed_player_picked_up(long player_index, short kind, long definition_index, short count);
/* port/linux/game/network_objects.c's */
void network_objects_client_picked_up_weapon(short local_player_index, long unit_index, long definition_index);
/* game_sound.c's */
long unspatialized_impulse_sound_new(long sound_definition_index, real scale);
@@ -1475,7 +1477,11 @@ void network_player_show_pickup(
{
hud_picked_up_weapon(player->local_player_index, definition_index);
if (player->unit_index != NONE)
{
player_control_unzoom(player->unit_index);
network_objects_client_picked_up_weapon(player->local_player_index, player->unit_index,
definition_index);
}
}
break;
case _network_pickup_ammunition:
@@ -2611,8 +2617,9 @@ boolean unit_should_autopick_weapon(
if ((unit_approve_weapon_pickup(unit_index, weapon_index) &&
TEST_FLAG(weapon_definition->weapon.flags, _weapon_doesnt_count_toward_maximum_bit)) ||
weapon_count == 0 ||
(!game_engine_running() &&
unit_approve_weapon_pickup(unit_index, weapon_index) &&
/* port: and in multiplayer (campaign's only), a second weapon into
the empty slot, readied (unit_add_weapon_to_inventory) */
(unit_approve_weapon_pickup(unit_index, weapon_index) &&
weapon_count < 2) ||
game_engine_force_autopickup(unit_index, weapon_index))
{
@@ -2672,7 +2679,7 @@ static boolean player_handle_weapon_swap(
switch (player->action_result)
{
case _player_action_result_swap_for_weapon:
if (unit_drop_current_weapon(player->unit_index, TRUE) &&
if (unit_drop_selected_weapon(player->unit_index) &&
unit_add_weapon_to_inventory(
player->unit_index,
player->action_object_index,
+48 -44
View File
@@ -961,6 +961,11 @@ static void hud_draw_players(
long player_index = local_player_get_player_index(render.local_player_index);
long team_index = player_get(player_index)->team_index;
#ifdef HALO_ANDROID
/* (VR: over the heads, with the names: hud_draw_player_name) */
if (halo_vr_names_apart())
return;
#endif
if (player_index != NONE)
{
/* the native builds' sessions hold more than 16 players */
@@ -996,9 +1001,11 @@ static void hud_draw_players(
/* port: in multiplayer, players' names above their heads
(display.player_names: "all", "allies", "enemies" or "none"). An ally's goes
above the triangle the game draws over teammates; an enemy's only while the
view sees them and they are not camouflaged, so that it never gives away
where they hide. */
above the triangle the game draws over teammates; an enemy's only within the
motion sensor's reach, while the view sees them and they are not
camouflaged, so that it never gives away where they hide. Whose names show
follows the gametype's motion tracker: none if it shows no players, only
allies' if it shows only friends (game_engine_draw_object_in_motion_sensor). */
/* the platform layer's (port/linux/src/port_config.c) */
const char *config_string(const char *name);
@@ -1089,52 +1096,18 @@ static boolean hud_player_name_in_sight(
return in_sight;
}
/* the farthest an enemy's name is shown, in world units (the sniper rifle's
at 2x; its 8x would reach across most maps) */
#define MAXIMUM_ENEMY_NAME_RANGE 70.0f
/* how far away enemies' names are shown: as far as the local player's
weapon turns its reticle red over an enemy (its autoaim distance, times its
zoom; a vehicle's gun when seated at one, none for a weapon that aims only
when zoomed, unzoomed), never less than the motion sensor's reach and never
more than MAXIMUM_ENEMY_NAME_RANGE */
/* how far away enemies' names are shown: the motion sensor's reach */
static real hud_player_name_enemy_range(
void)
{
long player_index = local_player_get_player_index(render.local_player_index);
real range = hud_globals ? hud_globals->defaults.motion_sensor_range : 0.0f;
long unit_index;
if (player_index == NONE || player_get(player_index)->unit_index == NONE)
return range;
unit_index = unit_get_aiming_unit_index(player_get(player_index)->unit_index);
if (unit_index != NONE)
{
struct unit_datum *unit = unit_get(unit_index);
long weapon_index = unit_inventory_get_weapon(unit_index, unit->unit.current_weapon_index);
if (weapon_index != NONE)
{
struct weapon_definition *definition = weapon_definition_get(weapon_get(weapon_index)->definition_index);
short zoom_level = player_control_get_zoom_level(render.local_player_index);
if (zoom_level != NONE || !TEST_FLAG(definition->weapon.flags, _weapon_aim_assists_only_when_zoomed_bit))
{
real weapon_range = definition->weapon.aim_assist_parameters.autoaim_distance *
weapon_get_zoom_magnification(weapon_index, zoom_level);
range = MAX(range, weapon_range);
}
}
}
return MIN(range, MAXIMUM_ENEMY_NAME_RANGE);
return hud_globals ? hud_globals->defaults.motion_sensor_range : 0.0f;
}
static void hud_draw_player_name(
long player_index,
boolean ally,
boolean indicator,
boolean name_shown,
real enemy_range,
boolean vr_apart)
{
@@ -1176,7 +1149,7 @@ static void hud_draw_player_name(
point[0] = head_position.x;
point[1] = head_position.y;
point[2] = head_position.z + (indicator ? 0.15f : 0.0f);
point[2] = head_position.z + (indicator ? 0.06f : 0.0f);
slot = halo_vr_add_name(point);
if (slot < 0)
return;
@@ -1185,7 +1158,31 @@ static void hud_draw_player_name(
y = (short)(slot_bounds[3] - 4);
/* (an ally's whole at any distance it is drawn at) */
view_position.z = -1.0f;
if (indicator)
{
/* (a teammate's triangle at the slot's foot, the name over it:
the game's, drawn on the HUD, would hang before the eyes) */
struct bitmap_data *bitmap = bitmap_group_get_bitmap_from_sequence(
interface_get_tag_index(_interface_bitmap_multiplayer_hud), 0, 0);
if (bitmap && _texture_cache_bitmap_get_hardware_format(bitmap, FALSE, TRUE))
{
point2d tip;
real_rectangle2d whole;
tip.x = x;
tip.y = (short)(slot_bounds[3] - 2);
whole.x0 = 0.0f;
whole.x1 = 1.0f;
whole.y0 = 0.0f;
whole.y1 = 1.0f;
interface_draw_bitmap(bitmap, &tip, &whole, 0.6f, 0.0f, 1.0f);
y = (short)(tip.y - bitmap->registration_point.y * 0.6f);
}
}
indicator = FALSE;
if (!name_shown)
return;
}
else
#endif
@@ -1271,22 +1268,29 @@ static void hud_draw_player_names_now(
long team_index;
real enemy_range;
if (setting == _player_names_none || player_index == NONE)
/* (VR: the triangles over teammates go with the names, shown or not) */
if ((setting == _player_names_none && !vr_apart) || player_index == NONE)
return;
team_index = player_get(player_index)->team_index;
indicators = game_engine_display_team_indicators();
enemy_range = hud_player_name_enemy_range();
/* (the players the motion tracker would show this local player) */
game_engine_motion_sensor_viewer(render.local_player_index);
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
/* (allies as the game's triangles tell them: the same team) */
boolean ally = player->team_index == team_index;
boolean shown = !(setting == _player_names_none || (ally && setting == _player_names_enemies) ||
(!ally && setting == _player_names_allies));
if (iterator.datum_index == player_index || player->unit_index == NONE)
continue;
if ((ally && setting == _player_names_enemies) || (!ally && setting == _player_names_allies))
if (!shown && !(vr_apart && ally && indicators))
continue;
hud_draw_player_name(iterator.datum_index, ally, ally && indicators, enemy_range, vr_apart);
if (!game_engine_draw_object_in_motion_sensor(player->unit_index))
continue;
hud_draw_player_name(iterator.datum_index, ally, ally && indicators, shown, enemy_range, vr_apart);
}
return;
+726 -7
View File
@@ -1427,9 +1427,40 @@ static void widget_instance_process_one_event_recursive(
boolean *return_widget_deleted);
static boolean ui_check_for_pause_game(
void);
static long spinner_string_list_extra_count(
long string_list_index);
static wchar_t *spinner_string_list_get_string(
long string_list_index,
short string_index);
/* ---------- globals */
/* port: text boxes' string list indices from here are the descriptions of
spinners' extra items (kills_to_win_extra_descriptions) */
#define SPINNER_EXTRA_DESCRIPTION_BASE 0x5000
/* ... and the pixels a spinner with extra items is wider (for three digits) */
#define SPINNER_EXTRA_WIDTH 12
/* port: the strings a spinner's string list has past the tag's own, as more
items: the higher kills to win of the Slayer game type editor
(ui_widget_event_handler_functions.c saves and loads them) */
static wchar_t const *const kills_to_win_extra_strings[] =
{
L"75", L"100", L"150", L"200", L"250", L"500",
};
/* ... their descriptions, as a text box's string list index of
SPINNER_EXTRA_DESCRIPTION_BASE and up (ui_widget_spinner_extra_description) */
static wchar_t const *const kills_to_win_extra_descriptions[] =
{
L"Seventy-five kills to win. Settle in for a long\r\nfight.",
L"A hundred kills to win. Made for big games.",
L"A hundred and fifty kills to win. Only a crowded\r\nserver gets there.",
L"Two hundred kills to win. Bring friends. Lots of\r\nthem.",
L"Two hundred and fifty kills to win.",
L"Five hundred kills to win. You'll be here a while.",
};
/* port: an error message of the port's own text (display_error_text_deferred):
the text waiting for its dialog, then the dialog's text box showing it */
static wchar_t const *ui_widget_port_error_pending_text = NULL;
@@ -3304,7 +3335,11 @@ static void event_handler_dispatch(
{
close_all = TRUE;
}
/* port: not from a widget its function deleted (it went back:
menu_functions.c's profile_save_changes), which the Xbox's opened
from regardless */
if (TEST_FLAG(handler->flags, _event_handler_open_widget_bit) &&
!widget_deleted &&
handler->widget_tag.index != NONE)
{
if (!ui_widget_launch_widget(widget, handler->widget_tag.index))
@@ -3510,7 +3545,8 @@ static boolean ui_widget_load_children_recursive(
string_list = unicode_string_list_definition_get(definition->text_label_string_list.index);
widget_globals.dont_load_children_recursive = TRUE;
for (string_index = 0;
string_index < string_list->strings.count;
string_index < string_list->strings.count +
spinner_string_list_extra_count(definition->text_label_string_list.index);
string_index++)
{
struct widget_instance *child = ui_widget_load_by_name_or_tag(
@@ -5069,6 +5105,12 @@ static void widget_instance_render_text_box(
string = widget == ui_widget_port_error_text_box && ui_widget_port_error_text ?
(wchar_t *)ui_widget_port_error_text :
unicode_string_list_get_string(definition->text_label_string_list.index, string_list_index);
/* port: the description of a spinner's extra item */
if (string_list_index >= SPINNER_EXTRA_DESCRIPTION_BASE &&
string_list_index < SPINNER_EXTRA_DESCRIPTION_BASE + (short)NUMBEROF(kills_to_win_extra_descriptions))
{
string = (wchar_t *)kills_to_win_extra_descriptions[string_list_index - SPINNER_EXTRA_DESCRIPTION_BASE];
}
length = ustrlen(string);
widget->parameters.text_box.text = pool_resize_pointer(
widget_memory_pool,
@@ -5222,6 +5264,13 @@ static void widget_instance_render_spinner_list(
csmemset(&parameters, 0, sizeof(parameters));
bounds = definition->list_header_bounds;
/* port: a spinner with extra items is wider, to the left, its arrow
with it */
if (spinner_string_list_extra_count(definition->text_label_string_list.index))
{
bounds.x0 -= SPINNER_EXTRA_WIDTH;
bounds.x1 -= SPINNER_EXTRA_WIDTH;
}
bounds.x0 += offset.x;
bounds.y0 += offset.y;
bounds.x1 += offset.x;
@@ -5266,7 +5315,7 @@ static void widget_instance_render_spinner_list(
if (definition->text_label_string_list.index != NONE)
{
short string_index = widget->parameters.list.selected_index;
wchar_t *string = unicode_string_list_get_string(
wchar_t *string = spinner_string_list_get_string(
definition->text_label_string_list.index,
string_index);
unsigned long length = ustrlen(string);
@@ -5341,6 +5390,13 @@ static void widget_instance_render_spinner_list(
bounds.y1 += offset.y;
bounds.x0 += offset.x;
bounds.y0 += offset.y;
/* port: a spinner with extra items (three digits) is wider, to the
left */
if (spinner_string_list_extra_count(definition->text_label_string_list.index))
{
bounds.x0 -= SPINNER_EXTRA_WIDTH;
clip.x0 -= SPINNER_EXTRA_WIDTH;
}
if (focus)
{
color.alpha = definition->text_color.alpha;
@@ -5886,7 +5942,9 @@ the public games and a way to type a code in, either of which then joins as
an invite link does (the game shows up in the list once its host is
reached). */
void platform_log(char const *format, ...);
int p2p_join_code(char *text, int size);
void halo_vr_pulse_pad_a(void);
int p2p_join_with_code(char const *typed);
int p2p_join_code_status(void);
void p2p_join_code_clear_status(void);
@@ -5924,9 +5982,12 @@ struct ui_public_game
char code[12];
int players;
int maximum_players;
/* (a game of the server browser's: no code, its invite) */
char invite[77];
};
void p2p_set_public_game(int public_game);
int p2p_join_invite(char const *text);
int p2p_public_game(void);
int p2p_public_games(struct ui_public_game *games, int maximum);
long config_integer(char const *name);
@@ -6046,8 +6107,15 @@ static void ui_join_code_panel_event(
}
else
{
struct ui_public_game const *game =
&ui_join_code_globals.games[ui_join_code_globals.selection - ONLINE_PANEL_FIXED_ROWS];
p2p_join_code_clear_status();
p2p_join_with_code(ui_join_code_globals.games[ui_join_code_globals.selection - 1].code);
/* (a game of the server browser's: by its invite) */
if (game->code[0])
p2p_join_with_code(game->code);
else
p2p_join_invite(game->invite);
}
break;
}
@@ -6194,6 +6262,13 @@ static boolean ui_join_code_take_pointer(
press.type = _event_type_button;
press.data.button.index = _gamepad_analog_button_a;
press.data.button.value = 1;
/* (the screen where controllers join by their A: a click is the
pad's A, there, not a menu's) */
if (pointer->left_clicks && y < 400 && ui_join_code_screen_active("4way_start2join_screen"))
{
halo_vr_pulse_pad_a();
return TRUE;
}
if (ui_join_code_globals.panel_open)
{
if (x >= 48 && x < 592 && y >= 128 && y < 128 + ONLINE_PANEL_ROWS * 22)
@@ -6225,6 +6300,18 @@ static boolean ui_join_code_take_pointer(
static void ui_join_code_update(
void)
{
{
/* (each screen's name in the log, as it comes up) */
static char const *logged;
char const *name = widget_globals.active_widgets[0] ? widget_globals.active_widgets[0]->name : NULL;
if (name != logged)
{
logged = name;
if (name)
platform_log("menus: screen %s", name);
}
}
char text[sizeof(ui_join_code_globals.typed) / sizeof(wchar_t)];
short index;
@@ -6366,13 +6453,16 @@ static void ui_join_code_render(
if (ui_join_code_screen_active("server_list_screen"))
{
char const *status = NULL;
real_argb_color hint_color = { 1.0f, 0.85f, 0.85f, 0.85f };
if (ui_join_code_globals.panel_open)
{
ui_join_code_render_panel();
return;
}
ui_join_code_draw_line(372, "%x-button Online games: public ones, or join by a code", TRUE, FALSE);
/* (in the key bar's free left, and under it: clear of the map's
picture) */
ui_join_code_draw_text(8, 200, 420, "%x-button Online / code", TRUE, 0, &hint_color);
switch (p2p_join_code_status())
{
case _join_code_looking: status = "Looking for the game of that code..."; break;
@@ -6395,7 +6485,7 @@ static void ui_join_code_render(
}
}
if (status)
ui_join_code_draw_line(392, status, FALSE, FALSE);
ui_join_code_draw_line(452, status, FALSE, FALSE);
}
else if (ui_join_code_screen_active("connected_pregame_screen") && global_network_game_server_get())
{
@@ -6458,6 +6548,561 @@ static void ui_join_code_render(
}
}
/* ---------- VR options (the VR port's)
In VR, Y on the main menu or the pause menu opens a panel of the VR
settings one may want to change while playing (vr.c reads them again as
they change), and from it a page of what the controllers' buttons do. */
int config_boolean(char const *name);
double config_real(char const *name);
int config_write(char const *name, char const *value);
int config_write_boolean(char const *name, int value);
enum
{
_vr_option_turning,
_vr_option_snap_angle,
_vr_option_smooth_speed,
_vr_option_vignette,
_vr_option_wheel,
_vr_option_laser,
_vr_option_crosshair,
_vr_option_motion_tracker,
_vr_option_controls,
_vr_option_quit,
NUMBER_OF_VR_OPTIONS,
VR_OPTIONS_TOP = 98,
VR_OPTIONS_ROW_HEIGHT = 26,
VR_OPTIONS_FOOTER = 362,
_vr_controls_on_foot = 0,
_vr_controls_vehicles,
_vr_controls_menus,
NUMBER_OF_VR_CONTROLS_PAGES,
};
static struct
{
boolean open;
boolean controls;
short selection;
short page;
/* "Quit Halo" chosen once: a second time quits */
boolean quit_armed;
} ui_vr_options_globals;
void platform_request_quit(void);
static real const vr_snap_angles[] = { 30.0f, 45.0f, 60.0f, 90.0f };
static real const vr_smooth_speeds[] = { 60.0f, 90.0f, 120.0f, 150.0f, 180.0f };
static real const vr_vignettes[] = { 0.0f, 0.35f, 0.7f, 1.0f };
static char const *const vr_vignette_names[] = { "Off", "Light", "Medium", "Strong" };
static boolean ui_vr_options_screen(
void)
{
struct widget_instance *screen = widget_globals.active_widgets[0];
return halo_vr_active() && screen && screen->name &&
(main_menu_screen_is_active() || strstr(screen->name, "pause_game") != NULL);
}
/* which of the choices a setting is nearest */
static short ui_vr_options_nearest(
real value,
real const *choices,
short count)
{
short index;
short nearest = 0;
for (index = 1; index < count; index++)
{
if (fabs(choices[index] - value) < fabs(choices[nearest] - value))
nearest = index;
}
return nearest;
}
static void ui_vr_options_step(
char const *name,
real const *choices,
short count,
short change)
{
char text[16];
short index = ui_vr_options_nearest((real)config_real(name), choices, count);
index = (short)((index + change + count) % count);
snprintf(text, sizeof(text), "%g", choices[index]);
config_write(name, text);
}
/* a row's setting changed: forward or back, going round */
static void ui_vr_options_change(
short change)
{
switch (ui_vr_options_globals.selection)
{
case _vr_option_turning: config_write_boolean("vr.smooth_turn", !config_boolean("vr.smooth_turn")); break;
case _vr_option_snap_angle:
ui_vr_options_step("vr.snap_turn_degrees", vr_snap_angles, NUMBEROF(vr_snap_angles), change);
break;
case _vr_option_smooth_speed:
ui_vr_options_step("vr.smooth_turn_speed", vr_smooth_speeds, NUMBEROF(vr_smooth_speeds), change);
break;
case _vr_option_vignette:
ui_vr_options_step("vr.comfort_vignette", vr_vignettes, NUMBEROF(vr_vignettes), change);
break;
case _vr_option_wheel: config_write_boolean("vr.vehicle_wheel", !config_boolean("vr.vehicle_wheel")); break;
case _vr_option_laser: config_write_boolean("vr.laser", !config_boolean("vr.laser")); break;
case _vr_option_crosshair: config_write_boolean("vr.crosshair", !config_boolean("vr.crosshair")); break;
case _vr_option_motion_tracker:
config_write_boolean("vr.motion_tracker_on_wrist", !config_boolean("vr.motion_tracker_on_wrist"));
break;
case _vr_option_controls:
ui_vr_options_globals.controls = TRUE;
ui_vr_options_globals.page = _vr_controls_on_foot;
break;
case _vr_option_quit:
/* (the Steam Frame's Steam cannot end the game, which runs in
Lepton's container: it ends itself; asked twice) */
if (ui_vr_options_globals.quit_armed)
platform_request_quit();
ui_vr_options_globals.quit_armed = TRUE;
break;
}
ui_play_audio_feedback_sound(_ui_audio_feedback_forward);
}
static void ui_vr_options_close(
void)
{
ui_vr_options_globals.quit_armed = FALSE;
ui_vr_options_globals.open = FALSE;
ui_vr_options_globals.controls = FALSE;
}
/* the panel's input: up and down choose a row, left and right (or A)
change it, B or Y closes it (the controls page: left and right turn its
pages, B goes back) */
static boolean ui_vr_options_take_event(
struct widget_instance *widget,
struct event_record const *event)
{
short move = 0;
short change = 0;
boolean accept = FALSE;
boolean back = FALSE;
if (widget != widget_globals.active_widgets[0])
return FALSE;
if (ui_vr_options_globals.open && !ui_vr_options_screen())
ui_vr_options_close();
if (!ui_vr_options_globals.open)
{
if (event->type == _event_type_button && event->data.button.value == 1 &&
event->data.button.index == _gamepad_analog_button_y && ui_vr_options_screen())
{
ui_vr_options_globals.open = TRUE;
ui_vr_options_globals.controls = FALSE;
ui_vr_options_globals.selection = 0;
ui_play_audio_feedback_sound(_ui_audio_feedback_forward);
return TRUE;
}
return FALSE;
}
if (event->type == _event_type_left_stick)
{
if (event->data.stick.y == SHORT_MAX)
move = -1;
else if (event->data.stick.y == SHORT_MIN)
move = 1;
else if (event->data.stick.x == SHORT_MAX)
change = 1;
else if (event->data.stick.x == SHORT_MIN)
change = -1;
}
else if (event->type == _event_type_button && event->data.button.value == 1)
{
switch (event->data.button.index)
{
case _gamepad_binary_button_dpad_up: move = -1; break;
case _gamepad_binary_button_dpad_down: move = 1; break;
case _gamepad_binary_button_dpad_right: change = 1; break;
case _gamepad_binary_button_dpad_left: change = -1; break;
case _gamepad_analog_button_a:
case _gamepad_binary_button_start:
accept = TRUE;
break;
case _gamepad_analog_button_b:
case _gamepad_analog_button_y:
case _gamepad_binary_button_back:
back = TRUE;
break;
}
}
if (ui_vr_options_globals.controls)
{
if (back)
{
ui_vr_options_globals.controls = FALSE;
ui_play_audio_feedback_sound(_ui_audio_feedback_back);
}
else if (change || accept)
{
ui_vr_options_globals.page = (short)((ui_vr_options_globals.page + (change ? change : 1) +
NUMBER_OF_VR_CONTROLS_PAGES) % NUMBER_OF_VR_CONTROLS_PAGES);
ui_play_audio_feedback_sound(_ui_audio_feedback_cursor);
}
return TRUE;
}
if (back)
{
ui_vr_options_close();
ui_play_audio_feedback_sound(_ui_audio_feedback_back);
}
else if (move)
{
ui_vr_options_globals.selection = (short)PIN(ui_vr_options_globals.selection + move, 0, NUMBER_OF_VR_OPTIONS - 1);
ui_vr_options_globals.quit_armed = FALSE;
ui_play_audio_feedback_sound(_ui_audio_feedback_cursor);
}
else if (accept)
{
ui_vr_options_change(1);
}
else if (change && ui_vr_options_globals.selection != _vr_option_controls)
{
ui_vr_options_change(change);
}
return TRUE;
}
/* the pointer over the panel: the row under it chosen, a click on it as A;
on the controls page the tabs, and the footer's Back (TRUE: the panel is
open and took it) */
static boolean ui_vr_options_take_pointer(
struct halo_ui_pointer const *pointer)
{
short x = pointer->left_clicks ? pointer->click_x : pointer->x;
short y = pointer->left_clicks ? pointer->click_y : pointer->y;
if (!ui_vr_options_globals.open)
{
/* (its hint clicked: open) */
if (pointer->left_clicks && x >= 256 && x < 400 && y >= 434 && y < 464 && ui_vr_options_screen())
{
ui_vr_options_globals.open = TRUE;
ui_vr_options_globals.controls = FALSE;
ui_vr_options_globals.selection = 0;
ui_play_audio_feedback_sound(_ui_audio_feedback_forward);
return TRUE;
}
return FALSE;
}
if (!ui_vr_options_screen())
{
ui_vr_options_close();
return FALSE;
}
if (ui_vr_options_globals.controls)
{
if (!pointer->left_clicks)
return TRUE;
if (y >= 50 && y < 74 && x >= 40 && x < 600)
{
ui_vr_options_globals.page = (short)PIN((x - 40) / 186, 0, NUMBER_OF_VR_CONTROLS_PAGES - 1);
ui_play_audio_feedback_sound(_ui_audio_feedback_cursor);
}
else if (y >= 410 && y < 434)
{
ui_vr_options_globals.controls = FALSE;
ui_play_audio_feedback_sound(_ui_audio_feedback_back);
}
return TRUE;
}
if (x >= 118 && x < 522 && y >= VR_OPTIONS_TOP - 4 &&
y < VR_OPTIONS_TOP - 4 + NUMBER_OF_VR_OPTIONS * VR_OPTIONS_ROW_HEIGHT)
{
short row = (short)((y - (VR_OPTIONS_TOP - 4)) / VR_OPTIONS_ROW_HEIGHT);
if (row != ui_vr_options_globals.selection)
ui_vr_options_globals.quit_armed = FALSE;
ui_vr_options_globals.selection = row;
if (pointer->left_clicks)
ui_vr_options_change(1);
}
else if (pointer->left_clicks && x >= 118 && x < 522 && y >= VR_OPTIONS_FOOTER + 4 && y < VR_OPTIONS_FOOTER + 30)
{
ui_vr_options_close();
ui_play_audio_feedback_sound(_ui_audio_feedback_back);
}
return TRUE;
}
/* a line of the controls page: what is pressed, dim, and what it does */
static void ui_vr_controls_line(
short x,
short y,
char const *control,
char const *action)
{
real_argb_color control_color = { 1.0f, 0.55f, 0.75f, 1.0f };
real_argb_color action_color = { 1.0f, 0.88f, 0.88f, 0.88f };
ui_join_code_draw_text(x, (short)(x + 92), y, control, FALSE, 0, &control_color);
ui_join_code_draw_text((short)(x + 92), (short)(x + 280), y, action, FALSE, 0, &action_color);
}
static void ui_vr_controls_note(
short y,
char const *text)
{
real_argb_color color = { 1.0f, 0.88f, 0.88f, 0.88f };
ui_join_code_draw_text(44, 596, y, text, FALSE, 0, &color);
}
static void ui_vr_controls_render(
void)
{
static char const *const tabs[] = { "ON FOOT", "VEHICLES", "MENUS" };
real_argb_color title_color = { 1.0f, 0.55f, 0.75f, 1.0f };
real_argb_color tab_color = { 1.0f, 0.6f, 0.6f, 0.6f };
real_argb_color selected_color = { 1.0f, 1.0f, 1.0f, 1.0f };
short tab;
short left = 44;
short right = 328;
short y = 112;
ui_join_code_draw_quad(24, 40, 616, 440, 0xFF050A18);
ui_join_code_draw_quad(24, 40, 616, 42, 0xFF3A6EA5);
ui_join_code_draw_quad(24, 438, 616, 440, 0xFF3A6EA5);
for (tab = 0; tab < NUMBER_OF_VR_CONTROLS_PAGES; tab++)
{
short x0 = (short)(40 + tab * 186);
if (tab == ui_vr_options_globals.page)
ui_join_code_draw_quad(x0, 50, (short)(x0 + 180), 74, 0x803A6EA5);
ui_join_code_draw_text(x0, (short)(x0 + 180), 52, tabs[tab], FALSE, 2,
tab == ui_vr_options_globals.page ? &selected_color : &tab_color);
}
ui_join_code_draw_text(left, (short)(left + 270), 86, "LEFT CONTROLLER", FALSE, 0, &title_color);
ui_join_code_draw_text(right, (short)(right + 270), 86, "RIGHT CONTROLLER", FALSE, 0, &title_color);
ui_join_code_draw_quad(320, 86, 321, 300, 0xFF3A6EA5);
if (halo_vr_controller_layout() == 1)
{
/* the Steam Frame's controllers: A, B, X, Y and a menu button on the
right one, a D-pad and a View button on the left one */
switch (ui_vr_options_globals.page)
{
case _vr_controls_on_foot:
ui_vr_controls_line(left, y, "Stick", "Move; click: crouch");
ui_vr_controls_line(left, y + 22, "Trigger", "Grenade: hold it,");
ui_vr_controls_line(left, y + 40, "", "let go as you throw");
ui_vr_controls_line(left, y + 62, "Grip", "Weapon in both hands");
ui_vr_controls_line(left, y + 84, "Bumper", "Switch grenades");
ui_vr_controls_line(left, y + 106, "View", "Scores");
ui_vr_controls_line(left, y + 128, "D-pad", "In the menus");
ui_vr_controls_line(right, y, "Stick", "Turn; click: zoom");
ui_vr_controls_line(right, y + 22, "Trigger", "Fire");
ui_vr_controls_line(right, y + 44, "Grip", "Melee (or punch)");
ui_vr_controls_line(right, y + 66, "A", "Jump");
ui_vr_controls_line(right, y + 88, "B", "Light; held: grenades");
ui_vr_controls_line(right, y + 110, "X", "Action, reload");
ui_vr_controls_line(right, y + 132, "Y", "Switch weapon");
ui_vr_controls_line(right, y + 154, "Menu", "Pause");
ui_vr_controls_note(320, "Motion tracker: turn the back of your left wrist towards you.");
ui_vr_controls_note(340, "Scope: zoom, and look through the disc above the weapon.");
ui_vr_controls_note(360, "Right bumper: the flashlight too.");
break;
case _vr_controls_vehicles:
ui_vr_controls_line(left, y, "Stick", "Drive");
ui_vr_controls_line(left, y + 22, "Trigger", "Second weapon");
ui_vr_controls_line(left, y + 40, "", "(Banshee, tank)");
ui_vr_controls_line(right, y, "Stick", "Steer; up and down:");
ui_vr_controls_line(right, y + 18, "", "climb, dive");
ui_vr_controls_line(right, y + 40, "Trigger", "Fire");
ui_vr_controls_line(right, y + 62, "X", "Get in, get out");
ui_vr_controls_line(right, y + 84, "B", "Flashlight");
ui_vr_controls_note(300, "Warthog: grip the wheel with one hand or both and turn it.");
ui_vr_controls_note(320, "Holding it, the right trigger accelerates, the left one brakes");
ui_vr_controls_note(338, "and reverses. Let go, and the right stick steers.");
ui_vr_controls_note(360, "At a gun or as a passenger: aim with the right controller.");
break;
default:
ui_vr_controls_line(left, y, "Stick", "Choose");
ui_vr_controls_line(left, y + 22, "D-pad", "Choose");
ui_vr_controls_line(left, y + 44, "Trigger", "Click where it points");
ui_vr_controls_line(right, y, "Stick", "Choose, scroll");
ui_vr_controls_line(right, y + 22, "A", "Accept");
ui_vr_controls_line(right, y + 44, "B", "Back");
ui_vr_controls_line(right, y + 66, "X, Y", "As the screen shows");
ui_vr_controls_line(right, y + 88, "Trigger", "Click where it points");
ui_vr_controls_note(300, "Point at the menus with either controller; its trigger clicks.");
ui_vr_controls_note(320, "On the keyboard, BACK and ENTER under it can be clicked too.");
ui_vr_controls_note(340, "Y on the main menu or the pause menu: these VR options.");
break;
}
ui_join_code_draw_text(44, 596, 412, "%b-button Back Left, right: the other pages", TRUE, 0, &tab_color);
return;
}
switch (ui_vr_options_globals.page)
{
case _vr_controls_on_foot:
ui_vr_controls_line(left, y, "Stick", "Move");
ui_vr_controls_line(left, y + 22, "Stick click", "Crouch");
ui_vr_controls_line(left, y + 44, "Trigger", "Grenade: hold it,");
ui_vr_controls_line(left, y + 62, "", "let go as you throw");
ui_vr_controls_line(left, y + 84, "Grip", "Weapon in both hands");
ui_vr_controls_line(left, y + 106, "X", "Action, reload");
ui_vr_controls_line(left, y + 128, "Y", "Switch weapon");
ui_vr_controls_line(left, y + 150, "Menu", "Pause");
ui_vr_controls_line(right, y, "Stick", "Turn");
ui_vr_controls_line(right, y + 22, "Stick click", "Zoom");
ui_vr_controls_line(right, y + 44, "Trigger", "Fire");
ui_vr_controls_line(right, y + 66, "Grip", "Melee (or punch");
ui_vr_controls_line(right, y + 84, "", "forward)");
ui_vr_controls_line(right, y + 106, "A", "Jump");
ui_vr_controls_line(right, y + 128, "B", "Flashlight");
ui_vr_controls_line(right, y + 150, "B held", "Switch grenades");
ui_vr_controls_note(320, "Motion tracker: turn the back of your left wrist towards you.");
ui_vr_controls_note(340, "Scope: zoom, and look through the disc above the weapon.");
ui_vr_controls_note(360, "Hold the Meta button to recentre the view.");
break;
case _vr_controls_vehicles:
ui_vr_controls_line(left, y, "Stick", "Drive");
ui_vr_controls_line(left, y + 22, "Trigger", "Second weapon");
ui_vr_controls_line(left, y + 40, "", "(Banshee, tank)");
ui_vr_controls_line(left, y + 62, "X", "Get in, get out");
ui_vr_controls_line(right, y, "Stick", "Steer; up and down:");
ui_vr_controls_line(right, y + 18, "", "climb, dive");
ui_vr_controls_line(right, y + 40, "Trigger", "Fire");
ui_vr_controls_line(right, y + 62, "B", "Flashlight");
ui_vr_controls_note(300, "Warthog: grip the wheel with one hand or both and turn it.");
ui_vr_controls_note(320, "Holding it, the right trigger accelerates, the left one brakes");
ui_vr_controls_note(338, "and reverses. Let go, and the right stick steers.");
ui_vr_controls_note(360, "At a gun or as a passenger: aim with the right controller.");
break;
default:
ui_vr_controls_line(left, y, "Stick", "Choose");
ui_vr_controls_line(left, y + 22, "X, Y", "As the screen shows");
ui_vr_controls_line(left, y + 44, "Trigger", "Click where it points");
ui_vr_controls_line(right, y, "Stick", "Choose, scroll");
ui_vr_controls_line(right, y + 22, "A", "Accept");
ui_vr_controls_line(right, y + 44, "B", "Back");
ui_vr_controls_line(right, y + 66, "Trigger", "Click where it points");
ui_vr_controls_note(300, "Point at the menus with either controller; its trigger clicks.");
ui_vr_controls_note(320, "On the keyboard, BACK and ENTER under it can be clicked too.");
ui_vr_controls_note(340, "Y on the main menu or the pause menu: these VR options.");
break;
}
ui_join_code_draw_text(44, 596, 412, "%b-button Back Left, right: the other pages", TRUE, 0, &tab_color);
}
static void ui_vr_options_render(
void)
{
real_argb_color title_color = { 1.0f, 0.55f, 0.75f, 1.0f };
real_argb_color text_color = { 1.0f, 0.88f, 0.88f, 0.88f };
real_argb_color dim_color = { 1.0f, 0.5f, 0.5f, 0.5f };
real_argb_color hint_color = { 1.0f, 0.85f, 0.85f, 0.85f };
char value[48];
short row;
boolean smooth = config_boolean("vr.smooth_turn") != 0;
if (!ui_vr_options_screen())
{
ui_vr_options_close();
return;
}
if (!ui_vr_options_globals.open)
{
/* (under the menu; centred by hand, as icons are drawn from the
left) */
ui_join_code_draw_text(266, 400, 440, "%y-button VR options", TRUE, 0, &hint_color);
return;
}
if (ui_vr_options_globals.controls)
{
ui_vr_controls_render();
return;
}
ui_join_code_draw_quad(110, 62, 530, 396, 0xFF050A18);
ui_join_code_draw_quad(110, 62, 530, 64, 0xFF3A6EA5);
ui_join_code_draw_quad(110, 394, 530, 396, 0xFF3A6EA5);
ui_join_code_draw_text(126, 514, 72, "VR OPTIONS", FALSE, 0, &title_color);
for (row = 0; row < NUMBER_OF_VR_OPTIONS; row++)
{
short y = (short)(VR_OPTIONS_TOP + row * VR_OPTIONS_ROW_HEIGHT);
char const *label = "";
boolean unused = FALSE;
value[0] = 0;
switch (row)
{
case _vr_option_turning:
label = "Turning";
snprintf(value, sizeof(value), "%s", smooth ? "Smooth" : "Snap");
break;
case _vr_option_snap_angle:
label = "Snap turn";
snprintf(value, sizeof(value), "%.0f degrees", vr_snap_angles[ui_vr_options_nearest(
(real)config_real("vr.snap_turn_degrees"), vr_snap_angles, NUMBEROF(vr_snap_angles))]);
unused = smooth;
break;
case _vr_option_smooth_speed:
label = "Smooth turn speed";
snprintf(value, sizeof(value), "%.0f degrees/s", vr_smooth_speeds[ui_vr_options_nearest(
(real)config_real("vr.smooth_turn_speed"), vr_smooth_speeds, NUMBEROF(vr_smooth_speeds))]);
unused = !smooth;
break;
case _vr_option_vignette:
label = "Vehicle comfort vignette";
snprintf(value, sizeof(value), "%s", vr_vignette_names[ui_vr_options_nearest(
(real)config_real("vr.comfort_vignette"), vr_vignettes, NUMBEROF(vr_vignettes))]);
break;
case _vr_option_wheel:
label = "Warthog steering wheel";
snprintf(value, sizeof(value), "%s", config_boolean("vr.vehicle_wheel") ? "On" : "Off (stick)");
break;
case _vr_option_laser:
label = "Weapon laser";
snprintf(value, sizeof(value), "%s", config_boolean("vr.laser") ? "On" : "Off");
break;
case _vr_option_crosshair:
label = "Crosshair";
snprintf(value, sizeof(value), "%s", config_boolean("vr.crosshair") ? "On" : "Off");
break;
case _vr_option_motion_tracker:
label = "Motion tracker";
snprintf(value, sizeof(value), "%s", config_boolean("vr.motion_tracker_on_wrist") ? "On the wrist" : "In the HUD");
break;
case _vr_option_controls:
label = "Controller buttons...";
break;
case _vr_option_quit:
label = ui_vr_options_globals.quit_armed && ui_vr_options_globals.selection == _vr_option_quit ?
"Quit Halo? Once more to quit" : "Quit Halo";
break;
}
if (row == ui_vr_options_globals.selection)
ui_join_code_draw_quad(118, (short)(y - 4), 522, (short)(y + VR_OPTIONS_ROW_HEIGHT - 6), 0x803A6EA5);
ui_join_code_draw_text(128, 330, y, label, FALSE, 0, unused ? &dim_color : &text_color);
if (value[0])
{
char shown[56];
snprintf(shown, sizeof(shown), "< %s >", value);
ui_join_code_draw_text(330, 512, y, shown, FALSE, 1, unused ? &dim_color : &text_color);
}
}
ui_join_code_draw_text(126, 514, VR_OPTIONS_FOOTER + 8, "%a-button Change %b-button Done", TRUE, 0, &text_color);
}
/* whether a menu is up (VR shows it on a screen in the room: render.c) */
boolean ui_widgets_menus_active(
void)
@@ -6477,6 +7122,23 @@ static boolean ui_mouse_selection_row(
(!strncmp(widget->name, "list_item_", 10) || !strncmp(widget->name, "server_item_", 12));
}
/* port: a press the menus post from their updates (menu_functions.c: the
server browser's join, once its game is reached), posted where the mouse's
are: one posted while the widgets update or draw would be overwritten by the
next frame's events (queue_event keeps the latest) */
static short ui_widget_port_press_controller = NONE;
static short ui_widget_port_press_button;
void ui_widget_port_post_button(
short controller_index,
short button_index)
{
ui_widget_port_press_controller = controller_index;
ui_widget_port_press_button = button_index;
return;
}
static void ui_widgets_process_mouse(
void)
{
@@ -6496,7 +7158,7 @@ static void ui_widgets_process_mouse(
{
#ifdef HALO_ANDROID
/* (the VR port's panels over the menus take the pointer themselves) */
if (ui_join_code_take_pointer(&pointer))
if (ui_vr_options_take_pointer(&pointer) || ui_join_code_take_pointer(&pointer))
return;
#endif
if (pointer.moved)
@@ -6976,6 +7638,7 @@ void render_ui_widgets(
}
#ifdef HALO_ANDROID
ui_join_code_render();
ui_vr_options_render();
#endif
if (widget_globals.fade_to_black >= 0.0f &&
widget_globals.fade_to_black <= 1.0f)
@@ -7003,6 +7666,57 @@ void render_ui_widgets(
/* ---------- private code */
static long spinner_string_list_extra_count(
long string_list_index)
{
if (string_list_index != NONE && !csstrcmp(tag_get_name(string_list_index),
"ui\\shell\\main_menu\\settings_select\\multiplayer_setup\\playlist_edit\\slayer_edit\\var_kills_to_win"))
{
return NUMBEROF(kills_to_win_extra_strings);
}
return 0;
}
/* a spinner's items of its string list's own (the descriptions of a list of
spinners count those: ui_widget_game_data_input_functions.c) */
short ui_widget_spinner_own_item_count(
struct widget_instance *spinner)
{
struct ui_widget_definition *definition = ui_widget_definition_get(spinner->definition_tag_index);
return (short)(spinner->parameters.list.number_of_items -
spinner_string_list_extra_count(definition->text_label_string_list.index));
}
/* the string list index of the description of a spinner's extra item, or
NONE for an item of its own */
short ui_widget_spinner_extra_description(
struct widget_instance *spinner,
short item_index)
{
short own = ui_widget_spinner_own_item_count(spinner);
if (item_index < own)
return NONE;
return (short)(SPINNER_EXTRA_DESCRIPTION_BASE + item_index - own);
}
/* a string of a string list, or of its extra strings past the tag's own */
static wchar_t *spinner_string_list_get_string(
long string_list_index,
short string_index)
{
struct string_list *string_list = unicode_string_list_definition_get(string_list_index);
if (string_list && string_index >= string_list->strings.count &&
string_index - string_list->strings.count < spinner_string_list_extra_count(string_list_index))
{
return (wchar_t *)kills_to_win_extra_strings[string_index - string_list->strings.count];
}
return unicode_string_list_get_string(string_list_index, string_index);
}
static void widget_instance_render_column_list(
struct widget_instance *widget,
struct ui_widget_definition *definition,
@@ -7889,6 +8603,11 @@ void process_ui_widgets(
widget_globals.initialized);
widget_globals.current_system_milliseconds = system_milliseconds();
ui_widgets_process_mouse();
if (ui_widget_port_press_controller != NONE)
{
event_manager_post_button(ui_widget_port_press_controller, ui_widget_port_press_button);
ui_widget_port_press_controller = NONE;
}
if (widget_globals.initialization_thread)
{
if (!thread_has_exited(widget_globals.initialization_thread))
@@ -8030,7 +8749,7 @@ void process_ui_widgets(
do
{
#ifdef HALO_ANDROID
if (ui_join_code_take_event(widget, &event))
if (ui_vr_options_take_event(widget, &event) || ui_join_code_take_event(widget, &event))
continue;
#endif
if (!pause_pressed)
+7
View File
@@ -158,6 +158,13 @@ real_argb_color get_ui_argb_white(
pixel32 modulate_pixel32_by_real_alpha(
pixel32 argb,
real alpha);
/* port: a spinner's items of its string list's own, and the description of
an extra item (ui_widget.c) */
short ui_widget_spinner_own_item_count(
struct widget_instance *spinner);
short ui_widget_spinner_extra_description(
struct widget_instance *spinner,
short item_index);
void ui_widget_delete(
struct widget_instance *widget);
void ui_widgets_close_all(
@@ -2029,6 +2029,10 @@ static boolean multiplayer_type_menu_initialize(
boolean *widget_deleted)
{
player_spawn_count = 1;
/* port: and no co-op player's controller, which one player with one
gamepad gives back to the keyboard's (xinput_sdl.c's port_gamepad): its
going is not a controller unplugged (input_abstraction.c) */
player_ui_reset_single_player_local_player_controllers();
return TRUE;
}
@@ -2315,6 +2319,8 @@ static boolean main_menu_initialize(
dispose_global_network_game_server();
network_game_accept_remote_connections(FALSE);
player_spawn_count = 1;
/* port: as multiplayer_type_menu_initialize */
player_ui_reset_single_player_local_player_controllers();
player_ui_end_editing_profile();
if (!ui_main_menu_music_active())
ui_start_main_menu_music();
@@ -3944,6 +3950,13 @@ static boolean playlist_profile_change_slayer_rules(
case 2: *(long *)(profile + 0x40) = 15; break;
case 3: *(long *)(profile + 0x40) = 25; break;
case 4: *(long *)(profile + 0x40) = 50; break;
/* port: higher, for big games (ui_widget.c's kills_to_win_extra_strings) */
case 5: *(long *)(profile + 0x40) = 75; break;
case 6: *(long *)(profile + 0x40) = 100; break;
case 7: *(long *)(profile + 0x40) = 150; break;
case 8: *(long *)(profile + 0x40) = 200; break;
case 9: *(long *)(profile + 0x40) = 250; break;
case 10: *(long *)(profile + 0x40) = 500; break;
default: error(2, "unknown option selected in 'kills to win' option spinner list"); break;
}
list_item = list_item->next;
@@ -4861,6 +4874,13 @@ static boolean playlist_profile_initialize_slayer_rules(
case 15: option_spinner->data3C.selected_index = 2; break;
case 25: option_spinner->data3C.selected_index = 3; break;
case 50: option_spinner->data3C.selected_index = 4; break;
/* port: higher, for big games (ui_widget.c's kills_to_win_extra_strings) */
case 75: option_spinner->data3C.selected_index = 5; break;
case 100: option_spinner->data3C.selected_index = 6; break;
case 150: option_spinner->data3C.selected_index = 7; break;
case 200: option_spinner->data3C.selected_index = 8; break;
case 250: option_spinner->data3C.selected_index = 9; break;
case 500: option_spinner->data3C.selected_index = 10; break;
default: option_spinner->data3C.selected_index = 0; break;
}
@@ -2303,11 +2303,23 @@ static void game_options_menu_update_text_desc(
if (column == widget->focused_child)
{
/* port: an extra item's own description (ui_widget.c) */
short extra_description = ui_widget_spinner_extra_description(spinner_list,
spinner_list->parameters.list.selected_list_item_index);
if (extra_description != NONE)
{
widget->parameters.list.extended_description->parameters.text_box.string_list_index =
extra_description;
return;
}
description_index += spinner_list->parameters.list.selected_list_item_index;
break;
}
description_index += spinner_list->parameters.list.number_of_items;
/* port: only the items of a spinner's string list's own have
descriptions in its tag */
description_index += ui_widget_spinner_own_item_count(spinner_list);
column = column->next;
}
}
@@ -2370,11 +2382,13 @@ static void game_options_menu_update_pic_desc(
if (column == widget->focused_child)
{
description_index += spinner_list->parameters.list.selected_list_item_index;
/* port: an extra item shows its spinner's last picture of its own */
description_index += MIN(spinner_list->parameters.list.selected_list_item_index,
ui_widget_spinner_own_item_count(spinner_list) - 1);
break;
}
description_index += spinner_list->parameters.list.number_of_items;
description_index += ui_widget_spinner_own_item_count(spinner_list);
column = column->next;
}
}
+26
View File
@@ -231,6 +231,7 @@ symbols in this file:
#include "units/units.h"
#ifdef HALO_ANDROID
#include "halo_vr.h"
#include "physics/collisions.h"
#endif
/* port/linux/game/pal_tags.c's */
@@ -2205,6 +2206,10 @@ static void trigger_create_projectiles(
long target_object_index= NONE;
long projectile_definition_index;
short projectile_count;
#ifdef HALO_ANDROID
/* VR: fired from the muzzle of the weapon in the hand */
boolean vr_from_muzzle= FALSE;
#endif
if (!TEST_FLAG(trigger_definition->flags, _weapon_trigger_projectiles_cannot_be_aimed_bit) &&
unit &&
@@ -2282,6 +2287,7 @@ static void trigger_create_projectiles(
origin.x = vr_origin[0];
origin.y = vr_origin[1];
origin.z = vr_origin[2];
vr_from_muzzle = TRUE;
}
}
#endif
@@ -2383,6 +2389,26 @@ static void trigger_create_projectiles(
real_point3d camera_position;
unit_get_camera_position(owner_object_index, &camera_position);
#ifdef HALO_ANDROID
if (vr_from_muzzle)
{
/* VR: from the eyes down to the hand the line runs through
the player's own body, which the game's test hits, moving the
shot up towards the eyes (it then flies parallel to the aim,
above it): only the level stops it, a hand through a wall */
struct collision_result collision;
struct object_datum *projectile_object= object_get(projectile_object_index);
if (collision_test_line(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
&camera_position, &projectile_object->object.position, owner_object_index, &collision) &&
collision.location.cluster_index!=NONE)
{
object_translate(projectile_object_index, &collision.point, &collision.location);
object_compute_node_matrices(projectile_object_index);
}
}
else
#endif
object_force_inside_bsp(projectile_object_index, &camera_position);
}
if (target_object_index!=NONE)
+64 -5
View File
@@ -88,6 +88,9 @@ symbols in this file:
#include "network_game_globals.h"
#include "network_game_manager.h"
#include "network_game_ui.h"
#include "networking/network_server_manager.h"
#include "objects/objects.h"
#include "units/units.h"
#include "text/unicode.h"
#include <xtl.h>
@@ -107,13 +110,34 @@ players in. So there each player's datum is its slot in the host's player
list: every machine makes it there (network_game_spawn_player), and the host
gives a player added to the game in progress a slot whose datum is free.
(Only in a game: in the lobby the host runs the user interface's scenario,
whose local player holds datum 0, and a full lobby could not use slot 0.) */
whose local player holds datum 0, and a full lobby could not use slot 0.)
A player who quit the game holds his slot no longer once his unit is gone:
his datum gives way to the next player there (network_game_spawn_player),
so that those who quit do not keep the game from filling. Nor in the lobby
between games, where the last game's datums stay until the next is made. */
/* a player who quit the game and has no unit left: his slot can be another's */
static boolean network_game_player_slot_reusable(
struct player_datum const *player)
{
return player->quit_out_of_game && player->unit_index == NONE;
}
static boolean network_game_player_slot_held(
long slot)
{
return game_in_progress() && game_engine_running() &&
player_data && player_data->valid && slot < player_data->maximum_count &&
((struct datum_header *)((byte *)player_data->data + player_data->size * slot))->identifier != 0;
struct network_game_server *server = global_network_game_server_get();
struct player_datum *player;
if (!game_in_progress() || !game_engine_running() ||
!player_data || !player_data->valid || slot >= player_data->maximum_count ||
(server && !network_game_server_playing(server)))
{
return FALSE;
}
player = (struct player_datum *)((byte *)player_data->data + player_data->size * slot);
if (((struct datum_header *)player)->identifier == 0)
return FALSE;
return !network_game_player_slot_reusable(player);
}
enum
@@ -458,6 +482,36 @@ boolean network_game_spawn_player(
give it) */
if (VALID_INDEX(player->player_list_index, NETWORK_GAME_PLAYER_SLOTS))
{
/* port: a player who quit there gives way (network_game_player_slot_held),
and his units forget him */
if (player_data && player_data->valid && player->player_list_index < player_data->maximum_count)
{
struct player_datum *quitter = (struct player_datum *)((byte *)player_data->data +
player_data->size * player->player_list_index);
/* (the host's choice stands: a client whose clock has not yet
come to the quit of the player there gives way too, to another
machine's player) */
if (((struct datum_header *)quitter)->identifier != 0 &&
(network_game_player_slot_reusable(quitter) ||
quitter->network_player_data.machine_index != player->machine_index ||
quitter->network_player_data.controller_index != player->controller_index))
{
long quitter_index = ((long)(word)((struct datum_header *)quitter)->identifier << 16) |
player->player_list_index;
struct object_iterator iterator;
object_iterator_new(&iterator, _object_mask_unit, 0);
while (object_iterator_next(&iterator))
{
struct unit_datum *unit = unit_get(iterator.index);
if (unit->unit.player_index == quitter_index)
unit->unit.player_index = NONE;
}
player_delete(quitter_index);
}
}
player_index = player_new(player->machine_index,
((long)(word)player_data->next_identifier << 16) | player->player_list_index, controller_index, player);
}
@@ -700,7 +754,12 @@ boolean network_game_create_game_objects(
game_precache_new_map(options.map_name, TRUE);
main_menu_unload();
if (game_in_progress())
/* port: and whenever a map is loaded. After a game the client is back in
the main menu's map with its clock ended (network_game_dispose_game_objects),
so not in progress: the next game's map was loaded over it, and the
texture cache, opened again, lost the menu's loaded textures (a crash in
lruv_block_new) */
if (game_in_progress() || game_map_loaded())
{
game_dispose_from_old_map();
game_unload();
+71 -14
View File
@@ -473,13 +473,19 @@ symbols in this file:
#include "cache/cache_files.h"
#include "interface/player_ui.h"
#include "tag_files/tag_files.h"
/* port: internet play's Discord presence (port/linux/src/p2p.c) */
/* port: internet play's Discord presence (port/linux/src/p2p.c), and the
server browser's listing of a public game (p2p_lobby.c) */
void p2p_set_game_player_counts(int count, int maximum);
#ifdef HALO_ANDROID
void p2p_set_game_description(char const *name, char const *map, char const *mode, int players,
int maximum_players);
void p2p_set_hosting_public(int public_game);
int p2p_public_game(void);
#endif
void p2p_set_game_listing(const char *name, const char *map, const char *gametype, int engine_type, int open,
int in_progress, int has_teams);
/* ---------- constants */
@@ -697,9 +703,10 @@ typedef char network_game_server_countdown_state_offset_assert[
offsetof(struct network_game_server, countdown_state) ==
8 + HALO_PORT_NETWORK_GAME_SIZE + MAXIMUM_NETWORK_MACHINE_COUNT * 0x10 + 0xC ? 1 : -1];
#ifdef HALO_ANDROID
/* bots' machines (below: the VR port's bots) */
/* bots' machines (the VR port's bots, port/linux/game/bots.c; on the
desktop none is ever marked, the writes live behind the Android guard) */
static boolean network_game_server_bot_machines[MAXIMUM_NETWORK_MACHINE_COUNT];
#ifdef HALO_ANDROID
static short network_game_server_bot_machine_count(struct network_game_server *server);
#endif
@@ -1321,6 +1328,40 @@ static boolean network_game_server_network_lost(
return now - down_time > NETWORK_GAME_SERVER_CLIENT_TIMEOUT;
}
/* port: wide text as the listing has it: ASCII, a Latin letter with a mark
its plain letter ('?' for the rest: player_name_character_ascii, as the
server browser validates the name as a player's) */
static void listing_text(char *text, int size, wchar_t const *wide, int length)
{
int index;
for (index = 0; index < size - 1 && index < length && wide[index]; index++)
text[index] = player_name_character_ascii(wide[index]);
text[index] = 0;
}
/* port: the server browser's listing of the game (shown only if hosted
for the internet and public: p2p_lobby.c), as its advertisement has it
(network_server_message_handler.c) */
static void network_game_server_list(
struct network_game_server *server)
{
struct network_game *game = &server->game;
short state = network_game_server_get_state(server, NULL);
char name[NUMBEROF(game->name) + 1];
char gametype[NUMBEROF(game->variant.human_readable_game_description) + 1];
boolean in_progress = state != _network_game_server_state_pregame || network_game_server_game_is_loading(server);
boolean open = state == _network_game_server_state_ingame ? network_game_server_accepts_late_joins(server) :
network_game_server_game_is_open(server) && network_game_has_free_player_slot(game);
listing_text(name, sizeof(name), game->name, NUMBEROF(game->name));
listing_text(gametype, sizeof(gametype), game->variant.human_readable_game_description,
NUMBEROF(game->variant.human_readable_game_description));
/* (the scenario's name, not its path: the listing has 32 characters) */
p2p_set_game_listing(name, tag_name_strip_path(game->map.name), gametype, game->variant.game_engine_index, open,
in_progress, game->variant.universal_variant.teams);
}
boolean network_game_server_idle(
struct network_game_server *server)
{
@@ -1337,7 +1378,8 @@ boolean network_game_server_idle(
}
}
/* (what Discord shows of a game hosted for internet play) */
/* (what Discord shows of a game hosted for internet play, and the
server browser's listing) */
p2p_set_game_player_counts(server->game.player_count, server->game.maximum_players);
#ifdef HALO_ANDROID
/* (and what a public game's listing says: port/linux/src/p2p_list.c) */
@@ -1373,8 +1415,12 @@ boolean network_game_server_idle(
mode[sizeof(mode) - 1] = 0;
}
p2p_set_game_description(name, map, mode, server->game.player_count, server->game.maximum_players);
/* (listed in the server browser too, as public as the VR port's list
has it) */
p2p_set_hosting_public(p2p_public_game());
}
#endif
network_game_server_list(server);
if (network_game_server_game_is_valid(server))
{
@@ -1849,6 +1895,14 @@ boolean network_game_server_game_is_open(
return game_is_open;
}
/* port: whether the host's game is being played (not its lobby, before or
after one) */
boolean network_game_server_playing(
struct network_game_server *server)
{
return server->state == _network_game_server_state_ingame;
}
/* port: whether the machines are loading the game (its start sent, still
in the pregame) */
boolean network_game_server_game_is_loading(
@@ -4287,6 +4341,19 @@ static boolean network_game_server_have_all_machines_have_precached(
return all_machines_have_precached;
}
/* whether the machine at this index is one of the host's bots (the VR
port's bots.c asks, on every platform: on the desktop no bot machine is
ever marked, so this is always false there) */
boolean network_game_server_is_bot_machine(
long machine_index)
{
struct network_game_server *server = global_network_game_server_get();
return server && VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) &&
network_game_server_bot_machines[machine_index] &&
server->game.machines[machine_index].machine_index == machine_index;
}
#ifdef HALO_ANDROID
/* ---------- bots (the VR port's: port/linux/game/bots.c)
@@ -4310,16 +4377,6 @@ static void network_game_server_bot_copy_name(
destination[index] = 0;
}
boolean network_game_server_is_bot_machine(
long machine_index)
{
struct network_game_server *server = global_network_game_server_get();
return server && VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) &&
network_game_server_bot_machines[machine_index] &&
server->game.machines[machine_index].machine_index == machine_index;
}
static short network_game_server_bot_machine_count(
struct network_game_server *server)
{
@@ -20,6 +20,10 @@ header included in hcex build.
/* ---------- prototypes/NETWORK_SERVER_MANAGER.C */
/* port: whether the host's game is being played (not its lobby) */
boolean network_game_server_playing(
struct network_game_server *server);
struct network_game_server;
struct game_variant;
+8
View File
@@ -918,6 +918,14 @@ static boolean playlist_profile_options_from_block(
csmemcpy(options, data + sizeof(header), sizeof(*options));
options->friendly_fire = (short)PIN(options->friendly_fire, 0, NUMBER_OF_FRIENDLY_FIRE_MODES - 1);
options->radar_players = (byte)PIN(options->radar_players, 0, NUMBER_OF_RADAR_PLAYERS - 1);
options->loadout = (byte)PIN(options->loadout, 0, NUMBER_OF_LOADOUTS - 1);
options->no_map_weapons = options->no_map_weapons != FALSE;
/* (the gravity rifle, a cut weapon, and the flamethrower, once loadout
weapons: rocket launchers, as the game makes them) */
if (options->primary_weapon >= NUMBER_OF_LOADOUT_WEAPONS)
options->primary_weapon = _loadout_weapon_rocket_launcher;
if (options->secondary_weapon >= NUMBER_OF_LOADOUT_WEAPONS)
options->secondary_weapon = _loadout_weapon_rocket_launcher;
return TRUE;
}
+4 -2
View File
@@ -4291,8 +4291,10 @@ boolean biped_update(
unit_get(biped_index)->unit.current_weapon_index);
/* (port: and with no weapon, which prevents it in the
Xbox game: a gametype's loadout of none) */
if ((weapon_index == NONE || !weapon_prevents_melee_attack(weapon_index)) &&
Xbox game: a gametype's loadout of none; not from a
vehicle's seat, which holds no weapon either) */
if (((weapon_index == NONE && biped->unit.parent_seat_index == NONE) ||
(weapon_index != NONE && !weapon_prevents_melee_attack(weapon_index))) &&
biped->unit.current_zoom_level==NONE)
{
short melee_speedup_ticks;
+76 -1
View File
@@ -4291,7 +4291,11 @@ boolean unit_throw_grenade_begin(
break;
default:
if (!weapon_prevents_grenade_throwing(weapon_index))
/* port: and with no weapon (a loadout of none), as melee
(bipeds.c), which weapon_prevents_grenade_throwing(NONE) prevents;
not from a vehicle's seat, which holds no weapon either */
if ((weapon_index == NONE && unit->unit.parent_seat_index == NONE) ||
(weapon_index != NONE && !weapon_prevents_grenade_throwing(weapon_index)))
{
struct animation_graph *animation_graph;
struct animation *animation;
@@ -7490,6 +7494,34 @@ boolean unit_drop_current_weapon(
return result;
}
/* port: a swap's weapon out: the one the player chose, though the weapon
in hand is still being put away for it (a network player's choice gets to
the host before the switch is done), not that one, their backup */
boolean unit_drop_selected_weapon(
long unit_index)
{
struct unit_datum *unit = unit_get(unit_index);
short slot = unit->unit.desired_weapon_index;
long weapon_index = unit_inventory_get_weapon(unit_index, slot);
if (weapon_index == NONE || slot == unit->unit.current_weapon_index ||
TEST_FLAG(weapon_definition_get(weapon_get(weapon_index)->definition_index)->weapon.flags,
_weapon_doesnt_count_toward_maximum_bit))
{
return unit_drop_current_weapon(unit_index, TRUE);
}
unit_drop_item(unit_index, weapon_index);
unit->unit.weapon_object_indices[slot] = NONE;
unit->unit.desired_weapon_index = unit->unit.current_weapon_index!=NONE ?
unit->unit.current_weapon_index : unit_weapon_next_index(unit_index, NONE, 0);
if (!weapon_can_be_fired(weapon_index))
{
object_delete(weapon_index);
}
return TRUE;
}
/* ---------- private code */
char const *base_seat_labels[NUMBER_OF_UNIT_BASE_SEATS] = {"asleep", "alert", "stand", "crouch", "flee", "flaming"};
@@ -8648,6 +8680,35 @@ enum
_collision_result_breakable_surface_bit = 3,
};
/* the globals' first multiplayer weapon, NONE where there is none: a
campaign map's globals list no multiplayer weapons (and its player starts
some levels unarmed, a10's), so its unarmed blow stays the game's, none */
static long unarmed_melee_weapon_definition_index(
void)
{
struct game_globals *game_globals = scenario_get_game_globals();
return game_globals && game_globals->weapon_list.count>0 ?
list_index_to_weapon_definition_index(0) : NONE;
}
/* port: the melee damage of a unit with no weapon (a gametype's loadout of
none): its own, else (a player's biped has none: players always had a
weapon) the blow of the globals' first multiplayer weapon, the assault
rifle's. network_damage.c takes it as the player's. */
long unit_unarmed_melee_damage(
long unit_index)
{
struct unit_definition *unit_definition = unit_definition_get(unit_get(unit_index)->definition_index);
long weapon_definition_index;
if (unit_definition->unit.melee_damage.index!=NONE)
return unit_definition->unit.melee_damage.index;
weapon_definition_index = unarmed_melee_weapon_definition_index();
return weapon_definition_index!=NONE ?
weapon_definition_get(weapon_definition_index)->weapon.melee_attack_damage.index : NONE;
}
void unit_cause_player_melee_damage(
long unit_index)
{
@@ -8816,6 +8877,20 @@ void unit_cause_player_melee_damage(
{
melee_damage_effect_index = unit_definition->unit.melee_damage.index;
}
/* port: a player with no weapon (a gametype's loadout of none), whose
biped has no blow of its own: unit_unarmed_melee_damage's, and its
response */
if (melee_damage_effect_index==NONE)
{
long weapon_definition_index = unarmed_melee_weapon_definition_index();
melee_damage_effect_index = unit_unarmed_melee_damage(unit_index);
if (weapon_definition_index!=NONE && melee_response_effect_index==NONE)
{
melee_response_effect_index =
weapon_definition_get(weapon_definition_index)->weapon.melee_attack_response.index;
}
}
if (best_object_index!=NONE)
{
+4
View File
@@ -725,6 +725,9 @@ boolean unit_add_equipment_to_inventory(
long unit_index,
long equipment_index,
short replace);
/* port: the melee damage of a unit with no weapon (units.c) */
long unit_unarmed_melee_damage(
long unit_index);
boolean unit_add_weapon_to_inventory(
long unit_index,
long weapon_index,
@@ -926,6 +929,7 @@ boolean unit_custom_animation_at_frame(
short frame_index);
boolean unit_drop_current_weapon(long unit_index, boolean immediate);
boolean unit_drop_selected_weapon(long unit_index);
boolean unit_throw_grenade_begin(long unit_index, real_vector2d const *alignment_vector);
+21
View File
@@ -853,6 +853,27 @@ done:
return !no_vehicles_moving;
}
short vehicle_bot_drive_kind(
long vehicle_index)
{
struct vehicle_datum *vehicle = vehicle_datum_get(vehicle_index);
struct vehicle_definition *definition = vehicle_specific_definition_get(
vehicle->definition_index);
switch (definition->vehicle_type)
{
case _vehicle_type_human_jeep:
case _vehicle_type_alien_scout:
return _vehicle_bot_drive_facing;
case _vehicle_type_human_tank:
return _vehicle_bot_drive_relative;
case _vehicle_type_turret:
return _vehicle_bot_drive_turret;
default:
return _vehicle_bot_drive_none;
}
}
#ifdef HALO_ANDROID
boolean vehicle_vr_steered_by_facing(
long vehicle_index)
+16
View File
@@ -81,6 +81,22 @@ void vehicle_render_debug(
/* ---------- public code */
/* port: how the VR port's bots (port/linux/game/bots.c) drive a vehicle:
turned to where its driver faces (a jeep, a scout), moved where he pushes
from where he faces (a tank), manned where it stands (a turret), or not
at all (the rest) */
enum
{
_vehicle_bot_drive_none = 0,
_vehicle_bot_drive_facing,
_vehicle_bot_drive_relative,
_vehicle_bot_drive_turret,
};
short vehicle_bot_drive_kind(
long vehicle_index);
boolean vehicle_is_flipped(
long vehicle_index);
#ifdef HALO_ANDROID
/* VR: whether the vehicle's driver steers it by where he faces (the jeep,
boat, plane, scout and fighter: the right stick steers them), as opposed to
+60 -3
View File
@@ -41,6 +41,7 @@ TOML_DIR = Path("port/third_party/tomlc17")
EXPAT_DIR = Path("port/third_party/expat")
EXPAT_SOURCES = ("xmlparse.c", "xmlrole.c", "xmltok.c")
KCP_DIR = Path("port/third_party/kcp")
MONOCYPHER_DIR = Path("port/third_party/monocypher")
MUSL_VERSION = "1.2.5"
MUSL_DIR = THIRD_PARTY / f"musl-{MUSL_VERSION}"
MUSL_URL = f"https://musl.libc.org/releases/musl-{MUSL_VERSION}.tar.gz"
@@ -168,6 +169,45 @@ def _find_ndk() -> Optional[Path]:
return None
# SDL3's Java takes the clipboard service for granted: Lepton, the Steam
# Frame's Android, has none, and the activity died starting. These edits let
# it do without one (the clipboard is then empty and keeps nothing).
SDL_JAVA_PATCHES = [
(" mClipMgr = (ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE);\n"
" mClipMgr.addPrimaryClipChangedListener(this);\n",
" mClipMgr = (ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE);\n"
" if (mClipMgr != null) /* halo-ce-vr: none in Lepton */\n"
" mClipMgr.addPrimaryClipChangedListener(this);\n"),
(" public boolean clipboardHasText() {\n",
" public boolean clipboardHasText() {\n"
" if (mClipMgr == null) /* halo-ce-vr */\n"
" return false;\n"),
(" public String clipboardGetText() {\n",
" public String clipboardGetText() {\n"
" if (mClipMgr == null) /* halo-ce-vr */\n"
" return null;\n"),
(" public void clipboardSetText(String string) {\n",
" public void clipboardSetText(String string) {\n"
" if (mClipMgr == null) /* halo-ce-vr */\n"
" return;\n"),
]
def patch_sdl_java() -> None:
"""SDL_JAVA_PATCHES, once, into the cloned SDL3's Java."""
path = SDL_DIR / "android-project" / "app" / "src" / "main" / "java" / "org" / "libsdl" / "app" / "SDLActivity.java"
if not path.is_file():
return
text = path.read_text()
if "halo-ce-vr: none in Lepton" in text:
return
for old, new in SDL_JAVA_PATCHES:
if text.count(old) != 1:
raise SystemExit(f"cannot patch {path}: SDL3's clipboard handler has changed")
text = text.replace(old, new)
path.write_text(text)
def fetch_third_party() -> None:
"""Download musl and SDL3 (configure time, once)."""
THIRD_PARTY.mkdir(parents=True, exist_ok=True)
@@ -181,6 +221,7 @@ def fetch_third_party() -> None:
print(f"Cloning SDL3 {SDL_TAG}")
subprocess.run(["git", "clone", "-q", "--depth", "1", "--branch", SDL_TAG, SDL_URL, str(SDL_DIR)],
check=True)
patch_sdl_java()
if not OPENXR_DIR.is_dir():
print(f"Downloading {OPENXR_URL}")
import zipfile
@@ -436,7 +477,8 @@ def generate_android_build(n: Writer, sln: Any) -> None:
guest_abi, guest_code, "-std=gnu11", "-D_GNU_SOURCE", "-DHALO_LINUX_PLATFORM_LAYER", "-w", profile_flags,
f"-include {prefix_header}", f"-include {platform_semantics_header}",
f"-I{LINUX_DIR}/src", f"-I{LINUX_DIR}/include", f"-I{PORT_DIR}/guest/runtime",
f"-I{PORT_DIR}/include", f"-I{TOML_DIR}", f"-I{EXPAT_DIR}", f"-I{KCP_DIR}", "-Isource -Isource/cseries",
f"-I{PORT_DIR}/include", f"-I{TOML_DIR}", f"-I{EXPAT_DIR}", f"-I{KCP_DIR}", f"-I{MONOCYPHER_DIR}",
"-Isource -Isource/cseries",
f"-I{SDL_DIR}/include", f"-I{gl_include}", *libc_includes, f"-idirafter {XDK_INCLUDE}",
])
guest_host_only = {"memory_watch.c"} # replaced by guest_memory_watch.c
@@ -454,6 +496,10 @@ def generate_android_build(n: Writer, sln: Any) -> None:
objects.append(guest_object(EXPAT_DIR / name, platform_cflags))
# internet play's reliable streams (port/third_party/kcp; p2p.c)
objects.append(guest_object(KCP_DIR / "ikcp.c", platform_cflags))
# internet play's signatures, for public games' listings
# (port/third_party/monocypher; p2p_crypto.c)
for name in ("monocypher.c", "monocypher-ed25519.c"):
objects.append(guest_object(MONOCYPHER_DIR / name, platform_cflags))
# the game's sin, pow and the rest, the same on every port
# (port/include/halo_math.h)
musl_math_cflags = " ".join([
@@ -565,6 +611,17 @@ def generate_android_build(n: Writer, sln: Any) -> None:
)
n.build(outputs=libmain, rule="android_host_link", inputs=host_objects, implicit=[libsdl])
# a do-nothing OpenXR layer under the Steam Frame's implicit layer's
# library name (port/android/xr_layer/fdm_injection_stub.c)
fdm_stub = jni_dir / "libVkLayer_VALVE_fdm_injection.so"
n.rule(
name="android_xr_layer",
command=(f"$android_host_cc -O2 -fPIC -shared -fvisibility=hidden -I{OPENXR_DIR}/include -o $out $in "
"-Wl,-z,max-page-size=16384"),
description="ANDROID XR LAYER $out",
)
n.build(outputs=fdm_stub, rule="android_xr_layer", inputs=PORT_DIR / "xr_layer" / "fdm_injection_stub.c")
# ---------- staging for Gradle
staged_sdl = jni_dir / "libSDL3.so"
@@ -572,7 +629,7 @@ def generate_android_build(n: Writer, sln: Any) -> None:
n.rule(name="android_copy", command="cp $in $out", description="ANDROID STAGE $out")
n.build(outputs=staged_sdl, rule="android_copy", inputs=libsdl)
n.build(outputs=staged_image, rule="android_copy", inputs=image)
n.build(outputs="android", rule="phony", inputs=[libmain, staged_sdl, staged_image])
n.build(outputs="android", rule="phony", inputs=[libmain, staged_sdl, staged_image, fdm_stub])
apk = PORT_DIR / "app" / "build" / "outputs" / "apk" / "debug" / "app-debug.apk"
n.rule(
@@ -583,6 +640,6 @@ def generate_android_build(n: Writer, sln: Any) -> None:
description="ANDROID GRADLE $out",
pool="console",
)
n.build(outputs=apk, rule="android_gradle", inputs=[libmain, staged_sdl, staged_image])
n.build(outputs=apk, rule="android_gradle", inputs=[libmain, staged_sdl, staged_image, fdm_stub])
n.build(outputs="android_apk", rule="phony", inputs=apk)
n.newline()
+35 -2
View File
@@ -70,9 +70,14 @@ LEFT_OUT = {
"ui\\shell\\main_menu\\multiplayer_type_select\\checking_updates_screen_join",
}
# children moved: (parent, child) to (x, y); the main menu's Quit up into
# Credits' place
# Credits' place; Multiplayer's Edit Gametypes and its line down a row, for
# Co-op (port_settings.WIDGET_PATCHES)
CHILD_OFFSETS = {
("ui\\shell\\main_menu\\main_menu_select_list", "ui\\shell\\main_menu\\main_menu_item_quit_game"): (192, 391),
("ui\\shell\\main_menu\\multiplayer_type_select\\multiplayer_type_select_list",
"ui\\shell\\main_menu\\multiplayer_type_select\\multiplayer_type_gametypes_item"): (0, 375),
("ui\\shell\\main_menu\\multiplayer_type_select\\multiplayer_type_select_screen",
"ui\\shell\\main_menu\\blueline"): (64, 358),
# the browser's rows up under the column titles, into the place of the
# scroll up button (hidden: the list does not scroll)
**{("ui\\shell\\main_menu\\multiplayer_type_select\\join_game\\join_game_items_list",
@@ -137,7 +142,8 @@ INCOMPATIBLE = set()
WIRED = {
"main menu quit game", "profile set edit begin", "mouse emit accept event", "mouse emit back event",
"mouse emit x event", "emit custom activation event", "single prev cl item activated", "controls back handler",
"gamespy back handler", "gamespy dismiss error", "gamespy dismiss filters", "gamespy screen init", "mp type set mode",
"gamespy back handler", "gamespy dismiss error", "gamespy dismiss filters", "gamespy screen init",
"gamespy screen dispose", "mp type set mode",
"campaign menu init", "campaign menu continue", "difficulty item select", "solo map list update",
"load game menu init", "load game menu dispose", "load game menu activated", "load game list update",
"load game menu delete request", "load game menu delete finish",
@@ -310,6 +316,22 @@ class Art:
f"\t\t<frame{attributes([('png', png), ('width', 512), ('height', 64)])}/>",
"\t</bitmap>"])
def port_picture(self, name: str, frames: list) -> str:
"""a <bitmap> of this port's own drawing: each frame an SVG (text,
width, height) written to port_svg/ and drawn"""
lines = [f"\t<bitmap{attributes([('name', name)])}>"]
for index, (text, width, height) in enumerate(frames):
svg = MENUS / "port_svg" / f"{name}__{index}.svg"
svg.parent.mkdir(parents=True, exist_ok=True)
svg.write_text(text)
png = f"ce/port/{name}__{index}.png"
(MENUS / png).parent.mkdir(parents=True, exist_ok=True)
Image.fromarray(render_svg(svg, SCALE), "RGBA").save(MENUS / png, optimize=True)
self.pngs.append(png)
lines.append(f"\t\t<frame{attributes([('png', png), ('width', width), ('height', height)])}/>")
lines.append("\t</bitmap>")
return "\n".join(lines)
def frame_sources(self, relative: str, index: int, count: int) -> list:
return [relative, f"{relative}__0"] if count == 1 else [f"{relative}__{index}"]
@@ -575,6 +597,8 @@ def main() -> None:
for tag in sorted(string_lists):
strings = [text_value(item["string"]) for item in tags.load(tag, "unicode_string_list")["strings"]]
strings = port_settings.STRING_OVERRIDES.get(our_name(tag), strings)
for index, texts in port_settings.STRING_INSERTS.get(our_name(tag), []):
strings = strings[:index] + texts + strings[index:]
lines.append(f"\t<strings{attributes([('name', our_name(tag))])}>")
lines += [f"\t\t<string{attributes([('text', text)])}/>" for text in strings]
lines.append("\t</strings>")
@@ -586,6 +610,15 @@ def main() -> None:
" (NON_HANDDRAWN.md; tools/ce_menus.py) -->", "<menus>"]
lines += [art.bitmap(tag) for tag in sorted(bitmaps)]
lines += [art.title(name, text) for name, text in sorted(port_settings.TITLES.items())]
# (the in-game pause menu's taller box: menu_tags.c's pause_patch)
for piece in ("left", "center", "right"):
lines.append(art.port_picture(f"pause/pausebox_{piece}", [
(port_settings.pause_box_svg(piece, port_settings.pause_box_height(buttons)), 4 if piece == "center" else 16,
256) for buttons in port_settings.PAUSE_BOX_BUTTONS]))
# (nothing: what a game map draws for a frame of ui.map's it has not,
# menu_tags.c)
lines.append(art.port_picture("blank", [
('<svg xmlns="http://www.w3.org/2000/svg" width="4" height="4" viewBox="0 0 4 4"/>\n', 4, 4)]))
lines.append("</menus>")
(CE / "bitmaps.xml").write_text("\n".join(lines) + "\n")
# what is not hand-drawn
+6
View File
@@ -117,6 +117,7 @@ TOML_DIR = Path("port/third_party/tomlc17")
EXPAT_DIR = Path("port/third_party/expat")
EXPAT_SOURCES = ("xmlparse.c", "xmlrole.c", "xmltok.c")
KCP_DIR = Path("port/third_party/kcp")
MONOCYPHER_DIR = Path("port/third_party/monocypher")
MUSL_MATH_DIR = Path("port/third_party/musl-math")
# the self-updater's TLS (port/linux/src/posix_update.c)
MBEDTLS_DIR = Path("port/third_party/mbedtls")
@@ -421,6 +422,7 @@ def generate_linux_build(n: Writer, sln: Any) -> None:
f"-I{TOML_DIR}",
f"-I{EXPAT_DIR}",
f"-I{KCP_DIR}",
f"-I{MONOCYPHER_DIR}",
"-Isource -Isource/cseries",
sdk_flags,
])
@@ -461,6 +463,10 @@ def generate_linux_build(n: Writer, sln: Any) -> None:
add_object(EXPAT_DIR / name, " ".join([abi, "-std=gnu11", f"-I{EXPAT_DIR}", "-w"]))
# internet play's reliable streams (port/third_party/kcp; p2p.c)
add_object(KCP_DIR / "ikcp.c", " ".join([abi, "-std=gnu11", "-w"]))
# internet play's signatures, for public games' listings
# (port/third_party/monocypher; p2p_crypto.c)
for name in ("monocypher.c", "monocypher-ed25519.c"):
add_object(MONOCYPHER_DIR / name, " ".join([abi, "-std=gnu11", "-w"]))
# the game's sin, pow and the rest, the same on every port
# (port/include/halo_math.h)
for source in musl_math_sources():
+300
View File
@@ -0,0 +1,300 @@
/*
P2P_LOBBY_CHECK.C
A check of internet play's signatures and the server browser's listings
(port/linux/src/p2p_crypto.c, port/linux/src/p2p_lobby.c), built with the
platform layer's flags by tools/test_linux_port.py: RFC 8032's Ed25519
vectors; the X25519 key a seed gives against its Ed25519 key's conversion;
bad signatures, other keys and small-order keys turned away; and a listing
from a host (this program, through stand-ins for p2p.c and p2p_signal.c) to
a browser (this program again): taken, and a tampered one, one on another's
slot, an older one, a retained one of long ago, a tombstone and a listing
older than it each handled as they must be. Prints PASS or the failures.
*/
#include "p2p_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---------- stand-ins for the rest of internet play */
pthread_mutex_t p2p_lock = PTHREAD_MUTEX_INITIALIZER;
static unsigned long clock_now = 1000;
static unsigned char seed[P2P_SEED_SIZE] = { 42 };
static unsigned char signing_key[P2P_KEY_SIZE], x25519_secret[P2P_KEY_SIZE], x25519_public[P2P_KEY_SIZE];
static unsigned char published[512];
static int published_size, published_closing, publish_count;
/* the game hosted (its invite's token; NULL: none) */
static const unsigned char *hosting_token;
void posix_random_bytes(void *buffer, unsigned long size) { memset(buffer, 0x5a, size); }
unsigned long p2p_now(void) { return clock_now; }
int config_boolean(const char *name) { (void)name; return 1; }
void platform_log(const char *format, ...) { (void)format; }
void p2p_hex(const unsigned char *bytes, int size, char *text)
{
int index;
for (index = 0; index < size; index++)
sprintf(text + 2 * index, "%02x", bytes[index]);
text[2 * size] = 0;
}
void p2p_key_hash(const unsigned char *key, unsigned char *hash)
{
unsigned char digest[P2P_SHA256_SIZE];
p2p_sha256(key, P2P_KEY_SIZE, digest);
memcpy(hash, digest, P2P_KEY_HASH_SIZE);
}
void p2p_identifier_from_hash(const unsigned char *hash, unsigned char *identifier)
{
memcpy(identifier, hash, P2P_IDENTIFIER_SIZE);
identifier[0] = (unsigned char)((identifier[0] & 0xFC) | 0x02);
}
const unsigned char *p2p_public_key(void) { return x25519_public; }
const unsigned char *p2p_signing_key(void) { return signing_key; }
void p2p_sign(const void *message, int size, unsigned char *signature)
{
p2p_ed25519_sign(seed, signing_key, message, size, signature);
}
void p2p_new_invite_if_listed(void) {}
void p2p_signal_lobby_topics(int listed, int browsing) { (void)listed; (void)browsing; }
void p2p_signal_lobby_query(void) {}
void p2p_signal_lobby_publish(const unsigned char *listing, int size, int closing)
{
memcpy(published, listing, (size_t)size);
published_size = size;
published_closing = closing;
publish_count++;
}
void p2p_signal_lobby_quit(void) {}
/* the p2p thread's calls: under p2p_lock, as there (p2p_lobby_update lets
go of it while it checks signatures) */
static void lobby_update(const unsigned char *token, int player_count, int maximum_player_count)
{
pthread_mutex_lock(&p2p_lock);
p2p_lobby_update(token, player_count, maximum_player_count);
pthread_mutex_unlock(&p2p_lock);
}
/* ---------- the checks */
static int failures;
static void check(int good, const char *what)
{
if (!good)
{
failures++;
printf("FAIL: %s\n", what);
}
}
static void unhex(const char *text, unsigned char *bytes)
{
size_t index;
for (index = 0; index < strlen(text) / 2; index++)
{
unsigned int byte;
sscanf(text + 2 * index, "%2x", &byte);
bytes[index] = (unsigned char)byte;
}
}
static void rfc8032(const char *seed_hex, const char *public_hex, const char *message_hex, const char *signature_hex)
{
unsigned char test_seed[32], expected_public[32], public_key[32], message[64], expected[64], signature[64];
int size = (int)strlen(message_hex) / 2;
unhex(seed_hex, test_seed);
unhex(public_hex, expected_public);
unhex(message_hex, message);
unhex(signature_hex, expected);
p2p_ed25519_public(test_seed, public_key, NULL);
p2p_ed25519_sign(test_seed, public_key, message, size, signature);
check(!memcmp(public_key, expected_public, 32), "RFC 8032 public key");
check(!memcmp(signature, expected, 64), "RFC 8032 signature");
check(p2p_ed25519_verify(public_key, message, size, signature), "RFC 8032 verification");
}
static void crypto_checks(void)
{
unsigned char test_seed[32], ed[32], secret[32], derived[32], converted[32], signature[64], other_ed[32];
unsigned char small[32] = { 1 };
unsigned char forged[64] = { 1 };
int index;
rfc8032("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", "",
"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b");
rfc8032("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
"3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", "72",
"92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00");
/* the X25519 key from a seed is its Ed25519 key's conversion, and
agrees on secrets */
for (index = 0; index < 16; index++)
{
unsigned char other[32], other_public[32], first[32], second[32];
memset(test_seed, index * 13 + 1, sizeof(test_seed));
p2p_ed25519_public(test_seed, ed, secret);
p2p_x25519(derived, secret, NULL);
check(p2p_ed25519_to_x25519(ed, converted) && !memcmp(converted, derived, 32), "X25519 key of a seed");
memset(other, index + 99, sizeof(other));
p2p_x25519(other_public, other, NULL);
p2p_x25519(first, secret, other_public);
p2p_x25519(second, other, converted);
check(!memcmp(first, second, 32), "secrets agree");
}
memset(test_seed, 3, sizeof(test_seed));
p2p_ed25519_public(test_seed, ed, NULL);
p2p_ed25519_sign(test_seed, ed, "listing", 7, signature);
check(p2p_ed25519_verify(ed, "listing", 7, signature), "a good signature");
check(!p2p_ed25519_verify(ed, "listinG", 7, signature), "another message");
signature[5] ^= 1;
check(!p2p_ed25519_verify(ed, "listing", 7, signature), "an altered R");
signature[5] ^= 1;
signature[40] ^= 1;
check(!p2p_ed25519_verify(ed, "listing", 7, signature), "an altered S");
signature[40] ^= 1;
memset(test_seed, 4, sizeof(test_seed));
p2p_ed25519_public(test_seed, other_ed, NULL);
check(!p2p_ed25519_verify(other_ed, "listing", 7, signature), "another key");
/* the identity, an order-8 point, and a signature anyone makes for the
identity */
check(!p2p_ed25519_to_x25519(small, converted), "the identity's conversion");
unhex("c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a", small);
check(!p2p_ed25519_to_x25519(small, converted), "an order-8 key's conversion");
memset(small, 0, sizeof(small));
small[0] = 1;
check(!p2p_ed25519_verify(small, "anything", 8, forged), "a small-order key's signature");
}
static int games(struct p2p_listing *listing)
{
struct p2p_listing all[4];
int count = p2p_lobby_games(all, 4);
if (count && listing)
*listing = all[0];
return count;
}
/* a payload heard on the host's slot (or another), then the browser's pass */
static void hear(const unsigned char *payload, int size, int retained, const char *slot)
{
unsigned char hash[P2P_KEY_HASH_SIZE];
char own[2 * P2P_KEY_HASH_SIZE + 1];
p2p_key_hash(x25519_public, hash);
p2p_hex(hash, P2P_KEY_HASH_SIZE, own);
pthread_mutex_lock(&p2p_lock);
p2p_lobby_slot_heard(slot ? slot : own, payload, size, retained);
pthread_mutex_unlock(&p2p_lock);
lobby_update(hosting_token, 3, 16);
}
static void lobby_checks(void)
{
static const unsigned char token[P2P_TOKEN_SIZE] = { 7, 7, 7 };
unsigned char first[512], tampered[512];
int first_size, count;
struct p2p_listing listing;
char expected_invite[P2P_LINK_SIZE];
p2p_ed25519_public(seed, signing_key, x25519_secret);
p2p_x25519(x25519_public, x25519_secret, NULL);
hosting_token = token;
/* hosting a public game: listed */
p2p_set_hosting_public(1);
p2p_set_game_listing("Test game", "bloodgulch", "Slayer", 2, 1, 0, 1);
lobby_update(token, 3, 16);
check(publish_count == 1 && !published_closing && p2p_lobby_listed(), "the host publishes its listing");
memcpy(first, published, (size_t)published_size);
first_size = published_size;
/* the browser takes it */
p2p_lobby_browse(1);
hear(first, first_size, 0, NULL);
count = games(&listing);
check(count == 1, "the browser takes the listing");
{
unsigned char bytes[P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE];
char text[2 * (P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE) + 1];
p2p_key_hash(x25519_public, bytes);
memcpy(bytes + P2P_KEY_HASH_SIZE, token, P2P_TOKEN_SIZE);
p2p_hex(bytes, sizeof(bytes), text);
snprintf(expected_invite, sizeof(expected_invite), "halo://join/%s", text);
}
check(count == 1 && !strcmp(listing.invite, expected_invite), "the listing's invite is the host's");
check(count == 1 && !strcmp(listing.name, "Test game") && !strcmp(listing.map, "bloodgulch") &&
!strcmp(listing.gametype, "Slayer") && listing.player_count == 3 && listing.maximum_player_count == 16 &&
listing.engine_type == 2 && listing.open && listing.has_teams && !listing.in_progress, "the listing's details");
/* a change: published again after TRIGGER_INTERVAL */
p2p_set_game_listing(NULL, NULL, NULL, 2, 1, 1, 1);
clock_now += 1000;
lobby_update(token, 3, 16);
check(publish_count == 1, "no publish sooner than 5 s after the last");
clock_now += 5000;
lobby_update(token, 3, 16);
check(publish_count == 2, "a change published within 5 s");
hear(published, published_size, 0, NULL);
check(games(&listing) == 1 && listing.in_progress, "the browser takes the newer listing");
/* the older one again: no change */
hear(first, first_size, 0, NULL);
check(games(&listing) == 1 && listing.in_progress, "an older listing is ignored");
/* tampered: a newer sequence and another name, with the old signature */
memcpy(tampered, published, (size_t)published_size);
tampered[9] = (unsigned char)(tampered[9] + 50);
tampered[66] = 'X';
hear(tampered, published_size, 0, NULL);
check(games(&listing) == 1 && listing.name[0] == 'T', "a tampered listing is ignored");
/* on another's slot */
hear(published, published_size, 0, "00112233445566778899aabbccddeeff");
check(games(NULL) == 1, "a listing on another's slot is ignored");
/* an emptied slot is no news */
hear(published, 0, 0, NULL);
check(games(NULL) == 1, "an emptied slot is not a delete");
/* expiry: 90 s unheard */
clock_now += 91000;
lobby_update(token, 3, 16);
check(games(NULL) == 0, "a listing unheard for 90 s expires");
/* the slot's retained copy, of about now: taken (the host published it
again on its 30 s) */
lobby_update(token, 3, 16);
hear(published, published_size, 1, NULL);
check(games(NULL) == 1, "a retained listing of about now is taken");
/* the host stops: a tombstone, which removes the game */
p2p_set_hosting_public(0);
lobby_update(token, 3, 16);
check(published_closing && !p2p_lobby_listed(), "the host publishes a tombstone");
hear(published, published_size, 0, NULL);
check(games(NULL) == 0, "a tombstone removes the game");
/* the listing before the tombstone does not bring it back */
hear(first, first_size, 0, NULL);
check(games(NULL) == 0, "a listing older than the tombstone is ignored");
/* listed again (a new sequence): shown again */
p2p_set_hosting_public(1);
clock_now += 10000;
lobby_update(token, 3, 16);
hear(published, published_size, 0, NULL);
check(games(NULL) == 1, "a listing newer than the tombstone is taken");
}
int main(void)
{
crypto_checks();
lobby_checks();
printf("%s (%d failures)\n", failures ? "FAIL" : "PASS", failures);
return failures != 0;
}
+166 -17
View File
@@ -186,7 +186,8 @@ def _screen(folder: str, spec: dict, rows: list, list_inputs: list, list_handler
f'<child{attributes([("widget", f"{base}/options_menu")])}/>'])
lines += _widget(f"{base}/{header}", [("controller", 1), ("left", 35), ("top", 11), ("width", 605), ("height", 59),
("bitmap", header_bitmap)], [])
lines += _widget(f"{base}/help", [("type", "text"), ("controller", 1), ("left", 68), ("top", 350), ("width", 482),
lines += _widget(f"{base}/help", [("type", "text"), ("controller", 1), ("left", 68),
("top", spec.get("help_top", 350)), ("width", 482),
("height", 60), ("string_list", f"{base}/help_strings"),
("font", "ui\\large_ui"), ("color", "#FFFFFFFF")], [])
children = [f'<data input="{name}"/>' for name in list_inputs] + list_handlers
@@ -315,9 +316,28 @@ def replaced(folder: str) -> bool:
MT = "main_menu/multiplayer_type_select"
# strings added to the PC version's lists: (at, strings) of each list. Slayer's
# kills to win goes up to 500, for big games (the Xbox editor's are
# source/interface/ui_widget.c's kills_to_win_extra_strings): the values,
# and their helps after the five of its own (its helps are the rows' values'
# in turn: menu_functions.c's gametype_option_help)
SLAYER_EDIT = "main_menu/settings_select/multiplayer_setup/playlist_edit/slayer_edit"
STRING_INSERTS = {
f"{SLAYER_EDIT}/var_kills_to_win": [(5, ["75", "100", "150", "200", "250", "500"])],
f"{SLAYER_EDIT}/cap_slayer": [(11, [
"Seventy-five kills to win. Settle in for a long\\nfight.",
"A hundred kills to win. Made for big games.",
"A hundred and fifty kills to win. Only a crowded\\nserver gets there.",
"Two hundred kills to win. Bring friends. Lots of\\nthem.",
"Two hundred and fifty kills to win.",
"Five hundred kills to win. You'll be here a while.",
])],
}
# a custom loadout's weapons (game_engine.h's _loadout_weapon_*)
LOADOUT_WEAPONS = ["NONE", "RANDOM", "ASSAULT RIFLE", "PISTOL", "SHOTGUN", "SNIPER RIFLE", "ROCKET LAUNCHER",
"PLASMA PISTOL", "PLASMA RIFLE", "NEEDLER", "FUEL ROD", "FLAMETHROWER"]
"PLASMA PISTOL", "PLASMA RIFLE", "NEEDLER"]
def LOADOUT_HELPS(slot: str) -> list:
@@ -330,20 +350,22 @@ def LOADOUT_HELPS(slot: str) -> list:
STRING_OVERRIDES.update({
f"{MT}/multiplayer_options": ["JOIN GAME", "CREATE GAME", "INTERNET", "LAN", "DIRECT LINK", "EDIT GAMETYPES",
"SERVER BROWSER"],
"SERVER BROWSER", "CO-OP CAMPAIGN"],
f"{MT}/multiplayer_option_descriptions": [
"Browse the games on the\\nInternet.",
"Join a multiplayer game on\\nyour LAN.",
"Join a game by its invite link,\\nor one a Discord invite\\nreached.",
"Host a game on the Internet:\\nplayers join by its invite\\nlink or Discord.",
"Host a game on your LAN\\nonly.",
"Play the campaign with a\\nfriend in split screen:\\nplayer 2 on a gamepad.",
"Set all the attributes for your \\nmultiplayer gametypes and\\nkeep them for future use.",
],
"main_menu/gametype_select/var_gametype_banks": ["STANDARD", "CUSTOM"],
# (the PC's weapon sets, then the Xbox's NO GRENADES: menu_functions.c's
# gametype_options maps them to the engine's)
"main_menu/settings_select/multiplayer_setup/item_options_edit/item_options_labels": ["INFINITE GRENADES:", "WEAPON SET:", "VEHICLE SET:", "STARTING EQUIPMENT:",
"VEHICLE RESPAWN TIME:", "LOADOUT:", "PRIMARY WEAPON:", "SECONDARY WEAPON:"],
"VEHICLE RESPAWN TIME:", "LOADOUT:", "PRIMARY WEAPON:", "SECONDARY WEAPON:",
"MAP WEAPONS:"],
# (the helps of each option's values in turn: menu_functions.c's
# gametype_option_help)
"main_menu/settings_select/multiplayer_setup/item_options_edit/cap_item_options": [
@@ -365,6 +387,8 @@ STRING_OVERRIDES.update({
"The map's weapons, without any grenades.",
"You will start the game with the weapon set that the \\nmap designers custom tailored for it.",
"Use the generic starting weapons across all maps.",
"The map's weapons appear where its designers\\nplaced them.",
"No weapons appear on the map: with a loadout of\\nNONE and NONE, melee and grenades only.",
"The weapons follow the weapon set above.",
"Everyone starts with the primary and secondary\\nweapons below; the map's weapons are its own.",
*LOADOUT_HELPS("primary"),
@@ -395,6 +419,10 @@ WIDGET_PATCHES = {
f'<on event="start" run="mp type set mode" open="{MT}/join_game/join_game_screen"/>',
'<on event="left_mouse" run="mouse emit accept event"/>',
]},
# (Co-op, after Create Game's: _coop)
f"{MT}/multiplayer_type_select_list": {"insert_before": {
f"{MT}/multiplayer_type_gametypes_item": [f'<child widget="{MT}/multiplayer_type_coop_item" y="309"/>'],
}},
f"{MT}/join_game/header_join_game": {"children": [
f'<child widget="{MT}/join_game/header_server_browser"/>',
f'<child widget="{MT}/join_game/header_direct_link"/>',
@@ -405,9 +433,10 @@ WIDGET_PATCHES = {
# (Item Options' loadout rows, over its buttons)
"main_menu/settings_select/multiplayer_setup/item_options_edit/item_options_menu": {"insert_before": {
"main_menu/settings_select/multiplayer_setup/item_options_edit/item_button_bar": [
f'<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_loadout" x="54" y="163"/>',
f'<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_primary_weapon" x="54" y="193"/>',
f'<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_secondary_weapon" x="54" y="223"/>',
f'<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_map_weapons" x="54" y="163"/>',
f'<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_loadout" x="54" y="193"/>',
f'<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_primary_weapon" x="54" y="223"/>',
f'<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_secondary_weapon" x="54" y="253"/>',
]}},
# (the PC's Vehicles row's Start opened Item Options)
"main_menu/settings_select/multiplayer_setup/playlist_edit/playlist_edit_vehicles_list_item": {"handlers": [
@@ -416,10 +445,12 @@ WIDGET_PATCHES = {
'<on event="left_mouse" run="mouse emit accept event"/>',
]},
# (Direct Link's clipboard button in the place of the PC version's
# Refresh, which this port has not: the lists update themselves; "swap"
# puts another widget in a child's place)
# Refresh, and Refresh in Get List's: the Server Browser's only, which
# asks the public games' hosts again; "swap" puts another widget in a
# child's place)
f"{MT}/join_game/join_game_button_bar": {"swap": {
f"{MT}/join_game/join_game_button_refresh": f"{MT}/join_game/button_clipboard",
f"{MT}/join_game/join_game_button_update": f"{MT}/join_game/join_game_button_refresh",
}},
}
@@ -429,6 +460,7 @@ TITLES = {
f"{MT}/join_game/header_server_browser": "SERVER BROWSER",
f"{MT}/join_game/header_direct_link": "DIRECT LINK",
f"{MT}/lobby/header_lobby": "GAME LOBBY",
f"{MT}/coop/header_player_2": "PLAYER 2 PROFILE",
}
@@ -448,6 +480,39 @@ def title_backdrop(width: float) -> str:
" </g>", "</svg>", ""])
# the in-game pause menu's box, taller for the buttons this port adds
# (menu_tags.c's pause_patch: SETTINGS, and the host's END GAME): the
# pausebox5 redraw's rounded box (ui-svg-handmade/shell/bitmaps), drawn to
# hold each number of buttons, one frame each (the Xbox's 2-button box is
# 159 units high, and each button 35 more)
PAUSE_BOX_BUTTONS = [3, 4]
def pause_box_height(buttons: int) -> int:
return 159 + (buttons - 2) * 35
def pause_box_svg(piece: str, height: int) -> str:
"""a piece of the pause box (left cap, centre strip, right cap; 9-slice:
the strip tiles across), height units high in its 256-high texture"""
width = 4 if piece == "center" else 16
lines = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="256" viewBox="0 0 {width} 256">']
if piece == "center":
lines += [f' <rect x="0" y="2" width="4" height="{height - 4}" fill="#0a2649" fill-opacity="0.75"/>',
' <rect x="0" y="0" width="4" height="2" fill="#2896ff"/>',
f' <rect x="0" y="{height - 2}" width="4" height="2" fill="#2896ff"/>']
else:
bottom, corner = height - 1, height - 9.5
cap = [f' <path d="M9.5,1 H17 V{bottom} H9.5 A8.5,8.5 0 0 1 1,{corner} V9.5 A8.5,8.5 0 0 1 9.5,1 Z" '
'fill="#0a2649" fill-opacity="0.75" fill-rule="evenodd"/>',
f' <path d="M17,1 H9.5 A8.5,8.5 0 0 0 1,9.5 V{corner} A8.5,8.5 0 0 0 9.5,{bottom} H17" fill="none" '
'stroke="#2896ff" stroke-width="2"/>']
lines += ([' <g transform="matrix(-1 0 0 1 16 0)">'] + cap + [' </g>']) if piece == "right" else \
[line[2:] for line in cap]
lines += ["</svg>", ""]
return "\n".join(lines)
# where the PC headers' text is (units of their 512x64), and its colour
TITLE_LEFT, TITLE_TOP, TITLE_CAP = 30.68, 33.5, 21.5
TITLE_COLOR = (0x29, 0x95, 0xFD, 255)
@@ -498,9 +563,12 @@ def _join_game_extras() -> list:
def _server_settings() -> list:
"""Create Game's server settings (in the PC version's place): the game's
name, the most players (up to the port's 128), and its invite link"""
name, the most players (up to the port's 128), its invite link, and
whether the server browser lists it (PUBLIC or PRIVATE: an internet
game's)"""
base = f"{MT}/server_settings"
spec = {"screen": "server_settings_screen", "spacing": 30,
# (ten rows: closer together, the help lower)
spec = {"screen": "server_settings_screen", "spacing": 28, "help_top": 358,
"header": ("header_server_settings", f"{base}/header_server_settings")}
rows, extra = [], []
extra += _value_row(base, "server_name", 0, "ss edit server name")
@@ -524,6 +592,25 @@ def _server_settings() -> list:
rows.append((f"{base}/op_max_players", None))
extra += _value_row(base, "invite", 2, "ss copy invite")
rows.append((f"{base}/op_invite", None))
# (PUBLIC or PRIVATE: menu_functions.c's server_settings_update)
extra += _widget(f"{base}/op_listing", [("width", 512), ("height", 28),
("flags", "pass_unhandled_to_focused_child"),
("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")],
[f'<child widget="{base}/listing_label"/>',
f'<child widget="{base}/listing_spinner" x="320" y="1"/>'])
extra += _widget(f"{base}/listing_label", [("type", "text"), ("controller", 1), ("width", 300),
("height", 22), ("string_list", f"{base}/labels"),
("string_index", 9), ("font", "ui\\large_ui"),
("color", "#FF2896FF"), ("text_x", 13), ("text_y", 4)], [])
extra += _widget(f"{base}/listing_spinner",
[("type", "spinner"), ("left", 3), ("top", 2), ("width", 147), ("height", 20),
("flags", "pass_unhandled_to_focused_child left_right_tabs_items"),
("strings", "PUBLIC|PRIVATE"), ("font", "ui\\large_ui"),
("color", "#FF2896FF"), ("align", "center"), ("text_y", 1),
("header_bitmap", "bitmaps/arrow_sm_left"), ("footer_bitmap", "bitmaps/arrow_sm_right"),
("header_bounds", "7 -6 19 0"), ("footer_bounds", "7 150 19 156")],
['<on event="left_mouse" run="mouse spinner 1wide click"/>'])
rows.append((f"{base}/op_listing", None))
# the gametype's options for this game (the gametype editor's screens,
# editing a copy of the gametype chosen: "port setup edit")
for index, (key, screen) in enumerate(SETUP_OPTION_SCREENS):
@@ -553,7 +640,7 @@ def _server_settings() -> list:
'<on event="left_mouse" run="mouse emit accept event"/>'])
extra += _strings(f"{base}/labels", ["GAME NAME:", "MAXIMUM PLAYERS:", "INVITE LINK:", "GAME TYPE:",
"PLAYER OPTIONS:", "ITEM OPTIONS:", "VEHICLE OPTIONS:", "INDICATOR OPTIONS:",
"TEAMPLAY OPTIONS:"])
"TEAMPLAY OPTIONS:", "LISTING:"])
extra += _strings(f"{base}/help_strings", [
"",
"The name the game shows in the lists of games.\\nEnter changes it.",
@@ -565,6 +652,9 @@ def _server_settings() -> list:
"Each team's vehicles and their respawn time, for\\nthis game.",
"The motion tracker and nav points, for this game.",
"Friendly fire and team balance, for this game.",
# (LISTING's, by its choice)
"Anyone can see and join your game: it is listed\\nin everyone's Server Browser.",
"Only players with your invite link can join.",
])
lines = _screen(base, spec, rows, ["server settings update"],
['<on event="created" run="server settings init"/>'], extra)
@@ -676,13 +766,70 @@ def _lobby() -> list:
return lines
def _coop() -> list:
"""Co-op, the campaign for two players on this machine in split screen
(the Xbox's Cooperative Play): Multiplayer's CO-OP CAMPAIGN, then player
2's profile, chosen with player 2's controller (its rows take any
controller's presses, as the shared rows do only controller 1's), then
New Game's levels and difficulty (menu_functions.c's coop_begin)"""
base = f"{MT}/coop"
lines = _widget(f"{MT}/multiplayer_type_coop_item",
[("type", "text"), ("left", 51), ("width", 232), ("height", 32), ("bitmap", "bitmaps/list_item_bkd"),
("string_list", f"{MT}/multiplayer_options"), ("string_index", 7), ("font", "ui\\large_ui"),
("color", "#FF2896FF"), ("text_x", 13), ("text_y", 5)],
[f'<on event="a" run="port coop begin" open="{base}/player_2_profile_screen" branch="true"/>',
f'<on event="start" run="port coop begin" open="{base}/player_2_profile_screen" branch="true"/>',
'<on event="left_mouse" run="mouse emit accept event"/>'])
lines += _widget(f"{base}/player_2_profile_screen", [("width", 640), ("height", 480),
("flags", "pass_unhandled_to_focused_child"),
("bitmap", "bitmaps/gradient")],
['<child widget="main_menu/new_select/sel_list_desc_bkd"/>',
f'<child widget="{base}/player_2_profile_list"/>',
f'<child widget="{base}/header_player_2"/>',
f'<child widget="{base}/player_2_help" x="20" y="416"/>'])
lines += _header(f"{base}/header_player_2", f"{base}/header_player_2")
rows = [f'<child widget="{base}/list_item_{index}" x="20" y="{73 + 30 * index}"/>' for index in range(11)]
lines += _widget(f"{base}/player_2_profile_list",
[("type", "column_list"), ("width", 640), ("height", 480),
("flags", "pass_unhandled_to_focused_child up_down_tabs_children"),
("description", "main_menu/profile_manager/player_profile_extended_desc")],
['<data input="3wide player profile list update"/>',
'<on event="created" run="port coop player 2 list initialize"/>',
'<on event="deleted" run="player profile list dispose"/>',
'<on event="custom_activation" run="port coop player 2" '
'open="main_menu/solo_level_select/solo_level_select_screen" branch="true"/>',
*rows, f'<child widget="{base}/player_2_button_bar" y="414"/>'])
for index in range(11):
lines += _widget(f"{base}/list_item_{index}",
[("width", 390), ("height", 28),
("bitmap", "bitmaps/sel_list_item_bkd_top" if index == 0 else "bitmaps/sel_list_item_bkd"),
("font", "ui\\large_ui"), ("color", "#FF2896FF"), ("align", "center"), ("text_y", 3)],
['<on event="a" run="single prev cl item activated"/>',
'<on event="start" run="single prev cl item activated"/>',
'<on event="left_mouse" run="mouse emit accept event"/>',
'<child widget="main_menu/new_select/list_item_text" x="25"/>',
'<child widget="main_menu/new_select/list_item_arrows"/>'])
lines += _widget(f"{base}/player_2_help", [("type", "text"), ("width", 350), ("height", 24),
("text", "Player 2: choose with your controller."),
("font", "ui\\small_ui"), ("color", "#FF2896FF"), ("text_y", 5),
("text_flags", "no_focus_test")], [])
lines += _widget(f"{base}/player_2_button_bar", [("type", "column_list"), ("width", 640), ("height", 28),
("flags", "pass_unhandled_to_focused_child left_right_tabs_items")],
['<data input="common button bar update"/>',
'<child widget="main_menu/profile_manager/profile_manager_button_ok" x="380" y="1"/>',
'<child widget="common_button_back" x="510" y="1"/>'])
return lines
def _item_options_extras() -> list:
"""Item Options' loadout rows: CATEGORY (the weapon set) or CUSTOM (each
player's primary and secondary weapons)"""
"""Item Options' rows of the port's: the map's weapons (YES or NO), and
the loadout, CATEGORY (the weapon set) or CUSTOM (each player's primary
and secondary weapons)"""
base = "main_menu/settings_select/multiplayer_setup/item_options_edit"
lines = []
for index, (key, strings) in enumerate((("loadout", "var_loadout"), ("primary_weapon", "var_loadout_weapon"),
("secondary_weapon", "var_loadout_weapon"))):
for key, strings, label in (("map_weapons", "var_map_weapons", 8), ("loadout", "var_loadout", 5),
("primary_weapon", "var_loadout_weapon", 6),
("secondary_weapon", "var_loadout_weapon", 7)):
lines += _widget(f"{base}/op_{key}", [("width", 512), ("height", 28),
("flags", "pass_unhandled_to_focused_child"),
("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")],
@@ -690,7 +837,7 @@ def _item_options_extras() -> list:
f'<child widget="{base}/{key}_spinner" x="286" y="1"/>'])
lines += _widget(f"{base}/{key}_label", [("type", "text"), ("controller", 1), ("width", 300), ("height", 22),
("string_list", f"{base}/item_options_labels"),
("string_index", 5 + index), ("font", "ui\\large_ui"),
("string_index", label), ("font", "ui\\large_ui"),
("color", "#FF2896FF"), ("text_x", 13), ("text_y", 4)], [])
lines += _widget(f"{base}/{key}_spinner",
[("type", "spinner"), ("top", 2), ("width", 206), ("height", 20),
@@ -700,6 +847,7 @@ def _item_options_extras() -> list:
("header_bitmap", "bitmaps/arrow_sm_left"), ("footer_bitmap", "bitmaps/arrow_sm_right"),
("header_bounds", "7 -13 19 -7"), ("footer_bounds", "7 208 19 214")],
['<on event="left_mouse" run="mouse spinner 1wide click"/>'])
lines += _strings(f"{base}/var_map_weapons", ["YES", "NO"])
lines += _strings(f"{base}/var_loadout", ["CATEGORY", "CUSTOM"])
lines += _strings(f"{base}/var_loadout_weapon", LOADOUT_WEAPONS)
return lines
@@ -714,6 +862,7 @@ def multiplayer_files() -> dict:
f"{MT}/join_game".replace("/", ".") + ".port.xml": head + _join_game_extras() + ["</menus>", ""],
f"{MT}/server_settings".replace("/", ".") + ".xml": head + _server_settings() + ["</menus>", ""],
f"{MT}/lobby".replace("/", ".") + ".xml": head + _lobby() + ["</menus>", ""],
f"{MT}/coop".replace("/", ".") + ".xml": head + _coop() + ["</menus>", ""],
"main_menu/settings_select/multiplayer_setup/item_options_edit".replace("/", ".") + ".port.xml": head + _item_options_extras() + ["</menus>", ""],
}
+37
View File
@@ -302,3 +302,40 @@ def test_menu_settings_exist():
controls = set(re.findall(r'"(controls\.[a-z_]+)"', (root / "port/linux/src/xinput_sdl.c").read_text()))
assert controls == {name for name in known if name.startswith("controls.")}
assert controls == set(re.findall(r'\{ "(controls\.[a-z_]+)", L"', functions))
def test_p2p_signatures_and_listings(tmp_path):
"""internet play's Ed25519 (RFC 8032), the X25519 key of a seed, and the
server browser's listings from host to browser (tools/p2p_lobby_check.c),
built with the flags ninja gives the platform layer"""
import shlex
if not shutil.which("clang") or not shutil.which("ninja") or not Path("build.ninja").is_file():
pytest.skip("needs clang, ninja and a configured build")
command = subprocess.run(["ninja", "-t", "commands", "build/linux/obj/port/linux/src/p2p_crypto.o"],
capture_output=True, text=True, check=True).stdout.strip().splitlines()[-1]
words = shlex.split(command)
# (the compiler, and whatever runs it, ccache in CI: up to the first flag)
while words and not words[0].startswith("-"):
words = words[1:]
flags = []
skip = False
for word in words:
if skip:
skip = False
elif word in ("-MF", "-o", "-c"):
skip = True
elif word == "-MMD" or word.startswith("-flto") or word.startswith("-fprofile-use"):
continue
else:
flags.append(word)
program = tmp_path / "p2p_lobby_check"
built = subprocess.run(["clang", *flags, "-O1", "-no-pie", "-Wl,--unresolved-symbols=ignore-all", "-o",
str(program), "tools/p2p_lobby_check.c", "port/linux/src/p2p_crypto.c",
"port/linux/src/p2p_lobby.c", "port/third_party/monocypher/monocypher.c",
"port/third_party/monocypher/monocypher-ed25519.c"],
capture_output=True, text=True)
assert built.returncode == 0, built.stderr[-4000:]
result = subprocess.run([str(program)], capture_output=True, text=True, timeout=60)
assert result.returncode == 0, result.stdout
assert "PASS" in result.stdout
+10
View File
@@ -52,6 +52,7 @@ TOML_DIR = Path("port/third_party/tomlc17")
EXPAT_DIR = Path("port/third_party/expat")
EXPAT_SOURCES = ("xmlparse.c", "xmlrole.c", "xmltok.c", "random_rand_s.c")
KCP_DIR = Path("port/third_party/kcp")
MONOCYPHER_DIR = Path("port/third_party/monocypher")
def updater_defines(release: bool) -> str:
@@ -335,6 +336,10 @@ def generate_windows_build(n: Writer, sln: Any) -> None:
f"-include {tags_header}",
f"-I{crt_include}",
f"-I{PORT_DIR / 'include'}",
# the port's headers the game's units include (halo_keyboard.h,
# halo_menus.h), but not the Linux build's C runtime wrappers
# next to them, which no game unit includes in quotes
f"-iquote {LINUX_DIR / 'include'}",
game_defines_and_includes(linux_config),
# the Xbox SDK declarations (port/include/xdk) come before the
# Windows SDK, which has headers of the same names
@@ -357,6 +362,7 @@ def generate_windows_build(n: Writer, sln: Any) -> None:
f"-I{TOML_DIR}",
f"-I{EXPAT_DIR}",
f"-I{KCP_DIR}",
f"-I{MONOCYPHER_DIR}",
# halo_linux_winsock_names.h, but not the Linux build's C runtime
# wrappers next to it
f"-iquote {LINUX_DIR / 'include'}",
@@ -401,6 +407,10 @@ def generate_windows_build(n: Writer, sln: Any) -> None:
add_object(EXPAT_DIR / name, " ".join([abi, "-std=gnu11", f"-I{EXPAT_DIR}", "-w"]))
# internet play's reliable streams (port/third_party/kcp; p2p.c)
add_object(KCP_DIR / "ikcp.c", " ".join([abi, "-std=gnu11", "-w"]))
# internet play's signatures, for public games' listings
# (port/third_party/monocypher; p2p_crypto.c)
for name in ("monocypher.c", "monocypher-ed25519.c"):
add_object(MONOCYPHER_DIR / name, " ".join([abi, "-std=gnu11", "-w"]))
# the game's sin, pow and the rest, the same on every port
# (port/include/halo_math.h)
for source in musl_math_sources():