v0.5.0

Provides script-level access to the virtual filesystem and handles ES module resolution for both JS and native modules.

Import #

import * as Assets from 'Syncromesh/Assets';

Functions #

getText
Returns the file contents as a UTF-8 string.
  • Path is resolved by the internal asset system (e.g. /rom/...).
  • Throws if the asset does not exist.
getText(path: string): Promise<string>
getBinary
Returns the file contents as raw bytes (0-255 values).
  • Path is resolved by the internal asset system (e.g. /rom/...).
  • Returns an empty array if the asset does not exist or is empty.
getBinary(path: string): Promise<number[]>
readDir
Returns all file paths under the provided virtual directory path.
  • The lookup is recursive for nested subdirectories.
  • Useful for discovering available assets at runtime.
readDir(path: string): Promise<string[]>
mount
Mounts a host directory or ZIP archive while the runtime is running.
  • The destination defaults to /rom.
  • Relative sources use the process working directory.
  • The Promise rejects when the source or virtual destination is invalid.
mount(source: string, destination?: string): Promise<boolean>
mountAdjacent
Mounts a data pack relative to the Syncromesh executable.
  • The source must be relative and cannot contain parent traversal.
  • Use it from the embedded bootstrap to bring in game data packs distributed beside the runtime.
mountAdjacent(source: string, destination?: string): Promise<boolean>

Mount sources and packaged assets #

Syncromesh automatically mounts an unmodified ZIP appended to its own executable at /rom. That archive can contain only a minimal index.js orchestrator which mounts game data packs distributed beside the executable. The -m option remains available for explicit development or override mounts.

# /rom/index.js is embedded; game-core.zip remains replaceable
./my-game
  • ZIP64 archives, empty files, and long member names are supported.
  • Absolute and parent-traversal archive members are rejected rather than exposed in the virtual filesystem.
  • Native hosts can mount a precise ZIP byte range with Helix::Assets::mountArchiveRange, including when the containing file has trailing data.
  • Later mounts take precedence when multiple sources provide the same virtual path.

Packaging a Syncromesh application #

Embed a small application archive containing /rom/index.js into the Syncromesh executable and distribute larger game content as sidecar ZIPs. This keeps the launcher cohesive while allowing maps, episodes, server data, or patches to be replaced independently.

source/
├── application/
│   └── index.js
└── packs/
    ├── game-core/
    │   ├── maps/
    │   ├── scripts/
    │   └── textures/
    └── episode-1/
        ├── maps/
        └── scripts/

Minimal embedded bootstrap #

// application/index.js
import * as Assets from 'Syncromesh/Assets';

export default async function main(args)
{
    await Assets.mountAdjacent('packs/game-core.zip', '/game');
    await Assets.mountAdjacent('packs/episode-1.zip', '/game');

    // Imports from a data pack must be dynamic because mounting happens at runtime.
    const game = await import('/game/scripts/main.js');
    await game.start(args);
}

Build the Linux distribution #

set -eu

GAME_ROOT=/path/to/source
SYNCROMESH_RUNTIME=/path/to/syncromesh
DIST=/path/to/dist/MyGame
PACKAGE_TMP=$(mktemp -d)
APP_ZIP=$PACKAGE_TMP/application.zip

mkdir -p "$DIST/packs"

(
  cd "$GAME_ROOT/application"
  zip -q -r "$APP_ZIP" .
)
install -m755 "$SYNCROMESH_RUNTIME" "$DIST/my-game"
dd if="$APP_ZIP" of="$DIST/my-game" oflag=append conv=notrunc status=none

(
  cd "$GAME_ROOT/packs/game-core"
  zip -q -r "$DIST/packs/game-core.zip" .
)
(
  cd "$GAME_ROOT/packs/episode-1"
  zip -q -r "$DIST/packs/episode-1.zip" .
)

rm -rf "$PACKAGE_TMP"

Build the Windows executable #

Create application.zip with index.js at its root, then append it to the completed Windows runtime from Command Prompt. Assemble the combined executable before applying any Authenticode signature.

mkdir MyGame\packs
copy /b syncromesh.exe+application.zip MyGame\my-game.exe
copy game-core.zip MyGame\packs\game-core.zip
copy episode-1.zip MyGame\packs\episode-1.zip
MyGame/
├── my-game                  # or my-game.exe on Windows
├── libhx-game.so            # optional Linux native module
├── libhx-game.dll           # optional Windows native module
└── packs/
    ├── game-core.zip
    └── episode-1.zip
  • The application ZIP root maps to /rom; the default bootstrap must therefore be named index.js at the ZIP root.
  • Each data-pack ZIP root maps to the destination supplied to mountAdjacent.
  • Use dynamic import() for modules provided by packs mounted during bootstrap; static imports are resolved before the bootstrap function runs.
  • Native modules must remain filesystem files beside the executable or in an explicit -n directory.
  • Test the final directory from an unrelated working directory on every target platform.

Module resolution #

When a JS import references a path, the module loader resolves it against the virtual filesystem. Files ending in .js or .json are evaluated as ES modules.

Imports starting with Module/ are resolved as native shared libraries. The loader searches for libhx-<name>.so in the -n directory (if specified), then falls back to the executable's directory. The native library must export an helix_plugin_integrate(ctx, name, host, instance) entry point.

Examples #

import * as Assets from 'Syncromesh/Assets';

// Mount replaceable game data distributed beside the executable.
await Assets.mountAdjacent('packs/game-core.zip', '/game');
await Assets.mountAdjacent('packs/episode-1.zip', '/game');

// Later packs override earlier files at the same virtual path.
const mapJson = await Assets.getText('/game/maps/overworld.json');
const map = JSON.parse(mapJson);
const files = await Assets.readDir('/game/maps');

// Import a native module
import * as WS from 'Module/ws';