Skip to main content

w3ts 3.x to reforged-ts 1.0

reforged-ts 1.0 continues w3ts 3.0.2 for Warcraft III 3.0.0 and later, under a new package name. Its first major removes the deprecated constructors and the old Hook code, gives every Wrapper one error rule, and moves the Systems onto real Promises. Follow this page from top to bottom:

  1. Install and tsconfig: replace the packages and select the Typings.
  2. Old-to-new table: every symbol reforged-ts 1.0 removes or renames, with its replacement.
  3. Behaviour changes: what changes at run time or in the compiler under a name that stays.
  4. Typings changes: the declarations of the Natives.
  5. What stays the same.

Install and tsconfig​

Replace w3ts with reforged-ts and its Typings, reforged-types, a peer dependency the Map project installs itself:

npm uninstall w3ts
npm install reforged-ts reforged-types

Until 1.0.0 is published, install the prereleases with @next: npm install reforged-ts@next reforged-types@next.

w3ts 3.x brought the Typings of Patch 1.33.0 through its own dependency, war3-types-strict, which its declarations referenced. reforged-ts references none: the Map project selects the Typings of its Game version in tsconfig.json, next to the typescript-to-lua language extensions:

{
"compilerOptions": {
"moduleResolution": "bundler",
"types": ["@typescript-to-lua/language-extensions", "reforged-types/3.0.0"]
}
}

The Typings entry brings the Lua 5.3 standard library through lua-types, so a lua-types/5.3 entry is no longer needed. w3ts 3.x asked for TypeScript 5; the Toolchain of reforged-ts is TypeScript 6.0.2, the version typescript-to-lua 1.37 requires, which has no moduleResolution: classic: use bundler. The compatibility matrix lists the Toolchain of each release, and the Template is a Map project already set up this way.

Then replace every import from w3ts with an import from reforged-ts. The lint plugin does it for you: its no-legacy-w3ts-names rule reports every name of the table below, and eslint --fix rewrites the imports and the one-to-one renames.

Old-to-new table​

Every public symbol of w3ts 3.x that reforged-ts 1.0 removes or renames, with its replacement: a symbol written as new Unit(...) is a constructor, Unit.create(...) a call, Frame.parent a member. Several replacements split the old symbol by argument shape or into a get/set pair, and "removed" means there is none: the note says what to write instead. The library publishes the same list with the package, reforged-ts/migration/renames.json, which the lint rule reads.

Generated section

This section is generated from packages/reforged-ts/migration/renames.json by docs:collect. Edit the source, not this section.

