diff --git a/docs/changelog.txt b/docs/changelog.txt index 4d9741ef81..89b02dd17a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -45,7 +45,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Misc Improvements - `dig-now`: handle fortification carving - `EventManager`: add new event type ``JOB_STARTED``, triggered when a job first gains a worker -- `EventManager`: add new event type ``NEW_UNIT_ACTIVE``, triggered when a new unit appears on the active list +- `EventManager`: add new event type ``UNIT_NEW_ACTIVE``, triggered when a new unit appears on the active list - `EventManager`: now each registered handler for an event can have its own frequency instead of all handlers using the lowest frequency of all handlers - `stocks`: allow search terms to match the full item label, even when the label is truncated for length - `dfhack-examples-guide`: add mugs to ``basic`` manager orders diff --git a/library/include/modules/EventManager.h b/library/include/modules/EventManager.h index 623fa0823e..775629fba4 100644 --- a/library/include/modules/EventManager.h +++ b/library/include/modules/EventManager.h @@ -13,6 +13,7 @@ #include "df/unit.h" #include "df/unit_inventory_item.h" #include "df/unit_wound.h" +#include "df/construction.h" namespace DFHack { namespace EventManager { @@ -28,8 +29,12 @@ namespace DFHack { UNIT_NEW_ACTIVE, UNIT_DEATH, ITEM_CREATED, - BUILDING, - CONSTRUCTION, + BUILDING_DESTROYED, + BUILDING_CREATED, + BUILDING, // todo: deprecate this event + CONSTRUCTION_REMOVED, + CONSTRUCTION_ADDED, + CONSTRUCTION, // todo: deprecate this event SYNDROME, INVASION, INVENTORY_CHANGE, @@ -45,6 +50,7 @@ namespace DFHack { typedef void (*callback_t)(color_ostream&, void*); //called when the event happens callback_t eventHandler; int32_t freq; //how often event is allowed to fire (in ticks) use 0 to always fire when possible + int32_t when; //when to fire event (global tick count) EventHandler(callback_t eventHandlerIn, int32_t freqIn): eventHandler(eventHandlerIn), freq(freqIn) { } @@ -60,8 +66,11 @@ namespace DFHack { struct SyndromeData { int32_t unitId; int32_t syndromeIndex; - SyndromeData(int32_t unitId_in, int32_t syndromeIndex_in): unitId(unitId_in), syndromeIndex(syndromeIndex_in) { - + SyndromeData(int32_t unitId_in, int32_t syndromeIndex_in) + : unitId(unitId_in), syndromeIndex(syndromeIndex_in) { + } + bool operator==(const SyndromeData &other) const { + return unitId == other.unitId && syndromeIndex == other.syndromeIndex; } }; @@ -72,18 +81,37 @@ namespace DFHack { InventoryItem() {} InventoryItem(int32_t id_in, df::unit_inventory_item item_in): itemId(id_in), item(item_in) {} }; + struct InventoryChangeData { int32_t unitId; - InventoryItem* item_old; - InventoryItem* item_new; + // todo: don't use pointers + std::shared_ptr item_old; + std::shared_ptr item_new; InventoryChangeData() {} - InventoryChangeData(int32_t id_in, InventoryItem* old_in, InventoryItem* new_in): unitId(id_in), item_old(old_in), item_new(new_in) {} + InventoryChangeData(int32_t id_in, InventoryItem* old_in, InventoryItem* new_in) : + unitId(id_in), + item_old(old_in), + item_new(new_in) {} + bool operator==(const InventoryChangeData &other) const { + bool unit = unitId == other.unitId; + bool newItem = (item_new && other.item_new && item_new->itemId == other.item_new->itemId) || + (!item_new && item_new == other.item_new); + bool oldItem = (item_old && other.item_old && item_old->itemId == other.item_old->itemId) || + (!item_old && item_old == other.item_old); + return unit && newItem && oldItem; + } }; struct UnitAttackData { + int32_t report_id; int32_t attacker; int32_t defender; int32_t wound; + + bool operator==(const UnitAttackData &other) const { + // fairly sure the report_id is the only thing that matters + return report_id == other.report_id && wound == other.wound; + } }; struct InteractionData { @@ -93,9 +121,15 @@ namespace DFHack { int32_t defender; int32_t attackReport; int32_t defendReport; + bool operator==(const InteractionData &other) const { + bool reports = attackReport == other.attackReport && defendReport == other.defendReport; + // based on code in the manager it doesn't need reports or verbs checked. + // since the units are deduced (it seems) from the reports... this + return reports; + } }; - DFHACK_EXPORT void registerListener(EventType::EventType e, EventHandler handler, Plugin* plugin); + DFHACK_EXPORT void registerListener(EventType::EventType e, EventHandler handler, Plugin* plugin, bool backlog = false); DFHACK_EXPORT int32_t registerTick(EventHandler handler, int32_t when, Plugin* plugin, bool absolute=false); DFHACK_EXPORT void unregister(EventType::EventType e, EventHandler handler, Plugin* plugin); DFHACK_EXPORT void unregisterAll(Plugin* plugin); @@ -126,6 +160,67 @@ namespace std { return r; } }; + template <> + struct hash { + std::size_t operator()(const df::construction& construct) const { + auto &c = construct.pos; + size_t r = 17; + const size_t m = 65537; + r = m*(r+c.x); + r = m*(r+c.y); + r = m*(r+c.z); + return r; + } + }; + template <> + struct hash { + std::size_t operator()(const DFHack::EventManager::SyndromeData& syndrome) const { + size_t r = 43; + const size_t m = 65537; + r = m*(r+syndrome.unitId); + r = m*(r+syndrome.syndromeIndex); + return r; + } + }; + template <> + struct hash { + std::size_t operator()(const DFHack::EventManager::InventoryChangeData& icd) const { + size_t r = 43; + const size_t m = 65537; + r = m*(r+icd.unitId); + if (icd.item_new) { + r=m*(r+icd.item_new->itemId); + } + if (icd.item_old) { + r=m*(r+(2*icd.item_old->itemId)); + } + return r; + } + }; + template <> + struct hash { + std::size_t operator()(const DFHack::EventManager::UnitAttackData& uad) const { + size_t r = 43; + const size_t m = 65537; + r = m*(r+uad.report_id); + r = m*(r+uad.attacker); + r = m*(r+uad.defender); + r = m*(r+uad.wound); + return r; + } + }; + template <> + struct hash { + std::size_t operator()(const DFHack::EventManager::InteractionData& interactionData) const { + size_t r = 43; + const size_t m = 65537; + r = m*(r+interactionData.attackReport); + r = m*(r+interactionData.defendReport); + r = m*(r+interactionData.attacker); + r = m*(r+interactionData.defender); + return r; + } + }; } #endif diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index c7401b469c..9711afa13c 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -58,11 +58,29 @@ static multimap tickQueue; //TODO: consider unordered_map of pairs, or unordered_map of unordered_set, or whatever static multimap handlers[EventType::EVENT_MAX]; -static int32_t eventLastTick[EventType::EVENT_MAX]; +static std::unordered_map eventLastTick; static const int32_t ticksPerYear = 403200; -void DFHack::EventManager::registerListener(EventType::EventType e, EventHandler handler, Plugin* plugin) { +void enqueueTickEvent(EventHandler &handler){ + int32_t when = 0; + df::world* world = df::global::world; + if ( world ) { + when = world->frame_counter + handler.freq; + } else { + if ( Once::doOnce("EventManager registerListener unhonored absolute=false") ) + Core::getInstance().getConsole().print("EventManager::registerTick: warning! absolute flag=false not honored.\n"); + } + handler.when = when; + tickQueue.insert(pair(handler.when, handler)); +} + +void DFHack::EventManager::registerListener(EventType::EventType e, EventHandler handler, Plugin* plugin, bool backlog) { + if(e == EventType::TICK){ + enqueueTickEvent(handler); + } + int32_t tick = backlog ? -2 : (df::global::world ? df::global::world->frame_counter : -1); + eventLastTick[handler] = tick; // received ranges.. // backlog: -1,n // !world: 0,n // world: tick,n handlers[e].insert(pair(plugin, handler)); } @@ -76,9 +94,11 @@ int32_t DFHack::EventManager::registerTick(EventHandler handler, int32_t when, P Core::getInstance().getConsole().print("EventManager::registerTick: warning! absolute flag=false not honored.\n"); } } - handler.freq = when; - tickQueue.insert(pair(handler.freq, handler)); - handlers[EventType::TICK].insert(pair(plugin,handler)); + handler.when = when; + tickQueue.insert(pair(handler.when, handler)); + // we don't track this handler, this allows registerTick to retain the old behaviour of needing to re-register the tick event + //handlers[EventType::TICK].insert(pair(plugin,handler)); + // since the event isn't added to the handlers, we don't need to unregister these events return when; } @@ -104,36 +124,47 @@ void DFHack::EventManager::unregister(EventType::EventType e, EventHandler handl continue; } i = handlers[e].erase(i); + eventLastTick.erase(handler); if ( e == EventType::TICK ) removeFromTickQueue(handler); } } void DFHack::EventManager::unregisterAll(Plugin* plugin) { - for ( auto i = handlers[EventType::TICK].find(plugin); i != handlers[EventType::TICK].end(); i++ ) { + for ( auto i = handlers[EventType::TICK].find(plugin); i != handlers[EventType::TICK].end(); ++i ) { if ( (*i).first != plugin ) break; + eventLastTick.erase(i->second); removeFromTickQueue((*i).second); } - for ( size_t a = 0; a < (size_t)EventType::EVENT_MAX; a++ ) { - handlers[a].erase(plugin); + for (auto &event_type : handlers) { + for (auto i = event_type.find(plugin); i != event_type.end(); ++i ) { + if ( (*i).first != plugin ) + break; + + eventLastTick.erase(i->second); + } + event_type.erase(plugin); } - return; } static void manageTickEvent(color_ostream& out); static void manageJobInitiatedEvent(color_ostream& out); static void manageJobStartedEvent(color_ostream& out); static void manageJobCompletedEvent(color_ostream& out); -static void manageNewUnitActiveEvent(color_ostream& out); +static void manageUnitNewActiveEvent(color_ostream& out); static void manageUnitDeathEvent(color_ostream& out); static void manageItemCreationEvent(color_ostream& out); static void manageBuildingEvent(color_ostream& out); +static void manageBuildingCreatedEvent(color_ostream& out); +static void manageBuildingDestroyedEvent(color_ostream& out); static void manageConstructionEvent(color_ostream& out); +static void manageConstructionAddedEvent(color_ostream& out); +static void manageConstructionRemovedEvent(color_ostream& out); static void manageSyndromeEvent(color_ostream& out); static void manageInvasionEvent(color_ostream& out); -static void manageEquipmentEvent(color_ostream& out); +static void manageInventoryChangeEvent(color_ostream& out); static void manageReportEvent(color_ostream& out); static void manageUnitAttackEvent(color_ostream& out); static void manageUnloadEvent(color_ostream& out){}; @@ -153,13 +184,21 @@ eventManager_t getManager(EventType::EventType t) { case EventType::JOB_COMPLETED: return manageJobCompletedEvent; case EventType::UNIT_NEW_ACTIVE: - return manageNewUnitActiveEvent; + return manageUnitNewActiveEvent; case EventType::UNIT_DEATH: return manageUnitDeathEvent; case EventType::ITEM_CREATED: return manageItemCreationEvent; + case EventType::BUILDING_DESTROYED: + return manageBuildingDestroyedEvent; + case EventType::BUILDING_CREATED: + return manageBuildingCreatedEvent; case EventType::BUILDING: return manageBuildingEvent; + case EventType::CONSTRUCTION_REMOVED: + return manageConstructionRemovedEvent; + case EventType::CONSTRUCTION_ADDED: + return manageConstructionAddedEvent; case EventType::CONSTRUCTION: return manageConstructionEvent; case EventType::SYNDROME: @@ -167,7 +206,7 @@ eventManager_t getManager(EventType::EventType t) { case EventType::INVASION: return manageInvasionEvent; case EventType::INVENTORY_CHANGE: - return manageEquipmentEvent; + return manageInventoryChangeEvent; case EventType::REPORT: return manageReportEvent; case EventType::UNIT_ATTACK: @@ -178,7 +217,7 @@ eventManager_t getManager(EventType::EventType t) { return manageInteractionEvent; case EventType::EVENT_MAX: return nullptr; - //default: + //default: //we don't do this... because then the compiler wouldn't error for missing cases in the enum } return nullptr; @@ -194,46 +233,139 @@ std::array compileManagerArray() { return managers; } +namespace df{ + bool operator==(const df::construction &A, const df::construction &B){ + return A.pos == B.pos; + } +} +template +class event_tracker { //todo: use inheritance? stl seems to use variadics, so it's unclear how well that would actually work +private: + std::unordered_map seen; + std::multimap history; +public: + event_tracker()= default; + void clear() { + seen.clear(); + history.clear(); + } + bool contains(const T &event_data) { + return seen.find(event_data) != seen.end(); + } + typename std::multimap::iterator find(const T &event_data) { + auto iter = seen.find(event_data); + if(iter != seen.end()){ + auto tick = iter->second; + for(auto jter = history.lower_bound(tick); jter != history.end(); ++jter) { + if (jter->second == event_data) { + return jter; + } + } + } + return history.end(); + } + typename std::multimap::iterator begin() { + return history.begin(); + } + typename std::multimap::iterator end() { + return history.end(); + } + typename std::multimap::iterator upper_bound(const int32_t &tick) { + return history.upper_bound(tick); + } + std::pair::iterator, bool> emplace(const int32_t &tick, const T &event_data) { + // data structure is replacing uses of std::unordered_set, so we can do this if(!contains) + if (!contains(event_data)) { + seen.emplace(event_data, tick); + return std::make_pair(history.emplace(tick, event_data), true); + } + return std::make_pair(history.end(), false); + } + typename std::multimap::iterator erase(typename std::multimap::iterator &pos) { + if (pos != history.end()) { + seen.erase(pos->second); + return history.erase(pos); + } + return history.end(); + } + typename std::multimap::iterator erase(const T& event_data) { + auto iter = seen.find(event_data); + if(iter != seen.end()){ + auto tick = iter->second; + seen.erase(iter); + for(auto jter = history.lower_bound(tick); jter != history.end(); ++jter) { + if(jter->second == event_data) { + return history.erase(jter); + } + } + } + return history.end(); + } +}; + +static int32_t getTime(){ + if(df::global::world) { + return (*df::global::cur_year) * ticksPerYear + (*df::global::cur_year_tick); + } + return -1; +} + //job initiated static int32_t lastJobId = -1; +// static std::unordered_set newJobIDs; // function fulfilled by lastJobId +static event_tracker newJobs; + +//job started +static std::unordered_set startedJobIDs; +static event_tracker startedJobs; //job completed -static unordered_map prevJobs; +//static std::unordered_set completedJobIDs; +static event_tracker completedJobs; + +//new unit active +static event_tracker activeUnits; //unit death -static unordered_set livingUnits; +static event_tracker deadUnits; //item creation static int32_t nextItem; +static event_tracker newItems; //building static int32_t nextBuilding; -static unordered_set buildings; +static event_tracker createdBuildings; +static event_tracker destroyedBuildings; //construction -static unordered_map constructions; +static event_tracker createdConstructions; +static event_tracker destroyedConstructions; static bool gameLoaded; //syndrome -static int32_t lastSyndromeTime; +static event_tracker syndromes; //invasion static int32_t nextInvasion; +static event_tracker invasions; //equipment change -//static unordered_map > equipmentLog; -static unordered_map > equipmentLog; +static unordered_map > inventoryLog; +static event_tracker equipmentChanges; //report -static int32_t lastReport; +static event_tracker newReports; //unit attack static int32_t lastReportUnitAttack; +static event_tracker attackEvents; static std::map > reportToRelevantUnits; static int32_t reportToRelevantUnitsTime = -1; //interaction static int32_t lastReportInteraction; +static event_tracker interactionEvents; void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event event) { static bool doOnce = false; @@ -248,35 +380,43 @@ void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event } if ( event == DFHack::SC_MAP_UNLOADED ) { lastJobId = -1; - for ( auto i = prevJobs.begin(); i != prevJobs.end(); i++ ) { - Job::deleteJobStruct((*i).second, true); + for (auto &key_value : newJobs) { + Job::deleteJobStruct(key_value.second, true); + } + for (auto &key_value : startedJobs) { + Job::deleteJobStruct(key_value.second, true); } - prevJobs.clear(); + for (auto &key_value : completedJobs) { + Job::deleteJobStruct(key_value.second, true); + } +// for (auto &key_value : inventoryLog) { +// //key_value +// } + newJobs.clear(); + startedJobs.clear(); + completedJobs.clear(); + activeUnits.clear(); + deadUnits.clear(); + createdBuildings.clear(); + destroyedBuildings.clear(); + createdConstructions.clear(); + destroyedConstructions.clear(); + inventoryLog.clear(); + equipmentChanges.clear(); + newReports.clear(); + //todo: clear reportToRelevantUnits? tickQueue.clear(); - livingUnits.clear(); - buildings.clear(); - constructions.clear(); - equipmentLog.clear(); Buildings::clearBuildings(out); - lastReport = -1; lastReportUnitAttack = -1; gameLoaded = false; multimap copy(handlers[EventType::UNLOAD].begin(), handlers[EventType::UNLOAD].end()); - for (auto a = copy.begin(); a != copy.end(); a++ ) { - (*a).second.eventHandler(out, NULL); + for (auto &key_value : copy) { + key_value.second.eventHandler(out, NULL); } } else if ( event == DFHack::SC_MAP_LOADED ) { - /* - int32_t tick = df::global::world->frame_counter; - multimap newTickQueue; - for ( auto i = tickQueue.begin(); i != tickQueue.end(); i++ ) - newTickQueue.insert(pair(tick+(*i).first, (*i).second)); - tickQueue.clear(); - tickQueue.insert(newTickQueue.begin(), newTickQueue.end()); - //out.print("%s,%d: on load, frame_counter = %d\n", __FILE__, __LINE__, tick); - */ + //tickQueue.clear(); if (!df::global::item_next_id) return; @@ -294,50 +434,79 @@ void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event nextInvasion = df::global::ui->invasions.next_id; lastJobId = -1 + *df::global::job_next_id; - constructions.clear(); - for ( auto i = df::global::world->constructions.begin(); i != df::global::world->constructions.end(); i++ ) { - df::construction* constr = *i; - if ( !constr ) { + // initialize our started jobs list + for ( df::job_list_link* link = df::global::world->jobs.list.next; link != nullptr; link = link->next ) { + df::job* job = link->item; + if (job && Job::getWorker(job)) { + // the job was already started, so emplace to a non-iterable area + startedJobIDs.emplace(job->id); + startedJobs.emplace(-1, Job::cloneJobStruct(job, true)); + } + } + // initialize our buildings list + for (df::building* building : df::global::world->buildings.all) { + Buildings::updateBuildings(out, (void*)intptr_t(building->id)); + // building already existed, so emplace to a non-iterable area + createdBuildings.emplace(-1, building->id); + } + // initialize our constructions list + for (auto construction : df::global::world->constructions) { + if ( !construction ) { if ( Once::doOnce("EventManager.onLoad null constr") ) { out.print("EventManager.onLoad: null construction.\n"); } continue; } - if ( constr->pos == df::coord() ) { - if ( Once::doOnce("EventManager.onLoad null position of construction.\n") ) + if (construction->pos == df::coord() ) { + if ( Once::doOnce("EventManager.onLoad null position of construction.\n") ) { out.print("EventManager.onLoad null position of construction.\n"); + } continue; } - constructions[constr->pos] = *constr; - } - for ( size_t a = 0; a < df::global::world->buildings.all.size(); a++ ) { - df::building* b = df::global::world->buildings.all[a]; - Buildings::updateBuildings(out, (void*)intptr_t(b->id)); - buildings.insert(b->id); - } - lastSyndromeTime = -1; - for ( size_t a = 0; a < df::global::world->units.all.size(); a++ ) { - df::unit* unit = df::global::world->units.all[a]; - for ( size_t b = 0; b < unit->syndromes.active.size(); b++ ) { - df::unit_syndrome* syndrome = unit->syndromes.active[b]; - int32_t startTime = syndrome->year*ticksPerYear + syndrome->year_time; - if ( startTime > lastSyndromeTime ) - lastSyndromeTime = startTime; + createdConstructions.emplace(-1, *construction); + } + int32_t current_time = getTime(); + // initialize our active units list + // initialize our dead units list + // initialize our inventory lists + // initialize our syndromes list + for ( df::unit *unit : df::global::world->units.all ) { + if(Units::isActive(unit)) { + activeUnits.emplace(-1, unit->id); + } + if (Units::isDead(unit)) { + deadUnits.emplace(-1, unit->id); + } + for (size_t idx = 0; idx < unit->syndromes.active.size(); ++idx) { + auto &syndrome = unit->syndromes.active[idx]; + int32_t startTime = syndrome->year * ticksPerYear + syndrome->year_time; + // add the syndrome if it started now or in the past + if (startTime <= current_time) { + SyndromeData data(unit->id, (int32_t)idx); + syndromes.emplace(-1, data); + } + } + //update equipment + unordered_map &last_tick_inventory = inventoryLog[unit->id]; + vector ¤t_tick_inventory = unit->inventory; + last_tick_inventory.clear(); + for (auto ct_item : current_tick_inventory) { + auto itemId = ct_item->item->id; + last_tick_inventory.emplace(itemId, InventoryItem(itemId, *ct_item)); } } - lastReport = -1; - if ( df::global::world->status.reports.size() > 0 ) { - lastReport = df::global::world->status.reports[df::global::world->status.reports.size()-1]->id; + // initialize our reports list + for(auto &r : df::global::world->status.reports){ + newReports.emplace(-1, r->id); } lastReportUnitAttack = -1; lastReportInteraction = -1; reportToRelevantUnitsTime = -1; reportToRelevantUnits.clear(); - for ( size_t a = 0; a < EventType::EVENT_MAX; a++ ) { - eventLastTick[a] = -1;//-1000000; - } - for ( size_t a = 0; a < df::global::world->history.figures.size(); a++ ) { - df::historical_figure* unit = df::global::world->history.figures[a]; +// for ( size_t a = 0; a < EventType::EVENT_MAX; ++a ) { +// eventLastTick[a] = -1;//-1000000; +// } + for (auto unit : df::global::world->history.figures) { if ( unit->id < 0 && unit->name.language < 0 ) unit->name.language = 0; } @@ -356,53 +525,54 @@ void DFHack::EventManager::manageEvents(color_ostream& out) { CoreSuspender suspender; - int32_t tick = df::global::world->frame_counter; - - for ( size_t a = 0; a < EventType::EVENT_MAX; a++ ) { - if ( handlers[a].empty() ) + // iterate the event types + for (size_t type = 0; type < EventType::EVENT_MAX; type++ ) { + if (handlers[type].empty()) continue; - int32_t eventFrequency = -100; - if ( a != EventType::TICK ) - for ( auto b = handlers[a].begin(); b != handlers[a].end(); b++ ) { - EventHandler bob = (*b).second; - if ( bob.freq < eventFrequency || eventFrequency == -100 ) - eventFrequency = bob.freq; + // events might be missed if we only do the processing when a listener wants to receive the events + eventManager[type](out); + /*bool call_events = false; + // iterate event type's handlers + for (auto &key_value : handlers[type]) { + const EventHandler &handler = key_value.second; + int32_t last_tick = eventLastTick[handler]; + // see if there is at least one handler ready to fire + if (tick - last_tick >= handler.freq){ + call_events = true; + break; } - else eventFrequency = 1; - - if ( tick >= eventLastTick[a] && tick - eventLastTick[a] < eventFrequency ) - continue; - - eventManager[a](out); - eventLastTick[a] = tick; + } + if(call_events)*/ } } static void manageTickEvent(color_ostream& out) { if (!df::global::world) return; - unordered_set toRemove; int32_t tick = df::global::world->frame_counter; - while ( !tickQueue.empty() ) { - if ( tick < (*tickQueue.begin()).first ) - break; - EventHandler handle = (*tickQueue.begin()).second; - tickQueue.erase(tickQueue.begin()); - handle.eventHandler(out, (void*)intptr_t(tick)); - toRemove.insert(handle); + unordered_set requeue; + // call due tick events + for (auto iter = tickQueue.begin(); iter != tickQueue.end() || iter->first > tick;) { + EventHandler &handler = iter->second; + handler.eventHandler(out, (void*) intptr_t(tick)); + requeue.emplace(handler); + iter = tickQueue.erase(iter); } - if ( toRemove.empty() ) - return; - for ( auto a = handlers[EventType::TICK].begin(); a != handlers[EventType::TICK].end(); ) { - EventHandler handle = (*a).second; - if ( toRemove.find(handle) == toRemove.end() ) { - a++; - continue; - } - a = handlers[EventType::TICK].erase(a); - toRemove.erase(handle); - if ( toRemove.empty() ) + // re-register tick events (only registered tick events though) + for (auto &key_value : handlers[EventType::TICK]) { + if (requeue.empty()) { break; + } + // check that this handler was fired + auto &handler = key_value.second; + if (requeue.erase(handler) != 0){ + /** + * todo(1): move re-queue to loop above (would mean ALL tick events get re-queued) + * todo(2): don't even use a queue, just follow the same pattern as other managers (fire handler according to freq, removes need for `when` member of handlers) + */ + // any registered handler that was fired must be re-queued + enqueueTickEvent(handler); + } } } @@ -411,50 +581,92 @@ static void manageJobInitiatedEvent(color_ostream& out) { return; if (!df::global::job_next_id) return; - if ( lastJobId == -1 ) { - lastJobId = *df::global::job_next_id - 1; - return; + int32_t tick = df::global::world->frame_counter; + int32_t oldest_last_tick = (uint16_t)-1; + + // update new jobs list + for ( df::job_list_link* link = df::global::world->jobs.list.next; link != nullptr; link = link->next ) { + df::job* job = link->item; + // the jobs must come after the last known job id + if (job && job->id > lastJobId) { + newJobs.emplace(tick, Job::cloneJobStruct(link->item, true)); + } } + lastJobId = *df::global::job_next_id - 1; - if ( lastJobId+1 == *df::global::job_next_id ) { - return; //no new jobs - } + // iterate event handler callbacks multimap copy(handlers[EventType::JOB_INITIATED].begin(), handlers[EventType::JOB_INITIATED].end()); - - for ( df::job_list_link* link = &df::global::world->jobs.list; link != NULL; link = link->next ) { - if ( link->item == NULL ) - continue; - if ( link->item->id <= lastJobId ) - continue; - for ( auto i = copy.begin(); i != copy.end(); i++ ) { - (*i).second.eventHandler(out, (void*)link->item); + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + oldest_last_tick = oldest_last_tick < last_tick ? oldest_last_tick : last_tick; + // make sure the frequency of this handler is obeyed + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + // send the handler all the new jobs since it last fired + auto jter = newJobs.upper_bound(last_tick); + for(;jter != newJobs.end(); ++jter){ + handler.eventHandler(out, (void*) jter->second); + } } } + // clean up memory we no longer need + auto iter = newJobs.begin(); + for(; iter != newJobs.end() && iter->first < oldest_last_tick;) { + // we cloned it we delete it + Job::deleteJobStruct(iter->second, true); + // if we delete it, we best not reference it + iter = newJobs.erase(iter); + } + - lastJobId = *df::global::job_next_id - 1; } static void manageJobStartedEvent(color_ostream& out) { if (!df::global::world) return; - - static unordered_set startedJobs; - + int32_t tick = df::global::world->frame_counter; + int32_t oldest_last_tick = (uint16_t)-1; + + // update the started jobs list for the current tick + for (df::job_list_link* link = df::global::world->jobs.list.next; link != NULL; link = link->next) { + df::job* job = link->item; + // the jobs must have a worker to start + if (job && Job::getWorker(job)) { + // the job won't be added if it already exists + if (startedJobIDs.emplace(job->id).second) { + // DF is going to delete completed jobs, and we might not send these before they're completed + startedJobs.emplace(tick, Job::cloneJobStruct(job, true)); + } + } + } // iterate event handler callbacks multimap copy(handlers[EventType::JOB_STARTED].begin(), handlers[EventType::JOB_STARTED].end()); for (auto &key_value : copy) { auto &handler = key_value.second; - for (df::job_list_link* link = df::global::world->jobs.list.next; link != NULL; link = link->next) { - df::job* job = link->item; - // the jobs must have a worker to start - if (job && Job::getWorker(job) && !startedJobs.count(job->id)) { - startedJobs.emplace(job->id); - handler.eventHandler(out, job); + auto last_tick = eventLastTick[handler]; + oldest_last_tick = oldest_last_tick < last_tick ? oldest_last_tick : last_tick; + // make sure the frequency of this handler is obeyed + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + // send the handler all the new jobs since it last fired + auto jter = startedJobs.upper_bound(last_tick); + for (; jter != startedJobs.end(); ++jter) { + // todo: entity check? before/inside handler loop? + handler.eventHandler(out, (void*) jter->second); } } } + // clean up memory we no longer need + auto iter = startedJobs.begin(); + for(; iter != startedJobs.end() && iter->first < oldest_last_tick;) { + startedJobIDs.erase(iter->second->id); + // we cloned it we delete it + Job::deleteJobStruct(iter->second, true); + // if we delete it, we best not reference it + iter = startedJobs.erase(iter); + } } - //helper function for manageJobCompletedEvent //static int32_t getWorkerID(df::job* job) { // auto ref = findRef(job->general_refs, general_ref_type::UNIT_WORKER); @@ -467,17 +679,88 @@ TODO: consider checking item creation / experience gain just in case static void manageJobCompletedEvent(color_ostream& out) { if (!df::global::world) return; - int32_t tick0 = eventLastTick[EventType::JOB_COMPLETED]; - int32_t tick1 = df::global::world->frame_counter; + static std::unordered_map last_tick_jobs; + static int32_t fn_last_tick = -1; + int32_t tick = df::global::world->frame_counter; + int32_t oldest_last_tick = (uint16_t)-1; + std::unordered_map current_jobs; + + // update the completed jobs list (this apparently isn't as straight forward as one might think) + // todo: verify the need for this check + //if it happened within a tick, must have been cancelled by the user or a plugin: not completed + if(tick > fn_last_tick) { + // we're going to need to check if started jobs are still on this list, so we copy the IDs to avoid a linear search into this list for each started_job + for (df::job_list_link* link = df::global::world->jobs.list.next; link != NULL; link = link->next) { + df::job* job = link->item; + //can't really minimize the list, because maybe somehow a non-started job will be complete next tick (hax) + //if(job && Job::getWorker(job)) { + current_jobs.emplace(job->id, Job::cloneJobStruct(job, true)); // clone now so we can swap into last_tick_jobs later + //} + } + // iterate the started jobs list + for (auto &key_value: last_tick_jobs) { + df::job* lt_job = key_value.second; + int32_t id = lt_job->id; + // check for the started job in the current jobs (the jobs we just copied above) + auto cter = current_jobs.find(id); + if (cter != current_jobs.end()) { + // if we found it, that doesn't mean it didn't complete.. it may be a repeat job + df::job* ct_job = cter->second; //ct: current tick + if (!lt_job->flags.bits.repeat) + continue; + // I'd comment these, but I don't know what we're checking exactly.. gl + if (lt_job->completion_timer != 0) + continue; + if (ct_job->completion_timer != -1) + continue; - multimap copy(handlers[EventType::JOB_COMPLETED].begin(), handlers[EventType::JOB_COMPLETED].end()); - map nowJobs; - for ( df::job_list_link* link = &df::global::world->jobs.list; link != NULL; link = link->next ) { - if ( link->item == NULL ) - continue; - nowJobs[link->item->id] = link->item; + //still false positive if cancelled at EXACTLY the right time, but experiments show this doesn't happen + + // the job won't be added if it already exists - NOTE: this means a repeat job can't be seen as completed again until it gets cleaned from the list + // This is probably going to be a huge problem as it means there is room for an interaction issue between plugins/scripts + completedJobs.emplace(tick, Job::cloneJobStruct(lt_job, true)); + continue; + } + // we didn't find it, so it's a recently finished or cancelled job + if (lt_job->flags.bits.repeat || lt_job->completion_timer != 0) + continue; + completedJobs.emplace(tick, Job::cloneJobStruct(lt_job, true)); + } } + multimap copy(handlers[EventType::JOB_COMPLETED].begin(), handlers[EventType::JOB_COMPLETED].end()); + // iterate event handler callbacks + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + oldest_last_tick = oldest_last_tick < last_tick ? oldest_last_tick : last_tick; + // enforce handler's callback frequency + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + fn_last_tick = tick; + // send the handler all the newly completed jobs since it last fired + auto iter = completedJobs.upper_bound(last_tick); + for (; iter != completedJobs.end(); ++iter) { + handler.eventHandler(out, iter->second); + } + } + } + // clean up memory of job clones which have been sent to all handlers + auto iter = completedJobs.begin(); + for(; iter != completedJobs.end() && iter->first < oldest_last_tick;) { + // we cloned it we delete it + Job::deleteJobStruct(iter->second, true); + // if we delete it, we best not reference it + iter = completedJobs.erase(iter); + } + // clean up memory of job clones from last tick + for(auto &key_value : last_tick_jobs){ + Job::deleteJobStruct(key_value.second, true); + } + // last tick jobs takes on the current tick's jobs + last_tick_jobs.swap(current_jobs); + + // moved to the bottom to be out of the way, but also all the structures used herein no longer exist #if 0 //testing info on job initiation/completion //newly allocated jobs @@ -494,7 +777,7 @@ static void manageJobCompletedEvent(color_ostream& out) { " completion_timer : %d\n" " workerID : %d\n" " time : %d -> %d\n" - "\n", job1.list_link->item, job1.id, job1.job_type, ENUM_ATTR(job_type, caption, job1.job_type), job1.flags.bits.working, job1.completion_timer, getWorkerID(&job1), tick0, tick1); + "\n", job1.list_link->item, job1.id, job1.job_type, ENUM_ATTR(job_type, caption, job1.job_type), job1.flags.bits.working, job1.completion_timer, getWorkerID(&job1), last_tick, tick); } for ( auto i = prevJobs.begin(); i != prevJobs.end(); i++ ) { df::job& job0 = *(*i).second; @@ -508,7 +791,7 @@ static void manageJobCompletedEvent(color_ostream& out) { " completion_timer : %d\n" " workerID : %d\n" " time : %d -> %d\n" - ,job0.list_link == NULL ? 0 : job0.list_link->item, job0.id, job0.job_type, ENUM_ATTR(job_type, caption, job0.job_type), job0.flags.bits.working, job0.completion_timer, getWorkerID(&job0), tick0, tick1); + ,job0.list_link == NULL ? 0 : job0.list_link->item, job0.id, job0.job_type, ENUM_ATTR(job_type, caption, job0.job_type), job0.flags.bits.working, job0.completion_timer, getWorkerID(&job0), last_tick, tick); continue; } df::job& job1 = *(*j).second; @@ -535,100 +818,72 @@ static void manageJobCompletedEvent(color_ostream& out) { job0.flags.bits.working, job1.flags.bits.working, job0.completion_timer, job1.completion_timer, getWorkerID(&job0), getWorkerID(&job1), - tick0, tick1 + last_tick, tick ); } #endif - - for ( auto i = prevJobs.begin(); i != prevJobs.end(); i++ ) { - //if it happened within a tick, must have been cancelled by the user or a plugin: not completed - if ( tick1 <= tick0 ) - continue; - - if ( nowJobs.find((*i).first) != nowJobs.end() ) { - //could have just finished if it's a repeat job - df::job& job0 = *(*i).second; - if ( !job0.flags.bits.repeat ) - continue; - df::job& job1 = *nowJobs[(*i).first]; - if ( job0.completion_timer != 0 ) - continue; - if ( job1.completion_timer != -1 ) - continue; - - //still false positive if cancelled at EXACTLY the right time, but experiments show this doesn't happen - for ( auto j = copy.begin(); j != copy.end(); j++ ) { - (*j).second.eventHandler(out, (void*)&job0); - } - continue; - } - - //recently finished or cancelled job - df::job& job0 = *(*i).second; - if ( job0.flags.bits.repeat || job0.completion_timer != 0 ) - continue; - - for ( auto j = copy.begin(); j != copy.end(); j++ ) { - (*j).second.eventHandler(out, (void*)&job0); - } - } - - //erase old jobs, copy over possibly altered jobs - for ( auto i = prevJobs.begin(); i != prevJobs.end(); i++ ) { - Job::deleteJobStruct((*i).second, true); - } - prevJobs.clear(); - - //create new jobs - for ( auto j = nowJobs.begin(); j != nowJobs.end(); j++ ) { - /*map::iterator i = prevJobs.find((*j).first); - if ( i != prevJobs.end() ) { - continue; - }*/ - - df::job* newJob = Job::cloneJobStruct((*j).second, true); - prevJobs[newJob->id] = newJob; - } } -static void manageNewUnitActiveEvent(color_ostream& out) { +static void manageUnitNewActiveEvent(color_ostream& out) { if (!df::global::world) return; - - static unordered_set activeUnits; + int32_t tick = df::global::world->frame_counter; + // update active units list for the current tick + for (df::unit* unit : df::global::world->units.active) { + int32_t id = unit->id; + if (!activeUnits.contains(id)) { + activeUnits.emplace(tick, id); + } + } multimap copy(handlers[EventType::UNIT_NEW_ACTIVE].begin(), handlers[EventType::UNIT_NEW_ACTIVE].end()); // iterate event handler callbacks for (auto &key_value : copy) { auto &handler = key_value.second; - for (df::unit* unit : df::global::world->units.active) { - int32_t id = unit->id; - if (!activeUnits.count(id)) { - handler.eventHandler(out, (void*) intptr_t(id)); // intptr_t() avoids cast from smaller type warning + auto last_tick = eventLastTick[handler]; + // enforce handler's callback frequency + if(tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + // send the handler all the new active unit id's since it last fired + auto jter = activeUnits.upper_bound(last_tick); + for(;jter != activeUnits.end(); ++jter){ + // todo: entity check? before/inside handler loop? + handler.eventHandler(out, (void*) intptr_t(jter->second)); // intptr_t() avoids cast from smaller type warning } } } } - static void manageUnitDeathEvent(color_ostream& out) { if (!df::global::world) return; - multimap copy(handlers[EventType::UNIT_DEATH].begin(), handlers[EventType::UNIT_DEATH].end()); - for ( size_t a = 0; a < df::global::world->units.all.size(); a++ ) { - df::unit* unit = df::global::world->units.all[a]; + int32_t tick = df::global::world->frame_counter; + // update dead units list for the current tick + for (df::unit* unit: df::global::world->units.all) { //if ( unit->counters.death_id == -1 ) { - if ( Units::isActive(unit) ) { - livingUnits.insert(unit->id); - continue; + if (Units::isDead(unit)) { + if (!deadUnits.contains(unit->id)) { + deadUnits.emplace(tick, unit->id); + activeUnits.erase(unit->id); + } + } else if (deadUnits.contains(unit->id)) { + // unit must have been revived + deadUnits.erase(unit->id); } - //dead: if dead since last check, trigger events - if ( livingUnits.find(unit->id) == livingUnits.end() ) - continue; - - for ( auto i = copy.begin(); i != copy.end(); i++ ) { - (*i).second.eventHandler(out, (void*)intptr_t(unit->id)); + } + // iterate event handler callbacks + multimap copy(handlers[EventType::UNIT_DEATH].begin(), handlers[EventType::UNIT_DEATH].end()); + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + // enforce handler's callback frequency + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + // send the handler all the new dead unit id's since it last fired + auto jter = deadUnits.upper_bound(last_tick); + for(; jter != deadUnits.end(); ++jter) { + handler.eventHandler(out, (void*) intptr_t(jter->second)); // intptr_t() avoids cast from smaller type warning + } } - livingUnits.erase(unit->id); } } @@ -640,32 +895,46 @@ static void manageItemCreationEvent(color_ostream& out) { if ( nextItem >= *df::global::item_next_id ) { return; } - - multimap copy(handlers[EventType::ITEM_CREATED].begin(), handlers[EventType::ITEM_CREATED].end()); + int32_t tick = df::global::world->frame_counter; size_t index = df::item::binsearch_index(df::global::world->items.all, nextItem, false); if ( index != 0 ) index--; - for ( size_t a = index; a < df::global::world->items.all.size(); a++ ) { + for (size_t a = index; a < df::global::world->items.all.size(); ++a) { df::item* item = df::global::world->items.all[a]; //already processed - if ( item->id < nextItem ) + if (item->id < nextItem) continue; //invaders - if ( item->flags.bits.foreign ) + if (item->flags.bits.foreign) continue; //traders who bring back your items? - if ( item->flags.bits.trader ) + if (item->flags.bits.trader) continue; //migrants - if ( item->flags.bits.owned ) + if (item->flags.bits.owned) continue; //spider webs don't count - if ( item->flags.bits.spider_web ) + if (item->flags.bits.spider_web) continue; - for ( auto i = copy.begin(); i != copy.end(); i++ ) { - (*i).second.eventHandler(out, (void*)intptr_t(item->id)); - } + newItems.emplace(tick, item->id); } nextItem = *df::global::item_next_id; + + // iterate event handlers + multimap copy(handlers[EventType::ITEM_CREATED].begin(), handlers[EventType::ITEM_CREATED].end()); + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + // enforce handler's callback frequency + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + // send the handler all the new item id's since it last fired + auto iter = newItems.upper_bound(last_tick); + for (; iter != newItems.end(); ++iter) { + handler.eventHandler(out, (void*) intptr_t(iter->second)); + } + } + } + } static void manageBuildingEvent(color_ostream& out) { @@ -677,289 +946,422 @@ static void manageBuildingEvent(color_ostream& out) { * TODO: could be faster * consider looking at jobs: building creation / destruction **/ - multimap copy(handlers[EventType::BUILDING].begin(), handlers[EventType::BUILDING].end()); - //first alert people about new buildings - for ( int32_t a = nextBuilding; a < *df::global::building_next_id; a++ ) { - int32_t index = df::building::binsearch_index(df::global::world->buildings.all, a); - if ( index == -1 ) { + int32_t tick = df::global::world->frame_counter; + // update destroyed building list + for (auto iter = createdBuildings.begin(); iter != createdBuildings.end();) { + int32_t id = iter->second; + int32_t index = df::building::binsearch_index(df::global::world->buildings.all, id); + // continue if we found the id in world->buildings.all + if (index != -1) { + ++iter; + continue; + } + // pretty sure we'd invalidate our loop if we added to buildings here, so we just save the id in an intermediary for now + destroyedBuildings.emplace(tick, id); + // handlers which haven't handled this building yet aren't going to (it would be very tricky to make this work) + iter = createdBuildings.erase(iter); + } + // update created building list + for (int32_t id = nextBuilding; id < *df::global::building_next_id; ++id) { + int32_t index = df::building::binsearch_index(df::global::world->buildings.all, id); + if (index == -1) { //out.print("%s, line %d: Couldn't find new building with id %d.\n", __FILE__, __LINE__, a); //the tricky thing is that when the game first starts, it's ok to skip buildings, but otherwise, if you skip buildings, something is probably wrong. TODO: make this smarter continue; } - buildings.insert(a); - for ( auto b = copy.begin(); b != copy.end(); b++ ) { - EventHandler bob = (*b).second; - bob.eventHandler(out, (void*)intptr_t(a)); - } + createdBuildings.emplace(tick, id); + // handlers which haven't handled this building yet aren't going to (it would be very tricky to make this work) + destroyedBuildings.erase(id); } nextBuilding = *df::global::building_next_id; - //now alert people about destroyed buildings - for ( auto a = buildings.begin(); a != buildings.end(); ) { - int32_t id = *a; - int32_t index = df::building::binsearch_index(df::global::world->buildings.all,id); - if ( index != -1 ) { - a++; - continue; - } - - for ( auto b = copy.begin(); b != copy.end(); b++ ) { - EventHandler bob = (*b).second; - bob.eventHandler(out, (void*)intptr_t(id)); + // todo: maybe we should create static lists, and compare sizes and only copy if they don't match, otherwise every manager function is copying their handlers every single they execute + multimap copy(handlers[EventType::BUILDING].begin(), handlers[EventType::BUILDING].end()); + // iterate event handler callbacks + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + // enforce handler's callback frequency + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + // send the handler all the destroyed buildings since it last fired + for (auto jter = destroyedBuildings.upper_bound(last_tick); jter != destroyedBuildings.end(); ++jter) { + handler.eventHandler(out, (void*) intptr_t(jter->second)); + } + // send the handler all the new buildings since it last fired + for (auto jter = createdBuildings.upper_bound(last_tick); jter != createdBuildings.end(); ++jter) { + handler.eventHandler(out, (void*) intptr_t(jter->second)); + } } - a = buildings.erase(a); } } -static void manageConstructionEvent(color_ostream& out) { +static void manageBuildingCreatedEvent(color_ostream& out) { if (!df::global::world) return; - //unordered_set constructionsNow(df::global::world->constructions.begin(), df::global::world->constructions.end()); - - multimap copy(handlers[EventType::CONSTRUCTION].begin(), handlers[EventType::CONSTRUCTION].end()); - for ( auto a = constructions.begin(); a != constructions.end(); ) { - df::construction& construction = (*a).second; - if ( df::construction::find(construction.pos) != NULL ) { - a++; + if (!df::global::building_next_id) + return; + int32_t tick = df::global::world->frame_counter; + // update created building list + for (int32_t id = nextBuilding; id < *df::global::building_next_id; ++id) { + int32_t index = df::building::binsearch_index(df::global::world->buildings.all, id); + if (index == -1) { + //out.print("%s, line %d: Couldn't find new building with id %d.\n", __FILE__, __LINE__, a); + //the tricky thing is that when the game first starts, it's ok to skip buildings, but otherwise, if you skip buildings, something is probably wrong. TODO: make this smarter continue; } - //construction removed - //out.print("Removed construction (%d,%d,%d)\n", construction.pos.x,construction.pos.y,construction.pos.z); - for ( auto b = copy.begin(); b != copy.end(); b++ ) { - EventHandler handle = (*b).second; - handle.eventHandler(out, (void*)&construction); + createdBuildings.emplace(tick, id); + // handlers which haven't handled this building yet aren't going to (it would be very tricky to make this work) + destroyedBuildings.erase(id); + } + nextBuilding = *df::global::building_next_id; + multimap copy(handlers[EventType::BUILDING_CREATED].begin(), handlers[EventType::BUILDING_CREATED].end()); + // iterate event handler callbacks + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + // enforce handler's callback frequency + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + // send the handler all the new & destroyed buildings since it last fired + auto jter = createdBuildings.upper_bound(last_tick); + for (; jter != createdBuildings.end(); ++jter) { + handler.eventHandler(out, (void*) intptr_t(jter->second)); + } } - a = constructions.erase(a); } +} - //for ( auto a = constructionsNow.begin(); a != constructionsNow.end(); a++ ) { - for ( auto a = df::global::world->constructions.begin(); a != df::global::world->constructions.end(); a++ ) { - df::construction* construction = *a; - bool b = constructions.find(construction->pos) != constructions.end(); - constructions[construction->pos] = *construction; - if ( b ) +static void manageBuildingDestroyedEvent(color_ostream& out) { + if (!df::global::world) + return; + int32_t tick = df::global::world->frame_counter; + // update destroyed building list + for (auto iter = createdBuildings.begin(); iter != createdBuildings.end();) { + int32_t id = iter->second; + int32_t index = df::building::binsearch_index(df::global::world->buildings.all, id); + // continue if we found the id in world->buildings.all + if (index != -1) { + ++iter; continue; - //construction created - //out.print("Created construction (%d,%d,%d)\n", construction->pos.x,construction->pos.y,construction->pos.z); - for ( auto b = copy.begin(); b != copy.end(); b++ ) { - EventHandler handle = (*b).second; - handle.eventHandler(out, (void*)construction); + } + // pretty sure we'd invalidate our loop if we added to buildings here, so we just save the id in an intermediary for now + destroyedBuildings.emplace(tick, id); + // handlers which haven't handled this building yet aren't going to (it would be very tricky to make this work) + iter = createdBuildings.erase(id); + } + multimap copy(handlers[EventType::BUILDING_DESTROYED].begin(), handlers[EventType::BUILDING_DESTROYED].end()); + // iterate event handler callbacks + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + // enforce handler's callback frequency + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + // send the handler all the new & destroyed buildings since it last fired + auto jter = destroyedBuildings.upper_bound(last_tick); + for (; jter != destroyedBuildings.end(); ++jter) { + handler.eventHandler(out, (void*) intptr_t(jter->second)); + } } } } -static void manageSyndromeEvent(color_ostream& out) { +static void manageConstructionEvent(color_ostream& out) { if (!df::global::world) return; - multimap copy(handlers[EventType::SYNDROME].begin(), handlers[EventType::SYNDROME].end()); - int32_t highestTime = -1; - for ( auto a = df::global::world->units.all.begin(); a != df::global::world->units.all.end(); a++ ) { - df::unit* unit = *a; -/* - if ( unit->flags1.bits.inactive ) + + int32_t tick = df::global::world->frame_counter; + for (auto &c : df::global::world->constructions) { + // anything on the global list, obviously exists.. so we ensure that coord is on the created list and isn't on the destroyed list + createdConstructions.emplace(tick, *c); // hashes based on c->pos (coord) + // handlers which haven't handled this construction yet aren't going to (it would be very tricky to make this work) + destroyedConstructions.erase(*c); + } + for (auto iter = createdConstructions.begin(); iter != createdConstructions.end();) { + // if we can't find it, it was removed + if (!df::construction::find(iter->second.pos)) { + destroyedConstructions.emplace(tick, iter->second); + // handlers which haven't handled this construction yet aren't going to (it would be very tricky to make this work) + iter = createdConstructions.erase(iter); continue; -*/ - for ( size_t b = 0; b < unit->syndromes.active.size(); b++ ) { - df::unit_syndrome* syndrome = unit->syndromes.active[b]; - int32_t startTime = syndrome->year*ticksPerYear + syndrome->year_time; - if ( startTime > highestTime ) - highestTime = startTime; - if ( startTime <= lastSyndromeTime ) - continue; + } + ++iter; + } - SyndromeData data(unit->id, b); - for ( auto c = copy.begin(); c != copy.end(); c++ ) { - EventHandler handle = (*c).second; - handle.eventHandler(out, (void*)&data); + // iterate event handler callbacks + multimap copy(handlers[EventType::CONSTRUCTION].begin(), handlers[EventType::CONSTRUCTION].end()); + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + // enforce handler's callback frequency + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + // send the handler all the removed constructions since it last fired + for(auto iter = destroyedConstructions.upper_bound(last_tick); iter != destroyedConstructions.end(); ++iter) { + handler.eventHandler(out, &iter->second); + } + // send the handler all the added constructions since it last fired + for(auto iter = createdConstructions.upper_bound(last_tick); iter != createdConstructions.end(); ++iter) { + handler.eventHandler(out, &iter->second); } } } - lastSyndromeTime = highestTime; } -static void manageInvasionEvent(color_ostream& out) { - if (!df::global::ui) - return; - multimap copy(handlers[EventType::INVASION].begin(), handlers[EventType::INVASION].end()); - - if ( df::global::ui->invasions.next_id <= nextInvasion ) +static void manageConstructionAddedEvent(color_ostream& out) { + if (!df::global::world) return; - nextInvasion = df::global::ui->invasions.next_id; - for ( auto a = copy.begin(); a != copy.end(); a++ ) { - EventHandler handle = (*a).second; - handle.eventHandler(out, (void*)intptr_t(nextInvasion-1)); + int32_t tick = df::global::world->frame_counter; + // update created construction list + for (auto &c : df::global::world->constructions) { + // anything on the global list, obviously exists.. so we ensure that coord is on the created list and isn't on the destroyed list + createdConstructions.emplace(tick, *c); // hashes based on c->pos (coord) + // handlers which haven't handled this construction yet aren't going to (it would be very tricky to make this work) + destroyedConstructions.erase(*c); + } + // iterate event handler callbacks + multimap copy(handlers[EventType::CONSTRUCTION_ADDED].begin(), handlers[EventType::CONSTRUCTION_ADDED].end()); + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + // enforce handler's callback frequency + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + // send the handler all the added constructions since it last fired + for(auto iter = createdConstructions.upper_bound(last_tick); iter != createdConstructions.end(); ++iter) { + handler.eventHandler(out, &iter->second); + } + } } } -static void manageEquipmentEvent(color_ostream& out) { +static void manageConstructionRemovedEvent(color_ostream& out) { if (!df::global::world) return; - multimap copy(handlers[EventType::INVENTORY_CHANGE].begin(), handlers[EventType::INVENTORY_CHANGE].end()); - unordered_map itemIdToInventoryItem; - unordered_set currentlyEquipped; - for ( auto a = df::global::world->units.all.begin(); a != df::global::world->units.all.end(); a++ ) { - itemIdToInventoryItem.clear(); - currentlyEquipped.clear(); - df::unit* unit = *a; - /*if ( unit->flags1.bits.inactive ) + int32_t tick = df::global::world->frame_counter; + // update destroyed constructions list + for (auto iter = createdConstructions.begin(); iter != createdConstructions.end();) { + // if we can't find it, it was removed + if (!df::construction::find(iter->second.pos)) { + destroyedConstructions.emplace(tick, iter->second); + // handlers which haven't handled this construction yet aren't going to (it would be very tricky to make this work) + iter = createdConstructions.erase(iter); continue; - */ - - auto oldEquipment = equipmentLog.find(unit->id); - bool hadEquipment = oldEquipment != equipmentLog.end(); - vector* temp; - if ( hadEquipment ) { - temp = &((*oldEquipment).second); - } else { - temp = new vector; - } - //vector& v = (*oldEquipment).second; - vector& v = *temp; - for ( auto b = v.begin(); b != v.end(); b++ ) { - InventoryItem& i = *b; - itemIdToInventoryItem[i.itemId] = i; - } - for ( size_t b = 0; b < unit->inventory.size(); b++ ) { - df::unit_inventory_item* dfitem_new = unit->inventory[b]; - currentlyEquipped.insert(dfitem_new->item->id); - InventoryItem item_new(dfitem_new->item->id, *dfitem_new); - auto c = itemIdToInventoryItem.find(dfitem_new->item->id); - if ( c == itemIdToInventoryItem.end() ) { - //new item equipped (probably just picked up) - InventoryChangeData data(unit->id, NULL, &item_new); - for ( auto h = copy.begin(); h != copy.end(); h++ ) { - EventHandler handle = (*h).second; - handle.eventHandler(out, (void*)&data); - } - continue; - } - InventoryItem item_old = (*c).second; - - df::unit_inventory_item& item0 = item_old.item; - df::unit_inventory_item& item1 = item_new.item; - if ( item0.mode == item1.mode && item0.body_part_id == item1.body_part_id && item0.wound_id == item1.wound_id ) - continue; - //some sort of change in how it's equipped - - InventoryChangeData data(unit->id, &item_old, &item_new); - for ( auto h = copy.begin(); h != copy.end(); h++ ) { - EventHandler handle = (*h).second; - handle.eventHandler(out, (void*)&data); - } } - //check for dropped items - for ( auto b = v.begin(); b != v.end(); b++ ) { - InventoryItem i = *b; - if ( currentlyEquipped.find(i.itemId) != currentlyEquipped.end() ) - continue; - //TODO: delete ptr if invalid - InventoryChangeData data(unit->id, &i, NULL); - for ( auto h = copy.begin(); h != copy.end(); h++ ) { - EventHandler handle = (*h).second; - handle.eventHandler(out, (void*)&data); + ++iter; + } + // iterate event handler callbacks + multimap copy(handlers[EventType::CONSTRUCTION_REMOVED].begin(), handlers[EventType::CONSTRUCTION_REMOVED].end()); + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + // enforce handler's callback frequency + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + // send the handler all the removed constructions since it last fired + for(auto iter = destroyedConstructions.upper_bound(last_tick); iter != destroyedConstructions.end(); ++iter) { + handler.eventHandler(out, &iter->second); } } - if ( !hadEquipment ) - delete temp; - - //update equipment - vector& equipment = equipmentLog[unit->id]; - equipment.clear(); - for ( size_t b = 0; b < unit->inventory.size(); b++ ) { - df::unit_inventory_item* dfitem = unit->inventory[b]; - InventoryItem item(dfitem->item->id, *dfitem); - equipment.push_back(item); - } } } -static void updateReportToRelevantUnits() { +static void manageSyndromeEvent(color_ostream& out) { if (!df::global::world) return; - if ( df::global::world->frame_counter <= reportToRelevantUnitsTime ) - return; - reportToRelevantUnitsTime = df::global::world->frame_counter; + int32_t tick = df::global::world->frame_counter; - for ( size_t a = 0; a < df::global::world->units.all.size(); a++ ) { - df::unit* unit = df::global::world->units.all[a]; - for ( int16_t b = df::enum_traits::first_item_value; b <= df::enum_traits::last_item_value; b++ ) { - if ( b == df::unit_report_type::Sparring ) - continue; - for ( size_t c = 0; c < unit->reports.log[b].size(); c++ ) { - int32_t report = unit->reports.log[b][c]; - if ( std::find(reportToRelevantUnits[report].begin(), reportToRelevantUnits[report].end(), unit->id) != reportToRelevantUnits[report].end() ) - continue; - reportToRelevantUnits[unit->reports.log[b][c]].push_back(unit->id); + int32_t current_time = getTime(); + // update syndromes list + for ( df::unit *unit : df::global::world->units.all ) { + for (size_t idx = 0; idx < unit->syndromes.active.size(); ++idx) { + auto &syndrome = unit->syndromes.active[idx]; + int32_t startTime = syndrome->year * ticksPerYear + syndrome->year_time; + // add the syndrome if it started now or in the past + if (startTime <= current_time) { + syndromes.emplace(tick, SyndromeData(unit->id, (int32_t)idx)); + } + } + } + + // iterate event handler callbacks + multimap copy(handlers[EventType::SYNDROME].begin(), handlers[EventType::SYNDROME].end()); + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + // enforce handler's callback frequency + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + // send all new syndromes since it last fired + for(auto iter = syndromes.upper_bound(last_tick); iter != syndromes.end(); ++iter){ + handler.eventHandler(out, &iter->second); } } } } -static void manageReportEvent(color_ostream& out) { +static void manageInvasionEvent(color_ostream& out) { if (!df::global::world) return; - multimap copy(handlers[EventType::REPORT].begin(), handlers[EventType::REPORT].end()); - std::vector& reports = df::global::world->status.reports; - size_t a = df::report::binsearch_index(reports, lastReport, false); - //this may or may not be needed: I don't know if binsearch_index goes earlier or later if it can't hit the target exactly - while (a < reports.size() && reports[a]->id <= lastReport) { - a++; + if (!df::global::ui) + return; + + int32_t tick = df::global::world->frame_counter; + invasions.emplace(tick, nextInvasion); + nextInvasion = df::global::ui->invasions.next_id; + + // iterate event handler callbacks + multimap copy(handlers[EventType::INVASION].begin(), handlers[EventType::INVASION].end()); + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + // enforce handler's callback frequency + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + // send all new invasions since it last fired + for(auto iter = invasions.upper_bound(last_tick); iter != invasions.end(); ++iter){ + handler.eventHandler(out, (void*)(intptr_t)iter->second); + } + } } - for ( ; a < reports.size(); a++ ) { - df::report* report = reports[a]; - for ( auto b = copy.begin(); b != copy.end(); b++ ) { - EventHandler handle = (*b).second; - handle.eventHandler(out, (void*)intptr_t(report->id)); +} + +static void manageInventoryChangeEvent(color_ostream& out) { + if (!df::global::world) + return; + + int32_t tick = df::global::world->frame_counter; + for (auto unit : df::global::world->units.all) { + auto unitId = unit->id; + unordered_map &last_tick_inventory = inventoryLog[unitId]; + vector ¤t_tick_inventory = unit->inventory; + unordered_set currentlyEquipped; + + // iterate current tick's inventory for unit (std::vector) + for (auto ct_item : current_tick_inventory) { + auto itemId = ct_item->item->id; + currentlyEquipped.emplace(itemId); + + // detect new + auto lti_iter = last_tick_inventory.find(itemId); + if (lti_iter == last_tick_inventory.end()) { + // add to list because the item didn't exist before + equipmentChanges.emplace(tick, InventoryChangeData(unitId, nullptr, new InventoryItem(itemId, *ct_item))); + continue; + } + + // detect change - (in how item is equipped) + InventoryItem item_old = lti_iter->second; + df::unit_inventory_item &item0 = item_old.item; + df::unit_inventory_item &item1 = *ct_item; + if (item0.mode == item1.mode && item0.body_part_id == item1.body_part_id && + item0.wound_id == item1.wound_id) + continue; + // add to list because item is not equipped in the same way + equipmentChanges.emplace(tick, InventoryChangeData(unit->id, new InventoryItem(item_old.itemId, item_old.item), new InventoryItem(itemId, *ct_item))); + } + //check for dropped items + for (auto &key_value : last_tick_inventory) { + auto &item = key_value.second; + // add to list if unit doesn't have the item + if (currentlyEquipped.find(item.itemId) == currentlyEquipped.end()) { + equipmentChanges.emplace(tick, InventoryChangeData(unit->id, new InventoryItem(item.itemId, item.item), nullptr)); + } + } + + //update equipment + last_tick_inventory.clear(); + for (auto ct_item : current_tick_inventory) { + auto itemId = ct_item->item->id; + last_tick_inventory.emplace(itemId, InventoryItem(itemId, *ct_item)); + } + } + // iterate event handler callbacks + multimap copy(handlers[EventType::INVENTORY_CHANGE].begin(), handlers[EventType::INVENTORY_CHANGE].end()); + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + // ensure handler's callback frequency + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + // send all new equipment changes since it last fired + for(auto iter = equipmentChanges.upper_bound(last_tick); iter != equipmentChanges.end(); ++iter){ + handler.eventHandler(out, &iter->second); + } } - lastReport = report->id; } } -static df::unit_wound* getWound(df::unit* attacker, df::unit* defender) { - for ( size_t a = 0; a < defender->body.wounds.size(); a++ ) { - df::unit_wound* wound = defender->body.wounds[a]; - if ( wound->age <= 1 && wound->attacker_unit_id == attacker->id ) { - return wound; +static void manageReportEvent(color_ostream& out) { + if (!df::global::world) + return; + + int32_t tick = df::global::world->frame_counter; + std::unordered_set valid_reports; + // update reports list + for(auto &r : df::global::world->status.reports){ + valid_reports.emplace(r->id); + newReports.emplace(tick, r->id); + } + + // iterate event handler callbacks + multimap copy(handlers[EventType::REPORT].begin(), handlers[EventType::REPORT].end()); + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + // enforce handler's callback frequency + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + // send all new reports since it last fired + auto iter = newReports.upper_bound(last_tick); + for(;iter != newReports.end();) { + if (valid_reports.count(iter->second)) { + handler.eventHandler(out, (void*) intptr_t(iter->second)); + ++iter; + } else { + iter = newReports.erase(iter->second); + } + } } } - return NULL; } + +static void updateReportToRelevantUnits(); +static df::unit_wound* getWound(df::unit* attacker, df::unit* defender); static void manageUnitAttackEvent(color_ostream& out) { if (!df::global::world) return; - multimap copy(handlers[EventType::UNIT_ATTACK].begin(), handlers[EventType::UNIT_ATTACK].end()); + int32_t tick = df::global::world->frame_counter; std::vector& reports = df::global::world->status.reports; size_t a = df::report::binsearch_index(reports, lastReportUnitAttack, false); //this may or may not be needed: I don't know if binsearch_index goes earlier or later if it can't hit the target exactly while (a < reports.size() && reports[a]->id <= lastReportUnitAttack) { - a++; + ++a; } std::set strikeReports; - for ( ; a < reports.size(); a++ ) { + for ( ; a < reports.size(); ++a ) { df::report* report = reports[a]; lastReportUnitAttack = report->id; - if ( report->flags.bits.continuation ) + if ( report->flags.bits.continuation ) { continue; + } df::announcement_type type = report->type; if ( type == df::announcement_type::COMBAT_STRIKE_DETAILS ) { strikeReports.insert(report->id); } } - - if ( strikeReports.empty() ) - return; updateReportToRelevantUnits(); map > alreadyDone; - for ( auto a = strikeReports.begin(); a != strikeReports.end(); a++ ) { - int32_t reportId = *a; + for (int reportId : strikeReports) { df::report* report = df::report::find(reportId); if ( !report ) continue; //TODO: error std::string reportStr = report->text; - for ( int32_t b = reportId+1; ; b++ ) { + for ( int32_t b = reportId+1; ; ++b ) { df::report* report2 = df::report::find(b); if ( !report2 ) break; @@ -967,7 +1369,7 @@ static void manageUnitAttackEvent(color_ostream& out) { break; if ( !report2->flags.bits.continuation ) break; - reportStr = reportStr + report2->text; + reportStr += report2->text; } std::vector& relevantUnits = reportToRelevantUnits[report->id]; @@ -982,53 +1384,47 @@ static void manageUnitAttackEvent(color_ostream& out) { df::unit_wound* wound2 = getWound(unit2,unit1); if ( wound1 && !alreadyDone[unit1->id][unit2->id] ) { - UnitAttackData data; + UnitAttackData data{}; + data.report_id = report->id; data.attacker = unit1->id; data.defender = unit2->id; data.wound = wound1->id; alreadyDone[data.attacker][data.defender] = 1; - for ( auto b = copy.begin(); b != copy.end(); b++ ) { - EventHandler handle = (*b).second; - handle.eventHandler(out, (void*)&data); - } + attackEvents.emplace(tick, data); } if ( wound2 && !alreadyDone[unit1->id][unit2->id] ) { - UnitAttackData data; + UnitAttackData data{}; + data.report_id = report->id; data.attacker = unit2->id; data.defender = unit1->id; data.wound = wound2->id; alreadyDone[data.attacker][data.defender] = 1; - for ( auto b = copy.begin(); b != copy.end(); b++ ) { - EventHandler handle = (*b).second; - handle.eventHandler(out, (void*)&data); - } + attackEvents.emplace(tick, data); } if ( Units::isKilled(unit1) ) { - UnitAttackData data; + UnitAttackData data{}; + data.report_id = report->id; data.attacker = unit2->id; data.defender = unit1->id; data.wound = -1; + alreadyDone[data.attacker][data.defender] = 1; - for ( auto b = copy.begin(); b != copy.end(); b++ ) { - EventHandler handle = (*b).second; - handle.eventHandler(out, (void*)&data); - } + attackEvents.emplace(tick, data); } if ( Units::isKilled(unit2) ) { - UnitAttackData data; + UnitAttackData data{}; + data.report_id = report->id; data.attacker = unit1->id; data.defender = unit2->id; data.wound = -1; + alreadyDone[data.attacker][data.defender] = 1; - for ( auto b = copy.begin(); b != copy.end(); b++ ) { - EventHandler handle = (*b).second; - handle.eventHandler(out, (void*)&data); - } + attackEvents.emplace(tick, data); } if ( !wound1 && !wound2 ) { @@ -1041,6 +1437,162 @@ static void manageUnitAttackEvent(color_ostream& out) { } } } + + multimap copy(handlers[EventType::UNIT_ATTACK].begin(), handlers[EventType::UNIT_ATTACK].end()); + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + for(auto iter = attackEvents.upper_bound(last_tick); iter != attackEvents.end(); ++iter){ + handler.eventHandler(out, &iter->second); + } + } + } +} + +static vector gatherRelevantUnits(color_ostream& out, df::report* r1, df::report* r2); +static InteractionData getAttacker(color_ostream& out, df::report* attackEvent, df::unit* lastAttacker, df::report* defendEvent, vector& relevantUnits); +static void manageInteractionEvent(color_ostream& out) { + if (!df::global::world) + return; + + int32_t tick = df::global::world->frame_counter; + std::vector& reports = df::global::world->status.reports; + size_t a = df::report::binsearch_index(reports, lastReportInteraction, false); + if ( a < reports.size() ) { + updateReportToRelevantUnits(); + } + + df::report* lastAttackEvent = NULL; + df::unit* lastAttacker = NULL; + //df::unit* lastDefender = NULL; + unordered_map > history; + for ( ; a < reports.size(); ++a ) { + df::report* report = reports[a]; + lastReportInteraction = report->id; + df::announcement_type type = report->type; + if ( type != df::announcement_type::INTERACTION_ACTOR && type != df::announcement_type::INTERACTION_TARGET ) + continue; + if ( report->flags.bits.continuation ) + continue; + bool attack = type == df::announcement_type::INTERACTION_ACTOR; + if ( attack ) { + lastAttackEvent = report; + lastAttacker = NULL; + //lastDefender = NULL; + } + vector relevantUnits = gatherRelevantUnits(out, lastAttackEvent, report); + InteractionData data = getAttacker(out, lastAttackEvent, lastAttacker, attack ? NULL : report, relevantUnits); + if ( data.attacker < 0 ) + continue; +//out.print("%s,%d\n",__FILE__,__LINE__); + //if ( !attack && lastAttacker && data.attacker == lastAttacker->id && lastDefender && data.defender == lastDefender->id ) + // continue; //lazy way of preventing duplicates + if ( attack && a+1 < reports.size() && reports[a+1]->type == df::announcement_type::INTERACTION_TARGET ) { +//out.print("%s,%d\n",__FILE__,__LINE__); + vector relevants = gatherRelevantUnits(out, lastAttackEvent, reports[a+1]); + InteractionData data2 = getAttacker(out, lastAttackEvent, lastAttacker, reports[a+1], relevants); + if ( data.attacker == data2.attacker && (data.defender == -1 || data.defender == data2.defender) ) { +//out.print("%s,%d\n",__FILE__,__LINE__); + data = data2; + ++a; + } + } + { +#define HISTORY_ITEM 1 +#if HISTORY_ITEM + unordered_set& b = history[data.attacker]; + if ( b.find(data.defender) != b.end() ) + continue; + history[data.attacker].insert(data.defender); + //b.insert(data.defender); +#else + unordered_set& b = history[data.attackReport]; + if ( b.find(data.defendReport) != b.end() ) + continue; + history[data.attackReport].insert(data.defendReport); + //b.insert(data.defendReport); +#endif + } +//out.print("%s,%d\n",__FILE__,__LINE__); + lastAttacker = df::unit::find(data.attacker); + //lastDefender = df::unit::find(data.defender); + //fire event + interactionEvents.emplace(tick, data); + //TODO: deduce attacker from latest defend event first + } + + multimap copy(handlers[EventType::INTERACTION].begin(), handlers[EventType::INTERACTION].end()); + for (auto &key_value : copy) { + auto &handler = key_value.second; + auto last_tick = eventLastTick[handler]; + if (tick - last_tick >= handler.freq) { + eventLastTick[handler] = tick; + for(auto iter = interactionEvents.upper_bound(last_tick); iter != interactionEvents.end(); ++iter){ + handler.eventHandler(out, &iter->second); + } + } + } +} + + + +static void updateReportToRelevantUnits() { + if (!df::global::world) + return; + if ( df::global::world->frame_counter <= reportToRelevantUnitsTime ) + return; + reportToRelevantUnitsTime = df::global::world->frame_counter; + + for ( df::unit *unit : df::global::world->units.all ) { + for ( int16_t b = df::enum_traits::first_item_value; b <= df::enum_traits::last_item_value; ++b ) { + if ( b == df::unit_report_type::Sparring ) + continue; + for ( size_t c = 0; c < unit->reports.log[b].size(); c++ ) { + int32_t report = unit->reports.log[b][c]; + if ( std::find(reportToRelevantUnits[report].begin(), reportToRelevantUnits[report].end(), unit->id) != reportToRelevantUnits[report].end() ) + continue; + reportToRelevantUnits[unit->reports.log[b][c]].push_back(unit->id); + } + } + } +} + +static df::unit_wound* getWound(df::unit* attacker, df::unit* defender) { + for ( size_t a = 0; a < defender->body.wounds.size(); ++a ) { + df::unit_wound* wound = defender->body.wounds[a]; + if ( wound->age <= 1 && wound->attacker_unit_id == attacker->id ) { + return wound; + } + } + return NULL; +} + +static vector gatherRelevantUnits(color_ostream& out, df::report* r1, df::report* r2) { + vector reports; + if ( r1 == r2 ) r2 = NULL; + if ( r1 ) reports.push_back(r1); + if ( r2 ) reports.push_back(r2); + vector result; + unordered_set ids; +//out.print("%s,%d\n",__FILE__,__LINE__); + for ( size_t a = 0; a < reports.size(); ++a ) { +//out.print("%s,%d\n",__FILE__,__LINE__); + vector& units = reportToRelevantUnits[reports[a]->id]; + if ( units.size() > 2 ) { + if ( Once::doOnce("EventManager interaction too many relevant units") ) { + out.print("%s,%d: too many relevant units. On report\n \'%s\'\n", __FILE__, __LINE__, reports[a]->text.c_str()); + } + } + for ( size_t b = 0; b < units.size(); ++b ) + if ( ids.find(units[b]) == ids.end() ) { + ids.insert(units[b]); + result.push_back(df::unit::find(units[b])); + } + } +//out.print("%s,%d\n",__FILE__,__LINE__); + return result; } static std::string getVerb(df::unit* unit, std::string reportStr) { @@ -1081,7 +1633,7 @@ static InteractionData getAttacker(color_ostream& out, df::report* attackEvent, //find valid interactions: TODO /*map > validInteractions; - for ( size_t a = 0; a < relevantUnits.size(); a++ ) { + for ( size_t a = 0; a < relevantUnits.size(); ++a ) { df::unit* unit = relevantUnits[a]; vector& interactions = validInteractions[unit->id]; for ( size_t b = 0; b < unit->body. @@ -1094,7 +1646,7 @@ static InteractionData getAttacker(color_ostream& out, df::report* attackEvent, std::string attackVerb; if ( attackEvent ) { //out.print("%s,%d\n",__FILE__,__LINE__); - for ( size_t a = 0; a < attackers.size(); a++ ) { + for ( size_t a = 0; a < attackers.size(); ++a ) { if ( attackers[a]->pos != attackEvent->pos ) { attackers.erase(attackers.begin()+a); a--; @@ -1123,7 +1675,7 @@ static InteractionData getAttacker(color_ostream& out, df::report* attackEvent, std::string defendVerb; if ( defendEvent ) { //out.print("%s,%d\n",__FILE__,__LINE__); - for ( size_t a = 0; a < defenders.size(); a++ ) { + for ( size_t a = 0; a < defenders.size(); ++a ) { if ( defenders[a]->pos != defendEvent->pos ) { defenders.erase(defenders.begin()+a); a--; @@ -1190,103 +1742,3 @@ static InteractionData getAttacker(color_ostream& out, df::report* attackEvent, return result; } -static vector gatherRelevantUnits(color_ostream& out, df::report* r1, df::report* r2) { - vector reports; - if ( r1 == r2 ) r2 = NULL; - if ( r1 ) reports.push_back(r1); - if ( r2 ) reports.push_back(r2); - vector result; - unordered_set ids; -//out.print("%s,%d\n",__FILE__,__LINE__); - for ( size_t a = 0; a < reports.size(); a++ ) { -//out.print("%s,%d\n",__FILE__,__LINE__); - vector& units = reportToRelevantUnits[reports[a]->id]; - if ( units.size() > 2 ) { - if ( Once::doOnce("EventManager interaction too many relevant units") ) { - out.print("%s,%d: too many relevant units. On report\n \'%s\'\n", __FILE__, __LINE__, reports[a]->text.c_str()); - } - } - for ( size_t b = 0; b < units.size(); b++ ) - if ( ids.find(units[b]) == ids.end() ) { - ids.insert(units[b]); - result.push_back(df::unit::find(units[b])); - } - } -//out.print("%s,%d\n",__FILE__,__LINE__); - return result; -} - -static void manageInteractionEvent(color_ostream& out) { - if (!df::global::world) - return; - multimap copy(handlers[EventType::INTERACTION].begin(), handlers[EventType::INTERACTION].end()); - std::vector& reports = df::global::world->status.reports; - size_t a = df::report::binsearch_index(reports, lastReportInteraction, false); - while (a < reports.size() && reports[a]->id <= lastReportInteraction) { - a++; - } - if ( a < reports.size() ) - updateReportToRelevantUnits(); - - df::report* lastAttackEvent = NULL; - df::unit* lastAttacker = NULL; - //df::unit* lastDefender = NULL; - unordered_map > history; - for ( ; a < reports.size(); a++ ) { - df::report* report = reports[a]; - lastReportInteraction = report->id; - df::announcement_type type = report->type; - if ( type != df::announcement_type::INTERACTION_ACTOR && type != df::announcement_type::INTERACTION_TARGET ) - continue; - if ( report->flags.bits.continuation ) - continue; - bool attack = type == df::announcement_type::INTERACTION_ACTOR; - if ( attack ) { - lastAttackEvent = report; - lastAttacker = NULL; - //lastDefender = NULL; - } - vector relevantUnits = gatherRelevantUnits(out, lastAttackEvent, report); - InteractionData data = getAttacker(out, lastAttackEvent, lastAttacker, attack ? NULL : report, relevantUnits); - if ( data.attacker < 0 ) - continue; -//out.print("%s,%d\n",__FILE__,__LINE__); - //if ( !attack && lastAttacker && data.attacker == lastAttacker->id && lastDefender && data.defender == lastDefender->id ) - // continue; //lazy way of preventing duplicates - if ( attack && a+1 < reports.size() && reports[a+1]->type == df::announcement_type::INTERACTION_TARGET ) { -//out.print("%s,%d\n",__FILE__,__LINE__); - vector relevants = gatherRelevantUnits(out, lastAttackEvent, reports[a+1]); - InteractionData data2 = getAttacker(out, lastAttackEvent, lastAttacker, reports[a+1], relevants); - if ( data.attacker == data2.attacker && (data.defender == -1 || data.defender == data2.defender) ) { -//out.print("%s,%d\n",__FILE__,__LINE__); - data = data2; - a++; - } - } - { -#define HISTORY_ITEM 1 -#if HISTORY_ITEM - unordered_set& b = history[data.attacker]; - if ( b.find(data.defender) != b.end() ) - continue; - history[data.attacker].insert(data.defender); - //b.insert(data.defender); -#else - unordered_set& b = history[data.attackReport]; - if ( b.find(data.defendReport) != b.end() ) - continue; - history[data.attackReport].insert(data.defendReport); - //b.insert(data.defendReport); -#endif - } -//out.print("%s,%d\n",__FILE__,__LINE__); - lastAttacker = df::unit::find(data.attacker); - //lastDefender = df::unit::find(data.defender); - //fire event - for ( auto b = copy.begin(); b != copy.end(); b++ ) { - EventHandler handle = (*b).second; - handle.eventHandler(out, (void*)&data); - } - //TODO: deduce attacker from latest defend event first - } -} diff --git a/plugins/devel/CMakeLists.txt b/plugins/devel/CMakeLists.txt index 6d15f09dd9..814a9c0ead 100644 --- a/plugins/devel/CMakeLists.txt +++ b/plugins/devel/CMakeLists.txt @@ -22,6 +22,7 @@ dfhack_plugin(stockcheck stockcheck.cpp) dfhack_plugin(stripcaged stripcaged.cpp) dfhack_plugin(tilesieve tilesieve.cpp) dfhack_plugin(zoom zoom.cpp) +dfhack_plugin(event-testing event-testing.cpp) if(UNIX) dfhack_plugin(ref-index ref-index.cpp) diff --git a/plugins/devel/event-testing.cpp b/plugins/devel/event-testing.cpp new file mode 100644 index 0000000000..b1a6ade482 --- /dev/null +++ b/plugins/devel/event-testing.cpp @@ -0,0 +1,164 @@ +#include "Core.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//#include "df/world.h" + +using namespace DFHack; +using namespace df::enums; + +DFHACK_PLUGIN("event-testing"); +DFHACK_PLUGIN_IS_ENABLED(enabled); +//REQUIRE_GLOBAL(world); + +void onTick(color_ostream& out, void* tick); +void onJob(color_ostream &out, void* job); +void onConstruction(color_ostream &out, void* construction); +void onSyndrome(color_ostream &out, void* syndrome); +void onDeath(color_ostream &out, void* unit_id); +void onNewActive(color_ostream &out, void* unit_id); +void onItem(color_ostream &out, void* inventory_change_data); +void onAttack(color_ostream &out, void* attack_data); + +command_result skeleton2 (color_ostream &out, std::vector & parameters); + +DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { + commands.push_back(PluginCommand("event-testing", + "~54 character description of plugin", //to use one line in the ``[DFHack]# ls`` output + skeleton2, + false, + "example usage")); + return CR_OK; +} + +command_result skeleton2 (color_ostream &out, std::vector & parameters) { + if (!parameters.empty()) + return CR_WRONG_USAGE; + CoreSuspender suspend; + out.print("blah"); + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + namespace EM = EventManager; + EM::unregisterAll(plugin_self); + return CR_OK; +} +const int32_t interval = 500; // 5 dwarf-hours + +void enable_job_events() { + namespace EM = EventManager; + using namespace EM::EventType; + EM::EventHandler e1(onJob, 0); // constantly + EM::EventHandler e2(onJob, interval); // every interval + EM::registerListener(EventType::JOB_INITIATED, e1, plugin_self); + EM::registerListener(EventType::JOB_INITIATED, e2, plugin_self); + EM::EventHandler e3(onJob, 0); // constantly + EM::EventHandler e4(onJob, interval); // every interval + EM::registerListener(EventType::JOB_STARTED, e3, plugin_self); + EM::registerListener(EventType::JOB_STARTED, e4, plugin_self); + EM::EventHandler e5(onJob, 0); // constantly + EM::EventHandler e6(onJob, interval); // every interval + EM::registerListener(EventType::JOB_COMPLETED, e5, plugin_self); + EM::registerListener(EventType::JOB_COMPLETED, e6, plugin_self); +} +void enable_construction_events() { + namespace EM = EventManager; + using namespace EM::EventType; + EM::EventHandler e1(onConstruction, 0); // constantly + EM::EventHandler e2(onConstruction, interval); // every interval + EM::registerListener(EventType::CONSTRUCTION_REMOVED, e1, plugin_self); + EM::registerListener(EventType::CONSTRUCTION_REMOVED, e2, plugin_self); +} + +void enable_unit_events() { + namespace EM = EventManager; + using namespace EM::EventType; + EM::EventHandler e1(onSyndrome, 0); // constantly + EM::EventHandler e2(onDeath, 0); // constantly + EM::EventHandler e3(onNewActive, 0); // constantly + EM::registerListener(EventType::SYNDROME, e1, plugin_self); + EM::registerListener(EventType::UNIT_DEATH, e2, plugin_self); + EM::registerListener(EventType::UNIT_NEW_ACTIVE, e3, plugin_self); +} + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + namespace EM = EventManager; + if (enable && !enabled) { + //enable_job_events(); + //enable_construction_events(); + //enable_unit_events(); + namespace EM = EventManager; + using namespace EM::EventType; +// EM::EventHandler e1(onItem, 0); +// EM::EventHandler e2(onItem, interval); +// EM::registerListener(EventType::INVENTORY_CHANGE, e1, plugin_self); +// EM::registerListener(EventType::INVENTORY_CHANGE, e2, plugin_self); + EM::EventHandler e3(onAttack, 0); + EM::EventHandler e4(onAttack, interval); + EM::registerListener(EventType::UNIT_ATTACK, e3, plugin_self); + EM::registerListener(EventType::UNIT_ATTACK, e4, plugin_self); + out.print("plugin enabled!\n"); + } else if (!enable && enabled) { + EM::unregisterAll(plugin_self); + out.print("plugin disabled!\n"); + } + enabled = enable; + return CR_OK; +} + +void onJob(color_ostream &out, void* job) { + auto j = (df::job*)job; + std::string type = ENUM_KEY_STR(job_type, j->job_type); + out.print("onJob: (id: %d) (type: %s) (expire: %d) (completion: %d) (wait: %d)\n", j->id, type.c_str(), j->expire_timer, j->completion_timer, j->wait_timer); +} + +void onConstruction(color_ostream &out, void* construction) { + auto c = (df::construction*)construction; + std::string type = ENUM_KEY_STR(item_type, c->item_type); + out.print("onConstruction: (type: %s)\n", type.c_str()); +} + +void onSyndrome(color_ostream &out, void* syndrome) { + using EventManager::SyndromeData; + auto s = (SyndromeData*)syndrome; + out.print("onSyndrome: (unit: %d) (syndrome: %d)\n", s->unitId, s->syndromeIndex); +} + +void onDeath(color_ostream &out, void* unit_id){ + auto id = (int32_t)(intptr_t)unit_id; + out.print("onDeath: (unit: %d)\n", id); +} + +void onNewActive(color_ostream &out, void* unit_id){ + auto id = (int32_t)(intptr_t)unit_id; + out.print("onNewActive: (unit: %d)\n", id); +} + +void onItem(color_ostream &out, void* inventory_change_data) { + namespace EM = EventManager; + auto data = (EM::InventoryChangeData*)inventory_change_data; + int32_t new_id = data->item_new ? data->item_new->itemId : -1; + int32_t old_id = data->item_old ? data->item_old->itemId : -1; + out.print("onItem: (old id: %d) (new id: %d)\n", old_id, new_id); +} + +void onAttack(color_ostream &out, void* attack_data) { + auto* data = (EventManager::UnitAttackData*) attack_data; + static std::unordered_set seen; + out.print(" onAttack: (report id: %d) (attacker: %d) (defender: %d) (wound: %d)\n", + data->report_id, data->attacker, data->defender, data->wound); + if (!seen.emplace(*data).second) { + out.print(" onAttack: duplicate event\n"); + } +} + + + diff --git a/plugins/eventful.cpp b/plugins/eventful.cpp index 980b2db9c6..b05f7543da 100644 --- a/plugins/eventful.cpp +++ b/plugins/eventful.cpp @@ -242,8 +242,18 @@ handler_t getManager(EventType t) { return ev_mng_unitDeath; case ITEM_CREATED: return ev_mng_itemCreate; + //todo: implement these new events + case BUILDING_DESTROYED: + case BUILDING_CREATED: + return nullptr; + //todo: deprecate this event case BUILDING: return ev_mng_building; + //todo: implement these new events + case CONSTRUCTION_REMOVED: + case CONSTRUCTION_ADDED: + return nullptr; + //todo: deprecate this event case CONSTRUCTION: return ev_mng_construction; case SYNDROME: @@ -262,7 +272,7 @@ handler_t getManager(EventType t) { return ev_mng_interaction; case EVENT_MAX: return nullptr; - //default: + //default: //we don't do this... because then the compiler wouldn't error for missing cases in the enum } return nullptr;