This War of Mine

This War of Mine

評價次數不足
Advanced modding guide
由 toomanynights 發表
This War of Mine modding tool is infamously limited. However, using the right tools, you can go significantly beyond the limits.
I am toomanynights, the author of "Against All Odds" mod. In this guide I will show you how you can achieve things I managed to achieve in AAO. It's best if you have some LUA knowledge to proceed, but this programming language is rather simplistic, so you will have a chance to learn along the way.
   
獎勵
加入最愛
已加入最愛
移除最愛
Storyteller abilities
Tools you will need for this section:
  • Storyteller (in game folder)
Before you go any further, I'd like to stress how important it is to realize just which exact abilities the default modding tool ("Storyteller" going further) gives you.
First thing you have to do is to carefully study each branch. You know by now that the interface is divided into two parts: "Tree" (on the left) and "Content" (on the right). Make sure you check the Content for every tree branch - including the parent (first level) branches. Somewhat counter-intuitive, but parent branches can have their own individual Content unavailable in child branches.

Here it is on the example of "Main config" - possibly the most config-reach parent branch there is:



There are a LOT of configs in Storyteller. Study them, take some time to see what each of them does. Some work, some others don't. Some work partially - for example, you can configure titles for "Anniversary edition" and "Final cut edition" infoboxes on "Choose your story" screen, but it will change nothing because they are hard coded. Don't dig too deep - if you see it doesn't work, leave it. There is no pattern to those peculiarities; they just forgot to de-hardcode some stuff, and it will stay that way, so no point wasting too much time on it.

I can suggest several guides that will help you to understand better what's what in Storyteller:
  • "Modding Instructions.pdf" in the game folder. It is a default guide the developers provided for the modding tool. It is predictably stingy on information; it doesn't cover even 10% of the modding tool, but it's a good place to start, because it gets you acquainted with scenarios, timelines and other basic stuff (which you will probably need anyway). So start with this one.
  • Kierk's "Modding Tools Guide". It's a good one; I've learned some really useful things from it back in the day.
  • Adrianna's "Создание модов (советы и хитрости)" (Mod creation: tips and tricks). Yes, it's entirely on Russian, but let me tell you, it is entirely worth it to read the whole thing using your browser's page translation tool. This here is a true gem; I've learned so much from it that I can even say AAO wouldn't have half of its stuff without Adrianna's insights. So, it's a must read if you are willing to sink some time into TWOM modding.

Here are a few less obvious things you can do with Storyteller. You can use them as a checklist - if you don't know how some of them are done, you probably should research the configs a bit more:
  • Configure how many days it takes for a child to get attached to a grown-up.
  • Make it so a character would always get their worst (or best) ending without changing the actual texts.
  • Change raw food's effects so it would make your characters sicker when eaten.
  • Adjust effectiveness of board-ups against raids.
  • Change your dwellers' combat efficiency without changing weapon modifiers.
  • Make Roman fight his comrades every night he stays in.
  • Remove all loot from Marko's shelter at the beginning of his scenario.
  • Make backstab instakills impossible, no matter the weapon or the attacker.
  • Make a certain scenario always have a pre-defined location pack.
  • Change location's description upon visit.
  • Remove beds from Anton&Cveta's shelter.
  • Make it so the characters would never fall ill.

Once you're completely certain that you know most of the stuff Storyteller allows you, and it still leaves you striving for more, go ahead and read further.
Before you begin tampering
Just a short preparation before we move forward.

Folder locations
  • Game folder - where the game files lay. ..\SteamLibrary\SteamApps\common\This War of Mine
  • Mods folder - this is where Storyteller keeps local mods. Game folder\Mods.
  • Workshop mods folder - mods you get from Workshop are downloaded into this folder and put into subfolder named by your mod's Workshop ID (see below). ..\SteamLibrary\steamapps\workshop\content\282070
Have in mind that local mods behave differently than Workshop mods. Before releasing your mod in public, make sure you first upload it to Workshop privately, switch to it in game's Modding center, and then check if everything works.

File structure
When you edit anything in Storyteller, the game creates a .bin file for the appropriate branch. Let's say you decide to modify the visitors: it's done in VisitsConfig.xml file, and upon any change in that file, Storyteller will create VisitsConfig.bin file. If there is no such file, it is considered that you never changed the appropriate .xml. Storyteller monitors changes in the mod's folder; so if you launch Storyteller, manually change an .xml file and the return to Storyteller - it will offer you to reload configs. Then it will create a .bin for the changes you've made.

When you pack a mod via Storyteller, it generates archives of your mod in Mods folder: they all start with a long sequence of symbols, and then after "_" symbol follows archive name. I will further refer to the sequence of symbols before "_" as a "Mod ID". Note that you should not change Mod ID to something more convenient - it will not work, even if you apply changes in Mods.list (the file in which information of active mods is stored).

When you send the mod to Workshop, Mod ID is removed from the file names: they are named just "common.dat", for example. Remember this fact, it will be useful later. However, for the Workshop mods you should keep in mind another variable: "Mod's Workshop ID". It consists of numbers; you may find it after "?id=" symbols in your mod page's URL. For example, Against All Odd's Workshop ID is 2517681641.


Console log
When console log is enabled, the game outputs lots of debug info into ConsoleOutput.html file located in the game folder. While programming your mod, you will need this a lot! So in Steam interface, go to the game's properties and add this launch attribute:
-console
At first, this file will look like a bunch of useless lines to you - and for the most part, it is that, but with time you will learn how to look for the helpful parts.

