Index: ps/trunk/source/lib/sysdep/os/osx/osx_atlas.h =================================================================== --- ps/trunk/source/lib/sysdep/os/osx/osx_atlas.h (nonexistent) +++ ps/trunk/source/lib/sysdep/os/osx/osx_atlas.h (revision 23926) @@ -0,0 +1,33 @@ +/* Copyright (C) 2020 Wildfire Games. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef OSX_ATLAS_H +#define OSX_ATLAS_H + + +/** + * Runs a new pyrogenesis process with the -editor argument. + * Necessary because SDL and WxWidgets conflict. + */ +void startNewAtlasProcess(); + +#endif // OSX_ATLAS_H Property changes on: ps/trunk/source/lib/sysdep/os/osx/osx_atlas.h ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/source/lib/sysdep/os/osx/osx_atlas.mm =================================================================== --- ps/trunk/source/lib/sysdep/os/osx/osx_atlas.mm (nonexistent) +++ ps/trunk/source/lib/sysdep/os/osx/osx_atlas.mm (revision 23926) @@ -0,0 +1,54 @@ +/* Copyright (C) 2020 Wildfire Games. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import // MAC_OS_X_VERSION_MIN_REQUIRED +#import + +#import "osx_atlas.h" + +#include +#include "lib/types.h" +#include "ps/CStr.h" + +extern std::vector g_modsLoaded; + +void startNewAtlasProcess() +{ + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + NSMutableArray *args = [[NSMutableArray alloc] init]; + [args addObject:@"--editor"]; + + // Pass mods on the command line. + for (const CStr& mod : g_modsLoaded) + { + std::string arg = std::string("-mod=") + mod; + [args addObject:[[NSString alloc] initWithUTF8String:arg.c_str()]]; + } + + // Apple documents this as (deprecated) NSWorkspaceLaunchConfigurationKey, but that's not available in early SDKs. + NSDictionary *params = @{ NSWorkspaceLaunchConfigurationArguments: args }; + + [[NSWorkspace sharedWorkspace] launchApplicationAtURL:[[NSRunningApplication currentApplication] executableURL] options:NSWorkspaceLaunchNewInstance configuration:params error:nil]; + + [pool drain]; +} Property changes on: ps/trunk/source/lib/sysdep/os/osx/osx_atlas.mm ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/source/main.cpp =================================================================== --- ps/trunk/source/main.cpp (revision 23925) +++ ps/trunk/source/main.cpp (revision 23926) @@ -1,762 +1,771 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* This module drives the game when running without Atlas (our integrated map editor). It receives input and OS messages via SDL and feeds them into the input dispatcher, where they are passed on to the game GUI and simulation. It also contains main(), which either runs the above controller or that of Atlas depending on commandline parameters. */ // not for any PCH effort, but instead for the (common) definitions // included there. #define MINIMAL_PCH 2 #include "lib/precompiled.h" #include #include "lib/debug.h" #include "lib/status.h" #include "lib/secure_crt.h" #include "lib/frequency_filter.h" #include "lib/input.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/external_libraries/libsdl.h" #include "ps/ArchiveBuilder.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Loader.h" #include "ps/ModInstaller.h" #include "ps/Profile.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" #include "ps/Replay.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/World.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/Paths.h" #include "ps/XML/Xeromyces.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "network/NetSession.h" #include "lobby/IXmppClient.h" #include "graphics/Camera.h" #include "graphics/GameView.h" #include "graphics/TextureManager.h" #include "gui/GUIManager.h" #include "renderer/Renderer.h" #include "rlinterface/RLInterface.cpp" #include "scriptinterface/ScriptEngine.h" #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" #include "soundmanager/ISoundManager.h" #if OS_UNIX #include // geteuid #endif // OS_UNIX +#if OS_MACOSX +#include "lib/sysdep/os/osx/osx_atlas.h" +#endif + #if MSC_VERSION #include #define getpid _getpid // Use the non-deprecated function name #endif extern CmdLineArgs g_args; extern CStrW g_UniqueLogPostfix; // Marks terrain as modified so the minimap can repaint (is there a cleaner way of handling this?) bool g_GameRestarted = false; // Determines the lifetime of the mainloop enum ShutdownType { // The application shall continue the main loop. None, // The process shall terminate as soon as possible. Quit, // The engine should be restarted in the same process, for instance to activate different mods. Restart, // Atlas should be started in the same process. RestartAsAtlas }; static ShutdownType g_Shutdown = ShutdownType::None; // to avoid redundant and/or recursive resizing, we save the new // size after VIDEORESIZE messages and only update the video mode // once per frame. // these values are the latest resize message, and reset to 0 once we've // updated the video mode static int g_ResizedW; static int g_ResizedH; static std::chrono::high_resolution_clock::time_point lastFrameTime; bool IsQuitRequested() { return g_Shutdown == ShutdownType::Quit; } void QuitEngine() { g_Shutdown = ShutdownType::Quit; } void RestartEngine() { g_Shutdown = ShutdownType::Restart; } void StartAtlas() { g_Shutdown = ShutdownType::RestartAsAtlas; } // main app message handler static InReaction MainInputHandler(const SDL_Event_* ev) { switch(ev->ev.type) { case SDL_WINDOWEVENT: switch(ev->ev.window.event) { case SDL_WINDOWEVENT_ENTER: RenderCursor(true); break; case SDL_WINDOWEVENT_LEAVE: RenderCursor(false); break; case SDL_WINDOWEVENT_RESIZED: g_ResizedW = ev->ev.window.data1; g_ResizedH = ev->ev.window.data2; break; case SDL_WINDOWEVENT_MOVED: g_VideoMode.UpdatePosition(ev->ev.window.data1, ev->ev.window.data2); } break; case SDL_QUIT: QuitEngine(); break; case SDL_HOTKEYPRESS: std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "exit") { QuitEngine(); return IN_HANDLED; } else if (hotkey == "screenshot") { WriteScreenshot(L".png"); return IN_HANDLED; } else if (hotkey == "bigscreenshot") { WriteBigScreenshot(L".bmp", 10); return IN_HANDLED; } else if (hotkey == "togglefullscreen") { g_VideoMode.ToggleFullscreen(); return IN_HANDLED; } else if (hotkey == "profile2.toggle") { g_Profiler2.Toggle(); return IN_HANDLED; } break; } return IN_PASS; } // dispatch all pending events to the various receivers. static void PumpEvents() { JSContext* cx = g_GUI->GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); PROFILE3("dispatch events"); SDL_Event_ ev; while (in_poll_event(&ev)) { PROFILE2("event"); if (g_GUI) { JS::RootedValue tmpVal(cx); ScriptInterface::ToJSVal(cx, &tmpVal, ev); std::string data = g_GUI->GetScriptInterface()->StringifyJSON(&tmpVal); PROFILE2_ATTR("%s", data.c_str()); } in_dispatch_event(&ev); } g_TouchInput.Frame(); } /** * Optionally throttle the render frequency in order to * prevent 100% workload of the currently used CPU core. */ inline static void LimitFPS() { if (g_VSync) return; double fpsLimit = 0.0; CFG_GET_VAL(g_Game && g_Game->IsGameStarted() ? "adaptivefps.session" : "adaptivefps.menu", fpsLimit); // Keep in sync with options.json if (fpsLimit < 20.0 || fpsLimit >= 100.0) return; double wait = 1000.0 / fpsLimit - std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - lastFrameTime).count() / 1000.0; if (wait > 0.0) SDL_Delay(wait); lastFrameTime = std::chrono::high_resolution_clock::now(); } static int ProgressiveLoad() { PROFILE3("progressive load"); wchar_t description[100]; int progress_percent; try { Status ret = LDR_ProgressiveLoad(10e-3, description, ARRAY_SIZE(description), &progress_percent); switch(ret) { // no load active => no-op (skip code below) case INFO::OK: return 0; // current task didn't complete. we only care about this insofar as the // load process is therefore not yet finished. case ERR::TIMED_OUT: break; // just finished loading case INFO::ALL_COMPLETE: g_Game->ReallyStartGame(); wcscpy_s(description, ARRAY_SIZE(description), L"Game is starting.."); // LDR_ProgressiveLoad returns L""; set to valid text to // avoid problems in converting to JSString break; // error! default: WARN_RETURN_STATUS_IF_ERR(ret); // can't do this above due to legit ERR::TIMED_OUT break; } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map loading failed // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } GUI_DisplayLoadProgress(progress_percent, description); return 0; } static void RendererIncrementalLoad() { PROFILE3("renderer incremental load"); const double maxTime = 0.1f; double startTime = timer_Time(); bool more; do { more = g_Renderer.GetTextureManager().MakeProgress(); } while (more && timer_Time() - startTime < maxTime); } static void Frame() { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); ogl_WarnIfError(); // get elapsed time const double time = timer_Time(); g_frequencyFilter->Update(time); // .. old method - "exact" but contains jumps #if 0 static double last_time; const double time = timer_Time(); const float TimeSinceLastFrame = (float)(time-last_time); last_time = time; ONCE(return); // first call: set last_time and return // .. new method - filtered and more smooth, but errors may accumulate #else const float realTimeSinceLastFrame = 1.0 / g_frequencyFilter->SmoothedFrequency(); #endif ENSURE(realTimeSinceLastFrame > 0.0f); // Decide if update is necessary bool need_update = true; // If we are not running a multiplayer game, disable updates when the game is // minimized or out of focus and relinquish the CPU a bit, in order to make // debugging easier. if (g_PauseOnFocusLoss && !g_NetClient && !g_app_has_focus) { PROFILE3("non-focus delay"); need_update = false; // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored SDL_Delay(10); } // this scans for changed files/directories and reloads them, thus // allowing hotloading (changes are immediately assimilated in-game). ReloadChangedFiles(); ProgressiveLoad(); RendererIncrementalLoad(); PumpEvents(); // if the user quit by closing the window, the GL context will be broken and // may crash when we call Render() on some drivers, so leave this loop // before rendering if (g_Shutdown != ShutdownType::None) return; // respond to pumped resize events if (g_ResizedW || g_ResizedH) { g_VideoMode.ResizeWindow(g_ResizedW, g_ResizedH); g_ResizedW = g_ResizedH = 0; } if (g_NetClient) g_NetClient->Poll(); ogl_WarnIfError(); g_GUI->TickObjects(); ogl_WarnIfError(); if (g_RLInterface) g_RLInterface->TryApplyMessage(); if (g_Game && g_Game->IsGameStarted() && need_update) { if (!g_RLInterface) g_Game->Update(realTimeSinceLastFrame); g_Game->GetView()->Update(float(realTimeSinceLastFrame)); } // Immediately flush any messages produced by simulation code if (g_NetClient) g_NetClient->Flush(); // Keep us connected to any XMPP servers if (g_XmppClient) g_XmppClient->recv(); g_UserReporter.Update(); g_Console->Update(realTimeSinceLastFrame); ogl_WarnIfError(); if (g_SoundManager) g_SoundManager->IdleTask(); if (ShouldRender()) { Render(); { PROFILE3("swap buffers"); SDL_GL_SwapWindow(g_VideoMode.GetWindow()); ogl_WarnIfError(); } g_Renderer.OnSwapBuffers(); } g_Profiler.Frame(); g_GameRestarted = false; LimitFPS(); } static void NonVisualFrame() { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); static u32 turn = 0; debug_printf("Turn %u (%u)...\n", turn++, DEFAULT_TURN_LENGTH_SP); g_Game->GetSimulation2()->Update(DEFAULT_TURN_LENGTH_SP); g_Profiler.Frame(); if (g_Game->IsGameFinished()) QuitEngine(); } static void MainControllerInit() { // add additional input handlers only needed by this controller: // must be registered after gui_handler. Should mayhap even be last. in_add_handler(MainInputHandler); } static void MainControllerShutdown() { in_reset_handlers(); } static void StartRLInterface(CmdLineArgs args) { std::string server_address; CFG_GET_VAL("rlinterface.address", server_address); if (!args.Get("rl-interface").empty()) server_address = args.Get("rl-interface"); g_RLInterface = new RLInterface(); g_RLInterface->EnableHTTP(server_address.c_str()); debug_printf("RL interface listening on %s\n", server_address.c_str()); } static void RunRLServer(const bool isNonVisual, const std::vector modsToInstall, const CmdLineArgs args) { int flags = INIT_MODS; while (!Init(args, flags)) { flags &= ~INIT_MODS; Shutdown(SHUTDOWN_FROM_CONFIG); } g_Shutdown = ShutdownType::None; std::vector installedMods; if (!modsToInstall.empty()) { Paths paths(args); CModInstaller installer(paths.UserData() / "mods", paths.Cache()); // Install the mods without deleting the pyromod files for (const OsPath& modPath : modsToInstall) installer.Install(modPath, g_ScriptRuntime, true); installedMods = installer.GetInstalledMods(); } if (isNonVisual) { InitNonVisual(args); StartRLInterface(args); while (g_Shutdown == ShutdownType::None) g_RLInterface->TryApplyMessage(); QuitEngine(); } else { InitGraphics(args, 0, installedMods); MainControllerInit(); StartRLInterface(args); while (g_Shutdown == ShutdownType::None) Frame(); } Shutdown(0); MainControllerShutdown(); CXeromyces::Terminate(); delete g_RLInterface; } // moved into a helper function to ensure args is destroyed before // exit(), which may result in a memory leak. static void RunGameOrAtlas(int argc, const char* argv[]) { CmdLineArgs args(argc, argv); g_args = args; if (args.Has("version")) { debug_printf("Pyrogenesis %s\n", engine_version); return; } if (args.Has("autostart-nonvisual") && args.Get("autostart").empty() && !args.Has("rl-interface")) { LOGERROR("-autostart-nonvisual cant be used alone. A map with -autostart=\"TYPEDIR/MAPNAME\" is needed."); return; } if (args.Has("unique-logs")) g_UniqueLogPostfix = L"_" + std::to_wstring(std::time(nullptr)) + L"_" + std::to_wstring(getpid()); const bool isVisualReplay = args.Has("replay-visual"); const bool isNonVisualReplay = args.Has("replay"); const bool isNonVisual = args.Has("autostart-nonvisual"); const OsPath replayFile( isVisualReplay ? args.Get("replay-visual") : isNonVisualReplay ? args.Get("replay") : ""); if (isVisualReplay || isNonVisualReplay) { if (!FileExists(replayFile)) { debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile.string8().c_str()); return; } if (DirectoryExists(replayFile)) { debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.string8().c_str()); return; } } std::vector modsToInstall; for (const CStr& arg : args.GetArgsWithoutName()) { const OsPath modPath(arg); if (!CModInstaller::IsDefaultModExtension(modPath.Extension())) { debug_printf("Skipping file '%s' which does not have a mod file extension.\n", modPath.string8().c_str()); continue; } if (!FileExists(modPath)) { debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath.string8().c_str()); continue; } if (DirectoryExists(modPath)) { debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath.string8().c_str()); continue; } modsToInstall.emplace_back(std::move(modPath)); } // We need to initialize SpiderMonkey and libxml2 in the main thread before // any thread uses them. So initialize them here before we might run Atlas. ScriptEngine scriptEngine; CXeromyces::Startup(); if (ATLAS_RunIfOnCmdLine(args, false)) { CXeromyces::Terminate(); return; } if (isNonVisualReplay) { if (!args.Has("mod")) { LOGERROR("At least one mod should be specified! Did you mean to add the argument '-mod=public'?"); CXeromyces::Terminate(); return; } Paths paths(args); g_VFS = CreateVfs(); g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); MountMods(paths, GetMods(args, INIT_MODS)); { CReplayPlayer replay; replay.Load(replayFile); replay.Replay( args.Has("serializationtest"), args.Has("rejointest") ? args.Get("rejointest").ToInt() : -1, args.Has("ooslog"), !args.Has("hashtest-full") || args.Get("hashtest-full") == "true", args.Has("hashtest-quick") && args.Get("hashtest-quick") == "true"); } g_VFS.reset(); CXeromyces::Terminate(); return; } // run in archive-building mode if requested if (args.Has("archivebuild")) { Paths paths(args); OsPath mod(args.Get("archivebuild")); OsPath zip; if (args.Has("archivebuild-output")) zip = args.Get("archivebuild-output"); else zip = mod.Filename().ChangeExtension(L".zip"); CArchiveBuilder builder(mod, paths.Cache()); // Add mods provided on the command line // NOTE: We do not handle mods in the user mod path here std::vector mods = args.GetMultiple("mod"); for (size_t i = 0; i < mods.size(); ++i) builder.AddBaseMod(paths.RData()/"mods"/mods[i]); builder.Build(zip, args.Has("archivebuild-compress")); CXeromyces::Terminate(); return; } const double res = timer_Resolution(); g_frequencyFilter = CreateFrequencyFilter(res, 30.0); if (args.Has("rl-interface")) { RunRLServer(isNonVisual, modsToInstall, args); return; } // run the game int flags = INIT_MODS; do { g_Shutdown = ShutdownType::None; if (!Init(args, flags)) { flags &= ~INIT_MODS; Shutdown(SHUTDOWN_FROM_CONFIG); continue; } std::vector installedMods; if (!modsToInstall.empty()) { Paths paths(args); CModInstaller installer(paths.UserData() / "mods", paths.Cache()); // Install the mods without deleting the pyromod files for (const OsPath& modPath : modsToInstall) installer.Install(modPath, g_ScriptRuntime, true); installedMods = installer.GetInstalledMods(); } if (isNonVisual) { InitNonVisual(args); while (g_Shutdown == ShutdownType::None) NonVisualFrame(); } else { InitGraphics(args, 0, installedMods); MainControllerInit(); while (g_Shutdown == ShutdownType::None) Frame(); } // Do not install mods again in case of restart (typically from the mod selector) modsToInstall.clear(); Shutdown(0); MainControllerShutdown(); flags &= ~INIT_MODS; } while (g_Shutdown == ShutdownType::Restart); +#if OS_MACOSX + if (g_Shutdown == ShutdownType::RestartAsAtlas) + startNewAtlasProcess(); +#else if (g_Shutdown == ShutdownType::RestartAsAtlas) ATLAS_RunIfOnCmdLine(args, true); +#endif CXeromyces::Terminate(); } #if OS_ANDROID // In Android we compile the engine as a shared library, not an executable, // so rename main() to a different symbol that the wrapper library can load #undef main #define main pyrogenesis_main extern "C" __attribute__((visibility ("default"))) int main(int argc, char* argv[]); #endif extern "C" int main(int argc, char* argv[]) { #if OS_UNIX // Don't allow people to run the game with root permissions, // because bad things can happen, check before we do anything if (geteuid() == 0) { std::cerr << "********************************************************\n" << "WARNING: Attempted to run the game with root permission!\n" << "This is not allowed because it can alter home directory \n" << "permissions and opens your system to vulnerabilities. \n" << "(You received this message because you were either \n" <<" logged in as root or used e.g. the 'sudo' command.) \n" << "********************************************************\n\n"; return EXIT_FAILURE; } #endif // OS_UNIX EarlyInit(); // must come at beginning of main RunGameOrAtlas(argc, const_cast(argv)); // Shut down profiler initialised by EarlyInit g_Profiler2.Shutdown(); return EXIT_SUCCESS; } Index: ps/trunk/source/ps/GameSetup/HWDetect.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/HWDetect.cpp (revision 23925) +++ ps/trunk/source/ps/GameSetup/HWDetect.cpp (revision 23926) @@ -1,717 +1,718 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "scriptinterface/ScriptInterface.h" #include "lib/ogl.h" #include "lib/snd.h" #include "lib/svn_revision.h" #include "lib/timer.h" #include "lib/utf8.h" #include "lib/external_libraries/libsdl.h" #include "lib/res/graphics/ogl_tex.h" #include "lib/posix/posix_utsname.h" #include "lib/sysdep/cpu.h" #include "lib/sysdep/gfx.h" #include "lib/sysdep/numa.h" #include "lib/sysdep/os_cpu.h" #if ARCH_X86_X64 # include "lib/sysdep/arch/x86_x64/cache.h" # include "lib/sysdep/arch/x86_x64/topology.h" #endif #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/scripting/JSInterface_ConfigDB.h" #include "ps/scripting/JSInterface_Debug.h" #include "ps/UserReport.h" #include "ps/VideoMode.h" // TODO: Support OpenGL platforms which don’t use GLX as well. #if defined(SDL_VIDEO_DRIVER_X11) && !CONFIG2_GLES #include #include // Define the GLX_MESA_query_renderer macros if built with // an old Mesa (<10.0) that doesn't provide them #ifndef GLX_MESA_query_renderer #define GLX_MESA_query_renderer 1 #define GLX_RENDERER_VENDOR_ID_MESA 0x8183 #define GLX_RENDERER_DEVICE_ID_MESA 0x8184 #define GLX_RENDERER_VERSION_MESA 0x8185 #define GLX_RENDERER_ACCELERATED_MESA 0x8186 #define GLX_RENDERER_VIDEO_MEMORY_MESA 0x8187 #define GLX_RENDERER_UNIFIED_MEMORY_ARCHITECTURE_MESA 0x8188 #define GLX_RENDERER_PREFERRED_PROFILE_MESA 0x8189 #define GLX_RENDERER_OPENGL_CORE_PROFILE_VERSION_MESA 0x818A #define GLX_RENDERER_OPENGL_COMPATIBILITY_PROFILE_VERSION_MESA 0x818B #define GLX_RENDERER_OPENGL_ES_PROFILE_VERSION_MESA 0x818C #define GLX_RENDERER_OPENGL_ES2_PROFILE_VERSION_MESA 0x818D #define GLX_RENDERER_ID_MESA 0x818E #endif /* GLX_MESA_query_renderer */ #endif static void ReportSDL(const ScriptInterface& scriptInterface, JS::HandleValue settings); static void ReportGLLimits(const ScriptInterface& scriptInterface, JS::HandleValue settings); #if ARCH_X86_X64 void ConvertCaches(const ScriptInterface& scriptInterface, x86_x64::IdxCache idxCache, JS::MutableHandleValue ret) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); ScriptInterface::CreateArray(cx, ret); for (size_t idxLevel = 0; idxLevel < x86_x64::Cache::maxLevels; ++idxLevel) { const x86_x64::Cache* pcache = x86_x64::Caches(idxCache+idxLevel); if (pcache->m_Type == x86_x64::Cache::kNull || pcache->m_NumEntries == 0) continue; JS::RootedValue cache(cx); ScriptInterface::CreateObject( cx, &cache, "type", static_cast(pcache->m_Type), "level", static_cast(pcache->m_Level), "associativity", static_cast(pcache->m_Associativity), "linesize", static_cast(pcache->m_EntrySize), "sharedby", static_cast(pcache->m_SharedBy), "totalsize", static_cast(pcache->TotalSize())); scriptInterface.SetPropertyInt(ret, idxLevel, cache); } } void ConvertTLBs(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); ScriptInterface::CreateArray(cx, ret); for(size_t i = 0; ; i++) { const x86_x64::Cache* ptlb = x86_x64::Caches(x86_x64::TLB+i); if (!ptlb) break; JS::RootedValue tlb(cx); ScriptInterface::CreateObject( cx, &tlb, "type", static_cast(ptlb->m_Type), "level", static_cast(ptlb->m_Level), "associativity", static_cast(ptlb->m_Associativity), "pagesize", static_cast(ptlb->m_EntrySize), "entries", static_cast(ptlb->m_NumEntries)); scriptInterface.SetPropertyInt(ret, i, tlb); } } #endif void SetDisableAudio(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), bool disabled) { g_DisableAudio = disabled; } void RunHardwareDetection() { TIMER(L"RunHardwareDetection"); ScriptInterface scriptInterface("Engine", "HWDetect", g_ScriptRuntime); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JSI_Debug::RegisterScriptFunctions(scriptInterface); // Engine.DisplayErrorDialog JSI_ConfigDB::RegisterScriptFunctions(scriptInterface); scriptInterface.RegisterFunction("SetDisableAudio"); // Load the detection script: const wchar_t* scriptName = L"hwdetect/hwdetect.js"; CVFSFile file; if (file.Load(g_VFS, scriptName) != PSRETURN_OK) { LOGERROR("Failed to load hardware detection script"); return; } std::string code = file.DecodeUTF8(); // assume it's UTF-8 scriptInterface.LoadScript(scriptName, code); // Collect all the settings we'll pass to the script: // (We'll use this same data for the opt-in online reporting system, so it // includes some fields that aren't directly useful for the hwdetect script) JS::RootedValue settings(cx); ScriptInterface::CreateObject(cx, &settings); scriptInterface.SetProperty(settings, "os_unix", OS_UNIX); scriptInterface.SetProperty(settings, "os_bsd", OS_BSD); scriptInterface.SetProperty(settings, "os_linux", OS_LINUX); scriptInterface.SetProperty(settings, "os_android", OS_ANDROID); scriptInterface.SetProperty(settings, "os_macosx", OS_MACOSX); scriptInterface.SetProperty(settings, "os_win", OS_WIN); scriptInterface.SetProperty(settings, "arch_ia32", ARCH_IA32); scriptInterface.SetProperty(settings, "arch_amd64", ARCH_AMD64); scriptInterface.SetProperty(settings, "arch_arm", ARCH_ARM); scriptInterface.SetProperty(settings, "arch_aarch64", ARCH_AARCH64); #ifdef NDEBUG scriptInterface.SetProperty(settings, "build_debug", 0); #else scriptInterface.SetProperty(settings, "build_debug", 1); #endif scriptInterface.SetProperty(settings, "build_opengles", CONFIG2_GLES); scriptInterface.SetProperty(settings, "build_datetime", std::string(__DATE__ " " __TIME__)); scriptInterface.SetProperty(settings, "build_revision", std::wstring(svn_revision)); scriptInterface.SetProperty(settings, "build_msc", (int)MSC_VERSION); scriptInterface.SetProperty(settings, "build_icc", (int)ICC_VERSION); scriptInterface.SetProperty(settings, "build_gcc", (int)GCC_VERSION); scriptInterface.SetProperty(settings, "build_clang", (int)CLANG_VERSION); scriptInterface.SetProperty(settings, "gfx_card", gfx::CardName()); scriptInterface.SetProperty(settings, "gfx_drv_ver", gfx::DriverInfo()); scriptInterface.SetProperty(settings, "snd_card", snd_card); scriptInterface.SetProperty(settings, "snd_drv_ver", snd_drv_ver); ReportSDL(scriptInterface, settings); ReportGLLimits(scriptInterface, settings); scriptInterface.SetProperty(settings, "video_desktop_xres", g_VideoMode.GetDesktopXRes()); scriptInterface.SetProperty(settings, "video_desktop_yres", g_VideoMode.GetDesktopYRes()); scriptInterface.SetProperty(settings, "video_desktop_bpp", g_VideoMode.GetDesktopBPP()); scriptInterface.SetProperty(settings, "video_desktop_freq", g_VideoMode.GetDesktopFreq()); struct utsname un; uname(&un); scriptInterface.SetProperty(settings, "uname_sysname", std::string(un.sysname)); scriptInterface.SetProperty(settings, "uname_release", std::string(un.release)); scriptInterface.SetProperty(settings, "uname_version", std::string(un.version)); scriptInterface.SetProperty(settings, "uname_machine", std::string(un.machine)); #if OS_LINUX { std::ifstream ifs("/etc/lsb-release"); if (ifs.good()) { std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); scriptInterface.SetProperty(settings, "linux_release", str); } } #endif scriptInterface.SetProperty(settings, "cpu_identifier", std::string(cpu_IdentifierString())); scriptInterface.SetProperty(settings, "cpu_frequency", os_cpu_ClockFrequency()); scriptInterface.SetProperty(settings, "cpu_pagesize", (u32)os_cpu_PageSize()); scriptInterface.SetProperty(settings, "cpu_largepagesize", (u32)os_cpu_LargePageSize()); scriptInterface.SetProperty(settings, "cpu_numprocs", (u32)os_cpu_NumProcessors()); #if ARCH_X86_X64 scriptInterface.SetProperty(settings, "cpu_numpackages", (u32)topology::NumPackages()); scriptInterface.SetProperty(settings, "cpu_coresperpackage", (u32)topology::CoresPerPackage()); scriptInterface.SetProperty(settings, "cpu_logicalpercore", (u32)topology::LogicalPerCore()); scriptInterface.SetProperty(settings, "cpu_numcaches", (u32)topology::NumCaches()); #endif scriptInterface.SetProperty(settings, "numa_numnodes", (u32)numa_NumNodes()); scriptInterface.SetProperty(settings, "numa_factor", numa_Factor()); scriptInterface.SetProperty(settings, "numa_interleaved", numa_IsMemoryInterleaved()); scriptInterface.SetProperty(settings, "ram_total", (u32)os_cpu_MemorySize()); scriptInterface.SetProperty(settings, "ram_total_os", (u32)os_cpu_QueryMemorySize()); #if ARCH_X86_X64 scriptInterface.SetProperty(settings, "x86_vendor", (u32)x86_x64::Vendor()); scriptInterface.SetProperty(settings, "x86_model", (u32)x86_x64::Model()); scriptInterface.SetProperty(settings, "x86_family", (u32)x86_x64::Family()); u32 caps0, caps1, caps2, caps3; x86_x64::GetCapBits(&caps0, &caps1, &caps2, &caps3); scriptInterface.SetProperty(settings, "x86_caps[0]", caps0); scriptInterface.SetProperty(settings, "x86_caps[1]", caps1); scriptInterface.SetProperty(settings, "x86_caps[2]", caps2); scriptInterface.SetProperty(settings, "x86_caps[3]", caps3); JS::RootedValue tmpVal(cx); ConvertCaches(scriptInterface, x86_x64::L1I, &tmpVal); scriptInterface.SetProperty(settings, "x86_icaches", tmpVal); ConvertCaches(scriptInterface, x86_x64::L1D, &tmpVal); scriptInterface.SetProperty(settings, "x86_dcaches", tmpVal); ConvertTLBs(scriptInterface, &tmpVal); scriptInterface.SetProperty(settings, "x86_tlbs", tmpVal); #endif scriptInterface.SetProperty(settings, "timer_resolution", timer_Resolution()); // The version should be increased for every meaningful change. const int reportVersion = 13; // Send the same data to the reporting system g_UserReporter.SubmitReport( "hwdetect", reportVersion, scriptInterface.StringifyJSON(&settings, false), scriptInterface.StringifyJSON(&settings, true)); // Run the detection script: JS::RootedValue global(cx, scriptInterface.GetGlobalObject()); scriptInterface.CallFunctionVoid(global, "RunHardwareDetection", settings); } static void ReportSDL(const ScriptInterface& scriptInterface, JS::HandleValue settings) { SDL_version build, runtime; SDL_VERSION(&build); char version[16]; snprintf(version, ARRAY_SIZE(version), "%d.%d.%d", build.major, build.minor, build.patch); scriptInterface.SetProperty(settings, "sdl_build_version", version); SDL_GetVersion(&runtime); snprintf(version, ARRAY_SIZE(version), "%d.%d.%d", runtime.major, runtime.minor, runtime.patch); scriptInterface.SetProperty(settings, "sdl_runtime_version", version); - const char* backend = GetSDLSubsystem(g_VideoMode.GetWindow()); + // This is null in atlas (and further the call triggers an assertion). + const char* backend = g_VideoMode.GetWindow() ? GetSDLSubsystem(g_VideoMode.GetWindow()) : "none"; scriptInterface.SetProperty(settings, "sdl_video_backend", backend ? backend : "unknown"); } static void ReportGLLimits(const ScriptInterface& scriptInterface, JS::HandleValue settings) { const char* errstr = "(error)"; #define INTEGER(id) do { \ GLint i = -1; \ glGetIntegerv(GL_##id, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ scriptInterface.SetProperty(settings, "GL_" #id, errstr); \ else \ scriptInterface.SetProperty(settings, "GL_" #id, i); \ } while (false) #define INTEGER2(id) do { \ GLint i[2] = { -1, -1 }; \ glGetIntegerv(GL_##id, i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) { \ scriptInterface.SetProperty(settings, "GL_" #id "[0]", errstr); \ scriptInterface.SetProperty(settings, "GL_" #id "[1]", errstr); \ } else { \ scriptInterface.SetProperty(settings, "GL_" #id "[0]", i[0]); \ scriptInterface.SetProperty(settings, "GL_" #id "[1]", i[1]); \ } \ } while (false) #define FLOAT(id) do { \ GLfloat f = std::numeric_limits::quiet_NaN(); \ glGetFloatv(GL_##id, &f); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ scriptInterface.SetProperty(settings, "GL_" #id, errstr); \ else \ scriptInterface.SetProperty(settings, "GL_" #id, f); \ } while (false) #define FLOAT2(id) do { \ GLfloat f[2] = { std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN() }; \ glGetFloatv(GL_##id, f); \ if (ogl_SquelchError(GL_INVALID_ENUM)) { \ scriptInterface.SetProperty(settings, "GL_" #id "[0]", errstr); \ scriptInterface.SetProperty(settings, "GL_" #id "[1]", errstr); \ } else { \ scriptInterface.SetProperty(settings, "GL_" #id "[0]", f[0]); \ scriptInterface.SetProperty(settings, "GL_" #id "[1]", f[1]); \ } \ } while (false) #define STRING(id) do { \ const char* c = (const char*)glGetString(GL_##id); \ if (!c) c = ""; \ if (ogl_SquelchError(GL_INVALID_ENUM)) c = errstr; \ scriptInterface.SetProperty(settings, "GL_" #id, std::string(c)); \ } while (false) #define QUERY(target, pname) do { \ GLint i = -1; \ pglGetQueryivARB(GL_##target, GL_##pname, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ scriptInterface.SetProperty(settings, "GL_" #target ".GL_" #pname, errstr); \ else \ scriptInterface.SetProperty(settings, "GL_" #target ".GL_" #pname, i); \ } while (false) #define VERTEXPROGRAM(id) do { \ GLint i = -1; \ pglGetProgramivARB(GL_VERTEX_PROGRAM_ARB, GL_##id, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ scriptInterface.SetProperty(settings, "GL_VERTEX_PROGRAM_ARB.GL_" #id, errstr); \ else \ scriptInterface.SetProperty(settings, "GL_VERTEX_PROGRAM_ARB.GL_" #id, i); \ } while (false) #define FRAGMENTPROGRAM(id) do { \ GLint i = -1; \ pglGetProgramivARB(GL_FRAGMENT_PROGRAM_ARB, GL_##id, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ scriptInterface.SetProperty(settings, "GL_FRAGMENT_PROGRAM_ARB.GL_" #id, errstr); \ else \ scriptInterface.SetProperty(settings, "GL_FRAGMENT_PROGRAM_ARB.GL_" #id, i); \ } while (false) #define BOOL(id) INTEGER(id) ogl_WarnIfError(); // Core OpenGL 1.3: // (We don't bother checking extension strings for anything older than 1.3; // it'll just produce harmless warnings) STRING(VERSION); STRING(VENDOR); STRING(RENDERER); STRING(EXTENSIONS); #if !CONFIG2_GLES INTEGER(MAX_LIGHTS); INTEGER(MAX_CLIP_PLANES); // Skip MAX_COLOR_MATRIX_STACK_DEPTH (only in imaging subset) INTEGER(MAX_MODELVIEW_STACK_DEPTH); INTEGER(MAX_PROJECTION_STACK_DEPTH); INTEGER(MAX_TEXTURE_STACK_DEPTH); #endif INTEGER(SUBPIXEL_BITS); #if !CONFIG2_GLES INTEGER(MAX_3D_TEXTURE_SIZE); #endif INTEGER(MAX_TEXTURE_SIZE); INTEGER(MAX_CUBE_MAP_TEXTURE_SIZE); #if !CONFIG2_GLES INTEGER(MAX_PIXEL_MAP_TABLE); INTEGER(MAX_NAME_STACK_DEPTH); INTEGER(MAX_LIST_NESTING); INTEGER(MAX_EVAL_ORDER); #endif INTEGER2(MAX_VIEWPORT_DIMS); #if !CONFIG2_GLES INTEGER(MAX_ATTRIB_STACK_DEPTH); INTEGER(MAX_CLIENT_ATTRIB_STACK_DEPTH); INTEGER(AUX_BUFFERS); BOOL(RGBA_MODE); BOOL(INDEX_MODE); BOOL(DOUBLEBUFFER); BOOL(STEREO); #endif FLOAT2(ALIASED_POINT_SIZE_RANGE); #if !CONFIG2_GLES FLOAT2(SMOOTH_POINT_SIZE_RANGE); FLOAT(SMOOTH_POINT_SIZE_GRANULARITY); #endif FLOAT2(ALIASED_LINE_WIDTH_RANGE); #if !CONFIG2_GLES FLOAT2(SMOOTH_LINE_WIDTH_RANGE); FLOAT(SMOOTH_LINE_WIDTH_GRANULARITY); // Skip MAX_CONVOLUTION_WIDTH, MAX_CONVOLUTION_HEIGHT (only in imaging subset) INTEGER(MAX_ELEMENTS_INDICES); INTEGER(MAX_ELEMENTS_VERTICES); INTEGER(MAX_TEXTURE_UNITS); #endif INTEGER(SAMPLE_BUFFERS); INTEGER(SAMPLES); // TODO: compressed texture formats INTEGER(RED_BITS); INTEGER(GREEN_BITS); INTEGER(BLUE_BITS); INTEGER(ALPHA_BITS); #if !CONFIG2_GLES INTEGER(INDEX_BITS); #endif INTEGER(DEPTH_BITS); INTEGER(STENCIL_BITS); #if !CONFIG2_GLES INTEGER(ACCUM_RED_BITS); INTEGER(ACCUM_GREEN_BITS); INTEGER(ACCUM_BLUE_BITS); INTEGER(ACCUM_ALPHA_BITS); #endif #if !CONFIG2_GLES // Core OpenGL 2.0 (treated as extensions): if (ogl_HaveExtension("GL_EXT_texture_lod_bias")) { FLOAT(MAX_TEXTURE_LOD_BIAS_EXT); } if (ogl_HaveExtension("GL_ARB_occlusion_query")) { QUERY(SAMPLES_PASSED, QUERY_COUNTER_BITS); } if (ogl_HaveExtension("GL_ARB_shading_language_100")) { STRING(SHADING_LANGUAGE_VERSION_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_shader")) { INTEGER(MAX_VERTEX_ATTRIBS_ARB); INTEGER(MAX_VERTEX_UNIFORM_COMPONENTS_ARB); INTEGER(MAX_VARYING_FLOATS_ARB); INTEGER(MAX_COMBINED_TEXTURE_IMAGE_UNITS_ARB); INTEGER(MAX_VERTEX_TEXTURE_IMAGE_UNITS_ARB); } if (ogl_HaveExtension("GL_ARB_fragment_shader")) { INTEGER(MAX_FRAGMENT_UNIFORM_COMPONENTS_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_shader") || ogl_HaveExtension("GL_ARB_fragment_shader") || ogl_HaveExtension("GL_ARB_vertex_program") || ogl_HaveExtension("GL_ARB_fragment_program")) { INTEGER(MAX_TEXTURE_IMAGE_UNITS_ARB); INTEGER(MAX_TEXTURE_COORDS_ARB); } if (ogl_HaveExtension("GL_ARB_draw_buffers")) { INTEGER(MAX_DRAW_BUFFERS_ARB); } // Core OpenGL 3.0: if (ogl_HaveExtension("GL_EXT_gpu_shader4")) { INTEGER(MIN_PROGRAM_TEXEL_OFFSET); // no _EXT version of these in glext.h INTEGER(MAX_PROGRAM_TEXEL_OFFSET); } if (ogl_HaveExtension("GL_EXT_framebuffer_object")) { INTEGER(MAX_COLOR_ATTACHMENTS_EXT); INTEGER(MAX_RENDERBUFFER_SIZE_EXT); } if (ogl_HaveExtension("GL_EXT_framebuffer_multisample")) { INTEGER(MAX_SAMPLES_EXT); } if (ogl_HaveExtension("GL_EXT_texture_array")) { INTEGER(MAX_ARRAY_TEXTURE_LAYERS_EXT); } if (ogl_HaveExtension("GL_EXT_transform_feedback")) { INTEGER(MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS_EXT); INTEGER(MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS_EXT); INTEGER(MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS_EXT); } // Other interesting extensions: if (ogl_HaveExtension("GL_EXT_timer_query") || ogl_HaveExtension("GL_ARB_timer_query")) { QUERY(TIME_ELAPSED, QUERY_COUNTER_BITS); } if (ogl_HaveExtension("GL_ARB_timer_query")) { QUERY(TIMESTAMP, QUERY_COUNTER_BITS); } if (ogl_HaveExtension("GL_EXT_texture_filter_anisotropic")) { FLOAT(MAX_TEXTURE_MAX_ANISOTROPY_EXT); } if (ogl_HaveExtension("GL_ARB_texture_rectangle")) { INTEGER(MAX_RECTANGLE_TEXTURE_SIZE_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_program") || ogl_HaveExtension("GL_ARB_fragment_program")) { INTEGER(MAX_PROGRAM_MATRICES_ARB); INTEGER(MAX_PROGRAM_MATRIX_STACK_DEPTH_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_program")) { VERTEXPROGRAM(MAX_PROGRAM_ENV_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_LOCAL_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_TEMPORARIES_ARB); VERTEXPROGRAM(MAX_PROGRAM_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_ATTRIBS_ARB); VERTEXPROGRAM(MAX_PROGRAM_ADDRESS_REGISTERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_TEMPORARIES_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_ATTRIBS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_ADDRESS_REGISTERS_ARB); if (ogl_HaveExtension("GL_ARB_fragment_program")) { // The spec seems to say these should be supported, but // Mesa complains about them so let's not bother /* VERTEXPROGRAM(MAX_PROGRAM_ALU_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_TEX_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_TEX_INDIRECTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_ALU_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_TEX_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_TEX_INDIRECTIONS_ARB); */ } } if (ogl_HaveExtension("GL_ARB_fragment_program")) { FRAGMENTPROGRAM(MAX_PROGRAM_ENV_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_LOCAL_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_ALU_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_TEX_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_TEX_INDIRECTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_TEMPORARIES_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_ATTRIBS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_ALU_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_TEX_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_TEX_INDIRECTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_TEMPORARIES_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_ATTRIBS_ARB); if (ogl_HaveExtension("GL_ARB_vertex_program")) { // The spec seems to say these should be supported, but // Intel drivers on Windows complain about them so let's not bother /* FRAGMENTPROGRAM(MAX_PROGRAM_ADDRESS_REGISTERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_ADDRESS_REGISTERS_ARB); */ } } if (ogl_HaveExtension("GL_ARB_geometry_shader4")) { INTEGER(MAX_GEOMETRY_TEXTURE_IMAGE_UNITS_ARB); INTEGER(MAX_GEOMETRY_OUTPUT_VERTICES_ARB); INTEGER(MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS_ARB); INTEGER(MAX_GEOMETRY_UNIFORM_COMPONENTS_ARB); INTEGER(MAX_GEOMETRY_VARYING_COMPONENTS_ARB); INTEGER(MAX_VERTEX_VARYING_COMPONENTS_ARB); } #else // CONFIG2_GLES // Core OpenGL ES 2.0: STRING(SHADING_LANGUAGE_VERSION); INTEGER(MAX_VERTEX_ATTRIBS); INTEGER(MAX_VERTEX_UNIFORM_VECTORS); INTEGER(MAX_VARYING_VECTORS); INTEGER(MAX_COMBINED_TEXTURE_IMAGE_UNITS); INTEGER(MAX_VERTEX_TEXTURE_IMAGE_UNITS); INTEGER(MAX_FRAGMENT_UNIFORM_VECTORS); INTEGER(MAX_TEXTURE_IMAGE_UNITS); INTEGER(MAX_RENDERBUFFER_SIZE); #endif // CONFIG2_GLES // TODO: Support OpenGL platforms which don’t use GLX as well. #if defined(SDL_VIDEO_DRIVER_X11) && !CONFIG2_GLES #define GLXQCR_INTEGER(id) do { \ unsigned int i = UINT_MAX; \ if (pglXQueryCurrentRendererIntegerMESA(id, &i)) \ scriptInterface.SetProperty(settings, #id, i); \ } while (false) #define GLXQCR_INTEGER2(id) do { \ unsigned int i[2] = { UINT_MAX, UINT_MAX }; \ if (pglXQueryCurrentRendererIntegerMESA(id, i)) { \ scriptInterface.SetProperty(settings, #id "[0]", i[0]); \ scriptInterface.SetProperty(settings, #id "[1]", i[1]); \ } \ } while (false) #define GLXQCR_INTEGER3(id) do { \ unsigned int i[3] = { UINT_MAX, UINT_MAX, UINT_MAX }; \ if (pglXQueryCurrentRendererIntegerMESA(id, i)) { \ scriptInterface.SetProperty(settings, #id "[0]", i[0]); \ scriptInterface.SetProperty(settings, #id "[1]", i[1]); \ scriptInterface.SetProperty(settings, #id "[2]", i[2]); \ } \ } while (false) #define GLXQCR_STRING(id) do { \ const char* str = pglXQueryCurrentRendererStringMESA(id); \ if (str) \ scriptInterface.SetProperty(settings, #id ".string", str); \ } while (false) SDL_SysWMinfo wminfo; SDL_VERSION(&wminfo.version); const int ret = SDL_GetWindowWMInfo(g_VideoMode.GetWindow(), &wminfo); if (ret && wminfo.subsystem == SDL_SYSWM_X11) { Display* dpy = wminfo.info.x11.display; int scrnum = DefaultScreen(dpy); const char* glxexts = glXQueryExtensionsString(dpy, scrnum); scriptInterface.SetProperty(settings, "glx_extensions", glxexts); if (strstr(glxexts, "GLX_MESA_query_renderer") && pglXQueryCurrentRendererIntegerMESA && pglXQueryCurrentRendererStringMESA) { GLXQCR_INTEGER(GLX_RENDERER_VENDOR_ID_MESA); GLXQCR_INTEGER(GLX_RENDERER_DEVICE_ID_MESA); GLXQCR_INTEGER3(GLX_RENDERER_VERSION_MESA); GLXQCR_INTEGER(GLX_RENDERER_ACCELERATED_MESA); GLXQCR_INTEGER(GLX_RENDERER_VIDEO_MEMORY_MESA); GLXQCR_INTEGER(GLX_RENDERER_UNIFIED_MEMORY_ARCHITECTURE_MESA); GLXQCR_INTEGER(GLX_RENDERER_PREFERRED_PROFILE_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_CORE_PROFILE_VERSION_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_COMPATIBILITY_PROFILE_VERSION_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_ES_PROFILE_VERSION_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_ES2_PROFILE_VERSION_MESA); GLXQCR_STRING(GLX_RENDERER_VENDOR_ID_MESA); GLXQCR_STRING(GLX_RENDERER_DEVICE_ID_MESA); } } #endif // SDL_VIDEO_DRIVER_X11 } Index: ps/trunk/source/tools/atlas/AtlasUI/Misc/DLLInterface.cpp =================================================================== --- ps/trunk/source/tools/atlas/AtlasUI/Misc/DLLInterface.cpp (revision 23925) +++ ps/trunk/source/tools/atlas/AtlasUI/Misc/DLLInterface.cpp (revision 23926) @@ -1,360 +1,353 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "DLLInterface.h" #include "General/AtlasEventLoop.h" #include "General/Datafile.h" #include "ActorEditor/ActorEditor.h" #include "ScenarioEditor/ScenarioEditor.h" #include "GameInterface/MessagePasser.h" #include "wx/config.h" #include "wx/debugrpt.h" #include "wx/file.h" // wx and libxml both want to define ATTRIBUTE_PRINTF (with similar // meanings), so undef it to avoid a warning #undef ATTRIBUTE_PRINTF #include #ifndef LIBXML_THREAD_ENABLED #error libxml2 must have threading support enabled #endif #ifdef __WXGTK__ #include #endif // If enabled, we'll try to use wxDebugReport to report fatal exceptions. // But this is broken on Linux and can cause the UI to deadlock (see comment // in OnFatalException), and it's never especially useful, so don't use it. #define USE_WX_FATAL_EXCEPTION_REPORT 0 // Shared memory allocation functions ATLASDLLIMPEXP void* ShareableMalloc(size_t n) { // TODO: make sure this is thread-safe everywhere. (It is in MSVC with the // multithreaded CRT.) return malloc(n); } ATLASDLLIMPEXP void ShareableFree(void* p) { free(p); } // Define the function pointers that we'll use when calling those functions. // (The game loads the addresses of the above functions, then does the same.) namespace AtlasMessage { void* (*ShareableMallocFptr) (size_t) = &ShareableMalloc; void (*ShareableFreeFptr) (void*) = &ShareableFree; } // Global variables, to remember state between DllMain and StartWindow and OnInit wxString g_InitialWindowType; bool g_IsLoaded = false; #ifdef __WXMSW__ HINSTANCE g_Module; BOOL APIENTRY DllMain(HINSTANCE hModule, DWORD fdwReason, LPVOID WXUNUSED(lpReserved)) { switch (fdwReason) { case DLL_PROCESS_ATTACH: g_Module = hModule; return TRUE; case DLL_PROCESS_DETACH: if (g_IsLoaded) { wxEntryCleanup(); g_IsLoaded = false; } break; } return TRUE; } #endif // __WXMSW__ using namespace AtlasMessage; MessagePasser* AtlasMessage::g_MessagePasser = NULL; ATLASDLLIMPEXP void Atlas_SetMessagePasser(MessagePasser* passer) { g_MessagePasser = passer; } bool g_HasSetDataDirectory = false; ATLASDLLIMPEXP void Atlas_SetDataDirectory(const wchar_t* path) { Datafile::SetDataDirectory(path); g_HasSetDataDirectory = true; } wxString g_ConfigDir; ATLASDLLIMPEXP void Atlas_SetConfigDirectory(const wchar_t* path) { wxFileName config (path); g_ConfigDir = config.GetPath(wxPATH_GET_SEPARATOR); } ATLASDLLIMPEXP void Atlas_StartWindow(const wchar_t* type) { // Initialise libxml2 // (If we're executed from the game instead, it has the responsibility to initialise libxml2) LIBXML_TEST_VERSION g_InitialWindowType = type; #ifdef __WXMSW__ wxEntry(g_Module); #else #ifdef __WXGTK__ // Because we do GL calls from a secondary thread, Xlib needs to // be told to support multiple threads safely int status = XInitThreads(); if (status == 0) { fprintf(stderr, "Error enabling thread-safety via XInitThreads\n"); } #endif int argc = 1; char atlas[] = "atlas"; char *argv[] = {atlas, NULL}; #ifndef __WXOSX__ wxEntry(argc, argv); #else // Fix for OS X init (see http://trac.wildfiregames.com/ticket/2427 ) // If we launched from in-game, SDL started NSApplication which will // break some things in wxWidgets wxEntryStart(argc, argv); wxTheApp->OnInit(); wxTheApp->OnRun(); wxTheApp->OnExit(); wxEntryCleanup(); #endif #endif } ATLASDLLIMPEXP void Atlas_DisplayError(const wchar_t* text, size_t WXUNUSED(flags)) { // This is called from the game thread. // wxLog appears to be thread-safe, so that's okay. wxLogError(L"%s", text); // TODO: wait for user response (if possible) before returning, // and return their status (break/continue/debug/etc), but only in // cases where we're certain it won't deadlock (i.e. the UI event loop // is still running and won't block before showing the dialog to the user) // and where it matters (i.e. errors, not warnings (unless they're going to // turn into errors after continuing)) // TODO: 'text' (or at least some copy of it) appears to get leaked when // this function is called } class AtlasDLLApp : public wxApp { public: -#ifdef __WXOSX__ - virtual bool OSXIsGUIApplication() - { - return false; - } -#endif - virtual bool OnInit() { // _CrtSetBreakAlloc(5632); #if wxUSE_ON_FATAL_EXCEPTION && USE_WX_FATAL_EXCEPTION_REPORT if (! wxIsDebuggerRunning()) wxHandleFatalExceptions(); #endif #ifndef __WXMSW__ // On Windows we use the registry so don't attempt to set the path. // When launching a standalone executable g_ConfigDir may not be // set. In this case we default to the XDG base dir spec and use // 0ad/config/ as the config directory. wxString configPath; if (!g_ConfigDir.IsEmpty()) { configPath = g_ConfigDir; } else { wxString xdgConfigHome; if (wxGetEnv(_T("XDG_CONFIG_HOME"), &xdgConfigHome) && !xdgConfigHome.IsEmpty()) configPath = xdgConfigHome + _T("/0ad/config/"); else configPath = wxFileName::GetHomeDir() + _T("/.config/0ad/config/"); } #endif // Initialise the global config file wxConfigBase::Set(new wxConfig(_T("Atlas Editor"), _T("Wildfire Games") #ifndef __WXMSW__ // On Windows we use wxRegConfig and setting this changes the Registry key , configPath + _T("atlas.ini") #endif )); if (! g_HasSetDataDirectory) { // Assume that the .exe is located in .../binaries/system. (We can't // just use the cwd, since that isn't correct when being executed by // dragging-and-dropping onto the program in Explorer.) Datafile::SetSystemDirectory(argv[0]); } // Display the appropriate window wxFrame* frame; if (g_InitialWindowType == _T("ActorEditor")) { frame = new ActorEditor(NULL); } else if (g_InitialWindowType == _T("ScenarioEditor")) { frame = new ScenarioEditor(NULL); } else { wxFAIL_MSG(_("Internal error: invalid window type")); return false; } frame->Show(); SetTopWindow(frame); AtlasWindow* win = wxDynamicCast(frame, AtlasWindow); if (win) { // One argument => argv[1] is probably a filename to open if (argc > 1) { wxString filename = argv[1]; if (filename[0] != _T('-')) // ignore -options { if (wxFile::Exists(filename)) { win->OpenFile(filename); } else wxLogError(_("Cannot find file '%s'"), filename.c_str()); } } } return true; } #if wxUSE_DEBUGREPORT && USE_WX_FATAL_EXCEPTION_REPORT virtual void OnFatalException() { // NOTE: At least on Linux, this might be called from a thread other // than the UI thread, so it's not safe to use any wx objects here wxDebugReport report; wxDebugReportPreviewStd preview; report.AddAll(); if (preview.Show(report)) { wxString dir = report.GetDirectory(); // save the string, since it gets cleared by Process report.Process(); OpenDirectory(dir); } } #endif // wxUSE_DEBUGREPORT /* Disabled (and should be removed if it turns out to be unnecessary) - see MessagePasserImpl.cpp for information virtual int MainLoop() { // Override the default MainLoop so that we can provide our own event loop wxEventLoop* old = m_mainLoop; m_mainLoop = new AtlasEventLoop; int ret = m_mainLoop->Run(); delete m_mainLoop; m_mainLoop = old; return ret; } */ private: bool OpenDirectory(const wxString& dir) { // Open a directory on the filesystem - used so people can find the // debug report files generated in OnFatalException easily #ifdef __WXMSW__ // Code largely copied from wxLaunchDefaultBrowser: typedef HINSTANCE (WINAPI *LPShellExecute)(HWND hwnd, const wxChar* lpOperation, const wxChar* lpFile, const wxChar* lpParameters, const wxChar* lpDirectory, INT nShowCmd); HINSTANCE hShellDll = ::LoadLibrary(_T("shell32.dll")); if (hShellDll == NULL) return false; LPShellExecute lpShellExecute = (LPShellExecute) ::GetProcAddress(hShellDll, wxString(_T("ShellExecute") # ifdef _UNICODE _T("W") # else _T("A") # endif ).mb_str(wxConvLocal)); if (lpShellExecute == NULL) return false; /*HINSTANCE nResult =*/ (*lpShellExecute)(NULL, _T("explore"), dir.c_str(), NULL, NULL, SW_SHOWNORMAL); // ignore return value, since we're not going to do anything if this fails ::FreeLibrary(hShellDll); return true; #else // Figure out what goes for "default browser" on unix/linux/whatever // open an xterm perhaps? :) (void)dir; return false; #endif } }; IMPLEMENT_APP_NO_MAIN(AtlasDLLApp); Index: ps/trunk/source/tools/atlas/AtlasUI/Misc/KeyMap.cpp =================================================================== --- ps/trunk/source/tools/atlas/AtlasUI/Misc/KeyMap.cpp (revision 23925) +++ ps/trunk/source/tools/atlas/AtlasUI/Misc/KeyMap.cpp (revision 23926) @@ -1,107 +1,109 @@ -/* Copyright (C) 2013 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "KeyMap.h" #include "SDL_version.h" #include "SDL_keycode.h" int GetSDLKeyFromWxKeyCode(int wxkey) { // wx gives uppercase letters, SDL expects lowercase if (wxkey >= 'A' && wxkey <= 'Z') return wxkey + 'a' - 'A'; if (wxkey < 256) return wxkey; switch (wxkey) { case WXK_NUMPAD0: return SDLK_KP_0; case WXK_NUMPAD1: return SDLK_KP_1; case WXK_NUMPAD2: return SDLK_KP_2; case WXK_NUMPAD3: return SDLK_KP_3; case WXK_NUMPAD4: return SDLK_KP_4; case WXK_NUMPAD5: return SDLK_KP_5; case WXK_NUMPAD6: return SDLK_KP_6; case WXK_NUMPAD7: return SDLK_KP_7; case WXK_NUMPAD8: return SDLK_KP_8; case WXK_NUMPAD9: return SDLK_KP_9; case WXK_NUMPAD_DECIMAL: return SDLK_KP_PERIOD; case WXK_NUMPAD_DIVIDE: return SDLK_KP_DIVIDE; case WXK_NUMPAD_MULTIPLY: return SDLK_KP_MULTIPLY; case WXK_NUMPAD_SUBTRACT: return SDLK_KP_MINUS; case WXK_NUMPAD_ADD: return SDLK_KP_PLUS; case WXK_NUMPAD_ENTER: return SDLK_KP_ENTER; case WXK_NUMPAD_EQUAL: return SDLK_KP_EQUALS; case WXK_UP: return SDLK_UP; case WXK_DOWN: return SDLK_DOWN; case WXK_RIGHT: return SDLK_RIGHT; case WXK_LEFT: return SDLK_LEFT; case WXK_INSERT: return SDLK_INSERT; case WXK_HOME: return SDLK_HOME; case WXK_END: return SDLK_END; case WXK_PAGEUP: return SDLK_PAGEUP; case WXK_PAGEDOWN: return SDLK_PAGEDOWN; case WXK_F1: return SDLK_F1; case WXK_F2: return SDLK_F2; case WXK_F3: return SDLK_F3; case WXK_F4: return SDLK_F4; case WXK_F5: return SDLK_F5; case WXK_F6: return SDLK_F6; case WXK_F7: return SDLK_F7; case WXK_F8: return SDLK_F8; case WXK_F9: return SDLK_F9; case WXK_F10: return SDLK_F10; case WXK_F11: return SDLK_F11; case WXK_F12: return SDLK_F12; case WXK_F13: return SDLK_F13; case WXK_F14: return SDLK_F14; case WXK_F15: return SDLK_F15; + case WXK_BACK: return SDLK_BACKSPACE; + case WXK_DELETE: return SDLK_DELETE; case WXK_NUMLOCK: return SDLK_NUMLOCKCLEAR; case WXK_SCROLL: return SDLK_SCROLLLOCK; // case WXK_: return SDLK_CAPSLOCK; case WXK_SHIFT: return SDLK_RSHIFT; // case WXK_: return SDLK_LSHIFT; case WXK_CONTROL: return SDLK_RCTRL; // case WXK_: return SDLK_LCTRL; case WXK_ALT: return SDLK_RALT; // case WXK_: return SDLK_LALT; // case WXK_: return SDLK_RMETA; // case WXK_: return SDLK_LMETA; // case WXK_: return SDLK_LSUPER; // case WXK_: return SDLK_RSUPER; // case WXK_: return SDLK_MODE; // case WXK_: return SDLK_COMPOSE; case WXK_HELP: return SDLK_HELP; case WXK_PRINT: return SDLK_PRINTSCREEN; // case WXK_: return SDLK_SYSREQ; // case WXK_: return SDLK_BREAK; case WXK_MENU: return SDLK_MENU; // case WXK_: return SDLK_POWER; // case WXK_: return SDLK_EURO; // case WXK_: return SDLK_UNDO; } return 0; } Index: ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.cpp =================================================================== --- ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.cpp (revision 23925) +++ ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.cpp (revision 23926) @@ -1,1146 +1,1142 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ScenarioEditor.h" #include "wx/busyinfo.h" #include "wx/clipbrd.h" #include "wx/config.h" #include "wx/dir.h" #include "wx/evtloop.h" #include "wx/ffile.h" #include "wx/filename.h" #include "wx/image.h" #include "wx/sstream.h" #include "wx/sysopt.h" #include "wx/tooltip.h" #include "wx/xml/xml.h" #include "General/AtlasEventLoop.h" #include "General/Datafile.h" #include "CustomControls/Buttons/ToolButton.h" #include "CustomControls/Canvas/Canvas.h" #include "CustomControls/HighResTimer/HighResTimer.h" #include "CustomControls/MapDialog/MapDialog.h" #include "GameInterface/MessagePasser.h" #include "GameInterface/Messages.h" #include "Misc/KeyMap.h" #include "Tools/Common/Tools.h" #include "Tools/Common/Brushes.h" #include "Tools/Common/MiscState.h" static HighResTimer g_Timer; /** * wxWidgets only registers the double click on mousedown. */ static int g_Clicks = 1; using namespace AtlasMessage; ////////////////////////////////////////////////////////////////////////// // GL functions exported from DLL, and called by game (in a separate // thread to the standard wx one) ATLASDLLIMPEXP void Atlas_GLSetCurrent(void* canvas) { static_cast(canvas)->SetCurrent(); } ATLASDLLIMPEXP void Atlas_GLSwapBuffers(void* canvas) { static_cast(canvas)->SwapBuffers(); } ////////////////////////////////////////////////////////////////////////// class GameCanvas : public Canvas { public: GameCanvas(ScenarioEditor& scenarioEditor, wxWindow* parent, int* attribList) : Canvas(parent, attribList, wxWANTS_CHARS), m_ScenarioEditor(scenarioEditor), m_MouseState(NONE), m_LastMouseState(NONE) { } private: bool KeyScroll(wxKeyEvent& evt, bool enable) { int dir; switch (evt.GetKeyCode()) { case 'A': case WXK_LEFT: dir = eScrollConstantDir::LEFT; break; case 'D': case WXK_RIGHT: dir = eScrollConstantDir::RIGHT; break; case 'W': case WXK_UP: dir = eScrollConstantDir::FORWARDS; break; case 'S': case WXK_DOWN: dir = eScrollConstantDir::BACKWARDS; break; case 'Q': case '[': dir = eScrollConstantDir::CLOCKWISE; break; case 'E': case ']': dir = eScrollConstantDir::ANTICLOCKWISE; break; case WXK_SHIFT: case WXK_CONTROL: dir = -1; break; default: return false; } float speed = 120.f * ScenarioEditor::GetSpeedModifier(); if (dir == -1) // changed modifier keys - update all currently-scrolling directions { if (wxGetKeyState(WXK_LEFT)) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::LEFT, speed)); if (wxGetKeyState(WXK_RIGHT)) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::RIGHT, speed)); if (wxGetKeyState(WXK_UP)) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::FORWARDS, speed)); if (wxGetKeyState(WXK_DOWN)) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::BACKWARDS, speed)); if (wxGetKeyState((wxKeyCode)'[')) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::CLOCKWISE, speed)); if (wxGetKeyState((wxKeyCode)']')) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::ANTICLOCKWISE, speed)); return false; } else { POST_MESSAGE(ScrollConstant, (eRenderView::GAME, dir, enable ? speed : 0.0f)); return true; } } void OnKeyDown(wxKeyEvent& evt) { if (m_ScenarioEditor.GetToolManager().GetCurrentTool()->OnKey(evt, ITool::KEY_DOWN)) { // Key event has been handled by the tool, so don't try // to use it for camera motion too return; } if (KeyScroll(evt, true)) return; - // Slight hack: Only pass 'special' keys; normal keys will generate a translated Char event instead - if (evt.GetKeyCode() >= 256) - POST_MESSAGE(GuiKeyEvent, (GetSDLKeyFromWxKeyCode(evt.GetKeyCode()), evt.GetUnicodeKey(), true)); + POST_MESSAGE(GuiKeyEvent, (GetSDLKeyFromWxKeyCode(evt.GetKeyCode()), evt.GetUnicodeKey(), true)); evt.Skip(); } void OnKeyUp(wxKeyEvent& evt) { if (m_ScenarioEditor.GetToolManager().GetCurrentTool()->OnKey(evt, ITool::KEY_UP)) return; if (KeyScroll(evt, false)) return; - // Slight hack: Only pass 'special' keys; normal keys will generate a translated Char event instead - if (evt.GetKeyCode() >= 256) - POST_MESSAGE(GuiKeyEvent, (GetSDLKeyFromWxKeyCode(evt.GetKeyCode()), evt.GetUnicodeKey(), false)); + POST_MESSAGE(GuiKeyEvent, (GetSDLKeyFromWxKeyCode(evt.GetKeyCode()), evt.GetUnicodeKey(), false)); evt.Skip(); } void OnChar(wxKeyEvent& evt) { if (m_ScenarioEditor.GetToolManager().GetCurrentTool()->OnKey(evt, ITool::KEY_CHAR)) return; // Alt+enter toggles fullscreen if (evt.GetKeyCode() == WXK_RETURN && wxGetKeyState(WXK_ALT)) { if (m_ScenarioEditor.IsFullScreen()) m_ScenarioEditor.ShowFullScreen(false); else m_ScenarioEditor.ShowFullScreen(true, wxFULLSCREEN_NOBORDER | wxFULLSCREEN_NOCAPTION); return; } if (evt.GetKeyCode() == 'c') { POST_MESSAGE(CameraReset, ()); return; } int dir = 0; if (evt.GetKeyCode() == '-' || evt.GetKeyCode() == '_') dir = -1; else if (evt.GetKeyCode() == '+' || evt.GetKeyCode() == '=') dir = +1; // TODO: internationalisation (-/_ and +/= don't always share a key) if (dir) { float speed = 16.f * ScenarioEditor::GetSpeedModifier(); POST_MESSAGE(SmoothZoom, (eRenderView::GAME, speed*dir)); } else { - // Slight hack: Only pass 'normal' keys; special keys will generate a KeyDown/KeyUp event instead - if (evt.GetKeyCode() < 256) + // Slight hack: Only pass 'alphanumeric' keys; special keys will generate a KeyDown/KeyUp event instead + if (evt.GetKeyCode() >= 33 && evt.GetKeyCode() <= 126) POST_MESSAGE(GuiCharEvent, (GetSDLKeyFromWxKeyCode(evt.GetKeyCode()), evt.GetUnicodeKey())); evt.Skip(); } } void OnKillFocus(wxFocusEvent& evt) { // Stop any scrolling, since otherwise we'll carry on forever if // we lose focus and the KeyUp events go to a different window POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::LEFT, 0.0f)); POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::RIGHT, 0.0f)); POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::FORWARDS, 0.0f)); POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::BACKWARDS, 0.0f)); POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::CLOCKWISE, 0.0f)); POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::ANTICLOCKWISE, 0.0f)); evt.Skip(); } virtual void HandleMouseEvent(wxMouseEvent& evt) { // TODO or at least to think about: When using other controls in the // editor, it's annoying that keyboard/scrollwheel no longer navigate // around the world until you click on it. // Setting focus back whenever the mouse moves over the GL window // feels like a fairly natural solution to me, since I can use // e.g. brush-editing controls normally, and then move the mouse to // see the brush outline and magically get given back full control // of the camera. if (evt.Moving()) SetFocus(); if (m_ScenarioEditor.GetToolManager().GetCurrentTool()->OnMouse(evt)) { // Mouse event has been handled by the tool, so don't try // to use it for camera motion too return; } // Global mouse event handlers (for camera motion) if (evt.GetWheelRotation()) { float speed = 16.f * ScenarioEditor::GetSpeedModifier(); POST_MESSAGE(SmoothZoom, (eRenderView::GAME, evt.GetWheelRotation() * speed / evt.GetWheelDelta())); } else { if (evt.MiddleIsDown()) { if (wxGetKeyState(WXK_CONTROL) || evt.RightIsDown()) m_MouseState = ROTATEAROUND; else m_MouseState = SCROLL; } else m_MouseState = NONE; if (m_MouseState != m_LastMouseState) { switch (m_MouseState) { case NONE: break; case SCROLL: POST_MESSAGE(Scroll, (eRenderView::GAME, eScrollType::FROM, evt.GetPosition())); break; case ROTATEAROUND: POST_MESSAGE(RotateAround, (eRenderView::GAME, eRotateAroundType::FROM, evt.GetPosition())); break; default: wxFAIL; } m_LastMouseState = m_MouseState; } else if (evt.Dragging()) { switch (m_MouseState) { case NONE: break; case SCROLL: POST_MESSAGE(Scroll, (eRenderView::GAME, eScrollType::TO, evt.GetPosition())); break; case ROTATEAROUND: POST_MESSAGE(RotateAround, (eRenderView::GAME, eRotateAroundType::TO, evt.GetPosition())); break; default: wxFAIL; } } } // Button down and double click appear to be mutually exclusive events, // meaning a second button down event is not sent before a double click if (evt.ButtonDown() || evt.ButtonDClick()) { g_Clicks = evt.ButtonDClick() ? 2 : 1; POST_MESSAGE(GuiMouseButtonEvent, (evt.GetButton(), true, evt.GetPosition(), g_Clicks)); } else if (evt.ButtonUp()) POST_MESSAGE(GuiMouseButtonEvent, (evt.GetButton(), false, evt.GetPosition(), g_Clicks)); else if (evt.GetEventType() == wxEVT_MOTION) POST_MESSAGE(GuiMouseMotionEvent, (evt.GetPosition())); } enum { NONE, SCROLL, ROTATEAROUND }; int m_MouseState, m_LastMouseState; ScenarioEditor& m_ScenarioEditor; DECLARE_EVENT_TABLE(); }; BEGIN_EVENT_TABLE(GameCanvas, Canvas) EVT_KEY_DOWN(GameCanvas::OnKeyDown) EVT_KEY_UP(GameCanvas::OnKeyUp) EVT_CHAR(GameCanvas::OnChar) EVT_KILL_FOCUS(GameCanvas::OnKillFocus) END_EVENT_TABLE() ////////////////////////////////////////////////////////////////////////// volatile bool g_FrameHasEnded; // Called from game thread ATLASDLLIMPEXP void Atlas_NotifyEndOfFrame() { g_FrameHasEnded = true; } enum { ID_Quit = 1, - ID_New, + ID_New, ID_Open, ID_Save, ID_SaveAs, ID_ImportHeightmap, - ID_Copy, - ID_Paste, + ID_Copy, + ID_Paste, ID_Wireframe, ID_MessageTrace, ID_Screenshot, ID_BigScreenshot, ID_JavaScript, ID_CameraReset, ID_RenderPathFixed, ID_RenderPathShader, ID_DumpState, ID_DumpBinaryState, ID_Manual, ID_ReportBug, ID_Toolbar // must be last in the list }; BEGIN_EVENT_TABLE(ScenarioEditor, wxFrame) EVT_CLOSE(ScenarioEditor::OnClose) EVT_TIMER(wxID_ANY, ScenarioEditor::OnTimer) - EVT_MENU(ID_New, ScenarioEditor::OnNew) + EVT_MENU(ID_New, ScenarioEditor::OnNew) EVT_MENU(ID_Open, ScenarioEditor::OnOpen) EVT_MENU(ID_Save, ScenarioEditor::OnSave) EVT_MENU(ID_SaveAs, ScenarioEditor::OnSaveAs) EVT_MENU(ID_ImportHeightmap, ScenarioEditor::OnImportHeightmap) EVT_MENU_RANGE(wxID_FILE1, wxID_FILE9, ScenarioEditor::OnMRUFile) EVT_MENU(ID_Quit, ScenarioEditor::OnQuit) EVT_MENU(wxID_UNDO, ScenarioEditor::OnUndo) EVT_MENU(wxID_REDO, ScenarioEditor::OnRedo) - EVT_MENU(ID_Copy, ScenarioEditor::OnCopy) - EVT_MENU(ID_Paste, ScenarioEditor::OnPaste) + EVT_MENU(ID_Copy, ScenarioEditor::OnCopy) + EVT_MENU(ID_Paste, ScenarioEditor::OnPaste) EVT_MENU(ID_Wireframe, ScenarioEditor::OnWireframe) EVT_MENU(ID_MessageTrace, ScenarioEditor::OnMessageTrace) EVT_MENU(ID_Screenshot, ScenarioEditor::OnScreenshot) EVT_MENU(ID_BigScreenshot, ScenarioEditor::OnScreenshot) EVT_MENU(ID_JavaScript, ScenarioEditor::OnJavaScript) EVT_MENU(ID_CameraReset, ScenarioEditor::OnCameraReset) EVT_MENU(ID_DumpState, ScenarioEditor::OnDumpState) EVT_MENU(ID_DumpBinaryState, ScenarioEditor::OnDumpState) EVT_MENU(ID_RenderPathFixed, ScenarioEditor::OnRenderPath) EVT_MENU(ID_RenderPathShader, ScenarioEditor::OnRenderPath) EVT_MENU(ID_Manual, ScenarioEditor::OnHelp) EVT_MENU(ID_ReportBug, ScenarioEditor::OnHelp) EVT_MENU_OPEN(ScenarioEditor::OnMenuOpen) EVT_IDLE(ScenarioEditor::OnIdle) END_EVENT_TABLE() static AtlasWindowCommandProc g_CommandProc; AtlasWindowCommandProc& ScenarioEditor::GetCommandProc() { return g_CommandProc; } ScenarioEditor::ScenarioEditor(wxWindow* parent) : wxFrame(parent, wxID_ANY, _T(""), wxDefaultPosition, wxSize(1024, 768)) , m_FileHistory(_T("Scenario Editor")) , m_ObjectSettings(g_SelectedObjects, AtlasMessage::eRenderView::GAME) , m_ToolManager(this) { // Global application initialisation: wxImage::AddHandler(new wxICOHandler); /* "osx.openfiledialog.always-show-types: Per default a wxFileDialog with wxFD_OPEN does not show a types-popup on OSX but allows the selection of files from any of the supported types. Setting this to 1 shows a wxChoice for selection (if there is more than one supported filetype)." */ wxSystemOptions::SetOption(_T("osx.openfiledialog.always-show-types"), 1); // has global effect // wxLog::SetTraceMask(wxTraceMessages); g_SelectedTexture = _T("grass1_spring"); g_SelectedTexture.NotifyObservers(); SetOpenFilename(_T("")); #if defined(__WXMSW__) m_Icon = wxIcon(_T("ICON_ScenarioEditor")); // load from atlas.rc #else { const wxString relativePath (_T("tools/atlas/icons/ScenarioEditor.ico")); wxFileName filename (relativePath, wxPATH_UNIX); filename.MakeAbsolute(Datafile::GetDataDirectory()); m_Icon = wxIcon(filename.GetFullPath(), wxBITMAP_TYPE_ICO); } #endif SetIcon(m_Icon); wxToolTip::Enable(true); wxImage::AddHandler(new wxPNGHandler); ////////////////////////////////////////////////////////////////////////// // Do some early game initialisation: // (This must happen before constructing the GL canvas.) POST_MESSAGE(Init, ()); // Wait for it to finish running Init qPing qry; qry.Post(); ////////////////////////////////////////////////////////////////////////// // Menu wxMenuBar* menuBar = new wxMenuBar; SetMenuBar(menuBar); wxMenu *menuFile = new wxMenu; menuBar->Append(menuFile, _("&File")); { - menuFile->Append(ID_New, _("&New...")); + menuFile->Append(ID_New, _("&New...")); menuFile->Append(ID_Open, _("&Open...")); menuFile->Append(ID_Save, _("&Save")); menuFile->Append(ID_SaveAs, _("Save &As...")); menuFile->AppendSeparator();//----------- menuFile->Append(ID_ImportHeightmap, _("&Import Heightmap...")); menuFile->AppendSeparator();//----------- menuFile->Append(ID_Quit, _("E&xit")); m_FileHistory.UseMenu(menuFile);//------- m_FileHistory.AddFilesToMenu(); } // m_menuItem_Save = menuFile->FindItem(ID_Save); // remember this item, to let it be greyed out // wxASSERT(m_menuItem_Save); wxMenu *menuEdit = new wxMenu; menuBar->Append(menuEdit, _("&Edit")); { menuEdit->Append(wxID_UNDO, _("&Undo")); menuEdit->Append(wxID_REDO, _("&Redo")); - menuEdit->AppendSeparator(); - menuEdit->Append(ID_Copy, _("&Copy")); - menuEdit->Enable(ID_Copy, false); - menuEdit->Append(ID_Paste, _("&Paste")); - menuEdit->Enable(ID_Paste, false); + menuEdit->AppendSeparator(); + menuEdit->Append(ID_Copy, _("&Copy")); + menuEdit->Enable(ID_Copy, false); + menuEdit->Append(ID_Paste, _("&Paste")); + menuEdit->Enable(ID_Paste, false); } - g_SelectedObjects.RegisterObserver(0, &ScenarioEditor::OnSelectedObjectsChange, this); + g_SelectedObjects.RegisterObserver(0, &ScenarioEditor::OnSelectedObjectsChange, this); GetCommandProc().SetEditMenu(menuEdit); GetCommandProc().Initialize(); wxMenu *menuMisc = new wxMenu; menuBar->Append(menuMisc, _("&Misc hacks")); { menuMisc->AppendCheckItem(ID_Wireframe, _("&Wireframe")); menuMisc->AppendCheckItem(ID_MessageTrace, _("Message debug trace")); menuMisc->Append(ID_Screenshot, _("&Screenshot")); menuMisc->Append(ID_BigScreenshot, _("Big screenshot")); menuMisc->Append(ID_JavaScript, _("&JS console")); menuMisc->Append(ID_CameraReset, _("&Reset camera")); wxMenu *menuSS = new wxMenu; menuMisc->AppendSubMenu(menuSS, _("Si&mulation state")); menuSS->Append(ID_DumpState, _("&Dump to disk")); menuSS->Append(ID_DumpBinaryState, _("Dump &binary to disk")); wxMenu *menuRP = new wxMenu; menuMisc->AppendSubMenu(menuRP, _("Render &path")); menuRP->Append(ID_RenderPathFixed, _("&Fixed function")); menuRP->Append(ID_RenderPathShader, _("&Shader (default)")); } wxMenu *menuHelp = new wxMenu; menuBar->Append(menuHelp, _("&Help")); { wxFileName helpPath (_T("tools/atlas/")); helpPath.MakeAbsolute(Datafile::GetDataDirectory()); helpPath.SetFullName("help.json"); if (wxFileExists(helpPath.GetFullPath())) { wxFFile helpFile(helpPath.GetFullPath()); wxString helpData; helpFile.ReadAll(&helpData); AtObj data = AtlasObject::LoadFromJSON(std::string(helpData)); #define ADD_HELP_ITEM(id) \ do { \ if (!data[#id].hasContent()) \ break; \ if (!data[#id]["title"].hasContent() || !data[#id]["url"].hasContent()) \ break; \ menuHelp->Append(ID_##id, _(wxString(data[#id]["title"])), _(wxString(data[#id]["tooltip"]))); \ m_HelpData.insert(std::make_pair( \ ID_##id, \ HelpItem(wxString::FromUTF8(data[#id]["title"]), wxString::FromUTF8(data[#id]["tooltip"]), wxString::FromUTF8(data[#id]["url"])) \ )); \ } while (0) ADD_HELP_ITEM(Manual); ADD_HELP_ITEM(ReportBug); #undef ADD_HELP_ITEM } else wxLogError(_("'%ls' does not exist"), helpPath.GetFullPath().c_str()); } m_FileHistory.LoadFromSubDir(*wxConfigBase::Get()); m_SectionLayout.SetWindow(this); // Toolbar: // wxOSX/Cocoa 2.9 doesn't seem to like SetToolBar, so we use CreateToolBar which implicitly associates // the toolbar with the frame, and use OnCreateToolBar to construct our custom toolbar // (this should be equivalent behavior on all platforms) CreateToolBar(wxNO_BORDER|wxTB_FLAT|wxTB_HORIZONTAL, ID_Toolbar)->Realize(); // Set the default tool to be selected m_ToolManager.SetCurrentTool(_T("")); // Set up GL canvas: int glAttribList[] = { WX_GL_RGBA, WX_GL_DOUBLEBUFFER, WX_GL_DEPTH_SIZE, 24, // TODO: wx documentation doesn't say 24 is valid WX_GL_STENCIL_SIZE, 8, WX_GL_BUFFER_SIZE, 24, // color bits WX_GL_MIN_ALPHA, 8, // alpha bits 0 }; Canvas* canvas = new GameCanvas(*this, m_SectionLayout.GetCanvasParent(), glAttribList); m_SectionLayout.SetCanvas(canvas); // Set up sidebars: m_SectionLayout.Build(*this); #if defined(__WXMSW__) // The canvas' context gets made current on creation; but it can only be // current for one thread at a time, and it needs to be current for the // thread that is doing the draw calls, so disable it for this one. wglMakeCurrent(NULL, NULL); #elif defined(__WXGTK__) || defined(__WXOSX__) || defined(__WXMAC__) // Need to make sure the canvas is realised, so that its context is valid // this solves the "invalid drawable" error Show(true); Raise(); #endif #ifdef __WXGTK__ // TODO: wxSafeYield causes issues on wxOSX 2.9, is it necessary? wxSafeYield(); #endif // Send setup messages to game engine: POST_MESSAGE(InitSDL, ()); POST_MESSAGE(SetCanvas, (static_cast(canvas), canvas->GetClientSize().GetWidth(), canvas->GetClientSize().GetHeight())); POST_MESSAGE(InitGraphics, ()); canvas->InitSize(); // Start with a blank map (so that the editor can assume there's always // a valid map loaded) POST_MESSAGE(LoadMap, (_T("maps/scenarios/_default.xml"))); POST_MESSAGE(SimPlay, (0.f, false)); // Select the initial sidebar (after the map has loaded) m_SectionLayout.SelectPage(_T("MapSidebar")); // Wait for blank map qry.Post(); POST_MESSAGE(RenderEnable, (eRenderView::GAME)); // Set up a timer to make sure tool-updates happen frequently (in addition // to the idle handler (which makes them happen more frequently if there's nothing // else to do)) m_Timer.SetOwner(this); m_Timer.Start(20); } wxToolBar* ScenarioEditor::OnCreateToolBar(long style, wxWindowID id, const wxString& WXUNUSED(name)) { ToolButtonBar* toolbar = new ToolButtonBar(m_ToolManager, this, &m_SectionLayout, id, style); // TODO: configurable small vs large icon images // (button label; tooltip text; image; internal tool name; section to switch to) toolbar->AddToolButton(_("Default"), _("Default"), _T("default.png"), _T(""), _T("")); toolbar->AddToolButton(_("Move"), _("Move/rotate object"), _T("moveobject.png"), _T("TransformObject"), _T("")/*_T("ObjectSidebar")*/); toolbar->AddToolButton(_("Elevation"), _("Alter terrain elevation"), _T("alterelevation.png"), _T("AlterElevation"), _T("")/*_T("TerrainSidebar")*/); toolbar->AddToolButton(_("Smooth"), _("Smooth terrain elevation"), _T("smoothelevation.png"), _T("SmoothElevation"), _T("")/*_T("TerrainSidebar")*/); toolbar->AddToolButton(_("Flatten"), _("Flatten terrain elevation"), _T("flattenelevation.png"), _T("FlattenElevation"), _T("")/*_T("TerrainSidebar")*/); toolbar->AddToolButton(_("Paint Terrain"), _("Paint terrain texture"), _T("paintterrain.png"), _T("PaintTerrain"), _T("")/*_T("TerrainSidebar")*/); toolbar->AddToolButton(_("Move"), _("Move cinema path nodes"), _T("movepath.png"), _T("TransformPath"), _T("")/*_T("CinemaSidebar")*/); return toolbar; } float ScenarioEditor::GetSpeedModifier() { if (wxGetKeyState(WXK_SHIFT) && wxGetKeyState(WXK_CONTROL)) return 1.f/64.f; else if (wxGetKeyState(WXK_CONTROL)) return 1.f/4.f; else if (wxGetKeyState(WXK_SHIFT)) return 4.f; else return 1.f; } void ScenarioEditor::OnClose(wxCloseEvent& event) { - if (event.CanVeto() && GetCommandProc().IsDirty()) - { - if (wxMessageBox(_T("You have unsaved changes. Are you sure you want to quit?"), _T("Discard unsaved changes?"), wxICON_QUESTION | wxYES_NO) != wxYES) - { - event.Veto(); - return; - } + if (event.CanVeto() && GetCommandProc().IsDirty()) + { + if (wxMessageBox(_T("You have unsaved changes. Are you sure you want to quit?"), _T("Discard unsaved changes?"), wxICON_QUESTION | wxYES_NO) != wxYES) + { + event.Veto(); + return; + } } m_ToolManager.SetCurrentTool(_T("")); m_FileHistory.SaveToSubDir(*wxConfigBase::Get()); POST_MESSAGE(Shutdown, ()); qExit().Post(); // blocks until engine has noticed the message, so we won't be // destroying the GLCanvas while it's still rendering Destroy(); } static void UpdateTool(ToolManager& toolManager) { // Don't keep posting events if the game can't keep up if (g_FrameHasEnded) { g_FrameHasEnded = false; // (thread safety doesn't matter here) // TODO: Smoother timing stuff? static double last = g_Timer.GetTime(); double time = g_Timer.GetTime(); toolManager.GetCurrentTool()->OnTick(time-last); last = time; } } void ScenarioEditor::OnTimer(wxTimerEvent&) { UpdateTool(m_ToolManager); } void ScenarioEditor::OnIdle(wxIdleEvent&) { UpdateTool(m_ToolManager); } void ScenarioEditor::OnQuit(wxCommandEvent&) { Close(); } void ScenarioEditor::OnUndo(wxCommandEvent&) { GetCommandProc().Undo(); } void ScenarioEditor::OnRedo(wxCommandEvent&) { GetCommandProc().Redo(); } void ScenarioEditor::OnCopy(wxCommandEvent& WXUNUSED(event)) { - if (GetToolManager().GetCurrentToolName() == _T("TransformObject")) - GetToolManager().GetCurrentTool()->OnCommand(_T("copy"), NULL); + if (GetToolManager().GetCurrentToolName() == _T("TransformObject")) + GetToolManager().GetCurrentTool()->OnCommand(_T("copy"), NULL); } void ScenarioEditor::OnPaste(wxCommandEvent& WXUNUSED(event)) { - if (GetToolManager().GetCurrentToolName() != _T("TransformObject")) - GetToolManager().SetCurrentTool(_T("TransformObject"), NULL); + if (GetToolManager().GetCurrentToolName() != _T("TransformObject")) + GetToolManager().SetCurrentTool(_T("TransformObject"), NULL); - GetToolManager().GetCurrentTool()->OnCommand(_T("paste"), NULL); + GetToolManager().GetCurrentTool()->OnCommand(_T("paste"), NULL); } ////////////////////////////////////////////////////////////////////////// void ScenarioEditor::OnNew(wxCommandEvent& WXUNUSED(event)) { if (wxMessageBox(_("Discard current map and start blank new map?"), _("New map"), wxOK|wxCANCEL|wxICON_QUESTION, this) == wxOK) OpenFile(_T(""), _T("maps/scenarios/_default.xml")); } bool ScenarioEditor::OpenFile(const wxString& name, const wxString& filename) { wxBusyInfo busy(_("Loading ") + name); wxBusyCursor busyc; AtlasMessage::qVFSFileExists qry(filename.wc_str()); qry.Post(); if (!qry.exists) return false; // Deactivate tools, so they don't carry forwards into the new CWorld // and crash. m_ToolManager.SetCurrentTool(_T("")); // TODO: clear the undo buffer, etc std::wstring map(filename.wc_str()); POST_MESSAGE(LoadMap, (map)); SetOpenFilename(name); { // Wait for it to load, while the wxBusyInfo is telling the user that we're doing that qPing pingQuery; pingQuery.Post(); } NotifyOnMapReload(); GetCommandProc().ClearCommands(); return true; // TODO: Make this a non-undoable command } // TODO (eventually): replace all this file-handling stuff with the Workspace Editor void ScenarioEditor::OnOpen(wxCommandEvent& WXUNUSED(event)) { if (DiscardChangesDialog()) return; MapDialog dlg (NULL, MAPDIALOG_OPEN, m_Icon); if (dlg.ShowModal() == wxID_OK) { wxString filePath = dlg.GetSelectedFilePath(); if (!OpenFile(filePath, filePath)) wxLogError(_("Map '%ls' does not exist"), filePath.c_str()); } // TODO: Make this a non-undoable command } void ScenarioEditor::OnImportHeightmap(wxCommandEvent& WXUNUSED(event)) { if (DiscardChangesDialog()) return; wxFileDialog dlg (NULL, wxFileSelectorPromptStr, _T(""), _T(""), _T("Valid image files (*.png, *.bmp)|*.png;*.bmp|All files (*.*)|*.*"), wxFD_OPEN); // Set default filter dlg.SetFilterIndex(0); if (dlg.ShowModal() != wxID_OK) return; OpenFile(_T(""), _T("maps/scenarios/_default.xml")); std::wstring image(dlg.GetPath().wc_str()); POST_MESSAGE(ImportHeightmap, (image)); // TODO: Make this a non-undoable command } void ScenarioEditor::OnMRUFile(wxCommandEvent& event) { wxString filename(m_FileHistory.GetHistoryFile(event.GetId() - wxID_FILE1)); // Handle old MRU filenames if (filename.Mid(0, 5) != _T("maps/")) { filename = L"maps/scenarios/" + filename; m_FileHistory.RemoveFileFromHistory(event.GetId() - wxID_FILE1); } if (DiscardChangesDialog()) return; if (!OpenFile(filename, filename)) { // Missing or invalid - warn and remove from MRU wxLogError(_("Map '%ls' does not exist"), filename.c_str()); m_FileHistory.RemoveFileFromHistory(event.GetId() - wxID_FILE1); } } void ScenarioEditor::OnSave(wxCommandEvent& event) { if (m_OpenFilename.IsEmpty()) { OnSaveAs(event); } else { wxBusyInfo busy(_("Saving ") + m_OpenFilename); wxBusyCursor busyc; // Deactivate tools, so things like unit previews don't get saved. // (TODO: Would be nicer to leave the tools active, and just not save // the preview units.) m_ToolManager.SetCurrentTool(_T("")); std::wstring map(m_OpenFilename.wc_str()); POST_MESSAGE(SaveMap, (map)); // Wait for it to finish saving qPing qry; qry.Post(); - GetCommandProc().MarkAsSaved(); + GetCommandProc().MarkAsSaved(); } } void ScenarioEditor::OnSaveAs(wxCommandEvent& WXUNUSED(event)) { MapDialog dlg(NULL, MAPDIALOG_SAVE, m_Icon); if (dlg.ShowModal() == wxID_OK) { wxString filePath(dlg.GetSelectedFilePath()); wxBusyInfo busy(_("Saving ") + filePath); wxBusyCursor busyc; m_ToolManager.SetCurrentTool(_T("")); std::wstring map(filePath.wc_str()); POST_MESSAGE(SaveMap, (map)); SetOpenFilename(filePath); // Wait for it to finish saving qPing qry; qry.Post(); - GetCommandProc().MarkAsSaved(); + GetCommandProc().MarkAsSaved(); } } void ScenarioEditor::SetOpenFilename(const wxString& filename) { SetTitle(wxString::Format(_("Atlas - Scenario Editor - %s"), (filename.IsEmpty() ? wxString(_("(untitled)")) : filename).c_str())); m_OpenFilename = filename; if (! filename.IsEmpty()) m_FileHistory.AddFileToHistory(filename); } void ScenarioEditor::NotifyOnMapReload() { m_SectionLayout.OnMapReload(); // Notify observers, here so it's independent of individual panels m_MapSettings.NotifyObservers(); } bool ScenarioEditor::DiscardChangesDialog() { return GetCommandProc().IsDirty() && wxMessageBox(_T("You have unsaved changes. Are you sure you want to open another map?"), _T("Discard unsaved changes?"), wxICON_QUESTION | wxYES_NO) != wxYES; } ////////////////////////////////////////////////////////////////////////// void ScenarioEditor::OnWireframe(wxCommandEvent& event) { POST_MESSAGE(RenderStyle, (event.IsChecked())); } void ScenarioEditor::OnMessageTrace(wxCommandEvent& event) { POST_MESSAGE(MessageTrace, (event.IsChecked())); } void ScenarioEditor::OnScreenshot(wxCommandEvent& event) { switch (event.GetId()) { case ID_BigScreenshot: POST_MESSAGE(Screenshot, (true, 10)); break; case ID_Screenshot: POST_MESSAGE(Screenshot, (false, 0)); break; } } void ScenarioEditor::OnJavaScript(wxCommandEvent& WXUNUSED(event)) { wxString cmd = ::wxGetTextFromUser(_T(""), _("JS command"), _T(""), this); if (cmd.IsEmpty()) return; POST_MESSAGE(JavaScript, ((std::wstring)cmd.wc_str())); } void ScenarioEditor::OnCameraReset(wxCommandEvent& WXUNUSED(event)) { POST_MESSAGE(CameraReset, ()); } void ScenarioEditor::OnRenderPath(wxCommandEvent& event) { switch (event.GetId()) { case ID_RenderPathFixed: POST_MESSAGE(SetViewParamS, (eRenderView::GAME, L"renderpath", L"fixed")); break; case ID_RenderPathShader: POST_MESSAGE(SetViewParamS, (eRenderView::GAME, L"renderpath", L"shader")); break; } } void ScenarioEditor::OnDumpState(wxCommandEvent& event) { wxDateTime time = wxDateTime::Now(); wxString filename; bool doBinary = false; switch (event.GetId()) { case ID_DumpState: filename = wxString::Format(_T("sim-dump-%d.txt"), time.GetTicks()); break; case ID_DumpBinaryState: doBinary = true; filename = wxString::Format(_T("sim-dump-%d.dat"), time.GetTicks()); break; } qSimStateDebugDump q(doBinary); q.Post(); std::wstring dump = *q.dump; wxString state(dump.c_str()); wxFFile file(filename.c_str(), _T("w")); if (file.IsOpened() && !file.Error()) { file.Write(state); file.Close(); } else { wxLogError(_("Error writing to file '%ls'"), filename.c_str()); } } void ScenarioEditor::OnSelectedObjectsChange(const std::vector& selectedObjects) { - GetMenuBar()->Enable(ID_Copy, !selectedObjects.empty()); + GetMenuBar()->Enable(ID_Copy, !selectedObjects.empty()); } void ScenarioEditor::OnHelp(wxCommandEvent& event) { std::map::const_iterator it = m_HelpData.find(event.GetId()); if (it == m_HelpData.end()) return; wxMessageDialog* dialog = new wxMessageDialog( nullptr, _("Do you want to open '" + it->second.m_URL + "'?"), _("Atlas"), wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION ); if (dialog->ShowModal() == wxID_YES) wxLaunchDefaultBrowser(it->second.m_URL); } void ScenarioEditor::OnMenuOpen(wxMenuEvent& event) { - // Ignore wxMSW system menu events, see https://trac.wildfiregames.com/ticket/5501 - const wxMenu* menu = event.GetMenu(); - if (!menu) - return; - - // This could be done far more elegantly if wxMenuItem had changeable id. - wxMenu* pasteMenuItem = NULL; - menu->FindItem(ID_Paste, &pasteMenuItem); - - GetMenuBar()->Enable(ID_Paste, false); - - if (!pasteMenuItem) - return; - - wxString content; - if (wxTheClipboard->Open()) - { - if (wxTheClipboard->IsSupported(wxDF_TEXT)) - { - wxTextDataObject data; - wxTheClipboard->GetData(data); - content = data.GetText(); - } - - wxTheClipboard->Close(); - } - - if (content.empty()) - return; - - wxInputStream* is = new wxStringInputStream(content); - wxXmlDocument doc; - { - wxLogNull stopComplaining; - static_cast(stopComplaining); - if (!doc.Load(*is)) - return; - } - - wxXmlNode* root = doc.GetRoot(); - if (!root || root->GetName() != wxT("Entities")) - return; + // Ignore wxMSW system menu events, see https://trac.wildfiregames.com/ticket/5501 + const wxMenu* menu = event.GetMenu(); + if (!menu) + return; + + // This could be done far more elegantly if wxMenuItem had changeable id. + wxMenu* pasteMenuItem = NULL; + menu->FindItem(ID_Paste, &pasteMenuItem); + + GetMenuBar()->Enable(ID_Paste, false); + + if (!pasteMenuItem) + return; + + wxString content; + if (wxTheClipboard->Open()) + { + if (wxTheClipboard->IsSupported(wxDF_TEXT)) + { + wxTextDataObject data; + wxTheClipboard->GetData(data); + content = data.GetText(); + } + + wxTheClipboard->Close(); + } + + if (content.empty()) + return; + + wxInputStream* is = new wxStringInputStream(content); + wxXmlDocument doc; + { + wxLogNull stopComplaining; + static_cast(stopComplaining); + if (!doc.Load(*is)) + return; + } + + wxXmlNode* root = doc.GetRoot(); + if (!root || root->GetName() != wxT("Entities")) + return; - GetMenuBar()->Enable(ID_Paste, true); + GetMenuBar()->Enable(ID_Paste, true); } ////////////////////////////////////////////////////////////////////////// Position::Position(const wxPoint& pt) : type(1) { type1.x = pt.x; type1.y = pt.y; } ////////////////////////////////////////////////////////////////////////// /* Disabled (and should be removed if it turns out to be unnecessary) - see MessagePasserImpl.cpp for information static void QueryCallback() { // If this thread completely blocked on the semaphore inside Query, it would // never respond to window messages, and the system deadlocks if the // game tries to display an assertion failure dialog. (See // WaitForSingleObject on MSDN.) // So, this callback is called occasionally, and gives wx a change to // handle messages. // This is kind of like wxYield, but without the ProcessPendingEvents - // it's enough to make Windows happy and stop deadlocking, without actually // calling the event handlers (which could lead to nasty recursion) // while (wxEventLoop::GetActive()->Pending()) // wxEventLoop::GetActive()->Dispatch(); // Oh dear, we can't use that either - it (at least in wx 2.6.3) still // processes messages, which causes reentry into various things that we // don't want to be reentrant. So do it all manually, accepting Windows // messages and sticking them on a list for later processing (in a custom // event loop class): // (TODO: Rethink this entire process on Linux) // (Alt TODO: Could we make the game never pop up windows (or use the Win32 // GUI in any other way) when it's running under Atlas, so we wouldn't need // to do any message processing here at all?) #ifdef _WIN32 AtlasEventLoop* evtLoop = (AtlasEventLoop*)wxEventLoop::GetActive(); // evtLoop might be NULL, particularly if we're still initialising windows // and haven't got into the normal event loop yet. But we'd have to process // messages anyway, to avoid the deadlocks that this is for. So, don't bother // with that and just crash instead. // (Maybe it could be solved better by constructing/finding an event loop // object here and setting it as the global one, assuming it's not overwritten // later by wx.) while (evtLoop->Pending()) { // Based on src/msw/evtloop.cpp's wxEventLoop::Dispatch() MSG msg; BOOL rc = ::GetMessage(&msg, (HWND) NULL, 0, 0); if (rc == 0) { // got WM_QUIT return; } if (rc == -1) { wxLogLastError(wxT("GetMessage")); return; } // Our special bits: if (msg.message == WM_PAINT) { // "GetMessage does not remove WM_PAINT messages from the queue. // The messages remain in the queue until processed." // So let's process them, to avoid infinite loops... PAINTSTRUCT paint; ::BeginPaint(msg.hwnd, &paint); ::EndPaint(msg.hwnd, &paint); // Remember that some painting was needed - we'll just repaint // the whole screen when this is finished. evtLoop->NeedsPaint(); } else { // Add this message to a queue for later processing. (That's // probably kind of valid, at least in most cases.) MSG* pMsg = new MSG(msg); evtLoop->AddMessage(pMsg); } } #endif } */ void QueryMessage::Post() { // g_MessagePasser->Query(this, &QueryCallback); g_MessagePasser->Query(this, NULL); } Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/MiscHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/MiscHandlers.cpp (revision 23925) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/MiscHandlers.cpp (revision 23926) @@ -1,214 +1,218 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "MessageHandler.h" #include "../MessagePasserImpl.h" #include "../GameLoop.h" #include "../View.h" #include "graphics/CinemaManager.h" #include "graphics/GameView.h" #include "gui/GUIManager.h" #include "gui/CGUI.h" #include "lib/external_libraries/libsdl.h" #include "lib/ogl.h" #include "lib/sysdep/cpu.h" #include "maths/MathUtil.h" #include "ps/Game.h" #include "ps/Util.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/GameSetup.h" #include "renderer/Renderer.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpSoundManager.h" extern void (*Atlas_GLSwapBuffers)(void* context); namespace AtlasMessage { MESSAGEHANDLER(MessageTrace) { ((MessagePasserImpl*)g_MessagePasser)->SetTrace(msg->enable); } MESSAGEHANDLER(Screenshot) { if (msg->big) WriteBigScreenshot(L".bmp", msg->tiles); else WriteScreenshot(L".png"); } QUERYHANDLER(CinemaRecord) { const int w = msg->width, h = msg->height; { g_Renderer.Resize(w, h); SViewPort vp = { 0, 0, w, h }; g_Game->GetView()->SetViewport(vp); } unsigned char* img = new unsigned char [w*h*3]; unsigned char* temp = new unsigned char[w*3]; int num_frames = msg->framerate * msg->duration; AtlasView::GetView_Game()->SaveState(L"cinema_record"); // Set it to update the simulation at normal speed AtlasView::GetView_Game()->SetSpeedMultiplier(1.f); for (int frame = 0; frame < num_frames; ++frame) { AtlasView::GetView_Game()->Update(1.f / msg->framerate); Render(); Atlas_GLSwapBuffers((void*)g_AtlasGameLoop->glCanvas); glReadPixels(0, 0, w, h, GL_RGB, GL_UNSIGNED_BYTE, img); // Swap the rows around, else the image will be upside down //* // TODO: BGR24 output doesn't need flipping, YUV420 and RGBA32 do for (int y = 0; y < h/2; ++y) { memcpy(temp, &img[y*w*3], w*3); memcpy(&img[y*w*3], &img[(h-1-y)*w*3], w*3); memcpy(&img[(h-1-y)*w*3], temp, w*3); } //*/ // Call the user-supplied function with this data, so they can // store it as a video sCinemaRecordCB cbdata = { img }; msg->cb.Call(cbdata); } // Pause the game once we've finished AtlasView::GetView_Game()->SetSpeedMultiplier(0.f); AtlasView::GetView_Game()->RestoreState(L"cinema_record"); // TODO: delete the saved state now that we don't need it any more delete[] img; delete[] temp; // Restore viewport { g_Renderer.Resize(g_xres, g_yres); SViewPort vp = { 0, 0, g_xres, g_yres }; g_Game->GetView()->SetViewport(vp); } } QUERYHANDLER(Ping) { UNUSED2(msg); } MESSAGEHANDLER(SimStopMusic) { UNUSED2(msg); CmpPtr cmpSoundManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpSoundManager) cmpSoundManager->StopMusic(); } MESSAGEHANDLER(SimStateSave) { AtlasView::GetView_Game()->SaveState(*msg->label); } MESSAGEHANDLER(SimStateRestore) { AtlasView::GetView_Game()->RestoreState(*msg->label); } QUERYHANDLER(SimStateDebugDump) { msg->dump = AtlasView::GetView_Game()->DumpState(msg->binary); } MESSAGEHANDLER(SimPlay) { AtlasView::GetView_Game()->SetSpeedMultiplier(msg->speed); AtlasView::GetView_Game()->SetTesting(msg->simTest); } MESSAGEHANDLER(JavaScript) { g_GUI->GetActiveGUI()->GetScriptInterface()->LoadGlobalScript(L"Atlas", *msg->command); } MESSAGEHANDLER(GuiSwitchPage) { g_GUI->SwitchPage(*msg->page, NULL, JS::UndefinedHandleValue); } MESSAGEHANDLER(GuiMouseButtonEvent) { SDL_Event_ ev = { { 0 } }; ev.ev.type = msg->pressed ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP; ev.ev.button.button = msg->button; ev.ev.button.state = msg->pressed ? SDL_PRESSED : SDL_RELEASED; ev.ev.button.clicks = msg->clicks; float x, y; msg->pos->GetScreenSpace(x, y); ev.ev.button.x = static_cast(Clamp(x, 0, g_xres)); ev.ev.button.y = static_cast(Clamp(y, 0, g_yres)); in_dispatch_event(&ev); } MESSAGEHANDLER(GuiMouseMotionEvent) { SDL_Event_ ev = { { 0 } }; ev.ev.type = SDL_MOUSEMOTION; float x, y; msg->pos->GetScreenSpace(x, y); ev.ev.motion.x = static_cast(Clamp(x, 0, g_xres)); ev.ev.motion.y = static_cast(Clamp(y, 0, g_yres)); in_dispatch_event(&ev); } MESSAGEHANDLER(GuiKeyEvent) { SDL_Event_ ev = { { 0 } }; ev.ev.type = msg->pressed ? SDL_KEYDOWN : SDL_KEYUP; ev.ev.key.keysym.sym = (SDL_Keycode)(int)msg->sdlkey; in_dispatch_event(&ev); } MESSAGEHANDLER(GuiCharEvent) { - // wxWidgets has special Char events but SDL doesn't, so convert it to - // a keydown+keyup sequence. (We do the conversion here instead of on - // the wx side to avoid nondeterministic behaviour caused by async messaging.) - + // Simulate special 'text input' events in the SDL + // This isn't quite compatible with WXWidget's handling, + // so to avoid trouble we only send 'letter-like' ASCII input. SDL_Event_ ev = { { 0 } }; - ev.ev.type = SDL_KEYDOWN; - ev.ev.key.keysym.sym = (SDL_Keycode)(int)msg->sdlkey; + ev.ev.type = SDL_TEXTEDITING; + ev.ev.text.type = SDL_TEXTEDITING; + ev.ev.text.text[0] = (char)msg->sdlkey; + ev.ev.text.text[1] = (char)0; in_dispatch_event(&ev); - ev.ev.type = SDL_KEYUP; + ev.ev.type = SDL_TEXTINPUT; + ev.ev.text.type = SDL_TEXTINPUT; + ev.ev.text.text[0] = (char)msg->sdlkey; + ev.ev.text.text[1] = (char)0; in_dispatch_event(&ev); } }