#Project registration

Supported sources are local folders, hosted mounts, Git repositories, built artifacts, and generated templates. Machine-specific absolute paths stay in the database. Registration resolves symlinks and junctions and rejects duplicate or nested registrations. Hosted mode additionally enforces STUDIO_ALLOWED_PROJECT_ROOTS; local mode accepts any real directory.

#GitLab and Git repository sources

Projects and the required GEngine source can be registered from an HTTPS or SSH Git URL. This supports self-hosted GitLab without depending on GitLab.com APIs. In hosted mode, checkouts are created below the first configured STUDIO_ALLOWED_PROJECT_ROOTS entry.

Do not embed deploy tokens, personal access tokens, usernames, or passwords in repository URLs. Studio rejects credential-bearing HTTPS URLs so secrets cannot reach settings, audit records, logs, or client bundles. Configure an SSH deploy key and known_hosts for both the Studio web and worker service accounts, or use a public HTTPS repository. Git runs with interactive prompts disabled.

The Projects table exposes Check update for every Git checkout. Studio compares the local commit with the remote branch without changing the checkout. If an update exists, a modal shows the old and new commits and asks for confirmation. Updates are fast-forward only; local changes or divergent history stop the operation. The worker builds a project server artifact when one is configured, then restarts active managed GEngine/project runtimes. Opening Studio also checks every registered Git source and lists available updates in one modal.

The first-run wizard accepts a folder or Git repository for GEngine and each optional game project. Runtime environment settings can clone and activate a replacement GEngine later.

The optional gengine.project.json is versioned and validated. Projects without it use evidence-based detection with confidence, warnings, and operator overrides. GEngine and game projects such as CandyCastle or SnackPot are registered source projects. The local UserApp test environment is bundled Studio infrastructure under packages/userapp-host and does not need a separate checkout or project entry. Studio never writes into a read-only source.

The top-bar Project context selector scopes project-aware views. All projects keeps portfolio views unfiltered. Selecting one registered project narrows applicable lists and preselects it where possible; it does not alter permissions or source access.

#The three runtime layers

#1. GEngine source

GEngine is a Knuddels UserApp codebase. It is loaded by a compatible UserApp host and does not provide its own Vite or HTTP development server. Its menu and games are opened by server-side AppContent calls.

Studio reads the protected GEngine source to discover:

  • the menu AppContent entry (mods/menu2/game.html);
  • loaded mod managers;
  • each mod's short identifier;
  • its declared uiInfo.htmlPath;
  • supported chat commands and event evidence.

This inspection is read-only. A game is not assigned a guessed route such as /candy or /snackpot.

#2. Local UserApp host

The executable development environment is the UserApp host bundled with Studio at packages/userapp-host. Studio selects it automatically; users only maintain the Studio, GEngine, and game-project checkouts. An explicitly configured external host path takes precedence only as an advanced compatibility override.

On a fresh local runtime, Studio seeds the minimal GEngine persistence needed to activate Menu2. This makes the source-defined /menü command and mods/menu2/game.html AppContent session available without writing to the external GEngine checkout. Existing GEngine module configuration is preserved unchanged.

Its canonical development profile is Local UserApp runtime. The profile starts the server process, not the debug UI Vite process, and uses port 3100 for the loopback-only API. Port 3101 is kept separate for the host's own optional live-source frontend. The profile is audit-scoped to the GEngine project (or another available project on older installations), but its working directory always remains the discovered runtime host. Game source is never executed as a fallback host.

#2.1 Registered game development clients

A registered game such as CandyCastle or SnackPot may own a Vite client outside GEngine. When the selected GEngine mod declares an AppContent path and the registered project exposes an existing Vite dev command, Studio automatically:

  1. detects the project's real client working directory and dev.html or index.html entry;
  2. restores its locked JavaScript dependencies in the worker when node_modules is absent;
  3. assigns a collision-free worker-internal development port from 5200 through 5499;
  4. starts that project's existing development profile in the worker;
  5. injects KS_APP_CONTENT_DEV_OVERRIDES into the local UserApp host before it starts; and
  6. maps the exact (GEngine, AppContent asset path) pair to the Vite entry.

