@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.
- package/LICENSE +21 -0
- package/Readme.md +53 -0
- package/dist/classes/bindings/toggle.d.ts +122 -0
- package/dist/classes/bindings/trigger.d.ts +79 -0
- package/dist/classes/bindings/value.d.ts +104 -0
- package/dist/classes/entity.d.ts +83 -0
- package/dist/classes/eventBus.d.ts +94 -0
- package/dist/classes/gameEngine.d.ts +57 -0
- package/dist/classes/input/gamepad.d.ts +94 -0
- package/dist/classes/input/keyboard.d.ts +66 -0
- package/dist/classes/input/mouse.d.ts +80 -0
- package/dist/classes/input/readers/gamepad.d.ts +77 -0
- package/dist/classes/input/readers/keyboard.d.ts +60 -0
- package/dist/classes/input/readers/mouse.d.ts +45 -0
- package/dist/classes/loggers/consoleBackend.d.ts +29 -0
- package/dist/classes/loggers/nullBackend.d.ts +14 -0
- package/dist/engines/scene.d.ts +11 -0
- package/dist/interfaces/action.d.ts +20 -0
- package/dist/interfaces/binding.d.ts +144 -0
- package/dist/interfaces/entity.d.ts +9 -0
- package/dist/interfaces/game.d.ts +26 -0
- package/dist/interfaces/input.d.ts +181 -0
- package/dist/interfaces/logger.d.ts +88 -0
- package/dist/managers/binding.d.ts +185 -0
- package/dist/managers/entity.d.ts +70 -0
- package/dist/managers/game.d.ts +20 -0
- package/dist/managers/input.d.ts +56 -0
- package/dist/managers/level.d.ts +55 -0
- package/dist/sage.d.ts +20 -0
- package/dist/sage.es.js +2208 -0
- package/dist/sage.es.js.map +1 -0
- package/dist/sage.umd.js +2 -0
- package/dist/sage.umd.js.map +1 -0
- package/dist/utils/capabilities.d.ts +2 -0
- package/dist/utils/graphics.d.ts +10 -0
- package/dist/utils/logger.d.ts +66 -0
- package/dist/utils/physics.d.ts +2 -0
- package/dist/utils/version.d.ts +5 -0
- package/docs/architecture.md +129 -0
- package/docs/behaviors.md +706 -0
- package/docs/binding_system.md +820 -0
- package/docs/design/input.md +86 -0
- package/docs/entity_system.md +538 -0
- package/docs/eventbus.md +225 -0
- package/docs/getting_started.md +264 -0
- package/docs/images/sage_logo.png +0 -0
- package/docs/images/sage_logo_shape.png +0 -0
- package/docs/overview.md +38 -0
- package/docs/physics_system.md +686 -0
- package/docs/scene_system.md +513 -0
- package/package.json +69 -0
- package/src/classes/bindings/toggle.ts +261 -0
- package/src/classes/bindings/trigger.ts +211 -0
- package/src/classes/bindings/value.ts +227 -0
- package/src/classes/entity.ts +256 -0
- package/src/classes/eventBus.ts +259 -0
- package/src/classes/gameEngine.ts +125 -0
- package/src/classes/input/gamepad.ts +388 -0
- package/src/classes/input/keyboard.ts +189 -0
- package/src/classes/input/mouse.ts +276 -0
- package/src/classes/input/readers/gamepad.ts +179 -0
- package/src/classes/input/readers/keyboard.ts +123 -0
- package/src/classes/input/readers/mouse.ts +133 -0
- package/src/classes/loggers/consoleBackend.ts +135 -0
- package/src/classes/loggers/nullBackend.ts +51 -0
- package/src/engines/scene.ts +112 -0
- package/src/images/sage_logo.svg +172 -0
- package/src/images/sage_logo_shape.svg +146 -0
- package/src/interfaces/action.ts +30 -0
- package/src/interfaces/binding.ts +191 -0
- package/src/interfaces/entity.ts +21 -0
- package/src/interfaces/game.ts +44 -0
- package/src/interfaces/input.ts +221 -0
- package/src/interfaces/logger.ts +118 -0
- package/src/managers/binding.ts +729 -0
- package/src/managers/entity.ts +252 -0
- package/src/managers/game.ts +111 -0
- package/src/managers/input.ts +233 -0
- package/src/managers/level.ts +261 -0
- package/src/sage.ts +119 -0
- package/src/types/global.d.ts +11 -0
- package/src/utils/capabilities.ts +16 -0
- package/src/utils/graphics.ts +148 -0
- package/src/utils/logger.ts +225 -0
- package/src/utils/physics.ts +16 -0
- 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
|
+
//----------------------------------------------------------------------------------------------------------------------
|