# Glue Project Loading

> Loading Glue Projects

- Skill: `vchelaru/glue-project-loading` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vchelaru/glue-project-loading`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vchelaru/glue-project-loading/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-project-loading

---


# Loading Glue Projects

FRB2 reads the JSON files Glue writes and builds Screens and Entities from them at runtime. There is
no generated C#: `src/Glue/` is the whole implementation, `GlueProject` is the entry point.

Booting one is a single call in `Game1.Initialize`, after `base.Initialize()`:

```csharp
FlatRedBallService.Default.Initialize(this, "Content/FrbEditor/MyGame.gluj");
```

The project names its own start-up screen, Gum project and resolution, so nothing else is passed —
the engine wires the screen's `Save`/`Project` itself. **The path must be relative**: it resolves
through `TitleContainer`, which throws on a rooted path, so an absolute one loads the project and
then fails every asset it references. To load a project but boot your own screen instead, use
`Initialize<MyScreen>(game, new EngineInitSettings { GlueProjectFile = … })`.

## Where to start

| Task | Go to |
|---|---|
| Boot a project | `Initialize(game, "Content/FrbEditor/MyGame.gluj")` |
| Load without booting | `GlueProject.Load(glujPath, content)` |
| Find/create an element | `GlueProject.FindScreen`/`FindEntity`/`CreateScreen`/`CreateEntity` |
| Move between screens | `Screen.MoveToScreen(string glueName)` |
| Read/write an authored variable | the indexer on `GlueScreen`/`GlueEntity`, or `Get<T>` |
| Claim a Glue type for FRB2 | `GlueTypeMap` |

## Every loaded element shares one C# type

`GlueScreen` and `GlueEntity` are what a loaded element *is*; only its `Save` data distinguishes it.
So `MoveToScreen<T>()` and `Factory<T>` cannot tell two loaded screens apart — the by-name overloads
(`MoveToScreen(string)`, `GlueProject.CreateEntity(name, screen)`) exist for exactly this reason.
Both directions work: a loaded screen can still `MoveToScreen<AHandWrittenScreen>()`.

Names are Glue's own, with the folder: `Screens\Level1`, `Entities\Player`. Either separator is
accepted and case is ignored, but the prefix is required — a bare leaf is ambiguous, since a screen
and an entity can share one.

## Landmines

**Assign `Save` inside the `configure` callback, never before it.** The engine retains that callback
and replays it on `RestartScreen`; assigning outside means a restarted screen rebuilds with no data —
a silently *empty* screen rather than an error. Only applies when you construct a `GlueScreen`
yourself: `Initialize(game, glujPath)` and `MoveToScreen(string)` both do it correctly.

**Loading is tolerant by design: it collects diagnostics instead of throwing.** A project that
references a type this build cannot construct still loads, minus that object. Check
`GlueLoadResult.HasErrors` and `Diagnostics`, and `GlueScreen.BuildDiagnostics` for per-element
problems — otherwise a half-built screen looks like a working one. `GlueLoadOptions.Strict` turns
the first diagnostic into a throw when you would rather fail loudly.

**Adding a row to `GlueTypeMap` requires a matching `DynamicDependency`.** The reflected set is
closed and the engine publishes AOT/trimmed; without it the type's properties are trimmed away and
every assignment to them silently does nothing — in published builds only, never in `dotnet build`.

**A Glue project's Gum UI must be loaded through `GlueProjectFile`, not `GumProjectFile`.** Gum only
resolves elements from a project its own `GumService` loaded, and the `.gumx` path is not known until
the `.gluj` has been read. See `gum-integration` for what goes wrong otherwise.

**Abstract elements cannot be created.** Glue marks an element abstract when it leaves an object for
a derived element to supply, so it is incomplete by construction. `CreateScreen`/`CreateEntity`
throw; this is normal for a `GameScreen` base that levels derive from.

## Hot reload

A `GlueScreen` watches the source tree the `.gluj` sits in and restarts itself when Glue writes
there: copy the changed file to the build output, reparse the project, rebuild the same screen from
the new data. Nothing to call — it registers in `CustomInitialize` whenever
`FlatRedBallService.SourceContentRoots` is non-empty, so it is dev-only by construction. Opt out with
`FlatRedBallService.IsGlueHotReloadEnabled` before the first screen starts.

**The reload restart replaces the retained `configure` callback.** `Save` and `Project` survive —
the replacement reassigns them — but anything else a callback you passed did, a difficulty or a seed
or a hand-built object, is gone from every restart after the first Glue edit. Put that setup in a
`GlueScreen` subclass's `CustomInitialize` or in `RestoreHotReloadState`.

Gum files are left to Gum's own in-place pipeline, and `bin`/`obj` are filtered by
`ContentDirectoryWatcher.IgnoredDirectories`. `content-hot-reload` covers the watch/copy machinery
underneath.

## Variables: two surfaces, two intents

`Objects` is the typed dictionary of built objects — `(Circle)screen.Objects["CooldownCircle"]`. The
indexer is the variable bag: `entity["Health"] = 100`, `entity.Get<int>("Health")`.

The indexer resolves in a fixed order, and the order is the point: a name matching a real CLR member
writes that member (`entity["X"] = 5f` moves the entity), a tunneling variable reaches the contained
object it targets, and only an unclaimed name is held by name. `Get<T>` is driven by `T`, not by the
variable's declared type — Glue's declared types are frequently not CLR types at all.

## Inheritance is resolved at load, once

`GlueProjectLoader` flattens every element into the union of its chain before anything inspects it,
so a derived screen already carries its base's objects, variables, states, and referenced files.
Code reading a `ScreenSave` never walks `BaseScreen` itself.

## What is not wired

Pooling (`PooledByFactory`), `SortAxis` partitioning, and input binding (`JumpInput`,
`MovementInput`, `EntitySave.InputDevice`) are parsed and not applied — a loaded platformer entity
has its authored physics but nothing telling it to move. `CustomClasses` and `Events` are out of
scope entirely. `plan/804-glue-project-loader/` records why for each.

