@skewedaspect/sage 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/Readme.md +53 -0
  3. package/dist/classes/bindings/toggle.d.ts +122 -0
  4. package/dist/classes/bindings/trigger.d.ts +79 -0
  5. package/dist/classes/bindings/value.d.ts +104 -0
  6. package/dist/classes/entity.d.ts +83 -0
  7. package/dist/classes/eventBus.d.ts +94 -0
  8. package/dist/classes/gameEngine.d.ts +57 -0
  9. package/dist/classes/input/gamepad.d.ts +94 -0
  10. package/dist/classes/input/keyboard.d.ts +66 -0
  11. package/dist/classes/input/mouse.d.ts +80 -0
  12. package/dist/classes/input/readers/gamepad.d.ts +77 -0
  13. package/dist/classes/input/readers/keyboard.d.ts +60 -0
  14. package/dist/classes/input/readers/mouse.d.ts +45 -0
  15. package/dist/classes/loggers/consoleBackend.d.ts +29 -0
  16. package/dist/classes/loggers/nullBackend.d.ts +14 -0
  17. package/dist/engines/scene.d.ts +11 -0
  18. package/dist/interfaces/action.d.ts +20 -0
  19. package/dist/interfaces/binding.d.ts +144 -0
  20. package/dist/interfaces/entity.d.ts +9 -0
  21. package/dist/interfaces/game.d.ts +26 -0
  22. package/dist/interfaces/input.d.ts +181 -0
  23. package/dist/interfaces/logger.d.ts +88 -0
  24. package/dist/managers/binding.d.ts +185 -0
  25. package/dist/managers/entity.d.ts +70 -0
  26. package/dist/managers/game.d.ts +20 -0
  27. package/dist/managers/input.d.ts +56 -0
  28. package/dist/managers/level.d.ts +55 -0
  29. package/dist/sage.d.ts +20 -0
  30. package/dist/sage.es.js +2208 -0
  31. package/dist/sage.es.js.map +1 -0
  32. package/dist/sage.umd.js +2 -0
  33. package/dist/sage.umd.js.map +1 -0
  34. package/dist/utils/capabilities.d.ts +2 -0
  35. package/dist/utils/graphics.d.ts +10 -0
  36. package/dist/utils/logger.d.ts +66 -0
  37. package/dist/utils/physics.d.ts +2 -0
  38. package/dist/utils/version.d.ts +5 -0
  39. package/docs/architecture.md +129 -0
  40. package/docs/behaviors.md +706 -0
  41. package/docs/binding_system.md +820 -0
  42. package/docs/design/input.md +86 -0
  43. package/docs/entity_system.md +538 -0
  44. package/docs/eventbus.md +225 -0
  45. package/docs/getting_started.md +264 -0
  46. package/docs/images/sage_logo.png +0 -0
  47. package/docs/images/sage_logo_shape.png +0 -0
  48. package/docs/overview.md +38 -0
  49. package/docs/physics_system.md +686 -0
  50. package/docs/scene_system.md +513 -0
  51. package/package.json +69 -0
  52. package/src/classes/bindings/toggle.ts +261 -0
  53. package/src/classes/bindings/trigger.ts +211 -0
  54. package/src/classes/bindings/value.ts +227 -0
  55. package/src/classes/entity.ts +256 -0
  56. package/src/classes/eventBus.ts +259 -0
  57. package/src/classes/gameEngine.ts +125 -0
  58. package/src/classes/input/gamepad.ts +388 -0
  59. package/src/classes/input/keyboard.ts +189 -0
  60. package/src/classes/input/mouse.ts +276 -0
  61. package/src/classes/input/readers/gamepad.ts +179 -0
  62. package/src/classes/input/readers/keyboard.ts +123 -0
  63. package/src/classes/input/readers/mouse.ts +133 -0
  64. package/src/classes/loggers/consoleBackend.ts +135 -0
  65. package/src/classes/loggers/nullBackend.ts +51 -0
  66. package/src/engines/scene.ts +112 -0
  67. package/src/images/sage_logo.svg +172 -0
  68. package/src/images/sage_logo_shape.svg +146 -0
  69. package/src/interfaces/action.ts +30 -0
  70. package/src/interfaces/binding.ts +191 -0
  71. package/src/interfaces/entity.ts +21 -0
  72. package/src/interfaces/game.ts +44 -0
  73. package/src/interfaces/input.ts +221 -0
  74. package/src/interfaces/logger.ts +118 -0
  75. package/src/managers/binding.ts +729 -0
  76. package/src/managers/entity.ts +252 -0
  77. package/src/managers/game.ts +111 -0
  78. package/src/managers/input.ts +233 -0
  79. package/src/managers/level.ts +261 -0
  80. package/src/sage.ts +119 -0
  81. package/src/types/global.d.ts +11 -0
  82. package/src/utils/capabilities.ts +16 -0
  83. package/src/utils/graphics.ts +148 -0
  84. package/src/utils/logger.ts +225 -0
  85. package/src/utils/physics.ts +16 -0
  86. package/src/utils/version.ts +11 -0
