Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FumoEngine

FumoEngine is Fumoware’s embedded JavaScript engine. It runs ECMAScript modules through QuickJS-NG and gives every installed package its own runtime, context, globals, module graph, and console stream.

All builds discover packages from:

%APPDATA%\FumowareV2\scripts

Safe mode is the default. It applies per-package runtime limits and does not expose process memory or native calls. Insecure mode is opt-in and enables fumo:ffi only for packages whose manifest also requests the ffi capability.

Insecure mode is not a sandbox. An FFI script runs native code inside the game process and can crash it, corrupt memory, or compromise the current Windows user.

Current implementation

The current engine includes:

  • Manifest-based package discovery
  • One QuickJS runtime per package
  • ES modules and package-relative imports
  • Global and module-based console logging
  • Promise startup and shutdown handling
  • Execution deadlines
  • Per-package enable and disable controls
  • Source editing, saving, and reload from the menu
  • A bounded JavaScript console window
  • Optional Win64 FFI and typed native memory access

Other modules described in the original architecture document remain planned until they appear in this reference.

Getting started

Create a package directory:

%APPDATA%\FumowareV2\scripts\hello-fumo\

Add manifest.json:

{
  "id": "example.hello-fumo",
  "name": "Hello Fumo",
  "version": "1.0.0",
  "apiVersion": 1,
  "entry": "index.js",
  "capabilities": ["console"]
}

Add index.js:

import { log } from "fumo:console";

log("Hello from FumoEngine");

export function shutdown() {
  console.log("Hello Fumo unloaded");
}

Open Fumoware’s Scripts tab and press R to rediscover packages. New packages appear disabled; turn the package on once to evaluate it. That choice is remembered across game restarts. Output appears in the JavaScript console.

Module imports

Package-relative imports are supported:

import { value } from "./helper.js";

Imports cannot leave the package directory. Arbitrary bare module names and QuickJS’s std and os modules are unavailable.

Packages and manifests

Every immediate child directory under the script root is treated as a possible package. A package must contain manifest.json.

Manifest fields

FieldTypeRequiredMeaning
idstringyesStable, unique package identifier.
namestringyesName shown in the Scripts workspace.
versionstringyesSemantic package version.
apiVersionintegeryesFumoEngine API version. Currently 1.
entrystringyesPackage-relative .js or .mjs entry file.
descriptionstringnoHuman-readable package summary.
authorstringnoPackage author.
capabilitiesstring[]yesHost features requested by the package.
dependenciesobjectnoPackage IDs mapped to semantic version ranges.
assetsstring[]noFiles decoded and cached before entry evaluation.
http.hostsstring[]noExact or wildcard hosts allowed for HTTP.

Paths are canonicalized before loading. Symlinked package directories, absolute paths, and paths escaping the package root are rejected.

Capabilities

Capabilities declare what a package intends to use. Common capabilities are console, events, draw, trace, input.write, assets, sound, http, and ffi.

FFI requires both a manifest request:

{
  "capabilities": ["console", "ffi"]
}

and Settings > Script Engine > Insecure scripting.

Dependencies

Dependencies load before their consumers. Supported ranges include exact versions, *, caret and tilde ranges, and whitespace-separated comparators:

{
  "dependencies": {
    "author.common-library": "^1.2.0",
    "author.protocol": ">=2.0.0 <3.0.0"
  }
}

Missing, disabled, incompatible, and cyclic dependencies keep the consumer unloaded and put a specific reason in its Scripts workspace status.

Assets and HTTP

Every file used through fumo:assets or fumo:sound must be declared:

{
  "assets": [
    "images/marker.png",
    "sounds/notify.wav",
    "data/presets.json"
  ],
  "http": {
    "hosts": ["api.example.com", "*.static.example.com"]
  }
}

The runtime preloads declared assets before entry evaluation. Safe mode only permits HTTPS.

Template catalog

Copy a template directory into %APPDATA%\FumowareV2\scripts, change its package ID when building from a starter, press R in the Scripts workspace, and enable it. New packages start disabled.

Download every template as a ZIP

Archive: 21 KiB | SHA-256: 33FF2310BEAB64DF6F87E36EDD67F23DBB55358F2825BD7346C76022D4C238EB

Starter templates

