RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
build_profile_manager.gd
Go to the documentation of this file.
1extends RefCounted
2
3
7
8const DataCacheManager = preload("res://scripts/systems/dataCacheManager/data_cache_manager.gd")
9
10var profiles := {}
11var data_cache_manager = DataCacheManager.new()
12
13
14
15func load_profiles(directory_path:String = "res://data/build_profiles") -> Array:
16 profiles.clear()
17 var loaded_ids := []
18 var dir = DirAccess.open(directory_path)
19 if dir == null:
20 return loaded_ids
21 dir.list_dir_begin()
22 var file_name = dir.get_next()
23 while file_name != "":
24 if not dir.current_is_dir() and file_name.ends_with(".json"):
25 var profile = load_profile("%s/%s" % [directory_path.trim_suffix("/"), file_name])
26 if not profile.is_empty():
27 loaded_ids.append(str(profile.get("game_id", "")))
28 file_name = dir.get_next()
29 dir.list_dir_end()
30 return loaded_ids
31
32
33
34func load_profile(file_path:String) -> Dictionary:
35 var data:Dictionary = data_cache_manager.load_json(file_path)
36 var profile = normalize_profile(data)
37 if str(profile.get("game_id", "")) != "":
38 profiles[str(profile["game_id"])] = profile
39 return profile
40
41
42
43func get_profile(game_id:String) -> Dictionary:
44 return profiles.get(game_id, {}).duplicate(true)
45
46
47
48func get_profile_ids() -> Array:
49 var ids = profiles.keys()
50 ids.sort()
51 return ids
52
53
54
55func get_profile_options(include_all_demos:bool = true) -> Array:
56 var rows := []
57 for id in get_profile_ids():
58 if not include_all_demos and str(id) == "all_demos":
59 continue
60 var profile = profiles[id]
61 var metadata = profile.get("metadata", {}) if profile.get("metadata", {}) is Dictionary else {}
62 rows.append({
63 "id": str(id),
64 "label": str(profile.get("label", id)),
65 "menu_label": str(metadata.get("menu_label", profile.get("label", id))),
66 "menu_route": str(profile.get("menu_route", id)),
67 "menu_order": int(metadata.get("menu_order", 999)),
68 "sprite_id": str(metadata.get("sprite_id", "")),
69 "description": str(metadata.get("description", "")),
70 "start_scene": str(profile.get("start_scene", "")),
71 "entry_scene": str(profile.get("entry_scene", profile.get("start_scene", ""))),
72 "launch_scene": str(profile.get("launch_scene", profile.get("start_scene", ""))),
73 "output_name": str(profile.get("output_name", id)),
74 "platforms": profile.get("platforms", {}).keys()
75 })
76 rows.sort_custom(func(a, b): return int(a.get("menu_order", 999)) < int(b.get("menu_order", 999)))
77 return rows
78
79
80
83func get_sprite_map_options(games_directory_path:String = "res://data/games") -> Array:
84 var rows := []
85 var discovered_paths := {}
86 for profile_option in get_profile_options(false):
87 var game_id := str(profile_option.get("id", ""))
88 var sprite_path := "%s/%s/sprites.json" % [games_directory_path.trim_suffix("/"), game_id]
89 if game_id == "" or not FileAccess.file_exists(sprite_path):
90 continue
91 rows.append({
92 "id": game_id,
93 "label": str(profile_option.get("label", game_id.capitalize())),
94 "path": sprite_path
95 })
96 discovered_paths[sprite_path] = true
97
98 var directory := DirAccess.open(games_directory_path)
99 if directory != null:
100 directory.list_dir_begin()
101 var game_directory := directory.get_next()
102 while game_directory != "":
103 if directory.current_is_dir() and not game_directory.begins_with("."):
104 var sprite_path := "%s/%s/sprites.json" % [games_directory_path.trim_suffix("/"), game_directory]
105 if FileAccess.file_exists(sprite_path) and not discovered_paths.has(sprite_path):
106 rows.append({
107 "id": game_directory,
108 "label": game_directory.capitalize().replace("_", " "),
109 "path": sprite_path
110 })
111 discovered_paths[sprite_path] = true
112 game_directory = directory.get_next()
113 directory.list_dir_end()
114
115 return rows
116
117
118
119func normalize_profile(profile:Dictionary) -> Dictionary:
120 var normalized = profile.duplicate(true)
121 normalized["game_id"] = str(normalized.get("game_id", ""))
122 normalized["label"] = str(normalized.get("label", normalized.get("game_id", "")))
123 normalized["start_scene"] = str(normalized.get("start_scene", "res://scenes/splash_screen.tscn"))
124 normalized["entry_scene"] = str(normalized.get("entry_scene", normalized.get("start_scene", "res://scenes/splash_screen.tscn")))
125 normalized["launch_scene"] = str(normalized.get("launch_scene", normalized.get("start_scene", "res://scenes/start_menu.tscn")))
126 normalized["menu_route"] = str(normalized.get("menu_route", normalized.get("game_id", "")))
127 normalized["output_name"] = str(normalized.get("output_name", normalized.get("game_id", "game")))
128 normalized["include_roots"] = _string_array(normalized.get("include_roots", []))
129 normalized["shared_roots"] = _string_array(normalized.get("shared_roots", []))
130 normalized["exclude_roots"] = _string_array(normalized.get("exclude_roots", []))
131 normalized["exclude_game_ids"] = _string_array(normalized.get("exclude_game_ids", []))
132 normalized["platforms"] = normalized.get("platforms", {}) if normalized.get("platforms", {}) is Dictionary else {}
133 normalized["metadata"] = normalized.get("metadata", {}) if normalized.get("metadata", {}) is Dictionary else {}
134 return normalized
135
136
137
138func validate_profile(profile:Dictionary) -> Array:
139 var normalized = normalize_profile(profile)
140 var issues := []
141 for field in ["game_id", "label", "start_scene", "output_name"]:
142 if str(normalized.get(field, "")).strip_edges() == "":
143 issues.append(_issue("missing_field", field, "Build profile is missing '%s'." % field))
144 if not FileAccess.file_exists(str(normalized.get("start_scene", ""))):
145 issues.append(_issue("missing_scene", "start_scene", "Start scene does not exist."))
146 if str(normalized.get("entry_scene", "")).strip_edges() != "" and not FileAccess.file_exists(str(normalized.get("entry_scene", ""))):
147 issues.append(_issue("missing_scene", "entry_scene", "Entry scene does not exist."))
148 if str(normalized.get("launch_scene", "")).strip_edges() != "" and not FileAccess.file_exists(str(normalized.get("launch_scene", ""))):
149 issues.append(_issue("missing_scene", "launch_scene", "Launch scene does not exist."))
150 if normalized.get("include_roots", []).is_empty():
151 issues.append(_issue("missing_include_roots", "include_roots", "At least one game include root is required."))
152 if normalized.get("platforms", {}).is_empty():
153 issues.append(_issue("missing_platforms", "platforms", "At least one platform artifact definition is required."))
154 for root in get_all_include_roots(normalized):
155 if not _resource_exists(root):
156 issues.append(_issue("missing_include_root", root, "Included resource root does not exist."))
157 for platform_id in normalized.get("platforms", {}).keys():
158 var platform = normalized["platforms"][platform_id]
159 if not platform is Dictionary:
160 issues.append(_issue("invalid_platform", str(platform_id), "Platform entry must be an object."))
161 continue
162 if str(platform.get("preset", "")).strip_edges() == "":
163 issues.append(_issue("missing_platform_preset", str(platform_id), "Platform entry is missing export preset name."))
164 if str(platform.get("artifact", "")).strip_edges() == "":
165 issues.append(_issue("missing_platform_artifact", str(platform_id), "Platform entry is missing artifact path."))
166 return issues
167
168
169
170func get_all_include_roots(profile:Dictionary) -> Array:
171 var roots := []
172 for root in _string_array(profile.get("shared_roots", [])) + _string_array(profile.get("include_roots", [])):
173 if root != "" and not roots.has(root):
174 roots.append(root)
175 return roots
176
177
178
180func get_exclude_roots(profile:Dictionary, all_game_ids:Array = []) -> Array:
181 var normalized = normalize_profile(profile)
182 var excludes := _string_array(normalized.get("exclude_roots", []))
183 var ids = _string_array(normalized.get("exclude_game_ids", []))
184 if ids.is_empty():
185 for game_id in _string_array(all_game_ids):
186 if game_id != str(normalized.get("game_id", "")):
187 ids.append(game_id)
188 for game_id in ids:
189 var root = "res://data/games/%s" % game_id
190 if not excludes.has(root):
191 excludes.append(root)
192 return excludes
193
194
195
196func get_platform_artifacts(profile:Dictionary) -> Dictionary:
197 var normalized = normalize_profile(profile)
198 var output_name = str(normalized.get("output_name", "Game"))
199 var artifacts := {}
200 for platform_id in normalized.get("platforms", {}).keys():
201 var platform = normalized["platforms"][platform_id]
202 if not platform is Dictionary:
203 continue
204 var row = platform.duplicate(true)
205 if str(row.get("artifact", "")) == "":
206 row["artifact"] = "builds/%s/%s" % [str(platform_id), output_name]
207 artifacts[str(platform_id)] = row
208 return artifacts
209
210
211
212func get_export_plan(profile:Dictionary, all_game_ids:Array = []) -> Dictionary:
213 var normalized = normalize_profile(profile)
214 return {
215 "game_id": normalized.get("game_id", ""),
216 "label": normalized.get("label", ""),
217 "start_scene": normalized.get("start_scene", ""),
218 "entry_scene": normalized.get("entry_scene", ""),
219 "launch_scene": normalized.get("launch_scene", ""),
220 "menu_route": normalized.get("menu_route", ""),
221 "include_roots": get_all_include_roots(normalized),
222 "exclude_roots": get_exclude_roots(normalized, all_game_ids),
223 "platforms": get_platform_artifacts(normalized),
224 "metadata": normalized.get("metadata", {})
225 }
226
227
228
231func audit_single_game_profile(profile:Dictionary, all_game_ids:Array = [], required_shared_roots:Array = [], required_game_roots:Array = []) -> Dictionary:
232 var normalized:Dictionary = normalize_profile(profile)
233 var game_id:String = str(normalized.get("game_id", ""))
234 var include_roots:Array = get_all_include_roots(normalized)
235 var exclude_roots:Array = get_exclude_roots(normalized, all_game_ids)
236 var missing_shared_roots:Array = []
237 var missing_game_roots:Array = []
238 var missing_exclude_roots:Array = []
239 var unexpected_game_data_includes:Array = []
240 for root_value in _string_array(required_shared_roots):
241 if not include_roots.has(str(root_value)):
242 missing_shared_roots.append(str(root_value))
243 for root_value in _string_array(required_game_roots):
244 if not include_roots.has(str(root_value)):
245 missing_game_roots.append(str(root_value))
246 for other_game_id_value in _string_array(all_game_ids):
247 var other_game_id:String = str(other_game_id_value)
248 if other_game_id == "" or other_game_id == game_id or other_game_id == "all_demos":
249 continue
250 var other_data_root:String = "res://data/games/%s" % other_game_id
251 if include_roots.has(other_data_root):
252 unexpected_game_data_includes.append(other_data_root)
253 if not exclude_roots.has(other_data_root):
254 missing_exclude_roots.append(other_data_root)
255 return {
256 "game_id": game_id,
257 "include_roots": include_roots,
258 "exclude_roots": exclude_roots,
259 "missing_shared_roots": missing_shared_roots,
260 "missing_game_roots": missing_game_roots,
261 "missing_exclude_roots": missing_exclude_roots,
262 "unexpected_game_data_includes": unexpected_game_data_includes,
263 "valid": missing_shared_roots.is_empty() and missing_game_roots.is_empty() and missing_exclude_roots.is_empty() and unexpected_game_data_includes.is_empty()
264 }
265
266
267
269func audit_single_game_profiles(profile_ids:Array = [], required_shared_roots_by_game:Dictionary = {}, required_game_roots_by_game:Dictionary = {}) -> Dictionary:
270 var ids:Array = _string_array(profile_ids)
271 if ids.is_empty():
272 ids = get_profile_ids()
273 var all_game_ids:Array = []
274 for id_value in ids:
275 var profile_id:String = str(id_value)
276 if profile_id != "" and profile_id != "all_demos":
277 all_game_ids.append(profile_id)
278 var audits := {}
279 var valid := true
280 for id_value in all_game_ids:
281 var game_id:String = str(id_value)
282 var profile:Dictionary = get_profile(game_id)
283 if profile.is_empty():
284 audits[game_id] = {
285 "game_id": game_id,
286 "valid": false,
287 "missing_profile": true
288 }
289 valid = false
290 continue
291 var shared_roots:Array = _string_array(required_shared_roots_by_game.get(game_id, []))
292 var game_roots:Array = _string_array(required_game_roots_by_game.get(game_id, []))
293 var audit:Dictionary = audit_single_game_profile(profile, all_game_ids, shared_roots, game_roots)
294 audits[game_id] = audit
295 if not bool(audit.get("valid", false)):
296 valid = false
297 return {
298 "valid": valid,
299 "audits": audits,
300 "game_ids": all_game_ids
301 }
302
303
304func _resource_exists(path:String) -> bool:
305 if path == "":
306 return false
307 if FileAccess.file_exists(path):
308 return true
309 return DirAccess.open(path) != null
310
311
312func get_cache_stats() -> Dictionary:
313 return data_cache_manager.get_cache_stats()
314
315
316func _string_array(value) -> Array:
317 var result := []
318 if not value is Array:
319 return result
320 for item in value:
321 result.append(str(item))
322 return result
323
324
325func _issue(type:String, path:String, message:String) -> Dictionary:
326 return {"type": type, "path": path, "message": message}
bool
Return compact rows suitable for editor pickers, menus, or CI dashboards.