OldNewKindNote
w3tsreforged-tspackageThe library is published under a new name: an import from w3ts reads the same exports from reforged-ts, less the symbols the other entries rename or remove.
new CameraSetup(...)CameraSetup.create(...)constructorThe deprecated constructor is gone: CameraSetup.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Destructable(...)Destructable.create(...)constructorThe constructor took the rawcode, x, y, z, facing, scale and variation in that order; Destructable.create takes one options object, { typeId, x, y, z, face, scale, variation }, and picks the creation Native from the options given.
new Dialog(...)Dialog.create(...)constructorThe deprecated constructor is gone: Dialog.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new DialogButton(...)DialogButton.create(...)constructorThe deprecated constructor is gone: DialogButton.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Effect(...)Effect.create(...), Effect.createAttachment(...)constructorThe overloads split by argument shape: (modelName, x, y) is Effect.create and (modelName, targetWidget, attachPointName) is Effect.createAttachment, each with the same arguments.
new FogModifier(...)FogModifier.create(...)constructorThe deprecated constructor is gone: FogModifier.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Force(...)Force.create(...)constructorThe deprecated constructor is gone: Force.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Frame(...)Frame.create(...), Frame.createSimple(...), Frame.createType(...)constructorThe overloads split by argument shape: (name, owner, priority, createContext) is Frame.create with the same arguments, (name, owner, priority) is Frame.createSimple with the same arguments, and (name, owner, priority, createContext, typeName, inherits) is Frame.createType(name, owner, createContext, typeName, inherits), which drops the priority the constructor ignored.
new GameCache(...)GameCache.create(...)constructorThe deprecated constructor is gone: GameCache.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Group(...)Group.create(...)constructorThe deprecated constructor is gone: Group.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Image(...)Image.create(...)constructorThe deprecated constructor is gone: Image.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Item(...)Item.create(...)constructorThe deprecated constructor is gone: Item.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Leaderboard(...)Leaderboard.create(...)constructorThe deprecated constructor is gone: Leaderboard.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new MapPlayer(...)MapPlayer.fromIndex(...)constructorPlayers are not created, so the constructor, private in w3ts 3.x, gives way to the lookup MapPlayer.fromIndex, which keeps the index argument but returns MapPlayer | undefined, undefined for a slot out of range, so the call site must handle the missing player.
new Multiboard(...)Multiboard.create(...)constructorThe deprecated constructor is gone: Multiboard.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new MultiboardItem(...)MultiboardItem.create(...)constructorThe deprecated constructor is gone: MultiboardItem.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Point(...)Point.create(...)constructorThe deprecated constructor is gone: Point.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Quest(...)Quest.create(...)constructorThe deprecated constructor is gone: Quest.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new QuestItem(...)QuestItem.create(...)constructorThe deprecated constructor is gone: QuestItem.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Rectangle(...)Rectangle.create(...)constructorThe deprecated constructor is gone: Rectangle.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Region(...)Region.create(...)constructorThe deprecated constructor is gone: Region.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Sound(...)Sound.create(...)constructorThe deprecated constructor is gone: Sound.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new TextTag(...)TextTag.create(...)constructorThe deprecated constructor is gone: TextTag.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Timer(...)Timer.create(...)constructorThe deprecated constructor is gone: Timer.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new TimerDialog(...)TimerDialog.create(...)constructorThe deprecated constructor is gone: TimerDialog.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Trigger(...)Trigger.create(...)constructorThe deprecated constructor is gone: Trigger.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Ubersplat(...)Ubersplat.create(...)constructorThe deprecated constructor is gone: Ubersplat.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new Unit(...)Unit.create(...)constructorThe deprecated constructor is gone: Unit.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
new WeatherEffect(...)WeatherEffect.create(...)constructorThe deprecated constructor is gone: WeatherEffect.create takes the same arguments and is the one way to create the Handle, throwing when the game returns nothing.
Frame.parentFrame.getParent, Frame.setParentaccessorThe deprecated accessor splits into getParent(), which returns undefined when the frame has no parent, and setParent(parent).
Unit.ownerUnit.getOwner, Unit.setOwneraccessorThe deprecated accessor splits into getOwner() and setOwner(player), whose changeColor argument defaults to true as the setter did.
Unit.pointUnit.getPoint, Unit.setPointaccessorThe deprecated accessor splits into getPoint(), which creates a new location on each call, and setPoint(point).
Group.getEnumUnitUnit.fromEnummemberThe enumerated unit has one accessor, on the class it returns, instead of two names for one Native.
Group.getFilterUnitUnit.fromFiltermemberThe filtered unit has one accessor, on the class it returns, instead of two names for one Native.
MapPlayer.createMapPlayer.fromIndexmemberPlayers are not created, so the factory, private in w3ts 3.x, gives way to the lookup MapPlayer.fromIndex, which returns undefined for a slot out of range.
Handle.getObjectHandle.fromHandlememberThe protected wrapping step of a Wrapper subclass gives way to the inherited fromHandle, typed on the class it is called on and returning undefined for an undefined Handle, while a creation member returns this.expect(Native(...), detail) instead.
Handle.initFromHandleremovedmemberNo constructor asks whether it is wrapping an existing Handle any more: the protected constructor takes the Handle and only stores it, and the base registers the Wrapper.
Trigger.registerTimerExpireEventTrigger.registerTimerExpirememberThe registration takes the Timer Wrapper instead of the raw timer handle, like the Trigger's other registrations, so the argument timer.handle becomes timer.
Trigger.triggerRegisterFrameEventTrigger.registerFrameEventmemberThe one registration whose name began with trigger takes the name of its siblings, with the same Frame and frameeventtype arguments.
Trigger.registerTrackableHitEventTrigger.registerTrackableHitmemberThe registration takes the new Trackable Wrapper, which Trackable.create returns, instead of the raw trackable handle.
Trigger.registerTrackableTrackEventTrigger.registerTrackableTrackmemberThe registration takes the new Trackable Wrapper, which Trackable.create returns, instead of the raw trackable handle.
Trigger.registerPlayerMouseEventTrigger.registerPlayerMouseEventmemberNot a rename: the second argument is MouseEventKind.Down, MouseEventKind.Up or MouseEventKind.Move instead of Blizzard.j's number (bj_MOUSEEVENTTYPE_DOWN, _UP, _MOVE), and a number is now a type error.
main::beforeInit.onGlobalsentryPointaddScriptHook(W3TS_HOOK.MAIN_BEFORE, hook) becomes Init.onGlobals(hook), which runs later, after InitGlobals, by design: nothing should be created before the map's globals exist, and the library's own setup (Players, the sync Trigger) runs first in that stage.
main::afterInit.onInitTriggersentryPointaddScriptHook(W3TS_HOOK.MAIN_AFTER, hook) becomes Init.onInitTriggers(hook), the same moment: RunInitializationTriggers is the last call of main, so the stage runs at the end of main.
config::beforeremovedentryPointNo Init stage runs in the lobby, before config, in the first release: code that needs lobby timing stays on addScriptHook, deprecated and removed in 2.0, until a lobby stage exists.
config::afterremovedentryPointNo Init stage runs in the lobby, after config, in the first release: code that needs lobby timing stays on addScriptHook, deprecated and removed in 2.0, until a lobby stage exists.
W3TS_HOOKremovedtypeThe enum of addScriptHook is deprecated with it and still exported for this one release; its four values are the entry points listed here, each with its stage or none, and both are removed in 2.0.
hookedMainremovedfunctionAn implementation detail of the old Hook code: the Init machinery installs the wrapper of main itself, in place or on the function's first assignment, and no wrapper has a name.
hookedConfigremovedfunctionAn implementation detail of the old Hook code: the Init machinery installs the wrapper of config itself, in place or on the function's first assignment, and no wrapper has a name.
executeHooksMainBeforeremovedfunctionAn implementation detail of the old Hook code: the wrapper of main runs the main::before queue, each hook under pcall, and nothing runs it by hand.
executeHooksMainAfterremovedfunctionAn implementation detail of the old Hook code: the wrapper of main runs the main::after queue, each hook under pcall, and nothing runs it by hand.
executeHooksConfigBeforeremovedfunctionAn implementation detail of the old Hook code: the wrapper of config runs the config::before queue, each hook under pcall, and nothing runs it by hand.
executeHooksConfigAfterremovedfunctionAn implementation detail of the old Hook code: the wrapper of config runs the config::after queue, each hook under pcall, and nothing runs it by hand.
new SyncRequest(...)SyncRequest.sendconstructorNot a rename: the constructor takes only the sender and the options and never starts a request, so the overloads that took the data and started at once are gone, and new SyncRequest(from, data, options) becomes SyncRequest.send(from, data, options).
SyncRequest.thenSyncRequest.start, SyncRequest.sendmemberthen stored one callback and returned the request, which await could not use; start(data) and SyncRequest.send return a real Promise of the SyncResponse, so request.then(callback) becomes request.start(data).then(callback) or an await.
SyncRequest.catchSyncRequest.start, SyncRequest.sendmemberThe Promise that start(data) and SyncRequest.send return rejects instead, with a string naming the request and the cause: a timeout, a cancellation or a network error.
SyncCallbackremovedtypeThe callback type of then and catch goes with them: a Promise continuation receives the SyncResponse, which carries the request.
ISyncResponseSyncResponsetypeThe type drops the I prefix and is read-only; it gains the sender, from, and the request, and loses status, which the request's own status holds.
ISyncOptionsSyncOptionstypeThe type drops the I prefix; its timeout is optional and read-only, zero or absent meaning no timeout.
SyncRequest.destroySyncRequest.cancelmemberNamed for what it does: it rejects a pending request with a cancellation reason and does nothing on a settled one; ids are never recycled, so there is nothing to release.
SyncRequest.fromIndexremovedmemberThe table of pending requests is private: the System matches a packet to its request by id itself, and a continuation receives the request in the SyncResponse.
onHostDetectHost.detectHostfunctionThe callback gives way to a Promise of the elected MapPlayer, the same Promise for every call, and Host.host reads the result once it resolved; nothing is detected until detectHost is called.
BinaryReader.readremovedmemberThe generic read that took a format and a size, the size the source of the readDouble misalignment, is private: the typed readX members advance by what string.unpack consumed.
BinaryReader.dataremovedmemberThe binary string read is private: the typed readX members read it, and position and remaining tell how far they got.
BinaryWriter.valuesremovedmemberThe array of values written so far is private: toString() packs them, and the typed writeX members are the only way to add one.
Destructable.createZDestructable.creatememberDestructable.create takes the z coordinate as its z option, Destructable.create({ typeId, x, y, z }), next to the facing, scale, variation and skin createZ took in order.
Camera.SetCinematicSceneCamera.setCinematicScenememberThe cinematic scene is camelCase like every other member of Camera; it takes the same arguments.
Camera.panCamera.panmemberNot a rename: zOffsetDest is optional, so Camera.pan(x, y) pans without a z-offset where w3ts 3.x needed Camera.pan(x, y, undefined), which still compiles. The parameters keep their order.
Camera.panTimedCamera.panTimedmemberNot a rename: zOffsetDest is optional and stays last, after duration, so Camera.panTimed(x, y, duration) pans without a z-offset where w3ts 3.x needed Camera.panTimed(x, y, duration, undefined), which still compiles. The parameters keep their order.
Camera.setCameraOrientControllerCamera.setOrientControllermemberThe member drops the redundant Camera prefix, like its twin Camera.setTargetController, and its first argument is the Unit Wrapper instead of the raw unit handle, so Camera.setCameraOrientController(unit.handle, x, y) becomes Camera.setOrientController(unit, x, y).
Camera.setTargetControllerCamera.setTargetControllermemberNot a rename: the first argument is the Unit Wrapper instead of the raw unit handle, so the argument unit.handle becomes unit.
Unit.getflyHeightUnit.getFlyHeightmemberThe flying height is camelCase with a capital F, like every other member of Unit.
Unit.setflyHeightUnit.setFlyHeightmemberThe flying height is camelCase with a capital F, like every other member of Unit; it takes the same arguments.
Unit.getIgnoreAlarmUnit.setIgnoreAlarmmemberThe member changes the setting through UnitIgnoreAlarm and reads nothing, so it is a setter; it takes the same argument. ignoreAlarmToggled reads the setting.
Unit.dropItemFromSlotUnit.moveItemToSlotmemberThe member moves the item to another slot of the inventory (UnitDropItemSlot); it drops nothing on the ground. It takes the same arguments.
Unit.setUnitAttackCooldownUnit.setAttackCooldownmemberThe attack cooldown has one setter instead of two names for one Native; it takes the same arguments.
Item.playerItem.getOwneraccessorThe accessor becomes getOwner(), named like its setter Item.setOwner and like Unit.getOwner, and returns the MapPlayer Wrapper, or undefined, instead of the raw player handle, so MapPlayer.fromHandle(item.player) becomes item.getOwner().
MapPlayer.getTaxRateMapPlayer.getTaxRatememberNot a rename: the first argument is the MapPlayer Wrapper instead of the raw player handle, as MapPlayer.setTaxRate takes it, so the argument other.handle becomes other.
MapPlayer.startLocationPointMapPlayer.getStartLocationPointaccessorThe accessor becomes getStartLocationPoint(), like Unit.getPoint, and returns a new Point to destroy instead of the raw location handle, and throws instead of returning undefined when the game returns none, so RemoveLocation(player.startLocationPoint) becomes player.getStartLocationPoint().destroy().
GameCache.restoreUnitGameCache.restoreUnitmemberNot a rename: cache.restoreUnit(...) returns the Unit Wrapper instead of the raw unit handle, so Unit.fromHandle(cache.restoreUnit(...)) becomes cache.restoreUnit(...). It throws reforged-ts: failed to create Unit (<key>) where it returned undefined: check cache.hasUnit(missionKey, key) first when the key may hold no unit.
GameCache.storeGameCache.storememberNot a rename: a unit is passed as the Unit Wrapper instead of the raw unit handle, so the argument unit.handle becomes unit.
Frame.setTextAlignmentFrame.setTextAlignmentmemberNot a rename: it takes the same arguments and returns the Frame instead of nothing, as the other Frame setters do, so it no longer ends a chain.

