• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

Warcraft 3 Trigger Format Specification (WTG!) - Reforged 1.36

Level 29
Joined
Sep 26, 2009
Messages
2,595

Warcraft 3 Trigger Format Specification (WTG!) - Reforged 1.36​


For classic Warcraft's WTG file definition see Warcraft 3 Trigger Format Specification (WTG!)

Disclaimers

The changes I will write about are for Reforged version 1.36, however that is because I started investigating wtg files on this version. It is entirely possible that these changes were introduced in earlier versions.
While I have noticed some odd structures, I don't have the resources to test hundreds/thousands of maps to ensure those are the only odd structures. As such, you may encounter some other odd structures that will not be covered in this post.


Since March 2024, I tried to create a tool for migrating custom races between maps. As such, it was important for me to be able to visualize all object editor and trigger editor related data so that I may pick and choose what to migrate and what not. During my investigations, I've noticed that certain GUI trigger functions use odd structures. Browsing the web, I've found multiple tools/github repos, but I didn't see any of them addressing those odd structures. That lead me to share here what I've found.

Basics​

Like other map files, the wtg file is a binary file. You should use a hex editor (like Hex Editor Neo) to investigate their content.
You will need to adhere to the structures described later in the post to read and evaluate groups of bytes.
Bytes are displayed in hexadecimal numeral system, e.g. 0x10 is hexadecimal representation of number 16, and 0xff of number 255.
Structs contain a list of data types and their name/meaning. You may read here https://www.thehelper.net/threads/explanation-of-w3m-and-w3x-files.35292/ what data types are supported in WC3, but a tl;dr for WTG files is:

Data typeSizeComment
integer4 bytesThese are stored using Little Endian order.
integer[n]nn-times repeated integer
boolean4 bytesThis is not a real type, it's more of a helper type for this document. It is basically an integer type with either value zero or one, matching boolean values false or true.
stringvariable lengthThese can be of any length, but they are always terminated by \0 (a 00 byte). It is possible to have an empty string, in which case the value consists only of terminator \0
char[n]n bytesFixed size string. Since it is fixed size, it is not terminated by \0.
structvariable lengthA struct can contain other struct(s). If the struct has square brackets like [n], then it means that struct is repeated n-times. For example a GUI trigger struct will contain multiple GUI function structs.

Terms​

Let's agree on the following terms about trigger editor:
  • Object Tree View - The left side panel where you see all your various objects like GUI triggers, categories/folders, etc, and where you organize your triggers, scripts and variables.
  • Object Detail View- The right-side panel which changes based on which object you have selected. From top to bottom, it has the following parts:
    • Flags - checkboxes like if the trigger is enabled, if it is turned on, etc.
    • Comment Section - Where you write comment
    • Function Section - Where you write script or place GUI actions
    • Based on object type, flags will be changed and some sections may not be avaiable
    • Variables have completely different right-side panel
  • Variable Editor - The classic way to create and manage variables, opened via CTRL + B keys.
  • ECA - a short name for Event/Condition/Action

Contents of WTG file​

What WTG file contains:
  • Tree structure of folders, variables and other trigger editor objects (the tree view)
  • Counts of each type of object
  • Table of defined global variables, including their definition (name, type, size, etc.)
  • Settings of each object type (i.e. whether a gui trigger is enabled or not)
  • Content of GUI triggers
What WTG file does not contain:
  • Map header's comment section
  • Map header's custom script section
  • Content of 'Custom script' files
  • Script content of GUI triggers that have been transformed to Custom Text via context menu
The content not stored in .wtg file is stored in .wct file instead.

triggerdata.txt​

The triggerdata.txt is an INI file, found in WC3's casc file under path .\war3.w3mod\ui\triggerdata.txt.
GUI triggers are tightly coupled with triggerdata.txt file: The wtg file uses an internal identifier for all its functions and these identifiers are referenced by the triggerdata.txt file. The reason for the tight coupling is because many GUI trigger functions have various amount of parameters. The WTG file does not contain information about how many parameters a function has. Instead, you need to use the identifier and search the triggerdata.txt file to determine parameter count.

Let's take the following GUI action as example:
  • Wait 2.00 seconds
When looking into the wtg file, we will see that its internal identifier is TriggerSleepAction. Looking into triggerdata.txt, we will find the following section:
Code:
TriggerSleepAction=0,real
_TriggerSleepAction_DisplayName="Wait"
_TriggerSleepAction_Parameters="Wait ",~Time," seconds"
_TriggerSleepAction_Defaults=2
_TriggerSleepAction_Limits=0,_
_TriggerSleepAction_Category=TC_WAIT
Important is the first line. We can ignore the first value which is either 0 or 1 (this is just a version this action is available from). But after that is a comma-delimited list of parameters. In this case, we can see that the function has only a single parameter of type real.

Another example:
  • Unit - Cause (Triggering unit) to damage (Triggering unit), dealing 500.00 damage of attack type Spells and damage type Normal
This has internal identifier UnitDamageTargetBJ which is described in triggerdata.txt as:
Code:
UnitDamageTargetBJ=1,unit,unit,real,attacktype,damagetype
_UnitDamageTargetBJ_DisplayName="Damage Target"
_UnitDamageTargetBJ_Parameters="Cause ",~Unit," to damage ",~Target,", dealing ",~Amount," damage of attack type ",~AttackType," and damage type ",~DamageType
_UnitDamageTargetBJ_Defaults=GetTriggerUnit,GetTriggerUnit,500,AttackTypeNormal,DamageTypeNormal
_UnitDamageTargetBJ_Category=TC_UNIT
From that, we can see that this function has 5 parameters: unit, unit, real, attacktype and damagetype.

Important:
Note that functions without parameters may actually have a single special parameter defined in triggerdata.txt file: nothing. For example this action:
  • Do nothing
