Compare commits

...
10 Commits
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 04cfd1a12c Host line of sight: a VR player's weapon hand is a muzzle too
Upstream's host now refuses a reported hit when the shooter had no line
of sight to the target, from the unit's camera (or a vehicle gun's
muzzle). A VR player fires from the weapon in their hand, and their view
follows their head up to vr.lean_limit from the body: a shot past a
corner or over an edge that the unit's camera is behind was refused.

The host takes the hand their machine sends (network_vr.c) as a muzzle
in the trail points, as a vehicle's gun: the line from the eye to it
must be clear (no hand through a wall), and it must be within 0.4 world
units (1.2 m) of the camera.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:15:43 +01:00
Stefan MaierandClaude Opus 5.5 2b40b42f75 Merge upstream/main (2026-10-25): host line of sight check, early inventories
Network version stays 38. The conflict: upstream's unbinding of the read
framebuffer before the swap (for OBS) goes into the window's branch,
where the swap is (VR presents through vr.c).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:11:13 +01:00
bnunu 7fb941b0b2 Merge pull request #474 from johncranston303/los-fix
Stop a client from instantly wiping the whole lobby through walls
2026-10-24 18:51:56 -04:00
John Cranston 933cad12b3 Host: refuse a reported hit with no line of sight to the target. 2026-10-24 13:22:27 -07:00
bnunu 3a2c4649e8 Merge pull request #466 from medic-code/fix/spawn-inventory-wait
fix(net): apply an inventory once its objects come
2026-10-24 14:49:33 -04:00
medic-code 32604c8e7e Describe in NETCODE.md what a client does with an early inventory
The inventory bullet gains what a client does with an inventory whose
unit or weapons have not come yet, and when it drops one, so the
protocol notes match the retry list.

Checked: documentation only; ninja linux links and
test_retried_inventories.py passes (17 tests).
2026-10-24 20:48:18 +02:00
medic-code 07c8430b5a Keep a client's inventory until its weapons have come too
A weapon is made on the stream too, so an inventory naming one not here
yet left its slot as it was until the host's next inventory. What is
here is still applied at once, and the inventory is kept until every
weapon it names has come, then applied in full with its ammunition.

Checked: ninja linux links with no warnings; test_retried_inventories.py
passes (17 tests): six cases, now including a weapon arriving late, ten
negative controls each failing its own case, and the wiring check.
2026-10-24 20:48:18 +02:00
medic-code 667c2f29a0 Keep a client's inventory until the unit it names has come
An inventory goes as a datagram and a unit is made on the stream, which
can trail it, so one naming a unit not here yet was dropped: a player
just spawned held nothing until the host's next. The client keeps the
newest of each unit's, one per player at most, and applies it once the
unit comes; it is dropped after three host refreshes or at a new game.

Checked: ninja linux links with no warnings; test_retried_inventories.py
passes (15 tests): five cases (unit late, newest kept, list full,
forgotten, expiry), nine negative controls each failing its own case,
and a wiring check.
2026-10-24 20:48:18 +02:00
bnunu 6036adfa2d Merge pull request #445 from johncranston303/obs-capture
OBS's game capture no longer records the game upside down
2026-10-24 14:39:31 -04:00
John CranstonandClaude Fable 5.1 a689a8a3fa Unbind the read framebuffer before the swap, for OBS's game capture.
The renderer left the back buffer's render target bound as the read
framebuffer after the display blit. OBS's game capture copies the window
from the read framebuffer bound in the context at SwapBuffers, so it copied
that render target, which is drawn top row first, and the recording was
upside down. Recorded with OBS 32.2.2 before and after: upside down, then
upright.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-22 01:58:46 -07:00
7 changed files with 803 additions and 24 deletions

No files matched your search

