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.
This commit is contained in:
@@ -45,10 +45,13 @@ func _ready() -> void:
|
||||
get_tree().quit(2)
|
||||
return
|
||||
for step: Dictionary in _flow["boot"]:
|
||||
_sequence.append(String(step["screen"]))
|
||||
_sequence.append(step)
|
||||
_film = args.get("film", "")
|
||||
|
||||
var name: String = _sequence[0] if not _sequence.is_empty() else args.get("screen", DEFAULT_SCREEN)
|
||||
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)
|
||||
@@ -117,7 +120,8 @@ func _ready() -> void:
|
||||
|
||||
var _frozen := false
|
||||
var _flow: Variant = null
|
||||
var _sequence: Array[String] = []
|
||||
var _sequence: Array[Dictionary] = []
|
||||
var _player: VideoStreamPlayer = null
|
||||
var _step := 0
|
||||
var _film := ""
|
||||
var _film_frame := 0
|
||||
@@ -133,7 +137,7 @@ func _process(delta: float) -> void:
|
||||
_elapsed += delta
|
||||
view.queue_redraw()
|
||||
|
||||
if _sequence.is_empty():
|
||||
if _sequence.is_empty() or _player != null:
|
||||
return
|
||||
|
||||
# A screen holds at `rest` until it has arrived, then plays itself out and
|
||||
@@ -158,15 +162,75 @@ func _process(delta: float) -> void:
|
||||
|
||||
func _advance() -> void:
|
||||
_step += 1
|
||||
var next := _sequence[_step]
|
||||
print(" -> %s at %.2f s" % [next, _elapsed])
|
||||
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, next):
|
||||
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
|
||||
|
||||
@@ -83,6 +83,20 @@ func screen(name: String) -> Dictionary:
|
||||
return {}
|
||||
|
||||
|
||||
# A transcoded movie, addressed by manifest name. The port never reads WMV --
|
||||
# the exporter emits Ogg Theora, which Godot plays natively (MISSION §2, §6).
|
||||
func video(name: String) -> Dictionary:
|
||||
for entry: Dictionary in manifest().get("videos", []):
|
||||
if entry.get("name") == name:
|
||||
var path := root.path_join(entry["file"])
|
||||
if not FileAccess.file_exists(path):
|
||||
error = "manifest lists %s but %s is not there" % [name, path]
|
||||
return {}
|
||||
return {"path": path, "command": entry.get("command", "")}
|
||||
error = "no video named %s in manifest.json" % name
|
||||
return {}
|
||||
|
||||
|
||||
func screen_names() -> PackedStringArray:
|
||||
var names := PackedStringArray()
|
||||
for entry: Dictionary in manifest().get("screens", []):
|
||||
|
||||
Reference in New Issue
Block a user