mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-17 16:00:26 +02:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6c2322df9 | ||
|
|
39fbaefaac | ||
|
|
c6397cb515 | ||
|
|
5ba6c13097 | ||
|
|
66c0d33e34 | ||
|
|
6b77fe428a | ||
|
|
0badc451cb | ||
|
|
39a6cc9794 | ||
|
|
35b90f26fc |
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -3050,6 +3050,8 @@ void network_distributed_new_game(
|
||||
update_queues_distributed_reset();
|
||||
network_objects_new_game();
|
||||
network_damage_new_game();
|
||||
network_vr_new_game();
|
||||
bots_new_game();
|
||||
}
|
||||
|
||||
/* after each tick (game_time.c) */
|
||||
@@ -3091,6 +3093,8 @@ void network_distributed_tick(
|
||||
distributed_send_statistics(game_time_get() % STATISTICS_REFRESH_TICKS == 0);
|
||||
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);
|
||||
@@ -3099,6 +3103,7 @@ void network_distributed_tick(
|
||||
{
|
||||
distributed_note_own_positions();
|
||||
distributed_client_send_inputs();
|
||||
network_vr_tick();
|
||||
distributed_client_send_predictions();
|
||||
network_objects_client_tick();
|
||||
network_damage_client_tick();
|
||||
@@ -3128,6 +3133,7 @@ static boolean distributed_message_stale(
|
||||
case _distributed_message_player_inputs:
|
||||
case _distributed_message_relayed_actions:
|
||||
case _distributed_message_damage_events:
|
||||
case _distributed_message_vr_poses:
|
||||
break;
|
||||
default:
|
||||
return FALSE;
|
||||
@@ -3572,6 +3578,7 @@ void network_distributed_handle_message(
|
||||
case _distributed_message_notice:
|
||||
case _distributed_message_client_ready: entry_size = 0; break;
|
||||
case _distributed_message_client_identity: entry_size = sizeof(struct distributed_client_identity); break;
|
||||
case _distributed_message_vr_poses: entry_size = network_vr_entry_size(); break;
|
||||
case _distributed_message_damage_events:
|
||||
case _distributed_message_hit_reports: entry_size = network_damage_entry_size(header.type); break;
|
||||
default: entry_size = network_objects_entry_size(header.type); break;
|
||||
@@ -3583,7 +3590,19 @@ void network_distributed_handle_message(
|
||||
}
|
||||
distributed_statistics.received++;
|
||||
|
||||
/* (each kind from the host, or from a client) */
|
||||
/* (each kind from the host, or from a client; a VR player's hands
|
||||
both ways) */
|
||||
if (header.type == _distributed_message_vr_poses)
|
||||
{
|
||||
if (machine_index == NONE ? game_connection() != _game_connection_network_client :
|
||||
game_connection() != _game_connection_network_server)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!distributed_message_stale(machine_index, &header))
|
||||
network_vr_handle_poses(machine_index, entries, header.count);
|
||||
return;
|
||||
}
|
||||
switch (header.type)
|
||||
{
|
||||
case _distributed_message_player_prediction:
|
||||
|
||||
@@ -62,6 +62,10 @@ enum
|
||||
/* a client's Discord user, as its Discord told it (reliable, with its
|
||||
ready: distributed_client_send_identity) */
|
||||
_distributed_message_client_identity,
|
||||
/* (the VR port's) a VR player's hands, every tick: a client's own to
|
||||
the host, the host's of the others to each client (unreliable;
|
||||
network_vr.c). Last, so that a build without it drops it. */
|
||||
_distributed_message_vr_poses,
|
||||
|
||||
NUMBER_OF_DISTRIBUTED_MESSAGES
|
||||
};
|
||||
@@ -226,4 +230,14 @@ void network_damage_handle_events(void const *entries, short count);
|
||||
void network_damage_handle_reports(long machine_index, void const *entries, short count);
|
||||
word network_damage_entry_size(byte type);
|
||||
|
||||
/* VR players' hands (network_vr.c) */
|
||||
void network_vr_new_game(void);
|
||||
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
|
||||
@@ -0,0 +1,506 @@
|
||||
/*
|
||||
NETWORK_VR.C
|
||||
|
||||
VR players' hands for the other machines: an extension of the distributed
|
||||
netcode (port/linux/NETCODE.md, network_distributed.c). A VR player's
|
||||
weapon aims where the right controller points and fires from the hand, and
|
||||
the left hand holds a grenade and throws it its own way (port/linux/src/
|
||||
vr.c); the input the netcode relays has only where the head looks, so the
|
||||
other machines would show that player aiming and throwing where they look.
|
||||
|
||||
Every tick, a machine with a VR player sends the host that player's hands
|
||||
(_distributed_message_vr_poses, unreliable): where the weapon aims, where
|
||||
its muzzle is from the unit's camera, whether the left hand holds a
|
||||
grenade, and the last throw (from where, which way, how hard) for a second
|
||||
after it. The host keeps them, its own VR players' too, and sends each
|
||||
client those of the players of other machines. A machine drives a VR
|
||||
player of another (players.c, weapons.c, units.c) as long as their hands
|
||||
keep coming, else as before.
|
||||
|
||||
What the hands decide is only how the game looks: damage is the shooter's
|
||||
report, which the host checks as any other (network_damage.c).
|
||||
|
||||
A machine that does not know the message (a build without this, of the same
|
||||
network version) drops it, as it drops any type past its own
|
||||
(network_distributed_handle_message): VR and other builds still play
|
||||
together, the others seeing a VR player aim where the head looks.
|
||||
*/
|
||||
|
||||
#include "cseries.h"
|
||||
#include "game/game.h"
|
||||
#include "game/players.h"
|
||||
#include "units/units.h"
|
||||
#include "network_distributed.h"
|
||||
#include "halo_vr.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
void platform_log(char const *format, ...);
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
enum
|
||||
{
|
||||
_vr_pose_aim_bit,
|
||||
_vr_pose_weapon_offset_bit,
|
||||
_vr_pose_grenade_held_bit,
|
||||
_vr_pose_throw_bit,
|
||||
};
|
||||
|
||||
/* how long a player's hands drive them after the last came (ticks): longer
|
||||
and they are driven as before (the hands stopped coming: the player left
|
||||
VR, or the network). Where the weapon aims and fires from is held a
|
||||
second, over the gaps of a network that delivers in bursts (as the input
|
||||
the netcode relays is); a grenade held, only a third of one, so that it
|
||||
does not hang in the hand after the hand let go. The host relays only
|
||||
hands as fresh as that. */
|
||||
#define VR_POSE_FRESH_TICKS 10
|
||||
#define VR_AIM_HOLD_TICKS 30
|
||||
/* how long a throw is sent after it (ticks), and taken after it came */
|
||||
#define VR_THROW_TICKS 30
|
||||
/* offsets from the unit's camera: in 1/4096 of a world unit, no further
|
||||
than a hand reaches */
|
||||
#define VR_OFFSET_SCALE 4096.0f
|
||||
#define VR_OFFSET_MAXIMUM 1.0f
|
||||
/* how often what was sent and heard is logged (ticks) */
|
||||
#define VR_LOG_TICKS 150
|
||||
|
||||
/* ---------- structures */
|
||||
|
||||
struct distributed_vr_pose
|
||||
{
|
||||
byte player_index;
|
||||
byte flags;
|
||||
byte throw_sequence;
|
||||
byte throw_strength;
|
||||
struct distributed_vector aim;
|
||||
struct distributed_vector weapon_offset;
|
||||
struct distributed_vector throw_offset;
|
||||
struct distributed_vector throw_direction;
|
||||
};
|
||||
|
||||
typedef char distributed_vr_pose_size_assert[sizeof(struct distributed_vr_pose) == 28 ? 1 : -1];
|
||||
|
||||
/* a player's hands, as last heard of (this machine's own VR players on the
|
||||
host too, to send) */
|
||||
struct network_vr_player
|
||||
{
|
||||
boolean valid;
|
||||
long received_time;
|
||||
struct distributed_vr_pose pose;
|
||||
/* the throw this machine has taken for the player's grenade */
|
||||
boolean throw_taken;
|
||||
byte throw_taken_sequence;
|
||||
long throw_received_time;
|
||||
byte throw_received_sequence;
|
||||
};
|
||||
|
||||
/* a local VR player's last throw */
|
||||
struct network_vr_throw
|
||||
{
|
||||
boolean valid;
|
||||
long time;
|
||||
byte sequence;
|
||||
real_vector3d offset;
|
||||
real_vector3d direction;
|
||||
real strength;
|
||||
};
|
||||
|
||||
/* ---------- globals */
|
||||
|
||||
static struct network_vr_player network_vr_players[MAXIMUM_TRACKED_PLAYERS];
|
||||
static struct network_vr_throw network_vr_throws[MAXIMUM_LOCAL_PLAYERS];
|
||||
static byte network_vr_throw_sequences[MAXIMUM_LOCAL_PLAYERS];
|
||||
/* what was sent and heard since last logged */
|
||||
static struct
|
||||
{
|
||||
long sent;
|
||||
long heard;
|
||||
long aimed;
|
||||
} network_vr_counts;
|
||||
|
||||
/* ---------- private code */
|
||||
|
||||
static boolean network_vr_in_network_game(
|
||||
void)
|
||||
{
|
||||
short connection = game_connection();
|
||||
|
||||
return connection == _game_connection_network_server || connection == _game_connection_network_client;
|
||||
}
|
||||
|
||||
static void network_vr_offset_pack(
|
||||
real_vector3d const *offset,
|
||||
struct distributed_vector *result)
|
||||
{
|
||||
distributed_vector_pack(offset, VR_OFFSET_SCALE, result);
|
||||
}
|
||||
|
||||
/* (FALSE: not a hand's offset) */
|
||||
static boolean network_vr_offset_unpack(
|
||||
struct distributed_vector const *packed,
|
||||
real_vector3d *offset)
|
||||
{
|
||||
distributed_vector_unpack(packed, VR_OFFSET_SCALE, offset);
|
||||
return fabsf(offset->i) <= VR_OFFSET_MAXIMUM && fabsf(offset->j) <= VR_OFFSET_MAXIMUM &&
|
||||
fabsf(offset->k) <= VR_OFFSET_MAXIMUM;
|
||||
}
|
||||
|
||||
/* (FALSE: no direction) */
|
||||
static boolean network_vr_direction_unpack(
|
||||
struct distributed_vector const *packed,
|
||||
real_vector3d *direction)
|
||||
{
|
||||
distributed_unit_vector_unpack(packed, direction);
|
||||
return direction->i * direction->i + direction->j * direction->j + direction->k * direction->k > 0.5f;
|
||||
}
|
||||
|
||||
/* a player of another machine whose hands came lately, or NULL */
|
||||
static struct network_vr_player *network_vr_remote_player(
|
||||
long player_index,
|
||||
long fresh_ticks)
|
||||
{
|
||||
struct network_vr_player *vr_player;
|
||||
long absolute_index;
|
||||
|
||||
if (player_index == NONE || !network_vr_in_network_game() || distributed_player_is_local(player_index))
|
||||
return NULL;
|
||||
absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index);
|
||||
if (absolute_index < 0 || absolute_index >= MAXIMUM_TRACKED_PLAYERS)
|
||||
return NULL;
|
||||
vr_player = &network_vr_players[absolute_index];
|
||||
if (!vr_player->valid || game_time_get() - vr_player->received_time > fresh_ticks ||
|
||||
game_time_get() < vr_player->received_time)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
return vr_player;
|
||||
}
|
||||
|
||||
/* what is said of a player's hands, kept (checked: a client's of its own) */
|
||||
static void network_vr_take_pose(
|
||||
struct distributed_vr_pose const *pose,
|
||||
boolean heard)
|
||||
{
|
||||
struct network_vr_player *vr_player;
|
||||
real_vector3d vector;
|
||||
|
||||
if (pose->player_index >= MAXIMUM_TRACKED_PLAYERS)
|
||||
return;
|
||||
vr_player = &network_vr_players[pose->player_index];
|
||||
vr_player->pose = *pose;
|
||||
if (TEST_FLAG(pose->flags, _vr_pose_aim_bit) && !network_vr_direction_unpack(&pose->aim, &vector))
|
||||
SET_FLAG(vr_player->pose.flags, _vr_pose_aim_bit, FALSE);
|
||||
if (TEST_FLAG(pose->flags, _vr_pose_weapon_offset_bit) && !network_vr_offset_unpack(&pose->weapon_offset, &vector))
|
||||
SET_FLAG(vr_player->pose.flags, _vr_pose_weapon_offset_bit, FALSE);
|
||||
if (TEST_FLAG(pose->flags, _vr_pose_throw_bit) &&
|
||||
(!network_vr_offset_unpack(&pose->throw_offset, &vector) ||
|
||||
!network_vr_direction_unpack(&pose->throw_direction, &vector)))
|
||||
{
|
||||
SET_FLAG(vr_player->pose.flags, _vr_pose_throw_bit, FALSE);
|
||||
}
|
||||
/* (a throw new to this machine: when it came) */
|
||||
if (TEST_FLAG(vr_player->pose.flags, _vr_pose_throw_bit) &&
|
||||
(!vr_player->valid || vr_player->throw_received_sequence != pose->throw_sequence ||
|
||||
vr_player->throw_received_time == NONE))
|
||||
{
|
||||
vr_player->throw_received_sequence = pose->throw_sequence;
|
||||
vr_player->throw_received_time = game_time_get();
|
||||
}
|
||||
vr_player->valid = TRUE;
|
||||
vr_player->received_time = game_time_get();
|
||||
if (heard)
|
||||
network_vr_counts.heard++;
|
||||
}
|
||||
|
||||
/* this machine's VR player's hands, to send; FALSE when it has none */
|
||||
static boolean network_vr_local_pose(
|
||||
short local_player_index,
|
||||
struct distributed_vr_pose *pose)
|
||||
{
|
||||
long player_index = local_player_get_player_index(local_player_index);
|
||||
struct player_datum *player = player_index != NONE ? player_try_and_get(player_index) : NULL;
|
||||
struct unit_datum *unit = player && player->unit_index != NONE ? unit_try_and_get(player->unit_index) : NULL;
|
||||
struct network_vr_throw *last_throw = &network_vr_throws[local_player_index];
|
||||
float vector[3];
|
||||
|
||||
if (!unit || !halo_vr_active())
|
||||
return FALSE;
|
||||
csmemset(pose, 0, sizeof(*pose));
|
||||
pose->player_index = distributed_player_to_byte(player_index);
|
||||
/* (on foot: seated, the seat aims, and the input says where) */
|
||||
if (unit->object.parent_object_index == NONE)
|
||||
{
|
||||
if (halo_vr_aim_vector(local_player_index, vector))
|
||||
{
|
||||
real_vector3d aim;
|
||||
|
||||
aim.i = vector[0];
|
||||
aim.j = vector[1];
|
||||
aim.k = vector[2];
|
||||
distributed_vector_pack(&aim, DISTRIBUTED_UNIT_SCALE, &pose->aim);
|
||||
SET_FLAG(pose->flags, _vr_pose_aim_bit, TRUE);
|
||||
}
|
||||
if (halo_vr_weapon_origin_offset(local_player_index, vector))
|
||||
{
|
||||
real_vector3d offset;
|
||||
|
||||
offset.i = vector[0];
|
||||
offset.j = vector[1];
|
||||
offset.k = vector[2];
|
||||
if (fabsf(offset.i) <= VR_OFFSET_MAXIMUM && fabsf(offset.j) <= VR_OFFSET_MAXIMUM &&
|
||||
fabsf(offset.k) <= VR_OFFSET_MAXIMUM)
|
||||
{
|
||||
network_vr_offset_pack(&offset, &pose->weapon_offset);
|
||||
SET_FLAG(pose->flags, _vr_pose_weapon_offset_bit, TRUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (halo_vr_grenade_held(local_player_index))
|
||||
SET_FLAG(pose->flags, _vr_pose_grenade_held_bit, TRUE);
|
||||
if (last_throw->valid && game_time_get() - last_throw->time <= VR_THROW_TICKS &&
|
||||
game_time_get() >= last_throw->time)
|
||||
{
|
||||
network_vr_offset_pack(&last_throw->offset, &pose->throw_offset);
|
||||
distributed_vector_pack(&last_throw->direction, DISTRIBUTED_UNIT_SCALE, &pose->throw_direction);
|
||||
pose->throw_sequence = last_throw->sequence;
|
||||
pose->throw_strength = (byte)(PIN(last_throw->strength, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
SET_FLAG(pose->flags, _vr_pose_throw_bit, TRUE);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
void network_vr_new_game(
|
||||
void)
|
||||
{
|
||||
short index;
|
||||
|
||||
csmemset(network_vr_players, 0, sizeof(network_vr_players));
|
||||
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
|
||||
network_vr_players[index].throw_received_time = NONE;
|
||||
csmemset(network_vr_throws, 0, sizeof(network_vr_throws));
|
||||
csmemset(&network_vr_counts, 0, sizeof(network_vr_counts));
|
||||
}
|
||||
|
||||
word network_vr_entry_size(
|
||||
void)
|
||||
{
|
||||
return sizeof(struct distributed_vr_pose);
|
||||
}
|
||||
|
||||
/* after each tick (network_distributed_tick): a client its own VR players'
|
||||
hands to the host; the host its own and its clients' to each client */
|
||||
void network_vr_tick(
|
||||
void)
|
||||
{
|
||||
short connection = game_connection();
|
||||
short local_player_index;
|
||||
struct
|
||||
{
|
||||
struct distributed_message_header header;
|
||||
struct distributed_vr_pose poses[MAXIMUM_TRACKED_PLAYERS];
|
||||
} message;
|
||||
short count = 0;
|
||||
|
||||
if (game_time_get() % VR_LOG_TICKS == 0 &&
|
||||
(network_vr_counts.sent || network_vr_counts.heard || network_vr_counts.aimed))
|
||||
{
|
||||
platform_log("VR network: hands sent %ld, heard %ld, other players aimed by them %ld (in %d ticks)",
|
||||
network_vr_counts.sent, network_vr_counts.heard, network_vr_counts.aimed, VR_LOG_TICKS);
|
||||
csmemset(&network_vr_counts, 0, sizeof(network_vr_counts));
|
||||
}
|
||||
if (connection == _game_connection_network_client)
|
||||
{
|
||||
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
|
||||
{
|
||||
if (network_vr_local_pose(local_player_index, &message.poses[count]))
|
||||
count++;
|
||||
}
|
||||
network_vr_counts.sent += count;
|
||||
if (count)
|
||||
{
|
||||
distributed_send(&message, _distributed_message_vr_poses, count,
|
||||
(word)(sizeof(message.header) + count * sizeof(struct distributed_vr_pose)), _distributed_to_host);
|
||||
}
|
||||
}
|
||||
else if (connection == _game_connection_network_server)
|
||||
{
|
||||
long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
|
||||
short machine_count;
|
||||
short machine_number;
|
||||
short player_index;
|
||||
|
||||
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
|
||||
{
|
||||
struct distributed_vr_pose pose;
|
||||
|
||||
if (network_vr_local_pose(local_player_index, &pose))
|
||||
network_vr_take_pose(&pose, FALSE);
|
||||
}
|
||||
machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES);
|
||||
for (machine_number = 0; machine_number < machine_count; machine_number++)
|
||||
{
|
||||
long machine_index = machine_indices[machine_number];
|
||||
|
||||
count = 0;
|
||||
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS && count < 255; player_index++)
|
||||
{
|
||||
struct network_vr_player const *vr_player = &network_vr_players[player_index];
|
||||
|
||||
if (vr_player->valid && game_time_get() - vr_player->received_time <= VR_POSE_FRESH_TICKS &&
|
||||
game_time_get() >= vr_player->received_time &&
|
||||
distributed_player(player_index) &&
|
||||
!distributed_machine_has_player(machine_index, player_index))
|
||||
{
|
||||
message.poses[count++] = vr_player->pose;
|
||||
}
|
||||
}
|
||||
network_vr_counts.sent += count;
|
||||
if (count)
|
||||
{
|
||||
distributed_send_to_machine(machine_index, &message, _distributed_message_vr_poses, count,
|
||||
(word)(sizeof(message.header) + count * sizeof(struct distributed_vr_pose)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* the hands a machine sent: the host's of a client's players, a client's
|
||||
of the others */
|
||||
void network_vr_handle_poses(
|
||||
long machine_index,
|
||||
void const *entries,
|
||||
short count)
|
||||
{
|
||||
short index;
|
||||
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
struct distributed_vr_pose pose;
|
||||
|
||||
csmemcpy(&pose, (byte const *)entries + index * sizeof(pose), sizeof(pose));
|
||||
if (pose.player_index >= MAXIMUM_TRACKED_PLAYERS || !distributed_player(pose.player_index))
|
||||
continue;
|
||||
if (machine_index != NONE)
|
||||
{
|
||||
/* (the host: only of that client's own players) */
|
||||
if (!distributed_machine_has_player(machine_index, pose.player_index))
|
||||
continue;
|
||||
}
|
||||
else if (distributed_player_is_local(distributed_player_from_byte(pose.player_index)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
network_vr_take_pose(&pose, TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- the game's side (halo_vr.h) */
|
||||
|
||||
int halo_vr_network_aim(long player_index, float vector[3])
|
||||
{
|
||||
struct network_vr_player *vr_player = network_vr_remote_player(player_index, VR_AIM_HOLD_TICKS);
|
||||
real_vector3d aim;
|
||||
|
||||
if (!vr_player || !TEST_FLAG(vr_player->pose.flags, _vr_pose_aim_bit) ||
|
||||
!network_vr_direction_unpack(&vr_player->pose.aim, &aim))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
vector[0] = aim.i;
|
||||
vector[1] = aim.j;
|
||||
vector[2] = aim.k;
|
||||
network_vr_counts.aimed++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int halo_vr_network_weapon_origin(long player_index, long unit_index, float origin[3])
|
||||
{
|
||||
struct network_vr_player *vr_player = network_vr_remote_player(player_index, VR_AIM_HOLD_TICKS);
|
||||
real_vector3d offset;
|
||||
real_point3d camera;
|
||||
|
||||
if (!vr_player || unit_index == NONE || !TEST_FLAG(vr_player->pose.flags, _vr_pose_weapon_offset_bit) ||
|
||||
!network_vr_offset_unpack(&vr_player->pose.weapon_offset, &offset))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
unit_get_camera_position(unit_index, &camera);
|
||||
origin[0] = camera.x + offset.i;
|
||||
origin[1] = camera.y + offset.j;
|
||||
origin[2] = camera.z + offset.k;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int halo_vr_network_grenade_held(long player_index)
|
||||
{
|
||||
struct network_vr_player *vr_player = network_vr_remote_player(player_index, VR_POSE_FRESH_TICKS);
|
||||
|
||||
return vr_player && TEST_FLAG(vr_player->pose.flags, _vr_pose_grenade_held_bit);
|
||||
}
|
||||
|
||||
int halo_vr_network_grenade_throw(long player_index, long unit_index, float origin[3], float direction[3],
|
||||
float *strength)
|
||||
{
|
||||
struct network_vr_player *vr_player = network_vr_remote_player(player_index, VR_THROW_TICKS);
|
||||
real_vector3d offset;
|
||||
real_vector3d way;
|
||||
real_point3d camera;
|
||||
|
||||
if (!vr_player || unit_index == NONE || !TEST_FLAG(vr_player->pose.flags, _vr_pose_throw_bit) ||
|
||||
vr_player->throw_received_time == NONE || game_time_get() - vr_player->throw_received_time > VR_THROW_TICKS ||
|
||||
(vr_player->throw_taken && vr_player->throw_taken_sequence == vr_player->pose.throw_sequence) ||
|
||||
!network_vr_offset_unpack(&vr_player->pose.throw_offset, &offset) ||
|
||||
!network_vr_direction_unpack(&vr_player->pose.throw_direction, &way))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
vr_player->throw_taken = TRUE;
|
||||
vr_player->throw_taken_sequence = vr_player->pose.throw_sequence;
|
||||
unit_get_camera_position(unit_index, &camera);
|
||||
origin[0] = camera.x + offset.i;
|
||||
origin[1] = camera.y + offset.j;
|
||||
origin[2] = camera.z + offset.k;
|
||||
direction[0] = way.i;
|
||||
direction[1] = way.j;
|
||||
direction[2] = way.k;
|
||||
*strength = (float)vr_player->pose.throw_strength / 255.0f;
|
||||
platform_log("VR network: player %ld's grenade thrown as their hand threw it (%.2f)",
|
||||
DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index), *strength);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void halo_vr_network_grenade_thrown(short local_player_index, long unit_index, const float origin[3],
|
||||
const float direction[3], float strength)
|
||||
{
|
||||
struct network_vr_throw *last_throw;
|
||||
real_point3d camera;
|
||||
|
||||
if (local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS || unit_index == NONE ||
|
||||
!network_vr_in_network_game())
|
||||
{
|
||||
return;
|
||||
}
|
||||
last_throw = &network_vr_throws[local_player_index];
|
||||
unit_get_camera_position(unit_index, &camera);
|
||||
last_throw->offset.i = origin[0] - camera.x;
|
||||
last_throw->offset.j = origin[1] - camera.y;
|
||||
last_throw->offset.k = origin[2] - camera.z;
|
||||
/* (a hand further off than it reaches: none sent) */
|
||||
if (fabsf(last_throw->offset.i) > VR_OFFSET_MAXIMUM || fabsf(last_throw->offset.j) > VR_OFFSET_MAXIMUM ||
|
||||
fabsf(last_throw->offset.k) > VR_OFFSET_MAXIMUM)
|
||||
{
|
||||
last_throw->valid = FALSE;
|
||||
return;
|
||||
}
|
||||
last_throw->direction.i = direction[0];
|
||||
last_throw->direction.j = direction[1];
|
||||
last_throw->direction.k = direction[2];
|
||||
last_throw->strength = strength;
|
||||
last_throw->time = game_time_get();
|
||||
last_throw->sequence = ++network_vr_throw_sequences[local_player_index];
|
||||
last_throw->valid = TRUE;
|
||||
platform_log("VR network: grenade throw #%d sent (%.2f)", last_throw->sequence, strength);
|
||||
}
|
||||
@@ -201,6 +201,23 @@ float halo_vr_enclosed_seat_raise(int hull);
|
||||
/* how strongly glass tints and reflects (1 as the game has it) */
|
||||
float halo_vr_glass_strength(void);
|
||||
|
||||
/* VR players of other machines in a network game (port/linux/game/
|
||||
network_vr.c): their hands, as their machines send them, while they keep
|
||||
coming (each returns 0 otherwise, and for this machine's own players, and
|
||||
the game does as before):
|
||||
- where the weapon aims, on foot;
|
||||
- where its shots start (world units), from the unit's camera;
|
||||
- whether the left hand holds the grenade;
|
||||
- the hand's throw, once, as the grenade goes (as halo_vr_grenade_throw);
|
||||
and this machine's own VR player's throw, to send (units.c). */
|
||||
int halo_vr_network_aim(long player_index, float vector[3]);
|
||||
int halo_vr_network_weapon_origin(long player_index, long unit_index, float origin[3]);
|
||||
int halo_vr_network_grenade_held(long player_index);
|
||||
int halo_vr_network_grenade_throw(long player_index, long unit_index, float origin[3], float direction[3],
|
||||
float *strength);
|
||||
void halo_vr_network_grenade_thrown(short local_player_index, long unit_index, const float origin[3],
|
||||
const float direction[3], float strength);
|
||||
|
||||
/* the platform layer's side (vr.c) */
|
||||
void halo_vr_initialize(void);
|
||||
void halo_vr_frame_begin(void);
|
||||
|
||||
+107
-2
@@ -331,6 +331,8 @@ static struct
|
||||
unsigned char join_host_hash[P2P_KEY_HASH_SIZE];
|
||||
unsigned char join_token[P2P_TOKEN_SIZE];
|
||||
unsigned long join_time;
|
||||
/* how the latest join goes, for the menus (p2p_join_state) */
|
||||
int join_state;
|
||||
|
||||
char clipboard[P2P_LINK_SIZE];
|
||||
int has_clipboard;
|
||||
@@ -840,6 +842,12 @@ static void drop_peer(struct peer *peer, const char *reason)
|
||||
struct retired_session *retired = &p2p.retired[p2p.retired_next++ % MAXIMUM_RETIRED_SESSIONS];
|
||||
int ask_again = p2p.joining && peer->is_host && !memcmp(peer->identifier, p2p.join_host, P2P_IDENTIFIER_SIZE);
|
||||
|
||||
if (peer->is_host && !memcmp(peer->identifier, p2p.join_host, P2P_IDENTIFIER_SIZE) &&
|
||||
p2p.join_state != P2P_JOIN_NONE)
|
||||
{
|
||||
p2p.join_state = peer->connected ? P2P_JOIN_LOST : P2P_JOIN_BLOCKED;
|
||||
}
|
||||
|
||||
platform_log("Internet play: %s %s: %s", peer->is_host ? "host" : "player", peer->name, reason);
|
||||
if (peer->connected)
|
||||
{
|
||||
@@ -1029,6 +1037,8 @@ static void peer_heard(struct peer *peer, unsigned long address, unsigned short
|
||||
address_text(address, port, text));
|
||||
if (peer->is_host)
|
||||
{
|
||||
if (!memcmp(p2p.join_host, peer->identifier, P2P_IDENTIFIER_SIZE) && p2p.join_state != P2P_JOIN_NONE)
|
||||
p2p.join_state = P2P_JOIN_CONNECTED;
|
||||
if (p2p.joining && !memcmp(p2p.join_host, peer->identifier, P2P_IDENTIFIER_SIZE))
|
||||
{
|
||||
p2p.joining = 0;
|
||||
@@ -2298,6 +2308,7 @@ static int join_invite(const char *text)
|
||||
memcpy(p2p.join_host_hash, hash, sizeof(hash));
|
||||
memcpy(p2p.join_token, token, sizeof(token));
|
||||
p2p.join_requested = 1;
|
||||
p2p.join_state = P2P_JOIN_REACHING;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -2314,10 +2325,93 @@ int p2p_join_invite(const char *text)
|
||||
return result > 0;
|
||||
}
|
||||
|
||||
int p2p_join_code(char *text, int size)
|
||||
{
|
||||
int result;
|
||||
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
result = p2p.running && p2p_code_hosted(text, size);
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
int p2p_join_with_code(const char *typed)
|
||||
{
|
||||
int result;
|
||||
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
result = p2p.running && p2p_code_look_up(typed);
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
int p2p_join_code_status(void)
|
||||
{
|
||||
int status;
|
||||
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
status = p2p.running ? p2p_code_status() : P2P_CODE_OFFLINE;
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
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;
|
||||
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
state = p2p.running ? p2p.join_state : P2P_JOIN_NONE;
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
return state;
|
||||
}
|
||||
|
||||
void p2p_join_code_clear_status(void)
|
||||
{
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
p2p_code_clear_status();
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
}
|
||||
|
||||
void p2p_invite_received(const char *text)
|
||||
{
|
||||
/* (an older version's is logged as such) */
|
||||
if (!join_invite(text))
|
||||
/* (an older version's is logged as such; a join code is looked up,
|
||||
p2p_code.c) */
|
||||
if (!join_invite(text) && !p2p_code_look_up(text))
|
||||
platform_log("Internet play: that is not an invite");
|
||||
}
|
||||
|
||||
@@ -2343,6 +2437,8 @@ static void update_joining(void)
|
||||
{
|
||||
p2p.joining = 0;
|
||||
p2p_signal_stop_joining();
|
||||
if (p2p.join_state == P2P_JOIN_REACHING)
|
||||
p2p.join_state = P2P_JOIN_NO_ANSWER;
|
||||
platform_log("Internet play: no answer from the invite's host; it may have stopped hosting or quit");
|
||||
}
|
||||
}
|
||||
@@ -2382,6 +2478,14 @@ static void update_hosting(void)
|
||||
p2p.stun_started = 1;
|
||||
p2p_signal_start();
|
||||
p2p_signal_host(p2p.token);
|
||||
{
|
||||
/* (and a join code for it, p2p_code.c) */
|
||||
unsigned char bytes[P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE];
|
||||
|
||||
p2p_key_hash(p2p_public_key(), bytes);
|
||||
memcpy(bytes + P2P_KEY_HASH_SIZE, p2p.token, P2P_TOKEN_SIZE);
|
||||
p2p_code_host(bytes);
|
||||
}
|
||||
platform_log("Internet play: hosting. Invite players with this link (it only works while this "
|
||||
"copy of the game runs): %s", p2p.invite);
|
||||
if (!p2p.invite_copied)
|
||||
@@ -2396,6 +2500,7 @@ static void update_hosting(void)
|
||||
{
|
||||
p2p.hosting = 0;
|
||||
p2p_signal_stop_hosting();
|
||||
p2p_code_stop_hosting();
|
||||
p2p_discord_set_hosting(NULL, 0, 0);
|
||||
}
|
||||
if (p2p.hosting)
|
||||
|
||||
@@ -26,6 +26,61 @@ int p2p_hand_off_invite(void);
|
||||
words around it. Returns nonzero if it held an invite */
|
||||
int p2p_join_invite(const char *text);
|
||||
|
||||
/* Join codes (p2p_code.c): the hosted game's code as XXXX-XXXX (0 if
|
||||
none); looking a typed one up, which then joins as its invite does (0 if
|
||||
it is not a code, or internet play is off); and how the look up goes */
|
||||
enum
|
||||
{
|
||||
P2P_CODE_IDLE,
|
||||
P2P_CODE_LOOKING,
|
||||
P2P_CODE_FOUND,
|
||||
P2P_CODE_NOT_FOUND,
|
||||
P2P_CODE_INVALID,
|
||||
P2P_CODE_OWN,
|
||||
P2P_CODE_OFFLINE,
|
||||
};
|
||||
int p2p_join_code(char *text, int size);
|
||||
int p2p_join_with_code(const char *typed);
|
||||
int p2p_join_code_status(void);
|
||||
void p2p_join_code_clear_status(void);
|
||||
/* how the latest join of an invite or code goes (the VR port's, for the
|
||||
menus): reaching the host, connected (its game is in the system link list),
|
||||
or why not */
|
||||
enum
|
||||
{
|
||||
P2P_JOIN_NONE,
|
||||
P2P_JOIN_REACHING,
|
||||
P2P_JOIN_CONNECTED,
|
||||
P2P_JOIN_NO_ANSWER,
|
||||
P2P_JOIN_BLOCKED,
|
||||
P2P_JOIN_LOST,
|
||||
};
|
||||
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
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
/*
|
||||
P2P_CODE.C
|
||||
|
||||
Join codes (the VR port's): a short code that stands for a hosted game's
|
||||
invite (p2p.c), for a player who cannot pass a link on, as in a headset,
|
||||
where a link is not copied out and 64 hex digits are not typed in.
|
||||
|
||||
- A host makes a code when it starts hosting: 8 characters of Crockford's
|
||||
base 32 (40 bits), shown as XXXX-XXXX (ui_join_code.c draws it in the
|
||||
lobby). It lasts while the host hosts.
|
||||
- From the code come a topic and a key (PBKDF2-HMAC-SHA256, slow on
|
||||
purpose: CODE_KDF_ITERATIONS). The host listens on the topic through the
|
||||
signalling brokers (p2p_signal.c). A player who types the code asks
|
||||
there (ASK: a nonce), and the host answers with its invite, sealed with
|
||||
the key, with the nonce (INVITE); the asker opens it and joins as an
|
||||
invite link would.
|
||||
- The brokers, and anyone who watches them, see the topic, but cannot tell
|
||||
the code from it, or open the answer, short of working the slow
|
||||
derivation out for codes until one fits; a code is guessed at one in 2^40
|
||||
of them. An asker who has the code has the invite, as one given the link
|
||||
has: it joins as anyone invited does.
|
||||
|
||||
A build without this (another of the same network version) has no codes,
|
||||
and its players join with the link (which the host still logs and copies).
|
||||
*/
|
||||
|
||||
#include "platform.h"
|
||||
#include "posix.h"
|
||||
#include "p2p_internal.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
enum
|
||||
{
|
||||
CODE_SYMBOLS = 8,
|
||||
CODE_KDF_ITERATIONS = 20000,
|
||||
CODE_NONCE_SIZE = 8,
|
||||
CODE_INVITE_SIZE = P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE,
|
||||
CODE_MESSAGE_VERSION = 1,
|
||||
/* milliseconds */
|
||||
ASK_INTERVAL = 1000,
|
||||
LOOK_TIMEOUT = 20000,
|
||||
/* the host's answers: this many at once, and one more each this often */
|
||||
MAXIMUM_ANSWERS = 8,
|
||||
ANSWER_INTERVAL = 250,
|
||||
};
|
||||
|
||||
enum
|
||||
{
|
||||
_code_message_ask = 'Q',
|
||||
_code_message_invite = 'I',
|
||||
};
|
||||
|
||||
/* Crockford's base 32: no I, L, O or U */
|
||||
static const char code_alphabet[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
||||
|
||||
static struct
|
||||
{
|
||||
/* hosting: the code, its topic and key, and the invite it stands for */
|
||||
int hosting;
|
||||
char host_code[CODE_SYMBOLS + 1];
|
||||
char host_topic[P2P_CODE_TOPIC_SIZE];
|
||||
unsigned char host_key[P2P_SHA256_SIZE];
|
||||
unsigned char invite[CODE_INVITE_SIZE];
|
||||
int answers_left;
|
||||
unsigned long answers_time;
|
||||
|
||||
/* looking a code up */
|
||||
int status;
|
||||
char look_topic[P2P_CODE_TOPIC_SIZE];
|
||||
unsigned char look_key[P2P_SHA256_SIZE];
|
||||
unsigned char look_nonce[CODE_NONCE_SIZE];
|
||||
unsigned long look_started_time;
|
||||
unsigned long asked_time;
|
||||
int asked;
|
||||
} code;
|
||||
|
||||
static int code_elapsed(unsigned long since, unsigned long time)
|
||||
{
|
||||
return (unsigned int)(p2p_now() - since) >= (unsigned int)time;
|
||||
}
|
||||
|
||||
/* the code's symbols from what was typed: letters and digits, read as
|
||||
Crockford reads them (O as 0, I and L as 1), the rest (dashes, spaces)
|
||||
left out; 0 unless they are exactly a code's */
|
||||
static int code_normalize(const char *text, char *symbols)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
for (; *text; text++)
|
||||
{
|
||||
char character = *text;
|
||||
|
||||
if (character >= 'a' && character <= 'z')
|
||||
character = (char)(character - 'a' + 'A');
|
||||
if (character == 'O')
|
||||
character = '0';
|
||||
else if (character == 'I' || character == 'L')
|
||||
character = '1';
|
||||
if (!strchr(code_alphabet, character) || !character)
|
||||
{
|
||||
if (character == '-' || character == ' ' || character == '\t' || character == '\r' ||
|
||||
character == '\n')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (count == CODE_SYMBOLS)
|
||||
return 0;
|
||||
symbols[count++] = character;
|
||||
}
|
||||
symbols[count] = 0;
|
||||
return count == CODE_SYMBOLS;
|
||||
}
|
||||
|
||||
/* the topic and key of a code (PBKDF2-HMAC-SHA256, one block) */
|
||||
static void code_derive(const char *symbols, char *topic, unsigned char *key)
|
||||
{
|
||||
static const char salt[] = "halo-ce-vr join code 1";
|
||||
unsigned char block[sizeof(salt) - 1 + 4];
|
||||
unsigned char u[P2P_SHA256_SIZE];
|
||||
unsigned char derived[P2P_SHA256_SIZE];
|
||||
unsigned char digest[P2P_SHA256_SIZE];
|
||||
char hex[2 * P2P_SHA256_SIZE + 1];
|
||||
int iteration;
|
||||
int index;
|
||||
|
||||
memcpy(block, salt, sizeof(salt) - 1);
|
||||
block[sizeof(salt) - 1] = 0;
|
||||
block[sizeof(salt)] = 0;
|
||||
block[sizeof(salt) + 1] = 0;
|
||||
block[sizeof(salt) + 2] = 1;
|
||||
p2p_hmac_sha256((const unsigned char *)symbols, CODE_SYMBOLS, block, sizeof(block), u);
|
||||
memcpy(derived, u, sizeof(derived));
|
||||
for (iteration = 1; iteration < CODE_KDF_ITERATIONS; iteration++)
|
||||
{
|
||||
p2p_hmac_sha256((const unsigned char *)symbols, CODE_SYMBOLS, u, sizeof(u), u);
|
||||
for (index = 0; index < P2P_SHA256_SIZE; index++)
|
||||
derived[index] ^= u[index];
|
||||
}
|
||||
p2p_hmac_sha256(derived, sizeof(derived), "topic", 5, digest);
|
||||
p2p_hex(digest, 16, hex);
|
||||
snprintf(topic, P2P_CODE_TOPIC_SIZE, "hcvr/1/%s", hex);
|
||||
p2p_hmac_sha256(derived, sizeof(derived), "seal", 4, key);
|
||||
}
|
||||
|
||||
/* ---------- p2p.c's side (under p2p_lock) */
|
||||
|
||||
void p2p_code_host(const unsigned char *invite)
|
||||
{
|
||||
unsigned char random[CODE_SYMBOLS];
|
||||
int index;
|
||||
|
||||
posix_random_bytes(random, sizeof(random));
|
||||
for (index = 0; index < CODE_SYMBOLS; index++)
|
||||
code.host_code[index] = code_alphabet[random[index] & 31];
|
||||
code.host_code[CODE_SYMBOLS] = 0;
|
||||
memcpy(code.invite, invite, CODE_INVITE_SIZE);
|
||||
code_derive(code.host_code, code.host_topic, code.host_key);
|
||||
code.answers_left = MAXIMUM_ANSWERS;
|
||||
code.answers_time = p2p_now();
|
||||
code.hosting = 1;
|
||||
platform_log("Internet play: join code %.4s-%.4s", code.host_code, code.host_code + 4);
|
||||
p2p_signal_sync_topics();
|
||||
}
|
||||
|
||||
void p2p_code_stop_hosting(void)
|
||||
{
|
||||
code.hosting = 0;
|
||||
code.host_code[0] = 0;
|
||||
p2p_signal_sync_topics();
|
||||
}
|
||||
|
||||
int p2p_code_hosted(char *text, int size)
|
||||
{
|
||||
if (!code.hosting || size < CODE_SYMBOLS + 2)
|
||||
return 0;
|
||||
snprintf(text, (size_t)size, "%.4s-%.4s", code.host_code, code.host_code + 4);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int p2p_code_look_up(const char *typed)
|
||||
{
|
||||
char symbols[CODE_SYMBOLS + 1];
|
||||
|
||||
if (!code_normalize(typed, symbols))
|
||||
{
|
||||
code.status = P2P_CODE_INVALID;
|
||||
return 0;
|
||||
}
|
||||
if (code.hosting && !strcmp(symbols, code.host_code))
|
||||
{
|
||||
code.status = P2P_CODE_OWN;
|
||||
return 0;
|
||||
}
|
||||
code_derive(symbols, code.look_topic, code.look_key);
|
||||
posix_random_bytes(code.look_nonce, sizeof(code.look_nonce));
|
||||
code.look_started_time = p2p_now();
|
||||
code.asked = 0;
|
||||
code.status = P2P_CODE_LOOKING;
|
||||
p2p_signal_sync_topics();
|
||||
return 1;
|
||||
}
|
||||
|
||||
int p2p_code_status(void)
|
||||
{
|
||||
return code.status;
|
||||
}
|
||||
|
||||
void p2p_code_clear_status(void)
|
||||
{
|
||||
if (code.status != P2P_CODE_LOOKING)
|
||||
code.status = P2P_CODE_IDLE;
|
||||
}
|
||||
|
||||
/* ---------- p2p_signal.c's side (under p2p_lock) */
|
||||
|
||||
void p2p_code_topics(const char **host_topic, const char **look_topic)
|
||||
{
|
||||
*host_topic = code.hosting ? code.host_topic : "";
|
||||
*look_topic = code.status == P2P_CODE_LOOKING ? code.look_topic : "";
|
||||
}
|
||||
|
||||
static void code_answer(const unsigned char *nonce)
|
||||
{
|
||||
unsigned char message[2 + CODE_NONCE_SIZE + CODE_INVITE_SIZE];
|
||||
unsigned char sealed[2 + sizeof(message) + P2P_SEAL_OVERHEAD];
|
||||
int sealed_size;
|
||||
|
||||
/* (anyone watching the brokers can ask, as often as they like) */
|
||||
while (code.answers_left < MAXIMUM_ANSWERS && code_elapsed(code.answers_time, ANSWER_INTERVAL))
|
||||
{
|
||||
code.answers_left++;
|
||||
code.answers_time += ANSWER_INTERVAL;
|
||||
}
|
||||
if (code.answers_left == MAXIMUM_ANSWERS)
|
||||
code.answers_time = p2p_now();
|
||||
if (code.answers_left <= 0)
|
||||
return;
|
||||
code.answers_left--;
|
||||
message[0] = _code_message_invite;
|
||||
message[1] = CODE_MESSAGE_VERSION;
|
||||
memcpy(message + 2, nonce, CODE_NONCE_SIZE);
|
||||
memcpy(message + 2 + CODE_NONCE_SIZE, code.invite, CODE_INVITE_SIZE);
|
||||
sealed[0] = _code_message_invite;
|
||||
sealed[1] = CODE_MESSAGE_VERSION;
|
||||
sealed_size = p2p_seal(code.host_key, message, sizeof(message), sealed + 2);
|
||||
p2p_signal_publish(code.host_topic, sealed, 2 + sealed_size);
|
||||
}
|
||||
|
||||
static void code_invite_received(const unsigned char *payload, int size)
|
||||
{
|
||||
unsigned char message[2 + CODE_NONCE_SIZE + CODE_INVITE_SIZE + 16];
|
||||
int message_size;
|
||||
char hex[2 * CODE_INVITE_SIZE + 1];
|
||||
char link[P2P_LINK_SIZE];
|
||||
|
||||
if (size > (int)sizeof(message) + P2P_SEAL_OVERHEAD)
|
||||
return;
|
||||
message_size = p2p_open(code.look_key, payload, size, message);
|
||||
if (message_size != 2 + CODE_NONCE_SIZE + CODE_INVITE_SIZE || message[0] != _code_message_invite ||
|
||||
message[1] != CODE_MESSAGE_VERSION || !p2p_equal(message + 2, code.look_nonce, CODE_NONCE_SIZE))
|
||||
{
|
||||
return;
|
||||
}
|
||||
code.status = P2P_CODE_FOUND;
|
||||
p2p_signal_sync_topics();
|
||||
p2p_hex(message + 2 + CODE_NONCE_SIZE, CODE_INVITE_SIZE, hex);
|
||||
snprintf(link, sizeof(link), "halo://join/%s", hex);
|
||||
platform_log("Internet play: the join code's host answered");
|
||||
p2p_invite_received(link);
|
||||
}
|
||||
|
||||
int p2p_code_received(const char *topic, const unsigned char *payload, int size)
|
||||
{
|
||||
if (size < 2 || payload[1] != CODE_MESSAGE_VERSION)
|
||||
return 0;
|
||||
if (code.hosting && !strcmp(topic, code.host_topic))
|
||||
{
|
||||
if (payload[0] == _code_message_ask && size == 2 + CODE_NONCE_SIZE)
|
||||
code_answer(payload + 2);
|
||||
return 1;
|
||||
}
|
||||
if (code.status == P2P_CODE_LOOKING && !strcmp(topic, code.look_topic))
|
||||
{
|
||||
if (payload[0] == _code_message_invite)
|
||||
code_invite_received(payload + 2, size - 2);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void p2p_code_update(void)
|
||||
{
|
||||
unsigned char message[2 + CODE_NONCE_SIZE];
|
||||
|
||||
if (code.status != P2P_CODE_LOOKING)
|
||||
return;
|
||||
p2p_signal_start();
|
||||
if (code_elapsed(code.look_started_time, LOOK_TIMEOUT))
|
||||
{
|
||||
code.status = P2P_CODE_NOT_FOUND;
|
||||
p2p_signal_sync_topics();
|
||||
platform_log("Internet play: no game answered the join code");
|
||||
return;
|
||||
}
|
||||
if (!p2p_signal_connected() || (code.asked && !code_elapsed(code.asked_time, ASK_INTERVAL)))
|
||||
return;
|
||||
message[0] = _code_message_ask;
|
||||
message[1] = CODE_MESSAGE_VERSION;
|
||||
memcpy(message + 2, code.look_nonce, CODE_NONCE_SIZE);
|
||||
p2p_signal_publish(code.look_topic, message, sizeof(message));
|
||||
code.asked = 1;
|
||||
code.asked_time = p2p_now();
|
||||
}
|
||||
@@ -107,6 +107,52 @@ void p2p_signal_join(const unsigned char *host_hash, const unsigned char *token)
|
||||
void p2p_signal_stop_joining(void);
|
||||
/* whether any broker is connected */
|
||||
int p2p_signal_connected(void);
|
||||
/* (for p2p_code.c) the topics every broker listens on, as they are now; and
|
||||
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) */
|
||||
|
||||
enum
|
||||
{
|
||||
P2P_CODE_TOPIC_SIZE = 7 + 32 + 1,
|
||||
};
|
||||
|
||||
/* hosting: a new code for the invite (its key hash and token), and none */
|
||||
void p2p_code_host(const unsigned char *invite);
|
||||
void p2p_code_stop_hosting(void);
|
||||
/* the code hosted, as XXXX-XXXX; 0 if none */
|
||||
int p2p_code_hosted(char *text, int size);
|
||||
/* starts looking up a typed code (0: not a code, or this machine's own;
|
||||
p2p_code_status says which) */
|
||||
int p2p_code_look_up(const char *typed);
|
||||
int p2p_code_status(void);
|
||||
/* forgets a finished look up's result */
|
||||
void p2p_code_clear_status(void);
|
||||
/* (p2p_signal.c) the topics to listen on (empty: none); a message on a
|
||||
topic, nonzero if it was a code's; and each pass of the p2p thread */
|
||||
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 */
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,8 @@ enum
|
||||
one is not forgotten while its host nonce lasts: USED_REQUEST_TIME) */
|
||||
MAXIMUM_USED_REQUESTS = 1024,
|
||||
TOPIC_SIZE = 7 + 32 + 1,
|
||||
/* (p2p_code.c's are as long) */
|
||||
TOPIC_SIZE_CHECK = 1 / (TOPIC_SIZE == P2P_CODE_TOPIC_SIZE),
|
||||
NONCE_SIZE = 8,
|
||||
/* an ACCEPT's or a proven JOIN's tag: the first half of an HMAC-SHA256 */
|
||||
TAG_SIZE = 16,
|
||||
@@ -152,6 +154,11 @@ struct broker
|
||||
/* the topics it has been asked for */
|
||||
char host_topic[TOPIC_SIZE];
|
||||
char join_topic[TOPIC_SIZE];
|
||||
/* (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];
|
||||
@@ -321,6 +328,9 @@ static void broker_close(struct broker *broker, int failed)
|
||||
broker->output_size = 0;
|
||||
broker->host_topic[0] = 0;
|
||||
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++;
|
||||
}
|
||||
@@ -410,17 +420,22 @@ 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[2];
|
||||
char *had[2];
|
||||
const char *wanted[5];
|
||||
char *had[5];
|
||||
int index;
|
||||
|
||||
if (broker->state != _broker_ready)
|
||||
return;
|
||||
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;
|
||||
for (index = 0; index < 2; index++)
|
||||
had[2] = broker->code_host_topic;
|
||||
had[3] = broker->code_look_topic;
|
||||
had[4] = broker->list_topic;
|
||||
for (index = 0; index < 5; index++)
|
||||
{
|
||||
if (!strcmp(wanted[index], had[index]))
|
||||
continue;
|
||||
@@ -939,6 +954,8 @@ 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) || p2p_list_received(topic, payload, size))
|
||||
return;
|
||||
if (signalling.hosting && !strcmp(topic, signalling.host_topic))
|
||||
{
|
||||
message_size = p2p_open(signalling.host_key, payload, size, message);
|
||||
@@ -1190,6 +1207,8 @@ 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)
|
||||
@@ -1212,6 +1231,39 @@ static void sync_all_topics(void)
|
||||
broker_sync_topics(&signalling.brokers[index]);
|
||||
}
|
||||
|
||||
void p2p_signal_sync_topics(void)
|
||||
{
|
||||
sync_all_topics();
|
||||
}
|
||||
|
||||
void p2p_signal_publish(const char *topic, const unsigned char *payload, int size)
|
||||
{
|
||||
if (size <= MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD)
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -614,18 +614,56 @@ static BOOL platform_text_has_invite_link(const char *text)
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* the last clipboard text looked at, kept beside the settings from run to
|
||||
run (the VR port's): a run's own invite, still on the clipboard when the
|
||||
game starts again, is not joined then (its host, the run before, is gone,
|
||||
and the attempt held up a join for a minute and a half) */
|
||||
static void platform_clipboard_seen(char *seen, int size, BOOL save)
|
||||
{
|
||||
char path[512];
|
||||
FILE *file;
|
||||
|
||||
snprintf(path, sizeof(path), "%s/clipboard_seen.txt", platform_data_root());
|
||||
if (save)
|
||||
{
|
||||
file = fopen(path, "wb");
|
||||
if (file)
|
||||
{
|
||||
fputs(seen, file);
|
||||
fclose(file);
|
||||
}
|
||||
return;
|
||||
}
|
||||
file = fopen(path, "rb");
|
||||
if (!file)
|
||||
return;
|
||||
{
|
||||
size_t length = fread(seen, 1, (size_t)size - 1, file);
|
||||
|
||||
seen[length] = 0;
|
||||
}
|
||||
fclose(file);
|
||||
}
|
||||
|
||||
/* puts a new invite on the clipboard, and joins one found there when the
|
||||
game comes to the front */
|
||||
static void platform_invite_clipboard(BOOL look)
|
||||
{
|
||||
/* the last clipboard text looked at, so each invite is joined once */
|
||||
static char seen[256];
|
||||
static BOOL seen_loaded;
|
||||
const char *invite = p2p_take_clipboard_text();
|
||||
|
||||
if (!seen_loaded)
|
||||
{
|
||||
seen_loaded = TRUE;
|
||||
platform_clipboard_seen(seen, sizeof(seen), FALSE);
|
||||
}
|
||||
if (invite)
|
||||
{
|
||||
SDL_SetClipboardText(invite);
|
||||
snprintf(seen, sizeof(seen), "%s", invite);
|
||||
platform_clipboard_seen(seen, sizeof(seen), TRUE);
|
||||
platform_log("Internet play: the invite link is on the clipboard");
|
||||
#ifdef HALO_ANDROID
|
||||
SDL_ShowAndroidToast("Hosting: the invite link is on the clipboard", 1, -1, 0, 0);
|
||||
@@ -638,6 +676,7 @@ static void platform_invite_clipboard(BOOL look)
|
||||
if (text && strcmp(text, seen) && strlen(text) < sizeof(seen))
|
||||
{
|
||||
snprintf(seen, sizeof(seen), "%s", text);
|
||||
platform_clipboard_seen(seen, sizeof(seen), TRUE);
|
||||
/* (a link, not a bare code: 64 hex digits alone are as often a
|
||||
checksum copied for something else) */
|
||||
if (platform_text_has_invite_link(text) && p2p_join_invite(text))
|
||||
|
||||
@@ -3771,7 +3771,11 @@ void players_update_before_game(
|
||||
control_data.looking_vector.j = vr_look[1];
|
||||
control_data.looking_vector.k = vr_look[2];
|
||||
}
|
||||
if (halo_vr_aim_vector(player->local_player_index, vr_aim))
|
||||
/* (a VR player of another machine: where their
|
||||
machine says their weapon aims, network_vr.c) */
|
||||
if (player->local_player_index != NONE ?
|
||||
halo_vr_aim_vector(player->local_player_index, vr_aim) :
|
||||
halo_vr_network_aim(iterator.datum_index, vr_aim))
|
||||
{
|
||||
control_data.aiming_vector.i = vr_aim[0];
|
||||
control_data.aiming_vector.j = vr_aim[1];
|
||||
|
||||
@@ -42,7 +42,13 @@ enum
|
||||
_event_type_right_stick,
|
||||
_event_type_button,
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* (the VR port's: a headset's controllers' thumbsticks are pushed as
|
||||
far less readily than a gamepad's) */
|
||||
STICK_EVENT_THRESHOLD = 22937,
|
||||
#else
|
||||
STICK_EVENT_THRESHOLD = 29490,
|
||||
#endif
|
||||
STICK_EVENT_REPEAT_MILLISECONDS = 250
|
||||
};
|
||||
|
||||
@@ -71,6 +77,24 @@ typedef char verify_event_manager_state_size[
|
||||
typedef char verify_event_manager_globals_size[
|
||||
sizeof(struct event_manager_globals) == 0x168 ? 1 : -1];
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* The VR port's: a stick held moves on once after a pause, then quickly,
|
||||
as menus do now (the Xbox's moved on every quarter second, which in a
|
||||
headset, with the keyboard's letters to cross, is slow going). */
|
||||
enum
|
||||
{
|
||||
STICK_REPEAT_FIRST_MILLISECONDS = 350,
|
||||
STICK_REPEAT_MILLISECONDS = 90,
|
||||
};
|
||||
|
||||
static short stick_repeats[NUMBER_OF_GAMEPAD_STICKS][MAXIMUM_GAMEPADS];
|
||||
|
||||
#define STICK_REPEAT_INTERVAL(stick, controller_index) \
|
||||
(stick_repeats[stick][controller_index] ? STICK_REPEAT_MILLISECONDS : STICK_REPEAT_FIRST_MILLISECONDS)
|
||||
#else
|
||||
#define STICK_REPEAT_INTERVAL(stick, controller_index) STICK_EVENT_REPEAT_MILLISECONDS
|
||||
#endif
|
||||
|
||||
/* ---------- prototypes */
|
||||
|
||||
static void queue_event(
|
||||
@@ -83,6 +107,30 @@ struct event_manager_globals event_manager_globals = { 0 };
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* an event the stick posts: a fresh push (it was within the threshold on
|
||||
that axis) starts the count of repeats again */
|
||||
static void stick_note_event(
|
||||
short stick,
|
||||
short controller_index,
|
||||
long x,
|
||||
long y,
|
||||
unsigned long time)
|
||||
{
|
||||
boolean fresh =
|
||||
(ABS(x) >= STICK_EVENT_THRESHOLD &&
|
||||
ABS(event_manager_globals.previous_stick_axes[stick][0][controller_index]) < STICK_EVENT_THRESHOLD) ||
|
||||
(ABS(y) >= STICK_EVENT_THRESHOLD &&
|
||||
ABS(event_manager_globals.previous_stick_axes[stick][1][controller_index]) < STICK_EVENT_THRESHOLD);
|
||||
|
||||
(void)time;
|
||||
if (fresh)
|
||||
stick_repeats[stick][controller_index] = 0;
|
||||
else if (stick_repeats[stick][controller_index] < SHORT_MAX)
|
||||
stick_repeats[stick][controller_index]++;
|
||||
}
|
||||
|
||||
#endif
|
||||
void event_manager_initialize(
|
||||
void)
|
||||
{
|
||||
@@ -209,12 +257,16 @@ static void queue_event(
|
||||
ABS(event_manager_globals.previous_stick_axes[_gamepad_stick_left][0][controller_index]) < STICK_EVENT_THRESHOLD) ||
|
||||
(ABS(y) >= STICK_EVENT_THRESHOLD &&
|
||||
ABS(event_manager_globals.previous_stick_axes[_gamepad_stick_left][1][controller_index]) < STICK_EVENT_THRESHOLD) ||
|
||||
time - event_manager_globals.stick_event_times[_gamepad_stick_left][controller_index] >= STICK_EVENT_REPEAT_MILLISECONDS))
|
||||
time - event_manager_globals.stick_event_times[_gamepad_stick_left][controller_index] >=
|
||||
STICK_REPEAT_INTERVAL(_gamepad_stick_left, controller_index)))
|
||||
{
|
||||
post = FALSE;
|
||||
}
|
||||
else
|
||||
{
|
||||
#ifdef HALO_ANDROID
|
||||
stick_note_event(_gamepad_stick_left, controller_index, x, y, time);
|
||||
#endif
|
||||
event_manager_globals.stick_event_times[_gamepad_stick_left][controller_index] = time;
|
||||
post = TRUE;
|
||||
|
||||
@@ -260,12 +312,16 @@ static void queue_event(
|
||||
ABS(event_manager_globals.previous_stick_axes[_gamepad_stick_right][0][controller_index]) < STICK_EVENT_THRESHOLD) ||
|
||||
(ABS(y) >= STICK_EVENT_THRESHOLD &&
|
||||
ABS(event_manager_globals.previous_stick_axes[_gamepad_stick_right][1][controller_index]) < STICK_EVENT_THRESHOLD) ||
|
||||
time - event_manager_globals.stick_event_times[_gamepad_stick_right][controller_index] >= STICK_EVENT_REPEAT_MILLISECONDS))
|
||||
time - event_manager_globals.stick_event_times[_gamepad_stick_right][controller_index] >=
|
||||
STICK_REPEAT_INTERVAL(_gamepad_stick_right, controller_index)))
|
||||
{
|
||||
post = FALSE;
|
||||
}
|
||||
else
|
||||
{
|
||||
#ifdef HALO_ANDROID
|
||||
stick_note_event(_gamepad_stick_right, controller_index, x, y, time);
|
||||
#endif
|
||||
event_manager_globals.stick_event_times[_gamepad_stick_right][controller_index] = time;
|
||||
post = TRUE;
|
||||
|
||||
|
||||
@@ -5591,6 +5591,543 @@ 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) 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);
|
||||
int p2p_join_code_status(void);
|
||||
void p2p_join_code_clear_status(void);
|
||||
int p2p_join_state(void);
|
||||
|
||||
enum
|
||||
{
|
||||
/* p2p.h's */
|
||||
_join_code_idle,
|
||||
_join_code_looking,
|
||||
_join_code_found,
|
||||
_join_code_not_found,
|
||||
_join_code_invalid,
|
||||
_join_code_own,
|
||||
_join_code_offline,
|
||||
};
|
||||
|
||||
enum
|
||||
{
|
||||
/* p2p.h's */
|
||||
_join_state_none,
|
||||
_join_state_reaching,
|
||||
_join_state_connected,
|
||||
_join_state_no_answer,
|
||||
_join_state_blocked,
|
||||
_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)
|
||||
{
|
||||
return widget_globals.active_widgets[0] && widget_globals.active_widgets[0]->name &&
|
||||
strcmp(widget_globals.active_widgets[0]->name, name) == 0;
|
||||
}
|
||||
|
||||
static void ui_join_code_launch_keyboard(
|
||||
void)
|
||||
{
|
||||
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;
|
||||
}
|
||||
p2p_join_code_clear_status();
|
||||
ui_join_code_globals.keyboard_launched = 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 */
|
||||
static void ui_join_code_update(
|
||||
void)
|
||||
{
|
||||
char text[sizeof(ui_join_code_globals.typed) / sizeof(wchar_t)];
|
||||
short index;
|
||||
|
||||
if (!ui_join_code_globals.keyboard_launched || virtual_keyboard_active())
|
||||
return;
|
||||
ui_join_code_globals.keyboard_launched = FALSE;
|
||||
virtual_keyboard_set_caption(NULL);
|
||||
if (!virtual_keyboard_last_exit_saved_text())
|
||||
return;
|
||||
for (index = 0; index < (short)(NUMBEROF(text) - 1) && ui_join_code_globals.typed[index]; index++)
|
||||
{
|
||||
wchar_t character = ui_join_code_globals.typed[index];
|
||||
|
||||
text[index] = character < 128 ? (char)character : '?';
|
||||
}
|
||||
text[index] = 0;
|
||||
p2p_join_with_code(text);
|
||||
}
|
||||
|
||||
/* 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,
|
||||
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");
|
||||
|
||||
if (font_index == NONE)
|
||||
return;
|
||||
for (index = 0; index < (short)(NUMBEROF(wide) - 1) && text[index]; index++)
|
||||
wide[index] = (wchar_t)(unsigned char)text[index];
|
||||
wide[index] = 0;
|
||||
bounds.y0 = y0;
|
||||
bounds.y1 = (short)(y0 + 20);
|
||||
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)
|
||||
{
|
||||
char code[16];
|
||||
|
||||
if (ui_join_code_screen_active("server_list_screen"))
|
||||
{
|
||||
char const *status = NULL;
|
||||
|
||||
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;
|
||||
case _join_code_found: status = "Found it: the game shows up in the list once its host is reached"; break;
|
||||
case _join_code_not_found: status = "No game answered that code"; break;
|
||||
case _join_code_invalid: status = "That is not a join code (8 letters and digits)"; break;
|
||||
case _join_code_own: status = "That is this game's own code"; break;
|
||||
case _join_code_offline: status = "Online play is off (network.online in config.toml)"; break;
|
||||
}
|
||||
/* (the code found, or a link: how reaching its host goes) */
|
||||
if (!status || p2p_join_code_status() == _join_code_found)
|
||||
{
|
||||
switch (p2p_join_state())
|
||||
{
|
||||
case _join_state_reaching: status = "Reaching the online game's host..."; break;
|
||||
case _join_state_connected: status = "Connected: the online game is in the list above"; break;
|
||||
case _join_state_no_answer: status = "The host did not answer: it may have stopped hosting"; break;
|
||||
case _join_state_blocked: status = "Blocked by the routers: allow UPnP on yours or the host's"; break;
|
||||
case _join_state_lost: status = "Lost the connection to the online game's host"; break;
|
||||
}
|
||||
}
|
||||
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())
|
||||
{
|
||||
char line[128];
|
||||
long bots = config_integer("bots.count");
|
||||
char bots_text[24];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* whether a menu is up (VR shows it on a screen in the room: render.c) */
|
||||
boolean ui_widgets_menus_active(
|
||||
void)
|
||||
@@ -6045,6 +6582,9 @@ void render_ui_widgets(
|
||||
}
|
||||
}
|
||||
}
|
||||
#ifdef HALO_ANDROID
|
||||
ui_join_code_render();
|
||||
#endif
|
||||
if (widget_globals.fade_to_black >= 0.0f &&
|
||||
widget_globals.fade_to_black <= 1.0f)
|
||||
{
|
||||
@@ -6987,6 +7527,9 @@ void process_ui_widgets(
|
||||
|
||||
return;
|
||||
}
|
||||
#ifdef HALO_ANDROID
|
||||
ui_join_code_update();
|
||||
#endif
|
||||
if (attract_mode_should_start())
|
||||
{
|
||||
attract_mode_start();
|
||||
@@ -7088,6 +7631,10 @@ void process_ui_widgets(
|
||||
{
|
||||
do
|
||||
{
|
||||
#ifdef HALO_ANDROID
|
||||
if (ui_join_code_take_event(widget, &event))
|
||||
continue;
|
||||
#endif
|
||||
if (!pause_pressed)
|
||||
{
|
||||
widget_instance_process_one_event_recursive(
|
||||
|
||||
@@ -96,6 +96,18 @@ symbols in this file:
|
||||
#include "text/text_group.h"
|
||||
#include "text/unicode.h"
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* the VR port's: a caption of its own instead of the string list's, for a
|
||||
keyboard it launches (ui_widget.c's join codes); NULL for the list's */
|
||||
static wchar_t const *virtual_keyboard_caption_override;
|
||||
|
||||
void virtual_keyboard_set_caption(
|
||||
wchar_t const *caption)
|
||||
{
|
||||
virtual_keyboard_caption_override = caption;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
enum
|
||||
@@ -671,6 +683,10 @@ static void virtual_keyboard_render_internal(
|
||||
wchar_t *caption = unicode_string_list_get_string(
|
||||
virtual_keyboard_globals.keyboard->special_key_labels_string_list_tag.index,
|
||||
virtual_keyboard_globals.caption_index);
|
||||
#ifdef HALO_ANDROID
|
||||
if (virtual_keyboard_caption_override)
|
||||
caption = (wchar_t *)virtual_keyboard_caption_override;
|
||||
#endif
|
||||
|
||||
bounds.y0 = 78;
|
||||
bounds.x0 = 114;
|
||||
|
||||
@@ -38,6 +38,10 @@ void virtual_keyboard_process(
|
||||
void);
|
||||
void virtual_keyboard_render(
|
||||
void);
|
||||
#ifdef HALO_ANDROID
|
||||
void virtual_keyboard_set_caption(
|
||||
wchar_t const *caption);
|
||||
#endif
|
||||
|
||||
/* ---------- globals */
|
||||
|
||||
|
||||
@@ -2272,8 +2272,12 @@ static void trigger_create_projectiles(
|
||||
projectile is kept inside the level from the camera below,
|
||||
so a hand through a wall does not fire through it */
|
||||
real vr_origin[3];
|
||||
short vr_local_player_index = player_get(player_index)->local_player_index;
|
||||
|
||||
if (halo_vr_weapon_origin(player_get(player_index)->local_player_index, vr_origin))
|
||||
/* (a VR player of another machine: from the hand
|
||||
their machine says, network_vr.c) */
|
||||
if (vr_local_player_index != NONE ? halo_vr_weapon_origin(vr_local_player_index, vr_origin) :
|
||||
halo_vr_network_weapon_origin(player_index, owner_object_index, vr_origin))
|
||||
{
|
||||
origin.x = vr_origin[0];
|
||||
origin.y = vr_origin[1];
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+18
-3
@@ -5732,6 +5732,10 @@ boolean unit_update(
|
||||
unit_throw_grenade_release(unit_index, FALSE);
|
||||
break;
|
||||
}
|
||||
/* (a VR player of another machine: held while their
|
||||
machine says, network_vr.c) */
|
||||
if (halo_vr_network_grenade_held(unit->unit.player_index))
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
if (unit->unit.animation.state!=_unit_state_throw_grenade)
|
||||
@@ -11065,8 +11069,11 @@ static void unit_throw_grenade_release(
|
||||
boolean vr_thrown = FALSE;
|
||||
|
||||
/* VR: the grenade stays in the hand while the trigger holds it */
|
||||
if (vr_local_player_index != NONE && halo_vr_grenade_held(vr_local_player_index))
|
||||
if (vr_local_player_index != NONE ? halo_vr_grenade_held(vr_local_player_index) :
|
||||
halo_vr_network_grenade_held(unit->unit.player_index))
|
||||
{
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (unit->unit.grenade_throw_state==_unit_grenade_throw_in_hand)
|
||||
@@ -11126,8 +11133,10 @@ static void unit_throw_grenade_release(
|
||||
float vr_origin[3], vr_direction[3], vr_strength;
|
||||
|
||||
/* VR: thrown by the hand, from it, its way and as hard */
|
||||
if (vr_local_player_index != NONE &&
|
||||
halo_vr_grenade_throw(vr_local_player_index, vr_origin, vr_direction, &vr_strength))
|
||||
if (vr_local_player_index != NONE ?
|
||||
halo_vr_grenade_throw(vr_local_player_index, vr_origin, vr_direction, &vr_strength) :
|
||||
halo_vr_network_grenade_throw(unit->unit.player_index, unit_index, vr_origin, vr_direction,
|
||||
&vr_strength))
|
||||
{
|
||||
real_point3d origin;
|
||||
real_vector3d direction;
|
||||
@@ -11141,6 +11150,12 @@ static void unit_throw_grenade_release(
|
||||
object_translate(grenade_index, &origin, NULL);
|
||||
scale_vector3d(&direction, scale * vr_strength, &initial_velocity);
|
||||
vr_thrown = TRUE;
|
||||
/* (and the other machines throw it so too) */
|
||||
if (vr_local_player_index != NONE)
|
||||
{
|
||||
halo_vr_network_grenade_thrown(vr_local_player_index, unit_index, vr_origin,
|
||||
vr_direction, vr_strength);
|
||||
}
|
||||
}
|
||||
else
|
||||
#endif
|
||||
|
||||
Reference in new issue
Block a user