Files
ClassicWasTaken--HaloSteamF…/scripts/check-experimental-engine.py
T

513 lines
33 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Offline checks of the pinned experimental engine and shipped local patch.
Reads a local vanilla/patched checkout. Applies the patch only to a temporary
projection and generates the real ARM64 Ninja graph without fetching tools,
compiling a game, using retail data, or connecting a headset.
"""
from __future__ import annotations
import argparse
import io
import json
from pathlib import Path, PurePosixPath
import re
import subprocess
import sys
import tarfile
import tempfile
import xml.etree.ElementTree as ET
ROOT = Path(__file__).resolve().parents[1]
SOURCE_COMMIT = "2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4"
PATCH_FILES = frozenset((
"port/linux/src/vr.c", "port/linux/src/vr.h", "port/linux/src/vr_locomotion.h",
"port/linux/src/port_config.c", "source/game/player_control.c", "tools/test_vr_locomotion.py",
"port/linux/game/menu_functions.c", "port/linux/game/vr_render.c",
"port/linux/src/d3d8_gl.c", "port/linux/src/nv2a_psh.c", "port/linux/src/xgpu.h",
"source/rasterizer/xbox/rasterizer_xbox_lights.c",
"tools/test_online_coop_menu.py", "tools/test_vr_sun_glow.py",
"source/items/weapons.c", "source/items/weapons.h",
"source/game/aim_assist.c", "source/game/aim_assist.h",
"tools/test_vr_projectile_reticle.py",
"port/assets/menus/ce/main_menu.multiplayer_type_select.coop.xml", "tools/port_settings.py",
"source/units/units.c", "source/units/units.h",
"source/hs/hs_library_external.c", "port/linux/arm64/host_vr.c",
"tools/test_vr_tutorial.py", "tools/test_vr_tracking.py", "tools/test_vr_tutorial_buttons.py",
"source/objects/object_lights.c", "tools/test_vr_flashlight.py", "tools/test_vr_menu.py",
"tools/android_build.py", "tools/test_build_sources.py",
))
class EngineCheckError(RuntimeError):
pass
def require(condition, message):
if not condition:
raise EngineCheckError(message)
def git(source, *arguments, check=True):
result = subprocess.run(["git", "-C", str(source), *arguments], capture_output=True, timeout=30)
if check:
require(result.returncode == 0, "A local Git source check failed; no engine files were changed.")
return result
def patch_files(text):
files = re.findall(r"^diff --git a/(\S+) b/(\S+)$", text, re.M)
require(bool(files), "The local engine patch has no ordinary file diffs.")
for before, after in files:
require(before == after and after in PATCH_FILES,
"The engine patch changes a path outside the reviewed integration files.")
return frozenset(after for _, after in files)
def c_function(text, name):
# Keep offsets while hiding comments and string contents from brace counting.
comments = re.compile(r"/\*.*?\*/|//[^\n]*", re.S)
clean = comments.sub(lambda match: " " * len(match.group()), text)
masked = re.sub(r'"(?:\\.|[^"\\])*"', lambda match: " " * len(match.group()), clean)
start = re.search(r"\b" + re.escape(name) + r"\s*\([^;{}]*\)\s*\{", masked)
require(start is not None, "Missing engine function: " + name)
depth = 1
for offset in range(start.end(), len(masked)):
depth += (masked[offset] == "{") - (masked[offset] == "}")
if not depth:
return clean[start.end():offset]
raise EngineCheckError("Unterminated engine function: " + name)
def check_texture_binding(text):
body = c_function(text, "bind_textures")
loops = list(re.finditer(r"for\s*\(stage\s*=\s*0;\s*stage\s*<\s*D3DTSS_MAXSTAGES;\s*stage\+\+\)", body))
bindings = list(re.finditer(r"\bstate_texture\s*\(", body))
require(len(loops) == 2 and len(bindings) == 1 and bindings[0].start() > loops[1].start(),
"Texture binding is not delayed until the second complete stage pass.")
resolve = body.find("xgpu_texture_get(")
require(loops[0].start() < resolve < loops[1].start(),
"Texture uploads must finish before the final stage-binding pass.")
require("state_texture(stage, gl_targets[stage], gl_textures[stage])" in body,
"Final texture bindings do not use the resolved per-stage arrays.")
def check_upstream(source):
read = lambda path: (source / path).read_text(encoding="utf-8")
limits = read("port/linux/include/halo_port_limits.h")
require(re.search(r"^#define HALO_PORT_NETWORK_VERSION 17\s*$", limits, re.M),
"The candidate must use native peer protocol 17.")
check_texture_binding(read("port/linux/src/d3d8_gl.c"))
shader = read("port/linux/src/nv2a_vsh.c")
require("clip_position = oPos" in shader and "clip_captured = true" in shader
and "if (!(abs(position.w) > 0.0))" in shader
and "position = vec4(0.0, 0.0, 0.0, -1.0)" in shader,
"The pinned vertex clip-position safeguards are missing.")
distributed = read("port/linux/game/network_distributed.c")
coop = read("port/linux/game/network_coop.c")
for hook in ("network_coop_host_tick", "network_coop_client_tick", "network_coop_handle_presentation", "network_coop_handle_events"):
require(hook + "(" in distributed and hook in coop, "Missing integrated co-op hook: " + hook)
config = read("port/linux/src/port_config.c")
require('"network.coop_enemies_mode"' in config and '"network.coop_enemies"' in config
and '"network.brokers_file"' in config, "Co-op/broker configuration is missing.")
require((source / "port/assets/network/brokers.txt").is_file(), "The candidate broker list is missing.")
require("saved games of builds before it no longer load" in read("port/linux/include/halo_port_capacity.h"),
"The candidate saved-game layout change needs explicit compatibility handling.")
def check_controls(source):
read = lambda path: (source / path).read_text(encoding="utf-8")
vr = read("port/linux/src/vr.c")
config = read("port/linux/src/port_config.c")
require(re.search(r'"vr\.movement",\s*_config_string,\s*"\\"head\\"",\s*"HALO_VR_MOVEMENT"', config),
"Head-directed walking is not the configurable default.")
movement = c_function(vr, "halo_vr_locomotion")
for guard in ("!vr.initialized", "!settings.head_movement", "!settings.controller_aim", "settings.gamepad_aim"):
require(guard in movement, "Missing unchanged-input mode guard: " + guard)
for guard in ("vr.views.focused", "vr.views.head.valid", "vr.origin_valid", "vr.aim_synced", "!vr.menus", "halo_vr_locomotion_orientation"):
require(guard in movement, "Missing valid-tracking locomotion guard: " + guard)
require("halo_vr_locomotion_project(" in movement, "The tested production movement math is not wired into VR input.")
player = read("source/game/player_control.c")
handling = c_function(player, "handle_one_player_input")
call = handling.find("halo_vr_locomotion(player->desired_angles.yaw")
require(call >= 0 and handling.find("player->desired_angles.yaw = vr_yaw") < call
< handling.find("player->throttle = input.throttle"),
"Movement must be transformed after aiming and before the player/action throttle is stored.")
guard = handling[max(0, call - 300):call]
for condition in ("local_player_index == 0", "unit->object.parent_object_index == NONE", "!director_inhibited_facing", "!cinematic_in_progress"):
require(condition in guard, "Missing local on-foot movement gate: " + condition)
gamepad = c_function(vr, "halo_vr_gamepad")
require("XINPUT_GAMEPAD_WHITE, buttons & VR_BUTTON_RIGHT_BUMPER" in gamepad
and "XINPUT_GAMEPAD_BLACK, buttons & VR_BUTTON_LEFT_BUMPER" in gamepad,
"The agreed Xbox bumper layout was not preserved.")
require("halo_vr_tutorial_look(" in c_function(player, "player_control_action_test_note"),
"The physical-head tutorial hook was not ported to the new co-op action test.")
c_function(vr, "halo_vr_tutorial_look")
require((source / "port/linux/src/vr_locomotion.h").is_file(), "The production locomotion header is missing.")
def check_stereo_glow_and_online_menu(source):
read = lambda path: (source / path).read_text(encoding="utf-8")
lights = read("source/rasterizer/xbox/rasterizer_xbox_lights.c")
draw = c_function(lights, "rasterizer_sun_glow_draw")
require("vr_render_sun_glow_begin(" in draw and "vr_render_sun_glow_end(" in draw,
"The per-eye sun-glow wrapper is not integrated into the actual renderer.")
require('config_real("vr.sun_glow_strength")' in draw
and 'rasterizer_sun_glow_draw_internal(flare, 1.0f)' in draw,
"The VR sun-strength option must preserve the original flat-screen composite.")
strength = c_function(lights, "rasterizer_vr_sun_glow_strength")
require("isfinite(configured)" in strength and "configured < 0.0" in strength
and "configured > 1.0" in strength and "return 0.5f" in strength,
"Sun-glow intensity must reject non-finite values and stay within 0–1.")
require('"vr.sun_glow_strength", _config_real, "0.5"' in read("port/linux/src/port_config.c"),
"The bounded half-strength sun-glow default is missing.")
renderer = read("port/linux/game/vr_render.c")
begin = c_function(renderer, "vr_render_sun_glow_begin")
for integration in ("vr_render.eye_cameras[eye]", "vr_render.eyes[eye]", "halo_vr_screen_space(1)", "halo_vr_draw_eye(eye)"):
require(integration in begin, "Missing per-eye glow state: " + integration)
require("halo_vr_screen_space(0)" in c_function(renderer, "vr_render_sun_glow_end"),
"Glow screen-space state is not restored.")
backend = c_function(read("port/linux/src/d3d8_gl.c"), "prepare_draw")
require(0 <= backend.find("key.sample_eye") < backend.find("fragment_shader_get(&key)"),
"Source-eye selection must participate in the actual fragment cache key.")
shader = read("port/linux/src/nv2a_psh.c")
require("key->multiview == XGPU_MULTIVIEW_NONE && key->sample_eye" in shader,
"A mono glow scratch target does not sample the selected stereo source eye.")
menu = read("port/linux/game/menu_functions.c")
route = c_function(menu, "cooperative_begin")
routes = {
"coop_host_lan": "cooperative_network_host(widget, event, controller, _multiplayer_mode_host_lan, widget_deleted)",
"coop_host_online": "cooperative_network_host(widget, event, controller, _multiplayer_mode_host_internet, widget_deleted)",
"coop_join_lan": "cooperative_network_join(widget, controller, _multiplayer_mode_lan, widget_deleted)",
"coop_join_online": "cooperative_network_join(widget, controller, _multiplayer_mode_server_browser, widget_deleted)",
"coop_join_invite": "cooperative_network_join(widget, controller, _multiplayer_mode_direct_link, widget_deleted)",
}
for name, call in routes.items():
require(re.search(r'if \(!strcmp\(widget->name, "' + name + r'"\)\)\s*return ' + re.escape(call) + r';', route),
"Missing LAN/Internet campaign dispatch: " + name)
require("cooperative_session_cleanup();" in route and '"network_screen"' in route,
"Leaving the campaign network chooser must clean up its pending session.")
single_player = c_function(menu, "cooperative_single_player")
for integration in ("player_spawn_count = 1", "player_ui_reset_single_player_local_player_controllers()",
"lobby_join_reset()", "player_ui_local_player_left_multiplayer_game(controller)"):
require(integration in single_player, "Missing one-player campaign reset: " + integration)
host = c_function(menu, "cooperative_network_host")
require('mode == _multiplayer_mode_host_internet && !config_boolean("network.online")' in host
and "multiplayer_host_mode(widget, event, controller, mode, TRUE, widget_deleted)" in host
and "cooperative_map_opening = TRUE" in host and "cooperative_map_opening = FALSE" in host,
"Campaign hosts must distinguish offline LAN from Internet and open campaign maps.")
join = c_function(menu, "cooperative_network_join")
require('mode != _multiplayer_mode_lan && !config_boolean("network.online")' in join
and "cooperative_network_game = TRUE" in join and "multiplayer.mode = mode" in join,
"Campaign joins must distinguish LAN, Internet browser and invite modes.")
handler = c_function(menu, "pc_menu_event_function_invoke")
require(re.search(r'else if \(!strcmp\(name, "port coop begin"\)\)\s*\{\s*return cooperative_begin\(', handler),
"Campaign dispatch must work in both flat and VR builds.")
require(re.search(r'else if \(!strcmp\(name, "port coop player 2 list initialize"\)[^{]+\{\s*return campaign_fail\(\);', handler)
and "player_spawn_count = 2;" not in menu and "vr_cooperative_begin" not in menu,
"Stale split-screen campaign routes must be rejected.")
for helper in ("lobby_add_player", "lobby_join_start", "lobby_player_choose", "preview_add"):
body = c_function(menu, helper)
require("lobby_campaign_single_player()" in body and "return campaign_local_player_block();" in body,
"Campaign must block a second local player: " + helper)
require("lobby_local_player_count() > 1" in c_function(menu, "map_list_choose")
and "cooperative_single_player()" in c_function(menu, "multiplayer_game_player"),
"Ordinary map/browser entry points must also use one local campaign player.")
require('!strcmp(name, "gamespy back handler") && cooperative_network_game &&' in handler
and "!global_network_game_server_get()" in handler,
"Browser cancellation must preserve a host when returning from Server Setup.")
asset = source / "port/assets/menus/ce/main_menu.multiplayer_type_select.coop.xml"
nodes = {node.attrib["name"].rsplit("/", 1)[-1]: node for node in ET.parse(asset).getroot()}
require("player_2_profile_screen" not in nodes and "player_2_profile_list" not in nodes,
"The campaign menu must not open a second-player profile screen.")
for event in ("a", "start"):
item = nodes["multiplayer_type_coop_item"].find('on[@event="' + event + '"]')
require(item is not None and item.get("run") == "port coop begin"
and item.get("open", "").endswith("coop/network_screen"),
"The campaign button must open the LAN/Internet chooser.")
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():
require(name in nodes and nodes[name].get("text") == caption, "Missing campaign menu choice: " + caption)
for event in ("a", "start"):
item = nodes[name].find('on[@event="' + event + '"]')
require(item is not None and item.attrib == {"event": event, "run": "port coop begin"},
"Campaign choices must dispatch their named route without a static widget fallback.")
generator = read("tools/port_settings.py")
generated_coop = generator.split("def _coop() -> list:", 1)[1].split("def _item_options_extras", 1)[0]
require(all(name in generated_coop for name in captions) and "network_screen" in generated_coop
and "player_2_profile_screen" not in generated_coop,
"The menu generator must retain the network-only campaign choices.")
for script in ("tools/test_vr_sun_glow.py", "tools/test_online_coop_menu.py"):
require((source / script).is_file(), "Missing production C regression: " + script)
def check_nominal_projectile_reticle(source):
read = lambda path: (source / path).read_text(encoding="utf-8")
weapons = read("source/items/weapons.c")
firing = c_function(weapons, "trigger_create_projectiles")
require(re.search(r"trigger_adjust_projectile_ray\([^;]+&error,\s*FALSE,\s*NULL\)", firing),
"Real shots must retain the shared firing calculation with target recording.")
preview = c_function(weapons, "weapon_preview_projectile_ray")
require(re.search(r"trigger_adjust_projectile_ray\([^;]+&error,\s*TRUE,\s*pose\)", preview),
"The reticle must use the same pre-spread calculation in preview mode.")
require(not re.search(r"\b(?:random|projectile_new|weapon_trigger_fire)\w*\s*\(", preview),
"Reticle preview must not fire or consume shot randomness.")
assist = read("source/game/aim_assist.c")
require("player_aim_projectile_internal(player_index, position, direction, FALSE, pose)"
in c_function(assist, "player_preview_projectile_aim"),
"Reticle preview must suppress player targeting metadata writes.")
require("player_aim_projectile_internal(player_index, position, direction, TRUE, NULL)"
in c_function(assist, "player_aim_projectile"),
"Actual shots must preserve player targeting metadata writes.")
renderer = read("port/linux/game/vr_render.c")
crosshairs = c_function(renderer, "vr_render_crosshairs")
point = c_function(renderer, "vr_render_projectile_point")
require("vr_render_projectile_point(" in crosshairs and "vr_render_projectile_ray(" in point
and "_collision_test_for_projectiles_flags" in point,
"The rendered reticle must trace the game's nominal projectile ray.")
require("MAXIMUM_COLLISION_USER_STACK_DEPTH - 2" in point,
"The reticle must reserve capacity for UI, aim-assist and nested line-of-sight contexts.")
require("weapon_preview_projectile_ray(weapon_index, 0, player_index"
in c_function(renderer, "vr_render_projectile_ray"),
"The reticle must query the equipped primary weapon's actual firing ray.")
require("crosshair_inverse_distance" not in crosshairs and "CROSSHAIR_SMOOTHING" not in crosshairs,
"Reticle depth must not lag behind its current collision hit.")
require((source / "tools/test_vr_projectile_reticle.py").is_file(),
"The production projectile-reticle regression is missing.")
pose = c_function(renderer, "vr_render_pointing_pose")
for guard in ("local_player_get_player_index(0)", "_object_type_biped", "parent_object_index != NONE",
"gunner_object_index != NONE", "_director_perspective_first_person", "director_inhibited_facing(0)",
"aiming_velocity_maximum != 0.0f", "aiming_acceleration_maximum != 0.0f"):
require(guard in pose, "Missing guarded render-rate preview state: " + guard)
for integration in ("view->hand_forward", "player_control_get_facing_direction(0",
"DEGREES_TO_RADIANS(85.0f)", "unit_clip_to_aiming_bounds(",
"render_interpolation_camera(0, observer_get_camera(0))"):
require(integration in pose, "Missing current-frame preview input: " + integration)
require("hand_position" not in pose and "camera_effects" not in pose,
"Preview origin must not add hand translation or apply camera effects again.")
require("vr_render_pointing_pose(player_index, unit, &vr_render.view, vr_render.base_yaw, pose)"
in c_function(renderer, "vr_render_projectile_pose"),
"The equipped reticle must retain the current render pose and yaw.")
unarmed = c_function(renderer, "vr_render_unarmed_ray")
require("unit_get_weapon_count(unit_index) != 0" in unarmed
and "_object_dead_bit" in unarmed and "unit->unit.player_index != player_index" in unarmed
and "unit_inventory_get_weapon" in unarmed and "vr_render_pointing_pose(" in unarmed,
"An unarmed reticle must be limited to a living owned player without an equipped weapon.")
ray = c_function(renderer, "vr_render_projectile_ray")
require("vr_render_projectile_pose(player_index, unit, &pose) ? &pose : NULL" in ray,
"The current-frame pose is not integrated into the primary reticle preview.")
units = read("source/units/units.c")
require("adjust_origin, use_aiming_vector, NULL, NULL" in c_function(units, "unit_adjust_projectile_ray"),
"Actual unit shots must not use presentation overrides.")
require("unit_adjust_projectile_ray_internal(" in c_function(units, "unit_preview_projectile_ray"),
"Unit preview must share the original camera/muzzle projection math.")
def check_unarmed_flashlight(source):
read = lambda path: (source / path).read_text(encoding="utf-8")
renderer = read("port/linux/game/vr_render.c")
pose = c_function(renderer, "vr_render_player_flashlight_pose")
for guard in ("!vr_render.active", "!vr_render.weapon_camera_set",
"!vr_render.weapon_camera_valid", "render.local_player_index != 0",
"director_camera_scripted && *director_camera_scripted",
"!halo_vr_aiming()", "!halo_vr_gaze_tracking_valid()",
"vr_render_unarmed_ray("):
require(guard in pose, "Missing unarmed flashlight render-pose guard: " + guard)
require("vr_render.weapon_camera.position" in pose and "vr_render.weapon_camera.up" in pose
and "dot_product3d(" in pose and "normalize3d(" in pose
and "halo_vr_view(" not in pose,
"The flashlight must use the cached rendered hand pose with a perpendicular up vector.")
lights = read("source/objects/object_lights.c")
override = c_function(lights, "light_vr_unarmed_flashlight_pose")
for guard in ("#ifdef HALO_VR", "light->parent_light_index == NONE",
"light->object_index != NONE", "_point_light_dynamic_bit",
"_light_definition_is_first_person_flashlight_bit",
"vr_render_player_flashlight_pose("):
require(guard in override, "Missing local flashlight light-definition guard: " + guard)
bounds = c_function(lights, "light_compute_render_bounding_sphere")
require("struct light_datum effective = *light_get(light_index)" in bounds
and "&effective.position" in bounds and "&effective.forward" in bounds
and "light_compute_bounding_sphere_from_basis(&effective" in bounds,
"Render flashlight bounds must use a copy of the simulation light.")
simulation = c_function(lights, "light_compute_bounding_sphere")
reconnect = c_function(lights, "light_reconnect_to_map")
require("light_compute_bounding_sphere_from_basis(light_get(light_index)" in simulation
and "light_compute_render_bounding_sphere(" not in reconnect
and "light_vr_unarmed_flashlight_pose(" not in reconnect,
"Flashlight presentation must not move the simulation light's BSP partition.")
include = c_function(lights, "lights_vr_include_unarmed_flashlights")
require("light_unmarked(light_index)" in include and "object_get_function_value(" in include
and "intensity <= 0.0f" in include and "slot < MAXIMUM_RENDERED_LIGHTS" in include
and "MAXIMUM_RENDERED_LIGHTS - 1" in include,
"Adding a local flashlight outside stale visibility must respect scene-list capacity.")
preprocess = c_function(lights, "lights_preprocess_scene")
require(0 <= preprocess.find("structure_visibility_find_objects(")
< preprocess.find("lights_vr_include_unarmed_flashlights()")
< preprocess.find("light_marker_end()"),
"The current flashlight must be included before finishing scene-light visibility.")
require("light_vr_unarmed_flashlight_pose(light, &light_parameters.position" in preprocess
and "first_person_weapon_center_flashlight(" in preprocess,
"The local unarmed override must be integrated without replacing armed flashlight markers.")
for name in ("light_get_bounding_sphere", "lights_render_diffuse", "lights_render_specular"):
require("light_compute_render_bounding_sphere(" in c_function(lights, name),
"Flashlight visibility and drawing must share rendered bounds: " + name)
for name in ("lights_render_diffuse", "lights_render_specular"):
require("light_render_skips_clusters(light)" in c_function(lights, name),
"The unarmed beam must not use stale tick cluster restrictions: " + name)
require((source / "tools/test_vr_flashlight.py").is_file(),
"Missing production C flashlight regression.")
def check_tutorial(source):
read = lambda path: (source / path).read_text(encoding="utf-8")
player = read("source/game/player_control.c")
require("halo_vr_turn_control(" in c_function(player, "handle_one_player_input")
and "halo_vr_tutorial_reset(" in c_function(player, "player_control_action_test_reset")
and "halo_vr_tutorial_turn(" in c_function(player, "player_control_action_test_note"),
"The script action tests must share actual VR turns and reset their head anchor.")
buttons = c_function(player, "player_control_action_test_check_reset_input_blob")
require("halo_vr_running()" in buttons and "halo_vr_active" not in buttons
and "vr_controls" in buttons
and "_unit_control_use_equipment_bit, FALSE" in c_function(player, "player_control_action_test_check_reset_input_blob"),
"A consumed VR tutorial Back button must suppress its mapped melee action.")
vr = read("port/linux/src/vr.c")
c_function(vr, "halo_vr_running")
require("int halo_vr_running(void);" in read("port/linux/src/vr.h"),
"The tutorial VR-running query must use the real engine API.")
units = c_function(read("source/units/units.c"), "unit_can_see_point")
require("vr_render_player_gaze(" in units and "local_player_index == 0" in units
and "_object_dead_bit" in units,
"Headset gaze must be limited to the living local primary player.")
hs = read("source/hs/hs_library_external.c")
panel = c_function(hs, "hs_vr_calibration_panel")
require("degrees != 5.f" in panel and "levels\\\\a10\\\\a10" in panel
and "_object_type_scenery" in panel,
"Hand-reticle acceptance must retain the original named a10 panel gate.")
require("hs_vr_calibration_panel(" in c_function(hs, "hs_unit_can_see_object")
and "vr_render_player_pointing_ray(" in c_function(hs, "hs_unit_can_see_object"),
"The unarmed presentation ray must be integrated into the panel check.")
locate = c_function(read("port/linux/arm64/host_vr.c"), "host_vr_locate")
require(0 <= locate.find("views->head.valid = 0") < locate.find("if (!vr.frame_begun)")
and "XR_VIEW_STATE_POSITION_VALID_BIT" in locate
and "XR_SPACE_LOCATION_POSITION_VALID_BIT" in locate,
"Fresh full-pose queries must invalidate stale data before any failure return.")
for script in ("tools/test_vr_tutorial.py", "tools/test_vr_tracking.py", "tools/test_vr_tutorial_buttons.py"):
require((source / script).is_file(), "Missing tutorial production C regression: " + script)
GRAPH_SMOKE = r'''
import io
from pathlib import Path
from types import SimpleNamespace
import sys
import re
sys.path.insert(0, str(Path.cwd()))
from tools import linux_arm64_build as build
from tools.ninja_syntax import Writer
headers = Path(".offline-check-headers")
(headers / "GLES3").mkdir(parents=True)
(headers / "GLES3/gl32.h").write_text("/* graph check only */")
build.GL_HEADERS = headers
build.is_linux_arm64 = lambda: True
build.fetch_third_party = lambda directory: None
sln = SimpleNamespace(port_release=True, port_vr=True, linux_arm64_cc="clang", port_pgo="off", port_lto="off")
output = io.StringIO()
build.generate_linux_arm64_build(Writer(output), sln)
graph = output.getvalue()
# pathlib follows the checker host even though this is a Linux build graph.
# Normalize only the inspected text; retain the actual generated graph below.
inspected_graph = re.sub(r"\$\r?\n[ \t]*", "", graph).replace("\\", "/")
for required in ("port/linux/game/network_coop.c", "port/linux/game/coop_scripts.c", "port/linux/game/coop_enemies.c", "port/linux/src/vr.c", "port/linux/arm64/host_vr.c", "-DHALO_VR=1", "-iquote port/linux/game", "build/linux_arm64/brokers.txt", "port/assets/network/brokers.txt"):
if required not in inspected_graph:
raise RuntimeError("The ARM64 graph omitted " + required)
if "-DHALO_GLES=1" in graph:
raise RuntimeError("The VR guest graph selected GLES instead of desktop OpenGL")
Path(".offline-check-build.ninja").write_text(graph, encoding="utf-8")
print("ARM64/OpenXR guest, host, co-op and broker graph generated without downloads or compilation.")
'''
def project_source(source, destination):
archive = git(source, "archive", "--format=tar", SOURCE_COMMIT).stdout
require(len(archive) <= 256 * 1024 * 1024, "The source archive exceeds the offline check limit.")
with tarfile.open(fileobj=io.BytesIO(archive), mode="r:") as entries:
for entry in entries:
name = PurePosixPath(entry.name)
require(not name.is_absolute() and ".." not in name.parts and (entry.isdir() or entry.isfile()),
"The source projection contains an unexpected path or link.")
target = destination.joinpath(*name.parts)
if entry.isdir():
target.mkdir(parents=True, exist_ok=True)
else:
target.parent.mkdir(parents=True, exist_ok=True)
with entries.extractfile(entry) as incoming, target.open("wb") as outgoing:
outgoing.write(incoming.read())
def check_source(source: Path):
source = source.resolve()
require(git(source, "rev-parse", "HEAD").stdout.decode().strip() == SOURCE_COMMIT,
"Use the exact pinned experimental engine commit; a moving branch is not accepted.")
patch = ROOT / "resources/frame-controls.patch"
changed = patch_files(patch.read_text(encoding="utf-8"))
dirty = git(source, "diff", "--name-only", "HEAD").stdout.decode().splitlines()
untracked = git(source, "ls-files", "--others", "--exclude-standard").stdout.decode().splitlines()
require(set(dirty + untracked) <= changed, "The engine checkout has changes outside the reviewed local patch.")
vanilla = git(source, "apply", "--check", str(patch), check=False).returncode == 0
patched = git(source, "apply", "--reverse", "--check", str(patch), check=False).returncode == 0
require(vanilla or patched, "The local patch neither applies cleanly nor exactly reverses in this checkout.")
require(not vanilla or not dirty + untracked, "A vanilla source input must have no local engine changes.")
# Always test a clean canonical projection, so a patched input is not trusted
# merely because its HEAD still names the selected revision.
(ROOT / "build").mkdir(exist_ok=True)
with tempfile.TemporaryDirectory(prefix="experimental-engine-check-", dir=ROOT / "build") as temporary:
projection = Path(temporary)
project_source(source, projection)
check_upstream(projection)
subprocess.run(["git", "init", "--quiet"], cwd=projection, check=True, capture_output=True, timeout=30)
for arguments in (("--check",), ()):
result = subprocess.run(["git", "apply", *arguments, str(patch)], cwd=projection,
capture_output=True, timeout=30)
require(result.returncode == 0, "The local patch does not apply to the canonical pinned engine source.")
if patched:
for path in changed:
require((source / path).read_bytes().replace(b"\r\n", b"\n")
== (projection / path).read_bytes().replace(b"\r\n", b"\n"),
"The patched input contains an extra change beyond the shipped local patch: " + path)
check_controls(projection)
check_stereo_glow_and_online_menu(projection)
check_nominal_projectile_reticle(projection)
check_tutorial(projection)
check_unarmed_flashlight(projection)
result = subprocess.run([sys.executable, "-I", "-c", GRAPH_SMOKE], cwd=projection,
capture_output=True, text=True, timeout=30)
require(result.returncode == 0, "Offline ARM64 configuration generation failed: " + result.stderr[-2000:])
return {"ok": True, "sourceCommit": SOURCE_COMMIT, "workingTree": "vanilla" if vanilla else "patched",
"protocolVersion": 17, "rendererTwoPassBinding": True, "campaignCoopIntegration": True,
"headDirectedMovementIntegration": True, "xboxControlsPreserved": True,
"stereoSunGlowIntegration": True, "boundedSunGlowStrength": True, "vrInternetCampaignMenu": True,
"nominalProjectileReticleIntegration": True, "renderRateUnarmedFlashlightIntegration": True,
"renderRateProjectilePreviewIntegration": True,
"networkCampaignOnlyIntegration": True,
"vrTutorialIntegration": True,
"freshFullPoseTrackingIntegration": True,
"offlineArm64NinjaGraph": True, "arm64LinkedBuild": False, "hardwareValidated": False}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("source", type=Path, help="Local exact-pinned vanilla or fully patched engine checkout")
arguments = parser.parse_args()
try:
result = check_source(arguments.source)
except (EngineCheckError, OSError, subprocess.SubprocessError) as error:
print(json.dumps({"ok": False, "error": str(error)}))
return 1
print(json.dumps(result, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())