Compare commits

...
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 d8034d37fd Raise the rocket launcher's scope disc 3.5 cm more (18 cm)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:03:19 +02:00
Stefan MaierandClaude Opus 5.5 23d14319a7 Raise the rocket launcher's scope disc 3.5 cm more
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:14:34 +02:00
Stefan MaierandClaude Opus 5.5 ff5b91f120 Raise the VR scope's disc over the rocket launcher's tube
The disc sits 7 cm over the hand, where a rifle's or pistol's scope is;
over the rocket launcher, carried higher on the shoulder, it was inside
the tube. A weapon may raise it (halo_vr_set_scope_raise): the rocket
launcher by 11 cm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:07:26 +02:00
Stefan MaierandClaude Opus 5.5 66c0d33e34 List public online games and join them from a panel
A host can make its game public (Y in the lobby, or network.public_game):
it then announces name, map, game type, players and join code every 10 s,
retained, on hcvr/1/g<network version>/<id> through the signalling
brokers, and clears it when it stops. On the system link list, X opens an
online games panel listing the public games heard lately (stale ones
dropped after 45 s) and a row to type a code; A joins by the game's code.
Announcements carry no addresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:00:57 +02:00
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
11 changed files with 822 additions and 31 deletions

No files matched your search

+3
View File
@@ -140,6 +140,9 @@ out (halo_vr_scope_pass). */
int halo_vr_scope(short local_player_index, float magnification, float position[3], float forward[3], float up[3], int halo_vr_scope(short local_player_index, float magnification, float position[3], float forward[3], float up[3],
float *vertical_field_of_view); float *vertical_field_of_view);
int halo_vr_scope_pass(void); int halo_vr_scope_pass(void);
/* how much higher than a rifle's the weapon in hand has its scope's disc
(metres; render.c, before halo_vr_scope) */
void halo_vr_set_scope_raise(float metres);
void halo_vr_capture_scope(float left, float top, float right, float bottom); void halo_vr_capture_scope(float left, float top, float right, float bottom);
/* where the weapon's gun node is as drawn, at the ready (world units; /* where the weapon's gun node is as drawn, at the ready (world units;
first_person_weapons.c, each frame): shots, the crosshair and the laser first_person_weapons.c, each frame): shots, the crosshair and the laser
+58
View File
@@ -331,6 +331,8 @@ static struct
unsigned char join_host_hash[P2P_KEY_HASH_SIZE]; unsigned char join_host_hash[P2P_KEY_HASH_SIZE];
unsigned char join_token[P2P_TOKEN_SIZE]; unsigned char join_token[P2P_TOKEN_SIZE];
unsigned long join_time; unsigned long join_time;
/* how the latest join goes, for the menus (p2p_join_state) */
int join_state;
char clipboard[P2P_LINK_SIZE]; char clipboard[P2P_LINK_SIZE];
int has_clipboard; 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]; 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); 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); platform_log("Internet play: %s %s: %s", peer->is_host ? "host" : "player", peer->name, reason);
if (peer->connected) if (peer->connected)
{ {
@@ -1029,6 +1037,8 @@ static void peer_heard(struct peer *peer, unsigned long address, unsigned short
address_text(address, port, text)); address_text(address, port, text));
if (peer->is_host) 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)) if (p2p.joining && !memcmp(p2p.join_host, peer->identifier, P2P_IDENTIFIER_SIZE))
{ {
p2p.joining = 0; 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_host_hash, hash, sizeof(hash));
memcpy(p2p.join_token, token, sizeof(token)); memcpy(p2p.join_token, token, sizeof(token));
p2p.join_requested = 1; p2p.join_requested = 1;
p2p.join_state = P2P_JOIN_REACHING;
return 1; return 1;
} }
@@ -2344,6 +2355,51 @@ int p2p_join_code_status(void)
return status; return status;
} }
void p2p_set_public_game(int public_game)
{
pthread_mutex_lock(&p2p_lock);
p2p_list_set_public(public_game);
pthread_mutex_unlock(&p2p_lock);
}
int p2p_public_game(void)
{
int result;
pthread_mutex_lock(&p2p_lock);
result = p2p_list_public();
pthread_mutex_unlock(&p2p_lock);
return result;
}
void p2p_set_game_description(const char *name, const char *map, const char *mode, int players,
int maximum_players)
{
pthread_mutex_lock(&p2p_lock);
p2p_list_set_description(name, map, mode, players, maximum_players);
pthread_mutex_unlock(&p2p_lock);
}
int p2p_public_games(struct p2p_public_game *games, int maximum)
{
int count;
pthread_mutex_lock(&p2p_lock);
count = p2p.running ? p2p_list_games(games, maximum) : 0;
pthread_mutex_unlock(&p2p_lock);
return count;
}
int p2p_join_state(void)
{
int state;
pthread_mutex_lock(&p2p_lock);
state = p2p.running ? p2p.join_state : P2P_JOIN_NONE;
pthread_mutex_unlock(&p2p_lock);
return state;
}
void p2p_join_code_clear_status(void) void p2p_join_code_clear_status(void)
{ {
pthread_mutex_lock(&p2p_lock); pthread_mutex_lock(&p2p_lock);
@@ -2381,6 +2437,8 @@ static void update_joining(void)
{ {
p2p.joining = 0; p2p.joining = 0;
p2p_signal_stop_joining(); 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"); platform_log("Internet play: no answer from the invite's host; it may have stopped hosting or quit");
} }
} }
+37
View File
@@ -43,6 +43,43 @@ int p2p_join_code(char *text, int size);
int p2p_join_with_code(const char *typed); int p2p_join_with_code(const char *typed);
int p2p_join_code_status(void); int p2p_join_code_status(void);
void p2p_join_code_clear_status(void); void p2p_join_code_clear_status(void);
/* how the latest join of an invite or code goes (the VR port's, for the
menus): reaching the host, connected (its game is in the system link list),
or why not */
enum
{
P2P_JOIN_NONE,
P2P_JOIN_REACHING,
P2P_JOIN_CONNECTED,
P2P_JOIN_NO_ANSWER,
P2P_JOIN_BLOCKED,
P2P_JOIN_LOST,
};
int p2p_join_state(void);
/* Public games (p2p_list.c): the hosted game listed for anyone or not, and
what it is (the game's server, as it changes); and the games others list,
joined by their codes (p2p_join_with_code) */
enum
{
P2P_PUBLIC_NAME_SIZE = 16,
P2P_PUBLIC_MAP_SIZE = 32,
P2P_PUBLIC_MODE_SIZE = 24,
};
struct p2p_public_game
{
char name[P2P_PUBLIC_NAME_SIZE];
char map[P2P_PUBLIC_MAP_SIZE];
char mode[P2P_PUBLIC_MODE_SIZE];
char code[12];
int players;
int maximum_players;
};
void p2p_set_public_game(int public_game);
int p2p_public_game(void);
void p2p_set_game_description(const char *name, const char *map, const char *mode, int players,
int maximum_players);
int p2p_public_games(struct p2p_public_game *games, int maximum);
/* this machine's identifier, which its XNADDR carries (6 bytes) */ /* this machine's identifier, which its XNADDR carries (6 bytes) */
const unsigned char *p2p_identifier(void); const unsigned char *p2p_identifier(void);
+18
View File
@@ -111,6 +111,9 @@ int p2p_signal_connected(void);
a message to a topic through every broker */ a message to a topic through every broker */
void p2p_signal_sync_topics(void); void p2p_signal_sync_topics(void);
void p2p_signal_publish(const char *topic, const unsigned char *payload, int size); void p2p_signal_publish(const char *topic, const unsigned char *payload, int size);
/* the same, kept by the brokers for whoever listens later (empty: no
longer) */
void p2p_signal_publish_retained(const char *topic, const unsigned char *payload, int size);
/* ---------- p2p_code.c: join codes (under p2p_lock) */ /* ---------- p2p_code.c: join codes (under p2p_lock) */
@@ -136,6 +139,21 @@ 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); int p2p_code_received(const char *topic, const unsigned char *payload, int size);
void p2p_code_update(void); void p2p_code_update(void);
/* ---------- p2p_list.c: public games (under p2p_lock) */
/* hosting: whether the game is listed, and what it is */
void p2p_list_set_public(int public_game);
int p2p_list_public(void);
void p2p_list_set_description(const char *name, const char *map, const char *mode, int players,
int maximum_players);
/* the games listed lately; asking keeps this machine listening a while */
int p2p_list_games(struct p2p_public_game *games, int maximum);
/* (p2p_signal.c) the topic to listen on (empty: none); a message on one
(nonzero if it was a listing's); and each pass, whether hosting */
const char *p2p_list_topic(void);
int p2p_list_received(const char *topic, const unsigned char *payload, int size);
void p2p_list_update(int hosting);
/* ---------- p2p_crypto.c */ /* ---------- p2p_crypto.c */
enum enum
+335
View File
@@ -0,0 +1,335 @@
/*
P2P_LIST.C
Public games (the VR port's): a host may list its game for anyone, who
finds it in a list of online games and joins it by its join code
(p2p_code.c), without a server of this project's.
- A public game's host announces it through the signalling brokers
(p2p_signal.c) every ANNOUNCE_INTERVAL, retained, on a topic of its own
under hcvr/1/g<network version>/: its name, map, game type, players, its
join code, and the time. Retained, a machine that starts listening gets
every game's latest at once. A host that stops (it closes its game, or
makes it private) clears its topic; one that quits without that leaves
its last announcement, which listeners drop once it is older than
STALE_SECONDS (by the machines' clocks).
- A machine that shows the list listens on the topics of its network
version (a wildcard), and keeps the games heard of lately.
- An announcement carries no address: a machine reaches a public game's
host only by its code, as anyone given the code does. Anyone can announce
anything, so what is shown is checked to be printable and in bounds, and
a game that is not what it says is only a code that does not answer.
*/
#include "platform.h"
#include "posix.h"
#include "port_config.h"
#include "p2p_internal.h"
#include "halo_port_limits.h"
#include <stdio.h>
#include <string.h>
#include <time.h>
enum
{
ANNOUNCEMENT_VERSION = 1,
/* milliseconds */
ANNOUNCE_INTERVAL = 10000,
/* a listener's interest lapses this long after the menu last asked */
WATCH_TIME = 3000,
STALE_SECONDS = 45,
MAXIMUM_LISTED_GAMES = 32,
};
/* what goes to the brokers (byte for byte) */
struct announcement
{
char magic[4];
unsigned char version;
unsigned char players;
unsigned char maximum_players;
unsigned char flags;
unsigned char time[8];
char code[8];
char name[P2P_PUBLIC_NAME_SIZE];
char map[P2P_PUBLIC_MAP_SIZE];
char mode[P2P_PUBLIC_MODE_SIZE];
};
struct listed_game
{
int used;
char topic[P2P_CODE_TOPIC_SIZE];
unsigned long heard_time;
long long announced_time;
struct p2p_public_game game;
};
static struct
{
/* hosting (whether public: network.public_game, until Y changes it) */
int initialized;
int public_game;
char topic[P2P_CODE_TOPIC_SIZE];
int announced;
unsigned long announced_time;
char name[P2P_PUBLIC_NAME_SIZE];
char map[P2P_PUBLIC_MAP_SIZE];
char mode[P2P_PUBLIC_MODE_SIZE];
int players;
int maximum_players;
/* listening */
unsigned long watched_time;
int watching;
char watch_topic[P2P_CODE_TOPIC_SIZE];
struct listed_game games[MAXIMUM_LISTED_GAMES];
} list;
static int list_elapsed(unsigned long since, unsigned long time)
{
return (unsigned int)(p2p_now() - since) >= (unsigned int)time;
}
/* text as shown: printable ASCII only, ended */
static void list_text(char *destination, int size, const char *source, int source_size)
{
int index;
for (index = 0; index < size - 1 && index < source_size && source[index]; index++)
destination[index] = source[index] >= 32 && source[index] < 127 ? source[index] : '?';
destination[index] = 0;
}
static long long list_time_now(void)
{
return (long long)time(NULL);
}
static void list_topics_make(void)
{
if (!list.topic[0])
{
unsigned char random[8];
char hex[17];
posix_random_bytes(random, sizeof(random));
p2p_hex(random, sizeof(random), hex);
snprintf(list.topic, sizeof(list.topic), "hcvr/1/g%d/%s", HALO_PORT_NETWORK_VERSION, hex);
}
if (!list.watch_topic[0])
snprintf(list.watch_topic, sizeof(list.watch_topic), "hcvr/1/g%d/+", HALO_PORT_NETWORK_VERSION);
}
static void list_announce(void)
{
struct announcement announcement;
char code[16];
long long now = list_time_now();
int index;
if (!p2p_code_hosted(code, sizeof(code)))
return;
memset(&announcement, 0, sizeof(announcement));
memcpy(announcement.magic, "HCVR", 4);
announcement.version = ANNOUNCEMENT_VERSION;
announcement.players = (unsigned char)(list.players < 0 ? 0 : list.players > 255 ? 255 : list.players);
announcement.maximum_players =
(unsigned char)(list.maximum_players < 0 ? 0 : list.maximum_players > 255 ? 255 : list.maximum_players);
for (index = 0; index < 8; index++)
announcement.time[index] = (unsigned char)(now >> (8 * index));
/* (the code without its dash) */
memcpy(announcement.code, code, 4);
memcpy(announcement.code + 4, code + 5, 4);
memcpy(announcement.name, list.name, sizeof(announcement.name));
memcpy(announcement.map, list.map, sizeof(announcement.map));
memcpy(announcement.mode, list.mode, sizeof(announcement.mode));
p2p_signal_publish_retained(list.topic, (const unsigned char *)&announcement, sizeof(announcement));
if (!list.announced)
platform_log("Internet play: the game is listed as public (%s, %s, %s)", list.name, list.map, list.mode);
list.announced = 1;
list.announced_time = p2p_now();
}
static void list_withdraw(void)
{
if (!list.announced)
return;
p2p_signal_publish_retained(list.topic, NULL, 0);
platform_log("Internet play: the game is no longer listed as public");
list.announced = 0;
}
/* ---------- p2p.c's side (under p2p_lock) */
void p2p_list_set_public(int public_game)
{
if (public_game == list.public_game)
return;
list.public_game = public_game;
list_topics_make();
if (!public_game)
list_withdraw();
else
list.announced_time = p2p_now() - ANNOUNCE_INTERVAL;
}
int p2p_list_public(void)
{
return list.public_game;
}
void p2p_list_set_description(const char *name, const char *map, const char *mode, int players,
int maximum_players)
{
int changed = strncmp(list.name, name, sizeof(list.name) - 1) || strncmp(list.map, map, sizeof(list.map) - 1) ||
strncmp(list.mode, mode, sizeof(list.mode) - 1) || players != list.players ||
maximum_players != list.maximum_players;
list_text(list.name, sizeof(list.name), name, P2P_PUBLIC_NAME_SIZE);
list_text(list.map, sizeof(list.map), map, P2P_PUBLIC_MAP_SIZE);
list_text(list.mode, sizeof(list.mode), mode, P2P_PUBLIC_MODE_SIZE);
list.players = players;
list.maximum_players = maximum_players;
/* (a change goes out soon, not at the next interval) */
if (changed && list.announced && list_elapsed(list.announced_time, 1000))
list.announced_time = p2p_now() - ANNOUNCE_INTERVAL;
}
int p2p_list_games(struct p2p_public_game *games, int maximum)
{
int count = 0;
int index;
list.watched_time = p2p_now();
list.watching = 1;
for (index = 0; index < MAXIMUM_LISTED_GAMES && count < maximum; index++)
{
if (list.games[index].used)
games[count++] = list.games[index].game;
}
return count;
}
/* ---------- p2p_signal.c's side (under p2p_lock) */
const char *p2p_list_topic(void)
{
return list.watching ? list.watch_topic : "";
}
int p2p_list_received(const char *topic, const unsigned char *payload, int size)
{
struct announcement announcement;
struct listed_game *slot = NULL;
long long announced = 0;
char code[16];
int index;
if (!list.watch_topic[0] || strncmp(topic, list.watch_topic, strlen(list.watch_topic) - 1))
return 0;
/* (this machine's own, as the broker echoes it) */
if (!strcmp(topic, list.topic))
return 1;
for (index = 0; index < MAXIMUM_LISTED_GAMES; index++)
{
if (list.games[index].used && !strcmp(list.games[index].topic, topic))
slot = &list.games[index];
}
if (size != (int)sizeof(announcement))
{
/* (empty: withdrawn) */
if (slot && size == 0)
slot->used = 0;
return 1;
}
memcpy(&announcement, payload, sizeof(announcement));
if (memcmp(announcement.magic, "HCVR", 4) || announcement.version != ANNOUNCEMENT_VERSION ||
!announcement.maximum_players || announcement.players > announcement.maximum_players)
{
return 1;
}
for (index = 0; index < 8; index++)
announced |= (long long)announcement.time[index] << (8 * index);
if (announced < list_time_now() - STALE_SECONDS || announced > list_time_now() + STALE_SECONDS)
{
if (slot)
slot->used = 0;
return 1;
}
snprintf(code, sizeof(code), "%.4s-%.4s", announcement.code, announcement.code + 4);
for (index = 0; code[index]; index++)
{
if (index != 4 && !((code[index] >= '0' && code[index] <= '9') || (code[index] >= 'A' && code[index] <= 'Z')))
return 1;
}
if (!slot)
{
int oldest = -1;
for (index = 0; index < MAXIMUM_LISTED_GAMES && !slot; index++)
{
if (!list.games[index].used)
slot = &list.games[index];
else if (oldest < 0 || list.games[index].announced_time < list.games[oldest].announced_time)
oldest = index;
}
if (!slot)
slot = &list.games[oldest];
memset(slot, 0, sizeof(*slot));
snprintf(slot->topic, sizeof(slot->topic), "%s", topic);
}
slot->used = 1;
slot->heard_time = p2p_now();
slot->announced_time = announced;
list_text(slot->game.name, sizeof(slot->game.name), announcement.name, sizeof(announcement.name));
list_text(slot->game.map, sizeof(slot->game.map), announcement.map, sizeof(announcement.map));
list_text(slot->game.mode, sizeof(slot->game.mode), announcement.mode, sizeof(announcement.mode));
snprintf(slot->game.code, sizeof(slot->game.code), "%s", code);
slot->game.players = announcement.players;
slot->game.maximum_players = announcement.maximum_players;
return 1;
}
void p2p_list_update(int hosting)
{
int index;
if (!list.initialized)
{
list.initialized = 1;
if (!list.public_game)
list.public_game = config_boolean("network.public_game");
}
list_topics_make();
/* hosting a public game: announced every so often; withdrawn when it
is not */
if (hosting && list.public_game)
{
p2p_signal_start();
if (p2p_signal_connected() && list_elapsed(list.announced_time, ANNOUNCE_INTERVAL))
list_announce();
}
else if (list.announced)
{
list_withdraw();
}
/* listening while the menu asks */
if (list.watching && list_elapsed(list.watched_time, WATCH_TIME))
{
list.watching = 0;
memset(list.games, 0, sizeof(list.games));
p2p_signal_sync_topics();
}
else if (list.watching)
{
p2p_signal_start();
p2p_signal_sync_topics();
}
for (index = 0; index < MAXIMUM_LISTED_GAMES; index++)
{
if (list.games[index].used && list.games[index].announced_time < list_time_now() - STALE_SECONDS)
list.games[index].used = 0;
}
}
+32 -4
View File
@@ -157,6 +157,8 @@ struct broker
/* (join codes' topics: p2p_code.c) */ /* (join codes' topics: p2p_code.c) */
char code_host_topic[TOPIC_SIZE]; char code_host_topic[TOPIC_SIZE];
char code_look_topic[TOPIC_SIZE]; char code_look_topic[TOPIC_SIZE];
/* (public games': p2p_list.c) */
char list_topic[TOPIC_SIZE];
unsigned char input[BUFFER_SIZE]; unsigned char input[BUFFER_SIZE];
int input_size; int input_size;
unsigned char output[BUFFER_SIZE]; unsigned char output[BUFFER_SIZE];
@@ -328,6 +330,7 @@ static void broker_close(struct broker *broker, int failed)
broker->join_topic[0] = 0; broker->join_topic[0] = 0;
broker->code_host_topic[0] = 0; broker->code_host_topic[0] = 0;
broker->code_look_topic[0] = 0; broker->code_look_topic[0] = 0;
broker->list_topic[0] = 0;
if (failed) if (failed)
broker->failures++; broker->failures++;
} }
@@ -417,8 +420,8 @@ static void broker_publish(struct broker *broker, const char *topic, const unsig
/* the topics a ready broker should be subscribed to */ /* the topics a ready broker should be subscribed to */
static void broker_sync_topics(struct broker *broker) static void broker_sync_topics(struct broker *broker)
{ {
const char *wanted[4]; const char *wanted[5];
char *had[4]; char *had[5];
int index; int index;
if (broker->state != _broker_ready) if (broker->state != _broker_ready)
@@ -426,11 +429,13 @@ static void broker_sync_topics(struct broker *broker)
wanted[0] = signalling.hosting ? signalling.host_topic : ""; wanted[0] = signalling.hosting ? signalling.host_topic : "";
wanted[1] = signalling.joining ? signalling.join_topic : ""; wanted[1] = signalling.joining ? signalling.join_topic : "";
p2p_code_topics(&wanted[2], &wanted[3]); p2p_code_topics(&wanted[2], &wanted[3]);
wanted[4] = p2p_list_topic();
had[0] = broker->host_topic; had[0] = broker->host_topic;
had[1] = broker->join_topic; had[1] = broker->join_topic;
had[2] = broker->code_host_topic; had[2] = broker->code_host_topic;
had[3] = broker->code_look_topic; had[3] = broker->code_look_topic;
for (index = 0; index < 4; index++) had[4] = broker->list_topic;
for (index = 0; index < 5; index++)
{ {
if (!strcmp(wanted[index], had[index])) if (!strcmp(wanted[index], had[index]))
continue; continue;
@@ -949,7 +954,7 @@ static void publish_received(struct broker *broker, const char *topic, const uns
if (size > MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD) if (size > MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD)
return; return;
if (p2p_code_received(topic, payload, size)) if (p2p_code_received(topic, payload, size) || p2p_list_received(topic, payload, size))
return; return;
if (signalling.hosting && !strcmp(topic, signalling.host_topic)) if (signalling.hosting && !strcmp(topic, signalling.host_topic))
{ {
@@ -1203,6 +1208,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)) if (signalling.joining && elapsed(signalling.join_sent_time, JOIN_INTERVAL))
send_join(); send_join();
p2p_code_update(); p2p_code_update();
p2p_list_update(signalling.hosting);
} }
int p2p_signal_connected(void) int p2p_signal_connected(void)
@@ -1236,6 +1242,28 @@ void p2p_signal_publish(const char *topic, const unsigned char *payload, int siz
publish_everywhere(topic, payload, size); publish_everywhere(topic, payload, size);
} }
void p2p_signal_publish_retained(const char *topic, const unsigned char *payload, int size)
{
unsigned char body[2 + TOPIC_SIZE + MAXIMUM_MESSAGE_SIZE];
int index;
if (size > MAXIMUM_MESSAGE_SIZE)
return;
for (index = 0; index < signalling.broker_count; index++)
{
struct broker *broker = &signalling.brokers[index];
int body_size;
if (broker->state != _broker_ready)
continue;
body_size = put_string(body, topic);
if (size)
memcpy(body + body_size, payload, (size_t)size);
/* (PUBLISH, at most once, retained: an empty one clears it) */
broker_send(broker, 0x31, body, body_size + size);
}
}
void p2p_signal_host(const unsigned char *token) void p2p_signal_host(const unsigned char *token)
{ {
if (!signalling.has_nonce_key) if (!signalling.has_nonce_key)
+3
View File
@@ -275,6 +275,9 @@ static const struct config_setting config_settings[] =
"clipboard) that lets whoever has it join over the internet; opening a\n" "clipboard) that lets whoever has it join over the internet; opening a\n"
"link (or copying one before switching to the game) joins. Only people\n" "link (or copying one before switching to the game) joins. Only people\n"
"with the invite can join. Off keeps system link to the local network." }, "with the invite can join. Off keeps system link to the local network." },
{ "network.public_game", _config_boolean, "false", "HALO_NET_PUBLIC_GAME", _environment_value, _platform_all,
"Host games as public: listed for anyone among the online games (the\n"
"system link screen's X), as Y in a hosted game's lobby makes them." },
{ "network.join_from_clipboard", _config_boolean, "true", "HALO_NET_JOIN_FROM_CLIPBOARD", _environment_value, { "network.join_from_clipboard", _config_boolean, "true", "HALO_NET_JOIN_FROM_CLIPBOARD", _environment_value,
_platform_all, _platform_all,
"Join the game of an invite link found on the clipboard when the game\n" "Join the game of an invite link found on the clipboard when the game\n"
+10 -1
View File
@@ -1957,6 +1957,14 @@ The world around it stays as it is (zooming the whole view sickens). */
#define SCOPE_ABOVE_HAND 0.07f #define SCOPE_ABOVE_HAND 0.07f
#define SCOPE_AHEAD_OF_HAND 0.22f #define SCOPE_AHEAD_OF_HAND 0.22f
/* (the weapon in hand's: halo_vr_set_scope_raise) */
static float scope_raise;
void halo_vr_set_scope_raise(float metres)
{
scope_raise = metres;
}
int halo_vr_scope(short local_player_index, float magnification, float position[3], float forward[3], float up[3], int halo_vr_scope(short local_player_index, float magnification, float position[3], float forward[3], float up[3],
float *vertical_field_of_view) float *vertical_field_of_view)
{ {
@@ -1975,7 +1983,7 @@ int halo_vr_scope(short local_player_index, float magnification, float position[
game_to_local(right, local_right); game_to_local(right, local_right);
for (axis = 0; axis < 3; axis++) for (axis = 0; axis < 3; axis++)
{ {
vr.scope_pose.position[axis] = hand->aim.position[axis] + local_up[axis] * SCOPE_ABOVE_HAND + vr.scope_pose.position[axis] = hand->aim.position[axis] + local_up[axis] * (SCOPE_ABOVE_HAND + scope_raise) +
local_forward[axis] * SCOPE_AHEAD_OF_HAND; local_forward[axis] * SCOPE_AHEAD_OF_HAND;
} }
if (barrel_known()) if (barrel_known())
@@ -2167,6 +2175,7 @@ int halo_vr_scope(short local_player_index, float magnification, float position[
return 0; return 0;
} }
int halo_vr_scope_pass(void) { return 0; } int halo_vr_scope_pass(void) { return 0; }
void halo_vr_set_scope_raise(float metres) { (void)metres; }
void halo_vr_capture_scope(float left, float top, float right, float bottom) void halo_vr_capture_scope(float left, float top, float right, float bottom)
{ {
(void)left; (void)top; (void)right; (void)bottom; (void)left; (void)top; (void)right; (void)bottom;
+276 -26
View File
@@ -5593,15 +5593,18 @@ static boolean ui_mouse_menus_active(
#ifdef HALO_ANDROID #ifdef HALO_ANDROID
/* ---------- join codes (the VR port's) /* ---------- join codes (the VR port's)
Internet play's join codes (port/linux/src/p2p_code.c): the host's lobby Internet play's join codes (port/linux/src/p2p_code.c) and public games
shows its game's code, and on the screen of games to join, X opens the (p2p_list.c): the host's lobby shows its game's code, and Y there lists the
keyboard to type one in, which then joins as an invite link does (the game game for anyone or not; on the screen of games to join, X opens a panel of
shows up in the list once its host is reached). */ the public games and a way to type a code in, either of which then joins as
an invite link does (the game shows up in the list once its host is
reached). */
int p2p_join_code(char *text, int size); int p2p_join_code(char *text, int size);
int p2p_join_with_code(char const *typed); int p2p_join_with_code(char const *typed);
int p2p_join_code_status(void); int p2p_join_code_status(void);
void p2p_join_code_clear_status(void); void p2p_join_code_clear_status(void);
int p2p_join_state(void);
enum enum
{ {
@@ -5615,12 +5618,74 @@ enum
_join_code_offline, _join_code_offline,
}; };
enum
{
/* p2p.h's */
_join_state_none,
_join_state_reaching,
_join_state_connected,
_join_state_no_answer,
_join_state_blocked,
_join_state_lost,
};
struct ui_public_game
{
/* p2p.h's struct p2p_public_game */
char name[16];
char map[32];
char mode[24];
char code[12];
int players;
int maximum_players;
};
void p2p_set_public_game(int public_game);
int p2p_public_game(void);
int p2p_public_games(struct ui_public_game *games, int maximum);
enum
{
MAXIMUM_SHOWN_PUBLIC_GAMES = 32,
/* the online games panel's rows: "enter a code" and the games */
ONLINE_PANEL_ROWS = 11,
};
static struct static struct
{ {
boolean keyboard_launched; boolean keyboard_launched;
wchar_t typed[12]; wchar_t typed[12];
/* the panel of online games, over the screen of games to join */
boolean panel_open;
short selection;
short scroll;
short game_count;
struct ui_public_game games[MAXIMUM_SHOWN_PUBLIC_GAMES];
} ui_join_code_globals; } ui_join_code_globals;
/* the multiplayer maps' names, as the menus show them */
static char const *ui_join_code_map_name(
char const *map)
{
static char const *const names[][2] = {
{ "beavercreek", "Battle Creek" }, { "bloodgulch", "Blood Gulch" }, { "boardingaction", "Boarding Action" },
{ "carousel", "Derelict" }, { "chillout", "Chill Out" }, { "damnation", "Damnation" },
{ "dangercanyon", "Danger Canyon" }, { "deathisland", "Death Island" }, { "gephyrophobia", "Gephyrophobia" },
{ "hangemhigh", "Hang 'Em High" }, { "icefields", "Ice Fields" }, { "infinity", "Infinity" },
{ "longest", "Longest" }, { "prisoner", "Prisoner" }, { "putput", "Chiron TL-34" },
{ "ratrace", "Rat Race" }, { "sidewinder", "Sidewinder" }, { "timberland", "Timberland" },
{ "wizard", "Wizard" },
};
short index;
for (index = 0; index < (short)NUMBEROF(names); index++)
{
if (!strcmp(map, names[index][0]))
return names[index][1];
}
return map;
}
static boolean ui_join_code_screen_active( static boolean ui_join_code_screen_active(
char const *name) char const *name)
{ {
@@ -5628,29 +5693,111 @@ static boolean ui_join_code_screen_active(
strcmp(widget_globals.active_widgets[0]->name, name) == 0; strcmp(widget_globals.active_widgets[0]->name, name) == 0;
} }
/* X on the screen of games to join: the keyboard, for a code (TRUE: the static void ui_join_code_launch_keyboard(
event is taken) */ void)
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; ui_join_code_globals.typed[0] = 0;
virtual_keyboard_set_caption(L"Enter the join code of an online game"); 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)) if (!virtual_keyboard_launch(ui_join_code_globals.typed, sizeof(ui_join_code_globals.typed), 8))
{ {
virtual_keyboard_set_caption(NULL); virtual_keyboard_set_caption(NULL);
return FALSE; return;
} }
p2p_join_code_clear_status(); p2p_join_code_clear_status();
ui_join_code_globals.keyboard_launched = TRUE; ui_join_code_globals.keyboard_launched = TRUE;
}
/* the panel's input: the stick or the pad's arrows choose a row, A takes
it, B closes it */
static void ui_join_code_panel_event(
struct event_record const *event)
{
short rows = (short)(ui_join_code_globals.game_count + 1);
short move = 0;
if (event->type == _event_type_left_stick)
{
if (event->data.stick.y == SHORT_MAX)
move = -1;
else if (event->data.stick.y == SHORT_MIN)
move = 1;
}
else if (event->type == _event_type_button && event->data.button.value == 1)
{
switch (event->data.button.index)
{
case _gamepad_binary_button_dpad_up: move = -1; break;
case _gamepad_binary_button_dpad_down: move = 1; break;
case _gamepad_analog_button_b:
case _gamepad_binary_button_back:
ui_join_code_globals.panel_open = FALSE;
break;
case _gamepad_analog_button_x:
ui_join_code_globals.panel_open = FALSE;
ui_join_code_launch_keyboard();
break;
case _gamepad_analog_button_a:
case _gamepad_binary_button_start:
ui_join_code_globals.panel_open = FALSE;
if (ui_join_code_globals.selection == 0 ||
ui_join_code_globals.selection > ui_join_code_globals.game_count)
{
ui_join_code_launch_keyboard();
}
else
{
p2p_join_code_clear_status();
p2p_join_with_code(ui_join_code_globals.games[ui_join_code_globals.selection - 1].code);
}
break;
}
}
if (move)
{
ui_join_code_globals.selection = (short)PIN(ui_join_code_globals.selection + move, 0, rows - 1);
if (ui_join_code_globals.selection < ui_join_code_globals.scroll)
ui_join_code_globals.scroll = ui_join_code_globals.selection;
else if (ui_join_code_globals.selection >= ui_join_code_globals.scroll + ONLINE_PANEL_ROWS)
ui_join_code_globals.scroll = (short)(ui_join_code_globals.selection - ONLINE_PANEL_ROWS + 1);
}
}
/* X on the screen of games to join: the panel of online games (and in it,
everything); Y in a hosted game's lobby: whether the game is public. TRUE:
the event is taken. */
static boolean ui_join_code_take_event(
struct widget_instance *widget,
struct event_record const *event)
{
if (widget != widget_globals.active_widgets[0])
return FALSE;
if (ui_join_code_globals.panel_open)
{
if (!ui_join_code_screen_active("server_list_screen"))
{
ui_join_code_globals.panel_open = FALSE;
return FALSE;
}
ui_join_code_panel_event(event);
return TRUE; return TRUE;
} }
if (event->type != _event_type_button || event->data.button.value != 1)
return FALSE;
if (event->data.button.index == _gamepad_analog_button_x && ui_join_code_screen_active("server_list_screen"))
{
ui_join_code_globals.panel_open = TRUE;
ui_join_code_globals.selection = 0;
ui_join_code_globals.scroll = 0;
return TRUE;
}
if (event->data.button.index == _gamepad_analog_button_y &&
ui_join_code_screen_active("connected_pregame_screen") && global_network_game_server_get())
{
p2p_set_public_game(!p2p_public_game());
return TRUE;
}
return FALSE;
}
/* the keyboard closed: the code typed, looked up */ /* the keyboard closed: the code typed, looked up */
static void ui_join_code_update( static void ui_join_code_update(
@@ -5675,17 +5822,20 @@ static void ui_join_code_update(
p2p_join_with_code(text); p2p_join_with_code(text);
} }
static void ui_join_code_draw_line( /* text in bounds, left, right or centred (0, 1, 2) */
static void ui_join_code_draw_text(
short x0,
short x1,
short y0, short y0,
char const *text, char const *text,
boolean icons, boolean icons,
boolean centered) short justification,
real_argb_color const *color)
{ {
rectangle2d bounds; rectangle2d bounds;
wchar_t wide[96]; wchar_t wide[96];
short index; short index;
long font_index = tag_loaded(FONT_GROUP_TAG, "ui\\small_ui"); 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) if (font_index == NONE)
return; return;
@@ -5694,15 +5844,97 @@ static void ui_join_code_draw_line(
wide[index] = 0; wide[index] = 0;
bounds.y0 = y0; bounds.y0 = y0;
bounds.y1 = (short)(y0 + 20); bounds.y1 = (short)(y0 + 20);
bounds.x0 = centered ? 0 : 48; bounds.x0 = x0;
bounds.x1 = centered ? 640 : 600; bounds.x1 = x1;
draw_string_set_draw_mode(font_index, NONE, centered ? 2 : 0, 0, &color); draw_string_set_draw_mode(font_index, NONE, justification, 0, color);
if (icons) if (icons)
draw_string_and_hack_in_icons(&bounds, &bounds, NULL, 0, wide, FALSE); draw_string_and_hack_in_icons(&bounds, &bounds, NULL, 0, wide, FALSE);
else else
rasterizer_draw_unicode_string(&bounds, &bounds, NULL, 0, wide); rasterizer_draw_unicode_string(&bounds, &bounds, NULL, 0, wide);
} }
static void ui_join_code_draw_line(
short y0,
char const *text,
boolean icons,
boolean centered)
{
real_argb_color color = { 1.0f, 0.85f, 0.85f, 0.85f };
ui_join_code_draw_text((short)(centered ? 0 : 48), (short)(centered ? 640 : 600), y0, text, icons,
(short)(centered ? 2 : 0), &color);
}
static void ui_join_code_draw_quad(
short x0,
short y0,
short x1,
short y1,
unsigned long color)
{
rectangle2d bounds;
bounds.x0 = x0;
bounds.y0 = y0;
bounds.x1 = x1;
bounds.y1 = y1;
draw_quad(&bounds, color);
}
/* the panel of online games: "enter a code", then the public games */
static void ui_join_code_render_panel(
void)
{
real_argb_color title_color = { 1.0f, 0.55f, 0.75f, 1.0f };
real_argb_color text_color = { 1.0f, 0.85f, 0.85f, 0.85f };
real_argb_color dim_color = { 1.0f, 0.6f, 0.6f, 0.6f };
char line[64];
short row;
ui_join_code_globals.game_count =
(short)p2p_public_games(ui_join_code_globals.games, MAXIMUM_SHOWN_PUBLIC_GAMES);
ui_join_code_globals.selection =
(short)PIN(ui_join_code_globals.selection, 0, ui_join_code_globals.game_count);
ui_join_code_draw_quad(40, 72, 600, 410, 0xF0050A18);
ui_join_code_draw_quad(40, 72, 600, 74, 0xFF3A6EA5);
ui_join_code_draw_quad(40, 408, 600, 410, 0xFF3A6EA5);
snprintf(line, sizeof(line), "ONLINE GAMES (%d public)", (int)ui_join_code_globals.game_count);
ui_join_code_draw_text(56, 584, 80, line, FALSE, 0, &title_color);
ui_join_code_draw_text(56, 216, 104, "Name", FALSE, 0, &dim_color);
ui_join_code_draw_text(216, 376, 104, "Map", FALSE, 0, &dim_color);
ui_join_code_draw_text(376, 516, 104, "Game", FALSE, 0, &dim_color);
ui_join_code_draw_text(516, 584, 104, "Players", FALSE, 1, &dim_color);
for (row = 0; row < ONLINE_PANEL_ROWS; row++)
{
short index = (short)(ui_join_code_globals.scroll + row);
short y = (short)(128 + row * 22);
if (index > ui_join_code_globals.game_count)
break;
if (index == ui_join_code_globals.selection)
ui_join_code_draw_quad(48, y, 592, (short)(y + 21), 0x803A6EA5);
if (index == 0)
{
ui_join_code_draw_text(56, 584, (short)(y + 1), "Enter a join code...", FALSE, 0, &text_color);
}
else
{
struct ui_public_game const *game = &ui_join_code_globals.games[index - 1];
ui_join_code_draw_text(56, 212, (short)(y + 1), game->name, FALSE, 0, &text_color);
ui_join_code_draw_text(216, 372, (short)(y + 1), ui_join_code_map_name(game->map), FALSE, 0,
&text_color);
ui_join_code_draw_text(376, 512, (short)(y + 1), game->mode, FALSE, 0, &text_color);
snprintf(line, sizeof(line), "%d/%d", game->players, game->maximum_players);
ui_join_code_draw_text(516, 584, (short)(y + 1), line, FALSE, 1, &text_color);
}
}
if (!ui_join_code_globals.game_count)
ui_join_code_draw_text(56, 584, 152, "No public games right now", FALSE, 0, &dim_color);
ui_join_code_draw_text(56, 584, 384, "%a-button Join %x-button Enter a code %b-button Back", TRUE, 0,
&text_color);
}
/* over the screens, as they are drawn */ /* over the screens, as they are drawn */
static void ui_join_code_render( static void ui_join_code_render(
void) void)
@@ -5713,7 +5945,12 @@ static void ui_join_code_render(
{ {
char const *status = NULL; char const *status = NULL;
ui_join_code_draw_line(372, "%x-button Join an online game with its code", TRUE, FALSE); if (ui_join_code_globals.panel_open)
{
ui_join_code_render_panel();
return;
}
ui_join_code_draw_line(372, "%x-button Online games: public ones, or join by a code", TRUE, FALSE);
switch (p2p_join_code_status()) switch (p2p_join_code_status())
{ {
case _join_code_looking: status = "Looking for the game of that code..."; break; case _join_code_looking: status = "Looking for the game of that code..."; break;
@@ -5723,17 +5960,30 @@ static void ui_join_code_render(
case _join_code_own: status = "That is this game's own code"; 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; 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) if (status)
ui_join_code_draw_line(392, status, FALSE, FALSE); ui_join_code_draw_line(392, status, FALSE, FALSE);
} }
else if (ui_join_code_screen_active("connected_pregame_screen") && global_network_game_server_get() && else if (ui_join_code_screen_active("connected_pregame_screen") && global_network_game_server_get() &&
p2p_join_code(code, sizeof(code))) p2p_join_code(code, sizeof(code)))
{ {
char line[48]; char line[96];
snprintf(line, sizeof(line), "Online join code: %s", code); snprintf(line, sizeof(line), "Online join code: %s %%y-button Public game: %s", code,
p2p_public_game() ? "Yes" : "No");
/* (under the buttons' key) */ /* (under the buttons' key) */
ui_join_code_draw_line(448, line, FALSE, TRUE); ui_join_code_draw_line(448, line, TRUE, TRUE);
} }
} }
@@ -475,6 +475,10 @@ symbols in this file:
/* port: internet play's Discord presence (port/linux/src/p2p.c) */ /* port: internet play's Discord presence (port/linux/src/p2p.c) */
void p2p_set_game_player_counts(int count, int maximum); void p2p_set_game_player_counts(int count, int maximum);
#ifdef HALO_ANDROID
void p2p_set_game_description(char const *name, char const *map, char const *mode, int players,
int maximum_players);
#endif
/* ---------- constants */ /* ---------- constants */
@@ -1308,6 +1312,42 @@ boolean network_game_server_idle(
/* (what Discord shows of a game hosted for internet play) */ /* (what Discord shows of a game hosted for internet play) */
p2p_set_game_player_counts(server->game.player_count, server->game.maximum_players); p2p_set_game_player_counts(server->game.player_count, server->game.maximum_players);
#ifdef HALO_ANDROID
/* (and what a public game's listing says: port/linux/src/p2p_list.c) */
{
char name[16], map[32], mode[24];
char const *map_name = server->game.map.name;
char const *separator;
short index;
for (index = 0; index < (short)(NUMBEROF(name) - 1) && server->game.name[index]; index++)
name[index] = server->game.name[index] < 128 ? (char)server->game.name[index] : '?';
name[index] = 0;
/* (the scenario's name, the last of its path) */
while ((separator = strpbrk(map_name, "\\/")) != NULL)
map_name = separator + 1;
csstrncpy(map, map_name, sizeof(map) - 1);
map[sizeof(map) - 1] = 0;
for (index = 0; index < (short)(NUMBEROF(mode) - 1) && index < 12 &&
server->game.variant.human_readable_game_description[index]; index++)
{
wchar_t character = server->game.variant.human_readable_game_description[index];
mode[index] = character < 128 ? (char)character : '?';
}
mode[index] = 0;
/* (a game type without a name of its own: its kind's) */
if (!mode[0])
{
static char const *const kinds[] = { "", "CTF", "Slayer", "Oddball", "King of the Hill", "Race" };
long kind = server->game.variant.game_engine_index;
csstrncpy(mode, kind > 0 && kind < (long)NUMBEROF(kinds) ? kinds[kind] : "Custom", sizeof(mode) - 1);
mode[sizeof(mode) - 1] = 0;
}
p2p_set_game_description(name, map, mode, server->game.player_count, server->game.maximum_players);
}
#endif
if (network_game_server_game_is_valid(server)) if (network_game_server_game_is_valid(server))
{ {
+10
View File
@@ -746,6 +746,16 @@ static void render_vr_scope(
if (zoom_level == NONE || player_index == NONE || player_get(player_index)->unit_index == NONE) if (zoom_level == NONE || player_index == NONE || player_get(player_index)->unit_index == NONE)
return; return;
magnification = unit_get_zoom_magnification(player_get(player_index)->unit_index, zoom_level); magnification = unit_get_zoom_magnification(player_get(player_index)->unit_index, zoom_level);
{
/* (a weapon whose body stands higher over the hand than a rifle's
has its scope's disc higher: the rocket launcher's tube) */
struct unit_datum *unit = unit_get(player_get(player_index)->unit_index);
long weapon_index = unit->unit.current_weapon_index >= 0 ?
unit->unit.weapon_object_indices[unit->unit.current_weapon_index] : NONE;
char const *name = weapon_index != NONE ? tag_get_name(object_get(weapon_index)->definition_index) : NULL;
halo_vr_set_scope_raise(name && strstr(name, "rocket launcher") ? 0.18f : 0.0f);
}
if (!halo_vr_scope(window->local_player_index, magnification, position, forward, up, &vertical_field_of_view)) if (!halo_vr_scope(window->local_player_index, magnification, position, forward, up, &vertical_field_of_view))
return; return;
scope_window = *window; scope_window = *window;