is defined like this:
Code:
DoNothing=0,nothing
_DoNothing_DisplayName="Do Nothing"
_DoNothing_Parameters="Do nothing"
_DoNothing_Defaults=
_DoNothing_Category=TC_NOTHING
As such, you should omit every parameter of type nothing when counting parameters.

Of special note is the line where the key starts with underscore _ and ends with _Category: These contain identifier to the category's icon and localized name. For example value of _UnitDamageTargetBJ_Category is TC_UNIT. Using that value as key in triggerdata.txt, we will find value WESTRING_TRIGCAT_UNIT,ReplaceableTextures\WorldEditUI\Actions-Unit. Splitting that by comma , we get two values:
  • The first value is a key for localized string/name
  • the second is path to category's icon.
The key WESTRING_TRIGCAT_UNIT represents key for localized string and its value can be found in .\war3.w3mod\_locales\{locale like enus}.w3mod\ui\worldeditstrings.txt.


WTG structs​

Struct fields are written in the order in which data are present in Wtg file, i.e. top-most field will be present first in the wtg.

Root

This is the struct with which each and every wtg file starts.
Rich (BB code):
char[4]                        FileId                     // Always contains bytes '0x 57 54 47 21', representing ascii characters 'WTG!'
integer                        MagicNumber                // Not clear on the meaning. Seems to contain bytes '0x 04 00 00 80'. Could be an identifier to determine Reforged version.
integer                        FileFormatVersion          // 4 = Reign of Chaos, 7 = Frozen Throne. This seems to be obsolete for Reforged and will always be version 7.
struct TypeInfo                MapHeaderInfo
struct TypeInfo                LibraryInfo
struct TypeInfo                CategoryInfo
struct TypeInfo                TriggerInfo
struct TypeInfo                CommentInfo
struct TypeInfo                ScriptInfo
struct TypeInfo                VariableInfo
integer                        Unknown #1                 // Always seems to be zero
integer                        Unknown #2                 // Always seems to be zero
integer                        TriggerDefinitionVersion
integer                        VariableDefinitionCount
struct VariableDefinition[n]   VariableDefinitions       // Number 'n' equals VariableDefinitionCount
integer                        TriggerObjectCount
struct TriggerObject[n]        TriggerObjects            // All trigger objects found in tree view. Number 'n' equals TriggerObjectCount


TypeInfo

This struct keeps track of number of existing trigger objects of the given type and how many were deleted. It also keeps track of all deleted objects' ids. Probably serves for reusing objectIds and generating new ones.
Rich (BB code):
integer     Total               // Total count of objects of the given type
integer     DeletedCount        // Number of deleted objects
integer[n]  DeletedObjectIds    // A list of deleted object ids of the given type


VariableDefinition

Definition of the variable, as set up in variable editor.
Rich (BB code):
string    Name              // Variable name
string    Type              // Variable type. They match values found in [TriggerTypes] section of triggerdata.txt
integer   Category          // Value seems to always be '1' that would match the 'Scripts/Initialization' category in variable editor
boolean   IsArray           // Whether variable is array
integer   ArraySize         // Size of the array. Non array variables always have value '1'
boolean   IsInitialized     // Whether this variable has an initial value or not
string    InitialValue      // If 'IsInitialized' is false, then this is empty string
integer   ObjectId          // Unique id of the variable, always ends with byte '0x06'
integer   ParentId          // Id of an object that this variable is child of (i.e. some category)

Raw bytes extracted from wtg:
Code:
0x:
44 69 61 67 00 64 69 61 6c 6f 67 00 01 00 00 00
00 00 00 00 01 00 00 00 00 00 00 00 00 01 00 00
06 01 00 00 02
Parsed as VariableDefinition:
Rich (BB code):
[44 69 61 67 00]            // ascii characters for 'Diag' terminated by null \0
[64 69 61 6c 6f 67 00]      // ascii characters for 'dialog' terminated by null \0
[01 00 00 00]               // category is '1'
[00 00 00 00]               // is array: false
[01 00 00 00]               // array size: 1
[00 00 00 00]               // is initialized: false
[00]                        // empty string for initial value
[01 00 00 06]               // object id of the variable, ends with 0x06
[01 00 00 02]               // parent id, points to category/folder



TriggerObject

This can be any object you can see in the tree view of the Trigger Editor. Each TriggerObject starts with its object type. Based on the type a different struct follows. Weirdly, one of the possible structs is a struct describing variables (although in this case, the variable struct is way simpler than VariableDefinition struct).
Rich (BB code):
integer   ObjectType            // Type of the object. Determines which struct follows. See table below for mapping.
struct    ObjectDefinition      // The actual struct matching ObjectType. See table below for mapping object types to their respective structs.
ObjectType valueMeaningStruct name
1Map headerMapHeader
2LibraryNo struct, it is unused as far as I know
4Category / FolderCategory
8TriggerTrigger
16Trigger commentTrigger
32Custom scriptTrigger
64Global variableVariable

As a side note, trigger objects are ordered in the order they appear in the tree view from top to bottom. Whether they are children of other objects or not does not play any role. Consider the following tree view. Numbers in the example denote in what order those objects are stored in wtg file:
Code:
#1 root.w3x
|- #2
|   |- #3
|   |- #4
|
|- #5
    |- #6
    |   |- #7
    |
    |- #8


MapHeader

Represents the root node/map node in Tree View
Rich (BB code):
integer   ObjectId          // Unique id of the object. The id is always '0'
string    Name              // Name of the map
boolean   IsComment         // Whether Script block is hidden. Always seems to be false
boolean   IsExpandable      // Whether this is expandable (has the +/- symbol next to name). Always seems to be false.
integer   ParentId          // Id of an object that this object is child of. Since this is root node, ParentId is always '-1'


Category

Represents a category folder or a category comment.
Rich (BB code):
integer   ObjectId          // Unique id of the object. Always ends with byte '0x02'
string    Name              // Name of the category
boolean   IsCategory        // Whether this is a category folder or category comment (interchangeable via right-click context menu)
boolean   IsExpandable      // Whether this category has child objects or not (i.e. whether it can be expanded in tree view or not)
integer   ParentId          // Id of an object that this object is child of

