Site Tools


tutorial:intro-to-elden-ring-emevd

Intro to Elden Ring EMEVD

Authors: thefifthmatt

Preamble

Elden Ring event scripts work a lot like event scripts in previous games. They use the EMEVD/EVD/EventMakerEx format. The main difference comes from new commands and the world itself, which is large and interconnected and composed of hundreds of different maps.

Event scripts are extremely powerful tools for modifying gameplay. They interact with many different game systems, but usually only in specific ways that the game needs. They can give you items and orchestrate boss encounters, but they can't spawn in an enemy not already present in map data. This means that the most successful approach to event modding is not to start from scratch. Instead:

  • Identify some modification to the game's functionality you want to make.
  • Understand that functionality when it appears elsewhere in the game. Investigate scripts, maps, params, until you fully understand how the functionality is configured and what all of the important pieces are.
  • Copy that functionality, piece by piece, and make small tweaks it until it resembles your desired modification.

The most important skill in modding is investigation. The game is too big for any one person to know all of it, and fromsoft provides no intentional documentation or modding support, so figuring mechanics out yourself is a critical part of the process. But it's not as daunting as it sounds! It's easy to identify patterns in game data and scripts using tools built by modders, since modders have already done some of it for you and will try to give you pointers when you ask.

This page isn't a full tutorial, but at least the minimum stuff you need to for how to get started modding event scripts. The first part applies to any way of editing event script, but later is some analysis of events decompiled with MattScript specifically.

Essential info

This section does not dive into details on the scripts themselves, but walks through the various tools and resources you'll need to browse and understand them. It's a bit of work to set things up upfront, but well worth it for making mods quickly and correctly!

Prerequisite tools

Event scripts interact with many different game systems, so to best understand them, you can use a variety of different specialized tools together in concert. This is just a quick listing. Check the tools page, and tutorials page for more specific guides. (Remember to copy oo2core_4_win64.dll if any tool needs it!!)

Required tools:

  • Use Selective UXM ( Github link, Nexus link) to select the game exe and unpack the game. This requires around 50GB of extra disk space.
  • Use Mod Engine 2 ( Github link) to use standalone directories to both modify game files and seamlessly switch between different mods. You can download this anywhere, but all of your mods will be stored here, so choose a suitable project directory for you.
  • Use DSMapStudio ( Github link) to view MSB files, which come from map\mapstudio in the game hierarchy, and also to view/edit game params. This tool is super important.
  • Use WitchyBND ( Github link) or the older Yabber ( Github link) for unpacking specific formats like archives (ending in bnd.dcx) and game messages (ending in fmg).
  • Use various Elden Ring reference pages, also listed below under “entry points”

Of course, also use an event script editor. These are available for Elden Ring:

These tools are useful in specific situations:

  • Use DSLuaDecompiler to decompile and view AI scripts. Currently, Altimor+nex3's fork is the most Elden-Ring-compatible. Note that it is a command line application and must be built with Visual Studio; you may be able to ask around for decompiled scripts.
  • Use ESDLang ( Github link) to view NPC dialogue trees and scripted menus like graces and shops.
  • Use Elden Ring Debug Tool ( Github link) and/or Elden Ring Practice Tool ( Github link) for easier in-game testing.

Tips for browsing maps and scripts

Before getting into event scripts themselves, here are some miscellaneous tricks and tips for using editors and viewers.

To search for an entity in currently loaded maps in DSMapStudio, go to “Search properties” and type “EntityID”. Enter any entity id to find currently loaded entities with that id. Select the enemy in the search results, then click on the central map viewport, then press F to jump to the entity. This works for many properties including “Name”, where you can enter a name like c2500_9000.

If there is some object in the way which is normally invisible in game, you can click on it, and press Ctrl+H to hide it. I would also recommend browsing the Map Object List using Type View, and switching between different Gizmos > Origin modes as appropriate.

If you've mass-dumped AI scripts or text fmgs or ESDs, Notepad++ is a great way to find things inside of them, because you can use Find in Files (Ctrl+Shift+F) within a directory.

DarkScript3 tips

Here are some key productivity features for in DarkScript3 in particular.

You can decompile all event scripts to JS files so you can search them easier. You can do this with File > Batch Dump and selecting all events. I would recommend making a whole separate copy of game events only just to browse them, so you always have a complete reliable dump of all vanilla events, but it depends on the kind of mod you're making. Select “Use MattScript” for easier-to-read scripts, *especially* if it's just a read-only copy of events. More details on that below. Either way, this will take a few minutes to complete. The program will hang, but don't exit out of it!

To search within the current script, use Ctrl+F. To search across all dumped JS files in the current directory, use Ctrl+Shift+F. Or select either respective option from the Edit menu. This is invaluable for finding all of the places where an entity or flag is referenced or where a command is used.

A list of all commands and conditions is accessible from Help > View EMEDF, and also this online mirror.

In event scripts, all events must be initialized to run in-game. You can Ctrl+Click on an event id to jump to its event definition, and Ctrl+Click again to go to its initializations. You can do the same with Ctrl+Enter while the text cursor is inside an event id. The status bar will contain info about the result of the search. One feature added in v3.3: when common_func.emevd.dcx is open in another tab, Ctrl+Click can jump from common event initializations into the common_func event definition.

Another minor convenience feature is Ctrl+1. When the text cursor is on an integer, DarkScript3 can show the bytes-equivalent floating point value. This is a stopgap until event initializations can be fully typed. Until then, you shouldn't replace them in the script itself, unfortunately. If it's decompiled as an int, it needs to remain an int for the time being.

"Is this thing on?"

Don't forget! If something isn't happening in the game when you expect it, make sure your changes are even recognized by the game in the first place. This is best accomplished by simple “is it on” checks which do something conspicuous. For instance, in DarkScript3:

DisplayBanner(TextBannerType.DutyFulfilled);

or

AwardItemLot(997200);

There are a few banners which the game can show, listed in the TextBannerType enum. Banners are very conspicuous, but they have the disadvantage of taking up most of the screen and having imprecise timing. You can usually get better results from awarding an item lot from ItemLotParam_map. Respawning resource pickups are a great choice here, like 997200 for Rowa Fruit or 997960 for Ruin Fragment. See item analysis to search by item name, but make sure no event flag is present in the item lot, or else the drop won't be repeatable.

So, if you're not sure if an event is executing at all, first make sure that it's actually initialized in a constructor. Add a debug instruction at the very top of the event, and make sure the effect occurs in-game upon quitting to the main menu then loading back in. Then move the existing debug instruction around, for instance to make sure a later condition triggers when you expect it to. Fix any bugs and repeat this process until everything triggers when you expect it to.

One last note: make sure to remove the debug instruction when you've fixed the issue! If you've ever played Dark Souls mods where nonsensical banners pop up, now you know why.

Commentated events (MattScript)

In lieu of more specific modmaking tutorials (planned), here are a few events in Elden Ring opened with DarkScript3 with MattScript enabled. You can find general MattScript documentation here, but basically, when enabled, MattScript simplifies event script control flow and makes them easier to understand and edit. Its major strength is that it allows dealing with complex conditions without dealing with condition groups, and eliminates the need to count skips ever. It's also easier to use as a base for future advanced features. I'd recommend it for experienced modders, but especially for newcomers. Even if you don't use it everywhere, you can still enable it for individual events at any time (use $Event instead of Event, it's just that simple!).

Below, I've included several different events and explanations for them. They deal with several different game systems, so if you find a term you're unfamiliar with, don't hesitate to check the Glossary. Feedback on how to improve clarity, or additional concepts to explain, is greatly appreciated.

If we want to add other dialects (like regular DarkScript3 or soulstruct), this can be split off into a separate page.

Intro to events

Let's start with some shorter events in the game, before getting into longer ones which pull together a ton of game systems. An event script defines many events which run while the script is loaded. You can think of these like functions in a programming language, each identified by a unique number. An event must be initialized to have any effect in the game at all. All events run in parallel, executing one instruction after the next from top to bottom, sometimes skipping optional instructions or stopping altogether until some condition is met.

For how loading works, that's a bit complicated. See the map list for an introduction to maps in Elden Ring. When a map is loaded, its event script is also loaded. When you die, or save and quit, or warp to a grace, all the loaded event scripts are loaded in again from scratch. Everything gets completely reinitialized, and if it's not stored in the save file somewhere, it's like it never happened.

How this works in the event script itself is that there are special events called constructors which get automatically initialized when an event script is loaded. These are events with low ids like 0 and 50. As you can see, these constructor events are responsible for initializing every other event in the file, and a few others too.

For instance, take a look at m14_00_00_00.emevd.dcx, which is the event script for Raya Lucaria. In DarkScript3, you can see its first constructor is a very long event which starts with these lines:

$Event(0, Default, function() {
    RegisterBonfire(14000002, 14001952, 0, 0, 0, 5);
    RegisterBonfire(14000003, 14001953, 0, 0, 0, 5);
    InitializeCommonEvent(0, 9005810, 14000800, 14000000, 14000950, 14001950, 1084227584);
    InitializeCommonEvent(0, 9005810, 14000850, 14000001, 14000951, 14001951, 1084227584);
    InitializeEvent(0, 14002080, 0);
    ...
});

Like all events, it has three main parts: an event id (0 in this case), a reset behavior, and a series of commands which execute from top to bottom. The reset behavior of “Default” means that the event does not reload when you rest at a grace, but “Restart” means that it does. There will be examples of both below.

Event 14002080: Virgin abduction (standard event flow)

You can Ctrl+Click on 14002080 in the above event (within DarkScript3) to see the event's definition. You will find this event, which is responsible for the Iron Virgin warp from Raya Lucaria to Volcano Manor:

$Event(14002080, Default, function() {
    EndIf(!PlayerIsInOwnWorld());
    WaitFor(PlayerIsInOwnWorld() && InArea(10000, 14002080) && CharacterHasSpEffect(10000, 14307));
    SetPlayerRespawnPoint(16002080);
    SaveRequest();
    SetEventFlagID(16000540, ON);
});

