diff --git a/CHANGELOG.md b/CHANGELOG.md index c76d7e5..eb954d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## HUD clearance and menu correction — source update, 2026-10-03 + +- Reduce the HUD's collision clearance above the ship/shield from 14 cm to 8 cm. Headset-following priority is unchanged. +- Show native in-run Ship, pause-menu and Options windows only on their floating world panel, preserving the status/crew HUD. Initial and hangar menus remain head-relative. +- Resolve and require `MenuScreenOnRender` and `OptionsScreenOnRender` hooks to isolate native menus from the supplemental HUD capture. Existing installations must rerun [hook resolution](docs/INSTALLATION.md#4-resolve-the-local-executable-hooks). + +Verification: 40 Python tests, focused Vulkan HUD/input checks and native before/after captures passed. The duplicate pause-menu/Options pixels disappeared from the HUD; complete world-panel captures remained unchanged. Physical headset comfort and stereo performance still need the maintainer's playtest. + ## HUD, wheel and launch corrections — source update, 2026-10-03 - Gameplay HUD follows the headset at a fixed readable size. It clamps above the transformed hull/shield only when its movement would intersect the ship; full menus remain head-relative. diff --git a/README.md b/README.md index 97e9afc..513ba61 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ An experimental OpenXR mod for **FTL: Faster Than Light**. Walk around a miniatu ## What it does - Floating player and enemy ships with opposing bows, room interiors, visible shields and movable encounter placement. -- Compact original HUD following the headset, with clearance above the ship when needed; upper status and crew roster, controller navigation, Tactical, shortcuts, system power and illustrated Help. +- Compact original HUD following the headset, with 8 cm clearance above the ship when needed; upper status and crew roster, controller navigation, Tactical, shortcuts, system power and illustrated Help. - Crew selection and drag-to-room orders, door controls, weapon/beam targeting and native system targeting. -- Contextual Jump/Ship/Store actions, a cockpit-anchored sector map, floating event dialogs and complete native shop/ship panels. +- Contextual Jump/Ship/Store actions, a cockpit-anchored sector map, floating event dialogs and complete native shop/ship/Options panels displayed only above the ship during a run. - An eight-slot action wheel with localized equipped names, family/ammo hints, Shift/Ctrl shortcuts and a QWERTY VR keyboard for renaming crew and ships. - Original procedural voxel crew, weapons and drones, weapon charge indicators, room-condition feedback, cross-shaped breaches and native miss cues. - Surrounding stars and native-state-driven asteroid, sun, nebula/storm and pulsar environments. @@ -66,11 +66,13 @@ The [controls guide](docs/CONTROLS.md) explains crew movement, room targeting, t ## Current state -The maintainer has played substantial portions of runs on Steam Frame and confirmed that earlier revisions are playable. The compact gameplay HUD now follows the headset first and stops above the ship only when it would intersect the hull or shield. Equipped weapon entries remain available on the wheel when no drones are installed. Room floors retain role icons and condition colors; health/status bars and counters are removed. System Power, storm-aware free power, native miss feedback and cross-shaped breaches remain available. +The maintainer has played substantial portions of runs on Steam Frame and confirmed that earlier revisions are playable. The compact gameplay HUD follows the headset first and stops above the ship only when it would intersect the hull or shield. Its collision clearance is now 8 cm, bringing it 6 cm closer. In-run Ship, pause-menu and Options windows appear only above the ship, without a duplicate on the headset HUD. Equipped weapon entries remain available on the wheel when no drones are installed. Room floors retain role icons and condition colors; health/status bars and counters are removed. System Power, storm-aware free power, native miss feedback and cross-shaped breaches remain available. Core game input, capture, native state and graceful launch/exit have been verified in an isolated game. **Complete campaign coverage and measured stereo headset performance remain open.** Hacking target acceptance is verified; drone attachment/effect completion still needs encounter testing. The renderer presents hazards and some effects approximately while the original game remains authoritative. -The **current source** passed 38 Python tests and nine Godot suites in the prepared source folder. Desktop and VR wrappers passed combined launch/exit in desktop smoke mode, including the GitHub Desktop checkout's FTL-VR alias, with clean client logs and no bridge errors; a separate copied campaign also verified native weapon entries in the rendered wheel. VR preflight passed, while physical Steam Frame comfort and stereo performance remain unmeasured for these changes. Campaign hashes and game executable fingerprints were preserved. Historical Tactical delivery reached about **28–29 updates/second in desktop scene checks**; this is texture delivery, not headset FPS. A clean installation on a different machine has not yet been tested. See [STATUS.md](docs/STATUS.md) for details. +The **current source** passed 40 Python tests and focused Vulkan HUD/input checks. Native before/after captures verified that Ship, pause-menu and Options windows remain complete above the ship while their duplicate HUD pixels are absent. Both local folders passed their VR-wrapper desktop smoke launches with clean client logs and no bridge errors; setup and VR readiness checks passed separately. Campaign files remained unchanged. **Existing installations must refresh their local hooks** using [Installation step 4](docs/INSTALLATION.md#4-resolve-the-local-executable-hooks). + +The preceding revision passed nine Godot suites and combined desktop launch/exit checks. Physical Steam Frame comfort and stereo performance remain to test for these changes. Historical Tactical delivery reached about **28–29 updates/second in desktop scene checks**; this is texture delivery, not headset FPS. A clean installation on a different machine has not yet been tested. See [STATUS.md](docs/STATUS.md) for details. ## Repository layout diff --git a/bridge/agent.js b/bridge/agent.js index 92bcb91..bf2b54c 100644 --- a/bridge/agent.js +++ b/bridge/agent.js @@ -341,7 +341,9 @@ function attachHudPass() { // Only supplemental draw calls are suppressed; the normal game draw runs. const symbols={hud_pass:hudPassFlag}; let code='extern unsigned int hud_pass;\n'; - const names=['TabbedWindowOnRender','ChoiceBoxOnRender','MouseControlOnRender','CommandGuiRenderPause','StarMapOnRender']; + // Esc and Options are separate render methods from the ship/store tabs. + // They belong only to the normal screen capture used by the world panel. + const names=['TabbedWindowOnRender','ChoiceBoxOnRender','MouseControlOnRender','CommandGuiRenderPause','StarMapOnRender','MenuScreenOnRender','OptionsScreenOnRender']; for (let i=0;i void: func _gameplay_hud_mode() -> bool: - if not bool(hud_state.get("ready", demo_mode)) or str(hud_state.get("ui_mode", "")) == "menu": + if not bool(hud_state.get("ready", demo_mode)): + return false + # In-run native windows have a separate world surface. A simultaneous + # native menu flag must not expand the HUD into a second copy of that window. + if str(hud_state.get("ui_mode", "")) == "menu" and not bool(hud_state.get("panel_open", false)): return false # Desktop preview still needs the complete screen for its Tactical/map view; # in VR those views have their own hand/world surfaces with native coordinates. diff --git a/tools/launch.py b/tools/launch.py index 2430e82..0ed371d 100644 --- a/tools/launch.py +++ b/tools/launch.py @@ -61,7 +61,7 @@ def preflight(config, desktop=False): raise ValueError('Regenerate lab hooks.json with tools/resolve_hooks.py for controller modifiers') if not all(name in hooks.get('rvas',{}) for name in ('OnTextInput','OnTextEvent','TextInputOnRender','TextInputStart')): raise ValueError('Regenerate lab hooks.json with tools/resolve_hooks.py for native renaming') - if not all(name in hooks.get('rvas',{}) for name in ('CommandGuiRenderStatic','CommandGuiRenderPause','TabbedWindowOnRender','ChoiceBoxOnRender','MouseControlOnRender','StarMapOnRender')): + if not all(name in hooks.get('rvas',{}) for name in ('CommandGuiRenderStatic','CommandGuiRenderPause','TabbedWindowOnRender','ChoiceBoxOnRender','MouseControlOnRender','StarMapOnRender','MenuScreenOnRender','OptionsScreenOnRender')): raise ValueError('Regenerate lab hooks.json with tools/resolve_hooks.py for live native HUD capture') if not marker.get('activated') or not (lab/'FTLGame.exe').is_file() or hashlib.sha1((lab/'FTLGame.exe').read_bytes()).hexdigest() != hooks.get('sha1'): raise ValueError('Lab is not activated or its executable fingerprint changed') diff --git a/tools/resolve_hooks.py b/tools/resolve_hooks.py index 2a1608c..222d809 100644 --- a/tools/resolve_hooks.py +++ b/tools/resolve_hooks.py @@ -79,7 +79,8 @@ def resolve(executable: Path, signatures: Path) -> dict: # render advances ship, jump or projectile animations. for class_name,method_names in {'CommandGui':('RenderStatic','RenderPause'), 'TabbedWindow':('OnRender',),'ChoiceBox':('OnRender',), - 'MouseControl':('OnRender',),'StarMap':('OnRender',)}.items(): + 'MouseControl':('OnRender',),'StarMap':('OnRender',), + 'MenuScreen':('OnRender',),'OptionsScreen':('OnRender',)}.items(): source=(signatures.parent/(class_name+'.zhl')).read_text() for signature,name in re.findall(r'"([.0-9a-fA-F?]+)":[^\n]*\n[^\n]*'+class_name+r'::(\w+)\(',source): if name not in method_names: continue diff --git a/tools/run_bridge.py b/tools/run_bridge.py index 22e3620..6c39496 100644 --- a/tools/run_bridge.py +++ b/tools/run_bridge.py @@ -129,7 +129,9 @@ def room_pixel(state, ship, room_id): def full_screen_capture(state): # Native shops, upgrades, crew and equipment are world panels, never HUDs. - return not state.get('ready') or state.get('ui_mode','menu') == 'menu' + # An in-run window owns its pixels even if the native menu flag also sets + # ui_mode to menu. Initial/hangar screens keep their full headset view. + return not state.get('ready') or (state.get('ui_mode','menu') == 'menu' and not state.get('panel_open')) def world_gameplay_available(state): diff --git a/tools/test_bridge.py b/tools/test_bridge.py index 2f2c351..8aec478 100644 --- a/tools/test_bridge.py +++ b/tools/test_bridge.py @@ -56,6 +56,13 @@ class BridgeTests(unittest.TestCase): self.assertTrue(full_screen_capture({'ready':True,'ui_mode':'menu'})) self.assertFalse(full_screen_capture(dict(base,panel_open=True,panel_kind='shop'))) + def test_in_run_modal_window_never_duplicates_on_full_headset_screen(self): + for kind in ('ship', 'window', 'options', 'sell'): + with self.subTest(kind=kind): + self.assertFalse(full_screen_capture({'ready':True,'ui_mode':'menu', + 'panel_open':True,'panel_kind':kind})) + self.assertTrue(full_screen_capture({'ready':False,'ui_mode':'menu','panel_open':True})) + def test_enemy_frame_removed_without_touching_player_widgets(self): pixels=np.full((720,1280,4),255,dtype=np.uint8) result=remove_enemy_hud(pixels) diff --git a/tools/test_hud_layout.gd b/tools/test_hud_layout.gd index d874b23..adf26db 100644 --- a/tools/test_hud_layout.gd +++ b/tools/test_hud_layout.gd @@ -60,6 +60,10 @@ func run() -> void: scene._position_hud(0.0, true) check(scene.hud_anchor.blocked and scene.hud_anchor.lift > 0, "Looking down toward the ship must engage the HUD height stop") _check_clearance(scene, "Looking down must not tunnel the HUD through the ship") + var stopped_ship: AABB = Anchor.transformed_bounds(scene._player_hud_local_bounds(), scene.player_ship.global_transform) + var stopped_panel: AABB = Anchor.panel_bounds(scene.hud_surface.global_transform, scene.hud_surface.surface_size) + var gap: float = stopped_panel.position.y - stopped_ship.end.y + check(gap >= .08 and gap < .10, "The blocked HUD must sit within ten centimetres of the ship while leaving a safe gap") var clearance_lift: float = scene.hud_anchor.lift scene.camera.rotation.y = PI scene._position_hud(1.0 / 90) diff --git a/tools/test_input.gd b/tools/test_input.gd index aaafe00..4f67d3b 100644 --- a/tools/test_input.gd +++ b/tools/test_input.gd @@ -107,6 +107,15 @@ func run() -> void: scene._position_hud(0.0, true) check(is_equal_approx(scene.nav_panel.scale.x, 0.6) and scene.hud_surface.get_parent()==scene and scene.hud_surface.source_rect==scene.GAMEPLAY_HUD_RECT,"Gameplay HUD must retain native upper/crew pixels, exclude the bottom strip and use independent world placement") check(scene.hud_surface.render_order == 100 and scene.panel_theme.get_child(1).material_override.render_priority > scene.pause_label.render_priority and scene.tracking_label.render_priority < 120,"Hand screen must render over all HUD layers, including pause and modifier labels") + var hud_mode_state: Dictionary = scene.hud_state.duplicate(true) + scene.hud_state = {"ready":true,"ui_mode":"menu","panel_open":true} + scene._sync_hud_layout() + check(scene.gameplay_hud and scene.hud_surface.source_rect == scene.GAMEPLAY_HUD_RECT,"An in-run world window must not promote the headset HUD into a second full menu") + scene.hud_state = {"ready":false,"ui_mode":"menu","panel_open":true} + scene._sync_hud_layout() + check(not scene.gameplay_hud and scene.hud_surface.source_rect == Rect2(0,0,1280,720),"Initial and hangar menus must keep their full headset display") + scene.hud_state = hud_mode_state + scene._sync_hud_layout() check(scene.help_label.get_script().resource_path.ends_with("controller_help.gd"),"Help must use a controller infographic instead of plain text") check(scene.hover_marker.mesh.radius <= 0.003 and scene.right_laser.mesh.top_radius <= 0.001,"Pointer endpoint must support precise native UI selection") scene.wheel.global_position = scene.camera.get_camera_transform().origin + Vector3(0.3,-0.5,-0.4) diff --git a/tools/test_launch_setup.py b/tools/test_launch_setup.py index 1f14595..657bb75 100644 --- a/tools/test_launch_setup.py +++ b/tools/test_launch_setup.py @@ -36,7 +36,7 @@ class LaunchSetupTests(unittest.TestCase): names = ("GetShiftState", "GetCtrlState", "ForceAutofireFlag", "OnTextInput", "OnTextEvent", "TextInputOnRender", "TextInputStart", "CommandGuiRenderStatic", "CommandGuiRenderPause", "TabbedWindowOnRender", "ChoiceBoxOnRender", - "MouseControlOnRender", "StarMapOnRender") + "MouseControlOnRender", "StarMapOnRender", "MenuScreenOnRender", "OptionsScreenOnRender") self.hooks = self.lab / "hooks.json" self.hooks.write_text(json.dumps({"rvas": dict.fromkeys(names, 1), "sha1": hashlib.sha1((self.lab / "FTLGame.exe").read_bytes()).hexdigest()})) @@ -63,6 +63,15 @@ class LaunchSetupTests(unittest.TestCase): def test_shared_preflight_accepts_complete_desktop_setup(self): self.assertEqual(launch.preflight(self.config, desktop=True)["mode"], "desktop") + def test_old_hooks_require_modal_hud_capture_update(self): + value = json.loads(self.hooks.read_text()) + for name in ("MenuScreenOnRender", "OptionsScreenOnRender"): + with self.subTest(name=name): + incomplete = dict(value, rvas={key: address for key, address in value["rvas"].items() if key != name}) + self.hooks.write_text(json.dumps(incomplete)) + with self.assertRaisesRegex(ValueError, "Regenerate lab hooks.json.*live native HUD capture"): + launch.preflight(self.config, desktop=True) + def test_missing_configuration_explains_setup(self): with self.assertRaisesRegex(ValueError, "Run SETUP.cmd"): launch.load_config(self.local / "launcher.json")