527 lines
14 KiB
C++
527 lines
14 KiB
C++
|
#include "PowderToySDL.h"
|
||
|
#include "Format.h"
|
||
|
#include "X86KillDenormals.h"
|
||
|
#include "prefs/GlobalPrefs.h"
|
||
|
#include "client/Client.h"
|
||
|
#include "client/GameSave.h"
|
||
|
#include "client/SaveFile.h"
|
||
|
#include "client/SaveInfo.h"
|
||
|
#include "client/http/RequestManager.h"
|
||
|
#include "common/Platform.h"
|
||
|
#include "graphics/Graphics.h"
|
||
|
#include "simulation/SaveRenderer.h"
|
||
|
#include "common/tpt-rand.h"
|
||
|
#include "gui/game/Favorite.h"
|
||
|
#include "gui/Style.h"
|
||
|
#include "gui/game/GameController.h"
|
||
|
#include "gui/game/GameView.h"
|
||
|
#include "gui/dialogues/ConfirmPrompt.h"
|
||
|
#include "gui/dialogues/ErrorMessage.h"
|
||
|
#include "gui/interface/Engine.h"
|
||
|
#include "Config.h"
|
||
|
#include "SimulationConfig.h"
|
||
|
#include <optional>
|
||
|
#include <climits>
|
||
|
#include <iostream>
|
||
|
#include <csignal>
|
||
|
#include <SDL.h>
|
||
|
|
||
|
void LoadWindowPosition()
|
||
|
{
|
||
|
if (Client::Ref().IsFirstRun())
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
auto &prefs = GlobalPrefs::Ref();
|
||
|
int savedWindowX = prefs.Get("WindowX", INT_MAX);
|
||
|
int savedWindowY = prefs.Get("WindowY", INT_MAX);
|
||
|
|
||
|
int borderTop, borderLeft;
|
||
|
SDL_GetWindowBordersSize(sdl_window, &borderTop, &borderLeft, nullptr, nullptr);
|
||
|
// Sometimes (Windows), the border size may not be reported for 200+ frames
|
||
|
// So just have a default of 5 to ensure the window doesn't get stuck where it can't be moved
|
||
|
if (borderTop == 0)
|
||
|
borderTop = 5;
|
||
|
|
||
|
int numDisplays = SDL_GetNumVideoDisplays();
|
||
|
SDL_Rect displayBounds;
|
||
|
bool ok = false;
|
||
|
for (int i = 0; i < numDisplays; i++)
|
||
|
{
|
||
|
SDL_GetDisplayBounds(i, &displayBounds);
|
||
|
if (savedWindowX + borderTop > displayBounds.x && savedWindowY + borderLeft > displayBounds.y &&
|
||
|
savedWindowX + borderTop < displayBounds.x + displayBounds.w &&
|
||
|
savedWindowY + borderLeft < displayBounds.y + displayBounds.h)
|
||
|
{
|
||
|
ok = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (ok)
|
||
|
SDL_SetWindowPosition(sdl_window, savedWindowX + borderLeft, savedWindowY + borderTop);
|
||
|
}
|
||
|
|
||
|
void SaveWindowPosition()
|
||
|
{
|
||
|
int x, y;
|
||
|
SDL_GetWindowPosition(sdl_window, &x, &y);
|
||
|
|
||
|
int borderTop, borderLeft;
|
||
|
SDL_GetWindowBordersSize(sdl_window, &borderTop, &borderLeft, nullptr, nullptr);
|
||
|
|
||
|
auto &prefs = GlobalPrefs::Ref();
|
||
|
prefs.Set("WindowX", x - borderLeft);
|
||
|
prefs.Set("WindowY", y - borderTop);
|
||
|
}
|
||
|
|
||
|
void LargeScreenDialog()
|
||
|
{
|
||
|
StringBuilder message;
|
||
|
message << "Switching to " << scale << "x size mode since your screen was determined to be large enough: ";
|
||
|
message << desktopWidth << "x" << desktopHeight << " detected, " << WINDOWW*scale << "x" << WINDOWH*scale << " required";
|
||
|
message << "\nTo undo this, hit Cancel. You can change this in settings at any time.";
|
||
|
if (!ConfirmPrompt::Blocking("Large screen detected", message.Build()))
|
||
|
{
|
||
|
GlobalPrefs::Ref().Set("Scale", 1);
|
||
|
ui::Engine::Ref().SetScale(1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void TickClient()
|
||
|
{
|
||
|
Client::Ref().Tick();
|
||
|
}
|
||
|
|
||
|
void BlueScreen(String detailMessage)
|
||
|
{
|
||
|
auto &engine = ui::Engine::Ref();
|
||
|
engine.g->fillrect(0, 0, engine.GetWidth(), engine.GetHeight(), 17, 114, 169, 210);
|
||
|
|
||
|
String errorTitle = "ERROR";
|
||
|
String errorDetails = "Details: " + detailMessage;
|
||
|
String errorHelp = String("An unrecoverable fault has occurred, please report the error by visiting the website below\n") + SCHEME + SERVER;
|
||
|
int currentY = 0, width, height;
|
||
|
int errorWidth = 0;
|
||
|
Graphics::textsize(errorHelp, errorWidth, height);
|
||
|
|
||
|
engine.g->drawtext((engine.GetWidth()/2)-(errorWidth/2), ((engine.GetHeight()/2)-100) + currentY, errorTitle.c_str(), 255, 255, 255, 255);
|
||
|
Graphics::textsize(errorTitle, width, height);
|
||
|
currentY += height + 4;
|
||
|
|
||
|
engine.g->drawtext((engine.GetWidth()/2)-(errorWidth/2), ((engine.GetHeight()/2)-100) + currentY, errorDetails.c_str(), 255, 255, 255, 255);
|
||
|
Graphics::textsize(errorTitle, width, height);
|
||
|
currentY += height + 4;
|
||
|
|
||
|
engine.g->drawtext((engine.GetWidth()/2)-(errorWidth/2), ((engine.GetHeight()/2)-100) + currentY, errorHelp.c_str(), 255, 255, 255, 255);
|
||
|
Graphics::textsize(errorTitle, width, height);
|
||
|
currentY += height + 4;
|
||
|
|
||
|
//Death loop
|
||
|
SDL_Event event;
|
||
|
while(true)
|
||
|
{
|
||
|
while (SDL_PollEvent(&event))
|
||
|
if(event.type == SDL_QUIT)
|
||
|
exit(-1);
|
||
|
blit(engine.g->vid);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void SigHandler(int signal)
|
||
|
{
|
||
|
switch(signal){
|
||
|
case SIGSEGV:
|
||
|
BlueScreen("Memory read/write error");
|
||
|
break;
|
||
|
case SIGFPE:
|
||
|
BlueScreen("Floating point exception");
|
||
|
break;
|
||
|
case SIGILL:
|
||
|
BlueScreen("Program execution exception");
|
||
|
break;
|
||
|
case SIGABRT:
|
||
|
BlueScreen("Unexpected program abort");
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
constexpr int SCALE_MAXIMUM = 10;
|
||
|
constexpr int SCALE_MARGIN = 30;
|
||
|
|
||
|
int GuessBestScale()
|
||
|
{
|
||
|
const int widthNoMargin = desktopWidth - SCALE_MARGIN;
|
||
|
const int widthGuess = widthNoMargin / WINDOWW;
|
||
|
|
||
|
const int heightNoMargin = desktopHeight - SCALE_MARGIN;
|
||
|
const int heightGuess = heightNoMargin / WINDOWH;
|
||
|
|
||
|
int guess = std::min(widthGuess, heightGuess);
|
||
|
if(guess < 1 || guess > SCALE_MAXIMUM)
|
||
|
guess = 1;
|
||
|
|
||
|
return guess;
|
||
|
}
|
||
|
|
||
|
struct ExplicitSingletons
|
||
|
{
|
||
|
// These need to be listed in the order they are populated in main.
|
||
|
std::unique_ptr<GlobalPrefs> globalPrefs;
|
||
|
http::RequestManagerPtr requestManager;
|
||
|
std::unique_ptr<Client> client;
|
||
|
std::unique_ptr<SaveRenderer> saveRenderer;
|
||
|
std::unique_ptr<RNG> rng;
|
||
|
std::unique_ptr<Favorite> favorite;
|
||
|
std::unique_ptr<ui::Engine> engine;
|
||
|
std::unique_ptr<GameController> gameController;
|
||
|
};
|
||
|
static std::unique_ptr<ExplicitSingletons> explicitSingletons;
|
||
|
|
||
|
int main(int argc, char * argv[])
|
||
|
{
|
||
|
Platform::SetupCrt();
|
||
|
atexit([]() {
|
||
|
ui::Engine::Ref().CloseWindow();
|
||
|
explicitSingletons.reset();
|
||
|
if (SDL_GetWindowFlags(sdl_window) & SDL_WINDOW_OPENGL)
|
||
|
{
|
||
|
// * nvidia-460 egl registers callbacks with x11 that end up being called
|
||
|
// after egl is unloaded unless we grab it here and release it after
|
||
|
// sdl closes the display. this is an nvidia driver weirdness but
|
||
|
// technically an sdl bug. glfw has this fixed:
|
||
|
// https://github.com/glfw/glfw/commit/9e6c0c747be838d1f3dc38c2924a47a42416c081
|
||
|
SDL_GL_LoadLibrary(NULL);
|
||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||
|
SDL_GL_UnloadLibrary();
|
||
|
}
|
||
|
SDL_Quit();
|
||
|
});
|
||
|
explicitSingletons = std::make_unique<ExplicitSingletons>();
|
||
|
|
||
|
|
||
|
// https://bugzilla.libsdl.org/show_bug.cgi?id=3796
|
||
|
if (SDL_Init(0) < 0)
|
||
|
{
|
||
|
fprintf(stderr, "Initializing SDL: %s\n", SDL_GetError());
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
Platform::originalCwd = Platform::GetCwd();
|
||
|
|
||
|
using Argument = std::optional<ByteString>;
|
||
|
std::map<ByteString, Argument> arguments;
|
||
|
|
||
|
for (auto i = 1; i < argc; ++i)
|
||
|
{
|
||
|
auto str = ByteString(argv[i]);
|
||
|
if (str.BeginsWith("file://"))
|
||
|
{
|
||
|
arguments.insert({ "open", format::URLDecode(str.substr(7 /* length of the "file://" prefix */)) });
|
||
|
}
|
||
|
else if (str.BeginsWith("ptsave:"))
|
||
|
{
|
||
|
arguments.insert({ "ptsave", str });
|
||
|
}
|
||
|
else if (auto split = str.SplitBy(':'))
|
||
|
{
|
||
|
arguments.insert({ split.Before(), split.After() });
|
||
|
}
|
||
|
else if (auto split = str.SplitBy('='))
|
||
|
{
|
||
|
arguments.insert({ split.Before(), split.After() });
|
||
|
}
|
||
|
else if (str == "open" || str == "ptsave" || str == "ddir")
|
||
|
{
|
||
|
if (i + 1 < argc)
|
||
|
{
|
||
|
arguments.insert({ str, argv[i + 1] });
|
||
|
i += 1;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
std::cerr << "no value provided for command line parameter " << str << std::endl;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
arguments.insert({ str, "" }); // so .has_value() is true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
auto ddirArg = arguments["ddir"];
|
||
|
if (ddirArg.has_value())
|
||
|
{
|
||
|
if (Platform::ChangeDir(ddirArg.value()))
|
||
|
Platform::sharedCwd = Platform::GetCwd();
|
||
|
else
|
||
|
perror("failed to chdir to requested ddir");
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
char *ddir = SDL_GetPrefPath(NULL, APPDATA);
|
||
|
if (!Platform::FileExists("powder.pref"))
|
||
|
{
|
||
|
if (ddir)
|
||
|
{
|
||
|
if (!Platform::ChangeDir(ddir))
|
||
|
{
|
||
|
perror("failed to chdir to default ddir");
|
||
|
SDL_free(ddir);
|
||
|
ddir = nullptr;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (ddir)
|
||
|
{
|
||
|
Platform::sharedCwd = ddir;
|
||
|
SDL_free(ddir);
|
||
|
}
|
||
|
}
|
||
|
// We're now in the correct directory, time to get prefs.
|
||
|
explicitSingletons->globalPrefs = std::make_unique<GlobalPrefs>();
|
||
|
|
||
|
auto &prefs = GlobalPrefs::Ref();
|
||
|
scale = prefs.Get("Scale", 1);
|
||
|
resizable = prefs.Get("Resizable", false);
|
||
|
fullscreen = prefs.Get("Fullscreen", false);
|
||
|
altFullscreen = prefs.Get("AltFullscreen", false);
|
||
|
forceIntegerScaling = prefs.Get("ForceIntegerScaling", true);
|
||
|
momentumScroll = prefs.Get("MomentumScroll", true);
|
||
|
showAvatars = prefs.Get("ShowAvatars", true);
|
||
|
|
||
|
auto true_string = [](ByteString str) {
|
||
|
str = str.ToLower();
|
||
|
return str == "true" ||
|
||
|
str == "t" ||
|
||
|
str == "on" ||
|
||
|
str == "yes" ||
|
||
|
str == "y" ||
|
||
|
str == "1" ||
|
||
|
str == ""; // standalone "redirect" or "disable-bluescreen" or similar arguments
|
||
|
};
|
||
|
auto true_arg = [&true_string](Argument arg) {
|
||
|
return arg.has_value() && true_string(arg.value());
|
||
|
};
|
||
|
|
||
|
auto kioskArg = arguments["kiosk"];
|
||
|
if (kioskArg.has_value())
|
||
|
{
|
||
|
fullscreen = true_string(kioskArg.value());
|
||
|
prefs.Set("Fullscreen", fullscreen);
|
||
|
}
|
||
|
|
||
|
if (true_arg(arguments["redirect"]))
|
||
|
{
|
||
|
FILE *new_stdout = freopen("stdout.log", "w", stdout);
|
||
|
FILE *new_stderr = freopen("stderr.log", "w", stderr);
|
||
|
if (!new_stdout || !new_stderr)
|
||
|
{
|
||
|
exit(42);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
auto scaleArg = arguments["scale"];
|
||
|
if (scaleArg.has_value())
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
scale = scaleArg.value().ToNumber<int>();
|
||
|
prefs.Set("Scale", scale);
|
||
|
}
|
||
|
catch (const std::runtime_error &e)
|
||
|
{
|
||
|
std::cerr << "failed to set scale: " << e.what() << std::endl;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
auto clientConfig = [&prefs](Argument arg, ByteString name, ByteString defaultValue) {
|
||
|
ByteString value;
|
||
|
if (arg.has_value())
|
||
|
{
|
||
|
value = arg.value();
|
||
|
if (value == "")
|
||
|
{
|
||
|
value = defaultValue;
|
||
|
}
|
||
|
prefs.Set(name, value);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
value = prefs.Get(name, defaultValue);
|
||
|
}
|
||
|
return value;
|
||
|
};
|
||
|
ByteString proxyString = clientConfig(arguments["proxy"], "Proxy", "");
|
||
|
ByteString cafileString = clientConfig(arguments["cafile"], "CAFile", "");
|
||
|
ByteString capathString = clientConfig(arguments["capath"], "CAPath", "");
|
||
|
bool disableNetwork = true_arg(arguments["disable-network"]);
|
||
|
explicitSingletons->requestManager = http::RequestManager::Create(proxyString, cafileString, capathString, disableNetwork);
|
||
|
|
||
|
explicitSingletons->client = std::make_unique<Client>();
|
||
|
Client::Ref().Initialize();
|
||
|
|
||
|
explicitSingletons->saveRenderer = std::make_unique<SaveRenderer>();
|
||
|
explicitSingletons->rng = std::make_unique<RNG>();
|
||
|
explicitSingletons->favorite = std::make_unique<Favorite>();
|
||
|
explicitSingletons->engine = std::make_unique<ui::Engine>();
|
||
|
|
||
|
// TODO: maybe bind the maximum allowed scale to screen size somehow
|
||
|
if(scale < 1 || scale > SCALE_MAXIMUM)
|
||
|
scale = 1;
|
||
|
|
||
|
SDLOpen();
|
||
|
|
||
|
if (Client::Ref().IsFirstRun())
|
||
|
{
|
||
|
scale = GuessBestScale();
|
||
|
if (scale > 1)
|
||
|
{
|
||
|
prefs.Set("Scale", scale);
|
||
|
SDL_SetWindowSize(sdl_window, WINDOWW * scale, WINDOWH * scale);
|
||
|
showLargeScreenDialog = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
StopTextInput();
|
||
|
|
||
|
auto &engine = ui::Engine::Ref();
|
||
|
engine.g = new Graphics();
|
||
|
engine.Scale = scale;
|
||
|
engine.SetResizable(resizable);
|
||
|
engine.Fullscreen = fullscreen;
|
||
|
engine.SetAltFullscreen(altFullscreen);
|
||
|
engine.SetForceIntegerScaling(forceIntegerScaling);
|
||
|
engine.MomentumScroll = momentumScroll;
|
||
|
engine.ShowAvatars = showAvatars;
|
||
|
engine.SetMaxSize(desktopWidth, desktopHeight);
|
||
|
engine.Begin(WINDOWW, WINDOWH);
|
||
|
engine.SetFastQuit(prefs.Get("FastQuit", true));
|
||
|
|
||
|
bool enableBluescreen = !DEBUG && !true_arg(arguments["disable-bluescreen"]);
|
||
|
if (enableBluescreen)
|
||
|
{
|
||
|
//Get ready to catch any dodgy errors
|
||
|
signal(SIGSEGV, SigHandler);
|
||
|
signal(SIGFPE, SigHandler);
|
||
|
signal(SIGILL, SigHandler);
|
||
|
signal(SIGABRT, SigHandler);
|
||
|
}
|
||
|
|
||
|
if constexpr (X86)
|
||
|
{
|
||
|
X86KillDenormals();
|
||
|
}
|
||
|
|
||
|
auto wrapWithBluescreen = [&]() {
|
||
|
explicitSingletons->gameController = std::make_unique<GameController>();
|
||
|
auto *gameController = explicitSingletons->gameController.get();
|
||
|
engine.ShowWindow(gameController->GetView());
|
||
|
|
||
|
auto openArg = arguments["open"];
|
||
|
if (openArg.has_value())
|
||
|
{
|
||
|
if constexpr (DEBUG)
|
||
|
{
|
||
|
std::cout << "Loading " << openArg.value() << std::endl;
|
||
|
}
|
||
|
if (Platform::FileExists(openArg.value()))
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
std::vector<char> gameSaveData;
|
||
|
if (!Platform::ReadFile(gameSaveData, openArg.value()))
|
||
|
{
|
||
|
new ErrorMessage("Error", "Could not read file");
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
SaveFile * newFile = new SaveFile(openArg.value());
|
||
|
GameSave * newSave = new GameSave(std::move(gameSaveData));
|
||
|
newFile->SetGameSave(newSave);
|
||
|
gameController->LoadSaveFile(newFile);
|
||
|
delete newFile;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
catch (std::exception & e)
|
||
|
{
|
||
|
new ErrorMessage("Error", "Could not open save file:\n" + ByteString(e.what()).FromUtf8()) ;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
new ErrorMessage("Error", "Could not open file");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
auto ptsaveArg = arguments["ptsave"];
|
||
|
if (ptsaveArg.has_value())
|
||
|
{
|
||
|
engine.g->Clear();
|
||
|
engine.g->fillrect((engine.GetWidth()/2)-101, (engine.GetHeight()/2)-26, 202, 52, 0, 0, 0, 210);
|
||
|
engine.g->drawrect((engine.GetWidth()/2)-100, (engine.GetHeight()/2)-25, 200, 50, 255, 255, 255, 180);
|
||
|
engine.g->drawtext((engine.GetWidth()/2)-(Graphics::textwidth("Loading save...")/2), (engine.GetHeight()/2)-5, "Loading save...", style::Colour::InformationTitle.Red, style::Colour::InformationTitle.Green, style::Colour::InformationTitle.Blue, 255);
|
||
|
|
||
|
blit(engine.g->vid);
|
||
|
try
|
||
|
{
|
||
|
ByteString saveIdPart;
|
||
|
if (ByteString::Split split = ptsaveArg.value().SplitBy(':'))
|
||
|
{
|
||
|
if (split.Before() != "ptsave")
|
||
|
throw std::runtime_error("Not a ptsave link");
|
||
|
saveIdPart = split.After().SplitBy('#').Before();
|
||
|
}
|
||
|
else
|
||
|
throw std::runtime_error("Invalid save link");
|
||
|
|
||
|
if (!saveIdPart.size())
|
||
|
throw std::runtime_error("No Save ID");
|
||
|
if constexpr (DEBUG)
|
||
|
{
|
||
|
std::cout << "Got Ptsave: id: " << saveIdPart << std::endl;
|
||
|
}
|
||
|
int saveId = saveIdPart.ToNumber<int>();
|
||
|
|
||
|
SaveInfo * newSave = Client::Ref().GetSave(saveId, 0);
|
||
|
if (!newSave)
|
||
|
throw std::runtime_error("Could not load save info");
|
||
|
auto saveData = Client::Ref().GetSaveData(saveId, 0);
|
||
|
if (!saveData.size())
|
||
|
throw std::runtime_error(("Could not load save\n" + Client::Ref().GetLastError()).ToUtf8());
|
||
|
GameSave * newGameSave = new GameSave(std::move(saveData));
|
||
|
newSave->SetGameSave(newGameSave);
|
||
|
|
||
|
gameController->LoadSave(newSave);
|
||
|
delete newSave;
|
||
|
}
|
||
|
catch (std::exception & e)
|
||
|
{
|
||
|
new ErrorMessage("Error", ByteString(e.what()).FromUtf8());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
EngineProcess();
|
||
|
SaveWindowPosition();
|
||
|
};
|
||
|
|
||
|
if (enableBluescreen)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
wrapWithBluescreen();
|
||
|
}
|
||
|
catch (const std::exception &e)
|
||
|
{
|
||
|
BlueScreen(ByteString(e.what()).FromUtf8());
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
wrapWithBluescreen();
|
||
|
}
|
||
|
return 0;
|
||
|
}
|