Backups
Now seriously, make backups! I can't even count how many catastrophes I've managed to dodge thanks to backups. Further in the guide, wherever you see words like "replace", "move", "edit" and so on - read them as "make a backup and then proceed". It should become your prime instinct in modding: when in doubt, make a backup.

Where to store a backup?
Using cloud would be a perfect solution - Google gives about 15GB of free cloud drive, for example. But even a separate hard drive is better than nothing.

How to make a backup?
I recommend MaxSyncUp[www.maxsyncup.com] - it's a great, very configurable solution that will allow you to automatically backup your whole Mods folder every N minutes, or on change, or by a manual triggering. Target destination can be either Google drive (or some other cloud solutions), or a local folder.
Unpacking game resources
Tools you will need for this section:
So, what exactly does Storyteller do? It unpacks the game's archives, generates some .xml files and gives you an interface to operate those files. When you're done, it packs the archives back up, and via Steam Workshop your subscribers will get those archives delivered to their PC. The game will try to use the mods' archives upon loading, and everything that's missing will be taken from the default game archives.
The game is limited there. It will only use the following archives to try and replace the default content:
  • common (main game scripts)
  • textures
  • sounds
However! It is limited to the archives, but not to their respective content. It means that if there's a script in the default common.dat archive in the game folder (for example), and the Storyteller does not modify that script, but you find a way to put that script in your mod archive anyway... Then the game will still use it! And this is where the unpackers come into play.

If you have read Adrianna's guide I've suggested above, they you're already aware that there are two unpacking solutions out there.

BSUnpacker. It's rather self-explanatory to use. You just put it into your game's folder, point it at the archive you want unpacked, and it goes ahead and unpacks all content it can into the folder of the same name (for example, "common"). But this one is only good for research. You can use it to study the structure of folders and script names. Whatever this one packs back, can not be used in your mod. So if you intend to actually push your changes into the mod, use another one:

Python unpacker. Now this one is the real deal. However it's a bit more complicated to use. Here's how:
  1. Install Python. Don't worry, you won't need to program anything on Python, but the kit itself you will need to run the unpacker.
  2. Unpack Python Unpacker to some handy folder. The location of unpacker will be further referred to as an "Unpack folder".
  3. Take all files of the archive you want to unpack and copy them to the "Data" subfolder in the Unpack Folder. Make sure you really get them all. For example, if you want to unpack the "Common" archive of your mod, you will need four files: ID_common.dat, ID_common.dat_items.dat, ID_common.idx, ID_common.str.
  4. Run "unpack.py" script in the Unpack Folder.

Here. Now all contents of that archive are in "Out" subfolder of the Unpack Folder. What to do with that bunch of stuff and how to push it into your mod - will be explained further in this guide, but for now you're doing good, you actually got a hold of the internal game files.

On the additional note, above I mentioned three types of archives: "common", "textures" and "sounds". However, if you check the game's main folder, you will find more .dat archives than those three. There are also:
  • scenes (scripts and models of locations)
  • templates (scrips and models of the characters)
  • animations
  • videos
  • voices
I have bad news for you: the game will always take the content for these additional archives from the game folder. Even if you modify those archives and put them into your mod's folder, the game won't recognize them. But we'll get to that later.
Script files overview
Tools you will need for this section:
  • WinGrep[tools.stefankueng.com]
  • Notepad++[notepad-plus-plus.org]
  • Code editor of your preference (mine is VSCode[code.visualstudio.com])
After you have unpacked an archive via Python Unpacker, you've found yourself with a bunch of files with seemingly random names. They can be separated into two categories:
  • Binary files that look like garbage when opened with Notepad++. You can only do precious little with those files, because adding even one new symbol on top of what's already there will break the game. A replacement however is possible in some cases - see more in Adrianna's guide from above.
  • LUA scripts that show a valid LUA code. Now these we are interested in.
Why so? Because this game works on 11 Bit Studios' in-house product, LiquidEngine, which is a mix of C++ and Lua (you will see more than one reference to C++ in Lua code). So, whatever is in C++, is encoded and can't be edited - however we can and will edit Lua scripts.

How do you distinguish one from the other, however, without the pain of manually trying to open each file? WinGrep will help you with that. Almost all Lua scripts will have at least one function declared in them; so, grep for "function" word, and exclude binary files from your search.



Take some time strolling around the scripts, studying the functions, building the picture in your mind of what can be made here. And if you're having a hard time reading Lua code - jump to the next section, there is some basic explanation of how this works, and then come back here.

From my side I will provide a list of files I personally use in my mods - but don't limit yourself to those, try to study as many files and functions as possible.
  • 1df112b3 - parameter changing: SolveParameterDependency (ran each time any parameter is changed), SetParamArray etc. If you need to check every character for something (parameter, weapon equipped etc) - this is a good place for that.
  • 2f7e3494 - BindActions, KosovoItemEntity
  • 3e413a73 - KosovoScene:OnDayBegin(), KosovoScene:OnEnterScavenge(), KosovoScene:NewShelterItemBuilt(). Perfect functions to which you can attach actions you want to be launched at certain game phaces (day/night beginning).
  • 3e619c44 - KosovoEnemyParamsComponent:SolveParameterDependency. This is being launched every time the enemy is wounded.
  • 0466a296 - system functions: log, print-Debug/Error, members, console
  • 0589fc9e - LockAction, UnlockAction, shelter door, blockades
  • 925bdc1a - Set-GetDwellerParam, cheat functions, stats save
  • 8303f269 - Scavenge actions, Remove blockade actions, Chop actions
  • 13783f40 - action defaults, speeches, duration, tags, icons.
  • 1789003e - ItemAction. Defines overall logic for all types of item actions (icons with a hand or a tool on them). There's also ItemAction:OnEvent
  • a0d11dd5 - NonInstantAction. I believe it's even cooler than ItemAction, because IA are called from NIA.
  • a660333e - igKosovoParamDefinition - parameters for dwellers (hunger, stamina, sickness etc)
  • b1d8530e - igParams.ItemActionBindings: actions icons, tools and resources. For some actions - also duration
  • c4d1ec48 - engine functions, from Game:OnInit() to LuaGameDelegate:OnTick()
  • ce6d60cf - igKosovoShelterParamDefinition: shelter parameters, most notably heat
  • fb89ae15 - KosovoCraftingBaseComponent, UseCrafter, CancelCrafting etc
