Files
Sylpheed/port/scripts/boot.gd
Sylpheed port agent 579c8096c1 port: P4 -- the intro video plays inside the boot sequence
The exporter transcodes ADV.wmv and S00A.wmv to Ogg Theora and records the exact
ffmpeg command in the manifest, per MISSION §6, so a modder who dislikes the
quality re-runs one line rather than reverse-engineering what was done.

Quality was MEASURED, not judged: SSIM against the decoded source over a 10 s
sample is 0.9863 / 0.9896 / 0.9924 at -q:v 6 / 8 / 10, and at 200 % zoom on the
reel's hardest case -- fine serif text and soft gradients over near-black, where
Theora breaks first -- q8 is indistinguishable. So MISSION §6's permitted
FFmpeg-GDExtension fallback is NOT needed and is NOT being proposed. No new
runtime dependency.

-ac 2 because the source is 6-channel WMA Pro; that downmix is a decision, so it
lives in the recorded command rather than in prose.

Encoding is cached on a .cmd sidecar holding the command and the source size --
any change to either re-encodes. export/ is still regenerated wholesale; this is
derived state validating derived state, not a hand-edit, and without it every
re-export pays ~4 minutes to produce a byte-identical file.

The player renders INTO the design SubViewport. Parenting it to the Boot node
played the movie to the window instead, and every captured frame came out black
-- which is worth more than a capture-bug note: a movie outside the 1280x720
design space is outside the coordinate system every screen is expressed in.

(A) skips a movie, because Q9 measured that (title at 57 s vs a 193 s baseline).

NOT VERIFIED, and stated as such: audible playback. This container has no audio
device and Godot falls back to the dummy driver. The Vorbis stream exists, is
2-channel and decodes; whether Godot emits it is unconfirmed.
2026-08-29 08:44:25 +00:00

274 lines
9.6 KiB
GDScript

