Writing Your Own Mod

From ΔV: Wiki

This guide covers the basics to getting started with writing a mod for ΔV. It is a modified version of Za'krin's modding guide, where a large portion of the credit will be given.

This guide is written for Windows users, however the equivalent for operating systems such as Linux and OS X should work extremely similarly.

Setup

This section will cover the resources used for modding. They are heavily encourage to be used, as it will make your life considerably easier with their use.

Downloaded Resources

First off, you will need a copy of ΔV: Rings of Saturn as a decompiled copy of the game will be used as a reference.

  • A demo build will work fine as well, as at the time of writing, it and the full release are identical.

Second, the Godot Game Engine which is used for the editing of mods.

  • ΔV is built off of Godot 3.5.3 at the time of writing, so we'll use the appropriate editor build.

Third, Godot Reverse Engineering Tools (a.k.a. GDRE), which is used to decompile the game.

  • 0.5.3 is the optimal version to use, as versions newer than this can take between 30 mins to several hours to load into the editor. I have no idea why this happens, it just does.

Extract and move these to somewhere accessible.

Decompiling

The process for decompiling ΔV is fairly straightforward when using GDRE.

  • Open the folder containing gdre_tools.exe in your file explorer.
  • Open the ΔV game folder (on steam it's Right-Click -> Manage -> Browse Local Files).
  • Copy Delta-V.pck (not .exe!) into the GDRE folder.
  • Open GDRE and select "Recover Project" from the "RE Tools" menu.
  • Select Delta-V.pck and wait for the file verification process to finish.
  • Leave the default settings, set the folder location, and extract the project.
  • Once the extraction process is finished, ΔV should be ready for import into Godot.

In the case where there have been problems using the GUI, the command line can be used instead. To use that instead of the GUI, follow the below steps:

  • Copy Delta-V.pck into the GDRE folder as before.
  • Open windows command prompt and cd into the folder (e.g. cd C:\godotTools\GDRE_tools)
  • Run gdre_tools.exe with the following command: gdre_tools --headless --recover=Delta-V.pck. This will create a new folder called containing the decompiled game files.
  • Move the decompiled files to wherever you want your project located.

For more detail on the usage of GDRE, see the usage section of their readme.

Importing into Godot

Loading the decompiled game files into Godot is rather simple.

  • Open your copy of Godot and select the 'Import' option.
  • Navigate to where you stored your decompiled version of ΔV and select the project.godot file.
  • Launch the Delta-V Project, and let Godot import all of the files.

The initial import process will take a couple of minutes at worst if everything is followed. In the case where it takes a significant amount of time, you may have used a newer version of GDRE. If it appears to freeze, don't worry, it is still processing, and can be used regardless. After the first import, future loading times usually will be significantly better, although it is always better to use GDRE 0.5.3 to ensure quick loading times.

Mod Structure

ΔV mods (and Godot projects in general) work in a hierarchical file structure centered around the project's root folder (referred to as res://). If any file isn't referenced relative to the executed code, it will be relative to res://. There are also other file system prefixes, such as user:// and the OS's file system paths, however these are very rarely used, especially for modding, so will only be covered briefly later down the line.

Mod Folder Structure

Mod zips will be mounted to the root as how they appear in the zip. For example, a zip with the structure of A_Mod.zip/ModFolder/ModMain.gd is the equivalent to having added a folder named ModFolder to the root folder and adding the ModMain.gd file in it. In terms of res://, it appears like res://ModFolder/ModMain.gd. Mods don't need to follow this structure explicitly, however it is extremely convenient when structured like this as it makes it easy to work with in the editor.

ModMain.gd

The ModMain.gd file is just about the most important file in the entirety of the mod. It is where the mod's resources are initially loaded and run. Even if only a single file is needed to be loaded to get the entire mod to run, it will not work if not referenced here.

It can be as simple or complex as you like, however the commonly utilized format works as follows:

extends Node

# Set mod priority if you want it to load before/after other mods
# Mods are loaded from lowest to highest priority, default is 0
const MOD_PRIORITY = 0

# Name and version of the mod, used for writing to the logs
const MOD_NAME = "Mod Name"
const MOD_VERSION = "1.0.0"

# Path of the mod folder, automatically generated on runtime
var modPath:String = get_script().resource_path.get_base_dir() + "/"
# Required var for the replaceScene() func to work
var _savedObjects := []

# Initialize the mod
# This function is executed before the majority of the game is loaded
# Only the Tool and Debug AutoLoads are available
# Script and scene replacements should be done here, before the originals are loaded
func _init(modLoader = ModLoader):
	l("Initializing DLC")
	loadDLC() # preloads DLC as things may break if this isn't done

# Do stuff on ready
# At this point all AutoLoads are available and the game is loaded
func _ready():
	l("Readying")
	l("Ready")


# Helper function to extend scripts
# Loads the script you pass, checks what script is extended, and overrides it
func installScriptExtension(path:String):
	var childPath:String = str(modPath + path)
	var childScript:Script = ResourceLoader.load(childPath)

	childScript.new()

	var parentScript:Script = childScript.get_base_script()
	var parentPath:String = parentScript.resource_path

	l("Installing script extension: %s <- %s" % [parentPath, childPath])

	childScript.take_over_path(parentPath)


# Helper function to replace scenes
# Can either be passed a single path, or two paths
# With a single path, it will replace the vanilla scene in the same relative position
func replaceScene(newPath:String, oldPath:String = ""):
	l("Updating scene: %s" % newPath)

	if oldPath.empty():
		oldPath = str("res://" + newPath)

	newPath = str(modPath + newPath)

	var scene := load(newPath)
	scene.take_over_path(oldPath)
	_savedObjects.append(scene)
	l("Finished updating: %s" % oldPath)


# Instances Settings.gd, loads DLC, then frees the script.
func loadDLC():
	l("Preloading DLC as workaround")
	var DLCLoader:Settings = preload("res://Settings.gd").new()
	DLCLoader.loadDLC()
	DLCLoader.queue_free()
	l("Finished loading DLC")


# Func to print messages to the logs
func l(msg:String, title:String = MOD_NAME, version:String = MOD_VERSION):
	Debug.l("[%s V%s]: %s" % [title, version, msg])

The additional functions provided with this script are not necessary, however are used by effectively every mod, and are left for convenience.

Runtime variables (variables used by the mod menu for the workings of other functions):

const MOD_PRIORITY = 0: The load priority of the mod. Zero by default, but can be changed to any integer. If a mod has the same priority, then it will go in alphabetical order of the zip files.

const MOD_NAME = "" AND const MOD_VERSION = "1.0.0": Mod name and versions. Directly used for logging purposes with the l() function later down the line, however is also used by the mod menu if you choose to install it (more on that later)

var modPath:String = get_script().resource_path.get_base_dir() + "/": used to get the ModMain's file path. Used for other functions so is best leaving alone.

var _savedObjects := []: variable used by the replaceScene() function. This should be left blank.

System functions (static functions that should be where the body of your ModMain are):

func _init(modLoader = ModLoader):: Any functions loaded in this function are done as soon as the script is run. The modloader variable is used to load various functions from the modloader as autoloads are not available yet.

loadDLC() should be loaded as early as possible in the _init() function. Ensures DLC loads properly as it breaks when mods are loaded for whatever reason

func _ready():: Functions loaded here will be loaded once everything else has loaded and all autoloads are available.

Load Functions (functions used to load resources from your mod):

replaceScene

replaceScene() overrides or replaces a vanilla scene.

  • newPath:String is the path to your modded scene, relative to the mod folder.
  • oldPath:String is an optional path to the vanilla scene you are overriding, if no path is provided, the same path as your new scene will be used, but relative to res:// instead.
  • Replacing scenes requires loading the scene, which loads any resources present in the scene. Make sure any resources present (e.g. scripts) are modified before the scene is replaced.

Examples:

replaceScene("ships/RA-TRTL.tscn") loads a scene from your mod at res://ModFolder/ships/RA-TRTL.tscn that modifies the K37 ship scene at the vanilla location res://ships/RA-TRTL.tscn.

replaceScene("modifications/ships/RA-TRTL.tscn","res://ships/RA-TRTL.tscn") loads a scene in your mod's folder at res://ModFolder/modifications/ships/RA-TRTL.tscn which modifies the K37 scene at the vanilla location res://ships/RA-TRTL.tscn.

installScriptExtension

installScriptExtension() is used to override vanilla script behavior.

  • path:String is the path to the new script, relative to the root of the mod folder.
  • The inherited script gets overridden, whenever it is used, your modified script will instead be applied.
  • Inheritance is very powerful, and can be used to override most vanilla behavior, with some exceptions.
  • Scripts must be extended before they are loaded. Once applied to a node, the script is locked in and modifying the script resource will not affect existing instances.
  • It can be used to load new scripts, however it is best used to modify preexisting scripts from the vanilla game.

Example:

installScriptExtension("ships/Shipyard.gd") loads a script from res://ModFolder/ships/Shipyard.gd

Add Translations

Translations are a useful part of any mod that adds something visually. They can be useful both for providing localized resources for anyone willing to translate, as well as giving names to any system names that couldn't be written as plain text (such as ship names). Translations are best handled through the updateTL() function, with the following function itself being placed somewhere within the ModMain file.:

# Helper script to load translations using csv format
# `path` is the path to the transalation file
# `delim` is the symbol used to seperate the values
# example usage: updateTL("i18n/translation.txt", "|")
func updateTL(path:String, delim:String = ","):
	path = str(modPath + path)
	l("Adding translations from: %s" % path)
	var tlFile:File = File.new()
	tlFile.open(path, File.READ)

	var translations := []

	var csvLine := tlFile.get_line().split(delim)
	l("Adding translations as: %s" % csvLine)
	for i in range(1, csvLine.size()):
		var translationObject := Translation.new()
		translationObject.locale = csvLine[i]
		translations.append(translationObject)

	while not tlFile.eof_reached():
		csvLine = tlFile.get_csv_line(delim)

		if csvLine.size() > 1:
			var translationID := csvLine[0]
			for i in range(1, csvLine.size()):
				translations[i - 1].add_message(translationID, csvLine[i].c_unescape())
			l("Added translation: %s" % csvLine)

	tlFile.close()

	for translationObject in translations:
		TranslationServer.add_translation(translationObject)

	l("Translations Updated")

Syntax for the command is handled as updateTL(path, delim) where path is the path relative to the ModMain, and delim is the character or string which the translation identifier and text are split with. | is commonly used as it is used very rarely.

Example:

updateTL("i18n/en.txt","|")

this loads a translation file at res://ModFolder/i18n/en.txt and used the | character as a delimiter.

Formatting for each translation file goes as follows:

locale<delim character><localization code>

LOCALIZATION_STRING<delim character>This is a translated string

<delim character> is whatever character is used as the delimination character in the updateTL() call.

locale|<localization code> is the identifier for the language to translate to. The translations currently supported by the game are as follows:

  • cs_CZ - Czech (Czech Republic)
  • de - German
  • el_GR - Greek (Greece)
  • en - English
  • es - Spanish
  • fr - French
  • hu_HU - Hungarian (Hungary)
  • it_IT - Italian (Italy)
  • ja - Japanese
  • ko_KR - Korean (South Korea)
  • nb_NO - Norwegian Bokmål (Norway)
  • nl_NL - Dutch (Netherlands)
  • pl - Polish
  • pt_BR - Portugese (Brazil)
  • ru_RU - Russian (Russia)
  • th - Thai
  • uk_UA - Ukrainian (Ukraine)
  • zh_CN - Chinese (China)
  • zh_HK - Chinese (Hong Kong)

Config File

Config files are useful for making variables or settings configurable to the user. Given the nature of it, it does require a bit more setup and a lot of extra work outside of the ModMain, which will be deferred to it's own wiki page once it is written. In the meantime, you may benefit at looking at mods that use it for reference.

Mod.manifest File

The mod.manifest is a more recent addition to the modding scope. It's not a required file by any means, however it's an adopted standard to provide additional information to any mods looking to take info from said mod. As many or as few of these can be filled out, and blank sections can be left as "". Current mods that use this may or may not support the removal of lines, so it is best to leave them in anyway.

An example of the file's structure is as follows:

[package]

id="test.ExampleMod"
name="Example Mod"
version="1.0.0"
description="A example description"
group="testing"
github_homepage="https://github.com/example/repo/"
github_releases="https://github.com/example/repo/releases/"
discord_thread="https://discord.gg/dv/"
nexus_page="https://www.nexusmods.com/dvringsofsaturn/mods/"
donations_page="https://example.url"
wiki_page="https://example.url"
custom_link="https://example.url"
custom_link_name="Example url for the custom link"

[package] is the identifer for the mod manifest, and should be left alone as such

id The unique identifier of the mod. Useful for differentiation with other mods that may use the same name. The common formatting is (nick)name.ModName, but it can be whatever you like.

name The mod's display name. Used for any mods that may need to fetch other mods. Similar to the MOD_NAME constant in the ModMain file, however is preferred for use over that name.

version The mod's version number. Can be any string, however is best kept numerical for version comparing. Similar to the MOD_VERSION constant in the ModMain file, however is preferred for use over that version identifier.

description A string used to describe the mod. Can be either plain text or a translation string.

group Used to group mods together. It's not currently used by any known mods, but useful to have for future reference.

github_homepage The Github URL for the mod.

github_releases The Github releases page, if the mod has one. Same as the github_homepage string, just with releases/ appended to the end.

discord_thread URL for an associated discord thread, channel or server. Current uses link to the #workshop channel in the ΔV Discord.

nexus_page URL for the Nexusmods page of the mod.

donations_page URL for a donations page if the writer chooses to add one.

wiki_page URL for any associated wiki page, be it ΔV Wiki, Github wiki, or another page.

custom_link A custom URL for an action button. Can be whatever you choose.

custom_link_name A string used for any text tooltips/labels used with the custom link. Can be either plain text or a translation string.