Functions overview
If you've spent enough time in the previous section, you probably have already noted some examples of the functions you might need for your mod ideas. I will add to that a bit, but before that, a few words of explanation how it all works.
You can declare a new function in any of the files, it will look like this:



This way you can make your own function "helloWorld()". Within it, you call a function "print()", which prints whatever you give it to the console log (whatever you put into the brackets after the function is an argument you pass to this function to further process - in this example, print to the log).

But you can not make the game launch it. It will address the functions by itself how it sees fit. It means that perfectly correct code from above will do nothing by itself.
However! You can call your function from some other function that you know for certain is being launched at the moment you need. Like this:



So now, whenever a new day starts, "Hello World!" will be printed to the console log - and you'll be able to find that exact moment in there.



When the game is launched, it runs through the entirety of code, including the scripts you provided, to validate the functions. So, don't be surprised if the game won't launch even though you've only created a new function but never called it. It also means that whatever variables you've declared anywhere, will work from any other function (well, almost... There is a load order here, so to avoid headache, it's best if you write your own functions in the files in which you will call them). So for example, you can declare a variable in your function from above, like this:



And then use it in another function like this:



But I'd recommend against it. When in doubt, better use local variables and pass them between functions, it will allow for more controlled data passage, and that way you'll be certain you won't mess some pre-existing valuables. So, better go like this:



What I did there: I declared a local variable scene. Local means it will be desctroyed when the function holding it completes. But you'll still get the value of this variable, because that's what it returns to the function that called it before termination. And we can to with that what we please - for example, print it:



Here we called helloWorld() function and said to print whatever output it gives. And here's how it looks in the log:



As you can see, "Hello World" was printed to the log - because it's a part of a function helloWorld() that was launched; and location name was printed too, because you said to print helloWorld()'s output.

This way you can pass data from your custom function into the pre-existing function that you know is being launched. But sometimes you'll need the opposite: to pass some info from pre-existing function into your own to be processed. This is where arguments come into play again.

First, you gotta declare that this function expects one argument:



Then you have to pass this argument to your function:


Now I'm getting location name in KosovoScene:OnDayBegin() function, so I don't need to do it in helloWorld() too. Thus, I'll remove the unnecessary location name fetching, and instead will tell helloWorld() to print the argument it received (note the ".." - it's a concatenation, you can combine several strings into one this way):



And it works!


Useful functions
Now you got the basics of functions: what they are, how they work, how you can create one, call it, pass information from one into another.

Now, as promised, here are the functions that are already available and ready to be used. Because you can't go purely on custom functions, you'll have to use in-built ones, a lot. However, remember that a function that works in one context will not work in other. Modding requires a LOT of trial and error - exercise both 🙂
  • In this [docs.google.com]sheet, you will find a mix of functions I've found most useful, with examples of usage and comments. You will see that some are marked as "?" - it means I didn't get around to research them. If you do that, add a comment and I'll gladly include your contribution 👍
  • And on a separate tab[docs.google.com] there are all functions that allow you to address C++ from Lua (mostly for reading, but sometimes they will allow you to write too!). There are lots of them! To use one, take the function name from the header, and then add a method name from one of the fields of this column (before first space); for example, gKosovoGlobalState:CheckGlobalItem().

Take a good while to study these lists, and in the next section we'll try to make something resembling an actual mod.
Making your own mod: planning
In this section we will try to do something a bit more complicated than helloWorld(). How about this: do you remember a leaflet in Supermarket that says "don't touch anything, it can be booby trapped"? So let's implement booby traps!

In modding, you have to improvise a lot, but it's always helpful if you start tampering having some sort of plan in mind. You can adjust it along the way, but an initial plan can help structure your research and avoid your mod from falling into production hell. So, for our booby trap mod, let's try the following plan:
  1. Find the action of scavenging a heap (we want to attach this logic to heaps only).
  2. At the start of this action roll a dice. We only want booby traps to have 5% probability to fire.
  3. Make dice roll results stick. We don't want this check to be performed every time. Booby trap is either there or not. On any other interaction but the first one we don't want to check for a trap.
  4. If the roll is bad, hurt the dweller performing an action and make them stop it. This is a core feature, so we'll be checking this one before any cosmetic effects.
  5. Consider applying wounds only if the dweller is not too wounded already. The thing is, there are heaps in shelter, and we don't really want a dweller to die in the day: currently in the game they always die off-screen, so dying right in the shelter may cause unwanted effects. But we'll check how it works and then see if anything needs to be done here.
  6. If the roll is bad, make the character say the appropriate speech. This is a core feature too: we need this speech so let the player know what's going on. But for the sake of the guide's structure, I'll be exploring this one in the very end, because it will require its own section.
  7. If the roll is bad, play an injured animation. As it happens, there is already this animation in rubble removal. It was cut out from the final version, but we'll use it nonetheless.
  8. If the roll is bad, play an explosion sound. At the moment of writing this plan I'm not even 100% sure it's possible, but let's try, this sound would fit out mod nicely.
  9. Make the feature exploit proof. This is a point you'll want to include into pretty much every new feature you develop. Players always find ways to cheese the game - don't make it too easy for them 😏