Raw bytes extracted from wtg:
Code:
0x:
04 00 00 00 00 00 00 02 4d 79 46 6f 6c 64 65 72
4e 61 6d 65 00 00 00 00 00 01 00 00 00 00 00 00
00
Parsed as TriggerObject:
Rich (BB code):
[04 00 00 00]                               // objectType is 'Category', so CATEGORY struct follows
[00 00 00 02]                               // objectId
[4d 79 46 6f 6c 64 65 72 4e 61 6d 65 00]    // ascii characters for 'MyFolderName' terminated by null \0
[00 00 00 00]                               // isComment = false
[01 00 00 00]                               // isExpandable = true, so this folder has child nodes
[00 00 00 00]                               // parentId is all zeroes, so parent is MapHeader/root node


Variable

This is a simplified version of VariableDefinition struct. It contains only data needed by the tree view.
Rich (BB code):
integer   ObjectId      // Unique id of the object. Always ends with byte '0x06'
string    Name          // Name of the variable
integer   ParentId      // Id of an object that this object is child of

Raw bytes extracted from wtg:
Code:
0x:
40 00 00 00 02 00 00 06 6c 6f 63 31 00 00 00 00
02
Parsed as TriggerObject:
Rich (BB code):
[40 00 00 00]       // objectType is 'Global Variable', so VARIABLE struct follow
[02 00 00 06]       // objectId
[6c 6f 63 31 00]    // ascii characters for 'loc1' terminated by null \0
[00 00 00 02]       // parentId, it ends with 0x02 so the parent is a category/folder



Trigger

This is a generic struct that is shared by trigger comments, GUI triggers, GUI triggers converted to custom text, and custom scripts (jass/lua).
The Detail View offers the following Flags (checkboxes) based on the type of the object:
  • GUI trigger: Enabled, Initially On
  • GUI trigger converted to Custom Text: Enabled, Run on Map Initialization
  • Script: Enabled
  • Any flag that is not available to a given type of object is always set to false
Rich (BB code):
string               Name               // Name of the object
string               CommentText        // Text in the Comment Section. Default value is empty string
boolean              IsComment          // Whether Function Section in Detail View is hidden or not. Only Trigger Comments have value true
integer              ObjectId           // Unique id of the object
boolean              IsEnabled          // Represents the 'Enabled' flag
boolean              IsCustomText       // Whether this object uses GUI functions or text script
boolean              IsInitiallyOff     // Represents an inverse value of 'Initially On' flag
boolean              RunOnMapIni        // Represents the 'Run on Map Initialization' flag
integer              ParentId           // Id of an object that this object is child of
integer              FunctionCount      // Non-GUI triggers always have zero. Represents how many ECA functions this object contains
struct Function[n]   Functions          // ECA functions of the GUI trigger. Number 'n' matches 'FunctionCount'

Notes:
  • IsComment is always true for Trigger Comment.
  • ObjectId ends with different byte based on type of object:
    • GUI trigger: 0x03
    • Trigger Comment: 0x04
    • Custom script: 0x05
  • IsCustomText will always be true for Custom Script and always false for Trigger Comment. GUI trigger will switch value based on whether it was or was not converted to Custom Text trigger.

Raw bytes extracted from wtg:
Code:
0x:
10 00 00 00 53 70 65 6c 6c 20 44 65 73 63 72 69
70 74 69 6f 6e 73 00 6c 6f 72 65 6d 20 69 70 73
75 6d 00 01 00 00 00 01 00 00 04 01 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 00
00 00 00 20 00 00 00 54 65 73 74 20 53 63 72 69
70 74 00 00 00 00 00 00 04 00 00 05 00 00 00 00
01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02
00 00 00 00
Parsed as TriggerObject:
Rich (BB code):
[10 00 00 00]                                               // objectType is 'Trigger Comment', so TRIGGER struct follow
[53 70 65 6c 6c 20 44 65 73 63 72 69 70 74 69 6f 6e 73 00]  // ascii characters for 'Spell Descriptions' terminated by null \0
[6c 6f 72 65 6d 20 69 70 73 75 6d 00]                       // ascii characters for 'lorem ipsum' terminated by null \0
[01 00 00 00]                                               // isComment = true
[01 00 00 04]                                               // objectId
[01 00 00 00]                                               // isEnabled ... doesn't matter for trigger comment
[00 00 00 00]                                               // isCustomText ... doesn't matter for trigger comment
[00 00 00 00]                                               // isInitiallyOff ... doesn't matter for trigger comment
[00 00 00 00]                                               // runOnMapIni ... doesn't matter for trigger comment
[00 00 00 02]                                               // parentId, it ends with 0x02 so the parent is a category/folder
[00 00 00 00]                                               // functionCount is 0 (and will always be zero for comments)

[20 00 00 00]                                               // objectType is 'Custom Script', so TRIGGER struct follow
[54 65 73 74 20 53 63 72 69 70 74 00]                       // ascii characters for 'Test Script' terminated by null \0
[00]                                                        // this object has no comment written in it, since it is just the null value \0
[00 00 00 00]                                               // isComment = false, meaning you can write jass/lua code inside
[04 00 00 05]                                               // objectId
[00 00 00 00]                                               // isEnabled = 0, so this custom script is disabled in the editor
[01 00 00 00]                                               // isCustomText = 1, so you can write code instead of picking GUI functions
[00 00 00 00]                                               // isInitiallyOff = 0, meaning this is enabled from map start
[00 00 00 00]                                               // runOnMapIni = 0
[00 00 00 02]                                               // parentId, it ends with 0x02 so the parent is a category/folder
[00 00 00 00]                                               // functionCount is 0 (and will always be zero for custom script)


