extends SceneTree
## RELEASE A STARTER MAP from your Home (docs/home-world-design.md §6).
##
## A deliberate dev step, run when you mean to ship a new starter -- never CI:
##
## tools/release-home-map.sh [map_id] # default map_test
##
## Reads your home room's map from the running keet-daemon (read-only: state.list),
## drops positions and undone edits (scenes/maps/home_starter.gd build()), copies
## every content file the map references from user://content into
## res://home/content/<file>.bin, and writes res://home/<map_id>.home.json. Then
## review the diff, commit, git push pear master -- the push is the release.
##
## Fails, writing nothing, if: no daemon, no home room yet, the map scene does not
## exist, or any referenced content file is missing on this machine (a starter that
## cannot be built offline is not shippable).
##
## Args after `--`: <map_id> [--out <dir>] [--content-dir <dir>]
## --out / --content-dir exist for the gate, which runs against a scratch daemon
## and must not touch res://home or the real user://content.
const HomeStarter := preload("res://scenes/maps/home_starter.gd")
var _map_id := "map_test"
var _out := HomeStarter.DIR
var _content_dir := "user://content"
var _ran := false
func _initialize() -> void:
var a := OS.get_cmdline_user_args()
var i := 0
while i < a.size():
match a[i]:
"--out":
i += 1
_out = a[i]
"--content-dir":
i += 1
_content_dir = a[i]
_:
_map_id = a[i]
i += 1
func _process(_delta: float) -> bool:
if _ran:
return false
_ran = true
_release()
return false
func _fail(msg: String) -> void:
printerr("release-home-map: FAILED — " + msg)
quit(1)
func _call(client: Object, method: String, params: Dictionary) -> Dictionary:
var box := {"done": false}
client.call_rpc(method, params, func(result: Variant, err: Variant) -> void:
box["result"] = result
box["err"] = err
box["done"] = true)
var t0 := Time.get_ticks_msec()
while not box["done"] and Time.get_ticks_msec() - t0 < 30000:
await process_frame
if not box["done"]:
return {"err": "%s timed out" % method}
return box
func _release() -> void:
var keet: Node = root.get_node_or_null("KeetService")
if keet == null:
_fail("no KeetService autoload")
return
var t0 := Time.get_ticks_msec()
while not bool(keet.client.is_connected) and Time.get_ticks_msec() - t0 < 15000:
await process_frame
if not bool(keet.client.is_connected):
_fail("no keet-daemon answered (start one: scripts/dev-run.sh --daemon-only)")
return
var scene := "res://scenes/maps/%s.tscn" % _map_id
if not ResourceLoader.exists(scene):
_fail("no map scene %s" % scene)
return
var h := await _call(keet.client, "home.get", {})
if h.get("err") != null or not (h.get("result") is Dictionary):
_fail("home.get: %s" % str(h.get("err")))
return
var home: Dictionary = h["result"]
if bool(home.get("created", false)):
_fail("this identity had no home room yet (one was just created, empty) — build your starter at Home first")
return
var room := String(home.get("roomId", ""))
var m := await _call(keet.client, "state.list", {"roomId": room, "prefix": "map:%s:" % _map_id})
var l := await _call(keet.client, "state.list", {"roomId": room, "prefix": "world:assets:"})
if m.get("err") != null or l.get("err") != null:
_fail("state.list: %s %s" % [str(m.get("err")), str(l.get("err"))])
return
var entries: Dictionary = (m["result"] as Dictionary).get("entries", {})
var assets: Dictionary = (l["result"] as Dictionary).get("entries", {})
var version := OS.get_environment("HYPER_TPS_BUILD")
if version == "" and FileAccess.file_exists("res://version.txt"):
version = FileAccess.get_file_as_string("res://version.txt").strip_edges()
var st: Dictionary = HomeStarter.build(_map_id, scene, entries, assets, version)
# Every referenced content file must be here, or nothing is written.
var files: Array = []
var missing: Array = []
for oid: String in HomeStarter.referenced_hashes(st):
var f := _find_content(oid)
if f == "":
missing.append(oid)
else:
files.append(f)
if not missing.is_empty():
_fail("%d referenced content file(s) not in %s: %s" % [missing.size(), _content_dir, str(missing)])
return
files.sort()
st["content"] = files
var cdir := _out.path_join("content")
DirAccess.make_dir_recursive_absolute(cdir)
var bytes_total := 0
for f: String in files:
var b := FileAccess.get_file_as_bytes(_content_dir.path_join(f))
bytes_total += b.size()
var w := FileAccess.open(cdir.path_join(f + HomeStarter.SHIPPED_SUFFIX), FileAccess.WRITE)
w.store_buffer(b)
w.close()
var out_path := _out.path_join("%s.home.json" % _map_id)
var w2 := FileAccess.open(out_path, FileAccess.WRITE)
if w2 == null:
_fail("cannot write %s" % out_path)
return
# Sorted keys, indented: a re-release is a readable diff.
w2.store_string(JSON.stringify(st, " ", true) + "\n")
w2.close()
var kinds := {}
for k: String in (st["entries"] as Dictionary):
var v: Variant = str_to_var(String(st["entries"][k]))
var kind := String((v as Dictionary).get("kind", "")) if v is Dictionary else ""
if kind == "":
kind = "(plain key)"
kinds[kind] = int(kinds.get(kind, 0)) + 1
print("release-home-map: %s from home %s" % [_map_id, room.left(16)])
print(" read %d keys, shipped %d (dropped %d: positions / undone)"
% [entries.size(), (st["entries"] as Dictionary).size(), entries.size() - (st["entries"] as Dictionary).size()])
print(" by kind: %s" % str(kinds))
print(" library entries: %d content files: %d (%d bytes)" % [(st["assets"] as Dictionary).size(), files.size(), bytes_total])
print(" wrote %s" % ProjectSettings.globalize_path(out_path))
quit(0)
## <oid>.<ext> in the content dir, or "".
func _find_content(oid: String) -> String:
var d := DirAccess.open(_content_dir)
if d == null:
return ""
for f in d.get_files():
if f == oid or f.begins_with(oid + "."):
return f
return ""