So this looks like a plan now. A good feature, not too complicated, but not too easy either: a perfect example for this guide. Just so you know, I've written this plan before trying to actually do anything, so there's a chance we'll have to eliminate some of the planned features - just like in the actual modding. So now, let's proceed with every planned step one by one.
Making your own mod: execution, p.1
0. Prepare yourself a mod
You'll need a brand new mod, so create it in Storyteller. Unpack sounds, no need for textures. I usually use a single test mod to test every new feature I make, you may do the same or create a new mod every time, as you see fit. Pack your mod with Storyteller and have the packed archives handy.

1. Find the action of scavenging a heap
For this I will use WinGrep on an unpacked "common" archive from the game folder. I'm looking for an action that's being performed on a heap pile, so I'll start my search with "scavenge" keyword. And I find something that catches my attention in the file b1d8530e:
ScavengeType = "TrashPileScavenge",
Looks like just what I need. So I go ahead, open this file and see this:



To me it looks like this is where an action attached to an item I need ("TrashPile"). The action itself seems to be called "ScavengeAction", so it looks like I need to inject my code into wherever "ScavengeAction" is defined, making sure to check that this action is attached to a "TrashPile" item.
So I search for function:ScavengeAction and, sure enough, I find it. It's in a 8303f269 file, and there are a lot of methods:
  • function ScavengeAction:OnInit()
  • function ScavengeAction:SetDefaultIcon()
  • function ScavengeAction:OnTagChange(newTag, isRemoved)
  • ...
Each one represents a certain stage of this action. You can go ahead and study them all in that file; what we need for our current project is called function ScavengeAction:OnBegin(user). Here's how this function looks:



From the look of this function I can tell that:
  • It expects "user" as incoming argument - which is good, we'll need it to hurt whoever gets caught by a booby trap.
  • It has "GetOwner()" method, which is great because it means we'll be able to get a name of the object that holds this action and apply our changes to trash piles only.
To make sure this is indeed what we're looking for, let's print this action's owner's name whenever the action starts, plus the name of the function to find it easier:



And now a bit of testing. To test how it works, take all parts of "common" archive of your mod, and unpack them with Python unpacker as explained above (section "Unpacking game resources"). Then take a script you've changed (b1d8530e), put it into ..\out\%modID% directory of your unpack folder, pack the thing by launching pack.py, take the repacked archives from ..\out\ folder, and put them into your Mods folder.

This is a procedure you'll have to run each time you change anything in your code or in Storyteller. Get used to it. 😎

Now, we've packed everything, loaded the game with console log enabled, made sure only our new mod is enabled, all cool. Let's dig in some scavenge points to see what the log will show us. Here's what we get when we search a trash pile:



A wardrobe:



And a closed locker:



So, yeah, looks like this is just the spot. We've found it 🎉Now we need to limit our action to only plies of heap, but let's do that a bit further.

2. At the start of this action roll a dice
Now seems like a good time to create a separate function for our logic. We've already established where we will need to call it from, so separating our code from the default one is a good way to make this thing more readable and easier to develop. I will put the function right on top of ScavengeAction:OnBegin(user), and I'll call it BoobyTrap(). I will also supply two arguments to it: the action instance and its user.



Notice a few things I've done there. When I call our new BoobyTrap() function, I provide two arguments: self - it always means the instance currently being operated on, in this case a scavenging action, and a user - the operator of this function which is being provided to ScavengeAction() function itself.

Now, we need to find a word "trash" in the action owner's name. But I didn't just grab that name; I've transfered it to lowercase, because string.find() is a case sensitive function, and we don't want our logic to break because one particular pile somewhere is named "trashPile_007" instead of "TrashPile_007".

Then I use math.random() function, which generates a number between 0 and 1. By limiting the function to only proceed if that number is less or equal than 0.05, I make it so its contents (they will be added to where that red arrow is) are only launched in 5% of cases.

Finally, when it comes to random chance, it's good to get some sort of feedback in any case, so we'd know that it works at all. Hence the "else" and another line of texts that will be printed if the trap didn't explode.

So, let's go ahead and dig a few trash piles to make sure this works as intended:



Looks like it does!

3. Make dice roll results stick

There is a great method :AddTag() which happens to work on actions. And we can add any tag we like and then check if the instance has it; for example:



But in this case, since there are two scenarios (exploded or not), we'd have to make two tags, one for each. In cases like this, I think it's better to add a new parameter to our action instance, like this:



As you can see, I now check for action.TrapExploded to be empty, and in every possible scenario I write something there. So it's not supposed to trigger more than once.

Notice another thing I did there: our dice roll is now a separate local variable called BoobyRoll, and I print it no matter what the result is. That's because I want to make sure that the program correctly makes a decision. And looks like it does:



But in this action our target was to make sure this check is performed only once; let's check that by attempting to interact with a single trash pile several times, better yet - even check it on another day:



And from these results you see the problem I encountered. Yes, what we did prevents the check from being fired on subsequent usages of this action and even on the next day; however, restarting the game resets the parameter we've set. I've gone ahead and checked how it works with the tags; exactly the same, so it's the compromise we'll have to accept: after the game is restarted, all trash piles the player has interacted with will be reset, and next interaction with them will cause a re-roll.

Lastly, I've modified the function a bit, so the roll would only be displayed if it's used to avoid confusion, and with that, we can move to the next item.


Making your own mod: execution, p.2
4. If the roll is bad, hurt the dweller performing an action and make them stop it

