From 08cd0c01d097333d918b9d1845d678ea2ff25b5d Mon Sep 17 00:00:00 2001 From: Jonas Volman Date: Sat, 26 Sep 2026 01:05:07 -0700 Subject: [PATCH 01/12] Port: add session-limit header and system link address settings halo_port_limits.h (force-included by the Linux and Windows prefix headers) defines the native builds' multiplayer limits, 128 players on up to 128 machines, and the struct network_game layout that follows from them. Game sources use it only under HALO_LINUX, so the matching build keeps the Xbox limits (16 players, 4 machines). xnet.c gains two settings: HALO_NET_ADDRESS binds the game's sockets to one local address and reports it as the machine's system link address, so several instances can share one computer on different loopback addresses; HALO_NET_BROADCAST sends the game's broadcasts to a list of addresses instead, for machines broadcasts do not reach. Co-Authored-By: Claude Opus 5.5 --- port/linux/include/halo_linux_prefix.h | 4 + port/linux/include/halo_port_limits.h | 45 +++++++ port/linux/src/xnet.c | 139 ++++++++++++++++++++- port/windows/include/halo_windows_prefix.h | 4 + 4 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 port/linux/include/halo_port_limits.h diff --git a/port/linux/include/halo_linux_prefix.h b/port/linux/include/halo_linux_prefix.h index adfd96cdb3..8a20979211 100644 --- a/port/linux/include/halo_linux_prefix.h +++ b/port/linux/include/halo_linux_prefix.h @@ -72,6 +72,10 @@ always runs and the handler is compiled out. */ #define __finally #define __leave +/* ---------- multiplayer session limits of the native builds */ + +#include "halo_port_limits.h" + /* ---------- Winsock Game code sees the XDK's Winsock under private names (see the header). The diff --git a/port/linux/include/halo_port_limits.h b/port/linux/include/halo_port_limits.h new file mode 100644 index 0000000000..886a5117a9 --- /dev/null +++ b/port/linux/include/halo_port_limits.h @@ -0,0 +1,45 @@ +/* +HALO_PORT_LIMITS.H + +Multiplayer session limits of the native builds (Windows, Linux, Android), +force-included by halo_linux_prefix.h and halo_windows_prefix.h. + +The Xbox game allows 16 players on at most 4 machines (up to 4 players each +on split screen). The native builds allow 128 players on up to 128 +machines; split screen stays at 4 players per machine. Game sources use +these values only under #ifdef HALO_LINUX, so the byte-matching MSVC build +keeps the original limits. + +128 is the largest session that fits the game's existing records: player, +machine and team indices are stored in signed chars (0..127 with NONE), and +a finishing place in 7 bits. +*/ + +#ifndef __HALO_PORT_LIMITS_H +#define __HALO_PORT_LIMITS_H + +/* ---------- session limits */ + +#define HALO_PORT_MAXIMUM_NETWORK_PLAYERS 128 +#define HALO_PORT_MAXIMUM_NETWORK_MACHINES 128 + +/* ---------- struct network_game layout + +The game settings record (struct network_game) is declared separately in +several networking and interface units; its layout follows from the limits. +Every copy checks its offsets against these values. The Xbox values (4 +machines, 16 players) are 0x226 and 0x434. */ + +#define HALO_PORT_NETWORK_MACHINE_SIZE 0x44 +#define HALO_PORT_NETWORK_PLAYER_SIZE 0x20 +#define HALO_PORT_NETWORK_GAME_MACHINES_OFFSET 0x114 +#define HALO_PORT_NETWORK_GAME_PLAYER_COUNT_OFFSET \ + (HALO_PORT_NETWORK_GAME_MACHINES_OFFSET + HALO_PORT_MAXIMUM_NETWORK_MACHINES * HALO_PORT_NETWORK_MACHINE_SIZE) +#define HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET (HALO_PORT_NETWORK_GAME_PLAYER_COUNT_OFFSET + 2) +#define HALO_PORT_NETWORK_GAME_PLAYERS_END \ + (HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET + HALO_PORT_MAXIMUM_NETWORK_PLAYERS * HALO_PORT_NETWORK_PLAYER_SIZE) +#define HALO_PORT_NETWORK_GAME_RANDOM_SEED_OFFSET (HALO_PORT_NETWORK_GAME_PLAYERS_END + 2) +#define HALO_PORT_NETWORK_GAME_LOCAL_DATA_OFFSET (HALO_PORT_NETWORK_GAME_PLAYERS_END + 0xA) +#define HALO_PORT_NETWORK_GAME_SIZE (HALO_PORT_NETWORK_GAME_PLAYERS_END + 0xE) + +#endif /* __HALO_PORT_LIMITS_H */ diff --git a/port/linux/src/xnet.c b/port/linux/src/xnet.c index fefb306e2c..84849b2698 100644 --- a/port/linux/src/xnet.c +++ b/port/linux/src/xnet.c @@ -11,13 +11,82 @@ XNet's secure addressing collapses to plain IPv4: a host's XNADDR carries its real address, key exchange keys are random but unused, and XNADDR to IN_ADDR translation is the identity. That is enough for system link play on a LAN. + +Two settings adjust the addressing: + +- HALO_NET_ADDRESS=a.b.c.d binds the game's sockets to that local address + instead of every interface, and reports it as this machine's system link + address. Several instances can then share one computer, each on its own + loopback address (127.0.0.2, 127.0.0.3, ...), or system link can be + pinned to one network interface. +- HALO_NET_BROADCAST=a.b.c.d[,e.f.g.h...] sends the game's broadcasts (a + client's system link game search, a host's game advertisement) to those + addresses instead of 255.255.255.255, to reach machines that broadcasts + do not: other loopback addresses, or machines across a VPN. */ #include "platform.h" #include "posix.h" +#include #include +/* ---------- address settings */ + +static int address_setting(const char *name, unsigned long *address) +{ + const char *text = getenv(name); + unsigned long value; + + if (!text || !*text) + return 0; + /* INADDR_NONE (also 255.255.255.255) is no use as either setting */ + value = halo_ws_inet_addr(text); + if (value == INADDR_NONE) + return 0; + *address = value; + return 1; +} + +/* the address to use in place of INADDR_ANY, if HALO_NET_ADDRESS is set */ +static int local_address_setting(unsigned long *address) +{ + return address_setting("HALO_NET_ADDRESS", address); +} + +enum +{ + MAXIMUM_BROADCAST_TARGETS = 256, +}; + +/* the addresses to send broadcasts to instead, if HALO_NET_BROADCAST is +set; returns their count */ +static int broadcast_targets(unsigned long *targets, int maximum_count) +{ + const char *text = getenv("HALO_NET_BROADCAST"); + int count = 0; + + while (text && *text && count < maximum_count) + { + char address[16]; + size_t length = strcspn(text, ","); + unsigned long value; + + if (length < sizeof(address)) + { + memcpy(address, text, length); + address[length] = 0; + value = halo_ws_inet_addr(address); + if (value != INADDR_NONE) + targets[count++] = value; + } + text += length; + if (*text == ',') + text++; + } + return count; +} + /* ---------- Winsock */ static int winsock_result(int result) @@ -79,11 +148,40 @@ int WSAAPI halo_ws_closesocket(SOCKET socket) int WSAAPI halo_ws_bind(SOCKET socket, const struct sockaddr *address, int address_length) { + struct sockaddr_in local; + unsigned long override; + + if (address && address->sa_family == AF_INET && address_length >= (int)sizeof(local) && + ((const struct sockaddr_in *)address)->sin_addr.s_addr == INADDR_ANY && + local_address_setting(&override)) + { + memcpy(&local, address, sizeof(local)); + local.sin_addr.s_addr = override; + address = (const struct sockaddr *)&local; + } return winsock_result(posix_socket_bind((int)socket, address, address_length)); } int WSAAPI halo_ws_connect(SOCKET socket, const struct sockaddr *address, int address_length) { + unsigned long override; + + /* a connection from an unbound socket would leave from whichever address + the route picks; with HALO_NET_ADDRESS it leaves from that address */ + if (address && address->sa_family == AF_INET && local_address_setting(&override)) + { + struct sockaddr_in bound; + int bound_length = sizeof(bound); + + if (posix_socket_getsockname((int)socket, &bound, &bound_length) < 0 || + (bound.sin_port == 0 && bound.sin_addr.s_addr == INADDR_ANY)) + { + memset(&bound, 0, sizeof(bound)); + bound.sin_family = AF_INET; + bound.sin_addr.s_addr = override; + posix_socket_bind((int)socket, &bound, sizeof(bound)); + } + } return winsock_result(posix_socket_connect((int)socket, address, address_length)); } @@ -112,6 +210,32 @@ int WSAAPI halo_ws_send(SOCKET socket, const char *buffer, int length, int flags int WSAAPI halo_ws_sendto(SOCKET socket, const char *buffer, int length, int flags, const struct sockaddr *address, int address_length) { + if (address && address->sa_family == AF_INET && address_length >= (int)sizeof(struct sockaddr_in) && + ((const struct sockaddr_in *)address)->sin_addr.s_addr == INADDR_BROADCAST) + { + unsigned long targets[MAXIMUM_BROADCAST_TARGETS]; + int target_count = broadcast_targets(targets, MAXIMUM_BROADCAST_TARGETS); + + if (target_count) + { + struct sockaddr_in target; + int index; + int result = 0; + + /* one datagram per target; the broadcast counts as sent if any is */ + memcpy(&target, address, sizeof(target)); + for (index = 0; index < target_count; index++) + { + int sent; + + target.sin_addr.s_addr = targets[index]; + sent = posix_socket_sendto((int)socket, buffer, length, flags, &target, sizeof(target)); + if (sent >= 0 || index == 0) + result = sent; + } + return winsock_result(result); + } + } return winsock_result(posix_socket_sendto((int)socket, buffer, length, flags, address, address_length)); } @@ -319,9 +443,20 @@ INT WSAAPI XNetXnAddrToInAddr(const XNADDR *address, const XNKID *key_identifier return 0; } +/* this machine's system link address: HALO_NET_ADDRESS, else the first +non-loopback IPv4 address */ +static unsigned long title_address(void) +{ + unsigned long override; + + if (local_address_setting(&override)) + return override; + return posix_local_ipv4_address(); +} + DWORD WSAAPI XNetGetTitleXnAddr(XNADDR *address) { - unsigned long ip = posix_local_ipv4_address(); + unsigned long ip = title_address(); memset(address, 0, sizeof(*address)); address->bSizeOfStruct = sizeof(*address); @@ -331,6 +466,6 @@ DWORD WSAAPI XNetGetTitleXnAddr(XNADDR *address) DWORD WSAAPI XNetGetEthernetLinkStatus(void) { - return posix_local_ipv4_address() ? + return title_address() ? (XNET_ETHERNET_LINK_ACTIVE | XNET_ETHERNET_LINK_100MBPS | XNET_ETHERNET_LINK_FULL_DUPLEX) : 0; } diff --git a/port/windows/include/halo_windows_prefix.h b/port/windows/include/halo_windows_prefix.h index 7dc57754bf..1c9e866dcd 100644 --- a/port/windows/include/halo_windows_prefix.h +++ b/port/windows/include/halo_windows_prefix.h @@ -57,6 +57,10 @@ crashes instead (port/windows/src/win32_memory_watch.c). */ #define __finally #define __leave +/* ---------- multiplayer session limits of the native builds */ + +#include "../../linux/include/halo_port_limits.h" + /* ---------- Xbox functions named like Windows functions */ #include "halo_windows_api_names.h" From d65a9b420e823d3c68cd25d43cdbbb9dd0e32f1f Mon Sep 17 00:00:00 2001 From: Jonas Volman Date: Sat, 26 Sep 2026 01:54:23 -0700 Subject: [PATCH 02/12] Port: treat HALO_NET_ADDRESS as this machine's loopback address With HALO_NET_ADDRESS set, connections and datagrams to 127.0.0.1 go to that address, and traffic from it is reported as coming from 127.0.0.1. The game uses 127.0.0.1 for itself (a host joins its own game through it, and admits only it to a split screen game), so an instance bound to another address can still host. HALO_NET_BROADCAST now takes a list of addresses: a host's game advertisement is a broadcast too, and several instances on one computer each need a copy. Co-Authored-By: Claude Opus 5.5 --- port/linux/src/xnet.c | 63 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/port/linux/src/xnet.c b/port/linux/src/xnet.c index 84849b2698..389bbecfeb 100644 --- a/port/linux/src/xnet.c +++ b/port/linux/src/xnet.c @@ -18,7 +18,11 @@ Two settings adjust the addressing: instead of every interface, and reports it as this machine's system link address. Several instances can then share one computer, each on its own loopback address (127.0.0.2, 127.0.0.3, ...), or system link can be - pinned to one network interface. + pinned to one network interface. That address then stands in for + 127.0.0.1, which the game uses for itself (a host joins its own game + through it, and admits only it to a split screen game): connections and + datagrams to 127.0.0.1 go to the address, and traffic from the address + is reported as coming from 127.0.0.1. - HALO_NET_BROADCAST=a.b.c.d[,e.f.g.h...] sends the game's broadcasts (a client's system link game search, a host's game advertisement) to those addresses instead of 255.255.255.255, to reach machines that broadcasts @@ -54,6 +58,42 @@ static int local_address_setting(unsigned long *address) return address_setting("HALO_NET_ADDRESS", address); } +/* 127.0.0.1 in network byte order */ +static unsigned long loopback_address(void) +{ + return halo_ws_htonl(0x7F000001); +} + +/* a destination of 127.0.0.1 means the HALO_NET_ADDRESS address */ +static const struct sockaddr *outgoing_address(const struct sockaddr *address, int address_length, + struct sockaddr_in *storage) +{ + unsigned long local; + + if (address && address->sa_family == AF_INET && address_length >= (int)sizeof(*storage) && + ((const struct sockaddr_in *)address)->sin_addr.s_addr == loopback_address() && + local_address_setting(&local)) + { + memcpy(storage, address, sizeof(*storage)); + storage->sin_addr.s_addr = local; + return (const struct sockaddr *)storage; + } + return address; +} + +/* traffic from the HALO_NET_ADDRESS address comes from 127.0.0.1 */ +static void incoming_address(struct sockaddr *address, const int *address_length) +{ + unsigned long local; + + if (address && address_length && *address_length >= (int)sizeof(struct sockaddr_in) && + address->sa_family == AF_INET && local_address_setting(&local) && + ((struct sockaddr_in *)address)->sin_addr.s_addr == local) + { + ((struct sockaddr_in *)address)->sin_addr.s_addr = loopback_address(); + } +} + enum { MAXIMUM_BROADCAST_TARGETS = 256, @@ -164,8 +204,10 @@ int WSAAPI halo_ws_bind(SOCKET socket, const struct sockaddr *address, int addre int WSAAPI halo_ws_connect(SOCKET socket, const struct sockaddr *address, int address_length) { + struct sockaddr_in target; unsigned long override; + address = outgoing_address(address, address_length, &target); /* a connection from an unbound socket would leave from whichever address the route picks; with HALO_NET_ADDRESS it leaves from that address */ if (address && address->sa_family == AF_INET && local_address_setting(&override)) @@ -199,6 +241,7 @@ SOCKET WSAAPI halo_ws_accept(SOCKET socket, struct sockaddr *address, int *addre WSASetLastError(posix_socket_last_error()); return INVALID_SOCKET; } + incoming_address(address, address_length); return (SOCKET)result; } @@ -236,6 +279,12 @@ int WSAAPI halo_ws_sendto(SOCKET socket, const char *buffer, int length, int fla return winsock_result(result); } } + else + { + struct sockaddr_in target; + + address = outgoing_address(address, address_length, &target); + } return winsock_result(posix_socket_sendto((int)socket, buffer, length, flags, address, address_length)); } @@ -247,7 +296,11 @@ int WSAAPI halo_ws_recv(SOCKET socket, char *buffer, int length, int flags) int WSAAPI halo_ws_recvfrom(SOCKET socket, char *buffer, int length, int flags, struct sockaddr *address, int *address_length) { - return winsock_result(posix_socket_recvfrom((int)socket, buffer, length, flags, address, address_length)); + int result = posix_socket_recvfrom((int)socket, buffer, length, flags, address, address_length); + + if (result >= 0) + incoming_address(address, address_length); + return winsock_result(result); } int WSAAPI halo_ws_shutdown(SOCKET socket, int how) @@ -286,7 +339,11 @@ int WSAAPI halo_ws_getsockname(SOCKET socket, struct sockaddr *address, int *add int WSAAPI halo_ws_getpeername(SOCKET socket, struct sockaddr *address, int *address_length) { - return winsock_result(posix_socket_getpeername((int)socket, address, address_length)); + int result = posix_socket_getpeername((int)socket, address, address_length); + + if (result >= 0) + incoming_address(address, address_length); + return winsock_result(result); } /* ---------- select and fd_set */ From db207ea1a427a8a266efae00c0364685eefc8632 Mon Sep 17 00:00:00 2001 From: Jonas Volman Date: Sat, 26 Sep 2026 01:54:23 -0700 Subject: [PATCH 03/12] Networking: 128 players on up to 128 machines in the native builds The Xbox game allows 16 players on 4 machines (4 split screen players each). The native builds raise both limits to 128, keeping 4 players per machine, and leave the byte-matching build unchanged: every change is under HALO_LINUX (the MSVC objects compare identical). - Limits: every local copy of the player and machine limits, and the bare 4s and 16s that stood for them, follow halo_port_limits.h. The player data array, the machine-to-player table (and its reset) and the per-tick update arrays grow with them. - Records: the copies of struct network_game agree on the larger layout and check it against halo_port_limits.h; maximum_player_count becomes a byte (128 does not fit a signed char); network_player_is_valid checks the machine against the machine limit, not the split screen one; a machine's input carries MAXIMUM_LOCAL_PLAYERS actions (16/4 would have become 128/128). - Protocol: a message holds at most 4095 bytes (its header has a 12-bit length), so the 13 KB game settings record is sent in 3.5 KB pieces that clients reassemble; the per-tick update of 128 players (3,857 bytes) fits. Buffers and the packet codec's limits grow to match, and the game search uses protocol version 2 so these builds and the Xbox game never see each other's games. - Connections: the host accepts a client connection per machine (the split screen count stood in for it), FD_SETSIZE is 256, stream writes send all of a message or drop a peer that stops reading instead of corrupting the stream, and the input and update queues are larger. - Host: lobby changes are sent at most every 250 ms instead of on every change (a full lobby would otherwise send ~650 MB), in-game joins wait in a queue instead of being dropped, the random name and colour pickers stop looping forever once names or colours run out, machines get 60 s to load a map, and the debug dump lists only the slots in use (128 empty slots took seconds to log). Co-Authored-By: Claude Opus 5.5 --- port/linux/include/halo_linux_prefix.h | 6 + port/linux/include/halo_port_limits.h | 26 +++ port/windows/include/halo_windows_prefix.h | 4 + .../network/transport_endpoint_set_winsock.c | 12 ++ source/game/player_queues_new.c | 25 +++ source/game/players.c | 15 ++ source/networking/network_client_manager.c | 73 ++++++++ .../network_client_message_handler.c | 111 ++++++++++++ source/networking/network_connection.c | 86 +++++++++- source/networking/network_game_globals.c | 18 ++ source/networking/network_game_manager.c | 64 +++++-- source/networking/network_messages.c | 41 +++++ source/networking/network_server_manager.c | 158 ++++++++++++++++++ .../network_server_message_handler.c | 144 ++++++++++++++++ .../network_server_message_handler.h | 10 ++ 15 files changed, 766 insertions(+), 27 deletions(-) diff --git a/port/linux/include/halo_linux_prefix.h b/port/linux/include/halo_linux_prefix.h index 8a20979211..57d3359dcc 100644 --- a/port/linux/include/halo_linux_prefix.h +++ b/port/linux/include/halo_linux_prefix.h @@ -76,6 +76,12 @@ always runs and the handler is compiled out. */ #include "halo_port_limits.h" +/* the Xbox Winsock headers' fd_set in game units (platform units see glibc's, +which is larger) */ +#ifndef HALO_LINUX_PLATFORM_LAYER +#define FD_SETSIZE HALO_PORT_FD_SETSIZE +#endif + /* ---------- Winsock Game code sees the XDK's Winsock under private names (see the header). The diff --git a/port/linux/include/halo_port_limits.h b/port/linux/include/halo_port_limits.h index 886a5117a9..31163ad5a4 100644 --- a/port/linux/include/halo_port_limits.h +++ b/port/linux/include/halo_port_limits.h @@ -23,6 +23,11 @@ a finishing place in 7 bits. #define HALO_PORT_MAXIMUM_NETWORK_PLAYERS 128 #define HALO_PORT_MAXIMUM_NETWORK_MACHINES 128 +/* a host polls its listening socket and one socket per machine; the Xbox's +Winsock headers default to 64 (the prefix headers define FD_SETSIZE from +this before any of them is read) */ +#define HALO_PORT_FD_SETSIZE 256 + /* ---------- struct network_game layout The game settings record (struct network_game) is declared separately in @@ -42,4 +47,25 @@ machines, 16 players) are 0x226 and 0x434. */ #define HALO_PORT_NETWORK_GAME_LOCAL_DATA_OFFSET (HALO_PORT_NETWORK_GAME_PLAYERS_END + 0xA) #define HALO_PORT_NETWORK_GAME_SIZE (HALO_PORT_NETWORK_GAME_PLAYERS_END + 0xE) +/* ---------- system link protocol + +The native builds' messages differ from the Xbox game's (longer arrays, the +game settings record in fragments), so they search for games with their own +protocol version and never see the Xbox game's, or it theirs. */ + +#define HALO_PORT_NETWORK_GAME_MESSAGE_VERSION 2 + +/* a message header's 12-bit length allows messages of up to 0xFFF bytes, +header included; the per-tick update of 128 players is 3,857 */ +#define HALO_PORT_MAXIMUM_NETWORK_MESSAGE_SIZE 0x1000 + +/* the packet codec's limit on a decoded or encoded packet; the per-tick +update of 128 players decodes to 0x1010 bytes */ +#define HALO_PORT_NETWORK_PACKET_SIZE 0x1100 + +/* the game settings record (HALO_PORT_NETWORK_GAME_SIZE, 13,092 bytes at 128 +machines and players) does not fit one message; it is sent in pieces of +this many bytes (4 pieces), each 3,594 bytes on the wire */ +#define HALO_PORT_NETWORK_GAME_SETTINGS_FRAGMENT_SIZE 0xE00 + #endif /* __HALO_PORT_LIMITS_H */ diff --git a/port/windows/include/halo_windows_prefix.h b/port/windows/include/halo_windows_prefix.h index 1c9e866dcd..512a0c9072 100644 --- a/port/windows/include/halo_windows_prefix.h +++ b/port/windows/include/halo_windows_prefix.h @@ -61,6 +61,10 @@ crashes instead (port/windows/src/win32_memory_watch.c). */ #include "../../linux/include/halo_port_limits.h" +/* the Xbox Winsock headers' fd_set, for game units and the platform's XNet +alike (the Windows-facing units never see this header) */ +#define FD_SETSIZE HALO_PORT_FD_SETSIZE + /* ---------- Xbox functions named like Windows functions */ #include "halo_windows_api_names.h" diff --git a/source/bungie_net/network/transport_endpoint_set_winsock.c b/source/bungie_net/network/transport_endpoint_set_winsock.c index 211687be91..848ef77526 100644 --- a/source/bungie_net/network/transport_endpoint_set_winsock.c +++ b/source/bungie_net/network/transport_endpoint_set_winsock.c @@ -137,6 +137,12 @@ struct transport_endpoint_set long needs_compaction; }; +#ifdef HALO_LINUX +/* the native builds raise FD_SETSIZE (port prefix headers) so that a server's +set holds its listening socket and a socket for every machine */ +typedef char winsock_fd_set_size_assert[ + FD_SETSIZE >= HALO_PORT_MAXIMUM_NETWORK_MACHINES + 1 ? 1 : -1]; +#else typedef char winsock_fd_set_size_assert[ sizeof(fd_set) == 0x104 ? 1 : -1]; typedef char transport_endpoint_set_ep_array_offset_assert[ @@ -151,6 +157,7 @@ typedef char transport_endpoint_set_needs_compaction_offset_assert[ offsetof(struct transport_endpoint_set, needs_compaction) == 0x114 ? 1 : -1]; typedef char transport_endpoint_set_size_assert[ sizeof(struct transport_endpoint_set) == 0x118 ? 1 : -1]; +#endif /* ---------- prototypes */ @@ -187,7 +194,12 @@ static long get_next_available_set_array_index( 0x39, set); /* January permits one-past-capacity here; preserve the original boundary. */ +#ifdef HALO_LINUX + /* ... except in the native builds, which keep to the array */ + if (set->last_endpoint_index >= set->max_endpoints - 1) +#else if (set->last_endpoint_index > set->max_endpoints - 1) +#endif { return NONE; } diff --git a/source/game/player_queues_new.c b/source/game/player_queues_new.c index a33fe9e4c7..6252abbc33 100644 --- a/source/game/player_queues_new.c +++ b/source/game/player_queues_new.c @@ -120,8 +120,14 @@ symbols in this file: enum { +#ifdef HALO_LINUX + /* the native builds' session limits (port/linux/include/halo_port_limits.h) */ + MAXIMUM_NUMBER_OF_PLAYERS = HALO_PORT_MAXIMUM_NETWORK_PLAYERS, + MAXIMUM_NETWORK_MACHINE_COUNT = HALO_PORT_MAXIMUM_NETWORK_MACHINES, +#else MAXIMUM_NUMBER_OF_PLAYERS = 16, MAXIMUM_NETWORK_MACHINE_COUNT = 4, +#endif MAXIMUM_SERVER_UPDATES = 32, MAXIMUM_CLIENT_UPDATES = 128, @@ -200,20 +206,39 @@ struct update_client_globals typedef char player_action_collection_size_assert[ sizeof(struct player_action_collection) == 0x80 ? 1 : -1]; +#ifdef HALO_LINUX +/* the update arrays follow the session limit (the networking units' copies +of struct server_update must have the same size) */ +typedef char server_update_size_assert[ + sizeof(struct server_update) == 4 + MAXIMUM_NUMBER_OF_PLAYERS * 0x20 ? 1 : -1]; +typedef char update_size_assert[ + sizeof(struct update) == 4 + sizeof(struct server_update) ? 1 : -1]; +#else typedef char server_update_size_assert[ sizeof(struct server_update) == 0x204 ? 1 : -1]; typedef char update_size_assert[ sizeof(struct update) == 0x208 ? 1 : -1]; +#endif typedef char update_server_queue_datum_size_assert[ sizeof(struct update_server_queue_datum) == 0x28 ? 1 : -1]; typedef char update_client_queue_datum_size_assert[ sizeof(struct update_client_queue_datum) == 0x28 ? 1 : -1]; +#ifdef HALO_LINUX +typedef char update_server_globals_size_assert[ + sizeof(struct update_server_globals) == 0xC + MAXIMUM_SERVER_UPDATES * sizeof(struct update) ? 1 : -1]; +#else typedef char update_server_globals_size_assert[ sizeof(struct update_server_globals) == 0x410C ? 1 : -1]; +#endif typedef char update_server_globals_queues_offset_assert[ offsetof(struct update_server_globals, queues) == 0x8 ? 1 : -1]; +#ifdef HALO_LINUX +typedef char update_client_globals_size_assert[ + sizeof(struct update_client_globals) == 0x94 + MAXIMUM_CLIENT_UPDATES * sizeof(struct update) ? 1 : -1]; +#else typedef char update_client_globals_size_assert[ sizeof(struct update_client_globals) == 0x10494 ? 1 : -1]; +#endif typedef char update_client_globals_saved_actions_offset_assert[ offsetof(struct update_client_globals, saved_action_collection) == 0xC ? 1 : -1]; typedef char update_client_globals_current_local_player_offset_assert[ diff --git a/source/game/players.c b/source/game/players.c index fa11d097c7..41f99ac35c 100644 --- a/source/game/players.c +++ b/source/game/players.c @@ -280,8 +280,14 @@ symbols in this file: enum { +#ifdef HALO_LINUX + /* the native builds' session limits (port/linux/include/halo_port_limits.h) */ + NETWORK_GAME_MAXIMUM_PLAYER_COUNT = HALO_PORT_MAXIMUM_NETWORK_PLAYERS, + MAXIMUM_NETWORK_MACHINE_COUNT = HALO_PORT_MAXIMUM_NETWORK_MACHINES, +#else NETWORK_GAME_MAXIMUM_PLAYER_COUNT = 16, MAXIMUM_NETWORK_MACHINE_COUNT = 4, +#endif MULTIPLAYER_GAME_TEXT_YOU_WERE_TELEFRAGGED = 183, _collision_test_for_player_teleport_flags = FLAG(_collision_test_front_facing_surfaces_bit) | @@ -477,7 +483,12 @@ void players_initialize( { player_data = game_state_data_new( "players", +#ifdef HALO_LINUX + /* a player's datum index is also its action slot in every update */ + NETWORK_GAME_MAXIMUM_PLAYER_COUNT, +#else 16, +#endif sizeof(struct player_datum)); team_data = game_state_data_new( "teams", @@ -527,7 +538,11 @@ void players_initialize_for_new_map( csmemset( machine_to_player_table, NONE, +#ifdef HALO_LINUX + sizeof(machine_to_player_table)); +#else 0x40); +#endif return; } diff --git a/source/networking/network_client_manager.c b/source/networking/network_client_manager.c index e94099013c..13ab678d5f 100644 --- a/source/networking/network_client_manager.c +++ b/source/networking/network_client_manager.c @@ -397,15 +397,27 @@ symbols in this file: enum { +#ifdef HALO_LINUX + /* the native builds' session limits (port/linux/include/halo_port_limits.h) */ + MAXIMUM_NETWORK_MACHINE_COUNT = HALO_PORT_MAXIMUM_NETWORK_MACHINES, + MAXIMUM_NETWORK_ADVERTISED_GAMES = 9, + MAXIMUM_NUMBER_OF_PLAYERS = HALO_PORT_MAXIMUM_NETWORK_PLAYERS, +#else MAXIMUM_NETWORK_MACHINE_COUNT = 4, MAXIMUM_NETWORK_ADVERTISED_GAMES = 9, MAXIMUM_NUMBER_OF_PLAYERS = 16, +#endif MAXIMUM_MACHINE_NAME_LENGTH = 32, NETWORK_GAME_NAME_LENGTH = 16, TRANSPORT_NONCE_LENGTH = 8, NETWORK_PLAYER_NAME_LENGTH = 12, NETWORK_GAME_MAP_NAME_LENGTH = 0x100, +#ifdef HALO_LINUX + /* the client's buffer for any message from the server */ + MAXIMUM_NETWORK_MESSAGE_SIZE = HALO_PORT_MAXIMUM_NETWORK_MESSAGE_SIZE, +#else MAXIMUM_NETWORK_MESSAGE_SIZE = 0x800, +#endif }; enum network_game_client_state @@ -497,7 +509,12 @@ struct network_game struct game_variant variant; byte __padding10C; byte game_mode; +#ifdef HALO_LINUX + /* 128 does not fit a signed char */ + byte maximum_player_count; +#else char maximum_player_count; +#endif byte __padding10F; short difficulty; short machine_count; @@ -700,26 +717,49 @@ typedef char network_machine_size_assert[ sizeof(struct network_machine) == 0x44 ? 1 : -1]; typedef char player_profile_size_assert[ sizeof(struct player_profile) == 0x30 ? 1 : -1]; +#ifdef HALO_LINUX +/* the update and settings layouts follow the session limits +(port/linux/include/halo_port_limits.h) */ +typedef char message_server_game_update_size_assert[ + sizeof(struct message_server_game_update) == 0x10 + MAXIMUM_NUMBER_OF_PLAYERS * 0x20 ? 1 : -1]; +typedef char server_update_size_assert[ + sizeof(struct server_update) == 4 + MAXIMUM_NUMBER_OF_PLAYERS * 0x20 ? 1 : -1]; +#else typedef char message_server_game_update_size_assert[ sizeof(struct message_server_game_update) == 0x210 ? 1 : -1]; typedef char server_update_size_assert[ sizeof(struct server_update) == 0x204 ? 1 : -1]; +#endif typedef char message_server_machine_accepted_size_assert[ sizeof(struct message_server_machine_accepted) == 8 ? 1 : -1]; typedef char message_client_settings_request_size_assert[ sizeof(struct message_client_settings_request) == 0x44 ? 1 : -1]; +#ifdef HALO_LINUX +typedef char network_game_size_assert[ + sizeof(struct network_game) == HALO_PORT_NETWORK_GAME_SIZE ? 1 : -1]; +#else typedef char network_game_size_assert[ sizeof(struct network_game) == 0x434 ? 1 : -1]; +#endif typedef char network_game_map_name_offset_assert[ offsetof(struct network_game, map.name) == 0x24 ? 1 : -1]; typedef char network_game_machine_count_offset_assert[ offsetof(struct network_game, machine_count) == 0x112 ? 1 : -1]; +#ifdef HALO_LINUX +typedef char network_game_local_data_offset_assert[ + offsetof(struct network_game, local_data) == HALO_PORT_NETWORK_GAME_LOCAL_DATA_OFFSET ? 1 : -1]; +typedef char network_game_player_count_offset_assert[ + offsetof(struct network_game, player_count) == HALO_PORT_NETWORK_GAME_PLAYER_COUNT_OFFSET ? 1 : -1]; +typedef char network_game_players_offset_assert[ + offsetof(struct network_game, players) == HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET ? 1 : -1]; +#else typedef char network_game_local_data_offset_assert[ offsetof(struct network_game, local_data) == 0x430 ? 1 : -1]; typedef char network_game_player_count_offset_assert[ offsetof(struct network_game, player_count) == 0x224 ? 1 : -1]; typedef char network_game_players_offset_assert[ offsetof(struct network_game, players) == 0x226 ? 1 : -1]; +#endif typedef char network_advertised_game_update_time_offset_assert[ offsetof(struct network_advertised_game, update_time) == 0x2C ? 1 : -1]; typedef char network_advertised_game_size_assert2[ @@ -756,6 +796,22 @@ typedef char network_game_client_join_parameters_offset_assert[ offsetof(struct network_game_client, join_parameters) == 0x838 ? 1 : -1]; typedef char network_game_client_game_offset_assert[ offsetof(struct network_game_client, game) == 0x85C ? 1 : -1]; +#ifdef HALO_LINUX +/* the fields after the game settings record move with its size */ +#define NETWORK_GAME_CLIENT_TAIL_OFFSET (0x85C + HALO_PORT_NETWORK_GAME_SIZE) +typedef char network_game_client_join_in_progress_offset_assert[ + offsetof(struct network_game_client, join_in_progress) == NETWORK_GAME_CLIENT_TAIL_OFFSET ? 1 : -1]; +typedef char network_game_client_next_update_number_offset_assert[ + offsetof(struct network_game_client, next_update_number) == NETWORK_GAME_CLIENT_TAIL_OFFSET + 8 ? 1 : -1]; +typedef char network_game_client_last_broadcast_search_time_offset_assert[ + offsetof(struct network_game_client, last_broadcast_search_time) == NETWORK_GAME_CLIENT_TAIL_OFFSET + 4 ? 1 : -1]; +typedef char network_game_client_last_update_time_offset_assert[ + offsetof(struct network_game_client, last_update_time) == NETWORK_GAME_CLIENT_TAIL_OFFSET + 0xC ? 1 : -1]; +typedef char network_game_client_flags_offset_assert[ + offsetof(struct network_game_client, flags) == NETWORK_GAME_CLIENT_TAIL_OFFSET + 0x1A ? 1 : -1]; +typedef char network_game_client_last_precache_time_offset_assert[ + offsetof(struct network_game_client, last_precache_time) == NETWORK_GAME_CLIENT_TAIL_OFFSET + 0x10 ? 1 : -1]; +#else typedef char network_game_client_join_in_progress_offset_assert[ offsetof(struct network_game_client, join_in_progress) == 0xC90 ? 1 : -1]; typedef char network_game_client_next_update_number_offset_assert[ @@ -768,12 +824,23 @@ typedef char network_game_client_flags_offset_assert[ offsetof(struct network_game_client, flags) == 0xCAA ? 1 : -1]; typedef char network_game_client_last_precache_time_offset_assert[ offsetof(struct network_game_client, last_precache_time) == 0xCA0 ? 1 : -1]; +#endif typedef char message_client_broadcast_game_search_size_assert[ sizeof(struct message_client_broadcast_game_search) == 0xC ? 1 : -1]; typedef char message_client_ping_size_assert[ sizeof(struct message_client_ping) == 8 ? 1 : -1]; typedef char message_client_join_game_request_size_assert[ sizeof(struct message_client_join_game_request) == 0x50 ? 1 : -1]; +#ifdef HALO_LINUX +typedef char network_game_client_seconds_to_game_start_offset_assert[ + offsetof(struct network_game_client, seconds_to_game_start) == NETWORK_GAME_CLIENT_TAIL_OFFSET + 0x14 ? 1 : -1]; +typedef char network_game_client_error_offset_assert[ + offsetof(struct network_game_client, error) == NETWORK_GAME_CLIENT_TAIL_OFFSET + 0x18 ? 1 : -1]; +typedef char network_game_client_out_of_sync_offset_assert[ + offsetof(struct network_game_client, out_of_sync) == NETWORK_GAME_CLIENT_TAIL_OFFSET + 0x1C ? 1 : -1]; +typedef char network_game_client_connection_silent_offset_assert[ + offsetof(struct network_game_client, connection_silent) == NETWORK_GAME_CLIENT_TAIL_OFFSET + 0x1D ? 1 : -1]; +#else typedef char network_game_client_seconds_to_game_start_offset_assert[ offsetof(struct network_game_client, seconds_to_game_start) == 0xCA4 ? 1 : -1]; typedef char network_game_client_error_offset_assert[ @@ -782,6 +849,7 @@ typedef char network_game_client_out_of_sync_offset_assert[ offsetof(struct network_game_client, out_of_sync) == 0xCAC ? 1 : -1]; typedef char network_game_client_connection_silent_offset_assert[ offsetof(struct network_game_client, connection_silent) == 0xCAD ? 1 : -1]; +#endif /* ---------- prototypes */ @@ -2588,7 +2656,12 @@ static boolean network_game_client_idle_searching( message_header *message; broadcast_game_search.port = NETWORK_GAME_CLIENT_PORT; +#ifdef HALO_LINUX + /* the search version: the native builds' games are not the Xbox's */ + broadcast_game_search.__unknown2 = HALO_PORT_NETWORK_GAME_MESSAGE_VERSION; +#else broadcast_game_search.__unknown2 = 1; +#endif transport_get_nonce( broadcast_game_search.nonce, sizeof(broadcast_game_search.nonce)); diff --git a/source/networking/network_client_message_handler.c b/source/networking/network_client_message_handler.c index f179843668..a905210eba 100644 --- a/source/networking/network_client_message_handler.c +++ b/source/networking/network_client_message_handler.c @@ -206,12 +206,23 @@ symbols in this file: enum { +#ifdef HALO_LINUX + /* the native builds' protocol and session limits + (port/linux/include/halo_port_limits.h) */ + NETWORK_GAME_MESSAGE_VERSION = HALO_PORT_NETWORK_GAME_MESSAGE_VERSION, + TRANSPORT_NONCE_LENGTH = 8, + TRANSPORT_ERROR_MESSAGE_TEXT_LENGTH = 0x80, + MAXIMUM_NETWORK_MACHINE_COUNT = HALO_PORT_MAXIMUM_NETWORK_MACHINES, + NETWORK_GAME_NAME_LENGTH = 16, + MAXIMUM_NUMBER_OF_PLAYERS = HALO_PORT_MAXIMUM_NETWORK_PLAYERS, +#else NETWORK_GAME_MESSAGE_VERSION = 1, TRANSPORT_NONCE_LENGTH = 8, TRANSPORT_ERROR_MESSAGE_TEXT_LENGTH = 0x80, MAXIMUM_NETWORK_MACHINE_COUNT = 4, NETWORK_GAME_NAME_LENGTH = 16, MAXIMUM_NUMBER_OF_PLAYERS = 16, +#endif JOIN_GAME_TOKEN_LENGTH = 16, }; @@ -273,7 +284,12 @@ struct network_game struct game_variant variant; byte unknown10C; char minimum_player_count; +#ifdef HALO_LINUX + /* 128 does not fit a signed char */ + byte maximum_player_count; +#else char maximum_player_count; +#endif byte team_count; short difficulty; short machine_count; @@ -286,6 +302,24 @@ struct network_game struct network_game_local_data local_data; }; +#ifdef HALO_LINUX +typedef char network_game_players_offset_assert[ + offsetof(struct network_game, players) == HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET ? 1 : -1]; +typedef char network_game_size_assert[ + sizeof(struct network_game) == HALO_PORT_NETWORK_GAME_SIZE ? 1 : -1]; + +/* one piece of the game settings record, which no longer fits one message +(network_server_message_handler.c sends them in order) */ +struct message_server_game_settings_update +{ + word total_size; + word offset; + word length; + word pad; + byte data[HALO_PORT_NETWORK_GAME_SETTINGS_FRAGMENT_SIZE]; +}; +#endif + struct message_server_game_advertise { byte client_nonce[TRANSPORT_NONCE_LENGTH]; @@ -884,6 +918,61 @@ static boolean network_game_client_handle_message_server_machine_rejected( return result; } +#ifdef HALO_LINUX +/* the game settings record as its pieces arrive; it is applied once the last +piece is in */ +static struct network_game network_game_client_settings_staging; +static long network_game_client_settings_staging_size = 0; + +static boolean network_game_client_receive_game_settings_piece( + struct network_game_client *client, + struct message_server_game_settings_update const *piece) +{ + boolean result = TRUE; + + if (piece->total_size != sizeof(network_game_client_settings_staging) || + piece->length > sizeof(piece->data) || + piece->offset + piece->length > sizeof(network_game_client_settings_staging)) + { + network_event("got a message_server_game_settings_update piece for a different game layout"); + network_game_client_settings_staging_size = 0; + result = FALSE; + } + else + { + /* each record is sent in order from its start, over a stream */ + if (piece->offset == 0) + { + network_game_client_settings_staging_size = 0; + } + if (piece->offset != network_game_client_settings_staging_size) + { + network_event("got a message_server_game_settings_update piece out of order; waiting for the next record"); + network_game_client_settings_staging_size = 0; + } + else + { + csmemcpy( + (byte *)&network_game_client_settings_staging + piece->offset, + piece->data, + piece->length); + network_game_client_settings_staging_size += piece->length; + if (network_game_client_settings_staging_size == piece->total_size) + { + network_game_client_settings_staging_size = 0; + result = network_game_client_game_settings_updated(client, &network_game_client_settings_staging); + if (!result) + { + network_event("network_game_client_game_settings_updated() failed"); + } + } + } + } + + return result; +} +#endif + static boolean network_game_client_handle_message_server_game_settings_update( struct network_game_client *client, word *message, @@ -899,6 +988,27 @@ static boolean network_game_client_handle_message_server_game_settings_update( { if (network_game_client_get_state(client, NULL) == _network_game_client_state_pregame) { +#ifdef HALO_LINUX + struct message_server_game_settings_update piece; + short packet_type = _message_server_game_settings_update; + short packet_version = NETWORK_GAME_MESSAGE_VERSION; + + message_size -= sizeof(word); + if (decode_network_game_message( + &piece, + message + 1, + &message_size, + &packet_type, + &packet_version, + _network_game_packet_class_pregame)) + { + result = network_game_client_receive_game_settings_piece(client, &piece); + } + else + { + network_event("failed to decode a message_server_game_settings_update packet"); + } +#else struct network_game game_settings; short packet_type = _message_server_game_settings_update; short packet_version = NETWORK_GAME_MESSAGE_VERSION; @@ -922,6 +1032,7 @@ static boolean network_game_client_handle_message_server_game_settings_update( { network_event("failed to decode a message_server_game_settings_update packet"); } +#endif } else { diff --git a/source/networking/network_connection.c b/source/networking/network_connection.c index 724cb5751f..73874d49a9 100644 --- a/source/networking/network_connection.c +++ b/source/networking/network_connection.c @@ -217,9 +217,26 @@ symbols in this file: /* ---------- constants */ +/* the client connections a server accepts: one per machine, the host's own +included. The Xbox game uses the split screen player count (4), which is +also its machine count; the native builds use their machine limit +(port/linux/include/halo_port_limits.h). */ +#ifdef HALO_LINUX +#define NETWORK_CONNECTION_MAXIMUM_CLIENTS HALO_PORT_MAXIMUM_NETWORK_MACHINES +#else +#define NETWORK_CONNECTION_MAXIMUM_CLIENTS MAXIMUM_NUMBER_OF_LOCAL_PLAYERS +#endif + enum { +#ifdef HALO_LINUX + /* the per-tick update of 128 players is 3,857 bytes */ + RELIABLE_MESSAGE_MAXIMUM_SIZE = HALO_PORT_MAXIMUM_NETWORK_MESSAGE_SIZE, + /* how long a stream write waits for a peer that is not reading */ + NETWORK_CONNECTION_WRITE_TIMEOUT = 2000, +#else RELIABLE_MESSAGE_MAXIMUM_SIZE = 2048, +#endif MAXIMUM_RESERVED_NETWORK_PORT = 1023, _transport_type_udp = 0x11, _transport_type_tcp, @@ -266,7 +283,7 @@ struct network_server_connection { struct network_connection connection; struct transport_endpoint_set *endpoint_set; - struct network_connection *client_list[MAXIMUM_NUMBER_OF_LOCAL_PLAYERS]; + struct network_connection *client_list[NETWORK_CONNECTION_MAXIMUM_CLIENTS]; boolean allow_client_connections; }; @@ -753,7 +770,7 @@ void network_connection_delete( client = server->client_list; if (client) { - for (client_index = 0; client_index < MAXIMUM_NUMBER_OF_LOCAL_PLAYERS; client_index++, client++) + for (client_index = 0; client_index < NETWORK_CONNECTION_MAXIMUM_CLIENTS; client_index++, client++) { if (*client) { @@ -877,6 +894,46 @@ boolean network_connection_write( (connection->flags&FLAG(_connection_create_clientside_client_bit)) || (connection->flags&FLAG(_connection_create_serverside_client_bit))); +#ifdef HALO_LINUX + /* A stream socket may take only part of a message (the per-tick + update of 128 players is 3.9 KB): send the rest too, or the peer + loses its place in the stream. A peer that stops reading for + NETWORK_CONNECTION_WRITE_TIMEOUT is dropped rather than stalling + everyone else. */ + { + long bytes_sent = 0; + unsigned long start_time = system_milliseconds(); + + bytes_written = 0; + while (bytes_sent < buffer_size) + { + long sent = write_endpoint( + connection->reliable_endpoint, + (byte *)message + bytes_sent, + buffer_size - bytes_sent); + + if (sent > 0) + { + bytes_sent += sent; + } + else if (sent != _transport_result_operation_would_block || + system_milliseconds() - start_time >= NETWORK_CONNECTION_WRITE_TIMEOUT) + { + bytes_written = sent < 0 ? sent : _transport_error_endpoint_io; + break; + } + } + if (bytes_sent == buffer_size) + { + bytes_written = bytes_sent; + } + else if (bytes_sent > 0) + { + /* part of a message is on its way: the stream cannot recover */ + SET_FLAG(connection->flags, _connection_closed_bit, TRUE); + } + } +#else do { bytes_written = write_endpoint( @@ -885,6 +942,7 @@ boolean network_connection_write( buffer_size); } while (bytes_written <= 0 && bytes_written == _transport_result_operation_would_block); +#endif if (bytes_written > 0) { @@ -1095,12 +1153,17 @@ struct network_connection *network_connection_new( if (server) { server->allow_client_connections = TRUE; - server->endpoint_set = create_endpoint_set(MAXIMUM_NUMBER_OF_LOCAL_PLAYERS + 1); + server->endpoint_set = create_endpoint_set(NETWORK_CONNECTION_MAXIMUM_CLIENTS + 1); if (server->endpoint_set) { connection = &server->connection; reliable_queue_size = 0; +#ifdef HALO_LINUX + /* room for every machine's input datagrams between two idles */ + unreliable_queue_size = 0x20000; +#else unreliable_queue_size = 0x1900; +#endif } else { @@ -1117,7 +1180,12 @@ struct network_connection *network_connection_new( 0xB6); if (connection) { +#ifdef HALO_LINUX + /* a few seconds of per-tick updates of 128 players (3.9 KB each) */ + reliable_queue_size = 0x40000; +#else reliable_queue_size = 0x8000; +#endif unreliable_queue_size = 0x640; } } @@ -1347,7 +1415,7 @@ boolean network_server_close_client_connection( 0x1FB, server->client_list); - for (client_index = 0; client_index < MAXIMUM_NUMBER_OF_LOCAL_PLAYERS; client_index++) + for (client_index = 0; client_index < NETWORK_CONNECTION_MAXIMUM_CLIENTS; client_index++) { if (server->client_list[client_index] && server->client_list[client_index] == client_connection) @@ -1443,7 +1511,7 @@ static boolean network_connection_idle_server_reliable_endpoint( if (endpoint == connection->connection.reliable_endpoint) { if (connection->allow_client_connections && - count_endpoints_in_set(connection->endpoint_set) < MAXIMUM_NUMBER_OF_LOCAL_PLAYERS + 1) + count_endpoints_in_set(connection->endpoint_set) < NETWORK_CONNECTION_MAXIMUM_CLIENTS + 1) { struct transport_endpoint *accepted_endpoint = accept_endpoint(endpoint); struct network_connection *client_connection = NULL; @@ -1457,7 +1525,7 @@ static boolean network_connection_idle_server_reliable_endpoint( { long client_index; - for (client_index = 0; client_index < MAXIMUM_NUMBER_OF_LOCAL_PLAYERS; client_index++) + for (client_index = 0; client_index < NETWORK_CONNECTION_MAXIMUM_CLIENTS; client_index++) { if (!connection->client_list[client_index]) { @@ -1466,7 +1534,7 @@ static boolean network_connection_idle_server_reliable_endpoint( break; } } - if (client_index >= MAXIMUM_NUMBER_OF_LOCAL_PLAYERS) + if (client_index >= NETWORK_CONNECTION_MAXIMUM_CLIENTS) { error(_error_silent, "error adding new client"); } @@ -1495,7 +1563,7 @@ static boolean network_connection_idle_server_reliable_endpoint( { long client_index; - for (client_index = 0; client_index < MAXIMUM_NUMBER_OF_LOCAL_PLAYERS; client_index++) + for (client_index = 0; client_index < NETWORK_CONNECTION_MAXIMUM_CLIENTS; client_index++) { if (connection->client_list[client_index] && connection->client_list[client_index]->reliable_endpoint == endpoint) @@ -1520,7 +1588,7 @@ static boolean network_connection_idle_server_reliable_endpoint( match_vassert( "c:\\halo\\SOURCE\\networking\\network_connection.c", 0x469, - client_index < MAXIMUM_NUMBER_OF_LOCAL_PLAYERS, + client_index < NETWORK_CONNECTION_MAXIMUM_CLIENTS, "rogue endpoint connected to the server"); } } diff --git a/source/networking/network_game_globals.c b/source/networking/network_game_globals.c index e6de0338b9..e1cb76e2df 100644 --- a/source/networking/network_game_globals.c +++ b/source/networking/network_game_globals.c @@ -191,8 +191,15 @@ struct local_network_player struct network_game { +#ifdef HALO_LINUX + /* the players follow the machines, which follow the native builds' + session limits (port/linux/include/halo_port_limits.h) */ + byte __unknown0[HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET]; + struct network_player players[HALO_PORT_MAXIMUM_NETWORK_PLAYERS]; +#else byte __unknown0[0x226]; struct network_player players[16]; +#endif byte __unknown426[2]; long random_seed; long number_of_games_played; @@ -200,10 +207,17 @@ struct network_game typedef char network_machine_index_offset_assert[ offsetof(struct network_machine, machine_index) == 0x40 ? 1 : -1]; +#ifdef HALO_LINUX +typedef char network_game_players_offset_assert[ + offsetof(struct network_game, players) == HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET ? 1 : -1]; +typedef char network_game_random_seed_offset_assert[ + offsetof(struct network_game, random_seed) == HALO_PORT_NETWORK_GAME_RANDOM_SEED_OFFSET ? 1 : -1]; +#else typedef char network_game_players_offset_assert[ offsetof(struct network_game, players) == 0x226 ? 1 : -1]; typedef char network_game_random_seed_offset_assert[ offsetof(struct network_game, random_seed) == 0x428 ? 1 : -1]; +#endif struct network_game_globals { @@ -658,7 +672,11 @@ void network_game_client_local_player_quit( { player_index = 0; player_machine_index = &game->players[0].machine_index; +#ifdef HALO_LINUX + while (player_index < HALO_PORT_MAXIMUM_NETWORK_PLAYERS) +#else while (player_index < 16) +#endif { test_player = (struct network_player *)( player_machine_index - offsetof(struct network_player, machine_index)); diff --git a/source/networking/network_game_manager.c b/source/networking/network_game_manager.c index c32d084206..39f6310153 100644 --- a/source/networking/network_game_manager.c +++ b/source/networking/network_game_manager.c @@ -91,15 +91,25 @@ symbols in this file: /* ---------- constants */ +/* the machine and player slots of a network game: the Xbox's 4 and 16, or the +native builds' session limits (port/linux/include/halo_port_limits.h) */ +#ifdef HALO_LINUX +#define NETWORK_GAME_MACHINE_SLOTS HALO_PORT_MAXIMUM_NETWORK_MACHINES +#define NETWORK_GAME_PLAYER_SLOTS HALO_PORT_MAXIMUM_NETWORK_PLAYERS +#else +#define NETWORK_GAME_MACHINE_SLOTS 4 +#define NETWORK_GAME_PLAYER_SLOTS 16 +#endif + enum { - MAXIMUM_NETWORK_MACHINE_COUNT = 4, + MAXIMUM_NETWORK_MACHINE_COUNT = NETWORK_GAME_MACHINE_SLOTS, }; /* ---------- macros */ #define network_machine_is_valid(machine) \ - ((machine) && (machine)->machine_index >= 0 && (machine)->machine_index < 4) + ((machine) && (machine)->machine_index >= 0 && (machine)->machine_index < NETWORK_GAME_MACHINE_SLOTS) /* ---------- structures */ @@ -138,19 +148,31 @@ struct network_game struct game_variant variant; byte __padding10C; byte game_mode; +#ifdef HALO_LINUX + /* 128 does not fit a signed char */ + byte maximum_player_count; +#else char maximum_player_count; +#endif byte __padding10F; short difficulty; short machine_count; - struct network_machine machines[4]; + struct network_machine machines[NETWORK_GAME_MACHINE_SLOTS]; short player_count; - struct network_player players[16]; + struct network_player players[NETWORK_GAME_PLAYER_SLOTS]; short __unknown426; unsigned long random_seed; byte __unknown42C[4]; struct network_game_local_data local_data; }; +#ifdef HALO_LINUX +typedef char network_game_players_offset_assert[ + offsetof(struct network_game, players) == HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET ? 1 : -1]; +typedef char network_game_size_assert[ + sizeof(struct network_game) == HALO_PORT_NETWORK_GAME_SIZE ? 1 : -1]; +#endif + /* ---------- prototypes */ static long compare_network_players( @@ -173,7 +195,7 @@ boolean network_game_add_machine( 0x6A, game && machine && network_machine_is_valid(machine)); - for (machine_index = 0; machine_index < 4; machine_index++) + for (machine_index = 0; machine_index < NETWORK_GAME_MACHINE_SLOTS; machine_index++) { if (!network_machine_is_valid(&game->machines[machine_index])) { @@ -199,7 +221,7 @@ boolean network_game_update_machine( 0x81, game && machine && network_machine_is_valid(machine)); - for (machine_index = 0; machine_index < 4; machine_index++) + for (machine_index = 0; machine_index < NETWORK_GAME_MACHINE_SLOTS; machine_index++) { if (game->machines[machine_index].machine_index == machine->machine_index) { @@ -311,7 +333,7 @@ boolean network_game_add_player( if (VALID_INDEX(player->machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) && VALID_INDEX(player->controller_index, MAXIMUM_LOCAL_PLAYERS)) { - for (player_index = 0; player_index < 16; player_index++) + for (player_index = 0; player_index < NETWORK_GAME_PLAYER_SLOTS; player_index++) { if (game->players[player_index].machine_index == player->machine_index && game->players[player_index].controller_index == player->controller_index) @@ -320,10 +342,10 @@ boolean network_game_add_player( } } - if (player_index == 16 && network_player_is_valid(player)) + if (player_index == NETWORK_GAME_PLAYER_SLOTS && network_player_is_valid(player)) { new_player_index = NONE; - for (player_index = 0; player_index < 16; player_index++) + for (player_index = 0; player_index < NETWORK_GAME_PLAYER_SLOTS; player_index++) { if (game->players[player_index].player_list_index == NONE) { @@ -358,7 +380,13 @@ boolean network_player_is_valid( player->controller_index >= 0 && player->controller_index < MAXIMUM_LOCAL_PLAYERS && player->machine_index >= 0 && +#ifdef HALO_LINUX + /* the Xbox game checks the machine against the split screen limit, + which only works while both are 4 */ + (long)player->machine_index < MAXIMUM_NETWORK_MACHINE_COUNT) +#else player->machine_index < MAXIMUM_LOCAL_PLAYERS) +#endif { return TRUE; } @@ -380,7 +408,7 @@ void network_game_invalidate_machine( game->machines[machine_index].machine_index = NONE; game->machines[machine_index].name[0] = 0; - for (player_index = 0; player_index < 16; player_index++) + for (player_index = 0; player_index < NETWORK_GAME_PLAYER_SLOTS; player_index++) { if (game->players[player_index].machine_index == machine_index) network_game_invalidate_player(&game->players[player_index]); @@ -427,7 +455,7 @@ boolean network_game_player_is_valid( if (network_player_is_valid(player)) { current_player = &game->players[0]; - for (player_index = 0; player_index < 16; player_index++, current_player++) + for (player_index = 0; player_index < NETWORK_GAME_PLAYER_SLOTS; player_index++, current_player++) { if (current_player->machine_index == player->machine_index && current_player->controller_index == player->controller_index) @@ -483,12 +511,12 @@ void network_game_invalidate( game->machine_count = 0; game->player_count = 0; - for (machine_index = 0; machine_index < 4; machine_index++) + for (machine_index = 0; machine_index < NETWORK_GAME_MACHINE_SLOTS; machine_index++) network_game_invalidate_machine(game, (short)machine_index); csmemset(game->players, NONE, sizeof(game->players)); game->game_mode = 2; - game->maximum_player_count = 16; + game->maximum_player_count = NETWORK_GAME_PLAYER_SLOTS; game->local_data.game_objects_loaded = FALSE; return; @@ -537,7 +565,7 @@ boolean network_game_remove_player( if (network_game_player_is_valid(player, game)) { - for (player_index = 0; player_index < 16; player_index++) + for (player_index = 0; player_index < NETWORK_GAME_PLAYER_SLOTS; player_index++) { if (network_player_is_valid(&game->players[player_index]) && game->players[player_index].machine_index == player->machine_index && @@ -573,11 +601,11 @@ boolean network_game_remove_machine( if (network_machine_is_valid(machine)) { - for (machine_index = 0; machine_index < 4; machine_index++) + for (machine_index = 0; machine_index < NETWORK_GAME_MACHINE_SLOTS; machine_index++) { if (game->machines[machine_index].machine_index == machine->machine_index) { - for (player_index = 0; player_index < 16; player_index++) + for (player_index = 0; player_index < NETWORK_GAME_PLAYER_SLOTS; player_index++) { if (network_player_is_valid(&game->players[player_index]) && game->players[player_index].machine_index == machine->machine_index) @@ -651,11 +679,11 @@ boolean network_game_create_game_objects( qsort( game->players, - 16, + NETWORK_GAME_PLAYER_SLOTS, sizeof(struct network_player), (int(__cdecl *)(const void *, const void *))compare_network_players); - for (player_index = 0; player_index < 16; player_index++) + for (player_index = 0; player_index < NETWORK_GAME_PLAYER_SLOTS; player_index++) { if (!network_player_is_valid(&game->players[player_index])) break; diff --git a/source/networking/network_messages.c b/source/networking/network_messages.c index 2c8a646cef..91ee29a44d 100644 --- a/source/networking/network_messages.c +++ b/source/networking/network_messages.c @@ -207,7 +207,11 @@ struct network_game_message_packet_definitions struct data_packet_definition server_machine_accepted; struct data_packet_field server_machine_rejected_fields[2]; struct data_packet_definition server_machine_rejected; +#ifdef HALO_LINUX + struct data_packet_field server_game_settings_update_fields[4]; +#else struct data_packet_field server_game_settings_update_fields[2]; +#endif struct data_packet_definition server_game_settings_update; struct data_packet_field server_pregame_countdown_fields[2]; struct data_packet_definition server_pregame_countdown; @@ -283,7 +287,13 @@ DEFINE_NETWORK_GAME_MESSAGE(message_server_game_advertise, 0x114); DEFINE_NETWORK_GAME_MESSAGE(message_server_pong, 0x04); DEFINE_NETWORK_GAME_MESSAGE(message_server_machine_accepted, 0x08); DEFINE_NETWORK_GAME_MESSAGE(message_server_machine_rejected, 0x02); +#ifdef HALO_LINUX +/* one piece of the game settings record: total size, offset, length, then the +bytes (network_server_message_handler.c) */ +DEFINE_NETWORK_GAME_MESSAGE(message_server_game_settings_update, 8 + HALO_PORT_NETWORK_GAME_SETTINGS_FRAGMENT_SIZE); +#else DEFINE_NETWORK_GAME_MESSAGE(message_server_game_settings_update, 0x434); +#endif DEFINE_NETWORK_GAME_MESSAGE(message_server_pregame_countdown, 0x02); DEFINE_NETWORK_GAME_MESSAGE(message_server_begin_game, 0x04); DEFINE_NETWORK_GAME_MESSAGE(message_server_graceful_game_exit_pregame, 0x04); @@ -297,7 +307,12 @@ DEFINE_NETWORK_GAME_MESSAGE(message_client_player_settings_request, 0x20); DEFINE_NETWORK_GAME_MESSAGE(message_client_game_start_request, 0x02); DEFINE_NETWORK_GAME_MESSAGE(message_client_graceful_game_exit_pregame, 0x04); DEFINE_NETWORK_GAME_MESSAGE(message_client_map_is_precached_pregame, 0x100); +#ifdef HALO_LINUX +/* one 0x20-byte action per player */ +DEFINE_NETWORK_GAME_MESSAGE(message_server_game_update, 0x10 + HALO_PORT_MAXIMUM_NETWORK_PLAYERS * 0x20); +#else DEFINE_NETWORK_GAME_MESSAGE(message_server_game_update, 0x210); +#endif DEFINE_NETWORK_GAME_MESSAGE(message_server_add_player_ingame, 0x20); DEFINE_NETWORK_GAME_MESSAGE(message_server_remove_player_ingame, 0x24); DEFINE_NETWORK_GAME_MESSAGE(message_server_game_over, 0x04); @@ -357,8 +372,15 @@ struct network_game_message_packet_definitions data_0030aa68 = }, NETWORK_GAME_MESSAGE_DEFINITION(server_machine_rejected, "message_server_machine_rejected_packet", message_server_machine_rejected), { +#ifdef HALO_LINUX + DATA_PACKET_FIELD(_data_packet_field_shorts, 3), + DATA_PACKET_FIELD(_data_packet_field_pad, 2), + DATA_PACKET_FIELD(_data_packet_field_raw, HALO_PORT_NETWORK_GAME_SETTINGS_FRAGMENT_SIZE), + DATA_PACKET_FIELD_END, +#else DATA_PACKET_FIELD(_data_packet_field_raw, 1076), DATA_PACKET_FIELD_END, +#endif }, NETWORK_GAME_MESSAGE_DEFINITION(server_game_settings_update, "message_server_game_settings_update_packet", message_server_game_settings_update), { @@ -438,7 +460,11 @@ struct network_game_message_packet_definitions data_0030aa68 = { DATA_PACKET_FIELD(_data_packet_field_longs, 3), DATA_PACKET_FIELD(_data_packet_field_pad, 2), +#ifdef HALO_LINUX + DATA_PACKET_FIELD(_data_packet_field_array, HALO_PORT_MAXIMUM_NETWORK_PLAYERS), +#else DATA_PACKET_FIELD(_data_packet_field_array, 16), +#endif DATA_PACKET_FIELD(_data_packet_field_longs, 6), DATA_PACKET_FIELD(_data_packet_field_shorts, 3), DATA_PACKET_FIELD(_data_packet_field_pad, 2), @@ -578,13 +604,23 @@ struct network_game_message_packet_definitions data_0030aa68 = "network_game_messages_group", 35, 8, +#ifdef HALO_LINUX + /* the per-tick update of 128 players decodes to 0x1010 bytes */ + HALO_PORT_NETWORK_PACKET_SIZE, + HALO_PORT_NETWORK_PACKET_SIZE, +#else 0x600, 0x800, +#endif data_0030aa68.packets, }, }; +#ifdef HALO_LINUX +static byte network_game_message_buffer[HALO_PORT_MAXIMUM_NETWORK_MESSAGE_SIZE + 4]; +#else static byte network_game_message_buffer[0x604]; +#endif /* ---------- public code */ @@ -632,7 +668,12 @@ void *create_network_game_message( const void *message_struct, short message_struct_size) { +#ifdef HALO_LINUX + /* as large as the packet group lets the encoder write */ + byte encoded_message[HALO_PORT_NETWORK_PACKET_SIZE]; +#else byte encoded_message[0x600]; +#endif union network_game_message_size encoded_message_size; void *message; diff --git a/source/networking/network_server_manager.c b/source/networking/network_server_manager.c index 8845e740fb..6de779fb3e 100644 --- a/source/networking/network_server_manager.c +++ b/source/networking/network_server_manager.c @@ -485,8 +485,14 @@ symbols in this file: enum { +#ifdef HALO_LINUX + /* the native builds' session limits (port/linux/include/halo_port_limits.h) */ + MAXIMUM_NETWORK_MACHINE_COUNT = HALO_PORT_MAXIMUM_NETWORK_MACHINES, + MAXIMUM_NETWORK_PLAYER_COUNT = HALO_PORT_MAXIMUM_NETWORK_PLAYERS, +#else MAXIMUM_NETWORK_MACHINE_COUNT = 4, MAXIMUM_NETWORK_PLAYER_COUNT = 16, +#endif NETWORK_GAME_NAME_LENGTH = 16, NETWORK_GAME_MAP_NAME_LENGTH = 0x80, NETWORK_PLAYER_NAME_LENGTH = 12, @@ -494,11 +500,24 @@ enum NUMBER_OF_MULTIPLAYER_TEAMS = 2, NETWORK_GAME_PLAYER_QUIT_DELAY = 33, NETWORK_GAME_CLIENT_STALL_TIMEOUT = 2000, +#ifdef HALO_LINUX + /* the time the other machines have to load the map once the first has + finished, allowing for many machines of mixed speed */ + NETWORK_GAME_SERVER_MAXIMUM_WAIT_TIME_FOR_LEVEL_LOADING = + 60 * MILLISECONDS_PER_SECOND, +#else NETWORK_GAME_SERVER_MAXIMUM_WAIT_TIME_FOR_LEVEL_LOADING = 15 * MILLISECONDS_PER_SECOND, +#endif MAXIMUM_PLAYERS_PER_MACHINE = MAXIMUM_LOCAL_PLAYERS, PLAYER_UPDATE_SIZE = 0x20, MAXIMUM_GOOD_COLOR_ATTEMPTS = 10, +#ifdef HALO_LINUX + /* with more players than random names or colours, the pickers settle for a + numbered name or a shared colour after this many tries */ + MAXIMUM_UNIQUE_NAME_ATTEMPTS = 64, + MAXIMUM_UNIQUE_COLOR_ATTEMPTS = 64, +#endif _client_update_out_of_sync_bit = 31, CLIENT_UPDATE_SEQUENCE_NUMBER_MASK = 0x7FFFFFFF, NETWORK_GAME_COUNTDOWN_TIME = 30999, @@ -699,19 +718,41 @@ struct network_game_server boolean queued_player_valid; boolean sent_start_game_message; byte padding4BA[2]; +#ifdef HALO_LINUX + /* in-game joins waiting behind queued_player (the Xbox game keeps one + and drops any other that arrives meanwhile) */ + struct network_player waiting_players[MAXIMUM_NETWORK_PLAYER_COUNT]; + long waiting_player_count; +#endif }; +#ifdef HALO_LINUX +/* the layout follows the session limits (port/linux/include/halo_port_limits.h) */ +typedef char network_game_players_offset_assert[ + offsetof(struct network_game, players) == HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET ? 1 : -1]; +#else typedef char network_game_players_offset_assert[ offsetof(struct network_game, players) == 0x226 ? 1 : -1]; +#endif typedef char network_game_variant_has_teams_offset_assert[ offsetof(struct network_game, variant) + offsetof(struct game_variant, universal_variant.teams) == 0xC0 ? 1 : -1]; +#ifdef HALO_LINUX +typedef char network_game_size_assert[ + sizeof(struct network_game) == HALO_PORT_NETWORK_GAME_SIZE ? 1 : -1]; +typedef char network_game_server_client_machines_offset_assert[ + offsetof(struct network_game_server, client_machines) == 8 + HALO_PORT_NETWORK_GAME_SIZE ? 1 : -1]; +typedef char network_game_server_countdown_state_offset_assert[ + offsetof(struct network_game_server, countdown_state) == + 8 + HALO_PORT_NETWORK_GAME_SIZE + MAXIMUM_NETWORK_MACHINE_COUNT * 0x10 + 0xC ? 1 : -1]; +#else typedef char network_game_size_assert[ sizeof(struct network_game) == 0x434 ? 1 : -1]; typedef char network_game_server_client_machines_offset_assert[ offsetof(struct network_game_server, client_machines) == 0x43C ? 1 : -1]; typedef char network_game_server_countdown_state_offset_assert[ offsetof(struct network_game_server, countdown_state) == 0x488 ? 1 : -1]; +#endif /* ---------- prototypes */ @@ -1230,12 +1271,18 @@ boolean network_game_server_start_network_game( struct message_server_begin_game begin_game = { 0 }; void *message; +#ifdef HALO_LINUX + /* the settings record goes out in pieces */ + (void)game_settings; + if (network_game_server_send_game_settings_to_all_machines(server, &server->game, sizeof(server->game)) && +#else csmemcpy(&game_settings, &server->game, sizeof(game_settings)); if (((message = create_network_game_message( _message_server_game_settings_update, &game_settings, sizeof(game_settings))) != NULL) && network_game_server_send_message_to_all_machines(server, message) && +#endif ((message = create_network_game_message( _message_server_begin_game, &begin_game, @@ -1740,6 +1787,18 @@ void network_game_server_update_ticks( } } +#ifdef HALO_LINUX + if (!server->queued_player_valid && server->waiting_player_count > 0) + { + csmemcpy(&server->queued_player, &server->waiting_players[0], sizeof(server->queued_player)); + server->waiting_player_count--; + csmemmove( + &server->waiting_players[0], + &server->waiting_players[1], + server->waiting_player_count * sizeof(struct network_player)); + server->queued_player_valid = TRUE; + } +#endif if (server->queued_player_valid) { for (client_machine_index = 0; @@ -1797,6 +1856,13 @@ void network_game_server_queue_player_for_addition( csmemcpy(&server->queued_player, player, sizeof(server->queued_player)); server->queued_player_valid = TRUE; } +#ifdef HALO_LINUX + else if (network_player_is_valid(player) && + server->waiting_player_count < MAXIMUM_NETWORK_PLAYER_COUNT) + { + csmemcpy(&server->waiting_players[server->waiting_player_count++], player, sizeof(struct network_player)); + } +#endif return; } @@ -2416,6 +2482,14 @@ static void dump_network_game_data( long itr; for (itr = 0; itr < MAXIMUM_NETWORK_MACHINE_COUNT; itr++) { +#ifdef HALO_LINUX + /* the native builds log only the slots in use, a line each: each + line reopens the log, and 128 empty slots took seconds */ + if (network_game_data->machines[itr].machine_index == NONE) + { + continue; + } +#endif network_event( "\t%smachine %d %x", prefix, @@ -2429,6 +2503,19 @@ static void dump_network_game_data( long itr; for (itr = 0; itr < MAXIMUM_NETWORK_PLAYER_COUNT; itr++) { +#ifdef HALO_LINUX + if (network_game_data->players[itr].machine_index == NONE) + { + continue; + } + network_event("%splayer %d: machine_index %x controller_index %x team_index %x player_list_index %x", + prefix, + itr, + network_game_data->players[itr].machine_index, + network_game_data->players[itr].controller_index, + network_game_data->players[itr].team_index, + network_game_data->players[itr].player_list_index); +#else network_event("%splayer %d", prefix, itr); network_event("%s\tmachine_index %x", prefix, network_game_data->players[itr].machine_index); @@ -2438,6 +2525,7 @@ static void dump_network_game_data( network_game_data->players[itr].team_index); network_event("%s\tplayer_list_index %x", prefix, network_game_data->players[itr].player_list_index); +#endif } } @@ -2475,6 +2563,20 @@ static void network_game_server_dump( ? "(active)" : "(dead)"; } +#ifdef HALO_LINUX + if (client_machine->connection == NULL && client_machine->machine_index == NONE) + { + continue; + } + network_event("\tclient %d: connection %x %s last_received_update_sequence_number %d stall_start_time %d machine_index %x flags %x", + itr, + client_machine->connection, + connection_status, + client_machine->last_received_update_sequence_number, + client_machine->stall_start_time, + client_machine->machine_index, + client_machine->flags); +#else network_event("\tclient %d", itr); network_event("\t\tconnection %x %s", client_machine->connection, connection_status); @@ -2483,6 +2585,7 @@ static void network_game_server_dump( network_event("\t\tstall_start_time %d", client_machine->stall_start_time); network_event("\t\tmachine_index %x", client_machine->machine_index); network_event("\t\tflags %x", client_machine->flags); +#endif } network_event("\tnext_update_number %d", server->next_update_number); @@ -2681,6 +2784,9 @@ void get_unique_random_name( { wchar_t const *name; long duplicate_count; +#ifdef HALO_LINUX + long attempt_count = 0; +#endif do { @@ -2703,11 +2809,46 @@ void get_unique_random_name( } } } +#ifdef HALO_LINUX + while (duplicate_count != 0 && ++attempt_count < MAXIMUM_UNIQUE_NAME_ATTEMPTS); +#else while (duplicate_count != 0); +#endif ustrncpy(player->name, name, NETWORK_PLAYER_NAME_LENGTH - 1); player->name[NETWORK_PLAYER_NAME_LENGTH - 1] = 0; +#ifdef HALO_LINUX + /* every random name is taken: number this one ("Name2", "Name3", ...) + until it is unique */ + if (duplicate_count != 0) + { + wchar_t base_name[NETWORK_PLAYER_NAME_LENGTH]; + long number; + + csmemcpy(base_name, player->name, sizeof(base_name)); + for (number = 2; number < 1000 && !player_name_is_unique(server, player->name); number++) + { + wchar_t digits[4]; + long digit_count = 0; + long base_length = (long)ustrlen(base_name); + long value; + + for (value = number; value; value /= 10) + { + digits[digit_count++] = (wchar_t)(L'0' + value % 10); + } + base_length = MIN(base_length, NETWORK_PLAYER_NAME_LENGTH - 1 - digit_count); + csmemcpy(player->name, base_name, base_length * sizeof(wchar_t)); + while (digit_count > 0) + { + player->name[base_length++] = digits[--digit_count]; + } + player->name[base_length] = 0; + } + } +#endif + return; } @@ -2743,7 +2884,11 @@ void get_unique_random_color( attempt_count++; } +#ifdef HALO_LINUX + while (!unique && attempt_count < MAXIMUM_UNIQUE_COLOR_ATTEMPTS); +#else while (!unique); +#endif player->primary_color_index = (short)color_index; @@ -3160,6 +3305,11 @@ static boolean network_game_server_idle_pregame_tasks( { long itr; +#ifdef HALO_LINUX + /* send the lobby changes collected since the last settings update */ + network_game_server_flush_game_data_pregame(server); +#endif + for (itr = 0; itr < MAXIMUM_NETWORK_MACHINE_COUNT; itr++) { struct network_game_server_client_machine *client_machine = @@ -3320,6 +3470,9 @@ boolean network_game_server_reset_to_pregame( server->time_of_first_client_loading_completion = 0; server->sent_start_game_message = FALSE; server->queued_player_valid = FALSE; +#ifdef HALO_LINUX + server->waiting_player_count = 0; +#endif /* Preserve January's 32-bit wrap without overflowing signed arithmetic. * VC7 converts the unsigned result back to the same signed bit pattern. */ @@ -3368,6 +3521,10 @@ 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)) { +#ifdef HALO_LINUX + /* the settings record goes out in pieces */ + if (network_game_server_send_game_settings_to_all_machines(server, &server->game, sizeof(server->game))) +#else struct network_game game_settings; csmemcpy(&game_settings, &server->game, sizeof(server->game)); @@ -3376,6 +3533,7 @@ boolean network_game_server_reset_to_pregame( &game_settings, sizeof(game_settings)); if (message && network_game_server_send_message_to_all_machines(server, message)) +#endif { server->state = _network_game_server_state_pregame; success = TRUE; diff --git a/source/networking/network_server_message_handler.c b/source/networking/network_server_message_handler.c index 884913080c..e0872338df 100644 --- a/source/networking/network_server_message_handler.c +++ b/source/networking/network_server_message_handler.c @@ -262,14 +262,26 @@ symbols in this file: #include "networking/network_server_manager_internal.h" #include "networking/network_server_message_handler.h" #include "text/unicode.h" +#ifdef HALO_LINUX +/* system_milliseconds(), for the settings update interval */ +#include "cseries/cseries_windows.h" +#endif /* ---------- constants */ enum { +#ifdef HALO_LINUX + /* the native builds' session limits and protocol + (port/linux/include/halo_port_limits.h) */ + MAXIMUM_NETWORK_MACHINE_COUNT = HALO_PORT_MAXIMUM_NETWORK_MACHINES, + NETWORK_MESSAGE_BUFFER_SIZE = HALO_PORT_MAXIMUM_NETWORK_MESSAGE_SIZE, + NETWORK_GAME_MESSAGE_VERSION = HALO_PORT_NETWORK_GAME_MESSAGE_VERSION, +#else MAXIMUM_NETWORK_MACHINE_COUNT = 4, NETWORK_MESSAGE_BUFFER_SIZE = 0x600, NETWORK_GAME_MESSAGE_VERSION = 1, +#endif MAXIMUM_MACHINE_NAME_LENGTH = 32, NETWORK_GAME_MAP_NAME_LENGTH = 0x100, JOIN_GAME_TOKEN_LENGTH = 16, @@ -278,7 +290,11 @@ enum REMOVE_PLAYER_INGAME_GAME_TIME_DELAY = 33, TRANSPORT_NONCE_LENGTH = 8, NETWORK_GAME_NAME_LENGTH = 16, +#ifdef HALO_LINUX + MAXIMUM_NUMBER_OF_PLAYERS = HALO_PORT_MAXIMUM_NETWORK_PLAYERS, +#else MAXIMUM_NUMBER_OF_PLAYERS = 16, +#endif }; enum @@ -356,7 +372,12 @@ struct network_game struct game_variant variant; byte unknown; char minimum_player_count; +#ifdef HALO_LINUX + /* 128 does not fit a signed char */ + byte maximum_player_count; +#else char maximum_player_count; +#endif byte team_count; short difficulty; short machine_count; @@ -369,6 +390,13 @@ struct network_game struct network_game_local_data local_data; }; +#ifdef HALO_LINUX +typedef char network_game_players_offset_assert[ + offsetof(struct network_game, players) == HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET ? 1 : -1]; +typedef char network_game_size_assert[ + sizeof(struct network_game) == HALO_PORT_NETWORK_GAME_SIZE ? 1 : -1]; +#endif + struct network_message { word header; @@ -393,7 +421,12 @@ struct message_client_game_update unsigned long update_number; short unknown; short player_count; +#ifdef HALO_LINUX + /* a machine's players, which 16 players / 4 machines only happened to give */ + struct player_action actions[MAXIMUM_LOCAL_PLAYERS]; +#else struct player_action actions[MAXIMUM_NUMBER_OF_PLAYERS / MAXIMUM_NETWORK_MACHINE_COUNT]; +#endif }; struct message_server_pong @@ -473,10 +506,23 @@ struct message_server_machine_rejected short reason; }; +#ifdef HALO_LINUX +/* the game settings record no longer fits one message: it goes out as +consecutive pieces, which the clients put back together */ +struct message_server_game_settings_update +{ + word total_size; + word offset; + word length; + word pad; + byte data[HALO_PORT_NETWORK_GAME_SETTINGS_FRAGMENT_SIZE]; +}; +#else struct message_server_game_settings_update { struct network_game game; }; +#endif struct message_server_remove_player_ingame { @@ -690,6 +736,103 @@ boolean network_game_server_send_player_joined_info_ingame( return FALSE; } +#ifdef HALO_LINUX +/* With up to 128 machines, sending the whole settings record (13 KB) to every +machine on each lobby change would flood the network while a lobby fills, so +changes are collected and sent at most this often; the server's pregame idle +sends what is pending. */ +enum +{ + NETWORK_GAME_SETTINGS_UPDATE_INTERVAL = MILLISECONDS_PER_SECOND / 4, +}; + +static boolean network_game_settings_update_pending = FALSE; +static unsigned long network_game_settings_update_time = 0; + +boolean network_game_server_send_game_settings_to_all_machines( + struct network_game_server *server, + void const *game, + long game_size) +{ + struct message_server_game_settings_update message; + long offset; + boolean result = TRUE; + + match_assert( + "c:\\halo\\SOURCE\\networking\\network_server_message_handler.c", + 0x1C8, + server); + + for (offset = 0; result && offset < game_size; offset += sizeof(message.data)) + { + void *encoded_message; + + message.total_size = (word)game_size; + message.offset = (word)offset; + message.length = (word)MIN((long)sizeof(message.data), game_size - offset); + message.pad = 0; + csmemset(message.data, 0, sizeof(message.data)); + csmemcpy(message.data, (byte const *)game + offset, message.length); + encoded_message = create_network_game_message( + _message_server_game_settings_update, + &message, + sizeof(message)); + if (encoded_message) + { + result = network_game_server_send_message_to_all_machines(server, encoded_message); + if (!result) + { + network_event( + "failed to send message_server_game_settings_update message to all machines"); + } + } + else + { + network_event("failed to create a message_server_game_settings_update message"); + result = FALSE; + } + } + + network_game_settings_update_pending = FALSE; + network_game_settings_update_time = system_milliseconds(); + + return result; +} + +boolean network_game_server_flush_game_data_pregame( + struct network_game_server *server) +{ + boolean result = TRUE; + + if (network_game_settings_update_pending && + system_milliseconds() - network_game_settings_update_time >= NETWORK_GAME_SETTINGS_UPDATE_INTERVAL) + { + struct network_game *game = network_game_server_get_game(server); + + if (game) + { + result = network_game_server_send_game_settings_to_all_machines(server, game, sizeof(*game)); + } + else + { + network_event( + "failed to handle a message_server_game_settings_update because their was no server game"); + network_game_settings_update_pending = FALSE; + result = FALSE; + } + } + + return result; +} + +boolean network_game_server_send_game_data_pregame( + struct network_game_server *server) +{ + network_game_settings_update_pending = TRUE; + + return network_game_server_flush_game_data_pregame(server); +} +#else boolean network_game_server_send_game_data_pregame( struct network_game_server *server) { @@ -733,6 +876,7 @@ boolean network_game_server_send_game_data_pregame( return result; } +#endif boolean network_game_server_handle_client_message( struct network_game_server *server, diff --git a/source/networking/network_server_message_handler.h b/source/networking/network_server_message_handler.h index 89528493d5..8572702c24 100644 --- a/source/networking/network_server_message_handler.h +++ b/source/networking/network_server_message_handler.h @@ -39,6 +39,16 @@ boolean network_game_server_send_player_joined_info_ingame( struct network_player *player); boolean network_game_server_send_game_data_pregame( struct network_game_server *server); +#ifdef HALO_LINUX +/* the native builds send the game settings record in pieces, and send pregame +changes at most four times a second */ +boolean network_game_server_send_game_settings_to_all_machines( + struct network_game_server *server, + void const *game, + long game_size); +boolean network_game_server_flush_game_data_pregame( + struct network_game_server *server); +#endif boolean network_game_server_handle_client_message( struct network_game_server *server, struct network_game_server_client_machine *machine, From 6651db882e1f71c14c107b264ba9b0f889dc3ace Mon Sep 17 00:00:00 2001 From: Jonas Volman Date: Sat, 26 Sep 2026 01:52:01 -0700 Subject: [PATCH 04/12] Port: raise the game engine to 128 players Native builds (HALO_LINUX) size the multiplayer game engine for HALO_PORT_MAXIMUM_NETWORK_PLAYERS; the matching build keeps the January text in #else branches and stays byte-identical. - game_engine.c: MULTIPLAYER_MAXIMUM_PLAYERS follows the port limit; the postgame statistic buffers hold an entry per player; global_goal has a slot per player (slayer kill-in-order uses the absolute player index) and the goal loops are bounded by the array instead of the address of global_variant; game_engine_get_place uses a 128-bit team mask; places past the 16th get code-generated English ordinals in the HUD score messages, scoreboard and postgame instead of asserting or clamping to "16th"; a free-for-all team is the player's absolute index. - slayer, oddball, king, race: per-team and per-player score arrays follow the port limit (oddball's ball arrays stay at 16); January size asserts are kept for the matching build. Race reads the lap flags of the player rather than of the player's team index. - game_allegiance.c: a team past the 10-team table is not its own enemy, like teams 0..9, so a free-for-all suicide no longer scores as a kill. Co-Authored-By: Claude Opus 5.5 --- source/game/game_allegiance.c | 11 +++ source/game/game_engine.c | 138 ++++++++++++++++++++++++++++++ source/game/game_engine_king.c | 5 ++ source/game/game_engine_king.h | 12 +++ source/game/game_engine_oddball.c | 10 +++ source/game/game_engine_race.c | 16 ++++ source/game/game_engine_slayer.c | 15 ++++ 7 files changed, 207 insertions(+) diff --git a/source/game/game_allegiance.c b/source/game/game_allegiance.c index c4b5508822..86a0ead363 100644 --- a/source/game/game_allegiance.c +++ b/source/game/game_allegiance.c @@ -202,6 +202,17 @@ boolean game_team_is_enemy( game_allegiance_globals->friendly_bitvector, NUMBER_OF_GAME_TEAMS * our_team_index + other_team_index); } +#ifdef HALO_LINUX + /* port: free for all gives every player a team, up to the player limit, + past the table's 10. Treat such a team like teams 0..9, whose diagonal is + friendly: it is not its own enemy, so a suicide does not score as a kill. + (game_team_is_ally already matches them: the ally diagonal is clear.) */ + else if (our_team_index == other_team_index && + our_team_index >= NUMBER_OF_GAME_TEAMS) + { + result = FALSE; + } +#endif return result; } diff --git a/source/game/game_engine.c b/source/game/game_engine.c index f22915a5e4..8791a388f2 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -579,7 +579,14 @@ symbols in this file: enum { +#ifdef HALO_LINUX + /* port: the native builds' session limit (halo_port_limits.h) */ + MULTIPLAYER_MAXIMUM_PLAYERS = HALO_PORT_MAXIMUM_NETWORK_PLAYERS, + /* ui\multiplayer_game_text only has strings for the first 16 places */ + NUMBER_OF_PLACE_STRINGS = 16, +#else MULTIPLAYER_MAXIMUM_PLAYERS = 16, +#endif }; /* ---------- macros */ @@ -999,7 +1006,13 @@ struct 1.0f }; +#ifdef HALO_LINUX +/* port: slayer's kill-in-order target uses the player's absolute index as +its goal index, so every player needs a goal slot */ +struct game_engine_goal global_goal[MAX(32, MULTIPLAYER_MAXIMUM_PLAYERS)] = { 0 }; +#else struct game_engine_goal global_goal[32] = { 0 }; +#endif struct game_variant global_variant = { 0 }; struct game_engine *game_engine = NULL; static long game_engine_teleport_message_ticks = 0; @@ -1073,6 +1086,45 @@ static void initialize_player_multiplayer_data( return; } +#ifdef HALO_LINUX +/* port: English ordinal for a zero-based place past the string list's 16 +("17th", "22nd", "111th"; "tied for 17th" when tied), in a static buffer */ +static wchar_t *place_ordinal_string( + long place, + boolean tied) +{ + static wchar_t string[32]; + long number = place + 1; + wchar_t const *suffix = L"th"; + + if (number % 100 < 11 || number % 100 > 13) + { + switch (number % 10) + { + case 1: + suffix = L"st"; + break; + case 2: + suffix = L"nd"; + break; + case 3: + suffix = L"rd"; + break; + } + } + + usnprintf( + string, + NUMBEROF(string), + tied ? L"tied for %d%s" : L"%d%s", + number, + suffix); + string[NUMBEROF(string) - 1] = 0; + + return string; +} +#endif + static wchar_t *get_place_string( struct postgame_statistic_entry *entry) { @@ -1080,6 +1132,12 @@ static wchar_t *get_place_string( long string_index; long string_list_index; +#ifdef HALO_LINUX + /* port: places past the 16th are spelled out instead of clamped (the + caller's format string marks a tie) */ + if (raw_index >= NUMBER_OF_PLACE_STRINGS) + return place_ordinal_string(raw_index, FALSE); +#endif if (raw_index < 0) string_index = 0; else if (raw_index > 15) @@ -1877,6 +1935,12 @@ static void game_engine_rasterize_in_game_score( } else place_string = L""; +#ifdef HALO_LINUX + /* port: places past the 16th are spelled out instead of + clamped (the rows never marked ties) */ + if (raw_place >= NUMBER_OF_PLACE_STRINGS) + place_string = place_ordinal_string(raw_place, FALSE); +#endif usprintf( row_string, @@ -1930,7 +1994,12 @@ static void drawline( void game_engine_post_rasterize_post_game( void) { +#ifdef HALO_LINUX + /* port: sized like every buffer handed to populate_statistic_buffer */ + struct postgame_statistic_entry entries[MULTIPLAYER_MAXIMUM_PLAYERS]; +#else struct postgame_statistic_entry entries[16]; +#endif wchar_t score_string[256]; wchar_t row_string[256]; short tab_stops[6]; @@ -2103,6 +2172,16 @@ void game_engine_post_rasterize_post_game( string_list_index != NONE ? unicode_string_list_get_string(string_list_index, place + 36) : L""; +#ifdef HALO_LINUX + /* port: places past the 16th are spelled out instead of clamped + (the rows never marked ties) */ + if ((entries[entry_index].values[6] & 0x7F) >= NUMBER_OF_PLACE_STRINGS) + { + place_string = place_ordinal_string( + entries[entry_index].values[6] & 0x7F, + FALSE); + } +#endif usnprintf(row_string, NUMBEROF(row_string), L" \t%s", place_string); row_string[NUMBEROF(row_string) - 1] = 0; drawline(row_string, draw_row, 0); @@ -4370,11 +4449,20 @@ wchar_t *get_place_name( long lookup_index; long string_list_index; +#ifdef HALO_LINUX + /* port: a session can have more places than the string list names */ + match_vassert( + "c:\\halo\\SOURCE\\game\\game_engine.c", + 0x1316, + place.place < MULTIPLAYER_MAXIMUM_PLAYERS, + "place.place < maximum_places"); +#else match_vassert( "c:\\halo\\SOURCE\\game\\game_engine.c", 0x1316, place.place < 16, "place.place < maximum_places"); +#endif if ((place.flags & FLAG(2)) && TEST_FLAG(place.flags, 0)) lookup_index = 35; @@ -4384,6 +4472,12 @@ wchar_t *get_place_name( lookup_index = 34; else if (TEST_FLAG(place.flags, 1)) lookup_index = 32; +#ifdef HALO_LINUX + /* port: places past the 16th are spelled out; a tie is marked like the + string list's tied places */ + else if (place.place >= NUMBER_OF_PLACE_STRINGS) + return place_ordinal_string(place.place, TEST_FLAG(place.flags, _place_tied)); +#endif else { lookup_index = place.place; @@ -4706,7 +4800,13 @@ short game_engine_player_get_custom_motion_sensor_positions( goal_index++; } /* January uses the adjacent global_variant address as the signed loop bound. */ +#ifdef HALO_LINUX + /* port: bounded by the goal array itself, which is larger here and + need not sit next to global_variant */ + while (goal < global_goal + NUMBEROF(global_goal)); +#else while ((long)goal < (long)&global_variant); +#endif } return count; @@ -4754,7 +4854,12 @@ void game_engine_render_nav_points( goal++; goal_index++; } +#ifdef HALO_LINUX + /* port: bounded by the goal array itself, as above */ + while (goal < global_goal + NUMBEROF(global_goal)); +#else while ((long)goal < (long)&global_variant); +#endif } } } @@ -4893,7 +4998,12 @@ static struct postgame_statistic_entry *game_engine_get_player_place( struct postgame_statistic_entry *entry, long player_index) { +#ifdef HALO_LINUX + /* port: populate_statistic_buffer writes an entry per player */ + struct postgame_statistic_entry entries[MULTIPLAYER_MAXIMUM_PLAYERS]; +#else struct postgame_statistic_entry entries[16]; +#endif long place = 0; populate_statistic_buffer(entries, 0, 0); @@ -5666,7 +5776,12 @@ long postgame_statistic_get_rating( long parameter1, long parameter2) { +#ifdef HALO_LINUX + /* port: populate_statistic_buffer writes an entry per player */ + struct postgame_statistic_entry entries[MULTIPLAYER_MAXIMUM_PLAYERS]; +#else struct postgame_statistic_entry entries[16]; +#endif long rating; long entry_count; long entry_index; @@ -6340,11 +6455,22 @@ void game_engine_player_added( } else { +#ifdef HALO_LINUX + /* port: a free-for-all team is the player's own slot, which stays + below the player limit and is the same on every machine */ + long team_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index); + + player->network_player_data.team_index = + (char)team_index; + player->team_index = + (signed char)team_index; +#else player->network_player_data.team_index = (char)*next_team_index; player->team_index = (signed char)*next_team_index; (*next_team_index)++; +#endif } if (player_index != NONE) @@ -7855,7 +7981,12 @@ struct game_engine_place game_engine_get_place( { struct data_iterator iterator; struct player_datum *other_player; +#ifdef HALO_LINUX + /* port: free-for-all team indices run up to the player limit */ + unsigned long team_mask[BIT_VECTOR_SIZE_IN_LONGS(MULTIPLAYER_MAXIMUM_PLAYERS)] = { 0 }; +#else unsigned long team_mask = 0; +#endif long score = game_engine->get_player_score(player_index, score_type); data_iterator_new(&iterator, player_data); @@ -7875,10 +8006,17 @@ struct game_engine_place game_engine_get_place( if (different_player && score_type == _get_score_team) { +#ifdef HALO_LINUX + if (BIT_VECTOR_TEST_FLAG(team_mask, other_player->team_index)) + different_player = FALSE; + else + BIT_VECTOR_SET_FLAG(team_mask, other_player->team_index, TRUE); +#else if (TEST_FLAG(team_mask, other_player->team_index)) different_player = FALSE; else SET_FLAG(team_mask, other_player->team_index, TRUE); +#endif } if (different_player) diff --git a/source/game/game_engine_king.c b/source/game/game_engine_king.c index da0ae0d8e9..fbe02de578 100644 --- a/source/game/game_engine_king.c +++ b/source/game/game_engine_king.c @@ -105,7 +105,12 @@ symbols in this file: enum { +#ifdef HALO_LINUX + /* port: king_globals' score slots follow the session player limit */ + MAXIMUM_KING_SCORE_SLOTS = HALO_PORT_MAXIMUM_NETWORK_PLAYERS, +#else MAXIMUM_KING_SCORE_SLOTS = 16, +#endif MAXIMUM_HILL_POINTS = 12, MAXIMUM_HILLS = 64, NUMBER_OF_DEFAULT_ANIMATION_VALUES = 4, diff --git a/source/game/game_engine_king.h b/source/game/game_engine_king.h index 25f673af7a..ecd9c57724 100644 --- a/source/game/game_engine_king.h +++ b/source/game/game_engine_king.h @@ -36,9 +36,18 @@ struct render_lighting; struct king_globals { +#ifdef HALO_LINUX + /* port: score and score_tick are indexed by team (free for all gives + every player a team), on_the_hill by absolute player index, so all three + follow the session player limit (halo_port_limits.h) */ + long score[HALO_PORT_MAXIMUM_NETWORK_PLAYERS]; + long score_tick[HALO_PORT_MAXIMUM_NETWORK_PLAYERS]; + boolean on_the_hill[HALO_PORT_MAXIMUM_NETWORK_PLAYERS]; +#else long score[16]; long score_tick[16]; boolean on_the_hill[16]; +#endif long hill_point_count; real_point3d hill_points[12]; real_point2d convex_hull[12]; @@ -52,6 +61,8 @@ struct king_globals long hill_timer; }; +/* January's layout; the port's per-player arrays are larger */ +#ifndef HALO_LINUX typedef char verify_king_globals_size[ sizeof(struct king_globals) == 0x1AC ? 1 : -1]; typedef char verify_king_globals_on_the_hill_offset[ @@ -60,6 +71,7 @@ typedef char verify_king_globals_convex_hull_offset[ offsetof(struct king_globals, convex_hull) == 0x124 ? 1 : -1]; typedef char verify_king_globals_hill_id_offset[ offsetof(struct king_globals, hill_id) == 0x1A4 ? 1 : -1]; +#endif /* ---------- prototypes/GAME_ENGINE_KING.C */ diff --git a/source/game/game_engine_oddball.c b/source/game/game_engine_oddball.c index a4bcc30e1d..45eca18a94 100644 --- a/source/game/game_engine_oddball.c +++ b/source/game/game_engine_oddball.c @@ -219,18 +219,28 @@ enum multiplayer_game_text struct oddball_globals { long score_to_win; +#ifdef HALO_LINUX + /* port: the scores are indexed by team (free for all gives every player + a team) and by absolute player index, not by ball */ + long team_score[HALO_PORT_MAXIMUM_NETWORK_PLAYERS]; + long individual_score[HALO_PORT_MAXIMUM_NETWORK_PLAYERS]; +#else long team_score[MAXIMUM_ODDBALLS]; long individual_score[MAXIMUM_ODDBALLS]; +#endif long ball_spawn_timer[MAXIMUM_ODDBALLS]; long current_ball_owner[MAXIMUM_ODDBALLS]; }; +/* January's layout; the port's score arrays are larger */ +#ifndef HALO_LINUX typedef char verify_oddball_globals_size[ sizeof(struct oddball_globals) == 0x104 ? 1 : -1]; typedef char verify_oddball_globals_ball_spawn_timer_offset[ offsetof(struct oddball_globals, ball_spawn_timer) == 0x84 ? 1 : -1]; typedef char verify_oddball_globals_current_ball_owner_offset[ offsetof(struct oddball_globals, current_ball_owner) == 0xC4 ? 1 : -1]; +#endif struct scenario_netgame_flag { diff --git a/source/game/game_engine_race.c b/source/game/game_engine_race.c index 3c86848a7e..cbbb409d35 100644 --- a/source/game/game_engine_race.c +++ b/source/game/game_engine_race.c @@ -123,7 +123,12 @@ enum MAXIMUM_RACE_FLAGS = 32, MAXIMUM_RACE_VEHICLES = 8, MAXIMUM_RACE_VEHICLES_TO_DELETE = 32, +#ifdef HALO_LINUX + /* port: the native builds' session limit (halo_port_limits.h) */ + MULTIPLAYER_MAXIMUM_PLAYERS = HALO_PORT_MAXIMUM_NETWORK_PLAYERS, +#else MULTIPLAYER_MAXIMUM_PLAYERS = 16, +#endif }; enum race_type @@ -226,10 +231,13 @@ struct race_globals byte padCD[3]; }; +/* January's layout; the port's per-player arrays are larger */ +#ifndef HALO_LINUX typedef char verify_race_globals_size[ sizeof(struct race_globals) == 0xD0 ? 1 : -1]; typedef char verify_race_globals_vehicles_have_been_added_offset[ offsetof(struct race_globals, vehicles_have_been_added) == 0xCC ? 1 : -1]; +#endif /* ---------- prototypes */ @@ -1072,9 +1080,17 @@ long race_engine_get_score( } else { +#ifdef HALO_LINUX + /* port: lap_bit_vector is kept per player; January read it by team + index, which picks another player's flags */ + long laps = player->statistics.multiplayer_statistics.race_statistics.laps; + long flags_touched = count_bits_32( + race_globals.lap_bit_vector[DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index)]); +#else long team_index = player->team_index; long laps = player->statistics.multiplayer_statistics.race_statistics.laps; long flags_touched = count_bits_32(race_globals.lap_bit_vector[team_index]); +#endif score = laps * (MAXIMUM_RACE_FLAGS + 1) + flags_touched; } diff --git a/source/game/game_engine_slayer.c b/source/game/game_engine_slayer.c index 5a12e487fb..dbeafd71b8 100644 --- a/source/game/game_engine_slayer.c +++ b/source/game/game_engine_slayer.c @@ -100,7 +100,12 @@ enum _string_name_kills_score_n_team_score_of_max = 0xB5, _string_name_kills_score_of_max = 0xB6, _game_engine_test_flag_rasterize_score = 1, +#ifdef HALO_LINUX + /* port: the native builds' session limit (halo_port_limits.h) */ + _slayer_maximum_players = HALO_PORT_MAXIMUM_NETWORK_PLAYERS, +#else _slayer_maximum_players = 16, +#endif }; /* ---------- macros */ @@ -117,12 +122,22 @@ enum struct slayer_globals { +#ifdef HALO_LINUX + /* port: indexed by team (free for all gives every player a team) and by + absolute player index, so both follow the session player limit */ + long team_score[HALO_PORT_MAXIMUM_NETWORK_PLAYERS]; + long individual_score[HALO_PORT_MAXIMUM_NETWORK_PLAYERS]; +#else long team_score[16]; long individual_score[16]; +#endif }; +/* January's layout; the port's arrays are larger */ +#ifndef HALO_LINUX typedef char verify_slayer_globals_size[ sizeof(struct slayer_globals) == 0x80 ? 1 : -1]; +#endif /* ---------- prototypes */ From 87a48e0e2f8bf6ed8f981cec5eb3d02dc57d5185 Mon Sep 17 00:00:00 2001 From: Jonas Volman Date: Sat, 26 Sep 2026 01:24:41 -0700 Subject: [PATCH 05/12] Port: interface support for 128-player, 128-machine sessions Native builds only (#ifdef HALO_LINUX); the matching MSVC build keeps the original text and stays byte-identical. - ui_widget_game_data_input_functions.c: the local machine limit follows HALO_PORT_MAXIMUM_NETWORK_MACHINES and the struct network_game copy holds HALO_PORT_MAXIMUM_NETWORK_PLAYERS players; its layout is checked against halo_port_limits.h. The pregame screen keeps its three remote machine panels and shows the first three remote machines instead of asserting (sizing its arrays by the machine limit read uninitialized widget pointers). netgame_prejoin_players scans every player slot, and the player count texts have room for three digits. network_machine_is_valid widens the char index before comparing it with 128. - ui_widget_event_handler_functions.c: the join/unjoin, team swap and countdown handlers use the port's players offset and player count instead of 0x226/0x242/0x243 and 16. - hud.c: hud_draw_players sizes its teammate buffer by the port's player limit and bounds the fill (more than 16 teammates overflowed the stack). - motion_sensor.c: multiplayer tests the (horizontal) range before a unit takes a blip slot, so distant units no longer crowd nearby ones out of the 16 slots. Co-Authored-By: Claude Opus 5.5 --- source/interface/hud.c | 11 +++ source/interface/motion_sensor.c | 14 ++++ .../ui_widget_event_handler_functions.c | 44 +++++++++++ .../ui_widget_game_data_input_functions.c | 79 +++++++++++++++++++ 4 files changed, 148 insertions(+) diff --git a/source/interface/hud.c b/source/interface/hud.c index e63cdafb69..ed240e4fa1 100644 --- a/source/interface/hud.c +++ b/source/interface/hud.c @@ -988,7 +988,12 @@ void hud_draw_players( if (player_index != NONE) { +#ifdef HALO_LINUX + /* the native builds' sessions hold more than 16 players */ + long teammate_indices[HALO_PORT_MAXIMUM_NETWORK_PLAYERS]; +#else long teammate_indices[16]; +#endif long teammate_count = 0; struct data_iterator iterator; struct player_datum *player; @@ -1001,7 +1006,13 @@ void hud_draw_players( team_index == player->team_index && player->unit_index != NONE) { +#ifdef HALO_LINUX + /* never write past the buffer, whatever the player data holds */ + if (teammate_count < (long)NUMBEROF(teammate_indices)) + teammate_indices[teammate_count++] = iterator.datum_index; +#else teammate_indices[teammate_count++] = iterator.datum_index; +#endif } } diff --git a/source/interface/motion_sensor.c b/source/interface/motion_sensor.c index 7720e0aa39..3d0b65d465 100644 --- a/source/interface/motion_sensor.c +++ b/source/interface/motion_sensor.c @@ -439,10 +439,24 @@ static void motion_sensor_update( object_position.z - camera_positions[scan_player_index].z; +#ifdef HALO_LINUX + /* test the range before taking a blip slot in multiplayer + too: with the native builds' larger sessions, units out of + range would otherwise fill the slots (update_motion_sensor + only drops them later) and crowd nearby ones out. Like that + test, the multiplayer range ignores height. */ + if (game_engine_running()) + displacement.k = 0.0f; + + if (magnitude_squared3d(&displacement) <= + hud_globals->defaults.motion_sensor_range * + hud_globals->defaults.motion_sensor_range) +#else if (game_engine_running() || magnitude_squared3d(&displacement) <= hud_globals->defaults.motion_sensor_range * hud_globals->defaults.motion_sensor_range) +#endif { struct motion_sensor_player *player = &motion_sensor_globals->players[scan_player_index]; diff --git a/source/interface/ui_widget_event_handler_functions.c b/source/interface/ui_widget_event_handler_functions.c index ecdbabb5a0..fbf7347c59 100644 --- a/source/interface/ui_widget_event_handler_functions.c +++ b/source/interface/ui_widget_event_handler_functions.c @@ -2583,8 +2583,15 @@ static boolean netgame_unjoin_player( if ((short)machine_index != NONE) { +#ifdef HALO_LINUX + /* the native builds' session limits move and extend the players + (port/linux/include/halo_port_limits.h) */ + test_player = game + HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET; + for (player_index = 0; player_index < HALO_PORT_MAXIMUM_NETWORK_PLAYERS; player_index++, test_player += 0x20) +#else test_player = game + 0x226; for (player_index = 0; player_index < 16; player_index++, test_player += 0x20) +#endif { if (network_player_is_valid(test_player) && (short)(signed char)test_player[0x1C] == (short)machine_index) @@ -2935,14 +2942,25 @@ static boolean multiplayer_game_swap_teams( if ((short)machine_index != NONE) { player_index = 0; +#ifdef HALO_LINUX + /* the players' machine indices, where the native builds' session + limits put them (port/linux/include/halo_port_limits.h) */ + player = game + HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET + 0x1C; + for (; player_index < HALO_PORT_MAXIMUM_NETWORK_PLAYERS; player_index++, player += 0x20) +#else player = game + 0x242; for (; player_index < 16; player_index++, player += 0x20) +#endif { if (network_player_is_valid(player - 0x1C) && (short)(signed char)player[0] == (short)machine_index && (short)(signed char)player[1] == event->controller_index) { +#ifdef HALO_LINUX + player_data = *(struct network_player_data *)(game + HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET + player_index * 0x20); +#else player_data = *(struct network_player_data *)(game + 0x226 + player_index * 0x20); +#endif player_data.data[0x1E] = !player_data.data[0x1E]; if (!network_game_client_update_local_player_data( global_network_game_client_get(), &player_data)) @@ -2973,8 +2991,15 @@ static boolean network_game_start_faster( machine_index = network_game_client_get_machine_index(client); player_index = 0; player = (byte *)game; +#ifdef HALO_LINUX + /* the players' machine indices, where the native builds' session + limits put them (port/linux/include/halo_port_limits.h) */ + player += HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET + 0x1C; + for (; player_index < HALO_PORT_MAXIMUM_NETWORK_PLAYERS; player_index++, player += 0x20) +#else player += 0x242; for (; player_index < 16; player_index++, player += 0x20) +#endif { if (network_player_is_valid(player - 0x1C) && (short)(signed char)player[0] == (short)machine_index && @@ -3005,8 +3030,15 @@ static boolean network_game_start_slower( machine_index = network_game_client_get_machine_index(client); player_index = 0; player = (byte *)game; +#ifdef HALO_LINUX + /* the players' machine indices, where the native builds' session + limits put them (port/linux/include/halo_port_limits.h) */ + player += HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET + 0x1C; + for (; player_index < HALO_PORT_MAXIMUM_NETWORK_PLAYERS; player_index++, player += 0x20) +#else player += 0x242; for (; player_index < 16; player_index++, player += 0x20) +#endif { if (network_player_is_valid(player - 0x1C) && (short)(signed char)player[0] == (short)machine_index && @@ -4349,6 +4381,17 @@ static boolean netgame_join_player( match_assert("c:\\halo\\SOURCE\\interface\\ui_widget_event_handler_functions.c", 1627, game); if ((short)machine_index != NONE) { +#ifdef HALO_LINUX + /* the native builds' session limits move and extend the players + (port/linux/include/halo_port_limits.h) */ + for (value = 0; value < HALO_PORT_MAXIMUM_NETWORK_PLAYERS; value++) + { + if (network_player_is_valid(game + HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET + value * 0x20) && + (short)(signed char)game[HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET + 0x1C + value * 0x20] == (short)machine_index && + (short)(signed char)game[HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET + 0x1D + value * 0x20] == event->controller_index) + return TRUE; + } +#else for (value = 0; value < 16; value++) { if (network_player_is_valid(game + 0x226 + value * 0x20) && @@ -4356,6 +4399,7 @@ static boolean netgame_join_player( (short)(signed char)game[0x243 + value * 0x20] == event->controller_index) return TRUE; } +#endif } if (!network_game_client_add_player(client, event->controller_index)) network_event("failed to send join request"); diff --git a/source/interface/ui_widget_game_data_input_functions.c b/source/interface/ui_widget_game_data_input_functions.c index 7491f58946..d95babcdfc 100644 --- a/source/interface/ui_widget_game_data_input_functions.c +++ b/source/interface/ui_widget_game_data_input_functions.c @@ -366,11 +366,25 @@ enum enum { +#ifdef HALO_LINUX + /* the native builds' session limit (port/linux/include/halo_port_limits.h) */ + MAXIMUM_NETWORK_MACHINE_COUNT = HALO_PORT_MAXIMUM_NETWORK_MACHINES, +#else MAXIMUM_NETWORK_MACHINE_COUNT = 4, +#endif MAXIMUM_NETWORK_ADVERTISED_GAMES = 9, MAXIMUM_DISPLAYED_SERVERS = 10, }; +#ifdef HALO_LINUX +enum +{ + /* the networked pregame screen has panels for the local machine and three + remote machines, fewer than the native builds' sessions can hold */ + NUMBER_OF_REMOTE_MACHINE_PANELS = 3, +}; +#endif + enum network_game_platform { _network_game_platform_xbox, @@ -417,9 +431,17 @@ enum multiplayer_game_bitmap_frame /* Same TU-local definition as network_client_manager.c, network_game_manager.c, network_server_manager.c and network_server_message_handler.c; no shared header owns it. January assertion strings in those objects preserve the macro name. */ +#ifdef HALO_LINUX +/* widened: a char machine index is always below the native builds' 128 +machines, and clang warns about the always-true char comparison */ +#define network_machine_is_valid(machine) \ + ((machine) && (machine)->machine_index >= 0 && \ + (long)(machine)->machine_index < MAXIMUM_NETWORK_MACHINE_COUNT) +#else #define network_machine_is_valid(machine) \ ((machine) && (machine)->machine_index >= 0 && \ (machine)->machine_index < MAXIMUM_NETWORK_MACHINE_COUNT) +#endif /* ---------- structures */ @@ -541,7 +563,12 @@ struct network_game short machine_count; struct network_machine machines[MAXIMUM_NETWORK_MACHINE_COUNT]; short player_count; +#ifdef HALO_LINUX + /* the native builds' session limit, as in the networking copies */ + struct network_player players[HALO_PORT_MAXIMUM_NETWORK_PLAYERS]; +#else struct network_player players[16]; +#endif }; typedef char network_advertised_game_size_assert[ @@ -554,8 +581,21 @@ typedef char network_advertised_game_platform_offset_assert[ offsetof(struct network_advertised_game, platform) == 0xDE ? 1 : -1]; typedef char network_machine_size_assert[ sizeof(struct network_machine) == 0x44 ? 1 : -1]; +#ifndef HALO_LINUX typedef char network_game_players_offset_assert[ offsetof(struct network_game, players) == 0x226 ? 1 : -1]; +#else +/* the native builds' session limits move the players; every copy of the +record is checked against port/linux/include/halo_port_limits.h (this one +declares it only up to the players) */ +typedef char network_game_players_offset_assert[ + offsetof(struct network_game, players) == HALO_PORT_NETWORK_GAME_PLAYERS_OFFSET ? 1 : -1]; +typedef char network_game_players_end_assert[ + offsetof(struct network_game, players) + sizeof(((struct network_game *)0)->players) == + HALO_PORT_NETWORK_GAME_PLAYERS_END ? 1 : -1]; +typedef char network_game_size_assert[ + sizeof(struct network_game) <= HALO_PORT_NETWORK_GAME_SIZE ? 1 : -1]; +#endif struct playlist_profile { @@ -1153,11 +1193,21 @@ void server_list_menu_update( 0x364); if (number_of_players_text->parameters.text_box.text) { +#ifdef HALO_LINUX + /* room for a three-digit count: the Linux runtime's + _vsnwprintf writes its terminator inside the count */ + usnprintf( + number_of_players_text->parameters.text_box.text, + 4, + L"%d", + server->player_count); +#else usnprintf( number_of_players_text->parameters.text_box.text, 3, L"%d", server->player_count); +#endif number_of_players_text->parameters.text_box.text[3] = 0; } @@ -1514,8 +1564,14 @@ void network_pregame_status_screen_update( } { +#ifdef HALO_LINUX + /* one entry per remote machine panel, not per remote machine */ + long machine_indices[NUMBER_OF_REMOTE_MACHINE_PANELS]; + struct widget_instance *machine_widgets[NUMBER_OF_REMOTE_MACHINE_PANELS]; +#else long machine_indices[MAXIMUM_NETWORK_MACHINE_COUNT - 1]; struct widget_instance *machine_widgets[MAXIMUM_NETWORK_MACHINE_COUNT - 1]; +#endif long machine_widget_index; long j; @@ -1532,10 +1588,16 @@ void network_pregame_status_screen_update( if (network_machine_is_valid(machine) && machine->machine_index != local_machine_index) { +#ifdef HALO_LINUX + /* more remote machines than panels: show the first ones */ + if (j >= NUMBER_OF_REMOTE_MACHINE_PANELS) + break; +#else match_assert( "c:\\halo\\SOURCE\\interface\\ui_widget_game_data_input_functions.c", 0x4D4, j<(MAXIMUM_NETWORK_MACHINE_COUNT-1)); +#endif machine_indices[j] = machine_index; j++; @@ -1543,7 +1605,11 @@ void network_pregame_status_screen_update( } for (machine_widget_index = 0; +#ifdef HALO_LINUX + machine_widget_index < NUMBER_OF_REMOTE_MACHINE_PANELS; +#else machine_widget_index < MAXIMUM_NETWORK_MACHINE_COUNT - 1; +#endif machine_widget_index++) { struct widget_instance *remote_machine_icon; @@ -1926,7 +1992,14 @@ void netgame_prejoin_players( player_ui_local_player_wants_to_play_multiplayer((short)state_or_index.index); } +#ifdef HALO_LINUX + /* every player slot of the native builds' larger sessions */ + for (state_or_index.index = 0; + (short)state_or_index.index < (short)NUMBEROF(game->players); + state_or_index.index++) +#else for (state_or_index.index = 0; (short)state_or_index.index < 16; state_or_index.index++) +#endif { if (network_player_is_valid(&game->players[(short)state_or_index.index]) && network_game_player_is_local(&game->players[(short)state_or_index.index])) @@ -2733,7 +2806,13 @@ void multiplayer_game_set_text_box_for_number_of_players( 0xB56); if (widget->parameters.text_box.text) { +#ifdef HALO_LINUX + /* room for a three-digit count: the Linux runtime's _vsnwprintf + writes its terminator inside the count */ + usnprintf(widget->parameters.text_box.text, 4, L"%d", game->player_count); +#else usnprintf(widget->parameters.text_box.text, 3, L"%d", game->player_count); +#endif widget->parameters.text_box.text[3] = 0; } return; From eb0c5a26db1fd668393cad4e2aba66a44786063d Mon Sep 17 00:00:00 2001 From: Jonas Volman Date: Sat, 26 Sep 2026 02:14:45 -0700 Subject: [PATCH 06/12] Port: keep game_engine_get_place's team mask in range The 128-bit team mask is indexed by team; a player whose team is not set (NONE) would read and write outside it, where the 32-bit mask only shifted. Such a player now counts on its own. Co-Authored-By: Claude Opus 5.5 --- source/game/game_engine.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/source/game/game_engine.c b/source/game/game_engine.c index 8791a388f2..d440fa5257 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -8007,10 +8007,14 @@ struct game_engine_place game_engine_get_place( score_type == _get_score_team) { #ifdef HALO_LINUX - if (BIT_VECTOR_TEST_FLAG(team_mask, other_player->team_index)) - different_player = FALSE; - else - BIT_VECTOR_SET_FLAG(team_mask, other_player->team_index, TRUE); + /* a team outside the mask (none yet) counts on its own */ + if (VALID_INDEX(other_player->team_index, MULTIPLAYER_MAXIMUM_PLAYERS)) + { + if (BIT_VECTOR_TEST_FLAG(team_mask, other_player->team_index)) + different_player = FALSE; + else + BIT_VECTOR_SET_FLAG(team_mask, other_player->team_index, TRUE); + } #else if (TEST_FLAG(team_mask, other_player->team_index)) different_player = FALSE; From e2d59b517b108108268c6516d014e365049fc92b Mon Sep 17 00:00:00 2001 From: Jonas Volman Date: Sat, 26 Sep 2026 02:50:35 -0700 Subject: [PATCH 07/12] Port: keep sendto's rewritten destination in scope halo_ws_sendto rewrote a destination of 127.0.0.1 to the HALO_NET_ADDRESS address in a structure local to the else block, then sent to it after the block had ended. At -O2 clang drops the store, so datagrams went to a garbage address. It hit the host's own machine: its client reaches the local server as 127.0.0.1, so with HALO_NET_ADDRESS set the server never got the host's input and dropped the host after the stall timeout. Co-Authored-By: Claude Opus 5.5 --- port/linux/src/xnet.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/port/linux/src/xnet.c b/port/linux/src/xnet.c index 389bbecfeb..a568efb846 100644 --- a/port/linux/src/xnet.c +++ b/port/linux/src/xnet.c @@ -253,6 +253,9 @@ int WSAAPI halo_ws_send(SOCKET socket, const char *buffer, int length, int flags int WSAAPI halo_ws_sendto(SOCKET socket, const char *buffer, int length, int flags, const struct sockaddr *address, int address_length) { + /* the rewritten destination: it must outlive the send */ + struct sockaddr_in target; + if (address && address->sa_family == AF_INET && address_length >= (int)sizeof(struct sockaddr_in) && ((const struct sockaddr_in *)address)->sin_addr.s_addr == INADDR_BROADCAST) { @@ -261,7 +264,6 @@ int WSAAPI halo_ws_sendto(SOCKET socket, const char *buffer, int length, int fla if (target_count) { - struct sockaddr_in target; int index; int result = 0; @@ -281,8 +283,6 @@ int WSAAPI halo_ws_sendto(SOCKET socket, const char *buffer, int length, int fla } else { - struct sockaddr_in target; - address = outgoing_address(address, address_length, &target); } return winsock_result(posix_socket_sendto((int)socket, buffer, length, flags, address, address_length)); From 9a376181f50215a5716b4eb3645eb71152297ad7 Mon Sep 17 00:00:00 2001 From: Jonas Volman Date: Sat, 26 Sep 2026 02:21:12 -0700 Subject: [PATCH 08/12] Port: grow the game state and object pools for 128 players The Xbox game state (0x345000 bytes at 0x80061000) ends exactly where the map-linked tag cache begins and has 948 bytes free, so a 128-player session cannot even allocate its player datums. The native builds now place a 16 MB game state (CPU 0xFC0000, GPU 0x40000) at 0x81A00000, above the tag cache and below everything the Xbox memory window hands out top-down, and grow the pools that scale with the number of players: - objects: 2048 -> 8192 headers, 1 MB -> 8 MB pool; the garbage limits keep January's proportions (5/10/20 % of the pool, 2.5/5/10 % of the headers, 75/50/30 active garbage objects per 16 players) - cluster partition reference lists 2048 -> 8192 (collideable and noncollideable objects, lights); a full list drops objects out of rendering, collision and sphere queries - effects 256 -> 2048, effect locations 512 -> 4096 - particles 1024 -> 8192 (render_particles' visible list, filled without a bound check, follows), particle systems 64 -> 256, their particles 512 -> 4096 - contrails 256 -> 1024, contrail points 1024 -> 8192 (MAXIMUM_CONTRAIL_POINTS stays the pool size for the same reason) - lights 896 -> 4096, object looping sounds 1024 -> 4096 - cached object render states and rendered objects 256 -> 1024 - script object-list references 128 -> 1024, objects one explosion can damage 64 -> 256 The values live in port/linux/include/halo_port_capacity.h, included by halo_port_limits.h. With 128 player datums the CPU part holds 14,256,908 bytes (2,258,164 free). Saved games and cores grow to 16 MB. Every change is under HALO_LINUX; the MSVC objects are unchanged. Co-Authored-By: Claude Opus 5.5 --- port/linux/include/halo_port_capacity.h | 61 +++++++++++++++++++++++++ port/linux/include/halo_port_limits.h | 4 ++ source/cache/physical_memory_map.c | 8 ++++ source/effects/contrails.c | 13 ++++++ source/effects/effects.c | 7 +++ source/effects/particle_systems.c | 7 +++ source/effects/particles.c | 7 +++ source/hs/object_lists.c | 6 +++ source/objects/damage.c | 6 +++ source/objects/objects.c | 6 +++ source/objects/objects.h | 24 ++++++++++ source/render/render.h | 5 ++ source/render/render_objects.c | 11 +++++ source/render/render_particles.c | 7 +++ source/saved games/game_state.c | 27 +++++++++++ source/saved games/game_state_xbox.c | 7 +++ source/sound/game_sound.c | 5 ++ source/structures/cluster_partitions.c | 10 ++++ 18 files changed, 221 insertions(+) create mode 100644 port/linux/include/halo_port_capacity.h diff --git a/port/linux/include/halo_port_capacity.h b/port/linux/include/halo_port_capacity.h new file mode 100644 index 0000000000..ca8aef6313 --- /dev/null +++ b/port/linux/include/halo_port_capacity.h @@ -0,0 +1,61 @@ +/* +HALO_PORT_CAPACITY.H + +Memory capacity of the native builds (Windows, Linux, Android), sized for the +session limits in halo_port_limits.h, which includes this file. Game sources +use these values only under #ifdef HALO_LINUX, so the byte-matching MSVC build +keeps the Xbox sizes (given in parentheses below). + +Every machine in a session must be built with the same values: the game is +simulated in lockstep on every machine, and a pool that runs full changes the +simulation (an object or a deterministic effect is not created). +*/ + +#ifndef __HALO_PORT_CAPACITY_H +#define __HALO_PORT_CAPACITY_H + +/* ---------- game state + +The Xbox game state is 0x345000 bytes at 0x80061000 and ends where the tag +cache begins (0x803A6000). Cache files are linked to that tag cache address, +so the game state cannot grow in place. The native builds put a 16 MB game +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 +fill 3,165,260 of its 0x305000 bytes); the GPU part holds only the decal +vertices, as on the Xbox. */ + +#define HALO_PORT_GAME_STATE_BASE_ADDRESS 0x81A00000 /* (0x80061000) */ +#define HALO_PORT_GAME_STATE_CPU_SIZE 0xFC0000 /* (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) + +/* ---------- objects */ + +#define HALO_PORT_MAXIMUM_OBJECTS_PER_MAP 8192 /* (2048) */ +#define HALO_PORT_OBJECT_MEMORY_POOL_SIZE 0x800000 /* (0x100000) */ +/* each of the two reference lists of every cluster partition (collideable +objects, noncollideable objects, lights) */ +#define HALO_PORT_MAXIMUM_CLUSTER_REFERENCES 8192 /* (2048) */ +#define HALO_PORT_MAXIMUM_RENDERED_OBJECTS 1024 /* (256) */ +#define HALO_PORT_MAXIMUM_CACHED_OBJECT_RENDER_STATES 1024 /* (256) */ +/* objects one explosion can damage */ +#define HALO_PORT_MAXIMUM_AREA_OF_EFFECT_OBJECTS 256 /* (64) */ +/* object references shared by all script object lists */ +#define HALO_PORT_MAXIMUM_LISTED_OBJECTS_PER_MAP 1024 /* (128) */ + +/* ---------- effects, particles, lights and sounds */ + +#define HALO_PORT_MAXIMUM_EFFECTS 2048 /* (256) */ +#define HALO_PORT_MAXIMUM_EFFECT_LOCATIONS 4096 /* (512) */ +#define HALO_PORT_MAXIMUM_PARTICLES 8192 /* (1024) */ +#define HALO_PORT_MAXIMUM_PARTICLE_SYSTEMS 256 /* (64) */ +#define HALO_PORT_MAXIMUM_SYSTEM_PARTICLES 4096 /* (512) */ +#define HALO_PORT_MAXIMUM_CONTRAILS 1024 /* (256) */ +#define HALO_PORT_MAXIMUM_CONTRAIL_POINTS 8192 /* (1024) */ +#define HALO_PORT_MAXIMUM_LIGHTS_PER_MAP 4096 /* (896) */ +#define HALO_PORT_MAXIMUM_GAME_LOOPING_SOUNDS 4096 /* (1024) */ + +#endif /* __HALO_PORT_CAPACITY_H */ diff --git a/port/linux/include/halo_port_limits.h b/port/linux/include/halo_port_limits.h index 31163ad5a4..a4857b6d97 100644 --- a/port/linux/include/halo_port_limits.h +++ b/port/linux/include/halo_port_limits.h @@ -28,6 +28,10 @@ Winsock headers default to 64 (the prefix headers define FD_SETSIZE from this before any of them is read) */ #define HALO_PORT_FD_SETSIZE 256 +/* ---------- memory capacity for these limits (game state and pools) */ + +#include "halo_port_capacity.h" + /* ---------- struct network_game layout The game settings record (struct network_game) is declared separately in diff --git a/source/cache/physical_memory_map.c b/source/cache/physical_memory_map.c index b357fa14f2..4e51f32ee5 100644 --- a/source/cache/physical_memory_map.c +++ b/source/cache/physical_memory_map.c @@ -40,9 +40,17 @@ symbols in this file: /* ---------- constants */ +#ifdef HALO_LINUX +/* the native builds' larger game state, placed above the tag cache +(halo_port_capacity.h); the verified part is the CPU part, as on the Xbox */ +#define GAME_STATE_BASE_ADDRESS HALO_PORT_GAME_STATE_BASE_ADDRESS +#define GAME_STATE_SIZE HALO_PORT_GAME_STATE_SIZE +#define GAME_STATE_VERIFY_SIZE HALO_PORT_GAME_STATE_CPU_SIZE +#else #define GAME_STATE_BASE_ADDRESS 0x80061000 #define GAME_STATE_SIZE 0x345000 #define GAME_STATE_VERIFY_SIZE 0x305000 +#endif #define TAG_CACHE_BASE_ADDRESS 0x803A6000 #define TAG_CACHE_SIZE 0x1600000 #define TEXTURE_CACHE_SIZE 0x1600000 diff --git a/source/effects/contrails.c b/source/effects/contrails.c index 40a6fa6f43..e31d55d368 100644 --- a/source/effects/contrails.c +++ b/source/effects/contrails.c @@ -82,7 +82,14 @@ symbols in this file: enum { MAXIMUM_CONTRAIL_INSTANCES = 4, +#ifdef HALO_LINUX + /* the native builds' larger contrail point pool (halo_port_capacity.h); + contrail_update_points lists a contrail's points in an array of this size + without a bound check, so it must stay the size of the pool */ + MAXIMUM_CONTRAIL_POINTS = HALO_PORT_MAXIMUM_CONTRAIL_POINTS +#else MAXIMUM_CONTRAIL_POINTS = 1024 +#endif }; enum @@ -187,8 +194,14 @@ real contrail_scale_random_value( void contrails_initialize( void) { +#ifdef HALO_LINUX + /* the native builds' larger contrail pools (halo_port_capacity.h) */ + contrail_data = game_state_data_new("contrail", HALO_PORT_MAXIMUM_CONTRAILS, 0x44); + contrail_point_data = game_state_data_new("contrail point", MAXIMUM_CONTRAIL_POINTS, 0x38); +#else contrail_data = game_state_data_new("contrail", 256, 0x44); contrail_point_data = game_state_data_new("contrail point", 1024, 0x38); +#endif if (contrail_data && contrail_point_data) return; diff --git a/source/effects/effects.c b/source/effects/effects.c index 5ea49a0cd1..a919ea64d6 100644 --- a/source/effects/effects.c +++ b/source/effects/effects.c @@ -668,8 +668,15 @@ static struct profile_section effects_update_section = {"effects_update", NONE, void effects_initialize( void) { +#ifdef HALO_LINUX + /* the native builds' larger effect pools (halo_port_capacity.h); a full + pool drops deterministic effects, damage included */ + effect_data = game_state_data_new("effect", HALO_PORT_MAXIMUM_EFFECTS, 0xFC); + effect_location_data = game_state_data_new("effect location", HALO_PORT_MAXIMUM_EFFECT_LOCATIONS, 0x3C); +#else effect_data = game_state_data_new("effect", 0x100, 0xFC); effect_location_data = game_state_data_new("effect location", 0x200, 0x3C); +#endif if (!effect_data || !effect_location_data) error(_error_immediate, "couldn't allocate effect globals"); diff --git a/source/effects/particle_systems.c b/source/effects/particle_systems.c index 67c59fc944..59f2ae3fa3 100644 --- a/source/effects/particle_systems.c +++ b/source/effects/particle_systems.c @@ -93,9 +93,16 @@ symbols in this file: enum { +#ifdef HALO_LINUX + /* the native builds' larger particle system pools (halo_port_capacity.h) */ + MAXIMUM_PARTICLE_SYSTEMS = HALO_PORT_MAXIMUM_PARTICLE_SYSTEMS, + PARTICLE_SYSTEM_DATUM_SIZE = 0x158, + MAXIMUM_SYSTEM_PARTICLES = HALO_PORT_MAXIMUM_SYSTEM_PARTICLES, +#else MAXIMUM_PARTICLE_SYSTEMS = 64, PARTICLE_SYSTEM_DATUM_SIZE = 0x158, MAXIMUM_SYSTEM_PARTICLES = 512, +#endif SYSTEM_PARTICLE_DATUM_SIZE = 0x80, }; diff --git a/source/effects/particles.c b/source/effects/particles.c index 87cfcbb9ae..3ac441fb7f 100644 --- a/source/effects/particles.c +++ b/source/effects/particles.c @@ -220,7 +220,14 @@ static char const *particle_effect_marker_names[NUMBER_OF_PARTICLE_EFFECT_MARKER void particles_initialize( void) { +#ifdef HALO_LINUX + /* the native builds' larger particle pool (halo_port_capacity.h); + render_particles sizes its unchecked list of visible particles by the same + value */ + particle_data = game_state_data_new("particle", HALO_PORT_MAXIMUM_PARTICLES, 0x70); +#else particle_data = game_state_data_new("particle", 1024, 0x70); +#endif if (!particle_data) error(_error_immediate, "couldn't allocate particle globals"); diff --git a/source/hs/object_lists.c b/source/hs/object_lists.c index 48ba5efd10..1fac20e49a 100644 --- a/source/hs/object_lists.c +++ b/source/hs/object_lists.c @@ -66,7 +66,13 @@ symbols in this file: enum { MAXIMUM_OBJECT_LISTS_PER_MAP = 48, +#ifdef HALO_LINUX + /* the native builds' larger pool of listed objects (halo_port_capacity.h): + a (players) list alone takes one per living player */ + MAXIMUM_LISTED_OBJECTS_PER_MAP = HALO_PORT_MAXIMUM_LISTED_OBJECTS_PER_MAP, +#else MAXIMUM_LISTED_OBJECTS_PER_MAP = 128, +#endif }; /* ---------- macros */ diff --git a/source/objects/damage.c b/source/objects/damage.c index 62d1dd4c29..2ab18bd80b 100644 --- a/source/objects/damage.c +++ b/source/objects/damage.c @@ -1781,7 +1781,13 @@ void area_of_effect_cause_damage( { struct damage_effect_definition *definition = damage_effect_definition_get(damage->definition_index); +#ifdef HALO_LINUX + /* the native builds damage more objects per explosion + (halo_port_capacity.h): 64 runs out in a crowd of 128 players */ + long object_indices[HALO_PORT_MAXIMUM_AREA_OF_EFFECT_OBJECTS]; +#else long object_indices[64]; +#endif short object_count; object_count = objects_in_sphere( diff --git a/source/objects/objects.c b/source/objects/objects.c index abcc8cb21b..c14409c141 100644 --- a/source/objects/objects.c +++ b/source/objects/objects.c @@ -989,7 +989,13 @@ void objects_initialize( } else { +#ifdef HALO_LINUX + /* five times the native builds' object limit would not fit a data + array's short count */ + object_header_data = data_new("object", SHORT_MAX, sizeof(struct object_header_datum)); +#else object_header_data = data_new("object", MAXIMUM_OBJECTS_PER_MAP*5, sizeof(struct object_header_datum)); +#endif object_memory_pool = memory_pool_new("objects", OBJECT_MEMORY_POOL_SIZE*5); } match_assert("c:\\halo\\SOURCE\\objects\\objects.c", 216, object_header_data && object_memory_pool); diff --git a/source/objects/objects.h b/source/objects/objects.h index 1256e31a2c..a837a2e913 100644 --- a/source/objects/objects.h +++ b/source/objects/objects.h @@ -23,6 +23,23 @@ enum { OBJECT_ITERATOR_SIGNATURE = 0x86868686, MAXIMUM_CLUSTERS_PER_OBJECT = 32, +#ifdef HALO_LINUX + /* the native builds' larger object pool (halo_port_capacity.h). The + garbage limits keep January's proportions: 5/10/20 % of the pool (the pool + only allocates at its end, so these are also the bytes one tick may + allocate before a compaction), 2.5/5/10 % of the object headers, and + 75/50/30 active garbage objects per 16 players */ + OBJECT_MEMORY_POOL_SIZE = HALO_PORT_OBJECT_MEMORY_POOL_SIZE, + GARBAGE_LIMIT_FREE_MEMORY_CRITICAL = HALO_PORT_OBJECT_MEMORY_POOL_SIZE/20, + GARBAGE_LIMIT_FREE_MEMORY_TRIGGER = HALO_PORT_OBJECT_MEMORY_POOL_SIZE/10, + GARBAGE_LIMIT_FREE_MEMORY_TARGET = HALO_PORT_OBJECT_MEMORY_POOL_SIZE/5, + GARBAGE_LIMIT_FREE_OBJECTS_CRITICAL = HALO_PORT_MAXIMUM_OBJECTS_PER_MAP/40, + GARBAGE_LIMIT_FREE_OBJECTS_TRIGGER = HALO_PORT_MAXIMUM_OBJECTS_PER_MAP/20, + GARBAGE_LIMIT_FREE_OBJECTS_TARGET = HALO_PORT_MAXIMUM_OBJECTS_PER_MAP/10, + GARBAGE_LIMIT_ACTIVE_GARBAGE_CRITICAL = 75*HALO_PORT_MAXIMUM_NETWORK_PLAYERS/16, + GARBAGE_LIMIT_ACTIVE_GARBAGE_TRIGGER = 50*HALO_PORT_MAXIMUM_NETWORK_PLAYERS/16, + GARBAGE_LIMIT_ACTIVE_GARBAGE_TARGET = 30*HALO_PORT_MAXIMUM_NETWORK_PLAYERS/16, +#else OBJECT_MEMORY_POOL_SIZE = 0x100000, GARBAGE_LIMIT_FREE_MEMORY_CRITICAL = 52428, GARBAGE_LIMIT_FREE_MEMORY_TRIGGER = 104857, @@ -33,13 +50,20 @@ enum GARBAGE_LIMIT_ACTIVE_GARBAGE_CRITICAL = 75, GARBAGE_LIMIT_ACTIVE_GARBAGE_TRIGGER = 50, GARBAGE_LIMIT_ACTIVE_GARBAGE_TARGET = 30, +#endif }; enum { MAXIMUM_MARKERS_PER_OBJECT = 64, +#ifdef HALO_LINUX + /* the native builds' larger object limits (halo_port_capacity.h) */ + MAXIMUM_OBJECTS_PER_MAP = HALO_PORT_MAXIMUM_OBJECTS_PER_MAP, + MAXIMUM_RENDERED_OBJECTS = HALO_PORT_MAXIMUM_RENDERED_OBJECTS, +#else MAXIMUM_OBJECTS_PER_MAP = 2048, MAXIMUM_RENDERED_OBJECTS = 256, +#endif }; enum diff --git a/source/render/render.h b/source/render/render.h index 6cfd139d74..91003a0588 100644 --- a/source/render/render.h +++ b/source/render/render.h @@ -24,7 +24,12 @@ enum MAXIMUM_RENDERED_CLUSTERS = 128, MAXIMUM_SURFACES_PER_STRUCTURE = 0x20000, MAXIMUM_RENDERED_LIGHTS = 128, +#ifdef HALO_LINUX + /* the native builds' larger light pool (halo_port_capacity.h) */ + MAXIMUM_LIGHTS_PER_MAP = HALO_PORT_MAXIMUM_LIGHTS_PER_MAP, +#else MAXIMUM_LIGHTS_PER_MAP = 896, +#endif MAXIMUM_LENS_FLARES_PER_LIGHT = 8, MAXIMUM_QUEUED_LENS_FLARES = 8, }; diff --git a/source/render/render_objects.c b/source/render/render_objects.c index 7615000a14..d2c0b5bfbc 100644 --- a/source/render/render_objects.c +++ b/source/render/render_objects.c @@ -113,7 +113,12 @@ symbols in this file: enum { +#ifdef HALO_LINUX + /* the native builds' larger render state cache (halo_port_capacity.h) */ + MAXIMUM_CACHED_OBJECT_RENDER_STATES = HALO_PORT_MAXIMUM_CACHED_OBJECT_RENDER_STATES, +#else MAXIMUM_CACHED_OBJECT_RENDER_STATES = 256, +#endif NUMBER_OF_SHADOW_VOLUME_PLANES = 6, MAXIMUM_OBJECT_RENDER_STATE_AGE = 1000, OBJECT_RENDER_STATE_LARGE_INTERVAL = 3, @@ -209,8 +214,14 @@ typedef char object_render_data_size_assert[ sizeof(struct object_render_data) == 0x48 ? 1 : -1]; typedef char object_render_state_size_assert[ sizeof(struct object_render_state) == 0x100 ? 1 : -1]; +#ifdef HALO_LINUX +/* the native builds render up to MAXIMUM_RENDERED_OBJECTS (objects.h) */ +typedef char render_object_globals_size_assert[ + sizeof(struct render_object_globals) == 4 + MAXIMUM_RENDERED_OBJECTS * sizeof(long) ? 1 : -1]; +#else typedef char render_object_globals_size_assert[ sizeof(struct render_object_globals) == 0x404 ? 1 : -1]; +#endif /* ---------- prototypes */ diff --git a/source/render/render_particles.c b/source/render/render_particles.c index 155c977b4c..b61b26eb42 100644 --- a/source/render/render_particles.c +++ b/source/render/render_particles.c @@ -34,7 +34,14 @@ symbols in this file: enum { +#ifdef HALO_LINUX + /* render_particles lists every visible particle in an array of this size + without a bound check, so it must stay the size of the native builds' + particle pool (effects/particles.c, halo_port_capacity.h) */ + MAXIMUM_RENDERED_PARTICLES = HALO_PORT_MAXIMUM_PARTICLES, +#else MAXIMUM_RENDERED_PARTICLES = 1024, +#endif MAXIMUM_RENDERED_PARTICLE_GROUPS = 512, }; diff --git a/source/saved games/game_state.c b/source/saved games/game_state.c index 159950fe03..efbdda92c6 100644 --- a/source/saved games/game_state.c +++ b/source/saved games/game_state.c @@ -125,8 +125,14 @@ symbols in this file: enum { +#ifdef HALO_LINUX + /* the native builds' larger game state (halo_port_capacity.h) */ + GAME_STATE_CPU_SIZE = HALO_PORT_GAME_STATE_CPU_SIZE, + GAME_STATE_GPU_SIZE = HALO_PORT_GAME_STATE_GPU_SIZE, +#else GAME_STATE_CPU_SIZE = 0x305000, GAME_STATE_GPU_SIZE = 0x40000, +#endif GAME_STATE_SIZE = GAME_STATE_CPU_SIZE+GAME_STATE_GPU_SIZE }; @@ -325,7 +331,12 @@ void game_state_save_to_persistent_storage( game_state_globals.base_address, &game_state_globals.header->checksum, sizeof(*game_state_globals.header), +#ifdef HALO_LINUX + /* the whole of the native builds' larger game state */ + GAME_STATE_SIZE); +#else 0x345000); +#endif } return; @@ -343,7 +354,12 @@ boolean game_state_test_persistent_storage( &header, &header.checksum, sizeof(*game_state_globals.header), +#ifdef HALO_LINUX + /* the whole of the native builds' larger game state */ + GAME_STATE_SIZE, +#else 0x345000, +#endif corrupted)) { *difficulty = header.difficulty; @@ -365,7 +381,12 @@ boolean game_state_test_persistent_storage( void game_state_save_core( const char *name) { +#ifdef HALO_LINUX + /* the whole of the native builds' larger game state */ + if (game_state_write_core(name, game_state_globals.base_address, GAME_STATE_SIZE)) +#else if (game_state_write_core(name, game_state_globals.base_address, 0x345000)) +#endif { console_printf(FALSE, "saved '%s'", name); } @@ -625,7 +646,13 @@ void game_state_initialize( void) { crc_new(&game_state_globals.allocation_size_checksum); +#ifdef HALO_LINUX + /* the native builds place their larger game state above the tag cache + (halo_port_capacity.h, cache/physical_memory_map.c) */ + game_state_globals.base_address = game_state_allocate_buffer(HALO_PORT_GAME_STATE_BASE_ADDRESS, GAME_STATE_CPU_SIZE, GAME_STATE_GPU_SIZE); +#else game_state_globals.base_address = game_state_allocate_buffer(0x80061000, GAME_STATE_CPU_SIZE, 0x40000); +#endif game_state_create_or_open_file(); game_state_globals.header = game_state_malloc("header", NULL, sizeof(*game_state_globals.header)); diff --git a/source/saved games/game_state_xbox.c b/source/saved games/game_state_xbox.c index bc64f77fdb..591a52b483 100644 --- a/source/saved games/game_state_xbox.c +++ b/source/saved games/game_state_xbox.c @@ -108,8 +108,15 @@ symbols in this file: enum { CPU_PAGE_SIZE = 0x1000, +#ifdef HALO_LINUX + /* the native builds' larger game state (halo_port_capacity.h); the saved + game files only need to hold it */ + GAME_STATE_SIZE = HALO_PORT_GAME_STATE_SIZE, + GAME_STATE_FILE_SIZE = HALO_PORT_GAME_STATE_SIZE +#else GAME_STATE_SIZE = 0x345000, GAME_STATE_FILE_SIZE = 0x380000 +#endif }; /* ---------- structures */ diff --git a/source/sound/game_sound.c b/source/sound/game_sound.c index 06d6c801ad..19c2267450 100644 --- a/source/sound/game_sound.c +++ b/source/sound/game_sound.c @@ -122,7 +122,12 @@ symbols in this file: enum { +#ifdef HALO_LINUX + /* the native builds' larger pool of object looping sounds (halo_port_capacity.h) */ + MAXIMUM_GAME_LOOPING_SOUNDS = HALO_PORT_MAXIMUM_GAME_LOOPING_SOUNDS, +#else MAXIMUM_GAME_LOOPING_SOUNDS = 1024, +#endif _cluster_sound_distance_unreachable_bit = 7, CLUSTER_SOUND_DISTANCE_VALUE_MASK = 0x7F, _game_looping_sound_unattached_bit = 0, diff --git a/source/structures/cluster_partitions.c b/source/structures/cluster_partitions.c index 159f920bd8..f13e9410c7 100644 --- a/source/structures/cluster_partitions.c +++ b/source/structures/cluster_partitions.c @@ -174,10 +174,20 @@ void cluster_partition_new( MAXIMUM_CLUSTERS_PER_STRUCTURE * sizeof(*partition->cluster_first_data_references)); sprintf(cluster_name, "cluster %s", name); +#ifdef HALO_LINUX + /* the native builds' longer reference lists (halo_port_capacity.h): an + object or light that cannot be referenced drops out of its clusters */ + partition->data_reference_data = reference_list_new(cluster_name, HALO_PORT_MAXIMUM_CLUSTER_REFERENCES); +#else partition->data_reference_data = reference_list_new(cluster_name, 2048); +#endif sprintf(cluster_name, "%s cluster", name); +#ifdef HALO_LINUX + partition->cluster_reference_data = reference_list_new(cluster_name, HALO_PORT_MAXIMUM_CLUSTER_REFERENCES); +#else partition->cluster_reference_data = reference_list_new(cluster_name, 2048); +#endif if (!partition->cluster_first_data_references || !partition->cluster_reference_data || From 5d93d24e87093d4bb5bcd72c496a7b840470d6d9 Mon Sep 17 00:00:00 2001 From: Jonas Volman Date: Sat, 26 Sep 2026 03:13:42 -0700 Subject: [PATCH 09/12] Networking: keep a host of 128 machines responsive Found by running a 128-machine session with tools/system_link_bots.py: - The listen backlog was 32 connections, so machines joining at once were refused; it is now the machine limit. - Socket buffers are at least 1 MB. A host's connection to a machine must hold every update the machine has not acknowledged, up to 128 ticks of 3.9 KB with 128 players; a full buffer blocked the host for the write timeout. The host's connection to its own client must also hold all it sends in one frame. - Each machine that finished loading logged a line for every machine still loading, 8,000 lines as 128 machines load; it now logs a count. - Removing a machine dumped the whole server state to the log; when many machines left at once the host wrote dumps for minutes. - Machines that had loaded heard nothing while the others loaded, and a client drops a connection that is silent for 15 seconds, less than the 60 seconds the host waits for slow machines. The host now sends its pregame keep-alive every 5 seconds while machines load; a client in game ignores it. All under HALO_LINUX; the MSVC build is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../network/transport_endpoint_winsock.c | 13 ++++++ source/networking/network_server_manager.c | 41 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/source/bungie_net/network/transport_endpoint_winsock.c b/source/bungie_net/network/transport_endpoint_winsock.c index 37cecff28b..20728443a5 100644 --- a/source/bungie_net/network/transport_endpoint_winsock.c +++ b/source/bungie_net/network/transport_endpoint_winsock.c @@ -292,9 +292,22 @@ enum { _transport_type_udp = 0x11, _transport_type_tcp, +#ifdef HALO_LINUX + /* every machine of a session may connect at once */ + MAXIMUM_PENDING_CONNECTIONS = HALO_PORT_MAXIMUM_NETWORK_MACHINES, +#else MAXIMUM_PENDING_CONNECTIONS = 32, +#endif MAXIMUM_ENDPOINT_THREADS = 64, +#ifdef HALO_LINUX + /* a host's connection to a machine holds the updates that machine has + not acknowledged, up to 128 ticks of 3.9 KB with 128 players, and the + host's connection to its own client all it sends in one frame; a full + buffer blocks the host (network_connection_write) */ + MINIMUM_ENDPOINT_SOCKET_BUFFER_SIZE = 1024 * 1024, +#else MINIMUM_ENDPOINT_SOCKET_BUFFER_SIZE = 16 * 1024, +#endif }; /* ---------- macros */ diff --git a/source/networking/network_server_manager.c b/source/networking/network_server_manager.c index 6de779fb3e..02eeefd99e 100644 --- a/source/networking/network_server_manager.c +++ b/source/networking/network_server_manager.c @@ -1603,6 +1603,9 @@ void network_game_server_client_machine_game_loading_complete( { boolean all_machines_loaded = TRUE; long client_machine_index; +#ifdef HALO_LINUX + long loading_machine_count = 0; +#endif match_assert(NETWORK_SERVER_MANAGER_FILE, 0x4ED, server); match_assert(NETWORK_SERVER_MANAGER_FILE, 0x4EE, machine); @@ -1620,13 +1623,26 @@ void network_game_server_client_machine_game_loading_complete( client_machine->machine_index < MAXIMUM_NETWORK_MACHINE_COUNT && !TEST_FLAG(client_machine->flags, _network_client_machine_level_loaded_bit)) { +#ifdef HALO_LINUX + loading_machine_count++; +#else network_event( "still waiting on machine #%d to finish loading", client_machine->machine_index); +#endif all_machines_loaded = FALSE; } } +#ifdef HALO_LINUX + /* a line per machine still loading, each time one finishes, is 8,000 + lines as 128 machines load, and the host writes its log a line at a time */ + if (loading_machine_count) + { + network_event("still waiting for machines to finish loading (%ld left)", loading_machine_count); + } +#endif + if (all_machines_loaded == TRUE) network_game_server_all_machines_have_loaded(server); @@ -3102,7 +3118,12 @@ static boolean network_game_server_handle_client_machines( network_event( "client machine %x removed from game", server->client_machines[i].machine_index); + /* the native builds skip this dump: it lists every machine + and player, and when many machines leave at once the + dumps keep the host writing its log for minutes */ +#ifndef HALO_LINUX network_game_server_dump(server); +#endif } else { @@ -3415,6 +3436,26 @@ static boolean network_game_server_idle_pregame_tasks( } else if (server->time_of_first_client_loading_completion) { +#ifdef HALO_LINUX + /* machines that have loaded wait for the others in silence, and a + client drops a connection it hears nothing on for 15 seconds, less than + the wait for the others: keep their connections alive (a client in game + ignores a pregame keep-alive) */ + if ((unsigned long)now - server->time_of_last_keep_alive > + 5UL * MILLISECONDS_PER_SECOND) + { + struct message_server_pregame_keep_alive message_packet = { 0 }; + struct network_message *message; + + message = create_network_game_message( + _message_server_pregame_keep_alive, + &message_packet, + sizeof(message_packet)); + network_game_server_send_message_to_all_machines(server, message); + + server->time_of_last_keep_alive = now; + } +#endif if (system_milliseconds() - server->time_of_first_client_loading_completion >= NETWORK_GAME_SERVER_MAXIMUM_WAIT_TIME_FOR_LEVEL_LOADING) { From e1e55bd22e9c63573a440f2ee0248f5c1e65f254 Mon Sep 17 00:00:00 2001 From: Jonas Volman Date: Sat, 26 Sep 2026 03:13:43 -0700 Subject: [PATCH 10/12] Port: keep the debug log open between lines write_to_error_file opened and closed debug.txt for every line, which takes milliseconds on Windows. A host logs thousands of lines when a hundred machines join, load or leave at once, and while it writes them its own client and every other machine wait. The native builds now keep the file open and flush each line. Co-Authored-By: Claude Opus 5.5 --- source/cseries/errors.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/source/cseries/errors.c b/source/cseries/errors.c index 7a446239ff..3877afc4bc 100644 --- a/source/cseries/errors.c +++ b/source/cseries/errors.c @@ -137,7 +137,19 @@ void write_to_error_file( if (error_globals.output_to_debug_file) { +#ifdef HALO_LINUX + /* the native builds keep the log open and flush each line: opening + and closing it per line takes milliseconds on Windows, and a host logs + thousands of lines when a hundred machines join, load or leave */ + static FILE *handle = NULL; + + if (!handle) + { + handle = fopen("d:\\debug.txt", "a+b"); + } +#else FILE *handle = fopen("d:\\debug.txt", "a+b"); +#endif if (handle) { if (date) @@ -165,7 +177,11 @@ void write_to_error_file( } } fprintf(handle, "%s", string); +#ifdef HALO_LINUX + fflush(handle); +#else fclose(handle); +#endif } } From cc86f2c50f523fe8c922551b2d02d63cf3ea943e Mon Sep 17 00:00:00 2001 From: Jonas Volman Date: Sat, 26 Sep 2026 03:13:57 -0700 Subject: [PATCH 11/12] Tools: stand-in machines for testing large system link sessions tools/system_link_bots.py joins a native-build host with many lightweight machines, each with one player, speaking the system link protocol directly: join, settings (reassembling the fragmented record), player, map precached, loaded, then a UDP input message each tick that acknowledges the host's updates, as a real client keeps a lockstep game running. Each machine binds its own loopback address (127.0.0.2 and up), since the host tells machines apart by address. python tools/system_link_bots.py --machines 127 --start fills a 128-machine session on the host at 127.0.0.1 and starts it. Co-Authored-By: Claude Opus 5.5 --- tools/system_link_bots.py | 388 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 tools/system_link_bots.py diff --git a/tools/system_link_bots.py b/tools/system_link_bots.py new file mode 100644 index 0000000000..f4e8749bca --- /dev/null +++ b/tools/system_link_bots.py @@ -0,0 +1,388 @@ +"""Stand-in system link machines for testing large multiplayer sessions. + +Joins a native-build Halo host (port/, built with HALO_LINUX) with many +lightweight machines, each with one player, speaking the game's system link +protocol directly: TCP to the host's port 5150 for the game's messages, UDP for +player input. Each machine binds its own loopback address (127.0.0.2, +127.0.0.3, ...) because the host tells machines apart by address, so run it on +the host's computer (or give it --first-address on a network where the +addresses are yours). + + python tools/system_link_bots.py --machines 127 --start + +joins 127 machines to the host at 127.0.0.1, marks the map precached, asks +the host to start once everyone is in, then plays: every machine +acknowledges each tick's update and sends its player's input (standing +still and slowly turning), the way a real client keeps a lockstep game +running. The bots do not simulate the game; they only keep up with the +host's update stream. Ctrl+C leaves. + +The protocol (network_messages.c): a message is a 2-byte big-endian header +(length << 4 | type << 2, length including the header), then a packet: a +version byte, the fields big-endian (bytes and raw fields as they are), and +the packet type in a trailing byte. The native builds send the game settings +record in pieces of HALO_PORT_NETWORK_GAME_SETTINGS_FRAGMENT_SIZE bytes. +""" + +import argparse +import math +import selectors +import socket +import struct +import sys +import time + +SERVER_PORT = 0x141E +CLIENT_PORT = 0x141F +MESSAGE_TYPE_PACKET = 3 +PACKET_VERSION = 1 +JOIN_TOKEN = b"message in a bottle"[:16] # network_game_generate_join_game_token (DEBUG builds) +SETTINGS_FRAGMENT_SIZE = 0xE00 +NONE = -1 + +# network_game_message_type +CLIENT_BROADCAST_GAME_SEARCH = 0 +SERVER_GAME_ADVERTISE = 2 +SERVER_MACHINE_ACCEPTED = 4 +SERVER_MACHINE_REJECTED = 5 +SERVER_GAME_SETTINGS_UPDATE = 6 +SERVER_PREGAME_COUNTDOWN = 7 +SERVER_BEGIN_GAME = 8 +SERVER_GRACEFUL_GAME_EXIT_PREGAME = 9 +SERVER_PREGAME_KEEP_ALIVE = 10 +SERVER_POSTGAME_KEEP_ALIVE = 11 +CLIENT_JOIN_GAME_REQUEST = 12 +CLIENT_ADD_PLAYER_REQUEST_PREGAME = 13 +CLIENT_SETTINGS_REQUEST = 15 +CLIENT_GAME_START_REQUEST = 17 +CLIENT_MAP_IS_PRECACHED_PREGAME = 19 +SERVER_GAME_UPDATE = 20 +SERVER_ADD_PLAYER_INGAME = 21 +SERVER_REMOVE_PLAYER_INGAME = 22 +SERVER_GAME_OVER = 23 +CLIENT_LOADED = 24 +CLIENT_GAME_UPDATE = 25 +SERVER_SWITCH_TO_PREGAME = 30 +SERVER_GRACEFUL_GAME_EXIT_POSTGAME = 31 + +COUNTDOWN_EVENT_START_IMMEDIATELY = 3 + +MESSAGE_NAMES = { + SERVER_GAME_ADVERTISE: "advertise", SERVER_MACHINE_ACCEPTED: "machine_accepted", + SERVER_MACHINE_REJECTED: "machine_rejected", SERVER_GAME_SETTINGS_UPDATE: "settings", + SERVER_PREGAME_COUNTDOWN: "countdown", SERVER_BEGIN_GAME: "begin_game", + SERVER_GRACEFUL_GAME_EXIT_PREGAME: "exit_pregame", SERVER_PREGAME_KEEP_ALIVE: "keep_alive", + SERVER_POSTGAME_KEEP_ALIVE: "postgame_keep_alive", SERVER_GAME_UPDATE: "game_update", + SERVER_ADD_PLAYER_INGAME: "add_player_ingame", SERVER_REMOVE_PLAYER_INGAME: "remove_player_ingame", + SERVER_GAME_OVER: "game_over", SERVER_SWITCH_TO_PREGAME: "switch_to_pregame", + SERVER_GRACEFUL_GAME_EXIT_POSTGAME: "exit_postgame", +} + + +def network_game_layout(machines, players): + """Offsets of struct network_game (port/linux/include/halo_port_limits.h).""" + player_count = 0x114 + machines * 0x44 + players_offset = player_count + 2 + size = players_offset + players * 0x20 + 0xE + return {"map_name": 0x24, "machine_count": 0x112, "player_count": player_count, + "players": players_offset, "size": size} + + +def wide(text, count): + units = [ord(c) for c in text[:count - 1]] + units += [0] * (count - len(units)) + return struct.pack(">%dH" % count, *units) + + +def message(packet_type, payload): + packet = bytes([PACKET_VERSION]) + payload + bytes([packet_type]) + length = len(packet) + 2 + assert length <= 0xFFF, "message too long for its header" + return struct.pack(">H", (length << 4) | (MESSAGE_TYPE_PACKET << 2)) + packet + + +def network_player(name, machine_index, color): + # shorts 12 (name), shorts 2 (colour, icon), bytes 4 (machine, controller, team, list index) + return (wide(name, 12) + struct.pack(">hh", color, 0) + + struct.pack("bbbb", machine_index, 0, NONE, NONE)) + + +def player_action(yaw): + # longs 6 (control flags, desired facing yaw and pitch, throttle i and j, + # primary trigger), shorts 3 (weapon, grenade, zoom level); the pad is not sent + return struct.pack(">Ifffff", 0, yaw, 0.0, 0.0, 0.0, 0.0) + struct.pack(">hhh", 0, 0, NONE) + + +class Machine: + def __init__(self, index, address, host, log): + self.index = index + self.address = address + self.host = host + self.log = log + self.name = "bot%d" % index + self.state = "connecting" + self.machine_index = None + self.buffer = b"" + self.settings = bytearray() + self.settings_complete = None + self.map_name = None + self.last_update_number = None + self.updates_received = 0 + self.bytes_received = 0 + self.last_precache_time = 0 + self.player_added = False + self.player_list_index = None + self.connect_attempts = 0 + self.retry_time = 0 + self.load_seconds = 1.0 + self.tcp = None + self.udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.udp.bind((address, CLIENT_PORT)) + self.udp.setblocking(False) + + def connect(self): + self.connect_attempts += 1 + self.state = "connecting" + self.tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.tcp.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1 << 20) + self.tcp.bind((self.address, 0)) + self.tcp.setblocking(False) + try: + self.tcp.connect((self.host, SERVER_PORT)) + except BlockingIOError: + pass + except OSError as error: + if error.errno not in (10035, 115, 36): # WSAEWOULDBLOCK / EINPROGRESS + raise + + def send(self, data): + view = memoryview(data) + deadline = time.time() + 5 + while view: + try: + sent = self.tcp.send(view) + view = view[sent:] + except BlockingIOError: + if time.time() > deadline: + raise + time.sleep(0.001) + + def joined(self): + self.send(message(CLIENT_JOIN_GAME_REQUEST, wide(self.name, 32) + JOIN_TOKEN)) + self.state = "joining" + + def receive(self): + try: + data = self.tcp.recv(1 << 20) + except BlockingIOError: + return True + except ConnectionError: + self.log("%s: connection lost" % self.name) + self.state = "closed" + return False + if not data: + self.log("%s: host closed the connection" % self.name) + self.state = "closed" + return False + self.bytes_received += len(data) + self.buffer += data + while len(self.buffer) >= 2: + header = struct.unpack(">H", self.buffer[:2])[0] + length = header >> 4 + if length < 3: + self.log("%s: bad message header %04x" % (self.name, header)) + self.state = "closed" + return False + if len(self.buffer) < length: + break + packet = self.buffer[2:length] + self.buffer = self.buffer[length:] + self.handle(packet[-1], packet[1:-1]) + return True + + def handle(self, packet_type, payload): + if packet_type == SERVER_MACHINE_ACCEPTED: + random_seed, self.machine_index = struct.unpack(">ih", payload[:6]) + self.state = "pregame" + self.send(message(CLIENT_SETTINGS_REQUEST, wide(self.name, 32) + bytes([self.machine_index & 0xFF]))) + elif packet_type == SERVER_MACHINE_REJECTED: + self.log("%s: rejected, reason %d" % (self.name, struct.unpack(">h", payload[:2])[0])) + self.state = "rejected" + elif packet_type == SERVER_GAME_SETTINGS_UPDATE: + total, offset, length = struct.unpack(">HHH", payload[:6]) + data = payload[6:6 + length] + if offset == 0: + self.settings = bytearray() + if offset == len(self.settings): + self.settings += data + if len(self.settings) == total: + self.settings_complete = bytes(self.settings) + self.map_name = self.settings_complete[0x24:0x24 + 0x80].split(b"\0")[0] + if not self.player_added and self.machine_index is not None: + self.player_added = True + color = self.index % 18 + self.send(message(CLIENT_ADD_PLAYER_REQUEST_PREGAME, + network_player(self.name, self.machine_index, color))) + elif packet_type == SERVER_BEGIN_GAME: + self.state = "loading" + self.loaded_at = time.time() + self.load_seconds + elif packet_type == SERVER_GAME_UPDATE: + update_number = struct.unpack(">I", payload[:4])[0] + self.last_update_number = update_number + self.updates_received += 1 + if self.state != "ingame": + self.state = "ingame" + elif packet_type in (SERVER_GAME_OVER,): + self.state = "postgame" + elif packet_type in (SERVER_SWITCH_TO_PREGAME,): + self.state = "pregame" + self.last_update_number = None + self.player_added = True + elif packet_type in (SERVER_GRACEFUL_GAME_EXIT_PREGAME, SERVER_GRACEFUL_GAME_EXIT_POSTGAME): + self.log("%s: host ended the game" % self.name) + self.state = "closed" + + def tick(self, now): + if self.state == "pregame" and self.map_name and now - self.last_precache_time > 1.0: + self.last_precache_time = now + self.send(message(CLIENT_MAP_IS_PRECACHED_PREGAME, self.map_name.ljust(256, b"\0")[:256])) + if self.state == "loading" and now >= self.loaded_at: + self.send(message(CLIENT_LOADED, struct.pack(">i", 0))) + self.state = "loaded" + if self.state == "ingame" and self.last_update_number is not None: + # the next update this machine expects (acknowledging the ones it + # has), then an array of its players' actions: a count byte and one + # action + yaw = (now * 0.5 + self.index) % (2 * math.pi) + payload = (struct.pack(">I", (self.last_update_number + 1) & 0x7FFFFFFF) + bytes([1]) + + player_action(yaw)) + try: + self.udp.sendto(message(CLIENT_GAME_UPDATE, payload), (self.host, SERVER_PORT)) + except OSError: + pass + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--machines", type=int, default=8) + parser.add_argument("--first-address", default="127.0.0.2", + help="each machine binds this address plus its number") + parser.add_argument("--start", action="store_true", + help="ask the host to start as soon as every machine has a player") + parser.add_argument("--start-delay", type=float, default=3.0) + parser.add_argument("--seconds", type=float, default=0, help="leave after this long (0: until Ctrl+C)") + parser.add_argument("--rate", type=float, default=30.0, help="input messages per second per machine") + parser.add_argument("--join-rate", type=float, default=20.0, help="machines connecting per second") + parser.add_argument("--load-seconds", type=float, default=1.0, + help="how long each machine takes to load the map once the game begins") + parser.add_argument("--status-every", type=float, default=5.0) + options = parser.parse_args() + + started = time.time() + + def log(text): + print("[%7.2f] %s" % (time.time() - started, text), flush=True) + + base = [int(part) for part in options.first_address.split(".")] + machines = [] + for index in range(options.machines): + value = (base[0] << 24 | base[1] << 16 | base[2] << 8 | base[3]) + index + address = "%d.%d.%d.%d" % (value >> 24, value >> 16 & 255, value >> 8 & 255, value & 255) + machine = Machine(index + 1, address, options.host, log) + machine.state = "waiting" + machine.load_seconds = options.load_seconds + machines.append(machine) + + # machines connect a few at a time: a host accepts connections from its + # frame loop, and one whose listen backlog is full refuses the rest + selector = selectors.DefaultSelector() + waiting = list(machines) + next_connect_time = 0 + log("connecting %d machines to %s:%d" % (len(machines), options.host, SERVER_PORT)) + + start_requested = False + all_in_time = None + last_status = 0 + last_input = 0 + try: + while True: + now = time.time() + if waiting and now >= next_connect_time: + machine = next((m for m in waiting if m.retry_time <= now), None) + if machine: + waiting.remove(machine) + machine.connect() + selector.register(machine.tcp, selectors.EVENT_READ | selectors.EVENT_WRITE, machine) + next_connect_time = now + 1.0 / options.join_rate + for key, events in selector.select(timeout=0.005): + machine = key.data + if machine.state == "connecting" and events & selectors.EVENT_WRITE: + error = machine.tcp.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if error: + selector.unregister(machine.tcp) + machine.tcp.close() + if error in (10061, 111) and machine.connect_attempts < 20: # connection refused + machine.state = "waiting" + machine.retry_time = now + 0.5 + waiting.append(machine) + else: + log("%s: connect failed (%d)" % (machine.name, error)) + machine.state = "closed" + continue + selector.modify(machine.tcp, selectors.EVENT_READ, machine) + machine.joined() + if events & selectors.EVENT_READ and machine.state != "closed": + if not machine.receive(): + selector.unregister(machine.tcp) + if now - last_input >= 1.0 / options.rate: + last_input = now + for machine in machines: + if machine.state != "closed": + try: + machine.tick(now) + except OSError as error: + log("%s: %s" % (machine.name, error)) + machine.state = "closed" + if options.start and not start_requested: + ready = [m for m in machines if m.state == "pregame" and m.player_added and m.settings_complete] + if len(ready) == len(machines) and all_in_time is None: + all_in_time = now + log("all %d machines are in the lobby" % len(machines)) + if all_in_time is not None and now - all_in_time >= options.start_delay: + start_requested = True + log("asking the host to start the game") + machines[0].send(message(CLIENT_GAME_START_REQUEST, struct.pack(">h", COUNTDOWN_EVENT_START_IMMEDIATELY))) + if now - last_status >= options.status_every: + last_status = now + states = {} + for machine in machines: + states[machine.state] = states.get(machine.state, 0) + 1 + sample = next((m for m in machines if m.settings_complete), None) + players = machines_in_game = None + if sample: + for machine_slots in (128, 4): + layout = network_game_layout(machine_slots, 128 if machine_slots == 128 else 16) + if len(sample.settings_complete) == layout["size"]: + machines_in_game = struct.unpack_from("= options.seconds: + break + if all(m.state in ("closed", "rejected") for m in machines): + log("every machine has left") + break + except KeyboardInterrupt: + pass + for machine in machines: + if machine.tcp: + machine.tcp.close() + machine.udp.close() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 7fc08be0ad142df2e43636a3c2c00a9cab45c9cf Mon Sep 17 00:00:00 2001 From: Jonas Volman Date: Sat, 26 Sep 2026 03:26:50 -0700 Subject: [PATCH 12/12] Docs: system link games of up to 128 players port/linux/README.md (which the Windows and Android READMEs refer to for settings) describes the native builds' session limits, what a session of 128 machines costs the host, the lobby and scoreboard behaviour, running several copies on one computer with HALO_NET_ADDRESS and HALO_NET_BROADCAST, and tools/system_link_bots.py, and lists the game sources the change touches. Co-Authored-By: Claude Opus 5.5 --- port/linux/README.md | 66 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/port/linux/README.md b/port/linux/README.md index b143234735..1a3ffed20e 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -85,6 +85,8 @@ further gamepads become controllers 2-4. | `HALO_LANGUAGE` | dashboard language: `en`, `ja`, `de`, `fr`, `es`, `it` | | `HALO_INTERPOLATION=0` | the original 30 frames per second (see Frame rate) | | `HALO_NO_VSYNC` | do not wait for the display between frames | +| `HALO_NET_ADDRESS=` | this machine's system link address: sockets bind to it instead of to every address, other machines see games at it, and traffic to 127.0.0.1 goes to it. Lets several copies of the game play together on one computer, each on its own loopback address (see System link) | +| `HALO_NET_BROADCAST=,...` | send the game search broadcast to these addresses instead of 255.255.255.255, for example to the loopback address of a host on the same computer | | `HALO_SCREENSHOT_DIR`, `HALO_SCREENSHOT_EVERY` | write every Nth presented frame as a BMP | | `HALO_GPU_STATS`, `HALO_GPU_TRACE=` (with `HALO_GPU_TRACE_CONSTANTS`), `HALO_GPU_DUMP_SHADERS=`, `HALO_TEXTURE_DUMP=`, `HALO_TEXTURE_LOG`, `HALO_GL_DEBUG`, `HALO_TEXTURE_NO_CACHE` | renderer debugging: per-frame counts, a full state trace of one frame, the generated GLSL, uploaded textures | | `HALO_GPU_SKIP_VS=,...`, `HALO_GPU_DEBUG_EXPR=`, `HALO_GPU_DEBUG_FLAT`, `HALO_GPU_DEBUG_T0` | renderer debugging: drop draws by vertex shader, or replace every pixel shader's output with a GLSL expression (for example `t0.rgb` or `xD0.rgb`) | @@ -112,6 +114,62 @@ the bottom right of the screen: in the native builds the frames per second averaged over half a second (the Xbox showed each frame's own rate, which cannot read above 100). +### System link + +The native builds play system link games of up to 128 players on up to +128 machines, where the Xbox game allows 16 players on up to 4. Split +screen stays at 4 players per machine. The two limits are constants in +`include/halo_port_limits.h`; the memory they need, a 16 MB game state at +`0x81A00000` instead of 3.3 MB and larger pools of objects, effects, +particles, contrails, lights and sounds, is set in +`include/halo_port_capacity.h`. The byte-matching build keeps the Xbox +limits: every change is under `#ifdef HALO_LINUX`. + +- Every machine in a game must run a build with the same limits and + capacities: the game runs in lockstep on every machine, and a pool that + fills on one machine and not on another changes the game. The native + builds' messages differ from the Xbox game's (longer arrays, and the + 13 KB game settings record sent in four pieces), so they search for + games with protocol version 2: they see neither the Xbox game nor older + native builds, and those do not see them. +- The host sends every machine every player's input each tick: 3.9 KB per + tick with 128 players, about 0.9 Mbit/s to each machine and 118 Mbit/s + of upload for a host of 128 machines (measured). The traffic grows with + the square of the session; a host of 32 machines with one player each + sends about 8 Mbit/s. +- The host waits up to 60 seconds (15 on the Xbox) for slower machines to + load the map, and keeps the machines that have loaded connected + meanwhile. +- The lobby has panels for the local machine and three remote machines, + and shows the first three remote machines to join; the others are in + the game all the same. Finishing places past 16th, which the game's + string lists lack, are written out in English (17th, 21st, 22nd, ...), + and in free-for-all games every player is a team of one. + +Several copies of the game can play together on one computer. The host +tells machines apart by address, so every copy needs its own loopback +address, the host included: for example +`HALO_NET_ADDRESS=127.0.0.200` for the host, and +`HALO_NET_ADDRESS=127.0.0.201` (`.202`, ...) with +`HALO_NET_BROADCAST=127.0.0.200` for the others, which then find the +host's game. Never give a copy 127.0.0.1: every copy reaches its own +address through 127.0.0.1. Linux and Windows route all of 127.0.0.0/8 to +the loopback interface without configuration. + +`tools/system_link_bots.py` fills a session without a hundred copies of the +game. It joins a host with lightweight stand-in machines, one player each, +that speak the system link protocol, acknowledge every tick and send input, +but do not simulate the game. On the host's computer (each stand-in binds +its own loopback address, 127.0.0.2 and up), create a game on the host and +run + +```sh +python tools/system_link_bots.py --host 127.0.0.200 --machines 127 --start +``` + +`--start` starts the game once every stand-in is in the lobby. A host +without `HALO_NET_ADDRESS` is found at the default `--host 127.0.0.1`. + ## What works | Area | Status | @@ -126,7 +184,7 @@ cannot read above 100). | Time | Tick count, performance counter (1 MHz), system time, x87 control word (`_control87`). | | Save games and signatures | `XCreateSaveGame` & co. with the Xbox `UDATA` layout; SHA-1 content signatures. | | C runtime | MSVC-only functions, and a 16-bit `wchar_t` runtime (UTF-16 like the Xbox) including MSVC-style wide `printf`. | -| Networking | Winsock over BSD sockets; XNet addresses collapse to plain IPv4 (system link on a LAN). | +| Networking | Winsock over BSD sockets; XNet addresses collapse to plain IPv4 (system link on a LAN), with games of up to 128 players on up to 128 machines (see System link). | | Bink video | Not supported (the RAD SDK is proprietary); `BinkOpen` fails and the game skips the movie. | | Debug monitor (`xbdm`) | Empty module lists. | @@ -220,14 +278,16 @@ structure visibility (whole areas of a level vanished depending on the view). The header copy now agrees with the real function; the matching report is unchanged. -Three more changes exist only under `#ifdef HALO_LINUX` (defined by the -Linux prefix header, never by the matching build): +Some changes exist only under `#ifdef HALO_LINUX` (defined by the Linux +prefix header, never by the matching build): | File | Change | | --- | --- | | `scenario/scenario.c` | the structure BSP connection tables are named directly instead of being addressed at MSVC's offsets from `global_structure_bsp_index` | | `rasterizer/xbox/rasterizer_xbox_environment_fog.c` | a local pointer initialized from the file-scope array of the same name; MSVC resolved the name in the initializer to the array, standard C to the new local | | `game/player_control.c` | adds direct mouse aim (`halo_linux_mouse_look`) to the facing change of the player on controller 1 | +| `networking/`, `game/` (players, player queues, game engine and its game types), `interface/` (lobby, HUD, motion sensor), `bungie_net/network/`, and the pools in `objects/`, `effects/`, `render/`, `sound/`, `hs/`, `structures/`, `cache/physical_memory_map.c` and `saved games/` | the system link limits and the memory they need (see System link); sizes and offsets that followed from the Xbox limits come from `include/halo_port_limits.h` and `include/halo_port_capacity.h` | +| `cseries/errors.c` | `debug.txt` stays open between lines (opening and closing it for each line took milliseconds on Windows, and a large session logs thousands of lines at once) | The game's x86 inline assembly is also replaced under `#ifdef HALO_LINUX`, which every native port (Linux, Windows, Android) defines, so the compiler