Skip to content

Commit 1529a82

Browse files
committed
serialize into ModData and ModDetails for better type safety and convenience
1 parent 9f3e6c9 commit 1529a82

File tree

3 files changed

+263
-163
lines changed

3 files changed

+263
-163
lines changed

addons/mod_loader/mod_data.gd

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
extends Resource
2+
class_name ModData
3+
4+
# These 2 files are always required by mods.
5+
# mod_main.gd = The main init file for the mod
6+
# manifest.json = Meta data for the mod, including its dependancies
7+
const LOG_NAME := "ModLoader:ModData"
8+
9+
enum required_mod_files {
10+
MOD_MAIN,
11+
MANIFEST,
12+
}
13+
14+
var dir_name := "" # technically a duplicate with ModDetails
15+
var dir_path := ""
16+
var is_loadable := true
17+
var importance := 0
18+
var details: ModDetails
19+
var config := {} # updated in _load_mod_configs
20+
21+
# debug
22+
var file_paths := []
23+
24+
25+
func _init(_dir_path: String) -> void:
26+
dir_path = _dir_path
27+
28+
29+
# Load meta data from a mod's manifest.json file
30+
func load_details(modLoader = ModLoader) -> void:
31+
if not has_required_files():
32+
return
33+
34+
modLoader.mod_log("Loading mod_details (manifest.json) for -> %s" % dir_name, LOG_NAME)
35+
36+
# Load meta data file
37+
var manifest_path = get_required_mod_file_path(required_mod_files.MANIFEST)
38+
var manifest_dict = modLoader._get_json_as_dict(manifest_path) # todo get from utils
39+
40+
modLoader.dev_log("%s loaded manifest data -> %s" % [dir_name, manifest_dict], LOG_NAME)
41+
42+
var mod_details := ModDetails.new(manifest_dict)
43+
44+
if not mod_details:
45+
is_loadable = false
46+
return
47+
48+
details = mod_details
49+
50+
51+
func is_mod_dir_name_same_as_id() -> bool:
52+
# Check that the mod ID is correct. This will fail if the mod's folder in
53+
# "res://mods-unpacked" does not match its full ID, which is `namespace.name`
54+
var manifest_id = details.get_mod_id()
55+
if dir_name != manifest_id:
56+
ModLoader.mod_log('ERROR - Mod directory name "%s" does not match the data in manifest.json. Expected "%s"' % [ dir_name, manifest_id ], LOG_NAME)
57+
is_loadable = false
58+
return false
59+
return true
60+
61+
62+
func has_required_files() -> bool:
63+
var file_check = File.new()
64+
65+
for required_file in required_mod_files:
66+
var file_path = get_required_mod_file_path(required_mod_files[required_file])
67+
68+
if !file_check.file_exists(file_path):
69+
ModLoader.mod_log("ERROR - %s is missing a required file: %s" % [dir_name, file_path], LOG_NAME)
70+
is_loadable = false
71+
return is_loadable
72+
73+
74+
func has_details() -> bool:
75+
return not details == null
76+
77+
78+
func get_required_mod_file_path(required_file: int) -> String:
79+
match required_file:
80+
required_mod_files.MOD_MAIN:
81+
return dir_path.plus_file("mod_main.gd")
82+
required_mod_files.MANIFEST:
83+
return dir_path.plus_file("manifest.json")
84+
return ""
85+
86+
87+
#func _to_string() -> String:
88+
# todo if we want it pretty printed
89+
90+

