1
0
mirror of https://github.com/OpenRCT2/OpenRCT2 synced 2026-01-25 15:54:31 +01:00

Fix emscripten support

This commit is contained in:
Ethan O'Brien
2024-12-31 09:31:33 -06:00
parent 752f169acf
commit 63b0106de8
28 changed files with 769 additions and 25 deletions

View File

@@ -17,5 +17,8 @@ file(GLOB_RECURSE OPENRCT2_CLI_SOURCES
add_executable(${PROJECT_NAME} ${OPENRCT2_CLI_SOURCES})
target_include_directories(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_LIST_DIR}/..")
ipo_set_target_properties(${PROJECT_NAME})
if (CMAKE_SYSTEM_NAME MATCHES "Emscripten")
target_link_libraries(${PROJECT_NAME} ${ICU_DT_LIBRARY_RELEASE} ${ICU_DATA_LIBRARIES})
endif ()
target_link_libraries(${PROJECT_NAME} libopenrct2 Threads::Threads)
target_link_platform_libraries(${PROJECT_NAME})

View File

@@ -11,7 +11,18 @@ option(DISABLE_VORBIS "Disable OGG/VORBIS support.")
option(DISABLE_OPENGL "Disable OpenGL support.")
# Third party libraries
if (MSVC)
if (CMAKE_SYSTEM_NAME MATCHES "Emscripten")
set(USE_FLAGS "${EMSCRIPTEN_FLAGS}")
set(SHARED_FLAGS "-fexceptions")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${USE_FLAGS} ${SHARED_FLAGS}")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${USE_FLAGS} ${SHARED_FLAGS}")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${EMSCRIPTEN_LDFLAGS} --bind ${SHARED_FLAGS}")
find_package(SpeexDSP REQUIRED)
if (NOT DISABLE_VORBIS)
PKG_CHECK_MODULES(OGG REQUIRED IMPORTED_TARGET ogg)
PKG_CHECK_MODULES(VORBISFILE REQUIRED IMPORTED_TARGET vorbisfile vorbisenc vorbis)
endif ()
elseif (MSVC)
find_package(SDL2 REQUIRED)
find_library(SPEEX_LDFLAGS libspeexdsp)
if (NOT DISABLE_FLAC)
@@ -33,7 +44,7 @@ else ()
endif ()
endif ()
if (NOT DISABLE_OPENGL)
if (NOT DISABLE_OPENGL AND NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten")
# GL doesn't work nicely with macOS, while find_package doesn't work with multiarch on Ubuntu.
if (APPLE)
find_package(OpenGL REQUIRED)
@@ -61,7 +72,12 @@ SET_CHECK_CXX_FLAGS(${PROJECT_NAME})
ipo_set_target_properties(${PROJECT_NAME})
# mingw builds cannot use the PkgConfig imported targets
if (NOT MSVC AND NOT WIN32)
if (CMAKE_SYSTEM_NAME MATCHES "Emscripten")
target_link_libraries(${PROJECT_NAME} "libopenrct2"
${SPEEXDSP_LIBRARIES}
${ICU_DATA_LIBRARIES}
${ICU_DT_LIBRARY_RELEASE})
elseif (NOT MSVC AND NOT WIN32)
target_link_libraries(${PROJECT_NAME} "libopenrct2"
PkgConfig::SDL2
PkgConfig::SPEEX)

View File

@@ -19,6 +19,10 @@
#include <openrct2/core/String.hpp>
#include <openrct2/core/UTF8.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
#ifdef __MACOSX__
// macOS uses COMMAND rather than CTRL for many keyboard shortcuts
#define KEYBOARD_PRIMARY_MODIFIER KMOD_GUI
@@ -170,7 +174,21 @@ void TextComposition::HandleMessage(const SDL_Event* e)
case SDLK_c:
if ((modifier & KEYBOARD_PRIMARY_MODIFIER) && _session.Length)
{
#ifndef __EMSCRIPTEN__
SDL_SetClipboardText(_session.Buffer->c_str());
#else
MAIN_THREAD_EM_ASM(
{
try
{
navigator.clipboard.writeText(UTF8ToString($0));
}
catch (e)
{
};
},
_session.Buffer->c_str());
#endif
ContextShowError(STR_COPY_INPUT_TO_CLIPBOARD, STR_NONE, {});
}
break;

View File

@@ -24,6 +24,10 @@
#include <openrct2/platform/Platform.h>
#include <openrct2/ui/UiContext.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
using namespace OpenRCT2;
using namespace OpenRCT2::Audio;
using namespace OpenRCT2::Ui;
@@ -43,6 +47,12 @@ int NormalisedMain(int argc, const char** argv)
int main(int argc, const char** argv)
#endif
{
#ifdef __EMSCRIPTEN__
MAIN_THREAD_EM_ASM({
specialHTMLTargets["!canvas"] = Module.canvas;
Module.canvas.addEventListener("contextmenu", (e) = > { e.preventDefault(); });
});
#endif
std::unique_ptr<IContext> context;
int32_t rc = EXIT_SUCCESS;
int runGame = CommandLineRun(argv, argc);

View File

@@ -27,6 +27,10 @@
#include <stdexcept>
#include <unistd.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
namespace OpenRCT2::Ui
{
enum class DIALOG_TYPE
@@ -129,8 +133,12 @@ namespace OpenRCT2::Ui
void OpenURL(const std::string& url) override
{
#ifndef __EMSCRIPTEN__
std::string cmd = String::stdFormat("xdg-open %s", url.c_str());
Platform::Execute(cmd);
#else
MAIN_THREAD_EM_ASM({ window.open(UTF8ToString($0)); }, url.c_str());
#endif
}
std::string ShowFileDialog(SDL_Window* window, const FileDialogDesc& desc) override

View File

@@ -49,6 +49,11 @@
#include <openrct2/world/Location.hpp>
#include <vector>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#endif
using namespace OpenRCT2;
using namespace OpenRCT2::Drawing;
using namespace OpenRCT2::Scripting;
@@ -713,7 +718,25 @@ public:
bool SetClipboardText(const utf8* target) override
{
#ifndef __EMSCRIPTEN__
return (SDL_SetClipboardText(target) == 0);
#else
return (
MAIN_THREAD_EM_ASM_INT(
{
try
{
navigator.clipboard.writeText(UTF8ToString($0));
return 0;
}
catch (e)
{
return -1;
};
},
gVersionInfoFull)
== 0);
#endif
}
ITitleSequencePlayer* GetTitleSequencePlayer() override
@@ -753,9 +776,19 @@ private:
void CreateWindow(const ScreenCoordsXY& windowPos)
{
#ifdef __EMSCRIPTEN__
MAIN_THREAD_EM_ASM({
Module.canvas.width = window.innerWidth;
Module.canvas.height = window.innerHeight;
});
int32_t width = 0;
int32_t height = 0;
emscripten_get_canvas_element_size("!canvas", &width, &height);
#else
// Get saved window size
int32_t width = Config::Get().general.WindowWidth;
int32_t height = Config::Get().general.WindowHeight;
#endif
if (width <= 0)
width = 640;
if (height <= 0)

View File

@@ -1115,6 +1115,10 @@ namespace OpenRCT2
STR_DRAWING_ENGINE_TIP = 5876,
STR_EARLY_COMPLETION_TIP = 6227,
STR_EDIT_ASSET_PACKS_BUTTON = 6640,
#ifdef __EMSCRIPTEN__
STR_EXPORT_EMSCRIPTEN = 6713,
STR_IMPORT_EMSCRIPTEN = 6714,
#endif
STR_EDIT_THEMES_BUTTON = 5153,
STR_EDIT_THEMES_BUTTON_TIP = 5837,
STR_EFFECTS_GROUP = 6256,
@@ -2282,4 +2286,4 @@ namespace OpenRCT2
STR_ADJUST_SMALLER_WATER_TIP = 2380,
STR_WATER = 2383,
};
}
} // namespace OpenRCT2

View File

@@ -42,7 +42,11 @@ OPENGL_PROC(PFNGLATTACHSHADERPROC, glAttachShader)
OPENGL_PROC(PFNGLBINDBUFFERPROC, glBindBuffer)
OPENGL_PROC(PFNGLBINDFRAGDATALOCATIONPROC, glBindFragDataLocation)
OPENGL_PROC(PFNGLBINDFRAMEBUFFERPROC, glBindFramebuffer)
#ifndef FAKE__EMSCRIPTEN__
OPENGL_PROC(PFNGLBINDVERTEXARRAYPROC, glBindVertexArray)
#else
extern "C" void glBindVertexArray(GLuint array);
#endif
OPENGL_PROC(PFNGLBLITFRAMEBUFFERPROC, glBlitFramebuffer)
OPENGL_PROC(PFNGLBUFFERDATAPROC, glBufferData)
OPENGL_PROC(PFNGLBUFFERSUBDATAPROC, glBufferSubData)
@@ -55,7 +59,11 @@ OPENGL_PROC(PFNGLDELETEBUFFERSPROC, glDeleteBuffers)
OPENGL_PROC(PFNGLDELETEFRAMEBUFFERSPROC, glDeleteFramebuffers)
OPENGL_PROC(PFNGLDELETEPROGRAMPROC, glDeleteProgram)
OPENGL_PROC(PFNGLDELETESHADERPROC, glDeleteShader)
#ifndef FAKE__EMSCRIPTEN__
OPENGL_PROC(PFNGLDELETEVERTEXARRAYSPROC, glDeleteVertexArrays)
#else
extern "C" void glDeleteVertexArrays(GLsizei n, const GLuint* arrays);
#endif
OPENGL_PROC(PFNGLDETACHSHADERPROC, glDetachShader)
OPENGL_PROC(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray)
OPENGL_PROC(PFNGLFRAMEBUFFERTEXTURE2DPROC, glFramebufferTexture2D)
@@ -67,7 +75,11 @@ OPENGL_PROC(PFNGLGETPROGRAMIVPROC, glGetProgramiv)
OPENGL_PROC(PFNGLGETSHADERINFOLOGPROC, glGetShaderInfoLog)
OPENGL_PROC(PFNGLGETSHADERIVPROC, glGetShaderiv)
OPENGL_PROC(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation)
#ifndef FAKE__EMSCRIPTEN__
OPENGL_PROC(PFNGLGENVERTEXARRAYSPROC, glGenVertexArrays)
#else
extern "C" void glGenVertexArrays(GLsizei n, GLuint* arrays);
#endif
OPENGL_PROC(PFNGLLINKPROGRAMPROC, glLinkProgram)
OPENGL_PROC(PFNGLSHADERSOURCEPROC, glShaderSource)
OPENGL_PROC(PFNGLUNIFORM1IPROC, glUniform1i)
@@ -82,6 +94,11 @@ OPENGL_PROC(PFNGLUNIFORM4FVPROC, glUniform4fv)
OPENGL_PROC(PFNGLUSEPROGRAMPROC, glUseProgram)
OPENGL_PROC(PFNGLVERTEXATTRIBIPOINTERPROC, glVertexAttribIPointer)
OPENGL_PROC(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer)
#ifndef FAKE__EMSCRIPTEN__
OPENGL_PROC(PFNGLDRAWARRAYSINSTANCEDPROC, glDrawArraysInstanced)
OPENGL_PROC(PFNGLVERTEXATTRIBDIVISORPROC, glVertexAttribDivisor)
#else
extern "C" void glDrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
extern "C" void glVertexAttribDivisor(GLuint index, GLuint divisor);
#endif
OPENGL_PROC(PFNGLBLENDFUNCSEPARATEPROC, glBlendFuncSeparate)

View File

@@ -70,7 +70,9 @@ private:
int32_t _drawCount = 0;
#ifndef NO_TTF
uint32_t _ttfGlId = 0;
#endif
struct
{

View File

@@ -605,6 +605,7 @@ static void InputViewportDragContinue()
}
}
#ifndef __EMSCRIPTEN__
const CursorState* cursorState = ContextGetCursorState();
if (cursorState->touch || Config::Get().general.InvertViewportDrag)
{
@@ -614,6 +615,9 @@ static void InputViewportDragContinue()
{
ContextSetCursorPosition(gInputDragLast);
}
#else
gInputDragLast = newDragCoords;
#endif
}
static void InputViewportDragEnd()

