Script Editing - Kizari/Flagrum GitHub Wiki
このガイドの日本語版をご覧になりたい方は、こちらをクリックしてください。
- Requirements
- Introduction and Terminology
- Finding and Converting Script Files
- Anatomy of an EXML File
- Connections
- Containers
- Deleting Existing Behaviour
- Altering Existing Behaviour
- Adding New Behaviour
- Putting the Script In Game
You will need:
- Final Fantasy XV Windows Edition
- Flagrum
- A text editor—preferably one that can handle XML syntax (Notepad++ is a good example, but any will do)
FFXV scripts are very similar to the likes of Blueprints in Unreal Engine. They are a series of nodes that are connected together to form a sequence of actions for the game to perform.
A visual representation of a FFXV Node Graph—this is not a real tool!
These node graphs are stored in a text-based format known as XML (eXtensible Markup Language), which is a common standard of data representation. If you are not already familiar with XML, it may benefit you to learn a bit more about how it works before diving deeper into this guide.
Unfortunately, FFXV does not have a visual node editor at this time, so the only way to alter these node graphs is by rewriting the XML.
The table below is optional for getting a better understanding of how the game refers to script files.
Term | Full Name | Purpose |
---|---|---|
ebex | Ebony Entity XML | This is the name of a FFXV XML file before it is converted to XMB2 for the game. You will see this extension in URIs in Flagrum's Asset Explorer, but the underlying file type is exml. |
exml | Ebony XML | This is the name given to FFXV XML files after they are converted to XMB/XMB2. This is the format that scripts you find in the game files will already be in. |
xmb | XML Binary | This is a binary representation of XML that the game uses for performance reasons. XMB is only used in older demo versions of the game. |
xmb2 | XML Binary Version 2 | This is version 2 of the binary representation of XML. XMB2 is used in all release versions of the game and is what is contained in exml files before conversion to xml. |
prefab | Prefabricated | This file is generally used in environments and is basically just a group of constructs that share a similar theme. These are effectively just another exml file. |
Flagrum's Asset Explorer is the best tool for this job. Any file that has the exml
extension is a script. While these scripts are in XMB2 form, Flagrum will automatically convert them to XML when left-clicking to preview so that they are human-readable. To edit a script, you can Right-Click > Export as XML
to save the file to disk.
WARNING: Do NOT try to [Ctrl + F] to find keywords in Flagrum's XML preview as it will
only search the text that is currently visible on screen! Export the file before searching it this way.
Every EXML file contains a <package>
element which is just a container for the contents of the file. These are often referred to by the engine as an Entity Package. You will seldom ever edit this.
Directly inside the package is an <objects>
element, which is just declares that a list of <object>
elements will be inside of it.
<package name="Name_of_Package">
<objects>
...
</objects>
</package>
Inside the <objects>
element is a list of <object>
elements. An <object>
represents one construct on the node graph. This is usually either a container, or a node. You can think of a node as one box on the visual representation at the start.
<object objectIndex="75" ownerPath="nodes_" checked="True" type="SQEX.Ebony.Framework.Sequence.Variable.SequenceConstInt" path="entities_.Ctrl.nodes_.[0].nodes_.[38].nodes_.[3]" name="[3]" ownerIndex="71" owner="entities_.Ctrl.nodes_.[0].nodes_.[38]">
<out_>
<connections_>
<reference objectIndex="72" reference="True" relativePath="refInPorts_.Mode" object="entities_.Ctrl.nodes_.[0].nodes_.[38].nodes_.[0]" />
</connections_>
</out_>
<value_ type="int">2</value_>
</object>
The above example is a simple node. Let's break it down.
Attribute | Purpose |
---|---|
objectIndex | This is a unique number for identifying the object. No two objects in the file can have the same index. |
ownerPath | This is the path to the container which this object is inside of. |
checked | This determines whether the object is active or not. |
type | The most important attribute, this is the type of object that this is and determines its behaviour. |
path | This is the path to the object. It is essentially just a combination of all paths of the containers it is within separated by a . plus its own name. |
name | This is the name of the object. This should also be unique. |
ownerIndex | The index of the container object that this object is inside of. |
owner | The path of the container object that this object is inside of. |
It is important to keep each of these attributes in mind when adding your own nodes, as they will need to have the correct values to function.
Nodes in the node graph are connected together to create the overall sequence of events. There are two types of connections:
Trigger Connections — These control the order in which nodes are executed
Value Connections — These pass values from one node to another
In general, nodes must be inside the same container to be connected successfully.
Take the following two nodes for example. Some of the data has been omitted to make it easier to read.
<object objectIndex="2" path="entities_.nodes_.[5]" name="[5]">
<out_>
<connections_>
<reference objectIndex="3" reference="True" relativePath="_in" object="entities_.nodes_.[6]" />
</connections_>
</out_>
</object>
We'll refer to this as "object 2."
<object objectIndex="3" path="entities_.nodes_.[6]" name="[6]">
<in_>
<connections_>
<reference objectIndex="2" reference="True" relativePath="_out" object="entities_.nodes_.[5]" />
</connections_>
</in_>
</object>
We'll refer to this as "object 3."
You can see that object 2 has an element called <out_>
. We know this is a port on the node because it has a <connections_>
element inside it. We can infer from the name that it is an output port. Likewise, object 3 has an input port.
We can see by the path of each object that they are inside the same container, as the path is identical besides for the name at the very end. Therefore, we know that these can be connected successfully.
Nodes must be connected at both ends to form a successful connection.
Inside of <connections_>
on object 2, the <reference>
element means that it's a reference to another object.
The structure is simple.
Attribute | Purpose |
---|---|
objectIndex | The index of the object this reference is pointing to |
reference | Not entirely sure why this was needed as it will always be true, but this must be here |
relativePath | The path from the reference object to the port this connection is connecting to |
object | The path of the object this reference is connecting to |
With this in mind, it is clear that this reference is a connection to object 3's <in_>
port.
Likewise, there is a reference back to object 2's <out_>
port on object 3, as this must be connected at both ends.
The above example can be visually represented as follows.
In EXML, a container is an object that can hold other objects.
A container may hold many nodes or only a few—not all nodes have to be in a container
The game's behaviour is mostly controlled by sequence scripts. As an example of a container, all sequence scripts have a sequence container that holds all the nodes that are part of the sequence of events.
<object objectIndex="1" ownerPath="entities_" checked="True" type="SQEX.Ebony.Framework.Sequence.SequenceContainer" path="entities_.Layout" name="Layout" ownerIndex="0" owner="">
<updateAtPause_ type="bool">False</updateAtPause_>
<nodes_>
<reference objectIndex="2" reference="True" object="entities_.Layout.nodes_.[70]" />
<reference objectIndex="3" reference="True" object="entities_.Layout.nodes_.[71]" />
<reference objectIndex="4" reference="True" object="entities_.Layout.nodes_.[72]" />
<reference objectIndex="5" reference="True" object="entities_.Layout.nodes_.[73]" />
<reference objectIndex="6" reference="True" object="entities_.Layout.nodes_.[74]" />
<reference objectIndex="7" reference="True" object="entities_.Layout.nodes_.[75]" />
<reference objectIndex="8" reference="True" object="entities_.Layout.nodes_.[76]" />
<reference objectIndex="9" reference="True" object="entities_.Layout.nodes_.[82]" />
<reference objectIndex="10" reference="True" object="entities_.Layout.nodes_.[83]" />
<reference objectIndex="11" reference="True" object="entities_.Layout.nodes_.[84]" />
<reference objectIndex="12" reference="True" object="entities_.Layout.nodes_.[77]" />
<reference objectIndex="13" reference="True" object="entities_.Layout.nodes_.[81]" />
<reference objectIndex="14" reference="True" object="entities_.Layout.nodes_.[80]" />
<reference objectIndex="15" reference="True" object="entities_.Layout.nodes_.[79]" />
<reference objectIndex="16" reference="True" object="entities_.Layout.nodes_.[78]" />
</nodes_>
<lastCenterX_ type="float">457.4387</lastCenterX_>
<lastCenterY_ type="float">52.94099</lastCenterY_>
<bIsPrefabTopSequence_ type="bool">True</bIsPrefabTopSequence_>
</object>
The important thing to note with container objects is that they contain a <nodes_>
element. This is what determines which nodes are inside of the container. The <reference>
elements within the <nodes_>
collection is identical to the <reference>
elements from the connections section, so you may refer back there for the structure.
Sometimes there are parts of the game that we don't like and may wish to remove. An example of a mod I created was one that removed the Photo Contest from Galdin Quay, as it was never removed after the event ended.
To remove a node, you must remove the node itself, as well as any references to it. Be aware that any behaviour that is connected to the <out_>
connector of the removed node will cease to function, so if this is not the intention, be sure to create a new connection to the previous node in the sequence.
To remove the node itself, simply delete the entire object starting at <object...>
and ending at </object>
.
To remove a reference to the node, delete the entire <reference ... />
tag.
There are countless nodes in the game, and very few have been explored, so it's not possible to cover this in great detail here. This is the part where you will need to experiment with different values to find out what does what. It would be great if you could also share any significant findings with us in the EXINERIS Discord so that we can improve this documentation further.
To alter existing behaviour, you want to change values inside the nodes other than the connections that may have an observable effect on the game.
Take the following example:
<object objectIndex="347" ownerPath="nodes_" checked="True" type="Black.Sequence.Actor.SequenceActionActorSetStatusInt" path="entities_.Ctrl.nodes_.[0].nodes_.[117]" name="[117]" ownerIndex="18" owner="entities_.Ctrl.nodes_.[0]">
<Isolated_ type="bool">False</Isolated_>
<in_>
<connections_>
<reference objectIndex="18" reference="True" relativePath="triInPorts_.CloseMenu" object="entities_.Ctrl.nodes_.[0]"/>
</connections_>
</in_>
<target_ value="10" type="enum">TARGET_PLAYER_NOCTIS</target_>
<kind_ value="789" type="enum">STATUS_KNIGHTS_OF_EOS</kind_>
<value_ type="int">1</value_>
</object>
A group in the EXINERIS Discord discovered how to activate the flying armiger mode by looking through the Leviathan fight.
By changing the <kind_>
value of the SequenceActionActorSetStatusInt
to 789 (STATUS_KNIGHTS_OF_EOS)
, and setting the <target_>
value to 10 (TARGET_PLAYER_NOCTIS)
, this mode could be enabled anywhere in the game by connecting it to a node somewhere else.
The original node also had an <out_>
element, which was removed as no additional behaviour was required to be executed after this, and an <inValue_>
element, which was removed as we just wanted it to use the <value_>
element instead to avoid needing an extra unnecessary node. The <value_>
itself was set to 1
for on
, but can also be set to 0
for off
to disable the mode if it is already active.
This is of course the most exciting prospect of script editing. The ability to add your own behaviour to the game.
Following on from the example above, while it is great to be able to alter the flying armiger mode in the Leviathan fight, it's much more useful to be able to enable it elsewhere in the game world.
As a simple activation method for the flying armiger mode, it was proposed to have it activate when interacting with a chocobo rental machine. I opened up menuswfentry_chocoborental.exml
and started scrolling until I saw this <CloseMenu>
element on object 4.
<CloseMenu dynamic="True" type="SQEX.Ebony.Framework.Node.GraphTriggerInputPin">
<pinName_ type="string">CloseMenu</pinName_>
<connections_>
<reference objectIndex="56" reference="True" relativePath="in_" object="entities_.Ctrl.nodes_.[0].nodes_.[74]" />
</connections_>
<isBrowsable_ type="bool">True</isBrowsable_>
<delayType_ value="1" type="enum">DT_TIME</delayType_>
<delayTime_ type="float">0</delayTime_>
<delayMaxTime_ type="float">-1</delayMaxTime_>
<pinType_ value="0" type="enum">PT_ARBITRARY</pinType_>
</CloseMenu>
I decided that we could plug the node in here so it could be activated when the menu for the rental is closed.
Seeing that there was a connection to object 56 here, I went down to that object and pasted the modified SequenceActionActorSetStatusInt
node from the Altering Existing Behaviour section of this guide right below it.
We know that no two objects in an exml
file can have the same index, so I scrolled to the very bottom of the file where I noticed the last object was 60. I updated the index of this newly pasted node to 61 so it would not conflict.
Looking at object 56, the ownerIndex was 4, so we know that object 56 is inside container object 4. I went up to this container to add a reference to this new node as covered in the Containers section.
<nodes_>
<reference objectIndex="5" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[13]" />
<reference objectIndex="6" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[14]" />
<reference objectIndex="7" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[15]" />
<reference objectIndex="8" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[16]" />
<reference objectIndex="14" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[31]" />
<reference objectIndex="15" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[49]" />
<reference objectIndex="16" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[55]" />
<reference objectIndex="31" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[58]" />
<reference objectIndex="32" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[60]" />
<reference objectIndex="33" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[61]" />
<reference objectIndex="34" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[62]" />
<reference objectIndex="35" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[64]" />
<reference objectIndex="42" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[65]" />
<reference objectIndex="43" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[66]" />
<reference objectIndex="44" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[67]" />
<reference objectIndex="45" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[68]" />
<reference objectIndex="46" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[69]" />
<reference objectIndex="47" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[70]" />
<reference objectIndex="48" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[71]" />
<reference objectIndex="49" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[72]" />
<reference objectIndex="55" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[73]" />
<reference objectIndex="56" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[74]" />
<reference objectIndex="57" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[75]" />
<reference objectIndex="61" reference="True" object="entities_.Ctrl.nodes_.[0].nodes_.[76]" />
</nodes_>
I simply copied the reference to 57, and updated it accordingly. I used 61
as that's the index I just gave the new node, and I used [76]
as the name, as it was the next available number in the sequence.
Back down to the new node, I copied the ownerPath
, path
, ownerIndex
, and owner
from object 56. This is because I want it to be in the same container as 56. I then updated name
to [76]
to match the new reference, and changed the end of the path to [76]
as well.
Lastly, this node needed to be connected to the <CloseMenu>
behaviour so it will be triggered when the menu is closed. As covered in the Connections section, a connection is needed at both ends to achieve this.
<object objectIndex="61" ownerPath="nodes_" checked="True" type="Black.Sequence.Actor.SequenceActionActorSetStatusInt" path="entities_.Ctrl.nodes_.[0].nodes_.[76]" name="[76]" ownerIndex="4" owner="entities_.Ctrl.nodes_.[0]">
<Isolated_ type="bool">False</Isolated_>
<in_>
<connections_>
<reference objectIndex="4" reference="True" relativePath="triInPorts_.CloseMenu" object="entities_.Ctrl.nodes_.[0]" />
</connections_>
</in_>
<target_ value="10" type="enum">TARGET_PLAYER_NOCTIS</target_>
<kind_ value="789" type="enum">STATUS_KNIGHTS_OF_EOS</kind_>
<value_ type="int">1</value_>
</object>
Above is the final XML for the flying armiger node. You can see the reference connects it back to the <CloseMenu>
port of the <triInPorts_>
element on object 4. Likewise, a reference to the <in_>
port on the new node (object 61) was added to the <CloseMenu>
element on object 4 to complete the connection.
<CloseMenu dynamic="True" type="SQEX.Ebony.Framework.Node.GraphTriggerInputPin">
<pinName_ type="string">CloseMenu</pinName_>
<connections_>
<reference objectIndex="56" reference="True" relativePath="in_" object="entities_.Ctrl.nodes_.[0].nodes_.[74]" />
<reference objectIndex="4" reference="True" relativePath="in_" object="entities_.Ctrl.nodes_.[0].nodes_.[76]" />
</connections_>
<isBrowsable_ type="bool">True</isBrowsable_>
<delayType_ value="1" type="enum">DT_TIME</delayType_>
<delayTime_ type="float">0</delayTime_>
<delayMaxTime_ type="float">-1</delayMaxTime_>
<pinType_ value="0" type="enum">PT_ARBITRARY</pinType_>
</CloseMenu>
With the script finished, all that was left was to put it in-game. Thankfully, Flagrum makes this a breeze. I simply created a new mod in the Mod Manager, replaced the menuswfentry_chocoborental.exml
file with my modified XML file, and hit save. Mod finished and ready to go as soon as the game is next launched.
For more details on using the Mod Manager either for managing mods or making your own, check out this guide.