460 lines
22 KiB
GDScript
460 lines
22 KiB
GDScript
extends RefCounted
|
|
## Diagnostic setup, never natural-play or OS-input proof.
|
|
const Cleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd")
|
|
const Codec := preload("res://tests/fixtures/p08-02/mixed_world.gd")
|
|
const MEMBERS := ["world.bin", "manifest.json", "thumbnail.png", "journal.log"]
|
|
var checks := 0
|
|
var failures := 0
|
|
var tree: SceneTree
|
|
var runtime: Variant
|
|
var clock: Variant
|
|
var main: Variant
|
|
var directory: String
|
|
var role: String
|
|
var actions: Array = []
|
|
var results: Array = []
|
|
var serial := 0
|
|
var target := Vector3i(-8, 0, 0)
|
|
var budget: Dictionary = {}
|
|
var input_method := "handler"
|
|
var observed_cell: Variant = null
|
|
var save_serial := 0
|
|
var save_records: Array = []
|
|
var operation_results: Array[Dictionary] = []
|
|
const INITIAL_SAVE := "user://saves/p08_initial"
|
|
|
|
func annotate(row: Dictionary) -> void:
|
|
row["input_method"] = input_method
|
|
|
|
func selected_cell(cell: Vector3i) -> void:
|
|
observed_cell = cell
|
|
|
|
|
|
func expect(ok: bool, message: String) -> void:
|
|
checks += 1
|
|
if not ok:
|
|
failures += 1
|
|
push_error("P08 placement: " + message)
|
|
|
|
func json_file(name: String, data: Variant) -> void:
|
|
var file := FileAccess.open(directory.path_join(name), FileAccess.WRITE)
|
|
expect(file != null, "write " + name)
|
|
if file != null:
|
|
file.store_string(JSON.stringify(data, "\t") + "\n")
|
|
file.close()
|
|
|
|
func capture(label: String) -> String:
|
|
return capture_model(label, runtime.create_snapshot())
|
|
|
|
func capture_model(label: String, snapshot: Dictionary) -> String:
|
|
var name := role + "-" + label
|
|
json_file(name + ".json", Codec.new().typed(snapshot))
|
|
var file := FileAccess.open(directory.path_join(name + ".var"), FileAccess.WRITE)
|
|
if file != null:
|
|
file.store_buffer(var_to_bytes(snapshot))
|
|
file.close()
|
|
else:
|
|
expect(false, "raw Variant " + name)
|
|
return name
|
|
|
|
func vector(cell: Vector3i) -> Array:
|
|
return [cell.x, cell.y, cell.z]
|
|
|
|
func affine(value: Transform2D) -> Array:
|
|
return [value.x.x, value.x.y, value.y.x, value.y.y, value.origin.x, value.origin.y]
|
|
|
|
func observe(result: CommandResult) -> void:
|
|
results.append(result.to_dictionary())
|
|
|
|
func frames() -> void:
|
|
await tree.process_frame
|
|
await tree.process_frame
|
|
|
|
func button(node: BaseButton) -> void:
|
|
# Actual production button signal/connection, explicitly not native OS.
|
|
expect(node != null and not node.disabled, "available production button")
|
|
if node == null or node.disabled:
|
|
return
|
|
if node.toggle_mode:
|
|
node.button_pressed = not node.button_pressed
|
|
node.pressed.emit()
|
|
await frames()
|
|
|
|
func choose(id: StringName) -> void:
|
|
if not main.palette_scroll.visible:
|
|
await button(main.palette_toggle_button)
|
|
var node: Button = main.build_buttons[id]
|
|
await reveal(node)
|
|
if not node.button_pressed:
|
|
await button(node)
|
|
expect(node.button_pressed and main._selected_definition_id == id, "chosen production definition")
|
|
|
|
func click(cell: Vector3i) -> Dictionary:
|
|
var view: WorldPreview = main.world_preview
|
|
var local := view.cell_to_canvas(cell)
|
|
var viewport := view.get_global_transform_with_canvas() * local
|
|
var event := InputEventMouseButton.new()
|
|
event.button_index = MOUSE_BUTTON_LEFT
|
|
event.pressed = true
|
|
event.position = local
|
|
# This is a handler test through the real WorldPreview, not an OS receipt.
|
|
observed_cell = null
|
|
view._gui_input(event)
|
|
await frames()
|
|
return {"world_cell": vector(cell), "selected_cell": vector(observed_cell if observed_cell is Vector3i else view.canvas_to_cell(local)),
|
|
"local": [local.x, local.y], "viewport": [viewport.x, viewport.y],
|
|
"viewport_transform": affine(view.get_global_transform_with_canvas()),
|
|
"map_position": [view.get_map_rect().position.x, view.get_map_rect().position.y],
|
|
"cell_dimensions": [view.get_cell_rect(cell).size.x, view.get_cell_rect(cell).size.y],
|
|
"bounds": runtime.world_service.get_bounds(), "zoom": view.zoom_level,
|
|
"pan": [view.pan_offset.x, view.pan_offset.y]}
|
|
|
|
func build(id: StringName, cell: Vector3i, label: String, expected: String = "") -> void:
|
|
await choose(id)
|
|
if label == "unsupported":
|
|
cell = unsupported_cell()
|
|
var report: Dictionary = main.world_preview.get_build_preview_report(cell)
|
|
var before := capture(label + "-before")
|
|
results.clear()
|
|
var position := await click(cell)
|
|
var status: String = main.build_status_label.text
|
|
var after := capture(label + "-after")
|
|
if expected.is_empty():
|
|
expect(not results.is_empty() and results.back().code == "OK", label + " accepted")
|
|
elif expected == "STAIR_LANDING_MISSING":
|
|
expect(not results.is_empty() and results.back().details.get("reason") == expected, label + " domain refusal")
|
|
else:
|
|
expect(expected in report.error_codes and results.is_empty(), label + " preview refusal")
|
|
var row := {"id": label, "kind": "build", "definition": String(id), "expected": expected,
|
|
"report": report, "before": before, "after": after, "position": position,
|
|
"results": results.duplicate(true), "status": status, "input_method": "handler"}
|
|
annotate(row)
|
|
actions.append(row)
|
|
if expected.is_empty():
|
|
await complete_build(id)
|
|
row["completed"] = capture(label + "-completed")
|
|
|
|
func unsupported_cell() -> Vector3i:
|
|
return Vector3i(4, 4, 0)
|
|
|
|
func demolish(cell: Vector3i, label: String, cancel := false, expected := "") -> void:
|
|
if not main.palette_scroll.visible:
|
|
await button(main.palette_toggle_button)
|
|
if not main._demolition_mode:
|
|
await button(main.demolition_button)
|
|
var before := capture(label + "-before")
|
|
results.clear()
|
|
var position := await click(cell)
|
|
var instance_id: String = String(main._demolition_target)
|
|
expect(main.demolition_dialog.visible, "normal confirmation visible")
|
|
if cancel:
|
|
await button(main.demolition_dialog.get_cancel_button())
|
|
else:
|
|
await button(main.demolition_dialog.get_ok_button())
|
|
await frames()
|
|
if cancel:
|
|
expect(results.is_empty(), "cancel submits no command")
|
|
else:
|
|
expect(not results.is_empty(), "confirmation submitted Runtime command")
|
|
if not results.is_empty():
|
|
expect(results.back().code == ("OK" if expected.is_empty() else "VALIDATION_FAILED"), label + " result code")
|
|
expect(String(results.back().details.get("reason", "")) == expected, label + " result reason")
|
|
var row := {"id": label, "kind": "cancel" if cancel else "demolish", "expected": expected,
|
|
"instance_id": instance_id, "position": position, "before": before, "after": capture(label + "-after"),
|
|
"results": results.duplicate(true), "status": main.build_status_label.text, "input_method": input_method}
|
|
annotate(row)
|
|
actions.append(row)
|
|
|
|
func camera() -> void:
|
|
await button(main.pan_button)
|
|
var view: WorldPreview = main.world_preview
|
|
var before := capture("pan-before")
|
|
var original := view.cell_to_canvas(target)
|
|
var event := InputEventMouseButton.new()
|
|
event.button_index = MOUSE_BUTTON_WHEEL_UP
|
|
event.pressed = true
|
|
event.factor = 1.0
|
|
event.position = view.size * 0.5
|
|
event.ctrl_pressed = false
|
|
var zoom: float = view.zoom_level
|
|
var original_pan: Vector2 = view.pan_offset
|
|
await wheel(event, true)
|
|
expect(view.zoom_level == zoom, "move-mode wheel pans without Ctrl")
|
|
var moved_pan: Vector2 = view.pan_offset
|
|
expect(moved_pan != original_pan, "move-mode wheel actually moves the camera")
|
|
await button(main.pan_button)
|
|
var steps: Array = []
|
|
for direction: int in [MOUSE_BUTTON_WHEEL_DOWN, MOUSE_BUTTON_WHEEL_UP]:
|
|
for unused: int in range(9):
|
|
event.button_index = direction
|
|
await wheel(event, false)
|
|
steps.append(view.zoom_level)
|
|
expect(view.zoom_level == (0.5 if direction == MOUSE_BUTTON_WHEEL_DOWN else 4.0), "existing zoom clamp")
|
|
# Use transformed pixels; camera state is not required to survive save/load.
|
|
var position := await click(target)
|
|
expect(position.world_cell == position.selected_cell, "camera world identity")
|
|
expect(view.cell_to_canvas(target) != original, "pixels transform")
|
|
var row := {"id": "pan_zoom", "kind": "camera", "before": before, "after": capture("pan-after"),
|
|
"position": position, "original_local": [original.x, original.y], "steps": steps,
|
|
"pan_before": [original_pan.x, original_pan.y], "pan_after": [moved_pan.x, moved_pan.y],
|
|
"ctrl_pressed": false, "input_method": input_method}
|
|
annotate(row)
|
|
actions.append(row)
|
|
|
|
func diagnostic_budget() -> void:
|
|
var state: Variant = tree.root.get_node("GameState")
|
|
var needed := {&"gold": 0, &"materials": 0}
|
|
var costs: Array = []
|
|
for id: StringName in [&"reception", &"reception", &"guest_stairs", &"guest_stairs"]:
|
|
var definition: FacilityDefinition = runtime.definition_registry.get_facility(id)
|
|
costs.append({"definition": String(id), "cost": definition.construction_cost.duplicate(true)})
|
|
for key: StringName in needed:
|
|
needed[key] += int(definition.construction_cost.get(key, 0))
|
|
var deltas := {}
|
|
for key: StringName in needed:
|
|
if needed[key] > state.get_resource(key):
|
|
deltas[key] = needed[key] - state.get_resource(key)
|
|
budget = {"setup": "diagnostic", "costs": costs, "needed": needed, "initial": state.resources.duplicate(true), "deltas": deltas}
|
|
if not deltas.is_empty():
|
|
expect(runtime.submit_command(&"change_resources", {&"deltas": deltas}).is_success(), "definition-derived diagnostic budget")
|
|
|
|
func writer() -> void:
|
|
diagnostic_budget()
|
|
await save_initial_fixture()
|
|
await build(&"standard_room", Vector3i(4, 4, 0), "unsupported", "UNSUPPORTED")
|
|
await build(&"reception", Vector3i(runtime.world_service.get_bounds()[&"max_x"], 0, 0), "out_of_bounds", "OUT_OF_BOUNDS")
|
|
await build(&"reception", target, "supported_connected")
|
|
await build(&"reception", target, "overlap", "OVERLAP")
|
|
# Existing reception supports this upper cell, but provides no stair landing.
|
|
await build(&"guest_stairs", Vector3i(-8, 2, 0), "missing_connection", "STAIR_LANDING_MISSING")
|
|
await build(&"guest_stairs", Vector3i(4, 1, 0), "lower_stair")
|
|
await build(&"guest_stairs", Vector3i(4, 2, 0), "upper_stair")
|
|
await demolish(Vector3i(4, 1, 0), "refused_demolition", false, "STAIR_LANDING_IN_USE")
|
|
await demolish(target, "cancel_demolition", true)
|
|
await demolish(target, "demolition")
|
|
await build(&"reception", target, "rebuild")
|
|
await camera()
|
|
capture("presave")
|
|
await button(main.find_child("ManualSaveButton", true, false))
|
|
expect(main.build_status_label.text == "手動セーブを保存しました。", "normal save")
|
|
capture("checkpoint")
|
|
await archive_formal()
|
|
var formal := inspect_formal()
|
|
var presave: Dictionary = bytes_to_var(FileAccess.get_file_as_bytes(directory.path_join("writer-presave.var")))
|
|
expect(var_to_bytes(presave) == var_to_bytes(formal), "whole presave formal model")
|
|
verify_operation(formal, operation_results.back(), &"SAVE_CHECKPOINT_SUCCEEDED")
|
|
var members := {}
|
|
for member: String in MEMBERS:
|
|
members[member] = FileAccess.get_sha256(directory.path_join("formal/" + member))
|
|
json_file("save.json", {"members": members, "instance_id": String(runtime.world_service.get_occupant(target)), "cell": vector(target), "pid": OS.get_process_id()})
|
|
|
|
func reader(distinct: bool) -> void:
|
|
var saved: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("save.json")))
|
|
if distinct:
|
|
expect(saved.pid != OS.get_process_id(), "separate actual writer/reader PID")
|
|
# Real load menu callback, no direct restore_snapshot or model mutation.
|
|
var menu: MenuButton = main.find_child("SaveLoadMenu", true, false)
|
|
await load_menu(menu)
|
|
await frames()
|
|
expect(main.build_status_label.text == "手動セーブを読み込みました。", "normal resume")
|
|
capture("checkpoint")
|
|
var formal := inspect_formal()
|
|
var original := FileAccess.get_file_as_bytes(directory.path_join("writer-formal-model.var"))
|
|
expect(original == var_to_bytes(formal), "whole formal resume model")
|
|
verify_operation(formal, operation_results.back(), &"SAVE_RECOVERY_LOADED")
|
|
expect(String(runtime.world_service.get_occupant(target)) == saved.instance_id, "same resumed instance/cell")
|
|
for member: String in MEMBERS:
|
|
expect(FileAccess.get_sha256(directory.path_join("formal/" + member)) == saved.members[member], "reader preserves formal member " + member)
|
|
await build(&"reception", target, "resume_overlap", "OVERLAP")
|
|
|
|
func run(p_tree: SceneTree, output: String, requested_role: String) -> Dictionary:
|
|
# Handler entry is never an OS evidence route. Passive subclass overrides endpoints.
|
|
tree = p_tree
|
|
directory = output
|
|
role = "reader" if requested_role == "reader" else "writer"
|
|
expect(DirAccess.make_dir_recursive_absolute(directory) == OK, "diagnostic directory")
|
|
runtime = tree.root.get_node("SimulationRuntime")
|
|
clock = tree.root.get_node("GameClock")
|
|
runtime.save_operation_completed.connect(record_save_operation)
|
|
await setup()
|
|
runtime.command_completed.connect(observe)
|
|
main.world_preview.cell_clicked.connect(selected_cell)
|
|
await frames()
|
|
if role == "writer":
|
|
await writer()
|
|
else:
|
|
await reader(true)
|
|
json_file(role + "-actions.json", {"setup": "diagnostic", "input_method": input_method, "budget": budget, "actions": actions})
|
|
if requested_role == "standalone":
|
|
role = "reader"
|
|
actions = []
|
|
save_records = []
|
|
await reader(false)
|
|
json_file(role + "-actions.json", {"setup": "diagnostic", "input_method": input_method, "actions": actions})
|
|
finish_observations()
|
|
runtime.command_completed.disconnect(observe)
|
|
runtime.save_operation_completed.disconnect(record_save_operation)
|
|
var cleanup: Dictionary = await Cleanup.free_owner_and_wait(tree, main)
|
|
expect(cleanup.get(&"passed", false), "ordinary Main/audio cleanup")
|
|
json_file(role + "-process.json", {"pid": OS.get_process_id(), "engine": Engine.get_version_info(), "checks": checks, "failures": failures, "role": requested_role, "setup": "diagnostic"})
|
|
return {"checks": checks, "failures": failures}
|
|
|
|
func reveal(node: Control) -> void:
|
|
main.palette_scroll.ensure_control_visible(node)
|
|
await frames()
|
|
|
|
func wheel(event: InputEventMouseButton, _move_mode: bool) -> void:
|
|
main.world_preview._gui_input(event)
|
|
await frames()
|
|
|
|
func complete_build(id: StringName) -> void:
|
|
clock.advance_minutes(runtime.definition_registry.get_facility(id).build_minutes)
|
|
await frames()
|
|
|
|
func load_menu(menu: MenuButton) -> void:
|
|
menu.get_popup().id_pressed.emit(0)
|
|
|
|
func setup() -> void:
|
|
clock.reset(1, 6, 0)
|
|
tree.root.get_node("GameState").reset()
|
|
runtime.initialize(clock, tree.root.get_node("GameState"), 42533)
|
|
clock.set_paused(true)
|
|
tree.root.size = Vector2i(1440, 810)
|
|
main = load("res://scenes/main/main.tscn").instantiate()
|
|
main.manual_save_path = "user://saves/manual_ui"
|
|
if role == "reader":
|
|
import_package(directory.path_join("formal"), String(main.manual_save_path))
|
|
tree.root.add_child(main)
|
|
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
|
|
|
func save_initial_fixture() -> void:
|
|
# Explicit diagnostic input, not an OS action or natural-play save.
|
|
var saved: Dictionary = SaveService.save_snapshot(INITIAL_SAVE, runtime.create_snapshot())
|
|
record_save_result("initial_save", INITIAL_SAVE, saved)
|
|
expect(bool(saved.get("ok", false)), "diagnostic initial formal save")
|
|
export_package(INITIAL_SAVE, "initial-formal")
|
|
# Seed the private normal slot, so its later normal save produces a backup.
|
|
import_package(ProjectSettings.globalize_path(INITIAL_SAVE), String(main.manual_save_path))
|
|
json_file("diagnostic-budget.json", budget)
|
|
|
|
func archive_formal() -> void:
|
|
export_package(String(main.manual_save_path), "formal")
|
|
|
|
func inspect_formal() -> Dictionary:
|
|
var stored := SaveService.inspect_snapshot(String(main.manual_save_path))
|
|
record_save_result("formal_inspect", String(main.manual_save_path), stored)
|
|
expect(bool(stored.get(&"ok", false)), "read-only formal inspection")
|
|
var formal: Dictionary = stored.get(&"snapshot", {})
|
|
capture_model("formal-model", formal)
|
|
return formal
|
|
|
|
func verify_operation(formal: Dictionary, result: Dictionary, event_type: StringName, prefix: String = "") -> void:
|
|
var expected := formal.duplicate(true)
|
|
var model: Dictionary = expected[&"systems"][&"runtime"]
|
|
var counters: Dictionary = model[&"id_generator"]["counters"]
|
|
var next: int = int(counters.get("domain_event", 0)) + 1
|
|
counters["domain_event"] = next
|
|
var event_id := "domain_event.%012d" % next
|
|
var payload := {
|
|
&"ok": bool(result.get(&"ok", false)),
|
|
&"code": StringName(result.get(&"code", &"UNKNOWN")),
|
|
&"message": str(result.get(&"message", "")),
|
|
&"operation": StringName(result.get(&"operation", &"unknown")),
|
|
&"save_path": str(result.get(&"save_path", result.get(&"path", ""))),
|
|
&"reason": StringName(result.get(&"reason", &"")),
|
|
&"request_id": int(result.get(&"request_id", 0)),
|
|
&"checkpoint_absolute_minute": int(result.get(&"checkpoint_absolute_minute", formal[&"clock"][&"absolute_minute"])),
|
|
&"duration_us": int(result.get(&"duration_us", 0)),
|
|
&"runtime_duration_us": int(result.get(&"runtime_duration_us", 0)),
|
|
}
|
|
if result.get(&"selected") is Dictionary:
|
|
var selected: Dictionary = result[&"selected"]
|
|
payload[&"selected_candidate_id"] = StringName(selected.get(&"id", &""))
|
|
payload[&"selected_candidate_kind"] = StringName(selected.get(&"kind", &""))
|
|
payload[&"selected_generation"] = int(selected.get(&"generation", -1))
|
|
if result.get(&"manifest") is Dictionary:
|
|
var manifest: Dictionary = result[&"manifest"]
|
|
payload[&"save_id"] = str(manifest.get("save_id", ""))
|
|
payload[&"saved_absolute_minute"] = int(manifest.get("absolute_minute", -1))
|
|
var stream: Dictionary = model[&"domain_events"]
|
|
stream["published_ids"].append(event_id)
|
|
stream["published_ids"].sort()
|
|
var notification := SimulationDomainEvent.new(StringName(event_id), event_type,
|
|
int(formal[&"clock"][&"absolute_minute"]), &"save_service", [], payload, 0)
|
|
stream["recent_events"].append(notification.to_dictionary())
|
|
if stream["recent_events"].size() > DomainEventStream.RECENT_EVENT_LIMIT:
|
|
stream["recent_events"].pop_front()
|
|
capture_model(prefix + "expected-operation", expected)
|
|
capture(prefix + "operation-actual")
|
|
var actual: Dictionary = bytes_to_var(FileAccess.get_file_as_bytes(directory.path_join(role + "-" + prefix + "checkpoint.var")))
|
|
json_file(role + "-" + prefix + "operation-comparison.json", {
|
|
"pid": OS.get_process_id(), "delta_kind": "source_defined_notification",
|
|
"formal_model": role + "-" + prefix + "formal-model", "actual_model": role + "-" + prefix + "checkpoint",
|
|
"expected_model": role + "-" + prefix + "expected-operation", "returned_result": Codec.new().typed(result),
|
|
"saved_recent_count": formal[&"systems"][&"runtime"][&"domain_events"]["recent_events"].size(),
|
|
"expected_recent_count": stream["recent_events"].size(),
|
|
"actual_recent_count": actual[&"systems"][&"runtime"][&"domain_events"]["recent_events"].size(),
|
|
"whole_raw_equal": var_to_bytes(expected) == var_to_bytes(actual),
|
|
})
|
|
# Complete raw actual/expected snapshots remain available on unknown drift.
|
|
expect(var_to_bytes(expected) == var_to_bytes(actual), "whole source-defined save operation model; see " + role + "-" + prefix + "operation-comparison.json")
|
|
expect(var_to_bytes(actual) == var_to_bytes(runtime.create_snapshot()), "read-only inspection preserves live model")
|
|
|
|
func record_save_operation(result: Dictionary) -> void:
|
|
operation_results.append(result.duplicate(true))
|
|
record_save_result("runtime_operation", String(main.manual_save_path), result)
|
|
|
|
func record_save_result(operation: String, path: String, result: Dictionary) -> void:
|
|
save_serial += 1
|
|
var stem := "%s-save-result-%04d" % [role, save_serial]
|
|
var raw := FileAccess.open(directory.path_join(stem + ".var"), FileAccess.WRITE)
|
|
expect(raw != null, "raw save result")
|
|
if raw != null:
|
|
raw.store_buffer(var_to_bytes(result))
|
|
raw.close()
|
|
json_file(stem + ".json", {"pid": OS.get_process_id(), "operation": operation,
|
|
"requested_path": path, "absolute_path": ProjectSettings.globalize_path(path),
|
|
"user_data_dir": OS.get_user_data_dir(), "result": Codec.new().typed(result),
|
|
"raw_sha256": FileAccess.get_sha256(directory.path_join(stem + ".var"))})
|
|
save_records.append(stem)
|
|
json_file(role + "-save-results.json", {"pid": OS.get_process_id(), "records": save_records})
|
|
|
|
func import_package(source: String, destination: String) -> void:
|
|
var absolute := ProjectSettings.globalize_path(destination)
|
|
expect(DirAccess.make_dir_recursive_absolute(absolute) == OK, "private save directory")
|
|
for member: String in MEMBERS:
|
|
var origin := source.path_join(member)
|
|
var copy := absolute.path_join(member)
|
|
expect(DirAccess.copy_absolute(origin, copy) == OK, "private save input " + member)
|
|
expect(FileAccess.get_sha256(origin) == FileAccess.get_sha256(copy), "private save input SHA " + member)
|
|
|
|
func export_package(path: String, name: String) -> void:
|
|
var absolute := ProjectSettings.globalize_path(path)
|
|
var pins := {}
|
|
for suffix: String in ["", ".bak"]:
|
|
var source := absolute + suffix
|
|
var present := DirAccess.dir_exists_absolute(source)
|
|
pins[name + suffix] = {"source_path": source, "present": present, "members": {}}
|
|
if not present:
|
|
expect(not suffix.is_empty(), "formal package exists")
|
|
continue
|
|
var destination := directory.path_join(name + suffix)
|
|
expect(DirAccess.make_dir_recursive_absolute(destination) == OK, "export directory")
|
|
for member: String in MEMBERS:
|
|
var origin := source.path_join(member)
|
|
var copy := destination.path_join(member)
|
|
expect(DirAccess.copy_absolute(origin, copy) == OK, "export " + member)
|
|
var digest := FileAccess.get_sha256(origin)
|
|
expect(not digest.is_empty() and digest == FileAccess.get_sha256(copy), "export SHA " + member)
|
|
pins[name + suffix]["members"][member] = {"sha256": digest, "bytes": FileAccess.get_file_as_bytes(copy).size()}
|
|
# Slot metadata lives in the existing manifest; copy it verbatim.
|
|
var metadata := absolute.path_join("manifest.json")
|
|
var metadata_copy := directory.path_join(name + "-slot-metadata.json")
|
|
expect(DirAccess.copy_absolute(metadata, metadata_copy) == OK, "slot metadata export")
|
|
json_file(name + "-export.json", {"pid": OS.get_process_id(), "requested_path": path,
|
|
"absolute_path": absolute, "user_data_dir": OS.get_user_data_dir(), "packages": pins,
|
|
"slot_metadata": {"file": name + "-slot-metadata.json", "sha256": FileAccess.get_sha256(metadata_copy)}})
|
|
|
|
func finish_observations() -> void:
|
|
pass
|