Important:
GUI triggers contain only top-level functions, they never contain nested functions. Take the If / Then / Else, Multiple function action as an example:
  • Example Trigger
    • Events
    • Conditions
    • Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • IsEnabled Equal to True
        • Then - Actions
          • Wait 5.22 seconds
        • Else - Actions
          • Skip remaining actions
As you can see, this trigger contains only a single top-level function - the If / Then / Else, Multiple function. Although the function itself contains multiple nested functions, the Trigger object will have FunctionCount equal 1 and describe only function If / Then / Else, Multiple function. The struct of the top-level function will describe the nested functions it contains.


(ECA) Function

Represents a single event, condition or action function.
Rich (BB code):
integer               Type                  // Type of the function. See table below for mapping.
integer               BlockId               // For nested functions only; top-level functions do not have this! It is the id of the function block this function belongs to
string                FunctionName          // The internal identifier of the function
boolean               IsEnabled             // Whether this function is enabled or not. Disabled functions in GUI have greyed-out text and red cogwheel for icon
struct Parameter[n]   Parameters            // Parameters of the function. The number `n` can only be from triggerdata.txt
integer               NestedFunctionCount   // Number of nested functions
struct Function[n]    NestedFunctions       // Nested ECA functions of this function. Number `n` matches `NestedFunctionCount`
Type valueName/meaning
0Event
1Condition
2Action


Let's take a very simple example. The ECA function is
  • Events
    • Map initialization
Raw bytes extracted from wtg:
Code:
0x:
00 00 00 00 4d 61 70 49 6e 69 74 69 61 6c 69 7a
61 74 69 6f 6e 45 76 65 6e 74 00 01 00 00 00 00
00 00 00
Parsed as Function:
Rich (BB code):
[00 00 00 00]                                                           // type is 'event'
[]                                                                      // this is a top level function, so blockId is missing
[4d 61 70 49 6e 69 74 69 61 6c 69 7a 61 74 69 6f 6e 45 76 65 6e 74 00]  // ascii characters for 'MapInitializationEvent' terminated by \0
[01 00 00 00]                                                           // is enabled: true
[]                                                                      // based on triggerdata.txt, this function has no parameters, so it is missing
[00 00 00 00]                                                           // nested function count: zero
[]                                                                      // no nested functions, so it is missing



Note about function blocks

Before digging deeper, let's discuss function blocks first. GUI triggers and some ECA functions can contain functions. In GUI trigger editor, you will usually see the block as a node. Each GUI trigger has the following block nodes:
  • Events
  • Conditions
  • Actions
And some ECA function can have blocks as well, like any of these:
  • Loop - Actions
  • If - Conditions
  • Then - Actions
These nodes are never present in the WTG file! Instead, you need to evaluate the context of what you are reading and insert these nodes yourself. For ECA functions, you can use their internal identifiers to make sense of it. Luckily, there is only a small list of ECA functions that have blocks, see the hidden list.

Rich (BB code):
// Functions with a single 'Conditions' block
AndMultiple
OrMultiple

// Functions with a single 'Loop - Actions' block
ForLoopAMultiple
ForLoopBMultiple
ForLoopVarMultiple
EnumDestructablesInRectAllMultiple
EnumDestructablesInCircleBJMultiple
EnumItemsInRectBJMultiple
ForForceMultiple
ForGroupMultiple

// Function with three blocks: 'If - Conditions', 'Then - Actions' and 'Else - Actions'
IfThenElseMultiple

Also, blocks are stored in reverse order inside WTG files. So a GUI trigger will first contain list of Actions, then Conditions and finally Events. However ECA functions inside each block are stored in order we see them, from top to bottom. For example consider the following trigger:
  • Example Trigger
    • Events
      • Unit - A unit Dies
    • Conditions
      • (Unit-type of (Triggering unit)) Equal to Footman
      • (Owner of (Triggering unit)) Equal to Player 1 (Red)
    • Actions
      • Special Effect - Create a special effect at (Position of (Triggering unit)) using ...
      • Unit - Explode (Triggering unit).
The WTG file will store them in this order:
  • Special Effect - Create a special effect at (Position of (Triggering unit)) using ...
  • Unit - Explode (Triggering unit).
  • (Unit-type of (Triggering unit)) Equal to Footman
  • (Owner of (Triggering unit)) Equal to Player 1 (Red)
  • Unit - A unit Dies
  • block order: from bottom to top
  • functions inside block: from top to bottom

This is not really important for GUI trigger events, condtions and actions, since you can easily determine that by their Type field, however it is good to know when it comes to If / Then / Else, Multiple function function since it follows same rules: First are stored actions of Else - Actions block, then actions of the Then - Actions block and finally conditions of the If - Conditions block.
You can use the BlockId field to determine the block, since these values are static:
BlockId valueBlock name
0If - Conditions
1Then - Actions
2Else - Actions

For ECA functions that contain only a single block, like Loop - Actions block of Pick Every Unit In Unit Group And Do Multiple Actions action, the BlockId will be present and its value will always be zero.


Parameter

Represents a single parameter of an ECA function.
Rich (BB code):
integer               Type                  // Type of the parameter. See table below for mapping.
string                Value                 // Value of the parameter. It is always stored as string even in cases where the value is a number
boolean               SupportsParameters    // Only applicable to parameters of type 'function'. If value is 'true', check triggerdata.txt to determine parameters
struct Parameter[n]   Parameters            // Parameters of this parameter. Number 'n' can be determined from triggerdata.txt
boolean               IsArray               // Usable only for parameters of type 'variable', for others this is always false.
integer               ArrayIndexValue       // Only applicable if 'IsArray' is true. If 'IsArray' is false, this field will not be present in the struct
Type valueName/meaning
0Preset
1Variable
2Function
3String
-1Invalid
Parameter will be marked as invalid for example when a parameter references directly a unit in your map and you delete that unit. Or reference a variable and you delete it or change its type.

Let's look at the following action:
  • Actions
    • Set VariableSet x = y