FolderPackageVersionAPICapabilitiesDescription
basicMy FumoJS Script
example.my-script
1.0.01console, events, drawStarter package using native controls and persistent drawing.
http-assetsHTTP and Assets Starter
example.http-assets
1.0.01console, assets, httpStarter package for preloaded data and allowlisted HTTPS.

Conformance templates

FolderPackageVersionAPICapabilitiesDescription
01-core-consoleCore console conformance
test.fumojs.core-console
1.0.01consoleCore console conformance
02-promisesPromise conformance
test.fumojs.promises
1.0.01consolePromise conformance
03-isolation-aIsolation writer
test.fumojs.isolation-a
1.0.01consoleIsolation writer
04-isolation-bIsolation reader
test.fumojs.isolation-b
1.0.01consoleIsolation reader
05-events-drawEvents and drawing
test.fumojs.events-draw
1.0.01consoleEvents and drawing
06-extended-drawExtended drawing
test.fumojs.extended-draw
1.0.01console, drawExtended drawing
07-native-controlsNative controls
test.fumojs.native-controls
1.0.01console, uiNative controls
08-trace-commandTrace and command lifetime
test.fumojs.trace-command
1.0.01console, events, trace, input.writeTrace and command lifetime
09-assetsCached assets
test.fumojs.assets
1.0.01console, assetsCached assets
10-http-policyHTTP policy
test.fumojs.http-policy
1.0.01console, httpHTTP policy
11-dependency-baseDependency base
test.fumojs.dependency-base
1.4.01consoleDependency base
12-dependency-consumerDependency consumer
test.fumojs.dependency-consumer
1.0.01consoleDependency consumer
13-texture-soundTexture and sound cache
test.fumojs.texture-sound
1.0.01console, events, draw, ui, assets, soundTexture and sound cache
14-command-readonlyCommand Read-only Test
test.fumojs.command-readonly
1.0.01noneVerifies command writes require input.write.
15-trace-quotaTrace Quota Test
test.fumojs.trace-quota
1.0.01traceVerifies the per-pump penetration quota.
16-http-credentialsHTTP Credential Test
test.fumojs.http-credentials
1.0.01httpVerifies URL credentials are rejected before a request starts.
20-render-persistenceRender Persistence Test
test.fumojs.render-persistence
1.0.01drawDraws once so persistent frame buffering can be checked visually.
21-http-liveHTTP Live Test
test.fumojs.http-live
1.0.01httpOptional live HTTPS promise and JSON response test.

Expected rejection templates

These packages are intentionally incompatible or unsafe and should remain unloaded with a specific status or console error.

FolderPackageVersionAPICapabilitiesDescription
17-dependency-mismatchDependency Mismatch Test
test.fumojs.dependency-mismatch
1.0.01noneMust remain unavailable because the installed base is version 1.x.
18-dependency-cycle-aDependency Cycle A
test.fumojs.dependency-cycle-a
1.0.01noneFirst half of a dependency cycle rejection test.
19-dependency-cycle-bDependency Cycle B
test.fumojs.dependency-cycle-b
1.0.01noneSecond half of a dependency cycle rejection test.
90-budget-interruptExpected rejection - execution budget
test.fumojs.budget-interrupt
1.0.01consoleExpected rejection - execution budget
91-memory-limitExpected rejection - memory limit
test.fumojs.memory-limit
1.0.01consoleExpected rejection - memory limit
92-bare-import-deniedExpected rejection - bare import
test.fumojs.bare-import-denied
1.0.01consoleExpected rejection - bare import
93-path-traversal-deniedExpected rejection - path traversal
test.fumojs.path-traversal-denied
1.0.01consoleExpected rejection - path traversal
94-invalid-manifestExpected rejection - invalid manifest
test.fumojs.invalid-manifest
1.0.0999consoleExpected rejection - invalid manifest
99-ffi-deniedExpected rejection - insecure FFI unavailable
test.fumojs.ffi-denied
1.0.01console, ffiExpected rejection - insecure FFI unavailable

Total templates: 29

Scripts workspace

The Scripts tab contains an installed-package rail and a source editor.

Toolbar controls:

ControlAction
RRediscover and reload all packages.
SSave the selected entry file and reload its package.
TOpen the JavaScript console window.