So as soon as Raya Lucaria loads, each command in this event starts getting executed one-by-one. First, the event might end immediately if the player is summoned or invading (if they're not in they're own world), since they're not eligible for the warp in those cases. Then, execution pauses until all of these conditions are met, based on the WaitFor line:

  • The player is still in their own world
  • The player is in the “basement” area in Raya Lucaria at the bottom of the giant elevator. The entity id 10000 refers to the player, and the EntityID of this region is 14002080.
  • The player has the SpEffect numbered 14307. A SpEffect is like a status flag set on an individual entity, which is capable of many different kinds of temporary and permanent character modifications. In this case, it's set on the player during the “getting eaten” animation to indicate they died from the Iron Virgin's attack.

Once all of the above conditions are met, execution can continue past the WaitFor. The event script knows the player is about to die, so it quickly changes some game state. It manually sets the player's respawn position, which would normally be their last grace, to Volcano Manor (16002080 is a region EntityID there). It issues a SaveRequest to make absolute sure the respawn point is saved before everything gets unloaded, and sets an event flag which is checked by the event script in Volcano Manor to know it should do special things like trapping the player there.

Most events are like this. They have some condition for ending early, some initial setup sometimes, some WaitFor condition before they properly activate, and some result when the condition is met.

I've mentioned event flags a few times. Event flags are values in an on or off state, accessed and stored by a numerical identifier. The id is important, since only a few ranges are recognized as valid by the game. They're used to track global state while the game is running, and depending on the id, to automatically store that state in save files.

Event 10002460: Blind eagles (event restarts)

Here is another event in Stormveil Castle (m10_00_00_00) which has two waits in it. It includes a RestartEvent, which is the one exception to events only running from top-to-bottom. When a restart command is encountered, execution stops there and jumps back up to the top. This basically resets the event to like it was when it was first initialized.

$Event(10002460, Restart, function() {
    WaitFor(TimeOfDayInRange(19, 0, 0, 5, 59, 0));
    SetSpEffect(10005460, 10930);
    WaitUntilTimeOfDayInRange(6, 0, 0, 18, 59, 0);
    ClearSpEffect(10005460, 10930);
    RestartEvent();
});

Without going into a ton of detail, SetSpEffect(10005460, 10930) makes all of the knife-talon eagles in Stormveil Castle worse at detecting the player, changing their sight range from 8 meters to 2.4 meters. ClearSpEffect undoes that effect.

The overall flow of this script is: wait until the in-game time of day is between 7pm and 5:59am, and make eagles more blind. If that's already the case when the map is loaded, this doesn't wait at all, and instead happens right away. Then, wait until a time between 6am and 6:59pm to restore their vision. Repeat ad infinitum.

Event 18002860: Soldier of Godrick boss fight start (if statements)

This event is in m18_00_00_00, Stranded Graveyard, which also includes the Cave of Knowledge. It follows a very common pattern with Elden Ring boss fights, which is that there's an event which disables the boss when it's defeated, and also initializes the fight when it isn't.

