The module to end all modules!
Basically just the same idea behind Eluna/ALE, but for JavaScript (Node.js, specifically) instead of Lua.
Requires core patch: azerothcore/azerothcore-wotlk#26648
I recorded my screen for a demo that includes building it all from scratch after checking out the code, then using the VSCode debugger to set breakpoints and step through the random ad-hoc script file from the examples/ directory here:
This particular demo was recorded with several other modules and a Playerbot fork because that's what I happen to be working with in my local environment right now, but mod-nodejs can be used as the only module on a Playerbot-free setup as well.
The usual process of mod_nodejs.conf.dist --> mod_nodejs.conf. Set NodeJs.Script to the path to your script file that will be loaded at world startup. It can import other script files as well (or it can be a single-file bundle), there just needs to be exactly one.
Documentation for all the APIs will come later (probably in the form of some TypeScript declaration files). For now, the best way to explore what's available on the various objects is to use the debugger and explore the various objects and types. See the config file for the flags you can turn on, and Node.js docs for the different ways you can connect to the debugger once it's running.
Click to expand the list of all hooks...
Generated by:
grep --recursive --perl-regexp --only-matching --no-filename '(?<=\binvoke_hook(?:_t)?\(")([^"]*)' src | sort -u
Hooks:
account:before-delete
account:can-create-character
account:email-change
account:failed-email-change
account:failed-login
account:failed-password-change
account:last-ip-update
account:login
account:password-change
all-creature:add-world
all-creature:before-select-level
all-creature:can-gossip-hello
all-creature:can-gossip-select
all-creature:can-gossip-select-code
all-creature:can-quest-accept
all-creature:can-quest-reward
all-creature:ffa-pvp-state-update
all-creature:remove-world
all-creature:save-to-db
all-creature:select-level
all-creature:update
all-game-object:add-world
all-game-object:can-gossip-hello
all-game-object:can-gossip-select
all-game-object:can-gossip-select-code
all-game-object:can-quest-accept
all-game-object:can-quest-reward
all-game-object:damaged
all-game-object:destroyed
all-game-object:loot-state-changed
all-game-object:modify-health
all-game-object:remove-world
all-game-object:save-to-db
all-game-object:state-changed
all-game-object:update
all-item:can-expire
all-item:can-quest-accept
all-item:can-remove
all-item:can-use
all-item:gossip-select
all-item:gossip-select-code
all-map:before-create-instance
all-map:create
all-map:destroy
all-map:destroy-instance
all-map:player-enter
all-map:player-leave
all-map:update
auction-house:add-auction
auction-house:before-send-auction-cancelled-to-bidder-mail
auction-house:before-send-auction-expired-mail
auction-house:before-send-auction-outbidded-mail
auction-house:before-send-auction-sale-pending-mail
auction-house:before-send-auction-successful-mail
auction-house:before-send-auction-won-mail
auction-house:before-update
auction-house:expire
auction-house:remove-auction
auction-house:successful
battleground:add-player
battleground:before-add-player
battleground:create
battleground:destroy
battleground:end
battleground:end-reward
battleground-queue:add-group
battleground-queue:before-send-exit-message-arena
battleground-queue:before-send-join-message-arena
battleground-queue:can-add-group-to-matching-pool
battleground-queue:can-fill-players
battleground-queue:can-send-message
battleground-queue:get-player-matchmaking-rating
battleground-queue:is-check-normal-match
battleground-queue:update
battleground-queue:update-validity
battleground:remove-player-at-leave
battleground:start
battleground:update
group:add-member
group:can-join-battleground-queue
group:change-leader
group:create
group:disband
group:invite-member
group:remove-member
guild:add-member
guild:can-send-bank-list
guild:create
guild:demote-player
guild:deposit-bank-item
guild:deposit-bank-money
guild:disband
guild:generic-logged-bank-event
guild:generic-logged-event
guild:info-changed
guild:invite-player
guild:item-move
guild:member-deposit-money
guild:member-withdraw-money
guild:motd-changed
guild:move-bank-item
guild:player-leave
guild:promote-player
guild:remove-member
guild:repair-bank-money
guild:uninvite-player
guild:withdraw-bank-item
guild:withdraw-bank-money
loot:money
misc:after-loot-template-process
misc:can-apply-soulbound-flag
misc:can-item-apply-equip-spell
misc:can-send-auction-hello
misc:get-dialog-status
misc:instance-save
misc:item-create
misc:player-set-phase
misc:validate-spell-at-cast-spell
misc:validate-spell-at-cast-spell-result
nodejs:before-shutdown
nodejs:startup
pet:add-to-world
pet:calculate-max-talent-points-for-level
pet:can-reset-talents
pet:can-unlearn-spell-default
pet:can-unlearn-spell-set
pet:init-stats-for-level
player:achievement-complete
player:achievement-save
player:add-to-battleground
player:after-creature-loot
player:after-creature-loot-money
player:after-guardian-init-stats-for-level
player:after-move-item-from-inventory
player:after-set-visible-item-slot
player:after-spec-slot-changed
player:after-store-or-equip-new-item
player:after-update-attack-power-and-damage
player:after-update-max-health
player:after-update-max-power
player:anticheat-check-movement-info
player:anticheat-handle-double-jump
player:anticheat:set-can-flyby-server
player:anticheat:set-jumping-by-opcode
player:anticheat:set-root-ack-upd
player:anticheat:set-under-ack-mount
player:anticheat:update-movement-info
player:apply-ammo-bonuses
player:apply-enchantment-item-mods-before
player:apply-item-mods-before
player:apply-weapon-damage
player:battleground-desertion
player:before-achievement-complete
player:before-buy-item-from-vendor
player:before-choose-graveyard
player:before-criteria-progress
player:before-durability-repair
player:before-fill-quest-loot-item
player:before-get-level-for-xp-gain
player:before-guardian-init-stats-for-level
player:before-init-talent-for-level
player:before-load-pet-from-db
player:before-logout
player:before-loot-money
player:before-open-item
player:before-quest-complete
player:before-send-chat-message
player:before-store-or-equip-new-item
player:before-teleport
player:before-temp-summon-init-stats
player:before-update
player:before-update-attack-power-and-damage
player:before-update-skill
player:being-charmed
player:bind-to-instance
player:calculate-talents-points
player:can-apply-enchantment
player:can-apply-equip-spell
player:can-apply-equip-spells-item-set
player:can-apply-weapon-dependent-aura-damage-mod
player:can-area-explore-and-outdoor
player:can-armor-damage-modifier
player:can-battle-field-port
player:can-cast-item-combat-spell
player:can-cast-item-use-spell
player:can-enter-map
player:can-equip-item
player:can-fly-in-zone
player:can-give-level
player:can-give-mail-reward-at-give-level
player:can-group-accept
player:can-group-invite
player:can-init-trade
player:can-join-in-arena-queue
player:can-join-in-battleground-queue
player:can-join-lfg
player:can-learn-talent
player:can-place-auction-bid
player:can-repop-at-graveyard
player:can-resurrect
player:can-save-equip-new-item
player:can-sell-item
player:can-send-error-already-looted
player:can-send-mail
player:can-set-trade-item
player:can-unequip-item
player:can-update-skill
player:can-use-chat
player:can-use-chat:channel
player:can-use-chat:group
player:can-use-chat:guild
player:can-use-chat:player
player:can-use-item
player:check-item-in-slot-at-load-inventory
player:complete-quest
player:create
player:create-item
player:creature-kill
player:creature-killed-by-pet
player:criteria-progress
player:criteria-save
player:custom-scaling-stat-value
player:custom-scaling-stat-value-before
player:delete
player:delete-from-db
player:duel-end
player:duel-request
player:duel-start
player:emote
player:enter-combat
player:equip
player:failed-delete
player:ffa-pvp-state-update
player:first-login
player:forgot-spell
player:free-talent-points-changed
player:get-arena-personal-rating
player:get-arena-team-id
player:get-feral-ap-bonus
player:get-max-personal-arena-rating-requirement
player:get-max-skill-value
player:get-max-skill-value-for-level
player:get-quest-rate
player:get-reputation-price-discount
player:get-reputation-price-discount:by-creature
player:give-reputation
player:give-xp
player:gossip-select
player:gossip-select-code
player:group-roll-reward-item
player:has-active-power-type
player:is-class
player:is-ffa-pvp
player:is-pvp
player:join-arena
player:join-bg
player:just-died
player:killed-by-creature
player:learn-spell
player:learn-talents
player:learn-taxi-node
player:leave-combat
player:level-changed
player:load-from-db
player:login
player:logout
player:loot-item
player:map-changed
player:money-changed
player:not-avoid-satisfy
player:not-set-arena-team-info-field
player:not-visible-globally-for
player:passed-quest-killed-monster-credit
player:petition-buy
player:petition-show-list
player:pvp-flag-change
player:pvp-kill
player:quest-abandon
player:quest-accept
player:quest-compute-xp
player:quest-reward-item
player:queue-random-dungeon
player:released-ghost
player:remove-from-battleground
player:reputation-change
player:reputation-rank-change
player:resurrect
player:reward-kill-rewarder
player:save
player:send-initial-packets-before-add-to-map
player:send-list-inventory
player:set-max-level
player:set-server-side-visibility
player:set-server-side-visibility-detect
player:set-skill
player:should-be-rewarded-with-money-instead-of-exp
player:spell-cast
player:store-new-item
player:talents-reset
player:text-emote
player:unequip
player:update
player:update-area
player:update-crafting-skill
player:update-faction
player:update-fishing-skill
player:update-gathering-skill
player:update-skill
player:update-zone
player:victim-reward-after
player:victim-reward-before
spell:before-aura-rank-for-level
spell:calc-max-duration
spell:can-prepare
spell:can-scaling-everything
spell:can-select-spec-talent
spell:cast
spell:cast-cancel
spell:check-cast
spell:dummy-effect:creature-target
spell:dummy-effect:game-object-target
spell:dummy-effect:item-target
spell:prepare
spell:remove-aura-scale-targets
spell:scale-aura-unit-add
ticket:close
ticket:create
ticket:resolve
ticket:status-update
ticket:update-last-change
unit:aura-apply
unit:aura-remove
unit:before-roll-melee-outcome-against
unit:can-set-phase-mask
unit:damage
unit:deal-damage
unit:death
unit:display-id-change
unit:enter-combat
unit:enter-evade-mode
unit:heal
unit:if-normal-reaction
unit:is-custom-build-values-update
unit:modify-heal-received
unit:modify-melee-damage
unit:modify-periodic-damage-auras-tick
unit:modify-spell-damage-taken
unit:patch-values-update
unit:set-shapeshift-form
unit:should-track-values-update-pos-by-index
unit:update
vehicle:add-passenger
vehicle:install
vehicle:install-accessory
vehicle:remove-passenger
vehicle:reset
vehicle:uninstall
world-object:create
world-object:install
world-object:reset-map
world-object:set-map
world-object:update
world:shutdown
world:startup
You could also try starting with this script file which shows off some of it:
// this is a full Node.js environment, so all the usual 'node:*' modules are available.
// that said, not all of them will work as you expect. it's still running inside AC, after all, so
// even though you can write to the console just fine, reading from stdin will probably do weird and
// system-dependent things because AC is *also* trying to read from that same stdin.
import util from 'node:util';
let someObject = {};
function logBobb(player) {
console.log('Properties of Acore:');
console.log(util.inspect(Acore, { depth: 1, showHidden: true, colors: true }));
console.log('Methods you can call on Acore:');
console.log(util.inspect(Acore.prototype, { depth: 1, showHidden: true, colors: true }));
console.log('Properties of Bobb:');
console.log(util.inspect({
player,
}, { depth: 2, showHidden: true, colors: true }));
console.log('Methods you can call on Bobb:');
console.log(util.inspect({
PlayerStuff: Acore.Player.prototype,
UnitStuff: Acore.Unit.prototype,
WorldObjectStuff: Acore.WorldObject.prototype,
ObjectStuff: Acore.ACObject.prototype,
}, { depth: 1, showHidden: true, colors: true }));
}
Acore.hooks.on('nodejs:startup', (args) => {
if ('persistData' in args) {
// this string is equal to whatever you passed to persistData during nodejs:before-shutdown.
Acore.logInfo('module', `persistData is: ${args.persistData}`);
someObject = JSON.parse(args.persistData);
if ('playerGuid' in someObject) {
const bobb = Acore.Player.byGuid(someObject.playerGuid);
if (bobb) {
Acore.logInfo('module', 'Bobb was logged in before we reloaded!');
logBobb(bobb);
}
}
} else {
Acore.logInfo('module', 'Node.js initial startup.');
someObject = {};
}
});
Acore.hooks.on('nodejs:before-shutdown', (args) => {
// the entire runtime environment is being destroyed.
if (args.reloading) {
// but a new one will be created in its place. mod-nodejs provides this persistData property
// as a way to save a string that the new runtime environment can read to recreate the state
// that would be unfortunate to lose entirely. even so, it's still advised not to rely on
// this outside of development.
args.persistData = JSON.stringify(someObject);
Acore.logInfo('module', 'Reload detected! See you on the other side...');
} else {
Acore.logInfo('module', 'Node.js final shutdown.');
}
});
Acore.hooks.on('player:login', (args) => {
if (args.player.name !== 'Bobb') {
return;
}
Acore.logInfo('module', 'Bobb logged in!');
logBobb(args.player);
// GUIDs are opaque to JavaScript code for performance reasons. if you need their values, use
// Acore.decodeGuid to unpack them into tuples of [HighGuid, entry, counter]. you can create the
// same tuples and pass them into functions that also accept the opaque GUIDs if you'd like.
// e.g., the player with ID 6 will have a GUID of [0, 0, 6], so thePlayer.guid will give you an
// opaque GUID (if you util.inspect it, it'll say [External: 6]), and Acore.decodeGuid will give
// you an array of [0, 0, 6]. Acore.Player.byGuid(thePlayer.guid) will give you the player back,
// but Acore.Player.byGuid([0, 0, 6]) will work just as well.
someObject.playerGuid = Acore.decodeGuid(args.player.guid);
});
Acore.hooks.on('player:logout', (args) => {
if (!Acore.guidsEqual(someObject.playerGuid, args.player.guid)) {
return;
}
Acore.logInfo('module', 'Bobb logged out! Everybody say "bye, Bobb!"');
delete someObject.playerGuid;
});You need Node.js headers and library. Even if you have Node.js installed for other reasons, you probably don't have what's needed here, so read on.
Untested, incomplete, YMMV. AFAICT you will need at least:
- from https://nodejs.org/dist/v26.5.0, the
*-headers.tar.xzfile - from https://nodejs.org/dist/v26.5.0/win-x64, both
node.exeandnode.lib
I think non-ancient versions of 7-zip support .tar.xz files now.
Where they go:
- after unzipping the headers, you should have a folder like
C:\something\blah\include\node. That is what you will use forNODEJS_INCLUDE_DIRwhen the time comes. node.exeandnode.libboth go into the same folder right next to each other. The full path tonode.libis what you will use forNODEJS_LIBwhen the time comes.
I've been developing this way, so I can be a bit more confident. Assumes you have Nix working (you don't need NixOS to run Nix).
From the build-helpers/ directory in this repo, replace /path/to/where/you/want/these with where you actually want it and run:
nix-build -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/nixpkgs-unstable.tar.gz -o /path/to/where/you/want/these extradeps.nix
It'll take a while, but this needs to be done only rarely. Once that finishes:
/path/to/where/you/want/these/include/nodeis yourNODEJS_INCLUDE_DIR/path/to/where/you/want/these/lib/libnode.so.147is yourNODEJS_LIB(you might need to bump up the version number)/path/to/where/you/want/these/lib/libuv.sois yourLIBUV_LIB
No worries if Nix isn't your thing. The Old Ways still work perfectly fine.
- First, make sure you have everything you need to build AC without this module.
- Then, make sure you have all the system setup stuff mentioned on https://github.com/nodejs/node/blob/v26.5.0/BUILDING.md#building-nodejs-on-supported-platforms
- There's a part that refers to "if building Node.js with Temporal support".
- Temporal IS required for this AC module, so follow that as well.
- Build shared Node.js with Temporal support (see sub-heading).
- I'll use the placeholder
/path/to/where/you/want/theseto refer to where you install it to.
- I'll use the placeholder
/path/to/where/you/want/these/include/nodeis yourNODEJS_INCLUDE_DIR/path/to/where/you/want/these/lib/libnode.so.147is yourNODEJS_LIB(you might need to bump up the version number)
The usual (replace /path/to/where/you/want/these with where you actually want it):
./configure --prefix=/path/to/where/you/want/these --v8-enable-temporal-support --shared
make -j$(nproc)
make install
If you want to use libnode for something else, check Node.js's BUILDING.md link (or ./configure --help) for the other flags you can pass. If you happen to use --shared-libuv, then you probably only need me to tell you the mod-nodejs specific detail that this is the trigger for when you need a LIBUV_LIB like in the Nix section.
https://github.com/nodejs/node/blob/v26.5.0/BUILDING.md#ccache is another notable section.
The previous section got you your NODEJS_INCLUDE_DIR and your NODEJS_LIB. You may also have a LIBUV_LIB.
Add each one's -DTHAT_VARIABLE=ITS_VALUE to your cmake command. You may need to use quotes if you have spaces in your paths.
This concludes the parts that are specific to mod-nodejs. Once you pass correct values for those extra args on your cmake command line, everything goes the same as with any other AC module.