Raw bytes extracted from wtg:
Code:
0x:
02 00 00 00 53 65 74 56 61 72 69 61 62 6c 65 00
01 00 00 00 01 00 00 00 78 00 00 00 00 00 00 00
00 00 01 00 00 00 79 00 00 00 00 00 00 00 00 00
00 00 00 00
Parsed as ECA Function:
Rich (BB code):
[02 00 00 00]                           // function type is 'action'
[53 65 74 56 61 72 69 61 62 6c 65 00]   // ascii characters for 'SetVariable' terminated by \0
[01 00 00 00]                           // is enabled: true
-- Start of Parameters:                 // triggerdata.txt shows two parameters
    first parameter:
    [01 00 00 00]                           // parameter type is 'variable'
    [78 00]                                 // ascii characters for 'x' terminated by \0
    [00 00 00 00]                           // supports parameters: false
    []                                      // this has no parameters, so that struct is missing
    [00 00 00 00]                           // is array: false
    []                                      // missing since this is not an array
 
    second parameter:
    [01 00 00 00]                           // parameter type is 'variable'
    [79 00]                                 // ascii characters for 'y' terminated by \0
    [00 00 00 00]                           // supports parameters: false
    []                                      // this has no parameters, so that struct is missing
    [00 00 00 00]                           // is array: false
    []                                      // missing since this is not an array
--End of parameters
[00 00 00 00]                           // nested function count: zero
[]                                      // no nested functions, so it is missing



WrapperParameter

This is a special type of parameter. I haven't seen anyone addressing this, nor am I sure since which version was such structure present. It may have been an accident, or a never finished work-in-progress on Blizzard's side, however I have seen this come up consistently during my investigations.
This stucture replaces the default Parameter structure whenever the type of parameter (as read from triggerdata.txt) is boolexpr, boolcall or code.
Simply put, it is a parameter with static values that wraps an ECA function.
Rich (BB code):
integer              Type               // Type of the parameter. Value is always '2' (Function)
string               Value              // For 'boolexpr' and 'boolcall', this is always empty string, for 'code' this is always 'DoNothing'
integer              FunctionCount      // This seems to always be '1'
struct Function[n]   Functions          // ECA functions. Number 'n' matches FunctionCount'
boolean              IsArray            // Is always 'false'
Technically speaking, one can replace WrapperParameter for Parameter struct, since their format is very similar, however I believe thinking of this in terms of ECA Function struct makes more sense. A boolexpr's Parameter would be of type 1, which would be Variable, but if we think of this as ECA Function, the type would match that of Condition. Same for code, where its parameter would be of type 2 (function), but in terms of ECA Function it would be of type Action.
Another reason is that any argument for parameter of boolexpr, boolcall and code can appear as a standalone ECA function, so it makes sense that the structure would be kept same even when such functions are inlined into different function.

We will look at the following action:
  • Actions
    • If (IsEnabled Equal to True) then do (Wait 5.22 seconds) else do (Skip remaining actions)
Raw bytes extracted from wtg:
Code:
0x:
02 00 00 00 49 66 54 68 65 6e 45 6c 73 65 00 01
00 00 00 02 00 00 00 00 01 00 00 00 01 00 00 00
4f 70 65 72 61 74 6f 72 43 6f 6d 70 61 72 65 42
6f 6f 6c 65 61 6e 00 01 00 00 00 01 00 00 00 49
73 45 6e 61 62 6c 65 64 00 00 00 00 00 00 00 00
00 00 00 00 00 4f 70 65 72 61 74 6f 72 45 71 75
61 6c 45 4e 45 00 00 00 00 00 00 00 00 00 03 00
00 00 74 72 75 65 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 02 00 00 00 44 6f 4e 6f 74
68 69 6e 67 00 01 00 00 00 02 00 00 00 54 72 69
67 67 65 72 53 6c 65 65 70 41 63 74 69 6f 6e 00
01 00 00 00 03 00 00 00 35 2e 32 32 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 02 00 00
00 44 6f 4e 6f 74 68 69 6e 67 00 01 00 00 00 02
00 00 00 52 65 74 75 72 6e 41 63 74 69 6f 6e 00
01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Parsed as ECA Function:
Rich (BB code):
[02 00 00 00]                       // type is 'action'
[]                                  // this is a top level function, so blockId is missing
[49 66 54 68 65 6e 45 6c 73 65 00]  // ascii characters for 'IfThenElse' terminated by \0
[01 00 00 00]                       // is enabled: true
--Start of Parameters:              // from triggerdata.txt we know it has three params: boolexpr, code and code
  first param:                          // param type is 'boolexpr', so WrapperParameter struct follows
    [02 00 00 00]                       // type is 'function'
    [00]                                // value is empty string
    [01 00 00 00]                       // function count: one
    --Start of wrapped ECA Function
        [01 00 00 00]                                                           // type is 'condition'
        []                                                                      // this is not a nested function, so blockId is missing
        [4f 70 65 72 61 74 6f 72 43 6f 6d 70 61 72 65 42 6f 6f 6c 65 61 6e 00]  // ascii characters for 'OperatorCompareBoolean' terminated by \0
        [01 00 00 00]                                                           // is enabled: true
        --Start of Parameters:                                                  // from triggerdata.txt we know that it has three params: boolean, EqualNotEqualOperator and boolean
          first param:
            [01 00 00 00]                           // type is 'variable'
            [49 73 45 6e 61 62 6c 65 64 00]         // ascii characters for 'IsEnabled' terminated by \0
            [00 00 00 00]                           // supports parameters: false
            []                                      // this has no parameters, so that struct is missing
            [00 00 00 00]                           // is array: false
            []                                      // missing since this is not an array

          second param:
            [00 00 00 00]                                           // type is 'preset'
            [4f 70 65 72 61 74 6f 72 45 71 75 61 6c 45 4e 45 00]    // ascii characters for 'OperatorEqualENE' terminated by \0
            [00 00 00 00]                                           // supports parameters: false
            []                                                      // this has no parameters, so that struct is missing
            [00 00 00 00]                                           // is array: false
            []                                                      // missing since this is not an array

          third param:
            [03 00 00 00]                           // type is 'string'
            [74 72 75 65 00]                        // ascii characters for 'true' terminated by \0
            [00 00 00 00]                           // supports parameters: false
            []                                      // this has no parameters, so that struct is missing
            [00 00 00 00]                           // is array: false
            []                                      // missing since this is not an array
        --End of parameters
        [00 00 00 00]                                                           // nested function count: zero
        []                                                                      // no nested functions, so it is missing
    --End of wrapped ECA Function
    [00 00 00 00]                       // is array: false
  second param:                         // param type is 'code', so WrapperParameter struct follows
    [02 00 00 00]                       // type is 'function'
    [44 6f 4e 6f 74 68 69 6e 67 00]     // ascii characters for 'DoNothing' terminated by \0
    [01 00 00 00]                       // function count: one
    --Start of wrapped ECA Function
        [02 00 00 00]                                               // type is 'action'
        []                                                          // this is not a nested function, so blockId is missing
        [54 72 69 67 67 65 72 53 6c 65 65 70 41 63 74 69 6f 6e 00]  // ascii characters for 'TriggerSleepAction' terminated by \0 
        [01 00 00 00]                                               // is enabled: true
        --Start of Parameters:                                      // from triggerdata.txt we know that 'TriggerSleepAction' has single param: real
          first param:
            [03 00 00 00]                           // type is 'string'
            [35 2e 32 32 00]                        // ascii characters for '5.22' terminated by \0
            [00 00 00 00]                           // supports parameters: false
            []                                      // this has no parameters, so that struct is missing
            [00 00 00 00]                           // is array: false
            []                                      // missing since this is not an array
        --End of parameters
        [00 00 00 00]                                               // nested function count: zero
        []                                                          // no nested functions, so it is missing
    --End of wrapped ECA Function
    [00 00 00 00]                       // is array: false
  third param:                          // param type is 'code', so WrapperParameter struct follows
    [02 00 00 00]                       // type is 'function'
    [44 6f 4e 6f 74 68 69 6e 67 00]     // ascii characters for 'DoNothing' terminated by \0
    [01 00 00 00]                       // function count: one
    --Start of wrapped ECA Function
        [02 00 00 00]                               // type is 'action'
        []                                          // this is not a nested function, so blockId is missing
        [52 65 74 75 72 6e 41 63 74 69 6f 6e 00]    // ascii characters for 'ReturnAction' terminated by \0
        [01 00 00 00]                               // is enabled: true
        []                                          // from triggerdata.txt we know that 'ReturnAction' has no params
        [00 00 00 00]                               // nested function count: zero
    --End of wrapped ECA Function
    [00 00 00 00]                       // is array: false
