diff --git a/storm/Event.cpp b/storm/Event.cpp new file mode 100644 index 0000000..442505f --- /dev/null +++ b/storm/Event.cpp @@ -0,0 +1,437 @@ +#include "Event.h" + +#include + +#include "list/TSList.hpp" +#include "thread/CCritSect.hpp" +#include "Atomic.hpp" +#include "Error.hpp" +#include "Memory.hpp" + + +struct BREAKCMD : public TSLinkedNode { + void* data; +}; + +static CCritSect s_critsect; +static TSList> s_breakcmdlist; +static int32_t s_modified; +static ATOMIC32 s_dispatchesinprogress; + + +struct _IDHASHENTRY { + uint32_t id; + uint32_t sequence; + SEVTHANDLER handler; + _IDHASHENTRY* next; +}; + +struct _IDHASHTABLE { + _IDHASHENTRY** data; + uint32_t size; + uint32_t used; + _IDHASHTABLE* next; +}; + +struct _TYPEHASHENTRY { + uint32_t type; + uint32_t subtype; + uint32_t sequence; + _IDHASHTABLE* idhashtable; + _TYPEHASHENTRY* next; +}; + +static _TYPEHASHENTRY **s_typehashtable; +static uint32_t s_typehashtablesize; +static uint32_t s_typehashtableused; + +// Fake function to make tests not bleed into each other +void SEvtCleanExtraDataForTests() { + s_breakcmdlist.Clear(); +} + +void DeleteIdHashTable(_IDHASHTABLE* pTable) { + for (uint32_t i = 0; i < pTable->size; i++) { + for (_IDHASHENTRY* pEntry = pTable->data[i]; pEntry; pEntry = pTable->data[i]) { + pTable->data[i] = pEntry->next; + delete pEntry; + } + } + SMemFree(pTable->data, __FILE__, __LINE__, 0); + delete pTable; +} + +_TYPEHASHENTRY* FindTypeHashEntry(uint32_t type, uint32_t subtype) { + if (!s_typehashtable || s_typehashtablesize == 0) { + return nullptr; + } + + for (_TYPEHASHENTRY* pEntry = s_typehashtable[(s_typehashtablesize - 1) & (subtype ^ type)]; pEntry; pEntry = pEntry->next) { + if (pEntry->type == type && pEntry->subtype == subtype) { + return pEntry; + } + } + return nullptr; +} + +uint32_t ComputeNewTableSize(uint32_t size) { + uint32_t result = 1; + while (result <= size * 2 + 2) { + result *= 2; + } + return result; +} + +void CopyIdHashTable(_IDHASHTABLE *dest, _IDHASHTABLE *source) { + dest->size = source->size; + dest->used = source->used; + dest->data = (_IDHASHENTRY**)STORM_ALLOC(sizeof(_IDHASHENTRY*) * dest->size); + + for (uint32_t i = 0; i < source->size; i++) { + _IDHASHENTRY* pSourceData = source->data[i]; + _IDHASHENTRY** ppDestData = &dest->data[i]; + for (; pSourceData; pSourceData = pSourceData->next) { + _IDHASHENTRY* pNewEntry = STORM_NEW(_IDHASHENTRY); + + *ppDestData = pNewEntry; + *pNewEntry = *pSourceData; + } + *ppDestData = nullptr; + } +} + +int32_t SEvtBreakHandlerChain(void* data) { + s_critsect.Enter(); + s_breakcmdlist.NewNode(2, 0, 0)->data = data; + s_critsect.Leave(); + return 1; +} + +int32_t SEvtDestroy() { + s_critsect.Enter(); + + for (uint32_t i = 0; i < s_typehashtablesize; i++) { + for (_TYPEHASHENTRY* pTypeEntry = s_typehashtable[i]; pTypeEntry; pTypeEntry = s_typehashtable[i]) { + for (_IDHASHTABLE* pTable = pTypeEntry->idhashtable; pTable; pTable = pTypeEntry->idhashtable) { + pTypeEntry->idhashtable = pTable->next; + DeleteIdHashTable(pTable); + } + s_typehashtable[i] = pTypeEntry->next; + delete pTypeEntry; + } + } + + if (s_typehashtable) { + delete s_typehashtable; + } + s_typehashtable = nullptr; + s_typehashtablesize = 0; + s_typehashtableused = 0; + + s_modified = 1; + + s_critsect.Leave(); + return 1; +} + +int32_t SEvtDispatch(uint32_t type, uint32_t subtype, uint32_t id, void* data) { + SInterlockedIncrement(&s_dispatchesinprogress); + + int32_t success = 0; + uint32_t currsequence = -1; + _IDHASHENTRY *currptr = nullptr; + + do { + s_critsect.Enter(); + + int32_t breakcmd = 0; + BREAKCMD* curr = s_breakcmdlist.Head(); + int32_t iterate_delete = 0; + while (reinterpret_cast(curr) > 0) { + if (curr->data == data) { + breakcmd = 1; + iterate_delete = -12; + } + + if (iterate_delete) { + curr = iterate_delete <= 0 ? nullptr : s_breakcmdlist.DeleteNode(curr); + iterate_delete = 0; + } + else { + curr = s_breakcmdlist.RawNext(curr); + } + } + if (breakcmd) { + s_critsect.Leave(); + break; + } + + if (!currptr || s_modified) { + currptr = nullptr; + auto typeentry = FindTypeHashEntry(type, subtype); + if (typeentry) { + _IDHASHTABLE* idhash = typeentry->idhashtable; + if (idhash->data && idhash->size != 0) { + for (currptr = idhash->data[id & (idhash->size - 1)]; currptr; currptr = currptr->next) { + if (currptr->id == id && currptr->sequence < currsequence) { + break; + } + } + + if (s_dispatchesinprogress == 1) { + s_modified = 0; + } + } + } + } + + SEVTHANDLER handler = nullptr; + + if (currptr) { + handler = currptr->handler; + currsequence = currptr->sequence; + do { + currptr = currptr->next; + } while (currptr && currptr->id != id); + } + + s_critsect.Leave(); + + if (handler) { + success = 1; + handler(data); + } + } while(currptr); + + SInterlockedDecrement(&s_dispatchesinprogress); + + if (s_breakcmdlist.Head()) { + s_critsect.Enter(); + + BREAKCMD* ptr = s_breakcmdlist.Head(); + int32_t iterate_delete = 0; + while (reinterpret_cast(ptr) > 0) { + if (ptr->data == data) { + iterate_delete = 12; + } + + if (iterate_delete) { + ptr = iterate_delete <= 0 ? nullptr : s_breakcmdlist.DeleteNode(ptr); + iterate_delete = 0; + } + else { + ptr = s_breakcmdlist.RawNext(ptr); + } + } + + s_critsect.Leave(); + } + return success; +} + +int32_t SEvtPopState(uint32_t type, uint32_t subtype) { + int32_t success = 0; + s_critsect.Enter(); + + _TYPEHASHENTRY* typeentry = FindTypeHashEntry(type, subtype); + if (typeentry) { + _IDHASHTABLE *next = typeentry->idhashtable->next; + if (next) { + DeleteIdHashTable(typeentry->idhashtable); + typeentry->idhashtable = next; + } + else { + // This WILL hang if called without recursive CCritSect. + // Provide a hack since it never gets called in WoW anyway. + // It also doesn't get called by SC for that matter. Nothing calls it. Classic. + #if !defined(WHOA_STORM_C_CRIT_SECT_RECURSIVE) + s_critsect.Leave(); + success = SEvtUnregisterType(type, subtype); + s_critsect.Enter(); + #else + success = SEvtUnregisterType(type, subtype); + #endif + } + } + + s_modified = 1; + s_critsect.Leave(); + return success; +} + +int32_t SEvtPushState(uint32_t type, uint32_t subtype) { + int32_t success = 0; + s_critsect.Enter(); + + _TYPEHASHENTRY* pTypeHash = FindTypeHashEntry(type, subtype); + if (pTypeHash) { + _IDHASHTABLE* pNewTable = STORM_NEW(_IDHASHTABLE); + + CopyIdHashTable(pNewTable, pTypeHash->idhashtable); + pNewTable->next = pTypeHash->idhashtable; + pTypeHash->idhashtable = pNewTable; + + success = 1; + s_modified = 1; + } + + s_critsect.Leave(); + return success; +} + +int32_t SEvtRegisterHandler(uint32_t type, uint32_t subtype, uint32_t id, uint32_t flags, SEVTHANDLER handler) { + STORM_VALIDATE_BEGIN; + STORM_VALIDATE(handler); + STORM_VALIDATE(!flags); + STORM_VALIDATE_END; + + s_critsect.Enter(); + + _TYPEHASHENTRY* pTypeHash = FindTypeHashEntry(type, subtype); + if (!pTypeHash) { + if (s_typehashtableused >= s_typehashtablesize / 2) { + uint32_t newsize = ComputeNewTableSize(s_typehashtableused); + _TYPEHASHENTRY** pNewTable = static_cast<_TYPEHASHENTRY**>(STORM_ALLOC_ZERO(sizeof(_TYPEHASHENTRY*) * newsize)); + + if (s_typehashtable) { + for (uint32_t i = 0; i < s_typehashtablesize; i++) { + _TYPEHASHENTRY* pNext; + for (_TYPEHASHENTRY* pTable = s_typehashtable[i]; pTable; pTable = pNext) { + pNext = pTable->next; + + uint32_t idx = (newsize - 1) & (pTable->type ^ pTable->subtype); + pTable->next = pNewTable[idx]; + pNewTable[idx] = pTable; + } + } + if (s_typehashtable) { + SMemFree(s_typehashtable, __FILE__, __LINE__, 0); + } + } + s_typehashtable = pNewTable; + s_typehashtablesize = newsize; + } + + uint32_t idx = (s_typehashtablesize - 1) & (type ^ subtype); + + _TYPEHASHENTRY* pNewTypeHash = STORM_NEW_ZERO(_TYPEHASHENTRY); + + pNewTypeHash->type = type; + pNewTypeHash->subtype = subtype; + pNewTypeHash->idhashtable = STORM_NEW_ZERO(_IDHASHTABLE); + pNewTypeHash->next = s_typehashtable[idx]; + + s_typehashtable[idx] = pNewTypeHash; + s_typehashtableused++; + + pTypeHash = pNewTypeHash; + } + + if (pTypeHash->idhashtable->used >= pTypeHash->idhashtable->size / 2) { + uint32_t newsize = ComputeNewTableSize(pTypeHash->idhashtable->size); + _IDHASHENTRY** pNewTable = static_cast<_IDHASHENTRY**>(STORM_ALLOC_ZERO(sizeof(_IDHASHENTRY*) * newsize)); + _IDHASHENTRY*** pTempTable = static_cast<_IDHASHENTRY***>(STORM_ALLOC_ZERO(sizeof(_IDHASHENTRY*) * newsize)); + + for (uint32_t i = 0; i < newsize; i++) { + pTempTable[i] = &pNewTable[i]; + } + + if (pTypeHash->idhashtable->data && pTypeHash->idhashtable->size != 0) { + for (uint32_t i = 0; i < pTypeHash->idhashtable->size; i++) { + _IDHASHENTRY* pNext; + for (_IDHASHENTRY* pEntry = pTypeHash->idhashtable->data[i]; pEntry; pEntry = pNext) { + uint32_t idx = (newsize - 1) & pEntry->id; + pNext = pEntry->next; + pEntry->next = nullptr; + + *pTempTable[idx] = pEntry; + pTempTable[idx] = &pEntry->next; + } + } + } + + SMemFree(pTempTable, __FILE__, __LINE__, 0); + if (pTypeHash->idhashtable->data) { + SMemFree(pTypeHash->idhashtable->data, __FILE__, __LINE__, 0); + } + pTypeHash->idhashtable->data = pNewTable; + pTypeHash->idhashtable->size = newsize; + } + + uint32_t idx = (pTypeHash->idhashtable->size - 1) & id; + _IDHASHENTRY* pNewIdHash = STORM_NEW_ZERO(_IDHASHENTRY); + + pNewIdHash->id = id; + pNewIdHash->sequence = ++pTypeHash->sequence; + pNewIdHash->handler = handler; + + pNewIdHash->next = pTypeHash->idhashtable->data[idx]; + pTypeHash->idhashtable->data[idx] = pNewIdHash; + + pTypeHash->idhashtable->used++; + + s_modified = 1; + s_critsect.Leave(); + return 1; +} + +int32_t SEvtUnregisterHandler(uint32_t type, uint32_t subtype, uint32_t id, SEVTHANDLER handler) { + int32_t success = 0; + s_critsect.Enter(); + + _TYPEHASHENTRY* pTypeEntry = FindTypeHashEntry(type, subtype); + if (pTypeEntry) { + _IDHASHTABLE* pTable = pTypeEntry->idhashtable; + if (pTable->data && pTable->size != 0) { + _IDHASHENTRY** ppNextEntry = &pTable->data[id & (pTable->size - 1)]; + for (_IDHASHENTRY* pEntry = *ppNextEntry; pEntry; pEntry = *ppNextEntry) { + if (pEntry->id == id && (!handler || pEntry->handler == handler)) { + *ppNextEntry = pEntry->next; + delete pEntry; + + success = 1; + s_modified = 1; + pTable->used--; + } + else { + ppNextEntry = &pEntry->next; + } + } + } + } + + s_critsect.Leave(); + return success; +} + +int32_t SEvtUnregisterType(uint32_t type, uint32_t subtype) { + int32_t success = 0; + s_critsect.Enter(); + + _TYPEHASHENTRY *pTypeEntry = FindTypeHashEntry(type, subtype); + if (pTypeEntry) { + for (auto pTable = pTypeEntry->idhashtable; pTable; pTable = pTypeEntry->idhashtable) { + pTypeEntry->idhashtable = pTable->next; + DeleteIdHashTable(pTable); + } + + uint32_t idx = (s_typehashtablesize - 1) & (subtype ^ type); + _TYPEHASHENTRY** ppNextEntry = &s_typehashtable[idx]; + for (_TYPEHASHENTRY* pEntry = *ppNextEntry; pEntry; pEntry = *ppNextEntry) { + if (pEntry == pTypeEntry) { + *ppNextEntry = pEntry->next; + delete pEntry; + s_typehashtableused--; + } + else { + ppNextEntry = &pEntry->next; + } + } + + success = 1; + s_modified = 1; + } + + s_critsect.Leave(); + return success; +} diff --git a/storm/Event.h b/storm/Event.h new file mode 100644 index 0000000..b4c381d --- /dev/null +++ b/storm/Event.h @@ -0,0 +1,34 @@ +#ifndef STORM_EVENT_HPP +#define STORM_EVENT_HPP + +#include + +#ifndef STORMAPI +#if defined(_MSC_VER) + #define STORMAPI __stdcall +#else + #define STORMAPI +#endif +#endif + +typedef void (STORMAPI* SEVTHANDLER)(void*); + + +int32_t SEvtBreakHandlerChain(void* data); + +int32_t SEvtDestroy(); + +int32_t SEvtDispatch(uint32_t type, uint32_t subtype, uint32_t id, void* data); + +int32_t SEvtPopState(uint32_t type, uint32_t subtype); + +int32_t SEvtPushState(uint32_t type, uint32_t subtype); + +int32_t SEvtRegisterHandler(uint32_t type, uint32_t subtype, uint32_t id, uint32_t flags, SEVTHANDLER handler); + +int32_t SEvtUnregisterHandler(uint32_t type, uint32_t subtype, uint32_t id, SEVTHANDLER handler); + +int32_t SEvtUnregisterType(uint32_t type, uint32_t subtype); + + +#endif diff --git a/storm/Memory.hpp b/storm/Memory.hpp index 18e8c2e..e6ccd54 100644 --- a/storm/Memory.hpp +++ b/storm/Memory.hpp @@ -3,6 +3,7 @@ #include #include +#include #define SMEM_FLAG_ZEROMEMORY 0x8 diff --git a/test/Event.cpp b/test/Event.cpp new file mode 100644 index 0000000..b5faccc --- /dev/null +++ b/test/Event.cpp @@ -0,0 +1,591 @@ +#include "EventTest.h" + + +static void STORMAPI TestBreakEventHandlerSelf(void* data) { + EventHandlerTest::RegisterCall(10, data); + + CHECK(SEvtBreakHandlerChain(data) == 1); +} + +static void STORMAPI TestBreakEventHandlerOther(void* data) { + EventHandlerTest::RegisterCall(10, data); + + int bunk = 0; + CHECK(SEvtBreakHandlerChain(&bunk) == 1); +} + +TEST_CASE("SEvtBreakHandlerChain", "[event]") { + EventHandlerTest test; + + SECTION("can use nullptr as data") { + CHECK(SEvtBreakHandlerChain(nullptr) == 1); + + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler1); + CHECK(SEvtDispatch(7357, 1, 0, nullptr) == 0); + } + + SECTION("causes SEvtDispatch to break early if one of many data matches") { + int data1 = 42, data2 = 1337, data3 = 0xFFFFFFFF; + + CHECK(SEvtBreakHandlerChain(nullptr) == 1); + CHECK(SEvtBreakHandlerChain(&data1) == 1); + CHECK(SEvtBreakHandlerChain(&data2) == 1); + CHECK(SEvtBreakHandlerChain(&data3) == 1); + + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler1); + CHECK(SEvtDispatch(7357, 1, 0, &data2) == 0); + } + + SECTION("doesn't break SEvtDispatch if no data matches") { + int data1 = 42, data2 = 1337, data3 = 0xFFFFFFFF; + + CHECK(SEvtBreakHandlerChain(nullptr) == 1); + CHECK(SEvtBreakHandlerChain(&data1) == 1); + CHECK(SEvtBreakHandlerChain(&data2) == 1); + CHECK(SEvtBreakHandlerChain(&data3) == 1); + + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler1); + CHECK(SEvtDispatch(7357, 1, 0, &test) == 1); + } + + SECTION("deduplicates multiple same-data breaks") { + CHECK(SEvtBreakHandlerChain(&test) == 1); + CHECK(SEvtBreakHandlerChain(&test) == 1); + CHECK(SEvtBreakHandlerChain(&test) == 1); + + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler1); + CHECK(SEvtDispatch(7357, 1, 0, &test) == 0); + CHECK(SEvtDispatch(7357, 1, 0, &test) == 1); + } + + SECTION("causes a dispatch to stop during handling") { + SEvtRegisterHandler(0, 0, 0, 0, &TestEventHandler1); + SEvtRegisterHandler(0, 0, 0, 0, &TestBreakEventHandlerSelf); + SEvtRegisterHandler(0, 0, 0, 0, &TestEventHandler2); + + CHECK(SEvtDispatch(0, 0, 0, nullptr) == 1); + CHECK(test.NumCalls() == 2); + // Calls are reverse order, TestEventHandler1 doesn't get called + CHECK_THAT(test.CallResult(), MatchesCall({ 2, nullptr })); + CHECK_THAT(test.CallResult(), MatchesCall({ 10, nullptr })); + } + + SECTION("doesn't cause a dispatch to stop during handling if data differs") { + SEvtRegisterHandler(0, 0, 0, 0, &TestEventHandler1); + SEvtRegisterHandler(0, 0, 0, 0, &TestBreakEventHandlerOther); + SEvtRegisterHandler(0, 0, 0, 0, &TestEventHandler2); + + CHECK(SEvtDispatch(0, 0, 0, nullptr) == 1); + CHECK(test.NumCalls() == 3); + // Calls are reverse order, TestEventHandler1 doesn't get called + CHECK_THAT(test.CallResult(), MatchesCall({ 2, nullptr })); + CHECK_THAT(test.CallResult(), MatchesCall({ 10, nullptr })); + CHECK_THAT(test.CallResult(), MatchesCall({ 1, nullptr })); + } +} + +TEST_CASE("SEvtDestroy", "[event]") { + EventHandlerTest test; + + SECTION("always returns 1") { + CHECK(SEvtDestroy() == 1); + } + + SECTION("destroys all event handlers") { + SEvtRegisterHandler(1, 1, 1, 0, &TestEventHandler1); + CHECK(SEvtDispatch(1, 1, 1, nullptr) == 1); + CHECK(test.NumCalls() == 1); + + CHECK(SEvtDispatch(1, 1, 1, nullptr) == 1); + CHECK(test.NumCalls() == 2); + + CHECK(SEvtDestroy() == 1); + + // Can't increment calls since the handler was destroyed + CHECK(SEvtDispatch(1, 1, 1, nullptr) == 0); + CHECK(test.NumCalls() == 2); + } + + SECTION("doesn't destroy break data") { + // not ideal but it's official behaviour + SEvtBreakHandlerChain(nullptr); + + CHECK(SEvtDestroy() == 1); + + SEvtRegisterHandler(0, 0, 0, 0, &TestEventHandler1); + CHECK(SEvtDispatch(0, 0, 0, nullptr) == 0); + CHECK(SEvtDispatch(0, 0, 0, nullptr) == 1); + } +} + +static void STORMAPI TestNestedDispatchEventHandler(void* data) { + EventHandlerTest::RegisterCall(20, data); + + CHECK(SEvtDispatch(1337, 420, 69, nullptr) == 1); +} + +TEST_CASE("SEvtDispatch", "[event]") { + EventHandlerTest test; + + SECTION("sends data to an event handler") { + SEvtRegisterHandler(1337, 42, 5, 0, &TestEventHandler1); + + int data = 5; + CHECK(SEvtDispatch(1337, 42, 5, &data) == 1); + + REQUIRE(test.NumCalls() == 1); + CHECK_THAT(test.CallResult(), MatchesCall({ 1, &data })); + } + + SECTION("can use 0 as valid handler ids") { + SEvtRegisterHandler(0, 0, 0, 0, &TestEventHandler1); + + int data = 5; + CHECK(SEvtDispatch(0, 0, 0, &data) == 1); + + REQUIRE(test.NumCalls() == 1); + CHECK_THAT(test.CallResult(), MatchesCall({ 1, &data })); + } + + SECTION("can pass nullptr to a handler") { + SEvtRegisterHandler(0, 0, 0, 0, &TestEventHandler1); + + CHECK(SEvtDispatch(0, 0, 0, nullptr) == 1); + + REQUIRE(test.NumCalls() == 1); + CHECK_THAT(test.CallResult(), MatchesCall({ 1, nullptr })); + } + + SECTION("sends data to multiple event handlers") { + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler1); + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler2); + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler3); + + int data = 1; + CHECK(SEvtDispatch(7357, 1, 0, &data) == 1); + + REQUIRE(test.NumCalls() == 3); + CHECK_THAT(test.CallResult(), MatchesCall({ 3, &data })); + CHECK_THAT(test.CallResult(), MatchesCall({ 2, &data })); + CHECK_THAT(test.CallResult(), MatchesCall({ 1, &data })); + } + + SECTION("sends data to multiple event handlers with reverse ordering") { + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler3); + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler1); + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler2); + + int data = 1; + CHECK(SEvtDispatch(7357, 1, 0, &data) == 1); + + REQUIRE(test.NumCalls() == 3); + CHECK_THAT(test.CallResult(), MatchesCall({ 2, &data })); + CHECK_THAT(test.CallResult(), MatchesCall({ 1, &data })); + CHECK_THAT(test.CallResult(), MatchesCall({ 3, &data })); + } + + SECTION("sends data to duplicate handlers") { + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler1); + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler1); + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler1); + + int data = 1; + CHECK(SEvtDispatch(7357, 1, 0, &data) == 1); + + REQUIRE(test.NumCalls() == 3); + CHECK_THAT(test.CallResult(), MatchesCall({ 1, &data })); + CHECK_THAT(test.CallResult(), MatchesCall({ 1, &data })); + CHECK_THAT(test.CallResult(), MatchesCall({ 1, &data })); + } + + SECTION("does nothing if there are no handlers in the type") { + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler1); + + CHECK(SEvtDispatch(42, 1, 0, nullptr) == 0); + CHECK(test.NumCalls() == 0); + } + + SECTION("does nothing if there are no handlers that match the subtype") { + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler1); + + CHECK(SEvtDispatch(7357, 2, 0, nullptr) == 0); + CHECK(test.NumCalls() == 0); + } + + SECTION("does nothing if there are no handlers that match the id") { + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler1); + + CHECK(SEvtDispatch(7357, 1, 1, nullptr) == 0); + CHECK(test.NumCalls() == 0); + } + + SECTION("breaks the next dispatch if data matches any in break chain") { + int data = 42; + SEvtBreakHandlerChain(&data); + + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler1); + + // doesn't call because data matches the break chain + CHECK(SEvtDispatch(7357, 1, 0, &data) == 0); + CHECK(test.NumCalls() == 0); + + // calls because the break already occurred + CHECK(SEvtDispatch(7357, 1, 0, &data) == 1); + CHECK(test.NumCalls() == 1); + } + + SECTION("calls handlers if data doesn't match any in break chain") { + int data = 42; + int data2 = 1337; + SEvtBreakHandlerChain(&data); + + SEvtRegisterHandler(7357, 1, 0, 0, &TestEventHandler1); + + CHECK(SEvtDispatch(7357, 1, 0, &data2) == 1); + CHECK(test.NumCalls() == 1); + } + + SECTION("finds the correct handler among many") { + SEvtRegisterHandler(9000, 0, 0, 0, &TestEventHandler1); + SEvtRegisterHandler(9000, 1, 0, 0, &TestEventHandler1); + SEvtRegisterHandler(9000, 1, 1, 0, &TestEventHandler1); + SEvtRegisterHandler(420, 0, 0, 0, &TestEventHandler1); + SEvtRegisterHandler(420, 1, 0, 0, &TestEventHandler1); + SEvtRegisterHandler(420, 1, 1, 0, &TestEventHandler2); + SEvtRegisterHandler(420, 1, 2, 0, &TestEventHandler1); + SEvtRegisterHandler(42, 0, 0, 0, &TestEventHandler1); + + CHECK(SEvtDispatch(420, 1, 1, nullptr) == 1); + + CHECK(test.NumCalls() == 1); + CHECK_THAT(test.CallResult(), MatchesCall({2, nullptr})); + } + + SECTION("can use a non-pointer as data") { + SEvtRegisterHandler(0, 0, 0, 0, &TestEventHandler1); + + CHECK(SEvtDispatch(0, 0, 0, reinterpret_cast(42)) == 1); + CHECK_THAT(test.CallResult(), MatchesCall({1, reinterpret_cast(42)})); + } + + SECTION("broken handler chain still results in success if at least 1 handler called") { + SEvtRegisterHandler(0, 0, 0, 0, &TestBreakEventHandlerOther); + + CHECK(SEvtDispatch(0, 0, 0, nullptr) == 1); + CHECK(test.NumCalls() == 1); + } + + SECTION("can be called from handlers") { + SEvtRegisterHandler(0, 0, 0, 0, &TestEventHandler1); + SEvtRegisterHandler(1337, 420, 69, 0, &TestEventHandler1); + SEvtRegisterHandler(0, 0, 0, 0, &TestEventHandler2); + SEvtRegisterHandler(0, 0, 0, 0, &TestNestedDispatchEventHandler); + SEvtRegisterHandler(1337, 420, 69, 0, &TestEventHandler3); + SEvtRegisterHandler(0, 0, 0, 0, &TestEventHandler4); + + CHECK(SEvtDispatch(0, 0, 0, &test) == 1); + CHECK(test.NumCalls() == 6); + CHECK_THAT(test.CallResult(), MatchesCall({ 4, &test })); + CHECK_THAT(test.CallResult(), MatchesCall({ 20, &test })); + CHECK_THAT(test.CallResult(), MatchesCall({ 3, nullptr })); + CHECK_THAT(test.CallResult(), MatchesCall({ 1, nullptr })); + CHECK_THAT(test.CallResult(), MatchesCall({ 2, &test })); + CHECK_THAT(test.CallResult(), MatchesCall({ 1, &test })); + } +} + +TEST_CASE("SEvtPopState", "[event]") { + EventHandlerTest test; + + SECTION("fails if there are no handlers") { + CHECK(SEvtPopState(0, 0) == 0); + } + + SECTION("unregisters type if it is already top level") { + SEvtRegisterHandler(1337, 420, 69, 0, &TestEventHandler1); + + CHECK(SEvtPopState(1337, 420) == 1); + CHECK(SEvtUnregisterType(1337, 420) == 0); + } + + SECTION("popped state can receive callbacks again") { + SEvtRegisterHandler(1337, 420, 69, 0, &TestEventHandler1); + CHECK(SEvtPushState(1337, 420) == 1); + + CHECK(SEvtDispatch(1337, 420, 69, nullptr) == 0); + CHECK(test.NumCalls() == 0); + + CHECK(SEvtPopState(1337, 420) == 0); + + CHECK(SEvtDispatch(1337, 420, 69, nullptr) == 1); + CHECK(test.NumCalls() == 1); + } + + SECTION("popped state can be unregistered again") { + SEvtRegisterHandler(1337, 420, 69, 0, &TestEventHandler1); + CHECK(SEvtPushState(1337, 420) == 1); + + CHECK(SEvtUnregisterHandler(1337, 420, 69, &TestEventHandler1) == 0); + CHECK(SEvtPopState(1337, 420) == 0); + CHECK(SEvtUnregisterHandler(1337, 420, 69, &TestEventHandler1) == 1); + } +} + +TEST_CASE("SEvtPushState", "[event]") { + EventHandlerTest test; + + SECTION("fails if there are no handlers") { + CHECK(SEvtPushState(0, 0) == 0); + } + + SECTION("fails if the type does not match any registered types") { + SEvtRegisterHandler(0, 0, 1, 0, &TestEventHandler1); + SEvtRegisterHandler(10, 0, 1, 0, &TestEventHandler1); + SEvtRegisterHandler(0, 9, 1, 0, &TestEventHandler1); + + CHECK(SEvtPushState(10, 9) == 0); + } + + SECTION("succeeds if the handler type exists") { + SEvtRegisterHandler(0, 0, 1, 0, &TestEventHandler1); + CHECK(SEvtPushState(0, 0) == 1); + } + + SECTION("pushed state won't receive callbacks") { + SEvtRegisterHandler(1337, 420, 69, 0, &TestEventHandler1); + CHECK(SEvtPushState(1337, 420) == 1); + + CHECK(SEvtDispatch(1337, 420, 69, nullptr) == 0); + CHECK(test.NumCalls() == 0); + } + + SECTION("pushed state applies to all ids") { + SEvtRegisterHandler(0, 0, 9, 0, &TestEventHandler1); + SEvtRegisterHandler(0, 0, 8, 0, &TestEventHandler1); + SEvtRegisterHandler(0, 0, 66, 0, &TestEventHandler1); + + CHECK(SEvtPushState(0, 0) == 1); + + CHECK(SEvtDispatch(0, 0, 9, nullptr) == 0); + CHECK(SEvtDispatch(0, 0, 8, nullptr) == 0); + CHECK(SEvtDispatch(0, 0, 66, nullptr) == 0); + CHECK(test.NumCalls() == 0); + } + + SECTION("pushed state can't be unregistered") { + SEvtRegisterHandler(0, 0, 0, 0, &TestEventHandler1); + SEvtRegisterHandler(0, 0, 0, 0, &TestEventHandler2); + + CHECK(SEvtPushState(0, 0) == 1); + CHECK(SEvtUnregisterHandler(0, 0, 0, &TestEventHandler1) == 0); + } +} + +static void STORMAPI TestRegisterEventHandler(void* data) { + EventHandlerTest::RegisterCall(30, data); + + CHECK(SEvtRegisterHandler(0, 0, 0, 0, &TestEventHandler4) == 1); +} + +TEST_CASE("SEvtRegisterHandler", "[event]") { + EventHandlerTest test; + + SECTION("registers an event handler") { + CHECK(SEvtRegisterHandler(7357, 1, 2, 0, &TestEventHandler1) == 1); + // true if found and removed + CHECK(SEvtUnregisterHandler(7357, 1, 2, &TestEventHandler1) == 1); + } + + SECTION("registers event handlers with different types") { + CHECK(SEvtRegisterHandler(7357, 1, 2, 0, &TestEventHandler1) == 1); + CHECK(SEvtRegisterHandler(69, 1, 2, 0, &TestEventHandler1) == 1); + + CHECK(SEvtUnregisterHandler(69, 1, 2, &TestEventHandler1) == 1); + CHECK(SEvtRegisterHandler(777, 1, 2, 0, &TestEventHandler1) == 1); + + CHECK(SEvtUnregisterHandler(7357, 1, 2, &TestEventHandler1) == 1); + CHECK(SEvtUnregisterHandler(777, 1, 2, &TestEventHandler1) == 1); + } + + SECTION("adds a new handler while being processed") { + CHECK(SEvtRegisterHandler(0, 0, 0, 0, &TestEventHandler1) == 1); + CHECK(SEvtRegisterHandler(0, 0, 0, 0, &TestRegisterEventHandler) == 1); + + SEvtDispatch(0, 0, 0, nullptr); + CHECK(test.NumCalls() == 2); + CHECK_THAT(test.CallResult(), MatchesCall({30})); + CHECK_THAT(test.CallResult(), MatchesCall({1})); + + SEvtDispatch(0, 0, 0, nullptr); + CHECK(test.NumCalls() == 3); + CHECK_THAT(test.CallResult(), MatchesCall({4})); + CHECK_THAT(test.CallResult(), MatchesCall({30})); + CHECK_THAT(test.CallResult(), MatchesCall({1})); + + SEvtDispatch(0, 0, 0, nullptr); + CHECK(test.NumCalls() == 4); + CHECK_THAT(test.CallResult(), MatchesCall({4})); + CHECK_THAT(test.CallResult(), MatchesCall({4})); + CHECK_THAT(test.CallResult(), MatchesCall({30})); + CHECK_THAT(test.CallResult(), MatchesCall({1})); + } +} + +static void STORMAPI TestUnregisterEventHandler(void* data) { + EventHandlerTest::RegisterCall(30, data); + + CHECK(SEvtUnregisterHandler(0, 0, 0, &TestUnregisterEventHandler) == 1); +} + +TEST_CASE("SEvtUnregisterHandler", "[event]") { + EventHandlerTest test; + + SECTION("does nothing if handler doesn't exist") { + CHECK(SEvtUnregisterHandler(7357, 2, 1, &TestEventHandler1) == 0); + CHECK(SEvtUnregisterHandler(7357, 2, 1, nullptr) == 0); + } + + SECTION("removes multiple handler functions if handler is null") { + SEvtRegisterHandler(7357, 1, 2, 0, &TestEventHandler1); + SEvtRegisterHandler(7357, 1, 2, 0, &TestEventHandler2); + + CHECK(SEvtUnregisterHandler(7357, 1, 2, nullptr) == 1); + + // Make sure nothing gets called + CHECK(SEvtDispatch(7357, 1, 2, nullptr) == 0); + CHECK(test.NumCalls() == 0); + } + + SECTION("removes a specific event handler") { + SEvtRegisterHandler(7357, 1, 2, 0, &TestEventHandler1); + SEvtRegisterHandler(7357, 1, 2, 0, &TestEventHandler2); + + CHECK(SEvtUnregisterHandler(7357, 1, 2, &TestEventHandler1) == 1); + + // Make sure the other handler wasn't removed by checking if it is called + CHECK(SEvtDispatch(7357, 1, 2, nullptr) == 1); + CHECK_THAT(test.CallResult(), MatchesCall({ 2, nullptr })); + } + + SECTION("removes nothing if only type differs") { + SEvtRegisterHandler(7357, 1, 2, 0, &TestEventHandler1); + + CHECK(SEvtUnregisterHandler(42, 1, 2, nullptr) == 0); + + CHECK(SEvtDispatch(7357, 1, 2, nullptr) == 1); + CHECK(test.NumCalls() == 1); + } + + SECTION("removes nothing if only subtype differs") { + SEvtRegisterHandler(7357, 1, 2, 0, &TestEventHandler1); + + CHECK(SEvtUnregisterHandler(7357, 2, 2, nullptr) == 0); + + CHECK(SEvtDispatch(7357, 1, 2, nullptr) == 1); + CHECK(test.NumCalls() == 1); + } + + SECTION("removes nothing if only id differs") { + SEvtRegisterHandler(7357, 1, 2, 0, &TestEventHandler1); + + CHECK(SEvtUnregisterHandler(7357, 1, 3, nullptr) == 0); + + CHECK(SEvtDispatch(7357, 1, 2, nullptr) == 1); + CHECK(test.NumCalls() == 1); + } + + SECTION("allows a handler to remove itself while being called") { + SEvtRegisterHandler(0, 0, 0, 0, &TestUnregisterEventHandler); + + CHECK(SEvtDispatch(0, 0, 0, nullptr) == 1); + CHECK(test.NumCalls() == 1); + + CHECK(SEvtDispatch(0, 0, 0, nullptr) == 0); + } + + SECTION("stops calls once a handler removes itself") { + SEvtRegisterHandler(0, 0, 0, 0, &TestUnregisterEventHandler); + SEvtRegisterHandler(0, 0, 0, 0, &TestUnregisterEventHandler); + SEvtRegisterHandler(0, 0, 0, 0, &TestUnregisterEventHandler); + + CHECK(SEvtDispatch(0, 0, 0, nullptr) == 1); + CHECK(test.NumCalls() == 1); + CHECK(SEvtDispatch(0, 0, 0, nullptr) == 0); + } +} + +static void STORMAPI TestUnregisterTypeHandler(void* data) { + EventHandlerTest::RegisterCall(30, data); + + CHECK(SEvtUnregisterType(0, 0) == 1); +} + +TEST_CASE("SEvtUnregisterType", "[event]") { + EventHandlerTest test; + + SECTION("removes all event handlers in matching type and subtype") { + SEvtRegisterHandler(7357, 1, 2, 0, &TestEventHandler1); + SEvtRegisterHandler(7357, 1, 2, 0, &TestEventHandler2); + + CHECK(SEvtUnregisterType(7357, 1) == 1); + + CHECK(SEvtDispatch(7357, 1, 2, nullptr) == 0); + CHECK(test.NumCalls() == 0); + } + + SECTION("removes nothing if only type matches") { + SEvtRegisterHandler(7357, 2, 2, 0, &TestEventHandler1); + + CHECK(SEvtUnregisterType(7357, 1) == 0); + + CHECK(SEvtDispatch(7357, 2, 2, nullptr) == 1); + CHECK(test.NumCalls() == 1); + } + + SECTION("removes nothing if only subtype matches") { + SEvtRegisterHandler(7357, 1, 2, 0, &TestEventHandler1); + + CHECK(SEvtUnregisterType(42, 1) == 0); + + CHECK(SEvtDispatch(7357, 1, 2, nullptr) == 1); + CHECK(test.NumCalls() == 1); + } + + SECTION("removes only matching handlers among many") { + SEvtRegisterHandler(123, 1, 2, 0, &TestEventHandler1); + SEvtRegisterHandler(123, 2, 2, 0, &TestEventHandler2); + SEvtRegisterHandler(123, 2, 3, 0, &TestEventHandler3); + SEvtRegisterHandler(123, 1, 1, 0, &TestEventHandler4); + SEvtRegisterHandler(123, 2, 1, 0, &TestEventHandler1); + + CHECK(SEvtUnregisterType(123, 2) == 1); + + CHECK(SEvtDispatch(123, 2, 1, nullptr) == 0); + CHECK(SEvtDispatch(123, 2, 2, nullptr) == 0); + CHECK(SEvtDispatch(123, 2, 3, nullptr) == 0); + + CHECK(SEvtDispatch(123, 1, 1, nullptr) == 1); + CHECK(test.NumCalls() == 1); + CHECK(SEvtDispatch(123, 1, 2, nullptr) == 1); + CHECK(test.NumCalls() == 2); + } + + SECTION("allows a handler to remove itself while being called") { + SEvtRegisterHandler(0, 0, 0, 0, &TestUnregisterTypeHandler); + + CHECK(SEvtDispatch(0, 0, 0, nullptr) == 1); + CHECK(test.NumCalls() == 1); + + CHECK(SEvtDispatch(0, 0, 0, nullptr) == 0); + } + + SECTION("stops calls once a handler removes itself") { + SEvtRegisterHandler(0, 0, 0, 0, &TestUnregisterTypeHandler); + SEvtRegisterHandler(0, 0, 0, 0, &TestUnregisterTypeHandler); + SEvtRegisterHandler(0, 0, 0, 0, &TestUnregisterTypeHandler); + + CHECK(SEvtDispatch(0, 0, 0, nullptr) == 1); + CHECK(test.NumCalls() == 1); + CHECK(SEvtDispatch(0, 0, 0, nullptr) == 0); + } +} diff --git a/test/EventTest.h b/test/EventTest.h new file mode 100644 index 0000000..353e265 --- /dev/null +++ b/test/EventTest.h @@ -0,0 +1,90 @@ +#include "test/Test.hpp" +#include "storm/Event.h" + +#include +#include + + +void SEvtCleanExtraDataForTests(); + + +struct EventHandlerCalledWith { + int handler; + void* data; +}; + +static std::deque EventHandlerCallResults; + +struct EventHandlerTest { + EventHandlerTest() { + EventHandlerCallResults.clear(); + SEvtDestroy(); + SEvtCleanExtraDataForTests(); + } + + static void RegisterCall(int handler, void* data) { + EventHandlerCalledWith calledWith = {handler, data}; + EventHandlerCallResults.push_back(calledWith); + } + + static size_t NumCalls() { + return EventHandlerCallResults.size(); + } + + static EventHandlerCalledWith CallResult() { + if (EventHandlerCallResults.empty()) { + return {-1, nullptr}; + } + + EventHandlerCalledWith result = EventHandlerCallResults.front(); + EventHandlerCallResults.pop_front(); + return result; + } +}; + + +static void STORMAPI TestEventHandler1(void* data) { + EventHandlerTest::RegisterCall(1, data); +} + +static void STORMAPI TestEventHandler2(void* data) { + EventHandlerTest::RegisterCall(2, data); +} + +static void STORMAPI TestEventHandler3(void* data) { + EventHandlerTest::RegisterCall(3, data); +} + +static void STORMAPI TestEventHandler4(void* data) { + EventHandlerTest::RegisterCall(4, data); +} + + +// Helpers for comparing EventHandlerCalledWith structs +std::ostream& operator <<(std::ostream& os, EventHandlerCalledWith const& value) { + os << "{ TestEventHandler" << value.handler << ", " << value.data << " }"; + return os; +} + +template +class EventHandlerCalledWithMatcher : public Catch::MatcherBase { +private: + T cmp; + +public: + EventHandlerCalledWithMatcher(T arg) : cmp(arg) {} + + bool match(T const& in) const override { + return cmp.handler == in.handler && cmp.data == in.data; + } + + std::string describe() const override { + std::ostringstream ss; + ss << "equals " << cmp; + return ss.str(); + } +}; + +EventHandlerCalledWithMatcher MatchesCall(EventHandlerCalledWith arg) { + return { arg }; +}