forked from GodotModding/godot-mod-loader
-
Notifications
You must be signed in to change notification settings - Fork 0
/
mod_manifest.gd
324 lines (260 loc) · 11.3 KB
/
mod_manifest.gd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
class_name ModManifest
extends Resource
# Stores and validates contents of the manifest set by the user
const LOG_NAME := "ModLoader:ModManifest"
# Mod name.
# Validated by [method is_name_or_namespace_valid]
var name := ""
# Mod namespace, most commonly the main author.
# Validated by [method is_name_or_namespace_valid]
var namespace := ""
# Semantic version. Not a number, but required to be named like this by Thunderstore
# Validated by [method is_semver_valid]
var version_number := "0.0.0"
var description := ""
var website_url := ""
# Used to determine mod load order
var dependencies: PoolStringArray = []
# Used to determine mod load order
var optional_dependencies: PoolStringArray = []
var authors: PoolStringArray = []
# only used for information
var compatible_game_version: PoolStringArray = []
# only used for information
# Validated by [method _handle_compatible_mod_loader_version]
var compatible_mod_loader_version: PoolStringArray = []
# only used for information
var incompatibilities: PoolStringArray = []
var load_before: PoolStringArray = []
var tags : PoolStringArray = []
var config_defaults := {}
var description_rich := ""
var image: StreamTexture
# Required keys in a mod's manifest.json file
const REQUIRED_MANIFEST_KEYS_ROOT = [
"name",
"namespace",
"version_number",
"website_url",
"description",
"dependencies",
"extra",
]
# Required keys in manifest's `json.extra.godot`
const REQUIRED_MANIFEST_KEYS_EXTRA = [
"authors",
"compatible_mod_loader_version",
"compatible_game_version",
"incompatibilities",
"config_defaults",
]
# Takes the manifest as [Dictionary] and validates everything.
# Will return null if something is invalid.
func _init(manifest: Dictionary) -> void:
if (not ModLoaderUtils.dict_has_fields(manifest, REQUIRED_MANIFEST_KEYS_ROOT) or
not ModLoaderUtils.dict_has_fields(manifest.extra, ["godot"]) or
not ModLoaderUtils.dict_has_fields(manifest.extra.godot, REQUIRED_MANIFEST_KEYS_EXTRA)):
return
name = manifest.name
namespace = manifest.namespace
version_number = manifest.version_number
if (not is_name_or_namespace_valid(name) or
not is_name_or_namespace_valid(namespace) or
not is_semver_valid(version_number)):
return
description = manifest.description
website_url = manifest.website_url
dependencies = manifest.dependencies
var godot_details: Dictionary = manifest.extra.godot
authors = ModLoaderUtils.get_array_from_dict(godot_details, "authors")
optional_dependencies = ModLoaderUtils.get_array_from_dict(godot_details, "optional_dependencies")
incompatibilities = ModLoaderUtils.get_array_from_dict(godot_details, "incompatibilities")
load_before = ModLoaderUtils.get_array_from_dict(godot_details, "load_before")
compatible_game_version = ModLoaderUtils.get_array_from_dict(godot_details, "compatible_game_version")
compatible_mod_loader_version = _handle_compatible_mod_loader_version(godot_details)
description_rich = ModLoaderUtils.get_string_from_dict(godot_details, "description_rich")
tags = ModLoaderUtils.get_array_from_dict(godot_details, "tags")
config_defaults = godot_details.config_defaults
var mod_id = get_mod_id()
if (not validate_dependencies_and_incompatibilities(mod_id, dependencies, incompatibilities) or
not validate_optional_dependencies(mod_id, optional_dependencies)):
return
# Mod ID used in the mod loader
# Format: {namespace}-{name}
func get_mod_id() -> String:
return "%s-%s" % [namespace, name]
# Package ID used by Thunderstore
# Format: {namespace}-{name}-{version_number}
func get_package_id() -> String:
return "%s-%s-%s" % [namespace, name, version_number]
# Returns the Manifest values as a dictionary
func get_as_dict() -> Dictionary:
return {
"name": name,
"namespace": namespace,
"version_number": version_number,
"description": description,
"website_url": website_url,
"dependencies": dependencies,
"optional_dependencies": optional_dependencies,
"authors": authors,
"compatible_game_version": compatible_game_version,
"compatible_mod_loader_version": compatible_mod_loader_version,
"incompatibilities": incompatibilities,
"load_before": load_before,
"tags": tags,
"config_defaults": config_defaults,
"description_rich": description_rich,
"image": image,
}
# Returns the Manifest values as JSON, in the manifest.json format
func to_json() -> String:
return JSON.print({
"name": name,
"namespace": namespace,
"version_number": version_number,
"description": description,
"website_url": website_url,
"dependencies": dependencies,
"extra": {
"godot":{
"authors": authors,
"optional_dependencies": optional_dependencies,
"compatible_game_version": compatible_game_version,
"compatible_mod_loader_version": compatible_mod_loader_version,
"incompatibilities": incompatibilities,
"load_before": load_before,
"tags": tags,
"config_defaults": config_defaults,
"description_rich": description_rich,
"image": image,
}
}
}, "\t")
# Handles deprecation of the single string value in the compatible_mod_loader_version.
func _handle_compatible_mod_loader_version(godot_details: Dictionary) -> Array:
var link_manifest_docs := "https://github.com/GodotModding/godot-mod-loader/wiki/Mod-Files#manifestjson"
var array_value := ModLoaderUtils.get_array_from_dict(godot_details, "compatible_mod_loader_version")
# If there are array values
if array_value.size() > 0:
# Check for valid versions
if not is_semver_version_array_valid(array_value):
return []
return array_value
# If the array is empty check if a string was passed
var string_value := ModLoaderUtils.get_string_from_dict(godot_details, "compatible_mod_loader_version")
# If an empty string was passed
if string_value == "":
ModLoaderUtils.log_error(
"\"compatible_mod_loader_version\" is a required field." +
" For more details visit " + link_manifest_docs,
LOG_NAME
)
return []
# If a string was passed
ModLoaderDeprecated.deprecated_message(
"The single String value for \"compatible_mod_loader_version\" is deprecated." +
" Please provide an Array. For more details visit " + link_manifest_docs,
"6.0.0"
)
return [string_value]
# A valid namespace may only use letters (any case), numbers and underscores
# and has to be longer than 3 characters
# a-z A-Z 0-9 _ (longer than 3 characters)
static func is_name_or_namespace_valid(check_name: String, is_silent := false) -> bool:
var re := RegEx.new()
var _compile_error_1 = re.compile("^[a-zA-Z0-9_]*$") # alphanumeric and _
if re.search(check_name) == null:
if not is_silent:
ModLoaderUtils.log_fatal('Invalid name or namespace: "%s". You may only use letters, numbers and underscores.' % check_name, LOG_NAME)
return false
var _compile_error_2 = re.compile("^[a-zA-Z0-9_]{3,}$") # at least 3 long
if re.search(check_name) == null:
if not is_silent:
ModLoaderUtils.log_fatal('Invalid name or namespace: "%s". Must be longer than 3 characters.' % check_name, LOG_NAME)
return false
return true
static func is_semver_version_array_valid(version_array: PoolStringArray, is_silent := false) -> bool:
var is_valid := true
for version in version_array:
if not is_semver_valid(version, is_silent):
is_valid = false
return is_valid
# A valid semantic version should follow this format: {mayor}.{minor}.{patch}
# reference https://semver.org/ for details
# {0-9}.{0-9}.{0-9} (no leading 0, shorter than 16 characters total)
static func is_semver_valid(check_version_number: String, is_silent := false) -> bool:
var re := RegEx.new()
var _compile_error = re.compile("^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$")
if re.search(check_version_number) == null:
if not is_silent:
# Using str() here because format strings cause an error
ModLoaderUtils.log_fatal(
str(
'Invalid semantic version: "%s".',
'You may only use numbers without leading zero and periods',
'following this format {mayor}.{minor}.{patch}'
) % check_version_number,
LOG_NAME
)
return false
if check_version_number.length() > 16:
if not is_silent:
ModLoaderUtils.log_fatal('Invalid semantic version: "%s". ' +
'Version number must be shorter than 16 characters.', LOG_NAME
)
return false
return true
static func validate_dependencies_and_incompatibilities(mod_id: String, dependencies: PoolStringArray, incompatibilities: PoolStringArray, is_silent := false) -> bool:
var valid_dependencies := validate_dependencies(mod_id, dependencies, is_silent)
var valid_incompatibilities := validate_incompatibilities(mod_id, incompatibilities, is_silent)
if not valid_dependencies or not valid_incompatibilities:
return false
return true
static func validate_optional_dependencies(mod_id: String, optional_dependencies: PoolStringArray, is_silent := false) -> bool:
return is_mod_id_array_valid(mod_id, optional_dependencies, "optional_dependency", is_silent)
static func validate_dependencies(mod_id: String, dependencies: PoolStringArray, is_silent := false) -> bool:
return is_mod_id_array_valid(mod_id, dependencies, "dependency", is_silent)
static func validate_incompatibilities(mod_id: String, incompatibilities: PoolStringArray, is_silent := false) -> bool:
return is_mod_id_array_valid(mod_id, incompatibilities, "incompatibility", is_silent)
static func is_mod_id_array_valid(own_mod_id: String, mod_id_array: PoolStringArray, mod_id_array_description: String, is_silent := false) -> bool:
var is_valid := true
# If there are mod ids
if mod_id_array.size() > 0:
for mod_id in mod_id_array:
# Check if mod id is the same as the mods mod id.
if mod_id == own_mod_id:
is_valid = false
if not is_silent:
ModLoaderUtils.log_fatal("The mod \"%s\" lists itself as \"%s\" in its own manifest.json file" % [mod_id, mod_id_array_description], LOG_NAME)
# Check if the mod id is a valid mod id.
if not is_mod_id_valid(own_mod_id, mod_id, mod_id_array_description, is_silent):
is_valid = false
return is_valid
static func is_mod_id_valid(original_mod_id: String, check_mod_id: String, type := "", is_silent := false) -> bool:
var intro_text = "A %s for the mod '%s' is invalid: " % [type, original_mod_id] if not type == "" else ""
# contains hyphen?
if not check_mod_id.count("-") == 1:
if not is_silent:
ModLoaderUtils.log_fatal(str(intro_text, 'Expected a single hyphen in the mod ID, but the %s was: "%s"' % [type, check_mod_id]), LOG_NAME)
return false
# at least 7 long (1 for hyphen, 3 each for namespace/name)
var mod_id_length = check_mod_id.length()
if mod_id_length < 7:
if not is_silent:
ModLoaderUtils.log_fatal(str(intro_text, 'Mod ID for "%s" is too short. It must be at least 7 characters, but its length is: %s' % [check_mod_id, mod_id_length]), LOG_NAME)
return false
var split = check_mod_id.split("-")
var check_namespace = split[0]
var check_name = split[1]
var re := RegEx.new()
re.compile("^[a-zA-Z0-9_]*$") # alphanumeric and _
if re.search(check_namespace) == null:
if not is_silent:
ModLoaderUtils.log_fatal(str(intro_text, 'Mod ID has an invalid namespace (author) for "%s". Namespace can only use letters, numbers and underscores, but was: "%s"' % [check_mod_id, check_namespace]), LOG_NAME)
return false
if re.search(check_name) == null:
if not is_silent:
ModLoaderUtils.log_fatal(str(intro_text, 'Mod ID has an invalid name for "%s". Name can only use letters, numbers and underscores, but was: "%s"' % [check_mod_id, check_name]), LOG_NAME)
return false
return true