--- a/rts/Sim/Units/Unit.cpp +++ b/rts/Sim/Units/Unit.cpp @@ -3,6 +3,9 @@ #include "System/RangesCompat.h" #include "UnitDef.h" #include "Unit.h" +#ifdef __EMSCRIPTEN__ +#include "Game/WebbarCloakEvents.h" +#endif #include "UnitHandler.h" #include "UnitDefHandler.h" #include "UnitLoader.h" @@ -103,6 +106,9 @@ CUnit::~CUnit() { +#ifdef __EMSCRIPTEN__ + webbarCloakEvents::Forget(id); +#endif RECOIL_DETAILED_TRACY_ZONE; assert(unitMemPool.mapped(this)); // clean up if we are still under MoveCtrl here @@ -2449,8 +2455,14 @@ if (oldCloak != newCloak) { if (newCloak) { eventHandler.UnitCloaked(this); +#ifdef __EMSCRIPTEN__ + webbarCloakEvents::Record(id, gs->frameNum, true, team, gu->myTeam); +#endif } else { eventHandler.UnitDecloaked(this); +#ifdef __EMSCRIPTEN__ + webbarCloakEvents::Record(id, gs->frameNum, false, team, gu->myTeam); +#endif } } @@ -2493,6 +2505,9 @@ isCloaked = false; eventHandler.UnitDecloaked(this); +#ifdef __EMSCRIPTEN__ + webbarCloakEvents::Record(id, gs->frameNum, false, team, gu->myTeam); +#endif return true; } --- a/rts/Game/WebbarCloak.h +++ b/rts/Game/WebbarCloak.h @@ -2,6 +2,7 @@ #pragma once #include "Lua/LuaRulesParams.h" +#include "WebbarCloakEvents.h" // Owner-only, read-only presentation. BAR owns cloak requests, resource use, // proximity checks and recloak delays. Never infer a reason by scanning enemies. @@ -10,8 +11,9 @@ struct Record { uint32_t unit, flags; float idleCost, movingCost, speed, decloakDistance; + int32_t transitionFrame; }; - static_assert(sizeof(Record) == 24); + static_assert(sizeof(Record) == 28); std::vector records; uint32_t dropped = 0; for (const CUnit* unit: unitHandler.GetActiveUnits()) { @@ -31,12 +33,23 @@ (uint32_t(rule("areacloaked") == 1) << 6) | (uint32_t(unit->IsStunned()) << 7) | (uint32_t(unit->beingBuilt) << 8) | (uint32_t(unit->stealth) << 9) | (uint32_t(unit->sonarStealth) << 10); records.push_back({uint32_t(unit->id), flags, std::max(0.0f, def->cloakCost), std::max(0.0f, def->cloakCostMoving), - unit->speed.w, std::max(0.0f, unit->decloakDistance)}); + unit->speed.w, std::max(0.0f, unit->decloakDistance), webbarCloakEvents::Frame(unit->id)}); } - const uint32_t header[] = {0x31434257, uint32_t(gs->frameNum), uint32_t(records.size()), dropped}; - std::vector packet(sizeof(header) + records.size() * sizeof(Record)); + std::vector events; + for (const auto& entry: webbarCloakEvents::pending) { + const CUnit* unit = unitHandler.GetUnit(entry.event.unit); + if (unit == nullptr || unit->isDead || unit->team != gu->myTeam || unit->team != entry.team || + entry.event.frame > uint32_t(gs->frameNum) || uint32_t(gs->frameNum) - entry.event.frame > 6) continue; + events.push_back(entry.event); + } + webbarCloakEvents::pending.clear(); + const uint32_t header[] = {0x33434257, uint32_t(gs->frameNum), uint32_t(records.size()), dropped, + uint32_t(events.size()), webbarCloakEvents::dropped}; + webbarCloakEvents::dropped = 0; + std::vector packet(sizeof(header) + records.size() * sizeof(Record) + events.size() * sizeof(webbarCloakEvents::Event)); std::memcpy(packet.data(), header, sizeof(header)); if (!records.empty()) std::memcpy(packet.data() + sizeof(header), records.data(), records.size() * sizeof(Record)); + if (!events.empty()) std::memcpy(packet.data() + sizeof(header) + records.size() * sizeof(Record), events.data(), events.size() * sizeof(webbarCloakEvents::Event)); MAIN_THREAD_EM_ASM({ if (Module['webbarCloak']) Module['webbarCloak'](HEAPU8.slice($0, $0 + $1).buffer); }, packet.data(), packet.size()); --- /dev/null +++ b/rts/Game/WebbarCloakEvents.h @@ -0,0 +1,47 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#pragma once + +#include +#include +#include +#include + +// Read-only presentation history, outside all simulation and checksum state. +// Called at the native cloak events, using BAR GL4's signed-frame convention. +// Destruction retires the entry before a unit ID can be reused. Only the +// owner-filtered WBC3 publisher can expose this history to the browser. +namespace webbarCloakEvents { + +inline std::unordered_map frames; +struct Event { uint32_t id, unit, frame, cloaked; }; +static_assert(sizeof(Event) == 16); +struct Pending { Event event; int team; }; +inline std::vector pending; +inline uint32_t nextID = 0, dropped = 0; + +inline void Record(int unitID, int frame, bool cloaked, int team, int localTeam) +{ + frames[unitID] = cloaked ? frame : -frame; + // Assign IDs and count overflow only for our own native events. Hidden + // enemy activity must not be observable through gaps or drop counters. + if (team != localTeam || frame < 0) return; + if (pending.size() >= 1024) { ++dropped; return; } + if (++nextID == 0) ++nextID; + pending.push_back({{nextID, uint32_t(unitID), uint32_t(frame), uint32_t(cloaked)}, team}); +} + +inline void Forget(int unitID) +{ + frames.erase(unitID); + pending.erase(std::remove_if(pending.begin(), pending.end(), [unitID](const Pending& entry) { + return entry.event.unit == uint32_t(unitID); + }), pending.end()); +} + +inline int Frame(int unitID) +{ + const auto it = frames.find(unitID); + return it != frames.end() ? it->second : 0; +} + +}