#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
shortidentifier; - 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:
-
detects the project's real client working directory and
dev.htmlorindex.htmlentry; -
restores its locked JavaScript dependencies in the worker when
node_modulesis absent; -
assigns a collision-free worker-internal development port from
5200through5499; - starts that project's existing development profile in the worker;
-
injects
KS_APP_CONTENT_DEV_OVERRIDESinto the local UserApp host before it starts; and -
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:
{
"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/dist → dist/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:
/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:
- Create or select a saved session.
- Choose enabled users from the persistent library.
- Assign exactly one launchable app to each selected user.
- 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/stateURL, 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:
{
"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.