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
10 changed files with 1013 additions and 37 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
-3
View File
@@ -140,9 +140,6 @@ out (halo_vr_scope_pass). */
int halo_vr_scope(short local_player_index, float magnification, float position[3], float forward[3], float up[3],
float *vertical_field_of_view);
int halo_vr_scope_pass(void);
/* how much higher than a rifle's the weapon in hand has its scope's disc
(metres; render.c, before halo_vr_scope) */
void halo_vr_set_scope_raise(float metres);
void halo_vr_capture_scope(float left, float top, float right, float bottom);
/* where the weapon's gun node is as drawn, at the ready (world units;
first_person_weapons.c, each frame): shots, the crosshair and the laser
+32 -5
View File
@@ -304,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)." },
@@ -953,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);
@@ -964,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);
@@ -1029,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
+1 -10
View File
@@ -1957,14 +1957,6 @@ The world around it stays as it is (zooming the whole view sickens). */
#define SCOPE_ABOVE_HAND 0.07f
#define SCOPE_AHEAD_OF_HAND 0.22f
/* (the weapon in hand's: halo_vr_set_scope_raise) */
static float scope_raise;
void halo_vr_set_scope_raise(float metres)
{
scope_raise = metres;
}
int halo_vr_scope(short local_player_index, float magnification, float position[3], float forward[3], float up[3],
float *vertical_field_of_view)
{
@@ -1983,7 +1975,7 @@ int halo_vr_scope(short local_player_index, float magnification, float position[
game_to_local(right, local_right);
for (axis = 0; axis < 3; axis++)
{
vr.scope_pose.position[axis] = hand->aim.position[axis] + local_up[axis] * (SCOPE_ABOVE_HAND + scope_raise) +
vr.scope_pose.position[axis] = hand->aim.position[axis] + local_up[axis] * SCOPE_ABOVE_HAND +
local_forward[axis] * SCOPE_AHEAD_OF_HAND;
}
if (barrel_known())
@@ -2175,7 +2167,6 @@ int halo_vr_scope(short local_player_index, float magnification, float position[
return 0;
}
int halo_vr_scope_pass(void) { return 0; }
void halo_vr_set_scope_raise(float metres) { (void)metres; }
void halo_vr_capture_scope(float left, float top, float right, float bottom)
{
(void)left; (void)top; (void)right; (void)bottom;
+149 -8
View File
@@ -5643,6 +5643,8 @@ struct ui_public_game
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
{
@@ -5661,8 +5663,18 @@ static struct
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)
@@ -5762,15 +5774,94 @@ static void ui_join_code_panel_event(
}
}
/* 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: whether the game is public. TRUE:
the event is taken. */
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"))
@@ -5793,7 +5884,8 @@ static boolean ui_join_code_take_event(
if (event->data.button.index == _gamepad_analog_button_y &&
ui_join_code_screen_active("connected_pregame_screen") && global_network_game_server_get())
{
p2p_set_public_game(!p2p_public_game());
ui_join_code_globals.host_panel_open = TRUE;
ui_join_code_globals.host_selection = 0;
return TRUE;
}
return FALSE;
@@ -5975,15 +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[96];
char line[128];
long bots = config_integer("bots.count");
char bots_text[24];
snprintf(line, sizeof(line), "Online join code: %s %%y-button Public game: %s", code,
p2p_public_game() ? "Yes" : "No");
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, 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);
}
}
}
+238 -1
View File
@@ -696,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(
@@ -2895,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;
@@ -3548,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;
@@ -3815,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 };
@@ -4184,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)
{
@@ -4194,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);
-10
View File
@@ -746,16 +746,6 @@ static void render_vr_scope(
if (zoom_level == NONE || player_index == NONE || player_get(player_index)->unit_index == NONE)
return;
magnification = unit_get_zoom_magnification(player_get(player_index)->unit_index, zoom_level);
{
/* (a weapon whose body stands higher over the hand than a rifle's
has its scope's disc higher: the rocket launcher's tube) */
struct unit_datum *unit = unit_get(player_get(player_index)->unit_index);
long weapon_index = unit->unit.current_weapon_index >= 0 ?
unit->unit.weapon_object_indices[unit->unit.current_weapon_index] : NONE;
char const *name = weapon_index != NONE ? tag_get_name(object_get(weapon_index)->definition_index) : NULL;
halo_vr_set_scope_raise(name && strstr(name, "rocket launcher") ? 0.18f : 0.0f);
}
if (!halo_vr_scope(window->local_player_index, magnification, position, forward, up, &vertical_field_of_view))
return;
scope_window = *window;