The key thing to note here is if (<condition>) { statements. Unlike WaitFor, these don't wait for a condition to become true, but instead immediately evaluate it.

$Event(18002860, Restart, function() {
    if (EventFlag(18000850)) {
        DisableCharacter(18000850);
        DisableCharacterCollision(18000850);
        ForceCharacterDeath(18000850, false);
        EndEvent();
    }
    DisableCharacterAI(18000850);
    WaitFor(EventFlag(18002855) && InArea(10000, 18002850));
    EnableCharacterAI(18000850);
    SetNetworkUpdateRate(18000850, true, CharacterUpdateFrequency.AlwaysUpdate);
    DisplayBossHealthBar(Enabled, 18000850, 0, 904311000);
});

You can break this event down into 4 key parts. For now, I'll skip over what the commands do exactly, since it's discussed as part of the Godskin Duo explanation below.

  • If the boss is defeated (event flag 18000850 is on), prevent the enemy from respawning, and then end the event. By default, all enemies respawn after resting at a bonfire, unless manually disabled in this way.
  • If the boss is not defeated:
  • Before the WaitFor, disable Soldier of Godrick's AI to make them unresponsive, so they don't detect the player before the fight actually begins. This happens as soon as the event script loads.
  • Wait for the fight to trigger. In this case, it's waiting for the player to pass through the fog gate. This is explained in more detail below.
  • After the WaitFor, reactivate the boss's AI and make “Soldier of Godrick” appear at the bottom of the screen.

One thing to observe here is that the entity id for Soldier of Godrick 18000850 uses the same number as the event flag 18000850. From the engine's perspective, however, they are completely independent. Setting the flag does not automatically affect the enemy, except through this event. You can distinguish one id from the other based on the context they appear in.

All of the above functions like DisableCharacter, which are usable in event scripts, are listed in the EMEDF HTML. Some of these may be simpler versions of other instructions, like DisableCharacter and EnableCharacter are both simpler versions of the longer ChangeCharacterEnableState

Custom event: Suffer for fashion (condition variables)

(Feel free to skip this section if it's overly technical.)

Most examples of condition variables in the vanilla game are somewhat complex. Event 10003736 in Stormveil Castle is one of the simpler ones, which controls Gostoc stealing runes from you, but it still involves a ton of quest-specific event flags.

Refer to MattScript documentation for all of the exact details, but in short, condition variables allow for declaring a condition expression on its own, rather than just passing it straight into a WaitFor, if statement, or various other special commands (EndIf, RestartIf, GotoIf). This doesn't actually evaluate the condition until it's used in one of those other commands, however. Normally, you should avoid using this, but it is needed in these situations:

  • If you want to WaitFor any of several conditions to become true (like using foo || bar to wait for either foo or bar) and check which one was true at the time the WaitFor passed.
  • If you want to make a condition which itself depends in other conditions. This is mainly useful for event scripts with parameters, where parameterized conditions might be invalid in some cases.

We'll get to the second case when we talk about parameters, but a quick example of the first use case is as follows. This event kills the player if they've been wearing a certain piece of armor for more than 30 seconds straight. It does this by waiting for one of two things: either 30 seconds have passed, or the armor is unequipped. It uses variable.Passed after the WaitFor to see which was the case, and reset the timer in the second case.

$Event(xxxxxxxx, Restart, function() {
    WaitFor(PlayerHasArmorEquipped(ArmorType.Legs, 1930300, -1));
    notWearingArmor = !PlayerHasArmorEquipped(ArmorType.Legs, 1930300, -1);
    WaitFor(notWearingArmor || ElapsedSeconds(30));
    if (notWearingArmor.Passed) {
        RestartEvent();
    }
    ForceCharacterDeath(10000, false);
});

Again, declaring a condition variable doesn't evaluate the condition. Instead, it “registers” it for later evaluation, though this has some important restrictions. It's a quirk of EMEVD that requires some caution. In MattScript, it's distinct from JavaScript variables, which are also supported; use const to declare those.

Godskin Duo

This is a full dissection of how the Godskin Duo fight works from an events perspective. The first three events are very standard and applicable to most bosses, but later on it gets into specific resurrection mechanics. Godskin Duo is somewhat complicated, but not as complicated as something like Rennala (in either phase). Feel free to look at any other boss, if you want, and try the same process on them.

It's highly recommend you watch Zullie's video "The Four Kings have a secret fifth member" since Godskin Duo is set up in a similar way. They both use generators to spawn the same enemy over and over, as many times as desired, with a hidden healthbar-only enemy. The main difference is that Godskin Duo's generators do not run on an automatic timer, but use the manual InvokeEnemyGenerator command.

Make sure you have DSMapStudio and DarkScript3 open while browsing these scripts. Open the m13_00_00_00 map in DSMapStudio to find entities mentioned below using “Search properties”.

Because these events are on the longer side, I've added commentary inside of the events themselves, using JavaScript comment lines (with \/\/). You can check out events without comments first if you want, but make sure to open up the version with comments to see a line-by-line explanation.

Event 13002860: Start Godskin Duo

Like with Soldier of Godrick, this event either initializes the fight or disables all of the boss enemies. Unlike Soldier of Godrick, Godskin Duo is more complicated because multiple enemies are involved, and there is different behavior depending on whether it's a first or subsequent encounter. This is the big if/else statement in the middle of the event, which switches between the two behaviors based on an event flag.

This fight actually involves three enemies, with these entity ids. Try locating them with DSMapStudio Property Search.

  • 13000850: The hidden healthbar enemy, lurking beneath the floor. This is technically a Godskin Apostle, but the only thing that matters is that it has a lot of HP. See the Four Kings video!
  • 13000851: The Godskin Apostle you fight
  • 13000852: The Godskin Noble you fight

After this event finishes, the entire rest of the fight is just fighting the godskins as if they were normal enemies (aside from the regeneration mechanic, which is handled by other events). They choose attacks based on their AI script, which is specified in their NpcThinkParam.

Show event

Hide

$Event(13002860, Restart, function() {
    if (EventFlag(13000850)) {
        DisableCharacter(13000850);
        DisableCharacter(13000851);
        DisableCharacter(13000852);
        DisableCharacterCollision(13000850);
        DisableCharacterCollision(13000851);
        DisableCharacterCollision(13000852);
        ForceCharacterDeath(13000850, false);
        ForceCharacterDeath(13000851, false);
        ForceCharacterDeath(13000852, false);
        EndEvent();
    }
L6:
    DisableCharacterAI(13000851);
    DisableCharacterAI(13000852);
    DisableCharacter(13000850);
    DisableCharacterGravity(13000850);
    DisableCharacterCollision(13000850);
    if (!EventFlag(13000851)) {
        DisableCharacter(13000851);
        DisableCharacter(13000852);
        ForceAnimationPlayback(13000851, 30001, true, false, false);
        ForceAnimationPlayback(13000852, 30001, true, false, false);
        WaitFor(
            (PlayerIsInOwnWorld() && InArea(10000, 13002851))
                || HasDamageType(13000851, 10000, DamageType.Unspecified)
                || HasDamageType(13000852, 10000, DamageType.Unspecified));
        SetNetworkconnectedEventFlagID(13000851, ON);
        EnableCharacter(13000851);
        EnableCharacter(13000852);
        ForceAnimationPlayback(13000851, 20001, false, false, false);
        ForceAnimationPlayback(13000852, 20001, false, false, false);
    } else {
L7:
        WaitFor(
            EventFlag(13002855)
                || InArea(10000, 13002850)
                || InArea(10000, 13002852)
                || InArea(10000, 13002853));
    }
L8:
    EnableCharacter(13000850);
    DisableCharacterAI(13000850);
    EnableCharacterAI(13000851);
    EnableCharacterAI(13000852);
    SetNetworkUpdateRate(13000850, true, CharacterUpdateFrequency.AlwaysUpdate);
    SetNetworkUpdateRate(13000851, true, CharacterUpdateFrequency.AlwaysUpdate);
    SetNetworkUpdateRate(13000852, true, CharacterUpdateFrequency.AlwaysUpdate);
    CreateReferredDamagePair(13005851, 13000850);
    DisplayBossHealthBar(Enabled, 13000850, 0, 903575000);
});

Show event with comments

Hide

$Event(13002860, Restart, function() {
    // This first block is executed only when the boss defeat flag is set. Otherwise, skip it.
   if (EventFlag(13000850)) {
        // The following commands collectively prevent the 3 enemies from appearing, even
        // killing them to make sure they stay inactive.
        // Note again that the healthbar enemy 13000850 uses the same number as the defeat
        // flag 13000850, but they refer to different things.
        DisableCharacter(13000850);
        DisableCharacter(13000851);
        DisableCharacter(13000852);
        DisableCharacterCollision(13000850);
        DisableCharacterCollision(13000851);
        DisableCharacterCollision(13000852);
        ForceCharacterDeath(13000850, false);
        ForceCharacterDeath(13000851, false);
        ForceCharacterDeath(13000852, false);
        // Now the boss enemies are all disabled in this branch, so stop the event here.
        EndEvent();
    }
L6:
    // If we got here instead, event flag 13000850 has not yet been turned on.
    // This disables the AI for the Godskins you fight, mainly for non-first encounters.
    // Without this, the enemies would detect you and attack you through the fog gate.
    DisableCharacterAI(13000851);
    DisableCharacterAI(13000852);
    // This sets up the healthbar entity in its initial disabled state.
    // "Character enable/disable" broadly includes the enemy being visible, being damageable,
    // and having a running AI script. When disabled, it's almost like the enemy isn't even
    // there. The enemy still has a position in the map, however, and you can change many
    // attributes like AI/gravity/speffects even in this state, despite no immediate effect.
    DisableCharacter(13000850);
    // Because the healthbar entity is below the map, we want to make sure it doesn't fall
    // to its death and end the fight prematurely, by disabling its gravity.
    DisableCharacterGravity(13000850);
    // Among other things, this controls whether the player can interact with an enemy (but
    // not the other way around), and in combination with character enable state, disabling it
    // means an enemy truly is not detectable in any way. This is not reenabled below,
    // as the healthbar entity is not meant to be attackable.
    DisableCharacterCollision(13000850);
    // Now, we branch depending on whether it's a first or subsequent encounter.
    if (!EventFlag(13000851)) {
        // It's the first encounter, where you can just walk in.
        // We disable the duo enemies so that you can enter the room and they are
        // nowhere to be seen, despite being positioned right there. This also sets
        // their initial animation to an invisible one, for good measure.
        DisableCharacter(13000851);
        DisableCharacter(13000852);
        // Click on an instruction in DarkScript3 to see what the arguments mean.
        // The "true" here means that the animation should loop indefinitely.
        ForceAnimationPlayback(13000851, 30001, true, false, false);
        ForceAnimationPlayback(13000852, 30001, true, false, false);
        // This is the point at which the fight properly starts. It's worth keeping in
        // mind that all of the above happens the moment you load into the map. If the script
        // got to this point, it will wait around forever for this condition to become true.
        // There are three ways the fight can start the first time: either the host is in
        // region 13002851, or either of the Godskins are damaged. Normally, they can't be
        // damaged while invisible, so the latter conditions are more of a failsafe.
        // The normal trigger has two parts. First, it only passes if the player is not a summon
        // or invader: if they're in their own world. Second, it requires the local player entity
        // (always represented as 10000) to be in region 13002851. Locate it by EntityID in
        // DSMapStudio. It has this internal name: 領域 ボス戦 神肌タッグ初戦開始
        // The important part is at the end, which translates to "first fight start".
        // It's a composite region, so to visualize it you'll have to search for its children
        // region by Name.
        WaitFor(
            (PlayerIsInOwnWorld() && InArea(10000, 13002851))
                || HasDamageType(13000851, 10000, DamageType.Unspecified)
                || HasDamageType(13000852, 10000, DamageType.Unspecified));
        // Now set the "encountered" flag, so this routine won't be run again.
        // Note the "Networkconnected" part which sends this flag change as a packet to other 
        // players. This is because this also has the effect of making fog gates appear and
        // summons able to join, discussed below.
        SetNetworkconnectedEventFlagID(13000851, ON);
        // Now, the duo are reenabled. This also plays a special fade-in animation on them,
        // rather than just popping them into existence. You can use ForceAnimationPlayback at
        // any time to see what an animation looks like in-game.
        EnableCharacter(13000851);
        EnableCharacter(13000852);
        ForceAnimationPlayback(13000851, 20001, false, false, false);
        ForceAnimationPlayback(13000852, 20001, false, false, false);
    } else {
L7:
        // In the subsequent encounter, the duo are just sitting around waiting for their AI to be
        // enabled. Here, we wait for event flag 13002855 to be set from common_func (discussed
        // below). This particular flag is turned on when the player traverses any of the boss fog
        // gates. Alternatively, as a failsafe, the player can step into any region on the inside
        // of the three fog gates.
        WaitFor(
            EventFlag(13002855)
                || InArea(10000, 13002850)
                || InArea(10000, 13002852)
                || InArea(10000, 13002853));
    }
L8:
    // Now, regardless of whether first or second encounter, the fight can start!
    // The healthbar entity is enabled, meaning the player can now indirectly deal damage
    // to it. It also becomes visible as a result, but it's still hidden below the arena.
    EnableCharacter(13000850);
    // The healthbar's entity AI was not previously disabled, so enabling it will also enable
    // its AI. So, explicitly disable its AI now. The enemy doesn't actually *have* an AI script,
    // (NpcThinkParam is 1) but this instruction is doing the safe thing regardless.
    DisableCharacterAI(13000850);
    // Enable godskin AIs: they can now find the player and choose attacks!
    EnableCharacterAI(13000851);
    EnableCharacterAI(13000852);
    // Now that the boss fight has started, this gives high rendering/network priority
    // to the boss enemies. This minimizes jank and issues from lag.
    SetNetworkUpdateRate(13000850, true, CharacterUpdateFrequency.AlwaysUpdate);
    SetNetworkUpdateRate(13000851, true, CharacterUpdateFrequency.AlwaysUpdate);
    SetNetworkUpdateRate(13000852, true, CharacterUpdateFrequency.AlwaysUpdate);
    // This command is specific to the Godskin Duo's mechanics, and is one of the most
    // important lines in this event. It means that whenever you damage entity 13005851,
    // the same damage is dealt to enemy 13000850. But wait, what's 13005851?
    // It's an entity group id, which is a way for scripts to refer to multiple entities
    // at once. The two godskins both have EntityGroupIDs[1] set to 13005851. So this
    // command is equivalent to creating *two* referred damage pairs, one from
    // 13000851 to 13000850, and another from 13000852 to 13000850.
    CreateReferredDamagePair(13005851, 13000850);
    // Finally, the boss healthbar is displayed for the healthbar entity.
    // Note that this is purely cosmetic, and doesn't affect the fight in any way other than
    // showing the player their progress. Doing this for an entity also removes their
    // mini overhead healthbar, but in this case, the two godskins still keep theirs.
    // The "0" means that the 0th slot is used (use 1 2 etc. to show multiple at once)
    // and 903575000 is an entry in NpcName.fmg in item.msgbnd.dcx, "Godskin Duo".
    DisplayBossHealthBar(Enabled, 13000850, 0, 903575000);
});

Make sure to click on “Show event with comments” before continuing on!

Event 13002850: End Godskin Duo

The defeat event also follows a very regular structure for all Elden Ring bosses with fixed boss arenas. The most important thing this event does is turn the boss defeat flag 13000850 on, but it's responsible for many other rewards and effects as well.

Show event

Hide

$Event(13002850, Restart, function() {
    EndIf(EventFlag(13000850));
    WaitFor(CharacterHPValue(13000850) <= 0);
    ForceCharacterDeath(13000851, false);
    ForceCharacterDeath(13000852, false);
    SetEventFlagID(13002854, ON);
    WaitFixedTimeSeconds(4);
    PlaySE(13000850, SoundType.SFX, 888880000);
    WaitFor(CharacterDead(13000850));
    HandleBossDefeatAndDisplayBanner(13000850, TextBannerType.GreatEnemyFelled);
    SetEventFlagID(13000850, ON);
    SetEventFlagID(9114, ON);
    if (PlayerIsInOwnWorld()) {
        SetEventFlagID(61114, ON);
    }
});

Show event with comments

Hide

$Event(13002850, Restart, function() {
    // If they're already defeated, there's nothing to do here, so stop the event.
    EndIf(EventFlag(13000850));
    // Otherwise, wait around here until the healthbar entity's HP is reduced to 0.
    WaitFor(CharacterHPValue(13000850) <= 0);
    // Once this is the case, kill both of the godskins. This causes them to collapse
    // even while you're fighting them. The "false" means to not award runes on death,
    // but they both drop 0 runes anyway.
    ForceCharacterDeath(13000851, false);
    ForceCharacterDeath(13000852, false);
    // We'll see flag 13002854 later: it's used to prevent the godskins from respawning
    // when the healthbar enemy has already died. We can't use the normal defeat flag
    // 13000850 for that purpose, because there is a delay in setting it, and we don't want
    // premature respawns in those few seconds' time.
    SetEventFlagID(13002854, ON);
    // A dramatic pause from when the enemies start playing their death animations
    // to when the boss rewards are dropped and banner is displayed.
    WaitFixedTimeSeconds(4);
    // The "fwoosh" death knell is actually triggered by an event script.
    // Try playing it on its own!
    PlaySE(13000850, SoundType.SFX, 888880000);
    // The final step in making sure a boss has died: wait for its death animation to
    // register it as dead. An enemy reaching 0 HP doesn't mean that it's died, and you
    // won't get runes from that alone. It actually has to play its death animation up to
    // some required point. Depending on the boss fight, this check may just be a formality,
    // as the enemy may already be dead. Effectively, this amounts to waiting for whatever
    // takes longer: the death animation or the WaitFixedTimeSeconds.
    WaitFor(CharacterDead(13000850));
    // This command does many things, including displaying the "Great Enemy Felled" banner,
    // awarding the boss soul drop based on GameAreaParam, disabling boss healthbars, and
    // probably also sending summons home.
    HandleBossDefeatAndDisplayBanner(13000850, TextBannerType.GreatEnemyFelled);
    // Finally! We set the boss defeat flag. Now the boss is truly considered defeated.
    SetEventFlagID(13000850, ON);
    // This is a separate flag which is used to mean "award the Godskin Duo boss item drop
    // after some delay". I'll skip the details for now, but it's done in common.emevd.
    SetEventFlagID(9114, ON);
    if (PlayerIsInOwnWorld()) {
        // Finally, for hosts only, set a flag to record that Godskin Duo was defeated.
        // This flag is in the 6xxxx range, meaning it persists across NG cycles.
        // I'm not sure if this flag is used for anything. It doesn't seem like it.
        SetEventFlagID(61114, ON);
    }
});

Event 13002861: Music change

Not every boss has an event like this, but this approach is used in any boss where there is some condition for the soundtrack to “heat up”.

Show event

Hide

$Event(13002861, Restart, function() {
    EndIf(EventFlag(13000850));
    WaitFor(HPRatio(13000850) <= 0.5 || EventFlag(13002873) || EventFlag(13002874));
    SetEventFlagID(13002852, ON);
});

Show event with comments

Hide

$Event(13002861, Restart, function() {
    // As usual, ignore the rest if the boss is already defeated.
    EndIf(EventFlag(13000850));
    // Here, there are three possible triggers for the music to move to phase 2:
    // 1. The total boss HP is down to 50%.
    // 2. Godskin Apostle respawns (flag 13002873 set)
    // 3. Godskin Noble respawns (flag 13002874 set)
    // In most fights, 2 or 3 will trigger it. These flags are set by other events below.
    WaitFor(HPRatio(13000850) <= 0.5 || EventFlag(13002873) || EventFlag(13002874));
    // Finally, this does the music change. However, it doesn't do it directly. There's a
    // different event in common_func doing the hard work here (calling SetBossBGM).
    SetEventFlagID(13002852, ON);
});

Event 13002890: Godskin summoning after one dies

Now, it's time to discuss event parameters. These can be hard to wrap your head around. It allows for multiple copies of an event to run at the same time, but to behave in slightly different ways, or apply to different enemies.

Parameterized events are a natural choice for Godskin Duo, because there are two of them. In one case, you'd want Godskin Noble to see that the Apostle has died and summon them, and for the Apostle to respawn. In another case, you'd want the exact same thing, but with the enemies swapped. Making an event with parameters allows you to reuse the same logic in both cases.

The next event starts out like this:

$Event(13002890, Restart, function(X0_4, X4_4, X8_4, X12_4) {
    ...
});

Note the extra X0_4, X4_4, and so on. Remember that all events must be initialized in order to be active. In this case, there is extra data passed in during initialization! If you Ctrl+Click on the event id here, you can hop to the initializations, and you'll see these two lines:

InitializeEvent(0, 13002890, 13002944, 13000851, 13000852, 15504);
InitializeEvent(1, 13002890, 13002945, 13000852, 13000851, 15454);

0 or 1 is the event slot, and 13002890 is the event id, and all of the rest of the arguments are copied into X0_4 X4_4 etc. in order. What this means is that there are two copies of the event running simultaneously, which can run completely independently from each other. One of them tracks Apostle, the other tracks Noble. (Side note: the syntax for this will be improved in future versions of MattScript. In a few cases meanwhile, the arguments won't exactly match, since they're technically byte offsets.) Now, onto the events.

In this event, when one of the godskins is dead for 20 seconds, prompt the other one to resurrect them. If it helps, you can think of specific values (like X4_4 being 13000851 and X8_4 being 13000852) as you read through the script. Note that the meaning of X4_4 can change completely from one event to another; the name is only based on the index of the parameter and doesn't mean anything by itself.

Show event

Hide

$Event(13002890, Restart, function(X0_4, X4_4, X8_4, X12_4) {
    EndIf(!PlayerIsInOwnWorld());
    EndIf(EventFlag(13000850));
    EndIf(EventFlag(13002854));
    WaitFor(!EventFlag(X0_4) && CharacterDead(X4_4));
    WaitFixedTimeSeconds(20);
    if (CharacterHPValue(X8_4) <= 0) {
        RestartEvent();
    }
L0:
    SetSpEffect(X8_4, X12_4);
    SetNetworkconnectedEventFlagID(X0_4, ON);
    RestartEvent();
});

Show event with comments

Hide

// X0_4: The event flag to set indicating that X4_4 should be respawned
// X4_4: Entity id of the godskin to respawn
// X8_4: Entity id of the godskin doing the summoning
// X12_2: A speffect to apply to X8_4, to prompt them to do the summon animation
$Event(13002890, Restart, function(X0_4, X4_4, X8_4, X12_4) {
    // Only the host player can run this logic, to avoid desyncs between godskins spawning.
    EndIf(!PlayerIsInOwnWorld());
    // Stop if boss already defeated, as per usual.
    EndIf(EventFlag(13000850));
    // This is a separate measure to prevent respawning once the healthbar entity has reached
    // 0 HP, but before it's technically considered "dead" by the game.
    EndIf(EventFlag(13002854));
    // The first time through, X0_4 will be off, so this is really just waiting for the
    // X4_4 godskin to die. When X4_4 is actively respawning, however, X0_4 is turned on.
    // So this is checking that the X4_4 godskin is dead but no respawn is scheduled yet.
    WaitFor(!EventFlag(X0_4) && CharacterDead(X4_4));
    // The player has 20 seconds alone with X8_4 before they try to summon X4_4.
    WaitFixedTimeSeconds(20);
    if (CharacterHPValue(X8_4) <= 0) {
        // If the summoner is dead, they can't summon, so try again.
        RestartEvent();
    }
L0:
    // Finally, signal that the respawn can occur.
    // This involves a two-way interaction between event scripts and AI scripts using SpEffects.
    // Remember that a speffect is like a status flag set on an individual enemy or on the player.     
    // Whenever the godskin AI script is finished with an attack and moves on to the next one,
    // it will see that e.g. speffect 15504 is set (for Godskin Apostle), and it will prioritize the
    // summon animation above all others.
    SetSpEffect(X8_4, X12_4);
    // Now turn on X0_4, indicating to a separate event that respawning X4_4 is requested.
    // We'll get to that event shortly.
    SetNetworkconnectedEventFlagID(X0_4, ON);
    // Go back to the top of the event for the next time respawning needs to occur.
    // Note that the condition won't repeat itself because X0_4 has now been turned on,
    // and it won't be turned off until X4_4 is no longer dead via InvokeEnemyGenerator.
    RestartEvent();
});

Event 13002891: Godskin spawning in after being summoned

This event also has two initializations, and it works together with 13002890 to finish the summon+resurrect mechanic.

It respawns a godskin when it has been requested by the other alive godskin.

InitializeEvent(0, 13002891, 13002944, 13003851, 13000852, 15506, 13000851, 13002873);
InitializeEvent(1, 13002891, 13002945, 13003852, 13000851, 15456, 13000852, 13002874);

Show event

Hide

$Event(13002891, Restart, function(X0_4, X4_4, X8_4, X12_4, X16_4, X20_4) {
    EndIf(!PlayerIsInOwnWorld());
    EndIf(EventFlag(13000850));
    EndIf(EventFlag(13002854));
    WaitFor((CharacterHasSpEffect(X8_4, X12_4) || CharacterDead(X8_4)) && EventFlag(X0_4));
    EndIf(EventFlag(13000850));
    EndIf(EventFlag(13002854));
    InvokeEnemyGenerator(X4_4);
    SetNetworkconnectedEventFlagID(X0_4, OFF);
    SetNetworkconnectedEventFlagID(X20_4, ON);
    SetNetworkUpdateRate(X16_4, true, CharacterUpdateFrequency.AlwaysUpdate);
    RestartEvent();
});

Show event with comments

Hide

// X0_4: The event flag to check indicating that X4_4 should be respawned
// X4_4: Generator id (not entity id!) of the godskin to respawn
// X8_4: Entity id of the godskin doing the summoning
// X12_2: A speffect to check from X8_4, indicating they're doing the summon animation
// X16_4: Entity id of the godskin to respawn
// X20_4: Flag to set once the godskin has been respawned, used for music change
$Event(13002891, Restart, function(X0_4, X4_4, X8_4, X12_4, X16_4, X20_4) {
    // The same checks as event 13002890.
    EndIf(!PlayerIsInOwnWorld());
    EndIf(EventFlag(13000850));
    EndIf(EventFlag(13002854));
    // This condition is slightly easier to read in reverse.
    // First, flag X0_4 (requesting respawn) must be set.
    // Second, either the summoning godskin is doing its summon animation which sets the
    // required speffect (15456 for Godskin Apostle), or it must have been interrupted from
    // doing so because it's died before it could even get it off.
    // Either way, once 20 seconds have elapsed in the previous event and X0_4 has been set,
    // this event *will* go through with respawning the dead godskin, one way or another.
    WaitFor((CharacterHasSpEffect(X8_4, X12_4) || CharacterDead(X8_4)) && EventFlag(X0_4));
    // Again, do not proceed with respawning if the fight is over.
    EndIf(EventFlag(13000850));
    EndIf(EventFlag(13002854));
    // This is what invokes the generator, returning the godskin from dead to alive.
    // Generators are found in DSMapStudio under "Event", not to be confused with events
    // in event scripts! It's a scripted behavior in the map itself. If you find the generator
    // with EntityID 13003852, it references c3570_9000, the name of the enemy with EntityID
    // 13000852 (aka Godskin Noble). Most generators respawn enemies on a timer, such as Four
    // Kings and Rennala phase 1, but this is manual.
    InvokeEnemyGenerator(X4_4);
    // We've respawned the enemy, so turn off the flag requesting it.
    SetNetworkconnectedEventFlagID(X0_4, OFF);
    // Turn on the flag which indicates this godskin has been respawned. This flag is one
    // of the possible triggers for phase 2 music to start.
    SetNetworkconnectedEventFlagID(X20_4, ON);
    // Some attributes of enemies are lost when they die and regenerate. Update rate is one
    // of them, so manually set it here. (SetSpEffect calls are also forgotten.)
    SetNetworkUpdateRate(X16_4, true, CharacterUpdateFrequency.AlwaysUpdate);
    // Loop around and wait for X0_4 to be set again.
    RestartEvent();
});

If you want to quickly verify the SpEffect interaction yourself, you can use Elden Ring Script SpEffect Animations. To summarize: in the case of Godskin Apostle signaling for Godskin Noble to respawn, Apostle emits SpEffect 15456. This corresponds to this line:

SpEffect 15456 (c3560): anim 3038 frame 96-99

Then you can use DSLuaDecompiler (linked above) to decompile 356000_battle.lua from script/356000_battle.luabnd.dcx and see that animation 3038 corresponds to Act18, In Goal.Activate, having SpEffect 15454 from the previous event leads to Act18s weight being set to 100%.

Event 13002892: Godskins spawning in after both die

From perusing the above scripts, you may have noticed a potential edge case. When one godskin is alive and the other is dead, they summon each other after 20 seconds. But what if they both die? Well, this final event includes handling for just that edge case. It's long, but the logic is straightforward.

When both godskins are dead, this event randomly picks one of them to respawn without an explicit summon. The randomly respawned godskin will then try to respawn their buddy using the above events.

Show event

Hide

$Event(13002892, Restart, function() {
    EndIf(!PlayerIsInOwnWorld());
    EndIf(EventFlag(13000850));
    EndIf(EventFlag(13002854));
    WaitFor(
        CharacterDead(13000851)
            && CharacterDead(13000852)
            && !EventFlag(13002944)
            && !EventFlag(13002945));
    EndIf(EventFlag(13000850));
    WaitFixedTimeSeconds(10);
    EndIf(EventFlag(13000850));
    EndIf(EventFlag(13002854));
    RandomlySetEventFlagInRange(13002875, 13002876, ON);
    GotoIf(L0, EventFlag(13002875));
    GotoIf(L1, EventFlag(13002876));
L0:
    InvokeEnemyGenerator(13003851);
    SetEventFlagID(13002873, ON);
    SetEventFlagID(13002875, OFF);
    RestartEvent();
L1:
    InvokeEnemyGenerator(13003852);
    SetEventFlagID(13002874, ON);
    SetEventFlagID(13002876, OFF);
    RestartEvent();
});

Show event with comments

Hide

$Event(13002892, Restart, function() {
    // Similar checks for host-only and for avoiding respawn after boss defeat.
    EndIf(!PlayerIsInOwnWorld());
    EndIf(EventFlag(13000850));
    EndIf(EventFlag(13002854));
    // The big condition here. Remember what these ids are:
    // 13000851: The Godskin Apostle you fight
    // 13000852: The Godskin Noble you fight
    // 13002944: The flag (X0_4 in event 13002891) used to signal respawning the Apostle.
    // 13002945: The flag (X0_4 in event 13002891) used to signal respawning the Noble.
    // In other words, both godskins must be dead, and there must not be a respawn
    // already in progress.
    WaitFor(
        CharacterDead(13000851)
            && CharacterDead(13000852)
            && !EventFlag(13002944)
            && !EventFlag(13002945));
    // More checks to avoid respawn after boss defeat, and also a 10 second wait where
    // the player is standing around with no enemies to fight.
    EndIf(EventFlag(13000850));
    WaitFixedTimeSeconds(10);
    EndIf(EventFlag(13000850));
    EndIf(EventFlag(13002854));
    // Now for an interesting use of event flags: adding randomness to event script execution.
    // These flags are not used outside of this event, and only exist to be set/checked here.
    // Effectively, if 13002875 and 13002876 are both off, this instruction results in one of them
    // getting turned on with 50/50 odds. If there are n flags in the range, each one has a 1/n
    // chance of being set to ON.
    RandomlySetEventFlagInRange(13002875, 13002876, ON);
    // This is a goto command, which jumps to the given label when the condition is true,
    // and continues on to the next line otherwise.
    // MattScript usually prefers to replace gotos with if statements during decompilation,
    // but in a few cases like this, gotos are usually clearer, so they are preserved.
    GotoIf(L0, EventFlag(13002875));
    GotoIf(L1, EventFlag(13002876));
L0:
    // Respawn Godskin Apostle using its generator.
    InvokeEnemyGenerator(13003851);
    // This turns the music change flag on, and resets the random flag for the next go-around.
    SetEventFlagID(13002873, ON);
    SetEventFlagID(13002875, OFF);
    RestartEvent();
L1:
    // Respawn Godskin Noble using its generator, analogous to Apostle.
    InvokeEnemyGenerator(13003852);
    SetEventFlagID(13002874, ON);
    SetEventFlagID(13002876, OFF);
    RestartEvent();
});

I've left out event 13002859, which is how Bernahl enters the fight after he's summoned. It is not all that complicated, and you can see how event scripts control him throughout the process. There's no specific logic relevant to the Godskin Duo fight, though, so I've excluded it here.

Fog gates

Let's talk about common_func, which is an emevd file with no initializations. Instead, it contains a bunch of parameterized events which get initialized from other event scripts, and its main purpose is to contain heavily reused events which would otherwise have to be copied to a hundred different event scripts.

DS1 had no common_func and as a result it contained a lot of duplication. The same fog gate event was copied in a dozen different maps, although it could be reused for multiple bosses within each map. Bloodborne introduced a prototype version of common_func which allowed all Chalice dungeons to share the same set of events with map-specific parameters. DS3 properly introduced common_func which was usable in all maps. (DS2, if you're curious, uses ESD instead of emevd, also with a lot of duplication.)

Fog gates are a good example to start with, though the logic behind them is quite complicated. Almost all boss fog gates in all maps use the same common_func events, and most map-specific boss start events wait for the fog gate entry flag to get set in order for the fight to start. This is flag 13002855 in the case of Godskin Duo.

Event 9005800 has 3 initializations for Godskin Duo in m13_00_00_00, one for each fog gate you can enter. It has just over 100 initializations in the entire game.

This event allows a host player to traverse a boss fog gate, and sets a flag once they do. Summoned players use a separate event 9005801 to traverse the fog gate after the host does, meaning when flag X12_4 here is set. They also run this event, but it has no effect for them.

InitializeCommonEvent(0, 9005800, 13000850, 13001850, 13002850, 13002855, 13005850, 10000, 13000851, 13002851);
InitializeCommonEvent(0, 9005800, 13000850, 13001852, 13002852, 13002855, 13005850, 10000, 13000851, 13002851);
InitializeCommonEvent(0, 9005800, 13000850, 13001853, 13002853, 13002855, 13005850, 10000, 13000851, 13002851);

Show event

Hide

$Event(9005800, Restart, function(X0_4, X4_4, X8_4, X12_4, X16_4, X20_4, X24_4, X28_4) {
    if (!EventFlag(X0_4)) {
        WaitFixedTimeFrames(1);
        if (X24_4 != 0) {
            GotoIf(L0, EventFlag(X24_4));
            if (X28_4 != 0) {
                areaFlag |= InArea(10000, X28_4);
            }
            areaFlag |= EventFlag(X24_4);
            WaitFor((areaFlag && PlayerIsInOwnWorld()) || EventFlag(X0_4));
            RestartIf(EventFlag(X0_4));
            Goto(L1);
        }
L0:
        if (PlayerIsInOwnWorld()) {
            WaitFor(
                EventFlag(X0_4)
                    || (PlayerIsInOwnWorld() && !EventFlag(X0_4) && ActionButtonInArea(X20_4, X4_4)));
            GotoIf(L2, !PlayerIsInOwnWorld());
            RestartIf(EventFlag(X0_4));
            SuppressSoundForFogGate(5);
            if (!CharacterHasSpEffect(10000, 4250)) {
                RotateCharacter(10000, X8_4, 60060, true);
            } else {
                RotateCharacter(10000, X8_4, 60060, false);
            }
        }
L3:
        GotoIf(L1, EventFlag(X12_4));
        time = ElapsedSeconds(3);
        WaitFor(
            ((InArea(10000, X8_4) || time) && PlayerIsInOwnWorld() && !EventFlag(X0_4))
                || EventFlag(X0_4));
        RestartIf(EventFlag(X0_4));
        RestartIf(time.Passed);
L1:
        if (PlayerIsInOwnWorld()) {
            IssueBossRoomEntryNotification();
            SetNetworkUpdateAuthority(X16_4, AuthorityLevel.Forced);
        }
L2:
        ActivateMultiplayerdependantBuffs(X16_4);
        SetNetworkconnectedEventFlagID(X12_4, ON);
        EndIf(!PlayerIsInOwnWorld());
        RestartEvent();
    }
L10:
    EndIf(!PlayerIsInOwnWorld());
    WaitFor(
        PlayerIsInOwnWorld()
            && EventFlag(X0_4)
            && (HasMultiplayerState(MultiplayerState.Invasion)
                || HasMultiplayerState(MultiplayerState.InvasionPending))
            && ActionButtonInArea(10000, X4_4));
    RotateCharacter(10000, X8_4, 60060, true);
    SendInvadingPhantomsHome(0);
    RestartEvent();
});

Show event with comments

Hide

// X0_4: Boss defeat event flag
// X4_4: Entity id for the fog gate asset
// X8_4: Entity id for the region on the inside of the fog gate
// X12_4: Event flag to turn on when the host has traversed the fog gate, or when a summon
//        becomes eligible to enter after the host.
// X16_4: Entity id (usually group entity id) of the boss, to buff boss enemies for multiplayer.
// X20_4: Action button param id for entering the fog gate. This determines when the "Traverse"
//        prompt shows up, usually based on how wide and tall the fog gate is.
// X24_4: (optional) The "boss encountered?" flag. If this is specified, the fog gate won't
//        allow traversal until this flag is set. This can also be a temporary flag (reset after
//        quit-out) which is used to trigger the fight indirectly, like with Radagon.
// X28_4: (optional) If the encounter flag is specified, a region entity id which can
//        alternatively be used to start the fight. This might be useful if there's some
//        delay in setting the encounter flag.
$Event(9005800, Restart, function(X0_4, X4_4, X8_4, X12_4, X16_4, X20_4, X24_4, X28_4) {
    // Most of this event takes place when the boss is *not* defeated.
    if (!EventFlag(X0_4)) {
        // A few events have a hacky 1-frame wait like this. Note that this is right before X24_4
        // is used. This allows X24_4 to be set in a separate event and this event will always see
        // the correct value, instead of being dependent on which is initialized first.
        // This uses "game logic" frames, which run at 30 FPS.
        WaitFixedTimeFrames(1);
        // This event has a decent amount of jumping around. X24_4 is an optional argument
        // marking a custom first encounter, and this jumps below if it's zero or on. In other
        // words, the following logic applies only if it's present (non-zero) and off.
        // One exceptional case is that Radagon's fog gate entrance uses a custom event for the
        // gate interaction, since it's not a real gate, and X24_4 is a temporary flag (19002801)
        // which resets after each attempt. The custom event sets the flag to start the fight.
        if (X24_4 != 0) {
            GotoIf(L0, EventFlag(X24_4));
            // If we got here, it means it's a custom first encounter.
            // This is a condition variable automatically named "areaFlag", used here to construct a
            // condition based on another condition. Remember that X28_4 is optional, meaning it can be
            // set to 0. It would be nonsensical to check InArea(10000, 0), so that's how you get
            // dynamic conditions like this.
            // In this case, if X28_4 is non-zero, then "areaFlag" can be used in a later condition
            // to mean "check if either the player has entered X28_4 or event flag X24_4 is on".
            // If X28_4 is 0, it only means "check if event flag X24_4 is on".
            if (X28_4 != 0) {
                areaFlag |= InArea(10000, X28_4);
            }
            areaFlag |= EventFlag(X24_4);
            // The areaFlag variable, as defined above, is now evaluated by the host player.
            // The other way this condition can pass is for the boss to be defeated. This could
            // happen with some glitches (remember the start and end events are separate). This
            // condition may also be used by summoned players.
            WaitFor((areaFlag && PlayerIsInOwnWorld()) || EventFlag(X0_4));
            // If the boss is already defeated, don't continue on to starting the fight.
            RestartIf(EventFlag(X0_4));
            // Since this a custom first encounter, skip the fog gate entry logic.
            Goto(L1);
        }
L0:
        // If we got here, this means this is a regular fog gate entrance (not by summons).
        // There are a ton of instances of PlayerIsInOwnWorld used redundantly all over the
        // place, so I'll mostly be ignoring them. I am pretty sure it's not possible for
        // host status to change while an event is executing.
        if (PlayerIsInOwnWorld()) {
            // This is the big condition. First, as explained above, it passes when the boss
            // is already defeated. Otherwise, the main check is ActionButton, which shows
            // the "Traverse" prompt and passes when the player uses it.
            // X20_4 is a reference to ActionButtonParam, which specifies the prompt text
            // to show, where on the fog gate to attach itself to, and the allowed range/angles
            // of entry. The second argument is the fog gate entity itself.
            // ActionButtonInArea is unusual because it's a condition that actually causes
            // a change in the game world while evaluating it: if all previous subconditions
            // are met, it *makes* the prompt to appear.
            WaitFor(
                EventFlag(X0_4)
                    || (PlayerIsInOwnWorld()
                        && !EventFlag(X0_4)
                        && ActionButtonInArea(X20_4, X4_4)));
            GotoIf(L2, !PlayerIsInOwnWorld());
            RestartIf(EventFlag(X0_4));
            // This command is only used for fog gate entry, and is active for 5 seconds.
            SuppressSoundForFogGate(5);
            // I'm not sure when this speffect applies. Either way, RotateCharacter is used,
            // which does a few things here:
            // 1. Forces playback of animation 60060 on the player.
            // 2. At the same time, makes the player rotate towards the fog gate (X8_4),
            //    in case they're standing at an odd angle.
            // 3. The animation plays which temporarily disables character collision, oriented
            //    towards the fog gate center, allowing the player to reach the other side.
            if (!CharacterHasSpEffect(10000, 4250)) {
                RotateCharacter(10000, X8_4, 60060, true);
            } else {
                RotateCharacter(10000, X8_4, 60060, false);
            }
        }
        // Side note: this is label L3, but you'll notice there's no "GotoIf(L3)" line above.
        // This *used* to be a goto, but it was replaced the above if statement.
        // When this is recompiled, it will turn into a goto L3 again.
L3:
        // The following still applies only to regular fog gate entry.
        // The only real important condition here is InArea(10000, X8_4): the fog gate
        // animation succeeded when the player reaches the region on the other side.
        // Otherwise, if it didn't happen after 3 seconds, something must have gone wrong,
        // so restart the event to allow them to try again.
        GotoIf(L1, EventFlag(X12_4));
        // Here we see another condition variable. It becomes true after the WaitFor has been
        // waiting for the specified number of the seconds.
        time = ElapsedSeconds(3);
        // The condition itself: ignoring the host stuff and boss defeat stuff, it's waiting
        // for either the player to enter area X8_4 or for 3 seconds to elapse.
        WaitFor(
            ((InArea(10000, X8_4) || time) && PlayerIsInOwnWorld() && !EventFlag(X0_4))
                || EventFlag(X0_4));
        RestartIf(EventFlag(X0_4));
        // This is the other use of condition variables: to detect whether this particular
        // subcondition was true at the time the WaitFor succeeded. If the player entered region
        // X8_4 before 3 seconds elapsed, it will be false, and execution will continue on.
        RestartIf(time.Passed);
L1:
        // The following now applies to both custom first encounters and fog gate entry.
        if (PlayerIsInOwnWorld()) {
            // Handle boss start multiplayer stuff.
            // The room entry notification sends invaders home, among other things.
            IssueBossRoomEntryNotification(0);
            SetNetworkUpdateAuthority(X16_4, AuthorityLevel.Forced);
        }
L2:
        // This scales up the boss according to how many summons are present.
        // For multi-boss fights, X16_4 is a group id of all of the boss enemies.
        ActivateMultiplayerdependantBuffs(X16_4);
        // Finally - set the flag to communicate to other event scripts that fog gate entry
        // (or custom first encounter) has completed.
        SetNetworkconnectedEventFlagID(X12_4, ON);
        EndIf(!PlayerIsInOwnWorld());
        RestartEvent();
    }
L10:
    // This seems to be a fallback case to send invaders home if they somehow stick around
    // or invade after the boss is already defeated. In this case, X4_4 will be re-enabled,
    // and the player will pass through into an empty boss room.
    EndIf(!PlayerIsInOwnWorld());
    WaitFor(
        PlayerIsInOwnWorld()
            && EventFlag(X0_4)
            && (HasMultiplayerState(MultiplayerState.Invasion)
                || HasMultiplayerState(MultiplayerState.InvasionPending))
            && ActionButtonInArea(10000, X4_4));
    RotateCharacter(10000, X8_4, 60060, true);
    SendInvadingPhantomsHome(0);
    RestartEvent();
});

So the above event allows you pass through a solid wall using RotateCharacter, which forces an animation which ignores walls. It assumes the fog gate is blocking your way, but event 9005811 is the event that's responsible for actually making the fog gate appear and disappear. It has 5 initializations in m13_00_00_00, one for each fog gate, including those on the upper level of the Dragon Temple:

InitializeCommonEvent(0, 9005811, 13000850, 13001850, 5, 13000851);
InitializeCommonEvent(0, 9005811, 13000850, 13001852, 5, 13000851);
InitializeCommonEvent(0, 9005811, 13000850, 13001853, 5, 13000851);
InitializeCommonEvent(0, 9005811, 13000850, 13001854, 5, 13000851);
InitializeCommonEvent(0, 9005811, 13000850, 13001855, 5, 13000851);

This event controls when the fog gate shows up and disappears using the ChangeAssetEnableState and CreateObjectfollowingSFX commands. I won't explain the details of the event here, as it behaves in a bunch of different complicated ways depending on whether you're a host, a summon, or the invader. Still, the arguments are as follows:

// X0_4: The boss defeat flag. When this is on, the fog gate does not show up.
// X4_4: Entity id for the fog gate asset
// X8_4: SFX id which is responsible for an appropriate amount of the swirly golden SFX.
//       Without this, the fog gate would just be a see-through wall.
// X12_4: An optional activation flag. If the fog gate has some delay in turning on, like
//        waiting for the first encounter to trigger, this is usually that encounter flag.
//        If the fog gate is up from the start, this is usually set to 0.
$Event(9005811, Restart, function(X0_4, X4_4, X8_4, X12_4) {
    // Explore the contents yourself!
});

Enemy activation

Here is one more common_func event which is not applied to bosses, but to regular enemies chilling in a level. Without any scripting, most enemies will activate when you enter vision or hearing ranges, which are broad spheres/cones defined in NpcThinkParam.

Some enemy encounters are more special, however, like enemies which hang from the ceiling and drop down when you step right below them, or wait around a corner to ambush you, or stand up from a slouch when you approach. In those cases, the enemy starts out in a looping animation which usually also greatly limits their vision and hearing so they can't detect you normally (speffects 5450 and 5080). Then, when some condition is met, like the player entering a region and/or radius, the enemy plays a wakeup animation. Alternatively, if the enemy is damaged they should also wake up, which usually leads to them playing their normal stagger animation and leaving their standby state that way.

Some version of this wakeup event is in every Soulsborne game, and it's a bit easier to understand if you're familiar with it from previous games. Still, this is a very commonly used event with dozens of variations, so it helps to be familiar with it!

Here is an example initialization in m14_00_00_00 (Raya Lucaria), which applies to the scholar in the middle of the Cuckoo Church:

InitializeCommonEvent(0, 90005200, 14000210, 30000, 20000, 14002451, 1065353216, 0, 0, 0, 0);

Here is another initialization in m30_04_00_00 (Murkwater Catacombs), which applies to an imp waiting to drop down from the ceiling when you approach:

InitializeCommonEvent(0, 90005200, 30040200, 30001, 20001, 30042200, 0, 1, 0, 0, 0);

In total, event 90005200 has nearly 600 total initializations across all maps in the game. It puts an enemy in standby and does a wakeup animation when the player enters a region. You can use ForceAnimationPlayback (or a Cheat Engine table with animation playback) to see these animations in-game.

Show event

Hide

$Event(90005200, Restart, function(X0_4, X4_4, X8_4, X12_4, X16_4, X20_4, X24_4, X28_4, X32_4) {
    EndIf(SpecialStandbyEndedFlag(X0_4));
    if (X20_4 != 0) {
        DisableCharacterGravity(X0_4);
        SetCharacterMaphit(X0_4, false);
    }
    ForceAnimationPlayback(X0_4, X4_4, true, false, false);
    chrSp = (CharacterType(10000, TargetType.BlackPhantom) && CharacterHasSpEffect(10000, 3710))
        || CharacterType(10000, TargetType.Alive)
        || CharacterType(10000, TargetType.BluePhantom)
        || CharacterType(10000, TargetType.WhitePhantom);
    areaChrSp &= InArea(10000, X12_4)
        && CharacterBackreadStatus(X0_4)
        && (CharacterHasSpEffect(X0_4, 5080) || CharacterHasSpEffect(X0_4, 5450));
    if (!(X24_4 == 0 && X28_4 == 0 && X32_4 == 0)) {
        if (X24_4 != 0) {
            chr |= CharacterAIState(X0_4, AIStateType.Combat);
        }
        if (X28_4 != 0) {
            chr |= CharacterAIState(X0_4, AIStateType.ActiveAlert);
        }
        if (X32_4 != 0) {
            chr |= CharacterAIState(X0_4, AIStateType.PassiveAlert);
        }
        areaChrSp &= chr;
    }
L9:
    sp = CharacterHasSpEffect(X0_4, 481)
        && !CharacterHasSpEffect(X0_4, 90100)
        && !CharacterHasSpEffect(X0_4, 90110)
        && !CharacterHasSpEffect(X0_4, 90160);
    sp2 = CharacterHasSpEffect(X0_4, 482)
        && !CharacterHasSpEffect(X0_4, 90100)
        && !CharacterHasSpEffect(X0_4, 90120)
        && !CharacterHasSpEffect(X0_4, 90160)
        && !CharacterHasSpEffect(X0_4, 90162);
    sp3 = CharacterHasSpEffect(X0_4, 483)
        && !CharacterHasSpEffect(X0_4, 90100)
        && !CharacterHasSpEffect(X0_4, 90140)
        && !CharacterHasSpEffect(X0_4, 90160)
        && !CharacterHasSpEffect(X0_4, 90161);
    sp4 = CharacterHasSpEffect(X0_4, 484)
        && !CharacterHasSpEffect(X0_4, 90100)
        && !CharacterHasSpEffect(X0_4, 90130)
        && !CharacterHasSpEffect(X0_4, 90161)
        && !CharacterHasSpEffect(X0_4, 90162);
    sp5 = CharacterHasSpEffect(X0_4, 487)
        && !CharacterHasSpEffect(X0_4, 90100)
        && !CharacterHasSpEffect(X0_4, 90150)
        && !CharacterHasSpEffect(X0_4, 90160);
    areaChrSp &= chrSp;
    WaitFor(
        areaChrSp
            || HasDamageType(X0_4, 0, DamageType.Unspecified)
            || CharacterHasStateInfo(X0_4, 436)
            || CharacterHasStateInfo(X0_4, 2)
            || CharacterHasStateInfo(X0_4, 5)
            || CharacterHasStateInfo(X0_4, 6)
            || CharacterHasStateInfo(X0_4, 260)
            || sp
            || sp2
            || sp3
            || sp4
            || sp5);
    WaitFixedTimeSeconds(0.1);
    SetNetworkconnectedThisEventSlot(ON);
    SetSpecialStandbyEndedFlag(X0_4, ON);
    if (!(!CharacterHasSpEffect(X0_4, 5080) && !CharacterHasSpEffect(X0_4, 5450))) {
        WaitFixedTimeSeconds(X16_4);
        if (X20_4 != 0) {
            EnableCharacterGravity(X0_4);
            SetCharacterMaphit(X0_4, true);
        }
        ForceAnimationPlayback(X0_4, X8_4, true, false, false);
        EndEvent();
    }
L0:
    if (X20_4 != 0) {
        EnableCharacterGravity(X0_4);
        SetCharacterMaphit(X0_4, true);
    }
    EndEvent();
});

Show event with comments

Hide

// X0_4: Entity id of the enemy
// X4_4: Animation id which loops while it's waiting for the trigger
// X8_4: Animation id which plays to wake the enemy up, assuming the enemy is leaving standby
//       on its own
// X12_4: Entity id of the trigger region for the enemy to wake up
// X16_4: Some amount of time to wait after the trigger condition is met, before doing X8_4.
//        You can use Ctrl+1 to see that the actual values are 1.0 and 0.0 respectively above.
// X20_4: Either 0 or 1. When set to 1, the enemy is put in a noclip state. This is used when
//        it's hanging from the ceiling or waiting to climb onto a ledge, for instance.
// X24_4: Either 0 or 1. When set to 1, this requires the enemy's AI to target the player
//        first before it leaves its stasis. Most enemies don't support this.
// X28_4: Like X24_4, but using a pre-combat AI state.
// X32_4: Like X24_4, but using a different pre-combat AI state.
$Event(90005200, Restart, function(X0_4, X4_4, X8_4, X12_4, X16_4, X20_4, X24_4, X28_4, X32_4) {
    // An enemy's "special-standby-ended flag" is an on/off value which is attached to it using
    // its entity id. It is set below when the enemy is woken up. This is relevant in multiplayer,
    // because a summon or invader can join after an enemy has already been woken up, and it 
    // should not force the enemy back into its previous standby animation.
    EndIf(SpecialStandbyEndedFlag(X0_4));
    if (X20_4 != 0) {
        // X20_4 is basically a boolean which controls whether the enemy starts out with
        // its gravity and map collision disabled, meaning it's floating in air and not
        // getting pushed out of walls.
        // It's a fair bit more common to use event 90005211 for this purpose, which has
        // a combined region and radius check.
        DisableCharacterGravity(X0_4);
        SetCharacterMaphit(X0_4, false);
    }
    // Play a looping animation X4_4 on enemy X0_4. Click on the instruction in
    // DarkScript to see what all of the arguments mean, or check out the EMEDF HTML.
    ForceAnimationPlayback(X0_4, X4_4, true, false, false);
    // Here, we start building up the condition for the enemy to wake up.
    // This is a really big condition and consists of several dozen subconditions.
    // chrSp requires the player to be able to aggro the enemy. This is not true of invaders.
    chrSp = (CharacterType(10000, TargetType.BlackPhantom) && CharacterHasSpEffect(10000, 3710))
        || CharacterType(10000, TargetType.Alive)
        || CharacterType(10000, TargetType.BluePhantom)
        || CharacterType(10000, TargetType.WhitePhantom);
    // areaChrSp contains the main trigger conditions, all of which must be true to activate:
    // 1. The player is in area X12_4
    // 2. The enemy is backread, meaning it's loaded in. Enemies in a level can dynamically
    //    load and unload when you get close/far away, based on drawgroups. Without this,
    //    other subconditions may not be calculated correctly.
    // 3. The enemy is still in its initial looping animation. During these animations,
    //    speffects 5450 or 5080 are applied to the enemy.
    // 4. (optional) The enemy AI wakes up and enters combat mode (targeting the player).
    // 5. chrSp is true. This is added below (scroll down a bit)
    areaChrSp &= InArea(10000, X12_4)
        && CharacterBackreadStatus(X0_4)
        && (CharacterHasSpEffect(X0_4, 5080) || CharacterHasSpEffect(X0_4, 5450));
    if (!(X24_4 == 0 && X28_4 == 0 && X32_4 == 0)) {
        if (X24_4 != 0) {
            chr |= CharacterAIState(X0_4, AIStateType.Combat);
        }
        if (X28_4 != 0) {
            chr |= CharacterAIState(X0_4, AIStateType.ActiveAlert);
        }
        if (X32_4 != 0) {
            chr |= CharacterAIState(X0_4, AIStateType.PassiveAlert);
        }
        areaChrSp &= chr;
    }
L9:
    // Now, for alternate wakeup conditions.
    // These account for cases where an enemy should wake up without the above trigger
    // condition being met. This first set of condition variables is a long series of speffect
    // checks which evidently have to do with fake targets, like perhaps Alluring Pots? 
    sp = CharacterHasSpEffect(X0_4, 481)
        && !CharacterHasSpEffect(X0_4, 90100)
        && !CharacterHasSpEffect(X0_4, 90110)
        && !CharacterHasSpEffect(X0_4, 90160);
    sp2 = CharacterHasSpEffect(X0_4, 482)
        && !CharacterHasSpEffect(X0_4, 90100)
        && !CharacterHasSpEffect(X0_4, 90120)
        && !CharacterHasSpEffect(X0_4, 90160)
        && !CharacterHasSpEffect(X0_4, 90162);
    sp3 = CharacterHasSpEffect(X0_4, 483)
        && !CharacterHasSpEffect(X0_4, 90100)
        && !CharacterHasSpEffect(X0_4, 90140)
        && !CharacterHasSpEffect(X0_4, 90160)
        && !CharacterHasSpEffect(X0_4, 90161);
    sp4 = CharacterHasSpEffect(X0_4, 484)
        && !CharacterHasSpEffect(X0_4, 90100)
        && !CharacterHasSpEffect(X0_4, 90130)
        && !CharacterHasSpEffect(X0_4, 90161)
        && !CharacterHasSpEffect(X0_4, 90162);
    sp5 = CharacterHasSpEffect(X0_4, 487)
        && !CharacterHasSpEffect(X0_4, 90100)
        && !CharacterHasSpEffect(X0_4, 90150)
        && !CharacterHasSpEffect(X0_4, 90160);
    // This is how chrSp becomes a required subcondition for areaChrSp, as mentioned above.
    // This ordering is a bit weird, but it matches how fromsoft chose to code this event.
    areaChrSp &= chrSp;
    // Finally, the main WaitFor. This requires either the main trigger condition, or various
    // other ways of detecting that the player has damaged the enemy.
    // The main way this might happen in practice is with HasDamageType, which checks for a type 
    // of direct damage (e.g. physical or fire) against the enemy. "0" is a default entity id,
    // which means the damage can come from any source, and "Unspecified" means that all damage
    // types count for this.
    // The argument to CharacterHasStateInfo is a state info, which is a field in SpEffectParam. 
    // This is used to detect when an enemy is poisoned, or similar status effects, without having
    // been directly damaged.
    WaitFor(
        areaChrSp
            || HasDamageType(X0_4, 0, DamageType.Unspecified)
            || CharacterHasStateInfo(X0_4, 436)
            || CharacterHasStateInfo(X0_4, 2)
            || CharacterHasStateInfo(X0_4, 5)
            || CharacterHasStateInfo(X0_4, 6)
            || CharacterHasStateInfo(X0_4, 260)
            || sp
            || sp2
            || sp3
            || sp4
            || sp5);
    WaitFixedTimeSeconds(0.1);
    // This command doesn't do anything here, but it's important in other events.
    SetNetworkconnectedThisEventSlot(ON);
    // As mentioned above, this marks the enemy as having ended their standby state.
    SetSpecialStandbyEndedFlag(X0_4, ON);
    // The final thing to do is play the wakeup animation, but only if the enemy is still
    // in the idle starting animation. This condition is a bit tricky to read; you can use
    // De Morgan's laws (see MattScript documentation) to read this as "if X0_4 has speffect
    // 5080 or speffect 5450".
    if (!(!CharacterHasSpEffect(X0_4, 5080) && !CharacterHasSpEffect(X0_4, 5450))) {
        // For dramatic effect or whatever other reason, the wakeup may be delayed.
        WaitFixedTimeSeconds(X16_4);
        // If gravity was initially disabled, reenable it again.
        if (X20_4 != 0) {
            EnableCharacterGravity(X0_4);
            SetCharacterMaphit(X0_4, true);
        }
        // Finally, playing the wakeup animation!
        ForceAnimationPlayback(X0_4, X8_4, true, false, false);
        EndEvent();
    }
L0:
    // Same as the above case, but without the animation playback or optional delay.
    if (X20_4 != 0) {
        EnableCharacterGravity(X0_4);
        SetCharacterMaphit(X0_4, true);
    }
    EndEvent();
});

How to investigate game behaviors

Remember that the key question for modding is “how does the game do X?”, which you have to answer before doing X yourself. But the game has a lot of data, the challenge isn't just understanding it, it's finding even the smallest foothold for what you should be looking for. The best way to do this is to have entry points: things you can observe in the game itself, in particular locations or circumstances. You can then use the below entry points to find the appropriate numerical ids, then search for those numbers in the appropriate places.

Now that you've gotten a taste of event scripts above, here is a more detailed guide on how to start an investigation yourself:

In-game text

  • Where it's located: In item.msgbnd.dcx and menu.msgbnd.dcx in msg/engus for English text, and other language/country names for other languages
  • How to browse it: Use DSMapStudio's FMG editor (a version from 12 July 2022 or later) to browse all FMGs and search for text in the game. Alternatively, use Yabber to unpack the msgbnd files, and then use again it to unpack all of the FMG files in each one, and use Notepad++ or grep to search across all of them.
  • How to use it: If you encounter text like “Enter evergaol” or “Traverse the mist” or a place name, NPC name, or item name, just search for it in FMGs. This will usually be easier if you know which FMG it is. This will give you a numerical id for the text, and you can search event scripts or params or ESDs for it, depending on the context. For instance, boss healthbar names are in NpcName.fmg and are always used in emevd directly.
  • Example: You want to mod evergaol entry, so you look for “Enter evergaol” in the EventTextForMap FMG, and you get id 20300. Using Ctrl+Shift+F in DarkScript3 with “Match whole word”, you see that it's a parameter to common_func event 90005881 in all of the maps with evergaols in them. This event includes a fade-to-black warp after you accept the prompt.

Item lots

  • Where it's located: In ItemLotParam_enemy (random enemy drops) and ItemLotParam_map (everything else) in params (regulation.bin). Any time you see an item popup in the game, it is through an item lot being acquired. There are also shops, in ShopLineupParam.
  • How to browse it: I would strongly recommend using Elden Ring Item Analysis, which is a summary of all item lots and shops in the game. You can also use a param editor to look at the params directly.
  • How to use it: If there's an item you get somewhere in the game, look for the item lot to see where and how it's triggered. Item lots are referenced from a bunch of different places: from other params, like random enemy drops in NpcParam and material pickups like AssetEnvironmentGeometryParam, from MSB files for treasures you can pick up, and directly from emevd and ESD for scripted item popups. You can search for item lot ids yourself, but Item Analysis contains all of the references in one place as a starting point, so you can start with the appropriate MSB/emevd/ESD/param file. It also contains all event flags, which can be useful since event scripts often reference the flag as shorthand for “did you acquire this item?”
  • Example: You want to see how Mohg's Great Rune is activated. From the item analysis page, you can see that there are two separate gifts for the inactive version (good 8152, acquired in Mohgwyn) and the active version (good 195, acquired in East Altus Divine Tower). Use an FMG viewer to verify that using the items' descriptions. Use Ctrl+Shift+F in DarkScript3 to search for item lot 34140710, and find an initialization of common_func 90005110. This event includes a “Great Rune Restored” banner.
  • Example: You want to see where you get the Drawing-Room Key. Again search for it in item analysis: you'll note that there are two different item lots, but they both use the same flag (400072), so they're mutually exclusive. One place is in her chair as “Shiny Item” 16001708. Search for this in DarkScript3 and you'll find an initialization of common_func 90005750, which depends on flag 16009270 getting set, which depends on flag 16000800 getting set (Rykard's defeat flag). The other item lot is 100720, which mentions her ESD 300001600. Look at t300001600.py from ESDLang and you'll see that it is awarded after selecting “Join Volcano Manor” in a few different dialogue variants, and based on flags you can trace back to event scripts.

Map ids

  • Where it's located: Maps are how most of the game world is split up, so finding a map id is critical to being able to search for something in a particular MSB (in map/mapstudio) or event script (in event).
  • How to browse it: Use Elden Ring Map List to find commonly used maps, or use the lists provided in various editors. Other dumps like Item Analysis also contain map references to everything.
  • How to use it: Open the files in a map/event editor!
  • Example: You want to view/edit something in Roundtable Hold. Find that it's m11_10_00_00 in the map list, and open those files in DarkScript3/DSMapStudio.

Model names

  • Where it's located: Character models are defined in the chr directory, and are referenced using the ModelName field in MSB parts like enemies, assets, etc.
  • How to browse it: Use Elden Ring CHR ID reference to identify the model you're looking for.
  • How to use it: In addition to or instead of the above reference, you can also use the Enemy Locations Dump to find specific maps to open, then open the relevant maps in DSMapStudio. To find out how the enemy is scripted, use its EntityID.
  • Example: You want to find Radahn. Use above references to see that he's c4730, and that his full name is m60_52_38_00-c4730_9002, but he's located in map m60_13_09_02. The different ids are because his physical location is in the 52_38 small tile, but he can travel far away from that, so his data is in the 13_09 big tile instead. See the map reference for details.

Entity ids

  • Where it's located: This is the most important thing to know about an enemy you want to script, or any other map entity like a region or generator. It's the EntityID field in MSB, and must be unique for any given entity. Alternatively, EntityGroupIDs are used in some contexts to refer to a group of entities at once.
  • How to browse it: Once you find an enemy in a map, you can find its EntityID field. You can also start from the Entity ID List which contains most entity ids and entity group ids in the game.
  • How to use it: The main use of entity ids is to search event scripts for it! Once you do that, you can find all scripted encounters involving that enemy. (Be aware that the same number can refer to either the entity id itself or to a flag/item lot, depending on context.) You can also jump to the entity in DSMapStudio to view it (see tips below). A handful of params also reference entity id.
  • Example: You want to find Radahn's scripts. You know he's a c4730 in m60_13_09_02, so you use one of the above references or DSMapStudio to find that his entity id is 1052380800. You can then Ctrl+Shift+F to find his scripting in DarkScript3, and you can see that most of it is in the m60_52_38_00 emevd.

Dialogue

  • Where it's located: Each NPC dialogue line is specified in TalkParam. The first line of dialogue of any sequence of lines is invoked from ESD files, which are used for NPC menus and dialogue trees.
  • How to browse it: For dialogue specifically, I'd recommend dumping the entire game using ESDLang and searching for the first line of dialogue using multi-file search like Notepad++ or grep. If you need to find the first line, do a general FMG search in TalkMsg.fmg, then find the starting line of that section of ids. This is slightly tricky because the TalkParam id does not always match the FMG id. Finally, note that the same dialogue trees are used in multiple different ESD files. To find the right one, crossreference the talk ID with the Enemy Location dump and find it in the MSB.
  • How to use it: Once you find the ESD file dialogue is used in, you can browse the script to see what triggers it and what else happens at the same time. Usually this is based on either reading flags or setting them, and you can then search event scripts to see how those flags in turn are triggered and used.
  • Example: You want to know what makes you warp to Roundtable Hold, so you can search ESDLang output for the line “Forgive me. I've been…testing you.” (or alternatively, the EventTalkForText entry “Go to the Roundtable Hold”), and you can see it's in t000003000.py. Grace Melina doesn't exist as a character in any map; she's spawned by the graces themselves (ESD 1000). You can search for the function name x48 and find it's called by x72, and find two conditions for activation: flags 10000851, 3062, 3063, 3064, or 3065. Search for these in event scripts to find that the first flag is for encountering Margit, and the other flags are for resting at an overworld grace outside of Limgrave.
tutorial/intro-to-elden-ring-emevd.txt · Last modified: by admin