Skip to content

Commit 14cf416

Browse files
author
DavidQ
committed
Add overlay plugin registry.
PR Details: - Enables structured overlay registration
1 parent d136f50 commit 14cf416

7 files changed

Lines changed: 297 additions & 24 deletions

File tree

docs/dev/CODEX_COMMANDS.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ MODEL: GPT-5.4
22
REASONING: medium
33

44
COMMAND:
5-
Create overlay expansion framework:
6-
- Define extension points
7-
- Ensure compatibility with existing overlay system
8-
- Add minimal testable example
5+
Implement overlay plugin registry:
6+
- Add registration system
7+
- Validate with one plugin
98
- Update roadmap status only
10-
11-
Package ZIP to <project folder>/tmp/
9+
- Package ZIP

docs/dev/COMMIT_COMMENT.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
Introduce overlay system expansion framework.
1+
Add overlay plugin registry.
22

33
PR Details:
4-
- Enables future overlay extensibility
5-
- Maintains compatibility with Level 19 baseline
4+
- Enables structured overlay registration
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[ ] Extension works
2-
[ ] No regressions
3-
[ ] Gameplay unaffected
1+
[ ] Plugin registers
2+
[ ] Plugin activates
3+
[ ] Plugin removes cleanly
44
[ ] Roadmap updated

docs/dev/roadmaps/MASTER_ROADMAP_HIGH_LEVEL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,7 @@
818818
- [ ] confirm no regression across phases
819819

820820
### Track G — Extensibility Readiness
821-
- [.] validate plugin/extension patterns
821+
- [x] validate plugin/extension patterns
822822
- [ ] validate adding new systems is clean
823823
- [ ] validate external integration points
824824
- [ ] ensure future phases can build cleanly

docs/pr/BUILD_PR.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
1-
# BUILD_PR_LEVEL_20_1_OVERLAY_SYSTEM_EXPANSION
1+
# BUILD_PR_LEVEL_20_2_OVERLAY_PLUGIN_REGISTRY
22

33
## Purpose
4-
Begin Level 20 by introducing a controlled expansion framework for new overlay types.
4+
Introduce a registry for managing overlay plugins.
55

66
## Roadmap Improvement
7-
Transitions from stable baseline (Level 19) to extensible overlay architecture.
7+
Enables structured registration and discovery of overlays.
88

99
## Scope
10-
- Define extension points for new overlays
11-
- Ensure compatibility with existing systems
12-
- Validate one sample extension
10+
- Define plugin registry
11+
- Allow registration/unregistration
12+
- Validate with one plugin
1313

1414
## Test Steps
15-
1. Load gameplay
16-
2. Register new overlay via extension
17-
3. Validate rendering + interaction
15+
1. Register plugin
16+
2. Activate overlay
17+
3. Remove plugin
1818