Behaviour changes​

What changes when a Map project keeps a name that did not change: a return type, an error that was silent and now throws, a callback that now runs under pcall, a wire format. Each section below is one step of the migration of the library; within it, each item names the member it concerns.

Generated section

This section is generated from packages/reforged-ts/migration/behaviour-changes.md by docs:collect. Edit the source, not this section.

Build step 3: one error rule for every Wrapper​

  • Creation throws, lookup returns undefined. A member whose Native allocates a new Handle is a creation: it is typed non-null and throws reforged-ts: failed to create <Wrapper> (<detail>) when the Native returns nothing, the detail being the identifying argument (a rawcode, a frame name, a model path) when there is one. A member whose Native returns an existing Handle, or nothing, is a lookup typed X | undefined. Every create factory follows the rule: the ones that were typed X | undefined lose the | undefined, and Timer.create, Trigger.create, Point.create, Rectangle.create and Region.create, typed non-null before without a check, now throw instead of returning a Wrapper around nothing. A thrown error inside a game thread ends that thread unless the code runs under pcall.
  • Two lookups become optional. MultiboardItem.fromHandle and WeatherEffect.fromHandle return undefined for undefined, like every other fromHandle, and are typed MultiboardItem | undefined and WeatherEffect | undefined.
  • Members that allocate are creations, whatever their name. These are now typed non-null and throw when the game returns nothing: Rectangle.fromPoint and Rectangle.getWorldBounds (a new rect), Force.fromPlayer (a new force), FogModifier.fromRect (a new fog modifier), unit.getPoint() (a new location), unit.addItemById() (a new item), cameraSetup.destPoint, Camera.eyePoint and Camera.targetPoint (a new location each). unit.rallyPoint stays a lookup typed Point | undefined: a unit with no rally point has nothing to give.
  • unit.getOwner() and MapPlayer.fromLocal() are typed non-null. A live unit always has an owner and the local player always exists; should the game ever break either invariant, the member throws with the message above instead of returning undefined.
  • MapPlayer.fromLocal() no longer prints. It printed ten lines on screen when the Native returned nothing; it now throws instead.
  • Force.fromPlayer no longer calls Blizzard.j. It creates the force with CreateForce and adds the player with ForceAddPlayer instead of calling GetForceOfPlayer, with the same result.
  • A frame that was not found is undefined. The game returns a frame whose handle id is 0 for a name it does not know: Frame.fromName, Frame.fromOrigin, Frame.fromEvent, frame.getParent() and frame.getChild() return undefined for it, and Frame.create, Frame.createSimple and Frame.createType throw when the frame definition is missing.
  • A lookup through a more specific class replaces the cached Wrapper. Unit.fromHandle(h) after Widget.fromEvent() returned the cached Widget in w3ts 3.x; it now returns a Unit, which becomes the one Wrapper for that Handle: later lookups through Widget return it too, and the earlier Widget object still works but is no longer === to them. The same holds for a Map project's own subclass of MapPlayer: looking a player up through it replaces the MapPlayer that tsGlobals.Players holds for that slot.
  • MapPlayer can be extended. Its constructor was private; every Wrapper's constructor is now protected, so a Map project can declare its own subclass of any Wrapper and get instances of it from the inherited fromHandle.
  • Handle is abstract and its constructor takes the Handle. A Map project's own Wrapper class that extends Handle directly no longer wraps through getObject and initFromHandle: it declares no constructor, or a protected one that takes the Handle and passes it to super, and it inherits fromHandle for lookups and the protected expect for creation members. The base registers each Wrapper when it wraps the Handle; the constructor only stores it.