The generated host page includes the local Knuddels client shim, React Refresh, and the Vite HMR client. Asset-base globals ending in ASSET_BASE_URL are discovered from the app source and set to the selected Vite origin. This is source-driven integration, not a CandyCastle or SnackPot route hardcoded into Studio. Static AppContent files continue to work unchanged when no Vite client can be discovered.

In hosted mode, browser HTTP and WebSocket traffic uses the authenticated same-origin path /api/live/runtime/<port>/…. Traefik forwards that path to the worker's fixed runtime gateway, which strips the prefix and connects to the active internal process. Vite receives the same prefix as its base path so module imports, assets, React Refresh, and HMR remain inside the gateway. Runtime ports are never published on the Docker host.

#2.2 Registered game server modules

A game project may also declare a buildable GEngine server-module overlay in gengine.project.json:

codejson
{
  "gengineAdapter": {
    "serverModule": {
      "sourcePath": "src/server",
      "outputPath": "dist/mods",
      "targetPath": "mods",
      "buildCommand": {
        "executable": "npm",
        "args": ["run", "build:server"]
      }
    }
  }
}

For compatibility with existing mod repositories that do not yet ship this optional manifest, Studio also recognizes a project-level build:server script together with src/server/build/build-mods.js and the compiled src/server/dist tree. It infers the equivalent src/server/distdist/mods overlay targeting mods. Explicit manifest metadata always takes precedence over this convention.

All paths are relative to the registered project root. Studio resolves their canonical locations, fingerprints the source and output trees, and injects the output as a read-only module overlay into the GEngine sandbox. A request below targetPath resolves from the project output first and falls back to the protected GEngine source when the overlay does not contain the requested file. Studio never copies generated modules into GEngine.

Server changes are deliberately manual. Client HMR remains automatic, but Studio only reports one of these server states:

  • Build required: source and output fingerprints differ, or output is missing.
  • Reload required: output is current, but the running sandbox loaded an older fingerprint.
  • Ready: the running sandbox loaded the current output.
  • Invalid: manifest paths or generated output are unusable.

Starting a saved session is blocked until every selected project's server output is current. The Projects workspace can build an inactive project's output. An active Session offers Preserve runtime to build and reload GEngine in the existing host process, or Restart session to build and restart the dedicated host. Preserve mode keeps runtime users and persisted storage, but changed overlay configuration requires one restart before in-place reload is available.

#3. AppContent sessions

The local host creates the real browser sessions. Studio reads /api/state, finds the AppContent session for a generated runtime user, and embeds the exact URL exposed by the host:

codetext
/app/<runtime-app-id>/<asset-path>?sessionId=<session-id>&userId=<runtime-user-id>

The iframe URL therefore comes from the live runtime state. Studio does not synthesize paths from project names and does not parse a Vite HTML fallback as application JSON.

#Persistent users

#Per-session GEngine settings

Each saved session starts its own local UserApp host process and receives a separate GEngine persistence directory. Settings → GEngine selects a running saved session, discovers the available modules from the configured external GEngine source, and shows each module's persisted enabled state. Changing a module updates only that session's app:modList and app:mod_state_<module> slots; it never edits the external GEngine checkout or another session's storage. Restart the selected session runtime after changing modules so GEngine can apply its module lifecycle with the new configuration.

The Users workspace is the source of truth for reusable test identities. A profile contains:

  • a unique editable display name;
  • one type: Owner, Manager, Player, or Bot;
  • a non-negative Knuddels balance; and
  • an enabled or disabled status.

The profile is stored independently from the local UserApp process. A runtime user ID is a short-lived synchronization detail and is refreshed when a session starts. Editing an identity updates the remote user when it is currently present. Disabling or deleting an identity removes it from saved active assignments and from the live host when possible.

