Declare the managers in boot.go, hand them to the engine whole
ci/woodpecker/push/ebitengine Pipeline was successful

boot.manager.go becomes boot.go and carries all seven managers as one
block, so what the game holds reads in one place instead of a line hidden
in each category file. The category files keep their entity alias.

The hand-off stops copying. Our managers are the same *inkwell.Manager[T]
the Game holds, so New assigns them onto it and engine and game share one
registry per category. content.manager.go is gone with the copy loop.

Themes stay additive — NewGame seeds four presets we would otherwise
throw away — and registerScene becomes prepareScene, which now has to run
before the hand-off: with no copy pass left, the scene defaults are
written back into SceneManager with Set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 10:37:02 +02:00
co-authored by Claude Opus 5
parent a70dff8771
commit 220985ec57
13 changed files with 96 additions and 78 deletions
+51 -31
View File
@@ -49,25 +49,25 @@ All game code lives in one flat package, `inc`. There are no subdirectories: a
file's name carries the structure, in the form `[category].[name].go`. file's name carries the structure, in the form `[category].[name].go`.
- The category is always **singular**: `item`, `scene`, `background`, - The category is always **singular**: `item`, `scene`, `background`,
`character`, `dialog`, `script`, `world`, `ui`, `theme`, `names`, `boot`, `character`, `dialog`, `script`, `world`, `ui`, `theme`, `names`.
`content`.
- `[category].manager.go` is the file that ties a category together — its - `[category].manager.go` is the file that ties a category together — its
manager, its entity type, whatever the category shares. entity type and whatever else the category shares.
- Every other file in a category holds exactly one entity: one scene, one - Every other file in a category holds exactly one entity: one scene, one
background, one item. The file registers it itself, in an `init()`. background, one item. The file registers it itself, in an `init()`.
- `boot.go` is the exception that has no category: it declares every manager
and builds the game.
``` ```
main.go flags + inkwell.Run main.go flags + inkwell.Run
inc/boot.manager.go wiring; New(Opts) builds the game inc/boot.go the managers; New(Opts) builds the game
inc/names.manager.go entity names and world-state keys inc/names.manager.go entity names and world-state keys
inc/theme.manager.go the theme manager and the colour helpers inc/theme.manager.go the Theme alias and the colour helpers
inc/theme.realworld.go realworld-93 inc/theme.realworld.go realworld-93
inc/theme.nokia_punk.go nokia-punk inc/theme.nokia_punk.go nokia-punk
inc/world.manager.go unsaved runtime state inc/world.manager.go unsaved runtime state
inc/world.action.go custom actions and the action pump inc/world.action.go custom actions and the action pump
inc/ui.manager.go HUD layout and widget registration inc/ui.manager.go HUD layout and widget registration
inc/ui.*.go custom widgets, coloured text inc/ui.*.go custom widgets, coloured text
inc/content.manager.go composition root; calls every register…
inc/background.*.go one image asset per scene inc/background.*.go one image asset per scene
inc/character.*.go the cast, tapes included inc/character.*.go the cast, tapes included
inc/item.*.go inventory inc/item.*.go inventory
@@ -84,18 +84,30 @@ Every category that owns a collection of entities has a manager, and they are
all the engine's own `inkwell.Manager[T]` — the same registry type the `*Game` all the engine's own `inkwell.Manager[T]` — the same registry type the `*Game`
hangs its content off. The game defines no registry of its own. hangs its content off. The game defines no registry of its own.
The entity types are aliases of engine structs, so they already carry All seven are declared together, in `boot.go`, so the list of what the game
`GetName()` and satisfy `inkwell.Named`; the manager needs nothing else: holds is one block rather than a line hidden in each category file:
```go
var (
BackgroundManager = inkwell.NewManager[Background]()
CharacterManager = inkwell.NewManager[Character]()
DialogManager = inkwell.NewManager[Dialog]()
ItemManager = inkwell.NewManager[Item]()
SceneManager = inkwell.NewManager[Scene]()
ScriptManager = inkwell.NewManager[Script]()
ThemeManager = inkwell.NewManager[Theme]()
)
```
The entity types stay in their own category files, one alias each, and that is
all a manager file holds now:
```go ```go
type Character = inkwell.Character type Character = inkwell.Character
var CharacterManager = inkwell.NewManager[Character]()
``` ```
That is the whole of a category's manager file — an alias and one line. There Aliases of engine structs already carry `GetName()` and satisfy
are seven managers: `BackgroundManager`, `CharacterManager`, `DialogManager`, `inkwell.Named`, which is the whole of what a manager asks of them.
`ItemManager`, `ScriptManager`, `SceneManager`, `ThemeManager`.
The methods the game uses are `Register`, `Set`, `Get`, `All` and `Each`. The methods the game uses are `Register`, `Set`, `Get`, `All` and `Each`.
`Register` panics on a duplicate name — a second registration is a `Register` panics on a duplicate name — a second registration is a
@@ -126,8 +138,9 @@ func init() {
``` ```
Adding an entity is adding a file. Deleting one is deleting a file. Package-level Adding an entity is adding a file. Deleting one is deleting a file. Package-level
variables are initialised before any `init()` runs, so the managers exist by the variables are initialised before any `init()` runs, whatever file each sits in,
time the first file registers into one. so the managers in `boot.go` exist by the time the first entity file registers
into one.
`init()` order is file-name order, so **registration order is alphabetical**. `init()` order is file-name order, so **registration order is alphabetical**.
Nothing may depend on it — including the order the arrow keys walk the scenes, Nothing may depend on it — including the order the arrow keys walk the scenes,
@@ -135,23 +148,30 @@ which is simply the order the files sit in.
### Handing a category to the engine ### Handing a category to the engine
Once the game exists, `Each` walks a manager and gives every entity to the Nothing is copied. `inkwell.NewGame` builds its own empty managers, and `New`
engine's own manager. `registerContent` is the whole of it: hands ours over in their place — the types are identical, so the engine and the
game end up sharing one registry per category rather than two in step:
```go ```go
func registerContent() { g.AssetManager = BackgroundManager
BackgroundManager.Each(World.G.AssetManager.Register) g.CharacterManager = CharacterManager
CharacterManager.Each(World.G.CharacterManager.Register) g.DialogueManager = DialogManager
ItemManager.Each(World.G.ItemManager.Register) g.ItemManager = ItemManager
DialogManager.Each(World.G.DialogueManager.Register) g.SceneManager = SceneManager
ScriptManager.Each(World.G.ScriptManager.Register) g.ScriptManager = ScriptManager
registerScene() ThemeManager.Each(g.ThemeManager.Register)
}
``` ```
`registerScene` is the one that needs its own function: it fills in the defaults Themes are the odd one out and stay a copy: `NewGame` puts four preset themes
a scene may leave out (Paul's starting position, the floor walkbox), and it into its `ThemeManager`, and replacing it would throw them away. Ours are added
derives the selector's pins by reading the exit graph backwards. to that set instead.
Two consequences of not copying. `prepareScene` has to run **before** the
hand-off, because there is no longer a copy pass to fill in the defaults a
scene leaves out — it writes them back into `SceneManager` with `Set`, and
derives the selector's pins by reading the exit graph backwards. And a manager
swapped in this way must be in place before `inkwell.Run`, which is where the
engine wires up the parts that hold a registry directly.
## The world ## The world
@@ -171,7 +191,7 @@ One package means one namespace, so an entity's constructor carries its
category as a prefix: category as a prefix:
```go ```go
sceneFloor sceneDefaults registerScene fillSelectorPins sceneFloor sceneDefaults prepareScene fillSelectorPins
``` ```
Entities themselves need no name at all — they are anonymous literals inside Entities themselves need no name at all — they are anonymous literals inside
@@ -183,8 +203,8 @@ constants in `names.manager.go`, the colour tokens, and the action constructors
(`TapeSay`, `Paused`, `SetMode`, `EnterScene`, `Back`, `Fn`). (`TapeSay`, `Paused`, `SetMode`, `EnterScene`, `Back`, `Fn`).
Everything else is machinery and stays unexported: the HUD widgets Everything else is machinery and stays unexported: the HUD widgets
(`tapeSlots`, `letterbox`, `hudFrame`, …), the `register…` functions, the (`tapeSlots`, `letterbox`, `hudFrame`, …), `registerUI`, the runners,
runners, `sceneDefaults`, `fillSelectorPins`. `prepareScene`, `sceneDefaults`, `fillSelectorPins`.
The word is **scene**, the engine's own. The wiki and the concept-art deck count The word is **scene**, the engine's own. The wiki and the concept-art deck count
*screens*, and this code used to as well, but everything a screen had that a *screens*, and this code used to as well, but everything a screen had that a
+18 -9
View File
@@ -153,16 +153,17 @@ licence.
The game is one flat package, `inc`. There are no subdirectories: a file's The game is one flat package, `inc`. There are no subdirectories: a file's
name says where it belongs, in the form `[category].[name].go`. The category is name says where it belongs, in the form `[category].[name].go`. The category is
always singular, and `[category].manager.go` is the file that ties that category always singular, and `[category].manager.go` is the file that ties that category
together — its manager, its shared types, its hand-off to the engine. together — its entity type and whatever else the category shares. `boot.go` is
the one file with no category: it declares every manager and builds the game.
``` ```
main.go flags + inkwell.Run main.go flags + inkwell.Run
inc/boot.go the managers, and New(Opts)
inc/names.manager.go entity names and world-state keys inc/names.manager.go entity names and world-state keys
inc/theme.*.go realworld-93 + nokia-punk inc/theme.*.go realworld-93 + nokia-punk
inc/world.*.go unsaved runtime state, custom actions, action pump inc/world.*.go unsaved runtime state, custom actions, action pump
inc/ui.*.go HUD: layout, custom widgets, coloured text inc/ui.*.go HUD: layout, custom widgets, coloured text
inc/<kind>.<name>.go one file per registered entity, by kind inc/<kind>.<name>.go one file per registered entity, by kind
inc/boot.manager.go wiring
``` ```
Nothing is enforced by the compiler any more, so the layering is a rule the Nothing is enforced by the compiler any more, so the layering is a rule the
@@ -194,13 +195,12 @@ Every category owns a manager, and they are all the engine's own
`inkwell.Manager[T]` — the same registry the `*Game` hangs its content off, kept `inkwell.Manager[T]` — the same registry the `*Game` hangs its content off, kept
in registration order and addressed by name. There is no second registry type in registration order and addressed by name. There is no second registry type
here: the entity types are aliases of engine structs, so they already satisfy here: the entity types are aliases of engine structs, so they already satisfy
`inkwell.Named` and need no help reading their own name. A category's manager `inkwell.Named` and need no help reading their own name. The seven of them are
file is an alias and one line: declared as one block in `boot.go`, and a category's manager file is left
holding the alias:
```go ```go
type Character = inkwell.Character type Character = inkwell.Character
var CharacterManager = inkwell.NewManager[Character]()
``` ```
An entity file is a literal that hands itself over in an `init()`: An entity file is a literal that hands itself over in an `init()`:
@@ -223,9 +223,18 @@ step with the files by hand, and the deck is a thing to look at, not a thing to
play through. play through.
The game's own catalogue is readable without going through the engine: The game's own catalogue is readable without going through the engine:
`SceneManager.Get("alley")` answers before a single scene has been handed over. `SceneManager.Get("alley")` answers long before there is a `*Game` to ask. And
`registerContent` is where the hand-off happens, one `Each` call per category — when the game is built, nothing is copied into it — `New` assigns our managers
the game's manager iterated straight into the engine manager's `Register`. onto the `*Game` in place of the empty ones `NewGame` made, so engine and game
share one registry per category instead of keeping two of them in step. Themes
are the exception: `NewGame` seeds its theme manager with four presets, so ours
are added to that set rather than replacing it.
The price of not copying is that the defaults a scene may leave out — Paul's
starting position, the floor walkbox — have to be written back into
`SceneManager` before the hand-off rather than filled in on the way past.
`prepareScene` does that with `Set`, and derives the selector's pins in the same
pass by reading the exit graph backwards.
There is one world, and it is a package-level singleton: `World`. Nothing takes There is one world, and it is a package-level singleton: `World`. Nothing takes
a `*world` parameter and no widget holds a back-reference, which is what lets a a `*world` parameter and no widget holds a back-reference, which is what lets a
+1 -1
View File
@@ -3,7 +3,7 @@ module git.teletypegames.org/games/realworld
go 1.26.3 go 1.26.3
require ( require (
git.teletypegames.org/engines/inkwell v0.1.1-0.20260830075028-08f15a7e3d11 git.teletypegames.org/engines/inkwell v0.1.1-0.20260830083613-39af65f3c98f
github.com/hajimehoshi/ebiten/v2 v2.9.9 github.com/hajimehoshi/ebiten/v2 v2.9.9
) )
+2 -2
View File
@@ -1,5 +1,5 @@
git.teletypegames.org/engines/inkwell v0.1.1-0.20260830075028-08f15a7e3d11 h1:DPN74Xy0xcyelfh5xLuv6XBj6wrdzKVME1a1HyACsj4= git.teletypegames.org/engines/inkwell v0.1.1-0.20260830083613-39af65f3c98f h1:Qw/sUeJeCBEFvdiqwGUn919hsmoTILJyKSPlkKvNgh0=
git.teletypegames.org/engines/inkwell v0.1.1-0.20260830075028-08f15a7e3d11/go.mod h1:/v6QtismTE8+e7kobbVR5B/CpTSrd4j2DYwJ8DvINhs= git.teletypegames.org/engines/inkwell v0.1.1-0.20260830083613-39af65f3c98f/go.mod h1:/v6QtismTE8+e7kobbVR5B/CpTSrd4j2DYwJ8DvINhs=
github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 h1:+kz5iTT3L7uU+VhlMfTb8hHcxLO3TlaELlX8wa4XjA0= github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 h1:+kz5iTT3L7uU+VhlMfTb8hHcxLO3TlaELlX8wa4XjA0=
github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1/go.mod h1:lKJoeixeJwnFmYsBny4vvCJGVFc3aYDalhuDsfZzWHI= github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1/go.mod h1:lKJoeixeJwnFmYsBny4vvCJGVFc3aYDalhuDsfZzWHI=
github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE=
-2
View File
@@ -5,5 +5,3 @@ import (
) )
type Background = inkwell.Asset type Background = inkwell.Asset
var BackgroundManager = inkwell.NewManager[Background]()
+20 -3
View File
@@ -4,6 +4,16 @@ import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
) )
var (
BackgroundManager = inkwell.NewManager[Background]()
CharacterManager = inkwell.NewManager[Character]()
DialogManager = inkwell.NewManager[Dialog]()
ItemManager = inkwell.NewManager[Item]()
SceneManager = inkwell.NewManager[Scene]()
ScriptManager = inkwell.NewManager[Script]()
ThemeManager = inkwell.NewManager[Theme]()
)
type Opts struct { type Opts struct {
Scene string Scene string
Finale bool Finale bool
@@ -15,6 +25,8 @@ func New(o Opts) *inkwell.Game {
start = SceneAlley start = SceneAlley
} }
prepareScene()
g := inkwell.NewGame("Real World", ScreenW, ScreenH) g := inkwell.NewGame("Real World", ScreenW, ScreenH)
g.MaxLogLines = 64 g.MaxLogLines = 64
g.SceneRect = inkwell.Rect(0, TopBarH, ScreenW, HUDTop-TopBarH) g.SceneRect = inkwell.Rect(0, TopBarH, ScreenW, HUDTop-TopBarH)
@@ -25,10 +37,15 @@ func New(o Opts) *inkwell.Game {
return inkwell.Say(Paul, "It's a way out, not a thing.") return inkwell.Say(Paul, "It's a way out, not a thing.")
} }
World.attach(g) g.AssetManager = BackgroundManager
registerTheme() g.CharacterManager = CharacterManager
registerContent() g.DialogueManager = DialogManager
g.ItemManager = ItemManager
g.SceneManager = SceneManager
g.ScriptManager = ScriptManager
ThemeManager.Each(g.ThemeManager.Register)
World.attach(g)
g.UseTheme(RealWorld) g.UseTheme(RealWorld)
registerUI() registerUI()
-2
View File
@@ -5,5 +5,3 @@ import (
) )
type Character = inkwell.Character type Character = inkwell.Character
var CharacterManager = inkwell.NewManager[Character]()
-10
View File
@@ -1,10 +0,0 @@
package inc
func registerContent() {
BackgroundManager.Each(World.G.AssetManager.Register)
CharacterManager.Each(World.G.CharacterManager.Register)
ItemManager.Each(World.G.ItemManager.Register)
DialogManager.Each(World.G.DialogueManager.Register)
ScriptManager.Each(World.G.ScriptManager.Register)
registerScene()
}
-2
View File
@@ -5,5 +5,3 @@ import (
) )
type Dialog = inkwell.Dialogue type Dialog = inkwell.Dialogue
var DialogManager = inkwell.NewManager[Dialog]()
-2
View File
@@ -5,5 +5,3 @@ import (
) )
type Item = inkwell.Item type Item = inkwell.Item
var ItemManager = inkwell.NewManager[Item]()
+4 -6
View File
@@ -6,13 +6,11 @@ import (
type Scene = inkwell.Scene type Scene = inkwell.Scene
var SceneManager = inkwell.NewManager[Scene]() func prepareScene() {
func registerScene() {
fillSelectorPins() fillSelectorPins()
SceneManager.Each(func(entity Scene) { for _, entity := range SceneManager.All() {
World.G.SceneManager.Register(sceneDefaults(entity)) SceneManager.Set(sceneDefaults(entity))
}) }
} }
var sceneFloor = []inkwell.Polygon{ var sceneFloor = []inkwell.Polygon{
-2
View File
@@ -5,5 +5,3 @@ import (
) )
type Script = inkwell.Script type Script = inkwell.Script
var ScriptManager = inkwell.NewManager[Script]()
-6
View File
@@ -8,8 +8,6 @@ import (
type Theme = inkwell.Theme type Theme = inkwell.Theme
var ThemeManager = inkwell.NewManager[Theme]()
const ( const (
RealWorld = "realworld-93" RealWorld = "realworld-93"
NokiaPunk = "nokia-punk" NokiaPunk = "nokia-punk"
@@ -32,7 +30,3 @@ func RGBA(hex uint32, a uint8) color.Color {
A: a, A: a,
} }
} }
func registerTheme() {
ThemeManager.Each(World.G.ThemeManager.Register)
}