--- a/rts/Game/WebbarBrowser.h +++ b/rts/Game/WebbarBrowser.h @@ -63,6 +63,7 @@ #include "WebbarResurrection.h" #include "WebbarNano.h" #include "WebbarSkirmish.h" +#include "WebbarReports.h" #include "WebbarPlacement.h" #include "WebbarWorld.h" #include "WebbarTerrain.h" @@ -74,7 +75,9 @@ 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); + // Spectators get a neutral result; the per-team won flags carry the winners. + const int outcome = (winners.empty() || gu->spectating) ? 3 : (std::find(winners.begin(), winners.end(), gu->myAllyTeam) != winners.end() ? 1 : 2); + PublishWebbarTeamStats(true, &winners); MAIN_THREAD_EM_ASM({ if (Module['webbarMatch']) Module['webbarMatch']($0, $1, 0, 0, $0); }, gs->frameNum, outcome); } }; @@ -84,8 +87,9 @@ 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'](9, 8191 /* restore | shields | cloak | resurrection | native work effects | area resurrection | manual launch | capture | smart trajectory | energy conversion | geothermal | selectability | build restrictions */); }); + if (std::getenv("WEBBAR_NETWORK") != nullptr) { static WebbarNetworkResult result; } else { static WebbarStatsResult stats; } + MAIN_THREAD_EM_ASM({ if (Module['webbarProtocol']) Module['webbarProtocol'](9, 65535 /* restore | shields | cloak | resurrection | native work effects | area resurrection | manual launch | capture | smart trajectory | energy conversion | geothermal | selectability | build restrictions | queue edit | game speed | engine memory */); }); + PublishWebbarEngineMemory(); MAIN_THREAD_EM_ASM({ if (Module['webbarIdentity']) Module['webbarIdentity']($0, $1, $2, $3); }, gu->myPlayerNum, gu->myTeam, gu->myAllyTeam, int(gu->spectating)); @@ -159,6 +163,9 @@ if (Module['webbarUnitState']) Module['webbarUnitState'](HEAPU8.slice($0, $0 + $1).buffer); }, packet.data(), packet.size()); PublishWebbarShields(); + if (gs->frameNum % 15 == 0) PublishWebbarUnitModifiers(); + if (gs->frameNum % 300 == 0) PublishWebbarEngineMemory(); + if (gs->frameNum > 0 && gs->frameNum % (TeamStatistics::statsPeriod * GAME_SPEED) == 0) PublishWebbarTeamStats(false, nullptr); webbarWorkAudio::Publish(); PublishWebbarWorld(); PublishWebbarResurrection(); @@ -181,6 +188,7 @@ clientNet->Send(CBaseNetProtocol::Get().SendUserSpeed(gu->myPlayerNum, std::clamp(std::strtof(speed, nullptr), 1.0f, 20.0f))); } + PublishWebbarSpeed(); static int lastPaused = -1; if (lastPaused != int(gs->paused)) { lastPaused = int(gs->paused); @@ -214,6 +222,11 @@ status = SetWebbarEnergyReserve(order.count, order.target, order.options); } else if (order.kind == 1000) { clientNet->Send(CBaseNetProtocol::Get().SendPause(gu->myPlayerNum, order.target != 0)); + } else if (order.kind == 1013) { + // Solo game speed (original setspeed): networked matches keep the shared speed. + if (std::getenv("WEBBAR_NETWORK") != nullptr) status = 1; + else if (!std::isfinite(order.x) || order.x <= 0.2f || order.x < gameSetup->minSpeed || order.x > gameSetup->maxSpeed) status = 3; + else clientNet->Send(CBaseNetProtocol::Get().SendUserSpeed(gu->myPlayerNum, order.x)); } else { for (uint32_t i = 0; i < order.count; ++i) { const CUnit* unit = unitHandler.GetUnit(order.units[i]); @@ -309,8 +322,10 @@ 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 || order.kind == BAR_MANUAL_LAUNCH) { + requireCapability(order.kind); const CUnit* target = unitHandler.GetUnit(order.target); - if (target == nullptr || target->isDead || teamHandler.Ally(gu->myAllyTeam, target->allyteam) || !WebbarVisible(target)) + if (ids.empty()) status = 8; + else if (target == nullptr || target->isDead || teamHandler.Ally(gu->myAllyTeam, target->allyteam) || !WebbarVisible(target)) status = 4; else command.PushParam(float(order.target)); @@ -353,6 +368,45 @@ // 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 == 1011 || order.kind == 1012) { + // Native queue edits: CMD_INSERT by tag (CommandAI::ExecuteInsert), then CMD_REMOVE for moves. + if (ids.size() != 1) status = 8; + else { + CUnit* unit = unitHandler.GetUnit(ids[0]); + const auto& queue = unit->commandAI->commandQue; + const bool factory = unit->unitDef->IsFactoryUnit(); + const uint8_t queueOpt = factory ? CONTROL_KEY : 0; + const auto find = [&](uint32_t tag) { return std::find_if(queue.begin(), queue.end(), [&](const Command& c) { return c.GetTag() == tag; }); }; + const bool toEnd = order.kind == 1011 && order.x < 0.0f; + const uint32_t anchor = toEnd ? 0 : uint32_t(order.x); + if (!toEnd && (!std::isfinite(order.x) || order.x >= 16777216.0f || find(anchor) == queue.end())) status = 4; + else if (order.kind == 1011) { + const auto moving = find(order.target); + if (moving == queue.end() || (moving->GetOpts() & INTERNAL_ORDER) || (!toEnd && moving->GetTag() == anchor)) status = 4; + else { + Command insert(CMD_INSERT, uint8_t(queueOpt | (toEnd ? ALT_KEY : 0))); + insert.PushParam(toEnd ? -1.0f : float(anchor)); + insert.PushParam(float(moving->GetID())); + insert.PushParam(float(moving->GetOpts())); + for (unsigned int p = 0; p < moving->GetNumParams(); ++p) insert.PushParam(moving->GetParam(p)); + Command remove(CMD_REMOVE, queueOpt); + remove.PushParam(float(order.target)); + selectedUnitsHandler.SendCommandsToUnits(ids, {insert, remove}, false); + } + } else { + const UnitDef* def = unitDefHandler->GetUnitDefByID(order.target); + const bool option = factory && def != nullptr && std::any_of(unit->unitDef->buildOptions.begin(), unit->unitDef->buildOptions.end(), [&](const auto& o) { return o.second == def->name; }); + if (!option || !WebbarSupports(def) || unit->beingBuilt) status = 8; + else if (WebbarBuildRestriction(def->id) != nullptr) status = 9; + else { + Command insert(CMD_INSERT, uint8_t(CONTROL_KEY | (order.radius > 0.5f ? RIGHT_MOUSE_KEY : 0))); + insert.PushParam(float(anchor)); + insert.PushParam(float(-def->id)); + insert.PushParam(0.0f); + selectedUnitsHandler.SendCommandsToUnits(ids, {insert}, false); + } + } } } else if (order.kind >= 1001 && order.kind <= 1003) { const UnitDef* def = unitDefHandler->GetUnitDefByID(order.target); @@ -388,7 +442,7 @@ status = 5; } if (order.kind == CMD_PATROL || order.kind == CMD_MANUALFIRE || order.kind == BAR_MANUAL_LAUNCH) requireCapability(order.kind); - if (status == 0 && order.kind != 1001) + if (status == 0 && order.kind != 1001 && order.kind != 1011 && order.kind != 1012) selectedUnitsHandler.SendCommandsToUnits(ids, {command}, false); } MAIN_THREAD_EM_ASM({ --- /dev/null +++ b/rts/Game/WebbarEffectQueue.h @@ -0,0 +1,47 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#pragma once + +// Browser-only, read-only observation queues filled at original native callsites +// (weapon fire sound, script/Lua/trail CEG spawns). Drained by WebbarEffects and +// WebbarBrowser after leaving synced code. Never consume RNG or change sim state. +#ifdef __EMSCRIPTEN__ +#include +#include +#include +#include "Game/GlobalUnsynced.h" +#include "Sim/Misc/GlobalSynced.h" +#include "Sim/Misc/LosHandler.h" +#include "Sim/Misc/TeamHandler.h" +#include "Sim/Units/Unit.h" +#include "Sim/Weapons/Weapon.h" +#include "System/float3.h" + +namespace webbarFireAudio { +struct Fire { const WeaponDef* def; int owner; float3 pos, speed; int frame; }; +inline std::vector pending; inline uint32_t dropped = 0; +// Same admission as projectile records: allied, or owner in LOS of the local ally team. +inline void Record(const CWeapon* w) { + const CUnit* o = w->owner; if (o == nullptr || gs->frameNum < 0) return; + if (!gu->spectatingFullView && !teamHandler.Ally(o->allyteam, gu->myAllyTeam) && !o->IsInLosForAllyTeam(gu->myAllyTeam)) return; + if (pending.size() >= 1024) { ++dropped; return; } + pending.push_back({w->weaponDef, o->id, o->pos, o->speed, gs->frameNum}); +} +} + +namespace webbarCeg { +// origin bits: 2 unit script EmitSfx, 4 Lua SpawnCEG, 8 weapon projectile trail. 0 = not observed. +inline uint32_t origin = 0; +struct Event { unsigned int generator; float3 pos, dir; float damage, radius, gfxMod; int owner; uint32_t origin; int frame; }; +inline std::vector pending; inline uint32_t dropped = 0; +inline void Record(unsigned int generator, const float3& pos, const float3& dir, float damage, float radius, float gfxMod, const CUnit* owner) { + if (origin == 0 || generator == -1u || gs->frameNum < 0) return; + if (!std::isfinite(pos.x + pos.y + pos.z + dir.x + dir.y + dir.z + damage + radius + gfxMod)) return; + const bool full = gu->spectatingFullView; + const bool ownerVisible = owner != nullptr && (full || owner->team == gu->myTeam || owner->IsInLosForAllyTeam(gu->myAllyTeam)); + if (!full && !(owner != nullptr && origin == 2 ? ownerVisible : losHandler->InLos(pos, gu->myAllyTeam))) return; + if (pending.size() >= 2048) { ++dropped; return; } + pending.push_back({generator, pos, dir, damage, radius, gfxMod, ownerVisible ? owner->id : -1, origin, gs->frameNum}); +} +struct Scope { uint32_t previous; explicit Scope(uint32_t value): previous(origin) { origin = value; } ~Scope() { origin = previous; } }; +} +#endif --- a/rts/Game/WebbarNano.h +++ b/rts/Game/WebbarNano.h @@ -25,14 +25,17 @@ inline void Emit(const CUnit* source, const float3& start, const float3& end, float radius, bool inverse, uint32_t kind, const CUnit* unit, const CFeature* feature = nullptr) { - // Initial scope: local player's work only, with a currently visible target. - if (!source->unitDef->showNanoSpray || source->isDead || source->team != gu->myTeam) return; - if (unit != nullptr && (unit->isDead || (unit->team != gu->myTeam && !unit->IsInLosForAllyTeam(gu->myAllyTeam)))) return; - if (feature != nullptr && (feature->deleteMe || !losHandler->InLos(feature->pos, gu->myAllyTeam))) return; - if (unit == nullptr && feature == nullptr) return; + // Native desktop scope: any source the local view can see (own, allied, enemy in LOS, + // full-view spectator) with a visible unit/feature target, or a visible ground point (kind 6). + const bool full = gu->spectatingFullView; + if (!source->unitDef->showNanoSpray || source->isDead) return; + if (!full && source->team != gu->myTeam && !source->IsInLosForAllyTeam(gu->myAllyTeam)) return; + if (unit != nullptr && (unit->isDead || (!full && unit->team != gu->myTeam && !unit->IsInLosForAllyTeam(gu->myAllyTeam)))) return; + if (feature != nullptr && (feature->deleteMe || (!full && !losHandler->InLos(feature->pos, gu->myAllyTeam)))) return; + if (unit == nullptr && feature == nullptr && (kind != 6 || (!full && !losHandler->InLos(end, gu->myAllyTeam)))) return; if (pending.size() >= 1024) { ++dropped; return; } const float3 color = source->unitDef->nanoColor; - pending.push_back({++nextId, uint32_t(gs->frameNum), uint32_t(source->id), uint32_t(unit ? unit->id : feature->id), kind, uint32_t(inverse) | (feature ? 2u : 0u), + pending.push_back({++nextId, uint32_t(gs->frameNum), uint32_t(source->id), uint32_t(unit ? unit->id : feature ? feature->id : 0), kind, uint32_t(inverse) | (feature ? 2u : 0u), {start.x, start.y, start.z}, {end.x, end.y, end.z}, radius, {color.x, color.y, color.z}}); } --- /dev/null +++ b/rts/Game/WebbarReports.h @@ -0,0 +1,104 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#pragma once + +// Read-only, low-rate browser reports: native team statistics (WST1), owned-unit +// live modifiers (WRX1), engine heap attribution and game speed. Unsynced +// observation only; never changes simulation state. +#include +#include +#include "Sim/Misc/TeamStatistics.h" +#include "Sim/Units/UnitMemPool.h" +#include "Sim/Features/FeatureMemPool.h" +#include "Sim/Projectiles/ProjectileMemPool.h" +#include "Sim/Weapons/WeaponMemPool.h" + +static_assert(sizeof(TeamStatistics) == 80); +static bool webbarFinalStatsSent = false; + +// Periodic: own + allied teams (all when full-view). Final: every non-Gaia team with won flags. +static void PublishWebbarTeamStats(bool final, const std::vector* winners) +{ + if (final) { if (webbarFinalStatsSent) return; webbarFinalStatsSent = true; } + std::vector packet(16); + uint32_t count = 0; + for (int t = 0; t < teamHandler.ActiveTeams() && count < 256; ++t) { + if (t == teamHandler.GaiaTeamID()) continue; + const CTeam* team = teamHandler.Team(t); + if (!final && !gu->spectatingFullView && !teamHandler.AlliedTeams(t, gu->myTeam)) continue; + const auto& history = team->statHistory; + if (history.empty()) continue; + const size_t first = history.size() > 2048 ? history.size() - 2048 : 0, entries = history.size() - first; + const int ally = teamHandler.AllyTeam(t); + const bool won = final && winners != nullptr && std::find(winners->begin(), winners->end(), (unsigned char)ally) != winners->end(); + const uint32_t head[] = {uint32_t(t), uint32_t(ally), uint32_t(won), uint32_t(entries)}; + const size_t at = packet.size(); packet.resize(at + 16 + entries * sizeof(TeamStatistics)); + std::memcpy(packet.data() + at, head, 16); + std::memcpy(packet.data() + at + 16, history.data() + first, entries * sizeof(TeamStatistics)); + ++count; + } + const uint32_t header[] = {0x31545357, uint32_t(gs->frameNum), count, uint32_t(final)}; + std::memcpy(packet.data(), header, 16); + MAIN_THREAD_EM_ASM({ if (Module['webbarTeamStats']) Module['webbarTeamStats'](HEAPU8.slice($0, $0 + $1).buffer); }, packet.data(), packet.size()); +} + +// Solo WebbarSkirmish outcome: 1 local ally team won, 2 the other ally teams, 3 none. +static void PublishWebbarSoloFinalStats(int outcome) +{ + if (outcome == 0 || webbarFinalStatsSent) return; + std::vector winners; + for (int a = 0; a < teamHandler.ActiveAllyTeams(); ++a) + if ((outcome == 1 && a == gu->myAllyTeam) || (outcome == 2 && a != gu->myAllyTeam && a != teamHandler.AllyTeam(teamHandler.GaiaTeamID()))) winners.push_back((unsigned char)a); + PublishWebbarTeamStats(true, &winners); +} + +// Native GameOver (BAR game_end winners) in solo and network matches. +class WebbarStatsResult final: public CEventClient +{ +public: + WebbarStatsResult(): CEventClient("Webbar team statistics", -99998, false) { eventHandler.AddClient(this); } + bool WantsEvent(const std::string& name) override { return name == "GameOver"; } + void GameOver(const std::vector& winners) override { PublishWebbarTeamStats(true, &winners); } +}; + +// WRX1: owned units only. u32 id; f32 experience, maxHealth, armoredMultiple (1 when open); +// u32 weaponCount (<= 8); then per weapon f32 reload seconds (XP-adjusted), f32 range. +static void PublishWebbarUnitModifiers() +{ + std::vector packet(16); + uint32_t count = 0; + const auto put = [&](const auto value) { const size_t at = packet.size(); packet.resize(at + 4); std::memcpy(packet.data() + at, &value, 4); }; + for (const CUnit* unit: unitHandler.GetActiveUnits()) { + if (unit->team != gu->myTeam || count >= 4096) continue; + ++count; + const uint32_t weapons = uint32_t(std::min(unit->weapons.size(), 8)); + put(uint32_t(unit->id)); put(unit->experience); put(unit->maxHealth); put(unit->armoredState ? unit->armoredMultiple : 1.0f); put(weapons); + for (uint32_t i = 0; i < weapons; ++i) { put(float(unit->weapons[i]->reloadTime) / GAME_SPEED); put(unit->weapons[i]->range); } + } + const uint32_t header[] = {0x31585257, uint32_t(gs->frameNum), count, 0}; + std::memcpy(packet.data(), header, 16); + MAIN_THREAD_EM_ASM({ if (Module['webbarUnitModifiers']) Module['webbarUnitModifiers'](HEAPU8.slice($0, $0 + $1).buffer); }, packet.data(), packet.size()); +} + +// Heap attribution (dlmalloc mallinfo + simulation object pools). QTPFS layers are not included. +static void PublishWebbarEngineMemory() +{ + const struct mallinfo info = mallinfo(); + const double values[] = {double(gs->frameNum), double(emscripten_get_heap_size()), double(info.arena), double(info.uordblks), double(info.fordblks), double(info.keepcost), + double(unitMemPool.alloc_size()), double(unitMemPool.freed_size()), double(featureMemPool.alloc_size()), double(featureMemPool.freed_size()), + double(projMemPool.alloc_size()), double(projMemPool.freed_size()), double(weaponMemPool.alloc_size()), double(weaponMemPool.freed_size())}; + MAIN_THREAD_EM_ASM({ + if (!Module['webbarEngineMemory']) return; + const v = HEAPF64.subarray($0 >> 3, ($0 >> 3) + 14); + Module['webbarEngineMemory']({frame: v[0], heapBytes: v[1], mallinfo: {arena: v[2], uordblks: v[3], fordblks: v[4], keepcost: v[5]}, + pools: {unit: {alloc: v[6], freed: v[7]}, feature: {alloc: v[8], freed: v[9]}, projectile: {alloc: v[10], freed: v[11]}, weapon: {alloc: v[12], freed: v[13]}}}); + }, values); +} + +// Requested (wanted) and actual (server lag-protected) speed, on change and while paused. +static void PublishWebbarSpeed() +{ + static float wanted = -1.0f, actual = -1.0f; static int paused = -1; + if (std::fabs(wanted - gs->wantedSpeedFactor) < 0.005f && std::fabs(actual - gs->speedFactor) < 0.005f && paused == int(gs->paused)) return; + wanted = gs->wantedSpeedFactor; actual = gs->speedFactor; paused = int(gs->paused); + MAIN_THREAD_EM_ASM({ if (Module['webbarSpeed']) Module['webbarSpeed']($0, $1, $2, $3); }, wanted, actual, paused, gs->frameNum); +} --- a/rts/Game/WebbarWeapons.h +++ b/rts/Game/WebbarWeapons.h @@ -2,6 +2,8 @@ #pragma once #include "Sim/Projectiles/WeaponProjectiles/WeaponProjectile.h" #include "Sim/Weapons/PlasmaRepulser.h" +#include "Game/WebbarEffectQueue.h" +#include // Read-only presentation. Definitions are sent once, after leaving synced code. // ProjectileCreated fires in the BASE constructor: use WeaponDef/start/target, @@ -18,9 +20,12 @@ 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; }; + // WBW2: owner team after definition (0xffffffff without one); the shot is already visible. + struct Shot { uint32_t id, definition, team; float x,y,z,vx,vy,vz,ex,ey,ez,ttl; }; + // WBC1 naval CEG record (wakes, bow splashes, Lua splashes, torpedo trails). + struct Ceg { uint32_t id, frame, generator, owner, flags, pad; float pos[3], dir[3], damage, radius, gfxMod, pad2; }; WebbarEffects(): CEventClient("Webbar browser effects", -100000, false) { eventHandler.AddClient(this); } - bool WantsEvent(const std::string& name) override { return name=="Explosion" || name=="UnitDestroyed" || name=="ProjectileCreated"; } + bool WantsEvent(const std::string& name) override { return name=="Explosion" || name=="UnitDestroyed" || name=="ProjectileCreated" || name=="UnitDamaged"; } 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 { @@ -37,17 +42,68 @@ void UnitDestroyed(const CUnit* unit,const CUnit*,int) override { if (WebbarVisible(unit)) Append(1,nullptr,unit->pos,unit->radius); } + // Kind 4: visible, non-paralyzer damage (battle volume). Music: BAR widget war meter input + // is every allied UnitDamaged with damage > 1, capped by the victim's max health. + void UnitDamaged(const CUnit* u,const CUnit*,float damage,int,int,bool paralyzer) override { + if (gs->frameNum<0) return; + if (damage>1.0f && u->allyteam==gu->myAllyTeam) musicDamage+=std::min(damage,u->maxHealth); + if (damage>0.0f && !paralyzer && (gu->spectatingFullView || teamHandler.Ally(u->allyteam,gu->myAllyTeam) || u->IsInLosForAllyTeam(gu->myAllyTeam))) { + if (webbarDamage.size()<1024) webbarDamage.push_back({u->id,u->pos,damage}); else ++dropped; + } + } 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); + 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, uint32_t cegDefinition=0xffffffff) { + const uint32_t definition=kind==5?cegDefinition: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}); } + // 0 unknown, 1 naval (WBC1), 2 other (scripted CEG kind 5). Tags are sent once per ID. + uint8_t Classify(unsigned int id) { + if (id>=cegClass.size()) cegClass.resize(id+1,0); + if (cegClass[id]) return cegClass[id]; + std::string tag=explGenHandler.GetGeneratorTag(id); + if (tag.rfind("custom:",0)==0) tag.erase(0,7); + for (auto& c: tag) c=char(std::tolower((unsigned char)c)); + static constexpr const char* naval[]={"waterwake-","bowsplash-","hover-wake-","subwake","splash-","torpedotrail"}; + cegClass[id]=std::any_of(std::begin(naval),std::end(naval),[&](const char* n){return tag.rfind(n,0)==0;})?1:2; + MAIN_THREAD_EM_ASM({const name=UTF8ToString($2);if($1===1){if(Module['webbarCegName'])Module['webbarCegName']($0,name);}else if(Module['webbarCegDef'])Module['webbarCegDef']({id:$0,name});},id,cegClass[id],tag.c_str()); + return cegClass[id]; + } + void DrainQueues() { + for (const auto& f: webbarFireAudio::pending) Append(3,f.def,f.pos,0,ZeroVector,0,f.speed,uint32_t(f.owner)); + dropped+=webbarFireAudio::dropped; webbarFireAudio::pending.clear(); webbarFireAudio::dropped=0; + for (const auto& h: webbarDamage) Append(4,nullptr,h.pos,0,ZeroVector,0,ZeroVector,uint32_t(h.unit),UpVector,h.damage); + webbarDamage.clear(); + std::vector naval; + for (const auto& e: webbarCeg::pending) { + const uint8_t kind=Classify(e.generator); + const float ground=CGround::GetHeightReal(e.pos.x,e.pos.z); + if (kind==1) { + if (naval.size()>=2048) {++cegDropped;continue;} + const uint32_t flags=(e.pos.y<0.0f?1u:0u)|e.origin; + naval.push_back({++cegSerial,uint32_t(e.frame),e.generator,e.owner>=0?uint32_t(e.owner):0u,flags,0,{e.pos.x,e.pos.y,e.pos.z},{e.dir.x,e.dir.y,e.dir.z},e.damage,e.radius,e.gfxMod,0}); + } else if (e.origin!=8 && e.generator<=65535) { + const uint32_t flags=CCustomExplosionGenerator::GetFlagsFromHeight(e.pos.y,ground)|CCustomExplosionGenerator::CEG_SPWF_NO_UNIT; + Append(5,nullptr,e.pos,std::max(0.0f,e.radius),ZeroVector,0,ZeroVector,e.owner>=0?uint32_t(e.owner):0xffffffff,e.dir,std::max(0.0f,e.damage),ground,flags,e.generator); + } + } + cegDropped+=webbarCeg::dropped; webbarCeg::pending.clear(); webbarCeg::dropped=0; + if (!naval.empty() || cegDropped) { + static_assert(sizeof(Ceg)==64); + const uint32_t header[]={0x31434257,uint32_t(gs->frameNum),uint32_t(naval.size()),cegDropped}; + std::vector packet(sizeof(header)+naval.size()*sizeof(Ceg)); + std::memcpy(packet.data(),header,sizeof(header));if(!naval.empty())std::memcpy(packet.data()+sizeof(header),naval.data(),naval.size()*sizeof(Ceg)); + MAIN_THREAD_EM_ASM({if(Module['webbarCeg'])Module['webbarCeg'](HEAPU8.slice($0,$0+$1).buffer);},packet.data(),packet.size()); + cegDropped=0; + } + if (musicDamage>0.0f) { MAIN_THREAD_EM_ASM({if(Module['webbarMusicDamage'])Module['webbarMusicDamage']($0,$1);},gs->frameNum,musicDamage); musicDamage=0.0f; } + } void Publish() { + DrainQueues(); std::vector shots; uint32_t omitted=0; for (const auto* p: projectileHandler.GetActiveProjectiles(true)) { @@ -57,7 +113,7 @@ 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())}); + shots.push_back({uint32_t(p->id),definition,p->GetTeamID()>=0?uint32_t(p->GetTeamID()):0xffffffff,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; @@ -70,7 +126,7 @@ },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); + static_assert(sizeof(Record)==88 && sizeof(Shot)==52); 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)); @@ -78,7 +134,7 @@ 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}; + const uint32_t header[]={0x32574257,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()); @@ -86,6 +142,10 @@ std::vector records; std::vector pending; std::unordered_set known; - uint32_t serial=0,dropped=0; + struct Hit { int unit; float3 pos; float damage; }; + std::vector webbarDamage; + std::vector cegClass; + float musicDamage=0.0f; + uint32_t serial=0,dropped=0,cegSerial=0,cegDropped=0; }; static WebbarEffects& GetWebbarEffects() { static WebbarEffects effects; return effects; } --- a/rts/Game/WebbarWorld.h +++ b/rts/Game/WebbarWorld.h @@ -7,6 +7,11 @@ { if (gs->frameNum % 15 != 0) return; PublishWebbarGeothermal(); + { + // Native wind for smoke drift (CSmokeProjectile / CGeoThermSmokeProjectile). + const float3& wind = envResHandler.GetCurrentWindVec(); + MAIN_THREAD_EM_ASM({ if (Module['webbarWind']) Module['webbarWind']($0, $1, $2, $3, $4); }, gs->frameNum, wind.x, wind.y, wind.z, envResHandler.GetCurrentWindStrength()); + } PublishWebbarBuildRestrictions(); constexpr uint32_t grid = 256; std::vector contacts; @@ -29,17 +34,20 @@ 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; + // Geovents have their own stream; undrawn features have no model. Non-reclaimable + // scenery is drawn but carries reclaim = -1 (schema 1) so it is never a reclaim target. + if (feature->def->geoThermal || feature->def->drawType == DRAWTYPE_NONE || !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 bool reclaimable = feature->def->reclaimable; + FeatureRecord record = {uint32_t(id), uint32_t(feature->def->id), reclaimable ? feature->resources.metal : 0.0f, reclaimable ? feature->reclaimLeft : -1.0f, {}}; 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}; + const uint32_t featureHeader[] = {0x31464257, uint32_t(gs->frameNum), uint32_t(features.size()), 1}; 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)); @@ -79,6 +87,7 @@ 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(); + PublishWebbarSoloFinalStats(match.outcome); MAIN_THREAD_EM_ASM({ if (Module['webbarMatch']) Module['webbarMatch']($0, $1, $2, $3, $4); }, gs->frameNum, match.outcome, match.wave, match.nextWave, match.finishedFrame); } } --- a/rts/Lua/LuaSyncedCtrl.cpp +++ b/rts/Lua/LuaSyncedCtrl.cpp @@ -4,6 +4,9 @@ #include #include "LuaSyncedCtrl.h" +#ifdef __EMSCRIPTEN__ +#include "Game/WebbarEffectQueue.h" +#endif #include "LuaInclude.h" #include "LuaConfig.h" @@ -7709,6 +7712,9 @@ // args from Lua are assumed not to include the prefix // (Spawn*C*EG implies only custom generators can fire) const unsigned int cegID = lua_isstring(L, 1)? explGenHandler.LoadCustomGeneratorID(lua_tostring(L, 1)): luaL_checkint(L, 1); +#ifdef __EMSCRIPTEN__ + const webbarCeg::Scope webbarOrigin(4); +#endif lua_pushboolean(L, explGenHandler.GenExplosion( cegID, --- a/rts/Sim/Projectiles/ExplosionGenerator.cpp +++ b/rts/Sim/Projectiles/ExplosionGenerator.cpp @@ -6,6 +6,9 @@ #include #include "ExplosionGenerator.h" +#ifdef __EMSCRIPTEN__ +#include "Game/WebbarEffectQueue.h" +#endif #include "ExpGenSpawner.h" //!! #include "ExpGenSpawnable.h" #include "ExpGenSpawnableMemberInfo.h" @@ -377,6 +380,9 @@ if (expGen == nullptr) return false; +#ifdef __EMSCRIPTEN__ + webbarCeg::Record(expGenID, pos, dir, damage, radius, gfxMod, owner); +#endif return expGen->Explosion( pos, dir, --- a/rts/Sim/Projectiles/ExplosionGenerator.h +++ b/rts/Sim/Projectiles/ExplosionGenerator.h @@ -62,6 +62,8 @@ IExplosionGenerator* LoadGenerator(const char* tag, const char* pre = ""); IExplosionGenerator* GetGenerator(unsigned int expGenID); + // Read-only tag lookup (with its "custom:" prefix) for browser presentation. + const char* GetGeneratorTag(unsigned int expGenID) const { const auto it = expGenIdentNameMap.find(expGenID); return (it == expGenIdentNameMap.end())? "": it->second.data(); } bool PredictExplosionVisible(const WeaponDef* weaponDef, const CExplosionParams& params, int allyTeamID); --- a/rts/Sim/Projectiles/WeaponProjectiles/MissileProjectile.cpp +++ b/rts/Sim/Projectiles/WeaponProjectiles/MissileProjectile.cpp @@ -5,6 +5,9 @@ #include "Game/GameHelper.h" #include "Map/Ground.h" #include "MissileProjectile.h" +#ifdef __EMSCRIPTEN__ +#include "Game/WebbarEffectQueue.h" +#endif #include "Rendering/GlobalRendering.h" #include "Rendering/Env/Particles/Classes/SmokeTrailProjectile.h" #include "Rendering/Env/Particles/ProjectileDrawer.h" @@ -218,6 +221,9 @@ SetDirectionAndSpeed(dir, speed.w); } +#ifdef __EMSCRIPTEN__ + const webbarCeg::Scope webbarOrigin(8); +#endif explGenHandler.GenExplosion( cegID, pos, --- a/rts/Sim/Projectiles/WeaponProjectiles/StarburstProjectile.cpp +++ b/rts/Sim/Projectiles/WeaponProjectiles/StarburstProjectile.cpp @@ -2,6 +2,9 @@ #include "StarburstProjectile.h" +#ifdef __EMSCRIPTEN__ +#include "Game/WebbarEffectQueue.h" +#endif #include "Game/Camera.h" #include "Game/GlobalUnsynced.h" #include "Map/Ground.h" @@ -148,6 +151,9 @@ } if (ttl > 0) +#ifdef __EMSCRIPTEN__ + const webbarCeg::Scope webbarOrigin(8); +#endif explGenHandler.GenExplosion( cegID, pos, --- a/rts/Sim/Projectiles/WeaponProjectiles/TorpedoProjectile.cpp +++ b/rts/Sim/Projectiles/WeaponProjectiles/TorpedoProjectile.cpp @@ -2,6 +2,9 @@ #include "TorpedoProjectile.h" +#ifdef __EMSCRIPTEN__ +#include "Game/WebbarEffectQueue.h" +#endif #include "Game/Camera.h" #include "Game/GameHelper.h" #include "Game/GlobalUnsynced.h" @@ -131,6 +134,9 @@ CWorldObject::SetVelocity(targetHitVel); } +#ifdef __EMSCRIPTEN__ + const webbarCeg::Scope webbarOrigin(8); +#endif explGenHandler.GenExplosion( cegID, pos, --- a/rts/Sim/Units/Scripts/UnitScript.cpp +++ b/rts/Sim/Units/Scripts/UnitScript.cpp @@ -2,6 +2,9 @@ /* heavily based on CobInstance.cpp */ #include "UnitScript.h" +#ifdef __EMSCRIPTEN__ +#include "Game/WebbarEffectQueue.h" +#endif #include "CobDefines.h" #include "CobFile.h" @@ -721,6 +724,9 @@ if ((sfxType & SFX_GLOBAL) != 0) { // emit defined explosion-generator (can only be custom, not standard) // index is made valid by callee, an ID of -1 means CEG failed to load +#ifdef __EMSCRIPTEN__ + const webbarCeg::Scope webbarOrigin(2); +#endif explGenHandler.GenExplosion( sfxType - SFX_GLOBAL, absPos, @@ -736,6 +742,9 @@ if ((sfxType & SFX_CEG) != 0) { // emit defined explosion-generator (can only be custom, not standard) // index is made valid by callee, an ID of -1 means CEG failed to load +#ifdef __EMSCRIPTEN__ + const webbarCeg::Scope webbarOrigin(2); +#endif explGenHandler.GenExplosion( ud->GetModelExpGenID(sfxType - SFX_CEG), absPos, --- a/rts/Sim/Units/Unit.cpp +++ b/rts/Sim/Units/Unit.cpp @@ -2329,8 +2329,12 @@ if (unitDef->targfac) losHandler->DecreaseAllyTeamRadarErrorSize(allyteam); - if (IsInLosForAllyTeam(gu->myAllyTeam)) + if (IsInLosForAllyTeam(gu->myAllyTeam)) { Channels::General->PlayRandomSample(unitDef->sounds.activate, this); +#ifdef __EMSCRIPTEN__ + webbarWorkAudio::Record(this, pos, 2); // actual (de)activation, read-only +#endif + } } @@ -2346,8 +2350,12 @@ if (unitDef->targfac) losHandler->IncreaseAllyTeamRadarErrorSize(allyteam); - if (IsInLosForAllyTeam(gu->myAllyTeam)) + if (IsInLosForAllyTeam(gu->myAllyTeam)) { Channels::General->PlayRandomSample(unitDef->sounds.deactivate, this); +#ifdef __EMSCRIPTEN__ + webbarWorkAudio::Record(this, pos, 3); // actual (de)activation, read-only +#endif + } } --- a/rts/Sim/Units/UnitTypes/Builder.cpp +++ b/rts/Sim/Units/UnitTypes/Builder.cpp @@ -1012,6 +1012,8 @@ if (reclaimingUnit) WebbarNano::Emit(this, nanoPos, goal, radius, inverse, 4, static_cast(curReclaim)); else WebbarNano::Emit(this, nanoPos, goal, radius, inverse, 4, nullptr, static_cast(curReclaim)); } else if (curCapture != nullptr) WebbarNano::Emit(this, nanoPos, goal, radius, inverse, 5, curCapture); + // Build-site levelling, Restore and terraform assist spray at the terraform centre. + else if (terraforming || helpTerraform != nullptr) WebbarNano::Emit(this, nanoPos, goal, radius, inverse, 6, nullptr); else if (curBuild != nullptr) WebbarNano::Emit(this, nanoPos, goal, radius, inverse, curBuild->beingBuilt ? 1 : 2, curBuild); #endif --- a/rts/Sim/Weapons/Weapon.cpp +++ b/rts/Sim/Weapons/Weapon.cpp @@ -1,6 +1,9 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ #include "Weapon.h" +#ifdef __EMSCRIPTEN__ +#include "Game/WebbarEffectQueue.h" +#endif #include "WeaponDefHandler.h" #include "WeaponMemPool.h" #include "Game/GameHelper.h" @@ -1277,8 +1280,12 @@ if (qfHasUnit) quadField.RemoveUnitIf(currentTarget.unit, currentTargetPos); - if (salvoLeft == (salvoSize - 1) || !weaponDef->soundTrigger) + if (salvoLeft == (salvoSize - 1) || !weaponDef->soundTrigger) { Channels::Battle->PlayRandomSample(weaponDef->fireSound, owner); +#ifdef __EMSCRIPTEN__ + webbarFireAudio::Record(this); // exact native fire-sound callsite, read-only +#endif + } } --- a/rts/System/Net/UDPListener.cpp +++ b/rts/System/Net/UDPListener.cpp @@ -28,9 +28,9 @@ UDPListener::UDPListener(int port, const std::string& ip): acceptNewConnections(false) { #ifdef __EMSCRIPTEN__ - if ((browserListener = WebbarDatagram::Listen()) != nullptr) { + if (!(browserListeners = WebbarDatagram::ListenSlots()).empty()) { SetAcceptingConnections(true); - LOG("[WebbarPeerHost] ABI 1 listening for peer slot 1"); + LOG("[WebbarPeerHost] ABI %d listening for %d remote peer slot(s)", std::getenv("WEBBAR_P2P_HOST_SLOTS") ? 2 : 1, int(browserListeners.size())); return; } #endif @@ -116,25 +116,29 @@ void UDPListener::Update(int loopSleepTime) { #ifdef __EMSCRIPTEN__ - if (browserListener) { + if (!browserListeners.empty()) { if (loopSleepTime > 0) spring_msecs(loopSleepTime).sleep(true); - // An internal address supplies native endpoint identity, never an OS - // socket or an address decoded from untrusted peer data. - const ip::udp::endpoint peerAddress(ip::address_v4(0x7f000002), 8460); - const auto it = connMap.find(peerAddress); - if (it != connMap.end()) { - if (auto connection = it->second.lock()) connection->Update(); - return; // no rejoin or slot reuse within this engine lifetime - } - if (!acceptNewConnections || browserListener->status.load() != 1) return; - for (uint32_t n = 0; n < WebbarDatagram::COUNT && browserListener->Receive(recvBuffer); ++n) { - Packet data(recvBuffer.data(), recvBuffer.size()); - if (data.lastContinuous != -1 || data.nakType != 0 || data.chunks.empty() || data.chunks.front()->chunkNumber != 0) continue; - auto incoming = std::make_shared(browserListener, peerAddress); - waiting.push(incoming); - connMap[peerAddress] = incoming; - incoming->ProcessRawPacket(data); - break; + for (size_t slot = 0; slot < browserListeners.size(); ++slot) { + WebbarDatagram* browserListener = browserListeners[slot]; + // An internal address per slot supplies native endpoint identity, never an + // OS socket or an address decoded from untrusted peer data. Native join + // credentials still decide which player a connection becomes. + const ip::udp::endpoint peerAddress(ip::address_v4(0x7f000002), 8460 + slot); + const auto it = connMap.find(peerAddress); + if (it != connMap.end()) { + if (auto connection = it->second.lock()) connection->Update(); + continue; // no rejoin or slot reuse within this engine lifetime + } + if (!acceptNewConnections || browserListener->status.load() != 1) continue; + for (uint32_t n = 0; n < WebbarDatagram::COUNT && browserListener->Receive(recvBuffer); ++n) { + Packet data(recvBuffer.data(), recvBuffer.size()); + if (data.lastContinuous != -1 || data.nakType != 0 || data.chunks.empty() || data.chunks.front()->chunkNumber != 0) continue; + auto incoming = std::make_shared(browserListener, peerAddress); + waiting.push(incoming); + connMap[peerAddress] = incoming; + incoming->ProcessRawPacket(data); + break; + } } return; } --- a/rts/System/Net/UDPListener.h +++ b/rts/System/Net/UDPListener.h @@ -86,7 +86,8 @@ */ bool acceptNewConnections; #ifdef __EMSCRIPTEN__ - WebbarDatagram* browserListener = nullptr; + // ABI 1: one remote slot. ABI 2 (WEBBAR_P2P_HOST_SLOTS=n): one ring pair per remote slot. + std::vector browserListeners; #endif /// socket being listened on --- a/rts/System/Net/WebbarDatagram.h +++ b/rts/System/Net/WebbarDatagram.h @@ -22,6 +22,8 @@ }; std::atomic status{0}; // connecting, open, closed, failed Ring incoming, outgoing; + // Appended after the rings so the JavaScript ABI offsets are unchanged. + std::atomic dropped{0}; static WebbarDatagram* Open() { @@ -30,8 +32,8 @@ // 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); + Module['webbarNetworkStart'](HEAPU8.buffer, $0, $1, $2, $3, $4, $5); + }, &transport->status, &transport->incoming, &transport->outgoing, COUNT, BYTES, &transport->dropped); return transport; } @@ -43,11 +45,32 @@ auto* transport = new WebbarDatagram{}; MAIN_THREAD_EM_ASM({ if (typeof Module['webbarPeerListen'] !== 'function') throw new Error('Missing peer host ABI 1'); - Module['webbarPeerListen'](1, 1, HEAPU8.buffer, $0, $1, $2, $3, $4); - }, &transport->status, &transport->incoming, &transport->outgoing, COUNT, BYTES); + Module['webbarPeerListen'](1, 1, HEAPU8.buffer, $0, $1, $2, $3, $4, $5); + }, &transport->status, &transport->incoming, &transport->outgoing, COUNT, BYTES, &transport->dropped); return transport; } + // ABI 2 (WEBBAR_P2P_HOST_SLOTS=1..7): one ring pair per remote slot s, announced as + // webbarPeerListen(2, s, ...). Without the variable, ABI 1 (Listen) is unchanged. + static std::vector ListenSlots() + { + std::vector slots; + const char* value = std::getenv("WEBBAR_P2P_HOST_SLOTS"); + if (value == nullptr) { if (auto* transport = Listen()) slots.push_back(transport); return slots; } + if (std::getenv("WEBBAR_P2P_HOST") == nullptr) return slots; + const int count = std::atoi(value); + if (count < 1 || count > 7) { MAIN_THREAD_EM_ASM({ throw new Error('Invalid peer host slot count'); }); return slots; } + for (int slot = 1; slot <= count; ++slot) { + auto* transport = new WebbarDatagram{}; + MAIN_THREAD_EM_ASM({ + if (typeof Module['webbarPeerListen'] !== 'function') throw new Error('Missing peer host ABI 2'); + Module['webbarPeerListen'](2, $0, HEAPU8.buffer, $1, $2, $3, $4, $5, $6); + }, slot, &transport->status, &transport->incoming, &transport->outgoing, COUNT, BYTES, &transport->dropped); + slots.push_back(transport); + } + return slots; + } + bool Receive(std::vector& bytes) { const uint32_t read = incoming.read.load(std::memory_order_relaxed); @@ -62,9 +85,13 @@ void Send(const std::vector& bytes) { if (status.load() >= 2) return; + if (bytes.size() > BYTES) { status.store(3); return; } // programming error 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; + if (uint32_t(write - outgoing.read.load(std::memory_order_acquire)) >= COUNT) { + // A stalled page drain behaves like a congested UDP path: UDPConnection + // resends unacknowledged chunks, and NetworkTimeout bounds a dead page. + dropped.fetch_add(1, std::memory_order_relaxed); + return; } auto& slot = outgoing.slots[write % COUNT]; slot.size = bytes.size();