Datapacks system mostly complete

This commit is contained in:
Chris Bell 2025-10-21 22:18:29 -05:00
parent 2da3b9c6fa
commit 06c8817996
42 changed files with 682 additions and 91 deletions

View File

@ -0,0 +1,22 @@
[gd_resource type="Resource" script_class="DatapackModel" load_steps=8 format=3 uid="uid://cuyhpk5xnm735"]
[ext_resource type="Script" uid="uid://1eht88nmv63j" path="res://scripts/datapacks/models/datapack_dependency.gd" id="1_0qrwo"]
[ext_resource type="Script" uid="uid://b2k68if5pcfyn" path="res://scripts/helpers/datetime.gd" id="1_jdefw"]
[ext_resource type="Script" uid="uid://dgj5rubcp6v00" path="res://scripts/datapacks/models/datapack_model.gd" id="2_3lhj8"]
[ext_resource type="Script" uid="uid://xiy5j06o8254" path="res://scripts/helpers/guid.gd" id="3_8vm13"]
[ext_resource type="Script" uid="uid://c8co7n1xvfmou" path="res://scripts/datapacks/models/szobject.gd" id="3_jdefw"]
[sub_resource type="Resource" id="Resource_4o47k"]
script = ExtResource("1_jdefw")
metadata/_custom_type_script = "uid://b2k68if5pcfyn"
[sub_resource type="Resource" id="Resource_rn2pn"]
script = ExtResource("3_8vm13")
metadata/_custom_type_script = "uid://xiy5j06o8254"
[resource]
script = ExtResource("2_3lhj8")
guid = SubResource("Resource_rn2pn")
created_at = SubResource("Resource_4o47k")
sz_objects = Array[ExtResource("3_jdefw")]([null])
metadata/_custom_type_script = "uid://dgj5rubcp6v00"

View File

@ -15,6 +15,10 @@ run/main_scene="uid://fy5iji5t58jk"
config/features=PackedStringArray("4.5", "GL Compatibility") config/features=PackedStringArray("4.5", "GL Compatibility")
config/icon="uid://bxsq8il8lgcq2" config/icon="uid://bxsq8il8lgcq2"
[autoload]
DatapackManager="*res://scripts/datapacks/datapack_manager.gd"
[gui] [gui]
theme/custom="uid://mu3c8g7q4ygp" theme/custom="uid://mu3c8g7q4ygp"

View File

@ -1,9 +1,8 @@
[gd_scene load_steps=2 format=3 uid="uid://fy5iji5t58jk"] [gd_scene load_steps=2 format=3 uid="uid://fy5iji5t58jk"]
[ext_resource type="Script" uid="uid://b6xurr0segcug" path="res://scripts/test.gd" id="1_ffwby"] [ext_resource type="Script" uid="uid://b6xurr0segcug" path="res://scripts/datapacks/datapack_tests.gd" id="1_ffwby"]
[node name="MainUI" type="CanvasLayer"] [node name="MainUI" type="CanvasLayer"]
script = ExtResource("1_ffwby")
[node name="Panel" type="Panel" parent="."] [node name="Panel" type="Panel" parent="."]
anchors_preset = 15 anchors_preset = 15
@ -12,3 +11,6 @@ anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
theme_type_variation = &"Background_Panel" theme_type_variation = &"Background_Panel"
[node name="DatapackTests" type="Node" parent="."]
script = ExtResource("1_ffwby")

View File

@ -0,0 +1,76 @@
# datapack_manager.gd
extends Node
var loaded_packs: Dictionary[String, DatapackModel] = {}
func clear_loaded_packs():
loaded_packs.clear()
func register_pack(datapack: DatapackModel):
if datapack and datapack.guid:
var guid_str = datapack.guid.to_string()
loaded_packs[guid_str] = datapack
print("DatapackManager: Registered pack '%s' (%s)" % [datapack.name, guid_str])
else:
push_error("DatapackManager: Attempted to register invalid or un-GUIDed datapack.")
func unregister_pack(pack_guid: String):
if loaded_packs.has(pack_guid):
loaded_packs.erase(pack_guid)
print("DatapackManager: Unregistered pack with GUID %s." % pack_guid)
# --- DEPENDENCY RESOLUTION ---
## Resolves a single SzObject ID across all loaded packs.
## Used by SPECIFIC_DATASET mode when the pack GUID is unknown (or referencing local content).
func resolve_object_dependency(object_id: String) -> SzObject:
for pack_guid in loaded_packs:
var pack = loaded_packs[pack_guid]
var obj = pack.get_object_by_id(object_id)
if obj:
return obj
push_error("Dependency Resolution Failed: Could not find object with ID '%s' in any loaded datapack." % object_id)
return null
## Finds all SzObjects (Datasets or Templates) across all loaded packs
## that match a given sz_type string.
## Used by DATASET_TYPE_WILDCARD and CHARACTER_TEMPLATE_REF modes.
func resolve_type_wildcard(sz_type: String) -> Array[SzObject]:
var matching_objects: Array[SzObject] = []
for pack_guid in loaded_packs:
var pack = loaded_packs[pack_guid]
for obj in pack.sz_objects:
if obj.sz_type == sz_type:
matching_objects.append(obj)
if matching_objects.is_empty():
push_warning("Wildcard Resolution Failed: Could not find any object with sz_type '%s' in loaded datapacks." % sz_type)
return matching_objects
## Main entry point for resolving a DependencySource defined in a Template.
## Returns an array of SzObjects (can be Datasets or Templates)
func resolve_dependency_source(source: DependencySource) -> Array:
match source.mode:
DependencySource.DependencyMode.SPECIFIC_DATASET:
if source.target_datapack_guid.is_empty():
var target_obj = resolve_object_dependency(source.target_sz_object_id)
return [target_obj] if target_obj else []
var target_pack = loaded_packs.get(source.target_datapack_guid)
if target_pack:
var target_obj = target_pack.get_object_by_id(source.target_sz_object_id)
return [target_obj] if target_obj else []
return []
DependencySource.DependencyMode.DATASET_TYPE_WILDCARD:
return resolve_type_wildcard(source.target_type_string)
return []