1919
## Expected
20-
- New overlays plug in cleanly
21-
- No regression
20+
- Plugins managed cleanly
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
createPhase19OverlayPluginRegistry.js
6+
*/
7+
import createPhase19OverlayExpansionFramework from '/samples/phase-19/shared/overlay/createPhase19OverlayExpansionFramework.js';
8+
9+
function normalizePluginId(pluginId) {
10+
return String(pluginId || '').trim();
11+
}
12+
13+
function normalizePluginDefinition(plugin) {
14+
if (!plugin || typeof plugin !== 'object') {
15+
throw new Error('Overlay plugin registration requires a plugin object.');
16+
}
17+
18+
const id = normalizePluginId(plugin.id);
19+
if (!id) {
20+
throw new Error('Overlay plugin requires a non-empty id.');
21+
}
22+
23+
const createOverlayExtension = typeof plugin.createOverlayExtension === 'function'
24+
? plugin.createOverlayExtension
25+
: null;
26+
const extension = plugin.extension && typeof plugin.extension === 'object'
27+
? plugin.extension
28+
: null;
29+
30+
if (!createOverlayExtension && !extension) {
31+
throw new Error(`Overlay plugin "${id}" requires createOverlayExtension() or extension.`);
32+
}
33+
34+
return Object.freeze({
35+
id,
36+
version: String(plugin.version || '').trim(),
37+
metadata: Object.freeze({ ...(plugin.metadata || {}) }),
38+
createOverlayExtension,
39+
extension,
40+
});
41+
}
42+
43+
function normalizePluginExtension(plugin, candidateExtension) {
44+
const extension = candidateExtension && typeof candidateExtension === 'object'
45+
? candidateExtension
46+
: null;
47+
if (!extension) {
48+
throw new Error(`Overlay plugin "${plugin.id}" returned an invalid extension.`);
49+
}
50+
const extensionId = normalizePluginId(extension.id) || `${plugin.id}.extension`;
51+
return {
52+
...extension,
53+
id: extensionId,
54+
};
55+
}
56+
57+
export default function createPhase19OverlayPluginRegistry({
58+
expansionFramework = createPhase19OverlayExpansionFramework(),
59+
plugins = [],
60+
} = {}) {
61+
const pluginMap = new Map();
62+
const pluginExtensionMap = new Map();
63+
let api = null;
64+
65+
function registerPlugin(plugin, context = {}) {
66+
const normalizedPlugin = normalizePluginDefinition(plugin);
67+
const pluginId = normalizedPlugin.id;
68+
69+
const resolvedExtension = normalizePluginExtension(
70+
normalizedPlugin,
71+
normalizedPlugin.createOverlayExtension
72+
? normalizedPlugin.createOverlayExtension({
73+
pluginId,
74+
registry: api,
75+
expansionFramework,
76+
...context,
77+
})
78+
: normalizedPlugin.extension
79+
);
80+
81+
const existingExtensionId = pluginExtensionMap.get(pluginId);
82+
if (existingExtensionId) {
83+
expansionFramework.unregisterExtension(existingExtensionId);
84+
}
85+
86+
const registeredExtension = expansionFramework.registerExtension(resolvedExtension);
87+
pluginMap.set(pluginId, normalizedPlugin);
88+
pluginExtensionMap.set(pluginId, registeredExtension.id);
89+
90+
return Object.freeze({
91+
pluginId,
92+
extensionId: registeredExtension.id,
93+
});
94+
}
95+
96+
function unregisterPlugin(pluginId) {
97+
const normalizedPluginId = normalizePluginId(pluginId);
98+
if (!normalizedPluginId || !pluginMap.has(normalizedPluginId)) {
99+
return false;
100+
}
101+
102+
const extensionId = pluginExtensionMap.get(normalizedPluginId);
103+
if (extensionId) {
104+
expansionFramework.unregisterExtension(extensionId);
105+
}
106+
pluginExtensionMap.delete(normalizedPluginId);
107+
pluginMap.delete(normalizedPluginId);
108+
return true;
109+
}
110+
111+
function getPlugin(pluginId) {
112+
const normalizedPluginId = normalizePluginId(pluginId);
113+
if (!normalizedPluginId) {
114+
return null;
115+
}
116+
return pluginMap.get(normalizedPluginId) ?? null;
117+
}
118+
119+
function getPluginExtensionId(pluginId) {
120+
const normalizedPluginId = normalizePluginId(pluginId);
121+
if (!normalizedPluginId) {
122+
return '';
123+
}
124+
return pluginExtensionMap.get(normalizedPluginId) || '';
125+
}
126+
127+
function listPlugins() {
128+
const entries = [];
129+
for (const [pluginId, plugin] of pluginMap.entries()) {
130+
entries.push({
131+
id: pluginId,
132+
version: plugin.version,
133+
extensionId: pluginExtensionMap.get(pluginId) || '',
134+
});
135+
}
136+
return Object.freeze(entries);
137+
}
138+
139+
api = {
140+
registerPlugin,
141+
unregisterPlugin,
142+
getPlugin,
143+
getPluginExtensionId,
144+
listPlugins,
145+
getFramework() {
146+
return expansionFramework;
147+
},
148+
};
149+
150+
if (Array.isArray(plugins)) {
151+
for (let i = 0; i < plugins.length; i += 1) {
152+
registerPlugin(plugins[i]);
153+
}
154+
}
155+
156+
return api;
157+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
Phase19OverlayPluginRegistry.test.mjs
6+
*/
7+
import assert from 'node:assert/strict';
8+
import { LEVEL17_OVERLAY_CYCLE_KEY } from '../../samples/phase-17/shared/overlayCycleInput.js';
9+
import {
10+
renderOverlayGameplayRuntime,
11+
stepOverlayGameplayRuntime,
12+
} from '../../samples/phase-17/shared/overlayGameplayRuntime.js';
13+
import { definePhase19OverlayExtension } from '../../samples/phase-19/shared/overlay/createPhase19OverlayExpansionFramework.js';
14+
import createPhase19OverlayPluginRegistry from '../../samples/phase-19/shared/overlay/createPhase19OverlayPluginRegistry.js';
15+
16+
function createRendererProbe(width = 960, height = 540) {
17+
return {
18+
getCanvasSize() {
19+
return { width, height };
20+
},
21+
clear() {},
22+
drawRect() {},
23+
strokeRect() {},
24+
drawText() {},
25+
drawLine() {},
26+
drawPolygon() {},
27+
drawImageFrame() {},
28+
};
29+
}
30+
31+
function assertPluginRegistrationAndRuntimeCompatibility() {
32+
const counters = { step: 0, render: 0 };
33+
const registry = createPhase19OverlayPluginRegistry();
34+
const result = registry.registerPlugin({
35+
id: 'phase19.runtime.plugin',
36+
version: '1.0.0',
37+
metadata: { owner: 'runtime-test' },
38+
createOverlayExtension() {
39+
return definePhase19OverlayExtension({
40+
id: 'phase19.runtime.plugin.overlay',
41+
overlays: [
42+
{ id: 'ui-layer', label: 'UI Layer' },
43+
{ id: 'plugin-runtime', label: 'Plugin Runtime' },
44+
],
45+
initialOverlayId: 'ui-layer',
46+
persistenceKey: 'phase19:plugin:overlay-index',
47+
cycleKey: LEVEL17_OVERLAY_CYCLE_KEY,
48+
runtimeExtensions: [
49+
{
50+
overlayId: 'plugin-runtime',
51+
onStep() {
52+
counters.step += 1;
53+
},
54+
onRender() {
55+
counters.render += 1;
56+
},
57+
},
58+
],
59+
});
60+
},
61+
});
62+
63+
assert.deepEqual(
64+
result,
65+
{ pluginId: 'phase19.runtime.plugin', extensionId: 'phase19.runtime.plugin.overlay' },
66+
'Plugin registry should return plugin id and resolved extension id.'
67+
);
68+
assert.equal(registry.listPlugins().length, 1, 'Plugin registry should track one registered plugin.');
69+
assert.equal(
70+
registry.getPluginExtensionId('phase19.runtime.plugin'),
71+
'phase19.runtime.plugin.overlay',
72+
'Plugin registry should expose extension id mapping.'
73+
);
74+
75+
const framework = registry.getFramework();
76+
assert.deepEqual(
77+
framework.listExtensionIds(),
78+
['phase19.runtime.plugin.overlay'],
79+
'Expansion framework should receive plugin overlay extension registration.'
80+
);
81+
82+
const runtime = framework.createRuntimeForExtension('phase19.runtime.plugin.overlay');
83+
assert.notEqual(runtime, null, 'Registered plugin extension should create overlay runtime.');
84+
assert.equal(runtime.interactionCycleKey, LEVEL17_OVERLAY_CYCLE_KEY, 'Plugin runtime should preserve shared non-Tab cycle key.');
85+
assert.equal(
86+
stepOverlayGameplayRuntime(runtime, { activeOverlayId: 'plugin-runtime' }),
87+
1,
88+
'Plugin runtime extension should execute step hook through existing overlay runtime.'
89+
);
90+
assert.equal(
91+
renderOverlayGameplayRuntime(runtime, {
92+
activeOverlayId: 'plugin-runtime',
93+
renderer: createRendererProbe(),
94+
}),
95+
1,
96+
'Plugin runtime extension should execute render hook through existing overlay runtime.'
97+
);
98+
assert.equal(counters.step, 1, 'Plugin runtime step hook should run exactly once.');
99+
assert.equal(counters.render, 1, 'Plugin runtime render hook should run exactly once.');
100+
}
101+
102+
function assertPluginUnregisterCleansFramework() {
103+
const registry = createPhase19OverlayPluginRegistry();
104+
registry.registerPlugin({
105+
id: 'phase19.cleanup.plugin',
106+
extension: definePhase19OverlayExtension({
107+
id: 'phase19.cleanup.overlay',
108+
overlays: [{ id: 'ui-layer', label: 'UI Layer' }],
109+
}),
110+
});
111+
112+
assert.equal(registry.unregisterPlugin('phase19.cleanup.plugin'), true, 'Registered plugin should be removable.');
113+
assert.equal(registry.unregisterPlugin('phase19.cleanup.plugin'), false, 'Repeated unregister should return false.');
114+
assert.equal(registry.getFramework().getExtension('phase19.cleanup.overlay'), null, 'Plugin removal should remove extension from framework.');
115+
}
116+
117+
export function run() {
118+
assertPluginRegistrationAndRuntimeCompatibility();
119+
assertPluginUnregisterCleansFramework();
120+
}

0 commit comments

Comments
 (0)