Skip to content

Architecture

These five diagrams document the @infernus/* monorepo from the outside in: the package graph and what each wrapper does; inside @infernus/core; inside the samp-node C++ plugin that embeds Node.js; how one event flows through core; and finally the full round trip between core and the plugin.

Each diagram is a self-contained artifact and follows the language you are reading the docs in. The embedded view is a static canvas — follow the link below any diagram for pan and zoom, node search and focus, relationship tracing, theme switching, and PNG/SVG export.

Monorepo and package internals

All 25 buildable packages under packages/* plus types, shared, the runtime host, and the build pipeline. The dependency spine runs left to right: a gamemode on @infernus/core, through the samp-node plugin, into the open.mp server. The four bordered regions group the wrapper packages by domain, and each node carries a concrete implementation fact — the native it wraps or the mechanism it uses.

Package names and native identifiers stay in English; region labels and the summary cards are authored in this page's language.

Inside @infernus/core

The internal subsystems: components (entities, the filterScript loader, the CmdBus command dispatcher, the GameMode lifecycle), utils (the bus.ts event engine, hook.ts interception, pools.ts entity registries), and wrapper (the typed native bindings and the swappable __inject__ tables).

internalPlayerProps is a Symbol key, which is why plugins can extend Player without colliding with core's private runtime fields.

Inside the samp-node plugin

@infernus/core is not a standalone library — every native call and every event ultimately goes through the C++ samp-node plugin, which embeds Node.js as a shared library (libnode). This diagram covers that plugin: the PLUGIN_EXPORT entry points (Load/Unload, OnPublicCall, AmxLoad, ProcessTick), the isolated V8 and uv_loop runtime created in nodeimpl.cpp, the resource.cpp bootstrap, and the events.cpp / natives.cpp / callbacks.cpp marshalling layers.

Two details explain most of the behaviour you observe in core. There is no separate Node thread: ProcessTick drives Tick on the server's main thread every frame. And since a listener may legitimately return a Promise, handlePromiseReturnValue spins Tick until that promise settles, which is why core can treat an async listener as returning a synchronous 0/1.

How defineEvent runs

The runtime path behind every event, read straight from packages/core/src/utils/bus.ts. Registration is an import side effect: defineEvent calls samp.registerEvent to declare the native callback signature and samp.on to attach the trigger.

From there trigger runs the middleware chain — beforeEach parses the native arguments into a context object, each listener receives { next, defaultValue, ...context }, and next(value) both advances the chain and merges value into that shared context. A listener that never calls next() halts propagation, and its return value becomes the event result after transformReturnValue normalizes it to 0 or 1. afterEach runs once at the end.

That is what makes a listener chain compose: any link can inspect or rewrite what the downstream links see, or stop the event outright.

core and the plugin, end to end

Putting both halves together: one native callback travelling inbound from open.mp through the plugin's AMX-to-V8 conversion into core's middleware chain, then outbound again through callNative and InvokeNativeArray.

The upper half of the timeline is plugin startup, including what the bootstrap script actually does; the lower half is the steady-state round trip. Read this diagram after the two above it — it is the join between them.