Since we've already established that the roll works fine, I will modify the function temporarily so that the trap would fire in 100% cases - for that I will use a 0 instead of roll results. It will make it easier to debug the function.

From the spreadsheet with functions from above you already know a great method that will do just what we need: :SetParameterValue(). So for this part, we will get the current "Wounded" value of a user and then increase it by 20.
As for the interruption, by analyzing other action functions you might've already seen that it's done like this: self:Interrupt(). So let's put all of this into our function:



And then let's try it. The result however is... Underwhelming. Cveta just stands doing nothing, while the action gets stuck in progress. She also doesn't get injured.



Note the errors in the log: they will almost always tell you which line makes an error.



There are three mistakes done here:
  1. We have put a call to our function at the very beginning of OnBegin stage. I presume that at this point action's user is not defined yet, so the game doesn't know on whom to inflict damage.
  2. Presumably, OnBegin is a stage at which crouching animation is not yet launched, so our trap will fire before the dweller even sits down to a trash pile.
  3. Just changing the amount of HP is not enough. You also have to apply these changes, which is done by calling a function SolveParameterDependency(), providing a user as a first argument. So, it may look either like this: SolveParameterDependency(user), or like this: user:SolveParameterDependency().
To fix this, we will call our function from ScavengeAction:UpdateProgress() instead of ScavengeAction:OnBegin(). To make it more interesting, let's choose a random moment from 10% to 80% of progress to fire our trap. And this small improvement unleashes a whole bunch of problems that I will go through now, just because it may be useful for you:
  • math.random() function by default gives you a random number between 0 and 1; if you want it to specify a range, you need to give it one integer as a range start and another one for the end, and then divide it by 100. That's how this function works 🤷
  • You should roll from outside ScavengeAction:UpdateProgress - for example, ScavengeAction:OnBegin() will do nicely. Because otherwise, a dice will be rolled every time a progress is updated (several times a second), making the result always very close to the range beginning.
  • And one more thing to consider: math.random() always gives very precise numbers, like 0.70000000298023. So if you compare your progress with them, always use comparison (> or <) instead of equality (==), because your progress will never hit this exact number, more likely going from 0.70 straight to 0.71.
So, here I've added a new progressRoll parameter to our action in ScavengeAction:OnBegin() and removed a BoobyTrap() call:



ScavengeAction:UpdateProgress() assepts "progress" argument, which we will use; however, there is no "user" argument, so we'll have to grab a user from the action itself (self.User). And this is how it will look:



Don't forget to also add user:SolveParameterDependency() to your function:



And it works just nicely!



5. Consider applying wounds only if the dweller is not too wounded already

It would be quite realistic if this booby trap could actually be lethal. The only thing that's stopping me is the fact that I don't know what will happen if the dweller dies in a middle of the day. So to check it, I'll temporarily crank the damage up to 100 and see what happens.
...
And it's actually quite hilarious: dweller just straight up disappears, while the rest react like they died yesterday 🤪 On scavenge, however, it works flawlessly. So it seems we will have to do something about it after all.

There are several options here, but I think the best one is to just exclude shelter trash piles from this logic altogether. A gGameDelegate:IsScavenge() function will help us out here:



By adding another condition to our function, I've made sure that it won't fire unless we're currently scavenging. And don't forget to tune the damage back to 20 here 😏

6. If the roll is bad, play an injured animation

The animation I've mentioned happens to be in this very file, and it's called like this: self.User:Suffer( "action-InterruptDigging-idle" ). So, let's try adding it and see if it works.
...
It does work perfectly! With one exception. For some reason, adding this animation makes the dweller ignore the action:Interrupt() function, and after "suffering" they go straight back to it. However, there is a workaround for this. Since they start the action from the beginning, thus triggering ScavengeAction:OnBegin() again, we can add another parameter upon trap triggering, check for it ScavengeAction:OnBegin(), if it's there - remove it and interrupt the action. So, here are the changes needed in BoobyTrap():


And this goes to ScavengeAction:OnBegin():



7. Make the feature exploit proof
I remember there are also sound and speech stages, but let's deal with them in separate sections. For now let's analyze our feature and think of its weak points. Are there any ways the players can use in this mod to either gain unmerited profits, or avoid the losses they would normally suffer with this mod?
  • It would be possible to overcome the damage if we'd put a "checked" flag on an action at the moment of interaction with it, but inflict the actual damage later. Then the player could "touch" the pile, interrupt immediately and then come back to it normally. But luckily, this will not work: the only thing the player can change that way is - at which point of progress will the check be performed. But they will not be able to fully perform an action without rolling a dice for booby trap, so we're good here.
  • If we'd make it so the characters already wounded wound not receive damage, then the player could abuse this in the simplest way: by sending already wounded characters to deal with all trash piles. In our case, however, it won't work.
  • At some point the player will realize that booby traps are not attached to pre-defined piles, and the decision is instead made at the moment of interaction with a scavenge action. Then they will realize they can simply restart the day and maybe they will be luckier next time.
    I'd say this is more of a vanilla exploit; the player can actually use it to cancel any bad event that happens on scavenge. There are indeed ways to work with it, like we could compile a complete catalogue of all trash pile names on all locations, then at the beginning of the game choose random 5% of them, log their names as events, and then check for those events at the beginning of every scavenge... We could, but in this case I don't really feel like it's worth the effort. All it would do is stop a small amount of players from abusing the game, bringing zero value to the honest majority.

So, it looks good to me! Let's move on.
Adding sounds to your mod
Sounds can be very important to your mod. Not only for decorative purposes, too; in case with our booby trap, it would be really neat if an explosion of a booby trap would produce an actual in-game ruckus that could attract enemies from nearby.

