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:
- Install and tsconfig: replace the packages and select the Typings.
- Old-to-new table: every symbol reforged-ts 1.0 removes or renames, with its replacement.
- Behaviour changes: what changes at run time or in the compiler under a name that stays.
- Typings changes: the declarations of the Natives.
- 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.
This section is generated from packages/reforged-ts/migration/renames.json by docs:collect. Edit the source, not this section.
| Old | New | Kind | Note |
|---|---|---|---|
w3ts | reforged-ts | package | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | Players 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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(...) | constructor | The 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.parent | Frame.getParent, Frame.setParent | accessor | The deprecated accessor splits into getParent(), which returns undefined when the frame has no parent, and setParent(parent). |
Unit.owner | Unit.getOwner, Unit.setOwner | accessor | The deprecated accessor splits into getOwner() and setOwner(player), whose changeColor argument defaults to true as the setter did. |
Unit.point | Unit.getPoint, Unit.setPoint | accessor | The deprecated accessor splits into getPoint(), which creates a new location on each call, and setPoint(point). |
Group.getEnumUnit | Unit.fromEnum | member | The enumerated unit has one accessor, on the class it returns, instead of two names for one Native. |
Group.getFilterUnit | Unit.fromFilter | member | The filtered unit has one accessor, on the class it returns, instead of two names for one Native. |
MapPlayer.create | MapPlayer.fromIndex | member | Players 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.getObject | Handle.fromHandle | member | The 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.initFromHandle | removed | member | No 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.registerTimerExpireEvent | Trigger.registerTimerExpire | member | The 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.triggerRegisterFrameEvent | Trigger.registerFrameEvent | member | The one registration whose name began with trigger takes the name of its siblings, with the same Frame and frameeventtype arguments. |
Trigger.registerTrackableHitEvent | Trigger.registerTrackableHit | member | The registration takes the new Trackable Wrapper, which Trackable.create returns, instead of the raw trackable handle. |
Trigger.registerTrackableTrackEvent | Trigger.registerTrackableTrack | member | The registration takes the new Trackable Wrapper, which Trackable.create returns, instead of the raw trackable handle. |
Trigger.registerPlayerMouseEvent | Trigger.registerPlayerMouseEvent | member | Not 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::before | Init.onGlobals | entryPoint | addScriptHook(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::after | Init.onInitTriggers | entryPoint | addScriptHook(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::before | removed | entryPoint | No 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::after | removed | entryPoint | No 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_HOOK | removed | type | The 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. |
hookedMain | removed | function | An 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. |
hookedConfig | removed | function | An 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. |
executeHooksMainBefore | removed | function | An 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. |
executeHooksMainAfter | removed | function | An 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. |
executeHooksConfigBefore | removed | function | An 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. |
executeHooksConfigAfter | removed | function | An 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.send | constructor | Not 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.then | SyncRequest.start, SyncRequest.send | member | then 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.catch | SyncRequest.start, SyncRequest.send | member | The 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. |
SyncCallback | removed | type | The callback type of then and catch goes with them: a Promise continuation receives the SyncResponse, which carries the request. |
ISyncResponse | SyncResponse | type | The 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. |
ISyncOptions | SyncOptions | type | The type drops the I prefix; its timeout is optional and read-only, zero or absent meaning no timeout. |
SyncRequest.destroy | SyncRequest.cancel | member | Named 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.fromIndex | removed | member | The 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. |
onHostDetect | Host.detectHost | function | The 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.read | removed | member | The 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.data | removed | member | The binary string read is private: the typed readX members read it, and position and remaining tell how far they got. |
BinaryWriter.values | removed | member | The array of values written so far is private: toString() packs them, and the typed writeX members are the only way to add one. |
Destructable.createZ | Destructable.create | member | Destructable.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.SetCinematicScene | Camera.setCinematicScene | member | The cinematic scene is camelCase like every other member of Camera; it takes the same arguments. |
Camera.pan | Camera.pan | member | Not 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.panTimed | Camera.panTimed | member | Not 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.setCameraOrientController | Camera.setOrientController | member | The 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.setTargetController | Camera.setTargetController | member | Not a rename: the first argument is the Unit Wrapper instead of the raw unit handle, so the argument unit.handle becomes unit. |
Unit.getflyHeight | Unit.getFlyHeight | member | The flying height is camelCase with a capital F, like every other member of Unit. |
Unit.setflyHeight | Unit.setFlyHeight | member | The flying height is camelCase with a capital F, like every other member of Unit; it takes the same arguments. |
Unit.getIgnoreAlarm | Unit.setIgnoreAlarm | member | The member changes the setting through UnitIgnoreAlarm and reads nothing, so it is a setter; it takes the same argument. ignoreAlarmToggled reads the setting. |
Unit.dropItemFromSlot | Unit.moveItemToSlot | member | The member moves the item to another slot of the inventory (UnitDropItemSlot); it drops nothing on the ground. It takes the same arguments. |
Unit.setUnitAttackCooldown | Unit.setAttackCooldown | member | The attack cooldown has one setter instead of two names for one Native; it takes the same arguments. |
Item.player | Item.getOwner | accessor | The 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.getTaxRate | MapPlayer.getTaxRate | member | Not 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.startLocationPoint | MapPlayer.getStartLocationPoint | accessor | The 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.restoreUnit | GameCache.restoreUnit | member | Not 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.store | GameCache.store | member | Not a rename: a unit is passed as the Unit Wrapper instead of the raw unit handle, so the argument unit.handle becomes unit. |
Frame.setTextAlignment | Frame.setTextAlignment | member | Not 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.
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 throwsreforged-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 typedX | undefined. Everycreatefactory follows the rule: the ones that were typedX | undefinedlose the| undefined, andTimer.create,Trigger.create,Point.create,Rectangle.createandRegion.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 underpcall. - Two lookups become optional.
MultiboardItem.fromHandleandWeatherEffect.fromHandlereturnundefinedforundefined, like every otherfromHandle, and are typedMultiboardItem | undefinedandWeatherEffect | undefined. - Members that allocate are creations, whatever their name. These are now typed non-null and throw when the game returns nothing:
Rectangle.fromPointandRectangle.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.eyePointandCamera.targetPoint(a new location each).unit.rallyPointstays a lookup typedPoint | undefined: a unit with no rally point has nothing to give. unit.getOwner()andMapPlayer.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 returningundefined.MapPlayer.fromLocal()no longer prints. It printed ten lines on screen when the Native returned nothing; it now throws instead.Force.fromPlayerno longer calls Blizzard.j. It creates the force withCreateForceand adds the player withForceAddPlayerinstead of callingGetForceOfPlayer, 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()andframe.getChild()returnundefinedfor it, andFrame.create,Frame.createSimpleandFrame.createTypethrow when the frame definition is missing. - A lookup through a more specific class replaces the cached Wrapper.
Unit.fromHandle(h)afterWidget.fromEvent()returned the cachedWidgetin w3ts 3.x; it now returns aUnit, which becomes the one Wrapper for that Handle: later lookups throughWidgetreturn it too, and the earlierWidgetobject still works but is no longer===to them. The same holds for a Map project's own subclass ofMapPlayer: looking a player up through it replaces theMapPlayerthattsGlobals.Playersholds for that slot. MapPlayercan 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 inheritedfromHandle.Handleis abstract and its constructor takes the Handle. A Map project's own Wrapper class that extendsHandledirectly no longer wraps throughgetObjectandinitFromHandle: it declares no constructor, or a protected one that takes the Handle and passes it tosuper, and it inheritsfromHandlefor lookups and the protectedexpectfor 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
Playersis empty until theglobalsstage. In w3ts 3.x the library filledtsGlobals.Playerswhen its modules loaded, soPlayers[0]at module top level held aMapPlayer; it now holds nothing there. The library fills the array in its ownglobalscallback, afterInitGlobalsand before anyInit.onGlobalscallback of the Map project, so read it fromInit.onGlobalsor a later stage. The array keeps its shape: oneMapPlayerper slot,Players[i]for sloti.addScriptHookhooks run underpcall. In w3ts 3.x a throwing hook endedmainorconfigat that point, silently, because a Lua error in a game thread prints nothing. Each hook now runs underpcall: 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 messagepcallreturned; 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::beforeruns later, by design.Init.onGlobalsruns afterInitGlobals, not beforemain: nothing should be created before the map's globals exist.main::afterandInit.onInitTriggersare the same moment, the end ofmain.addScriptHookitself keeps its old timing for this release, and no stage has the lobby timing ofconfig::beforeandconfig::after. - The game-time Timer starts after
MarkGameStarted. It was created atmain::after; it is created in the library'sgameStartcallback. Timers do not tick during initialization, sogetElapsedTime()reads the same on a running game; before the stage, it returns 0, as it did before the end ofmain. The host detection's Timer of w3ts 3.x is gone: build step 6 makes the detection opt-in, with a timeout Timer only onceHost.detectHost()runs. - The library makes no Handle-creating Native call at load. Requiring the library called
Playerfor every slot andCreateTriggerfor the sync System in w3ts 3.x, in the Lua root, before any initialization function ran. It calls neither now, norCreateTimer: every Handle of the library is born in a stage.FourCCstill 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
globalsstage. The Trigger was created when theSyncRequestclass was defined, and the sync events were registered by the first request. The library'sglobalscallback 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"). ASyncRequestmade 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
mainandconfigwhen 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_Gmetatable. The library's metatable composes with one the map installed before it, an undeclared-global warner for example: the map's__indexand__newindexkeep 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__newindexdoes not seeInitCustomTriggersormainbeing 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.configureafter a registration warns. A call that changesdevModeafter a callback was registered through the library printsreforged-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)callshandler(timer)on each expiry, with the Timer that was started, so a periodic handler pauses or destroys itself withoutTimer.fromExpired(). A handler written for w3ts 3.x takes no parameter and keeps working.Timer.fromExpired()stays and returnsundefinedwhen the game has no expired Timer, which happens when the Timer was destroyed right before its callback. Timer.destroyreturns nothing. It returned the Timer, which was no longer usable; it now returnsvoid, like every other Wrapper'sdestroy.pause,resumeandstartstill return the Timer.- Registration,
addActionandaddConditionreturn the Trigger. Everyregister*member returned theeventhandle of its Native,addActionthetriggeractionandaddConditionthetriggercondition; each now returns the Trigger, so a complete Trigger is one chained expression. No Native consumes anevent, so nothing is lost there; code that kept thetriggeractionortriggerconditionto remove one handler later subscribes withon()and ends the Subscription withdestroy()instead, or callsTriggerAddActionandTriggerAddConditionitself.removeActionandremoveConditionstay for handles obtained through the Natives. - Filter parameters are optional.
registerPlayerUnitEvent,registerFilterUnitEvent,registerEnterRegion,registerLeaveRegionandregisterUnitInRangetook a filter that had to be written,undefinedwhen there was none; it is now optional and may be left out. A filter given as a plain function is wrapped withFilter, aboolexpris passed as is, and a missing one passes nothing to the Native. addConditiontakes a plain function withoutCondition. It wraps a function withConditionitself, and adds aboolexpras is, so a Map project never callsConditionfor a Trigger. In w3ts 3.x it also returnedundefinedinstead of adding the condition whenConditionreturned nothing; it now always adds it.- The library makes no Blizzard.j call.
registerAnyUnitEvent(event)calledTriggerRegisterAnyUnitEventBJ; it now registers the player unit event withTriggerRegisterPlayerUnitEventfor the player of every slot belowbj_MAX_PLAYER_SLOTS, with no filter, the loop the Blizzard.j helper performs, so the Trigger receives the same events.registerPlayerMouseEvent(player, kind)calledTriggerRegisterPlayerMouseEventBJwith Blizzard.j's number (bj_MOUSEEVENTTYPE_DOWN,_UP,_MOVE); it now takesMouseEventKind.Down,MouseEventKind.UporMouseEventKind.Move, a string enum, and registersEVENT_PLAYER_MOUSE_DOWN,_UPor_MOVEwithTriggerRegisterPlayerEvent. 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 calledBlzGetTriggerFrameValue, so it returned the frame event's number; it now callsBlzGetTriggerFrameTextand is typedstring | undefined,undefinedfor 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.startreturns aPromise. It resolves on every client with aSyncResponseonce every packet of the sender's data arrived, and rejects on a timeout, a cancellation or a network error, soawaitworks and a failure reaches atry/catch. A request starts once: a secondstartthrowsreforged-ts: sync request <id> was already startedat 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 cancelledorreforged-ts: sync request <id> could not be sent (network error): a string, like every other error the library raises, not anErrorobject, whosetostringneeds thedebuglibrary the game does not have. A network failure no longer printsSyncData: Network Error; the request rejects on the sender's client instead. - The sync response and status.
SyncResponsecarries the data, the sender asfrom(read from the event of the last packet, so a continuation reads no event context), the elapsed game time astimeand therequest; it no longer carriesstatus, which the request's own read-onlystatusholds.SyncStatusgainsCancelledandNetworkError.SyncOptions.timeoutis 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 oncancel. 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 calledstartorsend, before the request starts: encode binary data first, for example withbase64Encode. - Host detection is opt-in and a
Promise.Host.detectHost()replacesonHostDetect: 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 thegameStartstage; 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,0for none): the longest lobby time wins and a tie goes to the lowest player index. It rejects with the stringreforged-ts: host detection received no lobby timeonly when no value arrived.Host.hostisundefineduntil the election resolved. The lobby time is still measured fromconfig, but0no longer reads as "not yet synced". - The binary string format is length-prefixed.
BinaryWriter.writeStringwrites a 2-byte big-endian length, then the bytes, so any byte, a zero included, round-trips;readStringreads 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
BinaryReaderthrows. EveryreadXadvances by exactly what it consumed, soreadDoubleno longer misaligns the values after it, and a read past the end throwsreforged-ts: <readX> past the end: position <P>, width <W>, <R> remaininginstead of returning 0 or garbage.positionandremainingread where the reader stands. - The
BinaryWriterrejects values outside a field's range. Every integerwriteX,writeInt32included, throwsreforged-ts: <writeX> takes <min> to <max>, got <value>for a value its width cannot hold, and NaN, at the call that writes it.writeUInt32andreadUInt32round-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. base64Decodethrows 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.base64Encodeaccepts any byte string and never prints.File.writeandFile.writeRawreturn nothing. They returned theFileclass; they now returnvoid.File.readreads contents holding the escape character followed byqcorrectly, where it returned a"in their place.sleepresolves with no value. It is typedPromise<void>, where it wasPromise<null>; the value at run time is nil either way. It runs onTimer.after.Item.getFieldandItem.setFieldreach the item field Natives. They compared the field's type with the unit field type names, so they returned 0 andfalsefor 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.createtakes 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 optionalz,face(0 by default),scale(1),variation(0),pitch,roll,skin,color(aplayercolor) anddead. The options given pick one of the game's 32 creation Natives:dead: truecreates a dead destructable,zplaces it at that height (whatcreateZ, now removed, did),pitchorrolltilts it with the absent one 0,skingives it a skin andcolora team colour. Positional arguments are a type error. It still throwsreforged-ts: failed to create Destructable (<rawcode>)at the calling line when the game creates nothing.GameCache.flushNumberflushes the real. It calledFlushStoredInteger, so it removed the integer stored under the key (the oneflushIntegerremoves) and left the real thatstorewrote with a number andgetNumberreads. It now callsFlushStoredReal. Code that flushed an integer throughflushNumbermust callflushInteger.Sound.setChannelsets the channel. It calledSetSoundDistanceCutoffwith the channel number, so it changed the sound's distance cutoff and left its channel. It now callsSetSoundChannel. Code that set a cutoff throughsetChannelmust callsetDistanceCutoff.unit.removeTyperemoves the unit type. In w3ts 3.x it calledUnitAddType, so it added the unit type it was asked to remove. It now callsUnitRemoveTypeand 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 otherFramesetters do, instead of nothing, so a chain can go on after it.MultiboardItem.create(board, x, y)andmultiboard.createItem(x, y)name their parametersrowandcolumn: the first is still the row and the second the column, both counted from 1, and the call passesrow - 1andcolumn - 1toMultiboardGetItemas before. Only the names change, so working code needs no edit.GameCachestores and restoresUnits.cache.restoreUnit()returns theUnitWrapper instead of the rawunithandle:Unit.fromHandle(cache.restoreUnit(...))becomescache.restoreUnit(...). It is a creation, so it throwsreforged-ts: failed to create Unit (<key>)whenRestoreUnitreturns nothing, as when no unit is stored under the key, where w3ts 3.x returnedundefined; checkcache.hasUnit(missionKey, key)first. In Dev mode the creation Guards apply to it.cache.store()takes a unit as theUnitWrapper: the argumentunit.handlebecomesunit.GetStoredString, and socache.getString(), is typedstringinstead ofstring | undefined: the game reads""for a missing key.item.getOwner()andMapPlayer.getTaxRateuseMapPlayer. Theitem.playeraccessor is renameditem.getOwner(), named like its setteritem.setOwner()and likeunit.getOwner(). It returns theMapPlayerWrapper of the owner, the objectMapPlayer.fromHandlegives, orundefinedwhenGetItemPlayerreturns nothing, instead of the rawplayerhandle:MapPlayer.fromHandle(item.player)becomesitem.getOwner().MapPlayer.getTaxRatetakes the other player as aMapPlayer, asMapPlayer.setTaxRatedoes: the argumentother.handlebecomesother.Unitmembers say what they do.unit.getflyHeight()andunit.setflyHeight()are renamedgetFlyHeightandsetFlyHeight.unit.getIgnoreAlarm(flag)is renamedsetIgnoreAlarm(flag): it changes the setting throughUnitIgnoreAlarmand reads nothing.unit.dropItemFromSlot(item, slot)is renamedmoveItemToSlot(item, slot): it moves the item to another inventory slot and drops nothing on the ground.unit.setUnitAttackCooldown()is removed: it duplicatedunit.setAttackCooldown(), which takes the same arguments. Each renamed member calls the same Native with the same arguments.Camerafollows the conventions of the other Wrappers.Camera.SetCinematicSceneis renamedCamera.setCinematicScene, with the same arguments.Camera.panandCamera.panTimedtakezOffsetDestas an optional last parameter, soCamera.pan(x, y)andCamera.panTimed(x, y, duration)pan without a z-offset; passingundefined, as w3ts 3.x required, still does the same, and the parameters keep their order.Camera.setCameraOrientControlleris renamedCamera.setOrientController, without the redundantCameraprefix, like its twinCamera.setTargetController. Both take theUnitWrapper instead of the rawunithandle: the argumentunit.handlebecomesunit.region.containsPoint()andleaderboard.hasPlayerItem()return their answer. In w3ts 3.x both called their Native (IsLocationInRegion,LeaderboardHasPlayerItem) without returning its result, so they always returnedundefined. They now return the Native's boolean.unit.skillPoints = nsets the skill points. In w3ts 3.x the setter calledUnitModifySkillPointswith its value, so it addednunspent points. It now sets the count ton, by the difference fromGetHeroSkillPoints; the game keeps it between 0 and what the hero can still spend. Code that granted points through the setter must callunit.modifySkillPoints(n).unit.addItemById()returns the item it dropped. When the inventory was full, the unit could not carry items or it was dead,UnitAddItemByIddropped 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 withCreateItemat the unit's position, puts it in the inventory withUnitAddItemand 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 checkhasItem.item.invulnerable = flaguses its value. In w3ts 3.x the setter passedtruetoSetItemInvulnerablewhatever the value, soitem.invulnerable = falsemade the item invulnerable. It now passes the value:falsemakes the item vulnerable again.player.startLocationPointbecomesplayer.getStartLocationPoint(), which returns aPoint. In w3ts 3.x the accessor returned the rawlocationhandleGetStartLocationLocallocates on each read, which the caller removed withRemoveLocation, orundefinedwhen the game returned none. It is now the methodgetStartLocationPoint(), likeunit.getPoint(), and returns a newPointwrapping that location: destroy it when done, soRemoveLocation(player.startLocationPoint)becomesplayer.getStartLocationPoint().destroy(). It is a creation, so it throwsreforged-ts: failed to create Pointwhen the game returns no location, and in Dev mode the creation Guards apply to it.player.startLocationXandplayer.startLocationYread the coordinates without creating anything.Group.addGroupFastandGroup.removeGroupFastchangethis. In w3ts 3.xa.addGroupFast(b)calledBlzGroupAddGroupFastwithafirst, and the game adds the units of the first group to the second (measured in 3.0.0), so it added the units ofatob;a.removeGroupFast(b)removed the units ofafrombthe same way. Both now pass their argument first:a.addGroupFast(b)adds the units ofbtoa,a.removeGroupFast(b)removes them froma, andbis left as it was. The return value is still the number of units added or removed. Code that calleda.addGroupFast(b)to fillbcallsb.addGroupFast(a), and code that calleda.removeGroupFast(b)to emptybof the units ofacallsb.removeGroupFast(a).OrderId.BattleroarandOrderId.Forkedlightninghold the game's ids. In w3ts 3.xOrderId.Battleroarheld the id ofbattlestations(852099) andOrderId.Forkedlightningheld the id ofelementalfury(852586); the mix-up went one way only:OrderId.BattlestationsandOrderId.Elementalfuryalways held the right ids. They are now 852599 and 852587, so both uses change. Code that issuedOrderId.BattleroarorOrderId.Forkedlightningto issuebattlestationsorelementalfuryissuesOrderId.BattlestationsorOrderId.Elementalfury. Code that compared an incoming order id (the order id of an order event,GetIssuedOrderId) withOrderId.BattleroarorOrderId.Forkedlightningto catchbattlestationsorelementalfuryno longer matches them: it compares withOrderId.BattlestationsorOrderId.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, soUnit.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'sdestroy()now removes the registry entry after its Native, in Dev mode and in release:fromHandlewith the same Handle afterwards returns a new Wrapper, not===to the destroyed one. Code that compared a Wrapper looked up afterdestroy()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 wrappedInitGlobalsstarted, typically at module top level, throwsreforged-ts: <Wrapper> created before the globals Init stage: create Handles in Init.onGlobals or a later stage, not at module top levelat the calling line. Creations insideInit.onGlobalscallbacks 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.afterorTimer.everyhandler ends the game thread silently. WithReforged.configure({ devMode: true }), the handler runs underpcall: the failure is shown on screen to the local player for thirty seconds and printed asreforged-ts: Timer#<id> Timer.start failed: <error>(the registering member, then the Lua error text with itswar3map.lualine), 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-
configurewarning names the first registration, and Dev mode warns on any late call. It readsreforged-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 isInit.onGlobals "<label>",addScriptHook("main::before") #1orTimer#<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. WithReforged.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 seconddestroy()) throwsreforged-ts: used after destroy: <Class>#<id>at the line of the access;tostringrenders<Class>#<id> (destroyed). Code that read.handleor.idof a Wrapper after destroying it must read them before. An older reference that the registry upgraded (aWidgetlooked up before theUnit) 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 newwrappersrows (class name, created, destroyed, live) come first, sorted by live descending, before the callback failures, andReforged.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 onlydestroy()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, andMapPlayer.fromLocal()andunit.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.addActionaction that throws is reported asreforged-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 (theTriggerregistration members and theGroup,ForceandRectangleenumeration 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. AGroup.for,Force.fororRectangle.enumItems/enumDestructablescallback that throws is reported and the enumeration continues with the next member. Anon()handler orwhenpredicate that throws is reported under the descriptor's name (reforged-ts: UnitEvents.death failed: <error>);EventDescriptorgained an optionalnamefor 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 runsfnon the client whose local player isplayeronly, in place of aGetLocalPlayer()comparison; with Dev mode off it is that comparison. In Dev modefnruns underpcall(an error is reported asreforged-ts: MapPlayer#<id> MapPlayer.runLocal failed: <error>and does not escape), and inside it creating or destroying a Wrapper,Group.for,Force.forand the firstFrame.fromNameof a frame the library has no Wrapper for raisereforged-ts: <action> inside MapPlayer.runLocal changes game state for one client, which desyncs the game: only visuals belong inside runLocalat 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 (throughregisterUnitEvent,registerFilterUnitEvent,registerPlayerUnitEventorregisterAnyUnitEvent, soon()damage subscriptions too) run one level deeper in a shared damage depth, andUnit.damageTargetraisesreforged-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 crasheswhen the depth exceeds the limit: eight nested dispatches by default, set throughReforged.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) becomesreforged-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 intypes(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.0declared the Natives ofcommon.aiwith the others. They are valid only in AI scripts, soreforged-types/3.0.0leaves them out; addreforged-types/3.0.0/common.aitotypesto 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 takesundefined. 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),@asyncwhen its value is valid only for the local player,@deprecatedwith the reason,@patchfor the Patch that added it, and a link to its reference page.
What stays the same
- The names.
Unit,Timer,Trigger,Frame,MapPlayerand 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.Playerskeeps its shape: oneMapPlayerper slot,Players[i]for sloti, filled at theglobalsInit stage (see the behaviour changes).addScriptHookstill 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 | undefinedfor a Native that can return nothing,Record<number, T>for the Jass arrays, and@noSelfInFilewiththis: voidcallbacks, 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.