@woosh/meep-engine 3.14.3 → 3.14.4
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/package.json +1 -1
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +733 -726
- package/src/engine/network/NetworkSession.d.ts +24 -4
- package/src/engine/network/NetworkSession.d.ts.map +1 -1
- package/src/engine/network/NetworkSession.js +28 -4
- package/src/engine/network/orchestrator/NetworkPeer.d.ts +47 -6
- package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/NetworkPeer.js +184 -56
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +97 -11
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +304 -22
- package/src/engine/network/replication/Replicator.d.ts +14 -1
- package/src/engine/network/replication/Replicator.d.ts.map +1 -1
- package/src/engine/network/replication/Replicator.js +44 -3
- package/src/engine/network/sim/ActionLog.d.ts +21 -3
- package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
- package/src/engine/network/sim/ActionLog.js +24 -3
- package/src/engine/network/sim/SimAction.d.ts +15 -0
- package/src/engine/network/sim/SimAction.d.ts.map +1 -1
- package/src/engine/network/sim/SimAction.js +15 -0
- package/src/engine/network/sim/SimActionExecutor.d.ts +51 -1
- package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
- package/src/engine/network/sim/SimActionExecutor.js +73 -5
- package/src/engine/network/transport/Channel.d.ts +1 -1
- package/src/engine/network/transport/Channel.js +1 -1
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +4 -1
- package/src/engine/ui/GUIEngine.d.ts.map +1 -1
- package/src/engine/ui/GUIEngine.js +6 -0
- package/src/shade/playground/particle_system/README.md +98 -0
- package/src/shade/playground/particle_system/index.html +124 -0
- package/src/shade/playground/particle_system/main.d.ts +2 -0
- package/src/shade/playground/particle_system/main.d.ts.map +1 -0
- package/src/shade/playground/particle_system/main.js +180 -0
- package/src/shade/playground/particle_system/particle_scene.d.ts +52 -0
- package/src/shade/playground/particle_system/particle_scene.d.ts.map +1 -0
- package/src/shade/playground/particle_system/particle_scene.js +552 -0
- package/src/shade/playground/skinned_mesh_soup/README.md +122 -0
- package/src/shade/playground/skinned_mesh_soup/index.html +199 -0
- package/src/shade/playground/skinned_mesh_soup/main.d.ts +7 -0
- package/src/shade/playground/skinned_mesh_soup/main.d.ts.map +1 -0
- package/src/shade/playground/skinned_mesh_soup/main.js +564 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts +23 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +409 -0
- package/src/shade/renderer/particles/cull/chunk_particle_cull.d.ts.map +1 -1
- package/src/shade/renderer/particles/cull/chunk_particle_cull.js +3 -1
- package/src/shade/renderer/particles/data/chunk_particle_emitter.js +1 -1
- package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts +1 -1
- package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
- package/src/shade/renderer/particles/isa/InstructionStream.d.ts +89 -0
- package/src/shade/renderer/particles/isa/InstructionStream.d.ts.map +1 -0
- package/src/shade/renderer/particles/isa/InstructionStream.js +162 -0
- package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts +1 -48
- package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts.map +1 -1
- package/src/shade/renderer/particles/isa/ParticleAssembler.js +25 -114
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.js +4 -2
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +4 -4
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +7 -5
- package/src/shade/renderer/particles/shaders/chunk_particle_render_math.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_render_math.js +3 -1
- package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_emit.js +3 -1
- package/src/shade/renderer/particles/shaders/shader_particle_finalize.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_finalize.js +5 -2
- package/src/shade/renderer/particles/shaders/shader_particle_render.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_render.js +9 -2
- package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +3 -1
- package/src/shade/renderer/particles/prototypeParticleSystem.d.ts +0 -2
- package/src/shade/renderer/particles/prototypeParticleSystem.d.ts.map +0 -1
- package/src/shade/renderer/particles/prototypeParticleSystem.js +0 -448
package/src/engine/Engine.js
CHANGED
|
@@ -1,726 +1,733 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { assert } from "../core/assert.js";
|
|
6
|
-
import Vector1 from "../core/geom/Vector1.js";
|
|
7
|
-
import { Localization } from "../core/localization/Localization.js";
|
|
8
|
-
import { ModuleRegistry } from "../core/model/ModuleRegistry.js";
|
|
9
|
-
import ObservedBoolean from "../core/model/ObservedBoolean.js";
|
|
10
|
-
import ConcurrentExecutor from '../core/process/executor/ConcurrentExecutor.js';
|
|
11
|
-
import EmptyView from "../view/elements/EmptyView.js";
|
|
12
|
-
import { ViewStack } from "../view/elements/navigation/ViewStack.js";
|
|
13
|
-
import { GraphicsContextFailureView } from "../view/graphics/GraphicsContextFailureView.js";
|
|
14
|
-
|
|
15
|
-
import { AssetManager } from './asset/AssetManager.js';
|
|
16
|
-
import { AssetPreloader } from "./asset/preloader/AssetPreloader.js";
|
|
17
|
-
import { MetricCollection } from "./development/performance/MetricCollection.js";
|
|
18
|
-
import { MetricStatistics } from "./development/performance/MetricStatistics.js";
|
|
19
|
-
import { PeriodicConsolePrinter } from "./development/performance/monitor/PeriodicConsolePrinter.js";
|
|
20
|
-
import { EntityManager } from "./ecs/EntityManager.js";
|
|
21
|
-
import { BinarySerializationRegistry } from "./ecs/storage/binary/BinarySerializationRegistry.js";
|
|
22
|
-
import { GraphicsEngine } from './graphics3/GraphicsEngine.js';
|
|
23
|
-
import { GamepadDevice } from "./input/devices/GamepadDevice.js";
|
|
24
|
-
import KeyboardDevice from "./input/devices/KeyboardDevice.js";
|
|
25
|
-
import { PointerDevice } from "./input/devices/PointerDevice.js";
|
|
26
|
-
import { StaticKnowledgeDatabase, StaticKnowledgeState } from "./knowledge/database/StaticKnowledgeDatabase.js";
|
|
27
|
-
import { logger } from "./logging/GlobalLogger.js";
|
|
28
|
-
import { OptionGroup } from "./options/OptionGroup.js";
|
|
29
|
-
import { EnginePluginManager } from "./plugin/EnginePluginManager.js";
|
|
30
|
-
import SceneManager from "./scene/SceneManager.js";
|
|
31
|
-
import Ticker from "./simulation/Ticker.js";
|
|
32
|
-
import SoundEngine from './sound/SoundEngine.js';
|
|
33
|
-
|
|
34
|
-
import GUIEngine from './ui/GUIEngine.js';
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class EngineSettings {
|
|
38
|
-
graphics_control_viewport_size = new ObservedBoolean(true);
|
|
39
|
-
simulation_speed = new Vector1(1);
|
|
40
|
-
input_mouse_sensitivity = new Vector1(5);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const METRIC_ID_FRAME = 'frame_delay';
|
|
44
|
-
const METRIC_ID_RENDER = 'render_time';
|
|
45
|
-
const METRIC_ID_SIMULATION = 'simulation_time';
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* How long a context loss is tolerated before the simulation is paused as well, in seconds. Most losses
|
|
49
|
-
* are resolved in well under a second, and stopping the world for those would be more disruptive than
|
|
50
|
-
* letting it run; a longer outage is worth pausing for, as the player cannot see what is happening.
|
|
51
|
-
* @type {number}
|
|
52
|
-
*/
|
|
53
|
-
const CONTEXT_LOSS_SIMULATION_PAUSE_DELAY = 2;
|
|
54
|
-
|
|
55
|
-
class Engine {
|
|
56
|
-
#last_frame_timestamp = 0;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Handle of the pending render-loop frame, `-1` when the loop is not running
|
|
60
|
-
* @type {number}
|
|
61
|
-
*/
|
|
62
|
-
#animation_frame_handle = -1;
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Handle of the pending "pause the simulation, this outage is taking a while" timer, `-1` when none
|
|
66
|
-
* @type {number}
|
|
67
|
-
*/
|
|
68
|
-
#context_loss_pause_timer = -1;
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Whether the simulation was paused because of a context loss, and therefore should be resumed once
|
|
72
|
-
* the context comes back
|
|
73
|
-
* @type {boolean}
|
|
74
|
-
*/
|
|
75
|
-
#paused_by_context_loss = false;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Absent when the engine is constructed with `enableGraphics: false`.
|
|
79
|
-
* @type {GraphicsEngine|null}
|
|
80
|
-
*/
|
|
81
|
-
graphics = null;
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Absent when the engine is constructed with `enableAudio: false`.
|
|
85
|
-
* @type {SoundEngine|null}
|
|
86
|
-
*/
|
|
87
|
-
sound = null;
|
|
88
|
-
|
|
89
|
-
viewStack = new ViewStack();
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
*
|
|
93
|
-
* Static database
|
|
94
|
-
* Can contain tables of structured data that the application often works with.
|
|
95
|
-
* Engine startup will load all tables ensuring that data is ready once startup finishes
|
|
96
|
-
* Use sparingly, this is not suitable for very large amounts of data as all the data will be in memory and deserialization will stall engine startup
|
|
97
|
-
* Example: inventory item table, monster table
|
|
98
|
-
* @readonly
|
|
99
|
-
* @type {StaticKnowledgeDatabase}
|
|
100
|
-
*/
|
|
101
|
-
staticKnowledge = new StaticKnowledgeDatabase();
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* @readonly
|
|
105
|
-
* @type {EnginePluginManager}
|
|
106
|
-
*/
|
|
107
|
-
plugins = new EnginePluginManager();
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Main simulation ticker
|
|
111
|
-
* @readonly
|
|
112
|
-
* @type {Ticker}
|
|
113
|
-
*/
|
|
114
|
-
ticker = new Ticker();
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Default executor, enables time-sharing concurrency
|
|
118
|
-
* @readonly
|
|
119
|
-
* @type {ConcurrentExecutor}
|
|
120
|
-
*/
|
|
121
|
-
executor = new ConcurrentExecutor(1, 10);
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
*
|
|
125
|
-
* @param {EnginePlatform} platform
|
|
126
|
-
* @param {EntityManager} [entityManager]
|
|
127
|
-
* @param {boolean} [enableGraphics]
|
|
128
|
-
* @param {boolean} [enableAudio]
|
|
129
|
-
* @param {boolean} [debug]
|
|
130
|
-
* @constructor
|
|
131
|
-
*/
|
|
132
|
-
constructor(platform, {
|
|
133
|
-
entityManager,
|
|
134
|
-
enableGraphics = true,
|
|
135
|
-
enableAudio = true,
|
|
136
|
-
debug = true
|
|
137
|
-
} = {}) {
|
|
138
|
-
assert.defined(platform, 'platform');
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
*
|
|
142
|
-
* @type {EnginePlatform}
|
|
143
|
-
*/
|
|
144
|
-
this.platform = platform;
|
|
145
|
-
|
|
146
|
-
this.staticKnowledge.validation_enabled = debug;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
*
|
|
151
|
-
* @type {MetricCollection}
|
|
152
|
-
*/
|
|
153
|
-
this.performance = new MetricCollection();
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
this.__using_external_entity_manager = entityManager !== undefined;
|
|
157
|
-
|
|
158
|
-
//setup entity component system
|
|
159
|
-
if (this.__using_external_entity_manager) {
|
|
160
|
-
// external entity manager provided
|
|
161
|
-
this.entityManager = entityManager;
|
|
162
|
-
} else {
|
|
163
|
-
this.entityManager = new EntityManager();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
*
|
|
169
|
-
* @type {AssetManager<Engine>}
|
|
170
|
-
*/
|
|
171
|
-
this.assetManager = new AssetManager({
|
|
172
|
-
context: this,
|
|
173
|
-
executor: this.executor
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
this.__performacne_monitor = new PeriodicConsolePrinter(15, () => {
|
|
177
|
-
const metrics = this.performance;
|
|
178
|
-
const stats = new MetricStatistics();
|
|
179
|
-
|
|
180
|
-
// RENDER
|
|
181
|
-
const m_render = metrics.get(METRIC_ID_RENDER);
|
|
182
|
-
|
|
183
|
-
m_render.computeStats(stats);
|
|
184
|
-
|
|
185
|
-
const v_render = (stats.mean * 1000).toFixed(2);
|
|
186
|
-
|
|
187
|
-
// SIMULATION
|
|
188
|
-
const m_sim = metrics.get(METRIC_ID_SIMULATION);
|
|
189
|
-
|
|
190
|
-
m_sim.computeStats(stats);
|
|
191
|
-
|
|
192
|
-
const v_sim = (stats.mean * 1000).toFixed(2);
|
|
193
|
-
|
|
194
|
-
// FPS
|
|
195
|
-
|
|
196
|
-
const m_frame = metrics.get(METRIC_ID_FRAME);
|
|
197
|
-
|
|
198
|
-
m_frame.computeStats(stats);
|
|
199
|
-
|
|
200
|
-
const v_fps = (1 / stats.mean).toFixed(2);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// clear stats
|
|
204
|
-
m_render.clear();
|
|
205
|
-
m_sim.clear();
|
|
206
|
-
m_frame.clear();
|
|
207
|
-
|
|
208
|
-
return `FPS: ${v_fps}, RENDER: ${v_render}ms, SIMULATION: ${v_sim}ms`;
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (!this.__using_external_entity_manager) {
|
|
213
|
-
// subscribe simulator to common ticker
|
|
214
|
-
this.ticker.onTick.add(timeDelta => {
|
|
215
|
-
const t0 = performance.now();
|
|
216
|
-
this.entityManager.update(timeDelta);
|
|
217
|
-
|
|
218
|
-
const t1 = performance.now();
|
|
219
|
-
const duration = (t1 - t0) / 1000;
|
|
220
|
-
|
|
221
|
-
// record frame time
|
|
222
|
-
this.performance.get(METRIC_ID_SIMULATION).record(duration);
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// initialize performance metrics
|
|
227
|
-
this.performance.create({
|
|
228
|
-
name: METRIC_ID_RENDER
|
|
229
|
-
});
|
|
230
|
-
this.performance.create({
|
|
231
|
-
name: METRIC_ID_SIMULATION
|
|
232
|
-
});
|
|
233
|
-
this.performance.create({
|
|
234
|
-
name: METRIC_ID_FRAME
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
*
|
|
239
|
-
* @type {OptionGroup}
|
|
240
|
-
*/
|
|
241
|
-
this.options = new OptionGroup("options");
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
*
|
|
245
|
-
* @type {ModuleRegistry}
|
|
246
|
-
*/
|
|
247
|
-
this.moduleRegistry = new ModuleRegistry();
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* @readonly
|
|
251
|
-
* @type {BinarySerializationRegistry}
|
|
252
|
-
*/
|
|
253
|
-
this.binarySerializationRegistry = new BinarySerializationRegistry();
|
|
254
|
-
|
|
255
|
-
this.settings = new EngineSettings();
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* @deprecated use plugins instead
|
|
260
|
-
*/
|
|
261
|
-
this.services = {};
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
*
|
|
265
|
-
* @type {Storage}
|
|
266
|
-
*/
|
|
267
|
-
this.storage = this.platform.getStorage();
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
this.localization = new Localization();
|
|
271
|
-
this.localization.setAssetManager(this.assetManager);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (enableGraphics !== false) {
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* @type {GraphicsEngine}
|
|
278
|
-
*/
|
|
279
|
-
const graphicsEngine = new GraphicsEngine();
|
|
280
|
-
|
|
281
|
-
this.graphics = graphicsEngine;
|
|
282
|
-
|
|
283
|
-
// The renderer is not started here. Acquiring a WebGPU device is asynchronous and a
|
|
284
|
-
// constructor is the wrong place to fail from, so `start()` does it — which is also
|
|
285
|
-
// what makes the device restartable (E1).
|
|
286
|
-
|
|
287
|
-
graphicsEngine.on.contextLost.add(this.#handle_context_lost, this);
|
|
288
|
-
graphicsEngine.on.contextRestored.add(this.#handle_context_restored, this);
|
|
289
|
-
graphicsEngine.on.contextFailed.add(this.#handle_context_failed, this);
|
|
290
|
-
|
|
291
|
-
} else {
|
|
292
|
-
logger.info('enableGraphics option is not set, no graphics engine will be created');
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (enableAudio !== false) {
|
|
296
|
-
//sound engine
|
|
297
|
-
const soundEngine = new SoundEngine();
|
|
298
|
-
soundEngine.volume = 1;
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
*
|
|
302
|
-
* @type {SoundEngine}
|
|
303
|
-
*/
|
|
304
|
-
this.sound = soundEngine;
|
|
305
|
-
} else {
|
|
306
|
-
logger.info('enableAudio option is not set, no audio engine will be created');
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Graphical User Interface engine
|
|
311
|
-
* @type {GUIEngine}
|
|
312
|
-
*/
|
|
313
|
-
this.gui = new GUIEngine();
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* @readonly
|
|
317
|
-
* @type {SceneManager}
|
|
318
|
-
*/
|
|
319
|
-
this.sceneManager = new SceneManager(this.entityManager, this.ticker.clock);
|
|
320
|
-
|
|
321
|
-
this.initializeViews();
|
|
322
|
-
|
|
323
|
-
this.devices = {
|
|
324
|
-
pointer: new PointerDevice(this.viewStack.el),
|
|
325
|
-
keyboard: new KeyboardDevice(this.viewStack.el),
|
|
326
|
-
gamepad: new GamepadDevice()
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
this.devices.pointer.start();
|
|
330
|
-
this.devices.keyboard.start();
|
|
331
|
-
this.devices.gamepad.start();
|
|
332
|
-
|
|
333
|
-
//process settings
|
|
334
|
-
this.initializeSettings();
|
|
335
|
-
|
|
336
|
-
logger.info("engine initialized");
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Toggles GraphicsEngine rendering on and off
|
|
340
|
-
* @type {boolean}
|
|
341
|
-
*/
|
|
342
|
-
this.renderingEnabled = true;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
this.plugins.initialize(this);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
#clear_context_loss_pause_timer() {
|
|
349
|
-
if (this.#context_loss_pause_timer !== -1) {
|
|
350
|
-
clearTimeout(this.#context_loss_pause_timer);
|
|
351
|
-
this.#context_loss_pause_timer = -1;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Pauses the simulation, if it is running. {@link Ticker#pause} freezes the clock, so no simulation
|
|
357
|
-
* time accrues while we wait and the first step after resuming is a normal-sized one.
|
|
358
|
-
*
|
|
359
|
-
* A ticker that is already paused is left alone - somebody else owns that pause and it is not ours to
|
|
360
|
-
* undo when the context comes back.
|
|
361
|
-
*/
|
|
362
|
-
#pause_simulation_for_context_loss() {
|
|
363
|
-
this.#context_loss_pause_timer = -1;
|
|
364
|
-
|
|
365
|
-
if (this.#paused_by_context_loss) {
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const ticker = this.ticker;
|
|
370
|
-
|
|
371
|
-
if (!ticker.is_active || ticker.is_paused) {
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
this.#paused_by_context_loss = true;
|
|
376
|
-
|
|
377
|
-
logger.warn('Graphics context outage is taking a while, pausing simulation');
|
|
378
|
-
|
|
379
|
-
ticker.pause();
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
#handle_context_lost() {
|
|
383
|
-
logger.warn('Graphics context lost, waiting for recovery');
|
|
384
|
-
|
|
385
|
-
this.#clear_context_loss_pause_timer();
|
|
386
|
-
|
|
387
|
-
this.#context_loss_pause_timer = setTimeout(
|
|
388
|
-
() => this.#pause_simulation_for_context_loss(),
|
|
389
|
-
CONTEXT_LOSS_SIMULATION_PAUSE_DELAY * 1000
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
#handle_context_restored() {
|
|
394
|
-
this.#clear_context_loss_pause_timer();
|
|
395
|
-
|
|
396
|
-
if (this.#paused_by_context_loss) {
|
|
397
|
-
this.#paused_by_context_loss = false;
|
|
398
|
-
|
|
399
|
-
if (this.ticker.is_active) {
|
|
400
|
-
this.ticker.resume();
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
logger.info('Graphics context restored, rendering resumed');
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* @param {ShadeDeviceFailure} reason carries a message written to be shown to a person
|
|
409
|
-
*/
|
|
410
|
-
#handle_context_failed(reason) {
|
|
411
|
-
logger.error(`Graphics context recovery abandoned (${reason}), no further rendering is possible`);
|
|
412
|
-
|
|
413
|
-
this.#clear_context_loss_pause_timer();
|
|
414
|
-
|
|
415
|
-
// nothing can be drawn any more, so let the simulation stop rather than burn cycles invisibly.
|
|
416
|
-
// Application state is left intact, which keeps a save-on-failure handler viable.
|
|
417
|
-
this.#pause_simulation_for_context_loss();
|
|
418
|
-
|
|
419
|
-
this.viewStack.addChild(new GraphicsContextFailureView());
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
#initialize_audio() {
|
|
423
|
-
const sound = this.sound;
|
|
424
|
-
if (sound === null) {
|
|
425
|
-
// no sound engine
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// hook up context resumption to input
|
|
430
|
-
if (this.devices.pointer !== undefined) {
|
|
431
|
-
|
|
432
|
-
this.devices.pointer.on.down.addOne(sound.resumeContext, sound);
|
|
433
|
-
this.devices.keyboard.on.down.addOne(sound.resumeContext, sound);
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
initializeViews() {
|
|
439
|
-
|
|
440
|
-
const gameView = new EmptyView();
|
|
441
|
-
|
|
442
|
-
gameView.addClass('game-view');
|
|
443
|
-
|
|
444
|
-
gameView.css({
|
|
445
|
-
left: 0,
|
|
446
|
-
top: 0,
|
|
447
|
-
position: "absolute",
|
|
448
|
-
pointerEvents: "none"
|
|
449
|
-
});
|
|
450
|
-
this.gameView = gameView;
|
|
451
|
-
|
|
452
|
-
this.viewStack.push(gameView, 'game-view');
|
|
453
|
-
|
|
454
|
-
if (this.graphics !== null) {
|
|
455
|
-
|
|
456
|
-
const viewport = this.graphics.viewport;
|
|
457
|
-
|
|
458
|
-
viewport.css({
|
|
459
|
-
pointerEvents: "auto"
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
gameView.addChild(viewport);
|
|
463
|
-
//bind size of renderer viewport to game view
|
|
464
|
-
viewport.bindSignal(gameView.size.onChanged, viewport.size.set.bind(viewport.size));
|
|
465
|
-
gameView.on.linked.add(function () {
|
|
466
|
-
viewport.size.copy(gameView.size);
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
initializeSettings() {
|
|
473
|
-
logger.info('Initializing engine settings...');
|
|
474
|
-
|
|
475
|
-
const engine = this;
|
|
476
|
-
|
|
477
|
-
function setViewportToWindowSize() {
|
|
478
|
-
// get parent element
|
|
479
|
-
let parentElement = engine.viewStack.el.parentElement;
|
|
480
|
-
|
|
481
|
-
while (parentElement !== null && parentElement.innerWidth === undefined) {
|
|
482
|
-
// traverse up until we find an element with defined dimensions
|
|
483
|
-
parentElement = parentElement.parentElement;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (parentElement === null || parentElement === undefined) {
|
|
487
|
-
// no parent element, default to window
|
|
488
|
-
parentElement = window;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
engine.viewStack.size.set(parentElement.innerWidth, parentElement.innerHeight);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
this.settings.graphics_control_viewport_size.process(function (value) {
|
|
495
|
-
if (value) {
|
|
496
|
-
setViewportToWindowSize();
|
|
497
|
-
window.addEventListener("resize", setViewportToWindowSize, false);
|
|
498
|
-
} else {
|
|
499
|
-
window.removeEventListener("resize", setViewportToWindowSize);
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
logger.info('Engine settings initilized.');
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
benchmark() {
|
|
507
|
-
const duration = 2000;
|
|
508
|
-
let count = 0;
|
|
509
|
-
const t0 = Date.now();
|
|
510
|
-
let t1;
|
|
511
|
-
while (true) {
|
|
512
|
-
this.entityManager.update(0.0000000001);
|
|
513
|
-
t1 = Date.now();
|
|
514
|
-
if (t1 - t0 > duration) {
|
|
515
|
-
break;
|
|
516
|
-
}
|
|
517
|
-
count++;
|
|
518
|
-
}
|
|
519
|
-
//normalize
|
|
520
|
-
const elapsed = (t1 - t0) / 1000;
|
|
521
|
-
const rate = (count / elapsed);
|
|
522
|
-
return rate;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Returns preloader object
|
|
527
|
-
* @param {String} listURL
|
|
528
|
-
*/
|
|
529
|
-
loadAssetList(listURL) {
|
|
530
|
-
const preloader = new AssetPreloader();
|
|
531
|
-
const assetManager = this.assetManager;
|
|
532
|
-
assetManager.get({
|
|
533
|
-
path: listURL, type: "json", callback: function (asset) {
|
|
534
|
-
preloader.addAll(asset.create());
|
|
535
|
-
preloader.load(assetManager);
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
return preloader;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
render() {
|
|
542
|
-
assert.isInstanceOf(this, Engine, 'this', 'Engine');
|
|
543
|
-
|
|
544
|
-
const graphics = this.graphics;
|
|
545
|
-
|
|
546
|
-
if (graphics === null) {
|
|
547
|
-
// headless
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
if (graphics.autoDraw) {
|
|
552
|
-
// request redraw
|
|
553
|
-
graphics.needDraw = true;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
if (this.renderingEnabled && graphics.needDraw) {
|
|
557
|
-
|
|
558
|
-
const metrics = this.performance;
|
|
559
|
-
|
|
560
|
-
const t0 = performance.now();
|
|
561
|
-
|
|
562
|
-
graphics.render();
|
|
563
|
-
|
|
564
|
-
const t1 = performance.now();
|
|
565
|
-
const frame_time = (t1 - t0) / 1000;
|
|
566
|
-
|
|
567
|
-
// record frame time
|
|
568
|
-
metrics.get(METRIC_ID_RENDER).record(frame_time);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Startup
|
|
574
|
-
* @returns {Promise}
|
|
575
|
-
*/
|
|
576
|
-
async start() {
|
|
577
|
-
// First, and awaited: systems read `graphics.viewport` and size themselves off it during
|
|
578
|
-
// their own startup, so the device has to exist before the entity manager runs.
|
|
579
|
-
if (this.graphics !== null) {
|
|
580
|
-
await this.graphics.start();
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
this.assetManager.startup();
|
|
584
|
-
|
|
585
|
-
this.#initialize_audio();
|
|
586
|
-
|
|
587
|
-
const promiseEntityManager = () => {
|
|
588
|
-
return new Promise((resolve, reject) => {
|
|
589
|
-
//initialize entity manager
|
|
590
|
-
this.entityManager.startup(resolve, reject);
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
this.__performacne_monitor.start();
|
|
595
|
-
|
|
596
|
-
const sound = this.sound;
|
|
597
|
-
|
|
598
|
-
const soundStarted = sound === null
|
|
599
|
-
? Promise.resolve()
|
|
600
|
-
: sound.start();
|
|
601
|
-
|
|
602
|
-
const knowledge = this.staticKnowledge;
|
|
603
|
-
|
|
604
|
-
// static knowledge is engine-lifetime content and a stop does not drop it, so a restart keeps
|
|
605
|
-
// what the first start loaded. `load` accepts the Initial state only; callers that want the
|
|
606
|
-
// tables re-read call `reset` first.
|
|
607
|
-
const knowledgeLoaded = knowledge.state === StaticKnowledgeState.Initial
|
|
608
|
-
? knowledge.load(this.assetManager, this.executor)
|
|
609
|
-
: Promise.resolve();
|
|
610
|
-
|
|
611
|
-
return Promise.all([
|
|
612
|
-
soundStarted.then(promiseEntityManager),
|
|
613
|
-
knowledgeLoaded,
|
|
614
|
-
this.gui.startup(this),
|
|
615
|
-
this.plugins.startup()
|
|
616
|
-
]).then(async () => {
|
|
617
|
-
|
|
618
|
-
this.#last_frame_timestamp = performance.now();
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Starting the engine
|
|
622
|
-
*/
|
|
623
|
-
this.#animation_frame_handle = requestAnimationFrame(this.#animation_frame);
|
|
624
|
-
|
|
625
|
-
//start simulation
|
|
626
|
-
this.ticker.start({ maxTimeout: 200 });
|
|
627
|
-
//self.uiController.init(self);
|
|
628
|
-
|
|
629
|
-
//load options
|
|
630
|
-
await this.options.attachToStorage('lazykitty.komrade.options', this.storage);
|
|
631
|
-
|
|
632
|
-
logger.info('engine started');
|
|
633
|
-
}, function (e) {
|
|
634
|
-
logger.error(`Engine Failed to start: ${e}`);
|
|
635
|
-
|
|
636
|
-
// the caller awaits start() to know whether the engine came up;
|
|
637
|
-
// swallowing this reported success after a failed startup
|
|
638
|
-
throw e;
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* Fired every animation frame via {@link requestAnimationFrame}
|
|
646
|
-
*/
|
|
647
|
-
#animation_frame = () => {
|
|
648
|
-
|
|
649
|
-
const t0 = this.#last_frame_timestamp;
|
|
650
|
-
const t1 = performance.now();
|
|
651
|
-
this.#animation_frame_handle = requestAnimationFrame(this.#animation_frame);
|
|
652
|
-
this.render();
|
|
653
|
-
|
|
654
|
-
const frame_delay = (t1 - t0) / 1000;
|
|
655
|
-
|
|
656
|
-
// swap variables
|
|
657
|
-
this.#last_frame_timestamp = t1;
|
|
658
|
-
|
|
659
|
-
// record metric
|
|
660
|
-
this.performance.get(METRIC_ID_FRAME).record(frame_delay);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* Shutdown the engine, disposing all relevant resources
|
|
665
|
-
*/
|
|
666
|
-
async stop() {
|
|
667
|
-
this.__performacne_monitor.stop();
|
|
668
|
-
|
|
669
|
-
// stop the ticker. pause() throws when the loop is not currently
|
|
670
|
-
// running, which would abort the whole teardown below
|
|
671
|
-
this.ticker.stop();
|
|
672
|
-
|
|
673
|
-
// and the render loop, which re-requests itself every frame and would
|
|
674
|
-
// otherwise keep calling render() on a torn-down engine forever
|
|
675
|
-
cancelAnimationFrame(this.#animation_frame_handle);
|
|
676
|
-
this.#animation_frame_handle = -1;
|
|
677
|
-
|
|
678
|
-
//
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
//
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { assert } from "../core/assert.js";
|
|
6
|
+
import Vector1 from "../core/geom/Vector1.js";
|
|
7
|
+
import { Localization } from "../core/localization/Localization.js";
|
|
8
|
+
import { ModuleRegistry } from "../core/model/ModuleRegistry.js";
|
|
9
|
+
import ObservedBoolean from "../core/model/ObservedBoolean.js";
|
|
10
|
+
import ConcurrentExecutor from '../core/process/executor/ConcurrentExecutor.js';
|
|
11
|
+
import EmptyView from "../view/elements/EmptyView.js";
|
|
12
|
+
import { ViewStack } from "../view/elements/navigation/ViewStack.js";
|
|
13
|
+
import { GraphicsContextFailureView } from "../view/graphics/GraphicsContextFailureView.js";
|
|
14
|
+
|
|
15
|
+
import { AssetManager } from './asset/AssetManager.js';
|
|
16
|
+
import { AssetPreloader } from "./asset/preloader/AssetPreloader.js";
|
|
17
|
+
import { MetricCollection } from "./development/performance/MetricCollection.js";
|
|
18
|
+
import { MetricStatistics } from "./development/performance/MetricStatistics.js";
|
|
19
|
+
import { PeriodicConsolePrinter } from "./development/performance/monitor/PeriodicConsolePrinter.js";
|
|
20
|
+
import { EntityManager } from "./ecs/EntityManager.js";
|
|
21
|
+
import { BinarySerializationRegistry } from "./ecs/storage/binary/BinarySerializationRegistry.js";
|
|
22
|
+
import { GraphicsEngine } from './graphics3/GraphicsEngine.js';
|
|
23
|
+
import { GamepadDevice } from "./input/devices/GamepadDevice.js";
|
|
24
|
+
import KeyboardDevice from "./input/devices/KeyboardDevice.js";
|
|
25
|
+
import { PointerDevice } from "./input/devices/PointerDevice.js";
|
|
26
|
+
import { StaticKnowledgeDatabase, StaticKnowledgeState } from "./knowledge/database/StaticKnowledgeDatabase.js";
|
|
27
|
+
import { logger } from "./logging/GlobalLogger.js";
|
|
28
|
+
import { OptionGroup } from "./options/OptionGroup.js";
|
|
29
|
+
import { EnginePluginManager } from "./plugin/EnginePluginManager.js";
|
|
30
|
+
import SceneManager from "./scene/SceneManager.js";
|
|
31
|
+
import Ticker from "./simulation/Ticker.js";
|
|
32
|
+
import SoundEngine from './sound/SoundEngine.js';
|
|
33
|
+
|
|
34
|
+
import GUIEngine from './ui/GUIEngine.js';
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class EngineSettings {
|
|
38
|
+
graphics_control_viewport_size = new ObservedBoolean(true);
|
|
39
|
+
simulation_speed = new Vector1(1);
|
|
40
|
+
input_mouse_sensitivity = new Vector1(5);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const METRIC_ID_FRAME = 'frame_delay';
|
|
44
|
+
const METRIC_ID_RENDER = 'render_time';
|
|
45
|
+
const METRIC_ID_SIMULATION = 'simulation_time';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* How long a context loss is tolerated before the simulation is paused as well, in seconds. Most losses
|
|
49
|
+
* are resolved in well under a second, and stopping the world for those would be more disruptive than
|
|
50
|
+
* letting it run; a longer outage is worth pausing for, as the player cannot see what is happening.
|
|
51
|
+
* @type {number}
|
|
52
|
+
*/
|
|
53
|
+
const CONTEXT_LOSS_SIMULATION_PAUSE_DELAY = 2;
|
|
54
|
+
|
|
55
|
+
class Engine {
|
|
56
|
+
#last_frame_timestamp = 0;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Handle of the pending render-loop frame, `-1` when the loop is not running
|
|
60
|
+
* @type {number}
|
|
61
|
+
*/
|
|
62
|
+
#animation_frame_handle = -1;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Handle of the pending "pause the simulation, this outage is taking a while" timer, `-1` when none
|
|
66
|
+
* @type {number}
|
|
67
|
+
*/
|
|
68
|
+
#context_loss_pause_timer = -1;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether the simulation was paused because of a context loss, and therefore should be resumed once
|
|
72
|
+
* the context comes back
|
|
73
|
+
* @type {boolean}
|
|
74
|
+
*/
|
|
75
|
+
#paused_by_context_loss = false;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Absent when the engine is constructed with `enableGraphics: false`.
|
|
79
|
+
* @type {GraphicsEngine|null}
|
|
80
|
+
*/
|
|
81
|
+
graphics = null;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Absent when the engine is constructed with `enableAudio: false`.
|
|
85
|
+
* @type {SoundEngine|null}
|
|
86
|
+
*/
|
|
87
|
+
sound = null;
|
|
88
|
+
|
|
89
|
+
viewStack = new ViewStack();
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
*
|
|
93
|
+
* Static database
|
|
94
|
+
* Can contain tables of structured data that the application often works with.
|
|
95
|
+
* Engine startup will load all tables ensuring that data is ready once startup finishes
|
|
96
|
+
* Use sparingly, this is not suitable for very large amounts of data as all the data will be in memory and deserialization will stall engine startup
|
|
97
|
+
* Example: inventory item table, monster table
|
|
98
|
+
* @readonly
|
|
99
|
+
* @type {StaticKnowledgeDatabase}
|
|
100
|
+
*/
|
|
101
|
+
staticKnowledge = new StaticKnowledgeDatabase();
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @readonly
|
|
105
|
+
* @type {EnginePluginManager}
|
|
106
|
+
*/
|
|
107
|
+
plugins = new EnginePluginManager();
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Main simulation ticker
|
|
111
|
+
* @readonly
|
|
112
|
+
* @type {Ticker}
|
|
113
|
+
*/
|
|
114
|
+
ticker = new Ticker();
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Default executor, enables time-sharing concurrency
|
|
118
|
+
* @readonly
|
|
119
|
+
* @type {ConcurrentExecutor}
|
|
120
|
+
*/
|
|
121
|
+
executor = new ConcurrentExecutor(1, 10);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
*
|
|
125
|
+
* @param {EnginePlatform} platform
|
|
126
|
+
* @param {EntityManager} [entityManager]
|
|
127
|
+
* @param {boolean} [enableGraphics]
|
|
128
|
+
* @param {boolean} [enableAudio]
|
|
129
|
+
* @param {boolean} [debug]
|
|
130
|
+
* @constructor
|
|
131
|
+
*/
|
|
132
|
+
constructor(platform, {
|
|
133
|
+
entityManager,
|
|
134
|
+
enableGraphics = true,
|
|
135
|
+
enableAudio = true,
|
|
136
|
+
debug = true
|
|
137
|
+
} = {}) {
|
|
138
|
+
assert.defined(platform, 'platform');
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
*
|
|
142
|
+
* @type {EnginePlatform}
|
|
143
|
+
*/
|
|
144
|
+
this.platform = platform;
|
|
145
|
+
|
|
146
|
+
this.staticKnowledge.validation_enabled = debug;
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
*
|
|
151
|
+
* @type {MetricCollection}
|
|
152
|
+
*/
|
|
153
|
+
this.performance = new MetricCollection();
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
this.__using_external_entity_manager = entityManager !== undefined;
|
|
157
|
+
|
|
158
|
+
//setup entity component system
|
|
159
|
+
if (this.__using_external_entity_manager) {
|
|
160
|
+
// external entity manager provided
|
|
161
|
+
this.entityManager = entityManager;
|
|
162
|
+
} else {
|
|
163
|
+
this.entityManager = new EntityManager();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
*
|
|
169
|
+
* @type {AssetManager<Engine>}
|
|
170
|
+
*/
|
|
171
|
+
this.assetManager = new AssetManager({
|
|
172
|
+
context: this,
|
|
173
|
+
executor: this.executor
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
this.__performacne_monitor = new PeriodicConsolePrinter(15, () => {
|
|
177
|
+
const metrics = this.performance;
|
|
178
|
+
const stats = new MetricStatistics();
|
|
179
|
+
|
|
180
|
+
// RENDER
|
|
181
|
+
const m_render = metrics.get(METRIC_ID_RENDER);
|
|
182
|
+
|
|
183
|
+
m_render.computeStats(stats);
|
|
184
|
+
|
|
185
|
+
const v_render = (stats.mean * 1000).toFixed(2);
|
|
186
|
+
|
|
187
|
+
// SIMULATION
|
|
188
|
+
const m_sim = metrics.get(METRIC_ID_SIMULATION);
|
|
189
|
+
|
|
190
|
+
m_sim.computeStats(stats);
|
|
191
|
+
|
|
192
|
+
const v_sim = (stats.mean * 1000).toFixed(2);
|
|
193
|
+
|
|
194
|
+
// FPS
|
|
195
|
+
|
|
196
|
+
const m_frame = metrics.get(METRIC_ID_FRAME);
|
|
197
|
+
|
|
198
|
+
m_frame.computeStats(stats);
|
|
199
|
+
|
|
200
|
+
const v_fps = (1 / stats.mean).toFixed(2);
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
// clear stats
|
|
204
|
+
m_render.clear();
|
|
205
|
+
m_sim.clear();
|
|
206
|
+
m_frame.clear();
|
|
207
|
+
|
|
208
|
+
return `FPS: ${v_fps}, RENDER: ${v_render}ms, SIMULATION: ${v_sim}ms`;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if (!this.__using_external_entity_manager) {
|
|
213
|
+
// subscribe simulator to common ticker
|
|
214
|
+
this.ticker.onTick.add(timeDelta => {
|
|
215
|
+
const t0 = performance.now();
|
|
216
|
+
this.entityManager.update(timeDelta);
|
|
217
|
+
|
|
218
|
+
const t1 = performance.now();
|
|
219
|
+
const duration = (t1 - t0) / 1000;
|
|
220
|
+
|
|
221
|
+
// record frame time
|
|
222
|
+
this.performance.get(METRIC_ID_SIMULATION).record(duration);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// initialize performance metrics
|
|
227
|
+
this.performance.create({
|
|
228
|
+
name: METRIC_ID_RENDER
|
|
229
|
+
});
|
|
230
|
+
this.performance.create({
|
|
231
|
+
name: METRIC_ID_SIMULATION
|
|
232
|
+
});
|
|
233
|
+
this.performance.create({
|
|
234
|
+
name: METRIC_ID_FRAME
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
*
|
|
239
|
+
* @type {OptionGroup}
|
|
240
|
+
*/
|
|
241
|
+
this.options = new OptionGroup("options");
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
*
|
|
245
|
+
* @type {ModuleRegistry}
|
|
246
|
+
*/
|
|
247
|
+
this.moduleRegistry = new ModuleRegistry();
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @readonly
|
|
251
|
+
* @type {BinarySerializationRegistry}
|
|
252
|
+
*/
|
|
253
|
+
this.binarySerializationRegistry = new BinarySerializationRegistry();
|
|
254
|
+
|
|
255
|
+
this.settings = new EngineSettings();
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @deprecated use plugins instead
|
|
260
|
+
*/
|
|
261
|
+
this.services = {};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
*
|
|
265
|
+
* @type {Storage}
|
|
266
|
+
*/
|
|
267
|
+
this.storage = this.platform.getStorage();
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
this.localization = new Localization();
|
|
271
|
+
this.localization.setAssetManager(this.assetManager);
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
if (enableGraphics !== false) {
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @type {GraphicsEngine}
|
|
278
|
+
*/
|
|
279
|
+
const graphicsEngine = new GraphicsEngine();
|
|
280
|
+
|
|
281
|
+
this.graphics = graphicsEngine;
|
|
282
|
+
|
|
283
|
+
// The renderer is not started here. Acquiring a WebGPU device is asynchronous and a
|
|
284
|
+
// constructor is the wrong place to fail from, so `start()` does it — which is also
|
|
285
|
+
// what makes the device restartable (E1).
|
|
286
|
+
|
|
287
|
+
graphicsEngine.on.contextLost.add(this.#handle_context_lost, this);
|
|
288
|
+
graphicsEngine.on.contextRestored.add(this.#handle_context_restored, this);
|
|
289
|
+
graphicsEngine.on.contextFailed.add(this.#handle_context_failed, this);
|
|
290
|
+
|
|
291
|
+
} else {
|
|
292
|
+
logger.info('enableGraphics option is not set, no graphics engine will be created');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (enableAudio !== false) {
|
|
296
|
+
//sound engine
|
|
297
|
+
const soundEngine = new SoundEngine();
|
|
298
|
+
soundEngine.volume = 1;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
*
|
|
302
|
+
* @type {SoundEngine}
|
|
303
|
+
*/
|
|
304
|
+
this.sound = soundEngine;
|
|
305
|
+
} else {
|
|
306
|
+
logger.info('enableAudio option is not set, no audio engine will be created');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Graphical User Interface engine
|
|
311
|
+
* @type {GUIEngine}
|
|
312
|
+
*/
|
|
313
|
+
this.gui = new GUIEngine();
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @readonly
|
|
317
|
+
* @type {SceneManager}
|
|
318
|
+
*/
|
|
319
|
+
this.sceneManager = new SceneManager(this.entityManager, this.ticker.clock);
|
|
320
|
+
|
|
321
|
+
this.initializeViews();
|
|
322
|
+
|
|
323
|
+
this.devices = {
|
|
324
|
+
pointer: new PointerDevice(this.viewStack.el),
|
|
325
|
+
keyboard: new KeyboardDevice(this.viewStack.el),
|
|
326
|
+
gamepad: new GamepadDevice()
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
this.devices.pointer.start();
|
|
330
|
+
this.devices.keyboard.start();
|
|
331
|
+
this.devices.gamepad.start();
|
|
332
|
+
|
|
333
|
+
//process settings
|
|
334
|
+
this.initializeSettings();
|
|
335
|
+
|
|
336
|
+
logger.info("engine initialized");
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Toggles GraphicsEngine rendering on and off
|
|
340
|
+
* @type {boolean}
|
|
341
|
+
*/
|
|
342
|
+
this.renderingEnabled = true;
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
this.plugins.initialize(this);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
#clear_context_loss_pause_timer() {
|
|
349
|
+
if (this.#context_loss_pause_timer !== -1) {
|
|
350
|
+
clearTimeout(this.#context_loss_pause_timer);
|
|
351
|
+
this.#context_loss_pause_timer = -1;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Pauses the simulation, if it is running. {@link Ticker#pause} freezes the clock, so no simulation
|
|
357
|
+
* time accrues while we wait and the first step after resuming is a normal-sized one.
|
|
358
|
+
*
|
|
359
|
+
* A ticker that is already paused is left alone - somebody else owns that pause and it is not ours to
|
|
360
|
+
* undo when the context comes back.
|
|
361
|
+
*/
|
|
362
|
+
#pause_simulation_for_context_loss() {
|
|
363
|
+
this.#context_loss_pause_timer = -1;
|
|
364
|
+
|
|
365
|
+
if (this.#paused_by_context_loss) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const ticker = this.ticker;
|
|
370
|
+
|
|
371
|
+
if (!ticker.is_active || ticker.is_paused) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this.#paused_by_context_loss = true;
|
|
376
|
+
|
|
377
|
+
logger.warn('Graphics context outage is taking a while, pausing simulation');
|
|
378
|
+
|
|
379
|
+
ticker.pause();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#handle_context_lost() {
|
|
383
|
+
logger.warn('Graphics context lost, waiting for recovery');
|
|
384
|
+
|
|
385
|
+
this.#clear_context_loss_pause_timer();
|
|
386
|
+
|
|
387
|
+
this.#context_loss_pause_timer = setTimeout(
|
|
388
|
+
() => this.#pause_simulation_for_context_loss(),
|
|
389
|
+
CONTEXT_LOSS_SIMULATION_PAUSE_DELAY * 1000
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#handle_context_restored() {
|
|
394
|
+
this.#clear_context_loss_pause_timer();
|
|
395
|
+
|
|
396
|
+
if (this.#paused_by_context_loss) {
|
|
397
|
+
this.#paused_by_context_loss = false;
|
|
398
|
+
|
|
399
|
+
if (this.ticker.is_active) {
|
|
400
|
+
this.ticker.resume();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
logger.info('Graphics context restored, rendering resumed');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* @param {ShadeDeviceFailure} reason carries a message written to be shown to a person
|
|
409
|
+
*/
|
|
410
|
+
#handle_context_failed(reason) {
|
|
411
|
+
logger.error(`Graphics context recovery abandoned (${reason}), no further rendering is possible`);
|
|
412
|
+
|
|
413
|
+
this.#clear_context_loss_pause_timer();
|
|
414
|
+
|
|
415
|
+
// nothing can be drawn any more, so let the simulation stop rather than burn cycles invisibly.
|
|
416
|
+
// Application state is left intact, which keeps a save-on-failure handler viable.
|
|
417
|
+
this.#pause_simulation_for_context_loss();
|
|
418
|
+
|
|
419
|
+
this.viewStack.addChild(new GraphicsContextFailureView());
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
#initialize_audio() {
|
|
423
|
+
const sound = this.sound;
|
|
424
|
+
if (sound === null) {
|
|
425
|
+
// no sound engine
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// hook up context resumption to input
|
|
430
|
+
if (this.devices.pointer !== undefined) {
|
|
431
|
+
|
|
432
|
+
this.devices.pointer.on.down.addOne(sound.resumeContext, sound);
|
|
433
|
+
this.devices.keyboard.on.down.addOne(sound.resumeContext, sound);
|
|
434
|
+
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
initializeViews() {
|
|
439
|
+
|
|
440
|
+
const gameView = new EmptyView();
|
|
441
|
+
|
|
442
|
+
gameView.addClass('game-view');
|
|
443
|
+
|
|
444
|
+
gameView.css({
|
|
445
|
+
left: 0,
|
|
446
|
+
top: 0,
|
|
447
|
+
position: "absolute",
|
|
448
|
+
pointerEvents: "none"
|
|
449
|
+
});
|
|
450
|
+
this.gameView = gameView;
|
|
451
|
+
|
|
452
|
+
this.viewStack.push(gameView, 'game-view');
|
|
453
|
+
|
|
454
|
+
if (this.graphics !== null) {
|
|
455
|
+
|
|
456
|
+
const viewport = this.graphics.viewport;
|
|
457
|
+
|
|
458
|
+
viewport.css({
|
|
459
|
+
pointerEvents: "auto"
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
gameView.addChild(viewport);
|
|
463
|
+
//bind size of renderer viewport to game view
|
|
464
|
+
viewport.bindSignal(gameView.size.onChanged, viewport.size.set.bind(viewport.size));
|
|
465
|
+
gameView.on.linked.add(function () {
|
|
466
|
+
viewport.size.copy(gameView.size);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
initializeSettings() {
|
|
473
|
+
logger.info('Initializing engine settings...');
|
|
474
|
+
|
|
475
|
+
const engine = this;
|
|
476
|
+
|
|
477
|
+
function setViewportToWindowSize() {
|
|
478
|
+
// get parent element
|
|
479
|
+
let parentElement = engine.viewStack.el.parentElement;
|
|
480
|
+
|
|
481
|
+
while (parentElement !== null && parentElement.innerWidth === undefined) {
|
|
482
|
+
// traverse up until we find an element with defined dimensions
|
|
483
|
+
parentElement = parentElement.parentElement;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (parentElement === null || parentElement === undefined) {
|
|
487
|
+
// no parent element, default to window
|
|
488
|
+
parentElement = window;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
engine.viewStack.size.set(parentElement.innerWidth, parentElement.innerHeight);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
this.settings.graphics_control_viewport_size.process(function (value) {
|
|
495
|
+
if (value) {
|
|
496
|
+
setViewportToWindowSize();
|
|
497
|
+
window.addEventListener("resize", setViewportToWindowSize, false);
|
|
498
|
+
} else {
|
|
499
|
+
window.removeEventListener("resize", setViewportToWindowSize);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
logger.info('Engine settings initilized.');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
benchmark() {
|
|
507
|
+
const duration = 2000;
|
|
508
|
+
let count = 0;
|
|
509
|
+
const t0 = Date.now();
|
|
510
|
+
let t1;
|
|
511
|
+
while (true) {
|
|
512
|
+
this.entityManager.update(0.0000000001);
|
|
513
|
+
t1 = Date.now();
|
|
514
|
+
if (t1 - t0 > duration) {
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
count++;
|
|
518
|
+
}
|
|
519
|
+
//normalize
|
|
520
|
+
const elapsed = (t1 - t0) / 1000;
|
|
521
|
+
const rate = (count / elapsed);
|
|
522
|
+
return rate;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Returns preloader object
|
|
527
|
+
* @param {String} listURL
|
|
528
|
+
*/
|
|
529
|
+
loadAssetList(listURL) {
|
|
530
|
+
const preloader = new AssetPreloader();
|
|
531
|
+
const assetManager = this.assetManager;
|
|
532
|
+
assetManager.get({
|
|
533
|
+
path: listURL, type: "json", callback: function (asset) {
|
|
534
|
+
preloader.addAll(asset.create());
|
|
535
|
+
preloader.load(assetManager);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
return preloader;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
render() {
|
|
542
|
+
assert.isInstanceOf(this, Engine, 'this', 'Engine');
|
|
543
|
+
|
|
544
|
+
const graphics = this.graphics;
|
|
545
|
+
|
|
546
|
+
if (graphics === null) {
|
|
547
|
+
// headless
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (graphics.autoDraw) {
|
|
552
|
+
// request redraw
|
|
553
|
+
graphics.needDraw = true;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (this.renderingEnabled && graphics.needDraw) {
|
|
557
|
+
|
|
558
|
+
const metrics = this.performance;
|
|
559
|
+
|
|
560
|
+
const t0 = performance.now();
|
|
561
|
+
|
|
562
|
+
graphics.render();
|
|
563
|
+
|
|
564
|
+
const t1 = performance.now();
|
|
565
|
+
const frame_time = (t1 - t0) / 1000;
|
|
566
|
+
|
|
567
|
+
// record frame time
|
|
568
|
+
metrics.get(METRIC_ID_RENDER).record(frame_time);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Startup
|
|
574
|
+
* @returns {Promise}
|
|
575
|
+
*/
|
|
576
|
+
async start() {
|
|
577
|
+
// First, and awaited: systems read `graphics.viewport` and size themselves off it during
|
|
578
|
+
// their own startup, so the device has to exist before the entity manager runs.
|
|
579
|
+
if (this.graphics !== null) {
|
|
580
|
+
await this.graphics.start();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
this.assetManager.startup();
|
|
584
|
+
|
|
585
|
+
this.#initialize_audio();
|
|
586
|
+
|
|
587
|
+
const promiseEntityManager = () => {
|
|
588
|
+
return new Promise((resolve, reject) => {
|
|
589
|
+
//initialize entity manager
|
|
590
|
+
this.entityManager.startup(resolve, reject);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
this.__performacne_monitor.start();
|
|
595
|
+
|
|
596
|
+
const sound = this.sound;
|
|
597
|
+
|
|
598
|
+
const soundStarted = sound === null
|
|
599
|
+
? Promise.resolve()
|
|
600
|
+
: sound.start();
|
|
601
|
+
|
|
602
|
+
const knowledge = this.staticKnowledge;
|
|
603
|
+
|
|
604
|
+
// static knowledge is engine-lifetime content and a stop does not drop it, so a restart keeps
|
|
605
|
+
// what the first start loaded. `load` accepts the Initial state only; callers that want the
|
|
606
|
+
// tables re-read call `reset` first.
|
|
607
|
+
const knowledgeLoaded = knowledge.state === StaticKnowledgeState.Initial
|
|
608
|
+
? knowledge.load(this.assetManager, this.executor)
|
|
609
|
+
: Promise.resolve();
|
|
610
|
+
|
|
611
|
+
return Promise.all([
|
|
612
|
+
soundStarted.then(promiseEntityManager),
|
|
613
|
+
knowledgeLoaded,
|
|
614
|
+
this.gui.startup(this),
|
|
615
|
+
this.plugins.startup()
|
|
616
|
+
]).then(async () => {
|
|
617
|
+
|
|
618
|
+
this.#last_frame_timestamp = performance.now();
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Starting the engine
|
|
622
|
+
*/
|
|
623
|
+
this.#animation_frame_handle = requestAnimationFrame(this.#animation_frame);
|
|
624
|
+
|
|
625
|
+
//start simulation
|
|
626
|
+
this.ticker.start({ maxTimeout: 200 });
|
|
627
|
+
//self.uiController.init(self);
|
|
628
|
+
|
|
629
|
+
//load options
|
|
630
|
+
await this.options.attachToStorage('lazykitty.komrade.options', this.storage);
|
|
631
|
+
|
|
632
|
+
logger.info('engine started');
|
|
633
|
+
}, function (e) {
|
|
634
|
+
logger.error(`Engine Failed to start: ${e}`);
|
|
635
|
+
|
|
636
|
+
// the caller awaits start() to know whether the engine came up;
|
|
637
|
+
// swallowing this reported success after a failed startup
|
|
638
|
+
throw e;
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Fired every animation frame via {@link requestAnimationFrame}
|
|
646
|
+
*/
|
|
647
|
+
#animation_frame = () => {
|
|
648
|
+
|
|
649
|
+
const t0 = this.#last_frame_timestamp;
|
|
650
|
+
const t1 = performance.now();
|
|
651
|
+
this.#animation_frame_handle = requestAnimationFrame(this.#animation_frame);
|
|
652
|
+
this.render();
|
|
653
|
+
|
|
654
|
+
const frame_delay = (t1 - t0) / 1000;
|
|
655
|
+
|
|
656
|
+
// swap variables
|
|
657
|
+
this.#last_frame_timestamp = t1;
|
|
658
|
+
|
|
659
|
+
// record metric
|
|
660
|
+
this.performance.get(METRIC_ID_FRAME).record(frame_delay);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Shutdown the engine, disposing all relevant resources
|
|
665
|
+
*/
|
|
666
|
+
async stop() {
|
|
667
|
+
this.__performacne_monitor.stop();
|
|
668
|
+
|
|
669
|
+
// stop the ticker. pause() throws when the loop is not currently
|
|
670
|
+
// running, which would abort the whole teardown below
|
|
671
|
+
this.ticker.stop();
|
|
672
|
+
|
|
673
|
+
// and the render loop, which re-requests itself every frame and would
|
|
674
|
+
// otherwise keep calling render() on a torn-down engine forever
|
|
675
|
+
cancelAnimationFrame(this.#animation_frame_handle);
|
|
676
|
+
this.#animation_frame_handle = -1;
|
|
677
|
+
|
|
678
|
+
// the input devices the constructor started. The gamepad one polls on its own animation
|
|
679
|
+
// frame loop, so left alone it outlives the engine the same way the render loop would;
|
|
680
|
+
// the other two hold listeners on a view stack that is about to go away
|
|
681
|
+
this.devices.pointer.stop();
|
|
682
|
+
this.devices.keyboard.stop();
|
|
683
|
+
this.devices.gamepad.stop();
|
|
684
|
+
|
|
685
|
+
// shutdown entity manager
|
|
686
|
+
if (!this.__using_external_entity_manager) {
|
|
687
|
+
await new Promise((resolve, reject) => this.entityManager.shutdown(resolve, reject));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// shutdown plugins
|
|
691
|
+
await this.plugins.shutdown();
|
|
692
|
+
|
|
693
|
+
// stop gui
|
|
694
|
+
await this.gui.shutdown();
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
// TODO shutdown executor
|
|
698
|
+
|
|
699
|
+
await this.assetManager.shutdown();
|
|
700
|
+
|
|
701
|
+
// last, so that everything above still has a live context to release its GPU resources through
|
|
702
|
+
const graphics = this.graphics;
|
|
703
|
+
|
|
704
|
+
if (graphics !== null) {
|
|
705
|
+
graphics.stop();
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
exit() {
|
|
710
|
+
window.close();
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* @returns {Promise}
|
|
715
|
+
*/
|
|
716
|
+
requestExit() {
|
|
717
|
+
return this.gui.confirmTextDialog({
|
|
718
|
+
title: this.localization.getString('system_confirm_exit_to_system.title'),
|
|
719
|
+
text: this.localization.getString('system_confirm_exit_to_system.text')
|
|
720
|
+
}).then(() => {
|
|
721
|
+
this.exit();
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* @readonly
|
|
729
|
+
* @type {boolean}
|
|
730
|
+
*/
|
|
731
|
+
Engine.prototype.isEngine = true;
|
|
732
|
+
|
|
733
|
+
export default Engine;
|