Jelajahi Sumber

started porting scripts from dawn
https://github.com/isinternets/Dawn

stan 1 hari lalu
induk
melakukan
2d24c0d25c

+ 1 - 0
Sunrise/Sunrise.vcxproj

@@ -815,6 +815,7 @@
     <ClCompile Include="src\server\activity\mission\mission_script_lua_presentation_api.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_lua_actor_api.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_lua_device_api.cpp" />
+    <ClCompile Include="src\server\activity\mission\mission_script_lua_generator_api.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_lua_actor_sequence_api.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_lua_value_api.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_lua_enum_api.cpp" />

+ 3 - 0
Sunrise/src/client/content/activity/activity_sdk_lua_contract.cpp

@@ -271,6 +271,9 @@ local EventKind = {
 ---@field watch_damage fun(self: SunriseSlot, args: {target: SunriseSlot}): SunriseRequestKey
 ---@field set_occupancy_condition fun(self: SunriseSlot, )lua"
             R"lua(args: {value: integer, filter: SunriseSlot?}): SunriseRequestKey
+---@field generate_map fun(self: SunriseSlot, args: {record: integer?, seed: integer?, )lua"
+            R"lua(mode: integer?, enabled: boolean?, anchors: table[]?, values: integer[]?, )lua"
+            R"lua(reals: number[]?, state_key: integer?}): SunriseRequestKey
 ---@field set_object_filter fun(self: SunriseSlot, args: {players: boolean?, )lua"
             R"lua(target: SunriseSlot?, inside: SunriseSlot?, )lua"
             R"lua(inside_any: SunriseSlot[]?}): SunriseRequestKey

+ 4 - 0
Sunrise/src/server/activity/activity_sdk_device_prepare.cpp

@@ -5,6 +5,7 @@
 #include "../../middleware/bap/activity_message/darkness_zone_auth.h"
 #include "../../middleware/bap/activity_message/ghost_link_auth.h"
 #include "../../middleware/bap/activity_message/interactable_object_auth.h"
+#include "../../middleware/bap/activity_message/map_generator_auth.h"
 #include "../../middleware/bap/activity_message/mission_effect_auth.h"
 #include "../../middleware/bap/activity_message/music_section_auth.h"
 #include "../../middleware/bap/activity_message/scene_events_auth.h"
@@ -475,6 +476,9 @@ prepare_slot(const sdk::BoundView& view, std::uint32_t slotRow, PreparedDevice&
                  message::ghost_link::kAuthSchema)
         || typed(
             format::kSquadSlotType, format::kSquadComponentClass, message::squad_objective::kSchema)
+        || typed(message::map_generator_auth::kSlotType,
+                 message::map_generator_auth::kComponentClass,
+                 message::map_generator_auth::kSchema)
         || typed(auth::kType2SlotType, auth::kType2ComponentClass, auth::kType2Schema);
     const bool occupancy = slotType == format::kOccupancySlotType
                            && authSchema == format::kOccupancyAuthSchema

+ 239 - 0
Sunrise/src/server/activity/mission/mission_script_lua_generator_api.cpp

@@ -0,0 +1,239 @@
+#include <array>
+#include <bit>
+#include <cstddef>
+#include <cstdint>
+#include <limits>
+#include <span>
+#include <string_view>
+
+#include "../../../middleware/bap/activity_message/map_generator_auth.h"
+#include "mission_script_lua_internal.h"
+
+namespace sunrise::server::activity::mission::lua_vm::detail {
+
+namespace generator = middleware::bap::activity_message::map_generator_auth;
+namespace format = state::activity_sdk::format;
+
+namespace {
+
+/** A record selects a field only when its own mask bit is set; the rest stay authored. */
+constexpr std::uint8_t kSeedMask = static_cast<std::uint8_t>(generator::Override::seed);
+constexpr std::uint8_t kModeMask = static_cast<std::uint8_t>(generator::Override::mode);
+constexpr std::uint8_t kAnchorMask = static_cast<std::uint8_t>(generator::Override::anchors);
+constexpr std::uint8_t kEnabledMask = static_cast<std::uint8_t>(generator::Override::enabled);
+/** Integer inputs three, four and five carry the last three mask bits, in record order. */
+constexpr std::array<std::uint8_t, 3> kIntegerMasks{
+    static_cast<std::uint8_t>(generator::Override::integer2),
+    static_cast<std::uint8_t>(generator::Override::integer3),
+    static_cast<std::uint8_t>(generator::Override::integer4)};
+/** Integer inputs one and two have no mask bit; they apply on any value other than -1. */
+constexpr std::size_t kUnmaskedIntegerCount = 2;
+
+/** @return True when one live Slot row is an exact type-37 map generator. */
+[[nodiscard]] bool exact_generator_slot(const SlotDefinition& definition) noexcept {
+    return definition.slotType == generator::kSlotType
+           && definition.componentClass == generator::kComponentClass
+           && definition.authSchema == generator::kSchema
+           && (definition.flags & format::kSlotSchemaJoinExact) != 0;
+}
+
+/** @return False with the Lua error already raised when the field is outside the native lane. */
+[[nodiscard]] bool
+signed_byte(lua_State* state, lua_Integer value, const char* name, std::int8_t& output) {
+    if (value < (std::numeric_limits<std::int8_t>::min)()
+        || value > (std::numeric_limits<std::int8_t>::max)()) {
+        return luaL_argerror(state, 2, name) == 0;
+    }
+    output = static_cast<std::int8_t>(value);
+    return true;
+}
+
+/** Reads one anchor row: two authored grid selectors, a float input and an enable flag. */
+[[nodiscard]] bool read_anchor(lua_State* state, int row, generator::Anchor& anchor) {
+    if (!lua_istable(state, row)) {
+        return luaL_argerror(state, 2, "each anchor must be a table") == 0;
+    }
+    lua_getfield(state, row, "first");
+    lua_getfield(state, row, "second");
+    lua_getfield(state, row, "value");
+    lua_getfield(state, row, "enabled");
+    const bool typed = lua_isinteger(state, -4) && lua_isinteger(state, -3)
+                       && lua_isnumber(state, -2) && lua_isboolean(state, -1);
+    if (!typed) {
+        lua_pop(state, 4);
+        return luaL_argerror(state, 2, "an anchor needs first, second, value and enabled") == 0;
+    }
+    const lua_Integer first = lua_tointeger(state, -4);
+    const lua_Integer second = lua_tointeger(state, -3);
+    anchor.value = static_cast<float>(lua_tonumber(state, -2));
+    anchor.enabled = lua_toboolean(state, -1) != 0;
+    lua_pop(state, 4);
+    return signed_byte(state, first, "anchor first", anchor.first)
+           && signed_byte(state, second, "anchor second", anchor.second);
+}
+
+/** Reads the four anchors and sets their mask bit. */
+[[nodiscard]] bool read_anchors(lua_State* state, generator::Record& record) {
+    if (push_argument(state, "anchors") == LUA_TNIL) {
+        lua_pop(state, 1);
+        return true;
+    }
+    if (!lua_istable(state, -1)
+        || lua_rawlen(state, -1) != static_cast<std::size_t>(generator::kAnchorCount)) {
+        lua_pop(state, 1);
+        return luaL_argerror(state, 2, "anchors must be a list of four") == 0;
+    }
+    const int list = lua_gettop(state);
+    for (std::size_t index = 0; index < generator::kAnchorCount; ++index) {
+        lua_rawgeti(state, list, static_cast<lua_Integer>(index + 1));
+        const bool read = read_anchor(state, lua_gettop(state), record.anchors[index]);
+        lua_pop(state, 1);
+        if (!read) {
+            lua_pop(state, 1);
+            return false;
+        }
+    }
+    lua_pop(state, 1);
+    record.overrides |= kAnchorMask;
+    return true;
+}
+
+/** Reads the five integer worker inputs in record order, keeping -1 where the caller is silent. */
+[[nodiscard]] bool read_integers(lua_State* state, generator::Record& record) {
+    if (push_argument(state, "values") == LUA_TNIL) {
+        lua_pop(state, 1);
+        return true;
+    }
+    if (!lua_istable(state, -1)
+        || lua_rawlen(state, -1) > static_cast<std::size_t>(generator::kIntegerCount)) {
+        lua_pop(state, 1);
+        return luaL_argerror(state, 2, "values must be a list of at most five integers") == 0;
+    }
+    const int list = lua_gettop(state);
+    const std::size_t count = lua_rawlen(state, list);
+    for (std::size_t index = 0; index < count; ++index) {
+        lua_rawgeti(state, list, static_cast<lua_Integer>(index + 1));
+        if (!lua_isinteger(state, -1)) {
+            lua_pop(state, 2);
+            return luaL_argerror(state, 2, "values must hold integers") == 0;
+        }
+        const lua_Integer value = lua_tointeger(state, -1);
+        lua_pop(state, 1);
+        if (value < (std::numeric_limits<std::int32_t>::min)()
+            || value > (std::numeric_limits<std::int32_t>::max)()) {
+            lua_pop(state, 1);
+            return luaL_argerror(state, 2, "values must be 32-bit signed integers") == 0;
+        }
+        record.integerInputs[index] = static_cast<std::int32_t>(value);
+        if (index >= kUnmaskedIntegerCount) {
+            record.overrides |= kIntegerMasks[index - kUnmaskedIntegerCount];
+        }
+    }
+    lua_pop(state, 1);
+    return true;
+}
+
+/** Reads the two float worker inputs; each keeps -1 until the caller names it. */
+[[nodiscard]] bool read_reals(lua_State* state, generator::Record& record) {
+    if (push_argument(state, "reals") == LUA_TNIL) {
+        lua_pop(state, 1);
+        return true;
+    }
+    if (!lua_istable(state, -1)
+        || lua_rawlen(state, -1) > static_cast<std::size_t>(generator::kRealCount)) {
+        lua_pop(state, 1);
+        return luaL_argerror(state, 2, "reals must be a list of at most two numbers") == 0;
+    }
+    const int list = lua_gettop(state);
+    const std::size_t count = lua_rawlen(state, list);
+    for (std::size_t index = 0; index < count; ++index) {
+        lua_rawgeti(state, list, static_cast<lua_Integer>(index + 1));
+        if (!lua_isnumber(state, -1)) {
+            lua_pop(state, 2);
+            return luaL_argerror(state, 2, "reals must hold numbers") == 0;
+        }
+        record.realInputs[index] = static_cast<float>(lua_tonumber(state, -1));
+        lua_pop(state, 1);
+    }
+    lua_pop(state, 1);
+    return true;
+}
+
+} // namespace
+
+/**
+ * Writes one record of an authored map generator and leaves the other authored.
+ * A field the caller omits keeps the worker's authored value: the masked fields through their
+ * mask bit, the two floats and the first two integers through their -1 sentinel.
+ */
+[[nodiscard]] int slot_generate_map(lua_State* state) {
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    // Named arguments this call accepts. Any other key is refused.
+    static constexpr std::array<std::string_view, 8> kDeclared{
+        "record", "seed", "mode", "anchors", "enabled", "values", "reals", "state_key"};
+    refuse_unknown_arguments(state, kDeclared);
+    SlotDefinition slot{};
+    if (!current_slot(state, *handle, slot)) {
+        return luaL_error(state, "activity slot is stale or invalid");
+    }
+    if (!exact_generator_slot(slot)) {
+        return luaL_error(state, "activity slot is not an exact type-37 map generator");
+    }
+    const lua_Integer record = optional_integer_argument(state, "record", 1);
+    if (record < 1 || record > static_cast<lua_Integer>(generator::kRecordCount)) {
+        return luaL_error(state, "record must be 1 or 2");
+    }
+    generator::Body body{};
+    generator::Record& target = body.records[static_cast<std::size_t>(record - 1)];
+    const lua_Integer seed = optional_integer_argument(state, "seed", -1);
+    if (seed >= 0) {
+        if (seed > static_cast<lua_Integer>((std::numeric_limits<std::uint32_t>::max)())) {
+            return luaL_error(state, "seed must be a 32-bit unsigned integer");
+        }
+        target.seed = static_cast<std::uint32_t>(seed);
+        target.overrides |= kSeedMask;
+    }
+    if (push_argument(state, "mode") != LUA_TNIL) {
+        if (!lua_isinteger(state, -1)) {
+            return luaL_error(state, "mode must be an integer");
+        }
+        const lua_Integer mode = lua_tointeger(state, -1);
+        lua_pop(state, 1);
+        if (!signed_byte(state, mode, "mode", target.mode)) {
+            return 0;
+        }
+        target.overrides |= kModeMask;
+    } else {
+        lua_pop(state, 1);
+    }
+    if (push_argument(state, "enabled") != LUA_TNIL) {
+        if (!lua_isboolean(state, -1)) {
+            return luaL_error(state, "enabled must be a boolean");
+        }
+        target.enabled = lua_toboolean(state, -1) != 0;
+        lua_pop(state, 1);
+        target.overrides |= kEnabledMask;
+    } else {
+        lua_pop(state, 1);
+    }
+    if (!read_anchors(state, target) || !read_integers(state, target)
+        || !read_reals(state, target)) {
+        return 0;
+    }
+    const lua_Integer stateKey = optional_integer_argument(state, "state_key", 0);
+    if (stateKey < 0
+        || stateKey > static_cast<lua_Integer>((std::numeric_limits<std::uint32_t>::max)())) {
+        return luaL_error(state, "state_key must be a 32-bit unsigned integer");
+    }
+    body.stateKey = static_cast<std::uint32_t>(stateKey);
+    std::array<std::byte, generator::kByteCount> bytes{};
+    std::size_t written = 0;
+    if (!generator::encode(body, bytes, written)) {
+        return luaL_error(state, "map generator encoder failed");
+    }
+    return queue_slot_auth(
+        state, slot, generator::kSchema, generator::kBitCount, std::span(bytes).first(written));
+}
+
+} // namespace sunrise::server::activity::mission::lua_vm::detail

+ 1 - 0
Sunrise/src/server/activity/mission/mission_script_lua_internal.h

@@ -207,6 +207,7 @@ void push_atom_kinds(lua_State* state);
 [[nodiscard]] int slot_bind_combatant_to_squad(lua_State* state);
 [[nodiscard]] int slot_set_darkness_zone(lua_State* state);
 [[nodiscard]] int slot_set_object_filter(lua_State* state);
+[[nodiscard]] int slot_generate_map(lua_State* state);
 [[nodiscard]] int slot_watch_damage(lua_State* state);
 [[nodiscard]] int slot_set_interactable_object(lua_State* state);
 [[nodiscard]] int slot_set_ghost_link(lua_State* state);

+ 2 - 0
Sunrise/src/server/activity/mission/mission_script_lua_slot_api.cpp

@@ -175,6 +175,8 @@ namespace auth_catalog = middleware::bap::activity_message::auth_schema_catalog;
         lua_pushcfunction(state, &slot_transition);
     } else if (key == "set_occupancy_condition") {
         lua_pushcfunction(state, &slot_set_occupancy_condition);
+    } else if (key == "generate_map") {
+        lua_pushcfunction(state, &slot_generate_map);
     } else if (key == "set_directive") {
         lua_pushcfunction(state, &slot_set_directive);
     } else if (key == "clear_directives") {

+ 48 - 0
Sunrise/src/server/bap/encrypted/push/activity/activity_mission_seed_roster.cpp

@@ -394,12 +394,40 @@ MissionSeedRosterResult append_initial_mission_seed(Session& session,
     const state::activity::membership::ClientPlacement placement =
         client_placement(session, refresh);
     const std::int32_t heldRegion = state::activity::membership::instantiated_region(placement);
+    // The public link keeps its own bubble loaded and never instantiates the selected region,
+    // so the window also closes on the link that reports one.
+    const std::int32_t liveRegion = state::activity::membership::instantiated_region(
+        state::activity::membership::reported_placement(
+            state::activity::membership::live_region_session(state::activity::kAbsentSessionId)));
     // The window closes on the exact packed region, so a sibling state of one bubble counts.
+    // Only this link's own arrival may close it: a sibling link's region is not this client's
+    // world, and closing early publishes the full set before the client can accept it.
     if (!adopting && lease.regionArrivalPending
         && mission_seed_arrival_window_closed(heldRegion, lease.plan.effectiveRegion)) {
         lease.regionArrivalPending = false;
     }
     const bool arrivalWindow = !adopting && lease.regionArrivalPending;
+    if (arrivalWindow) {
+        // Publication holds the previous plan until the client instantiates the selected region.
+        // Name both regions, because a wait that never ends looks the same as a slow one.
+        std::array<char, 192> wait{};
+        const int waitWritten = std::snprintf(
+            wait.data(),
+            wait.size(),
+            "ev=activity stage=mission_seed_arrival held=%d live=%d selected=%u previous=%u "
+            "rev=%llu/%llu",
+            heldRegion,
+            liveRegion,
+            lease.plan.effectiveRegion,
+            lease.previousPlan.effectiveRegion,
+            static_cast<unsigned long long>(lease.revision),
+            static_cast<unsigned long long>(lease.publishedRevision));
+        if (waitWritten > 0) {
+            core::log::write(core::log::Channel::server,
+                             core::log::Level::debug,
+                             {wait.data(), static_cast<std::size_t>(waitWritten)});
+        }
+    }
     const ActivityMissionSeedPlan& activePlan = arrivalWindow ? lease.previousPlan : lease.plan;
     const std::uint32_t selectedRegion =
         lease.configured ? activePlan.effectiveRegion : effectiveRegion;
@@ -412,6 +440,26 @@ MissionSeedRosterResult append_initial_mission_seed(Session& session,
         selectedRegion / middleware::content::packages::tables::kSliceSetIndexFactor;
     if (selectedBubble >= layouts::kBubbleCapacity
         || ((hostedBubbles >> selectedBubble) & 1U) == 0) {
+        // A selection this link cannot host leaves the lease unpublished with no refusal, so
+        // name the bubble and the link's hosted set.
+        if (lease.configured && lease.revision != lease.publishedRevision) {
+            std::array<char, 192> unhosted{};
+            const int unhostedWritten =
+                std::snprintf(unhosted.data(),
+                              unhosted.size(),
+                              "ev=activity stage=mission_seed_unhosted bubble=%u hosted=0x%016llX"
+                              " region=%u rev=%llu/%llu",
+                              selectedBubble,
+                              static_cast<unsigned long long>(hostedBubbles),
+                              selectedRegion,
+                              static_cast<unsigned long long>(lease.revision),
+                              static_cast<unsigned long long>(lease.publishedRevision));
+            if (unhostedWritten > 0) {
+                core::log::write(core::log::Channel::server,
+                                 core::log::Level::debug,
+                                 {unhosted.data(), static_cast<std::size_t>(unhostedWritten)});
+            }
+        }
         return MissionSeedRosterResult::inactive;
     }
     const std::size_t available = scratch.rosterGroups.size() - canonicalGroupCount;