The toggle beside each package uses the same toggle control as the rest of Fumoware. Disabling a package calls its exported shutdown() function, destroys its context, and destroys its runtime. Enabling it creates a fresh runtime and evaluates the entry module again.

New packages do not automatically run when the DLL is injected. Toggle state is stored immediately in %APPDATA%\FumowareV2\script-states.cfg and restored on the next game launch or injection.

The source editor currently edits the package entry file and enforces a 1 MiB source-file limit. Line numbers follow the editor’s vertical scroll.

The JavaScript console retains the newest 1,000 entries. It includes package output, unhandled promise rejections, load failures, engine lifecycle messages, and contained native faults.

API reference

FumoEngine APIs are exposed as globals or built-in ES modules.

Currently implemented:

APIAvailability
Global consoleEvery loaded package
fumo:consoleEvery loaded package
fumo:eventsEvery loaded package
fumo:drawEvery loaded package
fumo:uiEvery loaded package
fumo:gameEvery loaded package
createMove commandRead every package; writes need input.write
fumo:traceManifest trace capability
fumo:assetsDeclared package assets
fumo:soundManifest sound capability
fumo:httpManifest http capability and host allowlist
Global timers / fumo:timersEvery loaded package
fumo:storageEvery loaded package
fumo:ffiInsecure mode plus manifest ffi capability

An import fails during module linking when its requirements are not met.

console

Every package receives its own console object.

console.log("ready");
console.warn("slow operation");
console.error("operation failed");

Arguments are converted to strings and joined with spaces. Entries are tagged with the package ID and routed to both Fumoware’s debug log and the JavaScript console window.

Methods

interface Console {
  log(...values: unknown[]): void;
  warn(...values: unknown[]): void;
  error(...values: unknown[]): void;
}

fumo:console

The console module provides named functions and the shared package console object.

import {
  console,
  log,
  warn,
  error
} from "fumo:console";

log("package loaded");
warn("example warning");
error("example error");

The named functions behave identically to the matching global console methods.

fumo:events

fumo:events owns persistent callbacks. Callbacks execute on FumoEngine’s serialized script lane: createMove is pumped by the command hook and render is pumped by the view-render frame-start hook, never from Present.

import { events } from "fumo:events";

const unsubscribe = events.on("render", ({ displaySize, deltaTime }) => {
    console.log(displaySize.width, deltaTime);
});

events.once("mapLoad", ({ map }) => {
    console.log("Loaded", map);
});

events.on() and events.once() return an unsubscribe function. events.off accepts a numeric subscription ID for low-level integrations, although normal scripts should retain and call the returned function.

Implemented host events:

EventPayload
loadundefined
shutdownundefined
createMove{ displaySize, deltaTime }
render{ displaySize, deltaTime }
mapLoad{ map, value }
mapUnload{ map, value }
menuOpenChanged{ value: "true" | "false" }
configLoaded{ value }

frameStage, gameEvent, and input are reserved subscription names. Their native bridges are not emitted yet.

Each callback has an 8 ms hard execution deadline. Exceptions are written to the JavaScript console and do not unwind through the game hook.

fumo:draw

Drawing records native commands during render. The completed command list is immutable and remains visible until that script submits a newer frame, which avoids flicker when the JavaScript lane skips a Present.

import { events } from "fumo:events";
import { draw, Color } from "fumo:draw";

events.on("render", ({ displaySize }) => {
    const accent = Color.rgba(45, 170, 255, 220);
    draw.line(12, 12, displaySize.width - 12, 12, accent, 2);
    draw.rectFilled(24, 32, 140, 42, Color.rgba(10, 14, 20, 180), 5);
    draw.text(36, 44, "FumoEngine", Color.white, 14, true);
});

Implemented primitives:

line, rect, rectFilled
circle, circleFilled
text
polyline, polygon, polygonFilled
triangle, triangleFilled
rectGradient
pushClipRect, popClipRect
image

Point collections accept [[x, y], ...], {x, y} objects, or a flat coordinate array. draw.image accepts only a texture handle loaded by the same package.

draw.worldToScreen({x, y, z}) and draw.worldToScreen(x, y, z) return a copied {x, y} or null when projection is unavailable or behind the view.

