Writing Your Own Mod

From ΔV: Wiki
Revision as of 08:24, 17 March 2025 by Hev (talk | contribs) (initial edit. work is still needed)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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

func _ready():
	l("Readying")
	updateTL("i18n/en.txt","|")
	l("Ready")
# 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")

Config File

var modConfig = {} #Initializes the configuration variable for loadSettings.
func _init(modLoader = ModLoader):
	installScriptExtension("Settings.gd") # Modify Settings.gd first so we can load config and DLC
	loadSettings()
# This function is a helper to provide any file configurations to your mod
# You may want to replace any "Example" text with your own identifier to make it unique
# Check the example Settings.gd file for how to setup that side of it
func loadSettings():
	l(MOD_NAME + ": Loading mod settings")
	var settings = load("res://Settings.gd").new()
	settings.loadModMenuFromFile()
	settings.saveModMenuToFile()
	modConfig = settings.ModMenu
	l(MOD_NAME + ": Current settings: %s" % modConfig)
	settings.queue_free()
	l(MOD_NAME + ": Finished loading settings")

Mod Checking

# Func to scan the mods folder and return any files. Useful for checking dependancies. 
var modDependancy = []
func modsInstalled():
	var gameInstallDirectory = OS.get_executable_path().get_base_dir()
	if OS.get_name() == "OSX":
		gameInstallDirectory = gameInstallDirectory.get_base_dir().get_base_dir().get_base_dir()
	var modPathPrefix = gameInstallDirectory.plus_file("mods")
	l(MOD_NAME + ": Registering and verifying contents of the mods folder")
	var dir = Directory.new()
	dir.open(modPathPrefix)
	var dirName = dir.get_current_dir()
	dir.list_dir_begin(true)
	while true:
		var fileName = dir.get_next()
		dirName = dir.get_current_dir()
		l(fileName)
		if fileName == "":
			break
		if dir.current_is_dir():
			continue
		var modFSPath = modPathPrefix.plus_file(fileName)
		var modGlobalPath = ProjectSettings.globalize_path(modFSPath)
		if not ProjectSettings.load_resource_pack(modGlobalPath, true):
			l(MOD_NAME + ": %s failed to register." % fileName)
			continue
		var trueFileName = modFSPath.split("/")
		var trueFileNameLength = trueFileName.size()
		var getTrueFileName = trueFileName[trueFileNameLength - 1]
		modDependancy.append(getTrueFileName)
		l(MOD_NAME + ": %s registered." % fileName)
	l(MOD_NAME + ": Finished verification of mod folder.")