--- a/experiments/RecoilWasm/rts/Game/WebbarBrowser.h +++ b/experiments/RecoilWasm/rts/Game/WebbarBrowser.h @@ -62,6 +62,7 @@ #include "WebbarResurrection.h" #include "WebbarNano.h" #include "WebbarSkirmish.h" +#include "WebbarPlacement.h" #include "WebbarWorld.h" #include "WebbarTerrain.h" #include "WebbarTerrainFixture.h" @@ -83,15 +84,15 @@ if (!catalogSent) { catalogSent = true; if (std::getenv("WEBBAR_NETWORK") != nullptr) { static WebbarNetworkResult result; } - MAIN_THREAD_EM_ASM({ if (Module['webbarProtocol']) Module['webbarProtocol'](8, 1023 /* restore | shields | cloak | resurrection | native work effects | area resurrection | manual launch | capture | smart trajectory | energy conversion */); }); + 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 */); }); 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()); + if (Module['webbarCatalog']) Module['webbarCatalog']($0, UTF8ToString($1), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12); + }, 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(), def.needGeo); for (const auto& option: def.buildOptions) { const UnitDef* target = unitDefHandler->GetUnitDefByName(option.second); if (!WebbarSupports(target)) continue; @@ -139,9 +140,9 @@ 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}; + struct Record { uint32_t id; float progress; uint32_t queued; int32_t command; uint32_t target; float metal, energy; uint32_t transporter, cargo, selectionFlags; }; + static_assert(sizeof(Record) == 40); + const uint32_t header[] = {0x35534257, 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); @@ -152,7 +153,7 @@ 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}; + (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, (owned && unit->noSelect) ? 1u : 0u}; std::memcpy(packet.data() + offset, &record, sizeof(record)); offset += sizeof(record); } MAIN_THREAD_EM_ASM({ @@ -217,7 +218,7 @@ } 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()) + if (WebbarSelectable(unit) && 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)); @@ -365,7 +366,9 @@ for (const auto& option: unit->unitDef->buildOptions) if (option.second == def->name) return false; return true; }); - if (ids.empty()) { + if (WebbarBuildRestriction(def->id) != nullptr) { + status = 9; + } else 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 --- a/experiments/RecoilWasm/rts/Game/WebbarWorld.h +++ b/experiments/RecoilWasm/rts/Game/WebbarWorld.h @@ -6,6 +6,8 @@ static void PublishWebbarWorld() { if (gs->frameNum % 15 != 0) return; + PublishWebbarGeothermal(); + PublishWebbarBuildRestrictions(); constexpr uint32_t grid = 256; std::vector contacts; for (const CUnit* unit: unitHandler.GetActiveUnits()) --- /dev/null +++ b/experiments/RecoilWasm/rts/Game/WebbarPlacement.h @@ -0,0 +1,66 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#pragma once + +#include "Lua/LuaRulesParams.h" + +// api_build_blocking publishes the original gadget's current per-team reasons. +// Read that decision instead of duplicating map/option rules or calling synced +// AllowCommand from a local preview. +static const std::string* WebbarBuildRestriction(int definition) +{ + if (gu->spectating || !teamHandler.IsValidTeam(gu->myTeam)) return nullptr; + const auto& params = teamHandler.Team(gu->myTeam)->modParams; + const auto it = params.find("unitdef_blocked_" + std::to_string(definition)); + if (it == params.end() || !(it->second.los & LuaRulesParams::RULESPARAMLOS_PRIVATE_MASK)) return nullptr; + const auto* reason = std::get_if(&it->second.value); + return reason != nullptr && !reason->empty() ? reason : nullptr; +} + +static void PublishWebbarBuildRestrictions() +{ + if (gu->spectating || !teamHandler.IsValidTeam(gu->myTeam)) return; + std::vector words; + for (const UnitDef& def: unitDefHandler->GetUnitDefsVec()) { + if (!WebbarSupports(&def)) continue; + const auto* reason = WebbarBuildRestriction(def.id); + if (reason == nullptr) continue; + words.push_back(def.id); + words.push_back(uint32_t(reinterpret_cast(reason->c_str()))); + } + MAIN_THREAD_EM_ASM({ + if (!Module['webbarBuildRestrictions']) return; + const words = HEAPU32.subarray($0 >> 2, ($0 >> 2) + $1); + const entries = []; + for (let i = 0; i < words.length; i += 2) entries.push([words[i], UTF8ToString(words[i + 1])]); + Module['webbarBuildRestrictions']($2, $3, entries); + }, words.data(), words.size(), gs->frameNum, gu->myTeam); +} + +// Local presentation/admission only. The original gadgets own noSelect and +// the original build-square checks own geothermal placement and occupancy. +static bool WebbarSelectable(const CUnit* unit) +{ + return unit != nullptr && unit->team == gu->myTeam && !unit->isDead && !unit->noSelect; +} + +static bool WebbarGeoVisible(const CFeature* feature) +{ + return feature != nullptr && !feature->deleteMe && feature->def->geoThermal && feature->IsInLosForAllyTeam(gu->myAllyTeam); +} + +static void PublishWebbarGeothermal() +{ + struct GeoRecord { uint32_t id; float x, y, z; }; + static_assert(sizeof(GeoRecord) == 16); + std::vector vents; + for (const int id: featureHandler.GetActiveFeatureIDs()) { + const CFeature* feature = featureHandler.GetFeature(id); + if (!WebbarGeoVisible(feature)) continue; + vents.push_back({uint32_t(id), feature->pos.x, feature->pos.y, feature->pos.z}); + } + const uint32_t header[] = {0x31474257, uint32_t(gs->frameNum), uint32_t(vents.size()), 0}; + std::vector packet(sizeof(header) + vents.size() * sizeof(GeoRecord)); + std::memcpy(packet.data(), header, sizeof(header)); + std::memcpy(packet.data() + sizeof(header), vents.data(), vents.size() * sizeof(GeoRecord)); + MAIN_THREAD_EM_ASM({ if (Module['webbarGeothermal']) Module['webbarGeothermal'](HEAPU8.slice($0, $0 + $1).buffer); }, packet.data(), packet.size()); +}