Build step 4: Init stages, Reforged.configure and library Handles out of the Lua root​

  • Players is empty until the globals stage. In w3ts 3.x the library filled tsGlobals.Players when its modules loaded, so Players[0] at module top level held a MapPlayer; it now holds nothing there. The library fills the array in its own globals callback, after InitGlobals and before any Init.onGlobals callback of the Map project, so read it from Init.onGlobals or a later stage. The array keeps its shape: one MapPlayer per slot, Players[i] for slot i.
  • addScriptHook hooks run under pcall. In w3ts 3.x a throwing hook ended main or config at that point, silently, because a Lua error in a game thread prints nothing. Each hook now runs under pcall: a failure prints one line on screen, reforged-ts: main::before callback #2 failed: <message> (the entry point, the hook's ordinal in that entry point's queue, and the message pcall returned; the game has no traceback), and the other hooks of the entry point and the entry point itself still run. An Init stage callback prints the same line with the stage name and the label it was registered with, reforged-ts: globals callback "spawn heroes" failed: <message>.
  • The replacement of main::before runs later, by design. Init.onGlobals runs after InitGlobals, not before main: nothing should be created before the map's globals exist. main::after and Init.onInitTriggers are the same moment, the end of main. addScriptHook itself keeps its old timing for this release, and no stage has the lobby timing of config::before and config::after.
  • The game-time Timer starts after MarkGameStarted. It was created at main::after; it is created in the library's gameStart callback. Timers do not tick during initialization, so getElapsedTime() reads the same on a running game; before the stage, it returns 0, as it did before the end of main. The host detection's Timer of w3ts 3.x is gone: build step 6 makes the detection opt-in, with a timeout Timer only once Host.detectHost() runs.
  • The library makes no Handle-creating Native call at load. Requiring the library called Player for every slot and CreateTrigger for the sync System in w3ts 3.x, in the Lua root, before any initialization function ran. It calls neither now, nor CreateTimer: every Handle of the library is born in a stage. FourCC still runs at load in the file System; it is the game's Lua helper, not a Native, and creates nothing.
  • The sync Trigger and its events are created at the globals stage. The Trigger was created when the SyncRequest class was defined, and the sync events were registered by the first request. The library's globals callback now creates the Trigger and registers its events for every slot whose controller is a user and whose slot state is playing, for the one sync prefix "rts" since build step 6 (w3ts 3.x registered two, "T" and "S"). A SyncRequest made before the stage still creates the Trigger and its events on the way, and the stage then does nothing more.
  • From the map header, the library captures the Blizzard functions it wraps as the editor's script defines them. In w3ts 3.x the Hook code read main and config when the library loaded, so pasted into the map header, where both were still nil, it did nothing. The library now wraps each function it needs (InitGlobals, InitCustomTriggers, RunInitializationTriggers, MarkGameStarted, main, config) in place when it exists at load, and otherwise captures it on its first assignment through the library's own _G metatable. The library's metatable composes with one the map installed before it, an undeclared-global warner for example: the map's __index and __newindex keep working for every other key, and the map's metatable is _G's again once every pending name was captured. The wrapper of a captured name is stored with a raw set, so the map's __newindex does not see InitCustomTriggers or main being defined.
  • The library owns the global reforged-ts. It keeps its Init state under that global (a table), so a Lua root that executes twice keeps one set of wrappers and queues; maps must not touch it.
  • Reforged.configure after a registration warns. A call that changes devMode after a callback was registered through the library prints reforged-ts: Reforged.configure({ devMode: true }) called after a callback was registered: only later registrations see the new value (with the value being set) and records the value anyway; a call that changes nothing prints nothing. Call it once, first thing in the entry point.

