diff --git a/README.md b/README.md index 2db745cc..2d9943fb 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ builds of the latest release: | Platform | Release | Debug | | --- | --- | --- | -| Linux | [halo-linux-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-linux-debug.zip) | -| Windows | [halo-windows-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-windows-debug.zip) | -| Android | [halo-android-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-android-debug.zip) | +| Linux | [halo-linux-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-debug.zip) | +| Windows | [halo-windows-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-debug.zip) | +| Android | [halo-android-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-debug.zip) | Use the release build to play. The debug build stops at the first failed assertion and writes it to the log. Use the debug build to find and report @@ -32,7 +32,7 @@ if you want to install it. Refer to "Updates" in [port/linux/README.md](port/linux/README.md#updates). Each build of the `main` branch that passes on all three platforms is a new -release. The [Releases](https://github.com/cybersecurity/halo-ce-universal/releases) +release. The [Releases](https://github.com/OpenCommunityEdition/OpenCE/releases) page keeps the last five releases. If the latest build has a problem, get an older build from that page. diff --git a/port/android/host/host_main.c b/port/android/host/host_main.c index 31d30cf9..7cf6ab65 100644 --- a/port/android/host/host_main.c +++ b/port/android/host/host_main.c @@ -301,6 +301,19 @@ static void *game_main(void *unused) environment_set(&environment, "HALO_SAVE_ROOT", save_root); time_zone(zone, sizeof(zone)); environment_set(&environment, "TZ", zone); + /* internet play's MQTT brokers (network.brokers_file): the APK's list, + written beside config.toml at each start, as a desktop update replaces + the file beside its game */ + { + size_t brokers_size = 0; + void *brokers = SDL_LoadFile("brokers.txt", &brokers_size); + + snprintf(path, sizeof(path), "%s/brokers.txt", data_root); + if (!brokers || !SDL_SaveFile(path, brokers, brokers_size)) + host_logf(HOST_LOG_ERROR, "cannot write %s: %s", path, SDL_GetError()); + SDL_free(brokers); + } + snprintf(path, sizeof(path), "%s/config.toml", data_root); image = SDL_LoadFile("halo_guest.elf", &image_size); if (!image) diff --git a/port/assets/hud/hud_unit_backgrounds__6.png b/port/assets/hud/hud_unit_backgrounds__6.png index 79c8f2c3..74f0d399 100644 Binary files a/port/assets/hud/hud_unit_backgrounds__6.png and b/port/assets/hud/hud_unit_backgrounds__6.png differ diff --git a/port/assets/hud/hud_unit_backgrounds__7.png b/port/assets/hud/hud_unit_backgrounds__7.png index c9c10d86..ab854f15 100644 Binary files a/port/assets/hud/hud_unit_backgrounds__7.png and b/port/assets/hud/hud_unit_backgrounds__7.png differ diff --git a/port/assets/hud/svg/hud__bitmaps__combined__hud_unit_backgrounds__6.svg b/port/assets/hud/svg/hud__bitmaps__combined__hud_unit_backgrounds__6.svg index 55246c2c..36b2dd35 100644 --- a/port/assets/hud/svg/hud__bitmaps__combined__hud_unit_backgrounds__6.svg +++ b/port/assets/hud/svg/hud__bitmaps__combined__hud_unit_backgrounds__6.svg @@ -2,7 +2,7 @@ - + diff --git a/port/assets/hud/svg/hud__bitmaps__combined__hud_unit_backgrounds__7.svg b/port/assets/hud/svg/hud__bitmaps__combined__hud_unit_backgrounds__7.svg index f78451fb..d765b142 100644 --- a/port/assets/hud/svg/hud__bitmaps__combined__hud_unit_backgrounds__7.svg +++ b/port/assets/hud/svg/hud__bitmaps__combined__hud_unit_backgrounds__7.svg @@ -2,7 +2,7 @@ - + diff --git a/port/assets/menus/NON_HANDDRAWN.md b/port/assets/menus/NON_HANDDRAWN.md index f12ff5f2..e5ef361a 100644 --- a/port/assets/menus/NON_HANDDRAWN.md +++ b/port/assets/menus/NON_HANDDRAWN.md @@ -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 155 frames are placeholders. +13 of 156 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. diff --git a/port/assets/menus/ce/bitmaps.xml b/port/assets/menus/ce/bitmaps.xml index 3d6dd799..d83d40d8 100644 --- a/port/assets/menus/ce/bitmaps.xml +++ b/port/assets/menus/ce/bitmaps.xml @@ -318,6 +318,9 @@ + + + diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.lobby.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.lobby.xml index 28338f3a..2d5c5865 100644 --- a/port/assets/menus/ce/main_menu.multiplayer_type_select.lobby.xml +++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.lobby.xml @@ -1,34 +1,93 @@ - + + - - + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -36,6 +95,8 @@ + + @@ -45,10 +106,16 @@ - - + + + + + + + + @@ -64,14 +131,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + @@ -79,7 +178,8 @@ - + + @@ -87,6 +187,11 @@ + + + + + diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.mp_map_select.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.mp_map_select.xml index 3d822835..731abcd3 100644 --- a/port/assets/menus/ce/main_menu.multiplayer_type_select.mp_map_select.xml +++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.mp_map_select.xml @@ -13,6 +13,9 @@ + + + @@ -33,7 +36,9 @@ - + + + diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.server_settings.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.server_settings.xml index d6ef6d9e..456615fb 100644 --- a/port/assets/menus/ce/main_menu.multiplayer_type_select.server_settings.xml +++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.server_settings.xml @@ -18,6 +18,10 @@ + + + + @@ -74,6 +78,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -159,6 +207,10 @@ + + + + @@ -173,5 +225,14 @@ + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.new_select.port.xml b/port/assets/menus/ce/main_menu.new_select.port.xml new file mode 100644 index 00000000..df781299 --- /dev/null +++ b/port/assets/menus/ce/main_menu.new_select.port.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml index b66f0660..d8aa1c70 100644 --- a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml @@ -14,13 +14,15 @@ - - - - - - - + + + + + + + + + @@ -37,20 +39,38 @@ - - - + + + - + - + + + + + + + + + + + + + + + + + + + - + @@ -59,7 +79,7 @@ - + @@ -68,7 +88,7 @@ - + @@ -77,7 +97,7 @@ - + @@ -86,7 +106,7 @@ - + @@ -95,7 +115,7 @@ - + @@ -112,7 +132,9 @@ + + @@ -122,8 +144,10 @@ - - + + + + diff --git a/port/assets/menus/ce/main_menu.solo_level_select.xml b/port/assets/menus/ce/main_menu.solo_level_select.xml index 3da98277..0ab7778d 100644 --- a/port/assets/menus/ce/main_menu.solo_level_select.xml +++ b/port/assets/menus/ce/main_menu.solo_level_select.xml @@ -14,6 +14,9 @@ + + + @@ -38,7 +41,7 @@ - + diff --git a/port/assets/menus/ce/port/main_menu/multiplayer_type_select/lobby/header_add_player.png b/port/assets/menus/ce/port/main_menu/multiplayer_type_select/lobby/header_add_player.png new file mode 100644 index 00000000..9fc7d40c Binary files /dev/null and b/port/assets/menus/ce/port/main_menu/multiplayer_type_select/lobby/header_add_player.png differ diff --git a/port/assets/menus/menus.json b/port/assets/menus/menus.json index 9b734a6e..f98a53ed 100644 --- a/port/assets/menus/menus.json +++ b/port/assets/menus/menus.json @@ -17,6 +17,7 @@ "ce/main_menu.multiplayer_type_select.mp_map_select.xml", "ce/main_menu.multiplayer_type_select.server_settings.xml", "ce/main_menu.multiplayer_type_select.xml", + "ce/main_menu.new_select.port.xml", "ce/main_menu.new_select.xml", "ce/main_menu.player_profiles_select.xml", "ce/main_menu.profile_manager.xml", @@ -54,6 +55,7 @@ "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_add_player.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", diff --git a/port/assets/menus/port_svg/main_menu/multiplayer_type_select/lobby/header_add_player.svg b/port/assets/menus/port_svg/main_menu/multiplayer_type_select/lobby/header_add_player.svg new file mode 100644 index 00000000..7966bbc0 --- /dev/null +++ b/port/assets/menus/port_svg/main_menu/multiplayer_type_select/lobby/header_add_player.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/port/assets/network/brokers.txt b/port/assets/network/brokers.txt new file mode 100644 index 00000000..f583f8d7 --- /dev/null +++ b/port/assets/network/brokers.txt @@ -0,0 +1,10 @@ +# The public MQTT brokers of internet play: the machines of an invite find +# each other through them, and the server browser's listings go through +# them. The game uses every one at once (up to 4), so any one working is +# enough. One host:port on each line; "#" starts a comment. +# (The game reads this file beside config.toml: network.brokers_file. An +# update replaces it; keep a list of your own under another name.) +opence.milenko.org:1883 +broker.emqx.io:1883 +broker.hivemq.com:1883 +test.mosquitto.org:1883 diff --git a/port/linux/NETCODE.md b/port/linux/NETCODE.md index 2c3913a7..df63df06 100644 --- a/port/linux/NETCODE.md +++ b/port/linux/NETCODE.md @@ -21,6 +21,64 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick: vehicle it drives) from its local input at once. Remote players are driven by the inputs the host relays every tick, the latest one held until a newer arrives. +- **Actors driven.** Only the host runs the AI. Each tick it sends its + clients the control its actors gave their units (how they move, where + they face, aim and look, their trigger and buttons, their animation + impulses) with the units' state, those near a client's players every + tick and others less often; a client drives each unit with the latest + it has, as it drives a remote player's, until it hears nothing of it for + two seconds (`port/linux/game/network_actors.c`). +- **Co-op.** A network game on a campaign level with no game engine + (Create Game's Map screen, its SINGLEPLAYER maps, over LAN and the + internet) is co-op. Only the host runs the + level's scripts and spawns players. `network_coop.c` sends the clients + everything the scripts do that they would otherwise miss: + - every tick: the cinematic, camera, screen fade, the HUD settings the + scripts control (what is shown, the mission timer), the skip vote, and + which teams are allies and friends (the scripts' allegiances, so the + marines are the players' allies on every machine); + - once each, numbered so nothing is applied twice: script sounds, + chapter titles, help and objective text, "Checkpoint" messages, screen + shake, nav points, custom animations on units and scenery, and units + opening and closing (dropships' doors); + - device groups (doors, elevators, switches; a client sets none itself, + and its player's use of one is relayed to the host), and each device's + position and power as it changes: a client puts its device where the + host's is once that stops, or if they drift apart while it moves; + - which named objects exist, so scripted creates and deletes match. + + Every machine follows the host's structure BSP: a switch is sent at once + and reliably, and each client's input says which BSP it has loaded. + Until a client has the host's, the host takes none of its players' + movement and none of its loading zones, and after any switch no loading + zone switches again until every machine has the new BSP (ten seconds at + most). A loading zone into a + BSP the team hasn't been in brings every player to whoever crossed it, + as split screen does; one back into a BSP it has been in switches only + with two thirds of the living players at it (in the trigger, or within + 4 world units of the player in it), so one player can't drag the team + back through the level. A player outside the loaded BSP and falling for + two seconds is brought back beside a teammate. A dead player watches a + living teammate (`coop_spectate.c`) and comes back beside one once it is + safe. With everyone dead they come back where they were at the last + checkpoint, since only the host could revert. A level won ends the round + as in multiplayer, and the next round is the campaign's next level. + + Cutscenes are skipped by vote: more than half the machines must press + skip. The host then reverts as single player does, but keeps its clock + moving forward (the netcode depends on that) and moves the script + threads' wake times along with it. The object, device and name syncs + bring the clients up to date. + + The host's EXTRA ENEMIES (`coop_enemies.c`, `network.coop_enemies_mode`) + give each squad of enemies a level places more of itself: PER PLAYER, a + percentage of itself for each player past the first; STATIC MULTIPLIER, + that many times itself for any number of players. They stand in rings + about its starting locations where the ground is open, and never take the + actors a level needs for its own (the actor pool, `halo_port_capacity.h`, + holds 1024). Riders a dropship has no seats for are kept, and placed + beside its riders once they get out. Only the host runs the AI, so the + clients see them as the host's other actors. - **Host authoritative.** The host alone decides damage, deaths, spawns, pickups, scores and the game's objects; clients do not decide them but apply what the host sends. @@ -93,7 +151,15 @@ is dead; version 8 is the first whose clients play by the host's rules (below), so a build without them joins no host of it; version 9 tells every machine of a player the host dropped for cheating, each client tells the host its Discord user, and a machine's join request carries its -hardware id; version 10 sends every player's ping for the scoreboard. +hardware id; version 10 sends every player's ping for the scoreboard; +version 11 sends with the game's settings its gametype's PC options; +version 12 plays the campaign together (co-op, above), drives the host's +actors on its clients and sends the flinches and deaths the host picked; +version 13 drives up to 1056 of the host's AI units on its clients (co-op's +extra enemies), where 12 drove 288; version 14 sends co-op's device positions +and its units opening and closing; version 15 sends co-op's allegiances with +its presentation, a message of another size; version 16 has a client's input +say which structure BSP it has loaded (co-op). A client plays by its host's rules: in another's game (searching for it, in its lobby, or playing it) the developer console, the telnet console @@ -138,7 +204,9 @@ added to `bans.txt` beside `debug.txt` (a line each, as in `cheaters.txt`, with `ip=` and `hwid=`): the host refuses a machine joining whose address or hardware id is in it (a line taken out unbans). Both are as the player's machine tells them: anyone with administrator or root access can change -them, and players behind one address share it. +them, and players behind one address share it. The console's `kick ` drops a player as `ban` does (every machine told), but adds no line +and keeps no address out: the player may join again at once. A speed hack of less than a tenth is let be: the host's bounds on how far and how fast a client's player moves and fires hold it to the host's time anyway. @@ -282,8 +350,10 @@ a pregame keep-alive every five seconds from the host host's latest tick it had heard of when it made the report. - The host deals a report once it has checked it: from that machine's player; damage one of their weapons (a vehicle's a driver's or - gunner's; now or in the last ten seconds), their grenades (while they - have them, and for a while after) or the vehicle they drove (in the + gunner's; now or in the last ten seconds), a grenade the host's own game + saw them throw in the last ten seconds that has not gone off (each + throw's explosion is taken once, its other hits that tick with it: + holding grenades deals nothing) or the vehicle they drove (in the last ten seconds: its collisions) can deal (its projectiles' impacts and detonations, followed through the tags), no harder than it can be (all of it, but an airborne melee blow's half again); of the shape the diff --git a/port/linux/README.md b/port/linux/README.md index 832bafae..bfc82b4b 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -83,7 +83,8 @@ These files are in the data root: | `init.txt` | Console commands that the game does at start-up. For example, `map_name levels\a10\a10` starts the first campaign level. | The settings are in `config.toml` next to the executable. Refer to -"Settings". +"Settings". Internet play's MQTT brokers are in `brokers.txt` next to it +(`network.brokers_file`). If the game stops because of a fatal signal, it writes the address and a backtrace to the standard error. To find the function at the address, enter @@ -95,9 +96,11 @@ 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. 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 operate controllers 2 to 4. With two or more players on this +computer (co-op, or split screen in a network game) and only one gamepad, +that gamepad is controller 2 (player 2) and the keyboard and mouse stay +controller 1. The gamepad changes controller only when none of its buttons +is held. The profile's button layout (Settings > Gamepads) is the gamepads' only. | Action | Keys and buttons (default) | @@ -163,9 +166,22 @@ 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 +New Game's levels are those either profile has reached. Either player's +controller chooses the level and the difficulty. A co-op game does not continue a saved game of one player. +Network games have split screen too: up to 4 players on each computer. In +the game lobby, another controller presses START to join, and the new +player's profile is chosen on the ADD PLAYER screen that opens (with any +controller). With one gamepad, choose the lobby's ADD PLAYER button first: +until then that gamepad shares controller 1 with the keyboard. Two players on one profile get different names from the host. A +player's B in the lobby leaves the game alone, and the last player of the +computer leaves it for all of them. In the game, each player's pause menu +opens on their part of the screen, and its LEAVE GAME is theirs: their part +of the screen stays until the game ends. A game under way shows its own +screen before JOIN GAME: players join there the same way, START or ADD +PLAYER then START, and JOIN GAME brings them all into the game. + 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 @@ -204,16 +220,19 @@ the setting for one start of the game. It has priority over the file. | Setting | Default | Environment variable | Function | | --- | --- | --- | --- | -| `display.mode` | `""` | `HALO_DISPLAY_MODE` | `"fullscreen"`: the display, taken at its desktop resolution. `"borderless"`: a window over the whole desktop. Both draw at the resolution of the display, the picture 480 lines of the game and the width of the display. `"windowed"`: a window with the 640x480 picture of the Xbox, scaled. Empty: `display.fullscreen` decides (`true`: borderless). F11 changes between the window and the fullscreen mode. Video Setup sets it. | -| `display.fullscreen` | `true` | `HALO_FULLSCREEN` | `true`: fullscreen at the resolution of the display. The picture has 480 lines of the game and the width of the display. `false`: a window with the 640x480 picture of the Xbox. Used when `display.mode` is empty. | -| `display.window_scale` | `2` | `HALO_WINDOW_SCALE` | The size of the window, as a multiple of 640x480. You can change the size of the window. | +| `display.mode` | `""` | `HALO_DISPLAY_MODE` | `"fullscreen"`: the display, taken at the mode of `display.resolution` (the nearest the display has), or at its desktop mode. `"borderless"`: a window over the whole desktop, whose mode does not change. `"windowed"`: a window of `display.window_size`. Empty: `display.fullscreen` decides (`true`: borderless). F11 changes between the window and the fullscreen mode. Video Setup sets it. | +| `display.fullscreen` | `true` | `HALO_FULLSCREEN` | `true`: borderless, as `display.mode = "borderless"`. `false`: a window, as `display.mode = "windowed"`. Used when `display.mode` is empty. | +| `display.resolution` | `"native"` | `HALO_RESOLUTION` | What fullscreen and borderless draw at: `"native"`, the display's own resolution, or `"x"`, such as `"1920x1080"`, 640x480 or more. Fullscreen sets the display to it. Borderless draws at it and scales the picture to the display, where the display has room for it. The picture has 480 lines of the game and the width of the resolution's shape. A window draws at its own size instead. Video Setup's Resolution sets it, from the display's modes; it shows with Fullscreen and Borderless. | +| `display.resolution_scaling` | `"native"` | `HALO_RESOLUTION_SCALING` | `"native"`: the game draws at the resolution of the window, or of the display (or `display.resolution`) fullscreen. `"original"`: the game draws the 640x480 picture of the Xbox and scales it up to the window or the display, whatever `display.mode` is. Video Setup sets it. | +| `display.window_size` | `""` | `HALO_WINDOW_SIZE` | The size of the window, as `"x"`, such as `"1920x1080"`, 640x480 or more. You can change the size of the window; the game's picture takes its shape. Empty: `display.window_scale` decides. Video Setup's Window Size sets it, from sizes of each shape (4:3, 16:10, 16:9 and 21:9) that fit the desktop; it shows with Windowed. | +| `display.window_scale` | `2` | `HALO_WINDOW_SCALE` | Used when `display.window_size` is empty: the size of the window, as a multiple of 640x480. | | `display.vsync` | `true` | `HALO_NO_VSYNC=1` sets `false` | `true`: each frame waits for the display. | | `display.max_fps` | `0` | `HALO_MAX_FPS` | With vsync off, the most frames each second. `0`: twice the display's refresh rate. `-1`: no limit, which can hang some Intel graphics (Raptor Lake), resetting the desktop's graphics too. | | `debug.gpu_flush_draws` | `-1` | `HALO_GPU_FLUSH_DRAWS` | Flush the GPU's pipeline every this many draws. `-1`: every 3 on Intel graphics with Mesa's driver, which can otherwise hang in the game's long runs of small draws and reset the desktop's graphics too. `0`: never. | | `display.interpolation` | `true` | `HALO_INTERPOLATION` | `true`: one frame for each refresh of the display. `false`: 30 frames each second, as on the Xbox. Refer to "Frame rate". | | `display.direct_camera` | `true` | `HALO_DIRECT_CAMERA` | `true`: in first person, on foot, the view points where the player aims in each frame, not where the last tick left it. Refer to "Frame rate". | | `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.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 resolution the game draws at, 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 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. | @@ -241,7 +260,12 @@ the setting for one start of the game. It has priority over the file. | `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.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.coop_friendly_fire` | `"on"` | `HALO_NET_COOP_FRIENDLY_FIRE` | Whether the players of an online co-op game hurt each other: `"off"`, `"on"`, `"shields_only"` or `"explosives_only"`. FRIENDLY FIRE in co-op's Server Setup writes its choice here. Their AI allies they always can, as in the campaign. | +| `network.coop_enemies_mode` | `"per_player"` | `HALO_NET_COOP_ENEMIES_MODE` | Online co-op's extra enemies: `"none"`; `"per_player"`, each squad of enemies that a level places grows by `network.coop_enemies` for each player past the first; or `"multiplier"`, each squad is `network.coop_enemies_multiplier` times as large, for any number of players. The extra enemies stand around the squad's places, and those that a dropship has no seats for drop out of it after its passengers. EXTRA ENEMIES in co-op's Server Setup writes its choice here. | +| `network.coop_enemies` | `50` | `HALO_NET_COOP_ENEMIES` | The extra enemies per player, a percentage from `25` to `200`: for each player past the first, each squad of enemies gets this much of itself more (`100`: as many again, so four players meet four times the squad). PER PLAYER in co-op's Server Setup writes its choice here. | +| `network.coop_enemies_multiplier` | `2` | `HALO_NET_COOP_ENEMIES_MULTIPLIER` | The static multiplier of the enemies, `2` to `32`: each squad of enemies is this many times as large. MULTIPLIER in co-op's Server Setup writes its choice here. | +| `network.coop_public` | `false` | `HALO_NET_COOP_PUBLIC` | `true`: an online co-op game (Create Game > Internet, a SINGLEPLAYER map) starts as PUBLIC. `false`: it starts as PRIVATE. LISTING in co-op's Server Setup writes its choice here. Refer to "Server browser". | +| `network.brokers_file` | `"brokers.txt"` | `HALO_NET_BROKERS_FILE` | The file of the public MQTT brokers that let the machines of an invite find each other, and that carry the listings of the server browser: next to `config.toml`, unless a full path. One `host:port` on each line, up to 4; `#` starts a comment. | | `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. | @@ -433,7 +457,8 @@ Server Setup in Create Game > Internet has a LISTING row: - 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. +games start as PRIVATE). An online co-op game starts as PRIVATE, and keeps +the last choice of its LISTING (`network.coop_public`). 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. @@ -444,7 +469,7 @@ 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, + same MQTT brokers as the invites (`network.brokers_file`), retained, to a topic of the host (`hceu/3/lobby/s/`). - 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 @@ -466,8 +491,13 @@ 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 +The brokers are in `brokers.txt` next to the executable (from +`port/assets/network/brokers.txt`; on Android, the app writes it next to +`config.toml` at each start), one `host:port` on each line. The game uses +all of them at once (up to 4), so one that works is enough. An update +replaces `brokers.txt`: to use brokers of your own, put them in another +file and name it in `network.brokers_file`. 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. @@ -484,7 +514,7 @@ Only machines with the invite can find the game: random 16-byte token. The identifier of the host is from the first 6 bytes of the hash. - The machines exchange their public keys and addresses through public MQTT - brokers (`network.signalling_brokers`). The topics are HMACs of the token. + brokers (`network.brokers_file`). The topics are HMACs of the token. A key from the token encrypts and authenticates the messages (`src/p2p_signal.c`, `src/p2p_crypto.c`). The host authenticates its answer with a key that only it and the player can calculate. Its public key must @@ -524,7 +554,9 @@ Only machines with the invite can find the game: `bans.txt`, and refuses a machine whose address or hardware id is in it. - The host can ban a player with `ban ` in the developer console (Tab completes the name). Remove a line from `bans.txt` to unban. - Refer to `NETCODE.md`. So that every player can be named, the host trims + Refer to `NETCODE.md`. `kick ` drops the player the same + way, but keeps nothing: no line in `bans.txt`, and the player can join + again at once. So that every player can be named, the host trims the spaces around a name and removes characters that draw as nothing. A letter with a mark is typed as the plain letter (`ban jose` for "José"). A name with nothing left to type becomes "Player", and a name that another @@ -594,7 +626,7 @@ The game sends the activity only to a Discord client of the same user. | Game code | All 466 C files of the game. The changes are in "Game source changes". | | Graphics | Direct3D 8 on OpenGL 4.5 core through SDL3 (`src/d3d8_gl.c`). The port translates the NV2A vertex shaders and register combiners to GLSL. It decodes all the Xbox texture formats. The vertex and index buffers come from a GL copy of the Xbox memory. | | High-res HUD | The HUD is drawn from high-res assets: redraws at 8x the size of the maps' bitmaps (4x for the largest), in `port/assets/hud`. They cover the meters, counters, panels and their outlines, the motion sensor, reticles, waypoints and scopes, but no bitmap with English text. `tools/hud_assets.py` makes them from the SVG redraws, and the build embeds them in the executable. When the game uploads one of those bitmaps, `src/hud_hires.c` gives the high-res texture in its place, if the bitmap's pixels are those of the English maps: another language's maps keep their own. The game sizes and places the HUD from its tags as before. `display.high_res_hud = false` turns this off. | -| High-res text | The menus' and HUD's text is drawn with Overpass (`port/assets/fonts`, SIL Open Font License) in place of the maps' bitmap fonts, which are Interstate. `src/text_hires.c` rasterizes each glyph with stb_truetype (`port/third_party/stb`) at the display's resolution, into an atlas that a placeholder bitmap of the game stands for. The game lays the text out from its font tags as before. The menus' titles (the screens' headers and the main menu's items) are pictures of text in the maps, so they are drawn as the high-res HUD is: `tools/title_assets.py` sets each one again in OpenCE, Roger White's public-domain Newtown respaced to match the maps' commercial title typeface (`tools/title_font.py`), at 4x the bitmap's size over its own plate or glow, each letter placed where the map's letter is, in `port/assets/titles`. The postgame carnage report's title is set over a hand-made SVG redraw of its panel (`port/assets/titles/svg`) instead. `display.high_res_text = false` turns it off. | +| High-res text | The menus' and HUD's text is drawn with Overpass (`port/assets/fonts`, SIL Open Font License) in place of the maps' bitmap fonts, which are Interstate. `src/text_hires.c` rasterizes each glyph with stb_truetype (`port/third_party/stb`) at the resolution the game draws at, into an atlas that a placeholder bitmap of the game stands for. The game lays the text out from its font tags as before. The menus' titles (the screens' headers and the main menu's items) are pictures of text in the maps, so they are drawn as the high-res HUD is: `tools/title_assets.py` sets each one again in OpenCE, Roger White's public-domain Newtown respaced to match the maps' commercial title typeface (`tools/title_font.py`), at 4x the bitmap's size over its own plate or glow, each letter placed where the map's letter is, in `port/assets/titles`. The postgame carnage report's title is set over a hand-made SVG redraw of its panel (`port/assets/titles/svg`) instead. `display.high_res_text = false` turns it off. | | Sound | Xbox DirectSound on SDL3 audio (`src/dsound_sdl.c`): PCM and Xbox ADPCM, mixed at 48 kHz, with volume, pitch, mix bins, distance, stereo pan, occlusion and obstruction. There is no Doppler effect, no cones and no reverb. | | Input | XInput on SDL3 (`src/xinput_sdl.c`): keyboard, mouse, gamepads with rumble, and the debug keyboard for the console. | | Files | The Win32 file functions and the MSVC file functions on POSIX, with the translation of Xbox paths. | @@ -680,11 +712,11 @@ 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; the main menu and Multiplayer clear co-op's controllers, so that a gamepad going back to controller 1 is not a controller unplugged. | +| `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; the lobby's split screen players leave alone, and one who quits in game is not joined to the next game (`interface/player_ui.c`). | | `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`). | -| `rasterizer/rasterizer_text.c`, `text/draw_string.c` | Text is drawn from an atlas of the fonts' glyphs, rasterized at the display's resolution (`src/text_hires.c`), when the font has every character of the string. Text can be drawn scaled about a point (`rasterizer_text_set_scale`), as the players' names are. Each glyph's advance is centred on the font tag character's, so the layout is the same, and a glyph is cut at a text box only where the font tag's character visibly was. | +| `rasterizer/rasterizer_text.c`, `text/draw_string.c` | Text is drawn from an atlas of the fonts' glyphs, rasterized at the resolution the game draws at (`src/text_hires.c`), when the font has every character of the string. Text can be drawn scaled about a point (`rasterizer_text_set_scale`), as the players' names are. Each glyph's advance is centred on the font tag character's, so the layout is the same, and a glyph is cut at a text box only where the font tag's character visibly was. | The x86 inline assembly of the game is replaced by C. Thus the compiler can optimize that code for each processor: diff --git a/port/linux/game/coop_enemies.c b/port/linux/game/coop_enemies.c new file mode 100644 index 00000000..ade5c80f --- /dev/null +++ b/port/linux/game/coop_enemies.c @@ -0,0 +1,523 @@ +/* +COOP_ENEMIES.C + +Network co-op's extra enemies: Server Setup's EXTRA ENEMIES +(network.coop_enemies_mode). PER PLAYER (network.coop_enemies, a +percentage): for each player past the first, each squad of the players' +enemies that a level places (encounters.c's encounter_create) gets that +much of its count more; at 100%, two players meet twice the squad, four +players four times it. STATIC MULTIPLIER (network.coop_enemies_multiplier): +each squad is that many times as large, for any number of players. The +extra enemies stand in rings about the squad's starting locations, on +open ground, so they do not start inside each other. + +A squad a script loads into a dropship (vehicle_load_magic) can be larger +than the dropship's seats. The riders left without a seat are erased, and +kept: once the dropship's own riders get out (vehicle_unload, or any other +way), they are placed beside those riders, a few ticks apart, and drop and +fight as they do. A dropship gone first takes them with it. + +Only the host runs the AI, so all of this is the host's. +*/ + +#include "cseries.h" +#include "ai/actor_placement.h" +#include "ai/actors.h" +#include "ai/ai_scenario_definitions.h" +#include "ai/encounters.h" +#include "game/game.h" +#include "game/game_allegiance.h" +#include "game/players.h" +#include "networking/network_game_globals.h" +#include "networking/network_game_manager.h" +#include "objects/objects.h" +#include "physics/collisions.h" +#include "scenario/scenario.h" +#include "scenario/scenario_definitions.h" +#include "units/units.h" +#include "units/unit_definitions.h" + +#include "coop_enemies.h" +#include "network_coop.h" + +#include +#include + +/* port_config.c's */ +long config_integer(char const *name); +char const *config_string(char const *name); + +/* ---------- constants */ + +enum +{ + /* the actors a level keeps for its own placements, which the extra + enemies never take: as many as the Xbox's whole pool */ + COOP_ENEMIES_LEVEL_ACTORS = 256, + /* EXTRA ENEMIES' amounts, as Server Setup has them: 25 to 200% per + player, or 2 to 32 times (none is its NONE) */ + COOP_ENEMIES_MINIMUM_PERCENT = 25, + COOP_ENEMIES_MAXIMUM_PERCENT = 200, + COOP_ENEMIES_MINIMUM_MULTIPLIER = 2, + COOP_ENEMIES_MAXIMUM_MULTIPLIER = 32, + /* the places tried for an extra enemy, a ring further out each */ + SPREAD_ATTEMPTS = 8, + SPREAD_PLACES_PER_RING = 6, + MAXIMUM_RIDING_VEHICLES = 32, + MAXIMUM_SEATED_RIDERS = 16, + MAXIMUM_KEPT_RIDERS = 512, + /* a kept rider placed every this many ticks, as the seated ones + jumped out */ + RIDER_RELEASE_TICKS = 4, +}; + +/* the rings' spacing, and the height over the ground the way to a place is +looked along, and the ground looked for below it */ +#define SPREAD_SPACING 0.8f +#define SPREAD_STEP_HEIGHT 0.6f +#define SPREAD_GROUND_DEPTH 2.0f +/* a kept rider's place beside a rider who got out, each a little apart */ +#define RIDER_SPACING 0.5f + +/* EXTRA ENEMIES' choices (network.coop_enemies_mode's values) */ +enum +{ + _coop_enemies_none, + _coop_enemies_per_player, + _coop_enemies_multiplier, +}; + +/* ---------- structures */ + +/* a vehicle a script loaded riders into: its seated ones, and whether they +have begun to get out */ +struct coop_riding_vehicle +{ + long vehicle_index; + long seated_unit_indices[MAXIMUM_SEATED_RIDERS]; + short seat_indices[MAXIMUM_SEATED_RIDERS]; + short seated_count; + boolean releasing; + short released_count; + long next_release_time; +}; + +/* a rider left without a seat, to place once its vehicle's get out */ +struct coop_kept_rider +{ + long vehicle_index; + long variant_definition_index; + long encounter_index; + short squad_index; +}; + +/* ---------- globals */ + +static struct +{ + /* EXTRA ENEMIES, as the game began with it: its choice, and the amount + of PER PLAYER (a percentage) and of STATIC MULTIPLIER */ + short mode; + short percent; + short multiplier; + struct coop_riding_vehicle vehicles[MAXIMUM_RIDING_VEHICLES]; + short vehicle_count; + struct coop_kept_rider riders[MAXIMUM_KEPT_RIDERS]; + short rider_count; +} coop_enemies; + +/* ---------- private code */ + +static boolean coop_enemies_host( + void) +{ + return game_connection() == _game_connection_network_server && network_coop_active(); +} + +/* whether this game has extra enemies (it is a co-op host's, with some +chosen) */ +static boolean coop_enemies_on( + void) +{ + return coop_enemies.mode != _coop_enemies_none && coop_enemies_host(); +} + +/* the players in the game, every machine's: the network game's (a level +places its first enemies before the players' units are made, as it loads: +game_initialize_for_new_map) */ +static short coop_enemies_player_count( + void) +{ + struct network_game *game = network_game_get_game(); + short count = 0; + short index; + + for (index = 0; game && index < HALO_PORT_MAXIMUM_NETWORK_PLAYERS; index++) + { + if (network_player_is_valid(&game->players[index])) + count++; + } + return count; +} + +/* whether the way from a to b is open (no structure between them) */ +static boolean coop_enemies_open( + real_point3d const *a, + real_point3d const *b) +{ + struct collision_result collision; + real_vector3d vector; + + vector.i = b->x - a->x; + vector.j = b->y - a->y; + vector.k = b->z - a->z; + return !collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit) | + FLAG(_collision_test_back_facing_surfaces_bit), a, &vector, NONE, &collision); +} + +/* the ground below a point, within SPREAD_GROUND_DEPTH of it */ +static boolean coop_enemies_ground( + real_point3d const *point, + real_point3d *ground) +{ + struct collision_result collision; + real_vector3d vector = { 0.0f, 0.0f, -(SPREAD_STEP_HEIGHT + SPREAD_GROUND_DEPTH) }; + + if (!collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit), + point, &vector, NONE, &collision)) + { + return FALSE; + } + *ground = collision.point; + return TRUE; +} + +static struct coop_riding_vehicle *coop_enemies_riding_vehicle( + long vehicle_index, + boolean create) +{ + short index; + + for (index = 0; index < coop_enemies.vehicle_count; index++) + { + if (coop_enemies.vehicles[index].vehicle_index == vehicle_index) + return &coop_enemies.vehicles[index]; + } + if (!create || coop_enemies.vehicle_count >= MAXIMUM_RIDING_VEHICLES) + return NULL; + csmemset(&coop_enemies.vehicles[coop_enemies.vehicle_count], 0, sizeof(coop_enemies.vehicles[0])); + coop_enemies.vehicles[coop_enemies.vehicle_count].vehicle_index = vehicle_index; + return &coop_enemies.vehicles[coop_enemies.vehicle_count++]; +} + +/* the riders kept for a vehicle forgotten, and the vehicle with them */ +static void coop_enemies_forget_vehicle( + short vehicle_number) +{ + long vehicle_index = coop_enemies.vehicles[vehicle_number].vehicle_index; + short index, kept = 0; + + for (index = 0; index < coop_enemies.rider_count; index++) + { + if (coop_enemies.riders[index].vehicle_index != vehicle_index) + coop_enemies.riders[kept++] = coop_enemies.riders[index]; + } + coop_enemies.rider_count = kept; + coop_enemies.vehicles[vehicle_number] = coop_enemies.vehicles[--coop_enemies.vehicle_count]; +} + +/* whether a vehicle's seated riders have begun to get out: one alive and +out of it (one killed in its seat is not getting out) */ +static boolean coop_enemies_riders_getting_out( + struct coop_riding_vehicle const *vehicle) +{ + short index; + + for (index = 0; index < vehicle->seated_count; index++) + { + struct object_datum *unit = object_try_and_get_and_verify_type(vehicle->seated_unit_indices[index], + _object_mask_unit); + + if (unit && !TEST_FLAG(unit->object.damage_flags, _object_dead_bit) && + unit->object.parent_object_index != vehicle->vehicle_index) + { + return TRUE; + } + } + return FALSE; +} + +/* where a vehicle's `number`th kept rider is placed: beside one of the +riders who got out of it (where it is now: out of the vehicle, in the +open), a ring further about them each round; else at a seat's marker, else +the vehicle's place */ +static void coop_enemies_rider_place( + struct coop_riding_vehicle const *vehicle, + short number, + real_point3d *position, + real *facing) +{ + struct object_datum *object = object_get(vehicle->vehicle_index); + struct unit_definition *definition = unit_definition_get(object->definition_index); + long out_unit_indices[MAXIMUM_SEATED_RIDERS]; + short out_count = 0; + short index, round; + real angle; + + *position = object->object.position; + *facing = (real)atan2(object->object.forward.j, object->object.forward.i); + for (index = 0; index < vehicle->seated_count; index++) + { + struct object_datum *unit = object_try_and_get_and_verify_type(vehicle->seated_unit_indices[index], + _object_mask_unit); + + if (unit && !TEST_FLAG(unit->object.damage_flags, _object_dead_bit) && + unit->object.parent_object_index != vehicle->vehicle_index) + { + out_unit_indices[out_count++] = vehicle->seated_unit_indices[index]; + } + } + if (out_count > 0) + { + *position = object_get(out_unit_indices[number % out_count])->object.position; + round = (short)(number / out_count); + } + else + { + struct object_marker marker; + + round = number; + if (vehicle->seated_count > 0 && VALID_INDEX(vehicle->seat_indices[0], definition->unit.seats.count)) + { + struct unit_seat *seat = TAG_BLOCK_GET_ELEMENT(&definition->unit.seats, vehicle->seat_indices[0], + struct unit_seat); + + if (seat->marker_name[0] && + object_get_marker_by_name(vehicle->vehicle_index, seat->marker_name, &marker, 1) > 0) + { + *position = marker.matrix.position; + } + } + } + /* (never on the rider's own place: a ring about it, further each round) */ + angle = (real)number * (_pi * 2.0f / SPREAD_PLACES_PER_RING); + position->x += (real)cos(angle) * RIDER_SPACING * (real)(round / SPREAD_PLACES_PER_RING + 1); + position->y += (real)sin(angle) * RIDER_SPACING * (real)(round / SPREAD_PLACES_PER_RING + 1); +} + +/* ---------- public code */ + +void coop_enemies_new_game( + void) +{ + char const *mode = config_string("network.coop_enemies_mode"); + + csmemset(&coop_enemies, 0, sizeof(coop_enemies)); + /* (an amount past Server Setup's, as it shows it: its nearest choice's + end) */ + coop_enemies.percent = (short)PIN(config_integer("network.coop_enemies"), COOP_ENEMIES_MINIMUM_PERCENT, + COOP_ENEMIES_MAXIMUM_PERCENT); + coop_enemies.multiplier = (short)PIN(config_integer("network.coop_enemies_multiplier"), + COOP_ENEMIES_MINIMUM_MULTIPLIER, COOP_ENEMIES_MAXIMUM_MULTIPLIER); + coop_enemies.mode = !strcmp(mode, "per_player") ? _coop_enemies_per_player : + !strcmp(mode, "multiplier") ? _coop_enemies_multiplier : _coop_enemies_none; +} + +void coop_enemies_reset( + void) +{ + coop_enemies.vehicle_count = 0; + coop_enemies.rider_count = 0; +} + +short coop_enemies_extra_count( + long encounter_index, + short count) +{ + struct encounter_datum *encounter; + long extra, room; + short players; + + if (count <= 0 || !coop_enemies_on()) + return 0; + encounter = encounter_get(encounter_index); + if (!game_team_is_enemy(_game_team_player, encounter->team_index)) + return 0; + if (coop_enemies.mode == _coop_enemies_multiplier) + extra = (long)count * (coop_enemies.multiplier - 1); + else + { + players = coop_enemies_player_count(); + if (players <= 1) + return 0; + extra = ((long)count * coop_enemies.percent * (players - 1) + 50) / 100; + } + room = MAXIMUM_ACTORS - COOP_ENEMIES_LEVEL_ACTORS - actor_data->actual_count; + return (short)PIN(extra, 0, MAX(room, 0)); +} + +boolean coop_enemies_spread_position( + real_point3d const *origin, + short number, + real_point3d *position) +{ + real_point3d from = *origin; + short place = (short)((number - 1) % SPREAD_PLACES_PER_RING); + short ring = (short)((number - 1) / SPREAD_PLACES_PER_RING); + short attempt; + + from.z += SPREAD_STEP_HEIGHT; + for (attempt = 0; attempt < SPREAD_ATTEMPTS; attempt++, ring++) + { + /* (each ring turned half a place from the last, so its places fall + between the last's) */ + real angle = ((real)place + 0.5f * (real)(ring % 2)) * (_pi * 2.0f / SPREAD_PLACES_PER_RING); + real radius = SPREAD_SPACING * (real)(ring + 1); + real_point3d to = from; + + to.x += (real)cos(angle) * radius; + to.y += (real)sin(angle) * radius; + if (coop_enemies_open(&from, &to) && coop_enemies_ground(&to, position)) + return TRUE; + } + return FALSE; +} + +void coop_enemies_rider_seated( + long vehicle_index, + long unit_index) +{ + struct coop_riding_vehicle *vehicle; + struct unit_datum *unit; + + if (!coop_enemies_on()) + return; + vehicle = coop_enemies_riding_vehicle(vehicle_index, TRUE); + unit = unit_get(unit_index); + if (!vehicle) + return; + /* (a vehicle loaded again: the riders that got out of it before are not + this load's) */ + { + short index, kept = 0; + + for (index = 0; index < vehicle->seated_count; index++) + { + struct object_datum *seated = object_try_and_get_and_verify_type(vehicle->seated_unit_indices[index], + _object_mask_unit); + + if (seated && seated->object.parent_object_index == vehicle_index) + { + vehicle->seated_unit_indices[kept] = vehicle->seated_unit_indices[index]; + vehicle->seat_indices[kept++] = vehicle->seat_indices[index]; + } + } + vehicle->seated_count = kept; + } + if (vehicle->seated_count >= MAXIMUM_SEATED_RIDERS) + return; + vehicle->seated_unit_indices[vehicle->seated_count] = unit_index; + vehicle->seat_indices[vehicle->seated_count] = unit->unit.parent_seat_index; + vehicle->seated_count++; + /* (a vehicle loaded again, after its riders got out: they get out again) */ + vehicle->releasing = FALSE; +} + +boolean coop_enemies_rider_unseated( + long vehicle_index, + long unit_index) +{ + struct unit_datum *unit; + struct actor_datum *actor; + struct coop_kept_rider *rider; + struct coop_riding_vehicle *vehicle; + + if (!coop_enemies_on() || coop_enemies.rider_count >= MAXIMUM_KEPT_RIDERS) + return FALSE; + unit = unit_get(unit_index); + vehicle = coop_enemies_riding_vehicle(vehicle_index, FALSE); + /* (kept only for a vehicle with riders seated, whose getting out places + it: one of a load that seated nobody is left where it is) */ + if (unit->object.type != _object_type_biped || unit->unit.actor_index == NONE || !vehicle || + vehicle->seated_count == 0) + { + return FALSE; + } + actor = actor_get(unit->unit.actor_index); + if (actor->meta.swarm || actor->meta.encounter_index == NONE || + !game_team_is_enemy(_game_team_player, encounter_get(actor->meta.encounter_index)->team_index)) + { + return FALSE; + } + rider = &coop_enemies.riders[coop_enemies.rider_count++]; + rider->vehicle_index = vehicle_index; + rider->variant_definition_index = actor->meta.variant_definition_index; + rider->encounter_index = actor->meta.encounter_index; + rider->squad_index = actor->meta.squad_index; + return TRUE; +} + +void coop_enemies_update( + void) +{ + long now = game_time_get(); + short vehicle_number; + + if (!coop_enemies_host()) + return; + for (vehicle_number = coop_enemies.vehicle_count - 1; vehicle_number >= 0; vehicle_number--) + { + struct coop_riding_vehicle *vehicle = &coop_enemies.vehicles[vehicle_number]; + struct object_datum *object = object_try_and_get_and_verify_type(vehicle->vehicle_index, _object_mask_unit); + short index; + + /* (the vehicle's next kept rider) */ + for (index = 0; index < coop_enemies.rider_count; index++) + { + if (coop_enemies.riders[index].vehicle_index == vehicle->vehicle_index) + break; + } + /* (a vehicle gone, or destroyed, takes its kept riders with it; one + with none kept, every one loaded seated or all placed, is done with) */ + if (!object || TEST_FLAG(object->object.damage_flags, _object_dead_bit) || index >= coop_enemies.rider_count) + { + coop_enemies_forget_vehicle(vehicle_number); + continue; + } + if (!vehicle->releasing) + { + if (!coop_enemies_riders_getting_out(vehicle)) + continue; + vehicle->releasing = TRUE; + vehicle->released_count = 0; + vehicle->next_release_time = now; + } + if (now < vehicle->next_release_time) + continue; + { + struct coop_kept_rider rider = coop_enemies.riders[index]; + struct encounter_definition *encounter_definition; + struct squad_definition *squad_definition; + struct actor_starting_location location; + + coop_enemies.riders[index] = coop_enemies.riders[--coop_enemies.rider_count]; + encounter_definition = TAG_BLOCK_GET_ELEMENT(&global_scenario_get()->ai_encounters, + DATUM_INDEX_TO_ABSOLUTE_INDEX(rider.encounter_index), struct encounter_definition); + squad_definition = TAG_BLOCK_GET_ELEMENT(&encounter_definition->squads, rider.squad_index, + struct squad_definition); + /* (as the squad's: its states, no command list of a starting + location's) */ + csmemset(&location, 0, sizeof(location)); + coop_enemies_rider_place(vehicle, vehicle->released_count++, &location.position, &location.facing); + location.cluster_index = NONE; + location.noncombat_sequence_id = NONE; + location.default_state = squad_definition->default_state; + location.initial_state = squad_definition->initial_state; + location.actor_variant_index = NONE; + location.command_list_index = NONE; + actor_place(rider.variant_definition_index, rider.encounter_index, rider.squad_index, &location, FALSE, 0); + encounter_update_status(rider.encounter_index); + } + vehicle->next_release_time = now + RIDER_RELEASE_TICKS; + } +} diff --git a/port/linux/game/coop_enemies.h b/port/linux/game/coop_enemies.h new file mode 100644 index 00000000..e9e138e1 --- /dev/null +++ b/port/linux/game/coop_enemies.h @@ -0,0 +1,48 @@ +/* +COOP_ENEMIES.H + +Network co-op's extra enemies, for its players (coop_enemies.c). +*/ + +#ifndef __COOP_ENEMIES_H +#define __COOP_ENEMIES_H + +/* network_coop.c: a game begins (its EXTRA ENEMIES read), or the host's +game state went back (a cutscene skipped): the dropships' riders kept are +forgotten */ +void coop_enemies_new_game( + void); +void coop_enemies_reset( + void); + +/* encounters.c: how many enemies more a squad placed with `count` gets +(none but on a co-op host, for an encounter of the players' enemies) */ +short coop_enemies_extra_count( + long encounter_index, + short count); + +/* encounters.c: the place of a squad's `number`th extra enemy (from 1) at +its starting location `origin`: rings about it, on open ground; FALSE if +none is (the starting location itself then) */ +boolean coop_enemies_spread_position( + real_point3d const *origin, + short number, + real_point3d *position); + +/* units.c's vehicle_scripting_load_magic: a rider seated, and one left +without a seat (the vehicle's seats taken: an extra enemy), which is kept, +off the map, until the vehicle's riders get out (TRUE: kept, the caller +erases it) */ +void coop_enemies_rider_seated( + long vehicle_index, + long unit_index); +boolean coop_enemies_rider_unseated( + long vehicle_index, + long unit_index); + +/* network_coop.c, after each of the host's ticks: the riders kept placed +at their vehicle's seats once its own riders get out */ +void coop_enemies_update( + void); + +#endif diff --git a/port/linux/game/coop_spectate.c b/port/linux/game/coop_spectate.c new file mode 100644 index 00000000..0e25a59e --- /dev/null +++ b/port/linux/game/coop_spectate.c @@ -0,0 +1,285 @@ +/* +COOP_SPECTATE.C + +Spectating in network co-op. A dead player watches a living teammate +until they respawn (players.c respawns them beside a teammate once it is +safe). The campaign's dead camera would watch the body; instead this +follows a teammate from behind, and A (jump on the keyboard) switches to +the next one. director.c and hud.c call in here. + +It also draws the line telling players about the vote to skip a cutscene +(network_coop.c counts the votes). +*/ + +#include "cseries.h" +#include "camera/dead_camera.h" +#include "game/game.h" +#include "game/game_engine.h" +#include "game/players.h" +#include "input/input.h" +#include "interface/hud_messaging.h" +#include "objects/objects.h" +#include "rasterizer/rasterizer.h" +#include "render/render.h" +#include "text/draw_string.h" +#include "text/font_group.h" +#include "text/unicode.h" +#include "units/units.h" + +#include "coop_spectate.h" +#include "network_coop.h" + +#include + +/* ---------- constants */ + +/* camera distance behind the unit, pitch (radians), and how much of the +turn toward the unit's facing it makes each frame */ +#define SPECTATE_DISTANCE 3.5f +#define SPECTATE_PITCH -0.3f +#define SPECTATE_TURN 0.15f + +/* a passenger (riding a Pelican) is watched from in front of them, above +and looking down, so the camera stays inside the vehicle; a wide view (the +observer allows up to 90 degrees) takes in all of them from that close */ +#define SPECTATE_PASSENGER_DISTANCE 1.0f +#define SPECTATE_PASSENGER_PITCH -0.5f +#define SPECTATE_PASSENGER_FIELD_OF_VIEW DEGREES_TO_RADIANS(85.0f) +/* field of view for every other spectate view (dead_camera.c's default) */ +#define SPECTATE_FIELD_OF_VIEW DEGREES_TO_RADIANS(70.0f) + +/* a teammate the scripts hold (Pillar of Autumn's cryo tube) is watched from +in front, far enough back to see all of what holds them */ +#define SPECTATE_HELD_DISTANCE 2.0f +#define SPECTATE_HELD_PITCH -0.1f + +/* ---------- globals */ + +/* the player each local player is watching, or NONE */ +static long coop_spectate_watched[MAXIMUM_LOCAL_PLAYERS] = { NONE, NONE, NONE, NONE }; + +/* input_abstraction.c: how many ticks the keyboard's jump key has been held */ +byte input_abstraction_port_accept(short controller_index); + +/* ---------- private code */ + +/* The next living player after `after` (or the first, if `after` is NONE), +skipping `self` and wrapping around. NONE if nobody else is alive. */ +static long next_living_player(long self, long after) +{ + struct data_iterator iterator; + struct player_datum *player; + long first = NONE; + boolean passed = after == NONE; + + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + if (iterator.datum_index == after) + { + passed = TRUE; + continue; + } + if (iterator.datum_index == self || player->unit_index == NONE) + continue; + if (passed) + return iterator.datum_index; + if (first == NONE) + first = iterator.datum_index; + } + + return first; +} + +/* Returns TRUE once per press of A (or jump on the keyboard), however long +it is held. The buttons count ticks, and one tick can span several frames, +so testing "held" alone would switch more than once. */ +static boolean next_pressed(short controller_index) +{ + static boolean held[MAXIMUM_GAMEPADS]; + struct gamepad_state const *gamepad; + boolean down; + boolean pressed; + + if (controller_index < 0 || controller_index >= MAXIMUM_GAMEPADS) + return FALSE; + gamepad = input_get_gamepad_state(controller_index); + down = (gamepad && gamepad->buttons[FIRST_GAMEPAD_ANALOG_BUTTON + _gamepad_analog_button_a] > 0) || + input_abstraction_port_accept(controller_index) > 0; + pressed = down && !held[controller_index]; + held[controller_index] = down; + return pressed; +} + +/* whether the unit rides in a vehicle it isn't driving */ +static boolean unit_is_passenger( + long unit_index) +{ + long vehicle_index = object_get(unit_index)->object.parent_object_index; + + return vehicle_index != NONE && object_try_and_get_and_verify_type(vehicle_index, _object_mask_unit) && + unit_get(vehicle_index)->unit.driver_object_index != unit_index; +} + +/* ---------- public code */ + +boolean coop_spectating( + void) +{ + return network_coop_active(); +} + +long coop_spectate_unit( + short local_player_index) +{ + long self = local_player_get_player_index(local_player_index); + long *watched = &coop_spectate_watched[local_player_index]; + struct player_datum *player; + + boolean next; + + if (self == NONE) + return NONE; + player = player_get(self); + /* checked every frame so a press is never missed or counted twice */ + next = next_pressed(player->network_player_data.controller_index); + /* alive: keep `watched`, so the next death starts on the same teammate */ + if (player->unit_index != NONE) + return NONE; + if (*watched == NONE || !player_try_and_get(*watched) || player_get(*watched)->unit_index == NONE || next) + { + *watched = next_living_player(self, *watched); + } + + return *watched != NONE ? player_get(*watched)->unit_index : NONE; +} + +boolean coop_spectate_watching_rider( + short local_player_index) +{ + long self = local_player_get_player_index(local_player_index); + long watched = coop_spectate_watched[local_player_index]; + long unit_index; + long driver_index; + + if (self == NONE || player_get(self)->unit_index != NONE) + return FALSE; + /* the scripted camera may have been running since before this player + died, so nobody has been picked to watch yet */ + if (watched == NONE || !player_try_and_get(watched) || player_get(watched)->unit_index == NONE) + watched = next_living_player(self, NONE); + unit_index = watched != NONE ? player_get(watched)->unit_index : NONE; + if (unit_index == NONE || !unit_is_passenger(unit_index)) + return FALSE; + driver_index = unit_get(object_get(unit_index)->object.parent_object_index)->unit.driver_object_index; + return driver_index == NONE || unit_get(driver_index)->unit.player_index == NONE; +} + +boolean coop_spectate_nothing_to_watch( + short local_player_index) +{ + long self = local_player_get_player_index(local_player_index); + + return self != NONE && player_get(self)->unit_index == NONE && next_living_player(self, NONE) == NONE; +} + +void coop_spectate_camera( + struct dead_camera *camera) +{ + struct object_datum *unit = camera->unit_index != NONE ? object_try_and_get(camera->unit_index) : NULL; + real yaw, turn; + + if (!unit) + return; + yaw = (real)atan2(unit->object.forward.j, unit->object.forward.i); + if (unit_is_passenger(camera->unit_index)) + { + /* fixed in front of the seat, turning with the vehicle */ + camera->facing.yaw = yaw + _pi; + camera->facing.pitch = SPECTATE_PASSENGER_PITCH; + camera->distance = SPECTATE_PASSENGER_DISTANCE; + camera->field_of_view = SPECTATE_PASSENGER_FIELD_OF_VIEW; + return; + } + camera->field_of_view = SPECTATE_FIELD_OF_VIEW; + if (!player_input_enabled()) + { + camera->facing.yaw = yaw + _pi; + camera->facing.pitch = SPECTATE_HELD_PITCH; + camera->distance = SPECTATE_HELD_DISTANCE; + return; + } + /* ease the camera round behind the unit */ + turn = yaw - camera->facing.yaw; + while (turn > _pi) + turn -= 2.0f * _pi; + while (turn < -_pi) + turn += 2.0f * _pi; + camera->facing.yaw += turn * SPECTATE_TURN; + camera->facing.pitch = SPECTATE_PITCH; + camera->distance = SPECTATE_DISTANCE; +} + +/* centred text near the bottom of the screen, in the HUD's font */ +static void draw_bottom_text( + wchar_t const *text) +{ + long font_index = hud_get_font_index(); + struct font_header *font; + real_argb_color color = *global_real_argb_white; + rectangle2d bounds; + short height; + + if (font_index == NONE) + return; + font = font_definition_get(font_index); + height = (short)(font->ascending_height + font->descending_height); + bounds = render.camera.window_bounds; + bounds.y0 = (short)(bounds.y1 - 3 * height - 24); + bounds.y1 = (short)(bounds.y1 - 24); + /* draw mode 2: centered */ + draw_string_set_draw_mode(font_index, NONE, 2, 0, &color); + rasterizer_draw_unicode_string(&bounds, NULL, NULL, 0, text); +} + +void coop_spectate_draw( + short local_player_index) +{ + long watched = coop_spectate_watched[local_player_index]; + wchar_t text[96]; + wchar_t const *hint; + + /* nobody to watch yet (still joining): network_coop.c shows the host's + view from behind instead */ + if (watched == NONE || !player_try_and_get(watched)) + { + if (coop_spectate_nothing_to_watch(local_player_index)) + draw_bottom_text(L"JOINING THE GAME\r\nYou spawn beside a teammate once it is safe"); + return; + } + if (!players_coop_waiting_to_start(local_player_get_player_index(local_player_index))) + hint = L"You come back beside them once it is safe"; + else + hint = L"You join them when they're on foot"; + usnprintf(text, NUMBEROF(text) - 1, L"SPECTATING %.12s (A: NEXT)\r\n%s", player_get(watched)->name, hint); + text[NUMBEROF(text) - 1] = 0; + draw_bottom_text(text); +} + +void coop_skip_vote_draw( + short local_player_index) +{ + short votes, voters; + boolean voted; + wchar_t text[96]; + + /* once, for the first local player */ + if (local_player_index != local_player_get_next(NONE) || !network_coop_skip_vote_status(&votes, &voters, &voted)) + return; + if (voted) + usnprintf(text, NUMBEROF(text) - 1, L"VOTED TO SKIP %d OF %d", votes, voters); + else + usnprintf(text, NUMBEROF(text) - 1, L"PRESS SPACE OR A TO VOTE TO SKIP %d OF %d", votes, voters); + text[NUMBEROF(text) - 1] = 0; + draw_bottom_text(text); +} diff --git a/port/linux/game/coop_spectate.h b/port/linux/game/coop_spectate.h new file mode 100644 index 00000000..253fb9e0 --- /dev/null +++ b/port/linux/game/coop_spectate.h @@ -0,0 +1,45 @@ +/* +COOP_SPECTATE.H + +Spectating in network co-op, and the cutscene skip vote's line +(coop_spectate.c). +*/ + +#ifndef __COOP_SPECTATE_H +#define __COOP_SPECTATE_H + +struct dead_camera; + +/* whether this is network co-op: a network game with no game engine (a campaign map) */ +boolean coop_spectating( + void); + +/* the unit a dead local player watches (A switches teammate), or NONE if +the player is alive or nobody else is */ +long coop_spectate_unit( + short local_player_index); + +/* TRUE if the teammate this local player is spectating (while dead or not +yet spawned) rides in an AI-driven vehicle, such as an insertion Pelican. +network_coop.c then shows that seat instead of the scripted camera. */ +boolean coop_spectate_watching_rider( + short local_player_index); + +/* whether a local player has nothing to look at: no unit of its own and +no living teammate to watch (network_coop.c shows it the host's view) */ +boolean coop_spectate_nothing_to_watch( + short local_player_index); + +/* keeps the dead camera behind the watched unit as it turns */ +void coop_spectate_camera( + struct dead_camera *camera); + +/* the HUD line saying who a dead local player is watching */ +void coop_spectate_draw( + short local_player_index); + +/* during a skippable cutscene in network co-op, the vote count and how to vote */ +void coop_skip_vote_draw( + short local_player_index); + +#endif diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c index 59838c34..7d098faa 100644 --- a/port/linux/game/menu_functions.c +++ b/port/linux/game/menu_functions.c @@ -25,7 +25,8 @@ calls them from PC_MENU_FUNCTION_BASE on): - the settings screens' (tools/port_settings.py): "port settings save" (OK) writes those changed and applies the window's (the rest follow config.toml themselves), "port settings defaults" shows the defaults, - "port settings help" the help of the row chosen; + "port settings help" the help of the row chosen (and Video Setup's + Resolution or Window Size, for the display mode shown); - Controls Setup's: the keyboard and mouse's controls, two bindings each, shown a group at a time ("controls update menu"); "controls begin binding" takes the next key or button pressed for the binding left and @@ -42,7 +43,8 @@ the one last used, else the first) and the game's saved game in it: list update", "solo level set map") has those the profile has reached, and marks those it has finished on Normal, Heroic and Legendary, as the Xbox's list does; a level with the saved game in it goes on with it, at - its difficulty; + its difficulty. Its first row's chooser shows MULTIPLAYER's maps + instead, one chosen played alone at once; - the difficulty menu: "difficulty item select" (a difficulty chosen) and "set difficulty" (its OK button: the difficulty shown) start the game; - Load Game's list ("load game menu init", "load game list update", "load @@ -89,6 +91,7 @@ char const *pc_menu_game_data_input_name(long function_index); void event_manager_post_button(short controller_index, short button_index); int config_text(char const *name, char *text, unsigned int size); int config_write(char const *name, char const *value); +int config_boolean(char const *name); int config_default(char const *name, char *text, unsigned int size); char const *config_string(char const *name); void platform_display_apply(void); @@ -105,6 +108,7 @@ 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); +short players_port_local_player_count(void); boolean pc_menu_event_function_invoke(struct widget_instance *widget, struct event_record *event, long function_index, boolean *widget_deleted); @@ -118,6 +122,7 @@ void pc_menu_game_data_function_invoke(struct widget_instance *widget, long func #define BUTTON_A 0 #define BUTTON_B 1 #define BUTTON_X 2 +#define BUTTON_START 12 enum { @@ -312,6 +317,22 @@ static boolean setting_text(char const *name, char *text, unsigned int size, boo sdl_platform.c) */ if (!strcmp(name, "display.mode") && !text[0]) snprintf(text, size, "%s", default_value || config_boolean("display.fullscreen") ? "borderless" : "windowed"); + /* (display.window_size empty: 640x480 times display.window_scale, as + older versions set it) */ + if (!strcmp(name, "display.window_size") && !text[0]) + { + char scale_text[32] = ""; + long scale; + + if (default_value) + config_default("display.window_scale", scale_text, sizeof(scale_text)); + else + config_text("display.window_scale", scale_text, sizeof(scale_text)); + scale = atol(scale_text); + if (scale < 1) + scale = 1; + snprintf(text, size, "%ldx%ld", 640 * scale, 480 * scale); + } return TRUE; } for (index = 0; index < NUMBEROF(profile_settings); index++) @@ -500,6 +521,7 @@ reached, and (map_data) a saved game on the level */ #define MAXIMUM_PROFILES 32 #define SOUND_ERROR 4 #define SOUND_FORWARD 2 +#define SOUND_BACK 3 /* (an index whose profile reads: saved_game_files.c's) */ #define PROFILE_VALID_BIT 0x80000000UL @@ -837,12 +859,147 @@ static boolean campaign_continue(short controller) return TRUE; } -/* New Game's list: "initialize sp level list solo" starts on the level last -played */ +/* ---- the map lists (New Game's and the Map screen's): their first row's +chooser of SINGLEPLAYER (the campaign's levels) or MULTIPLAYER maps +(port_settings.MAP_KIND_CHOOSER), then the kind's rows, scrolling as +list_scroll scrolls */ + +enum +{ + MAP_KIND_SINGLEPLAYER, + MAP_KIND_MULTIPLAYER, + NUMBER_OF_MAP_KINDS +}; + +/* the rows after the chooser */ +#define MAP_KIND_ROWS 10 + +static void visible_set(struct widget_instance *widget, boolean visible); +short ui_widget_port_multiplayer_maps(char const *const **names, short *last_used); + +/* the kind the chooser shows (kept at `only`, unless NONE) */ +static short map_kind_shown(struct widget_instance *list, short only) +{ + struct widget_instance *spinner = named(list, "list_item_0_map_kind_spinner", 0); + + if (!spinner) + return only != NONE ? only : MAP_KIND_SINGLEPLAYER; + if (only != NONE) + spinner->parameters.list.selected_index = only; + return (short)PIN(spinner->parameters.list.selected_index, 0, NUMBER_OF_MAP_KINDS - 1); +} + +static void map_kind_set(struct widget_instance *list, short kind) +{ + struct widget_instance *spinner = named(list, "list_item_0_map_kind_spinner", 0); + + if (spinner) + spinner->parameters.list.selected_index = kind; +} + +/* the first of count entries shown, for the entry chosen to be in the middle */ +static short map_kind_first(short chosen, short count) +{ + return (short)PIN(chosen - MAP_KIND_ROWS / 2, 0, MAX(count - MAP_KIND_ROWS, 0)); +} + +static void map_kind_focus(struct widget_instance *list, short first, short chosen) +{ + focus_row(list, (short)(1 + chosen - first)); +} + +/* the rows after the chooser: count entries from *first's, scrolled on at +their ends; the entry focused, NONE when the chooser or the buttons have the +focus */ +static short map_kind_rows_update(struct widget_instance *list, short *first, short count, + void (*entry_text)(short entry, wchar_t *text)) +{ + short shown = (short)MIN(count, MAP_KIND_ROWS); + short row = focused_row(list), index = 0; + struct widget_instance *child; + + if (row != NONE && row >= 1 && row == shown && *first + shown < count) + { + (*first)++; + focus_row(list, --row); + } + else if (row == 1 && *first > 0) + { + (*first)--; + focus_row(list, ++row); + } + for (child = list->child; child; child = child->next, index++) + { + wchar_t text[ROW_TEXT_LENGTH]; + + if (index < 1) + continue; + if (strncmp(child->name, "list_item_", 10)) + break; + child->visible = index - 1 < shown; + if (index - 1 < shown) + { + entry_text((short)(*first + index - 1), text); + text_set(named(child, "list_item_text", 0), text); + } + visible_set(named(child, "list_item_arrows", 0), child == list->focused_child); + visible_set(named(child, "scroll_up_button", 0), FALSE); + visible_set(named(child, "scroll_down_button", 0), FALSE); + } + return row == NONE || row < 1 || row > shown ? NONE : (short)(*first + row - 1); +} + +static void multiplayer_map_text(short map, wchar_t *text) +{ + string_get("pc\\main_menu\\mp_map_list", map, text); +} + +/* the description's map (each list's has both kinds' widgets): a campaign +level's picture, name and words (level_description sets them), or a +multiplayer map's, or (both NONE) neither */ +static void map_description_show(struct widget_instance *description, short level, short map) +{ + struct widget_instance *widget; + short marker; + + visible_set(named(description, "replay_level_right_name", 0), level != NONE); + visible_set(named(description, "replay_level_right_pic", 0), level != NONE); + visible_set(named(description, "replay_level_right_data", 0), level != NONE); + visible_set(named(description, "mp_map_right_name", 0), map != NONE); + visible_set(named(description, "mp_map_right_pic", 0), map != NONE); + visible_set(named(description, "mp_map_right_data", 0), map != NONE); + if (level == NONE) + { + for (marker = 0; marker < 3; marker++) + visible_set(named(description, "difficulty_indicator", marker), FALSE); + } + if (map == NONE) + return; + if ((widget = named(description, "mp_map_right_name", 0)) != NULL) + widget->parameters.text_box.string_list_index = map; + if ((widget = named(description, "mp_map_right_pic", 0)) != NULL) + widget->animation.current_frame_index = map; + if ((widget = named(description, "mp_map_right_data", 0)) != NULL) + widget->parameters.text_box.string_list_index = map; +} + +/* New Game's list: SINGLEPLAYER's levels, the profile's reached, or +MULTIPLAYER's maps, played alone to walk around (no game engine: a campaign +game on the map) */ +static struct +{ + short kind; + short first, chosen; + /* the multiplayer maps (ui_widget_port_multiplayer_maps) */ + char const *const *map_names; + short map_count; +} level_list; + +/* "initialize sp level list solo" starts on the level last played */ static boolean level_list_initialize(struct widget_instance *list, short controller) { struct player_profile profile; - short level; + short level, last_used; if (!campaign_profile(controller, &profile)) return FALSE; @@ -853,37 +1010,70 @@ static boolean level_list_initialize(struct widget_instance *list, short control if (!campaign.levels[level].available) level = 0; campaign.shown_level = level; - focus_row(list, level); + level_list.map_count = ui_widget_port_multiplayer_maps(&level_list.map_names, &last_used); + level_list.kind = MAP_KIND_SINGLEPLAYER; + level_list.chosen = level; + level_list.first = map_kind_first(level, NUMBER_OF_SINGLE_PLAYER_LEVELS); + map_kind_set(list, level_list.kind); + map_kind_focus(list, level_list.first, level_list.chosen); return TRUE; } -static void level_row_text(short row, wchar_t *text) +static void level_row_text(short level, wchar_t *text) { - string_get("pc\\main_menu\\map_list", campaign.levels[row].available ? row : LEVEL_UNAVAILABLE, text); + string_get("pc\\main_menu\\map_list", campaign.levels[level].available ? level : LEVEL_UNAVAILABLE, text); } /* "solo map list update" */ static void level_list_update(struct widget_instance *list) { - short row = focused_row(list), level; + struct widget_instance *description = list->parameters.list.extended_description; + short kind = map_kind_shown(list, NONE), count, entry, level; - rows_update(list, NUMBER_OF_SINGLE_PLAYER_LEVELS, level_row_text); - if (row != NONE && row < NUMBER_OF_SINGLE_PLAYER_LEVELS) - campaign.shown_level = row; + if (kind != level_list.kind) + { + level_list.kind = kind; + level_list.first = level_list.chosen = 0; + } + count = kind == MAP_KIND_SINGLEPLAYER ? NUMBER_OF_SINGLE_PLAYER_LEVELS : level_list.map_count; + entry = map_kind_rows_update(list, &level_list.first, count, + kind == MAP_KIND_SINGLEPLAYER ? level_row_text : multiplayer_map_text); + if (entry != NONE) + level_list.chosen = entry; + if (kind == MAP_KIND_MULTIPLAYER) + { + map_description_show(description, NONE, level_list.chosen < count ? level_list.chosen : NONE); + profile_name_show(description); + return; + } + map_description_show(description, 0, NONE); + campaign.shown_level = level_list.chosen; level = campaign.shown_level; if (!campaign.levels[level].available) level = LEVEL_UNAVAILABLE; - level_description(list->parameters.list.extended_description, "replay_level", level, - level == campaign.saved_level, level == LEVEL_UNAVAILABLE ? NULL : campaign.levels[level].finished, NONE); - profile_name_show(list->parameters.list.extended_description); + level_description(description, "replay_level", level, level == campaign.saved_level, + level == LEVEL_UNAVAILABLE ? NULL : campaign.levels[level].finished, NONE); + profile_name_show(description); } /* "solo level set map": the level shown, if reached, for the difficulty -menu */ -static boolean level_choose(void) +menu; or the multiplayer map, played at once (FALSE: no difficulty menu) */ +static boolean level_choose(short controller) { short level = campaign.shown_level; + if (level_list.kind == MAP_KIND_MULTIPLAYER) + { + struct player_profile profile; + + if (level_list.chosen < 0 || level_list.chosen >= level_list.map_count || + !campaign_profile(controller, &profile)) + { + return campaign_fail(); + } + campaign_start(level_list.map_names[level_list.chosen], main_get_difficulty(), controller); + return FALSE; + } if (level < 0 || level >= NUMBER_OF_SINGLE_PLAYER_LEVELS || !campaign.levels[level].available) return campaign_fail(); /* (not yet: setting it at the main menu changes map, as the Xbox's has it @@ -1135,6 +1325,37 @@ static void settings_help(struct widget_instance *list) help->parameters.text_box.string_list_index = index; } +/* Video Setup's Resolution and Window Size, in the one row's place +(tools/port_settings.py): the one the display mode shown uses (Window Size +the window's, Resolution fullscreen's and borderless's) is shown, and the +list passes over the other, which takes no focus while it is hidden */ +static void video_rows_show(struct widget_instance *list) +{ + struct widget_instance *mode = named(list, "mode_spinner", 0); + struct widget_instance *resolution = named(list, "op_resolution", 0); + struct widget_instance *window_size = named(list, "op_window_size", 0); + struct pc_menu_setting *setting = mode ? pc_menu_setting_get(mode->definition_tag_index) : NULL; + struct widget_instance *shown, *child; + short index; + + if (!setting || !resolution || !window_size) + return; + index = mode->parameters.list.selected_index; + shown = index >= 0 && index < setting->value_count && !strcmp(setting->values[index], "windowed") ? + window_size : resolution; + resolution->visible = shown == resolution; + window_size->visible = shown == window_size; + /* (the focus on the one hidden goes to the one shown: Defaults can + change the mode while it has it) */ + if (list->focused_child == (shown == resolution ? window_size : resolution)) + { + for (index = 0, child = list->child; child && child != shown; child = child->next) + index++; + list->focused_child = shown; + list->parameters.list.selected_index = index; + } +} + /* ---------- Change Color: the profile's colour, from a list of the game's colours (more than its rows: it scrolls), by the Xbox's names for its spinner's functions */ @@ -1420,15 +1641,11 @@ static boolean profile_delete(void) 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; -} +(those either has reached) and difficulty, with either player's controller +(ui_widget.c, widget_takes_events_of_controller). The main menu and +Multiplayer go back to one player (main_menu_initialize, +multiplayer_type_menu_initialize). +With one gamepad, it is player 2's (pc_menu_split_players) */ /* "port coop begin": two players, player 1 on its profile (campaign_profile) and the controller that chose co-op */ @@ -1743,8 +1960,6 @@ Xbox's networking, run by the engine's port entry points (ui_widget_event_handler_functions.c) on our lists */ #define MAXIMUM_ADVERTISED_GAMES 9 -#define MULTIPLAYER_MAP_COUNT 13 -#define MAP_ROWS 11 #define GAMETYPE_ROWS 10 #define MAXIMUM_GAMETYPES 100 #define BROWSER_ROWS 15 @@ -1807,6 +2022,7 @@ typedef char verify_advertised_game_open_offset[offsetof(struct advertised_game, /* the engine's (port) */ short ui_widget_port_multiplayer_maps(char const *const **names, short *last_used); boolean ui_widget_port_multiplayer_map_choose(short level_index); +boolean ui_widget_port_cooperative_level_choose(char const *map_name, short difficulty); short ui_widget_port_gametypes(long *indices, short maximum, short *last_used); boolean ui_widget_port_gametype_choose(long profile_index); boolean ui_widget_port_host(struct widget_instance *widget, struct event_record *event, boolean *widget_deleted); @@ -1816,13 +2032,18 @@ boolean network_game_client_advertised_game_in_progress(void *client, struct adv boolean ui_widget_port_join(struct widget_instance *widget, void *advertised_game, char const *lobby_name, boolean *widget_deleted); boolean ui_widget_port_multiplayer_player(short controller_index, long profile_index); +boolean ui_widget_port_unjoin_player(struct widget_instance *widget, struct event_record *event, + boolean *widget_deleted); void network_game_server_port_set_settings(wchar_t const *name, long maximum_players); +void network_game_server_port_set_cooperative_friendly_fire(short friendly_fire); void *global_network_game_client_get(void); void *global_network_game_server_get(void); +boolean network_game_is_splitscreen_local(void); struct advertised_game *network_game_client_get_available_games(void *client); boolean network_game_client_advertised_game_is_valid(struct advertised_game *game); short network_game_client_get_state(void *client, short *state_data); struct network_game *network_game_client_get_game(void *client); +struct network_game *network_game_server_get_game(void *server); short network_game_client_get_local_machine_index(void); short network_game_client_get_seconds_to_game_start(void *client); boolean network_player_is_valid(struct network_player *player); @@ -1848,8 +2069,7 @@ static short const maximum_players[] = { 2, 4, 8, 12, 16, 24, 32, 48, 64, 96, 12 static struct { short mode; - /* Create Game's map and gametype lists */ - short map_first, map_chosen; + /* Create Game's gametype list */ long gametypes[MAXIMUM_GAMETYPES]; short gametype_count; /* (those of the bank the list's spinner shows: STANDARD, CUSTOM) */ @@ -1858,6 +2078,12 @@ static struct /* the server settings */ wchar_t game_name[16]; short maximum_players_index; + /* (co-op's own, so it leaves multiplayer's as it was: each game hosted + starts with COOPERATIVE_DEFAULT_PLAYERS players at most) */ + short cooperative_maximum_players_index; + boolean cooperative_maximum_players_set; + /* co-op's FRIENDLY FIRE shown (network.coop_friendly_fire's) */ + short cooperative_friendly_fire; /* the browser's games */ struct advertised_game *games[MAXIMUM_ADVERTISED_GAMES]; short game_count, game_chosen; @@ -2019,46 +2245,151 @@ static boolean multiplayer_host(struct widget_instance *widget, struct event_rec 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); + multiplayer.cooperative_maximum_players_set = FALSE; return ui_widget_port_host(widget, event, widget_deleted); } -/* ---- the map list */ +/* ---- the map list (the Map screen's): its chooser's SINGLEPLAYER levels +or MULTIPLAYER maps (the map lists', above). A level chosen lists the +difficulties (B goes back to the level) and is hosted as a network co-op +game, which goes to Server Setup; a map goes on to the gametypes. Split +screen hosts only multiplayer maps, its chooser kept at MULTIPLAYER. */ -static void map_row_text(short row, wchar_t *text) +enum { - string_get("pc\\main_menu\\mp_map_list", (short)(multiplayer.map_first + row), text); + MAP_STEP_MAPS, + MAP_STEP_DIFFICULTIES, +}; + +static struct +{ + short kind, step; + /* hosting over the network, which co-op needs */ + boolean hosting; + /* the multiplayer maps (ui_widget_port_multiplayer_maps) */ + short map_count; + short first, chosen; + /* the level whose difficulties are listed */ + short level; +} map_list; + +#define SERVER_SETUP_NAME "pc\\main_menu\\multiplayer_type_select\\server_settings\\server_settings_screen" + +static short map_step_count(void) +{ + if (map_list.step == MAP_STEP_DIFFICULTIES) + return NUMBER_OF_GAME_DIFFICULTY_LEVELS; + return map_list.kind == MAP_KIND_SINGLEPLAYER ? NUMBER_OF_SINGLE_PLAYER_LEVELS : map_list.map_count; } -/* "mp level list initialize" */ +/* opens a step at the entry given */ +static void map_step_open(struct widget_instance *list, short step, short entry) +{ + short count; + + map_list.step = step; + count = map_step_count(); + map_list.chosen = (short)PIN(entry, 0, MAX(count - 1, 0)); + map_list.first = map_kind_first(map_list.chosen, count); + map_kind_focus(list, map_list.first, map_list.chosen); +} + +static void map_level_text(short level, wchar_t *text) +{ + string_get("pc\\main_menu\\map_list", level, text); +} + +static void map_difficulty_text(short difficulty, wchar_t *text) +{ + string_get("pc\\main_menu\\player_profiles_select\\difficulty_names", difficulty, text); +} + +/* "mp level list initialize": on the multiplayer map used last */ static boolean map_list_initialize(struct widget_instance *list) { char const *const *names; + short last_used = 0; - ui_widget_port_multiplayer_maps(&names, &multiplayer.map_chosen); - multiplayer.map_first = (short)PIN(multiplayer.map_chosen - MAP_ROWS / 2, 0, MULTIPLAYER_MAP_COUNT - MAP_ROWS); - focus_row(list, (short)(multiplayer.map_chosen - multiplayer.map_first)); + map_list.hosting = global_network_game_server_get() != NULL && !network_game_is_splitscreen_local(); + map_list.map_count = ui_widget_port_multiplayer_maps(&names, &last_used); + map_list.kind = MAP_KIND_MULTIPLAYER; + map_kind_set(list, map_list.kind); + map_step_open(list, MAP_STEP_MAPS, last_used); return TRUE; } -/* "mp map list update" */ +/* "mp map list update": the chooser's kind, the step's rows, and beside +them the level or map chosen: its picture, name and words */ static void map_list_update(struct widget_instance *list) { struct widget_instance *description = list->parameters.list.extended_description; - struct widget_instance *widget; - short map = list_scroll(list, &multiplayer.map_first, MULTIPLAYER_MAP_COUNT, MAP_ROWS); + short kind = map_kind_shown(list, map_list.hosting ? NONE : MAP_KIND_MULTIPLAYER), count, entry; - if (map != NONE) - multiplayer.map_chosen = map; - rows_update(list, MAP_ROWS, map_row_text); - if ((widget = named(description, "mp_map_right_name", 0)) != NULL) - widget->parameters.text_box.string_list_index = multiplayer.map_chosen; - if ((widget = named(description, "mp_map_right_pic", 0)) != NULL) - widget->animation.current_frame_index = multiplayer.map_chosen; - if ((widget = named(description, "mp_map_right_data", 0)) != NULL) - widget->parameters.text_box.string_list_index = multiplayer.map_chosen; + if (kind != map_list.kind) + { + map_list.kind = kind; + map_list.step = MAP_STEP_MAPS; + map_list.first = map_list.chosen = 0; + } + count = map_step_count(); + entry = map_kind_rows_update(list, &map_list.first, count, map_list.step == MAP_STEP_DIFFICULTIES ? + map_difficulty_text : kind == MAP_KIND_SINGLEPLAYER ? map_level_text : multiplayer_map_text); + if (entry != NONE) + map_list.chosen = entry; + visible_set(named(description, "mp_map_right_item", 0), map_list.chosen < count); + if (map_list.step == MAP_STEP_DIFFICULTIES || kind == MAP_KIND_SINGLEPLAYER) + { + short level = map_list.step == MAP_STEP_DIFFICULTIES ? map_list.level : map_list.chosen; + + map_description_show(description, level, NONE); + level_description(description, "replay_level", level, FALSE, NULL, NONE); + } + else + { + map_description_show(description, NONE, map_list.chosen < count ? map_list.chosen : NONE); + } profile_name_show(description); } +/* "mp level select" (the list's OK): the map chosen, or a level's +difficulties, or the level at the difficulty chosen. FALSE stays on the Map +screen (the gametypes open on TRUE). */ +static boolean map_list_choose(struct widget_instance *list, boolean *widget_deleted) +{ + short chosen = map_list.chosen; + + if (chosen >= map_step_count()) + return campaign_fail(); + if (map_list.step == MAP_STEP_DIFFICULTIES) + { + /* the co-op game set up, then Server Setup in the gametypes' place */ + if (!ui_widget_port_cooperative_level_choose(main_get_solo_level_name(map_list.level), chosen)) + return campaign_fail(); + return ui_widget_port_open(list, SERVER_SETUP_NAME, widget_deleted); + } + if (map_list.kind == MAP_KIND_MULTIPLAYER) + return ui_widget_port_multiplayer_map_choose(chosen); + ui_play_audio_feedback_sound(SOUND_FORWARD); + map_list.level = chosen; + map_step_open(list, MAP_STEP_DIFFICULTIES, (short)PIN(main_get_difficulty(), 0, NUMBER_OF_GAME_DIFFICULTY_LEVELS - 1)); + return FALSE; +} + +/* "port map list back" (B on the list): from the difficulties back to the +level, else out of the Map screen */ +static boolean map_list_back(struct widget_instance *list, boolean *widget_deleted) +{ + ui_play_audio_feedback_sound(SOUND_BACK); + if (map_list.step == MAP_STEP_DIFFICULTIES) + { + map_step_open(list, MAP_STEP_MAPS, map_list.level); + return TRUE; + } + ui_widget_port_go_back(list); + *widget_deleted = TRUE; + return TRUE; +} + /* (the gametype editor's: Server Setup's copy of the game's gametype) */ static void gametype_setup_begin(void); static void gametype_setup_end(void); @@ -2221,6 +2552,95 @@ static void game_name_done(char const *text) multiplayer.game_name[index] = 0; } +/* Whether a network game is co-op: a campaign level with no game engine +(set up by ui_widget_port_cooperative_level_choose). */ +static boolean game_cooperative(struct network_game const *game) +{ + return game && !game->variant.game_engine_index && main_get_solo_level_from_name(game->map.name) != NONE; +} + +/* the same, for the game this machine is hosting */ +static boolean hosting_cooperative(void) +{ + void *server = global_network_game_server_get(); + + return server && game_cooperative(network_game_server_get_game(server)); +} + +/* Server Setup's gametype rows, which co-op hides */ +static char const *const server_settings_gametype_rows[] = +{ + "op_game_type", "op_player_options", "op_item_options", "op_vehicle_options", "op_indicator_options", + "op_team_options", +}; + +/* the most players a co-op game hosted starts with (maximum_players') */ +#define COOPERATIVE_DEFAULT_PLAYERS 16 +/* Server Setup's help for co-op's FRIENDLY FIRE and EXTRA ENEMIES, by +their choices, and EXTRA ENEMIES' PER PLAYER and MULTIPLIER (its +help_strings, tools/port_settings.py) */ +#define COOPERATIVE_FRIENDLY_FIRE_HELP 12 +#define COOPERATIVE_EXTRA_ENEMIES_HELP 16 +#define COOPERATIVE_ENEMIES_PER_PLAYER_HELP 19 +#define COOPERATIVE_ENEMIES_MULTIPLIER_HELP 20 + +/* co-op's EXTRA ENEMIES' choices (port_settings.COOP_ENEMIES_MODES, in this +order): its amount's row is the choice's */ +enum +{ + _cooperative_enemies_none, + _cooperative_enemies_per_player, + _cooperative_enemies_multiplier, + NUMBER_OF_COOPERATIVE_ENEMIES_MODES +}; + +/* co-op's FRIENDLY FIRE's choices (network.coop_friendly_fire's values, +port_settings.COOP_FRIENDLY_FIRE_VALUES, in this order) */ +static short const cooperative_friendly_fire_modes[] = +{ + _friendly_fire_off, _friendly_fire_on, _friendly_fire_shields_only, _friendly_fire_explosives_only +}; + +/* the most players Server Setup shows and sets: the multiplayer game's, or +the co-op game's (COOPERATIVE_DEFAULT_PLAYERS the first time in each game +hosted: multiplayer_host) */ +static short *server_settings_maximum_players_index(void) +{ + short index; + + if (!hosting_cooperative()) + return &multiplayer.maximum_players_index; + if (!multiplayer.cooperative_maximum_players_set) + { + multiplayer.cooperative_maximum_players_index = NUMBEROF(maximum_players) - 1; + for (index = 0; index < NUMBEROF(maximum_players); index++) + { + if (maximum_players[index] == COOPERATIVE_DEFAULT_PLAYERS) + multiplayer.cooperative_maximum_players_index = index; + } + multiplayer.cooperative_maximum_players_set = TRUE; + } + return &multiplayer.cooperative_maximum_players_index; +} + +/* Server Setup's LISTING, PRIVATE: the multiplayer game's (as +network.host_public started it), or co-op's, saved in network.coop_public */ +static boolean server_settings_private(void) +{ + return hosting_cooperative() ? !config_boolean("network.coop_public") : multiplayer.game_private; +} + +static void server_settings_private_set(boolean private_game) +{ + if (!hosting_cooperative()) + multiplayer.game_private = private_game; + else if (private_game != server_settings_private() && + !config_write("network.coop_public", private_game ? "false" : "true")) + { + platform_log("menus: could not write network.coop_public to config.toml"); + } +} + /* "server settings init": the game's name (player 1's, else the one given last), the most players, the gametype's copy (once: the screen is made again on coming back from an option's screen) */ @@ -2228,7 +2648,9 @@ static boolean server_settings_initialize(struct widget_instance *list) { struct widget_instance *spinner = named(list, "max_players_spinner", 0); - gametype_setup_begin(); + /* (co-op has no gametype to edit) */ + if (!hosting_cooperative()) + gametype_setup_begin(); if (!multiplayer.game_name[0] && player_ui_get_active_player_profile_index(0) != NONE) { struct player_profile profile; @@ -2237,11 +2659,12 @@ static boolean server_settings_initialize(struct widget_instance *list) ustrncpy(multiplayer.game_name, profile.player_name, NUMBEROF(multiplayer.game_name) - 1); } if (spinner) - spinner->parameters.list.selected_index = multiplayer.maximum_players_index; + spinner->parameters.list.selected_index = *server_settings_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; + spinner->parameters.list.selected_index = server_settings_private() ? 1 : 0; + p2p_set_hosting_public(multiplayer.mode == _multiplayer_mode_host_internet && !server_settings_private()); return TRUE; } @@ -2262,7 +2685,7 @@ static void server_settings_update(struct widget_instance *list) char text[TEXT_FIELD_LENGTH]; if (spinner) - multiplayer.maximum_players_index = (short)PIN(spinner->parameters.list.selected_index, 0, + *server_settings_maximum_players_index() = (short)PIN(spinner->parameters.list.selected_index, 0, NUMBEROF(maximum_players) - 1); wide_to_text(multiplayer.game_name, text, sizeof(text)); text_field_show(named(list, "server_name_value", 0), text, text_field_editing(row)); @@ -2280,6 +2703,50 @@ static void server_settings_update(struct widget_instance *list) text_set(named(list, "game_type_value", 0), type); } settings_help(list); + { + boolean cooperative = hosting_cooperative(); + short row; + + for (row = 0; row < NUMBEROF(server_settings_gametype_rows); row++) + visible_set(named(list, server_settings_gametype_rows[row], 0), !cooperative); + /* co-op's FRIENDLY FIRE, in their place: its help is its choice's */ + visible_set(named(list, "op_friendly_fire", 0), cooperative); + if (cooperative && (spinner = named(list, "friendly_fire_spinner", 0)) != NULL) + { + short choice = (short)PIN(spinner->parameters.list.selected_index, 0, + NUMBEROF(cooperative_friendly_fire_modes) - 1); + struct widget_instance *help = list->parameters.list.extended_description; + + multiplayer.cooperative_friendly_fire = cooperative_friendly_fire_modes[choice]; + if (help && list->focused_child == named(list, "op_friendly_fire", 0)) + help->parameters.text_box.string_list_index = (short)(COOPERATIVE_FRIENDLY_FIRE_HELP + choice); + } + /* ... and its EXTRA ENEMIES, with the row of the amount of the one + chosen below it (network.coop_enemies_mode and its amounts, which the + host reads as the game begins: coop_enemies.c) */ + { + struct widget_instance *help = list->parameters.list.extended_description; + short mode = _cooperative_enemies_none; + + if ((spinner = named(list, "extra_enemies_spinner", 0)) != NULL) + { + mode = (short)PIN(spinner->parameters.list.selected_index, 0, + NUMBER_OF_COOPERATIVE_ENEMIES_MODES - 1); + } + visible_set(named(list, "op_extra_enemies", 0), cooperative); + visible_set(named(list, "op_enemies_per_player", 0), cooperative && mode == _cooperative_enemies_per_player); + visible_set(named(list, "op_enemies_multiplier", 0), cooperative && mode == _cooperative_enemies_multiplier); + if (cooperative && help) + { + if (list->focused_child == named(list, "op_extra_enemies", 0)) + help->parameters.text_box.string_list_index = (short)(COOPERATIVE_EXTRA_ENEMIES_HELP + mode); + else if (list->focused_child == named(list, "op_enemies_per_player", 0)) + help->parameters.text_box.string_list_index = COOPERATIVE_ENEMIES_PER_PLAYER_HELP; + else if (list->focused_child == named(list, "op_enemies_multiplier", 0)) + help->parameters.text_box.string_list_index = COOPERATIVE_ENEMIES_MULTIPLIER_HELP; + } + } + } /* 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 && @@ -2289,7 +2756,7 @@ static void server_settings_update(struct widget_instance *list) boolean public = spinner->parameters.list.selected_index == 0; struct widget_instance *help = list->parameters.list.extended_description; - multiplayer.game_private = !public; + server_settings_private_set(!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); @@ -2329,9 +2796,15 @@ static boolean server_start(void) if (text_field_editing(NULL)) text_field_end(TRUE); network_game_server_port_set_settings(multiplayer.game_name, - maximum_players[PIN(multiplayer.maximum_players_index, 0, NUMBEROF(maximum_players) - 1)]); - /* (the gametype as Server Setup's options left it) */ - if (!gametype_setup_apply()) + maximum_players[PIN(*server_settings_maximum_players_index(), 0, NUMBEROF(maximum_players) - 1)]); + /* the gametype as Server Setup's options left it (co-op keeps its own, + and its FRIENDLY FIRE) */ + if (hosting_cooperative()) + { + gametype_setup_end(); + network_game_server_port_set_cooperative_friendly_fire(multiplayer.cooperative_friendly_fire); + } + else if (!gametype_setup_apply()) return campaign_fail(); return global_network_game_server_get() != NULL; } @@ -2503,8 +2976,30 @@ static char const *scenario_name(char const *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 */ +/* the campaign level whose scenario a path or name is (a network co-op +game's map), else NONE */ +static short campaign_level_of(char const *map_name) +{ + short level; + + for (level = 0; level < NUMBER_OF_SINGLE_PLAYER_LEVELS; level++) + { + if (!_stricmp(scenario_name(main_get_solo_level_name(level)), scenario_name(map_name))) + return level; + } + return NONE; +} + +/* a game's type as the browsers and lobby show it: its engine's name, or +CO-OP for one with none (network co-op: ui_widget_port_cooperative_level_choose) */ +static wchar_t const *game_type_name(long engine_type) +{ + return engine_type == 0 ? L"CO-OP" : engine_names[PIN(engine_type, 0, 5)]; +} + +/* a map's name as the menus show it (a campaign level's too, hosted as +network co-op; 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; @@ -2519,6 +3014,11 @@ static void map_display_name(char const *map_name, wchar_t *text) return; } } + if ((index = campaign_level_of(map_name)) != NONE) + { + string_get("pc\\main_menu\\map_list", index, text); + return; + } for (index = 0; map_name[index] && index < ROW_TEXT_LENGTH - 1; index++) text[index] = (wchar_t)(unsigned char)map_name[index]; text[index] = 0; @@ -2715,7 +3215,7 @@ static void lobby_browser_update(struct widget_instance *list) 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)]); + text_set(named(row, "server_item_type", 0), game_type_name(game->engine_type)); 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) */ @@ -2846,6 +3346,9 @@ static boolean lobby_browser_select(struct widget_instance *widget, short contro return campaign_fail(); if (advertised_in_progress(found)) { + /* (player 1 joining it: others join them there) */ + if (!multiplayer_player(controller)) + return FALSE; 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); @@ -2917,7 +3420,7 @@ static void browser_update(struct widget_instance *list) text_set(named(row, "server_item_server_name", 0), text); game_map_name(game, 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)]); + text_set(named(row, "server_item_type", 0), game_type_name(game->engine_type)); 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); text_set(named(row, "server_item_ping", 0), advertised_in_progress(game) ? L"LIVE" : L""); @@ -3019,6 +3522,9 @@ static boolean browser_select(struct widget_instance *widget, struct event_recor if (multiplayer.game_chosen < multiplayer.game_count && advertised_in_progress(multiplayer.games[multiplayer.game_chosen])) { + /* (player 1 joining it: others join them there) */ + if (!multiplayer_player(controller)) + return FALSE; csmemcpy(multiplayer.preview_key_id, multiplayer.games[multiplayer.game_chosen]->key_id, sizeof(multiplayer.preview_key_id)); csmemcpy(multiplayer.preview_xnaddr, multiplayer.games[multiplayer.game_chosen]->xnaddr, @@ -3068,6 +3574,205 @@ gametype, the countdown */ static struct network_player *lobby_players[HALO_PORT_MAXIMUM_NETWORK_PLAYERS]; static short lobby_player_count; +/* split screen: up to 4 players on this machine. In the lobby, a controller +not playing presses START to join ("port lobby join"), then chooses a +profile (its screen, "port lobby player choose"); ADD PLAYER ("port lobby +add player") gives one gamepad its own controller first (as it shares +player 1's: pc_menu_split_players). A player's B leaves the game alone ("port +lobby leave"; the machine's last leaves it). In game, a player's pause menu's +QUIT is theirs (the Xbox's "mp game player quit") */ +static struct +{ + /* ADD PLAYER chosen: one gamepad is the new player's */ + boolean adding; + /* the controller choosing its profile, NONE if none */ + short controller; +} lobby_join = { FALSE, NONE }; + +/* the client's player of the controller on this machine, NULL if none */ +static struct network_player *lobby_local_player(short controller) +{ + void *client = global_network_game_client_get(); + struct network_game *game = client ? network_game_client_get_game(client) : NULL; + short machine_index = network_game_client_get_local_machine_index(); + short index; + + for (index = 0; game && machine_index != NONE && index < HALO_PORT_MAXIMUM_NETWORK_PLAYERS; index++) + { + struct network_player *player = &game->players[index]; + + if (network_player_is_valid(player) && (short)player->machine_index == machine_index && + (short)player->controller_index == controller) + { + return player; + } + } + return NULL; +} + +/* whether the controller plays (or is to: joined, not yet added) */ +static boolean lobby_controller_playing(short controller) +{ + return player_ui_local_player_wants_to_play_multiplayer(controller) || lobby_local_player(controller); +} + +/* this machine's players in the network game (or to be) */ +static short lobby_local_player_count(void) +{ + short controller, count = 0; + + if (!global_network_game_client_get()) + return 0; + for (controller = 0; controller < MAXIMUM_LOCAL_PLAYERS; controller++) + count += lobby_controller_playing(controller) ? 1 : 0; + return count; +} + +/* (xinput_sdl.c) whether this machine has, or is adding, a second player: co-op, +or split screen in a network game (a player who quit keeps their part of the +screen until the game ends). Then one gamepad is its own controller, not +sharing player 1's with the keyboard */ +unsigned char pc_menu_split_players(void) +{ + return player_spawn_count >= 2 || lobby_join.adding || lobby_join.controller != NONE || + lobby_local_player_count() >= 2 || players_port_local_player_count() >= 2; +} + +/* the lobby's widget that has the focus (the one a press goes to) */ +static struct widget_instance *focused_leaf(struct widget_instance *widget) +{ + while (widget->parent) + widget = widget->parent; + while (widget->focused_child) + widget = widget->focused_child; + return widget; +} + +/* "port lobby open" (the lobby made, or come back to): no player being added */ +static boolean lobby_join_reset(void) +{ + lobby_join.adding = FALSE; + lobby_join.controller = NONE; + return TRUE; +} + +/* "port lobby add player" (ADD PLAYER): the next START of another controller +joins (one gamepad leaves the keyboard's controller for its own) */ +static boolean lobby_add_player(void) +{ + if (lobby_local_player_count() >= MAXIMUM_LOCAL_PLAYERS) + return campaign_fail(); + lobby_join.adding = TRUE; + return TRUE; +} + +/* "port lobby join" (START): a controller not playing joins, choosing its +profile next (FALSE: no profile screen); a player's START is the focused +button's */ +static boolean lobby_join_start(struct widget_instance *widget, short controller, boolean *widget_deleted) +{ + void *client = global_network_game_client_get(); + struct network_game *game = client ? network_game_client_get_game(client) : NULL; + short state_data; + + if (lobby_controller_playing(controller)) + { + ui_widget_port_dispatch_event(focused_leaf(widget), BUTTON_START, controller, widget_deleted); + return FALSE; + } + if (!game || network_game_client_get_state(client, &state_data) != _client_state_pregame) + return campaign_fail(); + if (lobby_player_count >= game->maximum_players) + { + display_error_text_deferred(L"The game is full.", NONE); + return campaign_fail(); + } + lobby_join.controller = controller; + return TRUE; +} + +/* "port lobby leave" (B): a player leaves the game, the machine's last +leaving it (TRUE: back from the lobby); a controller not playing cancels +ADD PLAYER */ +static boolean lobby_leave(struct widget_instance *widget, struct event_record *event, short controller, + boolean *widget_deleted) +{ + if (!lobby_controller_playing(controller)) + { + lobby_join_reset(); + return FALSE; + } + return ui_widget_port_unjoin_player(widget, event, widget_deleted); +} + +/* "port lobby player list initialize": on the joining controller's profile, +else the first no other player of this machine has */ +static boolean lobby_player_list_initialize(struct widget_instance *list) +{ + long mine = lobby_join.controller != NONE ? player_ui_get_active_player_profile_index(lobby_join.controller) : NONE; + short index, controller; + + profile_list_read(TRUE); + profile_list.chosen = 0; + for (index = profile_list.count - 1; index >= 0; index--) + { + boolean taken = FALSE; + + for (controller = 0; controller < MAXIMUM_LOCAL_PLAYERS; controller++) + { + taken |= controller != lobby_join.controller && lobby_controller_playing(controller) && + player_ui_get_active_player_profile_index(controller) == profile_list.indices[index]; + } + if (!taken) + profile_list.chosen = index; + } + for (index = 0; index < profile_list.count && mine != NONE; index++) + { + if (profile_list.indices[index] == mine) + 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 lobby player choose": the joining controller's player on the profile +chosen (the lobby's "net splitscreen prejoin players" adds them) */ +static boolean lobby_player_choose(void) +{ + short controller = lobby_join.controller; + + if (controller == NONE || profile_list.chosen >= profile_list.count || + !ui_widget_port_multiplayer_player(controller, profile_list.indices[profile_list.chosen])) + { + return campaign_fail(); + } + return TRUE; +} + +/* the lobby's line under its players: how another player joins */ +static void lobby_join_help(struct widget_instance *list) +{ + struct widget_instance *help = named(screen_of(list), "lobby_join_help", 0); + wchar_t const *text = L""; + short controller; + + if (lobby_join.adding) + { + text = L"New player: press START."; + } + else if (lobby_local_player_count() < MAXIMUM_LOCAL_PLAYERS) + { + for (controller = 0; controller < MAXIMUM_LOCAL_PLAYERS; controller++) + { + if (input_has_gamepad(controller) && !lobby_controller_playing(controller)) + text = L"Another controller: START joins."; + } + } + text_set(help, text); +} + static void lobby_row_text(short row, wchar_t *text) { struct network_player *player = lobby_players[multiplayer.lobby_first + row]; @@ -3080,6 +3785,13 @@ static void lobby_row_text(short row, wchar_t *text) usnprintf(text, ROW_TEXT_LENGTH - 1, L"%s (%s)", name, player->team_index ? L"BLUE" : L"RED"); else usnprintf(text, ROW_TEXT_LENGTH - 1, L"%s", name); + /* (this machine's players, when it has more than one: their controllers) */ + if (lobby_local_player_count() >= 2 && (short)player->machine_index == network_game_client_get_local_machine_index()) + { + size_t length = ustrlen(text); + + usnprintf(text + length, ROW_TEXT_LENGTH - 1 - length, L" [P%d]", player->controller_index + 1); + } text[ROW_TEXT_LENGTH - 1] = 0; } @@ -3088,6 +3800,7 @@ static void lobby_map_show(struct widget_instance *description, char const *map_ { char const *const *names; short last, count = ui_widget_port_multiplayer_maps(&names, &last), map = 19, index; + short level = campaign_level_of(map_name); struct widget_instance *widget; for (index = 0; index < count; index++) @@ -3095,10 +3808,22 @@ static void lobby_map_show(struct widget_instance *description, char const *map_ if (!_stricmp(names[index], map_name)) map = index; } + /* (a network co-op game's level: its picture and name, sp_levels' and + map_list's, in the place of the map's) */ + visible_set(named(description, "lobby_map_pic", 0), level == NONE); + visible_set(named(description, "lobby_map_name", 0), level == NONE); + visible_set(named(description, "replay_level_right_pic", 0), level != NONE); + visible_set(named(description, "replay_level_right_name", 0), level != NONE); if ((widget = named(description, "lobby_map_pic", 0)) != NULL) widget->animation.current_frame_index = map; if ((widget = named(description, "lobby_map_name", 0)) != NULL) widget->parameters.text_box.string_list_index = map; + if (level == NONE) + return; + if ((widget = named(description, "replay_level_right_pic", 0)) != NULL) + widget->animation.current_frame_index = level; + if ((widget = named(description, "replay_level_right_name", 0)) != NULL) + widget->parameters.text_box.string_list_index = level; } /* "port lobby update" */ @@ -3121,6 +3846,7 @@ static void lobby_update(struct widget_instance *list) if (multiplayer.lobby_first > MAX(0, lobby_player_count - LOBBY_ROWS)) multiplayer.lobby_first = (short)MAX(0, lobby_player_count - LOBBY_ROWS); rows_update(list, (short)MIN(lobby_player_count, LOBBY_ROWS), lobby_row_text); + lobby_join_help(list); visible_set(named(list, "lobby_button_team", 0), game && game->variant.universal_variant.teams); /* (the buttons' focus, off Switch Team when it is hidden) */ focus_off_hidden(named(list, "lobby_button_bar", 0)); @@ -3161,7 +3887,9 @@ static void lobby_update(struct widget_instance *list) /* ---- an in-progress game's lobby, before joining it (Direct Link and LAN's rows of games under way): what its advertisement tells (no players' -names: they come with joining), JOIN GAME */ +names: they come with joining), JOIN GAME. Split screen players join here +as in the lobby (lobby_join), for the game starts at once for a machine +that joins it: player 1 as it opens, the others with START */ /* the previewed game, found again among the client's (NULL: gone) */ static struct advertised_game *preview_game(void) @@ -3182,6 +3910,34 @@ static struct advertised_game *preview_game(void) return NULL; } +/* the preview's status's end: the players joining from here, when there +are more than one (split screen) */ +static void preview_players_text(wchar_t *text, short size) +{ + short controller; + size_t length; + + if (lobby_local_player_count() < 2) + return; + length = ustrlen(text); + usnprintf(text + length, size - 1 - length, L"\r\n\r\nJoining from here:"); + for (controller = 0; controller < MAXIMUM_LOCAL_PLAYERS; controller++) + { + struct player_profile profile; + wchar_t name[NUMBEROF(profile.player_name) + 1]; + + if (!player_ui_local_player_wants_to_play_multiplayer(controller)) + continue; + player_ui_get_active_player_profile(controller, &profile); + ustrncpy(name, profile.player_name, NUMBEROF(profile.player_name)); + name[NUMBEROF(profile.player_name)] = 0; + text[size - 1] = 0; + length = ustrlen(text); + usnprintf(text + length, size - 1 - length, L"\r\n%s [P%d]", name, controller + 1); + } + text[size - 1] = 0; +} + /* "port lobby preview update" */ static void preview_update(struct widget_instance *list) { @@ -3194,7 +3950,7 @@ static void preview_update(struct widget_instance *list) if (valid) { lobby_map_show(description, game->map_name); - usnprintf(text, NUMBEROF(text) - 1, L"%s\r\n%d of %d players\r\non %d machines", engine_names[PIN(game->engine_type, 0, 5)], + usnprintf(text, NUMBEROF(text) - 1, L"%s\r\n%d of %d players\r\non %d machines", game_type_name(game->engine_type), game->player_count, game->maximum_player_count, game->machine_count); text[NUMBEROF(text) - 1] = 0; text_set_length(named(description, "lobby_game_data", 0), text, LOBBY_TEXT_LENGTH); @@ -3204,22 +3960,74 @@ static void preview_update(struct widget_instance *list) usnprintf(text, NUMBEROF(text) - 1, L"%s\r\n\r\nThis game is under way.\r\n\r\n%s", name, game->open ? L"JOIN GAME joins it now;\r\nits players show then." : L"It cannot be joined now:\r\nit is loading, over or full."); + text[NUMBEROF(text) - 1] = 0; + preview_players_text(text, NUMBEROF(text)); } else usnprintf(text, NUMBEROF(text) - 1, L"The game is gone."); text[NUMBEROF(text) - 1] = 0; text_set_length(named(list, "preview_status", 0), text, LOBBY_TEXT_LENGTH); + lobby_join_help(list); profile_name_show(description); } -/* "port lobby preview join" */ +/* "port lobby preview add" (START): as the lobby's ("port lobby join"), a +controller not joining chooses its profile, to join with the others; a +joining player's START is the focused button's */ +static boolean preview_add(struct widget_instance *widget, short controller, boolean *widget_deleted) +{ + struct advertised_game *game = preview_game(); + short count = lobby_local_player_count(); + + if (lobby_controller_playing(controller)) + { + ui_widget_port_dispatch_event(focused_leaf(widget), BUTTON_START, controller, widget_deleted); + return FALSE; + } + if (!game || !game->open || count >= MAXIMUM_LOCAL_PLAYERS) + return campaign_fail(); + if (game->player_count + count >= game->maximum_player_count) + { + display_error_text_deferred(L"The game is full.", NONE); + return campaign_fail(); + } + lobby_join.controller = controller; + return TRUE; +} + +/* "port lobby preview leave" (B): a joining player stays out, the last of +them backing out (TRUE); a controller not joining cancels ADD PLAYER, else +backs out for them all (the game is not joined yet: none is kept here) */ +static boolean preview_leave(short controller) +{ + short index; + + if (!lobby_controller_playing(controller)) + { + if (lobby_join.adding) + { + lobby_join_reset(); + return FALSE; + } + for (index = 0; index < MAXIMUM_LOCAL_PLAYERS; index++) + player_ui_local_player_left_multiplayer_game(index); + return TRUE; + } + player_ui_local_player_left_multiplayer_game(controller); + return lobby_local_player_count() == 0; +} + +/* "port lobby preview join": with the players joining from here (player 1, +as the preview opened, and those added), whom the lobby asks the host for +all at once ("net splitscreen prejoin players"): it starts the machine in +the game once it has them all */ static boolean preview_join(struct widget_instance *widget, short controller, boolean *widget_deleted) { struct advertised_game *game = preview_game(); if (!game || !game->open) return campaign_fail(); - if (!multiplayer_player(controller)) + if (!lobby_local_player_count() && !multiplayer_player(controller)) return FALSE; return ui_widget_port_join(widget, game, LOBBY_NAME, widget_deleted); } @@ -3938,17 +4746,19 @@ boolean pc_menu_event_function_invoke( { return pc_menu_profile_edit_begin(); } + /* (the press posted is the controller's that chose the button: a + split screen player's LEAVE is theirs) */ else if (!strcmp(name, "mouse emit accept event")) { - event_manager_post_button(controller_of(widget), BUTTON_A); + event_manager_post_button(controller, BUTTON_A); } else if (!strcmp(name, "mouse emit back event")) { - event_manager_post_button(controller_of(widget), BUTTON_B); + event_manager_post_button(controller, BUTTON_B); } else if (!strcmp(name, "mouse emit x event")) { - event_manager_post_button(controller_of(widget), BUTTON_X); + event_manager_post_button(controller, BUTTON_X); } else if (!strcmp(name, "emit custom activation event")) { @@ -4056,7 +4866,11 @@ boolean pc_menu_event_function_invoke( } else if (!strcmp(name, "mp level select")) { - return ui_widget_port_multiplayer_map_choose(multiplayer.map_chosen); + return map_list_choose(widget, widget_deleted); + } + else if (!strcmp(name, "port map list back")) + { + return map_list_back(widget, widget_deleted); } else if (!strcmp(name, "mp profiles list initialize")) { @@ -4117,6 +4931,38 @@ boolean pc_menu_event_function_invoke( { return profile_choose(controller); } + else if (!strcmp(name, "port lobby open")) + { + return lobby_join_reset(); + } + else if (!strcmp(name, "port lobby add player")) + { + return lobby_add_player(); + } + else if (!strcmp(name, "port lobby join")) + { + return lobby_join_start(widget, controller, widget_deleted); + } + else if (!strcmp(name, "port lobby leave")) + { + return lobby_leave(widget, event, controller, widget_deleted); + } + else if (!strcmp(name, "port lobby player list initialize")) + { + return lobby_player_list_initialize(widget); + } + else if (!strcmp(name, "port lobby player choose")) + { + return lobby_player_choose(); + } + else if (!strcmp(name, "port lobby preview add")) + { + return preview_add(widget, controller, widget_deleted); + } + else if (!strcmp(name, "port lobby preview leave")) + { + return preview_leave(controller); + } else if (!strcmp(name, "port coop begin")) { return coop_begin(controller); @@ -4163,7 +5009,7 @@ boolean pc_menu_event_function_invoke( } else if (!strcmp(name, "solo level set map")) { - return level_choose(); + return level_choose(controller); } else if (!strcmp(name, "difficulty item select")) { @@ -4235,7 +5081,10 @@ void pc_menu_game_data_function_invoke( else if (!strcmp(name, "port lobby update")) lobby_update(widget); else if (!strcmp(name, "port settings help")) + { + video_rows_show(widget); settings_help(widget); + } else if (!strcmp(name, "controls update menu")) controls_update(widget); else if (!strcmp(name, "color picker update")) diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c index a4f61b36..f0e0d9d9 100644 --- a/port/linux/game/menu_tags.c +++ b/port/linux/game/menu_tags.c @@ -45,6 +45,8 @@ no screen pauses the game (a network game's clock runs on). /* the platform layer's (port/linux/src) */ void platform_log(char const *format, ...); char const *config_string(char const *name); +int platform_display_resolutions(long *widths, long *heights, int maximum); +int platform_window_sizes(long *widths, long *heights, int maximum); /* the game's network server (this machine hosts: network_game_globals.c) */ void *global_network_game_server_get(void); @@ -341,8 +343,12 @@ 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 map list back", "port pause end game", "port coop begin", "port coop player 2 list initialize", "port coop player 2", + "port lobby open", "port lobby add player", "port lobby join", "port lobby leave", + "port lobby player list initialize", "port lobby player choose", + "port lobby preview add", "port lobby preview leave", /* (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", @@ -698,6 +704,41 @@ static long split(char const *text, char const **pieces) return count; } +/* a spinner's strings, or its values, split: its own, but Video Setup's +sizes are the display's (sdl_platform.c), shown "1920 x 1080" and set +"1920x1080": its resolutions after Resolution's own (NATIVE), and the +window sizes that fit it in place of Window Size's own */ +static long spinner_split(struct halo_menu_widget const *widget, boolean values, char const **pieces) +{ + long count = split(values ? widget->values : widget->strings, pieces); + long widths[MAXIMUM_STRINGS], heights[MAXIMUM_STRINGS]; + long added, index; + + if (widget->setting && !strcmp(widget->setting, "display.resolution")) + added = platform_display_resolutions(widths, heights, (int)(MAXIMUM_STRINGS - count)); + else if (widget->setting && !strcmp(widget->setting, "display.window_size")) + { + added = platform_window_sizes(widths, heights, MAXIMUM_STRINGS); + if (added > 0) + count = 0; + } + else + return count; + for (index = 0; index < added; index++) + { + char text[32]; + char *piece; + + snprintf(text, sizeof(text), values ? "%ldx%ld" : "%ld x %ld", widths[index], heights[index]); + piece = allocate((long)strlen(text) + 1); + if (!piece) + break; + strcpy(piece, text); + pieces[count++] = piece; + } + return count; +} + static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index) { struct bitmap_group *group = allocate(sizeof(struct bitmap_group)); @@ -912,7 +953,7 @@ static void setting_add(struct halo_menu_widget const *source, long definition_i setting->definition_index = definition_index; setting->setting = source->setting; setting->loaded_index = NONE; - setting->value_count = split(source->values, setting->values); + setting->value_count = spinner_split(source, TRUE, setting->values); } /* a font: large, small, terminal, or the map's by its path */ @@ -1641,7 +1682,7 @@ void menu_tags_loaded( if (widget->strings) { char const *pieces[MAXIMUM_STRINGS]; - long count = split(widget->strings, pieces); + long count = spinner_split(widget, FALSE, pieces); instance_set(instances, UNICODE_STRING_LIST_TAG, build.spinner_tags[index], widget->name, " strings", string_list_build(pieces, count)); diff --git a/port/linux/game/network_actors.c b/port/linux/game/network_actors.c new file mode 100644 index 00000000..82e32d3a --- /dev/null +++ b/port/linux/game/network_actors.c @@ -0,0 +1,901 @@ +/* +NETWORK_ACTORS.C + +AI sync (port/linux/NETCODE.md). + +Only the host runs the AI (game.c skips ai_update on clients). An actor +drives its unit the way a player does, through unit_control. The clients +have the units (network_objects.c); this file makes them move like the +host's. + +Each tick the host records the control every actor gave its unit (a hook +in unit_control, and the bursts it holds the trigger for) and sends it to +each client with the unit's state: position, seat, shields and health, +camouflage, and whether it flees in a panic. Units near a client's players +are sent every tick, others less often, but a unit starting a one-tick +action (a jump, a grenade) goes to everyone that tick. Each tick a client +feeds the latest control into unit_control, the same path that drives a +remote player's unit, so the unit walks, aims, fires and throws as it does +on the host. The state corrects any drift. + +What the AI starts once is numbered, so a client plays each once however +many times it receives it: animation impulses (dives, vaults), melee +attacks and leaps (which the AI starts directly, not through the control), +speech, and custom animations (outside co-op, whose events carry them). +Pain and death sounds aren't sent: a client plays those itself as it +replays the host's damage. + +A client creates no actors of its own (actors.c): the host's are the only +ones. Deaths need nothing here: network_damage.c kills a client's copy of +an AI unit the same way it kills a player's. +*/ + +/* ---------- headers */ + +#include "cseries.h" +#include "game/game.h" +#include "networking/network_game_globals.h" +#include "objects/objects.h" +#include "objects/damage.h" +#include "sound/game_sound.h" +#include "sound/sound_definitions.h" +#include "units/units.h" +#include "units/unit_control_data.h" +#include "network_coop.h" +#include "network_distributed.h" + +#include + +/* ---------- constants */ + +enum +{ + /* AI units tracked at once: every actor (halo_port_capacity.h's + HALO_PORT_MAXIMUM_ACTORS) and the units the cutscenes' recorded + animations drive */ + MAXIMUM_NETWORK_ACTORS = HALO_PORT_MAXIMUM_ACTORS + 32, + MAXIMUM_ENTRIES_PER_MESSAGE = 64, + /* A client keeps applying a control for this long after the host last + sent one. After that the unit is left alone: the actor let go of it, or + the host went quiet. */ + CONTROL_HELD_TICKS = 2 * TICKS_PER_SECOND, + /* an impulse is repeated in this many entries, in case one is lost */ + IMPULSE_REPEAT_TICKS = 3, + /* the number of unit_start_animation_impulse impulses (private to units.c) */ + NUMBER_OF_UNIT_ANIMATION_IMPULSES = 14, + NO_IMPULSE = 0xFF, + NO_TEAM = 0xFF, + + /* An entry's impulse is one of the animation impulses or, past them, an + action the AI starts directly: a melee attack (unit_melee_attack_begin) + or a leap (unit_leap_begin). */ + _actor_action_melee = NUMBER_OF_UNIT_ANIMATION_IMPULSES, + _actor_action_leap, + NUMBER_OF_ACTOR_ACTIONS, + + /* control flags a unit acts on in one tick: a unit that sets one goes to + every client that tick, however far away, so the action isn't missed */ + ONE_SHOT_CONTROL_FLAGS = FLAG(_unit_control_jump_bit) | FLAG(_unit_control_action_bit) | + FLAG(_unit_control_weapon_reload_bit) | FLAG(_unit_control_throw_grenade_bit) | + FLAG(_unit_control_swap_weapons_bit), +}; + +/* struct distributed_actor_state flags */ +enum +{ + _distributed_actor_rides_bit = 0, + _distributed_actor_shield_depleted_bit, + _distributed_actor_shield_charging_bit, + _distributed_actor_shield_over_charging_bit, + _distributed_actor_camouflaged_bit, + _distributed_actor_super_camouflaged_bit, + /* the impulse turns the unit to impulse_alignment */ + _distributed_actor_impulse_aligned_bit, + /* fleeing in a panic, toward run_blindly_angle (unit_start_running_blindly) */ + _distributed_actor_running_blindly_bit, +}; + +/* facing, aiming and looking vectors are sent as yaw and pitch */ +enum +{ + _angle_yaw, + _angle_pitch, + NUMBER_OF_ANGLES +}; + +/* ---------- structures */ + +/* an AI unit's control and state at the end of the host's tick (like +struct distributed_unit_state for players) */ +struct distributed_actor_state +{ + long unit_index; + long vehicle_index; + short seat_index; + byte flags; + /* the impulse's sequence number; impulse is NO_IMPULSE when there is none */ + byte impulse_number; + byte impulse; + byte animation_state; + byte aiming_speed; + byte primary_trigger; + word control_flags; + signed char throttle[3]; + byte active_camouflage; + /* the speech's sequence number; 0 when there is none */ + byte speech_number; + /* the unit's team (NO_TEAM for none): the AI sets it after the unit is + made, so a client's copy would keep the team it was made with */ + byte team; + short facing[NUMBER_OF_ANGLES]; + short aiming[NUMBER_OF_ANGLES]; + short looking[NUMBER_OF_ANGLES]; + short impulse_alignment; + short run_blindly_angle; + word body_vitality; + word shield_vitality; + /* the sound the unit started saying (a sound tag) */ + long speech_sound; + /* the custom animation it started (unit_start_user_animation), and its + sequence number; the graph is NONE when there is none */ + long user_animation_graph; + short user_animation; + byte user_animation_number; + byte user_animation_interpolate; + real_point3d position; + struct distributed_vector velocity; + struct distributed_vector forward; + struct distributed_vector up; +}; + +/* the damage an AI unit is taking: its shield's flare and its body's wounds +are drawn by it */ +struct distributed_actor_damage +{ + long unit_index; + word current_shield_damage; + word recent_shield_damage; + word current_body_damage; + word recent_body_damage; +}; + +struct distributed_actor_damage_message +{ + struct distributed_message_header header; + struct distributed_actor_damage units[MAXIMUM_ENTRIES_PER_MESSAGE]; +}; + +struct distributed_actor_state_message +{ + struct distributed_message_header header; + struct distributed_actor_state states[MAXIMUM_ENTRIES_PER_MESSAGE]; +}; + +/* host: what an actor gave its unit this tick */ +struct host_actor +{ + long unit_index; + /* got a control since the last send (noted during game_tick, sent after it) */ + boolean noted; + /* its damage was sent last tick (and once more when it ends) */ + boolean damaged; + struct unit_control_data control; + /* the control flags of the last tick's entry */ + word sent_control_flags; + /* the latest impulse, how many more entries carry it, and its number */ + short impulse; + short impulse_sends; + byte impulse_number; + boolean impulse_aligned; + real_vector2d impulse_alignment; + /* the latest speech, how many more entries carry it, and its number */ + long speech_sound; + short speech_sends; + byte speech_number; + /* the latest custom animation, the same way */ + long user_animation_graph; + short user_animation; + boolean user_animation_interpolate; + short user_animation_sends; + byte user_animation_number; +}; + +/* client: the latest the host sent about an AI unit */ +struct client_actor +{ + long unit_index; + long received_time; + struct unit_control_data control; + /* the impulse to play, and the number of the last one played */ + short impulse; + byte impulse_number; + byte played_impulse_number; + boolean impulse_aligned; + real_vector2d impulse_alignment; + /* the speech to play (NONE when there is none), and the number of the last played */ + long speech_sound; + byte speech_number; + byte played_speech_number; + /* the custom animation to play (graph NONE when there is none), and the + number of the last played */ + long user_animation_graph; + short user_animation; + boolean user_animation_interpolate; + byte user_animation_number; + byte played_user_animation_number; + boolean driven; +}; + +/* ---------- globals */ + +static struct host_actor host_actors[MAXIMUM_NETWORK_ACTORS]; +static short host_actor_count; +/* host: the numbers of every unit's impulses, speech and animations, from +one counter, so a unit that drops out of the table and comes back can't +repeat the number a client last played */ +static byte host_event_number; + +static struct client_actor client_actors[MAXIMUM_NETWORK_ACTORS]; +static short client_actor_count; + +/* ---------- private code */ + +static void angles_pack( + real_vector3d const *vector, + short *angles) +{ + angles[_angle_yaw] = distributed_angle_pack((real)atan2(vector->j, vector->i)); + angles[_angle_pitch] = distributed_angle_pack((real)asin(PIN(vector->k, -1.0f, 1.0f))); +} + +static void angles_unpack( + short const *angles, + real_vector3d *vector) +{ + real yaw = distributed_angle_unpack(angles[_angle_yaw], FALSE); + real pitch = distributed_angle_unpack(angles[_angle_pitch], TRUE); + + vector->i = (real)(cos(pitch) * cos(yaw)); + vector->j = (real)(cos(pitch) * sin(yaw)); + vector->k = (real)sin(pitch); +} + +/* host: the next event's number (never 0, which a client's new entry has +as played) */ +static byte next_event_number( + void) +{ + if (++host_event_number == 0) + host_event_number = 1; + return host_event_number; +} + +/* the unit's entry, adding one if needed; NULL if the table is full */ +static struct host_actor *host_actor_for( + long unit_index) +{ + short index; + + for (index = 0; index < host_actor_count; index++) + { + if (host_actors[index].unit_index == unit_index) + return &host_actors[index]; + } + if (host_actor_count >= MAXIMUM_NETWORK_ACTORS) + return NULL; + csmemset(&host_actors[host_actor_count], 0, sizeof(host_actors[0])); + host_actors[host_actor_count].unit_index = unit_index; + host_actors[host_actor_count].impulse = NONE; + host_actors[host_actor_count].speech_sound = NONE; + host_actors[host_actor_count].user_animation_graph = NONE; + return &host_actors[host_actor_count++]; +} + +static struct client_actor *client_actor_for( + long unit_index) +{ + short index; + + for (index = 0; index < client_actor_count; index++) + { + if (client_actors[index].unit_index == unit_index) + return &client_actors[index]; + } + if (client_actor_count >= MAXIMUM_NETWORK_ACTORS) + return NULL; + csmemset(&client_actors[client_actor_count], 0, sizeof(client_actors[0])); + client_actors[client_actor_count].unit_index = unit_index; + client_actors[client_actor_count].impulse = NONE; + client_actors[client_actor_count].speech_sound = NONE; + client_actors[client_actor_count].user_animation_graph = NONE; + return &client_actors[client_actor_count++]; +} + +/* whether the unit exists, is alive, and isn't a player's */ +static boolean actor_unit_valid( + long unit_index) +{ + struct unit_datum *unit; + + if (!object_try_and_get_and_verify_type(unit_index, _object_mask_unit)) + return FALSE; + unit = unit_get(unit_index); + return !TEST_FLAG(unit->object.damage_flags, _object_dead_bit) && unit->unit.player_index == NONE; +} + +/* host: builds the entry to send for an AI unit */ +static void actor_state_from_unit( + struct host_actor const *actor, + struct distributed_actor_state *state) +{ + struct unit_datum *unit = unit_get(actor->unit_index); + struct damage_network_state damage; + real camouflage = PIN(unit->unit.active_camouflage, 0.0f, 1.0f); + + csmemset(state, 0, sizeof(*state)); + state->unit_index = actor->unit_index; + state->vehicle_index = NONE; + state->seat_index = NONE; + if (unit->object.parent_object_index != NONE && unit->unit.parent_seat_index != NONE) + { + SET_FLAG(state->flags, _distributed_actor_rides_bit, TRUE); + state->vehicle_index = unit->object.parent_object_index; + state->seat_index = unit->unit.parent_seat_index; + } + state->animation_state = (byte)actor->control.animation_state; + state->aiming_speed = (byte)actor->control.aiming_speed; + state->primary_trigger = (byte)(long)floor(PIN(actor->control.primary_trigger, 0.0f, 1.0f) * 255.0f + 0.5f); + state->control_flags = actor->control.control_flags; + /* for a burst the AI holds the trigger for (unit_persistent_control), + send the flags and trigger the unit's update produced this tick */ + if (unit->unit.persistent_control_timer > 0) + { + state->control_flags = (word)(unit->unit.control_flags & (FLAG(NUMBER_OF_UNIT_CONTROL_FLAGS) - 1)); + state->primary_trigger = (byte)(long)floor(PIN(unit->unit.primary_trigger, 0.0f, 1.0f) * 255.0f + 0.5f); + } + state->throttle[0] = (signed char)(long)floor(PIN(actor->control.throttle.i, -1.0f, 1.0f) * 127.0f + 0.5f); + state->throttle[1] = (signed char)(long)floor(PIN(actor->control.throttle.j, -1.0f, 1.0f) * 127.0f + 0.5f); + state->throttle[2] = (signed char)(long)floor(PIN(actor->control.throttle.k, -1.0f, 1.0f) * 127.0f + 0.5f); + angles_pack(&actor->control.facing_vector, state->facing); + angles_pack(&actor->control.aiming_vector, state->aiming); + angles_pack(&actor->control.looking_vector, state->looking); + state->impulse = NO_IMPULSE; + if (actor->impulse != NONE && actor->impulse_sends > 0) + { + state->impulse = (byte)actor->impulse; + state->impulse_number = actor->impulse_number; + SET_FLAG(state->flags, _distributed_actor_impulse_aligned_bit, actor->impulse_aligned); + if (actor->impulse_aligned) + { + state->impulse_alignment = distributed_angle_pack( + (real)atan2(actor->impulse_alignment.j, actor->impulse_alignment.i)); + } + } + state->speech_sound = NONE; + if (actor->speech_sound != NONE && actor->speech_sends > 0) + { + state->speech_sound = actor->speech_sound; + state->speech_number = actor->speech_number; + } + state->user_animation_graph = NONE; + if (actor->user_animation_graph != NONE && actor->user_animation_sends > 0) + { + state->user_animation_graph = actor->user_animation_graph; + state->user_animation = actor->user_animation; + state->user_animation_interpolate = (byte)actor->user_animation_interpolate; + state->user_animation_number = actor->user_animation_number; + } + if (TEST_FLAG(unit->unit.flags, _unit_running_blindly_bit)) + { + SET_FLAG(state->flags, _distributed_actor_running_blindly_bit, TRUE); + state->run_blindly_angle = distributed_angle_pack(unit->unit.run_blindly_angle); + } + damage_get_network_state(actor->unit_index, &damage); + SET_FLAG(state->flags, _distributed_actor_shield_depleted_bit, damage.shield_depleted); + SET_FLAG(state->flags, _distributed_actor_shield_charging_bit, damage.shield_charging); + SET_FLAG(state->flags, _distributed_actor_shield_over_charging_bit, damage.shield_over_charging); + state->body_vitality = distributed_vitality_pack(damage.body_vitality); + state->shield_vitality = distributed_vitality_pack(damage.shield_vitality); + SET_FLAG(state->flags, _distributed_actor_camouflaged_bit, + TEST_FLAG(unit->unit.flags, _unit_active_camouflaged_bit)); + SET_FLAG(state->flags, _distributed_actor_super_camouflaged_bit, + TEST_FLAG(unit->unit.flags, _unit_super_camouflaged_bit)); + state->active_camouflage = (byte)(long)floor(camouflage * 255.0f + 0.5f); + state->team = unit->object.owner_team_index >= 0 && unit->object.owner_team_index < NO_TEAM ? + (byte)unit->object.owner_team_index : NO_TEAM; + state->position = unit->object.position; + distributed_vector_pack(&unit->object.translational_velocity, DISTRIBUTED_VELOCITY_SCALE, &state->velocity); + distributed_vector_pack(&unit->object.forward, DISTRIBUTED_UNIT_SCALE, &state->forward); + distributed_vector_pack(&unit->object.up, DISTRIBUTED_UNIT_SCALE, &state->up); +} + +/* Client: stores the entry's control and applies its state to the unit. +Returns FALSE for an entry with invalid values. */ +static boolean actor_state_apply( + struct distributed_actor_state const *state, + long now) +{ + struct client_actor *actor; + struct unit_datum *unit; + struct damage_network_state damage; + real_vector3d velocity; + real_vector3d forward; + real_vector3d up; + + if (!distributed_object_index_valid(state->unit_index) || !network_objects_client_has(state->unit_index)) + return FALSE; + /* (a body the host's unit got back up from: a Flood combat form that + feigned death, which it does only once its body is at rest. Left dead, + the ragdoll was dragged about by the host's positions. The host sends + only living units.) */ + if (object_try_and_get_and_verify_type(state->unit_index, _object_mask_unit) && + TEST_FLAG(unit_get(state->unit_index)->object.damage_flags, _object_dead_bit) && + TEST_FLAG(unit_get(state->unit_index)->object.flags, _object_at_rest_bit) && + unit_get(state->unit_index)->unit.player_index == NONE && state->body_vitality > 0) + { + unit_port_resurrect(state->unit_index); + } + if (!actor_unit_valid(state->unit_index) || + state->animation_state >= NUMBER_OF_UNIT_ANIMATION_STATES || + state->aiming_speed >= NUMBER_OF_UNIT_AIMING_SPEEDS || + !VALID_FLAGS(state->control_flags, NUMBER_OF_UNIT_CONTROL_FLAGS) || + (state->impulse != NO_IMPULSE && state->impulse >= NUMBER_OF_ACTOR_ACTIONS) || + (state->speech_sound != NONE && !distributed_tag_of_group(state->speech_sound, SOUND_DEFINITION_TAG)) || + (state->user_animation_graph != NONE && + !distributed_graph_animation(state->user_animation_graph, state->user_animation)) || + !distributed_point_valid(&state->position, UNIT_WORLD_BOUND)) + { + return FALSE; + } + distributed_vector_unpack(&state->velocity, DISTRIBUTED_VELOCITY_SCALE, &velocity); + distributed_vector_unpack(&state->forward, DISTRIBUTED_UNIT_SCALE, &forward); + distributed_vector_unpack(&state->up, DISTRIBUTED_UNIT_SCALE, &up); + if (!distributed_axes_make_valid(&forward, &up)) + return FALSE; + actor = client_actor_for(state->unit_index); + if (!actor) + return TRUE; + + /* the control, applied every tick until the next entry */ + actor->received_time = now; + actor->control.animation_state = (char)state->animation_state; + actor->control.aiming_speed = (char)state->aiming_speed; + actor->control.control_flags = state->control_flags; + actor->control.weapon_index = NONE; + actor->control.grenade_index = NONE; + actor->control.zoom_level = NONE; + actor->control.throttle.i = (real)state->throttle[0] / 127.0f; + actor->control.throttle.j = (real)state->throttle[1] / 127.0f; + actor->control.throttle.k = (real)state->throttle[2] / 127.0f; + actor->control.primary_trigger = (real)state->primary_trigger / 255.0f; + angles_unpack(state->facing, &actor->control.facing_vector); + angles_unpack(state->aiming, &actor->control.aiming_vector); + angles_unpack(state->looking, &actor->control.looking_vector); + if (state->impulse != NO_IMPULSE && state->impulse_number != actor->played_impulse_number) + { + real yaw = distributed_angle_unpack(state->impulse_alignment, FALSE); + + actor->impulse = state->impulse; + actor->impulse_number = state->impulse_number; + actor->impulse_aligned = TEST_FLAG(state->flags, _distributed_actor_impulse_aligned_bit); + actor->impulse_alignment.i = (real)cos(yaw); + actor->impulse_alignment.j = (real)sin(yaw); + } + + if (state->speech_sound != NONE && state->speech_number != actor->played_speech_number) + { + actor->speech_sound = state->speech_sound; + actor->speech_number = state->speech_number; + } + if (state->user_animation_graph != NONE && state->user_animation_number != actor->played_user_animation_number) + { + actor->user_animation_graph = state->user_animation_graph; + actor->user_animation = state->user_animation; + actor->user_animation_interpolate = state->user_animation_interpolate != 0; + actor->user_animation_number = state->user_animation_number; + } + + /* the state: seat, shields and health, camouflage, running blindly, and + position (a unit in a vehicle goes where the vehicle goes) */ + unit = unit_get(state->unit_index); + if (TEST_FLAG(state->flags, _distributed_actor_rides_bit)) + { + if (unit->object.parent_object_index != state->vehicle_index || + unit->unit.parent_seat_index != state->seat_index) + { + network_objects_set_seat(state->unit_index, state->vehicle_index, state->seat_index); + } + } + else if (unit->object.parent_object_index != NONE) + { + network_objects_set_seat(state->unit_index, NONE, NONE); + } + damage_get_network_state(state->unit_index, &damage); + damage.shield_depleted = TEST_FLAG(state->flags, _distributed_actor_shield_depleted_bit); + damage.shield_charging = TEST_FLAG(state->flags, _distributed_actor_shield_charging_bit); + damage.shield_over_charging = TEST_FLAG(state->flags, _distributed_actor_shield_over_charging_bit); + damage.body_vitality = distributed_vitality_unpack(state->body_vitality); + damage.shield_vitality = distributed_vitality_unpack(state->shield_vitality); + damage_set_network_state(state->unit_index, &damage); + SET_FLAG(unit->unit.flags, _unit_active_camouflaged_bit, + TEST_FLAG(state->flags, _distributed_actor_camouflaged_bit)); + SET_FLAG(unit->unit.flags, _unit_super_camouflaged_bit, + TEST_FLAG(state->flags, _distributed_actor_super_camouflaged_bit)); + unit->unit.active_camouflage = (real)state->active_camouflage / 255.0f; + if (state->team != NO_TEAM) + unit->object.owner_team_index = state->team; + SET_FLAG(unit->unit.flags, _unit_running_blindly_bit, + TEST_FLAG(state->flags, _distributed_actor_running_blindly_bit)); + if (TEST_FLAG(state->flags, _distributed_actor_running_blindly_bit)) + unit->unit.run_blindly_angle = distributed_angle_unpack(state->run_blindly_angle, FALSE); + /* vehicles are placed only by the host's object states (network_objects.c) */ + if (unit->object.parent_object_index == NONE && unit->object.type != _object_type_vehicle) + { + real dx = state->position.x - unit->object.position.x; + real dy = state->position.y - unit->object.position.y; + real dz = state->position.z - unit->object.position.z; + + if (dx * dx + dy * dy + dz * dz > REMOTE_CORRECTION_TOLERANCE * REMOTE_CORRECTION_TOLERANCE && + network_objects_reconcile(state->unit_index, &state->position, &forward, &up, &velocity, NULL, + REMOTE_BLEND_DISTANCE)) + { + distributed_count_correction(); + } + } + return TRUE; +} + +/* Client: plays a sound at the unit's head, as unit_dialogue_update starts +a unit's speech. */ +static void speech_play( + long unit_index, + long sound_definition_index) +{ + struct object_marker marker; + real_point3d position = *global_origin3d; + real_vector3d forward = *global_forward3d; + short node_index = 0; + + if (object_get_marker_by_name(unit_index, "head", &marker, 1)) + { + node_index = marker.node_index; + position = marker.node_matrix.position; + forward = marker.node_matrix.forward; + } + object_impulse_sound_new(unit_index, sound_definition_index, node_index, &position, &forward, 1.0f); +} + +/* ---------- public code */ + +void network_actors_new_game( + void) +{ + host_actor_count = 0; + client_actor_count = 0; +} + +/* host: unit_control calls this when an actor drives its unit */ +void network_actors_note_control( + long unit_index, + struct unit_control_data const *control_data) +{ + struct host_actor *actor; + + if (game_connection() != _game_connection_network_server) + return; + actor = host_actor_for(unit_index); + if (!actor) + return; + actor->noted = TRUE; + actor->control = *control_data; +} + +/* host: an impulse or action for an AI unit, sent in the next few entries */ +static void note_action( + long unit_index, + short action, + real_vector2d const *alignment_vector) +{ + struct host_actor *actor; + + if (game_connection() != _game_connection_network_server) + return; + actor = host_actor_for(unit_index); + if (!actor) + return; + actor->impulse = action; + actor->impulse_sends = IMPULSE_REPEAT_TICKS; + actor->impulse_number = next_event_number(); + actor->impulse_aligned = alignment_vector != NULL; + if (alignment_vector) + actor->impulse_alignment = *alignment_vector; +} + +/* host: unit_start_animation_impulse calls this for an AI unit */ +void network_actors_note_impulse( + long unit_index, + short animation_impulse, + real_vector2d const *alignment_vector) +{ + note_action(unit_index, animation_impulse, alignment_vector); +} + +/* host: unit_melee_attack_begin calls this when an AI unit starts a melee attack */ +void network_actors_note_melee( + long unit_index, + real_vector2d const *alignment_vector) +{ + note_action(unit_index, _actor_action_melee, alignment_vector); +} + +/* host: unit_leap_begin calls this when an AI unit leaps */ +void network_actors_note_leap( + long unit_index, + real_vector2d const *alignment_vector) +{ + note_action(unit_index, _actor_action_leap, alignment_vector); +} + +/* host: unit_dialogue_update calls this when an AI unit starts saying +something (not a pain or death sound, which clients play themselves) */ +void network_actors_note_speech( + long unit_index, + long sound_definition_index) +{ + struct host_actor *actor; + + if (game_connection() != _game_connection_network_server) + return; + actor = host_actor_for(unit_index); + if (!actor) + return; + actor->speech_sound = sound_definition_index; + actor->speech_sends = IMPULSE_REPEAT_TICKS; + actor->speech_number = next_event_number(); +} + +/* host: unit_start_user_animation calls this when an AI unit starts a +custom animation (co-op's events carry them in co-op) */ +void network_actors_note_user_animation( + long unit_index, + long animation_graph_index, + short animation_index, + boolean interpolate) +{ + struct host_actor *actor; + + if (game_connection() != _game_connection_network_server || network_coop_active()) + return; + actor = host_actor_for(unit_index); + if (!actor) + return; + actor->user_animation_graph = animation_graph_index; + actor->user_animation = animation_index; + actor->user_animation_interpolate = interpolate; + actor->user_animation_sends = IMPULSE_REPEAT_TICKS; + actor->user_animation_number = next_event_number(); +} + +/* host: the damage of the AI units taking some, to every client (and once +more when it ends, so a flare goes) */ +static void host_send_damage( + void) +{ + struct distributed_actor_damage_message message; + short count = 0; + short index; + + for (index = 0; index < host_actor_count; index++) + { + struct host_actor *actor = &host_actors[index]; + struct damage_network_state damage; + boolean damaged; + + if (!actor_unit_valid(actor->unit_index)) + continue; + damage_get_network_state(actor->unit_index, &damage); + damaged = damage.current_shield_damage > 0.0f || damage.recent_shield_damage > 0.0f || + damage.current_body_damage > 0.0f || damage.recent_body_damage > 0.0f; + if (!damaged && !actor->damaged) + continue; + actor->damaged = damaged; + message.units[count].unit_index = actor->unit_index; + message.units[count].current_shield_damage = distributed_vitality_pack(damage.current_shield_damage); + message.units[count].recent_shield_damage = distributed_vitality_pack(damage.recent_shield_damage); + message.units[count].current_body_damage = distributed_vitality_pack(damage.current_body_damage); + message.units[count].recent_body_damage = distributed_vitality_pack(damage.recent_body_damage); + if (++count == MAXIMUM_ENTRIES_PER_MESSAGE) + { + distributed_send(&message, _distributed_message_actor_damage, count, + (word)(sizeof(message.header) + count * sizeof(message.units[0])), _distributed_to_clients); + count = 0; + } + } + if (count) + { + distributed_send(&message, _distributed_message_actor_damage, count, + (word)(sizeof(message.header) + count * sizeof(message.units[0])), _distributed_to_clients); + } +} + +/* Host, after each tick: sends each client the AI units driven this tick. +Units near the client's players go every tick, others less often. A unit +with a fresh impulse, action, speech or animation goes to everyone in each +of the next IMPULSE_REPEAT_TICKS ticks, and one starting a one-tick action +goes that tick. A unit no actor drove this tick is dropped from the table, +unless it is fleeing: a unit running blindly moves by itself, without its +actor's control, and dropped it stood running in place on the clients. */ +void network_actors_host_tick( + void) +{ + static struct distributed_actor_state states[MAXIMUM_NETWORK_ACTORS]; + /* sent to every client this tick, whatever its period */ + static boolean urgent[MAXIMUM_NETWORK_ACTORS]; + struct distributed_actor_state_message message; + long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES]; + short machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES); + long now = game_time_get(); + short state_count = 0; + short index; + short machine_number; + + for (index = 0; index < host_actor_count; index++) + { + struct host_actor *actor = &host_actors[index]; + + if (!actor_unit_valid(actor->unit_index) || + (!actor->noted && !TEST_FLAG(unit_get(actor->unit_index)->unit.flags, _unit_running_blindly_bit))) + { + host_actors[index--] = host_actors[--host_actor_count]; + continue; + } + actor_state_from_unit(actor, &states[state_count]); + urgent[state_count] = states[state_count].impulse != NO_IMPULSE || states[state_count].speech_sound != NONE || + states[state_count].user_animation_graph != NONE || + (states[state_count].control_flags & ~actor->sent_control_flags & ONE_SHOT_CONTROL_FLAGS) != 0; + actor->sent_control_flags = states[state_count].control_flags; + state_count++; + actor->noted = FALSE; + if (actor->impulse_sends > 0) + actor->impulse_sends--; + if (actor->speech_sends > 0) + actor->speech_sends--; + if (actor->user_animation_sends > 0) + actor->user_animation_sends--; + } + host_send_damage(); + for (machine_number = 0; machine_number < machine_count; machine_number++) + { + long machine_index = machine_indices[machine_number]; + short count = 0; + + for (index = 0; index < state_count; index++) + { + struct distributed_actor_state const *state = &states[index]; + short period = network_objects_send_period(machine_index, &state->position); + + if (!urgent[index] && (now + DATUM_INDEX_TO_ABSOLUTE_INDEX(state->unit_index)) % period != 0) + continue; + message.states[count++] = *state; + if (count == MAXIMUM_ENTRIES_PER_MESSAGE || count == DATAGRAM_ENTRIES(struct distributed_actor_state)) + { + distributed_send_to_machine(machine_index, &message, _distributed_message_actor_states, count, + (word)(sizeof(message.header) + count * sizeof(struct distributed_actor_state))); + count = 0; + } + } + if (count) + { + distributed_send_to_machine(machine_index, &message, _distributed_message_actor_states, count, + (word)(sizeof(message.header) + count * sizeof(struct distributed_actor_state))); + } + } +} + +word network_actors_damage_entry_size( + void) +{ + return sizeof(struct distributed_actor_damage); +} + +/* client: the damage the host's AI units are taking */ +void network_actors_handle_damage( + void const *entries, + short count) +{ + struct distributed_actor_damage const *units = entries; + short index; + + for (index = 0; index < count; index++) + { + struct damage_network_state damage; + + if (!distributed_object_index_valid(units[index].unit_index) || + !network_objects_client_has(units[index].unit_index) || !actor_unit_valid(units[index].unit_index)) + { + continue; + } + damage_get_network_state(units[index].unit_index, &damage); + damage.current_shield_damage = distributed_vitality_unpack(units[index].current_shield_damage); + damage.recent_shield_damage = distributed_vitality_unpack(units[index].recent_shield_damage); + damage.current_body_damage = distributed_vitality_unpack(units[index].current_body_damage); + damage.recent_body_damage = distributed_vitality_unpack(units[index].recent_body_damage); + damage_set_network_state(units[index].unit_index, &damage); + } +} + +word network_actors_entry_size( + void) +{ + return sizeof(struct distributed_actor_state); +} + +/* client: entries from the host */ +void network_actors_handle_states( + void const *entries, + short count) +{ + struct distributed_actor_state const *states = entries; + long now = game_time_get(); + short index; + + for (index = 0; index < count; index++) + actor_state_apply(&states[index], now); +} + +/* Client: called at the point in the tick where the host runs the AI. +Drives each AI unit with the last control the host sent. */ +void network_actors_drive( + void) +{ + long now = game_time_get(); + short index; + + for (index = 0; index < client_actor_count; index++) + { + struct client_actor *actor = &client_actors[index]; + + if (!actor_unit_valid(actor->unit_index) || now - actor->received_time > CONTROL_HELD_TICKS) + { + if (actor->driven && object_try_and_get_and_verify_type(actor->unit_index, _object_mask_unit)) + unit_set_actively_controlled(actor->unit_index, FALSE); + client_actors[index--] = client_actors[--client_actor_count]; + continue; + } + /* vehicles follow the host's transform instead (network_objects.c) */ + if (object_get(actor->unit_index)->object.type == _object_type_vehicle) + continue; + if (!actor->driven) + { + unit_set_actively_controlled(actor->unit_index, TRUE); + actor->driven = TRUE; + } + unit_control(actor->unit_index, &actor->control); + if (actor->impulse != NONE && actor->impulse_number != actor->played_impulse_number) + { + real_vector2d *alignment = actor->impulse_aligned ? &actor->impulse_alignment : NULL; + + if (actor->impulse == _actor_action_melee) + unit_melee_attack_begin(actor->unit_index, FALSE, alignment); + else if (actor->impulse == _actor_action_leap) + unit_leap_begin(actor->unit_index, alignment); + else + unit_start_animation_impulse(actor->unit_index, actor->impulse, alignment); + actor->played_impulse_number = actor->impulse_number; + actor->impulse = NONE; + } + if (actor->speech_sound != NONE) + { + speech_play(actor->unit_index, actor->speech_sound); + actor->played_speech_number = actor->speech_number; + actor->speech_sound = NONE; + } + if (actor->user_animation_graph != NONE) + { + unit_port_play_user_animation(actor->unit_index, actor->user_animation_graph, actor->user_animation, + actor->user_animation_interpolate, 0); + actor->played_user_animation_number = actor->user_animation_number; + actor->user_animation_graph = NONE; + } + } +} diff --git a/port/linux/game/network_coop.c b/port/linux/game/network_coop.c new file mode 100644 index 00000000..0c189e94 --- /dev/null +++ b/port/linux/game/network_coop.c @@ -0,0 +1,2998 @@ +/* +NETWORK_COOP.C + +Campaign co-op over the network (port/linux/NETCODE.md). + +A network game on a campaign map, with no game engine running, is co-op. +Only the host runs the map's scripts (game.c), so everything the scripts do +that clients need to see is sent from this file: + +- Presentation, every tick: whether a cinematic is playing, the letterbox, + the camera, the screen fade, the HUD settings the scripts control (what + is shown, the mission timer), the players' maximum vitality, and the + cutscene skip vote. A client plays them one a tick by the host's clock, a + couple of ticks behind the newest, so the camera keeps an even pace. It + starts and stops its own cinematic to match, and looks through the host's + camera meanwhile. If the host goes quiet for two seconds, the client ends + the cinematic so its players aren't stuck. +- Events, which happen once: script sounds (dialogue, music, ambience), + chapter titles, help and objective text, "Checkpoint" messages, screen + shake, nav points, and custom animations on units and scenery. Each is + sent in three ticks' messages in case one is lost, and numbered so a + client applies each exactly once, in order. The engine calls the + network_coop_note_ functions (network_coop.h) to queue them. +- Devices (doors, elevators, switches, lights). A device moves toward its + device group's value (devices.c). On a client nothing sets those values + but this file: doors a player walks up to and switches a player uses are + decided by the host (the player's action is relayed to it). The host + sends a group's value for three ticks when it changes, plus a few groups + every tick in rotation, those that have moved since the map loaded more + often. A scenario group is identified by its index, which is the same on + every machine; a device's own group by the device. +- Named objects (scenery and devices) the scripts create or destroy. + network_objects.c already handles units, vehicles, weapons and equipment. + Twice a second the host sends which object names currently exist. A + client that sees the same difference twice in a row, on the same BSP, + creates or deletes its copy. +- State a late joiner would otherwise lack, resent every two seconds and on + the tick a machine joins (host_resend): where scripted scenery and + machines have moved, how objects look (permutations, scale), what the + scripts have attached, the looping sounds playing, the full-screen + cinematic effect and the nav points; on a join also the objective, the + object names and every device that has moved. +- The view of a client with nobody to spectate yet: the host's view from + behind, eased so it doesn't jerk. + +The host also makes up for scripts written for one player: vitality they +set on player0 is set on every player (a10's shields), and a Pelican's +Warthog drop brings more Warthogs in a larger game. + +Skipping a cutscene is a vote. Pressing the skip key during a skippable +cutscene votes (a client sends its vote to the host every tick), and the +host skips once more than half the machines have voted. The skip itself is +the campaign's: the host reverts to the state saved when the cutscene began +and the script carries on past it. Clients catch up through the object, +device and name syncs. The host keeps its clock running through the revert, +because the netcode needs it to only go forward (network_coop_skip_reverted). + +Objects (scenery and devices) that aren't synced by network_objects.c are +found on a client by their scenario name, or, unnamed, by their object +index and tag, since the map placed them at the same index everywhere. +*/ + +/* ---------- headers */ + +#include "cseries.h" +#include "cseries/errors.h" +#include "cache/cache_files.h" +#include "camera/camera_scripting.h" +#include "camera/director.h" +#include "camera/observer.h" +#include "cutscene/cinematics.h" +#include "devices/devices.h" +#include "effects/effect_definitions.h" +#include "effects/player_effects.h" +#include "game/game.h" +#include "game/game_allegiance.h" +#include "game/game_engine.h" +#include "game/players.h" +#include "game/player_queues_new.h" +#include "hs/hs.h" +#include "hs/hs_library_external.h" +#include "interface/hud.h" +#include "interface/hud_definitions.h" +#include "interface/hud_messaging.h" +#include "interface/hud_unit.h" +#include "interface/hud_weapon.h" +#include "main/main.h" +#include "models/model_animation_definitions.h" +#include "models/model_definitions.h" +#include "models/models.h" +#include "objects/object_definitions.h" +#include "objects/objects.h" +#include "objects/object_types.h" +#include "objects/damage.h" +#include "objects/scenery.h" +#include "physics/collisions.h" +#include "rasterizer/rasterizer_cinematics.h" +#include "saved games/game_state.h" +#include "scenario/scenario.h" +#include "scenario/scenario_definitions.h" +#include "sound/game_sound.h" +#include "sound/sound_manager.h" +#include "sound/sound_definitions.h" +#include "units/units.h" +#include "coop_enemies.h" +#include "coop_spectate.h" +#include "network_coop.h" +#include "network_distributed.h" + +/* ---------- constants */ + +/* A joining player with nobody to spectate yet watches the host's view +from this far behind and above the host's eyes, easing a fraction of the way +toward it each tick. */ +#define WATCH_HOST_DISTANCE 3.0f +#define WATCH_HOST_HEIGHT 1.0f +#define WATCH_HOST_FOLLOW 0.2f + +/* how often the host sends again the state a client that joined since, or +missed a message, would lack (host_resend) */ +#define OBJECT_REFRESH_TICKS (2 * TICKS_PER_SECOND) + +/* the screen effect a client takes from its host: the rasterizer's +convolution types (none, blur, warp) and video overbright modes +(rasterizer_xbox_screen_effect.c), and at most this many extra convolution +passes, more than any script asks for */ +#define SCREEN_EFFECT_CONVOLUTION_TYPES 3 +#define SCREEN_EFFECT_OVERBRIGHT_MODES 3 +#define SCREEN_EFFECT_MAXIMUM_EXTRA_PASSES 8 + +enum +{ + /* the farthest an object the cutscene camera films can be from it + (camera_object_get) */ + CAMERA_OBJECT_DISTANCE = 8, + /* a client ends the cinematic after this long without hearing from the host */ + PRESENTATION_SILENCE_TICKS = 2 * TICKS_PER_SECOND, + /* A client plays the host's presentations (its cutscene camera, fades, + letterbox) one a tick, about this many ticks behind the newest it has, + so they keep an even pace however unevenly the network delivers them. */ + PRESENTATION_DELAY_TICKS = 2, + PRESENTATION_BUFFER_COUNT = 8, + /* the field of view (radians) is sent as a word, scaled by this */ + FIELD_OF_VIEW_SCALE = 10000, + + MAXIMUM_QUEUED_EVENTS = 128, + /* each event is sent in this many ticks' messages */ + EVENT_SENDS = 3, + + /* the size of devices.c's device group array */ + MAXIMUM_DEVICE_GROUPS = 1024, + /* a changed group is sent in this many ticks' messages */ + DEVICE_GROUP_SENDS = 3, + /* unchanged groups sent each tick, in rotation */ + DEVICE_GROUP_REFRESHES_PER_TICK = 8, + + OBJECT_NAMES_INTERVAL_TICKS = TICKS_PER_SECOND / 2, + OBJECT_NAME_BYTES = MAXIMUM_OBJECT_NAMES_PER_SCENARIO / 8, + /* the types network_objects.c syncs; the object names sync skips them */ + NETWORKED_OBJECT_TYPES = _object_mask_biped | _object_mask_vehicle | _object_mask_weapon | _object_mask_equipment, + + /* the host forgets a client's skip vote it hasn't heard again for this long */ + SKIP_VOTE_HELD_TICKS = TICKS_PER_SECOND, + /* After a skip the cutscene stays skippable until the script reaches + cinematic_skip_stop; votes still in flight must not skip it again. */ + SKIP_COOLDOWN_TICKS = 2 * TICKS_PER_SECOND, + + /* a Pelican's Warthog drop: one Warthog for every this many players, + at most this many in all */ + PLAYERS_PER_DROPPED_VEHICLE = 4, + MAXIMUM_DROPPED_VEHICLES = 5, +}; + +/* distributed_coop_event.kind */ +enum +{ + _coop_event_sound, + _coop_event_title, + _coop_event_hud, + _coop_event_player_effect, + _coop_event_nav_point, + _coop_event_unit_animation, + _coop_event_scenery_animation, + _coop_event_effect, + _coop_event_attach, + /* the host skipped the cutscene: a client stops its dialogue */ + _coop_event_cutscene_skipped, + /* a unit opened (value TRUE) or closed: a dropship's doors */ + _coop_event_unit_open, +}; + +/* distributed_coop_event.type of an effect: at a cutscene flag (value), or on +an object's marker (value: its index in the object's model's markers) */ +enum +{ + _coop_effect_at_flag, + _coop_effect_on_marker, +}; + +/* distributed_coop_event.type of an attach event */ +enum +{ + _coop_attach, + _coop_detach, +}; + +/* which of a device's two groups an entry is */ +enum +{ + _device_group_role_power, + _device_group_role_position, +}; + +/* distributed_coop_presentation.flags */ +enum +{ + _presentation_cinematic_bit = 0, + _presentation_letterbox_bit, + _presentation_fading_out_bit, + _presentation_skippable_bit, + _presentation_show_hud_bit, + _presentation_show_help_text_bit, + _presentation_timer_paused_bit, + _presentation_timer_enabled_bit, +}; + +/* ---------- structures */ + +struct distributed_coop_presentation +{ + byte flags; + byte fade_color[3]; + short fade_ticks; + /* ticks since the fade began, capped at SHORT_MAX */ + short fade_elapsed; + /* the host's game time when the fade began; a new value means a new fade */ + long fade_start_time; + real_point3d camera_position; + struct distributed_vector camera_forward; + struct distributed_vector camera_up; + word camera_field_of_view; + /* the skip vote: machines that voted, out of how many */ + byte skip_votes; + byte skip_voters; + /* the scripts' HUD settings (hud_unit.c, hud_weapon.c) */ + long unit_hud_flags; + long weapon_hud_flags; + /* the script timer (hud_messaging.c); the clocks agree, so its + reference time means the same on every machine */ + long timer_reference_time; + short timer_ticks; + short timer_flash_cutoff; + short timer_x; + short timer_y; + short timer_corner; + /* whether the scripts have the host's camera (camera_control): else a + cutscene leaves it with the player, and a client spectates */ + byte camera_scripted; + /* whether the scripts hold the players' controls (player_enable_input) */ + byte input_disabled; + /* the object the camera films (camera_object_get) and the camera's + offset from it: a client puts its camera by its own copy, which its + timing may have a tick from the host's */ + long camera_object_index; + real_vector3d camera_object_offset; + /* whether the scripts' screen shake is running on the host. A client + whose shake outlives it (the stop was skipped with a cutscene, or lost) + ends its own. */ + byte scripted_shake; + /* whether the scripts have set the players' maximum vitality, and to + what (network_coop_set_players_vitality) */ + byte players_vitality_set; + byte pad[2]; + real players_maximum_body_vitality; + real players_maximum_shield_vitality; + /* which teams are allies and friends (game_allegiance.c): the scripts + make them, so a client's would leave the marines its enemies */ + unsigned long ally_teams[GAME_ALLEGIANCE_BITVECTOR_LONGS]; + unsigned long friendly_teams[GAME_ALLEGIANCE_BITVECTOR_LONGS]; +}; + +struct distributed_coop_presentation_message +{ + struct distributed_message_header header; + struct distributed_coop_presentation presentation; +}; + +/* Something that happens once. Which fields mean what depends on the kind: + + sound type: _coop_sound_ kind; tag_index; object_index (the source, or NONE); reals[0]: scale + title value: title index; reals[0]: delay in seconds + hud type: _coop_hud_ kind; value + player_effect type: _coop_player_effect_ kind; reals: the function's arguments + nav_point type: _coop_nav_point_ kind; value: nav index (NONE to deactivate); + target: team or unit; object_index: cutscene flag or object; reals[0]: offset + unit_animation object_index: unit; tag_index: graph (NONE: stop); value: animation; + frame; interpolate + scenery_animation object_index, name_index, definition_index: the scenery; tag_index: graph; + value: animation; frame */ +struct distributed_coop_event +{ + byte kind; + byte type; + word number; + short value; + short frame; + short name_index; + byte interpolate; + byte pad; + long object_index; + long target; + long tag_index; + long definition_index; + real reals[3]; +}; + +struct distributed_coop_events_message +{ + struct distributed_message_header header; + struct distributed_coop_event events[MAXIMUM_QUEUED_EVENTS]; +}; + +/* A device group's state. group_index is set for a scenario group. For a +device's own group it is NONE, and the device is found by name_index, or +(unnamed) by object_index and definition_index. */ +struct distributed_coop_device_group +{ + short group_index; + short name_index; + long object_index; + long definition_index; + byte role; + byte flags; + /* counts the host's immediate sets (wraps at 256) */ + byte snaps; + /* sent because it changed, rather than in the rotation */ + byte changed; + real value; +}; + +#define MAXIMUM_DEVICE_GROUPS_PER_MESSAGE 64 + +struct distributed_coop_device_groups_message +{ + struct distributed_message_header header; + struct distributed_coop_device_group groups[MAXIMUM_DEVICE_GROUPS]; +}; + +/* one bit per scenario object name: whether the host has that object */ +struct distributed_coop_object_names +{ + short structure_bsp_index; + short name_count; + byte present[OBJECT_NAME_BYTES]; +}; + +struct distributed_coop_object_names_message +{ + struct distributed_message_header header; + struct distributed_coop_object_names names; +}; + +/* Where one of the host's scenery or machines is. Scripts move them (a +cutscene's drop pod) and nothing else tells a client. Found as the +scenery animations' are (object_find). */ +struct distributed_coop_object_transform +{ + short name_index; + short pad; + long object_index; + long definition_index; + real_point3d position; + struct distributed_vector forward; + struct distributed_vector up; +}; + +#define MAXIMUM_OBJECT_TRANSFORMS_PER_MESSAGE 64 +#define COOP_MOVED_OBJECTS (_object_mask_scenery | _object_mask_machine) + +struct distributed_coop_object_transforms_message +{ + struct distributed_message_header header; + struct distributed_coop_object_transform transforms[MAXIMUM_OBJECT_TRANSFORMS_PER_MESSAGE]; +}; + +/* How one of the host's objects looks: the permutation of each region of +its model (NONE for none) and its scale. The scripts change them (a +helmet off, a ship scaled) and so does damage. */ +struct distributed_coop_object_look +{ + short name_index; + short pad; + long object_index; + long definition_index; + byte region_permutations[MAXIMUM_REGIONS_PER_OBJECT]; + real scale; +}; + +#define MAXIMUM_OBJECT_LOOKS_PER_MESSAGE 64 + +struct distributed_coop_object_looks_message +{ + struct distributed_message_header header; + struct distributed_coop_object_look looks[MAXIMUM_OBJECT_LOOKS_PER_MESSAGE]; +}; + +/* a client's vote to skip the cinematic, sent every tick while it stands */ +struct distributed_coop_skip_vote +{ + byte voted; + byte pad[3]; +}; + +struct distributed_coop_skip_vote_message +{ + struct distributed_message_header header; + struct distributed_coop_skip_vote vote; +}; + +/* ---------- globals */ + +/* client: the cinematic it started to match the host's */ +static struct +{ + boolean cinematic_started; + /* looking through the host's camera: its scripted cutscene camera, or + its view for a player with nothing else to look at */ + boolean host_camera; + /* the eased view behind the host, used while there is nothing else to + watch (watching_host_valid is FALSE until it is first set) */ + boolean watching_host_valid; + real_point3d watching_host_position; + real_vector3d watching_host_forward; + long heard_time; + long fade_start_time; + /* the host's presentations as they arrived, by the host's tick; the + newest tick heard and the tick last played (nothing is played until + one has been heard) */ + struct distributed_coop_presentation buffered[PRESENTATION_BUFFER_COUNT]; + long buffered_times[PRESENTATION_BUFFER_COUNT]; + boolean heard_any; + boolean playing; + long newest_time; + long played_time; +} coop_presentation; + +/* host: events still to be sent; client: the last one applied */ +static struct +{ + struct distributed_coop_event events[MAXIMUM_QUEUED_EVENTS]; + short sends[MAXIMUM_QUEUED_EVENTS]; + short count; + word next_number; + word applied_number; + boolean applied_any; +} coop_events; + +/* host: each device group as last sent, indexed by group */ +static struct +{ + /* false until the first tick, which records the map's starting state */ + boolean started; + real values[MAXIMUM_DEVICE_GROUPS]; + byte flags[MAXIMUM_DEVICE_GROUPS]; + byte snaps[MAXIMUM_DEVICE_GROUPS]; + /* ticks this group is still to be sent as a change */ + byte sends[MAXIMUM_DEVICE_GROUPS]; + /* immediate sets so far (devices.c calls network_coop_note_device_snap) */ + byte snap_counts[MAXIMUM_DEVICE_GROUPS]; + /* whether this group has changed since the map loaded */ + boolean moved[MAXIMUM_DEVICE_GROUPS]; + short refresh_next, moved_refresh_next; + struct distributed_coop_device_groups_message message; +} host_devices; + +/* The players' maximum vitality, once the scripts have set a player's: on +the host as they set it, on a client as the host's presentation has it. +Every player's unit is kept at it, a new one included. */ +static struct +{ + boolean set; + real maximum_body; + real maximum_shield; +} players_vitality; + +/* host: the client machines it had last tick. On the tick a new one +appears, and every OBJECT_REFRESH_TICKS, the state a client that joined +since would lack is sent again (refresh), and on the join every device that +has moved since the map loaded too (joined). */ +static struct +{ + long machines[HALO_PORT_MAXIMUM_NETWORK_MACHINES]; + short machine_count; + boolean joined; + boolean refresh; +} host_resend; + +/* client: the host's snap count last seen for each of its groups */ +static struct +{ + byte snaps[MAXIMUM_DEVICE_GROUPS]; + boolean seen[MAXIMUM_DEVICE_GROUPS]; +} client_devices; + +/* client: the script sounds (impulse ones: dialogue) it played lately, +which a skipped cutscene stops */ +enum +{ + CLIENT_SCRIPT_SOUND_COUNT = 16, +}; + +static struct +{ + long definition_indices[CLIENT_SCRIPT_SOUND_COUNT]; + short next; +} client_script_sounds; + +/* client: the object names that differed from the host's last time */ +static byte client_names_differing[OBJECT_NAME_BYTES]; + +/* The cutscene skip vote. The host records when it last heard from each +client that is showing the cutscene, and when that client last voted. A +client keeps the host's latest tally for display. */ +static struct +{ + boolean voted; + boolean offered; + short votes; + short voters; + long client_heard_times[HALO_PORT_MAXIMUM_NETWORK_MACHINES]; + long client_vote_times[HALO_PORT_MAXIMUM_NETWORK_MACHINES]; + /* host: no voting before this game time (set after a skip) */ + long cooldown_until; + /* host: the vote passed and main_skip_cinematic was called once */ + boolean requested; + /* host: the save the script makes as the cutscene becomes skippable is + written (a skip reverts to it) */ + boolean skip_save_written; +} skip_vote; + +/* ---------- prototypes */ + +static void client_play_presentation(void); +/* unit_scripting_commands.c */ +void unit_scripting_set_current_vitality_of(long unit_index, real body_vitality, real shield_vitality); + +/* ---------- private code */ + +static boolean coop_game( + void) +{ + return network_coop_active(); +} + +static boolean coop_host( + void) +{ + return game_connection() == _game_connection_network_server && coop_game(); +} + +static boolean coop_client( + void) +{ + return game_connection() == _game_connection_network_client && coop_game(); +} + +/* host: sends count entries of entry_size in message to every client */ +static void send_to_clients( + void *message, + byte type, + short count, + word entry_size) +{ + distributed_send(message, type, count, (word)(sizeof(struct distributed_message_header) + count * entry_size), + _distributed_to_clients); +} + +/* host, each tick: whether a client machine joined since the last, and so +whether to resend this tick (host_resend) */ +static void host_resend_update( + void) +{ + long machines[HALO_PORT_MAXIMUM_NETWORK_MACHINES]; + short count = distributed_client_machines(machines, HALO_PORT_MAXIMUM_NETWORK_MACHINES); + short index, known; + + host_resend.joined = FALSE; + for (index = 0; index < count && !host_resend.joined; index++) + { + for (known = 0; known < host_resend.machine_count && host_resend.machines[known] != machines[index]; known++) + ; + host_resend.joined = known == host_resend.machine_count; + } + csmemcpy(host_resend.machines, machines, count * sizeof(machines[0])); + host_resend.machine_count = count; + host_resend.refresh = host_resend.joined || game_time_get() % OBJECT_REFRESH_TICKS == 0; + if (host_resend.joined) + error(_error_silent, "co-op: a machine joined; resending the game's state"); +} + +/* whether the object is a player's unit or carries one (scripts never delete those) */ +static boolean object_holds_player( + long object_index) +{ + long child_index; + + if (player_index_from_unit_index(object_index) != NONE) + return TRUE; + for (child_index = object_get(object_index)->object.first_child_object_index; child_index != NONE; + child_index = object_get(child_index)->object.next_object_index) + { + if (object_holds_player(child_index)) + return TRUE; + } + + return FALSE; +} + +/* the object type a scenario object name refers to, or NONE */ +static short object_name_type( + short name_index) +{ + struct scenario *scenario = global_scenario_get(); + + if (name_index < 0 || name_index >= scenario->object_names.count) + return NONE; + + return TAG_BLOCK_GET_ELEMENT(&scenario->object_names, name_index, struct scenario_object_name)->runtime_object_type; +} + +/* Client: finds the local copy of an object the host named, of one of the +types in type_mask, and returns its index (or NONE). An unnamed object sits +at the same object index on every machine; if its salt differs here, +matching the absolute index is enough, as long as the tag matches too. */ +static long object_find( + short name_index, + long object_index, + long definition_index, + unsigned long type_mask) +{ + struct object_datum *object; + + if (name_index != NONE) + object_index = object_index_from_name_index(name_index); + if (object_index == NONE) + return NONE; + object = object_try_and_get_and_verify_type(object_index, type_mask); + if (!object && name_index == NONE) + { + struct object_iterator iterator; + struct object_datum *candidate; + + object_iterator_new(&iterator, type_mask, 0); + while (!object && (candidate = object_iterator_next(&iterator)) != NULL) + { + if ((iterator.index & 0xFFFF) == (object_index & 0xFFFF)) + { + object = candidate; + object_index = iterator.index; + } + } + } + if (!object || object->definition_index != definition_index || + (name_index == NONE && object->object.name_index != NONE)) + { + return NONE; + } + + return object_index; +} + +/* host: a new event, filled in by the caller; NULL if it isn't wanted or the queue is full */ +static struct distributed_coop_event *event_new( + byte kind) +{ + struct distributed_coop_event *event; + + if (!coop_host()) + return NULL; + if (coop_events.count == MAXIMUM_QUEUED_EVENTS) + { + error(_error_silent, "co-op: event queue full; an event of kind %d is not sent", kind); + return NULL; + } + event = &coop_events.events[coop_events.count]; + csmemset(event, 0, sizeof(*event)); + event->kind = kind; + event->number = ++coop_events.next_number; + event->object_index = NONE; + event->target = NONE; + event->tag_index = NONE; + event->definition_index = NONE; + event->name_index = NONE; + coop_events.sends[coop_events.count] = 0; + coop_events.count++; + + return event; +} + +static void host_send_events( + void) +{ + struct distributed_coop_events_message message; + short index; + + if (!coop_events.count) + return; + csmemcpy(message.events, coop_events.events, coop_events.count * sizeof(message.events[0])); + send_to_clients(&message, _distributed_message_coop_events, coop_events.count, sizeof(message.events[0])); + /* drop the ones sent EVENT_SENDS times, keeping the rest in order */ + for (index = 0; index < coop_events.count; ) + { + if (++coop_events.sends[index] < EVENT_SENDS) + { + index++; + continue; + } + coop_events.count--; + csmemmove(&coop_events.events[index], &coop_events.events[index + 1], + (coop_events.count - index) * sizeof(coop_events.events[0])); + csmemmove(&coop_events.sends[index], &coop_events.sends[index + 1], + (coop_events.count - index) * sizeof(coop_events.sends[0])); + } +} + +/* Fills in the group's value, flags and snap count. Returns FALSE if the +group doesn't exist. */ +static boolean device_group_state( + short group_index, + struct distributed_coop_device_group *entry) +{ + word flags; + boolean runtime; + + if (group_index < 0 || group_index >= MAXIMUM_DEVICE_GROUPS || + !device_group_network_get(group_index, &entry->value, &flags, &runtime)) + { + return FALSE; + } + entry->flags = (byte)flags; + entry->snaps = host_devices.snap_counts[group_index]; + entry->changed = FALSE; + + return TRUE; +} + +/* Host: lists every device group, scenario groups first, then each device's +own groups. group_indices gets each entry's group index. Returns the count. */ +static short device_group_entries( + struct distributed_coop_device_group *entries, + short *group_indices) +{ + struct scenario *scenario = global_scenario_get(); + struct object_iterator iterator; + struct device_datum *device; + short count = 0, group_index; + + for (group_index = 0; group_index < scenario->device_groups.count && count < MAXIMUM_DEVICE_GROUPS; group_index++) + { + struct distributed_coop_device_group *entry = &entries[count]; + + entry->group_index = group_index; + entry->name_index = NONE; + entry->object_index = NONE; + entry->definition_index = NONE; + entry->role = _device_group_role_power; + if (device_group_state(group_index, entry)) + group_indices[count++] = group_index; + } + object_iterator_new(&iterator, _object_mask_device, 0); + while ((device = object_iterator_next(&iterator)) != NULL) + { + short groups[2]; + short role; + + groups[_device_group_role_power] = device->device.power_group_index; + groups[_device_group_role_position] = device->device.position_group_index; + for (role = 0; role < NUMBEROF(groups) && count < MAXIMUM_DEVICE_GROUPS; role++) + { + struct distributed_coop_device_group *entry = &entries[count]; + real value; + word flags; + boolean runtime; + + /* scenario groups were listed above */ + if (!device_group_network_get(groups[role], &value, &flags, &runtime) || !runtime) + continue; + entry->group_index = NONE; + entry->name_index = device->object.name_index; + entry->object_index = iterator.index; + entry->definition_index = device->definition_index; + entry->role = (byte)role; + if (device_group_state(groups[role], entry)) + group_indices[count++] = groups[role]; + } + } + + return count; +} + +/* client: the local group a host entry refers to, or NONE */ +static short device_group_find( + struct distributed_coop_device_group const *entry) +{ + struct device_datum *device; + long device_index; + real value; + word flags; + boolean runtime; + short group_index; + + if (entry->group_index != NONE) + { + group_index = entry->group_index; + return group_index >= 0 && group_index < global_scenario_get()->device_groups.count && + device_group_network_get(group_index, &value, &flags, &runtime) && !runtime ? group_index : NONE; + } + device_index = object_find(entry->name_index, entry->object_index, entry->definition_index, _object_mask_device); + if (device_index == NONE) + return NONE; + device = object_get_and_verify_type(device_index, _object_mask_device); + switch (entry->role) + { + case _device_group_role_power: group_index = device->device.power_group_index; break; + case _device_group_role_position: group_index = device->device.position_group_index; break; + default: return NONE; + } + + return group_index >= 0 && group_index < MAXIMUM_DEVICE_GROUPS && + device_group_network_get(group_index, &value, &flags, &runtime) && runtime ? group_index : NONE; +} + +/* host: sends the groups that changed, plus the next few in each rotation: +one through every group, and a quicker one through the groups that have +moved since the map loaded. A late joiner loaded the same map, so those are +the ones it lacks, and a level's few moved devices (a light bridge) reach it +within a tick or two instead of after a pass through hundreds. */ +static void host_send_device_groups( + void) +{ + static short group_indices[MAXIMUM_DEVICE_GROUPS]; + struct distributed_coop_device_group *entries = host_devices.message.groups; + short count = device_group_entries(entries, group_indices); + short moved_count = 0, moved_index = 0; + short index, sent = 0; + + for (index = 0; index < count; index++) + { + struct distributed_coop_device_group const *entry = &entries[index]; + short group_index = group_indices[index]; + + if (!host_devices.started || + entry->value != host_devices.values[group_index] || + entry->flags != host_devices.flags[group_index] || + entry->snaps != host_devices.snaps[group_index]) + { + host_devices.values[group_index] = entry->value; + host_devices.flags[group_index] = entry->flags; + host_devices.snaps[group_index] = entry->snaps; + /* clients loaded the same map, so the starting state isn't sent */ + if (host_devices.started) + { + host_devices.sends[group_index] = DEVICE_GROUP_SENDS; + host_devices.moved[group_index] = TRUE; + } + } + if (host_devices.moved[group_index]) + moved_count++; + } + if (host_devices.refresh_next >= count) + host_devices.refresh_next = 0; + if (host_devices.moved_refresh_next >= moved_count) + host_devices.moved_refresh_next = 0; + for (index = 0; index < count; index++) + { + struct distributed_coop_device_group *entry = &entries[index]; + short group_index = group_indices[index]; + boolean refresh = (index - host_devices.refresh_next + count) % count < DEVICE_GROUP_REFRESHES_PER_TICK; + + if (host_devices.moved[group_index]) + { + refresh = refresh || host_resend.joined || + (moved_index - host_devices.moved_refresh_next + moved_count) % moved_count < DEVICE_GROUP_REFRESHES_PER_TICK; + moved_index++; + } + entry->changed = host_devices.sends[group_index] != 0; + if (entry->changed) + host_devices.sends[group_index]--; + else if (!refresh) + continue; + entries[sent++] = *entry; + } + host_devices.started = TRUE; + host_devices.refresh_next = count ? (short)((host_devices.refresh_next + DEVICE_GROUP_REFRESHES_PER_TICK) % count) : 0; + host_devices.moved_refresh_next = moved_count ? + (short)((host_devices.moved_refresh_next + DEVICE_GROUP_REFRESHES_PER_TICK) % moved_count) : 0; + /* (in messages of at most MAXIMUM_DEVICE_GROUPS_PER_MESSAGE, since a + message counts its entries in a byte: each moved to the front, where the + message's entries begin, over those already sent) */ + for (index = 0; index < sent; index += MAXIMUM_DEVICE_GROUPS_PER_MESSAGE) + { + short message_count = (short)MIN(sent - index, MAXIMUM_DEVICE_GROUPS_PER_MESSAGE); + + if (index > 0) + csmemcpy(entries, &entries[index], message_count * sizeof(entries[0])); + send_to_clients(&host_devices.message, _distributed_message_coop_device_groups, message_count, + sizeof(entries[0])); + } +} + +/* Where one of the host's devices is: its position (open, closed, how far +up an elevator has gone) and power, which the device groups' values only +aim it at. Found as the scenery animations' are (object_find). */ +struct distributed_coop_device_state +{ + short name_index; + byte moving; + byte pad; + long object_index; + long definition_index; + real position; + real power; +}; + +#define MAXIMUM_DEVICE_STATES_PER_MESSAGE 64 + +struct distributed_coop_device_states_message +{ + struct distributed_message_header header; + struct distributed_coop_device_state states[MAXIMUM_DEVICE_STATES_PER_MESSAGE]; +}; + +/* a client puts its device where the host's is when the host's is at rest +there, or when the two are this far apart while it moves (of the device's +travel, 0 to 1) */ +#define DEVICE_POSITION_TOLERANCE 0.03f + +/* host: each device's position and power as last sent, by its absolute +index, and whether they have changed since the map placed it */ +static struct +{ + long object_index; + boolean moved; + real position; + real power; +} host_sent_devices[MAXIMUM_OBJECTS_PER_MAP]; + +/* host: the structure BSP each client player's machine last said it has, +NONE before its first input */ +static short host_player_structure_bsps[MAXIMUM_TRACKED_PLAYERS]; + +/* host, each tick: the devices that moved or changed power (and, with the +resent state, all that ever have) */ +static void host_send_device_states( + void) +{ + struct distributed_coop_device_states_message message; + struct object_iterator iterator; + struct device_datum *device; + short count = 0; + + object_iterator_new(&iterator, _object_mask_device, 0); + while ((device = object_iterator_next(&iterator)) != NULL) + { + short absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.index); + struct distributed_coop_device_state *state; + boolean changed; + + if (absolute_index < 0 || absolute_index >= MAXIMUM_OBJECTS_PER_MAP) + continue; + if (host_sent_devices[absolute_index].object_index != iterator.index) + { + /* (as the map placed it, on every machine) */ + host_sent_devices[absolute_index].object_index = iterator.index; + host_sent_devices[absolute_index].moved = FALSE; + host_sent_devices[absolute_index].position = device->device.position; + host_sent_devices[absolute_index].power = device->device.power; + continue; + } + changed = host_sent_devices[absolute_index].position != device->device.position || + host_sent_devices[absolute_index].power != device->device.power; + if (!changed && !(host_resend.refresh && host_sent_devices[absolute_index].moved)) + continue; + host_sent_devices[absolute_index].moved = TRUE; + host_sent_devices[absolute_index].position = device->device.position; + host_sent_devices[absolute_index].power = device->device.power; + state = &message.states[count++]; + state->name_index = device->object.name_index; + state->moving = (byte)(device->device.position_velocity != 0.0f); + state->pad = 0; + state->object_index = iterator.index; + state->definition_index = device->definition_index; + state->position = device->device.position; + state->power = device->device.power; + if (count == MAXIMUM_DEVICE_STATES_PER_MESSAGE) + { + send_to_clients(&message, _distributed_message_coop_device_states, count, sizeof(message.states[0])); + count = 0; + } + } + if (count > 0) + send_to_clients(&message, _distributed_message_coop_device_states, count, sizeof(message.states[0])); +} + +/* host: where each scenery or machine was last sent, by its absolute +index, and whether it has moved since the map placed it */ +static struct +{ + long object_index; + boolean moved; + real_point3d position; + real_vector3d forward; +} host_sent_transforms[MAXIMUM_OBJECTS_PER_MAP]; + +/* host, each tick: the scenery and machines that moved (and, every +OBJECT_REFRESH_TICKS, all that ever have) */ +static void host_send_object_transforms( + void) +{ + struct distributed_coop_object_transforms_message message; + struct object_iterator iterator; + struct object_datum *object; + short count = 0; + + object_iterator_new(&iterator, COOP_MOVED_OBJECTS, 0); + while ((object = object_iterator_next(&iterator)) != NULL) + { + short absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.index); + struct distributed_coop_object_transform *transform; + boolean moving; + + if (object->object.parent_object_index != NONE || absolute_index < 0 || absolute_index >= MAXIMUM_OBJECTS_PER_MAP) + continue; + if (host_sent_transforms[absolute_index].object_index != iterator.index) + { + /* an object seen for the first time is where the map placed it, on + every machine, so there is nothing to send yet */ + host_sent_transforms[absolute_index].object_index = iterator.index; + host_sent_transforms[absolute_index].moved = FALSE; + host_sent_transforms[absolute_index].position = object->object.position; + host_sent_transforms[absolute_index].forward = object->object.forward; + continue; + } + moving = distance_squared3d(&host_sent_transforms[absolute_index].position, &object->object.position) >= 0.0001f || + dot_product3d(&host_sent_transforms[absolute_index].forward, &object->object.forward) <= 0.9999f; + if (!moving && !(host_resend.refresh && host_sent_transforms[absolute_index].moved)) + continue; + host_sent_transforms[absolute_index].moved = TRUE; + host_sent_transforms[absolute_index].position = object->object.position; + host_sent_transforms[absolute_index].forward = object->object.forward; + transform = &message.transforms[count++]; + transform->name_index = object->object.name_index; + transform->pad = 0; + transform->object_index = iterator.index; + transform->definition_index = object->definition_index; + transform->position = object->object.position; + distributed_vector_pack(&object->object.forward, DISTRIBUTED_UNIT_SCALE, &transform->forward); + distributed_vector_pack(&object->object.up, DISTRIBUTED_UNIT_SCALE, &transform->up); + if (count == MAXIMUM_OBJECT_TRANSFORMS_PER_MESSAGE) + { + send_to_clients(&message, _distributed_message_coop_object_transforms, count, sizeof(message.transforms[0])); + count = 0; + } + } + if (count > 0) + send_to_clients(&message, _distributed_message_coop_object_transforms, count, sizeof(message.transforms[0])); +} + +/* host: how each object looked when last sent, by its absolute index, and +whether that has changed since it was made */ +static struct +{ + long object_index; + boolean changed; + byte region_permutations[MAXIMUM_REGIONS_PER_OBJECT]; + real scale; +} host_sent_looks[MAXIMUM_OBJECTS_PER_MAP]; + +/* host, each tick: the objects whose looks changed (and, every +OBJECT_REFRESH_TICKS, all whose looks ever have) */ +static void host_send_object_looks( + void) +{ + struct distributed_coop_object_looks_message message; + struct object_iterator iterator; + struct object_datum *object; + short count = 0; + + object_iterator_new(&iterator, _object_mask_all, 0); + while ((object = object_iterator_next(&iterator)) != NULL) + { + short absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.index); + struct distributed_coop_object_look *look; + boolean changing; + + if (absolute_index < 0 || absolute_index >= MAXIMUM_OBJECTS_PER_MAP) + continue; + if (host_sent_looks[absolute_index].object_index != iterator.index) + { + /* an object seen for the first time looks the same on every machine */ + host_sent_looks[absolute_index].object_index = iterator.index; + host_sent_looks[absolute_index].changed = FALSE; + host_sent_looks[absolute_index].scale = object->object.scale; + csmemcpy(host_sent_looks[absolute_index].region_permutations, object->object.region_permutations, + sizeof(object->object.region_permutations)); + continue; + } + changing = host_sent_looks[absolute_index].scale != object->object.scale || + csmemcmp(host_sent_looks[absolute_index].region_permutations, object->object.region_permutations, + sizeof(object->object.region_permutations)); + if (!changing && !(host_resend.refresh && host_sent_looks[absolute_index].changed)) + continue; + host_sent_looks[absolute_index].changed = TRUE; + host_sent_looks[absolute_index].scale = object->object.scale; + csmemcpy(host_sent_looks[absolute_index].region_permutations, object->object.region_permutations, + sizeof(object->object.region_permutations)); + look = &message.looks[count++]; + look->name_index = object->object.name_index; + look->pad = 0; + look->object_index = iterator.index; + look->definition_index = object->definition_index; + csmemcpy(look->region_permutations, object->object.region_permutations, sizeof(look->region_permutations)); + look->scale = object->object.scale; + if (count == MAXIMUM_OBJECT_LOOKS_PER_MESSAGE) + { + send_to_clients(&message, _distributed_message_coop_object_looks, count, sizeof(message.looks[0])); + count = 0; + } + } + if (count > 0) + send_to_clients(&message, _distributed_message_coop_object_looks, count, sizeof(message.looks[0])); +} + +/* ---------- the cinematic screen effect + +The scripts' full-screen effects (cinematic_screen_effect_*: blur, warp, +light enhancement, desaturation, the video effect) are state the host's +scripts set and a client's never do. The host sends it when it changes and +every OBJECT_REFRESH_TICKS, for a client that joined since or missed it. */ + +struct distributed_coop_screen_effect_message +{ + struct distributed_message_header header; + struct rasterizer_screen_effect_port_state state; +}; + +/* host: the state last sent */ +static struct rasterizer_screen_effect_port_state host_sent_screen_effect; + +static void host_send_screen_effect( + void) +{ + struct distributed_coop_screen_effect_message message; + + rasterizer_screen_effect_port_get(&message.state); + if (!host_resend.refresh && !csmemcmp(&message.state, &host_sent_screen_effect, sizeof(message.state))) + return; + host_sent_screen_effect = message.state; + send_to_clients(&message, _distributed_message_coop_screen_effect, 1, sizeof(message.state)); +} + +word network_coop_screen_effect_entry_size( + void) +{ + return sizeof(struct rasterizer_screen_effect_port_state); +} + +void network_coop_handle_screen_effect( + void const *entries, + short count) +{ + struct rasterizer_screen_effect_port_state state; + /* every field from the tint onward is a real (rasterizer_cinematics.h) */ + real const *reals = &state.filter_desaturation_tint.red; + short real_count = (short)((sizeof(state) - + offsetof(struct rasterizer_screen_effect_port_state, filter_desaturation_tint)) / sizeof(real)); + short index; + + if (!coop_client() || count != 1) + return; + csmemcpy(&state, entries, sizeof(state)); + for (index = 0; index < real_count; index++) + { + if (!distributed_real_valid(reals[index])) + return; + } + /* (what the rasterizer can draw: its convolution types and overbright + modes, a script's few extra passes, and video with no convolution, as + rasterizer_screen_effect_set_video leaves it) */ + state.convolution_type = (short)PIN(state.convolution_type, 0, SCREEN_EFFECT_CONVOLUTION_TYPES - 1); + state.convolution_extra_passes = (short)PIN(state.convolution_extra_passes, 0, SCREEN_EFFECT_MAXIMUM_EXTRA_PASSES); + state.video_overbright_mode = (short)PIN(state.video_overbright_mode, 0, SCREEN_EFFECT_OVERBRIGHT_MODES - 1); + if (state.video_on) + { + state.convolution_type = 0; + state.convolution_extra_passes = 0; + state.convolution_radius[0] = state.convolution_radius[1] = 0.0f; + } + /* the game can only blur one window, so split screen goes without */ + if (main_get_window_count() > 1) + { + state.convolution_type = 0; + state.convolution_radius[0] = state.convolution_radius[1] = 0.0f; + } + rasterizer_screen_effect_port_set(&state); +} + +/* host: what its scripts have attached, sent again every +OBJECT_REFRESH_TICKS for a client that joined since */ +enum +{ + MAXIMUM_ATTACHMENTS = 32, +}; + +static struct +{ + long parent_index; + long child_index; + short parent_marker_index; + short child_marker_index; +} host_attachments[MAXIMUM_ATTACHMENTS]; +static short host_attachment_count; + +/* host: the HUD state the scripts set that a late joiner would lack: the +objective, and the nav points active (their kind, nav index, target, marker +and offset as the note was given), resent by host_send_hud_state */ +enum +{ + MAXIMUM_NAV_POINTS = 16, +}; + +static struct +{ + short objective; + short nav_point_count; + struct + { + short kind; + short nav_index; + long target; + long marker; + real vertical_offset; + } nav_points[MAXIMUM_NAV_POINTS]; +} host_hud_state; + +/* host: the looping sounds the scripts have started (music, ambience). +They are resent every OBJECT_REFRESH_TICKS so a late joiner hears them. */ +enum +{ + MAXIMUM_LOOPING_SOUNDS = 32, +}; + +static struct +{ + long definition_index; + long object_index; + real scale; +} host_looping_sounds[MAXIMUM_LOOPING_SOUNDS]; +static short host_looping_sound_count; + +static void host_send_object_names( + void) +{ + struct distributed_coop_object_names_message message; + struct distributed_coop_object_names *names = &message.names; + short name_index; + + csmemset(names, 0, sizeof(*names)); + names->structure_bsp_index = global_structure_bsp_index_get(); + names->name_count = (short)MIN(global_scenario_get()->object_names.count, MAXIMUM_OBJECT_NAMES_PER_SCENARIO); + for (name_index = 0; name_index < names->name_count; name_index++) + { + if (object_index_from_name_index(name_index) != NONE) + names->present[name_index / 8] |= (byte)(1 << (name_index % 8)); + } + send_to_clients(&message, _distributed_message_coop_object_names, 1, sizeof(message.names)); +} + +static void skip_vote_clear( + void) +{ + short index; + + skip_vote.voted = FALSE; + skip_vote.votes = 0; + for (index = 0; index < NUMBEROF(skip_vote.client_vote_times); index++) + { + skip_vote.client_heard_times[index] = NONE; + skip_vote.client_vote_times[index] = NONE; + } +} + +/* host: counts the votes to skip the cinematic, and skips when more than +half the machines showing it have voted (one still loading, or joined since +it began, has no say until it shows it) */ +static void host_count_skip_votes( + void) +{ + long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES]; + short machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES); + long now = game_time_get(); + boolean skippable = cinematic_in_progress() && cinematic_can_be_skipped(); + boolean was_offered = skip_vote.offered; + short index; + + /* Wait until the save the script makes when the cutscene becomes + skippable has been written; reverting earlier would go back to the save + before it. A save requested later in the cutscene doesn't hold up the + vote, since the skip cancels it, as in a solo game. */ + if (!skippable) + skip_vote.skip_save_written = FALSE; + else if (!main_saving_map()) + skip_vote.skip_save_written = TRUE; + skip_vote.offered = skippable && skip_vote.skip_save_written && now >= skip_vote.cooldown_until; + if (skip_vote.offered && !was_offered) + error(_error_silent, "co-op: the cutscene can be skipped"); + if (!skip_vote.offered) + { + skip_vote_clear(); + skip_vote.voters = 0; + skip_vote.requested = FALSE; + return; + } + /* already requested: main.c skips at the end of this frame */ + if (skip_vote.requested) + return; + /* the host votes too, unless it is a dedicated server with no player */ + skip_vote.voters = local_player_get_next(NONE) != NONE ? 1 : 0; + skip_vote.votes = skip_vote.voted ? 1 : 0; + for (index = 0; index < machine_count; index++) + { + long heard_time = skip_vote.client_heard_times[machine_indices[index]]; + long vote_time = skip_vote.client_vote_times[machine_indices[index]]; + + if (heard_time == NONE || now - heard_time > SKIP_VOTE_HELD_TICKS) + continue; + skip_vote.voters++; + if (vote_time != NONE && now - vote_time <= SKIP_VOTE_HELD_TICKS) + skip_vote.votes++; + } + if (skip_vote.votes * 2 > skip_vote.voters) + { + error(_error_silent, "co-op: skipping the cutscene (%d of %d voted)", skip_vote.votes, skip_vote.voters); + skip_vote.requested = TRUE; + main_skip_cinematic(); + } +} + +/* the least speed (squared, world units a tick) of an object the cutscene +camera films when the scripts don't name it */ +#define CAMERA_OBJECT_SPEED 0.01f +/* the camera an observer accepts (observer.c) */ +#define CAMERA_WORLD_BOUND 5000.0f +#define CAMERA_MINIMUM_FIELD_OF_VIEW 0.001f +#define CAMERA_MAXIMUM_FIELD_OF_VIEW (_pi / 2.0f) + +/* Host: the object the cutscene camera films: the nearest vehicle moving +close to it (a camera riding along with a drop pod or a Pelican), else the +one the scripts set it relative to; NONE for a camera on its own. The last +one is kept while it stays close, so the camera doesn't jump between two. */ +static long camera_object_get( + struct observer_result const *camera) +{ + static long filmed = NONE; + real nearest = CAMERA_OBJECT_DISTANCE * CAMERA_OBJECT_DISTANCE; + struct object_iterator iterator; + struct object_datum *object; + + object = object_try_and_get_and_verify_type(filmed, _object_mask_vehicle); + if (object && distance_squared3d(&camera->position, &object->object.position) < nearest) + return filmed; + filmed = NONE; + object_iterator_new(&iterator, _object_mask_vehicle, 0); + while ((object = object_iterator_next(&iterator)) != NULL) + { + real distance = distance_squared3d(&camera->position, &object->object.position); + + if (distance < nearest && object->object.parent_object_index == NONE && + magnitude_squared3d(&object->object.translational_velocity) > CAMERA_OBJECT_SPEED) + { + nearest = distance; + filmed = iterator.index; + } + } + if (filmed != NONE) + return filmed; + return object_try_and_get(scripted_camera_object_relative_to()) ? scripted_camera_object_relative_to() : NONE; +} + +/* host: the presentation sent to every client this tick */ +static void host_presentation( + struct distributed_coop_presentation *presentation) +{ + struct observer_result const *camera = observer_get_camera(0); + struct hud_timer_state timer; + real_rgb_color fade_color; + boolean fading_out; + long elapsed; + + csmemset(presentation, 0, sizeof(*presentation)); + SET_FLAG(presentation->flags, _presentation_cinematic_bit, cinematic_in_progress()); + SET_FLAG(presentation->flags, _presentation_letterbox_bit, cinematic_globals->show_letterbox); + player_effect_port_screen_fade_get(&fade_color, &presentation->fade_ticks, &fading_out, + &presentation->fade_start_time); + SET_FLAG(presentation->flags, _presentation_fading_out_bit, fading_out); + presentation->fade_color[0] = (byte)(PIN(fade_color.red, 0.0f, 1.0f) * 255.0f + 0.5f); + presentation->fade_color[1] = (byte)(PIN(fade_color.green, 0.0f, 1.0f) * 255.0f + 0.5f); + presentation->fade_color[2] = (byte)(PIN(fade_color.blue, 0.0f, 1.0f) * 255.0f + 0.5f); + elapsed = game_time_get() - presentation->fade_start_time; + presentation->fade_elapsed = (short)PIN(elapsed, 0, SHORT_MAX); + if (camera) + { + presentation->camera_position = camera->position; + distributed_vector_pack(&camera->forward, DISTRIBUTED_UNIT_SCALE, &presentation->camera_forward); + distributed_vector_pack(&camera->up, DISTRIBUTED_UNIT_SCALE, &presentation->camera_up); + presentation->camera_field_of_view = + (word)PIN(camera->field_of_view * FIELD_OF_VIEW_SCALE + 0.5f, 1, UNSIGNED_SHORT_MAX); + presentation->camera_object_index = camera_object_get(camera); + if (presentation->camera_object_index != NONE) + { + vector_from_points3d(&object_get(presentation->camera_object_index)->object.position, &camera->position, + &presentation->camera_object_offset); + } + } + else + presentation->camera_object_index = NONE; + + presentation->camera_scripted = (byte)(*director_camera_scripted != FALSE); + presentation->input_disabled = (byte)!player_input_enabled(); + presentation->scripted_shake = (byte)player_effect_port_scripted_active(); + presentation->players_vitality_set = (byte)players_vitality.set; + presentation->players_maximum_body_vitality = players_vitality.maximum_body; + presentation->players_maximum_shield_vitality = players_vitality.maximum_shield; + game_allegiance_get_teams(presentation->ally_teams, presentation->friendly_teams); + SET_FLAG(presentation->flags, _presentation_skippable_bit, skip_vote.offered); + presentation->skip_votes = (byte)MIN(skip_vote.votes, 255); + presentation->skip_voters = (byte)MIN(skip_vote.voters, 255); + + SET_FLAG(presentation->flags, _presentation_show_hud_bit, hud_scripted_globals->show_hud); + SET_FLAG(presentation->flags, _presentation_show_help_text_bit, hud_scripted_globals->show_hud_help_text); + presentation->unit_hud_flags = hud_unit_port_script_flags(); + presentation->weapon_hud_flags = hud_weapon_port_script_flags(); + hud_messaging_port_timer_get(&timer); + SET_FLAG(presentation->flags, _presentation_timer_paused_bit, timer.paused); + SET_FLAG(presentation->flags, _presentation_timer_enabled_bit, timer.enabled); + presentation->timer_reference_time = timer.reference_time; + presentation->timer_ticks = timer.ticks; + presentation->timer_flash_cutoff = timer.flash_cutoff; + presentation->timer_x = timer.x; + presentation->timer_y = timer.y; + presentation->timer_corner = timer.corner; +} + +static void client_cinematic_end( + void) +{ + if (!coop_presentation.cinematic_started) + return; + coop_presentation.cinematic_started = FALSE; + cinematic_stop(); +} + +/* client with nothing else to watch: moves the camera (position, forward, +up) behind and above the host's eyes, like spectating a teammate. It eases +toward the target so the host looking around doesn't jerk it. */ +static void client_watch_host_from_behind( + real_point3d *position, + real_vector3d *forward, + real_vector3d *up) +{ + real_point3d behind; + real_vector3d level = { forward->i, forward->j, 0.0f }; + + if (normalize3d(&level) == 0.0f) + level = *forward; + point_from_line3d(position, &level, -WATCH_HOST_DISTANCE, &behind); + behind.z += WATCH_HOST_HEIGHT; + if (!coop_presentation.watching_host_valid) + { + coop_presentation.watching_host_valid = TRUE; + coop_presentation.watching_host_position = behind; + coop_presentation.watching_host_forward = *forward; + } + else + { + points_interpolate(&coop_presentation.watching_host_position, &behind, WATCH_HOST_FOLLOW, + &coop_presentation.watching_host_position); + vectors_interpolate(&coop_presentation.watching_host_forward, forward, WATCH_HOST_FOLLOW, + &coop_presentation.watching_host_forward); + normalize3d(&coop_presentation.watching_host_forward); + } + *position = coop_presentation.watching_host_position; + *forward = coop_presentation.watching_host_forward; + /* keep the camera upright */ + up->i = 0.0f; + up->j = 0.0f; + up->k = 1.0f; + distributed_axes_make_valid(forward, up); +} + +/* client: looks through the host's camera, or with its own */ +static void client_host_camera_set( + boolean host_camera) +{ + if (coop_presentation.host_camera == host_camera) + return; + coop_presentation.host_camera = host_camera; + director_script_camera(host_camera); +} + +static void client_apply_hud( + struct distributed_coop_event const *event) +{ + switch (event->type) + { + case _coop_hud_help_text: + hud_scripted_globals->show_hud_help_text = event->frame != 0; + if (event->value >= 0 && event->value < hud_messaging_port_message_count()) + scripted_hud_set_state_message(event->value); + break; + case _coop_hud_objective: + if (event->value >= 0 && event->value < hud_messaging_port_message_count()) + scripted_hud_set_objective(event->value); + break; + case _coop_hud_help_flash: + scripted_hud_set_flashing_state(event->value != 0); + break; + case _coop_hud_messages_clear: + scripted_hud_messages_clear(); + break; + case _coop_hud_checkpoint: + hud_autosave(event->value != 0); + break; + default: + break; + } +} + +static void client_apply_player_effect( + struct distributed_coop_event const *event) +{ + real const *reals = event->reals; + + switch (event->type) + { + case _coop_player_effect_translation: + scripted_player_effect_set_translation(reals[0], reals[1], reals[2]); + break; + case _coop_player_effect_rotation: + scripted_player_effect_set_rotation(reals[0], reals[1], reals[2]); + break; + case _coop_player_effect_start: + scripted_player_effect_start(PIN(reals[0], 0.0f, 1.0f), PIN(reals[1], 0.0f, 60.0f)); + break; + case _coop_player_effect_stop: + scripted_player_effect_stop(PIN(reals[0], 0.0f, 60.0f)); + break; + default: + break; + } +} + +static void client_apply_nav_point( + struct distributed_coop_event const *event) +{ + struct scenario *scenario = global_scenario_get(); + boolean activate = event->value != NONE; + boolean flag = event->type == _coop_nav_point_team_flag || event->type == _coop_nav_point_unit_flag; + boolean team = event->type == _coop_nav_point_team_flag || event->type == _coop_nav_point_team_object; + real offset = event->reals[0]; + + if (activate && (event->value < 0 || event->value >= hud_globals->waypoint.arrows.count)) + return; + if (flag && (event->object_index < 0 || event->object_index >= scenario->cutscene_flags.count)) + return; + if (team && (event->target < 0 || event->target >= NUMBER_OF_SOLO_CAMPAIGN_TEAMS)) + return; + if (!team && !object_try_and_get_and_verify_type(event->target, _object_mask_unit)) + return; + switch (event->type) + { + case _coop_nav_point_team_flag: + if (activate) + hud_activate_team_nav_point_with_flag(event->value, (short)event->target, (short)event->object_index, offset); + else + hud_deactivate_team_nav_point_with_flag((short)event->target, (short)event->object_index); + break; + case _coop_nav_point_team_object: + if (activate) + hud_activate_team_nav_point_with_object(event->value, (short)event->target, event->object_index, offset); + else + hud_deactivate_team_nav_point_with_object((short)event->target, event->object_index); + break; + case _coop_nav_point_unit_flag: + if (activate) + hud_unit_activate_nav_point_with_flag(event->value, event->target, (short)event->object_index, offset); + else + hud_unit_deactivate_nav_point_with_flag(event->target, (short)event->object_index); + break; + case _coop_nav_point_unit_object: + if (activate) + hud_unit_activate_nav_point_with_object(event->value, event->target, event->object_index, offset); + else + hud_unit_deactivate_nav_point_with_object(event->target, event->object_index); + break; + default: + break; + } +} + +static void client_apply_unit_animation( + struct distributed_coop_event const *event) +{ + if (!distributed_object_index_valid(event->object_index) || !network_objects_client_has(event->object_index) || + !object_try_and_get_and_verify_type(event->object_index, _object_mask_unit)) + { + return; + } + if (event->tag_index == NONE) + unit_stop_custom_animation(event->object_index); + else if (distributed_graph_animation(event->tag_index, event->value)) + unit_port_play_user_animation(event->object_index, event->tag_index, event->value, event->interpolate, event->frame); +} + +static void client_apply_scenery_animation( + struct distributed_coop_event const *event) +{ + long scenery_index = object_find(event->name_index, event->object_index, event->definition_index, + _object_mask_scenery); + struct animation *animation = distributed_graph_animation(event->tag_index, event->value); + + /* scenery animations have no random permutations, so the name finds the same one */ + if (scenery_index != NONE && animation) + scenery_animation_start_at_frame(scenery_index, event->tag_index, animation->name, event->frame); +} + +static void client_apply_sound( + struct distributed_coop_event const *event) +{ + long object_index = event->object_index != NONE && network_objects_client_has(event->object_index) ? + event->object_index : NONE; + real scale = PIN(event->reals[0], 0.0f, 1.0f); + + switch (event->type) + { + case _coop_sound_impulse: + if (distributed_tag_of_group(event->tag_index, SOUND_DEFINITION_TAG)) + { + scripted_sound_new(event->tag_index, object_index, scale); + client_script_sounds.definition_indices[client_script_sounds.next] = event->tag_index; + client_script_sounds.next = (short)((client_script_sounds.next + 1) % CLIENT_SCRIPT_SOUND_COUNT); + } + break; + case _coop_sound_looping_start: + /* a resent sound that is already playing here is left alone */ + if (distributed_tag_of_group(event->tag_index, LOOPING_SOUND_DEFINITION_TAG) && + looping_sound_definition_get(event->tag_index)->runtime_scripting_sound_index == NONE) + { + scripted_looping_sound_start(event->tag_index, object_index, scale); + } + break; + case _coop_sound_looping_stop: + if (distributed_tag_of_group(event->tag_index, LOOPING_SOUND_DEFINITION_TAG)) + scripted_looping_sound_stop(event->tag_index); + break; + default: + break; + } +} + +static void client_apply_effect( + struct distributed_coop_event const *event) +{ + long object_index; + struct object_datum *object; + struct model *model; + + if (!distributed_tag_of_group(event->tag_index, EFFECT_DEFINITION_TAG)) + return; + if (event->type == _coop_effect_at_flag) + { + if (event->value >= 0 && event->value < global_scenario_get()->cutscene_flags.count) + hs_effect_new(event->tag_index, event->value); + return; + } + object_index = object_find(event->name_index, event->object_index, event->definition_index, _object_mask_all); + object = object_index != NONE ? object_get(object_index) : NULL; + model = object && object_definition_get(object->definition_index)->object.model.index != NONE ? + model_definition_get(object_definition_get(object->definition_index)->object.model.index) : NULL; + if (model && event->value >= 0 && event->value < model->markers.count) + { + hs_effect_new_from_object_marker(event->tag_index, object_index, + TAG_BLOCK_GET_ELEMENT(&model->markers, event->value, struct model_marker)->name); + } +} + +/* the name of a marker of the object's model by its index there; "" (the +object's origin) for NONE or one it lacks */ +static char const *object_marker_name( + long object_index, + short marker_index) +{ + long model_index = object_definition_get(object_get(object_index)->definition_index)->object.model.index; + struct model *model = model_index != NONE ? model_definition_get(model_index) : NULL; + + if (!model || marker_index < 0 || marker_index >= model->markers.count) + return ""; + return TAG_BLOCK_GET_ELEMENT(&model->markers, marker_index, struct model_marker)->name; +} + +/* the index of a marker of the object's model by its name, NONE for "" or +one it lacks */ +static short object_marker_index( + long object_index, + char const *marker_name) +{ + long model_index = object_definition_get(object_get(object_index)->definition_index)->object.model.index; + + return model_index != NONE && marker_name && marker_name[0] ? model_find_marker(model_index, marker_name) : NONE; +} + +/* An attach or a detach. The parent is object_index, name_index and +definition_index; the child is target, with its definition in tag_index and +its name index in reals[0]; value and frame are the parent's and the child's +markers. */ +static void client_apply_attach( + struct distributed_coop_event const *event) +{ + long parent_index = object_find(event->name_index, event->object_index, event->definition_index, _object_mask_all); + long child_index = object_find((short)event->reals[0], event->target, event->tag_index, _object_mask_all); + long ancestor_index; + + if (parent_index == NONE || child_index == NONE || parent_index == child_index) + return; + /* a resent attach this machine already has */ + if (event->type == _coop_attach && object_get(child_index)->object.parent_object_index == parent_index) + return; + /* (not to one of its own children, which objects.c cannot do: copies + of the hierarchy that differ here, or a host's mistake) */ + for (ancestor_index = object_get(parent_index)->object.parent_object_index; event->type == _coop_attach && + ancestor_index != NONE; ancestor_index = object_get(ancestor_index)->object.parent_object_index) + { + if (ancestor_index == child_index) + return; + } + if (event->type == _coop_detach) + objects_scripting_detach(parent_index, child_index); + else + { + objects_scripting_attach(parent_index, object_marker_name(parent_index, event->value), child_index, + object_marker_name(child_index, event->frame)); + } +} + +/* client: the cutscene was skipped, so the dialogue it started stops (the +host's went with its revert; its music stops through host_send_looping_sounds) */ +static void client_stop_script_sounds( + void) +{ + short index; + + for (index = 0; index < CLIENT_SCRIPT_SOUND_COUNT; index++) + { + long definition_index = client_script_sounds.definition_indices[index]; + + if (definition_index != NONE && distributed_tag_of_group(definition_index, SOUND_DEFINITION_TAG)) + sound_stop_impulse(sound_definition_get(definition_index)->scripting_sound_index); + client_script_sounds.definition_indices[index] = NONE; + } +} + +static void client_apply_event( + struct distributed_coop_event const *event) +{ + switch (event->kind) + { + case _coop_event_sound: + client_apply_sound(event); + break; + case _coop_event_title: + if (event->value >= 0 && event->value < global_scenario_get()->cutscene_chapter_titles.count) + cinematic_set_title_delayed(event->value, PIN(event->reals[0], 0.0f, 60.0f)); + break; + case _coop_event_hud: + client_apply_hud(event); + break; + case _coop_event_player_effect: + client_apply_player_effect(event); + break; + case _coop_event_nav_point: + client_apply_nav_point(event); + break; + case _coop_event_unit_animation: + client_apply_unit_animation(event); + break; + case _coop_event_scenery_animation: + client_apply_scenery_animation(event); + break; + case _coop_event_effect: + client_apply_effect(event); + break; + case _coop_event_attach: + client_apply_attach(event); + break; + case _coop_event_cutscene_skipped: + client_stop_script_sounds(); + break; + case _coop_event_unit_open: + if (distributed_object_index_valid(event->object_index) && network_objects_client_has(event->object_index) && + object_try_and_get_and_verify_type(event->object_index, _object_mask_unit)) + { + if (event->value) + unit_open(event->object_index); + else + unit_close(event->object_index); + } + break; + default: + break; + } +} + +/* client: the HUD settings in the host's presentation */ +static void client_apply_hud_state( + struct distributed_coop_presentation const *presentation) +{ + struct hud_timer_state timer; + + hud_scripted_globals->show_hud = TEST_FLAG(presentation->flags, _presentation_show_hud_bit); + hud_scripted_globals->show_hud_help_text = TEST_FLAG(presentation->flags, _presentation_show_help_text_bit); + hud_unit_port_set_script_flags(presentation->unit_hud_flags); + hud_weapon_port_set_script_flags(presentation->weapon_hud_flags); + timer.reference_time = presentation->timer_reference_time; + timer.ticks = presentation->timer_ticks; + timer.flash_cutoff = presentation->timer_flash_cutoff; + timer.x = presentation->timer_x; + timer.y = presentation->timer_y; + timer.corner = presentation->timer_corner; + timer.paused = TEST_FLAG(presentation->flags, _presentation_timer_paused_bit); + timer.enabled = TEST_FLAG(presentation->flags, _presentation_timer_enabled_bit); + hud_messaging_port_timer_set(&timer); +} + +/* the players in the game */ +static short coop_player_count( + void) +{ + struct data_iterator iterator; + short count = 0; + + data_iterator_new(&iterator, player_data); + while (data_iterator_next(&iterator)) + count++; + + return count; +} + +static boolean tag_name_has( + long definition_index, + char const *text) +{ + return definition_index != NONE && strstr(tag_get_name(definition_index), text) != NULL; +} + +/* each tick: every player's unit at the players' maximum vitality. The +host's own units get their current vitality full, as the scripts' set does; +a client's follows the host's damage states. */ +static void players_vitality_keep( + void) +{ + struct data_iterator iterator; + struct player_datum *player; + + if (!players_vitality.set) + return; + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + struct object_datum *unit = object_try_and_get(player->unit_index); + + if (!unit || (unit->object.maximum_body_vitality == players_vitality.maximum_body && + unit->object.maximum_shield_vitality == players_vitality.maximum_shield)) + { + continue; + } + if (coop_host()) + object_initialize_vitality(player->unit_index, &players_vitality.maximum_body, &players_vitality.maximum_shield); + else + { + unit->object.maximum_body_vitality = players_vitality.maximum_body; + unit->object.maximum_shield_vitality = players_vitality.maximum_shield; + } + } +} + +/* ---------- public code */ + +void network_coop_new_game( + void) +{ + csmemset(&coop_presentation, 0, sizeof(coop_presentation)); + csmemset(&coop_events, 0, sizeof(coop_events)); + csmemset(&host_devices, 0, sizeof(host_devices)); + csmemset(&client_devices, 0, sizeof(client_devices)); + csmemset(client_names_differing, 0, sizeof(client_names_differing)); + csmemset(client_script_sounds.definition_indices, NONE, sizeof(client_script_sounds.definition_indices)); + csmemset(host_sent_transforms, 0, sizeof(host_sent_transforms)); + csmemset(host_sent_looks, 0, sizeof(host_sent_looks)); + csmemset(host_sent_devices, 0, sizeof(host_sent_devices)); + csmemset(host_player_structure_bsps, NONE, sizeof(host_player_structure_bsps)); + csmemset(&host_sent_screen_effect, 0, sizeof(host_sent_screen_effect)); + csmemset(&host_resend, 0, sizeof(host_resend)); + csmemset(&players_vitality, 0, sizeof(players_vitality)); + host_attachment_count = 0; + host_looping_sound_count = 0; + host_hud_state.objective = NONE; + host_hud_state.nav_point_count = 0; + skip_vote_clear(); + skip_vote.offered = FALSE; + skip_vote.voters = 0; + skip_vote.cooldown_until = 0; + skip_vote.skip_save_written = FALSE; + coop_enemies_new_game(); +} + +/* A network game on a campaign scenario with no game engine. Checking the +scenario matters: the main menu's scene keeps running while a network game +is set up, also with no game engine, and it is not co-op. */ +boolean network_coop_active( + void) +{ + short connection = game_connection(); + + return (connection == _game_connection_network_server || connection == _game_connection_network_client) && + global_scenario && global_scenario->type == _scenario_type_solo && !game_engine_running(); +} + +boolean network_coop_devices_remote( + void) +{ + return coop_client(); +} + +/* units.c: a vehicle left the vehicle carrying it. When a Pelican drops a +Warthog in a larger game, the host drops more beside it, so every player has +a ride: one Warthog for every four players, at most five. They go side by +side, alternately left and right, each only where no wall stands between it +and the first, and reach the clients as any vehicle does. */ +void network_coop_vehicle_dropped( + long vehicle_index, + long carrier_index) +{ + struct object_datum const *vehicle = object_get(vehicle_index); + short wanted = (short)MIN((coop_player_count() + PLAYERS_PER_DROPPED_VEHICLE - 1) / PLAYERS_PER_DROPPED_VEHICLE, + MAXIMUM_DROPPED_VEHICLES) - 1; + real_vector3d left; + short spot, placed = 0; + + if (!coop_host() || wanted <= 0 || !tag_name_has(vehicle->definition_index, "warthog") || + !tag_name_has(object_get(carrier_index)->definition_index, "pelican")) + { + return; + } + cross_product3d(&vehicle->object.up, &vehicle->object.forward, &left); + for (spot = 1; spot <= 2 * MAXIMUM_DROPPED_VEHICLES && placed < wanted; spot++) + { + /* (1.5 world units apart: a Warthog's width and a gap) */ + real distance = 1.5f * ((spot + 1) / 2) * (spot % 2 ? 1.0f : -1.0f); + struct object_placement_data data; + struct collision_result collision; + real_vector3d offset; + + scale_vector3d(&left, distance, &offset); + if (collision_test_vector(FLAG(_collision_test_structure_bit), &vehicle->object.position, &offset, NONE, + &collision)) + { + continue; + } + object_placement_data_new(&data, vehicle->definition_index, NONE); + point_from_line3d(&vehicle->object.position, &offset, 1.0f, &data.position); + data.forward = vehicle->object.forward; + data.up = vehicle->object.up; + data.translational_velocity = vehicle->object.translational_velocity; + if (object_new(&data) != NONE) + placed++; + } + error(_error_silent, "co-op: a Pelican dropped %d more Warthogs for %d players", placed, coop_player_count()); +} + +/* unit_scripting_commands.c: the scripts set a unit's maximum (maximum +TRUE) or current vitality. Campaign scripts name only player0 for what +every player should have, so in co-op a player's is set on every player's +unit, and a maximum is kept for those spawned later and sent to the +clients. FALSE if the unit isn't a co-op player's, for the caller to set. */ +boolean network_coop_set_players_vitality( + long unit_index, + boolean maximum, + real body, + real shield) +{ + struct data_iterator iterator; + struct player_datum *player; + + if (!coop_host() || player_index_from_unit_index(unit_index) == NONE) + return FALSE; + if (maximum) + { + players_vitality.set = TRUE; + players_vitality.maximum_body = body; + players_vitality.maximum_shield = shield; + } + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + struct object_datum *unit = object_try_and_get(player->unit_index); + + if (!unit || TEST_FLAG(unit->object.damage_flags, _object_dead_bit)) + continue; + if (maximum) + object_initialize_vitality(player->unit_index, &body, &shield); + else + unit_scripting_set_current_vitality_of(player->unit_index, body, shield); + } + return TRUE; +} + +void network_coop_note_device_snap( + short group_index) +{ + if (coop_host() && group_index >= 0 && group_index < MAXIMUM_DEVICE_GROUPS) + host_devices.snap_counts[group_index]++; +} + +/* index of the looping sound in host_looping_sounds, or NONE */ +static short host_looping_sound_find( + long definition_index) +{ + short index; + + for (index = 0; index < host_looping_sound_count; index++) + { + if (host_looping_sounds[index].definition_index == definition_index) + return index; + } + return NONE; +} + +/* host, each tick: a looping sound that stopped without the scripts +stopping it (it ended, or a skipped cutscene's revert took it) is stopped on +the clients too; the rest are started again with the resent state */ +static void host_send_looping_sounds( + void) +{ + short index = 0; + + while (index < host_looping_sound_count) + { + long definition_index = host_looping_sounds[index].definition_index; + struct distributed_coop_event *event; + + if (looping_sound_definition_get(definition_index)->runtime_scripting_sound_index == NONE) + { + if ((event = event_new(_coop_event_sound)) != NULL) + { + event->type = _coop_sound_looping_stop; + event->tag_index = definition_index; + } + host_looping_sounds[index] = host_looping_sounds[--host_looping_sound_count]; + continue; + } + if (host_resend.refresh && (event = event_new(_coop_event_sound)) != NULL) + { + event->type = _coop_sound_looping_start; + event->tag_index = definition_index; + event->object_index = host_looping_sounds[index].object_index; + event->reals[0] = host_looping_sounds[index].scale; + } + index++; + } +} + +void network_coop_note_sound( + short kind, + long definition_index, + long object_index, + real scale) +{ + struct distributed_coop_event *event = event_new(_coop_event_sound); + short index; + + if (!event) + return; + event->type = (byte)kind; + event->tag_index = definition_index; + event->object_index = object_index; + event->reals[0] = scale; + + index = host_looping_sound_find(definition_index); + if (kind == _coop_sound_looping_stop && index != NONE) + { + host_looping_sounds[index] = host_looping_sounds[--host_looping_sound_count]; + } + else if (kind == _coop_sound_looping_start) + { + if (index == NONE && host_looping_sound_count < MAXIMUM_LOOPING_SOUNDS) + index = host_looping_sound_count++; + if (index != NONE) + { + host_looping_sounds[index].definition_index = definition_index; + host_looping_sounds[index].object_index = object_index; + host_looping_sounds[index].scale = scale; + } + } +} + +void network_coop_note_title( + short title_index, + real delay) +{ + struct distributed_coop_event *event = event_new(_coop_event_title); + + if (!event) + return; + event->value = title_index; + event->reals[0] = delay; +} + +void network_coop_note_hud( + short kind, + short value) +{ + struct distributed_coop_event *event = event_new(_coop_event_hud); + + if (!event) + return; + event->type = (byte)kind; + event->value = value; + /* (help text shows only while the scripts show it; the presentation + saying so comes a little after the event, so the event carries it) */ + event->frame = (short)hud_scripted_globals->show_hud_help_text; + if (kind == _coop_hud_objective) + host_hud_state.objective = value; +} + +void network_coop_note_player_effect( + short kind, + real a, + real b, + real c) +{ + struct distributed_coop_event *event = event_new(_coop_event_player_effect); + + if (!event) + return; + event->type = (byte)kind; + event->reals[0] = a; + event->reals[1] = b; + event->reals[2] = c; +} + +static void send_nav_point( + short kind, + short nav_index, + long target, + long marker, + real vertical_offset) +{ + struct distributed_coop_event *event = event_new(_coop_event_nav_point); + + if (!event) + return; + event->type = (byte)kind; + event->value = nav_index; + event->target = target; + event->object_index = marker; + event->reals[0] = vertical_offset; +} + +void network_coop_note_nav_point( + short kind, + short nav_index, + long target, + long marker, + real vertical_offset) +{ + short index; + + if (!coop_host()) + return; + send_nav_point(kind, nav_index, target, marker, vertical_offset); + for (index = 0; index < host_hud_state.nav_point_count; index++) + { + if (host_hud_state.nav_points[index].kind == kind && host_hud_state.nav_points[index].target == target && + host_hud_state.nav_points[index].marker == marker) + { + break; + } + } + if (nav_index == NONE) + { + if (index < host_hud_state.nav_point_count) + host_hud_state.nav_points[index] = host_hud_state.nav_points[--host_hud_state.nav_point_count]; + return; + } + if (index == host_hud_state.nav_point_count) + { + if (index == MAXIMUM_NAV_POINTS) + return; + host_hud_state.nav_point_count++; + } + host_hud_state.nav_points[index].kind = kind; + host_hud_state.nav_points[index].nav_index = nav_index; + host_hud_state.nav_points[index].target = target; + host_hud_state.nav_points[index].marker = marker; + host_hud_state.nav_points[index].vertical_offset = vertical_offset; +} + +/* host: the active nav points again (host_resend), and the objective to a +machine that joined (it shows on screen when it comes, so not more often) */ +static void host_send_hud_state( + void) +{ + short index; + + if (!host_resend.refresh) + return; + if (host_resend.joined && host_hud_state.objective != NONE) + { + struct distributed_coop_event *event = event_new(_coop_event_hud); + + if (event) + { + event->type = _coop_hud_objective; + event->value = host_hud_state.objective; + } + } + for (index = 0; index < host_hud_state.nav_point_count; index++) + { + send_nav_point(host_hud_state.nav_points[index].kind, host_hud_state.nav_points[index].nav_index, + host_hud_state.nav_points[index].target, host_hud_state.nav_points[index].marker, + host_hud_state.nav_points[index].vertical_offset); + } +} + +void network_coop_note_unit_animation( + long unit_index, + long animation_graph_index, + short animation_index, + boolean interpolate) +{ + struct distributed_coop_event *event = event_new(_coop_event_unit_animation); + + if (!event) + return; + event->object_index = unit_index; + event->tag_index = animation_graph_index; + event->value = animation_index; + event->interpolate = (byte)interpolate; +} + +void network_coop_note_unit_open( + long unit_index, + boolean open) +{ + struct distributed_coop_event *event = event_new(_coop_event_unit_open); + + if (!event) + return; + event->object_index = unit_index; + event->value = (short)open; +} + +/* unit_custom_animation_at_frame starts the animation and then sets the +frame in the same call, so the start is still queued and unsent: the frame +goes into it */ +void network_coop_note_unit_animation_frame( + long unit_index, + short frame_index) +{ + short index; + + if (!coop_host()) + return; + for (index = coop_events.count - 1; index >= 0; index--) + { + struct distributed_coop_event *event = &coop_events.events[index]; + + if (event->kind == _coop_event_unit_animation && event->object_index == unit_index) + { + if (coop_events.sends[index] == 0) + event->frame = frame_index; + return; + } + } +} + +void network_coop_note_scenery_animation( + long object_index, + long animation_graph_index, + short animation_index, + short frame_index) +{ + struct object_datum *object = object_try_and_get(object_index); + struct distributed_coop_event *event; + + if (!object || !(event = event_new(_coop_event_scenery_animation))) + return; + event->object_index = object_index; + event->name_index = object->object.name_index; + event->definition_index = object->definition_index; + event->tag_index = animation_graph_index; + event->value = animation_index; + event->frame = frame_index; +} + +/* An attach (with the markers' indexes) or a detach (NONE markers). */ +static void send_attach( + byte type, + long parent_index, + short parent_marker_index, + long child_index, + short child_marker_index) +{ + struct object_datum *parent = object_try_and_get(parent_index); + struct object_datum *child = object_try_and_get(child_index); + struct distributed_coop_event *event; + + if (!parent || !child || !(event = event_new(_coop_event_attach))) + return; + event->type = type; + event->object_index = parent_index; + event->name_index = parent->object.name_index; + event->definition_index = parent->definition_index; + event->target = child_index; + event->tag_index = child->definition_index; + event->reals[0] = child->object.name_index; + event->value = parent_marker_index; + event->frame = child_marker_index; +} + +/* the child's attachment in host_attachments, or NONE */ +static short host_attachment_find( + long child_index) +{ + short index; + + for (index = 0; index < host_attachment_count; index++) + { + if (host_attachments[index].child_index == child_index) + return index; + } + return NONE; +} + +static void host_send_attachments( + void) +{ + short index = 0; + + if (!host_resend.refresh) + return; + while (index < host_attachment_count) + { + struct object_datum *child = object_try_and_get(host_attachments[index].child_index); + + /* the child is gone or has a new parent: forget it */ + if (!child || child->object.parent_object_index != host_attachments[index].parent_index) + { + host_attachments[index] = host_attachments[--host_attachment_count]; + continue; + } + send_attach(_coop_attach, host_attachments[index].parent_index, host_attachments[index].parent_marker_index, + host_attachments[index].child_index, host_attachments[index].child_marker_index); + index++; + } +} + +void network_coop_note_attach( + long parent_index, + char const *parent_marker_name, + long child_index, + char const *child_marker_name) +{ + short parent_marker_index, child_marker_index; + short index; + + if (!coop_host() || !object_try_and_get(parent_index) || !object_try_and_get(child_index)) + return; + parent_marker_index = object_marker_index(parent_index, parent_marker_name); + child_marker_index = object_marker_index(child_index, child_marker_name); + send_attach(_coop_attach, parent_index, parent_marker_index, child_index, child_marker_index); + + index = host_attachment_find(child_index); + if (index == NONE) + { + if (host_attachment_count == MAXIMUM_ATTACHMENTS) + return; + index = host_attachment_count++; + } + host_attachments[index].parent_index = parent_index; + host_attachments[index].child_index = child_index; + host_attachments[index].parent_marker_index = parent_marker_index; + host_attachments[index].child_marker_index = child_marker_index; +} + +void network_coop_note_detach( + long parent_index, + long child_index) +{ + short index; + + if (!coop_host()) + return; + send_attach(_coop_detach, parent_index, NONE, child_index, NONE); + index = host_attachment_find(child_index); + if (index != NONE) + host_attachments[index] = host_attachments[--host_attachment_count]; +} + +void network_coop_note_effect( + long effect_definition_index, + short cutscene_flag_index) +{ + struct distributed_coop_event *event = event_new(_coop_event_effect); + + if (!event) + return; + event->type = _coop_effect_at_flag; + event->tag_index = effect_definition_index; + event->value = cutscene_flag_index; +} + +void network_coop_note_object_effect( + long effect_definition_index, + long object_index, + char const *marker_name) +{ + struct object_datum *object = object_try_and_get(object_index); + long model_index = object ? object_definition_get(object->definition_index)->object.model.index : NONE; + short marker_index = model_index != NONE ? model_find_marker(model_index, marker_name) : NONE; + struct distributed_coop_event *event; + + if (marker_index == NONE || !(event = event_new(_coop_event_effect))) + return; + event->type = _coop_effect_on_marker; + event->tag_index = effect_definition_index; + event->object_index = object_index; + event->name_index = object->object.name_index; + event->definition_index = object->definition_index; + event->value = marker_index; +} + +boolean network_coop_skip_offered( + void) +{ + return coop_client() && skip_vote.offered && coop_presentation.cinematic_started; +} + +boolean network_coop_vote_skip( + void) +{ + if (!coop_host() && !coop_client()) + return FALSE; + skip_vote.voted = TRUE; + return TRUE; +} + +/* The skip reverted the host's game state, clock included. The clock is +put back, since clients and the netcode expect it to only go forward, and +the script threads' wake times move by the same amount so they still wake +when they would have. The revert also renumbered the input queues from the +old clock (update_queues_reset_and_fill_with_lies); they are renumbered +from the restored one, or no tick would run until they caught up. And the +revert is stamped with the restored time, since the script's game_reverted +compares the stamp with the clock: otherwise it would play the cinematic +it was meant to skip. */ +void network_coop_skip_reverted( + long now) +{ + long ticks; + + if (!coop_host()) + return; + ticks = now - game_time_get(); + if (ticks > 0) + { + game_time_set_distributed(now); + update_queues_reset_and_fill_with_lies(); + game_state_port_restamp_revert_time(); + hs_runtime_port_shift_sleep_times(ticks); + } + /* (the dropships' riders kept are of the game state reverted from) */ + coop_enemies_reset(); + error(_error_silent, "co-op: cutscene skipped; reverted %ld ticks, clock kept at %ld", ticks, now); + event_new(_coop_event_cutscene_skipped); + skip_vote_clear(); + skip_vote.requested = FALSE; + skip_vote.cooldown_until = game_time_get() + SKIP_COOLDOWN_TICKS; +} + +boolean network_coop_skip_vote_status( + short *votes, + short *voters, + boolean *voted) +{ + boolean offered = coop_host() ? skip_vote.offered : network_coop_skip_offered(); + + if (!offered || skip_vote.voters <= 0) + return FALSE; + *votes = skip_vote.votes; + *voters = skip_vote.voters; + *voted = skip_vote.voted; + return TRUE; +} + +/* host, after each tick */ +void network_coop_host_tick( + void) +{ + struct distributed_coop_presentation_message message; + + if (!coop_game()) + return; + host_resend_update(); + players_vitality_keep(); + coop_enemies_update(); + host_count_skip_votes(); + host_presentation(&message.presentation); + send_to_clients(&message, _distributed_message_coop_presentation, 1, sizeof(message.presentation)); + host_send_device_groups(); + if (host_resend.joined || game_time_get() % OBJECT_NAMES_INTERVAL_TICKS == 0) + host_send_object_names(); + host_send_object_transforms(); + host_send_device_states(); + host_send_object_looks(); + host_send_screen_effect(); + host_send_attachments(); + host_send_looping_sounds(); + host_send_hud_state(); + host_send_events(); +} + +/* client, after each tick */ +void network_coop_client_tick( + void) +{ + /* (a multiplayer game's input and camera are its own) */ + if (!coop_client()) + return; + client_play_presentation(); + players_vitality_keep(); + if (game_time_get() - coop_presentation.heard_time > PRESENTATION_SILENCE_TICKS) + { + client_cinematic_end(); + client_host_camera_set(FALSE); + player_input_enable(TRUE); + } + /* sent every tick while a skip is offered, voted or not, because the + host only counts machines it hears from */ + if (!network_coop_skip_offered()) + skip_vote.voted = FALSE; + else + { + struct distributed_coop_skip_vote_message message; + + csmemset(&message.vote, 0, sizeof(message.vote)); + message.vote.voted = skip_vote.voted; + distributed_send(&message, _distributed_message_coop_skip_vote, 1, (word)sizeof(message), _distributed_to_host); + } +} + +word network_coop_presentation_entry_size( + void) +{ + return sizeof(struct distributed_coop_presentation); +} + +word network_coop_event_entry_size( + void) +{ + return sizeof(struct distributed_coop_event); +} + +word network_coop_device_group_entry_size( + void) +{ + return sizeof(struct distributed_coop_device_group); +} + +void network_coop_note_player_structure_bsp( + short player_index, + short structure_bsp_index) +{ + if (player_index >= 0 && player_index < MAXIMUM_TRACKED_PLAYERS) + host_player_structure_bsps[player_index] = structure_bsp_index; +} + +boolean network_coop_player_has_structure_bsp( + long player_index) +{ + return !network_coop_active() || player_get(player_index)->local_player_index != NONE || + host_player_structure_bsps[DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index)] == global_structure_bsp_index_get(); +} + +word network_coop_device_state_entry_size( + void) +{ + return sizeof(struct distributed_coop_device_state); +} + +/* client: its devices where the host's are (DEVICE_POSITION_TOLERANCE) */ +void network_coop_handle_device_states( + void const *entries, + short count) +{ + struct distributed_coop_device_state const *states = entries; + short index; + + if (!coop_client()) + return; + for (index = 0; index < count; index++) + { + struct distributed_coop_device_state const *state = &states[index]; + long device_index = object_find(state->name_index, state->object_index, state->definition_index, + _object_mask_device); + struct device_datum *device; + real difference; + + if (device_index == NONE || !distributed_real_valid(state->position) || !distributed_real_valid(state->power)) + continue; + device = object_get_and_verify_type(device_index, _object_mask_device); + difference = device->device.position - state->position; + if (difference < 0.0f) + difference = -difference; + device_port_set_state(device_index, state->power, + (!state->moving && difference > 0.001f) || difference > DEVICE_POSITION_TOLERANCE, state->position); + } +} + +word network_coop_object_transform_entry_size( + void) +{ + return sizeof(struct distributed_coop_object_transform); +} + +void network_coop_handle_object_transforms( + void const *entries, + short count) +{ + struct distributed_coop_object_transform const *transforms = entries; + short index; + + if (!coop_client()) + return; + for (index = 0; index < count; index++) + { + struct distributed_coop_object_transform const *transform = &transforms[index]; + long object_index = object_find(transform->name_index, transform->object_index, transform->definition_index, + COOP_MOVED_OBJECTS); + real_vector3d forward, up; + + if (object_index == NONE || object_get(object_index)->object.parent_object_index != NONE) + continue; + distributed_vector_unpack(&transform->forward, DISTRIBUTED_UNIT_SCALE, &forward); + distributed_vector_unpack(&transform->up, DISTRIBUTED_UNIT_SCALE, &up); + if (distributed_point_valid(&transform->position, UNIT_WORLD_BOUND) && distributed_axes_make_valid(&forward, &up)) + object_set_position(object_index, &transform->position, &forward, &up); + } +} + +word network_coop_object_look_entry_size( + void) +{ + return sizeof(struct distributed_coop_object_look); +} + +void network_coop_handle_object_looks( + void const *entries, + short count) +{ + struct distributed_coop_object_look const *looks = entries; + short index; + + if (!coop_client()) + return; + for (index = 0; index < count; index++) + { + struct distributed_coop_object_look const *look = &looks[index]; + long object_index = object_find(look->name_index, look->object_index, look->definition_index, _object_mask_all); + struct object_datum *object = object_index != NONE ? object_get(object_index) : NULL; + long model_index = object ? object_definition_get(object->definition_index)->object.model.index : NONE; + struct model *model = model_index != NONE ? model_definition_get(model_index) : NULL; + short region_index; + + if (!object) + continue; + /* only a permutation its model has, or none */ + for (region_index = 0; model && region_index < model->regions.count && + region_index < MAXIMUM_REGIONS_PER_OBJECT; region_index++) + { + byte permutation_index = look->region_permutations[region_index]; + + if (permutation_index == (byte)NONE || + permutation_index < TAG_BLOCK_GET_ELEMENT(&model->regions, region_index, struct model_region)->permutations.count) + { + object->object.region_permutations[region_index] = permutation_index; + } + } + if (distributed_real_valid(look->scale) && look->scale > 0.0f && look->scale < 100.0f && + look->scale != object->object.scale) + { + objects_scripting_set_scale(object_index, look->scale, 0); + } + } +} + +word network_coop_object_names_entry_size( + void) +{ + return sizeof(struct distributed_coop_object_names); +} + +word network_coop_skip_vote_entry_size( + void) +{ + return sizeof(struct distributed_coop_skip_vote); +} + +/* host: a client's skip vote */ +void network_coop_handle_skip_vote( + long machine_index, + void const *entries) +{ + struct distributed_coop_skip_vote const *vote = entries; + + if (!coop_host() || machine_index < 0 || machine_index >= HALO_PORT_MAXIMUM_NETWORK_MACHINES) + return; + skip_vote.client_heard_times[machine_index] = game_time_get(); + skip_vote.client_vote_times[machine_index] = vote->voted ? game_time_get() : NONE; +} + +void network_coop_handle_device_groups( + void const *entries, + short count) +{ + struct distributed_coop_device_group const *groups = entries; + short index; + + if (!coop_client()) + return; + for (index = 0; index < count; index++) + { + struct distributed_coop_device_group const *entry = &groups[index]; + short group_index = device_group_find(entry); + real value, here; + word flags; + boolean runtime, snap; + + if (group_index == NONE || !device_group_network_get(group_index, &here, &flags, &runtime)) + continue; + if (!distributed_real_valid(entry->value)) + continue; + value = PIN(entry->value, 0.0f, 1.0f); + /* Jump straight to the value when the host did, or when a rotation + entry disagrees with us (we missed the change, or joined late). + A change entry animates, as it did on the host. */ + snap = (client_devices.seen[group_index] && entry->snaps != client_devices.snaps[group_index]) || + (!entry->changed && value != here); + if (snap || value != here || entry->flags != (byte)flags) + device_group_network_set(group_index, value, entry->flags, snap); + client_devices.snaps[group_index] = entry->snaps; + client_devices.seen[group_index] = TRUE; + } +} + +/* Client: creates or deletes named scenery and devices to match the host. +A difference must show up twice in a row, on the same BSP, so a message +that crosses a BSP switch or a script's create can't cause a false one. */ +void network_coop_handle_object_names( + void const *entries) +{ + struct distributed_coop_object_names const *names = entries; + short name_index; + + if (!coop_client()) + return; + if (names->structure_bsp_index != global_structure_bsp_index_get() || + names->name_count != MIN(global_scenario_get()->object_names.count, MAXIMUM_OBJECT_NAMES_PER_SCENARIO)) + { + csmemset(client_names_differing, 0, sizeof(client_names_differing)); + return; + } + for (name_index = 0; name_index < names->name_count; name_index++) + { + byte *differing = &client_names_differing[name_index / 8]; + byte bit = (byte)(1 << (name_index % 8)); + boolean host_has = (names->present[name_index / 8] & bit) != 0; + long object_index = object_index_from_name_index(name_index); + short type = object_name_type(name_index); + + if (type < 0 || type >= NUMBER_OF_OBJECT_TYPES || TEST_FLAG(NETWORKED_OBJECT_TYPES, type) || + host_has == (object_index != NONE)) + { + *differing &= (byte)~bit; + continue; + } + if (!(*differing & bit)) + { + *differing |= bit; + continue; + } + *differing &= (byte)~bit; + if (host_has) + object_new_by_name(name_index); + else if (!object_holds_player(object_index)) + object_delete(object_index); + } +} + +/* client: applies each event once, in order */ +void network_coop_handle_events( + void const *entries, + short count) +{ + struct distributed_coop_event const *events = entries; + short index; + + if (!coop_client()) + return; + for (index = 0; index < count; index++) + { + /* already applied (the comparison handles the number wrapping) */ + if (coop_events.applied_any && (short)(events[index].number - coop_events.applied_number) <= 0) + continue; + coop_events.applied_number = events[index].number; + coop_events.applied_any = TRUE; + /* (not with a number that isn't one) */ + if (!distributed_real_valid(events[index].reals[0]) || !distributed_real_valid(events[index].reals[1]) || + !distributed_real_valid(events[index].reals[2])) + { + continue; + } + client_apply_event(&events[index]); + } +} + +/* client: keeps the host's presentation of tick host_time, to be played +in its turn (client_play_presentation) */ +void network_coop_handle_presentation( + void const *entries, + long host_time) +{ + short slot; + + if (!coop_client() || host_time < 0) + return; + coop_presentation.heard_time = game_time_get(); + if (coop_presentation.playing && host_time <= coop_presentation.played_time) + return; + slot = (short)(host_time % PRESENTATION_BUFFER_COUNT); + coop_presentation.buffered[slot] = *(struct distributed_coop_presentation const *)entries; + coop_presentation.buffered_times[slot] = host_time; + if (!coop_presentation.heard_any || host_time > coop_presentation.newest_time) + coop_presentation.newest_time = host_time; + coop_presentation.heard_any = TRUE; +} + +static void client_presentation_apply( + struct distributed_coop_presentation const *presentation) +{ + boolean cinematic = TEST_FLAG(presentation->flags, _presentation_cinematic_bit); + + player_input_enable(!presentation->input_disabled); + if (presentation->players_vitality_set && distributed_real_valid(presentation->players_maximum_body_vitality) && + distributed_real_valid(presentation->players_maximum_shield_vitality) && + presentation->players_maximum_body_vitality > 0.0f && presentation->players_maximum_shield_vitality >= 0.0f) + { + players_vitality.set = TRUE; + players_vitality.maximum_body = presentation->players_maximum_body_vitality; + players_vitality.maximum_shield = presentation->players_maximum_shield_vitality; + } + game_allegiance_set_teams(presentation->ally_teams, presentation->friendly_teams); + if (!presentation->scripted_shake && player_effect_port_scripted_active()) + player_effect_port_scripted_end(); + + /* a machine that joined mid-cutscene may already have started it */ + if (cinematic && !coop_presentation.cinematic_started) + { + if (!cinematic_in_progress()) + cinematic_start(); + coop_presentation.cinematic_started = TRUE; + } + else if (!cinematic) + { + client_cinematic_end(); + } + /* Use the host's camera while its scripts control it. Spectate instead + when the cutscene leaves the camera with the player, or when the watched + teammate rides an AI-flown Pelican (a level's insertion): shots filmed + beside a moving Pelican shake against this machine's copy of it. A player + with no unit and nobody to watch also gets the host's view, rather than + a dead camera at the world origin. */ + client_host_camera_set((coop_presentation.cinematic_started && presentation->camera_scripted && + !coop_spectate_watching_rider(0)) || coop_spectate_nothing_to_watch(0)); + if (coop_presentation.cinematic_started) + cinematic_show_letterbox(TEST_FLAG(presentation->flags, _presentation_letterbox_bit)); + if (coop_presentation.host_camera) + { + real_vector3d forward, up; + + distributed_vector_unpack(&presentation->camera_forward, DISTRIBUTED_UNIT_SCALE, &forward); + distributed_vector_unpack(&presentation->camera_up, DISTRIBUTED_UNIT_SCALE, &up); + real_point3d position = presentation->camera_position; + struct object_datum *filmed = presentation->camera_object_index != NONE && + distributed_object_index_valid(presentation->camera_object_index) ? + object_try_and_get(presentation->camera_object_index) : NULL; + + /* place the camera relative to this machine's copy of the filmed + object, which may be a tick behind the host's */ + if (filmed && distributed_real_valid(presentation->camera_object_offset.i) && + distributed_real_valid(presentation->camera_object_offset.j) && + distributed_real_valid(presentation->camera_object_offset.k) && + magnitude_squared3d(&presentation->camera_object_offset) < CAMERA_OBJECT_DISTANCE * CAMERA_OBJECT_DISTANCE * 4.0f) + { + point_from_line3d(&filmed->object.position, &presentation->camera_object_offset, 1.0f, &position); + } + /* (where and how widely an observer can look: observer.c's + match_assert_valid_observer_command) */ + if (distributed_point_valid(&position, CAMERA_WORLD_BOUND) && distributed_axes_make_valid(&forward, &up)) + { + if (!(coop_presentation.cinematic_started && presentation->camera_scripted)) + client_watch_host_from_behind(&position, &forward, &up); + scripted_camera_set_camera_point_relative(&position, &forward, &up, + PIN((real)presentation->camera_field_of_view / FIELD_OF_VIEW_SCALE, CAMERA_MINIMUM_FIELD_OF_VIEW, + CAMERA_MAXIMUM_FIELD_OF_VIEW), 0, NONE); + } + } + else + { + coop_presentation.watching_host_valid = FALSE; + } + + /* start each new fade, backdated to when the host started it */ + if (presentation->fade_start_time != coop_presentation.fade_start_time) + { + real_rgb_color color; + + color.red = presentation->fade_color[0] / 255.0f; + color.green = presentation->fade_color[1] / 255.0f; + color.blue = presentation->fade_color[2] / 255.0f; + player_effect_port_screen_fade_set(&color, presentation->fade_ticks, + TEST_FLAG(presentation->flags, _presentation_fading_out_bit), + game_time_get() - presentation->fade_elapsed); + coop_presentation.fade_start_time = presentation->fade_start_time; + } + + skip_vote.offered = TEST_FLAG(presentation->flags, _presentation_skippable_bit); + skip_vote.votes = presentation->skip_votes; + skip_vote.voters = presentation->skip_voters; + client_apply_hud_state(presentation); +} + +/* client, each tick: plays the next of the host's presentations. It moves +one host tick a tick, two when it has fallen behind, and waits when it has +caught up with the newest; a lost tick's place is taken by the one before. */ +static void client_play_presentation( + void) +{ + struct distributed_coop_presentation const *presentation = NULL; + long lag, best_time = NONE; + short index; + + if (!coop_presentation.heard_any) + return; + lag = coop_presentation.newest_time - coop_presentation.played_time; + if (!coop_presentation.playing || lag > PRESENTATION_BUFFER_COUNT || lag < -PRESENTATION_BUFFER_COUNT) + coop_presentation.played_time = coop_presentation.newest_time - PRESENTATION_DELAY_TICKS; + else if (lag > PRESENTATION_DELAY_TICKS + 1) + coop_presentation.played_time += 2; + else if (lag > 0) + coop_presentation.played_time++; + else + return; + coop_presentation.playing = TRUE; + for (index = 0; index < PRESENTATION_BUFFER_COUNT; index++) + { + long time = coop_presentation.buffered_times[index]; + + if (time <= coop_presentation.played_time && time > coop_presentation.played_time - PRESENTATION_BUFFER_COUNT && + time > best_time) + { + best_time = time; + presentation = &coop_presentation.buffered[index]; + } + } + if (presentation) + client_presentation_apply(presentation); +} diff --git a/port/linux/game/network_coop.h b/port/linux/game/network_coop.h new file mode 100644 index 00000000..30c330d7 --- /dev/null +++ b/port/linux/game/network_coop.h @@ -0,0 +1,105 @@ +/* +NETWORK_COOP.H + +The calls Halo's own code (source/) makes into network_coop.c. On a co-op +host the note_ functions send what the scripts just did to the clients; on +any other machine they do nothing, so the clients can call the same engine +functions to apply what they receive. +*/ + +#ifndef __NETWORK_COOP_H +#define __NETWORK_COOP_H + +/* ---------- constants */ + +/* network_coop_note_hud */ +enum +{ + _coop_hud_help_text, + _coop_hud_objective, + _coop_hud_help_flash, + _coop_hud_messages_clear, + _coop_hud_checkpoint, +}; + +/* network_coop_note_player_effect */ +enum +{ + _coop_player_effect_translation, + _coop_player_effect_rotation, + _coop_player_effect_start, + _coop_player_effect_stop, +}; + +/* network_coop_note_nav_point */ +enum +{ + _coop_nav_point_team_flag, + _coop_nav_point_team_object, + _coop_nav_point_unit_flag, + _coop_nav_point_unit_object, +}; + +/* ---------- prototypes/NETWORK_COOP.C */ + +/* whether this is a network co-op game (not its lobby, whose menu scene also +has no game engine) */ +boolean network_coop_active(void); +/* whether this machine is a co-op client, whose devices only the host moves */ +boolean network_coop_devices_remote(void); +/* devices.c: a group's devices were set straight to its value */ +void network_coop_note_device_snap(short group_index); +/* unit_scripting_commands.c: a script set a unit's maximum or current +vitality; TRUE if it was a co-op player's, now set on every player's */ +boolean network_coop_set_players_vitality(long unit_index, boolean maximum, real body, real shield); +/* host: whether the player's machine has the host's structure BSP loaded +(always outside co-op, and for the host's own players). Until a client has, +the host takes none of its players' movement, which falls there with no +floor, and lets them trigger no BSP switch (players.c). */ +boolean network_coop_player_has_structure_bsp(long player_index); +/* network_distributed.c: the structure BSP a client's input says it has */ +void network_coop_note_player_structure_bsp(short player_index, short structure_bsp_index); +/* units.c: a vehicle left the vehicle carrying it (a Pelican's Warthog drop) */ +void network_coop_vehicle_dropped(long vehicle_index, long carrier_index); + +/* cinematics.c: a chapter title */ +void network_coop_note_title(short title_index, real delay); +/* hud_messaging.c, hud.c: value is a message index, or TRUE/FALSE */ +void network_coop_note_hud(short kind, short value); +/* player_effects.c: the script screen shake (a, b, c as the function takes them) */ +void network_coop_note_player_effect(short kind, real a, real b, real c); +/* hud_nav_points.c: activate (or, with nav_index NONE, deactivate) a nav point. +target is the team or the unit; marker is the cutscene flag or the object. */ +void network_coop_note_nav_point(short kind, short nav_index, long target, long marker, real vertical_offset); + +/* units.c: a unit started a custom animation (animation_index NONE: stopped) */ +void network_coop_note_unit_animation(long unit_index, long animation_graph_index, short animation_index, + boolean interpolate); +/* units.c: a unit (a dropship) opened or closed (unit_open, unit_close) */ +void network_coop_note_unit_open(long unit_index, boolean open); +/* units.c: unit_custom_animation_at_frame moved it to a frame */ +void network_coop_note_unit_animation_frame(long unit_index, short frame_index); +/* scenery.c: a scenery animation started */ +/* the scripts' effects (hs_library_external.c): at a cutscene flag, or on an +object's marker */ +void network_coop_note_effect(long effect_definition_index, short cutscene_flag_index); +/* the scripts' objects_attach and objects_detach (objects.c) */ +void network_coop_note_attach(long parent_index, char const *parent_marker_name, long child_index, + char const *child_marker_name); +void network_coop_note_detach(long parent_index, long child_index); +void network_coop_note_object_effect(long effect_definition_index, long object_index, char const *marker_name); +void network_coop_note_scenery_animation(long object_index, long animation_graph_index, short animation_index, + short frame_index); + +/* player_control.c: whether the skip key should do anything, and the press. +network_coop_vote_skip returns FALSE outside network co-op, where the +cinematic is skipped as usual. */ +boolean network_coop_skip_offered(void); +boolean network_coop_vote_skip(void); +/* main.c: after a co-op host reverts to skip a cinematic; now is the game +time before the revert */ +void network_coop_skip_reverted(long now); +/* the vote count to show, if a skippable cinematic is playing */ +boolean network_coop_skip_vote_status(short *votes, short *voters, boolean *voted); + +#endif diff --git a/port/linux/game/network_damage.c b/port/linux/game/network_damage.c index 0c23555d..e8d76efb 100644 --- a/port/linux/game/network_damage.c +++ b/port/linux/game/network_damage.c @@ -16,8 +16,9 @@ being hit looks and feels like on the clients). - A client deals no damage itself. What its own players' shots, grenades, melee and vehicles hit it reports to the host, which deals it once it has checked it: the report is of that machine's player, the damage one that - player's weapons (now or lately), grenades or the vehicle they drove - deal, of the shape the game gives it (at a point, a melee blow from the + player's weapons (now or lately), a grenade the host saw them throw + (each throw's explosion once) or the vehicle they drove deal, of the + shape the game gives it (at a point, a melee blow from the striker, a collision from the vehicle), the target about where the host had it when the shooter saw it (the host keeps a second of where players' units and vehicles were, and looks back as far as the report @@ -36,6 +37,7 @@ being hit looks and feels like on the clients). */ #include "cseries.h" +#include "cseries/errors.h" #include "game/game.h" #include "game/game_globals.h" #include "game/players.h" @@ -81,6 +83,13 @@ enum died or dropped the launcher) */ MAXIMUM_RECENT_WEAPONS = 8, RECENT_WEAPON_TICKS = 10 * TICKS_PER_SECOND, + /* the grenades a player has thrown lately, as the host's own game threw + them, whose damage the host takes from them: each goes off once, within + RECENT_WEAPON_TICKS of its throw. Each player has their own (all of + MAXIMUM_TRACKED_PLAYERS); one that hits nothing is kept the whole time, + so this is more than a player throws in it, infinite grenades and all + (a throw takes most of a second); past it the oldest gives way */ + MAXIMUM_GRENADE_THROWS = 32, /* a player's hits: a bucket of this many seconds of fire, filling at a second a second, a hit taking what one of its weapon's projectiles takes to fire (at twice the weapon's fastest rate: a shotgun's @@ -341,7 +350,9 @@ static struct { long definition_indices[MAXIMUM_RECENT_WEAPONS]; long times[MAXIMUM_RECENT_WEAPONS]; - long grenade_times[NUMBER_OF_UNIT_GRENADE_TYPES]; + /* (the grenades thrown, not yet gone off: NONE for none) */ + long throw_times[MAXIMUM_GRENADE_THROWS]; + short throw_types[MAXIMUM_GRENADE_THROWS]; long driven_time; /* (the blow of the player's unit with no weapon: units.c's unit_unarmed_melee_damage) */ @@ -1078,21 +1089,74 @@ static real distributed_source_deals( return distributed_source_deals_uncached(source_index, damage_index, grenade, kinds, reach); } +/* the oldest grenade of the type the host saw the player throw within +RECENT_WEAPON_TICKS that has not gone off yet (network_damage_note_grenade), +NONE for none */ +static short distributed_grenade_throw( + short player_index, + short grenade_type) +{ + short index; + short oldest = NONE; + + for (index = 0; index < MAXIMUM_GRENADE_THROWS; index++) + { + long time = damage_players[player_index].throw_times[index]; + + if (time != NONE && damage_players[player_index].throw_types[index] == grenade_type && + game_time_get() - time <= RECENT_WEAPON_TICKS && + (oldest == NONE || time < damage_players[player_index].throw_times[oldest])) + { + oldest = index; + } + } + return oldest; +} + +/* whether an explosion of the damage is among the player's this tick +(distributed_report_paid): at the epicenter, or (NULL) anywhere */ +static boolean distributed_explosion_this_tick( + short player_index, + long damage_index, + real_point3d const *epicenter) +{ + short index; + + if (damage_explosions[player_index].time != game_time_get()) + return FALSE; + for (index = 0; index < damage_explosions[player_index].count; index++) + { + if (damage_explosions[player_index].explosions[index].definition_index == damage_index && + (!epicenter || (damage_explosions[player_index].explosions[index].epicenter.x == epicenter->x && + damage_explosions[player_index].explosions[index].epicenter.y == epicenter->y && + damage_explosions[player_index].explosions[index].epicenter.z == epicenter->z))) + { + return TRUE; + } + } + return FALSE; +} + /* how many hits a second the player could deal of the damage: their -weapons, lately, their grenades, the vehicle they drove lately; 0 when none -of them deals it; how they deal it (the _damage_source flags); and how far -from them what they fire deals it */ +weapons, lately, a grenade the host saw them throw that has not gone off +(or one going off this tick, hitting more than one object), the vehicle +they drove lately; 0 when none of them deals it; how they deal it (the +_damage_source flags); how far from them what they fire deals it; and the +type of grenade when nothing but a grenade of theirs deals it, else NONE */ static real distributed_player_deals( short player_index, long damage_index, byte *kinds, - struct damage_reach *reach) + struct damage_reach *reach, + short *grenade_type) { struct game_globals *globals = scenario_get_game_globals(); short index; real rate = 0.0f; + real grenade_rate = 0.0f; *kinds = 0; + *grenade_type = NONE; reach->distance = 0.0f; reach->ticks = 0.0f; reach->carried_ticks = 0.0f; @@ -1112,25 +1176,6 @@ static real distributed_player_deals( *kinds |= weapon_kinds; } } - for (index = 0; index < globals->grenades.count && index < NUMBER_OF_UNIT_GRENADE_TYPES; index++) - { - struct game_globals_grenade *grenade = TAG_BLOCK_GET_ELEMENT(&globals->grenades, index, - struct game_globals_grenade); - - if (game_time_get() - damage_players[player_index].grenade_times[index] <= RECENT_WEAPON_TICKS && - grenade->projectile.index != NONE) - { - byte grenade_kinds; - struct damage_reach grenade_reach; - real grenade_rate = distributed_source_deals(grenade->projectile.index, damage_index, TRUE, &grenade_kinds, - &grenade_reach); - - if (grenade_rate > 0.0f) - distributed_reach_combine(reach, &grenade_reach); - rate = MAX(rate, grenade_rate); - *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) @@ -1152,6 +1197,36 @@ static real distributed_player_deals( SET_FLAG(*kinds, _damage_source_collision_bit, TRUE); } } + /* (a grenade's damage only from one thrown: holding grenades deals + nothing) */ + for (index = 0; index < globals->grenades.count && index < NUMBER_OF_UNIT_GRENADE_TYPES; index++) + { + struct game_globals_grenade *grenade = TAG_BLOCK_GET_ELEMENT(&globals->grenades, index, + struct game_globals_grenade); + boolean thrown = distributed_grenade_throw(player_index, index) != NONE; + + if ((thrown || distributed_explosion_this_tick(player_index, damage_index, NULL)) && + grenade->projectile.index != NONE) + { + byte this_kinds; + struct damage_reach this_reach; + real this_rate = distributed_source_deals(grenade->projectile.index, damage_index, TRUE, &this_kinds, + &this_reach); + + if (this_rate > 0.0f) + { + distributed_reach_combine(reach, &this_reach); + /* (one with a throw left before one only going off now) */ + if (*grenade_type == NONE || thrown) + *grenade_type = index; + } + grenade_rate = MAX(grenade_rate, this_rate); + *kinds |= this_kinds; + } + } + if (rate > 0.0f) + *grenade_type = NONE; + rate = MAX(rate, grenade_rate); return rate * HIT_REPORT_RATE_MARGIN; } @@ -1432,16 +1507,6 @@ static void distributed_note_weapons( if (vehicle && vehicle->unit.driver_object_index == unit_index) damage_players[player_index].driven_time = game_time_get(); } - { - struct unit_datum *unit = unit_get(unit_index); - short grenade_index; - - for (grenade_index = 0; grenade_index < NUMBER_OF_UNIT_GRENADE_TYPES; grenade_index++) - { - if (unit->unit.grenade_counts[grenade_index] > 0) - damage_players[player_index].grenade_times[grenade_index] = game_time_get(); - } - } for (unit_number = 0; unit_number < 2; unit_number++) { struct unit_datum *unit = units[unit_number] != NONE ? @@ -1639,6 +1704,8 @@ static boolean distributed_report_valid( real reach; struct damage_reach fired_reach; short seen; + short grenade_type; + short throw_index = NONE; /* that machine's player */ if (player_index == NO_PLAYER || player_index >= MAXIMUM_TRACKED_PLAYERS || @@ -1671,7 +1738,8 @@ static boolean distributed_report_valid( return FALSE; ticks = MIN(now - report->host_time + TARGET_HISTORY_SLACK_TICKS, TARGET_HISTORY_TICKS - 1); /* damage that player could deal, and how they deal it */ - rate = distributed_player_deals(player_index, report->damage.definition_index, &kinds, &fired_reach); + rate = distributed_player_deals(player_index, report->damage.definition_index, &kinds, &fired_reach, + &grenade_type); if (rate <= 0.0f) return FALSE; /* ... of its shape: at a point, else a melee blow, else a collision; and @@ -1713,12 +1781,26 @@ static boolean distributed_report_valid( reach += definition->cutoff_radius; if (!(distributed_distance_squared(&report->damage.origin, &report->target_position) <= reach * reach)) return FALSE; + /* what only a grenade of theirs deals, from one the host saw them throw + that has not gone off: its explosion once (the explosion's other objects + that tick come free, distributed_report_paid), what it hits at a point + while it is about */ + if (grenade_type != NONE && + !distributed_explosion_this_tick(player_index, report->damage.definition_index, &report->damage.epicenter)) + { + throw_index = distributed_grenade_throw(player_index, grenade_type); + if (throw_index == NONE) + return FALSE; + } /* no more than the weapon fires */ if (!distributed_report_paid(player_index, report, rate, area && !melee && !collision, definition->cutoff_radius > 0.0f)) { return FALSE; } + /* (the grenade gone off: its throw spent) */ + if (throw_index != NONE && area && !melee && !collision) + damage_players[player_index].throw_times[throw_index] = NONE; /* a melee blow from where the host had the player (its origin their head, its epicenter their body); a collision from the vehicle they drove */ @@ -1939,6 +2021,26 @@ static boolean distributed_event_goes_to( return FALSE; } +/* How often the host logs how many client hits it accepted and refused +(only when it refused some). Tells "my shots don't register" apart from +"I missed". */ +#define REPORT_LOG_TICKS (30 * TICKS_PER_SECOND) + +static void distributed_log_reports( + void) +{ + static long logged_dealt; + static long logged_rejected; + + if (game_time_get() % REPORT_LOG_TICKS != 0 || damage_rejected_reports == logged_rejected) + return; + error(_error_silent, "damage: clients' hits in the last %d seconds: %ld dealt, %ld refused", + REPORT_LOG_TICKS / TICKS_PER_SECOND, damage_dealt_reports - logged_dealt, + damage_rejected_reports - logged_rejected); + logged_dealt = damage_dealt_reports; + logged_rejected = damage_rejected_reports; +} + void network_damage_host_tick( void) { @@ -1949,6 +2051,7 @@ void network_damage_host_tick( distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES) : 0; short machine_number; + distributed_log_reports(); distributed_note_weapons(); distributed_note_targets(); distributed_note_event_destinations(); @@ -2102,6 +2205,41 @@ void network_damage_statistics( *replayed_events = damage_replayed_events; } +/* the host: a grenade a player's unit threw (units.c), as the host's own +game threw it, which that player's machine reports the damage of +(distributed_report_valid) */ +void network_damage_note_grenade( + long unit_index, + short grenade_type) +{ + struct unit_datum *unit = unit_get(unit_index); + long player_index; + short index; + short slot = 0; + + if (game_connection() != _game_connection_network_server || unit->unit.player_index == NONE || + grenade_type < 0 || grenade_type >= NUMBER_OF_UNIT_GRENADE_TYPES) + { + return; + } + player_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(unit->unit.player_index); + if (player_index >= MAXIMUM_TRACKED_PLAYERS) + return; + /* (in a free place, else the oldest's) */ + for (index = 0; index < MAXIMUM_GRENADE_THROWS; index++) + { + if (damage_players[player_index].throw_times[index] == NONE) + { + slot = index; + break; + } + if (damage_players[player_index].throw_times[index] < damage_players[player_index].throw_times[slot]) + slot = index; + } + damage_players[player_index].throw_times[slot] = game_time_get(); + damage_players[player_index].throw_types[slot] = grenade_type; +} + void network_damage_new_game( void) { @@ -2121,8 +2259,8 @@ 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; + for (index = 0; index < MAXIMUM_GRENADE_THROWS; index++) + damage_players[player_index].throw_times[index] = NONE; damage_players[player_index].driven_time = -RECENT_WEAPON_TICKS - 1; damage_players[player_index].hit_seconds = HIT_REPORT_BURST_SECONDS; damage_explosions[player_index].time = NONE; diff --git a/port/linux/game/network_distributed.c b/port/linux/game/network_distributed.c index 0cafde65..8fba9e47 100644 --- a/port/linux/game/network_distributed.c +++ b/port/linux/game/network_distributed.c @@ -53,18 +53,24 @@ machine (their datum identifiers need not be). #include "cseries.h" #include "cseries/errors.h" +#include "cache/cache_files.h" +#include "models/model_animation_definitions.h" #include "game/game.h" #include "game/game_globals.h" #include "game/players.h" +#include "game/game_engine.h" +#include "main/main.h" #include "game/player_queues_new.h" #include "networking/network_game_globals.h" #include "objects/objects.h" #include "objects/damage.h" #include "scenario/scenario.h" +#include "scenario/scenario_definitions.h" #include "structures/structure_bsp_definitions.h" #include "units/units.h" #include "units/biped_definitions.h" #include "units/bipeds.h" +#include "network_coop.h" #include "network_distributed.h" #include @@ -238,11 +244,8 @@ before the host takes it no longer, and before the client is put where the host has it (no further than that: between the two they would disagree for good, the host's player somewhere its own is not) */ #define HOST_ACCEPT_TOLERANCE 3.5f -#define REMOTE_CORRECTION_TOLERANCE 0.05f #define LOCAL_CORRECTION_TOLERANCE 3.0f -/* how far from the origin a unit is (world units), and how fast a client's -own player's unit moves at most (world units a tick) */ -#define UNIT_WORLD_BOUND 32768.0f +/* how fast a client's own player's unit moves at most (world units a tick) */ #define MAXIMUM_PREDICTED_SPEED 2.0f /* ... on foot: this many times as fast as a player runs and jumps, or as fast as the host's ticks threw its copy lately, whichever is more; and how @@ -317,7 +320,9 @@ ticks before it */ struct distributed_player_input { byte player_index; - byte pad[3]; + /* co-op: the structure BSP the client has loaded */ + byte structure_bsp_index; + byte pad[2]; /* the client's tick */ long tick; /* the latest host tick the client has had a message of (the host @@ -666,6 +671,40 @@ boolean distributed_point_valid( return fabsf(point->x) <= bound && fabsf(point->y) <= bound && fabsf(point->z) <= bound; } +/* whether a tag index from the host really is a tag of that group */ +boolean distributed_tag_of_group( + long tag_index, + unsigned long group_tag) +{ + struct tag_iterator iterator; + long index; + + tag_iterator_new(&iterator, group_tag); + while ((index = tag_iterator_next(&iterator)) != NONE) + { + if (index == tag_index) + return TRUE; + } + + return FALSE; +} + +/* the graph's animation, if the graph tag and index are valid */ +struct animation *distributed_graph_animation( + long animation_graph_index, + short animation_index) +{ + struct animation_graph *graph; + + if (!distributed_tag_of_group(animation_graph_index, ANIMATION_GRAPH_TAG)) + return NULL; + graph = animation_graph_definition_get(animation_graph_index); + if (animation_index < 0 || animation_index >= graph->animations.count) + return NULL; + + return TAG_BLOCK_GET_ELEMENT(&graph->animations, animation_index, struct animation); +} + boolean distributed_object_index_valid( long object_index) { @@ -749,7 +788,7 @@ void distributed_unit_vector_unpack( } } -static word distributed_vitality_pack( +word distributed_vitality_pack( real value) { value *= VITALITY_SCALE; @@ -757,15 +796,13 @@ static word distributed_vitality_pack( return (word)(long)floor(value + 0.5f); } -static real distributed_vitality_unpack( +real distributed_vitality_unpack( word value) { return (real)value / VITALITY_SCALE; } -/* an angle as a 16-bit fraction of a turn, and back (yaw from 0 to 2 pi, -pitch from -pi to pi) */ -static short distributed_angle_pack( +short distributed_angle_pack( real angle) { real turns = angle / (2.0f * _pi); @@ -774,7 +811,7 @@ static short distributed_angle_pack( return (short)(word)((long)floor(turns * 65536.0f + 0.5f) & 0xFFFF); } -static real distributed_angle_unpack( +real distributed_angle_unpack( short value, boolean signed_angle) { @@ -2048,9 +2085,14 @@ static void distributed_apply_predictions( { distributed_predictions[player_index].valid = FALSE; distributed_predictions[player_index].taken_host_time = NONE; - /* (of the unit it has now: not one of a life before) */ - if (unit_index != NONE && unit_index == state->unit_index) + /* (of the unit it has now: not one of a life before; and not + from a co-op client still loading the host's BSP, whose player + falls there with no floor under it) */ + if (unit_index != NONE && unit_index == state->unit_index && + network_coop_player_has_structure_bsp(unit_get(unit_index)->unit.player_index)) + { distributed_take_prediction(player_index, unit_index, &bound); + } } /* (what the host's next tick starts from) */ if (unit_index != NONE) @@ -2222,6 +2264,7 @@ static void distributed_client_send_inputs( if (!update_client_distributed_input(local_player_index, &input->tick, &input->action, input->control_flags)) continue; input->player_index = distributed_player_to_byte(player_index); + input->structure_bsp_index = (byte)global_structure_bsp_index_get(); input->host_time = distributed_host_time; count++; } @@ -2232,6 +2275,37 @@ static void distributed_client_send_inputs( } } +/* (the host, in co-op) where each client player's input last faced, by +absolute index: how far it turns between inputs is its looking around */ +static struct +{ + boolean valid; + real_euler_angles2d facing; +} distributed_input_facings[MAXIMUM_TRACKED_PLAYERS]; + +/* (the host, in co-op) a client player's input counted in the scripts' +action tests, as a local player's is (player_control_action_test_note) */ +static void distributed_note_input_actions( + short player_index, + struct player_action const *action) +{ + real_euler_angles2d turn = { 0.0f, 0.0f }; + + if (distributed_input_facings[player_index].valid) + { + turn.yaw = action->desired_facing.yaw - distributed_input_facings[player_index].facing.yaw; + turn.pitch = action->desired_facing.pitch - distributed_input_facings[player_index].facing.pitch; + /* (the short way round) */ + if (turn.yaw > _pi) + turn.yaw -= 2.0f * _pi; + else if (turn.yaw < -_pi) + turn.yaw += 2.0f * _pi; + } + distributed_input_facings[player_index].valid = TRUE; + distributed_input_facings[player_index].facing = action->desired_facing; + player_control_action_test_note(action->control_flags, &turn, &action->throttle, action->primary_trigger); +} + /* (the host) a client's players' input, and from it (once a message) how long a message takes that client and back */ static void distributed_handle_inputs( @@ -2261,6 +2335,10 @@ static void distributed_handle_inputs( action.throttle.i = PIN(action.throttle.i, -1.0f, 1.0f); action.throttle.j = PIN(action.throttle.j, -1.0f, 1.0f); action.primary_trigger = PIN(action.primary_trigger, 0.0f, 1.0f); + /* (a spectator's buttons pick whom it watches: not counted) */ + if (network_coop_active() && player->unit_index != NONE) + distributed_note_input_actions(input->player_index, &action); + network_coop_note_player_structure_bsp(input->player_index, input->structure_bsp_index); update_server_handle_distributed_input(DATUM_INDEX_NEW(input->player_index, player->identifier), input->tick, &action, input->control_flags, DISTRIBUTED_INPUT_HISTORY); if (input->host_time != NONE && (host_time == NONE || input->host_time > host_time)) @@ -2931,6 +3009,52 @@ static void distributed_send_pings( } } +/* Co-op: the host's structure BSP. Clients switch BSP only when told +(their own trigger volumes don't, players.c). A switch is sent at once and +reliably; it is also resent regularly so a client that joined late catches +up. */ +#define STRUCTURE_BSP_INTERVAL_TICKS (TICKS_PER_SECOND / 2) + +static short distributed_sent_structure_bsp_index = NONE; + +struct distributed_structure_bsp +{ + short structure_bsp_index; + short pad; +}; + +struct distributed_structure_bsp_message +{ + struct distributed_message_header header; + struct distributed_structure_bsp structure_bsp; +}; + +static void distributed_send_structure_bsp( + void) +{ + struct distributed_structure_bsp_message message; + short index = global_structure_bsp_index_get(); + boolean switched = index != distributed_sent_structure_bsp_index; + + message.structure_bsp.structure_bsp_index = index; + message.structure_bsp.pad = 0; + distributed_send(&message, _distributed_message_structure_bsp, 1, (word)sizeof(message), + switched ? _distributed_to_clients_reliably : _distributed_to_clients); + distributed_sent_structure_bsp_index = index; +} + +static void distributed_handle_structure_bsp( + struct distributed_structure_bsp const *structure_bsp) +{ + short index = structure_bsp->structure_bsp_index; + + if (network_coop_active() && index >= 0 && index < global_scenario_get()->structure_bsp_references.count && + index != global_structure_bsp_index_get()) + { + main_switch_structure_bsp(index); + } +} + /* the players' statistics that changed since they were last sent, and when refreshing, STATISTICS_REFRESH_PLAYERS more whatever they are, round them all (a client that lost a change has it again within eight seconds) */ @@ -3076,6 +3200,7 @@ void network_distributed_new_game( csmemset(distributed_accepted, 0, sizeof(distributed_accepted)); csmemset(distributed_host_speeds, 0, sizeof(distributed_host_speeds)); csmemset(distributed_round_trips, 0, sizeof(distributed_round_trips)); + csmemset(distributed_input_facings, 0, sizeof(distributed_input_facings)); csmemset(distributed_client_clocks, 0, sizeof(distributed_client_clocks)); csmemset(distributed_client_identities, 0, sizeof(distributed_client_identities)); distributed_identity_sent = FALSE; @@ -3136,6 +3261,8 @@ void network_distributed_new_game( network_damage_new_game(); network_vr_new_game(); bots_new_game(); + network_actors_new_game(); + network_coop_new_game(); } /* after each tick (game_time.c) */ @@ -3178,9 +3305,18 @@ void network_distributed_tick( distributed_statistics_due = FALSE; if (game_time_get() % PING_INTERVAL_TICKS == 0) distributed_send_pings(); + /* (before the players, so clients load a new BSP before they hear + where the host moved everyone into it) */ + if (network_coop_active() && (global_structure_bsp_index_get() != distributed_sent_structure_bsp_index || + game_time_get() % STRUCTURE_BSP_INTERVAL_TICKS == 0)) + { + distributed_send_structure_bsp(); + } distributed_host_send_players(); network_vr_tick(); bots_tick(); + network_actors_host_tick(); + network_coop_host_tick(); distributed_send_pickups(); if (game_time_get() % GAME_STATE_INTERVAL_TICKS == 0) distributed_send_game_state(NONE); @@ -3193,6 +3329,7 @@ void network_distributed_tick( distributed_client_send_predictions(); network_objects_client_tick(); network_damage_client_tick(); + network_coop_client_tick(); } distributed_batches_flush(); distributed_machines.in_tick = FALSE; @@ -3221,6 +3358,19 @@ static boolean distributed_message_stale( case _distributed_message_damage_events: case _distributed_message_vr_poses: case _distributed_message_pings: + case _distributed_message_actor_states: + case _distributed_message_structure_bsp: + case _distributed_message_coop_presentation: + case _distributed_message_coop_device_groups: + case _distributed_message_coop_object_names: + case _distributed_message_coop_skip_vote: + case _distributed_message_coop_events: + case _distributed_message_coop_object_transforms: + case _distributed_message_actor_damage: + case _distributed_message_coop_object_looks: + case _distributed_message_damage_animations: + case _distributed_message_coop_screen_effect: + case _distributed_message_coop_device_states: break; default: return FALSE; @@ -3523,6 +3673,27 @@ void network_distributed_ban( } } +/* (the host: network_server_manager.c, its kick command) players kicked by +the host, which may join again: every machine told, nothing kept (no line +in BANS_FILE) */ +void network_distributed_kick( + char const *names) +{ + char kept_names[64]; + char notice[MAXIMUM_NOTICE_LENGTH]; + + distributed_printable(kept_names, sizeof(kept_names), names); + snprintf(notice, sizeof(notice), "%s kicked by the host", kept_names); + /* (to every client in the game: in the lobby, the host's own) */ + if (game_in_progress()) + distributed_send_notice(notice); + else + { + console_warning("%s", notice); + error(_error_log, "%s", notice); + } +} + /* (the host) a client machine's tick, which one of its messages is stamped with: its clock measured, each window, against the host's; one whose game runs fast (distributed_client_clock) has its players' @@ -3658,6 +3829,19 @@ void network_distributed_handle_message( case _distributed_message_unit_states: entry_size = DISTRIBUTED_UNIT_STATE_MINIMUM_SIZE; break; case _distributed_message_player_statistics: entry_size = sizeof(struct distributed_player_statistics); break; case _distributed_message_pings: entry_size = sizeof(struct distributed_player_ping); break; + case _distributed_message_actor_states: entry_size = network_actors_entry_size(); break; + case _distributed_message_structure_bsp: entry_size = sizeof(struct distributed_structure_bsp); break; + case _distributed_message_coop_presentation: entry_size = network_coop_presentation_entry_size(); break; + case _distributed_message_coop_events: entry_size = network_coop_event_entry_size(); break; + case _distributed_message_coop_skip_vote: entry_size = network_coop_skip_vote_entry_size(); break; + case _distributed_message_coop_device_groups: entry_size = network_coop_device_group_entry_size(); break; + case _distributed_message_coop_object_names: entry_size = network_coop_object_names_entry_size(); break; + case _distributed_message_coop_object_transforms: entry_size = network_coop_object_transform_entry_size(); break; + case _distributed_message_actor_damage: entry_size = network_actors_damage_entry_size(); break; + case _distributed_message_coop_object_looks: entry_size = network_coop_object_look_entry_size(); break; + case _distributed_message_damage_animations: entry_size = network_objects_damage_animation_entry_size(); break; + case _distributed_message_coop_screen_effect: entry_size = network_coop_screen_effect_entry_size(); break; + case _distributed_message_coop_device_states: entry_size = network_coop_device_state_entry_size(); break; case _distributed_message_pickups: entry_size = sizeof(struct distributed_pickup); break; case _distributed_message_player_inputs: entry_size = sizeof(struct distributed_player_input); break; case _distributed_message_relayed_actions: entry_size = DISTRIBUTED_RELAYED_ACTION_MINIMUM_SIZE; break; @@ -3676,6 +3860,19 @@ void network_distributed_handle_message( { return; } + /* (the kinds whose handlers read one entry: not without it) */ + switch (header.type) + { + case _distributed_message_structure_bsp: + case _distributed_message_coop_presentation: + case _distributed_message_coop_object_names: + case _distributed_message_coop_skip_vote: + if (header.count < 1) + return; + break; + default: + break; + } distributed_statistics.received++; /* (each kind from the host, or from a client; a VR player's hands @@ -3699,6 +3896,7 @@ void network_distributed_handle_message( case _distributed_message_hit_reports: case _distributed_message_vehicle_prediction: case _distributed_message_player_inputs: + case _distributed_message_coop_skip_vote: if (machine_index == NONE || game_connection() != _game_connection_network_server) return; break; @@ -3735,6 +3933,45 @@ void network_distributed_handle_message( case _distributed_message_unit_states: distributed_handle_unit_states((byte const *)entries, (byte const *)message + size, header.count); break; + case _distributed_message_actor_states: + network_actors_handle_states(entries, header.count); + break; + case _distributed_message_structure_bsp: + distributed_handle_structure_bsp((struct distributed_structure_bsp const *)entries); + break; + case _distributed_message_coop_presentation: + network_coop_handle_presentation(entries, header.game_time); + break; + case _distributed_message_coop_events: + network_coop_handle_events(entries, header.count); + break; + case _distributed_message_coop_skip_vote: + network_coop_handle_skip_vote(machine_index, entries); + break; + case _distributed_message_coop_device_groups: + network_coop_handle_device_groups(entries, header.count); + break; + case _distributed_message_coop_object_names: + network_coop_handle_object_names(entries); + break; + case _distributed_message_coop_object_transforms: + network_coop_handle_object_transforms(entries, header.count); + break; + case _distributed_message_actor_damage: + network_actors_handle_damage(entries, header.count); + break; + case _distributed_message_coop_object_looks: + network_coop_handle_object_looks(entries, header.count); + break; + case _distributed_message_damage_animations: + network_objects_handle_damage_animations(entries, header.count); + break; + case _distributed_message_coop_screen_effect: + network_coop_handle_screen_effect(entries, header.count); + break; + case _distributed_message_coop_device_states: + network_coop_handle_device_states(entries, header.count); + break; case _distributed_message_player_statistics: { /* the host's count of kills, deaths, ... */ diff --git a/port/linux/game/network_distributed.h b/port/linux/game/network_distributed.h index 90e336c6..f27bf21b 100644 --- a/port/linux/game/network_distributed.h +++ b/port/linux/game/network_distributed.h @@ -65,6 +65,47 @@ enum /* every player's ping as the host measures it, every two seconds, for the scoreboard (unreliable) */ _distributed_message_pings, + /* the host's actors' units: their control and state (network_actors.c). + A number of its own, clear of the kinds upstream adds: a build without + it drops the message as a kind it does not know, so the network version + stays upstream's and its players join as before */ + _distributed_message_actor_states = 64, + /* co-op (a campaign map, no game engine): the host's structure BSP, + twice a second, for a client to switch to; numbered as actor_states */ + _distributed_message_structure_bsp, + /* co-op: the host's cinematic, camera and screen fade, every tick + (network_coop.c) */ + _distributed_message_coop_presentation, + /* (0.2.0's co-op script sounds; now part of coop_events. The number + stays taken so a 0.2.0 build never misreads a newer message.) */ + _distributed_message_coop_sounds_retired, + /* co-op: device group state (network_coop.c) */ + _distributed_message_coop_device_groups, + /* co-op: which named objects exist on the host (network_coop.c) */ + _distributed_message_coop_object_names, + /* co-op: a client's vote to skip the cutscene, to the host (network_coop.c) */ + _distributed_message_coop_skip_vote, + /* co-op: what the host's scripts did once: sounds, titles, HUD text, + animations (network_coop.c) */ + _distributed_message_coop_events, + /* co-op: where the host's scenery and machines are, when they move + (network_coop.c) */ + _distributed_message_coop_object_transforms, + /* the damage the host's AI units are taking, which their shields' + flares are drawn by (network_actors.c); 73 in upstream's AI sync too */ + _distributed_message_actor_damage = 73, + /* co-op: how the host's objects look (their permutations, scale), when + that changes (network_coop.c) */ + _distributed_message_coop_object_looks, + /* the flinch and death animations the host picked for its units, which a + client plays rather than its own picks (network_objects.c) */ + _distributed_message_damage_animations = 75, + /* co-op: the host's cinematic screen effect (blur, filters, video), when + it changes and every two seconds (network_coop.c) */ + _distributed_message_coop_screen_effect = 76, + /* co-op: where the host's devices are (doors, elevators), when they move + (network_coop.c) */ + _distributed_message_coop_device_states = 77, /* The VR port's own messages, numbered far past the game's (240 on), so that messages upstream adds never take their numbers: a build @@ -93,6 +134,10 @@ tick, drawn as it goes; further, it is put there, drawn gliding #define HOST_BLEND_DISTANCE 0.25f #define HOST_VEHICLE_BLEND_DISTANCE 0.5f #define REMOTE_BLEND_DISTANCE 1.0f +/* world units: how far a remote unit may be from where the host says +before it is put there, and how far from the origin a unit can be */ +#define REMOTE_CORRECTION_TOLERANCE 0.05f +#define UNIT_WORLD_BOUND 32768.0f #define REMOTE_VEHICLE_BLEND_DISTANCE 2.0f enum @@ -190,6 +235,10 @@ never 0, which any object at the index matches) */ boolean distributed_real_valid(real value); boolean distributed_point_valid(real_point3d const *point, real bound); boolean distributed_object_index_valid(long object_index); +/* whether a tag index from the host really is a tag of that group */ +boolean distributed_tag_of_group(long tag_index, unsigned long group_tag); +/* the graph's animation, if a graph tag and animation index from the host are valid */ +struct animation *distributed_graph_animation(long animation_graph_index, short animation_index); /* ... an orientation's two axes (unpacked): TRUE when they are one long and about square, then made exactly so */ boolean distributed_axes_make_valid(real_vector3d *forward, real_vector3d *up); @@ -200,12 +249,70 @@ void distributed_unit_vector_unpack(struct distributed_vector const *vector, rea #define DISTRIBUTED_UNIT_SCALE 32767.0f #define DISTRIBUTED_VELOCITY_SCALE 1024.0f #define DISTRIBUTED_ANGULAR_VELOCITY_SCALE 4096.0f +/* an angle as a 16-bit fraction of a turn, and back (yaw from 0 to 2 pi, +pitch from -pi to pi) */ +short distributed_angle_pack(real angle); +real distributed_angle_unpack(short value, boolean signed_angle); +/* shields and health in 16 bits */ +word distributed_vitality_pack(real value); +real distributed_vitality_unpack(word value); + +/* ---------- prototypes/NETWORK_ACTORS.C */ + +void network_actors_new_game(void); +/* (the host, after each tick) the units its actors drove this tick, to +each client */ +void network_actors_host_tick(void); +/* (a client) the host's word on its actors' units */ +void network_actors_handle_states(void const *entries, short count); +word network_actors_entry_size(void); +/* (a client, in its tick where the host runs its actors) each actor's unit +given the control the host last sent for it */ +void network_actors_drive(void); + +/* ---------- prototypes/NETWORK_COOP.C */ + +void network_coop_new_game(void); +/* after each tick, on the host and on a client */ +void network_coop_host_tick(void); +void network_coop_client_tick(void); +/* the message handlers, and their entry sizes */ +word network_coop_presentation_entry_size(void); +void network_coop_handle_presentation(void const *entries, long host_time); +word network_coop_event_entry_size(void); +void network_coop_handle_events(void const *entries, short count); +word network_coop_device_group_entry_size(void); +void network_coop_handle_device_groups(void const *entries, short count); +word network_coop_object_names_entry_size(void); +void network_coop_handle_object_names(void const *entries); +word network_coop_object_transform_entry_size(void); +word network_coop_object_look_entry_size(void); +void network_coop_handle_object_looks(void const *entries, short count); +word network_coop_screen_effect_entry_size(void); +word network_coop_device_state_entry_size(void); +void network_coop_handle_device_states(void const *entries, short count); +void network_coop_handle_screen_effect(void const *entries, short count); +word network_actors_damage_entry_size(void); +void network_actors_handle_damage(void const *entries, short count); +void network_coop_handle_object_transforms(void const *entries, short count); +word network_coop_skip_vote_entry_size(void); +void network_coop_handle_skip_vote(long machine_index, void const *entries); +/* game_sound.c: a sound the host's scripts played */ +enum +{ + _coop_sound_impulse, + _coop_sound_looping_start, + _coop_sound_looping_stop, +}; +void network_coop_note_sound(short kind, long definition_index, long object_index, real scale); /* ---------- prototypes/NETWORK_OBJECTS.C */ void network_objects_new_game(void); /* after each tick */ void network_objects_host_tick(void); +word network_objects_damage_animation_entry_size(void); +void network_objects_handle_damage_animations(void const *entries, short count); void network_objects_client_tick(void); /* (the host) a client has loaded the game and asks for the host's objects: again, having failed to make one of them */ @@ -232,10 +339,16 @@ boolean network_objects_reconcile(long object_index, real_point3d const *positio real blend_distance); /* a unit in the vehicle's seat as the host has it (NONE: in none) */ void network_objects_set_seat(long unit_index, long vehicle_index, short seat_index); +/* (the host) the ticks between sends to a machine of a unit at position, by +how near that machine's players are (network_actors.c) */ +short network_objects_send_period(long machine_index, real_point3d const *position); /* ---------- prototypes/NETWORK_DAMAGE.C */ void network_damage_new_game(void); +/* the host: a grenade a player's unit threw (units.c), whose damage that +player's machine reports */ +void network_damage_note_grenade(long unit_index, short grenade_type); void network_damage_host_tick(void); void network_damage_client_tick(void); void network_damage_handle_events(void const *entries, short count); diff --git a/port/linux/game/network_objects.c b/port/linux/game/network_objects.c index e592f6af..3f0f6a62 100644 --- a/port/linux/game/network_objects.c +++ b/port/linux/game/network_objects.c @@ -50,6 +50,7 @@ same datum index (identifier and all), so that any message can name one: #include "objects/objects.h" #include "objects/damage.h" #include "objects/object_definitions.h" +#include "models/model_animation_definitions.h" #include "models/model_definitions.h" #include "units/units.h" #include "units/biped_definitions.h" @@ -59,6 +60,8 @@ same datum index (identifier and all), so that any message can name one: #include "items/weapons.h" #include "items/weapon_definitions.h" #include "items/equipment_definitions.h" +#include "cutscene/cinematics.h" +#include "network_coop.h" #include "network_distributed.h" #include @@ -329,11 +332,14 @@ static struct } objects_host_inventories[MAXIMUM_TRACKED_OBJECTS]; /* ... each client machine, found once a tick: where its players' living units are, those units, and the vehicles they drive (with the player's -absolute index) */ +absolute index); and where every player's living unit is, for a machine +with none (dead or joining, it watches one of them) */ static struct { short count; long indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES]; + short player_unit_count; + real_point3d player_unit_origins[MAXIMUM_TRACKED_PLAYERS]; struct { short unit_count; @@ -637,6 +643,32 @@ static void distributed_vector_clamp( } } +/* Whether the object is a vehicle no player drives in co-op (a Pelican on +its flight path): nothing on a client steers it, so its copy is kept at rest +there (no physics of its own) and moved only to where the host has it. In +multiplayer an empty vehicle (one bailed out of, or tumbling) keeps its own +physics between updates. */ +static boolean distributed_vehicle_unsteered( + long object_index) +{ + struct unit_datum *vehicle; + + if (!network_coop_active() || object_get(object_index)->object.type != _object_type_vehicle) + return FALSE; + vehicle = unit_get(object_index); + return vehicle->unit.driver_object_index == NONE || + unit_get(vehicle->unit.driver_object_index)->unit.player_index == NONE; +} + +/* Whether a client puts its copy exactly where the host has it, rather than +closing on it: an unsteered vehicle, and anything in a co-op cutscene +(where it keeps up with the host's camera). */ +static boolean distributed_object_follows_host( + long object_index) +{ + return (network_coop_active() && cinematic_in_progress()) || distributed_vehicle_unsteered(object_index); +} + /* (the transform checked) */ static void distributed_object_move( long object_index, @@ -658,7 +690,10 @@ static void distributed_object_move( object->object.translational_velocity = *velocity; if (angular_velocity) object->object.angular_velocity = *angular_velocity; - render_interpolation_correct_object(object_index, &offset); + if (!distributed_object_follows_host(object_index)) + render_interpolation_correct_object(object_index, &offset); + if (distributed_vehicle_unsteered(object_index)) + SET_FLAG(object->object.flags, _object_at_rest_bit, TRUE); } void network_objects_correct( @@ -703,15 +738,33 @@ boolean network_objects_reconcile( distributed_object_move(object_index, position, &valid_forward, &valid_up, velocity, angular_velocity); return TRUE; } - /* (half of the way: the tick's snapshots draw it moving, no jump) */ - blended.x = object->object.position.x + dx * 0.5f; - blended.y = object->object.position.y + dy * 0.5f; - blended.z = object->object.position.z + dz * 0.5f; - object_set_position(object_index, &blended, &valid_forward, &valid_up); + /* Move halfway, and draw the move as a glide over the next few frames, + as larger corrections are. Drawn as one step, a unit corrected every tick + or two (a distant enemy) visibly hitched each time. */ + if (distributed_object_follows_host(object_index)) + blended = *position; + else + { + blended.x = object->object.position.x + dx * 0.5f; + blended.y = object->object.position.y + dy * 0.5f; + blended.z = object->object.position.z + dz * 0.5f; + } + { + real_vector3d offset; + + offset.i = object->object.position.x - blended.x; + offset.j = object->object.position.y - blended.y; + offset.k = object->object.position.z - blended.z; + object_set_position(object_index, &blended, &valid_forward, &valid_up); + if (!distributed_object_follows_host(object_index)) + render_interpolation_correct_object(object_index, &offset); + } if (velocity) object->object.translational_velocity = *velocity; if (angular_velocity) object->object.angular_velocity = *angular_velocity; + if (distributed_vehicle_unsteered(object_index)) + SET_FLAG(object->object.flags, _object_at_rest_bit, TRUE); return FALSE; } @@ -766,6 +819,175 @@ static long distributed_driven_vehicle( object_get(unit_index)->object.parent_object_index : NONE; } +/* ---------- damage animations + +A unit's flinches and deaths are animations picked by where the damage came +from and at random among their permutations, so a client replaying the +host's damage could pick others. The host sends its picks (each in three +ticks' messages, in case one is lost). A client plays the host's pick when +it gets there first, or switches to it if its own has only just begun +(unit_port_correct_damage_animation). */ + +enum +{ + DAMAGE_ANIMATION_SENDS = 3, + MAXIMUM_QUEUED_DAMAGE_ANIMATIONS = 32, + /* how long a client keeps the host's pick for a hit it hasn't replayed yet */ + DAMAGE_ANIMATION_KEPT_TICKS = 2 * TICKS_PER_SECOND, +}; + +/* type: units.h's _unit_damage_animation_soft_ping and the rest; number: +the pick's, so a client tells a pick sent again from a new one */ +struct distributed_damage_animation +{ + long unit_index; + short animation_index; + byte type; + byte number; +}; + +struct distributed_damage_animations_message +{ + struct distributed_message_header header; + struct distributed_damage_animation animations[MAXIMUM_QUEUED_DAMAGE_ANIMATIONS]; +}; + +/* host: the picks still to be sent */ +static struct +{ + struct distributed_damage_animation animation; + short sends; +} host_damage_animations[MAXIMUM_QUEUED_DAMAGE_ANIMATIONS]; +static short host_damage_animation_count; +static byte host_damage_animation_number; + +/* client: the host's latest pick of each type for each unit, by absolute +index: its number, and when it came (NONE once played) */ +static struct +{ + long unit_index; + short animation_index[NUMBER_OF_UNIT_DAMAGE_ANIMATIONS]; + byte number[NUMBER_OF_UNIT_DAMAGE_ANIMATIONS]; + long time[NUMBER_OF_UNIT_DAMAGE_ANIMATIONS]; +} client_damage_animations[MAXIMUM_TRACKED_OBJECTS]; + +/* whether the animation is one of the unit's graph */ +static boolean distributed_unit_has_animation( + long unit_index, + short animation_index) +{ + long graph_index = unit_definition_get(unit_get(unit_index)->definition_index)->object.animation_graph.index; + + return graph_index != NONE && animation_index >= 0 && + animation_index < animation_graph_definition_get(graph_index)->animations.count; +} + +/* units.c, as a unit flinches or dies: the host notes the animation it +picked; a client takes the host's pick when it has one */ +short network_objects_damage_animation( + long unit_index, + short type, + short animation_index) +{ + short connection = game_connection(); + long absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(unit_index); + + if (type < 0 || type >= NUMBER_OF_UNIT_DAMAGE_ANIMATIONS) + return animation_index; + if (connection == _game_connection_network_server && animation_index != NONE && + host_damage_animation_count < MAXIMUM_QUEUED_DAMAGE_ANIMATIONS) + { + struct distributed_damage_animation *animation = + &host_damage_animations[host_damage_animation_count].animation; + + animation->unit_index = unit_index; + animation->animation_index = animation_index; + animation->type = (byte)type; + animation->number = ++host_damage_animation_number; + host_damage_animations[host_damage_animation_count++].sends = DAMAGE_ANIMATION_SENDS; + } + else if (connection == _game_connection_network_client && absolute_index >= 0 && + absolute_index < MAXIMUM_TRACKED_OBJECTS && client_damage_animations[absolute_index].unit_index == unit_index) + { + long *time = &client_damage_animations[absolute_index].time[type]; + + if (*time != NONE && game_time_get() - *time < DAMAGE_ANIMATION_KEPT_TICKS) + { + *time = NONE; + return client_damage_animations[absolute_index].animation_index[type]; + } + } + return animation_index; +} + +/* host, each tick: the picks still to be sent, to every client */ +static void distributed_host_send_damage_animations( + void) +{ + struct distributed_damage_animations_message message; + short count = 0; + short index; + + for (index = 0; index < host_damage_animation_count; index++) + { + message.animations[count++] = host_damage_animations[index].animation; + if (--host_damage_animations[index].sends <= 0) + host_damage_animations[index--] = host_damage_animations[--host_damage_animation_count]; + } + if (count > 0) + { + distributed_send(&message, _distributed_message_damage_animations, count, + (word)(sizeof(message.header) + count * sizeof(message.animations[0])), _distributed_to_clients); + } +} + +word network_objects_damage_animation_entry_size( + void) +{ + return sizeof(struct distributed_damage_animation); +} + +void network_objects_handle_damage_animations( + void const *entries, + short count) +{ + struct distributed_damage_animation const *animations = entries; + short index; + + for (index = 0; index < count; index++) + { + struct distributed_damage_animation const *animation = &animations[index]; + long absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(animation->unit_index); + short type; + + if (!distributed_object_index_valid(animation->unit_index) || absolute_index >= MAXIMUM_TRACKED_OBJECTS || + animation->type >= NUMBER_OF_UNIT_DAMAGE_ANIMATIONS || !network_objects_client_has(animation->unit_index) || + !object_try_and_get_and_verify_type(animation->unit_index, _object_mask_unit) || + !distributed_unit_has_animation(animation->unit_index, animation->animation_index)) + { + continue; + } + if (client_damage_animations[absolute_index].unit_index != animation->unit_index) + { + csmemset(&client_damage_animations[absolute_index], 0, sizeof(client_damage_animations[absolute_index])); + client_damage_animations[absolute_index].unit_index = animation->unit_index; + for (type = 0; type < NUMBER_OF_UNIT_DAMAGE_ANIMATIONS; type++) + client_damage_animations[absolute_index].time[type] = NONE; + } + /* (a pick sent again) */ + else if (client_damage_animations[absolute_index].number[animation->type] == animation->number) + { + continue; + } + client_damage_animations[absolute_index].number[animation->type] = animation->number; + client_damage_animations[absolute_index].animation_index[animation->type] = animation->animation_index; + /* (replayed here already: switched to the host's if it has only just begun) */ + client_damage_animations[absolute_index].time[animation->type] = + unit_port_correct_damage_animation(animation->unit_index, animation->type, animation->animation_index) ? + NONE : game_time_get(); + } +} + /* ---------- the host */ static void distributed_change_from_object( @@ -877,6 +1099,30 @@ static void distributed_host_update_objects( } } +/* The order a newly loaded client receives the host's objects in, most +needed first: players' units (it may spectate them), the vehicles players +and AI ride, other units, then everything else. */ +enum +{ + _send_rank_player_unit, + _send_rank_vehicle, + _send_rank_unit, + _send_rank_other, + NUMBER_OF_SEND_RANKS +}; + +static short distributed_object_send_rank( + long object_index) +{ + struct object_datum *object = object_get(object_index); + + if (object->object.type == _object_type_vehicle) + return _send_rank_vehicle; + if (object_try_and_get_and_verify_type(object_index, _object_mask_unit)) + return unit_get(object_index)->unit.player_index != NONE ? _send_rank_player_unit : _send_rank_unit; + return _send_rank_other; +} + /* a client has loaded the game: every object the host has, to it alone, and word that that is all of them; asked again, only the word (they are on their way ahead of it), unless it failed to make one since it last asked @@ -889,6 +1135,7 @@ void network_objects_client_asked( short count = 0; short limit = MIN(MAXIMUM_ENTRIES_PER_MESSAGE, RELIABLE_ENTRIES(struct distributed_object_change)); long absolute_index; + short rank; long *player_list; if (machine_index < 0 || machine_index >= HALO_PORT_MAXIMUM_NETWORK_MACHINES) @@ -916,14 +1163,22 @@ void network_objects_client_asked( /* (what every unit carries, with the next of them) */ objects_host_inventories[absolute_index].checksum = 0; objects_host_inventories[absolute_index].weapons_checksum = 0; - if (objects_host_told[absolute_index] == NONE) - continue; - distributed_change_from_object(objects_host_told[absolute_index], &message.changes[count]); - if (++count == limit) + } + for (rank = 0; rank < NUMBER_OF_SEND_RANKS; rank++) + { + for (absolute_index = 0; absolute_index < objects_host_told_count; absolute_index++) { - distributed_send_to_machine_reliably(machine_index, &message, _distributed_message_object_changes, count, - (word)(sizeof(message.header) + count * sizeof(struct distributed_object_change))); - count = 0; + long object_index = objects_host_told[absolute_index]; + + if (object_index == NONE || distributed_object_send_rank(object_index) != rank) + continue; + distributed_change_from_object(object_index, &message.changes[count]); + if (++count == limit) + { + distributed_send_to_machine_reliably(machine_index, &message, _distributed_message_object_changes, count, + (word)(sizeof(message.header) + count * sizeof(struct distributed_object_change))); + count = 0; + } } } if (count) @@ -968,8 +1223,24 @@ static void distributed_host_find_viewers( { short machine_number; + struct data_iterator iterator; + struct player_datum *player; + objects_host_viewers.count = distributed_client_machines(objects_host_viewers.indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES); + objects_host_viewers.player_unit_count = 0; + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL && + objects_host_viewers.player_unit_count < MAXIMUM_TRACKED_PLAYERS) + { + long unit_index = distributed_living_unit(player); + + if (unit_index != NONE) + { + object_get_origin(unit_index, + &objects_host_viewers.player_unit_origins[objects_host_viewers.player_unit_count++]); + } + } for (machine_number = 0; machine_number < objects_host_viewers.count; machine_number++) { long *player_list = machine_get_player_list(objects_host_viewers.indices[machine_number]); @@ -1001,19 +1272,32 @@ static void distributed_host_find_viewers( } } -/* how often (ticks) the host sends that client machine an object where it -is, moving: by its nearest player; every tick to one with none in the -world (dead, it watches anyone) */ +/* How often, in ticks, the host sends a moving object's position to a +client machine, based on its distance from that machine's nearest player. +A machine with no player in the world (dead or joining) spectates someone, +so the distance is measured from every player instead. Sending such a +machine every moving object every tick flooded it on campaign levels and +threw everything it saw out of sync. */ static short distributed_host_object_period( short machine_number, real_point3d const *position) { + short count = objects_host_viewers.machines[machine_number].unit_count; + real_point3d const *origins = objects_host_viewers.machines[machine_number].origins; real nearest = -1.0f; short index; - for (index = 0; index < objects_host_viewers.machines[machine_number].unit_count; index++) + if (!count) { - real_point3d const *origin = &objects_host_viewers.machines[machine_number].origins[index]; + count = objects_host_viewers.player_unit_count; + origins = objects_host_viewers.player_unit_origins; + } + /* nobody is alive anywhere, so nothing needs to be sent often */ + if (!count) + return MAXIMUM_OBJECT_PERIOD_TICKS; + for (index = 0; index < count; index++) + { + real_point3d const *origin = &origins[index]; real dx = position->x - origin->x; real dy = position->y - origin->y; real dz = position->z - origin->z; @@ -1027,6 +1311,21 @@ static short distributed_host_object_period( nearest < FAR_OBJECT_DISTANCE * FAR_OBJECT_DISTANCE ? 3 : MAXIMUM_OBJECT_PERIOD_TICKS; } +/* ... for other units sent by a machine's index (network_actors.c) */ +short network_objects_send_period( + long machine_index, + real_point3d const *position) +{ + short machine_number; + + for (machine_number = 0; machine_number < objects_host_viewers.count; machine_number++) + { + if (objects_host_viewers.indices[machine_number] == machine_index) + return distributed_host_object_period(machine_number, position); + } + return 1; +} + /* the kinds of states sent this tick */ enum { @@ -1496,6 +1795,10 @@ void network_objects_apply_vehicle_predictions( { continue; } + /* (not from a co-op client still loading the host's BSP: the + vehicle falls there with no ground under it) */ + if (!network_coop_player_has_structure_bsp(driver->unit.player_index)) + continue; dx = state->position.x - vehicle->object.position.x; dy = state->position.y - vehicle->object.position.y; dz = state->position.z - vehicle->object.position.z; @@ -1644,6 +1947,7 @@ void network_objects_host_tick( distributed_host_send_states(); if (game_time_get() % INVENTORY_INTERVAL_TICKS == 0) distributed_host_send_inventories(); + distributed_host_send_damage_animations(); } /* ---------- a client */ @@ -1838,6 +2142,19 @@ static void distributed_client_apply_change( struct object_datum *object = object_get(object_index); csmemcpy(object->object.base_change_colors, change->change_colors, sizeof(object->object.base_change_colors)); + /* (and the colors drawn, which object_new chose from the tag: an AI unit's + are its variant's, set after it was made, actors.c) */ + { + short color_index; + + for (color_index = 0; color_index < NUMBER_OF_OBJECT_CHANGE_COLORS; color_index++) + { + object->object.outgoing_change_colors[color_index].red = PIN(change->change_colors[color_index].red, 0.0f, 1.0f); + object->object.outgoing_change_colors[color_index].green = + PIN(change->change_colors[color_index].green, 0.0f, 1.0f); + object->object.outgoing_change_colors[color_index].blue = PIN(change->change_colors[color_index].blue, 0.0f, 1.0f); + } + } /* (a permutation the model has, or none: the model's renderer takes it as it is) */ { @@ -2092,6 +2409,20 @@ static void distributed_client_dead_biped( } } +/* client: a vehicle the host says is dead is destroyed here too, with its +explosion. No killing blow comes for vehicles, only for bipeds. */ +static void distributed_client_dead_vehicle( + long object_index) +{ + struct unit_datum *vehicle = (struct unit_datum *)object_try_and_get_and_verify_type(object_index, + _object_mask_vehicle); + + if (!vehicle || TEST_FLAG(vehicle->object.damage_flags, _object_dead_bit)) + return; + unit_kill_no_statistics(object_index); + object_damage_update(object_index); +} + void network_objects_handle_states( void const *entries, short count) @@ -2115,6 +2446,7 @@ void network_objects_handle_states( if (TEST_FLAG(state->flags, _distributed_object_dead_bit)) { distributed_client_dead_biped(state->object_index); + distributed_client_dead_vehicle(state->object_index); /* (a body its tag destroys at once is gone) */ if (!object_try_and_get_and_verify_type(state->object_index, _object_mask_all)) continue; @@ -2147,6 +2479,12 @@ void network_objects_handle_states( if (blend_distance > 0.0f) blend_distance = REMOTE_VEHICLE_BLEND_DISTANCE; + /* (one no player drives follows every move the host sends, however small) */ + if (distributed_vehicle_unsteered(state->object_index)) + { + tolerance = 0.0f; + angle_tolerance = 2.0f; + } if (vehicle->unit.driver_object_index != NONE && distributed_player_is_local(unit_get(vehicle->unit.driver_object_index)->unit.player_index)) { @@ -2177,7 +2515,8 @@ void network_objects_handle_states( { distributed_count_correction(); } - SET_FLAG(object->object.flags, _object_at_rest_bit, TEST_FLAG(state->flags, _distributed_object_at_rest_bit)); + SET_FLAG(object->object.flags, _object_at_rest_bit, TEST_FLAG(state->flags, _distributed_object_at_rest_bit) || + distributed_vehicle_unsteered(state->object_index)); } } @@ -2598,6 +2937,51 @@ static void distributed_client_remove_own_objects( objects_client_new_object_count = 0; } +/* Client: keeps each AI-driven vehicle (such as a Pelican on its flight +path) moving at the velocity the host last sent. These vehicles are held at +rest here, and a distant one is only updated every few ticks, so otherwise +it would stop between updates and jump on each one. */ +static void distributed_client_carry_unsteered_vehicles( + void) +{ + struct object_iterator iterator; + struct object_datum *object; + + object_iterator_new(&iterator, _object_mask_vehicle, 0); + while ((object = object_iterator_next(&iterator)) != NULL) + { + real_vector3d const *velocity = &object->object.translational_velocity; + real_vector3d axis = object->object.angular_velocity; + real_vector3d forward = object->object.forward; + real_vector3d up = object->object.up; + real_point3d position; + real angle; + + if (object->object.parent_object_index != NONE || unit_get(iterator.index)->unit.driver_object_index == NONE || + !distributed_vehicle_unsteered(iterator.index)) + { + continue; + } + angle = normalize3d(&axis); + if (velocity->i == 0.0f && velocity->j == 0.0f && velocity->k == 0.0f && angle == 0.0f) + continue; + position.x = object->object.position.x + velocity->i; + position.y = object->object.position.y + velocity->j; + position.z = object->object.position.z + velocity->k; + /* rotate the same way physics.c applies angular velocity */ + if (angle != 0.0f) + { + real_matrix4x3 rotation; + + matrix4x3_rotation_from_axis_and_angle(&rotation, &axis, (real)sin(angle), (real)cos(angle)); + matrix4x3_transform_vector(&rotation, &object->object.forward, &forward); + matrix4x3_transform_vector(&rotation, &object->object.up, &up); + } + if (distributed_transform_valid(&position, &forward, &up, NULL, NULL, &forward, &up)) + object_set_position(iterator.index, &position, &forward, &up); + } +} + void network_objects_client_tick( void) { @@ -2625,6 +3009,7 @@ void network_objects_client_tick( distributed_client_ready_picked_up_weapons(); distributed_client_note_own_inventories(); distributed_client_send_vehicles(); + distributed_client_carry_unsteered_vehicles(); /* (who it is, as its Discord told it: once its ready went, which makes it a machine the host takes messages of) */ if (objects_client_ready_time != NONE) @@ -2642,6 +3027,8 @@ void network_objects_new_game( short index; csmemset(objects_host_inventories, 0, sizeof(objects_host_inventories)); + host_damage_animation_count = 0; + csmemset(client_damage_animations, 0, sizeof(client_damage_animations)); for (absolute_index = 0; absolute_index < MAXIMUM_TRACKED_OBJECTS; absolute_index++) { objects_host_told[absolute_index] = NONE; diff --git a/port/linux/include/StdDef.h b/port/linux/include/StdDef.h index c1bd9c8d..be8aa2b4 100644 --- a/port/linux/include/StdDef.h +++ b/port/linux/include/StdDef.h @@ -1,4 +1,4 @@ -/* the game includes ; Linux file names are case sensitive. - #include_next, since on a case-insensitive file system (macOS) this file - is also what finds */ +/* the game includes ; Linux file names are case sensitive. On a +case-insensitive file system (macOS) a plain would find this file +again; include_next goes to the real one, as the other shims do. */ #include_next diff --git a/port/linux/include/halo_port_capacity.h b/port/linux/include/halo_port_capacity.h index 281793d5..91d11a25 100644 --- a/port/linux/include/halo_port_capacity.h +++ b/port/linux/include/halo_port_capacity.h @@ -24,15 +24,27 @@ state above the tag cache (which ends at 0x819A6000), inside the Xbox memory window (0x80000000-0x88000000, port/linux/src/platform.h) and below everything the window hands out top-down (texture and sound caches, Direct3D resources). -The CPU part holds about 13.6 MB of pools at the sizes below (the Xbox pools +The CPU part holds about 17.2 MB of pools at the sizes below (the Xbox pools fill 3,165,260 of its 0x305000 bytes); the GPU part holds only the decal -vertices, as on the Xbox. */ +vertices, as on the Xbox. A change to a pool's size changes the game state's +layout: saved games of builds before it no longer load. */ #define HALO_PORT_GAME_STATE_BASE_ADDRESS 0x81A00000 /* (0x80061000) */ -#define HALO_PORT_GAME_STATE_CPU_SIZE 0xFC0000 /* (0x305000) */ +#define HALO_PORT_GAME_STATE_CPU_SIZE 0x13C0000 /* (0x305000) */ #define HALO_PORT_GAME_STATE_GPU_SIZE 0x40000 /* (0x40000) */ #define HALO_PORT_GAME_STATE_SIZE (HALO_PORT_GAME_STATE_CPU_SIZE+HALO_PORT_GAME_STATE_GPU_SIZE) +/* ---------- AI + +Network co-op adds enemies for its players (port/linux/game/coop_enemies.c): +the actors, and their knowledge of the units about them (props: with many +players, more each), have room for several times a level's own. */ + +#define HALO_PORT_MAXIMUM_ACTORS 1024 /* (256) */ +#define HALO_PORT_MAXIMUM_PROPS 8192 /* (768) */ +#define HALO_PORT_MAXIMUM_SWARMS 128 /* (32) */ +#define HALO_PORT_MAXIMUM_SWARM_COMPONENTS 1024 /* (256) */ + /* ---------- objects */ #define HALO_PORT_MAXIMUM_OBJECTS_PER_MAP 8192 /* (2048) */ diff --git a/port/linux/include/halo_port_limits.h b/port/linux/include/halo_port_limits.h index 92571c49..7107cdcb 100644 --- a/port/linux/include/halo_port_limits.h +++ b/port/linux/include/halo_port_limits.h @@ -66,7 +66,7 @@ is newer (network_client_manager.c). A host advertises it, with its netcode, in its game's advertisement's reserved bytes (network_server_message_handler.c), which hosts built before there was a version send as zeros: version 0. Raise it with any change to what the machines send each other. */ -#define HALO_PORT_NETWORK_VERSION 11 +#define HALO_PORT_NETWORK_VERSION 16 /* ... the advertisement's reserved bytes: the version (a little-endian word), then flags */ #define HALO_PORT_ADVERTISED_VERSION_OFFSET 0 diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 28d569b1..f4bb65f3 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -61,17 +61,19 @@ struct xgpu_capabilities xgpu_capabilities; The Xbox screen is 640x480. The native ports can draw a wider one: 480 lines, and as many columns as the display's shape gives. On Android that is display.screen_width (port_config.c; 640 keeps 4:3); on the desktop, the -display's shape while the game is fullscreen, and 640 in a window. The -game's camera derives its horizontal field of view from the viewport, so the -3D view simply widens. The menus and full-screen overlays are laid out for -640 columns; while they draw (halo_screen_ui_offset), everything shifts right -to center them. +shape of the window (of the display while the game is fullscreen, or of +display.resolution), and 640 where display.resolution_scaling is "original". +The game's camera derives its horizontal field of view from the viewport, so +the 3D view simply widens. The menus and full-screen overlays are laid out +for 640 columns; while they draw (halo_screen_ui_offset), everything shifts +right to center them. -Fullscreen on the desktop also draws at the display's resolution: render +The desktop also draws at that resolution (platform_screen_mode): render targets the size of the screen get that many pixels (screen_scale), and viewports, clears and visibility counts are scaled to match, so the game -still works in its 480 lines. The width and the scale change only between -frames, after one is presented (halo_screen_commit). */ +still works in its 480 lines; "original" draws 640x480, scaled up at +presentation. The width and the scale change only between frames, after one +is presented (halo_screen_commit). */ #define SCREEN_HEIGHT 480 #define SCREEN_MAXIMUM_WIDTH 1920 @@ -2463,6 +2465,10 @@ static GLuint mip_composite_get(const struct xgpu_texture_description *descripti static void bind_textures(struct nv2a_pixel_shader_key *key, float texture_scale[4][4]) { + /* Bind only after resolving every stage, since texture uploads can + overwrite the active unit's binding. */ + GLenum gl_targets[D3DTSS_MAXSTAGES]; + GLuint gl_textures[D3DTSS_MAXSTAGES]; int stage; for (stage = 0; stage < D3DTSS_MAXSTAGES; stage++) @@ -2474,7 +2480,8 @@ static void bind_textures(struct nv2a_pixel_shader_key *key, float texture_scale texture_scale[stage][2] = texture_scale[stage][3] = 1.0f; if (!texture || !texture->Data || mode == 0 || mode == 0x04 || mode == 0x05 || mode == 0x11) { - state_texture(stage, GL_TEXTURE_2D, 0); + gl_targets[stage] = GL_TEXTURE_2D; + gl_textures[stage] = 0; key->sampler_type[stage] = mode == 0x11 ? _xgpu_sampler_2d : _xgpu_sampler_none; continue; } @@ -2530,7 +2537,8 @@ static void bind_textures(struct nv2a_pixel_shader_key *key, float texture_scale texture_scale[stage][1] = 1.0f / (float)description.height; } } - state_texture(stage, gl_target, gl_texture); + gl_targets[stage] = gl_target; + gl_textures[stage] = gl_texture; state_sampler(stage, device.samplers[stage]); configure_sampler(stage, description.levels > 1, description.hires); if (stage == 0) @@ -2539,6 +2547,8 @@ static void bind_textures(struct nv2a_pixel_shader_key *key, float texture_scale gl_target == GL_TEXTURE_3D ? _xgpu_sampler_3d : _xgpu_sampler_2d; } } + for (stage = 0; stage < D3DTSS_MAXSTAGES; stage++) + state_texture(stage, gl_targets[stage], gl_textures[stage]); } static GLenum stencil_operation(DWORD operation) diff --git a/port/linux/src/nv2a_vsh.c b/port/linux/src/nv2a_vsh.c index bee469c9..6f06400d 100644 --- a/port/linux/src/nv2a_vsh.c +++ b/port/linux/src/nv2a_vsh.c @@ -249,9 +249,7 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru "\tvec4 oFog = vec4(1.0), oPts = vec4(point_size), oUnused = vec4(0.0);\n" "\tint a0 = 0;\n" "\tvec4 A, B, C, mac, ilu;\n"); -#ifdef HALO_ANDROID xgpu_text_append(&text, "\tvec4 clip_position = vec4(0.0);\n\tbool clip_captured = false;\n"); -#endif for (index = 0; index < instruction_count; index++) { @@ -303,7 +301,6 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru case _ilu_lit: xgpu_text_append(&text, "\tilu = nv2a_lit(C);\n"); break; default: xgpu_text_append(&text, "\tilu = vec4(0.0);\n"); break; } -#ifdef HALO_ANDROID /* the screen-space conversion takes the reciprocal of the clip-space position's w (rcc of r12.w); keep the position it converts */ if (ilu == _ilu_rcc && field(instruction, 3, 28, 2) == _mux_temporary && @@ -311,7 +308,6 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru { xgpu_text_append(&text, "\tclip_position = oPos;\n\tclip_captured = true;\n"); } -#endif /* results are written only after both units have read their inputs */ if (mac == _mac_arl) @@ -357,21 +353,33 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru /* Direct3D 8 puts pixel centres on integer screen coordinates (the game offsets its screen-space quads by -0.5 to match), OpenGL on half-integers */ -#ifdef HALO_ANDROID /* The conversion is screen = clip * c[-38] * rcc(w) + c[-37]; undoing it by multiplying by w again is lossy near the camera plane, where rcc clamps and 1/w rounds differently on each GPU (Mali put vertices of the first-person weapon at the vanishing point). Where the clip position was kept, the same result is computed without dividing. */ + "\tvec4 position;\n" "\tif (clip_captured)\n" - "\t\tgl_Position = vec4((clip_position.xyz * c[%d].xyz + (c[%d].xyz + vec3(0.5 + screen_offset, 0.5, 0.0)\n" + "\t\tposition = vec4((clip_position.xyz * c[%d].xyz + (c[%d].xyz + vec3(0.5 + screen_offset, 0.5, 0.0)\n" "\t\t\t- viewport_offset.xyz) * clip_position.w) / scale, clip_position.w);\n" "\telse\n" - "\t\tgl_Position = vec4((vec3(oPos.xy + vec2(0.5 + screen_offset, 0.5), oPos.z) - viewport_offset.xyz) / scale * oPos.w, oPos.w);\n" -#else - "\tvec3 ndc = (vec3(oPos.xy + vec2(0.5 + screen_offset, 0.5), oPos.z) - viewport_offset.xyz) / scale;\n" - "\tgl_Position = vec4(ndc * oPos.w, oPos.w);\n" -#endif + "\t{\n" + "\t\tvec3 ndc = (vec3(oPos.xy + vec2(0.5 + screen_offset, 0.5), oPos.z) - viewport_offset.xyz) / scale;\n" + "\t\tposition = vec4(ndc * oPos.w, oPos.w);\n" + "\t}\n" + /* A position whose w is zero, or is not a number, is the clip-space + origin: the screen conversion's reciprocal is clamped rather than + infinite, so a large position times a w of zero is exactly zero, and + the origin is inside the frustum. Nothing then clips the triangle + away and OpenGL divides zero by zero there: the vertex lands on the + middle of the screen and the triangle is drawn out to it from the + first-person weapon, whose pose follows the camera and so reaches the + camera plane. The divide on the Xbox sends such a vertex to infinity + and the clipper takes the triangle; put it behind the camera instead, + which the clipper also takes. */ + "\tif (!(abs(position.w) > 0.0))\n" + "\t\tposition = vec4(0.0, 0.0, 0.0, -1.0);\n" + "\tgl_Position = position;\n" #ifdef HALO_ANDROID /* what glClipControl(GL_UPPER_LEFT, GL_ZERO_TO_ONE) does on desktop GL: rows from the top, depth 0..1 */ @@ -389,16 +397,16 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru "\txT2 = oT2;\n" "\txT3 = oT3;\n" "\txFog = oFog.x;\n" - "}\n" + "}\n", + XGPU_VERTEX_CONSTANT_BIAS - 38, XGPU_VERTEX_CONSTANT_BIAS - 37 #ifdef HALO_ANDROID - , XGPU_VERTEX_CONSTANT_BIAS - 38, XGPU_VERTEX_CONSTANT_BIAS - 37, /* (what is drawn in screen space, which kept no clip position, is taken to each view as though infinitely far: a lens flare's sun lands where the sky's does) */ /* (what the game projects itself, a sprite or a lens flare's test, comes as a screen position with w 1 and its depth in z: its eye's offset is by that depth, where it is depth tested) */ - multiview ? + , multiview ? "\t{\n" "\t\tint row = int(gl_ViewID_OVR) * 2;\n" "\t\tfloat near = 0.0;\n" diff --git a/port/linux/src/p2p_signal.c b/port/linux/src/p2p_signal.c index 20342151..3c8a872d 100644 --- a/port/linux/src/p2p_signal.c +++ b/port/linux/src/p2p_signal.c @@ -3,9 +3,10 @@ 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 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). +(those in brokers.txt, network.brokers_file; 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 @@ -1521,8 +1522,50 @@ static void broker_readable(struct broker *broker) /* ---------- p2p.c's side */ +/* the brokers in network.brokers_file (beside config.toml, unless a full +path: port/assets/network/brokers.txt, which the builds put there), one on +each line, "#" starting a comment, into text: host:port entries separated by +commas; empty if the file cannot be read */ +static void brokers_list(char *text, size_t size) +{ + const char *name = config_string("network.brokers_file"); + char path[1024]; + char *file; + size_t file_size = 0, index, length = 0; + int comment = 0; + + text[0] = 0; + if (name[0] == '/' || name[0] == '\\' || (name[0] && name[1] == ':')) + snprintf(path, sizeof(path), "%s", name); + else + { + config_folder(path, sizeof(path)); + snprintf(path + strlen(path), sizeof(path) - strlen(path), "%s", name); + } + file = config_file_read(path, &file_size); + if (!file) + { + platform_log("Internet play: the brokers' file %s cannot be read (network.brokers_file)", path); + return; + } + for (index = 0; index < file_size && length + 1 < size; index++) + { + char character = file[index]; + + if (character == '\n' || character == '\r') + comment = 0; + else if (character == '#') + comment = 1; + if (!comment) + text[length++] = character == '\n' || character == '\r' || character == '\t' ? ',' : character; + } + text[length] = 0; + free(file); +} + void p2p_signal_start(void) { + char list[1024]; const char *text; unsigned char random[8]; char hex[17]; @@ -1539,7 +1582,8 @@ void p2p_signal_start(void) 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"); + brokers_list(list, sizeof(list)); + text = list; while (*text && signalling.broker_count < MAXIMUM_BROKERS) { const char *end = text + strcspn(text, ","); @@ -1571,7 +1615,9 @@ void p2p_signal_start(void) text = *end ? end + 1 : end; } if (!signalling.broker_count) - platform_log("Internet play: no signalling brokers (network.signalling_brokers), so invites cannot work"); + platform_log("Internet play: no signalling brokers (network.brokers_file), so invites cannot work"); + else if (text[strspn(text, ", ")]) + platform_log("Internet play: only the first %d signalling brokers are used", MAXIMUM_BROKERS); } void p2p_signal_select_sets(int *read, int *read_count, int *write, int *write_count, int maximum_count) diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 63ea7336..3b9d2d23 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -66,15 +66,28 @@ struct config_setting static const struct config_setting config_settings[] = { { "display.fullscreen", _config_boolean, "true", "HALO_FULLSCREEN", _environment_value, _platform_desktop, - "Start fullscreen, drawing at the display's resolution and shape; false\n" - "starts in a window, which draws the Xbox's 640x480. F11 switches." }, + "Where display.mode is empty: start borderless over the whole display;\n" + "false starts in a window. F11 switches." }, { "display.mode", _config_string, "\"\"", "HALO_DISPLAY_MODE", _environment_value, _platform_desktop, - "\"fullscreen\" takes the display at its desktop resolution, \"borderless\"\n" - "is a window over the whole desktop (both draw at the display's\n" - "resolution), \"windowed\" a window of the Xbox's 640x480, scaled. Empty:\n" - "display.fullscreen's (true: borderless). F11 switches to the window and back." }, + "\"fullscreen\" takes the display (at display.resolution's mode),\n" + "\"borderless\" is a window over the whole desktop, \"windowed\" a window\n" + "(display.window_size). Empty: display.fullscreen's (true: borderless).\n" + "F11 switches to the window and back." }, + { "display.resolution", _config_string, "\"native\"", "HALO_RESOLUTION", _environment_value, _platform_desktop, + "What fullscreen and borderless draw at: \"native\", the display's own, or\n" + "\"x\" (\"1920x1080\"), 640x480 or more. Fullscreen sets the\n" + "display to it; borderless draws it scaled to the display." }, + { "display.resolution_scaling", _config_string, "\"native\"", "HALO_RESOLUTION_SCALING", _environment_value, + _platform_desktop, + "\"native\" draws at the window's resolution (fullscreen, the display's or\n" + "display.resolution); \"original\" draws the Xbox's 640x480 and scales it\n" + "up." }, + { "display.window_size", _config_string, "\"\"", "HALO_WINDOW_SIZE", _environment_value, _platform_desktop, + "The window's size, \"x\" (\"1920x1080\"), 640x480 or more (it\n" + "can be resized). Empty: display.window_scale's." }, { "display.window_scale", _config_integer, "2", "HALO_WINDOW_SCALE", _environment_value, _platform_desktop, - "The window's size as a multiple of 640x480 (it can be resized)." }, + "Where display.window_size is empty: the window's size as a multiple of\n" + "640x480." }, { "display.screen_width", _config_integer, "0", "HALO_SCREEN_WIDTH", _environment_value, _platform_android, "Columns of the 480-line picture: 0 for the display's shape, 640 for the\n" "Xbox's 4:3." }, @@ -96,8 +109,8 @@ static const struct config_setting config_settings[] = "false draws the maps' own bitmaps." }, { "display.high_res_text", _config_boolean, "true", "HALO_HIGH_RES_TEXT", _environment_value, _platform_all, "Draw the menus' and HUD's text with the fonts in port/assets/fonts\n" - "(Overpass) at the display's resolution, and the menus' titles from\n" - "port/assets/titles; false draws the maps' bitmap fonts and titles." }, + "(Overpass) at the resolution the game draws at, and the menus' titles\n" + "from port/assets/titles; false draws the maps' bitmap fonts and titles." }, { "display.menus", _config_string, "\"pc\"", "HALO_MENUS", _environment_value, _platform_all, "The menus: \"pc\" for the PC version's main menu (port/assets/menus,\n" "and a menus folder here for your own), \"xbox\" for the Xbox's." }, @@ -399,11 +412,39 @@ static const struct config_setting config_settings[] = "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, - "Public MQTT brokers through which the machines of an invite find each\n" - "other (its messages are encrypted); comma-separated host:port." }, + { "network.coop_public", _config_boolean, "false", "HALO_NET_COOP_PUBLIC", _environment_value, _platform_all, + "Whether an online co-op game (Create Game > Internet, a SINGLEPLAYER\n" + "map) starts as PUBLIC or, false, PRIVATE: Server Setup's LISTING in\n" + "co-op, which writes its choice here." }, + { "network.coop_friendly_fire", _config_string, "\"on\"", "HALO_NET_COOP_FRIENDLY_FIRE", _environment_value, + _platform_all, + "Whether the players of an online co-op game hurt each other: \"off\",\n" + "\"on\", \"shields_only\" or \"explosives_only\" (Server Setup's FRIENDLY\n" + "FIRE in co-op, which writes its choice here). Their AI allies they\n" + "always can, as in the campaign." }, + { "network.coop_enemies_mode", _config_string, "\"per_player\"", "HALO_NET_COOP_ENEMIES_MODE", _environment_value, + _platform_all, + "Online co-op's extra enemies: \"none\", \"per_player\" (each squad of\n" + "enemies grows by coop_enemies for each player past the first) or\n" + "\"multiplier\" (each is coop_enemies_multiplier times as large, for any\n" + "number of players). Server Setup's EXTRA ENEMIES in co-op writes its\n" + "choice here." }, + { "network.coop_enemies", _config_integer, "50", "HALO_NET_COOP_ENEMIES", _environment_value, _platform_all, + "Online co-op's extra enemies per player, a percentage: for each player\n" + "past the first, each squad of enemies a level places gets this much of\n" + "itself more (100: as many again; 25 to 200). Server Setup's PER PLAYER\n" + "in co-op writes its choice here." }, + { "network.coop_enemies_multiplier", _config_integer, "2", "HALO_NET_COOP_ENEMIES_MULTIPLIER", _environment_value, + _platform_all, + "Online co-op's static multiplier of its enemies: each squad of enemies\n" + "a level places is this many times as large (2 to 32). Server Setup's\n" + "MULTIPLIER in co-op writes its choice here." }, + { "network.brokers_file", _config_string, "\"brokers.txt\"", + "HALO_NET_BROKERS_FILE", _environment_value, _platform_all, + "The file of the public MQTT brokers through which the machines of an\n" + "invite find each other (its messages are encrypted), beside this file\n" + "unless a full path: one host:port on each line, up to 4. Updates\n" + "replace brokers.txt: keep a list of your own under another name." }, { "network.stun_servers", _config_string, "\"stun.l.google.com:19302,stun.cloudflare.com:3478\"", "HALO_NET_STUN", _environment_value, _platform_all, "Public STUN servers that tell this machine its internet address;\n" @@ -1250,6 +1291,11 @@ int config_write_integer(const char *name, long value) /* ---------- public code */ +char *config_file_read(const char *path, size_t *size) +{ + return config_read_file(path, size); +} + unsigned long config_changes(void) { return config_change_count; diff --git a/port/linux/src/port_config.h b/port/linux/src/port_config.h index 0c12b910..e8f9a393 100644 --- a/port/linux/src/port_config.h +++ b/port/linux/src/port_config.h @@ -30,6 +30,9 @@ such setting */ int config_text(const char *name, char *text, size_t size); /* the folder config.toml is in, with its separator */ void config_folder(char *path, size_t size); +/* a whole file (a UTF-8 path), NUL terminated, or NULL; free() it (the Android +guest has no SDL_LoadFile) */ +char *config_file_read(const char *path, size_t *size); /* a setting's default, as text; 0 if there is no such setting */ int config_default(const char *name, char *text, size_t size); /* how many times config_write has changed a setting: what keeps one reads diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c index bb172da1..913ab573 100644 --- a/port/linux/src/sdl_platform.c +++ b/port/linux/src/sdl_platform.c @@ -379,38 +379,43 @@ static BOOL platform_fullscreen_setting(void) return !config_boolean("debug.hidden_window") && platform_display_mode() != _display_mode_windowed; } -/* the window's fullscreen kind (display.mode): borderless, a window over -the whole desktop (SDL's fullscreen without a mode), or fullscreen, the -display taken at its desktop resolution. Either draws at the display's -resolution (platform_screen_mode); F11 switches to the kind set. */ -static void platform_fullscreen_kind_apply(void) +/* a size as a setting has it, "x": whether it is one, and +the Xbox's 640x480 or more */ +static BOOL platform_size_parse(const char *text, long *width, long *height) { - static int applied = -1; - int exclusive = platform_display_mode() == _display_mode_fullscreen ? 1 : 0; - SDL_DisplayID display; + char *end; - if (!platform_window || exclusive == applied) - return; - applied = exclusive; - display = SDL_GetDisplayForWindow(platform_window); - SDL_SetWindowFullscreenMode(platform_window, - exclusive && display ? SDL_GetDesktopDisplayMode(display) : NULL); + *width = strtol(text, &end, 10); + *height = *end == 'x' || *end == 'X' ? strtol(end + 1, &end, 10) : 0; + return !*end && *width >= 640 && *height >= 480; } -/* whether the game is, or is to be, fullscreen, and if so the size in -pixels of the display it fills (d3d8_gl.c draws at that resolution) */ -BOOL platform_screen_mode(long *width, long *height) +/* display.resolution in pixels, or 0x0 for the display's own ("native"), +as for one the game cannot draw at */ +static void platform_resolution_setting(long *width, long *height) { - SDL_DisplayID display; - const SDL_DisplayMode *mode; + if (!platform_size_parse(config_string("display.resolution"), width, height)) + *width = *height = 0; +} - if (platform_window ? !(SDL_GetWindowFlags(platform_window) & SDL_WINDOW_FULLSCREEN) : - !platform_fullscreen_setting() || !platform_sdl_initialize()) +/* the window's size (display.window_size), else the Xbox's 640x480 times +display.window_scale, as older versions set it */ +static void platform_window_size_setting(long *width, long *height) +{ + long scale = config_integer("display.window_scale"); + + if (!platform_size_parse(config_string("display.window_size"), width, height)) { - return FALSE; + *width = 640 * (scale < 1 ? 1 : scale); + *height = 480 * (scale < 1 ? 1 : scale); } - display = platform_window ? SDL_GetDisplayForWindow(platform_window) : SDL_GetPrimaryDisplay(); - mode = display ? SDL_GetDesktopDisplayMode(display) : NULL; +} + +/* a display's own size in pixels (its desktop mode) */ +static BOOL platform_display_size(SDL_DisplayID display, long *width, long *height) +{ + const SDL_DisplayMode *mode = display ? SDL_GetDesktopDisplayMode(display) : NULL; + if (!mode) return FALSE; *width = (long)(mode->w * mode->pixel_density + 0.5f); @@ -418,23 +423,263 @@ BOOL platform_screen_mode(long *width, long *height) return TRUE; } +/* the window's fullscreen kind (display.mode): borderless, a window over +the whole desktop (SDL's fullscreen without a mode), or fullscreen, the +display taken at display.resolution's mode (the one nearest it), else at its +desktop one. F11 switches to the kind set. */ +static void platform_fullscreen_kind_apply(void) +{ + static int applied = -1; + static long applied_width, applied_height; + int exclusive = platform_display_mode() == _display_mode_fullscreen ? 1 : 0; + long width = 0, height = 0; + SDL_DisplayID display; + SDL_DisplayMode closest; + const SDL_DisplayMode *mode = NULL; + + if (!platform_window) + return; + if (exclusive) + platform_resolution_setting(&width, &height); + if (exclusive == applied && width == applied_width && height == applied_height) + return; + applied = exclusive; + applied_width = width; + applied_height = height; + display = SDL_GetDisplayForWindow(platform_window); + if (exclusive && display) + { + if (width && SDL_GetClosestFullscreenDisplayMode(display, (int)width, (int)height, 0.0f, false, &closest)) + mode = &closest; + else + mode = SDL_GetDesktopDisplayMode(display); + } + SDL_SetWindowFullscreenMode(platform_window, mode); +} + +/* whether the game last asked for the window to be fullscreen */ +static BOOL platform_fullscreen_requested = FALSE; + +static void platform_window_set_fullscreen(BOOL fullscreen) +{ + platform_fullscreen_requested = fullscreen; + SDL_SetWindowFullscreen(platform_window, fullscreen ? true : false); +} + +/* whether the window is fullscreen. SDL sets its flag when the window +manager confirms the request, and some never do: gamescope (the Steam +Deck's Game Mode) makes the window the size of the display but leaves the +flag unset, so the game drew 640x480 and gamescope stretched it. A window +that was asked to be fullscreen and covers its display counts too. */ +static BOOL platform_window_fullscreen(void) +{ + SDL_DisplayID display; + const SDL_DisplayMode *mode; + int width, height; + + if (SDL_GetWindowFlags(platform_window) & SDL_WINDOW_FULLSCREEN) + return TRUE; + if (!platform_fullscreen_requested) + return FALSE; + display = SDL_GetDisplayForWindow(platform_window); + mode = display ? SDL_GetDesktopDisplayMode(display) : NULL; + if (!mode || !SDL_GetWindowSizeInPixels(platform_window, &width, &height)) + return FALSE; + return width >= (int)(mode->w * mode->pixel_density + 0.5f) && + height >= (int)(mode->h * mode->pixel_density + 0.5f); +} + +/* the size in pixels the game draws its picture at (d3d8_gl.c): the +window's, the display's while fullscreen, or display.resolution's while +fullscreen (either kind) where the display has room for it; before the +window opens, what it will be. FALSE where display.resolution_scaling is +"original": the Xbox's 640x480, scaled to the window. */ +BOOL platform_screen_mode(long *width, long *height) +{ + /* (the size last given, for a window that has none: minimized) */ + static long last_width, last_height; + long resolution_width, resolution_height; + BOOL fullscreen; + + if (!strcmp(config_string("display.resolution_scaling"), "original")) + return FALSE; + if (platform_window) + { + int pixel_width = 0, pixel_height = 0; + + SDL_GetWindowSizeInPixels(platform_window, &pixel_width, &pixel_height); + if (pixel_width <= 0 || pixel_height <= 0) + { + *width = last_width; + *height = last_height; + return last_width > 0; + } + *width = pixel_width; + *height = pixel_height; + fullscreen = platform_window_fullscreen(); + } + else + { + if (!platform_sdl_initialize()) + return FALSE; + fullscreen = platform_fullscreen_setting(); + platform_window_size_setting(width, height); + if (fullscreen && !platform_display_size(SDL_GetPrimaryDisplay(), width, height)) + return FALSE; + } + /* (fullscreen's display is at the resolution already, where it has that + mode: platform_fullscreen_kind_apply; borderless's is scaled to) */ + platform_resolution_setting(&resolution_width, &resolution_height); + if (fullscreen && resolution_width && resolution_width <= *width && resolution_height <= *height) + { + *width = resolution_width; + *height = resolution_height; + } + last_width = *width; + last_height = *height; + return TRUE; +} + +/* the size added to the list unless it has it already; the count */ +static int platform_resolution_add(long *widths, long *heights, int count, int maximum, long width, long height) +{ + int index; + + for (index = 0; index < count; index++) + { + if (widths[index] == width && heights[index] == height) + return count; + } + if (count < maximum) + { + widths[count] = width; + heights[count] = height; + count++; + } + return count; +} + +int platform_display_resolutions(long *widths, long *heights, int maximum) +{ + SDL_DisplayID display; + SDL_DisplayMode **modes; + long display_width, display_height, width, height; + int mode_count = 0, count = 0, index; + + if (maximum < 1 || !platform_sdl_initialize()) + return 0; + display = platform_window ? SDL_GetDisplayForWindow(platform_window) : 0; + if (!display) + display = SDL_GetPrimaryDisplay(); + if (!platform_display_size(display, &display_width, &display_height)) + return 0; + modes = SDL_GetFullscreenDisplayModes(display, &mode_count); + for (index = 0; modes && index < mode_count; index++) + { + width = (long)(modes[index]->w * modes[index]->pixel_density + 0.5f); + height = (long)(modes[index]->h * modes[index]->pixel_density + 0.5f); + if (width >= 640 && height >= 480 && width <= display_width && height <= display_height && + (width != display_width || height != display_height)) + { + count = platform_resolution_add(widths, heights, count, maximum, width, height); + } + } + SDL_free(modes); + /* (the one set, though this display has no such mode, so that Video + Setup shows it) */ + platform_resolution_setting(&width, &height); + if (width && (width != display_width || height != display_height)) + count = platform_resolution_add(widths, heights, count, maximum, width, height); + /* largest first */ + for (index = 1; index < count; index++) + { + int place; + + width = widths[index]; + height = heights[index]; + for (place = index; place > 0 && (widths[place - 1] < width || + (widths[place - 1] == width && heights[place - 1] < height)); place--) + { + widths[place] = widths[place - 1]; + heights[place] = heights[place - 1]; + } + widths[place] = width; + heights[place] = height; + } + return count; +} + +/* Video Setup's window sizes, by shape (4:3, 16:10, 16:9, 21:9), each from +the smallest */ +static const short platform_window_sizes_offered[][2] = +{ + { 640, 480 }, { 800, 600 }, { 1024, 768 }, { 1280, 960 }, { 1600, 1200 }, { 1920, 1440 }, { 2560, 1920 }, + { 1280, 800 }, { 1440, 900 }, { 1680, 1050 }, { 1920, 1200 }, { 2560, 1600 }, + { 1280, 720 }, { 1600, 900 }, { 1920, 1080 }, { 2560, 1440 }, { 3840, 2160 }, + { 2560, 1080 }, { 3440, 1440 }, { 3840, 1600 }, { 5120, 2160 }, +}; + +int platform_window_sizes(long *widths, long *heights, int maximum) +{ + SDL_DisplayID display; + SDL_Rect usable; + long width, height; + int count = 0, index; + + if (maximum < 1 || !platform_sdl_initialize()) + return 0; + display = platform_window ? SDL_GetDisplayForWindow(platform_window) : 0; + if (!display) + display = SDL_GetPrimaryDisplay(); + if (!display || !SDL_GetDisplayUsableBounds(display, &usable)) + usable.w = usable.h = 0; + for (index = 0; index < (int)(sizeof(platform_window_sizes_offered) / sizeof(*platform_window_sizes_offered)); + index++) + { + width = platform_window_sizes_offered[index][0]; + height = platform_window_sizes_offered[index][1]; + /* (the Xbox's own whatever the desktop's size) */ + if (!index || (width <= usable.w && height <= usable.h)) + count = platform_resolution_add(widths, heights, count, maximum, width, height); + } + /* (the one set, though it is none of them or too big for this desktop, + so that Video Setup shows it) */ + platform_window_size_setting(&width, &height); + return platform_resolution_add(widths, heights, count, maximum, width, height); +} + +#else +int platform_display_resolutions(long *widths, long *heights, int maximum) +{ + (void)widths; + (void)heights; + (void)maximum; + return 0; +} + +int platform_window_sizes(long *widths, long *heights, int maximum) +{ + (void)widths; + (void)heights; + (void)maximum; + return 0; +} + +#endif +#ifndef HALO_ANDROID +/* the window's size (platform_window_size_setting), as the window was made +or last resized: platform_display_apply */ +static long platform_window_width = -1, platform_window_height = -1; #endif -/* the window's scale (display.window_scale, as the window was made or last -resized: platform_display_apply) */ -static long platform_window_scale = -1; BOOL platform_video_initialize(unsigned long width, unsigned long height) { - int scale = (int)config_integer("display.window_scale"); int version; if (platform_window) return TRUE; if (!platform_sdl_initialize()) return FALSE; - if (scale < 1) - scale = 1; - platform_window_scale = scale; #ifdef HALO_ANDROID SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); @@ -459,14 +704,23 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height) #endif #ifdef HALO_ANDROID - platform_window = SDL_CreateWindow("Halo", (int)(width * scale), (int)(height * scale), - SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN); + { + int scale = (int)config_integer("display.window_scale"); + + if (scale < 1) + scale = 1; + platform_window = SDL_CreateWindow("Halo", (int)(width * scale), (int)(height * scale), + SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN); + } #else - /* fullscreen at the desktop's resolution unless display.fullscreen is - false, where the game draws the display's shape at its resolution - (d3d8_gl.c); the window size is the windowed mode F11 switches to and - from, where it draws 640x480 */ - platform_window = SDL_CreateWindow("Halo", (int)(width * scale), (int)(height * scale), + /* fullscreen (either kind) unless display.mode is the window, which F11 + switches to and from: display.window_size, whatever shape the fullscreen + picture has. The game draws at the size platform_screen_mode gives + (d3d8_gl.c). */ + (void)width; + (void)height; + platform_window_size_setting(&platform_window_width, &platform_window_height); + platform_window = SDL_CreateWindow("Halo", (int)platform_window_width, (int)platform_window_height, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY | (config_boolean("debug.hidden_window") ? SDL_WINDOW_HIDDEN : 0) | (platform_fullscreen_setting() ? SDL_WINDOW_FULLSCREEN : 0)); @@ -477,6 +731,7 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height) return FALSE; } #ifndef HALO_ANDROID + platform_fullscreen_requested = platform_fullscreen_setting(); platform_fullscreen_kind_apply(); #endif platform_gl_context = SDL_GL_CreateContext(platform_window); @@ -512,23 +767,28 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height) return TRUE; } -/* display.fullscreen, display.window_scale (when it changes: the window can -be resized) and display.vsync, as Settings has written them */ +/* display.mode, display.resolution (fullscreen's display mode), +display.window_size (when it changes: the window can be resized) and +display.vsync, as Settings has written them; display.resolution_scaling +and borderless's resolution are taken up between frames +(halo_screen_commit) */ void platform_display_apply(void) { #ifndef HALO_ANDROID BOOL fullscreen = platform_fullscreen_setting(); - long scale = config_integer("display.window_scale"); + long width, height; if (!platform_window) return; platform_fullscreen_kind_apply(); - if (((SDL_GetWindowFlags(platform_window) & SDL_WINDOW_FULLSCREEN) != 0) != (fullscreen != FALSE)) - SDL_SetWindowFullscreen(platform_window, fullscreen ? true : false); - if (scale != platform_window_scale && scale >= 1) + if (platform_window_fullscreen() != (fullscreen != FALSE)) + platform_window_set_fullscreen(fullscreen); + platform_window_size_setting(&width, &height); + if (width != platform_window_width || height != platform_window_height) { - platform_window_scale = scale; - SDL_SetWindowSize(platform_window, (int)(640 * scale), (int)(480 * scale)); + platform_window_width = width; + platform_window_height = height; + SDL_SetWindowSize(platform_window, (int)width, (int)height); } #else if (!platform_window) @@ -911,13 +1171,13 @@ static void platform_show_pending_message(void) #else { /* (a box cannot show above a fullscreen game) */ - int fullscreen = (SDL_GetWindowFlags(platform_window) & SDL_WINDOW_FULLSCREEN) != 0; + BOOL fullscreen = platform_window_fullscreen(); if (fullscreen) - SDL_SetWindowFullscreen(platform_window, false); + platform_window_set_fullscreen(FALSE); SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_WARNING, title, text, platform_window); if (fullscreen) - SDL_SetWindowFullscreen(platform_window, true); + platform_window_set_fullscreen(TRUE); } #endif } @@ -1048,8 +1308,7 @@ void platform_pump_events(void) window's size and place while fullscreen) */ if (event.key.down && !event.key.repeat && event.key.scancode == SDL_SCANCODE_F11) { - SDL_SetWindowFullscreen(platform_window, - (SDL_GetWindowFlags(platform_window) & SDL_WINDOW_FULLSCREEN) ? false : true); + platform_window_set_fullscreen(!platform_window_fullscreen()); } #endif break; diff --git a/port/linux/src/sdl_platform.h b/port/linux/src/sdl_platform.h index 6ecd3b36..1c4978be 100644 --- a/port/linux/src/sdl_platform.h +++ b/port/linux/src/sdl_platform.h @@ -62,6 +62,15 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height); #ifndef HALO_ANDROID BOOL platform_screen_mode(long *width, long *height); #endif +/* Video Setup's resolutions (port/linux/game/menu_tags.c), in pixels: the +display's modes the game can draw at, largest first, each once, but the +display's own (Native), and display.resolution's where it is none of them; +how many. None on Android. */ +int platform_display_resolutions(long *widths, long *heights, int maximum); +/* Video Setup's window sizes: those of each shape (4:3, 16:10, 16:9, 21:9) +that fit the desktop, and display.window_size's; how many. None on +Android. */ +int platform_window_sizes(long *widths, long *heights, int maximum); void platform_video_drawable_size(int *width, int *height); /* the window's mode and size and V-Sync, from config.toml as Settings has just written it (the main thread's) */ diff --git a/port/linux/src/xinput_sdl.c b/port/linux/src/xinput_sdl.c index f366b92f..9bd022a6 100644 --- a/port/linux/src/xinput_sdl.c +++ b/port/linux/src/xinput_sdl.c @@ -55,9 +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); +/* port/linux/game/menu_functions.c: two or more players on this machine +(co-op, or split screen in a network game) */ +extern unsigned char pc_menu_split_players(void); /* ---------- device tables */ @@ -638,13 +638,43 @@ static int sdl_gamepads(SDL_Gamepad *gamepads[PORT_COUNT]) return found; } +/* whether one gamepad is port 1's (port_gamepad) */ +static BOOL lone_gamepad_split; + +/* no button of the gamepad held, its sticks and triggers at rest */ +static BOOL gamepad_idle(SDL_Gamepad *gamepad) +{ + int index; + + for (index = 0; index < SDL_GAMEPAD_BUTTON_COUNT; index++) + { + if (SDL_GetGamepadButton(gamepad, (SDL_GamepadButton)index)) + return FALSE; + } + for (index = 0; index < SDL_GAMEPAD_AXIS_COUNT; index++) + { + if (abs(SDL_GetGamepadAxis(gamepad, (SDL_GamepadAxis)index)) > 8000) + return FALSE; + } + return TRUE; +} + /* 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) */ +two or more players with one gamepad (co-op, split screen), port 1 has it +(the keyboard's player is 1, the gamepad's 2). It changes port only at rest: +a button held across the change would be pressed again on the other port +(the B that leaves a profile screen leaving the game as player 1's) */ 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; + if (count == 1) + { + BOOL split = pc_menu_split_players() != 0; + + if (split != lone_gamepad_split && gamepad_idle(gamepads[0])) + lone_gamepad_split = split; + if (lone_gamepad_split) + return port == 1 ? gamepads[0] : NULL; + } return port < count ? gamepads[port] : NULL; } diff --git a/port/windows/README.md b/port/windows/README.md index 7cbb2978..d3a7d4db 100644 --- a/port/windows/README.md +++ b/port/windows/README.md @@ -52,6 +52,7 @@ The game finds the game data as on Linux. Refer to "Start the game" in | Settings | `config.toml` next to `halo.exe` | | Saved games | `%APPDATA%\halo`, or `paths.saves` in `config.toml` | | Log | `debug.txt` in the data root (the folder that contains `maps\`) | +| Log of the port | The console. The release build has no console: `halo.log` next to `halo.exe` | ## How the port operates diff --git a/port/windows/src/win32_posix.c b/port/windows/src/win32_posix.c index f042bbe0..66bee9b6 100644 --- a/port/windows/src/win32_posix.c +++ b/port/windows/src/win32_posix.c @@ -37,6 +37,24 @@ static void windows_startup(void) /* Sleep() waits in 1 ms steps rather than the default 15.6 ms: the game's frame pacing sleeps for short intervals */ timeBeginPeriod(1); + /* a release build has no console (/SUBSYSTEM:WINDOWS), so the platform + layer's log (platform_log's stderr: invite links, the data root, crash + reports) goes to halo.log next to halo.exe, unless stderr already goes + somewhere (a pipe or a file the game was started with) */ + if (_fileno(stderr) < 0) + { + char path[MAX_PATH]; + DWORD length = GetModuleFileNameA(NULL, path, sizeof(path)); + char *slash; + + if (length && length < sizeof(path) && (slash = strrchr(path, '\\')) && + (size_t)(slash + 1 - path) + sizeof("halo.log") <= sizeof(path)) + { + strcpy(slash + 1, "halo.log"); + if (freopen(path, "w", stderr)) + setvbuf(stderr, NULL, _IONBF, 0); + } + } } static int errno_from_windows_error(DWORD error) diff --git a/source/ai/actors.c b/source/ai/actors.c index da4ec393..2ad33a0c 100644 --- a/source/ai/actors.c +++ b/source/ai/actors.c @@ -317,6 +317,7 @@ symbols in this file: #include "units/units.h" #include "units/vehicle_definitions.h" #include "units/vehicles.h" +#include "networking/network_game_globals.h" /* ---------- constants */ @@ -2224,6 +2225,12 @@ long actor_new( long actor_definition_index; long actor_index = NONE; + /* port: a client of the distributed netcode runs no AI: it has the host's + actors' units, driven as the host sends them (port/linux/game/network_actors.c), + and its own (placed at the map's start or by its scripts) would be extras + the host doesn't have */ + if (network_game_distributed_client()) + return NONE; if (actor_variant_definition_index != NONE) { actor_variant_definition = actor_variant_definition_get(actor_variant_definition_index); @@ -2662,6 +2669,9 @@ long actor_place( long unit_index; match_assert("c:\\halo\\SOURCE\\ai\\actors.c", 603, starting_location); + /* port: none on a client of the distributed netcode (actor_new) */ + if (network_game_distributed_client()) + return NONE; objects_garbage_collection(); actor_variant_definition = @@ -2831,7 +2841,8 @@ short actors_spawn_from_unit( { short spawned_actor_count = 0; - if (actor_variant_definition_index != NONE && actor_count > 0) + /* port: none on a client of the distributed netcode (actor_new) */ + if (actor_variant_definition_index != NONE && actor_count > 0 && !network_game_distributed_client()) { struct unit_datum *source_unit = unit_get(unit_index); long source_actor_index = source_unit->unit.swarm_actor_index != NONE ? diff --git a/source/ai/actors.h b/source/ai/actors.h index da8a1eff..f4b5a9fa 100644 --- a/source/ai/actors.h +++ b/source/ai/actors.h @@ -25,9 +25,10 @@ enum enum { - MAXIMUM_ACTORS = 256, - MAXIMUM_SWARMS = 32, - MAXIMUM_SWARM_COMPONENTS = 256, + /* port: the native builds' larger pools (halo_port_capacity.h) */ + MAXIMUM_ACTORS = HALO_PORT_MAXIMUM_ACTORS, + MAXIMUM_SWARMS = HALO_PORT_MAXIMUM_SWARMS, + MAXIMUM_SWARM_COMPONENTS = HALO_PORT_MAXIMUM_SWARM_COMPONENTS, SWARM_COMPONENT_DATUM_SIZE = 0x40, }; @@ -255,7 +256,11 @@ enum NUMBER_OF_ACTOR_PERCEPTION_TYPES, }; -#define MAXIMUM_NUMBER_OF_ACTORS 256 +/* port: the debug information's actors (ai_debug.c's actor_debug_array, +by actor index): every actor of the native builds' larger pool +(halo_port_capacity.h), else one past the Xbox's 256 is written past its +end */ +#define MAXIMUM_NUMBER_OF_ACTORS HALO_PORT_MAXIMUM_ACTORS #define MAXIMUM_NUMBER_OF_ACTOR_PATHS 32 #define MAXIMUM_UNIT_INDICES_PER_SWARM 16 diff --git a/source/ai/ai.c b/source/ai/ai.c index bee7eaa7..d590686a 100644 --- a/source/ai/ai.c +++ b/source/ai/ai.c @@ -465,7 +465,8 @@ typedef char ai_prop_unopposable_enemy_offset_assert[ static void ai_place_pending_mounted_weapons( void); static boolean ai_enemies_endanger_player( - boolean must_be_attacking); + boolean must_be_attacking, + long unit_index); static void ai_flush_spatial_effects( void); @@ -1971,8 +1972,11 @@ void ai_update( return; } +/* port: unit_index NONE is any player's unit (the game's own check); else +only enemies of that unit count (network co-op's respawn, players.c) */ static boolean ai_enemies_endanger_player( - boolean must_be_attacking) + boolean must_be_attacking, + long unit_index) { long current_time = game_time_get(); struct data_iterator iterator; @@ -1985,7 +1989,8 @@ static boolean ai_enemies_endanger_player( { if (prop->player && prop->enemy && - unit_get(prop->unit_index)->unit.player_index != NONE) + unit_get(prop->unit_index)->unit.player_index != NONE && + (unit_index == NONE || prop->unit_index == unit_index)) { struct actor_datum *actor = actor_get(prop->owner_actor_index); long object_index = actor->meta.swarm @@ -2068,13 +2073,20 @@ static boolean ai_enemies_endanger_player( boolean ai_enemies_can_see_player( void) { - return ai_enemies_endanger_player(FALSE); + return ai_enemies_endanger_player(FALSE, NONE); } boolean ai_enemies_attacking_player( void) { - return ai_enemies_endanger_player(TRUE); + return ai_enemies_endanger_player(TRUE, NONE); +} + +/* port: whether enemies are attacking this player's unit */ +boolean ai_port_enemies_attacking_unit( + long unit_index) +{ + return ai_enemies_endanger_player(TRUE, unit_index); } long ai_get_race_from_team_index( diff --git a/source/ai/ai.h b/source/ai/ai.h index be7cdfa1..a6460a09 100644 --- a/source/ai/ai.h +++ b/source/ai/ai.h @@ -142,6 +142,9 @@ boolean ai_consider_major_upgrade( short squad_index, real chance); +/* port: network co-op's respawn (players.c) */ +boolean ai_port_enemies_attacking_unit( + long unit_index); boolean ai_enemies_attacking_player( void); boolean ai_enemies_can_see_player( diff --git a/source/ai/ai_profile.c b/source/ai/ai_profile.c index 2c9876a4..fd67d200 100644 --- a/source/ai/ai_profile.c +++ b/source/ai/ai_profile.c @@ -298,7 +298,7 @@ void ai_profile_display( ai_profile.meters[_ai_meter_units_updated].current_value, ai_profile.meters[_ai_meter_units].current_value, ai_profile.meters[_ai_meter_props].current_value, - 768); + HALO_PORT_MAXIMUM_PROPS); return; } @@ -450,7 +450,7 @@ static void ai_profile_render_encounters( ai_profile.meters[_ai_meter_encounters_updated].current_value, ai_profile.meters[_ai_meter_encounters].current_value, ai_profile.meters[_ai_meter_props].current_value, - 768); + HALO_PORT_MAXIMUM_PROPS); ai_profile_draw_string(profilestring, NUMBEROF(tab_stops), tab_stops, global_real_argb_white); return; } diff --git a/source/ai/encounters.c b/source/ai/encounters.c index e41f47e0..83a3d7f0 100644 --- a/source/ai/encounters.c +++ b/source/ai/encounters.c @@ -311,6 +311,7 @@ symbols in this file: #include "scenario/scenario_definitions.h" #include "structures/structure_bsp_definitions.h" #include "units/units.h" +#include "coop_enemies.h" /* port: port/linux/game/coop_enemies.c */ #include @@ -540,7 +541,8 @@ static boolean encounter_place_actor( long encounter_index, short squad_index, short initial_variant, - boolean spawning); + boolean spawning, + short extra_number); static void encounterless_deactivate( long actor_index); static void encounters_test_activation( @@ -1836,7 +1838,7 @@ boolean encounter_spawn_actor( { if (ai_globals->ai_initialized_for_map) { - if (encounter_place_actor(encounter_index, squad_index, 0, TRUE)) + if (encounter_place_actor(encounter_index, squad_index, 0, TRUE, 0)) { struct encounter_datum *encounter = encounter_get(encounter_index); struct encounter_definition *encounter_definition = TAG_BLOCK_GET_ELEMENT( @@ -2245,9 +2247,17 @@ void encounter_create( for (i = 0; i < count; ++i) { - encounter_place_actor(encounter_index, squad_index, initial_variant, FALSE); + encounter_place_actor(encounter_index, squad_index, initial_variant, FALSE, 0); initial_variant = 0; } + /* port: network co-op's extra enemies, for its players + (coop_enemies.c) */ + { + short extra_count = coop_enemies_extra_count(encounter_index, count); + + for (i = 0; i < extra_count; ++i) + encounter_place_actor(encounter_index, squad_index, 0, FALSE, (short)(i + 1)); + } } encounter_update_status(encounter_index); @@ -2996,18 +3006,25 @@ static void encounter_post_combat( return; } +/* port: extra_number, from 1, places one of network co-op's extra enemies +(coop_enemies.c): at the squad's starting locations in turn, each the +place a ring about its own (0: the squad's own actor, as the Xbox's) */ static boolean encounter_place_actor( long encounter_index, short squad_index, short initial_variant, - boolean spawning) + boolean spawning, + short extra_number) { boolean placed = FALSE; struct encounter_definition *encounter_definition = TAG_BLOCK_GET_ELEMENT( &global_scenario_get()->ai_encounters, DATUM_INDEX_TO_ABSOLUTE_INDEX(encounter_index), struct encounter_definition); struct squad_definition *squad_definition = TAG_BLOCK_GET_ELEMENT( &encounter_definition->squads, squad_index, struct squad_definition); - short starting_location_index = encounter_get_actor_starting_location(encounter_index, squad_index, spawning); + short starting_location_index = extra_number > 0 && squad_definition->starting_locations.count > 0 ? + (short)((extra_number - 1) % squad_definition->starting_locations.count) : + encounter_get_actor_starting_location(encounter_index, squad_index, spawning); + struct actor_starting_location spread_location; if (starting_location_index != NONE) { @@ -3016,6 +3033,18 @@ static boolean encounter_place_actor( short actor_palette_index = squad_definition->actor_palette_index; struct scenario *scenario = global_scenario_get(); + /* port: an extra enemy a ring about the starting location */ + if (extra_number > 0) + { + spread_location = *starting_location; + if (coop_enemies_spread_position(&starting_location->position, + (short)((extra_number - 1) / squad_definition->starting_locations.count + 1), + &spread_location.position)) + { + starting_location = &spread_location; + } + } + if (starting_location->actor_variant_index != NONE) actor_palette_index = starting_location->actor_variant_index; diff --git a/source/ai/props.c b/source/ai/props.c index 933e937e..84334d76 100644 --- a/source/ai/props.c +++ b/source/ai/props.c @@ -95,7 +95,8 @@ static long last_prop_data_full_warn_time = NONE; void props_initialize( void) { - prop_data = game_state_data_new("prop", 768, sizeof(struct prop_datum)); + /* port: the native builds' larger pool (halo_port_capacity.h) */ + prop_data = game_state_data_new("prop", HALO_PORT_MAXIMUM_PROPS, sizeof(struct prop_datum)); match_assert("c:\\halo\\SOURCE\\ai\\props.c", 0x24, prop_data); @@ -141,7 +142,7 @@ static void prop_add( error( _error_silent, "AI knowledge database (%d entries) is full (warns once every 30 sec)", - 768); + HALO_PORT_MAXIMUM_PROPS); last_prop_data_full_warn_time = game_time; } } diff --git a/source/camera/camera_scripting.h b/source/camera/camera_scripting.h index a677c51e..cdd37bf4 100644 --- a/source/camera/camera_scripting.h +++ b/source/camera/camera_scripting.h @@ -53,6 +53,8 @@ void scripted_camera_set_camera_point_relative( long relative_object_index); short scripted_camera_time( void); +long scripted_camera_object_relative_to( + void); void scripted_camera_update( struct dead_camera *camera, struct camera_control const *controls, diff --git a/source/camera/dead_camera.c b/source/camera/dead_camera.c index 7c69a41a..6c2c0e9f 100644 --- a/source/camera/dead_camera.c +++ b/source/camera/dead_camera.c @@ -190,11 +190,9 @@ void dead_camera_update( } else { - next_unit_index = (long)result; + next_unit_index = NONE; } - /* January preserves the command pointer as the fallback object index - * when no player is found. This is a bug; a corrected build should - * initialize next_unit_index to NONE instead. */ + /* With no living player, keep observing the dead unit/current position. */ if (next_unit_index != camera->unit_index && next_unit_index != NONE) { diff --git a/source/camera/director.c b/source/camera/director.c index b8702a4d..b8b3fbf0 100644 --- a/source/camera/director.c +++ b/source/camera/director.c @@ -121,6 +121,7 @@ symbols in this file: #include "camera_scripting.h" #include "dead_camera.h" +#include "coop_spectate.h" /* port: port/linux/game/coop_spectate.c */ #include "editor_flying_camera.h" #include "first_person_camera.h" #include "flying_camera.h" @@ -708,8 +709,12 @@ static void director_choose_camera_game( } else { - struct player_datum *player = player_get(local_player_get_player_index(local_player_index)); - boolean use_dead_camera = player->unit_index == NONE && player->statistics.deaths > 0; + long player_index = local_player_get_player_index(local_player_index); + struct player_datum *player = player_get(player_index); + /* port: and a network co-op player who hasn't spawned yet, waiting for + the level's first checkpoint, watches too */ + boolean use_dead_camera = player->unit_index == NONE && + (player->statistics.deaths > 0 || players_coop_waiting_to_start(player_index)); /* (port: not a client in another's game, the host's rules: a flying camera would see all of it, and one behind the player round its @@ -721,17 +726,25 @@ static void director_choose_camera_game( director_choose_game_perspective(local_player_index, initialize); if (use_dead_camera) { - if (director->camera_proc != (director_camera_update_proc)dead_camera_update) + /* port: a dead network co-op player watches a living teammate + (port/linux/game/coop_spectate.c), or its own body if nobody is alive */ + long watched_unit_index = coop_spectating() ? coop_spectate_unit(local_player_index) : NONE; + + if (director->camera_proc != (director_camera_update_proc)dead_camera_update || + (watched_unit_index != NONE && + ((struct dead_camera *)director->camera_data)->unit_index != watched_unit_index)) { dead_camera_new( (struct dead_camera *)director->camera_data, local_player_index, - NONE); + watched_unit_index); director_set_camera( local_player_index, (director_camera_update_proc)dead_camera_update, TRUE); } + if (watched_unit_index != NONE) + coop_spectate_camera((struct dead_camera *)director->camera_data); } else if (director->camera_proc == (director_camera_update_proc)dead_camera_update) { diff --git a/source/cutscene/cinematics.c b/source/cutscene/cinematics.c index 6c9c4317..26d9f895 100644 --- a/source/cutscene/cinematics.c +++ b/source/cutscene/cinematics.c @@ -74,6 +74,7 @@ symbols in this file: #include "scenario/scenario_definitions.h" #include "text/draw_string.h" #include "text/text_group.h" +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ /* ---------- constants */ @@ -265,6 +266,7 @@ void cinematic_set_title_delayed( { short title_slot_index; + network_coop_note_title(title_index, delay); for (title_slot_index = 0; title_slot_index < MAXIMUM_QUEUED_CINEMATIC_TITLES && cinematic_globals->queued_titles[title_slot_index].title_index != NONE; diff --git a/source/devices/devices.c b/source/devices/devices.c index 4a6bd195..189461d2 100644 --- a/source/devices/devices.c +++ b/source/devices/devices.c @@ -98,6 +98,7 @@ symbols in this file: #include "scenario/scenario_definitions.h" #include "sound/game_sound.h" #include "sound/sound_definitions.h" +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ /* ---------- constants */ @@ -112,6 +113,10 @@ enum _scenario_device_group_can_change_only_once_bit = 0, }; +/* port: the group flags network_coop.c syncs */ +#define DEVICE_GROUP_NETWORK_FLAGS \ + (FLAG(_device_group_can_change_only_once_bit) | FLAG(_device_group_changed_once_bit)) + enum { _device_function_none = 0, @@ -186,6 +191,9 @@ typedef char device_animation_frame_count_offset_assert[ void device_group_set_actual_value( short group_index, real actual_value); +static boolean device_group_change( + short group_index, + real desired_value); static short device_group_new( real initial_value, unsigned long flags); @@ -594,6 +602,7 @@ void device_group_set_actual_value( group = datum_get(device_groups_data, group_index); group->actual_value = actual_value; + network_coop_note_device_snap(group_index); object_iterator_new(&iterator, _object_mask_device, 0); while ((device = object_iterator_next(&iterator)) != NULL) @@ -677,6 +686,74 @@ void device_render_debug( boolean device_group_set_desired_value( short group_index, real desired_value) +{ + /* port: on a co-op client only the host moves devices (network_coop.c) */ + if (network_coop_devices_remote()) + return FALSE; + + return device_group_change(group_index, desired_value); +} + +/* port: reads a group for network_coop.c; FALSE if it doesn't exist */ +boolean device_group_network_get( + short group_index, + real *value, + word *flags, + boolean *runtime) +{ + struct device_group_datum *group = group_index != NONE ? datum_try_and_get(device_groups_data, group_index) : NULL; + + if (!group) + return FALSE; + *value = group->actual_value; + *flags = group->flags & DEVICE_GROUP_NETWORK_FLAGS; + *runtime = TEST_FLAG(group->flags, _device_group_runtime_bit); + + return TRUE; +} + +/* port: a co-op client applies the host's group state. snap jumps the +devices to the value; otherwise they move there as usual. */ +void device_group_network_set( + short group_index, + real value, + word flags, + boolean snap) +{ + struct device_group_datum *group = group_index != NONE ? datum_try_and_get(device_groups_data, group_index) : NULL; + + if (!group) + return; + if (snap) + device_group_set_actual_value(group_index, value); + else + { + group->flags &= ~FLAG(_device_group_changed_once_bit); + device_group_change(group_index, value); + } + group->flags = (word)((group->flags & ~DEVICE_GROUP_NETWORK_FLAGS) | (flags & DEVICE_GROUP_NETWORK_FLAGS)); +} + +void device_port_set_state( + long device_index, + real power, + boolean move_to_position, + real position) +{ + struct device_datum *device = device_get(device_index); + + device->device.power = PIN(power, 0.0f, 1.0f); + if (move_to_position) + { + device->device.position = PIN(position, 0.0f, 1.0f); + device->device.position_velocity = 0.0f; + device->device.flags |= FLAG(_device_position_changed_bit); + } +} + +static boolean device_group_change( + short group_index, + real desired_value) { boolean changed = FALSE; diff --git a/source/devices/devices.h b/source/devices/devices.h index 591443f4..8bfd5f2e 100644 --- a/source/devices/devices.h +++ b/source/devices/devices.h @@ -129,6 +129,24 @@ void device_set_power( boolean device_group_set_desired_value( short group_index, real desired_value); +/* port: co-op device sync (port/linux/game/network_coop.c) */ +boolean device_group_network_get( + short group_index, + real *value, + word *flags, + boolean *runtime); +void device_group_network_set( + short group_index, + real value, + word flags, + boolean snap); +/* port: a co-op client puts a device at the host's power, and at its +position when move_to_position */ +void device_port_set_state( + long device_index, + real power, + boolean move_to_position, + real position); boolean device_can_change_position( long device_index); boolean device_frontfacing( diff --git a/source/effects/player_effects.c b/source/effects/player_effects.c index 17facdec..365d3885 100644 --- a/source/effects/player_effects.c +++ b/source/effects/player_effects.c @@ -107,6 +107,7 @@ symbols in this file: #include "units/units.h" #include +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ /* ---------- constants */ @@ -397,6 +398,7 @@ void scripted_player_effect_set_rotation( real pitch, real roll) { + network_coop_note_player_effect(_coop_player_effect_rotation, yaw, pitch, roll); player_effect_globals->scripted_effect.max_rotation.yaw = DEGREES_TO_RADIANS(yaw); player_effect_globals->scripted_effect.max_rotation.pitch = DEGREES_TO_RADIANS(pitch); player_effect_globals->scripted_effect.max_rotation.roll = DEGREES_TO_RADIANS(roll); @@ -458,6 +460,50 @@ void player_effect_screen_fade_out( return; } +/* port: read and set the screen fade, so a co-op host can send it to its +clients (port/linux/game/network_coop.c): color, length, direction, and +the game time it started */ +void player_effect_port_screen_fade_get( + real_rgb_color *color, + short *ticks, + boolean *fading_out, + long *start_time) +{ + *color = player_effect_globals->screen_fade.color; + *ticks = player_effect_globals->screen_fade.ticks; + *fading_out = player_effect_globals->screen_fade.fading_out; + *start_time = player_effect_globals->screen_fade.start_time; + + return; +} + +void player_effect_port_screen_fade_set( + real_rgb_color const *color, + short ticks, + boolean fading_out, + long start_time) +{ + player_effect_globals->screen_fade.color = *color; + player_effect_globals->screen_fade.ticks = ticks; + player_effect_globals->screen_fade.fading_out = fading_out; + player_effect_globals->screen_fade.start_time = start_time; + + return; +} + +boolean player_effect_port_scripted_active( + void) +{ + return TEST_FLAG(player_effect_globals->global_flags, _scripted_player_effect_active_bit); +} + +void player_effect_port_scripted_end( + void) +{ + SET_FLAG(player_effect_globals->global_flags, _scripted_player_effect_active_bit, FALSE); + SET_FLAG(player_effect_globals->global_flags, _scripted_player_effect_stopping_bit, FALSE); +} + void player_effect_get_damage_indicators( short local_player_index, byte *damage_indicators) @@ -609,6 +655,7 @@ void scripted_player_effect_set_translation( { real_vector3d *translation = &player_effect_globals->scripted_effect.max_translation; + network_coop_note_player_effect(_coop_player_effect_translation, horizontal, vertical, depth); translation->i = horizontal; translation->j = vertical; translation->k = depth; @@ -622,6 +669,7 @@ void scripted_player_effect_start( { short ticks; + network_coop_note_player_effect(_coop_player_effect_start, maximum_intensity, attack_time, 0.0f); player_effect_globals->scripted_effect.max_intensity = maximum_intensity; ticks = (short)fast_ftol(attack_time * TICKS_PER_SECOND); @@ -644,6 +692,7 @@ void scripted_player_effect_stop( { short ticks = (short)fast_ftol(duration * TICKS_PER_SECOND); + network_coop_note_player_effect(_coop_player_effect_stop, duration, 0.0f, 0.0f); player_effect_globals->scripted_effect.timer = ticks; player_effect_globals->scripted_effect.total_time = ticks; SET_FLAG( diff --git a/source/effects/player_effects.h b/source/effects/player_effects.h index 4f96a772..3688fe75 100644 --- a/source/effects/player_effects.h +++ b/source/effects/player_effects.h @@ -56,6 +56,21 @@ void player_effect_screen_fade_out( real green, real blue, short ticks); +/* port: screen fade sync for network co-op */ +void player_effect_port_screen_fade_get( + real_rgb_color *color, + short *ticks, + boolean *fading_out, + long *start_time); +void player_effect_port_screen_fade_set( + real_rgb_color const *color, + short ticks, + boolean fading_out, + long start_time); +/* port: whether the scripts' screen shake is running, and ending it at once +(network co-op keeps a client's in step with the host's) */ +boolean player_effect_port_scripted_active(void); +void player_effect_port_scripted_end(void); void player_effect_get_damage_indicators( short local_player_index, byte *damage_indicators); diff --git a/source/game/game.c b/source/game/game.c index 0549cc04..b6ed1634 100644 --- a/source/game/game.c +++ b/source/game/game.c @@ -162,6 +162,7 @@ struct game_options; #include "networking/network_messages.h" #include "networking/telnet_console.h" #include "objects/objects.h" +#include "objects/widgets/antenna.h" #include "objects/widgets/widgets.h" #include "physics/breakable_surfaces.h" #include "physics/collision_usage.h" @@ -179,9 +180,12 @@ struct game_options; #include "structures/structures.h" #include "units/units.h" #include "units/vehicles.h" +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ /* network_game_globals.c's */ boolean network_game_distributed_client(void); +/* port: a client drives the host's actors' units as the host sent them (port/linux/game/network_actors.c) */ +void network_actors_drive(void); /* ---------- constants */ @@ -325,19 +329,31 @@ void game_tick( positions, and could place objects of their own) */ if (!network_game_distributed_client()) ai_update(); + else + network_actors_drive(); players_update_before_game(); seconds_per_tick = game_globals->players_are_double_speed ? 1.0f / (2 * TICKS_PER_SECOND) : 1.0f / TICKS_PER_SECOND; effects_update(seconds_per_tick); + /* An antenna's chain is a simulation of the Xbox's own step, an update a + tick: stepped a frame at a time, its springs, its carry and its points' + physics act against the frames' own movement of it, and a vehicle at + speed swings it far wider than a tick's step does. The frames between the + ticks are drawn from what each tick leaves (antenna.c). */ + antennas_update(seconds_per_tick); lock_global_random_seed(); rumble_update(); first_person_weapons_update(); unlock_global_random_seed(); game_engine_update(); editor_update(); - hs_update(); + /* port: in network co-op only the host runs the scripts. A client running + them would place the map's actors and objects a second time and make + decisions that belong to the host. */ + if (!(network_game_distributed_client() && network_coop_active())) + hs_update(); recorded_animations_update(); objects_update(); players_update_after_game(); @@ -925,7 +941,9 @@ void remove_quitting_players_from_game( struct player_datum *player; long current_time; - if (!game_engine_running()) + /* port: also in network co-op, which has no game engine. Without this a + player who left kept their unit, and it respawned. */ + if (!game_engine_running() && !network_coop_active()) return; current_time = game_time_get(); diff --git a/source/game/game_allegiance.c b/source/game/game_allegiance.c index 15f2e970..4421b65f 100644 --- a/source/game/game_allegiance.c +++ b/source/game/game_allegiance.c @@ -88,8 +88,8 @@ struct game_allegiance_globals short allegiance_count; struct game_allegiance_record allegiances[8]; byte reserved92[2]; - unsigned long ally_bitvector[4]; - unsigned long friendly_bitvector[4]; + unsigned long ally_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS]; + unsigned long friendly_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS]; }; typedef char game_allegiance_record_size_assert[ @@ -452,6 +452,28 @@ void game_allegiance_update( return; } +/* port: which teams are allies and which friends. A co-op client takes the +host's, as only the host runs the scripts that make allegiances. */ +void game_allegiance_get_teams( + unsigned long ally_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS], + unsigned long friendly_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS]) +{ + csmemcpy(ally_bitvector, game_allegiance_globals->ally_bitvector, + sizeof(game_allegiance_globals->ally_bitvector)); + csmemcpy(friendly_bitvector, game_allegiance_globals->friendly_bitvector, + sizeof(game_allegiance_globals->friendly_bitvector)); +} + +void game_allegiance_set_teams( + unsigned long const ally_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS], + unsigned long const friendly_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS]) +{ + csmemcpy(game_allegiance_globals->ally_bitvector, ally_bitvector, + sizeof(game_allegiance_globals->ally_bitvector)); + csmemcpy(game_allegiance_globals->friendly_bitvector, friendly_bitvector, + sizeof(game_allegiance_globals->friendly_bitvector)); +} + void game_allegiance_create( short team1_index, boolean team1_suspicious, diff --git a/source/game/game_allegiance.h b/source/game/game_allegiance.h index e607caeb..fbbe2ab4 100644 --- a/source/game/game_allegiance.h +++ b/source/game/game_allegiance.h @@ -11,6 +11,9 @@ enum _allegiance_incident_forgive }; +/* port: the longs of game_allegiance_get_teams' bit vectors */ +#define GAME_ALLEGIANCE_BITVECTOR_LONGS 4 + /* ---------- prototypes/GAME_ALLEGIANCE.C */ void game_allegiance_initialize( @@ -23,6 +26,12 @@ void game_allegiance_dispose_from_old_map( void); void game_allegiance_update( void); +void game_allegiance_get_teams( + unsigned long ally_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS], + unsigned long friendly_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS]); +void game_allegiance_set_teams( + unsigned long const ally_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS], + unsigned long const friendly_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS]); void game_allegiance_create( short team1_index, boolean team1_suspicious, diff --git a/source/game/game_engine.c b/source/game/game_engine.c index 9ec84248..29800ae1 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -568,6 +568,7 @@ symbols in this file: #include "networking/network_game_globals.h" #include "networking/network_server_manager.h" #include "networking/network_game_manager.h" +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ /* (network_server_manager_internal.h's: the host's game record) */ struct network_game *network_game_server_get_game(struct network_game_server *server); #include "objects.h" @@ -7068,16 +7069,31 @@ short game_engine_friendly_damage( { struct game_variant_options const *options = game_variant_options_get(); struct player_datum *attacker; + short friendly_fire; - if (!game_engine || !global_variant.universal_variant.teams || attacker_player_index == NONE || - options->friendly_fire == _friendly_fire_on) + /* port: network co-op's friendly fire is Server Setup's FRIENDLY FIRE, + in the host's game settings (only the host deals damage), between its + players: their AI allies they always hurt, as in the campaign */ + if (network_coop_active()) { - return _friendly_damage_all; + struct network_game *game = network_game_get_game(); + struct unit_datum *unit = (struct unit_datum *)object_try_and_get_and_verify_type(object_index, + _object_mask_unit); + + if (!game || !unit || unit->unit.player_index == NONE) + return _friendly_damage_all; + friendly_fire = game->variant_options.friendly_fire; } + else if (!game_engine || !global_variant.universal_variant.teams) + return _friendly_damage_all; + else + friendly_fire = options->friendly_fire; + if (attacker_player_index == NONE || friendly_fire == _friendly_fire_on) + return _friendly_damage_all; attacker = (struct player_datum *)datum_try_and_get(player_data, attacker_player_index); if (!attacker || attacker->unit_index == object_index) return _friendly_damage_all; - switch (options->friendly_fire) + switch (friendly_fire) { case _friendly_fire_off: return _friendly_damage_none; case _friendly_fire_shields_only: return _friendly_damage_shields; diff --git a/source/game/game_time.c b/source/game/game_time.c index e3454dd5..739418be 100644 --- a/source/game/game_time.c +++ b/source/game/game_time.c @@ -482,7 +482,14 @@ void game_time_update( } } + /* The effects and the widgets that move every frame (game_frame) hang + off objects: they read the pose between the last two ticks, as the + renderer does, and the ticks above have taken their snapshots already + (port/linux/game/render_interpolation.c). The frames of one tick share + the fraction, so what they hang off is drawn where they put it. */ + render_interpolation_frame_begin(); game_frame(game_time_get_speed()*time_delta_sec); + render_interpolation_frame_end(); } else { diff --git a/source/game/player_control.c b/source/game/player_control.c index 1fbdc5ea..1e12d267 100644 --- a/source/game/player_control.c +++ b/source/game/player_control.c @@ -209,6 +209,7 @@ symbols in this file: #include "units/vehicles.h" #include "real_math.h" +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ /* ---------- constants */ @@ -1787,59 +1788,49 @@ boolean player_control_action_test_look_relative_all_directions( _player_control_look_relative_all_directions_flags); } -static void player_control_action_test_check_reset_input_blob( - struct input_blob *input) +/* The actions the scripts test for (player_control_action_test_*) that a +player's unit control, turning, sticks and trigger show. A local player's +come from its input blob; in network co-op the host's scripts count a +client's too (port/linux/game/network_distributed.c), so Pillar of +Autumn's tutorial goes on for whichever player does what it asks. */ +void player_control_action_test_note( + unsigned long unit_control_flags, + real_euler_angles2d const *facing_delta, + real_vector2d const *throttle, + real primary_trigger) { - struct player_control_globals_data *globals; + struct player_control_globals_data *globals = player_control_globals; - if (input->accept && cinematic_can_be_skipped()) - { - main_skip_cinematic(); - } - - globals = player_control_globals; - if (TEST_FLAG(input->unit_control_flags, _unit_control_action_bit)) + if (TEST_FLAG(unit_control_flags, _unit_control_action_bit)) { globals->action_flags |= FLAG(_player_control_action_bit); } - if (TEST_FLAG(input->unit_control_flags, _unit_control_jump_bit)) + if (TEST_FLAG(unit_control_flags, _unit_control_jump_bit)) { globals->action_flags |= FLAG(_player_control_jump_bit); } - if (input->accept) - { - globals->action_flags |= FLAG(_player_control_accept_bit); - } - if (input->back) - { - globals->action_flags |= FLAG(_player_control_back_bit); - } - if (input->primary_trigger > 0.f) - { - globals->action_flags |= FLAG(_player_control_primary_trigger_bit); - } - if (TEST_FLAG(input->unit_control_flags, _unit_control_throw_grenade_bit)) + if (TEST_FLAG(unit_control_flags, _unit_control_throw_grenade_bit)) { globals->action_flags |= FLAG(_player_control_grenade_trigger_bit); } - if (TEST_FLAG(input->player_control_flags, _player_control_input_zoom_bit)) + if (primary_trigger > 0.f) { - globals->action_flags |= FLAG(_player_control_zoom_bit); + globals->action_flags |= FLAG(_player_control_primary_trigger_bit); } - if (input->facing_delta.pitch > 0.f) + if (facing_delta->pitch > 0.f) { globals->action_flags |= FLAG(_player_control_look_relative_up_bit); } - else if (input->facing_delta.pitch < 0.f) + else if (facing_delta->pitch < 0.f) { globals->action_flags |= FLAG(_player_control_look_relative_down_bit); } - if (input->facing_delta.yaw > 0.f) + if (facing_delta->yaw > 0.f) { globals->action_flags |= FLAG(_player_control_look_relative_left_bit); } - else if (input->facing_delta.yaw < 0.f) + else if (facing_delta->yaw < 0.f) { globals->action_flags |= FLAG(_player_control_look_relative_right_bit); } @@ -1863,22 +1854,51 @@ static void player_control_action_test_check_reset_input_blob( } } #endif - if (input->throttle.i > 0.f) + if (throttle->i > 0.f) { globals->action_flags |= FLAG(_player_control_move_relative_forward_bit); } - else if (input->throttle.i < 0.f) + else if (throttle->i < 0.f) { globals->action_flags |= FLAG(_player_control_move_relative_backward_bit); } - if (input->throttle.j > 0.f) + if (throttle->j > 0.f) { globals->action_flags |= FLAG(_player_control_move_relative_right_bit); } - else if (input->throttle.j < 0.f) + else if (throttle->j < 0.f) { globals->action_flags |= FLAG(_player_control_move_relative_left_bit); } +} + +static void player_control_action_test_check_reset_input_blob( + struct input_blob *input) +{ + struct player_control_globals_data *globals; + + if (input->accept && (cinematic_can_be_skipped() || network_coop_skip_offered())) + { + /* port: in network co-op the players vote to skip (network_coop.c) */ + if (!network_coop_vote_skip()) + main_skip_cinematic(); + } + + player_control_action_test_note(input->unit_control_flags, &input->facing_delta, &input->throttle, + input->primary_trigger); + globals = player_control_globals; + if (input->accept) + { + globals->action_flags |= FLAG(_player_control_accept_bit); + } + if (input->back) + { + globals->action_flags |= FLAG(_player_control_back_bit); + } + if (TEST_FLAG(input->player_control_flags, _player_control_input_zoom_bit)) + { + globals->action_flags |= FLAG(_player_control_zoom_bit); + } if (!TEST_FLAG(globals->action_test_flags, _player_control_action_bit)) { diff --git a/source/game/players.c b/source/game/players.c index 0d750ebc..b3954598 100644 --- a/source/game/players.c +++ b/source/game/players.c @@ -236,6 +236,8 @@ symbols in this file: #include "camera/observer.h" #include "data.h" #include "devices/devices.h" +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ +#include "cutscene/cinematics.h" /* port: network co-op's first spawns */ #include "editor/editor_stubs.h" #include "effects/effects.h" #include "effects/player_effects.h" @@ -270,6 +272,7 @@ symbols in this file: #include "tag_files/tag_groups.h" #include "text/text_group.h" #include "text/unicode.h" +#include "objects/object_definitions.h" #include "units/biped_definitions.h" #include "units/bipeds.h" #include "units/units.h" @@ -408,6 +411,8 @@ typedef char players_static_data_screen_flash_offset_assert[ /* ---------- prototypes */ +static boolean players_respawn_network_coop( + void); static long is_player_in_trigger( short bsp_switch_trigger_volume_index, long object_index); @@ -457,6 +462,40 @@ static void player_handle_powerup_equipment( /* ---------- globals */ +/* port: network co-op timers, by player absolute index. Each holds the +game time plus one at which something started, or 0 when it isn't happening. +spawnable_since: when the player became somewhere others can spawn beside +(player_spawnable_beside); they wait a while after (players_coop_room_to_spawn). */ +static struct +{ + long spawnable_since[NETWORK_GAME_MAXIMUM_PLAYER_COUNT]; + /* when the dead started waiting for a safe teammate to respawn beside */ + long respawn_wait_since; + /* when each player's unit left the loaded structure BSP */ + long stranded_since[NETWORK_GAME_MAXIMUM_PLAYER_COUNT]; + /* the structure BSP loaded, and when it last switched, by a trigger, + a script or a skipped cutscene (players_coop_bsp_switch_ready) */ + short structure_bsp_index; + long bsp_switch_time; + /* the structure BSPs the players have been in this map, by bit */ + word visited_structure_bsps; + /* where each player last stood on the ground, and on which BSP: where + everyone comes back when the last checkpoint was on another one */ + short ground_structure_bsp_index; + boolean has_ground_position[NETWORK_GAME_MAXIMUM_PLAYER_COUNT]; + real_point3d ground_positions[NETWORK_GAME_MAXIMUM_PLAYER_COUNT]; +} players_coop_state; + +/* port: where each player was at the last checkpoint, in network co-op +(players_note_checkpoint) */ +static struct +{ + boolean valid; + short structure_bsp_index; + boolean has_position[NETWORK_GAME_MAXIMUM_PLAYER_COUNT]; + real_point3d positions[NETWORK_GAME_MAXIMUM_PLAYER_COUNT]; +} players_checkpoint; + struct players_globals *players_globals; struct data_array *team_data; struct data_array *player_data; @@ -553,6 +592,9 @@ void players_initialize_for_new_map( players_globals->all_dead = FALSE; players_globals->pending_teleport_starting_location_index = NONE; players_globals->respawn_failure = 0; + /* port: a new map has no network co-op checkpoint yet */ + csmemset(&players_checkpoint, 0, sizeof(players_checkpoint)); + csmemset(&players_coop_state, 0, sizeof(players_coop_state)); data_make_valid(player_data); data_make_valid(team_data); csmemset( @@ -707,6 +749,15 @@ short local_player_count( return players_globals->local_player_count; } +/* port: local_player_count, 0 before the players' globals are made (the +input's first polls ask it: port/linux/game/menu_functions.c's +pc_menu_split_players) */ +short players_port_local_player_count( + void) +{ + return players_globals ? players_globals->local_player_count : 0; +} + short local_player_get_next( short local_player_index) { @@ -916,7 +967,9 @@ boolean any_player_is_dead( player_data); while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL) { - if (player->unit_index == NONE) + /* port: ignore co-op players still waiting for the first checkpoint, + or the checkpoint that lets them in could never be saved */ + if (player->unit_index == NONE && !players_coop_waiting_to_start(iterator.datum_index)) return TRUE; } @@ -1260,6 +1313,31 @@ long find_best_starting_location_index( return best_starting_location_index; } +/* port: paints a network co-op player's unit in their profile colour. The +campaign Spartan is always green because its tag fixes that colour, which +overrides the colour the unit is created with; this replaces fixed colours. */ +static void players_coop_color_unit( + long unit_index, + real_rgb_color const *color) +{ + struct object_datum *object = object_get(unit_index); + struct object_definition *definition = object_definition_get(object->definition_index); + short index; + + for (index = 0; index < definition->object.change_colors.count && index < NUMBER_OF_OBJECT_CHANGE_COLORS; index++) + { + struct object_change_color_definition *change_color = TAG_BLOCK_GET_ELEMENT( + &definition->object.change_colors, index, struct object_change_color_definition); + + /* skip colours driven by a function, like the shield glow */ + if (change_color->permutations.count > 0 && !change_color->scaled_by && !change_color->darken_by) + { + object->object.base_change_colors[index] = *color; + object->object.outgoing_change_colors[index] = *color; + } + } +} + static void player_spawn( long player_index) { @@ -1370,6 +1448,8 @@ static void player_spawn( &placement_data, &change_color); unit_index = object_new(&placement_data); + if (unit_index != NONE && network_coop_active()) + players_coop_color_unit(unit_index, &change_color); if (unit_index != NONE) { unit = unit_try_and_get(unit_index); @@ -1798,14 +1878,17 @@ static boolean player_handle_action( switch (player->action_result) { case _player_action_result_touch_device: - device_touched(player->action_object_index, player->unit_index); + /* port: on a co-op client the host decides device use; the action is + relayed to it (network_coop.c) */ + if (!network_coop_devices_remote()) + device_touched(player->action_object_index, player->unit_index); result = TRUE; break; case _player_action_result_swap_for_powerup: - /* port: a distributed client's inventories are the host's (the - powerup is swapped where the host decides pickups, and the relayed - action of a remote player reaches here too): it swaps nothing */ + /* port: a distributed client swaps nothing: the host owns inventories + and decides pickups, and a remote player's relayed action also reaches + here */ if (!players_decide_pickups()) { result = TRUE; @@ -1907,7 +1990,7 @@ static boolean player_teleport_internal( match_assert( "c:\\halo\\SOURCE\\game\\players.c", 0x4FB, - source_unit_index==NONE || local_player_count()>1); + source_unit_index==NONE || local_player_count()>1 || game_connection()!=_game_connection_local); if (source_unit_index != NONE && object_get_ultimate_parent(source_unit_index) != source_unit_index) { @@ -2103,11 +2186,12 @@ static boolean player_teleport_internal( else { error(2, "couldn't teleport player into a valid location"); - match_assert( - "c:\\halo\\SOURCE\\game\\players.c", - 0x5AB, - player->local_player_index!=NONE); - player_pseudo_kill(player_index, source_unit_index); + /* port: only a local player is pseudo-killed, because the respawn + hands the unit back to a local player; another machine's stays put + (the Xbox halted for it). Network co-op respawns its own way, so its + players stay put too. */ + if (player->local_player_index != NONE && !network_coop_active()) + player_pseudo_kill(player_index, source_unit_index); } return result; @@ -2306,6 +2390,11 @@ boolean players_respawn_coop( boolean result; boolean dangerous; + /* port: network co-op checks safety around each teammate instead + (players_respawn_network_coop) */ + if (game_connection() == _game_connection_network_server && network_coop_active()) + return players_respawn_network_coop(); + players_globals->respawn_failure = 0; result = FALSE; if (!players_globals->respawn_failed) @@ -2401,6 +2490,579 @@ result_complete: return result; } +/* ---------- port: network co-op + +A network game on a campaign map with no game engine is co-op. Players +spawn on the host and respawn beside each other, as in local co-op +(players_respawn_coop). When everyone is dead, local co-op reverts to the +last checkpoint (game_state_revert), but every machine would have to do +that in step. Instead the players respawn where they were at the last +checkpoint (main_save_map_private records it), or at the map's start if +that was on another BSP. */ + +static boolean players_coop_room_to_spawn( + void); +static boolean player_place_beside_teammate( + long player_index); + +/* How long into a level the extra players wait before spawning. The opening +cutscene starts a tick or so in, and the first player may still be somewhere +with no room beside them (the drop pod on Halo). */ +#define COOP_LEVEL_START_TICKS (2 * TICKS_PER_SECOND) + +/* Co-op: TRUE while players after the first are held back from spawning: +during the level's first seconds, during a cutscene, or until a teammate has +been free on foot for a while (players_coop_room_to_spawn). */ +static boolean players_coop_extras_held( + void) +{ + return game_time_get() < COOP_LEVEL_START_TICKS || cinematic_in_progress() || !players_coop_room_to_spawn(); +} + +/* Co-op host: TRUE if a player who hasn't spawned on this level yet may +spawn now. The first player spawns at once (the level's script places them). +The others spawn beside a teammate (player_place_beside_teammate) once nothing +holds them back (players_coop_extras_held): when the opening cutscene ends or +is skipped, or straight away if they join later. */ +static boolean players_coop_may_spawn( + void) +{ + struct data_iterator iterator; + struct player_datum *player; + + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + if (player->unit_index != NONE) + return !players_coop_extras_held(); + } + + return TRUE; +} + +/* TRUE if this player hasn't spawned on the level yet and is being held +back (players_coop_extras_held); they spectate a teammate meanwhile */ +boolean players_coop_waiting_to_start( + long player_index) +{ + struct player_datum *player = player_try_and_get(player_index); + + return player && player->unit_index == NONE && player->statistics.deaths == 0 && network_coop_active() && + players_coop_extras_held(); +} + +/* an enemy projectile this close (world units) makes a teammate unsafe +to respawn beside */ +#define COOP_RESPAWN_DANGER_RADIUS 15.0f +/* how often the respawn's safety test runs while someone is dead */ +#define COOP_RESPAWN_CHECK_TICKS (TICKS_PER_SECOND / 2) +/* after waiting this long for a safe teammate, the dead respawn beside any +teammate standing on the ground */ +#define COOP_RESPAWN_FALLBACK_TICKS (10 * TICKS_PER_SECOND) + +/* TRUE if an enemy projectile (a grenade, a plasma bolt) is flying near the +unit. Players' own shots and needles stuck in a body don't count. */ +static boolean players_coop_danger_near( + long unit_index) +{ + real_point3d const *center = &object_get(unit_index)->object.bounding_sphere_center; + real radius_squared = COOP_RESPAWN_DANGER_RADIUS * COOP_RESPAWN_DANGER_RADIUS; + struct object_iterator iterator; + struct object_datum *object; + + object_iterator_new(&iterator, _object_mask_projectile, 0); + while ((object = object_iterator_next(&iterator)) != NULL) + { + if (object->object.owner_player_index == NONE && object->object.parent_object_index == NONE && + distance_squared3d(center, &object->object.bounding_sphere_center) <= radius_squared) + { + return TRUE; + } + } + + return FALSE; +} + +/* TRUE if the unit (or the vehicle it rides) is inside the loaded structure +BSP. In network co-op players can be left outside it after a BSP switch. */ +static boolean players_coop_unit_in_structure( + long unit_index) +{ + return object_get(object_get_ultimate_parent(unit_index))->object.location.cluster_index != NONE; +} + +/* TRUE if a living player's unit is on solid ground: inside the structure +BSP, not airborne and not in a moving vehicle */ +static boolean players_coop_unit_grounded( + long unit_index) +{ + long root_index = object_get_ultimate_parent(unit_index); + + if (!players_coop_unit_in_structure(unit_index)) + return FALSE; + if (root_index == unit_index) + { + struct biped_datum *biped = biped_try_and_get(unit_index); + + return !biped || !TEST_FLAG(biped->biped.flags, _biped_airborne_bit); + } + else + { + struct players_vehicle_datum *vehicle = (struct players_vehicle_datum *) + object_try_and_get_and_verify_type(root_index, _object_mask_vehicle); + + return !vehicle || vehicle->vehicle.unknown_state <= 0; + } +} + +/* TRUE if it is safe to respawn beside a living player's unit: grounded, +not under attack, and no enemy projectile nearby */ +static boolean players_coop_unit_safe( + long unit_index) +{ + return players_coop_unit_grounded(unit_index) && !ai_port_enemies_attacking_unit(unit_index) && + !players_coop_danger_near(unit_index); +} + +/* the unit of the first living player that passes `test`, or NONE */ +static long players_coop_unit_where( + boolean (*test)(long unit_index)) +{ + struct data_iterator iterator; + struct player_datum *player; + + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + if (player->unit_index != NONE && test(player->unit_index)) + return player->unit_index; + } + + return NONE; +} + +/* Network co-op respawn: the dead come back beside the first teammate who +is safe, or after COOP_RESPAWN_FALLBACK_TICKS beside one at least on the +ground, so a long fight can't keep them out. The campaign's own test +(players_respawn_coop) is map-wide (any projectile or enemy attack +anywhere), which with many players spread out would almost never pass. +Returns whether everyone waiting came back. */ +static boolean players_respawn_network_coop( + void) +{ + struct data_iterator iterator; + struct player_datum *player; + long safe_unit_index; + boolean result = TRUE; + + if (!players_coop_state.respawn_wait_since) + players_coop_state.respawn_wait_since = game_time_get() + 1; + /* main.c calls this every tick while someone is dead; the safety test + walks all the map's projectiles, so twice a second is enough */ + if (game_time_get() % COOP_RESPAWN_CHECK_TICKS != 0) + return FALSE; + safe_unit_index = players_coop_unit_where(players_coop_unit_safe); + if (safe_unit_index == NONE && + game_time_get() + 1 - players_coop_state.respawn_wait_since >= COOP_RESPAWN_FALLBACK_TICKS) + { + safe_unit_index = players_coop_unit_where(players_coop_unit_grounded); + } + if (safe_unit_index == NONE) + return FALSE; + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + /* players still waiting for the level's first checkpoint spawn then */ + if (player->unit_index != NONE || player->quit_out_of_game || + players_coop_waiting_to_start(iterator.datum_index)) + { + continue; + } + player_spawn(iterator.datum_index); + if (player->unit_index == NONE) + result = FALSE; + /* (beside the safe teammate, else any with room: never left at the + level's start, where player_spawn put them) */ + else if (!player_teleport(iterator.datum_index, safe_unit_index, + &object_get(safe_unit_index)->object.bounding_sphere_center) && + !player_place_beside_teammate(iterator.datum_index)) + { + result = FALSE; + } + } + if (result) + players_coop_state.respawn_wait_since = 0; + + return result; +} + +/* What a new player can spawn beside: the player's unit on foot, or the +vehicle it rides if a player drives it (a Warthog). NONE while scripts hold +the controls (Pillar of Autumn's cryo tube) or while it rides an AI vehicle +(Silent Cartographer's Pelican), which would put the new player inside. */ +static long player_spawnable_beside( + struct player_datum const *player) +{ + long vehicle_index; + long driver_index; + + if (player->unit_index == NONE || !player_input_enabled() || !players_coop_unit_in_structure(player->unit_index)) + return NONE; + vehicle_index = object_get_ultimate_parent(player->unit_index); + if (vehicle_index == player->unit_index) + return vehicle_index; + driver_index = object_try_and_get_and_verify_type(vehicle_index, _object_mask_unit) ? + unit_get(vehicle_index)->unit.driver_object_index : NONE; + return driver_index != NONE && unit_get(driver_index)->unit.player_index != NONE ? vehicle_index : NONE; +} + +/* How long a teammate must have been somewhere spawnable before others +spawn beside them, so nobody lands inside a vehicle they just left. */ +#define COOP_DISEMBARK_TICKS (4 * TICKS_PER_SECOND) + +/* Where the player comes back: their position at the last checkpoint, or, +when that was on another structure BSP (everyone died before the next one), +where they last stood on the ground on this one. NULL if the player has +neither (they joined since); they come back beside a teammate. */ +static real_point3d const *players_checkpoint_position( + long player_index) +{ + short index = DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index); + + if (players_checkpoint.valid && players_checkpoint.structure_bsp_index == global_structure_bsp_index_get()) + return players_checkpoint.has_position[index] ? &players_checkpoint.positions[index] : NULL; + if (players_coop_state.ground_structure_bsp_index == global_structure_bsp_index_get() && + players_coop_state.has_ground_position[index]) + { + return &players_coop_state.ground_positions[index]; + } + + return NULL; +} + +/* TRUE if a biped stands on something (not airborne), in the BSP or not */ +static boolean players_coop_unit_grounded_anywhere( + long unit_index) +{ + struct biped_datum *biped = biped_try_and_get(unit_index); + + return biped && !TEST_FLAG(biped->biped.flags, _biped_airborne_bit); +} + +/* TRUE if the unit is outside the loaded structure BSP, where it would fall +forever: on foot, or in a vehicle that is empty or player-driven. AI-driven +vehicles are ignored because the intro Pelicans fly outside the BSP. */ +static boolean players_coop_unit_stranded( + long unit_index) +{ + long vehicle_index = object_get_ultimate_parent(unit_index); + long driver_index; + + if (players_coop_unit_in_structure(unit_index)) + return FALSE; + /* (on foot: only while falling, not standing on something the BSP + doesn't hold, such as an elevator in its shaft) */ + if (vehicle_index == unit_index) + return !players_coop_unit_grounded_anywhere(unit_index); + driver_index = object_try_and_get_and_verify_type(vehicle_index, _object_mask_unit) ? + unit_get(vehicle_index)->unit.driver_object_index : NONE; + return driver_index == NONE || unit_get(driver_index)->unit.player_index != NONE; +} + +/* How long a player can be falling outside the BSP before being brought +back. A BSP switch settles in a tick or two; waiting longer avoids moving +someone who is only crossing a seam or riding over a gap. */ +#define COOP_STRANDED_TICKS (2 * TICKS_PER_SECOND) + +/* After a BSP switch, how long the co-op host lets no other trigger switch +it while the players are brought into the new one, and the longest it then +waits for every machine to have loaded it */ +#define COOP_BSP_SWITCH_SETTLE_TICKS (2 * TICKS_PER_SECOND) +#define COOP_BSP_SWITCH_WAIT_TICKS (10 * TICKS_PER_SECOND) + +/* Going back to a BSP the team has already been in needs two thirds of the +living players at the trigger: inside it, or this near the player in it +(world units) */ +#define COOP_BACKTRACK_GATHER_DISTANCE 4.0f + +/* Co-op host, each tick: a player stranded for COOP_STRANDED_TICKS is moved +beside a grounded teammate inside the BSP, else any teammate inside it, else +to the last checkpoint. Skipped while scripts hold the controls, since +cutscenes place the players themselves. A BSP switch already moves everyone +(players_reconnect_to_structure_bsp); this catches anyone it missed. */ +static void players_coop_rescue_stranded( + void) +{ + struct data_iterator iterator; + struct player_datum *player; + + if (!network_coop_active() || game_connection() != _game_connection_network_server) + return; + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + long *since = &players_coop_state.stranded_since[DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index)]; + long teammate_index; + boolean rescued; + + if (player->unit_index == NONE || !player_input_enabled() || !players_coop_unit_stranded(player->unit_index)) + { + *since = 0; + continue; + } + if (*since == 0) + *since = game_time_get() + 1; + if (game_time_get() + 1 - *since < COOP_STRANDED_TICKS) + continue; + teammate_index = players_coop_unit_where(players_coop_unit_grounded); + if (teammate_index == NONE) + teammate_index = players_coop_unit_where(players_coop_unit_in_structure); + if (teammate_index != NONE) + { + rescued = player_teleport(iterator.datum_index, teammate_index, + &object_get(teammate_index)->object.bounding_sphere_center); + } + else + { + real_point3d const *position = players_checkpoint_position(iterator.datum_index); + + rescued = position && player_teleport(iterator.datum_index, NONE, position); + } + if (rescued) + *since = 0; + } +} + +/* co-op, each tick: records when each player became somewhere spawnable, +and where each last stood on the ground */ +static void players_coop_note_on_foot( + void) +{ + struct data_iterator iterator; + struct player_datum *player; + + if (!network_coop_active()) + return; + if (players_coop_state.ground_structure_bsp_index != global_structure_bsp_index_get()) + { + players_coop_state.ground_structure_bsp_index = global_structure_bsp_index_get(); + csmemset(players_coop_state.has_ground_position, 0, sizeof(players_coop_state.has_ground_position)); + } + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + short index = DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index); + long *since = &players_coop_state.spawnable_since[index]; + + if (player_spawnable_beside(player) == NONE) + *since = 0; + else if (*since == 0) + *since = game_time_get() + 1; + if (player->unit_index != NONE && object_get_ultimate_parent(player->unit_index) == player->unit_index && + players_coop_unit_grounded(player->unit_index)) + { + players_coop_state.ground_positions[index] = object_get(player->unit_index)->object.bounding_sphere_center; + players_coop_state.has_ground_position[index] = TRUE; + } + } +} + +/* Co-op host: TRUE if a new player has somewhere to spawn, i.e. a teammate +who has been spawnable for COOP_DISEMBARK_TICKS. While every teammate rides +an AI vehicle (Silent Cartographer's Pelican) or is held by the scripts +(Pillar of Autumn's cryo tube), the others spectate. */ +static boolean players_coop_room_to_spawn( + void) +{ + struct data_iterator iterator; + struct player_datum *player; + + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + long since = players_coop_state.spawnable_since[DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index)]; + + if (player_spawnable_beside(player) != NONE && since != 0 && game_time_get() + 1 - since >= COOP_DISEMBARK_TICKS) + return TRUE; + } + + return FALSE; +} + +/* Co-op host: moves a newly spawned player beside a teammate who is on foot +or in a player-driven vehicle (player_spawnable_beside). player_teleport tries +a few spots around each. Teammates are tried in turn so a big lobby spreads +out. FALSE if nobody has room; the player stays where they spawned. */ +static boolean player_place_beside_teammate( + long player_index) +{ + struct data_iterator iterator; + struct player_datum *other; + + if (player_get(player_index)->unit_index == NONE) + return FALSE; + data_iterator_new(&iterator, player_data); + while ((other = data_iterator_next(&iterator)) != NULL) + { + if (iterator.datum_index != player_index && player_spawnable_beside(other) != NONE && + player_teleport(player_index, other->unit_index, &object_get(other->unit_index)->object.bounding_sphere_center)) + { + return TRUE; + } + } + + return FALSE; +} + +void players_note_checkpoint( + void) +{ + struct data_iterator iterator; + struct player_datum *player; + + csmemset(&players_checkpoint, 0, sizeof(players_checkpoint)); + players_checkpoint.structure_bsp_index = global_structure_bsp_index_get(); + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + short index = DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index); + + if (player->unit_index != NONE) + { + players_checkpoint.positions[index] = object_get(player->unit_index)->object.bounding_sphere_center; + players_checkpoint.has_position[index] = TRUE; + players_checkpoint.valid = TRUE; + } + } + + return; +} + +/* Everyone back after all died: each where they were at the last checkpoint +(players_checkpoint_position), then those with no such spot beside the +others. Players who come back are spawned first, at the level's start. */ +void players_respawn_at_checkpoint( + void) +{ + struct data_iterator iterator; + struct player_datum *player; + boolean unplaced[NETWORK_GAME_MAXIMUM_PLAYER_COUNT] = { FALSE }; + + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + real_point3d const *position; + + if (player->unit_index != NONE || player->quit_out_of_game) + continue; + player_spawn(iterator.datum_index); + position = players_checkpoint_position(iterator.datum_index); + unplaced[DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index)] = player->unit_index != NONE && + !(position && player_teleport(iterator.datum_index, NONE, position)); + } + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + if (unplaced[DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index)]) + player_place_beside_teammate(iterator.datum_index); + } + + return; +} + +/* port: whether the unit stands in a trigger that switches away from the +loaded structure BSP */ +static boolean players_coop_in_bsp_switch_trigger( + long unit_index) +{ + struct scenario *scenario = global_scenario_get(); + short index; + + for (index = 0; index < scenario->bsp_switch_trigger_volumes.count; index++) + { + struct scenario_bsp_switch_trigger_volume *volume = TAG_BLOCK_GET_ELEMENT( + &scenario->bsp_switch_trigger_volumes, index, struct scenario_bsp_switch_trigger_volume); + + if (volume->source_structure_bsp_index == global_structure_bsp_index && + scenario_trigger_volume_test_object(volume->trigger_volume_index, unit_index)) + { + return TRUE; + } + } + + return FALSE; +} + +/* port: whether a network co-op host lets any trigger switch the BSP yet. +After a switch it waits COOP_BSP_SWITCH_SETTLE_TICKS while the players are +brought in, then until every player's machine has loaded the new BSP, so no +client falls two switches behind. A client stuck longer than +COOP_BSP_SWITCH_WAIT_TICKS no longer holds the team. */ +static boolean players_coop_bsp_switch_ready( + void) +{ + long since = game_time_get() - players_coop_state.bsp_switch_time; + struct data_iterator iterator; + struct player_datum *player; + + if (since < COOP_BSP_SWITCH_SETTLE_TICKS) + return FALSE; + if (since >= COOP_BSP_SWITCH_WAIT_TICKS) + return TRUE; + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + if (player->unit_index != NONE && !network_coop_player_has_structure_bsp(iterator.datum_index)) + return FALSE; + } + + return TRUE; +} + +/* port: whether a network co-op host lets the unit on this trigger switch +the BSP, which brings the whole team along. Not while the unit's machine +is still loading the BSP the host has. Anyone can lead the team into a BSP +it hasn't been in; going back to one needs two thirds of the team there, +so one player can't drag everyone back through the level. */ +static boolean players_coop_bsp_switch_allowed( + short bsp_switch_trigger_volume_index, + long unit_index) +{ + struct scenario_bsp_switch_trigger_volume *volume = TAG_BLOCK_GET_ELEMENT( + &global_scenario_get()->bsp_switch_trigger_volumes, bsp_switch_trigger_volume_index, + struct scenario_bsp_switch_trigger_volume); + real_point3d const *center = &object_get(unit_index)->object.bounding_sphere_center; + struct data_iterator iterator; + struct player_datum *player; + short living_count = 0; + short gathered_count = 0; + + if (!network_coop_player_has_structure_bsp(unit_get(unit_index)->unit.player_index)) + return FALSE; + if (!VALID_INDEX(volume->destination_structure_bsp_index, MAXIMUM_STRUCTURE_BSPS_PER_SCENARIO) || + !TEST_FLAG(players_coop_state.visited_structure_bsps, volume->destination_structure_bsp_index)) + { + return TRUE; + } + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + struct object_datum *teammate = object_try_and_get(player->unit_index); + + if (!teammate || TEST_FLAG(teammate->object.damage_flags, _object_dead_bit)) + continue; + living_count++; + if (scenario_trigger_volume_test_object(volume->trigger_volume_index, player->unit_index) || + distance_squared3d(center, &teammate->object.bounding_sphere_center) <= + COOP_BACKTRACK_GATHER_DISTANCE * COOP_BACKTRACK_GATHER_DISTANCE) + { + gathered_count++; + } + } + + return gathered_count * 3 >= living_count * 2; +} + static void player_teleport_on_bsp_switch( long player_index, long source_unit_index, @@ -2422,7 +3084,16 @@ static void player_teleport_on_bsp_switch( position); if (biped) { - if (players_globals->pending_teleport_starting_location_index != NONE && + /* port: in network co-op a loading zone brings the whole team to + whoever crossed it, as split screen does. Only one BSP is ever + loaded, so anyone left elsewhere would stand outside it, or on a + trigger that switches it straight back (players_update_after_game + waits for players_coop_bsp_switch_ready before another switch). */ + if (network_coop_active()) + { + outside_switch_trigger = TRUE; + } + else if (players_globals->pending_teleport_starting_location_index != NONE && !scenario_trigger_volume_test_object( TAG_BLOCK_GET_ELEMENT( &global_scenario_get()->bsp_switch_trigger_volumes, @@ -2457,6 +3128,10 @@ static void player_teleport_on_bsp_switch( player_index, source_unit_index, position); + /* port: a big co-op team runs out of room around the + crosser, so the rest go beside whoever is already in */ + if (!teleport_succeeded && network_coop_active()) + teleport_succeeded = player_place_beside_teammate(player_index); } else { @@ -2481,13 +3156,13 @@ void players_reconnect_to_structure_bsp( long source_unit_index; boolean teleport_position_valid; boolean found_player; - short local_player_index; short cutscene_flag_index; - long player_index; long player_unit_index; + /* port: also on a network co-op host, whose other players are remote */ if (players_globals->pending_teleport_starting_location_index != NONE && - players_globals->local_player_count > 1) + (players_globals->local_player_count > 1 || + (network_coop_active() && game_connection() == _game_connection_network_server))) { scenario = global_scenario_get(); bsp_switch = TAG_BLOCK_GET_ELEMENT( @@ -2567,28 +3242,46 @@ void players_reconnect_to_structure_bsp( } } + /* port: in network co-op, if whoever crossed isn't in the new BSP yet, + the others gather to a player who is and stands on no switch trigger, + so nobody is left to switch it straight back */ + if (!found_player && network_coop_active()) + { + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL && !found_player) + { + struct object_datum *unit = object_try_and_get(player->unit_index); + + if (unit && scenario_leaf_index_from_point(&unit->object.bounding_sphere_center) != NONE && + !players_coop_in_bsp_switch_trigger(player->unit_index)) + { + source_unit_index = player->unit_index; + teleport_position = unit->object.position; + found_player = TRUE; + } + } + } + match_vassert( "c:\\halo\\SOURCE\\game\\players.c", 0x63E, found_player, "no players in the bsp"); + /* port: all players, not just local ones (the same set in split screen) */ if (found_player) { - local_player_index = local_player_get_next(NONE); - while (local_player_index != NONE) + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) { - player_index = local_player_get_player_index(local_player_index); - player = player_get(player_index); if (player->unit_index != NONE && player->unit_index != source_unit_index) { player_teleport_on_bsp_switch( - player_index, + iterator.datum_index, source_unit_index, &teleport_position); - player_get(player_index)->cluster_index = NONE; + player->cluster_index = NONE; } - local_player_index = local_player_get_next(local_player_index); } } players_globals->pending_teleport_starting_location_index = NONE; @@ -3632,6 +4325,8 @@ void players_update_before_game( short action_index; profile_enter(PLAYERS_UPDATE_BEFORE_GAME_PROFILE); + players_coop_note_on_foot(); + players_coop_rescue_stranded(); if (update_client_dequeue(actions)) { data_iterator_new(&iterator, player_data); @@ -3702,10 +4397,24 @@ void players_update_before_game( else if (network_game_distributed_client()) game_engine_client_respawn_countdown(iterator.datum_index); } - else if (!main_menu_is_active()) + /* port: in co-op only the host spawns players; clients get their units + from the network. One who quit doesn't come back. */ + else if (!main_menu_is_active() && !network_game_distributed_client() && !player->quit_out_of_game) { if (player->statistics.deaths == 0) - player_spawn(iterator.datum_index); + { + /* port: in network co-op the first player spawns at the map's + start; everyone else, late joiners included, spawns beside a + player already in the game once nothing holds them back + (players_coop_may_spawn) */ + if (game_connection() != _game_connection_network_server) + player_spawn(iterator.datum_index); + else if (players_coop_may_spawn()) + { + player_spawn(iterator.datum_index); + player_place_beside_teammate(iterator.datum_index); + } + } else if (!players_globals->all_dead) main_respawn(players_globals->respawn_failed); } @@ -3917,6 +4626,13 @@ void players_update_after_game( if (players_globals->double_speed_ticks == 0) game_set_players_are_double_speed(FALSE); } + if (VALID_INDEX(global_structure_bsp_index, MAXIMUM_STRUCTURE_BSPS_PER_SCENARIO)) + SET_FLAG(players_coop_state.visited_structure_bsps, global_structure_bsp_index, TRUE); + if (players_coop_state.structure_bsp_index != global_structure_bsp_index) + { + players_coop_state.structure_bsp_index = global_structure_bsp_index; + players_coop_state.bsp_switch_time = game_time_get(); + } data_iterator_new(&iterator, player_data); while (player = data_iterator_next(&iterator)) @@ -3963,7 +4679,11 @@ void players_update_after_game( { root_object_index = object_get_ultimate_parent(player->unit_index); root_object = object_get(root_object_index); - if (!TEST_FLAG(root_object->object.flags, _object_outside_of_map_bit)) + /* port: a co-op client only switches BSP when the host does + (network_distributed.c), and the host not again until everyone + has the new one */ + if (!TEST_FLAG(root_object->object.flags, _object_outside_of_map_bit) && + !(network_coop_active() && (network_game_distributed_client() || !players_coop_bsp_switch_ready()))) { scenario = global_scenario_get(); for (bsp_switch_trigger_volume_index = 0; @@ -3978,7 +4698,9 @@ void players_update_after_game( global_structure_bsp_index && scenario_trigger_volume_test_object( bsp_switch_trigger_volume->trigger_volume_index, - player->unit_index)) + player->unit_index) && + (!network_coop_active() || + players_coop_bsp_switch_allowed(bsp_switch_trigger_volume_index, player->unit_index))) { if (players_globals->local_player_triggered_switch != _local_player_triggered_switch_none && @@ -3997,6 +4719,7 @@ void players_update_after_game( bsp_switch_trigger_volume_index; main_switch_structure_bsp( bsp_switch_trigger_volume->destination_structure_bsp_index); + players_coop_state.bsp_switch_time = game_time_get(); } } } @@ -4020,7 +4743,7 @@ void players_update_after_game( if (players_globals->all_dead) { - if (!game_engine_running() && !players_lost_map_started) + if (!game_engine_running() && !players_lost_map_started && !network_game_distributed_client()) { main_lost_map(); players_lost_map_started = TRUE; diff --git a/source/game/players.h b/source/game/players.h index 11de4202..d77e29e1 100644 --- a/source/game/players.h +++ b/source/game/players.h @@ -295,6 +295,11 @@ void player_control_new_unit( long unit_index); void player_control_action_test_reset( void); +void player_control_action_test_note( + unsigned long unit_control_flags, + real_euler_angles2d const *facing_delta, + real_vector2d const *throttle, + real primary_trigger); boolean player_control_action_test_accept( void); boolean player_control_action_test_back( @@ -373,6 +378,17 @@ short players_get_respawn_failure( boolean players_respawn_coop( void); +/* port: network co-op checkpoints (players.c): remember where the players +were at the last checkpoint, and respawn everyone there. Players other than +the first are held back from spawning on a new level: until its opening +cutscene ends and a teammate has been free on foot a few seconds (not +riding a vehicle, nor held by the scripts). */ +boolean players_coop_waiting_to_start( + long player_index); +void players_note_checkpoint( + void); +void players_respawn_at_checkpoint( + void); void players_reconnect_to_structure_bsp( void); diff --git a/source/hs/hs.c b/source/hs/hs.c index fb6bdd1c..baa0b122 100644 --- a/source/hs/hs.c +++ b/source/hs/hs.c @@ -14088,6 +14088,29 @@ static boolean hs_expression_changes_no_game( static boolean hs_compile_and_evaluate_command( char const *expression); +/* port: the text after the host's command `word` ("ban", "kick") that an +expression starts with (spaces and an opening parenthesis before it, in +either case, then a space or the end), else NULL */ +static char const *hs_host_player_command( + char const *expression, + char const *word) +{ + char const *text = expression; + long index; + + while (*text == ' ' || *text == '\t' || *text == '(') + text++; + for (index = 0; word[index]; index++) + { + char character = text[index] >= 'A' && text[index] <= 'Z' ? text[index] - 'A' + 'a' : text[index]; + + if (character != word[index]) + return NULL; + } + text += index; + return *text == ' ' || *text == '\t' || *text == 0 ? text : NULL; +} + /* port: a command someone typed (the console, the telnet console, a cheat button, init.txt): what it logs is its answer, shown whatever config.toml's game.console_log is (terminal_command_running) */ @@ -14122,20 +14145,20 @@ static boolean hs_compile_and_evaluate_command( console_warning("not while playing in another's game: the host decides the game"); return FALSE; } - /* port: the host's ban command ("ban ", or its start: Tab - completes it), which is no script's */ + /* port: the host's ban and kick commands ("ban ", "kick + ", or the name's start: Tab completes it), which are no + script's */ { - char const *text = expression; + char const *text = hs_host_player_command(expression, "ban"); + boolean kick = FALSE; - while (*text == ' ' || *text == '\t' || *text == '(') - text++; - if ((text[0] == 'b' || text[0] == 'B') && (text[1] == 'a' || text[1] == 'A') && - (text[2] == 'n' || text[2] == 'N') && (text[3] == ' ' || text[3] == '\t' || text[3] == 0)) + if (!text && (text = hs_host_player_command(expression, "kick")) != NULL) + kick = TRUE; + if (text) { char name[64]; long length = 0; - text += 3; while (*text == ' ' || *text == '\t' || *text == '"') text++; while (*text && *text != '"' && *text != ')' && length < (long)sizeof(name) - 1) @@ -14143,7 +14166,7 @@ static boolean hs_compile_and_evaluate_command( while (length > 0 && (name[length - 1] == ' ' || name[length - 1] == '\t')) length--; name[length] = 0; - return network_game_server_ban_player(name); + return kick ? network_game_server_kick_player(name) : network_game_server_ban_player(name); } } csstrncpy(buffer, expression, sizeof(buffer)); diff --git a/source/hs/hs.h b/source/hs/hs.h index 2ec84cc9..98c7f317 100644 --- a/source/hs/hs.h +++ b/source/hs/hs.h @@ -179,6 +179,9 @@ void hs_runtime_initialize( void); void hs_runtime_initialize_for_new_map( void); +/* port: network co-op (port/linux/game/network_coop.c) */ +void hs_runtime_port_shift_sleep_times( + long ticks); void hs_runtime_update( void); long hs_runtime_evaluate( diff --git a/source/hs/hs_library_external.c b/source/hs/hs_library_external.c index bee90038..06aee84d 100644 --- a/source/hs/hs_library_external.c +++ b/source/hs/hs_library_external.c @@ -104,6 +104,7 @@ symbols in this file: #include "sound/sound_definitions.h" #include "units/units.h" #include "object_lists.h" +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ /* ---------- constants */ @@ -674,6 +675,8 @@ void hs_effect_new( struct scenario_cutscene_flag *cutscene_flag; real_vector3d forward; + /* port: and on network co-op's clients (port/linux/game/network_coop.c) */ + network_coop_note_effect(effect_definition_index, cutscene_flag_index); cutscene_flag = TAG_BLOCK_GET_ELEMENT( &global_scenario_get()->cutscene_flags, cutscene_flag_index, @@ -707,6 +710,9 @@ void hs_effect_new_from_object_marker( { struct object_marker marker; + /* port: and on network co-op's clients (port/linux/game/network_coop.c) */ + network_coop_note_object_effect(effect_definition_index, object_index, marker_name); + if (object_get_marker_by_name( object_index, marker_name, @@ -998,6 +1004,9 @@ void hs_teleport_players_not_in_trigger_volume( short cutscene_flag_index) { long player_index; + /* port: in network co-op the first player moved goes to the flag and + the rest around them, instead of all into the same spot */ + long first_unit_index = NONE; for (player_index = data_next_index(player_data, NONE); player_index != NONE; @@ -1011,11 +1020,17 @@ void hs_teleport_players_not_in_trigger_volume( trigger_volume_index, player->unit_index)) { + if (first_unit_index != NONE && network_coop_active()) + { + player_teleport(player_index, first_unit_index, &object_get(first_unit_index)->object.position); + continue; + } hs_object_orient( player->unit_index, cutscene_flag_index, TRUE, TRUE); + first_unit_index = player->unit_index; } } diff --git a/source/hs/hs_runtime.c b/source/hs/hs_runtime.c index 51e5b910..dcb98ee1 100644 --- a/source/hs/hs_runtime.c +++ b/source/hs/hs_runtime.c @@ -1120,6 +1120,24 @@ void render_debug_trigger_volumes( return; } +/* port: moves every sleeping script thread's wake time on by `ticks`, after +a network co-op host reverts but keeps its clock (network_coop.c) */ +void hs_runtime_port_shift_sleep_times( + long ticks) +{ + struct data_iterator iterator; + struct hs_thread_datum *thread; + + data_iterator_new(&iterator, hs_thread_data); + while ((thread = data_iterator_next(&iterator)) != NULL) + { + if (thread->sleep_until > 0) + thread->sleep_until += ticks; + if (thread->previous_sleep_until > 0) + thread->previous_sleep_until += ticks; + } +} + void hs_runtime_update( void) { diff --git a/source/interface/hud.c b/source/interface/hud.c index ece29b5e..536d1b10 100644 --- a/source/interface/hud.c +++ b/source/interface/hud.c @@ -107,6 +107,8 @@ symbols in this file: #include "sound/game_sound.h" #include "tag_files/tag_files.h" #include "text/draw_string.h" +#include "coop_spectate.h" /* port: port/linux/game/coop_spectate.c */ +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ #include "text/font_group.h" #include "text/text_group.h" #include "units/unit_definitions.h" @@ -871,6 +873,7 @@ void hud_autosave( : hud_globals->checkpoint_end_index; short local_player_index; + network_coop_note_hud(_coop_hud_checkpoint, active); scripted_hud_messages_clear(); if (active && hud_globals->checkpoint_sound.index != NONE) { @@ -999,7 +1002,7 @@ static void hud_draw_players( return; } -/* port: in multiplayer, players' names above their heads +/* port: in multiplayer and network co-op, 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 within the motion sensor's reach, while the view sees them and they are not @@ -1272,7 +1275,8 @@ static void hud_draw_player_names_now( 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(); + /* the campaign always draws teammate triangles (hud_draw_players) */ + indicators = game_engine_display_team_indicators() || !game_engine_running(); 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); @@ -1443,9 +1447,15 @@ void hud_draw_screen( hud_draw_players(); } - /* port: players' names above their heads, in multiplayer */ - if (game_engine_running() && !cinematic_in_progress()) + /* port: players' names above their heads, in multiplayer and network co-op */ + if ((game_engine_running() || network_coop_active()) && !cinematic_in_progress()) hud_draw_player_names(); + /* port: who a dead network co-op player is watching */ + if (player->unit_index == NONE && coop_spectating() && !cinematic_in_progress()) + coop_spectate_draw(render.local_player_index); + /* port: the network co-op vote to skip a cinematic */ + if (cinematic_in_progress()) + coop_skip_vote_draw(render.local_player_index); if (!game_time_get_paused() && render.local_player_index == local_player_get_next(NONE)) diff --git a/source/interface/hud_messaging.c b/source/interface/hud_messaging.c index 1413e765..7ce25c99 100644 --- a/source/interface/hud_messaging.c +++ b/source/interface/hud_messaging.c @@ -145,6 +145,7 @@ symbols in this file: #include "text/font_group.h" #include "text/text_group.h" #include "text/unicode.h" +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ #include @@ -606,6 +607,7 @@ void scripted_hud_set_state_message( { struct scenario *scenario = global_scenario_get(); + network_coop_note_hud(_coop_hud_help_text, message_index); if (hud_scripted_globals->show_hud_help_text && scenario->hud_messages.index != NONE) { @@ -626,6 +628,7 @@ void scripted_hud_set_flashing_state( { long time; + network_coop_note_hud(_coop_hud_help_flash, flash); if (flash && !hud_messaging_globals->use_flash) { time = game_time_get(); @@ -656,6 +659,7 @@ void scripted_hud_set_objective( { struct scenario *scenario = global_scenario_get(); + network_coop_note_hud(_coop_hud_objective, message_index); if (scenario->hud_messages.index != NONE) { struct hud_message_text_definition *hud_messages = @@ -737,6 +741,47 @@ void scripted_hud_show_timer( return; } +/* port: the script timer and the scenario's message count, for network +co-op (port/linux/game/network_coop.c) */ +void hud_messaging_port_timer_get( + struct hud_timer_state *state) +{ + struct hud_timer_data_definition const *timer = &hud_messaging_globals->timer; + + state->reference_time = timer->reference_time; + state->ticks = timer->ticks; + state->flash_cutoff = timer->flash_cutoff; + state->x = timer->position.n[0]; + state->y = timer->position.n[1]; + state->corner = timer->corner; + state->paused = timer->paused; + state->enabled = timer->enabled; +} + +void hud_messaging_port_timer_set( + struct hud_timer_state const *state) +{ + struct hud_timer_data_definition *timer = &hud_messaging_globals->timer; + + timer->reference_time = state->reference_time; + timer->ticks = state->ticks; + timer->flash_cutoff = state->flash_cutoff; + timer->position.n[0] = state->x; + timer->position.n[1] = state->y; + timer->corner = PIN(state->corner, 0, 4); + timer->paused = state->paused; + timer->enabled = state->enabled; +} + +short hud_messaging_port_message_count( + void) +{ + struct scenario *scenario = global_scenario_get(); + + return scenario->hud_messages.index != NONE ? + (short)HUD_MESSAGE_TEXT_DEFINITION_GET(scenario->hud_messages.index)->messages.count : 0; +} + void scripted_hud_pause_timer( boolean paused) { @@ -1129,6 +1174,7 @@ void scripted_hud_messages_clear( struct hud_messaging_datum_definition *datum = hud_messaging_globals->message_data; long datum_count = NUMBER_OF_HUD_MESSAGING_DATUMS; + network_coop_note_hud(_coop_hud_messages_clear, 0); do { struct hud_message_definition *message = datum->messages; diff --git a/source/interface/hud_messaging.h b/source/interface/hud_messaging.h index 76cb0e90..5e16c5a4 100644 --- a/source/interface/hud_messaging.h +++ b/source/interface/hud_messaging.h @@ -27,6 +27,24 @@ void hud_messaging_dispose_from_old_map( void); void hud_messaging_dispose( void); +/* port: network co-op (port/linux/game/network_coop.c) */ +struct hud_timer_state +{ + long reference_time; + short ticks; + short flash_cutoff; + short x, y; + short corner; + boolean paused; + boolean enabled; +}; +void hud_messaging_port_timer_get( + struct hud_timer_state *state); +void hud_messaging_port_timer_set( + struct hud_timer_state const *state); +short hud_messaging_port_message_count( + void); + void scripted_hud_set_state_message( short message_index); void hud_messaging_globals_update( diff --git a/source/interface/hud_nav_points.c b/source/interface/hud_nav_points.c index c0718e75..c253d21f 100644 --- a/source/interface/hud_nav_points.c +++ b/source/interface/hud_nav_points.c @@ -123,6 +123,7 @@ symbols in this file: #include "scenario/scenario.h" #include "scenario/scenario_definitions.h" #include "units/units.h" +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ /* ---------- constants */ @@ -496,6 +497,7 @@ void hud_activate_team_nav_point_with_flag( short flag_index, float vertical_offset) { + network_coop_note_nav_point(_coop_nav_point_team_flag, nav_index, team_index, flag_index, vertical_offset); hud_activate_team_nav_point(nav_index, team_index, _hud_nav_point_type_flag, flag_index, vertical_offset); return; @@ -507,6 +509,7 @@ void hud_activate_team_nav_point_with_object( long object_index, float vertical_offset) { + network_coop_note_nav_point(_coop_nav_point_team_object, nav_index, team_index, object_index, vertical_offset); hud_activate_team_nav_point(nav_index, team_index, _hud_nav_point_type_object, object_index, vertical_offset); return; @@ -553,6 +556,7 @@ void hud_deactivate_team_nav_point_with_flag( short team_index, short flag_index) { + network_coop_note_nav_point(_coop_nav_point_team_flag, NONE, team_index, flag_index, 0.0f); hud_deactivate_team_nav_point(team_index, _hud_nav_point_type_flag, flag_index); return; @@ -562,6 +566,7 @@ void hud_deactivate_team_nav_point_with_object( short team_index, long object_index) { + network_coop_note_nav_point(_coop_nav_point_team_object, NONE, team_index, object_index, 0.0f); hud_deactivate_team_nav_point(team_index, _hud_nav_point_type_object, object_index); return; @@ -575,6 +580,7 @@ void hud_unit_activate_nav_point_with_flag( { long player_index = player_index_from_unit_index(unit_index); + network_coop_note_nav_point(_coop_nav_point_unit_flag, nav_index, unit_index, flag_index, vertical_offset); if (player_index!=NONE) { hud_activate_nav_point(nav_index, player_index, _hud_nav_point_type_flag, flag_index, vertical_offset); @@ -591,6 +597,7 @@ void hud_unit_activate_nav_point_with_object( { long player_index = player_index_from_unit_index(unit_index); + network_coop_note_nav_point(_coop_nav_point_unit_object, nav_index, unit_index, object_index, vertical_offset); if (player_index!=NONE) { hud_activate_nav_point(nav_index, player_index, _hud_nav_point_type_object, object_index, vertical_offset); @@ -605,6 +612,7 @@ void hud_unit_deactivate_nav_point_with_flag( { long player_index = player_index_from_unit_index(unit_index); + network_coop_note_nav_point(_coop_nav_point_unit_flag, NONE, unit_index, flag_index, 0.0f); if (player_index!=NONE) { hud_deactivate_nav_point(player_index, _hud_nav_point_type_flag, flag_index); @@ -619,6 +627,7 @@ void hud_unit_deactivate_nav_point_with_object( { long player_index = player_index_from_unit_index(unit_index); + network_coop_note_nav_point(_coop_nav_point_unit_object, NONE, unit_index, object_index, 0.0f); if (player_index!=NONE) { hud_deactivate_nav_point(player_index, _hud_nav_point_type_object, object_index); diff --git a/source/interface/hud_unit.c b/source/interface/hud_unit.c index 5b04897f..c5d4fcf6 100644 --- a/source/interface/hud_unit.c +++ b/source/interface/hud_unit.c @@ -381,6 +381,20 @@ void scripted_hud_blink_motion_sensor( return; } +/* port: the scripts' show and blink flags, for network co-op +(port/linux/game/network_coop.c) */ +long hud_unit_port_script_flags( + void) +{ + return unit_hud_globals->script_flags; +} + +void hud_unit_port_set_script_flags( + long flags) +{ + unit_hud_globals->script_flags = flags; +} + void hud_play_unit_sounds( struct player_datum const *player, boolean show_hud) diff --git a/source/interface/hud_unit.h b/source/interface/hud_unit.h index 30a86371..41598a56 100644 --- a/source/interface/hud_unit.h +++ b/source/interface/hud_unit.h @@ -31,6 +31,11 @@ void hud_tick_shield( long player_index, real amount); +/* port: network co-op (port/linux/game/network_coop.c) */ +long hud_unit_port_script_flags( + void); +void hud_unit_port_set_script_flags( + long flags); void scripted_hud_show_health( boolean show); void scripted_hud_blink_health( diff --git a/source/interface/hud_weapon.c b/source/interface/hud_weapon.c index 1e56f940..b93ae0dd 100644 --- a/source/interface/hud_weapon.c +++ b/source/interface/hud_weapon.c @@ -483,6 +483,20 @@ void scripted_hud_show_crosshair( return; } +/* port: the scripts' crosshair flag, for network co-op +(port/linux/game/network_coop.c) */ +long hud_weapon_port_script_flags( + void) +{ + return weapon_hud_globals->script_flags; +} + +void hud_weapon_port_set_script_flags( + long flags) +{ + weapon_hud_globals->script_flags = flags; +} + static struct weapon_hud_state *get_hud_state( short local_player_index) { diff --git a/source/interface/hud_weapon.h b/source/interface/hud_weapon.h index e769e8f5..121dedb2 100644 --- a/source/interface/hud_weapon.h +++ b/source/interface/hud_weapon.h @@ -14,6 +14,11 @@ void hud_dispose_weapon_interface_from_old_map( void hud_dispose_weapon_interface( void); +/* port: network co-op (port/linux/game/network_coop.c) */ +long hud_weapon_port_script_flags( + void); +void hud_weapon_port_set_script_flags( + long flags); void scripted_hud_show_crosshair( boolean show); diff --git a/source/interface/interface.c b/source/interface/interface.c index ed9c9b69..353c05ca 100644 --- a/source/interface/interface.c +++ b/source/interface/interface.c @@ -1227,6 +1227,9 @@ static void interface_splitscreen_render( { rectangle2d bounds; short window_count; + /* port: the screen's width, wider than the Xbox's 640 on a wide screen, + whose middle the windows split at (compute_window_bounds) */ + short width = (short)halo_screen_width(); if (game_engine_force_single_screen() || cinematic_in_progress()) return; @@ -1239,7 +1242,7 @@ static void interface_splitscreen_render( bounds.y0 = 239; bounds.x0 = 0; bounds.y1 = 241; - bounds.x1 = 640; + bounds.x1 = width; draw_quad(&bounds, 0xFF000000); if (window_count <= 2) @@ -1248,18 +1251,18 @@ static void interface_splitscreen_render( if (window_count == 3) { bounds.y0 = 240; - bounds.x0 = 319; + bounds.x0 = width / 2 - 1; bounds.y1 = 480; - bounds.x1 = 321; + bounds.x1 = width / 2 + 1; draw_quad(&bounds, 0xFF000000); return; } bounds.y0 = 0; - bounds.x0 = 319; + bounds.x0 = width / 2 - 1; bounds.y1 = 480; - bounds.x1 = 321; + bounds.x1 = width / 2 + 1; match_assert( "c:\\halo\\SOURCE\\interface\\interface.c", diff --git a/source/interface/player_ui.c b/source/interface/player_ui.c index 201f413d..9c92e7e5 100644 --- a/source/interface/player_ui.c +++ b/source/interface/player_ui.c @@ -514,6 +514,19 @@ void player_ui_local_player_joined_multiplayer_game( return; } +/* port: the local player out of this game and the next (a split screen +player who quit: player_ui_autojoin_players_to_next_multiplayer_game would +join them again) */ +void player_ui_local_player_left_multiplayer_game( + short local_player_index) +{ + if (local_player_index < 0 || local_player_index >= MAXIMUM_NUMBER_OF_LOCAL_PLAYERS) + return; + player_ui_globals.local_players[local_player_index].prejoined_multiplayer = FALSE; + player_ui_globals.multiplayer_autojoin[local_player_index] = FALSE; + return; +} + boolean player_ui_rumble_disabled( short local_player_index) { diff --git a/source/interface/player_ui.h b/source/interface/player_ui.h index d38c900c..59a51f6c 100644 --- a/source/interface/player_ui.h +++ b/source/interface/player_ui.h @@ -72,6 +72,9 @@ short player_ui_get_last_single_player_level_played( short local_player_index); void player_ui_local_player_joined_multiplayer_game( short local_player_index); +/* port: the local player out of this game and the next */ +void player_ui_local_player_left_multiplayer_game( + short local_player_index); boolean player_ui_rumble_disabled( short local_player_index); boolean player_ui_get_path_to_local_player_profile_directory( diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index c580502a..3fd52f23 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -662,6 +662,7 @@ struct widget_instance; #include "networking/network_connection.h" #include "networking/network_game_globals.h" #include "networking/network_server_manager.h" +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ #include "rasterizer/rasterizer.h" #include "saved games/player_profile.h" #include "saved games/playlist_profile.h" @@ -3700,7 +3701,10 @@ static void widget_instance_initialize( widget->visible = TRUE; widget->render_regardless_of_controller_index = TEST_FLAG(definition->flags, _widget_render_regardless_of_controller_index_bit); - widget->pause_game_time = TEST_FLAG(definition->flags, _widget_pause_game_time_bit); + /* port: a network co-op game never pauses (it opens the campaign's pause + screen, which would) */ + widget->pause_game_time = TEST_FLAG(definition->flags, _widget_pause_game_time_bit) && + !network_coop_active(); widget->creation_time = widget_globals.current_system_milliseconds; widget->milliseconds_to_auto_close = MAX(definition->milliseconds_to_auto_close, 0); widget->auto_close_fade_time = MAX(definition->auto_close_fade_time, 0); @@ -7889,6 +7893,33 @@ static void widget_instance_tab_to_previous_valid_widget( return; } +/* port: whether a widget of the local player (NONE: any) takes the +controller's events. In co-op's menus (Multiplayer's CO-OP CAMPAIGN, +port/linux/game/menu_functions.c) the screens it shares with one player's +campaign, New Game's levels and the difficulty, are player 1's (their rows +the first controller's), and either player's controller uses them: player 1's +is the one that chose co-op, player 2's the one that chose their profile */ +static boolean widget_takes_events_of_controller( + struct widget_instance const *widget, + short controller_index) +{ + short player; + + if (widget->local_player_index == NONE || widget->local_player_index == controller_index) + return TRUE; + if (widget->local_player_index != 0 || !we_are_at_the_main_menu || player_spawn_count < 2 || + controller_index < 0 || controller_index >= MAXIMUM_NUMBER_OF_LOCAL_PLAYERS) + { + return FALSE; + } + for (player = 0; player < 2; player++) + { + if (player_ui_get_single_player_local_player_controller(player) == controller_index) + return TRUE; + } + return FALSE; +} + static void widget_instance_process_one_event_recursive( struct widget_instance *widget, struct ui_widget_definition *definition, @@ -7897,8 +7928,7 @@ static void widget_instance_process_one_event_recursive( { boolean event_handled = FALSE; boolean widget_deleted = FALSE; - boolean event_for_this_widget = widget->local_player_index == NONE || - widget->local_player_index == event->controller_index; + boolean event_for_this_widget = widget_takes_events_of_controller(widget, event->controller_index); long audio_feedback = _ui_audio_feedback_none; match_assert( @@ -8325,8 +8355,7 @@ static void widget_instance_process_one_event_recursive( for (child = widget->child; child; child = child->next) { - if (child->local_player_index == NONE || - child->local_player_index == event->controller_index) + if (widget_takes_events_of_controller(child, event->controller_index)) { widget_instance_process_one_event_recursive( child, @@ -8340,8 +8369,7 @@ static void widget_instance_process_one_event_recursive( } else if (widget->focused_child) { - if (widget->focused_child->local_player_index == NONE || - widget->focused_child->local_player_index == event->controller_index) + if (widget_takes_events_of_controller(widget->focused_child, event->controller_index)) { widget_instance_process_one_event_recursive( widget->focused_child, @@ -8447,7 +8475,10 @@ static boolean ui_check_for_pause_game( network_game_client_get_machine_index(client); char const *widget_name; - switch (local_player_count) + /* port: a campaign map has only the campaign's pause screen */ + if (network_coop_active()) + widget_name = "ui\\shell\\solo_game\\pause_game\\pause_game"; + else switch (local_player_count) { case 1: widget_name = diff --git a/source/interface/ui_widget_event_handler_functions.c b/source/interface/ui_widget_event_handler_functions.c index 32e90165..96ae7484 100644 --- a/source/interface/ui_widget_event_handler_functions.c +++ b/source/interface/ui_widget_event_handler_functions.c @@ -923,6 +923,7 @@ symbols in this file: #include "networking/network_game_manager.h" #include "networking/network_messages.h" #include "networking/network_server_manager.h" +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ #include "saved games/game_state.h" #include "saved games/player_profile.h" #include "interface/ui_widget_definitions.h" @@ -1991,6 +1992,9 @@ static boolean pause_game_restart_at_checkpoint( struct event_record *event, boolean *widget_deleted) { + /* port: in co-op this would revert only this machine */ + if (network_coop_active()) + return FALSE; main_revert_map(); return TRUE; } @@ -2000,6 +2004,9 @@ static boolean pause_game_restart_level( struct event_record *event, boolean *widget_deleted) { + /* port: in co-op this would restart only this machine */ + if (network_coop_active()) + return FALSE; main_reset_map(); return TRUE; } @@ -2009,7 +2016,21 @@ static boolean pause_game_quit_to_main_menu( struct event_record *event, boolean *widget_deleted) { - game_state_save_to_persistent_storage(); + /* port: in co-op, take every player on this machine out of the network + game with one press (not one per split screen player). The solo save is + left alone. */ + if (network_coop_active()) + { + short controller_index; + + for (controller_index = 0; controller_index < MAXIMUM_NUMBER_OF_LOCAL_PLAYERS; controller_index++) + network_game_client_local_player_quit(controller_index); + return TRUE; + } + /* port: a multiplayer map played alone (New Game's MULTIPLAYER maps) is + not saved, so it never takes the place of the campaign's saved game */ + if (main_get_current_solo_level() != NONE) + game_state_save_to_persistent_storage(); main_goto_main_menu(); return TRUE; } @@ -2411,6 +2432,10 @@ static boolean network_game_remove_local_player( event && event->controller_index >= 0 && event->controller_index < 4, "valid controller index required to remove player from network game"); network_game_client_local_player_quit(event->controller_index); + /* port: a split screen player who quit, the others staying, is not + joined to the next game */ + if (local_player_count() > 1) + player_ui_local_player_left_multiplayer_game(event->controller_index); return TRUE; } @@ -5924,6 +5949,30 @@ short ui_widget_port_gametypes( return (short)count; } +/* port: sets up the server for co-op (port/linux/game/menu_functions.c): +the campaign level, the difficulty, and a gametype with no game engine, +which is what makes a network game co-op (game.c, players.c). Returns FALSE +without a server or a campaign level. */ +boolean ui_widget_port_cooperative_level_choose( + char const *map_name, + short difficulty) +{ + struct network_game_server *server = global_network_game_server_get(); + struct game_variant variant; + + if (!server || !map_name || main_get_solo_level_from_name(map_name) == NONE) + return FALSE; + csmemset(&variant, 0, sizeof(variant)); + ustrncpy(variant.human_readable_game_description, L"Co-op", + NUMBEROF(variant.human_readable_game_description) - 1); + main_set_difficulty(difficulty); + main_set_multiplayer_map_name(map_name); + network_game_server_port_set_cooperative(server, difficulty); + network_game_server_change_map_name(server, map_name); + network_game_server_change_game_variant(server, &variant); + return TRUE; +} + /* the gametype chosen (as multiplayer_profile_set_for_game), the server's if there is one */ boolean ui_widget_port_gametype_choose( @@ -5956,12 +6005,18 @@ boolean ui_widget_port_gametype_choose( return TRUE; } -/* hosting (as the Xbox's server list's Y) */ +/* hosting (as the Xbox's server list's Y): always a new game. A game made +before and backed out of keeps its server (the lobby's last player leaving +pauses it: netgame_unjoin_player), and network_game_start_new_server joins +only a server it makes, so the client it made for that one never joined it +and the lobby had nobody in it */ boolean ui_widget_port_host( struct widget_instance *widget, struct event_record *event, boolean *widget_deleted) { + dispose_global_network_game_client(); + dispose_global_network_game_server(); return network_game_start_new_server(widget, event, widget_deleted); } @@ -6049,6 +6104,22 @@ boolean ui_widget_port_multiplayer_player( return TRUE; } +/* the lobby's B of a player (port/linux/game/menu_functions.c): that +controller's player leaves the game (netgame_unjoin_player); TRUE if they were +the machine's last, which leaves it (and are joined again if its host's +lobby comes back), else they leave the next game too */ +boolean ui_widget_port_unjoin_player( + struct widget_instance *widget, + struct event_record *event, + boolean *widget_deleted) +{ + boolean left = netgame_unjoin_player(widget, event, widget_deleted); + + if (!left && event && event->controller_index >= 0 && event->controller_index < MAXIMUM_NUMBER_OF_LOCAL_PLAYERS) + player_ui_local_player_left_multiplayer_game(event->controller_index); + return left; +} + /* a screen by name in place of the widget's (back returns to it: as ui_widget_port_join opens the lobby) */ boolean ui_widget_port_open( diff --git a/source/main/console.c b/source/main/console.c index 11e64fa5..7bc8c0fb 100644 --- a/source/main/console.c +++ b/source/main/console.c @@ -257,23 +257,38 @@ static char *console_get_text_to_autocomplete( return result; } -/* port: the text after the host's ban command ("ban "), which completes as -a player's name (network_game_server_matching_player_names); NULL if the -input is not it */ -static char *console_ban_command_name( +/* port: the text after the host's ban or kick command ("ban ", "kick "), +which completes as a player's name (network_game_server_matching_player_names); +NULL if the input is not one */ +static char *console_player_command_name( void) { + static char const *const commands[] = { "ban", "kick" }; char *text = console_globals.input_state.result; + short command; while (*text == ' ' || *text == '(') text++; - if ((text[0] == 'b' || text[0] == 'B') && (text[1] == 'a' || text[1] == 'A') && - (text[2] == 'n' || text[2] == 'N') && text[3] == ' ') + for (command = 0; command < (short)NUMBEROF(commands); command++) { - text += 3; - while (*text == ' ' || *text == '"') - text++; - return text; + char const *word = commands[command]; + long length = (long)strlen(word); + long index; + + for (index = 0; index < length; index++) + { + char character = text[index] >= 'A' && text[index] <= 'Z' ? text[index] - 'A' + 'a' : text[index]; + + if (character != word[index]) + break; + } + if (index == length && text[length] == ' ') + { + text += length; + while (*text == ' ' || *text == '"') + text++; + return text; + } } return NULL; } @@ -283,10 +298,10 @@ static void console_complete( { char *matching_items[256]; char print_buffer[1024]; - /* (port: the players' names the ban command completes) */ + /* (port: the players' names the ban and kick commands complete) */ static char player_names[64][NETWORK_GAME_SERVER_NAME_TEXT_SIZE]; - char *token = console_ban_command_name(); + char *token = console_player_command_name(); short count; if (token) diff --git a/source/main/main.c b/source/main/main.c index 65555b4e..75f3fd56 100644 --- a/source/main/main.c +++ b/source/main/main.c @@ -377,6 +377,9 @@ symbols in this file: #include "bink/bink_playback.h" #include "main/d3d_intimacy.h" #include "networking/network_game_globals.h" +#include "networking/network_game_manager.h" +#include "networking/network_server_manager.h" /* port: a co-op game's level won */ +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ #include "camera/director.h" #include "camera/observer.h" #include "cutscene/cinematics.h" @@ -703,6 +706,9 @@ static char const *scenario_paths[10] = }; static struct _main_globals main_globals = { 0 }; +/* Keep the original globals layout; these clocks belong to pending requests. */ +static long main_loss_last_tick; +static long main_respawn_last_tick; boolean debug_force_frame_rate_update = FALSE; boolean debug_no_drawing = FALSE; boolean debug_game_save = FALSE; @@ -906,6 +912,11 @@ void main_won_map( void main_lost_map( void) { + if (!main_globals.lost_map) + { + main_globals.loss_timer = 0; + main_loss_last_tick = game_time_get(); + } main_globals.saving_map = FALSE; main_globals.lost_map = TRUE; return; @@ -966,9 +977,14 @@ void main_save_map_nonsafe( void main_respawn( boolean in_multiplayer) { + if (!main_globals.respawn) + { + main_globals.respawn_timer = 0; + main_respawn_last_tick = game_time_get(); + } main_globals.respawn = TRUE; if (in_multiplayer) - main_globals.respawn_timer = 91; + main_globals.respawn_timer = 92; return; } @@ -1848,12 +1864,23 @@ static void main_revert_map_private( static void main_skip_cinematic_private( void) { - if (cinematic_can_be_skipped()) + /* port: a network host never reverts without a saved state, which + would reset the map on the host alone */ + if (cinematic_can_be_skipped() && + (game_connection() != _game_connection_network_server || game_state_port_saved_game_valid())) { + long now = game_time_get(); + game_state_revert(); + network_coop_skip_reverted(now); ui_widgets_disable_pause_game(30); main_globals.revert_map = FALSE; } + else if (game_connection() == _game_connection_network_server) + { + error(_error_silent, "co-op: cutscene not skipped (skippable %d, saved state %d)", + cinematic_can_be_skipped(), game_state_port_saved_game_valid()); + } main_globals.skip_cinematic = FALSE; return; } @@ -1933,6 +1960,8 @@ static void main_save_map_private( if (save_map) { + /* port: remember where the players are, for network co-op respawns */ + players_note_checkpoint(); hud_autosave(TRUE); main_globals.save_map_completed = TRUE; main_globals.saving_map = FALSE; @@ -1951,17 +1980,50 @@ static void main_switch_to_structure_bsp_private( return; } +/* The original post-increment test expires on its 92nd 30 Hz update. + Render-only frames must not advance it. Saturate so blocked co-op + respawns can retry indefinitely without overflowing the short counter. */ +static boolean main_death_timer_expired( + short *timer, + long *last_tick, + boolean advance) +{ + long current_tick = game_time_get(); + unsigned long elapsed_ticks = 0; + + if (current_tick < *last_tick) + { + /* A checkpoint/core load can move the simulation clock backwards. */ + *timer = 0; + } + else + { + elapsed_ticks = (unsigned long)current_tick - (unsigned long)*last_tick; + } + *last_tick = current_tick; + + if (!advance) + return FALSE; + + *timer = (short)MIN(92, (unsigned long)*timer + elapsed_ticks); + return *timer >= 92; +} + static void main_lost_map_private( void) { - if (!game_time_get_paused()) + if (main_death_timer_expired( + &main_globals.loss_timer, &main_loss_last_tick, + !game_time_get_paused())) { - if (main_globals.loss_timer++ > 90) - { - main_globals.lost_map = FALSE; - main_globals.loss_timer = 0; + main_globals.lost_map = FALSE; + main_globals.loss_timer = 0; + /* port: network co-op can't revert to a checkpoint (every machine would + have to), so the players respawn where they were at the last one */ + if (game_connection() == _game_connection_network_server) + players_respawn_at_checkpoint(); + else game_state_revert(); - } } return; } @@ -1969,13 +2031,13 @@ static void main_lost_map_private( static void main_respawn_private( void) { - if (!game_time_get_paused() && !cinematic_in_progress()) + if (main_death_timer_expired( + &main_globals.respawn_timer, &main_respawn_last_tick, + !game_time_get_paused() && !cinematic_in_progress()) && + players_respawn_coop()) { - if (main_globals.respawn_timer++ > 90 && players_respawn_coop()) - { - main_globals.respawn = FALSE; - main_globals.respawn_timer = 0; - } + main_globals.respawn = FALSE; + main_globals.respawn_timer = 0; } return; } @@ -2119,6 +2181,22 @@ static void main_won_map_private( { short level; short local_player_index; + + /* port: when a network co-op level is won, the round ends for everyone as + in multiplayer, back to the lobby, and the next round is the campaign's + next level (The Maw's: The Pillar of Autumn). A level not in the campaign + repeats. */ + if (game_connection() == _game_connection_network_server && network_coop_active()) + { + struct network_game *game = network_game_get_game(); + + main_globals.won_map = FALSE; + level = game ? main_get_solo_level_from_name(game->map.name) : NONE; + player_profile_save_level_completed(0); + network_game_server_port_cooperative_won(level != NONE ? + main_get_solo_level_name((level + 1) % NUMBER_OF_SINGLE_PLAYER_LEVELS) : NULL); + return; + } main_globals.want_to_be_at_main_menu = TRUE; main_globals.won_map = FALSE; level = main_get_solo_level_from_name(main_globals.soloplayer_map_name) + 1; diff --git a/source/networking/network_game_globals.c b/source/networking/network_game_globals.c index 2e59918c..0370d3d9 100644 --- a/source/networking/network_game_globals.c +++ b/source/networking/network_game_globals.c @@ -518,6 +518,16 @@ boolean network_game_client_start_frame( main_goto_main_menu(); result = TRUE; } + else if (!global_network_game_client) + { + /* port: no client (the menus let it go, as a lobby's last player + leaving does, without changing the connection): no network game, as + network_game_client_end_frame has it, which only a frame in a game + reaches */ + game_connection_set(0); + main_menu_ensure_player_queues_exist(); + result = TRUE; + } else { result = network_game_client_idle(global_network_game_client); diff --git a/source/networking/network_server_manager.c b/source/networking/network_server_manager.c index c0374fd1..704ade92 100644 --- a/source/networking/network_server_manager.c +++ b/source/networking/network_server_manager.c @@ -778,6 +778,10 @@ static void network_game_server_remove_players_gone_while_loading( /* ---------- globals */ +/* port: the map for the next co-op round after a win +(network_game_server_port_cooperative_won); empty when there is none */ +static char network_game_server_cooperative_next_map[sizeof(((struct network_game *)NULL)->map.name)]; + struct network_game_server network_game_server_memory_do_not_use_directly; boolean network_game_server_memory_do_not_use_directly_in_use = FALSE; @@ -804,6 +808,7 @@ static boolean network_game_server_countdown_slowed[MAXIMUM_NETWORK_MACHINE_COUN /* port/linux/game/network_distributed.c's (the host's bans: bans.txt) */ boolean network_distributed_banned(unsigned long address, char const *hardware_id); void network_distributed_ban(long machine_index, unsigned long address, char const *names); +void network_distributed_kick(char const *names); /* port/linux/src/p2p.c's */ enum { @@ -813,15 +818,22 @@ void p2p_hardware_id_sanitize(char *destination, int size, const char *source); /* console.c's */ void console_warning(const char *format, ...); -/* port: the client machines the distributed netcode asked to drop (their -games sped up: network_game_server_kick_machine), dropped as this server -next looks at its machines; and the addresses of those dropped, kept out -of this server's games while it lasts */ +/* port: the client machines to drop, as this server next looks at its +machines: those the distributed netcode asked to (their games sped up: +network_game_server_kick_machine) and those the host banned, whose +addresses are kept out of this server's games while it lasts; and those the +host kicked, which may join again at once */ enum { MAXIMUM_KICKED_ADDRESSES = 64, }; -static boolean network_game_server_kick_pending[MAXIMUM_NETWORK_MACHINE_COUNT]; +enum +{ + _kick_none = 0, + _kick_kept_out, + _kick_rejoinable, +}; +static byte network_game_server_kick_pending[MAXIMUM_NETWORK_MACHINE_COUNT]; /* port: each client machine's hardware id as it told it joining, hex only (p2p_hardware_id_sanitize), by slot */ static char network_game_server_hardware_ids[MAXIMUM_NETWORK_MACHINE_COUNT][P2P_HARDWARE_ID_SIZE]; @@ -943,11 +955,16 @@ short network_game_server_matching_player_names( return count; } -/* port: the host's ban command: the other machine of the player of the name -(in either case; else the one player whose name begins with it) dropped, its -address in bans.txt (network_distributed_ban), and kept out */ -boolean network_game_server_ban_player( - char const *text) +/* port: the host's ban and kick commands' player: the other machine of the +player of the name (in either case; else the one player whose name begins +with it), and the names of that machine's players (each command's warnings +start with its own name); FALSE (said) if there is none */ +static boolean network_game_server_named_machine( + char const *command, + char const *text, + long *machine_index, + char *names, + long names_size) { struct network_game_server *server = global_network_game_server_get(); long found_index = NONE; @@ -955,12 +972,11 @@ boolean network_game_server_ban_player( long exact_index = NONE; long exact_count = 0; long index; - long machine_index; - char names[96] = ""; + names[0] = 0; if (!server) { - console_warning("ban: only the host of a game bans"); + console_warning("%s: only the host of a game does this", command); return FALSE; } for (index = 0; index < MAXIMUM_NETWORK_PLAYER_COUNT; index++) @@ -986,7 +1002,7 @@ boolean network_game_server_ban_player( if (exact_count > 1) { /* (the host numbers players of the same name: network_server_message_handler.c) */ - console_warning("ban: %ld players are named \"%s\"", exact_count, text); + console_warning("%s: %ld players are named \"%s\"", command, exact_count, text); return FALSE; } if (exact_count == 1) @@ -996,25 +1012,25 @@ boolean network_game_server_ban_player( } if (!text[0]) { - console_warning("ban: give a player's name (Tab completes it)"); + console_warning("%s: give a player's name (Tab completes it)", command); return FALSE; } if (match_count > 1) { - console_warning("ban: %ld players' names begin with \"%s\": give more of it", match_count, text); + console_warning("%s: %ld players' names begin with \"%s\": give more of it", command, match_count, text); return FALSE; } if (match_count == 0) { - console_warning("ban: no player's name begins with \"%s\"", text); + console_warning("%s: no player's name begins with \"%s\"", command, text); return FALSE; } - machine_index = server->game.players[found_index].machine_index; - if (!VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) || - !network_game_server_client_machine_is_joined_to_game(server, &server->client_machines[machine_index]) || - network_game_server_client_machine_is_local(server, &server->client_machines[machine_index])) + *machine_index = server->game.players[found_index].machine_index; + if (!VALID_INDEX(*machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) || + !network_game_server_client_machine_is_joined_to_game(server, &server->client_machines[*machine_index]) || + network_game_server_client_machine_is_local(server, &server->client_machines[*machine_index])) { - console_warning("ban: not a player of the host's own machine, nor one not joined"); + console_warning("%s: not a player of the host's own machine, nor one not joined", command); return FALSE; } /* (every player of that machine, named) */ @@ -1023,16 +1039,46 @@ boolean network_game_server_ban_player( struct network_player const *player = &server->game.players[index]; char name[NETWORK_GAME_SERVER_NAME_TEXT_SIZE]; - if (!network_player_is_valid(player) || player->machine_index != machine_index) + if (!network_player_is_valid(player) || player->machine_index != *machine_index) continue; network_game_server_player_name_text(player, name, sizeof(name)); - if (names[0] && csstrlen(names) + 2 < sizeof(names)) + if (names[0] && csstrlen(names) + 2 < names_size) csstrcat(names, ", "); - if (csstrlen(names) + csstrlen(name) < sizeof(names)) + if (csstrlen(names) + csstrlen(name) < names_size) csstrcat(names, name); } + return TRUE; +} + +/* port: the host's ban command: the player's machine (as +network_game_server_named_machine finds it) dropped, its address in +bans.txt (network_distributed_ban), and kept out */ +boolean network_game_server_ban_player( + char const *text) +{ + long machine_index; + char names[96]; + + if (!network_game_server_named_machine("ban", text, &machine_index, names, sizeof(names))) + return FALSE; network_distributed_ban(machine_index, network_game_server_client_machine_addresses[machine_index], names); - network_game_server_kick_pending[machine_index] = TRUE; + network_game_server_kick_pending[machine_index] = _kick_kept_out; + return TRUE; +} + +/* port: the host's kick command: the player's machine (as the ban +command's) dropped, every machine told, and nothing kept: it may join again +at once */ +boolean network_game_server_kick_player( + char const *text) +{ + long machine_index; + char names[96]; + + if (!network_game_server_named_machine("kick", text, &machine_index, names, sizeof(names))) + return FALSE; + network_distributed_kick(names); + network_game_server_kick_pending[machine_index] = _kick_rejoinable; return TRUE; } @@ -1080,7 +1126,7 @@ void network_game_server_kick_machine( { return; } - network_game_server_kick_pending[machine_index] = TRUE; + network_game_server_kick_pending[machine_index] = _kick_kept_out; } /* (a client machine's player queued to add in game: one refused is as one @@ -1195,6 +1241,9 @@ void network_game_server_dispose( { match_assert(NETWORK_SERVER_MANAGER_FILE, 0x120, server); + /* port: a won co-op round's next level belongs to this server alone */ + network_game_server_cooperative_next_map[0] = 0; + switch (server->state) { case _network_game_server_state_pregame: @@ -1357,6 +1406,14 @@ static void network_game_server_list( 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)); + /* co-op: add the difficulty */ + if (!game->variant.game_engine_index && game->difficulty >= 0 && game->difficulty < 4) + { + static char const *const difficulty_names[] = { "Easy", "Normal", "Heroic", "Legendary" }; + size_t length = strlen(gametype); + + snprintf(gametype + length, sizeof(gametype) - length, " %s", difficulty_names[game->difficulty]); + } /* (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); @@ -1970,7 +2027,7 @@ boolean network_game_server_accept_client_machine_into_game( } /* (a kick asked for the slot's machine before is not this one's) */ if (VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT)) - network_game_server_kick_pending[machine_index] = FALSE; + network_game_server_kick_pending[machine_index] = _kick_none; if (VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) && machine == &server->client_machines[machine_index]) { @@ -3895,6 +3952,77 @@ void network_game_server_port_set_settings( network_game_server_port_settings_apply(server); } +void network_game_server_port_cooperative_won( + char const *next_map) +{ + struct network_game_server *server = global_network_game_server_get(); + + if (!server) + return; + csstrncpy(network_game_server_cooperative_next_map, next_map ? next_map : server->game.map.name, + sizeof(network_game_server_cooperative_next_map) - 1); + network_game_server_cooperative_next_map[sizeof(network_game_server_cooperative_next_map) - 1] = 0; + network_game_server_switch_to_postgame(server); + /* Back to the lobby at once, with the next level set up there to start + or change. Multiplayer leaves the postgame when the host presses a button + on its scoreboard (game_engine.c); co-op has neither, and every machine + was left on the level's last screen. */ + network_game_server_reset_to_pregame(server); +} + +/* port: reapply the co-op settings after a won round, since the playlist +the next round is set up from (network_game_server_setup_game_from_playlist) +has a multiplayer gametype and map */ +static void network_game_server_cooperative_round( + struct network_game_server *server) +{ + struct game_variant variant; + short friendly_fire = server->game.variant_options.friendly_fire; + + if (!network_game_server_cooperative_next_map[0]) + return; + csmemset(&variant, 0, sizeof(variant)); + ustrncpy(variant.human_readable_game_description, L"Co-op", + NUMBEROF(variant.human_readable_game_description) - 1); + csmemcpy(&server->game.variant, &variant, sizeof(server->game.variant)); + network_game_server_variant_options(&server->game.variant, &server->game.variant_options); + /* (Server Setup's FRIENDLY FIRE, for every level of the game) */ + server->game.variant_options.friendly_fire = friendly_fire; + csstrncpy(server->game.map.name, network_game_server_cooperative_next_map, sizeof(server->game.map.name) - 1); + server->game.map.name[sizeof(server->game.map.name) - 1] = 0; + main_set_multiplayer_map_name(server->game.map.name); + server->game.maximum_teams = 1; + network_game_server_cooperative_next_map[0] = 0; +} + +void network_game_server_port_set_cooperative( + struct network_game_server *server, + short difficulty) +{ + if (!server || server->state != _network_game_server_state_pregame) + return; + server->game.difficulty = difficulty; + /* (Server Setup's default for co-op, menu_functions.c's + COOPERATIVE_DEFAULT_PLAYERS, until it sets its own) */ + server->game.maximum_players = MIN(16, MAXIMUM_NETWORK_PLAYER_COUNT); + if (!network_game_server_send_game_data_pregame(server)) + network_event("network_game_server_port_set_cooperative() failed to send updated game settings to clients"); + + return; +} + +void network_game_server_port_set_cooperative_friendly_fire( + short friendly_fire) +{ + struct network_game_server *server = global_network_game_server_get(); + + if (!server || friendly_fire < 0 || friendly_fire >= NUMBER_OF_FRIENDLY_FIRE_MODES) + return; + server->game.variant_options.friendly_fire = friendly_fire; + if (server->state == _network_game_server_state_pregame && !network_game_server_send_game_data_pregame(server)) + network_event("network_game_server_port_set_cooperative_friendly_fire() failed to send updated game settings to clients"); +} + /* port: a gametype's PC options: the menus' (player_ui_set_game_variant_options) when it is the menus' gametype, else its defaults */ static void network_game_server_variant_options( @@ -4196,18 +4324,20 @@ static boolean network_game_server_handle_client_machines( if (!network_game_server_drop_client_machine(server, client_machine)) network_event("failed to remove client machine %x from game", machine_index); } - /* port: one the distributed netcode found cheating: told, and - dropped, its address kept out */ + /* port: one the distributed netcode found cheating, or the host + banned or kicked: told, and dropped; its address kept out but + for a kick's */ else if (VALID_INDEX(client_machine->machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) && - network_game_server_kick_pending[client_machine->machine_index]) + network_game_server_kick_pending[client_machine->machine_index] != _kick_none) { short machine_index = client_machine->machine_index; struct message_server_machine_rejected rejection = { _rejection_code_blacklisted_machine }; struct network_message *message; unsigned long address = network_game_server_client_machine_addresses[machine_index]; + boolean kept_out = network_game_server_kick_pending[machine_index] == _kick_kept_out; - network_game_server_kick_pending[machine_index] = FALSE; - if (address && !network_game_server_client_machine_is_local(server, client_machine)) + network_game_server_kick_pending[machine_index] = _kick_none; + if (kept_out && address && !network_game_server_client_machine_is_local(server, client_machine)) { network_game_server_kicked_addresses[network_game_server_kicked_address_next++ % MAXIMUM_KICKED_ADDRESSES] = address; @@ -4878,6 +5008,7 @@ boolean network_game_server_reset_to_pregame( network_game_reset_for_next_round(&server->game, FALSE); if (network_game_server_setup_game_from_playlist(server)) { + network_game_server_cooperative_round(server); /* the settings record goes out in pieces */ /* (the pregame whatever a machine missed: the machines are in it, and the pregame's flush sends the settings again) */ diff --git a/source/networking/network_server_manager.h b/source/networking/network_server_manager.h index e11c1276..9a013c42 100644 --- a/source/networking/network_server_manager.h +++ b/source/networking/network_server_manager.h @@ -59,8 +59,25 @@ every game the server sets up */ void network_game_server_port_set_settings( wchar_t const *name, long maximum_players); +/* port: a co-op level was won (main.c). Ends the round for everyone as in +multiplayer; the next round is on next_map (NULL repeats the level). */ +void network_game_server_port_cooperative_won( + char const *next_map); +/* port: co-op server settings: the difficulty, sent to clients, and the +most players a co-op game starts with (Server Setup sets its own) */ +void network_game_server_port_set_cooperative( + struct network_game_server *server, + short difficulty); +/* port: co-op's friendly fire between its players (a _friendly_fire_ mode: +Server Setup's FRIENDLY FIRE), kept for the levels after */ +void network_game_server_port_set_cooperative_friendly_fire( + short friendly_fire); boolean network_game_server_ban_player( char const *text); +/* port: the host's kick command: as the ban command, but nothing kept (no +bans.txt line, the address not kept out): the player may join again at once */ +boolean network_game_server_kick_player( + char const *text); short network_game_server_matching_player_names( char const *text, char (*names)[NETWORK_GAME_SERVER_NAME_TEXT_SIZE], diff --git a/source/objects/damage.c b/source/objects/damage.c index 5f00c31c..aab1f8d7 100644 --- a/source/objects/damage.c +++ b/source/objects/damage.c @@ -1693,7 +1693,12 @@ void object_cause_damage( force_kill = TRUE; } + /* port: nor does a teammate's hit kill outright (a melee from + behind, an instant kill) where the gametype's friendly fire + spares the body; a killing blow the host dealt (a client's + replay of it) is dealt as the host dealt it */ if (force_kill && + (friendly_damage == _friendly_damage_all || distributed_damage_authorized) && !TEST_FLAG(current_object->object.damage_flags, _object_dead_bit)) { current_object->object.body_vitality = 0.f; diff --git a/source/objects/objects.c b/source/objects/objects.c index 3ffb5ab7..f434efb2 100644 --- a/source/objects/objects.c +++ b/source/objects/objects.c @@ -117,6 +117,10 @@ on the host's word */ long network_objects_new_object_index(void); boolean network_objects_creating_host_object(void); boolean network_objects_may_delete(long object_index); +/* port/linux/game/network_coop.c's: the scripts' attaching, on co-op's clients */ +void network_coop_note_attach(long parent_index, char const *parent_marker_name, long child_index, + char const *child_marker_name); +void network_coop_note_detach(long parent_index, long child_index); static void object_connect_lights(long object_index, boolean disconnect, boolean reconnect); static void object_name_list_allocate(void); @@ -1857,9 +1861,11 @@ short object_get_marker_by_name( struct object_datum const *object = object_get(object_index); struct object_definition const *object_definition = object_definition_get(object->definition_index); - struct object_datum *matrix_object = object_get(object_index); - real_matrix4x3 const *matrices = (real_matrix4x3 *)object_header_block_get(object_index, - &matrix_object->object.node_matrices); + /* port: the matrices the renderer draws with, so a marker an effect, a + particle system or a contrail hangs off moves as the object does, between + the ticks too (port/linux/game/render_interpolation.c). While a tick runs, + which is when the game asks for markers, this is the same array. */ + real_matrix4x3 const *matrices = object_get_node_matrices(object_index); marker = model_get_marker_by_name( object_definition->object.model.index, @@ -2885,6 +2891,7 @@ void objects_scripting_detach( child_object_index!=NONE && object_get(child_object_index)->object.parent_object_index == parent_object_index) { + network_coop_note_detach(parent_object_index, child_object_index); object_detach(child_object_index); } @@ -3820,6 +3827,7 @@ void objects_scripting_attach( child_object_index !=NONE && object_get(child_object_index)->object.parent_object_index==NONE) { + network_coop_note_attach(parent_object_index, parent_marker_name, child_object_index, child_marker_name); object_attach_to_marker(parent_object_index, parent_marker_name, child_object_index, child_marker_name); } diff --git a/source/objects/scenery.c b/source/objects/scenery.c index 2c65141f..ec46b93c 100644 --- a/source/objects/scenery.c +++ b/source/objects/scenery.c @@ -42,6 +42,7 @@ symbols in this file: #include "objects/object_definitions.h" #include "scenario/scenario_definitions.h" #include "tag_files/tag_files.h" +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ /* ---------- constants */ @@ -239,6 +240,8 @@ static void scenery_animation_start_private( scenery->object.object.animation.state.frame_index = frame_index < 0 ? 0 : MIN(frame_index, animation->frame_count - 1); scenery->object.object.animation.animation_graph_index = animation_graph_index; + network_coop_note_scenery_animation(object_index, animation_graph_index, animation_index, + scenery->object.object.animation.state.frame_index); } else { diff --git a/source/objects/widgets/antenna.c b/source/objects/widgets/antenna.c index 00185605..6586c42b 100644 --- a/source/objects/widgets/antenna.c +++ b/source/objects/widgets/antenna.c @@ -51,6 +51,18 @@ symbols in this file: /* ---------- structures */ +/* A chain's shape after each of the last two ticks, relative to the point +its object's marker carries the base by: a frame between the ticks is drawn +with the part of the way between those two shapes that it is, as an object is +drawn between its own (port/linux/game/render_interpolation.c). */ +struct antenna_shape +{ + short count; + boolean has_previous; + real_point3d previous[MAXIMUM_ANTENNA_VERTICES + 1]; + real_point3d latest[MAXIMUM_ANTENNA_VERTICES + 1]; +}; + /* ---------- prototypes */ struct bitmap_data *bitmap_group_try_and_get_bitmap( @@ -63,7 +75,12 @@ static void antenna_update_attachment( struct location *attachment_location, real_point3d *attachment_point, real_vector3d *attachment_vector); +static void antenna_record_shape( + long antenna_index, + struct antenna_datum *antenna, + short count); static void antenna_render_proper( + long antenna_index, struct antenna_datum *antenna, struct antenna_definition *definition); static void antenna_update( @@ -74,6 +91,7 @@ static void antenna_update( /* ---------- globals */ struct data_array *antenna_data; +static struct antenna_shape antenna_shapes[MAXIMUM_ANTENNAS]; /* ---------- public code */ @@ -144,6 +162,9 @@ long antenna_new( antenna->definition_index = definition_index; antenna->object_index = NONE; antenna->updates_since_last_render = 0; + /* (a new chain blends from no shape: the slot's record may be a + deleted antenna's, or an earlier map's) */ + antenna_shapes[DATUM_INDEX_TO_ABSOLUTE_INDEX(antenna_index)].count = 0; antenna->last_attachment_location.z = 0.0f; antenna->last_attachment_location.y = 0.0f; antenna->last_attachment_location.x = 0.0f; @@ -247,7 +268,7 @@ void antenna_render( } antenna->updates_since_last_render = 0; - antenna_render_proper(antenna, definition); + antenna_render_proper(antenna_index, antenna, definition); } return; @@ -269,7 +290,19 @@ void antennas_update( { antenna->updates_since_last_render++; if (antenna->object_index != NONE && antenna->updates_since_last_render < 5) + { + /* The chain is a simulation of the Xbox's own step, an update + a tick: its springs, its carry and its points' physics are + per-update quantities (game.c steps it in the tick, not in + game_frame with the frames' own movement). */ antenna_update(antenna, definition, MIN(delta, 1.0f / 15.0f)); + /* what this tick leaves a frame between ticks to be drawn in + (antenna_render_proper) */ + antenna_record_shape( + antenna_index, + antenna, + (short)(definition->vertices.count + 1)); + } } } @@ -324,7 +357,51 @@ static void antenna_update_attachment( return; } +/* the shape this tick leaves the chain in, relative to the point the base +was put at (antenna_update_attachment): a tick on from the last one, so a +frame between the two is drawn from both (antenna_render_proper) */ +static void antenna_record_shape( + long antenna_index, + struct antenna_datum *antenna, + short count) +{ + long absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(antenna_index); + struct antenna_shape *shape; + real_point3d const *base = &antenna->vertices[0].position; + short vertex_index; + + if (count <= 0 || count > MAXIMUM_ANTENNA_VERTICES + 1 || + absolute_index < 0 || absolute_index >= MAXIMUM_ANTENNAS) + { + return; + } + + shape = &antenna_shapes[absolute_index]; + if (shape->count == count) + { + csmemcpy(shape->previous, shape->latest, sizeof(real_point3d) * count); + shape->has_previous = TRUE; + } + else + { + shape->count = count; + shape->has_previous = FALSE; + } + + for (vertex_index = 0; vertex_index < count; vertex_index++) + { + real_point3d const *position = &antenna->vertices[vertex_index].position; + + shape->latest[vertex_index].x = position->x - base->x; + shape->latest[vertex_index].y = position->y - base->y; + shape->latest[vertex_index].z = position->z - base->z; + } + + return; +} + static void antenna_render_proper( + long antenna_index, struct antenna_datum *antenna, struct antenna_definition *definition) { @@ -336,6 +413,12 @@ static void antenna_render_proper( real falloff_scale = (100.0f - definition->cutoff_pixels) / (definition->falloff_pixels - definition->cutoff_pixels); + struct antenna_shape const *shape = + &antenna_shapes[DATUM_INDEX_TO_ABSOLUTE_INDEX(antenna_index)]; + real_point3d chain[MAXIMUM_ANTENNA_VERTICES + 1]; + real fraction = render_interpolation_fraction(); + real_point3d base = antenna->vertices[0].position; + short chain_count = (short)(vertices->count + 1); short vertex_index; if (falloff_scale < 0.0f) @@ -343,6 +426,52 @@ static void antenna_render_proper( else if (falloff_scale > 1.0f) falloff_scale = 1.0f; + if (chain_count > MAXIMUM_ANTENNA_VERTICES + 1) + chain_count = MAXIMUM_ANTENNA_VERTICES + 1; + + /* The frames between the ticks (port/linux/game/render_interpolation.c): + the ticks left the chain's shape, a frame is drawn the part of the way + between the last two of them that it is, and the point its vehicle's + marker is at now carries the whole of it. The ticks' own base is what + the shape was recorded against, so with no such record the shape the + last tick left is drawn instead. */ + if (antenna->object_index != NONE) + { + struct object_marker marker; + + object_get_marker_by_name( + antenna->object_index, + definition->attachment_marker, + &marker, + 1); + base = marker.matrix.position; + } + + for (vertex_index = 0; vertex_index < chain_count; vertex_index++) + { + real_point3d offset; + + if (shape->count == chain_count && shape->has_previous) + { + offset.x = shape->previous[vertex_index].x + + (shape->latest[vertex_index].x - shape->previous[vertex_index].x) * fraction; + offset.y = shape->previous[vertex_index].y + + (shape->latest[vertex_index].y - shape->previous[vertex_index].y) * fraction; + offset.z = shape->previous[vertex_index].z + + (shape->latest[vertex_index].z - shape->previous[vertex_index].z) * fraction; + } + else + { + offset.x = antenna->vertices[vertex_index].position.x - antenna->vertices[0].position.x; + offset.y = antenna->vertices[vertex_index].position.y - antenna->vertices[0].position.y; + offset.z = antenna->vertices[vertex_index].position.z - antenna->vertices[0].position.z; + } + + chain[vertex_index].x = base.x + offset.x; + chain[vertex_index].y = base.y + offset.y; + chain[vertex_index].z = base.z + offset.z; + } + build_sprites_begin( &sprite_data, (short)vertices->count, @@ -351,12 +480,10 @@ static void antenna_render_proper( 0); for (vertex_index = 0; - vertex_index < vertices->count; + vertex_index < chain_count - 1; vertex_index = (short)(vertex_index + 1)) { struct antenna_vertex_datum *vertex = &antenna->vertices[vertex_index]; - struct antenna_vertex_datum *next_vertex = - &antenna->vertices[vertex_index + 1]; struct antenna_vertex_definition *definition_vertex = TAG_BLOCK_GET_ELEMENT( vertices, vertex_index, @@ -364,9 +491,9 @@ static void antenna_render_proper( real_vector3d direction; real_argb_color color; - direction.i = next_vertex->position.x - vertex->position.x; - direction.j = next_vertex->position.y - vertex->position.y; - direction.k = next_vertex->position.z - vertex->position.z; + direction.i = chain[vertex_index + 1].x - chain[vertex_index].x; + direction.j = chain[vertex_index + 1].y - chain[vertex_index].y; + direction.k = chain[vertex_index + 1].z - chain[vertex_index].z; color = definition_vertex->color; if (vertex->sprite_scale != 0.0f && falloff_scale > 0.0f) @@ -376,7 +503,7 @@ static void antenna_render_proper( 1, definition_vertex->sequence_index, 0, - &vertex->position, + &chain[vertex_index], &direction, 0.0f, vertex->sprite_scale, diff --git a/source/objects/widgets/widgets.c b/source/objects/widgets/widgets.c index 3fd0caf0..7c04f7e5 100644 --- a/source/objects/widgets/widgets.c +++ b/source/objects/widgets/widgets.c @@ -102,7 +102,9 @@ static struct widget_type_definition data_0030b2b0[NUMBER_OF_WIDGET_TYPES] = antennas_dispose, antenna_new, antenna_delete, - antennas_update, + /* the chain simulates at the Xbox's own rate, an update a tick, so + the tick steps it rather than the frame (game.c; antenna.c) */ + NULL, antenna_render }, { diff --git a/source/rasterizer/rasterizer_cinematics.c b/source/rasterizer/rasterizer_cinematics.c index a6bd37d4..95ac5bd4 100644 --- a/source/rasterizer/rasterizer_cinematics.c +++ b/source/rasterizer/rasterizer_cinematics.c @@ -497,4 +497,85 @@ real rasterizer_get_near_clip_distance( return near_clip_distance; } +/* port: the state for network co-op (rasterizer_cinematics.h) */ +void rasterizer_screen_effect_port_get( + struct rasterizer_screen_effect_port_state *state) +{ + struct rasterizer_cinematic_screen_effect_state const *globals = cinematic_screen_effect_globals; + + csmemset(state, 0, sizeof(*state)); + if (!globals) + return; + state->has_control = globals->has_control; + state->initialized = globals->initialized; + state->video_on = globals->parameters.video_on; + state->filter_desaturation_is_additive = globals->parameters.filter_desaturation_is_additive; + state->filter_light_enhancement_uses_convolution_mask = + globals->parameters.filter_light_enhancement_uses_convolution_mask; + state->filter_desaturation_uses_convolution_mask = globals->parameters.filter_desaturation_uses_convolution_mask; + state->convolution_extra_passes = globals->parameters.convolution_extra_passes; + state->convolution_type = globals->parameters.convolution_type; + state->video_overbright_mode = globals->parameters.video_overbright_mode; + state->filter_desaturation_tint = globals->parameters.filter_desaturation_tint; + state->video_noise_intensity = globals->parameters.video_noise_intensity; + csmemcpy(state->convolution_radius, globals->convolution_radius, sizeof(state->convolution_radius)); + csmemcpy(state->convolution_time, globals->convolution_time, sizeof(state->convolution_time)); + csmemcpy(state->filter_light_enhancement_intensity, globals->filter_light_enhancement_intensity, + sizeof(state->filter_light_enhancement_intensity)); + csmemcpy(state->filter_desaturation_intensity, globals->filter_desaturation_intensity, + sizeof(state->filter_desaturation_intensity)); + csmemcpy(state->filter_time, globals->filter_time, sizeof(state->filter_time)); + csmemcpy(state->script_values, globals->script_values, sizeof(state->script_values)); + state->near_clip_distance = globals->near_clip_distance; +} + +/* port: the first bitmap of one of the video effect's bitmap groups, as +rasterizer_screen_effect_set_video uses it */ +static struct bitmap_data *screen_effect_video_bitmap( + long bitmap_group_index) +{ + return TAG_BLOCK_GET_ELEMENT(&bitmap_group_get(bitmap_group_index)->bitmaps, 0, struct bitmap_data); +} + +/* port: the host's state put in place on a client, with the video effect's +bitmaps found on this machine */ +void rasterizer_screen_effect_port_set( + struct rasterizer_screen_effect_port_state const *state) +{ + struct rasterizer_cinematic_screen_effect_state *globals = cinematic_screen_effect_globals; + boolean video = state->video_on && global_rasterizer_data && + global_rasterizer_data->screen_effect_video_scanline_map.index != NONE && + global_rasterizer_data->screen_effect_video_noise_map.index != NONE; + + if (!globals) + return; + globals->has_control = state->has_control; + globals->initialized = state->initialized; + globals->parameters.convolution_extra_passes = state->convolution_extra_passes; + globals->parameters.convolution_type = state->convolution_type; + globals->parameters.convolution_mask = NULL; + globals->parameters.filter_desaturation_tint = state->filter_desaturation_tint; + globals->parameters.filter_desaturation_is_additive = state->filter_desaturation_is_additive; + globals->parameters.filter_light_enhancement_uses_convolution_mask = + state->filter_light_enhancement_uses_convolution_mask; + globals->parameters.filter_desaturation_uses_convolution_mask = state->filter_desaturation_uses_convolution_mask; + globals->parameters.video_on = video; + globals->parameters.video_overbright_mode = state->video_overbright_mode; + globals->parameters.video_noise_intensity = state->video_noise_intensity; + globals->parameters.video_noise_map_scale = video ? 1.0f : 0.0f; + globals->parameters.video_scanline_map = + video ? screen_effect_video_bitmap(global_rasterizer_data->screen_effect_video_scanline_map.index) : NULL; + globals->parameters.video_noise_map = + video ? screen_effect_video_bitmap(global_rasterizer_data->screen_effect_video_noise_map.index) : NULL; + csmemcpy(globals->convolution_radius, state->convolution_radius, sizeof(globals->convolution_radius)); + csmemcpy(globals->convolution_time, state->convolution_time, sizeof(globals->convolution_time)); + csmemcpy(globals->filter_light_enhancement_intensity, state->filter_light_enhancement_intensity, + sizeof(globals->filter_light_enhancement_intensity)); + csmemcpy(globals->filter_desaturation_intensity, state->filter_desaturation_intensity, + sizeof(globals->filter_desaturation_intensity)); + csmemcpy(globals->filter_time, state->filter_time, sizeof(globals->filter_time)); + csmemcpy(globals->script_values, state->script_values, sizeof(globals->script_values)); + globals->near_clip_distance = state->near_clip_distance; +} + /* ---------- private code */ diff --git a/source/rasterizer/rasterizer_cinematics.h b/source/rasterizer/rasterizer_cinematics.h index b74c6d1f..174425ed 100644 --- a/source/rasterizer/rasterizer_cinematics.h +++ b/source/rasterizer/rasterizer_cinematics.h @@ -87,6 +87,38 @@ void rasterizer_set_near_clip_distance( real rasterizer_get_near_clip_distance( void); +/* port: the cinematic screen effect as the scripts left it, without its +bitmaps' addresses, for network co-op to copy from the host to a client +(port/linux/game/network_coop.c). Its times are game time, the same on +every machine, so a client's effect fades as the host's does. Every field +from filter_desaturation_tint on is a real. */ +struct rasterizer_screen_effect_port_state +{ + byte has_control; + byte initialized; + byte video_on; + byte filter_desaturation_is_additive; + byte filter_light_enhancement_uses_convolution_mask; + byte filter_desaturation_uses_convolution_mask; + short convolution_extra_passes; + short convolution_type; + short video_overbright_mode; + real_rgb_color filter_desaturation_tint; + real video_noise_intensity; + real convolution_radius[2]; + real convolution_time[2]; + real filter_light_enhancement_intensity[2]; + real filter_desaturation_intensity[2]; + real filter_time[2]; + real script_values[4]; + real near_clip_distance; +}; + +void rasterizer_screen_effect_port_get( + struct rasterizer_screen_effect_port_state *state); +void rasterizer_screen_effect_port_set( + struct rasterizer_screen_effect_port_state const *state); + /* ---------- globals */ /* ---------- public code */ diff --git a/source/saved games/game_state.c b/source/saved games/game_state.c index 3388746c..726605c9 100644 --- a/source/saved games/game_state.c +++ b/source/saved games/game_state.c @@ -299,6 +299,22 @@ void game_state_save( return; } +/* port: whether game_state_revert has a saved state to go back to */ +boolean game_state_port_saved_game_valid( + void) +{ + return game_state_globals.saved_game_valid; +} + +/* port: stamps the revert at the current game time again, after a network +co-op host moved its clock on past the revert (game_state_reverted +compares the two) */ +void game_state_port_restamp_revert_time( + void) +{ + game_state_globals.revert_time = game_time_get(); +} + void game_state_revert( void) { diff --git a/source/saved games/game_state.h b/source/saved games/game_state.h index d82ee86a..b8e8e02a 100644 --- a/source/saved games/game_state.h +++ b/source/saved games/game_state.h @@ -38,6 +38,11 @@ void game_state_save( void); void game_state_revert( void); +/* port: network co-op's (game_state.c) */ +boolean game_state_port_saved_game_valid( + void); +void game_state_port_restamp_revert_time( + void); void game_state_save_to_persistent_storage( void); boolean game_state_test_persistent_storage( diff --git a/source/sound/game_sound.c b/source/sound/game_sound.c index a798be5b..7081db00 100644 --- a/source/sound/game_sound.c +++ b/source/sound/game_sound.c @@ -512,6 +512,16 @@ long unspatialized_impulse_sound_new( 0); } +/* port: a co-op host sends its scripts' sounds to its clients +(port/linux/game/network_coop.c; the kinds are in network_distributed.h) */ +enum +{ + _coop_sound_impulse, + _coop_sound_looping_start, + _coop_sound_looping_stop, +}; +void network_coop_note_sound(short kind, long definition_index, long object_index, real scale); + void scripted_sound_new( long definition_index, long source_object_index, @@ -525,6 +535,7 @@ void scripted_sound_new( if (definition_index != NONE) { + network_coop_note_sound(_coop_sound_impulse, definition_index, source_object_index, scale); definition = sound_definition_get(definition_index); sound_stop_impulse(definition->scripting_sound_index); definition->scripting_time = game_time_get() @@ -737,6 +748,8 @@ static void scripted_looping_sound_stop_internal( void scripted_looping_sound_stop( long sound_index) { + if (sound_index != NONE) + network_coop_note_sound(_coop_sound_looping_stop, sound_index, NONE, 0.0f); scripted_looping_sound_stop_internal(sound_index, FALSE); return; @@ -755,6 +768,8 @@ void scripted_looping_sound_start( { definition = looping_sound_definition_get(sound_index); scripted_looping_sound_stop(sound_index); + /* after the stop above, so clients get them in the same order */ + network_coop_note_sound(_coop_sound_looping_start, sound_index, source_object_index, gain); match_assert( "c:\\halo\\SOURCE\\sound\\game_sound.c", 495, diff --git a/source/units/unit_dialogue.c b/source/units/unit_dialogue.c index d62e4145..31172b59 100644 --- a/source/units/unit_dialogue.c +++ b/source/units/unit_dialogue.c @@ -94,6 +94,9 @@ symbols in this file: #include "tag_files/tag_groups.h" #include "unit_definitions.h" +/* port: the AI's speech goes to the clients' copies (port/linux/game/network_actors.c) */ +void network_actors_note_speech(long unit_index, long sound_definition_index); + /* ---------- constants */ enum unit_play_speech_type @@ -1036,6 +1039,15 @@ void unit_dialogue_update( &position, &forward, 1.0f); + /* port: the AI's speech, heard on the clients too (they + play pain and death sounds themselves) */ + if (unit->unit.player_index == NONE && + unit->unit.speech.current.priority != _unit_speech_pain && + unit->unit.speech.current.priority != _unit_speech_involuntary && + unit->unit.speech.current.priority != _unit_speech_death) + { + network_actors_note_speech(unit_index, unit->unit.speech.current.sound_definition_index); + } } ai_communication_started( diff --git a/source/units/unit_scripting_commands.c b/source/units/unit_scripting_commands.c index 47e83373..93db429c 100644 --- a/source/units/unit_scripting_commands.c +++ b/source/units/unit_scripting_commands.c @@ -33,6 +33,7 @@ symbols in this file: #include "items/weapons.h" #include "objects/damage.h" #include "units.h" +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ /* ---------- constants */ @@ -46,6 +47,10 @@ void unit_scripting_set_current_vitality( long unit_index, real body_vitality, real shield_vitality); +void unit_scripting_set_current_vitality_of( + long unit_index, + real body_vitality, + real shield_vitality); /* ---------- globals */ @@ -56,6 +61,9 @@ void unit_scripting_set_maximum_vitality( real body_vitality, real shield_vitality) { + /* port: in network co-op, a player's is every player's */ + if (unit_index != NONE && network_coop_set_players_vitality(unit_index, TRUE, body_vitality, shield_vitality)) + return; if (unit_index != NONE) { struct object_datum const *object; @@ -86,6 +94,17 @@ void unit_scripting_set_current_vitality( long unit_index, real body_vitality, real shield_vitality) +{ + /* port: in network co-op, a player's is every player's */ + if (unit_index != NONE && !network_coop_set_players_vitality(unit_index, FALSE, body_vitality, shield_vitality)) + unit_scripting_set_current_vitality_of(unit_index, body_vitality, shield_vitality); +} + +/* port: the set itself, for one unit (network_coop.c sets every player's) */ +void unit_scripting_set_current_vitality_of( + long unit_index, + real body_vitality, + real shield_vitality) { if (unit_index != NONE) { diff --git a/source/units/units.c b/source/units/units.c index feb4891d..8d39926b 100644 --- a/source/units/units.c +++ b/source/units/units.c @@ -691,6 +691,19 @@ symbols in this file: #include "saved games/game_state.h" #include "sound/game_sound.h" #include "vehicles.h" +#include "network_coop.h" /* port: port/linux/game/network_coop.c */ +#include "coop_enemies.h" /* port: port/linux/game/coop_enemies.c */ + +/* port: the control and animation impulses the host's actors give their +units go to the clients' copies (port/linux/game/network_actors.c) */ +void network_actors_note_control(long unit_index, struct unit_control_data const *control_data); +void network_actors_note_impulse(long unit_index, short animation_impulse, real_vector2d const *alignment_vector); +void network_actors_note_melee(long unit_index, real_vector2d const *alignment_vector); +void network_actors_note_leap(long unit_index, real_vector2d const *alignment_vector); +/* port/linux/game/network_objects.c: the host's pick of a flinch or death animation */ +short network_objects_damage_animation(long unit_index, short type, short animation_index); +void network_actors_note_user_animation(long unit_index, long animation_graph_index, short animation_index, + boolean interpolate); #ifdef HALO_ANDROID #include "halo_vr.h" @@ -739,14 +752,6 @@ enum _unit_debug_function_active_bit = 2, }; -enum -{ - _unit_damage_animation_soft_ping = 0, - _unit_damage_animation_hard_ping, - _unit_damage_animation_soft_kill, - _unit_damage_animation_hard_kill, -}; - enum { _unit_damage_direction_front = 0, @@ -981,6 +986,8 @@ static long unit_get_weapon(struct unit_datum *unit, short index); static void unit_drop_item(long unit_index, long item_index); /* port/linux/game/network_objects.c's */ boolean network_objects_creating_host_object(void); +/* port/linux/game/network_damage.c's */ +void network_damage_note_grenade(long unit_index, short grenade_type); /* network_game_globals.c's */ boolean network_game_distributed_client(void); static void unit_drop_grenades( @@ -2331,6 +2338,7 @@ boolean unit_custom_animation_at_frame( { unit->object.animation.state.frame_index = frame_index; success = TRUE; + network_coop_note_unit_animation_frame(unit_index, frame_index); } } @@ -3716,6 +3724,7 @@ void unit_detach_from_parent( if (unit->object.parent_object_index != NONE) { + long parent_index = unit->object.parent_object_index; real_point3d parent_origin; real_point3d unit_origin; real_vector3d velocity; @@ -3745,6 +3754,10 @@ void unit_detach_from_parent( &unit->object.translational_velocity); object_set_visibility(unit_index, TRUE); object_compute_node_matrices(unit_index); + /* port: a vehicle dropped by another comes with more in a large + co-op game (port/linux/game/network_coop.c) */ + if (unit->object.type == _object_type_vehicle && object_get(parent_index)->object.type == _object_type_vehicle) + network_coop_vehicle_dropped(unit_index, parent_index); } return; @@ -4061,6 +4074,13 @@ boolean unit_start_user_animation( TRUE); object_compute_node_matrices_recursive(unit_index); animation_started = TRUE; + network_coop_note_unit_animation(unit_index, animation_graph_index, animation_index, + interpolate); + if (unit->unit.player_index == NONE) + { + network_actors_note_user_animation(unit_index, animation_graph_index, animation_index, + interpolate); + } } } } @@ -4111,11 +4131,100 @@ void unit_stop_custom_animation( if (unit_index!=NONE && unit_get(unit_index)->unit.animation.state==_unit_state_user_animation) { unit_animation_set_state(unit_index, _unit_state_idle); + network_coop_note_unit_animation(unit_index, NONE, NONE, FALSE); } return; } +/* port: a unit that feigned death gets back up (a Flood combat form). The +host's does when its timer runs out (unit_update); a client's copy when the +host's word on it says it is alive again (network_actors.c). */ +void unit_port_resurrect( + long unit_index) +{ + struct unit_datum *unit = unit_get(unit_index); + short new_state = TEST_FLAG(unit->unit.animation.flags, _unit_animation_fallen_on_front_bit) ? + _unit_state_resurrect_front : _unit_state_resurrect_back; + + SET_FLAG(unit->object.damage_flags, _object_dead_bit, FALSE); + unit_set_actively_controlled(unit_index, TRUE); + unit_set_or_test_seat_and_weapon_label(unit_index, base_seat_label_get(_unit_animation_state_suspicious), NULL, + TRUE); + unit_animation_set_state(unit_index, new_state); + SET_FLAG(unit->unit.animation.flags, _unit_animation_ignore_translation_bit, FALSE); + if (unit->object.type == _object_type_biped) + biped_stop_limp_body_physics(unit_index); + unit_scream(unit_index, _unit_scream_resurrection); +} + +/* how far into its own flinch or death animation a client's unit still +switches to the host's pick */ +#define DAMAGE_ANIMATION_SWITCH_TICKS 10 + +/* port: whether the unit plays a flinch or death animation of the type +(the host's pick, port/linux/game/network_objects.c): it is switched to the +host's if its own has only just begun. FALSE if it plays none. */ +boolean unit_port_correct_damage_animation( + long unit_index, + short type, + short animation_index) +{ + struct unit_datum *unit = unit_get(unit_index); + long animation_graph_index = unit_definition_get(unit->definition_index)->object.animation_graph.index; + + if (type == _unit_damage_animation_soft_ping) + { + /* (an overlay, beside the unit's animation) */ + if (unit->unit.animation.soft_ping_animation.index == NONE) + return FALSE; + if (unit->unit.animation.soft_ping_animation.frame_index < DAMAGE_ANIMATION_SWITCH_TICKS) + unit->unit.animation.soft_ping_animation.index = animation_index; + return TRUE; + } + if (type == _unit_damage_animation_hard_ping ? + unit->unit.animation.state != _unit_state_hard_ping : + unit->unit.animation.state != _unit_state_dying && unit->unit.animation.state != _unit_state_dying_airborne) + { + return FALSE; + } + if (unit->object.animation.state.index != animation_index && + unit->object.animation.state.frame_index < DAMAGE_ANIMATION_SWITCH_TICKS) + { + unit_set_animation(unit_index, animation_graph_index, animation_index); + object_compute_node_matrices_recursive(unit_index); + } + return TRUE; +} + +/* port: a co-op client plays the custom animation the host's unit started +(network_coop.c): exactly that animation, not another random permutation */ +void unit_port_play_user_animation( + long unit_index, + long animation_graph_index, + short animation_index, + boolean interpolate, + short frame_index) +{ + struct unit_datum *unit = unit_get(unit_index); + struct animation_graph *animation_graph = animation_graph_definition_get(animation_graph_index); + struct animation *animation; + + if (animation_index < 0 || animation_index >= animation_graph->animations.count) + return; + animation = TAG_BLOCK_GET_ELEMENT(&animation_graph->animations, animation_index, struct animation); + if (animation->type != _animation_base) + return; + if (interpolate) + object_start_interpolation(unit_index, 6); + unit->unit.animation.state = _unit_state_user_animation; + unit_set_animation(unit_index, animation_graph_index, animation_index); + SET_FLAG(unit->unit.animation.flags, _unit_animation_postpone_weapon_ik_until_interpolation_ends_bit, TRUE); + if (frame_index > 0 && frame_index < animation->frame_count) + unit->object.animation.state.frame_index = frame_index; + object_compute_node_matrices_recursive(unit_index); +} + boolean unit_melee_attack_begin( long unit_index, boolean continuous, @@ -4195,6 +4304,9 @@ boolean unit_melee_attack_begin( else { unit->unit.melee_attack_state = 1; + /* port: the AI's swing, played on the clients too */ + if (unit->unit.player_index == NONE) + network_actors_note_melee(unit_index, alignment_vector); } result = TRUE; } @@ -4248,6 +4360,9 @@ boolean unit_leap_begin( { unit_align_facing(unit_index, alignment_vector); } + /* port: the AI's leap, played on the clients too */ + if (unit->unit.player_index == NONE) + network_actors_note_leap(unit_index, alignment_vector); result = TRUE; } @@ -5113,6 +5228,11 @@ short vehicle_scripting_load_magic( short available_seat_count; long reference_index; long unit_index; + /* port: the riders left without a seat that network co-op keeps + (coop_enemies.c), erased once the list is gone through */ + long unseated_actor_indices[64]; + short unseated_count = 0; + short unseated_number; available_seat_count = vehicle_scripting_find_available_seats( vehicle_index, @@ -5131,6 +5251,7 @@ short vehicle_scripting_load_magic( { struct unit_datum *unit = (struct unit_datum *)object; short available_seat_index; + long loaded_before = loaded_count; for (available_seat_index = 0; available_seat_index loaded_before) + coop_enemies_rider_seated(vehicle_index, unit_index); + else if (unit->object.parent_object_index == NONE && unit->unit.actor_index != NONE && + unseated_count < (short)NUMBEROF(unseated_actor_indices) && + coop_enemies_rider_unseated(vehicle_index, unit_index)) + { + unseated_actor_indices[unseated_count++] = unit->unit.actor_index; + } } unit_index = object_list_get_next(object_list_index, &reference_index); } + for (unseated_number = 0; unseated_number < unseated_count; unseated_number++) + actor_erase(unseated_actor_indices[unseated_number], FALSE); } return (short)loaded_count; @@ -5180,6 +5315,7 @@ void unit_open( { if (unit_index!=NONE) { + network_coop_note_unit_open(unit_index, TRUE); unit_animation_set_state(unit_index, _unit_state_opening); } @@ -5191,6 +5327,7 @@ void unit_close( { if (unit_index!=NONE) { + network_coop_note_unit_open(unit_index, FALSE); unit_animation_set_state(unit_index, _unit_state_closing); } @@ -5456,32 +5593,7 @@ boolean unit_update( { if (unit->object.body_vitality>0.f) { - short new_state = TEST_FLAG( - unit->unit.animation.flags, - _unit_animation_fallen_on_front_bit) ? _unit_state_resurrect_front : _unit_state_resurrect_back; - - SET_FLAG(unit->object.damage_flags, _object_dead_bit, FALSE); - - unit_set_actively_controlled(unit_index, TRUE); - unit_set_or_test_seat_and_weapon_label( - unit_index, - base_seat_label_get(_unit_animation_state_suspicious), - NULL, - TRUE - ); - unit_animation_set_state(unit_index, new_state); - - SET_FLAG( - unit->unit.animation.flags, - _unit_animation_ignore_translation_bit, - FALSE); - - if (unit->object.type==_object_type_biped) - { - biped_stop_limp_body_physics(unit_index); - } - - unit_scream(unit_index, _unit_scream_resurrection); + unit_port_resurrect(unit_index); } else { @@ -6426,6 +6538,9 @@ static void unit_ping_animation( animation_graph_index, selected_damage_animation_index); } + /* port: the host's pick on every machine (port/linux/game/network_objects.c) */ + animation_index = network_objects_damage_animation(unit_index, _unit_damage_animation_soft_ping, + animation_index); if (animation_index==NONE) { @@ -6550,6 +6665,8 @@ static void unit_ping_animation( animation_graph_index, selected_damage_animation_index); } + /* port: the host's pick on every machine (port/linux/game/network_objects.c) */ + animation_index = network_objects_damage_animation(unit_index, damage_animation_type, animation_index); if (animation_index!=NONE) { @@ -7693,6 +7810,9 @@ static void unit_throw_grenade_move_to_hand( object_attach_to_node(unit_index, object_index, marker.node_index); unit->unit.grenade_object_index = object_index; unit->unit.grenade_throw_state = _unit_grenade_throw_in_hand; + /* port: the host takes a client's report of the grenade's damage only + from a grenade thrown (network_damage.c) */ + network_damage_note_grenade(unit_index, unit->unit.current_grenade_index); } else { @@ -8507,6 +8627,8 @@ boolean unit_start_animation_impulse( { unit_align_facing(unit_index, alignment_vector); } + if (unit->unit.player_index == NONE) + network_actors_note_impulse(unit_index, animation_impulse, alignment_vector); result = TRUE; } @@ -11026,6 +11148,9 @@ void unit_control( { struct unit_datum *unit = unit_get(unit_index); + if (unit->unit.player_index == NONE) + network_actors_note_control(unit_index, control_data); + match_assert( "c:\\halo\\SOURCE\\units\\units.c", 0x5e1, diff --git a/source/units/units.h b/source/units/units.h index b3502064..ba4d9c2b 100644 --- a/source/units/units.h +++ b/source/units/units.h @@ -13,6 +13,17 @@ UNITS.H /* ---------- constants */ +/* a unit's damage animations: flinches (a soft ping plays over its +animation, a hard ping in its place) and deaths */ +enum +{ + _unit_damage_animation_soft_ping = 0, + _unit_damage_animation_hard_ping, + _unit_damage_animation_soft_kill, + _unit_damage_animation_hard_kill, + NUMBER_OF_UNIT_DAMAGE_ANIMATIONS +}; + enum { MAXIMUM_WEAPONS_PER_UNIT = 4, @@ -758,6 +769,23 @@ boolean unit_is_playing_custom_animation( long unit_index); boolean unit_flying_through_air( long unit_index); +/* port: a unit that feigned death gets back up (units.c) */ +void unit_port_resurrect( + long unit_index); +/* port: whether the unit plays a flinch or death animation of the type, +switched to the host's if it has only just begun +(port/linux/game/network_objects.c) */ +boolean unit_port_correct_damage_animation( + long unit_index, + short type, + short animation_index); +/* port: network co-op (port/linux/game/network_coop.c) */ +void unit_port_play_user_animation( + long unit_index, + long animation_graph_index, + short animation_index, + boolean interpolate, + short frame_index); void unit_stop_custom_animation( long unit_index); boolean unit_melee_attack_begin( diff --git a/tools/android_build.py b/tools/android_build.py index 71b138eb..afb2b6a9 100644 --- a/tools/android_build.py +++ b/tools/android_build.py @@ -283,13 +283,23 @@ def generate_android_build(n: Writer, sln: Any) -> None: import json config: Dict[str, Any] = json.loads(config_path.read_text(encoding="utf-8")) - # the NDK ships one prebuilt toolchain per build host (linux-x86_64, - # darwin-x86_64, windows-x86_64); use whichever this NDK has prebuilt = ndk / "toolchains" / "llvm" / "prebuilt" - host_tag = {"linux": "linux-x86_64", "darwin": "darwin-x86_64"}.get(sys.platform, "linux-x86_64") - toolchain = prebuilt / host_tag - if not toolchain.is_dir(): - toolchain = next((p for p in sorted(prebuilt.iterdir()) if p.is_dir()), toolchain) + _host_tag = os.environ.get("ANDROID_NDK_HOST_TAG", "") + if not _host_tag: + if sys.platform == "darwin": + _host_tag = "darwin-x86_64" + elif os.name == "nt": + _host_tag = "windows-x86_64" + else: + _host_tag = "linux-x86_64" + if not (prebuilt / _host_tag).is_dir(): + # an NDK that names its host folder differently: take the one there is + _tags = sorted(entry.name for entry in prebuilt.iterdir() if entry.is_dir()) if prebuilt.is_dir() else [] + if not _tags: + n.comment("Android build: the NDK has no LLVM toolchain") + return + _host_tag = _tags[0] + toolchain = prebuilt / _host_tag sysroot_include = toolchain / "sysroot" / "usr" / "include" host_cc = toolchain / "bin" / f"aarch64-linux-android{ANDROID_API}-clang" ndk_bin = toolchain / "bin" @@ -462,7 +472,11 @@ def generate_android_build(n: Writer, sln: Any) -> None: game_cflags = " ".join([ guest_abi, guest_code, " ".join(game_flags), profile_flags, f"-include {prefix_header}", f"-include {semantics_header}", - f"-I{LINUX_DIR}/include", game_defines_and_includes(config), *libc_includes, f"-idirafter {XDK_INCLUDE}", + f"-I{LINUX_DIR}/include", + # the headers of the port's own game units (port/linux/game), for the + # game sources that call them + f"-iquote {Path(config['game_sources'])}", + game_defines_and_includes(config), *libc_includes, f"-idirafter {XDK_INCLUDE}", ]) for source in game_sources(config): cflags = game_cflags @@ -629,7 +643,11 @@ 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, fdm_stub]) + # internet play's MQTT brokers, in the APK: the app writes them beside + # config.toml (port/android/host/host_main.c) + staged_brokers = assets_dir / "brokers.txt" + n.build(outputs=staged_brokers, rule="android_copy", inputs=Path("port/assets/network/brokers.txt")) + n.build(outputs="android", rule="phony", inputs=[libmain, staged_sdl, staged_image, fdm_stub, staged_brokers]) apk = PORT_DIR / "app" / "build" / "outputs" / "apk" / "debug" / "app-debug.apk" n.rule( @@ -640,6 +658,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, fdm_stub]) + n.build(outputs=apk, rule="android_gradle", inputs=[libmain, staged_sdl, staged_image, fdm_stub, staged_brokers]) n.build(outputs="android_apk", rule="phony", inputs=apk) n.newline() diff --git a/tools/ce_menus.py b/tools/ce_menus.py index af8a6294..45a8a451 100644 --- a/tools/ce_menus.py +++ b/tools/ce_menus.py @@ -83,6 +83,11 @@ CHILD_OFFSETS = { **{("ui\\shell\\main_menu\\multiplayer_type_select\\join_game\\join_game_items_list", f"ui\\shell\\main_menu\\multiplayer_type_select\\join_game\\server_item_{row}"): (10, 102 + 17 * (row - 1)) for row in range(1, 16)}, + # the map lists' first row, the SINGLEPLAYER or MULTIPLAYER chooser + # (port_settings.MAP_KIND_CHOOSER), where the gametype list has its own + **{(f"ui\\shell\\main_menu\\{list_tag}", "ui\\shell\\main_menu\\new_select\\list_item_0"): (82, 73) + for list_tag in ("multiplayer_type_select\\mp_map_select\\mp_map_select_list_2", + "solo_level_select\\solo_level_select_list")}, } SCALE = 4 MAXIMUM_SIZE = 2048 diff --git a/tools/ci_build.py b/tools/ci_build.py index 6e67e362..ef153ad5 100644 --- a/tools/ci_build.py +++ b/tools/ci_build.py @@ -98,6 +98,10 @@ def main() -> int: # the menus' XML parser (port/third_party/expat), in every build, whose # MIT license asks copies to carry its notice shutil.copy2(ROOT / "port/third_party/expat/COPYING", dist / "expat-COPYING.txt") + # internet play's MQTT brokers, a file beside the game (network.brokers_file; + # Android's APK has its own copy) + if args.platform != "android": + shutil.copy2(ROOT / "port/assets/network/brokers.txt", dist / "brokers.txt") return 0 diff --git a/tools/linux_build.py b/tools/linux_build.py index 4dd3ad70..ee68091b 100644 --- a/tools/linux_build.py +++ b/tools/linux_build.py @@ -401,6 +401,9 @@ def generate_linux_build(n: Writer, sln: Any) -> None: f"-include {prefix_header}", f"-include {semantics_header}", f"-I{port_include}", + # the headers of the port's own game units (port/linux/game), for + # the game sources that call them + f"-iquote {Path(config['game_sources'])}", game_defines_and_includes(config), sdk_flags, ]) @@ -503,5 +506,9 @@ def generate_linux_build(n: Writer, sln: Any) -> None: cflags, ldflags = lto_flags(sln, build_dir / "thinlto-cache") cflags += profile_use_flags(profile) emit(obj_dir, output, cflags, ldflags, [profile] if profile else []) - n.build(outputs="linux", rule="phony", inputs=output) + # internet play's MQTT brokers, a file beside the game (network.brokers_file) + brokers = build_dir / "brokers.txt" + n.rule(name="linux_copy", command="cp $in $out", description="LINUX COPY $out") + n.build(outputs=brokers, rule="linux_copy", inputs=Path("port/assets/network/brokers.txt")) + n.build(outputs="linux", rule="phony", inputs=[output, brokers]) n.newline() diff --git a/tools/port_settings.py b/tools/port_settings.py index b30cf026..c52e7089 100644 --- a/tools/port_settings.py +++ b/tools/port_settings.py @@ -29,14 +29,25 @@ SCREENS = { "screen": "video_settings_screen", "header": ("header_profile_video_settings", f"{PE}/video_settings/header_profile_video_settings"), "spacing": 26, + # (rows in the place of the row before them: Window Size in + # Resolution's, port/linux/game/menu_functions.c showing the one the + # display mode chosen uses) + "same_place": ["display.window_size"], "rows": [ ("DISPLAY MODE:", "display.mode", [("FULLSCREEN", "fullscreen"), ("BORDERLESS", "borderless"), ("WINDOWED", "windowed")], - "Fullscreen and borderless draw at the display's\nresolution; windowed, 640x480 scaled. F11: window.", + "Fullscreen takes the display; borderless covers it\nwith a window. F11 switches to the window.", "desktop"), - ("WINDOW SIZE:", "display.window_scale", - [("640 x 480", "1"), ("1280 x 960", "2"), ("1920 x 1440", "3"), ("2560 x 1920", "4")], - "The window's size when windowed (its edges can\nalso be dragged).", "desktop"), + # (port/linux/game/menu_tags.c adds the display's resolutions) + ("RESOLUTION:", "display.resolution", [("NATIVE", "native")], + "What fullscreen and borderless draw at. Fullscreen\nsets the display to it; borderless scales it.", + "desktop"), + # (port/linux/game/menu_tags.c puts the sizes that fit the desktop + # in place of this one) + ("WINDOW SIZE:", "display.window_size", [("1280 x 960", "1280x960")], + "The window's size: 4:3, then 16:10, 16:9 and 21:9\n(its edges can also be dragged).", "desktop"), + ("RESOLUTION SCALING:", "display.resolution_scaling", [("NATIVE", "native"), ("ORIGINAL", "original")], + "Native draws at the resolution; Original draws\nthe Xbox's 640x480 and scales it up.", "desktop"), ("V-SYNC:", "display.vsync", ON_OFF, "Wait for the display between frames, so that the\npicture never tears.", None), ("FRAME RATE LIMIT:", "display.max_fps", @@ -191,8 +202,10 @@ def _screen(folder: str, spec: dict, rows: list, list_inputs: list, list_handler ("height", 60), ("string_list", f"{base}/help_strings"), ("font", "ui\\large_ui"), ("color", "#FFFFFFFF")], []) children = [f'' for name in list_inputs] + list_handlers - for index, (row, platform) in enumerate(rows): - children.append(f'') + # (each row: its widget, platform, and the place it is in, else the next) + for index, (row, platform, *place) in enumerate(rows): + y = 73 + (place[0] if place else index) * spec["spacing"] + children.append(f'') children.append(f'') lines += _widget(f"{base}/options_menu", [("type", "column_list"), ("width", 640), ("height", 480), @@ -211,10 +224,13 @@ def _screen(folder: str, spec: dict, rows: list, list_inputs: list, list_handler def _setting_screen(folder: str, spec: dict) -> list: base = f"{PE}/{folder}" rows, extra = [], [] + place = -1 for index, (label, setting, choices, _, platform) in enumerate(spec["rows"]): key = setting.split(".", 1)[1] row = f"{base}/op_{key}" - rows.append((row, platform)) + if setting not in spec.get("same_place", ()): + place += 1 + rows.append((row, platform, place)) extra += _widget(row, [("width", 512), ("height", 28), ("flags", "pass_unhandled_to_focused_child"), ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF"), ("platform", platform)], [f'', @@ -399,6 +415,9 @@ STRING_OVERRIDES.update({ "SHORT RANGE", "HUMAN", "COVENANT", "CLASSIC", "HEAVY WEAPONS", "NO GRENADES"], }) +# the map lists' first row: SINGLEPLAYER or MULTIPLAYER maps (_map_kind) +MAP_KIND_CHOOSER = "main_menu/new_select/list_item_0_map_kind" + # changes to the PC version's widgets (by our names): attributes set, all # their handlers replaced, children added WIDGET_PATCHES = { @@ -423,6 +442,32 @@ WIDGET_PATCHES = { f"{MT}/multiplayer_type_select_list": {"insert_before": { f"{MT}/multiplayer_type_gametypes_item": [f''], }}, + # (the Map screen's and New Game's lists: SINGLEPLAYER or MULTIPLAYER + # maps, the first row's chooser's (_map_kind); B on the Map screen goes + # from a co-op level's difficulties back to the level, else out of the + # screen: menu_functions.c's map list) + f"{MT}/mp_map_select/mp_map_select_list_2": {"swap": {"main_menu/new_select/list_item_0": MAP_KIND_CHOOSER}, + "handlers": [ + '', + '', + f'', + '', + '', + ]}, + "main_menu/solo_level_select/solo_level_select_list": {"swap": {"main_menu/new_select/list_item_0": MAP_KIND_CHOOSER}}, + # (each description shows the other kind's maps too: a campaign level's + # picture, name and words on the Map screen, a multiplayer map's on New + # Game's) + f"{MT}/mp_map_select/mp_map_right_item": {"children": [ + '', + '', + '', + ]}, + "main_menu/solo_level_select/replay_level_right_item": {"children": [ + f'', + f'', + f'', + ]}, f"{MT}/join_game/header_join_game": {"children": [ f'', f'', @@ -461,6 +506,7 @@ TITLES = { 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", + f"{MT}/lobby/header_add_player": "ADD PLAYER", } @@ -611,6 +657,66 @@ def _server_settings() -> list: ("header_bounds", "7 -6 19 0"), ("footer_bounds", "7 150 19 156")], ['']) rows.append((f"{base}/op_listing", None)) + # co-op's FRIENDLY FIRE (between its players), in the place of the + # gametype's rows, which co-op hides as multiplayer hides it, laid out + # as Teamplay Options' (menu_functions.c's server_settings_update); its + # choice kept in network.coop_friendly_fire + extra += _widget(f"{base}/op_friendly_fire", [("width", 512), ("height", 28), + ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], + [f'', + f'']) + extra += _widget(f"{base}/friendly_fire_label", [("type", "text"), ("controller", 1), ("width", 300), + ("height", 22), ("string_list", f"{base}/labels"), + ("string_index", 10), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("text_x", 13), ("text_y", 4)], []) + extra += _widget(f"{base}/friendly_fire_spinner", + [("type", "spinner"), ("top", 2), ("width", 206), ("height", 20), + ("flags", "pass_unhandled_to_focused_child left_right_tabs_items"), + ("strings", "OFF|ON|SHIELD ONLY|EXPLOSIVES ONLY"), ("setting", "network.coop_friendly_fire"), + ("values", "|".join(COOP_FRIENDLY_FIRE_VALUES)), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("align", "center"), ("text_y", 4), + ("header_bitmap", "bitmaps/arrow_sm_left"), ("footer_bitmap", "bitmaps/arrow_sm_right"), + ("header_bounds", "7 -13 19 -7"), ("footer_bounds", "7 208 19 214")], + ['', '', + '']) + rows.append((f"{base}/op_friendly_fire", None, 4)) + # ... and its EXTRA ENEMIES (port/linux/game/coop_enemies.c), below it: + # NONE, PER PLAYER or STATIC MULTIPLIER, and below that the amount of the + # one chosen (its own row, the other hidden: menu_functions.c's + # server_settings_update); each kept in its network.coop_enemies setting + def spinner_row(key, label_index, strings, setting, values): + nonlocal extra + extra += _widget(f"{base}/op_{key}", [("width", 512), ("height", 28), + ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], + [f'', + f'']) + extra += _widget(f"{base}/{key}_label", [("type", "text"), ("controller", 1), ("width", 300), + ("height", 22), ("string_list", f"{base}/labels"), + ("string_index", label_index), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("text_x", 13), ("text_y", 4)], []) + extra += _widget(f"{base}/{key}_spinner", + [("type", "spinner"), ("top", 2), ("width", 206), ("height", 20), + ("flags", "pass_unhandled_to_focused_child left_right_tabs_items"), + ("strings", "|".join(strings)), ("setting", setting), + ("values", "|".join(str(value) for value in values)), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("align", "center"), ("text_y", 4), + ("header_bitmap", "bitmaps/arrow_sm_left"), ("footer_bitmap", "bitmaps/arrow_sm_right"), + ("header_bounds", "7 -13 19 -7"), ("footer_bounds", "7 208 19 214")], + ['', + '', + '']) + + spinner_row("extra_enemies", 11, ["NONE", "PER PLAYER", "STATIC MULTIPLIER"], "network.coop_enemies_mode", + COOP_ENEMIES_MODES) + rows.append((f"{base}/op_extra_enemies", None, 5)) + spinner_row("enemies_per_player", 12, [f"{value}%" for value in COOP_ENEMIES_PERCENTAGES], + "network.coop_enemies", COOP_ENEMIES_PERCENTAGES) + rows.append((f"{base}/op_enemies_per_player", None, 6)) + spinner_row("enemies_multiplier", 13, [f"{value}X" for value in COOP_ENEMIES_MULTIPLIERS], + "network.coop_enemies_multiplier", COOP_ENEMIES_MULTIPLIERS) + rows.append((f"{base}/op_enemies_multiplier", None, 6)) # 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): @@ -629,7 +735,7 @@ def _server_settings() -> list: extra += _widget(f"{base}/{key}_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")], []) - rows.append((f"{base}/op_{key}", None)) + rows.append((f"{base}/op_{key}", None, 4 + index)) extra += _button(f"{base}/button_defaults", 3, []) extra += _widget(f"{base}/button_ok", [("type", "text"), ("width", 128), ("height", 24), ("bitmap", "bitmaps/text_button_background"), ("text", "START GAME"), @@ -640,7 +746,8 @@ def _server_settings() -> list: '']) extra += _strings(f"{base}/labels", ["GAME NAME:", "MAXIMUM PLAYERS:", "INVITE LINK:", "GAME TYPE:", "PLAYER OPTIONS:", "ITEM OPTIONS:", "VEHICLE OPTIONS:", "INDICATOR OPTIONS:", - "TEAMPLAY OPTIONS:", "LISTING:"]) + "TEAMPLAY OPTIONS:", "LISTING:", "FRIENDLY FIRE:", "EXTRA ENEMIES:", + "PER PLAYER:", "MULTIPLIER:"]) extra += _strings(f"{base}/help_strings", [ "", "The name the game shows in the lists of games.\\nEnter changes it.", @@ -655,6 +762,18 @@ def _server_settings() -> list: # (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.", + # (FRIENDLY FIRE's, by its choice, as Teamplay Options' words them) + "Players can not be hurt by weapons and explosives\\nfired by the other players.", + "Players can be hurt by weapons or explosives\\nfired by the other players.", + "Damage from the other players will only reduce\\nshields. Health will be unaffected.", + "Players can be hurt by damage from explosives\\nfired by the other players.", + # (EXTRA ENEMIES', by its choice: COOP_ENEMIES_MODES' order) + "Enemy squads are as the campaign has them.", + "Enemy squads grow with the players: by the amount\\nbelow for each player past the first.", + "Enemy squads are the amount below times as large,\\nhowever many players there are.", + # (PER PLAYER's and MULTIPLIER's) + "For each player past the first, enemy squads get\\nthis much more of themselves (100%: as many again).", + "Each enemy squad is this many times as large.", ]) lines = _screen(base, spec, rows, ["server settings update"], [''], extra) @@ -667,6 +786,15 @@ def _server_settings() -> list: MAXIMUM_PLAYERS = [2, 4, 8, 12, 16, 24, 32, 48, 64, 96, 128] +# network.coop_friendly_fire's values, as Server Setup's FRIENDLY FIRE shows +# them (menu_functions.c's cooperative_friendly_fire_modes, in this order) +COOP_FRIENDLY_FIRE_VALUES = ["off", "on", "shields_only", "explosives_only"] +# Server Setup's EXTRA ENEMIES: network.coop_enemies_mode's values (in +# coop_enemies.c's order), and the amounts of PER PLAYER (network.coop_enemies, +# percentages) and STATIC MULTIPLIER (network.coop_enemies_multiplier) +COOP_ENEMIES_MODES = ["none", "per_player", "multiplier"] +COOP_ENEMIES_PERCENTAGES = [25, 50, 100, 150, 200] +COOP_ENEMIES_MULTIPLIERS = [2, 4, 8, 16, 32] # Server Setup's rows of the gametype's options: the gametype editor's # screens SETUP_OPTION_SCREENS = [ @@ -684,32 +812,58 @@ def _lobby() -> list: pregame's functions, which need no widget of theirs): up to the port's 128 players, scrolling; the game's map and gametype; the countdown""" base = f"{MT}/lobby" + # (split screen: another controller's START joins it, its B leaves alone: + # menu_functions.c's lobby_join. After a game it is the only screen, + # network_game_reset_to_pregame_ui opening it in place of the Xbox's + # pregame: leaving it goes to the main menu, as the PC version's postgame + # screens do) lines = _widget(f"{base}/lobby_screen", [("width", 640), ("height", 480), - ("flags", "pass_unhandled_to_focused_child"), + ("flags", "pass_unhandled_to_focused_child main_menu_if_no_history"), ("bitmap", "bitmaps/gradient")], - ['', + ['', + '', '', - '', - '', + '', + '', + f'', '', f'', - f'']) + f'', + f'']) lines += _header(f"{base}/header_lobby", f"{base}/header_lobby") - rows = [f'' - for index in range(11)] + lines += _widget(f"{base}/lobby_join_help", [("type", "text"), ("controller", 1), ("left", 355), ("top", 446), + ("width", 275), ("height", 24), ("font", "ui\\small_ui"), + ("color", "#FF2896FF"), ("align", "right"), ("text_y", 5), + ("text_flags", "no_focus_test")], []) + # (its rows: any controller's left and right switch its own player's team, + # on the rows only, so that they move along the buttons) + rows = [f'' for index in range(11)] lines += _widget(f"{base}/lobby_list", [("type", "column_list"), ("width", 640), ("height", 480), ("flags", "pass_unhandled_to_focused_child up_down_tabs_children"), ("description", f"{base}/lobby_desc")], ['', '', - '', *rows, + *rows, f'']) + 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)], + ['', + '', + '']) lines += _widget(f"{base}/lobby_desc", [("width", 640), ("height", 480)], ['', f'']) lines += _widget(f"{base}/lobby_right_item", [("controller", 1), ("left", 406), ("top", 75), ("width", 162), ("height", 326), ("bitmap", "bitmaps/spinner_list_3_wide_item_background")], + # (a campaign level's picture and name in place of the + # map's, for network co-op: menu_functions.c's + # lobby_map_show) [f'', f'', + '', + '', f'']) lines += _widget(f"{base}/lobby_map_pic", [("controller", 1), ("left", 419), ("top", 87), ("width", 140), ("height", 114), ("bitmap", "ui\\shell\\bitmaps\\mp_map_grafix")], []) @@ -721,10 +875,16 @@ def _lobby() -> list: ("color", "#FF2896FF")], []) lines += _widget(f"{base}/lobby_button_bar", [("type", "column_list"), ("width", 640), ("height", 28), ("flags", "pass_unhandled_to_focused_child left_right_tabs_items")], - [f'', - f'', + # (SWITCH TEAM first: hidden in a game without teams, which + # moves the focus to the first shown, START NOW; at the end + # it traps none) + [f'', + f'', + f'', f'']) for key, caption, handlers in ( + ("add", "ADD PLAYER", ['', + '']), ("team", "SWITCH TEAM", ['', '']), ("start", "START NOW", ['', '']), @@ -735,17 +895,49 @@ def _lobby() -> list: ("text", caption), ("font", "ui\\small_ui"), ("color", "#FFFFFFFF"), ("align", "center"), ("text_y", 2)], handlers + ['']) + # a split screen player's profile, chosen as they join the lobby (any + # controller's presses: Co-op's rows; the host's countdown waits) + lines += _widget(f"{base}/player_profile_screen", [("width", 640), ("height", 480), + ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/gradient")], + ['', + '', + f'', + f'', + f'']) + lines += _header(f"{base}/header_add_player", f"{base}/header_add_player") + lines += _widget(f"{base}/player_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")], + ['', + '', + '', + '', + *[f'' + for index in range(11)], + f'']) + lines += _widget(f"{base}/player_profile_help", [("type", "text"), ("width", 350), ("height", 24), + ("text", "The new player's profile."), + ("font", "ui\\small_ui"), ("color", "#FF2896FF"), ("text_y", 5), + ("text_flags", "no_focus_test")], []) # a game under way's lobby, before joining it (the browser's rows of - # games in progress): what its advertisement tells, JOIN GAME + # games in progress): what its advertisement tells, JOIN GAME. Split + # screen players join here, as in the lobby, before JOIN GAME: the game + # starts at once for the machine, with the players it brings lines += _widget(f"{base}/preview_screen", [("width", 640), ("height", 480), ("flags", "pass_unhandled_to_focused_child"), ("bitmap", "bitmaps/gradient")], - ['', '', + ['', + '', + '', + f'', '', f'', - f'']) + f'', + f'']) lines += _widget(f"{base}/preview_list", [("type", "column_list"), ("width", 640), ("height", 480), - ("flags", "pass_unhandled_to_focused_child left_right_tabs_items"), + ("flags", "pass_unhandled_to_focused_child up_down_tabs_children"), ("description", f"{base}/lobby_desc")], ['', f'', @@ -754,9 +946,12 @@ def _lobby() -> list: ("font", "ui\\large_ui"), ("color", "#FF2896FF")], []) lines += _widget(f"{base}/preview_button_bar", [("type", "column_list"), ("width", 640), ("height", 28), ("flags", "pass_unhandled_to_focused_child left_right_tabs_items")], - [f'', + [f'', + f'', f'']) - for key, caption, run in (("join", "JOIN GAME", "port lobby preview join"), ("back", "BACK", "mouse emit back event")): + for key, caption, run in (("join", "JOIN GAME", "port lobby preview join"), + ("add", "ADD PLAYER", "port lobby add player"), + ("back", "BACK", "mouse emit back event")): lines += _widget(f"{base}/preview_button_{key}", [("type", "text"), ("width", 128), ("height", 24), ("bitmap", "bitmaps/text_button_background"), ("text", caption), ("font", "ui\\small_ui"), @@ -771,7 +966,9 @@ def _coop() -> list: (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)""" + New Game's levels and difficulty, which either player's controller uses + (ui_widget.c's widget_takes_events_of_controller; 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"), @@ -853,6 +1050,25 @@ def _item_options_extras() -> list: return lines +def _map_kind() -> list: + """the map lists' first row (New Game's and the Map screen's), as the + gametype list's chooser: a spinner of SINGLEPLAYER or MULTIPLAYER maps, + for either controller (split screen co-op's New Game takes both)""" + lines = _widget(MAP_KIND_CHOOSER, [("width", 256), ("height", 28), ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/option_bkds_sm"), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("align", "center"), ("text_y", 3)], + [f'']) + lines += _widget(f"{MAP_KIND_CHOOSER}_spinner", + [("type", "spinner"), ("width", 226), ("height", 22), ("flags", "left_right_tabs_items"), + ("string_list", "main_menu/new_select/var_map_kinds"), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("align", "center"), ("text_y", 1), ("list_flags", "items_from_strings"), + ("header_bitmap", "bitmaps/arrow_sm_left"), ("footer_bitmap", "bitmaps/arrow_sm_right"), + ("header_bounds", "7 -6 19 0"), ("footer_bounds", "7 226 19 232")], + ['']) + lines += _strings("main_menu/new_select/var_map_kinds", ["SINGLEPLAYER", "MULTIPLAYER"]) + return lines + + def multiplayer_files() -> dict: """the port's multiplayer widgets: the browser's additions, the server settings, the lobby""" @@ -863,6 +1079,7 @@ def multiplayer_files() -> dict: f"{MT}/server_settings".replace("/", ".") + ".xml": head + _server_settings() + ["", ""], f"{MT}/lobby".replace("/", ".") + ".xml": head + _lobby() + ["", ""], f"{MT}/coop".replace("/", ".") + ".xml": head + _coop() + ["", ""], + "main_menu/new_select".replace("/", ".") + ".port.xml": head + _map_kind() + ["", ""], "main_menu/settings_select/multiplayer_setup/item_options_edit".replace("/", ".") + ".port.xml": head + _item_options_extras() + ["", ""], } diff --git a/tools/test_death_timing.py b/tools/test_death_timing.py new file mode 100644 index 00000000..8eaee390 --- /dev/null +++ b/tools/test_death_timing.py @@ -0,0 +1,260 @@ +"""Compile actual death handlers, co-op respawn and camera code against a fake world.""" +import argparse +import os +import re +import shutil +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + + +def function(source, name): + match = re.search(r"^(?:static )?[\w *]+\b" + name + r"\(\s*[^;{]*\)\s*\{", source, re.M) + if not match: + raise ValueError(f"Function not found: {name}") + start = source.index('{', match.start()) + depth, end = 1, start + 1 + # The extracted functions have no braces inside strings/comments. + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[match.start():end] + + +PRELUDE = r''' +#include +#include +#include +#include +typedef int boolean; +typedef float real; +typedef unsigned char byte; +typedef struct {real x,y,z;} real_point3d; +typedef struct {real i,j,k;} real_vector3d; +typedef struct {real yaw,pitch;} real_euler_angles2d; +#define TRUE 1 +#define FALSE 0 +#define NONE (-1) +#define MIN(a,b) ((a)<(b)?(a):(b)) +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define FLAG(b) (1u<<(b)) +#define TEST_FLAG(v,b) ((v)&FLAG(b)) +#define match_vassert(...) ((void)0) +static struct {short loss_timer,respawn_timer;boolean lost_map,respawn,saving_map,reset_map,revert_map;long switch_to_structure_bsp_index;} main_globals; +static long main_loss_last_tick,main_respawn_last_tick; +static long tick; +static int paused,cinematic,reverts,attempts,spawns,teleports,player_count=2; +static int projectiles,enemies,airborne,vehicle_moving,spawn_failed,teleport_failed,multiplayer; +static struct {int respawn_failure,respawn_failed;} players_state,*players_globals=&players_state; +struct player_datum {long unit_index,team_index;}; +static struct player_datum players[4]; +struct object_datum {struct {real_point3d bounding_sphere_center;} object;}; +static struct object_datum objects[4]; +struct biped_datum {struct {unsigned flags;} biped;}; +static struct biped_datum biped; +struct players_vehicle_datum {struct {int unknown_state;} vehicle;}; +static struct players_vehicle_datum vehicle; +struct data_iterator {long datum_index;int next;}; +static int player_data; +/* (the network co-op paths, off: this is a local game) */ +enum {_game_connection_local,_game_connection_network_client,_game_connection_network_server}; +static short game_connection(void) {return _game_connection_local;} +static boolean network_coop_active(void) {return FALSE;} +static boolean players_respawn_network_coop(void) {return FALSE;} +static void players_respawn_at_checkpoint(void) {} +enum {_biped_airborne_bit=0,_object_mask_vehicle=1}; +static void data_iterator_new(struct data_iterator *i,int d) {(void)d;i->next=0;} +static void *data_iterator_next(struct data_iterator *i) { + if(i->next>=player_count) return NULL; + i->datum_index=i->next++;return &players[i->datum_index]; +} +static struct player_datum *player_get(long i) {assert(i>=0&&i=100&&i<104);return &objects[i-100]; +} +static struct object_datum *object_get(long i) {return object_try_and_get(i);} +static long object_get_ultimate_parent(long i) {return vehicle_moving ? 999 : i;} +static struct biped_datum *biped_try_and_get(long i) {(void)i;biped.biped.flags=airborne;return &biped;} +static void *object_try_and_get_and_verify_type(long i,int mask) { + (void)i;(void)mask;vehicle.vehicle.unknown_state=vehicle_moving;return &vehicle; +} +static int dangerous_projectiles_near_player(void) {return projectiles;} +static int any_unit_is_dangerous(void) {return 0;} +static int ai_enemies_attacking_player(void) {return enemies;} +static void player_spawn(long i) {spawns++;if(!spawn_failed) players[i].unit_index=100+i;} +static int player_teleport(long i,long target,const real_point3d *p) { + (void)i;assert(target==100);assert(p->x==100);teleports++;return !teleport_failed; +} +static long game_time_get(void) {return tick;} +static int game_time_get_paused(void) {return paused;} +static int cinematic_in_progress(void) {return cinematic;} +static int game_engine_running(void) {return multiplayer;} +static void game_state_revert(void) {reverts++;} +static real_vector3d zero_vector,*global_zero_vector3d=&zero_vector; +static void vector3d_from_euler_angles2d(real_vector3d *v,const real_euler_angles2d *a) { + (void)a;v->i=1;v->j=v->k=0; +} +static void observer_up_from_forward(const real_vector3d *f,real_vector3d *u) { + (void)f;u->k=1;u->i=u->j=0; +} +static struct {real dead_timer,multiplayer_switch_timer,singleplayer_switch_timer;} dead_camera_constants={3,15,3}; +''' +TESTS = r''' +static void reset(int count) { + int i; + memset(&main_globals,0,sizeof(main_globals));memset(&players_state,0,sizeof(players_state)); + tick=main_loss_last_tick=main_respawn_last_tick=0; + paused=cinematic=reverts=attempts=spawns=teleports=0; + projectiles=enemies=airborne=vehicle_moving=spawn_failed=teleport_failed=multiplayer=0; + player_count=count; + for(i=0;i None: # 32-bit address space "-Wl,/LARGEADDRESSAWARE", "-Wl,/STACK:0x800000", - "-Wl,/SUBSYSTEM:CONSOLE", ] + if getattr(sln, "port_release", False): + # no console window (the port's log goes to halo.log instead, + # win32_posix.c): under Wine (Proton, gamescope) the console window + # can hide the game's window + base_ldflags += ["-Wl,/SUBSYSTEM:WINDOWS", "-Wl,/ENTRY:mainCRTStartup"] + else: + base_ldflags += ["-Wl,/SUBSYSTEM:CONSOLE"] def emit(obj_dir: Path, output: Path, extra_cflags: List[str], extra_ldflags: List[str], extra_objects: List[Path], implicit_inputs: List[Path]) -> None: @@ -340,6 +346,9 @@ def generate_windows_build(n: Writer, sln: Any) -> None: # 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'}", + # the headers of the port's own game units (port/linux/game), for + # the game sources that call them + f"-iquote {Path(linux_config['game_sources'])}", 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 @@ -477,5 +486,8 @@ def generate_windows_build(n: Writer, sln: Any) -> None: emit(obj_dir, output, lto_cflags + profile_use_flags(profile), lto_cflags + [OPTIMISATION] if lto_cflags else [], [], [profile] if profile else []) n.build(outputs=sdl_dll, rule="windows_copy", inputs=SDL_DIR / "lib" / "x86" / "SDL3.dll") - n.build(outputs="windows", rule="phony", inputs=[output, sdl_dll]) + # internet play's MQTT brokers, a file beside the game (network.brokers_file) + brokers = BUILD / "brokers.txt" + n.build(outputs=brokers, rule="windows_copy", inputs=Path("port/assets/network/brokers.txt")) + n.build(outputs="windows", rule="phony", inputs=[output, sdl_dll, brokers]) n.newline()