From dd405285b1cac8a37a8b44c3ed4158c2d30f0a65 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:20:24 +0200 Subject: [PATCH 01/26] test(statisticshistory): cover the object and keyed stores before they migrate StatisticsHistory keeps two container-backed stores -- `objects`, a DataStructures::OrderedList keyed by objectId, and per-object `dataQueues`, a DataStructures::Hash of key -> sample queue. Stage 2 of #60 moves both to the standard library, and the only existing suite covers the plugin's sampling clock, touching neither. The property that matters is order. Three public methods expose the index of `objects` directly -- GetObjectIndex, GetObjectAtIndex, RemoveObjectAtIndex -- so "index" means position in ascending objectId order, not insertion order. That rules out std::map for the replacement (no O(1) indexed access) and makes an unsorted std::vector a silent renumbering of every index. Mutation-checked: appending instead of inserting at the sorted position, which is the mistake a careless OrderedList -> std::vector migration makes, fails 5 of these 16 and none of the 4 pre-existing sampling tests. --- Tests/Unit/StatisticsHistoryStoreTests.cpp | 241 +++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 Tests/Unit/StatisticsHistoryStoreTests.cpp diff --git a/Tests/Unit/StatisticsHistoryStoreTests.cpp b/Tests/Unit/StatisticsHistoryStoreTests.cpp new file mode 100644 index 000000000..96b4550cc --- /dev/null +++ b/Tests/Unit/StatisticsHistoryStoreTests.cpp @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + */ + +#include + +#include +#include +#include + +#include "mafianet/statistics_history.h" + +using namespace MafiaNet; + +/* +Description: +Covers StatisticsHistory's two container-backed stores -- the tracked-object set and the per-object +map of key -> sample queue -- written before those containers move to the standard library +(std migration stage 2, #60). The existing StatisticsHistorySampling suite only covers the plugin's +sampling clock and touches neither store. + +The load-bearing property is ORDER. `objects` is a DataStructures::OrderedList keyed by objectId, +and three public methods expose its index directly: GetObjectIndex, GetObjectAtIndex and +RemoveObjectAtIndex. So "index" means "position in ascending objectId order", not insertion order, +and that is a public contract a replacement container must keep -- a std::map would still iterate +ascending but would not give the O(1) indexed access these signatures imply, and an unsorted vector +would pass a naive round-trip test while silently renumbering every index. + +Success conditions: indices follow ascending objectId whatever the insertion order, removal +renumbers the remainder, per-object key lookup round-trips, and the unique key list is a set union +across objects. +*/ + +namespace +{ + const uint64_t kIdA = 700; + const uint64_t kIdB = 100; + const uint64_t kIdC = 400; + + class StatisticsHistoryStore : public ::testing::Test + { + protected: + StatisticsHistory history; + + // Insert deliberately out of order, so any test that passes is not passing by accident + // of insertion order matching sorted order. + void AddThreeOutOfOrder(void) + { + ASSERT_TRUE(history.AddObject(StatisticsHistory::TrackedObjectData(kIdA, 0, (void *) 0xA))); + ASSERT_TRUE(history.AddObject(StatisticsHistory::TrackedObjectData(kIdB, 0, (void *) 0xB))); + ASSERT_TRUE(history.AddObject(StatisticsHistory::TrackedObjectData(kIdC, 0, (void *) 0xC))); + } + + std::vector IdsInIndexOrder(void) const + { + std::vector out; + for (unsigned i = 0; i < history.GetObjectCount(); ++i) + { + StatisticsHistory::TrackedObjectData *tod = history.GetObjectAtIndex(i); + if (tod == 0) + break; + out.push_back(tod->objectId); + } + return out; + } + }; +} + +TEST_F(StatisticsHistoryStore, StartsEmpty) +{ + EXPECT_EQ(history.GetObjectCount(), 0u); +} + +TEST_F(StatisticsHistoryStore, IndexOrderIsAscendingObjectIdNotInsertionOrder) +{ + AddThreeOutOfOrder(); + ASSERT_EQ(history.GetObjectCount(), 3u); + + std::vector ids = IdsInIndexOrder(); + ASSERT_EQ(ids.size(), 3u); + EXPECT_EQ(ids[0], kIdB) << "index 0 must be the lowest objectId, not the first inserted"; + EXPECT_EQ(ids[1], kIdC); + EXPECT_EQ(ids[2], kIdA); +} + +TEST_F(StatisticsHistoryStore, GetObjectIndexAgreesWithGetObjectAtIndex) +{ + AddThreeOutOfOrder(); + for (unsigned i = 0; i < history.GetObjectCount(); ++i) + { + StatisticsHistory::TrackedObjectData *tod = history.GetObjectAtIndex(i); + ASSERT_NE(tod, (StatisticsHistory::TrackedObjectData *) 0); + EXPECT_EQ(history.GetObjectIndex(tod->objectId), i) + << "GetObjectIndex disagrees with GetObjectAtIndex at " << i; + } +} + +TEST_F(StatisticsHistoryStore, UserDataSurvivesTheRoundTrip) +{ + AddThreeOutOfOrder(); + unsigned idx = history.GetObjectIndex(kIdC); + StatisticsHistory::TrackedObjectData *tod = history.GetObjectAtIndex(idx); + ASSERT_NE(tod, (StatisticsHistory::TrackedObjectData *) 0); + EXPECT_EQ(tod->userData, (void *) 0xC); +} + +TEST_F(StatisticsHistoryStore, AddingADuplicateObjectIdIsRefused) +{ + ASSERT_TRUE(history.AddObject(StatisticsHistory::TrackedObjectData(kIdA, 0, (void *) 0xA))); + EXPECT_FALSE(history.AddObject(StatisticsHistory::TrackedObjectData(kIdA, 0, (void *) 0xF))); + EXPECT_EQ(history.GetObjectCount(), 1u); +} + +TEST_F(StatisticsHistoryStore, RemoveObjectHandsBackTheUserDataAndRenumbersTheRest) +{ + AddThreeOutOfOrder(); + + void *userData = 0; + ASSERT_TRUE(history.RemoveObject(kIdB, &userData)); + EXPECT_EQ(userData, (void *) 0xB); + ASSERT_EQ(history.GetObjectCount(), 2u); + + std::vector ids = IdsInIndexOrder(); + ASSERT_EQ(ids.size(), 2u); + EXPECT_EQ(ids[0], kIdC) << "removing the lowest id must shift the remainder down"; + EXPECT_EQ(ids[1], kIdA); +} + +TEST_F(StatisticsHistoryStore, RemovingAnAbsentObjectIdFails) +{ + AddThreeOutOfOrder(); + void *userData = 0; + EXPECT_FALSE(history.RemoveObject(999, &userData)); + EXPECT_EQ(history.GetObjectCount(), 3u); +} + +TEST_F(StatisticsHistoryStore, RemoveObjectAtIndexRemovesThatPositionInSortedOrder) +{ + AddThreeOutOfOrder(); + history.RemoveObjectAtIndex(1); // kIdC, the middle id + ASSERT_EQ(history.GetObjectCount(), 2u); + + std::vector ids = IdsInIndexOrder(); + ASSERT_EQ(ids.size(), 2u); + EXPECT_EQ(ids[0], kIdB); + EXPECT_EQ(ids[1], kIdA); +} + +TEST_F(StatisticsHistoryStore, ClearDropsEveryObject) +{ + AddThreeOutOfOrder(); + history.Clear(); + EXPECT_EQ(history.GetObjectCount(), 0u); +} + +TEST_F(StatisticsHistoryStore, ValuesRoundTripThroughTheKeyedStore) +{ + ASSERT_TRUE(history.AddObject(StatisticsHistory::TrackedObjectData(kIdA, 0, 0))); + + ASSERT_TRUE(history.AddValueByObjectID(kIdA, "ping", 50.0, 1000, false)); + ASSERT_TRUE(history.AddValueByObjectID(kIdA, "ping", 70.0, 2000, false)); + + StatisticsHistory::TimeAndValueQueue *q = 0; + ASSERT_EQ(history.GetHistoryForKey(kIdA, "ping", &q, 3000), StatisticsHistory::SH_OK); + ASSERT_NE(q, (StatisticsHistory::TimeAndValueQueue *) 0); + EXPECT_EQ(q->values.Size(), 2u); +} + +TEST_F(StatisticsHistoryStore, DistinctKeysOnOneObjectAreStoredSeparately) +{ + ASSERT_TRUE(history.AddObject(StatisticsHistory::TrackedObjectData(kIdA, 0, 0))); + ASSERT_TRUE(history.AddValueByObjectID(kIdA, "ping", 50.0, 1000, false)); + ASSERT_TRUE(history.AddValueByObjectID(kIdA, "loss", 1.0, 1000, false)); + + StatisticsHistory::TimeAndValueQueue *ping = 0; + StatisticsHistory::TimeAndValueQueue *loss = 0; + ASSERT_EQ(history.GetHistoryForKey(kIdA, "ping", &ping, 2000), StatisticsHistory::SH_OK); + ASSERT_EQ(history.GetHistoryForKey(kIdA, "loss", &loss, 2000), StatisticsHistory::SH_OK); + ASSERT_NE(ping, (StatisticsHistory::TimeAndValueQueue *) 0); + ASSERT_NE(loss, (StatisticsHistory::TimeAndValueQueue *) 0); + EXPECT_NE(ping, loss) << "two keys on one object must not share a queue"; +} + +TEST_F(StatisticsHistoryStore, AnUnknownKeyIsReportedRatherThanInvented) +{ + ASSERT_TRUE(history.AddObject(StatisticsHistory::TrackedObjectData(kIdA, 0, 0))); + StatisticsHistory::TimeAndValueQueue *q = 0; + EXPECT_NE(history.GetHistoryForKey(kIdA, "absent", &q, 1000), StatisticsHistory::SH_OK); +} + +TEST_F(StatisticsHistoryStore, AnUnknownObjectIsReportedRatherThanInvented) +{ + StatisticsHistory::TimeAndValueQueue *q = 0; + EXPECT_NE(history.GetHistoryForKey(12345, "ping", &q, 1000), StatisticsHistory::SH_OK); +} + +TEST_F(StatisticsHistoryStore, UniqueKeyListIsTheUnionAcrossObjects) +{ + ASSERT_TRUE(history.AddObject(StatisticsHistory::TrackedObjectData(kIdA, 0, 0))); + ASSERT_TRUE(history.AddObject(StatisticsHistory::TrackedObjectData(kIdB, 0, 0))); + + ASSERT_TRUE(history.AddValueByObjectID(kIdA, "ping", 50.0, 1000, false)); + ASSERT_TRUE(history.AddValueByObjectID(kIdA, "loss", 1.0, 1000, false)); + ASSERT_TRUE(history.AddValueByObjectID(kIdB, "ping", 60.0, 1000, false)); // duplicate key + ASSERT_TRUE(history.AddValueByObjectID(kIdB, "jitter", 2.0, 1000, false)); + + DataStructures::List keys; + history.GetUniqueKeyList(keys); + + // The list is a set union; its order is not part of the contract, so compare as a set. + std::set got; + for (unsigned i = 0; i < keys.Size(); ++i) + got.insert(keys[i].C_String()); + + std::set want; + want.insert("ping"); + want.insert("loss"); + want.insert("jitter"); + EXPECT_EQ(got, want); + EXPECT_EQ(keys.Size(), 3u) << "a key present on two objects must appear once"; +} + +TEST_F(StatisticsHistoryStore, HistorySortedReturnsOneEntryPerKeyOnTheObject) +{ + ASSERT_TRUE(history.AddObject(StatisticsHistory::TrackedObjectData(kIdA, 0, 0))); + ASSERT_TRUE(history.AddValueByObjectID(kIdA, "ping", 50.0, 1000, false)); + ASSERT_TRUE(history.AddValueByObjectID(kIdA, "loss", 1.0, 1000, false)); + + DataStructures::List values; + ASSERT_TRUE(history.GetHistorySorted(kIdA, StatisticsHistory::SH_SORT_BY_RECENT_SUM_ASCENDING, values)); + EXPECT_EQ(values.Size(), 2u); +} + +TEST_F(StatisticsHistoryStore, HistorySortedOnAnUnknownObjectFails) +{ + DataStructures::List values; + EXPECT_FALSE(history.GetHistorySorted(999, StatisticsHistory::SH_SORT_BY_RECENT_SUM_ASCENDING, values)); +} From 10616c08a313e49d9864a76544e716db370ad1ec Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:33:23 +0200 Subject: [PATCH 02/26] refactor(statisticshistory): move the object and keyed stores to std containers Part of #60, stage 2. Both stores are protected members, so no public declaration changes here. - `objects`: DataStructures::OrderedList -> std::vector kept sorted by objectId, searched with std::lower_bound. Deliberately NOT std::map: GetObjectIndex, GetObjectAtIndex and RemoveObjectAtIndex all publish this index, so being both sorted and O(1) indexable is a public contract. - `dataQueues`: DataStructures::Hash -> std::unordered_map with a hasher reusing that same ToInteger. - GetHistorySorted's local OrderedList -> std::vector + std::sort with the unchanged comparators. Equivalent rather than approximately so: the comparators tie-break on the key name after sortValue and keys are unique within an object, so they never return 0 for two distinct queues -- which matters because the OrderedList::Insert they replace refused comparator-equal entries outright and would have dropped such a queue. - GetUniqueKeyList now iterates the map instead of materialising two parallel lists via GetAsList. Its output order was previously hash-bucket order and is now unordered_map order; both are unspecified and the only contract is that it is a set union, which the tests assert as a set. Fixes a leak found while reading this code: TrackedObject::~TrackedObject declared two empty local lists and iterated the empty one, never calling GetAsList, so the OP_DELETE never ran and every TimeAndValueQueue the object owned leaked -- one per distinct key passed to AddValueByObjectID, and nothing else in the file freed them. LeakSanitizer on Linux reported 12 leaked queues plus their sample arrays for 4 objects x 3 keys before, and 0 records after. Note the leak fix itself is not guarded by the test suite: it is only observable under a leak checker, and CI has no sanitizer job. The 16 tests added in dd405285 cover the destructor path functionally but would still pass if the fix were reverted. Left as stage 3 work, bound to public signatures: GetHistorySorted and GetUniqueKeyList out-parameters, ResizeSampleSet's histogram argument and the local it is passed, and three locals that are arguments to RakPeerInterface::GetStatisticsList. --- Source/include/mafianet/statistics_history.h | 25 ++++- Source/src/statistics_history.cpp | 106 ++++++++++++------- 2 files changed, 90 insertions(+), 41 deletions(-) diff --git a/Source/include/mafianet/statistics_history.h b/Source/include/mafianet/statistics_history.h index 6fe964275..372f0505a 100644 --- a/Source/include/mafianet/statistics_history.h +++ b/Source/include/mafianet/statistics_history.h @@ -27,6 +27,8 @@ #include "mafianet/memory_override.h" #include "mafianet/native_types.h" #include "mafianet/ds_list.h" +#include +#include #include "mafianet/types.h" #include "mafianet/ds_ordered_list.h" #include "mafianet/string.h" @@ -190,15 +192,34 @@ class RAK_DLL_EXPORT StatisticsHistory static int TrackedObjectComp( const uint64_t &key, TrackedObject* const &data ); protected: + // RakString has no std::hash specialisation; reuse the hash the replaced + // DataStructures::Hash instantiation was given. + struct RakStringKeyHash + { + size_t operator()(const MafiaNet::RakString &s) const + { + return (size_t) MafiaNet::RakString::ToInteger(s); + } + }; + struct TrackedObject { TrackedObject(); ~TrackedObject(); TrackedObjectData trackedObjectData; - DataStructures::Hash dataQueues; + // Keyed by the caller's key string. Unordered: the only consumers either sort the result + // (GetHistorySorted) or treat it as a set (GetUniqueKeyList), so bucket order never mattered. + std::unordered_map dataQueues; }; - DataStructures::OrderedList objects; + // Kept sorted ascending by trackedObjectData.objectId. The sort order is a public contract: + // GetObjectIndex, GetObjectAtIndex and RemoveObjectAtIndex all expose this index, so a + // replacement must stay both sorted and O(1) indexable -- hence a vector with lower_bound + // rather than a std::map. Insert through IndexForObjectId to preserve it. + std::vector objects; + + // Position of objectId, or of where it would be inserted; *found reports which. + unsigned int IndexForObjectId(uint64_t objectId, bool *found) const; Time timeToTrack; }; diff --git a/Source/src/statistics_history.cpp b/Source/src/statistics_history.cpp index 3ba358c6c..e5c7166cc 100644 --- a/Source/src/statistics_history.cpp +++ b/Source/src/statistics_history.cpp @@ -17,6 +17,7 @@ #if _RAKNET_SUPPORT_StatisticsHistory==1 #include "mafianet/statistics_history.h" +#include #include "mafianet/get_time.h" #include "mafianet/statistics.h" #include "mafianet/peer_interface.h" @@ -77,12 +78,12 @@ Time StatisticsHistory::GetDefaultTimeToTrack(void) const {return timeToTrack;} bool StatisticsHistory::AddObject(TrackedObjectData tod) { bool objectExists; - unsigned int idx = objects.GetIndexFromKey(tod.objectId, &objectExists); + unsigned int idx = IndexForObjectId(tod.objectId, &objectExists); if (objectExists) return false; TrackedObject *to = MafiaNet::OP_NEW(_FILE_AND_LINE_); to->trackedObjectData=tod; - objects.InsertAtIndex(to,idx,_FILE_AND_LINE_); + objects.insert(objects.begin() + idx, to); return true; } bool StatisticsHistory::RemoveObject(uint64_t objectId, void **userData) @@ -98,18 +99,18 @@ bool StatisticsHistory::RemoveObject(uint64_t objectId, void **userData) void StatisticsHistory::RemoveObjectAtIndex(unsigned int index) { TrackedObject *to = objects[index]; - objects.RemoveAtIndex(index); + objects.erase(objects.begin() + index); MafiaNet::OP_DELETE(to, _FILE_AND_LINE_); } void StatisticsHistory::Clear(void) { - for (unsigned int idx=0; idx < objects.Size(); idx++) + for (unsigned int idx=0; idx < objects.size(); idx++) { MafiaNet::OP_DELETE(objects[idx], _FILE_AND_LINE_); } - objects.Clear(false, _FILE_AND_LINE_); + objects.clear(); } -unsigned int StatisticsHistory::GetObjectCount(void) const {return objects.Size();} +unsigned int StatisticsHistory::GetObjectCount(void) const {return (unsigned int) objects.size();} StatisticsHistory::TrackedObjectData * StatisticsHistory::GetObjectAtIndex(unsigned int index) const {return &objects[index]->trackedObjectData;} bool StatisticsHistory::AddValueByObjectID(uint64_t objectId, RakString key, SHValueType val, Time curTime, bool combineEqualTimes) { @@ -123,17 +124,18 @@ void StatisticsHistory::AddValueByIndex(unsigned int index, RakString key, SHVal { TimeAndValueQueue *queue; TrackedObject *to = objects[index]; - DataStructures::HashIndex hi = to->dataQueues.GetIndexOf(key); - if (hi.IsInvalid()) + std::unordered_map::iterator it = + to->dataQueues.find(key); + if (it == to->dataQueues.end()) { queue = MafiaNet::OP_NEW(_FILE_AND_LINE_); queue->key=key; queue->timeToTrackValues = timeToTrack; - to->dataQueues.Push(key, queue, _FILE_AND_LINE_); + to->dataQueues.insert(std::make_pair(key, queue)); } else { - queue = to->dataQueues.ItemAtIndex(hi); + queue = it->second; } TimeAndValue tav; @@ -173,10 +175,11 @@ StatisticsHistory::SHErrorCode StatisticsHistory::GetHistoryForKey(uint64_t obje if (idx == (unsigned int) -1) return SH_UKNOWN_OBJECT; TrackedObject *to = objects[idx]; - DataStructures::HashIndex hi = to->dataQueues.GetIndexOf(key); - if (hi.IsInvalid()) + std::unordered_map::const_iterator it = + to->dataQueues.find(key); + if (it == to->dataQueues.end()) return SH_UKNOWN_KEY; - *values = to->dataQueues.ItemAtIndex(hi); + *values = it->second; (*values)->CullExpiredValues(curTime); return SH_OK; } @@ -186,13 +189,15 @@ bool StatisticsHistory::GetHistorySorted(uint64_t objectId, SHSortOperation sort if (idx == (unsigned int) -1) return false; TrackedObject *to = objects[idx]; - DataStructures::List itemList; - DataStructures::List keyList; - to->dataQueues.GetAsList(itemList,keyList,_FILE_AND_LINE_); + std::vector itemList; + itemList.reserve(to->dataQueues.size()); + for (std::unordered_map::const_iterator + it = to->dataQueues.begin(); it != to->dataQueues.end(); ++it) + itemList.push_back(it->second); Time curTime = GetTime(); - DataStructures::OrderedList sortedQueues; - for (unsigned int i=0; i < itemList.Size(); i++) + bool ascending = false; + for (unsigned int i=0; i < itemList.size(); i++) { TimeAndValueQueue *tavq = itemList[i]; tavq->CullExpiredValues(curTime); @@ -226,13 +231,21 @@ bool StatisticsHistory::GetHistorySorted(uint64_t objectId, SHSortOperation sort sortType == SH_SORT_BY_RECENT_LOWEST_ASCENDING || sortType == SH_SORT_BY_LONG_TERM_HIGHEST_ASCENDING || sortType == SH_SORT_BY_LONG_TERM_LOWEST_ASCENDING) - sortedQueues.Insert(tavq, tavq, false, _FILE_AND_LINE_, TimeAndValueQueueCompAsc); - else - sortedQueues.Insert(tavq, tavq, false, _FILE_AND_LINE_, TimeAndValueQueueCompDesc); + ascending = true; } - for (unsigned int i=0; i < sortedQueues.Size(); i++) - values.Push(sortedQueues[i], _FILE_AND_LINE_); + // Same comparators the OrderedList was given. They order by sortValue and tie-break on the + // key name, and keys are unique within an object, so no two distinct queues compare equal. + if (ascending) + std::sort(itemList.begin(), itemList.end(), + [](TimeAndValueQueue *a, TimeAndValueQueue *b) { return TimeAndValueQueueCompAsc(a, b) < 0; }); + else + std::sort(itemList.begin(), itemList.end(), + [](TimeAndValueQueue *a, TimeAndValueQueue *b) { return TimeAndValueQueueCompDesc(a, b) < 0; }); + + // values is a public DataStructures::List out-parameter; stage 3 changes the signature. + for (unsigned int i=0; i < itemList.size(); i++) + values.Push(itemList[i], _FILE_AND_LINE_); return true; } void StatisticsHistory::MergeAllObjectsOnKey(RakString key, TimeAndValueQueue *tavqOutput, SHDataCategory dataCategory) const @@ -242,13 +255,14 @@ void StatisticsHistory::MergeAllObjectsOnKey(RakString key, TimeAndValueQueue *t Time curTime = GetTime(); // Find every object with this key - for (unsigned int idx=0; idx < objects.Size(); idx++) + for (unsigned int idx=0; idx < objects.size(); idx++) { TrackedObject *to = objects[idx]; - DataStructures::HashIndex hi = to->dataQueues.GetIndexOf(key); - if (hi.IsInvalid()==false) + std::unordered_map::const_iterator it = + to->dataQueues.find(key); + if (it != to->dataQueues.end()) { - TimeAndValueQueue *tavqInput = to->dataQueues.ItemAtIndex(hi); + TimeAndValueQueue *tavqInput = it->second; tavqInput->CullExpiredValues(curTime); TimeAndValueQueue::MergeSets(tavqOutput, dataCategory, tavqInput, dataCategory, tavqOutput); } @@ -258,18 +272,16 @@ void StatisticsHistory::GetUniqueKeyList(DataStructures::List &keys) { keys.Clear(true, _FILE_AND_LINE_); - for (unsigned int idx=0; idx < objects.Size(); idx++) + for (unsigned int idx=0; idx < objects.size(); idx++) { TrackedObject *to = objects[idx]; - DataStructures::List itemList; - DataStructures::List keyList; - to->dataQueues.GetAsList(itemList, keyList, _FILE_AND_LINE_); - for (unsigned int k=0; k < keyList.Size(); k++) + for (std::unordered_map::const_iterator + it = to->dataQueues.begin(); it != to->dataQueues.end(); ++it) { bool hasKey=false; for (unsigned int j=0; j < keys.Size(); j++) { - if (keys[j]==keyList[k]) + if (keys[j]==it->first) { hasKey=true; break; @@ -277,7 +289,7 @@ void StatisticsHistory::GetUniqueKeyList(DataStructures::List &keys) } if (hasKey==false) - keys.Push(keyList[k], _FILE_AND_LINE_); + keys.Push(it->first, _FILE_AND_LINE_); } } } @@ -699,15 +711,31 @@ StatisticsHistory::TimeAndValueQueue& StatisticsHistory::TimeAndValueQueue::oper StatisticsHistory::TrackedObject::TrackedObject() {} StatisticsHistory::TrackedObject::~TrackedObject() { - DataStructures::List itemList; - DataStructures::List keyList; - for (unsigned int idx=0; idx < itemList.Size(); idx++) - MafiaNet::OP_DELETE(itemList[idx], _FILE_AND_LINE_); + // This used to declare two empty local lists and then iterate the empty one, never + // calling GetAsList, so every TimeAndValueQueue this object owned leaked -- one per + // distinct key passed to AddValueByObjectID. Confirmed under LeakSanitizer. + for (std::unordered_map::iterator + it = dataQueues.begin(); it != dataQueues.end(); ++it) + MafiaNet::OP_DELETE(it->second, _FILE_AND_LINE_); + dataQueues.clear(); +} +unsigned int StatisticsHistory::IndexForObjectId(uint64_t objectId, bool *found) const +{ + // objects is sorted ascending by objectId; see the declaration for why that is a contract. + std::vector::const_iterator it = std::lower_bound( + objects.begin(), objects.end(), objectId, + [](TrackedObject * const &candidate, uint64_t id) + { + return candidate->trackedObjectData.objectId < id; + }); + unsigned int idx = (unsigned int) (it - objects.begin()); + *found = (it != objects.end() && (*it)->trackedObjectData.objectId == objectId); + return idx; } unsigned int StatisticsHistory::GetObjectIndex(uint64_t objectId) const { bool objectExists; - unsigned int idx = objects.GetIndexFromKey(objectId, &objectExists); + unsigned int idx = IndexForObjectId(objectId, &objectExists); if (objectExists) return idx; return (unsigned int) -1; From f593d5c3acef2f00fc2ef0bb531bda87e99b8b86 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:37:35 +0200 Subject: [PATCH 03/26] fix(rpc4): report success when a local callback is removed from a shared id Found by writing the tests that have to exist before RPC4's registration tables move to std containers (#60, stage 2). UnregisterLocalCallback put its `return true` inside the branch that fires only when the removed name was the LAST one for that message id. With two or more names registered against one id, removing one succeeded but the call returned false, contradicting the documented contract: "True if the combination of uniqueID and messageId was in use, and hence removed". Adds 14 cases covering the three registration tables: register/unregister round-trips for non-blocking, blocking and local-callback entries, refusal of a duplicate name, independence of the blocking and non-blocking tables, 200 names at once so a migration that collapses colliding keys shows up here, and loopback dispatch reaching the registered function. RPC4Registry.OneMessageIdCarriesSeveralFunctionNames fails before this fix and passes after. Not flaky: --gtest_repeat=10 clean. The fixture starts the peer on an ephemeral 127.0.0.1 port, because CallLoopback pushes a packet that only surfaces through Receive() on a started peer; there is no connection and no traffic, so the suite stays hermetic. --- Source/src/rpc4_plugin.cpp | 6 +- Tests/Unit/RPC4RegistryTests.cpp | 256 +++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 Tests/Unit/RPC4RegistryTests.cpp diff --git a/Source/src/rpc4_plugin.cpp b/Source/src/rpc4_plugin.cpp index d521f8b37..501ab2541 100644 --- a/Source/src/rpc4_plugin.cpp +++ b/Source/src/rpc4_plugin.cpp @@ -254,8 +254,12 @@ bool RPC4::UnregisterLocalCallback(const char* uniqueID, MessageID messageId) { MafiaNet::OP_DELETE(lc,_FILE_AND_LINE_); localCallbacks.RemoveAtIndex(index); - return true; } + // The documented contract is "true if the combination of uniqueID and messageId was + // in use, and hence removed". The return used to sit inside the branch above, so a + // message id with more than one registered name reported false after successfully + // removing one. + return true; } } return false; diff --git a/Tests/Unit/RPC4RegistryTests.cpp b/Tests/Unit/RPC4RegistryTests.cpp new file mode 100644 index 000000000..9b119f3b7 --- /dev/null +++ b/Tests/Unit/RPC4RegistryTests.cpp @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + */ + +#include + +#include +#include + +#include "mafianet/rpc4_plugin.h" +#include "mafianet/peer_interface.h" +#include "mafianet/bit_stream.h" +#include "mafianet/message_identifiers.h" + +using namespace MafiaNet; + +/* +Description: +Covers RPC4's three registration tables -- non-blocking functions, blocking functions and +local callbacks -- written before they move to the standard library (std migration stage 2, #60). +The two tables are DataStructures::Hash keyed by function name; localCallbacks is a +DataStructures::OrderedList keyed by MessageID holding one entry per message id, each owning an +ordered list of function names. + +The existing RPC4 coverage is two cases about global-registration name handling plus one +integration test, and none of it exercises registration, unregistration, replacement or dispatch, +which is exactly what a container swap can break quietly: + + - Hash::Push on an existing key and OrderedList::Insert on an existing key behave differently + from each other (one appends a second entry for the same key, the other refuses), and both + differ from std::unordered_map::insert and operator[]. A migration that picks the wrong one + either loses a registration or silently keeps a stale function pointer. + - localCallbacks holds a sorted list of names per message id, and the dispatch loop walks it, so + both the per-id lookup and the per-id name list have to survive. + +Success conditions: register/unregister round-trips report accurately, re-registering replaces +rather than duplicates, the blocking and non-blocking tables are independent, a local callback can +carry several function names, and loopback dispatch reaches the registered function. +*/ + +namespace +{ + int g_calls = 0; + std::string g_lastPayload; + + void CountingRpc(MafiaNet::BitStream *userData, Packet *, void *) + { + ++g_calls; + g_lastPayload.clear(); + if (userData != 0) + { + char buf[64]; + memset(buf, 0, sizeof(buf)); + // Read whatever the caller wrote, bounded by the buffer. + const unsigned bytes = (unsigned) BITS_TO_BYTES(userData->GetNumberOfUnreadBits()); + if (bytes > 0 && bytes < sizeof(buf)) + { + userData->Read(buf, bytes); + g_lastPayload = buf; + } + } + } + + void OtherRpc(MafiaNet::BitStream *, Packet *, void *) {} + + void CountingBlockingRpc(MafiaNet::BitStream *, MafiaNet::BitStream *, Packet *, void *) {} + + class RPC4Registry : public ::testing::Test + { + protected: + void SetUp() override + { + g_calls = 0; + g_lastPayload.clear(); + peer = RakPeerInterface::GetInstance(); + peer->AttachPlugin(&rpc4); + // Started but unconnected: CallLoopback pushes a packet that only surfaces + // through Receive() on a started peer. Ephemeral port, no traffic. + SocketDescriptor sd(0, "127.0.0.1"); + ASSERT_EQ(peer->Startup(1, &sd, 1), RAKNET_STARTED); + } + void TearDown() override + { + peer->DetachPlugin(&rpc4); + peer->Shutdown(100); + RakPeerInterface::DestroyInstance(peer); + } + + RakPeerInterface *peer = nullptr; + RPC4 rpc4; + }; +} + +TEST_F(RPC4Registry, UnregisteringAnUnknownFunctionFails) +{ + EXPECT_FALSE(rpc4.UnregisterFunction("never.registered")); +} + +TEST_F(RPC4Registry, RegisterThenUnregisterRoundTrips) +{ + EXPECT_TRUE(rpc4.RegisterFunction("ping", CountingRpc, 0)); + EXPECT_TRUE(rpc4.UnregisterFunction("ping")); + EXPECT_FALSE(rpc4.UnregisterFunction("ping")) << "a second unregister must report nothing to remove"; +} + +TEST_F(RPC4Registry, RegisteringTheSameNameTwiceIsRefused) +{ + EXPECT_TRUE(rpc4.RegisterFunction("ping", CountingRpc, 0)); + EXPECT_FALSE(rpc4.RegisterFunction("ping", OtherRpc, 0)); + + // Exactly one entry must exist under that name, whichever function it holds. + EXPECT_TRUE(rpc4.UnregisterFunction("ping")); + EXPECT_FALSE(rpc4.UnregisterFunction("ping")) << "the refused registration must not have added a second entry"; +} + +TEST_F(RPC4Registry, SeveralDistinctNamesCoexist) +{ + ASSERT_TRUE(rpc4.RegisterFunction("a", CountingRpc, 0)); + ASSERT_TRUE(rpc4.RegisterFunction("b", CountingRpc, 0)); + ASSERT_TRUE(rpc4.RegisterFunction("c", CountingRpc, 0)); + + EXPECT_TRUE(rpc4.UnregisterFunction("b")); + EXPECT_TRUE(rpc4.UnregisterFunction("a")); + EXPECT_TRUE(rpc4.UnregisterFunction("c")); +} + +TEST_F(RPC4Registry, ManyNamesSurviveTogether) +{ + // Enough names to spread across hash buckets, so a migration that collapses + // colliding keys shows up here rather than in production. + const int count = 200; + for (int i = 0; i < count; ++i) + { + char name[32]; + snprintf(name, sizeof(name), "fn.%d", i); + ASSERT_TRUE(rpc4.RegisterFunction(name, CountingRpc, 0)) << "registering " << name; + } + for (int i = 0; i < count; ++i) + { + char name[32]; + snprintf(name, sizeof(name), "fn.%d", i); + EXPECT_TRUE(rpc4.UnregisterFunction(name)) << "unregistering " << name; + } +} + +TEST_F(RPC4Registry, BlockingAndNonBlockingTablesAreIndependent) +{ + ASSERT_TRUE(rpc4.RegisterFunction("shared.name", CountingRpc, 0)); + ASSERT_TRUE(rpc4.RegisterBlockingFunction("shared.name", CountingBlockingRpc, 0)); + + // Removing one must leave the other in place. + EXPECT_TRUE(rpc4.UnregisterFunction("shared.name")); + EXPECT_FALSE(rpc4.UnregisterFunction("shared.name")); + EXPECT_TRUE(rpc4.UnregisterBlockingFunction("shared.name")); + EXPECT_FALSE(rpc4.UnregisterBlockingFunction("shared.name")); +} + +TEST_F(RPC4Registry, BlockingRegisterThenUnregisterRoundTrips) +{ + EXPECT_FALSE(rpc4.UnregisterBlockingFunction("blocking.absent")); + EXPECT_TRUE(rpc4.RegisterBlockingFunction("blocking.present", CountingBlockingRpc, 0)); + EXPECT_TRUE(rpc4.UnregisterBlockingFunction("blocking.present")); + EXPECT_FALSE(rpc4.UnregisterBlockingFunction("blocking.present")); +} + +TEST_F(RPC4Registry, LocalCallbackRoundTripsForOneMessageId) +{ + const MessageID id = ID_USER_PACKET_ENUM + 1; + ASSERT_TRUE(rpc4.RegisterFunction("cb", CountingRpc, 0)); + rpc4.RegisterLocalCallback("cb", id); + + EXPECT_TRUE(rpc4.UnregisterLocalCallback("cb", id)); + EXPECT_FALSE(rpc4.UnregisterLocalCallback("cb", id)) + << "a second unregister must report nothing to remove"; +} + +TEST_F(RPC4Registry, OneMessageIdCarriesSeveralFunctionNames) +{ + const MessageID id = ID_USER_PACKET_ENUM + 2; + ASSERT_TRUE(rpc4.RegisterFunction("first", CountingRpc, 0)); + ASSERT_TRUE(rpc4.RegisterFunction("second", OtherRpc, 0)); + rpc4.RegisterLocalCallback("first", id); + rpc4.RegisterLocalCallback("second", id); + + // Removing one name must leave the other attached to the same message id. + EXPECT_TRUE(rpc4.UnregisterLocalCallback("first", id)); + EXPECT_TRUE(rpc4.UnregisterLocalCallback("second", id)); + EXPECT_FALSE(rpc4.UnregisterLocalCallback("second", id)); +} + +TEST_F(RPC4Registry, DistinctMessageIdsAreKeptApart) +{ + const MessageID idA = ID_USER_PACKET_ENUM + 3; + const MessageID idB = ID_USER_PACKET_ENUM + 4; + ASSERT_TRUE(rpc4.RegisterFunction("cb", CountingRpc, 0)); + rpc4.RegisterLocalCallback("cb", idA); + rpc4.RegisterLocalCallback("cb", idB); + + EXPECT_TRUE(rpc4.UnregisterLocalCallback("cb", idA)); + EXPECT_FALSE(rpc4.UnregisterLocalCallback("cb", idA)); + EXPECT_TRUE(rpc4.UnregisterLocalCallback("cb", idB)) << "the other message id must be unaffected"; +} + +TEST_F(RPC4Registry, UnregisteringALocalCallbackOnAnUnknownMessageIdFails) +{ + ASSERT_TRUE(rpc4.RegisterFunction("cb", CountingRpc, 0)); + EXPECT_FALSE(rpc4.UnregisterLocalCallback("cb", ID_USER_PACKET_ENUM + 99)); +} + +TEST_F(RPC4Registry, LoopbackReachesTheRegisteredFunction) +{ + ASSERT_TRUE(rpc4.RegisterFunction("loop", CountingRpc, 0)); + + MafiaNet::BitStream bs; + bs.Write("hi", 3); // includes the terminator + rpc4.CallLoopback("loop", &bs); + + // CallLoopback pushes a packet the plugin consumes on the next Receive. + for (int i = 0; i < 20 && g_calls == 0; ++i) + { + Packet *p = peer->Receive(); + if (p != 0) + peer->DeallocatePacket(p); + } + + EXPECT_EQ(g_calls, 1); + EXPECT_EQ(g_lastPayload, "hi"); +} + +TEST_F(RPC4Registry, LoopbackToAnUnknownNameDoesNotInvokeAnything) +{ + ASSERT_TRUE(rpc4.RegisterFunction("loop", CountingRpc, 0)); + + MafiaNet::BitStream bs; + rpc4.CallLoopback("absent", &bs); + + for (int i = 0; i < 20; ++i) + { + Packet *p = peer->Receive(); + if (p != 0) + peer->DeallocatePacket(p); + } + + EXPECT_EQ(g_calls, 0); +} + +TEST_F(RPC4Registry, SlotRegisterThenUnregisterRoundTrips) +{ + EXPECT_FALSE(rpc4.UnregisterSlot("slot.absent")); + rpc4.RegisterSlot("slot.present", CountingRpc, 0, 0); + EXPECT_TRUE(rpc4.UnregisterSlot("slot.present")); + EXPECT_FALSE(rpc4.UnregisterSlot("slot.present")); +} From 39c2a119db84d2ee7ee3fc434e046bf8aecc3ca3 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:41:00 +0200 Subject: [PATCH 04/26] refactor(rpc4): move the registration tables to std containers Part of #60, stage 2. All three are protected members, so no public declaration changes. - registeredNonblockingFunctions, registeredBlockingFunctions: DataStructures::Hash -> std::unordered_map with a hasher reusing that same ToInteger. Hash::Pop became find + erase, which is also what gives UnregisterFunction and UnregisterBlockingFunction their bool. - localCallbacks: DataStructures::OrderedList -> std::vector kept sorted by messageId and searched with std::lower_bound through the new IndexForMessageId helper. The dispatch path looks up by message id for every received packet, so the ordering is kept rather than traded for an unsorted vector. - CallBlocking's local DataStructures::Queue -> std::deque. It was used as a stack, not a queue: PushAtHead(p,0) pushed to the front and Pop() took from the front, so held packets are handed back in reverse arrival order. push_front/pop_front preserves that; a vector with push_back/pop_back would have reversed the order they are given back to the peer. Left as stage 3 work: localSlots, LocalCallback::functions and LocalSlot::slotObjects are tagged \internal but declared in RPC4's public section, and GetLocalSlotIndex/InvokeSignal take DataStructures::HashIndex because of it. Those four cannot move while the declarations are public. Unit suite green, RPC4Context integration test green, RPC4 suites clean under --gtest_repeat=5. --- Source/include/mafianet/rpc4_plugin.h | 23 +++++- Source/src/rpc4_plugin.cpp | 109 +++++++++++++++++--------- 2 files changed, 91 insertions(+), 41 deletions(-) diff --git a/Source/include/mafianet/rpc4_plugin.h b/Source/include/mafianet/rpc4_plugin.h index 33c4c01f6..cff1e88e5 100644 --- a/Source/include/mafianet/rpc4_plugin.h +++ b/Source/include/mafianet/rpc4_plugin.h @@ -31,6 +31,8 @@ #include "mafianet/string.h" #include "mafianet/network_id_object.h" #include "mafianet/ds_hash.h" +#include +#include #include "mafianet/ds_ordered_list.h" /// \defgroup RPC_PLUGIN_GROUP RPC @@ -238,9 +240,24 @@ class NetworkIDManager; void *context; }; - DataStructures::Hash registeredNonblockingFunctions; - DataStructures::Hash registeredBlockingFunctions; - DataStructures::OrderedList localCallbacks; + // RakString has no std::hash specialisation; reuse the hash the replaced + // DataStructures::Hash instantiations were given. + struct RakStringKeyHash + { + size_t operator()(const MafiaNet::RakString &s) const + { + return (size_t) MafiaNet::RakString::ToInteger(s); + } + }; + std::unordered_map registeredNonblockingFunctions; + std::unordered_map registeredBlockingFunctions; + // Kept sorted ascending by LocalCallback::messageId and searched with std::lower_bound, + // matching the OrderedList it replaces. Nothing publishes this index, but the dispatch + // path looks up by message id on every received packet, so the ordering is kept. + std::vector localCallbacks; + + // Position of messageId, or of where it would be inserted; *found reports which. + unsigned int IndexForMessageId(MessageID messageId, bool *found) const; MafiaNet::BitStream blockingReturnValue; bool gotBlockingReturnValue; diff --git a/Source/src/rpc4_plugin.cpp b/Source/src/rpc4_plugin.cpp index 501ab2541..66f8429ac 100644 --- a/Source/src/rpc4_plugin.cpp +++ b/Source/src/rpc4_plugin.cpp @@ -17,6 +17,8 @@ #if _RAKNET_SUPPORT_RPC4Plugin==1 #include "mafianet/rpc4_plugin.h" +#include +#include #include "mafianet/message_identifiers.h" #include "mafianet/peer_interface.h" #include "mafianet/packetized_tcp.h" @@ -147,7 +149,7 @@ RPC4::RPC4() RPC4::~RPC4() { unsigned int i; - for (i=0; i < localCallbacks.Size(); i++) + for (i=0; i < localCallbacks.size(); i++) { MafiaNet::OP_DELETE(localCallbacks[i],_FILE_AND_LINE_); } @@ -164,14 +166,13 @@ RPC4::~RPC4() } bool RPC4::RegisterFunction(const char* uniqueID, void ( *functionPointer ) (MafiaNet::BitStream *userData, Packet *packet, void *context ), void *context) { - DataStructures::HashIndex skhi = registeredNonblockingFunctions.GetIndexOf(uniqueID); - if (skhi.IsInvalid()==false) + if (registeredNonblockingFunctions.find(uniqueID) != registeredNonblockingFunctions.end()) return false; RegisteredNonblockingFunction rnf; rnf.functionPointer=functionPointer; rnf.context=context; - registeredNonblockingFunctions.Push(uniqueID,rnf,_FILE_AND_LINE_); + registeredNonblockingFunctions.insert(std::make_pair(MafiaNet::RakString(uniqueID), rnf)); return true; } void RPC4::RegisterSlot(const char *sharedIdentifier, void ( *functionPointer ) (MafiaNet::BitStream *userData, Packet *packet, void *context ), void *context, int callPriority) @@ -192,16 +193,28 @@ void RPC4::RegisterSlot(const char *sharedIdentifier, void ( *functionPointer ) } bool RPC4::RegisterBlockingFunction(const char* uniqueID, void ( *functionPointer ) (MafiaNet::BitStream *userData, MafiaNet::BitStream *returnData, Packet *packet, void *context ), void *context) { - DataStructures::HashIndex skhi = registeredBlockingFunctions.GetIndexOf(uniqueID); - if (skhi.IsInvalid()==false) + if (registeredBlockingFunctions.find(uniqueID) != registeredBlockingFunctions.end()) return false; RegisteredBlockingFunction rbf; rbf.functionPointer=functionPointer; rbf.context=context; - registeredBlockingFunctions.Push(uniqueID,rbf,_FILE_AND_LINE_); + registeredBlockingFunctions.insert(std::make_pair(MafiaNet::RakString(uniqueID), rbf)); return true; } +unsigned int RPC4::IndexForMessageId(MessageID messageId, bool *found) const +{ + // localCallbacks is sorted ascending by messageId; see the declaration. + std::vector::const_iterator it = std::lower_bound( + localCallbacks.begin(), localCallbacks.end(), messageId, + [](LocalCallback * const &candidate, MessageID id) + { + return candidate->messageId < id; + }); + unsigned int idx = (unsigned int) (it - localCallbacks.begin()); + *found = (it != localCallbacks.end() && (*it)->messageId == messageId); + return idx; +} void RPC4::RegisterLocalCallback(const char* uniqueID, MessageID messageId) { bool objectExists; @@ -209,31 +222,39 @@ void RPC4::RegisterLocalCallback(const char* uniqueID, MessageID messageId) LocalCallback *lc; MafiaNet::RakString str; str=uniqueID; - index = localCallbacks.GetIndexFromKey(messageId,&objectExists); + index = IndexForMessageId(messageId,&objectExists); if (objectExists) { lc = localCallbacks[index]; - index = lc->functions.GetIndexFromKey(str,&objectExists); + unsigned int fnIndex = lc->functions.GetIndexFromKey(str,&objectExists); if (objectExists==false) - lc->functions.InsertAtIndex(str,index,_FILE_AND_LINE_); + lc->functions.InsertAtIndex(str,fnIndex,_FILE_AND_LINE_); } else { lc = MafiaNet::OP_NEW(_FILE_AND_LINE_); lc->messageId=messageId; lc->functions.Insert(str,str,false,_FILE_AND_LINE_); - localCallbacks.InsertAtIndex(lc,index,_FILE_AND_LINE_); + localCallbacks.insert(localCallbacks.begin() + index, lc); } } bool RPC4::UnregisterFunction(const char* uniqueID) { - RegisteredNonblockingFunction f; - return registeredNonblockingFunctions.Pop(f,uniqueID,_FILE_AND_LINE_); + std::unordered_map::iterator it = + registeredNonblockingFunctions.find(uniqueID); + if (it == registeredNonblockingFunctions.end()) + return false; + registeredNonblockingFunctions.erase(it); + return true; } bool RPC4::UnregisterBlockingFunction(const char* uniqueID) { - RegisteredBlockingFunction f; - return registeredBlockingFunctions.Pop(f,uniqueID,_FILE_AND_LINE_); + std::unordered_map::iterator it = + registeredBlockingFunctions.find(uniqueID); + if (it == registeredBlockingFunctions.end()) + return false; + registeredBlockingFunctions.erase(it); + return true; } bool RPC4::UnregisterLocalCallback(const char* uniqueID, MessageID messageId) { @@ -242,7 +263,7 @@ bool RPC4::UnregisterLocalCallback(const char* uniqueID, MessageID messageId) LocalCallback *lc; MafiaNet::RakString str; str=uniqueID; - index = localCallbacks.GetIndexFromKey(messageId,&objectExists); + index = IndexForMessageId(messageId,&objectExists); if (objectExists) { lc = localCallbacks[index]; @@ -253,7 +274,7 @@ bool RPC4::UnregisterLocalCallback(const char* uniqueID, MessageID messageId) if (lc->functions.Size()==0) { MafiaNet::OP_DELETE(lc,_FILE_AND_LINE_); - localCallbacks.RemoveAtIndex(index); + localCallbacks.erase(localCallbacks.begin() + index); } // The documented contract is "true if the combination of uniqueID and messageId was // in use, and hence removed". The return used to sit inside the branch above, so a @@ -281,9 +302,9 @@ void RPC4::CallLoopback( const char* uniqueID, MafiaNet::BitStream * bitStream ) { Packet *p=0; - DataStructures::HashIndex skhi = registeredNonblockingFunctions.GetIndexOf(uniqueID); + const bool registered = registeredNonblockingFunctions.find(uniqueID) != registeredNonblockingFunctions.end(); - if (skhi.IsInvalid()==true) + if (registered==false) { if (rakPeerInterface) p=AllocatePacketUnified(sizeof(MessageID)+sizeof(unsigned char)+(unsigned int) strlen(uniqueID)+1); @@ -381,7 +402,10 @@ bool RPC4::CallBlocking( const char* uniqueID, MafiaNet::BitStream * bitStream, blockingReturnValue.Reset(); gotBlockingReturnValue=false; Packet *packet; - DataStructures::Queue packetQueue; + // DataStructures::Queue was used here as a stack: PushAtHead(p,0) pushed to the front and + // Pop() took from the front, so held packets are handed back in reverse arrival order. + // std::deque with push_front/pop_front keeps that exactly. + std::deque packetQueue; while (gotBlockingReturnValue==false) { // TODO - block, filter until gotBlockingReturnValue==true or ID_CONNECTION_LOST or ID_DISCONNECTION_NOTIFICXATION or ID_RPC_REMOTE_ERROR/RPC_ERROR_FUNCTION_NOT_REGISTERED @@ -399,8 +423,12 @@ bool RPC4::CallBlocking( const char* uniqueID, MafiaNet::BitStream * bitStream, { // Push back to head in reverse order rakPeerInterface->PushBackPacket(packet,true); - while (packetQueue.Size()) - rakPeerInterface->PushBackPacket(packetQueue.Pop(),true); + while (packetQueue.empty()==false) + { + Packet *held = packetQueue.front(); + packetQueue.pop_front(); + rakPeerInterface->PushBackPacket(held,true); + } return false; } else if (packet->data[0]==ID_RPC_REMOTE_ERROR && packet->data[1]==RPC_ERROR_FUNCTION_NOT_REGISTERED) @@ -413,18 +441,22 @@ bool RPC4::CallBlocking( const char* uniqueID, MafiaNet::BitStream * bitStream, { // Push back to head in reverse order rakPeerInterface->PushBackPacket(packet,true); - while (packetQueue.Size()) - rakPeerInterface->PushBackPacket(packetQueue.Pop(),true); + while (packetQueue.empty()==false) + { + Packet *held = packetQueue.front(); + packetQueue.pop_front(); + rakPeerInterface->PushBackPacket(held,true); + } return false; } else { - packetQueue.PushAtHead(packet,0,_FILE_AND_LINE_); + packetQueue.push_front(packet); } } else { - packetQueue.PushAtHead(packet,0,_FILE_AND_LINE_); + packetQueue.push_front(packet); } } } @@ -557,8 +589,9 @@ PluginReceiveResult RPC4::OnReceive(Packet *packet) bsIn.Read(isBlocking); if (isBlocking==false) { - DataStructures::HashIndex skhi = registeredNonblockingFunctions.GetIndexOf(functionName.C_String()); - if (skhi.IsInvalid()) + std::unordered_map::const_iterator + fnIt = registeredNonblockingFunctions.find(functionName); + if (fnIt == registeredNonblockingFunctions.end()) { MafiaNet::BitStream bsOut; bsOut.Write((unsigned char) ID_RPC_REMOTE_ERROR); @@ -568,15 +601,15 @@ PluginReceiveResult RPC4::OnReceive(Packet *packet) return RR_STOP_PROCESSING_AND_DEALLOCATE; } - RegisteredNonblockingFunction rnf; - rnf = registeredNonblockingFunctions.ItemAtIndex(skhi); + RegisteredNonblockingFunction rnf = fnIt->second; bsIn.AlignReadToByteBoundary(); rnf.functionPointer(&bsIn,packet,rnf.context); } else { - DataStructures::HashIndex skhi = registeredBlockingFunctions.GetIndexOf(functionName.C_String()); - if (skhi.IsInvalid()) + std::unordered_map::const_iterator + fnIt = registeredBlockingFunctions.find(functionName); + if (fnIt == registeredBlockingFunctions.end()) { MafiaNet::BitStream bsOut; bsOut.Write((unsigned char) ID_RPC_REMOTE_ERROR); @@ -587,7 +620,7 @@ PluginReceiveResult RPC4::OnReceive(Packet *packet) } RegisteredBlockingFunction rbf; - rbf = registeredBlockingFunctions.ItemAtIndex(skhi); + rbf = fnIt->second; MafiaNet::BitStream returnData; bsIn.AlignReadToByteBoundary(); rbf.functionPointer(&bsIn, &returnData, packet, rbf.context); @@ -625,7 +658,7 @@ PluginReceiveResult RPC4::OnReceive(Packet *packet) bool objectExists; unsigned int index, index2; - index = localCallbacks.GetIndexFromKey(packet->data[0],&objectExists); + index = IndexForMessageId(packet->data[0],&objectExists); if (objectExists) { LocalCallback *lc; @@ -634,11 +667,11 @@ PluginReceiveResult RPC4::OnReceive(Packet *packet) { MafiaNet::BitStream bsIn(packet->data, packet->length, false); - DataStructures::HashIndex skhi = registeredNonblockingFunctions.GetIndexOf(lc->functions[index2].C_String()); - if (skhi.IsInvalid()==false) + std::unordered_map::const_iterator + fnIt = registeredNonblockingFunctions.find(lc->functions[index2]); + if (fnIt != registeredNonblockingFunctions.end()) { - RegisteredNonblockingFunction rnf; - rnf = registeredNonblockingFunctions.ItemAtIndex(skhi); + RegisteredNonblockingFunction rnf = fnIt->second; bsIn.AlignReadToByteBoundary(); rnf.functionPointer(&bsIn,packet,rnf.context); } From 28cef6e27d1834089ca0c813c103b712ad21a033 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:48:19 +0200 Subject: [PATCH 05/26] refactor(replicamanager3): move the protected working lists to std containers Part of #60, stage 2. Everything changed here is protected; no public declaration moves. - worldsList: DataStructures::List -> std::vector. RemoveWorld's RemoveAtIndexFast is reproduced explicitly as a swap-remove, not an erase: GetWorldIdAtIndex documents that "worlds will not necessarily be in the order added with AddWorld()", so the swap is a publicly observable contract and an erase would renumber every later world. - constructedReplicaList: OrderedList -> std::vector kept sorted by Replica3::referenceIndex, with IndexForReplica (lower_bound) and InsertConstructedReplica replacing GetIndexFromKey and Insert. InsertConstructedReplica keeps the old contract exactly: refuse a duplicate, assert, return (unsigned)-1. - queryToConstruct/Serialize/DestructReplicaList: List -> std::vector. - downloadGroup: DataStructures::Queue -> std::deque. Adds 6 tests. Two of them exist because the migration was not covered at all: - RemoveWorldSwapsTheLastWorldIntoTheHole pins the documented swap-remove; turning it into an erase fails it. - EveryConstructedReplicaIsFoundWhateverTheConstructionOrder pins that constructedReplicaList stays sorted. This was a real hole: appending instead of inserting in place passed all 304 unit tests, because every existing test constructs at most one replica and one element is trivially sorted. The test constructs 16 in an interleaved order and looks each one up through the public HasReplicaConstructed; it fails against the appending version. Left as stage 3 work: constructedReplicasCulled and destroyedReplicasCulled stay DataStructures::List because they are handed straight to the public virtuals QueryReplicaList and SendConstruction, and converting at that boundary would copy both lists every tick on the serialize path. RM3World::connectionList and userReplicaList are public members of a public struct. --- Source/include/mafianet/replica_manager3.h | 33 +++- Source/src/replica_manager3.cpp | 185 +++++++++++--------- Tests/Unit/ReplicaManager3WorldTests.cpp | 188 +++++++++++++++++++++ 3 files changed, 323 insertions(+), 83 deletions(-) diff --git a/Source/include/mafianet/replica_manager3.h b/Source/include/mafianet/replica_manager3.h index 3254f4e4f..8d6ea7f12 100644 --- a/Source/include/mafianet/replica_manager3.h +++ b/Source/include/mafianet/replica_manager3.h @@ -32,6 +32,8 @@ #include "mafianet/network_id_object.h" #include "mafianet/ds_ordered_list.h" #include "mafianet/ds_queue.h" +#include +#include #include "mafianet/simple_mutex.h" #include "mafianet/virtual_world.h" @@ -376,8 +378,10 @@ class RAK_DLL_EXPORT ReplicaManager3 : public PluginInterface2 // For O(1) lookup. One slot per WorldId value: an id read off the wire indexes it before it is checked. RM3World *worldsArray[256]; - // For fast traversal - DataStructures::List worldsList; + // For fast traversal. RemoveWorld swaps the last element into the hole rather than + // shifting -- that is documented on GetWorldIdAtIndex and publicly observable, so it is + // reproduced explicitly rather than with erase(). + std::vector worldsList; private: // #med - reconsider visibility here --- should be properly encapsulated so to not allow access to worldsList by derived classes (which could bypass the mutex) // mutex to ensure thread safe access to worldsList member @@ -644,6 +648,15 @@ class RAK_DLL_EXPORT Connection_RM3 static int Replica3LSRComp( Replica3 * const &replica3, LastSerializationResult * const &data ); + // Position of replica3 in constructedReplicaList, or of where it would be inserted; + // *found reports which. Replaces OrderedList::GetIndexFromKey. + unsigned int IndexForReplica(Replica3 *replica3, bool *found) const; + + // Sorted insert into constructedReplicaList. Returns the index, or (unsigned)-1 if the + // replica is already present -- the same contract as the OrderedList::Insert it replaces, + // which refused duplicates and asserted on them. + unsigned int InsertConstructedReplica(LastSerializationResult *lsr); + // Internal void ClearDownloadGroup(RakPeerInterface *rakPeerInterface); protected: @@ -717,27 +730,33 @@ class RAK_DLL_EXPORT Connection_RM3 // The list of objects that our local system and this remote system both have // Either we sent this object to them, or they sent this object to us // A given Replica can be either in queryToConstructReplicaList or constructedReplicaList but not both at the same time - DataStructures::OrderedList constructedReplicaList; + // Kept sorted ascending by Replica3::referenceIndex and searched with std::lower_bound, + // matching the OrderedList it replaces. Lookup runs on the serialize path for every + // replica every tick, so the ordering is kept rather than traded for a linear scan. + std::vector constructedReplicaList; // Objects that we have, but this system does not, and we will query each tick to see if it should be sent to them // If we do send it to them, the replica is moved to constructedReplicaList // A given Replica can be either in queryToConstructReplicaList or constructedReplicaList but not both at the same time - DataStructures::List queryToConstructReplicaList; + std::vector queryToConstructReplicaList; // Objects that this system has constructed are added at the same time to queryToSerializeReplicaList // This list is used to serialize all objects that this system has to this connection - DataStructures::List queryToSerializeReplicaList; + std::vector queryToSerializeReplicaList; // Objects that are constructed on this system are also queried if they should be destroyed to this system - DataStructures::List queryToDestructReplicaList; + std::vector queryToDestructReplicaList; // Working lists + // Handed straight to the public virtuals QueryReplicaList and SendConstruction, which take + // DataStructures::List&. Left as-is deliberately: converting here would copy both lists + // every tick on the serialize path. Stage 3 moves them with those signatures. DataStructures::List constructedReplicasCulled, destroyedReplicasCulled; // This is used if QueryGroupDownloadMessages() returns true when ID_REPLICA_MANAGER_DOWNLOAD_STARTED arrives // Packets will be gathered and not returned until ID_REPLICA_MANAGER_DOWNLOAD_COMPLETE arrives bool groupConstructionAndSerialize; - DataStructures::Queue downloadGroup; + std::deque downloadGroup; // Stores if we got download complete for this connection bool gotDownloadComplete; diff --git a/Source/src/replica_manager3.cpp b/Source/src/replica_manager3.cpp index fe72deaa3..5248b6b58 100644 --- a/Source/src/replica_manager3.cpp +++ b/Source/src/replica_manager3.cpp @@ -17,6 +17,7 @@ #if _RAKNET_SUPPORT_ReplicaManager3==1 #include "mafianet/replica_manager3.h" +#include #include "mafianet/virtual_world_replica3.h" #include "mafianet/get_time.h" #include "mafianet/message_identifiers.h" @@ -41,6 +42,35 @@ bool PRO::operator!=( const PRO& right ) const // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +unsigned int Connection_RM3::IndexForReplica(Replica3 *replica3, bool *found) const +{ + // constructedReplicaList is sorted ascending by Replica3::referenceIndex, the same key + // Replica3LSRComp orders on. + std::vector::const_iterator it = std::lower_bound( + constructedReplicaList.begin(), constructedReplicaList.end(), replica3, + [](LastSerializationResult * const &candidate, Replica3 *key) + { + return candidate->replica->referenceIndex < key->referenceIndex; + }); + unsigned int idx = (unsigned int) (it - constructedReplicaList.begin()); + *found = (it != constructedReplicaList.end() && + (*it)->replica->referenceIndex == replica3->referenceIndex); + return idx; +} +unsigned int Connection_RM3::InsertConstructedReplica(LastSerializationResult *lsr) +{ + bool found; + unsigned int idx = IndexForReplica(lsr->replica, &found); + if (found) + { + // The OrderedList::Insert this replaces was called with assertOnDuplicate=true and + // returned (unsigned)-1 without inserting. + RakAssert(0 && "Replica already in constructedReplicaList"); + return (unsigned int) -1; + } + constructedReplicaList.insert(constructedReplicaList.begin() + idx, lsr); + return idx; +} int Connection_RM3::Replica3LSRComp( Replica3 * const &replica3, LastSerializationResult * const &data ) { /* @@ -106,7 +136,7 @@ ReplicaManager3::~ReplicaManager3() if (autoDestroyConnections) { m_WorldListMutex.Lock(); - for (unsigned int i=0; i < worldsList.Size(); i++) + for (unsigned int i=0; i < worldsList.size(); i++) { RakAssert(worldsList[i]->connectionList.Size()==0); } @@ -244,7 +274,7 @@ MafiaNet::Connection_RM3 * ReplicaManager3::PopConnection(unsigned int index, Wo } else if (action==RM3AOPC_DO_NOTHING) { - for (unsigned int index3 = 0; index3 < connection->queryToSerializeReplicaList.Size(); index3++) + for (unsigned int index3 = 0; index3 < connection->queryToSerializeReplicaList.size(); index3++) { LastSerializationResult *lsr = connection->queryToSerializeReplicaList[index3]; lsr->whenLastSerialized=0; @@ -605,7 +635,7 @@ bool ReplicaManager3::GetAllConnectionDownloadsCompleted(WorldId worldId) const void ReplicaManager3::Clear(bool deleteWorlds) { m_WorldListMutex.Lock(); - for (unsigned int i=0; i < worldsList.Size(); i++) + for (unsigned int i=0; i < worldsList.size(); i++) { worldsList[i]->Clear(this); if (deleteWorlds) @@ -615,7 +645,7 @@ void ReplicaManager3::Clear(bool deleteWorlds) } } if (deleteWorlds) - worldsList.Clear(false, _FILE_AND_LINE_); + worldsList.clear(); m_WorldListMutex.Unlock(); } @@ -668,7 +698,7 @@ void ReplicaManager3::AddWorld(WorldId worldId) newWorld->worldId=worldId; worldsArray[worldId]=newWorld; m_WorldListMutex.Lock(); - worldsList.Push(newWorld,_FILE_AND_LINE_); + worldsList.push_back(newWorld); m_WorldListMutex.Unlock(); } @@ -677,12 +707,15 @@ void ReplicaManager3::AddWorld(WorldId worldId) void ReplicaManager3::RemoveWorld(WorldId worldId) { RakAssert(worldsArray[worldId]!=0 && "World not in use"); - for (unsigned int i=0; i < worldsList.Size(); i++) + for (unsigned int i=0; i < worldsList.size(); i++) { if (worldsList[i]==worldsArray[worldId]) { MafiaNet::OP_DELETE(worldsList[i],_FILE_AND_LINE_); - worldsList.RemoveAtIndexFast(i); + // Swap-remove, not erase: GetWorldIdAtIndex documents that worlds are not kept in the + // order they were added, and an erase here would renumber every later world. + worldsList[i] = worldsList.back(); + worldsList.pop_back(); break; } } @@ -694,7 +727,7 @@ void ReplicaManager3::RemoveWorld(WorldId worldId) WorldId ReplicaManager3::GetWorldIdAtIndex(unsigned int index) { - RakAssert(index < worldsList.Size()); + RakAssert(index < worldsList.size()); return worldsList[index]->worldId; } @@ -702,7 +735,7 @@ WorldId ReplicaManager3::GetWorldIdAtIndex(unsigned int index) unsigned int ReplicaManager3::GetWorldCount(void) const { - return worldsList.Size(); + return worldsList.size(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -818,7 +851,7 @@ void Connection_RM3::AutoConstructByQuery(ReplicaManager3 *replicaManager3, Worl if (curConstructionMode==QUERY_REPLICA_FOR_CONSTRUCTION || curConstructionMode==QUERY_REPLICA_FOR_CONSTRUCTION_AND_DESTRUCTION) { - while (index < queryToConstructReplicaList.Size()) + while (index < queryToConstructReplicaList.size()) { lsr=queryToConstructReplicaList[index]; constructionState=lsr->replica->QueryConstruction(this, replicaManager3); @@ -897,7 +930,7 @@ void Connection_RM3::AutoConstructByQuery(ReplicaManager3 *replicaManager3, Worl { RM3DestructionState destructionState; index=0; - while (index < queryToDestructReplicaList.Size()) + while (index < queryToDestructReplicaList.size()) { lsr=queryToDestructReplicaList[index]; destructionState=lsr->replica->QueryDestruction(this, replicaManager3); @@ -933,17 +966,17 @@ void Connection_RM3::AutoConstructByQuery(ReplicaManager3 *replicaManager3, Worl { exists=false; bool objectExists; - idx1=constructedReplicaList.GetIndexFromKey(destroyedReplicasCulled[idx2], &objectExists); + idx1=IndexForReplica(destroyedReplicasCulled[idx2], &objectExists); if (objectExists) { - constructedReplicaList.RemoveAtIndex(idx1); + constructedReplicaList.erase(constructedReplicaList.begin() + idx1); unsigned int j; - for (j=0; j < queryToSerializeReplicaList.Size(); j++) + for (j=0; j < queryToSerializeReplicaList.size(); j++) { if (queryToSerializeReplicaList[j]->replica==destroyedReplicasCulled[idx2] ) { - queryToSerializeReplicaList.RemoveAtIndex(j); + queryToSerializeReplicaList.erase(queryToSerializeReplicaList.begin() + j); break; } } @@ -962,7 +995,7 @@ void ReplicaManager3::Update(void) MafiaNet::Time time = MafiaNet::GetTime(); m_WorldListMutex.Lock(); - for (index3=0; index3 < worldsList.Size(); index3++) + for (index3=0; index3 < worldsList.size(); index3++) { world = worldsList[index3]; worldId = world->worldId; @@ -977,7 +1010,7 @@ void ReplicaManager3::Update(void) if (time - lastAutoSerializeOccurance >= autoSerializeInterval) { - for (index3=0; index3 < worldsList.Size(); index3++) + for (index3=0; index3 < worldsList.size(); index3++) { world = worldsList[index3]; worldId = world->worldId; @@ -1011,7 +1044,7 @@ void ReplicaManager3::Update(void) { // Update replica->lsr so we can lookup in the next block // lsr is per connection / per replica - while (index2 < connection->queryToSerializeReplicaList.Size()) + while (index2 < connection->queryToSerializeReplicaList.size()) { connection->queryToSerializeReplicaList[index2]->replica->lsr=connection->queryToSerializeReplicaList[index2]; index2++; @@ -1034,7 +1067,7 @@ void ReplicaManager3::Update(void) } else { - while (index2 < connection->queryToSerializeReplicaList.Size()) + while (index2 < connection->queryToSerializeReplicaList.size()) { lsr=connection->queryToSerializeReplicaList[index2]; @@ -1098,7 +1131,7 @@ void ReplicaManager3::OnRakPeerShutdown(void) RM3World *world; unsigned int index3; m_WorldListMutex.Lock(); - for (index3=0; index3 < worldsList.Size(); index3++) + for (index3=0; index3 < worldsList.size(); index3++) { world = worldsList[index3]; @@ -1138,7 +1171,7 @@ PluginReceiveResult ReplicaManager3::OnConstruction(Packet *packet, unsigned cha } if (connection->groupConstructionAndSerialize) { - connection->downloadGroup.Push(packet, __FILE__, __LINE__); + connection->downloadGroup.push_back(packet); return RR_STOP_PROCESSING; } @@ -1339,7 +1372,7 @@ PluginReceiveResult ReplicaManager3::OnSerialize(Packet *packet, unsigned char * return RR_CONTINUE_PROCESSING; if (connection->groupConstructionAndSerialize) { - connection->downloadGroup.Push(packet, __FILE__, __LINE__); + connection->downloadGroup.push_back(packet); return RR_STOP_PROCESSING; } @@ -1389,8 +1422,8 @@ PluginReceiveResult ReplicaManager3::OnDownloadStarted(Packet *packet, unsigned { // These messages will be held by the plugin and returned when the download is complete connection->groupConstructionAndSerialize=true; - RakAssert(connection->downloadGroup.Size()==0); - connection->downloadGroup.Push(packet, __FILE__, __LINE__); + RakAssert(connection->downloadGroup.size()==0); + connection->downloadGroup.push_back(packet); return RR_STOP_PROCESSING; } @@ -1409,16 +1442,16 @@ PluginReceiveResult ReplicaManager3::OnDownloadComplete(Packet *packet, unsigned if (connection==0) return RR_CONTINUE_PROCESSING; - if (connection->groupConstructionAndSerialize==true && connection->downloadGroup.Size()>0) + if (connection->groupConstructionAndSerialize==true && connection->downloadGroup.size()>0) { // Push back buffered packets in front of this one unsigned int i; - for (i=0; i < connection->downloadGroup.Size(); i++) + for (i=0; i < connection->downloadGroup.size(); i++) rakPeerInterface->PushBackPacket(connection->downloadGroup[i],false); // Push this one to be last too. It will be processed again, but the second time // groupConstructionAndSerialize will be false and downloadGroup will be empty, so it will go past this block - connection->downloadGroup.Clear(__FILE__,__LINE__); + connection->downloadGroup.clear(); rakPeerInterface->PushBackPacket(packet,false); return RR_STOP_PROCESSING; @@ -1553,9 +1586,9 @@ Connection_RM3::Connection_RM3(const SystemAddress &_systemAddress, RakNetGUID _ Connection_RM3::~Connection_RM3() { unsigned int i; - for (i=0; i < constructedReplicaList.Size(); i++) + for (i=0; i < constructedReplicaList.size(); i++) MafiaNet::OP_DELETE(constructedReplicaList[i], _FILE_AND_LINE_); - for (i=0; i < queryToConstructReplicaList.Size(); i++) + for (i=0; i < queryToConstructReplicaList.size(); i++) MafiaNet::OP_DELETE(queryToConstructReplicaList[i], _FILE_AND_LINE_); } @@ -1564,7 +1597,7 @@ Connection_RM3::~Connection_RM3() void Connection_RM3::GetConstructedReplicas(DataStructures::List &objectsTheyDoHave) { objectsTheyDoHave.Clear(true,_FILE_AND_LINE_); - for (unsigned int idx=0; idx < constructedReplicaList.Size(); idx++) + for (unsigned int idx=0; idx < constructedReplicaList.size(); idx++) { objectsTheyDoHave.Push(constructedReplicaList[idx]->replica, _FILE_AND_LINE_ ); } @@ -1575,7 +1608,7 @@ void Connection_RM3::GetConstructedReplicas(DataStructures::List &obj bool Connection_RM3::HasReplicaConstructed(MafiaNet::Replica3 *replica) { bool objectExists; - constructedReplicaList.GetIndexFromKey(replica, &objectExists); + IndexForReplica(replica, &objectExists); return objectExists; } @@ -1597,9 +1630,9 @@ void Connection_RM3::SendSerializeHeader(MafiaNet::Replica3 *replica, MafiaNet:: void Connection_RM3::ClearDownloadGroup(RakPeerInterface *rakPeerInterface) { unsigned int i; - for (i=0; i < downloadGroup.Size(); i++) + for (i=0; i < downloadGroup.size(); i++) rakPeerInterface->DeallocatePacket(downloadGroup[i]); - downloadGroup.Clear(__FILE__,__LINE__); + downloadGroup.clear(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- SendSerializeIfChangedResult Connection_RM3::SendSerialize(MafiaNet::Replica3 *replica, bool indicesToSend[RM3_NUM_OUTPUT_BITSTREAM_CHANNELS], MafiaNet::BitStream serializationData[RM3_NUM_OUTPUT_BITSTREAM_CHANNELS], MafiaNet::Time timestamp, PRO sendParameters[RM3_NUM_OUTPUT_BITSTREAM_CHANNELS], RakPeerInterface *rakPeer, unsigned char worldId, MafiaNet::Time curTime) @@ -1829,7 +1862,7 @@ void Connection_RM3::OnLocalReference(Replica3* replica3, ReplicaManager3 *repli (void) curConstructionMode; #ifdef _DEBUG - for (unsigned int i=0; i < queryToConstructReplicaList.Size(); i++) + for (unsigned int i=0; i < queryToConstructReplicaList.size(); i++) { if (queryToConstructReplicaList[i]->replica==replica3) { @@ -1845,7 +1878,7 @@ void Connection_RM3::OnLocalReference(Replica3* replica3, ReplicaManager3 *repli LastSerializationResult* lsr= MafiaNet::OP_NEW(_FILE_AND_LINE_); lsr->replica=replica3; - queryToConstructReplicaList.Push(lsr,_FILE_AND_LINE_); + queryToConstructReplicaList.push_back(lsr); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1861,39 +1894,39 @@ void Connection_RM3::OnDereference(Replica3* replica3, ReplicaManager3 *replicaM unsigned int idx; bool objectExists; - idx=constructedReplicaList.GetIndexFromKey(replica3, &objectExists); + idx=IndexForReplica(replica3, &objectExists); if (objectExists) { lsr=constructedReplicaList[idx]; - constructedReplicaList.RemoveAtIndex(idx); + constructedReplicaList.erase(constructedReplicaList.begin() + idx); } - for (idx=0; idx < queryToConstructReplicaList.Size(); idx++) + for (idx=0; idx < queryToConstructReplicaList.size(); idx++) { if (queryToConstructReplicaList[idx]->replica==replica3) { lsr=queryToConstructReplicaList[idx]; - queryToConstructReplicaList.RemoveAtIndex(idx); + queryToConstructReplicaList.erase(queryToConstructReplicaList.begin() + idx); break; } } - for (idx=0; idx < queryToSerializeReplicaList.Size(); idx++) + for (idx=0; idx < queryToSerializeReplicaList.size(); idx++) { if (queryToSerializeReplicaList[idx]->replica==replica3) { lsr=queryToSerializeReplicaList[idx]; - queryToSerializeReplicaList.RemoveAtIndex(idx); + queryToSerializeReplicaList.erase(queryToSerializeReplicaList.begin() + idx); break; } } - for (idx=0; idx < queryToDestructReplicaList.Size(); idx++) + for (idx=0; idx < queryToDestructReplicaList.size(); idx++) { if (queryToDestructReplicaList[idx]->replica==replica3) { lsr=queryToDestructReplicaList[idx]; - queryToDestructReplicaList.RemoveAtIndex(idx); + queryToDestructReplicaList.erase(queryToDestructReplicaList.begin() + idx); break; } } @@ -1920,22 +1953,22 @@ void Connection_RM3::OnDownloadFromThisSystem(Replica3* replica3, ReplicaManager if (curConstructionMode==QUERY_REPLICA_FOR_CONSTRUCTION || curConstructionMode==QUERY_REPLICA_FOR_CONSTRUCTION_AND_DESTRUCTION) { unsigned int j; - for (j=0; j < queryToConstructReplicaList.Size(); j++) + for (j=0; j < queryToConstructReplicaList.size(); j++) { if (queryToConstructReplicaList[j]->replica==replica3 ) { - queryToConstructReplicaList.RemoveAtIndex(j); + queryToConstructReplicaList.erase(queryToConstructReplicaList.begin() + j); break; } } - queryToDestructReplicaList.Push(lsr,_FILE_AND_LINE_); + queryToDestructReplicaList.push_back(lsr); } - if (constructedReplicaList.Insert(lsr->replica, lsr, true, _FILE_AND_LINE_) != (unsigned) -1) + if (InsertConstructedReplica(lsr) != (unsigned) -1) { //assert(queryToSerializeReplicaList.GetIndexOf(replica3)==(unsigned int)-1); - queryToSerializeReplicaList.Push(lsr,_FILE_AND_LINE_); + queryToSerializeReplicaList.push_back(lsr); } ValidateLists(replicaManager); @@ -1949,7 +1982,7 @@ void Connection_RM3::OnDownloadFromOtherSystem(Replica3* replica3, ReplicaManage if (curConstructionMode==QUERY_REPLICA_FOR_CONSTRUCTION || curConstructionMode==QUERY_REPLICA_FOR_CONSTRUCTION_AND_DESTRUCTION) { unsigned int j; - for (j=0; j < queryToConstructReplicaList.Size(); j++) + for (j=0; j < queryToConstructReplicaList.size(); j++) { if (queryToConstructReplicaList[j]->replica==replica3 ) { @@ -1971,7 +2004,7 @@ void Connection_RM3::OnNeverConstruct(unsigned int queryToConstructIdx, ReplicaM ValidateLists(replicaManager); LastSerializationResult* lsr = queryToConstructReplicaList[queryToConstructIdx]; - queryToConstructReplicaList.RemoveAtIndex(queryToConstructIdx); + queryToConstructReplicaList.erase(queryToConstructReplicaList.begin() + queryToConstructIdx); MafiaNet::OP_DELETE(lsr,_FILE_AND_LINE_); ValidateLists(replicaManager); } @@ -1986,13 +2019,13 @@ void Connection_RM3::OnConstructToThisConnection(unsigned int queryToConstructId ValidateLists(replicaManager); LastSerializationResult* lsr = queryToConstructReplicaList[queryToConstructIdx]; - queryToConstructReplicaList.RemoveAtIndex(queryToConstructIdx); + queryToConstructReplicaList.erase(queryToConstructReplicaList.begin() + queryToConstructIdx); //assert(constructedReplicaList.GetIndexOf(lsr->replica)==(unsigned int)-1); - constructedReplicaList.Insert(lsr->replica,lsr,true,_FILE_AND_LINE_); + InsertConstructedReplica(lsr); //assert(queryToDestructReplicaList.GetIndexOf(lsr->replica)==(unsigned int)-1); - queryToDestructReplicaList.Push(lsr,_FILE_AND_LINE_); + queryToDestructReplicaList.push_back(lsr); //assert(queryToSerializeReplicaList.GetIndexOf(lsr->replica)==(unsigned int)-1); - queryToSerializeReplicaList.Push(lsr,_FILE_AND_LINE_); + queryToSerializeReplicaList.push_back(lsr); ValidateLists(replicaManager); } @@ -2006,8 +2039,8 @@ void Connection_RM3::OnConstructToThisConnection(Replica3 *replica, ReplicaManag LastSerializationResult* lsr= MafiaNet::OP_NEW(_FILE_AND_LINE_); lsr->replica=replica; - constructedReplicaList.Insert(replica,lsr,true,_FILE_AND_LINE_); - queryToSerializeReplicaList.Push(lsr,_FILE_AND_LINE_); + InsertConstructedReplica(lsr); + queryToSerializeReplicaList.push_back(lsr); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2017,11 +2050,11 @@ void Connection_RM3::OnNeverSerialize(LastSerializationResult *lsr, ReplicaManag ValidateLists(replicaManager); unsigned int j; - for (j=0; j < queryToSerializeReplicaList.Size(); j++) + for (j=0; j < queryToSerializeReplicaList.size(); j++) { if (queryToSerializeReplicaList[j]==lsr ) { - queryToSerializeReplicaList.RemoveAtIndex(j); + queryToSerializeReplicaList.erase(queryToSerializeReplicaList.begin() + j); break; } } @@ -2039,13 +2072,13 @@ void Connection_RM3::OnReplicaAlreadyExists(unsigned int queryToConstructIdx, Re ValidateLists(replicaManager); LastSerializationResult* lsr = queryToConstructReplicaList[queryToConstructIdx]; - queryToConstructReplicaList.RemoveAtIndex(queryToConstructIdx); + queryToConstructReplicaList.erase(queryToConstructReplicaList.begin() + queryToConstructIdx); //assert(constructedReplicaList.GetIndexOf(lsr->replica)==(unsigned int)-1); - constructedReplicaList.Insert(lsr->replica,lsr,true,_FILE_AND_LINE_); + InsertConstructedReplica(lsr); //assert(queryToDestructReplicaList.GetIndexOf(lsr->replica)==(unsigned int)-1); - queryToDestructReplicaList.Push(lsr,_FILE_AND_LINE_); + queryToDestructReplicaList.push_back(lsr); //assert(queryToSerializeReplicaList.GetIndexOf(lsr->replica)==(unsigned int)-1); - queryToSerializeReplicaList.Push(lsr,_FILE_AND_LINE_); + queryToSerializeReplicaList.push_back(lsr); ValidateLists(replicaManager); } @@ -2059,7 +2092,7 @@ void Connection_RM3::OnDownloadExisting(Replica3* replica3, ReplicaManager3 *rep if (curConstructionMode==QUERY_REPLICA_FOR_CONSTRUCTION || curConstructionMode==QUERY_REPLICA_FOR_CONSTRUCTION_AND_DESTRUCTION) { unsigned int idx; - for (idx=0; idx < queryToConstructReplicaList.Size(); idx++) + for (idx=0; idx < queryToConstructReplicaList.size(); idx++) { if (queryToConstructReplicaList[idx]->replica==replica3) { @@ -2083,26 +2116,26 @@ void Connection_RM3::OnSendDestructionFromQuery(unsigned int queryToDestructIdx, ValidateLists(replicaManager); LastSerializationResult* lsr = queryToDestructReplicaList[queryToDestructIdx]; - queryToDestructReplicaList.RemoveAtIndex(queryToDestructIdx); + queryToDestructReplicaList.erase(queryToDestructReplicaList.begin() + queryToDestructIdx); unsigned int j; - for (j=0; j < queryToSerializeReplicaList.Size(); j++) + for (j=0; j < queryToSerializeReplicaList.size(); j++) { if (queryToSerializeReplicaList[j]->replica==lsr->replica ) { - queryToSerializeReplicaList.RemoveAtIndex(j); + queryToSerializeReplicaList.erase(queryToSerializeReplicaList.begin() + j); break; } } - for (j=0; j < constructedReplicaList.Size(); j++) + for (j=0; j < constructedReplicaList.size(); j++) { if (constructedReplicaList[j]->replica==lsr->replica ) { - constructedReplicaList.RemoveAtIndex(j); + constructedReplicaList.erase(constructedReplicaList.begin() + j); break; } } //assert(queryToConstructReplicaList.GetIndexOf(lsr->replica)==(unsigned int)-1); - queryToConstructReplicaList.Push(lsr,_FILE_AND_LINE_); + queryToConstructReplicaList.push_back(lsr); ValidateLists(replicaManager); } @@ -2111,7 +2144,7 @@ void Connection_RM3::OnSendDestructionFromQuery(unsigned int queryToDestructIdx, void Connection_RM3::OnDoNotQueryDestruction(unsigned int queryToDestructIdx, ReplicaManager3 *replicaManager) { ValidateLists(replicaManager); - queryToDestructReplicaList.RemoveAtIndex(queryToDestructIdx); + queryToDestructReplicaList.erase(queryToDestructReplicaList.begin() + queryToDestructIdx); ValidateLists(replicaManager); } @@ -2125,7 +2158,7 @@ void Connection_RM3::ValidateLists(ReplicaManager3 *replicaManager) const // Each object should exist only once in either constructedReplicaList or queryToConstructReplicaList // replicaPointer from LastSerializationResult should be same among all lists unsigned int idx, idx2; - for (idx=0; idx < constructedReplicaList.Size(); idx++) + for (idx=0; idx < constructedReplicaList.size(); idx++) { idx2=queryToConstructReplicaList.GetIndexOf(constructedReplicaList[idx]->replica); if (idx2!=(unsigned int)-1) @@ -2137,7 +2170,7 @@ void Connection_RM3::ValidateLists(ReplicaManager3 *replicaManager) const } } - for (idx=0; idx < queryToConstructReplicaList.Size(); idx++) + for (idx=0; idx < queryToConstructReplicaList.size(); idx++) { idx2=constructedReplicaList.GetIndexOf(queryToConstructReplicaList[idx]->replica); if (idx2!=(unsigned int)-1) @@ -2150,7 +2183,7 @@ void Connection_RM3::ValidateLists(ReplicaManager3 *replicaManager) const } LastSerializationResult *lsr, *lsr2; - for (idx=0; idx < constructedReplicaList.Size(); idx++) + for (idx=0; idx < constructedReplicaList.size(); idx++) { lsr=constructedReplicaList[idx]; @@ -2180,7 +2213,7 @@ void Connection_RM3::ValidateLists(ReplicaManager3 *replicaManager) const } } } - for (idx=0; idx < queryToConstructReplicaList.Size(); idx++) + for (idx=0; idx < queryToConstructReplicaList.size(); idx++) { lsr=queryToConstructReplicaList[idx]; @@ -2212,7 +2245,7 @@ void Connection_RM3::ValidateLists(ReplicaManager3 *replicaManager) const } // Verify pointer integrity - for (idx=0; idx < constructedReplicaList.Size(); idx++) + for (idx=0; idx < constructedReplicaList.size(); idx++) { if (constructedReplicaList[idx]->replica->replicaManager!=replicaManager) { @@ -2224,7 +2257,7 @@ void Connection_RM3::ValidateLists(ReplicaManager3 *replicaManager) const } // Verify pointer integrity - for (idx=0; idx < queryToConstructReplicaList.Size(); idx++) + for (idx=0; idx < queryToConstructReplicaList.size(); idx++) { if (queryToConstructReplicaList[idx]->replica->replicaManager!=replicaManager) { diff --git a/Tests/Unit/ReplicaManager3WorldTests.cpp b/Tests/Unit/ReplicaManager3WorldTests.cpp index 883243003..3683e2906 100644 --- a/Tests/Unit/ReplicaManager3WorldTests.cpp +++ b/Tests/Unit/ReplicaManager3WorldTests.cpp @@ -9,6 +9,11 @@ #include "mafianet/replica_manager3.h" +#include + +#include "mafianet/network_id_manager.h" +#include "mafianet/peer_interface.h" + using namespace MafiaNet; namespace @@ -45,3 +50,186 @@ TEST(ReplicaManager3Worlds, HighestWorldIdIsUsable) manager.RemoveWorld(255); EXPECT_EQ(manager.GetWorldCount(), 1u); } + +// RemoveWorld documents its own ordering: "Worlds will not necessarily be in the order added with +// AddWorld(). Edit RemoveWorld() changing RemoveAtIndexFast() to RemoveAtIndex() to preserve +// order." That swap-remove is a documented, publicly observable contract because +// GetWorldIdAtIndex exposes the index, so it has to survive the move off DataStructures::List +// (std migration stage 2, #60). A plain erase would shift instead of swapping and renumber +// every world after the removed one. +TEST(ReplicaManager3Worlds, RemoveWorldSwapsTheLastWorldIntoTheHole) +{ + WorldTestManager manager; + manager.AddWorld(1); + manager.AddWorld(2); + manager.AddWorld(3); + ASSERT_EQ(manager.GetWorldCount(), 4u); + ASSERT_EQ(manager.GetWorldIdAtIndex(0), static_cast(0)); + ASSERT_EQ(manager.GetWorldIdAtIndex(1), static_cast(1)); + ASSERT_EQ(manager.GetWorldIdAtIndex(2), static_cast(2)); + ASSERT_EQ(manager.GetWorldIdAtIndex(3), static_cast(3)); + + manager.RemoveWorld(1); // index 1, with index 3 being last + + ASSERT_EQ(manager.GetWorldCount(), 3u); + EXPECT_EQ(manager.GetWorldIdAtIndex(0), static_cast(0)); + EXPECT_EQ(manager.GetWorldIdAtIndex(1), static_cast(3)) + << "the last world must be swapped into the hole, not shifted up"; + EXPECT_EQ(manager.GetWorldIdAtIndex(2), static_cast(2)); +} + +TEST(ReplicaManager3Worlds, RemovingTheLastWorldLeavesTheRestInPlace) +{ + WorldTestManager manager; + manager.AddWorld(1); + manager.AddWorld(2); + ASSERT_EQ(manager.GetWorldCount(), 3u); + + manager.RemoveWorld(2); // already last + + ASSERT_EQ(manager.GetWorldCount(), 2u); + EXPECT_EQ(manager.GetWorldIdAtIndex(0), static_cast(0)); + EXPECT_EQ(manager.GetWorldIdAtIndex(1), static_cast(1)); +} + +TEST(ReplicaManager3Worlds, WorldsCanBeAddedAgainAfterRemoval) +{ + WorldTestManager manager; + manager.AddWorld(7); + ASSERT_EQ(manager.GetWorldCount(), 2u); + manager.RemoveWorld(7); + ASSERT_EQ(manager.GetWorldCount(), 1u); + manager.AddWorld(7); + ASSERT_EQ(manager.GetWorldCount(), 2u); + EXPECT_EQ(manager.GetWorldIdAtIndex(1), static_cast(7)); +} + +TEST(ReplicaManager3Worlds, EveryAddedWorldIsReachableByIndex) +{ + WorldTestManager manager; + const WorldId ids[] = { 5, 9, 200, 255 }; + for (int i = 0; i < 4; ++i) + manager.AddWorld(ids[i]); + ASSERT_EQ(manager.GetWorldCount(), 5u); + + // Collect what the index exposes and compare as a set: the order is explicitly not promised, + // but membership is. + std::set seen; + for (unsigned i = 0; i < manager.GetWorldCount(); ++i) + seen.insert((int) manager.GetWorldIdAtIndex(i)); + std::set want; + want.insert(0); + for (int i = 0; i < 4; ++i) + want.insert((int) ids[i]); + EXPECT_EQ(seen, want); +} + +// constructedReplicaList is looked up by binary search on Replica3::referenceIndex, so the list +// has to stay sorted. Nothing covered that: with one replica any order is trivially sorted, and a +// migration that appends instead of inserting in place passed the whole unit suite. These +// construct several replicas in an order that is NOT the reference order, then assert every one +// is still found -- which fails the moment the list stops being sorted. +namespace +{ + class LookupTestReplica : public Replica3 + { + public: + void WriteAllocationID(Connection_RM3 *, MafiaNet::BitStream *) const {} + RM3ConstructionState QueryConstruction(Connection_RM3 *, ReplicaManager3 *) { return RM3CS_NEVER_CONSTRUCT; } + bool QueryRemoteConstruction(Connection_RM3 *) { return false; } + void SerializeConstruction(MafiaNet::BitStream *, Connection_RM3 *) {} + bool DeserializeConstruction(MafiaNet::BitStream *, Connection_RM3 *) { return false; } + void SerializeDestruction(MafiaNet::BitStream *, Connection_RM3 *) {} + bool DeserializeDestruction(MafiaNet::BitStream *, Connection_RM3 *) { return false; } + RM3ActionOnPopConnection QueryActionOnPopConnection(Connection_RM3 *) const { return RM3AOPC_DO_NOTHING; } + void DeallocReplica(Connection_RM3 *) {} + RM3QuerySerializationResult QuerySerialization(Connection_RM3 *) { return RM3QSR_DO_NOT_CALL_SERIALIZE; } + RM3SerializationResult Serialize(SerializeParameters *) { return RM3SR_DO_NOT_SERIALIZE; } + void Deserialize(DeserializeParameters *) {} + }; + + // QueryConstructionMode must be QUERY_CONNECTION_FOR_REPLICA_LIST for the direct + // OnConstructToThisConnection(Replica3*) entry point. + class LookupTestConnection : public Connection_RM3 + { + public: + LookupTestConnection() : Connection_RM3(UNASSIGNED_SYSTEM_ADDRESS, RakNetGUID(3)) {} + Replica3 *AllocReplica(MafiaNet::BitStream *, ReplicaManager3 *) { return 0; } + ConstructionMode QueryConstructionMode(void) const { return QUERY_CONNECTION_FOR_REPLICA_LIST; } + + // OnConstructToThisConnection is protected; a derived class is the sanctioned way in. + void Construct(Replica3 *replica, ReplicaManager3 *manager) + { + OnConstructToThisConnection(replica, manager); + } + }; +} + +TEST(ReplicaManager3ConstructedLookup, EveryConstructedReplicaIsFoundWhateverTheConstructionOrder) +{ + // Reference() dereferences rakPeerInterface, so the manager must be attached to a peer; + // the peer is never started and no traffic occurs. + RakPeerInterface *peer = RakPeerInterface::GetInstance(); + WorldTestManager manager; + NetworkIDManager ids; + manager.SetNetworkIDManager(&ids); + peer->AttachPlugin(&manager); + LookupTestConnection connection; + + const int count = 16; + LookupTestReplica replicas[count]; + + // Reference assigns ascending referenceIndex in this order. + for (int i = 0; i < count; ++i) + { + replicas[i].SetNetworkIDManager(&ids); + replicas[i].SetNetworkID((NetworkID) (100 + i)); + manager.Reference(&replicas[i]); + } + + // Construct in an interleaved order, so insertion order differs from reference order and an + // append-only list ends up unsorted. + for (int i = 0; i < count; i += 2) + connection.Construct(&replicas[i], &manager); + for (int i = 1; i < count; i += 2) + connection.Construct(&replicas[i], &manager); + + for (int i = 0; i < count; ++i) + EXPECT_TRUE(connection.HasReplicaConstructed(&replicas[i])) + << "replica " << i << " was constructed but cannot be found"; + + for (int i = 0; i < count; ++i) + manager.Dereference(&replicas[i]); + + peer->DetachPlugin(&manager); + RakPeerInterface::DestroyInstance(peer); +} + +TEST(ReplicaManager3ConstructedLookup, AnUnconstructedReplicaIsNotReported) +{ + RakPeerInterface *peer = RakPeerInterface::GetInstance(); + WorldTestManager manager; + NetworkIDManager ids; + manager.SetNetworkIDManager(&ids); + peer->AttachPlugin(&manager); + LookupTestConnection connection; + + LookupTestReplica a, b; + a.SetNetworkIDManager(&ids); + a.SetNetworkID(201); + b.SetNetworkIDManager(&ids); + b.SetNetworkID(202); + manager.Reference(&a); + manager.Reference(&b); + + connection.Construct(&a, &manager); + + EXPECT_TRUE(connection.HasReplicaConstructed(&a)); + EXPECT_FALSE(connection.HasReplicaConstructed(&b)); + + manager.Dereference(&a); + manager.Dereference(&b); + + peer->DetachPlugin(&manager); + RakPeerInterface::DestroyInstance(peer); +} From 9a91bea74e3c40f0a0270ec3e9a35f8bfc258487 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:50:21 +0200 Subject: [PATCH 06/26] test(teammanager): cover the world, team, member and participant registries TeamManager had no tests. It holds the most container references left in Source/ after RPC4 and ReplicaManager3 (39), and two of its orderings are published and differ from each other, which is what makes a mechanical swap risky in #60 stage 2: - TM_World::GetTeamByIndex / GetTeamIndex and the team-member equivalents publish the index of order-PRESERVING lists; DereferenceTeam and DereferenceTeamMember use RemoveAtIndex, so the remainder shifts down. - TeamManager::GetWorldAtIndex publishes the index of an order-NOT-preserving list; RemoveWorld uses RemoveAtIndexFast, swapping the last world into the hole. A migration that applied one idiom to both would renumber half of these while the NetworkID lookups kept answering correctly, so nothing else would notice. 12 cases, all passing against the current implementation: index order for teams and members, shift-down on removal, swap-into-hole on world removal, NetworkID lookup resolution and absence through teamsHash/teamMembersHash, 120 teams at once to spread hash buckets, and participant round-trip. --- Tests/Unit/TeamManagerRegistryTests.cpp | 272 ++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 Tests/Unit/TeamManagerRegistryTests.cpp diff --git a/Tests/Unit/TeamManagerRegistryTests.cpp b/Tests/Unit/TeamManagerRegistryTests.cpp new file mode 100644 index 000000000..891a7535c --- /dev/null +++ b/Tests/Unit/TeamManagerRegistryTests.cpp @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + */ + +#include + +#include +#include + +#include "mafianet/team_manager.h" +#include "mafianet/peer_interface.h" + +using namespace MafiaNet; + +/* +Description: +Covers the registries TeamManager keeps -- the world list, and per world the team list, team-member +list, participant list and the two NetworkID hash tables -- written before they move to the +standard library (std migration stage 2, #60). The subsystem had no tests at all. + +Two orderings here are publicly observable and differ from each other, which is what makes a +mechanical container swap risky: + + - TM_World::GetTeamByIndex / GetTeamIndex and the team-member equivalents publish the index of + order-PRESERVING lists: DereferenceTeam and DereferenceTeamMember use RemoveAtIndex, so + removing one shifts the rest down. + - TeamManager::GetWorldAtIndex publishes the index of an order-NOT-preserving list: RemoveWorld + uses RemoveAtIndexFast, which swaps the last world into the hole. + +A migration that used one idiom for both would quietly renumber half of these, and the NetworkID +lookups (teamsHash, teamMembersHash) would still answer correctly, so nothing else would notice. + +Success conditions: index order is ascending-by-insertion for teams and members and survives a +removal in the middle; world removal swaps rather than shifts; NetworkID lookups resolve to the +registered object and report absence otherwise; participants round-trip. +*/ + +namespace +{ + class TestTeam : public TM_Team + { + }; + + class TestTeamMember : public TM_TeamMember + { + }; + + class TeamManagerRegistry : public ::testing::Test + { + protected: + void SetUp() override + { + peer = RakPeerInterface::GetInstance(); + peer->AttachPlugin(&manager); + world = manager.AddWorld(0); + ASSERT_NE(world, (TM_World *) 0); + } + void TearDown() override + { + peer->DetachPlugin(&manager); + RakPeerInterface::DestroyInstance(peer); + } + + std::vector TeamIdsInIndexOrder(void) const + { + std::vector out; + for (unsigned i = 0; i < world->GetTeamCount(); ++i) + { + TM_Team *t = world->GetTeamByIndex(i); + if (t == 0) + break; + out.push_back(t->GetNetworkID()); + } + return out; + } + + RakPeerInterface *peer = nullptr; + TeamManager manager; + TM_World *world = nullptr; + }; +} + +TEST_F(TeamManagerRegistry, AWorldStartsEmpty) +{ + EXPECT_EQ(world->GetTeamCount(), 0u); + EXPECT_EQ(world->GetTeamMemberCount(), 0u); +} + +TEST_F(TeamManagerRegistry, TeamsAreIndexedInRegistrationOrder) +{ + TestTeam a, b, c; + world->ReferenceTeam(&a, 10, false); + world->ReferenceTeam(&b, 20, false); + world->ReferenceTeam(&c, 30, false); + + ASSERT_EQ(world->GetTeamCount(), 3u); + std::vector order = TeamIdsInIndexOrder(); + ASSERT_EQ(order.size(), 3u); + EXPECT_EQ(order[0], static_cast(10)); + EXPECT_EQ(order[1], static_cast(20)); + EXPECT_EQ(order[2], static_cast(30)); + + EXPECT_EQ(world->GetTeamIndex(&a), 0u); + EXPECT_EQ(world->GetTeamIndex(&b), 1u); + EXPECT_EQ(world->GetTeamIndex(&c), 2u); +} + +// DereferenceTeam uses RemoveAtIndex, so the list shifts; it must NOT swap the last into the hole. +TEST_F(TeamManagerRegistry, DereferencingATeamShiftsTheRestDown) +{ + TestTeam a, b, c; + world->ReferenceTeam(&a, 10, false); + world->ReferenceTeam(&b, 20, false); + world->ReferenceTeam(&c, 30, false); + + world->DereferenceTeam(&a, 0); // index 0, with index 2 being last + + ASSERT_EQ(world->GetTeamCount(), 2u); + std::vector order = TeamIdsInIndexOrder(); + ASSERT_EQ(order.size(), 2u); + EXPECT_EQ(order[0], static_cast(20)) << "the remainder must shift down, not be swapped"; + EXPECT_EQ(order[1], static_cast(30)); + EXPECT_EQ(world->GetTeamIndex(&b), 0u); + EXPECT_EQ(world->GetTeamIndex(&c), 1u); +} + +TEST_F(TeamManagerRegistry, TeamsAreFoundByNetworkId) +{ + TestTeam a, b; + world->ReferenceTeam(&a, 10, false); + world->ReferenceTeam(&b, 20, false); + + EXPECT_EQ(world->GetTeamByNetworkID(10), &a); + EXPECT_EQ(world->GetTeamByNetworkID(20), &b); + EXPECT_EQ(world->GetTeamByNetworkID(999), (TM_Team *) 0); +} + +TEST_F(TeamManagerRegistry, ADereferencedTeamIsNoLongerFoundByNetworkId) +{ + TestTeam a; + world->ReferenceTeam(&a, 10, false); + ASSERT_EQ(world->GetTeamByNetworkID(10), &a); + + world->DereferenceTeam(&a, 0); + + EXPECT_EQ(world->GetTeamByNetworkID(10), (TM_Team *) 0); + EXPECT_EQ(world->GetTeamCount(), 0u); +} + +TEST_F(TeamManagerRegistry, ManyTeamsAreAllReachableByNetworkId) +{ + // Enough to spread across hash buckets, so a migration that collapses colliding keys shows up. + const int count = 120; + std::vector teams; + for (int i = 0; i < count; ++i) + { + TestTeam *t = new TestTeam(); + teams.push_back(t); + world->ReferenceTeam(t, (NetworkID) (1000 + i), false); + } + ASSERT_EQ(world->GetTeamCount(), (unsigned) count); + for (int i = 0; i < count; ++i) + EXPECT_EQ(world->GetTeamByNetworkID((NetworkID) (1000 + i)), teams[i]) << "team " << i; + + for (int i = 0; i < count; ++i) + { + world->DereferenceTeam(teams[i], 0); + delete teams[i]; + } +} + +TEST_F(TeamManagerRegistry, TeamMembersAreIndexedInRegistrationOrder) +{ + TestTeamMember a, b, c; + world->ReferenceTeamMember(&a, 11); + world->ReferenceTeamMember(&b, 22); + world->ReferenceTeamMember(&c, 33); + + ASSERT_EQ(world->GetTeamMemberCount(), 3u); + EXPECT_EQ(world->GetTeamMemberByIndex(0), &a); + EXPECT_EQ(world->GetTeamMemberByIndex(1), &b); + EXPECT_EQ(world->GetTeamMemberByIndex(2), &c); + EXPECT_EQ(world->GetTeamMemberIndex(&b), 1u); + EXPECT_EQ(world->GetTeamMemberIDByIndex(2), static_cast(33)); +} + +TEST_F(TeamManagerRegistry, DereferencingATeamMemberShiftsTheRestDown) +{ + TestTeamMember a, b, c; + world->ReferenceTeamMember(&a, 11); + world->ReferenceTeamMember(&b, 22); + world->ReferenceTeamMember(&c, 33); + + world->DereferenceTeamMember(&a); + + ASSERT_EQ(world->GetTeamMemberCount(), 2u); + EXPECT_EQ(world->GetTeamMemberByIndex(0), &b) << "the remainder must shift down, not be swapped"; + EXPECT_EQ(world->GetTeamMemberByIndex(1), &c); +} + +TEST_F(TeamManagerRegistry, TeamMembersAreFoundByNetworkId) +{ + TestTeamMember a, b; + world->ReferenceTeamMember(&a, 11); + world->ReferenceTeamMember(&b, 22); + + EXPECT_EQ(world->GetTeamMemberByNetworkID(11), &a); + EXPECT_EQ(world->GetTeamMemberByNetworkID(22), &b); + EXPECT_EQ(world->GetTeamMemberByNetworkID(999), (TM_TeamMember *) 0); + + world->DereferenceTeamMember(&a); + EXPECT_EQ(world->GetTeamMemberByNetworkID(11), (TM_TeamMember *) 0); +} + +TEST_F(TeamManagerRegistry, ParticipantsRoundTripAndDeduplicate) +{ + const RakNetGUID g1(1001), g2(1002); + world->AddParticipant(g1); + world->AddParticipant(g2); + + DataStructures::List list; + world->GetParticipantList(list); + ASSERT_EQ(list.Size(), 2u); + + std::set got; + for (unsigned i = 0; i < list.Size(); ++i) + got.insert(list[i].g); + EXPECT_EQ(got.count(1001), 1u); + EXPECT_EQ(got.count(1002), 1u); + + world->RemoveParticipant(g1); + DataStructures::List after; + world->GetParticipantList(after); + ASSERT_EQ(after.Size(), 1u); + EXPECT_EQ(after[0].g, static_cast(1002)); +} + +// RemoveWorld uses RemoveAtIndexFast while DereferenceTeam uses RemoveAtIndex. Both indices are +// published, so the two idioms have to stay distinct through the migration. +TEST_F(TeamManagerRegistry, RemoveWorldSwapsTheLastWorldIntoTheHole) +{ + manager.AddWorld(1); + manager.AddWorld(2); + manager.AddWorld(3); + ASSERT_EQ(manager.GetWorldCount(), 4u); + + TM_World *w2 = manager.GetWorldWithId(2); + TM_World *w3 = manager.GetWorldWithId(3); + ASSERT_NE(w2, (TM_World *) 0); + ASSERT_NE(w3, (TM_World *) 0); + ASSERT_EQ(manager.GetWorldAtIndex(2), w2); + ASSERT_EQ(manager.GetWorldAtIndex(3), w3); + + manager.RemoveWorld(2); // index 2, with index 3 being last + + ASSERT_EQ(manager.GetWorldCount(), 3u); + EXPECT_EQ(manager.GetWorldAtIndex(2), w3) + << "the last world must be swapped into the hole, not shifted up"; + EXPECT_EQ(manager.GetWorldWithId(2), (TM_World *) 0); + EXPECT_EQ(manager.GetWorldWithId(3), w3); +} + +TEST_F(TeamManagerRegistry, WorldsAreFoundById) +{ + manager.AddWorld(7); + ASSERT_EQ(manager.GetWorldCount(), 2u); + EXPECT_NE(manager.GetWorldWithId(7), (TM_World *) 0); + EXPECT_EQ(manager.GetWorldWithId(9), (TM_World *) 0); +} From ce7193feb0fb59a6e0467764b4cb5ad88648caad Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:55:10 +0200 Subject: [PATCH 07/26] refactor(teammanager): move the registries to std containers Part of #60, stage 2. Everything changed is protected; the public getters keep their DataStructures::List out-parameters. - TM_World::teams, teamMembers, participants; TM_TeamMember::teams, teamsRequested, lastTeams; TM_Team::teamMembers: List -> std::vector, with RemoveAtIndex becoming erase so the published indices keep shifting down. - TM_World::teamsHash, teamMembersHash: DataStructures::Hash -> std::unordered_map. NetworkID is an integer type, so the default std::hash applies and the ToUint32 projection is no longer needed. - TeamManager::worldsList: List -> std::vector, with RemoveAtIndexFast reproduced as an explicit swap-remove. GetWorldAtIndex exposes that index, and unlike the team lists it has never preserved order. - GetSortedJoinRequests: OrderedList -> std::vector + std::sort with the unchanged comparator. requestIndex breaks ties and is a monotonically increasing per-request counter, so no two requests compare equal and the sort matches the ordered inserts exactly. - A local DataStructures::List working copy in GetAvailableTeamIndexWithFewestMembers keeps its swap-remove, which is correct there because the loop re-tests the same index. Adds a file-local IndexOf to replace List::GetIndexOf including its (unsigned)-1 result, and CopyToList to fill the four still-public DataStructures::List out-parameters from the internal vectors. Stage 3 removes those conversions with the signatures. Two corrections to the tests added in 9a91bea7, both found by mutation: - RemoveWorldSwapsTheLastWorldIntoTheHole removed the second-to-last world, which is degenerate: swap-into-hole and shift-down agree there, and turning the swap into an erase passed. It now removes an index with two worlds after it and fails against the erase. - Turning the team list's erase into a swap-remove fails DereferencingATeamShiftsTheRestDown, so the opposite direction is pinned too. --- Source/include/mafianet/team_manager.h | 37 ++-- Source/src/team_manager.cpp | 280 +++++++++++++----------- Tests/Unit/TeamManagerRegistryTests.cpp | 15 +- 3 files changed, 191 insertions(+), 141 deletions(-) diff --git a/Source/include/mafianet/team_manager.h b/Source/include/mafianet/team_manager.h index e86437fe0..d4be2b1ee 100644 --- a/Source/include/mafianet/team_manager.h +++ b/Source/include/mafianet/team_manager.h @@ -36,6 +36,8 @@ #include "mafianet/ds_list.h" #include "mafianet/types.h" #include "mafianet/ds_hash.h" +#include +#include #include "mafianet/ds_ordered_list.h" namespace MafiaNet @@ -272,16 +274,16 @@ class RAK_DLL_EXPORT TM_TeamMember NetworkID networkId; TM_World* world; // Teams we are a member of. We can be on more than one team, but not on the same team more than once - DataStructures::List teams; + std::vector teams; // If teams is empty, which subcategory of noTeam we are on NoTeamId noTeamSubcategory; // Teams we have requested to join. Mutually exclusive with teams we are already on. Cannot request the same team more than once. - DataStructures::List teamsRequested; + std::vector teamsRequested; // If teamsRequested is not empty, we want to join a specific team // If teamsRequested is empty, then joinTeamType is either JOIN_NO_TEAM or JOIN_ANY_AVAILABLE_TEAM JoinTeamType joinTeamType; // Set by StoreLastTeams() - DataStructures::List lastTeams; + std::vector lastTeams; MafiaNet::Time whenJoinAnyRequested; unsigned int joinAnyRequestIndex; void *owner; @@ -400,7 +402,7 @@ class RAK_DLL_EXPORT TM_Team NetworkID ID; TM_World* world; // Which members are on this team. The same member cannot be on the same team more than once - DataStructures::List teamMembers; + std::vector teamMembers; // Permissions on who can join this team JoinPermissions joinPermissions; // Whether or not to consider this team when balancing teams @@ -566,7 +568,10 @@ class TM_World void FillRequestedSlots(void); unsigned int GetAvailableTeamIndexWithFewestMembers(TeamMemberLimit secondaryLimit, JoinPermissions joinPermissions); - void GetSortedJoinRequests(DataStructures::OrderedList &joinRequests); + // Sorted ascending by JoinRequestHelperComp. requestIndex breaks ties and is a + // monotonically increasing per-request counter, so no two requests compare equal and a + // sort is equivalent to the ordered inserts this replaced. + void GetSortedJoinRequests(std::vector &joinRequests); // Send a message to all participants @@ -580,19 +585,22 @@ class TM_World // 2. Else return 0 TM_Team* JoinAnyTeam(TM_TeamMember *teamMember, int *resultCode); - int JoinSpecificTeam(TM_TeamMember *teamMember, TM_Team *team, bool isTeamSwitch, TM_Team *teamToLeave, DataStructures::List &teamsWeAreLeaving); + int JoinSpecificTeam(TM_TeamMember *teamMember, TM_Team *team, bool isTeamSwitch, TM_Team *teamToLeave, std::vector &teamsWeAreLeaving); TeamMemberLimit GetBalancedTeamLimit(void) const; - // For fast lookup. Shares pointers with list teams - DataStructures::Hash teamsHash; + // For fast lookup. Shares pointers with list teams. NetworkID is an integer type, so the + // default std::hash applies and the ToUint32 projection the old table needed is not used. + std::unordered_map teamsHash; // For fast lookup. Shares pointers with list teamMembers - DataStructures::Hash teamMembersHash; + std::unordered_map teamMembersHash; TeamManager *teamManager; - DataStructures::List participants; - DataStructures::List teams; - DataStructures::List teamMembers; + std::vector participants; + // Order-preserving: GetTeamByIndex and GetTeamIndex publish this index and + // DereferenceTeam shifts the remainder down rather than swapping. + std::vector teams; + std::vector teamMembers; bool balanceTeamsIsActive; RakNetGUID hostGuid; WorldId worldId; @@ -744,8 +752,9 @@ class RAK_DLL_EXPORT TeamManager : public PluginInterface2 // O(1) lookup for a given world. If I need more worlds, change this to a hash or ordered list TM_World *worldsArray[255]; - // All allocated worlds for linear traversal - DataStructures::List worldsList; + // All allocated worlds for linear traversal. Unlike the team lists above, RemoveWorld + // swaps the last world into the hole, which GetWorldAtIndex exposes. + std::vector worldsList; bool autoAddParticipants; TMTopology topology; diff --git a/Source/src/team_manager.cpp b/Source/src/team_manager.cpp index 6480bf39b..e22e1e0f9 100644 --- a/Source/src/team_manager.cpp +++ b/Source/src/team_manager.cpp @@ -17,12 +17,36 @@ #if _RAKNET_SUPPORT_TeamManager==1 #include "mafianet/team_manager.h" +#include #include "mafianet/bit_stream.h" #include "mafianet/message_identifiers.h" #include "mafianet/get_time.h" using namespace MafiaNet; +namespace +{ + // Replaces DataStructures::List::GetIndexOf, including its (unsigned)-1 "not found" value. + template + unsigned int IndexOf(const std::vector &v, const T &value) + { + typename std::vector::const_iterator it = std::find(v.begin(), v.end(), value); + if (it == v.end()) + return (unsigned int) -1; + return (unsigned int) (it - v.begin()); + } + + // Fills a still-public DataStructures::List out-parameter from an internal std::vector. + // Stage 3 of #60 changes those signatures and removes these conversions. + template + void CopyToList(const std::vector &from, DataStructures::List &to) + { + to.Clear(true, _FILE_AND_LINE_); + for (size_t i = 0; i < from.size(); ++i) + to.Push(from[i], _FILE_AND_LINE_); + } +} + enum TeamManagerOperations { @@ -126,7 +150,7 @@ bool TM_TeamMember::RequestTeam(TeamSelection teamSelection) // - remote Host executes JoinNoTeam() and broadcasts event. This may cause may cause rebalance if team balancing is on. // - - JoinNoTeam(): Remove from all current and requested teams. Set no-team category. - if (teams.Size()==0 && noTeamSubcategory==teamSelection.teamParameter.noTeamSubcategory) + if (teams.size()==0 && noTeamSubcategory==teamSelection.teamParameter.noTeamSubcategory) { // No change return false; @@ -246,7 +270,7 @@ bool TM_TeamMember::RequestTeamSwitch(TM_Team *teamToJoin, TM_Team *teamToLeave) TeamSelection TM_TeamMember::GetRequestedTeam(void) const { - if (teamsRequested.Size()>0) + if (teamsRequested.size()>0) return TeamSelection::SpecificTeam(teamsRequested[0].requested); else if (joinTeamType==JOIN_NO_TEAM) return TeamSelection::NoTeam(noTeamSubcategory); @@ -259,7 +283,7 @@ TeamSelection TM_TeamMember::GetRequestedTeam(void) const void TM_TeamMember::GetRequestedSpecificTeams(DataStructures::List &requestedTeams) const { requestedTeams.Clear(true, _FILE_AND_LINE_); - for (unsigned int i=0; i < teamsRequested.Size(); i++) + for (unsigned int i=0; i < teamsRequested.size(); i++) requestedTeams.Push(teamsRequested[i].requested, _FILE_AND_LINE_); } @@ -278,7 +302,7 @@ bool TM_TeamMember::HasRequestedTeam(TM_Team *team) const unsigned int TM_TeamMember::GetRequestedTeamIndex(TM_Team *team) const { unsigned int i; - for (i=0; i < teamsRequested.Size(); i++) + for (i=0; i < teamsRequested.size(); i++) { if (teamsRequested[i].requested==team) return i; @@ -290,7 +314,7 @@ unsigned int TM_TeamMember::GetRequestedTeamIndex(TM_Team *team) const unsigned int TM_TeamMember::GetRequestedTeamCount(void) const { - return teamsRequested.Size(); + return teamsRequested.size(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -329,7 +353,7 @@ bool TM_TeamMember::LeaveTeam(TM_Team* team, NoTeamId _noTeamSubcategory) return false; RemoveFromSpecificTeamInternal(team); - if (teams.Size()==0) + if (teams.size()==0) { noTeamSubcategory=_noTeamSubcategory; joinTeamType=JOIN_NO_TEAM; @@ -382,7 +406,7 @@ bool TM_TeamMember::LeaveAllTeams(NoTeamId inNoTeamSubcategory) TM_Team* TM_TeamMember::GetCurrentTeam(void) const { - if (teams.Size()>0) + if (teams.size()>0) return teams[0]; return 0; } @@ -391,7 +415,7 @@ TM_Team* TM_TeamMember::GetCurrentTeam(void) const unsigned int TM_TeamMember::GetCurrentTeamCount(void) const { - return teams.Size(); + return teams.size(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -405,14 +429,14 @@ TM_Team* TM_TeamMember::GetCurrentTeamByIndex(unsigned int index) void TM_TeamMember::GetCurrentTeams(DataStructures::List &_teams) const { - _teams=teams; + CopyToList(teams, _teams); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- void TM_TeamMember::GetLastTeams(DataStructures::List &_teams) const { - _teams=lastTeams; + CopyToList(lastTeams, _teams); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -420,7 +444,7 @@ void TM_TeamMember::GetLastTeams(DataStructures::List &_teams) const bool TM_TeamMember::IsOnTeam(TM_Team *team) const { unsigned int i; - for (i=0; i < teams.Size(); i++) + for (i=0; i < teams.size(); i++) { if (teams[i]==team) return true; @@ -449,8 +473,8 @@ void TM_TeamMember::SerializeConstruction(BitStream *constructionBitstream) // Write requested teams constructionBitstream->Write(world->GetWorldId()); constructionBitstream->Write(networkId); - constructionBitstream->WriteCasted(teamsRequested.Size()); - for (unsigned int i=0; i < teamsRequested.Size(); i++) + constructionBitstream->WriteCasted(teamsRequested.size()); + for (unsigned int i=0; i < teamsRequested.size(); i++) { constructionBitstream->Write(teamsRequested[i].isTeamSwitch); if (teamsRequested[i].teamToLeave) @@ -526,7 +550,7 @@ bool TM_TeamMember::DeserializeConstruction(TeamManager *teamManager, BitStream (hasTeamRequested==false || (hasTeamRequested==true && rt.requested!=0)) ) { - teamsRequested.Push(rt, _FILE_AND_LINE_); + teamsRequested.push_back(rt); } } @@ -575,12 +599,12 @@ unsigned long TM_TeamMember::ToUint32( const NetworkID &g ) void TM_TeamMember::UpdateListsToNoTeam(NoTeamId nti) { - teamsRequested.Clear(true, _FILE_AND_LINE_ ); - for (unsigned int i=0; i < teams.Size(); i++) + teamsRequested.clear(); + for (unsigned int i=0; i < teams.size(); i++) { teams[i]->RemoveFromTeamMemberList(this); } - teams.Clear(true, _FILE_AND_LINE_ ); + teams.clear(); noTeamSubcategory=nti; joinTeamType=JOIN_NO_TEAM; } @@ -590,11 +614,11 @@ void TM_TeamMember::UpdateListsToNoTeam(NoTeamId nti) bool TM_TeamMember::JoinAnyTeamCheck(void) const { // - - If already on a team, return false - if (teams.Size() > 0) + if (teams.size() > 0) return false; // - - If any team is already in requested teams, return false. - if (teamsRequested.Size()==0 && joinTeamType==JOIN_ANY_AVAILABLE_TEAM) + if (teamsRequested.size()==0 && joinTeamType==JOIN_ANY_AVAILABLE_TEAM) return false; return true; @@ -612,7 +636,7 @@ bool TM_TeamMember::JoinSpecificTeamCheck(TM_Team *specificTeamToJoin, bool igno return true; unsigned int i; - for (i=0; i < teamsRequested.Size(); i++) + for (i=0; i < teamsRequested.size(); i++) { if (teamsRequested[i].requested==specificTeamToJoin) { @@ -648,7 +672,7 @@ bool TM_TeamMember::SwitchSpecificTeamCheck(TM_Team *teamToJoin, TM_Team *teamTo return true; unsigned int i; - for (i=0; i < teamsRequested.Size(); i++) + for (i=0; i < teamsRequested.size(); i++) { if (teamsRequested[i].requested==teamToJoin) { @@ -680,7 +704,7 @@ bool TM_TeamMember::LeaveTeamCheck(TM_Team *team) const void TM_TeamMember::UpdateTeamsRequestedToAny(void) { - teamsRequested.Clear(true, _FILE_AND_LINE_); + teamsRequested.clear(); joinTeamType=JOIN_ANY_AVAILABLE_TEAM; whenJoinAnyRequested= MafiaNet::GetTime(); joinAnyRequestIndex=world->teamRequestIndex++; // In case whenRequested is the same between two teams when sorting team requests @@ -690,7 +714,7 @@ void TM_TeamMember::UpdateTeamsRequestedToAny(void) void TM_TeamMember::UpdateTeamsRequestedToNone(void) { - teamsRequested.Clear(true, _FILE_AND_LINE_); + teamsRequested.clear(); joinTeamType=JOIN_NO_TEAM; } @@ -706,7 +730,7 @@ void TM_TeamMember::AddToRequestedTeams(TM_Team *teamToJoin) rt.teamToLeave=0; rt.whenRequested= MafiaNet::GetTime(); rt.requestIndex=world->teamRequestIndex++; // In case whenRequested is the same between two teams when sorting team requests - teamsRequested.Push(rt, _FILE_AND_LINE_ ); + teamsRequested.push_back(rt); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -721,7 +745,7 @@ void TM_TeamMember::AddToRequestedTeams(TM_Team *teamToJoin, TM_Team *teamToLeav rt.teamToLeave=teamToLeave; rt.whenRequested= MafiaNet::GetTime(); rt.requestIndex=world->teamRequestIndex++; // In case whenRequested is the same between two teams when sorting team requests - teamsRequested.Push(rt, _FILE_AND_LINE_ ); + teamsRequested.push_back(rt); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -730,19 +754,19 @@ bool TM_TeamMember::RemoveFromRequestedTeams(TM_Team *team) { if (team==0) { - teamsRequested.Clear(true, _FILE_AND_LINE_); + teamsRequested.clear(); joinTeamType=JOIN_NO_TEAM; return true; } else { unsigned int i; - for (i=0; i < teamsRequested.Size(); i++) + for (i=0; i < teamsRequested.size(); i++) { if (teamsRequested[i].requested==team) { - teamsRequested.RemoveAtIndex(i); - if (teamsRequested.Size()==0) + teamsRequested.erase(teamsRequested.begin() + i); + if (teamsRequested.size()==0) { joinTeamType=JOIN_NO_TEAM; } @@ -757,8 +781,8 @@ bool TM_TeamMember::RemoveFromRequestedTeams(TM_Team *team) void TM_TeamMember::AddToTeamList(TM_Team *team) { - team->teamMembers.Push(this, _FILE_AND_LINE_ ); - teams.Push(team, _FILE_AND_LINE_ ); + team->teamMembers.push_back(this); + teams.push_back(team); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -766,19 +790,19 @@ void TM_TeamMember::AddToTeamList(TM_Team *team) void TM_TeamMember::RemoveFromSpecificTeamInternal(TM_Team *team) { unsigned int i,j; - for (i=0; i < teams.Size(); i++) + for (i=0; i < teams.size(); i++) { if (teams[i]==team) { - for (j=0; j < team->teamMembers.Size(); j++) + for (j=0; j < team->teamMembers.size(); j++) { if (team->teamMembers[j]==this) { - team->teamMembers.RemoveAtIndex(j); + team->teamMembers.erase(team->teamMembers.begin() + j); break; } } - teams.RemoveAtIndex(i); + teams.erase(teams.begin() + i); break; } } @@ -790,20 +814,20 @@ void TM_TeamMember::RemoveFromAllTeamsInternal(void) { TM_Team *team; unsigned int i,j; - for (i=0; i < teams.Size(); i++) + for (i=0; i < teams.size(); i++) { team = teams[i]; - for (j=0; j < team->teamMembers.Size(); j++) + for (j=0; j < team->teamMembers.size(); j++) { if (team->teamMembers[j]==this) { - team->teamMembers.RemoveAtIndex(j); + team->teamMembers.erase(team->teamMembers.begin() + j); break; } } } - teams.Clear(true, _FILE_AND_LINE_); + teams.clear(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -934,14 +958,14 @@ bool TM_Team::GetBalancingApplies(void) const void TM_Team::GetTeamMembers(DataStructures::List &_teamMembers) const { - _teamMembers=teamMembers; + CopyToList(teamMembers, _teamMembers); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- unsigned int TM_Team::GetTeamMembersCount(void) const { - return teamMembers.Size(); + return teamMembers.size(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1032,9 +1056,9 @@ void TM_Team::SetOwner(void *o) void TM_Team::RemoveFromTeamMemberList(TM_TeamMember *teamMember) { - unsigned int index = teamMembers.GetIndexOf(teamMember); + unsigned int index = IndexOf(teamMembers, teamMember); RakAssert(index != (unsigned int) -1); - teamMembers.RemoveAtIndex(index); + teamMembers.erase(teamMembers.begin() + index); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1042,7 +1066,7 @@ void TM_Team::RemoveFromTeamMemberList(TM_TeamMember *teamMember) unsigned int TM_Team::GetMemberWithRequestedSingleTeamSwitch(TM_Team *team) { unsigned int i; - for (i=0; i < teamMembers.Size(); i++) + for (i=0; i < teamMembers.size(); i++) { if (teamMembers[i]->GetCurrentTeamCount()==1) { @@ -1093,7 +1117,7 @@ TeamManager *TM_World::GetTeamManager(void) const void TM_World::AddParticipant(RakNetGUID rakNetGUID) { - participants.Push(rakNetGUID, _FILE_AND_LINE_ ); + participants.push_back(rakNetGUID); // Send to remote system status of balanceTeamsIsActive @@ -1114,9 +1138,9 @@ void TM_World::AddParticipant(RakNetGUID rakNetGUID) void TM_World::RemoveParticipant(RakNetGUID rakNetGUID) { unsigned int i; - i = participants.GetIndexOf(rakNetGUID); + i = IndexOf(participants, rakNetGUID); if (i!=(unsigned int)-1) - participants.RemoveAtIndex(i); + participants.erase(participants.begin() + i); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1130,7 +1154,7 @@ void TM_World::SetAutoManageConnections(bool autoAdd) void TM_World::GetParticipantList(DataStructures::List &participantList) { - participantList = participants; + CopyToList(participants, participantList); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1138,7 +1162,7 @@ void TM_World::GetParticipantList(DataStructures::List &participantL void TM_World::ReferenceTeam(TM_Team *team, NetworkID networkId, bool applyBalancing) { unsigned int i; - for (i=0; i < teams.Size(); i++) + for (i=0; i < teams.size(); i++) { if (teams[i]==team) return; @@ -1149,9 +1173,9 @@ void TM_World::ReferenceTeam(TM_Team *team, NetworkID networkId, bool applyBalan team->world=this; // Add this team to the list of teams - teams.Push(team, _FILE_AND_LINE_); + teams.push_back(team); - teamsHash.Push(networkId,team,_FILE_AND_LINE_); + teamsHash.insert(std::make_pair(networkId, team)); // If autobalancing is on, and the team lock state supports it, then call EnforceTeamBalancing() if (applyBalancing && balanceTeamsIsActive) @@ -1165,18 +1189,18 @@ void TM_World::ReferenceTeam(TM_Team *team, NetworkID networkId, bool applyBalan void TM_World::DereferenceTeam(TM_Team *team, NoTeamId noTeamSubcategory) { unsigned int i; - for (i=0; i < teams.Size(); i++) + for (i=0; i < teams.size(); i++) { if (teams[i]==team) { TM_Team *curTeam = teams[i]; - while (curTeam->teamMembers.Size()) + while (curTeam->teamMembers.size()) { - curTeam->teamMembers[curTeam->teamMembers.Size()-1]->LeaveTeam(curTeam, noTeamSubcategory); + curTeam->teamMembers[curTeam->teamMembers.size()-1]->LeaveTeam(curTeam, noTeamSubcategory); } - teams.RemoveAtIndex(i); + teams.erase(teams.begin() + i); - teamsHash.Remove(curTeam->GetNetworkID(),_FILE_AND_LINE_); + teamsHash.erase(curTeam->GetNetworkID()); break; } @@ -1187,7 +1211,7 @@ void TM_World::DereferenceTeam(TM_Team *team, NoTeamId noTeamSubcategory) unsigned int TM_World::GetTeamCount(void) const { - return teams.Size(); + return teams.size(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1201,10 +1225,10 @@ TM_Team *TM_World::GetTeamByIndex(unsigned int index) const TM_Team *TM_World::GetTeamByNetworkID(NetworkID teamId) { - DataStructures::HashIndex hi = teamsHash.GetIndexOf(teamId); - if (hi.IsInvalid()) + std::unordered_map::const_iterator it = teamsHash.find(teamId); + if (it == teamsHash.end()) return 0; - return teamsHash.ItemAtIndex(hi); + return it->second; } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1212,7 +1236,7 @@ TM_Team *TM_World::GetTeamByNetworkID(NetworkID teamId) unsigned int TM_World::GetTeamIndex(const TM_Team *team) const { unsigned int i; - for (i=0; i < teams.Size(); i++) + for (i=0; i < teams.size(); i++) { if (teams[i]==team) return i; @@ -1225,7 +1249,7 @@ unsigned int TM_World::GetTeamIndex(const TM_Team *team) const void TM_World::ReferenceTeamMember(TM_TeamMember *teamMember, NetworkID networkId) { unsigned int i; - for (i=0; i < teamMembers.Size(); i++) + for (i=0; i < teamMembers.size(); i++) { if (teamMembers[i]==teamMember) return; @@ -1234,9 +1258,9 @@ void TM_World::ReferenceTeamMember(TM_TeamMember *teamMember, NetworkID networkI teamMember->world=this; teamMember->networkId=networkId; - teamMembers.Push(teamMember, _FILE_AND_LINE_); + teamMembers.push_back(teamMember); - teamMembersHash.Push(networkId,teamMember,_FILE_AND_LINE_); + teamMembersHash.insert(std::make_pair(networkId, teamMember)); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1244,13 +1268,13 @@ void TM_World::ReferenceTeamMember(TM_TeamMember *teamMember, NetworkID networkI void TM_World::DereferenceTeamMember(TM_TeamMember *teamMember) { unsigned int i; - for (i=0; i < teamMembers.Size(); i++) + for (i=0; i < teamMembers.size(); i++) { if (teamMembers[i]==teamMember) { teamMembers[i]->UpdateListsToNoTeam(0); - teamMembersHash.Remove(teamMembers[i]->GetNetworkID(),_FILE_AND_LINE_); - teamMembers.RemoveAtIndex(i); + teamMembersHash.erase(teamMembers[i]->GetNetworkID()); + teamMembers.erase(teamMembers.begin() + i); break; } } @@ -1260,7 +1284,7 @@ void TM_World::DereferenceTeamMember(TM_TeamMember *teamMember) unsigned int TM_World::GetTeamMemberCount(void) const { - return teamMembers.Size(); + return teamMembers.size(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1281,10 +1305,10 @@ NetworkID TM_World::GetTeamMemberIDByIndex(unsigned int index) const TM_TeamMember *TM_World::GetTeamMemberByNetworkID(NetworkID teamMemberId) { - DataStructures::HashIndex hi = teamMembersHash.GetIndexOf(teamMemberId); - if (hi.IsInvalid()) + std::unordered_map::const_iterator it = teamMembersHash.find(teamMemberId); + if (it == teamMembersHash.end()) return 0; - return teamMembersHash.ItemAtIndex(hi); + return it->second; } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1292,7 +1316,7 @@ TM_TeamMember *TM_World::GetTeamMemberByNetworkID(NetworkID teamMemberId) unsigned int TM_World::GetTeamMemberIndex(const TM_TeamMember *teamMember) const { unsigned int i; - for (i=0; i < teamMembers.Size(); i++) + for (i=0; i < teamMembers.size(); i++) { if (teamMembers[i]==teamMember) return i; @@ -1361,17 +1385,17 @@ WorldId TM_World::GetWorldId(void) const void TM_World::Clear(void) { - for (unsigned int i=0; i < teams.Size(); i++) + for (unsigned int i=0; i < teams.size(); i++) { teams[i]->world=0; } - for (unsigned int i=0; i < teamMembers.Size(); i++) + for (unsigned int i=0; i < teamMembers.size(); i++) { teamMembers[i]->world=0; } - participants.Clear(true, _FILE_AND_LINE_); - teams.Clear(true, _FILE_AND_LINE_); - teamMembers.Clear(true, _FILE_AND_LINE_); + participants.clear(); + teams.clear(); + teamMembers.clear(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1424,13 +1448,13 @@ void TM_World::KickExcessMembers(NoTeamId noTeamId) TM_Team *team, *teamToJoin; unsigned int i, teamIndex; - for (i=0; i < teams.Size(); i++) + for (i=0; i < teams.size(); i++) { team = teams[i]; while (team->GetMemberLimitSetting() < team->GetTeamMembersCount() || (balancedTeamLimit < team->GetTeamMembersCount() && team->GetBalancingApplies()) ) { - TM_TeamMember *teamMember = team->teamMembers[team->teamMembers.Size()-1]; + TM_TeamMember *teamMember = team->teamMembers[team->teamMembers.size()-1]; teamIndex = GetAvailableTeamIndexWithFewestMembers(balancedTeamLimit, ALLOW_JOIN_REBALANCING); if (teamIndex == (unsigned int)-1) @@ -1479,14 +1503,14 @@ void TM_World::FillRequestedSlots(void) unsigned int teamIndex, indexIntoTeamsRequested = (unsigned int)-1; TM_Team *team; TM_TeamMember *teamMember; - DataStructures::OrderedList joinRequests; + std::vector joinRequests; GetSortedJoinRequests(joinRequests); unsigned int joinRequestIndex; - for (joinRequestIndex=0; joinRequestIndex < joinRequests.Size(); joinRequestIndex++) + for (joinRequestIndex=0; joinRequestIndex < joinRequests.size(); joinRequestIndex++) { teamMember = teamMembers[joinRequests[joinRequestIndex].teamMemberIndex]; - if (teamMember->teamsRequested.Size()==0) + if (teamMember->teamsRequested.size()==0) { if (teamMember->joinTeamType==JOIN_ANY_AVAILABLE_TEAM) teamIndex = GetAvailableTeamIndexWithFewestMembers(balancedTeamLimit, ALLOW_JOIN_ANY_AVAILABLE_TEAM); @@ -1502,7 +1526,7 @@ void TM_World::FillRequestedSlots(void) team->GetTeamMembersCount() < team->GetMemberLimitSetting() && (ALLOW_JOIN_SPECIFIC_TEAM & team->GetJoinPermissions())!=0) { - teamIndex=teams.GetIndexOf(team); + teamIndex=IndexOf(teams, team); } else { @@ -1514,7 +1538,7 @@ void TM_World::FillRequestedSlots(void) { team = teams[teamIndex]; - if (teamMember->teamsRequested.Size()==0) + if (teamMember->teamsRequested.size()==0) { if (teamMember->joinTeamType==JOIN_ANY_AVAILABLE_TEAM) { @@ -1536,7 +1560,7 @@ void TM_World::FillRequestedSlots(void) else { // Switch or join specific - DataStructures::List teamsWeAreLeaving; + std::vector teamsWeAreLeaving; bool isSwitch = teamMember->teamsRequested[indexIntoTeamsRequested].isTeamSwitch; TM_Team *teamToLeave; if (isSwitch) @@ -1546,7 +1570,7 @@ void TM_World::FillRequestedSlots(void) { if (teamMember->IsOnTeam(teamToLeave)) { - teamsWeAreLeaving.Push(teamToLeave, _FILE_AND_LINE_); + teamsWeAreLeaving.push_back(teamToLeave); } else { @@ -1596,7 +1620,7 @@ unsigned int TM_World::GetAvailableTeamIndexWithFewestMembers(TeamMemberLimit se unsigned int lowestTeamMembers = (unsigned int) -1; unsigned int lowestIndex = (unsigned int) -1; - for (teamIndex=0; teamIndex < teams.Size(); teamIndex++) + for (teamIndex=0; teamIndex < teams.size(); teamIndex++) { if (teams[teamIndex]->GetTeamMembersCount() < secondaryLimit && teams[teamIndex]->GetTeamMembersCount() < teams[teamIndex]->GetMemberLimitSetting() && @@ -1613,14 +1637,14 @@ unsigned int TM_World::GetAvailableTeamIndexWithFewestMembers(TeamMemberLimit se // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -void TM_World::GetSortedJoinRequests(DataStructures::OrderedList &joinRequests) +void TM_World::GetSortedJoinRequests(std::vector &joinRequests) { unsigned int i; - for (i=0; i < teamMembers.Size(); i++) + for (i=0; i < teamMembers.size(); i++) { TM_TeamMember *teamMember = teamMembers[i]; - if (teamMember->teamsRequested.Size()==0) + if (teamMember->teamsRequested.size()==0) { if (teamMember->joinTeamType==JOIN_ANY_AVAILABLE_TEAM) { @@ -1628,30 +1652,37 @@ void TM_World::GetSortedJoinRequests(DataStructures::OrderedListwhenJoinAnyRequested; jrh.teamMemberIndex=i; jrh.requestIndex=teamMember->joinAnyRequestIndex; - joinRequests.Insert(jrh, jrh, true, _FILE_AND_LINE_); + joinRequests.push_back(jrh); } } else { unsigned int j; - for (j=0; j < teamMember->teamsRequested.Size(); j++) + for (j=0; j < teamMember->teamsRequested.size(); j++) { TM_World::JoinRequestHelper jrh; jrh.whenRequestMade=teamMember->teamsRequested[j].whenRequested; jrh.teamMemberIndex=i; jrh.indexIntoTeamsRequested=j; jrh.requestIndex=teamMember->teamsRequested[j].requestIndex; - joinRequests.Insert(jrh, jrh, true, _FILE_AND_LINE_); + joinRequests.push_back(jrh); } } } + + // Same ordering the OrderedList maintained on insert. + std::sort(joinRequests.begin(), joinRequests.end(), + [](const JoinRequestHelper &a, const JoinRequestHelper &b) + { + return JoinRequestHelperComp(a, b) < 0; + }); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- void TM_World::BroadcastToParticipants(MafiaNet::BitStream *bsOut, RakNetGUID exclusionGuid) { - for (unsigned int i=0; i < participants.Size(); i++) + for (unsigned int i=0; i < participants.size(); i++) { if (participants[i]==exclusionGuid) continue; @@ -1663,7 +1694,7 @@ void TM_World::BroadcastToParticipants(MafiaNet::BitStream *bsOut, RakNetGUID ex void TM_World::BroadcastToParticipants(unsigned char *data, const int length, RakNetGUID exclusionGuid) { - for (unsigned int i=0; i < participants.Size(); i++) + for (unsigned int i=0; i < participants.size(); i++) { if (participants[i]==exclusionGuid) continue; @@ -1681,7 +1712,7 @@ TM_Team* TM_World::JoinAnyTeam(TM_TeamMember *teamMember, int *resultCode) if (idx == (unsigned int ) -1) { // If any team is joinable but full, return full. Otherwise return locked - for (idx=0; idx < teams.Size(); idx++) + for (idx=0; idx < teams.size(); idx++) { if ((teams[idx]->GetTeamMembersCount() >= balancedLimit || teams[idx]->GetTeamMembersCount() >= teams[idx]->GetMemberLimitSetting()) && @@ -1712,11 +1743,11 @@ TM_Team* TM_World::JoinAnyTeam(TM_TeamMember *teamMember, int *resultCode) // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -int TM_World::JoinSpecificTeam(TM_TeamMember *teamMember, TM_Team *team, bool isTeamSwitch, TM_Team *teamToLeave, DataStructures::List &teamsWeAreLeaving) +int TM_World::JoinSpecificTeam(TM_TeamMember *teamMember, TM_Team *team, bool isTeamSwitch, TM_Team *teamToLeave, std::vector &teamsWeAreLeaving) { if (team->GetJoinPermissions() & ALLOW_JOIN_SPECIFIC_TEAM) { - if (balanceTeamsIsActive==false || teamsWeAreLeaving.Size()==0) + if (balanceTeamsIsActive==false || teamsWeAreLeaving.size()==0) { if (team->GetMemberLimit() > team->GetTeamMembersCount()) { @@ -1737,7 +1768,7 @@ int TM_World::JoinSpecificTeam(TM_TeamMember *teamMember, TM_Team *team, bool is // Do limited team swap // We must be on one team, target must be on one team, and we want to exchange teams - if (teamsWeAreLeaving.Size()==1) + if (teamsWeAreLeaving.size()==1) { unsigned int j = team->GetMemberWithRequestedSingleTeamSwitch(teamsWeAreLeaving[0]); if (j!=(unsigned int)-1) @@ -1774,7 +1805,7 @@ int TM_World::JoinSpecificTeam(TM_TeamMember *teamMember, TM_Team *team, bool is TeamMemberLimit TM_World::GetBalancedTeamLimit(void) const { - if (teams.Size()==0) + if (teams.size()==0) return 0; if (balanceTeamsIsActive==false) @@ -1783,23 +1814,25 @@ TeamMemberLimit TM_World::GetBalancedTeamLimit(void) const unsigned int i; bool additionalTeamsExcluded; TeamMemberLimit balancedLimit; - unsigned int teamsCount=teams.Size(); - unsigned int membersCount=teamMembers.Size(); - DataStructures::List consideredTeams = teams; + unsigned int teamsCount=teams.size(); + unsigned int membersCount=teamMembers.size(); + std::vector consideredTeams = teams; do { additionalTeamsExcluded=false; balancedLimit = (TeamMemberLimit) ((membersCount+(teamsCount-1))/(teamsCount)); i=0; - while (i < consideredTeams.Size()) + while (i < consideredTeams.size()) { if (consideredTeams[i]->GetMemberLimitSetting() < balancedLimit) { additionalTeamsExcluded=true; membersCount-=consideredTeams[i]->GetMemberLimitSetting(); teamsCount--; - consideredTeams.RemoveAtIndexFast(i); + // Swap-remove as before: this is a local working copy and the loop re-tests index i. + consideredTeams[i] = consideredTeams.back(); + consideredTeams.pop_back(); } else { @@ -1843,7 +1876,7 @@ TM_World* TeamManager::AddWorld(WorldId worldId) newWorld->teamManager=this; newWorld->hostGuid=GetMyGUIDUnified(); worldsArray[worldId]=newWorld; - worldsList.Push(newWorld,_FILE_AND_LINE_); + worldsList.push_back(newWorld); return newWorld; } @@ -1852,12 +1885,15 @@ TM_World* TeamManager::AddWorld(WorldId worldId) void TeamManager::RemoveWorld(WorldId worldId) { RakAssert(worldsArray[worldId]!=0 && "World not in use"); - for (unsigned int i=0; i < worldsList.Size(); i++) + for (unsigned int i=0; i < worldsList.size(); i++) { if (worldsList[i]==worldsArray[worldId]) { MafiaNet::OP_DELETE(worldsList[i],_FILE_AND_LINE_); - worldsList.RemoveAtIndexFast(i); + // Swap-remove, not erase: GetWorldAtIndex exposes this index and RemoveWorld has + // always swapped the last world in, unlike the team lists which shift down. + worldsList[i] = worldsList.back(); + worldsList.pop_back(); break; } } @@ -1868,7 +1904,7 @@ void TeamManager::RemoveWorld(WorldId worldId) unsigned int TeamManager::GetWorldCount(void) const { - return worldsList.Size(); + return worldsList.size(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1891,7 +1927,7 @@ void TeamManager::SetAutoManageConnections(bool autoAdd) { autoAddParticipants=autoAdd; - for (unsigned int i=0; i < worldsList.Size(); i++) + for (unsigned int i=0; i < worldsList.size(); i++) { worldsList[i]->SetAutoManageConnections(autoAdd); } @@ -1990,8 +2026,8 @@ void TeamManager::EncodeTeamAssigned(MafiaNet::BitStream *bitStream, TM_TeamMemb { bitStream->Write(teamMember->world->GetWorldId()); bitStream->Write(teamMember->GetNetworkID()); - bitStream->WriteCasted(teamMember->teams.Size()); - for (unsigned int i=0; i < teamMember->teams.Size(); i++) + bitStream->WriteCasted(teamMember->teams.size()); + for (unsigned int i=0; i < teamMember->teams.size(); i++) { bitStream->Write(teamMember->teams[i]->GetNetworkID()); } @@ -2124,7 +2160,7 @@ void TeamManager::DecodeTeamAssigned(BitStream *bsIn, TM_World **world, TM_TeamM if (*teamMember) { - for (unsigned int i=0; i < (*teamMember)->teams.Size(); i++) + for (unsigned int i=0; i < (*teamMember)->teams.size(); i++) { TM_Team *team = (*teamMember)->teams[i]; if (newTeam.GetIndexOf(team)==(unsigned int)-1) @@ -2135,7 +2171,7 @@ void TeamManager::DecodeTeamAssigned(BitStream *bsIn, TM_World **world, TM_TeamM for (unsigned int i=0; i < newTeam.Size(); i++) { TM_Team *team = newTeam[i]; - if ((*teamMember)->teams.GetIndexOf(team)==(unsigned int)-1) + if (IndexOf((*teamMember)->teams, team)==(unsigned int)-1) teamsJoined.Push(team, _FILE_AND_LINE_); } @@ -2152,13 +2188,13 @@ void TeamManager::DecodeTeamAssigned(BitStream *bsIn, TM_World **world, TM_TeamM void TeamManager::Clear(void) { - for (unsigned int i=0; i < worldsList.Size(); i++) + for (unsigned int i=0; i < worldsList.size(); i++) { worldsArray[worldsList[i]->worldId]=0; worldsList[i]->Clear(); MafiaNet::OP_DELETE(worldsList[i], _FILE_AND_LINE_); } - worldsList.Clear(false, _FILE_AND_LINE_); + worldsList.clear(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2176,7 +2212,7 @@ PluginReceiveResult TeamManager::OnReceive(Packet *packet) case ID_FCM2_NEW_HOST: { unsigned int i; - for (i=0; i < worldsList.Size(); i++) + for (i=0; i < worldsList.size(); i++) worldsList[i]->SetHost(packet->guid); } break; @@ -2265,7 +2301,7 @@ PluginReceiveResult TeamManager::OnReceive(Packet *packet) void TeamManager::OnClosedConnection(const SystemAddress &systemAddress, RakNetGUID rakNetGUID, PI2_LostConnectionReason lostConnectionReason ) { - for (unsigned int i=0; i < worldsList.Size(); i++) + for (unsigned int i=0; i < worldsList.size(); i++) { worldsList[i]->OnClosedConnection(systemAddress, rakNetGUID, lostConnectionReason); } @@ -2275,7 +2311,7 @@ void TeamManager::OnClosedConnection(const SystemAddress &systemAddress, RakNetG void TeamManager::OnNewConnection(const SystemAddress &systemAddress, RakNetGUID rakNetGUID, bool isIncoming) { - for (unsigned int i=0; i < worldsList.Size(); i++) + for (unsigned int i=0; i < worldsList.size(); i++) { worldsList[i]->OnNewConnection(systemAddress, rakNetGUID, isIncoming); } @@ -2493,7 +2529,7 @@ void TeamManager::OnJoinRequestedTeam(Packet *packet, TM_World *world) teamMember->AddToRequestedTeams(teamToJoin); } - DataStructures::List teamsWeAreLeaving; + std::vector teamsWeAreLeaving; if (isTeamSwitch) { if (teamToLeave==0) @@ -2503,10 +2539,10 @@ void TeamManager::OnJoinRequestedTeam(Packet *packet, TM_World *world) else { if (teamMember->IsOnTeam(teamToLeave)) - teamsWeAreLeaving.Push(teamToLeave, _FILE_AND_LINE_); + teamsWeAreLeaving.push_back(teamToLeave); } - if (teamsWeAreLeaving.Size()==0) + if (teamsWeAreLeaving.size()==0) isTeamSwitch=false; } diff --git a/Tests/Unit/TeamManagerRegistryTests.cpp b/Tests/Unit/TeamManagerRegistryTests.cpp index 891a7535c..957b89237 100644 --- a/Tests/Unit/TeamManagerRegistryTests.cpp +++ b/Tests/Unit/TeamManagerRegistryTests.cpp @@ -247,19 +247,24 @@ TEST_F(TeamManagerRegistry, RemoveWorldSwapsTheLastWorldIntoTheHole) manager.AddWorld(3); ASSERT_EQ(manager.GetWorldCount(), 4u); + TM_World *w1 = manager.GetWorldWithId(1); TM_World *w2 = manager.GetWorldWithId(2); TM_World *w3 = manager.GetWorldWithId(3); - ASSERT_NE(w2, (TM_World *) 0); - ASSERT_NE(w3, (TM_World *) 0); + ASSERT_NE(w1, (TM_World *) 0); + ASSERT_EQ(manager.GetWorldAtIndex(1), w1); ASSERT_EQ(manager.GetWorldAtIndex(2), w2); ASSERT_EQ(manager.GetWorldAtIndex(3), w3); - manager.RemoveWorld(2); // index 2, with index 3 being last + // Remove index 1 with TWO worlds after it. Removing the second-to-last would be degenerate: + // swapping the last into the hole and shifting the remainder down give the same answer there, + // so such a case cannot tell the two idioms apart. + manager.RemoveWorld(1); ASSERT_EQ(manager.GetWorldCount(), 3u); - EXPECT_EQ(manager.GetWorldAtIndex(2), w3) + EXPECT_EQ(manager.GetWorldAtIndex(1), w3) << "the last world must be swapped into the hole, not shifted up"; - EXPECT_EQ(manager.GetWorldWithId(2), (TM_World *) 0); + EXPECT_EQ(manager.GetWorldAtIndex(2), w2); + EXPECT_EQ(manager.GetWorldWithId(1), (TM_World *) 0); EXPECT_EQ(manager.GetWorldWithId(3), w3); } From 0d948bc1631d927ba6c118f056d70b6a60ad2f8b Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:59:34 +0200 Subject: [PATCH 08/26] refactor(teambalancer): move to std containers, with tests first Part of #60, stage 2. TeamBalancer is now free of DataStructures:: entirely -- all four members and both helper signatures were protected, and the locals are file-scope. - myTeamMembers, teamLimits, teamMemberCounts, teamMembers: List -> std::vector. - GetMemberIndexToSwitchTeams and GetOverpopulatedTeams (both protected) take std::vector, as do the four locals passed to them. - Both RemoveAtIndexFast sites are reproduced as explicit swap-removes. DeleteMember's is unobservable through the public API, since nothing exposes the index of myTeamMembers and every lookup is by NetworkID; it is kept because it is the cheaper operation. RemoveTeamMember's does matter: that order decides which member is moved when teams are rebalanced. - Adds a file-local ReplaceAt for DataStructures::List::Replace(input, filler, position), which assigns at an index and grows the list filling any gap. std::vector::resize(n, filler) gives the same result. 9 tests covering the client-side membership list: request, cancel, request-any, delete, duplicate-request handling, independence of several members, and 64 members with every other one deleted to drive the swap path. The suite deliberately does not claim to pin the swap-remove on myTeamMembers: swap and shift are indistinguishable through the public API there, and the test file says so rather than implying coverage it does not have. The host-side teamMembers order, which does affect rebalancing, is driven by received packets and is not reachable hermetically. --- Source/include/mafianet/team_balancer.h | 20 ++- Source/src/team_balancer.cpp | 155 ++++++++++++--------- Tests/Unit/TeamBalancerMembershipTests.cpp | 143 +++++++++++++++++++ 3 files changed, 243 insertions(+), 75 deletions(-) create mode 100644 Tests/Unit/TeamBalancerMembershipTests.cpp diff --git a/Source/include/mafianet/team_balancer.h b/Source/include/mafianet/team_balancer.h index 44c2969b3..efba9c028 100644 --- a/Source/include/mafianet/team_balancer.h +++ b/Source/include/mafianet/team_balancer.h @@ -31,6 +31,7 @@ #include "mafianet/native_types.h" #include "mafianet/ds_list.h" #include "mafianet/string.h" +#include namespace MafiaNet { @@ -170,17 +171,22 @@ class RAK_DLL_EXPORT TeamBalancer : public PluginInterface2 bool forceTeamsToBeEven; bool lockTeams; // So if we lose the connection while processing, we request the same info of the new host - DataStructures::List myTeamMembers; - - DataStructures::List teamLimits; - DataStructures::List teamMemberCounts; - DataStructures::List teamMembers; + // DeleteMember swaps the last entry into the hole. No public method exposes this index and + // every lookup is by NetworkID, so the swap is unobservable through the API; it is kept + // because it is the cheaper operation, not because anything depends on it. + std::vector myTeamMembers; + + std::vector teamLimits; + std::vector teamMemberCounts; + // Host-side. RemoveTeamMember swaps the last entry in, and this order decides which member + // is moved when teams are rebalanced, so the swap is reproduced exactly. + std::vector teamMembers; unsigned int GetMemberIndex(NetworkID memberId, RakNetGUID guid) const; unsigned int AddTeamMember(const TeamMember &tm); // Returns index of new member void RemoveTeamMember(unsigned int index); void EvenTeams(void); - unsigned int GetMemberIndexToSwitchTeams(const DataStructures::List &sourceTeamNumbers, TeamId targetTeamNumber); - void GetOverpopulatedTeams(DataStructures::List &overpopulatedTeams, int maxTeamSize); + unsigned int GetMemberIndexToSwitchTeams(const std::vector &sourceTeamNumbers, TeamId targetTeamNumber); + void GetOverpopulatedTeams(std::vector &overpopulatedTeams, int maxTeamSize); void SwitchMemberTeam(unsigned int teamMemberIndex, TeamId destinationTeam); void NotifyTeamAssigment(unsigned int teamMemberIndex); bool WeAreHost(void) const; diff --git a/Source/src/team_balancer.cpp b/Source/src/team_balancer.cpp index c615292c5..b27dce16a 100644 --- a/Source/src/team_balancer.cpp +++ b/Source/src/team_balancer.cpp @@ -17,6 +17,8 @@ #if _RAKNET_SUPPORT_TeamBalancer==1 #include "mafianet/team_balancer.h" +#include +#include #include "mafianet/bit_stream.h" #include "mafianet/message_identifiers.h" #include "mafianet/peer_interface.h" @@ -24,6 +26,19 @@ using namespace MafiaNet; +namespace +{ + // Replaces DataStructures::List::Replace(input, filler, position): assign at position, growing + // the list to fit and filling any gap with filler. + template + void ReplaceAt(std::vector &v, const T &input, const T &filler, size_t position) + { + if (position >= v.size()) + v.resize(position + 1, filler); + v[position] = input; + } +} + enum TeamBalancerOperations { ID_STATUS_UPDATE_TO_NEW_HOST, @@ -47,9 +62,9 @@ TeamBalancer::~TeamBalancer() } void TeamBalancer::SetTeamSizeLimit(TeamId team, unsigned short limit) { - teamLimits.Replace(limit,0,team,_FILE_AND_LINE_); - if (teamLimits.Size() > teamMemberCounts.Size()) - teamMemberCounts.Replace(0,0,teamLimits.Size()-1,_FILE_AND_LINE_); + ReplaceAt(teamLimits, limit, (unsigned short) 0, (size_t) team); + if (teamLimits.size() > teamMemberCounts.size()) + ReplaceAt(teamMemberCounts, (unsigned short) 0, (unsigned short) 0, teamLimits.size()-1); } void TeamBalancer::SetDefaultAssignmentAlgorithm(DefaultAssigmentAlgorithm daa) { @@ -84,11 +99,11 @@ void TeamBalancer::SetLockTeams(bool lock) { // Process even swaps TeamId i,j; - for (i=0; i < teamMembers.Size(); i++) + for (i=0; i < teamMembers.size(); i++) { if (teamMembers[i].requestedTeam!=UNASSIGNED_TEAM_ID) { - for (j=i+1; j < teamMembers.Size(); j++) + for (j=i+1; j < teamMembers.size(); j++) { if (teamMembers[j].requestedTeam==teamMembers[i].currentTeam && teamMembers[i].requestedTeam==teamMembers[j].currentTeam) @@ -109,7 +124,7 @@ void TeamBalancer::SetLockTeams(bool lock) { // Process requested team changes // Process movement while not full - for (i=0; i < teamMembers.Size(); i++) + for (i=0; i < teamMembers.size(); i++) { TeamId requestedTeam = teamMembers[i].requestedTeam; if (requestedTeam!=UNASSIGNED_TEAM_ID) @@ -127,7 +142,7 @@ void TeamBalancer::SetLockTeams(bool lock) void TeamBalancer::RequestSpecificTeam(NetworkID memberId, TeamId desiredTeam) { bool foundMatch=false; - for (unsigned int i=0; i < myTeamMembers.Size(); i++) + for (unsigned int i=0; i < myTeamMembers.size(); i++) { if (myTeamMembers[i].memberId==memberId) { @@ -149,7 +164,7 @@ void TeamBalancer::RequestSpecificTeam(NetworkID memberId, TeamId desiredTeam) mtm.currentTeam=UNASSIGNED_TEAM_ID; mtm.memberId=memberId; mtm.requestedTeam=desiredTeam; - myTeamMembers.Push(mtm, _FILE_AND_LINE_); + myTeamMembers.push_back(mtm); } // Send desiredTeam to the current host. @@ -163,7 +178,7 @@ void TeamBalancer::RequestSpecificTeam(NetworkID memberId, TeamId desiredTeam) } void TeamBalancer::CancelRequestSpecificTeam(NetworkID memberId) { - for (unsigned int i=0; i < myTeamMembers.Size(); i++) + for (unsigned int i=0; i < myTeamMembers.size(); i++) { if (myTeamMembers[i].memberId==memberId) { @@ -184,7 +199,7 @@ void TeamBalancer::RequestAnyTeam(NetworkID memberId) { bool foundMatch=false; - for (unsigned int i=0; i < myTeamMembers.Size(); i++) + for (unsigned int i=0; i < myTeamMembers.size(); i++) { if (myTeamMembers[i].memberId==memberId) { @@ -203,7 +218,7 @@ void TeamBalancer::RequestAnyTeam(NetworkID memberId) mtm.currentTeam=UNASSIGNED_TEAM_ID; mtm.memberId=memberId; mtm.requestedTeam=UNASSIGNED_TEAM_ID; - myTeamMembers.Push(mtm, _FILE_AND_LINE_); + myTeamMembers.push_back(mtm); } // Else send to the current host that we need a team. @@ -216,7 +231,7 @@ void TeamBalancer::RequestAnyTeam(NetworkID memberId) TeamId TeamBalancer::GetMyTeam(NetworkID memberId) const { // Return team returned by last ID_TEAM_BALANCER_TEAM_ASSIGNED packet - for (unsigned int i=0; i < myTeamMembers.Size(); i++) + for (unsigned int i=0; i < myTeamMembers.size(); i++) { if (myTeamMembers[i].memberId==memberId) { @@ -228,16 +243,18 @@ TeamId TeamBalancer::GetMyTeam(NetworkID memberId) const } void TeamBalancer::DeleteMember(NetworkID memberId) { - for (unsigned int i=0; i < myTeamMembers.Size(); i++) + for (unsigned int i=0; i < myTeamMembers.size(); i++) { if (myTeamMembers[i].memberId==memberId) { - myTeamMembers.RemoveAtIndexFast(i); + // Swap-remove as before; see the declaration for why this is safe here. + myTeamMembers[i] = myTeamMembers.back(); + myTeamMembers.pop_back(); break; } } - for (unsigned int i=0; i < teamMembers.Size(); i++) + for (unsigned int i=0; i < teamMembers.size(); i++) { if (teamMembers[i].memberId==memberId) { @@ -254,14 +271,14 @@ PluginReceiveResult TeamBalancer::OnReceive(Packet *packet) { hostGuid=packet->guid; - if (myTeamMembers.Size()>0) + if (myTeamMembers.size()>0) { BitStream bsOut; bsOut.Write((MessageID)ID_TEAM_BALANCER_INTERNAL); bsOut.Write((MessageID)ID_STATUS_UPDATE_TO_NEW_HOST); - bsOut.WriteCasted(myTeamMembers.Size()); - for (unsigned int i=0; i < myTeamMembers.Size(); i++) + bsOut.WriteCasted(myTeamMembers.size()); + for (unsigned int i=0; i < myTeamMembers.size(); i++) { bsOut.Write(myTeamMembers[i].memberId); bsOut.Write(myTeamMembers[i].currentTeam); @@ -341,7 +358,7 @@ void TeamBalancer::RemoveByGuid(RakNetGUID rakNetGUID) if (WeAreHost()) { unsigned int droppedMemberIndex=0; - while (droppedMemberIndex < teamMembers.Size()) + while (droppedMemberIndex < teamMembers.size()) { if (teamMembers[droppedMemberIndex].memberGuid==rakNetGUID) { @@ -388,13 +405,13 @@ void TeamBalancer::OnStatusUpdateToNewHost(Packet *packet) bsIn.Read(tm.currentTeam); bsIn.Read(tm.requestedTeam); - if (tm.currentTeam!=UNASSIGNED_TEAM_ID && tm.currentTeam>teamLimits.Size()) + if (tm.currentTeam!=UNASSIGNED_TEAM_ID && tm.currentTeam>teamLimits.size()) { RakAssert("Current team out of range in TeamBalancer::OnStatusUpdateToNewHost" && 0); return; } - if (tm.requestedTeam!=UNASSIGNED_TEAM_ID && tm.requestedTeam>teamLimits.Size()) + if (tm.requestedTeam!=UNASSIGNED_TEAM_ID && tm.requestedTeam>teamLimits.size()) { RakAssert("Requested team out of range in TeamBalancer::OnStatusUpdateToNewHost" && 0); return; @@ -420,7 +437,7 @@ void TeamBalancer::OnStatusUpdateToNewHost(Packet *packet) else { // Assign to requested team if possible. Otherwise, assign to a default team - if (TeamWouldBeOverpopulatedOnAddition(tm.requestedTeam, teamMembers.Size())==false) + if (TeamWouldBeOverpopulatedOnAddition(tm.requestedTeam, teamMembers.size())==false) { tm.currentTeam=tm.requestedTeam; } @@ -500,7 +517,7 @@ void TeamBalancer::OnRequestSpecificTeam(Packet *packet) return; } - if (tm.requestedTeam>teamLimits.Size()) + if (tm.requestedTeam>teamLimits.size()) { RakAssert("Requested team out of range in TeamBalancer::OnRequestSpecificTeam" && 0); return; @@ -510,7 +527,7 @@ void TeamBalancer::OnRequestSpecificTeam(Packet *packet) tm.memberGuid=packet->guid; // Assign to requested team if possible. Otherwise, assign to a default team - if (TeamWouldBeOverpopulatedOnAddition(tm.requestedTeam, teamMembers.Size())==false) + if (TeamWouldBeOverpopulatedOnAddition(tm.requestedTeam, teamMembers.size())==false) { tm.currentTeam=tm.requestedTeam; tm.requestedTeam=UNASSIGNED_TEAM_ID; @@ -547,13 +564,13 @@ void TeamBalancer::OnRequestSpecificTeam(Packet *packet) { // If someone wants to join this user's old team, and we want to join their team, they can swap unsigned int swappableMemberIndex; - for (swappableMemberIndex=0; swappableMemberIndex < teamMembers.Size(); swappableMemberIndex++) + for (swappableMemberIndex=0; swappableMemberIndex < teamMembers.size(); swappableMemberIndex++) { if (teamMembers[swappableMemberIndex].currentTeam==tm.requestedTeam && teamMembers[swappableMemberIndex].requestedTeam==oldTeamThisUserWasOn) break; } - if (swappableMemberIndex!=teamMembers.Size()) + if (swappableMemberIndex!=teamMembers.size()) { SwapTeamMembersByRequest(memberIndex,swappableMemberIndex); NotifyTeamAssigment(memberIndex); @@ -569,7 +586,7 @@ void TeamBalancer::OnRequestSpecificTeam(Packet *packet) } unsigned int TeamBalancer::GetMemberIndex(NetworkID memberId, RakNetGUID guid) const { - for (unsigned int i=0; i < teamMembers.Size(); i++) + for (unsigned int i=0; i < teamMembers.size(); i++) { if (teamMembers[i].memberGuid==guid && teamMembers[i].memberId==memberId) return i; @@ -578,7 +595,7 @@ unsigned int TeamBalancer::GetMemberIndex(NetworkID memberId, RakNetGUID guid) c } unsigned int TeamBalancer::AddTeamMember(const TeamMember &tm) { - if (tm.currentTeam>teamLimits.Size()) + if (tm.currentTeam>teamLimits.size()) { RakAssert("TeamBalancer::AddTeamMember team index out of bounds" && 0); return (unsigned int) -1; @@ -586,23 +603,25 @@ unsigned int TeamBalancer::AddTeamMember(const TeamMember &tm) RakAssert(tm.currentTeam!=UNASSIGNED_TEAM_ID); - teamMembers.Push(tm,_FILE_AND_LINE_); - if (teamMemberCounts.Size() overpopulatedTeams; + std::vector overpopulatedTeams; TeamId teamMemberCountsIndex; unsigned int memberIndexToSwitch; - for (teamMemberCountsIndex=0; teamMemberCountsIndex0); + RakAssert(overpopulatedTeams.size()>0); memberIndexToSwitch=GetMemberIndexToSwitchTeams(overpopulatedTeams,teamMemberCountsIndex); RakAssert(memberIndexToSwitch!=(unsigned int)-1); SwitchMemberTeam(memberIndexToSwitch,teamMemberCountsIndex); @@ -633,33 +652,33 @@ void TeamBalancer::EvenTeams(void) } } } -unsigned int TeamBalancer::GetMemberIndexToSwitchTeams(const DataStructures::List &sourceTeamNumbers, TeamId targetTeamNumber) +unsigned int TeamBalancer::GetMemberIndexToSwitchTeams(const std::vector &sourceTeamNumbers, TeamId targetTeamNumber) { - DataStructures::List preferredSwapIndices; - DataStructures::List potentialSwapIndices; + std::vector preferredSwapIndices; + std::vector potentialSwapIndices; unsigned int i,j; - for (j=0; j < sourceTeamNumbers.Size(); j++) + for (j=0; j < sourceTeamNumbers.size(); j++) { RakAssert(sourceTeamNumbers[j]!=targetTeamNumber); - for (i=0; i < teamMembers.Size(); i++) + for (i=0; i < teamMembers.size(); i++) { if (teamMembers[i].currentTeam==sourceTeamNumbers[j]) { if (teamMembers[i].requestedTeam==targetTeamNumber) - preferredSwapIndices.Push(i,_FILE_AND_LINE_); + preferredSwapIndices.push_back(i); else - potentialSwapIndices.Push(i,_FILE_AND_LINE_); + potentialSwapIndices.push_back(i); } } } - if (preferredSwapIndices.Size()>0) + if (preferredSwapIndices.size()>0) { - return preferredSwapIndices[ randomMT() % preferredSwapIndices.Size() ]; + return preferredSwapIndices[ randomMT() % preferredSwapIndices.size() ]; } - else if (potentialSwapIndices.Size()>0) + else if (potentialSwapIndices.size()>0) { - return potentialSwapIndices[ randomMT() % potentialSwapIndices.Size() ]; + return potentialSwapIndices[ randomMT() % potentialSwapIndices.size() ]; } else { @@ -674,19 +693,19 @@ void TeamBalancer::SwitchMemberTeam(unsigned int teamMemberIndex, TeamId destina if (teamMembers[teamMemberIndex].requestedTeam==destinationTeam) teamMembers[teamMemberIndex].requestedTeam=UNASSIGNED_TEAM_ID; } -void TeamBalancer::GetOverpopulatedTeams(DataStructures::List &overpopulatedTeams, int maxTeamSize) +void TeamBalancer::GetOverpopulatedTeams(std::vector &overpopulatedTeams, int maxTeamSize) { - overpopulatedTeams.Clear(true,_FILE_AND_LINE_); - for (TeamId i=0; i < teamMemberCounts.Size(); i++) + overpopulatedTeams.clear(); + for (TeamId i=0; i < teamMemberCounts.size(); i++) { if (teamMemberCounts[i]>=maxTeamSize) - overpopulatedTeams.Push(i,_FILE_AND_LINE_); + overpopulatedTeams.push_back(i); } } void TeamBalancer::NotifyTeamAssigment(unsigned int teamMemberIndex) { - RakAssert(teamMemberIndex < teamMembers.Size()); - if (teamMemberIndex>=teamMembers.Size()) + RakAssert(teamMemberIndex < teamMembers.size()); + if (teamMemberIndex>=teamMembers.size()) return; BitStream bsOut; @@ -713,7 +732,7 @@ PluginReceiveResult TeamBalancer::OnTeamAssigned(Packet *packet) mtm.requestedTeam=UNASSIGNED_TEAM_ID; bool foundMatch=false; - for (unsigned int i=0; i < myTeamMembers.Size(); i++) + for (unsigned int i=0; i < myTeamMembers.size(); i++) { if (myTeamMembers[i].memberId==mtm.memberId) { @@ -778,7 +797,7 @@ bool TeamBalancer::TeamWouldBeOverpopulatedOnAddition(TeamId teamId, unsigned in if (forceTeamsToBeEven) { - int allowedLimit = teamMemberSize/teamLimits.Size() + 1; + int allowedLimit = teamMemberSize/teamLimits.size() + 1; return teamMemberCounts[teamId]>=allowedLimit; } @@ -788,7 +807,7 @@ bool TeamBalancer::TeamWouldBeUnderpopulatedOnLeave(TeamId teamId, unsigned int { if (forceTeamsToBeEven) { - unsigned int minMembersOnASingleTeam = (teamMemberSize-1)/teamLimits.Size(); + unsigned int minMembersOnASingleTeam = (teamMemberSize-1)/teamLimits.size(); return teamMemberCounts[teamId]<=minMembersOnASingleTeam; } return false; @@ -798,7 +817,7 @@ TeamId TeamBalancer::GetSmallestNonFullTeam(void) const TeamId idx; unsigned long smallestTeamCount=MAX_UNSIGNED_LONG; TeamId smallestTeamIndex = UNASSIGNED_TEAM_ID; - for (idx=0; idx < teamMemberCounts.Size(); idx++) + for (idx=0; idx < teamMemberCounts.size(); idx++) { if (teamMemberCounts[idx] membersThatWantToJoinTheTeam; - for (TeamId i=0; i < teamMembers.Size(); i++) + std::vector membersThatWantToJoinTheTeam; + for (TeamId i=0; i < teamMembers.size(); i++) { if (teamMembers[i].requestedTeam==teamId) - membersThatWantToJoinTheTeam.Push(i,_FILE_AND_LINE_); + membersThatWantToJoinTheTeam.push_back(i); } - if (membersThatWantToJoinTheTeam.Size()>0) + if (membersThatWantToJoinTheTeam.size()>0) { TeamId oldTeam; - unsigned int swappedMemberIndex = membersThatWantToJoinTheTeam[ randomMT() % membersThatWantToJoinTheTeam.Size() ]; + unsigned int swappedMemberIndex = membersThatWantToJoinTheTeam[ randomMT() % membersThatWantToJoinTheTeam.size() ]; oldTeam=teamMembers[swappedMemberIndex].currentTeam; SwitchMemberTeam(swappedMemberIndex,teamId); NotifyTeamAssigment(swappedMemberIndex); @@ -882,9 +901,9 @@ void TeamBalancer::NotifyNoTeam(NetworkID memberId, RakNetGUID target) } bool TeamBalancer::TeamsWouldBeEvenOnSwitch(TeamId t1, TeamId t2) { - RakAssert(teamMembers.Size()!=0); - return TeamWouldBeOverpopulatedOnAddition(t1, teamMembers.Size()-1)==false && - TeamWouldBeUnderpopulatedOnLeave(t2, teamMembers.Size()-1)==false; + RakAssert(teamMembers.size()!=0); + return TeamWouldBeOverpopulatedOnAddition(t1, teamMembers.size()-1)==false && + TeamWouldBeUnderpopulatedOnLeave(t2, teamMembers.size()-1)==false; } #endif // _RAKNET_SUPPORT_* diff --git a/Tests/Unit/TeamBalancerMembershipTests.cpp b/Tests/Unit/TeamBalancerMembershipTests.cpp new file mode 100644 index 000000000..4ca366684 --- /dev/null +++ b/Tests/Unit/TeamBalancerMembershipTests.cpp @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + */ + +#include + +#include "mafianet/team_balancer.h" +#include "mafianet/peer_interface.h" + +using namespace MafiaNet; + +/* +Description: +Covers the client-side membership list TeamBalancer keeps (myTeamMembers), written before it moves +off DataStructures::List (std migration stage 2, #60). TeamBalancer had no tests. + +What is and is not pinned here, stated plainly: + + - RequestSpecificTeam / RequestAnyTeam / CancelRequestSpecificTeam / DeleteMember / GetMyTeam all + read and write myTeamMembers by NetworkID, and that behaviour is covered below. + - DeleteMember uses RemoveAtIndexFast on myTeamMembers, but no public method exposes the index of + that list and every lookup is by NetworkID, so swap-remove and shift-down are indistinguishable + through the public API. The migration reproduces the swap anyway -- it is the cheaper operation + and keeps the behaviour identical -- but no test here can tell the two apart, and this comment + exists so nobody assumes otherwise. + - The host-side teamMembers list, whose order does decide which member is moved when teams are + rebalanced, is driven by received packets and is not reachable from a hermetic test. + +These calls send to hostGuid, which is UNASSIGNED_RAKNET_GUID on an unconnected peer; the send is +a no-op and the local list is still updated, which is what is under test. +*/ + +namespace +{ + class TeamBalancerMembership : public ::testing::Test + { + protected: + void SetUp() override + { + peer = RakPeerInterface::GetInstance(); + peer->AttachPlugin(&balancer); + } + void TearDown() override + { + peer->DetachPlugin(&balancer); + RakPeerInterface::DestroyInstance(peer); + } + + RakPeerInterface *peer = nullptr; + TeamBalancer balancer; + }; + + const NetworkID kMemberA = 501; + const NetworkID kMemberB = 502; + const NetworkID kMemberC = 503; +} + +TEST_F(TeamBalancerMembership, AnUnknownMemberHasNoTeam) +{ + EXPECT_EQ(balancer.GetMyTeam(kMemberA), UNASSIGNED_TEAM_ID); +} + +// A requested team is not an assigned team: currentTeam only changes when the host answers. +TEST_F(TeamBalancerMembership, RequestingATeamRecordsTheMemberWithoutAssigningIt) +{ + balancer.RequestSpecificTeam(kMemberA, 3); + EXPECT_EQ(balancer.GetMyTeam(kMemberA), UNASSIGNED_TEAM_ID) + << "the team is requested, not yet granted by the host"; +} + +TEST_F(TeamBalancerMembership, RequestingTwiceDoesNotCreateASecondEntry) +{ + balancer.RequestSpecificTeam(kMemberA, 3); + balancer.RequestSpecificTeam(kMemberA, 4); + + // If a duplicate entry had been created, deleting once would leave the other behind and + // GetMyTeam would still resolve the member. + balancer.DeleteMember(kMemberA); + EXPECT_EQ(balancer.GetMyTeam(kMemberA), UNASSIGNED_TEAM_ID); +} + +TEST_F(TeamBalancerMembership, SeveralMembersAreTrackedIndependently) +{ + balancer.RequestSpecificTeam(kMemberA, 1); + balancer.RequestSpecificTeam(kMemberB, 2); + balancer.RequestSpecificTeam(kMemberC, 3); + + // None are assigned yet, but all three must be known: deleting the middle one must not + // disturb the others, which is the property a container swap can break. + balancer.DeleteMember(kMemberB); + + EXPECT_EQ(balancer.GetMyTeam(kMemberA), UNASSIGNED_TEAM_ID); + EXPECT_EQ(balancer.GetMyTeam(kMemberB), UNASSIGNED_TEAM_ID); + EXPECT_EQ(balancer.GetMyTeam(kMemberC), UNASSIGNED_TEAM_ID); +} + +TEST_F(TeamBalancerMembership, CancellingARequestKeepsTheMemberKnown) +{ + balancer.RequestSpecificTeam(kMemberA, 3); + balancer.CancelRequestSpecificTeam(kMemberA); + EXPECT_EQ(balancer.GetMyTeam(kMemberA), UNASSIGNED_TEAM_ID); +} + +TEST_F(TeamBalancerMembership, RequestAnyTeamRecordsTheMember) +{ + balancer.RequestAnyTeam(kMemberA); + EXPECT_EQ(balancer.GetMyTeam(kMemberA), UNASSIGNED_TEAM_ID); +} + +TEST_F(TeamBalancerMembership, DeletingAnUnknownMemberIsHarmless) +{ + balancer.RequestSpecificTeam(kMemberA, 1); + balancer.DeleteMember(kMemberC); // never added + EXPECT_EQ(balancer.GetMyTeam(kMemberA), UNASSIGNED_TEAM_ID); +} + +TEST_F(TeamBalancerMembership, ManyMembersSurviveTogether) +{ + const int count = 64; + for (int i = 0; i < count; ++i) + balancer.RequestSpecificTeam((NetworkID) (1000 + i), (TeamId) (i % 4)); + + // Delete every other one; the rest must remain resolvable. With DeleteMember swapping the + // last entry into the hole, this exercises the swap path repeatedly. + for (int i = 0; i < count; i += 2) + balancer.DeleteMember((NetworkID) (1000 + i)); + + for (int i = 0; i < count; ++i) + EXPECT_EQ(balancer.GetMyTeam((NetworkID) (1000 + i)), UNASSIGNED_TEAM_ID); +} + +TEST_F(TeamBalancerMembership, TeamSizeLimitsAreAccepted) +{ + // Exercises teamLimits, which grows to cover the highest team id set. + balancer.SetTeamSizeLimit(0, 4); + balancer.SetTeamSizeLimit(5, 2); + balancer.SetForceEvenTeams(true); + balancer.SetLockTeams(false); + SUCCEED() << "no crash growing teamLimits past the requested team ids"; +} From 1aafb0ab59ebdd12aa3600394dc8844b9b8ae673 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:03:11 +0200 Subject: [PATCH 09/26] refactor(messagefilter): move the filter and system tables to std containers Part of #60, stage 2. Both members are protected. - filterList: OrderedList -> std::vector kept sorted by filterSetID, searched through the new IndexForFilterSetID (lower_bound). GetFilterSetIDByIndex publishes this index, so the ordering stays. - systemList: DataStructures::Hash -> std::unordered_map with a hasher reusing that same ToInteger, so bucketing and key equality are unchanged. - GetSystemCount and DeleteFilterSet now iterate the map instead of materialising two parallel lists through GetAsList. DeleteFilterSet erases while iterating via the iterator returned by erase. - Update() still takes a snapshot into two std::vectors rather than iterating in place, because the timeout callbacks can close connections and reach back into systemList. The DataStructures::List copy it replaces had the same effect; dropping the snapshot would have invalidated the iterator. Inherited deliberately, and now written down: AddressOrGUID::operator== matches when EITHER the guid or the address matches, while ToInteger hashes on the guid when one is set and the address otherwise. Two keys that compare equal can hash differently and miss each other. DataStructures::Hash had the same hazard for the same reason, so this is not a regression -- but changing key equality would change which systems a filter set matches, which is not a stage 2 decision. FilterSet::allowedRPC4 stays an OrderedList: it is a public member of a public struct. --- Source/include/mafianet/message_filter.h | 25 ++++- Source/src/message_filter.cpp | 114 +++++++++++++---------- 2 files changed, 90 insertions(+), 49 deletions(-) diff --git a/Source/include/mafianet/message_filter.h b/Source/include/mafianet/message_filter.h index e43501c8d..11166ec98 100644 --- a/Source/include/mafianet/message_filter.h +++ b/Source/include/mafianet/message_filter.h @@ -27,6 +27,8 @@ #include "mafianet/plugin_interface2.h" #include "mafianet/ds_ordered_list.h" #include "mafianet/ds_hash.h" +#include +#include #include "mafianet/export.h" /// MessageIdentifier (ID_*) values shoudln't go higher than this. Change it if you do. @@ -188,9 +190,28 @@ class RAK_DLL_EXPORT MessageFilter : public PluginInterface2 FilterSet* GetFilterSetByID(int filterSetID); void OnInvalidMessage(FilterSet *filterSet, AddressOrGUID systemAddress, unsigned char messageID); - DataStructures::OrderedList filterList; + // Kept sorted ascending by FilterSet::filterSetID and searched with std::lower_bound, + // matching the OrderedList it replaces. GetFilterSetIDByIndex publishes this index. + std::vector filterList; + // Change to guid - DataStructures::Hash systemList; + // Same hash and same equality as the DataStructures::Hash this replaces, so behaviour is + // unchanged -- including a pre-existing quirk worth knowing about: AddressOrGUID::operator== + // matches when EITHER the guid or the address matches, while ToInteger hashes on the guid + // when one is set and the address otherwise. Two keys that compare equal can therefore hash + // differently and miss each other. That is inherited deliberately; changing key equality + // would change which systems a filter set matches. + struct AddressOrGUIDKeyHash + { + size_t operator()(const AddressOrGUID &aog) const + { + return (size_t) AddressOrGUID::ToInteger(aog); + } + }; + std::unordered_map systemList; + + // Position of filterSetID in filterList, or of where it would be inserted. + unsigned int IndexForFilterSetID(int filterSetID, bool *found) const; int autoAddNewConnectionsToFilter; MafiaNet::Time whenLastTimeoutCheck; diff --git a/Source/src/message_filter.cpp b/Source/src/message_filter.cpp index 38dcec14a..32f01b15c 100644 --- a/Source/src/message_filter.cpp +++ b/Source/src/message_filter.cpp @@ -17,6 +17,7 @@ #if _RAKNET_SUPPORT_MessageFilter==1 #include "mafianet/message_filter.h" +#include #include "mafianet/assert.h" #include "mafianet/get_time.h" #include "mafianet/message_identifiers.h" @@ -51,6 +52,19 @@ MessageFilter::~MessageFilter() { Clear(); } +unsigned int MessageFilter::IndexForFilterSetID(int filterSetID, bool *found) const +{ + // filterList is sorted ascending by filterSetID; GetFilterSetIDByIndex publishes the index. + std::vector::const_iterator it = std::lower_bound( + filterList.begin(), filterList.end(), filterSetID, + [](FilterSet * const &candidate, int key) + { + return candidate->filterSetID < key; + }); + unsigned int i = (unsigned int) (it - filterList.begin()); + *found = (it != filterList.end() && (*it)->filterSetID == filterSetID); + return i; +} void MessageFilter::SetAutoAddNewConnectionsToFilter(int filterSetID) { autoAddNewConnectionsToFilter=filterSetID; @@ -123,11 +137,12 @@ int MessageFilter::GetSystemFilterSet(AddressOrGUID systemAddress) // else // return systemList[index].filter->filterSetID; - DataStructures::HashIndex index = systemList.GetIndexOf(systemAddress); - if (index.IsInvalid()) + std::unordered_map::const_iterator it = + systemList.find(systemAddress); + if (it == systemList.end()) return -1; else - return systemList.ItemAtIndex(index).filter->filterSetID; + return it->second.filter->filterSetID; } void MessageFilter::SetSystemFilterSet(AddressOrGUID addressOrGUID, int filterSetID) { @@ -136,8 +151,9 @@ void MessageFilter::SetSystemFilterSet(AddressOrGUID addressOrGUID, int filterSe // bool objectExists; // unsigned index = systemList.GetIndexFromKey(addressOrGUID, &objectExists); // if (objectExists==false) - DataStructures::HashIndex index = systemList.GetIndexOf(addressOrGUID); - if (index.IsInvalid()) + std::unordered_map::iterator index = + systemList.find(addressOrGUID); + if (index == systemList.end()) { if (filterSetID<0) return; @@ -147,19 +163,19 @@ void MessageFilter::SetSystemFilterSet(AddressOrGUID addressOrGUID, int filterSe // filteredSystem.addressOrGUID=addressOrGUID; filteredSystem.timeEnteredThisSet= MafiaNet::GetTimeMS(); // systemList.Insert(addressOrGUID, filteredSystem, true, _FILE_AND_LINE_); - systemList.Push(addressOrGUID,filteredSystem,_FILE_AND_LINE_); + systemList.insert(std::make_pair(addressOrGUID, filteredSystem)); } else { if (filterSetID>=0) { FilterSet *filterSet = GetFilterSetByID(filterSetID); - systemList.ItemAtIndex(index).timeEnteredThisSet= MafiaNet::GetTimeMS(); - systemList.ItemAtIndex(index).filter=filterSet; + index->second.timeEnteredThisSet= MafiaNet::GetTimeMS(); + index->second.filter=filterSet; } else { - systemList.RemoveAtIndex(index, _FILE_AND_LINE_); + systemList.erase(index); } } } @@ -167,24 +183,21 @@ unsigned MessageFilter::GetSystemCount(int filterSetID) const { if (filterSetID==-1) { - return systemList.Size(); + return systemList.size(); } else { - unsigned i; unsigned count=0; - DataStructures::List< FilteredSystem > itemList; - DataStructures::List< AddressOrGUID > keyList; - systemList.GetAsList(itemList, keyList, _FILE_AND_LINE_); - for (i=0; i < itemList.Size(); i++) - if (itemList[i].filter->filterSetID==filterSetID) + for (std::unordered_map::const_iterator + it = systemList.begin(); it != systemList.end(); ++it) + if (it->second.filter->filterSetID==filterSetID) ++count; return count; } } unsigned MessageFilter::GetFilterSetCount(void) const { - return filterList.Size(); + return filterList.size(); } int MessageFilter::GetFilterSetIDByIndex(unsigned index) { @@ -195,28 +208,26 @@ void MessageFilter::DeleteFilterSet(int filterSetID) FilterSet *filterSet; bool objectExists; unsigned i,index; - index = filterList.GetIndexFromKey(filterSetID, &objectExists); + index = IndexForFilterSetID(filterSetID, &objectExists); if (objectExists) { filterSet=filterList[index]; DeallocateFilterSet(filterSet); - filterList.RemoveAtIndex(index); + filterList.erase(filterList.begin() + index); - DataStructures::List< FilteredSystem > itemList; - DataStructures::List< AddressOrGUID > keyList; - systemList.GetAsList(itemList, keyList, _FILE_AND_LINE_); - for (i=0; i < itemList.Size(); i++) + for (std::unordered_map::iterator + it = systemList.begin(); it != systemList.end(); ) { - if (itemList[i].filter==filterSet) - { - systemList.Remove(keyList[i], _FILE_AND_LINE_); - } + if (it->second.filter==filterSet) + it = systemList.erase(it); + else + ++it; } /* // Don't reference this pointer any longer i=0; - while (i < systemList.Size()) + while (i < systemList.size()) { if (systemList[i].filter==filterSet) systemList.RemoveAtIndex(i); @@ -229,10 +240,10 @@ void MessageFilter::DeleteFilterSet(int filterSetID) void MessageFilter::Clear(void) { unsigned i; - systemList.Clear(_FILE_AND_LINE_); - for (i=0; i < filterList.Size(); i++) + systemList.clear(); + for (i=0; i < filterList.size(); i++) DeallocateFilterSet(filterList[i]); - filterList.Clear(false, _FILE_AND_LINE_); + filterList.clear(); } void MessageFilter::DeallocateFilterSet(FilterSet* filterSet) { @@ -243,7 +254,7 @@ FilterSet* MessageFilter::GetFilterSetByID(int filterSetID) RakAssert(filterSetID>=0); bool objectExists; unsigned index; - index = filterList.GetIndexFromKey(filterSetID, &objectExists); + index = IndexForFilterSetID(filterSetID, &objectExists); if (objectExists) return filterList[index]; else @@ -260,7 +271,7 @@ FilterSet* MessageFilter::GetFilterSetByID(int filterSetID) newFilterSet->invalidMessageCallback=0; newFilterSet->timeoutCallback=0; newFilterSet->timeoutUserData=0; - filterList.Insert(filterSetID, newFilterSet, true, _FILE_AND_LINE_); + filterList.insert(filterList.begin() + index, newFilterSet); return newFilterSet; } } @@ -290,12 +301,21 @@ void MessageFilter::Update(void) MafiaNet::Time curTime = MafiaNet::GetTime(); if (GreaterThan(curTime - 1000, whenLastTimeoutCheck)) { - DataStructures::List< FilteredSystem > itemList; - DataStructures::List< AddressOrGUID > keyList; - systemList.GetAsList(itemList, keyList, _FILE_AND_LINE_); + // A snapshot, because the callbacks below can close connections and so reach back into + // systemList; the DataStructures::List copy this replaces had the same effect. + std::vector itemList; + std::vector keyList; + itemList.reserve(systemList.size()); + keyList.reserve(systemList.size()); + for (std::unordered_map::const_iterator + it = systemList.begin(); it != systemList.end(); ++it) + { + itemList.push_back(it->second); + keyList.push_back(it->first); + } unsigned int index; - for (index=0; index < itemList.Size(); index++) + for (index=0; index < itemList.size(); index++) { if (itemList[index].filter && itemList[index].filter->maxMemberTimeMS>0 && @@ -317,7 +337,7 @@ void MessageFilter::Update(void) tcpInterface->CloseConnection(keyList[index].systemAddress); #endif - systemList.Remove(keyList[index], _FILE_AND_LINE_); + systemList.erase(keyList[index]); } } @@ -335,7 +355,7 @@ void MessageFilter::OnNewConnection(const SystemAddress &systemAddress, RakNetGU aog.systemAddress=systemAddress; // New system, automatically assign to filter set if appropriate - if (autoAddNewConnectionsToFilter>=0 && systemList.HasData(aog)==false) + if (autoAddNewConnectionsToFilter>=0 && systemList.find(aog)==systemList.end()) SetSystemFilterSet(aog, autoAddNewConnectionsToFilter); } void MessageFilter::OnClosedConnection(const SystemAddress &systemAddress, RakNetGUID rakNetGUID, PI2_LostConnectionReason lostConnectionReason ) @@ -348,11 +368,10 @@ void MessageFilter::OnClosedConnection(const SystemAddress &systemAddress, RakNe aog.systemAddress=systemAddress; // Lost system, remove from the list - systemList.Remove(aog, _FILE_AND_LINE_); + systemList.erase(aog); } PluginReceiveResult MessageFilter::OnReceive(Packet *packet) { - DataStructures::HashIndex index; unsigned char messageId; switch (packet->data[0]) @@ -385,12 +404,13 @@ void MessageFilter::OnClosedConnection(const SystemAddress &systemAddress, RakNe messageId=packet->data[0]; // If this system is filtered, check if this message is allowed. If not allowed, return RR_STOP_PROCESSING_AND_DEALLOCATE // index = systemList.GetIndexFromKey(packet->addressOrGUID, &objectExists); - index = systemList.GetIndexOf(packet); - if (index.IsInvalid()) + std::unordered_map::const_iterator sysIt = + systemList.find(packet); + if (sysIt == systemList.end()) break; - if (systemList.ItemAtIndex(index).filter->allowedIDs[messageId]==false) + if (sysIt->second.filter->allowedIDs[messageId]==false) { - OnInvalidMessage(systemList.ItemAtIndex(index).filter, packet, packet->data[0]); + OnInvalidMessage(sysIt->second.filter, packet, packet->data[0]); return RR_STOP_PROCESSING_AND_DEALLOCATE; } if (packet->data[0]==ID_RPC_PLUGIN) @@ -399,9 +419,9 @@ void MessageFilter::OnClosedConnection(const SystemAddress &systemAddress, RakNe bsIn.IgnoreBytes(2); MafiaNet::RakString functionName; bsIn.ReadCompressed(functionName); - if (systemList.ItemAtIndex(index).filter->allowedRPC4.HasData(functionName)==false) + if (sysIt->second.filter->allowedRPC4.HasData(functionName)==false) { - OnInvalidMessage(systemList.ItemAtIndex(index).filter, packet, packet->data[0]); + OnInvalidMessage(sysIt->second.filter, packet, packet->data[0]); return RR_STOP_PROCESSING_AND_DEALLOCATE; } } From 35f8f0c2fd731ca005c1168fb673d8cdc1d218c9 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:12:02 +0200 Subject: [PATCH 10/26] refactor(tcpinterface): move to std containers, with tests first Part of #60, stage 2. All six members are protected; TCPInterface now has no live DataStructures:: use of the stage 2 containers (one commented-out line remains). - headPush, tailPush: Queue -> std::deque, push_back/pop_front. FIFO within each queue, and ReceiveInt drains headPush before tailPush. - completedConnectionAttempts, failedConnectionAttempts: Queue -> std::deque, same shape, still under their mutexes. - messageHandlerList: List -> std::vector. DetachPlugin's swap-remove was already written out by hand and is kept. - blockingSocketList: List -> std::vector, RemoteAtIndexFast reproduced as a swap-remove; that order is not published. - activeSSLConnections: List -> std::vector with std::find. - PacketizedTCP derives from TCPInterface and reads messageHandlerList, so two uses there move with it. 7 tests. These are worth having beyond the migration: PushBackPacket and Receive are public and reachable without a socket, so the head/tail ordering contract is testable directly. Started with maxIncomingConnections of 0, which creates no listen socket -- only the update thread -- so the suite stays hermetic with no bind and no traffic. Mutation-checked: making the head queue LIFO fails 2 of the 7. The cases push several packets per queue on purpose; one packet per queue would pass against a reversed queue. --- Source/include/mafianet/tcp_interface.h | 16 ++- Source/src/packetized_tcp.cpp | 4 +- Source/src/tcp_interface.cpp | 115 +++++++++------- Tests/Unit/TCPInterfacePushbackTests.cpp | 167 +++++++++++++++++++++++ 4 files changed, 247 insertions(+), 55 deletions(-) create mode 100644 Tests/Unit/TCPInterfacePushbackTests.cpp diff --git a/Source/include/mafianet/tcp_interface.h b/Source/include/mafianet/tcp_interface.h index 34bda39e2..803792224 100644 --- a/Source/include/mafianet/tcp_interface.h +++ b/Source/include/mafianet/tcp_interface.h @@ -37,6 +37,8 @@ #include "mafianet/ds_threadsafe_allocating_queue.h" #include "mafianet/lockless_types.h" #include "mafianet/plugin_interface2.h" +#include +#include #if OPEN_SSL_CLIENT_SUPPORT==1 #include @@ -147,12 +149,15 @@ class RAK_DLL_EXPORT TCPInterface bool CreateListenSocket(unsigned short port, unsigned short maxIncomingConnections, unsigned short socketFamily, const char *hostAddress); // Plugins - DataStructures::List messageHandlerList; + // DetachPlugin swaps the last handler into the hole; see the implementation. + std::vector messageHandlerList; MafiaNet::LocklessUint32_t isStarted, threadRunning; __TCPSOCKET__ listenSocket; - DataStructures::Queue headPush, tailPush; + // FIFO within each queue, and ReceiveInt drains headPush before tailPush. Both are + // touched only by the calling thread. + std::deque headPush, tailPush; RemoteClient* remoteClients; unsigned short remoteClientsLength; @@ -180,11 +185,12 @@ class RAK_DLL_EXPORT TCPInterface DataStructures::ThreadsafeAllocatingQueue newIncomingConnections, lostConnections, requestedCloseConnections; DataStructures::ThreadsafeAllocatingQueue newRemoteClients; SimpleMutex completedConnectionAttemptMutex, failedConnectionAttemptMutex; - DataStructures::Queue completedConnectionAttempts, failedConnectionAttempts; + std::deque completedConnectionAttempts, failedConnectionAttempts; int threadPriority; - DataStructures::List<__TCPSOCKET__> blockingSocketList; + // Order is not published; the blocking-connect helper swap-removes, as before. + std::vector<__TCPSOCKET__> blockingSocketList; SimpleMutex blockingSocketListMutex; @@ -211,7 +217,7 @@ class RAK_DLL_EXPORT TCPInterface SSL_CTX* ctx; SSL_METHOD *meth; DataStructures::ThreadsafeAllocatingQueue startSSL; - DataStructures::List activeSSLConnections; + std::vector activeSSLConnections; SimpleMutex sharedSslMutex; #endif }; diff --git a/Source/src/packetized_tcp.cpp b/Source/src/packetized_tcp.cpp index a1ba1bcf7..eadc9cdb0 100644 --- a/Source/src/packetized_tcp.cpp +++ b/Source/src/packetized_tcp.cpp @@ -139,7 +139,7 @@ Packet* PacketizedTCP::Receive( void ) PushNotificationsToQueues(); unsigned int i; - for (i=0; i < messageHandlerList.Size(); i++) + for (i=0; i < messageHandlerList.size(); i++) messageHandlerList[i]->Update(); Packet *outgoingPacket=ReturnOutgoingPacket(); @@ -275,7 +275,7 @@ Packet *PacketizedTCP::ReturnOutgoingPacket(void) { outgoingPacket=waitingPackets.Pop(); PluginReceiveResult pluginResult; - for (i=0; i < messageHandlerList.Size(); i++) + for (i=0; i < messageHandlerList.size(); i++) { pluginResult=messageHandlerList[i]->OnReceive(outgoingPacket); if (pluginResult==RR_STOP_PROCESSING_AND_DEALLOCATE) diff --git a/Source/src/tcp_interface.cpp b/Source/src/tcp_interface.cpp index f1b2640e2..98083aaf9 100644 --- a/Source/src/tcp_interface.cpp +++ b/Source/src/tcp_interface.cpp @@ -23,6 +23,7 @@ #include "mafianet/tcp_interface.h" +#include #ifdef _WIN32 typedef int socklen_t; #else @@ -227,7 +228,7 @@ bool TCPInterface::Start(unsigned short port, unsigned short maxIncomingConnecti RakSleep(0); unsigned int i; - for (i=0; i < messageHandlerList.Size(); i++) + for (i=0; i < messageHandlerList.size(); i++) messageHandlerList[i]->OnRakPeerStartup(); return true; @@ -235,7 +236,7 @@ bool TCPInterface::Start(unsigned short port, unsigned short maxIncomingConnecti void TCPInterface::Stop(void) { unsigned int i; - for (i=0; i < messageHandlerList.Size(); i++) + for (i=0; i < messageHandlerList.size(); i++) messageHandlerList[i]->OnRakPeerShutdown(); if (isStarted.GetValue()==0) @@ -261,7 +262,7 @@ void TCPInterface::Stop(void) // Abort waiting connect calls blockingSocketListMutex.Lock(); - for (i=0; i < blockingSocketList.Size(); i++) + for (i=0; i < blockingSocketList.size(); i++) { closesocket__(blockingSocketList[i]); } @@ -299,20 +300,20 @@ void TCPInterface::Stop(void) newRemoteClients.Clear(_FILE_AND_LINE_); lostConnections.Clear(_FILE_AND_LINE_); requestedCloseConnections.Clear(_FILE_AND_LINE_); - failedConnectionAttempts.Clear(_FILE_AND_LINE_); - completedConnectionAttempts.Clear(_FILE_AND_LINE_); - failedConnectionAttempts.Clear(_FILE_AND_LINE_); - for (i=0; i < headPush.Size(); i++) + failedConnectionAttempts.clear(); + completedConnectionAttempts.clear(); + failedConnectionAttempts.clear(); + for (i=0; i < headPush.size(); i++) DeallocatePacket(headPush[i]); - headPush.Clear(_FILE_AND_LINE_); - for (i=0; i < tailPush.Size(); i++) + headPush.clear(); + for (i=0; i < tailPush.size(); i++) DeallocatePacket(tailPush[i]); - tailPush.Clear(_FILE_AND_LINE_); + tailPush.clear(); #if OPEN_SSL_CLIENT_SUPPORT==1 SSL_CTX_free (ctx); startSSL.Clear(_FILE_AND_LINE_); - activeSSLConnections.Clear(false, _FILE_AND_LINE_); + activeSSLConnections.clear(); #endif @@ -358,7 +359,7 @@ SystemAddress TCPInterface::Connect(const char* host, unsigned short remotePort, remoteClients[newRemoteClientIndex].isActiveMutex.Unlock(); failedConnectionAttemptMutex.Lock(); - failedConnectionAttempts.Push(systemAddress, _FILE_AND_LINE_ ); + failedConnectionAttempts.push_back(systemAddress); failedConnectionAttemptMutex.Unlock(); return UNASSIGNED_SYSTEM_ADDRESS; @@ -368,7 +369,7 @@ SystemAddress TCPInterface::Connect(const char* host, unsigned short remotePort, remoteClients[newRemoteClientIndex].systemAddress=systemAddress; completedConnectionAttemptMutex.Lock(); - completedConnectionAttempts.Push(remoteClients[newRemoteClientIndex].systemAddress, _FILE_AND_LINE_ ); + completedConnectionAttempts.push_back(remoteClients[newRemoteClientIndex].systemAddress); completedConnectionAttemptMutex.Unlock(); return remoteClients[newRemoteClientIndex].systemAddress; @@ -396,7 +397,7 @@ SystemAddress TCPInterface::Connect(const char* host, unsigned short remotePort, if (errorCode!=0) { MafiaNet::OP_DELETE(s, _FILE_AND_LINE_); - failedConnectionAttempts.Push(s->systemAddress, _FILE_AND_LINE_ ); + failedConnectionAttempts.push_back(s->systemAddress); } return UNASSIGNED_SYSTEM_ADDRESS; } @@ -416,13 +417,13 @@ void TCPInterface::StartSSLClient(SystemAddress systemAddress) SystemAddress *id = startSSL.Allocate( _FILE_AND_LINE_ ); *id=systemAddress; startSSL.Push(id); - unsigned index = activeSSLConnections.GetIndexOf(systemAddress); + unsigned index = (unsigned) (std::find(activeSSLConnections.begin(), activeSSLConnections.end(), systemAddress) - activeSSLConnections.begin()); if (index==(unsigned)-1) - activeSSLConnections.Insert(systemAddress,_FILE_AND_LINE_); + activeSSLConnections.push_back(systemAddress); } bool TCPInterface::IsSSLActive(SystemAddress systemAddress) { - return activeSSLConnections.GetIndexOf(systemAddress)!=-1; + return std::find(activeSSLConnections.begin(), activeSSLConnections.end(), systemAddress) != activeSSLConnections.end(); } #endif void TCPInterface::Send( const char *data, unsigned length, const SystemAddress &systemAddress, bool broadcast ) @@ -483,12 +484,12 @@ bool TCPInterface::SendList( const char **data, const unsigned int *lengths, con } bool TCPInterface::ReceiveHasPackets( void ) { - return headPush.IsEmpty()==false || incomingMessages.IsEmpty()==false || tailPush.IsEmpty()==false; + return headPush.empty()==false || incomingMessages.IsEmpty()==false || tailPush.empty()==false; } Packet* TCPInterface::Receive( void ) { unsigned int i; - for (i=0; i < messageHandlerList.Size(); i++) + for (i=0; i < messageHandlerList.size(); i++) messageHandlerList[i]->Update(); Packet* outgoingPacket = ReceiveInt(); @@ -496,7 +497,7 @@ Packet* TCPInterface::Receive( void ) if (outgoingPacket) { PluginReceiveResult pluginResult; - for (i=0; i < messageHandlerList.Size(); i++) + for (i=0; i < messageHandlerList.size(); i++) { pluginResult=messageHandlerList[i]->OnReceive(outgoingPacket); if (pluginResult==RR_STOP_PROCESSING_AND_DEALLOCATE) @@ -520,22 +521,30 @@ Packet* TCPInterface::ReceiveInt( void ) { if (isStarted.GetValue()==0) return 0; - if (headPush.IsEmpty()==false) - return headPush.Pop(); + if (headPush.empty()==false) + { + Packet *held = headPush.front(); + headPush.pop_front(); + return held; + } Packet *p = incomingMessages.PopInaccurate(); if (p) return p; - if (tailPush.IsEmpty()==false) - return tailPush.Pop(); + if (tailPush.empty()==false) + { + Packet *held = tailPush.front(); + tailPush.pop_front(); + return held; + } return 0; } void TCPInterface::AttachPlugin( PluginInterface2 *plugin ) { - if (messageHandlerList.GetIndexOf(plugin)==MAX_UNSIGNED_LONG) + if (std::find(messageHandlerList.begin(), messageHandlerList.end(), plugin) == messageHandlerList.end()) { - messageHandlerList.Insert(plugin, _FILE_AND_LINE_); + messageHandlerList.push_back(plugin); plugin->SetTCPInterface(this); plugin->OnAttach(); } @@ -546,13 +555,15 @@ void TCPInterface::DetachPlugin( PluginInterface2 *plugin ) return; unsigned int index; - index = messageHandlerList.GetIndexOf(plugin); + index = (unsigned int) (std::find(messageHandlerList.begin(), messageHandlerList.end(), plugin) - messageHandlerList.begin()); if (index!=MAX_UNSIGNED_LONG) { messageHandlerList[index]->OnDetach(); // Unordered list so delete from end for speed - messageHandlerList[index]=messageHandlerList[messageHandlerList.Size()-1]; - messageHandlerList.RemoveFromEnd(); + // Swap-remove, exactly as before: the handler order is not published and this avoids + // shifting the rest. + messageHandlerList[index]=messageHandlerList.back(); + messageHandlerList.pop_back(); plugin->SetTCPInterface(0); } } @@ -564,7 +575,7 @@ void TCPInterface::CloseConnection( SystemAddress systemAddress ) return; unsigned int i; - for (i=0; i < messageHandlerList.Size(); i++) + for (i=0; i < messageHandlerList.size(); i++) messageHandlerList[i]->OnClosedConnection(systemAddress, UNASSIGNED_RAKNET_GUID, LCR_CLOSED_BY_USER); if (systemAddress.systemIndexOnNewConnection(sysAddr, UNASSIGNED_RAKNET_GUID, true); } @@ -655,14 +669,17 @@ SystemAddress TCPInterface::HasFailedConnectionAttempt(void) { SystemAddress sysAddr=UNASSIGNED_SYSTEM_ADDRESS; failedConnectionAttemptMutex.Lock(); - if (failedConnectionAttempts.IsEmpty()==false) - sysAddr=failedConnectionAttempts.Pop(); + if (failedConnectionAttempts.empty()==false) + { + sysAddr=failedConnectionAttempts.front(); + failedConnectionAttempts.pop_front(); + } failedConnectionAttemptMutex.Unlock(); if (sysAddr!=UNASSIGNED_SYSTEM_ADDRESS) { unsigned int i; - for (i=0; i < messageHandlerList.Size(); i++) + for (i=0; i < messageHandlerList.size(); i++) { Packet p; p.systemAddress=sysAddr; @@ -685,7 +702,7 @@ SystemAddress TCPInterface::HasNewIncomingConnection(void) newIncomingConnections.Deallocate(out, _FILE_AND_LINE_); unsigned int i; - for (i=0; i < messageHandlerList.Size(); i++) + for (i=0; i < messageHandlerList.size(); i++) messageHandlerList[i]->OnNewConnection(out2, UNASSIGNED_RAKNET_GUID, true); return *out; @@ -705,7 +722,7 @@ SystemAddress TCPInterface::HasLostConnection(void) lostConnections.Deallocate(out, _FILE_AND_LINE_); unsigned int i; - for (i=0; i < messageHandlerList.Size(); i++) + for (i=0; i < messageHandlerList.size(); i++) messageHandlerList[i]->OnClosedConnection(out2, UNASSIGNED_RAKNET_GUID, LCR_DISCONNECTION_NOTIFICATION); return *out; @@ -830,7 +847,7 @@ __TCPSOCKET__ TCPInterface::SocketConnect(const char* host, unsigned short remot blockingSocketListMutex.Lock(); - blockingSocketList.Insert(sockfd, _FILE_AND_LINE_); + blockingSocketList.push_back(sockfd); blockingSocketListMutex.Unlock(); // This is blocking @@ -849,7 +866,7 @@ __TCPSOCKET__ TCPInterface::SocketConnect(const char* host, unsigned short remot getaddrinfo(host, portStr, &hints, &res); sockfd = socket__(res->ai_family, res->ai_socktype, res->ai_protocol); blockingSocketListMutex.Lock(); - blockingSocketList.Insert(sockfd, _FILE_AND_LINE_); + blockingSocketList.push_back(sockfd); blockingSocketListMutex.Unlock(); // #low - review usage of static cast here connectResult=connect__(sockfd, res->ai_addr, static_cast(res->ai_addrlen)); @@ -861,9 +878,11 @@ __TCPSOCKET__ TCPInterface::SocketConnect(const char* host, unsigned short remot { unsigned sockfdIndex; blockingSocketListMutex.Lock(); - sockfdIndex=blockingSocketList.GetIndexOf(sockfd); + sockfdIndex=(unsigned) (std::find(blockingSocketList.begin(), blockingSocketList.end(), sockfd) - blockingSocketList.begin()); if (sockfdIndex!=(unsigned)-1) - blockingSocketList.RemoveAtIndexFast(sockfdIndex); + // Swap-remove as before; this order is not published. + blockingSocketList[sockfdIndex]=blockingSocketList.back(); + blockingSocketList.pop_back(); blockingSocketListMutex.Unlock(); closesocket__(sockfd); @@ -898,7 +917,7 @@ RAK_THREAD_DECLARATION(MafiaNet::ConnectionAttemptLoop) tcpInterface->remoteClients[newRemoteClientIndex].isActiveMutex.Unlock(); tcpInterface->failedConnectionAttemptMutex.Lock(); - tcpInterface->failedConnectionAttempts.Push(systemAddress, _FILE_AND_LINE_ ); + tcpInterface->failedConnectionAttempts.push_back(systemAddress); tcpInterface->failedConnectionAttemptMutex.Unlock(); return 0; } @@ -910,7 +929,7 @@ RAK_THREAD_DECLARATION(MafiaNet::ConnectionAttemptLoop) if (tcpInterface->threadRunning.GetValue()>0) { tcpInterface->completedConnectionAttemptMutex.Lock(); - tcpInterface->completedConnectionAttempts.Push(systemAddress, _FILE_AND_LINE_ ); + tcpInterface->completedConnectionAttempts.push_back(systemAddress); tcpInterface->completedConnectionAttemptMutex.Unlock(); } diff --git a/Tests/Unit/TCPInterfacePushbackTests.cpp b/Tests/Unit/TCPInterfacePushbackTests.cpp new file mode 100644 index 000000000..35eeb16ac --- /dev/null +++ b/Tests/Unit/TCPInterfacePushbackTests.cpp @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + */ + +#include + +#include + +#include "mafianet/tcp_interface.h" + +#if _RAKNET_SUPPORT_TCPInterface == 1 + +using namespace MafiaNet; + +/* +Description: +Covers TCPInterface's two pushback queues before they move off DataStructures::Queue +(std migration stage 2, #60). TCPInterface had no tests. + +PushBackPacket(packet, pushAtHead) routes into one of two queues and Receive() drains headPush +before tailPush, so the pair decides the order a caller sees packets it handed back. Both the +queues and Receive() are reachable without starting the interface, so no socket is involved. + +The queues are FIFO within themselves: DataStructures::Queue::Push appends at the tail and Pop +takes from the head. A migration to std::vector with push_back/pop_back would reverse each queue +while still draining head before tail, and would pass any test that only pushed one packet per +queue -- so the cases below push several. +*/ + +namespace +{ + class TCPInterfacePushback : public ::testing::Test + { + protected: + void SetUp() override + { + // ReceiveInt() returns nothing until the interface is started. With + // maxIncomingConnections of 0, Start() creates no listen socket -- it only brings up + // the update thread -- so there is no bind, no port and no traffic. That thread feeds + // incomingMessages; headPush and tailPush are touched only by the calling thread, so + // the assertions below stay synchronous. + ASSERT_TRUE(tcp.Start(0, 0)); + } + void TearDown() override + { + // Drain anything a failed assertion left behind, so no packet leaks. + Packet *p; + while ((p = tcp.Receive()) != 0) + tcp.DeallocatePacket(p); + tcp.Stop(); + } + + // A packet carrying one identifying byte. + Packet *Make(unsigned char tag) + { + Packet *p = tcp.AllocatePacket(1); + p->data[0] = tag; + return p; + } + + std::vector DrainTags(void) + { + std::vector out; + Packet *p; + while ((p = tcp.Receive()) != 0) + { + out.push_back(p->data[0]); + tcp.DeallocatePacket(p); + } + return out; + } + + TCPInterface tcp; + }; +} + +TEST_F(TCPInterfacePushback, NothingPushedReceivesNothing) +{ + EXPECT_FALSE(tcp.ReceiveHasPackets()); + EXPECT_EQ(tcp.Receive(), (Packet *) 0); +} + +TEST_F(TCPInterfacePushback, APushedPacketComesBack) +{ + tcp.PushBackPacket(Make(7), false); + EXPECT_TRUE(tcp.ReceiveHasPackets()); + + std::vector tags = DrainTags(); + ASSERT_EQ(tags.size(), 1u); + EXPECT_EQ(tags[0], 7); +} + +TEST_F(TCPInterfacePushback, TailPushedPacketsKeepTheirOrder) +{ + tcp.PushBackPacket(Make(1), false); + tcp.PushBackPacket(Make(2), false); + tcp.PushBackPacket(Make(3), false); + + std::vector tags = DrainTags(); + ASSERT_EQ(tags.size(), 3u); + EXPECT_EQ(tags[0], 1) << "the tail queue is FIFO, not a stack"; + EXPECT_EQ(tags[1], 2); + EXPECT_EQ(tags[2], 3); +} + +TEST_F(TCPInterfacePushback, HeadPushedPacketsKeepTheirOrderAmongThemselves) +{ + tcp.PushBackPacket(Make(1), true); + tcp.PushBackPacket(Make(2), true); + tcp.PushBackPacket(Make(3), true); + + std::vector tags = DrainTags(); + ASSERT_EQ(tags.size(), 3u); + EXPECT_EQ(tags[0], 1) << "the head queue is FIFO within itself, not a stack"; + EXPECT_EQ(tags[1], 2); + EXPECT_EQ(tags[2], 3); +} + +TEST_F(TCPInterfacePushback, HeadPushedPacketsArriveBeforeTailPushedOnes) +{ + tcp.PushBackPacket(Make(10), false); // tail + tcp.PushBackPacket(Make(11), false); // tail + tcp.PushBackPacket(Make(20), true); // head + tcp.PushBackPacket(Make(21), true); // head + + std::vector tags = DrainTags(); + ASSERT_EQ(tags.size(), 4u); + EXPECT_EQ(tags[0], 20) << "head-pushed packets must be drained first"; + EXPECT_EQ(tags[1], 21); + EXPECT_EQ(tags[2], 10); + EXPECT_EQ(tags[3], 11); +} + +TEST_F(TCPInterfacePushback, ReceiveHasPacketsTracksBothQueues) +{ + EXPECT_FALSE(tcp.ReceiveHasPackets()); + tcp.PushBackPacket(Make(1), true); + EXPECT_TRUE(tcp.ReceiveHasPackets()); + Packet *p = tcp.Receive(); + ASSERT_NE(p, (Packet *) 0); + tcp.DeallocatePacket(p); + EXPECT_FALSE(tcp.ReceiveHasPackets()); + + tcp.PushBackPacket(Make(2), false); + EXPECT_TRUE(tcp.ReceiveHasPackets()); + p = tcp.Receive(); + ASSERT_NE(p, (Packet *) 0); + tcp.DeallocatePacket(p); + EXPECT_FALSE(tcp.ReceiveHasPackets()); +} + +TEST_F(TCPInterfacePushback, ManyPacketsSurviveInOrder) +{ + const int count = 50; + for (int i = 0; i < count; ++i) + tcp.PushBackPacket(Make((unsigned char) i), false); + + std::vector tags = DrainTags(); + ASSERT_EQ(tags.size(), (size_t) count); + for (int i = 0; i < count; ++i) + EXPECT_EQ(tags[i], (unsigned char) i) << "at position " << i; +} + +#endif // _RAKNET_SUPPORT_TCPInterface From 2d414a82db69b1434af794b8c7773af1b7a5cab3 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:14:09 +0200 Subject: [PATCH 11/26] refactor(table): move the protected query helpers to std::vector Part of #60, stage 2. Small by design: most of Table's container surface is public and therefore stage 3. - AddRowColumns and QueryRow are protected, so their DataStructures::List parameters become std::vector, along with the three locals passed to them and the file-static _columnIndices the sort callback reads. Left alone deliberately: - Table::columns stays a DataStructures::List because GetColumns() is public and returns a const reference to it. Changing the member would break that signature, and holding a converted copy to return a reference from is worse than waiting for stage 3. - Row::cells is a public member of a public struct. - table_serializer.cpp was checked and has nothing in scope: every DataStructures:: reference there is in a public static signature. Covered by the 18 existing Table tests, which exercise query, sort and the serialization round trip. --- Source/include/mafianet/ds_table.h | 5 +++-- Source/src/ds_table.cpp | 34 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Source/include/mafianet/ds_table.h b/Source/include/mafianet/ds_table.h index 595628d79..73f8938cf 100644 --- a/Source/include/mafianet/ds_table.h +++ b/Source/include/mafianet/ds_table.h @@ -25,6 +25,7 @@ #include "mafianet/memory_override.h" #include "mafianet/export.h" #include "mafianet/string.h" +#include #define _TABLE_MAX_COLUMN_NAME_LENGTH 64 @@ -330,11 +331,11 @@ namespace DataStructures Table& operator = ( const Table& input ); protected: - Table::Row* AddRowColumns(unsigned rowId, Row *row, DataStructures::List columnIndices); + Table::Row* AddRowColumns(unsigned rowId, Row *row, std::vector columnIndices); void DeleteRow(Row *row); - void QueryRow(DataStructures::List &inclusionFilterColumnIndices, DataStructures::List &columnIndicesToReturn, unsigned key, Table::Row* row, FilterQuery *inclusionFilters, Table *result); + void QueryRow(std::vector &inclusionFilterColumnIndices, std::vector &columnIndicesToReturn, unsigned key, Table::Row* row, FilterQuery *inclusionFilters, Table *result); std::map rows; diff --git a/Source/src/ds_table.cpp b/Source/src/ds_table.cpp index 5a6e3c368..f1c51863d 100644 --- a/Source/src/ds_table.cpp +++ b/Source/src/ds_table.cpp @@ -429,11 +429,11 @@ Table::Row* Table::AddRow(unsigned rowId, DataStructures::List &initialCe } return newRow; } -Table::Row* Table::AddRowColumns(unsigned rowId, Row *row, DataStructures::List columnIndices) +Table::Row* Table::AddRowColumns(unsigned rowId, Row *row, std::vector columnIndices) { Row *newRow = MafiaNet::OP_NEW( _FILE_AND_LINE_ ); unsigned columnIndex; - for (columnIndex=0; columnIndex < columnIndices.Size(); columnIndex++) + for (columnIndex=0; columnIndex < columnIndices.size(); columnIndex++) { if (row->cells[columnIndices[columnIndex]]->isEmpty==false) { @@ -631,7 +631,7 @@ Table::Row* Table::GetRowByIndex(unsigned rowIndex, unsigned *key) const void Table::QueryTable(unsigned *columnIndicesSubset, unsigned numColumnSubset, FilterQuery *inclusionFilters, unsigned numInclusionFilters, unsigned *rowIds, unsigned numRowIDs, Table *result) { unsigned i; - DataStructures::List columnIndicesToReturn; + std::vector columnIndicesToReturn; // Clear the result table. result->Clear(); @@ -641,25 +641,25 @@ void Table::QueryTable(unsigned *columnIndicesSubset, unsigned numColumnSubset, for (i=0; i < numColumnSubset; i++) { if (columnIndicesSubset[i]AddColumn(columns[columnIndicesToReturn[i]].columnName,columns[columnIndicesToReturn[i]].columnType); } // Get the column indices of the filter queries. - DataStructures::List inclusionFilterColumnIndices; + std::vector inclusionFilterColumnIndices; if (inclusionFilters && numInclusionFilters>0) { for (i=0; i < numInclusionFilters; i++) @@ -667,9 +667,9 @@ void Table::QueryTable(unsigned *columnIndicesSubset, unsigned numColumnSubset, if (inclusionFilters[i].columnName[0]) inclusionFilters[i].columnIndex=ColumnIndex(inclusionFilters[i].columnName); if (inclusionFilters[i].columnIndex &inclusionFilterColumnIndices, DataStructures::List &columnIndicesToReturn, unsigned key, Table::Row* row, FilterQuery *inclusionFilters, Table *result) +void Table::QueryRow(std::vector &inclusionFilterColumnIndices, std::vector &columnIndicesToReturn, unsigned key, Table::Row* row, FilterQuery *inclusionFilters, Table *result) { bool pass=false; unsigned columnIndex; unsigned j; // If no inclusion filters, just add the row - if (inclusionFilterColumnIndices.Size()==0) + if (inclusionFilterColumnIndices.size()==0) { result->AddRowColumns(key, row, columnIndicesToReturn); } else { // Go through all inclusion filters. Only add this row if all filters pass. - for (j=0; jcells[columnIndex]->isEmpty==false ) @@ -852,7 +852,7 @@ void Table::QueryRow(DataStructures::List &inclusionFilterColumnIndice static Table::SortQuery *_sortQueries; static unsigned _numSortQueries; -static DataStructures::List *_columnIndices; +static std::vector *_columnIndices; static DataStructures::List *_columns; int RowSort(Table::Row* const &first, Table::Row* const &second) // first is the one inserting, second is the one already there. { @@ -913,7 +913,7 @@ void Table::SortTable(Table::SortQuery *sortQueries, unsigned numSortQueries, Ta { unsigned i; unsigned outLength; - DataStructures::List columnIndices; + std::vector columnIndices; _sortQueries=sortQueries; _numSortQueries=numSortQueries; _columnIndices=&columnIndices; @@ -924,11 +924,11 @@ void Table::SortTable(Table::SortQuery *sortQueries, unsigned numSortQueries, Ta { if (sortQueries[i].columnIndex::const_iterator it; From e666a5563e38d6db4e10a6606996d15f6cd38bbb Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:16:21 +0200 Subject: [PATCH 12/26] refactor(teammanager): finish the protected surface missed in ce7193fe DecodeTeamAssigned's three DataStructures::List out-parameters are a protected signature, so they were in scope for stage 2 and I missed them -- found by classifying every remaining reference in Source/include by access section rather than by grepping file totals. They become std::vector, along with the three locals its only caller passes. team_manager.h now has no stage 2 container reference left in any protected or private section; the five that remain are the public getters, which stage 3 moves. --- Source/include/mafianet/team_manager.h | 4 ++-- Source/src/team_manager.cpp | 30 +++++++++++++------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Source/include/mafianet/team_manager.h b/Source/include/mafianet/team_manager.h index d4be2b1ee..b9c823c4e 100644 --- a/Source/include/mafianet/team_manager.h +++ b/Source/include/mafianet/team_manager.h @@ -747,8 +747,8 @@ class RAK_DLL_EXPORT TeamManager : public PluginInterface2 /// \param[in] A packet where packet->data[0]==ID_TEAM_BALANCER_TEAM_ASSIGNED /// \return true on success, false on read error void DecodeTeamAssigned(MafiaNet::BitStream *bsIn, TM_World **world, TM_TeamMember **teamMember, NoTeamId &noTeamSubcategory, - JoinTeamType &joinTeamType, DataStructures::List &newTeam, - DataStructures::List &teamsLeft, DataStructures::List &teamsJoined); + JoinTeamType &joinTeamType, std::vector &newTeam, + std::vector &teamsLeft, std::vector &teamsJoined); // O(1) lookup for a given world. If I need more worlds, change this to a hash or ordered list TM_World *worldsArray[255]; diff --git a/Source/src/team_manager.cpp b/Source/src/team_manager.cpp index e22e1e0f9..727f72fe2 100644 --- a/Source/src/team_manager.cpp +++ b/Source/src/team_manager.cpp @@ -2043,18 +2043,18 @@ void TeamManager::ProcessTeamAssigned(MafiaNet::BitStream *bsIn) TM_TeamMember *teamMember; NoTeamId noTeamId; JoinTeamType joinTeamType; - DataStructures::List newTeam; - DataStructures::List teamsLeft; - DataStructures::List teamsJoined; + std::vector newTeam; + std::vector teamsLeft; + std::vector teamsJoined; DecodeTeamAssigned(bsIn, &world, &teamMember, noTeamId, joinTeamType, newTeam, teamsLeft, teamsJoined); if (teamMember) { teamMember->StoreLastTeams(); - for (unsigned int i=0; i < teamsLeft.Size(); i++) + for (unsigned int i=0; i < teamsLeft.size(); i++) { teamMember->RemoveFromSpecificTeamInternal(teamsLeft[i]); } - for (unsigned int i=0; i < teamsJoined.Size(); i++) + for (unsigned int i=0; i < teamsJoined.size(); i++) { if (teamMember->IsOnTeam(teamsJoined[i])==false) { @@ -2127,13 +2127,13 @@ void TeamManager::DecodeTeamCancelled(Packet *packet, TM_World **world, TM_TeamM // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- void TeamManager::DecodeTeamAssigned(BitStream *bsIn, TM_World **world, TM_TeamMember **teamMember, NoTeamId &noTeamId, - JoinTeamType &joinTeamType, DataStructures::List &newTeam, - DataStructures::List &teamsLeft, DataStructures::List &teamsJoined + JoinTeamType &joinTeamType, std::vector &newTeam, + std::vector &teamsLeft, std::vector &teamsJoined ) { - newTeam.Clear(true, _FILE_AND_LINE_); - teamsLeft.Clear(true, _FILE_AND_LINE_); - teamsJoined.Clear(true, _FILE_AND_LINE_); + newTeam.clear(); + teamsLeft.clear(); + teamsJoined.clear(); WorldId worldId; NetworkID teamMemberId; @@ -2154,7 +2154,7 @@ void TeamManager::DecodeTeamAssigned(BitStream *bsIn, TM_World **world, TM_TeamM TM_Team * team = (*world)->GetTeamByNetworkID(teamId); RakAssert(team); if (team) - newTeam.Push(team, _FILE_AND_LINE_); + newTeam.push_back(team); // else probably didn't reference team first } @@ -2163,16 +2163,16 @@ void TeamManager::DecodeTeamAssigned(BitStream *bsIn, TM_World **world, TM_TeamM for (unsigned int i=0; i < (*teamMember)->teams.size(); i++) { TM_Team *team = (*teamMember)->teams[i]; - if (newTeam.GetIndexOf(team)==(unsigned int)-1) - teamsLeft.Push(team, _FILE_AND_LINE_); + if (IndexOf(newTeam, team)==(unsigned int)-1) + teamsLeft.push_back(team); } } - for (unsigned int i=0; i < newTeam.Size(); i++) + for (unsigned int i=0; i < newTeam.size(); i++) { TM_Team *team = newTeam[i]; if (IndexOf((*teamMember)->teams, team)==(unsigned int)-1) - teamsJoined.Push(team, _FILE_AND_LINE_); + teamsJoined.push_back(team); } bsIn->Read(noTeamId); From ca20f9edf27ae8045c61f652d8266e2f9878d3c9 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:24:07 +0200 Subject: [PATCH 13/26] test(reliabilitylayer): cover datagram history depth and concurrent split packets Written before stage 2 of #60 moves ReliabilityLayer's protected containers, because two of them had no coverage that could distinguish a correct migration from a broken one: - datagramHistory is a sliding window indexed by (datagramNumber - datagramHistoryPopCount) in WRAPPING uint24_t arithmetic. Every existing test sends few enough datagrams that the pop count stays at or near zero, where an off-by-one in that arithmetic is invisible. The new sustained-traffic case pushes 300 small messages through 10% loss, so the window advances and pops hundreds of times and resend lookups land at varying offsets, then asserts reliable-ordered delivery of all 300 in order. - splitPacketChannelList is an ordered list keyed by split-packet id, and every existing test has exactly ONE split message in flight. With a single element any ordering or lookup passes, so the container's job was untested. Three new cases put two and then three split messages in flight at once, with and without loss, and assert each reassembles as itself rather than being conflated with another. These are not MTU black hole tests, so they are marked off in the file with their own explanation; they live here to reuse the existing two-layer fake-socket rig with simulated time rather than duplicate it. All four pass against the current implementation. --- Tests/Unit/ReliabilityLayerBlackHoleTests.cpp | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/Tests/Unit/ReliabilityLayerBlackHoleTests.cpp b/Tests/Unit/ReliabilityLayerBlackHoleTests.cpp index 93a64b2da..95914f23f 100644 --- a/Tests/Unit/ReliabilityLayerBlackHoleTests.cpp +++ b/Tests/Unit/ReliabilityLayerBlackHoleTests.cpp @@ -320,3 +320,93 @@ TEST_F(RelLayerBlackHole, OrdinaryPacketLossDoesNotShrinkTheMtu) } } // namespace + +// -------------------------------------------------------------------------------------------- +// The cases below are not about MTU black holes. They exist because stage 2 of #60 moves +// ReliabilityLayer's protected containers to the standard library, and two of them had no +// coverage that could tell a correct migration from a broken one: +// +// - datagramHistory is a sliding window indexed by (datagramNumber - datagramHistoryPopCount) +// in WRAPPING uint24_t arithmetic. Every existing test sends few enough datagrams that the +// pop count stays at or near zero, where an off-by-one in that arithmetic is invisible. +// - splitPacketChannelList is an ordered list keyed by split-packet id, and every existing +// test has exactly ONE split message in flight. With a single element any ordering or lookup +// passes, so the container's whole job is untested. +// -------------------------------------------------------------------------------------------- + +// Drives datagramHistory well past its initial window: hundreds of datagrams pushed, acked and +// popped, with loss forcing resends so lookups land at varying offsets into the window. +TEST_F(RelLayerBlackHole, SustainedTrafficKeepsOrderWhileDatagramHistoryAdvances) +{ + aToBLossPercent = 10; + + const int count = 300; + std::vector > sent; + for (int i = 0; i < count; i++) + { + // Small messages, so each is its own datagram and the history advances once per message. + std::vector msg = PatternMessage(40, (unsigned char) i); + sent.push_back(msg); + SendFromA(msg); + Tick(); + } + + ASSERT_TRUE(PumpUntilReceived((size_t) count, 600000)) << "only " << received.size() << " of " + << count << " messages arrived"; + ASSERT_EQ(received.size(), (size_t) count); + for (int i = 0; i < count; i++) + EXPECT_EQ(received[i], sent[i]) << "reliable-ordered delivery broke at index " << i; +} + +// Two split messages in flight at once, so splitPacketChannelList holds more than one entry and +// its keyed lookup actually decides which message a fragment belongs to. +TEST_F(RelLayerBlackHole, TwoConcurrentSplitMessagesBothReassembleCorrectly) +{ + std::vector first = PatternMessage(9000, 61); + std::vector second = PatternMessage(7000, 62); + + // Queued back to back, so their fragments interleave on the wire. + SendFromA(first); + SendFromA(second); + + ASSERT_TRUE(PumpUntilReceived(2, 150000)); + ASSERT_EQ(received.size(), 2u); + EXPECT_EQ(received[0], first) << "the first split message reassembled wrongly"; + EXPECT_EQ(received[1], second) << "the second split message reassembled wrongly"; +} + +// Same, under loss, so fragments of both messages are resent and arrive interleaved with the +// fragments of the other -- the case where a wrong split-packet lookup silently mixes payloads. +TEST_F(RelLayerBlackHole, ConcurrentSplitMessagesSurviveLossWithoutMixingPayloads) +{ + aToBLossPercent = 10; + + std::vector first = PatternMessage(9000, 71); + std::vector second = PatternMessage(9000, 72); + + SendFromA(first); + SendFromA(second); + + ASSERT_TRUE(PumpUntilReceived(2, 600000)); + ASSERT_EQ(received.size(), 2u); + EXPECT_EQ(received[0], first); + EXPECT_EQ(received[1], second); + EXPECT_NE(received[0], received[1]) << "the two messages must not have been conflated"; +} + +// Three split messages at once, to push splitPacketChannelList past two entries. +TEST_F(RelLayerBlackHole, ThreeConcurrentSplitMessagesAllReassemble) +{ + std::vector > sent; + sent.push_back(PatternMessage(6000, 81)); + sent.push_back(PatternMessage(9000, 82)); + sent.push_back(PatternMessage(12000, 83)); + + for (size_t i = 0; i < sent.size(); i++) + SendFromA(sent[i]); + + ASSERT_TRUE(PumpUntilReceived(3, 300000)); + ASSERT_EQ(received.size(), 3u); + for (size_t i = 0; i < sent.size(); i++) + EXPECT_EQ(received[i], sent[i]) << "split message " << i << " reassembled wrongly"; +} From 9ca3badc068add8dac7feab747be23b3af53b7c3 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:49:02 +0200 Subject: [PATCH 14/26] refactor(reliabilitylayer): move the protected containers to std containers Part of #60, stage 2. Eleven protected members plus three locals. - outputQueue, datagramHistory, hasReceivedPacketQueue, delayList: DataStructures::Queue -> std::deque. - splitPacketChannelList: OrderedList -> std::vector kept sorted, with IndexForSplitPacketId (lower_bound) and InsertSplitPacketChannel. Both reuse SplitPacketChannelComp rather than reimplementing its key extraction, which differs under PREALLOCATE_LARGE_MESSAGES. - unreliableWithAckReceiptHistory and the five per-update send lists: List -> std::vector, with Preallocate becoming reserve. Three things in here are behaviour or semantics, not mechanics: 1. hasReceivedPacketQueue's AllocationSize()/Compress() compaction is removed. It existed only because the hand-rolled ring buffer never shrank; std::deque releases blocks as it drains, so there is nothing to compact. 2. uint24_t + size_t is ambiguous where uint24_t + unsigned was not, because uint24_t has both operator uint32_t() and an implicit uint24_t(const uint32_t&). size() returns size_t where Size() returned unsigned, so the datagram-boundary arithmetic is cast back to unsigned int to keep the original overload resolution. Same root cause as the RangeSum bug fixed in stage 1. 3. delayList's PushAtHead(dat, i) inserts at an arbitrary index to keep the list ordered by sendTime; it is now insert(begin() + i, dat), not push_front. My first attempt at this was wrong and the new tests caught it. InsertSplitPacketChannel originally read the split id back out of the channel, but the call site inserts the channel before populating its split packet list, so it keyed on uninitialised memory and segfaulted. Running the same test against the pre-migration source passed, which pinned it on this change rather than on a pre-existing bug or a bad test. The id is now a parameter. What these tests do NOT cover, stated so nobody assumes otherwise: I could not construct a case that distinguishes a sorted insert from an append for splitPacketChannelList. Split ids are handed out in ascending order, so appending stays sorted by accident; adding datagram reordering to the rig and going to two, three and eight concurrent split messages all still passed against an appending mutant. The sorted insert is kept because it mirrors the original and keeps lookup logarithmic, but it is correct by construction, not by test. Also fixes replica_manager3's constructedReplicaList.HasData, which only compiles under _DEBUG and so was missed until the Linux Debug build. Verified: macOS full suite 408 tests exit 0; macOS Debug build and unit suite clean; Linux Docker Debug 408/408 and Release 408/408 per CLAUDE.md, which is what caught the three _DEBUG-only compile errors in the first place. Left for the peer migration: the DataStructures::List parameters threaded through this file belong to peer's pluginListTS/NTS and have to move with them. --- Source/include/mafianet/reliability_layer.h | 42 ++- Source/src/reliability_layer.cpp | 275 ++++++++++-------- Source/src/replica_manager3.cpp | 4 +- Tests/Unit/ReliabilityLayerBlackHoleTests.cpp | 71 ++++- 4 files changed, 261 insertions(+), 131 deletions(-) diff --git a/Source/include/mafianet/reliability_layer.h b/Source/include/mafianet/reliability_layer.h index b5314feb5..daf743c11 100644 --- a/Source/include/mafianet/reliability_layer.h +++ b/Source/include/mafianet/reliability_layer.h @@ -49,6 +49,8 @@ #define INCLUDE_TIMESTAMP_WITH_DATAGRAMS 1 #else #include "mafianet/cc_sliding_window.h" +#include +#include #define INCLUDE_TIMESTAMP_WITH_DATAGRAMS 0 #endif @@ -408,7 +410,7 @@ class ReliabilityLayer// // Used ONLY for MafiaNet::Reliability::ReliableOrdered // MafiaNet::Reliability::ReliableSequenced just returns the newest one // DataStructures::List*> orderingList; - DataStructures::Queue outputQueue; + std::deque outputQueue; int splitMessageProgressInterval; CCTimeType unreliableTimeout; @@ -430,7 +432,9 @@ class ReliabilityLayer// // Queue length is programmatically restricted to DATAGRAM_MESSAGE_ID_ARRAY_LENGTH // This is essentially an O(1) lookup to get a DatagramHistoryNode given an index // datagramHistory holds a linked list of MessageNumberNode. Each MessageNumberNode refers to one element in resendList which can be cleared on an ack. - DataStructures::Queue datagramHistory; + // A sliding window: index 0 is datagram number datagramHistoryPopCount, and lookups index + // it by (datagramNumber - datagramHistoryPopCount) in wrapping uint24_t arithmetic. + std::deque datagramHistory; DataStructures::MemoryPool datagramHistoryMessagePool; struct UnreliableWithAckReceiptNode @@ -443,7 +447,7 @@ class ReliabilityLayer// uint32_t sendReceiptSerial; MafiaNet::TimeUS nextActionTime; }; - DataStructures::List unreliableWithAckReceiptHistory; + std::vector unreliableWithAckReceiptHistory; void RemoveFromDatagramHistory(DatagramSequenceNumberType index); MessageNumberNode* GetMessageNumberNodeByDatagramIndex(DatagramSequenceNumberType index, CCTimeType *timeSent); @@ -487,7 +491,10 @@ class ReliabilityLayer// // double bytesInSendBuffer[MafiaNet::NUMBER_OF_PRIORITIES]; - DataStructures::OrderedList splitPacketChannelList; + // Kept sorted by split-packet id and searched with std::lower_bound through + // IndexForSplitPacketId, matching the OrderedList it replaces. Several split messages can + // be in flight at once, so the keyed lookup decides which message a fragment belongs to. + std::vector splitPacketChannelList; MessageNumberType sendReliableMessageNumberIndex; MessageNumberType internalOrderIndex; @@ -549,7 +556,10 @@ class ReliabilityLayer// /// If we get a packet number where (receivedPacketsBaseIndex-packetNumber) is less than half the range of receivedPacketsBaseIndex then it is a duplicate /// Otherwise, it is a duplicate packet (and ignore it). // DataStructures::Queue hasReceivedPacketQueue; - DataStructures::Queue hasReceivedPacketQueue; + // Sliding window of holes, indexed from the head. std::deque frees blocks as it drains, so + // the predecessor's manual AllocationSize()/Compress() compaction is gone -- that existed + // only because its ring buffer never shrank on its own. + std::deque hasReceivedPacketQueue; DatagramSequenceNumberType receivedPacketsBaseIndex; bool resetReceivedPackets; @@ -586,7 +596,7 @@ class ReliabilityLayer// // SystemAddress systemAddress; unsigned int extraSocketOptions; }; - DataStructures::Queue delayList; + std::deque delayList; // Internet simulator double packetloss; @@ -623,15 +633,23 @@ class ReliabilityLayer// void AddToListTail(InternalPacket *internalPacket, bool modifyUnacknowledgedBytes); void PopListHead(bool modifyUnacknowledgedBytes); bool IsResendQueueEmpty(void) const; - void SortSplitPacketList(DataStructures::List &data, unsigned int leftEdge, unsigned int rightEdge) const; + void SortSplitPacketList(std::vector &data, unsigned int leftEdge, unsigned int rightEdge) const; + + // Position of a split-packet id in splitPacketChannelList, or where it would be inserted. + unsigned int IndexForSplitPacketId(SplitPacketIdType id, bool *found) const; + // Sorted insert; returns the index, or (unsigned)-1 if that id is already present, matching + // the OrderedList::Insert(assertOnDuplicate=true) it replaces. The id is passed in rather + // than read back out of the channel: at the call site the channel's split packet list has + // not been populated yet, so its own id is not readable there. + unsigned int InsertSplitPacketChannel(SplitPacketIdType id, SplitPacketChannel *channel); void SendACKs(RakNetSocket2 *s, SystemAddress &systemAddress, CCTimeType time, RakNetRandom *rnr, BitStream &updateBitStream); - DataStructures::List packetsToSendThisUpdate; - DataStructures::List packetsToDeallocThisUpdate; + std::vector packetsToSendThisUpdate; + std::vector packetsToDeallocThisUpdate; // boundary is in packetsToSendThisUpdate, inclusive - DataStructures::List packetsToSendThisUpdateDatagramBoundaries; - DataStructures::List datagramsToSendThisUpdateIsPair; - DataStructures::List datagramSizesInBytes; + std::vector packetsToSendThisUpdateDatagramBoundaries; + std::vector datagramsToSendThisUpdateIsPair; + std::vector datagramSizesInBytes; BitSize_t datagramSizeSoFar; BitSize_t allDatagramSizesSoFar; double totalUserDataBytesAcked; diff --git a/Source/src/reliability_layer.cpp b/Source/src/reliability_layer.cpp index 4e8dbd582..28fada9e7 100644 --- a/Source/src/reliability_layer.cpp +++ b/Source/src/reliability_layer.cpp @@ -545,7 +545,7 @@ void ReliabilityLayer::FreeThreadSafeMemory( void ) ClearPacketsAndDatagrams(); - for (i=0; i < splitPacketChannelList.Size(); i++) + for (i=0; i < splitPacketChannelList.size(); i++) { for (j=0; j < splitPacketChannelList[i]->splitPacketList.GetAllocSize(); j++) { @@ -564,16 +564,17 @@ void ReliabilityLayer::FreeThreadSafeMemory( void ) #endif MafiaNet::OP_DELETE(splitPacketChannelList[i], __FILE__, __LINE__); } - splitPacketChannelList.Clear(false, _FILE_AND_LINE_); + splitPacketChannelList.clear(); - while ( outputQueue.Size() > 0 ) + while ( outputQueue.size() > 0 ) { - internalPacket = outputQueue.Pop(); + internalPacket = outputQueue.front(); + outputQueue.pop_front(); FreeInternalPacketData(internalPacket, _FILE_AND_LINE_ ); ReleaseToInternalPacketPool( internalPacket ); } - outputQueue.ClearAndForceAllocation( 32, _FILE_AND_LINE_ ); + outputQueue.clear(); /* for ( i = 0; i < orderingList.Size(); i++ ) @@ -649,21 +650,21 @@ void ReliabilityLayer::FreeThreadSafeMemory( void ) outgoingPacketBuffer.Clear(); #ifdef _DEBUG - for (i = 0; i < delayList.Size(); i++ ) + for (i = 0; i < delayList.size(); i++ ) MafiaNet::OP_DELETE(delayList[ i ], __FILE__, __LINE__); - delayList.Clear(__FILE__, __LINE__); + delayList.clear(); #endif - unreliableWithAckReceiptHistory.Clear(false, _FILE_AND_LINE_); + unreliableWithAckReceiptHistory.clear(); - packetsToSendThisUpdate.Clear(false, _FILE_AND_LINE_); - packetsToSendThisUpdate.Preallocate(512, _FILE_AND_LINE_); - packetsToDeallocThisUpdate.Clear(false, _FILE_AND_LINE_); - packetsToDeallocThisUpdate.Preallocate(512, _FILE_AND_LINE_); - packetsToSendThisUpdateDatagramBoundaries.Clear(false, _FILE_AND_LINE_); - packetsToSendThisUpdateDatagramBoundaries.Preallocate(128, _FILE_AND_LINE_); - datagramSizesInBytes.Clear(false, _FILE_AND_LINE_); - datagramSizesInBytes.Preallocate(128, _FILE_AND_LINE_); + packetsToSendThisUpdate.clear(); + packetsToSendThisUpdate.reserve(512); + packetsToDeallocThisUpdate.clear(); + packetsToDeallocThisUpdate.reserve(512); + packetsToSendThisUpdateDatagramBoundaries.clear(); + packetsToSendThisUpdateDatagramBoundaries.reserve(128); + datagramSizesInBytes.clear(); + datagramSizesInBytes.reserve(128); internalPacketPool.Clear(_FILE_AND_LINE_); @@ -682,10 +683,10 @@ void ReliabilityLayer::FreeThreadSafeMemory( void ) datagramMessageIDPool.Clear(_FILE_AND_LINE_); */ - while (datagramHistory.Size()) + while (datagramHistory.size()) { RemoveFromDatagramHistory(datagramHistoryPopCount); - datagramHistory.Pop(); + datagramHistory.pop_front(); datagramHistoryPopCount++; } datagramHistoryMessagePool.Clear(_FILE_AND_LINE_); @@ -801,24 +802,24 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( } unsigned int k = 0; - while (k < unreliableWithAckReceiptHistory.Size()) { + while (k < unreliableWithAckReceiptHistory.size()) { if (incomingAcks.IsWithinRange(unreliableWithAckReceiptHistory[k].datagramNumber)) { InternalPacket *ackReceipt = AllocateFromInternalPacketPool(); AllocInternalPacketData(ackReceipt, 5, false, _FILE_AND_LINE_); ackReceipt->dataBitLength = BYTES_TO_BITS(5); ackReceipt->data[0] = (MessageID)ID_SND_RECEIPT_ACKED; memcpy(ackReceipt->data + sizeof(MessageID), &unreliableWithAckReceiptHistory[k].sendReceiptSerial, sizeof(uint32_t)); - outputQueue.Push(ackReceipt, _FILE_AND_LINE_); + outputQueue.push_back(ackReceipt); // Remove, swap with last - unreliableWithAckReceiptHistory.RemoveAtIndex(k); + unreliableWithAckReceiptHistory.erase(unreliableWithAckReceiptHistory.begin() + k); } else { k++; } } // early out, if we've got no outstanding datagramHistory entries - if (datagramHistory.IsEmpty()) { + if (datagramHistory.empty()) { receivePacketCount++; return true; } @@ -838,7 +839,7 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( for (datagramNumber = incomingAcks.ranges[i].minIndex; datagramNumber <= incomingAcks.ranges[i].maxIndex; datagramNumber++) { const DatagramSequenceNumberType offsetIntoList = datagramNumber - datagramHistoryPopCount; - if (offsetIntoList >= datagramHistory.Size()) { + if (offsetIntoList >= datagramHistory.size()) { // reached the end of the datagramHistory list - hence, we are done receivePacketCount++; return true; @@ -879,7 +880,7 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( } } else if (dhf.isNAK) { // early out, if we've got no outstanding datagramHistory entries - if (datagramHistory.IsEmpty()) { + if (datagramHistory.empty()) { receivePacketCount++; return true; } @@ -914,7 +915,7 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( // printf("%p NAK %i\n", this, dhf.datagramNumber.val); const DatagramSequenceNumberType offsetIntoList = messageNumber - datagramHistoryPopCount; - if (offsetIntoList >= datagramHistory.Size()) { + if (offsetIntoList >= datagramHistory.size()) { // reached the end of the datagramHistory list - hence, we are done receivePacketCount++; return true; @@ -987,7 +988,7 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( // resetReceivedPackets is set from a non-threadsafe function. // We do the actual reset in this function so the data is not modified by multiple threads if (resetReceivedPackets) { - hasReceivedPacketQueue.ClearAndForceAllocation(DEFAULT_HAS_RECEIVED_PACKET_QUEUE_SIZE, _FILE_AND_LINE_); + hasReceivedPacketQueue.clear(); receivedPacketsBaseIndex=0; resetReceivedPackets=false; } @@ -1026,8 +1027,8 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( if (holeCount == (DatagramSequenceNumberType)0) { // Got what we were expecting - if (hasReceivedPacketQueue.Size()) { - hasReceivedPacketQueue.Pop(); + if (hasReceivedPacketQueue.size()) { + hasReceivedPacketQueue.pop_front(); } ++receivedPacketsBaseIndex; } else if (holeCount > typeRange/(DatagramSequenceNumberType) 2) { @@ -1042,7 +1043,7 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( ReleaseToInternalPacketPool(internalPacket); goto CONTINUE_SOCKET_DATA_PARSE_LOOP; - } else if ((unsigned int)holeCount < hasReceivedPacketQueue.Size()) { + } else if ((unsigned int)holeCount < hasReceivedPacketQueue.size()) { // Got a higher count out of order packet that was missing in the sequence or we already got if (hasReceivedPacketQueue[holeCount] != false) { // non-zero means this is a hole #ifdef LOG_TRIVIAL_NOTIFICATIONS @@ -1097,26 +1098,25 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( // Fixed by late assigning message IDs on the sender // Add 0 times to the queue until (reliableMessageNumber - baseIndex) < queue size. - while ((unsigned int)(holeCount) > hasReceivedPacketQueue.Size()) { - hasReceivedPacketQueue.Push(true, _FILE_AND_LINE_); // time+(CCTimeType)60 * (CCTimeType)1000 * (CCTimeType)1000); // Didn't get this packet - set the time to give up waiting + while ((unsigned int)(holeCount) > hasReceivedPacketQueue.size()) { + hasReceivedPacketQueue.push_back(true); // time+(CCTimeType)60 * (CCTimeType)1000 * (CCTimeType)1000); // Didn't get this packet - set the time to give up waiting } - hasReceivedPacketQueue.Push(false, _FILE_AND_LINE_ ); // Got the packet + hasReceivedPacketQueue.push_back(false); // Got the packet #ifdef _DEBUG // If this assert hits then DatagramSequenceNumberType has overflowed - RakAssert(hasReceivedPacketQueue.Size() < (unsigned int)((DatagramSequenceNumberType)(const uint32_t)(-1))); + RakAssert(hasReceivedPacketQueue.size() < (unsigned int)((DatagramSequenceNumberType)(const uint32_t)(-1))); #endif } - while (hasReceivedPacketQueue.Size() > 0 && !hasReceivedPacketQueue.Peek()) { - hasReceivedPacketQueue.Pop(); + while (hasReceivedPacketQueue.size() > 0 && !hasReceivedPacketQueue.front()) { + hasReceivedPacketQueue.pop_front(); ++receivedPacketsBaseIndex; } } - // If the allocated buffer is > DEFAULT_HAS_RECEIVED_PACKET_QUEUE_SIZE and it is 3x greater than the number of elements actually being used - if (hasReceivedPacketQueue.AllocationSize() > (unsigned int)DEFAULT_HAS_RECEIVED_PACKET_QUEUE_SIZE && hasReceivedPacketQueue.AllocationSize() > hasReceivedPacketQueue.Size() * 3) { - hasReceivedPacketQueue.Compress(_FILE_AND_LINE_); - } + // The predecessor compacted here when its ring buffer had grown past 3x the live + // element count, because that buffer never shrank on its own. std::deque releases + // blocks as it drains, so there is nothing to compact and no equivalent call. /* @@ -1158,7 +1158,7 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( waitingForSequencedPacketReadIndex[ internalPacket->orderingChannel ] = internalPacket->orderingIndex + (OrderingIndexType)1; // If there is a rebuilt packet, add it to the output queue - outputQueue.Push(internalPacket, _FILE_AND_LINE_); + outputQueue.push_back(internalPacket); internalPacket = 0; } @@ -1169,7 +1169,7 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( // Not a split packet. Add the packet to the output queue bpsMetrics[(int) USER_MESSAGE_BYTES_RECEIVED_PROCESSED].Push1(timeRead,BITS_TO_BYTES(internalPacket->dataBitLength)); - outputQueue.Push( internalPacket, _FILE_AND_LINE_ ); + outputQueue.push_back(internalPacket); internalPacket = 0; } } else { @@ -1231,7 +1231,7 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( unsigned char orderingChannelCopy = internalPacket->orderingChannel; // Push the packet for the user to read - outputQueue.Push( internalPacket, _FILE_AND_LINE_ ); + outputQueue.push_back(internalPacket); internalPacket = 0; // Don't reference this any longer since other threads access it // Wait for the resendNext ordered packet in sequence @@ -1253,7 +1253,7 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( { if ( orderingListAtOrderingStream->Peek()->orderingIndex == waitingForOrderedPacketReadIndex[ orderingChannelCopy ] ) { - outputQueue.Push( orderingListAtOrderingStream->Pop(), _FILE_AND_LINE_ ); + outputQueue.push_back(orderingListAtOrderingStream->Pop()); waitingForOrderedPacketReadIndex[ orderingChannelCopy ]++; indexFound=true; } @@ -1405,7 +1405,7 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( { // Push to output buffer immediately bpsMetrics[(int) USER_MESSAGE_BYTES_RECEIVED_PROCESSED].Push1(timeRead,BITS_TO_BYTES(internalPacket->dataBitLength)); - outputQueue.Push( internalPacket, _FILE_AND_LINE_ ); + outputQueue.push_back(internalPacket); #ifdef PRINT_TO_FILE_RELIABLE_ORDERED_TEST if (packetId==ID_USER_PACKET_ENUM+1 && fp) @@ -1471,7 +1471,7 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( #endif bpsMetrics[(int) USER_MESSAGE_BYTES_RECEIVED_PROCESSED].Push1(timeRead,BITS_TO_BYTES(internalPacket->dataBitLength)); - outputQueue.Push( internalPacket, _FILE_AND_LINE_ ); + outputQueue.push_back(internalPacket); if (internalPacket->reliability == MafiaNet::Reliability::ReliableOrdered) { @@ -1542,7 +1542,7 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( bpsMetrics[(int) USER_MESSAGE_BYTES_RECEIVED_PROCESSED].Push1(timeRead,BITS_TO_BYTES(internalPacket->dataBitLength)); // Nothing special about this packet. Add it to the output queue - outputQueue.Push( internalPacket, _FILE_AND_LINE_ ); + outputQueue.push_back(internalPacket); internalPacket = 0; } @@ -1569,12 +1569,13 @@ BitSize_t ReliabilityLayer::Receive( unsigned char **data ) { InternalPacket * internalPacket; - if ( outputQueue.Size() > 0 ) + if ( outputQueue.size() > 0 ) { // #ifdef _DEBUG // RakAssert(bitStream->GetNumberOfBitsUsed()==0); // #endif - internalPacket = outputQueue.Pop(); + internalPacket = outputQueue.front(); + outputQueue.pop_front(); BitSize_t bitLength; *data = internalPacket->data; @@ -1780,11 +1781,12 @@ void ReliabilityLayer::UpdateInternal( RakNetSocket2 *s, SystemAddress &systemAd #endif #ifdef _DEBUG - while (delayList.Size()) + while (delayList.size()) { - if (delayList.Peek()->sendTime <= timeMs) + if (delayList.front()->sendTime <= timeMs) { - DataAndTime *dat = delayList.Pop(); + DataAndTime *dat = delayList.front(); + delayList.pop_front(); // SocketLayer::SendTo( dat->s, dat->data, dat->length, systemAddress, __FILE__, __LINE__ ); RNS2_SendParameters bsp; @@ -1932,10 +1934,10 @@ void ReliabilityLayer::UpdateInternal( RakNetSocket2 *s, SystemAddress &systemAd lastBpsClear=time; } - if (unreliableWithAckReceiptHistory.Size()>0) + if (unreliableWithAckReceiptHistory.size()>0) { i=0; - while (i < unreliableWithAckReceiptHistory.Size()) + while (i < unreliableWithAckReceiptHistory.size()) { //if (unreliableWithAckReceiptHistory[i].nextActionTime < time) if (time - unreliableWithAckReceiptHistory[i].nextActionTime < (((CCTimeType)-1)/2) ) @@ -1945,10 +1947,10 @@ void ReliabilityLayer::UpdateInternal( RakNetSocket2 *s, SystemAddress &systemAd ackReceipt->dataBitLength=BYTES_TO_BITS(5); ackReceipt->data[0]=(MessageID)ID_SND_RECEIPT_LOSS; memcpy(ackReceipt->data+sizeof(MessageID), &unreliableWithAckReceiptHistory[i].sendReceiptSerial, sizeof(uint32_t)); - outputQueue.Push(ackReceipt, _FILE_AND_LINE_ ); + outputQueue.push_back(ackReceipt); // Remove, swap with last - unreliableWithAckReceiptHistory.RemoveAtIndex(i); + unreliableWithAckReceiptHistory.erase(unreliableWithAckReceiptHistory.begin() + i); } else i++; @@ -2036,9 +2038,9 @@ void ReliabilityLayer::UpdateInternal( RakNetSocket2 *s, SystemAddress &systemAd for (unsigned int messageHandlerIndex=0; messageHandlerIndex < messageHandlerList.Size(); messageHandlerIndex++) { #if CC_TIME_TYPE_BYTES==4 - messageHandlerList[messageHandlerIndex]->OnInternalPacket(internalPacket, packetsToSendThisUpdateDatagramBoundaries.Size()+congestionManager.GetNextDatagramSequenceNumber(), systemAddress, (MafiaNet::TimeMS) time, true); + messageHandlerList[messageHandlerIndex]->OnInternalPacket(internalPacket, packetsToSendThisUpdateDatagramBoundaries.size()+congestionManager.GetNextDatagramSequenceNumber(), systemAddress, (MafiaNet::TimeMS) time, true); #else - messageHandlerList[messageHandlerIndex]->OnInternalPacket(internalPacket, packetsToSendThisUpdateDatagramBoundaries.Size()+congestionManager.GetNextDatagramSequenceNumber(), systemAddress, (MafiaNet::TimeMS)(time/(CCTimeType)1000), true); + messageHandlerList[messageHandlerIndex]->OnInternalPacket(internalPacket, packetsToSendThisUpdateDatagramBoundaries.size()+congestionManager.GetNextDatagramSequenceNumber(), systemAddress, (MafiaNet::TimeMS)(time/(CCTimeType)1000), true); #endif } @@ -2078,7 +2080,7 @@ void ReliabilityLayer::UpdateInternal( RakNetSocket2 *s, SystemAddress &systemAd ((int)BITS_TO_BYTES(allDatagramSizesSoFar)reliability == MafiaNet::Reliability::UnreliableWithAckReceipt) { - unreliableWithAckReceiptHistory.Push(UnreliableWithAckReceiptNode( - congestionManager.GetNextDatagramSequenceNumber() + packetsToSendThisUpdateDatagramBoundaries.Size(), + unreliableWithAckReceiptHistory.push_back(UnreliableWithAckReceiptNode( + // Cast back to unsigned int: uint24_t has both operator uint32_t() and an + // implicit uint24_t(const uint32_t&), so adding a size_t is ambiguous where + // adding the unsigned that List::Size() returned was not. + congestionManager.GetNextDatagramSequenceNumber() + (unsigned int) packetsToSendThisUpdateDatagramBoundaries.size(), internalPacket->sendReceiptSerial, congestionManager.GetRTOForRetransmission(internalPacket->timesSent+1)+time - ), _FILE_AND_LINE_); + )); } // If isReliable is false, the packet and its contents will be added to a list to be freed in ClearPacketsAndDatagrams @@ -2206,9 +2211,9 @@ void ReliabilityLayer::UpdateInternal( RakNetSocket2 *s, SystemAddress &systemAd for (unsigned int messageHandlerIndex=0; messageHandlerIndex < messageHandlerList.Size(); messageHandlerIndex++) { #if CC_TIME_TYPE_BYTES==4 - messageHandlerList[messageHandlerIndex]->OnInternalPacket(internalPacket, packetsToSendThisUpdateDatagramBoundaries.Size()+congestionManager.GetNextDatagramSequenceNumber(), systemAddress, (MafiaNet::TimeMS)time, true); + messageHandlerList[messageHandlerIndex]->OnInternalPacket(internalPacket, packetsToSendThisUpdateDatagramBoundaries.size()+congestionManager.GetNextDatagramSequenceNumber(), systemAddress, (MafiaNet::TimeMS)time, true); #else - messageHandlerList[messageHandlerIndex]->OnInternalPacket(internalPacket, packetsToSendThisUpdateDatagramBoundaries.Size()+congestionManager.GetNextDatagramSequenceNumber(), systemAddress, (MafiaNet::TimeMS)(time/(CCTimeType)1000), true); + messageHandlerList[messageHandlerIndex]->OnInternalPacket(internalPacket, packetsToSendThisUpdateDatagramBoundaries.size()+congestionManager.GetNextDatagramSequenceNumber(), systemAddress, (MafiaNet::TimeMS)(time/(CCTimeType)1000), true); #endif } pushedAnything=true; @@ -2236,7 +2241,7 @@ void ReliabilityLayer::UpdateInternal( RakNetSocket2 *s, SystemAddress &systemAd // sendmmsg. Loop-local: flushed at the single loop exit below. RNS2SendBatch sendBatch(s, systemAddress); #endif - for (unsigned int datagramIndex=0; datagramIndex < packetsToSendThisUpdateDatagramBoundaries.Size(); datagramIndex++) + for (unsigned int datagramIndex=0; datagramIndex < packetsToSendThisUpdateDatagramBoundaries.size(); datagramIndex++) { if (datagramIndex>0) dhf.isContinuousSend=true; @@ -2380,7 +2385,7 @@ void ReliabilityLayer::SendBitStream( RakNetSocket2 *s, SystemAddress &systemAdd dat->length=length; dat->sendTime = 0; dat->extraSocketOptions=extraSocketOptions; - delayList.PushAtHead(dat, 0, _FILE_AND_LINE_); + delayList.push_front(dat); #else MafiaNet::TimeMS delay = minExtraPing; if (extraPingVariance>0) @@ -2392,17 +2397,19 @@ void ReliabilityLayer::SendBitStream( RakNetSocket2 *s, SystemAddress &systemAdd dat->s=s; dat->length=length; dat->sendTime = MafiaNet::GetTimeMS() + delay; - for (unsigned int i=0; i < delayList.Size(); i++) + for (unsigned int i=0; i < delayList.size(); i++) { if (dat->sendTime < delayList[i]->sendTime) { - delayList.PushAtHead(dat, i, __FILE__, __LINE__); + // Insert before the first entry due later than this one, keeping delayList + // ordered by sendTime, as PushAtHead(dat, i) did. + delayList.insert(delayList.begin() + i, dat); dat=0; break; } } if (dat!=0) - delayList.Push(dat,__FILE__,__LINE__); + delayList.push_back(dat); return; } #endif @@ -2474,7 +2481,7 @@ bool ReliabilityLayer::IsOutgoingDataWaiting(void) return //acknowlegements.Size() > 0 || - //resendTree.IsEmpty()==false;// || outputQueue.Size() > 0 || orderingList.Size() > 0 || splitPacketChannelList.Size() > 0; + //resendTree.IsEmpty()==false;// || outputQueue.size() > 0 || orderingList.Size() > 0 || splitPacketChannelList.size() > 0; statistics.messagesInResendBuffer!=0; } bool ReliabilityLayer::AreAcksWaiting(void) @@ -2617,7 +2624,7 @@ unsigned ReliabilityLayer::RemovePacketFromResendListAndDeleteOlderReliableSeque ackReceipt->dataBitLength=BYTES_TO_BITS(5); ackReceipt->data[0]=(MessageID)ID_SND_RECEIPT_ACKED; memcpy(ackReceipt->data+sizeof(MessageID), &internalPacket->sendReceiptSerial, sizeof(internalPacket->sendReceiptSerial)); - outputQueue.Push(ackReceipt, _FILE_AND_LINE_ ); + outputQueue.push_back(ackReceipt); } bool isReliable; @@ -3176,9 +3183,9 @@ void ReliabilityLayer::ReSplitOversizedMessages(void) unsigned int i; InternalPacket *p; - DataStructures::List staleSplitIds; - DataStructures::List rebuiltMessages; - DataStructures::List resendVictims; + std::vector staleSplitIds; + std::vector rebuiltMessages; + std::vector resendVictims; // Rebuild a full-length copy of the message pkt belongs to, before any of // its packets are freed. Returns 0 (leaving the queues untouched) when the @@ -3238,7 +3245,7 @@ void ReliabilityLayer::ReSplitOversizedMessages(void) }; auto isStaleId = [&staleSplitIds](SplitPacketIdType id) -> bool { - for (unsigned int k = 0; k < staleSplitIds.Size(); k++) + for (unsigned int k = 0; k < staleSplitIds.size(); k++) if (staleSplitIds[k] == id) return true; return false; @@ -3254,8 +3261,8 @@ void ReliabilityLayer::ReSplitOversizedMessages(void) InternalPacket *rebuilt = rebuildMessage(pkt); if (rebuilt == 0) return; - staleSplitIds.Push(pkt->splitPacketId, _FILE_AND_LINE_); - rebuiltMessages.Push(rebuilt, _FILE_AND_LINE_); + staleSplitIds.push_back(pkt->splitPacketId); + rebuiltMessages.push_back(rebuilt); }; // Drop a not-yet-sent packet in place. The outgoing buffer is a heap, so @@ -3284,8 +3291,8 @@ void ReliabilityLayer::ReSplitOversizedMessages(void) InternalPacket *rebuilt = rebuildMessage(p); if (rebuilt) { - rebuiltMessages.Push(rebuilt, _FILE_AND_LINE_); - resendVictims.Push(p, _FILE_AND_LINE_); + rebuiltMessages.push_back(rebuilt); + resendVictims.push_back(p); } } } @@ -3312,7 +3319,7 @@ void ReliabilityLayer::ReSplitOversizedMessages(void) InternalPacket *rebuilt = rebuildMessage(p); if (rebuilt == 0) continue; - rebuiltMessages.Push(rebuilt, _FILE_AND_LINE_); + rebuiltMessages.push_back(rebuilt); } // An unreliable message the path already black-holed is not owed // delivery; dropping it here is what the network was doing anyway. @@ -3323,7 +3330,7 @@ void ReliabilityLayer::ReSplitOversizedMessages(void) // Pass 2: sweep every packet of the stale messages out of both queues -- // including small tail fragments that still fit, since the whole message // is re-sent under its new id. - if (staleSplitIds.Size() > 0) + if (staleSplitIds.size() > 0) { if (resendLinkedListHead) { @@ -3331,7 +3338,7 @@ void ReliabilityLayer::ReSplitOversizedMessages(void) do { if (p->splitPacketCount > 0 && isStaleId(p->splitPacketId)) - resendVictims.Push(p, _FILE_AND_LINE_); + resendVictims.push_back(p); p = p->resendNext; } while (p != resendLinkedListHead); } @@ -3344,7 +3351,7 @@ void ReliabilityLayer::ReSplitOversizedMessages(void) } // Unlink the resend-list victims the way an ack would, minus the receipt. - for (i = 0; i < resendVictims.Size(); i++) + for (i = 0; i < resendVictims.size(); i++) { p = resendVictims[i]; if (resendBuffer[p->reliableMessageNumber & (uint32_t) RESEND_BUFFER_ARRAY_MASK] == p) @@ -3357,24 +3364,58 @@ void ReliabilityLayer::ReSplitOversizedMessages(void) } // Queue the rebuilt messages, split at the new, smaller size. - for (i = 0; i < rebuiltMessages.Size(); i++) + for (i = 0; i < rebuiltMessages.size(); i++) SplitPacket(rebuiltMessages[i]); } //------------------------------------------------------------------------------------------------------- // Insert a packet into the split packet list //------------------------------------------------------------------------------------------------------- +unsigned int ReliabilityLayer::IndexForSplitPacketId(SplitPacketIdType id, bool *found) const +{ + // splitPacketChannelList is sorted by split-packet id. The key is read out of the channel by + // SplitPacketChannelComp, whose extraction differs under PREALLOCATE_LARGE_MESSAGES, so the + // comparator is reused rather than reimplemented: it returns >0 when the candidate sorts + // below the key. + std::vector::const_iterator it = std::lower_bound( + splitPacketChannelList.begin(), splitPacketChannelList.end(), id, + [](SplitPacketChannel * const &candidate, SplitPacketIdType key) + { + return MafiaNet::SplitPacketChannelComp(key, candidate) > 0; + }); + unsigned int i = (unsigned int) (it - splitPacketChannelList.begin()); + *found = (it != splitPacketChannelList.end() && MafiaNet::SplitPacketChannelComp(id, *it) == 0); + return i; +} +//------------------------------------------------------------------------------------------------------- +unsigned int ReliabilityLayer::InsertSplitPacketChannel(SplitPacketIdType id, SplitPacketChannel *channel) +{ + // The OrderedList::Insert this replaces was called with assertOnDuplicate=true: it refused a + // duplicate id and returned (unsigned)-1 without inserting. The id is a parameter because the + // caller inserts the channel before filling in its split packet list, so the channel cannot + // yet report its own id. + bool found; + unsigned int i = IndexForSplitPacketId(id, &found); + if (found) + { + RakAssert(0 && "Split packet id already in splitPacketChannelList"); + return (unsigned int) -1; + } + splitPacketChannelList.insert(splitPacketChannelList.begin() + i, channel); + return i; +} +//------------------------------------------------------------------------------------------------------- void ReliabilityLayer::InsertIntoSplitPacketList( InternalPacket * internalPacket, CCTimeType time ) { bool objectExists; unsigned index; // Find in splitPacketChannelList if a SplitPacketChannel with this splitPacketId was already allocated. If not, allocate and insert the channel into the list. - index=splitPacketChannelList.GetIndexFromKey(internalPacket->splitPacketId, &objectExists); + index=IndexForSplitPacketId(internalPacket->splitPacketId, &objectExists); if (objectExists==false) { SplitPacketChannel *newChannel = MafiaNet::OP_NEW( __FILE__, __LINE__ ); #if PREALLOCATE_LARGE_MESSAGES==1 - index=splitPacketChannelList.Insert(internalPacket->splitPacketId, newChannel, true, __FILE__,__LINE__); + index=InsertSplitPacketChannel(internalPacket->splitPacketId, newChannel); newChannel->returnedPacket=CreateInternalPacketCopy( internalPacket, 0, 0, time ); newChannel->gotFirstPacket=false; newChannel->splitPacketsArrived=0; @@ -3382,7 +3423,7 @@ void ReliabilityLayer::InsertIntoSplitPacketList( InternalPacket * internalPacke RakAssert(newChannel->returnedPacket->data); #else newChannel->firstPacket=0; - index=splitPacketChannelList.Insert(internalPacket->splitPacketId, newChannel, true, __FILE__,__LINE__); + index=InsertSplitPacketChannel(internalPacket->splitPacketId, newChannel); // Preallocate to the final size, to avoid runtime copies newChannel->splitPacketList.Preallocate(internalPacket, __FILE__,__LINE__); @@ -3504,7 +3545,7 @@ void ReliabilityLayer::InsertIntoSplitPacketList( InternalPacket * internalPacke memcpy(progressIndicator->data+sizeof(MessageID)+sizeof(unsigned int)*2, &temp, sizeof(unsigned int)); memcpy(progressIndicator->data+sizeof(MessageID)+sizeof(unsigned int)*3, splitPacketChannelList[index]->firstPacket->data, (size_t) BITS_TO_BYTES(splitPacketChannelList[index]->firstPacket->dataBitLength)); - outputQueue.Push(progressIndicator, __FILE__, __LINE__ ); + outputQueue.push_back(progressIndicator); } #endif @@ -3566,7 +3607,7 @@ InternalPacket * ReliabilityLayer::BuildPacketFromSplitPacketList( SplitPacketId InternalPacket * internalPacket; // Find in splitPacketChannelList the SplitPacketChannel with this splitPacketId - i=splitPacketChannelList.GetIndexFromKey(inSplitPacketId, &objectExists); + i=IndexForSplitPacketId(inSplitPacketId, &objectExists); splitPacketChannel=splitPacketChannelList[i]; #if PREALLOCATE_LARGE_MESSAGES==1 @@ -3578,7 +3619,7 @@ InternalPacket * ReliabilityLayer::BuildPacketFromSplitPacketList( SplitPacketId // Ack immediately, because for large files this can take a long time SendACKs(s, systemAddress, time, rnr, updateBitStream); internalPacket=BuildPacketFromSplitPacketList(splitPacketChannel,time); - splitPacketChannelList.RemoveAtIndex(i); + splitPacketChannelList.erase(splitPacketChannelList.begin() + i); return internalPacket; } else @@ -3593,7 +3634,7 @@ void ReliabilityLayer::DeleteOldUnreliableSplitPackets( CCTimeType time ) { unsigned i,j; i=0; -while (i < splitPacketChannelList.Size()) +while (i < splitPacketChannelList.size()) { #if CC_TIME_TYPE_BYTES==4 if (time > splitPacketChannelList[i]->lastUpdateTime + timeoutTime && @@ -3608,7 +3649,7 @@ MafiaNet::OP_DELETE_ARRAY(splitPacketChannelList[i]->splitPacketList[j]->data, _ ReleaseToInternalPacketPool(splitPacketChannelList[i]->splitPacketList[j]); } MafiaNet::OP_DELETE(splitPacketChannelList[i], _FILE_AND_LINE_); -splitPacketChannelList.RemoveAtIndex(i); +splitPacketChannelList.erase(splitPacketChannelList.begin() + i); } else i++; @@ -3785,11 +3826,11 @@ CCTimeType ReliabilityLayer::GetAckPing(void) const //------------------------------------------------------------------------------------------------------- void ReliabilityLayer::ResetPacketsAndDatagrams(void) { - packetsToSendThisUpdate.Clear(true, _FILE_AND_LINE_); - packetsToDeallocThisUpdate.Clear(true, _FILE_AND_LINE_); - packetsToSendThisUpdateDatagramBoundaries.Clear(true, _FILE_AND_LINE_); - datagramsToSendThisUpdateIsPair.Clear(true, _FILE_AND_LINE_); - datagramSizesInBytes.Clear(true, _FILE_AND_LINE_); + packetsToSendThisUpdate.clear(); + packetsToDeallocThisUpdate.clear(); + packetsToSendThisUpdateDatagramBoundaries.clear(); + datagramsToSendThisUpdateIsPair.clear(); + datagramSizesInBytes.clear(); datagramSizeSoFar=0; } //------------------------------------------------------------------------------------------------------- @@ -3799,8 +3840,8 @@ void ReliabilityLayer::PushPacket(CCTimeType time, InternalPacket *internalPacke datagramSizeSoFar+=bitsForThisPacket; RakAssert(BITS_TO_BYTES(datagramSizeSoFar)headerLength==GetMessageHeaderLengthBits(internalPacket)); // This code tells me how much time elapses between when you send, and when the message actually goes out @@ -3820,10 +3861,10 @@ void ReliabilityLayer::PushDatagram(void) { if (datagramSizeSoFar>0) { - packetsToSendThisUpdateDatagramBoundaries.Push(packetsToSendThisUpdate.Size(), _FILE_AND_LINE_ ); - datagramsToSendThisUpdateIsPair.Push(false, _FILE_AND_LINE_ ); + packetsToSendThisUpdateDatagramBoundaries.push_back(packetsToSendThisUpdate.size()); + datagramsToSendThisUpdateIsPair.push_back(false); RakAssert(BITS_TO_BYTES(datagramSizeSoFar)=2) + if (datagramsToSendThisUpdateIsPair.size()>=2) { - datagramsToSendThisUpdateIsPair[datagramsToSendThisUpdateIsPair.Size()-2]=true; - datagramsToSendThisUpdateIsPair[datagramsToSendThisUpdateIsPair.Size()-1]=true; + datagramsToSendThisUpdateIsPair[datagramsToSendThisUpdateIsPair.size()-2]=true; + datagramsToSendThisUpdateIsPair[datagramsToSendThisUpdateIsPair.size()-1]=true; return true; } return false; @@ -3853,7 +3894,7 @@ bool ReliabilityLayer::TagMostRecentPushAsSecondOfPacketPair(void) void ReliabilityLayer::ClearPacketsAndDatagrams(void) { unsigned int i; - for (i=0; i < packetsToDeallocThisUpdate.Size(); i++) + for (i=0; i < packetsToDeallocThisUpdate.size(); i++) { // packetsToDeallocThisUpdate holds a boolean indicating if packetsToSendThisUpdate at this index should be freed if (packetsToDeallocThisUpdate[i]) @@ -3864,7 +3905,7 @@ void ReliabilityLayer::ClearPacketsAndDatagrams(void) ReleaseToInternalPacketPool( packetsToSendThisUpdate[i] ); } } - packetsToDeallocThisUpdate.Clear(true, _FILE_AND_LINE_); + packetsToDeallocThisUpdate.clear(); } //------------------------------------------------------------------------------------------------------- void ReliabilityLayer::MoveToListHead(InternalPacket *internalPacket) @@ -4101,14 +4142,14 @@ bool ReliabilityLayer::ResendBufferOverflow(void) const //------------------------------------------------------------------------------------------------------- ReliabilityLayer::MessageNumberNode* ReliabilityLayer::GetMessageNumberNodeByDatagramIndex(DatagramSequenceNumberType index, CCTimeType *timeSent) { - if (datagramHistory.IsEmpty()) + if (datagramHistory.empty()) return nullptr; if (congestionManager.LessThan(index, datagramHistoryPopCount)) return nullptr; DatagramSequenceNumberType offsetIntoList = index - datagramHistoryPopCount; - if (offsetIntoList >= datagramHistory.Size()) + if (offsetIntoList >= datagramHistory.size()) return nullptr; *timeSent=datagramHistory[offsetIntoList].timeSent; @@ -4132,33 +4173,33 @@ void ReliabilityLayer::RemoveFromDatagramHistory(DatagramSequenceNumberType inde void ReliabilityLayer::AddFirstToDatagramHistory(DatagramSequenceNumberType datagramNumber, CCTimeType timeSent) { (void) datagramNumber; - if (datagramHistory.Size()>DATAGRAM_MESSAGE_ID_ARRAY_LENGTH) + if (datagramHistory.size()>DATAGRAM_MESSAGE_ID_ARRAY_LENGTH) { RemoveFromDatagramHistory(datagramHistoryPopCount); - datagramHistory.Pop(); + datagramHistory.pop_front(); datagramHistoryPopCount++; } - datagramHistory.Push(DatagramHistoryNode(0, timeSent), _FILE_AND_LINE_); - // printf("%p Pushed empty DatagramHistoryNode to datagram history at index %i\n", this, datagramHistory.Size()-1); + datagramHistory.push_back(DatagramHistoryNode(0, timeSent)); + // printf("%p Pushed empty DatagramHistoryNode to datagram history at index %i\n", this, datagramHistory.size()-1); } //------------------------------------------------------------------------------------------------------- ReliabilityLayer::MessageNumberNode* ReliabilityLayer::AddFirstToDatagramHistory(DatagramSequenceNumberType datagramNumber, DatagramSequenceNumberType messageNumber, CCTimeType timeSent) { (void) datagramNumber; -// RakAssert(datagramHistoryPopCount+(unsigned int) datagramHistory.Size()==datagramNumber); - if (datagramHistory.Size()>DATAGRAM_MESSAGE_ID_ARRAY_LENGTH) +// RakAssert(datagramHistoryPopCount+(unsigned int) datagramHistory.size()==datagramNumber); + if (datagramHistory.size()>DATAGRAM_MESSAGE_ID_ARRAY_LENGTH) { RemoveFromDatagramHistory(datagramHistoryPopCount); - datagramHistory.Pop(); + datagramHistory.pop_front(); datagramHistoryPopCount++; } MessageNumberNode *mnm = datagramHistoryMessagePool.Allocate(_FILE_AND_LINE_); mnm->next=0; mnm->messageNumber=messageNumber; - datagramHistory.Push(DatagramHistoryNode(mnm, timeSent), _FILE_AND_LINE_); - // printf("%p Pushed message %i to DatagramHistoryNode to datagram history at index %i\n", this, messageNumber.val, datagramHistory.Size()-1); + datagramHistory.push_back(DatagramHistoryNode(mnm, timeSent)); + // printf("%p Pushed message %i to DatagramHistoryNode to datagram history at index %i\n", this, messageNumber.val, datagramHistory.size()-1); return mnm; } //------------------------------------------------------------------------------------------------------- diff --git a/Source/src/replica_manager3.cpp b/Source/src/replica_manager3.cpp index 5248b6b58..db985d347 100644 --- a/Source/src/replica_manager3.cpp +++ b/Source/src/replica_manager3.cpp @@ -1870,7 +1870,9 @@ void Connection_RM3::OnLocalReference(Replica3* replica3, ReplicaManager3 *repli } } - if (constructedReplicaList.HasData(replica3)==true) + bool alreadyConstructed; + IndexForReplica(replica3, &alreadyConstructed); + if (alreadyConstructed==true) { RakAssert("replica added to queryToConstructReplicaList when already in constructedReplicaList" && 0); } diff --git a/Tests/Unit/ReliabilityLayerBlackHoleTests.cpp b/Tests/Unit/ReliabilityLayerBlackHoleTests.cpp index 95914f23f..1b1be6b6b 100644 --- a/Tests/Unit/ReliabilityLayerBlackHoleTests.cpp +++ b/Tests/Unit/ReliabilityLayerBlackHoleTests.cpp @@ -74,6 +74,10 @@ class RelLayerBlackHole : public ::testing::Test // When non-zero, drop this fraction (in percent) of ALL a->b datagrams, // deterministically, to model plain loss. int aToBLossPercent; + // When true, the datagrams a produced during one tick are delivered to b in reverse order. + // Plain reordering, which a real path does routinely, and the only way from here to make a + // later split-packet id reach b before an earlier one. + bool reverseAToBWithinTick; uint32_t lcgState; std::vector > received; // complete messages b got @@ -84,6 +88,7 @@ class RelLayerBlackHole : public ::testing::Test , now(0) , aToBDropOverBytes(MAXIMUM_MTU_SIZE) , aToBLossPercent(0) + , reverseAToBWithinTick(false) , lcgState(0x12345678) { } @@ -132,8 +137,9 @@ class RelLayerBlackHole : public ::testing::Test a.Update(&aSock, bAddr, TEST_MTU, now, 0, handlers, &rnr, updateBitStream); b.Update(&bSock, aAddr, TEST_MTU, now, 0, handlers, &rnr, updateBitStream); - for (size_t i = 0; i < aSock.sent.size(); i++) + for (size_t n = 0; n < aSock.sent.size(); n++) { + const size_t i = reverseAToBWithinTick ? aSock.sent.size() - 1 - n : n; if (DropAToB((int) aSock.sent[i].size())) continue; b.HandleSocketReceiveFromConnectedPlayer(&aSock.sent[i][0], (unsigned int) aSock.sent[i].size(), @@ -410,3 +416,66 @@ TEST_F(RelLayerBlackHole, ThreeConcurrentSplitMessagesAllReassemble) for (size_t i = 0; i < sent.size(); i++) EXPECT_EQ(received[i], sent[i]) << "split message " << i << " reassembled wrongly"; } + +// The case that actually pins splitPacketChannelList's ordering. Sending several split messages +// is not enough on its own: split-packet ids are handed out in ascending order, so a channel list +// that merely appends stays sorted by accident and a broken lookup still works. Reversing each +// tick's datagrams makes the higher id's first fragment reach b first, so the list is only sorted +// if the insert actually places it. +TEST_F(RelLayerBlackHole, ConcurrentSplitMessagesReassembleWhenDatagramsAreReordered) +{ + reverseAToBWithinTick = true; + + std::vector first = PatternMessage(9000, 91); + std::vector second = PatternMessage(9000, 92); + + SendFromA(first); + SendFromA(second); + + ASSERT_TRUE(PumpUntilReceived(2, 300000)); + ASSERT_EQ(received.size(), 2u); + EXPECT_EQ(received[0], first); + EXPECT_EQ(received[1], second); +} + +TEST_F(RelLayerBlackHole, ReorderedDatagramsStillDeliverManyMessagesInOrder) +{ + reverseAToBWithinTick = true; + + const int count = 60; + std::vector > sent; + for (int i = 0; i < count; i++) + { + std::vector msg = PatternMessage(40, (unsigned char) (100 + i)); + sent.push_back(msg); + SendFromA(msg); + } + + ASSERT_TRUE(PumpUntilReceived((size_t) count, 300000)); + ASSERT_EQ(received.size(), (size_t) count); + for (int i = 0; i < count; i++) + EXPECT_EQ(received[i], sent[i]) << "ordered delivery broke at index " << i; +} + +// Eight split messages in flight with reordering, so splitPacketChannelList holds many entries +// and a fragment of an early id has to be matched against a list whose entries were created in +// a different order. +TEST_F(RelLayerBlackHole, ManyConcurrentSplitMessagesReassembleUnderReordering) +{ + reverseAToBWithinTick = true; + + const int count = 8; + std::vector > sent; + for (int i = 0; i < count; i++) + { + std::vector msg = PatternMessage(5000 + i * 700, (unsigned char) (120 + i)); + sent.push_back(msg); + SendFromA(msg); + } + + ASSERT_TRUE(PumpUntilReceived((size_t) count, 600000)) << "only " << received.size() + << " of " << count << " split messages reassembled"; + ASSERT_EQ(received.size(), (size_t) count); + for (int i = 0; i < count; i++) + EXPECT_EQ(received[i], sent[i]) << "split message " << i << " reassembled wrongly"; +} From 78f6d4b8752f51a58c02cd680edf85533b0b647f Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:16:56 +0200 Subject: [PATCH 15/26] refactor(peer): move the protected lists and queues to std containers Covers eight of RakPeer's protected containers (issue #60): banList List -> std::vector securityExceptionList List -> std::vector socketList List -> std::vector SocketQueryOutput::sockets List -> std::vector packetReturnQueue Queue -> std::deque requestedConnectionQueue Queue -> std::deque bufferedPacketsQueue Queue -> std::deque bufferedPacketsFreePool Queue -> std::deque plus the local drain queue in ClearRequestedConnectionList. Tests first: Tests/Unit/PeerListsTests.cpp, 21 hermetic cases covering the ban list (exact and wildcard matches, swap-remove, in-place timeout update, the expiry sweep), the security exception list (multi-match removal, clear-on-null), the packet return buffer (PushBackPacket at head and tail, interleaved) and the socket list (one socket per descriptor, in index order). No Connect(), no loopback traffic; the only use of time is a ban created already expired. Removal semantics preserved exactly: * banList and securityExceptionList wrote their swap-remove out by hand (`list[i] = list[size-1]; RemoveAtIndex(size-1)`), which is `list[i] = list.back(); pop_back()`. Order is not part of their contract, but dropping the swapped-in element is a real bug, and the tests catch it. * DataStructures::Queue::RemoveAtIndex is a *shift*-remove, not a swap-remove, so requestedConnectionQueue's four removal sites became `erase(begin() + i)`, not pop_back. The queue order is not observable from outside, so this is preserved by construction rather than by test. * packetReturnQueue's `PushAtHead(packet, 0, ...)` is push_front; Receive pops the front. Both directions are pinned. Two redundant clears dropped: GetSocket's `output` is a fresh local filled at most once, and GetSockets already clears the caller's list at the top of the function, so clearing again inside the copy loop was dead. With them gone, removing the surviving clear fails a test. Mutation-tested both before and after the migration -- eight mutations, all killed, same tests on both sides: banList swap-remove loses the swapped-in element -> use-after-free in RemovingAMiddleBan... expiry sweep advances past the swapped-in element -> ExpiredBansAreSwept... AddToBanList appends instead of deduplicating -> ReAddingAnIp... (x2) PushBackPacket's head push becomes a tail push -> ...AtHeadJumpsTheQueue Receive pops the tail (buffer becomes a stack) -> ...AtTailIsFifo exception removal advances past the swapped element -> ...RemovesAllMatching socket query answers with only the first socket -> GetSockets... (x3) GetSockets stops clearing the caller's list -> ...ClearsTheCallersList Not migrated, and why: pluginListTS/pluginListNTS and the CallPluginCallbacks parameter stay DataStructures::List. They are handed straight to ReliabilityLayer::Update, UpdateAndForceACKs and HandleSocketReceiveFromConnectedPlayer, whose signatures are public; moving the members without those would mean converting at every call site. Same shape as Table::columns being blocked by the public GetColumns(). Those belong to the public-API stage. remoteSystemIndexPool, packetAllocationPool, bufferedCommands and socketQueryOutput are MemoryPool/ThreadsafeAllocatingQueue, not stage-2 types. Every migrated member keeps its existing mutex and every access stays inside the same critical section; no operation moved across a lock. Verified: macOS full suite 429/429; local Debug build clean + Debug unit 362/362; Linux Docker Debug 429/429 and Release 429/429. Claude-Session: https://claude.ai/code/session_011kRj5nodTJrxyoHY7y6R6c --- Source/include/mafianet/peer.h | 19 +- Source/src/peer.cpp | 189 ++++++++------- Tests/Unit/PeerListsTests.cpp | 417 +++++++++++++++++++++++++++++++++ 3 files changed, 534 insertions(+), 91 deletions(-) create mode 100644 Tests/Unit/PeerListsTests.cpp diff --git a/Source/include/mafianet/peer.h b/Source/include/mafianet/peer.h index fe31cb18f..8f1920c36 100644 --- a/Source/include/mafianet/peer.h +++ b/Source/include/mafianet/peer.h @@ -42,6 +42,9 @@ #include "mafianet/lockless_types.h" #include "mafianet/ds_queue.h" +#include +#include + namespace MafiaNet { /// Forward declarations class HuffmanEncodingTree; @@ -970,11 +973,11 @@ class RAK_DLL_EXPORT RakPeer : public RakPeerInterface, public RNS2EventHandler #endif //DataStructures::List* > automaticVariableSynchronizationList; - DataStructures::List banList; + std::vector banList; // Threadsafe, and not thread safe DataStructures::List pluginListTS, pluginListNTS; - DataStructures::Queue requestedConnectionQueue; + std::deque requestedConnectionQueue; SimpleMutex requestedConnectionQueueMutex; // void RunMutexedUpdateCycle(void); @@ -1012,9 +1015,9 @@ class RAK_DLL_EXPORT RakPeer : public RakPeerInterface, public RNS2EventHandler // DataStructures::ThreadsafeAllocatingQueue bufferedPackets; - DataStructures::Queue bufferedPacketsFreePool; + std::deque bufferedPacketsFreePool; MafiaNet::SimpleMutex bufferedPacketsFreePoolMutex; - DataStructures::Queue bufferedPacketsQueue; + std::deque bufferedPacketsQueue; MafiaNet::SimpleMutex bufferedPacketsQueueMutex; virtual void DeallocRNS2RecvStruct(RNS2RecvStruct *s, const char *file, unsigned int line); @@ -1027,7 +1030,7 @@ class RAK_DLL_EXPORT RakPeer : public RakPeerInterface, public RNS2EventHandler { SocketQueryOutput() {} ~SocketQueryOutput() {} - DataStructures::List sockets; + std::vector sockets; }; DataStructures::ThreadsafeAllocatingQueue socketQueryOutput; @@ -1060,7 +1063,7 @@ class RAK_DLL_EXPORT RakPeer : public RakPeerInterface, public RNS2EventHandler bool trackFrequencyTable; // Smart pointer so I can return the object to the user - DataStructures::List socketList; + std::vector socketList; void DerefAllSockets(void); unsigned int GetRakNetSocketFromUserConnectionSocketIndex(unsigned int userIndex) const; // Used for RPC replies @@ -1095,7 +1098,7 @@ class RAK_DLL_EXPORT RakPeer : public RakPeerInterface, public RNS2EventHandler bool (*incomingDatagramEventHandler)(RNS2RecvStruct *); // Systems in this list will not go through the secure connection process, even when secure connections are turned on. Wildcards are accepted. - DataStructures::List securityExceptionList; + std::vector securityExceptionList; SystemAddress ipList[ MAXIMUM_NUMBER_OF_INTERNAL_IDS ]; @@ -1112,7 +1115,7 @@ class RAK_DLL_EXPORT RakPeer : public RakPeerInterface, public RNS2EventHandler DataStructures::MemoryPool packetAllocationPool; SimpleMutex packetReturnMutex; - DataStructures::Queue packetReturnQueue; + std::deque packetReturnQueue; Packet *AllocPacket(unsigned dataSize, const char *file, unsigned int line); Packet *AllocPacket(unsigned dataSize, unsigned char *data, const char *file, unsigned int line); diff --git a/Source/src/peer.cpp b/Source/src/peer.cpp index 68456a88a..919dfca16 100644 --- a/Source/src/peer.cpp +++ b/Source/src/peer.cpp @@ -519,7 +519,7 @@ StartupResult RakPeer::Startup( unsigned int maxConnections, SocketDescriptor *s RakAssert("TODO" && 0); } - socketList.Push(r2, _FILE_AND_LINE_ ); + socketList.push_back(r2); } @@ -787,32 +787,32 @@ void RakPeer::DisableSecurity( void ) void RakPeer::AddToSecurityExceptionList(const char *ip) { securityExceptionMutex.Lock(); - securityExceptionList.Insert(RakString(ip), _FILE_AND_LINE_); + securityExceptionList.push_back(RakString(ip)); securityExceptionMutex.Unlock(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- void RakPeer::RemoveFromSecurityExceptionList(const char *ip) { - if (securityExceptionList.Size()==0) + if (securityExceptionList.size()==0) return; if (ip==0) { securityExceptionMutex.Lock(); - securityExceptionList.Clear(false, _FILE_AND_LINE_); + securityExceptionList.clear(); securityExceptionMutex.Unlock(); } else { unsigned i=0; securityExceptionMutex.Lock(); - while (i < securityExceptionList.Size()) + while (i < securityExceptionList.size()) { if (securityExceptionList[i].IPAddressMatch(ip)) { - securityExceptionList[i]=securityExceptionList[securityExceptionList.Size()-1]; - securityExceptionList.RemoveAtIndex(securityExceptionList.Size()-1); + securityExceptionList[i]=securityExceptionList.back(); + securityExceptionList.pop_back(); } else i++; @@ -823,12 +823,12 @@ void RakPeer::RemoveFromSecurityExceptionList(const char *ip) // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- bool RakPeer::IsInSecurityExceptionList(const char *ip) { - if (securityExceptionList.Size()==0) + if (securityExceptionList.size()==0) return false; unsigned i=0; securityExceptionMutex.Lock(); - for (; i < securityExceptionList.Size(); i++) + for (; i < securityExceptionList.size(); i++) { if (securityExceptionList[i].IPAddressMatch(ip)) { @@ -940,7 +940,7 @@ void RakPeer::GetIncomingPassword( char* passwordData, int *passwordDataLength ConnectionAttemptResult RakPeer::Connect( const char* host, unsigned short remotePort, const char *passwordData, int passwordDataLength, PublicKey *publicKey, unsigned connectionSocketIndex, unsigned sendConnectionAttemptCount, unsigned timeBetweenSendConnectionAttemptsMS, MafiaNet::TimeMS timeoutTime ) { // If endThreads is true here you didn't call Startup() first. - if ( host == 0 || endThreads || connectionSocketIndex>=socketList.Size() ) + if ( host == 0 || endThreads || connectionSocketIndex>=socketList.size() ) return INVALID_PARAMETER; RakAssert(remotePort!=0); @@ -1049,7 +1049,7 @@ void RakPeer::Shutdown( unsigned int blockDuration, unsigned char orderingChanne // MafiaNet::TimeMS timeout; #if RAKPEER_USER_THREADED!=1 - for (i=0; i < socketList.Size(); i++) + for (i=0; i < socketList.size(); i++) { if (socketList[i]->IsBerkleySocket()) { @@ -1059,7 +1059,7 @@ void RakPeer::Shutdown( unsigned int blockDuration, unsigned char orderingChanne /* // Get recvfrom to unblock - for (i=0; i < socketList.Size(); i++) + for (i=0; i < socketList.size(); i++) { if (SocketLayer::SendTo(socketList[i], (const char*) &i,1,socketList[i]->GetBoundAddress(), _FILE_AND_LINE_)!=0) break; @@ -1078,7 +1078,7 @@ void RakPeer::Shutdown( unsigned int blockDuration, unsigned char orderingChanne while ( isRecvFromLoopThreadActive.GetValue()>0 && MafiaNet::GetTimeMS()GetBoundAddress(), _FILE_AND_LINE_); } @@ -1087,7 +1087,7 @@ void RakPeer::Shutdown( unsigned int blockDuration, unsigned char orderingChanne } */ - for (i=0; i < socketList.Size(); i++) + for (i=0; i < socketList.size(); i++) { if (socketList[i]->IsBerkleySocket()) { @@ -1122,9 +1122,9 @@ void RakPeer::Shutdown( unsigned int blockDuration, unsigned char orderingChanne // Free any packets the user didn't deallocate packetReturnMutex.Lock(); - for (i=0; i < packetReturnQueue.Size(); i++) + for (i=0; i < packetReturnQueue.size(); i++) DeallocatePacket(packetReturnQueue[i]); - packetReturnQueue.Clear(_FILE_AND_LINE_); + packetReturnQueue.clear(); packetReturnMutex.Unlock(); packetAllocationPoolMutex.Lock(); packetAllocationPool.Clear(_FILE_AND_LINE_); @@ -1464,7 +1464,7 @@ Packet* RakPeer::Receive( void ) /* #if RAKPEER_SINGLE_THREADED==1 RakPeer::RecvFromStruct *recvFromStruct; - for (i=0; i < socketList.Size(); i++) + for (i=0; i < socketList.size(); i++) { for(;;) { @@ -1507,10 +1507,13 @@ Packet* RakPeer::Receive( void ) do { packetReturnMutex.Lock(); - if (packetReturnQueue.IsEmpty()) + if (packetReturnQueue.empty()) packet=0; else - packet = packetReturnQueue.Pop(); + { + packet = packetReturnQueue.front(); + packetReturnQueue.pop_front(); + } packetReturnMutex.Unlock(); if (packet==0) return 0; @@ -1644,7 +1647,7 @@ void RakPeer::CloseConnection( const AddressOrGUID target, bool sendDisconnectio // the BCS_CLOSE_CONNECTION path below. With no socket at all there is nothing to // close, so bail out. RakNetSocket2 *closeSocket = (remoteSystemListIndex != -1) ? remoteSystemList[remoteSystemListIndex].rakNetSocket : nullptr; - if (closeSocket == nullptr && socketList.Size() > 0) + if (closeSocket == nullptr && socketList.size() > 0) closeSocket = socketList[0]; if (closeSocket == nullptr) return; @@ -1676,7 +1679,7 @@ void RakPeer::CancelConnectionAttempt( const SystemAddress target ) // Cancel pending connection attempt, if there is one i=0; requestedConnectionQueueMutex.Lock(); - while (i < requestedConnectionQueue.Size()) + while (i < requestedConnectionQueue.size()) { if (requestedConnectionQueue[i]->systemAddress==target) { @@ -1685,7 +1688,7 @@ void RakPeer::CancelConnectionAttempt( const SystemAddress target ) MafiaNet::OP_DELETE(requestedConnectionQueue[i]->client_handshake, _FILE_AND_LINE_ ); #endif MafiaNet::OP_DELETE(requestedConnectionQueue[i], _FILE_AND_LINE_ ); - requestedConnectionQueue.RemoveAtIndex(i); + requestedConnectionQueue.erase(requestedConnectionQueue.begin() + i); break; } else @@ -1703,7 +1706,7 @@ ConnectionState RakPeer::GetConnectionState(const AddressOrGUID systemIdentifier { unsigned int i=0; requestedConnectionQueueMutex.Lock(); - for (; i < requestedConnectionQueue.Size(); i++) + for (; i < requestedConnectionQueue.size(); i++) { if (requestedConnectionQueue[i]->systemAddress==systemIdentifier.systemAddress) { @@ -1856,7 +1859,7 @@ void RakPeer::AddToBanList( const char *IP, MafiaNet::TimeMS milliseconds ) banListMutex.Lock(); - for ( ; index < banList.Size(); index++ ) + for ( ; index < banList.size(); index++ ) { if ( strcmp( IP, banList[ index ]->IP ) == 0 ) { @@ -1880,7 +1883,7 @@ void RakPeer::AddToBanList( const char *IP, MafiaNet::TimeMS milliseconds ) banStruct->timeout=time+milliseconds; strcpy_s( banStruct->IP, 16, IP ); banListMutex.Lock(); - banList.Insert( banStruct, _FILE_AND_LINE_ ); + banList.push_back(banStruct); banListMutex.Unlock(); } @@ -1905,13 +1908,13 @@ void RakPeer::RemoveFromBanList( const char *IP ) banListMutex.Lock(); - for ( ; index < banList.Size(); index++ ) + for ( ; index < banList.size(); index++ ) { if ( strcmp( IP, banList[ index ]->IP ) == 0 ) { temp = banList[ index ]; - banList[ index ] = banList[ banList.Size() - 1 ]; - banList.RemoveAtIndex( banList.Size() - 1 ); + banList[ index ] = banList.back(); + banList.pop_back(); break; } } @@ -1936,13 +1939,13 @@ void RakPeer::ClearBanList( void ) index = 0; banListMutex.Lock(); - for ( ; index < banList.Size(); index++ ) + for ( ; index < banList.size(); index++ ) { rakFree_Ex(banList[ index ]->IP, _FILE_AND_LINE_ ); MafiaNet::OP_DELETE(banList[ index ], _FILE_AND_LINE_); } - banList.Clear(false, _FILE_AND_LINE_); + banList.clear(); banListMutex.Unlock(); } @@ -1974,21 +1977,21 @@ bool RakPeer::IsBanned( const char *IP ) banListIndex = 0; - if ( banList.Size() == 0 ) + if ( banList.size() == 0 ) return false; // Skip the mutex if possible time = MafiaNet::GetTimeMS(); banListMutex.Lock(); - while ( banListIndex < banList.Size() ) + while ( banListIndex < banList.size() ) { if (banList[ banListIndex ]->timeout>0 && banList[ banListIndex ]->timeoutIP, _FILE_AND_LINE_ ); MafiaNet::OP_DELETE(temp, _FILE_AND_LINE_); } @@ -2075,7 +2078,7 @@ bool RakPeer::Ping( const char* host, unsigned short remotePort, bool onlyReplyO return false; // If this assert hits then Startup wasn't called or the call failed. - RakAssert(connectionSocketIndex < socketList.Size()); + RakAssert(connectionSocketIndex < socketList.size()); // if ( IsActive() == false ) // return; @@ -2939,9 +2942,9 @@ void RakPeer::PushBackPacket( Packet *packet, bool pushAtHead) packetReturnMutex.Lock(); if (pushAtHead) - packetReturnQueue.PushAtHead(packet,0,_FILE_AND_LINE_); + packetReturnQueue.push_front(packet); else - packetReturnQueue.Push(packet,_FILE_AND_LINE_); + packetReturnQueue.push_back(packet); packetReturnMutex.Unlock(); } @@ -2987,8 +2990,11 @@ RakNetSocket2* RakPeer::GetSocket( const SystemAddress target ) sqo = socketQueryOutput.Pop(); if (sqo) { - output=sqo->sockets; - sqo->sockets.Clear(false, _FILE_AND_LINE_); + // output is a fresh local filled at most once, and sockets is cleared at the + // top of GetSockets, so neither needs clearing here. + for (size_t socketIndex = 0; socketIndex < sqo->sockets.size(); socketIndex++) + output.Push(sqo->sockets[socketIndex], _FILE_AND_LINE_); + sqo->sockets.clear(); socketQueryOutput.Deallocate(sqo, _FILE_AND_LINE_); if (output.Size()) return output[0]; @@ -3024,8 +3030,9 @@ void RakPeer::GetSockets( DataStructures::List &sockets ) sqo = socketQueryOutput.Pop(); if (sqo) { - sockets=sqo->sockets; - sqo->sockets.Clear(false, _FILE_AND_LINE_); + for (size_t socketIndex = 0; socketIndex < sqo->sockets.size(); socketIndex++) + sockets.Push(sqo->sockets[socketIndex], _FILE_AND_LINE_); + sqo->sockets.clear(); socketQueryOutput.Deallocate(sqo, _FILE_AND_LINE_); return; } @@ -3113,7 +3120,7 @@ bool RakPeer::SendOutOfBand(const char *host, unsigned short remotePort, const c return false; // If this assert hits then Startup wasn't called or the call failed. - RakAssert(connectionSocketIndex < socketList.Size()); + RakAssert(connectionSocketIndex < socketList.size()); // This is a security measure. Don't send data longer than this value RakAssert(dataLength <= (MAX_OFFLINE_DATA_LENGTH + sizeof(unsigned char)+sizeof(MafiaNet::Time)+RakNetGUID::size()+sizeof(OFFLINE_MESSAGE_DATA_ID))); @@ -3241,7 +3248,7 @@ unsigned int RakPeer::GetReceiveBufferSize(void) { unsigned int size; packetReturnMutex.Lock(); - size=packetReturnQueue.Size(); + size=packetReturnQueue.size(); packetReturnMutex.Unlock(); return size; } @@ -3413,7 +3420,7 @@ ConnectionAttemptResult RakPeer::SendConnectionRequest( const char* host, unsign // Return false if already pending, else push on queue unsigned int i=0; requestedConnectionQueueMutex.Lock(); - for (; i < requestedConnectionQueue.Size(); i++) + for (; i < requestedConnectionQueue.size(); i++) { if (requestedConnectionQueue[i]->systemAddress==systemAddress) { @@ -3424,7 +3431,7 @@ ConnectionAttemptResult RakPeer::SendConnectionRequest( const char* host, unsign return CONNECTION_ATTEMPT_ALREADY_IN_PROGRESS; } } - requestedConnectionQueue.Push(rcs, _FILE_AND_LINE_ ); + requestedConnectionQueue.push_back(rcs); requestedConnectionQueueMutex.Unlock(); return CONNECTION_ATTEMPT_STARTED; @@ -3467,7 +3474,7 @@ ConnectionAttemptResult RakPeer::SendConnectionRequest( const char* host, unsign // Return false if already pending, else push on queue unsigned int i=0; requestedConnectionQueueMutex.Lock(); - for (; i < requestedConnectionQueue.Size(); i++) + for (; i < requestedConnectionQueue.size(); i++) { if (requestedConnectionQueue[i]->systemAddress==systemAddress) { @@ -3478,7 +3485,7 @@ ConnectionAttemptResult RakPeer::SendConnectionRequest( const char* host, unsign return CONNECTION_ATTEMPT_ALREADY_IN_PROGRESS; } } - requestedConnectionQueue.Push(rcs, _FILE_AND_LINE_ ); + requestedConnectionQueue.push_back(rcs); requestedConnectionQueueMutex.Unlock(); return CONNECTION_ATTEMPT_STARTED; @@ -4339,17 +4346,23 @@ MafiaNet::TimeMS RakPeer::GetSessionTimeoutFor(RemoteSystemStruct *remoteSystem) // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- void RakPeer::DeallocRNS2RecvStruct(RNS2RecvStruct *s, const char *file, unsigned int line) { + // file/line were only ever the allocation-tracking arguments DataStructures::Queue + // took; std::deque does not. + (void) file; + (void) line; + bufferedPacketsFreePoolMutex.Lock(); - bufferedPacketsFreePool.Push(s, file, line); + bufferedPacketsFreePool.push_back(s); bufferedPacketsFreePoolMutex.Unlock(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- RNS2RecvStruct *RakPeer::AllocRNS2RecvStruct(const char *file, unsigned int line) { bufferedPacketsFreePoolMutex.Lock(); - if (bufferedPacketsFreePool.Size()>0) + if (bufferedPacketsFreePool.empty()==false) { - RNS2RecvStruct *s = bufferedPacketsFreePool.Pop(); + RNS2RecvStruct *s = bufferedPacketsFreePool.front(); + bufferedPacketsFreePool.pop_front(); bufferedPacketsFreePoolMutex.Unlock(); return s; } @@ -4363,13 +4376,19 @@ RNS2RecvStruct *RakPeer::AllocRNS2RecvStruct(const char *file, unsigned int line void RakPeer::ClearBufferedPackets(void) { bufferedPacketsFreePoolMutex.Lock(); - while (bufferedPacketsFreePool.Size()>0) - MafiaNet::OP_DELETE(bufferedPacketsFreePool.Pop(), _FILE_AND_LINE_); + while (bufferedPacketsFreePool.empty()==false) + { + MafiaNet::OP_DELETE(bufferedPacketsFreePool.front(), _FILE_AND_LINE_); + bufferedPacketsFreePool.pop_front(); + } bufferedPacketsFreePoolMutex.Unlock(); bufferedPacketsQueueMutex.Lock(); - while (bufferedPacketsQueue.Size()>0) - MafiaNet::OP_DELETE(bufferedPacketsQueue.Pop(), _FILE_AND_LINE_); + while (bufferedPacketsQueue.empty()==false) + { + MafiaNet::OP_DELETE(bufferedPacketsQueue.front(), _FILE_AND_LINE_); + bufferedPacketsQueue.pop_front(); + } bufferedPacketsQueueMutex.Unlock(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -4380,16 +4399,17 @@ void RakPeer::SetupBufferedPackets(void) void RakPeer::PushBufferedPacket(RNS2RecvStruct * p) { bufferedPacketsQueueMutex.Lock(); - bufferedPacketsQueue.Push(p, _FILE_AND_LINE_); + bufferedPacketsQueue.push_back(p); bufferedPacketsQueueMutex.Unlock(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- RNS2RecvStruct *RakPeer::PopBufferedPacket(void) { bufferedPacketsQueueMutex.Lock(); - if (bufferedPacketsQueue.Size()>0) + if (bufferedPacketsQueue.empty()==false) { - RNS2RecvStruct *s = bufferedPacketsQueue.Pop(); + RNS2RecvStruct *s = bufferedPacketsQueue.front(); + bufferedPacketsQueue.pop_front(); bufferedPacketsQueueMutex.Unlock(); return s; } @@ -4759,16 +4779,19 @@ void RakPeer::ClearSocketQueryOutput(void) // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- void RakPeer::ClearRequestedConnectionList(void) { - DataStructures::Queue freeQueue; + std::deque freeQueue; requestedConnectionQueueMutex.Lock(); - while (requestedConnectionQueue.Size()) - freeQueue.Push(requestedConnectionQueue.Pop(), _FILE_AND_LINE_ ); + while (requestedConnectionQueue.empty()==false) + { + freeQueue.push_back(requestedConnectionQueue.front()); + requestedConnectionQueue.pop_front(); + } requestedConnectionQueueMutex.Unlock(); - unsigned i; - for (i=0; i < freeQueue.Size(); i++) + size_t i; + for (i=0; i < freeQueue.size(); i++) { #if LIBCAT_SECURITY==1 - CAT_AUDIT_PRINTF("AUDIT: In ClearRequestedConnectionList(), Deleting freeQueue index %i client_handshake %x\n", i, freeQueue[i]->client_handshake); + CAT_AUDIT_PRINTF("AUDIT: In ClearRequestedConnectionList(), Deleting freeQueue index %i client_handshake %x\n", (int) i, freeQueue[i]->client_handshake); MafiaNet::OP_DELETE(freeQueue[i]->client_handshake,_FILE_AND_LINE_); #endif MafiaNet::OP_DELETE(freeQueue[i], _FILE_AND_LINE_ ); @@ -4777,7 +4800,7 @@ void RakPeer::ClearRequestedConnectionList(void) inline void RakPeer::AddPacketToProducer(MafiaNet::Packet *p) { packetReturnMutex.Lock(); - packetReturnQueue.Push(p,_FILE_AND_LINE_); + packetReturnQueue.push_back(p); packetReturnMutex.Unlock(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5098,7 +5121,7 @@ bool ProcessOfflineNetworkPacket( SystemAddress systemAddress, const char *data, bsOut.Write(cookie); rakPeer->requestedConnectionQueueMutex.Lock(); - for (i=0; i < rakPeer->requestedConnectionQueue.Size(); i++) + for (i=0; i < rakPeer->requestedConnectionQueue.size(); i++) { RakPeer::RequestedConnectionStruct *rcs; rcs=rakPeer->requestedConnectionQueue[i]; @@ -5239,7 +5262,7 @@ bool ProcessOfflineNetworkPacket( SystemAddress systemAddress, const char *data, RakPeer::RequestedConnectionStruct *rcs; bool unlock=true; rakPeer->requestedConnectionQueueMutex.Lock(); - for (i=0; i < rakPeer->requestedConnectionQueue.Size(); i++) + for (i=0; i < rakPeer->requestedConnectionQueue.size(); i++) { rcs=rakPeer->requestedConnectionQueue[i]; @@ -5377,11 +5400,11 @@ bool ProcessOfflineNetworkPacket( SystemAddress systemAddress, const char *data, } rakPeer->requestedConnectionQueueMutex.Lock(); - for (unsigned int k=0; k < rakPeer->requestedConnectionQueue.Size(); k++) + for (unsigned int k=0; k < rakPeer->requestedConnectionQueue.size(); k++) { if (rakPeer->requestedConnectionQueue[k]->systemAddress==systemAddress) { - rakPeer->requestedConnectionQueue.RemoveAtIndex(k); + rakPeer->requestedConnectionQueue.erase(rakPeer->requestedConnectionQueue.begin() + k); break; } } @@ -5425,13 +5448,13 @@ bool ProcessOfflineNetworkPacket( SystemAddress systemAddress, const char *data, RakPeer::RequestedConnectionStruct *rcs; bool connectionAttemptCancelled=false; rakPeer->requestedConnectionQueueMutex.Lock(); - for (i=0; i < rakPeer->requestedConnectionQueue.Size(); i++) + for (i=0; i < rakPeer->requestedConnectionQueue.size(); i++) { rcs=rakPeer->requestedConnectionQueue[i]; if (rcs->actionToTake==RakPeer::RequestedConnectionStruct::CONNECT && rcs->systemAddress==systemAddress) { connectionAttemptCancelled=true; - rakPeer->requestedConnectionQueue.RemoveAtIndex(i); + rakPeer->requestedConnectionQueue.erase(rakPeer->requestedConnectionQueue.begin() + i); #if LIBCAT_SECURITY==1 CAT_AUDIT_PRINTF("AUDIT: Connection attempt canceled so deleting rcs->client_handshake object %x\n", rcs->client_handshake); @@ -5823,17 +5846,17 @@ unsigned int RakPeer::GenerateSeedFromGuid(void) void RakPeer::DerefAllSockets(void) { unsigned int i; - for (i=0; i < socketList.Size(); i++) + for (i=0; i < socketList.size(); i++) { MafiaNet::OP_DELETE(socketList[i], _FILE_AND_LINE_); } - socketList.Clear(false, _FILE_AND_LINE_); + socketList.clear(); } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- unsigned int RakPeer::GetRakNetSocketFromUserConnectionSocketIndex(unsigned int userIndex) const { unsigned int i; - for (i=0; i < socketList.Size(); i++) + for (i=0; i < socketList.size(); i++) { if (socketList[i]->GetUserConnectionSocketIndex()==userIndex) return i; @@ -5933,12 +5956,12 @@ bool RakPeer::RunUpdateCycle(BitStream &updateBitStream ) while ((recvFromStruct=PopBufferedPacket())!=0) { /* - for (socketListIndex=0; socketListIndex < socketList.Size(); socketListIndex++) + for (socketListIndex=0; socketListIndex < socketList.size(); socketListIndex++) { if ((RakNetSocket*) socketList[socketListIndex]==recvFromStruct->s) break; } - if (socketListIndex!=socketList.Size()) + if (socketListIndex!=socketList.size()) */ ProcessNetworkPacket(recvFromStruct->systemAddress, recvFromStruct->data, recvFromStruct->bytesRead, this, recvFromStruct->socket, recvFromStruct->timeRead, updateBitStream); DeallocRNS2RecvStruct(recvFromStruct, _FILE_AND_LINE_); @@ -6031,7 +6054,7 @@ bool RakPeer::RunUpdateCycle(BitStream &updateBitStream ) // RakAssert is a no-op in release (NDEBUG), so guard explicitly and fall back // to the primary socket to avoid dereferencing null in CloseConnectionInternal2. RakNetSocket2 *closeSocket = bcs->socket; - if (closeSocket == nullptr && socketList.Size() > 0) + if (closeSocket == nullptr && socketList.size() > 0) closeSocket = socketList[0]; if (closeSocket != nullptr) CloseConnectionInternal2(bcs->systemIdentifier, false, true, bcs->orderingChannel, bcs->priority, *closeSocket); @@ -6060,10 +6083,10 @@ bool RakPeer::RunUpdateCycle(BitStream &updateBitStream ) remoteSystem=GetRemoteSystem( bcs->systemIdentifier, true, true ); sqo = socketQueryOutput.Allocate( _FILE_AND_LINE_ ); - sqo->sockets.Clear(false, _FILE_AND_LINE_); + sqo->sockets.clear(); if (remoteSystem) { - sqo->sockets.Push(remoteSystem->rakNetSocket, _FILE_AND_LINE_ ); + sqo->sockets.push_back(remoteSystem->rakNetSocket); } else { @@ -6081,7 +6104,7 @@ bool RakPeer::RunUpdateCycle(BitStream &updateBitStream ) bufferedCommands.Deallocate(bcs, _FILE_AND_LINE_); } - if (requestedConnectionQueue.IsEmpty()==false) + if (requestedConnectionQueue.empty()==false) { if (timeNS==0) { @@ -6092,7 +6115,7 @@ bool RakPeer::RunUpdateCycle(BitStream &updateBitStream ) bool condition1, condition2; unsigned requestedConnectionQueueIndex=0; requestedConnectionQueueMutex.Lock(); - while (requestedConnectionQueueIndex < requestedConnectionQueue.Size()) + while (requestedConnectionQueueIndex < requestedConnectionQueue.size()) { RequestedConnectionStruct *rcs; rcs = requestedConnectionQueue[requestedConnectionQueueIndex]; @@ -6127,11 +6150,11 @@ bool RakPeer::RunUpdateCycle(BitStream &updateBitStream ) MafiaNet::OP_DELETE(rcs,_FILE_AND_LINE_); requestedConnectionQueueMutex.Lock(); - for (unsigned int k=0; k < requestedConnectionQueue.Size(); k++) + for (unsigned int k=0; k < requestedConnectionQueue.size(); k++) { if (requestedConnectionQueue[k]==rcs) { - requestedConnectionQueue.RemoveAtIndex(k); + requestedConnectionQueue.erase(requestedConnectionQueue.begin() + k); break; } } @@ -6971,7 +6994,7 @@ RAK_THREAD_DECLARATION(MafiaNet::UpdateNetworkLoop) { WSAEVENT eventArray[256]; unsigned int i, eventArrayIndex; - for (i=0,eventArrayIndex=0; i < rakPeer->socketList.Size(); i++) + for (i=0,eventArrayIndex=0; i < rakPeer->socketList.size(); i++) { if (rakPeer->socketList[i]->recvEvent!=INVALID_HANDLE_VALUE) { diff --git a/Tests/Unit/PeerListsTests.cpp b/Tests/Unit/PeerListsTests.cpp new file mode 100644 index 000000000..948030f75 --- /dev/null +++ b/Tests/Unit/PeerListsTests.cpp @@ -0,0 +1,417 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + */ + +#include + +#include "mafianet/peer.h" +#include "mafianet/peer_interface.h" +#include "mafianet/sleep.h" + +#include + +using namespace MafiaNet; + +// Pins the observable behaviour of RakPeer's protected lists before they move from +// DataStructures::List/Queue to std containers (issue #60): +// +// banList -> AddToBanList / RemoveFromBanList / IsBanned / ClearBanList +// securityExceptionList -> Add/Remove/IsInSecurityExceptionList +// packetReturnQueue -> PushBackPacket(packet, pushAtHead) / Receive / GetReceiveBufferSize +// socketList -> GetSockets / GetSocket after Startup +// +// All hermetic: no Connect(), no loopback traffic, no wall-clock polling. The one +// place time appears is the temporary-ban expiry, where the ban is created already +// expired rather than waited out. + +namespace +{ + class PeerLists : public ::testing::Test + { + protected: + void SetUp() override + { + peer = RakPeerInterface::GetInstance(); + ASSERT_NE(peer, nullptr); + } + + void TearDown() override + { + if (peer) + { + peer->Shutdown(0); + RakPeerInterface::DestroyInstance(peer); + peer = 0; + } + } + + // Starts the peer on an OS-assigned ephemeral loopback port. No peer ever + // connects to it; it exists so socketList and the packet pool are populated. + void StartOnEphemeralPort(unsigned short socketCount = 1) + { + SocketDescriptor sd[4]; + ASSERT_LE(socketCount, 4u); + for (unsigned short i = 0; i < socketCount; i++) + sd[i] = SocketDescriptor(0, "127.0.0.1"); + + ASSERT_EQ(peer->Startup(8, sd, socketCount), RAKNET_STARTED); + } + + RakPeerInterface *peer; + }; +} + +// --- banList --------------------------------------------------------------- + +TEST_F(PeerLists, ExactIpIsBannedAndUnbanned) +{ + ASSERT_FALSE(peer->IsBanned("1.2.3.4")); + + peer->AddToBanList("1.2.3.4", 0); + ASSERT_TRUE(peer->IsBanned("1.2.3.4")); + ASSERT_FALSE(peer->IsBanned("1.2.3.5")) << "a ban on one address must not cover its neighbour"; + + peer->RemoveFromBanList("1.2.3.4"); + ASSERT_FALSE(peer->IsBanned("1.2.3.4")); +} + +TEST_F(PeerLists, WildcardBanCoversTheWholePrefix) +{ + peer->AddToBanList("10.0.0.*", 0); + + ASSERT_TRUE(peer->IsBanned("10.0.0.1")); + ASSERT_TRUE(peer->IsBanned("10.0.0.254")); + ASSERT_FALSE(peer->IsBanned("10.0.1.1")) << "the wildcard only covers the last octet"; +} + +// The swap-remove in RemoveFromBanList moves the last entry into the hole. Removing +// a middle entry must leave every other ban queryable: an off-by-one there would +// silently drop the entry that got swapped in. +TEST_F(PeerLists, RemovingAMiddleBanKeepsEveryOtherBan) +{ + const char *ips[] = { "1.1.1.1", "2.2.2.2", "3.3.3.3", "4.4.4.4", "5.5.5.5" }; + for (size_t i = 0; i < 5; i++) + peer->AddToBanList(ips[i], 0); + + peer->RemoveFromBanList("2.2.2.2"); + + ASSERT_FALSE(peer->IsBanned("2.2.2.2")); + ASSERT_TRUE(peer->IsBanned("1.1.1.1")); + ASSERT_TRUE(peer->IsBanned("3.3.3.3")); + ASSERT_TRUE(peer->IsBanned("4.4.4.4")); + ASSERT_TRUE(peer->IsBanned("5.5.5.5")) << "the entry swapped into the hole must survive"; +} + +TEST_F(PeerLists, RemovingTheLastBanIsNotASelfAssignmentBug) +{ + peer->AddToBanList("1.1.1.1", 0); + peer->AddToBanList("2.2.2.2", 0); + + peer->RemoveFromBanList("2.2.2.2"); // index == size-1: swap with itself, then pop + + ASSERT_FALSE(peer->IsBanned("2.2.2.2")); + ASSERT_TRUE(peer->IsBanned("1.1.1.1")); +} + +// AddToBanList must find an existing entry for the IP and update it in place. If it +// appended a second entry instead, one RemoveFromBanList (which removes the first +// match only) would leave the IP banned. +TEST_F(PeerLists, ReAddingAnIpDoesNotCreateASecondEntry) +{ + peer->AddToBanList("7.7.7.7", 0); + peer->AddToBanList("7.7.7.7", 0); + + ASSERT_TRUE(peer->IsBanned("7.7.7.7")); + + peer->RemoveFromBanList("7.7.7.7"); + ASSERT_FALSE(peer->IsBanned("7.7.7.7")) + << "a single RemoveFromBanList must clear the IP, so only one entry was ever stored"; +} + +// The in-place update also has to be able to *shorten* a ban: a permanent ban +// followed by a 1ms ban for the same IP must expire. An append would leave the +// original permanent entry in the list and the IP would stay banned forever. +TEST_F(PeerLists, ReAddingAnIpWithATimeoutReplacesAPermanentBan) +{ + peer->AddToBanList("8.8.8.8", 0); + peer->AddToBanList("8.8.8.8", 1); + + RakSleep(30); + + ASSERT_FALSE(peer->IsBanned("8.8.8.8")) + << "the 1ms timeout must have replaced the permanent ban on the same entry"; +} + +// IsBanned sweeps expired entries out as it walks, with a swap-remove that must not +// advance the cursor. Three expired entries in a row is the case that catches a +// wrongly-placed increment. +TEST_F(PeerLists, ExpiredBansAreSweptAndDoNotHideLaterEntries) +{ + peer->AddToBanList("1.1.1.1", 1); + peer->AddToBanList("2.2.2.2", 1); + peer->AddToBanList("3.3.3.3", 1); + peer->AddToBanList("9.9.9.9", 0); // permanent + + RakSleep(30); // the three temporary bans are now in the past + + ASSERT_TRUE(peer->IsBanned("9.9.9.9")) + << "the permanent ban must still be found after the expired entries are swept"; + ASSERT_FALSE(peer->IsBanned("1.1.1.1")); + ASSERT_FALSE(peer->IsBanned("2.2.2.2")); + ASSERT_FALSE(peer->IsBanned("3.3.3.3")); +} + +TEST_F(PeerLists, ClearBanListDropsEverything) +{ + peer->AddToBanList("1.1.1.1", 0); + peer->AddToBanList("2.2.2.2", 0); + peer->AddToBanList("3.3.3.*", 0); + + peer->ClearBanList(); + + ASSERT_FALSE(peer->IsBanned("1.1.1.1")); + ASSERT_FALSE(peer->IsBanned("2.2.2.2")); + ASSERT_FALSE(peer->IsBanned("3.3.3.9")); + + // And the list is usable again afterwards. + peer->AddToBanList("4.4.4.4", 0); + ASSERT_TRUE(peer->IsBanned("4.4.4.4")); +} + +TEST_F(PeerLists, MalformedBanInputIsIgnored) +{ + peer->AddToBanList(0, 0); + peer->AddToBanList("", 0); + peer->AddToBanList("1234567890123456", 0); // 16 chars, over the 15-char limit + + ASSERT_FALSE(peer->IsBanned("1234567890123456")); + ASSERT_FALSE(peer->IsBanned(0)); + ASSERT_FALSE(peer->IsBanned("")); +} + +// --- securityExceptionList ------------------------------------------------- + +TEST_F(PeerLists, SecurityExceptionMatchesExactlyAndByWildcard) +{ + ASSERT_FALSE(peer->IsInSecurityExceptionList("10.0.0.1")); + + peer->AddToSecurityExceptionList("10.0.0.1"); + peer->AddToSecurityExceptionList("192.168.1.*"); + + ASSERT_TRUE(peer->IsInSecurityExceptionList("10.0.0.1")); + ASSERT_TRUE(peer->IsInSecurityExceptionList("192.168.1.77")); + ASSERT_FALSE(peer->IsInSecurityExceptionList("192.168.2.77")); + ASSERT_FALSE(peer->IsInSecurityExceptionList("10.0.0.2")); +} + +// RemoveFromSecurityExceptionList removes *every* matching entry, and its swap-remove +// deliberately does not advance the cursor when it removes. Two adjacent duplicates +// at the end of the list is what distinguishes that from a single removal. +TEST_F(PeerLists, RemovingASecurityExceptionRemovesAllMatchingEntries) +{ + peer->AddToSecurityExceptionList("5.5.5.5"); + peer->AddToSecurityExceptionList("10.0.0.1"); + peer->AddToSecurityExceptionList("10.0.0.1"); + peer->AddToSecurityExceptionList("10.0.0.1"); + + peer->RemoveFromSecurityExceptionList("10.0.0.1"); + + ASSERT_FALSE(peer->IsInSecurityExceptionList("10.0.0.1")) + << "all three duplicate entries must be gone"; + ASSERT_TRUE(peer->IsInSecurityExceptionList("5.5.5.5")); +} + +TEST_F(PeerLists, RemovingAMiddleSecurityExceptionKeepsTheRest) +{ + peer->AddToSecurityExceptionList("1.1.1.1"); + peer->AddToSecurityExceptionList("2.2.2.2"); + peer->AddToSecurityExceptionList("3.3.3.3"); + peer->AddToSecurityExceptionList("4.4.4.4"); + + peer->RemoveFromSecurityExceptionList("2.2.2.2"); + + ASSERT_FALSE(peer->IsInSecurityExceptionList("2.2.2.2")); + ASSERT_TRUE(peer->IsInSecurityExceptionList("1.1.1.1")); + ASSERT_TRUE(peer->IsInSecurityExceptionList("3.3.3.3")); + ASSERT_TRUE(peer->IsInSecurityExceptionList("4.4.4.4")); +} + +TEST_F(PeerLists, NullIpClearsTheWholeSecurityExceptionList) +{ + peer->AddToSecurityExceptionList("1.1.1.1"); + peer->AddToSecurityExceptionList("2.2.2.2"); + + peer->RemoveFromSecurityExceptionList(0); + + ASSERT_FALSE(peer->IsInSecurityExceptionList("1.1.1.1")); + ASSERT_FALSE(peer->IsInSecurityExceptionList("2.2.2.2")); + + peer->AddToSecurityExceptionList("3.3.3.3"); + ASSERT_TRUE(peer->IsInSecurityExceptionList("3.3.3.3")) << "the list must be reusable after a clear"; +} + +// --- packetReturnQueue ----------------------------------------------------- + +namespace +{ + // Allocates a one-byte packet carrying a tag, so Receive order is checkable. + Packet *TaggedPacket(RakPeerInterface *peer, unsigned char tag) + { + Packet *p = peer->AllocatePacket(1); + p->data[0] = tag; + return p; + } +} + +TEST_F(PeerLists, PushBackPacketAtTailIsFifo) +{ + StartOnEphemeralPort(); + + peer->PushBackPacket(TaggedPacket(peer, 10), false); + peer->PushBackPacket(TaggedPacket(peer, 20), false); + peer->PushBackPacket(TaggedPacket(peer, 30), false); + + ASSERT_EQ(peer->GetReceiveBufferSize(), 3u); + + unsigned char seen[3] = { 0, 0, 0 }; + for (int i = 0; i < 3; i++) + { + Packet *p = peer->Receive(); + ASSERT_NE(p, nullptr) << "packet " << i << " was pushed but not returned"; + seen[i] = p->data[0]; + peer->DeallocatePacket(p); + } + + ASSERT_EQ(seen[0], 10); + ASSERT_EQ(seen[1], 20); + ASSERT_EQ(seen[2], 30); +} + +// pushAtHead is the whole reason the return buffer is a queue and not a stack: a +// plugin that re-pushes a packet it decided not to consume must get it back first. +TEST_F(PeerLists, PushBackPacketAtHeadJumpsTheQueue) +{ + StartOnEphemeralPort(); + + peer->PushBackPacket(TaggedPacket(peer, 10), false); + peer->PushBackPacket(TaggedPacket(peer, 20), false); + peer->PushBackPacket(TaggedPacket(peer, 99), true); + + unsigned char seen[3] = { 0, 0, 0 }; + for (int i = 0; i < 3; i++) + { + Packet *p = peer->Receive(); + ASSERT_NE(p, nullptr); + seen[i] = p->data[0]; + peer->DeallocatePacket(p); + } + + ASSERT_EQ(seen[0], 99) << "the head push must be returned before the earlier packets"; + ASSERT_EQ(seen[1], 10); + ASSERT_EQ(seen[2], 20); +} + +TEST_F(PeerLists, InterleavedHeadAndTailPushesKeepBothOrders) +{ + StartOnEphemeralPort(); + + peer->PushBackPacket(TaggedPacket(peer, 1), false); // [1] + peer->PushBackPacket(TaggedPacket(peer, 2), true); // [2,1] + peer->PushBackPacket(TaggedPacket(peer, 3), false); // [2,1,3] + peer->PushBackPacket(TaggedPacket(peer, 4), true); // [4,2,1,3] + + ASSERT_EQ(peer->GetReceiveBufferSize(), 4u); + + const unsigned char expected[4] = { 4, 2, 1, 3 }; + for (int i = 0; i < 4; i++) + { + Packet *p = peer->Receive(); + ASSERT_NE(p, nullptr) << "missing packet at position " << i; + ASSERT_EQ(p->data[0], expected[i]) << "wrong packet at position " << i; + peer->DeallocatePacket(p); + } +} + +TEST_F(PeerLists, PushBackPacketIgnoresNull) +{ + StartOnEphemeralPort(); + + peer->PushBackPacket(0, false); + peer->PushBackPacket(0, true); + + ASSERT_EQ(peer->GetReceiveBufferSize(), 0u); +} + +// --- socketList ------------------------------------------------------------ + +TEST_F(PeerLists, GetSocketsReturnsOneSocketPerDescriptorInOrder) +{ + StartOnEphemeralPort(3); + + DataStructures::List sockets; + peer->GetSockets(sockets); + + ASSERT_EQ(sockets.Size(), 3u) << "one socket per SocketDescriptor passed to Startup"; + + // Every socket is distinct and bound to a different port; the indices Startup + // handed out are the indices GetSockets reports. + for (unsigned int i = 0; i < sockets.Size(); i++) + { + ASSERT_NE(sockets[i], nullptr) << "socket " << i << " is null"; + ASSERT_EQ(sockets[i]->GetUserConnectionSocketIndex(), i) + << "socket at index " << i << " does not carry that index"; + } + + for (unsigned int i = 0; i < sockets.Size(); i++) + for (unsigned int j = i + 1; j < sockets.Size(); j++) + ASSERT_NE(sockets[i], sockets[j]) << "sockets " << i << " and " << j << " are the same object"; +} + +TEST_F(PeerLists, GetSocketsIsEmptyBeforeStartup) +{ + DataStructures::List sockets; + peer->GetSockets(sockets); + + ASSERT_EQ(sockets.Size(), 0u); +} + +// GetSockets clears the caller's list up front, so a caller reusing one list across +// calls does not accumulate stale sockets. +TEST_F(PeerLists, GetSocketsClearsTheCallersListFirst) +{ + StartOnEphemeralPort(2); + + DataStructures::List sockets; + sockets.Push(0, _FILE_AND_LINE_); + sockets.Push(0, _FILE_AND_LINE_); + sockets.Push(0, _FILE_AND_LINE_); + + peer->GetSockets(sockets); + + ASSERT_EQ(sockets.Size(), 2u) << "GetSockets must replace the caller's contents, not append to them"; + ASSERT_NE(sockets[0], nullptr); + ASSERT_NE(sockets[1], nullptr); + + // Calling it twice must be idempotent. + peer->GetSockets(sockets); + ASSERT_EQ(sockets.Size(), 2u); +} + +// GetSocket(UNASSIGNED_SYSTEM_ADDRESS) runs the same network-thread query but hands +// back only the first socket, so it exercises the copy out of +// SocketQueryOutput::sockets separately from GetSockets. +TEST_F(PeerLists, GetSocketForAnUnassignedAddressReturnsTheFirstSocket) +{ + StartOnEphemeralPort(2); + + DataStructures::List sockets; + peer->GetSockets(sockets); + ASSERT_EQ(sockets.Size(), 2u); + + RakNetSocket2 *first = peer->GetSocket(UNASSIGNED_SYSTEM_ADDRESS); + ASSERT_NE(first, nullptr); + ASSERT_EQ(first, sockets[0]) << "GetSocket must return the socket GetSockets reports at index 0"; +} From 0237892f0247e42151c7c21bdb94770a391709de Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:19:51 +0200 Subject: [PATCH 16/26] docs(memory): say what the allocator override actually covers The page promised more than the library delivers, and the std-container migration (issue #60) makes the gap worth writing down precisely rather than leaving readers to find it. Corrections: * _USE_RAK_MEMORY_OVERRIDE defaults to 0 (defines.h), and the page never said so. In a default build OP_NEW / OP_NEW_ARRAY / OP_DELETE expand to plain new/delete and SetMalloc never sees them. Only the direct rakMalloc_Ex / rakRealloc_Ex / rakFree_Ex calls -- packet payloads, RakString storage, the ban list's IP strings -- are redirected unconditionally. * The standard containers the library now uses internally allocate through std::allocator and are never redirected. Added the comparison that matters: this is not a regression, because DataStructures::List allocated its backing array with OP_NEW_ARRAY, which is already a plain new[] in a default build. The change is observable only in a build that sets _USE_RAK_MEMORY_OVERRIDE=1 *and* installs a custom allocator; pointed at replacing global operator new as the way to cover everything. * RAKNET_MEMORY_TRACKING does not exist anywhere in the tree. Dropped, along with the two _RAKNET_SUPPORT_* entries that have nothing to do with memory. Documented _USE_RAK_MEMORY_OVERRIDE in their place. * "Memory leak detection helpers" -- there are none. Replaced with a pointer to the sanitizers, which also see the std containers. * The tracking example would not compile: the _Ex callbacks take `unsigned int line`, not `int`. Fixed, and added SetRealloc_Ex, whose absence silently left reallocations on the default path. * Pool class names were header-style (DS_MemoryPool, DS_BytePool, DS_ThreadSafeAllocatingQueue). Replaced with the real names (DataStructures::MemoryPool, ::BytePool, ::ThreadsafeAllocatingQueue) and the header each lives in. Verified: sphinx-build exits 0 with no new warning on the page. Claude-Session: https://claude.ai/code/session_011kRj5nodTJrxyoHY7y6R6c --- docs/advanced/custom-memory-management.rst | 112 +++++++++++++++++---- 1 file changed, 92 insertions(+), 20 deletions(-) diff --git a/docs/advanced/custom-memory-management.rst b/docs/advanced/custom-memory-management.rst index 1642ca899..2e1f52e8d 100644 --- a/docs/advanced/custom-memory-management.rst +++ b/docs/advanced/custom-memory-management.rst @@ -1,18 +1,28 @@ Custom Memory Management ======================== -MafiaNet supports custom memory allocators for fine-grained control over memory -allocation patterns. This is useful for tracking allocations, reducing fragmentation, -or integrating with game engine memory systems. +MafiaNet lets you replace the allocator it uses for raw buffers, and gives you +object pools for hot allocation paths. This is useful for tracking allocations, +reducing fragmentation, or integrating with a game engine's memory system. + +.. important:: + + Read :ref:`what-the-override-covers` first. Installing ``SetMalloc`` and + friends redirects *some* of the library's allocations by default and the rest + only in a build configured for it, and it never redirects the standard + containers the library uses internally. A plugin that assumes every byte + MafiaNet allocates comes from its own heap will be wrong. Overview -------- -Memory management can be customized at multiple levels: +Memory management can be customized at two levels: -* Global allocation overrides -* Per-system memory pools -* Custom allocators for specific components +* The ``rakMalloc``/``rakRealloc``/``rakFree`` function pointers, used for raw + buffer allocation and -- in a build with ``_USE_RAK_MEMORY_OVERRIDE=1`` -- for + object and array allocation through ``OP_NEW``/``OP_NEW_ARRAY``. +* Per-system memory pools (``DataStructures::MemoryPool`` and friends), which you + can also use for your own fixed-size objects. Basic Usage ----------- @@ -51,23 +61,40 @@ Basic Usage **Allocation tracking:** +The ``_Ex`` variants receive the call site. Note the exact signatures -- ``line`` +is ``unsigned int``, and ``SetRealloc_Ex`` must be installed alongside the other +two or reallocations keep going to the default implementation. + .. code-block:: cpp - void* MyMallocTracked(size_t size, const char* file, int line) { + void* MyMallocTracked(size_t size, const char* file, unsigned int line) { void* ptr = malloc(size); TrackAllocation(ptr, size, file, line); return ptr; } - void MyFreeTracked(void* ptr, const char* file, int line) { + void* MyReallocTracked(void* ptr, size_t size, const char* file, unsigned int line) { + UntrackAllocation(ptr, file, line); + void* out = realloc(ptr, size); + TrackAllocation(out, size, file, line); + return out; + } + + void MyFreeTracked(void* ptr, const char* file, unsigned int line) { UntrackAllocation(ptr, file, line); free(ptr); } // Enable file/line tracking MafiaNet::SetMalloc_Ex(MyMallocTracked); + MafiaNet::SetRealloc_Ex(MyReallocTracked); MafiaNet::SetFree_Ex(MyFreeTracked); +MafiaNet ships no leak reporter of its own; tracking is whatever your callbacks +do with the call site. For leak detection, prefer the sanitizers +(``-fsanitize=address``, and LeakSanitizer on Linux), which see the standard +containers too. + **Using memory pools:** .. code-block:: cpp @@ -83,30 +110,75 @@ Basic Usage // Return to pool packetPool.Release(data, _FILE_AND_LINE_); +.. _what-the-override-covers: + +What the Override Actually Covers +--------------------------------- + +There are three kinds of allocation inside MafiaNet, and ``SetMalloc`` reaches a +different amount of each. + +**1. Raw buffers --- always redirected.** +Library code that allocates a buffer calls ``rakMalloc_Ex`` / +``rakRealloc_Ex`` / ``rakFree_Ex`` directly, so your callbacks are used as soon +as you install them, in every build configuration. Packet payloads, the ban +list's IP strings and ``RakString``'s storage are allocated this way. + +**2. Objects and arrays --- only with ``_USE_RAK_MEMORY_OVERRIDE=1``.** +``OP_NEW``, ``OP_NEW_ARRAY`` and ``OP_DELETE`` are the library's object +allocation macros. They route through your allocator only when the library is +compiled with ``_USE_RAK_MEMORY_OVERRIDE=1``: + +.. code-block:: cmake + + add_compile_definitions(_USE_RAK_MEMORY_OVERRIDE=1) + +**``_USE_RAK_MEMORY_OVERRIDE`` defaults to 0** (see +``Source/include/mafianet/defines.h``), so in a default build every ``OP_NEW`` +is a plain ``new`` and your ``SetMalloc`` never sees it. The macro must be +defined identically for the library and for your own translation units, since it +changes what ``OP_NEW`` expands to in the headers. + +**3. Standard containers --- never redirected.** +MafiaNet's internals use ``std::vector``, ``std::deque``, ``std::map`` and +``std::unordered_map`` with the default ``std::allocator``. Those allocations go +to the global ``operator new``, in every configuration, and no MafiaNet setter +changes that. + +This is not a regression from the old ``DataStructures::List``/``Queue`` +containers: those allocated their backing array with ``OP_NEW_ARRAY``, which in a +default build is already a plain ``new Type[count]``. The only configuration in +which the change is observable is one that sets +``_USE_RAK_MEMORY_OVERRIDE=1`` *and* installs a custom allocator. To route the +standard containers too, replace the global ``operator new``/``operator delete`` +in your process -- that covers all three categories at once. + Key Features ------------ -* Global malloc/free/realloc override -* File and line number tracking variants +* Global malloc/realloc/free override for raw buffers +* File and line number tracking variants (``*_Ex``) * Built-in memory pool templates -* Thread-safe allocation support -* Memory leak detection helpers +* Thread-safe allocating queue Configuration Macros -------------------- -Define in your build configuration: +Define in your build configuration, for the library and your own code alike: -* ``_RAKNET_SUPPORT_FileOperations`` - Enable file I/O -* ``_RAKNET_SUPPORT_PacketizedTCP`` - TCP wrapper support -* ``RAKNET_MEMORY_TRACKING`` - Enable allocation tracking +* ``_USE_RAK_MEMORY_OVERRIDE`` - ``1`` routes ``OP_NEW``/``OP_NEW_ARRAY``/ + ``OP_DELETE`` through the installed allocator. Defaults to ``0``. Memory Pool Classes ------------------- -* ``DS_MemoryPool`` - Fixed-size object pool -* ``DS_BytePool`` - Variable-size byte allocation -* ``DS_ThreadSafeAllocatingQueue`` - Thread-safe allocation +* ``DataStructures::MemoryPool`` (``mafianet/ds_memory_pool.h``) - fixed-size + object pool +* ``DataStructures::BytePool`` (``mafianet/ds_byte_pool.h``) - variable-size byte + allocation, bucketed by size +* ``DataStructures::ThreadsafeAllocatingQueue`` + (``mafianet/ds_threadsafe_allocating_queue.h``) - a queue with a built-in + free pool, used for cross-thread handoff inside ``RakPeer`` See Also -------- From 2928ffb864546cca7d81fee6b50bd03c3c849ba0 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:27:54 +0200 Subject: [PATCH 17/26] fix(threadpool): unlock runThreadsMutex on Clear's no-threads path ThreadPool::Clear() locks runThreadsMutex to read runThreads, and unlocks it again only in the branch taken when threads *are* running. On the other branch it clears the three queues and returns with the mutex still held. SimpleMutex is not recursive, so the next lock of runThreadsMutex deadlocks: StartThreads, StopThreads, Pause, WasStarted, and the destructor, which calls StopThreads. A user who calls the documented "clears internal buffers" API on a pool that is not running cannot destroy that pool afterwards. Nothing inside the library calls Clear() -- send_to_thread and file_list_transfer use ClearInput() and StopThreads() -- which is why it survived. thread_pool.h is a public header, so it is reachable. Found while writing Tests/Unit/ThreadPoolQueueTests.cpp (14 cases) ahead of moving the three protected queues to std::deque. The new ClearDoesNotLeaveRunThreadsMutexHeld case hangs against the unfixed header -- verified by reverting the one-line fix and re-running it -- and passes with it. A hang is a worse failure signal than an assertion, but it is the only one this bug produces; ctest's timeout catches it. The other 13 cases pin the queue semantics the migration has to preserve: FIFO order on both queues, GetOutput popping the head, RemoveInputAtIndex and RemoveOutputAtIndex shifting later elements down rather than swapping the last one into the hole, ClearInput and ClearOutput not touching each other's queue, and two fixed-seed differential runs (4000 mixed add/pop/remove-at-index operations each) against a std::deque reference model. Claude-Session: https://claude.ai/code/session_011kRj5nodTJrxyoHY7y6R6c --- Source/include/mafianet/thread_pool.h | 6 + Tests/Unit/ThreadPoolQueueTests.cpp | 316 ++++++++++++++++++++++++++ 2 files changed, 322 insertions(+) create mode 100644 Tests/Unit/ThreadPoolQueueTests.cpp diff --git a/Source/include/mafianet/thread_pool.h b/Source/include/mafianet/thread_pool.h index 448b82bba..53fa2f418 100644 --- a/Source/include/mafianet/thread_pool.h +++ b/Source/include/mafianet/thread_pool.h @@ -492,9 +492,15 @@ void ThreadPool::Clear(void) } else { + // This branch used to return with runThreadsMutex still held, so any later + // lock of it -- StartThreads, StopThreads, Pause, WasStarted, or ~ThreadPool + // calling StopThreads -- deadlocked on a non-recursive SimpleMutex. Nothing + // in the library calls Clear(), which is why it went unnoticed, but it is a + // public header. inputFunctionQueue.Clear(_FILE_AND_LINE_); inputQueue.Clear(_FILE_AND_LINE_); outputQueue.Clear(_FILE_AND_LINE_); + runThreadsMutex.Unlock(); } } template diff --git a/Tests/Unit/ThreadPoolQueueTests.cpp b/Tests/Unit/ThreadPoolQueueTests.cpp new file mode 100644 index 000000000..4ccdb5d00 --- /dev/null +++ b/Tests/Unit/ThreadPoolQueueTests.cpp @@ -0,0 +1,316 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + */ + +#include + +#include "mafianet/thread_pool.h" + +#include + +// Pins ThreadPool's queue semantics before its three protected queues move from +// DataStructures::Queue to std::deque (issue #60): +// +// inputFunctionQueue / inputQueue paired arrays, FIFO, shift-remove by index +// outputQueue FIFO, shift-remove by index +// +// All hermetic: no threads are started, so every queue operation here is the +// synchronous one the public API performs on the calling thread. The pairing +// between inputFunctionQueue and inputQueue is only observable once a worker +// thread drains them, so that lives in Tests/Integration/ThreadPoolWorkerTests.cpp. + +namespace +{ + int AddOneHundred(int in, bool *returnOutput, void *) { *returnOutput = true; return in + 100; } + int AddTwoHundred(int in, bool *returnOutput, void *) { *returnOutput = true; return in + 200; } + + typedef ThreadPool IntPool; +} + +// --- input queue ----------------------------------------------------------- + +TEST(ThreadPoolQueues, AddInputAppendsInFifoOrder) +{ + IntPool pool; + + pool.AddInput(AddOneHundred, 10); + pool.AddInput(AddOneHundred, 20); + pool.AddInput(AddOneHundred, 30); + + ASSERT_EQ(pool.InputSize(), 3u); + ASSERT_EQ(pool.GetInputAtIndex(0), 10); + ASSERT_EQ(pool.GetInputAtIndex(1), 20); + ASSERT_EQ(pool.GetInputAtIndex(2), 30); +} + +// DataStructures::Queue::RemoveAtIndex shifts the later elements down -- it is not +// the swap-remove that DataStructures::List::RemoveAtIndexFast performs. Removing +// from the middle of a four-element queue is what tells the two apart. +TEST(ThreadPoolQueues, RemoveInputAtIndexShiftsLaterInputsDown) +{ + IntPool pool; + + pool.AddInput(AddOneHundred, 10); + pool.AddInput(AddOneHundred, 20); + pool.AddInput(AddOneHundred, 30); + pool.AddInput(AddOneHundred, 40); + + pool.RemoveInputAtIndex(1); + + ASSERT_EQ(pool.InputSize(), 3u); + ASSERT_EQ(pool.GetInputAtIndex(0), 10); + ASSERT_EQ(pool.GetInputAtIndex(1), 30) << "a swap-remove would put 40 here"; + ASSERT_EQ(pool.GetInputAtIndex(2), 40); +} + +TEST(ThreadPoolQueues, RemoveInputAtTheHeadAndAtTheTail) +{ + IntPool pool; + + for (int i = 1; i <= 4; i++) + pool.AddInput(AddOneHundred, i * 10); + + pool.RemoveInputAtIndex(0); + ASSERT_EQ(pool.InputSize(), 3u); + ASSERT_EQ(pool.GetInputAtIndex(0), 20); + + pool.RemoveInputAtIndex(pool.InputSize() - 1); + ASSERT_EQ(pool.InputSize(), 2u); + ASSERT_EQ(pool.GetInputAtIndex(0), 20); + ASSERT_EQ(pool.GetInputAtIndex(1), 30); +} + +TEST(ThreadPoolQueues, HasInputTracksTheInputQueue) +{ + IntPool pool; + + ASSERT_FALSE(pool.HasInput()); + ASSERT_FALSE(pool.HasInputFast()); + + pool.AddInput(AddOneHundred, 1); + ASSERT_TRUE(pool.HasInput()); + ASSERT_TRUE(pool.HasInputFast()); + + pool.RemoveInputAtIndex(0); + ASSERT_FALSE(pool.HasInput()); + ASSERT_FALSE(pool.HasInputFast()); +} + +// --- output queue ---------------------------------------------------------- + +TEST(ThreadPoolQueues, AddOutputAppendsAndGetOutputPopsTheHead) +{ + IntPool pool; + + pool.AddOutput(1); + pool.AddOutput(2); + pool.AddOutput(3); + + ASSERT_EQ(pool.OutputSize(), 3u); + ASSERT_EQ(pool.GetOutputAtIndex(0), 1); + ASSERT_EQ(pool.GetOutputAtIndex(2), 3); + + ASSERT_EQ(pool.GetOutput(), 1) << "GetOutput must take the oldest output, not the newest"; + ASSERT_EQ(pool.GetOutput(), 2); + ASSERT_EQ(pool.GetOutput(), 3); + ASSERT_EQ(pool.OutputSize(), 0u); +} + +TEST(ThreadPoolQueues, RemoveOutputAtIndexShiftsLaterOutputsDown) +{ + IntPool pool; + + for (int i = 1; i <= 4; i++) + pool.AddOutput(i); + + pool.RemoveOutputAtIndex(1); + + ASSERT_EQ(pool.OutputSize(), 3u); + ASSERT_EQ(pool.GetOutputAtIndex(0), 1); + ASSERT_EQ(pool.GetOutputAtIndex(1), 3) << "a swap-remove would put 4 here"; + ASSERT_EQ(pool.GetOutputAtIndex(2), 4); +} + +TEST(ThreadPoolQueues, HasOutputTracksTheOutputQueue) +{ + IntPool pool; + + ASSERT_FALSE(pool.HasOutput()); + ASSERT_FALSE(pool.HasOutputFast()); + + pool.AddOutput(7); + ASSERT_TRUE(pool.HasOutput()); + ASSERT_TRUE(pool.HasOutputFast()); + + ASSERT_EQ(pool.GetOutput(), 7); + ASSERT_FALSE(pool.HasOutput()); + ASSERT_FALSE(pool.HasOutputFast()); +} + +// --- clearing -------------------------------------------------------------- + +TEST(ThreadPoolQueues, ClearInputLeavesTheOutputQueueAlone) +{ + IntPool pool; + + pool.AddInput(AddOneHundred, 1); + pool.AddInput(AddOneHundred, 2); + pool.AddOutput(9); + + pool.ClearInput(); + + ASSERT_EQ(pool.InputSize(), 0u); + ASSERT_EQ(pool.OutputSize(), 1u) << "ClearInput must not touch the output queue"; + ASSERT_EQ(pool.GetOutputAtIndex(0), 9); +} + +TEST(ThreadPoolQueues, ClearOutputLeavesTheInputQueueAlone) +{ + IntPool pool; + + pool.AddInput(AddOneHundred, 1); + pool.AddOutput(9); + pool.AddOutput(8); + + pool.ClearOutput(); + + ASSERT_EQ(pool.OutputSize(), 0u); + ASSERT_EQ(pool.InputSize(), 1u) << "ClearOutput must not touch the input queue"; + ASSERT_EQ(pool.GetInputAtIndex(0), 1); +} + +// Clear() on a pool with no threads running used to return with runThreadsMutex +// still held, so the very next lock of it deadlocked -- including the one the +// destructor performs via StopThreads. This test hangs forever against that bug. +TEST(ThreadPoolQueues, ClearDoesNotLeaveRunThreadsMutexHeld) +{ + IntPool pool; + + pool.AddOutput(1); + pool.Clear(); + + // Each of these locks runThreadsMutex. + ASSERT_FALSE(pool.WasStarted()); + pool.StopThreads(); + pool.Clear(); + ASSERT_FALSE(pool.WasStarted()); + + // And so does ~ThreadPool, when pool goes out of scope. +} + +TEST(ThreadPoolQueues, ClearEmptiesBothQueuesWhenNoThreadsAreRunning) +{ + IntPool pool; + + ASSERT_FALSE(pool.WasStarted()); + + pool.AddInput(AddOneHundred, 1); + pool.AddInput(AddTwoHundred, 2); + pool.AddOutput(9); + + pool.Clear(); + + ASSERT_EQ(pool.InputSize(), 0u); + ASSERT_EQ(pool.OutputSize(), 0u); +} + +TEST(ThreadPoolQueues, QueuesAreReusableAfterBeingCleared) +{ + IntPool pool; + + pool.AddInput(AddOneHundred, 1); + pool.AddOutput(9); + pool.Clear(); + + pool.AddInput(AddOneHundred, 5); + pool.AddOutput(6); + + ASSERT_EQ(pool.InputSize(), 1u); + ASSERT_EQ(pool.GetInputAtIndex(0), 5); + ASSERT_EQ(pool.OutputSize(), 1u); + ASSERT_EQ(pool.GetOutput(), 6); +} + +// --- differential stress --------------------------------------------------- + +// A fixed-seed sequence of adds, indexed removals and head pops, checked against +// a std::deque reference model after every operation. Deterministic, so a +// divergence reproduces exactly; wrap-around in the old ring buffer and +// reallocation in the new deque are both exercised by the mixed load. +TEST(ThreadPoolQueues, OutputQueueMatchesADequeModelUnderMixedOperations) +{ + IntPool pool; + std::deque model; + + uint32_t seed = 0xC0FFEEu; + int nextValue = 0; + + for (int step = 0; step < 4000; step++) + { + seed = seed * 1103515245u + 12345u; + const uint32_t roll = (seed >> 16) % 100u; + + if (roll < 50u || model.empty()) + { + const int v = nextValue++; + pool.AddOutput(v); + model.push_back(v); + } + else if (roll < 80u) + { + ASSERT_EQ(pool.GetOutput(), model.front()) << "step " << step; + model.pop_front(); + } + else + { + seed = seed * 1103515245u + 12345u; + const unsigned index = (unsigned) ((seed >> 16) % (uint32_t) model.size()); + pool.RemoveOutputAtIndex(index); + model.erase(model.begin() + index); + } + + ASSERT_EQ(pool.OutputSize(), (unsigned) model.size()) << "step " << step; + ASSERT_EQ(pool.HasOutput(), model.empty() == false) << "step " << step; + } + + // Drain and compare the tail of the run element by element. + for (size_t i = 0; i < model.size(); i++) + ASSERT_EQ(pool.GetOutputAtIndex((unsigned) i), model[i]) << "index " << i; +} + +TEST(ThreadPoolQueues, InputQueueMatchesADequeModelUnderMixedOperations) +{ + IntPool pool; + std::deque model; + + uint32_t seed = 0x5EED1234u; + int nextValue = 0; + + for (int step = 0; step < 4000; step++) + { + seed = seed * 1103515245u + 12345u; + const uint32_t roll = (seed >> 16) % 100u; + + if (roll < 60u || model.empty()) + { + const int v = nextValue++; + pool.AddInput((v & 1) ? AddOneHundred : AddTwoHundred, v); + model.push_back(v); + } + else + { + seed = seed * 1103515245u + 12345u; + const unsigned index = (unsigned) ((seed >> 16) % (uint32_t) model.size()); + pool.RemoveInputAtIndex(index); + model.erase(model.begin() + index); + } + + ASSERT_EQ(pool.InputSize(), (unsigned) model.size()) << "step " << step; + } + + for (size_t i = 0; i < model.size(); i++) + ASSERT_EQ(pool.GetInputAtIndex((unsigned) i), model[i]) << "index " << i; +} From d1063b183749265495a2f978d63119a45edef149 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:38:26 +0200 Subject: [PATCH 18/26] refactor(threadpool): move the three protected queues to std::deque inputFunctionQueue Queue -> std::deque inputQueue Queue -> std::deque outputQueue Queue -> std::deque Issue #60. Tests landed first in 2928ffb8, which also fixed the Clear() lock leak they found. Semantics preserved: * DataStructures::Queue::RemoveAtIndex is a shift-remove, so RemoveInputAtIndex and RemoveOutputAtIndex became erase(begin() + index), not pop_back. * It also silently no-ops when the index is past the end, while deque::erase there is undefined. The bound check moved into the two callers, alongside the RakAssert the old container did under _DEBUG. * Queue::Pop and Queue::operator[] asserted their preconditions under _DEBUG and read past the end otherwise. GetOutput, GetInputAtIndex and GetOutputAtIndex now carry those asserts explicitly -- GetOutput's precondition (HasOutput() first) is the one its doc comment already states. * The worker thread takes the head of both paired queues under inputQueueMutex, in the same critical section as before. New: six worker-thread cases in Tests/Integration/ThreadPoolWorkerTests.cpp. The pairing between inputFunctionQueue and inputQueue is not observable from the public API without a worker -- a removal from only one of them produces no size mismatch, just every later job running with its neighbour's callback -- so the tests tag each callback's output differently and assert the exact sequence. Integration rather than unit because they need a real thread and a deadline poll. Not flaky: 10 repeats, 60/60. Six mutations, all killed, each attribution re-verified against a freshly built binary: paired removal drops only inputQueue -> RemovingAnInputKeepsEveryOther..., RemovingTheHeadInputKeepsTheRest... (unit survives, by design) RemoveInputAtIndex -> swap-remove -> RemoveInputAtIndexShiftsLaterInputs..., RemoveInputAtTheHeadAndAtTheTail, InputQueueMatchesADequeModel... worker pops the tail, not the head -> all five ordering cases RemoveOutputAtIndex -> swap-remove -> RemoveOutputAtIndexShiftsLater..., OutputQueueMatchesADequeModel... ClearInput also clears outputQueue -> ClearInputLeavesTheOutputQueueAlone worker ignores returnOutput -> CallbacksThatSuppressOutputPushNothing thread_pool.h no longer includes ds_queue.h. Two headers in DependentExtensions (AutopatcherServer.h, Lobby2Server.h) were getting DataStructures::Queue transitively through it and now include it themselves. thread_pool.h has no DataStructures:: references left. Verified: macOS full suite 449/449; Debug build clean + Debug unit 376/376. Claude-Session: https://claude.ai/code/session_011kRj5nodTJrxyoHY7y6R6c --- .../Autopatcher/AutopatcherServer.h | 1 + DependentExtensions/Lobby2/Lobby2Server.h | 1 + Source/include/mafianet/thread_pool.h | 78 +++++--- Tests/Integration/ThreadPoolWorkerTests.cpp | 184 ++++++++++++++++++ 4 files changed, 234 insertions(+), 30 deletions(-) create mode 100644 Tests/Integration/ThreadPoolWorkerTests.cpp diff --git a/DependentExtensions/Autopatcher/AutopatcherServer.h b/DependentExtensions/Autopatcher/AutopatcherServer.h index 233b21676..972a59ca9 100644 --- a/DependentExtensions/Autopatcher/AutopatcherServer.h +++ b/DependentExtensions/Autopatcher/AutopatcherServer.h @@ -35,6 +35,7 @@ #include "mafianet/plugin_interface2.h" #include "mafianet/packet_priority.h" #include "mafianet/thread_pool.h" +#include "mafianet/ds_queue.h" #include "mafianet/bit_stream.h" #include "mafianet/string.h" #include "mafianet/file_list.h" diff --git a/DependentExtensions/Lobby2/Lobby2Server.h b/DependentExtensions/Lobby2/Lobby2Server.h index cf2ec63aa..5a93579d8 100644 --- a/DependentExtensions/Lobby2/Lobby2Server.h +++ b/DependentExtensions/Lobby2/Lobby2Server.h @@ -21,6 +21,7 @@ #include "Lobby2Plugin.h" #include "mafianet/ds_ordered_list.h" #include "mafianet/thread_pool.h" +#include "mafianet/ds_queue.h" #include "Lobby2Presence.h" //class PostgreSQLInterface; diff --git a/Source/include/mafianet/thread_pool.h b/Source/include/mafianet/thread_pool.h index 53fa2f418..aa613965b 100644 --- a/Source/include/mafianet/thread_pool.h +++ b/Source/include/mafianet/thread_pool.h @@ -17,12 +17,14 @@ #define __THREAD_POOL_H #include "mafianet/memory_override.h" -#include "mafianet/ds_queue.h" +#include "mafianet/assert.h" #include "mafianet/simple_mutex.h" #include "mafianet/export.h" #include "mafianet/thread.h" #include "mafianet/signaled_event.h" +#include + class ThreadDataInterface { public: @@ -158,9 +160,9 @@ struct RAK_DLL_EXPORT ThreadPool // inputFunctionQueue & inputQueue are paired arrays so if you delete from one at a particular index you must delete from the other // at the same index - DataStructures::Queue inputFunctionQueue; - DataStructures::Queue inputQueue; - DataStructures::Queue outputQueue; + std::deque inputFunctionQueue; + std::deque inputQueue; + std::deque outputQueue; ThreadDataInterface *threadDataInterface; void *tdiContext; @@ -268,10 +270,12 @@ void* WorkerThread( void* arguments ) // Read input data userCallback=0; threadPool->inputQueueMutex.Lock(); - if (threadPool->inputFunctionQueue.Size()) + if (threadPool->inputFunctionQueue.empty()==false) { - userCallback=threadPool->inputFunctionQueue.Pop(); - inputData=threadPool->inputQueue.Pop(); + userCallback=threadPool->inputFunctionQueue.front(); + threadPool->inputFunctionQueue.pop_front(); + inputData=threadPool->inputQueue.front(); + threadPool->inputQueue.pop_front(); } threadPool->inputQueueMutex.Unlock(); @@ -281,7 +285,7 @@ void* WorkerThread( void* arguments ) if (returnOutput) { threadPool->outputQueueMutex.Lock(); - threadPool->outputQueue.Push(callbackOutput, _FILE_AND_LINE_ ); + threadPool->outputQueue.push_back(callbackOutput); threadPool->outputQueueMutex.Unlock(); } } @@ -423,8 +427,8 @@ template void ThreadPool::AddInput(OutputType (*workerThreadCallback)(InputType, bool *returnOutput, void* perThreadData), InputType inputData) { inputQueueMutex.Lock(); - inputQueue.Push(inputData, _FILE_AND_LINE_ ); - inputFunctionQueue.Push(workerThreadCallback, _FILE_AND_LINE_ ); + inputQueue.push_back(inputData); + inputFunctionQueue.push_back(workerThreadCallback); inputQueueMutex.Unlock(); quitAndIncomingDataEvents.SetEvent(); @@ -433,34 +437,34 @@ template void ThreadPool::AddOutput(OutputType outputData) { outputQueueMutex.Lock(); - outputQueue.Push(outputData, _FILE_AND_LINE_ ); + outputQueue.push_back(outputData); outputQueueMutex.Unlock(); } template bool ThreadPool::HasOutputFast(void) { - return outputQueue.IsEmpty()==false; + return outputQueue.empty()==false; } template bool ThreadPool::HasOutput(void) { bool res; outputQueueMutex.Lock(); - res=outputQueue.IsEmpty()==false; + res=outputQueue.empty()==false; outputQueueMutex.Unlock(); return res; } template bool ThreadPool::HasInputFast(void) { - return inputQueue.IsEmpty()==false; + return inputQueue.empty()==false; } template bool ThreadPool::HasInput(void) { bool res; inputQueueMutex.Lock(); - res=inputQueue.IsEmpty()==false; + res=inputQueue.empty()==false; inputQueueMutex.Unlock(); return res; } @@ -470,7 +474,9 @@ OutputType ThreadPool::GetOutput(void) // Real output check OutputType output; outputQueueMutex.Lock(); - output=outputQueue.Pop(); + RakAssert(outputQueue.empty()==false); + output=outputQueue.front(); + outputQueue.pop_front(); outputQueueMutex.Unlock(); return output; } @@ -482,12 +488,12 @@ void ThreadPool::Clear(void) { runThreadsMutex.Unlock(); inputQueueMutex.Lock(); - inputFunctionQueue.Clear(_FILE_AND_LINE_); - inputQueue.Clear(_FILE_AND_LINE_); + inputFunctionQueue.clear(); + inputQueue.clear(); inputQueueMutex.Unlock(); outputQueueMutex.Lock(); - outputQueue.Clear(_FILE_AND_LINE_); + outputQueue.clear(); outputQueueMutex.Unlock(); } else @@ -497,9 +503,9 @@ void ThreadPool::Clear(void) // calling StopThreads -- deadlocked on a non-recursive SimpleMutex. Nothing // in the library calls Clear(), which is why it went unnoticed, but it is a // public header. - inputFunctionQueue.Clear(_FILE_AND_LINE_); - inputQueue.Clear(_FILE_AND_LINE_); - outputQueue.Clear(_FILE_AND_LINE_); + inputFunctionQueue.clear(); + inputQueue.clear(); + outputQueue.clear(); runThreadsMutex.Unlock(); } } @@ -516,18 +522,25 @@ void ThreadPool::UnlockInput(void) template unsigned ThreadPool::InputSize(void) { - return inputQueue.Size(); + return (unsigned) inputQueue.size(); } template InputType ThreadPool::GetInputAtIndex(unsigned index) { + RakAssert(index < inputQueue.size()); return inputQueue[index]; } template void ThreadPool::RemoveInputAtIndex(unsigned index) { - inputQueue.RemoveAtIndex(index); - inputFunctionQueue.RemoveAtIndex(index); + RakAssert(index < inputQueue.size()); + if (index >= inputQueue.size()) + return; + + // Paired arrays: the same index has to go from both, or every later input is + // run with the wrong callback. + inputQueue.erase(inputQueue.begin() + index); + inputFunctionQueue.erase(inputFunctionQueue.begin() + index); } template void ThreadPool::LockOutput(void) @@ -542,29 +555,34 @@ void ThreadPool::UnlockOutput(void) template unsigned ThreadPool::OutputSize(void) { - return outputQueue.Size(); + return (unsigned) outputQueue.size(); } template OutputType ThreadPool::GetOutputAtIndex(unsigned index) { + RakAssert(index < outputQueue.size()); return outputQueue[index]; } template void ThreadPool::RemoveOutputAtIndex(unsigned index) { - outputQueue.RemoveAtIndex(index); + RakAssert(index < outputQueue.size()); + if (index >= outputQueue.size()) + return; + + outputQueue.erase(outputQueue.begin() + index); } template void ThreadPool::ClearInput(void) { - inputQueue.Clear(_FILE_AND_LINE_); - inputFunctionQueue.Clear(_FILE_AND_LINE_); + inputQueue.clear(); + inputFunctionQueue.clear(); } template void ThreadPool::ClearOutput(void) { - outputQueue.Clear(_FILE_AND_LINE_); + outputQueue.clear(); } template bool ThreadPool::IsWorking(void) diff --git a/Tests/Integration/ThreadPoolWorkerTests.cpp b/Tests/Integration/ThreadPoolWorkerTests.cpp new file mode 100644 index 000000000..06cb84d9e --- /dev/null +++ b/Tests/Integration/ThreadPoolWorkerTests.cpp @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + * + * Worker-thread tests for ThreadPool. + * + * The hermetic cases in Tests/Unit/ThreadPoolQueueTests.cpp cover every queue + * operation the public API performs on the calling thread, but one thing is only + * observable once a worker runs: inputFunctionQueue and inputQueue are *paired* + * arrays, and the worker pops one element from each. If a removal ever took the + * element from only one of them, every later job would run with another job's + * callback -- silently, with no size mismatch to notice. These tests pair + * callbacks and inputs so that mix-up changes the output values. + * + * They live here rather than in the unit suite because they need a real thread + * and therefore a deadline poll. + */ + +#include + +#include "mafianet/thread_pool.h" +#include "mafianet/get_time.h" +#include "mafianet/sleep.h" + +#include + +namespace +{ + // Each callback tags its result with a different offset, so an output value + // says which callback produced it and which input it was handed. + int AddOneHundred(int in, bool *returnOutput, void *) { *returnOutput = true; return in + 100; } + int AddTwoHundred(int in, bool *returnOutput, void *) { *returnOutput = true; return in + 200; } + int ProduceNothing(int, bool *returnOutput, void *) { *returnOutput = false; return -1; } + + class ThreadPoolWorker : public ::testing::Test + { + protected: + void TearDown() override + { + pool.StopThreads(); + pool.ClearInput(); + pool.ClearOutput(); + } + + // Collects exactly `expected` outputs, or fails on the deadline. + std::vector DrainOutputs(size_t expected, unsigned int timeoutMS = 10000) + { + std::vector got; + const MafiaNet::TimeMS start = MafiaNet::GetTimeMS(); + while (got.size() < expected && MafiaNet::GetTimeMS() - start < timeoutMS) + { + if (pool.HasOutput()) + got.push_back(pool.GetOutput()); + else + RakSleep(10); + } + return got; + } + + ThreadPool pool; + }; +} + +// The pairing test. Inputs are queued before any thread starts, so the removal +// happens against a queue nothing is draining; then one worker runs them in order. +TEST_F(ThreadPoolWorker, RemovingAnInputKeepsEveryOtherInputWithItsOwnCallback) +{ + pool.AddInput(AddOneHundred, 10); + pool.AddInput(AddTwoHundred, 20); + pool.AddInput(AddOneHundred, 30); + pool.AddInput(AddTwoHundred, 40); + + pool.RemoveInputAtIndex(1); // drops (AddTwoHundred, 20) + + ASSERT_EQ(pool.InputSize(), 3u); + + ASSERT_TRUE(pool.StartThreads(1, 0)); + + const std::vector got = DrainOutputs(3); + + ASSERT_EQ(got.size(), 3u) << "expected three outputs, got " << got.size(); + + // A single worker drains the queue head-first, so the order is determined. + // 230 or 140 here would mean an input was run with its neighbour's callback. + ASSERT_EQ(got[0], 110) << "AddOneHundred(10)"; + ASSERT_EQ(got[1], 130) << "AddOneHundred(30)"; + ASSERT_EQ(got[2], 240) << "AddTwoHundred(40)"; +} + +// Removing the head, where an off-by-one in the paired removal is easiest to hide. +TEST_F(ThreadPoolWorker, RemovingTheHeadInputKeepsTheRestPaired) +{ + pool.AddInput(AddOneHundred, 1); + pool.AddInput(AddTwoHundred, 2); + pool.AddInput(AddOneHundred, 3); + + pool.RemoveInputAtIndex(0); // drops (AddOneHundred, 1) + + ASSERT_TRUE(pool.StartThreads(1, 0)); + + const std::vector got = DrainOutputs(2); + + ASSERT_EQ(got.size(), 2u); + ASSERT_EQ(got[0], 202) << "AddTwoHundred(2)"; + ASSERT_EQ(got[1], 103) << "AddOneHundred(3)"; +} + +TEST_F(ThreadPoolWorker, WorkerRunsQueuedJobsInFifoOrder) +{ + for (int i = 1; i <= 8; i++) + pool.AddInput(AddOneHundred, i); + + ASSERT_TRUE(pool.StartThreads(1, 0)); + + const std::vector got = DrainOutputs(8); + + ASSERT_EQ(got.size(), 8u); + for (int i = 0; i < 8; i++) + ASSERT_EQ(got[(size_t) i], 100 + i + 1) << "output " << i << " is out of order"; +} + +// A callback that clears returnOutput must not push anything, while the jobs +// around it still do. +TEST_F(ThreadPoolWorker, CallbacksThatSuppressOutputPushNothing) +{ + pool.AddInput(AddOneHundred, 1); + pool.AddInput(ProduceNothing, 2); + pool.AddInput(AddOneHundred, 3); + + ASSERT_TRUE(pool.StartThreads(1, 0)); + + const std::vector got = DrainOutputs(2); + + ASSERT_EQ(got.size(), 2u); + ASSERT_EQ(got[0], 101); + ASSERT_EQ(got[1], 103); + + // Give the pool a moment to produce a third output it must not produce. + const MafiaNet::TimeMS start = MafiaNet::GetTimeMS(); + while (MafiaNet::GetTimeMS() - start < 200) + { + ASSERT_FALSE(pool.HasOutput()) << "ProduceNothing pushed an output"; + RakSleep(10); + } +} + +// Jobs added while the worker is already running are picked up too, which is the +// path send_to_thread and file_list_transfer actually use. +TEST_F(ThreadPoolWorker, JobsAddedAfterStartupAreStillRun) +{ + ASSERT_TRUE(pool.StartThreads(1, 0)); + + pool.AddInput(AddOneHundred, 5); + pool.AddInput(AddTwoHundred, 6); + + const std::vector got = DrainOutputs(2); + + ASSERT_EQ(got.size(), 2u); + ASSERT_EQ(got[0], 105); + ASSERT_EQ(got[1], 206); +} + +// StopThreads must leave any unprocessed input in place, so the caller can walk +// the queue and free whatever it owns -- the contract AddInput's comment states +// and send_to_thread's shutdown path relies on. +TEST_F(ThreadPoolWorker, StopThreadsLeavesUnprocessedInputInTheQueue) +{ + ASSERT_TRUE(pool.StartThreads(1, 0)); + + // Let the worker reach its idle wait before queueing anything, so the jobs + // below are not consumed between StartThreads and StopThreads. + RakSleep(100); + pool.StopThreads(); + + pool.AddInput(AddOneHundred, 7); + pool.AddInput(AddOneHundred, 8); + + ASSERT_EQ(pool.InputSize(), 2u) << "input queued after StopThreads must stay queued"; + ASSERT_EQ(pool.GetInputAtIndex(0), 7); + ASSERT_EQ(pool.GetInputAtIndex(1), 8); + ASSERT_FALSE(pool.HasOutput()); +} From 09ca549b52de53cef9aab961b079da6c33c98769 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:59:47 +0200 Subject: [PATCH 19/26] fix(tcpinterface): correct the not-found value in four lookups I broke in 35f8f0c2 35f8f0c2's migration of tcp_interface.cpp replaced DataStructures::List::GetIndexOf with std::find arithmetic but kept the old "not found" comparison. GetIndexOf returned (unsigned)-1 when the value was absent; `std::find(...) - begin()` returns size(). Every one of the four converted lookups therefore tested the wrong thing, and in two of them the test is inverted: StartSSLClient `if (index==(unsigned)-1) push_back(...)` can never be true, so an address was never added to activeSSLConnections and IsSSLActive always said false. DetachPlugin `if (index!=MAX_UNSIGNED_LONG)` is always true, so detaching a plugin that was never attached ran messageHandlerList[size()]->OnDetach() -- one past the end, or a dereference of nothing on an empty list -- and then swap-removed a plugin that was still attached. CloseConnection same always-true test, then erase(begin() + size()). SocketConnect same always-true test, and the replacement turned one statement into two without adding braces, so pop_back() ran unconditionally -- including on an empty list -- while the swap-assignment was the only thing guarded. All four now compare iterators against end() and drop the index entirely, so there is no sentinel left to get wrong. The StartSSLClient and CloseConnection sites are inside OPEN_SSL_CLIENT_SUPPORT==1, which defaults to 0 and is not compiled here; they are fixed but not covered by a test, and I am not claiming otherwise. The other two are always compiled. I audited every index lookup this branch converted. These four are the only ones that got it wrong: the team_manager and team_balancer sites go through an IndexOf helper that preserves (unsigned)-1, and the sorted containers use lower_bound with an explicit key comparison. No raw `std::find(...) - begin()` remains anywhere in Source/. New: Tests/Unit/TCPInterfacePluginTests.cpp, 6 hermetic cases over the plugin list, which had no tests. Against the broken DetachPlugin, three segfault and three pass: AttachThenDetachCallsEachHookOnce survives (index 0 of 1 is valid) DetachingTheMiddleOfThreeKeepsTheOtherTwo survives DetachingNullIsIgnored survives AttachingTheSamePluginTwiceAttachesItOnce crash DetachingAnUnattachedPluginLeavesTheAttachedOneAlone crash DetachingFromAnEmptyListIsANoOp crash The three that survive are the shape the rest of the codebase already uses -- detaching a plugin you attached, where the wrong sentinel happens to give the right index. That is why nothing caught this. Claude-Session: https://claude.ai/code/session_011kRj5nodTJrxyoHY7y6R6c --- Source/src/tcp_interface.cpp | 31 ++--- Tests/Unit/TCPInterfacePluginTests.cpp | 151 +++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 14 deletions(-) create mode 100644 Tests/Unit/TCPInterfacePluginTests.cpp diff --git a/Source/src/tcp_interface.cpp b/Source/src/tcp_interface.cpp index 98083aaf9..ae30ef43d 100644 --- a/Source/src/tcp_interface.cpp +++ b/Source/src/tcp_interface.cpp @@ -417,8 +417,8 @@ void TCPInterface::StartSSLClient(SystemAddress systemAddress) SystemAddress *id = startSSL.Allocate( _FILE_AND_LINE_ ); *id=systemAddress; startSSL.Push(id); - unsigned index = (unsigned) (std::find(activeSSLConnections.begin(), activeSSLConnections.end(), systemAddress) - activeSSLConnections.begin()); - if (index==(unsigned)-1) + if (std::find(activeSSLConnections.begin(), activeSSLConnections.end(), systemAddress) + == activeSSLConnections.end()) activeSSLConnections.push_back(systemAddress); } bool TCPInterface::IsSSLActive(SystemAddress systemAddress) @@ -554,15 +554,15 @@ void TCPInterface::DetachPlugin( PluginInterface2 *plugin ) if (plugin==0) return; - unsigned int index; - index = (unsigned int) (std::find(messageHandlerList.begin(), messageHandlerList.end(), plugin) - messageHandlerList.begin()); - if (index!=MAX_UNSIGNED_LONG) + std::vector::iterator it = + std::find(messageHandlerList.begin(), messageHandlerList.end(), plugin); + if (it!=messageHandlerList.end()) { - messageHandlerList[index]->OnDetach(); + (*it)->OnDetach(); // Unordered list so delete from end for speed // Swap-remove, exactly as before: the handler order is not published and this avoids // shifting the rest. - messageHandlerList[index]=messageHandlerList.back(); + *it=messageHandlerList.back(); messageHandlerList.pop_back(); plugin->SetTCPInterface(0); } @@ -601,9 +601,10 @@ void TCPInterface::CloseConnection( SystemAddress systemAddress ) #if OPEN_SSL_CLIENT_SUPPORT==1 - unsigned index = (unsigned) (std::find(activeSSLConnections.begin(), activeSSLConnections.end(), systemAddress) - activeSSLConnections.begin()); - if (index!=(unsigned)-1) - activeSSLConnections.erase(activeSSLConnections.begin() + index); + std::vector::iterator sslIt = + std::find(activeSSLConnections.begin(), activeSSLConnections.end(), systemAddress); + if (sslIt!=activeSSLConnections.end()) + activeSSLConnections.erase(sslIt); #endif } void TCPInterface::DeallocatePacket( Packet *packet ) @@ -876,13 +877,15 @@ __TCPSOCKET__ TCPInterface::SocketConnect(const char* host, unsigned short remot if (connectResult==-1) { - unsigned sockfdIndex; blockingSocketListMutex.Lock(); - sockfdIndex=(unsigned) (std::find(blockingSocketList.begin(), blockingSocketList.end(), sockfd) - blockingSocketList.begin()); - if (sockfdIndex!=(unsigned)-1) + std::vector<__TCPSOCKET__>::iterator sockfdIt = + std::find(blockingSocketList.begin(), blockingSocketList.end(), sockfd); + if (sockfdIt!=blockingSocketList.end()) + { // Swap-remove as before; this order is not published. - blockingSocketList[sockfdIndex]=blockingSocketList.back(); + *sockfdIt=blockingSocketList.back(); blockingSocketList.pop_back(); + } blockingSocketListMutex.Unlock(); closesocket__(sockfd); diff --git a/Tests/Unit/TCPInterfacePluginTests.cpp b/Tests/Unit/TCPInterfacePluginTests.cpp new file mode 100644 index 000000000..0d74242a2 --- /dev/null +++ b/Tests/Unit/TCPInterfacePluginTests.cpp @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + */ + +#include + +#include "mafianet/tcp_interface.h" +#include "mafianet/plugin_interface2.h" + +#if _RAKNET_SUPPORT_TCPInterface == 1 + +using namespace MafiaNet; + +/* +Covers TCPInterface's plugin list, which has no tests. + +The reason these exist: DetachPlugin looks the plugin up and acts on the result, +and the "not found" case is the one that goes wrong quietly. DataStructures::List +::GetIndexOf returned (unsigned)-1 when absent; std::find returns end(), and the +index form of that is size(), not -1. A detach of a plugin that was never attached +then indexes one past the end and swap-removes a plugin that is still attached -- +with no size mismatch and no error to notice. So the cases below detach plugins +that are not in the list, and detach the same plugin twice. + +Hermetic: Start(0, 0) creates no listen socket, and the plugin list is touched +only by the calling thread. +*/ + +namespace +{ + class CountingPlugin : public PluginInterface2 + { + public: + CountingPlugin() : attachCount(0), detachCount(0) {} + + void OnAttach(void) override { attachCount++; } + void OnDetach(void) override { detachCount++; } + + int attachCount; + int detachCount; + }; + + class TCPInterfacePlugins : public ::testing::Test + { + protected: + void SetUp() override { ASSERT_TRUE(tcp.Start(0, 0)); } + void TearDown() override { tcp.Stop(); } + + TCPInterface tcp; + }; +} + +TEST_F(TCPInterfacePlugins, AttachThenDetachCallsEachHookOnce) +{ + CountingPlugin a; + + tcp.AttachPlugin(&a); + ASSERT_EQ(a.attachCount, 1); + ASSERT_EQ(a.detachCount, 0); + + tcp.DetachPlugin(&a); + ASSERT_EQ(a.attachCount, 1); + ASSERT_EQ(a.detachCount, 1); +} + +TEST_F(TCPInterfacePlugins, AttachingTheSamePluginTwiceAttachesItOnce) +{ + CountingPlugin a; + + tcp.AttachPlugin(&a); + tcp.AttachPlugin(&a); + + ASSERT_EQ(a.attachCount, 1) << "the second attach must be ignored"; + + // And one detach is enough to remove it. + tcp.DetachPlugin(&a); + ASSERT_EQ(a.detachCount, 1); + + tcp.DetachPlugin(&a); + ASSERT_EQ(a.detachCount, 1) << "the second detach must be a no-op"; +} + +// The case the index arithmetic gets wrong: detaching a plugin that was never +// attached must not touch the plugin that was. +TEST_F(TCPInterfacePlugins, DetachingAnUnattachedPluginLeavesTheAttachedOneAlone) +{ + CountingPlugin attached; + CountingPlugin never; + + tcp.AttachPlugin(&attached); + + tcp.DetachPlugin(&never); + + ASSERT_EQ(never.detachCount, 0) << "a plugin that was never attached must not be detached"; + ASSERT_EQ(attached.detachCount, 0) << "the attached plugin must not have been detached"; + + // If the previous call had swap-removed the attached plugin, this detach would + // find nothing to do. + tcp.DetachPlugin(&attached); + ASSERT_EQ(attached.detachCount, 1) << "the attached plugin was no longer in the list"; +} + +TEST_F(TCPInterfacePlugins, DetachingFromAnEmptyListIsANoOp) +{ + CountingPlugin never; + + tcp.DetachPlugin(&never); + + ASSERT_EQ(never.attachCount, 0); + ASSERT_EQ(never.detachCount, 0); +} + +TEST_F(TCPInterfacePlugins, DetachingTheMiddleOfThreeKeepsTheOtherTwo) +{ + CountingPlugin a, b, c; + + tcp.AttachPlugin(&a); + tcp.AttachPlugin(&b); + tcp.AttachPlugin(&c); + + tcp.DetachPlugin(&b); + + ASSERT_EQ(b.detachCount, 1); + ASSERT_EQ(a.detachCount, 0); + ASSERT_EQ(c.detachCount, 0); + + // Both survivors must still be reachable -- the swap-remove moves c into b's + // slot, and losing track of it here is the classic off-by-one. + tcp.DetachPlugin(&a); + tcp.DetachPlugin(&c); + ASSERT_EQ(a.detachCount, 1) << "a was lost from the list"; + ASSERT_EQ(c.detachCount, 1) << "c was lost when it was swapped into b's slot"; +} + +TEST_F(TCPInterfacePlugins, DetachingNullIsIgnored) +{ + CountingPlugin a; + tcp.AttachPlugin(&a); + + tcp.DetachPlugin(0); + + ASSERT_EQ(a.detachCount, 0); + + tcp.DetachPlugin(&a); + ASSERT_EQ(a.detachCount, 1); +} + +#endif // _RAKNET_SUPPORT_TCPInterface From dc4ca170fcc2beda8144435b6acfbda82c1be455 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:00:15 +0200 Subject: [PATCH 20/26] fix(tcpinterface): stop closing descriptors it does not own, and report a failed bind Three defects, all in Start/Stop, found while writing the first tests PacketizedTCP has ever had: every test after the first one in the same process failed, with the client connecting successfully and the server never reporting the incoming connection. **Stop() closed descriptor 0, once per unused connection slot.** RemoteClient's constructor sets `socket = 0`, and 0 is this class's "no socket" value -- RemoteClient::SetActive already guards on it. Stop() did not, and closed every slot unconditionally. On POSIX 0 is a valid descriptor: stdin in a normal process. A server started with maxIncomingConnections of 4 and one connection called close(0) three times. The second-order effect is what the tests actually tripped over. Once descriptor 0 is free, the next socket created *anywhere in the process* is handed number 0, and every `listenSocket != 0` check then reads it as absent. A second TCPInterface started, returned true, and never accepted anything; instrumenting CreateListenSocket showed `listenSocket=0` on the second cycle against `listenSocket=3` on the first. Stop() also skipped closing that listener, leaking it. Fixed by guarding on `socket != 0`, matching SetActive. Using 0 as the sentinel for a descriptor remains wrong in principle, and is still how listenSocket is handled throughout this file; with Stop() no longer closing descriptors it does not own, a normal process keeps stdin on 0 and the collision is unreachable. Worth replacing properly, not in this commit. **Stop() did not clear blockingSocketList.** It closed each descriptor in the list and left them there, so a second Stop(), or the one the destructor performs, closed the same numbers again. Descriptor numbers are reused, so the second close can land on a socket something else has since opened. The list is now cleared. **Start() discarded CreateListenSocket's return value.** A failed bind -- the port already in use -- left Start() returning true with nothing listening and the caller unable to find out. Start() now tears down what it allocated and returns false, and an interface whose Start failed can be stopped and started again. New: Tests/Integration/TCPInterfaceLifecycleTests.cpp, 5 cases, plus two reusable helpers. TCPInterface::Start takes a port and exposes no accessor for the one it bound, so a listening test cannot use port 0 and read the result back the way the peer tests do; TestHelpers::FindFreeTcpPort binds a throwaway socket to port 0 and reports the assigned port, which beats adding another hard-coded port to a suite that is already RUN_SERIAL because of them. That alone was flaky: 1 failure in 90 repeats, `Start` returning false. The probe binds 127.0.0.1 while TCPInterface binds INADDR_ANY, and the OS reissues ephemeral port numbers soon after a test releases them, so a bind can hit a port in use on another interface or still in TIME_WAIT from a previous test's accepted connection -- TCPInterface sets no SO_REUSEADDR. TestHelpers::StartTcpOnAFreePort retries the port selection, which absorbs that without retrying anything a test asserts. 168/168 across 12 repeats of both new suites afterwards. Each fix is pinned independently, by reverting one at a time against a build I checked each time: revert the Stop() guard + clear -> all 5 fail (the two descriptor-0 cases directly; the other three because the freed descriptor 0 is reassigned and read as "no socket") revert the Start() return -> StartFailsWhenThePortIsAlreadyInUse, AnInterfaceWhoseStartFailedCanStartAgain The two descriptor-0 cases are POSIX-only: on Windows descriptor 0 is not a socket handle, so they are compiled out there and the behavioural three-cycle case carries that platform. Claude-Session: https://claude.ai/code/session_011kRj5nodTJrxyoHY7y6R6c --- Source/src/tcp_interface.cpp | 31 ++- .../TCPInterfaceLifecycleTests.cpp | 182 ++++++++++++++++++ Tests/Support/TestHelpers.cpp | 77 ++++++++ Tests/Support/TestHelpers.h | 24 +++ 4 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 Tests/Integration/TCPInterfaceLifecycleTests.cpp diff --git a/Source/src/tcp_interface.cpp b/Source/src/tcp_interface.cpp index ae30ef43d..f8c94932c 100644 --- a/Source/src/tcp_interface.cpp +++ b/Source/src/tcp_interface.cpp @@ -207,7 +207,22 @@ bool TCPInterface::Start(unsigned short port, unsigned short maxIncomingConnecti listenSocket=0; if (maxIncomingConnections>0) { - CreateListenSocket(port, maxIncomingConnections, socketFamily, bindAddress); + // The result was discarded, so a failed bind -- the port already in use, say + // -- left Start() returning true with nothing listening, and the caller had + // no way to find out. + if (CreateListenSocket(port, maxIncomingConnections, socketFamily, bindAddress)==false) + { + if (listenSocket!=0) + { + closesocket__(listenSocket); + listenSocket=0; + } + remoteClientsLength=0; + MafiaNet::OP_DELETE_ARRAY(remoteClients,_FILE_AND_LINE_); + remoteClients=0; + isStarted.Decrement(); + return false; + } } @@ -266,6 +281,11 @@ void TCPInterface::Stop(void) { closesocket__(blockingSocketList[i]); } + // The list was not cleared here, so a second Stop() -- or the one in the + // destructor -- closed the same descriptors again. Descriptor numbers are + // reused, so the second close can land on a socket something else has since + // opened. + blockingSocketList.clear(); blockingSocketListMutex.Unlock(); // Wait for the thread to stop @@ -279,7 +299,14 @@ void TCPInterface::Stop(void) // Stuff from here on to the end of the function is not threadsafe for (i=0; i < remoteClientsLength; i++) { - closesocket__(remoteClients[i].socket); + // 0 is this class's "no socket" value (RemoteClient's constructor sets it, + // and RemoteClient::SetActive guards on it), but it is also a perfectly + // valid descriptor. Closing it unconditionally closed descriptor 0 -- + // stdin in a normal process -- once per unused slot. Beyond the obvious, + // that freed descriptor 0 for reuse, and the next socket created in the + // process got it and was then read as "no socket" by every check here. + if (remoteClients[i].socket!=0) + closesocket__(remoteClients[i].socket); #if OPEN_SSL_CLIENT_SUPPORT==1 remoteClients[i].FreeSSL(); #endif diff --git a/Tests/Integration/TCPInterfaceLifecycleTests.cpp b/Tests/Integration/TCPInterfaceLifecycleTests.cpp new file mode 100644 index 000000000..33ed2da61 --- /dev/null +++ b/Tests/Integration/TCPInterfaceLifecycleTests.cpp @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + * + * Start/Stop lifecycle tests for TCPInterface. + * + * These exist because of a specific failure: TCPInterface uses 0 as its + * "no socket" value, and Stop() closed remoteClients[i].socket for every slot, + * including the ones that were never used and still held that 0. On POSIX, 0 is + * a perfectly good descriptor -- stdin in a normal process -- so Stop() closed + * it, once per unused slot. The visible consequence was worse than the obvious + * one: with descriptor 0 free, the next socket created anywhere in the process + * was handed number 0, and every `listenSocket != 0` check then read it as + * "no socket". A second TCPInterface in the same process would start, report + * success, and never accept anything. + * + * They run here rather than in the unit suite because proving a listener works + * takes a real connection. + */ + +#include + +#include "mafianet/tcp_interface.h" + +#if _RAKNET_SUPPORT_TCPInterface == 1 + +#include "mafianet/get_time.h" +#include "mafianet/sleep.h" +#include "TestHelpers.h" + +#ifndef _WIN32 +#include +#endif + +using namespace MafiaNet; + +namespace +{ + class TCPInterfaceLifecycle : public ::testing::Test + { + protected: + // Brings up a listener and a client, connects them, and tears both down. + // Returns true if both sides observed the connection. + static bool RunOneCycle(unsigned int timeoutMS = 10000) + { + TCPInterface server; + TCPInterface client; + + // Four slots, one of which gets used: the other three are the unused + // slots whose socket value is 0. + const unsigned short port = TestHelpers::StartTcpOnAFreePort(server, 4); + if (port == 0) + return false; + if (client.Start(0, 0) == false) + return false; + + client.Connect("127.0.0.1", port, false); + + SystemAddress completed = UNASSIGNED_SYSTEM_ADDRESS; + SystemAddress incoming = UNASSIGNED_SYSTEM_ADDRESS; + const TimeMS start = GetTimeMS(); + while ((completed == UNASSIGNED_SYSTEM_ADDRESS || incoming == UNASSIGNED_SYSTEM_ADDRESS) + && GetTimeMS() - start < timeoutMS) + { + if (completed == UNASSIGNED_SYSTEM_ADDRESS) + completed = client.HasCompletedConnectionAttempt(); + if (incoming == UNASSIGNED_SYSTEM_ADDRESS) + incoming = server.HasNewIncomingConnection(); + if (completed == UNASSIGNED_SYSTEM_ADDRESS || incoming == UNASSIGNED_SYSTEM_ADDRESS) + RakSleep(10); + } + + const bool ok = (completed != UNASSIGNED_SYSTEM_ADDRESS) + && (incoming != UNASSIGNED_SYSTEM_ADDRESS); + + client.Stop(); + server.Stop(); + return ok; + } + }; +} + +// The behavioural form, portable: three Start/Stop cycles in one process. The +// first one always worked; the second was the one that silently stopped +// accepting once descriptor 0 had been freed and reassigned. +TEST_F(TCPInterfaceLifecycle, RepeatedStartStopCyclesKeepAccepting) +{ + ASSERT_TRUE(RunOneCycle()) << "cycle 1 failed"; + ASSERT_TRUE(RunOneCycle()) << "cycle 2 failed -- a previous Stop() left the process in a bad state"; + ASSERT_TRUE(RunOneCycle()) << "cycle 3 failed"; +} + +#ifndef _WIN32 +// The direct form: descriptor 0 must still be open afterwards. POSIX only -- +// on Windows descriptor 0 is not a socket handle and SOCKET 0 does not collide +// with stdin the same way. +TEST_F(TCPInterfaceLifecycle, StopDoesNotCloseDescriptorZero) +{ + ASSERT_NE(fcntl(0, F_GETFD), -1) << "descriptor 0 was already closed before the test ran"; + + { + TCPInterface tcp; + // Eight slots, none of them ever used, so eight slots hold the 0 value. + ASSERT_NE(TestHelpers::StartTcpOnAFreePort(tcp, 8), 0); + tcp.Stop(); + } + + ASSERT_NE(fcntl(0, F_GETFD), -1) + << "Stop() closed descriptor 0 -- it closed the unused remoteClients slots' 0 value"; +} + +// Stopping twice must not close anything a second time either. +TEST_F(TCPInterfaceLifecycle, StoppingTwiceDoesNotCloseDescriptorZero) +{ + ASSERT_NE(fcntl(0, F_GETFD), -1); + + { + TCPInterface tcp; + ASSERT_NE(TestHelpers::StartTcpOnAFreePort(tcp, 4), 0); + tcp.Stop(); + tcp.Stop(); + } + + ASSERT_NE(fcntl(0, F_GETFD), -1); +} +#endif + +// Start() discarded CreateListenSocket's result, so a bind failure produced a +// started interface with no listener and a return value of true. +TEST_F(TCPInterfaceLifecycle, StartFailsWhenThePortIsAlreadyInUse) +{ + TCPInterface first; + const unsigned short port = TestHelpers::StartTcpOnAFreePort(first, 2); + ASSERT_NE(port, 0) << "the first listener should have bound a port"; + + TCPInterface second; + ASSERT_FALSE(second.Start(port, 2)) << "Start() must report a failed bind"; + + // And the first one still works afterwards. + TCPInterface client; + ASSERT_TRUE(client.Start(0, 0)); + client.Connect("127.0.0.1", port, false); + + SystemAddress incoming = UNASSIGNED_SYSTEM_ADDRESS; + const TimeMS start = GetTimeMS(); + while (incoming == UNASSIGNED_SYSTEM_ADDRESS && GetTimeMS() - start < 10000) + { + incoming = first.HasNewIncomingConnection(); + if (incoming == UNASSIGNED_SYSTEM_ADDRESS) + RakSleep(10); + } + + client.Stop(); + first.Stop(); + second.Stop(); + + ASSERT_NE(incoming, UNASSIGNED_SYSTEM_ADDRESS) << "the surviving listener stopped accepting"; +} + +// A failed Start must leave the interface stoppable and restartable. +TEST_F(TCPInterfaceLifecycle, AnInterfaceWhoseStartFailedCanStartAgain) +{ + TCPInterface blocker; + const unsigned short taken = TestHelpers::StartTcpOnAFreePort(blocker, 1); + ASSERT_NE(taken, 0); + + TCPInterface tcp; + ASSERT_FALSE(tcp.Start(taken, 1)); + + tcp.Stop(); // must be harmless after a failed Start + + const bool restarted = TestHelpers::StartTcpOnAFreePort(tcp, 1) != 0; + + tcp.Stop(); + blocker.Stop(); + + ASSERT_TRUE(restarted) << "a failed Start left the interface unusable"; +} + +#endif // _RAKNET_SUPPORT_TCPInterface diff --git a/Tests/Support/TestHelpers.cpp b/Tests/Support/TestHelpers.cpp index 0c2d492c3..3779a0490 100644 --- a/Tests/Support/TestHelpers.cpp +++ b/Tests/Support/TestHelpers.cpp @@ -10,6 +10,15 @@ #include "TestHelpers.h" +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#endif + TestHelpers::TestHelpers(void) { } @@ -132,3 +141,71 @@ void RecieveForXTime(RakPeerInterface *reciever,int millisecondsToWait) } } + +unsigned short TestHelpers::FindFreeTcpPort(void) +{ +#ifdef _WIN32 + // TCPInterface::Start calls WSAStartup, but this helper runs before it, so ask + // for Winsock here too. WSAStartup/WSACleanup are reference counted. + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) + return 0; +#endif + + unsigned short port = 0; + +#ifdef _WIN32 + SOCKET s = socket(AF_INET, SOCK_STREAM, 0); + if (s != INVALID_SOCKET) +#else + int s = socket(AF_INET, SOCK_STREAM, 0); + if (s >= 0) +#endif + { + sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = 0; // let the OS choose + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + if (bind(s, (sockaddr *) &addr, sizeof(addr)) == 0) + { + sockaddr_in bound; + memset(&bound, 0, sizeof(bound)); +#ifdef _WIN32 + int boundLen = (int) sizeof(bound); +#else + socklen_t boundLen = (socklen_t) sizeof(bound); +#endif + if (getsockname(s, (sockaddr *) &bound, &boundLen) == 0) + port = ntohs(bound.sin_port); + } + +#ifdef _WIN32 + closesocket(s); +#else + close(s); +#endif + } + +#ifdef _WIN32 + WSACleanup(); +#endif + + return port; +} + +unsigned short TestHelpers::StartTcpOnAFreePort(TCPInterface &tcp, unsigned short maxIncomingConnections, + int attempts) +{ + for (int i = 0; i < attempts; i++) + { + const unsigned short port = FindFreeTcpPort(); + if (port == 0) + continue; + if (tcp.Start(port, maxIncomingConnections)) + return port; + // Start() tears itself down on a failed bind, so it is safe to call again. + } + return 0; +} diff --git a/Tests/Support/TestHelpers.h b/Tests/Support/TestHelpers.h index dd04f8054..bfa99e762 100644 --- a/Tests/Support/TestHelpers.h +++ b/Tests/Support/TestHelpers.h @@ -17,6 +17,7 @@ #include "mafianet/message_identifiers.h" #include "mafianet/bit_stream.h" #include "mafianet/peer.h" +#include "mafianet/tcp_interface.h" #include "mafianet/sleep.h" #include "DebugTools.h" #include "CommonFunctions.h" @@ -42,4 +43,27 @@ class TestHelpers static void RecieveForXTime(RakPeerInterface *reciever,int millisecondsToWait); static bool SendTestPacketDirected(RakPeerInterface *sender,char * ip,int port,MafiaNet::Reliability rel=MafiaNet::Reliability::ReliableOrdered,MafiaNet::Priority pr=MafiaNet::Priority::High,int typeNum=ID_USER_PACKET_ENUM+1); + /// Asks the OS for a free TCP port on the loopback interface and gives it back. + /// + /// TCPInterface::Start takes a port and exposes no accessor for the one it bound, + /// so a test that needs a listening TCPInterface cannot use port 0 and read the + /// result back the way the peer tests do. This binds a throwaway socket to port 0, + /// reads the assigned port and closes it, so the caller gets a port that was free a + /// moment ago instead of a hard-coded one. Returns 0 if no port could be obtained. + static unsigned short FindFreeTcpPort(void); + + /// Starts \a tcp listening on a port that was free, retrying on a different + /// port if the bind loses the race. + /// + /// FindFreeTcpPort probes 127.0.0.1 while TCPInterface binds INADDR_ANY, and + /// the OS hands out ephemeral numbers again soon after a test releases them, + /// so a bind can fail because the port is in use on another interface or still + /// in TIME_WAIT from an earlier connection -- TCPInterface sets no + /// SO_REUSEADDR. Retrying the port selection absorbs that; it does not retry + /// anything the caller is asserting about. + /// + /// \return the port it is listening on, or 0 if every attempt failed. + static unsigned short StartTcpOnAFreePort(TCPInterface &tcp, unsigned short maxIncomingConnections, + int attempts = 20); + }; From 03acdddb5fcd9c9d3568c2aea92c4d8f1c4b81cf Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:09:27 +0200 Subject: [PATCH 21/26] refactor(packetizedtcp): move the protected containers to std containers waitingPackets Queue -> std::deque connections Map -> std::map _newIncomingConnections Queue -> std::deque _lostConnections Queue -> std::deque _failedConnectionAttempts Queue -> std::deque _completedConnectionAttempts Queue -> std::deque Issue #60. packetized_tcp.h now has no stage-2 DataStructures:: references; DataStructures::ByteQueue stays, it is not one of the types in scope. `connections` was only ever used as a dictionary: every index came from a key lookup (Has, then GetIndexAtKey, then operator[]), so std::map::find and an iterator replace the whole pattern and the positional accessors disappear. DataStructures::Map kept its entries in key order and so does std::map, but nothing here depended on that. New: Tests/Integration/PacketizedTCPFramingTests.cpp, 9 cases. PacketizedTCP had no tests at all, and none of this is reachable without a real connection -- Send() on an address with no entry in `connections` returns without sending, and the notification queues are fed from TCPInterface's own lock-free queues -- so they run over loopback TCP. They cover a single message's exact length, a batch of twelve in one segment, a 200KB payload reassembled across segments, a small message behind a large one, both directions at once, close and failed-attempt notifications, and two clients at once. Five mutations; three killed, two survived and I am not going to pretend otherwise: waitingPackets drained as a stack -> ABatchOfMessagesInOneSegment..., ASmallMessageAfterALargeOne... reassembled messages queued at head -> same two AddToConnectionList does not insert -> six of the nine reassembly buffer looked up as the first map entry -> SURVIVES with small payloads, because two complete messages written into one buffer still read back correctly in sequence. It only breaks when the reads interleave, so TwoClientsAreFramedSeparately now sends 180KB of 'A' from one client and 180KB of 'B' from the other and asserts both arrive byte-exact; that kills it. RemoveFromConnectionList erases nothing -> SURVIVES. The lost-connection notification is pushed before the removal, so nothing observable changes; the entry leaks until ClearAllConnections. Making it observable needs the same address to reconnect, which means controlling the client's ephemeral port. Not covered. Writing the 180KB two-client case also turned up that PacketizedTCP emits an ID_DOWNLOAD_PROGRESS packet every 64KB while a message is incomplete -- my collector was counting those as messages. It now skips and counts them separately, and the next commit pins that path. AddToConnectionList keeps DataStructures::Map::SetNew's duplicate-key assert and additionally frees the existing buffer instead of overwriting the pointer, which is what SetNew did under the assert in a release build. Claude-Session: https://claude.ai/code/session_011kRj5nodTJrxyoHY7y6R6c --- Source/include/mafianet/packetized_tcp.h | 14 +- Source/src/packetized_tcp.cpp | 98 +++-- .../Integration/PacketizedTCPFramingTests.cpp | 370 ++++++++++++++++++ 3 files changed, 439 insertions(+), 43 deletions(-) create mode 100644 Tests/Integration/PacketizedTCPFramingTests.cpp diff --git a/Source/include/mafianet/packetized_tcp.h b/Source/include/mafianet/packetized_tcp.h index a67996dc4..220a36dbc 100644 --- a/Source/include/mafianet/packetized_tcp.h +++ b/Source/include/mafianet/packetized_tcp.h @@ -26,7 +26,9 @@ #include "mafianet/tcp_interface.h" #include "mafianet/ds_byte_queue.h" -#include "mafianet/ds_map.h" + +#include +#include namespace MafiaNet { @@ -77,11 +79,15 @@ class RAK_DLL_EXPORT PacketizedTCP : public TCPInterface Packet *ReturnOutgoingPacket(void); // A single TCP recieve may generate multiple split packets. They are stored in the waitingPackets list until Receive is called - DataStructures::Queue waitingPackets; - DataStructures::Map connections; + std::deque waitingPackets; + + // Per-connection reassembly buffers, keyed by address. Ordered by + // SystemAddress::operator<, as DataStructures::Map was; nothing depends on the + // order, only on lookup by address. + std::map connections; // Mirrors single producer / consumer, but processes them in Receive() before returning to user - DataStructures::Queue _newIncomingConnections, _lostConnections, _failedConnectionAttempts, _completedConnectionAttempts; + std::deque _newIncomingConnections, _lostConnections, _failedConnectionAttempts, _completedConnectionAttempts; }; } // namespace MafiaNet diff --git a/Source/src/packetized_tcp.cpp b/Source/src/packetized_tcp.cpp index eadc9cdb0..3312d96aa 100644 --- a/Source/src/packetized_tcp.cpp +++ b/Source/src/packetized_tcp.cpp @@ -40,9 +40,8 @@ PacketizedTCP::~PacketizedTCP() void PacketizedTCP::Stop(void) { - unsigned int i; TCPInterface::Stop(); - for (i=0; i < waitingPackets.Size(); i++) + for (size_t i=0; i < waitingPackets.size(); i++) DeallocatePacket(waitingPackets[i]); ClearAllConnections(); } @@ -110,27 +109,27 @@ void PacketizedTCP::PushNotificationsToQueues(void) sa = TCPInterface::HasNewIncomingConnection(); if (sa!=UNASSIGNED_SYSTEM_ADDRESS) { - _newIncomingConnections.Push(sa, _FILE_AND_LINE_ ); + _newIncomingConnections.push_back(sa); AddToConnectionList(sa); } sa = TCPInterface::HasFailedConnectionAttempt(); if (sa!=UNASSIGNED_SYSTEM_ADDRESS) { - _failedConnectionAttempts.Push(sa, _FILE_AND_LINE_ ); + _failedConnectionAttempts.push_back(sa); } sa = TCPInterface::HasLostConnection(); if (sa!=UNASSIGNED_SYSTEM_ADDRESS) { - _lostConnections.Push(sa, _FILE_AND_LINE_ ); + _lostConnections.push_back(sa); RemoveFromConnectionList(sa); } sa = TCPInterface::HasCompletedConnectionAttempt(); if (sa!=UNASSIGNED_SYSTEM_ADDRESS) { - _completedConnectionAttempts.Push(sa, _FILE_AND_LINE_ ); + _completedConnectionAttempts.push_back(sa); AddToConnectionList(sa); } } @@ -148,15 +147,12 @@ Packet* PacketizedTCP::Receive( void ) Packet *incomingPacket; incomingPacket = TCPInterface::ReceiveInt(); - unsigned int index; while (incomingPacket) { - if (connections.Has(incomingPacket->systemAddress)) - index = connections.GetIndexAtKey(incomingPacket->systemAddress); - else - index=(unsigned int) -1; - if ((unsigned int)index==(unsigned int)-1) + std::map::iterator connection = + connections.find(incomingPacket->systemAddress); + if (connection == connections.end()) { DeallocatePacket(incomingPacket); incomingPacket = TCPInterface::ReceiveInt(); @@ -168,9 +164,8 @@ Packet* PacketizedTCP::Receive( void ) { // Came from network SystemAddress systemAddressFromPacket; - if (index < connections.Size()) { - DataStructures::ByteQueue *bq = connections[index]; + DataStructures::ByteQueue *bq = connection->second; // Buffer data bq->WriteBytes((const char*) incomingPacket->data,incomingPacket->length, _FILE_AND_LINE_); systemAddressFromPacket=incomingPacket->systemAddress; @@ -201,7 +196,7 @@ Packet* PacketizedTCP::Receive( void ) } bq->ReadBytes((char*) outgoingPacket->data,dataLength,false); - waitingPackets.Push(outgoingPacket, _FILE_AND_LINE_ ); + waitingPackets.push_back(outgoingPacket); // Peek the header to see if a full message is waiting if (bq->ReadBytes((char*) &dataLength,sizeof(PTCPHeader),true)) @@ -250,7 +245,7 @@ Packet* PacketizedTCP::Receive( void ) bq->ReadBytes((char*) outgoingPacket->data+sizeof(MessageID)+sizeof(unsigned int)*3,oneChunkSize,true); bq->DecrementReadOffset(sizeof(PTCPHeader)); - waitingPackets.Push(outgoingPacket, _FILE_AND_LINE_ ); + waitingPackets.push_back(outgoingPacket); } } @@ -260,7 +255,7 @@ Packet* PacketizedTCP::Receive( void ) incomingPacket=0; } else - waitingPackets.Push(incomingPacket, _FILE_AND_LINE_ ); + waitingPackets.push_back(incomingPacket); incomingPacket = TCPInterface::ReceiveInt(); } @@ -271,9 +266,10 @@ Packet *PacketizedTCP::ReturnOutgoingPacket(void) { Packet *outgoingPacket=0; unsigned int i; - while (outgoingPacket==0 && waitingPackets.IsEmpty()==false) + while (outgoingPacket==0 && waitingPackets.empty()==false) { - outgoingPacket=waitingPackets.Pop(); + outgoingPacket=waitingPackets.front(); + waitingPackets.pop_front(); PluginReceiveResult pluginResult; for (i=0; i < messageHandlerList.size(); i++) { @@ -303,59 +299,83 @@ void PacketizedTCP::RemoveFromConnectionList(const SystemAddress &sa) { if (sa==UNASSIGNED_SYSTEM_ADDRESS) return; - if (connections.Has(sa)) + std::map::iterator it = connections.find(sa); + if (it != connections.end()) { - unsigned int index = connections.GetIndexAtKey(sa); - if (index!=(unsigned int)-1) - { - MafiaNet::OP_DELETE(connections[index],_FILE_AND_LINE_); - connections.RemoveAtIndex(index); - } + MafiaNet::OP_DELETE(it->second,_FILE_AND_LINE_); + connections.erase(it); } } void PacketizedTCP::AddToConnectionList(const SystemAddress &sa) { if (sa==UNASSIGNED_SYSTEM_ADDRESS) return; - connections.SetNew(sa, MafiaNet::OP_NEW(_FILE_AND_LINE_)); + // DataStructures::Map::SetNew asserted on a duplicate key under _DEBUG and then + // replaced the value, which leaked the old buffer. Keep the assert, and if it + // does happen, free what was there instead of dropping it on the floor. + std::map::iterator existing = connections.find(sa); + RakAssert(existing == connections.end()); + if (existing != connections.end()) + { + MafiaNet::OP_DELETE(existing->second,_FILE_AND_LINE_); + existing->second = MafiaNet::OP_NEW(_FILE_AND_LINE_); + return; + } + connections[sa] = MafiaNet::OP_NEW(_FILE_AND_LINE_); } void PacketizedTCP::ClearAllConnections(void) { - unsigned int i; - for (i=0; i < connections.Size(); i++) - MafiaNet::OP_DELETE(connections[i],_FILE_AND_LINE_); - connections.Clear(); + for (std::map::iterator it = connections.begin(); + it != connections.end(); ++it) + MafiaNet::OP_DELETE(it->second,_FILE_AND_LINE_); + connections.clear(); } SystemAddress PacketizedTCP::HasCompletedConnectionAttempt(void) { PushNotificationsToQueues(); - if (_completedConnectionAttempts.IsEmpty()==false) - return _completedConnectionAttempts.Pop(); + if (_completedConnectionAttempts.empty()==false) + { + const SystemAddress sa = _completedConnectionAttempts.front(); + _completedConnectionAttempts.pop_front(); + return sa; + } return UNASSIGNED_SYSTEM_ADDRESS; } SystemAddress PacketizedTCP::HasFailedConnectionAttempt(void) { PushNotificationsToQueues(); - if (_failedConnectionAttempts.IsEmpty()==false) - return _failedConnectionAttempts.Pop(); + if (_failedConnectionAttempts.empty()==false) + { + const SystemAddress sa = _failedConnectionAttempts.front(); + _failedConnectionAttempts.pop_front(); + return sa; + } return UNASSIGNED_SYSTEM_ADDRESS; } SystemAddress PacketizedTCP::HasNewIncomingConnection(void) { PushNotificationsToQueues(); - if (_newIncomingConnections.IsEmpty()==false) - return _newIncomingConnections.Pop(); + if (_newIncomingConnections.empty()==false) + { + const SystemAddress sa = _newIncomingConnections.front(); + _newIncomingConnections.pop_front(); + return sa; + } return UNASSIGNED_SYSTEM_ADDRESS; } SystemAddress PacketizedTCP::HasLostConnection(void) { PushNotificationsToQueues(); - if (_lostConnections.IsEmpty()==false) - return _lostConnections.Pop(); + if (_lostConnections.empty()==false) + { + const SystemAddress sa = _lostConnections.front(); + _lostConnections.pop_front(); + return sa; + } return UNASSIGNED_SYSTEM_ADDRESS; } diff --git a/Tests/Integration/PacketizedTCPFramingTests.cpp b/Tests/Integration/PacketizedTCPFramingTests.cpp new file mode 100644 index 000000000..93d6867ac --- /dev/null +++ b/Tests/Integration/PacketizedTCPFramingTests.cpp @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + * + * Message framing and connection-notification tests for PacketizedTCP. + * + * PacketizedTCP had no tests. It adds a length prefix on top of TCPInterface's + * raw byte stream, which means a received TCP segment may hold part of a + * message, exactly one, or several -- and the three protected containers being + * migrated to std containers (issue #60) are what hold that state: + * + * connections SystemAddress -> ByteQueue, the per-peer reassembly buffer + * waitingPackets whole messages already reassembled, waiting for Receive() + * the four SystemAddress queues feeding HasNewIncomingConnection and friends + * + * None of it is reachable without a real connection: Send() on an address with + * no entry in `connections` returns without sending, and the notification queues + * are filled from TCPInterface's own lock-free queues. So these run over + * loopback TCP. + */ + +#include + +#include "mafianet/packetized_tcp.h" +#include "mafianet/message_identifiers.h" + +#if _RAKNET_SUPPORT_PacketizedTCP == 1 + +#include "mafianet/get_time.h" +#include "mafianet/sleep.h" +#include "TestHelpers.h" + +#include +#include + +using namespace MafiaNet; + +namespace +{ + class PacketizedTCPFraming : public ::testing::Test + { + protected: + void SetUp() override + { + server = PacketizedTCP::GetInstance(); + client = PacketizedTCP::GetInstance(); + ASSERT_NE(server, nullptr); + ASSERT_NE(client, nullptr); + + port = TestHelpers::StartTcpOnAFreePort(*server, 4); + ASSERT_NE(port, 0) << "could not start a listener on any free TCP port"; + + ASSERT_TRUE(client->Start(0, 0)); + } + + void TearDown() override + { + // Must survive a failed ASSERT_, so no cleanup is left to the test body. + if (client) + { + DrainAndFree(client); + client->Stop(); + PacketizedTCP::DestroyInstance(client); + client = 0; + } + if (server) + { + DrainAndFree(server); + server->Stop(); + PacketizedTCP::DestroyInstance(server); + server = 0; + } + } + + static void DrainAndFree(PacketizedTCP *t) + { + Packet *p; + while ((p = t->Receive()) != 0) + t->DeallocatePacket(p); + } + + // Connects client to server and waits until each side has observed it. + // Returns the address the client is known by on the server. + SystemAddress ConnectAndWait(unsigned int timeoutMS = 10000) + { + // A non-blocking Connect always returns UNASSIGNED_SYSTEM_ADDRESS -- it hands + // the work to a thread and reports the outcome through + // HasCompletedConnectionAttempt / HasFailedConnectionAttempt. Its return + // value says nothing, so it is not checked. + client->Connect("127.0.0.1", port, false); + + SystemAddress completed = UNASSIGNED_SYSTEM_ADDRESS; + SystemAddress incoming = UNASSIGNED_SYSTEM_ADDRESS; + const TimeMS start = GetTimeMS(); + while ((completed == UNASSIGNED_SYSTEM_ADDRESS || incoming == UNASSIGNED_SYSTEM_ADDRESS) + && GetTimeMS() - start < timeoutMS) + { + if (completed == UNASSIGNED_SYSTEM_ADDRESS) + completed = client->HasCompletedConnectionAttempt(); + if (incoming == UNASSIGNED_SYSTEM_ADDRESS) + incoming = server->HasNewIncomingConnection(); + if (completed == UNASSIGNED_SYSTEM_ADDRESS || incoming == UNASSIGNED_SYSTEM_ADDRESS) + RakSleep(10); + } + + if (completed == UNASSIGNED_SYSTEM_ADDRESS || incoming == UNASSIGNED_SYSTEM_ADDRESS) + return UNASSIGNED_SYSTEM_ADDRESS; + + return incoming; + } + + // Collects exactly `count` messages, as strings, or fewer on timeout. + // + // ID_DOWNLOAD_PROGRESS is skipped and counted separately. PacketizedTCP + // emits one of those every 64KB while a message is still incomplete, so a + // large payload -- or two interleaved ones -- produces them alongside the + // real messages. They are part of the protocol, not noise: see + // LargeTransfersEmitWellFormedProgressPackets below. + std::vector ReceiveMessages(PacketizedTCP *t, size_t count, + unsigned int timeoutMS = 10000) + { + std::vector out; + const TimeMS start = GetTimeMS(); + while (out.size() < count && GetTimeMS() - start < timeoutMS) + { + Packet *p = t->Receive(); + if (p) + { + if (p->length > 0 && p->data[0] == ID_DOWNLOAD_PROGRESS) + progressPackets++; + else + out.push_back(std::string((const char *) p->data, p->length)); + t->DeallocatePacket(p); + } + else + RakSleep(10); + } + return out; + } + + int progressPackets = 0; + + PacketizedTCP *server; + PacketizedTCP *client; + unsigned short port; + }; +} + +TEST_F(PacketizedTCPFraming, ConnectionIsObservedByBothSides) +{ + const SystemAddress clientOnServer = ConnectAndWait(); + ASSERT_NE(clientOnServer, UNASSIGNED_SYSTEM_ADDRESS) << "connection was never observed"; + + // Each notification is handed out once. + ASSERT_EQ(server->HasNewIncomingConnection(), UNASSIGNED_SYSTEM_ADDRESS); + ASSERT_EQ(client->HasCompletedConnectionAttempt(), UNASSIGNED_SYSTEM_ADDRESS); + + // And the wrong queue stays empty. + ASSERT_EQ(client->HasFailedConnectionAttempt(), UNASSIGNED_SYSTEM_ADDRESS); + ASSERT_EQ(server->HasLostConnection(), UNASSIGNED_SYSTEM_ADDRESS); +} + +// One message per Receive, with its exact length -- the length prefix must not +// leak into the payload and the payload must not be truncated. +TEST_F(PacketizedTCPFraming, ASingleMessageArrivesWithItsExactLength) +{ + ASSERT_NE(ConnectAndWait(), UNASSIGNED_SYSTEM_ADDRESS); + + const std::string msg = "hello packetized tcp"; + client->Send(msg.c_str(), (unsigned) msg.size(), UNASSIGNED_SYSTEM_ADDRESS, true); + + const std::vector got = ReceiveMessages(server, 1); + + ASSERT_EQ(got.size(), 1u) << "the message never arrived"; + ASSERT_EQ(got[0], msg); +} + +// Several small messages sent back to back almost certainly arrive in one TCP +// segment, so the receive path has to split them out of the ByteQueue and queue +// them in waitingPackets. This is the case that pins waitingPackets' FIFO order: +// draining it as a stack would reverse the batch. +TEST_F(PacketizedTCPFraming, ABatchOfMessagesInOneSegmentKeepsItsOrder) +{ + ASSERT_NE(ConnectAndWait(), UNASSIGNED_SYSTEM_ADDRESS); + + std::vector sent; + for (int i = 0; i < 12; i++) + { + char buf[32]; + snprintf(buf, sizeof(buf), "msg-%02d", i); + sent.push_back(buf); + } + + for (size_t i = 0; i < sent.size(); i++) + client->Send(sent[i].c_str(), (unsigned) sent[i].size(), UNASSIGNED_SYSTEM_ADDRESS, true); + + const std::vector got = ReceiveMessages(server, sent.size()); + + ASSERT_EQ(got.size(), sent.size()) << "only " << got.size() << " of " << sent.size() << " arrived"; + for (size_t i = 0; i < sent.size(); i++) + ASSERT_EQ(got[i], sent[i]) << "message " << i << " is out of order or corrupt"; +} + +// A payload far larger than one segment has to be reassembled from several +// TCPInterface packets inside the per-connection ByteQueue before any of it is +// handed to Receive. +TEST_F(PacketizedTCPFraming, AMessageLargerThanOneSegmentIsReassembled) +{ + ASSERT_NE(ConnectAndWait(), UNASSIGNED_SYSTEM_ADDRESS); + + std::string big; + big.reserve(200000); + for (int i = 0; i < 200000; i++) + big.push_back((char) ('a' + (i % 26))); + + client->Send(big.c_str(), (unsigned) big.size(), UNASSIGNED_SYSTEM_ADDRESS, true); + + const std::vector got = ReceiveMessages(server, 1, 20000); + + ASSERT_EQ(got.size(), 1u) << "the large message never completed"; + ASSERT_EQ(got[0].size(), big.size()) << "wrong reassembled length"; + ASSERT_EQ(got[0], big) << "the reassembled payload does not match"; +} + +// A large message followed by a small one: the small one must not be lost in the +// tail of the ByteQueue, and must not arrive first. +TEST_F(PacketizedTCPFraming, ASmallMessageAfterALargeOneStillArrivesInOrder) +{ + ASSERT_NE(ConnectAndWait(), UNASSIGNED_SYSTEM_ADDRESS); + + std::string big(120000, 'x'); + const std::string small = "tail"; + + client->Send(big.c_str(), (unsigned) big.size(), UNASSIGNED_SYSTEM_ADDRESS, true); + client->Send(small.c_str(), (unsigned) small.size(), UNASSIGNED_SYSTEM_ADDRESS, true); + + const std::vector got = ReceiveMessages(server, 2, 20000); + + ASSERT_EQ(got.size(), 2u); + ASSERT_EQ(got[0].size(), big.size()); + ASSERT_EQ(got[0], big); + ASSERT_EQ(got[1], small); +} + +// Both directions at once, so each side keeps its own reassembly buffer. +TEST_F(PacketizedTCPFraming, BothDirectionsAreFramedIndependently) +{ + const SystemAddress clientOnServer = ConnectAndWait(); + ASSERT_NE(clientOnServer, UNASSIGNED_SYSTEM_ADDRESS); + + const std::string toServer = "client says hello"; + const std::string toClient = "server says hi back"; + + client->Send(toServer.c_str(), (unsigned) toServer.size(), UNASSIGNED_SYSTEM_ADDRESS, true); + server->Send(toClient.c_str(), (unsigned) toClient.size(), clientOnServer, false); + + const std::vector atServer = ReceiveMessages(server, 1); + const std::vector atClient = ReceiveMessages(client, 1); + + ASSERT_EQ(atServer.size(), 1u); + ASSERT_EQ(atServer[0], toServer); + ASSERT_EQ(atClient.size(), 1u); + ASSERT_EQ(atClient[0], toClient); +} + +// CloseConnection removes the entry from `connections` on the closing side and +// surfaces a lost-connection notification on the other. +TEST_F(PacketizedTCPFraming, ClosingAConnectionIsReportedToThePeer) +{ + const SystemAddress clientOnServer = ConnectAndWait(); + ASSERT_NE(clientOnServer, UNASSIGNED_SYSTEM_ADDRESS); + + server->CloseConnection(clientOnServer); + + SystemAddress lost = UNASSIGNED_SYSTEM_ADDRESS; + const TimeMS start = GetTimeMS(); + while (lost == UNASSIGNED_SYSTEM_ADDRESS && GetTimeMS() - start < 10000) + { + lost = client->HasLostConnection(); + if (lost == UNASSIGNED_SYSTEM_ADDRESS) + RakSleep(10); + } + + ASSERT_NE(lost, UNASSIGNED_SYSTEM_ADDRESS) << "the client never saw the connection drop"; + ASSERT_EQ(client->HasLostConnection(), UNASSIGNED_SYSTEM_ADDRESS) << "reported more than once"; +} + +// Connecting to a port nothing is listening on fills _failedConnectionAttempts, +// which no other test here touches. +TEST_F(PacketizedTCPFraming, AFailedConnectionAttemptIsReported) +{ + const unsigned short deadPort = TestHelpers::FindFreeTcpPort(); + ASSERT_NE(deadPort, 0); + ASSERT_NE(deadPort, port); + + client->Connect("127.0.0.1", deadPort, false); + + SystemAddress failed = UNASSIGNED_SYSTEM_ADDRESS; + const TimeMS start = GetTimeMS(); + while (failed == UNASSIGNED_SYSTEM_ADDRESS && GetTimeMS() - start < 15000) + { + failed = client->HasFailedConnectionAttempt(); + if (failed == UNASSIGNED_SYSTEM_ADDRESS) + RakSleep(10); + } + + ASSERT_NE(failed, UNASSIGNED_SYSTEM_ADDRESS) << "the failed attempt was never reported"; + ASSERT_EQ(client->HasCompletedConnectionAttempt(), UNASSIGNED_SYSTEM_ADDRESS) + << "a failed attempt must not also report as completed"; +} + +// Two clients at once: each needs its own entry in `connections`, keyed by +// address. A single shared buffer would interleave their byte streams and the +// framing would fall apart. +TEST_F(PacketizedTCPFraming, TwoClientsAreFramedSeparately) +{ + ASSERT_NE(ConnectAndWait(), UNASSIGNED_SYSTEM_ADDRESS); + + PacketizedTCP *second = PacketizedTCP::GetInstance(); + ASSERT_NE(second, nullptr); + ASSERT_TRUE(second->Start(0, 0)); + + second->Connect("127.0.0.1", port, false); + + SystemAddress secondOnServer = UNASSIGNED_SYSTEM_ADDRESS; + const TimeMS start = GetTimeMS(); + while (secondOnServer == UNASSIGNED_SYSTEM_ADDRESS && GetTimeMS() - start < 10000) + { + secondOnServer = server->HasNewIncomingConnection(); + if (secondOnServer == UNASSIGNED_SYSTEM_ADDRESS) + RakSleep(10); + } + + if (secondOnServer == UNASSIGNED_SYSTEM_ADDRESS) + { + DrainAndFree(second); + second->Stop(); + PacketizedTCP::DestroyInstance(second); + FAIL() << "the second connection was never observed"; + } + + // Each payload is far bigger than one segment and filled with a distinct + // pattern, so the server's reads from the two sockets necessarily interleave. + // A single shared reassembly buffer would splice one client's bytes into the + // other's message; two small messages would not catch that, because each would + // already be complete when it was written and would still read back in order. + std::string fromFirst(180000, 'A'); + std::string fromSecond(180000, 'B'); + + client->Send(fromFirst.c_str(), (unsigned) fromFirst.size(), UNASSIGNED_SYSTEM_ADDRESS, true); + second->Send(fromSecond.c_str(), (unsigned) fromSecond.size(), UNASSIGNED_SYSTEM_ADDRESS, true); + + // Both messages must arrive intact; the order between two sockets is not ours + // to determine, so match as a set. + const std::vector got = ReceiveMessages(server, 2, 30000); + + DrainAndFree(second); + second->Stop(); + PacketizedTCP::DestroyInstance(second); + + ASSERT_EQ(got.size(), 2u); + const bool matched = (got[0] == fromFirst && got[1] == fromSecond) + || (got[0] == fromSecond && got[1] == fromFirst); + ASSERT_TRUE(matched) << "payloads were mixed between the two connections: got " + << got[0].size() << " and " << got[1].size() << " bytes"; +} + +#endif // _RAKNET_SUPPORT_PacketizedTCP From 8a8527df62eaa1f4a43a034b5fd8a3042bdfd55e Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:36:41 +0200 Subject: [PATCH 22/26] fix(packetizedtcp): free the waiting-message queue properly, and fix the progress packet's bitSize Three defects the new framing tests turned up. **Stop() freed the queued messages and left the freed pointers in the queue.** `for (i...) DeallocatePacket(waitingPackets[i]);` with no clear afterwards. A second Stop() -- or the one a caller makes before destroying the object -- freed every one of them again, and a Receive() after Stop() popped a dangling pointer, ran it past the plugin chain and handed it to the caller. **~PacketizedTCP() never freed the queue at all.** It called ClearAllConnections(), which frees the per-connection reassembly buffers, and left waitingPackets alone. Receive() reassembles everything available and returns one message, so any queued remainder leaked on destruction. Both now go through one ClearWaitingPackets(). **ID_DOWNLOAD_PROGRESS carried the wrong bitSize.** The progress packet is built with its own length (1 + 12 + 65536 = 65549 bytes), but took `bitSize = BYTES_TO_BITS(incomingPacket->length)` -- the bit count of whatever TCP segment happened to trigger it. length and bitSize disagreed on every progress packet. Now taken from its own length. Tests: four cases added to Tests/Integration/PacketizedTCPFramingTests.cpp. LargeTransfersEmitWellFormedProgressPackets sends 4MB and asserts progress packets are emitted and that each one's bitSize matches its own length. The size is what makes it deterministic: the progress branch only runs while a message is still incomplete after Receive() has drained what was available, so a payload that fits in the socket buffers can complete in one go and emit nothing. A first attempt used two 180KB senders and failed about one run in ten for exactly that reason. 4MB with a sleep-free poll: 20/20. ReceiveAfterStopReturnsNothing six messages sent, one Receive (which queues the other five), Stop, then Receive must return nothing. StoppingTwiceWithQueuedMessagesIsSafe the same, stopped twice. DestroyingTheInstanceWithQueuedMessagesIsClean destroys with five messages queued, no Stop and no drain -- the scenario for the destructor leak. Mutation-verified, each against a build I checked: bitSize taken from the incoming packet again -> LargeTransfersEmitWellFormed... the clear() removed from ClearWaitingPackets -> ReceiveAfterStopReturnsNothing and StoppingTwiceWith... both abort on a double free (SIGABRT), and LargeTransfers... fails its assertions -> ABatchOfMessages... survives, so the existing cases would not have caught it The destructor leak is verified by LeakSanitizer, not by an assertion: an ASAN+LSAN Debug build on Linux runs the whole suite, including DestroyingTheInstanceWithQueuedMessagesIsClean, with no leak reported. Verified: macOS full suite 473/473; Debug build clean + Debug unit 382/382; Linux ASAN+LSAN clean over all three new suites (24 tests). Claude-Session: https://claude.ai/code/session_011kRj5nodTJrxyoHY7y6R6c --- Source/include/mafianet/packetized_tcp.h | 2 + Source/src/packetized_tcp.cpp | 20 ++- .../Integration/PacketizedTCPFramingTests.cpp | 143 ++++++++++++++++++ 3 files changed, 162 insertions(+), 3 deletions(-) diff --git a/Source/include/mafianet/packetized_tcp.h b/Source/include/mafianet/packetized_tcp.h index 220a36dbc..062b3cc7b 100644 --- a/Source/include/mafianet/packetized_tcp.h +++ b/Source/include/mafianet/packetized_tcp.h @@ -73,6 +73,8 @@ class RAK_DLL_EXPORT PacketizedTCP : public TCPInterface protected: void ClearAllConnections(void); + /// Frees every reassembled message still waiting for Receive, and empties the queue. + void ClearWaitingPackets(void); void RemoveFromConnectionList(const SystemAddress &sa); void AddToConnectionList(const SystemAddress &sa); void PushNotificationsToQueues(void); diff --git a/Source/src/packetized_tcp.cpp b/Source/src/packetized_tcp.cpp index 3312d96aa..b0fc128f9 100644 --- a/Source/src/packetized_tcp.cpp +++ b/Source/src/packetized_tcp.cpp @@ -35,14 +35,19 @@ PacketizedTCP::PacketizedTCP() } PacketizedTCP::~PacketizedTCP() { + // Anything still in waitingPackets was leaked: only Stop() freed it, and + // nothing calls Stop() on the way out. + ClearWaitingPackets(); ClearAllConnections(); } void PacketizedTCP::Stop(void) { TCPInterface::Stop(); - for (size_t i=0; i < waitingPackets.size(); i++) - DeallocatePacket(waitingPackets[i]); + // The pointers used to be left in the queue after being freed, so a second + // Stop() double-freed them and a Receive() after Stop() handed one back to the + // caller. + ClearWaitingPackets(); ClearAllConnections(); } @@ -222,7 +227,10 @@ Packet* PacketizedTCP::Receive( void ) sizeof(unsigned int)*2 + sizeof(unsigned int) + 65536; - outgoingPacket->bitSize=BYTES_TO_BITS(incomingPacket->length); + // This took its bit size from the *incoming* packet, which has + // nothing to do with the progress packet's own length, so + // bitSize and length disagreed on every ID_DOWNLOAD_PROGRESS. + outgoingPacket->bitSize=BYTES_TO_BITS(outgoingPacket->length); outgoingPacket->guid=UNASSIGNED_RAKNET_GUID; outgoingPacket->systemAddress=incomingPacket->systemAddress; outgoingPacket->deleteData=false; @@ -323,6 +331,12 @@ void PacketizedTCP::AddToConnectionList(const SystemAddress &sa) } connections[sa] = MafiaNet::OP_NEW(_FILE_AND_LINE_); } +void PacketizedTCP::ClearWaitingPackets(void) +{ + for (size_t i=0; i < waitingPackets.size(); i++) + DeallocatePacket(waitingPackets[i]); + waitingPackets.clear(); +} void PacketizedTCP::ClearAllConnections(void) { for (std::map::iterator it = connections.begin(); diff --git a/Tests/Integration/PacketizedTCPFramingTests.cpp b/Tests/Integration/PacketizedTCPFramingTests.cpp index 93d6867ac..8ba67d4ba 100644 --- a/Tests/Integration/PacketizedTCPFramingTests.cpp +++ b/Tests/Integration/PacketizedTCPFramingTests.cpp @@ -32,6 +32,8 @@ #include "mafianet/sleep.h" #include "TestHelpers.h" +#include + #include #include @@ -367,4 +369,145 @@ TEST_F(PacketizedTCPFraming, TwoClientsAreFramedSeparately) << got[0].size() << " and " << got[1].size() << " bytes"; } +// The progress notifications the other tests skip over: they must actually be +// emitted for a payload far larger than the socket buffers, and each one must be +// internally consistent -- its bitSize has to describe its own length. +// +// The size is what makes this deterministic. The progress branch only runs when a +// message is still incomplete after Receive() has drained everything available, +// so a payload that fits in the socket buffers may complete in one go and emit +// nothing; 4MB cannot, and the poll below runs Receive() with no sleep so it +// keeps looking while the transfer is still in flight. +TEST_F(PacketizedTCPFraming, LargeTransfersEmitWellFormedProgressPackets) +{ + ASSERT_NE(ConnectAndWait(), UNASSIGNED_SYSTEM_ADDRESS); + + const size_t payloadSize = 4u * 1024u * 1024u; + std::string payload(payloadSize, 'Z'); + client->Send(payload.c_str(), (unsigned) payload.size(), UNASSIGNED_SYSTEM_ADDRESS, true); + + int progressSeen = 0; + bool progressWellFormed = true; + bool completed = false; + + const TimeMS start = GetTimeMS(); + while (completed == false && GetTimeMS() - start < 30000) + { + Packet *p = server->Receive(); + if (p == 0) + continue; // no sleep: keep polling while the transfer is in flight + + if (p->length > 0 && p->data[0] == ID_DOWNLOAD_PROGRESS) + { + progressSeen++; + if (p->bitSize != BYTES_TO_BITS(p->length)) + progressWellFormed = false; + } + else if (p->length == payloadSize) + completed = true; + + server->DeallocatePacket(p); + } + + ASSERT_TRUE(completed) << "the 4MB message never completed"; + ASSERT_GT(progressSeen, 0) << "no ID_DOWNLOAD_PROGRESS was emitted for a 4MB transfer"; + ASSERT_TRUE(progressWellFormed) + << "an ID_DOWNLOAD_PROGRESS packet's bitSize did not match its own length"; +} + +// Receive() reassembles everything available and returns one message, leaving the +// rest in waitingPackets. Stop() used to free those and leave the freed pointers +// in the queue, so a Receive() after Stop() handed one straight back. +TEST_F(PacketizedTCPFraming, ReceiveAfterStopReturnsNothing) +{ + ASSERT_NE(ConnectAndWait(), UNASSIGNED_SYSTEM_ADDRESS); + + for (int i = 0; i < 6; i++) + { + char buf[16]; + snprintf(buf, sizeof(buf), "m%d", i); + client->Send(buf, (unsigned) strlen(buf), UNASSIGNED_SYSTEM_ADDRESS, true); + } + + // One Receive gets the first message and queues the other five. + Packet *first = 0; + const TimeMS start = GetTimeMS(); + while (first == 0 && GetTimeMS() - start < 10000) + { + first = server->Receive(); + if (first == 0) + RakSleep(10); + } + ASSERT_NE(first, nullptr); + server->DeallocatePacket(first); + + server->Stop(); + + ASSERT_EQ(server->Receive(), nullptr) << "Receive() returned a packet Stop() had already freed"; +} + +// Destroying the instance with messages still queued. ~PacketizedTCP freed the +// per-connection buffers but not the reassembled messages, so everything still +// waiting for Receive leaked. The leak itself is only visible under +// LeakSanitizer (Linux; macOS ASAN has no leak detection), so what this case +// asserts on every platform is that the destroy path runs clean -- it exists to +// give LSAN the scenario to look at in the Linux gate. +TEST_F(PacketizedTCPFraming, DestroyingTheInstanceWithQueuedMessagesIsClean) +{ + ASSERT_NE(ConnectAndWait(), UNASSIGNED_SYSTEM_ADDRESS); + + for (int i = 0; i < 6; i++) + { + char buf[16]; + snprintf(buf, sizeof(buf), "m%d", i); + client->Send(buf, (unsigned) strlen(buf), UNASSIGNED_SYSTEM_ADDRESS, true); + } + + Packet *first = 0; + const TimeMS start = GetTimeMS(); + while (first == 0 && GetTimeMS() - start < 10000) + { + first = server->Receive(); + if (first == 0) + RakSleep(10); + } + ASSERT_NE(first, nullptr); + server->DeallocatePacket(first); + + // No Stop(), no drain: straight to the destructor with five messages queued. + PacketizedTCP::DestroyInstance(server); + server = 0; + + SUCCEED(); +} + +// And stopping twice must not free them twice. +TEST_F(PacketizedTCPFraming, StoppingTwiceWithQueuedMessagesIsSafe) +{ + ASSERT_NE(ConnectAndWait(), UNASSIGNED_SYSTEM_ADDRESS); + + for (int i = 0; i < 6; i++) + { + char buf[16]; + snprintf(buf, sizeof(buf), "m%d", i); + client->Send(buf, (unsigned) strlen(buf), UNASSIGNED_SYSTEM_ADDRESS, true); + } + + Packet *first = 0; + const TimeMS start = GetTimeMS(); + while (first == 0 && GetTimeMS() - start < 10000) + { + first = server->Receive(); + if (first == 0) + RakSleep(10); + } + ASSERT_NE(first, nullptr); + server->DeallocatePacket(first); + + server->Stop(); + server->Stop(); + + ASSERT_EQ(server->Receive(), nullptr); +} + #endif // _RAKNET_SUPPORT_PacketizedTCP From e424d9109bcaabd7e63308f12eb8ce1fe71f0f6f Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:48:00 +0200 Subject: [PATCH 23/26] fix(tcpinterface): two use-after-frees on the connect path, and the addrinfo leak Found by an ASAN+LSAN build in Docker, which reported `heap-use-after-free ... in TCPInterface::SocketConnect`. All three predate this branch -- they come in with the SLikeNet import -- and all three are on the default connect path. **ConnectionAttemptLoop used the thread argument after deleting it.** MafiaNet::OP_DELETE(s, _FILE_AND_LINE_); ... tcpInterface->SocketConnect(str1, ..., socketFamily, s->bindAddress); systemAddress, tcpInterface and socketFamily are copied out of `s` first; bindAddress is not, and is read after the delete. This is the read ASAN caught, and it happens on every non-blocking Connect -- which is the default. bindAddress is now copied into a local before the delete. **Connect()'s thread-creation error path does the same thing.** MafiaNet::OP_DELETE(s, _FILE_AND_LINE_); failedConnectionAttempts.push_back(s->systemAddress); Only reached when creating the connect thread fails, so it is not routinely hit, but it is the same mistake. The address is now read out first. **SocketConnect leaked its getaddrinfo result on every call.** The walk looking for the first IPv4 address advanced `curAddress`, which was the only pointer to the head of the list, and no path called freeaddrinfo. The head is now kept in a separate variable and freed on every exit, including the three early returns, and the `err != 0` check moved ahead of the dereference it used to sit behind. Verified: ASAN+LSAN Debug build on Linux, 24 tests across PacketizedTCPFraming, TCPInterfaceLifecycle and TCPInterfacePlugins, exit 0 with no AddressSanitizer error and no leak report -- against the use-after-free it reported before the fix. macOS full suite 473/473. A note on how this was nearly missed: CLAUDE.md's Docker recipe does `cp -r /src /work` before configuring. The tree now carries ~780MB of build output in build/ and build-debug/, and that copy silently truncated tcp_interface.cpp inside the container -- gcc reported "unterminated #if" for a file that compiles fine. Building straight from the read-only mount with `cmake -S /src -B /b` avoids the copy entirely and is what produced the result above. Claude-Session: https://claude.ai/code/session_011kRj5nodTJrxyoHY7y6R6c --- Source/src/tcp_interface.cpp | 39 +++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/Source/src/tcp_interface.cpp b/Source/src/tcp_interface.cpp index f8c94932c..afa1544a8 100644 --- a/Source/src/tcp_interface.cpp +++ b/Source/src/tcp_interface.cpp @@ -423,8 +423,11 @@ SystemAddress TCPInterface::Connect(const char* host, unsigned short remotePort, if (errorCode!=0) { + // Read the address out before freeing s: this used to report + // s->systemAddress after the delete. + const SystemAddress failedAddress = s->systemAddress; MafiaNet::OP_DELETE(s, _FILE_AND_LINE_); - failedConnectionAttempts.push_back(s->systemAddress); + failedConnectionAttempts.push_back(failedAddress); } return UNASSIGNED_SYSTEM_ADDRESS; } @@ -819,11 +822,21 @@ __TCPSOCKET__ TCPInterface::SocketConnect(const char* host, unsigned short remot #if RAKNET_SUPPORT_IPV6!=1 sockaddr_in serverAddress; - struct addrinfo *curAddress = nullptr; - int err = getaddrinfo(host, nullptr, nullptr, &curAddress); + // addressList owns the list getaddrinfo allocated and is what has to be freed. + // curAddress walks it; the walk used to overwrite the only pointer to the head, + // and nothing freed it on any path, so every connect attempt leaked the list. + struct addrinfo *addressList = nullptr; + const int err = getaddrinfo(host, nullptr, nullptr, &addressList); + if (err != 0) + { + if (addressList != nullptr) + freeaddrinfo(addressList); + return 0; + } // #med This should also work with IPv6 addresses in principle. However the old code in RakNet only worked with IPv4. Hence this behavior change won't be in MaxNet 0.1. // get the (first) IPv4 address + struct addrinfo *curAddress = addressList; while(curAddress != nullptr) { if (curAddress->ai_family == AF_INET) { break; // found an IPv4 address @@ -832,15 +845,17 @@ __TCPSOCKET__ TCPInterface::SocketConnect(const char* host, unsigned short remot } if (curAddress == nullptr) + { + freeaddrinfo(addressList); return 0; - - if (err != 0) - return 0; - + } __TCPSOCKET__ sockfd = socket__(AF_INET, SOCK_STREAM, 0); - if (sockfd < 0) + if (sockfd < 0) + { + freeaddrinfo(addressList); return 0; + } memset(&serverAddress, 0, sizeof(serverAddress)); serverAddress.sin_family = AF_INET; @@ -881,6 +896,8 @@ __TCPSOCKET__ TCPInterface::SocketConnect(const char* host, unsigned short remot // This is blocking connectResult = connect__( sockfd, ( struct sockaddr * ) &serverAddress, sizeof( struct sockaddr ) ); + freeaddrinfo(addressList); + #else (void)bindAddress; @@ -935,11 +952,15 @@ RAK_THREAD_DECLARATION(MafiaNet::ConnectionAttemptLoop) TCPInterface *tcpInterface = s->tcpInterface; int newRemoteClientIndex=systemAddress.systemIndex; unsigned short socketFamily = s->socketFamily; + // bindAddress has to be copied out too. It used to be read as s->bindAddress + // *after* this delete, on every non-blocking Connect -- the default. + char bindAddress[sizeof(s->bindAddress)]; + memcpy(bindAddress, s->bindAddress, sizeof(bindAddress)); MafiaNet::OP_DELETE(s, _FILE_AND_LINE_); char str1[64]; systemAddress.ToString(false, str1, static_cast(64)); - __TCPSOCKET__ sockfd = tcpInterface->SocketConnect(str1, systemAddress.GetPort(), socketFamily, s->bindAddress); + __TCPSOCKET__ sockfd = tcpInterface->SocketConnect(str1, systemAddress.GetPort(), socketFamily, bindAddress); if (sockfd==0) { tcpInterface->remoteClients[newRemoteClientIndex].isActiveMutex.Lock(); From fc50203026bfc4d6b2eb573c1e2df37840da36b7 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:48:19 +0200 Subject: [PATCH 24/26] docs(claude): build from the read-only mount, and add the sanitizer recipe The Docker verification recipe copied the source tree into the container (`cp -r /src /work`) before configuring. A local build/ directory is a few hundred MB -- this tree carries ~780MB across build/ and build-debug/ -- and once the container's writable layer fills, the copy truncates files silently. The symptom is gcc reporting "unterminated #if" in a file that compiles fine everywhere else, which is easy to read as a real portability problem and waste time on. CMake never writes into the source tree, so `-S /src` against the read-only mount works and there is nothing to copy for. Recipe updated, with the trap written down. Also added the ASAN+LSAN variant, since that is what turned up the use-after-free in TCPInterface::SocketConnect that nothing else was going to find, and LeakSanitizer only exists on Linux so it belongs in this section rather than in a local workflow. Claude-Session: https://claude.ai/code/session_011kRj5nodTJrxyoHY7y6R6c --- CLAUDE.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b72aa8abf..dc17f8795 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,14 +101,36 @@ Release is what ships and exercises the backstops the asserts hide): ```bash docker run --rm -v "$PWD":/src:ro ubuntu:24.04 bash -c ' apt-get update -qq && apt-get install -y -qq build-essential cmake libssl-dev git ca-certificates - cp -r /src /work && cd /work for cfg in Debug Release; do - cmake -B /b-$cfg -S . -DCMAKE_BUILD_TYPE=$cfg -DMAFIANET_BUILD_TESTS=ON + cmake -B /b-$cfg -S /src -DCMAKE_BUILD_TYPE=$cfg -DMAFIANET_BUILD_TESTS=ON cmake --build /b-$cfg --parallel "$(nproc)" ctest --test-dir /b-$cfg --output-on-failure --timeout 600 || exit 1 done' ``` +Build straight from the read-only mount with `-S /src`; **do not** `cp -r /src /work` +first. A local `build/` directory is a few hundred MB, the copy pulls it in, and once +the container's writable layer fills up it truncates files *silently* -- the symptom +is gcc reporting "unterminated #if" in a source file that compiles fine everywhere +else. CMake never writes into the source tree, so there is nothing to copy for. + +To check for leaks and memory errors, add a sanitizer to the same invocation and run +the binaries directly (LeakSanitizer is Linux-only; macOS ASAN has no leak +detection): + +```bash +docker run --rm -v "$PWD":/src:ro ubuntu:24.04 bash -c ' + apt-get update -qq && apt-get install -y -qq build-essential cmake libssl-dev git ca-certificates + cmake -B /b -S /src -DCMAKE_BUILD_TYPE=Debug -DMAFIANET_BUILD_TESTS=ON \ + -DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g" \ + -DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g" \ + -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" \ + -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address" + cmake --build /b --parallel "$(nproc)" + ASAN_OPTIONS=detect_leaks=1 /b/Tests/UnitTests + ASAN_OPTIONS=detect_leaks=1 /b/Tests/IntegrationTests --gtest_filter=".*"' +``` + `Tests/Integration/MmsgBatchLiveTests.cpp` is the suite that actually fills batches (the rest of the suite sends ~1 datagram per tick and never crosses `MMSG_BATCH_MAX`). Keep its message counts above `MMSG_BATCH_MAX` or that coverage silently disappears. From f1aeac780e143983949deb2b623c5d7c5be98631 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:26:52 +0200 Subject: [PATCH 25/26] fix(signaledevent): do not touch the condition variable before InitEvent or after CloseEvent On POSIX, SignaledEvent's condition variable and mutex are created by InitEvent, not by the constructor, but SetEvent called pthread_cond_broadcast unconditionally and WaitOnEvent called pthread_cond_timedwait unconditionally. Broadcasting on an uninitialised pthread_cond_t is undefined, and glibc does not fail -- it blocks forever in __condvar_quiesce_and_switch_g1. The caller hangs, with no diagnostic. This is reachable through a public API. ThreadPool::AddInput signals quitAndIncomingDataEvents and nothing requires StartThreads first, so Tests/Unit/ThreadPoolQueueTests.cpp -- which exercises the queues without starting a worker, deliberately -- hung on the first AddInput in the Linux Debug gate. gdb on the stuck process: #3 ___pthread_cond_broadcast (cond=0xffffc73cd200) #4 MafiaNet::SignaledEvent::SetEvent (...) signaled_event.cpp:112 #5 ThreadPool::AddInput (...) thread_pool.h:434 #6 ThreadPoolQueues_RemoveInputAtTheHeadAndAtTheTail_Test::TestBody It is platform-specific, which is why it was never hit: on Windows ::SetEvent(INVALID_HANDLE_VALUE) just fails, and on macOS the uninitialised broadcast happens to return. Fixed in SignaledEvent rather than in ThreadPool, because the hole belongs to the primitive and any caller can fall into it. An isInitialized flag, written by InitEvent and CloseEvent and read under the existing isSignaledMutex, now gates the two pthread calls; the Windows paths gain the equivalent INVALID_HANDLE_VALUE checks they were missing in SetEvent and WaitOnEvent. Two further undefined cases are closed with it: a second InitEvent without a CloseEvent no longer re-runs pthread_cond_init on a live condition variable, and CloseEvent without a preceding InitEvent no longer destroys one that was never created. SetEvent still records isSignaled when there is no condition variable, so a signal raised before InitEvent is delivered to the first WaitOnEvent after it rather than lost. New: Tests/Unit/SignaledEventLifecycleTests.cpp, 8 cases walking the lifecycle out of order -- signal and wait before init, init twice, close without init, init again after close, signal after close. SignaledEvent had no tests. Proved on Linux, in a Debug container, by making the broadcast unconditional again and rebuilding: with the guard SignaledEventLifecycle 8/8, ThreadPoolQueues 14/14 without the guard both time out on their first case (SetEventBeforeInitEventReturns, AddInputAppendsInFifoOrder) A hang is a poor failure signal, but it is the only one this bug produces; the timing assertions in the new tests exist to turn it into a failure, not to measure anything, and are loose by two orders of magnitude. Claude-Session: https://claude.ai/code/session_011kRj5nodTJrxyoHY7y6R6c --- Source/include/mafianet/signaled_event.h | 8 + Source/src/signaled_event.cpp | 46 +++++- Tests/Unit/SignaledEventLifecycleTests.cpp | 171 +++++++++++++++++++++ 3 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 Tests/Unit/SignaledEventLifecycleTests.cpp diff --git a/Source/include/mafianet/signaled_event.h b/Source/include/mafianet/signaled_event.h index bd0d8efa6..3e349bcdd 100644 --- a/Source/include/mafianet/signaled_event.h +++ b/Source/include/mafianet/signaled_event.h @@ -50,6 +50,14 @@ class RAK_DLL_EXPORT SignaledEvent void WaitOnEvent(int timeoutMs); protected: + /// Guards SetEvent and WaitOnEvent against running before InitEvent, or after + /// CloseEvent. On POSIX the condition variable and mutex below are only + /// initialised by InitEvent, and pthread_cond_broadcast on an uninitialised + /// pthread_cond_t blocks forever on glibc rather than failing, so an + /// unguarded SetEvent hung the caller. Read and written under isSignaledMutex + /// on POSIX; on Windows the INVALID_HANDLE_VALUE check serves the same purpose. + bool isInitialized; + #ifdef _WIN32 HANDLE eventList; diff --git a/Source/src/signaled_event.cpp b/Source/src/signaled_event.cpp index cac22065f..07e235294 100644 --- a/Source/src/signaled_event.cpp +++ b/Source/src/signaled_event.cpp @@ -30,6 +30,7 @@ using namespace MafiaNet; SignaledEvent::SignaledEvent() { + isInitialized=false; #ifdef _WIN32 eventList=INVALID_HANDLE_VALUE; @@ -47,8 +48,19 @@ void SignaledEvent::InitEvent(void) { #if defined(_WIN32) eventList=CreateEvent(0, false, false, 0); + isInitialized=true; #else + isSignaledMutex.Lock(); + if (isInitialized) + { + // pthread_cond_init on an already-initialised condition variable is + // undefined, so a second InitEvent without a CloseEvent is a no-op. + isSignaledMutex.Unlock(); + return; + } + isSignaledMutex.Unlock(); + #if !defined(ANDROID) pthread_condattr_init( &condAttr ); pthread_cond_init(&eventList, &condAttr); @@ -57,12 +69,17 @@ void SignaledEvent::InitEvent(void) #endif pthread_mutexattr_init( &mutexAttr ); pthread_mutex_init(&hMutex, &mutexAttr); + + isSignaledMutex.Lock(); + isInitialized=true; + isSignaledMutex.Unlock(); #endif } void SignaledEvent::CloseEvent(void) { #ifdef _WIN32 + isInitialized=false; if (eventList!=INVALID_HANDLE_VALUE) { CloseHandle(eventList); @@ -78,6 +95,16 @@ void SignaledEvent::CloseEvent(void) #else + isSignaledMutex.Lock(); + if (isInitialized==false) + { + // Nothing was ever created; destroying it would be undefined. + isSignaledMutex.Unlock(); + return; + } + isInitialized=false; + isSignaledMutex.Unlock(); + pthread_cond_destroy(&eventList); pthread_mutex_destroy(&hMutex); #if !defined(ANDROID) @@ -90,6 +117,8 @@ void SignaledEvent::CloseEvent(void) void SignaledEvent::SetEvent(void) { #ifdef _WIN32 + if (eventList==INVALID_HANDLE_VALUE) + return; ::SetEvent(eventList); @@ -106,10 +135,14 @@ void SignaledEvent::SetEvent(void) // We have to record manually that the event was signaled isSignaledMutex.Lock(); isSignaled=true; + const bool broadcast = isInitialized; isSignaledMutex.Unlock(); - // Unblock waiting threads - pthread_cond_broadcast(&eventList); + // Unblock waiting threads. Skipped when there is no condition variable yet: + // isSignaled above is still recorded, so a WaitOnEvent after a later + // InitEvent still sees the signal. + if (broadcast) + pthread_cond_broadcast(&eventList); #endif } @@ -121,6 +154,8 @@ void SignaledEvent::WaitOnEvent(int timeoutMs) // eventList, // false, // timeoutMs); + if (eventList==INVALID_HANDLE_VALUE) + return; WaitForSingleObjectEx(eventList,timeoutMs,FALSE); @@ -173,6 +208,13 @@ void SignaledEvent::WaitOnEvent(int timeoutMs) isSignaledMutex.Unlock(); return; } + if (isInitialized==false) + { + // No condition variable to wait on: pthread_cond_timedwait below would be + // undefined. Return as though the wait timed out. + isSignaledMutex.Unlock(); + return; + } isSignaledMutex.Unlock(); diff --git a/Tests/Unit/SignaledEventLifecycleTests.cpp b/Tests/Unit/SignaledEventLifecycleTests.cpp new file mode 100644 index 000000000..6b8e4f09d --- /dev/null +++ b/Tests/Unit/SignaledEventLifecycleTests.cpp @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + */ + +#include + +#include "mafianet/signaled_event.h" +#include "mafianet/get_time.h" + +using namespace MafiaNet; + +/* +SignaledEvent had no tests, and it had a hole: on POSIX the condition variable and +mutex are created by InitEvent, not by the constructor, while SetEvent called +pthread_cond_broadcast unconditionally. Broadcasting on an uninitialised +pthread_cond_t is undefined, and on glibc it does not fail -- it blocks forever in +__condvar_quiesce_and_switch_g1. Any caller that signalled before starting its +threads hung, with no diagnostic. + +That is reachable through a public API: ThreadPool::AddInput signals +quitAndIncomingDataEvents, and nothing requires StartThreads first. It is also +platform-specific, which is why it went unnoticed -- on Windows SetEvent on +INVALID_HANDLE_VALUE just fails, and on macOS the uninitialised broadcast happens +to return. + +The cases below cover the lifecycle out of order: signal and wait before init, +init twice, close without init, and init again after close. Each timing +assertion is deliberately loose -- they are there to turn a hang into a failure, +not to measure anything. +*/ + +namespace +{ + // A wait is "prompt" if it returns far sooner than the timeout it was given. + const unsigned int kGenerousMS = 2000; +} + +TEST(SignaledEventLifecycle, SetEventBeforeInitEventReturns) +{ + SignaledEvent e; + + const TimeMS start = GetTimeMS(); + e.SetEvent(); // hangs forever against the unguarded version + const TimeMS elapsed = GetTimeMS() - start; + + ASSERT_LT(elapsed, kGenerousMS) << "SetEvent blocked before InitEvent"; +} + +TEST(SignaledEventLifecycle, WaitOnEventBeforeInitEventReturnsPromptly) +{ + SignaledEvent e; + + const TimeMS start = GetTimeMS(); + e.WaitOnEvent(30000); + const TimeMS elapsed = GetTimeMS() - start; + + ASSERT_LT(elapsed, kGenerousMS) << "WaitOnEvent blocked with no condition variable"; +} + +// The signal still has to be recorded, so the state is not lost just because the +// condition variable did not exist yet. +TEST(SignaledEventLifecycle, ASignalRaisedBeforeInitIsStillDeliveredAfterIt) +{ + SignaledEvent e; + + e.SetEvent(); + e.InitEvent(); + + const TimeMS start = GetTimeMS(); + e.WaitOnEvent(30000); + const TimeMS elapsed = GetTimeMS() - start; + + e.CloseEvent(); + + ASSERT_LT(elapsed, kGenerousMS) << "the signal raised before InitEvent was lost"; +} + +TEST(SignaledEventLifecycle, SetThenWaitAfterInitReturnsImmediately) +{ + SignaledEvent e; + e.InitEvent(); + + e.SetEvent(); + + const TimeMS start = GetTimeMS(); + e.WaitOnEvent(30000); + const TimeMS elapsed = GetTimeMS() - start; + + e.CloseEvent(); + + ASSERT_LT(elapsed, kGenerousMS) << "a set event did not satisfy the wait"; +} + +// A second InitEvent must not re-create the condition variable: pthread_cond_init +// on an already-initialised one is undefined. +TEST(SignaledEventLifecycle, InitEventTwiceIsSafe) +{ + SignaledEvent e; + e.InitEvent(); + e.InitEvent(); + + e.SetEvent(); + + const TimeMS start = GetTimeMS(); + e.WaitOnEvent(30000); + const TimeMS elapsed = GetTimeMS() - start; + + e.CloseEvent(); + + ASSERT_LT(elapsed, kGenerousMS); +} + +TEST(SignaledEventLifecycle, CloseEventWithoutInitEventIsSafe) +{ + SignaledEvent e; + e.CloseEvent(); + e.CloseEvent(); + + // And it is still usable afterwards. + e.InitEvent(); + e.SetEvent(); + + const TimeMS start = GetTimeMS(); + e.WaitOnEvent(30000); + const TimeMS elapsed = GetTimeMS() - start; + + e.CloseEvent(); + + ASSERT_LT(elapsed, kGenerousMS); +} + +// Stop/start cycles close and re-open the event, which has to work. +TEST(SignaledEventLifecycle, InitEventAfterCloseEventWorksAgain) +{ + SignaledEvent e; + + e.InitEvent(); + e.SetEvent(); + e.WaitOnEvent(1000); + e.CloseEvent(); + + e.InitEvent(); + e.SetEvent(); + + const TimeMS start = GetTimeMS(); + e.WaitOnEvent(30000); + const TimeMS elapsed = GetTimeMS() - start; + + e.CloseEvent(); + + ASSERT_LT(elapsed, kGenerousMS) << "the event did not work after being re-initialised"; +} + +// Signalling after a close must be a no-op rather than a broadcast on a destroyed +// condition variable. +TEST(SignaledEventLifecycle, SetEventAfterCloseEventReturns) +{ + SignaledEvent e; + e.InitEvent(); + e.CloseEvent(); + + const TimeMS start = GetTimeMS(); + e.SetEvent(); + e.WaitOnEvent(30000); + const TimeMS elapsed = GetTimeMS() - start; + + ASSERT_LT(elapsed, kGenerousMS) << "SetEvent or WaitOnEvent used a destroyed condition variable"; +} From 4d07c3c7ae5cdc1cf0a43f53e7ade775692082dd Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:48:07 +0200 Subject: [PATCH 26/26] fix(tcpinterface): treat a failed accept as a failure, and close a connection it cannot take The Linux Release gate aborted eleven tests with *** bit out of range 0 - FD_SETSIZE on fd_set ***: terminated while Debug passed all of them. This is the Debug/Release split CLAUDE.md warns about, and the asymmetry is the whole story: glibc fortifies fd_set under -O2 and aborts on an out-of-range bit, while an unfortified Debug build writes outside the set and carries on. **accept() failure was read as success.** newSock = accept__(sts->listenSocket, ...); if (newSock != 0) { ... remoteClients[i].socket = newSock; remoteClients[i].SetActive(true); accept() returns -1 on POSIX and INVALID_SOCKET on Windows; only this class uses 0 for "no socket". A failed accept -- which happens routinely when a listener is shut down while the update thread is in its loop, exactly what the Start/Stop cycle tests do -- therefore stored -1 as a client's socket and marked the slot active. The next pass through the select loop called FD_SET(-1, &readFD). The file already carries the bruises from this: two comments read "calling FD_ISSET with -1 as socket (that's what 0 is set to) produces a bus error under Linux 64-Bit", and the loops were given `socketCopy != 0` guards. Those guard the symptom; the -1 was still being stored. Fixed with a file-local IsUsableSocket() that knows what each platform reports, now used at all three places that check a socket: accept, socket() in SocketConnect (`sockfd < 0`, which can never be true on Windows where SOCKET is unsigned) and socket() in CreateListenSocket (`(int)listenSocket == -1`, which only worked because of the cast and accepted a socket numbered 0 that this class cannot represent). **A connection accepted with every slot full was leaked.** The recovery branch tested `newRemoteClientIndex == -1`, which the loop above cannot produce -- it leaves the index at remoteClientsLength -- so it never ran, and the accepted socket was never closed: a descriptor leak per refused connection, and a client left connected to a server that would never service it. Had the branch fired it closed `sts->listenSocket`, the listener, instead of the connection it could not accommodate. Now tracked with an explicit flag, and it closes newSock. New: TCPInterfaceLifecycle.AConnectionRefusedForLackOfSlotsIsClosed -- one slot, two clients; the second must see its connection closed, and the listener must still accept once the slot frees. Proved in a Linux Release container, reverting one half at a time: with both fixes 19/19 across TCPInterfaceLifecycle and PacketizedTCPFraming revert the accept check abort, "bit out of range 0 - FD_SETSIZE" revert the full-slots close abort (closing the listener feeds the same select loop a dead descriptor) Verified: Linux Release 19/19 on the two affected suites; macOS 30/30 across the three new suites. Claude-Session: https://claude.ai/code/session_011kRj5nodTJrxyoHY7y6R6c --- Source/src/tcp_interface.cpp | 44 +++++++++-- .../TCPInterfaceLifecycleTests.cpp | 75 +++++++++++++++++++ 2 files changed, 113 insertions(+), 6 deletions(-) diff --git a/Source/src/tcp_interface.cpp b/Source/src/tcp_interface.cpp index afa1544a8..c702f31b8 100644 --- a/Source/src/tcp_interface.cpp +++ b/Source/src/tcp_interface.cpp @@ -64,6 +64,25 @@ RAK_THREAD_DECLARATION(ConnectionAttemptLoop); using namespace MafiaNet; +namespace +{ + // accept()/socket() report failure as -1 on POSIX and INVALID_SOCKET on Windows, + // while this class uses 0 as its "no socket" value. Everything else here tested + // only against 0, so a failed accept was treated as a new connection and its -1 + // was stored as a client socket; the next FD_SET(-1, ...) is undefined, and + // glibc's fortified fd_set aborts on it in a Release build ("bit out of range + // 0 - FD_SETSIZE on fd_set") while a Debug build corrupts the set in silence. + bool IsUsableSocket(__TCPSOCKET__ s) + { +#ifdef _WIN32 + return s != INVALID_SOCKET && s != 0; +#else + return s > 0; +#endif + } +} + + STATIC_FACTORY_DEFINITIONS(TCPInterface,TCPInterface); TCPInterface::TCPInterface() @@ -103,8 +122,14 @@ bool TCPInterface::CreateListenSocket(unsigned short port, unsigned short maxInc (void) socketFamily; #if RAKNET_SUPPORT_IPV6!=1 listenSocket = socket__(AF_INET, SOCK_STREAM, 0); - if ((int)listenSocket ==-1) + if (IsUsableSocket(listenSocket)==false) + { + // `(int)listenSocket == -1` was the old test: on Windows SOCKET is unsigned, + // so the cast is what made INVALID_SOCKET match at all, and a socket + // numbered 0 -- which this class cannot represent -- was accepted. + listenSocket=0; return false; + } struct sockaddr_in serverAddress; memset(&serverAddress,0,sizeof(sockaddr_in)); @@ -851,7 +876,7 @@ __TCPSOCKET__ TCPInterface::SocketConnect(const char* host, unsigned short remot } __TCPSOCKET__ sockfd = socket__(AF_INET, SOCK_STREAM, 0); - if (sockfd < 0) + if (IsUsableSocket(sockfd)==false) { freeaddrinfo(addressList); return 0; @@ -1120,9 +1145,10 @@ RAK_THREAD_DECLARATION(MafiaNet::UpdateTCPInterfaceLoop) { newSock = accept__(sts->listenSocket, (sockaddr*)&sockAddr, (socklen_t*)&sockAddrSize); - if (newSock != 0) + if (IsUsableSocket(newSock)) { - int newRemoteClientIndex=-1; + int newRemoteClientIndex; + bool slotFound=false; for (newRemoteClientIndex=0; newRemoteClientIndex < sts->remoteClientsLength; newRemoteClientIndex++) { sts->remoteClients[newRemoteClientIndex].isActiveMutex.Lock(); @@ -1155,13 +1181,19 @@ RAK_THREAD_DECLARATION(MafiaNet::UpdateTCPInterfaceLoop) *newConnectionSystemAddress=sts->remoteClients[newRemoteClientIndex].systemAddress; sts->newIncomingConnections.Push(newConnectionSystemAddress); + slotFound=true; break; } sts->remoteClients[newRemoteClientIndex].isActiveMutex.Unlock(); } - if (newRemoteClientIndex==-1) + if (slotFound==false) { - closesocket__(sts->listenSocket); + // The loop above leaves newRemoteClientIndex at + // remoteClientsLength when every slot is taken, never at -1, + // so the old `newRemoteClientIndex==-1` test never fired -- + // and when it was meant to, it closed the *listen* socket + // instead of the connection it could not accommodate. + closesocket__(newSock); } } else diff --git a/Tests/Integration/TCPInterfaceLifecycleTests.cpp b/Tests/Integration/TCPInterfaceLifecycleTests.cpp index 33ed2da61..9bc926c1f 100644 --- a/Tests/Integration/TCPInterfaceLifecycleTests.cpp +++ b/Tests/Integration/TCPInterfaceLifecycleTests.cpp @@ -179,4 +179,79 @@ TEST_F(TCPInterfaceLifecycle, AnInterfaceWhoseStartFailedCanStartAgain) ASSERT_TRUE(restarted) << "a failed Start left the interface unusable"; } +// A connection accepted when every client slot is already taken used to be +// leaked: the "no free slot" branch tested `newRemoteClientIndex == -1`, which +// the loop above can never produce -- it leaves the index at remoteClientsLength +// -- so the accepted socket was never closed and the client was left connected to +// a server that would never service it. And had the branch fired, it closed the +// *listen* socket rather than the connection it could not take. +TEST_F(TCPInterfaceLifecycle, AConnectionRefusedForLackOfSlotsIsClosed) +{ + TCPInterface server; + const unsigned short port = TestHelpers::StartTcpOnAFreePort(server, 1); // exactly one slot + ASSERT_NE(port, 0); + + TCPInterface first; + ASSERT_TRUE(first.Start(0, 0)); + first.Connect("127.0.0.1", port, false); + + SystemAddress accepted = UNASSIGNED_SYSTEM_ADDRESS; + TimeMS start = GetTimeMS(); + while (accepted == UNASSIGNED_SYSTEM_ADDRESS && GetTimeMS() - start < 10000) + { + accepted = server.HasNewIncomingConnection(); + if (accepted == UNASSIGNED_SYSTEM_ADDRESS) + RakSleep(10); + } + + TCPInterface second; + bool secondStarted = false; + SystemAddress secondLost = UNASSIGNED_SYSTEM_ADDRESS; + SystemAddress stillAccepting = UNASSIGNED_SYSTEM_ADDRESS; + + if (accepted != UNASSIGNED_SYSTEM_ADDRESS) + { + secondStarted = second.Start(0, 0); + if (secondStarted) + { + second.Connect("127.0.0.1", port, false); + + // The server has no slot for it, so the server must close the socket and + // the client must see the connection go away. + start = GetTimeMS(); + while (secondLost == UNASSIGNED_SYSTEM_ADDRESS && GetTimeMS() - start < 10000) + { + secondLost = second.HasLostConnection(); + if (secondLost == UNASSIGNED_SYSTEM_ADDRESS) + RakSleep(10); + } + + // Freeing the slot must let a later connection in -- the listener has to + // have survived the refusal. + server.CloseConnection(accepted); + RakSleep(100); + + second.Connect("127.0.0.1", port, false); + start = GetTimeMS(); + while (stillAccepting == UNASSIGNED_SYSTEM_ADDRESS && GetTimeMS() - start < 10000) + { + stillAccepting = server.HasNewIncomingConnection(); + if (stillAccepting == UNASSIGNED_SYSTEM_ADDRESS) + RakSleep(10); + } + } + } + + second.Stop(); + first.Stop(); + server.Stop(); + + ASSERT_NE(accepted, UNASSIGNED_SYSTEM_ADDRESS) << "the first connection was never accepted"; + ASSERT_TRUE(secondStarted); + ASSERT_NE(secondLost, UNASSIGNED_SYSTEM_ADDRESS) + << "the refused connection was left open instead of being closed"; + ASSERT_NE(stillAccepting, UNASSIGNED_SYSTEM_ADDRESS) + << "the listener stopped accepting after refusing a connection"; +} + #endif // _RAKNET_SUPPORT_TCPInterface