feat(client): ContentDB loads /content + mirrors the api id-resolution check
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
83
client/scripts/content/content_db.gd
Normal file
83
client/scripts/content/content_db.gd
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
class_name ContentDB
|
||||||
|
extends RefCounted
|
||||||
|
## Loads authored world content (/content) and cross-checks origin references.
|
||||||
|
## Mirrors api/app/content.py so client and api resolve ids identically. This is
|
||||||
|
## content integrity, distinct from JSON-schema validation.
|
||||||
|
|
||||||
|
var locations: Dictionary = {}
|
||||||
|
var npcs: Dictionary = {}
|
||||||
|
var quests: Dictionary = {}
|
||||||
|
var items: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
static func default_content_root() -> String:
|
||||||
|
return ProjectSettings.globalize_path("res://").path_join("../content").simplify_path()
|
||||||
|
|
||||||
|
|
||||||
|
static func origin_path(id: String) -> String:
|
||||||
|
return default_content_root().path_join("origins").path_join("%s.json" % id)
|
||||||
|
|
||||||
|
|
||||||
|
static func load_json(abs_path: String) -> Variant:
|
||||||
|
var text := FileAccess.get_file_as_string(abs_path)
|
||||||
|
return JSON.parse_string(text)
|
||||||
|
|
||||||
|
|
||||||
|
func load_from(content_root: String) -> void:
|
||||||
|
var world := content_root.path_join("world")
|
||||||
|
locations = _load_dir(world.path_join("locations"))
|
||||||
|
npcs = _load_dir(world.path_join("npcs"))
|
||||||
|
quests = _load_dir(world.path_join("quests"))
|
||||||
|
items = _load_dir(world.path_join("items"))
|
||||||
|
|
||||||
|
|
||||||
|
func _load_dir(dir_path: String) -> Dictionary:
|
||||||
|
var out: Dictionary = {}
|
||||||
|
var dir := DirAccess.open(dir_path)
|
||||||
|
if dir == null:
|
||||||
|
push_error("content dir not found: %s" % dir_path)
|
||||||
|
return out
|
||||||
|
for file in dir.get_files():
|
||||||
|
if not file.ends_with(".json"):
|
||||||
|
continue
|
||||||
|
var data: Variant = load_json(dir_path.path_join(file))
|
||||||
|
if typeof(data) != TYPE_DICTIONARY or not data.has("id"):
|
||||||
|
push_error("bad content file: %s" % file)
|
||||||
|
continue
|
||||||
|
out[data["id"]] = data
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
func location(id: String) -> Dictionary: return locations.get(id, {})
|
||||||
|
func npc(id: String) -> Dictionary: return npcs.get(id, {})
|
||||||
|
func quest(id: String) -> Dictionary: return quests.get(id, {})
|
||||||
|
func item(id: String) -> Dictionary: return items.get(id, {})
|
||||||
|
func has_location(id: String) -> bool: return locations.has(id)
|
||||||
|
func has_npc(id: String) -> bool: return npcs.has(id)
|
||||||
|
func has_quest(id: String) -> bool: return quests.has(id)
|
||||||
|
func has_item(id: String) -> bool: return items.has(id)
|
||||||
|
|
||||||
|
|
||||||
|
func companions() -> Array:
|
||||||
|
var out: Array = []
|
||||||
|
for id in npcs:
|
||||||
|
if npcs[id].get("role", "") == "companion":
|
||||||
|
out.append(npcs[id])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
func unresolved_refs(origin: Dictionary) -> Array:
|
||||||
|
var missing: Array = []
|
||||||
|
var loc: String = origin.get("start_location_id", "")
|
||||||
|
if not has_location(loc):
|
||||||
|
missing.append("location:%s" % loc)
|
||||||
|
var q: Variant = origin.get("start_quest_id", null)
|
||||||
|
if q != null and not has_quest(q):
|
||||||
|
missing.append("quest:%s" % q)
|
||||||
|
for grant in origin.get("inventory_grants", []):
|
||||||
|
if not has_item(grant.get("item_id", "")):
|
||||||
|
missing.append("item:%s" % grant.get("item_id", ""))
|
||||||
|
for npc_id in origin.get("disposition_overrides", {}):
|
||||||
|
if not has_npc(npc_id):
|
||||||
|
missing.append("npc:%s" % npc_id)
|
||||||
|
return missing
|
||||||
1
client/scripts/content/content_db.gd.uid
Normal file
1
client/scripts/content/content_db.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dnd1rmbg2c1el
|
||||||
52
client/tests/unit/test_content_db.gd
Normal file
52
client/tests/unit/test_content_db.gd
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
extends "res://addons/gut/test.gd"
|
||||||
|
|
||||||
|
const ContentDB = preload("res://scripts/content/content_db.gd")
|
||||||
|
|
||||||
|
var db
|
||||||
|
|
||||||
|
|
||||||
|
func before_each():
|
||||||
|
db = ContentDB.new()
|
||||||
|
db.load_from(ContentDB.default_content_root())
|
||||||
|
|
||||||
|
|
||||||
|
func _deserter() -> Dictionary:
|
||||||
|
return ContentDB.load_json(ContentDB.origin_path("deserter"))
|
||||||
|
|
||||||
|
|
||||||
|
func test_loads_world_ids():
|
||||||
|
assert_true(db.has_location("greywater_docks"))
|
||||||
|
assert_true(db.has_quest("find_the_ledger"))
|
||||||
|
assert_true(db.has_item("worn_shortsword"))
|
||||||
|
assert_true(db.has_item("coin"))
|
||||||
|
|
||||||
|
|
||||||
|
func test_companions_are_brannoc_and_cadwyn():
|
||||||
|
var ids := []
|
||||||
|
for n in db.companions():
|
||||||
|
ids.append(n["id"])
|
||||||
|
ids.sort()
|
||||||
|
assert_eq(ids, ["brannoc_thane","cadwyn_vell"])
|
||||||
|
|
||||||
|
|
||||||
|
func test_good_origin_has_no_unresolved_refs():
|
||||||
|
assert_eq(db.unresolved_refs(_deserter()), [])
|
||||||
|
|
||||||
|
|
||||||
|
func test_broken_location_ref_detected():
|
||||||
|
var o := _deserter()
|
||||||
|
o["start_location_id"] = "nowhere"
|
||||||
|
assert_true("location:nowhere" in db.unresolved_refs(o))
|
||||||
|
|
||||||
|
|
||||||
|
func test_broken_item_ref_detected():
|
||||||
|
var o := _deserter()
|
||||||
|
o["inventory_grants"] = o["inventory_grants"].duplicate(true)
|
||||||
|
o["inventory_grants"].append({"item_id": "ghost_blade", "qty": 1})
|
||||||
|
assert_true("item:ghost_blade" in db.unresolved_refs(o))
|
||||||
|
|
||||||
|
|
||||||
|
func test_null_quest_ref_resolves():
|
||||||
|
var o := _deserter()
|
||||||
|
o["start_quest_id"] = null
|
||||||
|
assert_eq(db.unresolved_refs(o), [])
|
||||||
1
client/tests/unit/test_content_db.gd.uid
Normal file
1
client/tests/unit/test_content_db.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bi2ysgccscqym
|
||||||
Reference in New Issue
Block a user