Mod suppport!!

Lancer Tactics now has official mod support & a source-available modkit! For players, mods can now be downloaded through the Omninet browser alongside maps and modules, and then managed in a new Omninet tab:

alt text

There’s a lot to talk about here so it seems like a natural focus of this month’s update.

Background — how do Godot mods work?

Godot projects have an internal filesystem much like the one on your computer; a series of folders and files. The root is referred to as “res://”

alt text

However, to run the game Godot needs assets (images, music, etc) to be in a different format than you’d find with them sitting in your filesystem. It compiles these files into a big pile of files that look like this, all jumbled together in one folder:

alt text

To keep knowing where things are, it maintains a skeleton of the original filesystem with all the orignal files swapped out with “.remap” placeholders that just point to where in that big pile the asset they represent went. Furthermore, whenever the game attempts to load a specific asset at a location (e.g. “res://assets/icons/mech.svg”), it caches whatever it loaded into memory. Whenever we ask it to load that asset again, it doesn’t have to go ask the .remap file where the original one went and load it up — it just returns whatever it ended up loading from there last time. This cache is like a ghost sitting on top of a skeleton.

For modding, this gives us an opportunity. Instead of needing to actually copy in modded files and override the originals, we can manipulate this ghost-filesystem-cache by lying about what files are at what locations. Here’s what a mod looks like for Lancer Tactics (using a modified system from Godot Mod Loader):

alt text

We have a special folder where we put the actual mod files: “unpacked”, then another folder with the specific mod’s identifier “Wickworks-Sawhorse”. Within that, we have a “res” folder, and that’s where the lying starts. When loading up the mod, we can grab everything inside of the mod’s “res” and tell the ghost-cache that the mod’s files are the ones that are actually at that location. In this example, by the end of it when we ask Godot to load “res://assets/frames/horus/mf_sawhorse.png”, it will return the file that’s actually over in “res://unpacked/Wickworks-Sawhorse/res/assets/frames/horus/mf_sawhorse.png”.

Now that we have the power to insert files as we’d like, Godot Mod Loader additionally has some functions that through dark magic allows multiple mods to all alter the same script instead of overriding it. I’m a little fuzzy on these details, but the shape of it is that while loading each successive script that extends the same file actually extends the previous one that also extended that script so all the changes end up getting applied.

(though there is a bug in the Godot engine that’s currently preventing doing this dark magic to any file that has a class_name; a pretty friggin major restriction!! If a Godot engine dev is reading this, a fix would be much appreciated thank you!)

In order to change what mods are active, the out-of-the-box Godot Mod Loader addon expects you to restart the game so it can start work with a clean slate each time. For Lancer Tactics, I modified this so it tracks all files inserted so we can go and undo all of our lies and reset the ghost-cache to files’ original locations. This was important because I’m expecting people to want to be able to make maps and modules with bundled-in mods that substantially alter the content of the game, and requiring a restart whenever you load one up would be a terrible experience.

Speaking of bundling mods with other content…

Security Concerns

Mods are code that some rando wrote that the game downloads and then runs on your machine. Godot does not have built-in sandboxing for gdscript, so this code has just as much access as the game does, ie, can read or write any file on your computer and talk to the internet. Uh oh!

To be fair, this is a issue with most mods for most games. It’s a very trust-based at-your-own-risk sort of country. This is fine if mods aren’t tightly integrated, but as soon as you as a developer start removing barriers and letting people accidentally stumble into downloading harmful code by just pressing buttons in-game, you start taking on the responsibility in the case of something bad happening.

Slay The Spire 2, another game made in Godot, handles this by putting a big warning screen that it forces you to click through before you can start installing mods. We’ve done something similar:

alt text

Additionally, LT’s system distinguishes between three different types of mods:

  • Asset Pack: There’s no security concern if there’s no scripts. Mods that consist of only inert files (images, sound, json) will be allowed to be bundled with maps and modules and loaded without requiring a confirmation from the user. Assets are placed entirely implicitly by their location in the mod’s “res” subfolder. Good for things like additional portrait options or alternate mech tokens.
  • Content Pack: These can have scripts, and will likely only swap in files implicitly like asset packs, but do have the power to control where their files go if they have special needs, but are responsible for unspooling any nonstandard changes they make on being deactivated.
  • Substantial Mod: as above, but the changes are not cleanly reversible so require a game restart to turn off or on. Mods are in charge of telling LT if they’re a content pack or substantial; the security concern is equivalent.

Given an extra four months, I could probably add the option to script game content in a nice sandboxed environment by setting up an internal .lua API for new abilities to use. This would allow new mechs and whatnot to count as asset packs, but the time-benefit tradeoff isn’t favorable given our time and labor constraints. I have it filed away as possible avenue to explore after we make a million dollars on Steam and the security concerns become less theoretical.

Mod Tooling

So! That’s a lot of preamble. How does one actually make a mod?

First, you need to get your hands on the source of the game. It’s possible to decompile any game made in Godot which is what bootleg (affectionate) modders have been doing up til now, but we’re making the source available through official channels for convenience. Lancer’s TTRPG creator ecosystem is based on a belief that it should be hackable & extensible, and we want to carry that culture forward.

We’re talking to a lawyer to make sure we have all our hatches buttoned down legally to make sure we maintain ownership of the game code itself (and maybe like watermark some assets?), but once we get that squared away we’ll add the modkit alongside the rest of the downloads on itch. Until then, you can request early access to it on LT’s Discord server.

Once you get the modkit, there’s a modding_guide.txt that should give you the information you need to start making stuff:

alt text

And make the mods in the godot editor itself; the same view I have when making the game:

alt text

I’d love to establish way to start accumulating community knowledge about modding the game that’s not controlled by google/discord. Getting a wiki spun up seems like the obvious choice — it’s on my to-do list.

I’m a bit nervous opening the floodgates for modding because it’s a significant increase in “surface area” that I’ll have to maintain. Mods can cause crashes I don’t have control over, changes I make will break mods that modders don’t have control over, the tooling is another editor to make sure correctly syncs with data, and runtime juggling of all these files is delicate and impossible to set up automated testing for.

But we gotta pull the trigger sometime, so might as well be now to give us a long runway before Steam release to work out the kinks with the whole thing.

The rest of the changelog

Characters

  • Fix pilot names with ABCDEF letters getting interpreted as HTML color codes and not loading properly
  • if we set barks for NHPs to unknown, mark it as intentional and don’t reset NHPs to their default on next full repair
  • fix some scylla barks missing unicode characters
  • cycling an NHP restores their Temperment (aka barks) to default
  • Don’t do gloating pilot barks unless the target is a character (no more “it’s nothing personal kid…” when the “kid” in question is a cardboard box)
  • stock NHP portraits + names automatically switch to newly-equipped ones if no changes were made

Mechanics

  • Wrecks are no longer able to swim
  • Flash lock only triggers for hostile sources of movement, and correctly blocks grapple movement.
  • Jericho cover asks to confirm before picking up the pieces, and extracts them instead of destroying them.
  • can call supply drops even when you yourself are not missing structure or limited charges (4 ur friends)
  • diluvian ark now skips activating if you/your ally are already invisible
  • Everest Initiative text says it gives you a free overcharge action instead of the free action thing.
  • Fixed manticore CP not charging up + added basic electricity fx for it
  • fix athena not triggering prophetic scanners
  • Run on-move triggers when flying is gained or lost so you can’t jump onto a Perimeter Command Plate to avoid its effect
  • fix: duskwing 1 neurospike mirage should let me make myself invisible to the target
  • fix: sherman’s solidcore charges should not reset when spending repair cap
  • fix: grounding charges’ mine should be a hull instead of agility save
  • can no longer circumvent ordnance restrictions by firing a weapon with UNCLE
  • shock wreath triggers post-attack-declared to give the damage time to kick in first (and potentially burn to set up a blind)
  • Lotus Projector now correctly works against “counts as invisible effects” like Witch’s Blur
  • Mastermind no longer can blind multiple targets by repeatedly cancelling the movement and taking the re-triggered flashbang.
  • fix missing spend_action for scorpion
  • fix not being able to equip Technophile III NHP if you’re already at the AI limit
  • flight systems also grant EVA, and you can no longer start flying if you’re already in zero G.
  • Fixed bug where zero G didn’t allow you to move up over obstacles like you should be able to with flight movement.
  • can no longer simply abort Hive’s drone barrage choice
  • Breach Ram can’t knock back units who are immune to knockback
  • Shifted reaction priority of metafold maze so puppet systems + nailgun + maze combo works
  • NPC kit randomizers are no longer unique
  • “Fixed survival sitrep” <– hey past olive, you gonna give me any more details about what this was, specifically? No?

UI / Graphics

  • Changed in-game colors to match the exact ones designed by Rob Sather, and got a license to actually use said logos.
  • Used the new Godot 4.7 control offset features to sprinkle UI animation throughout the game: alt text

alt text

  • Added animation for health bars under units when they get damaged or heated.
  • Bespoke modal for saving and loading games that cracks the json file open and gives some information about what’s going on inside that save.

alt text

  • Added console commands to manually see and edit module/sitrep state while playing.
  • don’t use a potentially-missing font for left/right pagination arrows
  • Objective zones are now indicated with Union phonetic alphabet, created by Kai. Zones are labelled sequentially as you progress through a campaign.
  • Sort icons in the check engine light section, color the negative ones
  • replaceable parts icon was real small for some reason, resized up to 64x64
  • fix missing localization for escaping the orochi snare drone
  • Reload lancers picked for instant action on any changes detected (so if you have a lancer queued up to play instant action and then back out and modify them in the Roster, when you come back to Instant Action the changes will be there too)
  • Increase OpFor Name Character Limit from 20 to 40
  • Add tip to NPC import pilot roster to tell you where you can import from
  • Can click non-characters like jericho cover to inspect their terrain properties and health.
  • Ruler shows Xs on out-of-bounds spots
  • Forbidden interior setpieces match color of the outside bounds as a whole

alt text

  • fix stasis barrier and magnetic shield fx not showing up
  • mech slot setpieces clear the foliage they’re placed on so grass doesn’t cover up your current HP etc
  • enemies spawn looking at the closest sitrep tile

Editors

  • Added reserves; a number of stock ones plus the ability to define custom ones for a module. They can be expended on use (bombardment) or add stats (extra repairs).

alt text

  • Can grant reserves via event & check their statuses with fetches
  • “Random reserve” fetcher to be able to pull from a defined table (or unit!)
  • When opening a NPC character sheet from the editor, don’t switch to player mode when you change to a deployable
  • Show descriptions for trigger activations instead of just listing their context variables:

alt text

  • Add tooltip descriptions for fetchers

alt text

  • enabled searchbars in large select lists

alt text

  • Added an event to set faction of a unit (either permanently or for a duration)
  • Added event to manually reset back to starting values.
  • die/extract events queue reactions before they remove the unit in question from the game to allow for easier inspection of what happened
  • new mount/dismount event instead of having to tangle with adding gear to a unit specifically for that
  • Depreciated the optional-and-flag-riddled event_zone_set_properties in favor of a setup event for all properties and individual events to set each property individually. Updated all sitreps to used this new setup event instead.
  • Add event to play generic text in a banner + log text to the combat log

alt text

  • trigger fetchers immediately update available types when in WITHIN/CONTAIN mode (instead of just not saving correctly)
  • Fix triggers being able to compare arrays to arrays (do an intersection: “are there any of X in Y?”
  • Fix event_map_damage.target_tiles getting expanded by 1, causing misalignments when asking what terrain tiles were damaged.
  • fix playtesting modules not preserving starting origin character’s persistent IDs, thus preventing triggers removing lancers by direct reference
  • Fix: Diorama tile selector doesn’t go away when diorama is deselected with X
  • correctly remove the sprites of removed lancers from dioramas
  • fix removing forbidden-interior setpieces via trigger not actually making those tiles not forbidden
  • placing down wrecks goes through the event_unit_spawn pipeline
  • fix not being able to delete the last conversation in a module/combat
  • fix editor spinboxes overwriting old values when selecting a new item (causing it to seem like Clock starting ticks or size doesn’t persist if you changed them and then selected a different clock)
  • Fix “customize pilot” portrait editor button coming straight from the combat editor not always saving changes
  • Correctly switch to the new module/combat after using “save as” & clear the existing mod.io data when doing so (so the saved-as map counts as something new)
  • Fix camera angle in VTT exporter; it should be working again!
  • Can control the autoplay priority of module trigger nodes (instead of them all happening immediately)
  • Add checkbox for briefing moments to skip the briefing and jump straight to the combat
  • Beats no longer allow short repairs when “none” is selected
  • fix modules not storing the state of lancers after playtesting from the editor
  • Can toggle if PCs eject when opening their sheet in the combat/campaign editor
  • can no longer open character sheets while importing NPC into a combat; it would copy NPCs into a weird undeletable state
  • Can give module conversation nodes human-friendly labels instead of the conversation_id.

[Updates that break module save game compatibility: previous mid-module saves will not work on this build]

  • Reworked how data is persisted for downtime so hidden etc for moments persists between beats, and only runs the “full repair” etc events when you enter a beat (as opposed to returning to it without going to another one). Should probably still add the ability to be able to control whether repairs and autoplays are one-time things or happen every time you come back to that beat.
  • Can link briefings to other moments instead of only beats
  • Add event that can set rest type for beats & jump to a given beat
  • Module nodes no longer reset to their “starts hidden” value by default each time you return to a beat.

Dialogue editor fixes:

  • fix playtest conversations softlocking on stage directions
  • Add a warning when a choice follows a stage direction (since I don’t know how to fix the underlying bug)
  • Add warning for lines with empty text that will not be saved.
  • warning when choice starts the conversation
  • narrator choices use BBcode correctly
  • Default to monologue layout if none is selected
  • fix conversation lines not grabbing focus when you focus on their text boxes
  • fix speech bubble getting way too tall (word wrapping??)
  • clearer import behavior for conversations; will warn when it is going to override conversations + now correctly updates the listings
  • guard against starting conversation IDs with a number

Modding

  • Add missing-data frames and kits so something gets loaded, but will fail is_valid checks
  • Don’t always detect changes when there are duplicate mod ids in the mods folder
  • When downloading an update to a mod, replace the old one in the mods/ folder
  • Fix handling duplicate mods & downloading updates. Add ability to refresh mod data from current files.
  • fix tags not showing up in the manifest editor correctly
  • autosave mod manifest instead of needing to press the button
  • Form to add a changelog when updating mods.
  • Added a localization editor to the mod tool (sorta janky)
  • Fix modded-in manufacturers not getting their libraries loaded.
  • moved a buncha functions out of UnitCore and into a service file so they’re accessible to be extended
  • fix mods all sharing the same thumbnail in the downloads manager
  • Use get_all_weapons to include integrated ones in a number of places (fixes Enkidu not being able to access Hunter talents)

Misc

  • I’ve bumped up the version number of the game from 0.7.x to 1.0.x. This doesn’t mean the game is out of early access or anything; popping this cherry is purely a matter of not breaking semantic compatibility with mods in the future. Read this for more about semantic versioning, but the short of it is that the first number bumping up indicates a major breaking change that requires mods to be updated. Up til now I’ve been definitely operating on pride versioning instead lol
  • Fixed downloading combats with empty names from mod.io; a few versions back there was a bug that meant all uploaded maps were named “.json” with no filename. This build should now be able to handle it when it downloads a combat with no name like that.
  • Marko noted that we’ve hit the 2000th bug card card on Trello! That so many 🪲😵‍💫🪲
  • I think I’ve finally fixed the issue where the game crashes on exit. I’m pretty sure the culprit was FMOD, gasp.
  • I set up a better export-build pipeline so I only have to run a script on the command line to get PC/Mac/Linux + modkit builds that takes like four minutes. It doesn’t sound like a lot but will save me a half hour of manually running around switching computers and deleting folders every time we make an update.
  • We have a website! Aidan Grealish designed the new ✨ lancertactics.com ✨ and it looks so good.

We also showcased at PIGCON, the first games convention the Portland Indie Game squad has done! I went to some of the first meetups the organizers put on back in 2012 & have been in-and-out since then, but it was so cool to see what a force of indie game community it’s become.

alt text

I always come away with pages of playtest notes & it was very cool to see some of my indie dev inspirations speak in-person. :)

— Olive

Organizational silos & superheroing