mani-game-engine 1.0.0-pre.34 → 1.0.0-pre.35

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,320 +1,322 @@
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: Function[] = [];
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
- }) => void
78
-
79
- interface AddScopeSignalOptions extends ScopeSignalOptions {
80
- context?: unknown;
81
- }
82
-
83
- // TODO: maybe move Signal resolver to Injector and dont use EcsInjector here?
84
- export class ScopeContext {
85
- readonly scope: Scope;
86
- readonly injector: EcsInjector;
87
- private readonly stack: ScopeStack; // TODO: naming
88
- private readonly signalBindings: ScopeSignalBinding[] = [];
89
-
90
- // Scope services will be active (signals bounds) in this scope and all sub scopes
91
- // when this scope exits, the signals will be unbound
92
- private serviceBindings: SignalBinding[] = [];
93
- private closed = false;
94
- private muteKeepAliveSignals = false;
95
-
96
- constructor(injector: EcsInjector, scopeClass: Class, mapping?: ScopeMapping, private parent?: ScopeContext) {
97
- if (!parent) {
98
- this.stack = new ScopeStack(this);
99
-
100
- } else {
101
- this.stack = parent.stack;
102
- this.stack.stack.push(this);
103
- }
104
-
105
- this.injector = injector.createChild();
106
- //TODO: i think it would really be better if we implement an injector.injectIntoUnmapped() instead of mapping here
107
- this.injector.map(scopeClass).toSingleton();
108
- this.injector.map(ScopeContext).toValue(this);
109
-
110
- mapping?.({
111
- injector: this.injector,
112
- registerScopeService: serviceClass => {
113
- this.injector.map(serviceClass).toSingleton();
114
- this.mapServiceSignals(this.injector.get(serviceClass));
115
- },
116
- });
117
-
118
- this.scope = this.injector.get(scopeClass) as Scope;
119
- this.mapScopeSignals();
120
- this.updateSignalBindings();
121
- // 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)
122
- this.scope.onEnter?.();
123
- this.scope.onActivate?.();
124
- }
125
-
126
- get isRoot() {
127
- return this === this.stack.rootScope;
128
- }
129
-
130
- // TODO: test this
131
- get target() {
132
- return this.stack.target;
133
- }
134
-
135
- get isActive() {
136
- return this === this.stack.activeContext;
137
- }
138
-
139
- get activeContext() { return this.stack.activeContext;}
140
-
141
- /**
142
- * returns the new scope only if there is currently no ongoing scope change happening
143
- * @param scopeClass
144
- * @param mapping
145
- */
146
- enterScope(scopeClass: Class, mapping?: ScopeMapping): ScopeContext | undefined {
147
- let newContext;
148
- const doChange = () => {
149
-
150
- this.activeContext.scope.onSubExit?.();
151
- this.activeContext.scope.onDeactivate?.();
152
-
153
- newContext = new ScopeContext(this.activeContext.injector, scopeClass, mapping, this.activeContext);
154
-
155
- this.logStack();
156
- const nextChange = this.stack.queuedScopeChanges.shift();
157
- if (nextChange) {
158
- nextChange();
159
- } else {
160
- this.stack.ongoingChange = false;
161
- }
162
- };
163
- // TODO: this queued changes arent somewhat experimental
164
- if (!this.stack.ongoingChange) {
165
- this.stack.ongoingChange = true;
166
- doChange();
167
- } else {
168
- this.stack.queuedScopeChanges.push(doChange);
169
- }
170
- return newContext;
171
- }
172
-
173
- /**
174
- * 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
175
- */
176
- exitScope(target?: Scope) {
177
- const doChange = () => {
178
- if (this.closed) throw new Error(`Scope already closed`);
179
- // TODO: check if target is in stack?
180
- this.stack.target = target || this.scope.constructor;
181
- while (true) {
182
- const ctx = this.stack.stack.pop();
183
- if (!ctx) {
184
- throw new Error('no scope in stack');
185
- }
186
- ctx!.exitThis();
187
- if (ctx!.scope.constructor === this.stack.target) {
188
- break;
189
- }
190
- }
191
- this.stack.target = undefined;
192
- const nextChange = this.stack.queuedScopeChanges.shift();
193
- if (nextChange) {
194
- nextChange();
195
- } else {
196
- this.stack.ongoingChange = false;
197
- }
198
-
199
- };
200
- if (!this.stack.ongoingChange) {
201
- this.stack.ongoingChange = true;
202
- doChange();
203
- } else {
204
- this.stack.queuedScopeChanges.push(doChange);
205
- }
206
- }
207
-
208
- closeSubScopes() {
209
- this.stack.target = this.scope.constructor;
210
- while (!this.isActive) {
211
- const ctx = this.stack.stack.pop();
212
- if (!ctx) {
213
- throw new Error('no scope in stack');
214
- }
215
- ctx.exitThis();
216
- }
217
- this.stack.target = undefined;
218
- }
219
-
220
- private logStack() {
221
- if (SCOPE_CONTEXT.log) {
222
- console.debug('%c' + this.stack.stack.map(c => c.scope.constructor.name).join(' -> '), 'color:yellow');
223
- }
224
- }
225
-
226
- getStackClasses(): Scope[] {
227
- return this.stack.stack.map(context => context.scope.constructor);
228
- }
229
-
230
- addScopeSignal<T>(signal: Signal<T>, callback: SignalCallback<T>, params?: AddScopeSignalOptions) {
231
- const signalBinding = new ScopeSignalBinding(signal.add(callback, params?.context), params);
232
- this.signalBindings.push(signalBinding);
233
- }
234
-
235
- disableKeepAliveSignals() {
236
- this.muteKeepAliveSignals = true;
237
- this.updateSignalBindings();
238
- }
239
-
240
- enableKeepAliveSignals() {
241
- this.muteKeepAliveSignals = false;
242
- this.updateSignalBindings();
243
- }
244
-
245
- private updateSignalBindings(muteKeepAlive = false) {
246
- for (const binding of this.signalBindings) {
247
- if (this.isActive) {
248
- binding.activate();
249
- } else if (muteKeepAlive) {
250
- binding.deactivate();
251
- } else if (binding.keepAlive === true) {
252
- binding.activate();
253
- } else if (!!(binding.keepAlive)) {
254
- // is array of scopes
255
- // OPT: cache active context?
256
- if (binding.keepAlive.indexOf(this.activeContext.scope.constructor) != -1) {
257
- binding.activate();
258
- } else {
259
- binding.deactivate();
260
- }
261
- } else {
262
- binding.deactivate();
263
- }
264
- }
265
- muteKeepAlive = muteKeepAlive || this.muteKeepAliveSignals;
266
- this.parent?.updateSignalBindings(muteKeepAlive);
267
- }
268
-
269
- private detachSignalBindings() {
270
- for (const signalBinding of this.signalBindings) {
271
- signalBinding.detach();
272
- }
273
- }
274
-
275
- private mapScopeSignals() {
276
- const handlers = scopeSignalHandlers.get(this.scope.constructor);
277
- if (!handlers) return;
278
-
279
- for (const [field, id, options] of handlers) {
280
- const signal = this.injector.getSignal(id);
281
- const callback = (<any>this.scope) [field];
282
- this.addScopeSignal(signal, callback, {...options, context: this.scope});
283
- }
284
- }
285
-
286
- private mapServiceSignals(scopeService: Object) {
287
- const handlers = signalHandlers.get(scopeService.constructor);
288
- if (!handlers) return;
289
-
290
- for (const [field, id] of handlers) {
291
- const signal = this.injector.getSignal(id);
292
- const signalBinding = signal.add((<any>scopeService) [field], scopeService);
293
- this.serviceBindings.push(signalBinding);
294
- }
295
- }
296
-
297
- private detachServiceBindings() {
298
- for (const binding of this.serviceBindings) {
299
- binding.detach();
300
- }
301
- this.serviceBindings = [];
302
- }
303
-
304
- private exitThis() {
305
-
306
- // TODO: BUG: when there is a scope change within another scope change there is some confusion about the call order
307
- if (!this.parent) {
308
- throw new Error('can\'t exit root scope?!');
309
- }
310
- this.logStack();
311
- this.detachSignalBindings();
312
- this.detachServiceBindings();
313
- this.scope.onDeactivate?.();
314
- this.scope.onExit?.();
315
- this.parent.updateSignalBindings();
316
- this.parent.scope.onSubReturn?.();
317
- this.parent.scope.onActivate?.();
318
- this.closed = true;
319
- }
320
- }
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: Function[] = [];
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
+ }) => void
78
+
79
+ interface AddScopeSignalOptions extends ScopeSignalOptions {
80
+ context?: unknown;
81
+ }
82
+
83
+ // TODO: maybe move Signal resolver to Injector and dont use EcsInjector here?
84
+ export class ScopeContext {
85
+ readonly scope: Scope;
86
+ readonly injector: EcsInjector;
87
+ private readonly stack: ScopeStack; // TODO: naming
88
+ private readonly signalBindings: ScopeSignalBinding[] = [];
89
+
90
+ // Scope services will be active (signals bounds) in this scope and all sub scopes
91
+ // when this scope exits, the signals will be unbound
92
+ private serviceBindings: SignalBinding[] = [];
93
+ private closed = false;
94
+ private muteKeepAliveSignals = false;
95
+
96
+ constructor(injector: EcsInjector, scopeClass: Class, mapping?: ScopeMapping, private parent?: ScopeContext) {
97
+ if (!parent) {
98
+ this.stack = new ScopeStack(this);
99
+
100
+ } else {
101
+ this.stack = parent.stack;
102
+ this.stack.stack.push(this);
103
+ }
104
+
105
+ // TODO: disable this for the rootscope?
106
+ this.injector = injector.createChild();
107
+
108
+ //TODO: i think it would really be better if we implement an injector.injectIntoUnmapped() instead of mapping here
109
+ this.injector.map(scopeClass).toSingleton();
110
+ this.injector.map(ScopeContext).toValue(this);
111
+
112
+ mapping?.({
113
+ injector: this.injector,
114
+ registerScopeService: serviceClass => {
115
+ this.injector.map(serviceClass).toSingleton();
116
+ this.mapServiceSignals(this.injector.get(serviceClass));
117
+ },
118
+ });
119
+
120
+ this.scope = this.injector.get(scopeClass) as Scope;
121
+ this.mapScopeSignals();
122
+ this.updateSignalBindings();
123
+ // 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)
124
+ this.scope.onEnter?.();
125
+ this.scope.onActivate?.();
126
+ }
127
+
128
+ get isRoot() {
129
+ return this === this.stack.rootScope;
130
+ }
131
+
132
+ // TODO: test this
133
+ get target() {
134
+ return this.stack.target;
135
+ }
136
+
137
+ get isActive() {
138
+ return this === this.stack.activeContext;
139
+ }
140
+
141
+ get activeContext() { return this.stack.activeContext;}
142
+
143
+ /**
144
+ * returns the new scope only if there is currently no ongoing scope change happening
145
+ * @param scopeClass
146
+ * @param mapping
147
+ */
148
+ enterScope(scopeClass: Class, mapping?: ScopeMapping): ScopeContext | undefined {
149
+ let newContext;
150
+ const doChange = () => {
151
+
152
+ this.activeContext.scope.onSubExit?.();
153
+ this.activeContext.scope.onDeactivate?.();
154
+
155
+ newContext = new ScopeContext(this.activeContext.injector, scopeClass, mapping, this.activeContext);
156
+
157
+ this.logStack();
158
+ const nextChange = this.stack.queuedScopeChanges.shift();
159
+ if (nextChange) {
160
+ nextChange();
161
+ } else {
162
+ this.stack.ongoingChange = false;
163
+ }
164
+ };
165
+ // TODO: this queued changes arent somewhat experimental
166
+ if (!this.stack.ongoingChange) {
167
+ this.stack.ongoingChange = true;
168
+ doChange();
169
+ } else {
170
+ this.stack.queuedScopeChanges.push(doChange);
171
+ }
172
+ return newContext;
173
+ }
174
+
175
+ /**
176
+ * 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
177
+ */
178
+ exitScope(target?: Scope) {
179
+ const doChange = () => {
180
+ if (this.closed) throw new Error(`Scope already closed`);
181
+ // TODO: check if target is in stack?
182
+ this.stack.target = target || this.scope.constructor;
183
+ while (true) {
184
+ const ctx = this.stack.stack.pop();
185
+ if (!ctx) {
186
+ throw new Error('no scope in stack');
187
+ }
188
+ ctx!.exitThis();
189
+ if (ctx!.scope.constructor === this.stack.target) {
190
+ break;
191
+ }
192
+ }
193
+ this.stack.target = undefined;
194
+ const nextChange = this.stack.queuedScopeChanges.shift();
195
+ if (nextChange) {
196
+ nextChange();
197
+ } else {
198
+ this.stack.ongoingChange = false;
199
+ }
200
+
201
+ };
202
+ if (!this.stack.ongoingChange) {
203
+ this.stack.ongoingChange = true;
204
+ doChange();
205
+ } else {
206
+ this.stack.queuedScopeChanges.push(doChange);
207
+ }
208
+ }
209
+
210
+ closeSubScopes() {
211
+ this.stack.target = this.scope.constructor;
212
+ while (!this.isActive) {
213
+ const ctx = this.stack.stack.pop();
214
+ if (!ctx) {
215
+ throw new Error('no scope in stack');
216
+ }
217
+ ctx.exitThis();
218
+ }
219
+ this.stack.target = undefined;
220
+ }
221
+
222
+ private logStack() {
223
+ if (SCOPE_CONTEXT.log) {
224
+ console.debug('%c' + this.stack.stack.map(c => c.scope.constructor.name).join(' -> '), 'color:yellow');
225
+ }
226
+ }
227
+
228
+ getStackClasses(): Scope[] {
229
+ return this.stack.stack.map(context => context.scope.constructor);
230
+ }
231
+
232
+ addScopeSignal<T>(signal: Signal<T>, callback: SignalCallback<T>, params?: AddScopeSignalOptions) {
233
+ const signalBinding = new ScopeSignalBinding(signal.add(callback, params?.context), params);
234
+ this.signalBindings.push(signalBinding);
235
+ }
236
+
237
+ disableKeepAliveSignals() {
238
+ this.muteKeepAliveSignals = true;
239
+ this.updateSignalBindings();
240
+ }
241
+
242
+ enableKeepAliveSignals() {
243
+ this.muteKeepAliveSignals = false;
244
+ this.updateSignalBindings();
245
+ }
246
+
247
+ private updateSignalBindings(muteKeepAlive = false) {
248
+ for (const binding of this.signalBindings) {
249
+ if (this.isActive) {
250
+ binding.activate();
251
+ } else if (muteKeepAlive) {
252
+ binding.deactivate();
253
+ } else if (binding.keepAlive === true) {
254
+ binding.activate();
255
+ } else if (!!(binding.keepAlive)) {
256
+ // is array of scopes
257
+ // OPT: cache active context?
258
+ if (binding.keepAlive.indexOf(this.activeContext.scope.constructor) != -1) {
259
+ binding.activate();
260
+ } else {
261
+ binding.deactivate();
262
+ }
263
+ } else {
264
+ binding.deactivate();
265
+ }
266
+ }
267
+ muteKeepAlive = muteKeepAlive || this.muteKeepAliveSignals;
268
+ this.parent?.updateSignalBindings(muteKeepAlive);
269
+ }
270
+
271
+ private detachSignalBindings() {
272
+ for (const signalBinding of this.signalBindings) {
273
+ signalBinding.detach();
274
+ }
275
+ }
276
+
277
+ private mapScopeSignals() {
278
+ const handlers = scopeSignalHandlers.get(this.scope.constructor);
279
+ if (!handlers) return;
280
+
281
+ for (const [field, id, options] of handlers) {
282
+ const signal = this.injector.getSignal(id);
283
+ const callback = (<any>this.scope) [field];
284
+ this.addScopeSignal(signal, callback, {...options, context: this.scope});
285
+ }
286
+ }
287
+
288
+ private mapServiceSignals(scopeService: Object) {
289
+ const handlers = signalHandlers.get(scopeService.constructor);
290
+ if (!handlers) return;
291
+
292
+ for (const [field, id] of handlers) {
293
+ const signal = this.injector.getSignal(id);
294
+ const signalBinding = signal.add((<any>scopeService) [field], scopeService);
295
+ this.serviceBindings.push(signalBinding);
296
+ }
297
+ }
298
+
299
+ private detachServiceBindings() {
300
+ for (const binding of this.serviceBindings) {
301
+ binding.detach();
302
+ }
303
+ this.serviceBindings = [];
304
+ }
305
+
306
+ private exitThis() {
307
+
308
+ // TODO: BUG: when there is a scope change within another scope change there is some confusion about the call order
309
+ if (!this.parent) {
310
+ throw new Error('can\'t exit root scope?!');
311
+ }
312
+ this.logStack();
313
+ this.detachSignalBindings();
314
+ this.detachServiceBindings();
315
+ this.scope.onDeactivate?.();
316
+ this.scope.onExit?.();
317
+ this.parent.updateSignalBindings();
318
+ this.parent.scope.onSubReturn?.();
319
+ this.parent.scope.onActivate?.();
320
+ this.closed = true;
321
+ }
322
+ }