Colors use packed 0xRRGGBBAA values. Color.rgba(r, g, b, a) creates one; Color.white and Color.black are predefined.

Draw calls outside a render callback throw. Per-frame command and point limits prevent one package from exhausting the renderer.

fumo:ui

Scripts register native controls at startup. Fumoware owns their widgets, values, RGB animation, and config serialization.

import { ui } from "fumo:ui";

ui.collapsible({
    id: "advanced",
    label: "Advanced",
    collapsed: false
});

const enabled = ui.checkbox({
    id: "enabled",
    label: "Enabled",
    default: true,
    group: "advanced"
});

const mode = ui.combo({
    id: "mode",
    label: "Mode",
    options: ["Quiet", "Detailed"],
    default: 0,
    group: "advanced",
    visibleWhen: { id: "enabled", value: true }
});

Controls:

  • checkbox, sliderFloat, sliderInt, and color
  • combo and multiSelect
  • hotkey and textInput
  • button with onClick
  • label and separator
  • collapsible

All controls expose a live value property. Common options are id, label, group, visibleWhen, and disabledWhen. A color may opt into native RGB cycling with rgbCycle: true.

Control IDs are namespaced by package and may contain letters, numbers, _, -, and .. Values are serialized as script_value/<package>/<control> config assignments. Before another config loads, controls reset to their declared defaults so state cannot leak between profiles.

fumo:game

fumo:game returns copied, immutable data. Safe scripts never receive entity, controller, scene-node, or bone pointers.

import { game } from "fumo:game";

const state = game.snapshot();
if (state.inGame) {
    for (const player of state.players) {
        console.log(player.id, player.name, player.health, player.origin);
    }
}

Methods:

  • game.snapshot() returns the full current snapshot.
  • game.players() returns the player array.
  • game.localPlayer() returns copied local team/origin data or null.

Player fields currently include:

id, generation, index, name, team, health, armor, distance,
enemy, visible, flashed, scoped, defusing, hasBomb, origin

The generation increments when the map or in-game state changes. An id combines that generation with the entity index, so a script can reject cached references after transitions.

The current player list is sourced from Fumoware’s validated visual snapshot. It contains players that reached that snapshot during the latest render data build; it is not a raw full entity-system iterator.

Commands

The createMove event contains a short-lived command:

import { events } from "fumo:events";

events.on("createMove", ({ command }) => {
    const angles = command.getViewAngles();
    if (angles) {
        command.setViewAngles({ ...angles, y: angles.y + 5 });
    }
});

Writing requires the input.write capability. Reads are allowed without it. Pitch, yaw, and roll are normalized and clamped natively.

The object expires immediately after that callback returns. It is already invalid in a Promise continuation, timer, another event callback, or a later command:

events.once("createMove", ({ command }) => {
    Promise.resolve().then(() => {
        console.log(command.isValid()); // false
    });
});

Copy values you need; never retain the command object.

fumo:trace

Trace calls require the trace capability.

import { trace } from "fumo:trace";

const from = { x: 0, y: 0, z: 64 };
const to = { x: 256, y: 0, z: 64 };

const ray = trace.ray({ from, to });
const hull = trace.hull({
    from,
    to,
    mins: { x: -16, y: -16, z: 0 },
    maxs: { x: 16, y: 16, z: 72 }
});

const visible = trace.visibility({ from, to });
const smoked = trace.throughSmoke({ from, to });

Ray and hull results are copied objects containing hit, world, startSolid, fraction, start, end, normal, and entityIndex. No engine pointer is exposed.

Penetration estimates accept copied weapon values:

const result = trace.penetration({
    from,
    to,
    damage: 40,
    penetration: 2,
    range: 8192,
    rangeModifier: 0.98,
    maximumPenetrations: 4
});

The runtime allows 128 regular trace/smoke calls and eight penetration estimates per owner-lane pump. Counters reset for the next pump.

Assets and sound

Declare package-relative assets in manifest.json. They are validated and decoded before the entry module evaluates, so first use does not stall a frame.

import { assets } from "fumo:assets";
import { sound } from "fumo:sound";

const settings = assets.loadJson("data/settings.json");
const text = assets.loadText("data/message.txt");
const marker = assets.loadTexture("images/marker.png");
const notify = sound.load("sounds/notify.wav");

