Skip to content

Lua Integration

Nefaro edited this page Aug 31, 2023 · 12 revisions

Lua Integration for Gnoll 1.14.0

Introduction

The current state of Gnomoria modding is more or less vanilla Gnomoria .xml or Gnoll. I haven't seen anything else for a long time now. Vanilla .xml is the official way. It's a static way of modding the game, you can only define the data and once it's loaded into the game, that's it. No more changes.

Gnoll on the other hand works on the source level, giving you pretty much the maximum level of flexibility to do what you want. But, this requires quite a bit of time investment, to learn C#, to get Gnoll and the ModLoader working, to figure out how you can hook into Gnomoria and what you can and cannot change etc. Additionally, every change needs to be compiled, copied into the right location and requires a game restart.

Wouldn't it be wonderful if you could just change things and they appear in-game ? Yes it would ...

Lua Integration is far from the perfect solution, but it gives much needed development flexibility and simplicity while also providing ways to make modding more dynamic and functional. And, you can refresh your Lua script in-game, no need to restart the game.

Moonsharp

The Lua Integration is provided by Moonsharp ( GitHub ).

In short, it's a "A Lua interpreter written entirely in C# for the .NET, Mono and Unity platforms.". Sounds good enough.

Sandboxing

One of the reasons I picked Moonsharp is because it provides an easy way to configure and sandbox the runtime. You can inspect the configuration here: LuaManager.cs In short:

private readonly static CoreModules DEFAULT_CORE_MODULES = CoreModules.Preset_HardSandbox | CoreModules.Json | CoreModules.ErrorHandling | CoreModules.Coroutine | CoreModules.OS_Time | CoreModules.LoadMethods | CoreModules.Metatables;

The official documentation has more info about what that means

Enabling/Disabling Lua Integration

By default Lua Integration is disabled. To enable it I made a LuaSupport mod. Enabling this mod (in-game) also enables the Lua subsystem. Additionally, LuaSupport contains helpful Lua definitions/enums and a script that helps to validate if the object mappings are in place and configured, among other things.

Lifecycle

The Lua scripts are loaded and the init script is run as early as possible, which means when Gnomoria main menu is loaded. This allows the ModLoader to manage the Mods via in-game UI and maybe in future allows Lua scripts to hook into the UI elements themselves.

Note however this also means, when the init script is executed, not everything is available, namely the game world does not exist.

When the Gnoll Mod (or LuaSupport) is disabled, then the init script is removed from Lua Support, but everything that has run is still present. As such fully disabling Lua based mods or LuaSupport needs a game restart

Current capabilities

What you currently can do is to modify the game definitions similar to the capabilities of the .xml modding. There is one bigger restriction, I didn't expose any functionality related to sprites and graphics. As such, you cannot override sprite configs and you cannot call out UI elements.

On the other hand, you can dynamically changes values and names etc. I included a small Lua mod, located in ExpLuaIntegration that randomizes the values of "wood" material on every new game. But the mod could as easily randomize the values during every new day or night even.

Bootstrapping

When talking about developing a new Lua mod, there are 2 ways to bootstrap the Lua code:

  • By creating a new Gnoll Mod - This is the way that ExpLuaIntegration works
  • By using a custom init script - I included functionality to run Lua scripts outside of Gnoll installation

Those 2 methods allow to run mods that are included with Gnoll while also be able to run mods, that are NOT related to Gnoll and that Gnoll does not override with new installations. More info below.

Via Gnoll Mods

For Gnoll to understand that a mod requires Lua support, the mod main class needs to be decorated with the IHasLuaScripts marker interface. This allows the mod loader to register given mod for Lua integration.

After that, the mod loader looks for ModInit.lua script file under the "Scripts" folder. This is considered the init script and as such it should contain all the maintenance and bookkeeping needed for the everything Lua related to work.

Remember the game world does not yet exist when the init script is run, since it is executed much earlier, before the world is created.

Another thing to note is that each Gnoll Mod Lua scripts are kept separated. As such there might be several init scripts that start and for different events there might be several handlers. Currently there is more-or-less no defined order in which they run, but internally all scripts are run in the registration order, which is currently alphabetical according to the Mod .dll filename.

Via Custom Init script