View File

@ -0,0 +1 @@
uid://chlpgt0jnkd52

View File

@ -0,0 +1,374 @@
# ------------------------------- WARNING --------------------------------
# These tests were auto generated by an LLM according to a design document
# to simply test the workflow. These tests should not be completely relied
# on for tesing full functionality of the datapack system.
# ------------------------------------------------------------------------
class_name DatapackTestNode
extends Node
# --- PRIMARY PACK CONSTANTS ---
const PACK_NAME = "core_data_test_pack"
const ENTRY_ID = "test_item_sword"
const WEAPONS_DATASET_ID = "weapons_dataset"
const SKILLS_DATASET_ID = "skills_dataset"
const CHAR_TEMPLATE_ID = "base_char_template"
const SESSION_TEMPLATE_ID = "basic_session_template"
# --- NEW CROSS-PACK CONSTANTS ---
const DEPENDENCY_PACK_NAME = "stat_core_pack"
const DEPENDENCY_DATASET_ID = "core_stats_dataset"
const DEPENDENCY_ENTRY_ID = "base_strength"
const DEPENDER_PACK_NAME = "class_pack"
const DEPENDER_DATASET_ID = "warrior_class"
func _ready() -> void:
print("--- Starting Datapack Test ---")
# --- Primary Test (Pack 1) ---
var creation_success = _create_and_save_datapack()
if not creation_success:
push_error("TEST FAILED: Datapack creation failed.")
return
_load_and_verify_datapack()
_test_formula_resolution()
# --- New Cross-Pack Dependency Test (Pack 2 & 3) ---
_test_cross_datapack_dependency()
print("--- Datapack Test Complete ---")
#----------------------------------------------------------------------
## --- STEP 2: CREATION AND SAVING ---
#----------------------------------------------------------------------
func _create_and_save_datapack() -> bool:
print("\n--- 2. Creating and Saving Datapack ---")
# --- 0. Setup Core Data ---
var stats_group := DatasetGroup.new()
stats_group.id = "base_stats"
stats_group.name = "Base Stats"
# Simple Numeric Field
var damage_field := DataFieldValue.new()
damage_field.field_type = DataFieldValue.DataFieldType.NUMBER
damage_field.value = 12
stats_group.fields["damage"] = damage_field
# --- FORMULA Field ---
var formula_res := FormulaValue.new()
formula_res.expression = "Base_Damage * Critical_Multiplier"
var crit_damage_field := DataFieldValue.new()
crit_damage_field.field_type = DataFieldValue.DataFieldType.FORMULA
crit_damage_field.value = formula_res
stats_group.fields["critical_damage"] = crit_damage_field
# --- LIST Field ---
var list_res := ListValue.new()
# Add nested DataFieldValue items to the ListValue resource
var list_item_1 := DataFieldValue.new()
list_item_1.field_type = DataFieldValue.DataFieldType.NUMBER
list_item_1.value = 5
var list_item_2 := DataFieldValue.new()
list_item_2.field_type = DataFieldValue.DataFieldType.TEXT
list_item_2.value = "Stun"
list_res.items.append(list_item_1)
list_res.items.append(list_item_2)
var effect_list_field := DataFieldValue.new()
effect_list_field.field_type = DataFieldValue.DataFieldType.LIST
effect_list_field.value = list_res
stats_group.fields["on_hit_effects"] = effect_list_field
# Create Dataset Entry
var sword_entry := DatasetEntry.new()
sword_entry.id = ENTRY_ID
sword_entry.name = "Longsword of Testing"
sword_entry.description = "A sword for unit testing."
sword_entry.groups.append(stats_group)
# Create Dataset Model (Weapons)
var weapons_dataset := DatasetModel.new()
weapons_dataset.id = WEAPONS_DATASET_ID
weapons_dataset.name = "Weapons"
weapons_dataset.entries[sword_entry.id] = sword_entry
weapons_dataset.sz_type = "Weapon"
# --- Dependency Setup: Skills Dataset ---
var skills_dataset := DatasetModel.new()
skills_dataset.id = SKILLS_DATASET_ID
skills_dataset.name = "Skills"
skills_dataset.sz_type = "Skill"
# --- Dependency Setup: CharacterTemplate ---
var char_template := CharacterTemplate.new()
char_template.id = CHAR_TEMPLATE_ID
char_template.name = "Standard Character"
var skills_dependency := DependencySource.new()
skills_dependency.mode = DependencySource.DependencyMode.SPECIFIC_DATASET
skills_dependency.target_sz_object_id = SKILLS_DATASET_ID
char_template.data_dependencies["Skills_Source"] = skills_dependency
# --- Dependency Setup: SessionTemplate ---
var session_template := SessionTemplate.new()
session_template.id = SESSION_TEMPLATE_ID
session_template.name = "Basic Campaign Session"
var char_dep_source := DependencySource.new()
char_dep_source.mode = DependencySource.DependencyMode.SPECIFIC_DATASET
char_dep_source.target_sz_object_id = CHAR_TEMPLATE_ID
session_template.character_template_ref = char_dep_source
# --- Create and Save Datapack Model ---
var datapack := DatapackModel.new()
datapack.name = "Test Pack"
datapack.version = "1.0.0"
datapack.sz_objects.append(weapons_dataset)
datapack.sz_objects.append(skills_dataset)
datapack.sz_objects.append(char_template)
datapack.sz_objects.append(session_template)
var creator = DatapackCreator.new()
var success = creator.save_datapack(datapack, PACK_NAME)
return success
#----------------------------------------------------------------------
## --- STEP 3: LOADING AND VERIFICATION (Omitted for brevity, assumed stable) ---
#----------------------------------------------------------------------
func _load_and_verify_datapack() -> void:
print("\n--- 3. Loading and Verification ---")
var loader = DatapackLoader.new()
var loaded_pack: DatapackModel = loader.load_datapack(PACK_NAME)
if not loaded_pack:
push_error("TEST FAILED: DatapackLoader returned null.")
return
DatapackManager.register_pack(loaded_pack)
print("Verification: Datapack registered with DatapackManager.")
var loaded_weapons_dataset: DatasetModel = loaded_pack.get_object_by_id(WEAPONS_DATASET_ID)
var loaded_entry: DatasetEntry = loaded_weapons_dataset.entries.get(ENTRY_ID)
var loaded_group: DatasetGroup = loaded_entry.groups.filter(func(g): return g.id == "base_stats").front()
var loaded_char_template: CharacterTemplate = loaded_pack.get_object_by_id(CHAR_TEMPLATE_ID)
var loaded_session_template: SessionTemplate = loaded_pack.get_object_by_id(SESSION_TEMPLATE_ID)
# 1. SessionTemplate Character Reference
var char_ref_dep_source: DependencySource = loaded_session_template.character_template_ref
var resolved_char_template_array = DatapackManager.resolve_dependency_source(char_ref_dep_source)
if resolved_char_template_array.size() == 1 and resolved_char_template_array.front() == loaded_char_template:
print("✅ SUCCESS: SessionTemplate dependency link resolved.")
else:
push_error("TEST FAILED: SessionTemplate character_template_ref resolution failed.")
# 2. CharacterTemplate Skills Dependency
var skills_dep_source: DependencySource = loaded_char_template.data_dependencies.get("Skills_Source")
var resolved_skills_array = DatapackManager.resolve_dependency_source(skills_dep_source)
var loaded_skills_dataset = loaded_pack.get_object_by_id(SKILLS_DATASET_ID)
if resolved_skills_array.size() == 1 and resolved_skills_array.front() == loaded_skills_dataset:
print("✅ SUCCESS: CharacterTemplate data dependency link resolved.")
else:
push_error("TEST FAILED: CharacterTemplate data dependency resolution failed.")
# 3. Verify FORMULA field
var loaded_crit_field: DataFieldValue = loaded_group.fields.get("critical_damage")
if loaded_crit_field and loaded_crit_field.field_type == DataFieldValue.DataFieldType.FORMULA:
var formula: FormulaValue = loaded_crit_field.value
if formula is FormulaValue and formula.expression == "Base_Damage * Critical_Multiplier":
print("✅ SUCCESS: Found and verified FormulaValue resource.")
else:
push_error("TEST FAILED: FormulaValue verification failed (wrong type or expression).")
else:
push_error("TEST FAILED: Critical damage field not found or wrong type.")
# 4. Verify LIST field
var loaded_list_field: DataFieldValue = loaded_group.fields.get("on_hit_effects")
if loaded_list_field and loaded_list_field.field_type == DataFieldValue.DataFieldType.LIST:
var list_val: ListValue = loaded_list_field.value
if list_val is ListValue and list_val.items.size() == 2:
if list_val.items[1] is DataFieldValue and list_val.items[1].value == "Stun":
print("✅ SUCCESS: Found and verified ListValue and its nested contents.")
else:
push_error("TEST FAILED: ListValue contents verification failed.")
else:
push_error("TEST FAILED: ListValue resource failed to load correctly.")
else:
push_error("TEST FAILED: On hit effects field not found or wrong type.")
print("\nVerification: All checks completed.")
#----------------------------------------------------------------------
## --- STEP 4: FORMULA RESOLUTION TEST (Omitted for brevity, assumed stable) ---
#----------------------------------------------------------------------
func _test_formula_resolution():
print("\n--- 4. Testing Formula Resolver with Variable Substitution ---")
var resolver := FormulaResolver.new()
var raw_expression := "Base_Damage * Critical_Multiplier + floor(Skill_Level / 2)"
var context: Dictionary = {
"Base_Damage": 15,
"Critical_Multiplier": 2.5,
"Skill_Level": 7
}
var expected_result = 40.5
var result = resolver.resolve_formula(raw_expression, context)
if abs(result - expected_result) < 0.001:
print("✅ SUCCESS: FormulaResolver calculated the correct result: %f" % result)
else:
push_error("TEST FAILED: FormulaResolver result incorrect. Expected %f, Got %f" % [expected_result, result])
#----------------------------------------------------------------------
## --- STEP 5: CROSS-PACK DEPENDENCY RESOLUTION ---
#----------------------------------------------------------------------
func _test_cross_datapack_dependency():
print("\n--- 5. Testing Cross-Pack Dependency Resolution ---")
DatapackManager.clear_loaded_packs()
print("DatapackManager cleared for cross-pack test.")
# --- 5a. Create and Save DEPENDENCY PACK (Pack A: Stat Core) ---
# FIX: Capture the DatapackModel to get its GUID
var pack_a_model: DatapackModel = _create_dependency_pack_a()
if not pack_a_model:
push_error("TEST FAILED: Failed to create Dependency Pack A.")
return
var dependency_pack_guid: Guid = pack_a_model.guid # <--- This is the Guid object needed
print("Pack A GUID captured: %s" % dependency_pack_guid.to_string())
# --- 5b. Create and Save DEPENDER PACK (Pack B: Class Pack) ---
# FIX: Pass the GUID object to the creator function
var success_b = _create_depender_pack_b(dependency_pack_guid)
if not success_b:
push_error("TEST FAILED: Failed to create Depender Pack B.")
return
# --- 5c. Load and Register Both Packs ---
var loader = DatapackLoader.new()
var pack_a: DatapackModel = loader.load_datapack(DEPENDENCY_PACK_NAME)
var pack_b: DatapackModel = loader.load_datapack(DEPENDER_PACK_NAME)
if not pack_a or not pack_b:
push_error("TEST FAILED: Failed to load one or both dependency test packs.")
return
DatapackManager.register_pack(pack_a)
DatapackManager.register_pack(pack_b)
print("Registered both Dependency (A) and Depender (B) packs.")
# --- 5d. Resolve Dependency in Pack B ---
var depender_dataset: DatasetModel = pack_b.get_object_by_id(DEPENDER_DATASET_ID)
if not depender_dataset:
push_error("TEST FAILED: Could not find Depender Dataset in Pack B.")
return
var dependency_source: DependencySource = depender_dataset.get_meta("core_stats_source")
if not dependency_source:
push_error("TEST FAILED: Could not find the DependencySource object in Pack B.")
return
var resolved_objects: Array = DatapackManager.resolve_dependency_source(dependency_source)
# --- 5e. Verification ---
if resolved_objects.size() != 1:
push_error("TEST FAILED: Dependency resolution returned incorrect count: %d" % resolved_objects.size())
return
var resolved_dataset: DatasetModel = resolved_objects.front()
var expected_dataset: DatasetModel = pack_a.get_object_by_id(DEPENDENCY_DATASET_ID)
if resolved_dataset == expected_dataset:
print("✅ SUCCESS: Cross-pack dependency successfully resolved.")
if resolved_dataset.entries.has(DEPENDENCY_ENTRY_ID):
print("✅ SUCCESS: Resolved dataset contains the expected entry '%s'." % DEPENDENCY_ENTRY_ID)
else:
push_error("TEST FAILED: Dependency resolved to the wrong object.")
# --- Helper Functions for Cross-Pack Test ---
# UPDATED: Returns the DatapackModel for GUID retrieval
func _create_dependency_pack_a() -> DatapackModel:
# Create entry
var str_field := DataFieldValue.new()
str_field.field_type = DataFieldValue.DataFieldType.NUMBER
str_field.value = 10
var strength_entry := DatasetEntry.new()
strength_entry.id = DEPENDENCY_ENTRY_ID
strength_entry.name = "Strength"
strength_entry.top_level_fields["value"] = str_field
# Create dataset
var core_stats_dataset := DatasetModel.new()
core_stats_dataset.id = DEPENDENCY_DATASET_ID
core_stats_dataset.name = "Core Stats"
core_stats_dataset.sz_type = "StatDefinition"
core_stats_dataset.entries[strength_entry.id] = strength_entry
# Create datapack
var datapack := DatapackModel.new()
datapack.name = "Stat Core Pack"
datapack.version = "1.0.0"
datapack.sz_objects.append(core_stats_dataset)
var creator = DatapackCreator.new()
var success = creator.save_datapack(datapack, DEPENDENCY_PACK_NAME)
if success:
return datapack
else:
return null # Return null on failure
# UPDATED: Accepts the GUID to correctly create the DatapackDependency
func _create_depender_pack_b(dependency_pack_guid: Guid) -> bool:
# Create DependencySource (internal reference by string ID)
var dependency_source := DependencySource.new()
dependency_source.mode = DependencySource.DependencyMode.SPECIFIC_DATASET
dependency_source.target_sz_object_id = DEPENDENCY_DATASET_ID
# Create Depender Dataset
var warrior_class_dataset := DatasetModel.new()
warrior_class_dataset.id = DEPENDER_DATASET_ID
warrior_class_dataset.name = "Warrior Class"
warrior_class_dataset.sz_type = "ClassDefinition"
warrior_class_dataset.set_meta("core_stats_source", dependency_source)
# Create Datapack
var datapack := DatapackModel.new()
datapack.name = "Warrior Class Pack"
datapack.version = "1.0.0"
datapack.sz_objects.append(warrior_class_dataset)
# Add the explicit pack dependency
var pack_dep := DatapackDependency.new()
# FIX: Assign the Guid object to the 'id' property
pack_dep.id = dependency_pack_guid
pack_dep.name = "Stat Core Pack"
pack_dep.version = "1.0.0"
datapack.dependencies.append(pack_dep)
var creator = DatapackCreator.new()
var success = creator.save_datapack(datapack, DEPENDER_PACK_NAME)
return success