Supported resources:

  • PNG, JPEG, WebP when a Windows WIC WebP codec is available, and BMP
  • PCM, float, and extensible WAV
  • JSON and text-like files

Textures produce stable package-owned handles. GPU views are created lazily, released on device loss, and recreated from the decoded cache. Sounds are played from cached memory with sound.play(handle).

Assets are size- and dimension-limited, canonicalized inside the package, and removed when the package unloads. A package cannot use another package’s handle.

fumo:http

HTTP requires the http capability and a manifest host allowlist:

{
  "capabilities": ["http"],
  "http": { "hosts": ["api.example.com"] }
}
import { http } from "fumo:http";

const response = await http.get("https://api.example.com/state", {
    timeoutMs: 5000,
    maximumBytes: 262144,
    accept: "application/json"
});

console.log(response.status, response.json);

request, get, and post return Promises. Supported methods are GET, POST, PUT, PATCH, and DELETE. Responses contain ok, status, body, contentType, and parsed json when applicable.

Safe mode enforces HTTPS. Hosts must match an exact declaration or a declared *.domain suffix. URLs with embedded credentials are rejected. Scripts cannot set authorization or cookie headers and WinHTTP does not reuse loader credentials or cookies.

Limits:

  • 256 KiB request body
  • 250 to 15,000 ms timeout
  • up to 4 MiB response
  • four concurrent and 1,024 lifetime requests per runtime

All pending requests are cancelled and joined when the script unloads.

Timers

Standard timer globals are installed in every runtime:

const timeout = setTimeout(() => console.log("once"), 250);
const interval = setInterval(() => console.log("tick"), 1000);

clearTimeout(timeout);
clearInterval(interval);

The equivalent module API is:

import { timers } from "fumo:timers";

const id = timers.setTimeout(callback, 250);
timers.clear(id);

Timers execute on the same owner lane as events and are checked at the next script pump. They do not create threads and do not enter QuickJS from Present. Delays are clamped to ten minutes, repeating timers have a minimum 1 ms delay, and one pump processes at most 64 due callbacks per package.

fumo:storage

Storage is a package-scoped JSON object:

import { storage } from "fumo:storage";

const position = storage.get("position", { x: 40, y: 60 });
storage.set("position", position);
storage.delete("old-value");

Methods return ordinary values, so using await is also valid.

Properties:

  • Keys contain 1–64 printable characters.
  • Values must be JSON-compatible.
  • The total package quota is 256 KiB.
  • Data is isolated under %APPDATA%\FumowareV2\script-data\<package-id>\storage.json.
  • set, delete, and clear only mutate memory during play.
  • Dirty storage flushes through an atomic replace during script reload, disable, or Fumoware unload.

No arbitrary path is accepted and safe storage never exposes filesystem handles.

fumo:ffi

fumo:ffi provides intentionally unsafe Win64 native access.

It is available only when:

  1. Settings → Script Engine → Insecure scripting is enabled.
  2. The package manifest includes the ffi capability.
  3. The package is reloaded after the setting changes.
import { ffi, NativeType } from "fumo:ffi";

Native types

const NativeType = {
  void: "void",
  bool: "bool",
  int8: "int8",
  uint8: "uint8",
  int16: "int16",
  uint16: "uint16",
  int32: "int32",
  uint32: "uint32",
  int64: "int64",
  uint64: "uint64",
  pointer: "pointer",
  cstring: "cstring",
  float32: "float32",
  float64: "float64"
};

float32 and float64 are currently supported by read and write, not by bound function signatures. Bound calls accept at most eight integer, pointer, or C-string arguments.

Loading and binding

const kernel32 = ffi.openModule("kernel32.dll");

const getCurrentProcessId = kernel32.bind("GetCurrentProcessId", {
  abi: "win64",
  returns: NativeType.uint32,
  args: []
});

console.log("PID", getCurrentProcessId());

ffi.module(name) is an alias for ffi.openModule(name). Existing modules are reused through GetModuleHandle; otherwise the engine calls LoadLibrary.

An opened module has:

interface NativeModule {
  readonly name: string;
  readonly base: bigint;
  bind(name: string, signature?: NativeSignature): NativeFunction;
}

