MattScript is an uncreatively named extension to DarkScript3 developed by thefifthmatt which adds additional syntax for control flow and condition expressions for writing advanced scripts with ease. It can be activated in events by defining them with $Event instead of Event. See Learning How To Use EMEVD for an overall tutorial of EMEVD, and Intro to Elden Ring EMEVD for a detailed walkthrough of MattScript in Elden Ring.
On its own, DarkScript3 works by evaluating a JavaScript file which calls many instruction functions. Each instruction function adds an instruction to an event. Inside of Events, you can also use regular JavaScript, like helper functions and loops.
Inside of $Events, JavaScript constructs are instead used to represent control flow logic in the script itself. When saving a file, they are not run as JavaScript directly. They are manually parsed, translated into a list of instructions as a preprocessing step, and then run.
For example, an $Event might have this code:
$Event(11005000, Default, function(entityId, objEntityId, animationId) { if (ThisEventSlot()) { DeactivateObject(objEntityId, Disabled); EndEvent(); } WaitFor(EntityInRadiusOfEntity(10000, entityId, 3)); ForceAnimationPlayback(objEntityId, animationId, false, true, false); DeactivateObject(objEntityId, Disabled); });
When saving a file, it is converted into this equivalent Event which is then executed by DarkScript3:
Event(11005000, Default, function(entityId, objEntityId, animationId) { SkipIfEventFlag(2, OFF, TargetEventFlagType.EventIDAndSlotNumber, 0); DeactivateObject(objEntityId, Disabled); EndUnconditionally(EventEndType.End); IfEntityInoutsideRadiusOfEntity(MAIN, InsideOutsideState.Inside, 10000, entityId, 3); ForceAnimationPlayback(objEntityId, animationId, false, true, false); DeactivateObject(objEntityId, Disabled); });
Files can be automatically converted to $Event by selecting "Convert To MattScript" in one of two places. It can be done when opening a new emevd file. It can also be performed on an existing JS file from the Edit menu. This will attempt to rewrite the entire file while preserving existing comments, and also show a preview of the converted version. Once you have a converted file, you can also preview its simpler compiled form in the View menu.
Condition expressions
$Events add condition functions for things which would otherwise be standalone instructions. These can only be used in a few specific places: if statements, built-in control flow commands like WaitFor, and condition variables. These are all turned into various instructions in the end.
A condition function cannot be used as an argument to another function or instruction. It can only be used in specific constructs described below.
A very commonly used condition function is event flag state, which is represented with EventFlag(400) to check if a flag is on and !EventFlag(400) to check if it's off. Most condition checks can be negated in this way. In the final EMEVD, this function will be compiled into IfEventFlag, EndIfEventFlag, SkipIfEventFlag, or GotoIfEventFlag depending on how it's used.
Most condition functions have multiple versions. For instance, there is a version of item ownership check with all possible arguments PlayerHasdoesntHaveItem(ItemType.Goods, 9405, OwnershipState.DoesntOwn), but there's also a shorter version where the yes/no enum is turned into regular negation, as !PlayerHasItem(ItemType.Goods, 9405). The longer version is only used in decompilation if this isn't possible, like if the enum is parameterized.
Condition functions based on comparing numerical values can also use comparison operators directly. The condition function CompareHPRatio(10000, ComparisonType.LessOrEqual, 0, ComparisonType.Equal, 1) can be represented in the shorter form HPRatio(10000) <= 0. The last two entity-group-related arguments are also allowed to be optional. For comparisons like this, the function call is always on the left side. It is forbidden to call HPRatio outside of a comparison. Direct numerical comparisons like paramValue != 0 are also permitted, using instructions like IfParameterComparison under the hood.
As a simple example, suppose there is an area in the map, and as long as a fight is active, we want to slowly auto-heal the player as long as they are under 70% HP and in that area. A simple version of that might be as follows:
$Event(13705010, Restart, function() { WaitFor(InArea(10000, 3704310)); EndIf(EventFlag(13700860)); if (HPRatio(10000) < 0.7) { SetSpEffect(10000, 4099); WaitFixedTimeSeconds(2.5); } RestartEvent(); });
Three condition functions are used here, InArea, EventFlag, and HPRatio. Clicking on any of them in DarkScript3, you can see their arguments and alternate versions in the doc view.
WaitFor is the only way to make a script temporarily stop based on a condition. if, EndIf, and everything else all check if the condition is true at that very moment and then act immediately based on that.
Finally, condition functions can be combined using the logical operators && and ||. A more standard way of writing the event might be as follows:
$Event(13705010, Restart, function() { WaitFor(InArea(10000, 3704310) && HPRatio(10000) < 0.7); EndIf(EventFlag(13700860)); SetSpEffect(10000, 4099); WaitFixedTimeSeconds(2.5); RestartEvent(); });
One useful trick for dealing with boolean logic is De Morgan's law, which is a way to rewrite conditions without changing behavior. In general, !(a && b && c) (not all of them true) can be rewritten as !a || !b || !c (at least one of them false), and !(a || b || c) (none of them true) can be rewritten as !a && !b && !c (all of them false). Basically, switch the inner/outer !s and switch the operator. This applies to events like this:
$Event(9182, Default, function() { EndIf(!CharacterType(10000, TargetType.Alive)); SetEventFlag(9184, OFF); WaitFor(HasMultiplayerState(MultiplayerState.ConnectingtoMultiplayer) && !HasMultiplayerState(MultiplayerState.Multiplayer)); SetEventFlag(9184, ON); WaitFor(!HasMultiplayerState(MultiplayerState.ConnectingtoMultiplayer) || HasMultiplayerState(MultiplayerState.Multiplayer)); RestartEvent(); });
Which is equivalent to the following:
$Event(9182, Default, function() { EndIf(!CharacterType(10000, TargetType.Alive)); SetEventFlag(9184, OFF); WaitFor(HasMultiplayerState(MultiplayerState.ConnectingtoMultiplayer) && !HasMultiplayerState(MultiplayerState.Multiplayer)); SetEventFlag(9184, ON); WaitFor(!(HasMultiplayerState(MultiplayerState.ConnectingtoMultiplayer) && !HasMultiplayerState(MultiplayerState.Multiplayer))); RestartEvent(); });
Condition variables
An even more fromsoft-ian way of structuring the 70% HP example might be as follows, which brings us to condition variables:
$Event(13705010, Restart, function() { fightOver = EventFlag(13700860); WaitFor(fightOver || (InArea(10000, 3704310) && HPRatio(10000) < 0.7)); EndIf(fightOver.Passed); SetSpEffect(10000, 4099); WaitFixedTimeSeconds(2.5); RestartEvent(); });
Every x = y statement in an $Event is assumed to be a condition variable assignment (excluding const declarations; see below). They allow more direct access to condition groups.
For the most part, $Event hides details of how condition groups are structured. Expressions using || and && are converted to use them, like this:
$Event(13705011, Restart, function() { WaitFor(InArea(10000, 3704310) && HPRatio(10000) < 0.7); });
This is compiled down to this low-level representation:
Event(13705011, Restart, function() { IfInoutsideArea(AND_01, InsideOutsideState.Inside, 10000, 3704310, 1); IfCharacterHPRatio(AND_01, 10000, ComparisonType.Less, 0.7, ComparisonType.Equal, 1); IfConditionGroup(MAIN, PASS, AND_01); });
Another way of writing the event using an explicit condition variable would be like this. (Note: it is strongly recommended to use the && expression instead, because explicit groups aren't automatically reused and there is a finite number of total groups.)
$Event(13705011, Restart, function() { areaLowHp &= InArea(10000, 3704310); areaLowHp &= HPRatio(10000) < 0.7; WaitFor(areaLowHp); });
A summary of their rules:
- Every assignment to a condition group/variable adds another thing to check for when evaluating it.
- Regular condition groups/variables come in two types, AND groups and OR groups. Adding a condition to an AND group adds another thing that must be true. Adding a condition to an OR group adds an alternative way for it to be true.
These are represented using the &= and |= assignment operators in $Events. These operators add a new condition to the given condition variable (which does not need to be declared in advance). You can also use = for standalone assignments, which will automatically turn into either &= or |= during compilation as appropriate. - After a WaitFor passes (also called the MAIN group, a special group that waits for its condition to become true), all condition variable conditions are cleared. All of their previous requirements are forgotten, and must be redeclared if they are to be reused in the same way.
- However, after a WaitFor, it does remember which conditions were true or false at the time the overall check succeeded. This is called "compiled" condition group state in regular emevd, and is represented by a made-up property called .Passed here.
- (edge cases) .Passed values will persist across multiple WaitFors as long as their condition groups are not redeclared and ClearCompiledConditionGroupState() is not used. If condition variables are referenced without being assigned to (don't do this!), they are true by default, except for unassigned .Passed expressions which are false by default.
tl;dr: Do not reuse condition variables after a WaitFor to reuse their logic!! The most you can do is either redeclare their logic or check myCond.Passed retroactively.
Note that condition variables named and01/or02/etc. will directly use that condition group number, but it is preferable to use more descriptive names where possible.
A common usage of condition variables is to have conditions whose definitions can change based on event parameters, like this excerpt from the DS3 fog gate traversal event:
$Event(20005800, Restart, function(eventFlagId, areaEntityId, areaEntityId2, eventFlagId2, actionButtonParameterId, chrEntityId, eventFlagId3, areaEntityId3) { ... if (areaEntityId3 != 0) { areaFlag |= InArea(10000, areaEntityId3); } areaFlag |= EventFlag(eventFlagId3); cond &= areaFlag && !PlayerIsNotInOwnWorld(); WaitFor(cond); ... });
Plenty of other interesting uses are possible, like this Sekiro event. Condition variables can basically be used anywhere a condition is expected, not just in WaitFors.
$Event(11105130, Restart, function() { SetEventFlag(11100130, OFF); if (!EventFlag(8302)) { flag |= EventFlag(11100301); } if (EventFlag(8302)) { flag |= EventFlag(11100480); } if (flag) { SetEventFlag(11100130, ON); } WaitFor( EventFlagState(CHANGE, TargetEventFlagType.EventFlag, 8302) || EventFlagState(CHANGE, TargetEventFlagType.EventFlag, 11100301) || EventFlagState(CHANGE, TargetEventFlagType.EventFlag, 11100480)); RestartEvent(); });
In this case, you could also implement it as follows, although this uses slightly more condition groups behind the scenes.
$Event(11105130, Restart, function() { if ((!EventFlag(8302) && EventFlag(11100301)) || (EventFlag(8302) && EventFlag(11100480))) { SetEventFlag(11100130, ON); } else { SetEventFlag(11100130, OFF); } ... });
The key thing to keep in mind is that conditions will keep accumulating until a WaitFor, after which point they will be wiped out.
Control flow
$Events have a bunch of built-in commands which are used to support condition functions and various high-level control flow constructs.
WaitFor(cond)
Pauses execution if the given condition is not true, and unpauses it when it becomes true.
In plain emevd, this corresponds to MAIN group evaluation.
EndEvent(), RestartEvent()
Ends or restarts the event.
These are directly equivalent to EndUnconditionally in plain emevd.
EndIf(cond), RestartIf(cond)
Ends or restarts the event if the condition is true, and continues to the next instruction otherwise.
This is equivalent to various EndIf instructions in plain emevd.
if (cond), else
Enters the if block if the condition is true. Otherwise, if there is an else block, it enters that that.
This is syntactic sugar for various skip/goto statements. It will turn into a goto rather than a skip if it's in a game that supports labels and there's a label command on the jump target.
Labels
These are declared as JavaScript labels, and they come in two types.
The first is a label command, only available in Bloodborne onwards, and these are represented using labels name L0 L1 all the way up to L20.
The second is a synthetic label only used within the compiler for calculating skip line amounts. It can be named anything.
A toy example of their use:
$Event(9194, Default, function() { GotoIf(L0, EventFlag(6006)); Goto(waitToRestart); L0: SetEventFlag(6006, OFF); waitToRestart: WaitFor(EventFlag(6007)); RestartEvent(); });
Becomes:
Event(9194, Default, function() { GotoIfEventFlag(Label.LABEL0, ON, TargetEventFlagType.EventFlag, 6006); SkipUnconditionally(2); Label0(); SetEventFlag(6006, OFF); IfEventFlag(MAIN, ON, TargetEventFlagType.EventFlag, 6007); EndUnconditionally(EventEndType.Restart); });
One downside of using JavaScript labels is that they must always have a statement following them, so in order to use a label at the end of an event or block, you can use the fake NoOp(); (no operation) command as a label target. As the name implies, it completely disappears in compilation.
Goto(label)
Jumps to the given real or synthetic label by name. Jumps are only allowed in a forwards direction. If you want to revisit previous parts of an event, you must carefully restart it. Probably split the looping part off into its own event.
Gotos are turned into GotoUnconditionally and SkipUnconditionally instructions depending on whether real labels are used or not.
GotoIf(label, cond)
Jumps to the given real or synthetic label if the condition is true, otherwise continues to the next line.
GotoIfs are turned into various GotoIf and SkipIf instructions in plain emevd depending on whether real labels are used or not.
Mixing regular JavaScript
Because JavaScript is mainly used for condition group definitions and for emevd control flow, the use of JavaScript for script-writing helpers is limited in $Events.
There are a few places where you can use arbitrary JavaScript which will be left as-is when evaluating the event script. This should be done carefully! These are basically like macros, and are useful for organizing scripts, but not used by the game itself.
Const variable declarations in events are permitted. An example of constants and simple expressions:
$Event(13105265, Restart, function() { const bossEntity = 3100395; WaitFor(CharacterAIState(bossEntity, AIStateType.Combat)); SetSpEffect(bossEntity, 15999); WaitFixedTimeSeconds(60 * buffMinutes[bossEntity]); ClearSpEffect(bossEntity, 15999); });
Calls to standalone functions are also left as-is, as long as their names start with a lowercase character. For the moment, MattScript can only call non-MattScript, so try not to use condition registers in helper functions until it's properly supported. These functions can be defined in other JS files when they are imported as modules.
For loops
For loops are specially processed by MattScript. The loop definition itself is retained as pure JS, but the contents are compiled. Regular for-loops (with let variables) and for-of-loops (with const variables) are supported. For instance, this lets you define an array of enemies, and make some combined condition from them.
$Event(13105266, Restart, function() { const bossEntities = [3100395, 3100398, 3100399]; for (const id of bossEntities) { allDead &= CharacterDead(id); } WaitFor(allDead); DisplayBanner(TextBannerType.LordofCinderFallen); });
The main restriction with loops is that all condition variables must be explicitly defined with either |= or &=, to avoid loop iterations interfering with each other. You can freely use if (EventFlag(id)), since this condition does not require a condition variable, but you cannot do WaitFor(CharacterDead(id)); without separating out the condition into its own variable.
In cases where you're repeating a series of if statements which require condition variables, this is fairly difficult even without MattScript. You can use a no-op condition group reset in between iterations.
$Event(13105267, Restart, function() { WaitFor(EventFlag(13105265)); for (let i = 0; i < 10; i++) { const id = 3100380 + i; dead |= CharacterDead(id); if (dead) { ChangeCharacterEnableState(id, Disabled); } WaitFor(ElapsedSeconds(0)); } });
Typed initializations
Typed event initializations allow for event declarations and initializations where the event parameters have names and types. They are not part of MattScript but they do preprocess the source file using the same parser routine.
// Old-style events use parameter names starting with X $Event(73791090, Default, function(X0_1, X1_1, X2_1, X3_1, X4_4, X8_4, X12_1)) { ... }); // New-style events use parameters with arbitrary non-X names $Event(73791090, Default, function(areaId, blockId, regionId, indexId, destEntityId, timeS, weather) { ... }); // Old-style inits use commands with integer event parameters InitializeEvent(0, 73791090, 2633277, 2046402020, 1069547520, 4); // New-style inits use $ versions of commands with parameters matching the event's usage $InitializeEvent(0, 73791090, 61, 46, 40, 0, 2046402020, 1.5, Weather.Fog);
New-style events must have unique event ids per file because the game may not consistently initialize the same event otherwise. All parameters of the event must be used in the event itself, and all non-trivial usages must have the same type (int, uint, byte, sbyte, short, ushort, or float). Also, if an event script can initialize events from linked event scripts like common_func, the linked file takes priority. As a result, you should convert common_func before converting any map files. Linked files are discussed further below.
Old-style events and inits can be automatically converted to new-style using "Edit > Preview Conversion to MattScript", and any events or inits which can't be converted will have warnings inserted into the diff. It's done alongside MattScript (re)conversion because rewriting source code is not possible using the low-level JS parse tree, and identifying instruction usages requires the MattScript compiler anyway. Comments and blank lines are preserved where possible but other formatting which is not tracked will be reformatted.
To be converted, events need to meet some requirements which are met by all vanilla events in all games:
- All arguments must be used in the event and all usages must have the same type. The only exception is that integer comparisons (value == 1) may be used with non-int types. Unused integer arguments can be explicitly prefixed with "unused" in new-style events, but automatic conversion doesn't support this.
- The X0_4-style argument name must have the correct width. int, uint, and float have a width of 4 and require a name like X0_4; short and ushort have a width of 2 and require a name like X0_2; byte and sbyte have a width of 1 and require a name like X0_1. (Mismatches produce warnings and may produce errors in the future.)
- All arguments need to be packed with the correct padding. This means an argument's offset needs to be a multiple of its width, so X6_4 and X9_2 are both invalid. And given that, there can't be any extra gaps between arguments: an argument's offset needs to be the next valid offset after the previous argument. X0_4 followed by X8_4 is invalid because X4_4 is missing between them. But also, X4_1 followed by X8_1 is invalid because it should be X5_1 instead. This makes it difficult to convert modded events with consecutive byte arguments so they may needed to be carefully migrated manually.
Converting inits to new-style inits is somewhat rudimentary and has the following requirements:
- The event was successfully converted.
- If the event has parameters, the number of bytes in the old-style init has to match the number of bytes in the event parameters. If the event has no parameters, the old-style init has to have exactly one argument which is set to 0.
- Currently, old-style init arguments can only be literal integer values, because the init is effectively packed to bytes then unpacked. Variables and enum values can't be used. The main exception is arguments formatted like floatArg(2.5) which were inserted using Ctrl+1. This can be improved in the future.
Linked files
After DS1, events can be initialized from different files like common_func.emevd.dcx. If an event id exists in both the main file and the linked file, the linked file always takes priority. This means that the linked file must be available to both compile and decompile any file which uses it, so to avoid complicated lookup schemes, DarkScript3 now requires that the linked file exists in the same directory as the main file (or parent dir for m29). If you have a project.json, the linked file will be copied automatically from the game directory if it doesn't exist; otherwise, you'll be prompted to select an appropriately named file manually.
Linked files are part of the emevd format itself and can be seen using "View > EMEVD Data", but only common_func, common, and m29 may be used for linked event initialization. common is linked by non-chalice map files in Bloodborne as well as some unused test maps in AC6. m29 is linked by chalice dungeons in Bloodborne and can be placed in the emevd file's parent directory instead, because all chalice dungeon event scripts are in subdirectories. Editing all 2641 chalice dungeon scripts manually is probably not a great idea, though.
To resolve a linked file, a file with a correct name in the same directory is used. JS files always take priority over emevd files when both exist. In the case of JS files, it scans the source code to find all eligible events and their parameter types and names. Otherwise, it calculates names and types for event parameters from the emevd file, assuming that they will become new-style events when decompiled.
Be careful when changing arguments in linked files like common_func because you will need to recompile all other files which initialize those events. If you add/remove an argument, all other initializations must be updated. If you change an argument type, recompilation may also be necessary, because using an int instead of a byte or vice versa may cause arguments to shift their offset. Using an int instead of a float or vice versa will change how the number is parsed. If you change vanilla common_func events, this may affect initializations even in files you haven't decompiled yet. Some kind of automated search may be possible to support these changes, but in general making a new copy of the event is always safer.





