mani-game-engine 1.0.0-pre.5 → 1.0.0-pre.51

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.
@@ -1,267 +1,380 @@
1
- import {Class, EcsInjector, Signal} from '../index';
2
- import {ID, putIfAbsent} from 'mani-injector';
3
- import {SignalBinding} from 'mani-signal';
4
- import {signalHandlers} from '../ecsInjector';
5
- import {SignalCallback} from 'mani-signal/src/signal';
6
-
7
- export type ScopeSignalOptions = { keepAlive?: boolean | Scope[], group?: string }
8
-
9
- export const scopeSignalHandlers = new Map<Object, [string, ID, ScopeSignalOptions?][]>();
10
-
11
- export const OnScopeSignal = (id: ID, options?: ScopeSignalOptions) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
12
- if (target instanceof Function) {
13
- throw new Error('only allowed on non static methods');
14
- } else {
15
- const mappingList = putIfAbsent(scopeSignalHandlers, target.constructor, (): [string, ID, ScopeSignalOptions?][] => []);
16
- mappingList.push([propertyKey, id, options]);
17
- }
18
- };
19
-
20
- export interface Scope {
21
- [propName: string]: any; // disable weak type detection
22
- onEnter?(): void;
23
- onExit?(): void;
24
- onSubReturn?(): void;
25
- onSubExit?(): void;
26
- onActivate?(): void;
27
- onDeactivate?(): void;
28
- }
29
-
30
- class ScopeSignalBinding {
31
- readonly keepAlive?: boolean | Scope[];
32
- private group?: string;
33
-
34
- constructor(private signalBinding: SignalBinding, options?: ScopeSignalOptions) {
35
- this.keepAlive = options?.keepAlive;
36
- this.group = options?.group;
37
- }
38
-
39
- activate() {
40
- this.signalBinding.setActive(true);
41
- }
42
-
43
- deactivate() {
44
- this.signalBinding.setActive(false);
45
- }
46
-
47
- detach() {
48
- this.signalBinding.detach();
49
- }
50
- }
51
-
52
- class ScopeStack {
53
- readonly stack: ScopeContext[] = [];
54
-
55
- constructor(readonly rootScope: ScopeContext) {
56
- this.stack.push(rootScope);
57
- }
58
-
59
- get rootContext() {
60
- return this.stack[0];
61
- }
62
-
63
- get activeContext() {
64
- return this.stack[this.stack.length - 1];
65
- }
66
-
67
- }
68
-
69
- export type ScopeMapping = (params: {
70
- injector: EcsInjector,
71
- registerScopeService: (serviceClass: Class) => void,
72
- }) => void
73
-
74
- interface AddScopeSignalOptions extends ScopeSignalOptions {
75
- context?: unknown;
76
- }
77
-
78
- // TODO: maybe move Signal resolver to Injector and dont use EcsInjector here?
79
- export class ScopeContext {
80
- readonly scope: Scope;
81
- readonly injector: EcsInjector;
82
- private readonly stack: ScopeStack; // TODO: naming
83
- private readonly signalBindings: ScopeSignalBinding[] = [];
84
-
85
- // Scope services will be active (signals bounds) in this scope and all sub scopes
86
- // when this scope exits, the signals will be unbound
87
- private serviceBindings: SignalBinding[] = [];
88
- private closed = false;
89
- private muteKeepAliveSignals = false;
90
-
91
- constructor(injector: EcsInjector, scopeClass: Class, mapping?: ScopeMapping, private parent?: ScopeContext) {
92
- if (!parent) {
93
- this.stack = new ScopeStack(this);
94
-
95
- } else {
96
- this.stack = parent.stack;
97
- this.stack.stack.push(this);
98
- }
99
-
100
- this.injector = injector.createChild();
101
- //TODO: i think it would really be better if we implement an injector.injectIntoUnmapped() instead of mapping here
102
- this.injector.map(scopeClass).toSingleton();
103
- this.injector.map(ScopeContext).toValue(this);
104
-
105
- mapping?.({
106
- injector: this.injector,
107
- registerScopeService: serviceClass => {
108
- this.injector.map(serviceClass).toSingleton();
109
- this.mapServiceSignals(this.injector.get(serviceClass));
110
- },
111
- });
112
-
113
- this.scope = this.injector.get(scopeClass) as Scope;
114
- this.mapScopeSignals();
115
- this.updateSignalBindings();
116
- // TODO: maybe the scope needs to prepare resources or something before it can react to signals, so we should move onEnter bevore the signal mapping and make it async (not working in constructor though)
117
- this.scope.onEnter?.();
118
- this.scope.onActivate?.();
119
- }
120
-
121
- get isRoot() {
122
- return this === this.stack.rootScope;
123
- }
124
-
125
- get isActive() {
126
- return this === this.stack.activeContext;
127
- }
128
-
129
- get activeContext() { return this.stack.activeContext;}
130
-
131
- enterScope(scopeClass: Class, mapping?: ScopeMapping) {
132
- this.activeContext.scope.onSubExit?.();
133
- this.activeContext.scope.onDeactivate?.();
134
-
135
- const newContext = new ScopeContext(this.activeContext.injector, scopeClass, mapping, this.activeContext);
136
-
137
- this.logStack();
138
- return newContext;
139
- }
140
-
141
- /**
142
- * Exits this scope (and all open subscopes=), if a scope class is given. all scopes from the stack are closed until after the scope with the given class
143
- */
144
- exitScope(target?: Scope) {
145
- if (this.closed) throw new Error(`Scope already closed`);
146
- // TODO: check if target is in stack?
147
- if (!target) target = this.scope.constructor;
148
- while (true) {
149
- const ctx = this.stack.stack.pop();
150
- if (!ctx) {
151
- throw new Error('no scope in stack');
152
- }
153
- ctx.exitThis();
154
- if (ctx.scope.constructor === target) {
155
- break;
156
- }
157
- }
158
- }
159
-
160
- closeSubScopes() {
161
- while (!this.isActive) {
162
- const ctx = this.stack.stack.pop();
163
- if (!ctx) {
164
- throw new Error('no scope in stack');
165
- }
166
- ctx.exitThis();
167
- }
168
- }
169
-
170
- private logStack() {
171
- console.debug('%c' + this.stack.stack.map(c => c.scope.constructor.name).join(' >> '), 'color:yellow');
172
- }
173
-
174
- getStackClasses(): Scope[] {
175
- return this.stack.stack.map(context => context.scope.constructor);
176
- }
177
-
178
- addScopeSignal<T>(signal: Signal<T>, callback: SignalCallback<T>, params?: AddScopeSignalOptions) {
179
- const signalBinding = new ScopeSignalBinding(signal.add(callback, params?.context), params);
180
- this.signalBindings.push(signalBinding);
181
- }
182
-
183
- disableKeepAliveSignals() {
184
- this.muteKeepAliveSignals = true;
185
- this.updateSignalBindings();
186
- }
187
-
188
- enableKeepAliveSignals() {
189
- this.muteKeepAliveSignals = false;
190
- this.updateSignalBindings();
191
- }
192
-
193
- private updateSignalBindings(muteKeepAlive = false) {
194
- for (const binding of this.signalBindings) {
195
- if (this.isActive) {
196
- binding.activate();
197
- } else if (muteKeepAlive) {
198
- binding.deactivate();
199
- } else if (binding.keepAlive === true) {
200
- binding.activate();
201
- } else if (!!(binding.keepAlive)) {
202
- // is array of scopes
203
- // OPT: cache active context?
204
- if (binding.keepAlive.indexOf(this.activeContext.scope.constructor) != -1) {
205
- binding.activate();
206
- } else {
207
- binding.deactivate();
208
- }
209
- } else {
210
- binding.deactivate();
211
- }
212
- }
213
- muteKeepAlive = muteKeepAlive || this.muteKeepAliveSignals;
214
- this.parent?.updateSignalBindings(muteKeepAlive);
215
- }
216
-
217
- private detachSignalBindings() {
218
- for (const signalBinding of this.signalBindings) {
219
- signalBinding.detach();
220
- }
221
- }
222
-
223
- private mapScopeSignals() {
224
- const handlers = scopeSignalHandlers.get(this.scope.constructor);
225
- if (!handlers) return;
226
-
227
- for (const [field, id, options] of handlers) {
228
- const signal = this.injector.getSignal(id);
229
- const callback = (<any>this.scope) [field];
230
- this.addScopeSignal(signal, callback, {...options, context: this.scope});
231
- }
232
- }
233
-
234
- private mapServiceSignals(scopeService: Object) {
235
- const handlers = signalHandlers.get(scopeService.constructor);
236
- if (!handlers) return;
237
-
238
- for (const [field, id] of handlers) {
239
- const signal = this.injector.getSignal(id);
240
- const signalBinding = signal.add((<any>scopeService) [field], scopeService);
241
- this.serviceBindings.push(signalBinding);
242
- }
243
- }
244
-
245
- private detachServiceBindings() {
246
- for (const binding of this.serviceBindings) {
247
- binding.detach();
248
- }
249
- this.serviceBindings = [];
250
- }
251
-
252
- private exitThis() {
253
- if (!this.parent) {
254
- throw new Error('can\'t exit root scope?!');
255
- }
256
- this.detachSignalBindings();
257
- this.detachServiceBindings();
258
- this.scope.onDeactivate?.();
259
- this.scope.onExit?.();
260
- this.parent.updateSignalBindings();
261
- this.parent.scope.onSubReturn?.();
262
- this.parent.scope.onActivate?.();
263
- this.closed = true;
264
- this.logStack();
265
- }
266
-
267
- }
1
+ import {Class, EcsInjector, Signal, SignalBinding, SignalCallback} from '../index';
2
+ import {signalHandlers} from '../ecsInjector';
3
+ import {ID, putIfAbsent} from '../injector';
4
+
5
+ export type ScopeSignalOptions = { keepAlive?: boolean | Scope[], group?: string }
6
+
7
+ export const scopeSignalHandlers = new Map<Object, [string, ID, ScopeSignalOptions?][]>();
8
+
9
+ export const OnScopeSignal = (id: ID, options?: ScopeSignalOptions) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
10
+ if (target instanceof Function) {
11
+ throw new Error('only allowed on non static methods');
12
+ } else {
13
+ const mappingList = putIfAbsent(scopeSignalHandlers, target.constructor, (): [string, ID, ScopeSignalOptions?][] => []);
14
+ mappingList.push([propertyKey, id, options]);
15
+ }
16
+ };
17
+
18
+ export const SCOPE_CONTEXT = {
19
+ log: true,
20
+ };
21
+
22
+ export interface Scope {
23
+ [propName: string]: any; // disable weak type detection
24
+ onEnter?(): void;
25
+ onExit?(): void;
26
+ onSubReturn?(): void;
27
+ onSubExit?(): void;
28
+ onActivate?(): void;
29
+ onDeactivate?(): void;
30
+ }
31
+
32
+ class ScopeSignalBinding {
33
+ readonly keepAlive?: boolean | Scope[];
34
+ private group?: string;
35
+
36
+ constructor(private signalBinding: SignalBinding, options?: ScopeSignalOptions) {
37
+ this.keepAlive = options?.keepAlive;
38
+ this.group = options?.group;
39
+ }
40
+
41
+ activate() {
42
+ this.signalBinding.setActive(true);
43
+ }
44
+
45
+ deactivate() {
46
+ this.signalBinding.setActive(false);
47
+ }
48
+
49
+ detach() {
50
+ this.signalBinding.detach();
51
+ }
52
+ }
53
+
54
+ class ScopeStack {
55
+ readonly stack: ScopeContext[] = [];
56
+ queuedScopeChanges: (() => Promise<void>)[] = [];
57
+ ongoingChange = false;
58
+ target?: Scope;
59
+
60
+ constructor(readonly rootScope: ScopeContext) {
61
+ this.stack.push(rootScope);
62
+ }
63
+
64
+ get rootContext() {
65
+ return this.stack[0];
66
+ }
67
+
68
+ get activeContext() {
69
+ return this.stack[this.stack.length - 1];
70
+ }
71
+
72
+ }
73
+
74
+ export type ScopeMapping = (params: {
75
+ injector: EcsInjector,
76
+ registerScopeService: (serviceClass: Class) => void,
77
+ onEnter: Signal<ScopeContext>,
78
+ onExit: Signal<ScopeContext>,
79
+ onSubReturn: Signal<ScopeContext>,
80
+ onSubExit: Signal<ScopeContext>,
81
+ onActivate: Signal<ScopeContext>,
82
+ onDeactivate: Signal<ScopeContext>,
83
+
84
+ }) => any | Promise<void>;
85
+
86
+ interface AddScopeSignalOptions extends ScopeSignalOptions {
87
+ context?: unknown;
88
+ }
89
+
90
+ // TODO: maybe move Signal resolver to Injector and dont use EcsInjector here?
91
+ export class ScopeContext {
92
+ readonly scope!: Scope;
93
+ readonly injector: EcsInjector;
94
+ private readonly stack: ScopeStack; // TODO: naming
95
+ private readonly signalBindings: ScopeSignalBinding[] = [];
96
+
97
+ // Scope services will be active (signals bounds) in this scope and all sub scopes
98
+ // when this scope exits, the signals will be unbound
99
+ private serviceBindings: SignalBinding[] = [];
100
+ private closed = false;
101
+ private muteKeepAliveSignals = false;
102
+
103
+ readonly onEnter = new Signal<ScopeContext>();
104
+ readonly onExit = new Signal<ScopeContext>();
105
+ readonly onSubReturn = new Signal<ScopeContext>();
106
+ readonly onSubExit = new Signal<ScopeContext>();
107
+ readonly onActivate = new Signal<ScopeContext>();
108
+ readonly onDeactivate = new Signal<ScopeContext>();
109
+ private readonly creationTime: DOMHighResTimeStamp;
110
+
111
+ get runtime() {
112
+ return (performance.now() - this.creationTime) / 1000;
113
+ }
114
+
115
+ constructor(injector: EcsInjector, scopeClass: Class, mapping?: ScopeMapping, private parent?: ScopeContext, resolve?: () => void) {
116
+ this.creationTime = performance.now();
117
+ if (!parent) {
118
+ this.stack = new ScopeStack(this);
119
+
120
+ } else {
121
+ this.stack = parent.stack;
122
+ this.stack.stack.push(this);
123
+ }
124
+
125
+ // TODO: disable this for the rootscope?
126
+ this.injector = injector.createChild();
127
+
128
+ //TODO: i think it would really be better if we implement an injector.injectIntoUnmapped() instead of mapping here
129
+ this.injector.map(scopeClass).toSingleton();
130
+ this.injector.map(ScopeContext).toValue(this);
131
+
132
+ const result = mapping?.({
133
+ injector: this.injector,
134
+ registerScopeService: serviceClass => {
135
+ this.injector.map(serviceClass).toSingleton();
136
+ this.mapServiceSignals(this.injector.get(serviceClass));
137
+ },
138
+ onEnter: this.onEnter,
139
+ onExit: this.onExit,
140
+ onSubReturn: this.onSubReturn,
141
+ onSubExit: this.onSubExit,
142
+ onActivate: this.onActivate,
143
+ onDeactivate: this.onDeactivate,
144
+ });
145
+
146
+ const completeScopeChange = () => {
147
+ (this.scope as Scope) = this.injector.get(scopeClass);
148
+ this.mapScopeSignals();
149
+ this.updateSignalBindings();
150
+ // TODO: maybe the scope needs to prepare resources or something before it can react to signals, so we should move onEnter bevore the signal mapping and make it async (not working in constructor though)
151
+ this.scope.onEnter?.();
152
+ this.onEnter.dispatch(this);
153
+ this.scope.onActivate?.();
154
+ this.onActivate.dispatch(this);
155
+ resolve?.();
156
+ };
157
+
158
+ if (result instanceof Promise) {
159
+ result.then(completeScopeChange);
160
+ } else {
161
+ completeScopeChange();
162
+ }
163
+ }
164
+
165
+ get isRoot() {
166
+ return this === this.stack.rootScope;
167
+ }
168
+
169
+ // TODO: test this
170
+ get target() {
171
+ return this.stack.target;
172
+ }
173
+
174
+ get isActive() {
175
+ return this === this.stack.activeContext;
176
+ }
177
+
178
+ get activeContext() { return this.stack.activeContext;}
179
+
180
+ /**
181
+ * returns the new scope only if there is currently no ongoing scope change happening
182
+ * @param scopeClass
183
+ * @param mapping
184
+ */
185
+ async enterScope(scopeClass: Class, mapping?: ScopeMapping) {
186
+ let newContext: ScopeContext | undefined;
187
+ const doChange = async () => {
188
+
189
+ this.activeContext.scope.onSubExit?.();
190
+ this.activeContext.onSubExit.dispatch(this);
191
+ this.activeContext.scope.onDeactivate?.();
192
+ this.activeContext.onDeactivate.dispatch(this);
193
+
194
+ await new Promise<void>(resolve => {
195
+ newContext = new ScopeContext(this.activeContext.injector, scopeClass, mapping, this.activeContext, resolve);
196
+ });
197
+
198
+ this.logStack();
199
+ const nextChange = this.stack.queuedScopeChanges.shift();
200
+ if (nextChange) {
201
+ await nextChange();
202
+ } else {
203
+ this.stack.ongoingChange = false;
204
+ }
205
+ };
206
+ // TODO: this queued changes are somewhat experimental
207
+ if (!this.stack.ongoingChange) {
208
+ this.stack.ongoingChange = true;
209
+ await doChange();
210
+ } else {
211
+ this.stack.queuedScopeChanges.push(doChange);
212
+ }
213
+ return newContext;
214
+ }
215
+
216
+ /**
217
+ * Exits this scope (and all open subscopes=), if a scope class is given. all scopes from the stack are closed until after the scope with the given class
218
+ */
219
+ async exitScope(target?: Scope) {
220
+ const doChange = async () => {
221
+ if (this.closed) throw new Error(`Scope already closed`);
222
+ // TODO: check if target is in stack?
223
+ this.stack.target = target || this.scope.constructor;
224
+ while (true) {
225
+ const ctx = this.stack.stack.pop();
226
+ if (!ctx) {
227
+ throw new Error('no scope in stack');
228
+ }
229
+ ctx!.exitThis();
230
+ if (ctx!.scope.constructor === this.stack.target) {
231
+ break;
232
+ }
233
+ }
234
+ this.stack.target = undefined;
235
+ const nextChange = this.stack.queuedScopeChanges.shift();
236
+ if (nextChange) {
237
+ await nextChange();
238
+ } else {
239
+ this.stack.ongoingChange = false;
240
+ }
241
+
242
+ };
243
+ if (!this.stack.ongoingChange) {
244
+ this.stack.ongoingChange = true;
245
+ await doChange();
246
+ } else {
247
+ this.stack.queuedScopeChanges.push(doChange);
248
+ }
249
+ }
250
+
251
+ closeSubScopes() {
252
+ this.stack.target = this.scope.constructor;
253
+ while (!this.isActive) {
254
+ const ctx = this.stack.stack.pop();
255
+ if (!ctx) {
256
+ throw new Error('no scope in stack');
257
+ }
258
+ ctx.exitThis();
259
+ }
260
+ this.stack.target = undefined;
261
+ }
262
+
263
+ private logStack() {
264
+ if (SCOPE_CONTEXT.log) {
265
+ console.debug('%c' + this.stack.stack.map(c => c.scope.constructor.name).join(' -> '), 'color:yellow');
266
+ }
267
+ }
268
+
269
+ getStackClasses(): Scope[] {
270
+ return this.stack.stack.map(context => context.scope.constructor);
271
+ }
272
+
273
+ addScopeSignal<T>(signal: Signal<T>, callback: SignalCallback<T>, params?: AddScopeSignalOptions) {
274
+ const signalBinding = new ScopeSignalBinding(signal.add(callback, params?.context), params);
275
+ this.signalBindings.push(signalBinding);
276
+ }
277
+
278
+ disableKeepAliveSignals() {
279
+ this.muteKeepAliveSignals = true;
280
+ this.updateSignalBindings();
281
+ }
282
+
283
+ enableKeepAliveSignals() {
284
+ this.muteKeepAliveSignals = false;
285
+ this.updateSignalBindings();
286
+ }
287
+
288
+ private updateSignalBindings(muteKeepAlive = false) {
289
+ for (const binding of this.signalBindings) {
290
+ if (this.isActive) {
291
+ binding.activate();
292
+ } else if (muteKeepAlive) {
293
+ binding.deactivate();
294
+ } else if (binding.keepAlive === true) {
295
+ binding.activate();
296
+ } else if (!!(binding.keepAlive)) {
297
+ // is array of scopes
298
+ // OPT: cache active context?
299
+ if (binding.keepAlive.indexOf(this.activeContext.scope.constructor) != -1) {
300
+ binding.activate();
301
+ } else {
302
+ binding.deactivate();
303
+ }
304
+ } else {
305
+ binding.deactivate();
306
+ }
307
+ }
308
+ muteKeepAlive = muteKeepAlive || this.muteKeepAliveSignals;
309
+ this.parent?.updateSignalBindings(muteKeepAlive);
310
+ }
311
+
312
+ private detachSignalBindings() {
313
+ for (const signalBinding of this.signalBindings) {
314
+ signalBinding.detach();
315
+ }
316
+ }
317
+
318
+ private mapScopeSignals() {
319
+ const handlers = scopeSignalHandlers.get(this.scope.constructor);
320
+ if (!handlers) return;
321
+
322
+ for (const [field, id, options] of handlers) {
323
+ const signal = this.injector.getSignal(id);
324
+ const callback = (<any>this.scope) [field];
325
+ this.addScopeSignal(signal, callback, {...options, context: this.scope});
326
+ }
327
+ }
328
+
329
+ private mapServiceSignals(scopeService: Object) {
330
+ const handlers = signalHandlers.get(scopeService.constructor);
331
+ if (!handlers) return;
332
+
333
+ for (const [field, id] of handlers) {
334
+ const signal = this.injector.getSignal(id);
335
+ const signalBinding = signal.add((<any>scopeService) [field], scopeService);
336
+ this.serviceBindings.push(signalBinding);
337
+ }
338
+ }
339
+
340
+ private detachServiceBindings() {
341
+ for (const binding of this.serviceBindings) {
342
+ binding.detach();
343
+ }
344
+ this.serviceBindings = [];
345
+ }
346
+
347
+ private exitThis() {
348
+
349
+ // TODO: BUG: when there is a scope change within another scope change there is some confusion about the call order
350
+ if (!this.parent) {
351
+ throw new Error('can\'t exit root scope?!');
352
+ }
353
+
354
+ this.logStack();
355
+ this.detachSignalBindings();
356
+ this.detachServiceBindings();
357
+ this.scope.onDeactivate?.();
358
+ this.onDeactivate.dispatch(this);
359
+
360
+ this.scope.onExit?.();
361
+ this.onExit.dispatch(this);
362
+
363
+ this.injector._dispose();
364
+
365
+ this.parent.updateSignalBindings();
366
+ this.parent.scope.onSubReturn?.();
367
+ this.onSubReturn.dispatch(this);
368
+ this.parent.scope.onActivate?.();
369
+ this.parent.onActivate.dispatch(this);
370
+ this.closed = true;
371
+
372
+ this.onEnter.detachAll();
373
+ this.onExit.detachAll();
374
+ this.onSubReturn.detachAll();
375
+ this.onSubExit.detachAll();
376
+ this.onActivate.detachAll();
377
+ this.onDeactivate.detachAll();
378
+
379
+ }
380
+ }