Skip to content

Launcher integration ​

@opys/runtime is the part of opys your players need. Give it a bundle and it installs the game and starts it.

It runs anywhere Node.js 20+ runs: Electron, the Node side of a Tauri app, a script.

sh
npm install @opys/runtime

The whole thing ​

js
import { launch } from '@opys/runtime';

const child = await launch(
  { url: 'https://example.com/packs/game.opys' },
  {
    vars: {
      root: '/home/player/.local/share/my-pack',
      username: 'Player',
      uuid: '00000000-0000-0000-0000-000000000001',
      token: '0',
    },
  },
);

child.on('exit', (code) => console.log('game exited with', code));

That downloads the bundle, installs or updates the game, and starts it. You get back a normal Node ChildProcess.

Two arguments, two owners. The bundle is what the pack author decided. vars is what only this machine knows. Always pass root, as an absolute path. All names: Variables.

Why a separate package: the runtime knows nothing about Forge or Modrinth. All of that was settled when the bundle was built, so your launcher ships something small.

Where the bundle comes from ​

SourceUse it when
{ url }The pack is on a server. Simplest.
{ bundle }You downloaded it yourself. Pass an absolute path.
{ manifest }A manifest in memory, with no carried files.

{ url } does not download the whole bundle. It reads the bundle's directory and manifest, a few hundred kilobytes, and then only the carried files the installation is missing or has changed. A launch with nothing new costs those few hundred kilobytes, however large the pack. The server has to answer range requests and send an ETag or Last-Modified; one that does not gets the bundle downloaded whole, every call.

Showing progress ​

js
await launch(source, {
  vars,
  install: {
    onProgress(p) {
      if (p.phase === 'download') {
        bar.set(p.totalBytes ? p.bytes / p.totalBytes : p.fetched / p.total);
      }
    },
  },
});

An install goes through phases, in order. p.phase tells you which:

phaseMeansExtra fields
resolveStarted.
downloadOverall download position.fetched, total, bytes, totalBytes
download:startOne file started.path, totalBytes
download:bytesOne file moved forward.path, bytes
download:doneOne file finished.path
verifyChecking hashes.
extractUnpacking archives.count
cleanupOld files were deleted.removed, directories

Files already on disk with the right hash are skipped. So a second install is fast, and an interrupted one picks up where it stopped.

Handling errors ​

js
import { install, NetworkError, RuntimeError } from '@opys/runtime';

try {
  await install(source, { vars });
} catch (err) {
  if (err instanceof NetworkError) showRetry(err.url, err.status);
  else if (err instanceof RuntimeError) showError(err.code, err.message);
  else throw err;
}

Every failure has a code. Decide from the code, never from the message text.

codeMeansDo
networkA download failed, after retries.Offer to retry.
integrityA file does not match its hash.Report it. The pack needs a rebuild.
extractionAn archive could not be unpacked.Check disk space and permissions.
manifestThe bundle is unreadable or unsafe.Report it to the pack author.
ioThe file system refused something.Show the message.
otherAnything else.Show the message.

An install cannot be cancelled from JavaScript yet.

Doing the steps yourself ​

launch is the short way. The pieces are there when you need control:

FunctionDoes
install(source, o)Installs. Starts nothing.
prepare(source, o)Installs, and returns what to start.
buildLaunch(source, o)Returns what to start. Installs nothing.
spawnLaunch(spec)Starts it.

"What to start" is { command, args, workdir, envs }.

Use case: capture the game's log. launch hands the game your terminal. To read its output instead, spawn it yourself:

js
import { spawn } from 'node:child_process';
import { prepare } from '@opys/runtime';

const spec = await prepare(source, { vars });
const child = spawn(spec.command, spec.args, {
  cwd: spec.workdir,
  env: { ...process.env, ...spec.envs },
  stdio: ['ignore', 'pipe', 'pipe'],
});

Options ​

OptionWhereMeans
varseverywhereValues for the bundle's variables.
featureseverywhereSwitches such as java_console.
platformeverywhereInstall for another OS or CPU. Default: this one.
cwdlaunch, prepareOverrides the working directory.
installlaunch, prepareInstall options, or false to skip installing.
onProgressinstallThe progress callback.
concurrencyinstallParallel downloads. Default 8.
verifyIntegrityinstallfalse skips hash checks. Leave it on.