To add a new sound to the game, you have to add a new entry sou "Sound config - Entries" in Storyteller. This is how the sound of a booby trap will look:



Let's go through every line:
  • Duration is for how long the noise will be produced in game. So you can take a sound that only lasts for 1 second, and make it be heard in game for 5 seconds - for example, to imitate echo.
  • Range is from how far away the sound will be heard by NPCs. There is no exact answer on how to choose this number; I picked 12 because, after checking other sounds, I've gathered that opening a lock with a crowbar uses range 13, and a small booby trap should produce noise about that loud.
  • SFX repeat time - I'm not sure what that is 🤔
  • SoundTable entry name - this is a link to the actual sound and its parameters, see below.
  • Tag - I'm not sure how it will be helpful in our case, but just for completion sake I've specified "Explosion".
  • Three Visible options - as I gather, this means the sound wave visualizing the sound.
OK, we got the sound entry configured, but there's also the actual sound to specify. As it happens, for this mod pre-existing "Ambient_Artillery_Close" sound fits perfectly, so I used that one, but you may need to create a new sound too. For that you will need "Sound table config" branch.



But I'm afraid there's a problem here. I didn't have any luck adding new entries here, the game simply doesn't see them, no matter how hard I tried. However, if you really need your own sounds, you can use some clearly unused entries on the bottom of the list ("XXX_OLD_Foley_HandToHandHit" and "XXX_OLD_Foley_HandToHandSwish"); and if you need more, just reuse some of the "Stories" items (they have WS in their titles). After all, stories are recommended to be played without any mods, so with lack of better options, you can use this workaround.

Some of those items have more than one sound:


If that's the case, it means that a sound to be played is picked at random from this group. "Remember last played sounds" parameter is used to set how many sounds have to be played before the very first played sound can be picked again.

OK, now one thing left is to actually make it so the sound would be played at the right moment. Searching by "Sound" keywords has led me to this line of code, which seems to do just what we need:
gScene:StartSound(Vector.ZERO4, self.User, "ForceAction");
From those arguments the first one - Vector.ZERO4 - seems to be a sound engine, which is always the same; self.User is a source that emits a sound; and "ForceAction" is a name of sound entry. So, let's add this to our function:



Checking this in the game, we can see (and hear) that it works perfectly.
Localizations: problems and workarounds
We need to add a line of speech that will trigger when the booby trap explodes. However, adding localization strings is, arguably, the most bugged part of Storyteller. Let's go through the list of things going wrong and how to work with that.

In some cases new lines in Localizations do not get recognised
Some texts, like intros at the beginning of scenarios, or item names/descriptions, work fine when added as new localization strings. And then some others, like speeches - don't. You must've seen how it looks when a game doesn't recognise a speech string:



But of course, that doesn't mean you can't add your own speeches. The developers' slopiness will actually help us here; they've left quite a lot of unused strings that you can reuse. For example:
  • /TEST (the whole group)
  • /UI/WarStories1-3 (some strings)
  • /AgeVerification (the whole branch is not used on PC)
  • /NPCSpeeches/LEVEL_SPECIFIC/WarStories/Story1/OldManTrader/EmptyTableSpeech
  • /NPCSpeeches/Story
...and more! Keep searching, and you shall find. Rule of thumb here is: if you see keywords "old/test" keywords in a string title, or if it's got only one language version, then most likely it's not used in the game and you can safely re-use it.

And this brings us to the second question... How?

Multi-line texts can't be pasted into Storyteller text fields
Let's say we want to paste this text into a Localization string in Storyteller:
This is line #1 And this is line #2
You will quickly realize, however, that everything past the first line will get lost. Like this:



You have no idea how many HOURS I've spent copying & pasting the invisible symbol that allows to have multi-line strings! And then separately copy&pasting the second, and the third line into each and every language field... I honestly have no idea how this have passed QA.

Anyway. Very recently I've made a discovery of a lifetime: you should just put click not on a line itself, but on its language code. Like this:



And quess what? If you do exactly that, then you can Ctrl+V and paste as many lines as needed at once. And even delete the whole thing with Backspace!


Making a character speak
But instead of those test lines, we will add something more suitable for the occasion, like this:



A quick search by "Speak" keyword will lead you to the fact that character speech is triggered by a line as simple as:
user:Speak("123")
Where "123" is not a line itself, but its address in Storyteller. As you remember, we re-use test for speeches, so in our mod it will look like this:



Localizations are not added to the mod
And here we reached the true plague of Storyteller. After all the hard work you've done, all texts you've edited, you pack your mod, launch the game and see how perfectly it works, You push it to Workshop, and after a while, reading comments, you begin to realize that something's off. The players don't see your texts...

I do understand your emotions. I've spent quite some time bashing my head against the wall about this one, and I've thought I'll have to use a separate mod for localizations like it's mostly being done. However, at the very last moment I've stumbled upon a well-hidden piece of advice (unfortunately, I forgot who made a discovery - if it's you, drop a comment, I'll credit you!).

In order to make it work, you'll have to use something I call a localization donor (or a donor mod). Here's what you need to do:
  1. Create a new mod. You won't use it for anything but for being a localization donor, so don't change anything there.
  2. Go to this mod's "Localizations" folder.
  3. Remove every file with ".lang" extension.
  4. Go to your actual mod's "Localizations" folder.
  5. Copy every .lang file that ends with "_%modID%" and paste it to your donor's "Localizations" folder.
  6. Remove your mod's ID and "_" character from these files' titles. So, for example, "ancient_3dde5318ba8641bfbdf863fb23f31480.lang" turns into ""ancient.lang".
  7. Pack your donor mod.
  8. Three "localizations" archives of your donor mod residing in "Mods" folder are the ones you're gonna push into Workshop instead of your actual mod's archives.
