Compare commits

..
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 6b77fe428a Show how joining an online game goes on the system link list
p2p_join_state tells the menus whether the host is being reached, was
reached (its game is in the list), did not answer, could not be reached
(both routers too strict: allow UPnP) or was lost; the join screen shows
it under the code's status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:37:25 +02:00
Stefan MaierandClaude Opus 5.5 0badc451cb Join online games with a short code in VR
A hosted game gets an 8-character join code (Crockford base 32) besides
its invite link, shown under the lobby's buttons. On the system link list,
X opens the game's keyboard to type a code in; the code's topic and key
come from a slow PBKDF2 derivation, the host answers an ask on that topic
through the signalling brokers with its invite sealed with the key, and
the joiner joins as with the link. Codes in join_link.txt work too.

Also: the clipboard text last seen is kept from run to run, so a run's
own invite is not joined again at the next start; in menus a held stick
repeats after 350 ms and then every 90 ms, and its threshold is 70%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:32:50 +02:00
Stefan MaierandClaude Opus 5.5 39a6cc9794 Hold a VR player's relayed aim for a second
Where another machine's VR player aims and fires from is kept for 30
ticks after their hands last came (was 10), over networks that deliver
in bursts; a held grenade still lets go after 10, and a throw is taken
within its own 30.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:36:34 +02:00
Stefan MaierandClaude Opus 5.5 35b90f26fc Send VR players' hands to the other machines
A new distributed message (_distributed_message_vr_poses, last in the
list so builds without it drop it) carries a VR player's controller aim,
muzzle offset, held grenade and last hand throw every tick: clients send
their own to the host, the host relays them with its own. Other machines
aim that player's weapon where the controller points, fire from the hand,
hold the grenade while the hand does and throw it as the hand threw it.
Damage stays the shooter's checked report; network version unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:58:52 +02:00
17 changed files with 1351 additions and 13 deletions

No files matched your search

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