--End of parameters
[00 00 00 00]                       // nested function count: zero
[]                                  // no nested functions, so it is missing



NestedParameter

This may be a bug, or an attempt to enforce parameter encapsulation in round brackets when shown in Trigger Editor, e.g. encapsulation in this condition:
Code:
((Triggering unit) is A structure) Equal to True
This structure seems to be present for all parameters of type function, unless the WrapperParameter custom structure is used. The structure completely matches the Parameter struct, but it is populated in the following way:
  • ParameterType: 2 (function)
  • Value: name of the function
  • SupportsParameters: always true
  • Parameters: this is always present and contains a single object - the actual parameter, but with a twist (see below).
  • IsArray: always false
The most notable thing about this is that this structure contains only part of the parameter's definition, while the nested parameter contains the rest:
  • The parameter has correct Type and Value, but never contains its own parameters as defined in triggerdata.txt.
  • The nested parameter is always present, has exactly same Value, but is always of Type 3 (string)!
  • Although the nested parameter is of Type 3, it actually contains its own parameters, as defined in triggerdata.txt!

Let's look at the following action:
  • Actions
    • Destructible - Pick every destructible in (Playable map area) and do (Destructible - Kill (Picked destructible))
Raw bytes extracted from wtg:
Code:
0x:
02 00 00 00 45 6e 75 6d 44 65 73 74 72 75 63 74
61 62 6c 65 73 49 6e 52 65 63 74 41 6c 6c 00 01
00 00 00 02 00 00 00 47 65 74 50 6c 61 79 61 62
6c 65 4d 61 70 52 65 63 74 00 01 00 00 00 03 00
00 00 47 65 74 50 6c 61 79 61 62 6c 65 4d 61 70
52 65 63 74 00 01 00 00 00 00 00 00 00 00 00 00
00 02 00 00 00 44 6f 4e 6f 74 68 69 6e 67 00 01
00 00 00 02 00 00 00 4b 69 6c 6c 44 65 73 74 72
75 63 74 61 62 6c 65 00 01 00 00 00 02 00 00 00
47 65 74 45 6e 75 6d 44 65 73 74 72 75 63 74 61
62 6c 65 00 01 00 00 00 03 00 00 00 47 65 74 45
6e 75 6d 44 65 73 74 72 75 63 74 61 62 6c 65 00
01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
Parsed as ECA Function:
Rich (BB code):
[02 00 00 00]                                                                       // type is 'action'
[]                                                                                  // this is a top level function, so blockId is missing
[45 6e 75 6d 44 65 73 74 72 75 63 74 61 62 6c 65 73 49 6e 52 65 63 74 41 6c 6c 00]  // ascii characters for 'EnumDestructablesInRectAll' terminated by \0
[01 00 00 00]                                                                       // is enabled: true
--Start of Parameters:                                                              // from triggerdata.txt we know it has two params: rect and code
    first param:
    [02 00 00 00]                                               // type is 'function'
    [47 65 74 50 6c 61 79 61 62 6c 65 4d 61 70 52 65 63 74 00]  // ascii characters for 'GetPlayableMapRect' terminated by \0
    [01 00 00 00]                                               // supports parameters: true
    --Start of Parameters:                                      // since this is param of type 'function', the NestedParameter struct is used, it will have a single param
        first param:
        [03 00 00 00]                                               // type is 'string'
        [47 65 74 50 6c 61 79 61 62 6c 65 4d 61 70 52 65 63 74 00]  // ascii characters for 'GetPlayableMapRect' terminated by \0
        [01 00 00 00]                                               // supports parameters: true
        []                                                          // based on triggerdata.txt, this has no parameters
        [00 00 00 00]                                               // is array: false
        []                                                          // missing since this is not an array
    --End of parameters
    [00 00 00 00]                                               // is array: false
    []                                                          // missing since this is not an array

    second param:                                               // param type is 'code', so WrapperParameter struct follows
    [02 00 00 00]                                               // type is 'function'
    [44 6f 4e 6f 74 68 69 6e 67 00]                             // ascii characters for 'DoNothing' terminated by \0
    [01 00 00 00]                                               // function count: one
    --Start of wrapped ECA Function
        [02 00 00 00]                                               // type is 'action'
        []                                                          // this is not a nested function, so blockId is missing
        [4b 69 6c 6c 44 65 73 74 72 75 63 74 61 62 6c 65 00]        // ascii characters for 'KillDestructable' terminated by \0
        [01 00 00 00]                                               // is enabled: true
        --Start of Parameters:                                      // from triggerdata.txt we know it has single param: destructable
            first param:
            [02 00 00 00]                                                   // type is 'function'
            [47 65 74 45 6e 75 6d 44 65 73 74 72 75 63 74 61 62 6c 65 00]   // ascii characters for 'GetEnumDestructable' terminated by \0
            [01 00 00 00]                                                   // supports parameters: true
            --Start of Parameters:                                          // since this is param of type 'function', the NestedParameter struct is used, it will have a single param
                first param:
                [03 00 00 00]                                                   // type is 'string'
                [47 65 74 45 6e 75 6d 44 65 73 74 72 75 63 74 61 62 6c 65 00]   // ascii characters for 'GetEnumDestructable' terminated by \0
                [01 00 00 00]                                                   // supports parameters: true
                []                                                              // based on triggerdata.txt, this has no parameters
                [00 00 00 00]                                                   // is array: false
                []                                                              // missing since this is not an array
            --End of parameters
            [00 00 00 00]                                                       // is array: false
            []                                                                  // missing since this is not an array
        --End of parameters
        [00 00 00 00]                                               // nested function count: zero
        []                                                          // no nested functions, so it is missing
    --End of wrapped ECA Function
    [00 00 00 00]                                               // is array: false
    []                                                          // missing since this is not an array
