diff --git a/docs/changelog.txt b/docs/changelog.txt index f819587de..315f37841 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,11 +58,14 @@ Template for new versions: ## Fixes - `autolabor`: now unconditionally re-enables vanilla work details when the fort or the plugin is unloaded - ``dfhack.TranslateName()``: fixed crash on certain invalid names, which affected `warn-starving` +- EventManager: Unit death event no longer misfires on units leaving the map ## Misc Improvements - `dig`: `digtype` command now has options to choose between designating only visible tiles or hidden tiles, as well as "auto" dig mode. Z-level options adjusted to allow choosing z-levels above, below, or the same as the cursor. - `hotkeys`: make the DFHack logo brighten on hover in ascii mode to indicate that it is clickable - `hotkeys`: use vertical bars instead of "!" symbols for the DFHack logo to make it easier to read +- EventManager: guarded against potential iterator invalidation if one of the event listeners modified the global data structure being iterated over +- EventManager: changed firing order of building created and building destroyed events to improve performance in the building location cache. ## Documentation diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index 02b1892e9..14fac42b3 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -43,6 +43,7 @@ #include #include #include +#include namespace DFHack { DBG_DECLARE(eventmanager, log, DebugCategory::LINFO); @@ -246,6 +247,15 @@ static int32_t reportToRelevantUnitsTime = -1; //interaction static int32_t lastReportInteraction; +struct hash_pair { + template + size_t operator()(const std::pair& p) const { + auto h1 = std::hash{}(p.first); + auto h2 = std::hash{}(p.second); + return h1 ^ (h2 << 1); + } +}; + void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event event) { static bool doOnce = false; // const string eventNames[] = {"world loaded", "world unloaded", "map loaded", "map unloaded", "viewscreen changed", "core initialized", "begin unload", "paused", "unpaused"}; @@ -276,9 +286,9 @@ void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event gameLoaded = false; multimap copy(handlers[EventType::UNLOAD].begin(), handlers[EventType::UNLOAD].end()); - for (auto &key_value : copy) { + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for map unloaded state change event\n"); - key_value.second.eventHandler(out, nullptr); + handle.eventHandler(out, nullptr); } } else if ( event == DFHack::SC_MAP_LOADED ) { /* @@ -375,8 +385,7 @@ void DFHack::EventManager::manageEvents(color_ostream& out) { continue; int32_t eventFrequency = -100; if ( a != EventType::TICK ) - for (auto &key_value : handlers[a]) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : handlers[a]) { if (handle.freq < eventFrequency || eventFrequency == -100 ) eventFrequency = handle.freq; } @@ -439,8 +448,7 @@ static void manageJobInitiatedEvent(color_ostream& out) { continue; if ( link->item->id <= lastJobId ) continue; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for job initiated event\n"); handle.eventHandler(out, (void*)link->item); } @@ -455,17 +463,24 @@ static void manageJobStartedEvent(color_ostream& out) { static unordered_set startedJobs; + vector new_started_jobs; // iterate event handler callbacks multimap copy(handlers[EventType::JOB_STARTED].begin(), handlers[EventType::JOB_STARTED].end()); - for (df::job_list_link* link = df::global::world->jobs.list.next; link != nullptr; link = link->next) { - df::job* job = link->item; + + for (df::job_list_link* link = &df::global::world->jobs.list; link->next != nullptr; link = link->next) { + df::job* job = link->next->item; + int32_t j_id = job->id; if (job && Job::getWorker(job) && !startedJobs.count(job->id)) { startedJobs.emplace(job->id); - for (auto &key_value : copy) { - auto &handler = key_value.second; + for (auto &[_,handle] : copy) { // the jobs must have a worker to start DEBUG(log,out).print("calling handler for job started event\n"); - handler.eventHandler(out, job); + handle.eventHandler(out, job); + } + } + if (link->next == nullptr || link->next->item->id != j_id) { + if ( Once::doOnce("EventManager jobstarted job removed") ) { + out.print("%s,%d: job %u removed from jobs linked list\n", __FILE__, __LINE__, j_id); } } } @@ -487,11 +502,11 @@ static void manageJobCompletedEvent(color_ostream& out) { int32_t tick1 = df::global::world->frame_counter; multimap copy(handlers[EventType::JOB_COMPLETED].begin(), handlers[EventType::JOB_COMPLETED].end()); - map nowJobs; + unordered_map nowJobs; for ( df::job_list_link* link = &df::global::world->jobs.list; link != nullptr; link = link->next ) { if ( link->item == nullptr ) continue; - nowJobs[link->item->id] = link->item; + nowJobs.emplace(link->item->id, link->item); } #if 0 @@ -573,10 +588,9 @@ static void manageJobCompletedEvent(color_ostream& out) { continue; //still false positive if cancelled at EXACTLY the right time, but experiments show this doesn't happen - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for repeated job completed event\n"); - handle.eventHandler(out, (void*)&job0); + handle.eventHandler(out, (void*) &job0); } continue; } @@ -586,28 +600,27 @@ static void manageJobCompletedEvent(color_ostream& out) { if ( job0.flags.bits.repeat || job0.completion_timer != 0 ) continue; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for job completed event\n"); - handle.eventHandler(out, (void*)&job0); + handle.eventHandler(out, (void*) &job0); } } //erase old jobs, copy over possibly altered jobs - for (auto &prevJob : prevJobs) { - Job::deleteJobStruct(prevJob.second, true); + for (auto &[_,prev_job] : prevJobs) { + Job::deleteJobStruct(prev_job, true); } prevJobs.clear(); //create new jobs - for (auto &nowJob : nowJobs) { + for (auto &[_,now_job] : nowJobs) { /*map::iterator i = prevJobs.find((*j).first); if ( i != prevJobs.end() ) { continue; }*/ - df::job* newJob = Job::cloneJobStruct(nowJob.second, true); - prevJobs[newJob->id] = newJob; + df::job* newJob = Job::cloneJobStruct(now_job, true); + prevJobs.emplace(newJob->id, newJob); } } @@ -617,15 +630,17 @@ static void manageNewUnitActiveEvent(color_ostream& out) { 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)) { - activeUnits.emplace(id); - DEBUG(log,out).print("calling handler for new unit event\n"); - handler.eventHandler(out, (void*) intptr_t(id)); // intptr_t() avoids cast from smaller type warning - } + vector new_active_unit_ids; + for (df::unit* unit : df::global::world->units.active) { + if (!activeUnits.count(unit->id)) { + activeUnits.emplace(unit->id); + new_active_unit_ids.emplace_back(unit->id); + } + } + for (int32_t unit_id : new_active_unit_ids) { + for (auto &[_,handle] : copy) { + DEBUG(log,out).print("calling handler for new unit event\n"); + handle.eventHandler(out, (void*) intptr_t(unit_id)); // intptr_t() avoids cast from smaller type warning } } } @@ -635,22 +650,26 @@ static void manageUnitDeathEvent(color_ostream& out) { if (!df::global::world) return; multimap copy(handlers[EventType::UNIT_DEATH].begin(), handlers[EventType::UNIT_DEATH].end()); + vector dead_unit_ids; for (auto 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)) continue; // for units that have left the map but aren't dead //dead: if dead since last check, trigger events if ( livingUnits.find(unit->id) == livingUnits.end() ) continue; + livingUnits.erase(unit->id); + dead_unit_ids.emplace_back(unit->id); + } - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (int32_t unit_id : dead_unit_ids) { + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for unit death event\n"); - handle.eventHandler(out, (void*)intptr_t(unit->id)); + handle.eventHandler(out, (void*)intptr_t(unit_id)); } - livingUnits.erase(unit->id); } } @@ -666,6 +685,8 @@ static void manageItemCreationEvent(color_ostream& out) { multimap copy(handlers[EventType::ITEM_CREATED].begin(), handlers[EventType::ITEM_CREATED].end()); size_t index = df::item::binsearch_index(df::global::world->items.all, nextItem, false); if ( index != 0 ) index--; + + std::vector created_items; for ( size_t a = index; a < df::global::world->items.all.size(); a++ ) { df::item* item = df::global::world->items.all[a]; //already processed @@ -683,12 +704,17 @@ static void manageItemCreationEvent(color_ostream& out) { //spider webs don't count if ( item->flags.bits.spider_web ) continue; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + created_items.push_back(item->id); + } + + // handle all created items + for (int32_t item_id : created_items) { + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for item created event\n"); - handle.eventHandler(out, (void*)intptr_t(item->id)); + handle.eventHandler(out, (void*)intptr_t(item_id)); } } + nextItem = *df::global::item_next_id; } @@ -703,6 +729,7 @@ static void manageBuildingEvent(color_ostream& out) { **/ multimap copy(handlers[EventType::BUILDING].begin(), handlers[EventType::BUILDING].end()); //first alert people about new buildings + vector 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 ) { @@ -711,30 +738,34 @@ static void manageBuildingEvent(color_ostream& out) { continue; } buildings.insert(a); - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; - DEBUG(log,out).print("calling handler for created building event\n"); - handle.eventHandler(out, (void*)intptr_t(a)); - } + new_buildings.emplace_back(a); + } nextBuilding = *df::global::building_next_id; //now alert people about destroyed buildings - for ( auto a = buildings.begin(); a != buildings.end(); ) { - int32_t id = *a; + for ( auto it = buildings.begin(); it != buildings.end(); ) { + int32_t id = *it; int32_t index = df::building::binsearch_index(df::global::world->buildings.all,id); if ( index != -1 ) { - a++; + ++it; continue; } - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for destroyed building event\n"); handle.eventHandler(out, (void*)intptr_t(id)); } - a = buildings.erase(a); + it = buildings.erase(it); } + + //alert people about newly created buildings + std::for_each(new_buildings.begin(), new_buildings.end(), [&](int32_t building){ + for (auto &[_,handle] : copy) { + DEBUG(log,out).print("calling handler for created building event\n"); + handle.eventHandler(out, (void*)intptr_t(building)); + } + }); } static void manageConstructionEvent(color_ostream& out) { @@ -743,35 +774,41 @@ static void manageConstructionEvent(color_ostream& out) { //unordered_set constructionsNow(df::global::world->constructions.begin(), df::global::world->constructions.end()); multimap copy(handlers[EventType::CONSTRUCTION].begin(), handlers[EventType::CONSTRUCTION].end()); - // find & send construction removals - for (auto iter = constructions.begin(); iter != constructions.end();) { - auto &construction = *iter; - // if we can't find it, it was removed - if (df::construction::find(construction.pos) != nullptr) { - ++iter; - continue; + + unordered_set next_construction_set; // will be swapped with constructions + next_construction_set.reserve(constructions.bucket_count()); + vector new_constructions; + + // find new constructions - swapping found constructions over from constructions to next_construction_set + for (auto c : df::global::world->constructions) { + auto &construction = *c; + auto it = constructions.find(construction); + if (it == constructions.end()) { + // handle new construction event later + new_constructions.emplace_back(construction); + } + else { + constructions.erase(it); } - // send construction to handlers, because it was removed - for (const auto &key_value: copy) { - EventHandler handle = key_value.second; + next_construction_set.emplace(construction); + } + + constructions.swap(next_construction_set); + + // now next_construction_set contains all the constructions that were removed (not found in df::global::world->constructions) + for (auto& construction : next_construction_set) { + // handle construction removed event + for (const auto &[_,handle]: copy) { DEBUG(log,out).print("calling handler for destroyed construction event\n"); handle.eventHandler(out, (void*) &construction); } - // erase from existent constructions - iter = constructions.erase(iter); } - // find & send construction additions - for (auto c: df::global::world->constructions) { - auto &construction = *c; - // add construction to constructions, if it isn't already present - if (constructions.emplace(construction).second) { - // send construction to handlers, because it is new - for (const auto &key_value: copy) { - EventHandler handle = key_value.second; - DEBUG(log,out).print("calling handler for created construction event\n"); - handle.eventHandler(out, (void*) &construction); - } + // now handle all the new constructions + for (auto& construction : new_constructions) { + for (const auto &[_,handle]: copy) { + DEBUG(log,out).print("calling handler for created construction event\n"); + handle.eventHandler(out, (void*) &construction); } } } @@ -781,6 +818,8 @@ static void manageSyndromeEvent(color_ostream& out) { return; multimap copy(handlers[EventType::SYNDROME].begin(), handlers[EventType::SYNDROME].end()); int32_t highestTime = -1; + + std::vector new_syndrome_data; for (auto unit : df::global::world->units.all) { /* @@ -795,14 +834,16 @@ static void manageSyndromeEvent(color_ostream& out) { if ( startTime <= lastSyndromeTime ) continue; - SyndromeData data(unit->id, b); - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; - DEBUG(log,out).print("calling handler for syndrome event\n"); - handle.eventHandler(out, (void*)&data); - } + new_syndrome_data.emplace_back(unit->id, b); } } + for (auto& data : new_syndrome_data) { + for (auto &[_,handle] : copy) { + DEBUG(log,out).print("calling handler for syndrome event\n"); + handle.eventHandler(out, (void*)&data); + } + } + lastSyndromeTime = highestTime; } @@ -815,8 +856,7 @@ static void manageInvasionEvent(color_ostream& out) { return; nextInvasion = df::global::plotinfo->invasions.next_id; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for invasion event\n"); handle.eventHandler(out, (void*)intptr_t(nextInvasion-1)); } @@ -829,6 +869,18 @@ static void manageEquipmentEvent(color_ostream& out) { unordered_map itemIdToInventoryItem; unordered_set currentlyEquipped; + + vector equipment_pickups; + vector equipment_drops; + vector equipment_changes; + + + // This vector stores the pointers to newly created changed items + // needed as the stack allocated temporary (in the loop) is lost when we go to + // handle the event calls, so we move that data to the heap if its needed, + // and then once we are done we delete everything. + vector changed_items; + for (auto unit : df::global::world->units.all) { itemIdToInventoryItem.clear(); currentlyEquipped.clear(); @@ -856,40 +908,30 @@ static void manageEquipmentEvent(color_ostream& out) { auto c = itemIdToInventoryItem.find(dfitem_new->item->id); if ( c == itemIdToInventoryItem.end() ) { //new item equipped (probably just picked up) - InventoryChangeData data(unit->id, nullptr, &item_new); - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; - DEBUG(log,out).print("calling handler for new item equipped inventory change event\n"); - handle.eventHandler(out, (void*)&data); - } + changed_items.emplace_back(new InventoryItem(item_new)); + equipment_pickups.emplace_back(unit->id, nullptr, changed_items.back()); continue; } - InventoryItem item_old = (*c).second; + 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 &key_value : copy) { - EventHandler &handle = key_value.second; - DEBUG(log,out).print("calling handler for inventory change event\n"); - handle.eventHandler(out, (void*)&data); - } + changed_items.emplace_back(new InventoryItem(item_new)); + InventoryItem* item_new_ptr = changed_items.back(); + changed_items.emplace_back(new InventoryItem(item_old)); + InventoryItem* item_old_ptr = changed_items.back(); + equipment_changes.emplace_back(unit->id, item_old_ptr, item_new_ptr); } //check for dropped items for (auto i : v) { if ( currentlyEquipped.find(i.itemId) != currentlyEquipped.end() ) continue; //TODO: delete ptr if invalid - InventoryChangeData data(unit->id, &i, nullptr); - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; - DEBUG(log,out).print("calling handler for dropped item inventory change event\n"); - handle.eventHandler(out, (void*)&data); - } + changed_items.emplace_back(new InventoryItem(i)); + equipment_drops.emplace_back(unit->id, changed_items.back(), nullptr); } if ( !hadEquipment ) delete temp; @@ -902,6 +944,31 @@ static void manageEquipmentEvent(color_ostream& out) { equipment.push_back(item); } } + + // now handle events + std::for_each(equipment_pickups.begin(), equipment_pickups.end(), [&](InventoryChangeData& data) { + for (auto &[_, handle] : copy) { + DEBUG(log,out).print("calling handler for new item equipped inventory change event\n"); + handle.eventHandler(out, (void*) &data); + } + }); + std::for_each(equipment_drops.begin(), equipment_drops.end(), [&](InventoryChangeData& data) { + for (auto &[_, handle] : copy) { + DEBUG(log,out).print("calling handler for dropped item inventory change event\n"); + handle.eventHandler(out, (void*) &data); + } + }); + std::for_each(equipment_changes.begin(), equipment_changes.end(), [&](InventoryChangeData& data) { + for (auto &[_, handle] : copy) { + DEBUG(log,out).print("calling handler for inventory change event\n"); + handle.eventHandler(out, (void*) &data); + } + }); + + // clean up changed items list + std::for_each(changed_items.begin(), changed_items.end(), [](InventoryItem* p){ + delete p; + }); } static void updateReportToRelevantUnits() { @@ -939,8 +1006,7 @@ static void manageReportEvent(color_ostream& out) { for ( ; idx < reports.size(); idx++ ) { df::report* report = reports[idx]; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for report event\n"); handle.eventHandler(out, (void*)intptr_t(report->id)); } @@ -981,7 +1047,7 @@ static void manageUnitAttackEvent(color_ostream& out) { if ( strikeReports.empty() ) return; updateReportToRelevantUnits(); - map > alreadyDone; + unordered_set, hash_pair> already_done; for (int reportId : strikeReports) { df::report* report = df::report::find(reportId); if ( !report ) @@ -1011,27 +1077,25 @@ static void manageUnitAttackEvent(color_ostream& out) { UnitAttackData data{}; data.report_id = report->id; - if ( wound1 && !alreadyDone[unit1->id][unit2->id] ) { + if ( wound1 && already_done.find(std::make_pair(unit1->id,unit2->id)) == already_done.end() ) { data.attacker = unit1->id; data.defender = unit2->id; data.wound = wound1->id; - alreadyDone[data.attacker][data.defender] = 1; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + already_done.emplace(unit1->id, unit2->id); + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for unit1 attack unit attack event\n"); handle.eventHandler(out, (void*)&data); } } - if ( wound2 && !alreadyDone[unit1->id][unit2->id] ) { + if ( wound2 && already_done.find(std::make_pair(unit1->id,unit2->id)) == already_done.end() ) { data.attacker = unit2->id; data.defender = unit1->id; data.wound = wound2->id; - alreadyDone[data.attacker][data.defender] = 1; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + already_done.emplace(unit1->id, unit2->id); + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for unit2 attack unit attack event\n"); handle.eventHandler(out, (void*)&data); } @@ -1041,9 +1105,9 @@ static void manageUnitAttackEvent(color_ostream& out) { data.attacker = unit2->id; data.defender = unit1->id; data.wound = -1; - alreadyDone[data.attacker][data.defender] = 1; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + + already_done.emplace(unit1->id, unit2->id); + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for unit1 killed unit attack event\n"); handle.eventHandler(out, (void*)&data); } @@ -1053,9 +1117,9 @@ static void manageUnitAttackEvent(color_ostream& out) { data.attacker = unit1->id; data.defender = unit2->id; data.wound = -1; - alreadyDone[data.attacker][data.defender] = 1; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + + already_done.emplace(unit1->id, unit2->id); + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for unit2 killed unit attack event\n"); handle.eventHandler(out, (void*)&data); } @@ -1313,8 +1377,7 @@ static void manageInteractionEvent(color_ostream& out) { lastAttacker = df::unit::find(data.attacker); //lastDefender = df::unit::find(data.defender); //fire event - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for interaction event\n"); handle.eventHandler(out, (void*)&data); }