Type mapping is source-driven through the local host API: Owner receives channel-owner and manager flags, Manager receives app-manager capability, Player is the default human, and Bot is an AppBot/service identity.

The CandyCastle Background Bots plugin is configured globally in Settings → GEngine. When enabled, native CandyCastle bots join a signup one at a time after the configured real-player threshold. Studio leaves the configured number of human seats open (three by default), and these native bots never create persistent Studio users, AppContent sessions, or visual tiles. Restart an active session runtime after changing the plugin settings. If CandyCastle AppContent clients are already open when /candy start creates a signup, the local host immediately refreshes each unregistered client with the authoritative signup snapshot so its countdown starts. Those clients then receive roster updates as spectators until they register; registered players continue through CandyCastle's normal player broadcast and do not receive duplicate updates.

#Saved Sessions workflow

The Sessions workspace stores reusable multiplayer scenarios rather than creating users implicitly:

  1. Create or select a saved session.
  2. Choose enabled users from the persistent library.
  3. Assign exactly one launchable app to each selected user.
  4. Start the session.

Studio then starts the selected projects' development clients when needed and starts or reuses the selected session's dedicated loopback UserApp/GEngine host. Each saved session receives its own runtime process and port, so users, commands, and app state cannot leak into another test scenario. Studio provisions each selected identity, joins it to GEngine, and opens the assigned GEngine menu or game. Human users receive exact AppContent iframe URLs from /api/state; Bot users remain connected service identities and do not create visual tiles.

The host also receives the selected projects' current server-module overlays. Its Live Source watcher observes server source only to update status; it never runs the project's backend build or reloads GEngine automatically.

The grid renders every human client at once. Its toolbar changes column count and base row height without reopening a dialog. Grid settings controls columns, visible rows, row height, spacing, dense placement, compact headers, and iframe scaling. Drag tile headers to reorder clients and drag the lower-right handle to resize a tile across columns or rows. Collapse the left Sessions rail to an icon strip, collapse Assigned users to its heading, or collapse Session chat to its icon strip when the clients need more horizontal or vertical space. These controls hide workspace chrome without unmounting AppContent clients. On narrow screens Studio expands the side panels again so their controls remain accessible. Studio stores the versioned order, sizing, and grid preferences per session in the current browser. Reset layout restores the compact three-column default. The Sessions workspace remains mounted after first use, so client-side navigation hides it without destroying iframe documents, WebSocket connections, or in-memory game state.

Each tile header can reload the client, open it in a new tab, or explicitly close that exact AppContent session. Closing preserves the assigned user and app; the Assigned users row then offers Open to recreate it on demand. Each tile also has an event/contract trace toggle. The trace overlays the right side of the game at full tile height, records time, sender (server or client), event type, and JSON data, and provides adjustable opacity plus a draggable width.

Stopping a session removes its live users and AppContent clients but preserves the saved user selection and app assignments for the next run. A full browser reload, explicit app switch, user removal, or host restart can still recreate affected AppContent clients.

For an active session, Restart session asks for confirmation before restarting its dedicated GEngine host. While the workspace remains open, Studio retries the saved users' AppContent connections. Before opening a client, synchronization verifies the GEngine source, the dedicated UserApp host process and API, and the project's development client. Hosted client probes retain the runtime gateway base path advertised to browsers. When a dependency is missing, Studio shows one recovery dialog listing the affected services and offers to start them. Use restart when the host appears stale. It cannot fix a rejected or failing GEngine launch command: Studio now shows that command error instead of leaving the client indefinitely in a waiting state. Inspect the session logs and the GEngine mod's UI-access rule when a launch error is reported.

#Chat, commands, and logs

Public, private, action, and slash-command input is delivered through the local UserApp debug API using the selected session identity. The command console lets you choose that session runtime explicitly and offers commands discovered from every loaded GEngine module (for example /candy show), even if the selected user's visible AppContent is the menu. Private messages require a different target user. Commands refuse execution when the host is stopped or the selected user is not connected.

