The engine already had the registry the game was reimplementing: inkwell.Manager[T Named], and every entity type here is an alias of an engine struct, so they all carry GetName() already. manager.manager.go and manager.interface.go are gone; a category manager is now one call to inkwell.NewManager. registerAll goes with it — Each does the hand-off — and the selector writes its derived pins back with Set, which keeps the scene in place in the registration order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,8 +58,6 @@ file's name carries the structure, in the form `[category].[name].go`.
|
|||||||
|
|
||||||
```
|
```
|
||||||
main.go flags + inkwell.Run
|
main.go flags + inkwell.Run
|
||||||
inc/manager.manager.go Manager[T]: the generic registry every category uses
|
|
||||||
inc/manager.interface.go ManagerInterface and the compile-time assertions
|
|
||||||
inc/boot.manager.go wiring; New(Opts) builds the game
|
inc/boot.manager.go wiring; 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 manager and the colour helpers
|
||||||
@@ -80,43 +78,30 @@ inc/scene.selector.go the map screen: its pins are derived from the graph
|
|||||||
inc/scene.*.go one file per scene
|
inc/scene.*.go one file per scene
|
||||||
```
|
```
|
||||||
|
|
||||||
`manager` is the one category with no entities of its own: it holds the registry
|
|
||||||
every other category is built from.
|
|
||||||
|
|
||||||
## Managers
|
## Managers
|
||||||
|
|
||||||
Every category that owns a collection of entities has a manager, and they are
|
Every category that owns a collection of entities has a manager, and they are
|
||||||
all the same generic type — `Manager[T]` in `manager.manager.go`. It keeps one
|
all the engine's own `inkwell.Manager[T]` — the same registry type the `*Game`
|
||||||
slice in registration order and one `map[string]int` beside it, so `GetByName`
|
hangs its content off. The game defines no registry of its own.
|
||||||
is a map lookup, not a scan.
|
|
||||||
|
|
||||||
Since the entity types are aliases of engine structs, no method can be attached
|
The entity types are aliases of engine structs, so they already carry
|
||||||
to them; the manager is told how to read a name instead, which is all it needs:
|
`GetName()` and satisfy `inkwell.Named`; the manager needs nothing else:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Character = inkwell.Character
|
type Character = inkwell.Character
|
||||||
|
|
||||||
var CharacterManager = NewManager(func(entity Character) string { return entity.Name })
|
var CharacterManager = inkwell.NewManager[Character]()
|
||||||
```
|
```
|
||||||
|
|
||||||
That is the whole of a category's manager file — an alias and one line. There
|
That is the whole of a category's manager file — an alias and one line. There
|
||||||
are seven managers: `BackgroundManager`, `CharacterManager`, `DialogManager`,
|
are seven managers: `BackgroundManager`, `CharacterManager`, `DialogManager`,
|
||||||
`ItemManager`, `ScriptManager`, `SceneManager`, `ThemeManager`.
|
`ItemManager`, `ScriptManager`, `SceneManager`, `ThemeManager`.
|
||||||
|
|
||||||
`manager.interface.go` holds the contract they all keep, and asserts each one
|
The methods the game uses are `Register`, `Set`, `Get`, `All` and `Each`.
|
||||||
against it. A new manager goes on that list.
|
`Register` panics on a duplicate name — a second registration is a
|
||||||
|
construction-time bug, not an update — so rewriting an entity that is already
|
||||||
```go
|
in the registry goes through `Set`, which keeps its position in the order.
|
||||||
type ManagerInterface[T any] interface {
|
`All` and `Each` both hand back the entities in registration order.
|
||||||
Register(entity T)
|
|
||||||
GetByName(name string) (T, bool)
|
|
||||||
GetAll() []T
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`Register` replaces by name and keeps the entity's position, so registering
|
|
||||||
twice is an update, never a duplicate. `GetAll` returns the slice itself, in
|
|
||||||
registration order.
|
|
||||||
|
|
||||||
Every entity type is an alias, `Scene` included. It was once a struct of our
|
Every entity type is an alias, `Scene` included. It was once a struct of our
|
||||||
own, because inkwell's `Scene` could not carry exits; that gap was closed in the
|
own, because inkwell's `Scene` could not carry exits; that gap was closed in the
|
||||||
@@ -150,16 +135,16 @@ 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, `registerAll` walks a manager and gives every entity to
|
Once the game exists, `Each` walks a manager and gives every entity to the
|
||||||
the engine's own manager. `registerContent` is the whole of it:
|
engine's own manager. `registerContent` is the whole of it:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func registerContent() {
|
func registerContent() {
|
||||||
registerAll(BackgroundManager, World.G.AssetManager.Register)
|
BackgroundManager.Each(World.G.AssetManager.Register)
|
||||||
registerAll(CharacterManager, World.G.CharacterManager.Register)
|
CharacterManager.Each(World.G.CharacterManager.Register)
|
||||||
registerAll(ItemManager, World.G.ItemManager.Register)
|
ItemManager.Each(World.G.ItemManager.Register)
|
||||||
registerAll(DialogManager, World.G.DialogueManager.Register)
|
DialogManager.Each(World.G.DialogueManager.Register)
|
||||||
registerAll(ScriptManager, World.G.ScriptManager.Register)
|
ScriptManager.Each(World.G.ScriptManager.Register)
|
||||||
registerScene()
|
registerScene()
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -157,8 +157,6 @@ together — its manager, its shared types, its hand-off to the engine.
|
|||||||
|
|
||||||
```
|
```
|
||||||
main.go flags + inkwell.Run
|
main.go flags + inkwell.Run
|
||||||
inc/manager.manager.go Manager[T], the generic registry
|
|
||||||
inc/manager.interface.go ManagerInterface, the contract every manager keeps
|
|
||||||
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
|
||||||
@@ -192,27 +190,17 @@ scene.*.go one file per scene: scene.alley.go, … + scene.selector.go
|
|||||||
Adding a scene means adding `scene.<name>.go` and `background.<name>.go`.
|
Adding a scene means adding `scene.<name>.go` and `background.<name>.go`.
|
||||||
Nothing else moves.
|
Nothing else moves.
|
||||||
|
|
||||||
Every category owns a manager, and they are all the same generic type,
|
Every category owns a manager, and they are all the engine's own
|
||||||
`Manager[T]` — one slice in registration order, one `map[string]int` beside it,
|
`inkwell.Manager[T]` — the same registry the `*Game` hangs its content off, kept
|
||||||
so a lookup by name is a map hit rather than a scan. The entity types are
|
in registration order and addressed by name. There is no second registry type
|
||||||
aliases of engine structs and cannot carry methods, so the manager is handed a
|
here: the entity types are aliases of engine structs, so they already satisfy
|
||||||
function that reads the name instead. A category's manager file is an alias and
|
`inkwell.Named` and need no help reading their own name. A category's manager
|
||||||
one line:
|
file is an alias and one line:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Character = inkwell.Character
|
type Character = inkwell.Character
|
||||||
|
|
||||||
var CharacterManager = NewManager(func(entity Character) string { return entity.Name })
|
var CharacterManager = inkwell.NewManager[Character]()
|
||||||
```
|
|
||||||
|
|
||||||
They all keep the same contract, asserted in `manager.interface.go`:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type ManagerInterface[T any] interface {
|
|
||||||
Register(entity T)
|
|
||||||
GetByName(name string) (T, bool)
|
|
||||||
GetAll() []T
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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()`:
|
||||||
@@ -235,9 +223,9 @@ 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.GetByName("alley")` answers before a single scene has been handed
|
`SceneManager.Get("alley")` answers before a single scene has been handed over.
|
||||||
over. `registerContent` is where the hand-off happens, one `registerAll` call
|
`registerContent` is where the hand-off happens, one `Each` call per category —
|
||||||
per category.
|
the game's manager iterated straight into the engine manager's `Register`.
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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.20260829220920-f9745e426624
|
git.teletypegames.org/engines/inkwell v0.1.1-0.20260830075028-08f15a7e3d11
|
||||||
github.com/hajimehoshi/ebiten/v2 v2.9.9
|
github.com/hajimehoshi/ebiten/v2 v2.9.9
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
git.teletypegames.org/engines/inkwell v0.1.0 h1:NJgT924aR3e0uLVRiTEhzLrXJY+Vnj+FArkKqMJJlVI=
|
git.teletypegames.org/engines/inkwell v0.1.1-0.20260830075028-08f15a7e3d11 h1:DPN74Xy0xcyelfh5xLuv6XBj6wrdzKVME1a1HyACsj4=
|
||||||
git.teletypegames.org/engines/inkwell v0.1.0/go.mod h1:/v6QtismTE8+e7kobbVR5B/CpTSrd4j2DYwJ8DvINhs=
|
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.20260829220920-f9745e426624 h1:lzKbik6RzcqIEub13X8a1J3V/MCQOPNo2XF8oOKm+Ts=
|
|
||||||
git.teletypegames.org/engines/inkwell v0.1.1-0.20260829220920-f9745e426624/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=
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import (
|
|||||||
|
|
||||||
type Background = inkwell.Asset
|
type Background = inkwell.Asset
|
||||||
|
|
||||||
var BackgroundManager = NewManager(func(entity Background) string { return entity.Name })
|
var BackgroundManager = inkwell.NewManager[Background]()
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import (
|
|||||||
|
|
||||||
type Character = inkwell.Character
|
type Character = inkwell.Character
|
||||||
|
|
||||||
var CharacterManager = NewManager(func(entity Character) string { return entity.Name })
|
var CharacterManager = inkwell.NewManager[Character]()
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
func registerContent() {
|
func registerContent() {
|
||||||
registerAll(BackgroundManager, World.G.AssetManager.Register)
|
BackgroundManager.Each(World.G.AssetManager.Register)
|
||||||
registerAll(CharacterManager, World.G.CharacterManager.Register)
|
CharacterManager.Each(World.G.CharacterManager.Register)
|
||||||
registerAll(ItemManager, World.G.ItemManager.Register)
|
ItemManager.Each(World.G.ItemManager.Register)
|
||||||
registerAll(DialogManager, World.G.DialogueManager.Register)
|
DialogManager.Each(World.G.DialogueManager.Register)
|
||||||
registerAll(ScriptManager, World.G.ScriptManager.Register)
|
ScriptManager.Each(World.G.ScriptManager.Register)
|
||||||
registerScene()
|
registerScene()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import (
|
|||||||
|
|
||||||
type Dialog = inkwell.Dialogue
|
type Dialog = inkwell.Dialogue
|
||||||
|
|
||||||
var DialogManager = NewManager(func(entity Dialog) string { return entity.Name })
|
var DialogManager = inkwell.NewManager[Dialog]()
|
||||||
|
|||||||
+1
-1
@@ -6,4 +6,4 @@ import (
|
|||||||
|
|
||||||
type Item = inkwell.Item
|
type Item = inkwell.Item
|
||||||
|
|
||||||
var ItemManager = NewManager(func(entity Item) string { return entity.Name })
|
var ItemManager = inkwell.NewManager[Item]()
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
package inc
|
|
||||||
|
|
||||||
type ManagerInterface[T any] interface {
|
|
||||||
Register(entity T)
|
|
||||||
GetByName(name string) (T, bool)
|
|
||||||
GetAll() []T
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ ManagerInterface[Background] = BackgroundManager
|
|
||||||
_ ManagerInterface[Character] = CharacterManager
|
|
||||||
_ ManagerInterface[Dialog] = DialogManager
|
|
||||||
_ ManagerInterface[Item] = ItemManager
|
|
||||||
_ ManagerInterface[Script] = ScriptManager
|
|
||||||
_ ManagerInterface[Scene] = SceneManager
|
|
||||||
_ ManagerInterface[Theme] = ThemeManager
|
|
||||||
)
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package inc
|
|
||||||
|
|
||||||
type Manager[T any] struct {
|
|
||||||
nameOf func(T) string
|
|
||||||
entities []T
|
|
||||||
index map[string]int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewManager[T any](nameOf func(T) string) *Manager[T] {
|
|
||||||
return &Manager[T]{
|
|
||||||
nameOf: nameOf,
|
|
||||||
index: map[string]int{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager[T]) Register(entity T) {
|
|
||||||
name := m.nameOf(entity)
|
|
||||||
if i, ok := m.index[name]; ok {
|
|
||||||
m.entities[i] = entity
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.index[name] = len(m.entities)
|
|
||||||
m.entities = append(m.entities, entity)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager[T]) GetByName(name string) (T, bool) {
|
|
||||||
i, ok := m.index[name]
|
|
||||||
if !ok {
|
|
||||||
var missing T
|
|
||||||
return missing, false
|
|
||||||
}
|
|
||||||
return m.entities[i], true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager[T]) GetAll() []T {
|
|
||||||
return m.entities
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerAll[T any](m *Manager[T], register func(T)) {
|
|
||||||
for _, entity := range m.GetAll() {
|
|
||||||
register(entity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,11 +6,11 @@ import (
|
|||||||
|
|
||||||
type Scene = inkwell.Scene
|
type Scene = inkwell.Scene
|
||||||
|
|
||||||
var SceneManager = NewManager(func(entity Scene) string { return entity.Name })
|
var SceneManager = inkwell.NewManager[Scene]()
|
||||||
|
|
||||||
func registerScene() {
|
func registerScene() {
|
||||||
fillSelectorPins()
|
fillSelectorPins()
|
||||||
registerAll(SceneManager, func(entity Scene) {
|
SceneManager.Each(func(entity Scene) {
|
||||||
World.G.SceneManager.Register(sceneDefaults(entity))
|
World.G.SceneManager.Register(sceneDefaults(entity))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fillSelectorPins() {
|
func fillSelectorPins() {
|
||||||
selector, ok := SceneManager.GetByName(SceneSelector)
|
selector, ok := SceneManager.Get(SceneSelector)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var exits []inkwell.Exit
|
var exits []inkwell.Exit
|
||||||
for _, entity := range SceneManager.GetAll() {
|
for _, entity := range SceneManager.All() {
|
||||||
if !leadsToSelector(entity) {
|
if !leadsToSelector(entity) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ func fillSelectorPins() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
selector.Exits = exits
|
selector.Exits = exits
|
||||||
SceneManager.Register(selector)
|
SceneManager.Set(selector)
|
||||||
}
|
}
|
||||||
|
|
||||||
func leadsToSelector(s Scene) bool {
|
func leadsToSelector(s Scene) bool {
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import (
|
|||||||
|
|
||||||
type Script = inkwell.Script
|
type Script = inkwell.Script
|
||||||
|
|
||||||
var ScriptManager = NewManager(func(entity Script) string { return entity.Name })
|
var ScriptManager = inkwell.NewManager[Script]()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Theme = inkwell.Theme
|
type Theme = inkwell.Theme
|
||||||
|
|
||||||
var ThemeManager = NewManager(func(entity Theme) string { return entity.Name })
|
var ThemeManager = inkwell.NewManager[Theme]()
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RealWorld = "realworld-93"
|
RealWorld = "realworld-93"
|
||||||
@@ -34,5 +34,5 @@ func RGBA(hex uint32, a uint8) color.Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func registerTheme() {
|
func registerTheme() {
|
||||||
registerAll(ThemeManager, World.G.ThemeManager.Register)
|
ThemeManager.Each(World.G.ThemeManager.Register)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user