Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Self installation #33

Closed
wants to merge 12 commits into from
65 changes: 13 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,24 @@

A general purpose mod-loader for GDScript-based Godot Games.

See the [Wiki](https://github.com/GodotModding/godot-mod-loader/wiki) for additional details, including [Helper Methods](https://github.com/GodotModding/godot-mod-loader/wiki/Helper-Methods) and [CLI Args](https://github.com/GodotModding/godot-mod-loader/wiki/CLI-Args).
## Quick Loader Setup

If the game you want to mod does not natively use this ModLoader, you will have to complete two steps to set it up:
1. Place the `/addons` folder from the ModLoader next to the executable of the game you want to mod.
2. set this flag `--script addons/mod_loader_tools/mod_loader_setup_helper.gd`
- Steam: right-click the game in the game list > press *properties* > enter in *startup options*
- Godot: Project Settings > press *Editor* > enter in *Main Run Args*
- Other: Search for "set launch (or command line) parameters [your platform]"

## Mod Setup
If the game window shows `(Modded)` in the title, setup was successful.

For more info, see the [docs for Delta-V Modding](https://gitlab.com/Delta-V-Modding/Mods/-/blob/main/MODDING.md), upon which ModLoader is based. The docs there cover mod setup in much greater detail.

### Structure

Mod ZIPs should have the structure shown below. The name of the ZIP is arbitrary.

```
yourmod.zip
├───.import
└───mods-unpacked
└───Author-ModName
├───mod_main.gd
└───manifest.json
```

#### Notes on .import

Adding the .import directory is only needed when your mod adds content such as PNGs and sound files. In these cases, your mod's .import folder should **only** include your custom assets, and should not include any vanilla files.
See the [Wiki](https://github.com/GodotModding/godot-mod-loader/wiki) for more details.

You can copy your custom assets from your project's .import directory. They can be easily identified by sorting by date. To clean up unused files, it's helpful to delete everything in .import that's not vanilla, then run the game again, which will re-create only the files that are actually used.
## Modding


### Required Files

Mods you create must have the following 2 files:

- **mod_main.gd** - The init file for your mod.
- **manifest.json** - Meta data for your mod (see below).

#### Example manifest.json

```json
{
"name": "ModName",
"version": "1.0.0",
"description": "Mod description goes here",
"website_url": "https://github.com/example/repo",
"dependencies": [
"Add IDs of other mods here, if your mod needs them to work"
],
"extra": {
"godot": {
"id": "AuthorName-ModName",
"incompatibilities": [
"Add IDs of other mods here, if your mod conflicts with them"
],
"authors": ["AuthorName"],
"compatible_game_version": ["0.6.1.6"],
}
}
}
```
Use these [Helper Methods](https://github.com/GodotModding/godot-mod-loader/wiki/Helper-Methods) and
[CLI Args](https://github.com/GodotModding/godot-mod-loader/wiki/CLI-Args).
For more info, see the [docs for Delta-V Modding](https://gitlab.com/Delta-V-Modding/Mods/-/blob/main/MODDING.md), upon which ModLoader is based. The docs there cover mod setup in much greater detail.

## Credits

Expand Down
100 changes: 100 additions & 0 deletions addons/mod_loader_tools/mod_details.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
extends Resource
class_name ModDetails

var name := ""
var version_number := "v0.0.0"
var description := ""
var website_url := ""
var dependencies := [] # Array[String]

var id := ""
var authors := [] # Array[String]
var compatible_game_version := [] # Array[String]
var tags := [] # Array[String]
var description_rich := ""
var incompatibilities := [] # Array[String]
var image: StreamTexture

ithinkandicode marked this conversation as resolved.
Show resolved Hide resolved

func _init(meta_data: Dictionary) -> void:
if not dict_has_fields(meta_data, ModLoaderHelper.REQUIRED_MANIFEST_KEYS_ROOT):
return

var godot_details: Dictionary = meta_data.extra.godot
if not godot_details:
assert(false, "Extra details for Godot are missing")
return

if not dict_has_fields(godot_details, ModLoaderHelper.REQUIRED_MANIFEST_KEYS_EXTRA):
return

id = godot_details.id
if not is_id_valid(id):
return

if not is_semver(meta_data.version_number):
assert(false, "Version \"%s\" does not follow semantic versioning! see: README.md" % meta_data.version_number)

name = meta_data.name
version_number = meta_data.version_number
description = meta_data.description
website_url = meta_data.website_url
dependencies = meta_data.dependencies

if godot_details.has("authors"):
authors = godot_details.authors
if godot_details.has("incompatibilities"):
incompatibilities = godot_details.incompatibilities
if godot_details.has("compatible_game_version"):
compatible_game_version = godot_details.compatible_game_version

if godot_details.has("description_rich"):
description_rich = godot_details.description_rich
if godot_details.has("tags"):
tags = godot_details.tags

# todo load file named icon.png when loading mods and use here
# image StreamTexture


func is_id_valid(id: String) -> bool:
if id == "":
assert(false, "Mod ID is empty")
return false

if false:
assert(false, "Mod ID \"%s\" is not a valid ID" % id)

# todo: validate id format
return true


func is_alphanumeric(string: String) -> bool:
# todo: implement

# Returns true if this string is a valid identifier. A valid identifier may contain only letters, digits and underscores (_) and the first character may not be a digit.
return string.is_valid_identifier()


func is_semver(version_number: String) -> bool:
# todo implement
return true


func dict_has_fields(dict: Dictionary, required_fields: Array) -> bool:
var missing_fields := required_fields

for key in dict.keys():
if(required_fields.has(key)):
missing_fields.erase(key)

if missing_fields.size() > 0:
assert(false, "Mod data is missing required fields: " + str(missing_fields))
return false

return true


#func _to_json() -> String:
# return ""

41 changes: 12 additions & 29 deletions loader/mod_loader.gd → addons/mod_loader_tools/mod_loader.gd
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# in 2022 by Vladimir Panteleev <git@cy.md>
# in 2023 by KANA <kai@kana.jetzt>
# in 2023 by Darkly77
# in 2023 by Qubus0 / Ste
#
# To the extent possible under law, the author(s) have
# dedicated all copyright and related and neighboring
Expand Down Expand Up @@ -48,26 +49,6 @@ const UNPACKED_DIR = "res://mods-unpacked/"
# manifest.json = Meta data for the mod, including its dependancies
const REQUIRED_MOD_FILES = ["mod_main.gd", "manifest.json"]

# 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 = [
"id",
"incompatibilities",
"authors",
"compatible_mod_loader_version",
"compatible_game_version",
]

# Set to true to require using "--enable-mods" to enable them
const REQUIRE_CMD_LINE = false

Expand Down Expand Up @@ -119,7 +100,7 @@ func _init():
# Loop over "res://mods" and add any mod zips to the unpacked virtual
# directory (UNPACKED_DIR)
_load_mod_zips()
mod_log("DONE: Unziped all Mods", LOG_NAME)
mod_log("DONE: Loaded all mod files", LOG_NAME)
Copy link
Collaborator

@ithinkandicode ithinkandicode Jan 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good intention, but the original wording might be better for reading logs (although there's a typo and unneeded capitalisation) -- as "loaded" suggests that their content has been loaded, whereas in this case all that's happened is that mods have been added to the virtual file system. For us developers, we know what "loaded" means in this context, I'm just thinking about end users.

Perhaps "Loaded all mod files into the virtual filesystem"?


# Loop over UNPACKED_DIR. This triggers _init_mod_data for each mod
# directory, which adds their data to mod_data.
Expand Down Expand Up @@ -151,6 +132,8 @@ func _init():
continue
_check_dependencies(mod_id, mod_data[mod_id].meta_data.dependencies)


func init_mods():
# Sort mod_load_order by the importance score of the mod
_get_load_order()

Expand Down Expand Up @@ -183,7 +166,7 @@ func mod_log(text:String, mod_name:String = "Unknown-Mod", pretty:bool = false)-
# Prefix with "{mod_name}: "
var prefix = mod_name + ": "

var date_time = Time.get_datetime_dict_from_system()
var date_time = OS.get_datetime() #Time.get_datetime_dict_from_system()

# Add leading zeroes if needed
var hour := (date_time.hour as String).pad_zeros(2)
Expand All @@ -203,7 +186,7 @@ func mod_log(text:String, mod_name:String = "Unknown-Mod", pretty:bool = false)-

var _error = log_file.open(MOD_LOG_PATH, File.READ_WRITE)
if _error:
print(_error)
printerr(_error)
return
log_file.seek_end()
if pretty:
Expand Down Expand Up @@ -393,17 +376,17 @@ func _load_meta_data(mod_id):

# Ensure manifest.json has all required keys
func _check_meta_file(meta_data):
var missing_keys_root = REQUIRED_MANIFEST_KEYS_ROOT.duplicate()
var missing_keys_extra = REQUIRED_MANIFEST_KEYS_EXTRA.duplicate()
var missing_keys_root = ModLoaderHelper.REQUIRED_MANIFEST_KEYS_ROOT.duplicate()
var missing_keys_extra = ModLoaderHelper.REQUIRED_MANIFEST_KEYS_EXTRA.duplicate()

for key in meta_data:
if(REQUIRED_MANIFEST_KEYS_ROOT.has(key)):
if(ModLoaderHelper.REQUIRED_MANIFEST_KEYS_ROOT.has(key)):
# remove the entry from missing fields if it is there
missing_keys_root.erase(key)

if meta_data.has("extra") && meta_data.extra.has("godot"):
for godot_key in meta_data.extra.godot:
if(REQUIRED_MANIFEST_KEYS_EXTRA.has(godot_key)):
if(ModLoaderHelper.REQUIRED_MANIFEST_KEYS_EXTRA.has(godot_key)):
missing_keys_extra.erase(godot_key)

# Combine both arrays, and reformat the "extra" keys
Expand Down Expand Up @@ -462,7 +445,7 @@ func _get_load_order():

# Add loadable mods to the mod load order array
for mod in mod_data_array:
if(mod.is_loadable):
if mod.is_loadable and ModLoaderHelper.is_mod_enabled(mod.meta_data.extra.godot.id):
mod_load_order.append(mod)

# Sort mods by the importance value
Expand Down Expand Up @@ -525,7 +508,7 @@ func _get_cmd_line_arg(argument) -> String:
func _get_local_folder_dir(subfolder:String = ""):
var game_install_directory = OS.get_executable_path().get_base_dir()

if OS.get_name() == "OSX":
if OS.has_feature("OSX"):
game_install_directory = game_install_directory.get_base_dir().get_base_dir()

# Fix for running the game through the Godot editor (as the EXE path would be
Expand Down
Loading