+4 -1
View File
@@ -647,7 +647,10 @@ its name alone beside it, has its name given back as the map lists find it.
players' ammunition and grenade counts from the host only once a
round trip has passed since it last fired, reloaded or threw (what
the host says before then is from before). How old what a unit
carries is travels in 16 bits.
carries is travels in 16 bits. A client keeps an inventory whose
unit or weapons have not come yet (the stream can trail the datagram)
and applies it in full once they have, the newest of a unit's winning;
one is dropped `EMPTY_INVENTORY_TICKS` after the newest came.
- Players take the units the host spawns them with, seats are the
host's (a client's own player's once it has ridden otherwise for
longer than a round trip), and the CTF flags and oddballs are the same
+319 -18
View File
@@ -61,11 +61,20 @@ being hit looks and feels like on the clients).
#include "objects/damage_effect_definitions.h"
#include "effects/effect_definitions.h"
#include "physics/collision_model_definitions.h"
#include "physics/collisions.h"
#include "items/weapon_definitions.h"
#include "items/projectile_definitions.h"
#include "scenario/scenario.h"
#include "units/units.h"
#include "network_distributed.h"
#ifdef HALO_ANDROID
#include "halo_vr.h"
/* port (Mjolnir VR): how far from the unit's camera a VR player's weapon
hand may be for the line of sight (world units, 1.2 m: vr.lean_limit's
0.35 m and an arm) */
#define VR_SIGHT_HAND_REACH 0.4f
#endif
#include <math.h>
@@ -151,6 +160,10 @@ enum
PLAYER_TRAIL_REPORT_SLACK_TICKS = TICKS_PER_SECOND,
PLAYER_TRAIL_POINTS = 128,
PLAYER_TRAIL_TICKS = PLAYER_TRAIL_INTERVAL_TICKS * PLAYER_TRAIL_POINTS,
REPORT_LINE_OF_SIGHT_TICKS = 2 * TICKS_PER_SECOND,
REPORT_LINE_OF_SIGHT_POINTS = REPORT_LINE_OF_SIGHT_TICKS / PLAYER_TRAIL_INTERVAL_TICKS,
MAXIMUM_STUCK_PROJECTILES = 16,
STUCK_PROJECTILE_TICKS = REPORT_MAXIMUM_AGE_TICKS + 2 * TICKS_PER_SECOND,
};
enum
@@ -182,6 +195,7 @@ enum
/* the vehicle they drive running into a unit: at the unit (the origin),
from the vehicle (the epicenter), area damage (physics.c) */
_damage_source_collision_bit,
_damage_source_attached_bit,
};
/* struct distributed_damage_event kill flags */
@@ -372,6 +386,15 @@ static short damage_event_count;
/* ... dealing a client's report */
static boolean damage_dealing_report;
/* ... each player's weapons of late, and hits left */
struct damage_stuck_projectile
{
long projectile_index;
long object_index;
long damage_index;
long time;
boolean spent;
};
static struct
{
long definition_indices[MAXIMUM_RECENT_WEAPONS];
@@ -379,6 +402,7 @@ static struct
/* (the grenades thrown, not yet gone off: NONE for none) */
long throw_times[MAXIMUM_GRENADE_THROWS];
short throw_types[MAXIMUM_GRENADE_THROWS];
struct damage_stuck_projectile stuck[MAXIMUM_STUCK_PROJECTILES];
long driven_time;
/* (the blow of the player's unit with no weapon: units.c's
unit_unarmed_melee_damage) */
@@ -422,6 +446,9 @@ static struct damage_trail_point
real_point3d position;
real speed;
real radius;
real eye;
boolean mounted;
real_point3d muzzle;
} damage_trails[MAXIMUM_TRACKED_PLAYERS][PLAYER_TRAIL_POINTS];
/* ... what each weapon or grenade deals, as the tags have it (their walk
is long): the hits a second of a damage effect from it, how it deals it
@@ -1021,6 +1048,8 @@ static byte distributed_projectile_deals(
{
SET_FLAG(kinds, _damage_source_impact_bit, TRUE);
}
if (projectile->projectile.attached_detonation_damage.index == damage_index)
SET_FLAG(kinds, _damage_source_attached_bit, TRUE);
kinds |= distributed_effect_deals(projectile->projectile.effect.index, damage_index, depth + 1, &from_here);
kinds |= distributed_effect_deals(projectile->projectile.super_detonation.index, damage_index, depth + 1,
&from_here);
@@ -1372,6 +1401,40 @@ static long static_target_near(
return nearest;
}
static boolean distributed_vehicle_muzzle(
long vehicle_index,
real_point3d *muzzle)
{
struct unit_datum *vehicle = unit_get(vehicle_index);
struct object_datum *weapon;
struct object_marker marker;
long weapon_index = NONE;
short slot = vehicle->unit.current_weapon_index;
short index;
if (slot >= 0 && slot < MAXIMUM_WEAPONS_PER_UNIT && vehicle->unit.weapon_object_indices[slot] != NONE &&
object_try_and_get(vehicle->unit.weapon_object_indices[slot]))
{
weapon_index = vehicle->unit.weapon_object_indices[slot];
}
for (index = 0; weapon_index == NONE && index < MAXIMUM_WEAPONS_PER_UNIT; index++)
{
if (vehicle->unit.weapon_object_indices[index] != NONE &&
object_try_and_get(vehicle->unit.weapon_object_indices[index]))
{
weapon_index = vehicle->unit.weapon_object_indices[index];
}
}
if (weapon_index == NONE)
return FALSE;
weapon = object_get(weapon_index);
object_get_marker_by_name(TEST_FLAG(weapon->object.flags, _object_invisible_bit) &&
weapon->object.parent_object_index != NONE ? weapon->object.parent_object_index : weapon_index,
"primary trigger", &marker, 1);
*muzzle = marker.matrix.position;
return TRUE;
}
/* where each player's unit and vehicle are this tick (and whether they
drive it), noted; and every few ticks where their unit is */
static void distributed_note_targets(
@@ -1411,27 +1474,155 @@ static void distributed_note_targets(
tick->objects[player_index][1].driving = vehicle && vehicle->unit.driver_object_index == unit_index;
}
if (game_time_get() % PLAYER_TRAIL_INTERVAL_TICKS == 0)
{
struct damage_trail_point *point =
&damage_trails[player_index][(game_time_get() / PLAYER_TRAIL_INTERVAL_TICKS) & (PLAYER_TRAIL_POINTS - 1)];
struct damage_history_tick const *last =
&damage_history[(game_time_get() - 1) & (TARGET_HISTORY_TICKS - 1)];
boolean appeared = unit_index != NONE &&
(last->time != game_time_get() - 1 || last->objects[player_index][0].object_index == NONE);
point->time = unit_index != NONE ? game_time_get() : NONE;
if (unit_index != NONE)
if (game_time_get() % PLAYER_TRAIL_INTERVAL_TICKS == 0 || appeared)
{
point->position = tick->objects[player_index][0].position;
point->speed = tick->objects[player_index][0].speed;
point->radius = 0.0f;
if (tick->objects[player_index][1].object_index != NONE)
struct damage_trail_point *point =
&damage_trails[player_index][(game_time_get() / PLAYER_TRAIL_INTERVAL_TICKS) & (PLAYER_TRAIL_POINTS - 1)];
point->time = unit_index != NONE ? game_time_get() : NONE;
if (unit_index != NONE)
{
point->speed = MAX(point->speed, tick->objects[player_index][1].speed);
point->radius = object_get(tick->objects[player_index][1].object_index)->object.bounding_sphere_radius;
real_point3d camera;
point->position = tick->objects[player_index][0].position;
point->speed = tick->objects[player_index][0].speed;
point->radius = 0.0f;
unit_get_camera_position(unit_index, &camera);
point->eye = camera.z - point->position.z;
point->mounted = FALSE;
if (tick->objects[player_index][1].object_index != NONE)
{
long vehicle_index = tick->objects[player_index][1].object_index;
struct unit_datum *vehicle = (struct unit_datum *)object_try_and_get_and_verify_type(
vehicle_index, _object_mask_vehicle);
point->speed = MAX(point->speed, tick->objects[player_index][1].speed);
point->radius = object_get(vehicle_index)->object.bounding_sphere_radius;
if (vehicle && (vehicle->unit.driver_object_index == unit_index ||
vehicle->unit.gunner_object_index == unit_index))
{
point->mounted = distributed_vehicle_muzzle(vehicle_index, &point->muzzle);
}
}
#ifdef HALO_ANDROID
/* port (Mjolnir VR): a VR player of another machine fires
from the weapon in their hand (network_vr.c), which may
be past a corner the unit's camera is behind: the hand is
a muzzle as a vehicle's gun is, with the line from the
eye to it clear (no hand through a wall), and no further
from the eye than a lean and an arm reach */
else
{
real vr_origin[3];
if (halo_vr_network_weapon_origin(player_index, unit_index, vr_origin))
{
real dx = vr_origin[0] - camera.x;
real dy = vr_origin[1] - camera.y;
real dz = vr_origin[2] - camera.z;
if (dx * dx + dy * dy + dz * dz <= VR_SIGHT_HAND_REACH * VR_SIGHT_HAND_REACH)
{
point->muzzle.x = vr_origin[0];
point->muzzle.y = vr_origin[1];
point->muzzle.z = vr_origin[2];
point->mounted = TRUE;
}
}
}
#endif
}
}
}
}
}
static void distributed_note_stuck_projectiles(
void)
{
struct object_iterator iterator;
struct object_datum *projectile;
object_iterator_new(&iterator, _object_mask_projectile, 0);
while ((projectile = (struct object_datum *)object_iterator_next(&iterator)) != NULL)
{
struct damage_stuck_projectile *stuck;
long damage_index;
short player_index;
short index;
short slot = NONE;
short slot_rank = 0;
boolean found = FALSE;
if (projectile->object.parent_object_index == NONE ||
projectile->object.parent_object_index == projectile->object.owner_object_index ||
projectile->object.owner_player_index == NONE || !player_try_and_get(projectile->object.owner_player_index))
{
continue;
}
player_index = (short)DATUM_INDEX_TO_ABSOLUTE_INDEX(projectile->object.owner_player_index);
damage_index = projectile_definition_get(projectile->definition_index)->projectile.attached_detonation_damage.index;
if (player_index >= MAXIMUM_TRACKED_PLAYERS || damage_index == NONE)
continue;
for (index = 0; index < MAXIMUM_STUCK_PROJECTILES; index++)
{
struct damage_stuck_projectile const *note = &damage_players[player_index].stuck[index];
short rank;
if (note->time != NONE && note->projectile_index == iterator.index)
{
slot = index;
found = TRUE;
break;
}
rank = note->time == NONE || game_time_get() - note->time > STUCK_PROJECTILE_TICKS ? 0 : note->spent ? 1 : 2;
if (slot == NONE || rank < slot_rank ||
(rank == slot_rank && note->time < damage_players[player_index].stuck[slot].time))
{
slot = index;
slot_rank = rank;
}
}
stuck = &damage_players[player_index].stuck[slot];
if (!found)
{
stuck->projectile_index = iterator.index;
stuck->damage_index = damage_index;
stuck->spent = FALSE;
}
stuck->object_index = projectile->object.parent_object_index;
stuck->time = game_time_get();
}
}
static boolean distributed_stuck_detonation(
short player_index,
long object_index,
long damage_index)
{
short index;
distributed_note_stuck_projectiles();
for (index = 0; index < MAXIMUM_STUCK_PROJECTILES; index++)
{
struct damage_stuck_projectile *stuck = &damage_players[player_index].stuck[index];
if (stuck->time != NONE && !stuck->spent && game_time_get() - stuck->time <= STUCK_PROJECTILE_TICKS &&
stuck->object_index == object_index && stuck->damage_index == damage_index)
{
stuck->spent = TRUE;
return TRUE;
}
}
return FALSE;
}
/* whether the host had the history's object within the tolerance of the
position (with how far it moves in a few ticks) */
static boolean distributed_history_near(
@@ -1453,9 +1644,12 @@ vehicle), unless it is not there. */
static short distributed_target_seen(
long object_index,
real_point3d const *position,
long ticks)
long ticks,
real_point3d *seen_position)
{
boolean latest = FALSE;
boolean seen = FALSE;
real seen_distance = 0.0f;
short column_player_index = NONE;
short column_slot = 0;
long back;
@@ -1464,6 +1658,8 @@ static short distributed_target_seen(
{
long time = game_time_get() - back;
struct damage_history_tick const *tick = &damage_history[time & (TARGET_HISTORY_TICKS - 1)];
real reach;
real distance;
if (tick->time != time)
break;
@@ -1493,12 +1689,18 @@ static short distributed_target_seen(
}
if (back == 0)
latest = TRUE;
if (distributed_history_near(&tick->objects[column_player_index][column_slot].position,
tick->objects[column_player_index][column_slot].speed, position, REPORT_HISTORY_TOLERANCE))
reach = REPORT_HISTORY_TOLERANCE +
REPORT_HISTORY_LEAD_TICKS * tick->objects[column_player_index][column_slot].speed;
distance = distributed_distance_squared(position, &tick->objects[column_player_index][column_slot].position);
if (distance <= reach * reach && (!seen || distance < seen_distance))
{
return TRUE;
seen = TRUE;
seen_distance = distance;
*seen_position = tick->objects[column_player_index][column_slot].position;
}
}
if (seen)
return TRUE;
/* (one that was a player's, a body, a vehicle left: where it is now) */
return latest ? FALSE : NONE;
}
@@ -1801,6 +2003,83 @@ static boolean distributed_report_paid(
return TRUE;
}
static boolean distributed_report_sight_clear(
unsigned long flags,
real_point3d const *from,
real_point3d const *impact,
real_point3d const *centre)
{
struct collision_result collision;
return !collision_test_line(flags, from, centre, NONE, &collision) ||
(!collision_test_line(flags, from, impact, NONE, &collision) &&
!collision_test_line(flags, impact, centre, NONE, &collision));
}
/* whether the structure left the shooter a line to the target from the
newest of their living trail points in the window (none: not alive to fire
it, refused). The eye and the target position are matched over windows, not
to one moment: deliberate, the lag compensation the other checks allow. */
static boolean distributed_report_line_of_sight(
long machine_index,
short player_index,
real_point3d const *impact_point,
real_point3d const *centre,
real radius,
struct damage_reach const *reach,
long host_time)
{
unsigned long flags = FLAG(_collision_test_structure_bit) |
FLAG(_collision_test_front_facing_surfaces_bit) |
FLAG(_collision_test_ignore_invisible_surfaces_bit) | FLAG(_collision_test_ignore_breakable_surfaces_bit) |
FLAG(_collision_test_ignore_two_sided_surfaces_bit);
real ticks = reach->ticks > 0.0f ? MIN(reach->ticks, (real)PLAYER_TRAIL_TICKS) : (real)REPORT_LINE_OF_SIGHT_TICKS;
long latest_made = game_time_get() - (long)distributed_machine_round_trip_ticks(machine_index) -
PLAYER_TRAIL_REPORT_SLACK_TICKS;
long newest = game_time_get() / PLAYER_TRAIL_INTERVAL_TICKS;
long earliest;
long bucket;
short traced = 0;
real_point3d impact = *impact_point;
struct collision_result collision;
{
real distance = (real)sqrt(distributed_distance_squared(centre, impact_point));
if (distance > radius && distance > 0.0f)
{
real scale = radius / distance;
impact.x = centre->x + (impact_point->x - centre->x) * scale;
impact.y = centre->y + (impact_point->y - centre->y) * scale;
impact.z = centre->z + (impact_point->z - centre->z) * scale;
}
}
if (host_time < latest_made)
host_time = latest_made;
earliest = host_time - (long)ticks - TARGET_HISTORY_SLACK_TICKS - PLAYER_TRAIL_INTERVAL_TICKS;
for (bucket = newest; bucket >= 0 && newest - bucket < PLAYER_TRAIL_POINTS &&
(bucket + 1) * PLAYER_TRAIL_INTERVAL_TICKS > earliest && traced < REPORT_LINE_OF_SIGHT_POINTS; bucket--)
{
struct damage_trail_point const *trail_point = &damage_trails[player_index][bucket & (PLAYER_TRAIL_POINTS - 1)];
real_point3d eye;
if (trail_point->time == NONE || trail_point->time / PLAYER_TRAIL_INTERVAL_TICKS != bucket)
continue;
traced++;
eye = trail_point->position;
eye.z += trail_point->eye;
if (distributed_report_sight_clear(flags, &eye, &impact, centre))
return TRUE;
if (trail_point->mounted && !collision_test_line(flags, &eye, &trail_point->muzzle, NONE, &collision) &&
distributed_report_sight_clear(flags, &trail_point->muzzle, &impact, centre))
{
return TRUE;
}
}
return FALSE;
}
/* whether the host takes the report, which it makes the damage as the
host deals it (the cheaper checks first, and the report paid for before the
history is looked through: a flood of reports costs its sender no more than
@@ -1834,6 +2113,7 @@ static boolean distributed_report_valid(
real reach;
struct damage_reach fired_reach;
short seen;
real_point3d seen_position;
short grenade_type;
short throw_index = NONE;
@@ -1955,8 +2235,7 @@ static boolean distributed_report_valid(
return FALSE;
}
/* what hits at a point (a shot, an explosion) within its reach of where
the host had the player since it was fired, where the tags bound that
(no line of sight: a wall between is not looked for) */
the host had the player since it was fired, where the tags bound that */
if (!melee && !collision && fired_reach.distance >= 0.0f &&
!distributed_player_could_fire(machine_index, player_index, &report->damage.origin, &fired_reach,
report->host_time))
@@ -1966,7 +2245,7 @@ static boolean distributed_report_valid(
/* the target about where the host had it when the shooter saw it: a
player's unit or vehicle as far back as the report was made, else (what
no player has) about where it is */
seen = distributed_target_seen(report->object_index, &report->target_position, ticks);
seen = distributed_target_seen(report->object_index, &report->target_position, ticks, &seen_position);
if (seen == FALSE)
return FALSE;
if (seen == NONE)
@@ -1982,6 +2261,25 @@ static boolean distributed_report_valid(
(real)sqrt(velocity.i * velocity.i + velocity.j * velocity.j + velocity.k * velocity.k);
if (!(distributed_distance_squared(&report->target_position, &origin) <= tolerance * tolerance))
return FALSE;
seen_position = origin;
}
/* a point hit needs a line to the target, unless it is the detonation of
a projectile the host has had stuck to it: that flew there, no line now */
if (!melee && !collision && !area &&
!(TEST_FLAG(kinds, _damage_source_attached_bit) &&
distributed_stuck_detonation(player_index, report->object_index, report->damage.definition_index)))
{
real_point3d centre, target_origin;
object_get_origin(report->object_index, &target_origin);
centre.x = seen_position.x + target->object.bounding_sphere_center.x - target_origin.x;
centre.y = seen_position.y + target->object.bounding_sphere_center.y - target_origin.y;
centre.z = seen_position.z + target->object.bounding_sphere_center.z - target_origin.z;
if (!distributed_report_line_of_sight(machine_index, player_index, &report->damage.origin, &centre,
target->object.bounding_sphere_radius, &fired_reach, report->host_time))
{
return FALSE;
}
}
/* the damage as the host deals it: what a client's hit can be, from the
player's own */
@@ -2245,6 +2543,7 @@ void network_damage_host_tick(
distributed_log_reports();
distributed_note_weapons();
distributed_note_targets();
distributed_note_stuck_projectiles();
distributed_note_event_destinations();
for (machine_number = 0; machine_number < machine_count; machine_number++)
{
@@ -2480,6 +2779,8 @@ void network_damage_new_game(
damage_players[player_index].unarmed_melee_damage_index = NONE;
for (index = 0; index < MAXIMUM_GRENADE_THROWS; index++)
damage_players[player_index].throw_times[index] = NONE;
for (index = 0; index < MAXIMUM_STUCK_PROJECTILES; index++)
damage_players[player_index].stuck[index].time = NONE;
damage_players[player_index].driven_time = -RECENT_WEAPON_TICKS - 1;
damage_players[player_index].hit_seconds = HIT_REPORT_BURST_SECONDS;
damage_explosions[player_index].time = NONE;
+132 -4
View File
@@ -142,6 +142,12 @@ enum
/* ... those of the kinds the host has, made since the last tick, checked
(more, and all of them are) */
MAXIMUM_CLIENT_NEW_OBJECTS = 1024,
/* ... the host's inventories it could not apply in full yet, one for
each unit that carries anything (a vehicle is a unit, its weapons are
objects) whose objects the stream has not brought: most of them while
it joins, few after. Room for as many as there are players; past that
the oldest goes, and its unit waits for the host's next inventory of it */
MAXIMUM_CLIENT_RETRIED_INVENTORIES = MAXIMUM_TRACKED_PLAYERS,
MAXIMUM_ENTRIES_PER_MESSAGE = 64,
/* the objects the host has at the same indices everywhere */
NETWORKED_OBJECT_TYPES =
@@ -470,6 +476,20 @@ static struct distributed_picked_up_weapon
long definition_index;
long time;
} objects_client_picked_up_weapons[MAXIMUM_LOCAL_PLAYERS];
/* ... the host's inventories that named a unit or a weapon not here yet. An
inventory goes as a datagram, and units and weapons are made on the stream,
which can trail it, so a player just spawned can be told what it carries
before its unit or its weapons are here, and would carry nothing until the
host's next inventory of it, a second later. Each is kept, the newest of its
unit's, and applied once, the tick all it names is here; let go of once
three of the host's refreshes pass without a newer (the host has stopped
sending it) */
static struct distributed_retried_inventory
{
struct distributed_inventory inventory;
long time;
} objects_client_retried_inventories[MAXIMUM_CLIENT_RETRIED_INVENTORIES];
static short objects_client_retried_inventory_count;
/* ... whether it failed to make one of the host's objects since it last
asked for them (it says so when it asks again) */
static boolean objects_client_ask_again;
@@ -2846,6 +2866,80 @@ static boolean distributed_client_own_settled(
return game_time_get() - time > (long)ceil(round_trip) + INVENTORY_INTERVAL_TICKS + OWN_INVENTORY_MARGIN_TICKS;
}
/* whether the weapon the host has in a slot is here (none always is) */
static boolean distributed_client_weapon_here(
long weapon_index)
{
return weapon_index == NONE ||
(network_objects_client_has(weapon_index) && object_try_and_get_and_verify_type(weapon_index, _object_mask_weapon));
}
/* whether the unit an inventory names and every weapon it names are here */
static boolean distributed_client_inventory_whole(
struct distributed_inventory const *inventory)
{
short weapon_slot;
if (!network_objects_client_has(inventory->unit_index) ||
!object_try_and_get_and_verify_type(inventory->unit_index, _object_mask_unit))
{
return FALSE;
}
for (weapon_slot = 0; weapon_slot < MAXIMUM_WEAPONS_PER_UNIT; weapon_slot++)
{
if (!distributed_client_weapon_here(inventory->weapon_indices[weapon_slot]))
return FALSE;
}
return TRUE;
}
/* the inventory kept to apply once all it names is here, the newest of its
unit's (in its entry, else a new one, else the oldest's) */
static void distributed_client_retry_inventory(
struct distributed_inventory const *inventory)
{
struct distributed_retried_inventory *retried = NULL;
short index;
for (index = 0; !retried && index < objects_client_retried_inventory_count; index++)
{
if (objects_client_retried_inventories[index].inventory.unit_index == inventory->unit_index)
retried = &objects_client_retried_inventories[index];
}
if (!retried && objects_client_retried_inventory_count < MAXIMUM_CLIENT_RETRIED_INVENTORIES)
retried = &objects_client_retried_inventories[objects_client_retried_inventory_count++];
if (!retried)
{
short oldest = 0;
for (index = 1; index < objects_client_retried_inventory_count; index++)
{
if (objects_client_retried_inventories[index].time < objects_client_retried_inventories[oldest].time)
oldest = index;
}
retried = &objects_client_retried_inventories[oldest];
}
retried->inventory = *inventory;
retried->time = game_time_get();
}
/* the unit's kept inventory, if it has one, kept no more */
static void distributed_client_forget_retried_inventory(
long unit_index)
{
short index;
for (index = 0; index < objects_client_retried_inventory_count; index++)
{
if (objects_client_retried_inventories[index].inventory.unit_index == unit_index)
{
objects_client_retried_inventories[index] =
objects_client_retried_inventories[--objects_client_retried_inventory_count];
return;
}
}
}
/* the unit carries what the host's does */
static void distributed_client_apply_inventory(
struct distributed_inventory const *inventory)
@@ -2855,6 +2949,12 @@ static void distributed_client_apply_inventory(
boolean local;
short weapon_slot;
/* (what is here of it now, and the rest once it is: the newest of the
unit's is the one kept) */
if (distributed_client_inventory_whole(inventory))
distributed_client_forget_retried_inventory(inventory->unit_index);
else
distributed_client_retry_inventory(inventory);
if (!network_objects_client_has(inventory->unit_index) ||
!object_try_and_get_and_verify_type(inventory->unit_index, _object_mask_unit))
{
@@ -2886,11 +2986,8 @@ static void distributed_client_apply_inventory(
if (weapon_index == wanted_index)
continue;
/* (not here yet: as it is, until it is) */
if (wanted_index != NONE &&
(!network_objects_client_has(wanted_index) || !object_try_and_get_and_verify_type(wanted_index, _object_mask_weapon)))
{
if (!distributed_client_weapon_here(wanted_index))
continue;
}
if (weapon_index != NONE)
unit_network_drop_weapon(inventory->unit_index, weapon_slot);
if (wanted_index != NONE)
@@ -3290,6 +3387,35 @@ static void distributed_client_carry_unsteered_vehicles(
}
}
/* the kept inventories all of whose unit and weapons have come: applied once,
as if they had come now */
static void distributed_client_apply_retried_inventories(
void)
{
short index = 0;
while (index < objects_client_retried_inventory_count)
{
struct distributed_retried_inventory *retried = &objects_client_retried_inventories[index];
if (game_time_get() - retried->time >= EMPTY_INVENTORY_TICKS)
{
*retried = objects_client_retried_inventories[--objects_client_retried_inventory_count];
}
else if (distributed_client_inventory_whole(&retried->inventory))
{
struct distributed_inventory inventory = retried->inventory;
*retried = objects_client_retried_inventories[--objects_client_retried_inventory_count];
distributed_client_apply_inventory(&inventory);
}
else
{
index++;
}
}
}
void network_objects_client_tick(
void)
{
@@ -3309,6 +3435,7 @@ void network_objects_client_tick(
(word)sizeof(message), _distributed_to_host_reliably);
objects_client_ask_again = FALSE;
}
distributed_client_apply_retried_inventories();
if (objects_client_synchronized)
distributed_client_remove_own_objects();
/* (too many to look at one by one later: all of them) */
@@ -3373,6 +3500,7 @@ void network_objects_new_game(
objects_client_creating = FALSE;
objects_client_deleting = FALSE;
objects_client_new_object_count = 0;
objects_client_retried_inventory_count = 0;
objects_client_check_all = TRUE;
objects_client_local_free_index = LOCAL_OBJECTS_FIRST_INDEX;
objects_client_object_count = NONE;
+2 -1
View File
@@ -21,7 +21,8 @@ a VR player of another (players.c, weapons.c, units.c) as long as their
hands keep coming, else as before.
What the hands decide is only how the game looks: damage is the shooter's
report, which the host checks as any other (network_damage.c).
report, which the host checks as any other (network_damage.c), except that
its line of sight may start at the weapon hand as well as the camera.
A machine that does not know the message (a build without this, of the same
network version) drops it, as it drops any type past its own
+1
View File
@@ -7812,6 +7812,7 @@ void WINAPI D3DDevice_Present(CONST RECT *source_rectangle, CONST RECT *destinat
x, y + height, x + width, y, GL_COLOR_BUFFER_BIT, GL_LINEAR);
}
glBindVertexArray(device.vertex_array);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
platform_video_swap();
}
#ifndef HALO_GLES
+256
View File
@@ -0,0 +1,256 @@
#include "harness.h"
#include <math.h>
typedef unsigned short word;
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#include "config.inc"
/* A client's world: the host's objects, each here or not yet (the stream
makes them, and can trail the host's inventories, which go as datagrams);
units that carry weapons of one magazine. */
#define OBJECT_COUNT 256
#define OBJECT(n) (0x10000L | (n))
#define TAG_BLOCK_GET_ELEMENT(block, index, type) (&((type *)(block)->address)[index])
struct tag_block { long count; void *address; };
struct weapon_magazine_definition { short rounds_total_initial, rounds_total_maximum, rounds_loaded_maximum; };
struct weapon_definition { struct { struct tag_block magazines; } weapon; };
struct weapon_magazine { short rounds_total, rounds_loaded; };
struct weapon_datum { long definition_index; struct { struct weapon_magazine magazines[2]; real age; } weapon; };
struct unit_datum
{
struct { unsigned long damage_flags; } object;
struct
{
long player_index;
long weapon_object_indices[MAXIMUM_WEAPONS_PER_UNIT];
short current_weapon_index, desired_weapon_index;
char grenade_counts[NUMBER_OF_UNIT_GRENADE_TYPES];
short grenade_throw_state, current_grenade_index, desired_grenade_index;
} unit;
};
struct player_datum { short local_player_index; };
static struct { boolean here; short type; } objects[OBJECT_COUNT];
static struct unit_datum units[OBJECT_COUNT];
static struct weapon_datum weapons[OBJECT_COUNT];
static struct weapon_magazine_definition magazine_definition = {0, 500, 100};
static struct weapon_definition weapon_definition = {{{1, &magazine_definition}}};
static struct player_datum player;
static long now;
static boolean here(long index)
{
return index != NONE && (index >> 16) == 1 && (index & 0xFFFF) < OBJECT_COUNT && objects[index & 0xFFFF].here;
}
static boolean network_objects_client_has(long index) { return here(index); }
static void *object_try_and_get_and_verify_type(long index, unsigned long mask)
{
if (!here(index) || !TEST_FLAG(mask, objects[index & 0xFFFF].type))
return NULL;
return objects[index & 0xFFFF].type == _object_type_weapon ? (void *)&weapons[index & 0xFFFF] : (void *)&units[index & 0xFFFF];
}
static struct unit_datum *unit_get(long index)
{
CHECK(object_try_and_get_and_verify_type(index, _object_mask_unit), "not a unit here: %lx", index);
return &units[index & 0xFFFF];
}
static struct weapon_datum *weapon_try_and_get(long index) { return object_try_and_get_and_verify_type(index, _object_mask_weapon); }
static struct weapon_definition *weapon_definition_get(long index) { return &weapon_definition; }
static long game_time_get(void) { return now; }
static struct player_datum *player_get(long index) { return &player; }
static boolean distributed_player_is_local(long player_index) { return FALSE; }
static boolean distributed_client_own_settled(long time) { return TRUE; }
static void distributed_client_release_item(long index, boolean dropped) {}
static void unit_network_drop_weapon(long unit_index, short slot) { unit_get(unit_index)->unit.weapon_object_indices[slot] = NONE; }
static void unit_network_add_weapon(long unit_index, long weapon_index, short slot)
{
unit_get(unit_index)->unit.weapon_object_indices[slot] = weapon_index;
}
struct distributed_own_inventory;
static void distributed_client_note_own_inventory(struct distributed_own_inventory *own, long unit_index, boolean compare) {}
#include "under_test.inc"
/* the stream makes one of the host's objects here: a unit carrying nothing,
or a weapon */
static void arrive(long index, short type)
{
struct unit_datum *unit = &units[index & 0xFFFF];
short slot;
objects[index & 0xFFFF].here = TRUE;
objects[index & 0xFFFF].type = type;
memset(unit, 0, sizeof(*unit));
unit->unit.player_index = NONE;
unit->unit.current_weapon_index = NONE;
unit->unit.current_grenade_index = NONE;
for (slot = 0; slot < MAXIMUM_WEAPONS_PER_UNIT; slot++)
unit->unit.weapon_object_indices[slot] = NONE;
}
/* the host's inventory of a unit carrying two weapons (NONE: none) */
static struct distributed_inventory carrying(long unit_index, long first, long second, char grenades)
{
struct distributed_inventory inventory;
short slot;
memset(&inventory, 0, sizeof(inventory));
inventory.unit_index = unit_index;
inventory.grenade_counts[0] = grenades;
inventory.current_weapon_index = 0;
for (slot = 0; slot < MAXIMUM_WEAPONS_PER_UNIT; slot++)
{
inventory.weapon_indices[slot] = slot == 0 ? first : slot == 1 ? second : NONE;
inventory.rounds_total[slot][0] = (short)(100 * (slot + 1));
inventory.rounds_loaded[slot][0] = (short)(10 * (slot + 1));
}
return inventory;
}
/* the host's inventory comes, as network_objects_handle_inventories takes it */
static void receive(struct distributed_inventory inventory)
{
distributed_client_apply_inventory(&inventory);
}
static boolean kept(long unit_index)
{
short index;
for (index = 0; index < objects_client_retried_inventory_count; index++)
{
if (objects_client_retried_inventories[index].inventory.unit_index == unit_index)
return TRUE;
}
return FALSE;
}
static long held(long unit_index, short slot) { return unit_get(unit_index)->unit.weapon_object_indices[slot]; }
static void reset(void)
{
memset(objects, 0, sizeof(objects));
memset(&player, 0, sizeof(player));
memset(objects_client_retried_inventories, 0, sizeof(objects_client_retried_inventories));
objects_client_retried_inventory_count = 0;
now = 1000;
}
int main(int argc, char **argv)
{
char const *case_name = argv[1];
long unit = OBJECT(1), first = OBJECT(2), second = OBJECT(3);
short index;
reset();
/* a player spawned: its inventory comes before its unit and its weapon,
is kept, and is applied in full the tick they have all come */
CASE("unit-late")
{
receive(carrying(unit, first, NONE, 3));
CHECK(kept(unit) && objects_client_retried_inventory_count == 1, "unit-late: not kept");
arrive(first, _object_type_weapon);
now++;
distributed_client_apply_retried_inventories();
CHECK(kept(unit), "unit-late: let go of before its unit came");
arrive(unit, _object_type_biped);
now++;
distributed_client_apply_retried_inventories();
CHECK(held(unit, 0) == first, "unit-late: its weapon not given once it came");
CHECK(units[1].unit.grenade_counts[0] == 3 && weapons[2].weapon.magazines[0].rounds_total == 100 &&
weapons[2].weapon.magazines[0].rounds_loaded == 10, "unit-late: its grenades or rounds not given");
CHECK(!kept(unit) && objects_client_retried_inventory_count == 0, "unit-late: kept once applied");
}
/* its unit here, one of its weapons not: what is here is given at once,
the rest once it comes */
CASE("weapon-late")
{
arrive(unit, _object_type_biped);
arrive(first, _object_type_weapon);
receive(carrying(unit, first, second, 2));
CHECK(held(unit, 0) == first && held(unit, 1) == NONE, "weapon-late: what is here not given at once");
CHECK(kept(unit), "weapon-late: not kept for the weapon to come");
now++;
distributed_client_apply_retried_inventories();
CHECK(kept(unit) && held(unit, 1) == NONE, "weapon-late: applied before its weapon came");
arrive(second, _object_type_weapon);
now++;
distributed_client_apply_retried_inventories();
CHECK(held(unit, 1) == second && weapons[3].weapon.magazines[0].rounds_total == 200,
"weapon-late: the weapon not given once it came");
CHECK(!kept(unit), "weapon-late: kept once applied");
}
/* a unit's newer inventory takes its older one's place */
CASE("newest")
{
receive(carrying(unit, first, NONE, 1));
now++;
receive(carrying(unit, first, NONE, 4));
CHECK(objects_client_retried_inventory_count == 1, "newest: %d kept of one unit", objects_client_retried_inventory_count);
arrive(unit, _object_type_biped);
arrive(first, _object_type_weapon);
now++;
distributed_client_apply_retried_inventories();
CHECK(units[1].unit.grenade_counts[0] == 4, "newest: not the newest applied");
}
/* the list full: one more takes the oldest's place, and a kept unit's
newer one its own */
CASE("full")
{
for (index = 0; index < MAXIMUM_CLIENT_RETRIED_INVENTORIES; index++)
{
now++;
receive(carrying(OBJECT(17 + index), NONE, NONE, 1));
}
CHECK(objects_client_retried_inventory_count == MAXIMUM_CLIENT_RETRIED_INVENTORIES,
"full: %d kept of %d", objects_client_retried_inventory_count, MAXIMUM_CLIENT_RETRIED_INVENTORIES);
now++;
receive(carrying(OBJECT(200), NONE, NONE, 1));
CHECK(!kept(OBJECT(17)) && kept(OBJECT(18)) && kept(OBJECT(200)), "full: not the oldest's place taken");
now++;
receive(carrying(OBJECT(201), NONE, NONE, 1));
CHECK(!kept(OBJECT(18)) && kept(OBJECT(19)) && kept(OBJECT(201)), "full: not the next oldest's taken");
now++;
receive(carrying(OBJECT(19), NONE, NONE, 2));
CHECK(kept(OBJECT(19)) && kept(OBJECT(20)), "full: a kept unit's newer one took another's place");
CHECK(objects_client_retried_inventory_count == MAXIMUM_CLIENT_RETRIED_INVENTORIES, "full: %d kept",
objects_client_retried_inventory_count);
}
/* a unit's whole inventory, come once all it names is here: its kept one
is let go of, and never applied over it */
CASE("forget")
{
long other = OBJECT(6);
receive(carrying(unit, first, NONE, 1));
receive(carrying(other, NONE, NONE, 1));
arrive(unit, _object_type_biped);
arrive(first, _object_type_weapon);
receive(carrying(unit, first, NONE, 5));
CHECK(!kept(unit) && kept(other) && objects_client_retried_inventory_count == 1, "forget: not let go of");
now++;
distributed_client_apply_retried_inventories();
CHECK(units[1].unit.grenade_counts[0] == 5, "forget: the older applied over the newer");
}
/* let go of EMPTY_INVENTORY_TICKS after the newest of its unit's came,
and not applied when its unit comes after */
CASE("expiry")
{
long newest = now + EMPTY_INVENTORY_TICKS / 2;
receive(carrying(unit, first, NONE, 1));
now = newest;
receive(carrying(unit, first, NONE, 1));
now = newest + EMPTY_INVENTORY_TICKS - 1;
distributed_client_apply_retried_inventories();
CHECK(kept(unit), "expiry: let go of %d ticks after the newest", EMPTY_INVENTORY_TICKS - 1);
now = newest + EMPTY_INVENTORY_TICKS;
distributed_client_apply_retried_inventories();
CHECK(!kept(unit), "expiry: kept %d ticks after the newest", EMPTY_INVENTORY_TICKS);
arrive(unit, _object_type_biped);
arrive(first, _object_type_weapon);
now++;
distributed_client_apply_retried_inventories();
CHECK(held(unit, 0) == NONE, "expiry: applied after it was let go of");
}
return 0;
}
@@ -0,0 +1,89 @@
"""A client's inventories from the host whose unit or weapons have not come yet: kept, and applied once they have."""
import re
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from harness import CHECK_FAILED, build, constant, enum_with, function, mutated, read, run, structure
CASES = ['unit-late', 'weapon-late', 'newest', 'full', 'forget', 'expiry']
CONTROLS = [
('else\n\t\tdistributed_client_retry_inventory(inventory);', 'else\n\t\t(void)0;', 'unit-late'),
('distributed_client_apply_inventory(&inventory);', '(void)inventory;', 'unit-late'),
('if (!distributed_client_weapon_here(inventory->weapon_indices[weapon_slot]))', 'if (FALSE)', 'weapon-late'),
('if (objects_client_retried_inventories[index].inventory.unit_index == inventory->unit_index)', 'if (FALSE)',
'newest'),
('.time < objects_client_retried_inventories[oldest].time', '.time > objects_client_retried_inventories[oldest].time',
'full'),
('objects_client_retried_inventory_count < MAXIMUM_CLIENT_RETRIED_INVENTORIES',
'objects_client_retried_inventory_count < MAXIMUM_CLIENT_RETRIED_INVENTORIES - 1', 'full'),
('objects_client_retried_inventories[index] =\n\t\t\t\tobjects_client_retried_inventories[--objects_client_retried_inventory_count];',
'--objects_client_retried_inventory_count;', 'forget'),
('distributed_client_forget_retried_inventory(inventory->unit_index);\n\telse', '(void)0;\n\telse', 'forget'),
('- retried->time >= EMPTY_INVENTORY_TICKS', '- retried->time > EMPTY_INVENTORY_TICKS', 'expiry'),
('\tretried->inventory = *inventory;\n\tretried->time = game_time_get();',
'\tif (retried->inventory.unit_index != inventory->unit_index)\n\t\tretried->time = game_time_get();\n'
'\tretried->inventory = *inventory;', 'expiry'),
]
def declaration(source, first, last):
"""The text from one marker to the end of another, as a static's declaration with its structure."""
start = source.index(first)
return source[start:source.index(last, start) + len(last)]
def generated(fault=None):
network = read('port/linux/game/network_objects.c')
units = read('source/units/units.h')
limits = read('port/linux/game/network_distributed.h') + read('port/linux/include/halo_port_limits.h')
config = '\n'.join([
'#define TICKS_PER_SECOND %d' % constant(read('source/cseries/cseries.h'), 'TICKS_PER_SECOND'),
'#define MAXIMUM_OBJECTS_PER_MAP %d' % constant(read('source/objects/objects.h'), 'MAXIMUM_OBJECTS_PER_MAP'),
'#define MAXIMUM_TRACKED_PLAYERS %d' % constant(limits, 'MAXIMUM_TRACKED_PLAYERS'),
re.search(r'^#define AGE_SCALE .*$', network, re.M).group(0),
enum_with(read('source/objects/object_types.h'), '_object_mask_unit'),
enum_with(read('source/objects/objects.h'), '_object_dead_bit'),
enum_with(read('source/game/players.h'), 'MAXIMUM_LOCAL_PLAYERS'),
enum_with(units, 'MAXIMUM_WEAPONS_PER_UNIT ='),
enum_with(units, 'NUMBER_OF_UNIT_GRENADE_TYPES\n'),
enum_with(units, '_unit_grenade_throw_idle ='),
enum_with(network, 'EMPTY_INVENTORY_TICKS ='),
structure(network, 'distributed_inventory'),
]) + '\n'
code = '\n'.join([
declaration(network, 'static struct distributed_own_inventory\n',
'objects_client_own_inventories[MAXIMUM_LOCAL_PLAYERS];'),
declaration(network, 'static struct distributed_retried_inventory\n',
'static short objects_client_retried_inventory_count;'),
function(network, 'distributed_client_weapon_here'),
function(network, 'distributed_client_inventory_whole'),
function(network, 'distributed_client_retry_inventory'),
function(network, 'distributed_client_forget_retried_inventory'),
function(network, 'distributed_client_apply_inventory'),
function(network, 'distributed_client_apply_retried_inventories'),
])
if fault:
code = mutated(code, *fault)
return (('config.inc', config), ('under_test.inc', code))
@pytest.mark.parametrize('case', CASES)
def test_case(case):
status, output = run(build('retried_inventories', generated()), case)
assert status == 0, output
@pytest.mark.parametrize('before,after,case', CONTROLS)
def test_negative_control(before, after, case):
status, output = run(build('retried_inventories', generated((before, after))), case)
assert status == CHECK_FAILED, output
def test_wired():
# Each client tick applies the kept ones; a new game keeps none.
network = read('port/linux/game/network_objects.c')
assert 'distributed_client_apply_retried_inventories();' in function(network, 'network_objects_client_tick')
assert 'objects_client_retried_inventory_count = 0;' in function(network, 'network_objects_new_game')