-
Notifications
You must be signed in to change notification settings - Fork 69
Expand file tree
/
Copy pathmain.cpp
More file actions
499 lines (451 loc) · 20.8 KB
/
Copy pathmain.cpp
File metadata and controls
499 lines (451 loc) · 20.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
//
// Created by Engin Manap on 10.02.2016.
//
#include "main.h"
#include "limonAPI/Graphics/GraphicsInterface.h"
#include "SDL2Helper.h"
#include "World.h"
#include "WorldLoader.h"
#include "GameObjects/GUIImage.h"
#include "Python/ScriptManager.h"
#include "Profiler/ProfilerSystem.h"
#include <pthread.h>
#include "Profiler/ProfilerMacros.h"
#include "Utils/FrameTimeTracker.h"
#include "Graphics/TransformTextureRing.h"
#include "Material.h"
#include "Assets/ModelToWorldConverter.h"
#include <algorithm>
#include <cstdlib>
const std::string PROGRAM_NAME = "LimonEngine";
const std::string RELEASE_FILE = "./Data/Release.xml";
const std::string ENGINE_OPTIONS_FILE = "./Engine/Options.xml";
const std::string USER_OPTIONS_FILE = "./Data/Options.xml";
bool GameEngine::loadAndChangeWorld(const std::string &worldFile) {
graphicsWrapper->clearDepthBuffer();
std::unique_ptr<std::string> loadingImagePath = worldLoader->getLoadingImage(worldFile);
if(loadingImagePath != nullptr) {
this->loadingImage = new GUIImage(0, options, assetManager, "loadingImage", *loadingImagePath);
renderLoadingImage();
}
LimonAPI* apiInstance = getNewLimonAPI();
if(currentWorld != nullptr) {
currentWorld->setupForPauseOrStop();//We have to make sure occlusion threads stopped before interpreter change
}
if(loadedWorlds.find(worldFile) != loadedWorlds.end()) {
unloadWorldForReload(worldFile);//must go before createWorldInterpreter, which would destroy the old interpreter under the same name
}
#ifdef PYTHON_DEBUGGING
std::cout << "[ScriptManager] calling create world Interpreter for " << worldFile << std::endl;
#endif
scriptManager->createWorldInterpreter(worldFile);
scriptManager->setActiveSubInterpreter(worldFile);
World* newWorld = worldLoader->loadWorld(worldFile, apiInstance);
if(newWorld == nullptr) {
//the current world is already paused and its interpreter deactivated, carrying on would hide a broken world
std::cerr << "World load for file " << worldFile << " failed. Exiting." << std::endl;
exit(-1);
}
currentWorld = newWorld;
scriptManager->setActiveSubInterpreter(worldFile);
currentWorld->setupForPlay(*inputHandler);
loadedWorlds[worldFile].first = currentWorld;
loadedWorlds[worldFile].second = apiInstance;
returnWorldStack.push_back(currentWorld);
simulationStartWallTime = SDL2Helper::getTicks();
ticksRun = 0;
return true;
}
void GameEngine::unloadWorldForReload(const std::string &worldFile) {
World* oldWorld = loadedWorlds[worldFile].first;
LimonAPI* oldAPI = loadedWorlds[worldFile].second;
scriptManager->setActiveSubInterpreter(worldFile);//its Python objects must be released inside their own interpreter
delete oldAPI;
delete oldWorld;
scriptManager->removeWorldInterpreter(worldFile);
loadedWorlds.erase(worldFile);
returnWorldStack.erase(std::remove(returnWorldStack.begin(), returnWorldStack.end(), oldWorld), returnWorldStack.end());
if(currentWorld == oldWorld) {
currentWorld = nullptr;
}
}
void GameEngine::renderLoadingImage() const {
if(loadingImage != nullptr) {
loadingImage->setFullScreen(true);
//FIXME: this definition here seems wrong. I can't think of another way, we should explore
std::shared_ptr<GraphicsProgram> imageRenderProgram = std::make_shared<GraphicsProgram>(assetManager.get(),"./Engine/Shaders/GUIImage/vertex.glsl",
"./Engine/Shaders/GUIImage/fragment.glsl");
Material::configureProgram(imageRenderProgram);
sdlHelper->swap();
loadingImage->renderWithProgram(imageRenderProgram, 0);
sdlHelper->swap();
}
}
bool GameEngine::returnOrLoadMap(const std::string &worldFile) {
if(loadedWorlds.find(worldFile) != loadedWorlds.end()) {
if(currentWorld != loadedWorlds[worldFile].first) {
if (currentWorld != nullptr) {
currentWorld->setupForPauseOrStop();
}
currentWorld = loadedWorlds[worldFile].first;
currentWorld->setupForUnpause();
// Activate the corresponding interpreter when switching to this world
scriptManager->setActiveSubInterpreter(worldFile);
}
} else { //world is not in the map case
renderLoadingImage();
LimonAPI* apiInstance = getNewLimonAPI();
if (currentWorld != nullptr) {
currentWorld->setupForPauseOrStop();//We have to make sure occlusion threads stopped before interpreter change
}
scriptManager->createWorldInterpreter(worldFile);
scriptManager->setActiveSubInterpreter(worldFile);
World* newWorld = worldLoader->loadWorld(worldFile, apiInstance);
if(newWorld == nullptr) {
//the current world is already paused and its interpreter deactivated, carrying on would hide a broken world
std::cerr << "World load for file " << worldFile << " failed. Exiting." << std::endl;
exit(-1);
}
currentWorld = newWorld;
loadedWorlds[worldFile].first = currentWorld;
loadedWorlds[worldFile].second = apiInstance;
}
currentWorld->setupForPlay(*inputHandler);
returnWorldStack.push_back(currentWorld);
simulationStartWallTime = SDL2Helper::getTicks();
ticksRun = 0;
return true;
}
bool GameEngine::LoadNewAndRemoveCurrent(const std::string &worldFile) {
if(currentWorld->getName() == worldFile) {
return false;
}
renderLoadingImage();
World* temp = currentWorld;
LimonAPI* tempAPI = loadedWorlds[temp->getName()].second;
std::string oldWorldName = temp->getName();
loadedWorlds.erase(oldWorldName);
returnOrLoadMap(worldFile);//exits on failure
returnWorldStack.clear();
returnWorldStack.push_back(currentWorld);
// Defer deletion: if this was called from inside World::play() (e.g. via a
// trigger), deleting temp here would free the object whose play() is still
// on the stack. Queue it for deletion after play() returns instead.
pendingWorldDeletes.push_back({oldWorldName, temp, tempAPI});
simulationStartWallTime = SDL2Helper::getTicks();
ticksRun = 0;
return true;
}
void GameEngine::returnPreviousMap() {
if(returnWorldStack.size() >1) {
returnWorldStack.pop_back();
if (currentWorld != nullptr) {
currentWorld->setupForPauseOrStop();
}
currentWorld = returnWorldStack[returnWorldStack.size()-1];
scriptManager->setActiveSubInterpreter(currentWorld->getName());
currentWorld->setupForUnpause();
currentWorld->setupForPlay(*inputHandler);
}
simulationStartWallTime = SDL2Helper::getTicks();
ticksRun = 0;
}
GameEngine::GameEngine() {
scriptManager = new ScriptManager("./Engine/Scripts", "./Data/Scripts");
options = new OptionsUtil::Options([](){return static_cast<uint32_t>(SDL2Helper::getTicks());});
if (!options->loadOptionsNew(ENGINE_OPTIONS_FILE)) {
std::cerr << "Failed to load engine options from " << ENGINE_OPTIONS_FILE << std::endl;
exit(-1);
}
if (!options->loadOptionsNew(USER_OPTIONS_FILE)) {
std::cout << "No user options found, using engine defaults" << std::endl;
}
std::cout << "Options loaded successfully" << std::endl;
profilerSystem = new ProfilerSystem(options);
frameTimeTracker = new FrameTimeTracker(options);
sdlHelper = new SDL2Helper(options);
std::string backendName = options->getOption<std::string>(options->getHash("render_backend")).getOrDefault("libOpenGLGraphicsBackend");
std::cout << "Selected Graphics Backend: " << backendName << std::endl;
std::string graphicsBackendFileName = "./";
#ifdef _WIN32
graphicsBackendFileName += backendName + ".dll";
#elif __APPLE__
graphicsBackendFileName += backendName + ".dylib";
#else
graphicsBackendFileName += backendName + ".so";
#endif
graphicsWrapper = sdlHelper->loadGraphicsBackend(graphicsBackendFileName, options);
if(graphicsWrapper == nullptr) {
std::cerr << "failed to load graphics backend. Please check " << graphicsBackendFileName << std::endl;
exit(1);
}
sdlHelper->initWindow(PROGRAM_NAME.c_str(), graphicsWrapper->getContextInformation());
if(!sdlHelper->createContext()) {
std::cout << "Context creation failed, trying fallback..." << std::endl;
sdlHelper->destroyWindow();
GraphicsInterface::ContextInformation fallbackContext;
if(graphicsWrapper->getFallbackContextInformation(fallbackContext)) {
sdlHelper->initWindow(PROGRAM_NAME.c_str(), fallbackContext);
if(!sdlHelper->createContext()) {
std::cerr << "Failed to create fallback context. Exiting." << std::endl;
exit(1);
}
} else {
std::cerr << "Failed to create context and no fallback is available. Exiting." << std::endl;
exit(1);
}
}
if(!graphicsWrapper->verifyContext()) {
std::cerr << "Context verification failed." << std::endl;
exit(1);
}
if(!graphicsWrapper->createGraphicsBackend()) {
std::cerr << "failed to create graphics backend. Please check " << graphicsBackendFileName << std::endl;
exit(1);
}
graphicsWrapper->initGpuContext();
graphicsWrapper->reshape();
modelTransformRing = new TransformTextureRing(graphicsWrapper.get(), 4 * NR_MAX_MODELS, 2, GraphicsInterface::MODEL_BONE_TRANSFORM_TEXTURE_UNIT_START);
boneTransformRing = new TransformTextureRing(graphicsWrapper.get(), 4 * NR_BONE, NR_MAX_MODELS, GraphicsInterface::MODEL_BONE_TRANSFORM_TEXTURE_UNIT_START + 1);
#ifdef _WIN32
sdlHelper->loadCustomTriggers("libcustomTriggers.dll");
#elif __APPLE__
sdlHelper->loadCustomTriggers("./libcustomTriggers.dylib");
#else
sdlHelper->loadCustomTriggers("./libcustomTriggers.so");
#endif
alHelper = new ALHelper();
inputHandler = new InputHandler(sdlHelper->getWindow(), options);
assetManager = std::make_shared<AssetManager>(graphicsWrapper.get(), alHelper);
worldLoader = new WorldLoader(assetManager, inputHandler, options, profilerSystem, frameTimeTracker, modelTransformRing, boneTransformRing);
}
bool GameEngine::convertModelToWorld(const std::string &sourceFile, const std::string &outputDirectory, float scale) {
ModelToWorldConverter converter(assetManager, sourceFile, outputDirectory, scale);
return converter.convert();
}
void GameEngine::applyPendingSwitch() {
PendingSwitch ps = pendingSwitch;
pendingSwitch = {};
switch (ps.type) {
case PendingSwitchType::LOAD_AND_CHANGE: loadAndChangeWorld(ps.worldFile); break;
case PendingSwitchType::RETURN_OR_LOAD: returnOrLoadMap(ps.worldFile); break;
case PendingSwitchType::LOAD_NEW_REMOVE_CURRENT: LoadNewAndRemoveCurrent(ps.worldFile); break;
case PendingSwitchType::RETURN_PREVIOUS: returnPreviousMap(); break;
default: break;
}
}
LimonAPI *GameEngine::getNewLimonAPI() {
std::function<bool(const std::string &)> limonLoadWorld = [this](const std::string& worldFile) {
pendingSwitch = {PendingSwitchType::LOAD_AND_CHANGE, worldFile};
return true;
};
std::function<bool(const std::string &)> limonReturnOrLoadWorld = [this](const std::string& worldFile) {
pendingSwitch = {PendingSwitchType::RETURN_OR_LOAD, worldFile};
return true;
};
std::function<bool(const std::string &)> limonLoadNewAndRemoveCurrentWorld = [this](const std::string& worldFile) {
pendingSwitch = {PendingSwitchType::LOAD_NEW_REMOVE_CURRENT, worldFile};
return true;
};
std::function<void()> limonExitGame = [this] { setWorldQuit(); };
std::function<void()> limonReturnPrevious = [this] {
pendingSwitch = {PendingSwitchType::RETURN_PREVIOUS, ""};
};
std::function<const OptionsUtil::Options*()> limonGetOptions = [this] { return options; };
std::function<bool()> limonSaveOptions = [this] { return options->saveOptions(USER_OPTIONS_FILE); };
return new LimonAPI(limonLoadWorld, limonReturnOrLoadWorld, limonLoadNewAndRemoveCurrentWorld, limonExitGame,
limonReturnPrevious, limonGetOptions, limonSaveOptions);
}
void GameEngine::run() {
graphicsWrapper->clearFrame();
simulationStartWallTime = SDL2Helper::getTicks();
ticksRun = 0;
while (!worldQuit) {
PROFILE_OVERALL("Frame");
// We send ticks run, instead of time. Then the world calculates the timestamp based on it, per
// simulation step. Passing time itself prevents lockstep
// Calculating it in 64 bits because otherwise it overflows within a day
uint64_t ticksDue = (SDL2Helper::getTicks() - simulationStartWallTime) * TICK_PER_SECOND / 1000;
uint64_t ticksToRun = ticksDue - ticksRun;
if (ticksToRun > (uint64_t)MAX_CATCHUP_TICKS) {
//hopelessly behind, drop the surplus rather than spiral trying to catch up
ticksRun = ticksDue - MAX_CATCHUP_TICKS;
ticksToRun = MAX_CATCHUP_TICKS;
}
bool simulationRun = false;
while (ticksToRun > 0 && !worldQuit) {
--ticksToRun;
//we don't need to check for input, if we won't update world state
{
PROFILE_OVERALL("Input");
inputHandler->mapInput();
}
currentWorld->play(*inputHandler, (uint32_t)SDL2Helper::getTicks());
++ticksRun;
simulationRun = true;
bool worldSwitched = false;
if (pendingSwitch.type != PendingSwitchType::NONE && !worldQuit) {
PROFILE_OVERALL("WorldSwitch");
applyPendingSwitch();
worldSwitched = true;
}
// Process worlds that were queued for deletion during play().
// Must happen after play() returns so we never delete a World whose
// play() method is still on the call stack.
if (!pendingWorldDeletes.empty()) {
for (auto& pd : pendingWorldDeletes) {
scriptManager->setActiveSubInterpreter(pd.name);
delete pd.api;
delete pd.world;
scriptManager->removeWorldInterpreter(pd.name);
}
pendingWorldDeletes.clear();
// Restore the current world's interpreter after cleanup
if (currentWorld != nullptr) {
scriptManager->setActiveSubInterpreter(currentWorld->getName());
}
}
if (worldSwitched) {
// If we switched worlds, we want ticksDue and ticksToRun to reset, so we quit inner while
// so the world that renders is not the one we simulated
simulationRun = false;
break;
}
}
if (simulationRun) {
// if we run a simulation, it means we need to update what we render, here it is.
currentWorld->prepareFrame();
}
graphicsWrapper->clearFrame();
frameTimeTracker->tick();//same boundary as the stats snapshot in clearFrame, so both describe the same frame
{
PROFILE_RENDERING("Render");
currentWorld->render();
}
{
PROFILE_RENDERING("swap");
sdlHelper->swap();
}
{
PROFILE_OVERALL("ProfilerUpdate");
graphicsWrapper->collectGpuProfilingData();
if (profilerSystem) profilerSystem->Update();
}
PROFILE_FRAME();
}
}
GameEngine::~GameEngine() {
for (auto iterator = loadedWorlds.begin(); iterator != loadedWorlds.end(); ++iterator) {
scriptManager->setActiveSubInterpreter(iterator->first);
// Run GC before deleting the world so that cyclic Python references (e.g. actor
// objects holding a ref to themselves via a callback) are broken while the
// sub-interpreter's pybind11 internals and C++ objects are still alive.
// Without this, those cyclic objects survive until Py_Finalize() in the main
// interpreter, where the pybind11 internals are gone and all_type_info() returns
// an empty vector, triggering assert(!types->empty()) in clear_instance().
scriptManager->runGC();
delete iterator->second.first;//delete world
delete iterator->second.second;//delete API
scriptManager->removeWorldInterpreter(iterator->first);
}
delete scriptManager;
this->assetManager = nullptr;
delete worldLoader;
delete inputHandler;
delete alHelper;
delete modelTransformRing;//textures, so before the backend goes
delete boneTransformRing;
graphicsWrapper = nullptr;//FIXME this should be part of SdlHelper, because it is created and deleted by it. now it is order dependent because if it.
delete sdlHelper;
delete frameTimeTracker;
delete profilerSystem;
delete options;
}
bool getWorldNameFromReleaseXML(std::string &worldName) {
tinyxml2::XMLDocument xmlDoc;
tinyxml2::XMLError eResult = xmlDoc.LoadFile(RELEASE_FILE.c_str());
if (eResult != tinyxml2::XML_SUCCESS) {
std::cerr << "Error loading release settings from "<< RELEASE_FILE << ": " << xmlDoc.ErrorName() << std::endl;
return false;
}
tinyxml2::XMLNode* releaseNode = xmlDoc.FirstChild();
if (releaseNode == nullptr) {
std::cerr << "Release settings is not valid." << std::endl;
return false;
}
tinyxml2::XMLElement* startingWorldNode = releaseNode->FirstChildElement("StartingWorld");
if (startingWorldNode == nullptr || startingWorldNode->GetText() == nullptr) {
std::cerr << "Starting world is not set." << std::endl;
return false;
}
std::cout << "read starting world as " << startingWorldNode->GetText() << std::endl;
std::string worldNameTemp = startingWorldNode->GetText();
worldName = worldNameTemp;
return true;
}
int main(int argc, char *argv[]) {
if(argc >= 2 && std::string(argv[1]) == "--convert") {
const std::string usage = "usage: " + PROGRAM_NAME + " --convert <model file> [output directory] [--scale <factor>]";
if(argc < 3) {
std::cerr << usage << std::endl;
return -1;
}
std::string outputDirectory;
float scale = 1.0f;
for(int argumentIndex = 3; argumentIndex < argc; ++argumentIndex) {
std::string argument = argv[argumentIndex];
if(argument == "--scale") {
if(argumentIndex + 1 >= argc) {
std::cerr << "--scale needs a value. " << usage << std::endl;
return -1;
}
char *parseEnd = nullptr;
scale = std::strtof(argv[argumentIndex + 1], &parseEnd);
if(parseEnd == argv[argumentIndex + 1] || *parseEnd != '\0') {
std::cerr << "--scale value " << argv[argumentIndex + 1] << " is not a number. " << usage << std::endl;
return -1;
}
++argumentIndex;
} else if(outputDirectory.empty()) {
outputDirectory = argument;
} else {
std::cerr << "unexpected argument " << argument << ". " << usage << std::endl;
return -1;
}
}
//checked before the engine starts, so a typo doesn't cost a window and a backend
if(!(scale > 0.0f)) {
std::cerr << "--scale must be greater than 0, a negative one would mirror the whole map." << std::endl;
return -1;
}
GameEngine converterEngine;
return converterEngine.convertModelToWorld(argv[2], outputDirectory, scale) ? 0 : -1;
}
std::string worldName;
if(argc == 1) {
std::cout << "No world file specified, world select from release settings" << std::endl;
if(!getWorldNameFromReleaseXML(worldName)) {
std::cout << "release settings read failed, defaulting to ./Data/Maps/World001.xml" << std::endl;
worldName = "./Data/Maps/World001.xml";
}
} else if(argc == 2) {
worldName = argv[1];
std::cout << "Trying to load " << worldName << std::endl;
} else {
worldName = argv[1];
std::cout << "Trying to load " << worldName << std::endl;
std::cout << PROGRAM_NAME + " only takes one parameter. First one is processed as Map file, rest discarded." << std::endl;
}
#ifdef __APPLE__
pthread_setname_np("Main thread");//MacOS don't have naming other threads, so the signature is different
#else
pthread_setname_np(pthread_self(), "Main thread");
#endif
GameEngine game;
if(!game.loadAndChangeWorld(worldName)) {
std::cerr << "WorldLoader didn't hand out a valid world. exiting.." << std::endl;
exit(-1);
}
game.run();
return 0;
}