mirror of
https://github.com/ClassicWasTaken/HaloCE-SteamFrame.git
synced 2026-10-15 03:00:34 +02:00
768 lines
51 KiB
Python
768 lines
51 KiB
Python
"""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 ast
|
||
import io
|
||
import json
|
||
from pathlib import Path, PurePosixPath
|
||
import re
|
||
import shlex
|
||
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",
|
||
"port/android/guest/runtime/guest_host.h", "port/android/host/host_gl.c",
|
||
"port/linux/arm64/host_imports.list", "port/linux/src/vr_vehicle.h",
|
||
"source/interface/ui_widget.c", "source/interface/motion_sensor.c",
|
||
"source/rasterizer/rasterizer_transparent_geometry.c",
|
||
"port/linux/game/menu_tags.c", "tools/ce_menus.py",
|
||
"port/assets/menus/ce/bitmaps.xml", "port/assets/menus/menus.json",
|
||
"port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.vr_display.xml",
|
||
"port/assets/menus/ce/port/pause/pausebox_left__2.png",
|
||
"port/assets/menus/ce/port/pause/pausebox_center__2.png",
|
||
"port/assets/menus/ce/port/pause/pausebox_right__2.png",
|
||
"port/assets/menus/port_svg/pause/pausebox_left__2.svg",
|
||
"port/assets/menus/port_svg/pause/pausebox_center__2.svg",
|
||
"port/assets/menus/port_svg/pause/pausebox_right__2.svg",
|
||
"tools/test_vr_pause_menu.py", "tools/test_vr_pointer_radar.py",
|
||
"tools/test_persistent_stream_buffers.py", "tools/test_vr_vehicle.py",
|
||
"tools/test_vr_cinematic_hands.py",
|
||
"port/linux/src/vr_host.h", "source/render/render.c",
|
||
"tools/test_vr_zoom_aim.py", "tools/test_vr_stock_zoom_hud.py",
|
||
"port/linux/src/gl.h", "tools/test_vr_renderer_link.py",
|
||
"port/linux/src/vr_driving.h", "tools/test_vr_driving.py", "tools/test_vr_driving_game.py",
|
||
"port/linux/game/vr_hands.c", "port/linux/game/vr_hands.h",
|
||
"tools/test_vr_framebuffer.py", "tools/test_vr_vehicle_pose.py",
|
||
"source/interface/first_person_weapons.c", "source/interface/hud_weapon.c",
|
||
))
|
||
|
||
|
||
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)
|
||
|
||
|
||
def check_private_menu_and_vehicle_integration(source):
|
||
read = lambda path: (source / path).read_text(encoding="utf-8")
|
||
config = read("port/linux/src/port_config.c")
|
||
for key, default in (("vr.menu_pointer", "true"), ("vr.radar_head_heading", "true"),
|
||
("vr.vehicle_seat_calibration", "true"), ("vr.vehicle_clear_windshield", "true"),
|
||
("display.persistent_stream_buffers", "false")):
|
||
require(re.search(re.escape('"' + key + '"') + r',\s*_config_boolean,\s*"' + default + r'"', config),
|
||
"Missing reviewed private option/default: " + key)
|
||
tags = read("port/linux/game/menu_tags.c")
|
||
loaded = c_function(tags, "menu_tags_loaded")
|
||
require("halo_vr_running() && tag_loaded(UI_WIDGET_DEFINITION_TAG, SOLO_PAUSE_SCREEN) != NONE" in loaded
|
||
and "build.multiplayer_map = game_map && multiplayer_map" in loaded,
|
||
"Pause VR Setup must use the exact campaign root and distinguish network-map pause flags.")
|
||
require("if (build.multiplayer_map)" in c_function(tags, "widget_build")
|
||
and "pause_vr_widget()" in c_function(tags, "pause_patch")
|
||
and '"VR SETUP", NONE' in c_function(tags, "pause_list_patch"),
|
||
"Pause VR Setup must reuse existing widgets without a profile-edit or network function.")
|
||
ui = read("source/interface/ui_widget.c")
|
||
require("!network_coop_active()" in c_function(ui, "widget_instance_initialize")
|
||
and "vr_render_menu_pointer_draw()" in c_function(ui, "render_ui_widgets"),
|
||
"Pause settings must retain co-op clock policy and render the native UI pointer.")
|
||
vr = read("port/linux/src/vr.c")
|
||
pointer = c_function(vr, "halo_vr_menu_pointer")
|
||
require("menu_pointer_intersection(" in pointer and "menu_pointer_button(" in pointer
|
||
and "!vr.pending_layers.hud_head_locked" in pointer,
|
||
"The tracked pointer must intersect the fixed rendered menu panel.")
|
||
backend = read("port/linux/src/d3d8_gl.c")
|
||
desktop_pointer = c_function(backend[backend.rfind("int halo_ui_pointer_update"):], "halo_ui_pointer_update")
|
||
require("halo_vr_menu_pointer(menus_active, pointer)" in desktop_pointer,
|
||
"VR pointing must feed the existing native widget input path.")
|
||
renderer = read("port/linux/game/vr_render.c")
|
||
require("vr_render_player_gaze(" in c_function(renderer, "vr_render_motion_sensor_yaw"),
|
||
"Optional head-heading radar must use the current rendered HMD gaze.")
|
||
radar = c_function(read("source/interface/motion_sensor.c"), "render_motion_sensor")
|
||
require("vr_render_motion_sensor_yaw(" in radar and "draw_sensor = *sensor" in radar
|
||
and "render_sensor = &draw_sensor" in radar,
|
||
"Radar head heading must alter a draw copy, preserving tick/history data.")
|
||
display = ET.parse(source / "port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.vr_display.xml")
|
||
require(any(node.get("setting") == "display.persistent_stream_buffers"
|
||
and node.get("values") == "true|false" for node in display.getroot()),
|
||
"The existing VR Display screen must expose the optional buffer-streaming setting.")
|
||
require("halo_vr_vehicle_reference(" in c_function(renderer, "vr_render_vehicle_reference")
|
||
and "vr_render_vehicle_reference(" in c_function(renderer, "vr_render_camera")
|
||
and "vr_render_vehicle_reference(" in c_function(read("source/game/player_control.c"), "handle_one_player_input"),
|
||
"Seated reference calibration must be integrated into both current view and player input.")
|
||
require("halo_vr_vehicle_update(" in c_function(vr, "halo_vr_vehicle_reference")
|
||
and "vehicle_capture()" in c_function(vr, "recentre")
|
||
and (source / "port/linux/src/vr_vehicle.h").is_file(),
|
||
"Vehicle entry/recenter must use its separate production tracking reference.")
|
||
require("menu_level_orientation(" in c_function(vr, "menu_place"),
|
||
"Menu placement must remove head roll while preserving the gaze direction.")
|
||
seat_camera = c_function(renderer, "vr_render_vehicle_camera_position")
|
||
require("object_get_marker_by_name(" in seat_camera and "isfinite(" in seat_camera
|
||
and "vr_render_vehicle_camera_position(" in c_function(renderer, "vr_render_player_gaze")
|
||
and "vr_render_vehicle_camera_position(" in c_function(renderer, "vr_render_camera"),
|
||
"The local vehicle view and gaze must share a checked seat anchor.")
|
||
presentation = c_function(renderer, "vr_render_controller_presentation")
|
||
require("halo_vr_presentation_tracking_valid()" in presentation
|
||
and "_director_perspective_first_person" in presentation
|
||
and "_object_dead_bit" in presentation
|
||
and "vr_render_controller_presentation()" in c_function(renderer, "vr_render_weapon_camera"),
|
||
"Controller presentation must remain available in a visible first-person arrival rig.")
|
||
tracking = c_function(vr, "halo_vr_presentation_tracking_valid")
|
||
require("vr.views.focused" in tracking and "vr.views.head.valid" in tracking
|
||
and "isfinite(" in tracking and "vr.aim_synced" not in tracking,
|
||
"Presentation must require current finite tracking without enabling gameplay aim.")
|
||
glass = c_function(renderer, "vr_render_hides_vehicle_glass")
|
||
require('"vehicles\\\\warthog\\\\warthog"' in glass and "seated_unit()" in glass
|
||
and "object_index != unit->object.parent_object_index" in glass,
|
||
"The windshield override must be limited to the occupied stock Warthog.")
|
||
transparent = c_function(read("source/rasterizer/rasterizer_transparent_geometry.c"),
|
||
"rasterizer_transparent_geometry_draw")
|
||
require("_shader_type_transparent_glass" in transparent and "vr_render_hides_vehicle_glass(" in transparent
|
||
and re.search(r"transparent_geometry_group_index\+\+;\s*continue;", transparent),
|
||
"The local glass override must advance the existing draw queue without changing retail tags.")
|
||
for script in ("tools/test_vr_pause_menu.py", "tools/test_vr_pointer_radar.py", "tools/test_vr_vehicle.py",
|
||
"tools/test_vr_cinematic_hands.py"):
|
||
require((source / script).is_file(), "Missing private production C regression: " + script)
|
||
|
||
|
||
def check_menu_controller_visibility(source):
|
||
"""Verify the shipped menu/tracking fix reaches the actual draw paths."""
|
||
read = lambda path: (source / path).read_text(encoding="utf-8")
|
||
vr = read("port/linux/src/vr.c")
|
||
view = c_function(vr, "halo_vr_view")
|
||
require("int controller_aim_requested;" in read("port/linux/src/vr.h")
|
||
and "view->controller_aim_requested = settings.controller_aim" in view,
|
||
"Controller aim mode must remain distinct from current pose validity.")
|
||
require(view.find("view->controller_aim_requested = settings.controller_aim") < view.find("host_vr_locate("),
|
||
"Late tracking failure must retain the selected controller aim mode.")
|
||
renderer = read("port/linux/game/vr_render.c")
|
||
visible = c_function(renderer, "vr_render_first_person_visible")
|
||
crosshairs = c_function(renderer, "vr_render_hud_crosshairs_visible")
|
||
for body in (visible, crosshairs):
|
||
require("halo_vr_frame_active()" in body and "controller_aim_requested" in body,
|
||
"Controller visibility must distinguish native VR mode from flat/gamepad/head paths.")
|
||
require("ui_widgets_active_for_local_player(" in body or "vr_render_gameplay_hud_visible(" in body,
|
||
"Controller visibility must read current menu state before a stale input flag.")
|
||
update = c_function(read("source/interface/first_person_weapons.c"), "first_person_weapon_render_update")
|
||
require("vr_render_first_person_visible(render.local_player_index)" in update
|
||
and update.find("vr_render_first_person_visible(") < update.find("first_person_weapon_set_visibility("),
|
||
"The first-person rig must apply visibility before building or drawing head-camera nodes.")
|
||
hud = c_function(read("source/interface/hud_weapon.c"), "crosshairs_draw")
|
||
require("vr_render_hud_crosshairs_visible(" in hud
|
||
and hud.find("vr_render_hud_crosshairs_visible(") < hud.find("TEST_FLAG(weapon_hud_globals->script_flags"),
|
||
"The ordinary HUD crosshair fallback must share the controller-mode visibility guard.")
|
||
|
||
|
||
def check_optional_native_buffer_streaming(source):
|
||
read = lambda path: (source / path).read_text(encoding="utf-8")
|
||
backend = read("port/linux/src/d3d8_gl.c")
|
||
require("#if defined(HALO_ARM64_GUEST) && !defined(HALO_GLES)\n#define XGPU_PERSISTENT_STREAMS" in backend,
|
||
"Persistent streaming must remain limited to the native desktop-GL guest.")
|
||
initialize = c_function(backend, "stream_buffers_initialize")
|
||
require('config_boolean("display.persistent_stream_buffers")' in initialize
|
||
and "host_gl_buffer_create_persistent(" in initialize
|
||
and "host_gl_buffer_destroy_persistent(" in initialize,
|
||
"Optional streaming must initialize transactionally and retain the ordinary-buffer fallback.")
|
||
frame = c_function(backend, "frame_end_buffers")
|
||
require("host_gl_wait_frame_checked(" in frame and "stream_ring_reset(ring_ready)" in frame,
|
||
"Mapped ring reuse must be gated by a completed host fence.")
|
||
reset = c_function(backend, "stream_ring_reset")
|
||
require("if (!ready)" in reset and "device.persistent_ring[ring] = FALSE" in reset
|
||
and "glGenBuffers(" in reset,
|
||
"An unfinished mapped slot must retire its mappings before mutable reuse.")
|
||
host = read("port/android/host/host_gl.c")
|
||
functions = ("host_gl_buffer_create_persistent", "host_gl_buffer_destroy_persistent",
|
||
"host_gl_buffer_write_persistent", "host_gl_wait_frame_checked")
|
||
imports = set(read("port/linux/arm64/host_imports.list").splitlines())
|
||
declarations = read("port/android/guest/runtime/guest_host.h") + read("port/linux/src/xgpu.h")
|
||
for function in functions:
|
||
c_function(host, function)
|
||
require(function in imports and function + "(" in declarations,
|
||
"Missing real host definition/import/declaration: " + function)
|
||
create = c_function(host, "host_gl_buffer_create_persistent")
|
||
write = c_function(host, "host_gl_buffer_write_persistent")
|
||
require("#ifdef HALO_DESKTOP_GL" in create and "GL_ARB_buffer_storage" in create
|
||
and "glBufferStorage" in create and "glDeleteBuffers(" in create,
|
||
"Persistent allocation must check desktop capability and delete a failed immutable allocation.")
|
||
require("size > persistent_buffers[slot].size - offset" in write and "size && !data" in write,
|
||
"Host-owned mapped writes must enforce the buffer's recorded range and non-null data.")
|
||
require((source / "tools/test_persistent_stream_buffers.py").is_file(),
|
||
"Missing production host/guest buffer-streaming regression.")
|
||
|
||
|
||
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", "port/linux/game/menu_tags.c", "source/interface/ui_widget.c", "source/interface/motion_sensor.c", "source/rasterizer/rasterizer_transparent_geometry.c", "port/android/host/host_gl.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 build_recipe_cli_options(candidate: Path, visited=None):
|
||
"""Read declared options, including a local test's delegated main parser."""
|
||
visited = set() if visited is None else visited
|
||
if candidate in visited:
|
||
return set()
|
||
require(len(visited) < 32, "Native test CLI delegation exceeds the source check limit.")
|
||
visited.add(candidate)
|
||
try:
|
||
tree = ast.parse(candidate.read_text(encoding="utf-8"), filename=str(candidate))
|
||
except SyntaxError as error:
|
||
raise EngineCheckError("A native build test has invalid Python syntax: " + candidate.name) from error
|
||
options = {argument.value for node in ast.walk(tree) if isinstance(node, ast.Call)
|
||
and isinstance(node.func, ast.Attribute) and node.func.attr == "add_argument"
|
||
for argument in node.args if isinstance(argument, ast.Constant)
|
||
and isinstance(argument.value, str) and argument.value.startswith("--")}
|
||
called_main = {node.func.value.id for node in ast.walk(tree) if isinstance(node, ast.Call)
|
||
and isinstance(node.func, ast.Attribute) and node.func.attr == "main"
|
||
and isinstance(node.func.value, ast.Name)}
|
||
for node in ast.walk(tree):
|
||
if not isinstance(node, ast.Import):
|
||
continue
|
||
for imported in node.names:
|
||
if (imported.asname or imported.name) not in called_main:
|
||
continue
|
||
delegate = candidate.parent / (imported.name + ".py")
|
||
if delegate.is_file() and not delegate.is_symlink():
|
||
options.update(build_recipe_cli_options(delegate, visited))
|
||
return options
|
||
|
||
|
||
def check_build_recipe(source: Path, recipe: str | None = None):
|
||
"""Check the actual container commands against the canonical patched tree."""
|
||
recipe = recipe if recipe is not None else (ROOT / "resources/build-native.sh").read_text(encoding="utf-8")
|
||
require("set -euo pipefail" in recipe, "The native build recipe must stop on failed checks.")
|
||
commands = []
|
||
for line in recipe.splitlines():
|
||
if re.match(r"^\s*python3\s", line):
|
||
try:
|
||
argv = shlex.split(line, comments=True)
|
||
except ValueError as error:
|
||
raise EngineCheckError("An invalid Python command appears in the native build recipe.") from error
|
||
require(len(argv) >= 2 and all(token not in (";", "||", "&&", "|") for token in argv),
|
||
"The native build recipe contains an unsupported Python command.")
|
||
commands.append(argv)
|
||
tests = [argv for argv in commands if argv[1].startswith("tools/test_")]
|
||
expected = {path for path in PATCH_FILES if path.startswith("tools/test_") and path.endswith(".py")}
|
||
actual = [argv[1] for argv in tests]
|
||
require(len(actual) == len(set(actual)), "The native build recipe repeats a regression test.")
|
||
require(set(actual) == expected,
|
||
"The native build recipe does not match the shipped engine tests: " +
|
||
", ".join(sorted(set(actual) ^ expected)))
|
||
for argv in commands:
|
||
path = PurePosixPath(argv[1])
|
||
require(not path.is_absolute() and ".." not in path.parts and path.suffix == ".py",
|
||
"The native build recipe references an unsafe script path.")
|
||
candidate = source.joinpath(*path.parts)
|
||
require(candidate.is_file() and not candidate.is_symlink() and candidate.resolve().is_relative_to(source.resolve()),
|
||
"The native build recipe references a missing script: " + argv[1])
|
||
if argv in tests:
|
||
options = build_recipe_cli_options(candidate)
|
||
require(all(argument in options for argument in argv[2:] if argument.startswith("--")),
|
||
"The native build recipe passes an unsupported option to " + argv[1])
|
||
if argv[1] != "tools/test_build_sources.py":
|
||
require("--cc" in argv and argv[argv.index("--cc") + 1:argv.index("--cc") + 2] == ["clang"],
|
||
"The native build recipe must use the installed compiler for " + argv[1])
|
||
configure = [argv for argv in commands if argv[1] == "configure.py"]
|
||
require(configure == [["python3", "configure.py", "--release", "--vr", "--linux-arm64-cc", "clang"]],
|
||
"The native build recipe must configure the release ARM64 VR target exactly once.")
|
||
require(commands[-1] == configure[0], "Native regression checks must complete before configuration.")
|
||
require(re.search(r"^ninja\s+-j4\s+linux_arm64\s*$", recipe, re.M),
|
||
"The native build recipe must build the configured ARM64 target.")
|
||
return actual
|
||
|
||
|
||
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:
|
||
actual, expected = (source / path).read_bytes(), (projection / path).read_bytes()
|
||
if not path.endswith(".png"):
|
||
actual, expected = actual.replace(b"\r\n", b"\n"), expected.replace(b"\r\n", b"\n")
|
||
require(actual == expected,
|
||
"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)
|
||
check_private_menu_and_vehicle_integration(projection)
|
||
check_menu_controller_visibility(projection)
|
||
check_optional_native_buffer_streaming(projection)
|
||
check_build_recipe(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,
|
||
"pauseVRSetupIntegration": True, "trackedVRMenuPointerIntegration": True,
|
||
"menuControllerRigVisibility": True, "controllerReticleFallbackSuppressed": True,
|
||
"renderOnlyHeadRadarIntegration": True, "seatedVehicleReferenceIntegration": True,
|
||
"localWarthogGlassIntegration": True, "optionalNativePersistentBuffersIntegration": True,
|
||
"buildRecipeIntegration": 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())
|