Skip to content
27 changes: 27 additions & 0 deletions docs/development/prototype-teaser-capture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Prototype teaser capture

Owner-requested 24-second local Telegram teaser, using Godot 4.7.2 native Movie Maker and ordinary FFmpeg editing. The route extends the existing machinery integration test: finite source material is extracted, refined and assembled through real commands before the scene is retained. The recorded commands build, dismantle and rotate a wall from one recovered plate, pack and relocate the fitted printer, replace an extractor head with a refinery head, and refine its actual 1 kg ice into water. Final snapshot validation and finite-material checks must pass.

Existing ProceduralHabitatPreview, MachineryPreview and inventory meshes are rendered from current owner observations. Static close camera shots, capture-only lighting and compact edge captions differ from normal presentation. The normal HUD is hidden so it cannot obscure the machinery or wall. Debug labels, fixture IDs and the preview avatar are hidden. No canonical state grants, fabricated receipts, service listeners, external sends, new fonts/assets, generative image tools or game release are involved. Captions use the existing engine UI font. First-party geometry/capture code is MIT; existing engine font remains part of the engine runtime, not an added redistributed asset.

Run in a clean isolated worktree after import:

```powershell
./tools/ci/capture-telegram-teaser.ps1 -Godot 'D:/Godot_v4.7.2-stable_win64.exe/Godot_v4.7.2-stable_win64_console.exe' -OutputDirectory 'D:/GitHub/Frontier Systems/var/captures/telegram-teaser'
```

The wrapper owns one bounded native process, disables external telemetry, isolates the user directory and removes its temporary override. It never opens a socket or touches the regular saved world. Capture is graphical on Windows using GL compatibility. Automated fixture and camera operation are disclosed onscreen; this is not footage of human play, controller/VR acceptance or a published build. Local media can be given to the owner for the explicitly requested personal teaser; no Telegram upload is performed by this route.

Review native keyframes before encoding. Use H.264/yuv420p MP4, 1280x720, 30 fps and faststart for mobile sharing. Keep raw provenance and output hashes alongside ignored local media; do not add current internal gallery images to the teaser or upload it to other services.

## Resumed camera pass, 2026-10-22

The capture branch now includes the current integration branch's improved assets, machinery activity, walker poses and contextual UI. Four deliberate cuts separate construction, printer relocation, head replacement and material recovery. Each shot holds its subject while a small eased dolly adds depth; the last shot reframes toward the rebuilt wall before its placement command. Captions remain at the edges and the normal HUD stays hidden. This is an automated workshop demonstration, not first-person player footage.

Acceptance requires a new native render showing each commanded object clearly before and after the change, no subject hidden by captions or geometry, smooth within-shot motion, and the existing finite-material assertions passing. Native verification and rendering are deferred while the independently owned Gaia restore holds runtime capacity. The source change alone is not visual acceptance or a finished video.

## Dependency integration, 2026-10-29

Integrated `dev-master` at `5b75fa462f0bab133a3f820c8d8234fdfe4d2f0d` without conflicts. The four shots and genuine command sequence are unchanged. The updated habitat preview hides its obsolete worksite wall through the existing `focus_production()` call. `teaser_choreography_test.gd` exposes the capture script's existing headless path to the standard Windows/Linux CI runner, including inherited machinery assertions and final finite-world checks. It does not render or establish camera quality.

The retained October 22 capture belongs to `539e78da3686e0b77deb7da5eb041863d5ce0a9a`; it is not visual evidence for this integrated revision. No new local native execution or capture is authorized. Source readiness and CI review are separate from owner motion acceptance and a finished trailer.
4 changes: 4 additions & 0 deletions tests/godot/teaser_choreography_test.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
extends "res://tools/godot/capture_telegram_teaser.gd"

