RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
custom_level_library.gd
Go to the documentation of this file.
1extends RefCounted
2
3
10
11const LevelManager = preload("res://scripts/systems/levelManager/level_manager.gd")
12
13const DEFAULT_ROOT := "res://data/games"
14
15var level_manager = LevelManager.new()
16
17
18func ensure_root(root_path:String = DEFAULT_ROOT) -> String:
19 var root:String = root_path.strip_edges()
20 if root == "":
21 root = DEFAULT_ROOT
22 DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(root))
23 return root.trim_suffix("/")
24
25
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))
29 return dir_path
30
31
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]
35
36
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]
40
41
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)
45 var rows:Array = []
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())
48 return rows
49
50
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)
58 if file == null:
59 return ""
60 file.store_string(JSON.stringify(level_data, "\t"))
61 return file_path
62
63
64func load_level_data(file_path:String) -> Dictionary:
65 var file = FileAccess.open(file_path, FileAccess.READ)
66 if file == null:
67 return {}
68 var parsed = JSON.parse_string(file.get_as_text())
69 return parsed.duplicate(true) if parsed is Dictionary else {}
70
71
72func get_summary_for_path(file_path:String) -> Dictionary:
73 var data:Dictionary = load_level_data(file_path)
74 if data.is_empty():
75 return {}
76 return _summary_for_data(data, file_path)
77
78
79func _scan_dir(dir_path:String, accepted_types:Array, rows:Array, in_custom_levels := false) -> void:
80 var dir = DirAccess.open(dir_path)
81 if dir == null:
82 return
83 dir.list_dir_begin()
84 var entry:String = dir.get_next()
85 while entry != "":
86 if entry.begins_with("."):
87 entry = dir.get_next()
88 continue
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):
95 rows.append(row)
96 entry = dir.get_next()
97 dir.list_dir_end()
98
99
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
114 return summary
115
116
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"))
122
123
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)))
129
130
131func _matches_game_type(row:Dictionary, accepted_types:Array) -> bool:
132 if accepted_types.is_empty():
133 return true
134 var game_type:String = str(row.get("game_type", "shared")).to_lower()
135 return accepted_types.has(game_type)
136
137
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)
144 return normalized
145
146
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"