pioneer/src/Pi.cpp

1308 lines
35 KiB
C++

// Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
#include "Input.h"
#include "buildopts.h"
#include "Pi.h"
#include "BaseSphere.h"
#include "CityOnPlanet.h"
#include "DeathView.h"
#include "EnumStrings.h"
#include "FaceParts.h"
#include "FileSystem.h"
#include "Frame.h"
#include "Game.h"
#include "GameConfig.h"
#include "GameLog.h"
#include "GameSaveError.h"
#include "Intro.h"
#include "Lang.h"
#include "Missile.h"
#include "ModManager.h"
#include "ModelCache.h"
#include "NavLights.h"
#include "core/GuiApplication.h"
#include "core/Log.h"
#include "core/OS.h"
#include "graphics/opengl/RendererGL.h"
#include "imgui/imgui.h"
#include "lua/Lua.h"
#include "lua/LuaConsole.h"
#include "lua/LuaEvent.h"
#include "lua/LuaTimer.h"
#include "profiler/Profiler.h"
#include "sound/AmbientSounds.h"
#if WITH_OBJECTVIEWER
#include "ObjectViewerView.h"
#endif
#include "Beam.h"
#include "Player.h"
#include "PngWriter.h"
#include "Projectile.h"
#include "SectorView.h"
#include "Sfx.h"
#include "Shields.h"
#include "ShipType.h"
#include "Space.h"
#include "SpaceStation.h"
#include "Star.h"
#include "StringF.h"
#include "SystemInfoView.h"
#include "Tombstone.h"
#include "TransferPlanner.h"
#include "WorldView.h"
#include "galaxy/GalaxyGenerator.h"
#include "libs.h"
#include "pigui/LuaPiGui.h"
#include "pigui/PerfInfo.h"
#include "pigui/PiGui.h"
#include "ship/PlayerShipController.h"
#include "ship/ShipViewController.h"
#include "sound/Sound.h"
#include "sound/SoundMusic.h"
#include "graphics/Renderer.h"
#if WITH_DEVKEYS
#include "graphics/Graphics.h"
#include "graphics/Light.h"
#include "graphics/Stats.h"
#endif // WITH_DEVKEYS
#include "scenegraph/Lua.h"
#include "versioningInfo.h"
#ifdef PROFILE_LUA_TIME
#include <time.h>
#endif
#if defined(_MSC_VER) || defined(__MINGW32__)
// RegisterClassA and RegisterClassW are defined as macros in WinUser.h
#ifdef RegisterClass
#undef RegisterClass
#endif
#endif
#if !defined(_MSC_VER) && !defined(__MINGW32__)
#define _popen popen
#define _pclose pclose
#endif
/*
===============================================================================
DEFINITIONS
===============================================================================
*/
float Pi::gameTickAlpha;
LuaSerializer *Pi::luaSerializer;
LuaTimer *Pi::luaTimer;
LuaNameGen *Pi::luaNameGen;
#ifdef ENABLE_SERVER_AGENT
ServerAgent *Pi::serverAgent;
#endif
Input::Manager *Pi::input;
Player *Pi::player;
View *Pi::currentView;
TransferPlanner *Pi::planner;
std::unique_ptr<LuaConsole> Pi::luaConsole;
Game *Pi::game;
Random Pi::rng;
float Pi::frameTime;
bool Pi::doingMouseGrab;
bool Pi::showDebugInfo = false;
#if PIONEER_PROFILER
std::string Pi::profilerPath;
std::string Pi::profileOnePath;
bool Pi::doProfileSlow = false;
bool Pi::doProfileOne = false;
#endif
int Pi::statSceneTris = 0;
int Pi::statNumPatches = 0;
GameConfig *Pi::config;
DetailLevel Pi::detail;
bool Pi::navTunnelDisplayed = false;
bool Pi::speedLinesDisplayed = false;
bool Pi::hudTrailsDisplayed = false;
bool Pi::bRefreshBackgroundStars = true;
float Pi::amountOfBackgroundStarsDisplayed = 1.0f;
bool Pi::DrawGUI = true;
Graphics::Renderer *Pi::renderer;
PiGui::Instance *Pi::pigui = nullptr;
ModelCache *Pi::modelCache;
Intro *Pi::intro;
SDLGraphics *Pi::sdl;
Graphics::RenderTarget *Pi::renderTarget;
RefCountedPtr<Graphics::Texture> Pi::renderTexture;
std::unique_ptr<Graphics::Drawables::TexturedQuad> Pi::renderQuad;
Graphics::RenderState *Pi::quadRenderState = nullptr;
bool Pi::isRecordingVideo = false;
FILE *Pi::ffmpegFile = nullptr;
Pi::App *Pi::m_instance = nullptr;
Sound::MusicPlayer Pi::musicPlayer;
std::unique_ptr<AsyncJobQueue> Pi::asyncJobQueue;
std::unique_ptr<SyncJobQueue> Pi::syncJobQueue;
class LoadStep : public Application::Lifecycle {
public:
LoadStep() :
Lifecycle(true)
{}
protected:
struct LoaderStep {
// TODO: use a lighter-weight wrapper over lambdas instead of std::function
std::function<void()> fn;
std::string name;
};
std::vector<LoaderStep> m_loaders;
size_t m_currentLoader = 0;
template <typename T>
void AddStep(std::string name, T fn)
{
m_loaders.push_back(LoaderStep{ fn, name });
}
Profiler::Clock m_loadTimer;
void Start() override;
void Update(float) override;
};
// FIXME: this is a hack, this class should have its lifecycle managed elsewhere
// Ideally an application framework class handles this (as well as the rest of the main loop)
// but for now this is the best we have.
std::unique_ptr<PiGui::PerfInfo> perfInfoDisplay;
class MainMenu : public Application::Lifecycle {
public:
void SetStartPath(const SystemPath &path)
{
m_startPath = path;
m_skipMenu = true;
}
protected:
std::unique_ptr<Intro> m_intro;
void Start() override;
void Update(float) override;
void End() override;
bool m_skipMenu;
SystemPath m_startPath;
};
class GameLoop : public Application::Lifecycle {
protected:
void Start() override;
void Update(float) override;
void End() override;
void InitGame();
void EndGame();
double time_player_died;
// Used to measure frame and physics performance timing info
// with no portable way to map cycles -> ns, Profiler::Timer is useless for taking measurements
// Use Profiler::Clock which is backed by std::chrono::steady_clock (== high_resolution_clock for 99% of uses)
Profiler::Clock perfTimer;
float frame_time_real; // higher resolution than SDL's 1ms, for detailed frame info
float phys_time;
int frame_stat;
int phys_stat;
int MAX_PHYSICS_TICKS;
double accumulator;
Uint32 last_stats = SDL_GetTicks();
};
class TombstoneLoop : public Application::Lifecycle {
protected:
void Start() override;
void Update(float) override;
double startTime = 0.0;
double accumTime = 0.0;
std::unique_ptr<Tombstone> tombstone;
};
/*
===============================================================================
INITIALIZATION
===============================================================================
*/
// TODO: refine this interface
// We don't use options for anything but the config object,
// and instead of passing no_gui, we should instead use a different application
// object devoted to whatever headless work we intend to do
void Pi::Init(const std::map<std::string, std::string> &options, bool no_gui)
{
Pi::config = new GameConfig(options);
m_instance = new Pi::App();
GetApp()->m_loader = std::make_shared<LoadStep>();
GetApp()->m_mainMenu = std::make_shared<MainMenu>();
GetApp()->m_gameLoop = std::make_shared<GameLoop>();
m_instance->m_noGui = no_gui;
}
void Pi::App::SetStartPath(const SystemPath &startPath)
{
static_cast<MainMenu *>(m_mainMenu.get())->SetStartPath(startPath);
}
void Pi::RequestProfileFrame(const std::string &profilePath)
{
// don't do anything if we're building without profiler.
#ifdef PIONEER_PROFILER
if (!profilePath.empty()) {
profileOnePath = FileSystem::JoinPathBelow(Pi::profilerPath, profilePath);
FileSystem::userFiles.MakeDirectory(FileSystem::JoinPathBelow("profiler/", profilePath));
}
doProfileOne = true;
#endif
}
void TestGPUJobsSupport()
{
bool supportsGPUJobs = (Pi::config->Int("EnableGPUJobs") == 1);
if (supportsGPUJobs) {
Uint32 octaves = 8;
for (Uint32 i = 0; i < 6; i++) {
std::unique_ptr<Graphics::Material> material;
Graphics::MaterialDescriptor desc;
desc.effect = Graphics::EFFECT_GEN_GASGIANT_TEXTURE;
desc.quality = (octaves << 16) | i;
desc.textures = 3;
material.reset(Pi::renderer->CreateMaterial(desc));
supportsGPUJobs &= material->IsProgramLoaded();
}
if (!supportsGPUJobs) {
// failed - retry
// reset the GPU jobs flag
supportsGPUJobs = true;
// retry the shader compilation
octaves = 5; // reduce the number of octaves
for (Uint32 i = 0; i < 6; i++) {
std::unique_ptr<Graphics::Material> material;
Graphics::MaterialDescriptor desc;
desc.effect = Graphics::EFFECT_GEN_GASGIANT_TEXTURE;
desc.quality = (octaves << 16) | i;
desc.textures = 3;
material.reset(Pi::renderer->CreateMaterial(desc));
supportsGPUJobs &= material->IsProgramLoaded();
}
if (!supportsGPUJobs) {
// failed
Warning("EnableGPUJobs DISABLED");
Pi::config->SetInt("EnableGPUJobs", 0); // disable GPU Jobs
Pi::config->Save();
}
}
}
}
void Pi::App::Startup()
{
PROFILE_SCOPED()
Profiler::Clock startupTimer;
startupTimer.Start();
Application::Startup();
#if PIONEER_PROFILER
Pi::profilerPath = FileSystem::JoinPathBelow(FileSystem::userFiles.GetRoot(), "profiler");
for (std::string target : { "", "SaveGame/", "NewGame/" }) {
FileSystem::userFiles.MakeDirectory("profiler/" + target);
}
#endif
Log::GetLog()->SetLogFile("output.txt");
std::string version(PIONEER_VERSION);
if (strlen(PIONEER_EXTRAVERSION)) version += " (" PIONEER_EXTRAVERSION ")";
const char *platformName = SDL_GetPlatform();
if (platformName)
Output("ver %s on: %s\n\n", version.c_str(), platformName);
else
Output("ver %s but could not detect platform name.\n\n", version.c_str());
Output("%s\n", OS::GetOSInfoString().c_str());
ModManager::Init();
Lang::Resource res(Lang::GetResource("core", config->String("Lang")));
Lang::MakeCore(res);
// FIXME: move these out of the Pi namespace
// TODO: add a better configuration interface for this kind of thing
Pi::SetAmountBackgroundStars(config->Float("AmountOfBackgroundStars"));
Pi::detail.planets = config->Int("DetailPlanets");
Pi::detail.cities = config->Int("DetailCities");
Graphics::RendererOGL::RegisterRenderer();
Pi::renderer = StartupRenderer(Pi::config);
Pi::rng.IncRefCount(); // so nothing tries to free it
Pi::rng.seed(time(0));
Pi::input = StartupInput(config);
Pi::input->onKeyPress.connect(sigc::ptr_fun(&Pi::HandleKeyDown));
// Register all C++-side input bindings.
// TODO: handle registering Lua input bindings in the startup phase.
for (auto &registrar : Input::GetBindingRegistration()) {
registrar(Pi::input);
}
Pi::pigui = StartupPiGui();
// FIXME: move these into the appropriate class!
navTunnelDisplayed = (config->Int("DisplayNavTunnel")) ? true : false;
speedLinesDisplayed = (config->Int("SpeedLines")) ? true : false;
hudTrailsDisplayed = (config->Int("HudTrails")) ? true : false;
TestGPUJobsSupport();
EnumStrings::Init();
// Can be initialized directly after FileSystem::Init, but put it here for convenience
GalacticEconomy::Init();
Profiler::Clock threadTimer;
threadTimer.Start();
// get threads up
Uint32 numThreads = config->Int("WorkerThreads");
numThreads = numThreads ? numThreads : std::max(OS::GetNumCores() - 1, 1U);
Pi::asyncJobQueue.reset(new AsyncJobQueue(numThreads));
Pi::syncJobQueue.reset(new SyncJobQueue);
threadTimer.Stop();
Output("started %d worker threads in %.2fms\n", numThreads, threadTimer.milliseconds());
QueueLifecycle(m_loader);
// Don't start the main menu if we don't have a GUI
if (!m_noGui)
QueueLifecycle(m_mainMenu);
startupTimer.Stop();
Output("\n\nEngine startup took %.2fms\n", startupTimer.milliseconds());
}
/*
===============================================================================
DEINITIALIZATION
===============================================================================
*/
// Immediately destroy everything and end the game.
void Pi::App::Shutdown()
{
PROFILE_SCOPED()
Output("Pi shutting down.\n");
// This function should only be called at the very end of the shutdown procedure.
assert(Pi::game == nullptr);
if (Pi::ffmpegFile != nullptr) {
_pclose(Pi::ffmpegFile);
}
perfInfoDisplay.reset();
// TODO: connect initializers and deinitializers in a single Module interface
// Will need to think about dependency injection for e.g. modules which need a
// reference to the renderer
Projectile::FreeModel();
Beam::FreeModel();
delete Pi::intro;
Pi::luaConsole.reset();
NavLights::Uninit();
Shields::Uninit();
SfxManager::Uninit();
Sound::Uninit();
CityOnPlanet::Uninit();
BaseSphere::Uninit();
FaceParts::Uninit();
Graphics::Uninit();
PiGui::Lua::Uninit();
ShutdownPiGui();
Pi::pigui = nullptr;
Lua::UninitModules();
Lua::Uninit();
Gui::Uninit();
delete Pi::modelCache;
GalaxyGenerator::Uninit();
ShutdownRenderer();
Pi::renderer = nullptr;
ShutdownInput();
Pi::input = nullptr;
delete Pi::config;
delete Pi::planner;
asyncJobQueue.reset();
syncJobQueue.reset();
Application::Shutdown();
SDL_Quit();
delete Pi::m_instance;
exit(0);
}
/*
===============================================================================
LOADING
===============================================================================
*/
// TODO: investigate constructing a DAG out of Init functions and dependencies
void LoadStep::Start()
{
PROFILE_SCOPED()
Output("LoadStep::Start()\n");
m_loadTimer.Reset();
m_loadTimer.Start();
Output("ShipType::Init()\n");
// XXX early, Lua init needs it
ShipType::Init();
// XXX UI requires Lua but Pi::ui must exist before we start loading
// templates. so now we have crap everywhere :/
Output("Lua::Init()\n");
Lua::Init();
// TODO: Get the lua state responsible for drawing the init progress up as fast as possible
// Investigate using a pigui-only Lua state that we can initialize without depending on
// normal init flow, or drawing the init screen in C++ instead?
// Loads just the PiGui class and PiGui-related modules
PiGui::Lua::Init();
// FIXME: this just exists to load the theme out-of-order from Lua::InitModules. Needs a better solution
PiGui::LoadThemeFromDisk("default");
PiGui::LoadTheme(ImGui::GetStyle(), "default");
// Don't render the first frame, just make sure all of our fonts are loaded
Pi::pigui->NewFrame();
PiGui::RunHandler(0.01, "init");
Pi::pigui->EndFrame();
#ifdef ENABLE_SERVER_AGENT
AddStep("Initialize ServerAgent", []() {
Pi::serverAgent = 0;
if (Pi::config->Int("EnableServerAgent")) {
const std::string endpoint(Pi::config->String("ServerEndpoint"));
if (endpoint.size() > 0) {
Output("Server agent enabled, endpoint: %s\n", endpoint.c_str());
Pi::serverAgent = new HTTPServerAgent(endpoint);
}
}
if (!Pi::serverAgent) {
Output("Server agent disabled\n");
Pi::serverAgent = new NullServerAgent();
}
});
#endif
// TODO: expose the AddStep interface so Lua::InitModules can granularize its registration
AddStep("Lua::InitModules()", &Lua::InitModules);
AddStep("Gui::Init()", []() {
Gui::Init(Pi::renderer, Graphics::GetScreenWidth(), Graphics::GetScreenHeight(), 800, 600);
});
AddStep("GalaxyGenerator::Init()", []() {
if (Pi::config->HasEntry("GalaxyGenerator"))
GalaxyGenerator::Init(Pi::config->String("GalaxyGenerator"),
Pi::config->Int("GalaxyGeneratorVersion", GalaxyGenerator::LAST_VERSION));
else
GalaxyGenerator::Init();
});
AddStep("FaceParts::Init()", &FaceParts::Init);
AddStep("new ModelCache", []() {
Pi::modelCache = new ModelCache(Pi::renderer);
});
AddStep("Shields::Init", []() {
Shields::Init(Pi::renderer);
});
AddStep("BaseSphere::Init", &BaseSphere::Init);
AddStep("CityOnPlanet::Init", &CityOnPlanet::Init);
AddStep("SpaceStation::Init", &SpaceStation::Init);
AddStep("NavLights::Init", []() {
NavLights::Init(Pi::renderer);
});
AddStep("Sfx::Init", []() {
SfxManager::Init(Pi::renderer);
});
AddStep("Sound::Init", []() {
if (Pi::GetApp()->HeadlessMode() || Pi::config->Int("DisableSound"))
return;
Sound::Init();
Sound::SetMasterVolume(Pi::config->Float("MasterVolume"));
Sound::SetSfxVolume(Pi::config->Float("SfxVolume"));
Pi::GetMusicPlayer().SetVolume(Pi::config->Float("MusicVolume"));
Sound::Pause(0);
if (Pi::config->Int("MasterMuted")) Sound::Pause(1);
if (Pi::config->Int("SfxMuted")) Sound::SetSfxVolume(0.f);
if (Pi::config->Int("MusicMuted")) Pi::GetMusicPlayer().SetEnabled(false);
});
AddStep("PostLoad", []() {
Pi::luaConsole.reset(new LuaConsole());
Pi::luaConsole->SetupBindings();
Pi::planner = new TransferPlanner();
perfInfoDisplay.reset(new PiGui::PerfInfo());
});
}
void LoadStep::Update(float deltaTime)
{
PROFILE_SCOPED()
if (m_currentLoader < m_loaders.size()) {
LoaderStep &loader = m_loaders[m_currentLoader++];
float progress = (m_currentLoader) / float(m_loaders.size());
Output("Loading [%02.f%%]: %s started\n", progress * 100., loader.name.c_str());
Profiler::Clock timer;
timer.Start();
loader.fn();
timer.Stop();
Output("Loading [%02.f%%]: %s took %.2fms\n", progress * 100.,
loader.name.c_str(), timer.milliseconds());
Pi::pigui->NewFrame();
PiGui::RunHandler(progress, "init");
Pi::pigui->Render();
} else {
OS::NotifyLoadEnd();
RequestEndLifecycle();
m_loadTimer.Stop();
Output("\n\nPioneer loading took %.2fms\n", m_loadTimer.milliseconds());
Pi::RequestProfileFrame();
}
}
/*
===============================================================================
MAIN MENU
===============================================================================
*/
void MainMenu::Start()
{
Pi::intro = new Intro(Pi::renderer, Graphics::GetScreenWidth(), Graphics::GetScreenHeight());
if (m_skipMenu) {
Output("Loading new game immediately!\n");
Pi::StartGame(new Game(m_startPath, 0.0));
m_skipMenu = false; // Show the main menu once we're done here.
}
//XXX global ambient colour hack to make explicit the old default ambient colour dependency
// for some models
Pi::renderer->SetAmbientColor(Color(51, 51, 51, 255));
}
void MainMenu::Update(float deltaTime)
{
Pi::GetApp()->HandleEvents();
Pi::intro->Draw(deltaTime);
Pi::pigui->NewFrame();
PiGui::RunHandler(deltaTime, "mainMenu");
perfInfoDisplay->Update(deltaTime * 1e3, 0.0);
if (Pi::showDebugInfo) {
Pi::pigui->SetDebugStyle();
perfInfoDisplay->Draw();
Pi::pigui->SetNormalStyle();
}
Pi::pigui->Render();
if (Pi::game) {
RequestEndLifecycle();
}
#ifdef ENABLE_SERVER_AGENT
Pi::serverAgent->ProcessResponses();
#endif
}
void MainMenu::End()
{
delete Pi::intro;
Pi::intro = nullptr;
}
// Not much better than setting Pi::game in a Lua function,
// but it works for now.
void Pi::StartGame(Game *game)
{
// FIXME: Game.cpp sets Pi::game because some other things depend on that variable
// if (Pi::game != nullptr)
// Error("Attempt to start a new game while one is already running!\n");
Pi::game = game;
Pi::GetApp()->GetActiveLifecycle()->RequestEndLifecycle();
Pi::GetApp()->QueueLifecycle(Pi::GetApp()->m_gameLoop);
}
/*
===============================================================================
EVENT HANDLING
===============================================================================
*/
void Pi::HandleKeyDown(SDL_Keysym *key)
{
const bool CTRL = input->KeyState(SDLK_LCTRL) || input->KeyState(SDLK_RCTRL);
if (!CTRL) {
return;
}
// special keys: CTRL+[KEY].
switch (key->sym) {
case SDLK_q: // Quit
Pi::RequestQuit();
break;
case SDLK_PRINTSCREEN: // print
case SDLK_KP_MULTIPLY: // screen
{
char buf[256];
const time_t t = time(0);
struct tm *_tm = localtime(&t);
strftime(buf, sizeof(buf), "screenshot-%Y%m%d-%H%M%S.png", _tm);
Graphics::ScreendumpState sd;
Pi::renderer->Screendump(sd);
write_screenshot(sd, buf);
break;
}
#if 0 // FIXME: find a better home / interface for video recording
case SDLK_SCROLLLOCK: // toggle video recording
SetVideoRecording(!Pi::isRecordingVideo);
break;
#endif
case SDLK_i: // Toggle Debug info
Pi::showDebugInfo = !Pi::showDebugInfo;
break;
#if WITH_DEVKEYS
#ifdef PIONEER_PROFILER
case SDLK_p: // alert it that we want to profile
if (input->KeyState(SDLK_LSHIFT) || input->KeyState(SDLK_RSHIFT))
Pi::doProfileOne = true;
else {
Pi::doProfileSlow = !Pi::doProfileSlow;
Output("slow frame profiling %s\n", Pi::doProfileSlow ? "enabled" : "disabled");
}
break;
#endif
case SDLK_F11: // Reload shaders
renderer->ReloadShaders();
break;
#endif /* DEVKEYS */
#if WITH_OBJECTVIEWER
case SDLK_F10: {
if (!Pi::game)
break;
if (Pi::GetView() == Pi::game->GetObjectViewerView())
Pi::SetView(Pi::game->GetWorldView());
else if (Pi::player->GetNavTarget())
Pi::SetView(Pi::game->GetObjectViewerView());
break;
}
#endif
case SDLK_F9: // Quicksave
{
if (!Pi::game)
break;
if (Pi::game->IsHyperspace())
Pi::game->log->Add(Lang::CANT_SAVE_IN_HYPERSPACE);
else {
const std::string name = "_quicksave";
const std::string path = FileSystem::JoinPath(GetSaveDir(), name);
try {
Game::SaveGame(name, Pi::game);
Pi::game->log->Add(Lang::GAME_SAVED_TO + path);
} catch (CouldNotOpenFileException) {
Pi::game->log->Add(stringf(Lang::COULD_NOT_OPEN_FILENAME, formatarg("path", path)));
} catch (CouldNotWriteToFileException) {
Pi::game->log->Add(Lang::GAME_SAVE_CANNOT_WRITE);
}
}
break;
}
default:
break; // This does nothing but it stops the compiler warnings
}
}
// Return true if the event has been handled and shouldn't be passed through
// to the normal input system.
bool Pi::App::HandleEvent(SDL_Event &event)
{
PROFILE_SCOPED()
// HACK for most keypresses SDL will generate KEYUP/KEYDOWN and TEXTINPUT
// events. keybindings run off KEYUP/KEYDOWN. the console is opened/closed
// via keybinding. the console TextInput widget uses TEXTINPUT events. thus
// after switching the console, the stray TEXTINPUT event causes the
// console key (backtick) to appear in the text entry field. we hack around
// this by setting this flag if the console was switched. if its set, we
// swallow the TEXTINPUT event this hack must remain until we have a
// unified input system
// This is safely able to be removed once GUI and newUI are gone
Gui::HandleSDLEvent(&event);
return false;
}
void Pi::App::HandleRequests()
{
for (auto request : internalRequests) {
switch (request) {
case InternalRequests::END_GAME: {
if (!Pi::game)
break;
m_gameLoop->RequestEndLifecycle();
} break;
case InternalRequests::QUIT_GAME: {
GetActiveLifecycle()->RequestEndLifecycle();
RequestQuit();
} break;
default:
Output("Pi::HandleRequests, unhandled request type %d processed.\n", int(request));
break;
}
}
internalRequests.clear();
}
/*
===============================================================================
GAME LOOP
===============================================================================
*/
void Pi::App::PreUpdate()
{
PROFILE_SCOPED()
Pi::frameTime = DeltaTime();
GuiApplication::PreUpdate();
}
void Pi::App::PostUpdate()
{
PROFILE_SCOPED()
GuiApplication::PostUpdate();
HandleRequests();
#ifdef PIONEER_PROFILER
// TODO: profileSlow is profiling the previous frame, need to move that functionality to Application
if (Pi::doProfileOne || (Pi::doProfileSlow && (GetFrameTime() > 0.1))) { // slow: < ~10fps
Pi::doProfileOne = false;
if (!Pi::profileOnePath.empty()) {
Profiler::dumphtml(Pi::profileOnePath.c_str());
Pi::profileOnePath.clear();
} else {
Profiler::dumphtml(Pi::profilerPath.c_str());
}
}
#endif
}
void Pi::App::RunJobs()
{
Pi::syncJobQueue->RunJobs(SYNC_JOBS_PER_LOOP);
Pi::asyncJobQueue->FinishJobs();
Pi::syncJobQueue->FinishJobs();
}
// FIXME: delete/move this function out of Pi.cpp
static void OnPlayerDockOrUndock();
void GameLoop::Start()
{
// this is a bit brittle. skank may be forgotten and survive between
// games
Pi::input->InitGame();
if (!Pi::config->Int("DisableSound")) AmbientSounds::Init();
LuaEvent::Clear();
Pi::player->onDock.connect(sigc::ptr_fun(&OnPlayerDockOrUndock));
Pi::player->onUndock.connect(sigc::ptr_fun(&OnPlayerDockOrUndock));
Pi::player->onLanded.connect(sigc::ptr_fun(&OnPlayerDockOrUndock));
Pi::DrawGUI = true;
Pi::SetView(Pi::game->GetWorldView());
#ifdef REMOTE_LUA_REPL
#ifndef REMOTE_LUA_REPL_PORT
#define REMOTE_LUA_REPL_PORT 12345
#endif
luaConsole->OpenTCPDebugConnection(REMOTE_LUA_REPL_PORT);
#endif
// fire event before the first frame
LuaEvent::Queue("onGameStart");
LuaEvent::Emit();
frame_stat = 0;
phys_stat = 0;
accumulator = Pi::game->GetTimeStep();
time_player_died = 0.0;
MAX_PHYSICS_TICKS = Pi::config->Int("MaxPhysicsCyclesPerRender");
if (MAX_PHYSICS_TICKS <= 0)
MAX_PHYSICS_TICKS = 4;
Pi::SetGameTickAlpha(0);
// If we have a tombstone loop, we will SetNextLifecycle() so it runs before
// we jump back to the main menu
Pi::GetApp()->QueueLifecycle(Pi::GetApp()->m_mainMenu);
}
void GameLoop::Update(float deltaTime)
{
PROFILE_SCOPED()
perfTimer.SoftReset(); // Reset() + Start()
frame_time_real = deltaTime * 1e3; // convert to ms
frame_stat++;
#ifdef ENABLE_SERVER_AGENT
Pi::serverAgent->ProcessResponses();
#endif
// TODO: is it necessary to limit frame delta to 1/4th second?
// Presumably if we're rendering < 4 FPS, we don't care about physics error either
// if (Pi::frameTime > 0.25) Pi::frameTime = 0.25;
accumulator += deltaTime * Pi::game->GetTimeAccelRate();
const float step = Pi::game->GetTimeStep();
if (step > 0.0f) {
PROFILE_SCOPED_RAW("Physics Update [unpaused]")
int phys_ticks = 0;
while (accumulator >= step) {
if (++phys_ticks >= MAX_PHYSICS_TICKS) {
accumulator = 0.0;
break;
}
Pi::game->TimeStep(step);
BaseSphere::UpdateAllBaseSphereDerivatives();
accumulator -= step;
}
// rendering interpolation between frames: don't use when docked
// FIXME: this is the player's concern, the player should be responsible for calling Pi::SetInterpolation(false) when docked
int pstate = Pi::game->GetPlayer()->GetFlightState();
if (pstate == Ship::DOCKED || pstate == Ship::DOCKING || pstate == Ship::UNDOCKING)
Pi::SetGameTickAlpha(1.0);
else
Pi::SetGameTickAlpha(accumulator / step);
phys_stat += phys_ticks;
} else {
// paused
PROFILE_SCOPED_RAW("Physics Update [paused]")
BaseSphere::UpdateAllBaseSphereDerivatives();
}
// Record physics timestep but keep information about current frame timing.
perfTimer.SoftStop();
// store the physics time until the end of the frame
phys_time = perfTimer.milliseconds();
// did the player die?
if (Pi::game->GetPlayer()->IsDead()) {
// FIXME: this is NOT Pi's concern at all! At the very minimum, this should go in Game.cpp
if (!(time_player_died > 0.0)) {
Pi::game->SetTimeAccel(Game::TIMEACCEL_1X);
Pi::game->GetDeathView()->Init();
Pi::SetView(Pi::game->GetDeathView());
time_player_died = Pi::game->GetTime();
} else if (Pi::game->GetTime() - time_player_died > 8.0) {
// This also shouldn't go here, though we should evaluate to what degree
// Pi.cpp is involved in queuing the tombstone loop.
SetNextLifecycle(std::make_shared<TombstoneLoop>());
RequestEndLifecycle();
}
}
Pi::renderer->SetTransform(matrix4x4f::Identity());
/* Calculate position for this rendered frame (interpolated between two physics ticks */
// XXX should this be here? what is this anyway?
for (Body *b : Pi::game->GetSpace()->GetBodies()) {
b->UpdateInterpTransform(Pi::GetGameTickAlpha());
}
Frame::GetFrame(Pi::game->GetSpace()->GetRootFrame())->UpdateInterpTransform(Pi::GetGameTickAlpha());
Pi::GetView()->Update();
Pi::GetView()->Draw3D();
// FIXME: HandleEvents at the moment must be after view->Draw3D and before
// Gui::Draw so that labels drawn to screen can have mouse events correctly
// detected. Gui::Draw wipes memory of label positions.
Pi::GetApp()->HandleEvents();
#ifdef REMOTE_LUA_REPL
Pi::luaConsole->HandleTCPDebugConnections();
#endif
// Reset the depth buffer so our UI can get drawn right overtop
Pi::renderer->ClearDepthBuffer();
// FIXME: GUI must die!
if (Pi::DrawGUI) {
Gui::Draw();
}
// Ask ImGui to hide OS cursor if we're capturing it for input:
// it will do this if GetMouseCursor == ImGuiMouseCursor_None.
if (Pi::input->IsCapturingMouse()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_None);
}
// TODO: the escape menu depends on HandleEvents() being called before NewFrame()
// Move HandleEvents to either the end of the loop or the very start of the loop
// The goal is to be able to call imgui functions for debugging inside C++ code
Pi::pigui->NewFrame();
if (Pi::game && !Pi::player->IsDead()) {
// TODO: this mechanism still isn't perfect, but it gets us out of newUI
if (Pi::luaConsole->IsActive())
Pi::luaConsole->Draw();
else {
Pi::GetView()->DrawPiGui();
PiGui::RunHandler(deltaTime, "game");
}
}
// Render this even when we're dead.
if (Pi::showDebugInfo) {
Pi::pigui->SetDebugStyle();
perfInfoDisplay->Draw();
Pi::pigui->SetNormalStyle();
}
Pi::pigui->Render();
if (Pi::game->UpdateTimeAccel())
accumulator = 0; // fix for huge pauses 10000x -> 1x
if (!Pi::player->IsDead()) {
// XXX should this really be limited to while the player is alive?
// this is something we need not do every turn...
if (!Pi::config->Int("DisableSound")) AmbientSounds::Update();
}
Pi::GetMusicPlayer().Update();
Pi::GetApp()->RunJobs();
perfInfoDisplay->Update(frame_time_real, phys_time);
if (Pi::showDebugInfo && SDL_GetTicks() - last_stats >= 1000) {
perfInfoDisplay->UpdateFrameInfo(frame_stat, phys_stat);
frame_stat = 0;
phys_stat = 0;
Text::TextureFont::ClearGlyphCount();
if (SDL_GetTicks() - last_stats > 1200)
last_stats = SDL_GetTicks();
else
last_stats += 1000;
}
Pi::statSceneTris = 0;
Pi::statNumPatches = 0;
#if 0 // FIXME: decouple video recording from Pi
if (Pi::isRecordingVideo && (Pi::ffmpegFile != nullptr)) {
Graphics::ScreendumpState sd;
Pi::renderer->FrameGrab(sd);
fwrite(sd.pixels.get(), sizeof(uint32_t) * Pi::renderer->GetWindowWidth() * Pi::renderer->GetWindowHeight(), 1, Pi::ffmpegFile);
}
#endif
}
void GameLoop::End()
{
// When Pi::game goes, so too goes the death view.
Pi::SetView(0);
// Clean up any left-over mouse state
Pi::input->SetCapturingMouse(false);
Pi::SetMouseGrab(false);
// final event
LuaEvent::Queue("onGameEnd");
LuaEvent::Emit();
Pi::luaTimer->RemoveAll();
Lua::manager->CollectGarbage();
if (!Pi::config->Int("DisableSound")) AmbientSounds::Uninit();
Sound::DestroyAllEventsExceptMusic();
assert(Pi::game);
delete Pi::game;
Pi::game = nullptr;
Pi::player = nullptr;
}
/*
===============================================================================
TOMBSTONE LOOP
===============================================================================
*/
void TombstoneLoop::Start()
{
tombstone.reset(new Tombstone(Pi::renderer, Graphics::GetScreenWidth(), Graphics::GetScreenHeight()));
startTime = Pi::GetApp()->GetTime();
}
void TombstoneLoop::Update(float deltaTime)
{
Pi::GetApp()->HandleEvents();
// TODO: improve Tombstone, add pigui drawing, etc.
tombstone->Draw(accumTime);
accumTime += deltaTime;
bool hasInput = Pi::input->MouseButtonState(SDL_BUTTON_LEFT) || Pi::input->MouseButtonState(SDL_BUTTON_RIGHT) || Pi::input->KeyState(SDLK_SPACE);
if ((accumTime > 2.0 && hasInput) || accumTime > 8.0) {
RequestEndLifecycle();
Pi::GetMusicPlayer().Stop();
Sound::DestroyAllEvents();
}
}
/*
===============================================================================
MISCELLANEOUS GARBAGE THAT OUGHT NOT TO BE IN THIS CLASS
===============================================================================
*/
SceneGraph::Model *Pi::FindModel(const std::string &name, bool allowPlaceholder)
{
SceneGraph::Model *m = 0;
try {
m = Pi::modelCache->FindModel(name);
} catch (const ModelCache::ModelNotFoundException &) {
Output("Could not find model: %s\n", name.c_str());
if (allowPlaceholder) {
try {
m = Pi::modelCache->FindModel("error");
} catch (const ModelCache::ModelNotFoundException &) {
Error("Could not find placeholder model");
}
}
}
return m;
}
const char Pi::SAVE_DIR_NAME[] = "savefiles";
std::string Pi::GetSaveDir()
{
return FileSystem::JoinPath(FileSystem::GetUserDir(), Pi::SAVE_DIR_NAME);
}
// request that the game is ended as soon as safely possible
void Pi::RequestEndGame()
{
GetApp()->internalRequests.push_back(App::InternalRequests::END_GAME);
}
void Pi::RequestQuit()
{
GetApp()->internalRequests.push_back(App::InternalRequests::QUIT_GAME);
}
/*
GARBAGE THAT ESPECIALLY SHOULD NOT BE HERE
*/
void Pi::SetView(View *v)
{
if (currentView) currentView->Detach();
currentView = v;
if (currentView) currentView->Attach();
LuaEvent::Queue("onViewChanged");
}
void Pi::OnChangeDetailLevel()
{
BaseSphere::OnChangeDetailLevel();
}
static void OnPlayerDockOrUndock()
{
Pi::game->RequestTimeAccel(Game::TIMEACCEL_1X);
Pi::game->SetTimeAccel(Game::TIMEACCEL_1X);
}
float Pi::GetMoveSpeedShiftModifier()
{
// Suggestion: make x1000 speed on pressing both keys?
if (Pi::input->KeyState(SDLK_LSHIFT)) return 100.f;
if (Pi::input->KeyState(SDLK_RSHIFT)) return 10.f;
return 1;
}
void Pi::SetMouseGrab(bool on)
{
if (!doingMouseGrab && on) {
Pi::input->SetCapturingMouse(true);
doingMouseGrab = true;
} else if (doingMouseGrab && !on) {
Pi::input->SetCapturingMouse(false);
doingMouseGrab = false;
}
}
// This absolutely ought not to be part of the Pi class
#if 0
static void SetVideoRecording(bool enabled)
{
Pi::isRecordingVideo = enabled;
if (Pi::isRecordingVideo) {
char videoName[256];
const time_t t = time(0);
struct tm *_tm = localtime(&t);
strftime(videoName, sizeof(videoName), "pioneer-%Y%m%d-%H%M%S", _tm);
const std::string dir = "videos";
FileSystem::userFiles.MakeDirectory(dir);
const std::string fname = FileSystem::JoinPathBelow(FileSystem::userFiles.GetRoot() + "/" + dir, videoName);
Output("Video Recording started to %s.\n", fname.c_str());
// start ffmpeg telling it to expect raw rgba 720p-60hz frames
// -i - tells it to read frames from stdin
// if given no frame rate (-r 60), it will just use vfr
char cmd[256] = { 0 };
snprintf(cmd, sizeof(cmd), "ffmpeg -f rawvideo -pix_fmt rgba -s %dx%d -i - -threads 0 -preset fast -y -pix_fmt yuv420p -crf 21 -vf vflip %s.mp4", config->Int("ScrWidth"), config->Int("ScrHeight"), fname.c_str());
// open pipe to ffmpeg's stdin in binary write mode
#if defined(_MSC_VER) || defined(__MINGW32__)
Pi::ffmpegFile = _popen(cmd, "wb");
#else
Pi::ffmpegFile = _popen(cmd, "w");
#endif
} else {
Output("Video Recording ended.\n");
if (Pi::ffmpegFile != nullptr) {
_pclose(Pi::ffmpegFile);
Pi::ffmpegFile = nullptr;
}
}
}
#endif
#if 0
void printShipStats()
{
// test code to produce list of ship stats
FILE *pStatFile = fopen("shipstat.csv", "wt");
if (pStatFile) {
fprintf(pStatFile, "name,modelname,hullmass,capacity,fakevol,rescale,xsize,ysize,zsize,facc,racc,uacc,sacc,aacc,exvel\n");
for (auto iter : ShipType::types) {
const ShipType *shipdef = &(iter.second);
SceneGraph::Model *model = Pi::FindModel(shipdef->modelName, false);
double hullmass = shipdef->hullMass;
double capacity = shipdef->capacity;
double xsize = 0.0, ysize = 0.0, zsize = 0.0, fakevol = 0.0, rescale = 0.0, brad = 0.0;
if (model) {
std::unique_ptr<SceneGraph::Model> inst(model->MakeInstance());
model->CreateCollisionMesh();
Aabb aabb = model->GetCollisionMesh()->GetAabb();
xsize = aabb.max.x - aabb.min.x;
ysize = aabb.max.y - aabb.min.y;
zsize = aabb.max.z - aabb.min.z;
fakevol = xsize * ysize * zsize;
brad = aabb.GetRadius();
rescale = pow(fakevol / (100 * (hullmass + capacity)), 0.3333333333);
}
double simass = (hullmass + capacity) * 1000.0;
double angInertia = (2 / 5.0) * simass * brad * brad;
double acc1 = shipdef->linThrust[Thruster::THRUSTER_FORWARD] / (9.81 * simass);
double acc2 = shipdef->linThrust[Thruster::THRUSTER_REVERSE] / (9.81 * simass);
double acc3 = shipdef->linThrust[Thruster::THRUSTER_UP] / (9.81 * simass);
double acc4 = shipdef->linThrust[Thruster::THRUSTER_RIGHT] / (9.81 * simass);
double acca = shipdef->angThrust / angInertia;
double exvel = shipdef->effectiveExhaustVelocity;
fprintf(pStatFile, "%s,%s,%.1f,%.1f,%.1f,%.3f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%f,%.1f\n",
shipdef->name.c_str(), shipdef->modelName.c_str(), hullmass, capacity,
fakevol, rescale, xsize, ysize, zsize, acc1, acc2, acc3, acc4, acca, exvel);
}
fclose(pStatFile);
}
}
#endif