# Glue Live Edit

> Glue Live Edit

- Skill: `vchelaru/glue-live-edit` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vchelaru/glue-live-edit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vchelaru/glue-live-edit/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: vchelaru (https://skillmd.com/u/vchelaru)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/vchelaru/glue-live-edit

---


# Glue Live Edit

Lets a user run a game launched by Glue, switch it into edit mode, and move/resize/tweak objects live. Edits push back to Glue for persistence + codegen. This is FRB1-only — not being redesigned for FRB2, so treat its foundation as fixed rather than something to refactor.

## Architecture

Two processes talk over **two raw TCP sockets on loopback** (`GameConnectionManager`, one per direction), port from `CompilerSettings.json` (random 8000-8999 per project). Messages are plain text: `"{DtoTypeName}:{json payload}"`.

- **Glue → game**: `CommandSender.Self.Send(dto)` (`FRBDK\Glue\GameCommunicationPlugin\GlueControl\CommandSending\CommandSender.cs`) serializes a DTO and sends it. Game's `CommandReceiver.Receive` splits on the first `:`, reflects over its own `HandleDto` overloads to find one whose single parameter type name matches, deserializes, and dispatches.
- **Game → Glue** (e.g. drag/resize results): the game enqueues onto `GlueControlManager.GameToGlueCommands` (a `ConcurrentQueue`); `GlueControlManager`'s socket loop drains it and writes to the `gameToGlueSocket`.
- `RefreshManager` (Glue side) is the hub that decides, for every kind of Glue-side change (new object, renamed element, variable edit, state created, file changed...), whether to push an incremental command or fall back to `CreateStopAndRestartTask` (kill + rebuild + relaunch the game) when a live update isn't supported.
- `VariableSendingManager` turns a Glue property-grid change into `GlueVariableSetData` DTOs (handles `X/Y/Z` → `RelativeX/Y/Z` when attached, collision relationships, tile shape collections, states, etc.) before `CommandSender` ships them.

The `"{DtoTypeName}:{json}"` string above is the *inner* payload. It's wrapped in a JSON `Packet {PacketType, Payload}` (PacketType `"OldDTO"`) and carried by the actual transport, `GameJsonCommunicationPlugin.Common.GameConnectionManager` (`Common\GameConnectionManager.cs`, Glue side) ↔ its embedded twin (`Embedded\GameConnectionManager.cs`, game side). `CommandSender.SendPacketInternal` builds the `Packet`; each direction is a length-prefixed (8-byte size, then ASCII body) blocking send. Connection handshake: game connects two sockets and sends one identifying byte each — `1` = glueToGame, `2` = gameToGlue; Glue's listener `Accept`s exactly two and routes by that byte.

## Connection lifecycle & self-heal (landmine)

Each side runs a 100ms `StatusCheck` that reconnects (game side) / re-listens (Glue side) whenever the connection is marked dead — but **the dead-marking is the bug surface**. Both sides track health with a single `_isConnected`/`IsConnected` flag that historically was cleared in **only one place: the `finally` of the game→Glue *receive* loop.** A failure on the *send* direction (`glueToGameSocket.Send` throwing `SocketException` 10054 "forcibly closed") did **not** clear the flag, so `StatusCheck` never re-listened and every subsequent send hit the same dead socket forever — the classic "live edit connected once, now every Play/Edit switch fails and never recovers." The trigger is usually the *other* process's reconnect: when the game's receive loop throws, its `StartConnecting` **disposes both sockets** before reconnecting, and that dispose is the 10054 the still-"connected" Glue side sees.

Fix pattern (already applied in both `GameConnectionManager.cs` files): any send-path socket failure calls a `ResetConnection(reason)` that disposes **both** sockets and clears the flag, letting the existing `StatusCheck` re-handshake. Must be symmetric — reset both directions together, because the handshake is order-dependent (byte 1 then byte 2, two accepts); a half-reset desyncs. Diagnostics: the Glue side logs connect/disconnect/reset transitions via `PluginManager.CallPluginMethod("Compiler Plugin", "HandleOutput", ...)` (game side only reaches `Debug.WriteLine`, since its output can't cross the broken socket).

**Never log or raise a plugin event while holding `_lock` (Glue side).** That log reaches `BuildTabView.PrintOutput`, and the plugin event reaches arbitrary plugin code — both can end up waiting on the editor's UI thread, while `StatusCheck` takes the same `_lock` every 100ms. Background thread holds the lock and waits on the UI thread; UI thread waits on the lock. Glue then freezes with **no CPU use, no memory growth, nothing in the output, and a dead close button** — indistinguishable from a plain hang, which is why it is expensive to find. Mutate state under the lock, collect the notifications, invoke them after releasing. `StatusCheckTask`'s `Task.Delay` also needs `ConfigureAwait(false)`: without it the loop resumes on the captured WinForms context and takes the lock on the UI thread in the first place.

**The two receive directions need opposite timeout semantics, and one shared `Receive` helper cannot serve both.** The request/response read (`SendItemImmediately` waiting for the game's reply) *must* time out, or a game that died mid-request leaves the caller awaiting forever. The inbound game→Glue loop must *not* — that socket is idle whenever the user is not interacting with the game, so a timeout there tears down a healthy connection and re-handshakes on a fixed period forever, which then drives repeated embedded-game window repositioning (`Runner_MoveWindow`, `GameHostView.ForceRefreshGameArea`'s deliberate ±1 panel jiggle). If you touch the receive path, check which caller you are changing.

Suspecting a hard-to-repro live-edit bug rather than a socket problem? `EmbeddedDiagnosticsLogger` (game-side, `Embedded\Editing\EditingManager.cs`) always runs - no toggle, so a bug that already happened doesn't need logging predicted in advance. It keeps a capped in-memory buffer (`Capacity`, currently 2000 entries) of every DTO the game receives and its response, click attempts, and selection changes. The Build tab's **View Diagnostics Log** button fetches the current buffer via `GetEmbeddedDiagnosticsLogDto`/`CommandReceiver.HandleDto`, writes it to a fresh timestamped file under `%LOCALAPPDATA%\FlatRedBall\Glue\Diagnostics\`, and opens it - a snapshot per click, not a continuously-appended file, so it only works while the game is still connected.

## Embedded → Generated: how "the editor injects code" works

`FRBDK\Glue\GameCommunicationPlugin\GlueControl\Embedded\**\*.cs` are the **master source templates** for the entire runtime live-edit system (command receiver, DTOs, editing manager, variable assignment, models, etc.) — hand-edit these, never the copies.

`EmbeddedCodeManager.EmbedAll()` (`CodeGeneration\EmbeddedCodeManager.cs`) copies each listed file into the game project under `GlueControl/`, converting `Editing.Managers.GlueCommands.cs` → `Editing/Managers/GlueCommands.Generated.cs` (dots become path separators, `.Generated` suffix added). This is what runs on Glux load / whenever live-edit settings change (`HandleGluxLoaded`, `HandlePortOrGenerateCheckedChanged` in `MainCompilerPlugin.cs`). The game project therefore contains a full mirrored copy of the DTOs and runtime logic — there is no shared assembly between Glue and the game.

**Landmine: every file in `EmbeddedCodeManager.filesToSave` is `<Compile Remove>`d from `GameCommunicationPlugin.csproj`.** They only compile inside a game project, so a clean build of `Glue with All.sln` proves nothing about them and a typo ships to every live-edit user. Only a game-project build — the BuildSmoke tests that run `EmbedAll` and compile the output, or launching a sample into edit mode — verifies them.

## Gotchas

- **A reply's `Succeeded` is the only readiness signal; its payload is not.** Most `HandleDto` overloads
  return `void`, so a healthy game answers them with an empty body — "did it send anything back?" is not a
  test for whether the command landed. The game marks a command it cannot dispatch yet (`GlueControlManager.Self`
  still null during `Game1.Initialize`) with `GameConnectionManager.NotReadyPayload`, which
  `SendItemWithResponse` turns into an unsuccessful `GeneralResponse`. `CommandSender.Send<T>` additionally
  fails an empty reply, since a typed caller asked for data.
- **The debug/edit-mode hook is `CustomActivityEditMode()`, not "CustomDebugActivity."** `ScreenManager` calls `Screen.ActivityEditMode()` (virtual, `Engines\FlatRedBallXNA\FlatRedBall\Screens\Screen.cs`) instead of normal `Activity` while `ScreenManager.IsInEditMode` is true. Per-element codegen (`CodeWriter.GenerateActivityEditMode`, `FRBDK\Glue\Glue\CodeGeneration\CodeWriter.cs:1422`) calls each named object's own `ActivityEditMode()` and then `CustomActivityEditMode()` — an empty `partial void` users can implement in their hand-written partial class, following the normal generated/custom partial-class split.
- **Variable edits during live play are an overlay, not real codegen.** Since the game can't reload generated code while running, edited values are applied through `GlueControl.Editing.VariableAssignmentLogic.SetVariable` (`Embedded\Editing\VariableAssignmentLogic.cs`) — a large, manually-maintained switch over variable name/type/target-instance-kind (collision relationships, tile shape collections, states, lists, `AttachToContainer`, etc.) that reflects/`screen.ApplyVariable`s the value onto the live instance. Any variable kind not special-cased here either falls through to generic reflection (works for simple properties) or silently fails to apply — this is the brittleness the user should expect: a new variable type showing up correctly in Glue but not visually updating live almost always means this file needs a new case, not that the DTO plumbing is broken. Actual codegen only happens on the next full rebuild.
- **Adding an object type to live edit takes two halves; creation alone is useless.** A `case` in `InstanceLogic.HandleCreateInstanceCommandFromGlueInner`'s switch makes the instance, but `VariableAssignmentLogic.GetRuntimeInstance` still has to find it by name — and it searches the FRB managers (`SpriteManager.ManagedPositionedObjects`, `ShapeManager.Visible*`, `InstanceLogic.Self.*AddedAtRuntime`), not the screen's fields, which don't exist until the next rebuild. A type living outside those managers (`Camera`, in `SpriteManager.Cameras`) needs its own tracking list and lookup probe or every variable set on it fails.
- **`RefreshManager.ShouldRestartOnChange` / `CreateStopAndRestartTask` is the "give up and restart" escape valve.** Many Glue-side changes (new variable on an existing type, excluding a variable from a state category, failed object-add/remove round trips) aren't attempted live at all — they just queue a stop+rebuild+relaunch. If a live-edit feature "doesn't work," check whether the relevant `RefreshManager`/`VariableSendingManager` handler actually attempts a live push or just restarts.
- **An exception on the Glue side of a variable push kills Glue, not just the push.** `RefreshManager`'s `ReactTo*` handlers are `async void`, so anything `VariableSendingManager.ConvertValue` throws (a `TypeManager.GetDefaultForType` on a non-primitive type, for one) skips the output tab and ends the process; the only trace is the `crash-*.log` `Program.HandleExceptionsUnified` writes under `%LOCALAPPDATA%\FlatRedBall\Glue\Diagnostics`. A "Glue crashes during live edit" report almost always means one of these.
- **A restart is only lossless if the glux already holds the user's full intent.** Every `CreateStopAndRestartTask` call site abandons whatever the in-flight operation had not yet persisted, so anything a live command implies must be written to the glux before the command is sent, never in a success-only branch after it.
- File-change filtering: `RefreshManager.GetIfShouldReactToFileChange` explicitly ignores `*.Generated.cs`/`*.Generated.xml` changes so that codegen's own file writes don't trigger a feedback loop of restarts.
- **`ReactToPlayOrEditSet()` (`MainCompilerPlugin.cs`) fires twice per launch-into-edit-mode — once too early.** `GameHostController.StartRunInEditMode` sets `IsEditChecked = true` before `Compile()`/`DoRun` run (so the toolbar shows edit mode while building), which fires `PlayOrEdit`'s change handler and calls `ReactToPlayOrEditSet()` while the game process doesn't exist yet — guarded with an `IsRunning` early-out now, since the real send happens later via `Runner_GameStarted`. The command-line `IsInEditMode=` launch arg (`Game1GlueControlGenerator.cs`) looks like an alternate path but its handling is commented out/dead — the socket DTO is the only mechanism.

## Camera: edit mode vs. game mode

`Embedded\Editing\CameraLogic.cs` (static class `GlueControl.Editing.CameraLogic`) is the edit-mode camera controller. It manipulates the same `Camera.Main` singleton directly (no separate edit-camera object) and saves/restores position+zoom per screen type in a dictionary, so each screen remembers its last edit-mode camera state. Zoom is a discrete lookup table (`zoomLevels[]`, 10000%→5%) driven by mouse wheel / Ctrl+/-; panning is middle-mouse drag or edge-of-window drag-scroll.

Game mode's camera is set up by generated `Setup/CameraSetup.Generated.cs` (from `FRBDK\Glue\Glue\Plugins\EmbeddedPlugins\CameraPlugin\CameraSetupCodeGenerator.cs`), not by `CameraLogic.cs` at all — that class only compiles into live-edit builds.

| Behavior | Game mode | Edit mode |
|---|---|---|
| Zoom | Fixed at startup (`ResetCamera`/`SetupCamera`); only changes via window resize with `IncreaseVisibleArea`, or an opt-in `CameraControllingEntity.ApplyZoom()` | Freely adjustable — mouse wheel / hotkeys via `CameraLogic.UpdateCameraToZoomLevel()` |
| Bounds | Unclamped by default; clamping is opt-in per-screen via `CameraControllingEntity` (needs a `Map` assigned) — `Engines\FlatRedBallXNA\FlatRedBall\Entities\CameraControllingEntity.cs:309-393` | Always unclamped — no bounds code exists anywhere under `GlueControl\Embedded` |
| Aspect ratio | Fixed per `DisplaySettings.AspectRatioWidth/Height`; pillarbox/letterbox via `CameraSetup.SetAspectRatioTo` computing a `DestinationRectangle` smaller than the backbuffer | Unconstrained — Glue sends `SetCameraAspectRatioDto` with `AspectRatio = null` on entering edit mode (`MainCompilerPlugin.cs:851-876`), which makes `SetAspectRatioTo` fill the whole window with no bars |

The edit/game aspect-ratio toggle is a **one-shot DTO on Play/Edit switch**, not a persistent `if IsInEditMode` check in the render loop — `CommandReceiver.HandleDto(SetCameraAspectRatioDto)` (`Embedded\CommandReceiver.cs:850-863`) calls `CameraSetup.ResetCamera()` once and, if already in edit mode, also `CameraLogic.UpdateCameraToZoomLevel()`.

### Gum zoom — high-landmine area, read before touching zoom code

Gum renders UI in its own **pixel-space canvas** (`GraphicalUiElement.CanvasWidth/Height`), completely separate from FRB's world-space `Camera.Main`. This means **two independent zoom values must be kept in sync by hand** on every camera zoom change:

1. `Camera.Main.OrthogonalHeight` — the FRB world camera.
2. `RenderingLibrary.SystemManagers.Default.Renderer.Camera.Zoom` + per-layer `LayerCameraSettings.Zoom` — Gum's own scale factor.

`CameraLogic.UpdateCameraToZoomLevel()` (`Embedded\Editing\CameraLogic.cs:293-359`) sets both, then **must** call `CameraSetup.ResetGumResolutionValues()` (generated by `CameraSetupCodeGenerator.cs:217-310`) **before** setting the zoom, not after — it recomputes `CanvasWidth/Height` from window size and also resets `Renderer.Camera.Zoom` with no knowledge of the edit-mode zoom level, so calling it last silently clobbers the zoom just set.

Known landmines, in order of how likely they are to bite:

- **World-space (entity-attached) and HUD/screen-space Gum content now have genuinely independent zoom, edit-mode only, by design.** `PositionedObjectGueWrapper` (see [[gum-integration]]) re-homes any `AttachToContainer` Gum object onto a dedicated layer pair — FRB `FrbEntityAttachmentZoomLayer` / Gum `FrbEntityAttachmentGumZoomLayer`, lazily created by `GetOrCreateEntityAttachmentZoomLayer()` — the moment `ScreenManager.IsInEditMode` is true. `CameraLogic.UpdateCameraToZoomLevel()` only ever sets that one layer's `LayerCameraSettings.Zoom`; every other Gum layer (`MainLayer`, HUD layers) is deliberately left untouched, so HUD content never zooms with the editor control. Outside edit mode this layer is never created.
- **Anything that later moves an entity-attached GUE onto another layer silently undoes edit-mode zoom tracking, with no way for the wrapper to intercept it.** Game code commonly does this itself — e.g. re-parenting a spawned enemy's health bar onto a HUD layer via `gue.MoveToFrbLayer(hudLayer, GumIdb.Self)` right after spawn. `PositionedObjectGueWrapper.UpdateGumObject()` defends against this by re-asserting the zoom layer every frame while in edit mode, rather than trusting nothing else to touch the object after construction.
- **The lazily-created entity-attachment FRB layer can land before a screen's own layers in draw order.** First construction can happen before the screen finishes creating its own layers (e.g. a darkness/fog overlay), landing it at index 0 and getting covered. `EnsureEntityAttachmentFrbLayerDrawsLast()` fixes this from the constructor and `ScreenManager.ScreenLoaded` only — never from the per-frame update path, since mutating `SpriteManager`'s layer list mid-`DrawLayers` iteration draws the reordered layer twice in one frame.
- **Window resize races the zoom sync.** Generated `HandleResolutionChange` (`CameraSetupCodeGenerator.cs:688-706`) also calls `ResetGumResolutionValues()`, but recomputes canvas/zoom from base `Data.ResolutionWidth/Height` and never reapplies the edit-mode zoom multiplier to the entity-attachment layer — resizing while zoomed can leave world-space content's zoom stale.
- The shipped/generated `CameraLogic.Generated.cs` in real projects has historically matched the `Embedded` template byte-for-byte (verified against a real project) — if you find a project where it doesn't, that diff itself is signal of a manual workaround worth investigating.

## Key files

| Side | File | Purpose |
|---|---|---|
| Glue | `GameCommunicationPlugin\GlueControl\MainCompilerPlugin.cs` | Plugin entry point; owns build/run/edit-mode toggle, wires up embedding + codegen on Glux load |
| Glue | `Managers\GameHostController.cs` | Launches the game process, embeds its window in the Game tab, builds run args (`IsInEditMode=true`, startup screen) |
| Glue | `Managers\RefreshManager.cs` | Central dispatcher: Glue-side change → live command vs. stop/rebuild/restart |
| Glue | `Managers\VariableSendingManager.cs` | Glue property-grid change → `GlueVariableSetData` DTO(s) |
| Glue | `CommandSending\CommandSender.cs` | Serializes + sends DTOs, wraps `GameConnectionManager` socket calls |
| Glue | `Dtos\Dtos.cs` | Glue-side DTO definitions (mirrored, not shared, with the game copy) |
| Glue | `CodeGeneration\EmbeddedCodeManager.cs` | Copies `Embedded\*.cs` → game's `GlueControl\*.Generated.cs` |
| Glue | `Embedded\**` | Master source for everything the game gets — edit these, not the `.Generated.cs` copies in a game project |
| Game (generated) | `GlueControl\GlueControlManager.Generated.cs` | Runtime entry point; owns the socket, the `GameToGlueCommands` queue, edit-mode state |
| Game (generated) | `GlueControl\CommandReceiver.Generated.cs` | Deserializes incoming DTOs by type name, dispatches to `HandleDto` overloads |
| Game (generated) | `GlueControl\Editing\EditingManager.Generated.cs` | Selection, drag/resize input handling, pushes changes into `GameToGlueCommands` |
| Game (generated) | `GlueControl\Editing\VariableAssignmentLogic.Generated.cs` | The brittle live variable-overlay logic (see Gotchas) |
| Game (generated) | `GlueControl\Screens\EntityViewingScreen.Generated.cs` | Sandbox screen used when live-editing a single Entity outside any Screen |

