Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Changing fields that have no impact on the game
Not all fields have functional use in-game. Some are used just to display something, like level descriptions for example. Now that we have isolated one level in our rundown, let's change some fields to distinguish it from R6B2.
First let's change the "name" fields of both rundown and level layout. As we know, this is one of the fields shared by all datablocks and must be unique in the file. It is never visible in-game without mods, but having good names helps with readability and lets us describe the block. This is especially relevant here since JSON has no comments (although there are ways).
The exact names are up to you. I can only try to give a few tips, whether to follow them is up to you:
Keep a consistent style of naming.
Describe what the block is concisely. A longer name is better than a shorter one that has no meaning.
Don't use abbreviations you might not remember.
Use practical names; no jokes.
You can mark unfinished blocks by adding TODO (and even what exactly is unfinished) in the name if you need to remind yourself.
If you're working on a block that has many entries and you're modifying only a few (e.g. archetype), you can think of a prefix to attach to all names so you know which ones you changed.
Here are the names I chose.
This is the only field we're changing that's usually invisible to the players.
Remember a lot of text fields are of the LocalizedText type, these can be both numbers and strings. If you want localization in your custom rundown you can use the LocalizedText type, in this guide we'll only use strings. You can always check the original text you're changing by looking for the ID in TextDataBlock.
Let's mark the fields we'll focus on:
In rundown datablock:
"Title" - the displayed title of the rundown;
"SurfaceDescription" - displayed when clicking the "intel" button (nobody reads this though);
"Prefix" - Mission prefix, in base game always used to specify tier. In old times the devs asked us to distinguish from base game rundowns (e.g. "act-1") but nowadays it doesn't matter;
"PublicName" - the name of the mission;
"ExpeditionDepth" - Drop cage target depth in expedition details;
"ExpeditionDescription" - text displayed under "://Intel_" in expedition details;
"RoleplayedWardenIntel" - text displayed under ":://Interrupted_Communications_" in expedition details and when dropping in.
In level layout:
"ZoneAliasStart" - used to calculate zone alias unless overriden. By default a zone's alias is this number + localindex offset.
There are more but this is enough for now.
Since this all has no impact on gameplay (except ZoneAliasStart but that has minimal impact), the values are up to you. Here's what I used:
The results:
There are more fields related to visual data in rundown and level layout but with this we have enough to make unique vanilla-style rundown & level information. We can move on to the 3rd step in the guide.
How to take a rundown and reduce it to one level
This step isn't necessary (and in some ways it's better not to do this at all), but for learning purposes it's good to understand exactly what we'll be working with.
For this step we're going to trim the main datablocks that we'll be working with (specifically Rundown and LevelLayout) so that they only contain one level.
This also doubles as an exercise in using VS Code.
To start, we'll grab a copy of the unedited Rundown, LevelLayout and GameSetup datablocks. You can follow along with how to generate them using MTFO from The Complete Newbie Guide or grab them from the archived versions at OriginalDataBlocks . You can also view the final version of our edited datablocks here: Final datablocks version page. Put these inside a folder in the GameData
directory of BepInEx. You'll have something like this:
MTFO can load datablocks directly from the GameData
folder, but it is better to keep them organized in a separate folder. MTFO also used to support loading datablocks from the plugins
folder instead, which is what you'll see in many mods and other guides (and even in other parts of this wiki!). Placing datablocks in the GameData
folder is now preferred.
Open your GameData
folder with VS Code.
Open the Rundown datablock and delete the headers.
Headers have no impact on modded datablocks, deleting them is optional. They're used only by the devs in their editor tools.
Now we'll delete all but one rundown from the "Blocks"
part of the datablock.
First, find the "Tutorial Holder"
rundown's block. This contains the game's tutorial, and its presence is hardcoded so we can't delete it.
Go to the start of the block, collapse it, and copy it. (Or, if using VS Code, you can use Control + Shift + [
to collapse the block your cursor is in).
Paste it at the bottom of the blocks.
Find the rundown that contains whichever level you'd like to use as a foundation. In this case, we'll use Rundown 6.
Collapse it, copy it and paste it at the bottom of the blocks.
Delete all the other blocks, aside from our two at the bottom (it's easiest to do this by collapsing them all). Once we're down to just our two blocks you might need to fix some formatting. There should be a comma between our two blocks, but not one at the end.
We only have one rundown and we want to keep only one level in it. Let's go with B2.
Collapse all the tier blocks.
Delete everything but B tier.
Make sure you don't delete the tiers themselves, just leave them empty. Otherwise the rundown will not load correctly.
Confirm that the 2nd object in B tier is B2 "Contaminant" and collapse the tier blocks
Delete all but the 2nd block.
B2 has now become B1 and is the only level in the rundown.
At the bottom of the rundown's block there is the "persistentID"
. When the rundown's ID is not 1, GTFO API ensures all levels are unlocked, so we don't have to take care of that ourselves. If you want to include level progression, you'll have to set the ID to 1 and then use the "Accessibility", "UnlockedByExpedition", and "CustomProgressionLock" fields to control levels being unlocked.
Contaminant is the only level in our rundown, but it still has a secondary layer. Let's thoroughly remove the secondary layer from the Rundown datablock. We'll have to remove quite a few sections, see the image below for what we have to remove. We need to remove the bulkhead information from the main layer, and also a bunch of stuff from the secondary layer. After we're done, the secondary layer should look the same as the third layer.
Now we have to do the same to the LevelLayout datablock. We'll find the correct level layout ID and delete the rest.
In the picture above from our rundown block, you can see that LevelLayoutData
is set to 162. That's our main layer level layout.
Open the LevelLayout datablock and find the block with this ID.
Repeat the same process as for our rundown blocks: collapse it, copy it, collapse all blocks, delete them, paste the copied block, delete the comma.
Remember VS Code has to process over 200k lines here so don't be too harsh if it lags a bit. Deleting at least a part of the blocks does have practical use here as VS Code and its plugins won't take time to load when editing huge files like this one.
There should only be a few thousand lines left in this file.
Most of the work is now done, and we only have to do a minor tweak to the GameSetup datablock. Open it up and change the "RundownIdsToLoad"
to only contain the rundown ID for your rundown. The rundown's ID is the "PersistentID"
number at the bottom of the rundown's block.
We talked briefly about changing the ID of our rundown block earlier. While this is not a necessary step anymore thanks to GTFO API, it was originally agreed with devs to change all custom rundowns' ID to 1. If you did do this, you'll obviously have to change the "RundownIdsToLoad"
to match.
You're also allowed to change the LevelLayout block IDs. If you do so, then you'll have to change the "LevelLayoutData"
from the rundown's block to match.
Remember to save everything.
Launch the game. You should now see only B1 in the rundown menu.
Drop into the level. You should now have only the one rundown, and the level itself should load just fine. If you have freecam or some other mods, you can use them to check what the level looks like more easily. See if anything has changed.
You should see some marker (all sorts of objects placed in the level, a whole different topic) placement differences, also the lack of the secondary layer.
Oh and we deleted the bulkhead key, controller, and the bulkhead door (which is now just a security door) from the main layer. If you want to restore that, you can do so by restoring these 3 fields from the original datablock we kept as a backup (you did keep some as a backup, right?):
ZonesWithBulkheadEntrance - zones in same layer (main) that have bulkhead entrances.
BulkheadDoorControllerPlacements - where to place bulkhead door controllers. If a zone with a bulkhead entrance does not have a door controller, the entrance will not require a bulkhead key.
BulkheadKeyPlacements - where to place bulkhead keys in the layer.
Make sure you restore the ones under MainLayerData
and not secondary. You can also just copy the whole MainLayerData
block.
Later sections of the guide will assume you restored these (or didn't delete them to begin with).
If you did actually restore and verify again, the terminal near the bulkhead might be near the zone door in front of the door controller now.
The Rundown, LevelLayout and GameSetup blocks are now cleaned up and we can move onto the 2nd step in the guide.
This guide should cover level editing enough to get you started making your own levels
The Rundown currently out is Rundown 8, revision 34800. If you're reading afterwards, you may have to adjust. If we're lucky any future changes to level generation will be backwards compatible.
Be aware that game updates can break datablocks, and levels/rundowns usually suffer the most from this. Rundown developers often don't even port their rundowns after some updates.
There's now a page with finished blocks stored and updates noted located here.
If you get stuck or are unsure if some changes were made as updates passed, check it out.
As you probably already know, levels in GTFO are generated, and their data is stored in datablocks. In terms of difficulty, editing levels is between intermediate and one of the most advanced things in datablocks. This guide is not meant to teach all thing related to level editing, but it will hopefully give you enough to get you going in creating your own levels.
This goes without saying, but if you're looking to learn, I highly recommend editing and testing the blocks yourself as you read along.
Throughout the guide, I'll be posting datablocks or parts of them. If you move along, you won't have to copy-paste them, but you can verify they match using Diffchecker for example.
At this point you should be familiar with JSON, datablocks, and the following:
VS Code editor (technically not required but guide assumes you use it)
Recommended:
CConsole or some other modding toolkit
The guide is made of 5 parts:
Isolating a level
Editing rundown and level metadata
Adding and editing zones
Editing warden objective
Adding a secondary sector
The datablocks we work with in this guide:
Main:
RundownDataBlock
LevelLayoutDataBlock
WardenObjectiveDataBlock
Edited:
ExpeditionBalanceDataBlock
EnemyGroupDataBlock
EnemyPopulationDataBlock
FogSettingsDataBlock
SurvivalWaveSettingsDataBlock
ChainedPuzzleDataBlock
Touched:
LightSettingsDataBlock
ConsumableDistributionDataBlock
StaticSpawnDataBlock
BigPickupDistributionDataBlock
ComplexResourceSetDataBlock
ChainedPuzzleTypeDataBlock
TypeList (OriginalDataBlocks)
Datablock Editor (but actually just level layout editor)
If we were in R4, this would be a guide on "optional" error alarms
Finally, the last part of the guide. level guide using as many mods as possible when?
We have already settled on the objective, but we have to make the sector before we apply it. We're not going to generate a third sector because it's the exact same process.
Start with making a new level layout. For a placeholder let's copy the block we have, delete all zones but 0, and clean up all the nightmares we added. Change the alias start to 200, and remember to rename and change ID.
Here's our placeholder layout:
We already have a placeholder objective, so let's move straight to rundown db.
Enable the secondary layer, set layout, set to build from zone 4 in main layer, set datablock ID to the unsolvable objective.
If you want to lock it with a bulkhead key, all you have to do is add an entry to main layer bulkhead door controller placements and set it to spawn in the same zone as the bulkhead. Remember to also take care of keys in that case. I'll go with secondary always unlocked.
Quick test to see if secondary did spawn and it did. Time to generate the proper layout.
Let's keep this small, to reactor basics. The first zone is fine. We're adding a bridge and a reactor geo for a total of 3 zones.
First thing to do here is find the custom geos. Remember you can only spawn something as custom geo if it is in that complex resource set's custom geos. In rundown db we can see we're using ComplexResourceData 1. Open ComplexResourceSetDataBlock, find ID 1. To make it easier to find what geos we have, I'm going to use breadcrumbs to find the custom geos:
For the most part we care about the Objective block. I'll copy that to a new file to make search easier.
I can see reactor gives 3 results and bridge 0, and I'm not sure the 3 reactor results are actual reactors anyway. Time to consult the Geomorph sheet (although the search in google docs doesn't seem to be the best search ever).
As suspected, the 3rd result was not an actual reactor geo. I'll take the first reactor (geo_64x64_mining_reactor_open_HA_01) and the classic reactor bridge (geo_64x64_mining_refinery_I_HA_05). I can see that both of these are in complex resource set 1 so I don't have to modify it.
If you're wondering whether you could spawn geomorphs from other complexes by messing with complex resource set, the answer is no. Only certain geos are loaded based on the level. The only way to mix complexes is to use mods.
Right, so, copy the first zone 2 more times, fix local indexes, add custom geos, and test.
Now this took me a few times with LG being finicky, but I got both geos to spawn in the correct directions. If you want to try for yourself with just pointers, check out how the map generates and adjust start positions and coverage. As I understand (I don't), LG will quit trying if it can't generate what it wants, and it can also skip generating the custom geos if it fills up the coverage size too early (normally when custom geo is set, that's the only thing that generates in the zone, but parts of random-generated geos can carry over from previous zones, filling up the size).
LG is rocket science that deserves its own guide if any poor fool ever bothered to make one, and on top of that it's still changing, so we won't dive too deep into why things happened here.
Anyway here's the level layout I got:
Some results:
If you have your own working layout, you don't have to copy mine. Note that there are some questionable terminal placements in there.
We've already made the objective, so we just need to apply it in rundown db and test it out. If it works, we don't have to do anything here.
Future me says it works fine. Excellent. The secondary layer is complete.
With the nightmare fully assembled, it's time for the last test. Go beat the whole level.
I'd love to insert a full video of me clearing it with Calle QA recording in the background, game muted (except for the combat music mod playing looming dread), cheats enabled, but I'm not gonna do that.
Finally done, feels like this took forever to write. Hopefully it wasn't so bad to get through as a reader. I hope you learned something, especially something about how to learn from existing examples and debug. Remember, you're not even limited to base game blocks. If you ask nicely, I'm sure most modders will let you use their blocks as examples.
I also highly recommend checking out the various different mods that expand datablock limitations.
Now if you'll excuse me, I need to go cast this abomination of a level into oblivion and then turn myself in. Good luck on your future endeavors.
If you're looking for a guide that explains events, you've come to the wrong place
Warden objectives are a core part of any level and they're quite convoluted complex just by themselves, especially with the new event system (that we won't look at). Our level is quite a nightmare by now but we're still not done torturing it for science. It's time to change the warden objective.
But seeing how in the next part we're adding a secondary sector, here we're going for 3 objectives:
Uplink terminal for main;
Empty (unsolvable) objective for secondary;
Reactor objective for secondary.
Alright, so, as you might guess, the zone we'll be adding this to is the zone we created, and we're looking for it to land in the 2nd terminal (further down the zone).
Funny enough we just yeeted an uplink objective with the original level's secondary layer, so let's steal that and also steal the text info from an uplink made before localized text became a thing (because we're not looking to deal with localization right now).
Remember, using existing blocks is often better than starting from 0, but be careful about which blocks you pick - some might become outdated with game updates. If you can, try to go for the current rundown.
We'll open up the WardenObjective datablock, and start by copying objective 274 (the original secondary objective) and the Header - GoToWinCondition_ToMainLayer fields from 19 (an uplink objective without localized text). After changing also changing the ID and name we get this:
Let's go through a bit of analysis before we change it. Know that not all fields are relevant and not all fields behave the same on different objectives.
Type is eWardenObjectiveType. Here it's 8, terminal uplink.
The info fields are straight-forward and can be easily checked in-game. Pay attention to strings in capitals in [] clauses, these are special values that get converted in code.
Waves during uplinks are set to settings 44, population 1. Population is standard, settings are set to standard and special only, single wave (10000 points), 5 enemies per group and 8 seconds between groups.
WaveOnGotoWinTrigger trigger is 0 (when objective is completed) but WavesOnGotoWin is empty, so there's no extraction alarm.
ChainedPuzzleToActive is 10 so a team scan is required before uplink starts.
ChainedPuzzleAtExit is 11 (always is) and speed multiplier is 0.2 (at 1.0 speed I believe it takes around 20 seconds to scan).
Rounds per uplink is 4 and there's 2 terminals.
Wave spawn type is 1 (InSuppliedCourseNodeZone), meaning they'll spawn in uplink zone.
The terminal picking and exit condition (elevator or exit geo) are set elsewhere, we'll look at that later.
Pretty much all of these settings are fair enough, we just need to change the uplink count to 1 and increase the exit scan speed.
Now let's configure the objective in rundown db.
Only 2 things to change here - the datablock id and the local index, the rest are already correct (make sure you edit the ObjectiveData inside MainLayerData).
Note that ZonePlacementDatas is a list of lists. If I recall correctly, the outer list is for different uplinks (if we had kept 2 uplinks for example), the inner list is for different locations for the same uplink (rng between runs).
The only other thing to note is WinCondition. 1 means exit geo. This field only affects info fields to my understanding, and only for the main layer. The actual exit location is always the exit geo if there is one.
You can have exit geos in layers, but that means you'll always have to do that layer.
You cannot alternate between several exit geos and/or elevator in the same level.
A quick test shows that all went according to plan, just one thing was forgotten - a door towards exit was previously locked until the objective was completed. We can copy that for our uplink objective and edit the text or we can disable the lock. I'll go with the latter and set PuzzleType to 0 on zone 9.
This is used for times when you don't want to deal with creating an objective but still need one (i.e. while you're still making a level) or when you have other means of completing the objective.
We're looking to make an objective as simple and universal as possible and in my experience the easiest way is a gather small items objective that spawns no items. The only thing you need to do once you have this block is set the objective ID in rundown db and you're done.
The block in question:
We'll try this out once we get to making the secondary sector.
Reminder that you shouldn't make objectives before you have the level.
Before we get into any specifics, remember that reactor is an objective heavily relying on waves. If you've read the heat explanation in the mod ConfigurableGlobalWaveSettings, you'll know there's a heat bug in base game and reactors will definitely mess with that, so you not only have to build your waves around the bug, it'll also affect the rest of the level and all other attempts until the game is restarted.
Let's steal R6D1 objective and text info from elsewhere. Reactor blocks are pretty huge thanks to each reactor wave defined separately, and this one also has a bunch of events. We'll have to analyze it in parts. Here's the whole block before editing:
Reactor objective type is 1; text fields are self-explanatory so we'll skip them.
Immediately on landing (EventsOnElevatorLand) we have 2 events, the first turns the lights off and the second adds a subobjective. We don't need either so we'll remove those events.
Then we have EventsOnActivate, I believe run upon completing the scan after inputting the startup command. Weirdly enough only one event seems relevant here - turning the lights on. The others don't seem like they would do anything - the 2nd event is type 0 (none) and the last is type 10 (StopEnemyWaves) but there are no waves to stop as far as I know. Maybe I misremember, or maybe there have been some changes to the level during development and they didn't get cleaned up properly. Either way we need none of these so we'll remove them as well.
Since we're on events, let's check the last remaining event in this block as well - "Events" under the 4th reactor wave. Usually the zone-unlock events for verification would be here, but R6D1 doesn't do that. Here we have one event - type 5 with trigger 1 - sound on start, with a timed delay. Looks like this is the tank spawn roar. We're removing it.
One last thing before we look at reactor waves - DoNotSolveObjectiveOnReactorComplete. This is used to make R6D1 seem like it's a terminal command objective when it's actually a reactor. Set this to false so the objective is completed after reactor.
Alright, reactor waves it is. Here's the first block:
The first settings are all about time. Time for warmup, wave, and verify phases, as well as for repeat phases marked as "fail".
VerifyInOtherZone determines if you get the code on HUD or if you have to find it on a terminal. ZoneForVerification is the local index of the zone to place the code in. No placement data here, which means we don't get to choose weights.
There's 4 waves total here - settings, population, and spawn type are all familiar at this point. The only thing new is "SpawnTimeRel" - when to start spawning the wave. Say, 0.55 would result in 80 * 0.55 = at 44 seconds into the wave. Remember that settings can delay spawn, groups don't all spawn instantly, spawn cap exists etc. so you have to be careful about not overdoing the waves, otherwise people will have to fight through the verify time.
I won't dive deep into the settings and population. I can guess that they'll mostly be very specific in spawn timing, enemies, and especially population points.
The other 3 blocks are the exact same thing, just different numbers, so there's no need to analyze them for me. This is all up to level design and balancing, and we don't do that here. I'll delete the 4th wave and leave everything as-is.
Here's the final block:
Time to finish this.
Don't be fooled by the fact that this guide is 5 parts, this right here is half of it
This is the meat of the guide. We're going to add a zone, add sleepers, alarms, blood doors, spitters, respawns, resources, a generator puzzle, a pitch black zone, fog.. there's tons of stuff we can do here.
However, as always, we can't explore everything or we'll be here all week (and this part is already really long). I'll try not to go into too much detail on the meaning of fields this time either, since you can check the relevant blocks' reference to see what all the fields do.
In practice I would just use the more readable blocks from the typelist (or some people opt to make levels using the datablockeditor), but we started with MTFO generated blocks so let's continue with them.
There's a block we can change once for the whole rundown to make level editing life a bit easier in the long run.
The datablock in question is ExpeditionBalance. We won't cover all of it, only the relevant parts.
In the Rundown datablock, ExpeditionBalance is always usually set to 1. If you want to make changes specific to levels, you can make a new expedition balance block and set the ID just for the level.
Yes, we're about to screw up the balance of the current level, but when making your own levels you shouldn't start with preset resources and sleeper spawns.
The fields in question:
These are the base multipliers for resources. Say, with Health being 1.0, setting "HealthMulti" in a zone to 0.8 will result in 4 uses of health, or 80% health for a player. This basically requires no calculation.
The values we don't like - the ones that complicate the math - are weapon and tool ammo.
Set both of those fields to 1 so you don't have to think about how many uses of ammo/tool you'll spawn.
Artifact progress is not saved when using mods, so unless you have special mods that make use of it, it's just an annoyance. What's more, they can even cause your layout to reroll if there's not enough space to place all of them, affecting not only one zone, but the rest of the layout generated afterwards.
Set these 2 fields to 0 to avoid artifact spawns:
Sleeper spawning system is convoluted, you can read about it here. The relevant part here is that when we're going with relative value, the points assigned are calculated like this: EnemyPopulationPerZone * DistributionValue
(from expedition balance and level layout).
So with a distribution value of 1, we'll have 25 points. But if we want to have exactly 12 points for example, we have to do some math.
Since we don't like math, let's set EnemyPopulationPerZone to 10. Now all we have to do to get 12 points is assign 1.2 in DistributionValue. Why not just go with 1? Because devs decided to automatically fail if it's set to less than 5 regardless of DistributionValue.
Reminder that we have just screwed up the resources and especially sleepers in our level, but in reality we wouldn't start with resources and sleepers already set anyway.
Now that that's taken care of, we can start messing with the level layout itself.
Let's add a new zone to the existing level. We're going to do this in the following order:
Copy the first zone and paste it at the end;
Change the local index;
Setup build parameters - where it's built from, the direction, size, altitude, subcomplex;
Adjust terminals;
Adjust lighting;
Set sleepers;
Set resources and consumables.
There's more we can do, but these 7 are plenty to get a full zone. And this is already too much to do at once without testing.
Go to level layout, copy the first zone, and paste it after the last zone. If you collapse all the zones now, you should see 12 in total:
An easier way to see is by using Breadcrumbs:
Anyway, the last zone had localindex 10, so this one should have 11. You can verify by searching for localIndex 11:
Remember, if you don't know what a field does, check the datablocks reference to see if it's documented.
SubComplex is 2, so this would be a Storage zone. Let's change it to DigSite (0) instead.
BuildFromLocalIndex is 0, so the source zone here will be the elevator zone. This is fine.
StartPosition is 1 (From_Start). The elevator zone is pretty small so this might not have impact, but let's change it to 2 (From_AverageCenter).
StartExpansion is 1. Let's change it to 3, hoping it will generate a security door to the right.
ZoneExpansion is 3. Let's also change this to go right, setting it to 5.
AltitudeData is 3. I would normally allow all here, but this level has infection fog, so let's go with 5. Medium-high should generate mostly above infection fog (of course that depends on fog settings but we already have existing medium zones and we know those are above fog).
CoverageMinMax is 40-45. That should result in a zone around medium-ish size. Let's set it to 65-65. We'll hopefully get a huge room and around 2 larges or a mix.
This is all guesswork. If we wanted precision, we'd have to try mods or keep rerolling the layout until we get what we want. But for now let's roll with what we get, even if it turns out completely different from my expectations.
The generated zone should be decently big now, so let's have 2 terminals.
Find TerminalPlacements, we'll set the placement weights to 0, leaving the terminal placement up to rng. Then copy-paste that block.
The results:
Lights are decided by "LightSettings" field. If this is set to 0, the light settings in rundown db will be chosen instead.
Right now it's set to 56. If we have a look at the LightSettings datablock and find that entry, we'll see it's named "AlmostWhite_1". Let's try 71 "RustyRedToBrown_1" instead. If you want, you can pick something different or even make your own light settings.
The sleeper spawning system is convoluted, you can read about it here. Yes I already said it before in this page.
We'll need the EnemyPopulation and EnemyGroup datablocks, and we'll also still need to make use of LevelLayout. Since we'll have to alternate between several files here, you might want to use split editor.
We're going to create 1 randomized group of either strikers, shooters, or big strikers, and 1 group with forced scout. We can reuse existing groups and populations but let's make our own for practice. A bottom-up approach is more appropriate here, so let's start with the EnemyPopulation datablock.
Deleting all existing blocks and remaking from scratch would introduce a few problems with hardcoded ID uses and the current level spawns so let's not do that (but when making full custom rundowns that might be better in the long run). Instead we'll be using numbers that are guaranteed not to mix up with base game blocks.
I'll follow the "rules" set in enemy spawning system. Considering the special enum values, limitations, and the total size of the enums in base game (ensuring scout entry doesn't step on one), here are the 4 new "RoleDatas" for our population entry:
These will go inside the "RoleDatas" of the population block that our expedition is using (you can see which population your expedition is using in the Rundown datablock). In our case, our expedition is using an "EnemyPopulation" of 1 (pretty much every level does). So we'll place these new "RoleDatas" at the end of this population, like so:
Population entries don't have a "Comment" field. But you can add any fields to JSON and as long as there are no syntax errors the game will ignore them. Comment fields have been added for clarity.
As we can see the role-difficulty pairs are unique so these entries won't be randomized at population-level, only at group-level.
Now to add enemy groups, these will just go below all the existing enemy groups in the EnemyGroup datablock:
1 forced scout group, and 4 mixed groups of the 3 enemies. Considering the relative weights, this should be a striker-dominant zone. MaxScores are the same all around so these will be a bit more predictable sizes. If we wanted more randomized sizes we could go with a range of 2-6 for example. It's just best not to give really big sizes to groups since a whole group always spawns in the same room, possibly resulting in high concentration in one area and rest zone empty.
Note I included an update to the LastPersistentID value. That's just so the logs don't clutter saying blocks with IDs above last were found.
Also remember MaxScore has that obnoxious hardcoded random multiplier on it, screwing with our spawns for no good reason.
Finally, we can set the spawns in level layout:
Comments again for clarity.
30 score spawns + 1 forced scout sounds fair for this zone size, but that of course depends on balancing.
Also normally you would take a look at the generated layout and settle on it before starting to spawn sleepers.
All that's left before we finally drop in is resources and consumables.
Before setting any spawns, always make sure these 2 are set to true in our LevelLayout block:
Resources are fairly straight-forward and purely dependent on balance so let's just use them to verify ExpeditionBalance values, setting all to 2 - should be 10 uses of each resource.
Consumables are set by just one field - "ConsumableDistributionInZone". If we tried to make something specific we'd have to constantly look at ItemDataBlock. Let's do the same as we did with lights and judge by names here instead. We'll have a peek at the names in the ConsumableDistribution datablock.
It's currently set to 45, "OnlyFogReps_Alt". Fog repeller balancing is important for an infection level, but our zone specifically is set to medium-high altitude data. While it can still generate something under infection fog, it should mostly stick to above that. Our options are combatting darkness, fog, giving foam/tripmines which are very powerful, or just generic consumable distribution that doesn't focus on anything in particular. Let's go with the generic one, setting it to 1.
A more proper level-making process could be something like this for example:
The entire layout;
Objective;
Resources;
Consumables and other spawns;
Sleepers;
Alarms.
But since we're just learning we simply focused on a single zone and finally made it to testing.
In lobby, remember to select the biotracker if you don't have mods that'll let you count and avoid enemies easier (you really should be using some though).
Before we drop in, we need to be ready for errors. We have 2 options here.
If bepinex console is enabled and unity log listening is enabled in bepinex config.
While dropping in, look at the console to check if you don't see tons of errors (red text) looping, meaning we've entered an infinite cage drop.
This is if we can't see game logs in bepinex console.
Open the game logs folder %userprofile%\AppData\LocalLow\10 Chambers Collective\GTFO
and locate the current log. Monitor it while dropping in. If the level generation fails, the latest log can start rapidly rising in size, meaning we entered an infinite error loop and will never finish cage drop.
If infinite cage drop happens, exit the game and relaunch after debugging and coming up with a fix. Exiting just the level can work but we can't guarantee no after-effects will carry over to the next drop.
Even if we drop in successfully we'll check for exceptions to see if anything failed without causing infinite cage drop. During the whole testing process and after exiting the level you should still look at the logs to verify no errors appeared.
Immediately upon dropping in and revealing the map I can something new at the right side of the map generated. Let's compare before/after.
We can see the map is the same apart from something new generated at the top right, which would be our zone. However, even without going in to test, it's clearly visible that the zone is not generated from the elevator zone. Verifying this we can see it generated from the zone to the right of elevator, and the source entry is to the north (forward):
Why did this happen?
There's no errors in the bepinex console and no particular errors in the game logs, but let's look closer.
The "BlockAndCleanFailedAreasFromZone" part is ok, it happens. I'm not an LG expert, but if I had to guess I'd say it either collided with other zones or ran out of plugs to connect areas.
Line 6 explains the cause. Zone 0 has nowhere to generate a security door so it moved on to another zone.
I'd say LG handled this well but we still need to fix it. A few options here:
Expand the elevator zone - screw up the whole existing layout.
Move source to zone 103 (right-most zone) - that is a fog zone and there are other balance shenanigans.
Move source to zone 102 - this is what LG did.
Let's go with option 3. We should also change the entrance to north like LG generated because there's likely no entrance to generate on the right side, but we can still try that and see what happens. Spoilers - it still generated from the north. Let's go ahead and change both fields now:
Here's our map now:
We can see it still generated in the same spot, as expected, and this time the layout of the zone is different. This is the exact same map as before changing entrance direction to north.
Time to explore.
The zone is heading right indeed, it's above fog everywhere except in parts of the last room, which is the huge one so that's expected. The lighting is also as expected. Here's a view of the last room from its entrance:
If it seems relatively dark, it's because I'm using the lights adjustment mod.
Two terminals generated, one near the middle, another at the very end.
Overall, I would say the layout is a great success.
24 total sleepers spawned - 2 big strikers, 1 scout, 11 strikers and 10 shooters. The spread is fair, only one area got 3 groups which is still very manageable. The spawns are very easy in this zone. I would say we can easily double the distribution here unless there's some special reason not to (like an error alarm), but we're not trying to balance the level. The spawns are all there, the count and spread is as expected. The sleepers part is successful.
10 uses of each spawned in various pack sizes. Right on target. If you get a bit more it might still be fine, because there's a base game bug where a resource pack spawns bigger than it should be.
With consumables we weren't going for anything special, so there's not much to verify apart from the fact that the usual stuff spawned.
We've seen everything and after leaving the level there are still no errors to see. We've successfully added a new zone.
Unfortunately, we're still not done here. Out of the things we said we would do in the overview, we still have these to do: alarms, blood doors, spitters, respawns, a generator puzzle, a pitch black zone, and fog.
Might as well go for some sort of world record in page length.
Or speedrun the rest of these topics.
Ah yes, the best difficulty filler in the game, abused on every single level. Even the error shenanigans in R4 are technically all alarms.
We'll eventually add our alarm by changing our "ChainedPuzzleToEnter" for our new zone in the LevelLayout block, but we'll do things from the ground up again.
Chained puzzles use 3 other datablocks (in addition to the ChainedPuzzle datablock itself):
SurvivalWaveSettings - the wave settings. Defines what, when, how many, and where;
SurvivalWavePopulation - wave settings specify what roles can spawn, this block maps roles to enemies, making settings more reusable;
ChainedPuzzleType - the types of scans, like full team, big, small, cluster etc. Mostly you just see the names and decide what scan to pick. I don't know any reason to edit this datablock.
In this example we'll only be editing ChainedPuzzle and SurvivalWaveSettings, though we'll reference things in the other blocks.
Survival wave population ID 1 will give us striker in standard, shooter in special, and big striker in miniboss. As basic an alarm setup as it gets.
For survival wave settings let's make something new. Something the base game doesn't do for alarms.
A little bit of analysis:
Updating LastPersistentID to match our new highest PersistentID.
Wave pause settings set to basically enforce 30-ish seconds between waves. The little variation is because something bugs if min-max are equal.
Population filter (with some help from base game boss bug) will allow only the 3 roles to spawn.
The most notable thing here is the limited population. Combined with points per wave, we get exactly 2 waves, with group spawns being more intense on the 2nd wave.
This alarm should keep the players occupied for at least a minute and depending on their clearing power, not much longer than that.
Tip: Whenever adding your own custom blocks, always check that the "persistentID"s that you choose aren't already in use.
Moving on to chained puzzle:
We're using the blocks we settled on - 400 for our new survival wave setting, and 1 for the basic survival wave population. For scans, one type 33 - the geo scan (introduced in R6.5) (highly likely not to function well in a random geo, but let's try it) - should be enough and we're not disabling the waves when the scan is completed, so the players will have to kill the 2 full waves.
Now they could just cheese it by running away and mining wherever until the population runs out, but from my experience they don't do that:
Barely anybody even notices something out of the ordinary with spawns when they're made well. If they do, they don't realize the specifics.
People hate wasting time. Even with cheese they'd have to sit in a sustain full-team scan for a while doing absolutely nothing.
All that's left is to set "ChainedPuzzleToEnter": 300
in our level layout and test.
Unsurprisingly the geo scan is screwed up but it's mostly functional so it's fine. Moving on.
As noted here, only hunter groups work properly for blood doors.
The new zone has suffered enough so let's leave it alone for now. Let's add a blood door to the zone left from the elevator - localIndex 1.
Setting up a blood door is fairly easy as long as you know your enemy groups. It's controlled by "ActiveEnemyWave" in level layout. Let's skip making hunter groups this time and just see what's in stock. I see "Hunter Easy Bullrush" in groups with ID 32. MaxScore 8 makes me guess it should be about 8 spawns, but it seems the cost is 2 in population so it's actually 4. Let's spawn one group on the door and 2 in the area for a total of 12 chargers:
A quick test here shows the predictions were spot on. On to the next topic.
Spitters, respawns, a generator puzzle, a pitch black zone, fog - we've got 5 things left to do, and at this point I feel like we've had enough practice to tackle them without too many details.
Spitters are static spawn enemies, controlled by "StaticSpawnDataContainers". This references StaticSpawnDataBlock, but it's rare to edit that directly in my experience. We can tackle just the level layout datablock after seeing that spitters are ID 1 in static spawns. Here's our block, placed in the first zone:
We're spawning just a few because I hate spitters. Most fields besides the ID and Count are for location randomization.
Respawner sacks are also a static spawn I believe, but they seem to be a special one, you only set up enemy respawning and they'll spawn automatically. So let's do that, again placing them in the first zone because we hate everyone who plays our levels:
The fields are all straight-forward here, but as always you can check the datablock reference for explanations. We don't need any respawn exclusions here so we don't even need to have that field.
Now when we messed with expedition balancing we reduced the first zone's population to barely anything, and there will be few respawn sacks as well because of that. Still enough to verify it's there and respawn works though.
I was going to make a keycard puzzle but we already have that example in the early zones. And generators have a little optional trick - we can spawn cells using big pickups distribution instead of ZonePlacementData. Scientific research suggests that thanks to how markers are set up, we'll get more possible placements inside our zone using big pickups instead of zone placement.
In zone 0, set ForceBigPickupsAllocation to true and BigPickupDistributionInZone to 5. In zone 1, set PuzzleType to 2 and PlacementCount to 0.
Now the zone to the left of spawn should require a generator to be activated which spawns in its entrance zone, and in elevator zone we spawned a cell using big pickups.
This one's just a matter of light settings, and there already seem to be 2 settings for darkness. Let's set LightSettings to 73 in zone 1.
This can't be set per zone, we can either edit the level's fog settings or setup some warden objective stuff. From rundown db we can see the level uses FogSettings with an ID of 90. We should copy that to a new persistent ID and set it, then edit the settings. But let's just edit ID 90 directly in the FogSettings datablock.
If we don't want to flip the balance of the level, we can only do something boring, which means we're flipping the balance of the level. Here are the new settings:
Fog density has been increased, altitude moved way up to 5, range and max boost changed to 0. Now the fog is upside-down and cancer is in the high zones instead of low. Flip successful.
If you didn't test these 5 parts yet, now's the time. We're all done with the changes. Here's a picture where you can see the fog, generator, spitters, and respawners:
Bonus points if you get hit by a spitter as soon as you drop in.
I would like to thank all my fans, my mother, and everyone that supported me. I finally got to finish this damn page.
Jokes aside, we can finally move on to warden objective. That topic's not simple either but fortunately I just have to settle on one thing and I get to choose a simple objective.
I mentioned typelist blocks before so I thought I'd throw in a few examples of how our blocks would look like then:
Shows where you can see the finished datablocks and the changes made since then
Since R6 is gone now and so are the source blocks, some things have changed and the guide may be hard to follow. You may want to see what blocks were used when originally developing the guide, or perhaps you just want to check/copy a specific block. Either way, the guide blocks are now stored on git:
You can also check out the commits to see any changes made since initial release. I will also cover some changes here as well.
Since MTFO now supports loading only specific blocks (also to fix some crap broken by R7 like players' shoes, shooters with pouncer stance and so on), the blocks have been minimized.
Additionally, the exit zone size has been changed so the max size isn't reached before exit geomorph is generated.
Here's the new map:
Now that devs have confirmed they're officially killing the game (and keeping old rundowns), code-wise it shouldn't change much, which means less maintenance work for rundown developers.
With this update, there's only 2 things to do to get old levels to work:
Change GameSetup rundown ids to load into a list
Add the tutorial rundown (its reference is hardcoded so we can't get rid of it)
That's it. The layout doesn't even reroll. Unless you were using some mods that are now broken (this guide only uses MTFO), the level should be fully functional.
It seems no changes are required.
Since this is likely to continue, the notes will only be updated if there are relevant changes.
The start of the guide has been reworked to include the R1 ALT update required changes. Additionally some ID numbers have been changed to avoid collisions.