Compare commits

..
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 f6c2322df9 Host options in the lobby: public, bots and their skill
Y in a hosted game's lobby opens a panel to make the game public, choose
how many bots join (none by default, up to 15) and how well they play
(bots.skill: easy, normal, hard; hard is the first bots' play, normal a
little slower to notice, turn and aim). Integer settings can now be
written to config.toml as booleans could.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:05:34 +02:00
Stefan MaierandClaude Opus 5.5 39fbaefaac Keep the new settings out of the network table
The settings file is written in the table's order, so settings of another
section between network.* entries wrote [network] twice, and the whole
file was then refused (defaults used). They now follow the network
settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:52:14 +02:00
Stefan MaierandClaude Opus 5.5 c6397cb515 Bots find their way with the game's pathfinding (phase 1)
A bot walks to its goal along a path from ai/path.c (the maps' pathfinding
surfaces, as the campaign's actors use them), found again every half
second: in a fight towards its target, weaving; out of sight hunting the
nearest enemy, or roaming near someone; looking where it goes. With no
path it walks straight at the goal, and when stuck picks another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:05:12 +02:00
Stefan MaierandClaude Opus 5.5 5ba6c13097 Bots as players of the host (phase 0)
bots.count bots are added in the lobby as players of machines of the
host's own that nothing connects to (four a machine, from the top slot
down): new connections skip their slots, and they count as machines for
the countdown, so a game of the host and bots starts. Every machine sees
them as players; each tick the host thinks for them (bots.c) and takes
the result as their input through update_server_handle_distributed_input.
The first brain finds the nearest visible enemy, turns at a player's rate
with some aim error, walks at it weaving and fires on target, and else
wanders, jumping and turning when stuck. Every multiplayer map has
pathfinding surfaces for the next phase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:08:09 +02:00
Stefan MaierandClaude Opus 5.5 66c0d33e34 List public online games and join them from a panel
A host can make its game public (Y in the lobby, or network.public_game):
it then announces name, map, game type, players and join code every 10 s,
retained, on hcvr/1/g<network version>/<id> through the signalling
brokers, and clears it when it stops. On the system link list, X opens an
online games panel listing the public games heard lately (stale ones
dropped after 45 s) and a row to type a code; A joins by the game's code.
Announcements carry no addresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:00:57 +02:00
12 changed files with 1746 additions and 39 deletions

No files matched your search