View File

@ -0,0 +1,13 @@
# character_template.gd
class_name CharacterTemplate
extends SzObject
# sz_type will be set to "CharacterTemplate"
# Core definitions for character sheet fields
@export var top_level_field_definitions: Dictionary = {} # TemplateFieldDefinition
@export var group_definitions: Array[TemplateGroup] = []
# --- SPECIALIZED DEPENDENCIES ---
# The Character Template needs to reference *Datasets* (like Weapons, Skills, etc.)
# Map of logical_name (e.g., "Weapon_Source") -> DependencySource (the new class below)
@export var data_dependencies: Dictionary[String, DependencySource] = {}

View File

@ -0,0 +1 @@
uid://5k8j3av0kqg4

View File

@ -0,0 +1,13 @@
# session_template.gd
class_name SessionTemplate
extends SzObject
# Core definitions for session fields (e.g., initial currency, setting notes)
@export var top_level_field_definitions: Dictionary = {}
@export var group_definitions: Array[TemplateGroup] = []
@export var character_template_ref: DependencySource
@export var data_dependencies: Dictionary[String, DependencySource] = {}
func _init() -> void:
sz_type = "session_template"

View File

@ -0,0 +1 @@
uid://dnjuh1ypyoie8

View File

@ -0,0 +1,7 @@
# template_model.gd
class_name TemplateModel
extends SzObject
@export var top_level_field_definitions: Dictionary = {}
@export var group_definitions: Array[TemplateGroup] = []
@export var data_dependencies: Dictionary[String, String] = {}