View File

@@ -21,6 +21,10 @@
#include <openrct2/sprites.h>
#include <openrct2/ui/UiContext.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
namespace OpenRCT2::Ui::Windows
{
static constexpr int32_t WW = 400;
@@ -119,7 +123,22 @@ namespace OpenRCT2::Ui::Windows
ContextOpenWindowView(WV_NEW_VERSION_INFO);
break;
case WIDX_COPY_BUILD_INFO:
#ifndef __EMSCRIPTEN__
SDL_SetClipboardText(gVersionInfoFull);
#else
MAIN_THREAD_EM_ASM(
{
try
{
navigator.clipboard.writeText(UTF8ToString($0));
}
catch (e)
{
// Ignore
};
},
gVersionInfoFull);
#endif
break;
case WIDX_CONTRIBUTORS_BUTTON:
ContextOpenWindowView(WV_CONTRIBUTORS);

View File

@@ -46,6 +46,10 @@
#include <openrct2/sprites.h>
#include <openrct2/ui/UiContext.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
using namespace OpenRCT2;
using namespace OpenRCT2::Audio;
@@ -218,6 +222,10 @@ namespace OpenRCT2::Ui::Windows
WIDX_PATH_TO_RCT1_BUTTON,
WIDX_PATH_TO_RCT1_CLEAR,
WIDX_ASSET_PACKS,
#ifdef __EMSCRIPTEN__
WIDX_EXPORT_EMSCRIPTEN_DATA,
WIDX_IMPORT_EMSCRIPTEN_DATA,
#endif
};
// clang-format off
@@ -404,6 +412,10 @@ namespace OpenRCT2::Ui::Windows
MakeWidget ({ 24, 160}, {266, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_NONE, STR_STRING_TOOLTIP ), // RCT 1 path button
MakeWidget ({289, 160}, { 11, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_CLOSE_X, STR_PATH_TO_RCT1_CLEAR_TIP ), // RCT 1 path clear button
MakeWidget ({150, 176}, {150, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_EDIT_ASSET_PACKS_BUTTON, STR_NONE ), // Asset packs
#ifdef __EMSCRIPTEN__
MakeWidget ({150, 192}, {150, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_EXPORT_EMSCRIPTEN, STR_NONE ), // Emscripten data export
MakeWidget ({150, 208}, {150, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_IMPORT_EMSCRIPTEN, STR_NONE ), // Emscripten data import
#endif
kWidgetsEnd,
};
@@ -1973,6 +1985,14 @@ namespace OpenRCT2::Ui::Windows
case WIDX_ASSET_PACKS:
ContextOpenWindow(WindowClass::AssetPacks);
break;
#ifdef __EMSCRIPTEN__
case WIDX_EXPORT_EMSCRIPTEN_DATA:
MAIN_THREAD_EM_ASM({ Module.funcs.export(); });
break;
case WIDX_IMPORT_EMSCRIPTEN_DATA:
MAIN_THREAD_EM_ASM({ Module.funcs.import(); });
break;
#endif
}
}

View File

@@ -114,7 +114,19 @@ if (NOT DISABLE_GOOGLE_BENCHMARK)
endif ()
# Third party libraries
if (MSVC)
if (CMAKE_SYSTEM_NAME MATCHES "Emscripten")
target_include_directories(${PROJECT_NAME} SYSTEM PRIVATE ${ICU_INCLUDE_DIR})
set(USE_FLAGS "${EMSCRIPTEN_FLAGS}")
set(SHARED_FLAGS "-fexceptions")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${USE_FLAGS} ${SHARED_FLAGS}")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${USE_FLAGS} ${SHARED_FLAGS}")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${EMSCRIPTEN_LDFLAGS} --bind ${SHARED_FLAGS}")
find_package(SpeexDSP REQUIRED)
if (NOT DISABLE_VORBIS)
PKG_CHECK_MODULES(OGG REQUIRED IMPORTED_TARGET ogg)
PKG_CHECK_MODULES(VORBISFILE REQUIRED IMPORTED_TARGET vorbisfile vorbisenc vorbis)
endif ()
elseif (MSVC)
find_package(png 1.6 REQUIRED)
find_package(zlib REQUIRED)
@@ -142,7 +154,7 @@ if (STATIC)
${ZLIB_STATIC_LIBRARIES}
${LIBZIP_STATIC_LIBRARIES})
else ()
if (NOT MSVC)
if (NOT MSVC AND NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten")
target_link_libraries(${PROJECT_NAME}
PkgConfig::PNG
PkgConfig::ZLIB
@@ -171,7 +183,7 @@ set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
target_link_libraries(${PROJECT_NAME} Threads::Threads)
if (NOT MINGW AND NOT MSVC)
if (NOT MINGW AND NOT MSVC AND NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten")
if (APPLE AND NOT MACOS_USE_DEPENDENCIES)
execute_process(COMMAND brew --prefix icu4c OUTPUT_VARIABLE HOMEBREW_PREFIX_ICU OUTPUT_STRIP_TRAILING_WHITESPACE)
# Needed for linking with non-broken icu on Apple platforms
@@ -249,7 +261,7 @@ if (NOT OPENRCT2_COMMIT_SHA1_SHORT STREQUAL "HEAD" AND NOT OPENRCT2_COMMIT_SHA1_
OPENRCT2_COMMIT_SHA1_SHORT="${OPENRCT2_COMMIT_SHA1_SHORT}")
endif()
if((X86 OR X86_64) AND NOT MSVC)
if((X86 OR X86_64) AND NOT MSVC AND NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten")
set_source_files_properties(${CMAKE_CURRENT_LIST_DIR}/drawing/SSE41Drawing.cpp PROPERTIES COMPILE_FLAGS -msse4.1)
set_source_files_properties(${CMAKE_CURRENT_LIST_DIR}/drawing/AVX2Drawing.cpp PROPERTIES COMPILE_FLAGS -mavx2)
endif()

View File

@@ -1201,9 +1201,21 @@ namespace OpenRCT2
{
SwitchToStartUpScene();
}
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop_arg(
[](void* vctx) {
auto ctx = reinterpret_cast<Context*>(vctx);
if (ctx->_finished)
{
emscripten_cancel_main_loop();
}
ctx->RunFrame();
},
this, 0, 1);
#else
_stdInOutConsole.Start();
RunGameLoop();
#endif
}
bool ShouldDraw()
@@ -1229,6 +1241,7 @@ namespace OpenRCT2
/**
* Run the main game loop until the finished flag is set.
*/
#ifndef __EMSCRIPTEN__
void RunGameLoop()
{
PROFILED_FUNCTION();
@@ -1236,22 +1249,14 @@ namespace OpenRCT2
LOG_VERBOSE("begin openrct2 loop");
_finished = false;
#ifndef __EMSCRIPTEN__
_variableFrame = ShouldRunVariableFrame();
do
{
RunFrame();
} while (!_finished);
#else
emscripten_set_main_loop_arg(
[](void* vctx) -> {
auto ctx = reinterpret_cast<Context*>(vctx);
ctx->RunFrame();
},
this, 0, 1);
#endif // __EMSCRIPTEN__
LOG_VERBOSE("finish openrct2 loop");
}
#endif // __EMSCRIPTEN__
void RunFrame()
{

View File

@@ -35,8 +35,10 @@
#elif defined(__riscv)
#define OPENRCT2_ARCHITECTURE "RISC-V"
#endif
#ifdef __EMSCRIPTEN__
#define OPENRCT2_ARCHITECTURE "Emscripten"
#ifdef __wasm32__
#define OPENRCT2_ARCHITECTURE "wasm32"
#elif defined(__wasm64__)
#define OPENRCT2_ARCHITECTURE "wasm64"
#endif
#ifndef OPENRCT2_ARCHITECTURE

View File

@@ -26,6 +26,12 @@
#include <stdexcept>
#include <unordered_map>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <iostream>
#include <sstream>
#endif
namespace OpenRCT2::Imaging
{
constexpr auto EXCEPTION_IMAGE_FORMAT_UNKNOWN = "Unknown image format.";
@@ -334,8 +340,27 @@ namespace OpenRCT2::Imaging
break;
case IMAGE_FORMAT::PNG:
{
#ifndef __EMSCRIPTEN__
std::ofstream fs(fs::u8path(path), std::ios::binary);
WritePng(fs, image);
#else
std::ostringstream stream(std::ios::binary);
WritePng(stream, image);
std::string dataStr = stream.str();
void* data = reinterpret_cast<void*>(dataStr.data());
MAIN_THREAD_EM_ASM(
{
const a = document.createElement("a");
// Blob requires the data must not be shared
const data = new Uint8Array(HEAPU8.subarray($0, $0 + $1));
a.href = URL.createObjectURL(new Blob([data]));
a.download = UTF8ToString($2).split("/").pop();
a.click();
setTimeout(function(){ URL.revokeObjectURL(a.href) }, 1000);
},
data, dataStr.size(), std::string(path).c_str());
free(data);
#endif
break;
}
default:

View File

@@ -24,7 +24,7 @@ using money64 = fixed64_1dp;
// really tries to use a gigantic constant that can't fit in a double, they are
// probably going to be breaking other things anyways.
// For more details, see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=26374
constexpr money64 operator"" _GBP(long double money) noexcept
constexpr money64 operator""_GBP(long double money) noexcept
{
return static_cast<double>(money) * 10;
}

View File

@@ -12,7 +12,7 @@
#include <cstdint>
// Note: Only valid for 5 decimal places.
constexpr int32_t operator"" _mph(long double speedMph)
constexpr int32_t operator""_mph(long double speedMph)
{
uint32_t wholeNumber = speedMph;
uint64_t fraction = (speedMph - wholeNumber) * 100000;

View File

@@ -158,7 +158,7 @@ namespace OpenRCT2::Platform
{
LOG_FATAL("failed to get process path");
}
#elif defined(__OpenBSD__)
#elif defined(__OpenBSD__) || defined(__EMSCRIPTEN__)
// There is no way to get the path name of a running executable.
// If you are not using the port or package, you may have to change this line!
strlcpy(exePath, "/usr/local/bin/", sizeof(exePath));

View File

@@ -161,7 +161,7 @@ namespace OpenRCT2::Platform
// Return exit code
return pclose(fpipe);
#else
LOG_WARNING("Emscripten cannot execute processes. The commandline was '%s'.", command.c_str());
LOG_WARNING("Emscripten cannot execute processes. The commandline was '%s'.", std::string(command).c_str());
return -1;
#endif // __EMSCRIPTEN__
}

View File

@@ -13,7 +13,7 @@
using namespace OpenRCT2;
constexpr int operator"" _MPH(unsigned long long x) noexcept
constexpr int operator""_MPH(unsigned long long x) noexcept
{
return x * 29127;
}