+586
View File
@@ -0,0 +1,586 @@
/*
BOTS.C
Bots in multiplayer (the VR port's): players of the host that it drives
itself. They are the players of machines of the host's own that nothing
connects to (network_server_manager.c: bots.count of them, added in the
lobby), so every machine sees them as it sees any other machine's players,
and their input goes where a client's does: every tick, the host makes each
bot's player_action and takes it as a client's input
(update_server_handle_distributed_input).
A bot looks for the nearest enemy it can see, turns to it no faster than a
player would (and not quite right), closes on it weaving from side to side
and fires while it is on target. With none in sight it hunts the nearest
enemy, or else roams to a point picked at random near someone. It finds its
way with the game's own pathfinding (ai/path.c: the maps' pathfinding
surfaces), as the campaign's actors do: a path a few times a second, whose
next point it walks to, looking where it goes; where no path is found it
walks straight at the goal, jumping and turning off when it is stuck.
*/
#include "cseries.h"
#include "game/game.h"
#include "game/game_engine.h"
#include "game/players.h"
#include "game/player_queues_new.h"
#include "objects/objects.h"
#include "physics/collisions.h"
#include "ai/path.h"
#include "units/bipeds.h"
#include "scenario/scenario.h"
#include "structures/structure_bsp_definitions.h"
#include "units/units.h"
#include "network_distributed.h"
#include <math.h>
void platform_log(char const *format, ...);
long config_integer(char const *name);
boolean network_game_server_is_bot_machine(long machine_index);
/* ---------- constants */
enum
{
MAXIMUM_BOTS = 16,
/* how often a bot finds its path again (ticks) */
PATH_REFRESH_TICKS = 15,
};
/* how near a path's point counts as reached, and the goal (world units) */
#define BOT_STEP_RADIUS 0.4f
#define BOT_GOAL_RADIUS 1.0f
/* how far a bot sees and shoots (world units), and how near it closes */
#define BOT_FIRE_RANGE 30.0f
#define BOT_CLOSE_RANGE 3.0f
/* how far off target it still fires (radians) */
#define BOT_FIRE_CONE 0.08f
/* ---------- structures */
struct bot
{
long player_index;
long target_unit_index;
short target_seen_ticks;
real yaw;
real pitch;
real aim_error_yaw;
real aim_error_pitch;
short error_ticks;
real_point3d last_position;
short stuck_ticks;
real wander_yaw;
short weave_phase;
/* where it is going, and the way there */
boolean has_goal;
real_point3d goal;
long goal_time;
long path_time;
struct path_result path;
};
/* ---------- globals */
/* (one search at a time, shared: 80 kB) */
static struct path_state bot_path_state;
static struct
{
boolean map_logged;
struct bot bots[MAXIMUM_BOTS];
long seed;
long logged_time;
long paths_found;
long paths_failed;
/* how well they play (bots.skill, as the game began) */
short reaction_ticks;
real turn_rate;
real sight_range;
real aim_error;
} bots;
/* easy, normal, hard: the ticks a bot takes to notice an enemy, how fast it
turns (radians a tick), how far it sees and how far off its aim wanders */
static struct
{
short reaction_ticks;
real turn_rate;
real sight_range;
real aim_error;
} const bot_skills[] =
{
{ 20, 2.2f / 30.0f, 25.0f, 3.0f },
{ 13, 2.8f / 30.0f, 32.0f, 1.8f },
{ 9, 3.5f / 30.0f, 40.0f, 1.0f },
};
/* ---------- private code */
static real bot_random(
void)
{
bots.seed = bots.seed * 1103515245 + 12345;
return (real)((bots.seed >> 16) & 0x7FFF) / 32767.0f;
}
static real bot_angle_difference(
real to,
real from)
{
real difference = to - from;
while (difference > _pi)
difference -= 2.0f * _pi;
while (difference < -_pi)
difference += 2.0f * _pi;
return difference;
}
static boolean bot_can_see(
long unit_index,
real_point3d const *from,
real_point3d const *to)
{
struct collision_result collision;
real_vector3d vector;
vector.i = to->x - from->x;
vector.j = to->y - from->y;
vector.k = to->z - from->z;
return !collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
from, &vector, unit_index, &collision);
}
/* the nearest enemy unit the bot can see, or NONE */
static long bot_find_target(
struct player_datum const *player,
real_point3d const *eye)
{
boolean teams = game_engine_has_teams();
long best = NONE;
real best_distance = bots.sight_range;
short index;
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
{
struct player_datum *other = distributed_player(index);
long unit_index;
real_point3d head;
real distance;
if (!other || other == player || (teams && other->team_index == player->team_index))
continue;
unit_index = distributed_living_unit(other);
if (unit_index == NONE)
continue;
unit_get_camera_position(unit_index, &head);
distance = distance3d(eye, &head);
if (distance < best_distance && bot_can_see(player->unit_index, eye, &head))
{
best = unit_index;
best_distance = distance;
}
}
return best;
}
/* the way from where the bot's unit stands to the goal (FALSE: none) */
static boolean bot_find_path(
struct bot *bot,
long unit_index)
{
struct path_input input;
real_point3d start;
long start_surface = biped_find_pathfinding_surface_index(unit_index, &start);
real_point3d goal = bot->goal;
long goal_surface = NONE;
bot->path.valid = FALSE;
if (start_surface == NONE)
return FALSE;
/* (the goal's surface: under it, as a biped would stand there) */
{
struct collision_result collision;
real_vector3d down = { 0.0f, 0.0f, -3.0f };
real_point3d above = goal;
above.z += 0.5f;
if (collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
&above, &down, NONE, &collision))
{
goal = collision.point;
goal_surface = collision.surface_index;
}
}
if (goal_surface == NONE)
return FALSE;
path_input_new(&input, 0.25f, FALSE, unit_index);
path_input_set_start(&input, &start, start_surface);
path_state_new(&input, &bot_path_state, NULL);
path_state_destination(&bot_path_state, &goal, goal_surface, BOT_GOAL_RADIUS);
if (!path_state_find(&bot_path_state) || !path_state_build_path(&bot_path_state, &bot->path))
{
bot->path.valid = FALSE;
bots.paths_failed++;
return FALSE;
}
bots.paths_found += bot->path.valid != FALSE;
return bot->path.valid;
}
/* the point it walks to now: the path's next, or the goal itself */
static boolean bot_next_point(
struct bot *bot,
long unit_index,
real_point3d const *position,
real_point3d *point)
{
if (!bot->has_goal)
return FALSE;
if (game_time_get() - bot->path_time >= PATH_REFRESH_TICKS || game_time_get() < bot->path_time)
{
bot->path_time = game_time_get();
bot_find_path(bot, unit_index);
}
if (bot->path.valid)
{
while (bot->path.step_index < bot->path.step_count)
{
struct path_step const *step = &bot->path.steps[(short)bot->path.step_index];
real dx = step->point.x - position->x;
real dy = step->point.y - position->y;
if (dx * dx + dy * dy > BOT_STEP_RADIUS * BOT_STEP_RADIUS)
{
*point = step->point;
return TRUE;
}
bot->path.step_index++;
}
/* (the steps done: on to the end, or a path found anew soon) */
if (!bot->path.steps_finish_path && game_time_get() - bot->path_time > 5)
bot->path_time = game_time_get() - PATH_REFRESH_TICKS;
*point = bot->path.endpoint.point;
return TRUE;
}
*point = bot->goal;
return TRUE;
}
/* a goal when none is in sight: the nearest enemy, or somewhere near
someone at random */
static void bot_choose_goal(
struct bot *bot,
struct player_datum const *player,
real_point3d const *position)
{
boolean teams = game_engine_has_teams();
real best_distance = 1000000.0f;
short index;
boolean found = FALSE;
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
{
struct player_datum *other = distributed_player(index);
long unit_index;
real_point3d origin;
real distance;
if (!other || other == player || (teams && other->team_index == player->team_index))
continue;
unit_index = distributed_living_unit(other);
if (unit_index == NONE)
continue;
object_get_origin(unit_index, &origin);
distance = distance3d(position, &origin);
if (distance < best_distance)
{
best_distance = distance;
bot->goal = origin;
found = TRUE;
}
}
if (!found)
{
/* (no enemy alive: a few metres off, somewhere) */
real angle = bot_random() * 2.0f * _pi;
bot->goal = *position;
bot->goal.x += (real)cos(angle) * 5.0f;
bot->goal.y += (real)sin(angle) * 5.0f;
}
bot->has_goal = TRUE;
bot->goal_time = game_time_get();
bot->path_time = game_time_get() - PATH_REFRESH_TICKS;
}
/* the throttle that walks a unit facing yaw towards a point */
static void bot_walk_to(
struct player_action *action,
real yaw,
real_point3d const *position,
real_point3d const *point)
{
real dx = point->x - position->x;
real dy = point->y - position->y;
real length = (real)sqrt(dx * dx + dy * dy);
if (length < 0.001f)
return;
dx /= length;
dy /= length;
/* (forward along the facing; left, its strafe's positive) */
action->throttle.i = dx * (real)cos(yaw) + dy * (real)sin(yaw);
action->throttle.j = -dx * (real)sin(yaw) + dy * (real)cos(yaw);
}
static void bot_think(
struct bot *bot,
struct player_datum *player,
struct player_action *action)
{
struct unit_datum *unit = unit_try_and_get(player->unit_index);
real_point3d eye;
real_point3d position;
real wanted_yaw;
real wanted_pitch = 0.0f;
boolean firing = FALSE;
real moved;
csmemset(action, 0, sizeof(*action));
action->desired_weapon_index = NONE;
action->desired_grenade_index = NONE;
action->desired_zoom_level = NONE;
if (!unit || unit->object.parent_object_index != NONE)
{
bot->target_unit_index = NONE;
return;
}
action->desired_weapon_index = unit->unit.current_weapon_index;
action->desired_grenade_index = unit->unit.current_grenade_index;
unit_get_camera_position(player->unit_index, &eye);
object_get_origin(player->unit_index, &position);
/* (where it faces now, the first time) */
if (bot->error_ticks == 0 && bot->yaw == 0.0f && bot->pitch == 0.0f)
{
bot->yaw = (real)atan2(unit->unit.aiming_vector.j, unit->unit.aiming_vector.i);
bot->wander_yaw = bot->yaw;
}
/* the enemy: noticed after a moment, kept while in sight */
{
long target = bot_find_target(player, &eye);
if (target != bot->target_unit_index)
{
bot->target_unit_index = target;
bot->target_seen_ticks = 0;
}
else if (target != NONE && bot->target_seen_ticks < SHORT_MAX)
{
bot->target_seen_ticks++;
}
}
/* (its aim wanders a little, anew now and then) */
if (--bot->error_ticks <= 0)
{
bot->error_ticks = 10 + (short)(bot_random() * 20.0f);
bot->aim_error_yaw = (bot_random() - 0.5f) * 0.06f * bots.aim_error;
bot->aim_error_pitch = (bot_random() - 0.5f) * 0.04f * bots.aim_error;
}
if (bot->target_unit_index != NONE && bot->target_seen_ticks >= bots.reaction_ticks)
{
real_point3d head;
real dx, dy, dz, flat, distance;
unit_get_camera_position(bot->target_unit_index, &head);
/* (at the chest) */
head.z -= 0.15f;
dx = head.x - eye.x;
dy = head.y - eye.y;
dz = head.z - eye.z;
flat = (real)sqrt(dx * dx + dy * dy);
distance = (real)sqrt(dx * dx + dy * dy + dz * dz);
wanted_yaw = (real)atan2(dy, dx) + bot->aim_error_yaw;
wanted_pitch = (real)atan2(dz, flat) + bot->aim_error_pitch;
/* closes on it along its way, weaving, until near */
bot->weave_phase++;
if (distance > BOT_CLOSE_RANGE)
{
real_point3d point;
object_get_origin(bot->target_unit_index, &bot->goal);
bot->has_goal = TRUE;
if (bot_next_point(bot, player->unit_index, &position, &point))
bot_walk_to(action, bot->yaw, &position, &point);
}
action->throttle.j += (real)sin(bot->weave_phase * 0.08f) * 0.6f;
firing = distance < BOT_FIRE_RANGE &&
fabsf(bot_angle_difference(wanted_yaw, bot->yaw)) < BOT_FIRE_CONE &&
fabsf(wanted_pitch - bot->pitch) < BOT_FIRE_CONE;
/* (a melee blow when it is upon it) */
if (distance < 1.0f)
SET_FLAG(action->control_flags, _unit_control_use_equipment_bit, TRUE);
}
else
{
real_point3d point;
/* hunts, or roams: along its way, looking where it goes */
if (!bot->has_goal || game_time_get() - bot->goal_time > 90 ||
distance3d(&position, &bot->goal) < BOT_GOAL_RADIUS)
{
bot_choose_goal(bot, player, &position);
}
wanted_yaw = bot->wander_yaw;
if (bot_next_point(bot, player->unit_index, &position, &point))
{
real dx = point.x - position.x;
real dy = point.y - position.y;
if (dx * dx + dy * dy > 0.0001f)
bot->wander_yaw = wanted_yaw = (real)atan2(dy, dx);
bot_walk_to(action, bot->yaw, &position, &point);
}
if (bot->stuck_ticks > 20)
{
/* (stuck: somewhere else) */
bot_choose_goal(bot, player, &position);
bot->goal.x += (bot_random() - 0.5f) * 6.0f;
bot->goal.y += (bot_random() - 0.5f) * 6.0f;
bot->stuck_ticks = 0;
}
}
/* turned no faster than a player turns */
{
real turn = bot_angle_difference(wanted_yaw, bot->yaw);
bot->yaw += PIN(turn, -bots.turn_rate, bots.turn_rate);
bot->pitch += PIN(wanted_pitch - bot->pitch, -bots.turn_rate, bots.turn_rate);
bot->pitch = PIN(bot->pitch, -1.4f, 1.4f);
while (bot->yaw > _pi)
bot->yaw -= 2.0f * _pi;
while (bot->yaw < -_pi)
bot->yaw += 2.0f * _pi;
}
action->desired_facing.yaw = bot->yaw < 0.0f ? bot->yaw + 2.0f * _pi : bot->yaw;
action->desired_facing.pitch = bot->pitch;
if (firing)
{
action->primary_trigger = 1.0f;
SET_FLAG(action->control_flags, _unit_control_weapon_primary_trigger_bit, TRUE);
}
/* stuck: hardly moved while walking (it jumps, and then turns) */
moved = distance3d(&position, &bot->last_position);
bot->last_position = position;
action->throttle.i = PIN(action->throttle.i, -1.0f, 1.0f);
action->throttle.j = PIN(action->throttle.j, -1.0f, 1.0f);
if ((fabsf(action->throttle.i) > 0.5f || fabsf(action->throttle.j) > 0.5f) && moved < 0.02f)
{
bot->stuck_ticks++;
if (bot->stuck_ticks == 8)
SET_FLAG(action->control_flags, _unit_control_jump_bit, TRUE);
}
else
{
bot->stuck_ticks = 0;
}
}
/* the bot of a player, made when first needed */
static struct bot *bot_get(
long player_index)
{
short index;
struct bot *free_bot = NULL;
for (index = 0; index < MAXIMUM_BOTS; index++)
{
if (bots.bots[index].player_index == player_index)
return &bots.bots[index];
if (bots.bots[index].player_index == NONE && !free_bot)
free_bot = &bots.bots[index];
}
if (free_bot)
{
csmemset(free_bot, 0, sizeof(*free_bot));
free_bot->player_index = player_index;
free_bot->target_unit_index = NONE;
}
return free_bot;
}
/* ---------- public code */
void bots_new_game(
void)
{
short index;
bots.map_logged = FALSE;
for (index = 0; index < MAXIMUM_BOTS; index++)
bots.bots[index].player_index = NONE;
bots.seed = 12345;
bots.logged_time = 0;
{
long skill = PIN(config_integer("bots.skill"), 0, (long)NUMBEROF(bot_skills) - 1);
bots.reaction_ticks = bot_skills[skill].reaction_ticks;
bots.turn_rate = bot_skills[skill].turn_rate;
bots.sight_range = bot_skills[skill].sight_range;
bots.aim_error = bot_skills[skill].aim_error;
}
}
/* (the host) after each tick: each bot's input for the next */
void bots_tick(
void)
{
short index;
short count = 0;
if (!bots.map_logged)
{
struct structure_bsp *bsp = global_structure_bsp_get();
bots.map_logged = TRUE;
if (bsp)
{
platform_log("bots: the map's pathfinding: %ld surfaces, %ld edges",
bsp->pathfinding_surfaces.count, bsp->pathfinding_edges.count);
}
}
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
{
struct player_datum *player = distributed_player(index);
struct bot *bot;
struct player_action action;
unsigned short control_flags;
if (!player || player->local_player_index != NONE ||
!network_game_server_is_bot_machine(player->network_player_data.machine_index))
{
continue;
}
bot = bot_get(DATUM_INDEX_NEW(index, player->identifier));
if (!bot)
continue;
bot_think(bot, player, &action);
control_flags = (unsigned short)action.control_flags;
update_server_handle_distributed_input(DATUM_INDEX_NEW(index, player->identifier), game_time_get(), &action,
&control_flags, 1);
count++;
}
if (count && game_time_get() - bots.logged_time >= 300)
{
bots.logged_time = game_time_get();
platform_log("bots: driving %d; paths found %ld, not %ld", (int)count, bots.paths_found, bots.paths_failed);
bots.paths_found = bots.paths_failed = 0;
}
}
+2
View File
@@ -3051,6 +3051,7 @@ void network_distributed_new_game(
network_objects_new_game();
network_damage_new_game();
network_vr_new_game();
bots_new_game();
}
/* after each tick (game_time.c) */
@@ -3093,6 +3094,7 @@ void network_distributed_tick(
distributed_statistics_due = FALSE;
distributed_host_send_players();
network_vr_tick();
bots_tick();
distributed_send_pickups();
if (game_time_get() % GAME_STATE_INTERVAL_TICKS == 0)
distributed_send_game_state(NONE);
+4
View File
@@ -236,4 +236,8 @@ void network_vr_tick(void);
word network_vr_entry_size(void);
void network_vr_handle_poses(long machine_index, void const *entries, short count);
/* bots (bots.c) */
void bots_new_game(void);
void bots_tick(void);
#endif // __NETWORK_DISTRIBUTED_H
+35
View File
@@ -2355,6 +2355,41 @@ int p2p_join_code_status(void)
return status;
}
void p2p_set_public_game(int public_game)
{
pthread_mutex_lock(&p2p_lock);
p2p_list_set_public(public_game);
pthread_mutex_unlock(&p2p_lock);
}
int p2p_public_game(void)
{
int result;
pthread_mutex_lock(&p2p_lock);
result = p2p_list_public();
pthread_mutex_unlock(&p2p_lock);
return result;
}
void p2p_set_game_description(const char *name, const char *map, const char *mode, int players,
int maximum_players)
{
pthread_mutex_lock(&p2p_lock);
p2p_list_set_description(name, map, mode, players, maximum_players);
pthread_mutex_unlock(&p2p_lock);
}
int p2p_public_games(struct p2p_public_game *games, int maximum)
{
int count;
pthread_mutex_lock(&p2p_lock);
count = p2p.running ? p2p_list_games(games, maximum) : 0;
pthread_mutex_unlock(&p2p_lock);
return count;
}
int p2p_join_state(void)
{
int state;
+24
View File
@@ -57,6 +57,30 @@ enum
};
int p2p_join_state(void);
/* Public games (p2p_list.c): the hosted game listed for anyone or not, and
what it is (the game's server, as it changes); and the games others list,
joined by their codes (p2p_join_with_code) */
enum
{
P2P_PUBLIC_NAME_SIZE = 16,
P2P_PUBLIC_MAP_SIZE = 32,
P2P_PUBLIC_MODE_SIZE = 24,
};
struct p2p_public_game
{
char name[P2P_PUBLIC_NAME_SIZE];
char map[P2P_PUBLIC_MAP_SIZE];
char mode[P2P_PUBLIC_MODE_SIZE];
char code[12];
int players;
int maximum_players;
};
void p2p_set_public_game(int public_game);
int p2p_public_game(void);
void p2p_set_game_description(const char *name, const char *map, const char *mode, int players,
int maximum_players);
int p2p_public_games(struct p2p_public_game *games, int maximum);
/* this machine's identifier, which its XNADDR carries (6 bytes) */
const unsigned char *p2p_identifier(void);
/* the address the game reaches the machine with this identifier at, if it
+18
View File
@@ -111,6 +111,9 @@ int p2p_signal_connected(void);
a message to a topic through every broker */
void p2p_signal_sync_topics(void);
void p2p_signal_publish(const char *topic, const unsigned char *payload, int size);
/* the same, kept by the brokers for whoever listens later (empty: no
longer) */
void p2p_signal_publish_retained(const char *topic, const unsigned char *payload, int size);
/* ---------- p2p_code.c: join codes (under p2p_lock) */
@@ -136,6 +139,21 @@ void p2p_code_topics(const char **host_topic, const char **look_topic);
int p2p_code_received(const char *topic, const unsigned char *payload, int size);
void p2p_code_update(void);
/* ---------- p2p_list.c: public games (under p2p_lock) */
/* hosting: whether the game is listed, and what it is */
void p2p_list_set_public(int public_game);
int p2p_list_public(void);
void p2p_list_set_description(const char *name, const char *map, const char *mode, int players,
int maximum_players);
/* the games listed lately; asking keeps this machine listening a while */
int p2p_list_games(struct p2p_public_game *games, int maximum);
/* (p2p_signal.c) the topic to listen on (empty: none); a message on one
(nonzero if it was a listing's); and each pass, whether hosting */
const char *p2p_list_topic(void);
int p2p_list_received(const char *topic, const unsigned char *payload, int size);
void p2p_list_update(int hosting);
/* ---------- p2p_crypto.c */
enum
+335
View File
@@ -0,0 +1,335 @@
/*
P2P_LIST.C
Public games (the VR port's): a host may list its game for anyone, who
finds it in a list of online games and joins it by its join code
(p2p_code.c), without a server of this project's.
- A public game's host announces it through the signalling brokers
(p2p_signal.c) every ANNOUNCE_INTERVAL, retained, on a topic of its own
under hcvr/1/g<network version>/: its name, map, game type, players, its
join code, and the time. Retained, a machine that starts listening gets
every game's latest at once. A host that stops (it closes its game, or
makes it private) clears its topic; one that quits without that leaves
its last announcement, which listeners drop once it is older than
STALE_SECONDS (by the machines' clocks).
- A machine that shows the list listens on the topics of its network
version (a wildcard), and keeps the games heard of lately.
- An announcement carries no address: a machine reaches a public game's
host only by its code, as anyone given the code does. Anyone can announce
anything, so what is shown is checked to be printable and in bounds, and
a game that is not what it says is only a code that does not answer.
*/
#include "platform.h"
#include "posix.h"
#include "port_config.h"
#include "p2p_internal.h"
#include "halo_port_limits.h"
#include <stdio.h>
#include <string.h>
#include <time.h>
enum
{
ANNOUNCEMENT_VERSION = 1,
/* milliseconds */
ANNOUNCE_INTERVAL = 10000,
/* a listener's interest lapses this long after the menu last asked */
WATCH_TIME = 3000,
STALE_SECONDS = 45,
MAXIMUM_LISTED_GAMES = 32,
};
/* what goes to the brokers (byte for byte) */
struct announcement
{
char magic[4];
unsigned char version;
unsigned char players;
unsigned char maximum_players;
unsigned char flags;
unsigned char time[8];
char code[8];
char name[P2P_PUBLIC_NAME_SIZE];
char map[P2P_PUBLIC_MAP_SIZE];
char mode[P2P_PUBLIC_MODE_SIZE];
};
struct listed_game
{
int used;
char topic[P2P_CODE_TOPIC_SIZE];
unsigned long heard_time;
long long announced_time;
struct p2p_public_game game;
};
static struct
{
/* hosting (whether public: network.public_game, until Y changes it) */
int initialized;
int public_game;
char topic[P2P_CODE_TOPIC_SIZE];
int announced;
unsigned long announced_time;
char name[P2P_PUBLIC_NAME_SIZE];
char map[P2P_PUBLIC_MAP_SIZE];
char mode[P2P_PUBLIC_MODE_SIZE];
int players;
int maximum_players;
/* listening */
unsigned long watched_time;
int watching;
char watch_topic[P2P_CODE_TOPIC_SIZE];
struct listed_game games[MAXIMUM_LISTED_GAMES];
} list;
static int list_elapsed(unsigned long since, unsigned long time)
{
return (unsigned int)(p2p_now() - since) >= (unsigned int)time;
}
/* text as shown: printable ASCII only, ended */
static void list_text(char *destination, int size, const char *source, int source_size)
{
int index;
for (index = 0; index < size - 1 && index < source_size && source[index]; index++)
destination[index] = source[index] >= 32 && source[index] < 127 ? source[index] : '?';
destination[index] = 0;
}
static long long list_time_now(void)
{
return (long long)time(NULL);
}
static void list_topics_make(void)
{
if (!list.topic[0])
{
unsigned char random[8];
char hex[17];
posix_random_bytes(random, sizeof(random));
p2p_hex(random, sizeof(random), hex);
snprintf(list.topic, sizeof(list.topic), "hcvr/1/g%d/%s", HALO_PORT_NETWORK_VERSION, hex);
}
if (!list.watch_topic[0])
snprintf(list.watch_topic, sizeof(list.watch_topic), "hcvr/1/g%d/+", HALO_PORT_NETWORK_VERSION);
}
static void list_announce(void)
{
struct announcement announcement;
char code[16];
long long now = list_time_now();
int index;
if (!p2p_code_hosted(code, sizeof(code)))
return;
memset(&announcement, 0, sizeof(announcement));
memcpy(announcement.magic, "HCVR", 4);
announcement.version = ANNOUNCEMENT_VERSION;
announcement.players = (unsigned char)(list.players < 0 ? 0 : list.players > 255 ? 255 : list.players);
announcement.maximum_players =
(unsigned char)(list.maximum_players < 0 ? 0 : list.maximum_players > 255 ? 255 : list.maximum_players);
for (index = 0; index < 8; index++)
announcement.time[index] = (unsigned char)(now >> (8 * index));
/* (the code without its dash) */
memcpy(announcement.code, code, 4);
memcpy(announcement.code + 4, code + 5, 4);
memcpy(announcement.name, list.name, sizeof(announcement.name));
memcpy(announcement.map, list.map, sizeof(announcement.map));
memcpy(announcement.mode, list.mode, sizeof(announcement.mode));
p2p_signal_publish_retained(list.topic, (const unsigned char *)&announcement, sizeof(announcement));
if (!list.announced)
platform_log("Internet play: the game is listed as public (%s, %s, %s)", list.name, list.map, list.mode);
list.announced = 1;
list.announced_time = p2p_now();
}
static void list_withdraw(void)
{
if (!list.announced)
return;
p2p_signal_publish_retained(list.topic, NULL, 0);
platform_log("Internet play: the game is no longer listed as public");
list.announced = 0;
}
/* ---------- p2p.c's side (under p2p_lock) */
void p2p_list_set_public(int public_game)
{
if (public_game == list.public_game)
return;
list.public_game = public_game;
list_topics_make();
if (!public_game)
list_withdraw();
else
list.announced_time = p2p_now() - ANNOUNCE_INTERVAL;
}
int p2p_list_public(void)
{
return list.public_game;
}
void p2p_list_set_description(const char *name, const char *map, const char *mode, int players,
int maximum_players)
{
int changed = strncmp(list.name, name, sizeof(list.name) - 1) || strncmp(list.map, map, sizeof(list.map) - 1) ||
strncmp(list.mode, mode, sizeof(list.mode) - 1) || players != list.players ||
maximum_players != list.maximum_players;
list_text(list.name, sizeof(list.name), name, P2P_PUBLIC_NAME_SIZE);
list_text(list.map, sizeof(list.map), map, P2P_PUBLIC_MAP_SIZE);
list_text(list.mode, sizeof(list.mode), mode, P2P_PUBLIC_MODE_SIZE);
list.players = players;
list.maximum_players = maximum_players;
/* (a change goes out soon, not at the next interval) */
if (changed && list.announced && list_elapsed(list.announced_time, 1000))
list.announced_time = p2p_now() - ANNOUNCE_INTERVAL;
}
int p2p_list_games(struct p2p_public_game *games, int maximum)
{
int count = 0;
int index;
list.watched_time = p2p_now();
list.watching = 1;
for (index = 0; index < MAXIMUM_LISTED_GAMES && count < maximum; index++)
{
if (list.games[index].used)
games[count++] = list.games[index].game;
}
return count;
}
/* ---------- p2p_signal.c's side (under p2p_lock) */
const char *p2p_list_topic(void)
{
return list.watching ? list.watch_topic : "";
}
int p2p_list_received(const char *topic, const unsigned char *payload, int size)
{
struct announcement announcement;
struct listed_game *slot = NULL;
long long announced = 0;
char code[16];
int index;
if (!list.watch_topic[0] || strncmp(topic, list.watch_topic, strlen(list.watch_topic) - 1))
return 0;
/* (this machine's own, as the broker echoes it) */
if (!strcmp(topic, list.topic))
return 1;
for (index = 0; index < MAXIMUM_LISTED_GAMES; index++)
{
if (list.games[index].used && !strcmp(list.games[index].topic, topic))
slot = &list.games[index];
}
if (size != (int)sizeof(announcement))
{
/* (empty: withdrawn) */
if (slot && size == 0)
slot->used = 0;
return 1;
}
memcpy(&announcement, payload, sizeof(announcement));
if (memcmp(announcement.magic, "HCVR", 4) || announcement.version != ANNOUNCEMENT_VERSION ||
!announcement.maximum_players || announcement.players > announcement.maximum_players)
{
return 1;
}
for (index = 0; index < 8; index++)
announced |= (long long)announcement.time[index] << (8 * index);
if (announced < list_time_now() - STALE_SECONDS || announced > list_time_now() + STALE_SECONDS)
{
if (slot)
slot->used = 0;
return 1;
}
snprintf(code, sizeof(code), "%.4s-%.4s", announcement.code, announcement.code + 4);
for (index = 0; code[index]; index++)
{
if (index != 4 && !((code[index] >= '0' && code[index] <= '9') || (code[index] >= 'A' && code[index] <= 'Z')))
return 1;
}
if (!slot)
{
int oldest = -1;
for (index = 0; index < MAXIMUM_LISTED_GAMES && !slot; index++)
{
if (!list.games[index].used)
slot = &list.games[index];
else if (oldest < 0 || list.games[index].announced_time < list.games[oldest].announced_time)
oldest = index;
}
if (!slot)
slot = &list.games[oldest];
memset(slot, 0, sizeof(*slot));
snprintf(slot->topic, sizeof(slot->topic), "%s", topic);
}
slot->used = 1;
slot->heard_time = p2p_now();
slot->announced_time = announced;
list_text(slot->game.name, sizeof(slot->game.name), announcement.name, sizeof(announcement.name));
list_text(slot->game.map, sizeof(slot->game.map), announcement.map, sizeof(announcement.map));
list_text(slot->game.mode, sizeof(slot->game.mode), announcement.mode, sizeof(announcement.mode));
snprintf(slot->game.code, sizeof(slot->game.code), "%s", code);
slot->game.players = announcement.players;
slot->game.maximum_players = announcement.maximum_players;
return 1;
}
void p2p_list_update(int hosting)
{
int index;
if (!list.initialized)
{
list.initialized = 1;
if (!list.public_game)
list.public_game = config_boolean("network.public_game");
}
list_topics_make();
/* hosting a public game: announced every so often; withdrawn when it
is not */
if (hosting && list.public_game)
{
p2p_signal_start();
if (p2p_signal_connected() && list_elapsed(list.announced_time, ANNOUNCE_INTERVAL))
list_announce();
}
else if (list.announced)
{
list_withdraw();
}
/* listening while the menu asks */
if (list.watching && list_elapsed(list.watched_time, WATCH_TIME))
{
list.watching = 0;
memset(list.games, 0, sizeof(list.games));
p2p_signal_sync_topics();
}
else if (list.watching)
{
p2p_signal_start();
p2p_signal_sync_topics();
}
for (index = 0; index < MAXIMUM_LISTED_GAMES; index++)
{
if (list.games[index].used && list.games[index].announced_time < list_time_now() - STALE_SECONDS)
list.games[index].used = 0;
}
}
+32 -4
View File
@@ -157,6 +157,8 @@ struct broker
/* (join codes' topics: p2p_code.c) */
char code_host_topic[TOPIC_SIZE];
char code_look_topic[TOPIC_SIZE];
/* (public games': p2p_list.c) */
char list_topic[TOPIC_SIZE];
unsigned char input[BUFFER_SIZE];
int input_size;
unsigned char output[BUFFER_SIZE];
@@ -328,6 +330,7 @@ static void broker_close(struct broker *broker, int failed)
broker->join_topic[0] = 0;
broker->code_host_topic[0] = 0;
broker->code_look_topic[0] = 0;
broker->list_topic[0] = 0;
if (failed)
broker->failures++;
}
@@ -417,8 +420,8 @@ static void broker_publish(struct broker *broker, const char *topic, const unsig
/* the topics a ready broker should be subscribed to */
static void broker_sync_topics(struct broker *broker)
{
const char *wanted[4];
char *had[4];
const char *wanted[5];
char *had[5];
int index;
if (broker->state != _broker_ready)
@@ -426,11 +429,13 @@ static void broker_sync_topics(struct broker *broker)
wanted[0] = signalling.hosting ? signalling.host_topic : "";
wanted[1] = signalling.joining ? signalling.join_topic : "";
p2p_code_topics(&wanted[2], &wanted[3]);
wanted[4] = p2p_list_topic();
had[0] = broker->host_topic;
had[1] = broker->join_topic;
had[2] = broker->code_host_topic;
had[3] = broker->code_look_topic;
for (index = 0; index < 4; index++)
had[4] = broker->list_topic;
for (index = 0; index < 5; index++)
{
if (!strcmp(wanted[index], had[index]))
continue;
@@ -949,7 +954,7 @@ static void publish_received(struct broker *broker, const char *topic, const uns
if (size > MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD)
return;
if (p2p_code_received(topic, payload, size))
if (p2p_code_received(topic, payload, size) || p2p_list_received(topic, payload, size))
return;
if (signalling.hosting && !strcmp(topic, signalling.host_topic))
{
@@ -1203,6 +1208,7 @@ void p2p_signal_update(const int *read, int read_count, const int *write, int wr
if (signalling.joining && elapsed(signalling.join_sent_time, JOIN_INTERVAL))
send_join();
p2p_code_update();
p2p_list_update(signalling.hosting);
}
int p2p_signal_connected(void)
@@ -1236,6 +1242,28 @@ void p2p_signal_publish(const char *topic, const unsigned char *payload, int siz
publish_everywhere(topic, payload, size);
}
void p2p_signal_publish_retained(const char *topic, const unsigned char *payload, int size)
{
unsigned char body[2 + TOPIC_SIZE + MAXIMUM_MESSAGE_SIZE];
int index;
if (size > MAXIMUM_MESSAGE_SIZE)
return;
for (index = 0; index < signalling.broker_count; index++)
{
struct broker *broker = &signalling.brokers[index];
int body_size;
if (broker->state != _broker_ready)
continue;
body_size = put_string(body, topic);
if (size)
memcpy(body + body_size, payload, (size_t)size);
/* (PUBLISH, at most once, retained: an empty one clears it) */
broker_send(broker, 0x31, body, body_size + size);
}
}
void p2p_signal_host(const unsigned char *token)
{
if (!signalling.has_nonce_key)
+35 -5
View File
@@ -275,6 +275,9 @@ static const struct config_setting config_settings[] =
"clipboard) that lets whoever has it join over the internet; opening a\n"
"link (or copying one before switching to the game) joins. Only people\n"
"with the invite can join. Off keeps system link to the local network." },
{ "network.public_game", _config_boolean, "false", "HALO_NET_PUBLIC_GAME", _environment_value, _platform_all,
"Host games as public: listed for anyone among the online games (the\n"
"system link screen's X), as Y in a hosted game's lobby makes them." },
{ "network.join_from_clipboard", _config_boolean, "true", "HALO_NET_JOIN_FROM_CLIPBOARD", _environment_value,
_platform_all,
"Join the game of an invite link found on the clipboard when the game\n"
@@ -301,6 +304,12 @@ static const struct config_setting config_settings[] =
"The Discord application internet play invites go through while the\n"
"Discord desktop client runs; empty for none." },
{ "bots.count", _config_integer, "0", "HALO_BOTS_COUNT", _environment_value, _platform_android,
"Bots in the multiplayer games this machine hosts (0 to 15; 0: none):\n"
"players it drives itself, which every machine sees as players. Set in\n"
"the lobby's host options (Y)." },
{ "bots.skill", _config_integer, "1", "HALO_BOTS_SKILL", _environment_value, _platform_android,
"How well the bots play: 0 easy, 1 normal, 2 hard." },
{ "update.auto", _config_boolean, "true", "HALO_UPDATE_AUTO", _environment_value, _platform_all,
"Look for a new version when the game starts, and offer to update to it;\n"
"false never looks (the game's \"Do not ask again\" writes false here)." },
@@ -950,7 +959,9 @@ static int config_line_section(const char *line, const char *end, char *section,
/* sets a boolean setting, for now and in config.toml: its line there is
changed (or added), the rest of the file kept as it is */
int config_write_boolean(const char *name, int value)
/* a boolean's or an integer's new value, kept and written into the file
(the line of its key replaced, else added to its section) */
static int config_write_value(const char *name, int type, long value)
{
const char *dot = strchr(name, '.');
long index = config_setting_index(name);
@@ -961,15 +972,24 @@ int config_write_boolean(const char *name, int value)
const char *line;
int written = 0, in_section = 0, succeeded;
if (index < 0 || config_settings[index].type != _config_boolean || !dot || (size_t)(dot - name) >= sizeof(section))
if (index < 0 || config_settings[index].type != type || !dot || (size_t)(dot - name) >= sizeof(section))
return 0;
/* (the file read first, as the other settings are) */
config_boolean(name);
if (type == _config_boolean)
config_boolean(name);
else
config_integer(name);
pthread_mutex_lock(&config_lock);
config_values[index].boolean = value != 0;
if (type == _config_boolean)
config_values[index].boolean = value != 0;
else
config_values[index].integer = value;
snprintf(section, sizeof(section), "%.*s", (int)(dot - name), name);
snprintf(key, sizeof(key), "%s", dot + 1);
snprintf(line_text, sizeof(line_text), "%s = %s\n", key, value ? "true" : "false");
if (type == _config_boolean)
snprintf(line_text, sizeof(line_text), "%s = %s\n", key, value ? "true" : "false");
else
snprintf(line_text, sizeof(line_text), "%s = %ld\n", key, value);
snprintf(wanted, sizeof(wanted), "%s", section);
config_path(path, sizeof(path));
text = config_read_file(path, &size);
@@ -1026,6 +1046,16 @@ int config_write_boolean(const char *name, int value)
return succeeded;
}
int config_write_boolean(const char *name, int value)
{
return config_write_value(name, _config_boolean, value != 0);
}
int config_write_integer(const char *name, long value)
{
return config_write_value(name, _config_integer, value);
}
/* ---------- public code */
int config_boolean(const char *name)
+1
View File
@@ -21,5 +21,6 @@ const char *config_string(const char *name);
/* sets a boolean setting, and writes it into config.toml (only its line
changes); 1 on success */
int config_write_boolean(const char *name, int value);
int config_write_integer(const char *name, long value);
#endif
+396 -29
View File
@@ -5593,10 +5593,12 @@ static boolean ui_mouse_menus_active(
#ifdef HALO_ANDROID
/* ---------- join codes (the VR port's)
Internet play's join codes (port/linux/src/p2p_code.c): the host's lobby
shows its game's code, and on the screen of games to join, X opens the
keyboard to type one in, which then joins as an invite link does (the game
shows up in the list once its host is reached). */
Internet play's join codes (port/linux/src/p2p_code.c) and public games
(p2p_list.c): the host's lobby shows its game's code, and Y there lists the
game for anyone or not; on the screen of games to join, X opens a panel of
the public games and a way to type a code in, either of which then joins as
an invite link does (the game shows up in the list once its host is
reached). */
int p2p_join_code(char *text, int size);
int p2p_join_with_code(char const *typed);
@@ -5627,12 +5629,75 @@ enum
_join_state_lost,
};
struct ui_public_game
{
/* p2p.h's struct p2p_public_game */
char name[16];
char map[32];
char mode[24];
char code[12];
int players;
int maximum_players;
};
void p2p_set_public_game(int public_game);
int p2p_public_game(void);
int p2p_public_games(struct ui_public_game *games, int maximum);
long config_integer(char const *name);
int config_write_integer(char const *name, long value);
enum
{
MAXIMUM_SHOWN_PUBLIC_GAMES = 32,
/* the online games panel's rows: "enter a code" and the games */
ONLINE_PANEL_ROWS = 11,
};
static struct
{
boolean keyboard_launched;
wchar_t typed[12];
/* the panel of online games, over the screen of games to join */
boolean panel_open;
short selection;
short scroll;
short game_count;
struct ui_public_game games[MAXIMUM_SHOWN_PUBLIC_GAMES];
/* the host options panel, over a hosted game's lobby */
boolean host_panel_open;
short host_selection;
} ui_join_code_globals;
enum
{
/* the host options: public, bots, their skill */
HOST_PANEL_ROWS = 3,
MAXIMUM_BOTS_SHOWN = 15,
};
/* the multiplayer maps' names, as the menus show them */
static char const *ui_join_code_map_name(
char const *map)
{
static char const *const names[][2] = {
{ "beavercreek", "Battle Creek" }, { "bloodgulch", "Blood Gulch" }, { "boardingaction", "Boarding Action" },
{ "carousel", "Derelict" }, { "chillout", "Chill Out" }, { "damnation", "Damnation" },
{ "dangercanyon", "Danger Canyon" }, { "deathisland", "Death Island" }, { "gephyrophobia", "Gephyrophobia" },
{ "hangemhigh", "Hang 'Em High" }, { "icefields", "Ice Fields" }, { "infinity", "Infinity" },
{ "longest", "Longest" }, { "prisoner", "Prisoner" }, { "putput", "Chiron TL-34" },
{ "ratrace", "Rat Race" }, { "sidewinder", "Sidewinder" }, { "timberland", "Timberland" },
{ "wizard", "Wizard" },
};
short index;
for (index = 0; index < (short)NUMBEROF(names); index++)
{
if (!strcmp(map, names[index][0]))
return names[index][1];
}
return map;
}
static boolean ui_join_code_screen_active(
char const *name)
{
@@ -5640,28 +5705,190 @@ static boolean ui_join_code_screen_active(
strcmp(widget_globals.active_widgets[0]->name, name) == 0;
}
/* X on the screen of games to join: the keyboard, for a code (TRUE: the
event is taken) */
static boolean ui_join_code_take_event(
struct widget_instance *widget,
struct event_record const *event)
static void ui_join_code_launch_keyboard(
void)
{
if (event->type != _event_type_button || event->data.button.value != 1 ||
event->data.button.index != _gamepad_analog_button_x || widget != widget_globals.active_widgets[0] ||
!ui_join_code_screen_active("server_list_screen"))
{
return FALSE;
}
ui_join_code_globals.typed[0] = 0;
virtual_keyboard_set_caption(L"Enter the join code of an online game");
if (!virtual_keyboard_launch(ui_join_code_globals.typed, sizeof(ui_join_code_globals.typed), 8))
{
virtual_keyboard_set_caption(NULL);
return FALSE;
return;
}
p2p_join_code_clear_status();
ui_join_code_globals.keyboard_launched = TRUE;
return TRUE;
}
/* the panel's input: the stick or the pad's arrows choose a row, A takes
it, B closes it */
static void ui_join_code_panel_event(
struct event_record const *event)
{
short rows = (short)(ui_join_code_globals.game_count + 1);
short move = 0;
if (event->type == _event_type_left_stick)
{
if (event->data.stick.y == SHORT_MAX)
move = -1;
else if (event->data.stick.y == SHORT_MIN)
move = 1;
}
else if (event->type == _event_type_button && event->data.button.value == 1)
{
switch (event->data.button.index)
{
case _gamepad_binary_button_dpad_up: move = -1; break;
case _gamepad_binary_button_dpad_down: move = 1; break;
case _gamepad_analog_button_b:
case _gamepad_binary_button_back:
ui_join_code_globals.panel_open = FALSE;
break;
case _gamepad_analog_button_x:
ui_join_code_globals.panel_open = FALSE;
ui_join_code_launch_keyboard();
break;
case _gamepad_analog_button_a:
case _gamepad_binary_button_start:
ui_join_code_globals.panel_open = FALSE;
if (ui_join_code_globals.selection == 0 ||
ui_join_code_globals.selection > ui_join_code_globals.game_count)
{
ui_join_code_launch_keyboard();
}
else
{
p2p_join_code_clear_status();
p2p_join_with_code(ui_join_code_globals.games[ui_join_code_globals.selection - 1].code);
}
break;
}
}
if (move)
{
ui_join_code_globals.selection = (short)PIN(ui_join_code_globals.selection + move, 0, rows - 1);
if (ui_join_code_globals.selection < ui_join_code_globals.scroll)
ui_join_code_globals.scroll = ui_join_code_globals.selection;
else if (ui_join_code_globals.selection >= ui_join_code_globals.scroll + ONLINE_PANEL_ROWS)
ui_join_code_globals.scroll = (short)(ui_join_code_globals.selection - ONLINE_PANEL_ROWS + 1);
}
}
/* the host options panel's input: the stick or the pad's arrows up and
down choose a row, left and right (or A) change it, B or Y closes it */
static void ui_join_code_host_panel_event(
struct event_record const *event)
{
short move = 0;
short change = 0;
if (event->type == _event_type_left_stick)
{
if (event->data.stick.y == SHORT_MAX)
move = -1;
else if (event->data.stick.y == SHORT_MIN)
move = 1;
else if (event->data.stick.x == SHORT_MAX)
change = 1;
else if (event->data.stick.x == SHORT_MIN)
change = -1;
}
else if (event->type == _event_type_button && event->data.button.value == 1)
{
switch (event->data.button.index)
{
case _gamepad_binary_button_dpad_up: move = -1; break;
case _gamepad_binary_button_dpad_down: move = 1; break;
case _gamepad_binary_button_dpad_right:
case _gamepad_analog_button_a:
change = 1;
break;
case _gamepad_binary_button_dpad_left: change = -1; break;
case _gamepad_analog_button_b:
case _gamepad_analog_button_y:
case _gamepad_binary_button_back:
ui_join_code_globals.host_panel_open = FALSE;
break;
}
}
if (move)
ui_join_code_globals.host_selection = (short)PIN(ui_join_code_globals.host_selection + move, 0, HOST_PANEL_ROWS - 1);
if (!change)
return;
switch (ui_join_code_globals.host_selection)
{
case 0:
p2p_set_public_game(!p2p_public_game());
break;
case 1:
{
/* (A goes round: past the most, none) */
long count = config_integer("bots.count") + change;
if (count > MAXIMUM_BOTS_SHOWN)
count = event->type == _event_type_button && event->data.button.index == _gamepad_analog_button_a ? 0 :
MAXIMUM_BOTS_SHOWN;
config_write_integer("bots.count", PIN(count, 0, MAXIMUM_BOTS_SHOWN));
break;
}
case 2:
{
long skill = config_integer("bots.skill") + change;
if (skill > 2)
skill = event->type == _event_type_button && event->data.button.index == _gamepad_analog_button_a ? 0 : 2;
config_write_integer("bots.skill", PIN(skill, 0, 2));
break;
}
}
}
/* X on the screen of games to join: the panel of online games (and in it,
everything); Y in a hosted game's lobby: the host options (public, bots).
TRUE: the event is taken. */
static boolean ui_join_code_take_event(
struct widget_instance *widget,
struct event_record const *event)
{
if (widget != widget_globals.active_widgets[0])
return FALSE;
if (ui_join_code_globals.host_panel_open)
{
if (!ui_join_code_screen_active("connected_pregame_screen") || !global_network_game_server_get())
{
ui_join_code_globals.host_panel_open = FALSE;
return FALSE;
}
ui_join_code_host_panel_event(event);
return TRUE;
}
if (ui_join_code_globals.panel_open)
{
if (!ui_join_code_screen_active("server_list_screen"))
{
ui_join_code_globals.panel_open = FALSE;
return FALSE;
}
ui_join_code_panel_event(event);
return TRUE;
}
if (event->type != _event_type_button || event->data.button.value != 1)
return FALSE;
if (event->data.button.index == _gamepad_analog_button_x && ui_join_code_screen_active("server_list_screen"))
{
ui_join_code_globals.panel_open = TRUE;
ui_join_code_globals.selection = 0;
ui_join_code_globals.scroll = 0;
return TRUE;
}
if (event->data.button.index == _gamepad_analog_button_y &&
ui_join_code_screen_active("connected_pregame_screen") && global_network_game_server_get())
{
ui_join_code_globals.host_panel_open = TRUE;
ui_join_code_globals.host_selection = 0;
return TRUE;
}
return FALSE;
}
/* the keyboard closed: the code typed, looked up */
@@ -5687,17 +5914,20 @@ static void ui_join_code_update(
p2p_join_with_code(text);
}
static void ui_join_code_draw_line(
/* text in bounds, left, right or centred (0, 1, 2) */
static void ui_join_code_draw_text(
short x0,
short x1,
short y0,
char const *text,
boolean icons,
boolean centered)
short justification,
real_argb_color const *color)
{
rectangle2d bounds;
wchar_t wide[96];
short index;
long font_index = tag_loaded(FONT_GROUP_TAG, "ui\\small_ui");
real_argb_color color = { 1.0f, 0.85f, 0.85f, 0.85f };
if (font_index == NONE)
return;
@@ -5706,15 +5936,97 @@ static void ui_join_code_draw_line(
wide[index] = 0;
bounds.y0 = y0;
bounds.y1 = (short)(y0 + 20);
bounds.x0 = centered ? 0 : 48;
bounds.x1 = centered ? 640 : 600;
draw_string_set_draw_mode(font_index, NONE, centered ? 2 : 0, 0, &color);
bounds.x0 = x0;
bounds.x1 = x1;
draw_string_set_draw_mode(font_index, NONE, justification, 0, color);
if (icons)
draw_string_and_hack_in_icons(&bounds, &bounds, NULL, 0, wide, FALSE);
else
rasterizer_draw_unicode_string(&bounds, &bounds, NULL, 0, wide);
}
static void ui_join_code_draw_line(
short y0,
char const *text,
boolean icons,
boolean centered)
{
real_argb_color color = { 1.0f, 0.85f, 0.85f, 0.85f };
ui_join_code_draw_text((short)(centered ? 0 : 48), (short)(centered ? 640 : 600), y0, text, icons,
(short)(centered ? 2 : 0), &color);
}
static void ui_join_code_draw_quad(
short x0,
short y0,
short x1,
short y1,
unsigned long color)
{
rectangle2d bounds;
bounds.x0 = x0;
bounds.y0 = y0;
bounds.x1 = x1;
bounds.y1 = y1;
draw_quad(&bounds, color);
}
/* the panel of online games: "enter a code", then the public games */
static void ui_join_code_render_panel(
void)
{
real_argb_color title_color = { 1.0f, 0.55f, 0.75f, 1.0f };
real_argb_color text_color = { 1.0f, 0.85f, 0.85f, 0.85f };
real_argb_color dim_color = { 1.0f, 0.6f, 0.6f, 0.6f };
char line[64];
short row;
ui_join_code_globals.game_count =
(short)p2p_public_games(ui_join_code_globals.games, MAXIMUM_SHOWN_PUBLIC_GAMES);
ui_join_code_globals.selection =
(short)PIN(ui_join_code_globals.selection, 0, ui_join_code_globals.game_count);
ui_join_code_draw_quad(40, 72, 600, 410, 0xF0050A18);
ui_join_code_draw_quad(40, 72, 600, 74, 0xFF3A6EA5);
ui_join_code_draw_quad(40, 408, 600, 410, 0xFF3A6EA5);
snprintf(line, sizeof(line), "ONLINE GAMES (%d public)", (int)ui_join_code_globals.game_count);
ui_join_code_draw_text(56, 584, 80, line, FALSE, 0, &title_color);
ui_join_code_draw_text(56, 216, 104, "Name", FALSE, 0, &dim_color);
ui_join_code_draw_text(216, 376, 104, "Map", FALSE, 0, &dim_color);
ui_join_code_draw_text(376, 516, 104, "Game", FALSE, 0, &dim_color);
ui_join_code_draw_text(516, 584, 104, "Players", FALSE, 1, &dim_color);
for (row = 0; row < ONLINE_PANEL_ROWS; row++)
{
short index = (short)(ui_join_code_globals.scroll + row);
short y = (short)(128 + row * 22);
if (index > ui_join_code_globals.game_count)
break;
if (index == ui_join_code_globals.selection)
ui_join_code_draw_quad(48, y, 592, (short)(y + 21), 0x803A6EA5);
if (index == 0)
{
ui_join_code_draw_text(56, 584, (short)(y + 1), "Enter a join code...", FALSE, 0, &text_color);
}
else
{
struct ui_public_game const *game = &ui_join_code_globals.games[index - 1];
ui_join_code_draw_text(56, 212, (short)(y + 1), game->name, FALSE, 0, &text_color);
ui_join_code_draw_text(216, 372, (short)(y + 1), ui_join_code_map_name(game->map), FALSE, 0,
&text_color);
ui_join_code_draw_text(376, 512, (short)(y + 1), game->mode, FALSE, 0, &text_color);
snprintf(line, sizeof(line), "%d/%d", game->players, game->maximum_players);
ui_join_code_draw_text(516, 584, (short)(y + 1), line, FALSE, 1, &text_color);
}
}
if (!ui_join_code_globals.game_count)
ui_join_code_draw_text(56, 584, 152, "No public games right now", FALSE, 0, &dim_color);
ui_join_code_draw_text(56, 584, 384, "%a-button Join %x-button Enter a code %b-button Back", TRUE, 0,
&text_color);
}
/* over the screens, as they are drawn */
static void ui_join_code_render(
void)
@@ -5725,7 +6037,12 @@ static void ui_join_code_render(
{
char const *status = NULL;
ui_join_code_draw_line(372, "%x-button Join an online game with its code", TRUE, FALSE);
if (ui_join_code_globals.panel_open)
{
ui_join_code_render_panel();
return;
}
ui_join_code_draw_line(372, "%x-button Online games: public ones, or join by a code", TRUE, FALSE);
switch (p2p_join_code_status())
{
case _join_code_looking: status = "Looking for the game of that code..."; break;
@@ -5750,14 +6067,64 @@ static void ui_join_code_render(
if (status)
ui_join_code_draw_line(392, status, FALSE, FALSE);
}
else if (ui_join_code_screen_active("connected_pregame_screen") && global_network_game_server_get() &&
p2p_join_code(code, sizeof(code)))
else if (ui_join_code_screen_active("connected_pregame_screen") && global_network_game_server_get())
{
char line[48];
char line[128];
long bots = config_integer("bots.count");
char bots_text[24];
snprintf(line, sizeof(line), "Online join code: %s", code);
if (bots > 0)
snprintf(bots_text, sizeof(bots_text), "%ld bot%s", bots, bots == 1 ? "" : "s");
else
snprintf(bots_text, sizeof(bots_text), "no bots");
if (p2p_join_code(code, sizeof(code)))
{
snprintf(line, sizeof(line), "Online join code: %s %%y-button Host options (%s, %s)", code,
p2p_public_game() ? "public" : "private", bots_text);
}
else
{
snprintf(line, sizeof(line), "%%y-button Host options (%s)", bots_text);
}
/* (under the buttons' key) */
ui_join_code_draw_line(448, line, FALSE, TRUE);
ui_join_code_draw_line(448, line, TRUE, TRUE);
if (ui_join_code_globals.host_panel_open)
{
static char const *const skills[] = { "Easy", "Normal", "Hard" };
real_argb_color title_color = { 1.0f, 0.55f, 0.75f, 1.0f };
real_argb_color text_color = { 1.0f, 0.85f, 0.85f, 0.85f };
short row;
ui_join_code_draw_quad(140, 150, 500, 330, 0xF0050A18);
ui_join_code_draw_quad(140, 150, 500, 152, 0xFF3A6EA5);
ui_join_code_draw_quad(140, 328, 500, 330, 0xFF3A6EA5);
ui_join_code_draw_text(156, 484, 158, "HOST OPTIONS", FALSE, 0, &title_color);
for (row = 0; row < HOST_PANEL_ROWS; row++)
{
short y = (short)(190 + row * 30);
if (row == ui_join_code_globals.host_selection)
ui_join_code_draw_quad(148, (short)(y - 3), 492, (short)(y + 21), 0x803A6EA5);
switch (row)
{
case 0:
snprintf(line, sizeof(line), "Public game: %s", p2p_public_game() ? "Yes (listed online)" : "No");
break;
case 1:
snprintf(line, sizeof(line), "Bots: %s", bots > 0 ? bots_text : "None");
break;
default:
{
long skill = PIN(config_integer("bots.skill"), 0, 2);
snprintf(line, sizeof(line), "Bot skill: %s", skills[skill]);
break;
}
}
ui_join_code_draw_text(160, 484, y, line, FALSE, 0, &text_color);
}
ui_join_code_draw_text(156, 484, 300, "%a-button Change %b-button Done", TRUE, 0, &text_color);
}
}
}
+278 -1
View File
@@ -475,6 +475,10 @@ symbols in this file:
/* port: internet play's Discord presence (port/linux/src/p2p.c) */
void p2p_set_game_player_counts(int count, int maximum);
#ifdef HALO_ANDROID
void p2p_set_game_description(char const *name, char const *map, char const *mode, int players,
int maximum_players);
#endif
/* ---------- constants */
@@ -692,6 +696,12 @@ typedef char network_game_server_countdown_state_offset_assert[
offsetof(struct network_game_server, countdown_state) ==
8 + HALO_PORT_NETWORK_GAME_SIZE + MAXIMUM_NETWORK_MACHINE_COUNT * 0x10 + 0xC ? 1 : -1];
#ifdef HALO_ANDROID
/* bots' machines (below: the VR port's bots) */
static boolean network_game_server_bot_machines[MAXIMUM_NETWORK_MACHINE_COUNT];
static short network_game_server_bot_machine_count(struct network_game_server *server);
#endif
/* ---------- prototypes */
void countdown_timer_increment(
@@ -1308,6 +1318,42 @@ boolean network_game_server_idle(
/* (what Discord shows of a game hosted for internet play) */
p2p_set_game_player_counts(server->game.player_count, server->game.maximum_players);
#ifdef HALO_ANDROID
/* (and what a public game's listing says: port/linux/src/p2p_list.c) */
{
char name[16], map[32], mode[24];
char const *map_name = server->game.map.name;
char const *separator;
short index;
for (index = 0; index < (short)(NUMBEROF(name) - 1) && server->game.name[index]; index++)
name[index] = server->game.name[index] < 128 ? (char)server->game.name[index] : '?';
name[index] = 0;
/* (the scenario's name, the last of its path) */
while ((separator = strpbrk(map_name, "\\/")) != NULL)
map_name = separator + 1;
csstrncpy(map, map_name, sizeof(map) - 1);
map[sizeof(map) - 1] = 0;
for (index = 0; index < (short)(NUMBEROF(mode) - 1) && index < 12 &&
server->game.variant.human_readable_game_description[index]; index++)
{
wchar_t character = server->game.variant.human_readable_game_description[index];
mode[index] = character < 128 ? (char)character : '?';
}
mode[index] = 0;
/* (a game type without a name of its own: its kind's) */
if (!mode[0])
{
static char const *const kinds[] = { "", "CTF", "Slayer", "Oddball", "King of the Hill", "Race" };
long kind = server->game.variant.game_engine_index;
csstrncpy(mode, kind > 0 && kind < (long)NUMBEROF(kinds) ? kinds[kind] : "Custom", sizeof(mode) - 1);
mode[sizeof(mode) - 1] = 0;
}
p2p_set_game_description(name, map, mode, server->game.player_count, server->game.maximum_players);
}
#endif
if (network_game_server_game_is_valid(server))
{
@@ -2855,6 +2901,10 @@ boolean server_has_enough_machines(
}
}
#ifdef HALO_ANDROID
/* (bots' machines count: the VR port's) */
machine_count += network_game_server_bot_machine_count(server);
#endif
has_enough_machines = machine_count >= minimum_machine_count;
return has_enough_machines;
@@ -3508,7 +3558,11 @@ void network_game_server_update_countdown(
else
{
if (network_game_should_accept_remote_connections() == FALSE ||
network_game_server_get_client_machine_count(server) > 1)
network_game_server_get_client_machine_count(server)
#ifdef HALO_ANDROID
+ network_game_server_bot_machine_count(server)
#endif
> 1)
{
unsigned long countdown;
@@ -3775,6 +3829,11 @@ static boolean network_game_server_add_new_client(
{
for (i = 0; i < MAXIMUM_NETWORK_MACHINE_COUNT; i++)
{
#ifdef HALO_ANDROID
/* (not a bot machine's slot) */
if (network_game_server_bot_machines[i] && server->game.machines[i].machine_index == i)
continue;
#endif
if (server->client_machines[i].machine_index == NONE)
{
struct transport_address client_address = { 0 };
@@ -4144,6 +4203,221 @@ static boolean network_game_server_have_all_machines_have_precached(
return all_machines_have_precached;
}
#ifdef HALO_ANDROID
/* ---------- bots (the VR port's: port/linux/game/bots.c)
Bots are the players of machines of the host's own that nothing connects
to: from the top slot down, four players each (a machine's most). The host
drives them (bots_tick); every other machine sees them as any other
machine's players. A new connection never takes a bot's slot, and in the
lobby they count as machines, so that a game of the host and bots starts. */
long config_integer(char const *name);
static void network_game_server_bot_copy_name(
wchar_t *destination,
wchar_t const *source,
short size)
{
short index;
for (index = 0; index < size - 1 && source[index]; index++)
destination[index] = source[index];
destination[index] = 0;
}
boolean network_game_server_is_bot_machine(
long machine_index)
{
struct network_game_server *server = global_network_game_server_get();
return server && VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) &&
network_game_server_bot_machines[machine_index] &&
server->game.machines[machine_index].machine_index == machine_index;
}
static short network_game_server_bot_machine_count(
struct network_game_server *server)
{
short count = 0;
long machine_index;
for (machine_index = 0; machine_index < MAXIMUM_NETWORK_MACHINE_COUNT; machine_index++)
{
if (network_game_server_bot_machines[machine_index] &&
server->game.machines[machine_index].machine_index == machine_index &&
network_game_server_machine_has_players(server, (short)machine_index))
{
count++;
}
}
return count;
}
static short network_game_server_bot_count(
struct network_game_server *server,
struct network_player **last)
{
short count = 0;
long player_index;
*last = NULL;
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) && VALID_INDEX(player->machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) &&
network_game_server_bot_machines[(long)player->machine_index])
{
count++;
*last = player;
}
}
return count;
}
static boolean network_game_server_add_bot(
struct network_game_server *server,
short number)
{
struct network_player bot;
long machine_index;
short controller_index;
if (!network_game_has_free_player_slot(&server->game))
return FALSE;
/* (a bot machine with room, else a new one from the top) */
for (machine_index = MAXIMUM_NETWORK_MACHINE_COUNT - 1; machine_index >= 0; machine_index--)
{
if (network_game_server_bot_machines[machine_index] &&
server->game.machines[machine_index].machine_index == machine_index)
{
for (controller_index = 0; controller_index < MAXIMUM_LOCAL_PLAYERS; controller_index++)
{
long player_index;
boolean taken = FALSE;
for (player_index = 0; player_index < MAXIMUM_NETWORK_PLAYER_COUNT && !taken; player_index++)
{
struct network_player *player = &server->game.players[player_index];
taken = network_player_is_valid(player) && player->machine_index == machine_index &&
player->controller_index == controller_index;
}
if (!taken)
goto found;
}
}
}
for (machine_index = MAXIMUM_NETWORK_MACHINE_COUNT - 1; machine_index >= 0; machine_index--)
{
if (!server->client_machines[machine_index].connection &&
server->client_machines[machine_index].machine_index == NONE &&
server->game.machines[machine_index].machine_index == NONE)
{
struct network_machine machine;
csmemset(&machine, 0, sizeof(machine));
network_game_server_bot_copy_name(machine.name, L"Bots", NUMBEROF(machine.name));
machine.machine_index = (char)machine_index;
if (!network_game_add_machine(&server->game, &machine))
return FALSE;
network_game_server_bot_machines[machine_index] = TRUE;
controller_index = 0;
goto found;
}
}
return FALSE;
found:
csmemset(&bot, 0, sizeof(bot));
{
wchar_t name[NETWORK_PLAYER_NAME_LENGTH];
short length = 0;
short digits = number < 10 ? 1 : number < 100 ? 2 : 3;
short digit;
network_game_server_bot_copy_name(name, L"Bot ", NUMBEROF(name));
while (name[length])
length++;
for (digit = digits - 1; digit >= 0 && length + digit < NETWORK_PLAYER_NAME_LENGTH - 1; digit--)
{
name[length + digit] = (wchar_t)(L'0' + number % 10);
number /= 10;
}
name[length + digits] = 0;
network_game_server_bot_copy_name(bot.name, name, NUMBEROF(bot.name));
}
bot.machine_index = (char)machine_index;
bot.controller_index = (char)controller_index;
bot.primary_color_index = NONE;
bot.icon_index = 0;
{
long player_count_by_team[NUMBER_OF_MULTIPLAYER_TEAMS] = { 0, 0 };
long player_index;
for (player_index = 0; player_index < MAXIMUM_NETWORK_PLAYER_COUNT; player_index++)
{
struct network_player *other_player = &server->game.players[player_index];
if (network_player_is_valid(other_player) && VALID_INDEX(other_player->team_index, NUMBER_OF_MULTIPLAYER_TEAMS))
player_count_by_team[other_player->team_index]++;
}
bot.team_index = player_count_by_team[1] < player_count_by_team[0] ? 1 : 0;
}
if (!player_name_is_unique(server, bot.name))
get_unique_random_name(server, &bot);
get_unique_random_color(server, &bot);
bot.player_list_index = NONE;
if (!network_game_add_player(&server->game, &bot))
return FALSE;
network_event("added a bot on machine %ld", machine_index);
return TRUE;
}
/* (the lobby) as many bots as bots.count asks for */
static void network_game_server_update_bots(
struct network_game_server *server)
{
long wanted = config_integer("bots.count");
struct network_player *last;
short count = network_game_server_bot_count(server, &last);
boolean changed = FALSE;
long machine_index;
/* (bot machines a new game no longer has) */
for (machine_index = 0; machine_index < MAXIMUM_NETWORK_MACHINE_COUNT; machine_index++)
{
if (network_game_server_bot_machines[machine_index] &&
server->game.machines[machine_index].machine_index != machine_index)
{
network_game_server_bot_machines[machine_index] = FALSE;
}
}
wanted = PIN(wanted, 0, 15);
while (count < wanted && network_game_server_add_bot(server, (short)(count + 1)))
{
count++;
changed = TRUE;
}
while (count > wanted && network_game_server_bot_count(server, &last) > 0 && last)
{
long bot_machine = last->machine_index;
network_game_remove_player(&server->game, last);
if (!network_game_server_machine_has_players(server, (short)bot_machine))
{
network_game_remove_machine(&server->game, &server->game.machines[bot_machine]);
network_game_server_bot_machines[bot_machine] = FALSE;
}
count--;
changed = TRUE;
}
if (changed)
network_game_server_send_game_data_pregame(server);
}
#endif
static boolean network_game_server_idle_pregame_tasks(
struct network_game_server *server)
{
@@ -4154,6 +4428,9 @@ static boolean network_game_server_idle_pregame_tasks(
{
long itr;
#ifdef HALO_ANDROID
network_game_server_update_bots(server);
#endif
/* send the lobby changes collected since the last settings update */
network_game_server_flush_game_data_pregame(server);