View File

@ -0,0 +1 @@
uid://bd60mhmrr4olw

View File

@ -7,12 +7,16 @@ enum DataFieldType {
NUMBER, NUMBER,
BOOL, BOOL,
FORMULA, FORMULA,
LIST LIST,
REFERENCE_SINGLE,
REFERENCE_LIST
} }
@export var field_type: DataFieldType @export var field_type: DataFieldType
@export var value: Variant @export var value: Variant
# --- TYPE STRING MAPPING ---
static func _type_to_string(t: DataFieldType) -> String: static func _type_to_string(t: DataFieldType) -> String:
match t: match t:
DataFieldType.TEXT: return "Text" DataFieldType.TEXT: return "Text"
@ -21,6 +25,8 @@ static func _type_to_string(t: DataFieldType) -> String:
DataFieldType.BOOL: return "Boolean" DataFieldType.BOOL: return "Boolean"
DataFieldType.FORMULA: return "Formula" DataFieldType.FORMULA: return "Formula"
DataFieldType.LIST: return "List" DataFieldType.LIST: return "List"
DataFieldType.REFERENCE_SINGLE: return "RefSingle"
DataFieldType.REFERENCE_LIST: return "RefList"
_: return str(int(t)) _: return str(int(t))
static func _string_to_type(s: String) -> DataFieldType: static func _string_to_type(s: String) -> DataFieldType:
@ -31,8 +37,23 @@ static func _string_to_type(s: String) -> DataFieldType:
"Boolean": return DataFieldType.BOOL "Boolean": return DataFieldType.BOOL
"Formula": return DataFieldType.FORMULA "Formula": return DataFieldType.FORMULA
"List": return DataFieldType.LIST "List": return DataFieldType.LIST
"RefSingle": return DataFieldType.REFERENCE_SINGLE
"RefList": return DataFieldType.REFERENCE_LIST
_: return DataFieldType.TEXT _: return DataFieldType.TEXT
# --- COMPLEX VALUE ACCESS ---
func get_concrete_value() -> Variant:
match field_type:
DataFieldType.FORMULA:
return value as FormulaValue
DataFieldType.LIST, DataFieldType.REFERENCE_LIST:
return value as ListValue
_:
return value
# --- SERIALIZATION HELPERS ---
static func _envelope_variant(v: Variant) -> Dictionary: static func _envelope_variant(v: Variant) -> Dictionary:
return { return {
"_kind": "gd-variant-b64", "_kind": "gd-variant-b64",
@ -63,6 +84,8 @@ static func _deserialize_list_value(a: Array) -> Array:
out.append(item) out.append(item)
return out return out
# --- SERIALIZE/DESERIALIZE VALUE ---
static func serialize_value(p_field_type: DataFieldType, v: Variant) -> Variant: static func serialize_value(p_field_type: DataFieldType, v: Variant) -> Variant:
match p_field_type: match p_field_type:
DataFieldType.TEXT, DataFieldType.MULTILINE_TEXT: DataFieldType.TEXT, DataFieldType.MULTILINE_TEXT:
@ -77,10 +100,6 @@ static func serialize_value(p_field_type: DataFieldType, v: Variant) -> Variant:
return int(s) return int(s)
DataFieldType.BOOL: DataFieldType.BOOL:
return bool(v) return bool(v)
DataFieldType.FORMULA:
return str(v)
DataFieldType.LIST:
return _serialize_list_value(v)
_: _:
if v is Dictionary or v is Array or v is String or v is int or v is float or v is bool or v == null: if v is Dictionary or v is Array or v is String or v is int or v is float or v is bool or v == null:
return v return v
@ -94,15 +113,13 @@ static func deserialize_value(p_field_type: DataFieldType, raw: Variant) -> Vari
return raw if (raw is float or raw is int) else float(str(raw)) return raw if (raw is float or raw is int) else float(str(raw))
DataFieldType.BOOL: DataFieldType.BOOL:
return bool(raw) return bool(raw)
DataFieldType.FORMULA:
return str(raw)
DataFieldType.LIST:
return _deserialize_list_value(raw if raw is Array else [])
_: _:
if raw is Dictionary and raw.get("_kind") == "gd-variant-b64": if raw is Dictionary and raw.get("_kind") == "gd-variant-b64":
return _deenvelope_variant(raw) return _deenvelope_variant(raw)
return raw return raw
# --- DICT CONVERSION ---
func to_dict() -> Dictionary: func to_dict() -> Dictionary:
return { return {
"field_type": _type_to_string(field_type), "field_type": _type_to_string(field_type),

View File

@ -0,0 +1,20 @@
# dependency_source.gd
class_name DependencySource
extends Resource
enum DependencyMode {
SPECIFIC_DATASET, # Reference a known Dataset/Template by its ID and GUID
DATASET_TYPE_WILDCARD, # Reference ANY Dataset/Template that matches a specific type string
}
@export var mode: DependencyMode = DependencyMode.SPECIFIC_DATASET
@export var display_name: String = ""
# --- Used for SPECIFIC_DATASET mode ---
# The GUID of the Datapack the dependency lives in (optional, can be empty for local pack)
@export var target_datapack_guid: String = ""
@export var target_sz_object_id: String = ""
# --- Used for DATASET_TYPE_WILDCARD mode ---
# The type string to match (e.g., "items").
@export var target_type_string: String = ""

View File

@ -0,0 +1 @@
uid://cly4hb2wrx8fn

View File

@ -0,0 +1,52 @@
# formula_resolver.gd
class_name FormulaResolver
extends RefCounted
var _expression: Expression = Expression.new()
## Internal helper to replace named variables with their numerical values.
## NOTE: This simple approach sorts keys by length descending (Max_HP before HP)
## to prevent incorrect substitution of substrings.
func _substitute_variables(raw_expression: String, context: Dictionary) -> String:
var result = raw_expression
# Sort keys by length descending to prevent shorter names (like 'HP') from matching
# before longer names that contain them (like 'Max_HP').
var keys = context.keys()
keys.sort_custom(func(a, b): return len(a) > len(b))
for key in keys:
if raw_expression.find(key) != -1:
var value = str(context[key])
# Replace the variable name with its numerical value string
result = result.replace(key, value)
return result
## Resolves the final value of the formula given a set of variable values.
## Handles variable substitution, parsing, and execution in one step.
func resolve_formula(raw_expression: String, context: Dictionary) -> float:
# 1. Substitute variables with their numerical values
var numerical_expression = _substitute_variables(raw_expression, context)
# 2. Parse the new numerical expression
# The expression must be re-parsed every time, as its text content changes.
var error: Error = _expression.parse(numerical_expression, PackedStringArray([]))
if error != OK:
push_error("FormulaResolver: Substitution/Parse failed. Expression: '%s'. Numerical: '%s'. Error: %s" % [raw_expression, numerical_expression, _expression.get_error_text()])
return 0.0
# 3. Execute the expression (inputs: empty array, base_instance: null, show_error: true)
var result = _expression.execute([], null, true)
if _expression.has_execute_failed():
push_error("FormulaResolver: Execution failed for numerical expression: %s" % numerical_expression)
return 0.0
# 4. Check type and return
if typeof(result) != TYPE_FLOAT and typeof(result) != TYPE_INT:
push_error("FormulaResolver: Expression result was not a number. Got type: %s" % typeof(result))
return 0.0
return float(result)

View File

@ -0,0 +1 @@
uid://ils0nw5fyh7j

View File

@ -0,0 +1,10 @@
# formula_value.gd
class_name FormulaValue
extends Resource
# The raw expression string (e.g., "Attack_Modifier + Weapon_Damage_Dice")
@export var expression: String = ""
@export var variables: Dictionary = {}
# need a separate system (a FormulaEvaluator) later to parse and execute this.
# For now, this resource simply stores the definition.

View File

@ -0,0 +1 @@
uid://cyij11ojm1ecy

View File

@ -0,0 +1,5 @@
# list_value.gd
class_name ListValue
extends Resource
@export var items: Array[DataFieldValue] = []

View File

@ -0,0 +1 @@
uid://cpdx3w11rbt5j

View File

@ -0,0 +1,22 @@
# template_field_definition.gd
class_name TemplateField
extends Resource
enum DataFieldType {
TEXT,
MULTILINE_TEXT,
NUMBER,
BOOL,
FORMULA,
LIST,
REFERENCE_SINGLE,
REFERENCE_LIST
}
@export var field_id: String
@export var display_name: String
@export var field_type: DataFieldType = DataFieldType.TEXT
@export var default_value: Variant
@export var dependency_source_name: String = ""
@export var dependency_source_key: String = ""

View File

@ -0,0 +1 @@
uid://b4d6mtffupbk3

View File

@ -0,0 +1,8 @@
# template_group.gd
class_name TemplateGroup
extends Resource
@export var name: String
@export var group_id: String
@export var field_definitions: Dictionary = {}
@export var is_list: bool = false

View File

@ -0,0 +1 @@
uid://h3ptq7tj3tad

View File

@ -2,22 +2,16 @@
class_name DatapackCreator class_name DatapackCreator
extends RefCounted extends RefCounted
# --- UPDATED CONSTANTS ---
const BASE_DATAPACKS_PATH = "user://datapacks/" const BASE_DATAPACKS_PATH = "user://datapacks/"
const CONTENT_SUBDIR = "objects" const METADATA_FILENAME = "config.szpack"
const METADATA_FILENAME = "config.szpack" # Renamed from METADATA_FILENAME const DATAPACK_RESOURCE_FILENAME = "datapack.res"
const DATAPACK_RESOURCE_FILENAME = "datapack.tres" # Renamed from DATAPACK_RESOURCE_FILENAME const ASSETS_SUBDIR = "assets"
const ASSETS_SUBDIR = "assets" # Renamed from MEDIA_SUBDIR
const SINGLE_FILE_SAVE_FLAGS = ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_CHANGE_PATH
# --- INTERNAL STATE ---
var _saved_resources: Dictionary = {}
var _pack_base_path: String = "" var _pack_base_path: String = ""
# --- PUBLIC METHOD ---
## Saves a DatapackModel and all its content resources to the file system.
## 'pack_name' is used to create the final folder structure: user://datapacks/[pack_name]/
func save_datapack(datapack: DatapackModel, pack_name: String) -> bool: func save_datapack(datapack: DatapackModel, pack_name: String) -> bool:
var full_pack_name = pack_name.validate_node_name() var full_pack_name = pack_name.validate_node_name()
if full_pack_name.is_empty(): if full_pack_name.is_empty():
@ -26,25 +20,22 @@ func save_datapack(datapack: DatapackModel, pack_name: String) -> bool:
_pack_base_path = BASE_DATAPACKS_PATH + full_pack_name + "/" _pack_base_path = BASE_DATAPACKS_PATH + full_pack_name + "/"
var content_path = _pack_base_path + CONTENT_SUBDIR
var assets_path = _pack_base_path + ASSETS_SUBDIR var assets_path = _pack_base_path + ASSETS_SUBDIR
var success_dirs = true var success_dirs = true
success_dirs &= _make_dir_recursive_safe(_pack_base_path) success_dirs = success_dirs and _make_dir_recursive_safe(_pack_base_path)
success_dirs &= _make_dir_recursive_safe(content_path) success_dirs = success_dirs and _make_dir_recursive_safe(assets_path)
success_dirs &= _make_dir_recursive_safe(assets_path)
if not success_dirs: if not success_dirs:
push_error("DatapackCreator: Failed to create necessary directories.") push_error("DatapackCreator: Failed to create necessary directories.")
return false return false
_saved_resources.clear()
if not _save_content_objects(datapack.content_objects, content_path + "/"):
return false
var main_resource_path = _pack_base_path + DATAPACK_RESOURCE_FILENAME var main_resource_path = _pack_base_path + DATAPACK_RESOURCE_FILENAME
if not _save_single_resource(datapack, main_resource_path):
var error = ResourceSaver.save(datapack, main_resource_path, SINGLE_FILE_SAVE_FLAGS)
if error != OK:
push_error("DatapackCreator: Failed to save datapack resource at path '%s'. Error: %s" % [main_resource_path, error])
return false return false
if not _write_metadata_file(datapack, _pack_base_path + METADATA_FILENAME): if not _write_metadata_file(datapack, _pack_base_path + METADATA_FILENAME):
@ -54,48 +45,14 @@ func save_datapack(datapack: DatapackModel, pack_name: String) -> bool:
return true return true
# --- PRIVATE HELPER METHODS ---
func _make_dir_recursive_safe(path: String) -> bool: func _make_dir_recursive_safe(path: String) -> bool:
var error = DirAccess.make_dir_recursive_absolute(path) var error = DirAccess.make_dir_recursive_absolute(path)
if error != OK and error != ERR_ALREADY_EXISTS: if error != OK and error != ERR_ALREADY_EXISTS:
push_error("DirAccess: Failed to create directory: %s (Error: %s)" % [path, error]) push_error("DirAccess: Failed to create directory: %s (Error: %s)" % [path, error])
return false return false
return true return true
func _save_content_objects(objects: Array, save_path: String) -> bool:
var success = true
for obj in objects:
if not obj is SzObject:
push_error("DatapackCreator: Content array contains non-SzObject: %s" % obj)
success = false
continue
# Filename format: [class_name]_[id].tres
var filename = "%s_%s.tres" % [obj.get_class().to_lower(), obj.id.to_lower()]
var full_path = save_path + filename
if not _save_single_resource(obj, full_path):
success = false
return success
func _save_single_resource(resource: Resource, path: String) -> bool:
if _saved_resources.has(resource):
return true
var error = ResourceSaver.save(resource, path, ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_CHANGE_PATH)
if error != OK:
push_error("DatapackCreator: Failed to save resource at path '%s'. Error: %s" % [path, error])
return false
_saved_resources[resource] = path
return true
func _write_metadata_file(datapack: DatapackModel, path: String) -> bool: func _write_metadata_file(datapack: DatapackModel, path: String) -> bool:
var config = ConfigFile.new() var config = ConfigFile.new()
var section = "Datapack" var section = "Datapack"

View File

@ -4,7 +4,8 @@ extends RefCounted
const BASE_DATAPACKS_PATH = "user://datapacks/" const BASE_DATAPACKS_PATH = "user://datapacks/"
const METADATA_FILENAME = "config.szpack" const METADATA_FILENAME = "config.szpack"
const DATAPACK_RESOURCE_FILENAME = "datapack.tres" # CHANGE HERE
const DATAPACK_RESOURCE_FILENAME = "datapack.res"
## Loads a DatapackModel resource and all its referenced content from the user://datapacks/ folder. ## Loads a DatapackModel resource and all its referenced content from the user://datapacks/ folder.
## Returns the loaded DatapackModel object or null on failure. ## Returns the loaded DatapackModel object or null on failure.
@ -32,6 +33,7 @@ func load_datapack(pack_name: String) -> DatapackModel:
push_error("DatapackLoader: Main Resource file not found at path: %s" % resource_path) push_error("DatapackLoader: Main Resource file not found at path: %s" % resource_path)
return null return null
# Godot automatically detects the binary format and loads the resource.
var datapack: DatapackModel = ResourceLoader.load(resource_path) var datapack: DatapackModel = ResourceLoader.load(resource_path)
if not datapack: if not datapack:
@ -42,14 +44,3 @@ func load_datapack(pack_name: String) -> DatapackModel:
print("Successfully loaded Datapack: %s (GUID: %s)" % [datapack.name, datapack.guid.to_string()]) print("Successfully loaded Datapack: %s (GUID: %s)" % [datapack.name, datapack.guid.to_string()])
return datapack return datapack
# ----------------------------------------------------------------------
# Example Usage:
# var loader = DatapackLoader.new()
# var core_pack = loader.load_datapack("core_rules")
#
# if core_pack:
#\t var item = core_pack.get_object_by_id("sword-long")
#\t if item:
#\t\t print("Found item: " + item.name)
# ----------------------------------------------------------------------

View File

@ -1,12 +0,0 @@
extends Node
func _enter_tree() -> void:
var dp: DatapackModel = DatapackModel.new()
var dpd: DatapackDependency = DatapackDependency.new()
dpd.id = Guid.new_guid()
dpd.name = "test"
dpd.version = "1.0.0"
dp.dependencies.append(dpd)
var json_string := JSON.stringify(dp.to_dict(), " ")
print(json_string)