Files
ClassicWasTaken--HaloSteamF…/resources/frame-controls.patch
T

6392 lines
337 KiB
Diff

diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.coop.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.coop.xml
index 1368453f1fd9f19937f27149eb2c9928becf72e2..8612491537dd482e464f9c583c6e101ae00f2883 100644
--- a/port/assets/menus/ce/main_menu.multiplayer_type_select.coop.xml
+++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.coop.xml
@@ -2,35 +2,54 @@
<!-- The port's multiplayer screens, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/multiplayer_type_select/multiplayer_type_coop_item" type="text" left="51" width="232" height="32" bitmap="bitmaps/list_item_bkd" string_list="main_menu/multiplayer_type_select/multiplayer_options" string_index="7" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="5">
- <on event="a" run="port coop begin" open="main_menu/multiplayer_type_select/coop/player_2_profile_screen" branch="true"/>
- <on event="start" run="port coop begin" open="main_menu/multiplayer_type_select/coop/player_2_profile_screen" branch="true"/>
+ <on event="a" run="port coop begin" open="main_menu/multiplayer_type_select/coop/network_screen" branch="true"/>
+ <on event="start" run="port coop begin" open="main_menu/multiplayer_type_select/coop/network_screen" branch="true"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
- <widget name="main_menu/multiplayer_type_select/coop/player_2_profile_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
+ <widget name="main_menu/multiplayer_type_select/coop/network_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
+ <on event="b" run="port coop begin" back="true"/>
+ <on event="back" run="port coop begin" back="true"/>
<child widget="main_menu/new_select/sel_list_desc_bkd"/>
- <child widget="main_menu/multiplayer_type_select/coop/player_2_profile_list"/>
- <child widget="main_menu/multiplayer_type_select/coop/header_player_2"/>
- <child widget="main_menu/multiplayer_type_select/coop/player_2_help" x="20" y="416"/>
- </widget>
- <widget name="main_menu/multiplayer_type_select/coop/header_player_2" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/coop/header_player_2">
- </widget>
- <widget name="main_menu/multiplayer_type_select/coop/player_2_profile_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/profile_manager/player_profile_extended_desc">
- <data input="3wide player profile list update"/>
- <on event="created" run="port coop player 2 list initialize"/>
- <on event="deleted" run="player profile list dispose"/>
- <on event="custom_activation" run="port coop player 2" open="main_menu/solo_level_select/solo_level_select_screen" branch="true"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_0" x="20" y="73"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_1" x="20" y="103"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_2" x="20" y="133"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_3" x="20" y="163"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_4" x="20" y="193"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_5" x="20" y="223"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_6" x="20" y="253"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_7" x="20" y="283"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_8" x="20" y="313"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_9" x="20" y="343"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_10" x="20" y="373"/>
- <child widget="main_menu/multiplayer_type_select/coop/player_2_button_bar" y="414"/>
+ <child widget="main_menu/multiplayer_type_select/coop/network_title" x="35" y="25"/>
+ <child widget="main_menu/multiplayer_type_select/coop/network_list"/>
+ <child widget="main_menu/multiplayer_type_select/coop/network_help" x="30" y="340"/>
+ <child widget="common_button_back" x="510" y="415"/>
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/network_title" type="text" controller="1" width="570" height="35" text="CO-OP CAMPAIGN" font="ui\large_ui" color="#FF2896FF" text_flags="no_focus_test">
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/network_list" type="column_list" width="640" height="310" flags="pass_unhandled_to_focused_child up_down_tabs_children">
+ <child widget="main_menu/multiplayer_type_select/coop/coop_host_lan" x="45" y="90"/>
+ <child widget="main_menu/multiplayer_type_select/coop/coop_join_lan" x="45" y="132"/>
+ <child widget="main_menu/multiplayer_type_select/coop/coop_host_online" x="45" y="174"/>
+ <child widget="main_menu/multiplayer_type_select/coop/coop_join_online" x="45" y="216"/>
+ <child widget="main_menu/multiplayer_type_select/coop/coop_join_invite" x="45" y="258"/>
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/coop_host_lan" type="text" width="550" height="35" bitmap="bitmaps/list_item_bkd" text="HOST LOCAL (LAN)" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="5">
+ <on event="a" run="port coop begin"/>
+ <on event="start" run="port coop begin"/>
+ <on event="left_mouse" run="mouse emit accept event"/>
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/coop_join_lan" type="text" width="550" height="35" bitmap="bitmaps/list_item_bkd" text="JOIN LOCAL (LAN)" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="5">
+ <on event="a" run="port coop begin"/>
+ <on event="start" run="port coop begin"/>
+ <on event="left_mouse" run="mouse emit accept event"/>
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/coop_host_online" type="text" width="550" height="35" bitmap="bitmaps/list_item_bkd" text="HOST ONLINE (INTERNET)" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="5">
+ <on event="a" run="port coop begin"/>
+ <on event="start" run="port coop begin"/>
+ <on event="left_mouse" run="mouse emit accept event"/>
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/coop_join_online" type="text" width="550" height="35" bitmap="bitmaps/list_item_bkd" text="JOIN ONLINE (INTERNET)" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="5">
+ <on event="a" run="port coop begin"/>
+ <on event="start" run="port coop begin"/>
+ <on event="left_mouse" run="mouse emit accept event"/>
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/coop_join_invite" type="text" width="550" height="35" bitmap="bitmaps/list_item_bkd" text="JOIN INVITE LINK" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="5">
+ <on event="a" run="port coop begin"/>
+ <on event="start" run="port coop begin"/>
+ <on event="left_mouse" run="mouse emit accept event"/>
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/network_help" type="text" controller="1" width="580" height="65" text="One player per headset or PC. LAN uses your local network.&#10;Online hosts: share your invite, or choose PUBLIC in Server Setup." font="ui\small_ui" color="#FFFFFFFF" text_flags="no_focus_test">
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_0" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd_top" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
@@ -109,8 +128,6 @@
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
- <widget name="main_menu/multiplayer_type_select/coop/player_2_help" type="text" width="350" height="24" text="Player 2: choose with your controller." font="ui\small_ui" color="#FF2896FF" text_y="5" text_flags="no_focus_test">
- </widget>
<widget name="main_menu/multiplayer_type_select/coop/player_2_button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<data input="common button bar update"/>
<child widget="main_menu/profile_manager/profile_manager_button_ok" x="380" y="1"/>
diff --git a/port/linux/arm64/host_vr.c b/port/linux/arm64/host_vr.c
index 4379c9516d90c051e288a68122b522680618906b..cac1ba7b43500f691adb4f5131da9a14905a6aff 100644
--- a/port/linux/arm64/host_vr.c
+++ b/port/linux/arm64/host_vr.c
@@ -875,9 +875,20 @@ int host_vr_locate(struct vr_host_views *views)
XrViewState state = { XR_TYPE_VIEW_STATE };
XrView located[2] = { { XR_TYPE_VIEW }, { XR_TYPE_VIEW } };
XrSpaceLocation location = { XR_TYPE_SPACE_LOCATION };
+ const XrViewStateFlags eye_pose_flags = XR_VIEW_STATE_ORIENTATION_VALID_BIT | XR_VIEW_STATE_POSITION_VALID_BIT;
+ const XrSpaceLocationFlags space_pose_flags = XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | XR_SPACE_LOCATION_POSITION_VALID_BIT;
uint32_t count = 0;
int eye, hand, valid;
+ /* A failed fresh locate must not keep a preceding frame's valid pose. */
+ for (eye = 0; eye < 2; eye++)
+ views->eye[eye].valid = 0;
+ views->head.valid = 0;
+ for (hand = 0; hand < 2; hand++)
+ {
+ views->aim[hand].valid = 0;
+ views->grip[hand].valid = 0;
+ }
if (!vr.frame_begun)
return 0;
locate.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
@@ -885,7 +896,8 @@ int host_vr_locate(struct vr_host_views *views)
locate.space = vr.play_space;
if (xrLocateViews(vr.session, &locate, &state, 2, &count, located) != XR_SUCCESS || count != 2)
return 0;
- valid = (state.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) != 0;
+ /* The game consumes both rotation and origin from these 6DoF poses. */
+ valid = (state.viewStateFlags & eye_pose_flags) == eye_pose_flags;
for (eye = 0; eye < 2; eye++)
{
pose_from_xr(&located[eye].pose, valid, &views->eye[eye]);
@@ -895,17 +907,17 @@ int host_vr_locate(struct vr_host_views *views)
views->fov[eye][3] = tanf(located[eye].fov.angleDown);
}
if (xrLocateSpace(vr.view_space, vr.play_space, vr.frame.predictedDisplayTime, &location) == XR_SUCCESS)
- pose_from_xr(&location.pose, (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0, &views->head);
+ pose_from_xr(&location.pose, (location.locationFlags & space_pose_flags) == space_pose_flags, &views->head);
for (hand = 0; hand < 2; hand++)
{
location.type = XR_TYPE_SPACE_LOCATION;
location.next = NULL;
if (xrLocateSpace(vr.aim_spaces[hand], vr.play_space, vr.frame.predictedDisplayTime, &location) == XR_SUCCESS)
- pose_from_xr(&location.pose, (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0,
+ pose_from_xr(&location.pose, (location.locationFlags & space_pose_flags) == space_pose_flags,
&views->aim[hand]);
location.type = XR_TYPE_SPACE_LOCATION;
if (xrLocateSpace(vr.grip_spaces[hand], vr.play_space, vr.frame.predictedDisplayTime, &location) == XR_SUCCESS)
- pose_from_xr(&location.pose, (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0,
+ pose_from_xr(&location.pose, (location.locationFlags & space_pose_flags) == space_pose_flags,
&views->grip[hand]);
}
return 1;
diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c
index 7d098faad67b44185095673ab6019668945f5b9f..19a001895aca7bbf2adfca4a20a2dab0185cee78 100644
--- a/port/linux/game/menu_functions.c
+++ b/port/linux/game/menu_functions.c
@@ -53,10 +53,10 @@ the one last used, else the first) and the game's saved game in it:
The Xbox's functions of those names take the Xbox's widgets (a spinner of
levels, the difficulty list itself), so ours run instead (menu_tags.c).
-Co-op (the Xbox's Cooperative Play, Multiplayer's CO-OP CAMPAIGN): "port coop
-begin" makes two players, player 1 on its profile; "port coop player 2"
-gives player 2 the profile chosen and the controller that chose it; then the
-campaign's New Game and difficulty start the game for both.
+Multiplayer's CO-OP CAMPAIGN hosts or joins over LAN or Internet, with one
+local player per machine in flat and VR play. Internet hosts start with
+network.coop_public's listing choice; private games use invite links.
+Competitive multiplayer retains its local-player controls.
The rest are the PC version's, which its menus (port/assets/menus/ce)
name and the Xbox's has not: they do nothing yet, and succeed, so that what
@@ -688,7 +688,7 @@ static void rows_update(struct widget_instance *list, short count, void (*row_te
/* player 1's profile, read again (the active one, else the one last used,
else the first), on the controller (in co-op, on the one that chose co-op:
-coop_begin); FALSE if there is none */
+network campaign); FALSE if there is none */
static boolean campaign_profile(short controller, struct player_profile *profile)
{
long profile_index = player_ui_get_active_player_profile_index(0);
@@ -1637,78 +1637,6 @@ static boolean profile_delete(void)
return TRUE;
}
-/* ---------- Co-op: the campaign for two players on this machine, in split
-screen (the Xbox's Cooperative Play, which the PC version has not):
-Multiplayer's CO-OP CAMPAIGN ("port coop begin"), player 2's profile, chosen
-with player 2's controller ("port coop player 2"), then New Game's levels
-(those either has reached) and difficulty, with either player's controller
-(ui_widget.c, widget_takes_events_of_controller). The main menu and
-Multiplayer go back to one player (main_menu_initialize,
-multiplayer_type_menu_initialize).
-With one gamepad, it is player 2's (pc_menu_split_players) */
-
-/* "port coop begin": two players, player 1 on its profile (campaign_profile)
-and the controller that chose co-op */
-static boolean coop_begin(short controller)
-{
- struct player_profile profile;
-
- player_spawn_count = 1;
- player_ui_reset_single_player_local_player_controllers();
- if (!campaign_profile(controller, &profile))
- return campaign_fail();
- player_spawn_count = 2;
- return TRUE;
-}
-
-/* "port coop player 2 list initialize": on player 2's profile, else the first
-that is not player 1's */
-static boolean coop_player2_list_initialize(struct widget_instance *list)
-{
- long player1 = player_ui_get_active_player_profile_index(0);
- long player2 = player_ui_get_active_player_profile_index(1);
- short index;
-
- profile_list_read(TRUE);
- profile_list.chosen = 0;
- for (index = profile_list.count - 1; index >= 0; index--)
- {
- if (profile_list.indices[index] != player1)
- profile_list.chosen = index;
- }
- for (index = 0; index < profile_list.count && player2 != NONE; index++)
- {
- if (profile_list.indices[index] == player2)
- profile_list.chosen = index;
- }
- profile_list.first = (short)PIN(profile_list.chosen - PROFILE_ROWS / 2, 0,
- MAX(0, profile_list.count + 1 - PROFILE_ROWS));
- focus_row(list, (short)(profile_list.chosen - profile_list.first));
- return TRUE;
-}
-
-/* "port coop player 2": the profile chosen made player 2's, on the controller
-that chose it, which must not be player 1's */
-static boolean coop_player2_choose(short controller)
-{
- struct player_profile profile;
-
- if (player_spawn_count < 2 || profile_list.chosen >= profile_list.count ||
- !player_profile_get(profile_list.indices[profile_list.chosen], &profile))
- {
- return campaign_fail();
- }
- if (controller == player_ui_get_single_player_local_player_controller(0))
- {
- display_error_text_deferred(L"Player 2 chooses their\r\nprofile with their own\r\ncontroller.",
- NONE);
- return campaign_fail();
- }
- player_ui_set_single_player_local_player_controller(1, controller);
- player_ui_set_active_player_profile(1, profile_list.indices[profile_list.chosen], &profile);
- return TRUE;
-}
-
/* ---------- Controls Setup: the keyboard and mouse's controls
(port/linux/include/halo_keyboard.h), shown a group at a time, two
bindings each */
@@ -2038,6 +1966,9 @@ void network_game_server_port_set_settings(wchar_t const *name, long maximum_pla
void network_game_server_port_set_cooperative_friendly_fire(short friendly_fire);
void *global_network_game_client_get(void);
void *global_network_game_server_get(void);
+void dispose_global_network_game_client(void);
+void dispose_global_network_game_server(void);
+void network_game_accept_remote_connections(boolean accept);
boolean network_game_is_splitscreen_local(void);
struct advertised_game *network_game_client_get_available_games(void *client);
boolean network_game_client_advertised_game_is_valid(struct advertised_game *game);
@@ -2098,6 +2029,15 @@ static struct
boolean game_private;
} multiplayer = { 0, 0, 0, { 0 }, 0, { 0 }, 0, 0, 0, 0, { 0 }, NUMBEROF(maximum_players) - 1 };
+/* Scoped to the synchronous screen load below. A cancelled/failed opening
+cannot leave the next ordinary Create Game choosing campaign maps. */
+static boolean cooperative_map_opening;
+/* Campaign intent persists while its lobby receives the game. */
+static boolean cooperative_network_game;
+static boolean lobby_join_reset(void);
+static short lobby_local_player_count(void);
+static short campaign_level_of(char const *map_name);
+
/* ---- a text field (Direct Link's link, the game's name): the keyboard
types into it (Ctrl+V pastes), its row's A (enter) is done, B (escape)
cancels */
@@ -2213,6 +2153,8 @@ static void multiplayer_mode_set(struct widget_instance *widget)
{
char const *name = widget->name;
+ cooperative_network_game = FALSE;
+ cooperative_map_opening = FALSE;
/* (an instance's name is cut to 31 characters:
"multiplayer_type_create_interne") */
multiplayer.mode =
@@ -2234,19 +2176,135 @@ static boolean multiplayer_player(short controller)
/* Create Game's ("join controller to mp game", first): player 1 in, and
the server made: an internet one (an invite, Discord) or a LAN one */
-static boolean multiplayer_host(struct widget_instance *widget, struct event_record *event, short controller,
- boolean *widget_deleted)
+static boolean multiplayer_host_mode(struct widget_instance *widget, struct event_record *event, short controller,
+ short mode, boolean cooperative, boolean *widget_deleted)
{
- multiplayer_mode_set(widget);
+ boolean result;
+
+ cooperative_map_opening = FALSE;
+ cooperative_network_game = cooperative;
if (!multiplayer_player(controller))
return FALSE;
+ multiplayer.mode = mode;
p2p_set_hosting_allowed(multiplayer.mode == _multiplayer_mode_host_internet);
/* (in the server browser, if PUBLIC: Server Setup's LISTING, which a
- new game starts with as network.host_public says) */
- multiplayer.game_private = !config_boolean("network.host_public");
+ new game starts with as network.host_public says; network campaign
+ uses co-op's choice from the start, PRIVATE by default) */
+ multiplayer.game_private = !config_boolean(cooperative ? "network.coop_public" : "network.host_public");
p2p_set_hosting_public(multiplayer.mode == _multiplayer_mode_host_internet && !multiplayer.game_private);
multiplayer.cooperative_maximum_players_set = FALSE;
- return ui_widget_port_host(widget, event, widget_deleted);
+ result = ui_widget_port_host(widget, event, widget_deleted);
+ if (!result)
+ {
+ game_connection_set(0);
+ p2p_set_hosting_allowed(FALSE);
+ p2p_set_hosting_public(FALSE);
+ }
+ return result;
+}
+
+static boolean multiplayer_host(struct widget_instance *widget, struct event_record *event, short controller,
+ boolean *widget_deleted)
+{
+ multiplayer_mode_set(widget);
+ return multiplayer_host_mode(widget, event, controller, multiplayer.mode, FALSE, widget_deleted);
+}
+
+/* Campaign uses one local player. Clear pending competitive additions. */
+static void cooperative_single_player(void)
+{
+ short controller;
+ player_spawn_count = 1;
+ player_ui_reset_single_player_local_player_controllers();
+ lobby_join_reset();
+ for (controller = 0; controller < MAXIMUM_LOCAL_PLAYERS; controller++)
+ player_ui_local_player_left_multiplayer_game(controller);
+}
+
+static void cooperative_session_cleanup(void)
+{
+ cooperative_map_opening = FALSE;
+ cooperative_network_game = FALSE;
+ dispose_global_network_game_client();
+ dispose_global_network_game_server();
+ network_game_accept_remote_connections(FALSE);
+ player_ui_clear_multiplayer_variant();
+ game_connection_set(0);
+ p2p_set_hosting_allowed(FALSE);
+ p2p_set_hosting_public(FALSE);
+ p2p_lobby_browse(FALSE);
+ cooperative_single_player();
+}
+
+static boolean cooperative_network_host(struct widget_instance *widget, struct event_record *event,
+ short controller, short mode, boolean *widget_deleted)
+{
+ boolean opened;
+ cooperative_session_cleanup();
+ if (mode == _multiplayer_mode_host_internet && !config_boolean("network.online"))
+ {
+ display_error_text_deferred(L"Internet play is off. Enable it\r\nin Network Setup, then restart Halo.", NONE);
+ return campaign_fail();
+ }
+ if (!multiplayer_host_mode(widget, event, controller, mode, TRUE, widget_deleted))
+ {
+ cooperative_session_cleanup();
+ return FALSE;
+ }
+ lan_mode = mode == _multiplayer_mode_host_lan;
+ /* Child creation is synchronous; only this opening chooses campaign maps. */
+ cooperative_map_opening = TRUE;
+ opened = ui_widget_port_open(widget,
+ "pc\\main_menu\\multiplayer_type_select\\connected\\connected_map_select_wrapper", widget_deleted);
+ cooperative_map_opening = FALSE;
+ if (!opened)
+ cooperative_session_cleanup();
+ return opened;
+}
+
+static boolean cooperative_network_join(struct widget_instance *widget, short controller,
+ short mode, boolean *widget_deleted)
+{
+ cooperative_session_cleanup();
+ if (mode != _multiplayer_mode_lan && !config_boolean("network.online"))
+ {
+ display_error_text_deferred(L"Internet play is off. Enable it\r\nin Network Setup, then restart Halo.", NONE);
+ return campaign_fail();
+ }
+ cooperative_network_game = TRUE;
+ multiplayer.mode = mode;
+ lan_mode = mode == _multiplayer_mode_lan;
+ if (!multiplayer_player(controller) || !ui_widget_port_open(widget,
+ "pc\\main_menu\\multiplayer_type_select\\join_game\\join_game_screen", widget_deleted))
+ {
+ cooperative_session_cleanup();
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/* Registered for both flat and VR. Named rows select LAN or Internet. */
+static boolean cooperative_begin(struct widget_instance *widget, struct event_record *event,
+ short controller, boolean *widget_deleted)
+{
+ struct player_profile profile;
+ if (!strcmp(widget->name, "coop_host_lan"))
+ return cooperative_network_host(widget, event, controller, _multiplayer_mode_host_lan, widget_deleted);
+ if (!strcmp(widget->name, "coop_host_online"))
+ return cooperative_network_host(widget, event, controller, _multiplayer_mode_host_internet, widget_deleted);
+ if (!strcmp(widget->name, "coop_join_lan"))
+ return cooperative_network_join(widget, controller, _multiplayer_mode_lan, widget_deleted);
+ if (!strcmp(widget->name, "coop_join_online"))
+ return cooperative_network_join(widget, controller, _multiplayer_mode_server_browser, widget_deleted);
+ if (!strcmp(widget->name, "coop_join_invite"))
+ return cooperative_network_join(widget, controller, _multiplayer_mode_direct_link, widget_deleted);
+ cooperative_session_cleanup();
+ if (!strcmp(widget->name, "network_screen"))
+ return TRUE;
+ if (strcmp(widget->name, "multiplayer_type_coop_item") || !campaign_profile(controller, &profile))
+ return campaign_fail();
+ cooperative_network_game = TRUE;
+ return TRUE;
}
/* ---- the map list (the Map screen's): its chooser's SINGLEPLAYER levels
@@ -2266,6 +2324,8 @@ static struct
short kind, step;
/* hosting over the network, which co-op needs */
boolean hosting;
+ /* Dedicated campaign routes cannot choose competitive maps. */
+ boolean campaign_only;
/* the multiplayer maps (ui_widget_port_multiplayer_maps) */
short map_count;
short first, chosen;
@@ -2312,9 +2372,12 @@ static boolean map_list_initialize(struct widget_instance *list)
map_list.hosting = global_network_game_server_get() != NULL && !network_game_is_splitscreen_local();
map_list.map_count = ui_widget_port_multiplayer_maps(&names, &last_used);
- map_list.kind = MAP_KIND_MULTIPLAYER;
+ /* Returning from Server Setup recreates this list; keep campaign intent. */
+ map_list.campaign_only = (cooperative_map_opening || cooperative_network_game) && map_list.hosting;
+ map_list.kind = map_list.campaign_only ? MAP_KIND_SINGLEPLAYER : MAP_KIND_MULTIPLAYER;
+ cooperative_map_opening = FALSE;
map_kind_set(list, map_list.kind);
- map_step_open(list, MAP_STEP_MAPS, last_used);
+ map_step_open(list, MAP_STEP_MAPS, map_list.kind == MAP_KIND_SINGLEPLAYER ? 0 : last_used);
return TRUE;
}
@@ -2323,7 +2386,8 @@ them the level or map chosen: its picture, name and words */
static void map_list_update(struct widget_instance *list)
{
struct widget_instance *description = list->parameters.list.extended_description;
- short kind = map_kind_shown(list, map_list.hosting ? NONE : MAP_KIND_MULTIPLAYER), count, entry;
+ short kind = map_kind_shown(list, map_list.campaign_only ? MAP_KIND_SINGLEPLAYER :
+ map_list.hosting ? NONE : MAP_KIND_MULTIPLAYER), count, entry;
if (kind != map_list.kind)
{
@@ -2358,13 +2422,22 @@ static boolean map_list_choose(struct widget_instance *list, boolean *widget_del
{
short chosen = map_list.chosen;
+ if (map_list.campaign_only)
+ map_list.kind = MAP_KIND_SINGLEPLAYER;
if (chosen >= map_step_count())
return campaign_fail();
if (map_list.step == MAP_STEP_DIFFICULTIES)
{
+ /* Ordinary Create Game cannot create a split campaign either. */
+ if (player_spawn_count >= 2 || lobby_local_player_count() > 1)
+ {
+ display_error_text_deferred(L"Campaign uses one player per machine.\r\nLeave extra local players first.", NONE);
+ return campaign_fail();
+ }
/* the co-op game set up, then Server Setup in the gametypes' place */
if (!ui_widget_port_cooperative_level_choose(main_get_solo_level_name(map_list.level), chosen))
return campaign_fail();
+ cooperative_network_game = TRUE;
return ui_widget_port_open(list, SERVER_SETUP_NAME, widget_deleted);
}
if (map_list.kind == MAP_KIND_MULTIPLAYER)
@@ -2385,6 +2458,11 @@ static boolean map_list_back(struct widget_instance *list, boolean *widget_delet
map_step_open(list, MAP_STEP_MAPS, map_list.level);
return TRUE;
}
+ if (map_list.campaign_only)
+ {
+ cooperative_session_cleanup();
+ map_list.campaign_only = FALSE;
+ }
ui_widget_port_go_back(list);
*widget_deleted = TRUE;
return TRUE;
@@ -2845,7 +2923,8 @@ static void browser_games_read(void)
struct advertised_game *game = &games[index];
if (network_game_client_advertised_game_is_valid(game) && !advertised_in_progress(game) == (pass == 0) &&
- game_from_peer(game) == (multiplayer.mode == _multiplayer_mode_direct_link))
+ game_from_peer(game) == (multiplayer.mode == _multiplayer_mode_direct_link) &&
+ (!cooperative_network_game || (!game->engine_type && campaign_level_of(game->map_name) != NONE)))
{
multiplayer.games[multiplayer.game_count++] = game;
}
@@ -2931,6 +3010,7 @@ static boolean lobby_browser_select(struct widget_instance *widget, short contro
static boolean browser_initialize(struct widget_instance *screen, struct event_record *event,
boolean *widget_deleted)
{
+ boolean result;
char const *title = multiplayer.mode == _multiplayer_mode_lan ? "header_lan" :
multiplayer.mode == _multiplayer_mode_direct_link ? "header_direct_link" : "header_server_browser";
char const *const titles[] = { "header_internet", "header_lan", "header_direct_link", "header_server_browser" };
@@ -2965,7 +3045,10 @@ static boolean browser_initialize(struct widget_instance *screen, struct event_r
one reaches its host, whose game the client then finds as Direct Link's) */
if (multiplayer.mode == _multiplayer_mode_server_browser)
lobby_browser_begin();
- return ui_widget_port_browse(screen, event, widget_deleted);
+ result = ui_widget_port_browse(screen, event, widget_deleted);
+ if (!result && cooperative_network_game)
+ cooperative_session_cleanup();
+ return result;
}
/* a scenario's name without its path */
@@ -3029,6 +3112,24 @@ static void game_map_name(struct advertised_game const *game, wchar_t *text)
map_display_name(game->map_name, text);
}
+/* Network campaign rows are identified by both their engine and map. */
+static boolean advertised_cooperative(struct advertised_game const *game)
+{
+ return game && !game->engine_type && campaign_level_of(game->map_name) != NONE;
+}
+
+/* Joining campaign from any browser also uses one local player. Competitive
+joins keep their existing local-player selection. */
+static boolean multiplayer_game_player(short controller, struct advertised_game const *game)
+{
+ if (advertised_cooperative(game))
+ {
+ cooperative_single_player();
+ cooperative_network_game = TRUE;
+ }
+ return multiplayer_player(controller);
+}
+
/* ---- Join Game > Server Browser: internet play's public games (their
hosts' listings, p2p_lobby.c). Joining one joins its invite (as Direct
Link's PASTE LINK); once its host is reached its game is among the client's,
@@ -3105,6 +3206,8 @@ static short lobby_browser_valid_games(struct p2p_listing *games, short count)
{
wchar_t name[P2P_LISTING_NAME_SIZE + 1];
short index;
+ if (cooperative_network_game && (games[read].engine_type || campaign_level_of(games[read].map) == NONE))
+ continue;
text_to_wide(games[read].name, name, NUMBEROF(name));
if (!player_name_clean(name, NUMBEROF(name)))
@@ -3347,7 +3450,7 @@ static boolean lobby_browser_select(struct widget_instance *widget, short contro
if (advertised_in_progress(found))
{
/* (player 1 joining it: others join them there) */
- if (!multiplayer_player(controller))
+ if (!multiplayer_game_player(controller, found))
return FALSE;
csmemcpy(multiplayer.preview_key_id, found->key_id, sizeof(multiplayer.preview_key_id));
csmemcpy(multiplayer.preview_xnaddr, found->xnaddr, sizeof(multiplayer.preview_xnaddr));
@@ -3355,7 +3458,7 @@ static boolean lobby_browser_select(struct widget_instance *widget, short contro
}
if (!found->open)
return campaign_fail();
- if (!multiplayer_player(controller))
+ if (!multiplayer_game_player(controller, found))
return FALSE;
return ui_widget_port_join(widget, found, LOBBY_NAME, widget_deleted);
}
@@ -3500,7 +3603,7 @@ static boolean browser_join(struct widget_instance *widget, short controller, bo
{
if (multiplayer.game_chosen >= multiplayer.game_count)
return campaign_fail();
- if (!multiplayer_player(controller))
+ if (!multiplayer_game_player(controller, multiplayer.games[multiplayer.game_chosen]))
return FALSE;
return ui_widget_port_join(widget, multiplayer.games[multiplayer.game_chosen], LOBBY_NAME, widget_deleted);
}
@@ -3523,7 +3626,7 @@ static boolean browser_select(struct widget_instance *widget, struct event_recor
advertised_in_progress(multiplayer.games[multiplayer.game_chosen]))
{
/* (player 1 joining it: others join them there) */
- if (!multiplayer_player(controller))
+ if (!multiplayer_game_player(controller, multiplayer.games[multiplayer.game_chosen]))
return FALSE;
csmemcpy(multiplayer.preview_key_id, multiplayer.games[multiplayer.game_chosen]->key_id,
sizeof(multiplayer.preview_key_id));
@@ -3656,10 +3759,27 @@ static boolean lobby_join_reset(void)
return TRUE;
}
+/* Campaign never adds another local controller, including while the game
+is loading its lobby. Other game modes keep competitive split-screen. */
+static boolean lobby_campaign_single_player(void)
+{
+ void *client = global_network_game_client_get();
+ return cooperative_network_game || (client && game_cooperative(network_game_client_get_game(client)));
+}
+
+static boolean campaign_local_player_block(void)
+{
+ lobby_join_reset();
+ display_error_text_deferred(L"One player per machine in campaign.\r\nJoin from another headset or PC.", NONE);
+ return campaign_fail();
+}
+
/* "port lobby add player" (ADD PLAYER): the next START of another controller
joins (one gamepad leaves the keyboard's controller for its own) */
static boolean lobby_add_player(void)
{
+ if (lobby_campaign_single_player())
+ return campaign_local_player_block();
if (lobby_local_player_count() >= MAXIMUM_LOCAL_PLAYERS)
return campaign_fail();
lobby_join.adding = TRUE;
@@ -3680,6 +3800,8 @@ static boolean lobby_join_start(struct widget_instance *widget, short controller
ui_widget_port_dispatch_event(focused_leaf(widget), BUTTON_START, controller, widget_deleted);
return FALSE;
}
+ if (lobby_campaign_single_player())
+ return campaign_local_player_block();
if (!game || network_game_client_get_state(client, &state_data) != _client_state_pregame)
return campaign_fail();
if (lobby_player_count >= game->maximum_players)
@@ -3743,6 +3865,8 @@ static boolean lobby_player_choose(void)
{
short controller = lobby_join.controller;
+ if (lobby_campaign_single_player())
+ return campaign_local_player_block();
if (controller == NONE || profile_list.chosen >= profile_list.count ||
!ui_widget_port_multiplayer_player(controller, profile_list.indices[profile_list.chosen]))
{
@@ -3758,7 +3882,15 @@ static void lobby_join_help(struct widget_instance *list)
wchar_t const *text = L"";
short controller;
- if (lobby_join.adding)
+ visible_set(named(screen_of(list), "lobby_button_add", 0), !lobby_campaign_single_player());
+ visible_set(named(screen_of(list), "preview_button_add", 0), !lobby_campaign_single_player());
+ focus_off_hidden(named(screen_of(list), "lobby_button_bar", 0));
+ focus_off_hidden(named(screen_of(list), "preview_button_bar", 0));
+ if (lobby_campaign_single_player())
+ {
+ text = L"One player per machine. Join from another headset or PC.";
+ }
+ else if (lobby_join.adding)
{
text = L"New player: press START.";
}
@@ -3984,6 +4116,8 @@ static boolean preview_add(struct widget_instance *widget, short controller, boo
ui_widget_port_dispatch_event(focused_leaf(widget), BUTTON_START, controller, widget_deleted);
return FALSE;
}
+ if (lobby_campaign_single_player() || advertised_cooperative(game))
+ return campaign_local_player_block();
if (!game || !game->open || count >= MAXIMUM_LOCAL_PLAYERS)
return campaign_fail();
if (game->player_count + count >= game->maximum_player_count)
@@ -4027,7 +4161,12 @@ static boolean preview_join(struct widget_instance *widget, short controller, bo
if (!game || !game->open)
return campaign_fail();
- if (!lobby_local_player_count() && !multiplayer_player(controller))
+ if (advertised_cooperative(game))
+ {
+ if (!multiplayer_game_player(controller, game))
+ return FALSE;
+ }
+ else if (!lobby_local_player_count() && !multiplayer_player(controller))
return FALSE;
return ui_widget_port_join(widget, game, LOBBY_NAME, widget_deleted);
}
@@ -4965,39 +5104,12 @@ boolean pc_menu_event_function_invoke(
}
else if (!strcmp(name, "port coop begin"))
{
- return coop_begin(controller);
+ return cooperative_begin(widget, event, controller, widget_deleted);
}
- else if (!strcmp(name, "port coop player 2 list initialize"))
+ else if (!strcmp(name, "port coop player 2 list initialize") || !strcmp(name, "port coop player 2"))
{
- return coop_player2_list_initialize(widget);
- }
- else if (!strcmp(name, "port coop player 2"))
- {
- return coop_player2_choose(controller);
- }
- else if (!strcmp(name, "request del player profile"))
- {
- return profile_delete_request();
- }
- else if (!strcmp(name, "final del player profile"))
- {
- return profile_delete();
- }
- else if (!strcmp(name, "color picker menu initialize"))
- {
- return color_list_initialize(widget);
- }
- else if (!strcmp(name, "color picker select color"))
- {
- return color_choose();
- }
- else if (!strcmp(name, "controls binding slot"))
- {
- controls_screen.slot = (short)!controls_screen.slot;
- }
- else if (!strcmp(name, "campaign menu init"))
- {
- return campaign_menu_initialize(controller);
+ /* Reject stale split-campaign handlers. */
+ return campaign_fail();
}
else if (!strcmp(name, "campaign menu continue"))
{
@@ -5038,6 +5150,9 @@ boolean pc_menu_event_function_invoke(
else if (!strcmp(name, "controls back handler") || !strcmp(name, "gamespy back handler") ||
!strcmp(name, "gamespy dismiss error") || !strcmp(name, "gamespy dismiss filters"))
{
+ if (!strcmp(name, "gamespy back handler") && cooperative_network_game &&
+ !global_network_game_server_get())
+ cooperative_session_cleanup();
ui_widget_port_go_back(widget);
*widget_deleted = TRUE;
}
diff --git a/port/linux/game/vr_render.c b/port/linux/game/vr_render.c
index 6b98a0b7739800bc5c90de07b0a821629e462705..1a290927f48107f754b10052a1ea4f98fd6af45b 100644
--- a/port/linux/game/vr_render.c
+++ b/port/linux/game/vr_render.c
@@ -30,9 +30,12 @@ they belong on the layer before the eyes.
#include "render/render_cameras_internal.h"
#include "cutscene/cinematics.h"
#include "camera/director.h"
+#include "camera/observer.h"
#include "game/players.h"
+#include "items/weapons.h"
#include "objects/objects.h"
#include "units/units.h"
+#include "units/unit_definitions.h"
#include "tag_files/tag_files.h"
#include "interface/hud.h"
#include "physics/collision_usage.h"
@@ -48,12 +51,9 @@ they belong on the layer before the eyes.
/* the union of the eyes' fields of view, a little wider (the eyes are
apart, and the culling's planes start at the centre) */
#define UNION_MARGIN 1.03f
-/* the crosshairs in the eyes (vr_render_crosshairs): where the aim meets
-the world, no farther than this (world units) nor nearer than this, the
-distance's changes smoothed over this time constant (seconds) */
+/* The crosshairs show the nominal primary projectile's current hit point,
+before spread and ballistics, no farther than this (world units). */
#define CROSSHAIR_FARTHEST 100.0f
-#define CROSSHAIR_NEAREST 0.15f
-#define CROSSHAIR_SMOOTHING 0.06f
static struct
{
@@ -64,6 +64,7 @@ static struct
real_rectangle2d bounds;
/* each eye's frustum, of the eye's own camera */
struct render_frustum eyes[2];
+ struct render_camera eye_cameras[2];
/* the player's eye as the game has it, and the heading the head's and
controllers' poses are turned by */
real_point3d base_position;
@@ -95,10 +96,8 @@ static struct
real_vector3d aim_forward;
real_vector3d aim_up;
/* the crosshairs were drawn in the eyes this frame (not in the HUD's
- layer), and the inverse of their distance, smoothed */
+ layer) */
boolean crosshairs_drawn;
- boolean crosshair_distance_valid;
- real crosshair_inverse_distance;
} vr_render;
/* where the crosshairs are drawn from the HUD's middle, in its 640x480
@@ -248,6 +247,58 @@ static long seated_unit(void)
return unit_index;
}
+/* Scripts run between eye passes too. Build the same HMD basis and centre
+origin as the world camera from a fresh view, rather than using the ended
+render pass or the controller's weapon-facing pose. */
+boolean vr_render_player_gaze(long unit_index, real_point3d *origin, real_vector3d *forward)
+{
+ long player_index = local_player_get_player_index(0);
+ struct unit_datum *unit;
+ struct halo_vr_view view;
+ struct observer_result const *observer;
+ director_perspective perspective;
+ real_point3d position;
+ real_vector3d pointing, offset;
+ real yaw, pitch = 0.0f, length;
+ boolean aiming;
+ if (!origin || !forward || player_index == NONE || unit_index == NONE ||
+ cinematic_in_progress() || (director_camera_scripted && *director_camera_scripted) ||
+ player_get(player_index)->unit_index != unit_index ||
+ !(unit = unit_try_and_get(unit_index)) || unit->unit.player_index != player_index ||
+ unit->object.type != _object_type_biped || TEST_FLAG(unit->object.damage_flags, _object_dead_bit))
+ return FALSE;
+ perspective = director_get_perspective(0);
+ if (perspective == _director_perspective_scripted || perspective == _director_perspective_neutral ||
+ director_inhibited_facing(0) || !halo_vr_view(&view) || !halo_vr_gaze_tracking_valid())
+ return FALSE;
+ observer = render_interpolation_camera(0, observer_get_camera(0));
+ if (!observer || !isfinite(view.body_yaw) || !isfinite(view.world_units_per_metre) ||
+ view.world_units_per_metre <= 0.0f)
+ return FALSE;
+ aiming = halo_vr_aiming();
+ yaw = aiming ? view.body_yaw : (real)atan2(observer->forward.j, observer->forward.i);
+ if (view.gamepad_pitch && aiming)
+ pitch = player_control_get_facing_angles(0)->pitch;
+ if (!isfinite(yaw) || !isfinite(pitch))
+ return FALSE;
+ position = observer->position;
+ if (view.vehicle_first_person && aiming && seated_unit() == unit_index)
+ unit_get_head_position(unit_index, &position);
+ view_rotate(yaw, pitch, view.head_forward, &pointing);
+ view_rotate(yaw, pitch, view.head_position, &offset);
+ position.x += offset.i * view.world_units_per_metre;
+ position.y += offset.j * view.world_units_per_metre;
+ position.z += offset.k * view.world_units_per_metre;
+ length = magnitude3d(&pointing);
+ if (!isfinite(position.x) || !isfinite(position.y) || !isfinite(position.z) ||
+ !isfinite(length) || length <= 0.0f)
+ return FALSE;
+ normalize3d(&pointing);
+ *origin = position;
+ *forward = pointing;
+ return TRUE;
+}
+
/* turns the window's camera into the VR frame's centre camera (both its
render and rasterizer cameras); FALSE without a VR frame to draw */
boolean vr_render_camera(struct render_camera *camera, struct render_camera *rasterizer_camera)
@@ -356,6 +407,7 @@ boolean vr_render_camera(struct render_camera *camera, struct render_camera *ras
eye_bounds.y0 = view->fov[eye][3] / tangent;
eye_bounds.y1 = view->fov[eye][2] / tangent;
render_camera_build_frustum(&eye_camera, &eye_bounds, &vr_render.eyes[eye], TRUE);
+ vr_render.eye_cameras[eye] = eye_camera;
render_camera_build_frustum(camera, &eye_bounds, &vr_render.eyes_at_centre[eye], TRUE);
}
{
@@ -543,6 +595,38 @@ void vr_render_end(void)
vr_render.seated_unit_index = NONE;
vr_render.infinite = FALSE;
vr_render_screen_point_end();
+ halo_vr_screen_space(0);
+}
+
+/* The legacy sun glow stamps an occlusion mask into the eye, copies that
+eye's pixels into mono 64x64 scratch textures, blurs and composites them.
+Run it once per eye: its CPU projection, depth test and source texture must
+all refer to the same eye. These screen coordinates are already eye-local. */
+boolean vr_render_sun_glow_begin(short eye, struct render_camera *camera, struct render_frustum *frustum)
+{
+ short k;
+ real depth[4];
+ if (!vr_render.active || eye < 0 || eye >= 2)
+ return FALSE;
+ for (k = 0; k < 4; k++)
+ depth[k] = frustum->projection_matrix[k][2];
+ *camera = vr_render.eye_cameras[eye];
+ *frustum = vr_render.eyes[eye];
+ /* Match the depth range currently used by the scene, as the world
+ geometry's eye transforms do; x/y use the eye's asymmetric projection. */
+ for (k = 0; k < 4; k++)
+ frustum->projection_matrix[k][2] = depth[k];
+ vr_render_screen_point_end();
+ halo_vr_screen_space(1);
+ halo_vr_draw_eye(eye);
+ return TRUE;
+}
+
+void vr_render_sun_glow_end(void)
+{
+ halo_vr_draw_eye(-1);
+ halo_vr_screen_space(0);
+ vr_render_screen_point_end();
}
/* the object is the player's unit, which the eye pass views its vehicle
@@ -578,41 +662,206 @@ boolean vr_render_hud_camera(struct render_camera *camera)
view_rotate(base_yaw, pitch, view->head_forward, &camera->forward);
view_rotate(base_yaw, pitch, view->head_up, &camera->up);
camera->vertical_field_of_view = 2.0f * (real)atan(view->hud_tangent);
- if ((view->controller_aim || view->gamepad_aim) && halo_vr_aiming() && !vr_render_crosshair_offset[0])
+ /* Unsupported projectile previews retain the ordinary HUD centre. */
+ }
+ return TRUE;
+}
+
+/* The nominal ray is drawn every frame, while unit aiming is 30 Hz.
+Only an unrestricted local player on foot previews the current rendered
+pointing pose. The paired camera is the interpolated observer before camera
+shake and physical head/hand offsets; it retains the shot's origin rules. */
+static boolean vr_render_pointing_pose(long player_index, struct unit_datum *unit,
+ struct halo_vr_view const *view, real base_yaw, struct projectile_preview_pose *pose)
+{
+ struct unit_definition *definition;
+ struct observer_result const *observer;
+ real length;
+ if (player_index != local_player_get_player_index(0) ||
+ unit->object.type != _object_type_biped || unit->object.parent_object_index != NONE ||
+ unit->unit.gunner_object_index != NONE ||
+ director_get_perspective(0) != _director_perspective_first_person || director_inhibited_facing(0))
+ return FALSE;
+ definition = unit_definition_get(unit->definition_index);
+ if (!TEST_FLAG(definition->unit.flags, _unit_fires_from_camera_bit) ||
+ definition->unit.aiming_velocity_maximum != 0.0f || definition->unit.aiming_acceleration_maximum != 0.0f)
+ return FALSE;
+ if (view->controller_aim)
+ {
+ real yaw, pitch, horizontal;
+ yaw_rotate(base_yaw, view->hand_forward, &pose->forward);
+ length = magnitude3d(&pose->forward);
+ if (!isfinite(length) || length <= 0.0f)
+ return FALSE;
+ normalize3d(&pose->forward);
+ yaw = (real)atan2(pose->forward.j, pose->forward.i);
+ pitch = PIN((real)asin(PIN(pose->forward.k, -1.0f, 1.0f)),
+ -DEGREES_TO_RADIANS(85.0f), DEGREES_TO_RADIANS(85.0f));
+ horizontal = (real)cos(pitch);
+ pose->forward.i = horizontal * (real)cos(yaw);
+ pose->forward.j = horizontal * (real)sin(yaw);
+ pose->forward.k = (real)sin(pitch);
+ }
+ else if (view->gamepad_aim)
+ player_control_get_facing_direction(0, &pose->forward);
+ else
+ return FALSE;
+ length = magnitude3d(&pose->forward);
+ if (!isfinite(length) || length <= 0.0f)
+ return FALSE;
+ normalize3d(&pose->forward);
+ unit_clip_to_aiming_bounds(player_get(player_index)->unit_index, &pose->forward, TRUE);
+ observer = render_interpolation_camera(0, observer_get_camera(0));
+ if (!observer)
+ return FALSE;
+ pose->camera_position = observer->position;
+ return isfinite(pose->camera_position.x) && isfinite(pose->camera_position.y) && isfinite(pose->camera_position.z) &&
+ isfinite(pose->forward.i) && isfinite(pose->forward.j) && isfinite(pose->forward.k);
+}
+
+static boolean vr_render_projectile_pose(long player_index, struct unit_datum *unit,
+ struct projectile_preview_pose *pose)
+{
+ return vr_render_pointing_pose(player_index, unit, &vr_render.view, vr_render.base_yaw, pose);
+}
+
+static boolean vr_render_unarmed_ray(long player_index, long unit_index, struct unit_datum *unit,
+ struct halo_vr_view const *view, real base_yaw, real_point3d *origin, real_vector3d *forward)
+{
+ struct projectile_preview_pose pose;
+ if (unit->unit.player_index != player_index ||
+ TEST_FLAG(unit->object.damage_flags, _object_dead_bit) ||
+ unit_get_weapon_count(unit_index) != 0 || cinematic_in_progress() ||
+ unit_inventory_get_weapon(unit_index, unit->unit.current_weapon_index) != NONE ||
+ !vr_render_pointing_pose(player_index, unit, view, base_yaw, &pose))
+ return FALSE;
+ *origin = pose.camera_position;
+ *forward = pose.forward;
+ return TRUE;
+}
+
+/* A weapon's flashlight marker is posed each render frame. Without a
+weapon, use that same rendered controller camera instead of the biped's
+30 Hz attachment. All outputs are presentation copies, shared by both eyes. */
+boolean vr_render_player_flashlight_pose(long unit_index, real_point3d *position,
+ real_vector3d *forward, real_vector3d *up)
+{
+ long player_index = local_player_get_player_index(0);
+ struct unit_datum *unit;
+ real_point3d ignored_origin, source;
+ real_vector3d direction, vertical;
+ real along, length;
+ if (!position || !forward || !up || !vr_render.active ||
+ !vr_render.weapon_camera_set || !vr_render.weapon_camera_valid ||
+ render.local_player_index != 0 || player_index == NONE || unit_index == NONE ||
+ (director_camera_scripted && *director_camera_scripted) ||
+ player_get(player_index)->unit_index != unit_index || !(unit = unit_try_and_get(unit_index)) ||
+ !halo_vr_aiming() || !halo_vr_gaze_tracking_valid() ||
+ !vr_render_unarmed_ray(player_index, unit_index, unit, &vr_render.view, vr_render.base_yaw,
+ &ignored_origin, &direction))
+ return FALSE;
+ source = vr_render.weapon_camera.position;
+ vertical = vr_render.weapon_camera.up;
+ if (!isfinite(source.x) || !isfinite(source.y) || !isfinite(source.z) ||
+ !isfinite(vertical.i) || !isfinite(vertical.j) || !isfinite(vertical.k))
+ return FALSE;
+ /* Preserve controller roll while accommodating the same pitch limits
+ used by the unarmed reticle; never apply body yaw or HMD pitch twice. */
+ along = dot_product3d(&vertical, &direction);
+ vertical.i -= direction.i * along;
+ vertical.j -= direction.j * along;
+ vertical.k -= direction.k * along;
+ length = magnitude3d(&vertical);
+ if (!isfinite(length) || length <= 0.00001f)
+ return FALSE;
+ normalize3d(&vertical);
+ *position = source;
+ *forward = direction;
+ *up = vertical;
+ return TRUE;
+}
+
+/* The tutorial's unarmed calibration panels use the same presentation ray
+as their crosshair. A script query refreshes tracking without changing the
+renderer cache or the simulation's facing/aiming vectors. */
+boolean vr_render_player_pointing_ray(long unit_index, real_point3d *origin, real_vector3d *forward)
+{
+ long player_index = local_player_get_player_index(0);
+ struct unit_datum *unit;
+ struct halo_vr_view view;
+ if (!origin || !forward || player_index == NONE || unit_index == NONE ||
+ player_get(player_index)->unit_index != unit_index || !(unit = unit_try_and_get(unit_index)) ||
+ !halo_vr_aiming() || !halo_vr_view(&view) || !halo_vr_gaze_tracking_valid())
+ return FALSE;
+ return vr_render_unarmed_ray(player_index, unit_index, unit, &view, view.body_yaw, origin, forward);
+}
+
+/* Query the same primary weapon as the HUD, including a vehicle gunner's
+weapon. An unarmed player on foot has the HUD's default crosshair but no
+projectile: place that crosshair along the current bounded pointing pose.
+Unsupported weapon previews retain the ordinary HUD path. */
+static boolean vr_render_projectile_ray(long player_index, real_point3d *origin,
+ real_vector3d *direction, long *owner_object_index)
+{
+ long unit_index = player_get(player_index)->unit_index;
+ struct unit_datum *unit;
+ long weapon_index;
+ struct projectile_preview_pose pose;
+ if (unit_index == NONE || !(unit = unit_try_and_get(unit_index)))
+ return FALSE;
+ weapon_index = unit_inventory_get_weapon(unit_index, unit->unit.current_weapon_index);
+ if (weapon_index == NONE)
+ {
+ long aiming_unit = unit_get_aiming_unit_index(unit_index);
+ if (aiming_unit != NONE && aiming_unit != unit_index)
{
- /* a point far along the controller (or the player's facing, where
- the gamepad aims), where the HUD shows it */
- struct render_frustum frustum;
- real_vector3d forward;
- real_point3d point;
- real_point2d screen;
-
- render_camera_build_frustum(camera, NULL, &frustum, TRUE);
- if (view->gamepad_aim)
- forward = vr_render.aim_forward;
- else
- yaw_rotate(base_yaw, view->hand_forward, &forward);
- point.x = camera->position.x + forward.i * 100.0f;
- point.y = camera->position.y + forward.j * 100.0f;
- point.z = camera->position.z + forward.k * 100.0f;
- if (render_camera_world_to_screen(camera, &frustum, &point, &screen))
- {
- vr_render_crosshair_offset[0] = (short)(screen.x - (camera->viewport_bounds.x0 + camera->viewport_bounds.x1) * 0.5f);
- vr_render_crosshair_offset[1] = (short)(screen.y - (camera->viewport_bounds.y0 + camera->viewport_bounds.y1) * 0.5f);
- }
- else
- {
- /* (off the HUD: not drawn) */
- vr_render_crosshair_offset[0] = 10000;
- }
+ struct unit_datum *parent = unit_try_and_get(aiming_unit);
+ if (parent && parent->unit.gunner_object_index == unit_index)
+ weapon_index = unit_inventory_get_weapon(aiming_unit, parent->unit.current_weapon_index);
}
}
- return TRUE;
+ if (weapon_index == NONE)
+ {
+ if (!vr_render_unarmed_ray(player_index, unit_index, unit, &vr_render.view, vr_render.base_yaw,
+ origin, direction))
+ return FALSE;
+ *owner_object_index = unit_index;
+ return TRUE;
+ }
+ return weapon_preview_projectile_ray(weapon_index, 0, player_index, origin, direction, owner_object_index,
+ vr_render_projectile_pose(player_index, unit, &pose) ? &pose : NULL);
+}
+
+/* One query per frame, before projecting into both eyes. The UI context
+also leaves room for aim assist and its nested line-of-sight collision user. */
+static boolean vr_render_projectile_point(long player_index, real_point3d *point)
+{
+ real_point3d origin;
+ real_vector3d direction, vector;
+ struct collision_result collision;
+ long owner;
+ if (global_current_collision_user_depth >= MAXIMUM_COLLISION_USER_STACK_DEPTH - 2)
+ return FALSE;
+ global_current_collision_users[global_current_collision_user_depth++] = _collision_user_ui;
+ if (!vr_render_projectile_ray(player_index, &origin, &direction, &owner))
+ {
+ --global_current_collision_user_depth;
+ return FALSE;
+ }
+ vector.i = direction.i * CROSSHAIR_FARTHEST;
+ vector.j = direction.j * CROSSHAIR_FARTHEST;
+ vector.k = direction.k * CROSSHAIR_FARTHEST;
+ point->x = origin.x + vector.i;
+ point->y = origin.y + vector.j;
+ point->z = origin.z + vector.k;
+ if (collision_test_vector(_collision_test_for_projectiles_flags, &origin, &vector, owner, &collision))
+ *point = collision.point;
+ --global_current_collision_user_depth;
+ return isfinite(point->x) && isfinite(point->y) && isfinite(point->z);
}
/* render.c, at the end of the eye pass: the crosshairs, drawn in each eye
-where the aim meets the world (vr.aim = "gamepad": the facing from the
-player's eye; "controller": the right controller's line), as the
+at the nominal primary projectile's current collision point, as the
first-person weapon is, and as large as in the HUD's layer. In that layer,
in front of the head, they were placed for the head's pose as the frame was
drawn, which the compositor moved them with to the pose shown while it
@@ -624,65 +873,19 @@ rounding to the HUD's pixels). */
void vr_render_crosshairs(void)
{
struct halo_vr_view *view = &vr_render.view;
- real_point3d origin, point;
- real_vector3d direction, vector;
- struct collision_result collision;
+ real_point3d point;
long player_index = local_player_get_player_index(0);
- long ignore_index = NONE;
- real distance = CROSSHAIR_FARTHEST, width, height, aspect, centre_x, centre_y;
+ real width, height, aspect, centre_x, centre_y;
float corrections[2][4];
short eye, i;
if (!vr_render.active || !halo_vr_aiming() || cinematic_in_progress() || view->hud_tangent <= 0.0f ||
!(view->gamepad_aim || view->controller_aim) || player_index == NONE)
{
- vr_render.crosshair_distance_valid = FALSE;
return;
}
- if (view->gamepad_aim)
- {
- origin = vr_render.base_position;
- direction = vr_render.aim_forward;
- }
- else
- {
- hand_point(view->hand_position, &origin);
- yaw_rotate(vr_render.base_yaw, view->hand_forward, &direction);
- }
- if (normalize3d(&direction) <= 0.0f)
+ if (!vr_render_projectile_point(player_index, &point))
return;
- if (player_get(player_index)->unit_index != NONE)
- ignore_index = object_get_ultimate_parent(player_get(player_index)->unit_index);
- vector.i = direction.i * CROSSHAIR_FARTHEST;
- vector.j = direction.j * CROSSHAIR_FARTHEST;
- vector.k = direction.k * CROSSHAIR_FARTHEST;
- if (global_current_collision_user_depth < MAXIMUM_COLLISION_USER_STACK_DEPTH)
- {
- global_current_collision_users[global_current_collision_user_depth++] = _collision_user_ui;
- if (collision_test_vector(_collision_test_for_line_of_sight_flags | FLAG(_collision_test_objects_bipeds_bit),
- &origin, &vector, ignore_index, &collision))
- {
- distance = (real)sqrt(distance_squared3d(&origin, &collision.point));
- }
- --global_current_collision_user_depth;
- }
- if (distance < CROSSHAIR_NEAREST)
- distance = CROSSHAIR_NEAREST;
- if (!vr_render.crosshair_distance_valid)
- {
- vr_render.crosshair_inverse_distance = 1.0f / distance;
- vr_render.crosshair_distance_valid = TRUE;
- }
- else
- {
- real t = view->seconds > 0.0f ? 1.0f - (real)exp(-view->seconds / CROSSHAIR_SMOOTHING) : 1.0f;
-
- vr_render.crosshair_inverse_distance += (1.0f / distance - vr_render.crosshair_inverse_distance) * t;
- }
- distance = 1.0f / vr_render.crosshair_inverse_distance;
- point.x = origin.x + direction.i * distance;
- point.y = origin.y + direction.j * distance;
- point.z = origin.z + direction.k * distance;
/* the game's screen (the viewport, its pixels: x = ((ndc + 1) width -
1) / 2, y from the top), and its middle, where the game draws them */
diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c
index 304179be60aeebea6a7f1ed3c297fed82700db1f..08dfc34ffdc824b272f420cc6c29c15081093f13 100644
--- a/port/linux/src/d3d8_gl.c
+++ b/port/linux/src/d3d8_gl.c
@@ -479,6 +479,7 @@ static struct
float eye_transforms[2][16];
float screen_corrections[2][4];
BOOL screen_points;
+ BOOL screen_space;
unsigned long eye_serial;
/* the bound framebuffer draws both eyes (bind_targets), or one eye's
layer alone: in the eye pass, a visibility test is drawn into the left
@@ -3074,6 +3075,9 @@ static struct program_entry *prepare_draw(BOOL immediate)
#ifdef HALO_VR
key.multiview = vr_gl.multiview ? XGPU_MULTIVIEW_EYES : vr_gl.left_eye ? XGPU_MULTIVIEW_LEFT_EYE :
vr_gl.right_eye ? XGPU_MULTIVIEW_RIGHT_EYE : XGPU_MULTIVIEW_NONE;
+ /* Copying an eye into a mono scratch target still samples that eye;
+ the scratch geometry itself remains a normal single-view draw. */
+ key.sample_eye = vr_gl.eye_alone == 1 ? 1 : 0;
entry = program_get(vertex_shader_get(program, immediate, key.multiview), fragment_shader_get(&key));
if (vr_gl.multiview && vr_gl.statistics && !program->projects)
{
@@ -3122,7 +3126,7 @@ static struct program_entry *prepare_draw(BOOL immediate)
if (entry->screen_correction >= 0)
glUniform4fv(entry->screen_correction, 2, &vr_gl.screen_corrections[0][0]);
if (entry->screen_points >= 0)
- glUniform1f(entry->screen_points, vr_gl.screen_points ? 1.0f : 0.0f);
+ glUniform1f(entry->screen_points, vr_gl.screen_points || vr_gl.screen_space ? 1.0f : 0.0f);
entry->eye_serial = vr_gl.eye_serial;
}
#endif
@@ -4400,6 +4404,7 @@ void halo_vr_pass(int pass)
else if (pass == HALO_VR_PASS_HUD && vr_gl.pass == HALO_VR_PASS_EYES)
vr_timing(2);
vr_gl.pass = pass;
+ halo_vr_screen_space(0);
vr_gl.multiview = FALSE;
vr_gl.left_eye = FALSE;
vr_gl.right_eye = FALSE;
@@ -4458,6 +4463,16 @@ void halo_vr_draw_eye(int eye)
vr_gl.eye_alone = eye;
}
+void halo_vr_screen_space(int active)
+{
+ BOOL screen_space = active != 0;
+ if (vr_gl.screen_space != screen_space)
+ {
+ vr_gl.screen_space = screen_space;
+ vr_gl.eye_serial++;
+ }
+}
+
void halo_vr_set_screen_corrections(const float corrections[2][4])
{
static const float none[2][4] = { { 1.0f, 1.0f, 0.0f, 0.0f }, { 1.0f, 1.0f, 0.0f, 0.0f } };
diff --git a/port/linux/src/nv2a_psh.c b/port/linux/src/nv2a_psh.c
index c2e5de729dae16b503b1c5eee07834c8add9e94d..6c3a0814611add89f0d6a2abdfb9d27b01e769c2 100644
--- a/port/linux/src/nv2a_psh.c
+++ b/port/linux/src/nv2a_psh.c
@@ -353,7 +353,8 @@ static void sample(struct xgpu_text *text, const struct nv2a_pixel_shader_key *k
xgpu_text_append(text, "texture(tex%d, vec3((%s).xy * texture_scale[%d].xy * vec2(1.0, -1.0) + vec2(0.0, 1.0), %s)"
SAMPLE_BIAS ")", stage,
coordinates, stage, key->multiview == XGPU_MULTIVIEW_EYES ? "float(gl_ViewID_OVR)" :
- key->multiview == XGPU_MULTIVIEW_RIGHT_EYE ? "1.0" : "0.0"
+ key->multiview == XGPU_MULTIVIEW_RIGHT_EYE ||
+ (key->multiview == XGPU_MULTIVIEW_NONE && key->sample_eye) ? "1.0" : "0.0"
#ifdef HALO_GLES
, stage
#endif
diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c
index c23181bd889e417aa70dd1b549765265a254ba5c..f997e57f79e1253b8e36528e32ad92f2d5750d86 100644
--- a/port/linux/src/port_config.c
+++ b/port/linux/src/port_config.c
@@ -159,12 +159,21 @@ static const struct config_setting config_settings[] =
{ "vr.refresh_rate", _config_real, "90.0", "HALO_VR_REFRESH_RATE", _environment_value, _platform_desktop,
"The headset's refresh rate to ask for, in Hz (the game draws a frame for\n"
"each refresh); 0 leaves the runtime's." },
+ { "vr.sun_glow_strength", _config_real, "0.5", "HALO_VR_SUN_GLOW_STRENGTH", _environment_value, _platform_desktop,
+ "The final stereo sun-glow intensity, from 0.0 to 1.0. Does not change\n"
+ "the sun's occlusion mask or the flat-screen effect." },
{ "vr.aim", _config_string, "\"controller\"", "HALO_VR_AIM", _environment_value, _platform_desktop,
"What aims: \"controller\" the right controller, which holds the weapon\n"
"(the reticle shows where it points), \"head\" the head (the reticle in\n"
"the middle of the view, the weapon before the eyes), \"gamepad\" a\n"
"gamepad, as on the flat screen (the stick turns the view, and the head\n"
"looks about from it: vr.gamepad_view)." },
+ { "vr.movement", _config_string, "\"head\"", "HALO_VR_MOVEMENT", _environment_value, _platform_desktop,
+ "On foot with controller aiming, \"head\" walks in the head's horizontal\n"
+ "direction while the controller still aims the weapon. \"aim\" walks\n"
+ "in the weapon's direction, as before. Vehicles and gamepad aiming\n"
+ "keep their controls. Looking nearly straight up or down keeps the last\n"
+ "usable head heading; losing head tracking stops head-relative walking." },
{ "vr.gamepad_view", _config_string, "\"camera\"", "HALO_VR_GAMEPAD_VIEW", _environment_value,
_platform_desktop,
"With vr.aim = \"gamepad\", how the stick moves the view: \"camera\" turns\n"
@@ -208,7 +217,7 @@ static const struct config_setting config_settings[] =
"Degrees of each snap turn." },
{ "vr.smooth_turn_speed", _config_real, "150.0", "HALO_VR_SMOOTH_TURN_SPEED", _environment_value, _platform_desktop,
"Degrees a second of smooth turning, the stick pushed all the way." },
- { "vr.height", _config_string, "\"seated\"", "HALO_VR_HEIGHT", _environment_value, _platform_desktop,
+ { "vr.height", _config_string, "\"standing\"", "HALO_VR_HEIGHT", _environment_value, _platform_desktop,
"\"seated\": the head where it is when recentred (both grips, or a\n"
"gamepad's Back, held) is the player's eye. \"standing\": the head\n"
"vr.player_height above the floor is." },
diff --git a/port/linux/src/vr.c b/port/linux/src/vr.c
index 89cfb6f0d81d1d406b6b9cff2e76a66129202b4a..110884526b023bccde19ff04b657867e1bb482b6 100644
--- a/port/linux/src/vr.c
+++ b/port/linux/src/vr.c
@@ -31,6 +31,7 @@ it or in steps ("level", "snap"). The controllers are a gamepad's halves.
#include "port_config.h"
#include "vr.h"
#include "vr_host.h"
+#include "vr_locomotion.h"
#include <SDL3/SDL.h>
#include <math.h>
@@ -99,6 +100,14 @@ static struct
int aiming;
int aimed;
int aim_synced;
+ /* Head-relative walking has its own heading; it never changes aim. */
+ struct halo_vr_locomotion_heading movement_heading;
+ /* the tutorial's look tests observe head motion independently of the
+ controller's aim, without feeding it into the game's facing */
+ int tutorial_head_valid;
+ float tutorial_head_yaw, tutorial_head_pitch;
+ int tutorial_turn_flags;
+ int turn_control_allowed;
int snap_latched;
float recentre_time;
/* a gamepad's Back held (halo_vr_gamepad_back), and for how long */
@@ -153,6 +162,7 @@ static struct
float menu_width;
int controller_aim;
int gamepad_aim;
+ int head_movement;
/* vr.gamepad_view: the view's heading in steps ("snap"), and pitched
with the facing ("camera") */
int gamepad_snap;
@@ -236,6 +246,7 @@ static void settings_read(void)
settings.menu_width = (float)config_real("vr.menu_width");
settings.gamepad_aim = !strcmp(config_string("vr.aim"), "gamepad");
settings.controller_aim = !settings.gamepad_aim && strcmp(config_string("vr.aim"), "head") != 0;
+ settings.head_movement = strcmp(config_string("vr.movement"), "aim") != 0;
settings.gamepad_snap = !strcmp(config_string("vr.gamepad_view"), "snap");
settings.gamepad_pitch = !settings.gamepad_snap && strcmp(config_string("vr.gamepad_view"), "level") != 0;
settings.melee_gesture = config_boolean("vr.melee_gesture");
@@ -395,6 +406,7 @@ static void recentre(void)
else if (vr.origin_valid)
vr.body_yaw += yaw - vr.origin_yaw;
vr.origin_yaw = yaw;
+ halo_vr_locomotion_reset(&vr.movement_heading);
vr.origin_position[0] = head->position[0];
vr.origin_position[1] = settings.standing ? settings.player_height : head->position[1];
vr.origin_position[2] = head->position[2];
@@ -511,6 +523,43 @@ static void weapon_pointing(float forward[3], float up[3])
}
/* the controllers' turning and recentring, once a frame */
+/* Apply a deliberate stick view turn only while the game permits free-look.
+The tutorial observes the same turn that the headset displays. */
+static void tutorial_turn_input(float x, float seconds)
+{
+ float delta = 0.0f;
+
+ if (!vr.turn_control_allowed || settings.gamepad_aim ||
+ !halo_vr_gaze_tracking_valid() || !isfinite(x) || !isfinite(seconds) || seconds < 0.0f)
+ {
+ vr.tutorial_turn_flags = 0;
+ vr.tutorial_head_valid = 0;
+ /* A held stick must return to neutral after tracking/control recovers. */
+ vr.snap_latched = !isfinite(x) || fabsf(x) >= 0.3f;
+ return;
+ }
+ if (settings.smooth_turn)
+ {
+ if (fabsf(x) > 0.2f && isfinite(settings.smooth_speed))
+ delta = -(x - (x > 0.0f ? 0.2f : -0.2f)) / 0.8f * settings.smooth_speed * seconds;
+ }
+ else if (!vr.snap_latched && fabsf(x) > 0.7f)
+ {
+ if (isfinite(settings.snap_angle) && settings.snap_angle > 0.0f)
+ delta = x > 0.0f ? -settings.snap_angle : settings.snap_angle;
+ vr.snap_latched = 1;
+ }
+ else if (vr.snap_latched && fabsf(x) < 0.3f)
+ {
+ vr.snap_latched = 0;
+ }
+ if (isfinite(delta) && delta != 0.0f)
+ {
+ vr.body_yaw += delta;
+ vr.tutorial_turn_flags |= delta > 0.0f ? HALO_VR_TUTORIAL_TURN_LEFT : HALO_VR_TUTORIAL_TURN_RIGHT;
+ }
+}
+
static void input_update(void)
{
unsigned long now = GetTickCount();
@@ -564,20 +613,7 @@ static void input_update(void)
{
vr.gamepad_recentre_time = 0.0f;
}
- if (settings.smooth_turn)
- {
- if (fabsf(x) > 0.2f)
- vr.body_yaw -= (x - (x > 0.0f ? 0.2f : -0.2f)) / 0.8f * settings.smooth_speed * seconds;
- }
- else if (!vr.snap_latched && fabsf(x) > 0.7f)
- {
- vr.body_yaw -= x > 0.0f ? settings.snap_angle : -settings.snap_angle;
- vr.snap_latched = 1;
- }
- else if (vr.snap_latched && fabsf(x) < 0.3f)
- {
- vr.snap_latched = 0;
- }
+ tutorial_turn_input(x, seconds);
/* (not while the left hand holds a long gun's foregrip) */
if (vr.input.left_grip > 0.8f && vr.input.right_grip > 0.8f && !vr.two_handed)
{
@@ -1044,6 +1080,126 @@ int halo_vr_view(struct halo_vr_view *view)
/* ---------- aiming */
+void halo_vr_locomotion(float aim_yaw, float *forward, float *left)
+{
+ static const float forwards[3] = { 0.0f, 0.0f, -1.0f };
+ float orientation[4], pointing[3], facing[3] = { 0.0f, 0.0f, 0.0f };
+ int tracked;
+ settings_read();
+ /* Flat play, head aiming and a gamepad already use the game's correct
+ throttle basis. The optional old aim-relative mode does too. */
+ if (!vr.initialized || !settings.head_movement || !settings.controller_aim || settings.gamepad_aim)
+ {
+ halo_vr_locomotion_reset(&vr.movement_heading);
+ return;
+ }
+ tracked = (vr.frame || vr.pending) && vr.views.should_render && vr.views.focused &&
+ vr.views.head.valid && vr.origin_valid && vr.aim_synced && !vr.menus &&
+ isfinite(vr.origin_yaw) &&
+ halo_vr_locomotion_orientation(vr.views.head.orientation, orientation);
+ if (tracked)
+ {
+ quaternion_rotate(orientation, forwards, pointing);
+ game_axes(pointing, vr.origin_yaw, facing);
+ }
+ halo_vr_locomotion_project(&vr.movement_heading, tracked, facing, vr.body_yaw, aim_yaw, forward, left);
+}
+
+int halo_vr_gaze_tracking_valid(void)
+{
+ float normalized[4];
+ int eye, axis;
+
+ if (!vr.initialized || !(vr.frame || vr.pending) || !vr.views.should_render ||
+ !vr.views.focused || !vr.views.head.valid || vr.menus || !vr.origin_valid || !vr.aim_synced ||
+ !isfinite(vr.origin_yaw) || !isfinite(vr.body_yaw) ||
+ !halo_vr_locomotion_orientation(vr.views.head.orientation, normalized))
+ return 0;
+ for (eye = 0; eye < 2; eye++)
+ {
+ if (!vr.views.eye[eye].valid ||
+ !halo_vr_locomotion_orientation(vr.views.eye[eye].orientation, normalized))
+ return 0;
+ for (axis = 0; axis < 3; axis++)
+ if (!isfinite(vr.views.eye[eye].position[axis]) || !isfinite(vr.origin_position[axis]))
+ return 0;
+ }
+ return 1;
+}
+
+void halo_vr_turn_control(int allowed)
+{
+ vr.turn_control_allowed = allowed != 0;
+ if (!allowed)
+ {
+ vr.tutorial_turn_flags = 0;
+ vr.tutorial_head_valid = 0;
+ }
+}
+
+int halo_vr_tutorial_turn(void)
+{
+ int flags = vr.tutorial_turn_flags;
+
+ vr.tutorial_turn_flags = 0;
+ return vr.turn_control_allowed && halo_vr_gaze_tracking_valid() ? flags : 0;
+}
+
+int halo_vr_tutorial_look(float *yaw, float *pitch)
+{
+ static const float forwards[3] = { 0.0f, 0.0f, -1.0f };
+ const float threshold = 0.01f; /* about half a degree; ignore tracking jitter */
+ float orientation[4], pointing[3], facing[3], head_yaw, head_pitch;
+
+ *yaw = *pitch = 0.0f;
+ if (!halo_vr_gaze_tracking_valid() || !settings.controller_aim || !vr.turn_control_allowed ||
+ !halo_vr_locomotion_orientation(vr.views.head.orientation, orientation))
+ {
+ vr.tutorial_head_valid = 0;
+ return 0;
+ }
+ /* Use the tracking space, rather than body_yaw or the recentred origin:
+ stick turns and recentring must not count as physical head movement. */
+ quaternion_rotate(orientation, forwards, pointing);
+ game_axes(pointing, 0.0f, facing);
+ head_yaw = atan2f(facing[1], facing[0]);
+ head_pitch = asinf(facing[2] < -1.0f ? -1.0f : facing[2] > 1.0f ? 1.0f : facing[2]);
+ if (!isfinite(head_yaw) || !isfinite(head_pitch))
+ {
+ vr.tutorial_head_valid = 0;
+ return 0;
+ }
+ if (!vr.tutorial_head_valid || !isfinite(vr.tutorial_head_yaw) || !isfinite(vr.tutorial_head_pitch))
+ {
+ vr.tutorial_head_valid = 1;
+ vr.tutorial_head_yaw = head_yaw;
+ vr.tutorial_head_pitch = head_pitch;
+ return 0;
+ }
+ *yaw = remainderf(head_yaw - vr.tutorial_head_yaw, 2.0f * (float)M_PI);
+ *pitch = head_pitch - vr.tutorial_head_pitch;
+ /* Keep each anchor until movement crosses the threshold, so a slow
+ look still counts and sub-threshold jitter does not. */
+ if (fabsf(*yaw) >= threshold)
+ vr.tutorial_head_yaw = head_yaw;
+ else
+ *yaw = 0.0f;
+ if (fabsf(*pitch) >= threshold)
+ vr.tutorial_head_pitch = head_pitch;
+ else
+ *pitch = 0.0f;
+ return *yaw != 0.0f || *pitch != 0.0f;
+}
+
+void halo_vr_tutorial_reset(void)
+{
+ float yaw, pitch;
+
+ vr.tutorial_turn_flags = 0;
+ vr.tutorial_head_valid = 0;
+ (void)halo_vr_tutorial_look(&yaw, &pitch);
+}
+
int halo_vr_aim(float current_yaw, int long_gun, float *yaw, float *pitch)
{
static const float backwards[3] = { 0.0f, 0.0f, -1.0f };
@@ -1105,11 +1261,17 @@ void halo_vr_aim_release(void)
{
vr.aim_synced = 0;
vr.aiming = 0;
+ halo_vr_locomotion_reset(&vr.movement_heading);
}
void halo_vr_menus(int active)
{
vr.menus = active;
+ if (active)
+ {
+ vr.tutorial_turn_flags = 0;
+ vr.tutorial_head_valid = 0;
+ }
}
int halo_vr_aiming(void)
@@ -1161,10 +1323,10 @@ void halo_vr_gamepad(void *gamepad)
analog(pad, XINPUT_GAMEPAD_B, (buttons & VR_BUTTON_B) || vr.melee_frames > 0);
analog(pad, XINPUT_GAMEPAD_X, buttons & VR_BUTTON_X);
analog(pad, XINPUT_GAMEPAD_Y, buttons & VR_BUTTON_Y);
- /* the bumpers where the Xbox's white and black buttons were: the
- flashlight and the grenade switch */
- analog(pad, XINPUT_GAMEPAD_WHITE, buttons & VR_BUTTON_LEFT_BUMPER);
- analog(pad, XINPUT_GAMEPAD_BLACK, buttons & VR_BUTTON_RIGHT_BUMPER);
+ /* Keep the user's Xbox layout: left bumper changes grenade, and the
+ right bumper toggles the flashlight. */
+ analog(pad, XINPUT_GAMEPAD_WHITE, buttons & VR_BUTTON_RIGHT_BUMPER);
+ analog(pad, XINPUT_GAMEPAD_BLACK, buttons & VR_BUTTON_LEFT_BUMPER);
trigger = (BYTE)(vr.input.left_trigger * 255.0f);
if (trigger > pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER])
pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER] = trigger;
diff --git a/port/linux/src/vr.h b/port/linux/src/vr.h
index 9f78308d963134ea03d74c3b856263ae430af933..354d738cd31e3c383641c4b66a835d24e0a9b264 100644
--- a/port/linux/src/vr.h
+++ b/port/linux/src/vr.h
@@ -106,6 +106,27 @@ stays the game's (vr.aim = "gamepad"); 0 when it does not (no VR frame, a
cinematic). With a long gun, the left hand on its foregrip aims it along
both hands (vr.two_handed). */
int halo_vr_aim(float current_yaw, int long_gun, float *yaw, float *pitch);
+/* On-foot local input only, after aim updates: rotate forward/left throttle
+to the head's horizontal heading without changing weapon aim. */
+void halo_vr_locomotion(float aim_yaw, float *forward, float *left);
+/* Physical head movement for the campaign's tutorial look-direction tests;
+does not change controller aiming or the player's facing. */
+int halo_vr_tutorial_look(float *yaw, float *pitch);
+/* Fresh, finite, focused HMD/eye tracking for game-side world-space gaze. */
+int halo_vr_gaze_tracking_valid(void);
+/* Game free-look gates apply to platform turns too; disabled gameplay input
+alone does not disable free-look (the cryopod tutorial). */
+void halo_vr_turn_control(int allowed);
+enum
+{
+ HALO_VR_TUTORIAL_TURN_LEFT = 1,
+ HALO_VR_TUTORIAL_TURN_RIGHT = 2,
+};
+/* Deliberate view turns since the last call, excluding recentering. */
+int halo_vr_tutorial_turn(void);
+/* The script's action reset clears recorded turns and rebases physical head
+motion at the current pose, rather than carrying a prior prompt's anchor. */
+void halo_vr_tutorial_reset(void);
/* the aim no longer follows the head (a cinematic, a vehicle's camera
taking over): the next halo_vr_aim starts again from the player's facing */
void halo_vr_aim_release(void);
@@ -153,6 +174,9 @@ void halo_vr_set_eye_transforms(const float transforms[2][16]);
from the centre view's (identity: 1, 1, 0, 0), for what the game projects
to the screen itself (vr_render.c, vr_render_screen_point) */
void halo_vr_set_screen_corrections(const float corrections[2][4]);
+/* Already projected for an individual eye: screen coordinates bypass the
+world clip correction even when their scale/offset is identity. */
+void halo_vr_screen_space(int active);
/* the draws from now on go into that eye's layer alone (0 left, 1 right),
or both at once (-1, multiview) */
void halo_vr_draw_eye(int eye);
diff --git a/port/linux/src/vr_locomotion.h b/port/linux/src/vr_locomotion.h
new file mode 100644
index 0000000000000000000000000000000000000000..9fb2eeb599ae20ed8e74d4e72a703379e8b7d76c
--- /dev/null
+++ b/port/linux/src/vr_locomotion.h
@@ -0,0 +1,86 @@
+/* Independent head-relative walking. Throttle axes are forward and left;
+the engine applies them relative to the player's (weapon) facing. Keep this
+math separate from OpenXR so the production code can be tested on a host. */
+#ifndef HALO_VR_LOCOMOTION_H
+#define HALO_VR_LOCOMOTION_H
+
+#include <math.h>
+
+struct halo_vr_locomotion_heading
+{
+ int valid;
+ float yaw;
+};
+
+static inline void halo_vr_locomotion_reset(struct halo_vr_locomotion_heading *heading)
+{
+ heading->valid = 0;
+ heading->yaw = 0.0f;
+}
+
+/* A tracking quaternion must be finite and reasonably close to unit length.
+Normalize its small drift before projecting the head's forward direction. */
+static inline int halo_vr_locomotion_orientation(const float pose[4], float normalized[4])
+{
+ float length_squared = 0.0f;
+ float inverse_length;
+ int index;
+ for (index = 0; index < 4; index++)
+ {
+ if (!isfinite(pose[index]))
+ return 0;
+ length_squared += pose[index] * pose[index];
+ }
+ if (!isfinite(length_squared) || length_squared < 0.5f || length_squared > 1.5f)
+ return 0;
+ inverse_length = 1.0f / sqrtf(length_squared);
+ for (index = 0; index < 4; index++)
+ normalized[index] = pose[index] * inverse_length;
+ return 1;
+}
+
+/* Head forward is in the recentred game axes. At nearly vertical pitch its
+horizontal projection has no reliable yaw: keep the last usable heading,
+or wait for one. Tracking loss invalidates that history and stops walking. */
+static inline int halo_vr_locomotion_project(struct halo_vr_locomotion_heading *heading,
+ int tracked, const float head_forward[3], float body_yaw, float aim_yaw,
+ float *forward, float *left)
+{
+ const float tau = 6.2831853071795864769f;
+ float horizontal_squared, length_squared, delta, cosine, sine, original_forward, original_left;
+ if (!tracked || !isfinite(body_yaw) || !isfinite(aim_yaw) ||
+ !isfinite(*forward) || !isfinite(*left) || !isfinite(head_forward[0]) ||
+ !isfinite(head_forward[1]) || !isfinite(head_forward[2]))
+ goto invalid;
+ horizontal_squared = head_forward[0] * head_forward[0] + head_forward[1] * head_forward[1];
+ length_squared = horizontal_squared + head_forward[2] * head_forward[2];
+ if (!isfinite(length_squared) || length_squared < 0.5f || length_squared > 1.5f)
+ goto invalid;
+ if (horizontal_squared >= 0.01f)
+ {
+ heading->yaw = atan2f(head_forward[1], head_forward[0]);
+ heading->valid = 1;
+ }
+ if (!heading->valid || !isfinite(heading->yaw))
+ goto invalid;
+ delta = remainderf(body_yaw + heading->yaw - aim_yaw, tau);
+ if (!isfinite(delta))
+ goto invalid;
+ cosine = cosf(delta);
+ sine = sinf(delta);
+ original_forward = *forward;
+ original_left = *left;
+ *forward = cosine * original_forward - sine * original_left;
+ *left = sine * original_forward + cosine * original_left;
+ if (!isfinite(*forward) || !isfinite(*left))
+ goto invalid;
+ return 1;
+
+invalid:
+ halo_vr_locomotion_reset(heading);
+ *forward = 0.0f;
+ *left = 0.0f;
+ return 0;
+}
+
+#endif
diff --git a/port/linux/src/xgpu.h b/port/linux/src/xgpu.h
index 27a29747ac09406a721d886ef8775912c1f82b71..eef1d507cf784d25895796a48665187f8e539840 100644
--- a/port/linux/src/xgpu.h
+++ b/port/linux/src/xgpu.h
@@ -135,7 +135,10 @@ struct nv2a_pixel_shader_key
/* drawn into both eyes at once (the VR mode, d3d8_gl.c): layered
render targets are sampled at the view's layer (XGPU_MULTIVIEW_*) */
unsigned char multiview;
- unsigned char reserved[3];
+ /* A mono scratch pass copying a layered source still selects an eye;
+ this is independent of the destination's multiview geometry. */
+ unsigned char sample_eye;
+ unsigned char reserved[2];
};
char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key);
diff --git a/source/game/aim_assist.c b/source/game/aim_assist.c
index dd5c80fab626818dc7dede27c44f3b07e7884358..7b94680c99e554921376d467ada09807f8eb5031 100644
--- a/source/game/aim_assist.c
+++ b/source/game/aim_assist.c
@@ -539,15 +539,20 @@ boolean aim_assist(
return FALSE;
}
-long player_aim_projectile(
+static long player_aim_projectile_internal(
long player_index,
real_point3d const *position,
- real_vector3d *direction)
+ real_vector3d *direction,
+ boolean record_target,
+ struct projectile_preview_pose const *pose)
{
long target_object_index= NONE;
struct player_datum *player= player_get(player_index);
long aiming_unit_index= unit_get_aiming_unit_index(player->unit_index);
struct aim_assist_parameters parameters;
+#ifndef HALO_VR
+ (void)pose;
+#endif
match_assert_valid_real_normal3d(
"c:\\halo\\SOURCE\\game\\aim_assist.c",
@@ -567,8 +572,19 @@ long player_aim_projectile(
real_vector3d target_direction;
real_vector3d collision_direction;
real autoaim_level;
+#ifdef HALO_VR
+ real_point3d simulation_camera_position;
+#endif
director_camera_deterministic(player->unit_index, &camera_position, &camera_direction);
+#ifdef HALO_VR
+ simulation_camera_position = camera_position;
+ if (pose)
+ {
+ camera_position = pose->camera_position;
+ camera_direction = pose->forward;
+ }
+#endif
target_direction= *direction;
autoaim_level= 0.f;
@@ -598,6 +614,12 @@ long player_aim_projectile(
real camera_to_unit_distance;
vector_from_points3d(&unit->object.position, &camera_position, &camera_to_unit);
+#ifdef HALO_VR
+ /* Keep the deterministic camera-height displacement. An
+ interpolated eye against a raw unit origin would add motion lag. */
+ if (pose)
+ vector_from_points3d(&unit->object.position, &simulation_camera_position, &camera_to_unit);
+#endif
camera_to_unit_distance= magnitude3d(&camera_to_unit);
camera_displacement= camera_direction;
normalize3d(&camera_displacement);
@@ -631,12 +653,36 @@ long player_aim_projectile(
global_current_collision_user_depth > 1);
--global_current_collision_user_depth;
- player->aim_assist_unit_index= target_object_index;
- player->aim_assist_timestamp= game_time_get();
+ if (record_target)
+ {
+ player->aim_assist_unit_index= target_object_index;
+ player->aim_assist_timestamp= game_time_get();
+ }
return target_object_index;
}
+long player_aim_projectile(
+ long player_index,
+ real_point3d const *position,
+ real_vector3d *direction)
+{
+ return player_aim_projectile_internal(player_index, position, direction, TRUE, NULL);
+}
+
+/* A reticle uses the same nominal direction calculation as a shot, without
+recording a target or advancing the player's targeting timestamp. */
+#ifdef HALO_VR
+long player_preview_projectile_aim(
+ long player_index,
+ real_point3d const *position,
+ real_vector3d *direction,
+ struct projectile_preview_pose const *pose)
+{
+ return player_aim_projectile_internal(player_index, position, direction, FALSE, pose);
+}
+#endif
+
long local_player_aim_assist(
short local_player_index,
real *autoaim_level,
diff --git a/source/game/aim_assist.h b/source/game/aim_assist.h
index f5570893c4325fc94c6fd32e96e1a62d9443a318..6a6a22cbaeb975a928b18d989ac13935fd42f0c5 100644
--- a/source/game/aim_assist.h
+++ b/source/game/aim_assist.h
@@ -19,6 +19,7 @@ header included in hcex build.
/* ---------- structures */
struct aim_assist_target;
+struct projectile_preview_pose;
struct aim_assist_parameters
{
@@ -63,6 +64,15 @@ long player_aim_projectile(
long player_index,
real_point3d const *position,
real_vector3d *direction);
+/* Same direction calculation as player_aim_projectile, without changing
+the player's recorded aim-assist target or timestamp. */
+#ifdef HALO_VR
+long player_preview_projectile_aim(
+ long player_index,
+ real_point3d const *position,
+ real_vector3d *direction,
+ struct projectile_preview_pose const *pose);
+#endif
long local_player_aim_assist(
short local_player_index,
real *autoaim_level,
diff --git a/source/game/player_control.c b/source/game/player_control.c
index f954ef16dd217cefa37f2988d4269f8c4cf6db3d..592216bab610e89cdda157ac89f184a79c774280 100644
--- a/source/game/player_control.c
+++ b/source/game/player_control.c
@@ -619,6 +619,17 @@ static void handle_one_player_input(
&input,
0xFA,
sizeof(input));
+#ifdef HALO_VR
+ if (local_player_index == 0)
+ {
+ extern void halo_vr_turn_control(int allowed);
+ /* player_enable_input(false) still allows the tutorial's free-look. */
+ halo_vr_turn_control(player->unit_index != NONE &&
+ !TEST_FLAG(unit_get(player->unit_index)->object.damage_flags, _object_dead_bit) &&
+ player_control_camera_control_is_active() && !director_inhibited_facing(local_player_index) &&
+ !cinematic_in_progress());
+ }
+#endif
get_local_player_input_blob(
local_player_index,
time_delta_sec,
@@ -747,6 +758,7 @@ static void handle_one_player_input(
and the netcode sends an ordinary player's facing */
extern int halo_vr_aim(float current_yaw, int long_gun, float *yaw, float *pitch);
extern void halo_vr_aim_release(void);
+ extern void halo_vr_locomotion(float aim_yaw, float *forward, float *left);
extern boolean vr_render_long_gun(long weapon_index);
float vr_yaw, vr_pitch;
int vr_aim = 0;
@@ -761,6 +773,10 @@ static void handle_one_player_input(
unit->object.parent_object_index == NONE && vr_render_long_gun(current_weapon_index),
&vr_yaw, &vr_pitch);
}
+ /* Reject a corrupt tracking result before wrapping yaw: infinity
+ would never leave the loop, and NaN must not reach the action queue. */
+ if (vr_aim == 1 && (!isfinite(vr_yaw) || !isfinite(vr_pitch)))
+ vr_aim = 0;
if (vr_aim == 1)
{
while (vr_yaw < 0.f)
@@ -774,6 +790,13 @@ static void handle_one_player_input(
{
halo_vr_aim_release();
}
+ /* Walking follows the head independently of controller/long-gun
+ aim. Vehicle steering, cinematics and other players stay unchanged. */
+ if (local_player_index == 0 && unit->object.parent_object_index == NONE &&
+ !director_inhibited_facing(local_player_index) && !cinematic_in_progress())
+ {
+ halo_vr_locomotion(player->desired_angles.yaw, &input.throttle.i, &input.throttle.j);
+ }
}
#endif
@@ -1599,6 +1622,12 @@ void player_control_action_test_reset(
globals->action_flags = 0;
globals->action_test_flags = 0;
+#ifdef HALO_VR
+ {
+ extern void halo_vr_tutorial_reset(void);
+ halo_vr_tutorial_reset();
+ }
+#endif
return;
}
@@ -1762,6 +1791,31 @@ void player_control_action_test_note(
{
globals->action_flags |= FLAG(_player_control_look_relative_right_bit);
}
+ /* In VR the controller aims the weapon; physical head motion answers
+ the tutorial's look tests independently, without moving that aim. */
+#ifdef HALO_VR
+ {
+ extern int halo_vr_tutorial_look(float *yaw, float *pitch);
+ extern int halo_vr_tutorial_turn(void);
+ float vr_yaw, vr_pitch;
+ int vr_turn = halo_vr_tutorial_turn();
+ if (vr_turn & 1)
+ globals->action_flags |= FLAG(_player_control_look_relative_left_bit);
+ if (vr_turn & 2)
+ globals->action_flags |= FLAG(_player_control_look_relative_right_bit);
+ if (halo_vr_tutorial_look(&vr_yaw, &vr_pitch))
+ {
+ if (vr_pitch > 0.f)
+ globals->action_flags |= FLAG(_player_control_look_relative_up_bit);
+ else if (vr_pitch < 0.f)
+ globals->action_flags |= FLAG(_player_control_look_relative_down_bit);
+ if (vr_yaw > 0.f)
+ globals->action_flags |= FLAG(_player_control_look_relative_left_bit);
+ else if (vr_yaw < 0.f)
+ globals->action_flags |= FLAG(_player_control_look_relative_right_bit);
+ }
+ }
+#endif
if (throttle->i > 0.f)
{
globals->action_flags |= FLAG(_player_control_move_relative_forward_bit);
@@ -1784,6 +1838,10 @@ static void player_control_action_test_check_reset_input_blob(
struct input_blob *input)
{
struct player_control_globals_data *globals;
+#ifdef HALO_VR
+ extern int halo_vr_running(void);
+ boolean vr_controls = halo_vr_running() != 0;
+#endif
if (input->accept && (cinematic_can_be_skipped() || network_coop_skip_offered()))
{
@@ -1807,6 +1865,22 @@ static void player_control_action_test_check_reset_input_blob(
{
globals->action_flags |= FLAG(_player_control_zoom_bit);
}
+#ifdef HALO_VR
+ /* The tutorial tests physical B, while the VR Xbox layout maps B to
+ melee. Consume its mapped actions until that button is released; using
+ the old weapon-switch flag here releases suppression too early. */
+ if (vr_controls && (TEST_FLAG(globals->action_test_flags, _player_control_back_bit) ||
+ TEST_FLAG(globals->suppressed_action_flags, _player_control_back_bit)))
+ {
+ SET_FLAG(globals->suppressed_action_flags, _player_control_back_bit, input->back);
+ SET_FLAG(input->unit_control_flags, _unit_control_use_equipment_bit, FALSE);
+ SET_FLAG(input->unit_control_flags, _unit_control_throw_grenade_bit, FALSE);
+ SET_FLAG(input->unit_control_flags, _unit_control_weapon_secondary_trigger_bit, FALSE);
+ SET_FLAG(input->player_control_flags, _player_control_input_zoom_bit, FALSE);
+ SET_FLAG(input->player_control_flags, _player_control_rotate_weapons_bit, FALSE);
+ SET_FLAG(input->player_control_flags, _player_control_rotate_grenades_bit, FALSE);
+ }
+#endif
if (!TEST_FLAG(globals->action_test_flags, _player_control_action_bit))
{
@@ -1838,6 +1912,10 @@ check_accept:
SET_FLAG(input->unit_control_flags, _unit_control_action_bit, FALSE);
check_back_unswapped:
+#ifdef HALO_VR
+ if (vr_controls)
+ return;
+#endif
if (!TEST_FLAG(globals->action_test_flags, _player_control_back_bit))
{
if (!TEST_FLAG(globals->suppressed_action_flags, _player_control_back_bit))
@@ -1867,6 +1945,10 @@ check_accept:
SET_FLAG(input->unit_control_flags, _unit_control_jump_bit, FALSE);
check_back_swapped:
+#ifdef HALO_VR
+ if (vr_controls)
+ return;
+#endif
if (!TEST_FLAG(globals->action_test_flags, _player_control_back_bit))
{
if (!TEST_FLAG(globals->suppressed_action_flags, _player_control_accept_bit))
diff --git a/source/hs/hs_library_external.c b/source/hs/hs_library_external.c
index 0402e0234b124837d2c1f7434b5e5aa8ef0cda89..a75c12fbc851ba5706dff9f002ea79ee44266679 100644
--- a/source/hs/hs_library_external.c
+++ b/source/hs/hs_library_external.c
@@ -237,6 +237,37 @@ boolean hs_trigger_volume_test_objects(
return result;
}
+#ifdef HALO_VR
+/* The five original a10 calibration targets use a strict five-degree gate.
+Only these named scenery objects may also be pointed at with the unarmed
+hand reticle; technician/AI/other-map gaze keeps its original predicate. */
+static boolean hs_vr_calibration_panel(long object_index, real degrees)
+{
+ static char const *const names[] = {
+ "looking_panel_center", "looking_panel_left", "looking_panel_right",
+ "looking_panel_up", "looking_panel_down",
+ };
+ struct object_datum *object;
+ struct scenario *scenario;
+ struct scenario_object_name *name;
+ short index;
+
+ if (degrees != 5.f || global_scenario_index == NONE ||
+ strcmp(tag_get_name(global_scenario_index), "levels\\a10\\a10"))
+ return FALSE;
+ object = object_get(object_index);
+ scenario = global_scenario_get();
+ if (object->object.type != _object_type_scenery ||
+ !VALID_INDEX(object->object.name_index, scenario->object_names.count))
+ return FALSE;
+ name = TAG_BLOCK_GET_ELEMENT(&scenario->object_names, object->object.name_index, struct scenario_object_name);
+ for (index = 0; index < NUMBEROF(names); index++)
+ if (!strncmp(name->name, names[index], sizeof(name->name)))
+ return TRUE;
+ return FALSE;
+}
+#endif
+
boolean hs_unit_can_see_object(
long unit_index,
long object_index,
@@ -258,6 +289,20 @@ boolean hs_unit_can_see_object(
unit_index,
&target_point,
DEGREES_TO_RADIANS(degrees));
+#ifdef HALO_VR
+ if (!result && hs_vr_calibration_panel(object_index, degrees))
+ {
+ extern boolean vr_render_player_pointing_ray(long unit_index, real_point3d *origin, real_vector3d *forward);
+ real_point3d origin;
+ real_vector3d forward, direction;
+ if (vr_render_player_pointing_ray(unit_index, &origin, &forward))
+ {
+ vector_from_points3d(&origin, &target_point, &direction);
+ if (normalize3d(&direction) > 0.f)
+ result = dot_product3d(&direction, &forward) > cosine(DEGREES_TO_RADIANS(degrees));
+ }
+ }
+#endif
}
return result;
diff --git a/source/items/weapons.c b/source/items/weapons.c
index b2e1403d795586b3fc1ab5464a6fae3472ebf805..7785206b6c960acaadb475f15bd6fd87b30cd116 100644
--- a/source/items/weapons.c
+++ b/source/items/weapons.c
@@ -220,6 +220,7 @@ symbols in this file:
#include "game/game.h"
#include "game/game_engine.h"
#include "game/players.h"
+#include "game/aim_assist.h"
#include "interface/first_person_weapons.h"
#include "math/periodic_functions.h"
#include "models/model_animation_definitions.h"
@@ -2165,6 +2166,206 @@ static void projectile_distribute(
return;
}
+/* Shared pre-spread ray: both real shots and VR previews use these exact
+origin/aim/trigger-offset calculations. A preview may supply a render pose
+and never records an aim-assist target. */
+static long trigger_adjust_projectile_ray(
+ long owner_object_index,
+ struct unit_datum *unit,
+ struct weapon_trigger_definition const *trigger_definition,
+ real_point3d *origin,
+ real_vector3d *forward,
+ real *velocity,
+ real *error,
+ boolean preview,
+ struct projectile_preview_pose const *pose)
+{
+ long target_object_index = NONE;
+#ifndef HALO_VR
+ (void)preview;
+ (void)pose;
+#endif
+ if (!TEST_FLAG(trigger_definition->flags, _weapon_trigger_projectiles_cannot_be_aimed_bit) &&
+ unit &&
+ !TEST_FLAG(unit->object.damage_flags, _object_dead_bit))
+ {
+ struct unit_definition *unit_definition= unit_definition_get(unit->definition_index);
+ long player_index= unit->unit.player_index;
+ long actor_index= unit->unit.actor_index;
+ boolean adjust_origin;
+ boolean use_aiming_vector;
+
+ if (unit->unit.gunner_object_index!=NONE)
+ {
+ struct unit_datum *gunner= unit_get(unit->unit.gunner_object_index);
+
+ player_index= gunner->unit.player_index;
+ actor_index= gunner->unit.actor_index;
+ }
+
+ adjust_origin= TEST_FLAG(unit_definition->unit.flags, _unit_fires_from_camera_bit);
+ use_aiming_vector= TRUE;
+ if (actor_index!=NONE && actor_firing_blindly(actor_index))
+ {
+ use_aiming_vector= FALSE;
+ }
+ if (unit->unit.gunner_object_index!=NONE)
+ {
+ use_aiming_vector= FALSE;
+ }
+
+#ifdef HALO_VR
+ if (preview && !use_aiming_vector)
+ pose = NULL;
+ if (preview && pose)
+ unit_preview_projectile_ray(owner_object_index, origin, forward, velocity,
+ adjust_origin, use_aiming_vector, &pose->camera_position, &pose->forward);
+ else
+#endif
+ unit_adjust_projectile_ray(owner_object_index, origin, forward, velocity, adjust_origin, use_aiming_vector);
+
+ if (player_index!=NONE)
+ {
+ real_vector3d right;
+ real_vector3d up;
+
+ cross_product3d(global_up3d, forward, &right);
+ if (normalize3d(&right)==0.0f)
+ {
+ right= *global_left3d;
+ }
+ cross_product3d(forward, &right, &up);
+ normalize3d(&up);
+
+ point_from_line3d(
+ origin,
+ forward,
+ trigger_definition->first_person_weapon_offset.x,
+ origin);
+ point_from_line3d(
+ origin,
+ &right,
+ trigger_definition->first_person_weapon_offset.y,
+ origin);
+ point_from_line3d(
+ origin,
+ &up,
+ trigger_definition->first_person_weapon_offset.z,
+ origin);
+
+#ifdef HALO_VR
+ if (preview)
+ {
+ real length = magnitude3d(forward);
+ if (!isfinite(origin->x) || !isfinite(origin->y) || !isfinite(origin->z) ||
+ !isfinite(forward->i) || !isfinite(forward->j) || !isfinite(forward->k) ||
+ !isfinite(length) || length <= 0.0f)
+ {
+ /* Keep invalid preview data out of the shared aim assertions.
+ A zero output also makes the wrapper suppress this preview. */
+ forward->i = forward->j = forward->k = 0.0f;
+ return NONE;
+ }
+ target_object_index = player_preview_projectile_aim(player_index, origin, forward, pose);
+ }
+ else
+#endif
+ target_object_index = player_aim_projectile(player_index, origin, forward);
+ }
+ else if (actor_index!=NONE)
+ {
+ target_object_index= actor_aim_projectile(actor_index, origin, forward, error);
+ }
+ }
+ return target_object_index;
+}
+
+#ifdef HALO_VR
+boolean weapon_preview_projectile_ray(
+ long weapon_index,
+ short trigger_index,
+ long player_index,
+ real_point3d *origin,
+ real_vector3d *forward,
+ long *owner_object_index,
+ struct projectile_preview_pose const *pose)
+{
+ struct weapon_datum *weapon;
+ struct weapon_definition *definition;
+ struct weapon_trigger_definition *trigger;
+ struct unit_datum *unit;
+ struct object_marker marker;
+ struct projectile_preview_pose validated_pose;
+ long owner, firing_player;
+ real velocity = 0.0f, error = 0.0f, length;
+
+ if (weapon_index == NONE || player_index == NONE)
+ return FALSE;
+ weapon = weapon_try_and_get(weapon_index);
+ if (!weapon)
+ return FALSE;
+ definition = weapon_definition_get(weapon->definition_index);
+ if (trigger_index < 0 || trigger_index >= MAXIMUM_NUMBER_OF_TRIGGERS_PER_WEAPON ||
+ trigger_index >= definition->weapon.triggers.count)
+ return FALSE;
+ owner = weapon_get_owner_object_index(weapon_index);
+ unit = unit_try_and_get(owner);
+ if (!unit || TEST_FLAG(unit->object.damage_flags, _object_dead_bit))
+ return FALSE;
+ firing_player = unit->unit.player_index;
+ if (unit->unit.gunner_object_index != NONE)
+ {
+ struct unit_datum *gunner = unit_try_and_get(unit->unit.gunner_object_index);
+ if (!gunner)
+ return FALSE;
+ firing_player = gunner->unit.player_index;
+ }
+ if (firing_player != player_index)
+ return FALSE;
+ trigger = TAG_BLOCK_GET_ELEMENT(&definition->weapon.triggers, trigger_index, struct weapon_trigger_definition);
+ if (pose)
+ {
+ struct unit_definition *unit_definition = unit_definition_get(unit->definition_index);
+ /* Limited turrets, vehicle seats and unaimed projectiles keep the
+ authoritative path; a render pose cannot bypass their firing rules. */
+ if (unit->object.type != _object_type_biped || unit->object.parent_object_index != NONE ||
+ unit->unit.gunner_object_index != NONE ||
+ !TEST_FLAG(unit_definition->unit.flags, _unit_fires_from_camera_bit) ||
+ unit_definition->unit.aiming_velocity_maximum != 0.0f ||
+ unit_definition->unit.aiming_acceleration_maximum != 0.0f ||
+ TEST_FLAG(trigger->flags, _weapon_trigger_projectiles_cannot_be_aimed_bit))
+ pose = NULL;
+ }
+ if (pose)
+ {
+ length = magnitude3d(&pose->forward);
+ if (!isfinite(pose->camera_position.x) || !isfinite(pose->camera_position.y) ||
+ !isfinite(pose->camera_position.z) || !isfinite(length) || length <= 0.0f)
+ return FALSE;
+ validated_pose = *pose;
+ normalize3d(&validated_pose.forward);
+ pose = &validated_pose;
+ }
+
+ /* object_get_marker_by_name supplies node zero when the named marker
+ is missing, the same fallback used by trigger_create_projectiles. */
+ object_get_marker_by_name(weapon_get_effect_object_index(weapon_index),
+ trigger_index == 0 ? "primary trigger" : "secondary trigger", &marker, 1);
+ *origin = marker.matrix.position;
+ *forward = marker.matrix.forward;
+ trigger_adjust_projectile_ray(owner, unit, trigger, origin, forward, &velocity, &error, TRUE, pose);
+ if (TEST_FLAG(trigger->flags, _weapon_trigger_uses_weapon_origin_bit))
+ *origin = marker.matrix.position;
+ length = magnitude3d(forward);
+ if (!isfinite(origin->x) || !isfinite(origin->y) || !isfinite(origin->z) ||
+ !isfinite(forward->i) || !isfinite(forward->j) || !isfinite(forward->k) ||
+ !isfinite(length) || length <= 0.0f || normalize3d(forward) <= 0.0f)
+ return FALSE;
+ *owner_object_index = object_get_ultimate_parent(weapon_get_projectile_owner_object_index(weapon_index));
+ return TRUE;
+}
+#endif
+
static void trigger_create_projectiles(
long weapon_index,
short trigger_index)
@@ -2203,73 +2404,8 @@ static void trigger_create_projectiles(
long projectile_definition_index;
short projectile_count;
- if (!TEST_FLAG(trigger_definition->flags, _weapon_trigger_projectiles_cannot_be_aimed_bit) &&
- unit &&
- !TEST_FLAG(unit->object.damage_flags, _object_dead_bit))
- {
- struct unit_definition *unit_definition= unit_definition_get(unit->definition_index);
- long player_index= unit->unit.player_index;
- long actor_index= unit->unit.actor_index;
- boolean adjust_origin;
- boolean use_aiming_vector;
-
- if (unit->unit.gunner_object_index!=NONE)
- {
- struct unit_datum *gunner= unit_get(unit->unit.gunner_object_index);
-
- player_index= gunner->unit.player_index;
- actor_index= gunner->unit.actor_index;
- }
-
- adjust_origin= TEST_FLAG(unit_definition->unit.flags, _unit_fires_from_camera_bit);
- use_aiming_vector= TRUE;
- if (actor_index!=NONE && actor_firing_blindly(actor_index))
- {
- use_aiming_vector= FALSE;
- }
- if (unit->unit.gunner_object_index!=NONE)
- {
- use_aiming_vector= FALSE;
- }
-
- unit_adjust_projectile_ray(owner_object_index, &origin, &forward, &velocity, adjust_origin, use_aiming_vector);
-
- if (player_index!=NONE)
- {
- real_vector3d right;
- real_vector3d up;
-
- cross_product3d(global_up3d, &forward, &right);
- if (normalize3d(&right)==0.0f)
- {
- right= *global_left3d;
- }
- cross_product3d(&forward, &right, &up);
- normalize3d(&up);
-
- point_from_line3d(
- &origin,
- &forward,
- trigger_definition->first_person_weapon_offset.x,
- &origin);
- point_from_line3d(
- &origin,
- &right,
- trigger_definition->first_person_weapon_offset.y,
- &origin);
- point_from_line3d(
- &origin,
- &up,
- trigger_definition->first_person_weapon_offset.z,
- &origin);
-
- target_object_index= player_aim_projectile(player_index, &origin, &forward);
- }
- else if (actor_index!=NONE)
- {
- target_object_index= actor_aim_projectile(actor_index, &origin, &forward, &error);
- }
- }
+ target_object_index = trigger_adjust_projectile_ray(owner_object_index, unit, trigger_definition,
+ &origin, &forward, &velocity, &error, FALSE, NULL);
if (TEST_FLAG(trigger_definition->flags, _weapon_trigger_uses_weapon_origin_bit))
{
diff --git a/source/items/weapons.h b/source/items/weapons.h
index a9c5c8308e82e1ff2ada96554b92a582fc639606..f451943f30ad6485f8a131faef5afd6b1dea3237 100644
--- a/source/items/weapons.h
+++ b/source/items/weapons.h
@@ -309,6 +309,27 @@ real weapon_estimate_time_to_target(
real distance);
/* ---------- globals */
+/* Nominal ray for the first primary projectile, before random spread and
+ballistics. Read-only: returns FALSE for an unsupported player/weapon state.
+The returned owner is the ultimate parent the shot's collision trace ignores. */
+struct projectile_preview_pose;
+#ifdef HALO_VR
+/* Render-only inputs: unshaken interpolated gameplay camera, and the local
+player's bounded current pointing direction. Never a physical hand origin. */
+struct projectile_preview_pose
+{
+ real_point3d camera_position;
+ real_vector3d forward;
+};
+boolean weapon_preview_projectile_ray(
+ long weapon_index,
+ short trigger_index,
+ long player_index,
+ real_point3d *origin,
+ real_vector3d *forward,
+ long *owner_object_index,
+ struct projectile_preview_pose const *pose);
+#endif
/* ---------- public code */
diff --git a/source/objects/object_lights.c b/source/objects/object_lights.c
index 919df847818bd36658fdf9448404ea67d8eead2a..3b1e447d48831e7d6fe26db60d317d53e3356ec4 100644
--- a/source/objects/object_lights.c
+++ b/source/objects/object_lights.c
@@ -157,6 +157,9 @@ symbols in this file:
#include "cache/texture_cache.h"
#include "game/game.h"
#include "game/game_engine.h"
+#ifdef HALO_VR
+#include "game/players.h"
+#endif
#include "interface/first_person_weapons.h"
#include "math/periodic_functions.h"
#include "memory/data.h"
@@ -450,12 +453,19 @@ static void light_compute_bounding_sphere(
boolean lens_flare_only,
real_point3d *position,
real *radius);
+static void light_compute_render_bounding_sphere(
+ long light_index, boolean maximum, boolean specular, boolean lens_flare_only,
+ real_point3d *position, real *radius);
static void render_debug_light(
long light_index);
static void light_get_bounding_sphere(
long light_index,
real_point3d *position,
real *radius);
+#ifdef HALO_VR
+extern boolean vr_render_player_flashlight_pose(long unit_index, real_point3d *position,
+ real_vector3d *forward, real_vector3d *up);
+#endif
/* ---------- globals */
@@ -815,6 +825,61 @@ long light_new_unattached(
return light_index;
}
+/* Only a living local unarmed VR player's actual flashlight has a
+render-time pose. Ordinary lights, weapon markers and simulation datums
+retain their original paths. */
+static boolean light_vr_unarmed_flashlight_pose(struct light_datum const *light,
+ real_point3d *position, real_vector3d *forward, real_vector3d *up)
+{
+#ifdef HALO_VR
+ if (light->parent_light_index == NONE && light->object_index != NONE &&
+ TEST_FLAG(light->flags, _point_light_dynamic_bit) &&
+ TEST_FLAG(light_definition_get(light->definition_index)->flags,
+ _light_definition_is_first_person_flashlight_bit))
+ return vr_render_player_flashlight_pose(light->object_index, position, forward, up);
+#else
+ (void)light; (void)position; (void)forward; (void)up;
+#endif
+ return FALSE;
+}
+
+/* Simulation cluster membership can lag behind a controller that points
+elsewhere between ticks. Include this one local render light without
+changing the partition. Reserve no extra capacity or unbounded writes. */
+static void lights_vr_include_unarmed_flashlights(void)
+{
+#ifdef HALO_VR
+ long light_index, player_index;
+ real_point3d source;
+ real_vector3d direction, vertical;
+ if (render.local_player_index != 0 || (player_index = local_player_get_player_index(0)) == NONE ||
+ !vr_render_player_flashlight_pose(player_get(player_index)->unit_index, &source, &direction, &vertical))
+ return;
+ for (light_index = data_next_index(light_data, NONE); light_index != NONE;
+ light_index = data_next_index(light_data, light_index))
+ {
+ real_point3d position;
+ real_vector3d forward, up;
+ if (light_unmarked(light_index) && light_vr_unarmed_flashlight_pose(light_get(light_index),
+ &position, &forward, &up))
+ {
+ struct light_datum const *light = light_get(light_index);
+ real intensity;
+ short slot = lights_globals.scene_point_light_count;
+ object_get_function_value(light->object_index, light->function_index, &intensity);
+ if (intensity <= 0.0f)
+ continue;
+ if (slot < MAXIMUM_RENDERED_LIGHTS)
+ lights_globals.scene_point_light_count++;
+ else
+ slot = MAXIMUM_RENDERED_LIGHTS - 1;
+ lights_globals.scene_point_lights[slot] = light_index;
+ light_mark(light_index);
+ }
+ }
+#endif
+}
+
void lights_preprocess_scene(
void)
{
@@ -869,6 +934,7 @@ void lights_preprocess_scene(
light_get_bounding_sphere,
light_unmarked,
light_mark);
+ lights_vr_include_unarmed_flashlights();
light_marker_end();
rasterizer_lights_begin();
for (rendered_cluster_index = 0;
@@ -1053,6 +1119,8 @@ void lights_preprocess_scene(
TRUE);
}
}
+ light_vr_unarmed_flashlight_pose(light, &light_parameters.position,
+ &light_parameters.forward, &light_parameters.up);
light->rasterizer_light_index = rasterizer_light_submit(
&light_parameters);
@@ -1550,15 +1618,14 @@ static long cluster_get_next_light(
reference_index);
}
-static void light_compute_bounding_sphere(
- long light_index,
+static void light_compute_bounding_sphere_from_basis(
+ struct light_datum const *light,
boolean maximum,
boolean specular,
boolean lens_flare_only,
real_point3d *position,
real *radius)
{
- struct light_datum *light = light_get(light_index);
struct point_light_definition *definition = light_definition_get(light->definition_index);
real light_radius = maximum
? definition->radius_modifier_upper_bound * definition->radius
@@ -1602,6 +1669,34 @@ static void light_compute_bounding_sphere(
return;
}
+/* Tick attachment/reconnection continues using the simulation basis. */
+static void light_compute_bounding_sphere(long light_index, boolean maximum,
+ boolean specular, boolean lens_flare_only, real_point3d *position, real *radius)
+{
+ light_compute_bounding_sphere_from_basis(light_get(light_index), maximum, specular,
+ lens_flare_only, position, radius);
+}
+
+/* Visibility and diffuse/specular use the same frame pose as submission.
+The effective light is a stack copy; neither its datum nor clusters move. */
+static void light_compute_render_bounding_sphere(long light_index, boolean maximum,
+ boolean specular, boolean lens_flare_only, real_point3d *position, real *radius)
+{
+ struct light_datum effective = *light_get(light_index);
+ light_vr_unarmed_flashlight_pose(&effective, &effective.position, &effective.forward, &effective.up);
+ light_compute_bounding_sphere_from_basis(&effective, maximum, specular, lens_flare_only, position, radius);
+}
+
+static boolean light_render_skips_clusters(struct light_datum const *light)
+{
+ real_point3d position;
+ real_vector3d forward, up;
+ return (TEST_FLAG(light->flags, _point_light_attached_to_first_person_weapon_bit) &&
+ TEST_FLAG(light_definition_get(light->definition_index)->flags,
+ _light_definition_supersize_in_first_person_bit)) ||
+ light_vr_unarmed_flashlight_pose(light, &position, &forward, &up);
+}
+
static short light_build_cluster_array(
long light_index,
short maximum_count,
@@ -1636,7 +1731,7 @@ static void light_get_bounding_sphere(
real_point3d *position,
real *radius)
{
- light_compute_bounding_sphere(
+ light_compute_render_bounding_sphere(
light_index,
TRUE,
FALSE,
@@ -1666,10 +1761,7 @@ void lights_render_diffuse(
if (TEST_FLAG(light->flags, _point_light_dynamic_bit)
&& light->rasterizer_light_index != NONE)
{
- boolean skip_clusters = TEST_FLAG(light->flags,
- _point_light_attached_to_first_person_weapon_bit)
- && TEST_FLAG(light_definition_get(light->definition_index)->flags,
- _light_definition_supersize_in_first_person_bit);
+ boolean skip_clusters = light_render_skips_clusters(light);
short cluster_count = 0;
real_point3d center;
real radius;
@@ -1681,7 +1773,7 @@ void lights_render_diffuse(
MAXIMUM_CLUSTERS_PER_LIGHT,
cluster_indices);
}
- light_compute_bounding_sphere(
+ light_compute_render_bounding_sphere(
lights_globals.scene_point_lights[light_index],
FALSE,
FALSE,
@@ -1723,10 +1815,7 @@ void lights_render_specular(
&& !TEST_FLAG(light_definition_get(light->definition_index)->flags,
_light_definition_no_specular_bit))
{
- boolean skip_clusters = TEST_FLAG(light->flags,
- _point_light_attached_to_first_person_weapon_bit)
- && TEST_FLAG(light_definition_get(light->definition_index)->flags,
- _light_definition_supersize_in_first_person_bit);
+ boolean skip_clusters = light_render_skips_clusters(light);
short cluster_count = 0;
real_point3d center;
real radius;
@@ -1738,7 +1827,7 @@ void lights_render_specular(
MAXIMUM_CLUSTERS_PER_LIGHT,
cluster_indices);
}
- light_compute_bounding_sphere(
+ light_compute_render_bounding_sphere(
lights_globals.scene_point_lights[light_index],
FALSE,
TRUE,
diff --git a/source/rasterizer/xbox/rasterizer_xbox_lights.c b/source/rasterizer/xbox/rasterizer_xbox_lights.c
index 2cfc3f9be838a7c92c6c43484b7fcea5b6557afa..5c8e7b0b4c35e45de68a5a2b1f115584d2d8bdc0 100644
--- a/source/rasterizer/xbox/rasterizer_xbox_lights.c
+++ b/source/rasterizer/xbox/rasterizer_xbox_lights.c
@@ -154,8 +154,9 @@ extern struct pixel_shader_definition pixel_shader;
/* ---------- public code */
-void rasterizer_sun_glow_draw(
- struct rasterizer_lens_flare_submit_parameters const *flare)
+static void rasterizer_sun_glow_draw_internal(
+ struct rasterizer_lens_flare_submit_parameters const *flare,
+ real glow_strength)
{
real vertex_constants[5][4];
real_point3d occlusion_point;
@@ -519,7 +520,7 @@ void rasterizer_sun_glow_draw(
0.0f,
0.0f,
0.0f,
- brightness / (real)(pass + 1)) >= 0 && success)
+ brightness * glow_strength / (real)(pass + 1)) >= 0 && success)
{
success = TRUE;
}
@@ -680,6 +681,55 @@ void rasterizer_sun_glow_draw(
return;
}
+#ifdef HALO_VR
+/* Only the final sun-ray composite is scaled. Keep the visibility mask,
+ * depth test and per-eye blur at their original strength. */
+static real rasterizer_vr_sun_glow_strength(double configured)
+{
+ if (!isfinite(configured))
+ return 0.5f;
+ if (configured < 0.0)
+ return 0.0f;
+ if (configured > 1.0)
+ return 1.0f;
+ return (real)configured;
+}
+#endif
+
+void rasterizer_sun_glow_draw(
+ struct rasterizer_lens_flare_submit_parameters const *flare)
+{
+#ifdef HALO_VR
+ extern boolean vr_render_sun_glow_begin(short eye, struct render_camera *camera, struct render_frustum *frustum);
+ extern void vr_render_sun_glow_end(void);
+ extern double config_real(const char *name);
+ struct render_camera camera = global_window_parameters.camera;
+ struct render_frustum frustum = global_window_parameters.frustum;
+ short eye;
+ if (vr_render_sun_glow_begin(0, &global_window_parameters.camera, &global_window_parameters.frustum))
+ {
+ real glow_strength = rasterizer_vr_sun_glow_strength(config_real("vr.sun_glow_strength"));
+ for (eye = 0; eye < 2; eye++)
+ {
+ if (eye != 0)
+ {
+ /* Start from the saved depth range, rather than the previous
+ eye's temporary camera/frustum. Scratch textures are overwritten. */
+ global_window_parameters.camera = camera;
+ global_window_parameters.frustum = frustum;
+ vr_render_sun_glow_begin(eye, &global_window_parameters.camera, &global_window_parameters.frustum);
+ }
+ rasterizer_sun_glow_draw_internal(flare, glow_strength);
+ }
+ global_window_parameters.camera = camera;
+ global_window_parameters.frustum = frustum;
+ vr_render_sun_glow_end();
+ return;
+ }
+#endif
+ rasterizer_sun_glow_draw_internal(flare, 1.0f);
+}
+
/* ---------- private code */
static boolean rasterizer_project_billboard(
diff --git a/source/units/units.c b/source/units/units.c
index 4857fb4d0fe4f45e18d71834d648230f60be946d..817cfa4d15bbdf2f6dbbeeb63da62d1ffecd67d7 100644
--- a/source/units/units.c
+++ b/source/units/units.c
@@ -2911,12 +2911,25 @@ boolean unit_can_see_point(
struct unit_datum *unit = unit_get(unit_index);
struct object_marker head_marker;
real_vector3d direction;
+ real_vector3d looking = unit->unit.looking_vector;
object_get_marker_by_name(unit_index, "head", &head_marker, 1);
+#ifdef HALO_VR
+ /* Only the living primary player's gaze follows the actual headset
+ view. AI/remote units retain their simulation looking vectors. */
+ if (unit->unit.player_index != NONE &&
+ !TEST_FLAG(unit->object.damage_flags, _object_dead_bit) &&
+ player_get(unit->unit.player_index)->local_player_index == 0 &&
+ player_get(unit->unit.player_index)->unit_index == unit_index)
+ {
+ extern boolean vr_render_player_gaze(long unit_index, real_point3d *origin, real_vector3d *forward);
+ (void)vr_render_player_gaze(unit_index, &head_marker.matrix.position, &looking);
+ }
+#endif
vector_from_points3d(&head_marker.matrix.position, point, &direction);
normalize3d(&direction);
- if (dot_product3d(&direction, &unit->unit.looking_vector)>cosine(field_of_view))
+ if (dot_product3d(&direction, &looking)>cosine(field_of_view))
{
can_see_point = TRUE;
}
@@ -4845,13 +4858,15 @@ void unit_scripting_exit_vehicle(
return;
}
-void unit_adjust_projectile_ray(
+static void unit_adjust_projectile_ray_internal(
long unit_index,
real_point3d *origin,
real_vector3d *direction,
real *velocity,
boolean adjust_origin,
- boolean use_aiming_vector)
+ boolean use_aiming_vector,
+ real_point3d const *preview_camera,
+ real_vector3d const *preview_forward)
{
struct unit_datum *unit;
real projection;
@@ -4860,14 +4875,17 @@ void unit_adjust_projectile_ray(
unit = unit_get(unit_index);
if (use_aiming_vector)
{
- *direction = unit->unit.aiming_vector;
+ *direction = preview_forward ? *preview_forward : unit->unit.aiming_vector;
}
if (adjust_origin)
{
real_point3d camera_position;
- unit_get_camera_position(unit_index, &camera_position);
+ if (preview_camera)
+ camera_position = *preview_camera;
+ else
+ unit_get_camera_position(unit_index, &camera_position);
vector_from_points3d(&camera_position, origin, &relative);
projection =
((relative.i*direction->i + relative.k*direction->k) +
@@ -4885,6 +4903,35 @@ void unit_adjust_projectile_ray(
return;
}
+void unit_adjust_projectile_ray(
+ long unit_index,
+ real_point3d *origin,
+ real_vector3d *direction,
+ real *velocity,
+ boolean adjust_origin,
+ boolean use_aiming_vector)
+{
+ unit_adjust_projectile_ray_internal(unit_index, origin, direction, velocity,
+ adjust_origin, use_aiming_vector, NULL, NULL);
+}
+
+/* A read-only render preview shares the shot's camera/muzzle projection. */
+#ifdef HALO_VR
+void unit_preview_projectile_ray(
+ long unit_index,
+ real_point3d *origin,
+ real_vector3d *direction,
+ real *velocity,
+ boolean adjust_origin,
+ boolean use_aiming_vector,
+ real_point3d const *camera,
+ real_vector3d const *forward)
+{
+ unit_adjust_projectile_ray_internal(unit_index, origin, direction, velocity,
+ adjust_origin, use_aiming_vector, camera, forward);
+}
+#endif
+
void unit_render_debug(
long unit_index)
{
diff --git a/source/units/units.h b/source/units/units.h
index ba4d9c2bf3a8fd98bd2076eb8c40210a49de343a..d2ebc960f4904431fb0dd291506f721bd718b067 100644
--- a/source/units/units.h
+++ b/source/units/units.h
@@ -820,6 +820,13 @@ void unit_adjust_projectile_ray(
real *velocity,
boolean adjust_origin,
boolean use_aiming_vector);
+#ifdef HALO_VR
+void unit_preview_projectile_ray(long unit_index, real_point3d *origin,
+ real_vector3d *direction, real *velocity, boolean adjust_origin,
+ boolean use_aiming_vector, real_point3d const *camera,
+ real_vector3d const *forward);
+#endif
+
void unit_render_debug(
long object_index);
boolean unit_clip_to_aiming_bounds(long unit_index, real_vector3d *vector, boolean use_aiming_screen);
diff --git a/tools/port_settings.py b/tools/port_settings.py
index 63166f2ee98e00214ed4edbadda8f57e63a75d87..381b6661615ced04a955117ea747e22b13a778bd 100644
--- a/tools/port_settings.py
+++ b/tools/port_settings.py
@@ -1085,40 +1085,48 @@ def _lobby() -> list:
def _coop() -> list:
- """Co-op, the campaign for two players on this machine in split screen
- (the Xbox's Cooperative Play): Multiplayer's CO-OP CAMPAIGN, then player
- 2's profile, chosen with player 2's controller (its rows take any
- controller's presses, as the shared rows do only controller 1's), then
- New Game's levels and difficulty, which either player's controller uses
- (ui_widget.c's widget_takes_events_of_controller; menu_functions.c's
- coop_begin)"""
+ """Network campaign: one player here, other players on LAN or Internet.
+ Shared profile rows below remain available to competitive multiplayer.
+ """
base = f"{MT}/coop"
lines = _widget(f"{MT}/multiplayer_type_coop_item",
[("type", "text"), ("left", 51), ("width", 232), ("height", 32), ("bitmap", "bitmaps/list_item_bkd"),
("string_list", f"{MT}/multiplayer_options"), ("string_index", 7), ("font", "ui\\large_ui"),
("color", "#FF2896FF"), ("text_x", 13), ("text_y", 5)],
- [f'<on event="a" run="port coop begin" open="{base}/player_2_profile_screen" branch="true"/>',
- f'<on event="start" run="port coop begin" open="{base}/player_2_profile_screen" branch="true"/>',
+ [f'<on event="a" run="port coop begin" open="{base}/network_screen" branch="true"/>',
+ f'<on event="start" run="port coop begin" open="{base}/network_screen" branch="true"/>',
'<on event="left_mouse" run="mouse emit accept event"/>'])
- lines += _widget(f"{base}/player_2_profile_screen", [("width", 640), ("height", 480),
- ("flags", "pass_unhandled_to_focused_child"),
- ("bitmap", "bitmaps/gradient")],
- ['<child widget="main_menu/new_select/sel_list_desc_bkd"/>',
- f'<child widget="{base}/player_2_profile_list"/>',
- f'<child widget="{base}/header_player_2"/>',
- f'<child widget="{base}/player_2_help" x="20" y="416"/>'])
- lines += _header(f"{base}/header_player_2", f"{base}/header_player_2")
- rows = [f'<child widget="{base}/list_item_{index}" x="20" y="{73 + 30 * index}"/>' for index in range(11)]
- lines += _widget(f"{base}/player_2_profile_list",
- [("type", "column_list"), ("width", 640), ("height", 480),
- ("flags", "pass_unhandled_to_focused_child up_down_tabs_children"),
- ("description", "main_menu/profile_manager/player_profile_extended_desc")],
- ['<data input="3wide player profile list update"/>',
- '<on event="created" run="port coop player 2 list initialize"/>',
- '<on event="deleted" run="player profile list dispose"/>',
- '<on event="custom_activation" run="port coop player 2" '
- 'open="main_menu/solo_level_select/solo_level_select_screen" branch="true"/>',
- *rows, f'<child widget="{base}/player_2_button_bar" y="414"/>'])
+ lines += _widget(f"{base}/network_screen", [("width", 640), ("height", 480),
+ ("flags", "pass_unhandled_to_focused_child"),
+ ("bitmap", "bitmaps/gradient")],
+ ['<on event="b" run="port coop begin" back="true"/>',
+ '<on event="back" run="port coop begin" back="true"/>',
+ '<child widget="main_menu/new_select/sel_list_desc_bkd"/>',
+ f'<child widget="{base}/network_title" x="35" y="25"/>',
+ f'<child widget="{base}/network_list"/>',
+ f'<child widget="{base}/network_help" x="30" y="340"/>',
+ '<child widget="common_button_back" x="510" y="415"/>'])
+ lines += _widget(f"{base}/network_title", [("type", "text"), ("controller", 1), ("width", 570), ("height", 35),
+ ("text", "CO-OP CAMPAIGN"), ("font", "ui\\large_ui"),
+ ("color", "#FF2896FF"), ("text_flags", "no_focus_test")], [])
+ choices = (("coop_host_lan", "HOST LOCAL (LAN)"), ("coop_join_lan", "JOIN LOCAL (LAN)"),
+ ("coop_host_online", "HOST ONLINE (INTERNET)"), ("coop_join_online", "JOIN ONLINE (INTERNET)"),
+ ("coop_join_invite", "JOIN INVITE LINK"))
+ lines += _widget(f"{base}/network_list", [("type", "column_list"), ("width", 640), ("height", 310),
+ ("flags", "pass_unhandled_to_focused_child up_down_tabs_children")],
+ [f'<child widget="{base}/{key}" x="45" y="{90 + 42 * index}"/>'
+ for index, (key, _) in enumerate(choices)])
+ for key, caption in choices:
+ lines += _widget(f"{base}/{key}", [("type", "text"), ("width", 550), ("height", 35),
+ ("bitmap", "bitmaps/list_item_bkd"), ("text", caption),
+ ("font", "ui\\large_ui"), ("color", "#FF2896FF"), ("text_x", 13), ("text_y", 5)],
+ ['<on event="a" run="port coop begin"/>', '<on event="start" run="port coop begin"/>',
+ '<on event="left_mouse" run="mouse emit accept event"/>'])
+ lines += _widget(f"{base}/network_help", [("type", "text"), ("controller", 1), ("width", 580), ("height", 65),
+ ("text", "One player per headset or PC. LAN uses your local network.\nOnline hosts: share your invite, or choose PUBLIC in Server Setup."),
+ ("font", "ui\\small_ui"), ("color", "#FFFFFFFF"),
+ ("text_flags", "no_focus_test")], [])
+ # Competitive lobbies reuse these profile rows and their button bar.
for index in range(11):
lines += _widget(f"{base}/list_item_{index}",
[("width", 390), ("height", 28),
@@ -1129,10 +1137,6 @@ def _coop() -> list:
'<on event="left_mouse" run="mouse emit accept event"/>',
'<child widget="main_menu/new_select/list_item_text" x="25"/>',
'<child widget="main_menu/new_select/list_item_arrows"/>'])
- lines += _widget(f"{base}/player_2_help", [("type", "text"), ("width", 350), ("height", 24),
- ("text", "Player 2: choose with your controller."),
- ("font", "ui\\small_ui"), ("color", "#FF2896FF"), ("text_y", 5),
- ("text_flags", "no_focus_test")], [])
lines += _widget(f"{base}/player_2_button_bar", [("type", "column_list"), ("width", 640), ("height", 28),
("flags", "pass_unhandled_to_focused_child left_right_tabs_items")],
['<data input="common button bar update"/>',
@@ -1176,7 +1180,7 @@ def _item_options_extras() -> list:
def _map_kind() -> list:
"""the map lists' first row (New Game's and the Map screen's), as the
gametype list's chooser: a spinner of SINGLEPLAYER or MULTIPLAYER maps,
- for either controller (split screen co-op's New Game takes both)"""
+ for the player choosing a map"""
lines = _widget(MAP_KIND_CHOOSER, [("width", 256), ("height", 28), ("flags", "pass_unhandled_to_focused_child"),
("bitmap", "bitmaps/option_bkds_sm"), ("font", "ui\\large_ui"),
("color", "#FF2896FF"), ("align", "center"), ("text_y", 3)],
diff --git a/tools/test_online_coop_menu.py b/tools/test_online_coop_menu.py
new file mode 100644
index 0000000000000000000000000000000000000000..72086bda0960abfdbd70213e48f3e4f868a75ce7
--- /dev/null
+++ b/tools/test_online_coop_menu.py
@@ -0,0 +1,536 @@
+#!/usr/bin/env python3
+"""Execute production LAN/Internet campaign menu routes in flat and VR builds.
+
+Validate actual generated XML, event dispatch, map and browser filters,
+failure/cancel cleanup and one-player-per-machine campaign guards. No game,
+network or headset is used; compile the extracted C with warnings as errors.
+"""
+import argparse
+import importlib.util
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+import xml.etree.ElementTree as ET
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def mask(text):
+ return re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"',
+ lambda match: re.sub(r'[^\n]', ' ', match.group()), text, flags=re.S)
+
+
+def block(text, start):
+ hidden = mask(text)
+ opening = hidden.index('{', start)
+ depth, end = 1, opening + 1
+ while depth:
+ depth += (hidden[end] == '{') - (hidden[end] == '}')
+ end += 1
+ return text[start:end]
+
+
+def function(text, name):
+ found = re.search(r'(?:static\s+)?(?:boolean|void|short|int|char const\s*\*)\s*' + re.escape(name) +
+ r'\s*\([^;]*?\)\s*\{', text)
+ if not found:
+ raise AssertionError('Missing production helper: ' + name)
+ return block(text, found.start())
+
+
+def source_integration():
+ menu = (ROOT / 'port/linux/game/menu_functions.c').read_text()
+ events = (ROOT / 'source/interface/ui_widget_event_handler_functions.c').read_text()
+ widget = (ROOT / 'source/interface/ui_widget.c').read_text()
+ asset = ROOT / 'port/assets/menus/ce/main_menu.multiplayer_type_select.coop.xml'
+ spec = importlib.util.spec_from_file_location('coop_settings_generator', ROOT / 'tools/port_settings.py')
+ generator = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(generator)
+ assert asset.read_text() == '\n'.join(generator.multiplayer_files()[asset.name])
+ xml = ET.parse(asset).getroot()
+ nodes = {node.attrib['name'].rsplit('/', 1)[-1]: node for node in xml}
+ for button in ('a', 'start'):
+ handler = next(node for node in nodes['multiplayer_type_coop_item'] if node.attrib.get('event') == button)
+ assert handler.attrib['run'] == 'port coop begin'
+ assert handler.attrib['open'].endswith('coop/network_screen') and handler.attrib['branch'] == 'true'
+ for button in ('b', 'back'):
+ handler = next(node for node in nodes['network_screen'] if node.attrib.get('event') == button)
+ assert handler.attrib['run'] == 'port coop begin' and handler.attrib['back'] == 'true'
+ captions = {'coop_host_lan': 'HOST LOCAL (LAN)', 'coop_join_lan': 'JOIN LOCAL (LAN)',
+ 'coop_host_online': 'HOST ONLINE (INTERNET)', 'coop_join_online': 'JOIN ONLINE (INTERNET)',
+ 'coop_join_invite': 'JOIN INVITE LINK'}
+ for name, caption in captions.items():
+ assert nodes[name].attrib['text'] == caption and len(name) <= 31
+ for button in ('a', 'start'):
+ handler = next(node for node in nodes[name] if node.attrib.get('event') == button)
+ assert handler.attrib == {'event': button, 'run': 'port coop begin'}
+ assert 'player_2_profile_screen' not in nodes and 'player_2_profile_list' not in nodes
+ assert not any(node.attrib.get('run', '').startswith('port coop player 2') for node in xml.iter())
+ assert len([name for name in nodes if name.startswith('list_item_')]) == 11
+ assert 'player_2_button_bar' in nodes
+ lobby = ET.parse(ROOT / 'port/assets/menus/ce/main_menu.multiplayer_type_select.lobby.xml').getroot()
+ prefix = 'main_menu/multiplayer_type_select/coop/'
+ for node in lobby.iter():
+ reference = node.attrib.get('widget', '')
+ if reference.startswith(prefix):
+ assert reference[len(prefix):] in nodes
+ assert '"port coop begin"' in (ROOT / 'port/linux/game/menu_tags.c').read_text()
+ assert 'player_spawn_count = 2;' not in menu and 'vr_cooperative_begin' not in menu
+ dispatcher = function(menu, 'pc_menu_event_function_invoke')
+ coop = block(dispatcher, dispatcher.index('else if (!strcmp(name, "port coop begin"))'))
+ assert '#ifdef' not in coop and 'return cooperative_begin(' in coop
+ stale = block(dispatcher, dispatcher.index('else if (!strcmp(name, "port coop player 2 list initialize")'))
+ assert 'return campaign_fail();' in stale
+ back = block(dispatcher, dispatcher.index('else if (!strcmp(name, "controls back handler")'))
+ assert '!global_network_game_server_get()' in back and 'cooperative_session_cleanup();' in back
+ assert 'if (!result && cooperative_network_game)' in function(menu, 'browser_initialize')
+ assert 'lobby_local_player_count() > 1' in function(menu, 'map_list_choose')
+ assert 'map_list.campaign_only ? MAP_KIND_SINGLEPLAYER' in function(menu, 'map_list_update')
+ assert function(menu, 'lobby_browser_select').count('multiplayer_game_player(controller, found)') == 2
+ native = (ROOT / 'source/networking/network_server_message_handler.c').read_text()
+ response = function(native, 'network_game_server_handle_message_client_broadcast_game_search')
+ assert not any(name in response for name in ('game_private', 'coop_public', 'p2p_lobby'))
+ assert 'advertisement.engine_type = (short)game->variant.game_engine_index;' in response
+ assert 'csmemcpy(&advertisement.map, &game->map, sizeof(game->map));' in response
+ assert 'network_game_accept_remote_connections(TRUE);' in function(events, 'network_game_start_new_server')
+ dispatch = function(widget, 'event_handler_dispatch')
+ fallback = block(dispatch, dispatch.index('if (TEST_FLAG(handler->flags, _event_handler_open_widget_bit)'))
+ assert '!widget_deleted' in fallback
+ assert widget.index('ui_widget_load_children_recursive(widget, definition)') < widget.index(
+ 'if (handler->event_type == _widget_event_created)', widget.index('ui_widget_load_children_recursive(widget, definition)'))
+ helpers = '\n\n'.join(function(menu, name) for name in (
+ 'lobby_join_reset', 'scenario_name', 'campaign_level_of', 'game_cooperative',
+ 'lobby_local_player_count', 'multiplayer_mode_set', 'multiplayer_player', 'multiplayer_host_mode',
+ 'multiplayer_host', 'cooperative_single_player', 'cooperative_session_cleanup',
+ 'cooperative_network_host', 'cooperative_network_join', 'cooperative_begin',
+ 'map_step_count', 'map_step_open', 'map_list_initialize', 'map_list_choose', 'map_list_back',
+ 'advertised_cooperative', 'multiplayer_game_player', 'browser_games_read',
+ 'text_to_wide', 'lobby_browser_valid_games', 'browser_initialize', 'browser_join',
+ 'lobby_campaign_single_player', 'campaign_local_player_block', 'lobby_add_player',
+ 'lobby_join_start', 'lobby_player_choose', 'lobby_join_help', 'preview_add', 'preview_join'))
+ helpers += '\n' + function(events, 'ui_widget_port_open')
+ return helpers, coop, back, fallback
+
+
+HARNESS = r'''
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <wchar.h>
+typedef int boolean;
+#define FALSE 0
+#define TRUE 1
+#define NONE (-1)
+#define MIN(a,b) ((a)<(b)?(a):(b))
+#define MAX(a,b) ((a)>(b)?(a):(b))
+#define PIN(v,a,b) MIN(MAX(v,a),b)
+#define NUMBEROF(a) ((short)(sizeof(a)/sizeof((a)[0])))
+#define MAXIMUM_LOCAL_PLAYERS 4
+#define MAXIMUM_ADVERTISED_GAMES 4
+#define NUMBER_OF_GAME_DIFFICULTY_LEVELS 4
+#define NUMBER_OF_SINGLE_PLAYER_LEVELS 10
+#define P2P_LISTING_NAME_SIZE 31
+#define TEST_FLAG(value,bit) ((value) & (1U << (bit)))
+#define _event_handler_open_widget_bit 0
+#define _ui_audio_feedback_none 0
+#define _ui_audio_feedback_forward 1
+#define _error_silent 0
+#define MAP_KIND_SINGLEPLAYER 0
+#define MAP_KIND_MULTIPLAYER 1
+#define MAP_STEP_MAPS 0
+#define MAP_STEP_DIFFICULTIES 1
+#define SOUND_BACK 3
+#define SOUND_FORWARD 2
+#define BUTTON_START 7
+#define _client_state_pregame 1
+#define SERVER_SETUP_NAME "pc\\main_menu\\multiplayer_type_select\\server_settings\\server_settings_screen"
+#define LOBBY_NAME "lobby"
+enum { _multiplayer_mode_server_browser, _multiplayer_mode_lan, _multiplayer_mode_direct_link,
+ _multiplayer_mode_host_internet, _multiplayer_mode_host_lan };
+struct widget_instance {
+ const char *name; struct widget_instance *parent; int definition_tag_index;
+ struct widget_instance *child, *next; boolean disabled, visible;
+};
+struct event_record { short controller_index; };
+struct player_profile { int marker; };
+struct ui_widget_event_handler_reference { unsigned flags; struct { int index; } widget_tag; };
+struct advertised_game {
+ short engine_type, player_count, maximum_player_count;
+ const char *map_name; boolean valid, in_progress, peer, open;
+};
+struct p2p_listing { short engine_type; char name[32], map[32]; };
+struct network_game { struct { short game_engine_index; } variant; struct { const char *name; } map; short maximum_players; };
+static struct {
+ short mode; boolean game_private, cooperative_maximum_players_set;
+ short game_count, game_chosen; struct advertised_game *games[MAXIMUM_ADVERTISED_GAMES];
+} multiplayer;
+static struct { short kind,step; boolean hosting,campaign_only; short map_count,first,chosen,level; } map_list;
+static struct { boolean adding; short controller; } lobby_join;
+static struct { short chosen,count; long indices[4]; } profile_list;
+static struct network_game client_game;
+static struct advertised_game available[MAXIMUM_ADVERTISED_GAMES], *selected_preview;
+static boolean cooperative_map_opening, cooperative_network_game, lan_mode;
+static int checks, player_spawn_count, online, host_public, coop_public;
+static int profile_ok, player_ok, host_ok, open_ok, browse_ok, initialize_child, server_exists, client_exists, local_split;
+static int host_calls, open_calls, reset_calls, panel_opens, failures, errors, went_back;
+static int allow_hosting, public_hosting, spinner_kind, focused_entry, disposed_client, disposed_server;
+static int accept_remote, variant_cleared, connection, expected_controller, players[4], old_players_cleared, browse_calls;
+static int choose_ok, choose_calls, chosen_level, chosen_difficulty, mp_map_calls, dispatch_calls, join_calls, join_ok;
+static int client_state, lobby_player_count, copy_public_browse, campaign_ui_visible;
+static struct widget_instance maps={"maps",NULL,5,NULL,NULL,FALSE,TRUE};
+static struct widget_instance helper_widget={"help",NULL,5,NULL,NULL,FALSE,TRUE};
+static const char *const map_names[]={"bloodgulch"};
+static const char *const campaign_names[]={"a10","a30","a50","b30","b40","c10","c20","c40","d20","d40"};
+#define CHECK(c) do { checks++; if (!(c)) { fprintf(stderr,"FAIL line %d: %s\n",__LINE__,#c); exit(1); } } while(0)
+static int config_boolean(const char *name) {
+ if (!strcmp(name,"network.online")) return online;
+ if (!strcmp(name,"network.host_public")) return host_public;
+ CHECK(!strcmp(name,"network.coop_public")); return coop_public;
+}
+static boolean campaign_profile(short controller,struct player_profile *profile) {
+ (void)profile; CHECK(controller==expected_controller); return profile_ok;
+}
+static boolean campaign_fail(void) { failures++; return FALSE; }
+static void display_error_text_deferred(const wchar_t *message,short controller) {
+ CHECK(message && controller==NONE); errors++;
+}
+static void player_ui_reset_single_player_local_player_controllers(void) { reset_calls++; }
+static void player_ui_local_player_left_multiplayer_game(short controller) {
+ CHECK(controller>=0 && controller<4); players[controller]=0; old_players_cleared++;
+}
+static boolean lobby_controller_playing(short controller) { return players[controller]; }
+static long player_ui_get_active_player_profile_index(int index) { CHECK(index==0); return 9; }
+static boolean ui_widget_port_multiplayer_player(short controller,long profile) {
+ CHECK(controller==expected_controller && profile==9); if(player_ok) players[controller]=1; return player_ok;
+}
+static void p2p_set_hosting_allowed(int value) { allow_hosting=value; }
+static void p2p_set_hosting_public(int value) { public_hosting=value; }
+static void p2p_lobby_browse(int value) { copy_public_browse=value; }
+static boolean ui_widget_port_host(struct widget_instance *w,struct event_record *e,boolean *deleted) {
+ (void)w;(void)e;(void)deleted; host_calls++; server_exists=client_exists=host_ok;
+ accept_remote=host_ok; connection=host_ok?2:0; return host_ok;
+}
+static void *global_network_game_server_get(void) { return server_exists?&server_exists:NULL; }
+static void *global_network_game_client_get(void) { return client_exists?&client_exists:NULL; }
+static struct network_game *network_game_client_get_game(void *client) { CHECK(client!=NULL); return &client_game; }
+static short network_game_client_get_state(void *client,short *data) { CHECK(client!=NULL);*data=0;return (short)client_state; }
+static boolean network_game_is_splitscreen_local(void) { return local_split; }
+static short ui_widget_port_multiplayer_maps(const char *const **names,short *last) {
+ *names=map_names;*last=7;return 13;
+}
+static const char *main_get_solo_level_name(short level) { CHECK(level>=0 && level<10);return campaign_names[level]; }
+static short main_get_solo_level_from_name(const char *name) {
+ short i;for(i=0;i<10;i++) if(!strcmp(name,campaign_names[i])) return i;return NONE;
+}
+static short main_get_difficulty(void) { return 2; }
+#ifndef _WIN32
+static int _stricmp(const char *a,const char *b) { return strcmp(a,b); }
+#endif
+static boolean ui_widget_port_cooperative_level_choose(const char *name,short difficulty) {
+ choose_calls++;chosen_level=main_get_solo_level_from_name(name);chosen_difficulty=difficulty;
+ CHECK(server_exists && chosen_level!=NONE && difficulty>=0 && difficulty<4);
+ if(choose_ok){client_game.variant.game_engine_index=0;client_game.map.name=name;} return choose_ok;
+}
+static boolean ui_widget_port_multiplayer_map_choose(short chosen) { CHECK(chosen>=0);mp_map_calls++;return TRUE; }
+static void map_kind_set(struct widget_instance *list,short kind) { CHECK(list==&maps);spinner_kind=kind; }
+static short map_kind_first(short chosen,short count) { (void)count;return chosen; }
+static void map_kind_focus(struct widget_instance *list,short first,short chosen) { CHECK(list==&maps && first==chosen);focused_entry=chosen; }
+static void ui_play_audio_feedback_sound(int sound) { CHECK(sound==SOUND_BACK || sound==SOUND_FORWARD); }
+static void ui_widget_port_go_back(struct widget_instance *list) { CHECK(list!=NULL);went_back++; }
+static void dispose_global_network_game_client(void) { disposed_client++;client_exists=0; }
+static void dispose_global_network_game_server(void) { disposed_server++;server_exists=0; }
+static void network_game_accept_remote_connections(boolean accept) { accept_remote=accept; }
+static void player_ui_clear_multiplayer_variant(void) { variant_cleared++; }
+static void game_connection_set(short value) { connection=value; }
+static struct widget_instance *widget_instance_get_topmost_parent(struct widget_instance *w) { return w; }
+static int widget_instance_get_child_index_from_parent(struct widget_instance *w) { (void)w;return 0; }
+static void error(int level,const char *text,...) { (void)level;(void)text;errors++; }
+static boolean map_list_initialize(struct widget_instance *list);
+static boolean browser_initialize(struct widget_instance *screen,struct event_record *event,boolean *deleted);
+static struct widget_instance *ui_widget_load_by_name_or_tag(const char *name,int tag,void *other,int index,
+ int previous,int parent,int child) {
+ struct event_record event={(short)expected_controller};boolean deleted=FALSE;
+ (void)tag;(void)other;(void)index;(void)previous;(void)parent;(void)child;open_calls++;
+ if(strstr(name,"connected_map_select_wrapper")) {
+ CHECK(cooperative_map_opening && player_spawn_count==1);
+ if(initialize_child) CHECK(map_list_initialize(&maps));
+ } else if(strstr(name,"join_game_screen")) {
+ CHECK(cooperative_network_game && !cooperative_map_opening && player_spawn_count==1);
+ if(initialize_child) (void)browser_initialize(&maps,&event,&deleted);
+ } else CHECK(!strcmp(name,SERVER_SETUP_NAME));
+ return open_ok?&maps:NULL;
+}
+static boolean ui_widget_launch_widget(struct widget_instance *widget,int tag) {
+ (void)widget;CHECK(tag==55);panel_opens++;return TRUE;
+}
+static boolean ui_widget_port_open(struct widget_instance *widget,const char *name,boolean *deleted);
+static struct widget_instance *screen_of(struct widget_instance *list) { return list; }
+static struct widget_instance *named(struct widget_instance *screen,const char *name,short index) {
+ (void)screen;(void)index;
+ if(!strcmp(name,"lobby_join_help") || strstr(name,"button_add") || strstr(name,"button_bar")) return &helper_widget;
+ return NULL;
+}
+static void visible_set(struct widget_instance *widget,boolean visible) { if(widget)campaign_ui_visible=visible; }
+static void focus_off_hidden(struct widget_instance *widget) { (void)widget; }
+static void text_set(struct widget_instance *widget,const wchar_t *text) {
+ CHECK(widget && text);if(cooperative_network_game)CHECK(wcsstr(text,L"One player per machine")!=NULL);
+}
+static boolean input_has_gamepad(short controller) { (void)controller;return TRUE; }
+static void server_browser_initialize(struct widget_instance *screen) { (void)screen; }
+static boolean browser_item_usable(struct widget_instance *child) { (void)child;return TRUE; }
+static void browser_focus(struct widget_instance *list) { (void)list; }
+static void lobby_browser_begin(void) { p2p_lobby_browse(TRUE); }
+static boolean ui_widget_port_browse(struct widget_instance *screen,struct event_record *event,boolean *deleted) {
+ (void)screen;(void)event;(void)deleted;browse_calls++;client_exists=browse_ok;return browse_ok;
+}
+static struct advertised_game *network_game_client_get_available_games(void *client) { CHECK(client!=NULL);return available; }
+static boolean network_game_client_advertised_game_is_valid(struct advertised_game *game) { return game->valid; }
+static boolean advertised_in_progress(struct advertised_game *game) { return game->in_progress; }
+static boolean game_from_peer(struct advertised_game *game) { return game->peer; }
+static boolean player_name_clean(wchar_t *name,short size) { (void)size;return *name!=0; }
+static struct widget_instance *focused_leaf(struct widget_instance *widget) { return widget; }
+static void ui_widget_port_dispatch_event(struct widget_instance *widget,int button,short controller,boolean *deleted) {
+ (void)widget;(void)controller;(void)deleted;CHECK(button==BUTTON_START);dispatch_calls++;
+}
+static struct advertised_game *preview_game(void) { return selected_preview; }
+static boolean ui_widget_port_join(struct widget_instance *widget,struct advertised_game *game,const char *name,boolean *deleted) {
+ (void)widget;(void)deleted;CHECK(game && !strcmp(name,LOBBY_NAME));join_calls++;return join_ok;
+}
+@@HELPERS@@
+static boolean dispatch(struct widget_instance *widget,struct event_record *event,boolean *widget_deleted) {
+ short controller=event->controller_index;const char *name="port coop begin";
+ if(0)return FALSE;
+ @@COOP_DISPATCH@@
+ return FALSE;
+}
+static boolean dispatch_back(struct widget_instance *widget,const char *name,boolean *widget_deleted) {
+ if(0)return FALSE;
+ @@BACK_DISPATCH@@
+ return TRUE;
+}
+static void xml_open_fallback(struct widget_instance *widget,boolean *deleted) {
+ struct ui_widget_event_handler_reference reference={1U,{55}};
+ struct ui_widget_event_handler_reference *handler=&reference;
+ boolean widget_deleted=*deleted,success=TRUE;long audio_feedback=_ui_audio_feedback_none;
+ @@XML_FALLBACK@@
+ *deleted=widget_deleted;(void)success;(void)audio_feedback;
+}
+static boolean press_coop(struct widget_instance *widget,struct event_record *event,boolean *deleted) {
+ boolean handled=dispatch(widget,event,deleted);
+ if(handled && !strcmp(widget->name,"multiplayer_type_coop_item"))xml_open_fallback(widget,deleted);
+ return handled;
+}
+static void reset_state(void) {
+ memset(&multiplayer,0,sizeof(multiplayer));memset(&map_list,0,sizeof(map_list));memset(players,0,sizeof(players));
+ memset(available,0,sizeof(available));cooperative_map_opening=cooperative_network_game=lan_mode=FALSE;
+ player_spawn_count=1;online=1;host_public=1;coop_public=0;
+ profile_ok=player_ok=host_ok=open_ok=browse_ok=choose_ok=join_ok=initialize_child=1;
+ server_exists=client_exists=local_split=host_calls=open_calls=reset_calls=panel_opens=failures=errors=went_back=0;
+ allow_hosting=public_hosting=0;spinner_kind=focused_entry=-1;
+ disposed_client=disposed_server=variant_cleared=old_players_cleared=browse_calls=0;
+ accept_remote=1;connection=2;choose_calls=mp_map_calls=dispatch_calls=join_calls=copy_public_browse=0;
+ chosen_level=chosen_difficulty=-1;client_state=_client_state_pregame;lobby_player_count=1;selected_preview=NULL;
+ client_game.variant.game_engine_index=1;client_game.map.name="bloodgulch";client_game.maximum_players=16;
+ profile_list.chosen=0;profile_list.count=1;profile_list.indices[0]=9;
+ lobby_join.adding=FALSE;lobby_join.controller=NONE;campaign_ui_visible=TRUE;
+}
+static void expect_cleanup(void) {
+ CHECK(!cooperative_map_opening && !cooperative_network_game && !server_exists && !client_exists);
+ CHECK(!allow_hosting && !public_hosting && !accept_remote && !connection && !copy_public_browse);
+ CHECK(player_spawn_count==1 && lobby_join.controller==NONE && !lobby_join.adding);
+ CHECK(players[0]+players[1]+players[2]+players[3]==0);
+}
+static void test_routes(short controller) {
+ struct widget_instance item={"multiplayer_type_coop_item",NULL,2,NULL,NULL,FALSE,TRUE};
+ struct event_record event={controller};boolean deleted;int i,public_choice,child;
+ const char *routes[]={"coop_host_lan","coop_host_online","coop_join_lan","coop_join_online","coop_join_invite"};
+ const int modes[]={_multiplayer_mode_host_lan,_multiplayer_mode_host_internet,_multiplayer_mode_lan,
+ _multiplayer_mode_server_browser,_multiplayer_mode_direct_link};
+ expected_controller=controller;
+ reset_state();players[0]=players[1]=players[2]=players[3]=1;player_spawn_count=4;
+ lobby_join.adding=TRUE;lobby_join.controller=2;deleted=FALSE;
+ CHECK(press_coop(&item,&event,&deleted));CHECK(deleted && panel_opens==1 && !host_calls && !open_calls);
+ CHECK(player_spawn_count==1 && cooperative_network_game && old_players_cleared==4);
+ item.name="network_screen";deleted=FALSE;CHECK(press_coop(&item,&event,&deleted));expect_cleanup();
+ for(i=0;i<5;i++) {
+ item.name=routes[i];reset_state();deleted=FALSE;
+ players[0]=players[1]=players[2]=players[3]=1;player_spawn_count=4;lobby_join.adding=TRUE;
+ online=(i==0 || i==2)?0:1;
+ CHECK(press_coop(&item,&event,&deleted));CHECK(deleted && !panel_opens && player_spawn_count==1);
+ CHECK(multiplayer.mode==modes[i] && cooperative_network_game && !cooperative_map_opening);
+ CHECK(players[0]+players[1]+players[2]+players[3]==1 && players[controller]);
+ CHECK(lan_mode==(i==0 || i==2) && !lobby_join.adding && lobby_join.controller==NONE);
+ if(i<2) {
+ CHECK(host_calls==1 && server_exists && client_exists && accept_remote);
+ CHECK(allow_hosting==(i==1) && !public_hosting && multiplayer.game_private);
+ CHECK(map_list.kind==MAP_KIND_SINGLEPLAYER && map_list.campaign_only && spinner_kind==MAP_KIND_SINGLEPLAYER);
+ CHECK(focused_entry==0);
+ /* Campaign route cannot be redirected by a forged spinner state. */
+ map_list.kind=MAP_KIND_MULTIPLAYER;map_list.chosen=3;deleted=FALSE;
+ CHECK(!map_list_choose(&maps,&deleted));CHECK(map_list.step==MAP_STEP_DIFFICULTIES && map_list.level==3);
+ map_list.chosen=1;CHECK(map_list_choose(&maps,&deleted));CHECK(deleted && choose_calls==1);
+ CHECK(chosen_level==3 && chosen_difficulty==1 && !mp_map_calls);
+ deleted=FALSE;CHECK(dispatch_back(&maps,"gamespy back handler",&deleted));
+ CHECK(deleted && server_exists && cooperative_network_game); /* Server Setup -> map keeps host. */
+ CHECK(map_list_initialize(&maps)); /* Back reconstructs the real map list. */
+ CHECK(map_list.campaign_only && map_list.kind==MAP_KIND_SINGLEPLAYER && spinner_kind==MAP_KIND_SINGLEPLAYER);
+ map_list.chosen=3;deleted=FALSE;CHECK(!map_list_choose(&maps,&deleted));
+ CHECK(map_list_back(&maps,&deleted));CHECK(!deleted && map_list.step==MAP_STEP_MAPS);
+ CHECK(map_list_back(&maps,&deleted));CHECK(deleted);expect_cleanup();
+ CHECK(map_list_initialize(&maps));CHECK(map_list.kind==MAP_KIND_MULTIPLAYER && !map_list.campaign_only);
+ } else {
+ CHECK(!host_calls && browse_calls==1 && client_exists && !server_exists);
+ CHECK(copy_public_browse==(i==3));
+ deleted=FALSE;CHECK(dispatch_back(&maps,"gamespy back handler",&deleted));CHECK(deleted);expect_cleanup();
+ }
+ /* Profile rejection and failed screen loads never inherit old campaign intent. */
+ reset_state();deleted=FALSE;profile_ok=0;CHECK(!press_coop(&item,&event,&deleted));expect_cleanup();
+ reset_state();deleted=FALSE;player_ok=0;CHECK(!press_coop(&item,&event,&deleted));expect_cleanup();
+ for(child=0;child<2;child++) {
+ reset_state();deleted=FALSE;open_ok=0;initialize_child=child;
+ CHECK(!press_coop(&item,&event,&deleted));CHECK(!deleted);expect_cleanup();
+ }
+ if(i==1 || i==3 || i==4) {
+ reset_state();deleted=FALSE;online=0;CHECK(!press_coop(&item,&event,&deleted));
+ CHECK(errors==1 && failures==1 && !host_calls && !open_calls);expect_cleanup();
+ }
+ if(i<2) {
+ reset_state();deleted=FALSE;host_ok=0;CHECK(!press_coop(&item,&event,&deleted));expect_cleanup();
+ } else {
+ reset_state();deleted=FALSE;browse_ok=0;CHECK(press_coop(&item,&event,&deleted));
+ /* Creation succeeds even if its created callback fails; connection is still disposed. */
+ expect_cleanup();
+ }
+ }
+ for(public_choice=0;public_choice<2;public_choice++) {
+ reset_state();deleted=FALSE;item.name="coop_host_online";coop_public=public_choice;host_public=!public_choice;
+ CHECK(press_coop(&item,&event,&deleted));CHECK(public_hosting==public_choice && multiplayer.game_private==!public_choice);
+ reset_state();deleted=FALSE;item.name="coop_host_lan";coop_public=public_choice;online=0;
+ CHECK(press_coop(&item,&event,&deleted));CHECK(!public_hosting && !allow_hosting && accept_remote);
+ }
+ /* Competitive hosts retain normal preference and map choice. */
+ reset_state();deleted=FALSE;item.name="multiplayer_type_create_interne";cooperative_network_game=TRUE;cooperative_map_opening=TRUE;
+ CHECK(multiplayer_host(&item,&event,controller,&deleted));CHECK(!cooperative_network_game && !cooperative_map_opening);
+ CHECK(multiplayer.mode==_multiplayer_mode_host_internet && public_hosting==host_public);
+ CHECK(map_list_initialize(&maps));CHECK(map_list.kind==MAP_KIND_MULTIPLAYER && !map_list.campaign_only);
+ reset_state();item.name="multiplayer_type_create_lan_item";CHECK(multiplayer_host(&item,&event,controller,&deleted));
+ CHECK(multiplayer.mode==_multiplayer_mode_host_lan && !allow_hosting && !public_hosting);
+}
+static void test_discovery(void) {
+ struct p2p_listing listings[3]={{0,"campaign","a10"},{1,"competitive","bloodgulch"},{0,"bad campaign","bloodgulch"}};
+ struct widget_instance item={"join",NULL,2,NULL,NULL,FALSE,TRUE};boolean deleted=FALSE;
+ reset_state();expected_controller=0;client_exists=TRUE;cooperative_network_game=TRUE;multiplayer.mode=_multiplayer_mode_lan;online=0;
+ available[0]=(struct advertised_game){0,1,8,"levels\\a10\\a10",TRUE,FALSE,FALSE,TRUE};
+ available[1]=(struct advertised_game){1,1,16,"bloodgulch",TRUE,FALSE,FALSE,TRUE};
+ available[2]=(struct advertised_game){0,1,8,"a30",TRUE,TRUE,FALSE,TRUE};
+ available[3]=(struct advertised_game){0,1,8,"a50",TRUE,FALSE,TRUE,TRUE};
+ browser_games_read();CHECK(multiplayer.game_count==2 && multiplayer.games[0]==&available[0] && multiplayer.games[1]==&available[2]);
+ CHECK(browser_join(&item,0,&deleted));CHECK(join_calls==1 && players[0] && player_spawn_count==1);
+ multiplayer.mode=_multiplayer_mode_direct_link;browser_games_read();CHECK(multiplayer.game_count==1 && multiplayer.games[0]==&available[3]);
+ CHECK(lobby_browser_valid_games(listings,3)==1 && !strcmp(listings[0].map,"a10"));
+ cooperative_network_game=FALSE;multiplayer.mode=_multiplayer_mode_lan;browser_games_read();CHECK(multiplayer.game_count==3);
+ players[0]=players[1]=players[2]=players[3]=1;player_spawn_count=4;
+ CHECK(multiplayer_game_player(0,&available[0]));CHECK(cooperative_network_game && player_spawn_count==1);
+ CHECK(players[0] && !players[1] && !players[2] && !players[3]);
+ reset_state();client_exists=TRUE;players[1]=1;
+ available[1]=(struct advertised_game){1,1,16,"bloodgulch",TRUE,FALSE,FALSE,TRUE};
+ CHECK(multiplayer_game_player(0,&available[1]));CHECK(players[0] && players[1]);CHECK(!cooperative_network_game && !reset_calls);
+}
+static void test_single_player_guards(void) {
+ struct widget_instance item={"lobby",NULL,2,NULL,NULL,FALSE,TRUE};boolean deleted=FALSE;int source;
+ for(source=0;source<3;source++) {
+ reset_state();expected_controller=1;client_exists=TRUE;players[0]=1;
+ cooperative_network_game=source==0;
+ client_game.variant.game_engine_index=source==1?0:1;client_game.map.name=source==1?"a10":"bloodgulch";
+ available[0]=(struct advertised_game){0,1,8,"a10",TRUE,TRUE,FALSE,TRUE};selected_preview=&available[0];
+ if(source<2) {
+ CHECK(!lobby_add_player());CHECK(!lobby_join.adding && lobby_join.controller==NONE);
+ CHECK(!lobby_join_start(&item,1,&deleted));CHECK(lobby_join.controller==NONE);
+ lobby_join.controller=1;CHECK(!lobby_player_choose());CHECK(!players[1] && lobby_join.controller==NONE);
+ lobby_join_help(&item);CHECK(!campaign_ui_visible);
+ }
+ CHECK(!preview_add(&item,1,&deleted));CHECK(lobby_join.controller==NONE && !players[1]);
+ CHECK(!preview_add(&item,0,&deleted));CHECK(dispatch_calls==1);
+ if(source<2){CHECK(!lobby_join_start(&item,0,&deleted));CHECK(dispatch_calls==2);}
+ }
+ reset_state();expected_controller=1;client_exists=TRUE;players[0]=1;
+ CHECK(lobby_add_player());CHECK(lobby_join.adding);
+ CHECK(lobby_join_start(&item,1,&deleted));CHECK(lobby_join.controller==1);
+ CHECK(lobby_player_choose());CHECK(players[0] && players[1]);lobby_join_help(&item);CHECK(campaign_ui_visible);
+ reset_state();server_exists=client_exists=TRUE;map_list.kind=MAP_KIND_SINGLEPLAYER;map_list.step=MAP_STEP_DIFFICULTIES;
+ map_list.level=0;map_list.chosen=1;players[0]=players[1]=1;
+ CHECK(!map_list_choose(&maps,&deleted));CHECK(!choose_calls);
+ players[1]=0;player_spawn_count=2;CHECK(!map_list_choose(&maps,&deleted));CHECK(!choose_calls);
+ reset_state();expected_controller=0;client_exists=TRUE;players[0]=players[1]=1;player_spawn_count=2;
+ available[0]=(struct advertised_game){0,1,8,"a10",TRUE,TRUE,FALSE,TRUE};selected_preview=&available[0];
+ CHECK(preview_join(&item,0,&deleted));CHECK(join_calls==1 && players[0] && !players[1] && player_spawn_count==1);
+ reset_state();client_exists=TRUE;players[0]=players[1]=1;
+ available[0]=(struct advertised_game){1,1,16,"bloodgulch",TRUE,TRUE,FALSE,TRUE};selected_preview=&available[0];
+ CHECK(preview_join(&item,0,&deleted));CHECK(players[0] && players[1] && !reset_calls);
+}
+int main(void) {
+ short controller;for(controller=0;controller<4;controller++)test_routes(controller);
+ test_discovery();test_single_player_guards();
+ printf("PASS: %d production campaign routing / single-player assertions (%s)\n",checks,
+#ifdef HALO_VR
+ "VR"
+#else
+ "flat"
+#endif
+ );return 0;
+}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc', help='C compiler executable: cl, clang, gcc or cc')
+ parser.add_argument('--vcvarsall', type=Path, help='Visual Studio vcvarsall.bat')
+ args = parser.parse_args()
+ helpers, dispatch, back_dispatch, fallback = source_integration()
+ harness = HARNESS.replace('@@HELPERS@@', helpers).replace('@@COOP_DISPATCH@@', dispatch).replace('@@BACK_DISPATCH@@', back_dispatch).replace('@@XML_FALLBACK@@', fallback)
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ installations = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(installations.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1])
+ vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No host C compiler; select one with --cc.')
+ with tempfile.TemporaryDirectory(prefix='halo-online-coop-menu-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory)
+ source = temp / 'test.c'
+ source.write_text(harness)
+ for vr in (True, False):
+ executable = temp / ('test-vr' if vr else 'test-flat')
+ if os.name == 'nt':
+ executable = executable.with_suffix('.exe')
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/wd4505',
+ '/D_CRT_SECURE_NO_WARNINGS', *(['/DHALO_VR'] if vr else []),
+ '/Fe:' + str(executable), '/Fo:' + str(temp / ('test-vr.obj' if vr else 'test-flat.obj')), str(source)]
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported compiler/temp path shell metacharacter.')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\n'
+ 'if errorlevel 1 exit /b 1\n' + subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-Wno-unused-function',
+ *(['-DHALO_VR'] if vr else []), str(source), '-o', str(executable)]
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+ print('PASS: generated five-route XML, LAN discovery, and single-player campaign integration')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_flashlight.py b/tools/test_vr_flashlight.py
new file mode 100644
index 0000000000000000000000000000000000000000..0b0f6b83d8e2a90962c81eac5ae46a8110b61c1f
--- /dev/null
+++ b/tools/test_vr_flashlight.py
@@ -0,0 +1,282 @@
+#!/usr/bin/env python3
+"""Host checks of production unarmed VR flashlight pose and rendering bounds.
+
+Reuses the real projectile/reticle regression harness, the actual VR view
+layout and extracted production light functions. Deterministic engine services
+stand in for maps/GPU/headset; this does not validate physical beam placement
+or dynamic object-light cluster selection on hardware.
+"""
+from pathlib import Path
+import re
+import subprocess
+
+import test_vr_projectile_reticle as reticle
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+LIGHT_FIXTURE = r'''
+#define MAXIMUM_RENDERED_LIGHTS 2
+#define MAXIMUM_CLUSTERS_PER_LIGHT 8
+static const real _pi=3.14159265358979323846f;
+enum { _point_light_dynamic_bit=0, _point_light_attached_to_first_person_weapon_bit=3 };
+enum { _light_definition_no_specular_bit=1, _light_definition_supersize_in_first_person_bit=3,
+ _light_definition_is_first_person_flashlight_bit=4 };
+struct light_datum {
+ unsigned flags; long definition_index,object_index,parent_light_index,rasterizer_light_index; short function_index;
+ real_point3d position; real_vector3d forward,up; real radius;
+};
+struct point_light_definition {
+ unsigned flags; real radius,radius_modifier_upper_bound,cutoff_angle,lens_flare_radius;
+ real runtime_cosine_cutoff_angle,runtime_sine_cutoff_angle,specular_radius_multiplier;
+};
+static struct light_datum fixture_lights[4];
+static struct point_light_definition fixture_light_definition;
+static struct { short scene_point_light_count; long scene_point_lights[MAXIMUM_RENDERED_LIGHTS]; } lights_globals;
+static int light_data=1,marked[4],cluster_reads,diffuse_calls,specular_calls,diffuse_skip,specular_skip,data_next_calls;
+static real_point3d diffuse_center,specular_center;
+static real diffuse_radius,specular_radius;
+static real fixture_light_intensity;
+static boolean object_get_function_value(long object_index,short function_index,real *value) {
+ CHECK(object_index==0 && function_index==0); *value=fixture_light_intensity; return TRUE;
+}
+static struct light_datum *light_get(long index) { CHECK(index>=0 && index<4); return &fixture_lights[index]; }
+static struct point_light_definition *light_definition_get(long index) { CHECK(index==0); return &fixture_light_definition; }
+static long data_next_index(int data,long index) { CHECK(data==light_data); data_next_calls++; return index<3 ? index+1 : NONE; }
+static boolean light_unmarked(long index) { CHECK(index>=0 && index<4); return !marked[index]; }
+static boolean light_mark(long index) { CHECK(index>=0 && index<4); marked[index]=1; return TRUE; }
+static boolean should_render_lights(void) { return TRUE; }
+static short light_build_cluster_array(long index,short maximum,short *clusters) {
+ CHECK(index>=0 && index<4 && maximum==MAXIMUM_CLUSTERS_PER_LIGHT); cluster_reads++; clusters[0]=17; return 1;
+}
+static void rasterizer_environment_diffuse_lights_begin(void) { }
+static void rasterizer_environment_diffuse_lights_end(void) { }
+static void rasterizer_environment_specular_lights_begin(void) { }
+static void rasterizer_environment_specular_lights_end(void) { }
+static void structure_render_diffuse_light(long index,real_point3d const *center,real radius,short count,short const *clusters) {
+ CHECK(index==3); diffuse_calls++; diffuse_center=*center; diffuse_radius=radius;
+ diffuse_skip=clusters==NULL; CHECK(count==(diffuse_skip ? 0 : 1));
+}
+static void structure_render_specular_light(long index,real_point3d const *center,real radius,short count,short const *clusters) {
+ CHECK(index==3); specular_calls++; specular_center=*center; specular_radius=radius;
+ specular_skip=clusters==NULL; CHECK(count==(specular_skip ? 0 : 1));
+}
+@@FLASHLIGHT_FUNCTIONS@@
+static void flashlight_reset(void) {
+ int index;
+ reset(); inventory[0]=NONE; fixture_weapon_count[0]=0;
+ unit_definitions[0].unit.flags=1u<<_unit_fires_from_camera_bit;
+ vr_render.weapon_camera_set=TRUE; vr_render.weapon_camera_valid=TRUE;
+ vr_render.weapon_camera.position=(real_point3d){3,4,0.4f};
+ vr_render.weapon_camera.up=(real_vector3d){0,0,1};
+ render.local_player_index=0;
+ memset(fixture_lights,0,sizeof(fixture_lights)); memset(marked,0,sizeof(marked));
+ memset(&lights_globals,0,sizeof(lights_globals));
+ memset(&fixture_light_definition,0,sizeof(fixture_light_definition));
+ fixture_light_definition.flags=1u<<_light_definition_is_first_person_flashlight_bit;
+ fixture_light_definition.radius=5; fixture_light_definition.radius_modifier_upper_bound=2;
+ fixture_light_definition.cutoff_angle=0.3f;
+ fixture_light_definition.runtime_cosine_cutoff_angle=cosf(0.3f);
+ fixture_light_definition.runtime_sine_cutoff_angle=sinf(0.3f);
+ fixture_light_definition.specular_radius_multiplier=1.2f;
+ for(index=0;index<4;index++) {
+ fixture_lights[index].flags=1u<<_point_light_dynamic_bit;
+ fixture_lights[index].object_index=index ? 1 : 0;
+ fixture_lights[index].parent_light_index=NONE;
+ fixture_lights[index].rasterizer_light_index=3;
+ fixture_lights[index].radius=5;
+ fixture_lights[index].position=(real_point3d){7,8,9};
+ fixture_lights[index].forward=(real_vector3d){1,0,0};
+ fixture_lights[index].up=(real_vector3d){0,0,1};
+ }
+ cluster_reads=diffuse_calls=specular_calls=diffuse_skip=specular_skip=0;
+ data_next_calls=0;
+ fixture_light_intensity=1;
+}
+static void check_flashlight_render_refresh(void) {
+ int frame;
+ real_point3d position,expected_position,ignored_origin;
+ real_vector3d forward,up,expected_forward;
+ struct light_datum tick;
+ flashlight_reset(); tick=fixture_lights[0];
+ /* Ninety rendered poses with a completely unchanged simulation unit/light. */
+ for(frame=0;frame<90;frame++) {
+ real angle=(real)frame*0.025f, pitch=sinf(angle)*0.7f;
+ vr_render.base_yaw=0.5f;
+ vr_render.view.hand_forward[0]=cosf(angle)*cosf(pitch);
+ vr_render.view.hand_forward[1]=sinf(angle)*cosf(pitch);
+ vr_render.view.hand_forward[2]=sinf(pitch);
+ vr_render.weapon_camera.position.z=(real)frame*0.003f;
+ expected_position=vr_render.weapon_camera.position;
+ CHECK(vr_render_unarmed_ray(0,0,&units[0],&vr_render.view,vr_render.base_yaw,&ignored_origin,&expected_forward));
+ CHECK(vr_render_player_flashlight_pose(0,&position,&forward,&up));
+ point_equal(position,expected_position); direction_equal(forward,expected_forward);
+ CHECK(near(dot_product3d(&forward,&up),0)); CHECK(near(magnitude3d(&up),1));
+ CHECK(memcmp(&tick,&fixture_lights[0],sizeof(tick))==0);
+ direction_equal(units[0].unit.aiming_vector,(real_vector3d){1,0,0});
+ /* The second eye queries identical centre-frame presentation, not a new locate. */
+ CHECK(vr_render_player_flashlight_pose(0,&position,&forward,&up));
+ point_equal(position,expected_position); direction_equal(forward,expected_forward);
+ }
+ /* Hand/controller height must not be replaced by the observer/HMD origin. */
+ fixture_observer.position.z=12; vr_render.weapon_camera.position.z=-0.2f;
+ CHECK(vr_render_player_flashlight_pose(0,&position,&forward,&up)); CHECK(near(position.z,-0.2f));
+ vr_render.view.hand_forward[0]=0; vr_render.view.hand_forward[1]=0; vr_render.view.hand_forward[2]=1;
+ CHECK(vr_render_player_flashlight_pose(0,&position,&forward,&up));
+ CHECK(near(forward.k,sinf(DEGREES_TO_RADIANS(85.f)))); CHECK(near(dot_product3d(&forward,&up),0));
+ vr_render.view.hand_forward[2]=-1;
+ CHECK(vr_render_player_flashlight_pose(0,&position,&forward,&up)); CHECK(near(forward.k,-sinf(DEGREES_TO_RADIANS(85.f))));
+ vr_render.view.hand_forward[0]=1; vr_render.view.hand_forward[2]=0; vr_render.base_yaw=0;
+ vr_render.weapon_camera.up=(real_vector3d){0,1,0};
+ CHECK(vr_render_player_flashlight_pose(0,&position,&forward,&up)); direction_equal(up,(real_vector3d){0,1,0});
+}
+static void check_flashlight_rejection(void) {
+ int scenario;
+ for(scenario=0;scenario<20;scenario++) {
+ real_point3d position={11,12,13}; real_vector3d forward={14,15,16},up={17,18,19};
+ flashlight_reset();
+ switch(scenario) {
+ case 0: vr_render.active=FALSE; break;
+ case 1: vr_render.weapon_camera_set=FALSE; break;
+ case 2: vr_render.weapon_camera_valid=FALSE; break;
+ case 3: fixture_vr_aiming=FALSE; break;
+ case 4: fixture_tracking_valid=FALSE; break;
+ case 5: render.local_player_index=1; break;
+ case 6: fixture_weapon_count[0]=1; break;
+ case 7: inventory[0]=0; break;
+ case 8: units[0].object.damage_flags=1u<<_object_dead_bit; break;
+ case 9: units[0].object.parent_object_index=1; break;
+ case 10: units[0].unit.gunner_object_index=1; break;
+ case 11: fixture_cinematic=TRUE; break;
+ case 12: fixture_inhibited=TRUE; break;
+ case 13: fixture_perspective=0; break;
+ case 14: vr_render.weapon_camera.position.z=NAN; break;
+ case 15: vr_render.weapon_camera.up.i=NAN; break;
+ case 16: vr_render.weapon_camera.up=(real_vector3d){0,1,0}; break; /* parallel to default pointing */
+ case 17: vr_render.view.hand_forward[1]=NAN; break;
+ case 18: fixture_local_player=NONE; break;
+ case 19: fixture_scripted_camera=TRUE; break;
+ }
+ CHECK(!vr_render_player_flashlight_pose(0,&position,&forward,&up));
+ point_equal(position,(real_point3d){11,12,13}); direction_equal(forward,(real_vector3d){14,15,16});
+ direction_equal(up,(real_vector3d){17,18,19});
+ lights_vr_include_unarmed_flashlights(); CHECK(data_next_calls==0);
+ }
+ flashlight_reset(); {
+ real_point3d position; real_vector3d forward,up;
+ CHECK(!vr_render_player_flashlight_pose(1,&position,&forward,&up));
+ CHECK(!vr_render_player_flashlight_pose(NONE,&position,&forward,&up));
+ CHECK(!vr_render_player_flashlight_pose(0,NULL,&forward,&up));
+ CHECK(!vr_render_player_flashlight_pose(0,&position,NULL,&up));
+ CHECK(!vr_render_player_flashlight_pose(0,&position,&forward,NULL));
+ }
+}
+static void check_flashlight_bounds_and_clusters(void) {
+ int scenario;
+ flashlight_reset(); {
+ struct light_datum tick=fixture_lights[0];
+ real_point3d center,tick_center,position; real_vector3d forward,up; real radius,tick_radius;
+ /* Tick cone points right; current rendered controller points left. */
+ vr_render.view.hand_forward[0]=-1; vr_render.view.hand_forward[1]=0;
+ light_get_bounding_sphere(0,&center,&radius);
+ light_compute_bounding_sphere(0,TRUE,FALSE,TRUE,&tick_center,&tick_radius);
+ CHECK(center.x<vr_render.weapon_camera.position.x); CHECK(tick_center.x>tick.position.x);
+ CHECK(near(radius,tick_radius)); CHECK(memcmp(&tick,&fixture_lights[0],sizeof(tick))==0);
+ CHECK(light_vr_unarmed_flashlight_pose(&fixture_lights[0],&position,&forward,&up));
+ CHECK(near(center.x,position.x+radius*forward.i));
+ /* Missing from stale clusters: included once without partition/datum edits. */
+ lights_vr_include_unarmed_flashlights(); CHECK(lights_globals.scene_point_light_count==1);
+ CHECK(data_next_calls>0);
+ CHECK(lights_globals.scene_point_lights[0]==0);
+ lights_vr_include_unarmed_flashlights(); CHECK(lights_globals.scene_point_light_count==1);
+ lights_render_diffuse(); lights_render_specular();
+ CHECK(diffuse_calls==1 && specular_calls==1 && diffuse_skip && specular_skip && cluster_reads==0);
+ CHECK(near(diffuse_center.x,position.x+diffuse_radius*forward.i));
+ CHECK(near(specular_center.x,position.x+specular_radius*forward.i));
+ CHECK(memcmp(&tick,&fixture_lights[0],sizeof(tick))==0);
+ }
+ /* Full visibility budget stays bounded and keeps the player's flashlight. */
+ flashlight_reset(); lights_globals.scene_point_light_count=MAXIMUM_RENDERED_LIGHTS;
+ lights_globals.scene_point_lights[0]=2; lights_globals.scene_point_lights[1]=3;
+ marked[2]=marked[3]=1; lights_vr_include_unarmed_flashlights();
+ CHECK(lights_globals.scene_point_light_count==MAXIMUM_RENDERED_LIGHTS);
+ CHECK(lights_globals.scene_point_lights[0]==2 && lights_globals.scene_point_lights[1]==0);
+ flashlight_reset(); fixture_light_intensity=0;
+ lights_globals.scene_point_light_count=MAXIMUM_RENDERED_LIGHTS;
+ lights_globals.scene_point_lights[0]=2; lights_globals.scene_point_lights[1]=3;
+ marked[2]=marked[3]=1; lights_vr_include_unarmed_flashlights();
+ CHECK(lights_globals.scene_point_light_count==MAXIMUM_RENDERED_LIGHTS);
+ CHECK(lights_globals.scene_point_lights[0]==2 && lights_globals.scene_point_lights[1]==3);
+ for(scenario=0;scenario<6;scenario++) {
+ real_point3d rendered,tick; real radius,tick_radius;
+ flashlight_reset();
+ switch(scenario) {
+ case 0: fixture_light_definition.flags=0; break; /* ordinary dynamic light */
+ case 1: fixture_lights[0].parent_light_index=5; break; /* transient effect */
+ case 2: fixture_lights[0].flags=0; break; /* static light */
+ case 3: inventory[0]=0; fixture_weapon_count[0]=1; break; /* armed marker path */
+ case 4: vr_render.active=FALSE; break; /* flat/HUD/ended pass */
+ case 5: fixture_lights[0].object_index=1; break; /* remote owner */
+ }
+ light_get_bounding_sphere(0,&rendered,&radius);
+ light_compute_bounding_sphere(0,TRUE,FALSE,TRUE,&tick,&tick_radius);
+ point_equal(rendered,tick); CHECK(near(radius,tick_radius));
+ CHECK(!light_render_skips_clusters(&fixture_lights[0]));
+ lights_vr_include_unarmed_flashlights(); CHECK(lights_globals.scene_point_light_count==0);
+ if(scenario==3 || scenario==4) CHECK(data_next_calls==0);
+ }
+ /* Armed supersize convention remains exactly as before. */
+ flashlight_reset(); inventory[0]=0; fixture_weapon_count[0]=1;
+ fixture_lights[0].flags|=1u<<_point_light_attached_to_first_person_weapon_bit;
+ fixture_light_definition.flags|=1u<<_light_definition_supersize_in_first_person_bit;
+ CHECK(light_render_skips_clusters(&fixture_lights[0]));
+}
+'''
+
+
+def main():
+ render = (ROOT / 'port/linux/game/vr_render.c').read_text()
+ lights = (ROOT / 'source/objects/object_lights.c').read_text()
+ first_person = (ROOT / 'source/interface/first_person_weapons.c').read_text()
+ old_first_person = subprocess.check_output(['git', 'show', reticle.ORIGINAL_ENGINE_COMMIT +
+ ':source/interface/first_person_weapons.c'], cwd=ROOT, text=True)
+ assert reticle.function(first_person, 'first_person_weapon_center_flashlight') == reticle.function(old_first_person, 'first_person_weapon_center_flashlight'), 'Armed marker behavior changed'
+ old_lights = subprocess.check_output(['git', 'show', reticle.ORIGINAL_ENGINE_COMMIT +
+ ':source/objects/object_lights.c'], cwd=ROOT, text=True)
+ # Preserve recovered cone math and its simulation-only wrapper exactly.
+ old_body = reticle.function(old_lights, 'light_compute_bounding_sphere')
+ old_body = old_body[old_body.index('{'):].replace('struct light_datum *light = light_get(light_index);', '')
+ new_body = reticle.function(lights, 'light_compute_bounding_sphere_from_basis')
+ assert reticle.normalized(old_body) == reticle.normalized(new_body[new_body.index('{'):])
+ reconnect = reticle.function(lights, 'light_reconnect_to_map')
+ assert 'light_compute_render_bounding_sphere' not in reconnect and 'light_compute_bounding_sphere(' in reconnect
+ preprocess = reticle.function(lights, 'lights_preprocess_scene')
+ assert preprocess.index('lights_vr_include_unarmed_flashlights();') < preprocess.index('light_marker_end();')
+ assert preprocess.index('light_vr_unarmed_flashlight_pose(light, &light_parameters.position,') < preprocess.index('rasterizer_light_submit(')
+ helper = reticle.function(render, 'vr_render_player_flashlight_pose')
+ assert 'halo_vr_view(' not in helper and 'weapon_camera.position' in helper
+ names = ['light_vr_unarmed_flashlight_pose', 'lights_vr_include_unarmed_flashlights',
+ 'light_compute_bounding_sphere_from_basis', 'light_compute_bounding_sphere',
+ 'light_compute_render_bounding_sphere', 'light_render_skips_clusters',
+ 'light_get_bounding_sphere', 'lights_render_diffuse', 'lights_render_specular']
+ production = helper + '\n\n' + '\n\n'.join(reticle.function(lights, name) for name in names)
+ extension = LIGHT_FIXTURE.replace('@@FLASHLIGHT_FUNCTIONS@@', production)
+ harness = reticle.HARNESS
+ view_header = (ROOT / 'port/linux/src/vr.h').read_text()
+ view_begin = view_header.index('struct halo_vr_view\n{')
+ view_end = reticle.block_end(view_header, view_header.index('{', view_begin))
+ harness = re.sub(r'struct halo_vr_view \{[^\n]+\};',
+ lambda match: view_header[view_begin:view_end] + ';', harness)
+ harness = harness.replace('boolean active,crosshairs_drawn;',
+ 'boolean active,crosshairs_drawn,weapon_camera_set,weapon_camera_valid; struct render_camera weapon_camera;')
+ harness = harness.replace('static struct { struct render_camera camera; } render;',
+ 'static struct { struct render_camera camera; short local_player_index; } render;')
+ harness = harness.replace('int main(void) {', extension + '\nint main(void) {\n' +
+ ' check_flashlight_render_refresh(); check_flashlight_rejection(); check_flashlight_bounds_and_clusters();')
+ reticle.HARNESS = harness
+ reticle.main()
+ print('PASS: actual render-rate flashlight pose, unchanged armed/tick math and read-only visibility/diffuse/specular bounds')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_locomotion.py b/tools/test_vr_locomotion.py
new file mode 100644
index 0000000000000000000000000000000000000000..84926978f6b60e17d53891f4ce6df113833000bc
--- /dev/null
+++ b/tools/test_vr_locomotion.py
@@ -0,0 +1,440 @@
+#!/usr/bin/env python3
+"""Compile and run the production VR walking math and integration on a host.
+
+No headset, game data, SDL or OpenXR is required. Uses cc/clang/gcc on POSIX
+or Visual Studio's C11 compiler on Windows; --cc and --vcvarsall can override.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+import textwrap
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def masked(text):
+ return re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"',
+ lambda match: re.sub(r'[^\n]', ' ', match.group()), text, flags=re.S)
+
+
+def function(text, name):
+ match = re.search(r'(?:static\s+)?(?:void|float|int)\s+' + re.escape(name) +
+ r'\s*\([^;]*?\)\s*\{', text)
+ if not match:
+ raise AssertionError(f"Missing production function: {name}")
+ code = masked(text)
+ depth = 1
+ end = code.index('{', match.start()) + 1
+ while depth:
+ depth += (code[end] == '{') - (code[end] == '}')
+ end += 1
+ return text[match.start():end]
+
+
+def source_checks(vr, player):
+ baseline = subprocess.check_output(['git', 'show', '2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4:port/linux/src/vr.c'],
+ cwd=ROOT, text=True)
+ assert function(vr, 'halo_vr_aim') == function(baseline, 'halo_vr_aim'), 'Weapon aim changed'
+ assert function(vr, 'weapon_pointing') == function(baseline, 'weapon_pointing'), 'Long-gun aim changed'
+ old_pad = masked(function(baseline, 'halo_vr_gamepad'))
+ new_pad = masked(function(vr, 'halo_vr_gamepad'))
+ old_pad = old_pad.replace('XINPUT_GAMEPAD_WHITE, buttons & VR_BUTTON_LEFT_BUMPER',
+ 'XINPUT_GAMEPAD_WHITE, buttons & VR_BUTTON_RIGHT_BUMPER')
+ old_pad = old_pad.replace('XINPUT_GAMEPAD_BLACK, buttons & VR_BUTTON_RIGHT_BUMPER',
+ 'XINPUT_GAMEPAD_BLACK, buttons & VR_BUTTON_LEFT_BUMPER')
+ assert re.sub(r'\s+', '', old_pad) == re.sub(r'\s+', '', new_pad), 'Xbox controls changed beyond bumper fix'
+ config = (ROOT / 'port/linux/src/port_config.c').read_text()
+ assert re.search(r'"vr\.movement",\s*_config_string,\s*"\\"head\\"",\s*"HALO_VR_MOVEMENT"', config)
+ hook_start = player.index('if (local_player_index == 0 && unit->object.parent_object_index == NONE &&')
+ hook_open = player.index('{', hook_start)
+ hook_end = player.index('}', hook_open) + 1
+ hook = player[hook_start:hook_end]
+ assert '!director_inhibited_facing(local_player_index)' in hook and '!cinematic_in_progress()' in hook
+ assert 'halo_vr_locomotion(player->desired_angles.yaw, &input.throttle.i, &input.throttle.j)' in hook
+ assert player.index('player->desired_angles.yaw = vr_yaw;') < hook_start
+ assert hook_end < player.index('player->throttle = input.throttle;') < player.index('update_client_queue(&action);')
+ guard = re.search(r'if \(vr_aim == 1 && \(!isfinite\(vr_yaw\) \|\| !isfinite\(vr_pitch\)\)\)\s*vr_aim = 0;', player)
+ assert guard and guard.start() < player.index('while (vr_yaw < 0.f)')
+ # Include the production statement's leading indent before dedenting.
+ # The extracted if body must retain its relative indent in generated C.
+ guard_line_start = player.rfind('\n', 0, guard.start()) + 1
+ guard_source = player[guard_line_start:guard.end()].expandtabs(4)
+ return hook, textwrap.indent(textwrap.dedent(guard_source), ' ')
+
+
+HARNESS = r'''
+#define _USE_MATH_DEFINES
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "vr_host.h"
+#include "vr_locomotion.h"
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+#define NONE (-1)
+#define FLAG(bit) (1UL << (bit))
+#define TEST_FLAG(value, bit) (((value) & FLAG(bit)) != 0)
+#define EPSILON 0.00005f
+static int checks;
+#define CHECK(condition) do { checks++; if (!(condition)) { \
+ fprintf(stderr, "failed line %d: %s\n", __LINE__, #condition); exit(1); } } while (0)
+static int close_float(float first, float second) { return fabsf(first - second) <= EPSILON; }
+static struct {
+ int initialized, frame, pending, origin_valid, aim_synced, menus, aiming, long_gun;
+ struct vr_host_views views;
+ float origin_yaw, body_yaw, origin_position[3];
+ struct halo_vr_locomotion_heading movement_heading;
+ int tutorial_head_valid, tutorial_turn_flags, turn_control_allowed;
+ float tutorial_head_yaw, tutorial_head_pitch;
+} vr;
+static struct {
+ int head_movement, controller_aim, gamepad_aim, gamepad_snap, standing;
+ float snap_angle, player_height;
+} settings;
+static void settings_read(void) {}
+static void platform_log(const char *format, const char *height) { (void)format; (void)height; }
+static int weapon_calls;
+/* The production aiming function is unchanged; the host stub supplies the
+controller pose that the unmodified long-gun/weapon_pointing code computes. */
+static void weapon_pointing(float point[3], float up[3]);
+@@FUNCTIONS@@
+static void weapon_pointing(float point[3], float up[3]) {
+ static const float forward[3] = { 0.0f, 0.0f, -1.0f };
+ static const float upwards[3] = { 0.0f, 1.0f, 0.0f };
+ weapon_calls++;
+ quaternion_rotate(vr.views.aim[1].orientation, forward, point);
+ quaternion_rotate(vr.views.aim[1].orientation, upwards, up);
+}
+typedef struct { float yaw, pitch; } real_euler_angles2d;
+typedef struct { float i, j; } real_vector2d;
+typedef float real;
+enum {
+ _unit_control_action_bit, _unit_control_jump_bit, _unit_control_throw_grenade_bit,
+ _player_control_action_bit, _player_control_jump_bit, _player_control_grenade_trigger_bit,
+ _player_control_primary_trigger_bit, _player_control_look_relative_up_bit,
+ _player_control_look_relative_down_bit, _player_control_look_relative_left_bit,
+ _player_control_look_relative_right_bit, _player_control_move_relative_forward_bit,
+ _player_control_move_relative_backward_bit, _player_control_move_relative_right_bit,
+ _player_control_move_relative_left_bit
+};
+struct player_control_globals_data { unsigned long action_flags; } tutorial;
+static struct player_control_globals_data *player_control_globals = &tutorial;
+@@TUTORIAL@@
+static int local_player_index, inhibited, cinematic, dispatch_calls;
+static struct { struct { long parent_object_index; } object; } unit_storage, *unit = &unit_storage;
+static struct { real_euler_angles2d desired_angles; } player_storage, *player = &player_storage;
+static struct { real_vector2d throttle; } input;
+static int director_inhibited_facing(int index) { (void)index; return inhibited; }
+static int cinematic_in_progress(void) { return cinematic; }
+static void record_locomotion(float yaw, float *forward, float *left) {
+ dispatch_calls++;
+ halo_vr_locomotion(yaw, forward, left);
+}
+#define halo_vr_locomotion record_locomotion
+static void dispatch(void) { @@HOOK@@ }
+#undef halo_vr_locomotion
+static int guard_aim(float vr_yaw, float vr_pitch, int vr_aim) {
+@@AIM_GUARD@@
+ return vr_aim;
+}
+
+static void head_pose(float yaw, float pitch) {
+ float cy = cosf(yaw * 0.5f), sy = sinf(yaw * 0.5f);
+ float cp = cosf(pitch * 0.5f), sp = sinf(pitch * 0.5f);
+ vr.views.head.orientation[0] = cy * sp;
+ vr.views.head.orientation[1] = sy * cp;
+ vr.views.head.orientation[2] = -sy * sp;
+ vr.views.head.orientation[3] = cy * cp;
+}
+static void reset_state(void) {
+ memset(&vr, 0, sizeof(vr)); memset(&settings, 0, sizeof(settings));
+ vr.initialized = vr.frame = vr.origin_valid = vr.aim_synced = 1;
+ vr.views.should_render = vr.views.focused = vr.views.head.valid = 1;
+ vr.turn_control_allowed = 1;
+ vr.views.eye[0].valid = vr.views.eye[1].valid = 1;
+ vr.views.eye[0].orientation[3] = vr.views.eye[1].orientation[3] = 1.0f;
+ settings.head_movement = settings.controller_aim = 1;
+ settings.player_height = 1.7f;
+ head_pose(0.0f, 0.0f);
+ tutorial.action_flags = 0;
+}
+static void expect_walk(float aim, float forward, float left, float expected_forward, float expected_left) {
+ halo_vr_locomotion(aim, &forward, &left);
+ CHECK(close_float(forward, expected_forward)); CHECK(close_float(left, expected_left));
+}
+static void check_directions(void) {
+ const float half_pi = (float)M_PI * 0.5f;
+ float angle, aim, forward, left, expected_x, expected_y, actual_x, actual_y;
+ int head_step, aim_step;
+ reset_state(); head_pose(half_pi, 0.0f);
+ expect_walk(0.0f, 1.0f, 0.0f, 0.0f, 1.0f);
+ expect_walk(0.0f, 0.0f, 1.0f, -1.0f, 0.0f);
+ reset_state(); expect_walk(half_pi, 1.0f, 0.0f, 0.0f, -1.0f);
+ /* Compare actual world-space motion, not a second copy of rotation:
+ engine throttle uses weapon facing. Independently form head forward
+ and head left vectors and compare those with engine-applied throttle. */
+ for (head_step = -24; head_step <= 24; head_step++) {
+ angle = (float)head_step * 0.19f;
+ for (aim_step = -24; aim_step <= 24; aim_step++) {
+ aim = (float)aim_step * 0.27f;
+ reset_state(); vr.body_yaw = 0.43f; vr.origin_yaw = -0.31f;
+ head_pose(angle, 0.75f);
+ forward = 0.6f; left = 0.8f;
+ halo_vr_locomotion(aim, &forward, &left);
+ expected_x = 0.6f * cosf(angle + 0.74f) - 0.8f * sinf(angle + 0.74f);
+ expected_y = 0.6f * sinf(angle + 0.74f) + 0.8f * cosf(angle + 0.74f);
+ actual_x = forward * cosf(aim) - left * sinf(aim);
+ actual_y = forward * sinf(aim) + left * cosf(aim);
+ CHECK(close_float(actual_x, expected_x)); CHECK(close_float(actual_y, expected_y));
+ CHECK(close_float(forward * forward + left * left, 1.0f));
+ }
+ }
+ reset_state(); head_pose(0.7f, 0.0f);
+ expect_walk(12.0f * (float)M_PI + 0.7f, -0.6f, 0.4f, -0.6f, 0.4f);
+ vr.body_yaw = half_pi; head_pose(0.0f, 0.0f);
+ expect_walk(0.0f, 1.0f, 0.0f, 0.0f, 1.0f); /* snap turn */
+ vr.body_yaw = 0.25f;
+ expect_walk(0.0f, 1.0f, 0.0f, cosf(0.25f), sinf(0.25f)); /* smooth turn */
+}
+static void check_pitch_and_recentering(void) {
+ float before_forward = 1.0f, before_left = 0.0f;
+ reset_state(); head_pose(0.9f, 0.0f);
+ halo_vr_locomotion(0.2f, &before_forward, &before_left);
+ head_pose(-2.0f, (float)M_PI * 0.5f);
+ expect_walk(0.2f, 1.0f, 0.0f, before_forward, before_left);
+ /* Body turns still move the cached vertical heading; weapon turns do
+ not redirect walking. Recenter adjusts body/origin by opposite amounts. */
+ vr.body_yaw += 0.3f;
+ expect_walk(0.5f, 1.0f, 0.0f, before_forward, before_left);
+ reset_state(); head_pose(2.0f, -(float)M_PI * 0.5f);
+ expect_walk(0.0f, 1.0f, 0.0f, 0.0f, 0.0f);
+ CHECK(!vr.movement_heading.valid);
+ reset_state(); vr.origin_yaw = -0.4f; vr.body_yaw = 0.2f; head_pose(0.7f, 0.0f);
+ before_forward = 1.0f; before_left = 0.0f;
+ halo_vr_locomotion(0.1f, &before_forward, &before_left);
+ recentre(); CHECK(!vr.movement_heading.valid);
+ expect_walk(0.1f, 1.0f, 0.0f, before_forward, before_left);
+ halo_vr_aim_release(); CHECK(!vr.movement_heading.valid);
+}
+static void check_tracking_guards(void) {
+ float forward, left, normalized[4], pose[4] = { 0.0f, 0.0f, 0.0f, 1.01f };
+ int flag;
+ for (flag = 0; flag < 8; flag++) {
+ reset_state(); head_pose(0.9f, 0.0f);
+ forward = 1.0f; left = 0.0f; halo_vr_locomotion(0.0f, &forward, &left);
+ CHECK(vr.movement_heading.valid);
+ switch (flag) {
+ case 0: vr.views.head.valid = 0; break;
+ case 1: vr.views.focused = 0; break;
+ case 2: vr.views.should_render = 0; break;
+ case 3: vr.frame = vr.pending = 0; break;
+ case 4: vr.origin_valid = 0; break;
+ case 5: vr.aim_synced = 0; break;
+ case 6: vr.menus = 1; break;
+ case 7: vr.origin_yaw = NAN; break;
+ }
+ expect_walk(0.0f, 1.0f, 0.4f, 0.0f, 0.0f); CHECK(!vr.movement_heading.valid);
+ vr.views.head.valid = vr.views.focused = vr.views.should_render = vr.frame = 1;
+ vr.origin_valid = vr.aim_synced = 1; vr.menus = 0; vr.origin_yaw = 0.0f;
+ head_pose(-1.0f, (float)M_PI * 0.5f);
+ expect_walk(0.0f, 1.0f, 0.0f, 0.0f, 0.0f); /* no stale yaw after lost tracking */
+ }
+ reset_state(); vr.frame = 0; vr.pending = 1;
+ expect_walk(0.0f, 0.6f, 0.8f, 0.6f, 0.8f);
+ reset_state(); vr.views.head.orientation[1] = NAN;
+ expect_walk(0.0f, 1.0f, 0.2f, 0.0f, 0.0f);
+ reset_state(); vr.views.head.orientation[3] = INFINITY;
+ expect_walk(0.0f, 1.0f, 0.2f, 0.0f, 0.0f);
+ reset_state(); memset(vr.views.head.orientation, 0, sizeof(vr.views.head.orientation));
+ expect_walk(0.0f, 1.0f, 0.2f, 0.0f, 0.0f);
+ reset_state(); expect_walk(NAN, 1.0f, 0.2f, 0.0f, 0.0f);
+ reset_state(); vr.body_yaw = INFINITY; expect_walk(0.0f, 1.0f, 0.2f, 0.0f, 0.0f);
+ reset_state(); expect_walk(0.0f, NAN, 0.2f, 0.0f, 0.0f);
+ reset_state(); expect_walk(0.0f, 0.2f, INFINITY, 0.0f, 0.0f);
+ CHECK(halo_vr_locomotion_orientation(pose, normalized));
+ CHECK(close_float(normalized[3], 1.0f));
+ pose[3] = 10.0f; CHECK(!halo_vr_locomotion_orientation(pose, normalized));
+ pose[3] = 0.0f; CHECK(!halo_vr_locomotion_orientation(pose, normalized));
+}
+static void check_exclusions_and_aim(void) {
+ float aim, pitch, saved_aim, saved_pitch, forward, left;
+ int exclusion;
+ CHECK(guard_aim(0.3f, -0.4f, 1) == 1);
+ CHECK(guard_aim(INFINITY, 0.0f, 1) == 0);
+ CHECK(guard_aim(-INFINITY, 0.0f, 1) == 0);
+ CHECK(guard_aim(NAN, 0.0f, 1) == 0);
+ CHECK(guard_aim(0.0f, NAN, 1) == 0);
+ CHECK(guard_aim(0.0f, INFINITY, 1) == 0);
+ CHECK(guard_aim(0.3f, -0.4f, 2) == 2);
+ for (exclusion = 0; exclusion < 4; exclusion++) {
+ reset_state(); head_pose(1.2f, 0.0f); vr.views.head.valid = 0;
+ switch (exclusion) {
+ case 0: vr.initialized = 0; break;
+ case 1: settings.head_movement = 0; break;
+ case 2: settings.gamepad_aim = 1; break;
+ case 3: settings.controller_aim = 0; break;
+ }
+ expect_walk(0.0f, 0.4f, -0.7f, 0.4f, -0.7f);
+ }
+ for (exclusion = 0; exclusion < 5; exclusion++) {
+ reset_state(); head_pose((float)M_PI * 0.5f, 0.0f);
+ local_player_index = inhibited = cinematic = dispatch_calls = 0;
+ unit->object.parent_object_index = NONE; player->desired_angles.yaw = 0.0f;
+ input.throttle.i = 1.0f; input.throttle.j = 0.0f;
+ switch (exclusion) {
+ case 1: local_player_index = 1; break;
+ case 2: unit->object.parent_object_index = 42; break;
+ case 3: inhibited = 1; break;
+ case 4: cinematic = 1; break;
+ }
+ dispatch(); CHECK(dispatch_calls == (exclusion == 0));
+ CHECK(close_float(input.throttle.i, exclusion == 0 ? 0.0f : 1.0f));
+ CHECK(close_float(input.throttle.j, exclusion == 0 ? 1.0f : 0.0f));
+ }
+ reset_state(); head_pose(0.8f, 0.1f);
+ memcpy(vr.views.aim[1].orientation, vr.views.head.orientation, sizeof(vr.views.head.orientation));
+ vr.views.aim[1].valid = 1; head_pose(0.0f, 0.0f); weapon_calls = 0;
+ CHECK(halo_vr_aim(0.0f, 1, &aim, &pitch) == 1); CHECK(weapon_calls == 1 && vr.long_gun == 1);
+ saved_aim = aim; saved_pitch = pitch; CHECK(close_float(aim, 0.8f));
+ forward = 1.0f; left = 0.0f; halo_vr_locomotion(aim, &forward, &left);
+ CHECK(aim == saved_aim && pitch == saved_pitch && weapon_calls == 1 && vr.long_gun == 1);
+ CHECK(close_float(forward * cosf(aim) - left * sinf(aim), 1.0f));
+ CHECK(close_float(forward * sinf(aim) + left * cosf(aim), 0.0f));
+}
+static void check_tutorial(void) {
+ const real_euler_angles2d still = { 0.0f, 0.0f };
+ const real_vector2d idle = { 0.0f, 0.0f };
+ const real_vector2d walk = { 1.0f, -1.0f };
+ reset_state();
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(tutorial.action_flags == 0); /* establish physical-head anchor */
+ head_pose(0.005f, 0.0f); player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(tutorial.action_flags == 0); /* jitter rejected */
+ head_pose(0.03f, 0.04f); player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_up_bit));
+ tutorial.action_flags = 0; head_pose(-0.04f, -0.05f);
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_right_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_down_bit));
+ tutorial.action_flags = 0; vr.views.head.valid = 0;
+ player_control_action_test_note(FLAG(_unit_control_jump_bit), &still, &walk, 1.0f);
+ CHECK(!vr.tutorial_head_valid);
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_jump_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_primary_trigger_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_move_relative_forward_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_move_relative_left_bit));
+}
+static void check_tutorial_tracking_recovery(void) {
+ const real_euler_angles2d still = { 0.0f, 0.0f };
+ const real_vector2d idle = { 0.0f, 0.0f };
+ int corruption, component;
+ for (corruption = 0; corruption < 5; corruption++) {
+ reset_state();
+ if (corruption != 0) {
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(vr.tutorial_head_valid);
+ }
+ switch (corruption) {
+ case 0: case 1: vr.views.head.orientation[1] = NAN; break;
+ case 2: vr.views.head.orientation[3] = INFINITY; break;
+ case 3: memset(vr.views.head.orientation, 0, sizeof(vr.views.head.orientation)); break;
+ case 4: vr.views.head.orientation[3] = 10.0f; break;
+ }
+ /* A pose incorrectly marked valid must neither count movement nor
+ poison the next healthy head-motion anchor. */
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(tutorial.action_flags == 0); CHECK(!vr.tutorial_head_valid);
+ head_pose(0.0f, 0.0f);
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(vr.tutorial_head_valid); CHECK(tutorial.action_flags == 0);
+ head_pose(0.03f, 0.04f);
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_up_bit));
+ }
+ reset_state(); player_control_action_test_note(0, &still, &idle, 0.0f);
+ head_pose(0.03f, 0.04f);
+ for (component = 0; component < 4; component++) vr.views.head.orientation[component] *= 1.01f;
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_up_bit));
+ reset_state(); vr.tutorial_head_valid = 1;
+ vr.tutorial_head_yaw = NAN; vr.tutorial_head_pitch = INFINITY;
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(vr.tutorial_head_valid && isfinite(vr.tutorial_head_yaw) && isfinite(vr.tutorial_head_pitch));
+ CHECK(tutorial.action_flags == 0);
+ head_pose(-0.03f, -0.04f);
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_right_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_down_bit));
+}
+int main(void) {
+ check_directions(); check_pitch_and_recentering(); check_tracking_guards();
+ check_exclusions_and_aim(); check_tutorial(); check_tutorial_tracking_recovery();
+ printf("PASS: %d production VR locomotion / input / tutorial assertions\n", checks);
+ return 0;
+}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc', help='C compiler executable (cl, clang, gcc or cc)')
+ parser.add_argument('--vcvarsall', type=Path, help='Visual Studio VC/Auxiliary/Build/vcvarsall.bat')
+ args = parser.parse_args()
+ vr = (ROOT / 'port/linux/src/vr.c').read_text()
+ player = (ROOT / 'source/game/player_control.c').read_text()
+ hook, guard = source_checks(vr, player)
+ functions = '\n\n'.join(function(vr, name) for name in (
+ 'quaternion_rotate', 'quaternion_yaw', 'game_axes', 'recentre',
+ 'halo_vr_locomotion', 'halo_vr_gaze_tracking_valid', 'halo_vr_tutorial_turn',
+ 'halo_vr_tutorial_look', 'halo_vr_aim', 'halo_vr_aim_release'))
+ harness = HARNESS.replace('@@FUNCTIONS@@', functions).replace('@@HOOK@@', hook)
+ harness = harness.replace('@@AIM_GUARD@@', guard)
+ harness = harness.replace('@@TUTORIAL@@', function(player, 'player_control_action_test_note'))
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ installations = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(installations.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1])
+ vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No host C compiler found. Install/select a compiler with --cc.')
+ with tempfile.TemporaryDirectory(prefix='halo-vr-locomotion-') as directory:
+ temp = Path(directory)
+ source = temp / 'test.c'
+ executable = temp / ('test.exe' if os.name == 'nt' else 'test')
+ source.write_text(harness)
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX',
+ '/D_CRT_SECURE_NO_WARNINGS', '/DHALO_VR', '/I' + str(ROOT / 'port/linux/src'),
+ '/Fe:' + str(executable), '/Fo:' + str(temp / 'test.obj'), str(source)]
+ if vcvars:
+ # Initialize only this child shell; never print or save its environment.
+ for path in (vcvars, Path(compiler), temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported shell metacharacter in compiler/temp path.')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\n'
+ 'if errorlevel 1 exit /b 1\n' + subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-DHALO_VR',
+ '-I' + str(ROOT / 'port/linux/src'), str(source), *([] if os.name == 'nt' else ['-lm']), '-o', str(executable)]
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+ print('PASS: production source integration, Xbox mapping and unchanged controller/long-gun aim')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_projectile_reticle.py b/tools/test_vr_projectile_reticle.py
new file mode 100644
index 0000000000000000000000000000000000000000..8725f0da3a55b453f0b4718e5f29235a317b6c6a
--- /dev/null
+++ b/tools/test_vr_projectile_reticle.py
@@ -0,0 +1,777 @@
+#!/usr/bin/env python3
+"""Compile actual nominal firing/preview/reticle functions with a C11 host.
+
+Checks source equivalence to the pinned firing path, camera/trigger offsets,
+read-only aim assist, vehicle collision ignores, unarmed pointing, current hit
+depth and stereo projection. Engine services are deterministic stubs; no headset or maps needed.
+This does not validate random spread, ballistics or GPU/headset presentation.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+
+ROOT = Path(__file__).resolve().parents[1]
+ORIGINAL_ENGINE_COMMIT = '2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4'
+
+
+def function(text, name):
+ match = re.search(r'(?:static\s+)?(?:void|float|real|int|boolean|long|short)\s+' +
+ re.escape(name) + r'\s*\([^;]*?\)\s*\{', text)
+ assert match, name
+ start = text.index('{', match.start())
+ return text[match.start():block_end(text, start)]
+
+
+def block_end(text, start):
+ masked = re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"',
+ lambda item: re.sub(r'[^\n]', ' ', item.group()), text, flags=re.S)
+ end, depth = start + 1, 1
+ while depth:
+ depth += (masked[end] == '{') - (masked[end] == '}')
+ end += 1
+ return end
+
+
+def normalized(text):
+ return re.sub(r'\s+', '', text)
+
+
+def verify_refactors(weapons, aim, render, units):
+ old_weapons = subprocess.check_output(['git', 'show', ORIGINAL_ENGINE_COMMIT + ':source/items/weapons.c'], cwd=ROOT, text=True)
+ old_aim = subprocess.check_output(['git', 'show', ORIGINAL_ENGINE_COMMIT + ':source/game/aim_assist.c'], cwd=ROOT, text=True)
+ old_fire = function(old_weapons, 'trigger_create_projectiles')
+ start = old_fire.index('if (!TEST_FLAG(trigger_definition->flags, _weapon_trigger_projectiles_cannot_be_aimed_bit)')
+ end = block_end(old_fire, old_fire.index('{', start))
+ preparation = old_fire[start:end]
+ # Reinsert the shared call into the full real-shot function: everything
+ # after nominal preparation (spread, placement, ammo and RNG) must match.
+ new_fire = function(weapons, 'trigger_create_projectiles')
+ call = 'target_object_index = trigger_adjust_projectile_ray(owner_object_index, unit, trigger_definition,\n\t\t\t&origin, &forward, &velocity, &error, FALSE, NULL);'
+ assert new_fire.count(call) == 1
+ assert normalized(new_fire.replace(call, preparation)) == normalized(old_fire), 'Real firing behavior changed'
+ shared = function(weapons, 'trigger_adjust_projectile_ray')
+ shared_start = shared.index('if (!TEST_FLAG(trigger_definition->flags, _weapon_trigger_projectiles_cannot_be_aimed_bit)')
+ shared_block = shared[shared_start:block_end(shared, shared.index('{', shared_start))]
+ assert 'player_preview_projectile_aim' in shared_block and 'length <= 0.0f' in shared_block
+ shared_block = re.sub(r'#ifdef HALO_VR\n.*?#endif\n', '', shared_block, flags=re.S)
+ expected = preparation
+ for local in ('origin', 'forward', 'velocity', 'error'):
+ expected = re.sub(r'&' + local + r'\b', local, expected)
+ assert normalized(shared_block) == normalized(expected), 'Shared trigger preparation changed math'
+ old_body = function(old_aim, 'player_aim_projectile')
+ old_body = old_body[old_body.index('{'):]
+ new_body = function(aim, 'player_aim_projectile_internal')
+ new_body = new_body[new_body.index('{'):]
+ new_body = re.sub(r'#ifn?def HALO_VR\n.*?#endif\n', '', new_body, flags=re.S)
+ recording = re.search(r'if \(record_target\)\s*\{([^{}]*)\}', new_body)
+ assert recording and 'aim_assist_unit_index' in recording.group(1) and 'aim_assist_timestamp' in recording.group(1)
+ assert normalized(new_body[:recording.start()] + recording.group(1) + new_body[recording.end():]) == normalized(old_body), 'Shared aim math changed'
+ assert 'TRUE' in function(aim, 'player_aim_projectile') and 'FALSE' in function(aim, 'player_preview_projectile_aim')
+ old_units = subprocess.check_output(['git', 'show', ORIGINAL_ENGINE_COMMIT + ':source/units/units.c'], cwd=ROOT, text=True)
+ original_unit = function(old_units, 'unit_adjust_projectile_ray')
+ new_unit = function(units, 'unit_adjust_projectile_ray_internal')
+ new_unit = new_unit[new_unit.index('{'):]
+ new_unit = new_unit.replace('preview_forward ? *preview_forward : unit->unit.aiming_vector', 'unit->unit.aiming_vector')
+ new_unit = new_unit.replace('if (preview_camera)\n\t\t\tcamera_position = *preview_camera;\n\t\telse\n\t\t\tunit_get_camera_position(unit_index, &camera_position);',
+ 'unit_get_camera_position(unit_index, &camera_position);')
+ assert normalized(new_unit) == normalized(original_unit[original_unit.index('{'):]), 'Unit NULL firing math changed'
+ assert 'NULL, NULL' in function(units, 'unit_adjust_projectile_ray')
+ assert function(units, 'unit_clip_to_aiming_bounds') == function(old_units, 'unit_clip_to_aiming_bounds')
+ pose_builder = function(render, 'vr_render_pointing_pose')
+ assert 'render_interpolation_camera(0, observer_get_camera(0))' in pose_builder
+ assert 'hand_position' not in pose_builder and 'base_position' not in pose_builder and 'player_effect_get_camera_effect_matrix' not in pose_builder
+ assert 'DEGREES_TO_RADIANS(85.0f)' in pose_builder and 'unit_clip_to_aiming_bounds' in pose_builder
+ assert 'vr_render_pointing_pose(player_index, unit, &vr_render.view, vr_render.base_yaw, pose)' in function(render, 'vr_render_projectile_pose')
+ preview = function(weapons, 'weapon_preview_projectile_ray')
+ assert 'object_get_ultimate_parent(weapon_get_projectile_owner_object_index(weapon_index))' in preview
+ assert 'MAXIMUM_NUMBER_OF_TRIGGERS_PER_WEAPON' in preview
+ hud = function(render, 'vr_render_hud_camera')
+ assert 'hand_forward' not in hud and 'render_camera_world_to_screen' not in hud and '100.0f' not in hud, 'Unsupported preview retains fake hand trajectory'
+ point = function(render, 'vr_render_projectile_point')
+ assert point.index('MAXIMUM_COLLISION_USER_STACK_DEPTH - 2') < point.index('vr_render_projectile_ray(')
+ assert '_collision_test_for_projectiles_flags' in point and '*point = collision.point;' in point
+ crosshairs = function(render, 'vr_render_crosshairs')
+ assert crosshairs.count('vr_render_projectile_point(') == 1
+ assert 'CROSSHAIR_SMOOTHING' not in render and 'crosshair_inverse_distance' not in render
+ print('PASS: pinned unit/firing/aim NULL math identical; preview/HUD/collision contracts')
+
+
+HARNESS = r'''
+#include <math.h>
+#include <float.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+typedef float real;
+typedef int boolean;
+typedef struct { real x,y,z; } real_point3d;
+typedef struct { real i,j,k; } real_vector3d;
+typedef struct { short x0,y0,x1,y1; } rectangle2d;
+#define TRUE 1
+#define FALSE 0
+#define NONE -1
+#define PIN(v,l,h) ((v)<(l)?(l):(v)>(h)?(h):(v))
+#define DEGREES_TO_RADIANS(v) ((v)*0.017453292519943295f)
+#define TEST_FLAG(value, bit) (((value) & (1u << (bit))) != 0)
+#define MAXIMUM_NUMBER_OF_TRIGGERS_PER_WEAPON 2
+#define MAXIMUM_COLLISION_USER_STACK_DEPTH 8
+#define CROSSHAIR_FARTHEST 100.0f
+enum { _object_dead_bit, _object_invisible_bit, _unit_fires_from_camera_bit,
+ _weapon_trigger_projectiles_cannot_be_aimed_bit, _weapon_trigger_uses_weapon_origin_bit };
+enum { _object_type_biped=0, _object_type_vehicle=1, _director_perspective_first_person=1 };
+enum { _object_mask_unit=3, _director_perspective_scripted=2, _director_perspective_neutral=3 };
+typedef int director_perspective;
+enum { _collision_user_ui, _collision_user_aim_assist };
+enum { _collision_test_for_projectiles_flags = 31, _collision_test_for_line_of_sight_flags = 7, _collision_result_object = 1 };
+static int checks;
+#define CHECK(c) do { checks++; if (!(c)) { fprintf(stderr,"failed line %d: %s\n",__LINE__,#c); exit(1); } } while (0)
+#define match_assert(file,line,c) CHECK(c)
+#define match_assert_valid_real_normal3d(file,line,v) CHECK(isfinite((v)->i) && isfinite((v)->j) && isfinite((v)->k))
+static int near(real a, real b) { return fabsf(a-b)<0.002f; }
+static void point_equal(real_point3d a, real_point3d b) { CHECK(near(a.x,b.x)); CHECK(near(a.y,b.y)); CHECK(near(a.z,b.z)); }
+static void direction_equal(real_vector3d a, real_vector3d b) { CHECK(near(a.i,b.i)); CHECK(near(a.j,b.j)); CHECK(near(a.k,b.k)); }
+struct object_data { int type; unsigned flags,damage_flags; long parent_object_index; real_point3d position; };
+struct object_datum { struct object_data object; };
+struct unit_datum { long definition_index; struct object_data object; struct { long player_index,actor_index,gunner_object_index; short current_weapon_index; real_vector3d aiming_vector; } unit; };
+struct weapon_datum { long definition_index; struct object_data object; };
+struct player_datum { long unit_index,aim_assist_unit_index,aim_assist_timestamp; short team_index; };
+struct weapon_trigger_definition { unsigned flags; real_point3d first_person_weapon_offset; };
+struct trigger_block { short count; struct weapon_trigger_definition data[2]; };
+struct weapon_definition { struct { struct trigger_block triggers; } weapon; };
+struct unit_definition { struct { unsigned flags; real aiming_velocity_maximum,aiming_acceleration_maximum; } unit; };
+struct projectile_preview_pose { real_point3d camera_position; real_vector3d forward; };
+struct observer_result { real_point3d position; real_vector3d forward; };
+typedef struct { real yaw,pitch; } real_euler_angles2d;
+struct object_marker { struct { real_point3d position; real_vector3d forward; } matrix; };
+struct aim_assist_parameters { real deviation_angle; };
+struct aim_assist_target { long object_index; real_point3d position; real autoaim_level; };
+struct collision_result { real_point3d point; int type; long object_index; };
+struct render_frustum { struct { real n[4][3]; } world_to_view; real projection_matrix[4][4]; };
+struct render_camera { real_point3d position; real_vector3d forward,up; real vertical_field_of_view; rectangle2d viewport_bounds,window_bounds; };
+struct halo_vr_view { real body_yaw,hud_tangent,world_units_per_metre; boolean controller_aim,gamepad_aim,gamepad_pitch,vehicle_first_person; real head_forward[3],head_up[3],head_position[3],hand_forward[3]; float fov[2][4]; };
+static struct { real base_yaw; boolean active,crosshairs_drawn; struct halo_vr_view view; struct render_frustum eyes[2]; } vr_render;
+static struct { struct render_camera camera; } render;
+static short vr_render_crosshair_offset[2];
+static struct unit_datum units[4];
+static struct object_datum fixture_objects[4];
+static struct weapon_datum weapons[2];
+static struct player_datum players[1];
+static struct weapon_definition definitions[1];
+static struct unit_definition unit_definitions[1];
+static struct object_marker fixture_marker;
+static real_point3d unit_camera, deterministic_camera, assist_target, fixture_assist_camera;
+static real_vector3d fixture_assist_forward;
+static real_point3d fixture_aim_trace_origin;
+static real_vector3d deterministic_direction, fixture_velocity;
+static real fixture_autoaim_level, deviation_angle, wall_x;
+static int unit_valid[4], weapon_valid[2], inventory[4], fixture_weapon_count[4], fixture_aiming_unit[4];
+static int params_enabled, assist_enabled, actor_calls, time_calls, marker_count, marker_object;
+static int collision_calls, aim_calls, los_calls, max_depth, last_flags;
+static long last_ignore;
+static int global_current_collision_user_depth, global_current_collision_users[MAXIMUM_COLLISION_USER_STACK_DEPTH];
+static real_vector3d up_vector={0,0,1}, left_vector={0,1,0};
+static real_vector3d const *global_up3d=&up_vector,*global_left3d=&left_vector;
+#define TAG_BLOCK_GET_ELEMENT(block,index,type) (&(block)->data[(index)])
+static struct unit_datum *unit_try_and_get(long index) { return index>=0 && index<4 && unit_valid[index] ? &units[index] : NULL; }
+static struct unit_datum *unit_get(long index) { CHECK(unit_try_and_get(index)!=NULL); return &units[index]; }
+static struct object_datum *object_get(long index) { CHECK(unit_try_and_get(index)!=NULL); fixture_objects[index].object=units[index].object; return &fixture_objects[index]; }
+static struct weapon_datum *weapon_try_and_get(long index) { return index>=0 && index<2 && weapon_valid[index] ? &weapons[index] : NULL; }
+static struct weapon_datum *weapon_get(long index) { CHECK(weapon_try_and_get(index)!=NULL); return &weapons[index]; }
+static struct player_datum *player_get(long index) { CHECK(index==0); return &players[0]; }
+static struct weapon_definition *weapon_definition_get(long index) { CHECK(index==0); return &definitions[0]; }
+static struct unit_definition *unit_definition_get(long index) { CHECK(index==0); return &unit_definitions[0]; }
+static long unit_inventory_get_weapon(long index,short slot) { (void)slot; CHECK(index>=0 && index<4); return inventory[index]; }
+static short unit_get_weapon_count(long index) { CHECK(index>=0 && index<4); return (short)fixture_weapon_count[index]; }
+static long unit_get_aiming_unit_index(long index) { CHECK(index>=0 && index<4); return fixture_aiming_unit[index]; }
+static short unit_get_zoom_level(long index) { (void)index; return 0; }
+static int actor_firing_blindly(long index) { (void)index; return FALSE; }
+static long actor_aim_projectile(long actor,real_point3d *origin,real_vector3d *forward,real *error) { (void)actor; (void)origin; (void)forward; (void)error; actor_calls++; return NONE; }
+static long game_time_get(void) { time_calls++; return 1234; }
+static long object_get_ultimate_parent(long index) { int steps=0; while (index>=0 && index<4 && units[index].object.parent_object_index!=NONE) { CHECK(++steps<4); index=units[index].object.parent_object_index; } return index; }
+static short object_get_marker_by_name(long object,char const *name,struct object_marker *result,short maximum) { CHECK(maximum==1); CHECK(strcmp(name,"primary trigger")==0 || strcmp(name,"secondary trigger")==0); marker_object=(int)object; *result=fixture_marker; return (short)marker_count; }
+static void unit_get_camera_position(long index,real_point3d *result) { (void)index; *result=unit_camera; }
+static void object_get_velocities(long index,real_vector3d *velocity,void *unused) { (void)index; (void)unused; *velocity=fixture_velocity; }
+static real dot_product3d(real_vector3d const *a,real_vector3d const *b) { return a->i*b->i+a->j*b->j+a->k*b->k; }
+static void vector_from_points3d(real_point3d const *a,real_point3d const *b,real_vector3d *r) { r->i=b->x-a->x; r->j=b->y-a->y; r->k=b->z-a->z; }
+static real magnitude3d(real_vector3d const *v) { return sqrtf(dot_product3d(v,v)); }
+static real normalize3d(real_vector3d *v) { real length=magnitude3d(v); if(length>0) { v->i/=length; v->j/=length; v->k/=length; } return length; }
+static void fast_normalize3d(real_vector3d *v) { normalize3d(v); }
+static void scale_vector3d(real_vector3d const *v,real amount,real_vector3d *r) { r->i=v->i*amount; r->j=v->j*amount; r->k=v->k*amount; }
+static void point_from_line3d(real_point3d const *o,real_vector3d const *v,real t,real_point3d *r) { r->x=o->x+v->i*t; r->y=o->y+v->j*t; r->z=o->z+v->k*t; }
+static void set_real_point3d(real_point3d *r,real x,real y,real z) { r->x=x; r->y=y; r->z=z; }
+static void cross_product3d(real_vector3d const *a,real_vector3d const *b,real_vector3d *r) { real_vector3d v={a->j*b->k-a->k*b->j,a->k*b->i-a->i*b->k,a->i*b->j-a->j*b->i}; *r=v; }
+static void perpendicular3d(real_vector3d const *v,real_vector3d *r) { real_vector3d basis=fabsf(v->i)<0.8f ? (real_vector3d){1,0,0} : (real_vector3d){0,1,0}; cross_product3d(v,&basis,r); }
+static void rotate_vector_about_axis(real_vector3d *v,real_vector3d const *axis,real sine,real cosine) { real_vector3d cross; real dot=dot_product3d(axis,v); cross_product3d(axis,v,&cross); v->i=v->i*cosine+cross.i*sine+axis->i*dot*(1-cosine); v->j=v->j*cosine+cross.j*sine+axis->j*dot*(1-cosine); v->k=v->k*cosine+cross.k*sine+axis->k*dot*(1-cosine); }
+static real sine(real angle) { return sinf(angle); }
+static real cosine(real angle) { return cosf(angle); }
+static boolean unit_get_aim_assist_parameters(long unit,short zoom,struct aim_assist_parameters *p) { (void)unit; (void)zoom; p->deviation_angle=deviation_angle; return params_enabled; }
+static void director_camera_deterministic(long unit,real_point3d *position,real_vector3d *direction) { (void)unit; *position=deterministic_camera; *direction=deterministic_direction; }
+static boolean collision_test_vector(int flags,real_point3d const *origin,real_vector3d const *vector,long ignore,struct collision_result *collision) {
+ real t;
+ CHECK(global_current_collision_user_depth>0 && global_current_collision_user_depth<=MAXIMUM_COLLISION_USER_STACK_DEPTH);
+ if(global_current_collision_user_depth>max_depth) max_depth=global_current_collision_user_depth;
+ collision_calls++; last_flags=flags; last_ignore=ignore;
+ if(flags==_collision_test_for_projectiles_flags && global_current_collision_users[global_current_collision_user_depth-1]==_collision_user_aim_assist)
+ fixture_aim_trace_origin=*origin;
+ point_from_line3d(origin,vector,1,&collision->point); collision->type=0; collision->object_index=NONE;
+ t=vector->i!=0 ? (wall_x-origin->x)/vector->i : -1;
+ if(t>=0 && t<=1) { point_from_line3d(origin,vector,t,&collision->point); return TRUE; }
+ return FALSE;
+}
+@@LOS@@
+static boolean aim_assist(struct aim_assist_parameters const *parameters,real_point3d const *position,real_vector3d const *direction,long ignore,short team,struct aim_assist_target *target) {
+ (void)parameters; (void)team; aim_calls++; fixture_assist_camera=*position; fixture_assist_forward=*direction;
+ if(!assist_enabled) return FALSE;
+ /* Exercise the production nested LOS bracket used by actual autoaim. */
+ los_calls++; (void)aim_assist_clear_line_of_sight(position,&assist_target,ignore,3);
+ target->position=assist_target; target->object_index=3; target->autoaim_level=fixture_autoaim_level; return TRUE;
+}
+@@MATH@@
+@@AIM@@
+@@UNIT@@
+@@WEAPON_HELPERS@@
+@@PREVIEW@@
+static struct observer_result fixture_observer;
+static real_point3d fixture_head_position;
+static real_euler_angles2d fixture_facing_angles;
+static boolean fixture_scripted_camera;
+static boolean *director_camera_scripted=&fixture_scripted_camera;
+static int fixture_perspective,fixture_inhibited,fixture_observer_valid,fixture_clip_calls;
+static real fixture_clip_pitch;
+static real_vector3d fixture_facing;
+static int director_get_perspective(short local) { (void)local; return fixture_perspective; }
+static int director_inhibited_facing(short local) { (void)local; return fixture_inhibited; }
+static struct observer_result const *observer_get_camera(short local) { (void)local; return fixture_observer_valid ? &fixture_observer : NULL; }
+static struct observer_result const *render_interpolation_camera(short local,struct observer_result const *observer) { (void)local; return observer; }
+static real_vector3d *player_control_get_facing_direction(short local,real_vector3d *direction) { (void)local; *direction=fixture_facing; return direction; }
+static real_euler_angles2d const *player_control_get_facing_angles(short local) { (void)local; return &fixture_facing_angles; }
+static void unit_get_head_position(long index,real_point3d *position) { CHECK(index==0); *position=fixture_head_position; }
+static boolean unit_clip_to_aiming_bounds(long index,real_vector3d *direction,boolean aiming) {
+ real yaw,pitch,horizontal;
+ CHECK(index==0 && aiming); fixture_clip_calls++;
+ if(fixture_clip_pitch<=0) return FALSE;
+ yaw=atan2f(direction->j,direction->i); pitch=asinf(PIN(direction->k,-1,1));
+ pitch=PIN(pitch,-fixture_clip_pitch,fixture_clip_pitch); horizontal=cosf(pitch);
+ *direction=(real_vector3d){horizontal*cosf(yaw),horizontal*sinf(yaw),sinf(pitch)}; return TRUE;
+}
+static int drawn_eye,draw_count;
+static float draw_corrections[2][4],drawn_points[2][2];
+static boolean halo_vr_frame_active(void) { return vr_render.active; }
+static int fixture_vr_aiming,fixture_cinematic;
+static long fixture_local_player;
+static struct halo_vr_view fixture_live_view;
+static int fixture_live_view_valid,fixture_tracking_valid;
+static boolean halo_vr_aiming(void) { return fixture_vr_aiming; }
+static boolean cinematic_in_progress(void) { return fixture_cinematic; }
+static long local_player_get_player_index(short local) { (void)local; return fixture_local_player; }
+static int halo_vr_view(struct halo_vr_view *view) { if(!fixture_live_view_valid) return FALSE; *view=fixture_live_view; return TRUE; }
+static int halo_vr_gaze_tracking_valid(void) { return fixture_tracking_valid; }
+static real view_pitch(void) { return 0; }
+static void halo_vr_set_screen_corrections(float value[2][4]) { memcpy(draw_corrections,value,sizeof(draw_corrections)); }
+static void halo_vr_draw_eye(int eye) { drawn_eye=eye; }
+static void hud_draw_vr_crosshairs(void) { CHECK(drawn_eye==0 || drawn_eye==1); drawn_points[drawn_eye][0]=draw_corrections[drawn_eye][0]*320+draw_corrections[drawn_eye][2]; drawn_points[drawn_eye][1]=draw_corrections[drawn_eye][1]*240+draw_corrections[drawn_eye][3]; draw_count++; }
+static void vr_render_screen_point_end(void) { }
+@@RENDER@@
+static void reset(void) {
+ int i;
+ memset(units,0,sizeof(units)); memset(weapons,0,sizeof(weapons)); memset(definitions,0,sizeof(definitions));
+ memset(players,0,sizeof(players)); memset(unit_definitions,0,sizeof(unit_definitions)); memset(&vr_render,0,sizeof(vr_render));
+ memset(&render,0,sizeof(render));
+ for(i=0;i<4;i++) { unit_valid[i]=1; units[i].object.parent_object_index=NONE; units[i].unit.player_index=NONE; units[i].unit.actor_index=NONE; units[i].unit.gunner_object_index=NONE; inventory[i]=NONE; fixture_weapon_count[i]=0; fixture_aiming_unit[i]=i; }
+ for(i=0;i<2;i++) { weapon_valid[i]=1; weapons[i].object.parent_object_index=NONE; }
+ units[0].unit.player_index=0; units[0].unit.aiming_vector=(real_vector3d){1,0,0}; inventory[0]=0; fixture_weapon_count[0]=1;
+ players[0].unit_index=0; players[0].aim_assist_unit_index=41; players[0].aim_assist_timestamp=42;
+ weapons[0].object.parent_object_index=0; definitions[0].weapon.triggers.count=1;
+ fixture_marker.matrix.position=(real_point3d){2,0.7f,0.3f}; fixture_marker.matrix.forward=(real_vector3d){0.97f,0.12f,-0.08f}; normalize3d(&fixture_marker.matrix.forward);
+ unit_camera=(real_point3d){0,0,0.5f}; deterministic_camera=unit_camera; deterministic_direction=(real_vector3d){1,0,0};
+ fixture_velocity=(real_vector3d){0.3f,0.4f,0.2f}; assist_target=(real_point3d){20,0.1f,0.6f}; fixture_autoaim_level=0.65f; deviation_angle=1;
+ params_enabled=assist_enabled=actor_calls=time_calls=collision_calls=aim_calls=los_calls=max_depth=0;
+ marker_count=1; marker_object=NONE; wall_x=20; last_ignore=NONE; last_flags=0;
+ global_current_collision_user_depth=1; memset(global_current_collision_users,0,sizeof(global_current_collision_users));
+ vr_render.active=TRUE; vr_render.view.controller_aim=TRUE; vr_render.view.hud_tangent=1;
+ vr_render.view.head_forward[0]=1; vr_render.view.head_up[2]=1;
+ vr_render.view.hand_forward[1]=1; /* Deliberately very different from actual unit firing. */
+ render.camera.forward=(real_vector3d){1,0,0}; render.camera.viewport_bounds=(rectangle2d){0,0,640,480}; render.camera.window_bounds=render.camera.viewport_bounds;
+ for(i=0;i<2;i++) {
+ struct render_frustum *eye=&vr_render.eyes[i];
+ eye->world_to_view.n[0][2]=1; eye->world_to_view.n[1][0]=-1; eye->world_to_view.n[2][1]=1;
+ eye->world_to_view.n[3][0]=i==0 ? -0.035f : 0.035f;
+ eye->projection_matrix[0][0]=1; eye->projection_matrix[1][1]=1; eye->projection_matrix[2][2]=1; eye->projection_matrix[2][3]=1;
+ vr_render.view.fov[i][0]=-1; vr_render.view.fov[i][1]=1; vr_render.view.fov[i][2]=1; vr_render.view.fov[i][3]=-1;
+ }
+ fixture_observer.position=unit_camera; fixture_facing=units[0].unit.aiming_vector;
+ fixture_observer.forward=(real_vector3d){1,0,0}; fixture_head_position=(real_point3d){3,4,5};
+ fixture_facing_angles=(real_euler_angles2d){0,0}; fixture_scripted_camera=FALSE;
+ vr_render.view.world_units_per_metre=2;
+ fixture_perspective=_director_perspective_first_person; fixture_inhibited=0; fixture_observer_valid=1;
+ fixture_clip_pitch=0; fixture_clip_calls=0;
+ fixture_vr_aiming=TRUE; fixture_cinematic=FALSE; fixture_local_player=0;
+ fixture_live_view=vr_render.view; fixture_live_view_valid=TRUE; fixture_tracking_valid=TRUE;
+ draw_count=0; drawn_eye=-1; vr_render_crosshair_offset[0]=vr_render_crosshair_offset[1]=0;
+}
+static void check_ray_equivalence(void) {
+ int camera,offset,assist,origin_flag,orientation;
+ real_vector3d orientations[]={{1,0,0},{0.9f,0.2f,0.35f},{0,0,1},{0,0,-1}};
+ for(camera=0;camera<2;camera++) for(offset=0;offset<2;offset++) for(assist=0;assist<3;assist++)
+ for(origin_flag=0;origin_flag<2;origin_flag++) for(orientation=0;orientation<4;orientation++) {
+ real_point3d preview_origin,fire_origin; real_vector3d preview_direction,fire_direction;
+ real velocity=0,error=0; long ignore,target; struct weapon_trigger_definition *trigger;
+ reset(); units[0].unit.aiming_vector=orientations[orientation]; normalize3d(&units[0].unit.aiming_vector);
+ deterministic_direction=units[0].unit.aiming_vector; unit_definitions[0].unit.flags=camera ? 1u<<_unit_fires_from_camera_bit : 0;
+ params_enabled=assist>0; assist_enabled=assist>1;
+ trigger=&definitions[0].weapon.triggers.data[0]; trigger->first_person_weapon_offset=offset ? (real_point3d){0.2f,-0.15f,0.1f} : (real_point3d){0,0,0};
+ trigger->flags=origin_flag ? 1u<<_weapon_trigger_uses_weapon_origin_bit : 0;
+ CHECK(weapon_preview_projectile_ray(0,0,0,&preview_origin,&preview_direction,&ignore,NULL));
+ CHECK(ignore==0); CHECK(players[0].aim_assist_unit_index==41 && players[0].aim_assist_timestamp==42);
+ CHECK(time_calls==0 && actor_calls==0 && global_current_collision_user_depth==1);
+ fire_origin=fixture_marker.matrix.position; fire_direction=fixture_marker.matrix.forward;
+ target=trigger_adjust_projectile_ray(0,&units[0],trigger,&fire_origin,&fire_direction,&velocity,&error,FALSE,NULL);
+ if(origin_flag) fire_origin=fixture_marker.matrix.position;
+ normalize3d(&fire_direction);
+ point_equal(preview_origin,fire_origin); direction_equal(preview_direction,fire_direction);
+ CHECK(players[0].aim_assist_unit_index==target); CHECK(time_calls==1); CHECK(global_current_collision_user_depth==1);
+ if(assist>1) CHECK(max_depth==3); /* base1 + aim + LOS */
+ }
+ reset(); marker_count=0; CHECK(weapon_preview_projectile_ray(0,0,0,&unit_camera,&deterministic_direction,&last_ignore,NULL)); CHECK(marker_object==0);
+ reset(); weapons[0].object.flags=1u<<_object_invisible_bit; CHECK(weapon_preview_projectile_ray(0,0,0,&unit_camera,&deterministic_direction,&last_ignore,NULL)); CHECK(marker_object==0);
+}
+static void check_vehicles_and_guards(void) {
+ real_point3d point,origin; real_vector3d direction; long ignore;
+ reset(); units[0].object.parent_object_index=1; CHECK(vr_render_projectile_point(0,&point)); CHECK(last_ignore==1);
+ CHECK(weapon_preview_projectile_ray(0,0,0,&origin,&direction,&ignore,NULL)); CHECK(ignore==1);
+ reset(); inventory[0]=NONE; inventory[1]=0; fixture_aiming_unit[0]=1; weapons[0].object.parent_object_index=1; units[1].unit.gunner_object_index=0; units[0].object.parent_object_index=1;
+ CHECK(vr_render_projectile_point(0,&point)); CHECK(last_ignore==1);
+ direction_equal(fixture_marker.matrix.forward,(real_vector3d){(point.x-fixture_marker.matrix.position.x)/magnitude3d(&(real_vector3d){point.x-fixture_marker.matrix.position.x,point.y-fixture_marker.matrix.position.y,point.z-fixture_marker.matrix.position.z}),
+ (point.y-fixture_marker.matrix.position.y)/magnitude3d(&(real_vector3d){point.x-fixture_marker.matrix.position.x,point.y-fixture_marker.matrix.position.y,point.z-fixture_marker.matrix.position.z}),
+ (point.z-fixture_marker.matrix.position.z)/magnitude3d(&(real_vector3d){point.x-fixture_marker.matrix.position.x,point.y-fixture_marker.matrix.position.y,point.z-fixture_marker.matrix.position.z})});
+ CHECK(actor_calls==0 && time_calls==0);
+ reset(); CHECK(!weapon_preview_projectile_ray(NONE,0,0,&origin,&direction,&ignore,NULL));
+ CHECK(!weapon_preview_projectile_ray(0,-1,0,&origin,&direction,&ignore,NULL)); CHECK(!weapon_preview_projectile_ray(0,2,0,&origin,&direction,&ignore,NULL));
+ definitions[0].weapon.triggers.count=0; CHECK(!weapon_preview_projectile_ray(0,0,0,&origin,&direction,&ignore,NULL));
+ reset(); unit_valid[0]=0; CHECK(!vr_render_projectile_point(0,&point)); CHECK(global_current_collision_user_depth==1);
+ reset(); units[0].object.damage_flags=1u<<_object_dead_bit; CHECK(!vr_render_projectile_point(0,&point));
+ reset(); units[0].unit.player_index=1; CHECK(!vr_render_projectile_point(0,&point)); CHECK(actor_calls==0 && collision_calls==0);
+ reset(); fixture_marker.matrix.position.y=NAN; CHECK(!vr_render_projectile_point(0,&point)); CHECK(global_current_collision_user_depth==1);
+ reset(); fixture_marker.matrix.forward=(real_vector3d){0,0,0}; definitions[0].weapon.triggers.data[0].flags=1u<<_weapon_trigger_projectiles_cannot_be_aimed_bit; CHECK(!vr_render_projectile_point(0,&point));
+ reset(); params_enabled=assist_enabled=TRUE; fixture_marker.matrix.position.x=NAN;
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(aim_calls==0 && time_calls==0 && global_current_collision_user_depth==1);
+ CHECK(players[0].aim_assist_unit_index==41 && players[0].aim_assist_timestamp==42);
+ reset(); params_enabled=assist_enabled=TRUE; units[0].unit.aiming_vector.i=NAN;
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(aim_calls==0 && time_calls==0 && global_current_collision_user_depth==1);
+ reset(); params_enabled=assist_enabled=TRUE; units[0].unit.aiming_vector=(real_vector3d){FLT_MAX,FLT_MAX,0};
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(aim_calls==0 && time_calls==0 && global_current_collision_user_depth==1);
+ reset(); params_enabled=assist_enabled=TRUE; definitions[0].weapon.triggers.data[0].flags=1u<<_weapon_trigger_uses_weapon_origin_bit;
+ definitions[0].weapon.triggers.data[0].first_person_weapon_offset.x=NAN;
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(aim_calls==0 && time_calls==0 && global_current_collision_user_depth==1);
+ reset(); params_enabled=assist_enabled=TRUE; definitions[0].weapon.triggers.data[0].flags=1u<<_weapon_trigger_projectiles_cannot_be_aimed_bit;
+ fixture_marker.matrix.forward=(real_vector3d){FLT_MAX,FLT_MAX,0};
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(aim_calls==0 && time_calls==0 && global_current_collision_user_depth==1);
+
+ reset(); units[0].unit.gunner_object_index=3; unit_valid[3]=0; CHECK(!vr_render_projectile_point(0,&point));
+ reset(); weapons[0].object.parent_object_index=NONE; CHECK(!vr_render_projectile_point(0,&point));
+ reset(); inventory[0]=NONE; CHECK(!vr_render_projectile_point(0,&point)); vr_render_crosshairs(); CHECK(draw_count==0 && !vr_render.crosshairs_drawn);
+ CHECK(vr_render_hud_camera(&render.camera)); CHECK(vr_render_crosshair_offset[0]==0 && vr_render_crosshair_offset[1]==0);
+ reset(); params_enabled=assist_enabled=TRUE; global_current_collision_user_depth=MAXIMUM_COLLISION_USER_STACK_DEPTH-3;
+ CHECK(vr_render_projectile_point(0,&point)); CHECK(max_depth==MAXIMUM_COLLISION_USER_STACK_DEPTH); CHECK(global_current_collision_user_depth==MAXIMUM_COLLISION_USER_STACK_DEPTH-3);
+ reset(); params_enabled=assist_enabled=TRUE; global_current_collision_user_depth=MAXIMUM_COLLISION_USER_STACK_DEPTH-2;
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(aim_calls==0 && collision_calls==0 && time_calls==0); CHECK(global_current_collision_user_depth==MAXIMUM_COLLISION_USER_STACK_DEPTH-2);
+}
+static void check_collision_and_projection(void) {
+ real_point3d point; real walls[]={3,50,4,80}; int wall,eye;
+ reset(); params_enabled=assist_enabled=TRUE;
+ for(wall=0;wall<4;wall++) {
+ int previous_aim=aim_calls;
+ wall_x=walls[wall]; CHECK(vr_render_projectile_point(0,&point)); CHECK(near(point.x,wall_x));
+ CHECK(last_flags==_collision_test_for_projectiles_flags && last_ignore==0); CHECK(global_current_collision_user_depth==1);
+ draw_count=0; vr_render_crosshairs(); CHECK(draw_count==2 && drawn_eye==-1); CHECK(aim_calls-previous_aim==2); /* one explicit point + one draw query */
+ for(eye=0;eye<2;eye++) {
+ real eye_y=eye==0 ? -0.035f : 0.035f;
+ CHECK(near(drawn_points[eye][0], ((-(point.y-eye_y)/point.x+1)*640-1)*0.5f));
+ CHECK(near(drawn_points[eye][1], ((1-point.z/point.x)*480-1)*0.5f));
+ }
+ CHECK(vr_render.crosshairs_drawn); CHECK(vr_render_hud_camera(&render.camera)); CHECK(vr_render_crosshair_offset[0]==10000 && !vr_render.crosshairs_drawn);
+ CHECK(players[0].aim_assist_unit_index==41 && players[0].aim_assist_timestamp==42 && time_calls==0);
+ }
+ /* A moved/asymmetric eye camera changes only where the same hit projects. */
+ reset(); CHECK(vr_render_projectile_point(0,&point)); vr_render.eyes[0].world_to_view.n[3][0]=-0.2f;
+ vr_render.eyes[0].projection_matrix[2][0]=0.12f; vr_render_crosshairs();
+ CHECK(near(drawn_points[0][0], ((-(point.y+0.2f)/point.x+0.12f+1)*640-1)*0.5f));
+ CHECK(near(point.x,wall_x));
+ reset(); wall_x=1000; CHECK(vr_render_projectile_point(0,&point)); CHECK(near(point.x,fixture_marker.matrix.position.x+100));
+}
+static void check_render_pose_anchors(void) {
+ int assist,weapon_origin;
+ for(assist=0;assist<3;assist++) for(weapon_origin=0;weapon_origin<2;weapon_origin++) {
+ struct projectile_preview_pose pose;
+ real_point3d old_origin,new_origin; real_vector3d old_direction,new_direction; long old_ignore,new_ignore;
+ reset(); unit_definitions[0].unit.flags=1u<<_unit_fires_from_camera_bit;
+ params_enabled=assist>0; assist_enabled=assist>1;
+ definitions[0].weapon.triggers.data[0].first_person_weapon_offset=(real_point3d){0.2f,-0.15f,0.1f};
+ definitions[0].weapon.triggers.data[0].flags=weapon_origin ? 1u<<_weapon_trigger_uses_weapon_origin_bit : 0;
+ pose.camera_position=unit_camera; pose.forward=units[0].unit.aiming_vector;
+ CHECK(weapon_preview_projectile_ray(0,0,0,&old_origin,&old_direction,&old_ignore,NULL));
+ CHECK(weapon_preview_projectile_ray(0,0,0,&new_origin,&new_direction,&new_ignore,&pose));
+ point_equal(old_origin,new_origin); direction_equal(old_direction,new_direction); CHECK(old_ignore==new_ignore);
+ CHECK(time_calls==0 && actor_calls==0 && global_current_collision_user_depth==1);
+ CHECK(players[0].aim_assist_unit_index==41 && players[0].aim_assist_timestamp==42);
+ }
+}
+static void check_render_frame_refresh(void) {
+ struct unit_datum simulation;
+ struct projectile_preview_pose pose;
+ real_point3d point;
+ real last_y=-1000;
+ int frame;
+ reset(); unit_definitions[0].unit.flags=1u<<_unit_fires_from_camera_bit; params_enabled=TRUE;
+ simulation=units[0]; render.camera.position=(real_point3d){9,9,9}; /* Presentation shake must not become a shot origin. */
+ for(frame=0;frame<12;frame++) {
+ real yaw=-0.25f+0.04f*(real)frame;
+ int old_aim=aim_calls;
+ /* A fixed 30 Hz simulation sample, twelve distinct displayed poses. */
+ vr_render.view.hand_forward[0]=cosf(yaw); vr_render.view.hand_forward[1]=sinf(yaw); vr_render.view.hand_forward[2]=0;
+ fixture_observer.position=(real_point3d){0.01f*(real)frame,0.02f*(real)frame,0.5f};
+ CHECK(vr_render_projectile_pose(0,&units[0],&pose));
+ direction_equal(pose.forward,(real_vector3d){cosf(yaw),sinf(yaw),0});
+ point_equal(pose.camera_position,fixture_observer.position);
+ CHECK(vr_render_projectile_point(0,&point)); CHECK(aim_calls-old_aim==1);
+ CHECK(near(point.x,wall_x)); CHECK(near(point.y,fixture_observer.position.y+(wall_x-fixture_observer.position.x)*tanf(yaw)));
+ CHECK(point.y>last_y); last_y=point.y;
+ point_equal(fixture_assist_camera,fixture_observer.position); direction_equal(fixture_assist_forward,pose.forward);
+ /* The paired render camera moves, but the original deterministic
+ camera-height displacement must not acquire a horizontal lag. */
+ point_equal(fixture_aim_trace_origin,(real_point3d){fixture_observer.position.x+pose.forward.i*0.5f,
+ fixture_observer.position.y+pose.forward.j*0.5f,fixture_observer.position.z+pose.forward.k*0.5f});
+ CHECK(memcmp(&units[0],&simulation,sizeof(simulation))==0);
+ CHECK(time_calls==0 && actor_calls==0 && global_current_collision_user_depth==1);
+ CHECK(players[0].aim_assist_unit_index==41 && players[0].aim_assist_timestamp==42);
+ old_aim=aim_calls; draw_count=0; vr_render_crosshairs(); CHECK(aim_calls-old_aim==1 && draw_count==2);
+ CHECK(near(drawn_points[0][0], ((-(point.y+0.035f)/point.x+1)*640-1)*0.5f));
+ CHECK(near(drawn_points[1][0], ((-(point.y-0.035f)/point.x+1)*640-1)*0.5f));
+ }
+ /* Gamepad facing also changes every rendered frame without changing unit aim. */
+ reset(); unit_definitions[0].unit.flags=1u<<_unit_fires_from_camera_bit;
+ vr_render.view.controller_aim=FALSE; vr_render.view.gamepad_aim=TRUE;
+ for(frame=0;frame<6;frame++) {
+ real yaw=0.1f*(real)frame;
+ fixture_facing=(real_vector3d){cosf(yaw),sinf(yaw),0};
+ CHECK(vr_render_projectile_pose(0,&units[0],&pose)); direction_equal(pose.forward,fixture_facing);
+ CHECK(vr_render_projectile_point(0,&point)); CHECK(near(point.y,wall_x*tanf(yaw)));
+ direction_equal(units[0].unit.aiming_vector,(real_vector3d){1,0,0});
+ }
+}
+static void unarmed(void) {
+ reset(); inventory[0]=NONE; fixture_weapon_count[0]=0;
+ unit_definitions[0].unit.flags=1u<<_unit_fires_from_camera_bit;
+ vr_render.view.hand_forward[0]=1; vr_render.view.hand_forward[1]=0;
+ fixture_live_view=vr_render.view;
+}
+static void check_unarmed_refresh_and_transition(void) {
+ struct unit_datum simulation;
+ struct player_datum player_state;
+ real_point3d origin,point,armed_origin; real_vector3d direction,armed_direction;
+ struct projectile_preview_pose pose;
+ long ignore,armed_ignore; int frame;
+ unarmed(); simulation=units[0]; player_state=players[0];
+ /* No weapon, trigger or projectile metadata is available. */
+ weapon_valid[0]=FALSE; definitions[0].weapon.triggers.count=0;
+ params_enabled=assist_enabled=TRUE;
+ render.camera.position=(real_point3d){9,9,9};
+ for(frame=0;frame<12;frame++) {
+ real yaw=-0.25f+0.04f*(real)frame;
+ int previous_collision;
+ vr_render.view.hand_forward[0]=cosf(yaw); vr_render.view.hand_forward[1]=sinf(yaw);
+ fixture_observer.position=(real_point3d){0.01f*(real)frame,0.02f*(real)frame,0.5f};
+ CHECK(vr_render_projectile_ray(0,&origin,&direction,&ignore));
+ point_equal(origin,fixture_observer.position);
+ direction_equal(direction,(real_vector3d){cosf(yaw),sinf(yaw),0}); CHECK(ignore==0);
+ wall_x=frame%2 ? 40.0f : 4.0f;
+ CHECK(vr_render_projectile_point(0,&point)); CHECK(near(point.x,wall_x));
+ CHECK(near(point.y,origin.y+(wall_x-origin.x)*tanf(yaw)));
+ CHECK(last_ignore==0 && last_flags==_collision_test_for_projectiles_flags);
+ previous_collision=collision_calls; draw_count=0; vr_render_crosshairs();
+ CHECK(collision_calls==previous_collision+1 && draw_count==2 && vr_render.crosshairs_drawn);
+ CHECK(near(drawn_points[0][0], ((-(point.y+0.035f)/point.x+1)*640-1)*0.5f));
+ CHECK(near(drawn_points[1][0], ((-(point.y-0.035f)/point.x+1)*640-1)*0.5f));
+ CHECK(vr_render_hud_camera(&render.camera));
+ CHECK(vr_render_crosshair_offset[0]==10000 && !vr_render.crosshairs_drawn);
+ CHECK(aim_calls==0 && marker_object==NONE && actor_calls==0 && time_calls==0);
+ CHECK(global_current_collision_user_depth==1);
+ CHECK(memcmp(&units[0],&simulation,sizeof(simulation))==0);
+ CHECK(memcmp(&players[0],&player_state,sizeof(player_state))==0);
+ }
+ /* Acquiring a weapon immediately restores its nominal origin/aim rules. */
+ inventory[0]=0; fixture_weapon_count[0]=1; weapon_valid[0]=TRUE;
+ definitions[0].weapon.triggers.count=1;
+ definitions[0].weapon.triggers.data[0].first_person_weapon_offset=(real_point3d){0.4f,0.2f,0.1f};
+ CHECK(vr_render_projectile_pose(0,&units[0],&pose));
+ CHECK(weapon_preview_projectile_ray(0,0,0,&armed_origin,&armed_direction,&armed_ignore,&pose));
+ CHECK(vr_render_projectile_ray(0,&origin,&direction,&ignore));
+ point_equal(origin,armed_origin); direction_equal(direction,armed_direction); CHECK(ignore==armed_ignore);
+ CHECK(aim_calls==2 && marker_object==0 && time_calls==0);
+ /* Gamepad unarmed pointing also refreshes without changing simulation aim. */
+ unarmed(); vr_render.view.controller_aim=FALSE; vr_render.view.gamepad_aim=TRUE;
+ for(frame=0;frame<6;frame++) {
+ real yaw=0.1f*(real)frame;
+ fixture_facing=(real_vector3d){cosf(yaw),sinf(yaw),0};
+ CHECK(vr_render_projectile_point(0,&point)); CHECK(near(point.y,wall_x*tanf(yaw)));
+ direction_equal(units[0].unit.aiming_vector,(real_vector3d){1,0,0});
+ CHECK(aim_calls==0 && time_calls==0 && global_current_collision_user_depth==1);
+ }
+ unarmed(); wall_x=1000; fixture_observer.position=(real_point3d){0.2f,0.3f,0.5f};
+ CHECK(vr_render_projectile_point(0,&point));
+ point_equal(point,(real_point3d){100.2f,0.3f,0.5f}); CHECK(collision_calls==1 && aim_calls==0);
+}
+static void check_unarmed_guards(void) {
+ real_point3d point,origin; real_vector3d direction; long ignore;
+ unarmed(); fixture_weapon_count[0]=1; CHECK(!vr_render_projectile_point(0,&point));
+ vr_render_crosshairs(); CHECK(draw_count==0 && !vr_render.crosshairs_drawn);
+ CHECK(vr_render_hud_camera(&render.camera)); CHECK(vr_render_crosshair_offset[0]==0);
+ unarmed(); units[0].unit.player_index=NONE; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); units[0].unit.player_index=1; CHECK(!vr_render_projectile_point(0,&point));
+ origin=(real_point3d){7,8,9}; direction=(real_vector3d){3,4,5}; ignore=123;
+ CHECK(!vr_render_projectile_ray(0,&origin,&direction,&ignore));
+ point_equal(origin,(real_point3d){7,8,9}); direction_equal(direction,(real_vector3d){3,4,5}); CHECK(ignore==123);
+ unarmed(); players[0].unit_index=NONE; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); unit_valid[0]=FALSE; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); units[0].object.damage_flags=1u<<_object_dead_bit; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); units[0].object.parent_object_index=1; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); units[0].unit.gunner_object_index=2; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); units[0].object.type=_object_type_vehicle; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); fixture_local_player=NONE; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); fixture_inhibited=TRUE; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); fixture_perspective=0; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); fixture_cinematic=TRUE; CHECK(!vr_render_projectile_point(0,&point));
+ vr_render_crosshairs(); CHECK(draw_count==0 && !vr_render.crosshairs_drawn);
+ unarmed(); fixture_vr_aiming=FALSE; vr_render_crosshairs(); CHECK(draw_count==0);
+ unarmed(); vr_render.active=FALSE; vr_render_crosshairs(); CHECK(draw_count==0);
+ unarmed(); fixture_observer_valid=FALSE; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); fixture_observer.position.x=NAN; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); vr_render.view.controller_aim=FALSE; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); vr_render.view.hand_forward[0]=NAN; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); vr_render.view.hand_forward[0]=INFINITY; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); vr_render.view.hand_forward[0]=0; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); unit_definitions[0].unit.flags=0; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); unit_definitions[0].unit.aiming_velocity_maximum=1; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); unit_definitions[0].unit.aiming_acceleration_maximum=1; CHECK(!vr_render_projectile_point(0,&point));
+ /* Existing but unsupported weapons must never become an unarmed ray. */
+ unarmed(); inventory[0]=0; definitions[0].weapon.triggers.count=0;
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(aim_calls==0 && collision_calls==0);
+ unarmed(); global_current_collision_user_depth=MAXIMUM_COLLISION_USER_STACK_DEPTH-2;
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(collision_calls==0);
+ CHECK(global_current_collision_user_depth==MAXIMUM_COLLISION_USER_STACK_DEPTH-2);
+ /* Bounds remain shared with render-rate armed pointing. */
+ unarmed(); vr_render.view.hand_forward[0]=0; vr_render.view.hand_forward[2]=1;
+ CHECK(vr_render_projectile_ray(0,&origin,&direction,&ignore));
+ CHECK(near(direction.k,sinf(DEGREES_TO_RADIANS(85.0f))));
+ fixture_clip_pitch=0.25f; CHECK(vr_render_projectile_ray(0,&origin,&direction,&ignore));
+ CHECK(near(direction.k,sinf(0.25f)) && fixture_clip_calls==2);
+}
+static void check_script_pointing_ray(void) {
+ struct unit_datum simulation;
+ struct halo_vr_view cached_view;
+ real_point3d origin,render_origin; real_vector3d forward,render_forward; long owner;
+ unarmed(); simulation=units[0]; cached_view=vr_render.view;
+ vr_render.active=FALSE; /* Scripts run after the eye pass has ended. */
+ fixture_live_view.body_yaw=0.3f;
+ fixture_live_view.hand_forward[0]=cosf(0.8f); fixture_live_view.hand_forward[1]=sinf(0.8f);
+ fixture_observer.position=(real_point3d){0.2f,0.3f,0.5f};
+ CHECK(vr_render_player_pointing_ray(0,&origin,&forward));
+ point_equal(origin,fixture_observer.position);
+ direction_equal(forward,(real_vector3d){cosf(1.1f),sinf(1.1f),0});
+ CHECK(memcmp(&vr_render.view,&cached_view,sizeof(cached_view))==0 && !vr_render.active);
+ CHECK(memcmp(&units[0],&simulation,sizeof(simulation))==0);
+ CHECK(collision_calls==0 && aim_calls==0 && marker_object==NONE && time_calls==0);
+ CHECK(players[0].aim_assist_unit_index==41 && players[0].aim_assist_timestamp==42);
+ /* The next rendered view uses precisely the same unarmed ray. */
+ vr_render.view=fixture_live_view; vr_render.base_yaw=fixture_live_view.body_yaw;
+ CHECK(vr_render_projectile_ray(0,&render_origin,&render_forward,&owner));
+ point_equal(origin,render_origin); direction_equal(forward,render_forward); CHECK(owner==0);
+ fixture_tracking_valid=FALSE; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ fixture_tracking_valid=TRUE; fixture_live_view_valid=FALSE; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ fixture_live_view_valid=TRUE; fixture_vr_aiming=FALSE; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); CHECK(!vr_render_player_pointing_ray(1,&origin,&forward));
+ CHECK(!vr_render_player_pointing_ray(NONE,&origin,&forward));
+ CHECK(!vr_render_player_pointing_ray(0,NULL,&forward)); CHECK(!vr_render_player_pointing_ray(0,&origin,NULL));
+ unarmed(); fixture_local_player=NONE; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); fixture_weapon_count[0]=1; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); inventory[0]=0; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); units[0].unit.player_index=1; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); units[0].object.damage_flags=1u<<_object_dead_bit; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); units[0].object.parent_object_index=1; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); fixture_cinematic=TRUE; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); fixture_inhibited=TRUE; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); fixture_perspective=0; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); fixture_live_view.hand_forward[0]=NAN;
+ origin=(real_point3d){7,8,9}; forward=(real_vector3d){3,4,5};
+ CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ point_equal(origin,(real_point3d){7,8,9}); direction_equal(forward,(real_vector3d){3,4,5});
+}
+static void check_script_hmd_gaze(void) {
+ struct unit_datum simulation;
+ struct halo_vr_view cached_view;
+ real_point3d origin,expected_position; real_vector3d forward,expected_forward;
+ int frame;
+ reset(); simulation=units[0]; cached_view=vr_render.view; vr_render.active=FALSE;
+ for(frame=0;frame<12;frame++) {
+ real yaw=0.1f*(real)frame,head_yaw=-0.3f+0.02f*(real)frame;
+ real pitch=frame>=6 ? 0.15f : 0;
+ real px,py,pz;
+ fixture_live_view.body_yaw=yaw;
+ fixture_live_view.head_forward[0]=cosf(head_yaw); fixture_live_view.head_forward[1]=sinf(head_yaw);
+ fixture_live_view.head_forward[2]=0;
+ fixture_live_view.head_position[0]=0.1f; fixture_live_view.head_position[1]=0.2f; fixture_live_view.head_position[2]=0.3f;
+ fixture_live_view.gamepad_pitch=frame>=6; fixture_facing_angles.pitch=pitch;
+ fixture_observer.position=(real_point3d){0.01f*(real)frame,0.02f*(real)frame,0.5f};
+ CHECK(vr_render_player_gaze(0,&origin,&forward));
+ px=cosf(head_yaw)*cosf(pitch); py=sinf(head_yaw); pz=cosf(head_yaw)*sinf(pitch);
+ expected_forward=(real_vector3d){cosf(yaw)*px-sinf(yaw)*py,sinf(yaw)*px+cosf(yaw)*py,pz};
+ direction_equal(forward,expected_forward);
+ px=0.1f*cosf(pitch)-0.3f*sinf(pitch); py=0.2f; pz=0.1f*sinf(pitch)+0.3f*cosf(pitch);
+ expected_position=(real_point3d){fixture_observer.position.x+2*(cosf(yaw)*px-sinf(yaw)*py),
+ fixture_observer.position.y+2*(sinf(yaw)*px+cosf(yaw)*py),fixture_observer.position.z+2*pz};
+ point_equal(origin,expected_position);
+ CHECK(memcmp(&units[0],&simulation,sizeof(simulation))==0);
+ CHECK(memcmp(&vr_render.view,&cached_view,sizeof(cached_view))==0 && !vr_render.active);
+ CHECK(aim_calls==0 && collision_calls==0 && marker_object==NONE && time_calls==0);
+ }
+ /* Without current VR aiming, the rendered camera supplies the base yaw. */
+ reset(); fixture_vr_aiming=FALSE; fixture_live_view.body_yaw=-2;
+ fixture_observer.forward=(real_vector3d){0,1,0};
+ CHECK(vr_render_player_gaze(0,&origin,&forward)); direction_equal(forward,(real_vector3d){0,1,0});
+ /* Vehicle first-person uses the same seated head origin as rendering. */
+ reset(); units[0].object.parent_object_index=1; units[1].object.type=_object_type_vehicle;
+ fixture_live_view.vehicle_first_person=TRUE; fixture_live_view.head_position[0]=0.2f;
+ CHECK(vr_render_player_gaze(0,&origin,&forward));
+ point_equal(origin,(real_point3d){3.4f,4,5});
+ fixture_live_view.vehicle_first_person=FALSE; CHECK(vr_render_player_gaze(0,&origin,&forward));
+ point_equal(origin,(real_point3d){0.4f,0,0.5f});
+ reset(); CHECK(!vr_render_player_gaze(NONE,&origin,&forward));
+ CHECK(!vr_render_player_gaze(1,&origin,&forward));
+ CHECK(!vr_render_player_gaze(0,NULL,&forward)); CHECK(!vr_render_player_gaze(0,&origin,NULL));
+ reset(); fixture_local_player=NONE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); units[0].unit.player_index=1; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); unit_valid[0]=FALSE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); units[0].object.damage_flags=1u<<_object_dead_bit; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); units[0].object.type=_object_type_vehicle; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_cinematic=TRUE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_scripted_camera=TRUE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_perspective=_director_perspective_scripted; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_perspective=_director_perspective_neutral; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_inhibited=TRUE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_tracking_valid=FALSE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_live_view_valid=FALSE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_observer_valid=FALSE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_observer.position.x=NAN; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_live_view.body_yaw=NAN; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_live_view.world_units_per_metre=0; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_live_view.head_position[0]=INFINITY; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_live_view.head_forward[0]=0; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_live_view.gamepad_pitch=TRUE; fixture_facing_angles.pitch=NAN;
+ origin=(real_point3d){7,8,9}; forward=(real_vector3d){3,4,5};
+ CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ point_equal(origin,(real_point3d){7,8,9}); direction_equal(forward,(real_vector3d){3,4,5});
+}
+static void check_pose_limits_and_fallbacks(void) {
+ struct projectile_preview_pose pose;
+ real_point3d origin,baseline_origin; real_vector3d direction,baseline_direction; long ignore;
+ reset(); unit_definitions[0].unit.flags=1u<<_unit_fires_from_camera_bit;
+ vr_render.view.hand_forward[0]=0; vr_render.view.hand_forward[1]=0; vr_render.view.hand_forward[2]=1;
+ CHECK(vr_render_projectile_pose(0,&units[0],&pose)); CHECK(near(pose.forward.k,sinf(DEGREES_TO_RADIANS(85.0f))));
+ CHECK(near(pose.forward.i,cosf(DEGREES_TO_RADIANS(85.0f))));
+ vr_render.view.hand_forward[2]=-1; CHECK(vr_render_projectile_pose(0,&units[0],&pose)); CHECK(near(pose.forward.k,-sinf(DEGREES_TO_RADIANS(85.0f))));
+ fixture_clip_pitch=0.25f; CHECK(vr_render_projectile_pose(0,&units[0],&pose)); CHECK(near(pose.forward.k,-sinf(0.25f)));
+ CHECK(fixture_clip_calls==3);
+ fixture_clip_pitch=0; vr_render.view.hand_forward[0]=1; vr_render.view.hand_forward[2]=0; vr_render.base_yaw=0.75f;
+ CHECK(vr_render_projectile_pose(0,&units[0],&pose)); direction_equal(pose.forward,(real_vector3d){cosf(0.75f),sinf(0.75f),0});
+ fixture_inhibited=1; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); fixture_inhibited=0;
+ fixture_perspective=0; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); fixture_perspective=_director_perspective_first_person;
+ fixture_observer_valid=0; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); fixture_observer_valid=1;
+ fixture_observer.position.x=NAN; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); fixture_observer.position=unit_camera;
+ vr_render.view.hand_forward[0]=NAN; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); vr_render.view.hand_forward[0]=1;
+ unit_definitions[0].unit.aiming_velocity_maximum=1; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); unit_definitions[0].unit.aiming_velocity_maximum=0;
+ unit_definitions[0].unit.aiming_acceleration_maximum=1; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); unit_definitions[0].unit.aiming_acceleration_maximum=0;
+ units[0].object.parent_object_index=1; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); units[0].object.parent_object_index=NONE;
+ units[0].object.type=_object_type_vehicle; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); units[0].object.type=_object_type_biped;
+ units[0].unit.gunner_object_index=2; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); units[0].unit.gunner_object_index=NONE;
+ pose.camera_position=(real_point3d){0,0,0.5f}; pose.forward=(real_vector3d){0,1,0};
+ CHECK(weapon_preview_projectile_ray(0,0,0,&baseline_origin,&baseline_direction,&ignore,NULL));
+ unit_definitions[0].unit.aiming_velocity_maximum=1;
+ CHECK(weapon_preview_projectile_ray(0,0,0,&origin,&direction,&ignore,&pose)); point_equal(origin,baseline_origin); direction_equal(direction,baseline_direction);
+ unit_definitions[0].unit.aiming_velocity_maximum=0; pose.forward=(real_vector3d){NAN,0,0};
+ CHECK(!weapon_preview_projectile_ray(0,0,0,&origin,&direction,&ignore,&pose)); CHECK(time_calls==0 && actor_calls==0 && global_current_collision_user_depth==1);
+ pose.forward=(real_vector3d){0,0,0}; CHECK(!weapon_preview_projectile_ray(0,0,0,&origin,&direction,&ignore,&pose));
+ pose.forward=(real_vector3d){0,2,0}; CHECK(weapon_preview_projectile_ray(0,0,0,&origin,&direction,&ignore,&pose)); CHECK(near(magnitude3d(&direction),1));
+ pose.camera_position.x=NAN; CHECK(!weapon_preview_projectile_ray(0,0,0,&origin,&direction,&ignore,&pose));
+}
+int main(void) {
+ check_render_pose_anchors(); check_render_frame_refresh(); check_pose_limits_and_fallbacks();
+ check_unarmed_refresh_and_transition(); check_unarmed_guards();
+ check_script_pointing_ray();
+ check_script_hmd_gaze();
+ check_ray_equivalence(); check_vehicles_and_guards(); check_collision_and_projection();
+ printf("PASS: %d production projectile/reticle assertions\n",checks); return 0;
+}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ args = parser.parse_args()
+ weapons = (ROOT / 'source/items/weapons.c').read_text()
+ aim = (ROOT / 'source/game/aim_assist.c').read_text()
+ render = (ROOT / 'port/linux/game/vr_render.c').read_text()
+ units = (ROOT / 'source/units/units.c').read_text()
+ math = (ROOT / 'source/math/real_math.c').read_text()
+ verify_refactors(weapons, aim, render, units)
+ harness = HARNESS.replace('@@LOS@@', function(aim, 'aim_assist_clear_line_of_sight'))
+ groups = {
+ '@@MATH@@': (math, ['fast_normals_interpolate', 'pin_normal_to_cone3d']),
+ '@@AIM@@': (aim, ['player_aim_projectile_internal', 'player_aim_projectile', 'player_preview_projectile_aim']),
+ '@@UNIT@@': (units, ['unit_adjust_projectile_ray_internal', 'unit_adjust_projectile_ray', 'unit_preview_projectile_ray']),
+ '@@WEAPON_HELPERS@@': (weapons, ['weapon_get_effect_object_index', 'weapon_get_owner_object_index', 'weapon_get_projectile_owner_object_index', 'trigger_adjust_projectile_ray']),
+ '@@PREVIEW@@': (weapons, ['weapon_preview_projectile_ray']),
+ '@@RENDER@@': (render, ['frustum_clip_transform', 'yaw_rotate', 'view_rotate', 'seated_unit', 'vr_render_player_gaze', 'vr_render_hud_camera', 'vr_render_pointing_pose', 'vr_render_projectile_pose', 'vr_render_unarmed_ray', 'vr_render_player_pointing_ray', 'vr_render_projectile_ray', 'vr_render_projectile_point', 'vr_render_crosshairs']),
+ }
+ for placeholder, (text, names) in groups.items():
+ harness = harness.replace(placeholder, '\n\n'.join(function(text, name) for name in names))
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ installations = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(installations.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1]); vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No C11 host compiler found; select one with --cc.')
+ with tempfile.TemporaryDirectory(prefix='halo-vr-projectile-check-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory); source = temp / 'test.c'; executable = temp / ('test.exe' if os.name == 'nt' else 'test')
+ source.write_text(harness)
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/DHALO_VR', '/D_CRT_SECURE_NO_WARNINGS',
+ '/Fe:' + str(executable), '/Fo:' + str(temp / 'test.obj'), str(source)]
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported shell metacharacter in compiler/temp path.')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\nif errorlevel 1 exit /b 1\n' +
+ subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-DHALO_VR', str(source)]
+ if os.name != 'nt':
+ command.append('-lm')
+ command.extend(['-o', str(executable)])
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_sun_glow.py b/tools/test_vr_sun_glow.py
new file mode 100644
index 0000000000000000000000000000000000000000..ea4b89d6b015c5fb63083ee0b4b51cdb7b8e407b
--- /dev/null
+++ b/tools/test_vr_sun_glow.py
@@ -0,0 +1,331 @@
+#!/usr/bin/env python3
+"""Run production stereo sun projection, dispatch and shader-sampling checks.
+
+Host C11 compiler only; no headset, game data, SDL or OpenXR is required.
+This validates CPU/matrix and shader source contracts, not GPU visual output.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def function(text, name):
+ match = re.search(r'(?:static\s+)?(?:void|float|real|int|boolean)\s+' + re.escape(name) +
+ r'\s*\([^;]*?\)\s*\{', text)
+ assert match, name
+ masked = re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"',
+ lambda item: re.sub(r'[^\n]', ' ', item.group()), text, flags=re.S)
+ end, depth = masked.index('{', match.start()) + 1, 1
+ while depth:
+ depth += (masked[end] == '{') - (masked[end] == '}')
+ end += 1
+ return text[match.start():end]
+
+
+HARNESS = r'''
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+typedef float real;
+typedef int boolean;
+typedef int BOOL;
+typedef unsigned int DWORD;
+#define TRUE 1
+#define FALSE 0
+#define MIN(a,b) ((a) < (b) ? (a) : (b))
+#define D3DRS_PS_MAX 128
+#define SAMPLE_BIAS ""
+static int checks;
+#define CHECK(condition) do { checks++; if (!(condition)) { \
+ fprintf(stderr, "failed line %d: %s\n", __LINE__, #condition); exit(1); } } while (0)
+static int near_float(float a, float b) { return fabsf(a - b) < 0.002f; }
+typedef struct { real x, y, z; } real_point3d;
+typedef struct { real i, j; } real_vector2d;
+typedef struct { short x0, y0, x1, y1; } rectangle2d;
+typedef struct { real n[4][3]; } real_matrix4x3;
+struct render_camera { real_point3d position; rectangle2d viewport_bounds; real marker; };
+struct render_frustum { real_matrix4x3 world_to_view; real projection_matrix[4][4]; real marker; };
+struct rasterizer_lens_flare_submit_parameters { real_point3d position; };
+static struct { struct render_camera camera; struct render_frustum frustum; } global_window_parameters;
+static struct { boolean active; struct render_camera eye_cameras[2]; struct render_frustum eyes[2]; } vr_render;
+static struct { int screen_space, screen_points, eye_alone; unsigned long eye_serial; } vr_gl;
+static int corrections_calls;
+static void halo_vr_set_screen_corrections(const float corrections[2][4]) {
+ static const float identity[2][4] = { { 1.0f, 1.0f, 0.0f, 0.0f }, { 1.0f, 1.0f, 0.0f, 0.0f } };
+ CHECK(memcmp(corrections, identity, sizeof(identity)) == 0);
+ vr_gl.screen_points = 0; corrections_calls++;
+}
+static void matrix4x3_transform_point(const real_matrix4x3 *matrix, const real_point3d *point, real_point3d *out) {
+ out->x = matrix->n[0][0]*point->x + matrix->n[1][0]*point->y + matrix->n[2][0]*point->z + matrix->n[3][0];
+ out->y = matrix->n[0][1]*point->x + matrix->n[1][1]*point->y + matrix->n[2][1]*point->z + matrix->n[3][1];
+ out->z = matrix->n[0][2]*point->x + matrix->n[1][2]*point->y + matrix->n[2][2]*point->z + matrix->n[3][2];
+}
+@@FUNCTIONS@@
+enum { XGPU_MULTIVIEW_NONE, XGPU_MULTIVIEW_EYES, XGPU_MULTIVIEW_LEFT_EYE, XGPU_MULTIVIEW_RIGHT_EYE };
+enum { _xgpu_sampler_none, _xgpu_sampler_2d, _xgpu_sampler_3d, _xgpu_sampler_cube, _xgpu_sampler_2d_layered };
+@@KEY@@
+@@OLD_KEY@@
+struct xgpu_text { char buffer[2048]; unsigned long length; };
+static void xgpu_text_append(struct xgpu_text *text, const char *format, ...) {
+ va_list arguments;
+ int count;
+ va_start(arguments, format);
+ count = vsnprintf(text->buffer + text->length, sizeof(text->buffer) - text->length, format, arguments);
+ va_end(arguments);
+ CHECK(count > 0 && (unsigned long)count < sizeof(text->buffer) - text->length);
+ text->length += (unsigned long)count;
+}
+@@SAMPLE@@
+static void choose_source_eye(struct nv2a_pixel_shader_key *output) {
+ struct nv2a_pixel_shader_key key = *output;
+ @@SAMPLE_BINDING@@
+ *output = key;
+}
+static int draws, draw_eye[2], visible[2];
+static float draw_x[2], draw_y[2], depth[2], wall_depth[2];
+static double configured_glow = 0.5;
+static float composite_strength[2];
+static double config_real(const char *name) {
+ CHECK(strcmp(name, "vr.sun_glow_strength") == 0);
+ return configured_glow;
+}
+@@STRENGTH@@
+static void rasterizer_sun_glow_draw_internal(struct rasterizer_lens_flare_submit_parameters const *flare, real glow_strength) {
+ real_point3d center;
+ real_vector2d axes;
+ struct nv2a_pixel_shader_key key;
+ struct xgpu_text text;
+ int slot = draws++;
+ CHECK(slot < 2); composite_strength[slot] = glow_strength;
+ if (!vr_render.active) return;
+ CHECK(vr_gl.screen_space && !vr_gl.screen_points);
+ CHECK(vr_gl.eye_alone == slot); CHECK(global_window_parameters.camera.marker == (float)(slot + 1));
+ CHECK(rasterizer_project_billboard(&flare->position, 0.5f, &center, &axes));
+ draw_eye[slot] = vr_gl.eye_alone; draw_x[slot] = center.x; draw_y[slot] = center.y; depth[slot] = center.z;
+ /* The unchanged legacy mask uses ZENABLE + LESSEQUAL against this
+ eye's depth, then this eye alone is copied into the shared scratch. */
+ visible[slot] = center.z <= wall_depth[slot];
+ memset(&key, 0, sizeof(key)); memset(&text, 0, sizeof(text));
+ key.sampler_type[0] = _xgpu_sampler_2d_layered; key.multiview = XGPU_MULTIVIEW_NONE;
+ choose_source_eye(&key); sample(&text, &key, 0, "point");
+ CHECK(key.sample_eye == slot);
+ CHECK(strstr(text.buffer, slot ? ", 1.0)" : ", 0.0)") != NULL);
+}
+@@WRAPPER@@
+static void frustum(struct render_frustum *target, float eye, const float fov[4], float near_z, float far_z) {
+ memset(target, 0, sizeof(*target));
+ target->world_to_view.n[0][0] = target->world_to_view.n[1][1] = target->world_to_view.n[2][2] = 1.0f;
+ target->world_to_view.n[3][0] = -eye;
+ target->projection_matrix[0][0] = 2.0f / (fov[1] - fov[0]);
+ target->projection_matrix[1][1] = 2.0f / (fov[2] - fov[3]);
+ target->projection_matrix[2][0] = -(fov[1] + fov[0]) / (fov[1] - fov[0]);
+ target->projection_matrix[2][1] = -(fov[2] + fov[3]) / (fov[2] - fov[3]);
+ target->projection_matrix[2][2] = far_z / (far_z - near_z);
+ target->projection_matrix[3][2] = -near_z * far_z / (far_z - near_z);
+ target->projection_matrix[2][3] = 1.0f;
+}
+static const float fovs[2][4] = { { -1.12f, 0.83f, 1.02f, -0.94f }, { -0.79f, 1.17f, 0.96f, -1.03f } };
+static void reset_state(void) {
+ int eye;
+ memset(&vr_render, 0, sizeof(vr_render)); memset(&vr_gl, 0, sizeof(vr_gl));
+ memset(&global_window_parameters, 0, sizeof(global_window_parameters));
+ vr_render.active = 1; vr_gl.eye_alone = -1;
+ draws = corrections_calls = 0;
+ for (eye = 0; eye < 2; eye++) {
+ struct render_camera *camera = &vr_render.eye_cameras[eye];
+ camera->position.x = eye ? 0.03f : -0.03f;
+ camera->viewport_bounds.x1 = 640; camera->viewport_bounds.y1 = 480; camera->marker = (float)(eye + 1);
+ frustum(&vr_render.eyes[eye], camera->position.x, fovs[eye], 0.0625f, 1024.0f);
+ }
+ global_window_parameters.camera = vr_render.eye_cameras[0];
+ global_window_parameters.camera.marker = 9.0f;
+ frustum(&global_window_parameters.frustum, 0.0f, fovs[0], 0.125f, 4096.0f);
+ global_window_parameters.frustum.marker = 9.0f;
+ wall_depth[0] = 1.0f; wall_depth[1] = 0.0f;
+}
+static void check_projection(void) {
+ int eye, lateral, vertical, distance_step, k;
+ const float distances[4] = { 0.2f, 2.0f, 30.0f, 974.0f };
+ reset_state();
+ for (eye = 0; eye < 2; eye++) {
+ struct render_camera camera = global_window_parameters.camera;
+ struct render_frustum selected = global_window_parameters.frustum;
+ float hardware_depth[4];
+ for (k = 0; k < 4; k++) hardware_depth[k] = selected.projection_matrix[k][2];
+ CHECK(vr_render_sun_glow_begin((short)eye, &camera, &selected));
+ for (k = 0; k < 4; k++) CHECK(hardware_depth[k] == selected.projection_matrix[k][2]);
+ global_window_parameters.camera = camera; global_window_parameters.frustum = selected;
+ for (distance_step = 0; distance_step < 4; distance_step++) {
+ float z = distances[distance_step];
+ for (lateral = -8; lateral <= 8; lateral++) {
+ for (vertical = -5; vertical <= 5; vertical++) {
+ float tangent_x = (float)lateral * 0.11f, tangent_y = (float)vertical * 0.13f;
+ real_point3d point = { tangent_x*z, tangent_y*z, z }, center;
+ real_vector2d axes;
+ const float *fov = fovs[eye];
+ float x = tangent_x - camera.position.x / z;
+ float expected_x = ((2.0f*x - fov[1] - fov[0]) / (fov[1] - fov[0]) + 1.0f) * 320.0f - 0.5f;
+ float expected_y = (1.0f - (2.0f*tangent_y - fov[2] - fov[3]) / (fov[2] - fov[3])) * 240.0f - 0.5f;
+ float expected_depth = hardware_depth[2] + hardware_depth[3] / z;
+ CHECK(rasterizer_project_billboard(&point, 0.1f, &center, &axes));
+ CHECK(near_float(center.x, expected_x)); CHECK(near_float(center.y, expected_y));
+ CHECK(near_float(center.z, expected_depth));
+ CHECK(near_float(axes.i, 64.0f / ((fov[1] - fov[0]) * z)));
+ CHECK(near_float(axes.j, 48.0f / ((fov[2] - fov[3]) * z)));
+ }
+ }
+ }
+ vr_render_sun_glow_end();
+ }
+}
+static void check_dispatch_and_occlusion(void) {
+ struct rasterizer_lens_flare_submit_parameters flare = { { 2.0f, 0.3f, 5.0f } };
+ struct render_camera saved_camera;
+ struct render_frustum saved_frustum;
+ unsigned long serial;
+ reset_state(); saved_camera = global_window_parameters.camera; saved_frustum = global_window_parameters.frustum;
+ rasterizer_sun_glow_draw(&flare);
+ CHECK(draws == 2 && draw_eye[0] == 0 && draw_eye[1] == 1);
+ CHECK(composite_strength[0] == 0.5f && composite_strength[1] == 0.5f);
+ CHECK(visible[0] && !visible[1]); /* independent eye masks; right wall cannot reuse left visibility */
+ CHECK(fabsf(draw_x[0] - draw_x[1]) > 20.0f && fabsf(draw_y[0] - draw_y[1]) > 5.0f);
+ CHECK(near_float(depth[0], depth[1]));
+ CHECK(memcmp(&saved_camera, &global_window_parameters.camera, sizeof(saved_camera)) == 0);
+ CHECK(memcmp(&saved_frustum, &global_window_parameters.frustum, sizeof(saved_frustum)) == 0);
+ CHECK(vr_gl.eye_alone == -1 && !vr_gl.screen_space && !vr_gl.screen_points);
+ CHECK(vr_gl.eye_serial == 2);
+ serial = vr_gl.eye_serial; halo_vr_screen_space(0); CHECK(vr_gl.eye_serial == serial);
+ halo_vr_screen_space(1); CHECK(vr_gl.eye_serial == serial + 1);
+ halo_vr_screen_space(1); CHECK(vr_gl.eye_serial == serial + 1);
+ halo_vr_screen_space(0); CHECK(vr_gl.eye_serial == serial + 2);
+ CHECK(!vr_render_sun_glow_begin(-1, &saved_camera, &saved_frustum));
+ CHECK(!vr_render_sun_glow_begin(2, &saved_camera, &saved_frustum));
+ reset_state(); vr_render.active = 0; saved_camera = global_window_parameters.camera; saved_frustum = global_window_parameters.frustum;
+ rasterizer_sun_glow_draw(&flare);
+ CHECK(draws == 1 && corrections_calls == 0);
+ CHECK(composite_strength[0] == 1.0f);
+ CHECK(!vr_gl.screen_space && vr_gl.eye_alone == -1);
+ CHECK(memcmp(&saved_camera, &global_window_parameters.camera, sizeof(saved_camera)) == 0);
+ CHECK(memcmp(&saved_frustum, &global_window_parameters.frustum, sizeof(saved_frustum)) == 0);
+}
+static void check_glow_strength(void) {
+ const double inputs[] = { -1.0, 0.0, 0.2, 0.5, 1.0, 1.5, NAN, INFINITY, -INFINITY };
+ const float expected[] = { 0.0f, 0.0f, 0.2f, 0.5f, 1.0f, 1.0f, 0.5f, 0.5f, 0.5f };
+ struct rasterizer_lens_flare_submit_parameters flare = { { 2.0f, 0.3f, 5.0f } };
+ unsigned int setting;
+ for (setting = 0; setting < sizeof(inputs) / sizeof(inputs[0]); setting++) {
+ reset_state(); configured_glow = inputs[setting];
+ rasterizer_sun_glow_draw(&flare);
+ CHECK(draws == 2);
+ CHECK(near_float(composite_strength[0], expected[setting]));
+ CHECK(near_float(composite_strength[1], expected[setting]));
+ CHECK(visible[0] && !visible[1]);
+ CHECK(vr_gl.eye_alone == -1 && !vr_gl.screen_space);
+ }
+ configured_glow = 0.5;
+}
+static void check_shader_sampling(void) {
+ struct nv2a_pixel_shader_key key;
+ struct xgpu_text text;
+ int view, source_eye;
+ size_t key_size = sizeof(struct nv2a_pixel_shader_key), old_size = sizeof(struct original_pixel_shader_key);
+ CHECK(key_size == old_size);
+ for (view = XGPU_MULTIVIEW_NONE; view <= XGPU_MULTIVIEW_RIGHT_EYE; view++) {
+ for (source_eye = 0; source_eye < 2; source_eye++) {
+ const char *expected = view == XGPU_MULTIVIEW_EYES ? "float(gl_ViewID_OVR)" :
+ view == XGPU_MULTIVIEW_RIGHT_EYE || (view == XGPU_MULTIVIEW_NONE && source_eye) ? ", 1.0)" : ", 0.0)";
+ memset(&key, 0, sizeof(key)); memset(&text, 0, sizeof(text));
+ key.sampler_type[0] = _xgpu_sampler_2d_layered; key.multiview = (unsigned char)view;
+ key.sample_eye = (unsigned char)source_eye; sample(&text, &key, 0, "point");
+ CHECK(strstr(text.buffer, expected) != NULL);
+ }
+ }
+ memset(&key, 0, sizeof(key)); memset(&text, 0, sizeof(text));
+ key.sampler_type[0] = _xgpu_sampler_2d; key.sample_eye = 1;
+ sample(&text, &key, 0, "point"); CHECK(strstr(text.buffer, "vec3") == NULL);
+}
+int main(void) {
+ check_projection(); check_dispatch_and_occlusion(); check_glow_strength(); check_shader_sampling();
+ printf("PASS: %d production stereo sun-glow assertions\n", checks);
+ return 0;
+}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ args = parser.parse_args()
+ lights_path = 'source/rasterizer/xbox/rasterizer_xbox_lights.c'
+ lights = (ROOT / lights_path).read_text()
+ render = (ROOT / 'port/linux/game/vr_render.c').read_text()
+ gl = (ROOT / 'port/linux/src/d3d8_gl.c').read_text()
+ psh = (ROOT / 'port/linux/src/nv2a_psh.c').read_text()
+ xgpu = (ROOT / 'port/linux/src/xgpu.h').read_text()
+ old_lights = subprocess.check_output(['git', 'show', 'HEAD:' + lights_path], cwd=ROOT, text=True)
+ old_xgpu = subprocess.check_output(['git', 'show', 'HEAD:port/linux/src/xgpu.h'], cwd=ROOT, text=True)
+ old_draw = function(old_lights, 'rasterizer_sun_glow_draw')
+ new_draw = function(lights, 'rasterizer_sun_glow_draw_internal')
+ # The sole change inside the legacy routine is the final composite alpha.
+ original_alpha = 'brightness / (real)(pass + 1)) >= 0 && success)'
+ scaled_alpha = 'brightness * glow_strength / (real)(pass + 1)) >= 0 && success)'
+ assert new_draw.count(scaled_alpha) == 1 and old_draw.count(original_alpha) == 1
+ assert new_draw[new_draw.index('{'):].replace(scaled_alpha, original_alpha) == old_draw[old_draw.index('{'):], 'Legacy mask/blur/depth path changed'
+ assert function(lights, 'rasterizer_project_billboard') == function(old_lights, 'rasterizer_project_billboard')
+ assert 'D3DRS_ZFUNC,\n\t\t\t\tD3DCMP_LESSEQUAL' in new_draw and 'D3DRS_ZENABLE,\n\t\t\t\tTRUE' in new_draw
+ binding = re.search(r'key\.sample_eye = vr_gl\.eye_alone == 1 \? 1 : 0;', gl)
+ assert binding and binding.start() < gl.index('entry = program_get(vertex_shader_get(program, immediate, key.multiview)')
+ assert 'vr_gl.screen_points || vr_gl.screen_space ? 1.0f : 0.0f' in gl
+ assert 'halo_vr_screen_space(0);' in function(render, 'vr_render_end')
+ assert 'vr_render.eye_cameras[eye] = eye_camera;' in render
+ key = re.search(r'struct nv2a_pixel_shader_key\s*\{.*?\};', xgpu, re.S).group()
+ old_key = re.search(r'struct nv2a_pixel_shader_key\s*\{.*?\};', old_xgpu, re.S).group()
+ functions = '\n\n'.join([function(gl, 'halo_vr_screen_space'), function(gl, 'halo_vr_draw_eye'),
+ function(render, 'vr_render_screen_point_end'), function(render, 'vr_render_sun_glow_begin'),
+ function(render, 'vr_render_sun_glow_end'), function(lights, 'rasterizer_project_billboard')])
+ harness = HARNESS.replace('@@FUNCTIONS@@', functions).replace('@@WRAPPER@@', function(lights, 'rasterizer_sun_glow_draw'))
+ harness = harness.replace('@@STRENGTH@@', function(lights, 'rasterizer_vr_sun_glow_strength'))
+ harness = harness.replace('@@KEY@@', key).replace('@@OLD_KEY@@', old_key.replace('nv2a_pixel_shader_key', 'original_pixel_shader_key'))
+ harness = harness.replace('@@SAMPLE@@', function(psh, 'sample')).replace('@@SAMPLE_BINDING@@', binding.group())
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ installations = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(installations.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1]); vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No C11 host compiler found; select one with --cc.')
+ with tempfile.TemporaryDirectory(prefix='halo-vr-sun-check-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory); source = temp / 'test.c'; executable = temp / ('test.exe' if os.name == 'nt' else 'test')
+ source.write_text(harness)
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/DHALO_VR', '/D_CRT_SECURE_NO_WARNINGS',
+ '/Fe:' + str(executable), '/Fo:' + str(temp / 'test.obj'), str(source)]
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported shell metacharacter in compiler/temp path.')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\nif errorlevel 1 exit /b 1\n' +
+ subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-DHALO_VR', str(source), '-lm', '-o', str(executable)]
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+ print('PASS: original glow/depth pipeline preserved; stereo source and screen-space binding contracts')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_tracking.py b/tools/test_vr_tracking.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff5c227a1bc9fe89e517753befbc12759ded8011
--- /dev/null
+++ b/tools/test_vr_tracking.py
@@ -0,0 +1,220 @@
+#!/usr/bin/env python3
+"""Compile the production OpenXR locate functions against deterministic failures.
+
+Uses the checked-in Khronos and guest/host ABI headers. No runtime or headset
+is loaded. Verifies fresh-query validity after prior successful tracking.
+"""
+import argparse
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import sys
+import tempfile
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT))
+from tools.test_vr_projectile_reticle import function
+
+
+HARNESS = r'''
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <openxr/openxr.h>
+#include "vr_host.h"
+static unsigned checks;
+#define CHECK(c) do { checks++; if(!(c)) { fprintf(stderr,"failed line %d: %s\n",__LINE__,#c); exit(1); } } while(0)
+static struct {
+ int frame_begun;
+ XrFrameState frame;
+ XrSession session;
+ XrSpace play_space,view_space,aim_spaces[2],grip_spaces[2];
+} vr;
+static XrResult views_result,space_result[5];
+static uint32_t view_count;
+static XrViewStateFlags view_flags;
+static XrSpaceLocationFlags space_flags[5];
+static int view_calls,space_calls,generation;
+XRAPI_ATTR XrResult XRAPI_CALL xrLocateViews(XrSession session,
+ const XrViewLocateInfo *info,XrViewState *state,uint32_t capacity,
+ uint32_t *count,XrView *views) {
+ int eye;
+ CHECK(session==vr.session && capacity==2);
+ CHECK(info->type==XR_TYPE_VIEW_LOCATE_INFO && info->next==NULL);
+ CHECK(info->displayTime==vr.frame.predictedDisplayTime && info->space==vr.play_space);
+ CHECK(state->type==XR_TYPE_VIEW_STATE && state->next==NULL);
+ view_calls++;
+ if(views_result!=XR_SUCCESS) return views_result;
+ *count=view_count; state->viewStateFlags=view_flags;
+ for(eye=0;eye<2;eye++) {
+ CHECK(views[eye].type==XR_TYPE_VIEW && views[eye].next==NULL);
+ views[eye].pose.position=(XrVector3f){(float)(10*generation+eye),2,3};
+ views[eye].pose.orientation=(XrQuaternionf){0,0,0,1};
+ views[eye].fov=(XrFovf){-0.5f,0.5f,0.5f,-0.5f};
+ }
+ return XR_SUCCESS;
+}
+XRAPI_ATTR XrResult XRAPI_CALL xrLocateSpace(XrSpace space,XrSpace base,
+ XrTime time,XrSpaceLocation *location) {
+ int slot=(int)(uintptr_t)space-2;
+ CHECK(slot>=0 && slot<5 && base==vr.play_space && time==vr.frame.predictedDisplayTime);
+ CHECK(location->type==XR_TYPE_SPACE_LOCATION && location->next==NULL);
+ space_calls++;
+ if(space_result[slot]!=XR_SUCCESS) return space_result[slot];
+ location->locationFlags=space_flags[slot];
+ location->pose.position=(XrVector3f){(float)(100*generation+slot),4,5};
+ location->pose.orientation=(XrQuaternionf){0,0,0,1};
+ return XR_SUCCESS;
+}
+/* OpenXR's standard { TYPE } initializer intentionally zeroes every other
+ member. Suppress only Clang's warning about this established API idiom. */
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+#endif
+@@PRODUCTION@@
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+static void fixture(void) {
+ int slot;
+ memset(&vr,0,sizeof(vr)); vr.frame_begun=1;
+ vr.frame.predictedDisplayTime=123456;
+ vr.session=(XrSession)(uintptr_t)1; vr.play_space=(XrSpace)(uintptr_t)1;
+ vr.view_space=(XrSpace)(uintptr_t)2;
+ vr.aim_spaces[0]=(XrSpace)(uintptr_t)3; vr.grip_spaces[0]=(XrSpace)(uintptr_t)4;
+ vr.aim_spaces[1]=(XrSpace)(uintptr_t)5; vr.grip_spaces[1]=(XrSpace)(uintptr_t)6;
+ views_result=XR_SUCCESS; view_count=2;
+ view_flags=XR_VIEW_STATE_ORIENTATION_VALID_BIT|XR_VIEW_STATE_POSITION_VALID_BIT;
+ for(slot=0;slot<5;slot++) {
+ space_result[slot]=XR_SUCCESS;
+ space_flags[slot]=XR_SPACE_LOCATION_ORIENTATION_VALID_BIT|XR_SPACE_LOCATION_POSITION_VALID_BIT;
+ }
+ view_calls=space_calls=0; generation=1;
+}
+static struct vr_host_pose *space_pose(struct vr_host_views *views,int slot) {
+ switch(slot) {
+ case 0: return &views->head;
+ case 1: return &views->aim[0];
+ case 2: return &views->grip[0];
+ case 3: return &views->aim[1];
+ default: return &views->grip[1];
+ }
+}
+static void all_valid(struct vr_host_views *views,int expected) {
+ int eye,slot;
+ for(eye=0;eye<2;eye++) CHECK(views->eye[eye].valid==expected);
+ for(slot=0;slot<5;slot++) CHECK(space_pose(views,slot)->valid==expected);
+ CHECK(views->should_render==1 && views->focused==1 && views->display_elapsed==0.01f);
+}
+static void success(struct vr_host_views *views) {
+ int eye,slot;
+ CHECK(host_vr_locate(views)); all_valid(views,1);
+ for(eye=0;eye<2;eye++) CHECK(views->eye[eye].position[0]==(float)(10*generation+eye));
+ for(slot=0;slot<5;slot++) CHECK(space_pose(views,slot)->position[0]==(float)(100*generation+slot));
+}
+int main(void) {
+ struct vr_host_views views;
+ unsigned mask; int slot,eye;
+ memset(&views,0,sizeof(views)); views.should_render=1; views.focused=1; views.display_elapsed=0.01f;
+ fixture(); success(&views);
+ views_result=XR_ERROR_RUNTIME_FAILURE; generation=2;
+ CHECK(!host_vr_locate(&views)); all_valid(&views,0);
+ CHECK(space_calls==5); /* A view failure must not query individual spaces. */
+ views_result=XR_SUCCESS; success(&views);
+ view_count=1; CHECK(!host_vr_locate(&views)); all_valid(&views,0);
+ view_count=2; success(&views);
+ vr.frame_begun=0; view_calls=space_calls=0;
+ CHECK(!host_vr_locate(&views)); all_valid(&views,0); CHECK(view_calls==0 && space_calls==0);
+ vr.frame_begun=1; success(&views);
+ for(mask=1;mask<32;mask++) {
+ fixture(); success(&views); generation=2;
+ for(slot=0;slot<5;slot++) if(mask&(1u<<slot)) space_result[slot]=XR_ERROR_RUNTIME_FAILURE;
+ CHECK(host_vr_locate(&views));
+ for(eye=0;eye<2;eye++) CHECK(views.eye[eye].valid==1 && views.eye[eye].position[0]==(float)(20+eye));
+ for(slot=0;slot<5;slot++) {
+ struct vr_host_pose *pose=space_pose(&views,slot);
+ CHECK(pose->valid==((mask&(1u<<slot)) ? 0 : 1));
+ CHECK(pose->position[0]==(float)(((mask&(1u<<slot)) ? 100 : 200)+slot));
+ }
+ for(slot=0;slot<5;slot++) space_result[slot]=XR_SUCCESS;
+ generation=3; success(&views);
+ }
+ fixture(); success(&views); view_flags=XR_VIEW_STATE_POSITION_VALID_BIT;
+ for(slot=0;slot<5;slot++) space_flags[slot]=XR_SPACE_LOCATION_POSITION_VALID_BIT;
+ CHECK(host_vr_locate(&views)); all_valid(&views,0);
+ view_flags=XR_VIEW_STATE_ORIENTATION_VALID_BIT;
+ for(slot=0;slot<5;slot++) space_flags[slot]=XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
+ CHECK(host_vr_locate(&views)); all_valid(&views,0);
+ view_flags|=XR_VIEW_STATE_ORIENTATION_VALID_BIT;
+ view_flags|=XR_VIEW_STATE_POSITION_VALID_BIT;
+ for(slot=0;slot<5;slot++) space_flags[slot]|=XR_SPACE_LOCATION_POSITION_VALID_BIT;
+ generation=2; success(&views);
+ for(slot=0;slot<5;slot++) {
+ int other;
+ space_flags[slot]=XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
+ CHECK(host_vr_locate(&views));
+ CHECK(views.eye[0].valid && views.eye[1].valid);
+ for(other=0;other<5;other++) CHECK(space_pose(&views,other)->valid==(other!=slot));
+ space_flags[slot]|=XR_SPACE_LOCATION_POSITION_VALID_BIT;
+ success(&views);
+ }
+ printf("PASS: %u production OpenXR tracking assertions\n",checks); return 0;
+}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ args = parser.parse_args()
+ source_text = (ROOT / 'port/linux/arm64/host_vr.c').read_text()
+ harness = HARNESS.replace('@@PRODUCTION@@', '\n\n'.join(
+ function(source_text, name) for name in ('pose_from_xr', 'host_vr_locate')))
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ installations = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(installations.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1])
+ vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No C11 host compiler found; select one with --cc.')
+ include_paths = [ROOT / 'port/third_party/openxr', ROOT / 'port/linux/src']
+ with tempfile.TemporaryDirectory(prefix='halo-vr-tracking-check-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory)
+ source = temp / 'test.c'
+ executable = temp / ('test.exe' if os.name == 'nt' else 'test')
+ source.write_text(harness)
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/D_CRT_SECURE_NO_WARNINGS',
+ '/Fe:' + str(executable), '/Fo:' + str(temp / 'test.obj')]
+ command.extend('/I' + str(path) for path in include_paths)
+ command.append(str(source))
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported shell metacharacter in compiler/temp path.')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\nif errorlevel 1 exit /b 1\n' +
+ subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror']
+ command.extend('-I' + str(path) for path in include_paths)
+ command.append(str(source))
+ if os.name != 'nt':
+ command.append('-lm')
+ command.extend(['-o', str(executable)])
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_tutorial.py b/tools/test_vr_tutorial.py
new file mode 100644
index 0000000000000000000000000000000000000000..d1bbd2b2a0b367bad3c51b3ff2b96a647ccc6b6b
--- /dev/null
+++ b/tools/test_vr_tutorial.py
@@ -0,0 +1,536 @@
+#!/usr/bin/env python3
+"""Host regressions for production VR tutorial gates; no headset or map required.
+
+An optional --xbox-a10 reads an authorized local Xbox map passively to verify
+the actual tutorial predicates. Its bytes and scripts are never saved or copied.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import struct
+import subprocess
+import tempfile
+import zlib
+
+import test_vr_locomotion as walking
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def function(source, name):
+ match = re.search(r'(?:static\s+)?(?:void|float|int|boolean)\s+' + re.escape(name) +
+ r'\s*\([^;]*?\)\s*\{', source)
+ assert match, f'Missing production function {name}'
+ code = walking.masked(source)
+ end = code.index('{', match.start()) + 1
+ depth = 1
+ while depth:
+ depth += (code[end] == '{') - (code[end] == '}')
+ end += 1
+ return source[match.start():end]
+
+
+TURN_CHECKS = r'''
+static int camera_enabled = 1, paused;
+enum { _object_dead_bit = 0 };
+static struct unit_fixture { struct { unsigned long damage_flags; } object; } living;
+static struct unit_fixture *unit_get(long index) { CHECK(index != NONE); return &living; }
+static int player_control_camera_control_is_active(void) { return camera_enabled && !paused; }
+static void turn_control_dispatch(void) { @@TURN_CONTROL@@ }
+
+static void note_idle(void) {
+ const real_euler_angles2d still = { 0.f, 0.f };
+ const real_vector2d idle = { 0.f, 0.f };
+ player_control_action_test_note(0, &still, &idle, 0.f);
+}
+static void tutorial_reset(void) {
+ reset_state(); settings.snap_angle = (float)M_PI / 4.f;
+ vr.snap_latched = 0; camera_enabled = 1; paused = inhibited = cinematic = 0;
+ living.object.damage_flags = 0; player->unit_index = 7; local_player_index = 0;
+ player_control_action_test_reset();
+}
+static void check_snap_and_reset(void) {
+ float yaw, pitch, before;
+ tutorial_reset();
+ /* Reproduce input-disabled cryopod: the script freezes game-facing,
+ but free look and the displayed body-yaw turn remain permitted. */
+ turn_control_dispatch(); CHECK(vr.turn_control_allowed);
+ tutorial_turn_input(0.9f, 0.016f); CHECK(close_float(vr.body_yaw, -(float)M_PI / 4.f));
+ note_idle(); CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_right_bit));
+ CHECK(!TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ player_control_action_test_reset(); before = vr.body_yaw;
+ tutorial_turn_input(0.9f, 0.016f); note_idle();
+ CHECK(close_float(vr.body_yaw, before)); CHECK(tutorial.action_flags == 0); /* held stick */
+ tutorial_turn_input(0.f, 0.016f); tutorial_turn_input(-0.9f, 0.016f); note_idle();
+ CHECK(close_float(vr.body_yaw, 0.f));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ /* Multiple render frames between game ticks must retain both directions. */
+ player_control_action_test_reset(); tutorial_turn_input(0.f, 0.016f);
+ tutorial_turn_input(0.9f, 0.016f); tutorial_turn_input(0.f, 0.016f);
+ tutorial_turn_input(-0.9f, 0.016f); note_idle();
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_right_bit));
+ head_pose(0.f, 0.04f); note_idle(); head_pose(0.f, -0.04f); note_idle();
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_up_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_down_bit));
+ /* The actual a10 gate requires all four accumulated flags. */
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit) &&
+ TEST_FLAG(tutorial.action_flags, _player_control_look_relative_right_bit) &&
+ TEST_FLAG(tutorial.action_flags, _player_control_look_relative_up_bit) &&
+ TEST_FLAG(tutorial.action_flags, _player_control_look_relative_down_bit));
+ tutorial_reset(); head_pose(0.8f, 0.4f); vr.tutorial_turn_flags = 3;
+ tutorial.action_flags = tutorial.action_test_flags = ~0UL;
+ player_control_action_test_reset(); note_idle();
+ CHECK(tutorial.action_flags == 0 && tutorial.action_test_flags == 0);
+ CHECK(vr.tutorial_head_valid && vr.tutorial_turn_flags == 0);
+ CHECK(close_float(vr.tutorial_head_yaw, 0.8f) && close_float(vr.tutorial_head_pitch, 0.4f));
+ head_pose(0.805f, 0.405f); note_idle(); CHECK(tutorial.action_flags == 0);
+ head_pose(0.83f, 0.44f); note_idle();
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_up_bit));
+ tutorial_reset(); head_pose(0.6f, 0.2f); player_control_action_test_reset(); recentre(); note_idle();
+ CHECK(tutorial.action_flags == 0); /* rebasing origin is not deliberate looking */
+ tutorial_reset(); halo_vr_turn_control(0); head_pose(0.9f, 0.4f);
+ CHECK(!halo_vr_tutorial_look(&yaw, &pitch) && yaw == 0.f && pitch == 0.f);
+}
+static void check_smooth_and_controls(void) {
+ int exclusion;
+ float before;
+ tutorial_reset(); settings.smooth_turn = 1; settings.smooth_speed = 2.f;
+ tutorial_turn_input(0.6f, 0.25f); CHECK(close_float(vr.body_yaw, -0.25f));
+ note_idle(); CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_right_bit));
+ player_control_action_test_reset(); tutorial_turn_input(-0.6f, 0.25f); note_idle();
+ CHECK(close_float(vr.body_yaw, 0.f));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ for (exclusion = 0; exclusion < 6; exclusion++) {
+ tutorial_reset();
+ switch (exclusion) {
+ case 0: camera_enabled = 0; break;
+ case 1: paused = 1; break;
+ case 2: inhibited = 1; break;
+ case 3: cinematic = 1; break;
+ case 4: player->unit_index = NONE; break;
+ case 5: living.object.damage_flags = FLAG(_object_dead_bit); break;
+ }
+ turn_control_dispatch(); CHECK(!vr.turn_control_allowed);
+ before = vr.body_yaw; tutorial_turn_input(0.9f, 0.016f);
+ head_pose(0.9f, 0.4f); note_idle();
+ CHECK(vr.body_yaw == before && tutorial.action_flags == 0 && !vr.tutorial_head_valid);
+ }
+ tutorial_reset(); settings.gamepad_aim = 1;
+ tutorial_turn_input(0.9f, 0.016f); note_idle();
+ CHECK(vr.body_yaw == 0.f && tutorial.action_flags == 0); /* gamepad owns ordinary turn */
+ tutorial_reset(); tutorial_turn_input(NAN, 0.016f); tutorial_turn_input(0.9f, INFINITY);
+ tutorial_turn_input(0.9f, -0.01f); note_idle(); CHECK(vr.body_yaw == 0.f && tutorial.action_flags == 0);
+}
+static void check_pose_masks(void) {
+ int corruption;
+ for (corruption = 0; corruption < 16; corruption++) {
+ tutorial_reset();
+ switch (corruption) {
+ case 0: vr.initialized = 0; break;
+ case 1: vr.frame = vr.pending = 0; break;
+ case 2: vr.views.should_render = 0; break;
+ case 3: vr.views.focused = 0; break;
+ case 4: vr.views.head.valid = 0; break;
+ case 5: vr.menus = 1; break;
+ case 6: vr.origin_valid = 0; break;
+ case 7: vr.aim_synced = 0; break;
+ case 8: vr.origin_yaw = NAN; break;
+ case 9: vr.body_yaw = INFINITY; break;
+ case 10: vr.views.head.orientation[0] = NAN; break;
+ case 11: vr.views.eye[0].valid = 0; break;
+ case 12: vr.views.eye[1].orientation[3] = 0.f; break;
+ case 13: vr.views.eye[0].position[2] = INFINITY; break;
+ case 14: vr.origin_position[1] = NAN; break;
+ case 15: vr.views.eye[1].orientation[0] = NAN; break;
+ }
+ CHECK(!halo_vr_gaze_tracking_valid());
+ tutorial_turn_input(0.9f, 0.016f); note_idle();
+ CHECK(tutorial.action_flags == 0 && !vr.tutorial_head_valid && vr.tutorial_turn_flags == 0);
+ }
+ tutorial_reset(); vr.frame = 0; vr.pending = 1; CHECK(halo_vr_gaze_tracking_valid());
+ vr.views.focused = 0; tutorial_turn_input(0.9f, 0.016f);
+ vr.views.focused = 1; tutorial_turn_input(0.9f, 0.016f); note_idle();
+ CHECK(vr.body_yaw == 0.f && tutorial.action_flags == 0); /* recovery requires release */
+ tutorial_turn_input(0.f, 0.016f); tutorial_turn_input(0.9f, 0.016f); note_idle();
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_right_bit));
+ tutorial_reset(); tutorial_turn_input(0.9f, 0.016f); vr.views.focused = 0; note_idle();
+ CHECK(tutorial.action_flags == 0 && vr.tutorial_turn_flags == 0); /* queued event invalidated */
+ tutorial_reset(); vr.views.focused = 0; tutorial_turn_input(0.f, 0.016f);
+ head_pose(0.9f, 0.4f); vr.views.focused = 1; note_idle();
+ CHECK(tutorial.action_flags == 0 && vr.tutorial_head_valid); /* no game tick during loss */
+ tutorial_reset(); tutorial_turn_input(0.9f, 0.016f); halo_vr_menus(1);
+ CHECK(vr.tutorial_turn_flags == 0 && !vr.tutorial_head_valid);
+ head_pose(0.9f, 0.4f); halo_vr_menus(0); note_idle(); CHECK(tutorial.action_flags == 0);
+}
+'''
+
+
+GAZE_HARNESS = r'''
+#define _USE_MATH_DEFINES
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#define NONE (-1)
+#define TRUE 1
+#define FALSE 0
+#define FLAG(bit) (1UL << (bit))
+#define TEST_FLAG(value, bit) (((value) & FLAG(bit)) != 0)
+#define VALID_INDEX(index, count) ((index) >= 0 && (index) < (count))
+#define NUMBEROF(array) ((short)(sizeof(array) / sizeof((array)[0])))
+#define DEGREES_TO_RADIANS(value) ((value) * 0.01745329251994329577f)
+#define cosine cosf
+#define TAG_BLOCK_GET_ELEMENT(block, index, type) (&((type *)(block)->address)[index])
+typedef int boolean;
+typedef float real;
+typedef struct { float x, y, z; } real_point3d;
+typedef struct { float i, j, k; } real_vector3d;
+enum { _object_dead_bit = 0, _object_type_scenery = 6 };
+struct object_fields { unsigned long damage_flags; short type, name_index; real_point3d bounding_sphere_center; };
+struct unit_datum { struct object_fields object; struct { long player_index; real_vector3d looking_vector; } unit; };
+struct object_datum { struct object_fields object; };
+struct player_fixture { long unit_index; short local_player_index; };
+struct object_marker { struct { real_point3d position; } matrix; };
+struct scenario_object_name { char name[32]; short runtime_object_type, runtime_scenario_datum_index; };
+struct scenario { struct { long count; void *address; } object_names; };
+static struct unit_datum units[3];
+static struct object_datum target;
+static struct player_fixture players[2];
+static struct scenario_object_name fixture_names[6];
+static struct scenario scenario_fixture;
+static long global_scenario_index;
+static const char *scenario_path;
+static real_point3d sim_origin, head_origin, hand_origin;
+static real_vector3d head_forward, hand_forward;
+static int gaze_valid, pointing_valid, gaze_calls, pointing_calls, checks;
+#define CHECK(condition) do { checks++; if (!(condition)) { \
+ fprintf(stderr, "failed gaze line %d: %s\n", __LINE__, #condition); exit(1); } } while (0)
+static struct unit_datum *unit_get(long index) { CHECK(index >= 0 && index < 3); return &units[index]; }
+static struct unit_datum *unit_try_and_get(long index) { return index >= 0 && index < 3 ? &units[index] : NULL; }
+static struct object_datum *object_get(long index) { CHECK(index == 5); return &target; }
+static struct player_fixture *player_get(long index) { CHECK(index >= 0 && index < 2); return &players[index]; }
+static struct scenario *global_scenario_get(void) { return &scenario_fixture; }
+static const char *tag_get_name(long index) { CHECK(index == 9); return scenario_path; }
+static void object_get_marker_by_name(long index, const char *name, struct object_marker *marker, int count) {
+ (void)index; CHECK(!strcmp(name, "head") && count == 1); marker->matrix.position = sim_origin;
+}
+static void unit_get_head_position(long index, real_point3d *point) { (void)index; *point = target.object.bounding_sphere_center; }
+static void vector_from_points3d(const real_point3d *from, const real_point3d *to, real_vector3d *vector) {
+ vector->i = to->x - from->x; vector->j = to->y - from->y; vector->k = to->z - from->z;
+}
+static float normalize3d(real_vector3d *v) {
+ float length = sqrtf(v->i*v->i + v->j*v->j + v->k*v->k);
+ if (length > 0.f) { v->i /= length; v->j /= length; v->k /= length; }
+ return length;
+}
+static float dot_product3d(const real_vector3d *a, const real_vector3d *b) {
+ return a->i*b->i + a->j*b->j + a->k*b->k;
+}
+/* Presentation math/freshness is independently tested in the production
+ reticle harness. These boundary stubs preserve its unchanged-output and
+ living-owned-unarmed eligibility contract while testing the real callers. */
+boolean vr_render_player_gaze(long index, real_point3d *origin, real_vector3d *forward) {
+ gaze_calls++;
+ if (!gaze_valid) return FALSE;
+ CHECK(index == 0); *origin = head_origin; *forward = head_forward; return TRUE;
+}
+boolean vr_render_player_pointing_ray(long index, real_point3d *origin, real_vector3d *forward) {
+ pointing_calls++;
+ if (!pointing_valid || index != 0 || units[index].unit.player_index != 0 ||
+ players[0].local_player_index != 0 || players[0].unit_index != index ||
+ TEST_FLAG(units[index].object.damage_flags, _object_dead_bit)) return FALSE;
+ *origin = hand_origin; *forward = hand_forward; return TRUE;
+}
+@@FUNCTIONS@@
+static void reset_gaze(void) {
+ int index;
+ memset(units, 0, sizeof(units)); memset(players, 0, sizeof(players));
+ memset(&target, 0, sizeof(target)); memset(fixture_names, 0, sizeof(fixture_names));
+ for (index = 0; index < 3; index++) {
+ units[index].unit.player_index = NONE; units[index].unit.looking_vector.i = 1.f;
+ }
+ units[0].unit.player_index = 0; players[0].unit_index = 0; players[0].local_player_index = 0;
+ units[1].unit.player_index = 1; players[1].unit_index = 1; players[1].local_player_index = NONE;
+ sim_origin = (real_point3d){0.f, 0.f, 0.f}; head_origin = (real_point3d){0.4f, -0.3f, 1.7f};
+ hand_origin = (real_point3d){0.6f, -0.5f, 1.1f};
+ head_forward = (real_vector3d){0.f, 1.f, 0.f}; hand_forward = (real_vector3d){1.f, 0.f, 0.f};
+ target.object.type = _object_type_scenery; target.object.name_index = 0;
+ target.object.bounding_sphere_center = (real_point3d){5.6f, -0.5f, 1.1f};
+ snprintf(fixture_names[0].name, sizeof(fixture_names[0].name), "%s", "looking_panel_center"); scenario_fixture.object_names.count = 6;
+ scenario_fixture.object_names.address = fixture_names; global_scenario_index = 9; scenario_path = "levels\\a10\\a10";
+ gaze_valid = pointing_valid = 1; gaze_calls = pointing_calls = 0;
+}
+static void check_technician_gaze(void) {
+ real_point3d point;
+ int exclusion, angle;
+ reset_gaze(); point = (real_point3d){0.4f, 4.7f, 1.7f};
+ CHECK(unit_can_see_point(0, &point, DEGREES_TO_RADIANS(15.f))); /* HMD vs frozen sim */
+ CHECK(!unit_can_see_point(1, &point, DEGREES_TO_RADIANS(15.f))); /* remote */
+ CHECK(!unit_can_see_point(2, &point, DEGREES_TO_RADIANS(15.f))); /* AI */
+ CHECK(!unit_can_see_point(NONE, &point, DEGREES_TO_RADIANS(15.f)));
+ for (angle = 0; angle < 2; angle++) {
+ float gate = angle ? 20.f : 15.f;
+ point = (real_point3d){head_origin.x + 5.f*sinf(DEGREES_TO_RADIANS(gate - 0.1f)),
+ head_origin.y + 5.f*cosf(DEGREES_TO_RADIANS(gate - 0.1f)), head_origin.z};
+ CHECK(unit_can_see_point(0, &point, DEGREES_TO_RADIANS(gate)));
+ point.x = head_origin.x + 5.f*sinf(DEGREES_TO_RADIANS(gate + 0.1f));
+ point.y = head_origin.y + 5.f*cosf(DEGREES_TO_RADIANS(gate + 0.1f));
+ CHECK(!unit_can_see_point(0, &point, DEGREES_TO_RADIANS(gate)));
+ }
+ for (exclusion = 0; exclusion < 5; exclusion++) {
+ reset_gaze(); point = (real_point3d){0.4f, 4.7f, 1.7f};
+ switch (exclusion) {
+ case 0: units[0].unit.player_index = NONE; break;
+ case 1: units[0].object.damage_flags = FLAG(_object_dead_bit); break;
+ case 2: players[0].local_player_index = 1; break;
+ case 3: players[0].unit_index = 2; break;
+ case 4: gaze_valid = 0; break;
+ }
+ CHECK(!unit_can_see_point(0, &point, DEGREES_TO_RADIANS(15.f)));
+ CHECK(gaze_calls == (exclusion == 4));
+ point = (real_point3d){5.f, 0.f, 0.f};
+ CHECK(unit_can_see_point(0, &point, DEGREES_TO_RADIANS(15.f))); /* baseline preserved */
+ }
+}
+static void check_panel_eligibility(void) {
+ int exclusion, index;
+ static const char *const active[] = {"looking_panel_center", "looking_panel_left", "looking_panel_right",
+ "looking_panel_up", "looking_panel_down"};
+ for (index = 0; index < 5; index++) {
+ reset_gaze(); snprintf(fixture_names[0].name, sizeof(fixture_names[0].name), "%s", active[index]);
+ CHECK(hs_unit_can_see_object(0, 5, 5.f)); CHECK(pointing_calls == 1);
+ }
+ for (exclusion = 0; exclusion < 13; exclusion++) {
+ reset_gaze();
+ switch (exclusion) {
+ case 0: scenario_path = "levels\\a30\\a30"; break;
+ case 1: global_scenario_index = NONE; break;
+ case 2: target.object.type = 7; break;
+ case 3: target.object.name_index = NONE; break;
+ case 4: target.object.name_index = 6; break;
+ case 5: snprintf(fixture_names[0].name, sizeof(fixture_names[0].name), "%s", "looking_panel_center_success"); break;
+ case 6: snprintf(fixture_names[0].name, sizeof(fixture_names[0].name), "%s", "looking_panel_center_extra"); break;
+ case 7: memset(fixture_names[0].name, 'x', sizeof(fixture_names[0].name)); break;
+ case 8: pointing_valid = 0; break; /* armed, vehicle, cinematic, stale pose */
+ case 9: units[0].object.damage_flags = FLAG(_object_dead_bit); break;
+ case 10: players[0].local_player_index = 1; break;
+ case 11: players[0].unit_index = 2; break;
+ case 12: units[0].unit.player_index = NONE; break;
+ }
+ CHECK(!hs_unit_can_see_object(0, 5, 5.f));
+ }
+ reset_gaze(); CHECK(!hs_unit_can_see_object(0, 5, 6.f)); CHECK(pointing_calls == 0);
+ CHECK(!hs_unit_can_see_object(1, 5, 5.f)); /* remote retains simulation result */
+ CHECK(!hs_unit_can_see_object(NONE, NONE, 5.f));
+ reset_gaze(); head_forward = hand_forward; head_origin = hand_origin;
+ pointing_valid = 0; CHECK(hs_unit_can_see_object(0, 5, 5.f)); CHECK(pointing_calls == 0); /* HMD also accepted */
+}
+static void check_panel_cone_origin_height(void) {
+ int height, distance, outside, axis;
+ for (height = -2; height <= 4; height++) for (distance = 1; distance <= 12; distance++) {
+ for (outside = 0; outside < 2; outside++) for (axis = 0; axis < 2; axis++) {
+ float angle = DEGREES_TO_RADIANS(outside ? 5.1f : 4.9f);
+ reset_gaze(); hand_origin.z = (float)height * 0.6f;
+ target.object.bounding_sphere_center.x = hand_origin.x + distance * cosf(angle);
+ target.object.bounding_sphere_center.y = hand_origin.y + (axis ? 0.f : distance * sinf(angle));
+ target.object.bounding_sphere_center.z = hand_origin.z + (axis ? distance * sinf(angle) : 0.f);
+ CHECK(hs_unit_can_see_object(0, 5, 5.f) == !outside);
+ }
+ }
+ reset_gaze(); target.object.bounding_sphere_center = hand_origin;
+ CHECK(!hs_unit_can_see_object(0, 5, 5.f)); /* degenerate direction */
+}
+int main(void) {
+ check_technician_gaze(); check_panel_eligibility(); check_panel_cone_origin_height();
+ printf("PASS: %d production tutorial HMD / named-panel ray assertions\n", checks);
+ return 0;
+}
+'''
+
+
+def compile_run(harness, compiler, vcvars):
+ with tempfile.TemporaryDirectory(prefix='halo-vr-tutorial-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory)
+ source = temp / 'test.c'
+ exe = temp / ('test.exe' if os.name == 'nt' else 'test')
+ source.write_text(harness)
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/D_CRT_SECURE_NO_WARNINGS',
+ '/DHALO_VR', '/I' + str(ROOT / 'port/linux/src'), '/Fe:' + str(exe),
+ '/Fo:' + str(temp / 'test.obj'), str(source)]
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported compiler/temp shell metacharacter')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\n'
+ 'if errorlevel 1 exit /b 1\n' + subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-DHALO_VR',
+ '-I' + str(ROOT / 'port/linux/src'), str(source),
+ *([] if os.name == 'nt' else ['-lm']), '-o', str(exe)]
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(exe)], cwd=temp, check=True)
+
+
+def verify_xbox_a10(path):
+ """Bounded passive inspection; return facts, never distribute retail scripts."""
+ raw = path.read_bytes()
+ assert 2048 < len(raw) < 600_000_000 and raw[:4] == b'daeh'
+ declared = struct.unpack_from('<I', raw, 8)[0]
+ assert 2048 < declared < 600_000_000
+ stream = zlib.decompressobj()
+ expanded = stream.decompress(raw[2048:], declared - 2048 + 1)
+ assert stream.eof and not stream.unconsumed_tail and len(expanded) == declared - 2048
+ data = raw[:2048] + expanded
+ assert struct.unpack_from('<I', data, 4)[0] == 5
+ def u32(offset):
+ assert 0 <= offset <= len(data) - 4
+ return struct.unpack_from('<I', data, offset)[0]
+ index = u32(16)
+ table = index + 36
+ magic = u32(index) - table
+ tag_count = u32(index + 12)
+ assert 0 < tag_count < 65536 and table + tag_count * 32 <= len(data)
+ scenario_handle = u32(index + 4)
+ scenario = u32(table + (scenario_handle & 65535)*32 + 20) - magic
+ assert 0 <= scenario < len(data) - 1188
+ syntax, strings = u32(scenario + 1152) - magic, u32(scenario + 1172) - magic
+ strings_size, syntax_size = u32(scenario + 1160), u32(scenario + 1140)
+ assert 0 <= strings <= len(data) - strings_size and 0 <= syntax <= len(data) - syntax_size
+ count = struct.unpack_from('<H', data, syntax + 46)[0]
+ assert 56 + count * 20 <= syntax_size
+ nodes = [struct.unpack_from('<4H3I', data, syntax + 56 + n*20) for n in range(count)]
+ def string(offset):
+ assert 0 <= offset < strings_size
+ end = data.index(b'\0', strings + offset, strings + strings_size)
+ return data[strings + offset:end].decode('ascii')
+ def expression(handle, ancestors=frozenset()):
+ n = handle & 65535
+ assert n < count and n not in ancestors
+ node = nodes[n]
+ if node[3] & 1:
+ if node[3] & 4:
+ return string(node[5]) if node[5] < strings_size else node[6]
+ if node[2] == 6:
+ return struct.unpack('<f', struct.pack('<I', node[6]))[0]
+ if node[2] == 5:
+ return bool(node[6] & 1)
+ return string(node[5]) if node[5] < strings_size else node[6]
+ children = []
+ child = node[6]
+ seen = set()
+ while child != 0xffffffff:
+ slot = child & 65535
+ assert slot < count and slot not in seen
+ seen.add(slot)
+ children.append(expression(child, ancestors | {n}))
+ child = nodes[slot][4]
+ return children
+ script_count, script_table = u32(scenario + 1180), u32(scenario + 1184) - magic
+ assert 0 < script_count < 4096 and 0 <= script_table <= len(data) - script_count*92
+ scripts = {}
+ for n in range(script_count):
+ offset = script_table + n*92
+ name = data[offset:offset+32].split(b'\0', 1)[0].decode('ascii')
+ if name in ('tutorial_looking', 'tutorial_setup', 'test_looking_tech', 'mission_a10') or name.startswith('test_looking_cycle_'):
+ scripts[name] = expression(u32(offset + 36))
+ def calls(tree, name):
+ found = []
+ if isinstance(tree, list):
+ if tree and tree[0] == name:
+ found.append(tree)
+ for child in tree:
+ found.extend(calls(child, name))
+ return found
+ look = scripts['tutorial_looking']
+ assert calls(look, 'player_action_test_reset')
+ for direction in ('left', 'right', 'up', 'down'):
+ assert calls(look, 'player_action_test_look_relative_' + direction)
+ assert any(len(call) == 2 and call[1] is False for call in calls(scripts['tutorial_setup'], 'player_enable_input'))
+ for name in ('center', 'left', 'right', 'up', 'down'):
+ gate = calls(scripts['test_looking_cycle_' + name], 'objects_can_see_object')
+ assert any('looking_panel_' + name in call and call[-1] == 5.0 for call in gate)
+ assert any(call[-1] == 15.0 for call in calls(scripts['test_looking_tech'], 'objects_can_see_object'))
+ assert any(call[-1] == 20.0 for call in calls(scripts['test_looking_tech'], 'objects_can_see_object'))
+ # The five actual named targets are scenery; success variants are different names.
+ name_count, name_table = u32(scenario + 516), u32(scenario + 520) - magic
+ assert 5 <= name_count < 65536 and 0 <= name_table <= len(data) - name_count*36
+ for n, suffix in enumerate(('center', 'right', 'left', 'up', 'down')):
+ offset = name_table + n*36
+ assert data[offset:offset+32].split(b'\0', 1)[0].decode('ascii') == 'looking_panel_' + suffix
+ assert struct.unpack_from('<h', data, offset+32)[0] == 6
+ # Check tutorial routing remains authored by the map: co-op and difficulty.
+ mission = scripts['mission_a10']
+ assert calls(mission, 'game_is_cooperative') and calls(mission, 'game_difficulty_get')
+ print('PASS: passive original Xbox a10 predicates: input-disabled look, all four direction flags, '
+ 'five named scenery targets at 5 degrees, technician 15/20 degrees, authored difficulty/co-op routing')
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ parser.add_argument('--xbox-a10', type=Path, help='Authorized original Xbox a10.map, passive read only')
+ args = parser.parse_args()
+ vr = (ROOT / 'port/linux/src/vr.c').read_text()
+ player = (ROOT / 'source/game/player_control.c').read_text()
+ hook, aim_guard = walking.source_checks(vr, player)
+ dispatch_function = function(player, 'handle_one_player_input')
+ start = dispatch_function.index('if (local_player_index == 0)')
+ opening = dispatch_function.index('{', start)
+ # This integration block is deliberately simple and has one nested-free body.
+ gate = dispatch_function[start:dispatch_function.index('}', opening) + 1]
+ assert 'halo_vr_turn_control' in gate and 'player_control_camera_control_is_active()' in gate
+ assert 'player_input_is_enabled' not in walking.masked(gate)
+ assert player.index('halo_vr_turn_control(') < player.index('get_local_player_input_blob(', player.index('static void handle_one_player_input('))
+ harness = walking.HARNESS
+ harness = harness.replace('int tutorial_head_valid, tutorial_turn_flags, turn_control_allowed;',
+ 'int tutorial_head_valid, tutorial_turn_flags, turn_control_allowed, snap_latched;')
+ harness = harness.replace('int head_movement, controller_aim, gamepad_aim, gamepad_snap, standing;',
+ 'int head_movement, controller_aim, gamepad_aim, gamepad_snap, standing, smooth_turn;')
+ harness = harness.replace('float snap_angle, player_height;', 'float snap_angle, player_height, smooth_speed;')
+ harness = harness.replace('real_euler_angles2d desired_angles;', 'long unit_index; real_euler_angles2d desired_angles;')
+ harness = harness.replace('unsigned long action_flags;', 'unsigned long action_flags, action_test_flags;')
+ functions = '\n\n'.join(function(vr, name) for name in (
+ 'quaternion_rotate', 'quaternion_yaw', 'game_axes', 'recentre', 'halo_vr_locomotion',
+ 'halo_vr_gaze_tracking_valid', 'halo_vr_turn_control', 'halo_vr_tutorial_turn',
+ 'tutorial_turn_input', 'halo_vr_tutorial_look', 'halo_vr_tutorial_reset',
+ 'halo_vr_aim', 'halo_vr_aim_release', 'halo_vr_menus'))
+ functions = '#define HALO_VR_TUTORIAL_TURN_LEFT 1\n#define HALO_VR_TUTORIAL_TURN_RIGHT 2\n' + functions
+ harness = harness.replace('@@FUNCTIONS@@', functions).replace('@@HOOK@@', hook).replace('@@AIM_GUARD@@', aim_guard)
+ harness = harness.replace('@@TUTORIAL@@', function(player, 'player_control_action_test_note') + '\n\n' +
+ function(player, 'player_control_action_test_reset'))
+ harness = harness.replace('int main(void) {', TURN_CHECKS.replace('@@TURN_CONTROL@@', gate) + '\nint main(void) {\n'
+ ' check_snap_and_reset(); check_smooth_and_controls(); check_pose_masks();')
+ gaze = GAZE_HARNESS.replace('@@FUNCTIONS@@', '\n\n'.join((
+ function((ROOT / 'source/units/units.c').read_text(), 'unit_can_see_point'),
+ function((ROOT / 'source/hs/hs_library_external.c').read_text(), 'hs_vr_calibration_panel'),
+ function((ROOT / 'source/hs/hs_library_external.c').read_text(), 'hs_unit_can_see_object'))))
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ base = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(base.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1])
+ vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No C compiler; select --cc')
+ compile_run(harness, compiler, vcvars)
+ compile_run(gaze, compiler, vcvars)
+ if args.xbox_a10:
+ verify_xbox_a10(args.xbox_a10)
+ else:
+ print('SKIP: optional original Xbox a10 data verification (provide --xbox-a10)')
+ print('PASS: unchanged armed aim / Xbox controls; no tutorial script, difficulty or skip changes')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_tutorial_buttons.py b/tools/test_vr_tutorial_buttons.py
new file mode 100644
index 0000000000000000000000000000000000000000..29f780a9132add7a896623679d28377b306220b3
--- /dev/null
+++ b/tools/test_vr_tutorial_buttons.py
@@ -0,0 +1,288 @@
+#!/usr/bin/env python3
+"""Run production tutorial button consumption and inversion persistence logic.
+
+The same extracted input function is compiled with and without its VR branch,
+so the legacy melee leak and unchanged flat behavior are tested directly. The
+profile setter is also extracted; its disk/HUD boundaries are controlled stubs,
+not a claim that the reported headset crash has been reproduced. VR activity
+links a separate translation unit with the real vr.h declaration and extracted
+vr.c definition; no invented activity API is provided by this harness.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def function(text, name):
+ hidden = re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"',
+ lambda match: re.sub(r'[^\n]', ' ', match.group()), text, flags=re.S)
+ found = re.search(r'(?:static\s+)?(?:boolean|void|int)\s+' + re.escape(name) +
+ r'\s*\([^;]*?\)\s*\{', hidden)
+ if not found:
+ raise AssertionError('Missing production function: ' + name)
+ start = found.start()
+ opening = hidden.index('{', start)
+ depth, end = 1, opening + 1
+ while depth:
+ depth += (hidden[end] == '{') - (hidden[end] == '}')
+ end += 1
+ return text[start:end]
+
+
+HARNESS = r'''
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <wchar.h>
+@@VR_HEADER@@
+typedef int boolean;
+#define FALSE 0
+#define TRUE 1
+#define NONE (-1)
+#define FLAG(bit) (1u << (bit))
+#define TEST_FLAG(value,bit) ((value) & FLAG(bit))
+#define SET_FLAG(value,bit,on) ((value) = ((value) & ~FLAG(bit)) | ((on) ? FLAG(bit) : 0u))
+#define UNICODE_STRING_LIST_TAG 123
+enum {
+ _player_control_action_bit, _player_control_accept_bit, _player_control_back_bit,
+ _player_control_zoom_bit, _player_control_input_zoom_bit,
+ _player_control_rotate_weapons_bit, _player_control_rotate_grenades_bit
+};
+enum {
+ _unit_control_action_bit, _unit_control_jump_bit, _unit_control_use_equipment_bit,
+ _unit_control_throw_grenade_bit, _unit_control_weapon_secondary_trigger_bit,
+ _unit_control_weapon_primary_trigger_bit
+};
+struct facing { float yaw,pitch; };
+struct throttle { float i,j; };
+struct input_blob {
+ boolean accept,back; unsigned unit_control_flags,player_control_flags;
+ struct facing facing_delta; struct throttle throttle; float primary_trigger;
+};
+struct player_control_globals_data {
+ unsigned action_flags,action_test_flags,suppressed_action_flags;
+};
+static struct player_control_globals_data fixture_globals;
+static struct player_control_globals_data *player_control_globals = &fixture_globals;
+static boolean controls_swapped;
+static int cinematic_skip, coop_skip, voted, skipped, votes, notes, checks;
+void fixture_set_vr_running(int initialized);
+static int cinematic_can_be_skipped(void) { return cinematic_skip; }
+static int network_coop_skip_offered(void) { return coop_skip; }
+static int network_coop_vote_skip(void) { ++votes; return voted; }
+static void main_skip_cinematic(void) { ++skipped; }
+static void player_control_action_test_note(unsigned flags,const struct facing *f,
+ const struct throttle *t,float trigger) {
+ (void)flags; (void)f; (void)t; (void)trigger; ++notes;
+}
+struct player_profile { struct { boolean invert_look; } controller_settings; };
+static struct {
+ struct { struct player_profile profile; long active_profile_index; } local_players[4];
+} player_ui_globals;
+static int saved, messaged, applied, saved_inversion, applied_inversion;
+static long string_tag = 22;
+static const wchar_t message_text[] = L"Profile saved";
+static long tag_loaded(int group,const char *path) {
+ if (group != UNICODE_STRING_LIST_TAG || strcmp(path,"ui\\shell\\strings\\temp_strings")) abort();
+ return string_tag;
+}
+static const wchar_t *unicode_string_list_get_string(long tag,long index) {
+ if(tag != string_tag || index != 1) abort(); return message_text;
+}
+static void hud_message_to_all(const wchar_t *message) { if(!message) abort(); ++messaged; }
+static void player_profile_save(long index,const struct player_profile *profile) {
+ if(index != 7 || !profile) abort(); ++saved; saved_inversion=profile->controller_settings.invert_look;
+}
+static void set_local_player_controls_from_player_profile(short local) {
+ if(local != 0) abort(); ++applied;
+ applied_inversion=player_ui_globals.local_players[0].profile.controller_settings.invert_look;
+}
+#define CHECK(condition) do { ++checks; if(!(condition)) { \
+ fprintf(stderr,"FAIL line %d: %s\n",__LINE__,#condition); exit(1); } } while(0)
+
+/* Compile the actual legacy branch alongside the VR-enabled production code. */
+#undef HALO_VR
+@@LEGACY@@
+#define HALO_VR 1
+@@INPUT@@
+@@BACK@@
+@@ACCEPT@@
+@@INVERT@@
+
+static struct input_blob button_input(boolean down) {
+ struct input_blob input={0};
+ input.back=down;
+ input.unit_control_flags=FLAG(_unit_control_use_equipment_bit) |
+ FLAG(_unit_control_throw_grenade_bit) | FLAG(_unit_control_weapon_secondary_trigger_bit) |
+ FLAG(_unit_control_weapon_primary_trigger_bit);
+ input.player_control_flags=FLAG(_player_control_rotate_weapons_bit) |
+ FLAG(_player_control_rotate_grenades_bit) | FLAG(_player_control_input_zoom_bit);
+ return input;
+}
+static void consumed(const struct input_blob *input) {
+ CHECK(!TEST_FLAG(input->unit_control_flags,_unit_control_use_equipment_bit));
+ CHECK(!TEST_FLAG(input->unit_control_flags,_unit_control_throw_grenade_bit));
+ CHECK(!TEST_FLAG(input->unit_control_flags,_unit_control_weapon_secondary_trigger_bit));
+ CHECK(!TEST_FLAG(input->player_control_flags,_player_control_rotate_weapons_bit));
+ CHECK(!TEST_FLAG(input->player_control_flags,_player_control_rotate_grenades_bit));
+ CHECK(!TEST_FLAG(input->player_control_flags,_player_control_input_zoom_bit));
+ CHECK(TEST_FLAG(input->unit_control_flags,_unit_control_weapon_primary_trigger_bit));
+}
+static void legacy_leak_and_release(void) {
+ int swapped;
+ for(swapped=0;swapped<2;++swapped) {
+ struct input_blob input;
+ controls_swapped=swapped; fixture_set_vr_running(1); memset(&fixture_globals,0,sizeof(fixture_globals));
+ CHECK(halo_vr_running());
+ CHECK(!player_control_action_test_back()); input=button_input(TRUE);
+ original_action_test_input(&input);
+ CHECK(TEST_FLAG(input.unit_control_flags,_unit_control_use_equipment_bit));
+ memset(&fixture_globals,0,sizeof(fixture_globals)); CHECK(!player_control_action_test_back());
+ input=button_input(TRUE); player_control_action_test_check_reset_input_blob(&input);
+ consumed(&input); CHECK(player_control_action_test_back());
+ CHECK(TEST_FLAG(fixture_globals.suppressed_action_flags,_player_control_back_bit));
+ /* Script reset while B remains held must retain suppression. */
+ fixture_globals.action_test_flags=0; fixture_globals.action_flags=0;
+ input=button_input(TRUE); player_control_action_test_check_reset_input_blob(&input);
+ consumed(&input); CHECK(TEST_FLAG(fixture_globals.suppressed_action_flags,_player_control_back_bit));
+ input=button_input(FALSE); player_control_action_test_check_reset_input_blob(&input);
+ consumed(&input); CHECK(!TEST_FLAG(fixture_globals.suppressed_action_flags,_player_control_back_bit));
+ input=button_input(TRUE); player_control_action_test_check_reset_input_blob(&input);
+ CHECK(TEST_FLAG(input.unit_control_flags,_unit_control_use_equipment_bit));
+ CHECK(TEST_FLAG(input.unit_control_flags,_unit_control_throw_grenade_bit));
+ CHECK(TEST_FLAG(input.player_control_flags,_player_control_rotate_weapons_bit));
+ CHECK(TEST_FLAG(input.player_control_flags,_player_control_rotate_grenades_bit));
+ }
+}
+static void flat_equivalence(void) {
+ unsigned requested,suppressed; int swapped,accept,back;
+ fixture_set_vr_running(0);
+ CHECK(!halo_vr_running());
+ for(requested=0;requested<8;++requested) for(suppressed=0;suppressed<8;++suppressed)
+ for(swapped=0;swapped<2;++swapped) for(accept=0;accept<2;++accept) for(back=0;back<2;++back) {
+ struct player_control_globals_data initial={0}, expected;
+ struct input_blob before=button_input(back), original, current;
+ before.accept=accept; before.unit_control_flags|=FLAG(_unit_control_action_bit)|FLAG(_unit_control_jump_bit);
+ initial.action_test_flags=requested;
+ initial.suppressed_action_flags=suppressed;
+ controls_swapped=swapped; fixture_globals=initial; original=before;
+ original_action_test_input(&original); expected=fixture_globals;
+ fixture_globals=initial; current=before; player_control_action_test_check_reset_input_blob(&current);
+ CHECK(!memcmp(&original,&current,sizeof(current)));
+ CHECK(!memcmp(&expected,&fixture_globals,sizeof(fixture_globals)));
+ }
+}
+static void inversion_and_accept(void) {
+ struct input_blob input={0};
+ fixture_set_vr_running(1); controls_swapped=0; memset(&fixture_globals,0,sizeof(fixture_globals));
+ memset(&player_ui_globals,0,sizeof(player_ui_globals));
+ player_ui_globals.local_players[0].active_profile_index=7;
+ player0_look_invert_pitch(TRUE);
+ CHECK(saved==1 && applied==1 && messaged==1 && saved_inversion && applied_inversion);
+ CHECK(!player_control_action_test_accept()); CHECK(!player_control_action_test_back());
+ input=button_input(TRUE); player_control_action_test_check_reset_input_blob(&input);
+ CHECK(!player_control_action_test_accept() && player_control_action_test_back());
+ consumed(&input);
+ /* This is the tutorial's Back branch; persistence remains the real setter. */
+ player0_look_invert_pitch(FALSE);
+ CHECK(saved==2 && applied==2 && messaged==2 && !saved_inversion && !applied_inversion);
+ CHECK(!player_ui_globals.local_players[0].profile.controller_settings.invert_look);
+ player0_look_invert_pitch(TRUE);
+ CHECK(saved==3 && applied==3 && saved_inversion && applied_inversion);
+ player_ui_globals.local_players[0].active_profile_index=NONE;
+ player0_look_invert_pitch(FALSE);
+ CHECK(saved==3 && messaged==3 && applied==4 && !applied_inversion);
+ player_ui_globals.local_players[0].active_profile_index=7; string_tag=NONE;
+ player0_look_invert_pitch(TRUE); CHECK(saved==4 && messaged==4 && applied==5);
+ memset(&fixture_globals,0,sizeof(fixture_globals)); input=(struct input_blob){0}; input.accept=TRUE;
+ input.unit_control_flags=FLAG(_unit_control_jump_bit)|FLAG(_unit_control_action_bit);
+ CHECK(!player_control_action_test_accept());
+ player_control_action_test_check_reset_input_blob(&input);
+ CHECK(player_control_action_test_accept() && !player_control_action_test_back());
+ CHECK(TEST_FLAG(input.unit_control_flags,_unit_control_jump_bit));
+ cinematic_skip=1; coop_skip=1; voted=0;
+ player_control_action_test_check_reset_input_blob(&input); CHECK(votes==1 && skipped==1);
+ voted=1; player_control_action_test_check_reset_input_blob(&input); CHECK(votes==2 && skipped==1);
+}
+int main(void) {
+ legacy_leak_and_release(); flat_equivalence(); inversion_and_accept();
+ CHECK(notes>0);
+ printf("PASS: %d production tutorial-button/inversion assertions\n",checks);
+ return 0;
+}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ args = parser.parse_args()
+ controls = (ROOT / 'source/game/player_control.c').read_text()
+ profile = (ROOT / 'source/interface/player_ui.c').read_text()
+ vr = (ROOT / 'port/linux/src/vr.c').read_text()
+ vr_header = ROOT / 'port/linux/src/vr.h'
+ header = vr_header.read_text()
+ assert 'input->back = gamepad->buttons[_gamepad_analog_button_b];' in controls
+ assert 'player_profile_save(' in function(profile, 'player0_look_invert_pitch')
+ actual = function(controls, 'player_control_action_test_check_reset_input_blob')
+ assert 'halo_vr_active' not in actual, 'Do not mask a nonexistent VR activity API.'
+ assert 'halo_vr_running() != 0' in actual
+ assert re.search(r'\bint\s+halo_vr_running\s*\(\s*void\s*\)\s*;', header)
+ production_activity = function(vr, 'halo_vr_running')
+ include = '#include "' + vr_header.as_posix() + '"'
+ activity_source = (include + '\nstatic struct { int initialized; } vr;\n' +
+ production_activity + '\nvoid fixture_set_vr_running(int initialized) {\n' +
+ ' vr.initialized = initialized;\n}\n')
+ harness = HARNESS.replace('@@INPUT@@', actual).replace('@@LEGACY@@', actual.replace(
+ 'player_control_action_test_check_reset_input_blob', 'original_action_test_input'))
+ harness = harness.replace('@@VR_HEADER@@', include)
+ for placeholder,name in (('@@BACK@@','player_control_action_test_back'),
+ ('@@ACCEPT@@','player_control_action_test_accept')):
+ harness = harness.replace(placeholder,function(controls,name))
+ harness = harness.replace('@@INVERT@@',function(profile,'player0_look_invert_pitch'))
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ installations = Path(os.environ.get('ProgramFiles','C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(installations.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1]); vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No C11 host compiler found; select one with --cc.')
+ compiler = str(Path(shutil.which(compiler) or compiler).resolve())
+ with tempfile.TemporaryDirectory(prefix='halo-vr-buttons-',ignore_cleanup_errors=True) as directory:
+ temp=Path(directory); source=temp/'test.c'; executable=temp/('test.exe' if os.name=='nt' else 'test')
+ activity=temp/'vr_activity.c'; activity_object=temp/('vr_activity.obj' if os.name=='nt' else 'vr_activity.o')
+ source.write_text(harness)
+ activity.write_text(activity_source)
+ if Path(compiler).name.lower() in ('cl','cl.exe'):
+ activity_command=[compiler,'/nologo','/std:c11','/W4','/WX','/c',
+ '/Fo:'+str(activity_object),str(activity)]
+ command=[compiler,'/nologo','/std:c11','/W4','/WX','/D_CRT_SECURE_NO_WARNINGS',
+ '/Fe:'+str(executable),'/Fo:'+str(temp/'test.obj'),str(source),str(activity_object)]
+ else:
+ activity_command=[compiler,'-std=c11','-Wall','-Wextra','-Werror','-c',str(activity),'-o',str(activity_object)]
+ command=[compiler,'-std=c11','-Wall','-Wextra','-Werror',str(source),str(activity_object),'-o',str(executable)]
+ for index,step in enumerate((activity_command,command)):
+ if vcvars:
+ for path in (vcvars,Path(compiler),temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported shell metacharacter in compiler/temp path.')
+ batch=temp/('compile-'+str(index)+'.cmd')
+ batch.write_text('@echo off\ncall "'+str(vcvars)+'" x64 >nul\nif errorlevel 1 exit /b 1\n'+
+ subprocess.list2cmdline(step)+'\n')
+ step=['cmd.exe','/d','/c',str(batch)]
+ subprocess.run(step,cwd=temp,check=True)
+ subprocess.run([str(executable)],cwd=temp,check=True)
+
+
+if __name__ == '__main__':
+ main()