Why does it work like that? I don't know and I don't wanna now. All I care about is that this is the only known way to make it work without asking your subscribers to use additional localization mods on top of your actual mod.
Complicated? Indeed; but if you read further, I might be able to help you to make it a bit easier.
Pack your mod and send it to Workshop
Tools you will need for this section:
We're about done with this mod. Once you've checked that everything works locally (and removed all temporary stuff, in this example, dice roll was replaced with 0 for easier testing), you gotta push it to Workshop for everyone to use. I recommend against using in-game mod pushing interface however; it's not very controllable and doesn't give you as much abilities as SteamCMD does. So, unpack SteamCMD somewhere handy - we will need it a bit later.

Pro tip: NEVER use the "stick" symbol - this one | - either in mod title or in description. It will break your mod to smithereens, as @adrianna learned - the hard way.

The whole procedure includes the following steps:
  1. Create a folder to which you will put your archives to be sent to Steam Workshop (will be further referred to as "pre-Workshop folder").
  2. Pack your mod via Storyteller.
  3. From mods folder, copy all your mod's archives except for "common" and "localizations" to pre-Workshop folder.
  4. Add your customized scripts into "common" archive, as seen in "Making your own mod: execution, p.1" section, subsection #1: "Find the action of scavenging a heap".
  5. Copy the archives that underwent re-packing from unpacker/out into pre-Workshop folder.
  6. Also copy common.dat_items.dat and common.str archives of your mod from Mods folder and into pre-Workshop folder.
  7. Create localization archives via donor, as explained in "Localizations: problems and workarounds Edit" section, subsection "Localizations are not added to the mod".
  8. Put localization archives created via donor into pre-Workshop folder.
  9. Remove all prefixes from your mod's files. So, for example, "8d3ef3d9599a4935b737a59d813aa553_common.dat" should turn into "common.dat".
  10. In your SteamCMD folder, create a .vdf file. If it's your initial release, then it should contain:
    "workshopitem" { "appid" "282070" "visibility" "0" "changenote" "Optional release note" "contentfolder" "Path_to_pre-Workshop_folder" "title" "Your mod's title" "description" "Your mod's description" "previewfile" "Path_to_thumbnail_file" }
    If you're updating an existing mod, then it should go:
    "workshopitem" { "appid" "282070" "visibility" "0" "publishedfileid" "Mod's_Workshop_ID" "contentfolder" "Path_to_pre-Workshop_folder" "changenote" "Optional release note" }
    Make sure to replace the values values with actual data.
  11. Run SteamCMD, log in using your Steam credentials. (Note: your Steam client will log off, it is normal.)
  12. Run the following command in SteamCMD:
    workshop_build_item VDF_title.vdf
    Replace VDF_title with actual title of your mod.
Is that a lot to take in? Surely; well, this guide isn't called "Basic modding guide" for a reason 😀However, now you know how you can make something a little bit more interesting than the default Storyteller stuff.
https://youtu.be/TtqQbwmLzgA
Go through the whole procedure several times, and then you can move on to the next section.
Making packing process easier
When you make new mods, a lot of testing is inevitable. And when you do a lot of testing, you do a lot of repacking, too. One day I've repacked my mod one time too many, decided it was enough, and written a batch script that does all the hard work for me. No manual renaming, no drag&drops, no keeping a dozen things in your mind at once. And I want to share that script with you.

But! I really must insist that before you download it, you go through the whole process manually at least a few times. You gotta understand very well what the script is supposed to be doing; after all, I've written it for myself, and I can't 100% guarantee that it will work for you.

However, I've tried my best; I've actually doubled up on the code to implement a config mechanic, so you wouldn't have to manually edit the paths, names etc. Upon the first launch, the script will ask you everything it needs to work; it's a lot of questions, but you only gotta do it once, your input will be saved for further use. Make sure you provide the correct data though: it has some basic validations, but if you provide an existing path to the wrong folder, for example, then it's likely that it simply won't work. If you messed up with some answer - don't worry, simply close the program, remove the config file and start over.

The repacker is available here[github.com]. Put it into your Python unpacker folder, launch it and follow the instructions.
Replacing character models
Tools you will need for this section:
Yes, in this particular case you gotta use the first unpacker - not the Python one you've used to edit the scripts.

Intro
So the Storyteller gives you the ability to completely overwrite any character's bio, change their name, alter how their trait is called (you can't change the trait itself, but you can replay it - say, "good cook" can turn into a "Moonshiner"). You can also change their photo. And finally, thanks to Lua scripting, you can add some new effects - like, you can make a "Resilient" character who will have a chance to shake off the wound even without a bandage, or make the character "Sickly" and have them randomly getting sick even with normal temperature outside. So pretty much anything can be changed, but what's the missing piece here? That's right - character model. And with this trick you can actually alter it too!

Finding your characters' models
In BSUnpacker interface select templates arhive in your game's folder. It will be unpacked into "templates" subfolder in the game folder. Playable characters' models are located here:
\templates\gfx\characters\rdy2
Sometimes they are named in accordance with the character's name (Pavle, Bruno), sometimes the nicknames are used ("matematyk" = "mathematician", Anton; "zlodziejka" = "female thief", Arica). These are the files you have to replace with some others in order to change playable characters' appearance. For that you can use almost any NPC's model - and now let's get into how you find one.

