mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 23:49:19 +02:00
Today's work on the binary scene format plan (binary-scene-format-plan.md), from the first measurement to a terrain that bakes itself on first run and streams from a thread. The step-by-step history is kept on backup/scene-streaming-format-presquash-0916. Measuring first. -loadprofile times a scenery load by stage and by node kind and reports node counts and resident memory. On l204 terrain is 95% of the bytes and 99.8% of the nodes; on quarkmce2007 vehicles, not models, dominate what is left. The terrain turned out to be a heightfield in all but name, a ribbon covering 12% of its bounding box. The heightfield. scene/heightfieldformat.h is the .ehf layout: Morton-ordered tiles of 128 samples with one shared edge sample, a point-sampled mip chain, zstd per tile, a nodata sentinel, a material palette with a measured tiling per material, and a stamp of the rules that decided what counted as terrain. scene/heightfieldreader.h reads it for the tool, the renderer and the loader alike, and scene/terraincooker.h writes it, shared by the terraincook tool and the engine so a first-run bake is the file the tool would have made. The cooker rasterises every triangle where it is drawn (through scene::place_in_world, the placement the region applies too), keeps an upper surface laid within half a metre over another and the lower of two further apart, and then asks each triangle whether the heightfield shows it: laid, reaching a sample, keeping nearly all it claimed, and with the ground around it drawn at every level out to 2 km. Baking on first run. simulation/terrainbake bakes <scenario>.ehf the first time a scenery is opened, choosing the grid step from the first 200,000 vertices, and on later loads leaves out the triangles its node table says the heightfield shows. Everything else - trees, grass tufts, gantries, bridge decks - is drawn as written, so a bad bake can at worst draw something twice, and the heightfield sits 2 cm under the triangles so the original wins there. Nodes are keyed by file, line, include parameters and placement; an edited file changes its keys and those nodes are drawn again. A bake under older rules, or without its table, is thrown away and redone. A launcher copy named $scenario shares the scenario's files. -nobaketerrain shows the triangles as written. A scenery can still name hand-cooked fields, several at different steps. Drawing and streaming. rendering/terrainclipmap draws resident tiles at the mip level their distance earns, deriving the grid from gl_VertexID and texturing in world space with the scenery's own materials. rendering/terraintileloader reads and decodes tiles on a thread of its own; the renderer rescans what is in range only after moving a quarter of a tile, asks for what is missing nearest first, uploads at most 32 tiles a frame, drops arrivals the camera has moved past, and keeps drawing a tile at the level it has until the right one arrives. The terrain has its own range, terrainrange in the ini and a slider in the debug panel, 20 km by default and independent of basedrawrange; the far plane of an ordinary view is pushed out to it. The debug panel shows the range, tiles in view, on the gpu, wanted and queued, and how many sit at each level. Events. The event queue moved out of the events into a binary heap with a deterministic tie-break, and an event is handed its activator instead of storing it, so basic_event is a definition and nothing else. The logic container. scene/logicformat.h and scene/logiccontainer write a scenery's events, their targets and sibling chains as flat, deterministic arrays with a schema hash that refuses a mismatched build. simulation/logiccook cooks it from the loaded state whenever it is missing, stale or unreadable, reads it straight back to check it, and resolves each target to a position in the registry its event kind searches. -cooklogic forces a cook, -verifylogic only checks. Nothing reads it at load yet. Prefabs. scene/prefab describes a signal by the aspects it can show - lights, speeds, animation channels, a sound - instead of pasting an .inc per variant, and expands an instance to ordinary scenery text. Tried on a light signal and a shape signal. Screenshots without a person present: -shot x,y,z[,yaw[,pitch]], -shotsettle, -shotstay.
1632 lines
41 KiB
C++
1632 lines
41 KiB
C++
module;
|
|
#include <deque>
|
|
#include <limits>
|
|
#include <mutex>
|
|
#include <ostream>
|
|
#include <type_traits>
|
|
#include <unordered_map>
|
|
#include <cstring>
|
|
#include <cstdint>
|
|
#include <array>
|
|
#include <cstddef>
|
|
#include <ios>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <stack>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
#include <filesystem>
|
|
#include "glad/glad.h"
|
|
#include <GLFW/glfw3.h>
|
|
#include <random>
|
|
#include <thread>
|
|
#include "utilities/Globals_macros.h"
|
|
#include "utilities/utilities_macros.h"
|
|
#include "utilities/translation_macros.h"
|
|
#include "version_info.h"
|
|
#include <chrono>
|
|
#include "utilities/translation_macros.h"
|
|
#if WITH_DISCORD_RPC
|
|
#include "ref/discord-rpc/include/discord_rpc.h"
|
|
#endif
|
|
#if WITH_DISCORD_RPC
|
|
#include <discord_rpc.h>
|
|
#endif
|
|
#include <chrono>
|
|
#include "utilities/translation_macros.h"
|
|
#include "imgui/imgui.h"
|
|
#include "utilities/crashreporter_macros.h"
|
|
#include "widgets/imgui_scale.h"
|
|
#include <iostream>
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#endif
|
|
#ifdef __unix__
|
|
#include <unistd.h>
|
|
#endif
|
|
#ifdef __unix__
|
|
#include <sys/stat.h>
|
|
#endif
|
|
|
|
module eu07.application.application;
|
|
import eu07.rendering.screenshot;
|
|
import eu07.utilities.parser;
|
|
import eu07.audio.sound;
|
|
import eu07.glm;
|
|
import eu07.application.drivermode;
|
|
import eu07.application.editormode;
|
|
import eu07.application.scenarioloadermode;
|
|
import eu07.editor.editorterrainstreamer;
|
|
import eu07.input.command;
|
|
import eu07.launcher.launchermode;
|
|
import eu07.model.texture;
|
|
import eu07.simcore;
|
|
import eu07.utilities.globals;
|
|
import eu07.utilities.utilities;
|
|
import eu07.simulation.simulation;
|
|
import eu07.simulation.loadprofile;
|
|
import eu07.application.shotrunner;
|
|
import eu07.simulation.simulationsounds;
|
|
import eu07.vehicle.train;
|
|
import eu07.utilities.dictionary;
|
|
import eu07.scene.sceneeditor;
|
|
import eu07.rendering.renderer;
|
|
import eu07.application.uilayer;
|
|
import eu07.utilities.logs;
|
|
import eu07.utilities.translation;
|
|
import eu07.utilities.timer;
|
|
import eu07.utilities.crashreporter;
|
|
/*
|
|
This Source Code Form is subject to the
|
|
terms of the Mozilla Public License, v.
|
|
2.0. If a copy of the MPL was not
|
|
distributed with this file, You can
|
|
obtain one at
|
|
http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
|
|
|
|
#if WITH_DISCORD_RPC
|
|
#endif
|
|
|
|
|
|
#ifdef _WIN32
|
|
#endif
|
|
|
|
#ifdef _WIN32
|
|
#pragma comment(lib, "dsound.lib")
|
|
#pragma comment(lib, "winmm.lib")
|
|
#pragma comment(lib, "setupapi.lib")
|
|
#pragma comment(lib, "dbghelp.lib")
|
|
#pragma comment(lib, "version.lib")
|
|
#endif
|
|
|
|
#ifdef __unix__
|
|
#endif
|
|
|
|
eu07_application Application;
|
|
screenshot_manager screenshot_man;
|
|
ui_layer uilayerstaticinitializer;
|
|
|
|
#ifdef _WIN32
|
|
extern "C"
|
|
{
|
|
GLFWAPI HWND glfwGetWin32Window(GLFWwindow *window);
|
|
}
|
|
|
|
LRESULT APIENTRY WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
|
|
extern HWND Hwnd;
|
|
extern WNDPROC BaseWindowProc;
|
|
#endif
|
|
|
|
// user input callbacks
|
|
|
|
void focus_callback(GLFWwindow *window, int focus)
|
|
{
|
|
Application.on_focus_change(focus != 0);
|
|
}
|
|
|
|
void framebuffer_resize_callback(GLFWwindow *, int w, int h)
|
|
{
|
|
Global.fb_size = glm::ivec2(w, h);
|
|
}
|
|
|
|
void window_resize_callback(GLFWwindow *, int w, int h)
|
|
{
|
|
Global.window_size = glm::ivec2(w, h);
|
|
Application.on_window_resize(w, h);
|
|
}
|
|
|
|
void cursor_pos_callback(GLFWwindow *window, double x, double y)
|
|
{
|
|
Global.cursor_pos = glm::ivec2(x, y);
|
|
Application.on_cursor_pos(x, y);
|
|
}
|
|
|
|
void mouse_button_callback(GLFWwindow *window, int button, int action, int mods)
|
|
{
|
|
Application.on_mouse_button(button, action, mods);
|
|
}
|
|
|
|
void scroll_callback(GLFWwindow *window, double xoffset, double yoffset)
|
|
{
|
|
Application.on_scroll(xoffset, yoffset);
|
|
}
|
|
|
|
void key_callback(GLFWwindow *window, int key, int scancode, int action, int mods)
|
|
{
|
|
|
|
Application.on_key(key, scancode, action, mods);
|
|
}
|
|
|
|
void char_callback(GLFWwindow *window, unsigned int c)
|
|
{
|
|
Application.on_char(c);
|
|
}
|
|
|
|
// public:
|
|
|
|
void eu07_application::queue_screenshot()
|
|
{
|
|
m_screenshot_queued = true;
|
|
}
|
|
|
|
int eu07_application::run_crashgui()
|
|
{
|
|
bool autoup = false;
|
|
|
|
while (!glfwWindowShouldClose(m_windows.front()))
|
|
{
|
|
glfwPollEvents();
|
|
|
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
|
ui_layer::begin_ui_frame_internal();
|
|
|
|
bool y, n;
|
|
|
|
if (Global.asLang == "pl")
|
|
{
|
|
ImGui::Begin("Raportowanie błędów", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoResize);
|
|
ImGui::TextUnformatted("Podczas ostatniego uruchomienia symulatora wystąpił błąd.\nWysłać raport o błędzie do deweloperów?\n");
|
|
ImGui::TextUnformatted(("Usługa udostępniana przez " + crashreport_get_provider() + "\n").c_str());
|
|
y = ImGui::Button("Tak", ImVec2S(60, 0));
|
|
ImGui::SameLine();
|
|
ImGui::Checkbox("W przyszłości przesyłaj raporty o błędach automatycznie", &autoup);
|
|
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("(?)");
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::BeginTooltip();
|
|
ImGui::TextUnformatted("W celu wyłączenia tej funkcji będzie trzeba skasować plik crashdumps/autoupload_enabled.conf");
|
|
ImGui::EndTooltip();
|
|
}
|
|
|
|
ImGui::NewLine();
|
|
n = ImGui::Button("Nie", ImVec2S(60, 0));
|
|
ImGui::End();
|
|
}
|
|
else
|
|
{
|
|
ImGui::Begin("Crash reporting", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoResize);
|
|
ImGui::TextUnformatted("Crash occurred during last launch of the simulator.\nSend crash report to developers?\n");
|
|
ImGui::TextUnformatted(("Service provided by " + crashreport_get_provider() + "\n").c_str());
|
|
y = ImGui::Button("Yes", ImVec2S(60, 0));
|
|
ImGui::SameLine();
|
|
ImGui::Checkbox("In future send crash reports automatically", &autoup);
|
|
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("(?)");
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::BeginTooltip();
|
|
ImGui::TextUnformatted("To disable this feature remove file crashdumps/autoupload_enabled.conf");
|
|
ImGui::EndTooltip();
|
|
}
|
|
|
|
ImGui::NewLine();
|
|
n = ImGui::Button("No", ImVec2S(60, 0));
|
|
ImGui::End();
|
|
}
|
|
|
|
ui_layer::render_internal();
|
|
glfwSwapBuffers(m_windows.front());
|
|
|
|
if (y)
|
|
{
|
|
crashreport_upload_accept();
|
|
if (autoup)
|
|
crashreport_set_autoupload();
|
|
return 0;
|
|
}
|
|
|
|
if (n)
|
|
{
|
|
crashreport_upload_reject();
|
|
return 0;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
#if WITH_DISCORD_RPC
|
|
void eu07_application::DiscordRPCService()
|
|
{
|
|
// initialize discord-rpc
|
|
WriteLog("Initializing Discord Rich Presence...");
|
|
static const char *discord_app_id = "1343662664504840222";
|
|
DiscordEventHandlers handlers;
|
|
memset(&handlers, 0, sizeof(handlers));
|
|
Discord_Initialize(discord_app_id, &handlers, 1, nullptr);
|
|
|
|
// calculate startup timestamp
|
|
auto now = std::chrono::system_clock::now();
|
|
auto now_c = std::chrono::system_clock::to_time_t(now);
|
|
|
|
// Init RPC object
|
|
static DiscordRichPresence discord_rpc;
|
|
memset(&discord_rpc, 0, sizeof(discord_rpc));
|
|
discord_rpc.startTimestamp = static_cast<int64_t>(now_c);
|
|
discord_rpc.largeImageText = "MaSzyna";
|
|
|
|
// run until quit — do NOT require a non-empty mode stack: pop/push during scenery reload
|
|
// briefly empties the stack, and exiting here would Discord_Shutdown() mid-session (or race on top())
|
|
while (!Global.applicationQuitOrder && !m_windows.empty() && !glfwWindowShouldClose(m_windows.front()))
|
|
{
|
|
if (m_modestack.empty())
|
|
{
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
continue;
|
|
}
|
|
|
|
auto const currentMode = m_modestack.top();
|
|
if (currentMode == mode::launcher)
|
|
{
|
|
discord_rpc.state = Translations.lookup_c("In main menu");
|
|
discord_rpc.details = Translations.lookup_c("Browsing scenarios...");
|
|
discord_rpc.largeImageKey = "";
|
|
discord_rpc.largeImageText = "MaSzyna";
|
|
Discord_UpdatePresence(&discord_rpc);
|
|
}
|
|
else if (currentMode == mode::scenarioloader)
|
|
{
|
|
std::string rpcScnName = Global.SceneryFile;
|
|
if (!rpcScnName.empty() && rpcScnName.front() == '$')
|
|
rpcScnName.erase(0, 1);
|
|
if (rpcScnName.size() >= 4)
|
|
rpcScnName.erase(rpcScnName.size() - 4, 4);
|
|
std::replace(rpcScnName.begin(), rpcScnName.end(), '_', ' ');
|
|
|
|
static std::string state;
|
|
state = Translations.lookup_s("Scenery: ") + rpcScnName;
|
|
discord_rpc.state = state.c_str();
|
|
discord_rpc.details = Translations.lookup_c("Loading scenery...");
|
|
discord_rpc.largeImageKey = "logo";
|
|
discord_rpc.largeImageText = "MaSzyna";
|
|
Discord_UpdatePresence(&discord_rpc);
|
|
}
|
|
else if (currentMode == mode::editor)
|
|
{
|
|
discord_rpc.state = "Dłubie w edytorze";
|
|
discord_rpc.details = Global.SceneryFile.c_str();
|
|
discord_rpc.largeImageKey = "logo";
|
|
discord_rpc.largeImageText = "MaSzyna";
|
|
discord_rpc.smallImageKey = "";
|
|
discord_rpc.smallImageText = "";
|
|
Discord_UpdatePresence(&discord_rpc);
|
|
}
|
|
else if (currentMode == mode::driver && simulation::is_ready)
|
|
{
|
|
std::string PlayerVehicle;
|
|
if (simulation::Train != nullptr && simulation::Train->Dynamic() != nullptr)
|
|
{
|
|
PlayerVehicle = simulation::Train->name();
|
|
for (auto &c : PlayerVehicle)
|
|
c = static_cast<char>(toupper(static_cast<unsigned char>(c)));
|
|
|
|
PlayerVehicle = Translations.lookup_s("Driving: ") + PlayerVehicle;
|
|
discord_rpc.details = PlayerVehicle.c_str();
|
|
|
|
auto const playerTrainVelocity = static_cast<uint16_t>(simulation::Train->Dynamic()->GetVelocity());
|
|
if (playerTrainVelocity > 1)
|
|
{
|
|
discord_rpc.smallImageKey = "driving";
|
|
static std::string smallText;
|
|
smallText = Translations.lookup_s("Speed: ") + std::to_string(playerTrainVelocity) + " km/h";
|
|
discord_rpc.smallImageText = smallText.c_str();
|
|
}
|
|
else
|
|
{
|
|
discord_rpc.smallImageKey = "halt";
|
|
discord_rpc.smallImageText = Translations.lookup_c("Stopped");
|
|
}
|
|
}
|
|
|
|
Discord_UpdatePresence(&discord_rpc);
|
|
}
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
|
|
}
|
|
|
|
Discord_Shutdown();
|
|
|
|
}
|
|
#endif
|
|
|
|
int eu07_application::init(int Argc, char *Argv[])
|
|
{
|
|
|
|
int result{0};
|
|
// start logging service
|
|
std::thread sLoggingService(LogService);
|
|
Global.threads.emplace("LogService", std::move(sLoggingService));
|
|
|
|
init_debug();
|
|
init_files();
|
|
if ((result = init_settings(Argc, Argv)) != 0)
|
|
{
|
|
ErrorLog("Failed to initialize settings! Maybe you're missing eu07.ini file?");
|
|
return result;
|
|
}
|
|
|
|
if (!Global.random_seed)
|
|
Global.random_seed = std::random_device{}();
|
|
Global.random_engine.seed(Global.random_seed);
|
|
|
|
// configure the OS console according to Globals.ShowSystemConsole.
|
|
// must run AFTER init_settings (so the ini-loaded value is honoured) and
|
|
// BEFORE the first WriteLog below (so colored \033[...] sequences emitted
|
|
// by utilities/Logs.cpp land in a console with VT processing enabled).
|
|
init_console();
|
|
|
|
WriteLog("Starting MaSzyna rail vehicle simulator (release: " + Global.asVersion + ")");
|
|
WriteLog("For online documentation and additional files refer to: http://eu07.pl");
|
|
WriteLog("Authors: Marcin_EU, McZapkie, ABu, Winger, Tolaris, nbmx, OLO_EU, Bart, Quark-t, "
|
|
"ShaXbee, Oli_EU, youBy, KURS90, Ra, hunter, szociu, Stele, Q, firleju and others");
|
|
|
|
if (!crashreport_get_provider().empty())
|
|
WriteLog("Crashdump analysis provided by " + crashreport_get_provider() + "\n");
|
|
|
|
{
|
|
WriteLog("// settings");
|
|
std::stringstream settingspipe;
|
|
Global.export_as_text(settingspipe);
|
|
WriteLog(settingspipe.str());
|
|
}
|
|
|
|
// cruel way to prevent crashes because of threaded upload from python
|
|
if (Global.NvRenderer)
|
|
Global.python_threadedupload = false;
|
|
|
|
WriteLog("// startup");
|
|
|
|
if ((result = init_glfw()) != 0)
|
|
{
|
|
ErrorLog("Failed to initialize glfw!");
|
|
return result;
|
|
}
|
|
if (needs_ogl() && (result = init_ogl()) != 0)
|
|
{
|
|
ErrorLog("Failed to initialize ogl!");
|
|
return result;
|
|
}
|
|
|
|
if (crashreport_is_pending())
|
|
{ // run crashgui as early as possible
|
|
if ((result = run_crashgui()) != 0)
|
|
{
|
|
ErrorLog("Failed to run crash gui!");
|
|
return result;
|
|
}
|
|
}
|
|
|
|
if ((result = init_locale()) != 0)
|
|
{
|
|
ErrorLog("Failed to initialize locales! Maybe you're missing lang directory?");
|
|
return result;
|
|
}
|
|
|
|
if ((result = init_gfx()) != 0)
|
|
{
|
|
ErrorLog("Failed to initialize GFX!");
|
|
return result;
|
|
}
|
|
|
|
if ((result = init_ui()) != 0)
|
|
{ // ui now depends on activated renderer
|
|
ErrorLog("Failed to init UI!");
|
|
return result;
|
|
}
|
|
if ((result = init_audio()) != 0)
|
|
{
|
|
ErrorLog("Failed to initialize OpenAL");
|
|
return result;
|
|
}
|
|
if ((result = init_data()) != 0)
|
|
{
|
|
ErrorLog("Failed to load data/ contents! Maybe your installation is broken?");
|
|
return result;
|
|
}
|
|
crashreport_add_info("python_enabled", Global.python_enabled ? "yes" : "no");
|
|
if (Global.python_enabled)
|
|
{
|
|
m_taskqueue.init();
|
|
}
|
|
if ((result = init_modes()) != 0)
|
|
{
|
|
ErrorLog("Failed to initialize game modes");
|
|
return result;
|
|
}
|
|
|
|
|
|
#if WITH_DISCORD_RPC
|
|
// Run DiscordRPC service
|
|
std::thread sDiscordRPC(&eu07_application::DiscordRPCService, this);
|
|
Global.threads.emplace("DiscordRPC", std::move(sDiscordRPC));
|
|
#endif
|
|
|
|
if (!init_network())
|
|
return -1;
|
|
|
|
return result;
|
|
}
|
|
|
|
double eu07_application::generate_sync()
|
|
{
|
|
if (Timer::GetDeltaTime() == 0.0)
|
|
return 0.0;
|
|
double sync = 0.0;
|
|
for (const TDynamicObject *vehicle : simulation::Vehicles.sequence())
|
|
{
|
|
auto const pos{vehicle->GetPosition()};
|
|
sync += pos.x + pos.y + pos.z;
|
|
}
|
|
sync += Random(1.0, 100.0);
|
|
return sync;
|
|
}
|
|
|
|
void eu07_application::queue_quit(bool direct)
|
|
{
|
|
if (direct || !m_modes[m_modestack.top()]->is_command_processor())
|
|
{
|
|
glfwSetWindowShouldClose(m_windows[0], GLFW_TRUE);
|
|
return;
|
|
}
|
|
|
|
command_relay relay;
|
|
relay.post(user_command::quitsimulation, 0.0, 0.0, GLFW_PRESS, 0);
|
|
}
|
|
|
|
bool eu07_application::is_server() const
|
|
{
|
|
|
|
return m_network && m_network->servers;
|
|
}
|
|
|
|
bool eu07_application::is_client() const
|
|
{
|
|
|
|
return m_network && m_network->client;
|
|
}
|
|
|
|
int eu07_application::run()
|
|
{
|
|
auto frame{0};
|
|
// main application loop
|
|
while (!glfwWindowShouldClose(m_windows.front()) && !m_modestack.empty())
|
|
{
|
|
Timer::subsystem.mainloop_total.start();
|
|
glfwPollEvents();
|
|
|
|
if (m_headtrack)
|
|
m_headtrack->update();
|
|
|
|
begin_ui_frame();
|
|
|
|
// -------------------------------------------------------------------
|
|
// multiplayer command relaying logic can seem a bit complex
|
|
//
|
|
// we are simultaneously:
|
|
// => master (not client) OR slave (client)
|
|
// => server OR not
|
|
//
|
|
// trivia: being client and server is possible
|
|
|
|
double frameStartTime = Timer::GetTime();
|
|
|
|
if (m_modes[m_modestack.top()]->is_command_processor())
|
|
{
|
|
// active mode is doing real calculations (e.g. drivermode)
|
|
int loop_remaining = MAX_NETWORK_PER_FRAME;
|
|
while (--loop_remaining > 0)
|
|
{
|
|
command_queue::commands_map commands_to_exec;
|
|
command_queue::commands_map local_commands = simulation::Commands.pop_intercept_queue();
|
|
double slave_sync;
|
|
|
|
// if we're the server
|
|
if (m_network && m_network->servers)
|
|
{
|
|
// fetch from network layer command requests received from clients
|
|
command_queue::commands_map remote_commands = m_network->servers->pop_commands();
|
|
|
|
// push these into local queue
|
|
add_to_dequemap(local_commands, remote_commands);
|
|
}
|
|
|
|
// if we're slave
|
|
if (m_network && m_network->client)
|
|
{
|
|
// fetch frame info from network layer,
|
|
auto frame_info = m_network->client->get_next_delta(MAX_NETWORK_PER_FRAME - loop_remaining);
|
|
|
|
// use delta and commands received from master
|
|
double delta = std::get<0>(frame_info);
|
|
Timer::set_delta_override(delta);
|
|
slave_sync = std::get<1>(frame_info);
|
|
add_to_dequemap(commands_to_exec, std::get<2>(frame_info));
|
|
|
|
// and send our local commands to master
|
|
m_network->client->send_commands(local_commands);
|
|
|
|
if (delta == 0.0)
|
|
loop_remaining = -1;
|
|
}
|
|
// if we're master
|
|
else
|
|
{
|
|
// just push local commands to execution
|
|
add_to_dequemap(commands_to_exec, local_commands);
|
|
|
|
loop_remaining = -1;
|
|
}
|
|
|
|
// send commands to command queue
|
|
simulation::Commands.push_commands(commands_to_exec);
|
|
|
|
// do actual frame processing (depending on mode)
|
|
if (!m_modes[m_modestack.top()]->update())
|
|
return 0;
|
|
|
|
// update continuous commands
|
|
simulation::Commands.update();
|
|
|
|
double sync = generate_sync();
|
|
|
|
// if we're the server
|
|
if (m_network && m_network->servers)
|
|
{
|
|
// send delta, sync, and commands we just executed to clients
|
|
double delta = Timer::GetDeltaTime();
|
|
double render = Timer::GetDeltaRenderTime();
|
|
m_network->servers->push_delta(render, delta, sync, commands_to_exec);
|
|
}
|
|
|
|
// if we're slave
|
|
if (m_network && m_network->client)
|
|
{
|
|
// verify sync
|
|
if (sync != slave_sync)
|
|
{
|
|
WriteLog("net: desync! calculated: " + std::to_string(sync) + ", received: " + std::to_string(slave_sync), logtype::net);
|
|
|
|
Global.desync = slave_sync - sync;
|
|
}
|
|
|
|
// set total delta for rendering code
|
|
double totalDelta = Timer::GetTime() - frameStartTime;
|
|
Timer::set_delta_override(totalDelta);
|
|
}
|
|
}
|
|
|
|
if (!loop_remaining)
|
|
{
|
|
// loop break forced by counter
|
|
float received = m_network->client->get_frame_counter();
|
|
float awaiting = m_network->client->get_awaiting_frames();
|
|
|
|
// TODO: don't meddle with mode progresbar
|
|
m_modes[m_modestack.top()]->set_progress(100.0f * (received - awaiting) / received);
|
|
}
|
|
else
|
|
{
|
|
m_modes[m_modestack.top()]->set_progress(0.0f, 0.0f);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// active mode is loader
|
|
|
|
// clear local command queue
|
|
simulation::Commands.pop_intercept_queue();
|
|
|
|
// do actual frame processing
|
|
if (!m_modes[m_modestack.top()]->update())
|
|
return 0;
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
// keep streamed terrain loaded around the active camera in every mode (editor + driver),
|
|
// once the simulation is live (avoid streaming while the scenery is still loading)
|
|
if (simulation::is_ready && EditorTerrain.active())
|
|
EditorTerrain.update(Global.pCamera.Pos);
|
|
|
|
m_taskqueue.update();
|
|
opengl_texture::reset_unit_cache();
|
|
|
|
if (!GfxRenderer->Render())
|
|
break;
|
|
|
|
GfxRenderer->SwapBuffers();
|
|
|
|
if (m_modestack.empty())
|
|
break;
|
|
|
|
m_modes[m_modestack.top()]->on_event_poll();
|
|
|
|
if (m_screenshot_queued)
|
|
{
|
|
m_screenshot_queued = false;
|
|
GfxRenderer->MakeScreenshot();
|
|
}
|
|
|
|
if (m_network)
|
|
m_network->update();
|
|
|
|
auto const frametime{Timer::subsystem.mainloop_total.stop()};
|
|
if (Global.minframetime.count() != 0.0f && (Global.minframetime - frametime).count() > 0.0f)
|
|
{
|
|
std::this_thread::sleep_for(Global.minframetime - frametime);
|
|
}
|
|
}
|
|
// Handled by application.exit
|
|
// Global.applicationQuitOrder = true;
|
|
// auto it = Global.threads.find("LogService");
|
|
// if (it != Global.threads.end())
|
|
// {
|
|
// if (it->second.joinable())
|
|
// it->second.join();
|
|
// }
|
|
//
|
|
// it = Global.threads.find("DiscordRPC");
|
|
// if (it != Global.threads.end())
|
|
// {
|
|
// if (it->second.joinable())
|
|
// it->second.join();
|
|
// }
|
|
return 0;
|
|
}
|
|
|
|
// issues request for a worker thread to perform specified task. returns: true if task was scheduled
|
|
bool eu07_application::request(python_taskqueue::task_request const &Task)
|
|
{
|
|
|
|
auto const result{m_taskqueue.insert(Task)};
|
|
if (false == result && Task.input != nullptr)
|
|
{
|
|
// clean up allocated resources since the worker won't
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ensures the main thread holds the python gil and can safely execute python calls
|
|
void eu07_application::acquire_python_lock()
|
|
{
|
|
|
|
m_taskqueue.acquire_lock();
|
|
}
|
|
|
|
// frees the python gil and swaps out the main thread
|
|
void eu07_application::release_python_lock()
|
|
{
|
|
|
|
m_taskqueue.release_lock();
|
|
}
|
|
|
|
void eu07_application::exit()
|
|
{
|
|
Global.applicationQuitOrder = true;
|
|
for (auto &mode : m_modes)
|
|
mode.reset();
|
|
|
|
GfxRenderer->Shutdown();
|
|
m_network.reset();
|
|
|
|
// SafeDelete( simulation::Train );
|
|
SafeDelete(simulation::Region);
|
|
|
|
ui_layer::shutdown();
|
|
|
|
for (auto *window : m_windows)
|
|
{
|
|
glfwDestroyWindow(window);
|
|
}
|
|
m_taskqueue.exit();
|
|
glfwPollEvents(); // TODO: This fixes a segfault on Wayland when closing. Remove after updating glfw to 3.5.
|
|
glfwTerminate();
|
|
|
|
if (!Global.exec_on_exit.empty())
|
|
system(Global.exec_on_exit.c_str());
|
|
|
|
auto it = Global.threads.find("LogService");
|
|
if (it != Global.threads.end())
|
|
{
|
|
if (it->second.joinable())
|
|
it->second.join();
|
|
}
|
|
|
|
#if WITH_DISCORD_RPC
|
|
it = Global.threads.find("DiscordRPC");
|
|
if (it != Global.threads.end())
|
|
{
|
|
if (it->second.joinable())
|
|
it->second.join();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void eu07_application::render_ui()
|
|
{
|
|
|
|
if (m_modestack.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_modes[m_modestack.top()]->render_ui();
|
|
}
|
|
|
|
void eu07_application::begin_ui_frame()
|
|
{
|
|
|
|
if (m_modestack.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_modes[m_modestack.top()]->begin_ui_frame();
|
|
}
|
|
|
|
bool eu07_application::pop_mode()
|
|
{
|
|
if (m_modestack.empty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
m_modes[m_modestack.top()]->exit();
|
|
m_modestack.pop();
|
|
return true;
|
|
}
|
|
|
|
bool eu07_application::push_mode(mode const Mode)
|
|
{
|
|
|
|
if (Mode >= count_)
|
|
return false;
|
|
|
|
if (!m_modes[Mode])
|
|
{
|
|
if (Mode == launcher)
|
|
m_modes[Mode] = std::make_shared<launcher_mode>();
|
|
if (Mode == scenarioloader)
|
|
m_modes[Mode] = std::make_shared<scenarioloader_mode>();
|
|
if (Mode == driver)
|
|
m_modes[Mode] = std::make_shared<driver_mode>();
|
|
if (Mode == editor)
|
|
m_modes[Mode] = std::make_shared<editor_mode>();
|
|
|
|
if (!m_modes[Mode]->init())
|
|
return false;
|
|
}
|
|
|
|
m_modes[Mode]->enter();
|
|
m_modestack.push(Mode);
|
|
|
|
return true;
|
|
}
|
|
|
|
void eu07_application::set_title(std::string const &Title)
|
|
{
|
|
|
|
glfwSetWindowTitle(m_windows.front(), Title.c_str());
|
|
}
|
|
|
|
void eu07_application::set_progress(float const Progress, float const Subtaskprogress)
|
|
{
|
|
|
|
if (m_modestack.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_modes[m_modestack.top()]->set_progress(Progress, Subtaskprogress);
|
|
}
|
|
|
|
void eu07_application::set_tooltip(std::string const &Tooltip)
|
|
{
|
|
|
|
if (m_modestack.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_modes[m_modestack.top()]->set_tooltip(Tooltip);
|
|
}
|
|
|
|
void eu07_application::set_cursor(int const Mode)
|
|
{
|
|
|
|
ui_layer::set_cursor(Mode);
|
|
}
|
|
|
|
void eu07_application::set_cursor_pos(double const Horizontal, double const Vertical)
|
|
{
|
|
|
|
glfwSetCursorPos(m_windows.front(), Horizontal, Vertical);
|
|
}
|
|
|
|
/*
|
|
// provides keyboard mapping associated with specified control item
|
|
std::string
|
|
eu07_application::get_input_hint( user_command const Command ) const {
|
|
|
|
if( m_modestack.empty() ) { return ""; }
|
|
|
|
return m_modes[ m_modestack.top() ]->get_input_hint( Command );
|
|
}
|
|
*/
|
|
|
|
void eu07_application::on_key(int const Key, int const Scancode, int const Action, int const Mods)
|
|
{
|
|
|
|
if (ui_layer::key_callback(Key, Scancode, Action, Mods))
|
|
return;
|
|
|
|
if (m_modestack.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
#ifdef __unix__
|
|
if (Key == GLFW_KEY_LEFT_SHIFT || Key == GLFW_KEY_RIGHT_SHIFT)
|
|
Global.shiftState = Action == GLFW_PRESS;
|
|
if (Key == GLFW_KEY_LEFT_CONTROL || Key == GLFW_KEY_RIGHT_CONTROL)
|
|
Global.ctrlState = Action == GLFW_PRESS;
|
|
if (Key == GLFW_KEY_LEFT_ALT || Key == GLFW_KEY_RIGHT_ALT)
|
|
Global.altState = Action == GLFW_PRESS;
|
|
#endif
|
|
|
|
m_modes[m_modestack.top()]->on_key(Key, Scancode, Action, Mods);
|
|
}
|
|
|
|
void eu07_application::on_cursor_pos(double const Horizontal, double const Vertical)
|
|
{
|
|
|
|
if (m_modestack.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_modes[m_modestack.top()]->on_cursor_pos(Horizontal, Vertical);
|
|
}
|
|
|
|
void eu07_application::on_mouse_button(int const Button, int const Action, int const Mods)
|
|
{
|
|
|
|
if (ui_layer::mouse_button_callback(Button, Action, Mods))
|
|
return;
|
|
|
|
if (m_modestack.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_modes[m_modestack.top()]->on_mouse_button(Button, Action, Mods);
|
|
}
|
|
|
|
void eu07_application::on_scroll(double const Xoffset, double const Yoffset)
|
|
{
|
|
|
|
if (ui_layer::scroll_callback(Xoffset, Yoffset))
|
|
return;
|
|
|
|
if (m_modestack.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_modes[m_modestack.top()]->on_scroll(Xoffset, Yoffset);
|
|
}
|
|
|
|
void eu07_application::on_char(unsigned int c)
|
|
{
|
|
if (ui_layer::char_callback(c))
|
|
return;
|
|
}
|
|
|
|
void eu07_application::on_focus_change(bool focus)
|
|
{
|
|
if (Global.bInactivePause && m_network.has_value() && !m_network->client)
|
|
{ // jeśli ma być pauzowanie okna w tle
|
|
command_relay relay;
|
|
relay.post(user_command::focuspauseset, focus ? 1.0 : 0.0, 0.0, GLFW_PRESS, 0);
|
|
}
|
|
}
|
|
|
|
void eu07_application::on_window_resize(int w, int h)
|
|
{
|
|
if (m_modestack.empty())
|
|
return;
|
|
m_modes[m_modestack.top()]->on_window_resize(w, h);
|
|
}
|
|
|
|
GLFWwindow *eu07_application::window(int const Windowindex, bool visible, int width, int height, GLFWmonitor *monitor, bool keep_ownership, bool share_ctx)
|
|
{
|
|
|
|
if (Windowindex >= 0)
|
|
{
|
|
return Windowindex < m_windows.size() ? m_windows[Windowindex] : nullptr;
|
|
}
|
|
// for index -1 create a new child window
|
|
|
|
auto const *vmode{glfwGetVideoMode(monitor ? monitor : glfwGetPrimaryMonitor())};
|
|
|
|
// match requested video mode to current to allow for
|
|
// fullwindow creation when resolution is the same
|
|
glfwWindowHint(GLFW_RED_BITS, vmode->redBits);
|
|
glfwWindowHint(GLFW_GREEN_BITS, vmode->greenBits);
|
|
glfwWindowHint(GLFW_BLUE_BITS, vmode->blueBits);
|
|
glfwWindowHint(GLFW_REFRESH_RATE, vmode->refreshRate);
|
|
|
|
glfwWindowHint(GLFW_VISIBLE, visible);
|
|
|
|
auto *childwindow = glfwCreateWindow(width, height, "eu07window", monitor, share_ctx ? m_windows.front() : nullptr);
|
|
if (!childwindow)
|
|
return nullptr;
|
|
|
|
if (keep_ownership)
|
|
m_windows.emplace_back(childwindow);
|
|
|
|
glfwFocusWindow(m_windows.front()); // restore focus to main window
|
|
|
|
return childwindow;
|
|
}
|
|
|
|
// private:
|
|
GLFWmonitor *eu07_application::find_monitor(const std::string &str) const
|
|
{
|
|
int monitor_count;
|
|
GLFWmonitor **monitors = glfwGetMonitors(&monitor_count);
|
|
|
|
for (size_t i = 0; i < monitor_count; i++)
|
|
{
|
|
if (describe_monitor(monitors[i]) == str)
|
|
return monitors[i];
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
std::string eu07_application::describe_monitor(GLFWmonitor *monitor) const
|
|
{
|
|
std::string name(glfwGetMonitorName(monitor));
|
|
std::replace(std::begin(name), std::end(name), ' ', '_');
|
|
|
|
int x, y;
|
|
glfwGetMonitorPos(monitor, &x, &y);
|
|
|
|
return name + ":" + std::to_string(x) + "," + std::to_string(y);
|
|
}
|
|
|
|
// private:
|
|
|
|
bool eu07_application::needs_ogl() const
|
|
{
|
|
return !Global.NvRenderer;
|
|
}
|
|
|
|
void eu07_application::init_debug()
|
|
{
|
|
|
|
#if defined(_MSC_VER) && defined(_DEBUG)
|
|
// memory leaks
|
|
_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
|
|
/*
|
|
// floating point operation errors
|
|
auto state { _clearfp() };
|
|
state = _control87( 0, 0 );
|
|
// this will turn on FPE for #IND and zerodiv
|
|
state = _control87( state & ~( _EM_ZERODIVIDE | _EM_INVALID ), _MCW_EM );
|
|
*/
|
|
#endif
|
|
}
|
|
|
|
// Honours Globals.ShowSystemConsole (default: true).
|
|
//
|
|
// Windows: if the toggle is on, ensure a console is attached (allocate one when
|
|
// the binary was linked as the WINDOWS subsystem and none is inherited), then
|
|
// reopen the C stdio streams onto it and turn on ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
// so the ANSI colour escapes used by utilities/Logs.cpp render correctly.
|
|
// If the toggle is off, hide the console window (or free an allocated one) so
|
|
// the GUI launches without a stray cmd window.
|
|
//
|
|
// Other platforms: no-op. stdout is the launching terminal (or already detached);
|
|
// hiding it on Linux/macOS would mean redirecting to /dev/null and is left to the
|
|
// user's shell (e.g. "./eu07 >/dev/null 2>&1").
|
|
void eu07_application::init_console()
|
|
{
|
|
#ifdef _WIN32
|
|
HWND consoleWnd = ::GetConsoleWindow();
|
|
const bool hadConsole = consoleWnd != nullptr;
|
|
|
|
if (Global.ShowSystemConsole)
|
|
{
|
|
if (!hadConsole)
|
|
{
|
|
// no console inherited (e.g. WINDOWS subsystem build, or launched
|
|
// detached) -- create one and wire stdio to it so printf in
|
|
// utilities/Logs.cpp actually reaches the user
|
|
if (::AllocConsole())
|
|
{
|
|
FILE *fp = nullptr;
|
|
freopen_s(&fp, "CONOUT$", "w", stdout);
|
|
freopen_s(&fp, "CONOUT$", "w", stderr);
|
|
freopen_s(&fp, "CONIN$", "r", stdin);
|
|
// keep C++ streams in sync with the redirected C streams
|
|
std::ios::sync_with_stdio(true);
|
|
std::cout.clear();
|
|
std::cerr.clear();
|
|
std::clog.clear();
|
|
std::cin.clear();
|
|
consoleWnd = ::GetConsoleWindow();
|
|
}
|
|
}
|
|
|
|
// enable ANSI virtual-terminal processing so the colour escapes in
|
|
// utilities/Logs.cpp ("\033[1;37;41m...", "\033[32m...") render as
|
|
// colours instead of literal garbage on the conhost.
|
|
//
|
|
// IMPORTANT: do NOT rely on GetStdHandle(STD_OUTPUT_HANDLE). After
|
|
// AllocConsole + freopen_s the CRT may have rewired things, and on a
|
|
// console-subsystem build the std handle can also point to a redirected
|
|
// pipe/file rather than the actual console screen buffer -- in either
|
|
// case SetConsoleMode silently fails and the escape bytes leak through
|
|
// as text ("←[32m" etc). Open CONOUT$ directly to get the real screen
|
|
// buffer handle, then flip VT on it.
|
|
auto enable_vt = [](const char *devName) {
|
|
HANDLE h = ::CreateFileA(
|
|
devName,
|
|
GENERIC_READ | GENERIC_WRITE,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
nullptr,
|
|
OPEN_EXISTING,
|
|
0,
|
|
nullptr);
|
|
if (h == INVALID_HANDLE_VALUE)
|
|
return;
|
|
DWORD mode = 0;
|
|
if (::GetConsoleMode(h, &mode))
|
|
{
|
|
::SetConsoleMode(h, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT);
|
|
}
|
|
::CloseHandle(h);
|
|
};
|
|
// CONOUT$ is the console's active screen buffer; enabling VT on it
|
|
// affects everything that ends up being written there, regardless of
|
|
// which FILE* / std handle the writer used
|
|
enable_vt("CONOUT$");
|
|
|
|
if (consoleWnd)
|
|
{
|
|
::ShowWindow(consoleWnd, SW_SHOW);
|
|
// give the console a recognisable title
|
|
::SetConsoleTitleA("MaSzyna log");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// user wants no console window -- hide whatever we have
|
|
if (consoleWnd)
|
|
{
|
|
::ShowWindow(consoleWnd, SW_HIDE);
|
|
}
|
|
// if we'd allocated one ourselves on a previous run path we'd FreeConsole here,
|
|
// but on first init the console (if any) was inherited from the launcher and
|
|
// belongs to it -- just hiding the window is the least-surprising behaviour
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void eu07_application::init_files()
|
|
{
|
|
|
|
#ifdef _WIN32
|
|
DeleteFile("log.txt");
|
|
DeleteFile("errors.txt");
|
|
CreateDirectory("logs", nullptr);
|
|
#elif __unix__
|
|
unlink("log.txt");
|
|
unlink("errors.txt");
|
|
mkdir("logs", 0755);
|
|
#endif
|
|
}
|
|
namespace fs = std::filesystem;
|
|
|
|
int eu07_application::init_settings(int Argc, char *Argv[])
|
|
{
|
|
Global.asVersion = VERSION_INFO;
|
|
|
|
fs::path iniPath = user_config_path("eu07.ini");
|
|
|
|
if (!iniPath.empty() && fs::exists(iniPath))
|
|
{
|
|
Global.LoadIniFile(iniPath.string().c_str());
|
|
}
|
|
else
|
|
{
|
|
Global.LoadIniFile("eu07.ini");
|
|
}
|
|
|
|
// process command line arguments
|
|
auto scenerywasasked = false;
|
|
std::vector<shot_runner::shot> shots;
|
|
auto shotsettle = 3.0;
|
|
auto shotquit = true;
|
|
for (int i = 1; i < Argc; ++i)
|
|
{
|
|
|
|
std::string token{Argv[i]};
|
|
|
|
// a switch may be written with one dash or two: both spellings are common on the command
|
|
// line and neither says anything different, so the second dash is simply dropped here
|
|
if (token.size() > 2 && token.compare(0, 2, "--") == 0)
|
|
{
|
|
token.erase(0, 1);
|
|
}
|
|
|
|
if (token == "-s")
|
|
{
|
|
if (i + 1 < Argc)
|
|
{
|
|
Global.SceneryFile = ToLower(Argv[++i]);
|
|
scenerywasasked = true;
|
|
}
|
|
}
|
|
else if (token == "-v")
|
|
{
|
|
if (i + 1 < Argc)
|
|
{
|
|
Global.local_start_vehicle = ToLower(Argv[++i]);
|
|
}
|
|
}
|
|
else if (token == "-editor")
|
|
{
|
|
Global.editor_startup = true;
|
|
}
|
|
else if (token == "-terrain")
|
|
{
|
|
if (i + 1 < Argc)
|
|
{
|
|
// cooked heightfield produced by tools/terraincook; may be repeated
|
|
Global.terrain_heightfields.push_back(Argv[++i]);
|
|
}
|
|
}
|
|
else if (token == "-shot")
|
|
{
|
|
// unattended screenshot: "x,y,z[,yaw[,pitch]]", repeatable for a sequence
|
|
if (i + 1 < Argc)
|
|
{
|
|
shot_runner::shot shot;
|
|
if (shot_runner::parse(Argv[++i], shot))
|
|
{
|
|
shots.push_back(shot);
|
|
}
|
|
else
|
|
{
|
|
std::cout << "bad -shot viewpoint: " << Argv[i] << std::endl;
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
else if (token == "-shotsettle")
|
|
{
|
|
// seconds to wait at each viewpoint before saving, so terrain tiles and
|
|
// models have time to stream in
|
|
if (i + 1 < Argc)
|
|
{
|
|
shotsettle = std::atof(Argv[++i]);
|
|
}
|
|
}
|
|
else if (token == "-shotstay")
|
|
{
|
|
// leave the simulation running once the sequence is done
|
|
shotquit = false;
|
|
}
|
|
else if (token == "-loadprofile")
|
|
{
|
|
// scenery load profiling: where the load time goes, what the scenery holds,
|
|
// what it costs in memory. writes to the log and to loadprofile.txt
|
|
loadprofile::enable(true);
|
|
}
|
|
else if (token == "-cooklogic")
|
|
{
|
|
// cook the scenery's LOGIC container once it has loaded, then read it back and
|
|
// check it against the events in memory
|
|
Global.cook_logic = true;
|
|
}
|
|
else if (token == "-nobaketerrain")
|
|
{
|
|
// load the scenery's terrain triangles as written, without baking or using a
|
|
// heightfield baked beside it. the way to see the original for comparison
|
|
Global.bake_terrain = false;
|
|
}
|
|
else if (token == "-verifylogic")
|
|
{
|
|
// read the LOGIC container that is already on disk and check it against the
|
|
// scenery, writing nothing
|
|
Global.verify_logic = true;
|
|
}
|
|
else
|
|
{
|
|
std::cout << "usage: " << std::string(Argv[0]) << " [-s sceneryfilepath]"
|
|
<< " [-v vehiclename]"
|
|
<< " [-editor]"
|
|
<< " [-terrain terrain.ehf]"
|
|
<< " [-shot x,y,z[,yaw[,pitch]]]"
|
|
<< " [-shotsettle seconds] [-shotstay]"
|
|
<< " [-loadprofile]"
|
|
<< " [-cooklogic]"
|
|
<< " [-verifylogic]"
|
|
<< " [-nobaketerrain]"
|
|
<< " (every switch also takes a double dash: --editor)" << std::endl;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (false == shots.empty())
|
|
{
|
|
Shots.configure(shots, shotsettle, shotquit);
|
|
}
|
|
|
|
if (Global.editor_startup && false == scenerywasasked)
|
|
{
|
|
// the editor opens on empty ground unless a scenery was actually asked for. whatever the ini
|
|
// happens to name is the last thing that was driven, which has nothing to do with editing; an
|
|
// empty scenery still gives the region, the lighting and the clock, just nothing standing on it
|
|
Global.SceneryFile = "pusta.scn";
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int eu07_application::init_locale()
|
|
{
|
|
|
|
Translations.init();
|
|
|
|
return 0;
|
|
}
|
|
|
|
int eu07_application::init_glfw()
|
|
{
|
|
{
|
|
int glfw_major, glfw_minor, glfw_rev;
|
|
glfwGetVersion(&glfw_major, &glfw_minor, &glfw_rev);
|
|
m_glfwversion = glfw_major * 10000 + glfw_minor * 100 + glfw_minor;
|
|
}
|
|
|
|
#ifdef GLFW_ANGLE_PLATFORM_TYPE
|
|
if (m_glfwversion >= 30400)
|
|
{
|
|
int platform = GLFW_ANGLE_PLATFORM_TYPE_NONE;
|
|
if (Global.gfx_angleplatform == "opengl")
|
|
platform = GLFW_ANGLE_PLATFORM_TYPE_OPENGL;
|
|
else if (Global.gfx_angleplatform == "opengles")
|
|
platform = GLFW_ANGLE_PLATFORM_TYPE_OPENGLES;
|
|
else if (Global.gfx_angleplatform == "d3d9")
|
|
platform = GLFW_ANGLE_PLATFORM_TYPE_D3D9;
|
|
else if (Global.gfx_angleplatform == "d3d11")
|
|
platform = GLFW_ANGLE_PLATFORM_TYPE_D3D11;
|
|
else if (Global.gfx_angleplatform == "vulkan")
|
|
platform = GLFW_ANGLE_PLATFORM_TYPE_VULKAN;
|
|
else if (Global.gfx_angleplatform == "metal")
|
|
platform = GLFW_ANGLE_PLATFORM_TYPE_METAL;
|
|
|
|
glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE, platform);
|
|
}
|
|
#endif
|
|
|
|
if (glfwInit() == GLFW_FALSE)
|
|
{
|
|
ErrorLog("Bad init: failed to initialize glfw");
|
|
return -1;
|
|
}
|
|
|
|
{
|
|
int monitor_count;
|
|
GLFWmonitor **monitors = glfwGetMonitors(&monitor_count);
|
|
|
|
WriteLog("available monitors:");
|
|
for (size_t i = 0; i < monitor_count; i++)
|
|
{
|
|
WriteLog(describe_monitor(monitors[i]));
|
|
}
|
|
}
|
|
|
|
auto *monitor{find_monitor(Global.fullscreen_monitor)};
|
|
if (!monitor)
|
|
monitor = glfwGetPrimaryMonitor();
|
|
|
|
glfwWindowHint(GLFW_AUTO_ICONIFY, GLFW_FALSE);
|
|
if ((Global.gfx_skippipeline || Global.LegacyRenderer) && Global.iMultisampling > 0)
|
|
{
|
|
glfwWindowHint(GLFW_SAMPLES, 1 << Global.iMultisampling);
|
|
}
|
|
|
|
crashreport_add_info("gfxrenderer", Global.GfxRenderer);
|
|
|
|
if (!needs_ogl())
|
|
{
|
|
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
|
|
}
|
|
else
|
|
{
|
|
if (!Global.LegacyRenderer)
|
|
{
|
|
Global.bUseVBO = true;
|
|
// activate core profile for opengl 3.3 renderer
|
|
if (!Global.gfx_usegles)
|
|
{
|
|
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
|
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
|
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
|
}
|
|
else
|
|
{
|
|
#ifdef GLFW_CONTEXT_CREATION_API
|
|
if (m_glfwversion >= 30200)
|
|
glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API);
|
|
#endif
|
|
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
|
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Global.gfx_usegles)
|
|
{
|
|
ErrorLog("legacy renderer not supported in gles mode");
|
|
return -1;
|
|
}
|
|
Global.gfx_shadergamma = false;
|
|
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_ANY_PROFILE);
|
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
|
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
|
|
}
|
|
|
|
if (Global.gfx_gldebug)
|
|
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
|
|
}
|
|
|
|
glfwWindowHint(GLFW_SRGB_CAPABLE, !Global.gfx_shadergamma);
|
|
|
|
if (Global.fullscreen_windowed)
|
|
{
|
|
auto const mode = glfwGetVideoMode(monitor);
|
|
Global.window_size.x = mode->width;
|
|
Global.window_size.y = mode->height;
|
|
Global.bFullScreen = true;
|
|
}
|
|
|
|
auto *mainwindow = window(-1, true, Global.window_size.x, Global.window_size.y, Global.bFullScreen ? monitor : nullptr, true, false);
|
|
|
|
if (mainwindow == nullptr)
|
|
{
|
|
ErrorLog("Bad init: failed to create glfw window");
|
|
return -1;
|
|
}
|
|
|
|
glfwMakeContextCurrent(mainwindow);
|
|
glfwSwapInterval(Global.VSync ? 1 : 0); // vsync
|
|
|
|
{
|
|
int width, height;
|
|
|
|
glfwGetFramebufferSize(mainwindow, &width, &height);
|
|
framebuffer_resize_callback(mainwindow, width, height);
|
|
|
|
glfwGetWindowSize(mainwindow, &width, &height);
|
|
window_resize_callback(mainwindow, width, height);
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
// setup wrapper for base glfw window proc, to handle copydata messages
|
|
Hwnd = glfwGetWin32Window(mainwindow);
|
|
BaseWindowProc = (WNDPROC)::SetWindowLongPtr(Hwnd, GWLP_WNDPROC, (LONG_PTR)WndProc);
|
|
// switch off the topmost flag
|
|
::SetWindowPos(Hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
|
|
#endif
|
|
|
|
return 0;
|
|
}
|
|
|
|
void eu07_application::init_callbacks()
|
|
{
|
|
|
|
auto *window{m_windows.front()};
|
|
glfwSetWindowSizeCallback(window, window_resize_callback);
|
|
glfwSetFramebufferSizeCallback(window, framebuffer_resize_callback);
|
|
glfwSetCursorPosCallback(window, cursor_pos_callback);
|
|
glfwSetMouseButtonCallback(window, mouse_button_callback);
|
|
glfwSetKeyCallback(window, key_callback);
|
|
glfwSetScrollCallback(window, scroll_callback);
|
|
glfwSetCharCallback(window, char_callback);
|
|
glfwSetWindowFocusCallback(window, focus_callback);
|
|
}
|
|
|
|
int eu07_application::init_ogl()
|
|
{
|
|
if (!Global.gfx_usegles)
|
|
{
|
|
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
|
|
{
|
|
ErrorLog("Bad init: failed to initialize glad");
|
|
return -1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!gladLoadGLES2Loader((GLADloadproc)glfwGetProcAddress))
|
|
{
|
|
ErrorLog("Bad init: failed to initialize glad");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int eu07_application::init_ui()
|
|
{
|
|
if (false == ui_layer::init(m_windows.front()))
|
|
{
|
|
return -1;
|
|
}
|
|
init_callbacks();
|
|
return 0;
|
|
}
|
|
|
|
int eu07_application::init_gfx()
|
|
{
|
|
|
|
if (Global.GfxRenderer == "default")
|
|
{
|
|
// default render path
|
|
GfxRenderer = gfx_renderer_factory::get_instance()->create("modern");
|
|
}
|
|
else if (Global.GfxRenderer == "experimental")
|
|
{
|
|
GfxRenderer = gfx_renderer_factory::get_instance()->create(Global.GfxRenderer);
|
|
}
|
|
else
|
|
{
|
|
// legacy render path
|
|
GfxRenderer = gfx_renderer_factory::get_instance()->create("legacy");
|
|
Global.DisabledLogTypes |= static_cast<unsigned int>(logtype::material);
|
|
}
|
|
|
|
if (!GfxRenderer)
|
|
{
|
|
ErrorLog("no renderer found!");
|
|
return -1;
|
|
}
|
|
|
|
if (false == GfxRenderer->Init(m_windows.front()))
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
for (const global_settings::extraviewport_config &conf : Global.extra_viewports)
|
|
if (!GfxRenderer->AddViewport(conf))
|
|
return -1;
|
|
|
|
if (!Global.headtrack_conf.joy.empty())
|
|
m_headtrack.emplace();
|
|
|
|
return 0;
|
|
}
|
|
|
|
int eu07_application::init_audio()
|
|
{
|
|
|
|
if (Global.bSoundEnabled)
|
|
{
|
|
Global.bSoundEnabled &= audio::renderer.init();
|
|
}
|
|
// NOTE: lack of audio isn't deemed a failure serious enough to throw in the towel
|
|
return 0;
|
|
}
|
|
|
|
int eu07_application::init_data()
|
|
{
|
|
|
|
// HACK: grab content of the first {} block in load_unit_weights using temporary parser, then parse it normally. on any error our weight list will be empty string
|
|
auto loadweights{cParser(cParser("data/load_weights.txt", cParser::buffer_FILE).getToken<std::string>(true, "{}"), cParser::buffer_TEXT)};
|
|
while (true == loadweights.getTokens(2))
|
|
{
|
|
std::pair<std::string, float> weightpair;
|
|
loadweights >> weightpair.first >> weightpair.second;
|
|
weightpair.first.erase(weightpair.first.end() - 1); // trim trailing ':' from the key
|
|
simulation::Weights.emplace(weightpair.first, weightpair.second);
|
|
}
|
|
cParser override_parser("data/sound_overrides.txt", cParser::buffer_FILE);
|
|
deserialize_map(simulation::Sound_overrides, override_parser);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int eu07_application::init_modes()
|
|
{
|
|
Global.local_random_engine.seed(std::random_device{}());
|
|
|
|
if ((!Global.network_servers.empty() || Global.network_client) && Global.SceneryFile.empty())
|
|
{
|
|
ErrorLog("launcher mode is currently not supported in network mode");
|
|
return -1;
|
|
}
|
|
|
|
// activate the default mode
|
|
if (Global.SceneryFile.empty())
|
|
push_mode(mode::launcher);
|
|
else
|
|
push_mode(mode::scenarioloader);
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool eu07_application::init_network()
|
|
{
|
|
if (!Global.network_servers.empty() || Global.network_client)
|
|
{
|
|
// create network manager
|
|
m_network.emplace();
|
|
}
|
|
|
|
for (auto const &pair : Global.network_servers)
|
|
{
|
|
// create all servers
|
|
m_network->create_server(pair.first, pair.second);
|
|
}
|
|
|
|
if (Global.network_client)
|
|
{
|
|
// create client
|
|
m_network->connect(Global.network_client->first, Global.network_client->second);
|
|
}
|
|
else
|
|
{
|
|
// we're simulation master
|
|
// TODO: sort out this timezone mess
|
|
std::time_t utc_now = std::time(nullptr);
|
|
|
|
tm tm_local, tm_utc;
|
|
tm *tmp = std::localtime(&utc_now);
|
|
memcpy(&tm_local, tmp, sizeof(tm));
|
|
tmp = std::gmtime(&utc_now);
|
|
memcpy(&tm_utc, tmp, sizeof(tm));
|
|
|
|
int64_t offset = tm_local.tm_hour * 3600 + tm_local.tm_min * 60 + tm_local.tm_sec - (tm_utc.tm_hour * 3600 + tm_utc.tm_min * 60 + tm_utc.tm_sec);
|
|
|
|
Global.starting_timestamp = utc_now + offset;
|
|
Global.ready_to_load = true;
|
|
}
|
|
|
|
return true;
|
|
}
|