diff --git a/Makefile b/Makefile index 45abb20..8ea9db2 100644 --- a/Makefile +++ b/Makefile @@ -111,6 +111,7 @@ check: export-windows: mkdir -p build/windows $(GODOT) --headless --path . --export-release "Windows Desktop" + $(MAKE) test-exported-opening # Pixel cutaway: interactive disposable inn, capture and input regression. .PHONY: preview-pixel capture-pixel test-pixel-ui @@ -187,3 +188,9 @@ test-stairs: test-transport-comparison: $(GODOT) --headless --path . --script res://tests/transport/test_transport_comparison.gd $(GODOT) --headless --path . --script res://tests/transport/test_runtime_transport_comparison.gd + +# Check the actual shipped data outside this checkout, including first build. +WINDOWS_PCK ?= build/windows/TheTree.pck +.PHONY: test-exported-opening +test-exported-opening: + python3 tools/verify_exported_opening.py "$(WINDOWS_PCK)" --godot "$(GODOT)" diff --git a/docs/qa/WINDOWS_RECEPTION_EXPORT_FIX.md b/docs/qa/WINDOWS_RECEPTION_EXPORT_FIX.md new file mode 100644 index 0000000..1fe4a54 --- /dev/null +++ b/docs/qa/WINDOWS_RECEPTION_EXPORT_FIX.md @@ -0,0 +1,26 @@ +# Windows配布版で建築候補が空になる問題 + +対象: 最初のWindows配布 `fb4ad23`。受付が建築候補に現れず開業できないという報告を、配布済みPCKで再現した。 + +## 原因と修正 + +GodotのRelease出力ではFacilityの `.tres` が `.res` に変換され、元のパスは `.remap` を通して読み込まれる。DefinitionRegistryが `FileAccess.file_exists` で元の物理ファイルを調べていたため、全18施設を `resource_not_found` として除外していた。リソースの存在判定を `ResourceLoader.exists` に変更し、既存の型・内容・IDの検証は維持した。 + +制作フォルダーからPCKを実行すると、物理ファイルがローカルにあるため不具合が隠れる。前回の単なる起動smokeでも候補の欠落は検出できなかった。 + +## 検証 + +- 制作フォルダー外の一時ディレクトリで、配布済みPCKだけを起動。`FileAccess=false / ResourceLoader=true` を確認し、18施設の欠落・パレット空を再現。 +- 修正版PCKを同じ隔離条件で起動し、13項目成功。制作ファイルへのフォールバックなし、全18施設、受付ボタンの表示・選択、初日ガイドからの選択、マップ配置、500Gの支払い、120分後の完成、客室建設への案内を確認。 +- macOS版Godotによる配布PCKの実描画で受付完成と建築候補を確認。Windows実機での再検証は未実施。 +- DefinitionRegistryの既存266項目が成功し、存在しないデータの拒否も維持。 + +`make export-windows` は以後、書き出し後に隔離した実建設チェックを実行する。既存PCKの検証は次のコマンドで行える。 + +``` +make test-exported-opening WINDOWS_PCK=/absolute/path/TheTree.pck +``` + +`tools/verify_exported_opening.py` が一時ディレクトリから `tests/export/test_exported_opening.gd` を実行する。試験では保存を無効にし、実際の建設UIとCommandを使う。制作ファイルが見つかった場合も失敗させる。 + +証跡: `artifacts/windows-opening-before-isolated.log`、`windows-opening-fixed-visual.log`、`windows-registry-fix.log`、`windows-reception-fixed.png`。修正版配布は `build/windows/TheTree-Windows-reception-fix.zip`。EXEとPCKを含めて全体を展開し、新しいフォルダーから起動する。 diff --git a/scripts/core/definition_registry.gd b/scripts/core/definition_registry.gd index ca7bdbc..7b8fea3 100644 --- a/scripts/core/definition_registry.gd +++ b/scripts/core/definition_registry.gd @@ -266,7 +266,9 @@ func _validate_requested_path(path: String) -> bool: "Facility definitions must use the .tres extension." ) valid = false - if not FileAccess.file_exists(path): + # Exported .tres resources are remapped to binary .res files. Physical + # FileAccess checks incorrectly reject them when no source checkout exists. + if not ResourceLoader.exists(path): _add_error( &"resource_not_found", &"", diff --git a/tests/export/test_exported_opening.gd b/tests/export/test_exported_opening.gd new file mode 100644 index 0000000..38fa63a --- /dev/null +++ b/tests/export/test_exported_opening.gd @@ -0,0 +1,67 @@ +extends SceneTree + +## Run against the distributed pack, not the source checkout: +## godot --headless --main-pack /absolute/TheTree.pck --script /absolute/this_script.gd +var _failures := 0 +var _checks := 0 + +func _initialize() -> void: + call_deferred("_run") + +func _run() -> void: + var runtime: Variant = root.get_node("SimulationRuntime") + var clock: Variant = root.get_node("GameClock") + clock.set_paused(true) + runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false}) + if OS.get_environment("THE_TREE_REQUIRE_EXPORTED") == "1": + _expect(not FileAccess.file_exists("res://project.godot") and not FileAccess.file_exists("res://data/facilities/reception.tres"), "release regression has no source checkout fallback") + print("RECEPTION_RESOURCE file=%s resource=%s" % [FileAccess.file_exists("res://data/facilities/reception.tres"), ResourceLoader.exists("res://data/facilities/reception.tres")]) + _expect(runtime.definition_registry.is_valid(), "exported facility definitions validate: %s" % runtime.definition_registry.get_report()) + _expect(runtime.definition_registry.facility_count() == 18, "all 18 buildable definitions are present in the shipped pack") + var main: Control = load("res://scenes/main/main.tscn").instantiate() + root.add_child(main) + runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false}) + await process_frame + await process_frame + var buttons: Dictionary = main.get("build_buttons") + _expect(buttons.size() == 18, "the actual build palette exposes all 18 facilities") + _expect(buttons.has(&"reception"), "reception is selectable in the actual build palette") + if not buttons.has(&"reception"): + _finish() + return + _expect("受付" in buttons[&"reception"].text and not buttons[&"reception"].disabled, "reception button is labeled and enabled") + var guide: Variant = main.find_child("InnOpeningGuide", true, false) + _expect(guide != null and guide.get_progress().get(&"action") == &"build_reception", "new-game guide offers reception construction") + if guide != null: + (guide.get("_action") as Button).pressed.emit() + await process_frame + _expect(main.get("palette_scroll").visible and buttons[&"reception"].is_visible_in_tree(), "guide action opens the palette with a visible reception button") + buttons[&"reception"].button_pressed = false + buttons[&"reception"].button_pressed = true + _expect(main.get("_selected_definition_id") == &"reception", "the reception button selects the real construction tool") + main.call("_on_map_cell_clicked", Vector3i(-8, 0, 0)) + _expect(runtime.construction_service.get_jobs().size() == 1, "map placement starts one real reception construction job") + _expect(runtime.economy_service.get_balance(&"PLAYER_CASH") == 4500, "reception construction charges the actual 500 G cost") + clock.advance_minutes(120) + var reception: Variant = null + for facility: Variant in runtime.facility_service.get_instances(): + if facility.definition_id == &"reception": + reception = facility + _expect(reception != null, "the reception completes from the exported game data") + _expect(guide != null and guide.get_progress().get(&"stage") == &"room", "opening guide advances to building a room") + if OS.get_environment("THE_TREE_EXPORT_CAPTURE") == "1": + main.call("_refresh_dashboard") + await process_frame + await RenderingServer.frame_post_draw + root.get_texture().get_image().save_png(OS.get_environment("THE_TREE_EXPORT_CAPTURE_PATH")) + _finish() + +func _expect(ok: bool, label: String) -> void: + _checks += 1 + if not ok: + _failures += 1 + print("%s: %s" % ["PASS" if ok else "FAIL", label]) + +func _finish() -> void: + print("Exported opening: %d checks, %d failures" % [_checks, _failures]) + quit(1 if _failures else 0) diff --git a/tools/verify_exported_opening.py b/tools/verify_exported_opening.py new file mode 100644 index 0000000..811cd77 --- /dev/null +++ b/tools/verify_exported_opening.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Exercise the distributed PCK with no source-tree fallback or player saves.""" +import argparse +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("pack", type=Path) + parser.add_argument("--godot", default="godot") + parser.add_argument("--capture", type=Path) + args = parser.parse_args() + pack = args.pack.resolve(strict=True) + godot = shutil.which(args.godot) + if godot is None: + parser.error(f"Godot executable not found: {args.godot}") + script = Path(__file__).resolve().parents[1] / "tests/export/test_exported_opening.gd" + env = dict(os.environ, THE_TREE_REQUIRE_EXPORTED="1") + command = [godot] + if args.capture: + output = args.capture.resolve() + output.parent.mkdir(parents=True, exist_ok=True) + env.update(THE_TREE_EXPORT_CAPTURE="1", THE_TREE_EXPORT_CAPTURE_PATH=str(output)) + else: + command.append("--headless") + command.extend(["--main-pack", str(pack), "--script", str(script)]) + # Running inside the checkout masks missing packed resources: FileAccess + # can read original .tres files from disk even when the PCK only has .remap. + with tempfile.TemporaryDirectory(prefix="the-tree-export-check-") as isolated: + result = subprocess.run(command, cwd=isolated, env=env, timeout=180) + return result.returncode + + +if __name__ == "__main__": + raise SystemExit(main())