Finding models to replace the default ones with
You gotta use WinGrep to look through "templates" folder; there are two variants of line you're looking for:
C:\Users\11-bit\Documents\3dsMax\export
C:\Users\Dominik\Desktop\TWOM\Animations\
Those are binary files, so you'll have to open each one you find. This is how they look like:


There is no way to guess how they will look like in game, so one thing you can do is heavy testing. Create a test scenario which will contain all playable characters. Then find the models you're interested in, and replace playable characters' models with the ones you found. For example, to change Katia's model for the one called "D0660B9D" in files names, remoce "katia.binarytemplate" file, move "D0660B9D" into that folder, and rename it into "katia.binarytemplate".

Packing & loading
After that, use BSPacker (goes in one package with BSUnpacker) to create an updated archive. The interface is simple; make sure to set Packing options -> This war of mine. Then replace the "templates" archive in your game's folder with the one you created. And - try to start your game.

If it won't start after several attempts, you probably made a mistake somewhere. But you remember Rule #1 and have backups, right? Then go back and try again.
This is how I made Katia and Roman look like:



How do I add it into my mod?


Like I said before, "templates" is the archive that the game will not use even if the mod will provide it. Of course, you are welcome to try, maybe you'll find some way I didn't. Otherwise, best thing you can do is to create a mod via Storyteller (to send it to Steam Workshop), and then on your Workshop page publish a link to the modified "templates" archive with the instructions, like I did in "Music" mod. https://gtm.you1.cn/sharedfiles/filedetails/?id=619418946
Don't waste your time
In this penultimate section I would like to share something that will be most useful in your modding experience: Value Your Time.

It is more important than you think. Every minute that you waste on doing something in a long run stacks into hours and hours that you might've spent on working on some exciting feature. Every delay increases the possibility that you'll burn out and abandon potentially great idea right before the big break-through. I know I sound a bit like Jeff Bezos here, but in this example you are your own employer, and the assets you are trying to save every possible bit of are the time you have left in this world.

No one's here to stay. Choose carefully what you invest yourself into.
  • Loading the game multiple times to test something? Don't waste your time on watching or even clicking through those intro screens. Disable them in your test mod.
  • If your mod is not connected to the daily needs of your survivors, enable god mode. This way you'll not waste time on caring for their needs while testing.
  • Need to craft things? Take a couple of minutes to bulk change every item recipe using WinGrep and regular expressions so they would all take 0.1 hours to craft, require 1 wood, and use BasicWorkshop as crafting station. Better use your time on studying regexp than on useless going through crafting time and again.
  • Make sure to edit your shelter's loot generator so you would have abundance of every possible item. Never waste time on searching for some item while you're testing.
  • Testing something on scavenge? Use trainer with NoClip and invulnerability. This way, instead of climbing, opening doors, removing rubbles etc, you will just teleport to wherever you need, and you won't risk dying from enemies' attacks, salvaging yourself time you'd waste on restarting the night.
  • Make sure your test scenario has a lot of dwellers (preferrably every each one).

If I think of something else, I'll add it here. Or better yet - if you know some good time savers, share them!
As a conclusion
Phew! This guide took a ton of time to write, I'll tell you that. But I hope you found it useful. I am sincerely thrilled to see some advanced mods made with it, so make sure to share your work in comments! And if you have any questions, that comments section is for you too, I'll try to help. However I've spilled pretty much all I know here.

Happy modding!




This guide is dedicated to my cat Boss, who's been sitting on my lap throughout most of the writing. Give'em hell up there, Boss!


RIP 2001-2023


44 則留言
toomanynights  [作者] 11 月 11 日 下午 11:12 
@Necromorph I did not see any functions that could've influenced backpack slots. Do experiment though, perhaps you'll find a way :)
Necromorph 11 月 11 日 下午 10:05 
@toomanynights
Do you think we can make the bag function (equip to add carry slots) work with the coding way?
toomanynights  [作者] 10 月 7 日 上午 2:34 
@7Ztan If I'm honest, I hope they will leave it alone. Patches tend to break mods, and I'm not looking forward to fixing all of mine.
7Ztan 10 月 6 日 下午 5:02 
@toomanynights thanks for responding. It's a shame it's not possible with Storyteller. I will send a daily email to the developers, at least until November 14th. I hope they give us something for the 10th anniversary, even if it is a small patch.
toomanynights  [作者] 9 月 30 日 下午 9:51 
@7Ztan I'd say this problem is far beyond Storyteller. I don't think it's possible to fix with mods.
7Ztan 9 月 30 日 下午 9:41 
@toomanynights Hi. Did you know about a bug where every child that comes to the door at radiostation shelter immediately leaves?. I was looking for a mod to fix that bug but i think i'll try making one myself. I'm curious to know if it's possible to fix that bug making a mod using only Storyteller.exe. If it's possible where could i start?
toomanynights  [作者] 1 月 30 日 下午 10:52 
@solton You're welcome! Hope it works out for you.
solton 1 月 30 日 下午 9:38 
thank you so much
toomanynights  [作者] 1 月 29 日 下午 9:48 
@solton Hey. Localization strings can be changed to reflect the gender "swap" (see the appropriate section above). Not so sure about the model though; as far as I know, there's no model of Amelia separate from the bed (but feel free to try anyway, there's a section about model swapping as well).
solton 1 月 28 日 下午 10:00 
Hi I was trying to mod genders for stories. (I wanted to change gender of the kid, because I remade it and it's father to a man and his son, that I know personally) I need to change the gender because of my language. In my language for example word "hungry" has two different forms based on gender "Hladová" a "Hladový" So when I simply change the kids picture to a boy it is still pretty obvios that it is female.... Has anyone tried it? if yes could you please help