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 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 buildandamxts devdo that. A plugin built against another version gets a line in the console instead of the call.
What crosses between plugins
| In the module | Between plugins |
|---|---|
number, boolean, string, a union of strings | as they are |
T[], T | null | as they are |
Player | the 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 fields | a 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, aSet, 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,{},[]ornullis fine; - a generic or
asyncfunction, 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).
HTTP
A plugin talks to web services with two functions, both globals - no import line:
Creating a module
A module is TypeScript code the plugins of a project use - greeter.greet(player), without an import line, by the name the module gives itself. What it exports - its functions and types - is its API. It says what it is with defineModule, a project lists it in amxts.config.ts, and the server runs one instance of it. menu-core and config-core are modules. Code without state of its own on the server - a client for some service, helpers - can instead be a library: compiled into each plugin that imports it.