Build step 5: a chainable Trigger, Timer handlers and Event descriptors​

  • The Timer handler receives its Timer. timer.start(timeout, periodic, handler) calls handler(timer) on each expiry, with the Timer that was started, so a periodic handler pauses or destroys itself without Timer.fromExpired(). A handler written for w3ts 3.x takes no parameter and keeps working. Timer.fromExpired() stays and returns undefined when the game has no expired Timer, which happens when the Timer was destroyed right before its callback.
  • Timer.destroy returns nothing. It returned the Timer, which was no longer usable; it now returns void, like every other Wrapper's destroy. pause, resume and start still return the Timer.
  • Registration, addAction and addCondition return the Trigger. Every register* member returned the event handle of its Native, addAction the triggeraction and addCondition the triggercondition; each now returns the Trigger, so a complete Trigger is one chained expression. No Native consumes an event, so nothing is lost there; code that kept the triggeraction or triggercondition to remove one handler later subscribes with on() and ends the Subscription with destroy() instead, or calls TriggerAddAction and TriggerAddCondition itself. removeAction and removeCondition stay for handles obtained through the Natives.
  • Filter parameters are optional. registerPlayerUnitEvent, registerFilterUnitEvent, registerEnterRegion, registerLeaveRegion and registerUnitInRange took a filter that had to be written, undefined when there was none; it is now optional and may be left out. A filter given as a plain function is wrapped with Filter, a boolexpr is passed as is, and a missing one passes nothing to the Native.
  • addCondition takes a plain function without Condition. It wraps a function with Condition itself, and adds a boolexpr as is, so a Map project never calls Condition for a Trigger. In w3ts 3.x it also returned undefined instead of adding the condition when Condition returned nothing; it now always adds it.
  • The library makes no Blizzard.j call. registerAnyUnitEvent(event) called TriggerRegisterAnyUnitEventBJ; it now registers the player unit event with TriggerRegisterPlayerUnitEvent for the player of every slot below bj_MAX_PLAYER_SLOTS, with no filter, the loop the Blizzard.j helper performs, so the Trigger receives the same events. registerPlayerMouseEvent(player, kind) called TriggerRegisterPlayerMouseEventBJ with Blizzard.j's number (bj_MOUSEEVENTTYPE_DOWN, _UP, _MOVE); it now takes MouseEventKind.Down, MouseEventKind.Up or MouseEventKind.Move, a string enum, and registers EVENT_PLAYER_MOUSE_DOWN, _UP or _MOVE with TriggerRegisterPlayerEvent. A number is a type error instead of a value the helper ignored when it was not one of the three.
  • Frame.getEventText() reads the text. It called BlzGetTriggerFrameValue, so it returned the frame event's number; it now calls BlzGetTriggerFrameText and is typed string | undefined, undefined for an event that carries no text, such as a click.

Build step 6: Systems with real Promises, one wire format and one error mode​

  • SyncRequest.start returns a Promise. It resolves on every client with a SyncResponse once every packet of the sender's data arrived, and rejects on a timeout, a cancellation or a network error, so await works and a failure reaches a try/catch. A request starts once: a second start throws reforged-ts: sync request <id> was already started at the calling line, and the constructor, which takes only the sender and the options, never starts one. SyncRequest.send(from, data, options?) creates and starts in one call. cancel() rejects only a request still waiting for its packets; on one never started or already settled it does nothing.
  • Sync rejections are strings. A rejected request's reason is reforged-ts: sync request <id> timed out after <seconds> seconds, reforged-ts: sync request <id> was cancelled or reforged-ts: sync request <id> could not be sent (network error): a string, like every other error the library raises, not an Error object, whose tostring needs the debug library the game does not have. A network failure no longer prints SyncData: Network Error; the request rejects on the sender's client instead.
  • The sync response and status. SyncResponse carries the data, the sender as from (read from the event of the last packet, so a continuation reads no event context), the elapsed game time as time and the request; it no longer carries status, which the request's own read-only status holds. SyncStatus gains Cancelled and NetworkError. SyncOptions.timeout is optional, in seconds, zero or absent meaning no timeout.
  • The sync prefix and wire format changed. Every packet the System sends has the one prefix "rts", where w3ts 3.x used "T" for a short request and "S" for a chunk: a Map project picks another prefix for its own sync traffic. A packet is an 8-character header, the request id, the chunk index (from 0) and the chunk count as unsigned 16-bit big-endian fields packed and base64-encoded, then at most 244 bytes of the data, raw, with no zero terminator; a request with a single chunk, or with no data, is chunk 0 of 1. Request ids are a 16-bit counter from 1 that wraps around, never recycled on cancel. A packet with another prefix, a header that does not decode, a chunk index out of range, an id with no pending request, a sender other than the request's, a different chunk count or a chunk already received is ignored. On the sender's client, data holding a zero byte, which the game would cut the packet at, or needing more than 65,535 chunks throws at the line that called start or send, before the request starts: encode binary data first, for example with base64Encode.
  • Host detection is opt-in and a Promise. Host.detectHost() replaces onHostDetect: nothing is measured, sent or created for the election, and no sync id is taken, until it is called, where w3ts 3.x always ran a detection at game start. Called before the game starts, the election runs at the gameStart stage; called later, it runs at once. It takes the requests' ids when it runs, so call it on every client in the same order relative to other sync requests. It settles when every playing user has answered or left, or when its timeout expires (10 seconds by default, 0 for none): the longest lobby time wins and a tie goes to the lowest player index. It rejects with the string reforged-ts: host detection received no lobby time only when no value arrived. Host.host is undefined until the election resolved. The lobby time is still measured from config, but 0 no longer reads as "not yet synced".
  • The binary string format is length-prefixed. BinaryWriter.writeString writes a 2-byte big-endian length, then the bytes, so any byte, a zero included, round-trips; readString reads that format. A string longer than 65,535 bytes throws at write. Data written with w3ts 3.x's zero-terminated strings does not read back.
  • Reading past the end of a BinaryReader throws. Every readX advances by exactly what it consumed, so readDouble no longer misaligns the values after it, and a read past the end throws reforged-ts: <readX> past the end: position <P>, width <W>, <R> remaining instead of returning 0 or garbage. position and remaining read where the reader stands.
  • The BinaryWriter rejects values outside a field's range. Every integer writeX, writeInt32 included, throws reforged-ts: <writeX> takes <min> to <max>, got <value> for a value its width cannot hold, and NaN, at the call that writes it. writeUInt32 and readUInt32 round-trip 0 to 2^32 − 1 on the 32-bit game and the 64-bit test VM alike; in the game a value from 2^31 up reads back as a float.
  • base64Decode throws on malformed input. It printed 'base64Decode' failed: … and returned an empty string, which a caller could not tell from an empty payload; it now throws, naming the offset, when the length is not a multiple of four, a character is outside the alphabet or the padding is in the wrong place. base64Encode accepts any byte string and never prints.
  • File.write and File.writeRaw return nothing. They returned the File class; they now return void. File.read reads contents holding the escape character followed by q correctly, where it returned a " in their place.
  • sleep resolves with no value. It is typed Promise<void>, where it was Promise<null>; the value at run time is nil either way. It runs on Timer.after.
  • Item.getField and Item.setField reach the item field Natives. They compared the field's type with the unit field type names, so they returned 0 and false for every item field without calling a Native; they now read and write item fields.