--End of parameters
[00 00 00 00]                                                                       // nested function count: zero
[]                                                                                  // no nested functions, so it is missing
Below is another example that shows that the function's parameters are actually stored in the nested parameter.

Let's look at the following action:
  • Actions
    • Set VariableSet i = (i + 15)
Raw bytes extracted from wtg:
Code:
0x:
02 00 00 00 53 65 74 56 61 72 69 61 62 6c 65 00
01 00 00 00 01 00 00 00 69 00 00 00 00 00 00 00
00 00 02 00 00 00 4f 70 65 72 61 74 6f 72 49 6e
74 00 01 00 00 00 03 00 00 00 4f 70 65 72 61 74
6f 72 49 6e 74 00 01 00 00 00 01 00 00 00 69 00
00 00 00 00 00 00 00 00 00 00 00 00 4f 70 65 72
61 74 6f 72 41 64 64 00 00 00 00 00 00 00 00 00
03 00 00 00 31 35 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00
Parsed as ECA Function:
Rich (BB code):
[02 00 00 00]                           // type is 'action'
[]                                      // this is a top level function, so blockId is missing
[53 65 74 56 61 72 69 61 62 6c 65 00]   // ascii characters for 'SetVariable' terminated by \0
[01 00 00 00]                           // is enabled
--Start of Parameters:                  // from triggerdata.txt we know it has two params: AnyGlobal and Null
    first param:
    [01 00 00 00]       // type is 'variable'
    [69 00]             // ascii characters for 'i' terminated by \0
    [00 00 00 00]       // supports parameters: false
    []                  // params
    [00 00 00 00]       // is array: false
    []                  // missing since this is not an array
    second param:
    [02 00 00 00]                           // type is 'function'
    [4f 70 65 72 61 74 6f 72 49 6e 74 00]   // ascii characters for 'OperatorInt' terminated by \0
    [01 00 00 00]                           // supports parameters: true
    --Start of Parameters:                  // since this is param of type 'function', the NestedParameter struct is used, it will have a single param
        first param:
        [03 00 00 00]                           // type is 'string'
        [4f 70 65 72 61 74 6f 72 49 6e 74 00]   // ascii characters for 'OperatorInt' terminated by \0
        [01 00 00 00]                           // supports parameters: true
        --Start of Parameters:                  // from triggerdata.txt we know it has three params: integer, ArithmeticOperator and integer
            first param:
            [01 00 00 00]   // type is 'variable'
            [69 00]         // ascii characters for 'i' terminated by \0
            [00 00 00 00]   // supports parameters: false
            []              // params
            [00 00 00 00]   // is array: false
            []              // missing since this is not an array

            second param:
            [00 00 00 00]                           // type is 'preset'
            [4f 70 65 72 61 74 6f 72 41 64 64 00]   // ascii characters for 'OperatorAdd' terminated by \0
            [00 00 00 00]                           // supports parameters: false
            []                                      // params
            [00 00 00 00]                           // is array: false
            []                                      // missing since this is not an array

            third param:
            [03 00 00 00]   // type is 'string'
            [31 35 00]      // ascii characters for '15' terminated by \0
            [00 00 00 00]   // supports parameters: false
            []              // params
            [00 00 00 00]   // is array: false
            []              // missing since this is not an array
        --End of Parameters
        [00 00 00 00]                           // is array: false
        []                                      // missing since this is not an array
    --End of Parameters
    [00 00 00 00]                           // is array: false
    []                                      // missing since this is not an array
