#include "framework/framework.h" #include "framework/ThreadPool/ThreadPool.h" #include "framework/apocresources/cursor.h" #include "framework/configfile.h" #include "framework/data.h" #include "framework/event.h" #include "framework/filesystem.h" #include "framework/image.h" #include "framework/jukebox.h" #include "framework/logger_file.h" #include "framework/logger_sdldialog.h" #include "framework/options.h" #include "framework/renderer.h" #include "framework/renderer_interface.h" #include "framework/sound_interface.h" #include "framework/stagestack.h" #include "library/sp.h" #include "library/xorshift.h" #include #include #include #include #include #include #include #include #include #ifdef __APPLE__ // Used for NASTY chdir() app bundle hacks #include #endif // SDL_syswm includes windows.h on windows, which does all kinds of polluting // defines/namespace stuff, so try to avoid that #define WIN32_LEAN_AND_MEAN #define NOMINMAX #include // Windows isn't the only thing that pollutes stuff with #defines - X gets in on it too with 'None' #undef None // Use physfs to get prefs dir #include // Boost locale for setting the system locale #include using namespace OpenApoc; namespace OpenApoc { UString Framework::getDataDir() const { return Options::dataPathOption.get(); } UString Framework::getCDPath() const { return Options::cdPathOption.get(); } Framework *Framework::instance = nullptr; class FrameworkPrivate { private: friend class Framework; bool quitProgram; SDL_DisplayMode screenMode; SDL_Window *window; SDL_GLContext context; std::map> registeredRenderers; std::map> registeredSoundBackends; std::list> eventQueue; std::mutex eventQueueLock; StageStack ProgramStages; sp defaultSurface; // The display size may be scaled up to windowSize Vec2 displaySize; Vec2 windowSize; sp scaleSurface; up threadPool; std::atomic toolTipTimerId = 0; up toolTipTimerEvent; sp toolTipImage; Vec2 toolTipPosition; FrameworkPrivate() : quitProgram(false), window(nullptr), context(0), displaySize(0, 0), windowSize(0, 0) { int threadPoolSize = Options::threadPoolSizeOption.get(); if (threadPoolSize > 0) { LogInfo("Set thread pool size to {0}", threadPoolSize); } else if (std::thread::hardware_concurrency() != 0) { threadPoolSize = std::thread::hardware_concurrency(); LogInfo("Set thread pool size to reported HW concurrency of {0}", threadPoolSize); } else { threadPoolSize = 2; LogInfo("Failed to get HW concurrency, falling back to pool size {0}", threadPoolSize); } this->threadPool.reset(new ThreadPool(threadPoolSize)); } }; Framework::Framework(const UString programName, bool createWindow) : p(new FrameworkPrivate), programName(programName), createWindow(createWindow) { LogInfo("Starting framework"); if (this->instance) { LogError("Multiple Framework instances created"); } this->instance = this; #ifdef __APPLE__ { // FIXME: A hack to set the working directory to the Resources directory in the app bundle. char *basePath = SDL_GetBasePath(); // FIXME: How to check we're being run from the app bundle and not directly from the // terminal? On my testing (macos 10.15.1 19B88) it seems to have a "/" working directory, // which is unlikely in terminal use, so use that? if (fs::current_path() == "/") { LogWarning("Setting working directory to \"{0}\"", basePath); chdir(basePath); } else { LogWarning("Leaving default working directory \"{0}\"", fs::current_path().string()); } SDL_free(basePath); } #endif if (!PHYSFS_isInit()) { if (PHYSFS_init(programName.c_str()) == 0) { PHYSFS_ErrorCode error = PHYSFS_getLastErrorCode(); LogError("Failed to init code {0} PHYSFS: {1}", (int)error, PHYSFS_getErrorByCode(error)); } } #ifdef ANDROID SDL_SetHint(SDL_HINT_ANDROID_SEPARATE_MOUSE_AND_TOUCH, "1"); #endif // Initialize subsystems separately? if (SDL_Init(SDL_INIT_EVENTS | SDL_INIT_TIMER) < 0) { LogError("Cannot init SDL2"); LogError("SDL error: {0}", SDL_GetError()); p->quitProgram = true; return; } if (createWindow) { if (SDL_InitSubSystem(SDL_INIT_VIDEO) < 0) { LogError("Cannot init SDL_VIDEO - \"{0}\"", SDL_GetError()); p->quitProgram = true; return; } } LogInfo("Loading config\n"); p->quitProgram = false; UString settingsPath(PHYSFS_getPrefDir(PROGRAM_ORGANISATION, PROGRAM_NAME)); settingsPath += "/settings.cfg"; UString logPath(PHYSFS_getPrefDir(PROGRAM_ORGANISATION, PROGRAM_NAME)); std::ifstream portableFile("./portable.txt"); if (portableFile) { logPath = "."; } logPath += "/log.txt"; enableFileLogger(logPath.c_str()); Options::dumpOptionsToLog(); // This is always set, the default being an empty string (which correctly chooses 'system // language') UString desiredLanguageName; if (!Options::languageOption.get().empty()) { desiredLanguageName = Options::languageOption.get(); } LogInfo("Setting up locale \"{0}\"", desiredLanguageName); boost::locale::generator gen; std::vector resourcePaths; resourcePaths.push_back(Options::cdPathOption.get()); resourcePaths.push_back(Options::dataPathOption.get()); for (auto &path : resourcePaths) { auto langPath = path + "/languages"; LogInfo("Adding \"{0}\" to language path", langPath); gen.add_messages_path(langPath); } std::vector translationDomains = {"openapoc"}; for (auto &domain : translationDomains) { LogInfo("Adding \"{0}\" to translation domains", domain); gen.add_messages_domain(domain); } std::locale loc = gen(desiredLanguageName); std::locale::global(loc); auto localeName = std::use_facet(loc).name(); auto localeLang = std::use_facet(loc).language(); auto localeCountry = std::use_facet(loc).country(); auto localeVariant = std::use_facet(loc).variant(); auto localeEncoding = std::use_facet(loc).encoding(); auto isUTF8 = std::use_facet(loc).utf8(); LogInfo("Locale info: Name \"{0}\" language \"{1}\" country \"{2}\" variant \"{3}\" encoding " "\"{4}\" utf8:{5}", localeName.c_str(), localeLang.c_str(), localeCountry.c_str(), localeVariant.c_str(), localeEncoding.c_str(), isUTF8 ? "true" : "false"); this->language = localeLang; this->languageCountry = localeCountry; this->data.reset(Data::createData(resourcePaths)); auto testFile = this->data->fs.open("music"); if (!testFile) { LogError("Failed to open \"music\" from the CD - likely the cd couldn't be loaded or paths " "are incorrect if using an extracted CD image"); } auto testFile2 = this->data->fs.open("filedoesntexist"); if (testFile2) { LogError("Succeeded in opening \"FileDoesntExist\" - either you have the weirdest filename " "preferences or something is wrong"); } srand(static_cast(SDL_GetTicks())); if (createWindow) { displayInitialise(); enableSDLDialogLogger(p->window); } audioInitialise(!createWindow); } Framework::~Framework() { LogInfo("Destroying framework"); // Stop any audio first, as if you've got ongoing music/samples it could call back into the // framework for the threadpool/data read/all kinda of stuff it shouldn't do on a // half-destroyed framework audioShutdown(); LogInfo("Stopping threadpool"); p->threadPool.reset(); LogInfo("Clearing stages"); p->ProgramStages.clear(); LogInfo("Saving config"); if (config().getBool("Config.Save")) config().save(); LogInfo("Shutdown"); // Make sure we destroy the data implementation before the renderer to ensure any possibly // cached images are already destroyed this->data.reset(); if (createWindow) { displayShutdown(); } LogInfo("SDL shutdown"); PHYSFS_deinit(); SDL_Quit(); instance = nullptr; } Framework &Framework::getInstance() { if (!instance) { LogError("Framework::getInstance() called with no live Framework"); } return *instance; } Framework *Framework::tryGetInstance() { return instance; } void Framework::run(sp initialStage) { size_t frameCount = Options::frameLimit.get(); if (!createWindow) { LogError("Trying to run framework without window"); return; } size_t frame = 0; LogInfo("Program loop started"); auto target_frame_duration = std::chrono::duration(1000000 / Options::targetFPS.get()); p->ProgramStages.push(initialStage); this->renderer->setPalette(this->data->loadPalette("xcom3/ufodata/pal_06.dat")); auto expected_frame_time = std::chrono::steady_clock::now(); bool frame_time_limited_warning_shown = false; while (!p->quitProgram) { auto frame_time_now = std::chrono::steady_clock::now(); if (expected_frame_time > frame_time_now) { auto time_to_sleep = expected_frame_time - frame_time_now; auto time_to_sleep_us = std::chrono::duration_cast(time_to_sleep); LogDebug("sleeping for {0} us", time_to_sleep_us.count()); std::this_thread::sleep_for(time_to_sleep); continue; } expected_frame_time += target_frame_duration; frame++; if (!frame_time_limited_warning_shown && frame_time_now > expected_frame_time + 5 * target_frame_duration) { frame_time_limited_warning_shown = true; LogWarning("Over 5 frames behind - likely vsync limited?"); } processEvents(); if (p->ProgramStages.isEmpty()) { break; } { p->ProgramStages.current()->update(); } for (StageCmd cmd : stageCommands) { switch (cmd.cmd) { case StageCmd::Command::CONTINUE: break; case StageCmd::Command::REPLACE: p->ProgramStages.pop(); p->ProgramStages.push(cmd.nextStage); break; case StageCmd::Command::REPLACEALL: p->ProgramStages.clear(); p->ProgramStages.push(cmd.nextStage); break; case StageCmd::Command::PUSH: p->ProgramStages.push(cmd.nextStage); break; case StageCmd::Command::POP: p->ProgramStages.pop(); break; case StageCmd::Command::QUIT: p->quitProgram = true; p->ProgramStages.clear(); break; } if (p->quitProgram) { break; } } stageCommands.clear(); auto surface = p->scaleSurface ? p->scaleSurface : p->defaultSurface; RendererSurfaceBinding b(*this->renderer, surface); { this->renderer->clear(); } if (!p->ProgramStages.isEmpty()) { p->ProgramStages.current()->render(); if (p->toolTipImage) { renderer->draw(p->toolTipImage, p->toolTipPosition); } this->cursor->render(); if (p->scaleSurface) { RendererSurfaceBinding scaleBind(*this->renderer, p->defaultSurface); this->renderer->clear(); this->renderer->drawScaled(p->scaleSurface, {0, 0}, p->windowSize); } { this->renderer->flush(); this->renderer->newFrame(); SDL_GL_SwapWindow(p->window); } } if (frameCount && frame == frameCount) { LogWarning("Quitting hitting frame count limit of {0}", (unsigned long long)frame); p->quitProgram = true; } } } void Framework::processEvents() { if (p->ProgramStages.isEmpty()) { p->quitProgram = true; return; } // TODO: Consider threading the translation translateSdlEvents(); while (p->eventQueue.size() > 0 && !p->ProgramStages.isEmpty()) { up e; { std::lock_guard l(p->eventQueueLock); e = std::move(p->eventQueue.front()); p->eventQueue.pop_front(); } if (!e) { LogError("Invalid event on queue"); continue; } this->cursor->eventOccured(e.get()); if (e->type() == EVENT_KEY_DOWN) { if (e->keyboard().KeyCode == SDLK_PRINTSCREEN) { int screenshotId = 0; UString screenshotName; do { screenshotName = format("screenshot{0:03}.png", screenshotId); screenshotId++; } while (fs::exists(fs::path(screenshotName))); LogWarning("Writing screenshot to \"{0}\"", screenshotName); if (!p->defaultSurface->rendererPrivateData) { LogWarning("No renderer data on surface - nothing drawn yet?"); } else { auto img = p->defaultSurface->rendererPrivateData->readBack(); if (!img) { LogWarning("No image returned"); } else { this->threadPoolTaskEnqueue( [img, screenshotName] { auto ret = fw().data->writeImage(screenshotName, img); if (!ret) { LogWarning("Failed to write screenshot"); } else { LogWarning("Wrote screenshot to \"{0}\"", screenshotName); } }); } } } } switch (e->type()) { case EVENT_WINDOW_CLOSED: shutdownFramework(); return; default: p->ProgramStages.current()->eventOccurred(e.get()); break; } } /* Drop any events left in the list, as it's possible an event caused the last stage to pop * with events outstanding, but they can safely be ignored as we're quitting anyway */ { std::lock_guard l(p->eventQueueLock); p->eventQueue.clear(); } } void Framework::pushEvent(up e) { std::lock_guard l(p->eventQueueLock); p->eventQueue.push_back(std::move(e)); } void Framework::pushEvent(Event *e) { this->pushEvent(up(e)); } void Framework::translateSdlEvents() { SDL_Event e; Event *fwE; bool touch_events_enabled = Options::optionEnableTouchEvents.get(); // FIXME: That's not the right way to figure out the primary finger! int primaryFingerID = -1; if (SDL_GetNumTouchDevices()) { SDL_Finger *primaryFinger = SDL_GetTouchFinger(SDL_GetTouchDevice(0), 0); if (primaryFinger) { primaryFingerID = primaryFinger->id; } } while (SDL_PollEvent(&e)) { switch (e.type) { case SDL_QUIT: fwE = new DisplayEvent(EVENT_WINDOW_CLOSED); pushEvent(up(fwE)); break; case SDL_JOYDEVICEADDED: case SDL_JOYDEVICEREMOVED: // FIXME: Do nothing? break; case SDL_KEYDOWN: fwE = new KeyboardEvent(EVENT_KEY_DOWN); fwE->keyboard().KeyCode = e.key.keysym.sym; fwE->keyboard().ScanCode = e.key.keysym.scancode; fwE->keyboard().Modifiers = e.key.keysym.mod; pushEvent(up(fwE)); break; case SDL_KEYUP: fwE = new KeyboardEvent(EVENT_KEY_UP); fwE->keyboard().KeyCode = e.key.keysym.sym; fwE->keyboard().ScanCode = e.key.keysym.scancode; fwE->keyboard().Modifiers = e.key.keysym.mod; pushEvent(up(fwE)); break; case SDL_TEXTINPUT: fwE = new TextEvent(); fwE->text().Input = e.text.text; pushEvent(up(fwE)); break; case SDL_TEXTEDITING: // FIXME: Do nothing? break; case SDL_MOUSEMOTION: fwE = new MouseEvent(EVENT_MOUSE_MOVE); fwE->mouse().X = coordWindowToDisplayX(e.motion.x); fwE->mouse().Y = coordWindowToDisplayY(e.motion.y); fwE->mouse().DeltaX = e.motion.xrel; fwE->mouse().DeltaY = e.motion.yrel; fwE->mouse().WheelVertical = 0; // These should be handled fwE->mouse().WheelHorizontal = 0; // in a separate event fwE->mouse().Button = e.motion.state; pushEvent(up(fwE)); break; case SDL_MOUSEWHEEL: // FIXME: Check these values for sanity fwE = new MouseEvent(EVENT_MOUSE_SCROLL); // Since I'm using some variables that are not used anywhere else, // this code should be in its own small block. { int mx, my; fwE->mouse().Button = SDL_GetMouseState(&mx, &my); fwE->mouse().X = coordWindowToDisplayX(mx); fwE->mouse().Y = coordWindowToDisplayY(my); fwE->mouse().DeltaX = 0; // FIXME: This might cause problems? fwE->mouse().DeltaY = 0; fwE->mouse().WheelVertical = e.wheel.y; fwE->mouse().WheelHorizontal = e.wheel.x; } pushEvent(up(fwE)); break; case SDL_MOUSEBUTTONDOWN: fwE = new MouseEvent(EVENT_MOUSE_DOWN); fwE->mouse().X = coordWindowToDisplayX(e.button.x); fwE->mouse().Y = coordWindowToDisplayY(e.button.y); fwE->mouse().DeltaX = 0; // FIXME: This might cause problems? fwE->mouse().DeltaY = 0; fwE->mouse().WheelVertical = 0; fwE->mouse().WheelHorizontal = 0; fwE->mouse().Button = SDL_BUTTON(e.button.button); pushEvent(up(fwE)); break; case SDL_MOUSEBUTTONUP: fwE = new MouseEvent(EVENT_MOUSE_UP); fwE->mouse().X = coordWindowToDisplayX(e.button.x); fwE->mouse().Y = coordWindowToDisplayY(e.button.y); fwE->mouse().DeltaX = 0; // FIXME: This might cause problems? fwE->mouse().DeltaY = 0; fwE->mouse().WheelVertical = 0; fwE->mouse().WheelHorizontal = 0; fwE->mouse().Button = SDL_BUTTON(e.button.button); pushEvent(up(fwE)); break; case SDL_FINGERDOWN: if (!touch_events_enabled) break; fwE = new FingerEvent(EVENT_FINGER_DOWN); fwE->finger().X = static_cast(e.tfinger.x * displayGetWidth()); fwE->finger().Y = static_cast(e.tfinger.y * displayGetHeight()); fwE->finger().DeltaX = static_cast(e.tfinger.dx * displayGetWidth()); fwE->finger().DeltaY = static_cast(e.tfinger.dy * displayGetHeight()); fwE->finger().Id = e.tfinger.fingerId; fwE->finger().IsPrimary = e.tfinger.fingerId == primaryFingerID; // FIXME: Try to remember the ID of // the first touching finger! pushEvent(up(fwE)); break; case SDL_FINGERUP: if (!touch_events_enabled) break; fwE = new FingerEvent(EVENT_FINGER_UP); fwE->finger().X = static_cast(e.tfinger.x * displayGetWidth()); fwE->finger().Y = static_cast(e.tfinger.y * displayGetHeight()); fwE->finger().DeltaX = static_cast(e.tfinger.dx * displayGetWidth()); fwE->finger().DeltaY = static_cast(e.tfinger.dy * displayGetHeight()); fwE->finger().Id = e.tfinger.fingerId; fwE->finger().IsPrimary = e.tfinger.fingerId == primaryFingerID; // FIXME: Try to remember the ID of // the first touching finger! pushEvent(up(fwE)); break; case SDL_FINGERMOTION: if (!touch_events_enabled) break; fwE = new FingerEvent(EVENT_FINGER_MOVE); fwE->finger().X = static_cast(e.tfinger.x * displayGetWidth()); fwE->finger().Y = static_cast(e.tfinger.y * displayGetHeight()); fwE->finger().DeltaX = static_cast(e.tfinger.dx * displayGetWidth()); fwE->finger().DeltaY = static_cast(e.tfinger.dy * displayGetHeight()); fwE->finger().Id = e.tfinger.fingerId; fwE->finger().IsPrimary = e.tfinger.fingerId == primaryFingerID; // FIXME: Try to remember the ID of // the first touching finger! pushEvent(up(fwE)); break; case SDL_WINDOWEVENT: // Window events get special treatment switch (e.window.event) { case SDL_WINDOWEVENT_RESIZED: // FIXME: Do we care about SDL_WINDOWEVENT_SIZE_CHANGED? fwE = new DisplayEvent(EVENT_WINDOW_RESIZE); fwE->display().X = 0; fwE->display().Y = 0; fwE->display().Width = e.window.data1; fwE->display().Height = e.window.data2; fwE->display().Active = true; pushEvent(up(fwE)); break; case SDL_WINDOWEVENT_HIDDEN: case SDL_WINDOWEVENT_MINIMIZED: case SDL_WINDOWEVENT_LEAVE: // FIXME: Check if we should react this way for each of those events // FIXME: Check if we're missing some of the events fwE = new DisplayEvent(EVENT_WINDOW_DEACTIVATE); fwE->display().X = 0; fwE->display().Y = 0; // FIXME: Is this even necessary? SDL_GetWindowSize(p->window, &(fwE->display().Width), &(fwE->display().Height)); fwE->display().Active = false; pushEvent(up(fwE)); break; case SDL_WINDOWEVENT_SHOWN: case SDL_WINDOWEVENT_EXPOSED: case SDL_WINDOWEVENT_RESTORED: case SDL_WINDOWEVENT_ENTER: // FIXME: Should we handle all these events as "aaand we're back" // events? fwE = new DisplayEvent(EVENT_WINDOW_ACTIVATE); fwE->display().X = 0; fwE->display().Y = 0; // FIXME: Is this even necessary? SDL_GetWindowSize(p->window, &(fwE->display().Width), &(fwE->display().Height)); fwE->display().Active = false; pushEvent(up(fwE)); break; case SDL_WINDOWEVENT_CLOSE: // Closing a window will be a "quit" event. e.type = SDL_QUIT; SDL_PushEvent(&e); break; } break; default: break; } } } void Framework::shutdownFramework() { LogInfo("Shutdown framework"); p->ProgramStages.clear(); p->quitProgram = true; } enum class ScreenMode { Unknown, Windowed, FullScreen, Borderless }; static ScreenMode optionsScreenMode() { constexpr std::array, 3> mode_names = { {{"windowed", ScreenMode::Windowed}, {"fullscreen", ScreenMode::FullScreen}, {"borderless", ScreenMode::Borderless}}}; for (const auto &mode_name : mode_names) { if (Options::screenModeOption.get() == mode_name.first) { return mode_name.second; } } return ScreenMode::Unknown; } void Framework::displayInitialise() { if (!this->createWindow) { return; } LogInfo("Init display"); int display_flags = SDL_WINDOW_OPENGL; #ifdef OPENAPOC_GLES SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); #else #ifdef SDL_OPENGL_CORE SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); #endif #endif #ifdef DEBUG_RENDERER SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG); #endif // Request context version 3.0 SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); // Request RGBA8888 - change if needed SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetSwapInterval(Options::swapInterval.get()); ScreenMode mode = optionsScreenMode(); if (mode == ScreenMode::Unknown) { LogError("Unknown screen mode specified: {{{0}}}", Options::screenModeOption.get()); mode = ScreenMode::Windowed; } if (mode == ScreenMode::FullScreen) display_flags |= SDL_WINDOW_FULLSCREEN; else if (mode == ScreenMode::Borderless) display_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; int displayNumber = Options::screenDisplayNumberOption.get(); if (displayNumber >= SDL_GetNumVideoDisplays()) { LogWarning("Requested display number ({0}) does not exist. Using display 0", displayNumber); displayNumber = 0; } int scrW = Options::screenWidthOption.get(); int scrH = Options::screenHeightOption.get(); if (scrW < 640 || scrH < 480) { LogError("Requested display size of {{{0},{1}}} is lower than {{640,480}} and probably " "won't work", scrW, scrH); } p->window = SDL_CreateWindow("OpenApoc", SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayNumber), SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayNumber), scrW, scrH, display_flags); if (!p->window) { LogError("Failed to create window \"{0}\"", SDL_GetError()); exit(1); } p->context = SDL_GL_CreateContext(p->window); if (!p->context) { LogWarning("Could not create GL context! [SDLError: {0}]", SDL_GetError()); LogWarning("Attempting to create context by lowering the requested version"); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); p->context = SDL_GL_CreateContext(p->window); if (!p->context) { LogError("Failed to create GL context! [SDLerror: {0}]", SDL_GetError()); SDL_DestroyWindow(p->window); exit(1); } } // Output the context parameters LogInfo("Created OpenGL context, parameters:"); int value; SDL_GL_GetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, &value); UString profileType; switch (value) { case SDL_GL_CONTEXT_PROFILE_ES: profileType = "ES"; break; case SDL_GL_CONTEXT_PROFILE_CORE: profileType = "Core"; break; case SDL_GL_CONTEXT_PROFILE_COMPATIBILITY: profileType = "Compatibility"; break; default: profileType = "Unknown"; } LogInfo(" Context profile: {0}", profileType); int ctxMajor, ctxMinor; SDL_GL_GetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, &ctxMajor); SDL_GL_GetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, &ctxMinor); LogInfo(" Context version: {0}.{1}", ctxMajor, ctxMinor); int bitsRed, bitsGreen, bitsBlue, bitsAlpha; SDL_GL_GetAttribute(SDL_GL_RED_SIZE, &bitsRed); SDL_GL_GetAttribute(SDL_GL_GREEN_SIZE, &bitsGreen); SDL_GL_GetAttribute(SDL_GL_BLUE_SIZE, &bitsBlue); SDL_GL_GetAttribute(SDL_GL_ALPHA_SIZE, &bitsAlpha); LogInfo(" RGBA bits: {0}-{1}-{2}-{3}", bitsRed, bitsGreen, bitsBlue, bitsAlpha); SDL_GL_MakeCurrent(p->window, p->context); // for good measure? SDL_ShowCursor(SDL_DISABLE); p->registeredRenderers["GLES_3_0"].reset(getGLES30RendererFactory()); #ifndef __ANDROID__ // GL2 is not available on Android p->registeredRenderers["GL_2_0"].reset(getGL20RendererFactory()); #endif for (auto &rendererName : split(Options::renderersOption.get(), ":")) { auto rendererFactory = p->registeredRenderers.find(rendererName); if (rendererFactory == p->registeredRenderers.end()) { LogInfo("Renderer \"{0}\" not in supported list", rendererName); continue; } Renderer *r = rendererFactory->second->create(); if (!r) { LogInfo("Renderer \"{0}\" failed to init", rendererName); continue; } this->renderer.reset(r); LogInfo("Using renderer: {0}", this->renderer->getName()); break; } if (!this->renderer) { LogError("No functional renderer found"); abort(); } this->p->defaultSurface = this->renderer->getDefaultSurface(); int width, height; SDL_GetWindowSize(p->window, &width, &height); p->windowSize = {width, height}; setMouseGrab(); // FIXME: Scale is currently stored as an integer in 1/100 units (ie 100 is 1.0 == same // size) int scaleX = Options::screenScaleXOption.get(); int scaleY = Options::screenScaleYOption.get(); const bool autoScale = Options::screenAutoScale.get(); if (scaleX != 100 || scaleY != 100 || autoScale) { float scaleXFloat = (float)scaleX / 100.0f; float scaleYFloat = (float)scaleY / 100.0f; if (autoScale) { constexpr int referenceWidth = 1280; scaleYFloat = scaleXFloat = (float)referenceWidth / p->windowSize.x; LogInfo("Autoscaling enabled, scaling by ({0},{1})", scaleXFloat, scaleYFloat); } p->displaySize.x = (int)((float)p->windowSize.x * scaleXFloat); p->displaySize.y = (int)((float)p->windowSize.y * scaleYFloat); if (p->displaySize.x < 640 || p->displaySize.y < 480) { LogWarning("Requested scaled size of {0} is lower than {{640,480}} and probably " "won't work, so forcing 640x480", p->displaySize.x); p->displaySize.x = std::max(640, p->displaySize.x); p->displaySize.y = std::max(480, p->displaySize.y); } LogInfo("Scaling from {0} to {1}", p->displaySize, p->windowSize); p->scaleSurface = mksp(p->displaySize); } else { p->displaySize = p->windowSize; } this->cursor.reset(new ApocCursor(this->data->loadPalette("xcom3/tacdata/tactical.pal"))); } void Framework::displayShutdown() { this->cursor.reset(); if (!p->window) { return; } LogInfo("Shutdown Display"); p->defaultSurface.reset(); renderer.reset(); SDL_GL_DeleteContext(p->context); SDL_DestroyWindow(p->window); } int Framework::displayGetWidth() { return p->displaySize.x; } int Framework::displayGetHeight() { return p->displaySize.y; } Vec2 Framework::displayGetSize() { return p->displaySize; } int Framework::coordWindowToDisplayX(int x) const { return (float)x / p->windowSize.x * p->displaySize.x; } int Framework::coordWindowToDisplayY(int y) const { return (float)y / p->windowSize.y * p->displaySize.y; } Vec2 Framework::coordWindowsToDisplay(const Vec2 &coord) const { return Vec2(coordWindowToDisplayX(coord.x), coordWindowToDisplayY(coord.y)); } bool Framework::displayHasWindow() const { if (createWindow == false) return false; if (!p->window) return false; return true; } void Framework::displaySetTitle(UString NewTitle) { if (p->window) { SDL_SetWindowTitle(p->window, NewTitle.c_str()); } } void Framework::displaySetIcon(sp image) { if (!p->window) { return; } #ifdef _WIN32 SDL_SysWMinfo info; SDL_VERSION(&info.version); SDL_GetWindowWMInfo(p->window, &info); HINSTANCE handle = GetModuleHandle(NULL); HICON icon = LoadIcon(handle, L"ALLEGRO_ICON"); HWND hwnd = info.info.win.window; SetClassLongPtr(hwnd, GCLP_HICON, (LONG_PTR)icon); #else RGBImageLock reader(image, ImageLockUse::Read); // TODO: Should set the pixels instead of using a void* SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(reader.getData(), image->size.x, image->size.y, 32, 0, 0xF000, 0x0F00, 0x00F0, 0x000F); SDL_SetWindowIcon(p->window, surface); SDL_FreeSurface(surface); #endif } void Framework::audioInitialise(bool headless) { LogInfo("Initialise Audio"); if (!headless) { p->registeredSoundBackends["SDLRaw"].reset(getSDLSoundBackend()); } p->registeredSoundBackends["null"].reset(getNullSoundBackend()); auto concurrent_sample_count = Options::audioConcurrentSampleCount.get(); for (auto &soundBackendName : split(Options::audioBackendsOption.get(), ":")) { auto backendFactory = p->registeredSoundBackends.find(soundBackendName); if (backendFactory == p->registeredSoundBackends.end()) { LogInfo("Sound backend {0} not in supported list", soundBackendName); continue; } SoundBackend *backend = backendFactory->second->create(concurrent_sample_count); if (!backend) { LogInfo("Sound backend {0} failed to init", soundBackendName); continue; } this->soundBackend.reset(backend); LogInfo("Using sound backend {0}", soundBackendName); break; } if (!this->soundBackend) { LogError("No functional sound backend found"); } this->jukebox = createJukebox(*this); /* Setup initial gain */ this->soundBackend->setGain(SoundBackend::Gain::Global, static_cast(Options::audioGlobalGainOption.get()) / 20.0f); this->soundBackend->setGain(SoundBackend::Gain::Music, static_cast(Options::audioMusicGainOption.get()) / 20.0f); this->soundBackend->setGain(SoundBackend::Gain::Sample, static_cast(Options::audioSampleGainOption.get()) / 20.0f); } void Framework::audioShutdown() { LogInfo("Shutdown Audio"); this->jukebox.reset(); this->soundBackend.reset(); } sp Framework::stageGetCurrent() { return p->ProgramStages.current(); } sp Framework::stageGetPrevious() { return p->ProgramStages.previous(); } sp Framework::stageGetPrevious(sp From) { return p->ProgramStages.previous(From); } void Framework::stageQueueCommand(const StageCmd &cmd) { stageCommands.emplace_back(cmd); } ApocCursor &Framework::getCursor() { return *this->cursor; } void Framework::textStartInput() { SDL_StartTextInput(); } void Framework::textStopInput() { SDL_StopTextInput(); } void Framework::toolTipStartTimer(up e) { int delay = config().getInt("Options.Misc.ToolTipDelay"); if (delay <= 0) { return; } // remove any pending timers toolTipStopTimer(); p->toolTipTimerEvent = std::move(e); p->toolTipTimerId = SDL_AddTimer( delay, [](unsigned int interval, void *data) -> unsigned int { fw().toolTipTimerCallback(interval, data); // remove this sdl timer return 0; }, nullptr); } void Framework::toolTipStopTimer() { if (p->toolTipTimerId) { SDL_RemoveTimer(p->toolTipTimerId); p->toolTipTimerId = 0; } p->toolTipTimerEvent.reset(); p->toolTipImage.reset(); } void Framework::toolTipTimerCallback(unsigned int interval [[maybe_unused]], void *data [[maybe_unused]]) { // the sdl timer will be removed, so we forget about // clear the timerid and reset the event pushEvent(std::move(p->toolTipTimerEvent)); p->toolTipTimerId = 0; } void Framework::showToolTip(sp image, const Vec2 &position) { p->toolTipImage = image; p->toolTipPosition = position; } void Framework::setMouseGrab() { auto mouseCapture = Options::mouseCaptureOption.get(); SDL_SetWindowMouseGrab(p->window, mouseCapture ? SDL_TRUE : SDL_FALSE); SDL_SetRelativeMouseMode(mouseCapture ? SDL_TRUE : SDL_FALSE); } UString Framework::textGetClipboard() { UString str; char *text = SDL_GetClipboardText(); if (text != nullptr) { str = text; SDL_free(text); } return str; } void Framework::threadPoolTaskEnqueue(std::function task) { p->threadPool->enqueue(task); } void *Framework::getWindowHandle() const { return static_cast(p->window); } void Framework::setupModDataPaths() { auto mods = split(Options::modList.get(), ":"); for (const auto &modString : mods) { LogWarning("Loading mod data \"{0}\"", modString); auto modPath = Options::modPath.get() + "/" + modString; auto _modInfo = ModInfo::getInfo(modPath); if (!_modInfo) { LogError("Failed to load ModInfo for mod \"{0}\"", modString); continue; } const auto modInfo = *_modInfo; auto modDataPath = modPath + "/" + modInfo.getDataPath(); LogInfo("Loaded modinfo for mod ID \"{0}\"", modInfo.getID()); if (modInfo.getDataPath() != "") { LogInfo("Appending data path \"{0}\"", modDataPath); this->data->fs.addPath(modDataPath); } LogInfo("Loading FW mod language"); auto _language = getModLanguageInfo(modInfo); if (_language) { const auto language = *_language; LogInfo("Loading mod language ID {0}", language.ID); if (!language.data.empty()) { const auto dataPath = modPath + "/" + language.data; LogInfo("Appending mod language data path \"{0}\" from \"{1}\"", dataPath, language.data); this->data->fs.addPath(dataPath); } } LogInfo("Loading FW mod language"); } } }; // namespace OpenApoc