@@ -0,0 +1,729 @@
1
+ //----------------------------------------------------------------------------------------------------------------------
2
+ // Binding Manager
3
+ //----------------------------------------------------------------------------------------------------------------------
4
+
5
+ import { GameEventBus } from '../classes/eventBus.ts';
6
+
7
+ // Interfaces
8
+ import { type Binding, type BindingDefinition, type Context, bindingTypes } from '../interfaces/binding.ts';
9
+ import type {
10
+ DeviceValueReader,
11
+ DeviceValueReaderDefinition,
12
+ InputDevice,
13
+ InputState,
14
+ } from '../interfaces/input.ts';
15
+ import type { LoggerInterface } from '../interfaces/logger.ts';
16
+ import type { Action } from '../interfaces/action.ts';
17
+
18
+ // Bindings
19
+ import { TriggerBinding } from '../classes/bindings/trigger.ts';
20
+ import { ToggleBinding } from '../classes/bindings/toggle.ts';
21
+ import { ValueBinding } from '../classes/bindings/value.ts';
22
+
23
+ // Value Readers
24
+ import { KeyboardValueReader } from '../classes/input/readers/keyboard.ts';
25
+ import { MouseValueReader } from '../classes/input/readers/mouse.ts';
26
+ import { GamepadValueReader } from '../classes/input/readers/gamepad.ts';
27
+
28
+ // Utils
29
+ import { LoggingUtility, SAGELogger } from '../utils/logger.ts';
30
+
31
+ //----------------------------------------------------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Configuration object structure for binding manager
35
+ */
36
+ export interface BindingConfiguration
37
+ {
38
+ /** All registered actions */
39
+ actions : Action[];
40
+
41
+ /** All registered bindings */
42
+ bindings : BindingDefinition[];
43
+
44
+ /** All registered contexts */
45
+ contexts : {
46
+ name : string;
47
+ exclusive : boolean;
48
+ active : boolean;
49
+ }[];
50
+ }
51
+
52
+ /* eslint-disable no-continue */
53
+
54
+ /**
55
+ * Manages input bindings and actions for the game
56
+ */
57
+ export class BindingManager
58
+ {
59
+ /** Map of device IDs to their bindings */
60
+ private _bindings = new Map<string, Binding[]>();
61
+
62
+ /** Map of action names to their action definitions */
63
+ private _actions = new Map<string, Action>();
64
+
65
+ /** Map of all registered contexts by name for O(1) lookup */
66
+ private _contexts = new Map<string, Context>();
67
+
68
+ /**
69
+ * Set of all active contexts (both exclusive and non-exclusive)
70
+ */
71
+ private _activeContexts = new Set<string>();
72
+
73
+ /** Event bus for handling game events */
74
+ private _eventBus : GameEventBus;
75
+
76
+ /** Logger instance */
77
+ private _log : LoggerInterface;
78
+
79
+ /**
80
+ * Creates an instance of BindingManager.
81
+ *
82
+ * @param eventBus - The game event bus to publish events to
83
+ * @param logger - The logging utility to use
84
+ */
85
+ constructor(eventBus : GameEventBus, logger ?: LoggingUtility)
86
+ {
87
+ this._eventBus = eventBus;
88
+
89
+ // Bind to input events
90
+ this._eventBus.subscribe<{ device : InputDevice, state : InputState }>('input:changed', (event) =>
91
+ {
92
+ if(event.payload)
93
+ {
94
+ this.$handleInput(event.payload.device, event.payload.state);
95
+ }
96
+ });
97
+
98
+ this._log = logger?.getLogger('BindingManager') || new SAGELogger('BindingManager');
99
+ this._log.debug('BindingManager initialized');
100
+ }
101
+
102
+ //------------------------------------------------------------------------------------------------------------------
103
+ // Private Methods
104
+ //------------------------------------------------------------------------------------------------------------------
105
+
106
+ /**
107
+ * Checks if a binding's context is currently active
108
+ *
109
+ * @param binding - The binding to check
110
+ * @returns True if binding's context is active or has no context
111
+ */
112
+ private _isBindingContextActive(binding : Binding) : boolean
113
+ {
114
+ // If no context is specified, binding is always active regardless of active contexts
115
+ if(!binding.context)
116
+ {
117
+ return true;
118
+ }
119
+
120
+ // Otherwise check if the binding's specific context is active
121
+ return this._activeContexts.has(binding.context);
122
+ }
123
+
124
+ /**
125
+ * Get a context by name, creating it if it doesn't exist
126
+ *
127
+ * @param contextName - The name of the context to get or create
128
+ * @param exclusive - Whether the context is exclusive (only used if creating)
129
+ * @returns The context object
130
+ */
131
+ private _getOrCreateContext(contextName : string, exclusive = true) : Context
132
+ {
133
+ let context = this._contexts.get(contextName);
134
+
135
+ if(!context)
136
+ {
137
+ context = {
138
+ name: contextName,
139
+ exclusive,
140
+ };
141
+
142
+ this._contexts.set(contextName, context);
143
+ this._log.debug(`Auto-created context "${ contextName }" (exclusive: ${ exclusive })`);
144
+ }
145
+
146
+ return context;
147
+ }
148
+
149
+ /**
150
+ * Deactivate all exclusive contexts except the specified one
151
+ *
152
+ * @param exceptContextName - Name of context to not deactivate
153
+ * @returns Array of deactivated context names
154
+ */
155
+ private _deactivateExclusiveContexts(exceptContextName ?: string) : string[]
156
+ {
157
+ const deactivatedContexts : string[] = [];
158
+
159
+ // Find all active exclusive contexts
160
+ for(const contextName of this._activeContexts)
161
+ {
162
+ // Skip the context we want to keep
163
+ if(contextName === exceptContextName)
164
+ {
165
+ continue;
166
+ }
167
+
168
+ const context = this._contexts.get(contextName);
169
+
170
+ // If this is an exclusive context, deactivate it
171
+ if(context?.exclusive)
172
+ {
173
+ this._activeContexts.delete(contextName);
174
+ deactivatedContexts.push(contextName);
175
+ }
176
+ }
177
+
178
+ return deactivatedContexts;
179
+ }
180
+
181
+ /**
182
+ * Create a binding from a binding definition
183
+ *
184
+ * @param definition - The binding definition to create a binding from
185
+ * @returns A new binding instance or null if the type is not supported
186
+ */
187
+ private _createBindingFromDefinition(definition : BindingDefinition) : Binding | null
188
+ {
189
+ // Get the full action object from the action name
190
+ const action = this._actions.get(definition.action);
191
+
192
+ // If the action doesn't exist, log a warning and return null
193
+ if(!action)
194
+ {
195
+ this._log.warn(`Cannot create binding: Action "${ definition.action }" not found.`);
196
+ return null;
197
+ }
198
+
199
+ // Extract deviceID and create reader from the input definition
200
+ const { deviceID, ...readerDef } = definition.input;
201
+
202
+ switch (definition.type)
203
+ {
204
+ case 'trigger': {
205
+ // Create the binding with the device value reader based on definition's source
206
+ return new TriggerBinding(
207
+ action,
208
+ deviceID,
209
+ this._createInputSourceFromDefinition(readerDef),
210
+ {
211
+ ...(definition.options || {}),
212
+ context: definition.context,
213
+ }
214
+ );
215
+ }
216
+
217
+ case 'toggle': {
218
+ // Create the binding with the device value reader based on definition's source
219
+ return new ToggleBinding(
220
+ action,
221
+ deviceID,
222
+ this._createInputSourceFromDefinition(readerDef),
223
+ {
224
+ ...(definition.options || {}),
225
+ context: definition.context,
226
+ }
227
+ );
228
+ }
229
+
230
+ case 'value': {
231
+ // Create the binding with the device value reader based on definition's source
232
+ return new ValueBinding(
233
+ action,
234
+ deviceID,
235
+ this._createInputSourceFromDefinition(readerDef),
236
+ {
237
+ ...(definition.options || {}),
238
+ context: definition.context,
239
+ }
240
+ );
241
+ }
242
+
243
+ default:
244
+ this._log.error(`Binding type not implemented: ${ (definition as any).type }`);
245
+ return null;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Create a device value reader from a definition object
251
+ *
252
+ * @param definition - The device value reader definition
253
+ * @returns A new device value reader instance
254
+ * @throws Error if the reader type is not supported
255
+ */
256
+ private _createInputSourceFromDefinition(definition : DeviceValueReaderDefinition) : DeviceValueReader
257
+ {
258
+ // Create reader based on device type
259
+ switch (definition.type)
260
+ {
261
+ case 'keyboard':
262
+ return new KeyboardValueReader(
263
+ definition.sourceKey,
264
+ definition.options
265
+ );
266
+
267
+ case 'mouse':
268
+ {
269
+ // Check if sourceType is valid
270
+ const sourceType = definition.sourceType;
271
+ if(!(sourceType === 'button' || sourceType === 'position' || sourceType === 'wheel'))
272
+ {
273
+ throw new Error(`Invalid mouse source type: ${ sourceType }`);
274
+ }
275
+
276
+ return new MouseValueReader(
277
+ sourceType,
278
+ definition.sourceKey
279
+ );
280
+ }
281
+
282
+ case 'gamepad':
283
+ {
284
+ // Check if sourceType is valid
285
+ const sourceType = definition.sourceType;
286
+ if(!(sourceType === 'button' || sourceType === 'axis'))
287
+ {
288
+ throw new Error(`Invalid gamepad source type: ${ sourceType }`);
289
+ }
290
+ return new GamepadValueReader(
291
+ sourceType,
292
+ definition.sourceKey,
293
+ definition.options
294
+ );
295
+ }
296
+
297
+ default:
298
+ throw new Error(`Unsupported input source type: ${ (definition as any).type }`);
299
+ }
300
+ }
301
+
302
+ //------------------------------------------------------------------------------------------------------------------
303
+ // Internal API
304
+ //------------------------------------------------------------------------------------------------------------------
305
+
306
+ /**
307
+ * Handle input from a device and process all relevant bindings
308
+ *
309
+ * @param device - The input device
310
+ * @param state - Current input state
311
+ */
312
+ public $handleInput(device : InputDevice, state : InputState) : void
313
+ {
314
+ const bindings = this._bindings.get(device.id);
315
+ if(!bindings || bindings.length === 0)
316
+ {
317
+ return;
318
+ }
319
+
320
+ if(this._activeContexts.size === 0 && bindings.some((binding) => binding.context))
321
+ {
322
+ return;
323
+ }
324
+
325
+ for(const binding of bindings)
326
+ {
327
+ if(!this._isBindingContextActive(binding))
328
+ {
329
+ continue;
330
+ }
331
+
332
+ binding.process(state, this._eventBus);
333
+ }
334
+ }
335
+
336
+ //------------------------------------------------------------------------------------------------------------------
337
+ // Action Management API
338
+ //------------------------------------------------------------------------------------------------------------------
339
+
340
+ /**
341
+ * Registers an action
342
+ *
343
+ * @param action - The action to register
344
+ * @throws Error if action is already registered
345
+ */
346
+ public registerAction(action : Action) : void
347
+ {
348
+ this._log.debug(`Registering action "${ action.name }"`);
349
+
350
+ if(this._actions.has(action.name))
351
+ {
352
+ const errorMsg = `Action "${ action.name }" already registered.`;
353
+ this._log.error(errorMsg);
354
+ throw new Error(errorMsg);
355
+ }
356
+
357
+ this._actions.set(action.name, action);
358
+ this._log.debug(`Action "${ action.name }" registered successfully`);
359
+ }
360
+
361
+ /**
362
+ * Gets an action by name
363
+ *
364
+ * @param actionName - The name of the action to get
365
+ * @returns The action or null if not found
366
+ */
367
+ public getAction(actionName : string) : Action | null
368
+ {
369
+ this._log.trace(`Getting action "${ actionName }"`);
370
+
371
+ const action = this._actions.get(actionName) ?? null;
372
+ if(!action)
373
+ {
374
+ this._log.debug(`Action "${ actionName }" not found`);
375
+ }
376
+
377
+ return action;
378
+ }
379
+
380
+ //------------------------------------------------------------------------------------------------------------------
381
+ // Context Management API
382
+ //------------------------------------------------------------------------------------------------------------------
383
+
384
+ /**
385
+ * Registers a context with specific options
386
+ *
387
+ * @param contextName - The name of the context to register
388
+ * @param exclusive - Whether the context is exclusive (default: true)
389
+ * @returns The registered context
390
+ */
391
+ public registerContext(contextName : string, exclusive = true) : Context
392
+ {
393
+ const context = this._getOrCreateContext(contextName, exclusive);
394
+
395
+ // Update context's exclusivity if it already exists
396
+ if(context.exclusive !== exclusive)
397
+ {
398
+ context.exclusive = exclusive;
399
+ this._log.info(`Updated context "${ contextName }" exclusivity: ${ exclusive }`);
400
+ }
401
+
402
+ return context;
403
+ }
404
+
405
+ /**
406
+ * Activates a context, enabling all bindings associated with it.
407
+ * If the context is exclusive, all other exclusive contexts will be deactivated.
408
+ *
409
+ * @param contextName - The name of the context to activate
410
+ */
411
+ public activateContext(contextName : string) : void
412
+ {
413
+ // Get or create context
414
+ const context = this._getOrCreateContext(contextName);
415
+
416
+ // Get the context's exclusivity setting
417
+ const isExclusive = context.exclusive;
418
+
419
+ this._log.debug(`Activating context "${ contextName }" (exclusive: ${ isExclusive })`);
420
+
421
+ // If already active, nothing to do unless we're making an exclusive activation
422
+ const isAlreadyActive = this._activeContexts.has(contextName);
423
+
424
+ if(isExclusive)
425
+ {
426
+ // Deactivate all other exclusive contexts
427
+ const deactivated = this._deactivateExclusiveContexts(contextName);
428
+
429
+ if(deactivated.length > 0)
430
+ {
431
+ this._log.info(`Deactivated exclusive contexts: ${ deactivated.join(', ') }`);
432
+ }
433
+ }
434
+
435
+ // Add to active contexts if not already there
436
+ if(!isAlreadyActive)
437
+ {
438
+ this._activeContexts.add(contextName);
439
+ this._log.info(`Context "${ contextName }" activated${ isExclusive ? ' as exclusive' : '' }`);
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Deactivates a context, disabling all bindings associated with it
445
+ *
446
+ * @param contextName - The name of the context to deactivate
447
+ */
448
+ public deactivateContext(contextName : string) : void
449
+ {
450
+ this._log.debug(`Deactivating context "${ contextName }"`);
451
+
452
+ if(this._activeContexts.has(contextName))
453
+ {
454
+ this._activeContexts.delete(contextName);
455
+ this._log.info(`Context "${ contextName }" deactivated`);
456
+ }
457
+ else
458
+ {
459
+ this._log.debug(`Context "${ contextName }" was not active`);
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Returns a list of all active contexts
465
+ *
466
+ * @returns Array of active context names
467
+ */
468
+ public getActiveContexts() : string[]
469
+ {
470
+ return [ ...this._activeContexts ];
471
+ }
472
+
473
+ /**
474
+ * Returns whether a context is active
475
+ *
476
+ * @param contextName - The context to check
477
+ * @returns True if the context is active
478
+ */
479
+ public isContextActive(contextName : string) : boolean
480
+ {
481
+ return this._activeContexts.has(contextName);
482
+ }
483
+
484
+ /**
485
+ * Gets a context by name
486
+ *
487
+ * @param contextName - The context name to get
488
+ * @returns The context or null if not found
489
+ */
490
+ public getContext(contextName : string) : Context | null
491
+ {
492
+ return this._contexts.get(contextName) || null;
493
+ }
494
+
495
+ //------------------------------------------------------------------------------------------------------------------
496
+ // Binding Management API
497
+ //------------------------------------------------------------------------------------------------------------------
498
+
499
+ /**
500
+ * Register a binding for an input device
501
+ *
502
+ * @param binding - The binding to register
503
+ * @throws Error if binding type is invalid
504
+ */
505
+ public $registerBinding(binding : Binding) : void
506
+ {
507
+ if(!bindingTypes.includes(binding.type))
508
+ {
509
+ throw new Error(`Invalid binding type: ${ binding.type }`);
510
+ }
511
+
512
+ // If binding has a context but context isn't registered yet, register it with default settings
513
+ if(binding.context && !this._contexts.has(binding.context))
514
+ {
515
+ this.registerContext(binding.context);
516
+ }
517
+
518
+ // Initialize device bindings array if it doesn't exist
519
+ if(!this._bindings.has(binding.deviceID))
520
+ {
521
+ this._bindings.set(binding.deviceID, []);
522
+ }
523
+
524
+ // Add the binding
525
+ this._bindings.get(binding.deviceID)?.push(binding);
526
+ this._log.debug(`Registered ${ binding.type } binding for "${ binding.action.name }" in context `
527
+ + `"${ binding.context || null }"`);
528
+ }
529
+
530
+ /**
531
+ * Register a binding using a binding definition object
532
+ *
533
+ * @param definition - The binding definition to register
534
+ */
535
+ public registerBinding(definition : BindingDefinition) : void
536
+ {
537
+ const binding = this._createBindingFromDefinition(definition);
538
+
539
+ if(binding)
540
+ {
541
+ this.$registerBinding(binding);
542
+ }
543
+ else
544
+ {
545
+ this._log.error(`Failed to create binding for action "${ definition.action }" with type `
546
+ + `"${ definition.type }"`);
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Unregister all bindings for an action within a context
552
+ *
553
+ * @param actionName - The name of the action
554
+ * @param context - The context to unregister from (defaults to null)
555
+ */
556
+ public unregisterBindings(actionName : string, context : string | null = null) : void
557
+ {
558
+ this._log.debug(`Unregistering all bindings for action "${ actionName }" in context "${ context }"`);
559
+
560
+ let bindingsRemoved = 0;
561
+
562
+ // Iterate through all devices and their bindings
563
+ for(const [ deviceId, deviceBindings ] of this._bindings.entries())
564
+ {
565
+ const newBindings = deviceBindings.filter((binding) =>
566
+ {
567
+ const bindingContext = binding.context || null;
568
+ const shouldKeep = binding.action.name !== actionName || bindingContext !== context;
569
+ if(!shouldKeep)
570
+ {
571
+ bindingsRemoved++;
572
+ }
573
+ return shouldKeep;
574
+ });
575
+
576
+ // Only update the map if we actually removed something
577
+ if(newBindings.length !== deviceBindings.length)
578
+ {
579
+ this._bindings.set(deviceId, newBindings);
580
+ }
581
+ }
582
+
583
+ this._log.info(`Removed ${ bindingsRemoved } bindings for action "${ actionName }" in context "${ context }"`);
584
+ }
585
+
586
+ /**
587
+ * Gets all bindings for a specific action
588
+ *
589
+ * @param actionName - The name of the action
590
+ * @param context - The context to get bindings from (optional)
591
+ * @returns Array of bindings that match the criteria
592
+ */
593
+ public getBindingsForAction(actionName : string, context ?: string | null) : Binding[]
594
+ {
595
+ const result : Binding[] = [];
596
+
597
+ for(const deviceBindings of this._bindings.values())
598
+ {
599
+ for(const binding of deviceBindings)
600
+ {
601
+ const bindingContext = binding.context || null;
602
+ if(binding.action.name === actionName)
603
+ {
604
+ // If context specified, only include bindings from that context
605
+ if(!context || bindingContext === context)
606
+ {
607
+ result.push(binding);
608
+ }
609
+ }
610
+ }
611
+ }
612
+
613
+ return result;
614
+ }
615
+
616
+ //------------------------------------------------------------------------------------------------------------------
617
+ // Configuration Management API
618
+ //------------------------------------------------------------------------------------------------------------------
619
+
620
+ /**
621
+ * Exports the current configuration to a serializable object
622
+ *
623
+ * @returns A BindingConfiguration object representing the current state
624
+ */
625
+ public exportConfiguration() : BindingConfiguration
626
+ {
627
+ this._log.debug('Exporting binding configuration');
628
+
629
+ // Export all registered actions
630
+ const actions = [ ...this._actions.values() ].map((action) =>
631
+ {
632
+ // Ensure we include all analog action properties when exporting
633
+ if(action.type === 'analog')
634
+ {
635
+ return {
636
+ name: action.name,
637
+ type: action.type,
638
+ minValue: action.minValue ?? 0,
639
+ maxValue: action.maxValue ?? 1,
640
+ };
641
+ }
642
+ return action;
643
+ });
644
+
645
+ // Export all registered bindings using their toJSON methods
646
+ const bindings : BindingDefinition[] = [];
647
+
648
+ // Iterate through all device bindings
649
+ for(const deviceBindings of this._bindings.values())
650
+ {
651
+ for(const binding of deviceBindings)
652
+ {
653
+ // Let the binding serialize itself to the BindingDefinition format
654
+ bindings.push(binding.toJSON());
655
+ }
656
+ }
657
+
658
+ // Export all contexts with their active state
659
+ const contexts = [ ...this._contexts.values() ].map((context) => ({
660
+ name: context.name,
661
+ exclusive: context.exclusive,
662
+ active: this._activeContexts.has(context.name),
663
+ }));
664
+
665
+ this._log.info(`Configuration exported: ${ actions.length } actions, ${ bindings.length } bindings, `
666
+ + `${ contexts.length } contexts`);
667
+
668
+ return {
669
+ actions,
670
+ bindings,
671
+ contexts,
672
+ };
673
+ }
674
+
675
+ /**
676
+ * Imports a configuration from an object
677
+ *
678
+ * @param config - The configuration to import
679
+ */
680
+ public importConfiguration(config : BindingConfiguration) : void
681
+ {
682
+ this._log.debug('Importing binding configuration');
683
+
684
+ // Clear existing configuration
685
+ this._bindings.clear();
686
+ this._actions.clear();
687
+ this._contexts.clear();
688
+ this._activeContexts.clear();
689
+
690
+ // Import actions
691
+ for(const action of config.actions)
692
+ {
693
+ this.registerAction(action);
694
+ }
695
+
696
+ // Import contexts first so they exist before bindings reference them
697
+ for(const contextData of config.contexts)
698
+ {
699
+ this.registerContext(contextData.name, contextData.exclusive);
700
+
701
+ // Activate contexts that were active
702
+ if(contextData.active)
703
+ {
704
+ this.activateContext(contextData.name);
705
+ }
706
+ }
707
+
708
+ // Import bindings
709
+ for(const bindingDef of config.bindings)
710
+ {
711
+ try
712
+ {
713
+ this.registerBinding(bindingDef);
714
+ }
715
+ catch (err : unknown)
716
+ {
717
+ if(err instanceof Error)
718
+ {
719
+ this._log.error(`Failed to import binding for action "${ bindingDef.action }": ${ err.message }`);
720
+ }
721
+ }
722
+ }
723
+
724
+ this._log.info(`Configuration imported: ${ config.actions.length } actions, `
725
+ + `${ config.bindings.length } bindings, ${ config.contexts.length } contexts`);
726
+ }
727
+ }
728
+
729
+ //----------------------------------------------------------------------------------------------------------------------