diff --git a/AI/Interfaces/C/CMakeLists.txt b/AI/Interfaces/C/CMakeLists.txt index 5b2d4b5..f2c28d4 100644 --- a/AI/Interfaces/C/CMakeLists.txt +++ b/AI/Interfaces/C/CMakeLists.txt @@ -45,7 +45,13 @@ macro (configure_native_skirmish_ai mySourceDirRel_var additionalSources_var get_native_sources_recursive(mySources "${mySourceDir}" "${myDir}") # Compile the library - add_library(${myTarget} MODULE ${mySources} ${additionalSources} ${myVersionDepFile}) + if(EMSCRIPTEN) + # This vendored developer tool has a main() and is not part of the AI. + list(FILTER mySources EXCLUDE REGEX "/autowrapper/generator/") + add_library(${myTarget} STATIC ${mySources} ${myVersionDepFile}) + else() + add_library(${myTarget} MODULE ${mySources} ${additionalSources} ${myVersionDepFile}) + endif() set_target_properties(${myTarget} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/data) fix_lib_name(${myTarget}) diff --git a/CMakeLists.txt b/CMakeLists.txt index 49e7b0a..8ffd9c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,15 @@ endif (APPLE) ### Compiler flags and defines based on build type include(TestCXXFlags) +option(RECOIL_EXPERIMENTAL_WASM "Allow unsupported WebAssembly configure experiments" OFF) +if(EMSCRIPTEN AND RECOIL_EXPERIMENTAL_WASM) + set(MARCH_FLAG "generic" CACHE STRING "CPU optimization") + math(EXPR WASM_BUILD_BITS "${CMAKE_SIZEOF_VOID_P} * 8") + set(MARCH_BITS ${WASM_BUILD_BITS} CACHE INTERNAL "" FORCE) + set(BUILD_BITS ${WASM_BUILD_BITS} CACHE INTERNAL "Target architecture bitness" FORCE) + set(IS_ARM64_BUILD FALSE) + message(WARNING "Experimental WASM${WASM_BUILD_BITS}: full engine support is not implemented") +else() ## 64-bit architecture check set(MARCH_FLAG ${MARCH} CACHE STRING "CPU optimization (use `generic` for generic optimization)") if (NOT CMAKE_SIZEOF_VOID_P EQUAL 8) @@ -122,6 +131,8 @@ message(STATUS "Building Spring on a 64-bit environment") set(BUILD_BITS 64 CACHE INTERNAL "Target architecture bitness" FORCE) +endif() + ### Install paths (relative to CMAKE_INSTALL_PREFIX) if (UNIX AND NOT MINGW) if (INSTALL_PORTABLE) @@ -500,7 +511,9 @@ elseif (MSVC) add_definitions(-D_SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS) elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") # Clang on ARM64: no SSE flags needed, use armv8-a with NEON SIMD - if (IS_ARM64_BUILD) + if (EMSCRIPTEN) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ffp-contract=off") + elseif (IS_ARM64_BUILD) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${FP_CONTRACT_FLAG}") else() # Clang on x86_64: use SSE like GCC @@ -540,7 +553,9 @@ else (MSVC) elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") # Clang does not support -fsingle-precision-constant. # -frounding-math is supported on Clang 14+; use it if available. - check_and_add_flags(CMAKE_CXX_FLAGS "-frounding-math") + if(NOT EMSCRIPTEN) + check_and_add_flags(CMAKE_CXX_FLAGS "-frounding-math") + endif() endif() # Experiment with frame pointers to see whether it improves the quality of the stack unwinds. # We're having trouble getting meaningful information form crash reports. Trying this before diff --git a/cont/base/CMakeLists.txt b/cont/base/CMakeLists.txt index 18a7048..bbeafda 100644 --- a/cont/base/CMakeLists.txt +++ b/cont/base/CMakeLists.txt @@ -4,7 +4,9 @@ # * ${BUILD_DIR}/maphelper.sdz # * ${BUILD_DIR}/cursors.sdz -find_package(SevenZip REQUIRED) +if(NOT EMSCRIPTEN) + find_package(SevenZip REQUIRED) +endif() add_custom_target(basecontent ALL) @@ -16,9 +18,14 @@ macro(create_base_content_archive outputdir filename files) set(QUIET ">nul") endif() set(outputfile "${Spring_BINARY_DIR}/${outputdir}/${filename}") + if(EMSCRIPTEN) + set(archive_command ${CMAKE_COMMAND} -E tar cf ${outputfile} --format=zip -- ${files}) + else() + set(archive_command ${SEVENZIP_BIN} a -tzip ${outputfile} ${files} ${QUIET}) + endif() add_custom_command( OUTPUT "${outputfile}" - COMMAND ${SEVENZIP_BIN} a -tzip ${outputfile} ${files} ${QUIET} + COMMAND ${archive_command} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/ COMMENT "Creating ${outputfile}" DEPENDS ${files} @@ -46,4 +53,4 @@ add_subdirectory(cursors) install(FILES "RecoilEngine_4K.png" DESTINATION "${DATADIR}/base") -#install(FILES "fontscache.bmp" DESTINATION "${DATADIR}/base") \ No newline at end of file +#install(FILES "fontscache.bmp" DESTINATION "${DATADIR}/base") diff --git a/rts/CMakeLists.txt b/rts/CMakeLists.txt index ae47f2a..aee231a 100644 --- a/rts/CMakeLists.txt +++ b/rts/CMakeLists.txt @@ -106,7 +106,7 @@ if (USE_MIMALLOC) endif (USE_MIMALLOC) -if(UNIX AND NOT (CMAKE_SYSTEM_NAME MATCHES "OpenBSD")) +if(UNIX AND NOT EMSCRIPTEN AND NOT (CMAKE_SYSTEM_NAME MATCHES "OpenBSD")) find_package_static(Libunwind 1.4.0 REQUIRED) prefer_static_libs() find_library(LZMA_LIBRARY lzma) diff --git a/rts/ExternalAI/AIInterfaceLibrary.cpp b/rts/ExternalAI/AIInterfaceLibrary.cpp index d738066..a6e8fc9 100644 --- a/rts/ExternalAI/AIInterfaceLibrary.cpp +++ b/rts/ExternalAI/AIInterfaceLibrary.cpp @@ -13,6 +13,10 @@ #include "System/SafeUtil.h" #include "AILibraryManager.h" +#ifdef __EMSCRIPTEN__ +#include "WebbarStaticAI.h" +#endif + CAIInterfaceLibrary::CAIInterfaceLibrary(const CAIInterfaceLibraryInfo& _info) : interfaceId(-1) @@ -20,6 +24,16 @@ CAIInterfaceLibrary::CAIInterfaceLibrary(const CAIInterfaceLibraryInfo& _info) , sAIInterfaceLibrary({ nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}) , info(_info) { + sharedLib = nullptr; +#ifdef __EMSCRIPTEN__ + if (info.GetKey().GetShortName() == "C") { + sAIInterfaceLibrary.loadSkirmishAILibrary = WebbarLoadBARb; + sAIInterfaceLibrary.unloadSkirmishAILibrary = [](const char*, const char*) { return 0; }; + sAIInterfaceLibrary.unloadAllSkirmishAILibraries = []() { return 0; }; + initialized = true; + return; + } +#endif libFilePath = FindLibFile(); sharedLib = SharedLib::Instantiate(libFilePath); diff --git a/rts/Game/Game.cpp b/rts/Game/Game.cpp index 63701b2..0f09347 100644 --- a/rts/Game/Game.cpp +++ b/rts/Game/Game.cpp @@ -1,6 +1,7 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ #include "Rendering/GL/myGL.h" +#include #include #include @@ -65,6 +66,7 @@ #include "Map/MapDamage.h" #include "Map/MapInfo.h" #include "Map/ReadMap.h" +#include "Map/Ground.h" #include "Net/GameServer.h" #include "Net/Protocol/NetProtocol.h" #include "Sim/Ecs/Registry.h" @@ -882,6 +884,12 @@ void CGame::LoadLua(bool dryRun, bool onlyUnsynced) LEAVE_SYNCED_CODE(); if (!dryRun) { + #if defined(__EMSCRIPTEN__) + // Browser practice supplies its own UI. Native scenario widgets would + // otherwise pause at an invisible mission briefing on frame zero. + if (std::getenv("WEBBAR_PLAYABLE") != nullptr) + return; + #endif loadscreen->SetLoadMessage("Loading LuaUI"); auto lock = CLoadLock::GetUniqueLock(); CLuaUI::LoadFreeHandler(); @@ -1175,11 +1183,18 @@ int CGame::TextEditing(const std::string& utf8Text, unsigned int start, unsigned } +#if defined(__EMSCRIPTEN__) +static void PollWebbarCommands(); +#endif + bool CGame::Update() { RECOIL_DETAILED_TRACY_ZONE; good_fpu_control_registers("CGame::Update"); + #if defined(__EMSCRIPTEN__) + PollWebbarCommands(); + #endif jobDispatcher.Update(); clientNet->Update(); @@ -1692,6 +1707,10 @@ void CGame::StartPlaying() eventHandler.GameStart(); } +#if defined(__EMSCRIPTEN__) +#include "WebbarBrowser.h" +#endif + static const char* const tracingSimFrameName = "SimFrame"; void CGame::SimFrame() { @@ -1840,6 +1859,9 @@ void CGame::SimFrame() { ASSERT_SYNCED(gsRNG.GetGenState()); LEAVE_SYNCED_CODE(); +#if defined(__EMSCRIPTEN__) + PublishWebbarFrame(); +#endif } @@ -2224,4 +2246,3 @@ const ActionList& CGame::GetLastActionList() { return gameInputReceiver.lastActionList; } - diff --git a/rts/Game/LoadScreen.cpp b/rts/Game/LoadScreen.cpp index eb9ec9e..0bd5614 100644 --- a/rts/Game/LoadScreen.cpp +++ b/rts/Game/LoadScreen.cpp @@ -5,6 +5,10 @@ #include "Rendering/GL/myGL.h" #include "LoadScreen.h" +#ifdef __EMSCRIPTEN__ +#include +#include +#endif #include "Game.h" #include "GlobalUnsynced.h" #include "Game/Players/Player.h" @@ -343,6 +347,10 @@ void CLoadScreen::SetLoadMessage(const std::string& text, bool replaceLast) LOG("[LoadScreen::%s] text=\"%s\"", __func__, text.c_str()); LOG_CLEANUP(); +#ifdef __EMSCRIPTEN__ + const auto allocation = mallinfo(); + LOG("[WebbarMemory] stage=\"%s\" live=%u free=%u heap=%u", text.c_str(), uint32_t(allocation.uordblks), uint32_t(allocation.fordblks), uint32_t(emscripten_get_heap_size())); +#endif // be paranoid about FPU state for the loading thread since some // external library might reset it (main thread state is checked diff --git a/rts/Map/SMF/SMFGroundDrawer.cpp b/rts/Map/SMF/SMFGroundDrawer.cpp index 0515c31..0b912ae 100644 --- a/rts/Map/SMF/SMFGroundDrawer.cpp +++ b/rts/Map/SMF/SMFGroundDrawer.cpp @@ -141,8 +141,24 @@ CSMFGroundDrawer::~CSMFGroundDrawer() +#if defined(__EMSCRIPTEN__) && defined(HEADLESS) +// Retain the drawer interface for unsynced Lua and height notifications without +// allocating the desktop ROAM mesh. The browser has its own terrain geometry. +class WebbarHeadlessMesh final: public IMeshDrawer +{ +public: + void Update() override {} + void DrawMesh(const DrawPass::e&) override {} + void DrawBorderMesh(const DrawPass::e&) override {} +}; +#endif + IMeshDrawer* CSMFGroundDrawer::SwitchMeshDrawer(int wantedMode) { +#if defined(__EMSCRIPTEN__) && defined(HEADLESS) + if (meshDrawer == nullptr) meshDrawer = new WebbarHeadlessMesh(); + return meshDrawer; +#endif RECOIL_DETAILED_TRACY_ZONE; // toggle if (wantedMode <= -1) { diff --git a/rts/Map/SMF/SMFGroundTextures.cpp b/rts/Map/SMF/SMFGroundTextures.cpp index 633ceb0..f1247ad 100644 --- a/rts/Map/SMF/SMFGroundTextures.cpp +++ b/rts/Map/SMF/SMFGroundTextures.cpp @@ -74,6 +74,14 @@ CSMFGroundTextures::CSMFGroundTextures(CSMFReadMap* rm): smfMap(rm) smfTextureStreaming = configHandler->GetBool("SMFTextureStreaming"); smfTextureLodBias = configHandler->GetFloat("SMFTextureLodBias"); +#if defined(__EMSCRIPTEN__) && defined(HEADLESS) + // The browser renders terrain separately. Preserve square metadata for Lua, + // but do not decompress SMT tiles or allocate desktop texture staging memory. + smfTextureStreaming = false; + squares.clear(); + squares.resize(smfMap->numBigTexX * smfMap->numBigTexY); + return; +#endif LoadTiles(smfMap->GetMapFile()); if (smfTextureStreaming) { LoadSquareTextures(3); diff --git a/rts/Map/SMF/SMFReadMap.cpp b/rts/Map/SMF/SMFReadMap.cpp index 436e7e2..4073c21 100644 --- a/rts/Map/SMF/SMFReadMap.cpp +++ b/rts/Map/SMF/SMFReadMap.cpp @@ -82,6 +82,9 @@ CSMFReadMap::CSMFReadMap(const std::string& mapName): CEventClient("[CSMFReadMap LoadHeightMap(); CReadMap::Initialize(); +#if !defined(__EMSCRIPTEN__) || !defined(HEADLESS) + // Desktop graphics resources are unnecessary in the browser simulation host. + // Synced terrain, normals, features and collision data above stay intact. ConfigureTexAnisotropyLevels(); { auto lock = CLoadLock::GetUniqueLock(); @@ -97,6 +100,7 @@ CSMFReadMap::CSMFReadMap(const std::string& mapName): CEventClient("[CSMFReadMap CreateHeightMapTex(); CreateShadingGL(); } +#endif mapFile.ReadFeatureInfo(); } @@ -519,6 +523,9 @@ void CSMFReadMap::UpdateCornerHeightMapUnsynced(const SRectangle& update) void CSMFReadMap::UpdateHeightMapTexture(const SRectangle& update) { +#if defined(__EMSCRIPTEN__) && defined(HEADLESS) + return; // Browser graphics are owned by JavaScript, not the headless GL shim. +#endif // consider full update if the area of update is >= 50% of full update const auto refFullUpdateThreshold = (mapDims.mapx * mapDims.mapy) >> 1; if (update.GetArea() >= refFullUpdateThreshold) { @@ -704,6 +711,9 @@ void CSMFReadMap::UpdateShadingTexture() void CSMFReadMap::UpdateVisNormalsAndShadingTexture(const SRectangle& update) { +#if defined(__EMSCRIPTEN__) && defined(HEADLESS) + return; // Browser graphics are owned by JavaScript, not the headless GL shim. +#endif RECOIL_DETAILED_TRACY_ZONE; #ifndef HEADLESS @@ -766,6 +776,9 @@ void CSMFReadMap::SunChanged() void CSMFReadMap::ReloadTextures() { +#if defined(__EMSCRIPTEN__) && defined(HEADLESS) + return; // Browser graphics are owned by JavaScript, not the headless GL shim. +#endif RECOIL_DETAILED_TRACY_ZONE; const auto ReloadTextureFunc = [](const std::string& texName, MapTexture& mt, float aniso = 0.0f, float lodBias = 0.0f, bool mipmaps = false) { /// perhaps *mt.GetIDPtr() == 0 should not be reloaded diff --git a/rts/Rendering/Common/ModelDrawer.h b/rts/Rendering/Common/ModelDrawer.h index b71b05b..bad99db 100644 --- a/rts/Rendering/Common/ModelDrawer.h +++ b/rts/Rendering/Common/ModelDrawer.h @@ -1,3 +1,4 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ #pragma once #include @@ -10,6 +11,7 @@ #include "ModelDrawerState.hpp" #include "ModelDrawerHelpers.h" #include "System/Log/ILog.h" +#include "System/TimeProfiler.h" #include "System/TypeToStr.h" #include "Rendering/LuaObjectDrawer.h" #include "Rendering/GL/LightHandler.h" diff --git a/rts/Rendering/Textures/Bitmap.cpp b/rts/Rendering/Textures/Bitmap.cpp index db5a706..18efc5d 100644 --- a/rts/Rendering/Textures/Bitmap.cpp +++ b/rts/Rendering/Textures/Bitmap.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -2133,4 +2134,4 @@ void CBitmap::ReverseYAxis() ITexMemPool::texMemPool->Free(tmp, memSize); #endif -} \ No newline at end of file +} diff --git a/rts/Rendering/Textures/TextureAtlas.cpp b/rts/Rendering/Textures/TextureAtlas.cpp index a5dab62..68f43df 100644 --- a/rts/Rendering/Textures/TextureAtlas.cpp +++ b/rts/Rendering/Textures/TextureAtlas.cpp @@ -119,10 +119,16 @@ size_t CTextureAtlas::AddTexFromFile(std::string texName, const std::string& fil } CBitmap bitmap; +#if defined(__EMSCRIPTEN__) && defined(HEADLESS) + // Keep named atlas entries for CEG/weapon metadata. Pixel decoding and the + // desktop texture copy are unnecessary in the WASM simulation host. + bitmap.AllocDummy(); +#else if (!bitmap.Load(file)) { bitmap.Alloc(2, 2, 4); LOG_L(L_WARNING, "[TexAtlas::%s] could not load texture from file \"%s\"", __func__, file.c_str()); } +#endif // only support RGBA for now if (bitmap.channels != 4 || bitmap.compressed) diff --git a/rts/Sim/CMakeLists.txt b/rts/Sim/CMakeLists.txt index 882f06b..b113b73 100644 --- a/rts/Sim/CMakeLists.txt +++ b/rts/Sim/CMakeLists.txt @@ -164,7 +164,7 @@ if(ENABLE_STREFLOP) target_link_libraries(engineSim streflop) endif() -if( CMAKE_COMPILER_IS_GNUCXX) +if(CMAKE_COMPILER_IS_GNUCXX OR EMSCRIPTEN) # FIXME: hack to avoid linkers to remove not referenced symbols. required because of # https://springrts.com/mantis/view.php?id=4511 if(APPLE) diff --git a/rts/Sim/Misc/GlobalSynced.cpp b/rts/Sim/Misc/GlobalSynced.cpp index 3c4f5d6..86b59c3 100644 --- a/rts/Sim/Misc/GlobalSynced.cpp +++ b/rts/Sim/Misc/GlobalSynced.cpp @@ -12,6 +12,7 @@ #include "Sim/Misc/TeamHandler.h" #include "Sim/Misc/GlobalConstants.h" #include "System/SafeUtil.h" +#include "System/Misc/TracyDefs.h" #include "System/Log/FramePrefixer.h" #ifdef SYNCCHECK @@ -97,4 +98,3 @@ void CGlobalSynced::LoadFromSetup(const CGameSetup* setup) skirmishAIHandler.ResetState(); skirmishAIHandler.LoadFromSetup(*setup); } - diff --git a/rts/Sim/Misc/LosMap.cpp b/rts/Sim/Misc/LosMap.cpp index 57cc5d5..6e1845b 100644 --- a/rts/Sim/Misc/LosMap.cpp +++ b/rts/Sim/Misc/LosMap.cpp @@ -4,6 +4,7 @@ #include #include "LosMap.h" +#include "System/Misc/TracyDefs.h" #include "LosHandler.h" #include "Map/ReadMap.h" #include "System/SpringMath.h" diff --git a/rts/Sim/Misc/SimObjectIDPool.cpp b/rts/Sim/Misc/SimObjectIDPool.cpp index 89b2d44..8a6bae9 100644 --- a/rts/Sim/Misc/SimObjectIDPool.cpp +++ b/rts/Sim/Misc/SimObjectIDPool.cpp @@ -1,6 +1,7 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ #include "SimObjectIDPool.h" +#include #include "GlobalConstants.h" #include "GlobalSynced.h" #include "Sim/Objects/SolidObject.h" @@ -145,4 +146,3 @@ bool SimObjectIDPool::HasID(uint32_t uid) const { return (freeIDs.find(idx) != freeIDs.end()); } - diff --git a/rts/Sim/Misc/Wind.cpp b/rts/Sim/Misc/Wind.cpp index a4090a3..85ba045 100644 --- a/rts/Sim/Misc/Wind.cpp +++ b/rts/Sim/Misc/Wind.cpp @@ -3,10 +3,12 @@ #include "Wind.h" #include "GlobalSynced.h" +#ifndef UNIT_TEST #include "Sim/Units/Unit.h" #include "Sim/Units/UnitHandler.h" #include "Sim/Misc/ModInfo.h" #include "System/ContainerUtil.h" +#endif #include "System/SpringMath.h" #include "System/Misc/TracyDefs.h" @@ -76,6 +78,7 @@ void EnvResourceHandler::LoadWind(float minStrength, float maxStrength) } +#ifndef UNIT_TEST bool EnvResourceHandler::AddGenerator(CUnit* u) { RECOIL_DETAILED_TRACY_ZONE; // duplicates should never happen, no need to check @@ -90,6 +93,8 @@ bool EnvResourceHandler::DelGenerator(CUnit* u) { +#endif + void EnvResourceHandler::Update() { RECOIL_DETAILED_TRACY_ZONE; @@ -125,6 +130,7 @@ void EnvResourceHandler::Update() curWindDir = curWindVec; curWindVec = curWindDir * curWindStrength; + #ifndef UNIT_TEST if (const auto& wcrp = modInfo.windChangeReportPeriod; wcrp > 0 && gs->frameNum % wcrp == 0) { // update generators every modInfo.windChangeReportPeriod frames for (auto unitID : allGeneratorIDs) { @@ -139,6 +145,10 @@ void EnvResourceHandler::Update() allGeneratorIDs.push_back(unitID); } newGeneratorIDs.clear(); + #else + // Component tests have no units. Keep all wind arithmetic and RNG calls intact. + assert(allGeneratorIDs.empty() && newGeneratorIDs.empty()); + #endif windDirTimer = (windDirTimer + 1) % (WIND_UPDATE_RATE + 1); } diff --git a/rts/Sim/Path/HAPFS/PathingState.cpp b/rts/Sim/Path/HAPFS/PathingState.cpp index ec88f87..6a4c2f8 100644 --- a/rts/Sim/Path/HAPFS/PathingState.cpp +++ b/rts/Sim/Path/HAPFS/PathingState.cpp @@ -1,6 +1,7 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ #include "PathingState.h" +#include "System/TimeProfiler.h" #include "zlib.h" #include "minizip/zip.h" @@ -943,4 +944,4 @@ std::uint32_t PathingState::CalcHash(const char* caller) const return peHashCode; } -} \ No newline at end of file +} diff --git a/rts/Sim/Path/QTPFS/Components/SyncUpdatedPaths.h b/rts/Sim/Path/QTPFS/Components/SyncUpdatedPaths.h index e0494fa..b9883e5 100644 --- a/rts/Sim/Path/QTPFS/Components/SyncUpdatedPaths.h +++ b/rts/Sim/Path/QTPFS/Components/SyncUpdatedPaths.h @@ -1,7 +1,6 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ -#ifndef QTPFS_SYSTEMS_SYNC_UPDATED_PATHS_H__ -#define QTPFS_SYSTEMS_SYNC_UPDATED_PATHS_H__ +#pragma once // #include #include "System/Ecs/Components/BaseComponents.h" @@ -10,7 +9,11 @@ namespace QTPFS { +#ifdef THREADPOOL typedef TaskPool &>::FuncTaskGroupPtr BackgroundTaskPtr; +#else +using BackgroundTaskPtr = std::shared_ptr; +#endif struct SyncUpdatedPathsComponent { static constexpr std::size_t page_size = 1; @@ -19,5 +22,3 @@ struct SyncUpdatedPathsComponent { }; } - -#endif \ No newline at end of file diff --git a/rts/Sim/Path/QTPFS/NodeLayer.h b/rts/Sim/Path/QTPFS/NodeLayer.h index b05b0d0..a7c1bee 100644 --- a/rts/Sim/Path/QTPFS/NodeLayer.h +++ b/rts/Sim/Path/QTPFS/NodeLayer.h @@ -1,7 +1,7 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ -#ifndef QTPFS_NODELAYER_H_ -#define QTPFS_NODELAYER_H_ +#pragma once +#include "System/Misc/TracyDefs.h" // #undef NDEBUG @@ -326,4 +326,3 @@ private: }; } -#endif \ No newline at end of file diff --git a/rts/Sim/Path/QTPFS/PathThreads.h b/rts/Sim/Path/QTPFS/PathThreads.h index fd04675..06d0d6c 100644 --- a/rts/Sim/Path/QTPFS/PathThreads.h +++ b/rts/Sim/Path/QTPFS/PathThreads.h @@ -1,5 +1,6 @@ -#ifndef PATH_THREADS_H__ -#define PATH_THREADS_H__ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#pragma once +#include "System/Misc/TracyDefs.h" #include #include @@ -272,4 +273,3 @@ namespace QTPFS { }; } -#endif \ No newline at end of file diff --git a/rts/Sim/Units/Scripts/CobInstance.h b/rts/Sim/Units/Scripts/CobInstance.h index 96614a2..608ceca 100644 --- a/rts/Sim/Units/Scripts/CobInstance.h +++ b/rts/Sim/Units/Scripts/CobInstance.h @@ -17,12 +17,7 @@ // should generally be enough static constexpr unsigned int MAX_COB_ARGS = 16; -static constexpr int COBSCALE = 65536; -static constexpr int COBSCALE_HALF = COBSCALE / 2; -static constexpr float COBSCALE_INV = 1.0f / COBSCALE; - -static const float RAD2TAANG = COBSCALE_HALF / math::PI; -static const float TAANG2RAD = math::PI / COBSCALE_HALF; +#include "CobConstants.h" class CCobThread; diff --git a/rts/Sim/Units/Scripts/UnitScriptEngine.cpp b/rts/Sim/Units/Scripts/UnitScriptEngine.cpp index 261861a..f4f0248 100644 --- a/rts/Sim/Units/Scripts/UnitScriptEngine.cpp +++ b/rts/Sim/Units/Scripts/UnitScriptEngine.cpp @@ -3,6 +3,7 @@ /* heavily based on CobEngine.cpp */ #include "UnitScriptEngine.h" +#include "System/TimeProfiler.h" #include "CobEngine.h" #include "CobFileHandler.h" diff --git a/rts/System/CMakeLists.txt b/rts/System/CMakeLists.txt index 00022fa..e431189 100644 --- a/rts/System/CMakeLists.txt +++ b/rts/System/CMakeLists.txt @@ -203,7 +203,17 @@ set(sources_engine_System ### only use the target platform related directory -if (APPLE) +if(EMSCRIPTEN) + make_global_var(sources_engine_System_Threading + ${sources_engine_System_Threading} + "${CMAKE_CURRENT_SOURCE_DIR}/Platform/Mac/Signal.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Platform/Emscripten/Platform.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Platform/Emscripten/ThreadSupport.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Platform/Linux/SoLib.cpp") + make_global_var(sources_engine_System + ${sources_engine_System} ${sources_engine_System_Threading} + "${CMAKE_CURRENT_SOURCE_DIR}/Platform/Linux/WindowManagerHelper.cpp") +elseif (APPLE) make_global_var(sources_engine_System_Threading ${sources_engine_System_Threading} ${sources_engine_System_Threading_Mac}) make_global_var(sources_engine_System ${sources_engine_System} diff --git a/rts/System/FastMath.h b/rts/System/FastMath.h index 312f1df..fbde06a 100644 --- a/rts/System/FastMath.h +++ b/rts/System/FastMath.h @@ -3,7 +3,9 @@ #ifndef FASTMATH_H #define FASTMATH_H +#if !defined(__wasm__) #include "System/simd_compat.h" +#endif #include // Tell streflop_cond.h not to define math::sqrt(float) - we'll provide a faster one @@ -48,9 +50,13 @@ namespace fastmath { __FORCE_ALIGN_STACK__ inline float sqrt_sse(float x) { + #if defined(__wasm__) + return __builtin_sqrtf(x); + #else __m128 vec = _mm_set_ss(x); vec = _mm_sqrt_ss(vec); return _mm_cvtss_f32(vec); + #endif } diff --git a/rts/System/FileSystem/DataDirLocater.cpp b/rts/System/FileSystem/DataDirLocater.cpp index 760fa1e..859ba8e 100644 --- a/rts/System/FileSystem/DataDirLocater.cpp +++ b/rts/System/FileSystem/DataDirLocater.cpp @@ -140,6 +140,9 @@ std::string DataDirLocater::SubstEnvVars(const std::string& in) const return ""; out = nowide::narrow(out_ws.c_str()); +#elif defined(__EMSCRIPTEN__) + // The host supplies literal VFS paths; WASM has no shell word expansion. + out = in; #else std::string previous = in; @@ -601,4 +604,3 @@ void DataDirLocater::FreeInstance() { spring::SafeDelete(instance); } - diff --git a/rts/System/MainDefines.h b/rts/System/MainDefines.h index 9794737..5068b8b 100644 --- a/rts/System/MainDefines.h +++ b/rts/System/MainDefines.h @@ -17,13 +17,13 @@ #endif /* !defined __cplusplus && !defined bool */ /* define if we have a X11 environment (:= linux/freebsd) */ -#if !defined(__APPLE__) && !defined(_WIN32) +#if !defined(__APPLE__) && !defined(_WIN32) && !defined(__EMSCRIPTEN__) //FIXME move this check to cmake, which has FindX11.cmake? #define _X11 #endif -#if (defined(__alpha__) || defined(__arm__) || defined(__aarch64__) || defined(__mips__) || defined(__powerpc__) || defined(__sparc__) || defined(__m68k__) || defined(__ia64__)) +#if (defined(__wasm__) || defined(__alpha__) || defined(__arm__) || defined(__aarch64__) || defined(__mips__) || defined(__powerpc__) || defined(__sparc__) || defined(__m68k__) || defined(__ia64__)) #define __is_x86_arch__ 0 #elif (defined(__i386__) || defined(__x86_64__) || defined(__amd64__) || defined(_M_AMD64) || defined(_M_IX86) || defined(_M_X64)) #define __is_x86_arch__ 1 @@ -181,4 +181,3 @@ #endif #endif /* MAIN_DEFINES_H */ - diff --git a/rts/System/MemPoolTypes.h b/rts/System/MemPoolTypes.h index af15588..b8f77f9 100644 --- a/rts/System/MemPoolTypes.h +++ b/rts/System/MemPoolTypes.h @@ -1,7 +1,6 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ -#ifndef MEMPOOL_TYPES_H -#define MEMPOOL_TYPES_H +#pragma once #include #include @@ -77,7 +76,7 @@ inline constexpr size_t TypesMemSize = [] { template struct DynMemPool { public: void* allocMem(size_t size) { - assert(size <= PAGE_SIZE()); + assert(size <= PageSize()); uint8_t* m = nullptr; size_t i = 0; @@ -99,7 +98,7 @@ public: template T* alloc(A&&... a) { - static_assert(sizeof(T) <= PAGE_SIZE(), ""); + static_assert(sizeof(T) <= PageSize(), ""); static_assert(Alignment >= alignof(T), "Memory pool memory is not sufficiently aligned"); return new (allocMem(sizeof(T))) T(std::forward(a)...); } @@ -111,7 +110,7 @@ public: const auto iter = table.find(m); const auto pair = std::pair{iter->first, iter->second}; - std::memset(pages[pair.second].data, 0, PAGE_SIZE()); + std::memset(pages[pair.second].data, 0, PageSize()); indcs.push_back(pair.second); table.erase(pair.first); @@ -129,10 +128,10 @@ public: freeMem(m); } - static constexpr size_t PAGE_SIZE() { return S; } + static constexpr size_t PageSize() { return S; } - size_t alloc_size() const { return (pages.size() * PAGE_SIZE()); } // size of total number of pages added over the pool's lifetime - size_t freed_size() const { return (indcs.size() * PAGE_SIZE()); } // size of number of pages that were freed and are awaiting reuse + size_t alloc_size() const { return (pages.size() * PageSize()); } // size of total number of pages added over the pool's lifetime + size_t freed_size() const { return (indcs.size() * PageSize()); } // size of number of pages that were freed and are awaiting reuse bool mapped(void* p) const { return (table.find(p) != table.end()); } bool alloced(void* p) const { return ((curr_page_index < pages.size()) && (pages[curr_page_index].data == p)); } @@ -177,7 +176,7 @@ using DynMemPoolT = DynMemPool, TypesMemAlignment>; template struct FixedDynMemPool { public: template T* alloc(A&&... a) { - static_assert(sizeof(T) <= PAGE_SIZE(), ""); + static_assert(sizeof(T) <= PageSize(), ""); static_assert(Alignment >= alignof(T), "Memory pool memory is not sufficiently aligned"); return (new (allocMem(sizeof(T))) T(std::forward(a)...)); } @@ -203,7 +202,7 @@ public: const uint32_t idx = spring::VectorBackPop(indcs); - assert(size <= PAGE_SIZE()); + assert(size <= PageSize()); t_page_mem* page = page_mem(idx); page_index = page->index = idx; num_allocs++; @@ -212,7 +211,7 @@ public: template void free(T*& ptr) { - static_assert(sizeof(T) <= PAGE_SIZE(), ""); + static_assert(sizeof(T) <= PageSize(), ""); T* tmp = ptr; @@ -246,10 +245,10 @@ public: static constexpr size_t NUM_CHUNKS() { return N; } // size K*S static constexpr size_t NUM_PAGES() { return K; } // per chunk - static constexpr size_t PAGE_SIZE() { return S; } + static constexpr size_t PageSize() { return S; } - size_t alloc_size() const { return (num_chunks * NUM_PAGES() * PAGE_SIZE()); } // size of total number of pages added over the pool's lifetime - size_t freed_size() const { return (indcs.size() * PAGE_SIZE()); } // size of number of pages that were freed and are awaiting reuse + size_t alloc_size() const { return (num_chunks * NUM_PAGES() * PageSize()); } // size of total number of pages added over the pool's lifetime + size_t freed_size() const { return (indcs.size() * PageSize()); } // size of number of pages that were freed and are awaiting reuse bool mapped(void* ptr) const { return ((page_mem_from_ptr(ptr)->index < (num_chunks * K)) && (page_mem(page_mem_from_ptr(ptr)->index)->data == ptr)); } bool alloced(void* ptr) const { return ((page_index < (num_chunks * K)) && (page_mem(page_index)->data == ptr)); } @@ -298,7 +297,7 @@ public: StaticMemPool() { clear(); } void* allocMem(size_t size) { - assert(size <= PAGE_SIZE()); + assert(size <= PageSize()); static_assert(NUM_PAGES() != 0, ""); size_t i = 0; @@ -316,7 +315,7 @@ public: template T* alloc(A&&... a) { - static_assert(sizeof(T) <= PAGE_SIZE(), ""); + static_assert(sizeof(T) <= PageSize(), ""); static_assert(Alignment >= alignof(T), "Memory pool memory is not sufficiently aligned"); return new (allocMem(sizeof(T))) T(std::forward(a)...); } @@ -325,10 +324,10 @@ public: assert(can_free()); assert(mapped(m)); - std::memset(m, 0, PAGE_SIZE()); + std::memset(m, 0, PageSize()); // mark page as free - indcs[free_page_count++] = base_offset(m) / PAGE_SIZE(); + indcs[free_page_count++] = base_offset(m) / PageSize(); } @@ -342,14 +341,14 @@ public: static constexpr size_t NUM_PAGES() { return N; } - static constexpr size_t PAGE_SIZE() { return S; } + static constexpr size_t PageSize() { return S; } - size_t alloc_size() const { return (used_page_count * PAGE_SIZE()); } // size of total number of pages added over the pool's lifetime - size_t freed_size() const { return (free_page_count * PAGE_SIZE()); } // size of number of pages that were freed and are awaiting reuse - size_t total_size() const { return (NUM_PAGES() * PAGE_SIZE()); } + size_t alloc_size() const { return (used_page_count * PageSize()); } // size of total number of pages added over the pool's lifetime + size_t freed_size() const { return (free_page_count * PageSize()); } // size of number of pages that were freed and are awaiting reuse + size_t total_size() const { return (NUM_PAGES() * PageSize()); } size_t base_offset(const void* p) const { return (reinterpret_cast(p) - reinterpret_cast(pages[0].data())); } - bool mapped(const void* p) const { return (((base_offset(p) / PAGE_SIZE()) < total_size()) && ((base_offset(p) % PAGE_SIZE()) == 0)); } + bool mapped(const void* p) const { return (((base_offset(p) / PageSize()) < total_size()) && ((base_offset(p) % PageSize()) == 0)); } bool alloced(const void* p) const { return (pages[curr_page_index].data() == p); } bool can_alloc() const { return (used_page_count < NUM_PAGES() || free_page_count > 0); } @@ -558,5 +557,5 @@ inline void StablePosAllocator::Free(size_t firstElem, size_t numElems, const myLog("StablePosAllocator::Free(%u, %u)", uint32_t(firstElem), uint32_t(numElems)); } -#endif + diff --git a/rts/System/Net/UDPConnection.cpp b/rts/System/Net/UDPConnection.cpp index f000bd6..0dc6981 100644 --- a/rts/System/Net/UDPConnection.cpp +++ b/rts/System/Net/UDPConnection.cpp @@ -1,6 +1,7 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ #include "UDPConnection.h" +#include "WebbarDatagram.h" #include @@ -242,6 +243,9 @@ UDPConnection::UDPConnection(std::shared_ptr netSocket, const i UDPConnection::UDPConnection(int sourcePort, const std::string& address, const unsigned port) : sharedSocket(false) { + #ifdef __EMSCRIPTEN__ + if ((browserTransport = WebbarDatagram::Open()) != nullptr) { Init(); return; } + #endif asio::error_code err; addr = ResolveAddr(address, port, &err); @@ -344,6 +348,9 @@ UDPConnection::~UDPConnection() waitingPackets.clear(); Flush(true); + #ifdef __EMSCRIPTEN__ + if (browserTransport) browserTransport->status.store(2); + #endif } void UDPConnection::SendData(std::shared_ptr pkt) @@ -437,6 +444,16 @@ void UDPConnection::Update() #endif + #ifdef __EMSCRIPTEN__ + if (browserTransport) { + while (!closed && browserTransport->Receive(recvBuffer)) { + Packet packet(recvBuffer.data(), recvBuffer.size()); + ProcessRawPacket(packet); + } + Flush(false); + return; + } + #endif if (!sharedSocket && !closed) { // duplicated code with UDPListener netservice.poll(); @@ -767,6 +784,9 @@ void UDPConnection::Flush(const bool forced) } bool UDPConnection::CheckTimeout(int seconds, bool initial) const { + #ifdef __EMSCRIPTEN__ + if (browserTransport && browserTransport->status.load() >= 2) return true; + #endif int timeout; @@ -798,6 +818,9 @@ bool UDPConnection::NeedsReconnect() { } bool UDPConnection::CanReconnect() const { + #ifdef __EMSCRIPTEN__ + if (browserTransport) return false; + #endif return (globalConfig.reconnectTimeout > 0); } @@ -822,6 +845,9 @@ std::string UDPConnection::Statistics() const std::string UDPConnection::GetFullAddress() const { + #ifdef __EMSCRIPTEN__ + if (browserTransport) return "private Webbar room"; + #endif return spring::format("[%s]:%u", addr.address().to_string().c_str(), addr.port()); } @@ -1053,6 +1079,14 @@ void UDPConnection::SendPacket(Packet& pkt) outgoing.DataSent(sendBuffer.size()); lastPacketSendTime = spring_gettime(); + #ifdef __EMSCRIPTEN__ + if (browserTransport) { + browserTransport->Send(sendBuffer); + dataSent += sendBuffer.size(); + sentPackets += 1; + return; + } + #endif ip::udp::socket::message_flags flags = 0; asio::error_code err; @@ -1132,6 +1166,9 @@ void UDPConnection::Close(bool flush) { Flush(flush); muted = true; + #ifdef __EMSCRIPTEN__ + if (browserTransport) { browserTransport->status.store(2); closed = true; return; } + #endif if (!sharedSocket) { try { mySocket->close(); diff --git a/rts/System/Net/UDPConnection.h b/rts/System/Net/UDPConnection.h index 72a7b79..c39344b 100644 --- a/rts/System/Net/UDPConnection.h +++ b/rts/System/Net/UDPConnection.h @@ -1,7 +1,6 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ -#ifndef _UDP_CONNECTION_H -#define _UDP_CONNECTION_H +#pragma once #include #include @@ -15,6 +14,9 @@ class CRC; namespace netcode { +#ifdef __EMSCRIPTEN__ +struct WebbarDatagram; +#endif // for reliability testing, introduce fake packet loss with a percentage probability #define NETWORK_TEST 0 // in [0, 1] // enable network reliability testing mode @@ -219,6 +221,10 @@ private: int lastInOrder; int lastNak; + #ifdef __EMSCRIPTEN__ + WebbarDatagram* browserTransport = nullptr; + #endif + /// Our socket std::shared_ptr mySocket; @@ -265,5 +271,5 @@ private: } // namespace netcode -#endif // _UDP_CONNECTION_H + diff --git a/rts/System/Platform/CpuID.cpp b/rts/System/Platform/CpuID.cpp index 79fab4b..b09c20f 100644 --- a/rts/System/Platform/CpuID.cpp +++ b/rts/System/Platform/CpuID.cpp @@ -77,6 +77,11 @@ namespace springproc { *d = regs[3]; } +#elif defined(__EMSCRIPTEN__) + void ExecCPUID(unsigned int* a, unsigned int* b, unsigned int* c, unsigned int* d) + { + *a = *b = *c = *d = 0; + } #else // no-op on other compilers / platforms (ARM has no cpuid instruction, etc) diff --git a/rts/System/Platform/Linux/ThreadSupport.h b/rts/System/Platform/Linux/ThreadSupport.h index d44fbf4..0d04c01 100644 --- a/rts/System/Platform/Linux/ThreadSupport.h +++ b/rts/System/Platform/Linux/ThreadSupport.h @@ -1,13 +1,14 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ -#ifndef THREADSIGNALHANDLER_H -#define THREADSIGNALHANDLER_H +#pragma once #if defined(__APPLE__) // FIXME: exclusively for ucontext.h #define _XOPEN_SOURCE 700 #endif +#ifndef __EMSCRIPTEN__ #include +#endif #include #include @@ -21,4 +22,4 @@ namespace Threading { ); } -#endif // THREADSIGNALHANDLER_H + diff --git a/rts/System/Platform/Misc.cpp b/rts/System/Platform/Misc.cpp index e78c15d..d3fe80a 100644 --- a/rts/System/Platform/Misc.cpp +++ b/rts/System/Platform/Misc.cpp @@ -178,7 +178,12 @@ namespace Platform // error will only be used if procExeFilePath stays empty const char* error = nullptr; - #if defined(__linux__) + #if defined(__EMSCRIPTEN__) + // The Node launcher supplies its absolute module path. A browser host + // will supply the corresponding path inside its mounted filesystem. + const char* executable = getenv("RECOIL_WASM_EXECUTABLE"); + procExeFilePath = (executable != nullptr) ? executable : GetOrigCWD() + "spring-headless.cjs"; + #elif defined(__linux__) char file[512]; const int ret = readlink("/proc/self/exe", file, sizeof(file) - 1); @@ -250,7 +255,12 @@ namespace Platform // this will only be used if moduleFilePath stays empty const char* error = nullptr; - #if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) + #if defined(__EMSCRIPTEN__) + if (moduleName.empty()) + moduleFilePath = GetProcessExecutableFile(); + else + error = "Dynamic native modules are unavailable in this WASM build"; + #elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) #ifdef __APPLE__ #define SHARED_LIBRARY_EXTENSION "dylib" #else @@ -412,6 +422,8 @@ namespace Platform return "x86_64"; #elif defined(__aarch64__) || defined(_M_ARM64) return "arm64"; + #elif defined(__wasm32__) + return "wasm32"; #else #error "Unsupported architecture" #endif @@ -749,10 +761,10 @@ namespace Platform return (GetMacType(macAddr, 0), macAddr); } - #elif defined(__APPLE__) + #elif defined(__APPLE__) || defined(__EMSCRIPTEN__) std::array GetRawMacAddr() { - // TODO: http://lists.freebsd.org/pipermail/freebsd-hackers/2004-June/007415.html + // Unavailable on these platforms; WASM cannot query host adapters. return {{0, 0, 0, 0, 0, 0}}; } diff --git a/rts/System/Platform/ThreadAffinityGuard.cpp b/rts/System/Platform/ThreadAffinityGuard.cpp index 713b7b5..96b141a 100644 --- a/rts/System/Platform/ThreadAffinityGuard.cpp +++ b/rts/System/Platform/ThreadAffinityGuard.cpp @@ -1,9 +1,10 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ #include "ThreadAffinityGuard.h" #include "System/Log/ILog.h" #ifdef _WIN32 #include -#else +#elif !defined(__EMSCRIPTEN__) #include #include #include @@ -18,7 +19,7 @@ ThreadAffinityGuard::ThreadAffinityGuard() : affinitySaved(false) { if (!affinitySaved) { LOG_L(L_WARNING, "GetThreadAffinityMask failed with error code: %lu", GetLastError()); } -#else +#elif !defined(__EMSCRIPTEN__) tid = syscall(SYS_gettid); // Get thread ID CPU_ZERO(&savedAffinity); if (sched_getaffinity(tid, sizeof(cpu_set_t), &savedAffinity) == 0) { @@ -36,7 +37,7 @@ ThreadAffinityGuard::~ThreadAffinityGuard() { if (!SetThreadAffinityMask(threadHandle, savedAffinity)) { LOG_L(L_WARNING, "SetThreadAffinityMask failed with error code: %lu", GetLastError()); } -#else +#elif !defined(__EMSCRIPTEN__) if (sched_setaffinity(tid, sizeof(cpu_set_t), &savedAffinity) != 0) { LOG_L(L_WARNING, "Failed to restore thread affinity."); } diff --git a/rts/System/Platform/ThreadAffinityGuard.h b/rts/System/Platform/ThreadAffinityGuard.h index 2e456c5..cf1e7be 100644 --- a/rts/System/Platform/ThreadAffinityGuard.h +++ b/rts/System/Platform/ThreadAffinityGuard.h @@ -1,9 +1,9 @@ -#ifndef THREAD_AFFINITY_GUARD_H__ -#define THREAD_AFFINITY_GUARD_H__ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#pragma once #ifdef _WIN32 #include -#else +#elif !defined(__EMSCRIPTEN__) #include #endif @@ -12,7 +12,7 @@ private: #ifdef _WIN32 DWORD_PTR savedAffinity; HANDLE threadHandle; -#else +#elif !defined(__EMSCRIPTEN__) cpu_set_t savedAffinity; pid_t tid; #endif @@ -31,5 +31,3 @@ public: // Delete copy assignment operator to prevent assignment ThreadAffinityGuard& operator=(const ThreadAffinityGuard&) = delete; }; - -#endif diff --git a/rts/System/Platform/Threading.cpp b/rts/System/Platform/Threading.cpp index 45e0d8d..a0e009a 100644 --- a/rts/System/Platform/Threading.cpp +++ b/rts/System/Platform/Threading.cpp @@ -18,7 +18,10 @@ #include #include #include -#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) +#ifdef __EMSCRIPTEN__ +#include +#endif +#if defined(__EMSCRIPTEN__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) #elif defined(_WIN32) #include #include "System/Platform/Win/DllLib.h" @@ -84,7 +87,7 @@ namespace Threading { static NativeThreadId nativeThreadIDs[THREAD_IDX_LAST] = {}; static Error threadError; -#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) +#if defined(__EMSCRIPTEN__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) #elif defined(_WIN32) static DWORD_PTR cpusSystem = 0; #else @@ -94,7 +97,7 @@ namespace Threading { void DetectCores() { - #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) + #if defined(__EMSCRIPTEN__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) // no-op #elif defined(_WIN32) @@ -122,7 +125,7 @@ namespace Threading { - #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) + #if defined(__EMSCRIPTEN__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) #elif defined(_WIN32) #else static std::uint32_t CalcCoreAffinityMask(const cpu_set_t* cpuSet) { @@ -232,6 +235,8 @@ namespace Threading { // Apple Silicon CCDs are grouped into a minimum of 4 cores with atrocious cross-CCD latency, so avoid // multiple CCDs there as well. constexpr uint32_t threadCountThreshold = 4; +#elif defined(__EMSCRIPTEN__) + constexpr uint32_t threadCountThreshold = 1; #else #error "Unsupported architecture" #endif @@ -255,7 +260,7 @@ namespace Threading { std::uint32_t GetAffinity() { - #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) + #if defined(__EMSCRIPTEN__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) // no-op return 0; @@ -278,7 +283,7 @@ namespace Threading { if (coreMask == 0) return (~0); - #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) + #if defined(__EMSCRIPTEN__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) // These platforms don't support thread affinity; return ~0 ("not set") return (~0); @@ -334,7 +339,7 @@ namespace Threading { std::uint32_t GetAvailableCoresMask() { - #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) + #if defined(__EMSCRIPTEN__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) // no-op return (~0); #elif defined(_WIN32) @@ -373,7 +378,7 @@ namespace Threading { void SetThreadScheduler() { - #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) + #if defined(__EMSCRIPTEN__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) // no-op #elif defined(_WIN32) @@ -428,7 +433,7 @@ namespace Threading { handle(0), running(false) { -#ifndef _WIN32 +#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) memset(&ucontext, 0, sizeof(ucontext_t)); #endif } @@ -512,7 +517,9 @@ namespace Threading { tracy::SetThreadName(newname.c_str()); #endif #ifndef _WIN32 - #ifdef __APPLE__ + #ifdef __EMSCRIPTEN__ + emscripten_set_thread_name(pthread_self(), newname.c_str()); + #elif defined(__APPLE__) pthread_setname_np(newname.c_str()); #else prctl(PR_SET_NAME, newname.c_str(), 0, 0, 0); diff --git a/rts/System/Platform/Threading.h b/rts/System/Platform/Threading.h index 277f49f..27e90bf 100644 --- a/rts/System/Platform/Threading.h +++ b/rts/System/Platform/Threading.h @@ -1,7 +1,6 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ -#ifndef _THREADING_H_ -#define _THREADING_H_ +#pragma once #include #ifndef _WIN32 @@ -109,7 +108,9 @@ namespace Threading { #ifndef _WIN32 spring::mutex mutSuspend; spring::condition_variable condInitialized; + #ifndef __EMSCRIPTEN__ ucontext_t ucontext; + #endif pid_t thread_id; #endif }; @@ -241,4 +242,4 @@ namespace Threading { } } -#endif // _THREADING_H_ + diff --git a/rts/System/SpringMath.cpp b/rts/System/SpringMath.cpp index 4bd235b..48e5fe3 100644 --- a/rts/System/SpringMath.cpp +++ b/rts/System/SpringMath.cpp @@ -8,7 +8,7 @@ #include "System/Exceptions.h" #include "System/Sync/FPUCheck.h" #include "System/Log/ILog.h" -#include "Sim/Units/Scripts/CobInstance.h" // for TAANG2RAD (ugh) +#include "Sim/Units/Scripts/CobConstants.h" #undef far #undef near diff --git a/rts/System/Sync/FPUCheck.cpp b/rts/System/Sync/FPUCheck.cpp index a971489..d6fd6e8 100644 --- a/rts/System/Sync/FPUCheck.cpp +++ b/rts/System/Sync/FPUCheck.cpp @@ -27,8 +27,8 @@ void good_fpu_init() { #else -#if !defined(STREFLOP_SSE) && !defined(STREFLOP_NEON) && !defined(STREFLOP_X87) - #error "streflop FP-math mode must be either SSE or NEON or X87" +#if !defined(STREFLOP_SSE) && !defined(STREFLOP_NEON) && !defined(STREFLOP_X87) && !defined(STREFLOP_WASM) + #error "streflop FP-math mode must be SSE, NEON, X87 or WASM" #endif @@ -105,7 +105,10 @@ void good_fpu_control_registers(const char* text) streflop::fpenv_t fenv; streflop::fegetenv(&fenv); - #if defined(STREFLOP_SSE) + #if defined(STREFLOP_WASM) + if (streflop::fegetround() != streflop::FE_TONEAREST) + throw unsupported_error("WASM requires round-to-nearest arithmetic"); + #elif defined(STREFLOP_SSE) const int sse_flag = fenv.sse_mode & 0xFF80; const int x87_flag = fenv.x87_mode & 0x1F3F; @@ -149,6 +152,8 @@ void good_fpu_init() LOG("[%s][STREFLOP_SSE]", __func__); #elif (defined(STREFLOP_NEON)) LOG("[%s][STREFLOP_NEON]", __func__); + #elif (defined(STREFLOP_WASM)) + LOG("[%s][STREFLOP_WASM]", __func__); #elif (defined(STREFLOP_X87)) LOG("[%s][STREFLOP_X87]", __func__); #else @@ -176,6 +181,8 @@ void good_fpu_init() streflop::fegetenv(&fenv); LOG("\tFPCR: 0x%08llX", (unsigned long long)fenv.fpcr); } + #elif (defined(STREFLOP_WASM)) + LOG("\tWASM fixed round-to-nearest; gradual underflow; FP traps unavailable"); #elif (defined(STREFLOP_X87)) LOG_L(L_WARNING, "\tStreflop floating-point math is set to X87 mode"); LOG_L(L_WARNING, "\tThis may cause desyncs during multi-player games"); diff --git a/rts/System/Threading/SpringThreading.h b/rts/System/Threading/SpringThreading.h index d03c5d1..6426690 100644 --- a/rts/System/Threading/SpringThreading.h +++ b/rts/System/Threading/SpringThreading.h @@ -3,7 +3,9 @@ #ifndef SPRINGTHREADING_H #define SPRINGTHREADING_H +#if !defined(__EMSCRIPTEN__) #define USE_FUTEX +#endif #include #include diff --git a/rts/System/Threading/ThreadPool.h b/rts/System/Threading/ThreadPool.h index 48fdc10..9e0d320 100644 --- a/rts/System/Threading/ThreadPool.h +++ b/rts/System/Threading/ThreadPool.h @@ -1,7 +1,6 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ -#ifndef _THREADPOOL_H -#define _THREADPOOL_H +#pragma once #ifndef THREADPOOL #include @@ -49,17 +48,18 @@ static inline void for_mt(int start, int end, F&& f) } template -static inline void for_mt_background(int start, int end, int step, F&& f) +static inline std::shared_ptr for_mt_background(int start, int end, int step, F&& f) { for (int i = start; i < end; i += step) { f(i); } + return {}; // All work completed synchronously; no pending task to wait on. } template -static inline void for_mt_background(int start, int end, F&& f) +static inline std::shared_ptr for_mt_background(int start, int end, F&& f) { - for_mt(start, end, 1, std::move(f)); + return for_mt_background(start, end, 1, std::forward(f)); } template @@ -1006,5 +1006,3 @@ namespace ThreadPool { } #endif -#endif - diff --git a/rts/System/simd_compat.h b/rts/System/simd_compat.h index c68b59e..4892f43 100644 --- a/rts/System/simd_compat.h +++ b/rts/System/simd_compat.h @@ -1,7 +1,12 @@ #ifndef _SIMD_COMPAT_H #define _SIMD_COMPAT_H -#ifdef SSE2NEON +#if defined(__EMSCRIPTEN__) + // Emscripten supplies SSE-to-WASM compatibility headers. The native + // x86intrin umbrella also imports unsupported architecture intrinsics. + #include + #include +#elif defined(SSE2NEON) #include "lib/sse2neon/sse2neon.h" // sse2neon leaks 's FE_XXX macros, which collide with the ones streflop // redefines and trigger a #warning. Undef them here so streflop gets a clean slate. diff --git a/rts/builds/CMakeLists.txt b/rts/builds/CMakeLists.txt index 61835bb..e1cfc2a 100644 --- a/rts/builds/CMakeLists.txt +++ b/rts/builds/CMakeLists.txt @@ -21,9 +21,11 @@ macro (create_engine_build_and_install_target targetName) set(${targetName}-Deps engine-${targetName} basecontent - unitsync ${DEPS_AI_ALL} ) + if(NOT EMSCRIPTEN) + list(APPEND ${targetName}-Deps unitsync) + endif() if (CREATE_MAN_PAGES) list(APPEND ${targetName}-Deps manpages) endif() diff --git a/rts/builds/headless/CMakeLists.txt b/rts/builds/headless/CMakeLists.txt index 7a7a031..1dc41b0 100644 --- a/rts/builds/headless/CMakeLists.txt +++ b/rts/builds/headless/CMakeLists.txt @@ -13,7 +13,9 @@ remove_definitions(-DAVI_CAPTURING) set(OpenGL_GL_PREFERENCE LEGACY) -find_package(OpenGL 3.0 REQUIRED) +if(NOT EMSCRIPTEN) + find_package(OpenGL 3.0 REQUIRED) +endif() # NOTE(create_headless_target): We don't need to copy COMPILE_DEFINTIONS till the add_definitions are used. # We don't need to copy COMPILE_DEFINTIONS because the headless defines are defined at the folder level @@ -50,7 +52,13 @@ include_directories(${ENGINE_SRC_ROOT_DIR}/lib/asio/include) include_directories(${ENGINE_SRC_ROOT_DIR}/lib/slimsig/include) include_directories(${ENGINE_SRC_ROOT_DIR}/lib/cereal/include) -create_headless_target_from(Game) +if(TARGET Game) + create_headless_target_from(Game) +else() + add_library(GameHeadless STATIC ${sources_engine_Game}) + target_include_directories(GameHeadless PRIVATE "${CMAKE_BINARY_DIR}/src-generated/engine") + add_dependencies(GameHeadless generateVersionFiles) +endif() target_link_libraries(GameHeadless PRIVATE Tracy::TracyClient headlessStubs @@ -64,6 +72,18 @@ target_link_libraries(GameHeadless PRIVATE add_executable(engine-headless ${engineSources} ${ENGINE_ICON}) target_link_libraries(engine-headless no-sound ${engineHeadlessLibraries} GameHeadless no-sound) +if(EMSCRIPTEN) + target_link_libraries(engine-headless BARb) + target_link_options(engine-headless PRIVATE + -pthread -fexceptions -msimd128 + -sUSE_LIBPNG=1 -sUSE_LIBJPEG=1 -sUSE_ZLIB=1 -sUSE_FREETYPE=1 + -sPROXY_TO_PTHREAD=1 -sPTHREAD_POOL_SIZE=8 + -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=268435456 -sMAXIMUM_MEMORY=4294967296 + -sSTACK_SIZE=8388608 -sDEFAULT_PTHREAD_STACK_SIZE=8388608 + -sEXIT_RUNTIME=1 -sENVIRONMENT=node,worker -sNODERAWFS=1) + set_target_properties(engine-headless PROPERTIES SUFFIX ".cjs") +endif() + # Export symbols for plugin access (replaces CMP0065 OLD behavior) set_target_properties(engine-headless PROPERTIES ENABLE_EXPORTS TRUE) @@ -85,3 +105,19 @@ install(TARGETS engine-headless DESTINATION ${BINDIR}) # * make spring-headless # * make install-spring-headless create_engine_build_and_install_target(headless) + +# Browser host shares the actual engine libraries with the Node experiment. +if(EMSCRIPTEN) + add_executable(engine-browser ${engineSources} ${ENGINE_ICON}) + target_link_libraries(engine-browser BARb) + target_link_libraries(engine-browser no-sound ${engineHeadlessLibraries} GameHeadless no-sound) + target_link_options(engine-browser PRIVATE + -pthread -fexceptions -msimd128 + -sUSE_LIBPNG=1 -sUSE_LIBJPEG=1 -sUSE_ZLIB=1 -sUSE_FREETYPE=1 + -sPROXY_TO_PTHREAD=1 -sPTHREAD_POOL_SIZE=8 + -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=268435456 -sMAXIMUM_MEMORY=4294967296 + -sSTACK_SIZE=8388608 -sDEFAULT_PTHREAD_STACK_SIZE=8388608 + -sEXIT_RUNTIME=1 -sENVIRONMENT=web,worker -sFORCE_FILESYSTEM=1 + -sEXPORTED_RUNTIME_METHODS=FS,ENV,callMain) + set_target_properties(engine-browser PROPERTIES OUTPUT_NAME "spring-browser" SUFFIX ".js" ENABLE_EXPORTS TRUE) +endif() diff --git a/rts/lib/luasocket/src/inet.cpp b/rts/lib/luasocket/src/inet.cpp index 90a2358..fd780f7 100644 --- a/rts/lib/luasocket/src/inet.cpp +++ b/rts/lib/luasocket/src/inet.cpp @@ -235,7 +235,7 @@ bool isAllowed(p_socket ps, const char *address, unsigned short port, bool conne if (connect) type = CLuaSocketRestrictions::TCP_CONNECT; else - type = CLuaSocketRestrictions::TCP_LISTEN; + type = CLuaSocketRestrictions::TCP_LISTEN_RULE; else //SOCK_DGRAM if (connect) type = CLuaSocketRestrictions::UDP_CONNECT; diff --git a/rts/lib/luasocket/src/restrictions.cpp b/rts/lib/luasocket/src/restrictions.cpp index 4ed3a81..f88b27b 100644 --- a/rts/lib/luasocket/src/restrictions.cpp +++ b/rts/lib/luasocket/src/restrictions.cpp @@ -42,7 +42,7 @@ CLuaSocketRestrictions::CLuaSocketRestrictions() { #ifndef TEST addRules(TCP_CONNECT, configHandler->GetString("TCPAllowConnect")); - addRules(TCP_LISTEN, configHandler->GetString("TCPAllowListen")); + addRules(TCP_LISTEN_RULE, configHandler->GetString("TCPAllowListen")); addRules(UDP_CONNECT, configHandler->GetString("UDPAllowConnect")); addRules(UDP_LISTEN, configHandler->GetString("UDPAllowListen")); #endif @@ -187,7 +187,7 @@ void CLuaSocketRestrictions::addIP(const char* hostname, const char* ip) const char* CLuaSocketRestrictions::ruleToStr(RestrictType type) { switch (type) { case TCP_CONNECT: return "TCP_CONNECT"; - case TCP_LISTEN : return "TCP_LISTEN "; + case TCP_LISTEN_RULE : return "TCP_LISTEN_RULE "; case UDP_LISTEN : return "UDP_LISTEN "; case UDP_CONNECT: return "UDP_CONNECT"; default: return "INVALID"; diff --git a/rts/lib/luasocket/src/restrictions.h b/rts/lib/luasocket/src/restrictions.h index a6bb1ba..bda9d27 100644 --- a/rts/lib/luasocket/src/restrictions.h +++ b/rts/lib/luasocket/src/restrictions.h @@ -22,7 +22,7 @@ public: enum RestrictType{ TCP_CONNECT = 0, - TCP_LISTEN, + TCP_LISTEN_RULE, UDP_CONNECT, UDP_LISTEN, ALL_RULES diff --git a/rts/lib/smmalloc/smmalloc.h b/rts/lib/smmalloc/smmalloc.h index 4bc0786..e0d166c 100644 --- a/rts/lib/smmalloc/smmalloc.h +++ b/rts/lib/smmalloc/smmalloc.h @@ -25,8 +25,10 @@ #include #include #include +#include #include #include +#include #include //#define SMMALLOC_STATS_SUPPORT diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 6834ff1..30f8bc2 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -1,4 +1,9 @@ +if(EMSCRIPTEN) + include("${CMAKE_SOURCE_DIR}/rts/System/Platform/Emscripten/OfflineDownloader.cmake") + return() +endif() + add_subdirectory(unitsync) add_subdirectory(DemoTool) diff --git a/rts/ExternalAI/WebbarStaticAI.h b/rts/ExternalAI/WebbarStaticAI.h new file mode 100644 index 0000000..758e8d0 --- /dev/null +++ b/rts/ExternalAI/WebbarStaticAI.h @@ -0,0 +1,17 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#pragma once +#include "Interface/SSkirmishAILibrary.h" +#include + +extern "C" int WebbarBARbInit(int, const SSkirmishAICallback*); +extern "C" int WebbarBARbRelease(int); +extern "C" int WebbarBARbHandleEvent(int, int, const void*); + +// Only the pinned, statically linked AI is available in the WASM host. +inline const SSkirmishAILibrary* WebbarLoadBARb(const char* name, const char* version) +{ + if (std::strcmp(name, "BARb") != 0 || std::strcmp(version, "stable") != 0) + return nullptr; + static const SSkirmishAILibrary library = {nullptr, WebbarBARbInit, WebbarBARbRelease, WebbarBARbHandleEvent}; + return &library; +} diff --git a/rts/Game/WebbarSkirmish.h b/rts/Game/WebbarSkirmish.h new file mode 100644 index 0000000..f5e1ce2 --- /dev/null +++ b/rts/Game/WebbarSkirmish.h @@ -0,0 +1,95 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#pragma once + +// A small, deterministic practice opponent. Enabled only by WEBBAR_PLAYABLE. +// Every action uses native commands and normal resources, build times and LOS. +// Its only prior knowledge is the two starting base locations in this scenario. +class WebbarSkirmish final: public CEventClient +{ +public: + WebbarSkirmish(): CEventClient("Webbar skirmish", 100000, true) { eventHandler.AddClient(this); } + bool WantsEvent(const std::string& name) override { return name == "GameFrame"; } + void GameFrame(int frame) override { + if (frame < 30 || frame % 30 != 0) return; + if (const char* testFrames = std::getenv("WEBBAR_AI_TEST_FRAMES")) { + if (frame % 900 == 0) { + int units = 0, building = 0, factories = 0, playerUnits = 0; + std::string definitions; + for (const CUnit* unit: unitHandler.GetActiveUnits()) { + if (!unit->isDead && unit->team == 0) ++playerUnits; + if (unit->isDead || unit->team != 1) continue; + definitions += unit->unitDef->name + ","; + ++units; building += unit->beingBuilt; + factories += unit->unitDef->IsFactoryUnit() && !unit->beingBuilt; + } + LOG("[WebbarAI] frame=%d enemyUnits=%d underConstruction=%d factories=%d playerUnits=%d definitions=%s", frame, units, building, factories, playerUnits, definitions.c_str()); + } + if (frame >= std::atoi(testFrames)) gu->globalQuit = true; + } + if (outcome != 0) return; + CUnit* commander = nullptr; + std::vector factories, tanks; + bool playerAlive = false; + for (CUnit* unit: unitHandler.GetActiveUnits()) { + if (unit->isDead) continue; + if (unit->team == 0 && (unit->unitDef->name == "armcom" || unit->unitDef->name == "corcom")) playerAlive = true; + if (unit->team != 1) continue; + if (unit->unitDef->name == commanderName) commander = unit; + if (unit->unitDef->IsFactoryUnit()) factories.push_back(unit); + if (unit->unitDef->name == tankName && !unit->beingBuilt) tanks.push_back(unit); + } + if (!playerAlive) { outcome = 2; finishedFrame = frame; return; } + if (commander == nullptr && factories.empty()) { outcome = 1; finishedFrame = frame; return; } + // BARb owns all opposing orders when selected; retain only result detection. + if (std::getenv("WEBBAR_AI") != nullptr) return; + + // Expand power and claim a second metal deposit. A blocked site is skipped; + // no resources or units are created by this opponent controller. + struct Site { const char* name; float x, z; }; + static const Site sites[] = {{"corsolar", 8176, 2864}, {"corsolar", 8336, 2864}, {"cormex", 7376, 3632}, {"corllt", 7744, 3248}, {"corsolar", 8176, 3024}}; + if (commander != nullptr && commander->commandAI->commandQue.empty() && site < std::size(sites)) { + Site s = sites[site++]; + if (std::getenv("WEBBAR_SETUP")) { + static const float dx[] = {256, 416, 0, -192, 256}; + static const float dz[] = {-256, -256, 0, 96, -96}; + s = {site == 3 ? mexName : site == 4 ? towerName : solarName, enemyX + dx[site - 1], enemyZ + dz[site - 1]}; + if (site == 3) { s.x = EnvFloat("WEBBAR_MEX_X", enemyX); s.z = EnvFloat("WEBBAR_MEX_Z", enemyZ); } + } + const UnitDef* def = unitDefHandler->GetUnitDefByName(s.name); + BuildInfo build(def, float3(s.x, CGround::GetHeightReal(s.x, s.z), s.z), 0); + build.pos = CGameHelper::Pos2BuildPos(build, true); + CFeature* feature = nullptr; + if (CGameHelper::TestUnitBuildSquare(build, feature, commander->allyteam, true) != CGameHelper::BUILDSQUARE_BLOCKED) + commander->commandAI->GiveCommand(build.CreateCommand()); + } + for (CUnit* factory: factories) { + if (!factory->beingBuilt && factory->commandAI->commandQue.empty() && tanks.size() < 20) { + const UnitDef* tank = unitDefHandler->GetUnitDefByName(tankName); + factory->commandAI->GiveCommand(Command(-tank->id)); + } + } + // At 90 seconds, then once a minute, send the available armor to the + // known starting foothold. FIGHT handles acquisition through normal LOS. + if (frame >= nextWave) { + if (tanks.empty()) { nextWave = frame + 300; return; } + ++wave; nextWave = frame + 1800; + for (CUnit* tank: tanks) { + Command attack(CMD_FIGHT); + attack.PushPos(float3(playerX, CGround::GetHeightReal(playerX, playerZ), playerZ)); + tank->commandAI->GiveCommand(attack); + } + } + } + static float EnvFloat(const char* name, float fallback) { const char* value = std::getenv(name); return value ? std::strtof(value, nullptr) : fallback; } + const bool armada = std::getenv("WEBBAR_ENEMY_ARMADA") != nullptr; + const char* commanderName = armada ? "armcom" : "corcom"; + const char* tankName = armada ? "armstump" : "corraid"; + const char* solarName = armada ? "armsolar" : "corsolar"; + const char* mexName = armada ? "armmex" : "cormex"; + const char* towerName = armada ? "armllt" : "corllt"; + const float playerX = EnvFloat("WEBBAR_PLAYER_X", 6500), playerZ = EnvFloat("WEBBAR_PLAYER_Z", 3330); + const float enemyX = EnvFloat("WEBBAR_ENEMY_X", 7910), enemyZ = EnvFloat("WEBBAR_ENEMY_Z", 3110); + int outcome = 0, finishedFrame = 0, wave = 0, nextWave = 2700; + size_t site = 0; +}; +static WebbarSkirmish& GetWebbarSkirmish() { static WebbarSkirmish skirmish; return skirmish; } diff --git a/rts/Game/WebbarWeapons.h b/rts/Game/WebbarWeapons.h new file mode 100644 index 0000000..f09a8bc --- /dev/null +++ b/rts/Game/WebbarWeapons.h @@ -0,0 +1,91 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#pragma once +#include "Sim/Projectiles/WeaponProjectiles/WeaponProjectile.h" +#include "Sim/Weapons/PlasmaRepulser.h" + +// Read-only presentation. Definitions are sent once, after leaving synced code. +// ProjectileCreated fires in the BASE constructor: use WeaponDef/start/target, +// never derived members or virtual calls on a partially constructed projectile. +class WebbarEffects final: public CEventClient +{ +public: + struct Record { + uint32_t serial, frame, kind, definition; + float x, y, z, radius; + float ex, ey, ez, ttl; + float vx, vy, vz; + uint32_t projectile; + float dx, dy, dz, damage, groundHeight; + uint32_t impactFlags; + }; + struct Shot { uint32_t id, definition; float x,y,z,vx,vy,vz,ex,ey,ez,ttl; }; + WebbarEffects(): CEventClient("Webbar browser effects", -100000, false) { eventHandler.AddClient(this); } + bool WantsEvent(const std::string& name) override { return name=="Explosion" || name=="UnitDestroyed" || name=="ProjectileCreated"; } + int GetReadAllyTeam() const override { return AllAccessTeam; } + uint32_t Remember(const WeaponDef* def) { if (!def) return 0xffffffff; if (known.insert(def->id).second) pending.push_back(def); return def->id; } + bool Explosion(int, const WeaponDef* def, const CExplosionParams& p) override { + if (!explGenHandler.PredictExplosionVisible(def, p, gu->myAllyTeam)) return false; + const float ground = CGround::GetHeightReal(p.pos.x, p.pos.z); + uint32_t flags = CCustomExplosionGenerator::GetFlagsFromHeight(p.pos.y, ground); + flags |= p.hitObject.HasStored() ? CCustomExplosionGenerator::CEG_SPWF_UNIT : CCustomExplosionGenerator::CEG_SPWF_NO_UNIT; + if (auto* weapon = p.hitObject.GetTyped()) { + flags |= dynamic_cast(weapon) ? CCustomExplosionGenerator::CEG_SPWF_SHIELD : CCustomExplosionGenerator::CEG_SPWF_INTERCEPTED; + } + Append(0, def, p.pos, p.damageAreaOfEffect, ZeroVector, 0, ZeroVector, p.projectileID, p.dir, p.damages.GetDefault(), ground, flags); + return false; + } + void UnitDestroyed(const CUnit* unit,const CUnit*,int) override { + if (WebbarVisible(unit)) Append(1,nullptr,unit->pos,unit->radius); + } + void ProjectileCreated(const CProjectile* p) override { + if (!p->weapon || !WebbarProjectileVisible(p)) return; + const auto* w=static_cast(p); + Append(2,w->GetWeaponDef(),p->pos,0,w->GetTargetPos(),w->GetTimeToLive(),p->speed,p->id); + } + void Append(uint32_t kind,const WeaponDef* def,const float3& pos,float radius,const float3& end=ZeroVector,float ttl=0,const float3& velocity=ZeroVector,uint32_t projectile=0xffffffff, const float3& dir=UpVector, float damage=0, float groundHeight=0, uint32_t impactFlags=0) { + const uint32_t definition=Remember(def); + if (records.size()>=2048) {++dropped;return;} + records.push_back({++serial,uint32_t(gs->frameNum),kind,definition,pos.x,pos.y,pos.z,radius,end.x,end.y,end.z,ttl,velocity.x,velocity.y,velocity.z,projectile,dir.x,dir.y,dir.z,damage,groundHeight,impactFlags}); + } + void Publish() { + std::vector shots; + uint32_t omitted=0; + for (const auto* p: projectileHandler.GetActiveProjectiles(true)) { + if (!p->weapon || p->deleteMe || !WebbarProjectileVisible(p)) continue; + const auto* w=static_cast(p);const auto* d=w->GetWeaponDef(); + const uint32_t definition=Remember(d); + if (shots.size()>=4096) {++omitted;continue;} + const float3& pos=d->IsHitScanWeapon()?w->GetStartPos():p->pos; + const auto& end=w->GetTargetPos(); + shots.push_back({uint32_t(p->id),definition,pos.x,pos.y,pos.z,p->speed.x,p->speed.y,p->speed.z,end.x,end.y,end.z,float(w->GetTimeToLive())}); + } + for (const auto* d: pending) { + const auto& v=d->visuals; + const auto original=[&](const char* key,float fallback) { const auto it=d->customParams.find(key); if(it==d->customParams.end())return fallback; char* end=nullptr;const float value=std::strtof(it->second.c_str(),&end);return end!=it->second.c_str() && *end=='\0' && std::isfinite(value) && value>=0?value:fallback; }; + // BAR's GL4 gadgets hide default sprites and preserve their real dimensions. + const float width=original("beam_thickness_orig",v.thickness),core=original("beam_corethickness_orig",v.corethickness),flare=original("beam_laserflaresize_orig",v.laserflaresize),size=original("plasma_size_orig",d->size); + const float values[]={v.color.x,v.color.y,v.color.z,v.color2.x,v.color2.y,v.color2.z,width,core,size,flare,v.beamdecay,float(d->beamLaserTTL),float(v.smokeTrail),v.smokeSize,v.smokeColor,float(v.smokeTime),float(v.smokePeriod),d->range,d->damages.damageAreaOfEffect}; + MAIN_THREAD_EM_ASM({ + if(Module['webbarWeaponDef']) Module['webbarWeaponDef']({id:$0,name:UTF8ToString($1),type:UTF8ToString($2),impact:UTF8ToString($3),trail:UTF8ToString($4),model:UTF8ToString($5),preservedVisuals:!!$7,values:Array.from(HEAPF32.subarray($6>>2,($6>>2)+19))}); + },d->id,d->name.c_str(),d->type.c_str(),v.impactExpGenTag.c_str(),v.ptrailExpGenTag.c_str(),v.modelName.c_str(),values,(d->customParams.contains("beam_thickness_orig")||d->customParams.contains("plasma_size_orig"))); + } + pending.clear(); + static_assert(sizeof(Record)==88 && sizeof(Shot)==48); + if (!records.empty() || dropped) { + const uint32_t header[]={0x33454257,uint32_t(gs->frameNum),uint32_t(records.size()),dropped}; + std::vector packet(sizeof(header)+records.size()*sizeof(Record)); + std::memcpy(packet.data(),header,sizeof(header));std::memcpy(packet.data()+sizeof(header),records.data(),records.size()*sizeof(Record)); + MAIN_THREAD_EM_ASM({if(Module['webbarEffects'])Module['webbarEffects'](HEAPU8.slice($0,$0+$1).buffer);},packet.data(),packet.size()); + records.clear();dropped=0; + } + const uint32_t header[]={0x31574257,uint32_t(gs->frameNum),uint32_t(shots.size()),omitted}; + std::vector packet(sizeof(header)+shots.size()*sizeof(Shot)); + std::memcpy(packet.data(),header,sizeof(header));std::memcpy(packet.data()+sizeof(header),shots.data(),shots.size()*sizeof(Shot)); + MAIN_THREAD_EM_ASM({if(Module['webbarWeapons'])Module['webbarWeapons'](HEAPU8.slice($0,$0+$1).buffer);},packet.data(),packet.size()); + } + std::vector records; + std::vector pending; + std::unordered_set known; + uint32_t serial=0,dropped=0; +}; +static WebbarEffects& GetWebbarEffects() { static WebbarEffects effects; return effects; } diff --git a/rts/Game/WebbarWorld.h b/rts/Game/WebbarWorld.h new file mode 100644 index 0000000..d1cba41 --- /dev/null +++ b/rts/Game/WebbarWorld.h @@ -0,0 +1,75 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#pragma once + +// Low-frequency, read-only UI data. Hidden enemies never enter the unit, pose, +// feature or queue streams. Radar contacts contain positions, not unit identity. +static void PublishWebbarWorld() +{ + if (gs->frameNum % 15 != 0) return; + constexpr uint32_t grid = 256; + std::vector contacts; + for (const CUnit* unit: unitHandler.GetActiveUnits()) + if (!WebbarVisible(unit) && losHandler->InRadar(unit, gu->myAllyTeam)) contacts.push_back(unit->GetErrorPos(gu->myAllyTeam)); + const uint32_t visionHeader[] = {0x31564257, uint32_t(gs->frameNum), grid, uint32_t(contacts.size()), uint32_t(mapDims.mapx * SQUARE_SIZE), uint32_t(mapDims.mapy * SQUARE_SIZE), 0, 0}; + std::vector vision(sizeof(visionHeader) + grid * grid + contacts.size() * sizeof(float3)); + std::memcpy(vision.data(), visionHeader, sizeof(visionHeader)); + for (uint32_t z = 0; z < grid; ++z) for (uint32_t x = 0; x < grid; ++x) { + const float px = (x + 0.5f) * visionHeader[4] / grid, pz = (z + 0.5f) * visionHeader[5] / grid; + const float3 pos(px, CGround::GetHeightReal(px, pz), pz); + vision[sizeof(visionHeader) + z * grid + x] = (losHandler->InLos(pos, gu->myAllyTeam) ? 1 : 0) | (losHandler->InRadar(pos, gu->myAllyTeam) ? 2 : 0); + } + std::memcpy(vision.data() + sizeof(visionHeader) + grid * grid, contacts.data(), contacts.size() * sizeof(float3)); + MAIN_THREAD_EM_ASM({ if (Module['webbarVision']) Module['webbarVision'](HEAPU8.slice($0, $0 + $1).buffer); }, vision.data(), vision.size()); + + struct FeatureRecord { uint32_t id, def; float metal, reclaim; float matrix[16]; }; + static_assert(sizeof(FeatureRecord) == 80); + std::vector features; + static std::vector featureDefs; + for (const int id: featureHandler.GetActiveFeatureIDs()) { + const CFeature* feature = featureHandler.GetFeature(id); + if (!feature->def->reclaimable || !losHandler->InLos(feature->pos, gu->myAllyTeam)) continue; + if (std::find(featureDefs.begin(), featureDefs.end(), feature->def->id) == featureDefs.end()) { + featureDefs.push_back(feature->def->id); + MAIN_THREAD_EM_ASM({ if (Module['webbarFeatureDef']) Module['webbarFeatureDef']($0, UTF8ToString($1)); }, feature->def->id, feature->def->modelName.c_str()); + } + FeatureRecord record = {uint32_t(id), uint32_t(feature->def->id), feature->resources.metal, feature->reclaimLeft, {}}; + const CMatrix44f matrix = feature->GetTransformMatrix(true); + std::memcpy(record.matrix, matrix.m, sizeof(record.matrix)); + features.push_back(record); + } + const uint32_t featureHeader[] = {0x31464257, uint32_t(gs->frameNum), uint32_t(features.size()), 0}; + std::vector featurePacket(sizeof(featureHeader) + features.size() * sizeof(FeatureRecord)); + std::memcpy(featurePacket.data(), featureHeader, sizeof(featureHeader)); + std::memcpy(featurePacket.data() + sizeof(featureHeader), features.data(), features.size() * sizeof(FeatureRecord)); + MAIN_THREAD_EM_ASM({ if (Module['webbarFeatures']) Module['webbarFeatures'](HEAPU8.slice($0, $0 + $1).buffer); }, featurePacket.data(), featurePacket.size()); + + struct QueueRecord { uint32_t unit, tag; int32_t command; uint32_t target; float x, y, z; uint32_t index; }; + static_assert(sizeof(QueueRecord) == 32); + std::vector queues; + for (const CUnit* unit: unitHandler.GetActiveUnits()) { + if (unit->team != gu->myTeam) continue; + uint32_t index = 0; + for (const Command& command: unit->commandAI->commandQue) { + if (index >= 24 || queues.size() >= 1024) break; + const bool position = command.GetNumParams() >= 3; + queues.push_back({uint32_t(unit->id), command.GetTag(), command.GetID(), command.GetNumParams() == 1 ? uint32_t(command.GetParam(0)) : 0xffffffff, + position ? command.GetParam(0) : 0.0f, position ? command.GetParam(1) : 0.0f, position ? command.GetParam(2) : 0.0f, index++}); + } + if (unit->unitDef->IsFactoryUnit()) { + const auto& rally = static_cast(unit->commandAI)->newUnitCommands; + if (!rally.empty() && rally.back().GetNumParams() >= 3) { + const auto& command = rally.back(); + queues.push_back({uint32_t(unit->id), 0, CMD_MOVE, 0xffffffff, command.GetParam(0), command.GetParam(1), command.GetParam(2), 0xffffffff}); + } + } + } + const uint32_t queueHeader[] = {0x31514257, uint32_t(gs->frameNum), uint32_t(queues.size()), 0}; + std::vector queuePacket(sizeof(queueHeader) + queues.size() * sizeof(QueueRecord)); + std::memcpy(queuePacket.data(), queueHeader, sizeof(queueHeader)); + std::memcpy(queuePacket.data() + sizeof(queueHeader), queues.data(), queues.size() * sizeof(QueueRecord)); + MAIN_THREAD_EM_ASM({ if (Module['webbarQueues']) Module['webbarQueues'](HEAPU8.slice($0, $0 + $1).buffer); }, queuePacket.data(), queuePacket.size()); + if (std::getenv("WEBBAR_NETWORK") == nullptr) { + const auto& match = GetWebbarSkirmish(); + MAIN_THREAD_EM_ASM({ if (Module['webbarMatch']) Module['webbarMatch']($0, $1, $2, $3, $4); }, gs->frameNum, match.outcome, match.wave, match.nextWave, match.finishedFrame); + } +} diff --git a/rts/Sim/Units/Scripts/CobConstants.h b/rts/Sim/Units/Scripts/CobConstants.h new file mode 100644 index 0000000..25ad3c0 --- /dev/null +++ b/rts/Sim/Units/Scripts/CobConstants.h @@ -0,0 +1,12 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ + +#pragma once + +#include "System/MathConstants.h" + +static constexpr int COBSCALE = 65536; +static constexpr int COBSCALE_HALF = COBSCALE / 2; +static constexpr float COBSCALE_INV = 1.0f / COBSCALE; + +static const float RAD2TAANG = COBSCALE_HALF / math::PI; +static const float TAANG2RAD = math::PI / COBSCALE_HALF; diff --git a/rts/System/Net/WebbarDatagram.h b/rts/System/Net/WebbarDatagram.h new file mode 100644 index 0000000..fb117b8 --- /dev/null +++ b/rts/System/Net/WebbarDatagram.h @@ -0,0 +1,65 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#pragma once + +#ifdef __EMSCRIPTEN__ +#include +#include +#include +#include +#include +#include + +namespace netcode { +// Single producer/single consumer on each ring. Release/acquire publication +// prevents the socket Worker and native networking thread seeing partial data. +struct WebbarDatagram { + static constexpr uint32_t COUNT = 256; + static constexpr uint32_t BYTES = 4096; + struct Slot { uint32_t size; uint8_t bytes[BYTES]; }; + struct Ring { + std::atomic read{0}, write{0}; + Slot slots[COUNT]; + }; + std::atomic status{0}; // connecting, open, closed, failed + Ring incoming, outgoing; + + static WebbarDatagram* Open() + { + if (std::getenv("WEBBAR_NETWORK") == nullptr) return nullptr; + // One connection per engine lifetime. Keep storage alive until the Worker + // tree terminates, including while socket shutdown is still in flight. + auto* transport = new WebbarDatagram{}; + MAIN_THREAD_EM_ASM({ + Module['webbarNetworkStart'](HEAPU8.buffer, $0, $1, $2, $3, $4); + }, &transport->status, &transport->incoming, &transport->outgoing, COUNT, BYTES); + return transport; + } + + bool Receive(std::vector& bytes) + { + const uint32_t read = incoming.read.load(std::memory_order_relaxed); + if (read == incoming.write.load(std::memory_order_acquire)) return false; + const auto& slot = incoming.slots[read % COUNT]; + if (slot.size < 6 || slot.size > BYTES) { status.store(3); return false; } + bytes.assign(slot.bytes, slot.bytes + slot.size); + incoming.read.store(read + 1, std::memory_order_release); + return true; + } + + void Send(const std::vector& bytes) + { + if (status.load() >= 2) return; + const uint32_t write = outgoing.write.load(std::memory_order_relaxed); + if (bytes.size() > BYTES || uint32_t(write - outgoing.read.load(std::memory_order_acquire)) >= COUNT) { + status.store(3); return; + } + auto& slot = outgoing.slots[write % COUNT]; + slot.size = bytes.size(); + std::memcpy(slot.bytes, bytes.data(), bytes.size()); + outgoing.write.store(write + 1, std::memory_order_release); + } +}; +static_assert(sizeof(std::atomic) == 4); +static_assert(sizeof(WebbarDatagram::Slot) == 4100); +} +#endif diff --git a/rts/System/Platform/Emscripten/OfflineDownloader.cmake b/rts/System/Platform/Emscripten/OfflineDownloader.cmake new file mode 100644 index 0000000..4a37bd1 --- /dev/null +++ b/rts/System/Platform/Emscripten/OfflineDownloader.cmake @@ -0,0 +1,14 @@ +# Local-content headless build. The native downloader remains available as a +# separate host tool; network downloads are explicitly unsupported in WASM. +set(prd_source "${CMAKE_SOURCE_DIR}/tools/pr-downloader/src") +add_subdirectory("${prd_source}/lib/7z" "${CMAKE_BINARY_DIR}/offline/7z") +add_subdirectory("${prd_source}/lib/base64" "${CMAKE_BINARY_DIR}/offline/base64") +add_subdirectory("${prd_source}/lib/md5" "${CMAKE_BINARY_DIR}/offline/md5") +add_subdirectory("${prd_source}/lib/jsoncpp" "${CMAKE_BINARY_DIR}/offline/jsoncpp") +add_library(prd::base64 ALIAS pr-base64) +add_library(prd::jsoncpp ALIAS pr-jsoncpp) +add_library(pr-downloader STATIC + "${CMAKE_SOURCE_DIR}/rts/System/Platform/Emscripten/OfflineDownloader.cpp" + "${prd_source}/Downloader/DownloadEnum.cpp") +target_include_directories(pr-downloader PUBLIC "${prd_source}") +target_link_libraries(pr-downloader PUBLIC pr-base64 pr-md5 pr-jsoncpp) diff --git a/rts/System/Platform/Emscripten/OfflineDownloader.cpp b/rts/System/Platform/Emscripten/OfflineDownloader.cpp new file mode 100644 index 0000000..4d25257 --- /dev/null +++ b/rts/System/Platform/Emscripten/OfflineDownloader.cpp @@ -0,0 +1,47 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ + +#include "pr-downloader.h" +#include "lib/md5/md5.h" +#include +#include +#include +#include + +// Content must be mounted before this offline WASM executable starts. Never +// report successful downloads: callers receive the normal failure path. +int DownloadSearch(DownloadEnum::Category, const char*) +{ + std::fputs("WASM offline build: mount game/map content before starting. Downloads are unavailable.\n", stderr); + return 0; +} +int DownloadSearch(std::vector&) { return -1; } +int DownloadStart() { return 2; } +int DownloadAddByUrl(DownloadEnum::Category, const char*, const char*) { return 2; } +bool DownloadAdd(unsigned int) { return false; } +bool DownloadGetInfo(int, downloadInfo&) { return false; } +void DownloadInit() {} +void DownloadShutdown() {} +bool DownloadSetConfig(CONFIG, const void*) { return false; } +bool DownloadGetConfig(CONFIG, const void**) { return false; } +void DownloadDisableLogging(bool) {} +void SetDownloadListener(IDownloaderProcessUpdateListener) {} +void SetAbortDownloads(bool) {} +bool DownloadRapidValidate(bool) { return false; } +bool DownloadDumpSDP(const char*) { return false; } +bool ValidateSDP(const char*) { return false; } +DownloadEnum::Category getPlatformEngineCat() { return DownloadEnum::CAT_NONE; } + +// Same MD5/base64 operation as pr-downloader's CalcHash. This is used by Lua +// independently of network downloads, so keep its real implementation. +char* CalcHash(const char* str, int size, int type) +{ + if (type != 0 || size < 0) return nullptr; + MD5_CTX ctx; + MD5Init(&ctx); + MD5Update(&ctx, reinterpret_cast(const_cast(str)), size); + MD5Final(&ctx); + const std::string encoded = base64_encode(ctx.digest, 16); + char* result = static_cast(std::malloc(encoded.size() + 1)); + if (result != nullptr) std::memcpy(result, encoded.c_str(), encoded.size() + 1); + return result; +} diff --git a/rts/System/Platform/Emscripten/Platform.cpp b/rts/System/Platform/Emscripten/Platform.cpp new file mode 100644 index 0000000..58021b5 --- /dev/null +++ b/rts/System/Platform/Emscripten/Platform.cpp @@ -0,0 +1,76 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ + +#include "System/Platform/CrashHandler.h" +#include "System/Platform/Hardware.h" +#include "System/Platform/MessageBox.h" +#include "System/Log/ILog.h" + +#include +#include +#include +#include +#include + +namespace cpu_topology { + +ThreadPinPolicy GetThreadPinPolicy() { return THREAD_PIN_POLICY_NONE; } + +ProcessorMasks GetProcessorMasks() +{ + // Worker capacity only: the host does not expose physical cores or affinity. + const auto count = std::clamp(std::thread::hardware_concurrency(), 1u, 32u); + ProcessorMasks masks; + masks.performanceCoreMask = UINT32_MAX >> (32 - count); + return masks; +} + +ProcessorCaches GetProcessorCache() +{ + ProcessorGroupCaches group; + group.groupMask = GetProcessorMasks().performanceCoreMask; + return {{group}}; // Cache sizes are unavailable to WASM. +} + +} + +namespace Platform { + +uint64_t TotalRAM() { return emscripten_get_heap_max(); } +uint64_t TotalPageFile() { return 0; } + +void MsgBox(const char* message, const char* caption, unsigned int) +{ + std::fprintf(stderr, "%s: %s\n", caption, message); +} + +} + +namespace CrashHandler { + +void Install() { LOG("WASM runtime provides trap diagnostics; foreign-thread stack capture unavailable"); } +void Remove() {} +void PrepareStacktrace(int) {} +void CleanupStacktrace(int) {} + +void OutputStacktrace() +{ + char stack[8192]; + emscripten_get_callstack(EM_LOG_C_STACK | EM_LOG_JS_STACK, stack, sizeof(stack)); + LOG_L(L_ERROR, "%s", stack); +} + +void Stacktrace(Threading::NativeThreadHandle thread, const std::string& name, int) +{ + if (Threading::NativeThreadIdsEqual(thread, Threading::GetCurrentThread())) { + OutputStacktrace(); + } else { + LOG_L(L_WARNING, "WASM cannot capture another worker's stack (%s)", name.c_str()); + } +} + +void SuspendedStacktrace(Threading::ThreadControls*, const char* name) +{ + LOG_L(L_WARNING, "WASM cannot suspend worker %s for a stack trace", name); +} + +} diff --git a/rts/System/Platform/Emscripten/ThreadSupport.cpp b/rts/System/Platform/Emscripten/ThreadSupport.cpp new file mode 100644 index 0000000..c755f2d --- /dev/null +++ b/rts/System/Platform/Emscripten/ThreadSupport.cpp @@ -0,0 +1,33 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ + +#include "System/Platform/Threading.h" + +namespace Threading { + +void SetupCurrentThreadControls(std::shared_ptr& controls) +{ + controls = std::make_shared(); + controls->handle = GetCurrentThread(); + controls->thread_id = GetCurrentThreadIdAsU32(); + controls->running = true; +} + +void ThreadStart(std::function task, std::shared_ptr* result, ThreadControls* startup) +{ + SetupCurrentThreadControls(localThreadControls); + if (result != nullptr) + *result = localThreadControls; + { + std::lock_guard lock(startup->mutSuspend); + startup->condInitialized.notify_all(); + } + task(); + localThreadControls->running = false; +} + +// WASM workers cannot be suspended with POSIX signals. Watchdog diagnostics +// report this limitation instead of claiming a foreign stack was captured. +SuspendResult ThreadControls::Suspend() { return THREADERR_MISC; } +SuspendResult ThreadControls::Resume() { return THREADERR_MISC; } + +} diff --git a/rts/Game/WebbarBrowser.h b/rts/Game/WebbarBrowser.h new file mode 100644 index 0000000..a864407 --- /dev/null +++ b/rts/Game/WebbarBrowser.h @@ -0,0 +1,452 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "Sim/Units/CommandAI/CommandDescription.h" +#include "Game/GameHelper.h" +#include "Sim/Misc/Team.h" +#include "Sim/Units/BuildInfo.h" +#include "Sim/Units/UnitTypes/Builder.h" +#include "Sim/Units/UnitTypes/Factory.h" +#include "System/EventClient.h" +#include "Sim/Misc/LosHandler.h" +#include "Sim/Projectiles/ExplosionGenerator.h" +#include "Sim/Features/FeatureHandler.h" +#include "Sim/Features/Feature.h" +#include "Sim/Features/FeatureDef.h" +#include "Sim/Units/CommandAI/FactoryCAI.h" + +static bool WebbarVisible(const CUnit* unit) +{ + return unit->team == gu->myTeam || unit->IsInLosForAllyTeam(gu->myAllyTeam); +} +// Match the native projectile drawer: owned/allied shots remain visible and +// airborne projectiles use the engine's air-LOS test, including motion bounds. +static bool WebbarProjectileVisible(const CProjectile* projectile) +{ + const int allyTeam = projectile->GetAllyteamID(); + return (teamHandler.IsValidAllyTeam(allyTeam) && teamHandler.Ally(allyTeam, gu->myAllyTeam)) || losHandler->InLos(projectile, gu->myAllyTeam); +} +static std::vector WebbarUnits() +{ + std::vector units; + const bool playable = std::getenv("WEBBAR_PLAYABLE") != nullptr; + for (CUnit* unit: unitHandler.GetActiveUnits()) + if (!playable || WebbarVisible(unit)) units.push_back(unit); + return units; +} + +#include "WebbarWeapons.h" + +static bool WebbarSupports(const UnitDef* def) +{ + static const std::unordered_set names = [] { + std::unordered_set result; + const char* content = std::getenv("WEBBAR_CONTENT"); + std::istringstream stream(content ? content : ""); + for (std::string name; std::getline(stream, name, ',');) result.insert(name); + return result; + }(); + return def != nullptr && names.contains(def->name); +} + +#include "WebbarSkirmish.h" +#include "WebbarWorld.h" +#include "WebbarTerrain.h" +#include "WebbarTerrainFixture.h" + +class WebbarNetworkResult final: public CEventClient +{ +public: + WebbarNetworkResult(): CEventClient("Webbar network result", -99999, false) { eventHandler.AddClient(this); } + bool WantsEvent(const std::string& name) override { return name == "GameOver"; } + void GameOver(const std::vector& winners) override { + const int outcome = winners.empty() ? 3 : (std::find(winners.begin(), winners.end(), gu->myAllyTeam) != winners.end() ? 1 : 2); + MAIN_THREAD_EM_ASM({ if (Module['webbarMatch']) Module['webbarMatch']($0, $1, 0, 0, $0); }, gs->frameNum, outcome); + } +}; + +static void PublishWebbarPlayState() +{ + static bool catalogSent = false; + if (!catalogSent) { + catalogSent = true; + if (std::getenv("WEBBAR_NETWORK") != nullptr) { static WebbarNetworkResult result; } + MAIN_THREAD_EM_ASM({ if (Module['webbarProtocol']) Module['webbarProtocol'](8, 1 /* area restore */); }); + MAIN_THREAD_EM_ASM({ + if (Module['webbarIdentity']) Module['webbarIdentity']($0, $1, $2, $3); + }, gu->myPlayerNum, gu->myTeam, gu->myAllyTeam, int(gu->spectating)); + for (const UnitDef& def: unitDefHandler->GetUnitDefsVec()) { + if (!WebbarSupports(&def)) continue; + MAIN_THREAD_EM_ASM({ + if (Module['webbarCatalog']) Module['webbarCatalog']($0, UTF8ToString($1), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11); + }, def.id, def.name.c_str(), def.cost.metal, def.cost.energy, def.buildTime, def.xsize * SQUARE_SIZE, def.zsize * SQUARE_SIZE, def.IsBuilderUnit(), def.IsFactoryUnit(), def.IsExtractorUnit(), !def.IsImmobileUnit(), def.CanDamage()); + for (const auto& option: def.buildOptions) { + const UnitDef* target = unitDefHandler->GetUnitDefByName(option.second); + if (!WebbarSupports(target)) continue; + MAIN_THREAD_EM_ASM({ if (Module['webbarBuildOption']) Module['webbarBuildOption']($0, $1); }, def.id, target->id); + } + } + } + if (gs->frameNum % 30 == 0) { + // Batch descriptions into one host call, including native state labels. + // Strings are decoded synchronously before the temporary pointer table dies. + std::vector words(1, 0); + const auto pointer = [](const std::string& value) { return uint32_t(reinterpret_cast(value.c_str())); }; + for (const CUnit* unit: unitHandler.GetActiveUnits()) { + if (unit->team != gu->myTeam) continue; + ++words[0]; + const auto& commands = unit->commandAI->GetPossibleCommands(); + words.push_back(unit->id); words.push_back(commands.size()); + for (const auto* command: commands) { + words.insert(words.end(), {uint32_t(command->id), uint32_t(command->type), uint32_t(command->disabled), uint32_t(command->hidden), pointer(command->action), pointer(command->name), pointer(command->tooltip), uint32_t(command->params.size())}); + for (const auto& param: command->params) words.push_back(pointer(param)); + } + } + MAIN_THREAD_EM_ASM({ + if (!Module['webbarCommandDescriptions']) return; + const words = HEAPU32.subarray($0 >> 2, ($0 >> 2) + $1); const units = []; let cursor = 1; + for (let i = 0; i < words[0]; i++) { + const unit = words[cursor++]; const count = words[cursor++]; const commands = []; + for (let j = 0; j < count; j++) { + const id = words[cursor++] | 0; const kind = words[cursor++]; const disabled = Boolean(words[cursor++]); const hidden = Boolean(words[cursor++]); + const action = UTF8ToString(words[cursor++]); const name = UTF8ToString(words[cursor++]); const tooltip = UTF8ToString(words[cursor++]); const size = words[cursor++]; const params = []; + for (let k = 0; k < size; k++) params.push(UTF8ToString(words[cursor++])); + commands.push({id, kind, disabled, hidden, action, name, tooltip, params}); + } + units.push([unit, commands]); + } + Module['webbarCommandDescriptions'](units); + }, words.data(), words.size()); + } + const CTeam* team = teamHandler.Team(gu->myTeam); + const float economy[] = {team->res.metal, team->resStorage.metal, team->resPrevIncome.metal, team->resPrevExpense.metal, team->resPrevPull.metal, + team->res.energy, team->resStorage.energy, team->resPrevIncome.energy, team->resPrevExpense.energy, team->resPrevPull.energy}; + MAIN_THREAD_EM_ASM({ + if (Module['webbarEconomy']) Module['webbarEconomy']($0, HEAPF32.slice($1 >> 2, ($1 >> 2) + 10).buffer); + }, gs->frameNum, economy); + const auto units = WebbarUnits(); + struct Record { uint32_t id; float progress; uint32_t queued; int32_t command; uint32_t target; float metal, energy; uint32_t transporter, cargo; }; + static_assert(sizeof(Record) == 36); + const uint32_t header[] = {0x34534257, uint32_t(gs->frameNum), uint32_t(units.size()), 0}; + std::vector packet(sizeof(header) + units.size() * sizeof(Record)); + std::memcpy(packet.data(), header, sizeof(header)); + size_t offset = sizeof(header); + for (const CUnit* unit: units) { + const CUnit* target = nullptr; + if (unit->unitDef->IsFactoryUnit()) target = static_cast(unit)->curBuild; + else if (unit->unitDef->IsBuilderUnit()) target = static_cast(unit)->curBuild; + const auto& queue = unit->commandAI->commandQue; + const bool owned = unit->team == gu->myTeam; + const Record record = {uint32_t(unit->id), unit->buildProgress, owned ? uint32_t(queue.size()) : 0, (!owned || queue.empty()) ? 0 : queue.front().GetID(), + (owned && target) ? uint32_t(target->id) : 0xffffffff, owned ? unit->resourcesMake.metal : 0.0f, owned ? unit->resourcesMake.energy : 0.0f, unit->GetTransporter() ? uint32_t(unit->GetTransporter()->id) : 0xffffffff, owned ? uint32_t(unit->transportedUnits.size()) : 0}; + std::memcpy(packet.data() + offset, &record, sizeof(record)); offset += sizeof(record); + } + MAIN_THREAD_EM_ASM({ + if (Module['webbarUnitState']) Module['webbarUnitState'](HEAPU8.slice($0, $0 + $1).buffer); + }, packet.data(), packet.size()); + PublishWebbarWorld(); +} + +// Input is polled on the engine thread outside the synced simulation. Orders use +// the same local server protocol as a desktop player's selection and commands. +static void PollWebbarCommands() +{ + static const bool enabled = std::getenv("WEBBAR_PLAYABLE") != nullptr; + if (!enabled || !game->playing || gs->frameNum < 0) + return; + GetWebbarEffects(); + PollWebbarTerrainFixture(); + if (std::getenv("WEBBAR_NETWORK") == nullptr) GetWebbarSkirmish(); + static bool testSpeedSet = false; + if (!testSpeedSet) { + testSpeedSet = true; + if (const char* speed = std::getenv("WEBBAR_AI_TEST_SPEED")) + clientNet->Send(CBaseNetProtocol::Get().SendUserSpeed(gu->myPlayerNum, std::clamp(std::strtof(speed, nullptr), 1.0f, 20.0f))); + } + + static int lastPaused = -1; + if (lastPaused != int(gs->paused)) { + lastPaused = int(gs->paused); + MAIN_THREAD_EM_ASM({ + if (Module['webbarState']) Module['webbarState']($0, $1); + }, lastPaused, gs->frameNum); + } + + struct BrowserOrder { + uint32_t request, kind, count, target, options; + float x, z, radius; + uint32_t units[64]; + }; + static_assert(sizeof(BrowserOrder) == 288); + for (int batch = 0; batch < 8; ++batch) { + BrowserOrder order = {}; + const int available = MAIN_THREAD_EM_ASM_INT({ + return Module['webbarPollCommand'] ? Module['webbarPollCommand']($0) : 0; + }, &order); + if (!available) + break; + + int status = 0; + float3 buildPos(order.x, 0.0f, order.z); + std::vector ids; + if (order.count > 64 || gu->spectating || !playerHandler.IsValidPlayer(gu->myPlayerNum)) { + status = 1; + } else if (order.kind == 1000) { + clientNet->Send(CBaseNetProtocol::Get().SendPause(gu->myPlayerNum, order.target != 0)); + } else { + for (uint32_t i = 0; i < order.count; ++i) { + const CUnit* unit = unitHandler.GetUnit(order.units[i]); + if (unit != nullptr && unit->team == gu->myTeam && !unit->isDead && std::find(ids.begin(), ids.end(), unit->id) == ids.end()) + ids.push_back(unit->id); + } + Command command(order.kind, uint8_t(order.options & SHIFT_KEY)); + const auto description = [](int id, int kind) -> const SCommandDescription* { + for (const auto* desc: unitHandler.GetUnit(id)->commandAI->GetPossibleCommands()) + if (desc->id == kind && !desc->disabled && !desc->hidden) return desc; + return nullptr; + }; + const auto requireCapability = [&](int kind) { + std::erase_if(ids, [&](int id) { return unitHandler.GetUnit(id)->beingBuilt || description(id, kind) == nullptr; }); + if (ids.empty()) status = 8; + }; + const auto validPosition = [&] { + return std::isfinite(order.x) && std::isfinite(order.z) && order.x >= 0 && order.z >= 0 && order.x < mapDims.mapx * SQUARE_SIZE && order.z < mapDims.mapy * SQUARE_SIZE; + }; + if (ids.empty()) { + status = 2; + } else if (order.kind == CMD_MOVE || order.kind == CMD_FIGHT || order.kind == CMD_PATROL) { + if (!std::isfinite(order.x) || !std::isfinite(order.z) || order.x < 0 || order.z < 0 || order.x >= mapDims.mapx * SQUARE_SIZE || order.z >= mapDims.mapy * SQUARE_SIZE) { + status = 3; + } else { + command.PushPos(float3(order.x, CGround::GetHeightReal(order.x, order.z), order.z)); + } + } else if (order.kind == CMD_FIRE_STATE || order.kind == CMD_MOVE_STATE || order.kind == CMD_REPEAT || order.kind == CMD_ONOFF || order.kind == 37382 /* BAR CMD_WANT_CLOAK */ || order.kind == CMD_TRAJECTORY || order.kind == CMD_AUTOREPAIRLEVEL || order.kind == CMD_IDLEMODE) { + requireCapability(order.kind); + for (int id: ids) { + const auto* desc = description(id, order.kind); + if (desc->type != CMDTYPE_ICON_MODE || desc->params.size() < 2 || order.target >= desc->params.size() - 1) status = 6; + } + command = Command(order.kind); + command.PushParam(float(order.target)); + } else if (order.kind == CMD_LOAD_UNITS) { + requireCapability(CMD_LOAD_UNITS); + std::erase(ids, int(order.target)); + const CUnit* target = unitHandler.GetUnit(order.target); + if (ids.empty()) status = 8; + else if (target == nullptr || target->isDead || target->team != gu->myTeam || target->beingBuilt || target->GetTransporter() != nullptr) status = 4; + else { + std::erase_if(ids, [&](int id) { return !unitHandler.GetUnit(id)->CanTransport(target); }); + if (ids.empty()) status = 8; + command.PushParam(float(order.target)); + } + } else if (order.kind == CMD_UNLOAD_UNITS) { + requireCapability(CMD_UNLOAD_UNITS); + if (!validPosition() || !std::isfinite(order.radius) || order.radius <= 0.0f || order.radius > 2000.0f) status = 3; + else { + command.PushPos(float3(order.x, CGround::GetHeightReal(order.x, order.z), order.z)); + command.PushParam(order.radius); + } + } else if (order.kind == CMD_GUARD) { + requireCapability(CMD_GUARD); + std::erase(ids, int(order.target)); + const CUnit* target = unitHandler.GetUnit(order.target); + if (ids.empty()) status = 8; + else if (target == nullptr || target->isDead || target->team != gu->myTeam) status = 4; + else command.PushParam(float(order.target)); + } else if (order.kind == CMD_RESTORE) { + requireCapability(CMD_RESTORE); + // Match BuilderCAI's 200-unit restore limit, including the preview. + if (!validPosition() || !std::isfinite(order.radius) || order.radius <= 0.0f || order.radius > 200.0f) status = 3; + else { + command.PushPos(float3(order.x, CGround::GetHeightReal(order.x, order.z), order.z)); + command.PushParam(order.radius); + } + } else if (order.kind == 1006 || order.kind == 1007) { + const int kind = order.kind == 1006 ? CMD_REPAIR : CMD_RECLAIM; + requireCapability(kind); + if (!validPosition() || !std::isfinite(order.radius) || order.radius <= 0.0f || order.radius > 2000.0f) status = 3; + else { + command = Command(kind, uint8_t(order.options & SHIFT_KEY)); + command.PushPos(float3(order.x, CGround::GetHeightReal(order.x, order.z), order.z)); + command.PushParam(order.radius); + } + } else if (order.kind == CMD_STOCKPILE || order.kind == 1009) { + requireCapability(CMD_STOCKPILE); + command = Command(CMD_STOCKPILE, order.kind == 1009 ? RIGHT_MOUSE_KEY : 0); + } else if ((order.kind == CMD_ATTACK || order.kind == CMD_MANUALFIRE) && order.target == 0xffffffff) { + requireCapability(order.kind); + if (!validPosition()) status = 3; + else command.PushPos(float3(order.x, CGround::GetHeightReal(order.x, order.z), order.z)); + } else if (order.kind == CMD_ATTACK || order.kind == CMD_MANUALFIRE) { + const CUnit* target = unitHandler.GetUnit(order.target); + if (target == nullptr || target->isDead || teamHandler.Ally(gu->myAllyTeam, target->allyteam) || !WebbarVisible(target)) + status = 4; + else + command.PushParam(float(order.target)); + } else if (order.kind == CMD_REPAIR || order.kind == CMD_RECLAIM) { + std::erase_if(ids, [&](int id) { const auto* def = unitHandler.GetUnit(id)->unitDef; return !def->IsBuilderUnit() || (order.kind == CMD_REPAIR ? !def->canRepair : !def->canReclaim); }); + const CUnit* target = unitHandler.GetUnit(order.target); + if (ids.empty()) status = 8; + else if (target == nullptr || target->team != gu->myTeam || target->isDead) status = 4; + else command.PushParam(float(order.target)); + } else if (order.kind == 1004) { + // Wreck IDs have a distinct command kind; never trust a browser-supplied offset. + std::erase_if(ids, [&](int id) { return !unitHandler.GetUnit(id)->unitDef->canReclaim; }); + const CFeature* feature = featureHandler.GetFeature(order.target); + if (ids.empty()) status = 8; + else if (feature == nullptr || !feature->def->reclaimable || !losHandler->InLos(feature->pos, gu->myAllyTeam)) status = 4; + else { command = Command(CMD_RECLAIM, uint8_t(order.options & SHIFT_KEY)); command.PushParam(float(order.target + unitHandler.MaxUnits())); } + } else if (order.kind == 1005) { + // Native queue tags make individual cancellation unambiguous. + std::erase_if(ids, [&](int id) { const auto& queue = unitHandler.GetUnit(id)->commandAI->commandQue; return std::none_of(queue.begin(), queue.end(), [&](const Command& c) { return c.GetTag() == order.target; }); }); + if (ids.empty()) status = 4; + else { + // CTRL selects the factory production queue instead of its rally queue. + command = Command(CMD_REMOVE, CONTROL_KEY); + command.PushParam(float(order.target)); + } + } else if (order.kind >= 1001 && order.kind <= 1003) { + const UnitDef* def = unitDefHandler->GetUnitDefByID(order.target); + if (!WebbarSupports(def)) { + status = 8; + } else { + std::erase_if(ids, [&](int id) { + const CUnit* unit = unitHandler.GetUnit(id); + if (unit->beingBuilt || unit->unitDef->IsFactoryUnit() != (order.kind == 1003)) return true; + for (const auto& option: unit->unitDef->buildOptions) if (option.second == def->name) return false; + return true; + }); + if (ids.empty()) { + status = 8; + } else if (order.kind == 1003) { + command = Command(-def->id); // one unit per click; SHIFT means five to a native factory + } else if (!def->IsBuildingUnit() || !std::isfinite(order.x) || !std::isfinite(order.z) || order.x < 0 || order.z < 0 || order.x >= mapDims.mapx * SQUARE_SIZE || order.z >= mapDims.mapy * SQUARE_SIZE) { + status = 3; + } else { + BuildInfo build(def, buildPos, (order.options >> 8) & 3); + build.pos = buildPos = CGameHelper::Pos2BuildPos(build, true); + CFeature* feature = nullptr; + if (CGameHelper::TestUnitBuildSquare(build, feature, gu->myAllyTeam, true) == CGameHelper::BUILDSQUARE_BLOCKED) status = 7; + command = build.CreateCommand(uint8_t(order.options & SHIFT_KEY)); + } + } + } else if (order.kind != CMD_STOP) { + status = 5; + } + if (order.kind == CMD_PATROL || order.kind == CMD_MANUALFIRE) requireCapability(order.kind); + if (status == 0 && order.kind != 1001) + selectedUnitsHandler.SendCommandsToUnits(ids, {command}, false); + } + MAIN_THREAD_EM_ASM({ + if (Module['webbarCommandResult']) Module['webbarCommandResult']($0, $1, $2, $3, $4, $5, $6); + }, order.request, status, ids.size(), gs->frameNum, buildPos.x, buildPos.y, buildPos.z); + } +} + +// Separate, versioned pose stream used only by the playable client. Compose +// matrices from copied animation state, without updating the engine's caches. +static void PublishWebbarPoses() +{ + const auto units = WebbarUnits(); + uint32_t pieceCount = 0; + for (const CUnit* unit: units) + pieceCount += unit->localModel.pieces.size(); + const uint32_t header[] = {0x324d4257, uint32_t(gs->frameNum), uint32_t(units.size()), pieceCount}; + std::vector packet(16 + units.size() * 80 + pieceCount * 72); + uint8_t* cursor = packet.data(); + auto append = [&](const void* data, size_t bytes) { std::memcpy(cursor, data, bytes); cursor += bytes; }; + append(header, sizeof(header)); + static std::vector definitions; + for (const CUnit* unit: units) { + if (std::find(definitions.begin(), definitions.end(), unit->unitDef->id) == definitions.end()) { + definitions.push_back(unit->unitDef->id); + MAIN_THREAD_EM_ASM({ + if (Module['webbarDefinition']) Module['webbarDefinition']($0, UTF8ToString($1)); + }, unit->unitDef->id, unit->unitDef->name.c_str()); + } + const auto& pieces = unit->localModel.pieces; + const uint32_t record[] = {uint32_t(unit->id), uint32_t(unit->unitDef->id), uint32_t(unit->team), uint32_t(pieces.size())}; + append(record, sizeof(record)); + const CMatrix44f world = unit->GetTransformMatrix(true); + append(world.m, sizeof(world.m)); + std::vector matrices(pieces.size()); + for (size_t i = 0; i < pieces.size(); ++i) { + const auto& piece = pieces[i]; + const CMatrix44f local = piece.CalcPieceSpaceTransform(piece.GetPosition(), piece.GetRotation(), piece.GetScaling()).ToMatrix(); + matrices[i] = piece.parent ? matrices[piece.parent->GetLModelPieceIndex()] * local : local; + const uint32_t flags[] = {uint32_t(i), uint32_t(piece.GetScriptVisible())}; + append(flags, sizeof(flags)); + append(matrices[i].m, sizeof(matrices[i].m)); + } + } + MAIN_THREAD_EM_ASM({ + if (Module['webbarPoses']) Module['webbarPoses'](HEAPU8.slice($0, $0 + $1).buffer); + }, packet.data(), packet.size()); +} + +// Read-only observer, after LEAVE_SYNCED_CODE. This never drives game state. +static void PublishWebbarFrame() +{ + static const char* output = std::getenv("WEBBAR_SNAPSHOTS"); + static const bool browser = std::getenv("WEBBAR_BROWSER") != nullptr; + if (browser && std::getenv("WEBBAR_PLAYABLE") != nullptr && gs->frameNum >= 0) GetWebbarEffects().Publish(); + if ((!output && !browser) || gs->frameNum % 3 != 0) + return; + + const auto units = WebbarUnits(); + std::vector projectiles; + const bool playable = std::getenv("WEBBAR_PLAYABLE") != nullptr; + for (const CProjectile* p: projectileHandler.GetActiveProjectiles(true)) + if (!playable || WebbarProjectileVisible(p)) projectiles.push_back(p); + struct UnitRecord { + uint32_t id, def, team, kind; + float x, y, z, heading, health, radius, vx, vz; + }; + struct ProjectileRecord { uint32_t id, team; float x, y, z; }; + static_assert(sizeof(UnitRecord) == 48 && sizeof(ProjectileRecord) == 20); + const uint32_t header[] = {0x31524257, uint32_t(gs->frameNum), uint32_t(units.size()), + uint32_t(projectiles.size()), CSyncChecker::GetChecksum(), uint32_t(mapDims.mapx * SQUARE_SIZE), + uint32_t(mapDims.mapy * SQUARE_SIZE), 1}; + std::vector packet(sizeof(header) + units.size() * sizeof(UnitRecord) + projectiles.size() * sizeof(ProjectileRecord)); + uint8_t* cursor = packet.data(); + auto append = [&](const auto& record) { + std::memcpy(cursor, &record, sizeof(record)); + cursor += sizeof(record); + }; + append(header); + for (const CUnit* unit: units) { + const UnitRecord record = {uint32_t(unit->id), uint32_t(unit->unitDef->id), uint32_t(unit->team), + unit->unitDef->canfly ? 1u : (unit->unitDef->IsImmobileUnit() ? 2u : 0u), + unit->pos.x, unit->pos.y, unit->pos.z, float(unit->heading) * (3.14159265358979323846f / 32768.0f), + unit->health / std::max(1.0f, unit->maxHealth), unit->radius, unit->speed.x, unit->speed.z}; + append(record); + } + for (const CProjectile* projectile: projectiles) { + const ProjectileRecord record = {uint32_t(projectile->id), projectile->GetTeamID(), + projectile->pos.x, projectile->pos.y, projectile->pos.z}; + append(record); + } + if (output) { + static FILE* file = std::fopen(output, "wb"); + if (file) { + std::fwrite(packet.data(), 1, packet.size(), file); + std::fflush(file); + } + } + if (browser) { + if (std::getenv("WEBBAR_PLAYABLE") != nullptr) { + PublishWebbarTerrain(); + PublishWebbarPlayState(); + PublishWebbarPoses(); + } + MAIN_THREAD_EM_ASM({ + if (Module['webbarSnapshot']) Module['webbarSnapshot'](HEAPU8.slice($0, $0 + $1).buffer); + }, packet.data(), packet.size()); + } +} diff --git a/rts/Game/WebbarTerrain.h b/rts/Game/WebbarTerrain.h new file mode 100644 index 0000000..2993657 --- /dev/null +++ b/rts/Game/WebbarTerrain.h @@ -0,0 +1,68 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#pragma once +#include "Map/ReadMap.h" +#include "System/Rectangle.h" + +// WHT1 is an additive, independently versioned presentation stream. Only the +// native UNSYNCED height map may cross this bridge. Never read the live synced +// grid here: it includes deformation outside the player's current knowledge. +class WebbarTerrain final: public CEventClient { +public: + WebbarTerrain(): CEventClient("Webbar visible terrain", 99999, false) { + width = mapDims.mapxp1; height = mapDims.mapyp1; + if (width > 2049 || height > 2049) return; + columns = (width - 2) / 64 + 1; rows = (height - 2) / 64 + 1; + dirty.assign(columns * rows, 1); + const float* original = readMap->GetOriginalHeightMapSynced(); + previous.assign(original, original + width * height); + eventHandler.AddClient(this); + } + bool WantsEvent(const std::string& name) override { return name == "UnsyncedHeightMapUpdate"; } + void UnsyncedHeightMapUpdate(const SRectangle& rect) override { + if (dirty.empty()) return; + // Inclusive corner rectangles; shared boundary vertices belong to both tiles. + const int x0 = std::max(0, rect.x1 - 1) / 64, z0 = std::max(0, rect.z1 - 1) / 64; + for (int z = z0; z <= std::min(rows - 1, rect.z2 / 64); ++z) + for (int x = x0; x <= std::min(columns - 1, rect.x2 / 64); ++x) dirty[z * columns + x] = 1; + } + void Publish() { + if (dirty.empty()) return; + const float* visible = readMap->GetCornerHeightMapUnsynced(); + std::vector packet = {0x31544857, uint32_t(gs->frameNum), sequence + 1, uint32_t(width), uint32_t(height), 0, 0, 0}; + int checked = 0; + for (size_t visited = 0; visited < dirty.size() && checked < 64 && packet[5] < 16; ++visited) { + const int tile = cursor; cursor = (cursor + 1) % dirty.size(); + if (!dirty[tile]) continue; + ++checked; dirty[tile] = 0; + const int x = (tile % columns) * 64, z = (tile / columns) * 64; + const int w = std::min(65, width - x), h = std::min(65, height - z); + bool changed = false; + for (int row = 0; row < h && !changed; ++row) + changed = std::memcmp(visible + (z + row) * width + x, previous.data() + (z + row) * width + x, w * sizeof(float)) != 0; + if (!changed) continue; + packet.insert(packet.end(), {uint32_t(x), uint32_t(z), uint32_t(w), uint32_t(h)}); + const size_t start = packet.size(); packet.resize(start + w * h); + for (int row = 0; row < h; ++row) { + const float* source = visible + (z + row) * width + x; + std::memcpy(packet.data() + start + row * w, source, w * sizeof(float)); + std::memcpy(previous.data() + (z + row) * width + x, source, w * sizeof(float)); + } + ++packet[5]; packet[6] += w * h; + } + if (sequence != 0 && packet[5] == 0) return; + ++sequence; + MAIN_THREAD_EM_ASM({ if (Module['webbarTerrain']) Module['webbarTerrain'](HEAPU8.slice($0, $0 + $1).buffer); }, packet.data(), packet.size() * sizeof(uint32_t)); + } +private: + int width = 0, height = 0, columns = 0, rows = 0, cursor = 0; + uint32_t sequence = 0; + std::vector dirty; + std::vector previous; +}; +static void PublishWebbarTerrain() { + static WebbarTerrain terrain; + // HEADLESS has no guaranteed graphical WorldDrawer tick. Drain its existing + // visibility-filtered update queue, then publish the resulting unsynced grid. + readMap->UpdateDraw(false); + terrain.Publish(); +} diff --git a/rts/Game/WebbarTerrainFixture.h b/rts/Game/WebbarTerrainFixture.h new file mode 100644 index 0000000..9114a35 --- /dev/null +++ b/rts/Game/WebbarTerrainFixture.h @@ -0,0 +1,29 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#pragma once +#include "Map/MapDamage.h" +// Explicit acceptance fixture, never installed in network games. It asks the +// engine's real crater implementation to modify one visible and one hidden spot. +// Diagnostic synced heights are emitted ONLY by this opt-in fixture. +class WebbarTerrainFixture final: public CEventClient { +public: + WebbarTerrainFixture(): CEventClient("Webbar terrain acceptance", 100001, true) { eventHandler.AddClient(this); } + bool WantsEvent(const std::string& name) override { return name == "GameFrame"; } + void GameFrame(int frame) override { + if (frame == 90) { + for (const auto& p: positions) {float diff = 0;mapDamage->Explosion(float3(p.x, CGround::GetHeightReal(p.x,p.y), p.y), 20000, 120, diff);} + } + if (frame != 60 && frame != 120 && frame % 90 != 0) return; + for (const auto& p: positions) { + const int index = int(p.y / SQUARE_SIZE) * mapDims.mapxp1 + int(p.x / SQUARE_SIZE); + const float synced = readMap->GetCornerHeightMapSynced()[index], visible = readMap->GetCornerHeightMapUnsynced()[index]; + const bool los = losHandler->InLos(float3(p.x,synced,p.y),gu->myAllyTeam); + MAIN_THREAD_EM_ASM({if(Module['webbarTerrainCheck'])Module['webbarTerrainCheck']({frame:$0,disabled:!!$1,x:$2,z:$3,synced:$4,visible:$5,los:!!$6});},frame,int(mapDamage->Disabled()),p.x,p.y,synced,visible,int(los)); + } + } +private: + const std::array positions={float2(6784,3392),float2(8000,4000)}; +}; +static void PollWebbarTerrainFixture() { + if(std::getenv("WEBBAR_TERRAIN_TEST") == nullptr || std::getenv("WEBBAR_NETWORK") != nullptr)return; + static WebbarTerrainFixture fixture; +}