## Runs the genuine finite workshop choreography through the standard headless
## test runner. This verifies commands and final state, not rendered motion.
57 changes: 57 additions & 0 deletions tools/ci/capture-telegram-teaser.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
param(
[string]$Godot = 'godot',
[string]$OutputDirectory = '',
[ValidateRange(30, 300)][int]$TimeoutSeconds = 180
)
# Native non-generative movie capture, no sockets, uploads or persistent game data.
$ErrorActionPreference = 'Stop'
$projectRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$executable = (Get-Command $Godot -CommandType Application).Source
if (-not $OutputDirectory) { $OutputDirectory = Join-Path $projectRoot 'var/captures/telegram-teaser' }
New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
$OutputDirectory = (Resolve-Path -LiteralPath $OutputDirectory).Path
$overridePath = Join-Path $projectRoot 'override.cfg'
if (Test-Path -LiteralPath $overridePath) { throw 'Preserve existing override.cfg; use a clean isolated worktree.' }
$profileName = 'FrontierSystems-teaser-' + [Guid]::NewGuid().ToString('N')
$process = $null
try {
[IO.File]::WriteAllText($overridePath, "[application]`nconfig/use_custom_user_dir=true`nconfig/custom_user_dir_name=`"$profileName`"`n`n[display]`nwindow/size/viewport_width=1280`nwindow/size/viewport_height=720`n")
$info = [Diagnostics.ProcessStartInfo]::new()
$info.FileName = $executable
$info.WorkingDirectory = $projectRoot
$info.UseShellExecute = $false
$info.CreateNoWindow = $true
$info.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden
$info.RedirectStandardOutput = $true
$info.RedirectStandardError = $true
$info.Environment['FRONTIER_EVENT_URL'] = ''
$info.Environment['FRONTIER_EVENT_TOKEN'] = ''
$info.Environment['FRONTIER_TEASER_OUTPUT'] = $OutputDirectory
foreach ($argument in @('--xr-mode','off','--audio-driver','Dummy','--rendering-method','gl_compatibility','--resolution','1280x720','--path',$projectRoot,'--script','res://tools/godot/capture_telegram_teaser.gd','--fixed-fps','30','--write-movie',(Join-Path $OutputDirectory 'raw.avi'))) {
$info.ArgumentList.Add($argument)
}
$process = [Diagnostics.Process]::new()
$process.StartInfo = $info
if (-not $process.Start()) { throw 'Native movie capture did not start.' }
$stdout = $process.StandardOutput.ReadToEndAsync()
$stderr = $process.StandardError.ReadToEndAsync()
if (-not $process.WaitForExit($TimeoutSeconds * 1000)) {
$process.Kill($true)
$process.WaitForExit()
throw 'Owned movie capture exceeded its deadline.'
}
$log = $stdout.GetAwaiter().GetResult() + $stderr.GetAwaiter().GetResult()
[IO.File]::WriteAllText((Join-Path $OutputDirectory 'capture.log'), $log)
if ($process.ExitCode -ne 0 -or $log -match '(?m)SCRIPT ERROR|(?:^|\s)ERROR:') {
Write-Host $log.Substring(0, [Math]::Min(3000, $log.Length))
throw 'Native capture failed; inspect capture.log.'
}
Write-Host "Native capture completed: $OutputDirectory"
$log | Select-String -Pattern 'Machinery integration|TEASER_'
} finally {
if ($null -ne $process) {
if (-not $process.HasExited) { $process.Kill($true) }
$process.Dispose()
}
Remove-Item -LiteralPath $overridePath -ErrorAction SilentlyContinue
}
244 changes: 244 additions & 0 deletions tools/godot/capture_telegram_teaser.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
extends "res://tests/godot/machinery_runtime_test.gd"

## Genuine native render of the existing finite integration world.
## Only the camera, lighting and caption layer differ from ordinary presentation.
const FPS: int = 30
const FRAMES: int = 720
var habitat: ProceduralHabitatPreview
var machinery: MachineryPreview
var title: Label
var detail: Label
var _output: String
var _extractor: String = ""
var _extractor_head: String = ""
var _refinery: String = ""
var _refinery_head: String = ""
var _printer: String = ""
var _plate: String = ""
var _wall: String = ""


func finish() -> void:
if failures > 0:
world.free()
quit(1)
return
_prepare_actions()
if DisplayServer.get_name() == "headless":
for frame in range(FRAMES):
_action(frame)
step(2)
_verify_actions()
world.free()
quit(1 if failures else 0)
return
_output = OS.get_environment("FRONTIER_TEASER_OUTPUT")
if _output.is_empty(): _output = ProjectSettings.globalize_path("res://var/captures/telegram-teaser")
DirAccess.make_dir_recursive_absolute(_output)
root.size = Vector2i(1280, 720)
root.content_scale_size = Vector2i(1280, 720)
root.msaa_3d = Viewport.MSAA_4X
habitat = ProceduralHabitatPreview.new()
root.add_child(habitat)
habitat.character.set_navigation_enabled(false)
habitat.character.visible = false
habitat.world_inventory.skip_machine_roots = true
habitat.world_inventory.labels_visible = false
habitat.camera.set_process(false)
habitat.camera.set_physics_process(false)
habitat.camera.fov = 49.0
habitat.focus_production()
machinery = MachineryPreview.new()
habitat.add_child(machinery)
for child in habitat.get_children():
if child is WorldEnvironment:
child.environment.background_color = Color("#101923")
child.environment.ambient_light_energy = 0.38
if child is DirectionalLight3D:
child.light_energy = 0.95
child.rotation = Vector3(-1.1, -0.5, 0)
if child is CanvasLayer: child.visible = false
_make_captions()
_update_scene()
# Start recording only after native scene setup, for reproducible trimming.
print("TEASER_START_FRAME %d" % Engine.get_frames_drawn())
for frame in range(FRAMES):
_action(frame)
step(2) # Canonical 60 Hz ticks for each 30 fps recorded frame.
_update_scene()
_shot(frame)
await RenderingServer.frame_post_draw
if frame in [60, 240, 450, 660]:
check(root.get_texture().get_image().save_png(_output.path_join("frame-%03d.png" % frame)) == OK, "native keyframe")
await process_frame
_verify_actions()
print("TEASER_END_FRAME %d; failures %d" % [Engine.get_frames_drawn(), failures])
habitat.free()
world.free()
quit(1 if failures else 0)


func _prepare_actions() -> void:
var workshop: Dictionary = production.capture_snapshot().value["machinery"]
for id in workshop["mounts"]:
match workshop["parts"][id]["kind"]:
"extractor":
_extractor = workshop["mounts"][id]
_extractor_head = id
"refinery":
_refinery = workshop["mounts"][id]
_refinery_head = id
"printhead": _printer = workshop["mounts"][id]
check(not _extractor.is_empty() and not _printer.is_empty(), "actual modular workshop available")
# Reclaim the unused disconnected frame, retaining its working refinery head.
move([-300, 0, -1100])
command("machine_pack", {"machine_id": _refinery})
for id in workshop["mounts"]:
if workshop["mounts"][id] != _refinery: continue
command("machine_unfit", {"machine_id": _refinery, "item_id": id, "socket_id": ""})
if id != _refinery_head: command("machine_recycle", {"item_id": id})
command("machine_recycle", {"item_id": _refinery})
move([0, 0, 0])
command("process", {"recipe_id": "craft.plate"})
step(120)
command("materialize_plate", {})
var plates: Array = production.observation(context).value["components"]
check(not plates.is_empty(), "one physical plate crafted from remaining finite stock")
if not plates.is_empty(): _plate = plates.back()
move([-2000, 0, 3500])


func _action(frame: int) -> void:
match frame:
40:
command("wall_build", {"item_id": _plate, "position": [-2000, 0, 2500], "quarter_turn": 0})
if not production.capture_snapshot().value["walls"].is_empty(): _wall = production.capture_snapshot().value["walls"].keys().back()
120:
command("wall_remove", {"wall_id": _wall})
var plates: Array = production.observation(context).value["components"]
if not plates.is_empty(): _plate = plates.back()
150:
command("wall_build", {"item_id": _plate, "position": [-2000, 0, 2500], "quarter_turn": 1})
if not production.capture_snapshot().value["walls"].is_empty(): _wall = production.capture_snapshot().value["walls"].keys().back()
210:
move([0, 0, 3500])
move([900, 0, 0])
command("machine_pack", {"machine_id": _printer})
270:
move([2500, 0, 0])
command("machine_deploy", {"machine_id": _printer, "position": [2500, 0, -1000], "mode": "surface"})
330:
command("machine_pack", {"machine_id": _printer})
move([900, 0, 0])
command("machine_deploy", {"machine_id": _printer, "position": [900, 0, -1000], "mode": "surface"})
370:
move([-6000, 0, -1000])
command("machine_unfit", {"machine_id": _extractor, "item_id": _extractor_head, "socket_id": ""})
420:
fit(_extractor, _refinery_head)
450:
command("machine_run", {"machine_id": _extractor, "recipe": "refine.ice"})
580:
move([-4000, 0, 3500])
move([-2000, 0, 3500])
command("wall_remove", {"wall_id": _wall})
var plates: Array = production.observation(context).value["components"]
if not plates.is_empty(): _plate = plates.back()
630:
move([-3500, 0, 3500])
command("wall_build", {"item_id": _plate, "position": [-3500, 0, 2500], "quarter_turn": 0})


func _verify_actions() -> void:
check(production.capture_snapshot().value["cargo"]["resource-canister-1"]["water"] == 1000000, "replacement refinery processes actual ice into water")
check(production.capture_snapshot().value["walls"].size() == 1, "one recovered plate rebuilds exactly one wall")
check(production.validate_snapshot_record(production.capture_snapshot().value, runner.tick_number).is_success, "recorded finite world still validates")
print("Visible workshop choreography: %d failures" % failures)


func _update_scene() -> void:
var observed: Dictionary = production.observation(context).value
habitat.production.observe(observed)
habitat.production.set_labels_visible(false)
machinery.observe(observed)
habitat.world_inventory.observe(inventory.world_inventory_observation(context).value)
_quiet(habitat)


func _quiet(node: Node) -> void:
if node is Label3D: node.visible = false
for child in node.get_children(): _quiet(child)


func _make_captions() -> void:
var layer := CanvasLayer.new()
root.add_child(layer)
var header := ColorRect.new()
header.size = Vector2(1280, 48)
header.color = Color(0.035, 0.055, 0.075, 0.9)
layer.add_child(header)
var footer := ColorRect.new()
footer.position = Vector2(0, 634)
footer.size = Vector2(1280, 86)
footer.color = Color(0.035, 0.055, 0.075, 0.91)
layer.add_child(footer)
var brand := Label.new()
brand.position = Vector2(24, 10)
brand.text = "FRONTIER SYSTEMS"
brand.add_theme_font_size_override("font_size", 20)
layer.add_child(brand)
var status := Label.new()
status.position = Vector2(1050, 14)
status.text = "SOURCE PROTOTYPE"
status.add_theme_font_size_override("font_size", 12)
status.modulate = Color("#a3d6d4")
layer.add_child(status)
title = Label.new()
title.position = Vector2(24, 642)
title.add_theme_font_size_override("font_size", 25)
layer.add_child(title)
detail = Label.new()
detail.position = Vector2(24, 676)
detail.add_theme_font_size_override("font_size", 17)
detail.modulate = Color("#b5c7d2")
layer.add_child(detail)
var note := Label.new()
note.position = Vector2(845, 703)
note.text = "Godot prototype · Automated commands"
note.add_theme_font_size_override("font_size", 12)
note.modulate = Color("#80949f")
layer.add_child(note)


func _shot(frame: int) -> void:
var segment: int = frame / 180
var local_frame: int = frame % 180
var starts: Array[Vector3] = [Vector3(-4.4, 3.2, 1), Vector3(3.8, 3.3, 1), Vector3(-8.4, 2.3, 0.7), Vector3(-5, 4, 0.3)]
var targets: Array[Vector3] = [Vector3(-2, 0.9, 2.5), Vector3(1.4, 0.7, -1), Vector3(-6, 0.8, -2), Vector3(-2.7, 0.9, 2.5)]
# Deliberate cuts separate activities; each shot settles on its subject before
# the command fires. Ease a small dolly rather than orbiting away from it.
var travel: float = smoothstep(0.0, 179.0, float(local_frame))
var target: Vector3 = targets[segment]
if segment == 3:
# The plate is removed at 580 and rebuilt at 630. Follow its destination
# during that empty interval, then hold on the rebuilt wall.
var relocation: float = smoothstep(580.0, 620.0, float(frame))
target = Vector3(-2, 0.9, 2.5).lerp(Vector3(-3.5, 0.9, 2.5), relocation)
var display_origin: Vector3 = ProceduralWorldInventoryPreview.DISPLAY_ORIGIN
var camera_start: Vector3 = starts[segment]
habitat.camera.position = display_origin + camera_start.lerp(target + (camera_start - target) * 0.94, travel)
habitat.camera.look_at(display_origin + target)
match segment:
0:
title.text = "A physical plate becomes a wall"
detail.text = "Build it, dismantle it, then turn the recovered plate"
1:
title.text = "Pack the printer and move it"
detail.text = "The whole fitted machine keeps its components"
2:
title.text = "Swap the working head"
var water: int = int(production.capture_snapshot().value["cargo"]["resource-canister-1"]["water"])
detail.text = "Extractor to refinery. Stored water: %.2f kg" % (float(water) / 1000000.0)
3:
title.text = "Recover the material and build again"
detail.text = "The same plate, in a different place"
1 change: 1 addition & 0 deletions tools/godot/capture_telegram_teaser.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://muqcy2l6dawg
Loading