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
81func normalize_profile(profile:Dictionary) -> Dictionary:
82 var normalized = profile.duplicate(true)
83 normalized["game_id"] = str(normalized.get("game_id", ""))
84 normalized["label"] = str(normalized.get("label", normalized.get("game_id", "")))
85 normalized["start_scene"] = str(normalized.get("start_scene", "res://scenes/splash_screen.tscn"))
86 normalized["entry_scene"] = str(normalized.get("entry_scene", normalized.get("start_scene", "res://scenes/splash_screen.tscn")))
87 normalized["launch_scene"] = str(normalized.get("launch_scene", normalized.get("start_scene", "res://scenes/start_menu.tscn")))
88 normalized["menu_route"] = str(normalized.get("menu_route", normalized.get("game_id", "")))
89 normalized["output_name"] = str(normalized.get("output_name", normalized.get("game_id", "game")))
90 normalized["include_roots"] = _string_array(normalized.get("include_roots", []))
91 normalized["shared_roots"] = _string_array(normalized.get("shared_roots", []))
92 normalized["exclude_roots"] = _string_array(normalized.get("exclude_roots", []))
93 normalized["exclude_game_ids"] = _string_array(normalized.get("exclude_game_ids", []))
94 normalized["platforms"] = normalized.get("platforms", {}) if normalized.get("platforms", {}) is Dictionary else {}
95 normalized["metadata"] = normalized.get("metadata", {}) if normalized.get("metadata", {}) is Dictionary else {}
96 return normalized
97
98
99
100func validate_profile(profile:Dictionary) -> Array:
101 var normalized = normalize_profile(profile)
102 var issues := []
103 for field in ["game_id", "label", "start_scene", "output_name"]:
104 if str(normalized.get(field, "")).strip_edges() == "":
105 issues.append(_issue("missing_field", field, "Build profile is missing '%s'." % field))
106 if not FileAccess.file_exists(str(normalized.get("start_scene", ""))):
107 issues.append(_issue("missing_scene", "start_scene", "Start scene does not exist."))
108 if str(normalized.get("entry_scene", "")).strip_edges() != "" and not FileAccess.file_exists(str(normalized.get("entry_scene", ""))):
109 issues.append(_issue("missing_scene", "entry_scene", "Entry scene does not exist."))
110 if str(normalized.get("launch_scene", "")).strip_edges() != "" and not FileAccess.file_exists(str(normalized.get("launch_scene", ""))):
111 issues.append(_issue("missing_scene", "launch_scene", "Launch scene does not exist."))
112 if normalized.get("include_roots", []).is_empty():
113 issues.append(_issue("missing_include_roots", "include_roots", "At least one game include root is required."))
114 if normalized.get("platforms", {}).is_empty():
115 issues.append(_issue("missing_platforms", "platforms", "At least one platform artifact definition is required."))
116 for root in get_all_include_roots(normalized):
117 if not _resource_exists(root):
118 issues.append(_issue("missing_include_root", root, "Included resource root does not exist."))
119 for platform_id in normalized.get("platforms", {}).keys():
120 var platform = normalized["platforms"][platform_id]
121 if not platform is Dictionary:
122 issues.append(_issue("invalid_platform", str(platform_id), "Platform entry must be an object."))
123 continue
124 if str(platform.get("preset", "")).strip_edges() == "":
125 issues.append(_issue("missing_platform_preset", str(platform_id), "Platform entry is missing export preset name."))
126 if str(platform.get("artifact", "")).strip_edges() == "":
127 issues.append(_issue("missing_platform_artifact", str(platform_id), "Platform entry is missing artifact path."))
128 return issues
129
130
131
132func get_all_include_roots(profile:Dictionary) -> Array:
133 var roots := []
134 for root in _string_array(profile.get("shared_roots", [])) + _string_array(profile.get("include_roots", [])):
135 if root != "" and not roots.has(root):
136 roots.append(root)
137 return roots
138
139
140
142func get_exclude_roots(profile:Dictionary, all_game_ids:Array = []) -> Array:
143 var normalized = normalize_profile(profile)
144 var excludes := _string_array(normalized.get("exclude_roots", []))
145 var ids = _string_array(normalized.get("exclude_game_ids", []))
146 if ids.is_empty():
147 for game_id in _string_array(all_game_ids):
148 if game_id != str(normalized.get("game_id", "")):
149 ids.append(game_id)
150 for game_id in ids:
151 var root = "res://data/games/%s" % game_id
152 if not excludes.has(root):
153 excludes.append(root)
154 return excludes
155
156
157
158func get_platform_artifacts(profile:Dictionary) -> Dictionary:
159 var normalized = normalize_profile(profile)
160 var output_name = str(normalized.get("output_name", "Game"))
161 var artifacts := {}
162 for platform_id in normalized.get("platforms", {}).keys():
163 var platform = normalized["platforms"][platform_id]
164 if not platform is Dictionary:
165 continue
166 var row = platform.duplicate(true)
167 if str(row.get("artifact", "")) == "":
168 row["artifact"] = "builds/%s/%s" % [str(platform_id), output_name]
169 artifacts[str(platform_id)] = row
170 return artifacts
171
172
173
174func get_export_plan(profile:Dictionary, all_game_ids:Array = []) -> Dictionary:
175 var normalized = normalize_profile(profile)
176 return {
177 "game_id": normalized.get("game_id", ""),
178 "label": normalized.get("label", ""),
179 "start_scene": normalized.get("start_scene", ""),
180 "entry_scene": normalized.get("entry_scene", ""),
181 "launch_scene": normalized.get("launch_scene", ""),
182 "menu_route": normalized.get("menu_route", ""),
183 "include_roots": get_all_include_roots(normalized),
184 "exclude_roots": get_exclude_roots(normalized, all_game_ids),
185 "platforms": get_platform_artifacts(normalized),
186 "metadata": normalized.get("metadata", {})
187 }
188
189
190
193func audit_single_game_profile(profile:Dictionary, all_game_ids:Array = [], required_shared_roots:Array = [], required_game_roots:Array = []) -> Dictionary:
194 var normalized:Dictionary = normalize_profile(profile)
195 var game_id:String = str(normalized.get("game_id", ""))
196 var include_roots:Array = get_all_include_roots(normalized)
197 var exclude_roots:Array = get_exclude_roots(normalized, all_game_ids)
198 var missing_shared_roots:Array = []
199 var missing_game_roots:Array = []
200 var missing_exclude_roots:Array = []
201 var unexpected_game_data_includes:Array = []
202 for root_value in _string_array(required_shared_roots):
203 if not include_roots.has(str(root_value)):
204 missing_shared_roots.append(str(root_value))
205 for root_value in _string_array(required_game_roots):
206 if not include_roots.has(str(root_value)):
207 missing_game_roots.append(str(root_value))
208 for other_game_id_value in _string_array(all_game_ids):
209 var other_game_id:String = str(other_game_id_value)
210 if other_game_id == "" or other_game_id == game_id or other_game_id == "all_demos":
211 continue
212 var other_data_root:String = "res://data/games/%s" % other_game_id
213 if include_roots.has(other_data_root):
214 unexpected_game_data_includes.append(other_data_root)
215 if not exclude_roots.has(other_data_root):
216 missing_exclude_roots.append(other_data_root)
217 return {
218 "game_id": game_id,
219 "include_roots": include_roots,
220 "exclude_roots": exclude_roots,
221 "missing_shared_roots": missing_shared_roots,
222 "missing_game_roots": missing_game_roots,
223 "missing_exclude_roots": missing_exclude_roots,
224 "unexpected_game_data_includes": unexpected_game_data_includes,
225 "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()
226 }
227
228
229
231func audit_single_game_profiles(profile_ids:Array = [], required_shared_roots_by_game:Dictionary = {}, required_game_roots_by_game:Dictionary = {}) -> Dictionary:
232 var ids:Array = _string_array(profile_ids)
233 if ids.is_empty():
234 ids = get_profile_ids()
235 var all_game_ids:Array = []
236 for id_value in ids:
237 var profile_id:String = str(id_value)
238 if profile_id != "" and profile_id != "all_demos":
239 all_game_ids.append(profile_id)
240 var audits := {}
241 var valid := true
242 for id_value in all_game_ids:
243 var game_id:String = str(id_value)
244 var profile:Dictionary = get_profile(game_id)
245 if profile.is_empty():
246 audits[game_id] = {
247 "game_id": game_id,
248 "valid": false,
249 "missing_profile": true
250 }
251 valid = false
252 continue
253 var shared_roots:Array = _string_array(required_shared_roots_by_game.get(game_id, []))
254 var game_roots:Array = _string_array(required_game_roots_by_game.get(game_id, []))
255 var audit:Dictionary = audit_single_game_profile(profile, all_game_ids, shared_roots, game_roots)
256 audits[game_id] = audit
257 if not bool(audit.get("valid", false)):
258 valid = false
259 return {
260 "valid": valid,
261 "audits": audits,
262 "game_ids": all_game_ids
263 }
264
265
266func _resource_exists(path:String) -> bool:
267 if path == "":
268 return false
269 if FileAccess.file_exists(path):
270 return true
271 return DirAccess.open(path) != null
272
273
274func get_cache_stats() -> Dictionary:
275 return data_cache_manager.get_cache_stats()
276
277
278func _string_array(value) -> Array:
279 var result := []
280 if not value is Array:
281 return result
282 for item in value:
283 result.append(str(item))
284 return result
285
286
287func _issue(type:String, path:String, message:String) -> Dictionary:
288 return {"type": type, "path": path, "message": message}
bool
Return compact rows suitable for editor pickers, menus, or CI dashboards.