11const LevelManager = preload(
"res://scripts/systems/levelManager/level_manager.gd")
13const DEFAULT_ROOT :=
"res://data/games"
15var level_manager = LevelManager.new()
18func ensure_root(root_path:String = DEFAULT_ROOT) -> String:
19 var root:String = root_path.strip_edges()
22 DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(root))
23 return root.trim_suffix(
"/")
26func ensure_game_dir(game_id:String, root_path:String = DEFAULT_ROOT) -> String:
27 var dir_path:String = game_dir(game_id, root_path)
28 DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(dir_path))
32func game_dir(game_id:String, root_path:String = DEFAULT_ROOT) -> String:
33 var safe_game:String = _safe_id(game_id
if game_id.strip_edges() !=
"" else "shared")
34 return "%s/%s/custom_levels" % [ensure_root(root_path), safe_game]
37func path_for_level(game_id:String, level_id:String, root_path:String = DEFAULT_ROOT) -> String:
38 var safe_level:String = _safe_id(level_id
if level_id.strip_edges() !=
"" else "custom_level")
39 return "%s/%s.json" % [ensure_game_dir(game_id, root_path), safe_level]
42func list_levels(game_types:Array = [], root_path:String = DEFAULT_ROOT) -> Array:
43 var root:String = ensure_root(root_path)
44 var accepted_types:Array = _normalize_game_types(game_types)
46 _scan_dir(root, accepted_types, rows, root.ends_with(
"/custom_levels"))
47 rows.sort_custom(func(a, b):
return str(a.get(
"name", a.get(
"id",
""))).to_lower() < str(b.get(
"name", b.get(
"id",
""))).to_lower())
51func save_level(level_data:Dictionary, game_id:String =
"", root_path:String = DEFAULT_ROOT) -> String:
52 var resolved_game:String = game_id.strip_edges()
53 if resolved_game ==
"":
54 resolved_game = _game_id_for_level(level_data)
55 var level_id:String = str(level_data.get(
"id",
"custom_level"))
56 var file_path:String = path_for_level(resolved_game, level_id, root_path)
57 var file = FileAccess.open(file_path, FileAccess.WRITE)
60 file.store_string(JSON.stringify(level_data,
"\t"))
64func load_level_data(file_path:String) -> Dictionary:
65 var file = FileAccess.open(file_path, FileAccess.READ)
68 var parsed = JSON.parse_string(file.get_as_text())
69 return parsed.duplicate(true)
if parsed
is Dictionary
else {}
72func get_summary_for_path(file_path:String) -> Dictionary:
73 var data:Dictionary = load_level_data(file_path)
76 return _summary_for_data(data, file_path)
79func _scan_dir(dir_path:String, accepted_types:Array, rows:Array, in_custom_levels := false) -> void:
80 var dir = DirAccess.open(dir_path)
84 var entry:String = dir.get_next()
86 if entry.begins_with(
"."):
87 entry = dir.get_next()
89 var child_path:String =
"%s/%s" % [dir_path.trim_suffix(
"/"), entry]
90 if dir.current_is_dir():
91 _scan_dir(child_path, accepted_types, rows, in_custom_levels
or entry ==
"custom_levels")
92 elif in_custom_levels
and entry.get_extension().to_lower() ==
"json":
93 var row:Dictionary = get_summary_for_path(child_path)
94 if not row.is_empty()
and _matches_game_type(row, accepted_types):
96 entry = dir.get_next()
100func _summary_for_data(data:Dictionary, file_path:String) -> Dictionary:
101 var summary:Dictionary = level_manager.get_summary(data)
102 var game_type:String = _game_type_for_level(data)
103 summary[
"id"] = str(data.get(
"id", summary.get(
"id", file_path.get_file().get_basename())))
104 summary[
"name"] = str(data.get(
"name", summary.get(
"name", summary[
"id"])))
105 summary[
"description"] = str(data.get(
"description",
"Custom level"))
106 summary[
"path"] = file_path
107 summary[
"source"] =
"custom"
108 summary[
"custom"] = true
109 summary[
"game_type"] = game_type
110 summary[
"metadata"] = data.get(
"metadata", {}).duplicate(true)
if data.get(
"metadata", {})
is Dictionary
else {}
111 var issues:Array = level_manager.validate_level(data)
112 summary[
"valid"] = issues.is_empty()
113 summary[
"issues"] = issues
117func _game_type_for_level(data:Dictionary) -> String:
118 var metadata = data.get(
"metadata", {})
119 if metadata
is Dictionary
and str(metadata.get(
"game_type",
"")).strip_edges() !=
"":
120 return str(metadata.get(
"game_type",
"shared"))
121 return str(data.get(
"game_type",
"shared"))
124func _game_id_for_level(data:Dictionary) -> String:
125 var metadata = data.get(
"metadata", {})
126 if metadata
is Dictionary
and str(metadata.get(
"game_id",
"")).strip_edges() !=
"":
127 return str(metadata.get(
"game_id",
"shared"))
128 return str(data.get(
"game_id", _game_type_for_level(data)))
131func _matches_game_type(row:Dictionary, accepted_types:Array) -> bool:
132 if accepted_types.is_empty():
134 var game_type:String = str(row.get(
"game_type",
"shared")).to_lower()
135 return accepted_types.has(game_type)
138func _normalize_game_types(game_types:Array) -> Array:
139 var normalized:Array = []
140 for value
in game_types:
141 var id:String = str(value).strip_edges().to_lower()
142 if id !=
"" and not normalized.has(id):
143 normalized.append(id)
147func _safe_id(value:String) -> String:
148 var raw:String = value.strip_edges().to_lower()
149 var result:String =
""
150 for index
in range(raw.length()):
151 var character:String = raw.substr(index, 1)
152 var allowed:bool = (character >=
"a" and character <=
"z")
or (character >=
"0" and character <=
"9")
or character ==
"_" or character ==
"-"
153 result += character
if allowed
else "_"
154 while result.contains(
"__"):
155 result = result.replace(
"__",
"_")
156 result = result.strip_edges().trim_prefix(
"_").trim_suffix(
"_")
157 return result
if result !=
"" else "custom_level"