diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b4db03d6..e6fb880e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -505,6 +505,10 @@ IF (MINGW) SET(OD_SOURCEFILES ${OD_SOURCEFILES} ${SRC}/utils/StackTraceWinMinGW.cpp) ELSEIF(MSVC) SET(OD_SOURCEFILES ${OD_SOURCEFILES} ${SRC}/utils/StackTraceWinMSVC.cpp) +ELSEIF(APPLE) + # StackTraceUnix relies on struct sigcontext and the deprecated ucontext + # routines, neither of which exists on macOS. + SET(OD_SOURCEFILES ${OD_SOURCEFILES} ${SRC}/utils/StackTraceStub.cpp) ELSEIF(UNIX) SET(OD_SOURCEFILES ${OD_SOURCEFILES} ${SRC}/utils/StackTraceUnix.cpp) ELSE() diff --git a/source/ODApplication.cpp b/source/ODApplication.cpp index 70abe9154..f476e664d 100644 --- a/source/ODApplication.cpp +++ b/source/ODApplication.cpp @@ -231,7 +231,11 @@ void ODApplication::startClient() HWND hwnd; renderWindow->getCustomAttribute("WINDOW", static_cast(&hwnd)); HINSTANCE hInst = static_cast(GetModuleHandle(nullptr)); - SetClassLong(hwnd, GCL_HICON, reinterpret_cast(LoadIcon(hInst, MAKEINTRESOURCE(IDI_ICON1)))); + // SetClassLong takes a 32 bit value, so casting the icon handle to one only works + // while a handle is 32 bits wide: a 64 bit build does not compile. The Ptr form is + // the same call on a 32 bit build and the right one on a 64 bit build. + SetClassLongPtr(hwnd, GCLP_HICON, + reinterpret_cast(LoadIcon(hInst, MAKEINTRESOURCE(IDI_ICON1)))); #endif //Initialise RTshader system diff --git a/source/modes/AdvertMode.cpp b/source/modes/AdvertMode.cpp index 90b370b77..308ef9b7f 100755 --- a/source/modes/AdvertMode.cpp +++ b/source/modes/AdvertMode.cpp @@ -23,6 +23,43 @@ #include #include +#include + +#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 +// MinGW's libstdc++ predefines NOMINMAX, and the game builds with -Werror: +// an unconditional redefinition is fatal there. +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#endif + +namespace +{ + //! \brief Hands a link to whatever the system uses to open one. Each platform has its + //! own way: xdg-open is the freedesktop one and does not exist on the other two. + void openInBrowser(const std::string& url) + { +#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 + ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL); +#else +#if OGRE_PLATFORM == OGRE_PLATFORM_APPLE + const std::string command = "open '" + url + "'"; +#else + const std::string command = "xdg-open '" + url + "'"; +#endif + // The result is worth looking at only to say so: there is nothing to fall back on, + // and the game is on its way out by the time this runs. + if(std::system(command.c_str()) != 0) + OD_LOG_WRN("Couldn't open " + url); +#endif + } +} + AdvertMode::AdvertMode(ModeManager* modeManager): AbstractApplicationMode(modeManager, ModeManager::ADVERTISMENT) @@ -85,7 +122,7 @@ void AdvertMode::activate() bool AdvertMode::showWWW() { ODFrameListener::getSingletonPtr()->requestExit(); - system("xdg-open 'https://discord.gg/K2JPXuchZV'"); + openInBrowser("https://discord.gg/K2JPXuchZV"); return true; } diff --git a/source/modes/EditorMode.cpp b/source/modes/EditorMode.cpp index 597854a03..7deb58026 100755 --- a/source/modes/EditorMode.cpp +++ b/source/modes/EditorMode.cpp @@ -57,7 +57,15 @@ #if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 -#include +// MinGW's libstdc++ predefines NOMINMAX, and the game builds with -Werror: +// an unconditional redefinition is fatal there. +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include #endif #include @@ -470,16 +478,21 @@ void EditorMode::activate() // Hide also the Replay check-box as it doesn't make sense for the editor guiSheet->getChild("ConfirmExit/SaveReplayCheckbox")->hide(); guiSheet->getChild("GameChatWindow/GameChatEditBox")->hide(); + // Start the file dialogs in the folder the player's own levels are saved to. That used + // to be $HOME, which is only set on Windows if someone has set it: there the dialogs + // opened on nothing at all. ResourceManager resolves this one per platform, and it is + // where a level being loaded or saved from the editor belongs anyway. + const std::string userLevelPath = ResourceManager::getSingleton().getUserLevelPathSkirmish(); guiSheet->getChild("MenuEditorLoad")->hide(); guiSheet->getChild("MenuEditorLoad")->getChild("LevelWindowFrame") - ->getChild("FilePath")->setText(getEnv("HOME")); + ->getChild("FilePath")->setText(userLevelPath); guiSheet->getChild("MenuEditorLoad")->getChild("LevelWindowFrame") - ->getChild("FilePath")->fireEvent(CEGUI::Editbox::EventTextAccepted,args); + ->getChild("FilePath")->fireEvent(CEGUI::Editbox::EventTextAccepted,args); guiSheet->getChild("MenuEditorSave")->hide(); guiSheet->getChild("MenuEditorSave")->getChild("LevelWindowFrame") - ->getChild("FilePath")->setText(getEnv("HOME")); + ->getChild("FilePath")->setText(userLevelPath); guiSheet->getChild("MenuEditorSave")->getChild("LevelWindowFrame") - ->getChild("FilePath")->fireEvent(CEGUI::Editbox::EventTextAccepted,args); + ->getChild("FilePath")->fireEvent(CEGUI::Editbox::EventTextAccepted,args); CEGUI::Combobox* levelTypeCb = static_cast (mRootWindow->getChild("LevelWindowFrame/LevelTypeSelect")); levelTypeCb->setItemSelectState(static_cast(0), true); @@ -1565,7 +1578,7 @@ bool EditorMode::loadMenuFilePathTextChanged( const CEGUI::EventArgs& /*arg*/) int nn = 1; for (directory_entry& xx : directory_iterator(pp)) { - if(!(isFileHidden(xx.path().filename().generic_string()) + if(!(isFileHidden(xx.path()) && !isCheckboxSelected("MenuEditorLoad/LevelWindowFrame/HiddenFiles"))) { if(xx.path().has_extension() && xx.path().extension().compare(L".level") == 0) @@ -1719,7 +1732,7 @@ bool EditorMode::saveMenuFilePathTextChanged(const CEGUI::EventArgs& /*arg*/) int nn = 1; for (directory_entry& xx : directory_iterator(pp)) { - if(!(isFileHidden(xx.path().filename().generic_string()) && !isCheckboxSelected("MenuEditorSave/LevelWindowFrame/HiddenFiles"))) + if(!(isFileHidden(xx.path()) && !isCheckboxSelected("MenuEditorSave/LevelWindowFrame/HiddenFiles"))) { if(xx.path().has_extension() && xx.path().extension().compare(std::string(".level")) == 0) { @@ -2026,23 +2039,6 @@ bool EditorMode::updateDescription(const CEGUI::EventArgs&) return true; } -std::string EditorMode::getEnv( const std::string & var ) -{ - // WINDOWS: - // "you could look at HOMEDRIVE, HOMEPATH or USERPROFILE - // env variables on windows. or i guess SHGetFolderPathA()" - const char * val = std::getenv( var.c_str() ); - if ( val == nullptr ) - { // invalid to assign nullptr to std::string - return ""; - } - else - { - return val; - } -} - - void EditorMode::uninstallRecentlyUsedFilesButtons() { CEGUI::Window *pm = mRootWindow->getChild("Menubar")->getChild("File")->getChild("PopupMenu1")->getChild("RecentlyUsed")->getChild("PopupMenu2"); @@ -2122,16 +2118,20 @@ bool EditorMode::isCheckboxSelected(const CEGUI::String& checkbox) } -bool EditorMode::isFileHidden(std::string path) +bool EditorMode::isFileHidden(const boost::filesystem::path& path) { #if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 - - //DWORD attributes = GetFileAttributes(path); - //return (attributes & FILE_ATTRIBUTE_HIDDEN); - return false; + // Windows keeps it as an attribute of the file rather than in its name, so the whole + // path is needed to ask for it. A file we cannot read the attributes of is not hidden. + const DWORD attributes = GetFileAttributesA(path.string().c_str()); + if(attributes == INVALID_FILE_ATTRIBUTES) + return false; + + return (attributes & (FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM)) != 0; #else - return (path[0] == '.'); -#endif + const std::string filename = path.filename().string(); + return !filename.empty() && (filename[0] == '.'); +#endif } diff --git a/source/modes/EditorMode.h b/source/modes/EditorMode.h index 6de7bca85..f0a87a15a 100755 --- a/source/modes/EditorMode.h +++ b/source/modes/EditorMode.h @@ -116,7 +116,6 @@ friend class ODClient; void displayText(const Ogre::ColourValue& txtColour, const std::string& txt) override; bool updateDescription(const CEGUI::EventArgs& e = {}); - std::string getEnv( const std::string & var ); bool isCheckboxSelected(const CEGUI::String& checkbox); private: @@ -187,7 +186,10 @@ friend class ODClient; bool loadLevelFromFile(const std::string&); //! \brief file path to currently choosen file via load / save menu - bool isFileHidden(std::string path); + //! \brief Whether the file should be kept out of the level lists unless the player + //! asked for hidden ones. Takes the whole path: on Windows being hidden is an attribute + //! of the file, not a dot in front of its name. + bool isFileHidden(const boost::filesystem::path& path); void addPathNameToList(boost::filesystem::directory_entry& xx, CEGUI::Listbox* levelSelectList, CEGUI::Colour cc, int& nn ); std::string dialogFullPath; diff --git a/source/utils/Helper.cpp b/source/utils/Helper.cpp index 7ee2a4f79..1232d8d10 100755 --- a/source/utils/Helper.cpp +++ b/source/utils/Helper.cpp @@ -184,6 +184,16 @@ namespace Helper while (baseLevelFile.good()) { std::getline(baseLevelFile, nextParam); + + // A file written on Windows ends its lines with a carriage return before the + // line feed, and only Windows strips it back off when reading. Everywhere else + // it stays at the end of the line, where it makes the last value on it, or a + // section marker standing alone, something nothing here recognises: a level + // saved on Windows would not load anywhere at all. Take it off ourselves so + // that it makes no difference where a file was written. + if(!nextParam.empty() && (*nextParam.rbegin() == '\r')) + nextParam.erase(nextParam.size() - 1); + /* Find the first occurrence of the comment symbol on the * line and return everything before that character. */ @@ -284,7 +294,7 @@ namespace Helper { return TTostring(d); } - #if defined(__OpenBSD__) && defined(__LP64__) + #if defined(__APPLE__) || (defined(__OpenBSD__) && defined(__LP64__)) std::string toString(size_t d) { return TTostring(d); diff --git a/source/utils/Helper.h b/source/utils/Helper.h index 30602fc7d..9f0610131 100755 --- a/source/utils/Helper.h +++ b/source/utils/Helper.h @@ -136,7 +136,10 @@ namespace Helper std::string toString(uint32_t d); std::string toString(int64_t d); std::string toString(uint64_t d); - #if defined(__OpenBSD__) && defined(__LP64__) + // On LP64 systems whose size_t is unsigned long while uint64_t is + // unsigned long long (macOS, OpenBSD), size_t matches none of the fixed + // width overloads and calls become ambiguous. + #if defined(__APPLE__) || (defined(__OpenBSD__) && defined(__LP64__)) std::string toString(size_t d); #endif std::string toString(const Ogre::Vector2& v); diff --git a/source/utils/ResourceManager.cpp b/source/utils/ResourceManager.cpp index 6774100c0..3a6d33763 100755 --- a/source/utils/ResourceManager.cpp +++ b/source/utils/ResourceManager.cpp @@ -56,6 +56,20 @@ #include +namespace +{ + //! \brief Whether the character ends a folder name. Windows takes both, and the paths + //! here are a mix: some are built with '/', some come from the system or the player. + bool isDirectorySeparator(char c) + { +#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 + return (c == '/') || (c == '\\'); +#else + return c == '/'; +#endif + } +} + template<> ResourceManager* Ogre::Singleton::msSingleton = nullptr; #if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 && defined(OD_DEBUG) //On windows, if the application is compiled in debug mode, use the plugins with debug prefix. @@ -98,6 +112,7 @@ ResourceManager::ResourceManager(boost::program_options::variables_map& options) void ResourceManager::setupDataPath(boost::program_options::variables_map& options) { + std::string path; #if OGRE_PLATFORM == OGRE_PLATFORM_APPLE //TODO - Test osx support char applePath[1024]; @@ -115,12 +130,12 @@ void ResourceManager::setupDataPath(boost::program_options::variables_map& optio CFRelease(mainBundleURL); CFRelease(cfStringRef); - mMacBundlePath = std::string(applePath + "/"); + // Not applePath + "/": that is a pointer plus a pointer, which does not compile. + mMacBundlePath = std::string(applePath) + "/"; mGameDataPath = mMacBundlePath + "Contents/Resources/"; #else // Windows and linux - std::string path; #ifdef OD_DATA_PATH path = std::string(OD_DATA_PATH); #else @@ -130,7 +145,7 @@ void ResourceManager::setupDataPath(boost::program_options::variables_map& optio if(!path.empty()) { mGameDataPath = path; - if (*mGameDataPath.rbegin() != '/') + if (!isDirectorySeparator(*mGameDataPath.rbegin())) { mGameDataPath.append("/"); } @@ -139,7 +154,12 @@ void ResourceManager::setupDataPath(boost::program_options::variables_map& optio mGameDataPath = Ogre::FileSystemLayer::resolveBundlePath(mGameDataPath); #endif } +#endif // Windows and Linux + // From here on the logic is the same on every platform: data or a plugins.cfg + // in the current folder win over the installed ones. This is also what lets a + // macOS build run at all outside an .app bundle: the Apple branch above knows + // only the bundle layout, and nothing used to set the plugins path there. // Test whether there is data in "./" and remove the system path in that case. // Useful for developers. std::string resourceCfg = "./" + RESOURCECFG; @@ -179,8 +199,6 @@ void ResourceManager::setupDataPath(boost::program_options::variables_map& optio mPluginsPath = pluginsCfg; } -#endif // Windows and Linux - OD_LOG_INF( PLUGINSCFG + " path is: " + mPluginsPath + '\n'); mScriptPath = mGameDataPath + SCRIPTSUBPATH; @@ -202,8 +220,7 @@ void ResourceManager::setupUserDataFolders(boost::program_options::variables_map mUserDataPath = itOption->second.as(); if(!mUserDataPath.empty()) { - uint32_t len = mUserDataPath.length(); - if((mUserDataPath.at(len - 1) != '/') && (mUserDataPath.at(len - 1) != '\\')) + if(!isDirectorySeparator(*mUserDataPath.rbegin())) mUserDataPath += '/'; mUserConfigPath = mUserDataPath + "cfg/"; @@ -495,7 +512,9 @@ void ResourceManager::setupOgreResources(uint16_t shaderLanguageVersion) const Ogre::String& typeName = setting.first; Ogre::String archName = setting.second; - if(!archName.empty() && archName.front() != '/') // do not modify absolute paths + // Do not modify absolute paths. A leading '/' is not what makes one on Windows, + // where they start with a drive letter, so let boost decide. + if(!archName.empty() && !boost::filesystem::path(archName).is_absolute()) archName = mGameDataPath + archName; else archName = Ogre::FileSystemLayer::resolveBundlePath(archName);