The other way to start a Lua mod is by a custom init script. This is a built-in way to bypass the need for a Gnoll Mod to run. The Lua subsystem checks if there is a file at the Gnomoria userdata location at "C:\Users<username>\Documents\My Games\Gnomoria\Gnoll\Scripts\CustomInit.lua" and executes it. As such, to get started, it's enough to enable LuaSupport and create the file mentioned. Again, this is considered a init script and as such it should contain all the bookkeeping, imports, etc, everything needed for the script to work.

Again, remember the game world does not yet exist when the init script is run, since it is executed much earlier, before the world is created.

Script Search Path

For imports (or requires in Lua) there is a search path. First any required script is searched on the mod Scripts folder, if not found then "LuaSupport/Scripts" folder is searched. Basically those 2 locations and their sub-folders are on search path and first accordingly named script file is loaded.

For custom init script, the userdata location ("C:\Users<username>\Documents\My Games\Gnomoria\Gnoll\Scripts") is used as the first search path.

Enabling Debug for Lua Buttons

For easier work with Lua files, I suggest enabling Gnomoria debug mode. This allows to the Lua subsystem to insert 2 Lua debug buttons into the UI. From those buttons "Lua Reload" is the more helpful one. This one reloads, from the filesystem, all the init scripts and re-runs them, allowing the developer a hot-reload Lua scripts.

The other button runs a built-in validation script

Note however, if your Lua script has overwritten some definition data, ie renamed "wood" into "lumber" then those changes are still present after Lua reload.

To enable Gnomoria debug mode, find the Gnomoria settings file (C:\Users<username>\Documents\My Games\Gnomoria\settings.ini) and replace (or add) under [Gameplay] the line: Debug = True

API

Architecture

The general architecture is event based. The idea is to define and hook up as many events as possible and let the scripts think of new ways to interact with the world. Currently there are only a handful events defined but hopefully this list will expand in the future.

I do not want to give a direct access to the hot "update" main loop since running Lua is not free and as such might kill the FPS counter. But this might change in the future.

How to Read C# -> Lua mapping

Each object exposed to Lua code has a corresponding proxy object. As an example, for Gnomoria definitions the main entry is "GameDefs" and the appropriate proxy object is GameDefsProxy. The proxy object has exposed most properties, with some exceptions, of the original object. The properties map 1:1 to from C# code to Lua.

From our example, "GameDefsProxy" has "AmmoDefs" property. Looking at GnollLuaValidationScript.lua line 19, the appropriate Lua code is tbl = gameDefs.AmmoDefs;

Global Gnomoria Table

In Lua context there Gnomoria global table defined, named _GNOMORIA. It exposes some few useful properties that can be accessed at any time. Currently there are only 3 properties defined, you can take a look at them here: GnomoriaGlobalTable.cs. From those "getGameDefs()" is used the most in the example Lua scripts.

In future there will be more properties, things that might be useful at any given moment.

Supported Events

Currently, there are only a handful events, but future should bring more.

Note: Most of the events (if not all) have an example included in the ExpLuaIntegration mod.

  • OnNewGameStarted - Called once when a new game has started. This is after the definitions have been loaded and the world is generated, but before the UI is drawn.
  • OnSeasonChange( season ) - On the start of the day when the season has change (spring -> summer -> fall -> winter -> spring etc). Argument "season" holds the season enum (also defined in Season.lua, )
  • OnNightStart- Called when the night starts, after sundown.
  • OnDayStart- Called when the new day starts, after sunrise.
  • OnDayChange- Called slightly before OnDayStart
  • OnGameSave( saver ) - Called when saving is in process (either manual or auto). Argument "saver" is defined here, has a single method called "save" and expects a C# dictionary or a Lua table (that can be converted to a dictionary object).
  • OnSaveGameLoaded( loader ) - Called when the game has loaded, but before the UI shown and control is given to the palyer. The "loader" object is described here, includes only one method "load()" which returns the same object that was given to the "saver".

Planned Future Features

This all is the first pass of the integration. There are plans do expand the Lua support and make more of the in-game structures available.

More Mapped Events and

Currently only the manipulation of the game definitions is available, but the plan is to give access to the entities as well. For example, OnEntitySpawn (or equivalent) which would allow us to add stuff like "skill based extra drops" etc

Access to the Entities

Also access to all in-game entities ie the gnomes, mants, workshops, well, everything, really.

Maybe ...

However, there is currently no roadmap nor are there any promises. Everything might happen if and when there is time. On the other hand, nothing might happen as well...