--End of Parameters
[00 00 00 00]                           // nested function count: zero
[]                                      // no nested functions, so it is missing



Closing words​

Hopefully this will be found helpful. If you see something incorrect or missing, let me know and I will try to update the post.
 
Last edited:
Level 15
Joined
Aug 16, 2019
Messages
199
I will probably need to read the article several times to fully understand it.

Since our last conversation, I have also worked on binary files. Using WC3MapTranslator, I managed to write an automatic parser for w3* map files into JSON, but I encountered a difficulty with the error (The value of 'offset' is out of range) for certain files, and I haven't progressed further.

Returning to the topic, is this related to the fact that binary files have changed their structure in the new Reforged version?
 
Level 14
Joined
Oct 18, 2013
Messages
724
Wow, really impressive! Should help for rebuilding .wtg of maps long lost so that someone can fix 'em :3
 
Level 15
Joined
Aug 7, 2013
Messages
1,342
Thank you for documenting the new .wtg format.

Now my only request is to package this up into a cross platform library in a language like Python or Java, so it can be re-used across many different projects that could want to edit the .wtg/triggers directly/programmatically.

I did this for StarCraft's TRIG format in Python: richchk/src/richchk/model/richchk/trig at master · sethmachine/richchk. Albeit it's a much a simpler format, I think this could be done for .wtg file too.

If you have a pointer to your library that does this already that would be super awesome to share!
 
Level 29
Joined
Sep 26, 2009
Messages
2,595
If you have a pointer to your library that does this already that would be super awesome to share!
no, the app I was writing is just on my PC. I did not get to the level where I would be comfortable making it a public repo :D Namely, I was missing parsing casc and wct files, as well as a lot of other stuff I wanted to have in the app (i.e. source vs target comparisons, export selection, etc.), but I got the the point where I could read and display object editor data, gui triggers and the trigger tree view in installed wc3 language.

It is written in C# using latest .NET. For first version I was visualizing it using windows forms (as that's the one I have enough knowledge to get it to work). But I did keep GUI and core libs separated so that I could then migrate to some cross-platform framework like Uno.

I left the work unfinished, since W3 was pretty much dead from dev's PoV at that time, but with version 2.0 out, I could resume the project.

Anyway, I will try to make some free time in the following days. I could at least make some MVP version of just the wtg parser, put it in a standalone lib and publish it in public github repo.
 
Level 15
Joined
Aug 7, 2013
Messages
1,342
The parsers are solved by Kaitai Struct definitions. Ralle published one, and Water's version (if I'm not mistaken) is going to be more thorough and in line with his other definitions.
What KSY doesn't work for yet is serialization. Only here is coding required.

The Kai Struct definitions are pretty cool, thanks for sharing this project! Does it only do deserialization as you suggested, or decoding the binary format into human readable values?

Actually I think the easiest part of dealing with binary formats is the parsing/decoding/deserialization part (once you know the format/have reverse engineered it of course :]). But I think even if that worked, you generally don't want humans directly editing the low level fields of a binary format/file.

For me, the harder part is making a higher level abstraction that a human can use to edit the format without needing to do any painstaking bookkeeping.

Does Kai Struct definitions lets you create relations among the different parts of a binary file? For example, consider the STR section in StarCraft maps. There is a sequence/list of string offsets (u16) followed by string/character data. Each offset points to the start of the bytes of the string it references. Does Kai Struct let you encode this relationship somehow? As if I just want to "add a string", I need to choose the offset based on all the other bytes in the STR so it correctly lines up with the string. Note that a human is never going to care about this, they just want to use a new string and not worry how it's handled internally. Second, think about the operation of "deleting a String". It would require shifting all the bytes in the STR, and then finding all references to that deleted String among all other binary files. I would be amazed if Kai Struct let you encode this relationship. Of course, the best answer may be you never allowed deleting Strings. But the issue is, there's a dependent relationship between 1 binary format to another, and this seems arbitrary to me, so it could be tricky to generify without writing the code for it yourself. There's no semantics in StarCraft's binary formats that tell you how they relate and effect each other exactly. So custom code is going to be needed to enforce these relationships. And I highly doubt WarCraft III binary formats encode any relations between other binary formats, because they wanted to keep the map files as small as possible for download between peers/server.

My last point, at least in Blizzard land. It doesn't make sense to allow a human to directly edit or view all parts of a map file. I'm using STR again, but a human doesn't care about string bookkeeping. They just want to name a unit, region, display a message, etc. So a tool that can generate code to parse the STR is nice, but that's a far cry from saying it's "solved." by Kaitai Struct definitions.

Of course if I'm totally wrong or it can do all this, then it's quite the holy grail.

Edit: looks like it's possible to do some relationship encoding after all in Kaitai based on ChatGPT:

By using instances, you can describe more complex relationships between fields, such as how one field determines the position of another.

This should model the relationship between the number of strings, their offsets, and the actual string data. If you want more advanced control or adjustments, Kaitai Struct also supports expressions and custom types.
 
Last edited:
Level 20
Joined
Jan 3, 2022
Messages
364
Does it only do deserialization as you suggested, or decoding the binary format into human readable values?
I'm confused by this question? The web IDE allows you to directly upload and parse the files, see the individually parsed values. The only workflow it's not well optimized for is the handling of multiple files. In the end it's supposed to generate parser code for the language you want, where you have free reign over the data. No, it's not some automatic JSONator converter, although close in spirit.

Yes, Kaitai Struct supports basic forms of such relationships. Yet the syntax becomes increasingly difficult to find and write. This is the breaking point, why the (roundtrip) serialization has been in development for years and in Java only for now. Not active development, mind you. From my understanding, it's a cool and relatively unknown project that would be happy to have sponsors.
 
Top