Build step 7: Wrappers for the 3.0.0 systems​

  • Destructable.create takes an options object. It took the rawcode, x, y, facing, scale, variation and skin in that order; it now takes one object, Destructable.create({ typeId, x, y }), with the optional z, face (0 by default), scale (1), variation (0), pitch, roll, skin, color (a playercolor) and dead. The options given pick one of the game's 32 creation Natives: dead: true creates a dead destructable, z places it at that height (what createZ, now removed, did), pitch or roll tilts it with the absent one 0, skin gives it a skin and color a team colour. Positional arguments are a type error. It still throws reforged-ts: failed to create Destructable (<rawcode>) at the calling line when the game creates nothing.
  • GameCache.flushNumber flushes the real. It called FlushStoredInteger, so it removed the integer stored under the key (the one flushInteger removes) and left the real that store wrote with a number and getNumber reads. It now calls FlushStoredReal. Code that flushed an integer through flushNumber must call flushInteger.
  • Sound.setChannel sets the channel. It called SetSoundDistanceCutoff with the channel number, so it changed the sound's distance cutoff and left its channel. It now calls SetSoundChannel. Code that set a cutoff through setChannel must call setDistanceCutoff.
  • unit.removeType removes the unit type. In w3ts 3.x it called UnitAddType, so it added the unit type it was asked to remove. It now calls UnitRemoveType and returns its answer.

