Glue Live Game Testing
GameCommunicationPlugin/GlueControl/Embedded/*.cs (CommandReceiver, GlueControlManager, EditingManager,
...) is <Compile Remove>d from GameCommunicationPlugin.csproj — it only exists inside a compiled,
running game process, never inside Glue itself. glue-unit-test-bootstrap's
GoldProjectCompileTests prove that closure compiles; they never run it. If a bug is in what
CommandReceiver.HandleDto actually does at runtime (screen transitions, entity selection, edit-mode
state), a compile-only test cannot catch it — you need a real running game process.
The harness
GlueUnitTests/TestSupport/LiveGameProcess.cs builds a gold project, launches its real built .exe, and
drives it through Glue's actual CommandSender/GameJsonCommunicationPlugin.Common.GameConnectionManager
socket protocol — the same wire protocol production Glue uses, not a stand-in.
[Trait("Category", "LiveGame")]
[StaFact]
public async Task MyTest()
{
GlueTestBootstrap.EnsureGameProjectPluginsRegistered();
using var game = await LiveGameProcess.StartAsync(
"Samples/EditorTest1",
csprojRelativeToProjectRoot: "EditorTest1/EditorTest1.csproj",
exeRelativeToProjectRoot: "EditorTest1/bin/Debug/net9.0/EditorTest1.exe");
var screenName = await game.GetCurrentScreenName(); // "" if no screen loaded
var response = await game.SelectEntity("Entities\\Entity1");
}
StartAsync (default refreshLiveEditCodeFromSource: true) loads the copied project into Glue and calls
GoldProject.EmbedLiveEditCode() before building, so the test exercises the CURRENT branch's
Embedded/*.cs, not whatever was checked in. See LiveGameProcessTests.cs for worked examples.
Adding a new drive/observe method
Follow GetCurrentScreenName()/SelectEntity(): build the real DTO
(GameCommunicationPlugin.GlueControl.Dtos) and call CommandSender.Self.Send(dto) directly.
Landmine — do not set GlueState.Self.CurrentEntitySave/CurrentScreenSave/CurrentNamedObjectSave to
drive selection. Their setters route through GlueState.Find.TreeNodeByTag(value), which needs a real,
populated WPF tree view. There isn't one in this headless host, so the assignment silently no-ops
(CurrentElement stays null) and RefreshManager.PushGlueSelectionToGame sends nothing — no exception, no
signal, just a test that mysteriously never sees the effect. Skip Glue's UI-bound selection state entirely
and build the DTO by hand.
Only Samples/EditorTest1 works as a target project today
LiveGameProcess needs a project whose Game1.Generated.cs already constructs
GlueControlManager/GameConnectionManager — i.e. one that went through a real "enable live edit" Glue
session. That wiring comes from MainCompilerPlugin.HandleGluxLoaded (Game1GlueControlGenerator), and
MainCompilerPlugin cannot run in the test host (it builds real WPF tabs and opens sockets on
registration — same reason GoldProject.EmbedLiveEditCode() calls EmbeddedCodeManager.EmbedAll directly
instead of going through it). So Game1.Generated.cs can't be regenerated headlessly; LiveGameProcess
preserves it exactly as checked in (only patching its port) rather than deleting/regenerating it.
Samples/EditorTest1 is checked in with its Generated.cs committed - a .gitignore exception like
BeefballKni's, since *.Generated.cs is gitignored repo-wide otherwise. To add another target project:
turn on live edit for real in a real Glue session, copy the project in the same way (exclude bin/obj,
keep the sibling .sln - ProjectLoader needs it), add the same two-line .gitignore exception.
Wire protocol, if you need to touch it
Game connects OUT to Glue (Glue listens). Two separate TCP sockets, one per direction, each opened with a
1-byte handshake (1 = glue→game, 2 = game→glue) — see GameCommunicationPlugin/Common/GameConnectionManager.cs
(Glue-side, server) and GlueControl/Embedded/GameConnectionManager.cs (game-side, client, namespace
GlueCommunication). Port is baked into Game1.Generated.cs as a literal int at two call sites (was
8846 in the checked-in fixture) - LiveGameProcess text-patches both before building, to a fresh port
per run so it never collides with a real Glue instance on the dev machine.
The actual DTO dispatch is GlueControlManager.ProcessMessage (Embedded/GlueControlManager.cs) -
"GetCurrentScreen" is a raw-string command handled specially; everything else is "{DtoTypeName}:{json}"
routed to CommandReceiver.Receive/HandleDto(SelectObjectDto) etc.
CI
Tagged Category=LiveGame and run by its own step in pr-tests.yml, ahead of the slower build smoke test.
glue.yml still excludes it, since that workflow publishes releases and should not wait on a game window.
Locally: dotnet test ... --filter "Category=LiveGame".
Landmine — the runners have no GPU. opengl32.dll resolves to Windows' generic OpenGL 1.1, which has
no framebuffer objects, so MonoGame's GraphicsDevice throws NoSuitableGraphicsDeviceException and the
game dies before it can connect. pr-tests.yml's "Install Mesa llvmpipe" step downloads a software GL and
points FRB_LIVE_GAME_TEST_GL_RUNTIME at it; LiveGameProcess copies those DLLs next to each game, since
Windows resolves opengl32.dll from the exe's own directory ahead of System32. Every game runs from its
own temp directory, so CI cannot stage them at a fixed path.