Files
Sylpheed/godot-import/port/scripts/export_tree.gd
MechaCat02 f44ebced59 merge the Godot port's history into the monorepo
Brought in with a subtree merge rather than a copy, so the port's 31 commits
survive as history rather than arriving as one anonymous import. Landed under
godot-import/ and moved into the final layout in the next commit, which keeps
git's rename detection able to follow each file across the move.
2026-08-29 11:32:01 +02:00

121 lines
4.0 KiB
GDScript

# Locating and reading the open export tree.
#
# The Godot project NEVER reads a disc format (docs/MISSION.md §2). Everything
# it draws comes from `export/`, which is derived, gitignored and regenerated
# wholesale by `crates/sylpheed-export`. This class is the only place that knows
# where that tree is on disk.
class_name ExportTree
extends RefCounted
const FORMAT_SCREEN := "sylpheed.screen/3"
const FORMAT_MANIFEST := "sylpheed.manifest/1"
var root: String = ""
var error: String = ""
# `SYLPHEED_EXPORT` wins, so a modder can point the game at their own tree
# without touching the project. Otherwise `<project>/../export`, which is the
# layout this repository has.
static func locate() -> ExportTree:
var t := ExportTree.new()
var env := OS.get_environment("SYLPHEED_EXPORT")
var candidate := env
if candidate == "":
candidate = ProjectSettings.globalize_path("res://").path_join("../export").simplify_path()
if not FileAccess.file_exists(candidate.path_join("manifest.json")):
t.error = "no manifest.json under %s -- run `sylpheed-export` first" % candidate
return t
t.root = candidate
return t
# `authored/` sits beside `export/`, never inside it: it is hand-written and
# committed, and a re-export must not be able to touch it.
func authored(name: String) -> Variant:
var path := root.path_join("../authored").simplify_path().path_join(name)
var text := FileAccess.get_file_as_string(path)
if text == "":
error = "cannot read %s" % path
return null
return JSON.parse_string(text)
func read_json(rel: String) -> Variant:
var path := root.path_join(rel)
var text := FileAccess.get_file_as_string(path)
if text == "":
error = "cannot read %s" % path
return null
var parsed: Variant = JSON.parse_string(text)
if parsed == null:
error = "%s is not JSON" % path
return null
return parsed
func manifest() -> Dictionary:
var m: Variant = read_json("manifest.json")
if m == null:
return {}
if m.get("format") != FORMAT_MANIFEST:
error = "manifest.json is %s, this build reads %s" % [m.get("format"), FORMAT_MANIFEST]
return {}
return m
# Screens are addressed by their manifest name, not by a path, so the caller
# never has to know the archive's subdirectory.
func screen(name: String) -> Dictionary:
var m := manifest()
if m.is_empty():
return {}
for entry: Dictionary in m.get("screens", []):
if entry.get("name") == name:
var s: Variant = read_json(entry["file"])
if s == null:
return {}
if s.get("format") != FORMAT_SCREEN:
error = "%s is %s, this build reads %s" % [name, s.get("format"), FORMAT_SCREEN]
return {}
return s
error = "no screen named %s in manifest.json" % name
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", []):
names.append(entry["name"])
return names
# Textures live outside res://, so they are read as bytes and decoded at
# runtime rather than imported. Nearest-neighbour: the export is a 1:1 copy of
# the disc's own texels and several elements are drawn at 200 %, where a
# bilinear filter would invent detail the disc does not have.
func texture(rel: String) -> Texture2D:
var bytes := FileAccess.get_file_as_bytes(root.path_join(rel))
if bytes.is_empty():
error = "cannot read sprite %s" % rel
return null
var img := Image.new()
if img.load_png_from_buffer(bytes) != OK:
error = "%s is not a PNG" % rel
return null
return ImageTexture.create_from_image(img)