Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions src/votv-coop/src/coop/interactables/drive_rack_sync.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#include "coop/net/blob_chunks.h"
#include "coop/net/session.h"
#include "coop/props/prop_lifecycle.h" // DestroyLocalProp (deny-ghost teardown)
#include "coop/props/prop_save_data.h" // DeclareClassOwnedElsewhere (the rack's rows are this lane's)
#include "coop/session/join_progress.h" // a joiner's pending canonicals wait out its join

#include "ue_wrap/core/log.h"
#include "ue_wrap/core/reflection.h"
Expand Down Expand Up @@ -84,6 +86,7 @@ Clock::time_point g_nextStats{};
// ---- counters (60 s line -- dead matchers visible) ----
std::atomic<uint64_t> g_cMarksRack{0};
uint64_t g_cRackOps = 0, g_cRackCanon = 0, g_cDenies = 0, g_cCanonApplied = 0;
uint64_t g_cMassRebased = 0; // CLIENT: sweeps where several rows vanished at once (a reset, not a take), rebased

// --------------------------------------------------------------------------
// helpers
Expand Down Expand Up @@ -228,6 +231,26 @@ void SweepRacks(bool announce) {
bool havePrev = false;
auto sh = g_rackShadow.find(eid);
if (sh != g_rackShadow.end()) havePrev = ParseRackCanonical(sh->second, prev);

// Safety net: a player takes ONE drive at a time. Several rows vanishing in a single
// sweep is a reset or a restore (a loadData re-running the rack's init, a world
// change), not a player -- sending takes for it would make the host clear the rack
// and broadcast it empty. Rebase on what is there and send nothing; the host's next
// canonical corrects any real divergence.
if (havePrev) {
int removed = 0;
for (int i = 0; i < DC::kRackSlots; ++i)
if (prev[i].has && !rows[i].has) ++removed;
if (removed > 1) {
++g_cMassRebased;
UE_LOGW("drive_rack: rack eid=%u lost %d rows at once -- not a player take, rebased "
"(no ops sent)", eid, removed);
g_rackBase[eid] = h;
g_rackShadow[eid] = std::move(blob);
continue;
}
}

for (int i = 0; i < DC::kRackSlots; ++i) {
const bool was = havePrev && prev[i].has;
if (was && !rows[i].has) {
Expand Down Expand Up @@ -420,8 +443,12 @@ void ClientApplyRackBlob(const coop::net::RackStateHead& h, const std::vector<ui
void RetryPendingTick() {
if (g_pending.empty()) return;
const auto now = Clock::now();
// A joiner's wait starts once its join is over: its racks arrive with the world's snapshot, on
// another lane (same rule as drive_payload_sync::RetryParked).
const bool joining = coop::join_progress::CurrentPhase() != coop::join_progress::Phase::Idle;
std::vector<Pending> keep;
for (auto& pd : g_pending) {
if (joining) pd.until = now + kPendingTtl;
if (now >= pd.until) {
UE_LOGW("drive_rack: pending canonical expired (rack never resolved)");
continue;
Expand All @@ -448,6 +475,10 @@ void PrimeRacks() {
// --------------------------------------------------------------------------

void Install(coop::net::Session* session) {
// This lane owns prop_driveRack's rows by canonical; the save-record lane's loadData must not
// rewrite them from a second address space (a reset there reads as the player taking every
// drive). Idempotent -- only a NEW claim does any work -- so a per-tick call is safe.
coop::prop_save_data::DeclareClassOwnedElsewhere(L"prop_driveRack_C");
g_session.store(session, std::memory_order_release);
// Verb matchers live in drive_sync (one-callback-per-verb-name; putDriveIn
// is shared slot/rack ctx) -- it forwards rack marks via MarkDirtyFromVerb.
Expand Down Expand Up @@ -484,11 +515,11 @@ void Tick() {
if (now >= g_nextStats) {
g_nextStats = now + std::chrono::seconds(60);
UE_LOGI("drive_rack: 60s marks=%llu | ops=%llu canon=%llu applied=%llu | denies=%llu "
"pending=%zu",
"massRebased=%llu pending=%zu",
(unsigned long long)g_cMarksRack.load(std::memory_order_relaxed),
(unsigned long long)g_cRackOps, (unsigned long long)g_cRackCanon,
(unsigned long long)g_cCanonApplied, (unsigned long long)g_cDenies,
g_pending.size());
(unsigned long long)g_cMassRebased, g_pending.size());
}
}

Expand Down Expand Up @@ -543,6 +574,7 @@ void OnDisconnect() {
for (auto& d : g_denies) d = DenyRec{};
for (auto& t : g_takenRing) t = TakenRow{};
g_takenNext = 0;
g_cMassRebased = 0;
g_primed = false;
g_wasConnected = false;
g_session.store(nullptr, std::memory_order_release);
Expand Down
14 changes: 13 additions & 1 deletion src/votv-coop/src/coop/interactables/floppy_slot_sync.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ std::wstring FromUtf8(const std::string& s) {
return w;
}

// The wire's string encoder: UTF-8 of every code unit, control characters included. coop::chat_feed::ToUtf8 is a chat
// sanitiser and drops CR and LF, which a disc's rows are made of ("$t", "Server data: ...", "/n kilo", "/d " are
// separate lines). A row sent through it arrived with its line breaks gone, so the laptop could not parse it.
std::string ToUtf8Exact(const std::wstring& w) {
if (w.empty()) return std::string();
const int n = ::WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), nullptr, 0, nullptr, nullptr);
if (n <= 0) return std::string();
std::string s(static_cast<size_t>(n), '\0');
::WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), s.data(), n, nullptr, nullptr);
return s;
}

// ---- wire ----
// head [u8 op][u8 deviceKind]; op 0=claim{[u8 index][slot]}, 1=canonical{[u32 generation][u16 n]{[u8 index][slot]}}
// The generation counts the host's changes of the laptop's occupancy (0 for the boxes): the laptop's file quad carries
Expand All @@ -87,7 +99,7 @@ void PutU32(std::vector<uint8_t>& b, uint32_t v) {
// A row or a JSON can be long, so both take a 32-bit length; the reader bounds them against the
// blob it is reading out of.
void PutStr32(std::vector<uint8_t>& b, const std::wstring& w) {
const std::string u = coop::chat_feed::ToUtf8(w);
const std::string u = ToUtf8Exact(w);
PutU32(b, static_cast<uint32_t>(u.size()));
b.insert(b.end(), u.begin(), u.end());
}
Expand Down
16 changes: 14 additions & 2 deletions src/votv-coop/src/coop/interactables/floppybox_sync.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,20 @@ std::wstring FromUtf8(const std::string& s) {
return w;
}

// The wire's string encoder: UTF-8 of every code unit, control characters included. coop::chat_feed::ToUtf8 is a chat
// sanitiser and drops CR and LF, which a disc's rows are made of ("$t", "Server data: ...", "/n kilo", "/d " are
// separate lines). A row sent through it arrived with its line breaks gone, so the laptop could not parse it.
std::string ToUtf8Exact(const std::wstring& w) {
if (w.empty()) return std::string();
const int n = ::WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), nullptr, 0, nullptr, nullptr);
if (n <= 0) return std::string();
std::string s(static_cast<size_t>(n), '\0');
::WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), s.data(), n, nullptr, nullptr);
return s;
}

uint64_t HashStr(const std::wstring& w) {
const std::string u = coop::chat_feed::ToUtf8(w);
const std::string u = ToUtf8Exact(w);
return coop::blob_chunks::Fnv64(reinterpret_cast<const uint8_t*>(u.data()), u.size());
}

Expand All @@ -71,7 +83,7 @@ void PutU64(std::vector<uint8_t>& b, uint64_t v) {
for (int i = 0; i < 8; ++i) b.push_back(static_cast<uint8_t>((v >> (i * 8)) & 0xFF));
}
void PutStr(std::vector<uint8_t>& b, const std::wstring& w) {
std::string u = coop::chat_feed::ToUtf8(w);
std::string u = ToUtf8Exact(w);
if (u.size() > 0xFFFF) u.resize(0xFFFF);
PutU16(b, static_cast<uint16_t>(u.size()));
b.insert(b.end(), u.begin(), u.end());
Expand Down
16 changes: 14 additions & 2 deletions src/votv-coop/src/coop/interactables/laptop_buffer_sync.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,20 @@ std::wstring FromUtf8(const std::string& s) {
return w;
}

// The wire's string encoder: UTF-8 of every code unit, control characters included. coop::chat_feed::ToUtf8 is a chat
// sanitiser and drops CR and LF, which a disc's rows are made of ("$t", "Server data: ...", "/n kilo", "/d " are
// separate lines). A row sent through it arrived with its line breaks gone, so the laptop could not parse it.
std::string ToUtf8Exact(const std::wstring& w) {
if (w.empty()) return std::string();
const int n = ::WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), nullptr, 0, nullptr, nullptr);
if (n <= 0) return std::string();
std::string s(static_cast<size_t>(n), '\0');
::WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), s.data(), n, nullptr, nullptr);
return s;
}

uint64_t HashStr(const std::wstring& w) {
const std::string u = coop::chat_feed::ToUtf8(w);
const std::string u = ToUtf8Exact(w);
return coop::blob_chunks::Fnv64(reinterpret_cast<const uint8_t*>(u.data()), u.size());
}

Expand Down Expand Up @@ -101,7 +113,7 @@ void PutU64(std::vector<uint8_t>& b, uint64_t v) {
for (int i = 0; i < 8; ++i) b.push_back(static_cast<uint8_t>((v >> (i * 8)) & 0xFF));
}
void PutStr(std::vector<uint8_t>& b, const std::wstring& w) {
std::string u = coop::chat_feed::ToUtf8(w);
std::string u = ToUtf8Exact(w);
if (u.size() > 0xFFFF) u.resize(0xFFFF);
PutU16(b, static_cast<uint16_t>(u.size()));
b.insert(b.end(), u.begin(), u.end());
Expand Down