Compare commits

...
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 30f362ff3d Bots pick up weapons, powerups and grenades, and throw grenades (phase 2)
Out of a fight a bot looks every second for what lies within 25 world
units worth having: a weapon better than the worse of its two (it changes
to that one and holds the swap while the game offers the weapon), ammo for
an empty weapon it carries, an overshield, camouflage, a health pack when
hurt, grenades when it has few. In a fight it takes the weapon that suits
the distance (shotgun near, sniper rifle far, no rocket up close), at
most every two seconds, changes from an empty one, and now and then lobs
a grenade at an enemy 6 to 20 world units off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:41:48 +02:00
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
7 changed files with 1375 additions and 14 deletions

No files matched your search

+949
View File
@@ -0,0 +1,949 @@
/*
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.
Out of a fight, a bot goes for what lies near that is worth having: a
weapon better than the worse of its two (and swaps that one for it, as a
player holding the action button does), ammunition for one it has when it
runs low, an overshield, camouflage, a health pack when hurt, grenades.
In a fight it changes to its other weapon when the one in hand is empty,
and now and then lobs a grenade at an enemy some way off.
*/
#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 "items/items.h"
#include "items/weapons.h"
#include "tag_files/tag_files.h"
#include <string.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 often it looks around for things to pick up (ticks) */
ITEM_LOOK_TICKS = 30,
/* the least time between its grenades (ticks) */
GRENADE_INTERVAL_TICKS = 150,
};
/* how far off it goes for things (world units) */
#define BOT_ITEM_RANGE 25.0f
/* a grenade's range: at least, at most (world units) */
#define BOT_GRENADE_MINIMUM 6.0f
#define BOT_GRENADE_MAXIMUM 20.0f
/* 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;
/* the thing it goes for, and when it last looked */
long item_index;
long item_look_time;
long grenade_time;
long weapon_switch_time;
};
/* ---------- 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;
long grenades;
long pickups;
long switches;
/* 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;
}
/* ---------- things to pick up */
/* how much a weapon is worth having, by its tag's name (0: not known) */
static real bot_weapon_worth(
long definition_index)
{
static struct
{
char const *name;
real worth;
} const weapons[] =
{
{ "rocket launcher", 9.0f }, { "sniper rifle", 8.0f }, { "fuel rod", 8.0f }, { "shotgun", 7.0f },
{ "flamethrower", 6.0f }, { "pistol", 6.0f }, { "plasma rifle", 5.0f }, { "assault rifle", 5.0f },
{ "needler", 4.0f }, { "plasma pistol", 4.0f },
};
char const *name = tag_get_name(definition_index);
short index;
if (!name)
return 0.0f;
/* (plasma pistol before pistol: the longer name first) */
if (strstr(name, "plasma pistol"))
return 4.0f;
for (index = 0; index < (short)NUMBEROF(weapons); index++)
{
if (strstr(name, weapons[index].name))
return weapons[index].worth;
}
return 3.0f;
}
/* how well a weapon suits a fight at that distance (world units) */
static real bot_weapon_suits(
long definition_index,
real distance)
{
char const *name = tag_get_name(definition_index);
real worth = bot_weapon_worth(definition_index);
if (!name)
return worth;
if (strstr(name, "shotgun") || strstr(name, "flamethrower"))
return distance < 6.0f ? worth + 5.0f : distance > 10.0f ? worth - 5.0f : worth;
if (strstr(name, "sniper rifle"))
return distance > 12.0f ? worth + 3.0f : worth - 4.0f;
if (strstr(name, "rocket launcher") || strstr(name, "fuel rod"))
return distance < 4.0f ? worth - 8.0f : worth + 1.0f;
if (strstr(name, "plasma pistol") || strstr(name, "needler"))
return worth;
if (strstr(name, "pistol"))
return distance > 10.0f ? worth + 2.0f : worth;
return worth;
}
/* whether a weapon has rounds left (a battery: charge) */
static boolean bot_weapon_has_ammunition(
long weapon_index)
{
struct weapon_datum *weapon = weapon_try_and_get(weapon_index);
if (!weapon)
return FALSE;
if (weapon->weapon.magazines[0].rounds_total > 0 || weapon->weapon.magazines[0].rounds_loaded > 0)
return TRUE;
/* (no magazine: a battery, spent when its age reaches 1) */
return weapon->weapon.magazines[0].rounds_loaded == 0 && weapon->weapon.magazines[0].rounds_total == 0 &&
weapon->weapon.age < 1.0f && weapon->weapon.magazines[0].original_time == 0;
}
/* how much the bot would gain by a thing lying there (0: nothing) */
static real bot_item_worth(
struct unit_datum const *unit,
long item_index)
{
struct object_datum *object = object_get(item_index);
char const *name = tag_get_name(object->definition_index);
short slot;
if (!name)
return 0.0f;
if (object->object.parent_object_index != NONE)
return 0.0f;
if (TEST_FLAG(_object_mask_equipment, object->object.type))
{
if (strstr(name, "over shield") || strstr(name, "overshield"))
return 9.0f;
if (strstr(name, "camouflage"))
return 8.0f;
if (strstr(name, "health"))
return unit->object.body_vitality < 0.7f ? 7.0f : 0.0f;
if (strstr(name, "frag grenade"))
return unit->unit.grenade_counts[0] < 2 ? 3.0f : 0.0f;
if (strstr(name, "plasma grenade"))
return NUMBER_OF_UNIT_GRENADE_TYPES > 1 && unit->unit.grenade_counts[1] < 2 ? 3.0f : 0.0f;
return 0.0f;
}
if (TEST_FLAG(_object_mask_weapon, object->object.type))
{
real worth = bot_weapon_worth(object->definition_index);
real worst = 1000.0f;
short carried = 0;
for (slot = 0; slot < MAXIMUM_WEAPONS_PER_UNIT; slot++)
{
long carried_index = unit->unit.weapon_object_indices[slot];
struct object_datum *carried_weapon = carried_index != NONE ? object_try_and_get(carried_index) : NULL;
if (!carried_weapon)
continue;
carried++;
/* (one it has: worth its ammunition when it runs low) */
if (carried_weapon->definition_index == object->definition_index)
return bot_weapon_has_ammunition(carried_index) ? 0.0f : 4.0f;
worst = MIN(worst, bot_weapon_worth(carried_weapon->definition_index));
}
if (carried < 2)
return worth;
return worth - worst > 0.5f ? worth - worst : 0.0f;
}
return 0.0f;
}
/* the thing most worth going for near the bot, or NONE */
static long bot_find_item(
struct unit_datum const *unit,
real_point3d const *position)
{
struct object_iterator iterator;
long best = NONE;
real best_score = 0.0f;
object_iterator_new(&iterator, _object_mask_weapon | _object_mask_equipment, 0);
while (object_iterator_next(&iterator))
{
real_point3d origin;
real distance;
real worth;
real score;
object_get_origin(iterator.index, &origin);
distance = distance3d(position, &origin);
if (distance > BOT_ITEM_RANGE)
continue;
worth = bot_item_worth(unit, iterator.index);
if (worth <= 0.0f)
continue;
score = worth / (1.0f + distance / 8.0f);
if (score > best_score)
{
best_score = score;
best = iterator.index;
}
}
return best;
}
/* near a weapon it wants: the worse of its two in hand (the one a swap
leaves), and the action held while the game offers the weapon */
static void bot_take_item(
struct bot *bot,
struct player_datum const *player,
struct unit_datum const *unit,
struct player_action *action)
{
struct object_datum *item = object_try_and_get(bot->item_index);
short slot;
short worst_slot = NONE;
real worst = 1000.0f;
short carried = 0;
if (!item || !TEST_FLAG(_object_mask_weapon, item->object.type))
return;
for (slot = 0; slot < MAXIMUM_WEAPONS_PER_UNIT; slot++)
{
long carried_index = unit->unit.weapon_object_indices[slot];
struct object_datum *carried_weapon = carried_index != NONE ? object_try_and_get(carried_index) : NULL;
real worth;
if (!carried_weapon)
continue;
carried++;
worth = bot_weapon_worth(carried_weapon->definition_index);
if (worth < worst)
{
worst = worth;
worst_slot = slot;
}
}
if (carried >= 2 && worst_slot != NONE)
action->desired_weapon_index = worst_slot;
if (player->action_object_index == bot->item_index &&
(player->action_result == _player_action_result_swap_for_weapon ||
player->action_result == _player_action_result_add_weapon_to_inventory) &&
(game_time_get() & 1))
{
SET_FLAG(action->control_flags, _unit_control_swap_weapons_bit, TRUE);
bots.pickups++;
}
}
/* 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;
/* (the weapon in hand empty: the other) */
if (unit->unit.current_weapon_index >= 0 &&
!bot_weapon_has_ammunition(unit->unit.weapon_object_indices[unit->unit.current_weapon_index]))
{
short slot;
for (slot = 0; slot < MAXIMUM_WEAPONS_PER_UNIT; slot++)
{
if (slot != unit->unit.current_weapon_index && unit->unit.weapon_object_indices[slot] != NONE &&
bot_weapon_has_ammunition(unit->unit.weapon_object_indices[slot]))
{
action->desired_weapon_index = slot;
break;
}
}
}
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;
/* the weapon that suits the distance, if it has rounds (changed no
more than every two seconds) */
if (game_time_get() - bot->weapon_switch_time > 60 || game_time_get() < bot->weapon_switch_time)
{
short slot;
short best_slot = NONE;
real best = -1000.0f;
for (slot = 0; slot < MAXIMUM_WEAPONS_PER_UNIT; slot++)
{
long weapon_index = unit->unit.weapon_object_indices[slot];
real suits;
if (weapon_index == NONE || !bot_weapon_has_ammunition(weapon_index))
continue;
suits = bot_weapon_suits(object_get(weapon_index)->definition_index, distance) +
(slot == unit->unit.current_weapon_index ? 1.0f : 0.0f);
if (suits > best)
{
best = suits;
best_slot = slot;
}
}
if (best_slot != NONE && best_slot != unit->unit.current_weapon_index)
{
action->desired_weapon_index = best_slot;
bot->weapon_switch_time = game_time_get();
bots.switches++;
}
}
/* 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);
/* (a grenade now and then, lobbed at one some way off) */
if (distance > BOT_GRENADE_MINIMUM && distance < BOT_GRENADE_MAXIMUM &&
game_time_get() - bot->grenade_time > GRENADE_INTERVAL_TICKS && bot_random() < 0.02f)
{
short type;
for (type = 0; type < NUMBER_OF_UNIT_GRENADE_TYPES; type++)
{
if (unit->unit.grenade_counts[type] > 0)
break;
}
if (type < NUMBER_OF_UNIT_GRENADE_TYPES)
{
bot->grenade_time = game_time_get();
bots.grenades++;
action->desired_grenade_index = type;
wanted_pitch += PIN(distance * 0.02f, 0.05f, 0.4f);
SET_FLAG(action->control_flags, _unit_control_throw_grenade_bit, TRUE);
SET_FLAG(action->control_flags, _unit_control_weapon_secondary_trigger_bit, TRUE);
firing = FALSE;
}
}
}
else
{
real_point3d point;
/* something worth having near: there first */
if (game_time_get() - bot->item_look_time >= ITEM_LOOK_TICKS || game_time_get() < bot->item_look_time)
{
long item = bot_find_item(unit, &position);
bot->item_look_time = game_time_get();
if (item != bot->item_index)
{
bot->item_index = item;
if (item != NONE)
{
object_get_origin(item, &bot->goal);
bot->has_goal = TRUE;
bot->goal_time = game_time_get();
bot->path_time = game_time_get() - PATH_REFRESH_TICKS;
}
}
}
if (bot->item_index != NONE)
{
/* (taken or gone: on) */
if (!object_try_and_get(bot->item_index) || bot_item_worth(unit, bot->item_index) <= 0.0f)
{
bot->item_index = NONE;
bot->has_goal = FALSE;
}
else
{
object_get_origin(bot->item_index, &bot->goal);
bot->goal_time = game_time_get();
if (distance3d(&position, &bot->goal) < 2.0f)
bot_take_item(bot, player, unit, action);
}
}
/* hunts, or roams: along its way, looking where it goes */
if (bot->item_index == NONE &&
(!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;
free_bot->item_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; weapon swaps asked %ld; grenades %ld; changes %ld",
(int)count, bots.paths_found, bots.paths_failed, bots.pickups, bots.grenades, bots.switches);
bots.paths_found = bots.paths_failed = bots.pickups = bots.grenades = bots.switches = 0;
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
{
struct player_datum *player = distributed_player(index);
struct unit_datum *unit;
short slot;
char text[160] = "";
if (!player || !network_game_server_is_bot_machine(player->network_player_data.machine_index) ||
!(unit = unit_try_and_get(player->unit_index)))
{
continue;
}
for (slot = 0; slot < MAXIMUM_WEAPONS_PER_UNIT; slot++)
{
long weapon_index = unit->unit.weapon_object_indices[slot];
char const *name = weapon_index != NONE ? tag_get_name(object_get(weapon_index)->definition_index) : NULL;
char const *last = name ? strrchr(name, '\\') : NULL;
if (name)
{
strncat(text, last ? last + 1 : name, sizeof(text) - strlen(text) - 3);
strncat(text, slot == unit->unit.current_weapon_index ? "* " : " ", sizeof(text) - strlen(text) - 1);
}
}
platform_log("bots: player %d carries %s, grenades %d/%d", index, text, unit->unit.grenade_counts[0],
unit->unit.grenade_counts[1]);
}
}
}
+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
+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
+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);