# Entry point.
#
# P1 shows one exported screen, statically, so that its pixels can be diffed
# against `sylpheed-cli screen render` of the same build. The boot sequence
# proper (splash -> intro -> title -> menu) is P3 and is not here.
#
# godot --path port -- --screen=main_menu
# godot --path port -- --screen=main_menu --capture=/tmp/godot.png
# godot --path port -- --screen=main_menu --time=0.5 --capture=/tmp/at-half.png
# godot --path port -- --screen=main_menu --pose=rest --capture=/tmp/rest.png
# godot --path port -- --boot # the whole boot sequence
# godot --path port -- --boot --film=/tmp/boot # ...and a frame every 0.25 s
#
# `--time` is in SECONDS and freezes the timeline there; without it the screen
# animates in real time from t=0. `--pose=rest` draws the export's declared
# resting pose instead of the timeline -- what the reference renderer draws, so
# that a renderer-vs-renderer diff compares like with like.
#
# The screen is drawn into a SubViewport sized to the export's own `design`
# rectangle and shown through a container that scales it to the window. That is
# the same separation the project settings already make -- design space is
# fixed, the window is not -- and it makes `--capture` exact: the PNG is the
# design rectangle itself, never the window, so it is directly comparable with
# `screen render`'s composite with no cropping or rescaling.
extends Node
const DEFAULT_SCREEN := "main_menu"
var view: ScreenView = null
var viewport: SubViewport = null
func _ready() -> void:
var args := _args()
var export_tree := ExportTree.locate()
if export_tree.root == "":
push_error(export_tree.error)
get_tree().quit(2)
return
_flow = export_tree.authored("flow.json")
if args.has("boot"):
if _flow == null:
push_error(export_tree.error)
get_tree().quit(2)
return
for step: Dictionary in _flow["boot"]:
_sequence.append(step)
_film = args.get("film", "")
var name: String = String(_sequence[0].get("screen", "")) if not _sequence.is_empty() \
else args.get("screen", DEFAULT_SCREEN)
if name == "":
name = DEFAULT_SCREEN # the sequence opens on a video; load something to size the viewport
var screen: Dictionary = export_tree.screen(name)
if screen.is_empty():
push_error(export_tree.error)
print("screens in this export: ", ", ".join(export_tree.screen_names()))
get_tree().quit(2)
return
var design: Array = screen.get("design", [1280, 720])
var container := SubViewportContainer.new()
container.stretch = true
container.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(container)
viewport = SubViewport.new()
viewport.size = Vector2i(int(design[0]), int(design[1]))
viewport.transparent_bg = false
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
container.add_child(viewport)
view = ScreenView.new()
# The export is a 1:1 copy of the disc's texels and elements are drawn at up
# to 500 %. Nearest is also what the reference renderer does
# (`ui_layout::blit` maps destination to source by integer division), so a
# filter difference cannot masquerade as a placement difference in the diff.
view.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
view.focused_id = args.get("focus", "")
if args.get("pose", "") == "rest":
view.pose_mode = ScreenView.Pose.REST
# The keyframe unit is MEASURED, not on the disc, so it is authored and read
# in exactly one place -- here.
var timing: Variant = export_tree.authored("timing.json")
if timing == null:
push_error(export_tree.error)
get_tree().quit(2)
return
view.units_per_second = float(timing["keyframe_units_per_second"])
# The one unknown duration per screen: the ramp into the final untimed
# keyframe. Authored, because the disc has no time slot there.
view.exit_ramp_units = float(timing["exit_ramp_units"])
viewport.add_child(view)
if not view.load_screen(export_tree, name):
push_error(export_tree.error)
get_tree().quit(2)
return
var settle := view.settle_time()
print("screen %s: %d elements, %d in paint order, design %dx%d, settles at t=%d (%.3f s)" % [
name, view.screen["elements"].size(), view.screen["paint_order"].size(),
design[0], design[1], settle, settle / view.units_per_second])
if args.has("time"):
_frozen = true
view.time_units = float(args["time"]) * view.units_per_second
view.queue_redraw()
if _film != "":
set_process(true)
_film_capture()
if args.has("capture"):
await _capture(args["capture"])
get_tree().quit(0)
var _frozen := false
var _flow: Variant = null
var _sequence: Array[Dictionary] = []
var _player: VideoStreamPlayer = null
var _step := 0
var _film := ""
var _film_frame := 0
var _film_next := 0.0
var _elapsed := 0.0
var _boot_done := false
func _process(delta: float) -> void:
if _frozen or view == null:
return
view.time_units += delta * view.units_per_second
_elapsed += delta
view.queue_redraw()
if _sequence.is_empty() or _player != null:
return
# A screen holds at `rest` until it has arrived, then plays itself out and
# the next one begins. Nothing waits on a timer the disc does not carry: the
# pacing is each group's own timeline (authored/flow.json, `dwell`).
if view.holding and view.time_units >= view.settle_time():
# The LAST screen in the sequence keeps holding. A screen plays itself
# out because something is taking its place; nothing is taking the
# title's place here, and a boot that ends by fading to black is a boot
# that looks like it crashed. P4 puts the intro video in front of the
# title, and P5 gives the title somewhere to go.
if _step + 1 < _sequence.size():
view.holding = false
elif not _boot_done:
_boot_done = true
print("boot sequence complete after %.2f s, holding on %s" % [_elapsed, _sequence[_step]])
if _film == "":
get_tree().quit(0)
elif not view.holding and view.time_units >= view.exit_time():
_advance()
func _advance() -> void:
_step += 1
var next: Dictionary = _sequence[_step]
if next.has("video"):
_play_video(String(next["video"]), bool(next.get("skippable", false)))
return
var name := String(next["screen"])
print(" -> %s at %.2f s" % [name, _elapsed])
view.holding = true
view.time_units = 0.0
if not view.load_screen(view.tree, name):
push_error(view.tree.error)
get_tree().quit(2)
## Play one transcoded movie, full-bleed over the screen.
##
## The port never reads WMV: the exporter transcoded this to Ogg Theora and
## recorded the exact ffmpeg command in the manifest (MISSION §6), so a modder
## who dislikes the quality re-runs one line.
func _play_video(name: String, skippable: bool) -> void:
var v := view.tree.video(name)
if v.is_empty():
push_error(view.tree.error)
get_tree().quit(2)
return
print(" -> video %s at %.2f s (%s)" % [name, _elapsed, v["path"]])
var stream := VideoStreamTheora.new()
stream.file = v["path"]
_player = VideoStreamPlayer.new()
_player.stream = stream
_player.expand = true
_player.set_anchors_preset(Control.PRESET_FULL_RECT)
# Into the SubViewport, not beside it. Everything this port draws composes in
# the export's own 1280x720 design space; a player parented to the Boot node
# renders to the window instead and is invisible to `--capture`, which reads
# the SubViewport. That is not only a capture artefact -- it would also put
# the movie outside the space every screen coordinate is expressed in.
viewport.add_child(_player)
_skippable = skippable
# `play()` needs the node in the tree; calling it before that is an error
# the engine reports and then ignores, which looks like a video that simply
# never starts.
await get_tree().process_frame
_player.finished.connect(_video_finished)
_player.play()
var _skippable := false
func _video_finished() -> void:
print(" video ended at %.2f s" % _elapsed)
_player.queue_free()
_player = null
_advance()
func _unhandled_input(event: InputEvent) -> void:
# HANDOFF Q9, measured: one (A) press skips a movie -- the title was reached
# at 57 s against a 193 s baseline. This is the only input the port handles
# so far; menu navigation is P5.
if _player == null or not _skippable:
return
if event.is_action_pressed("ui_accept") or event.is_action_pressed("ui_cancel"):
print(" video skipped at %.2f s" % _elapsed)
_player.stop()
_video_finished()
func _capture(path: String) -> void:
# Two frames: the first is the one this callback is still inside of.
await RenderingServer.frame_post_draw
await RenderingServer.frame_post_draw
var img := viewport.get_texture().get_image()
print("t = %.2f units (%.3f s), pose = %s" % [
view.time_units, view.time_units / view.units_per_second,
"rest" if view.pose_mode == ScreenView.Pose.REST else "timeline"])
print("drew %d: %s" % [view.drawn.size(), ", ".join(view.drawn)])
if not view.skipped.is_empty():
print("not drawn %d: %s" % [view.skipped.size(), ", ".join(view.skipped)])
var err := img.save_png(path)
if err != OK:
push_error("cannot write %s (%d)" % [path, err])
return
print("captured %dx%d -> %s" % [img.get_width(), img.get_height(), path])
## A frame every 0.25 s for the whole run, so an unattended boot leaves a
## filmstrip behind rather than requiring someone to be watching it.
func _film_capture() -> void:
while true:
await RenderingServer.frame_post_draw
if _elapsed >= _film_next:
var img := viewport.get_texture().get_image()
img.save_png("%s_%03d.png" % [_film, _film_frame])
_film_frame += 1
_film_next += 0.25
# Godot passes everything after `--` through untouched; take `--key=value`.
static func _args() -> Dictionary:
var out := {}
for arg in OS.get_cmdline_user_args():
if arg.begins_with("--") and arg.contains("="):
var pair := arg.substr(2).split("=", true, 1)
out[pair[0]] = pair[1]
elif arg.begins_with("--"):
out[arg.substr(2)] = "1"
return out