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.
121 lines
4.0 KiB
GDScript
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)
|