interface NativeSignature {
  abi?: "win64";
  returns?: NativeType;
  args?: NativeType[];
}

Pointer and 64-bit return values are JavaScript bigint values. Native calls are surrounded by a Windows exception boundary, but not every native failure is recoverable.

Allocating memory

const block = ffi.alloc(64);
try {
  ffi.write(block, NativeType.uint32, 0x12345678);
  console.log(ffi.read(block, NativeType.uint32));
} finally {
  ffi.free(block);
}

alloc uses read/write VirtualAlloc pages. Package-owned allocations are released automatically when the package unloads. free accepts only an address returned by that package’s ffi.alloc.

Reading and writing memory

ffi.read(address: bigint, type: NativeType, maxLength?: number): unknown;
ffi.write(address: bigint, type: NativeType, value: unknown): void;

read and write accept arbitrary process addresses. cstring reads stop at the first null byte and default to a 4 KiB maximum. The optional maximum is clamped to 1 MiB. C-string writes are not currently supported.

Invalid memory accesses normally throw a JavaScript error containing the Windows exception code.

Warning

Signatures are not verified. Calling an address with the wrong argument or return types can corrupt registers, the stack, heap state, or game state. Only enable insecure mode for code you fully trust.

TypeScript declarations

The maintained declaration file is types/fumo.d.ts. Add its path to your editor project for autocomplete while keeping runtime source as JavaScript modules.

Example jsconfig.json:

{
  "compilerOptions": {
    "checkJs": true,
    "target": "ES2023"
  },
  "include": [
    "./**/*.js",
    "C:/path/to/fumoengine/types/fumo.d.ts"
  ]
}

Starter packages and conformance examples live in:

fumoengine/templates/

See the template catalog for the generated complete list. Copy a starter into %APPDATA%\FumowareV2\scripts, assign a unique package ID, and press R in the Scripts workspace. It is discovered in the off state until you explicitly enable it.

Runtime behavior

Each package runs in a separate QuickJS-NG runtime and context.

Safe mode

Safe mode applies:

  • 32 MiB QuickJS heap limit per package
  • 512 KiB QuickJS stack limit per package
  • 50 ms startup execution deadline
  • 10 ms shutdown execution deadline
  • Blocking operations disabled
  • fumo:ffi unavailable

Insecure mode

Insecure mode changes runtime creation:

  • No QuickJS heap limit is installed
  • No QuickJS maximum stack size is installed
  • Blocking operations are allowed
  • fumo:ffi may load for packages requesting the capability

Execution deadlines and module source-size validation remain active. The setting causes packages to reload so existing runtimes cannot retain the previous security profile.

Module evaluation

Entry files are compiled as ES modules. Top-level promises are pumped during startup and must settle before the startup budget expires. An exported shutdown function is called when the package is disabled, reloaded, or the engine stops.

Unhandled promise rejections are written to the package console.

Enabled state

Newly discovered packages start disabled. The Scripts workspace writes every toggle immediately to:

%APPDATA%\FumowareV2\script-states.cfg

This state is independent of Fumoware configs and is restored on the next injection. It is flushed again during a normal engine shutdown.

Host object lifetime

Game data and trace results are copied values. A UserCommand is valid only while its own createMove callback is running. It expires before Promise jobs or microtasks execute, so retaining one cannot mutate a later command.

Async work

HTTP runs on cancellable native workers and settles Promises on the script owner lane. Unloading a package cancels and joins its requests before destroying QuickJS.

Security model

Safe mode and insecure mode have different trust assumptions.

Safe mode

Safe mode is designed for host-provided APIs that exchange copied values, opaque handles, and bounded resources. It does not expose raw native addresses.

Package isolation prevents ordinary JavaScript objects and globals from being shared across runtimes. It does not turn an injected module into an operating-system security boundary.

Insecure mode

Insecure mode trusts the complete package, including every relative module it imports. FFI can:

  • Read or overwrite process memory
  • Load DLLs
  • Invoke exported native functions
  • Block the game thread
  • Bypass future safe host-API validation
  • Crash the process despite exception containment

The global toggle is persisted in Fumoware configs. Loading a config without the setting returns the engine to safe mode, preventing insecure state from leaking between configs.

Do not enable insecure mode for downloaded or obfuscated packages.