mirror of
https://github.com/ClassicWasTaken/HaloCE-SteamFrame.git
synced 2026-10-26 11:00:34 +01:00
6392 lines
337 KiB
Diff
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. 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,¢er,&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, ¢er, &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, ¢er, &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 ⌖ }
|
|
+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(¤t);
|
|
+ CHECK(!memcmp(&original,¤t,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()
|