Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion port/linux/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,9 @@ player's profile is chosen on the ADD PLAYER screen that opens (with any
controller). With one gamepad, choose the lobby's ADD PLAYER button first:
until then that gamepad shares controller 1 with the keyboard. Two players on one profile get different names from the host. A
player's B in the lobby leaves the game alone, and the last player of the
computer leaves it for all of them. In the game, each player's pause menu
computer leaves it for all of them. When all the players are on the host's
computer, START NOW starts the game without another computer, and a team
game does not need a player on each team. In the game, each player's pause menu
opens on their part of the screen, and its LEAVE GAME is theirs: their part
of the screen stays until the game ends. A game under way shows its own
screen before JOIN GAME: players join there the same way, START or ADD
Expand Down
2 changes: 1 addition & 1 deletion source/interface/ui_widget_game_data_input_functions.c
Original file line number Diff line number Diff line change
Expand Up @@ -2922,7 +2922,7 @@ static void multiplayer_game_directions(

if (server)
{
/* port: one machine and one player may start (server_alone) */
/* port: one machine and its players may start (server_players_on_one_machine) */
boolean waiting_for_machines = !network_game_is_splitscreen_local() &&
game &&
game->machine_count < 1;
Expand Down
38 changes: 30 additions & 8 deletions source/networking/network_server_manager.c
Original file line number Diff line number Diff line change
Expand Up @@ -3308,12 +3308,34 @@ boolean server_has_enough_machines(
return has_enough_machines;
}

/* port: a game's one player, with no one else in it yet, may start it:
a system link or internet game's host, or split screen's first player */
static boolean server_alone(
/* port: TRUE if the game has at least one player and every player is on the
same machine; FALSE with no player. Such a game may start as a host alone
does: a system link or internet game's host, alone or with split screen
players. Machines are not counted, only players' machines: a machine that
has joined without a player does not stop the host's countdown
(server_ok_to_countdown) */
static boolean server_players_on_one_machine(
struct network_game_server *server)
{
return server->game.player_count == 1;
short first_machine_index = NONE;
long player_index;

for (player_index = 0;
player_index < MAXIMUM_NETWORK_PLAYER_COUNT;
player_index++)
{
struct network_player *player = &server->game.players[player_index];

if (!network_player_is_valid(player))
continue;

if (first_machine_index == NONE)
first_machine_index = (short)player->machine_index;
else if ((short)player->machine_index != first_machine_index)
return FALSE;
}

return first_machine_index != NONE;
}

/* port: a machine joining or leaving is in the game before its player is.
Expand All @@ -3324,8 +3346,8 @@ boolean server_ok_to_countdown(
struct network_game_server *server)
{
if (server_has_enough_machines(server) &&
(!server_needs_more_teams(server) || server_alone(server)) &&
(server->game.player_count >= server->game.minimum_players || server_alone(server)))
(!server_needs_more_teams(server) || server_players_on_one_machine(server)) &&
(server->game.player_count >= server->game.minimum_players || server_players_on_one_machine(server)))
{
return TRUE;
}
Expand Down Expand Up @@ -3486,7 +3508,7 @@ boolean network_game_server_game_can_start(
match_assert(NETWORK_SERVER_MANAGER_FILE, 0x782, server);

return server->state == 0 &&
(server->game.player_count >= server->game.minimum_players || server_alone(server));
(server->game.player_count >= server->game.minimum_players || server_players_on_one_machine(server));
}

void network_game_server_pause_countdown(
Expand Down Expand Up @@ -4003,7 +4025,7 @@ void network_game_server_update_countdown(
{
if (network_game_should_accept_remote_connections() == FALSE ||
network_game_server_get_client_machine_count(server) > 1 ||
server_alone(server))
server_players_on_one_machine(server))
{
unsigned long countdown;

Expand Down
181 changes: 181 additions & 0 deletions tools/harness/tests/lobby_countdown.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
LOBBY_COUNTDOWN.C (test)

The real pregame countdown of source/networking/network_server_manager.c:
whether the lobby's START NOW (the "faster" request,
_network_game_server_countdown_event_player_joined) starts the countdown,
for a host alone, a host with split screen players, and machines together.
test_lobby_countdown.py takes the constants, enums and timer structures
(config.inc) and the code (under_test.inc).
*/

#include "harness.h"

#define NETWORK_SERVER_MANAGER_FILE "server"
#define MIN(a, b) ((a) < (b) ? (a) : (b))

#include "config.inc"

struct network_player
{
boolean valid;
signed char machine_index;
signed char team_index;
};

struct network_game_server_client_machine
{
void *connection;
boolean joined;
};

struct network_game_server
{
short state;
struct
{
short player_count;
short minimum_players;
struct
{
struct
{
boolean teams;
} universal_variant;
} variant;
struct network_player players[MAXIMUM_NETWORK_PLAYER_COUNT];
} game;
struct network_game_server_client_machine client_machines[MAXIMUM_NETWORK_MACHINE_COUNT];
struct network_game_server_countdown_state countdown_state;
};

static struct network_game_server server;
static int connection;

static unsigned long system_milliseconds(void) { return 1000; }
static boolean network_game_should_accept_remote_connections(void) { return TRUE; }
static boolean network_game_is_splitscreen_local(void) { return FALSE; }
static boolean network_player_is_valid(struct network_player *player) { return player->valid; }
static boolean network_game_server_client_machine_is_joined_to_game(
struct network_game_server *s,
struct network_game_server_client_machine *machine)
{
return machine->joined;
}
static void network_game_server_countdown_started(struct network_game_server *s) { }

#include "under_test.inc"

static void join_machine(
short machine_index)
{
server.client_machines[machine_index].connection = &connection;
server.client_machines[machine_index].joined = TRUE;
}

/* machine 0 is the host's; no players yet */
static void lobby(
short machines)
{
short index;

memset(&server, 0, sizeof(server));
server.state = _network_game_server_state_pregame;
server.game.minimum_players = 2;
for (index = 0; index < machines; index++)
join_machine(index);
}

static void add_player(
short machine_index,
short team_index)
{
struct network_player *player = &server.game.players[server.game.player_count++];

player->valid = TRUE;
player->machine_index = machine_index;
player->team_index = team_index;
}

/* the lobby's START NOW */
static void start_now(void)
{
network_game_server_update_countdown(&server, _network_game_server_countdown_event_player_joined);
}

int main(int argc, char **argv)
{
char const *case_name = argc > 1 ? argv[1] : "";

CASE("split-screen-start-now")
{
lobby(1);
add_player(0, 0);
add_player(0, 1);
start_now();
CHECK(server.countdown_state.active, "one machine's two players never start the countdown");
CHECK(server.countdown_state.timer.time_remaining == NETWORK_GAME_COUNTDOWN_TIME,
"%ld ms to go, not a system link game's countdown", server.countdown_state.timer.time_remaining);
return 0;
}
CASE("alone-start-now")
{
lobby(1);
add_player(0, 0);
start_now();
CHECK(server.countdown_state.active, "the host alone no longer starts the countdown");
return 0;
}
CASE("empty-lobby-waits")
{
lobby(1);
CHECK(!server_ok_to_countdown(&server), "an empty lobby may count down");
start_now();
CHECK(!server.countdown_state.active, "an empty lobby started the countdown");
return 0;
}
CASE("two-machines-start-now")
{
lobby(2);
add_player(0, 0);
add_player(1, 1);
start_now();
CHECK(server.countdown_state.active, "two machines no longer start the countdown");
return 0;
}
CASE("two-machines-team-game-needs-both-teams")
{
lobby(2);
server.game.variant.universal_variant.teams = TRUE;
add_player(0, 0);
add_player(1, 0);
CHECK(!server_ok_to_countdown(&server), "two machines' players of one team may count down");
start_now();
CHECK(!server.countdown_state.active, "two machines' players of one team started the countdown");
return 0;
}
/* server_ok_to_countdown is checked each tick, and a failure stops the countdown */
CASE("playerless-machine-keeps-countdown")
{
lobby(1);
add_player(0, 0);
start_now();
CHECK(server.countdown_state.active, "the host alone no longer starts the countdown");
join_machine(1);
CHECK(server_ok_to_countdown(&server), "a machine joining without a player stops the countdown");
return 0;
}
/* intended: one machine skips the team rule, as a lone host does */
CASE("split-screen-team-game-one-team")
{
lobby(1);
server.game.variant.universal_variant.teams = TRUE;
add_player(0, 0);
add_player(0, 0);
start_now();
CHECK(server.countdown_state.active, "one machine's players of one team never start the countdown");
return 0;
}
fprintf(stderr, "unknown case: %s\n", case_name);
return 2;
}
84 changes: 84 additions & 0 deletions tools/harness/tests/test_lobby_countdown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""The real pregame countdown of network_server_manager.c: the lobby's START NOW starts it for a host alone, a host with
split screen players and machines together, and a machine joining without a player does not stop it."""

import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from harness import CHECK_FAILED, build, constant, enum_with, function, mutated, read, run, structure # noqa: E402

CASES = [
"split-screen-start-now", "alone-start-now", "empty-lobby-waits", "two-machines-start-now",
"two-machines-team-game-needs-both-teams", "playerless-machine-keeps-countdown",
"split-screen-team-game-one-team",
]
FUNCTIONS = (
"countdown_timer_update", "countdown_timer_get_time_remaining", "countdown_timer_increment",
"countdown_timer_decrement", "countdown_timer_set_time_remaining",
"network_game_server_get_client_machine_count", "server_needs_more_teams",
"server_has_enough_machines", "server_players_on_one_machine", "server_ok_to_countdown",
"network_game_server_update_countdown",
)
CONSTANTS = (
"MAXIMUM_NETWORK_MACHINE_COUNT", "MAXIMUM_NETWORK_PLAYER_COUNT", "NUMBER_OF_MULTIPLAYER_TEAMS",
"NETWORK_GAME_COUNTDOWN_TIME", "NETWORK_GAME_SPLITSCREEN_COUNTDOWN_TIME",
"NETWORK_GAME_COUNTDOWN_ADJUSTMENT", "NETWORK_GAME_MINIMUM_COUNTDOWN_TIME",
)

# a fault in network_server_manager.c, the function it is in, and the case that must catch it
NEGATIVE_CONTROLS = {
"one-player-only": (
("else if ((short)player->machine_index != first_machine_index)", "else"),
"server_players_on_one_machine", "split-screen-start-now",
),
"machines-ignored": (
("else if ((short)player->machine_index != first_machine_index)", "else if (0)"),
"server_players_on_one_machine", "two-machines-team-game-needs-both-teams",
),
"no-player-counts": (
("return first_machine_index != NONE;", "return TRUE;"),
"server_players_on_one_machine", "empty-lobby-waits",
),
"joined-machines-counted": (
(
"return first_machine_index != NONE;",
"return first_machine_index != NONE && network_game_server_get_client_machine_count(server) == 1;",
),
"server_players_on_one_machine", "playerless-machine-keeps-countdown",
),
}


def generated(fault=None, faulty_function=None):
source = read("source/networking/network_server_manager.c")
# (the machine and player counts are halo_port_limits.h's, which constant() does not follow into)
with_limits = source + read("port/linux/include/halo_port_limits.h")
config = ""
for name in CONSTANTS:
config += f"#define {name} {constant(with_limits, name)}\n"
for member in ("_network_game_server_state_pregame", "_network_game_server_countdown_event_player_left"):
config += enum_with(source, member) + "\n"
for name in ("countdown_timer", "network_game_server_countdown_state"):
config += structure(source, name) + "\n"
text = ""
for name in FUNCTIONS:
code = function(source, name)
if fault and name == faulty_function:
code = mutated(code, *fault)
text += code + "\n"
return (("config.inc", config), ("under_test.inc", text))


@pytest.mark.parametrize("case", CASES)
def test_case(case):
status, output = run(build("lobby_countdown", generated()), case)
assert status == 0, output


@pytest.mark.parametrize("control", NEGATIVE_CONTROLS)
def test_negative_control(control):
fault, faulty_function, case = NEGATIVE_CONTROLS[control]
status, output = run(build("lobby_countdown", generated(fault, faulty_function)), case)
assert status == CHECK_FAILED, f"network_server_manager.c with a fault ({control}) passed '{case}': the test cannot see it"