When a loaded module declares chatCommandsInfo, the selected command shows a compact usage guide in the command modal: required access, parameter names, their separator, and the command description. Studio reads only display metadata from source and never executes project code to render the guide; dynamic description values therefore remain unavailable until the command itself runs.

The session Logs panel combines bounded worker-captured stdout/stderr with live UserApp module logs. Tabs are generated from actual sources: UserApp host, registered app process, and each runtime appId. Nothing is hardcoded to CandyCastle or SnackPot. Process output is persisted in bounded execution metadata so startup failures remain inspectable after a process exits.

#Runtime settings and diagnostics

Settings → Runtime owns the authoritative gengine.protectedPath. An optional advanced runtime.userAppHostPath override is available when sibling discovery is not suitable. The diagnostics view shows:

  • configured and resolved GEngine paths;
  • registered-project path match;
  • discovered UserApp host path and evidence;
  • canonical profile command, working directory, state, PID, and exit reason;
  • exact /api/state URL, HTTP status, content type, JSON validity, and response preview; and
  • the latest runtime start/restart/stop job error.

Start, Restart, and Stop always target the profile named Local UserApp runtime. Runtime execution and pending-job deduplication are profile-scoped, so a Vite client in the same registered project cannot be reused accidentally as the host. A response beginning with <!doctype> is reported as a wrong-port/wrong-process diagnosis instead of a generic JSON parse error.

#Project-owned contracts

Contracts are app-specific and are not a Studio-maintained CandyCastle or SnackPot list. The Contracts workspace scans the selected registered source, or the protected GEngine source, for event evidence such as sendEvent, Client.sendEvent, event listeners, comparisons, and switch cases. Results are grouped by inferred namespace and include the source file, line, direction evidence, and occurrence count.

Source discovery identifies event names and direction evidence; it cannot infer a strict payload schema from arbitrary JavaScript. Projects that require schema validation should own versioned metadata in their own gengine.project.json. Studio accepts events or contracts as either a keyed object or an array:

codejson
{
  "contractMetadata": {
    "namespace": "cc",
    "events": {
      "cc_matchInit": {
        "direction": "server-to-client",
        "description": "Initial multiplayer match state.",
        "exampleFile": "src/client/contract-examples/cc_matchInit.json",
        "schema": {
          "type": "object",
          "additionalProperties": true
        }
      }
    }
  }
}

Each event may use name, event, or type; direction supports server-to-client, client-to-server, and bidirectional. Examples may be inline through examplePayload, example, or payload, or referenced through exampleFile / payloadFile. Referenced files must stay inside the project root. Studio also discovers project-owned JSON files in contract, payload, or example folders and merges them with source evidence. The app source remains authoritative; Studio does not maintain or generate a game-specific catalog.

#Process and terminal boundaries

Runtime profiles store executable and argument arrays, working directory, environment references, health and port policy, timeouts, output limits, restart policy, resources, network policy, and write policy. The worker receives an environment allowlist and stops Unix process groups or Windows process trees.

The runtime gateway listens only on the Compose network. Traefik authenticates HTTP requests and WebSocket upgrades through the web app before forwarding them. Access requires runtime.read and sessions.observe. The worker accepts a target port only while a matching runtime execution is in RUNNING state, and it limits UserApp targets to AppContent reads and iframe sockets. The gateway is not a general-purpose proxy to worker-local services.

Browser terminals require terminal.open and terminal.execute. Local bypass identities are reconciled to their persisted Studio user before terminal and audit rows are created. The web server checks project access and issues a signed worker claim. Read-only sources are refused. WebSocket origin, payload, rate, expiry, and disconnect limits are enforced, and open/close events are audited.

Compatibility redirects keep /sessions and /players pointing at /runtime. Internal GameSession and generated-player records remain implementation details for persistence, audit, and recovery; they are not separate operator workflows.