Fixes found while documenting the Wrappers​

  • frame.setTextAlignment() chains, and the Multiboard cells take a row and a column. frame.setTextAlignment(vert, horz) returns the Frame, as the other Frame setters do, instead of nothing, so a chain can go on after it. MultiboardItem.create(board, x, y) and multiboard.createItem(x, y) name their parameters row and column: the first is still the row and the second the column, both counted from 1, and the call passes row - 1 and column - 1 to MultiboardGetItem as before. Only the names change, so working code needs no edit.
  • GameCache stores and restores Units. cache.restoreUnit() returns the Unit Wrapper instead of the raw unit handle: Unit.fromHandle(cache.restoreUnit(...)) becomes cache.restoreUnit(...). It is a creation, so it throws reforged-ts: failed to create Unit (<key>) when RestoreUnit returns nothing, as when no unit is stored under the key, where w3ts 3.x returned undefined; check cache.hasUnit(missionKey, key) first. In Dev mode the creation Guards apply to it. cache.store() takes a unit as the Unit Wrapper: the argument unit.handle becomes unit. GetStoredString, and so cache.getString(), is typed string instead of string | undefined: the game reads "" for a missing key.
  • item.getOwner() and MapPlayer.getTaxRate use MapPlayer. The item.player accessor is renamed item.getOwner(), named like its setter item.setOwner() and like unit.getOwner(). It returns the MapPlayer Wrapper of the owner, the object MapPlayer.fromHandle gives, or undefined when GetItemPlayer returns nothing, instead of the raw player handle: MapPlayer.fromHandle(item.player) becomes item.getOwner(). MapPlayer.getTaxRate takes the other player as a MapPlayer, as MapPlayer.setTaxRate does: the argument other.handle becomes other.
  • Unit members say what they do. unit.getflyHeight() and unit.setflyHeight() are renamed getFlyHeight and setFlyHeight. unit.getIgnoreAlarm(flag) is renamed setIgnoreAlarm(flag): it changes the setting through UnitIgnoreAlarm and reads nothing. unit.dropItemFromSlot(item, slot) is renamed moveItemToSlot(item, slot): it moves the item to another inventory slot and drops nothing on the ground. unit.setUnitAttackCooldown() is removed: it duplicated unit.setAttackCooldown(), which takes the same arguments. Each renamed member calls the same Native with the same arguments.
  • Camera follows the conventions of the other Wrappers. Camera.SetCinematicScene is renamed Camera.setCinematicScene, with the same arguments. Camera.pan and Camera.panTimed take zOffsetDest as an optional last parameter, so Camera.pan(x, y) and Camera.panTimed(x, y, duration) pan without a z-offset; passing undefined, as w3ts 3.x required, still does the same, and the parameters keep their order. Camera.setCameraOrientController is renamed Camera.setOrientController, without the redundant Camera prefix, like its twin Camera.setTargetController. Both take the Unit Wrapper instead of the raw unit handle: the argument unit.handle becomes unit.
  • region.containsPoint() and leaderboard.hasPlayerItem() return their answer. In w3ts 3.x both called their Native (IsLocationInRegion, LeaderboardHasPlayerItem) without returning its result, so they always returned undefined. They now return the Native's boolean.
  • unit.skillPoints = n sets the skill points. In w3ts 3.x the setter called UnitModifySkillPoints with its value, so it added n unspent points. It now sets the count to n, by the difference from GetHeroSkillPoints; the game keeps it between 0 and what the hero can still spend. Code that granted points through the setter must call unit.modifySkillPoints(n).
  • unit.addItemById() returns the item it dropped. When the inventory was full, the unit could not carry items or it was dead, UnitAddItemById dropped the new item at the unit's feet and returned nothing, so the member returned nothing in w3ts 3.x and threw since build step 3. It now creates the item with CreateItem at the unit's position, puts it in the inventory with UnitAddItem and returns it either way; unit.hasItem(item) tells whether it is in the inventory. Code that took a missing item for a full inventory must check hasItem.
  • item.invulnerable = flag uses its value. In w3ts 3.x the setter passed true to SetItemInvulnerable whatever the value, so item.invulnerable = false made the item invulnerable. It now passes the value: false makes the item vulnerable again.
  • player.startLocationPoint becomes player.getStartLocationPoint(), which returns a Point. In w3ts 3.x the accessor returned the raw location handle GetStartLocationLoc allocates on each read, which the caller removed with RemoveLocation, or undefined when the game returned none. It is now the method getStartLocationPoint(), like unit.getPoint(), and returns a new Point wrapping that location: destroy it when done, so RemoveLocation(player.startLocationPoint) becomes player.getStartLocationPoint().destroy(). It is a creation, so it throws reforged-ts: failed to create Point when the game returns no location, and in Dev mode the creation Guards apply to it. player.startLocationX and player.startLocationY read the coordinates without creating anything.
  • Group.addGroupFast and Group.removeGroupFast change this. In w3ts 3.x a.addGroupFast(b) called BlzGroupAddGroupFast with a first, and the game adds the units of the first group to the second (measured in 3.0.0), so it added the units of a to b; a.removeGroupFast(b) removed the units of a from b the same way. Both now pass their argument first: a.addGroupFast(b) adds the units of b to a, a.removeGroupFast(b) removes them from a, and b is left as it was. The return value is still the number of units added or removed. Code that called a.addGroupFast(b) to fill b calls b.addGroupFast(a), and code that called a.removeGroupFast(b) to empty b of the units of a calls b.removeGroupFast(a).
  • OrderId.Battleroar and OrderId.Forkedlightning hold the game's ids. In w3ts 3.x OrderId.Battleroar held the id of battlestations (852099) and OrderId.Forkedlightning held the id of elementalfury (852586); the mix-up went one way only: OrderId.Battlestations and OrderId.Elementalfury always held the right ids. They are now 852599 and 852587, so both uses change. Code that issued OrderId.Battleroar or OrderId.Forkedlightning to issue battlestations or elementalfury issues OrderId.Battlestations or OrderId.Elementalfury. Code that compared an incoming order id (the order id of an order event, GetIssuedOrderId) with OrderId.Battleroar or OrderId.Forkedlightning to catch battlestations or elementalfury no longer matches them: it compares with OrderId.Battlestations or OrderId.Elementalfury.

Runtime Guards​

