Datascripts
Datascripts are the TSWoW way to generate static game data for the server (SQL, DBC) and the client (DBC,lua,xml). Datascripts contain powerful tools to create anything from spells, creatures and transports to entire classes, maps, dungeons and much more.
Basics
Datascripts are kept in the datascripts
endpoint in your module, and you can make tswow generate all necessary configuration files simply by creating this directory while tswow is running.
Datascripts are executed in tswow with the build data
command, which will load all script files it can find, execute them, and then store the generated data in the various files and databases that wow clients and servers normally use.
The standard entrypoint for datascripts is the file datascripts/datascripts.ts
, but datascripts do not really need an entrypoint as the build system will automatically run every script file it can find.
A very simple datascript that only prints out a message to the console when you build data
:
console.log("Hello world")
TSWoW Standard Library
The tswow standard library (wow/wotlk) contains most of the useful tools that you will need when creating datascripts. Almost all of its tools are defined via a single interface that only requires one import in your scripts:
import { std } from "wow/wotlk"
Creating Entities
To create new entities, we typically use the create
method on one of the substructs we can find through the std interface. Below is a basic example to create a new custom creature:
import { std } from "wow/wotlk"
std.CreatureTemplates.create('my-module','my-custom-creature')
Loading Entities
Loading existing entities instead uses the load
method, also commonly found on substructs to std
:
import { std } from "wow/wotlk"
// Loads creature template with id 25
std.CreatureTemplates.load(25)
Modifying Entities
Once we have created or loaded an entity, we can modify it using a special convention evolved from method-chaining that the early developers of tswow dubbed “cell-based programming”. Before elaborating, a brief example of how this looks:
import { std } from "wow/wotlk"
std.CreatureTemplates
.create('my-module','my-creature')
.Name.enGB.set('My Creature')
.Level.set(10)
.Stats.ArmorMod.set(2)
In cell-based programming, we create data by accessing entities in a long method chain and modifying individual properties via “set” functions (or, sometimes, other functions). Once a property has been “set”, the method chain always returns to the root entity again, and the process starts over. This allows a very efficient way to create data for many different types of entities in a way that almost just looks like a configuration or json file.
Understanding how a cell-based method chain works can be a bit difficult at first, and some users prefer to ignore the technical details and assume the code does what it looks like it’s doing, while others prefer to avoid looping back by storing created objects in a variable, as it makes the code look a little bit more like something you would normally expect in the language:
import { std } from "wow/wotlk"
// This looks a little more like "normal" TypeScript,
// but is more verbose and cluttered.
const MY_CREATURE = std.CreatureTemplates.create('my-module','my-creature')
MY_CREATURE.Name.enGB.set('My Creature')
MY_CREATURE.Level.set(10)
MY_CREATURE.Stats.ArmorMod.set(2)
Enums
Some values in the game can only take one of a few predetermined values. For example, Creatures can be one of a few predetermined Types
, such as UNDEAD
, DEMON
, HUMANOID
, BEAST
and so on. These special types are known as Enums
, and are commonly represented as a number in databases. TSWoW often offers convenient ways to write and read these using sensible names instead of raw numbers:
std.CreatureTemplates.create('my-mod','my-creature')
// Changes the creature type to UNDEAD
// Note how enum values are commonly written in ALL_UPPERCASE.
.Type.UNDEAD.set()
Masks and Flags
Enums can normally only take on one value at a time. A Creature cannot be both UNDEAD
and a BEAST
at the same time.
Masks are similar to enums in that they can represent a predetermined set of values, but contrary to enums can take on multiple such values at once.
note: In the database, masks are commonly represented as a single number just like enums. The technical details about how multiple discrete values can be stored in a single number isn’t necessary to understand how to use masks in tswow, but for the curious reader wikipedia has a good article on how masks/bitmasks work.
Flags
Masks commonly implement on/off settings fields. Masks used in that way are commonly known as Flags. Generally, flags are initialized to be unset / false until enabled. For example, a spells Attributes
field is a mask/flag value that allows specifying a large amount of individual on/off settings for spells:
// Creates a spell and enables multiple flags
std.Spells.create('my-mod','my-spell')
// Turns the spell into a passive spell that automatically applies
// any aura effects it contains.
.Attributes.IS_PASSIVE.set(true)
// Hides the spell from the spellbook when taught.
.Attributes.IS_HIDDEN_IN_SPELLBOOK.set(true)
Classmasks and Racemasks
Another very common use of masks are classmasks and racemasks, which allow or deny access to a resource to certain combination of classes or races.
For example, ClassMasks can be applied to items to only allow certain classes to use them:
// Creates an item limited to human mages, priests and warlocks
std.Items.create('my-mod','my-item')
.ClassMask.PRIEST.set(true)
.ClassMask.MAGE.set(true)
.ClassMask.WARLOCK.set(true)
.RaceMask.HUMAN.set(true)
Set/Add
An alternative convention to set masks is to use set/add
on the mask object itself and specify arguments to enable in a list:
// Does the same thing as the previous example
std.Items.create('my-mod','my-item')
.ClassMask.set(['PRIEST','MAGE','WARLOCK'])
.RaceMask.set(['HUMAN'])
Similarly, we can call remove
to disable multiple values at the same time:
std.Items.create('my-mod','my-item')
.ClassMask.set(['PRIEST','MAGE','WARLOCK']) // priest + mage + lock enabled
.ClassMask.remove(['PRIEST']) // mage + lock enabled
Important: There is an important difference between set
and add
when called on the mask object directly:
std.Items.create('my-mod','my-item')
.ClassMask.PRIEST.set(true) // priests enabled
.ClassMask.MAGE.set(true) // priests + mages enabled
.ClassMask.add(['WARLOCK']) // priests + mages + warlocks enabled
.ClassMask.set(['WARRIOR']) // only warrior is now enabled,
// because "set" overwrites any
// previous data.
Raw manipulation
If you are comfortable writing your own bitmasks, it is still possible to set mask values by raw numbers. It is also possible to specify a second argument to specify how the input number should be merged with the existing value:
std.Items.create('my-mod','my-item')
.ClassMask.set(0x1|0x2) // warrior + paladin enabled
.ClassMask.set(0x1|0x4,'AND') // warrior enabled
.ClassMask.set(0x4,'OR') // warrior + hunter enabled
Refs
Some entities in World of Warcraft are easy to explain. A CreatureTemplate is a type of creature in the world, defining its name, stats, level, and so on. A Class is a playable character type with a defined set of learnable spells, talents and attribute points.
Other entities are more difficult to explain. For example, any data that defines how a specific spell looks is an entity on its own, called SpellVisual
, and a single SpellVisual
can be shared by multiple spells at once! Spells contain a special id on them to point at such a spell visual. The common name for these “pointer” ids is Refs
.
TSWoW has a special convention to allow users fast and powerful control over how to access and modify properties across references. When accessing a reference property, the user will be met with a selection of four common functions: set
, get
, getRef
, modRef
, getRefCopy
and modRefCopy
. The following section will briefly explain each and provide code examples for how to use them, and what they achieve.
get/set()
Refs are simply numeric ids stored on an entity, and calling get/set
on them will achieve the same as calling it on any normal property: it will write the id to the underlying cell with no extra magic applied.
// Loads spell with id 133 (Firebolt rank 1)
const spell = std.Spells.load(133)
// Sets the visual reference to 0 (no reference = spell has no visuals)
spell.Visual.set(0);
// Sets the visual reference to 870 (the SpellVisual used by ShadowBolt, causes spell to look like shadowbolt when cast)
spell.Visual.set(870);
// Will print out 870, since we just set the id to 870
console.log(spell.Visual.get());
getRef() / getRefCopy()
Calling getRef()
on a reference will return the referenced entity itself, rather than the reference id. This means you can modify entities across references without externally loading the referenced entity:
import { std } from "wow/wotlk"
const spell = std.Spells.load(133)
// Returns the SpellVisual referenced by spell 133
const visual = spell.Visual.getRef();
// Changes the missile model of this spell visual
visual.Missile.Model.setSimple('Spells\\Shadowbolt_Missile.mdx')
One issue of getRef
(and references in general) is that we don’t really know what other entities might reference it. In the example of spells, all ranks of Firebolt (including those cast by monsters) all reference the exact same spell visual, meaning that if we use getRef
on one spell to modify the reference, it will change it not just for our own spell, but for every single spell that reference it.
The solution is to use getRefCopy
instead, which creates a copy of the previously referenced entity with a new id and writes this new id to the reference cell before returning the copy. Now, any changes made to the referenced entity are sure to only affect the specific entity we are modifying
A general rule of thumb is to only ever use getRef
when reading data from a reference, and getRefCopy
when writing data to a reference.
modRef / modRefCopy ()
An alternative convention to using getRef/getRefCopy
to modify references is to use modRef/modRefCopy
, which instead of returning the referenced entity accepts a callback that receives it. This allows modifying referenced objects without breaking cell-based method chains:
import { std } from "wow/wotlk"
std.Spells.load(133)
.Name.enGB.set('New Firebolt spell')
.Visual.modRefCopy(visual=>{
visual.Missile.Model
.setSimple('Spells\\Shadowbolt_Missile.mdx')
})
// outside of the callback, we are still modifying the Spell
.Power.setMana(100)
You should almost always prefer modRefCopy
to modRef
Arrays and One-To-Many relationships
Arrays and one-to-many relationships are handled similarly to references, typically offering functions on the form addGet(...)
, addMod(...,callback)
, get(...)
, mod(...)
. The exact arguments these functions accept vary depending on the type of relation it describes. Typically, they do not have copy
versions, since they cannot be shared by multiple outbound references at the same time the way Refs
can.
Arrays
Some entities in the game contain arrays of other entities. One such example is that Spells
can contain 3 separate SpellEffects
, all stored inside the spell itself.
Example of various ways to access array entries (using Spell#Effects
):
import { std } from "wow/wotlk"
const spell = std.Spells.create('my-mod','my-new-spell')
// Adds a new spell effect (maximum of 3 per spell)
const eff = spell.Effects.addGet();
eff.Type.SCHOOL_DAMAGE.set();
// Modifies an existing spell effect
const oldEff = spell.Effects.get(0);
oldEff.Type.SCHOOL_DAMAGE.set();
// Adds a new spell effect and modifies it in a callback
spell.Effects.addMod((e)=>{
e.Type.SCHOOL_DAMAGE.set();
})
// Modifies an existing spell effect in a callback
spell.Effects.mod(0, (e)=>{
e.Type.SCHOOL_DAMAGE.set()
})
One-To-Many Relationships
Similar to entity arrays, some entities in the game are defined as with a backreference to another entity. One such example is SkillLineAbilities
, which defines a unique way that a specific spell can be taught, by which classes/races and to which SkillLine
it belongs.
Example of various ways to access one-to-many entities:
// Makes spellid 133 (Firebolt rank 1) learnable
// through the fishing skill for druids
const spell = std.Spells.load(133)
let sla = spell.SkillLines.addGet()
sla.SkillLine.set(356); // skilline 356 = fishing
sla.ClassMask.set(['DRUID'])
sla.AcquireMethod.TRAINER.set()
Building data
Once we have written a datascript to create or modify an entity in tswow, we use the build data
command in the tswow shell to apply these changes to game. Normally, it will then restart the client and server for us automatically so that we can immediately log back in again and see our changes.
The build data
command comes with a couple of useful flags:
-
--readonly
: Will execute all datascripts, but will not output any data to the game. Useful for debugging, will not restart server or client on use. -
--client-only
: Will execute all datascripts, but will only write client and not server data. Useful for aesthetic changes, and will only restart the client on completion. -
--no-shutdown
: Will execute all datascripts and write data, but will not shutdown or restart the server or client. -
--rebuild
: Will rebuild the entire destinationworld
database, which is sometimes necessary to clean out changes to existing entities in the game. This is a slow operation, and should only be used when you really find old game data.
Dirty database
TSWoW is a migration system that expects users to continuously execute their scripts to update the world state.
To achieve this, datascripts separates the input data that it reads from the output data that it writes. For DBC, this means tswow reads from one set of DBC files and writes to another set. For SQL, it means tswow reads from one world database and writes to another.
Most data that tswow builds is completely rewritten and undone on each execution, but not all of it. SQL data specifically is so slow to rebuild entirely that we have to leave some of it hanging until a full rebuild is triggered with the --rebuild
flag. TSWoW does its best to clean up after itself even without it, but there are a few instances where it falls short:
Modifying an entity in the game and removing the modification:
First build:
import { std } from "wow/wotlk"
std.CreatureTemplates.load(25).Name.enGB.set('Hello');
Second build:
import { std } from "wow/wotlk"
//std.CreatureTemplates.load(25).Name.enGB.set('Hello');
Becasue creature names live in the SQL database, tswow will not notice that the name of creature with id 25 is no longer changed from its original value. The solution to this is simply to use the --rebuild
flag when you need to test something with the original data again.
Client Interface Manipulation (Lua/XML mods)
The graphical interface in the game are implemented using Lua and XML files. There are three ways that client interfaces can be manipulated and created in tswow:
-
Replacing existing files by overriding them in your mods
assets
directory. -
Creating an addon
-
LUAXML manipulations in datascripts.
Overriding interface files via the assets
directory or an addon are commonly easier for major changes, but will not work if multiple modules or parts of the build pipeline need to modify the same files. This is especially common to break if modifying class-related interface files and also creating your own custom classes using datascripts, as tswow will automatically apply modifications to lua and xml files to make those work.
For files that need to be accessed by multiple modules, or that collide with custom class modifications, it is therefore recommended to use the third method and manipulate luaxml through datascripts.
This can be done via the std.LUAXML
interface. Through this interface, we can either replace
lines, or insert values before
or after
a line matching a regular expression.
import { std } from "wow/wotlk"
const file = std.LUAXML.file('Interface/GlueXML/GlueStrings.lua')
file.replace(/RACE_INFO_BLOODELF =/,'RACE_INFO_BLOODELF = "Custom blood elf description";')
file.before(/RACE_INFO_BLOODELF/, '-- Comment before blood elf description')
file.after(/RACE_INFO_BLOODELF/, '-- Comment after blood elf description')
To allow multiple edits targeting the same tags or line numbers, edits are stored and not applied until all scripts have finished executing. It is also possible to access a previously made edit and change its text to avoid adding multiple edits to a single line:
const file = std.LUAXML.file('Interface/GlueXML/GlueStrings.lua')
const blood_elf_description =
file.replace(
/RACE_INFO_BLOODELF = /
, `RACE_INFO_BLOODELF = "Your description here"`
)
blood_elf_description.text = `RACE_INFO_BLOODELF = "My description"`;
InlineScripts
InlineScripts is an advanced and highly experimental feature
Do you hate having to switch between datascripts and livescripts and spamming out GetID declarations for every single entity you want to register events for?
InlineScripts is a new API to write a livescripts inline with your datascripts. A simple example:
import { std } from "wow/wotlk"
// This is a normal datascript
std.Spells.create('my-module','my-spell')
.Name.enGB.set('My Spell')
.CastTime.setSimple(1000)
.Cooldown.set(1000)
.InlineScripts.OnCast((spell)=>{
// now, we're inside a livescript!
if(spell.GetCaster().IsPlayer()) {
spell.GetCaster().ToPlayer()
.SendBroadcastMessage(`You cast "My Spell"!`)
}
})
It is also possible to register non id-bound events through InlineScripts:
import { std } from "wow/wotlk"
std.InlineScripts.Player.OnSay((player, message)=>{
player.SendBroadcastMessage(`You said: ${message.get()}`);
});
While InlineScripts allow a very convenient way to keep scripts in the same files as you define the data for them, they come with a lot of caveats:
-
InlineScripts cannot access any variables or functions outside of themselves and the arguments that they receive, not even constant primitives. External IDs need to be fetched with
GetID
calls just like normal livescripts. -
InlineScripts are experimental and much more prone to compiler bugs than normal livescripts, and could end up locking you to specific versions of
swc
ortsc
to build correctly in the future. -
InlineScripts can be very deceiving for people that don’t already know what they are. All of a sudden a callback runs in a completely different runtime from the outside scope, and isn’t even transpiled to the same language in the end. It looks like you can access variables or functions just like a normal callback, but in reality that is not possible at all.
-
InlineScripts cannot be written in C++
-
InlineScripts must always execute at least the datascript files that they belong to in order to update. The more InlineScripts you add to different datascript files, the more your livescripts build times will depend on your datascript build times.
Generally, InlineScripts are handy to add very simple scripted behavior on the level of a SmartScript
, but very quickly falls apart for complex behavior or anything that references multiple entity ids. More complex entities, such as battlegrounds, are generally much cleaner to write in a real livescript.
Building
For completely new entities it is necessary to run both build data
and build scripts
for the entity to be created and the script to work, but once the entity has already been created it is enough to run build scripts
to add, remove or modify InlineScripts.
InlineScripts were inspired by the Warcraft III modding language WurstScript.