Files
astromaddie--HaloCE-VR/port/linux/game/network_distributed.c
T
2026-10-01 13:12:54 +13:00

3747 lines
139 KiB
C

/*
NETWORK_DISTRIBUTED.C
The distributed netcode's own messages (port/linux/NETCODE.md): the game's
"data" message kind (message header type 2, which the Xbox game never sent),
beside its packets, and handled here (network_*_message_handler.c).
The host decides; its clients predict their own players and show the
rest as the host has it:
- The game's objects (units, vehicles, weapons, equipment) are the host's,
at the same datum index on every machine (network_objects.c): the host
says which it has, where they are and what units carry.
- Every tick, a client sends the host its own players' input, each tick's
buttons again with the next three ticks' (a press is lost only with four
datagrams in a row), and the host takes each tick's buttons once. The
host sends every client the other players' input as its tick ran it, the
same way, and the clients drive those players with it
(player_queues_new.c).
- Every tick, a client sends the host where its own players' units are (it
predicts them from its own input); the host takes that as they are,
within a tolerance, as later Halo engines do, at its next tick.
- Every tick, the host sends every client every player's unit: which unit
the player has, alive or not, the seat it rides, its shields and health
(down, recharging, the damage they show), and where it is (dead: who
killed it, which a client announces when its copy dies). A client binds,
kills, seats and places its copies to match (it decides no deaths or
spawns itself), and its own only when far off (a respawn, a teleport).
Players far from a client's own, or out of their sight, are sent to it
(their units and their input) less often, but at once when they come into
sight, and their input every tick while their buttons change.
- Twice a second, and with every kill, the host sends the players'
statistics (kills, deaths, ...) that changed, and once a second a few
more round them all, which clients take as they are; a machine that has
loaded, all of them.
- The host sends the game type's state (the scores, the flags, the balls
and the hill) when it changes (looked at five times a second), once a
second, and to a machine that has loaded, which clients take as it is
(game_engine_write_network_state).
- Damage is the host's: a client reports its own players' hits, which the
host checks and deals, and replays the damage the host deals for its
effects (network_damage.c).
The unreliable messages of a tick to a machine go in as few datagrams (a
batch) as they fill, and each carries its tick (its header's game time):
one that arrives after a newer of its kind is dropped. Vectors travel in 16
bits a part, and a unit's state and a player's input only with the parts
that are not their defaults.
Players are named by their absolute index, which is the same on every
machine (their datum identifiers need not be).
*/
#include "cseries.h"
#include "cseries/errors.h"
#include "game/game.h"
#include "game/game_globals.h"
#include "game/players.h"
#include "game/player_queues_new.h"
#include "networking/network_game_globals.h"
#include "objects/objects.h"
#include "objects/damage.h"
#include "scenario/scenario.h"
#include "structures/structure_bsp_definitions.h"
#include "units/units.h"
#include "units/biped_definitions.h"
#include "units/bipeds.h"
#include "network_distributed.h"
#include <limits.h>
#include <stdio.h>
#include <time.h>
#include <math.h>
/* network_game_globals.c's and network_server_message_handler.c's */
boolean network_distributed_client_send(void *message, word size);
boolean network_distributed_client_send_reliably(void *message, word size);
boolean network_distributed_server_send_to_all_reliably(void *message, word size);
boolean network_distributed_server_send_to_machine(long machine_index, void *message, word size);
boolean network_distributed_server_send_to_machine_reliably(long machine_index, void *message, word size);
short network_distributed_server_machines(long *machine_indices, short maximum);
/* players.c's */
void network_player_attach_unit(long player_index, long unit_index);
void network_player_detach_unit(long player_index);
void network_player_show_pickup(long player_index, short kind, long definition_index, short count);
/* game_engine.c's */
long game_engine_write_network_state(byte *buffer, long size);
void game_engine_read_network_state(byte const *buffer, long size);
/* physics.c's (world units a tick, each tick) */
extern real global_gravity;
/* network_server_manager.c's, cseries_windows.c's, console.c's, p2p.c's */
void network_game_server_kick_machine(long machine_index);
unsigned long network_game_server_machine_address(long machine_index);
char const *network_game_server_machine_hardware_id(long machine_index);
void p2p_hardware_id_sanitize(char *destination, int size, const char *source);
void p2p_discord_sanitize(char *destination, int size, const char *source, int name);
void p2p_discord_identity(char *id, int id_size, char *name, int name_size);
unsigned long p2p_peer_endpoint_address(unsigned long virtual_address);
unsigned long system_milliseconds(void);
void console_warning(const char *format, ...);
/* the host: a client's game run faster than its clock (a speed hack, which
speeds up the machine's own clock: nothing on it can tell), found by its
ticks, which its messages are stamped with, going by faster than the
host's own clock's time, and ahead of the host's (a client's clock never
is: it starts at the host's, and only ever jumps forward to it, when it is
behind; a host that stalled has every client ahead, their ticks going by as
fast as time). Measured over each window this long (milliseconds)... */
#define CLIENT_CLOCK_WINDOW_MILLISECONDS 2000
/* ... faster than this many times as fast as time, and this many ticks
ahead of the host at its end: its players' predictions not taken (the
host's copies go as its own ticks have them) ... */
#define CLIENT_CLOCK_FAST_RATE 1.1f
#define CLIENT_CLOCK_AHEAD_TICKS 15
/* ... and so many windows in a row, dropped */
#define CLIENT_CLOCK_FAST_WINDOWS 5
/* the longest notice's text (_distributed_message_notice) */
#define MAXIMUM_NOTICE_LENGTH 160
/* a Discord user's id and name as kept, with their ends (p2p.h's
P2P_DISCORD_ID_SIZE and P2P_DISCORD_NAME_SIZE) */
#define DISCORD_ID_SIZE 24
#define DISCORD_NAME_SIZE 40
/* where the host logs the players it dropped for cheating, a line each */
#define CHEATERS_FILE "d:\\cheaters.txt"
/* ... and those it bans (by hand, and cheaters): their addresses refused */
#define BANS_FILE "d:\\bans.txt"
/* a client's Discord user, as told (_distributed_message_client_identity) */
struct distributed_client_identity
{
char discord_id[DISCORD_ID_SIZE];
char discord_name[DISCORD_NAME_SIZE];
};
enum
{
STATISTICS_INTERVAL_TICKS = 15,
/* the players' statistics sent unchanged once a second, round them all
(the message is unreliable) */
STATISTICS_REFRESH_TICKS = TICKS_PER_SECOND,
STATISTICS_REFRESH_PLAYERS = 16,
/* the game type's state looked at, and sent unchanged */
GAME_STATE_INTERVAL_TICKS = 6,
/* ... when it changes (reliably: no refresh), no more often than this
(the king's and the ball's scores change every tick), but the game's end
at once */
GAME_STATE_MINIMUM_TICKS = 2 * GAME_STATE_INTERVAL_TICKS,
MAXIMUM_GAME_STATE_SIZE = 0xF00,
MAXIMUM_STATISTICS_PER_MESSAGE = 64,
MAXIMUM_PICKUPS_PER_TICK = 64,
/* ticks a client's own player may ride where the host says it does not
(or the other way round) before it is put where the host has it: its
own prediction reaches the host and comes back in about a round trip */
SEAT_DISAGREEMENT_TICKS = 15,
/* the machines, and the host: a client's messages' sender */
MAXIMUM_SENDERS = HALO_PORT_MAXIMUM_NETWORK_MACHINES + 1,
HOST_SENDER = HALO_PORT_MAXIMUM_NETWORK_MACHINES,
/* a round trip before one is measured, and the longest taken (ticks) */
DEFAULT_ROUND_TRIP_TICKS = 6,
MAXIMUM_ROUND_TRIP_TICKS = 60,
/* ... and beyond it, the ticks a client's own player may ride otherwise
than the host has it */
SEAT_DISAGREEMENT_SLACK_TICKS = 6,
/* a client: where its own players' units were, the last ticks (a power
of two, more than the longest round trip) */
OWN_POSITION_TICKS = 64,
/* the host: the ticks after it took a client's player's position that it
still tells the client which of its ticks it has them at */
PREDICTION_ECHO_TICKS = 3,
/* ... and the client's ticks it takes as run since the anchor beyond
the host's (its messages delayed, then bunched) */
PREDICTION_JITTER_TICKS = 6,
/* ... and the most of a client's round trip it takes a client's player
in the air to be ahead of its copy by (distributed_on_foot_ceiling) */
PREDICTION_CEILING_LEAD_TICKS = 15,
/* ... how long it measures predictions from one it took, the anchor,
before it takes a newer as the anchor (the jitter and the blend granted
once a second, not once a tick) */
PREDICTION_ANCHOR_TICKS = TICKS_PER_SECOND,
/* ... and how long it remembers how fast its own copy of a client's
player went (an anchor's second, and the client's round trip: a client
learns of an explosion that throws its player a round trip late) */
PREDICTION_SPEED_TICKS = PREDICTION_ANCHOR_TICKS + MAXIMUM_ROUND_TRIP_TICKS,
};
/* struct distributed_unit_state flags */
enum
{
/* the player has a unit that is alive */
_distributed_unit_alive_bit = 0,
/* ... and not riding (its position is its own) */
_distributed_unit_placed_bit,
/* (dead) how it died: killed by a teammate, or by an empty vehicle */
_distributed_unit_friendly_fire_bit,
_distributed_unit_killed_by_vehicle_bit,
/* (alive) its shields: down, charging, overcharging */
_distributed_unit_shield_depleted_bit,
_distributed_unit_shield_charging_bit,
_distributed_unit_shield_over_charging_bit,
/* (alive) the host has the unit where the client's own prediction had
it at the tick in predicted_time */
_distributed_unit_predicted_bit,
};
/* struct distributed_unit_state unit flags */
enum
{
/* (alive) camouflaged, and doubly so */
_distributed_unit_camouflaged_bit = 0,
_distributed_unit_super_camouflaged_bit,
};
/* a unit state's parts on the wire: which of those that are not always
there it has (distributed_unit_state_write); one it has not is its default */
enum
{
/* (alive) the vehicle it rides and the seat */
_distributed_unit_part_riding_bit = 0,
/* (alive, placed) its up, when not straight up */
_distributed_unit_part_up_bit,
/* (alive) the damage its shields and body show, when any */
_distributed_unit_part_damage_bit,
/* (alive) how long its powerups have left, when any */
_distributed_unit_part_powerups_bit,
/* (alive) its camouflage, when any */
_distributed_unit_part_camouflage_bit,
/* (dead) who killed it, when anyone */
_distributed_unit_part_killer_bit,
};
/* world units: how far a client's own player's unit may be from the host's
before the host takes it no longer, and before the client is put where the
host has it (no further than that: between the two they would disagree for
good, the host's player somewhere its own is not) */
#define HOST_ACCEPT_TOLERANCE 3.5f
#define REMOTE_CORRECTION_TOLERANCE 0.05f
#define LOCAL_CORRECTION_TOLERANCE 3.0f
/* how far from the origin a unit is (world units), and how fast a client's
own player's unit moves at most (world units a tick) */
#define UNIT_WORLD_BOUND 32768.0f
#define MAXIMUM_PREDICTED_SPEED 2.0f
/* ... on foot: this many times as fast as a player runs and jumps, or as
fast as the host's ticks threw its copy lately, whichever is more; and how
much further a tick than that it may be (world units a tick) */
#define PREDICTED_ON_FOOT_SPEED_SCALE 2.0f
#define PREDICTED_ON_FOOT_SPEED_MARGIN 0.05f
/* ... and how much higher than a jump takes it above where the host last
had it on the ground it may be (world units: steps climbed in the ticks a
prediction is late, where the host and the client have the ground) */
#define PREDICTED_RISE_TOLERANCE 1.0f
/* how far players are from a client's own (world units) before the host
sends them to it (their units and their input) every second tick, every
third, and every fourth; one no cluster of the client's players' can see
(the map's potentially visible set), or dead, every sixth. The set errs on
the side of seeing: a player comes into view in it before any line of sight
does, and is sent at once when they do, as a player whose buttons have
changed in the ticks their input carries is sent every tick. A player a client's player
aims near is sent at least every second tick, and every tick through a
scope (a sniper sees a far player as well as a near one, as Ares does).
(Cosines of the half angles.) */
#define NEAR_PLAYER_DISTANCE 25.0f
#define MIDDLE_PLAYER_DISTANCE 60.0f
#define FAR_PLAYER_DISTANCE 120.0f
#define AIMED_AT_COSINE 0.819f /* 35 degrees: on the screen */
#define SCOPED_AT_COSINE 0.940f /* 20 degrees: in a scope's view, and round it */
enum
{
HIDDEN_PLAYER_PERIOD_TICKS = 6,
};
/* shields, health and the damage they show in 16 bits: 0 to 4 */
#define VITALITY_SCALE 16384.0f
/* a player's unit, as this machine has it (its bytes on the wire are fewer:
distributed_unit_state_write) */
struct distributed_unit_state
{
byte player_index;
byte flags;
/* (dead) the player who killed it, NO_PLAYER for none */
byte killing_player_index;
byte unit_flags;
/* the player's unit (the host's), NONE for none */
long unit_index;
/* the vehicle it rides and its seat, NONE for none */
long vehicle_index;
short seat_index;
/* how camouflaged the unit is, of 255 */
byte active_camouflage;
byte pad;
real_point3d position;
struct distributed_vector velocity;
struct distributed_vector forward;
struct distributed_vector up;
/* (_distributed_unit_predicted_bit) the client's tick, its low 16 bits */
short predicted_time;
/* (VITALITY_SCALE) */
word body_vitality;
word shield_vitality;
/* what the shields' and the HUD's effects show */
word current_body_damage;
word recent_body_damage;
word current_shield_damage;
word recent_shield_damage;
/* the player's powerups: how long each has left */
short powerup_durations[NUMBER_OF_PLAYER_POWERUPS];
};
/* a client's player's input at one of its ticks, with the buttons of the
ticks before it */
struct distributed_player_input
{
byte player_index;
byte pad[3];
/* the client's tick */
long tick;
/* the latest host tick the client has had a message of (the host
measures the round trip by it) */
long host_time;
struct player_action action;
unsigned short control_flags[DISTRIBUTED_INPUT_HISTORY];
};
typedef char distributed_player_input_size_assert[
sizeof(struct distributed_player_input) == 12 + 0x20 + 2 * DISTRIBUTED_INPUT_HISTORY ? 1 : -1];
typedef char distributed_player_input_action_offset_assert[
offsetof(struct distributed_player_input, action) == 12 ? 1 : -1];
/* a player's input as the host ran it, as this machine has it: the host's
update (the same for every player of a tick) goes once a message, and on
the wire the ticks' buttons before the latest only where they differ from
the tick after (distributed_relayed_action_write) */
struct distributed_relayed_action
{
byte player_index;
signed char desired_weapon_index;
signed char desired_grenade_index;
signed char desired_zoom_level;
unsigned short control_flags[DISTRIBUTED_INPUT_HISTORY];
/* of a turn */
short yaw;
short pitch;
/* of 127, and 255 */
signed char throttle_i;
signed char throttle_j;
byte primary_trigger;
};
/* a relayed action's parts on the wire: which of the ticks' buttons before
the latest it has (bit h - 1 for tick h), the others those of the tick
after */
typedef char distributed_relayed_action_parts_assert[DISTRIBUTED_INPUT_HISTORY - 1 <= 8 ? 1 : -1];
enum
{
/* a unit state's bytes on the wire: its player, flags and parts, the
most it has, and a relayed action's (past the update each message has
first) */
DISTRIBUTED_UNIT_STATE_MINIMUM_SIZE = 3,
DISTRIBUTED_UNIT_STATE_MAXIMUM_SIZE = 3 + 4 + (4 + 2) + (12 + 3 * 6) + 2 + 2 * 2 + 4 * 2 +
2 * NUMBER_OF_PLAYER_POWERUPS + 2 + 1,
DISTRIBUTED_RELAYED_ACTION_MINIMUM_SIZE = 14,
DISTRIBUTED_RELAYED_ACTION_MAXIMUM_SIZE = 14 + 2 * (DISTRIBUTED_INPUT_HISTORY - 1),
};
struct distributed_pickup
{
byte player_index;
byte kind;
short count;
long definition_index;
};
typedef char distributed_pickup_size_assert[sizeof(struct distributed_pickup) == 8 ? 1 : -1];
struct distributed_player_statistics
{
short player_index;
short pad;
struct game_statistics statistics;
};
typedef char distributed_player_statistics_size_assert[
sizeof(struct distributed_player_statistics) == 4 + sizeof(struct game_statistics) ? 1 : -1];
struct distributed_statistics_message
{
struct distributed_message_header header;
struct distributed_player_statistics players[MAXIMUM_STATISTICS_PER_MESSAGE];
};
/* a batch's datagram: its header, then each message's size and its bytes
past its message header */
struct distributed_batch
{
word size;
byte data[DATAGRAM_MAXIMUM_SIZE];
};
/* what each message in a batch takes besides its entries: its size, and
its header but for its message header */
#define BATCH_MESSAGE_OVERHEAD \
((word)(sizeof(word) + sizeof(struct distributed_message_header) - sizeof(message_header)))
/* messages of entries of a size of their own, to one machine (or the host),
each as long as the batch has room for (distributed_packer_add): nothing
else goes into that batch while it packs */
struct distributed_packer
{
short sender;
byte type;
short count;
/* the bytes every message has first (the relayed actions' update), and
the bytes past the header so far, of room */
word prefix_size;
word size;
word room;
struct
{
struct distributed_message_header header;
byte data[DATAGRAM_MAXIMUM_SIZE];
} message;
};
/* ---------- globals */
static long distributed_last_sent_time = NONE;
/* how each player last died, by absolute index: the host's own, which it
sends its clients, and a client's copy of the host's */
static struct distributed_death
{
boolean valid;
/* the killer's absolute index, or NONE */
short killing_player_index;
boolean friendly_fire;
boolean killed_by_vehicle;
} distributed_deaths[MAXIMUM_TRACKED_PLAYERS];
/* the host: a kill this tick, whose statistics the clients should have
with it */
static boolean distributed_statistics_due;
/* the host: what players on other machines picked up this tick */
static struct distributed_pickup distributed_pickups[MAXIMUM_PICKUPS_PER_TICK];
static short distributed_pickup_count;
/* a client: the ticks each of its own players has ridden other than as
the host has it */
static short distributed_seat_disagreements[MAXIMUM_TRACKED_PLAYERS];
/* the host: each client's player's latest prediction, taken at the next
tick, and the client's tick of the one taken last and when */
static struct
{
boolean valid;
long time;
struct distributed_unit_state state;
long taken_time;
long taken_host_time;
} distributed_predictions[MAXIMUM_TRACKED_PLAYERS];
/* the host: the anchor, where it took each client's player to be (one
taken, a newer once a second: the next may be no further from it than the
player moves in the client's ticks since), at which of the client's ticks
and its own; and when it last took one, and where that left its unit */
static struct
{
boolean valid;
long unit_index;
long time;
long host_time;
real_point3d position;
long taken_host_time;
real_point3d taken_host_position;
} distributed_accepted[MAXIMUM_TRACKED_PLAYERS];
/* ... what its own ticks did to its copy of each client's player's unit on
foot, beyond the client's word it took (an explosion's throw, a jump): how
fast they sent it, and how fast up, the most of this span of
PREDICTION_SPEED_TICKS and of the one before; the unit (NONE: none on
foot), the velocity it took (what its next tick started from) and whether
the unit was on the ground then; where it last had the unit on the ground
(or where it started: a new life, a ride's end, a teleporter) and the
highest it has had it since; and those speeds kept through a flight begun
while it had them (a throw's flight outlasting the span) */
static struct distributed_host_speed
{
long time;
real speeds[2];
real rises[2];
real flight_speed;
real flight_rise;
long unit_index;
real_vector3d velocity;
boolean grounded;
real ground_height;
real top_height;
/* the last tick it had the unit on the ground (or where it started) */
long ground_time;
} distributed_host_speeds[MAXIMUM_TRACKED_PLAYERS];
/* a client: where each of its own players' units was at its last ticks,
and how long the host takes to have them (ticks) */
static struct distributed_own_position
{
long time;
long unit_index;
real_point3d position;
} distributed_own_positions[MAXIMUM_LOCAL_PLAYERS][OWN_POSITION_TICKS];
static real distributed_own_round_trip;
/* the host: the players each machine had when it last told the host that it
had loaded (a machine new at its index has none of the old one's state) */
static long distributed_machine_players[HALO_PORT_MAXIMUM_NETWORK_MACHINES][MAXIMUM_LOCAL_PLAYERS];
/* the host: what each player's unit was last sent as (a change goes to
every client at once) */
static struct
{
byte flags;
long unit_index;
long vehicle_index;
short seat_index;
} distributed_sent_units[MAXIMUM_TRACKED_PLAYERS];
/* the host: whether each client's players could see each player last tick
(one who comes into sight is sent at once) */
static boolean distributed_seen[HALO_PORT_MAXIMUM_NETWORK_MACHINES][MAXIMUM_TRACKED_PLAYERS];
/* the host: where each client's players were when last alive (a client
whose players are dead watches from there) */
static struct distributed_viewer
{
boolean valid;
short cluster_index;
real_point3d position;
real_vector3d aim;
} distributed_viewers[HALO_PORT_MAXIMUM_NETWORK_MACHINES][MAXIMUM_LOCAL_PLAYERS];
/* the host, this tick: every player's unit and input, what each client is
sent of each player (decided before the damage, which goes where the
players it hurt do), and the client each player is on (NONE for none: the
host's own) */
enum
{
_distributed_send_state_bit = 0,
_distributed_send_action_bit,
/* the client's players can see the player (whether its state goes this
tick or not): what hurts the player it is told of */
_distributed_send_sees_bit,
};
static struct distributed_unit_state distributed_host_states[MAXIMUM_TRACKED_PLAYERS];
static struct distributed_relayed_action distributed_host_actions[MAXIMUM_TRACKED_PLAYERS];
static long distributed_host_update_number;
static byte distributed_sends[HALO_PORT_MAXIMUM_NETWORK_MACHINES][MAXIMUM_TRACKED_PLAYERS];
static long distributed_player_machines[MAXIMUM_TRACKED_PLAYERS];
/* the host: each player's statistics as last sent (only a change is sent,
and once a second a few players' whatever they are, round them all) */
static unsigned long distributed_sent_statistics[MAXIMUM_TRACKED_PLAYERS];
static short distributed_statistics_cursor;
/* the host: the game type's state as last sent (its checksum and its
leading postgame state), and when */
static unsigned long distributed_game_state_checksum;
static long distributed_game_state_postgame;
static long distributed_game_state_time;
/* the latest tick of each kind of unreliable message had from each sender
(a machine, or the host), NONE for none */
static long distributed_received_times[MAXIMUM_SENDERS][NUMBER_OF_DISTRIBUTED_MESSAGES];
/* the host: each client machine's clock, measured (CLIENT_CLOCK_WINDOW_MILLISECONDS):
its latest tick, and its tick and the host's time as the window began
(NONE: none begun); how many windows in a row it went fast, and whether
the last did */
/* the host: each client machine's Discord user, as it told it, the text
kept only of what is allowed (p2p_discord_sanitize) */
static struct distributed_client_identity distributed_client_identities[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
/* a client: its Discord user as last sent this game, and whether it was */
static struct distributed_client_identity distributed_sent_identity;
static boolean distributed_identity_sent;
static struct distributed_client_clock
{
long latest_tick;
long window_tick;
unsigned long window_time;
short fast_windows;
boolean fast;
} distributed_client_clocks[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
/* a client: the host's latest tick it has had a message of */
static long distributed_host_time = NONE;
/* the host: each client's round trip, in ticks, and its jitter */
static struct
{
boolean valid;
real average;
real deviation;
} distributed_round_trips[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
/* the host: its clients' machines, found once a tick */
static struct
{
boolean in_tick;
boolean valid;
short count;
long indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
} distributed_machines;
/* the unreliable messages of this tick, a batch for each machine and one
for the host */
static struct distributed_batch distributed_batches[MAXIMUM_SENDERS];
/* (the largest messages, packed a machine at a time) */
static struct distributed_packer distributed_packer;
/* for the automated tests' reports (network_test.c) */
static struct
{
long sent;
long received;
long corrections;
} distributed_statistics;
/* ---------- shared (network_distributed.h) */
void network_distributed_statistics(
long *sent,
long *received,
long *corrections)
{
*sent = distributed_statistics.sent;
*received = distributed_statistics.received;
*corrections = distributed_statistics.corrections;
}
void distributed_count_correction(
void)
{
distributed_statistics.corrections++;
}
boolean distributed_real_valid(
real value)
{
/* (NaN and the infinities less themselves are not 0) */
return value - value == 0.0f;
}
boolean distributed_point_valid(
real_point3d const *point,
real bound)
{
return fabsf(point->x) <= bound && fabsf(point->y) <= bound && fabsf(point->z) <= bound;
}
boolean distributed_object_index_valid(
long object_index)
{
return object_index != NONE && DATUM_INDEX_TO_IDENTIFIER(object_index) != 0;
}
boolean distributed_axes_make_valid(
real_vector3d *forward,
real_vector3d *up)
{
real forward_length = (real)sqrt(forward->i * forward->i + forward->j * forward->j + forward->k * forward->k);
real up_length = (real)sqrt(up->i * up->i + up->j * up->j + up->k * up->k);
real dot;
real length;
if (!(forward_length > 0.5f && forward_length < 1.5f && up_length > 0.5f && up_length < 1.5f))
return FALSE;
forward->i /= forward_length;
forward->j /= forward_length;
forward->k /= forward_length;
dot = (forward->i * up->i + forward->j * up->j + forward->k * up->k) / up_length;
if (!(fabsf(dot) < 0.1f))
return FALSE;
/* (up square to forward) */
up->i -= dot * up_length * forward->i;
up->j -= dot * up_length * forward->j;
up->k -= dot * up_length * forward->k;
length = (real)sqrt(up->i * up->i + up->j * up->j + up->k * up->k);
up->i /= length;
up->j /= length;
up->k /= length;
return TRUE;
}
void distributed_vector_pack(
real_vector3d const *vector,
real scale,
struct distributed_vector *result)
{
real parts[3];
short index;
parts[0] = vector->i;
parts[1] = vector->j;
parts[2] = vector->k;
for (index = 0; index < 3; index++)
{
real value = parts[index] * scale;
value = value > 32767.0f ? 32767.0f : value < -32767.0f ? -32767.0f : value;
parts[index] = (real)floor(value + 0.5f);
}
result->i = (short)parts[0];
result->j = (short)parts[1];
result->k = (short)parts[2];
}
void distributed_vector_unpack(
struct distributed_vector const *vector,
real scale,
real_vector3d *result)
{
result->i = (real)vector->i / scale;
result->j = (real)vector->j / scale;
result->k = (real)vector->k / scale;
}
void distributed_unit_vector_unpack(
struct distributed_vector const *vector,
real_vector3d *result)
{
real length;
distributed_vector_unpack(vector, DISTRIBUTED_UNIT_SCALE, result);
length = (real)sqrt(result->i * result->i + result->j * result->j + result->k * result->k);
if (length > 0.0f)
{
result->i /= length;
result->j /= length;
result->k /= length;
}
}
static word distributed_vitality_pack(
real value)
{
value *= VITALITY_SCALE;
value = value > 65535.0f ? 65535.0f : value < 0.0f ? 0.0f : value;
return (word)(long)floor(value + 0.5f);
}
static real distributed_vitality_unpack(
word value)
{
return (real)value / VITALITY_SCALE;
}
/* an angle as a 16-bit fraction of a turn, and back (yaw from 0 to 2 pi,
pitch from -pi to pi) */
static short distributed_angle_pack(
real angle)
{
real turns = angle / (2.0f * _pi);
turns -= (real)floor(turns);
return (short)(word)((long)floor(turns * 65536.0f + 0.5f) & 0xFFFF);
}
static real distributed_angle_unpack(
short value,
boolean signed_angle)
{
real angle = (real)(word)value * (2.0f * _pi) / 65536.0f;
if (signed_angle && angle >= _pi)
angle -= 2.0f * _pi;
return angle;
}
static void distributed_batch_flush(
short sender)
{
struct distributed_batch *batch = &distributed_batches[sender];
struct distributed_message_header *header = (struct distributed_message_header *)batch->data;
if (!batch->size)
return;
header->type = _distributed_message_batch;
header->count = 0;
header->game_time = game_time_get();
header->header = 0;
build_message_header(&header->header, batch->size, 2, 0);
if (sender == HOST_SENDER)
network_distributed_client_send(batch->data, batch->size);
else
network_distributed_server_send_to_machine(sender, batch->data, batch->size);
batch->size = 0;
}
/* the bytes a message's entries (past its header) have in the machine's
batch now; when fewer than minimum, the batch is sent first, and they are
an empty batch's */
static word distributed_batch_room(
short sender,
word minimum)
{
struct distributed_batch *batch = &distributed_batches[sender];
word used = batch->size ? batch->size : (word)sizeof(struct distributed_message_header);
if (used + BATCH_MESSAGE_OVERHEAD + minimum > DATAGRAM_MAXIMUM_SIZE)
{
distributed_batch_flush(sender);
used = sizeof(struct distributed_message_header);
}
return (word)(DATAGRAM_MAXIMUM_SIZE - used - BATCH_MESSAGE_OVERHEAD);
}
/* a message's header and entries into the machine's batch, which has room
for them */
static void distributed_batch_append(
short sender,
struct distributed_message_header const *header,
byte const *entries,
word entries_size)
{
struct distributed_batch *batch = &distributed_batches[sender];
word length = (word)(sizeof(*header) - sizeof(message_header) + entries_size);
if (!batch->size)
batch->size = sizeof(struct distributed_message_header);
csmemcpy(batch->data + batch->size, &length, sizeof(word));
csmemcpy(batch->data + batch->size + sizeof(word), (byte const *)header + sizeof(message_header),
sizeof(*header) - sizeof(message_header));
csmemcpy(batch->data + batch->size + BATCH_MESSAGE_OVERHEAD, entries, entries_size);
batch->size += (word)(BATCH_MESSAGE_OVERHEAD + entries_size);
}
/* where a message's bytes past its message header go: a machine's batch,
sent at the tick's end (or sooner, full). A message of entries of
entry_size bytes each is split where a batch is full, the rest going in
the next; one not to be split (entry_size 0) goes whole, and one larger
than a datagram, which is a mistake, is dropped. */
static void distributed_batch_add(
short sender,
void const *message,
word size,
word entry_size)
{
struct distributed_message_header header;
byte const *entries = (byte const *)message + sizeof(header);
word entries_size;
short count;
if (size < sizeof(header))
return;
csmemcpy(&header, message, sizeof(header));
entries_size = (word)(size - sizeof(header));
count = header.count;
if (!entry_size || !count || entries_size != count * entry_size)
{
if (distributed_batch_room(sender, entries_size) < entries_size)
{
error(_error_silent, "distributed message of type #%d is #%d bytes, too large for a datagram; dropped",
header.type, size);
return;
}
distributed_batch_append(sender, &header, entries, entries_size);
return;
}
while (count > 0)
{
short fit = (short)MIN(count, distributed_batch_room(sender, entry_size) / entry_size);
if (fit <= 0)
{
error(_error_silent,
"distributed message of type #%d has entries of #%d bytes, too large for a datagram; dropped",
header.type, entry_size);
return;
}
header.count = (byte)fit;
distributed_batch_append(sender, &header, entries, (word)(fit * entry_size));
entries += fit * entry_size;
count -= fit;
}
}
/* (the machines' datagrams in a turn that starts one further each tick:
none always last) */
static void distributed_batches_flush(
void)
{
short first = (short)((unsigned long)game_time_get() % MAXIMUM_SENDERS);
short step;
for (step = 0; step < MAXIMUM_SENDERS; step++)
distributed_batch_flush((short)((first + step) % MAXIMUM_SENDERS));
}
static void distributed_fill_header(
void *message,
byte type,
short count,
word size)
{
struct distributed_message_header *header = (struct distributed_message_header *)message;
header->type = type;
header->count = (byte)count;
header->game_time = game_time_get();
header->header = 0;
build_message_header(&header->header, size, 2, 0);
distributed_statistics.sent++;
}
/* the size of each of a message's entries, when they are alike, else 0 */
static word distributed_entry_size(
short count,
word size)
{
word entries_size = (word)(size - sizeof(struct distributed_message_header));
if (count <= 0 || size <= sizeof(struct distributed_message_header) || entries_size % count)
return 0;
return (word)(entries_size / count);
}
void distributed_send(
void *message,
byte type,
short count,
word size,
short destination)
{
distributed_fill_header(message, type, count, size);
switch (destination)
{
case _distributed_to_clients:
{
long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
short machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES);
short index;
for (index = 0; index < machine_count; index++)
distributed_batch_add((short)machine_indices[index], message, size, distributed_entry_size(count, size));
break;
}
case _distributed_to_clients_reliably: network_distributed_server_send_to_all_reliably(message, size); break;
case _distributed_to_host:
distributed_batch_add(HOST_SENDER, message, size, distributed_entry_size(count, size));
break;
case _distributed_to_host_reliably: network_distributed_client_send_reliably(message, size); break;
}
}
void distributed_send_to_machine(
long machine_index,
void *message,
byte type,
short count,
word size)
{
if (machine_index < 0 || machine_index >= HALO_PORT_MAXIMUM_NETWORK_MACHINES)
return;
distributed_fill_header(message, type, count, size);
distributed_batch_add((short)machine_index, message, size, distributed_entry_size(count, size));
}
void distributed_send_to_machine_reliably(
long machine_index,
void *message,
byte type,
short count,
word size)
{
distributed_fill_header(message, type, count, size);
network_distributed_server_send_to_machine_reliably(machine_index, message, size);
}
/* a packer's messages of the type, to the machine (or the host), each
beginning with the prefix */
static void distributed_packer_begin(
struct distributed_packer *packer,
short sender,
byte type,
void const *prefix,
word prefix_size)
{
packer->sender = sender;
packer->type = type;
packer->count = 0;
packer->prefix_size = prefix_size;
packer->size = prefix_size;
packer->room = 0;
if (prefix_size)
csmemcpy(packer->message.data, prefix, prefix_size);
}
/* the message packed so far, into the batch */
static void distributed_packer_flush(
struct distributed_packer *packer)
{
word size = (word)(sizeof(packer->message.header) + packer->size);
if (!packer->count)
return;
distributed_fill_header(&packer->message, packer->type, packer->count, size);
distributed_batch_add(packer->sender, &packer->message, size, 0);
packer->count = 0;
packer->size = packer->prefix_size;
}
/* an entry into the message, which goes into the batch first when the
entry would not fit it (and a batch full goes first then) */
static void distributed_packer_add(
struct distributed_packer *packer,
void const *entry,
word entry_size)
{
if (packer->count && (packer->size + entry_size > packer->room || packer->count >= 255))
distributed_packer_flush(packer);
if (!packer->count)
packer->room = MIN(distributed_batch_room(packer->sender, (word)(packer->prefix_size + entry_size)),
(word)sizeof(packer->message.data));
if (packer->size + entry_size > packer->room)
return;
csmemcpy(packer->message.data + packer->size, entry, entry_size);
packer->size += entry_size;
packer->count++;
}
/* the bytes of what a message says, in order: written, and read no further
than its end (FALSE past it) */
static byte *distributed_put(
byte *cursor,
void const *data,
short size)
{
csmemcpy(cursor, data, size);
return cursor + size;
}
static boolean distributed_take(
byte const **cursor,
byte const *end,
void *data,
short size)
{
if (end - *cursor < size)
return FALSE;
csmemcpy(data, *cursor, size);
*cursor += size;
return TRUE;
}
struct player_datum *distributed_player(
short player_index)
{
struct player_datum *player;
if (player_index < 0 || player_index >= player_data->maximum_count)
return NULL;
player = (struct player_datum *)((byte *)player_data->data + player_index * player_data->size);
return player->identifier ? player : NULL;
}
byte distributed_player_to_byte(
long player_index)
{
return player_index != NONE ? (byte)DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) : NO_PLAYER;
}
long distributed_player_from_byte(
byte player_index)
{
struct player_datum *player = player_index != NO_PLAYER ? distributed_player(player_index) : NULL;
return player ? DATUM_INDEX_NEW(player_index, player->identifier) : NONE;
}
boolean distributed_player_is_local(
long player_index)
{
struct player_datum *player = player_index != NONE ? player_try_and_get(player_index) : NULL;
return player && player->local_player_index != NONE;
}
long distributed_living_unit(
struct player_datum const *player)
{
if (!player || player->unit_index == NONE || !object_try_and_get(player->unit_index) ||
TEST_FLAG(object_get(player->unit_index)->object.damage_flags, _object_dead_bit))
{
return NONE;
}
return player->unit_index;
}
boolean distributed_machine_has_player(
long machine_index,
short player_index)
{
long *player_list = machine_get_player_list(machine_index);
short local_player_index;
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
{
if (player_list[local_player_index] != NONE &&
DATUM_INDEX_TO_ABSOLUTE_INDEX(player_list[local_player_index]) == player_index)
{
return TRUE;
}
}
return FALSE;
}
/* whether the machine's players are this machine's (the host's own machine
is in the game as its clients are) */
static boolean distributed_machine_is_local(
long machine_index)
{
long *player_list = machine_get_player_list(machine_index);
short local_player_index;
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
{
struct player_datum *player = player_list[local_player_index] != NONE ?
player_try_and_get(player_list[local_player_index]) : NULL;
if (player && player->local_player_index != NONE)
return TRUE;
}
return FALSE;
}
short distributed_client_machines(
long *machine_indices,
short maximum)
{
short index;
/* (found once a tick: the machines do not change during one) */
if (!distributed_machines.valid)
{
short count = network_distributed_server_machines(distributed_machines.indices,
HALO_PORT_MAXIMUM_NETWORK_MACHINES);
distributed_machines.count = 0;
for (index = 0; index < count; index++)
{
if (distributed_machines.indices[index] >= 0 &&
distributed_machines.indices[index] < HALO_PORT_MAXIMUM_NETWORK_MACHINES &&
!distributed_machine_is_local(distributed_machines.indices[index]))
{
distributed_machines.indices[distributed_machines.count++] = distributed_machines.indices[index];
}
}
distributed_machines.valid = distributed_machines.in_tick;
}
for (index = 0; index < distributed_machines.count && index < maximum; index++)
machine_indices[index] = distributed_machines.indices[index];
return index;
}
/* (the host) a machine has loaded the game: if it is new at its index (its
players are not those the index had), none of the old machine's state, and
TRUE (it has none of the game's either); FALSE for one that asks again */
static boolean distributed_machine_loaded(
long machine_index)
{
long *player_list;
short type;
short player_index;
if (machine_index < 0 || machine_index >= HALO_PORT_MAXIMUM_NETWORK_MACHINES)
return FALSE;
player_list = machine_get_player_list(machine_index);
if (!csmemcmp(distributed_machine_players[machine_index], player_list,
sizeof(distributed_machine_players[machine_index])))
{
return FALSE;
}
csmemcpy(distributed_machine_players[machine_index], player_list,
sizeof(distributed_machine_players[machine_index]));
for (type = 0; type < NUMBER_OF_DISTRIBUTED_MESSAGES; type++)
distributed_received_times[machine_index][type] = NONE;
csmemset(&distributed_round_trips[machine_index], 0, sizeof(distributed_round_trips[machine_index]));
csmemset(distributed_viewers[machine_index], 0, sizeof(distributed_viewers[machine_index]));
csmemset(&distributed_client_clocks[machine_index], 0, sizeof(distributed_client_clocks[machine_index]));
distributed_client_clocks[machine_index].window_tick = NONE;
csmemset(&distributed_client_identities[machine_index], 0, sizeof(distributed_client_identities[machine_index]));
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++)
distributed_seen[machine_index][player_index] = FALSE;
return TRUE;
}
long distributed_latest_host_time(
void)
{
return distributed_host_time;
}
real distributed_own_round_trip_ticks(
void)
{
return distributed_own_round_trip;
}
long distributed_player_machine(
short player_index)
{
return player_index >= 0 && player_index < MAXIMUM_TRACKED_PLAYERS ?
distributed_player_machines[player_index] : NONE;
}
boolean distributed_machine_sees_player(
long machine_index,
short player_index)
{
return machine_index >= 0 && machine_index < HALO_PORT_MAXIMUM_NETWORK_MACHINES &&
player_index >= 0 && player_index < MAXIMUM_TRACKED_PLAYERS &&
TEST_FLAG(distributed_sends[machine_index][player_index], _distributed_send_sees_bit);
}
real distributed_machine_round_trip_ticks(
long machine_index)
{
if (machine_index < 0 || machine_index >= HALO_PORT_MAXIMUM_NETWORK_MACHINES ||
!distributed_round_trips[machine_index].valid)
{
return (real)DEFAULT_ROUND_TRIP_TICKS;
}
return distributed_round_trips[machine_index].average + 2.0f * distributed_round_trips[machine_index].deviation;
}
/* ---------- units */
static void distributed_state_from_player(
short player_index,
struct distributed_unit_state *state)
{
struct player_datum *player = distributed_player(player_index);
long unit_index = distributed_living_unit(player);
csmemset(state, 0, sizeof(*state));
state->player_index = (byte)player_index;
state->unit_index = NONE;
state->vehicle_index = NONE;
state->seat_index = NONE;
if (unit_index != NONE)
{
struct unit_datum *unit = unit_get(unit_index);
struct damage_network_state damage;
real camouflage = unit->unit.active_camouflage;
state->unit_index = unit_index;
SET_FLAG(state->flags, _distributed_unit_alive_bit, TRUE);
SET_FLAG(state->flags, _distributed_unit_placed_bit, unit->object.parent_object_index == NONE);
if (unit->object.parent_object_index != NONE && unit->unit.parent_seat_index != NONE)
{
state->vehicle_index = unit->object.parent_object_index;
state->seat_index = unit->unit.parent_seat_index;
}
state->position = unit->object.position;
distributed_vector_pack(&unit->object.translational_velocity, DISTRIBUTED_VELOCITY_SCALE, &state->velocity);
distributed_vector_pack(&unit->object.forward, DISTRIBUTED_UNIT_SCALE, &state->forward);
distributed_vector_pack(&unit->object.up, DISTRIBUTED_UNIT_SCALE, &state->up);
damage_get_network_state(unit_index, &damage);
SET_FLAG(state->flags, _distributed_unit_shield_depleted_bit, damage.shield_depleted);
SET_FLAG(state->flags, _distributed_unit_shield_charging_bit, damage.shield_charging);
SET_FLAG(state->flags, _distributed_unit_shield_over_charging_bit, damage.shield_over_charging);
state->body_vitality = distributed_vitality_pack(damage.body_vitality);
state->shield_vitality = distributed_vitality_pack(damage.shield_vitality);
state->current_body_damage = distributed_vitality_pack(damage.current_body_damage);
state->recent_body_damage = distributed_vitality_pack(damage.recent_body_damage);
state->current_shield_damage = distributed_vitality_pack(damage.current_shield_damage);
state->recent_shield_damage = distributed_vitality_pack(damage.recent_shield_damage);
csmemcpy(state->powerup_durations, player->powerup_durations, sizeof(state->powerup_durations));
SET_FLAG(state->unit_flags, _distributed_unit_camouflaged_bit,
TEST_FLAG(unit->unit.flags, _unit_active_camouflaged_bit));
SET_FLAG(state->unit_flags, _distributed_unit_super_camouflaged_bit,
TEST_FLAG(unit->unit.flags, _unit_super_camouflaged_bit));
camouflage = camouflage > 1.0f ? 1.0f : camouflage < 0.0f ? 0.0f : camouflage;
state->active_camouflage = (byte)(long)floor(camouflage * 255.0f + 0.5f);
/* (the host) a client's player where the client had them: at which of
its ticks (with the ticks run since) */
if (game_connection() == _game_connection_network_server && player_index < MAXIMUM_TRACKED_PLAYERS &&
distributed_predictions[player_index].taken_host_time != NONE &&
game_time_get() - distributed_predictions[player_index].taken_host_time < PREDICTION_ECHO_TICKS)
{
SET_FLAG(state->flags, _distributed_unit_predicted_bit, TRUE);
state->predicted_time = (short)(word)(distributed_predictions[player_index].taken_time +
game_time_get() - distributed_predictions[player_index].taken_host_time);
}
}
state->killing_player_index = NO_PLAYER;
if (unit_index == NONE && player_index < MAXIMUM_TRACKED_PLAYERS && distributed_deaths[player_index].valid)
{
struct distributed_death const *death = &distributed_deaths[player_index];
if (death->killing_player_index != NONE)
state->killing_player_index = (byte)death->killing_player_index;
SET_FLAG(state->flags, _distributed_unit_friendly_fire_bit, death->friendly_fire);
SET_FLAG(state->flags, _distributed_unit_killed_by_vehicle_bit, death->killed_by_vehicle);
}
}
/* the state's bytes on the wire (at most DISTRIBUTED_UNIT_STATE_MAXIMUM_SIZE):
its player, flags and parts, then (alive) its unit, the vehicle and seat it
rides, (placed) where it is, how fast it moves and which way it faces,
(predicted) the client's tick, its shields and health, the damage they
show, its powerups and camouflage, or (dead) who killed it; what is its
default is left out (distributed_unit_state_read). The predicted tick is
its own client's alone (own). */
static word distributed_unit_state_write(
struct distributed_unit_state const *state,
boolean own,
byte *buffer)
{
byte flags = state->flags;
byte parts = 0;
byte *cursor = buffer + DISTRIBUTED_UNIT_STATE_MINIMUM_SIZE;
if (!own)
SET_FLAG(flags, _distributed_unit_predicted_bit, FALSE);
if (TEST_FLAG(flags, _distributed_unit_alive_bit))
{
cursor = distributed_put(cursor, &state->unit_index, sizeof(state->unit_index));
if (state->vehicle_index != NONE)
{
SET_FLAG(parts, _distributed_unit_part_riding_bit, TRUE);
cursor = distributed_put(cursor, &state->vehicle_index, sizeof(state->vehicle_index));
cursor = distributed_put(cursor, &state->seat_index, sizeof(state->seat_index));
}
if (TEST_FLAG(flags, _distributed_unit_placed_bit))
{
cursor = distributed_put(cursor, &state->position, sizeof(state->position));
cursor = distributed_put(cursor, &state->velocity, sizeof(state->velocity));
cursor = distributed_put(cursor, &state->forward, sizeof(state->forward));
if (state->up.i || state->up.j || state->up.k != (short)DISTRIBUTED_UNIT_SCALE)
{
SET_FLAG(parts, _distributed_unit_part_up_bit, TRUE);
cursor = distributed_put(cursor, &state->up, sizeof(state->up));
}
}
if (TEST_FLAG(flags, _distributed_unit_predicted_bit))
cursor = distributed_put(cursor, &state->predicted_time, sizeof(state->predicted_time));
cursor = distributed_put(cursor, &state->body_vitality, sizeof(state->body_vitality));
cursor = distributed_put(cursor, &state->shield_vitality, sizeof(state->shield_vitality));
if (state->current_body_damage || state->recent_body_damage || state->current_shield_damage ||
state->recent_shield_damage)
{
SET_FLAG(parts, _distributed_unit_part_damage_bit, TRUE);
cursor = distributed_put(cursor, &state->current_body_damage, sizeof(state->current_body_damage));
cursor = distributed_put(cursor, &state->recent_body_damage, sizeof(state->recent_body_damage));
cursor = distributed_put(cursor, &state->current_shield_damage, sizeof(state->current_shield_damage));
cursor = distributed_put(cursor, &state->recent_shield_damage, sizeof(state->recent_shield_damage));
}
{
short powerup_index;
for (powerup_index = 0; powerup_index < NUMBER_OF_PLAYER_POWERUPS; powerup_index++)
{
if (state->powerup_durations[powerup_index])
break;
}
if (powerup_index < NUMBER_OF_PLAYER_POWERUPS)
{
SET_FLAG(parts, _distributed_unit_part_powerups_bit, TRUE);
cursor = distributed_put(cursor, state->powerup_durations, sizeof(state->powerup_durations));
}
}
if (state->unit_flags || state->active_camouflage)
{
SET_FLAG(parts, _distributed_unit_part_camouflage_bit, TRUE);
cursor = distributed_put(cursor, &state->unit_flags, sizeof(state->unit_flags));
cursor = distributed_put(cursor, &state->active_camouflage, sizeof(state->active_camouflage));
}
}
else if (state->killing_player_index != NO_PLAYER)
{
SET_FLAG(parts, _distributed_unit_part_killer_bit, TRUE);
cursor = distributed_put(cursor, &state->killing_player_index, sizeof(state->killing_player_index));
}
buffer[0] = state->player_index;
buffer[1] = flags;
buffer[2] = parts;
return (word)(cursor - buffer);
}
/* a state from its bytes on the wire, no further than end: the bytes it
took, 0 for none that make one */
static word distributed_unit_state_read(
byte const *buffer,
byte const *end,
struct distributed_unit_state *state)
{
byte const *cursor = buffer;
byte parts;
csmemset(state, 0, sizeof(*state));
state->unit_index = NONE;
state->vehicle_index = NONE;
state->seat_index = NONE;
state->killing_player_index = NO_PLAYER;
state->up.k = (short)DISTRIBUTED_UNIT_SCALE;
if (!distributed_take(&cursor, end, &state->player_index, sizeof(state->player_index)) ||
!distributed_take(&cursor, end, &state->flags, sizeof(state->flags)) ||
!distributed_take(&cursor, end, &parts, sizeof(parts)))
{
return 0;
}
if (TEST_FLAG(state->flags, _distributed_unit_alive_bit))
{
if (!distributed_take(&cursor, end, &state->unit_index, sizeof(state->unit_index)))
return 0;
if (TEST_FLAG(parts, _distributed_unit_part_riding_bit) &&
(!distributed_take(&cursor, end, &state->vehicle_index, sizeof(state->vehicle_index)) ||
!distributed_take(&cursor, end, &state->seat_index, sizeof(state->seat_index))))
{
return 0;
}
if (TEST_FLAG(state->flags, _distributed_unit_placed_bit) &&
(!distributed_take(&cursor, end, &state->position, sizeof(state->position)) ||
!distributed_take(&cursor, end, &state->velocity, sizeof(state->velocity)) ||
!distributed_take(&cursor, end, &state->forward, sizeof(state->forward)) ||
(TEST_FLAG(parts, _distributed_unit_part_up_bit) &&
!distributed_take(&cursor, end, &state->up, sizeof(state->up)))))
{
return 0;
}
if (TEST_FLAG(state->flags, _distributed_unit_predicted_bit) &&
!distributed_take(&cursor, end, &state->predicted_time, sizeof(state->predicted_time)))
{
return 0;
}
if (!distributed_take(&cursor, end, &state->body_vitality, sizeof(state->body_vitality)) ||
!distributed_take(&cursor, end, &state->shield_vitality, sizeof(state->shield_vitality)))
{
return 0;
}
if (TEST_FLAG(parts, _distributed_unit_part_damage_bit) &&
(!distributed_take(&cursor, end, &state->current_body_damage, sizeof(state->current_body_damage)) ||
!distributed_take(&cursor, end, &state->recent_body_damage, sizeof(state->recent_body_damage)) ||
!distributed_take(&cursor, end, &state->current_shield_damage, sizeof(state->current_shield_damage)) ||
!distributed_take(&cursor, end, &state->recent_shield_damage, sizeof(state->recent_shield_damage))))
{
return 0;
}
if (TEST_FLAG(parts, _distributed_unit_part_powerups_bit) &&
!distributed_take(&cursor, end, state->powerup_durations, sizeof(state->powerup_durations)))
{
return 0;
}
if (TEST_FLAG(parts, _distributed_unit_part_camouflage_bit) &&
(!distributed_take(&cursor, end, &state->unit_flags, sizeof(state->unit_flags)) ||
!distributed_take(&cursor, end, &state->active_camouflage, sizeof(state->active_camouflage))))
{
return 0;
}
}
else if (TEST_FLAG(parts, _distributed_unit_part_killer_bit) &&
!distributed_take(&cursor, end, &state->killing_player_index, sizeof(state->killing_player_index)))
{
return 0;
}
return (word)(cursor - buffer);
}
/* moves the unit toward the state (at position) if it is further than
tolerance from it: within blend_distance part of the way, further all of it,
no faster than maximum_speed across and up and maximum_fall_speed down (0
for any); FALSE for a state that cannot be */
static boolean distributed_apply_state(
long unit_index,
struct distributed_unit_state const *state,
real_point3d const *position,
real tolerance,
real blend_distance,
real maximum_speed,
real maximum_fall_speed)
{
struct object_datum *object = object_get(unit_index);
real_vector3d error;
real_vector3d velocity;
real_vector3d forward;
real_vector3d up;
distributed_vector_unpack(&state->velocity, DISTRIBUTED_VELOCITY_SCALE, &velocity);
distributed_vector_unpack(&state->forward, DISTRIBUTED_UNIT_SCALE, &forward);
distributed_vector_unpack(&state->up, DISTRIBUTED_UNIT_SCALE, &up);
if (!distributed_point_valid(position, UNIT_WORLD_BOUND) || !distributed_axes_make_valid(&forward, &up))
return FALSE;
if (maximum_speed > 0.0f)
{
real rise = velocity.k > 0.0f ? velocity.k : 0.0f;
real speed = (real)sqrt(velocity.i * velocity.i + velocity.j * velocity.j + rise * rise);
if (speed > maximum_speed)
{
velocity.i *= maximum_speed / speed;
velocity.j *= maximum_speed / speed;
if (velocity.k > 0.0f)
velocity.k *= maximum_speed / speed;
}
if (maximum_fall_speed > 0.0f && velocity.k < -maximum_fall_speed)
velocity.k = -maximum_fall_speed;
}
error.i = position->x - object->object.position.x;
error.j = position->y - object->object.position.y;
error.k = position->z - object->object.position.z;
if (error.i * error.i + error.j * error.j + error.k * error.k <= tolerance * tolerance)
return TRUE;
if (network_objects_reconcile(unit_index, position, &forward, &up, &velocity, NULL, blend_distance))
distributed_statistics.corrections++;
return TRUE;
}
/* (a client) where its own players' units are this tick, noted */
static void distributed_note_own_positions(
void)
{
short local_player_index;
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
{
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;
long unit_index = distributed_living_unit(player);
struct distributed_own_position *own =
&distributed_own_positions[local_player_index][game_time_get() & (OWN_POSITION_TICKS - 1)];
/* (not riding: a rider's position is its seat's) */
if (unit_index != NONE && object_get(unit_index)->object.parent_object_index != NONE)
unit_index = NONE;
own->time = game_time_get();
own->unit_index = unit_index;
if (unit_index != NONE)
own->position = object_get(unit_index)->object.position;
}
}
/* (a client) its own player's unit where the host has it, if that is
further than a tolerance from where this machine had it at the tick the
host has it at (its own prediction come back): moved by the difference,
keeping what it has done since (no rubber band a round trip long); without
that tick, from where it is */
static void distributed_correct_own_unit(
struct player_datum const *player,
long unit_index,
struct distributed_unit_state const *state)
{
short local_player_index = player->local_player_index;
real_point3d position = state->position;
if (TEST_FLAG(state->flags, _distributed_unit_predicted_bit) &&
local_player_index >= 0 && local_player_index < MAXIMUM_LOCAL_PLAYERS)
{
long now = game_time_get();
long time = now - (long)(word)((word)now - (word)state->predicted_time);
struct distributed_own_position *own =
&distributed_own_positions[local_player_index][time & (OWN_POSITION_TICKS - 1)];
if (own->time == time && own->unit_index == unit_index)
{
struct object_datum *object = object_get(unit_index);
real_vector3d error;
short index;
/* (how long the host takes to have this machine's players, as
the round trip is smoothed on the host) */
if (distributed_own_round_trip <= 0.0f)
distributed_own_round_trip = (real)(now - time);
else
distributed_own_round_trip += ((real)(now - time) - distributed_own_round_trip) / 8.0f;
error.i = state->position.x - own->position.x;
error.j = state->position.y - own->position.y;
error.k = state->position.z - own->position.z;
if (!(error.i * error.i + error.j * error.j + error.k * error.k >
LOCAL_CORRECTION_TOLERANCE * LOCAL_CORRECTION_TOLERANCE))
{
return;
}
{
real_point3d before = object->object.position;
position.x = before.x + error.i;
position.y = before.y + error.j;
position.z = before.z + error.k;
distributed_apply_state(unit_index, state, &position, LOCAL_CORRECTION_TOLERANCE, 0.0f, 0.0f, 0.0f);
/* (the ticks noted since moved as it was, not corrected again) */
error.i = object->object.position.x - before.x;
error.j = object->object.position.y - before.y;
error.k = object->object.position.z - before.z;
for (index = 0; index < OWN_POSITION_TICKS; index++)
{
struct distributed_own_position *noted = &distributed_own_positions[local_player_index][index];
if (noted->unit_index == unit_index)
{
noted->position.x += error.i;
noted->position.y += error.j;
noted->position.z += error.k;
}
}
return;
}
}
}
distributed_apply_state(unit_index, state, &position, LOCAL_CORRECTION_TOLERANCE, 0.0f, 0.0f, 0.0f);
}
/* (a client) its own players' units, to the host */
static void distributed_client_send_predictions(
void)
{
struct data_iterator iterator;
struct player_datum *player;
distributed_packer_begin(&distributed_packer, HOST_SENDER, _distributed_message_player_prediction, NULL, 0);
data_iterator_new(&iterator, player_data);
while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL)
{
long unit_index = distributed_living_unit(player);
struct distributed_unit_state state;
byte entry[DISTRIBUTED_UNIT_STATE_MAXIMUM_SIZE];
/* a client speaks for its own players only, where they are */
if (player->local_player_index == NONE || unit_index == NONE ||
object_get(unit_index)->object.parent_object_index != NONE)
{
continue;
}
distributed_state_from_player((short)DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index), &state);
distributed_packer_add(&distributed_packer, entry, distributed_unit_state_write(&state, TRUE, entry));
}
distributed_packer_flush(&distributed_packer);
}
/* (the host) a client's own players: the latest of each, taken at the next
tick */
static void distributed_handle_predictions(
long machine_index,
long time,
byte const *data,
byte const *end,
short count)
{
short index;
for (index = 0; index < count; index++)
{
struct distributed_unit_state state;
word size = distributed_unit_state_read(data, end, &state);
if (!size)
break;
data += size;
if (state.player_index >= MAXIMUM_TRACKED_PLAYERS ||
!distributed_machine_has_player(machine_index, state.player_index) ||
!TEST_FLAG(state.flags, _distributed_unit_alive_bit) ||
!TEST_FLAG(state.flags, _distributed_unit_placed_bit))
{
continue;
}
distributed_predictions[state.player_index].valid = TRUE;
distributed_predictions[state.player_index].time = time;
distributed_predictions[state.player_index].state = state;
}
}
/* (the host) how fast a client's player's unit on foot may go, a tick, and
how high (distributed_on_foot_bound) */
struct distributed_on_foot_bound
{
/* across and up (world units a tick) */
real speed;
/* the highest the host has had it since it last had it on the ground,
and how far above where it had it then it may be (world units) */
real top_height;
real rise;
/* ... in the air, the speed up it may have left the ground at and the
height it may have had then, and the ticks the host's copy has been in
the air (none on the ground): as high as a thing thrown up so, falling
since (distributed_on_foot_ceiling) */
real rise_speed;
real rise_base;
long airborne_ticks;
};
/* (the host) how fast a client's player's unit on foot may go, a tick, and
how high: across and up twice as fast as the player runs and jumps (their
speed in the game, double speed), or as fast as the host's own ticks sent
its copy lately (an explosion's throw, which the client learns of a round
trip late), whichever is more, no more than MAXIMUM_PREDICTED_SPEED; down
as fast as that and a fall from the highest it has had it since it was on
the ground (distributed_on_foot_fall_speed); up no higher above where it
last had it on the ground than a jump takes it (or a run up a slope, and
the legs drawn up) and what its ticks threw it up. What its ticks sent it
is its speed less as much as the velocity it took of the client was faster
than the player goes of their own, and up what its tick added to that, but
a jump's: a client that says it goes faster than that gains nothing by it
(its gravity on a copy said to hover and fall, its turn at a wall), nor
any height. */
static void distributed_on_foot_bound(
short player_index,
long unit_index,
struct distributed_on_foot_bound *bound)
{
struct distributed_host_speed *noted = &distributed_host_speeds[player_index];
struct object_datum *object = object_get(unit_index);
struct biped_datum *biped = (struct biped_datum *)object_try_and_get_and_verify_type(unit_index,
_object_mask_biped);
struct player_datum *player = distributed_player(player_index);
struct game_globals *globals = scenario_get_game_globals();
long now = game_time_get();
real_vector3d const *velocity = &object->object.translational_velocity;
real host_speed = (real)sqrt(velocity->i * velocity->i + velocity->j * velocity->j + velocity->k * velocity->k);
real taken_speed = (real)sqrt(noted->velocity.i * noted->velocity.i + noted->velocity.j * noted->velocity.j +
noted->velocity.k * noted->velocity.k);
real run_speed = 0.0f;
real jump_speed = 0.0f;
real crouch_rise = 0.0f;
real base_speed;
real added_rise;
real rise_speed;
/* (a unit new on foot, a new life or a ride's end: all its speed its
ticks', from where it is) */
if (noted->unit_index != unit_index)
{
noted->unit_index = unit_index;
noted->velocity.i = 0.0f;
noted->velocity.j = 0.0f;
noted->velocity.k = 0.0f;
noted->grounded = FALSE;
noted->flight_speed = 0.0f;
noted->flight_rise = 0.0f;
noted->ground_height = object->object.position.z;
noted->top_height = object->object.position.z;
noted->ground_time = now;
taken_speed = 0.0f;
}
/* (the most of each span) */
if (now < noted->time || now - noted->time >= 2 * PREDICTION_SPEED_TICKS)
{
noted->time = now;
noted->speeds[0] = 0.0f;
noted->speeds[1] = 0.0f;
noted->rises[0] = 0.0f;
noted->rises[1] = 0.0f;
}
else if (now - noted->time >= PREDICTION_SPEED_TICKS)
{
noted->time = now;
noted->speeds[1] = noted->speeds[0];
noted->speeds[0] = 0.0f;
noted->rises[1] = noted->rises[0];
noted->rises[0] = 0.0f;
}
/* (as bipeds.c moves a player: forward or back and sideways at once) */
if (globals && globals->player_information.count > 0)
{
struct game_globals_player_information *information = TAG_BLOCK_GET_ELEMENT(&globals->player_information, 0,
struct game_globals_player_information);
real forward = MAX(information->run_forward_speed, information->run_backward_speed);
real sideways = information->run_sideways_speed;
run_speed = (real)sqrt(forward * forward + sideways * sideways) / TICKS_PER_SECOND;
if (game_players_are_double_speed() && information->double_speed_multiplier > 1.0f)
run_speed *= information->double_speed_multiplier;
}
if (player && player->speed_multiplier > 1.0f)
run_speed *= player->speed_multiplier;
if (biped)
{
struct biped_definition *definition = biped_definition_get(biped->definition_index);
jump_speed = definition->biped.jump_velocity;
crouch_rise = definition->biped.collision_height_standing - definition->biped.collision_height_crouching;
}
base_speed = PREDICTED_ON_FOOT_SPEED_SCALE * (real)sqrt(run_speed * run_speed + jump_speed * jump_speed);
/* (so written that a tag's speed not a number is none) */
if (!(base_speed >= 0.0f))
base_speed = 0.0f;
if (!(jump_speed >= 0.0f))
jump_speed = 0.0f;
if (!(crouch_rise >= 0.0f))
crouch_rise = 0.0f;
/* (what its tick sent it, noted; so written that a speed not a number
is none) */
if (taken_speed > base_speed)
host_speed -= taken_speed - base_speed;
added_rise = MAX(velocity->k, 0.0f) - MAX(noted->velocity.k, 0.0f);
if (noted->grounded)
added_rise -= jump_speed;
if (host_speed > noted->speeds[0])
noted->speeds[0] = host_speed;
if (added_rise > noted->rises[0])
noted->rises[0] = added_rise;
/* (how high: where it is on the ground, or the highest since; and in
the air, a throw's speeds kept till it lands, a few seconds at most) */
noted->grounded = !biped || !TEST_FLAG(biped->biped.flags, _biped_airborne_bit);
if (noted->grounded)
{
noted->ground_height = object->object.position.z;
noted->ground_time = now;
}
if (noted->grounded || object->object.position.z > noted->top_height)
noted->top_height = object->object.position.z;
if (noted->grounded || biped->biped.airborne_ticks >= SCHAR_MAX)
{
noted->flight_speed = 0.0f;
noted->flight_rise = 0.0f;
}
else
{
noted->flight_speed = MAX(noted->flight_speed, MAX(noted->speeds[0], noted->speeds[1]));
noted->flight_rise = MAX(noted->flight_rise, MAX(noted->rises[0], noted->rises[1]));
}
bound->speed = MAX(base_speed, MAX(MAX(noted->speeds[0], noted->speeds[1]), noted->flight_speed));
/* (some: none is no bound, distributed_apply_state) */
bound->speed = MAX(bound->speed, PREDICTED_ON_FOOT_SPEED_MARGIN);
bound->speed = MIN(bound->speed, MAXIMUM_PREDICTED_SPEED);
bound->top_height = noted->top_height;
/* (a jump's rise, as fast up as it or a run up a slope is: its speed
squared over twice gravity) */
rise_speed = MAX(MAX(noted->rises[0], noted->rises[1]), noted->flight_rise);
/* (a throw of it on the ground was noted less a jump, which its own
client may have made as it came: a jump's again on top) */
if (rise_speed > 0.0f)
rise_speed += jump_speed;
rise_speed += MAX(jump_speed, run_speed);
bound->rise = UNIT_WORLD_BOUND;
if (biped && global_gravity > 0.0f)
bound->rise = rise_speed * rise_speed / (2.0f * global_gravity) + crouch_rise + PREDICTED_RISE_TOLERANCE;
bound->rise_speed = rise_speed;
bound->rise_base = crouch_rise + PREDICTED_RISE_TOLERANCE;
bound->airborne_ticks = noted->grounded ? 0 : now - noted->ground_time;
if (!(bound->rise >= 0.0f))
bound->rise = UNIT_WORLD_BOUND;
}
/* (the host) how fast a client's player's unit on foot may fall at a
height (world units a tick): as fast as it goes across, and as fast as a
fall from the highest the host has had it since it was on the ground, no
more than MAXIMUM_PREDICTED_SPEED */
static real distributed_on_foot_fall_speed(
struct distributed_on_foot_bound const *bound,
real height)
{
real drop = bound->top_height - height;
real speed = bound->speed;
if (drop > 0.0f && global_gravity > 0.0f)
speed += (real)sqrt(2.0f * global_gravity * drop);
return MIN(speed, MAXIMUM_PREDICTED_SPEED);
}
/* (the host) how far above where the host last had a client's player's
unit on the ground it may be: a jump's height (or a throw's), and in the
air no higher than a thing thrown up at that speed falls to since (the
ticks its copy has been in the air, less the client's round trip and
jitter, which are the client's ahead): no hovering in the air, nor coming
down slowly */
static real distributed_on_foot_ceiling(
short player_index,
struct distributed_on_foot_bound const *bound)
{
real rise = bound->rise;
/* (a round trip a client makes long gains it no more than this) */
real ticks = (real)bound->airborne_ticks -
MIN(distributed_machine_round_trip_ticks(distributed_player_machine(player_index)),
(real)PREDICTION_CEILING_LEAD_TICKS) -
(real)PREDICTION_JITTER_TICKS;
if (bound->airborne_ticks > 0 && ticks > 0.0f && global_gravity > 0.0f)
{
real ceiling = bound->rise_base + bound->rise_speed * ticks - 0.5f * global_gravity * ticks * ticks;
if (ceiling < rise)
rise = ceiling;
}
return rise;
}
/* (the host) whether a client's player's unit on foot goes from one point
to the other in the ticks: across and up no further than at its speed, down
no further than it falls to there, a little more a tick (and a blend) */
static boolean distributed_on_foot_move_valid(
struct distributed_on_foot_bound const *bound,
real_point3d const *from,
real_point3d const *to,
long ticks)
{
real dx = to->x - from->x;
real dy = to->y - from->y;
real dz = to->z - from->z;
real rise = dz > 0.0f ? dz : 0.0f;
real reach = MIN(bound->speed + PREDICTED_ON_FOOT_SPEED_MARGIN, MAXIMUM_PREDICTED_SPEED) * (real)ticks +
HOST_BLEND_DISTANCE;
real fall_reach = MIN(distributed_on_foot_fall_speed(bound, to->z) + PREDICTED_ON_FOOT_SPEED_MARGIN,
MAXIMUM_PREDICTED_SPEED) * (real)ticks + HOST_BLEND_DISTANCE;
/* (so written that a position not a number is not within) */
return dx * dx + dy * dy + rise * rise <= reach * reach && -dz <= fall_reach;
}
/* (the host) a client's player's prediction of its unit on foot, taken
where it says within a tolerance (distributed_apply_predictions) */
static void distributed_take_prediction(
short player_index,
long unit_index,
struct distributed_on_foot_bound const *bound)
{
struct distributed_unit_state const *state = &distributed_predictions[player_index].state;
struct object_datum *object = object_get(unit_index);
long now = game_time_get();
real dx = state->position.x - object->object.position.x;
real dy = state->position.y - object->object.position.y;
real dz = state->position.z - object->object.position.z;
if (!(dx * dx + dy * dy + dz * dz <= HOST_ACCEPT_TOLERANCE * HOST_ACCEPT_TOLERANCE))
return;
if (distributed_accepted[player_index].valid)
{
long ticks = MIN(distributed_predictions[player_index].time - distributed_accepted[player_index].time,
now - distributed_accepted[player_index].host_time + PREDICTION_JITTER_TICKS);
/* (the host's own unit moved further than a unit moves since the
last taken, which only the host moves it by: taken from where it
is, a new anchor, and its height where it starts) */
if (!distributed_on_foot_move_valid(bound, &distributed_accepted[player_index].taken_host_position,
&object->object.position, now - distributed_accepted[player_index].taken_host_time))
{
distributed_accepted[player_index].valid = FALSE;
distributed_host_speeds[player_index].ground_height = object->object.position.z;
distributed_host_speeds[player_index].top_height = object->object.position.z;
distributed_host_speeds[player_index].ground_time = now;
}
else if (ticks <= 0 || !distributed_on_foot_move_valid(bound, &distributed_accepted[player_index].position,
&state->position, ticks))
{
return;
}
}
/* (no higher above where the host last had it on the ground than a
jump, or a throw its ticks gave it, takes it, and in the air falling:
past that the host's copy falls as its ticks have it) */
if (!(state->position.z - distributed_host_speeds[player_index].ground_height <=
distributed_on_foot_ceiling(player_index, bound)))
{
return;
}
/* (the client told which of its ticks the host has it at) */
if (distributed_apply_state(unit_index, state, &state->position, 0.0f, HOST_BLEND_DISTANCE, bound->speed,
distributed_on_foot_fall_speed(bound, state->position.z)))
{
distributed_predictions[player_index].taken_time = distributed_predictions[player_index].time;
distributed_predictions[player_index].taken_host_time = now;
/* (a new anchor: the first, one a second on, or after a jump) */
if (!distributed_accepted[player_index].valid ||
now - distributed_accepted[player_index].host_time >= PREDICTION_ANCHOR_TICKS)
{
distributed_accepted[player_index].valid = TRUE;
distributed_accepted[player_index].unit_index = unit_index;
distributed_accepted[player_index].time = distributed_predictions[player_index].time;
distributed_accepted[player_index].host_time = now;
distributed_accepted[player_index].position = state->position;
}
distributed_accepted[player_index].taken_host_time = now;
distributed_accepted[player_index].taken_host_position = object->object.position;
}
}
/* (the host) the clients' players where they say, within a tolerance: a
little off closed by half, more put there. Each accepted moves the host's
unit, and the next is measured from there, so a client could take its
player HOST_ACCEPT_TOLERANCE further each tick: a prediction is also no
further from the anchor (one taken, a newer once a second) than the player
moves in the client's ticks since (distributed_on_foot_bound, and a little
more), those no more than the host's since and a little jitter, no higher
than it climbs or jumps; its velocity is no faster. A teleporter moves the
host's own unit too, further than it moves since the last taken: it is
measured from where the host has it only, a new anchor (as it is after a
new life, or a ride). */
static void distributed_apply_predictions(
void)
{
short player_index;
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++)
{
struct distributed_unit_state const *state = &distributed_predictions[player_index].state;
struct distributed_on_foot_bound bound;
long unit_index;
if (!distributed_predictions[player_index].valid && !distributed_accepted[player_index].valid &&
distributed_host_speeds[player_index].unit_index == NONE)
{
continue;
}
unit_index = distributed_living_unit(distributed_player(player_index));
if (unit_index != NONE && object_get(unit_index)->object.parent_object_index != NONE)
unit_index = NONE;
if (distributed_accepted[player_index].valid && distributed_accepted[player_index].unit_index != unit_index)
distributed_accepted[player_index].valid = FALSE;
/* (how fast it may go, a tick, what the host's tick did to its own
copy noted before the client's word is taken; riding or dead, noted
afresh on foot) */
if (unit_index != NONE)
distributed_on_foot_bound(player_index, unit_index, &bound);
else
distributed_host_speeds[player_index].unit_index = NONE;
if (distributed_predictions[player_index].valid)
{
distributed_predictions[player_index].valid = FALSE;
distributed_predictions[player_index].taken_host_time = NONE;
/* (of the unit it has now: not one of a life before) */
if (unit_index != NONE && unit_index == state->unit_index)
distributed_take_prediction(player_index, unit_index, &bound);
}
/* (what the host's next tick starts from) */
if (unit_index != NONE)
{
struct object_datum *object = object_get(unit_index);
distributed_host_speeds[player_index].velocity = object->object.translational_velocity;
if (object->object.position.z > distributed_host_speeds[player_index].top_height)
distributed_host_speeds[player_index].top_height = object->object.position.z;
}
}
}
/* (a client) the host's word on a player's unit */
static void distributed_handle_unit_state(
struct distributed_unit_state const *state)
{
struct player_datum *player = distributed_player(state->player_index);
long player_index;
long unit_index;
boolean alive = TEST_FLAG(state->flags, _distributed_unit_alive_bit);
boolean local;
if (!player)
return;
player_index = DATUM_INDEX_NEW(state->player_index, player->identifier);
local = player->local_player_index != NONE;
/* how it died, for when this machine's copy dies
(network_distributed_player_killed) */
if (state->player_index < MAXIMUM_TRACKED_PLAYERS)
{
struct distributed_death *death = &distributed_deaths[state->player_index];
death->valid = !alive;
death->killing_player_index = state->killing_player_index != NO_PLAYER ?
state->killing_player_index : NONE;
death->friendly_fire = TEST_FLAG(state->flags, _distributed_unit_friendly_fire_bit);
death->killed_by_vehicle = TEST_FLAG(state->flags, _distributed_unit_killed_by_vehicle_bit);
}
unit_index = distributed_living_unit(player);
if (!alive)
{
/* died on the host (who counts it; the damage that killed it,
network_damage.c, usually kills it here first) */
if (unit_index != NONE)
unit_kill_no_statistics(unit_index);
return;
}
/* spawned on the host: the host's unit is the player's here too, once
this machine has it (network_objects.c) */
if (state->unit_index == NONE || !network_objects_client_has(state->unit_index) ||
!object_try_and_get_and_verify_type(state->unit_index, _object_mask_unit) ||
TEST_FLAG(object_get(state->unit_index)->object.damage_flags, _object_dead_bit))
{
return;
}
if (player->unit_index != state->unit_index)
{
if (player->unit_index != NONE)
network_player_detach_unit(player_index);
network_player_attach_unit(player_index, state->unit_index);
/* (a unit of a life this machine missed the end of, no player's
now: unit_kill_no_statistics is for players' units only) */
if (unit_index != NONE && unit_index != state->unit_index)
unit_kill(unit_index);
}
unit_index = state->unit_index;
/* the seat it rides: a client's own player's, once it has ridden
otherwise for longer than its prediction takes to reach the host and
come back */
{
struct unit_datum *unit = unit_get(unit_index);
long vehicle_index = unit->object.parent_object_index != NONE && unit->unit.parent_seat_index != NONE ?
unit->object.parent_object_index : NONE;
boolean same = vehicle_index == state->vehicle_index &&
(vehicle_index == NONE || unit->unit.parent_seat_index == state->seat_index);
short *disagreement = &distributed_seat_disagreements[state->player_index];
if (same)
*disagreement = 0;
else if (!local || ++*disagreement > MAX(SEAT_DISAGREEMENT_TICKS,
(short)ceil(distributed_own_round_trip) + SEAT_DISAGREEMENT_SLACK_TICKS))
{
network_objects_set_seat(unit_index, state->vehicle_index, state->seat_index);
*disagreement = 0;
}
}
{
struct damage_network_state damage;
damage.shield_depleted = TEST_FLAG(state->flags, _distributed_unit_shield_depleted_bit);
damage.shield_charging = TEST_FLAG(state->flags, _distributed_unit_shield_charging_bit);
damage.shield_over_charging = TEST_FLAG(state->flags, _distributed_unit_shield_over_charging_bit);
damage.body_vitality = distributed_vitality_unpack(state->body_vitality);
damage.shield_vitality = distributed_vitality_unpack(state->shield_vitality);
damage.current_body_damage = distributed_vitality_unpack(state->current_body_damage);
damage.recent_body_damage = distributed_vitality_unpack(state->recent_body_damage);
damage.current_shield_damage = distributed_vitality_unpack(state->current_shield_damage);
damage.recent_shield_damage = distributed_vitality_unpack(state->recent_shield_damage);
damage_set_network_state(unit_index, &damage);
}
/* the host's powerups (the host decides pickups) */
{
struct unit_datum *unit = unit_get(unit_index);
csmemcpy(player->powerup_durations, state->powerup_durations, sizeof(player->powerup_durations));
SET_FLAG(unit->unit.flags, _unit_active_camouflaged_bit,
TEST_FLAG(state->unit_flags, _distributed_unit_camouflaged_bit));
SET_FLAG(unit->unit.flags, _unit_super_camouflaged_bit,
TEST_FLAG(state->unit_flags, _distributed_unit_super_camouflaged_bit));
unit->unit.active_camouflage = (real)state->active_camouflage / 255.0f;
}
if (TEST_FLAG(state->flags, _distributed_unit_placed_bit) &&
object_get(unit_index)->object.parent_object_index == NONE)
{
if (local)
distributed_correct_own_unit(player, unit_index, state);
else
{
distributed_apply_state(unit_index, state, &state->position, REMOTE_CORRECTION_TOLERANCE,
REMOTE_BLEND_DISTANCE, 0.0f, 0.0f);
}
}
}
/* (a client) the host's word on every player's unit */
static void distributed_handle_unit_states(
byte const *data,
byte const *end,
short count)
{
short index;
for (index = 0; index < count; index++)
{
struct distributed_unit_state state;
word size = distributed_unit_state_read(data, end, &state);
if (!size)
break;
data += size;
distributed_handle_unit_state(&state);
}
}
/* ---------- input */
/* (a client) its own players' input of the tick just run, with the buttons
of the ticks before it, to the host */
static void distributed_client_send_inputs(
void)
{
struct
{
struct distributed_message_header header;
struct distributed_player_input inputs[MAXIMUM_LOCAL_PLAYERS];
} message;
short count = 0;
short local_player_index;
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
{
long player_index = local_player_get_player_index(local_player_index);
struct distributed_player_input *input = &message.inputs[count];
if (player_index == NONE)
continue;
csmemset(input, 0, sizeof(*input));
if (!update_client_distributed_input(local_player_index, &input->tick, &input->action, input->control_flags))
continue;
input->player_index = distributed_player_to_byte(player_index);
input->host_time = distributed_host_time;
count++;
}
if (count)
{
distributed_send(&message, _distributed_message_player_inputs, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_player_input)), _distributed_to_host);
}
}
/* (the host) a client's players' input, and from it (once a message) how
long a message takes that client and back */
static void distributed_handle_inputs(
long machine_index,
struct distributed_player_input const *inputs,
short count)
{
long host_time = NONE;
short index;
for (index = 0; index < count; index++)
{
struct distributed_player_input const *input = &inputs[index];
struct player_datum *player;
struct player_action action;
if (input->player_index >= MAXIMUM_TRACKED_PLAYERS ||
!distributed_machine_has_player(machine_index, input->player_index))
{
continue;
}
player = distributed_player(input->player_index);
if (!player)
continue;
/* (the sticks and the trigger no further than a controller's) */
action = input->action;
action.throttle.i = PIN(action.throttle.i, -1.0f, 1.0f);
action.throttle.j = PIN(action.throttle.j, -1.0f, 1.0f);
action.primary_trigger = PIN(action.primary_trigger, 0.0f, 1.0f);
update_server_handle_distributed_input(DATUM_INDEX_NEW(input->player_index, player->identifier), input->tick,
&action, input->control_flags, DISTRIBUTED_INPUT_HISTORY);
if (input->host_time != NONE && (host_time == NONE || input->host_time > host_time))
host_time = input->host_time;
}
if (host_time != NONE && machine_index >= 0 && machine_index < HALO_PORT_MAXIMUM_NETWORK_MACHINES)
{
long sample = game_time_get() - host_time;
/* (a longer one as the longest taken) */
if (sample >= 0)
{
sample = MIN(sample, MAXIMUM_ROUND_TRIP_TICKS);
/* (as TCP smooths its own: an eighth, a quarter) */
if (!distributed_round_trips[machine_index].valid)
{
distributed_round_trips[machine_index].valid = TRUE;
distributed_round_trips[machine_index].average = (real)sample;
distributed_round_trips[machine_index].deviation = (real)sample / 2.0f;
}
else
{
real difference = (real)sample - distributed_round_trips[machine_index].average;
distributed_round_trips[machine_index].average += difference / 8.0f;
distributed_round_trips[machine_index].deviation +=
((real)fabs(difference) - distributed_round_trips[machine_index].deviation) / 4.0f;
}
}
}
}
/* the cluster the object (or what it rides) is in, NONE for none */
static short distributed_object_cluster(
long object_index)
{
struct object_datum *object = object_get(object_index);
while (object->object.parent_object_index != NONE)
object = object_get(object->object.parent_object_index);
return object->object.location.cluster_index;
}
/* the player's input as the host's tick ran it, in fewer bytes; whether
their buttons or choices changed in the ticks it carries */
static boolean distributed_relayed_action_from(
short player_index,
struct player_action const **recent,
short const *recent_counts,
struct distributed_relayed_action *relayed)
{
struct player_action const *action = &recent[0][player_index];
real throttle_i = action->throttle.i > 1.0f ? 1.0f : action->throttle.i < -1.0f ? -1.0f : action->throttle.i;
real throttle_j = action->throttle.j > 1.0f ? 1.0f : action->throttle.j < -1.0f ? -1.0f : action->throttle.j;
real trigger = action->primary_trigger > 1.0f ? 1.0f : action->primary_trigger < 0.0f ? 0.0f :
action->primary_trigger;
boolean changed = FALSE;
short history;
csmemset(relayed, 0, sizeof(*relayed));
relayed->player_index = (byte)player_index;
relayed->desired_weapon_index = (signed char)action->desired_weapon_index;
relayed->desired_grenade_index = (signed char)action->desired_grenade_index;
relayed->desired_zoom_level = (signed char)MIN(action->desired_zoom_level, 127);
for (history = 0; history < DISTRIBUTED_INPUT_HISTORY; history++)
{
struct player_action const *past = recent[history] && player_index < recent_counts[history] ?
&recent[history][player_index] : NULL;
relayed->control_flags[history] = past ? (unsigned short)past->control_flags : 0;
if (history > 0 && (!past || relayed->control_flags[history] != relayed->control_flags[history - 1] ||
past->desired_weapon_index != action->desired_weapon_index ||
past->desired_grenade_index != action->desired_grenade_index ||
past->desired_zoom_level != action->desired_zoom_level))
{
changed = TRUE;
}
}
relayed->yaw = distributed_angle_pack(action->desired_facing.yaw);
relayed->pitch = distributed_angle_pack(action->desired_facing.pitch);
relayed->throttle_i = (signed char)(long)floor(throttle_i * 127.0f + 0.5f);
relayed->throttle_j = (signed char)(long)floor(throttle_j * 127.0f + 0.5f);
relayed->primary_trigger = (byte)(long)floor(trigger * 255.0f + 0.5f);
return changed;
}
/* the action's bytes on the wire (at most
DISTRIBUTED_RELAYED_ACTION_MAXIMUM_SIZE): its player and parts, the
choices, facing, sticks and trigger, the latest tick's buttons, and those of
the ticks before only where they differ from the tick after's
(distributed_relayed_action_read) */
static word distributed_relayed_action_write(
struct distributed_relayed_action const *relayed,
byte *buffer)
{
byte parts = 0;
byte *cursor = buffer + 2;
short history;
cursor = distributed_put(cursor, &relayed->desired_weapon_index, sizeof(relayed->desired_weapon_index));
cursor = distributed_put(cursor, &relayed->desired_grenade_index, sizeof(relayed->desired_grenade_index));
cursor = distributed_put(cursor, &relayed->desired_zoom_level, sizeof(relayed->desired_zoom_level));
cursor = distributed_put(cursor, &relayed->yaw, sizeof(relayed->yaw));
cursor = distributed_put(cursor, &relayed->pitch, sizeof(relayed->pitch));
cursor = distributed_put(cursor, &relayed->throttle_i, sizeof(relayed->throttle_i));
cursor = distributed_put(cursor, &relayed->throttle_j, sizeof(relayed->throttle_j));
cursor = distributed_put(cursor, &relayed->primary_trigger, sizeof(relayed->primary_trigger));
cursor = distributed_put(cursor, &relayed->control_flags[0], sizeof(relayed->control_flags[0]));
for (history = 1; history < DISTRIBUTED_INPUT_HISTORY; history++)
{
if (relayed->control_flags[history] != relayed->control_flags[history - 1])
{
SET_FLAG(parts, history - 1, TRUE);
cursor = distributed_put(cursor, &relayed->control_flags[history], sizeof(relayed->control_flags[history]));
}
}
buffer[0] = relayed->player_index;
buffer[1] = parts;
return (word)(cursor - buffer);
}
/* an action from its bytes on the wire, no further than end: the bytes it
took, 0 for none that make one */
static word distributed_relayed_action_read(
byte const *buffer,
byte const *end,
struct distributed_relayed_action *relayed)
{
byte const *cursor = buffer;
byte parts;
short history;
csmemset(relayed, 0, sizeof(*relayed));
if (!distributed_take(&cursor, end, &relayed->player_index, sizeof(relayed->player_index)) ||
!distributed_take(&cursor, end, &parts, sizeof(parts)) ||
!distributed_take(&cursor, end, &relayed->desired_weapon_index, sizeof(relayed->desired_weapon_index)) ||
!distributed_take(&cursor, end, &relayed->desired_grenade_index, sizeof(relayed->desired_grenade_index)) ||
!distributed_take(&cursor, end, &relayed->desired_zoom_level, sizeof(relayed->desired_zoom_level)) ||
!distributed_take(&cursor, end, &relayed->yaw, sizeof(relayed->yaw)) ||
!distributed_take(&cursor, end, &relayed->pitch, sizeof(relayed->pitch)) ||
!distributed_take(&cursor, end, &relayed->throttle_i, sizeof(relayed->throttle_i)) ||
!distributed_take(&cursor, end, &relayed->throttle_j, sizeof(relayed->throttle_j)) ||
!distributed_take(&cursor, end, &relayed->primary_trigger, sizeof(relayed->primary_trigger)) ||
!distributed_take(&cursor, end, &relayed->control_flags[0], sizeof(relayed->control_flags[0])))
{
return 0;
}
for (history = 1; history < DISTRIBUTED_INPUT_HISTORY; history++)
{
if (!TEST_FLAG(parts, history - 1))
relayed->control_flags[history] = relayed->control_flags[history - 1];
else if (!distributed_take(&cursor, end, &relayed->control_flags[history],
sizeof(relayed->control_flags[history])))
{
return 0;
}
}
return (word)(cursor - buffer);
}
/* (the host) what each client is sent this tick of every player: their unit,
and their input as its last tick ran it (with the buttons of the ticks
before it). Those near the client's own players every tick, those further,
or out of their sight, less often (NEAR_PLAYER_DISTANCE), and those dead or
not yet spawned as those out of sight; whose life, seat or shields' state
changed, or who came into sight, at once; whose buttons changed lately,
their input every tick; one who has left the game, only a change. A
client's own players' units every tick, their input never (it has its own),
and no dead player's input (it drives nothing). A client whose players are
dead watches from where they last were alive. Decided before the damage is
sent (network_damage.c), which goes where the players it hurt do; sent
after it (distributed_host_send_players). */
static void distributed_host_plan_players(
void)
{
static boolean present[MAXIMUM_TRACKED_PLAYERS];
static boolean changed[MAXIMUM_TRACKED_PLAYERS];
static boolean has_action[MAXIMUM_TRACKED_PLAYERS];
static boolean action_changed[MAXIMUM_TRACKED_PLAYERS];
static boolean quit[MAXIMUM_TRACKED_PLAYERS];
static real_point3d origins[MAXIMUM_TRACKED_PLAYERS];
static short clusters[MAXIMUM_TRACKED_PLAYERS];
static boolean placed[MAXIMUM_TRACKED_PLAYERS];
long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
short machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES);
struct structure_bsp *structure_bsp = global_structure_bsp_get();
short cluster_count = structure_bsp ? (short)structure_bsp->clusters.count : 0;
long now = game_time_get();
struct player_action const *recent[DISTRIBUTED_INPUT_HISTORY];
short recent_counts[DISTRIBUTED_INPUT_HISTORY];
short player_index;
short machine_number;
short history;
distributed_host_update_number = update_server_ticked_update_number();
for (history = 0; history < DISTRIBUTED_INPUT_HISTORY; history++)
{
recent[history] = distributed_host_update_number != NONE ?
update_server_update_actions(distributed_host_update_number - history, &recent_counts[history]) : NULL;
}
csmemset(distributed_sends, 0, sizeof(distributed_sends));
/* (the client each player is on) */
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++)
distributed_player_machines[player_index] = NONE;
for (machine_number = 0; machine_number < machine_count; machine_number++)
{
long *player_list = machine_get_player_list(machine_indices[machine_number]);
short local_player_index;
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
{
short index = player_list[local_player_index] != NONE ?
(short)DATUM_INDEX_TO_ABSOLUTE_INDEX(player_list[local_player_index]) : NONE;
if (index >= 0 && index < MAXIMUM_TRACKED_PLAYERS)
distributed_player_machines[index] = machine_indices[machine_number];
}
}
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++)
{
struct player_datum *player = distributed_player(player_index);
struct distributed_unit_state *state = &distributed_host_states[player_index];
long unit_index;
present[player_index] = player != NULL;
placed[player_index] = FALSE;
has_action[player_index] = FALSE;
if (!player)
continue;
quit[player_index] = player->quit_out_of_game;
distributed_state_from_player(player_index, state);
unit_index = distributed_living_unit(player);
if (unit_index != NONE)
{
object_get_origin(unit_index, &origins[player_index]);
clusters[player_index] = distributed_object_cluster(unit_index);
placed[player_index] = TRUE;
}
changed[player_index] =
/* (the predicted bit is its own client's alone) */
distributed_sent_units[player_index].flags != (byte)(state->flags & ~FLAG(_distributed_unit_predicted_bit)) ||
distributed_sent_units[player_index].unit_index != state->unit_index ||
distributed_sent_units[player_index].vehicle_index != state->vehicle_index ||
distributed_sent_units[player_index].seat_index != state->seat_index;
distributed_sent_units[player_index].flags = (byte)(state->flags & ~FLAG(_distributed_unit_predicted_bit));
distributed_sent_units[player_index].unit_index = state->unit_index;
distributed_sent_units[player_index].vehicle_index = state->vehicle_index;
distributed_sent_units[player_index].seat_index = state->seat_index;
if (placed[player_index] && recent[0] && player_index < recent_counts[0])
{
has_action[player_index] = TRUE;
action_changed[player_index] = distributed_relayed_action_from(player_index, recent, recent_counts,
&distributed_host_actions[player_index]);
}
}
for (machine_number = 0; machine_number < machine_count; machine_number++)
{
long machine_index = machine_indices[machine_number];
long *player_list = machine_get_player_list(machine_index);
real_point3d viewers[MAXIMUM_LOCAL_PLAYERS];
unsigned long *viewer_pvs[MAXIMUM_LOCAL_PLAYERS];
real_vector3d viewer_aims[MAXIMUM_LOCAL_PLAYERS];
boolean viewer_scoped[MAXIMUM_LOCAL_PLAYERS];
short viewer_count = 0;
short index;
/* its players where they are, or were when last alive (each viewer's
row of the map's potentially visible set) */
for (index = 0; index < MAXIMUM_LOCAL_PLAYERS; index++)
{
short viewer_index = player_list[index] != NONE ?
(short)DATUM_INDEX_TO_ABSOLUTE_INDEX(player_list[index]) : NONE;
struct distributed_viewer *viewer = &distributed_viewers[machine_index][index];
boolean scoped = FALSE;
if (viewer_index < 0 || viewer_index >= MAXIMUM_TRACKED_PLAYERS)
viewer->valid = FALSE;
else if (placed[viewer_index])
{
struct unit_datum *unit = unit_get(distributed_living_unit(distributed_player(viewer_index)));
viewer->valid = TRUE;
viewer->cluster_index = clusters[viewer_index];
viewer->position = origins[viewer_index];
viewer->aim = unit->unit.aiming_vector;
/* (NONE unzoomed: a char, which is unsigned on ARM) */
scoped = (signed char)unit->unit.current_zoom_level >= 0;
}
if (!viewer->valid)
continue;
viewers[viewer_count] = viewer->position;
viewer_pvs[viewer_count] = viewer->cluster_index >= 0 && viewer->cluster_index < cluster_count ?
structure_bsp_get_cluster_pvs(structure_bsp, viewer->cluster_index) : NULL;
viewer_aims[viewer_count] = viewer->aim;
viewer_scoped[viewer_count++] = scoped;
}
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++)
{
boolean own = distributed_player_machines[player_index] == machine_index;
byte *sends = &distributed_sends[machine_index][player_index];
boolean send = FALSE;
boolean seen = TRUE;
short period = 1;
if (!present[player_index] || (quit[player_index] && !own && !changed[player_index]))
continue;
if (!own && !placed[player_index])
{
/* (dead, or not yet spawned: a spawn is a change, sent at once) */
period = HIDDEN_PLAYER_PERIOD_TICKS;
distributed_seen[machine_index][player_index] = TRUE;
}
/* (a client that has had no player alive watches anyone) */
else if (!own && viewer_count)
{
real distances_squared[MAXIMUM_LOCAL_PLAYERS];
boolean sees[MAXIMUM_LOCAL_PLAYERS];
short to = clusters[player_index];
real nearest = -1.0f;
boolean visible = FALSE;
for (index = 0; index < viewer_count; index++)
{
real dx = origins[player_index].x - viewers[index].x;
real dy = origins[player_index].y - viewers[index].y;
real dz = origins[player_index].z - viewers[index].z;
distances_squared[index] = dx * dx + dy * dy + dz * dz;
if (nearest < 0.0f || distances_squared[index] < nearest)
nearest = distances_squared[index];
/* (outside the map's clusters: in sight, to be safe) */
sees[index] = !viewer_pvs[index] || to < 0 || to >= cluster_count ||
BIT_VECTOR_TEST_FLAG(viewer_pvs[index], to);
visible |= sees[index];
}
period = !visible ? HIDDEN_PLAYER_PERIOD_TICKS :
nearest < NEAR_PLAYER_DISTANCE * NEAR_PLAYER_DISTANCE ? 1 :
nearest < MIDDLE_PLAYER_DISTANCE * MIDDLE_PLAYER_DISTANCE ? 2 :
nearest < FAR_PLAYER_DISTANCE * FAR_PLAYER_DISTANCE ? 3 : 4;
/* (came into sight: at once) */
if (visible && !distributed_seen[machine_index][player_index])
send = TRUE;
distributed_seen[machine_index][player_index] = visible;
seen = visible;
/* aimed near, when not sent this tick otherwise: the cosine of the
angle off the aim, compared squared (along / distance >= cosine) */
if (!send && !changed[player_index] && period > 1 && (now + player_index) % period != 0)
{
short aimed_period = HIDDEN_PLAYER_PERIOD_TICKS;
for (index = 0; index < viewer_count; index++)
{
real along;
if (!sees[index])
continue;
along = (origins[player_index].x - viewers[index].x) * viewer_aims[index].i +
(origins[player_index].y - viewers[index].y) * viewer_aims[index].j +
(origins[player_index].z - viewers[index].z) * viewer_aims[index].k;
if (along > 0.0f && viewer_scoped[index] &&
along * along >= SCOPED_AT_COSINE * SCOPED_AT_COSINE * distances_squared[index])
{
aimed_period = 1;
}
else if (along > 0.0f &&
along * along >= AIMED_AT_COSINE * AIMED_AT_COSINE * distances_squared[index])
{
aimed_period = MIN(aimed_period, 2);
}
}
period = MIN(period, aimed_period);
}
}
else
{
distributed_seen[machine_index][player_index] = TRUE;
}
send |= changed[player_index] || (now + player_index) % period == 0;
if (seen || own)
SET_FLAG(*sends, _distributed_send_sees_bit, TRUE);
if (send || own)
SET_FLAG(*sends, _distributed_send_state_bit, TRUE);
if (!own && has_action[player_index] && (send || action_changed[player_index]))
SET_FLAG(*sends, _distributed_send_action_bit, TRUE);
}
}
}
/* (the host) to each client what distributed_host_plan_players decided, the
input before the units, as the host's tick had them */
static void distributed_host_send_players(
void)
{
long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
short machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES);
short machine_number;
for (machine_number = 0; machine_number < machine_count; machine_number++)
{
long machine_index = machine_indices[machine_number];
short player_index;
distributed_packer_begin(&distributed_packer, (short)machine_index, _distributed_message_relayed_actions,
&distributed_host_update_number, sizeof(distributed_host_update_number));
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++)
{
byte entry[DISTRIBUTED_RELAYED_ACTION_MAXIMUM_SIZE];
if (TEST_FLAG(distributed_sends[machine_index][player_index], _distributed_send_action_bit))
{
distributed_packer_add(&distributed_packer, entry,
distributed_relayed_action_write(&distributed_host_actions[player_index], entry));
}
}
distributed_packer_flush(&distributed_packer);
distributed_packer_begin(&distributed_packer, (short)machine_index, _distributed_message_unit_states, NULL, 0);
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++)
{
byte entry[DISTRIBUTED_UNIT_STATE_MAXIMUM_SIZE];
if (TEST_FLAG(distributed_sends[machine_index][player_index], _distributed_send_state_bit))
{
distributed_packer_add(&distributed_packer, entry,
distributed_unit_state_write(&distributed_host_states[player_index],
distributed_player_machines[player_index] == machine_index, entry));
}
}
distributed_packer_flush(&distributed_packer);
}
}
/* (a client) the host's input for its players, for the others it drives:
the host's update, then each player's */
static void distributed_handle_actions(
byte const *data,
byte const *end,
short count)
{
long update_number;
short index;
if (!distributed_take(&data, end, &update_number, sizeof(update_number)))
return;
for (index = 0; index < count; index++)
{
struct distributed_relayed_action relayed;
struct player_action action;
word size = distributed_relayed_action_read(data, end, &relayed);
if (!size)
break;
data += size;
csmemset(&action, 0, sizeof(action));
action.control_flags = relayed.control_flags[0];
action.desired_facing.yaw = distributed_angle_unpack(relayed.yaw, FALSE);
action.desired_facing.pitch = distributed_angle_unpack(relayed.pitch, TRUE);
action.throttle.i = (real)relayed.throttle_i / 127.0f;
action.throttle.j = (real)relayed.throttle_j / 127.0f;
action.primary_trigger = (real)relayed.primary_trigger / 255.0f;
action.desired_weapon_index = relayed.desired_weapon_index;
action.desired_grenade_index = relayed.desired_grenade_index;
action.desired_zoom_level = relayed.desired_zoom_level;
update_client_handle_relayed_action(relayed.player_index, update_number, &action,
relayed.control_flags, DISTRIBUTED_INPUT_HISTORY);
}
}
/* ---------- deaths */
/* a player died (game_engine_player_killed, before it announces who killed
whom): the host notes who killed them for its clients; on a client, whose
copy of the death knows nothing of the killer, the host's killer, as this
machine has them */
void network_distributed_player_killed(
long *killing_player_index,
long *killing_object_index,
long dead_player_index,
boolean *friendly_fire)
{
short dead_absolute_index = (short)DATUM_INDEX_TO_ABSOLUTE_INDEX(dead_player_index);
struct distributed_death *death;
if (dead_absolute_index < 0 || dead_absolute_index >= MAXIMUM_TRACKED_PLAYERS)
return;
death = &distributed_deaths[dead_absolute_index];
if (game_connection() == _game_connection_network_server)
{
struct object_datum *killing_object = *killing_object_index != NONE ?
object_try_and_get(*killing_object_index) : NULL;
death->valid = TRUE;
death->killing_player_index = *killing_player_index != NONE ?
(short)DATUM_INDEX_TO_ABSOLUTE_INDEX(*killing_player_index) : NONE;
death->friendly_fire = *friendly_fire;
death->killed_by_vehicle = *killing_player_index == NONE && killing_object &&
killing_object->object.type == _object_type_vehicle;
distributed_statistics_due = TRUE;
}
else if (game_connection() == _game_connection_network_client && death->valid)
{
struct player_datum *killing_player = death->killing_player_index != NONE ?
distributed_player(death->killing_player_index) : NULL;
*friendly_fire = death->friendly_fire;
*killing_player_index = NONE;
if (killing_player)
{
*killing_player_index = DATUM_INDEX_NEW(death->killing_player_index, killing_player->identifier);
*killing_object_index = killing_player->unit_index;
}
/* (an empty vehicle's: the one that did it, the same object here) */
else if (!death->killed_by_vehicle || *killing_object_index == NONE ||
!object_try_and_get(*killing_object_index) ||
object_get(*killing_object_index)->object.type != _object_type_vehicle)
{
*killing_object_index = NONE;
}
}
}
/* (a client) the host's word on how a player died, with the damage that
killed them (network_damage.c), before this machine's copy dies */
void distributed_set_death(
short dead_player_index,
byte killing_player_index,
boolean friendly_fire,
boolean killed_by_vehicle)
{
struct distributed_death *death;
if (dead_player_index < 0 || dead_player_index >= MAXIMUM_TRACKED_PLAYERS)
return;
death = &distributed_deaths[dead_player_index];
death->valid = TRUE;
death->killing_player_index = killing_player_index != NO_PLAYER ? killing_player_index : NONE;
death->friendly_fire = friendly_fire;
death->killed_by_vehicle = killed_by_vehicle;
}
/* (the host) how a player died, for the damage that killed them */
boolean distributed_get_death(
short dead_player_index,
byte *killing_player_index,
boolean *friendly_fire,
boolean *killed_by_vehicle)
{
struct distributed_death const *death;
if (dead_player_index < 0 || dead_player_index >= MAXIMUM_TRACKED_PLAYERS)
return FALSE;
death = &distributed_deaths[dead_player_index];
*killing_player_index = death->killing_player_index != NONE ? (byte)death->killing_player_index : NO_PLAYER;
*friendly_fire = death->friendly_fire;
*killed_by_vehicle = death->killed_by_vehicle;
return death->valid;
}
/* ---------- pickups */
/* (the host) a player on another machine picked something up (players.c),
for that machine to show */
void network_distributed_player_picked_up(
long player_index,
short kind,
long definition_index,
short count)
{
struct distributed_pickup *pickup;
if (game_connection() != _game_connection_network_server ||
distributed_pickup_count >= MAXIMUM_PICKUPS_PER_TICK)
{
return;
}
pickup = &distributed_pickups[distributed_pickup_count++];
pickup->player_index = distributed_player_to_byte(player_index);
pickup->kind = (byte)kind;
pickup->count = count;
pickup->definition_index = definition_index;
}
/* (the host) each machine what its players picked up (no other machine
shows it) */
static void distributed_send_pickups(
void)
{
struct
{
struct distributed_message_header header;
struct distributed_pickup pickups[MAXIMUM_PICKUPS_PER_TICK];
} message;
short index;
for (index = 0; index < distributed_pickup_count; index++)
{
long machine_index = distributed_player_machine(distributed_pickups[index].player_index);
short other;
short count = 0;
/* (each machine once, with all of its players') */
for (other = 0; other < index; other++)
{
if (distributed_player_machine(distributed_pickups[other].player_index) == machine_index)
break;
}
if (machine_index == NONE || other < index)
continue;
for (other = index; other < distributed_pickup_count; other++)
{
if (distributed_player_machine(distributed_pickups[other].player_index) == machine_index)
message.pickups[count++] = distributed_pickups[other];
}
distributed_send_to_machine_reliably(machine_index, &message, _distributed_message_pickups, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_pickup)));
}
distributed_pickup_count = 0;
}
/* ---------- statistics */
/* the bytes' checksum (FNV-1a) */
static unsigned long distributed_checksum(
void const *data,
long size)
{
byte const *bytes = (byte const *)data;
unsigned long checksum = 2166136261UL;
long index;
for (index = 0; index < size; index++)
checksum = (checksum ^ bytes[index]) * 16777619UL;
return checksum;
}
/* the players' statistics that changed since they were last sent, and when
refreshing, STATISTICS_REFRESH_PLAYERS more whatever they are, round them
all (a client that lost a change has it again within eight seconds) */
static void distributed_send_statistics(
boolean refresh)
{
struct distributed_statistics_message message;
short limit = MIN(MAXIMUM_STATISTICS_PER_MESSAGE, RELIABLE_ENTRIES(struct distributed_player_statistics));
short count = 0;
short refreshed = refresh ? 0 : STATISTICS_REFRESH_PLAYERS;
/* (where this round starts: the cursor moves past the last player it
refreshed, which must not shift the players this round visits) */
short first = distributed_statistics_cursor;
short step;
for (step = 0; step < MAXIMUM_TRACKED_PLAYERS; step++)
{
short player_index = (short)((first + step) % MAXIMUM_TRACKED_PLAYERS);
struct player_datum *player = distributed_player(player_index);
unsigned long checksum;
if (!player)
continue;
checksum = distributed_checksum(&player->statistics, sizeof(player->statistics));
if (checksum == distributed_sent_statistics[player_index])
{
if (refreshed >= STATISTICS_REFRESH_PLAYERS)
continue;
refreshed++;
distributed_statistics_cursor = (short)((player_index + 1) % MAXIMUM_TRACKED_PLAYERS);
}
distributed_sent_statistics[player_index] = checksum;
message.players[count].player_index = player_index;
message.players[count].pad = 0;
message.players[count].statistics = player->statistics;
count++;
if (count == limit)
{
distributed_send(&message, _distributed_message_player_statistics, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_player_statistics)),
_distributed_to_clients);
count = 0;
}
}
if (count)
{
distributed_send(&message, _distributed_message_player_statistics, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_player_statistics)),
_distributed_to_clients);
}
}
/* every player's statistics, to a machine that has loaded (the others', and
what was last sent them, as they are) */
static void distributed_send_all_statistics(
long machine_index)
{
struct distributed_statistics_message message;
short limit = MIN(MAXIMUM_STATISTICS_PER_MESSAGE, RELIABLE_ENTRIES(struct distributed_player_statistics));
short count = 0;
short player_index;
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++)
{
struct player_datum *player = distributed_player(player_index);
if (!player)
continue;
message.players[count].player_index = player_index;
message.players[count].pad = 0;
message.players[count].statistics = player->statistics;
count++;
if (count == limit)
{
distributed_send_to_machine(machine_index, &message, _distributed_message_player_statistics, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_player_statistics)));
count = 0;
}
}
if (count)
{
distributed_send_to_machine(machine_index, &message, _distributed_message_player_statistics, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_player_statistics)));
}
}
/* ---------- the game type's state */
/* the game type's state (larger than a datagram: reliably) to a machine
that has loaded, or (NONE) to every client when it changed (at most every
other look, unless the game ended) */
static void distributed_send_game_state(
long machine_index)
{
struct
{
struct distributed_message_header header;
byte data[MAXIMUM_GAME_STATE_SIZE];
} message;
long size = game_engine_write_network_state(message.data, sizeof(message.data));
unsigned long checksum;
if (size <= 0)
return;
if (machine_index != NONE)
{
distributed_send_to_machine_reliably(machine_index, &message, _distributed_message_game_state, 0,
(word)(sizeof(message.header) + size));
return;
}
checksum = distributed_checksum(message.data, size);
if (distributed_game_state_time != NONE &&
(checksum == distributed_game_state_checksum ||
(game_time_get() - distributed_game_state_time < GAME_STATE_MINIMUM_TICKS &&
(size < (long)sizeof(long) ||
csmemcmp(message.data, &distributed_game_state_postgame, sizeof(long)) == 0))))
{
return;
}
if (size >= (long)sizeof(long))
csmemcpy(&distributed_game_state_postgame, message.data, sizeof(long));
distributed_game_state_checksum = checksum;
distributed_game_state_time = game_time_get();
distributed_send(&message, _distributed_message_game_state, 0, (word)(sizeof(message.header) + size),
_distributed_to_clients_reliably);
}
/* ---------- the game */
/* a new map loading (game.c), before any of the new game's messages can
apply: nothing sent or had yet */
void network_distributed_new_game(
void)
{
short sender;
short type;
short player_index;
distributed_last_sent_time = NONE;
csmemset(distributed_deaths, 0, sizeof(distributed_deaths));
csmemset(distributed_seat_disagreements, 0, sizeof(distributed_seat_disagreements));
csmemset(distributed_predictions, 0, sizeof(distributed_predictions));
csmemset(distributed_accepted, 0, sizeof(distributed_accepted));
csmemset(distributed_host_speeds, 0, sizeof(distributed_host_speeds));
csmemset(distributed_round_trips, 0, sizeof(distributed_round_trips));
csmemset(distributed_client_clocks, 0, sizeof(distributed_client_clocks));
csmemset(distributed_client_identities, 0, sizeof(distributed_client_identities));
distributed_identity_sent = FALSE;
{
short machine_index;
for (machine_index = 0; machine_index < HALO_PORT_MAXIMUM_NETWORK_MACHINES; machine_index++)
distributed_client_clocks[machine_index].window_tick = NONE;
}
csmemset(distributed_seen, 0, sizeof(distributed_seen));
csmemset(distributed_viewers, 0, sizeof(distributed_viewers));
csmemset(distributed_sends, 0, sizeof(distributed_sends));
/* (none sent: every player's the first time) */
csmemset(distributed_sent_statistics, 0, sizeof(distributed_sent_statistics));
distributed_statistics_cursor = 0;
distributed_game_state_checksum = 0;
distributed_game_state_postgame = 0;
distributed_game_state_time = NONE;
distributed_host_update_number = NONE;
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++)
{
distributed_sent_units[player_index].flags = 0;
distributed_sent_units[player_index].unit_index = NONE;
distributed_sent_units[player_index].vehicle_index = NONE;
distributed_sent_units[player_index].seat_index = NONE;
}
for (sender = 0; sender < MAXIMUM_SENDERS; sender++)
{
distributed_batches[sender].size = 0;
for (type = 0; type < NUMBER_OF_DISTRIBUTED_MESSAGES; type++)
distributed_received_times[sender][type] = NONE;
}
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++)
{
distributed_predictions[player_index].taken_host_time = NONE;
distributed_host_speeds[player_index].unit_index = NONE;
distributed_player_machines[player_index] = NONE;
}
for (sender = 0; sender < MAXIMUM_LOCAL_PLAYERS; sender++)
{
for (type = 0; type < OWN_POSITION_TICKS; type++)
{
distributed_own_positions[sender][type].time = NONE;
distributed_own_positions[sender][type].unit_index = NONE;
}
}
distributed_own_round_trip = 0.0f;
csmemset(distributed_machine_players, 0xFF, sizeof(distributed_machine_players));
distributed_machines.in_tick = FALSE;
distributed_machines.valid = FALSE;
distributed_host_time = NONE;
distributed_statistics_due = FALSE;
distributed_pickup_count = 0;
/* (each player's latest input: player_queues_new.c) */
update_queues_distributed_reset();
network_objects_new_game();
network_damage_new_game();
}
/* after each tick (game_time.c) */
void network_distributed_tick(
void)
{
short connection = game_connection();
if (game_time_get() == distributed_last_sent_time)
return;
distributed_last_sent_time = game_time_get();
distributed_machines.in_tick = TRUE;
if (connection == _game_connection_network_server)
{
short player_index;
/* (a living player's last death is behind them: a death the game
does not note, one it does not score, has no killer) */
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++)
{
if (distributed_deaths[player_index].valid &&
distributed_living_unit(distributed_player(player_index)) != NONE)
{
distributed_deaths[player_index].valid = FALSE;
}
}
/* the clients' players where they say they are, then the objects
first (created before anything names them), the damage dealt this
tick before the units it hurt and killed (but where the units go
decided first: the damage goes where the players it hurt do), and a
kill's statistics before the kill, so that a client announcing it
counts it (a double kill, a killing spree) */
distributed_apply_predictions();
network_objects_apply_vehicle_predictions();
network_objects_host_tick();
distributed_host_plan_players();
network_damage_host_tick();
if (distributed_statistics_due || game_time_get() % STATISTICS_INTERVAL_TICKS == 0)
distributed_send_statistics(game_time_get() % STATISTICS_REFRESH_TICKS == 0);
distributed_statistics_due = FALSE;
distributed_host_send_players();
distributed_send_pickups();
if (game_time_get() % GAME_STATE_INTERVAL_TICKS == 0)
distributed_send_game_state(NONE);
}
else if (connection == _game_connection_network_client)
{
distributed_note_own_positions();
distributed_client_send_inputs();
distributed_client_send_predictions();
network_objects_client_tick();
network_damage_client_tick();
}
distributed_batches_flush();
distributed_machines.in_tick = FALSE;
distributed_machines.valid = FALSE;
}
/* whether an unreliable message of the kind is older than one had already
(then it is dropped: the newer has overtaken it) */
static boolean distributed_message_stale(
long machine_index,
struct distributed_message_header const *header)
{
short sender = machine_index == NONE ? HOST_SENDER : (short)machine_index;
long *latest;
switch (header->type)
{
case _distributed_message_player_prediction:
case _distributed_message_unit_states:
case _distributed_message_player_statistics:
case _distributed_message_inventories:
case _distributed_message_object_states:
case _distributed_message_vehicle_prediction:
case _distributed_message_player_inputs:
case _distributed_message_relayed_actions:
case _distributed_message_damage_events:
break;
default:
return FALSE;
}
if (sender < 0 || sender >= MAXIMUM_SENDERS)
return TRUE;
latest = &distributed_received_times[sender][header->type];
/* (a tick's several messages of a kind have its time alike) */
if (*latest != NONE && header->game_time < *latest)
return TRUE;
*latest = header->game_time;
return FALSE;
}
/* (the host) a text shown in red on every machine's console: its own, and
every client's (_distributed_message_notice) */
static void distributed_send_notice(
char const *text)
{
struct
{
struct distributed_message_header header;
char text[MAXIMUM_NOTICE_LENGTH];
} message;
long length = csstrlen(text);
if (length > MAXIMUM_NOTICE_LENGTH - 1)
length = MAXIMUM_NOTICE_LENGTH - 1;
csmemset(&message, 0, sizeof(message));
csmemcpy(message.text, text, length);
console_warning("%s", message.text);
error(_error_log, "%s", message.text);
distributed_send(&message, _distributed_message_notice, 0, (word)(sizeof(message.header) + length + 1),
_distributed_to_clients_reliably);
}
/* (the host) the names of a client machine's players, in ASCII, for a
notice ("?" for what is not ASCII) */
static void distributed_machine_player_names(
long machine_index,
char *names,
long size)
{
struct data_iterator iterator;
struct player_datum *player;
long length = 0;
names[0] = 0;
data_iterator_new(&iterator, player_data);
while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL)
{
short index;
if (distributed_player_machine((short)DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index)) != machine_index)
continue;
if (length && length + 2 < size)
{
names[length++] = ',';
names[length++] = ' ';
}
for (index = 0; index < (short)NUMBEROF(player->name) && player->name[index] && length + 1 < size; index++)
names[length++] = player->name[index] >= 32 && player->name[index] < 127 ? (char)player->name[index] : '?';
names[length] = 0;
}
if (!length)
snprintf(names, size, "machine #%ld", machine_index);
}
/* (a client) its Discord user, as its Discord told it (none without one:
not running, or internet play off), to the host: once it is a machine the
host takes messages of (its ready sent: network_objects_client_tick), and
again when it changes (Discord connecting later), looked at once a second */
void distributed_client_send_identity(
void)
{
struct
{
struct distributed_message_header header;
struct distributed_client_identity identity;
} message;
if (distributed_identity_sent && game_time_get() % TICKS_PER_SECOND != 0)
return;
csmemset(&message, 0, sizeof(message));
p2p_discord_identity(message.identity.discord_id, sizeof(message.identity.discord_id),
message.identity.discord_name, sizeof(message.identity.discord_name));
if (distributed_identity_sent &&
!csmemcmp(&message.identity, &distributed_sent_identity, sizeof(distributed_sent_identity)))
{
return;
}
distributed_sent_identity = message.identity;
distributed_identity_sent = TRUE;
distributed_send(&message, _distributed_message_client_identity, 1, (word)sizeof(message),
_distributed_to_host_reliably);
}
/* (the host) a client machine's address as text: its real one, for an
internet play peer's stand-in (p2p.c) */
static void distributed_address_text(
unsigned long address,
char *text,
long size)
{
/* 100.64.0.0/10: an internet play peer's, by its real address (network
byte order: its first number the lowest byte) */
if ((address & 0xFFC00000) == 0x64400000)
{
unsigned long network = (address >> 24) | ((address >> 8) & 0xFF00) | ((address << 8) & 0xFF0000) |
(address << 24);
unsigned long real = p2p_peer_endpoint_address(network);
if (real)
{
snprintf(text, size, "%lu.%lu.%lu.%lu", real & 255, (real >> 8) & 255, (real >> 16) & 255,
(real >> 24) & 255);
return;
}
}
if (address)
{
snprintf(text, size, "%lu.%lu.%lu.%lu", (address >> 24) & 255, (address >> 16) & 255, (address >> 8) & 255,
address & 255);
}
else
{
snprintf(text, size, "unknown");
}
}
/* (the host) a client machine's address as text (distributed_address_text) */
static void distributed_machine_address_text(
long machine_index,
char *text,
long size)
{
distributed_address_text(network_game_server_machine_address(machine_index), text, size);
}
/* text of a player's (their names) kept to printable ASCII, no longer
than the size: no line of theirs breaks or runs on */
static void distributed_printable(
char *destination,
long size,
char const *source)
{
long length = 0;
for (; source && *source && length < size - 1; source++)
destination[length++] = *source >= 32 && *source < 127 ? *source : '?';
destination[length] = 0;
}
/* (the host) a line in a list of players (CHEATERS_FILE, BANS_FILE): when,
their address, Discord user and names, and why; separated by tabs, each
part kept to the characters allowed and their lengths (what a player could
tell: their Discord user and names). The Discord user of the machine at
the index, if it is one in the game (NONE: none) */
static void distributed_write_player_record(
char const *file_name,
char const *address,
long machine_index,
char const *names,
char const *reason)
{
char discord_id[DISCORD_ID_SIZE] = "";
char discord_name[DISCORD_NAME_SIZE] = "";
char hardware_id[40] = "";
char kept_names[96];
char kept_reason[96];
char kept_address[32];
char when[32] = "";
time_t now = time(NULL);
struct tm *local = localtime(&now);
FILE *file;
if (machine_index >= 0 && machine_index < HALO_PORT_MAXIMUM_NETWORK_MACHINES && game_in_progress())
{
p2p_discord_sanitize(discord_id, sizeof(discord_id), distributed_client_identities[machine_index].discord_id, 0);
p2p_discord_sanitize(discord_name, sizeof(discord_name),
distributed_client_identities[machine_index].discord_name, 1);
}
if (machine_index >= 0 && machine_index < HALO_PORT_MAXIMUM_NETWORK_MACHINES)
p2p_hardware_id_sanitize(hardware_id, 33, network_game_server_machine_hardware_id(machine_index));
distributed_printable(kept_names, sizeof(kept_names), names);
distributed_printable(kept_reason, sizeof(kept_reason), reason);
distributed_printable(kept_address, sizeof(kept_address), address);
if (local)
strftime(when, sizeof(when), "%Y-%m-%d %H:%M:%S", local);
file = fopen(file_name, "a");
if (!file)
{
error(_error_log, "could not open %s to add a player to it", file_name);
return;
}
fprintf(file, "%s\tip=%s\thwid=%s\tdiscord_username=%s\tdiscord_id=%s\tplayers=%s\treason=%s\n", when,
kept_address, hardware_id[0] ? hardware_id : "none", discord_name[0] ? discord_name : "none",
discord_id[0] ? discord_id : "none", kept_names, kept_reason);
fclose(file);
}
/* (the host) a player dropped for cheating: in CHEATERS_FILE, and banned
(BANS_FILE) */
static void distributed_log_cheater(
long machine_index,
char const *names,
char const *reason)
{
char address[32];
distributed_machine_address_text(machine_index, address, sizeof(address));
distributed_write_player_record(CHEATERS_FILE, address, machine_index, names, reason);
distributed_write_player_record(BANS_FILE, address, machine_index, names, reason);
}
/* (the host: network_server_manager.c) whether a machine of this address
(host byte order; an internet play peer's by its real one) is banned: its
address one of BANS_FILE's (each line's "ip=", which a host may add or
take out by hand) */
boolean network_distributed_banned(
unsigned long address,
char const *hardware_id)
{
char text[32];
char line[512];
char kept_hardware_id[40];
FILE *file;
boolean banned = FALSE;
p2p_hardware_id_sanitize(kept_hardware_id, 33, hardware_id);
if (address)
distributed_address_text(address, text, sizeof(text));
else
text[0] = 0;
file = fopen(BANS_FILE, "r");
if (!file)
return FALSE;
while (!banned && fgets(line, sizeof(line), file))
{
char const *ip = strstr(line, "ip=");
char const *hwid = strstr(line, "hwid=");
size_t length = csstrlen(text);
size_t hardware_id_length = csstrlen(kept_hardware_id);
/* (the whole address: 1.2.3.4 is not 1.2.3.45) */
if (length && ip && !strncmp(ip + 3, text, length) &&
(ip[3 + length] == '\t' || ip[3 + length] == '\n' || ip[3 + length] == '\r' ||
ip[3 + length] == ' ' || ip[3 + length] == 0))
{
banned = TRUE;
}
/* (or the whole hardware id) */
if (hardware_id_length && hwid && !strncmp(hwid + 5, kept_hardware_id, hardware_id_length) &&
(hwid[5 + hardware_id_length] == '\t' || hwid[5 + hardware_id_length] == '\n' ||
hwid[5 + hardware_id_length] == '\r' || hwid[5 + hardware_id_length] == ' ' ||
hwid[5 + hardware_id_length] == 0))
{
banned = TRUE;
}
}
fclose(file);
return banned;
}
/* (the host: network_server_manager.c, its ban command) a machine banned
by the host: its line in BANS_FILE, and every machine told (the Discord
user of the machine at the index, if it is one in the game; its address,
host byte order) */
void network_distributed_ban(
long machine_index,
unsigned long address,
char const *names)
{
char text[32];
char kept_names[64];
char discord_id[DISCORD_ID_SIZE] = "";
char discord_name[DISCORD_NAME_SIZE] = "";
char discord[DISCORD_ID_SIZE + DISCORD_NAME_SIZE + 16] = "";
char notice[MAXIMUM_NOTICE_LENGTH];
distributed_address_text(address, text, sizeof(text));
distributed_write_player_record(BANS_FILE, text, machine_index, names, "banned by the host");
distributed_printable(kept_names, sizeof(kept_names), names);
if (game_in_progress() && machine_index >= 0 && machine_index < HALO_PORT_MAXIMUM_NETWORK_MACHINES)
{
p2p_discord_sanitize(discord_id, sizeof(discord_id), distributed_client_identities[machine_index].discord_id, 0);
p2p_discord_sanitize(discord_name, sizeof(discord_name),
distributed_client_identities[machine_index].discord_name, 1);
}
if (discord_id[0] || discord_name[0])
snprintf(discord, sizeof(discord), " (Discord: %s, %s)", discord_name, discord_id);
snprintf(notice, sizeof(notice), "%s%s banned by the host", kept_names, discord);
/* (to every client in the game: in the lobby, the host's own) */
if (game_in_progress())
distributed_send_notice(notice);
else
{
console_warning("%s", notice);
error(_error_log, "%s", notice);
}
}
/* (the host) a client machine's tick, which one of its messages is
stamped with: its clock measured, each window, against the host's; one
whose game runs fast (distributed_client_clock) has its players'
predictions refused, and if it goes on, is dropped */
static void distributed_note_client_clock(
long machine_index,
long tick)
{
struct distributed_client_clock *clock;
unsigned long now = system_milliseconds();
unsigned long elapsed;
if (machine_index < 0 || machine_index >= HALO_PORT_MAXIMUM_NETWORK_MACHINES)
return;
clock = &distributed_client_clocks[machine_index];
if (clock->window_tick == NONE)
{
clock->latest_tick = tick;
clock->window_tick = tick;
clock->window_time = now;
return;
}
if (tick > clock->latest_tick)
clock->latest_tick = tick;
elapsed = now - clock->window_time;
if (elapsed < CLIENT_CLOCK_WINDOW_MILLISECONDS)
return;
{
real rate = (real)(clock->latest_tick - clock->window_tick) /
((real)elapsed * (real)TICKS_PER_SECOND / 1000.0f);
long ahead = clock->latest_tick - game_time_get();
clock->fast = rate > CLIENT_CLOCK_FAST_RATE && ahead > CLIENT_CLOCK_AHEAD_TICKS;
if (!clock->fast)
{
clock->fast_windows = 0;
}
else if (++clock->fast_windows == 1)
{
error(_error_log, "machine #%ld's game runs %.2f times as fast as this host's (%ld ticks ahead): "
"its players' predictions refused", machine_index, rate, ahead);
}
else if (clock->fast_windows >= CLIENT_CLOCK_FAST_WINDOWS)
{
error(_error_log, "machine #%ld's game ran %.2f times as fast as this host's for %d seconds "
"(%ld ticks ahead): dropped", machine_index, rate,
CLIENT_CLOCK_FAST_WINDOWS * CLIENT_CLOCK_WINDOW_MILLISECONDS / 1000, ahead);
{
char names[64];
char discord_id[DISCORD_ID_SIZE];
char discord_name[DISCORD_NAME_SIZE];
char discord[DISCORD_ID_SIZE + DISCORD_NAME_SIZE + 16] = "";
char reason[64];
char text[MAXIMUM_NOTICE_LENGTH];
distributed_machine_player_names(machine_index, names, sizeof(names));
p2p_discord_sanitize(discord_id, sizeof(discord_id),
distributed_client_identities[machine_index].discord_id, 0);
p2p_discord_sanitize(discord_name, sizeof(discord_name),
distributed_client_identities[machine_index].discord_name, 1);
if (discord_id[0] || discord_name[0])
snprintf(discord, sizeof(discord), " (Discord: %s, %s)", discord_name, discord_id);
snprintf(reason, sizeof(reason), "speed hack (game ran %.2f times as fast)", rate);
snprintf(text, sizeof(text), "%s%s kicked by the host: their game ran %.2f times as fast (a speed hack)",
names, discord, rate);
distributed_send_notice(text);
distributed_log_cheater(machine_index, names, reason);
}
network_game_server_kick_machine(machine_index);
clock->fast_windows = 0;
}
}
clock->window_tick = clock->latest_tick;
clock->window_time = now;
}
/* whether a client machine's game runs fast (distributed_note_client_clock):
its players' predictions not taken */
boolean distributed_machine_clock_fast(
long machine_index)
{
return machine_index >= 0 && machine_index < HALO_PORT_MAXIMUM_NETWORK_MACHINES &&
distributed_client_clocks[machine_index].fast;
}
/* a message of the distributed kind; machine_index is the sender's on the
host, NONE on a client */
void network_distributed_handle_message(
long machine_index,
word const *message,
word size)
{
struct distributed_message_header header;
void const *entries = (byte const *)message + sizeof(header);
short index;
word entry_size;
/* (none between games: loading, or in the menus) */
if (size < sizeof(header) || !game_in_progress())
return;
csmemcpy(&header, message, sizeof(header));
/* a tick's messages in one: each as if it came alone */
if (header.type == _distributed_message_batch)
{
word offset = sizeof(header);
while (offset + sizeof(word) <= size)
{
word buffer[(sizeof(message_header) + DATAGRAM_MAXIMUM_SIZE + 1) / sizeof(word)];
word length;
csmemcpy(&length, (byte const *)message + offset, sizeof(word));
offset += sizeof(word);
if (length > size - offset || length < sizeof(header) - sizeof(message_header) ||
sizeof(message_header) + length > sizeof(buffer))
{
break;
}
csmemcpy(buffer, message, sizeof(message_header));
csmemcpy((byte *)buffer + sizeof(message_header), (byte const *)message + offset, length);
offset += length;
/* (no batch in a batch) */
if (((struct distributed_message_header const *)buffer)->type != _distributed_message_batch)
network_distributed_handle_message(machine_index, buffer, (word)(sizeof(message_header) + length));
}
return;
}
/* (entries of a size of their own: their least here, and each read no
further than the message's end) */
switch (header.type)
{
case _distributed_message_player_prediction:
case _distributed_message_unit_states: entry_size = DISTRIBUTED_UNIT_STATE_MINIMUM_SIZE; break;
case _distributed_message_player_statistics: entry_size = sizeof(struct distributed_player_statistics); break;
case _distributed_message_pickups: entry_size = sizeof(struct distributed_pickup); break;
case _distributed_message_player_inputs: entry_size = sizeof(struct distributed_player_input); break;
case _distributed_message_relayed_actions: entry_size = DISTRIBUTED_RELAYED_ACTION_MINIMUM_SIZE; break;
case _distributed_message_game_state:
case _distributed_message_objects_synchronized:
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_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;
}
if (header.type == 0 || header.type >= NUMBER_OF_DISTRIBUTED_MESSAGES ||
size < sizeof(header) + header.count * entry_size)
{
return;
}
distributed_statistics.received++;
/* (each kind from the host, or from a client) */
switch (header.type)
{
case _distributed_message_player_prediction:
case _distributed_message_client_ready:
case _distributed_message_client_identity:
case _distributed_message_hit_reports:
case _distributed_message_vehicle_prediction:
case _distributed_message_player_inputs:
if (machine_index == NONE || game_connection() != _game_connection_network_server)
return;
break;
default:
if (game_connection() != _game_connection_network_client)
return;
/* (the host's latest tick, which this client's input messages tell
it back) */
if (distributed_host_time == NONE || header.game_time > distributed_host_time)
distributed_host_time = header.game_time;
break;
}
if (distributed_message_stale(machine_index, &header))
return;
/* (the host: a client's clock, by its messages' ticks; and its players'
predictions not taken while its game runs fast) */
if (machine_index != NONE)
{
distributed_note_client_clock(machine_index, header.game_time);
if ((header.type == _distributed_message_player_prediction ||
header.type == _distributed_message_vehicle_prediction) &&
distributed_machine_clock_fast(machine_index))
{
return;
}
}
switch (header.type)
{
case _distributed_message_player_prediction:
distributed_handle_predictions(machine_index, header.game_time, (byte const *)entries,
(byte const *)message + size, header.count);
break;
case _distributed_message_unit_states:
distributed_handle_unit_states((byte const *)entries, (byte const *)message + size, header.count);
break;
case _distributed_message_player_statistics:
{
/* the host's count of kills, deaths, ... */
struct distributed_player_statistics const *players = (struct distributed_player_statistics const *)entries;
for (index = 0; index < header.count; index++)
{
struct player_datum *player = distributed_player(players[index].player_index);
if (player)
player->statistics = players[index].statistics;
}
break;
}
case _distributed_message_inventories:
network_objects_handle_inventories(entries, header.count);
break;
case _distributed_message_object_changes:
network_objects_handle_changes(entries, header.count);
break;
case _distributed_message_object_states:
network_objects_handle_states(entries, header.count);
break;
case _distributed_message_game_state:
game_engine_read_network_state((byte const *)entries, size - sizeof(header));
break;
case _distributed_message_objects_synchronized:
network_objects_handle_synchronized();
break;
case _distributed_message_client_identity:
/* (a client's Discord user, as it tells it: kept only of what is
allowed, whatever it sent) */
if (header.count >= 1 && machine_index >= 0 && machine_index < HALO_PORT_MAXIMUM_NETWORK_MACHINES)
{
struct distributed_client_identity identity;
csmemcpy(&identity, entries, sizeof(identity));
identity.discord_id[sizeof(identity.discord_id) - 1] = 0;
identity.discord_name[sizeof(identity.discord_name) - 1] = 0;
p2p_discord_sanitize(distributed_client_identities[machine_index].discord_id,
sizeof(distributed_client_identities[machine_index].discord_id), identity.discord_id, 0);
p2p_discord_sanitize(distributed_client_identities[machine_index].discord_name,
sizeof(distributed_client_identities[machine_index].discord_name), identity.discord_name, 1);
}
break;
case _distributed_message_notice:
{
/* the host's text, in red on the console: printable, and ended */
char text[MAXIMUM_NOTICE_LENGTH];
long length = size - sizeof(header);
long index;
if (length > MAXIMUM_NOTICE_LENGTH - 1)
length = MAXIMUM_NOTICE_LENGTH - 1;
csmemcpy(text, entries, length);
text[length] = 0;
for (index = 0; index < length && text[index]; index++)
{
if (text[index] < 32 || text[index] > 126)
text[index] = '?';
}
console_warning("%s", text);
error(_error_log, "the host: %s", text);
break;
}
case _distributed_message_client_ready:
{
boolean loaded = distributed_machine_loaded(machine_index);
/* (its count 1: the client asks again, having failed to make one of
the host's objects) */
network_objects_client_asked(machine_index, header.count != 0);
/* (a machine new at its index, which may have joined the game in
progress: every player's statistics and the game type's state, after
the objects it names) */
if (loaded)
{
distributed_send_all_statistics(machine_index);
distributed_send_game_state(machine_index);
}
break;
}
case _distributed_message_damage_events:
network_damage_handle_events(entries, header.count);
break;
case _distributed_message_hit_reports:
network_damage_handle_reports(machine_index, entries, header.count);
break;
case _distributed_message_vehicle_prediction:
network_objects_handle_vehicle_prediction(machine_index, entries, header.count);
break;
case _distributed_message_player_inputs:
distributed_handle_inputs(machine_index, (struct distributed_player_input const *)entries, header.count);
break;
case _distributed_message_relayed_actions:
distributed_handle_actions((byte const *)entries, (byte const *)message + size, header.count);
break;
case _distributed_message_pickups:
{
/* what the host says this machine's players picked up */
struct distributed_pickup const *pickups = (struct distributed_pickup const *)entries;
for (index = 0; index < header.count; index++)
{
long player_index = distributed_player_from_byte(pickups[index].player_index);
if (distributed_player_is_local(player_index))
{
network_player_show_pickup(player_index, pickups[index].kind, pickups[index].definition_index,
pickups[index].count);
}
}
break;
}
}
}