185 lines
7.8 KiB
GDScript
185 lines
7.8 KiB
GDScript
extends SceneTree
|
|
|
|
## godot --path . --script res://tests/ui/capture_pixel_tower.gd
|
|
## THE_TREE_PIXEL_PLAY=1 keeps an interactive, disposable scenario open instead.
|
|
## THE_TREE_CAPTURE_SMOKE=1 validates the real scenario with --headless.
|
|
## Optional: THE_TREE_CAPTURE_PATH, WIDTH, HEIGHT, MINUTES, GAP_MINUTES, LAYER.
|
|
## THE_TREE_CAPTURE_ZOOM=1 (0.5..4); FOCUS_ROOMS=1 centers the built room area.
|
|
## THE_TREE_CAPTURE_ANIMATION=1 also saves 72 live frames at 12 fps.
|
|
## PATH names the first PNG; the second PNG and a state JSON use its basename.
|
|
|
|
const Scenario := preload("res://scripts/ui/pixel_tower_demo.gd")
|
|
const DEFAULT_PATH := "res://artifacts/pixel-tower/pixel-tower.png"
|
|
|
|
var _scenario: RefCounted
|
|
var _runtime: Variant
|
|
var _clock: Variant
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
_clock = root.get_node("GameClock")
|
|
_runtime = root.get_node("SimulationRuntime")
|
|
var state: Variant = root.get_node("GameState")
|
|
_clock.reset(1, 6, 0)
|
|
state.reset()
|
|
var initialized: Dictionary = _runtime.initialize(_clock, state, 424242)
|
|
_clock.set_paused(true)
|
|
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
|
if not bool(initialized.get(&"valid", false)):
|
|
push_error("Could not initialize pixel viewing scenario.")
|
|
quit(1)
|
|
return
|
|
_scenario = Scenario.new()
|
|
if not await _scenario.prepare(_runtime, _clock, self):
|
|
quit(1)
|
|
return
|
|
if not await _scenario.advance(_env_int("THE_TREE_CAPTURE_MINUTES", 83, 0, 180)):
|
|
quit(1)
|
|
return
|
|
if OS.get_environment("THE_TREE_CAPTURE_SMOKE") == "1":
|
|
print("PIXEL_SCENARIO ", JSON.stringify(_scenario.describe()))
|
|
quit(0)
|
|
return
|
|
var capture_size := Vector2i(_env_int("THE_TREE_CAPTURE_WIDTH", 1440, 1024, 7680),
|
|
_env_int("THE_TREE_CAPTURE_HEIGHT", 810, 640, 4320))
|
|
var play := OS.get_environment("THE_TREE_PIXEL_PLAY") == "1"
|
|
var container: Node = root
|
|
var capture_viewport: SubViewport
|
|
if play:
|
|
root.size = capture_size
|
|
root.title = "The Tree · ドット絵の見学(保存なし)"
|
|
else:
|
|
capture_viewport = SubViewport.new()
|
|
capture_viewport.size = capture_size
|
|
capture_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
|
root.add_child(capture_viewport)
|
|
container = capture_viewport
|
|
var main := load("res://scenes/main/main.tscn").instantiate() as Control
|
|
if main == null or not main.has_method("_set_layer"):
|
|
push_error("Main scene did not load; run godot --headless --editor --path . --import first.")
|
|
quit(ERR_CANT_ACQUIRE_RESOURCE)
|
|
return
|
|
container.add_child(main)
|
|
main.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
if main.get("world_preview") == null:
|
|
push_error("Main scene failed to create the live world preview.")
|
|
quit(ERR_CANT_ACQUIRE_RESOURCE)
|
|
return
|
|
# main enables its production save policy in _ready; immediately turn it off
|
|
# for this disposable scenario and make the manual save/load controls inert.
|
|
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
|
for button: Node in main.find_children("*", "Button", true, false):
|
|
var save_control := button as Button
|
|
for connection: Dictionary in save_control.pressed.get_connections():
|
|
var callback: Callable = connection[&"callable"]
|
|
if callback.get_method() in [&"_save_manual", &"_load_manual"]:
|
|
save_control.disabled = true
|
|
save_control.tooltip_text = "この見学モードは保存しません。"
|
|
var layer := _env_int("THE_TREE_CAPTURE_LAYER", 0, 0, 1)
|
|
main.call("_set_layer", layer)
|
|
main.call("_clear_active_tool")
|
|
for frame: int in 8:
|
|
await process_frame
|
|
var preview: Variant = main.get("world_preview")
|
|
preview.set_zoom(_env_float("THE_TREE_CAPTURE_ZOOM", 1.0, 0.5, 4.0))
|
|
if OS.get_environment("THE_TREE_CAPTURE_FOCUS_ROOMS") == "1":
|
|
preview.pan_offset += preview.size * 0.5 - preview.cell_to_canvas(Vector3i(-2, 1, layer))
|
|
preview.call("_clamp_pan")
|
|
preview.queue_redraw()
|
|
if play:
|
|
main.call("_set_status", "ドット絵の見学:実際の客・従業員・建設済み施設で運営中(保存なし)", true, true)
|
|
_clock.set_paused(false)
|
|
print("PIXEL_PLAY: disposable scenario ready; normal saves are disabled.")
|
|
return
|
|
var path := OS.get_environment("THE_TREE_CAPTURE_PATH")
|
|
if path.is_empty():
|
|
path = DEFAULT_PATH
|
|
var absolute_path := ProjectSettings.globalize_path(path)
|
|
var directory_result := DirAccess.make_dir_recursive_absolute(absolute_path.get_base_dir())
|
|
if directory_result != OK:
|
|
push_error("Could not create capture directory.")
|
|
quit(directory_result)
|
|
return
|
|
var records: Array[Dictionary] = []
|
|
for index: int in 2:
|
|
for frame: int in 8:
|
|
await process_frame
|
|
await RenderingServer.frame_post_draw
|
|
var image := capture_viewport.get_texture().get_image()
|
|
if image == null or image.is_empty():
|
|
push_error("Capture needs a rendering display; use CAPTURE_SMOKE for headless validation.")
|
|
quit(ERR_CANT_ACQUIRE_RESOURCE)
|
|
return
|
|
var output := absolute_path if index == 0 else absolute_path.get_basename() + "-later.png"
|
|
var result := image.save_png(output)
|
|
if result != OK:
|
|
push_error("Could not write %s: %s" % [output, error_string(result)])
|
|
quit(result)
|
|
return
|
|
var description: Dictionary = _scenario.describe()
|
|
description["image"] = output
|
|
description["tracked_actor_count"] = (preview.get("_actor_tracks") as Dictionary).size()
|
|
description["viewport_size"] = [capture_size.x, capture_size.y]
|
|
records.append(description)
|
|
print("PIXEL_CAPTURE: %s at %s" % [output, _clock.formatted_time()])
|
|
if index == 0 and not await _scenario.advance(_env_int("THE_TREE_CAPTURE_GAP_MINUTES", 18, 1, 180)):
|
|
quit(1)
|
|
return
|
|
var animation: Dictionary = {}
|
|
if OS.get_environment("THE_TREE_CAPTURE_ANIMATION") == "1":
|
|
var frames_path := absolute_path.get_basename() + "-frames"
|
|
var frames_directory_result := DirAccess.make_dir_recursive_absolute(frames_path)
|
|
if frames_directory_result != OK:
|
|
push_error("Could not create animation frames directory.")
|
|
quit(frames_directory_result)
|
|
return
|
|
animation = {"directory": frames_path, "fps": 12, "frame_count": 72,
|
|
"start": _scenario.describe()}
|
|
_clock.set_speed(1)
|
|
_clock.set_paused(false)
|
|
main.call("_update_pause_and_speed")
|
|
# Normal real-time processing owns the clock and actor movement here.
|
|
# Never manually advance simulation ticks or animation time for a frame.
|
|
for frame: int in 72:
|
|
await create_timer(1.0 / 12.0).timeout
|
|
await RenderingServer.frame_post_draw
|
|
var frame_image := capture_viewport.get_texture().get_image()
|
|
if frame_image == null or frame_image.is_empty():
|
|
_clock.set_paused(true)
|
|
push_error("Could not read a live animation frame.")
|
|
quit(ERR_CANT_ACQUIRE_RESOURCE)
|
|
return
|
|
var frame_output := frames_path.path_join("%03d.png" % frame)
|
|
var frame_result := frame_image.save_png(frame_output)
|
|
if frame_result != OK:
|
|
_clock.set_paused(true)
|
|
push_error("Could not write %s: %s" % [frame_output, error_string(frame_result)])
|
|
quit(frame_result)
|
|
return
|
|
_clock.set_paused(true)
|
|
animation["end"] = _scenario.describe()
|
|
print("PIXEL_ANIMATION: %s (72 frames at 12 fps)" % frames_path)
|
|
var manifest := FileAccess.open(absolute_path.get_basename() + "-state.json", FileAccess.WRITE)
|
|
if manifest == null:
|
|
quit(FileAccess.get_open_error())
|
|
return
|
|
var manifest_data := {"scenario": "disposable production simulation", "captures": records}
|
|
if not animation.is_empty():
|
|
manifest_data["animation"] = animation
|
|
manifest.store_string(JSON.stringify(manifest_data, "\t"))
|
|
quit(0)
|
|
|
|
|
|
func _env_int(name: String, fallback: int, minimum: int, maximum: int) -> int:
|
|
var value := OS.get_environment(name)
|
|
return clampi(int(value), minimum, maximum) if value.is_valid_int() else fallback
|
|
|
|
|
|
func _env_float(name: String, fallback: float, minimum: float, maximum: float) -> float:
|
|
var value := OS.get_environment(name)
|
|
return clampf(float(value), minimum, maximum) if value.is_valid_float() else fallback
|