Every bullet below but the first applies in Dev mode only. With Dev mode off (the default, and every release build) the only change is the first: the functions handed to Natives are the Map project's own, errors propagate as in w3ts 3.x, MapPlayer.runLocal is a GetLocalPlayer() comparison, and destroy() calls its Native and nothing else besides forgetting the Wrapper. The "Desync safety and guards" guide lists every Guard with its message.

  • A destroyed Handle never resolves to its dead Wrapper. In w3ts 3.x, destroy() left the Wrapper in the Handle registry, so Unit.fromHandle(h) with the Handle of a removed unit (read back from a variable or a stale event) returned the same object that was destroyed. Every Wrapper's destroy() now removes the registry entry after its Native, in Dev mode and in release: fromHandle with the same Handle afterwards returns a new Wrapper, not === to the destroyed one. Code that compared a Wrapper looked up after destroy() with the one it destroyed sees them differ.
  • In Dev mode, creating a Wrapper before the globals Init stage raises. Every creation member (Timer.create, Unit.create, ...) called before the wrapped InitGlobals started, typically at module top level, throws reforged-ts: <Wrapper> created before the globals Init stage: create Handles in Init.onGlobals or a later stage, not at module top level at the calling line. Creations inside Init.onGlobals callbacks or later pass. With Dev mode off nothing changes.
  • In Dev mode a Timer handler that throws is reported, and the thread survives. In w3ts 3.x, and with Dev mode off, an error in a Timer.start, Timer.after or Timer.every handler ends the game thread silently. With Reforged.configure({ devMode: true }), the handler runs under pcall: the failure is shown on screen to the local player for thirty seconds and printed as reforged-ts: Timer#<id> Timer.start failed: <error> (the registering member, then the Lua error text with its war3map.lua line), and the error no longer propagates out of the expiry. The same handler failing with the same message is reported once and counted afterwards; Reforged.debug.report() prints and returns the counts, Reforged.debug.reset() zeroes them. A handler keeps the mode in force when it was registered.
  • The late-configure warning names the first registration, and Dev mode warns on any late call. It reads reforged-ts: Reforged.configure({ devMode: <value> }) called after a callback was registered (the first: <registration>): call it first in the entry point; a callback keeps the mode it was registered under, where the registration is Init.onGlobals "<label>", addScriptHook("main::before") #1 or Timer#<id> Timer.start. A Timer handler counts as a registration, in both modes. It is printed for any call after a registration when Dev mode is on before or after the call, one that changes nothing included, except the first call of a second execution of the Lua root keeping the mode; with Dev mode off before and after, nothing is printed.
  • In Dev mode a destroyed Wrapper is a tombstone. In w3ts 3.x, and with Dev mode off, a Wrapper kept working after destroy(): its members called Natives on the dead Handle. With Reforged.configure({ devMode: true }), destroy() clears the Wrapper's fields, the Handle included, and any later access (reading a property or .handle, calling a method, writing a field, a second destroy()) throws reforged-ts: used after destroy: <Class>#<id> at the line of the access; tostring renders <Class>#<id> (destroyed). Code that read .handle or .id of a Wrapper after destroying it must read them before. An older reference that the registry upgraded (a Widget looked up before the Unit) is not the canonical Wrapper and is not turned into a tombstone.
  • In Dev mode Reforged.debug.report() counts Wrappers per class. The report's new wrappers rows (class name, created, destroyed, live) come first, sorted by live descending, before the callback failures, and Reforged.debug.reset() zeroes them (the rows stay, at zero). The report prints, second, that it counts only the Wrappers the library created and destroyed. Only creation members count as created and only destroy() as destroyed, of a Wrapper counted created since the last reset (so a count never goes below zero); lookups (fromHandle, fromEvent, MapPlayer.fromLocal(), unit.getOwner()) never count, and MapPlayer.fromLocal() and unit.getOwner() pass before the globals Init stage. The counts are a heuristic: a unit that decayed or an effect the game removed stays live.
  • In Dev mode trigger actions, conditions, filters and enumeration callbacks are protected as Timer handlers are. A Trigger.addAction action that throws is reported as reforged-ts: Trigger#<id> Trigger.addAction failed: <error> and the trigger's next action still runs. A function given as a condition (Trigger.addCondition) or as a filter (the Trigger registration members and the Group, Force and Rectangle enumeration members) that throws is reported under that member and evaluates false, as the game evaluates a crashed condition, so the trigger does not fire or the unit is excluded. A Group.for, Force.for or Rectangle.enumItems/enumDestructables callback that throws is reported and the enumeration continues with the next member. An on() handler or when predicate that throws is reported under the descriptor's name (reforged-ts: UnitEvents.death failed: <error>); EventDescriptor gained an optional name for it. With Dev mode off the Natives receive the Map project's functions unchanged and errors propagate as in w3ts 3.x.
  • MapPlayer.runLocal(player, fn) runs local-only code, and Dev mode guards it. It runs fn on the client whose local player is player only, in place of a GetLocalPlayer() comparison; with Dev mode off it is that comparison. In Dev mode fn runs under pcall (an error is reported as reforged-ts: MapPlayer#<id> MapPlayer.runLocal failed: <error> and does not escape), and inside it creating or destroying a Wrapper, Group.for, Force.for and the first Frame.fromName of a frame the library has no Wrapper for raise reforged-ts: <action> inside MapPlayer.runLocal changes game state for one client, which desyncs the game: only visuals belong inside runLocal at the calling line. player.isLocal() stays.
  • In Dev mode a runaway damage loop is stopped. In w3ts 3.x, and with Dev mode off, a damage handler that deals damage back without a stop fires the damage events again and again until the client crashes. With Reforged.configure({ devMode: true }), the actions and conditions of a Trigger registered for a damaged or damaging event (through registerUnitEvent, registerFilterUnitEvent, registerPlayerUnitEvent or registerAnyUnitEvent, so on() damage subscriptions too) run one level deeper in a shared damage depth, and Unit.damageTarget raises reforged-ts: Unit#<id> Unit.damageTarget at damage depth <depth>, past the limit of <limit>: a damage handler that deals damage fires the damage events again, which loops until the client crashes when the depth exceeds the limit: eight nested dispatches by default, set through Reforged.configure({ damageDepthLimit }) (a call without it keeps the current limit). The error is reported as the failure of the handler that dealt the damage. A single bounce, the reflect-damage pattern, passes.

Typings changes​

  • The package. war3-types-strict (Patch 1.33.0) becomes reforged-types, a peer dependency of reforged-ts whose range is a caret on the version the library was released with, so the package manager warns when a major does not match. The Map project installs it and lists its entry in types (see Install and tsconfig).
  • The Patch. The declarations are generated from the Patch files of Game version 3.0.0, so the Natives, types and globals the game added after Patch 1.33.0 are declared. The compatibility matrix lists the Patch each release supports.
  • The AI Natives are opt-in. war3-types-strict/1.33.0 declared the Natives of common.ai with the others. They are valid only in AI scripts, so reforged-types/3.0.0 leaves them out; add reforged-types/3.0.0/common.ai to types to declare them.
  • Nullability is a reviewed decision per Native. A hand-curated Overlay, seeded with war3-types-strict's nullability decisions, says for every Native whether it can return nothing (T | undefined) and for each parameter whether it takes undefined. The generator fails on a Native without an entry, so every nullable type is a decision someone reviewed.
  • The hover says more. Each Native shows its Jass types (integer (32-bit), real), @async when its value is valid only for the local player, @deprecated with the reason, @patch for the Patch that added it, and a link to its reference page.

What stays the same​

  • The names. Unit, Timer, Trigger, Frame, MapPlayer and the other Wrappers, and the Systems (sync, host, file, binary, base64, gametime), are imported from the package root as before. A symbol missing from the table above keeps its name; what changes under it is in the behaviour changes.
  • tsGlobals.Players keeps its shape: one MapPlayer per slot, Players[i] for slot i, filled at the globals Init stage (see the behaviour changes).
  • addScriptHook still works. It is the deprecated alias of the Init stages and keeps its timing for this release; its entry points are in the table with the stage that replaces each.
  • The Typings declare the Natives in the same shape: Handle types as branded interfaces that mirror the Patch hierarchy, T | undefined for a Native that can return nothing, Record<number, T> for the Jass arrays, and @noSelfInFile with this: void callbacks, so typescript-to-lua emits plain Lua functions.
  • The build. The package ships compiled Lua and its declarations, which typescript-to-lua resolves like any typescript-to-lua library.
  • With Dev mode off, the default and every release build, the runtime Guards change nothing but one thing: a destroyed Handle no longer resolves to its dead Wrapper (see Runtime Guards above). Desync safety and guards says what Dev mode adds.