Modules

Shared modules

A module is a package of TypeScript that plugins use - the official menu-core and config-core, or a module of your own. A module has one instance on the server, shared by every plugin that uses it:

menus
.
addCondition
("IS_ALIVE", (
player
) =>
player
.
isAlive
);
menus
.
addAction
("RESET_SCORE", ({
player
}) => resetScore(
player
));
server
.
addCommand
("/menu", ({
player
}) =>
menus
.
show
(
player
, "MAIN_MENU"));
function resetScore(
player
: Player) {
player
.
frags
= 0;
player
.
deaths
= 0;
}

Here the condition and the action this plugin adds are the menu's, whichever plugin shows it; a player has one open menu, not one per plugin; and a config folder one plugin sets is the folder for all of them. The code looks the same as a call inside one plugin, and so do the types the editor shows.

A module is listed in the project's amxts.config.ts (modules: ["@amxts/menu-core"], or npx amxts module add menu-core), and plugins use it by the name it gives - menus - without an import (auto-imports), or import it by its package name. A module no plugin uses is not built.

Where a module runs

A module runs in a plugin of its own, which the build makes and names after the package - menu-core.aot for @amxts/menu-core - and lists in plugins.ini before your plugins, after the modules it needs. When your plugin calls the module, the call runs there, and the answer comes back.

The module is ready once its plugin has loaded. A plugin listed after it in plugins.ini, as the build lists every plugin, can call it from its top level. If the module's plugin is not running, a call writes one line to the server console - menu-core: no plugin runs it - is menu-core.aot in plugins.ini? - and returns nothing: false, 0, "" or null.

When one of your plugins is unloaded or reloaded - amxts dev reloads the plugin you save - a module drops what that plugin gave it: menu-core the menus it made and the items it added, resemiclip its rule. The new load gives them again from its top level, so a menu made there is there once. A player who has that menu open keeps it through a reload, drawn again from the new load on the same page; after an unload it closes. A module of your own does the same with onPluginStop.

A call to another plugin
  • A call copies its arguments both ways. It costs far more than a call inside one plugin: fine at start-up and on a command, worth avoiding for every player on every frame.
  • Build together. A module's plugin and the plugins that use it are built from the same version of the module; amxts build and amxts dev do that. A plugin built against another version gets a line in the console instead of the call.

What crosses between plugins

In the moduleBetween plugins
number, boolean, string, a union of stringsas they are
T[], T | nullas they are
Playerthe same player
an object the module keeps - one it hands out and takes back, one with methods of its own (Menu, ConfigNode), or one it exports (export const semiclip = new Semiclip())the module's own object: its fields and accessors are read and written in the module - a setter runs there - and its methods run there
any other object - an interface (MenuShowOptions), a class of fieldsa copy; a field left out stays left out
an object with methods passed to a callback (MenuEvent)a copy, whose fields come back after the call: event.preventDefault() works
a function (a condition, onSelect)stays in the plugin that wrote it; the module calls it back there

An object the module keeps is the same object in every plugin: menus.find("SHOP") twice gives the same Menu, menu.title = "..." changes the module's menu, and a function given to one of its methods is called back in the plugin that wrote it. A function passed twice is the same function, so the module can tell it is one already registered.

What does not cross

A module's exported function that cannot work across plugins stops the build, naming the module, the function and the parameter:

  • a parameter or a result of a type that cannot cross - a Map, a Set, a generic class, a class from the standard library, a type the module does not export;
  • a default another plugin cannot work out itself - index = base; a literal, {}, [] or null is fine;
  • a generic or async function, or a rest parameter (...args);
  • an exported variable (export let count): export a function that reads or sets it, or an object of a class of the module's own (export const counter = new Counter()), whose accessors do.
~/modules/bad: export function remember - parameter "counts": Map from the standard library cannot cross - pass its contents as an array or a record

Code that needs none of this to be shared - a client for a service, async from end to end - can be a library instead: compiled into each plugin that imports it, with nothing to cross.

Limits

  • An array or object field of a shared object reads as a copy.menu.items.push(item) changes the copy. Use the module's own method (menu.addItem(...)), or assign the whole field.

A file of shared code in your plugins folder (plugins/lib/format.ts, imported as ~/lib/format) is not a module: it is compiled into every plugin that imports it, with its own variables in each. A file plugins/modules/<name>.ts beside a plugin plugins/<name>.ts is a module of the project: that plugin runs it, and the others call it there (creating a module).