Fix third netcode review

This commit is contained in:
MrBruh committed 2026-10-01 04:57:51 +13:00
1 parent 0d548798e5
commit 4f3ce00a8e
19 files changed
+698 -241

No files matched your search

+17 -13
View File
@@ -34,9 +34,11 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick:
that with where it had the player at that tick, not now, and moves the
player by the difference, keeping what they did since. The host takes a
client's player where the client says within a tolerance of its own,
and no further from where it last took them than a player moves in the
client's ticks since (so the tolerance is not gained again each tick);
a teleporter, which moves the host's own copy too, starts afresh.
and no further from an anchor (one it took, a newer once a second) than
a player moves in the client's ticks since, those no more than its own
since and a little jitter (so the tolerance and the jitter are not gained
again each tick); a teleporter, which moves the host's own copy too,
starts afresh.
- **Shooter's hits.** A client reports what its own players hit; the host
checks the report (the player's, a weapon they carry, the target where
the host had it when the shooter saw it, no faster than weapons fire) and
@@ -124,10 +126,11 @@ a pregame keep-alive every five seconds from the host
only that client is told;
- twice a second and with every kill, the players' statistics that
changed (once a second a few more, round them all, as the message is
unreliable); when it changes (looked at five times a second) and once a
second, the game type's state (scores, the flags, the balls and their
carriers, the king's hill, the players' speeds) and whether the game is
over. A machine that has loaded is sent all of both at once. It counts
unreliable); when it changes (looked at five times a second, sent at
most every other look unless the game ended; reliably, so never
again unchanged), the game type's state (scores, the flags, the balls
and their carriers, the king's hill, the players' speeds) and whether
the game is over. A machine that has loaded is sent all of both at once. It counts
the game type's events (captures, grabs and returns of the flags,
laps, the balls reset), and a client announces those it has not had,
as the host does its own; a client runs the rest of the game type's
@@ -173,11 +176,12 @@ a pregame keep-alive every five seconds from the host
drawn fading over a few ticks (`render_interpolation.c`) instead of a
jump. A client drives its own player's vehicle and sends where it is,
which the host takes within a tolerance, as it does its own player's
unit, and no further from where it last took it than the vehicle moves
in the client's ticks since (its speed, the host's copy's or twice the
tag's top speed, whichever is more, no more than 3 world units a tick,
and a tenth more a tick, with the blend distance; a teleporter falls
back to the tolerance). The host sends the client its own vehicle
unit, and no further from an anchor (one it took, a newer once a
second) than the vehicle moves in the client's ticks since, those no
more than its own since and a little jitter (its speed, the host's
copy's or twice the tag's top speed, whichever is more, no more than 3
world units a tick, and a tenth more a tick, with the blend distance; a
teleporter falls back to the tolerance). The host sends the client its own vehicle
every third tick, with the client's tick it took the vehicle at
(`_distributed_object_predicted_bit` and a 16-bit time in the object's
state); the client compares that with where it had the vehicle at that
@@ -271,7 +275,7 @@ Unity's Netcode for Entities, lightyear, netfox and the Ares source):
round all of them, four a tick; the players' statistics when
they change (with every kill, twice a second), with sixteen more
players' once a second round them all; the game type's state when it
changes, and once a second.
changes, at most two and a half times a second.
- **The players each client needs, when it needs them.** The host sends a
client every player's unit and input every tick when they are within 25
world units of the client's own players, every second tick within 60,
+4
View File
@@ -355,6 +355,10 @@ Only machines with the invite can find the game:
(`src/p2p_signal.c`, `src/p2p_crypto.c`). The host authenticates its answer
with a key that only it and the player can calculate. Its public key must
agree with the identifier in the link.
- Then the player shows in the same way that it has the private key of its
public key. Only then does the host make a session for the player. Thus
other machines with the invite cannot make sessions in the name of a
player (such a session would keep the player out).
- Each two machines get the keys of their packets from their key pairs and
a random number from each. The keys do not go through the brokers. Thus
other machines with the invite cannot read or change the packets.
+30 -6
View File
@@ -452,7 +452,8 @@ void network_damage_aftermath(
short body_part,
short node_index,
short region_index,
short material_index)
short material_index,
long victim_player_index)
{
struct distributed_damage_event *event;
struct unit_datum *unit;
@@ -464,8 +465,9 @@ void network_damage_aftermath(
unit = (struct unit_datum *)object_try_and_get_and_verify_type(object_index, _object_mask_unit);
if (!unit)
return;
/* (the last events kept for killing blows) */
kill = TEST_FLAG(being_damaged_flags, _object_being_damaged_body_depleted_bit) && unit->unit.player_index != NONE;
/* (the last events kept for killing blows: of a player's unit, whose
player the blow's aftermath has already taken from it, unit_died) */
kill = TEST_FLAG(being_damaged_flags, _object_being_damaged_body_depleted_bit) && victim_player_index != NONE;
if (damage_event_count >= MAXIMUM_DAMAGE_EVENTS_PER_TICK - (kill ? 0 : RESERVED_KILL_EVENTS))
return;
event = &damage_events[damage_event_count++];
@@ -490,7 +492,7 @@ void network_damage_aftermath(
event->kind = _damage_event_kill;
/* (no killer when the game noted none: a death it did not score) */
if (!distributed_get_death((short)DATUM_INDEX_TO_ABSOLUTE_INDEX(unit->unit.player_index),
if (!distributed_get_death((short)DATUM_INDEX_TO_ABSOLUTE_INDEX(victim_player_index),
&event->player_index, &friendly_fire, &killed_by_vehicle))
{
event->player_index = NO_PLAYER;
@@ -1234,8 +1236,30 @@ static void distributed_note_event_destinations(
}
/* (a player's body, living or dead) */
unit = (struct unit_datum *)object_try_and_get_and_verify_type(event->object_index, _object_mask_unit);
if (unit && unit->unit.player_index != NONE)
player_indices[(*count)++] = (short)DATUM_INDEX_TO_ABSOLUTE_INDEX(unit->unit.player_index);
if (unit)
{
long unit_player_index = unit->unit.player_index;
/* (a body's player is no longer its unit's, but has it as their
dead one, player_died) */
if (unit_player_index == NONE)
{
struct data_iterator iterator;
struct player_datum *player;
data_iterator_new(&iterator, player_data);
while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL)
{
if (player->dead_unit_index == event->object_index)
{
unit_player_index = iterator.datum_index;
break;
}
}
}
if (unit_player_index != NONE)
player_indices[(*count)++] = (short)DATUM_INDEX_TO_ABSOLUTE_INDEX(unit_player_index);
}
if (tick->time != game_time_get())
continue;
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++)
+56 -31
View File
@@ -90,7 +90,10 @@ enum
STATISTICS_REFRESH_PLAYERS = 16,
/* the game type's state looked at, and sent unchanged */
GAME_STATE_INTERVAL_TICKS = 6,
GAME_STATE_REFRESH_TICKS = TICKS_PER_SECOND,
/* ... 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,
@@ -113,9 +116,13 @@ enum
/* 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 between two of its own
beyond the host's (its messages delayed, then bunched) */
/* ... 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,
/* ... 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,
};
/* struct distributed_unit_state flags */
@@ -383,9 +390,10 @@ static struct
long taken_time;
long taken_host_time;
} distributed_predictions[MAXIMUM_TRACKED_PLAYERS];
/* the host: where it last took each client's player to be (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 where that left its unit */
/* 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;
@@ -393,7 +401,8 @@ static struct
long time;
long host_time;
real_point3d position;
real_point3d host_position;
long taken_host_time;
real_point3d taken_host_position;
} distributed_accepted[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) */
@@ -450,8 +459,10 @@ static long distributed_player_machines[MAXIMUM_TRACKED_PLAYERS];
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, and when */
/* 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
@@ -1529,11 +1540,12 @@ static void distributed_handle_predictions(
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 last one taken than the player moves in the client's
ticks since (MAXIMUM_PREDICTED_SPEED), those no more than the host's since
and a little jitter. A teleporter moves the host's own unit too, further
than that since the last one taken: it is measured from where the host has
it only (as it is after a new life, or a ride). */
further from the anchor (one taken, a newer once a second) than the player
moves in the client's ticks since (MAXIMUM_PREDICTED_SPEED), those no more
than the host's since and a little jitter. 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)
{
@@ -1568,16 +1580,17 @@ static void distributed_apply_predictions(
continue;
if (distributed_accepted[player_index].valid)
{
long host_ticks = now - distributed_accepted[player_index].host_time;
long ticks = MIN(distributed_predictions[player_index].time - distributed_accepted[player_index].time,
host_ticks + PREDICTION_JITTER_TICKS);
real reach = MAXIMUM_PREDICTED_SPEED * (real)host_ticks + HOST_BLEND_DISTANCE;
now - distributed_accepted[player_index].host_time + PREDICTION_JITTER_TICKS);
real reach = MAXIMUM_PREDICTED_SPEED * (real)(now - distributed_accepted[player_index].taken_host_time) +
HOST_BLEND_DISTANCE;
dx = object->object.position.x - distributed_accepted[player_index].host_position.x;
dy = object->object.position.y - distributed_accepted[player_index].host_position.y;
dz = object->object.position.z - distributed_accepted[player_index].host_position.z;
/* (the host's own unit moved further than a unit moves: taken
from where it is) */
dx = object->object.position.x - distributed_accepted[player_index].taken_host_position.x;
dy = object->object.position.y - distributed_accepted[player_index].taken_host_position.y;
dz = object->object.position.z - distributed_accepted[player_index].taken_host_position.z;
/* (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) */
if (!(dx * dx + dy * dy + dz * dz <= reach * reach))
distributed_accepted[player_index].valid = FALSE;
else
@@ -1596,12 +1609,18 @@ static void distributed_apply_predictions(
{
distributed_predictions[player_index].taken_time = distributed_predictions[player_index].time;
distributed_predictions[player_index].taken_host_time = now;
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].host_position = object->object.position;
/* (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;
}
}
}
@@ -2527,8 +2546,8 @@ static void distributed_send_all_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 or it has not
been sent for a second */
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)
{
@@ -2549,11 +2568,16 @@ static void distributed_send_game_state(
return;
}
checksum = distributed_checksum(message.data, size);
if (checksum == distributed_game_state_checksum && distributed_game_state_time != NONE &&
game_time_get() - distributed_game_state_time < GAME_STATE_REFRESH_TICKS)
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),
@@ -2584,6 +2608,7 @@ void network_distributed_new_game(
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++)
+66 -44
View File
@@ -100,9 +100,9 @@ enum
up to the most */
CLIENT_READY_INTERVAL_TICKS = TICKS_PER_SECOND,
CLIENT_READY_MAXIMUM_INTERVAL_TICKS = 4 * TICKS_PER_SECOND,
/* (a machine that does not say whether it asks again: network_objects_
client_ready) the host tells it all its objects at most once in this
long (asked again sooner, they are on their way) */
/* the host tells a machine all its objects again at most once in this
long, asked again (a client that failed to make one: asked again sooner,
they are on their way; a flood of asking is not answered in full) */
HOST_OBJECTS_RESEND_TICKS = 10 * TICKS_PER_SECOND,
/* a client that failed to make one of the host's objects asks for them
all again this long after, twice as long each time it fails again up to
@@ -126,15 +126,17 @@ enum
/* a client: where the vehicles its own players drive were, the last
ticks (a power of two, more than the longest round trip) */
OWN_VEHICLE_POSITION_TICKS = 64,
/* the host: the ticks after it took a client's vehicle that it still
tells the client which of its ticks it has it at */
VEHICLE_PREDICTION_ECHO_TICKS = 3,
/* ... and the client's ticks it takes as run between two it took beyond
the host's (its messages delayed, then bunched) */
/* the host: the client's ticks it takes as run since a client's vehicle's
anchor beyond its own (its messages delayed, then bunched) */
VEHICLE_PREDICTION_JITTER_TICKS = 6,
/* ... and how long it measures the next from the last it took (longer,
from where it has the vehicle only: a ride begun again) */
VEHICLE_PREDICTION_REFERENCE_TICKS = TICKS_PER_SECOND,
/* ... 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) */
VEHICLE_PREDICTION_ANCHOR_TICKS = TICKS_PER_SECOND,
/* ... and how long, with none taken, it keeps the anchor, and tells the
client which of its ticks it has the vehicle at (longer: from where it
has the vehicle only, a ride begun again) */
VEHICLE_PREDICTION_REFERENCE_TICKS = 2 * TICKS_PER_SECOND,
/* a client: its own player's ammunition and grenades are the host's once
the host has had this long past a round trip to see what it did with
them (the host's inventories go every INVENTORY_INTERVAL_TICKS) */
@@ -312,20 +314,22 @@ static struct
long player_indices[MAXIMUM_LOCAL_PLAYERS];
} objects_host_machines[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
/* ... each client's player's latest vehicle prediction, taken at the next
tick; whether it took the latest it had; the vehicle it last took (NONE:
none), at which of the client's ticks and its own, where the client had it
and where that left the host's */
tick; the vehicle it last took (NONE: none), at which of the client's ticks
and its own, and where that left the host's copy; and the anchor
predictions are measured from: one it took, at which of the client's ticks
and its own, and where the client had it */
static struct
{
boolean valid;
boolean echo;
long machine_index;
struct distributed_object_state state;
long accepted_vehicle_index;
word accepted_time;
long accepted_host_time;
real_point3d accepted_position;
real_point3d accepted_host_position;
word anchor_time;
long anchor_host_time;
real_point3d anchor_position;
} objects_host_vehicle_predictions[MAXIMUM_TRACKED_PLAYERS];
/* a client: the host's objects it has, by absolute index */
static long objects_client_has[MAXIMUM_TRACKED_OBJECTS];
@@ -820,7 +824,8 @@ void network_objects_client_asked(
return;
player_list = machine_get_player_list(machine_index);
/* (another machine in its place has other players) */
if (again || objects_host_machines[machine_index].time == NONE ||
if ((again && game_time_get() - objects_host_machines[machine_index].time >= HOST_OBJECTS_RESEND_TICKS) ||
objects_host_machines[machine_index].time == NONE ||
csmemcmp(objects_host_machines[machine_index].player_indices, player_list,
sizeof(objects_host_machines[machine_index].player_indices)) != 0)
{
@@ -1050,11 +1055,12 @@ static void distributed_host_send_states(
{
short player_index = objects_host_viewers.machines[machine_number].vehicle_player_indices[own_vehicle];
/* (the client's clock against the host's, which a gap or a
prediction refused since leaves as it was) */
if (player_index >= 0 && player_index < MAXIMUM_TRACKED_PLAYERS &&
objects_host_vehicle_predictions[player_index].echo &&
objects_host_vehicle_predictions[player_index].accepted_vehicle_index == state->object_index &&
game_time_get() - objects_host_vehicle_predictions[player_index].accepted_host_time <
VEHICLE_PREDICTION_ECHO_TICKS)
VEHICLE_PREDICTION_REFERENCE_TICKS)
{
SET_FLAG(state->flags, _distributed_object_predicted_bit, TRUE);
state->time = (short)(word)(objects_host_vehicle_predictions[player_index].accepted_time +
@@ -1307,13 +1313,14 @@ void network_objects_handle_vehicle_prediction(
still drives it: a little off closed by half, more put there. Each taken
moves the host's copy, and the next is measured from there, so a client
could take its vehicle HOST_VEHICLE_ACCEPT_TOLERANCE further each tick: a
prediction is also no further from the last one taken than the vehicle goes
in the client's ticks since (those no more than the host's since and a
little jitter), and no faster: as fast as twice its tag's top speed, or as
the host's copy goes (falling, thrown), whichever is faster, with a margin.
A teleporter moves the host's copy too, further than that since the last one
taken: it is measured from where the host has it only. Which of the
client's ticks the host has it at is noted, to tell the client. */
prediction is also no further from the anchor (one taken, a newer once a
second) than the vehicle goes in the client's ticks since (after it, and no
more than the host's since and a little jitter), and no faster: as fast as
twice its tag's top speed, or as the host's copy goes (falling, thrown),
whichever is faster, with a margin. A teleporter moves the host's copy too,
further than it goes since the last taken: it is measured from where the
host has it only, a new anchor. Which of the client's ticks the host has it at is noted, to tell
the client. */
void network_objects_apply_vehicle_predictions(
void)
{
@@ -1328,11 +1335,11 @@ void network_objects_apply_vehicle_predictions(
real_vector3d forward, up, velocity, angular_velocity;
real dx, dy, dz;
real speed;
boolean anchored;
if (!objects_host_vehicle_predictions[player_index].valid)
continue;
objects_host_vehicle_predictions[player_index].valid = FALSE;
objects_host_vehicle_predictions[player_index].echo = FALSE;
vehicle = (struct unit_datum *)object_try_and_get_and_verify_type(state->object_index, _object_mask_vehicle);
if (!vehicle || vehicle->unit.driver_object_index == NONE || vehicle->object.parent_object_index != NONE)
continue;
@@ -1363,27 +1370,36 @@ void network_objects_apply_vehicle_predictions(
speed = top_speed;
speed = MIN(speed, MAXIMUM_PREDICTED_VEHICLE_SPEED);
}
if (objects_host_vehicle_predictions[player_index].accepted_vehicle_index == state->object_index &&
now - objects_host_vehicle_predictions[player_index].accepted_host_time < VEHICLE_PREDICTION_REFERENCE_TICKS)
anchored = objects_host_vehicle_predictions[player_index].accepted_vehicle_index == state->object_index &&
now - objects_host_vehicle_predictions[player_index].anchor_host_time < VEHICLE_PREDICTION_REFERENCE_TICKS;
if (anchored)
{
long host_ticks = now - objects_host_vehicle_predictions[player_index].accepted_host_time;
long ticks = MIN((long)(short)(word)((word)state->time - objects_host_vehicle_predictions[player_index].accepted_time),
host_ticks + VEHICLE_PREDICTION_JITTER_TICKS);
real reach = (speed + PREDICTED_VEHICLE_SPEED_MARGIN) * (real)host_ticks + HOST_VEHICLE_BLEND_DISTANCE;
real_point3d const *accepted = &objects_host_vehicle_predictions[player_index].accepted_host_position;
real_point3d const *from = &objects_host_vehicle_predictions[player_index].accepted_host_position;
dx = vehicle->object.position.x - accepted->x;
dy = vehicle->object.position.y - accepted->y;
dz = vehicle->object.position.z - accepted->z;
/* (the host's copy moved further than it goes: taken from where
it is) */
if (dx * dx + dy * dy + dz * dz <= reach * reach)
dx = vehicle->object.position.x - from->x;
dy = vehicle->object.position.y - from->y;
dz = vehicle->object.position.z - from->z;
/* (the host's copy moved further than it goes since the last
taken, which only the host moves it by: taken from where it is,
a new anchor) */
if (!(dx * dx + dy * dy + dz * dz <= reach * reach))
{
anchored = FALSE;
}
else
{
/* (a client's clock that jumped counts no more than the
host's ticks since and the jitter) */
long ticks = MIN((long)(short)(word)((word)state->time - objects_host_vehicle_predictions[player_index].anchor_time),
now - objects_host_vehicle_predictions[player_index].anchor_host_time + VEHICLE_PREDICTION_JITTER_TICKS);
reach = (speed + PREDICTED_VEHICLE_SPEED_MARGIN) * (real)ticks + HOST_VEHICLE_BLEND_DISTANCE;
accepted = &objects_host_vehicle_predictions[player_index].accepted_position;
dx = state->position.x - accepted->x;
dy = state->position.y - accepted->y;
dz = state->position.z - accepted->z;
from = &objects_host_vehicle_predictions[player_index].anchor_position;
dx = state->position.x - from->x;
dy = state->position.y - from->y;
dz = state->position.z - from->z;
if (ticks <= 0 || !(dx * dx + dy * dy + dz * dz <= reach * reach))
continue;
}
@@ -1396,12 +1412,18 @@ void network_objects_apply_vehicle_predictions(
continue;
network_objects_reconcile(state->object_index, &state->position, &forward, &up, &velocity, &angular_velocity,
HOST_VEHICLE_BLEND_DISTANCE);
objects_host_vehicle_predictions[player_index].echo = TRUE;
objects_host_vehicle_predictions[player_index].accepted_vehicle_index = state->object_index;
objects_host_vehicle_predictions[player_index].accepted_time = (word)state->time;
objects_host_vehicle_predictions[player_index].accepted_host_time = now;
objects_host_vehicle_predictions[player_index].accepted_position = state->position;
objects_host_vehicle_predictions[player_index].accepted_host_position = vehicle->object.position;
/* (a new anchor: the first, one a second on, or after a jump) */
if (!anchored ||
now - objects_host_vehicle_predictions[player_index].anchor_host_time >= VEHICLE_PREDICTION_ANCHOR_TICKS)
{
objects_host_vehicle_predictions[player_index].anchor_time = (word)state->time;
objects_host_vehicle_predictions[player_index].anchor_host_time = now;
objects_host_vehicle_predictions[player_index].anchor_position = state->position;
}
}
}
+17 -4
View File
@@ -13,9 +13,10 @@ link games as if they were on one LAN, without a server of this project's.
- Signalling (p2p_signal.c) goes through public MQTT brokers, on topics
that are hashes of the token, with messages sealed with a key derived
from it (p2p_crypto.c). A joiner offers its public key and the addresses
it can be reached at; the host answers with its own. The secret of their
session comes from the two keys (X25519) and a nonce of each, and never
travels.
it can be reached at; the host answers with its own, and makes the
session once the joiner, answered, proves that it holds its key. The
secret of their session comes from the two keys (X25519) and a nonce of
each, and never travels.
- The tunnel is one UDP socket. Each machine learns its public address from
public STUN servers, and both then send to each other's addresses until
packets get through (hole punching). There is no relay: two machines whose
@@ -1213,7 +1214,8 @@ static int find_closed(int stream, unsigned long *address, unsigned short *port)
return 0;
}
/* a stand-in that opens has a port no closed one stands for now */
/* a stand-in, or a socket of the game's, that has the port now: no closed
stand-in stands for it */
static void forget_closed(int stream, unsigned short local_port)
{
int index;
@@ -1570,6 +1572,17 @@ void p2p_socket_port(int socket, int stream, int listening, unsigned short port)
/* the game listens for connections while it hosts */
if (stream && listening)
p2p.hosting_socket = socket;
/* (and no stand-in that closed has the port now) */
forget_closed(stream != 0, port);
pthread_mutex_unlock(&p2p_lock);
}
void p2p_port_taken(int stream, unsigned short port)
{
if (!p2p.running || !port)
return;
pthread_mutex_lock(&p2p_lock);
forget_closed(stream != 0, port);
pthread_mutex_unlock(&p2p_lock);
}
+3
View File
@@ -60,6 +60,9 @@ int p2p_broadcast_datagram(unsigned short source_port, unsigned short port, cons
(stream and listening: it is hosting). Peers reach only these ports (a
stream's only while it listens), and datagram ports it sent them from */
void p2p_socket_port(int socket, int stream, int listening, unsigned short port);
/* a socket that peers are not to reach (bound to 127.0.0.1 alone) has this
local port: traffic from it is not a peer's whose stand-in had the port */
void p2p_port_taken(int stream, unsigned short port);
/* the game closes a socket */
void p2p_socket_closed(int socket);
+307 -106
View File
@@ -10,27 +10,40 @@ Everything that passes through them is sealed with a key derived from the
invite's token, and goes to topics that are hashes of it, so the brokers
(and anyone watching them) learn nothing and can join nothing:
- the host listens on hceu/2/<HMAC(token, "host" | host)>, where a joiner
- the host listens on hceu/3/<HMAC(token, "host" | host)>, where a joiner
sends JOIN: its public key (its identifier is the key's hash), a nonce,
and its addresses;
- the joiner listens on hceu/2/<HMAC(token, "joiner" | joiner)>, where the
- the joiner listens on hceu/3/<HMAC(token, "joiner" | joiner)>, where the
host answers ACCEPT: its public key, the joiner's nonce, one of its own,
its addresses, and a tag that only the two of them can make (from their
keys).
keys);
- the joiner then repeats its JOIN with the host's nonce and a tag of its
own, made the same way, which shows that it holds the key it gave. Only
then does the host make a session.
Their tunnel's keys come from their X25519 shared secret and the two
nonces, and never travel: another holder of the invite reads the messages
but cannot work them out, and cannot answer as the host (whose key must
hash to the identifier in the invite). It can send a JOIN in another
machine's name, which gets it nothing: the tunnel takes that machine's key,
and a session with a machine is not replaced while it lives (p2p.c).
machine's name, but cannot prove it: the host answers it (to that machine)
and makes no session of it. A session no one could complete would keep that
machine out while it lived (a session with a machine is not replaced while
it lives, p2p.c), and a JOIN every so often would keep it out for good.
The host's nonce is a hash of the request with a key of the host's and the
time (it changes every HOST_NONCE_PERIOD), so the host need not remember
what it answered: anyone with the invite can ask in a machine's name as
often as it likes, pushing out whatever the host remembered. What it does
keep of requests not proven yet (the work of their keys) only saves work.
A joiner repeats its JOIN until the tunnel reaches the host. The host makes
one session of a JOIN (its public key and nonce) at most: anyone watching
the brokers could send it again after the session ended, and have the host
reach for the joiner's old addresses in its name, keeping it out. So a
joiner asks with a new nonce when its session with the host ends before the
tunnel reached it, or when the host has not answered it in a while.
one session of a request (a public key and nonce) at most: anyone watching
the brokers could send the proven JOIN again after the session ended, and
have the host reach for the joiner's old addresses in its name, keeping it
out (with the same keys again). It remembers the request at least while its
host nonce lasts. So a joiner asks with a new nonce when its session with
the host ends before the tunnel reached it, or when the host has not
answered it in a while.
*/
#include "platform.h"
@@ -45,16 +58,22 @@ tunnel reached it, or when the host has not answered it in a while.
enum
{
MAXIMUM_BROKERS = 4,
/* the joiners a host remembers, so a repeated request gets the same
session: as many as it takes */
/* the joiners a host made sessions for, so a repeated request gets the
same session: as many as it takes */
MAXIMUM_JOINERS = P2P_MAXIMUM_PEERS + 1,
/* the requests not proven yet whose keys' work a host keeps; only to
save work (a flood of requests pushes them out) */
MAXIMUM_ASKERS = 16,
/* the requests a session was made from, which make none again (a
host takes at most a few new players a minute: this is hours of them) */
host takes at most a few new players a minute: this is hours of them;
one is not forgotten while its host nonce lasts: USED_REQUEST_TIME) */
MAXIMUM_USED_REQUESTS = 1024,
TOPIC_SIZE = 7 + 32 + 1,
NONCE_SIZE = 8,
/* an ACCEPT's tag: the first half of an HMAC-SHA256 */
/* an ACCEPT's or a proven JOIN's tag: the first half of an HMAC-SHA256 */
TAG_SIZE = 16,
/* a proven JOIN's end: the host's nonce, and the tag */
PROOF_SIZE = NONCE_SIZE + TAG_SIZE,
BUFFER_SIZE = 4096,
MAXIMUM_MESSAGE_SIZE = 256,
@@ -67,9 +86,12 @@ enum
JOIN_INTERVAL = 2000,
ANSWER_INTERVAL = 1000,
/* a joiner the host has not answered in this long asks anew, with a
new nonce (the host may have made a session from its request whose
answer was lost, and makes no other from it) */
new nonce (a fresh start: the host makes nothing of a request it did
not answer, as it makes a session only of a proven one) */
UNANSWERED_TIME = 20000,
/* a host nonce is taken in the period it is made in and the next */
HOST_NONCE_PERIOD = 30000,
USED_REQUEST_TIME = 2 * HOST_NONCE_PERIOD,
};
enum
@@ -84,7 +106,8 @@ enum
{
_message_join = 'J',
_message_accept = 'A',
MESSAGE_VERSION = 2,
/* 3: a JOIN proves its key before the host makes a session */
MESSAGE_VERSION = 3,
};
struct broker
@@ -109,6 +132,8 @@ struct broker
int output_size;
};
/* a request: of a session made (joiners), or not proven yet (askers, which
have no host nonce or secret) */
struct joiner
{
unsigned char identifier[P2P_IDENTIFIER_SIZE];
@@ -123,6 +148,13 @@ struct joiner
int used;
};
struct used_request
{
/* the joiner's identifier and nonce */
unsigned char request[P2P_IDENTIFIER_SIZE + NONCE_SIZE];
unsigned long time;
};
static struct
{
int started;
@@ -137,9 +169,14 @@ static struct
char host_topic[TOPIC_SIZE];
struct joiner joiners[MAXIMUM_JOINERS];
int next_joiner;
/* the joiner's identifier and nonce of each request a session was made
from (kept while hosting stops and starts: an invite lasts the run) */
unsigned char used_requests[MAXIMUM_USED_REQUESTS][P2P_IDENTIFIER_SIZE + NONCE_SIZE];
struct joiner askers[MAXIMUM_ASKERS];
int next_asker;
/* the key of the host's nonces (host_nonce_for), for the run */
int has_nonce_key;
unsigned char nonce_key[P2P_SHA256_SIZE];
/* each request a session was made from (kept while hosting stops and
starts: an invite lasts the run) */
struct used_request used_requests[MAXIMUM_USED_REQUESTS];
int used_request_count;
int used_request_next;
@@ -155,9 +192,11 @@ static struct
char join_host_topic[TOPIC_SIZE];
char join_topic[TOPIC_SIZE];
unsigned long join_sent_time;
/* when join_nonce was made, and whether the host answered it */
/* when join_nonce was made, and whether the host answered it (with
join_host_nonce, which its proof carries) */
unsigned long join_nonce_time;
int join_answered;
unsigned char join_host_nonce[NONCE_SIZE];
} signalling;
static int elapsed(unsigned long since, unsigned long time)
@@ -196,7 +235,7 @@ static void make_topic(const unsigned char *token, const char *label, const unsi
derive(token, label, identifier, digest);
p2p_hex(digest, 16, text);
snprintf(topic, TOPIC_SIZE, "hceu/2/%s", text);
snprintf(topic, TOPIC_SIZE, "hceu/3/%s", text);
}
/* ---------- MQTT */
@@ -463,18 +502,63 @@ static void session_secret(const unsigned char *base, const unsigned char *nonce
p2p_hmac_sha256(base, P2P_SHA256_SIZE, data, sizeof(data), secret);
}
/* an ACCEPT's tag, over what precedes it */
static void accept_tag(const unsigned char *base, const unsigned char *message, int size, unsigned char *tag)
/* an ACCEPT's tag (label "accept") or a proven JOIN's ("join"), over what
precedes it */
static void message_tag(const unsigned char *base, const char *label, const unsigned char *message, int size,
unsigned char *tag)
{
unsigned char data[6 + MAXIMUM_MESSAGE_SIZE];
unsigned char digest[P2P_SHA256_SIZE];
int label_size = (int)strlen(label);
memcpy(data, "accept", 6);
memcpy(data + 6, message, (size_t)size);
p2p_hmac_sha256(base, P2P_SHA256_SIZE, data, 6 + size, digest);
memcpy(data, label, (size_t)label_size);
memcpy(data + label_size, message, (size_t)size);
p2p_hmac_sha256(base, P2P_SHA256_SIZE, data, label_size + size, digest);
memcpy(tag, digest, TAG_SIZE);
}
/* whether a message's tag (its last TAG_SIZE bytes) is right */
static int tag_right(const unsigned char *base, const char *label, const unsigned char *message, int size)
{
unsigned char tag[TAG_SIZE];
message_tag(base, label, message, size - TAG_SIZE, tag);
return p2p_equal(tag, message + size - TAG_SIZE, TAG_SIZE);
}
/* the host's nonce for a request (a joiner's public key and nonce) in a
period of HOST_NONCE_PERIOD */
static void host_nonce_for(const unsigned char *public_key, const unsigned char *nonce, unsigned long period,
unsigned char *host_nonce)
{
unsigned char data[4 + P2P_KEY_SIZE + NONCE_SIZE];
unsigned char digest[P2P_SHA256_SIZE];
int index;
for (index = 0; index < 4; index++)
data[index] = (unsigned char)(period >> (index * 8));
memcpy(data + 4, public_key, P2P_KEY_SIZE);
memcpy(data + 4 + P2P_KEY_SIZE, nonce, NONCE_SIZE);
p2p_hmac_sha256(signalling.nonce_key, P2P_SHA256_SIZE, data, sizeof(data), digest);
memcpy(host_nonce, digest, NONCE_SIZE);
}
/* whether the host answered a request with this nonce lately: in this
period, or the last */
static int host_nonce_current(const unsigned char *public_key, const unsigned char *nonce,
const unsigned char *host_nonce)
{
unsigned long period = p2p_now() / HOST_NONCE_PERIOD;
unsigned char expected[NONCE_SIZE];
host_nonce_for(public_key, nonce, period, expected);
if (p2p_equal(expected, host_nonce, NONCE_SIZE))
return 1;
/* (the last before the clock wraps, before the first) */
host_nonce_for(public_key, nonce, period ? period - 1 : 0xFFFFFFFFUL / HOST_NONCE_PERIOD, expected);
return p2p_equal(expected, host_nonce, NONCE_SIZE);
}
/* ---------- the messages */
static void send_join(void)
@@ -490,107 +574,208 @@ static void send_join(void)
memcpy(message + size, signalling.join_nonce, NONCE_SIZE);
size += NONCE_SIZE;
size += put_candidates(message + size);
/* answered: the proof that this machine holds its key, of which the host
makes the session */
if (signalling.join_answered)
{
memcpy(message + size, signalling.join_host_nonce, NONCE_SIZE);
size += NONCE_SIZE;
message_tag(signalling.join_base, "join", message, size, message + size);
size += TAG_SIZE;
}
size = p2p_seal(signalling.join_key, message, size, sealed);
publish_everywhere(signalling.join_host_topic, sealed, size);
signalling.join_sent_time = p2p_now();
}
/* the host: a joiner asked */
static struct joiner *find_joiner(struct joiner *list, int count, const unsigned char *public_key,
const unsigned char *nonce)
{
int index;
for (index = 0; index < count; index++)
{
if (list[index].used && !memcmp(list[index].public_key, public_key, P2P_KEY_SIZE) &&
!memcmp(list[index].nonce, nonce, NONCE_SIZE))
{
return &list[index];
}
}
return NULL;
}
/* pair_base with a joiner: kept from a request of its, else worked out */
static int joiner_base(const unsigned char *public_key, unsigned char *base)
{
int index;
for (index = 0; index < MAXIMUM_JOINERS + MAXIMUM_ASKERS; index++)
{
struct joiner const *joiner = index < MAXIMUM_JOINERS ? &signalling.joiners[index] :
&signalling.askers[index - MAXIMUM_JOINERS];
if (joiner->used && !memcmp(joiner->public_key, public_key, P2P_KEY_SIZE))
{
memcpy(base, joiner->base, P2P_SHA256_SIZE);
return 1;
}
}
return pair_base(public_key, p2p_public_key(), public_key, base);
}
static int request_used(const unsigned char *request)
{
int index;
for (index = 0; index < signalling.used_request_count; index++)
{
if (!memcmp(signalling.used_requests[index].request, request, P2P_IDENTIFIER_SIZE + NONCE_SIZE))
return 1;
}
return 0;
}
/* the host's answer to a request */
static void send_accept(const unsigned char *identifier, const unsigned char *nonce, const unsigned char *host_nonce,
const unsigned char *base)
{
unsigned char answer[MAXIMUM_MESSAGE_SIZE];
unsigned char sealed[MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD];
char topic[TOPIC_SIZE];
int size = 0;
answer[size++] = _message_accept;
answer[size++] = MESSAGE_VERSION;
memcpy(answer + size, p2p_public_key(), P2P_KEY_SIZE);
size += P2P_KEY_SIZE;
memcpy(answer + size, nonce, NONCE_SIZE);
size += NONCE_SIZE;
memcpy(answer + size, host_nonce, NONCE_SIZE);
size += NONCE_SIZE;
size += put_candidates(answer + size);
message_tag(base, "accept", answer, size, answer + size);
size += TAG_SIZE;
size = p2p_seal(signalling.host_key, answer, size, sealed);
make_topic(signalling.host_token, "joiner", identifier, topic);
publish_everywhere(topic, sealed, size);
}
/* the host: a joiner asked; with a proof (the host's nonce, and a tag) once
the host answered it */
static void join_received(const unsigned char *message, int size)
{
struct p2p_candidate candidates[P2P_MAXIMUM_CANDIDATES];
unsigned char answer[MAXIMUM_MESSAGE_SIZE];
unsigned char sealed[MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD];
unsigned char identifier[P2P_IDENTIFIER_SIZE];
unsigned char request[P2P_IDENTIFIER_SIZE + NONCE_SIZE];
unsigned char host_nonce[NONCE_SIZE];
unsigned char base[P2P_SHA256_SIZE];
unsigned char secret[P2P_SHA256_SIZE];
const unsigned char *public_key = message + 2;
const unsigned char *nonce = public_key + P2P_KEY_SIZE;
int fixed = 2 + P2P_KEY_SIZE + NONCE_SIZE;
struct joiner *joiner = NULL;
char topic[TOPIC_SIZE];
int answered = 0;
struct joiner *joiner;
struct used_request *used;
int proven;
int count;
int index;
int answer_size = 0;
if (size < fixed + 1)
return;
count = get_candidates(message + fixed, size - fixed, candidates);
if (count < 0)
return;
proven = size - fixed - 1 - count * 6;
if (proven != 0 && proven != PROOF_SIZE)
return;
p2p_identifier_for(public_key, identifier);
for (index = 0; index < MAXIMUM_JOINERS; index++)
joiner = find_joiner(signalling.joiners, MAXIMUM_JOINERS, public_key, nonce);
if (joiner)
{
if (signalling.joiners[index].used &&
!memcmp(signalling.joiners[index].identifier, identifier, P2P_IDENTIFIER_SIZE))
/* a request a session was made from, again (through another broker,
or repeated until the tunnel reaches the host): that session's
answer, while it lasts; never another. The addresses only from a
proof (anyone can send the rest) */
if (proven && (memcmp(message + size - PROOF_SIZE, joiner->host_nonce, NONCE_SIZE) ||
!tag_right(joiner->base, "join", message, size)))
{
joiner = &signalling.joiners[index];
break;
}
}
if (joiner && !memcmp(joiner->public_key, public_key, P2P_KEY_SIZE) && !memcmp(joiner->nonce, nonce, NONCE_SIZE))
{
/* the same request again, through another broker or repeated: the
same session, while it lasts; never another (anyone watching the
brokers can send it again once the session ends, and a session in
the joiner's name would keep the joiner out while it lasts: a joiner
asks again with a new nonce) */
if (!elapsed(joiner->answered_time, ANSWER_INTERVAL) ||
!p2p_peer_reoffered(identifier, joiner->secret, candidates, count))
return;
answered = 1;
}
if (!answered)
{
/* a new session (p2p.c turns it away while another with that machine
lives), from a request no session was made from */
unsigned char base[P2P_SHA256_SIZE], host_nonce[NONCE_SIZE], secret[P2P_SHA256_SIZE];
unsigned char request[P2P_IDENTIFIER_SIZE + NONCE_SIZE];
memcpy(request, identifier, P2P_IDENTIFIER_SIZE);
memcpy(request + P2P_IDENTIFIER_SIZE, nonce, NONCE_SIZE);
for (index = 0; index < signalling.used_request_count; index++)
{
if (!memcmp(signalling.used_requests[index], request, sizeof(request)))
return;
}
if (!elapsed(joiner->answered_time, ANSWER_INTERVAL) ||
!p2p_peer_reoffered(identifier, joiner->secret, candidates, proven ? count : 0))
{
return;
}
joiner->answered_time = p2p_now();
send_accept(identifier, joiner->nonce, joiner->host_nonce, joiner->base);
return;
}
memcpy(request, identifier, P2P_IDENTIFIER_SIZE);
memcpy(request + P2P_IDENTIFIER_SIZE, nonce, NONCE_SIZE);
if (request_used(request))
return;
if (!proven)
{
/* an answer (to the machine whose key it is, which alone can prove
the request), and nothing else: the host keeps nothing of it that it
needs */
struct joiner *asker = find_joiner(signalling.askers, MAXIMUM_ASKERS, public_key, nonce);
if (asker && !elapsed(asker->answered_time, ANSWER_INTERVAL))
return;
/* (checked before the work of the keys, which anyone with the invite
can ask for as often as they like) */
if (p2p_peer_turned_away(identifier, 0) || !pair_base(public_key, p2p_public_key(), public_key, base))
if (p2p_peer_turned_away(identifier, 0))
return;
posix_random_bytes(host_nonce, NONCE_SIZE);
session_secret(base, nonce, host_nonce, secret);
if (!p2p_peer_offered(identifier, secret, candidates, count, 0))
return;
memcpy(signalling.used_requests[signalling.used_request_next], request, sizeof(request));
signalling.used_request_next = (signalling.used_request_next + 1) % MAXIMUM_USED_REQUESTS;
if (signalling.used_request_count < MAXIMUM_USED_REQUESTS)
signalling.used_request_count++;
if (!joiner)
joiner = &signalling.joiners[signalling.next_joiner++ % MAXIMUM_JOINERS];
memcpy(joiner->identifier, identifier, P2P_IDENTIFIER_SIZE);
memcpy(joiner->public_key, public_key, P2P_KEY_SIZE);
memcpy(joiner->nonce, nonce, NONCE_SIZE);
memcpy(joiner->host_nonce, host_nonce, NONCE_SIZE);
memcpy(joiner->base, base, P2P_SHA256_SIZE);
memcpy(joiner->secret, secret, P2P_SHA256_SIZE);
joiner->used = 1;
if (!asker)
{
if (!joiner_base(public_key, base))
return;
asker = &signalling.askers[signalling.next_asker];
signalling.next_asker = (signalling.next_asker + 1) % MAXIMUM_ASKERS;
memset(asker, 0, sizeof(*asker));
memcpy(asker->identifier, identifier, P2P_IDENTIFIER_SIZE);
memcpy(asker->public_key, public_key, P2P_KEY_SIZE);
memcpy(asker->nonce, nonce, NONCE_SIZE);
memcpy(asker->base, base, P2P_SHA256_SIZE);
asker->used = 1;
}
asker->answered_time = p2p_now();
host_nonce_for(public_key, nonce, p2p_now() / HOST_NONCE_PERIOD, host_nonce);
send_accept(identifier, nonce, host_nonce, asker->base);
return;
}
/* proven: with a nonce the host answered the request with lately, and a
tag only the key's holder can make. A new session (p2p.c turns it away
while another with that machine lives) */
memcpy(host_nonce, message + size - PROOF_SIZE, NONCE_SIZE);
if (!host_nonce_current(public_key, nonce, host_nonce) || p2p_peer_turned_away(identifier, 0) ||
!joiner_base(public_key, base) || !tag_right(base, "join", message, size))
{
return;
}
/* (a request is remembered while its proof lasts, at least: a copy of it
would make the session again, with the same keys) */
used = &signalling.used_requests[signalling.used_request_next];
if (signalling.used_request_count == MAXIMUM_USED_REQUESTS && !elapsed(used->time, USED_REQUEST_TIME))
return;
session_secret(base, nonce, host_nonce, secret);
if (!p2p_peer_offered(identifier, secret, candidates, count, 0))
return;
memcpy(used->request, request, sizeof(request));
used->time = p2p_now();
signalling.used_request_next = (signalling.used_request_next + 1) % MAXIMUM_USED_REQUESTS;
if (signalling.used_request_count < MAXIMUM_USED_REQUESTS)
signalling.used_request_count++;
joiner = &signalling.joiners[signalling.next_joiner];
signalling.next_joiner = (signalling.next_joiner + 1) % MAXIMUM_JOINERS;
memcpy(joiner->identifier, identifier, P2P_IDENTIFIER_SIZE);
memcpy(joiner->public_key, public_key, P2P_KEY_SIZE);
memcpy(joiner->nonce, nonce, NONCE_SIZE);
memcpy(joiner->host_nonce, host_nonce, NONCE_SIZE);
memcpy(joiner->base, base, P2P_SHA256_SIZE);
memcpy(joiner->secret, secret, P2P_SHA256_SIZE);
joiner->answered_time = p2p_now();
answer[answer_size++] = _message_accept;
answer[answer_size++] = MESSAGE_VERSION;
memcpy(answer + answer_size, p2p_public_key(), P2P_KEY_SIZE);
answer_size += P2P_KEY_SIZE;
memcpy(answer + answer_size, joiner->nonce, NONCE_SIZE);
answer_size += NONCE_SIZE;
memcpy(answer + answer_size, joiner->host_nonce, NONCE_SIZE);
answer_size += NONCE_SIZE;
answer_size += put_candidates(answer + answer_size);
accept_tag(joiner->base, answer, answer_size, answer + answer_size);
answer_size += TAG_SIZE;
answer_size = p2p_seal(signalling.host_key, answer, answer_size, sealed);
make_topic(signalling.host_token, "joiner", identifier, topic);
publish_everywhere(topic, sealed, answer_size);
joiner->used = 1;
send_accept(identifier, nonce, host_nonce, base);
}
/* the joiner: the host answered */
@@ -598,7 +783,6 @@ static void accept_received(const unsigned char *message, int size)
{
struct p2p_candidate candidates[P2P_MAXIMUM_CANDIDATES];
unsigned char identifier[P2P_IDENTIFIER_SIZE];
unsigned char tag[TAG_SIZE];
unsigned char secret[P2P_SHA256_SIZE];
const unsigned char *host_public = message + 2;
const unsigned char *nonce = host_public + P2P_KEY_SIZE;
@@ -608,6 +792,13 @@ static void accept_received(const unsigned char *message, int size)
if (size < fixed + 1 + TAG_SIZE || memcmp(nonce, signalling.join_nonce, NONCE_SIZE))
return;
/* the first answer to the request holds: the proof carries its host
nonce (the host's changes every HOST_NONCE_PERIOD) */
if (signalling.join_answered && (memcmp(host_public, signalling.join_host_public, P2P_KEY_SIZE) ||
memcmp(host_nonce, signalling.join_host_nonce, NONCE_SIZE)))
{
return;
}
/* the invite's host: its key hashes to the identifier in the invite */
p2p_identifier_for(host_public, identifier);
if (memcmp(identifier, signalling.join_host, P2P_IDENTIFIER_SIZE))
@@ -620,15 +811,18 @@ static void accept_received(const unsigned char *message, int size)
signalling.join_has_base = 1;
}
/* and the answer is its */
accept_tag(signalling.join_base, message, size - TAG_SIZE, tag);
if (!p2p_equal(tag, message + size - TAG_SIZE, TAG_SIZE))
if (!tag_right(signalling.join_base, "accept", message, size))
return;
count = get_candidates(message + fixed, size - TAG_SIZE - fixed, candidates);
if (count < 0)
return;
signalling.join_answered = 1;
session_secret(signalling.join_base, nonce, host_nonce, secret);
p2p_peer_offered(signalling.join_host, secret, candidates, count, 1);
if (!p2p_peer_offered(signalling.join_host, secret, candidates, count, 1) || signalling.join_answered)
return;
memcpy(signalling.join_host_nonce, host_nonce, NONCE_SIZE);
signalling.join_answered = 1;
/* the proof, at once */
send_join();
}
static void publish_received(const char *topic, const unsigned char *payload, int size)
@@ -721,10 +915,11 @@ static void broker_parse(struct broker *broker)
publish_received(topic, body + offset, remaining - offset);
}
}
memmove(broker->input, broker->input + total, (size_t)(broker->input_size - total));
broker->input_size -= total;
/* (what it sent in answer may have closed it, emptying input) */
if (broker->socket < 0)
return;
memmove(broker->input, broker->input + total, (size_t)(broker->input_size - total));
broker->input_size -= total;
}
}
@@ -909,6 +1104,11 @@ static void sync_all_topics(void)
void p2p_signal_host(const unsigned char *token)
{
if (!signalling.has_nonce_key)
{
posix_random_bytes(signalling.nonce_key, P2P_SHA256_SIZE);
signalling.has_nonce_key = 1;
}
memcpy(signalling.host_token, token, P2P_TOKEN_SIZE);
derive(token, "seal", NULL, signalling.host_key);
make_topic(token, "host", p2p_identifier(), signalling.host_topic);
@@ -920,6 +1120,7 @@ void p2p_signal_stop_hosting(void)
{
signalling.hosting = 0;
memset(signalling.joiners, 0, sizeof(signalling.joiners));
memset(signalling.askers, 0, sizeof(signalling.askers));
sync_all_topics();
}
+2 -2
View File
@@ -333,9 +333,9 @@ int posix_socket_select(int *read, int *read_count, int *write, int *write_count
/* poll, which takes any descriptor (select none from FD_SETSIZE on,
which a process allowed more files has), with select's readiness: read
for data, the end or an error, write for room or an error, error for
urgent data */
urgent data or (as Winsock's) a connect that failed */
static const short events[3] = { POLLIN, POLLOUT, POLLPRI };
static const short ready[3] = { POLLIN | POLLHUP | POLLERR, POLLOUT | POLLERR, POLLPRI };
static const short ready[3] = { POLLIN | POLLHUP | POLLERR, POLLOUT | POLLERR, POLLPRI | POLLERR };
/* (larger sets, as internet play's thread waits on, in a buffer each
thread keeps: not one allocation each time) */
static __thread struct pollfd *buffer;
+44 -14
View File
@@ -240,8 +240,8 @@ static int peer_outgoing_address(int stream, int connecting, const struct sockad
return 1;
}
/* tells internet play the local port the game's socket has (bound, or
listening) */
/* tells internet play the local port the game's socket has (bound, given
one by a connection or a send, or listening) */
static void note_socket_port(SOCKET socket, int listening)
{
struct sockaddr_in bound;
@@ -249,15 +249,18 @@ static void note_socket_port(SOCKET socket, int listening)
int type = SOCK_DGRAM;
int type_length = sizeof(type);
/* (not one bound to this machine alone: the telnet console's, which no
peer is to reach, and which is not hosting) */
if (posix_socket_getsockname((int)socket, &bound, &length) < 0 || bound.sin_family != AF_INET ||
bound.sin_addr.s_addr == loopback_address())
!bound.sin_port)
{
return;
}
posix_socket_getsockopt((int)socket, SOL_SOCKET, SO_TYPE, &type, &type_length);
p2p_socket_port((int)socket, type == SOCK_STREAM, listening, bound.sin_port);
/* (one bound to this machine alone, as the telnet console's, is not for
peers to reach, and is not hosting; its port is still no stand-in's) */
if (bound.sin_addr.s_addr == loopback_address())
p2p_port_taken(type == SOCK_STREAM, bound.sin_port);
else
p2p_socket_port((int)socket, type == SOCK_STREAM, listening, bound.sin_port);
}
/* traffic from an internet play peer's stand-in comes from the peer, and
@@ -406,6 +409,8 @@ int WSAAPI halo_ws_connect(SOCKET socket, const struct sockaddr *address, int ad
unsigned long override;
int type = SOCK_STREAM;
int type_length = sizeof(type);
int result;
int error;
/* an internet play peer's TCP or UDP port */
if (address && address->sa_family == AF_INET &&
@@ -437,7 +442,18 @@ int WSAAPI halo_ws_connect(SOCKET socket, const struct sockaddr *address, int ad
posix_socket_bind((int)socket, &bound, sizeof(bound));
}
}
return winsock_result(posix_socket_connect((int)socket, address, address_length));
result = posix_socket_connect((int)socket, address, address_length);
error = result < 0 ? posix_socket_last_error() : 0;
/* the port the system gave it (a connection under way has one too, and
a datagram socket's that failed keeps its: a stream socket's that failed
has none) */
note_socket_port(socket, 0);
if (result < 0)
{
WSASetLastError(error);
return SOCKET_ERROR;
}
return result;
}
int WSAAPI halo_ws_listen(SOCKET socket, int backlog)
@@ -474,6 +490,10 @@ int WSAAPI halo_ws_sendto(SOCKET socket, const char *buffer, int length, int fla
{
/* the rewritten destination: it must outlive the send */
struct sockaddr_in target;
/* 0: not bound yet, which the first send binds it */
unsigned short source_port = socket_port(socket);
int result;
int send_error;
if (address && address->sa_family == AF_INET && address_length >= (int)sizeof(struct sockaddr_in) &&
((const struct sockaddr_in *)address)->sin_addr.s_addr == INADDR_BROADCAST)
@@ -481,11 +501,8 @@ int WSAAPI halo_ws_sendto(SOCKET socket, const char *buffer, int length, int fla
unsigned long targets[MAXIMUM_BROADCAST_TARGETS];
unsigned short ports[P2P_BROADCAST_PEERS];
int target_count = broadcast_targets(targets, MAXIMUM_BROADCAST_TARGETS);
unsigned short source_port;
int peer_count;
int send_error;
int index;
int result;
/* one datagram per target; the broadcast counts as sent if any is */
memcpy(&target, address, sizeof(target));
@@ -511,7 +528,12 @@ int WSAAPI halo_ws_sendto(SOCKET socket, const char *buffer, int length, int fla
/* and to every internet play peer (after the send above, which binds
the socket if it was not): onto the tunnel at once, else through the
stand-ins */
source_port = socket_port(socket);
if (!source_port)
{
source_port = socket_port(socket);
if (source_port)
note_socket_port(socket, 0);
}
if (source_port)
{
if (p2p_broadcast_datagram(source_port, ((const struct sockaddr_in *)address)->sin_port, buffer,
@@ -551,8 +573,6 @@ int WSAAPI halo_ws_sendto(SOCKET socket, const char *buffer, int length, int fla
if (address && address->sa_family == AF_INET && address_length >= (int)sizeof(struct sockaddr_in) &&
(halo_ws_ntohl(((const struct sockaddr_in *)address)->sin_addr.s_addr) & 0xFFC00000) == 0x64400000)
{
unsigned short source_port = socket_port(socket);
switch (source_port ? p2p_send_datagram(source_port, ((const struct sockaddr_in *)address)->sin_addr.s_addr,
((const struct sockaddr_in *)address)->sin_port, buffer, length) : 0)
{
@@ -571,7 +591,17 @@ int WSAAPI halo_ws_sendto(SOCKET socket, const char *buffer, int length, int fla
address = outgoing_address(address, address_length, &target);
break;
}
return winsock_result(posix_socket_sendto((int)socket, buffer, length, flags, address, address_length));
result = posix_socket_sendto((int)socket, buffer, length, flags, address, address_length);
send_error = result < 0 ? posix_socket_last_error() : 0;
/* the port the send gave it (one that failed binds it too) */
if (!source_port)
note_socket_port(socket, 0);
if (result < 0)
{
WSASetLastError(send_error);
return SOCKET_ERROR;
}
return result;
}
/* debug.network_latency and debug.network_loss: what this machine receives
@@ -1228,30 +1228,35 @@ short connect_endpoint(
do
{
fd_set writeable;
fd_set failed;
writeable.fd_array[0] = ep->socket;
writeable.fd_count = 1;
failed.fd_array[0] = ep->socket;
failed.fd_count = 1;
if (select(1, NULL, &writeable, NULL, &timeval) == 1)
/* port: a connect that failed is in the error set (not
writeable, and its would-block left as it was) */
if (select(1, NULL, &writeable, &failed, &timeval) > 0)
{
error = 0;
error = failed.fd_count ? WSAECONNREFUSED : 0;
}
else
{
error = WSAGetLastError();
}
if (system_milliseconds() > timeout)
/* port: a select that ends with the connection still being
made leaves the connect's would-block: waited on, as in
progress is, until the timeout (a connection made after a
SYN sent again takes a second or three) */
if ((error == WSAEINPROGRESS || error == WSAEWOULDBLOCK) &&
(long)(system_milliseconds() - timeout) > 0)
{
error = WSAEINPROGRESS;
closesocket(ep->socket);
/* port: closed here, not again when the endpoint is (its
number may be another socket's by then) */
ep->socket = INVALID_SOCKET;
break;
}
}
while (error == WSAEINPROGRESS);
while (error == WSAEINPROGRESS || error == WSAEWOULDBLOCK);
}
}
@@ -1259,6 +1264,12 @@ short connect_endpoint(
{
winsock_error_to_string(error);
result = _transport_error_connect_failed;
/* port: none of the attempt kept (its socket half made and not
blocking, which the next would reuse): the next makes a new one,
and its number is not closed again when the endpoint is */
closesocket(ep->socket);
ep->socket = INVALID_SOCKET;
SET_FLAG(ep->flags, _transport_endpoint_nonblocking_bit, FALSE);
}
else
{
+7
View File
@@ -3825,6 +3825,13 @@ void game_engine_player_killed(
killer (port/linux/game/network_distributed.c) */
network_distributed_player_killed(&killing_player_index, &killing_object_index, dead_player_index,
&friendly_fire);
/* the host's kill of a player who quit, ahead of this client's clock
(game_update_quit_players has not come to its time yet) */
if (network_game_distributed_client() && dead_player->quit_out_of_game_time != NONE &&
killing_player_index == dead_player_index)
{
dead_player->quit_out_of_game = TRUE;
}
dead_player->death_time = game_time_get();
if (game_engine->player_killed_player)
{
+10 -2
View File
@@ -3850,10 +3850,15 @@ void players_update_after_game(
telefrag_ticks = player->telefrag_timeout;
if (telefrag_ticks >= 90)
{
/* a client's view of who blocks is a latency late: the
host's kill arrives with its damage events (the message
shown once) */
boolean client = network_game_distributed_client();
if (player->unit_index != NONE)
{
unit = unit_get(player->unit_index);
if (!TEST_FLAG(unit->object.damage_flags, _object_die_act_of_god_bit))
if (client ? telefrag_ticks == 90 : !TEST_FLAG(unit->object.damage_flags, _object_die_act_of_god_bit))
{
if (player->local_player_index != NONE)
{
@@ -3870,9 +3875,12 @@ void players_update_after_game(
}
player_telefrag_effect_stop(iterator.datum_index);
unit_kill(player->unit_index);
if (!client)
unit_kill(player->unit_index);
}
}
if (client)
player_telefrag_effect_stop(iterator.datum_index);
}
else
{
+5 -4
View File
@@ -2312,11 +2312,12 @@ static struct widget_instance *ui_widget_launch_widget(
struct widget_instance *new_widget;
short local_player_index;
/* port: the multiplayer menus open only on maps of a build that plays
multiplayer with the others (cache_files.c, cache_files_multiplayer_region);
otherwise the player is told why, and the main menu stays */
/* port: the menus of multiplayer with other machines (not split screen's
or co-op's) open only on maps of a build that plays multiplayer with the
others (cache_files.c, cache_files_multiplayer_region); otherwise the
player is told why, and the menu stays */
{
static char const multiplayer_menus[] = "ui\\shell\\main_menu\\multiplayer_type_select\\";
static char const multiplayer_menus[] = "ui\\shell\\main_menu\\multiplayer_type_select\\connected\\";
char const *name = tag_get_name(new_widget_tag_index);
char build[0x20];
@@ -5551,7 +5551,8 @@ static boolean multiplayer_level_select(
{
char build[0x20];
if (global_network_game_server_get() && !cache_files_map_plays_multiplayer(map_name, build))
if (global_network_game_server_get() && !network_game_is_splitscreen_local() &&
!cache_files_map_plays_multiplayer(map_name, build))
{
cache_files_show_multiplayer_unavailable(map_name, build);
return FALSE;
+5 -1
View File
@@ -1261,9 +1261,13 @@ boolean network_game_client_game_settings_updated(
/* port: a map of a build this version does not play with others
(its objects would not be the host's): said, and the game left */
if (!cache_files_map_plays_multiplayer(message_packet->map.name, build))
if (!network_game_is_splitscreen_local() &&
!cache_files_map_plays_multiplayer(message_packet->map.name, build))
{
cache_files_show_multiplayer_unavailable(message_packet->map.name, build);
/* (the menu's error the join's, not the connection lost that
the failure would otherwise give) */
display_error_when_main_menu_loaded(_error_network_failed_to_join_game);
return FALSE;
}
network_event("precaching map '%s'...", message_packet->map.name);
+4 -1
View File
@@ -1896,10 +1896,13 @@ static void network_connection_flush_reliable_last(
{
unsigned long start_time = system_milliseconds();
/* (and not at all for a peer that has taken nothing lately: gone, its
connection dropped for it) */
while (connection->reliable_outgoing_queue && circular_queue_size(connection->reliable_outgoing_queue) > 0 &&
network_connection_flush_reliable(connection) &&
circular_queue_size(connection->reliable_outgoing_queue) > 0 &&
system_milliseconds() - start_time < 500)
system_milliseconds() - start_time < 500 &&
system_milliseconds() - connection->reliable_outgoing_time < 100)
{
Sleep(5);
}
+88 -1
View File
@@ -779,6 +779,71 @@ static long network_game_server_frequent_updates_until[MAXIMUM_NETWORK_MACHINE_C
network_game_server_client_machine_may_slow_countdown) */
static boolean network_game_server_countdown_slowed[MAXIMUM_NETWORK_MACHINE_COUNT];
/* port: the players added to the game in progress from each client
machine's address (a machine joins from one address, and joining again
drops the old: network_game_server_accept_client_machine_into_game), no
more than MAXIMUM_INGAME_ADDITIONS_PER_ADDRESS a game, so that players
added and removed, or a machine joining again and again, do not take
every player of the game (a player gone stays for its scores) */
enum
{
MAXIMUM_INGAME_ADDITIONS_PER_ADDRESS = 2 * MAXIMUM_PLAYERS_PER_MACHINE,
};
static struct
{
unsigned long address;
short count;
} network_game_server_ingame_additions[MAXIMUM_NETWORK_PLAYER_COUNT];
static short *network_game_server_ingame_addition_count(
unsigned long address,
boolean create)
{
long index;
if (!address)
return NULL;
for (index = 0; index < (long)NUMBEROF(network_game_server_ingame_additions); index++)
{
if (network_game_server_ingame_additions[index].address == address)
return &network_game_server_ingame_additions[index].count;
}
/* (more addresses than players: the game is full anyway) */
for (index = 0; create && index < (long)NUMBEROF(network_game_server_ingame_additions); index++)
{
if (!network_game_server_ingame_additions[index].address)
{
network_game_server_ingame_additions[index].address = address;
network_game_server_ingame_additions[index].count = 0;
return &network_game_server_ingame_additions[index].count;
}
}
return NULL;
}
/* (a client machine's player queued to add in game: one refused is as one
the game has no room for) */
static boolean network_game_server_machine_may_add_player_ingame(
struct network_game_server *server,
long machine_index)
{
short *count;
if (!VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) ||
network_game_server_client_machine_is_local(server, &server->client_machines[machine_index]))
{
return TRUE;
}
count = network_game_server_ingame_addition_count(network_game_server_client_machine_addresses[machine_index], FALSE);
if (count && *count >= MAXIMUM_INGAME_ADDITIONS_PER_ADDRESS)
{
network_event("client machine #%ld added too many players in game", machine_index);
return FALSE;
}
return TRUE;
}
/* ---------- public code */
struct network_game_server *network_game_server_create(
@@ -963,11 +1028,23 @@ static boolean network_game_server_network_lost(
{
static unsigned long down_time;
static unsigned long checked_time;
static boolean asked;
static boolean available_asked;
static unsigned long asked_time;
unsigned long now = system_milliseconds();
boolean in_game = server->state == _network_game_server_state_ingame ||
server->state == _network_game_server_state_postgame;
boolean available = transport_network_available();
boolean recent = checked_time && now - checked_time <= 2000;
boolean available;
/* (asked once a second: each asking lists the machine's interfaces) */
if (!asked || now - asked_time >= 1000)
{
available_asked = transport_network_available();
asked_time = now;
asked = TRUE;
}
available = available_asked;
checked_time = now;
if (available)
@@ -1969,11 +2046,20 @@ void network_game_server_update_ticks(
}
if (client_machine &&
network_game_server_machine_may_add_player_ingame(server, client_machine_index) &&
network_game_server_add_player_to_game(
server,
client_machine,
&server->queued_player))
{
if (!network_game_server_client_machine_is_local(server, client_machine))
{
short *count = network_game_server_ingame_addition_count(
network_game_server_client_machine_addresses[client_machine_index], TRUE);
if (count)
(*count)++;
}
if (!network_game_server_send_player_joined_info_ingame(
server,
&server->queued_player))
@@ -3941,6 +4027,7 @@ boolean network_game_server_reset_to_pregame(
server->sent_start_game_message = FALSE;
server->queued_player_valid = FALSE;
server->waiting_player_count = 0;
csmemset(network_game_server_ingame_additions, 0, sizeof(network_game_server_ingame_additions));
/* Preserve January's 32-bit wrap without overflowing signed arithmetic.
* VC7 converts the unsigned result back to the same signed bit pattern.
*/
+11 -2
View File
@@ -147,7 +147,7 @@ boolean network_damage_replaying_kill(void);
void network_damage_player_effect(long player_index, struct damage_data const *damage, real total_damage);
void network_damage_aftermath(long object_index, struct damage_data const *damage, unsigned long being_damaged_flags,
real shield_damage, real body_damage, real body_damage_multiplier, short body_part, short node_index,
short region_index, short material_index);
short region_index, short material_index, long victim_player_index);
/* set while a distributed client carries out a kill it does not decide (an
act of god: the host's word, network_distributed.c, or the world's) */
@@ -1373,6 +1373,7 @@ void object_cause_damage(
struct object_datum *current_object;
struct damage_resistance_material const *damage_material;
long current_object_index;
long victim_player_index;
unsigned long being_damaged_flags;
real shield_damage;
real body_damage;
@@ -1790,6 +1791,13 @@ void object_cause_damage(
}
}
/* (the player of the unit, which a killing blow's aftermath
takes from it, unit_died) */
{
struct unit_datum *victim = unit_try_and_get(current_object_index);
victim_player_index = victim ? victim->unit.player_index : NONE;
}
object_damage_aftermath(
current_object_index,
damage,
@@ -1809,7 +1817,8 @@ void object_cause_damage(
body_part,
current_object_index == object_index ? node_index : NONE,
current_object_index == object_index ? region_index : NONE,
current_object_index == object_index ? material_index : NONE);
current_object_index == object_index ? material_index : NONE,
victim_player_index);
if (TEST_FLAG(
being_damaged_flags,
_object_being_damaged_body_destroyed_bit))