# 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/2" 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 `/../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 {} 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)