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'sCommandReceiver.Receivesplits on the first:, reflects over its ownHandleDtooverloads 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(aConcurrentQueue);GlueControlManager's socket loop drains it and writes to thegameToGlueSocket. 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 toCreateStopAndRestartTask(kill + rebuild + relaunch the game) when a live update isn't supported.VariableSendingManagerturns a Glue property-grid change intoGlueVariableSetDataDTOs (handlesX/Y/Z→RelativeX/Y/Zwhen attached, collision relationships, tile shape collections, states, etc.) beforeCommandSenderships 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 Accepts 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
Succeededis the only readiness signal; its payload is not. MostHandleDtooverloads returnvoid, 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.Selfstill null duringGame1.Initialize) withGameConnectionManager.NotReadyPayload, whichSendItemWithResponseturns into an unsuccessfulGeneralResponse.CommandSender.Send<T>additionally fails an empty reply, since a typed caller asked for data. - The debug/edit-mode hook is
CustomActivityEditMode(), not "CustomDebugActivity."ScreenManagercallsScreen.ActivityEditMode()(virtual,Engines\FlatRedBallXNA\FlatRedBall\Screens\Screen.cs) instead of normalActivitywhileScreenManager.IsInEditModeis true. Per-element codegen (CodeWriter.GenerateActivityEditMode,FRBDK\Glue\Glue\CodeGeneration\CodeWriter.cs:1422) calls each named object's ownActivityEditMode()and thenCustomActivityEditMode()— an emptypartial voidusers 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.ApplyVariables 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
caseinInstanceLogic.HandleCreateInstanceCommandFromGlueInner's switch makes the instance, butVariableAssignmentLogic.GetRuntimeInstancestill 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, inSpriteManager.Cameras) needs its own tracking list and lookup probe or every variable set on it fails. RefreshManager.ShouldRestartOnChange/CreateStopAndRestartTaskis 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 relevantRefreshManager/VariableSendingManagerhandler 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'sReactTo*handlers areasync void, so anythingVariableSendingManager.ConvertValuethrows (aTypeManager.GetDefaultForTypeon a non-primitive type, for one) skips the output tab and ends the process; the only trace is thecrash-*.logProgram.HandleExceptionsUnifiedwrites 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
CreateStopAndRestartTaskcall 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.GetIfShouldReactToFileChangeexplicitly ignores*.Generated.cs/*.Generated.xmlchanges 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.StartRunInEditModesetsIsEditChecked = truebeforeCompile()/DoRunrun (so the toolbar shows edit mode while building), which firesPlayOrEdit's change handler and callsReactToPlayOrEditSet()while the game process doesn't exist yet — guarded with anIsRunningearly-out now, since the real send happens later viaRunner_GameStarted. The command-lineIsInEditMode=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:
Camera.Main.OrthogonalHeight— the FRB world camera.RenderingLibrary.SystemManagers.Default.Renderer.Camera.Zoom+ per-layerLayerCameraSettings.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 anyAttachToContainerGum object onto a dedicated layer pair — FRBFrbEntityAttachmentZoomLayer/ GumFrbEntityAttachmentGumZoomLayer, lazily created byGetOrCreateEntityAttachmentZoomLayer()— the momentScreenManager.IsInEditModeis true.CameraLogic.UpdateCameraToZoomLevel()only ever sets that one layer'sLayerCameraSettings.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 andScreenManager.ScreenLoadedonly — never from the per-frame update path, since mutatingSpriteManager's layer list mid-DrawLayersiteration draws the reordered layer twice in one frame. - Window resize races the zoom sync. Generated
HandleResolutionChange(CameraSetupCodeGenerator.cs:688-706) also callsResetGumResolutionValues(), but recomputes canvas/zoom from baseData.ResolutionWidth/Heightand 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.csin real projects has historically matched theEmbeddedtemplate 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 |