addons/mod_loader/mod_details.gd

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
extends Resource
2+
class_name ModDetails
3+
4+
var name := ""
5+
var namespace := ""
6+
var version_number := "v0.0.0"
7+
var description := ""
8+
var website_url := ""
9+
var dependencies := [] # Array[String]
10+
11+
var authors := [] # Array[String]
12+
var compatible_game_version := [] # Array[String]
13+
var tags := [] # Array[String]
14+
var description_rich := ""
15+
var incompatibilities := [] # Array[String]
16+
var image: StreamTexture
17+
18+
## Required keys in a mod's manifest.json file
19+
const REQUIRED_MANIFEST_KEYS_ROOT = [
20+
"name",
21+
"namespace",
22+
"version_number",
23+
"website_url",
24+
"description",
25+
"dependencies",
26+
"extra",
27+
]
28+
29+
## Required keys in manifest's `json.extra.godot`
30+
const REQUIRED_MANIFEST_KEYS_EXTRA = [
31+
"authors",
32+
"compatible_mod_loader_version",
33+
"compatible_game_version",
34+
"incompatibilities",
35+
"config_defaults",
36+
]
37+
38+
39+
func _init(manifest: Dictionary) -> void:
40+
if (not dict_has_fields(manifest, REQUIRED_MANIFEST_KEYS_ROOT) or
41+
not dict_has_fields(manifest.extra, ["godot"]) or
42+
not dict_has_fields(manifest.extra.godot, REQUIRED_MANIFEST_KEYS_EXTRA)):
43+
return
44+
45+
name = manifest.name
46+
namespace = manifest.namespace
47+
version_number = manifest.version_number
48+
if (not is_name_or_namespace_valid(name) or
49+
not is_name_or_namespace_valid(namespace) or
50+
not is_semver_valid(version_number)):
51+
return
52+
53+
54+
description = manifest.description
55+
website_url = manifest.website_url
56+
dependencies = manifest.dependencies
57+
58+
var godot_details: Dictionary = manifest.extra.godot
59+
authors = _get_array_from_dict(godot_details, "authors")
60+
incompatibilities = _get_array_from_dict(godot_details, "incompatibilities")
61+
compatible_game_version = _get_array_from_dict(godot_details, "compatible_game_version")
62+
description_rich = _get_string_from_dict(godot_details, "description_rich")
63+
tags = _get_array_from_dict(godot_details, "tags")
64+
65+
# todo load file named icon.png when loading mods and use here
66+
# image StreamTexture
67+
68+
69+
func get_mod_id() -> String:
70+
return "%s-%s" % [namespace, name]
71+
72+
73+
func get_package_id() -> String:
74+
return "%s-%s-%s" % [namespace, name, version_number]
75+
76+
77+
static func is_name_or_namespace_valid(name: String) -> bool:
78+
var re := RegEx.new()
79+
re.compile("^[a-zA-Z0-9_]*$") # alphanumeric and _
80+
81+
if re.search(name) == null:
82+
printerr('Invalid name or namespace: "%s". You may only use letters, numbers and underscores.' % name)
83+
return false
84+
85+
re.compile("^[a-zA-Z0-9_]{3,}$") # at least 3 long
86+
if re.search(name) == null:
87+
printerr('Invalid name or namespace: "%s". Must be longer than 3 characters.' % name)
88+
return false
89+
90+
return true
91+
92+
93+
func _get_string_from_dict(dict: Dictionary, key: String) -> String:
94+
if not dict.has(key):
95+
return ""
96+
return dict[key]
97+
98+
99+
func _get_array_from_dict(dict: Dictionary, key: String) -> Array:
100+
if not dict.has(key):
101+
return []
102+
return dict[key]
103+
104+
105+
static func is_semver_valid(version_number: String) -> bool:
106+
var re := RegEx.new()
107+
re.compile("^[0-9]+\\.[0-9]+\\.[0-9]+$")
108+
109+
if re.search(version_number) == null:
110+
printerr('Invalid semantic version: "%s". You may only use numbers and periods in this format {mayor}.{minor}.{patch}' % version_number)
111+
return false
112+
113+
return true
114+
115+
116+
static func dict_has_fields(dict: Dictionary, required_fields: Array) -> bool:
117+
var missing_fields := required_fields
118+
119+
for key in dict.keys():
120+
if(required_fields.has(key)):
121+
missing_fields.erase(key)
122+
123+
if missing_fields.size() > 0:
124+
printerr("Mod data is missing required fields: " + str(missing_fields))
125+
return false
126+
127+
return true
128+
129+
130+
#func _to_json() -> String:
131+
# return ""
132+

0 commit comments

Comments
 (0)