Compare commits
4 commits
main
...
feature/mo
Author | SHA1 | Date | |
---|---|---|---|
04caa89093 | |||
33188c1380 | |||
3cd2327edc | |||
63cca5c2fb |
15 changed files with 341 additions and 1 deletions
153
assetloader/AssetLoader.gd
Normal file
153
assetloader/AssetLoader.gd
Normal file
|
@ -0,0 +1,153 @@
|
|||
@tool
|
||||
extends Object
|
||||
class_name AssetLoader
|
||||
|
||||
#region Declaration ---
|
||||
# === CONST ===
|
||||
|
||||
## If you want to change the mod extension. Keep in mind that it's still a json under the hood
|
||||
const MOD_INFOS_EXTENSION = "modinfo"
|
||||
## If you want to change the localization extension. Keep in mind that it's still a json under the hood
|
||||
const LOC_EXTENSION = "loc"
|
||||
|
||||
## Path to the mods folder : [param res://mods] by default
|
||||
## [br]
|
||||
## (is not a const but should be treated as such, so the uppercase)
|
||||
var ASSETS_PATH
|
||||
|
||||
# === VAR ===
|
||||
var dir:DirAccess
|
||||
var mod_folders:PackedStringArray
|
||||
var critical_error := false
|
||||
|
||||
var mod_manifests: Dictionary[StringName,ModManifest]
|
||||
var mod_localizations: Dictionary[StringName,Dictionary] # {mod_id: {locale: {key: value}}}
|
||||
|
||||
# === SIGNALS ===
|
||||
signal loading_finished
|
||||
|
||||
#endregion ---
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
print_verbose("------ MOD LOADING STARTED ------")
|
||||
# Use res://mods/ in editor, executable's mods/ folder in exported builds
|
||||
if OS.has_feature("editor"):
|
||||
ASSETS_PATH = "res://"+ProjectSettings.get_setting("game/mods/mod_folder_name", "mods")
|
||||
else:
|
||||
# fallbakc to use mods folder next to executable
|
||||
ASSETS_PATH = OS.get_executable_path().get_base_dir() + "/" + ProjectSettings.get_setting("game/mods/mod_folder_name", "mods")
|
||||
|
||||
dir = DirAccess.open(ASSETS_PATH)
|
||||
print_debug(ASSETS_PATH)
|
||||
if not dir:
|
||||
push_error("AssetLoader: Mods folder not found at '%s'" % ASSETS_PATH)
|
||||
push_error("AssetLoader:",DirAccess.get_open_error())
|
||||
_show_error_popup("Mods folder not found at '%s'" % ASSETS_PATH)
|
||||
critical_error = true
|
||||
return
|
||||
mod_folders = dir.get_directories()
|
||||
if mod_folders.is_empty():
|
||||
push_error("AssetLoader: Mods folder '%s' is empty — no mods to load." % ASSETS_PATH)
|
||||
_show_error_popup("Mods folder '%s' is empty — no mods to load." % ASSETS_PATH)
|
||||
critical_error = true
|
||||
return
|
||||
|
||||
# === PRIVATE ===
|
||||
|
||||
|
||||
## This will show a native MessageBox on Windows,
|
||||
## a native dialog on macOS, and GTK/QT dialog on Linux.
|
||||
func _show_error_popup(message: String) -> void:
|
||||
OS.alert("AssetLoader:"+message, "AssetLoader:Error")
|
||||
|
||||
|
||||
# === PUBLIC ===
|
||||
func load_all():
|
||||
load_mods()
|
||||
load_mods_content()
|
||||
|
||||
|
||||
## Load and unpack all .pck before serialization
|
||||
func load_mods():
|
||||
for mod in mod_folders:
|
||||
var mod_name = mod
|
||||
var mod_path = ASSETS_PATH + "/" + mod_name
|
||||
var manifest_path = mod_path + "/" + mod_name + "." + MOD_INFOS_EXTENSION
|
||||
if FileAccess.file_exists(manifest_path):
|
||||
var manifest_file := FileAccess.open(manifest_path, FileAccess.READ)
|
||||
var manifest: ModManifest = ModManifest.new_from_file(manifest_file)
|
||||
if !manifest:continue
|
||||
if !mod_manifests.has(manifest.id):
|
||||
mod_manifests[manifest.id] = manifest
|
||||
manifest.path = mod_path
|
||||
print_verbose("Mod manifest is loaded: %s" % manifest.name)
|
||||
else:
|
||||
push_warning("Another mod as the same id:\n %s will not be loaded" % manifest_path)
|
||||
|
||||
else:
|
||||
push_warning("No manifest found in %s" % manifest_path)
|
||||
for mod in mod_manifests:
|
||||
print("Mod loaded: %s" % mod)
|
||||
|
||||
|
||||
func load_localizations():
|
||||
if mod_manifests.size() == 0:
|
||||
print_verbose("No mods to load localizations from.")
|
||||
return
|
||||
for mod_id in mod_manifests:
|
||||
var mod_path: String = mod_manifests[mod_id]["path"]
|
||||
var mod_name: String = mod_path.get_file()
|
||||
mod_localizations[mod_id] = {}
|
||||
dir.change_dir(mod_path)
|
||||
dir.list_dir_begin()
|
||||
var file_name = dir.get_next()
|
||||
while file_name != "":
|
||||
if file_name.ends_with("." + LOC_EXTENSION):
|
||||
var locale = file_name.get_basename().get_extension()
|
||||
var loc_path = mod_path + "/" + file_name
|
||||
var loc_file = FileAccess.open(loc_path, FileAccess.READ)
|
||||
if loc_file:
|
||||
var loc_data: Dictionary = JSON.parse_string(loc_file.get_as_text())
|
||||
if typeof(loc_data) == TYPE_DICTIONARY:
|
||||
mod_localizations[mod_id][locale] = loc_data
|
||||
print_verbose("Loaded localization '%s' for mod '%s'" % [locale, mod_id])
|
||||
else:
|
||||
push_warning("Invalid localization file: %s" % loc_path)
|
||||
else:
|
||||
push_warning("Failed to open localization file: %s" % loc_path)
|
||||
file_name = dir.get_next()
|
||||
dir.list_dir_end()
|
||||
|
||||
|
||||
func register_localizations():
|
||||
for mod_id in mod_localizations:
|
||||
for locale in mod_localizations[mod_id]:
|
||||
var translation = Translation.new()
|
||||
translation.locale = locale
|
||||
for key in mod_localizations[mod_id][locale]:
|
||||
translation.add_message(key, mod_localizations[mod_id][locale][key])
|
||||
TranslationServer.add_translation(translation)
|
||||
|
||||
|
||||
func load_mods_content():
|
||||
if OS.has_feature("editor"):
|
||||
# Assets should already be loaded/imported.
|
||||
# FIXME: here we should implement something to serialize ? maybe not.
|
||||
return
|
||||
if mod_manifests.size() == 0:
|
||||
print_verbose("No mods to load content from.")
|
||||
return
|
||||
for mod_id in mod_manifests:
|
||||
var modpacks:Array = mod_manifests[mod_id].get_mod_packs()
|
||||
for pack in modpacks:
|
||||
# Load assets from the PCKs (mounted at res://)
|
||||
var pck_path:StringName = mod_manifests[mod_id].path+ "/" + pack + ".pck"
|
||||
if dir.file_exists(pck_path):
|
||||
if ProjectSettings.load_resource_pack(pck_path):
|
||||
print_verbose("Loaded PCK for mod '%s': %s" % [mod_id, pck_path])
|
||||
mod_manifests[mod_id]._validated = true
|
||||
else:
|
||||
push_warning("Failed to load PCK: %s" % pck_path)
|
||||
else:
|
||||
push_warning("PCK not found: %s" % pck_path)
|
1
assetloader/AssetLoader.gd.uid
Normal file
1
assetloader/AssetLoader.gd.uid
Normal file
|
@ -0,0 +1 @@
|
|||
uid://qkh5uvr4st7i
|
77
assetloader/ModManifest.gd
Normal file
77
assetloader/ModManifest.gd
Normal file
|
@ -0,0 +1,77 @@
|
|||
extends RefCounted
|
||||
class_name ModManifest
|
||||
## Object for easier json manifest operations.
|
||||
|
||||
#region declaration
|
||||
var id: StringName ## Mod unique id. Internal usage only.
|
||||
var name: String ## Displayed mod name
|
||||
var version: String ## Displayed mod version
|
||||
var desc: String ## Displayed mod description
|
||||
var author:String ## Mod Author
|
||||
var dependencies := [] ## Mod dependecies, optional.
|
||||
var tags:= [] ## ["units", "vehicles", "buildings", "textures", "maps", "quests"]
|
||||
var packs := [] ## Optional. [br] By default [param mod_id.pck] is loaded.
|
||||
|
||||
var _validated = false ## true if the manifest as a correct syntax
|
||||
var path:StringName ## set if the mode is loaded
|
||||
#endregion
|
||||
|
||||
# === Init === # Initialization logic, if needed
|
||||
|
||||
|
||||
func _init(_id:String,_name:=_id,_desc:="",_dependencies:=[],_packs:=[],_author := "",_version:="1.0",_tags:=[]) -> void:
|
||||
if _id == "":
|
||||
push_warning("Mod id cannot be null, fallback to "+_name)
|
||||
self.id = StringName(_name.strip_edges())
|
||||
else:
|
||||
self.id = StringName(_id)
|
||||
self.name= _name
|
||||
self.version = _version
|
||||
self.desc=_desc
|
||||
self.author = _author
|
||||
self.dependencies = _dependencies
|
||||
self.tags= _tags
|
||||
self.packs =_packs
|
||||
|
||||
|
||||
# === PUBLIC FUNCTIONS ===
|
||||
func is_valid() -> bool:
|
||||
return _validated
|
||||
|
||||
|
||||
## Return [param null] if the json is not parsed correctly
|
||||
static func new_from_file(_manifest_file:FileAccess) -> ModManifest:
|
||||
var man_dic:Dictionary = JSON.parse_string(_manifest_file.get_as_text())
|
||||
if !man_dic:
|
||||
push_warning("Invalid manifest format: %s" % _manifest_file.get_path())
|
||||
return null
|
||||
var _id: String
|
||||
|
||||
_id = man_dic.get("id",_manifest_file.get_path().get_file().get_basename().strip_edges()) # ugly. But should fallback on a generated one.
|
||||
return ModManifest.new(
|
||||
_id,
|
||||
man_dic.get("name", _id),
|
||||
man_dic.get("desc", ""),
|
||||
man_dic.get("dependencies", []),
|
||||
man_dic.get("packs", []),
|
||||
man_dic.get("author", ""),
|
||||
str(man_dic.get("version", "1.0")),
|
||||
man_dic.get("tags", [])
|
||||
)
|
||||
|
||||
|
||||
## Check if the array is empty, if not, return the packs and the main one if it's not on the list.
|
||||
func get_mod_packs() -> Array[String]:
|
||||
if packs.size() > 0:
|
||||
# check if the main pack is in the list
|
||||
if packs.has(self.id):
|
||||
return self.packs
|
||||
else:
|
||||
var pck = self.packs
|
||||
pck.append(id)
|
||||
return pck
|
||||
return [id]
|
||||
|
||||
# === PRIVATE FUNCTIONS === # use _underscore() to make the difference between private and public functions
|
||||
|
||||
# ====================
|
1
assetloader/ModManifest.gd.uid
Normal file
1
assetloader/ModManifest.gd.uid
Normal file
|
@ -0,0 +1 @@
|
|||
uid://5ii0g6tl4dpj
|
24
autoloads/bootstrap.gd
Normal file
24
autoloads/bootstrap.gd
Normal file
|
@ -0,0 +1,24 @@
|
|||
extends Node
|
||||
## Should always be at the top of autoloads
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var loader := AssetLoader.new()
|
||||
# No point to load the game if no asset are loaded
|
||||
if loader.critical_error:
|
||||
Engine.get_main_loop().quit(1)
|
||||
return
|
||||
loader.load_all() # TODO: Making it async
|
||||
_start_game()
|
||||
loader = null
|
||||
|
||||
|
||||
## This will show a native MessageBox on Windows,
|
||||
## a native dialog on macOS, and GTK/QT dialog on Linux.
|
||||
func _show_error_popup(context:String, message: String) -> void:
|
||||
OS.alert(context+":"+message, context+":Error")
|
||||
|
||||
|
||||
func _start_game() -> void:
|
||||
print("Game starting...")
|
||||
#TODO: Load main menu or world scene
|
1
autoloads/bootstrap.gd.uid
Normal file
1
autoloads/bootstrap.gd.uid
Normal file
|
@ -0,0 +1 @@
|
|||
uid://bbmtxflx0ivgp
|
|
@ -16,7 +16,7 @@ func _process(_delta):
|
|||
#update_cursor(shape)
|
||||
|
||||
|
||||
func update_cursor(image: Resource, shape: Input.CursorShape = 0, hotspot: Vector2 = Vector2(0, 0)):
|
||||
func update_cursor(image: Resource, _shape:= 0 as Input.CursorShape, hotspot: Vector2 = Vector2(0, 0)):
|
||||
# Get the custom cursor data from the main script
|
||||
if image != null:
|
||||
texture_rect.texture = image
|
||||
|
|
23
editor/script_templates/Object/clean_code_template.gd
Normal file
23
editor/script_templates/Object/clean_code_template.gd
Normal file
|
@ -0,0 +1,23 @@
|
|||
# meta-name: Clean Code Template
|
||||
# meta-description: Use this format to structure your code into clear sections
|
||||
# meta-default: true
|
||||
# meta-space-indent: 4
|
||||
extends _BASE_
|
||||
|
||||
#region declaration
|
||||
# === CONST === # Constants and immutables, in UPPERCASE
|
||||
|
||||
# === STATIC === # Static variables/functions
|
||||
|
||||
#endregion
|
||||
|
||||
# === Init === # Initialization logic, if needed
|
||||
|
||||
func _init() -> void:
|
||||
pass
|
||||
|
||||
# === PUBLIC FUNCTIONS ===
|
||||
|
||||
# === PRIVATE FUNCTIONS === # use _underscore() to make the difference between private and public functions
|
||||
|
||||
# ====================
|
|
@ -0,0 +1 @@
|
|||
uid://d524ti2uyikh
|
25
mods/README.md
Normal file
25
mods/README.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Modding
|
||||
|
||||
By default a modding system is made.
|
||||
Each mods need to be in the mod folder, each folder is a mod containing at least a `mod_id.modinfo` and a `mod_id.pck` and optionnaly a `mod_id.<locale>.loc`
|
||||
|
||||
## modinfo example
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "mod_id",
|
||||
"name": "Mod Displayed Name",
|
||||
"version": "1.0",
|
||||
"desc": "Mod displayed description",
|
||||
"author": "Mod Author",
|
||||
"dependencies": [],
|
||||
"tags": ["units", "vehicles", "buildings", "textures", "maps", "quests"],
|
||||
"packs": ["mod_id", "addon_pack"]
|
||||
}
|
||||
```
|
||||
|
||||
*id* (string) : should be the unique name of the mod. Only used internally. should be the same as the modinfo and the folder name
|
||||
*name* (string) : the displayed name of the mode
|
||||
*version* (string) : to keep track of the version of you mod. displayed in the mod manager
|
||||
*author* (string) : you, the awesome modder
|
||||
*dependecies* (array of strings) : list of ids of requiered mod. be sure to load them before this one
|
9
mods/base_content/base_content.json
Normal file
9
mods/base_content/base_content.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "base_content",
|
||||
"name": "Core Game Elements",
|
||||
"version": 1.0,
|
||||
"desc": "Factions, unités, bâtiments et cartes de base.",
|
||||
"author": "TonStudio",
|
||||
"dependencies": [],
|
||||
"tags": ["units","vehicles","buildings","textures","maps","quests"]
|
||||
}
|
9
mods/base_content/base_content.modinfo
Normal file
9
mods/base_content/base_content.modinfo
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "base_content",
|
||||
"name": "Core Game Elements",
|
||||
"version": 1.0,
|
||||
"desc": "Factions, unités, bâtiments et cartes de base.",
|
||||
"author": "TonStudio",
|
||||
"dependencies": [],
|
||||
"tags": ["units","vehicles","buildings","textures","maps","quests"]
|
||||
}
|
0
mods/base_content/units/soldier.json
Normal file
0
mods/base_content/units/soldier.json
Normal file
10
mods/modinfo_example.json
Normal file
10
mods/modinfo_example.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "mod_id",
|
||||
"name": "Mod Displayed Name",
|
||||
"version": "1.0",
|
||||
"desc": "Mod displayed description",
|
||||
"author": "Mod Author",
|
||||
"dependencies": [],
|
||||
"tags": ["units", "vehicles", "buildings", "textures", "maps", "quests"],
|
||||
"packs": ["mod_id", "addon_pack"]
|
||||
}
|
|
@ -21,6 +21,7 @@ config/windows_native_icon="res://4589AD_icon.ico"
|
|||
|
||||
[autoload]
|
||||
|
||||
Bootstrap="*res://autoloads/bootstrap.gd"
|
||||
DebugTools="*res://dev/debug_tools.tscn"
|
||||
|
||||
[debug]
|
||||
|
@ -37,6 +38,11 @@ window/size/window_height_override=720
|
|||
mouse_cursor/custom_image="uid://dp4ed16rb1754"
|
||||
mouse_cursor/custom_image_hotspot=Vector2(14, 2)
|
||||
|
||||
[editor]
|
||||
|
||||
script/search_in_file_extensions=PackedStringArray("gd", "gdshader", "json", "modinfo")
|
||||
script/templates_search_path="res://editor/script_templates"
|
||||
|
||||
[editor_plugins]
|
||||
|
||||
enabled=PackedStringArray("res://addons/SimpleFormatOnSave/plugin.cfg", "res://addons/Todo_Manager/plugin.cfg")
|
||||
|
|
Loading…
Reference in a new issue