#include "assignJob.h" #include "edgeCost.h" #include "Core.h" #include "Console.h" #include "DataDefs.h" #include "Export.h" #include "PluginManager.h" #include "Types.h" #include "modules/Buildings.h" #include "modules/EventManager.h" #include "modules/Gui.h" #include "modules/Job.h" #include "modules/Maps.h" #include "modules/MapCache.h" #include "modules/Units.h" #include "modules/World.h" #include "df/body_part_raw_flags.h" #include "df/building.h" #include "df/building_type.h" #include "df/caste_body_info.h" #include "df/coord.h" #include "df/creature_raw.h" #include "df/general_ref.h" #include "df/general_ref_building_holderst.h" #include "df/general_ref_unit.h" #include "df/general_ref_unit_holderst.h" #include "df/general_ref_unit_workerst.h" #include "df/global_objects.h" #include "df/invasion_info.h" #include "df/item.h" #include "df/itemdef_weaponst.h" #include "df/item_quality.h" #include "df/item_weaponst.h" #include "df/inorganic_raw.h" #include "df/job.h" #include "df/job_list_link.h" #include "df/job_skill.h" #include "df/job_type.h" #include "df/map_block.h" #include "df/strain_type.h" #include "df/tile_building_occ.h" #include "df/tile_occupancy.h" #include "df/tiletype.h" #include "df/tiletype_material.h" #include "df/tiletype_shape.h" #include "df/tiletype_shape_basic.h" #include "df/ui.h" #include "df/unit.h" #include "df/unit_inventory_item.h" #include "df/world.h" #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace DFHack; using namespace df::enums; command_result diggingInvadersCommand(color_ostream &out, std::vector & parameters); void watchForJobComplete(color_ostream& out, void* ptr); void newInvasionHandler(color_ostream& out, void* ptr); void clearDijkstra(); void findAndAssignInvasionJob(color_ostream& out, void*); //int32_t manageInvasion(color_ostream& out); DFHACK_PLUGIN_IS_ENABLED(enabled); DFHACK_PLUGIN("diggingInvaders"); REQUIRE_GLOBAL(world); //TODO: when world unloads static int32_t lastInvasionJob=-1; static int32_t lastInvasionDigger = -1; static int32_t edgesPerTick = 100; //static EventManager::EventHandler jobCompleteHandler(watchForJobComplete, 5); static bool activeDigging=false; static unordered_set diggingRaces; static unordered_set invaderJobs; static df::coord lastDebugEdgeCostPoint; unordered_map digAbilities; static cost_t costWeightDefault[] = { //Distance 1, //Destroy Building 2, //Dig 10000, //DestroyRoughConstruction 1000, //DestroySmoothConstruction 100, }; static int32_t jobDelayDefault[] = { //Distance -1, //Destroy Building 1000, //Dig 1000, //DestroyRoughConstruction 1000, //DestroySmoothConstruction 100, }; DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { commands.push_back(PluginCommand( "diggingInvaders", "Makes invaders dig to your dwarves.", diggingInvadersCommand, false, /* true means that the command can't be used from non-interactive user interface */ " diggingInvaders 0\n disables the plugin\n" " diggingInvaders 1\n enables the plugin\n" " diggingInvaders enable\n enables the plugin\n" " diggingInvaders disable\n disables the plugin\n" " diggingInvaders add GOBLIN\n registers the race GOBLIN as a digging invader. Case-sensitive.\n" " diggingInvaders remove GOBLIN\n unregisters the race GOBLIN as a digging invader. Case-sensitive.\n" " diggingInvaders setCost GOBLIN walk n\n sets the walk cost in the path algorithm for the race GOBLIN\n" " diggingInvaders setCost GOBLIN destroyBuilding n\n" " diggingInvaders setCost GOBLIN dig n\n" " diggingInvaders setCost GOBLIN destroyRoughConstruction n\n rough constructions are made from boulders\n" " diggingInvaders setCost GOBLIN destroySmoothConstruction n\n smooth constructions are made from blocks or bars instead of boulders\n" " diggingInvaders setDelay GOBLIN destroyBuilding n\n adds to the job_completion_timer of destroy building jobs that are assigned to invaders\n" " diggingInvaders setDelay GOBLIN dig n\n" " diggingInvaders setDelay GOBLIN destroyRoughConstruction n\n" " diggingInvaders setDelay GOBLIN destroySmoothConstruction n\n" " diggingInvaders now\n makes invaders try to dig now, if plugin is enabled\n" " diggingInvaders clear\n clears all digging invader races\n" " diggingInvaders edgesPerTick n\n makes the pathfinding algorithm work on at most n edges per tick. Set to 0 or lower to make it unlimited." // " diggingInvaders\n Makes invaders try to dig now.\n" )); //*df::global::debug_showambush = true; return CR_OK; } DFhackCExport command_result plugin_shutdown ( color_ostream &out ) { return CR_OK; } DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { if ( enabled == enable ) return CR_OK; enabled = enable; EventManager::unregisterAll(plugin_self); clearDijkstra(); lastInvasionJob = lastInvasionDigger = -1; activeDigging = false; invaderJobs.clear(); if ( enabled ) { EventManager::EventHandler handler(newInvasionHandler, 1000); EventManager::registerListener(EventManager::EventType::INVASION, handler, plugin_self); findAndAssignInvasionJob(out, (void*)0); } return CR_OK; } DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { switch (event) { case DFHack::SC_WORLD_LOADED: //TODO: check game mode //in case there are invaders when the game is loaded, we check if there's work to be done activeDigging = enabled; clearDijkstra(); findAndAssignInvasionJob(out, (void*)0); break; case DFHack::SC_WORLD_UNLOADED: // cleanup plugin_enable(out, false); break; default: break; } return CR_OK; } df::coord getRoot(df::coord point, unordered_map& rootMap); class PointComp { public: unordered_map *pointCost; PointComp(unordered_map *p): pointCost(p) { } int32_t operator()(df::coord p1, df::coord p2) const { if ( p1 == p2 ) return 0; auto i1 = pointCost->find(p1); auto i2 = pointCost->find(p2); if ( i1 == pointCost->end() && i2 == pointCost->end() ) return p1 < p2; if ( i1 == pointCost->end() ) return true; if ( i2 == pointCost->end() ) return false; cost_t c1 = (*i1).second; cost_t c2 = (*i2).second; if ( c1 != c2 ) return c1 < c2; return p1 < p2; } }; //bool important(df::coord pos, map >& edges, df::coord prev, set& importantPoints, set& importantEdges); void newInvasionHandler(color_ostream& out, void* ptr) { if ( activeDigging ) return; activeDigging = true; findAndAssignInvasionJob(out, (void*)0); } command_result diggingInvadersCommand(color_ostream& out, std::vector& parameters) { for ( size_t a = 0; a < parameters.size(); a++ ) { if ( parameters[a] == "1" || parameters[a] == "enable" ) { plugin_enable(out,true); } else if ( parameters[a] == "0" || parameters[a] == "disable" ) { plugin_enable(out,false); } else if ( parameters[a] == "add" || parameters[a] == "remove" ) { if ( a+1 >= parameters.size() ) return CR_WRONG_USAGE; string race = parameters[a+1]; if ( parameters[a] == "add" ) { diggingRaces.insert(race); DigAbilities& abilities = digAbilities[race]; memcpy(abilities.costWeight, costWeightDefault, costDim*sizeof(cost_t)); memcpy(abilities.jobDelay, jobDelayDefault, costDim*sizeof(int32_t)); } else { diggingRaces.erase(race); digAbilities.erase(race); } a++; } else if ( parameters[a] == "setCost" || parameters[a] == "setDelay" ) { if ( a+3 >= parameters.size() ) return CR_WRONG_USAGE; string raceString = parameters[a+1]; if ( digAbilities.find(raceString) == digAbilities.end() ) { DigAbilities bob; memset(&bob, 0xFF, sizeof(bob)); digAbilities[raceString] = bob; } DigAbilities& abilities = digAbilities[raceString]; string costStr = parameters[a+2]; int32_t costDim = -1; if ( costStr == "walk" ) { costDim = CostDimension::Walk; if ( parameters[a] == "setDelay" ) return CR_WRONG_USAGE; } else if ( costStr == "destroyBuilding" ) { costDim = CostDimension::DestroyBuilding; } else if ( costStr == "dig" ) { costDim = CostDimension::Dig; } else if ( costStr == "destroyRoughConstruction" ) { costDim = CostDimension::DestroyRoughConstruction; } else if ( costStr == "destroySmoothConstruction" ) { costDim = CostDimension::DestroySmoothConstruction; } else { return CR_WRONG_USAGE; } cost_t value; stringstream asdf(parameters[a+3]); asdf >> value; //if ( parameters[a] == "setCost" && value <= 0 ) // return CR_WRONG_USAGE; if ( parameters[a] == "setCost" ) { abilities.costWeight[costDim] = value; } else { abilities.jobDelay[costDim] = value; } a += 3; } else if ( parameters[a] == "edgeCost" ) { if ( a+1 >= parameters.size() ) return CR_WRONG_USAGE; string raceString = parameters[a+1]; if ( digAbilities.find(raceString) == digAbilities.end() ) { out.print("Race %s does not have dig abilities assigned.\n", raceString.c_str()); return CR_WRONG_USAGE; } DigAbilities& abilities = digAbilities[raceString]; df::coord bob = Gui::getCursorPos(); out.print("(%d,%d,%d), (%d,%d,%d): cost = %lld\n", lastDebugEdgeCostPoint.x, lastDebugEdgeCostPoint.y, lastDebugEdgeCostPoint.z, bob.x, bob.y, bob.z, getEdgeCost(out, lastDebugEdgeCostPoint, bob, abilities)); lastDebugEdgeCostPoint = bob; a++; } else if ( parameters[a] == "now" ) { activeDigging = true; findAndAssignInvasionJob(out, (void*)0); } else if ( parameters[a] == "clear" ) { diggingRaces.clear(); digAbilities.clear(); } else if ( parameters[a] == "edgesPerTick" ) { if ( a+1 >= parameters.size() ) return CR_WRONG_USAGE; stringstream asdf(parameters[a+1]); int32_t edgeCount = 100; asdf >> edgeCount; edgesPerTick = edgeCount; a++; } else { return CR_WRONG_USAGE; } } activeDigging = enabled; out.print("diggingInvaders: enabled = %d, activeDigging = %d, edgesPerTick = %d\n", enabled, activeDigging, edgesPerTick); return CR_OK; } ///////////////////////////////////////////////////////////////////////////////////////// //dijkstra globals vector invaders; unordered_set invaderPts; unordered_set localPts; unordered_map parentMap; unordered_map costMap; PointComp comp(&costMap); set fringe(comp); EventManager::EventHandler findJobTickHandler(findAndAssignInvasionJob, 1); int32_t localPtsFound = 0; unordered_set closedSet; unordered_map workNeeded; //non-walking work needed to get there bool foundTarget = false; int32_t edgeCount = 0; void clearDijkstra() { invaders.clear(); invaderPts.clear(); localPts.clear(); parentMap.clear(); costMap.clear(); comp = PointComp(&costMap); fringe = set(comp); localPtsFound = edgeCount = 0; foundTarget = false; closedSet.clear(); workNeeded.clear(); } ///////////////////////////////////////////////////////////////////////////////////////// void findAndAssignInvasionJob(color_ostream& out, void* tickTime) { CoreSuspender suspend; //returns the worker id of the job created //used to //out.print("%s, %d: %d\n", __FILE__, __LINE__, (int32_t)tickTime); if ( !enabled || !activeDigging ) { clearDijkstra(); return; } EventManager::unregister(EventManager::EventType::TICK, findJobTickHandler, plugin_self); EventManager::registerTick(findJobTickHandler, 1, plugin_self); if ( fringe.empty() ) { df::unit* lastDigger = df::unit::find(lastInvasionDigger); if ( lastDigger && lastDigger->job.current_job && lastDigger->job.current_job->id == lastInvasionJob ) { return; } //out.print("%s,%d: lastDigger = %d, last job = %d, last digger's job = %d\n", __FILE__, __LINE__, lastInvasionDigger, lastInvasionJob, !lastDigger ? -1 : (!lastDigger->job.current_job ? -1 : lastDigger->job.current_job->id)); lastInvasionDigger = lastInvasionJob = -1; clearDijkstra(); unordered_set invaderConnectivity; unordered_set localConnectivity; //find all locals and invaders for ( size_t a = 0; a < world->units.all.size(); a++ ) { df::unit* unit = world->units.all[a]; if ( !Units::isActive(unit) ) continue; if ( Units::isCitizen(unit) ) { if ( localPts.find(unit->pos) != localPts.end() ) continue; localPts.insert(unit->pos); df::map_block* block = Maps::getTileBlock(unit->pos); localConnectivity.insert(block->walkable[unit->pos.x&0xF][unit->pos.y&0xF]); } else if ( unit->flags1.bits.active_invader ) { df::creature_raw* raw = df::creature_raw::find(unit->race); if ( raw == NULL ) { out.print("%s,%d: WTF? Couldn't find creature raw.\n", __FILE__, __LINE__); continue; } /* if ( diggingRaces.find(raw->creature_id) == diggingRaces.end() ) continue; */ if ( digAbilities.find(raw->creature_id) == digAbilities.end() ) continue; if ( invaderPts.find(unit->pos) != invaderPts.end() ) continue; //must be able to wield a pick: this is overly pessimistic if ( unit->status2.limbs_grasp_max <= 0 || unit->status2.limbs_grasp_count < unit->status2.limbs_grasp_max ) continue; df::map_block* block = Maps::getTileBlock(unit->pos); invaderConnectivity.insert(block->walkable[unit->pos.x&0xF][unit->pos.y&0xF]); if ( invaderPts.size() > 0 ) continue; invaderPts.insert(unit->pos); costMap[unit->pos] = 0; fringe.insert(unit->pos); invaders.push_back(unit->id); } else { continue; } } if ( invaders.empty() || localPts.empty() ) { activeDigging = false; return; } //if local connectivity is not disjoint from invader connectivity, no digging required bool overlap = false; for ( auto a = localConnectivity.begin(); a != localConnectivity.end(); a++ ) { uint16_t conn = *a; if ( invaderConnectivity.find(conn) == invaderConnectivity.end() ) continue; overlap = true; break; } if ( overlap ) { //still keep checking next frame: might kill a few outsiders then dig down return; } } df::unit* firstInvader = df::unit::find(invaders[0]); if ( firstInvader == NULL ) { fringe.clear(); return; } df::creature_raw* creature_raw = df::creature_raw::find(firstInvader->race); if ( creature_raw == NULL || digAbilities.find(creature_raw->creature_id) == digAbilities.end() ) { //inappropriate digger: no dig abilities fringe.clear(); return; } DigAbilities& abilities = digAbilities[creature_raw->creature_id]; //TODO: check that firstInvader is an appropriate digger //out << firstInvader->id << endl; //out << firstInvader->pos.x << ", " << firstInvader->pos.y << ", " << firstInvader->pos.z << endl; //out << __LINE__ << endl; uint32_t xMax, yMax, zMax; Maps::getSize(xMax,yMax,zMax); xMax *= 16; yMax *= 16; MapExtras::MapCache cache; clock_t t0 = clock(); clock_t totalEdgeTime = 0; int32_t edgesExpanded = 0; while(!fringe.empty()) { if ( edgesPerTick > 0 && edgesExpanded++ >= edgesPerTick ) { return; } df::coord pt = *(fringe.begin()); fringe.erase(fringe.begin()); //out.print("line %d: fringe size = %d, localPtsFound = %d / %d, closedSetSize = %d, pt = %d,%d,%d\n", __LINE__, fringe.size(), localPtsFound, localPts.size(), closedSet.size(), pt.x,pt.y,pt.z); if ( closedSet.find(pt) != closedSet.end() ) { out.print("%s, line %d: Double closure! Bad!\n", __FILE__, __LINE__); break; } closedSet.insert(pt); if ( localPts.find(pt) != localPts.end() ) { localPtsFound++; if ( true || localPtsFound >= localPts.size() ) { foundTarget = true; break; } if ( workNeeded.find(pt) == workNeeded.end() || workNeeded[pt] == 0 ) { //there are still dwarves to kill that don't require digging to get to return; } } cost_t myCost = costMap[pt]; clock_t edgeTime = clock(); vector* myEdges = getEdgeSet(out, pt, cache, xMax, yMax, zMax, abilities); totalEdgeTime += (clock() - edgeTime); for ( auto a = myEdges->begin(); a != myEdges->end(); a++ ) { Edge &e = *a; if ( e.p1 == df::coord() ) break; edgeCount++; df::coord& other = e.p1; if ( other == pt ) other = e.p2; //if ( closedSet.find(other) != closedSet.end() ) // continue; auto i = costMap.find(other); if ( i != costMap.end() ) { cost_t cost = (*i).second; if ( cost <= myCost + e.cost ) { continue; } fringe.erase((*i).first); } costMap[other] = myCost + e.cost; fringe.insert(other); parentMap[other] = pt; workNeeded[other] = (e.cost > 1 ? 1 : 0) + workNeeded[pt]; } delete myEdges; } clock_t time = clock() - t0; //out.print("tickTime = %d, time = %d, totalEdgeTime = %d, total points = %d, total edges = %d, time per point = %.3f, time per edge = %.3f, clocks/sec = %d\n", (int32_t)tickTime, time, totalEdgeTime, closedSet.size(), edgeCount, (float)time / closedSet.size(), (float)time / edgeCount, CLOCKS_PER_SEC); fringe.clear(); if ( !foundTarget ) return; unordered_set requiresZNeg; unordered_set requiresZPos; //find important edges Edge firstImportantEdge(df::coord(), df::coord(), -1); //df::coord closest; //cost_t closestCostEstimate=0; //cost_t closestCostActual=0; for ( auto i = localPts.begin(); i != localPts.end(); i++ ) { df::coord pt = *i; if ( costMap.find(pt) == costMap.end() ) continue; if ( parentMap.find(pt) == parentMap.end() ) continue; //closest = pt; //closestCostEstimate = costMap[closest]; //if ( workNeeded[pt] == 0 ) // continue; while ( parentMap.find(pt) != parentMap.end() ) { //out.print("(%d,%d,%d)\n", pt.x, pt.y, pt.z); df::coord parent = parentMap[pt]; cost_t cost = getEdgeCost(out, parent, pt, abilities); if ( cost < 0 ) { //path invalidated return; } //closestCostActual += cost; if ( Maps::canStepBetween(parent, pt) ) { } else { if ( pt.x == parent.x && pt.y == parent.y ) { if ( pt.z < parent.z ) { requiresZNeg.insert(parent); requiresZPos.insert(pt); } else if ( pt.z > parent.z ) { requiresZNeg.insert(pt); requiresZPos.insert(parent); } } //if ( workNeeded[pt] > workNeeded[parent] ) { //importantEdges.push_front(Edge(pt,parent,0)); //} firstImportantEdge = Edge(pt,parent,0); //out.print("(%d,%d,%d) -> (%d,%d,%d)\n", parent.x,parent.y,parent.z, pt.x,pt.y,pt.z); } pt = parent; } break; } if ( firstImportantEdge.p1 == df::coord() ) return; /* if ( closestCostActual != closestCostEstimate ) { out.print("%s,%d: closest = (%d,%d,%d), estimate = %lld != actual = %lld\n", __FILE__, __LINE__, closest.x,closest.y,closest.z, closestCostEstimate, closestCostActual); return; } */ assignJob(out, firstImportantEdge, parentMap, costMap, invaders, requiresZNeg, requiresZPos, cache, abilities); lastInvasionDigger = firstInvader->id; lastInvasionJob = firstInvader->job.current_job ? firstInvader->job.current_job->id : -1; invaderJobs.erase(lastInvasionJob); for ( df::job_list_link* link = &world->jobs.list; link != NULL; link = link->next ) { if ( link->item == NULL ) continue; df::job* job = link->item; if ( invaderJobs.find(job->id) == invaderJobs.end() ) { continue; } //cancel it job->flags.bits.item_lost = 1; out.print("%s,%d: cancelling job %d.\n", __FILE__,__LINE__, job->id); //invaderJobs.remove(job->id); } invaderJobs.erase(lastInvasionJob); return; } df::coord getRoot(df::coord point, map& rootMap) { map::iterator i = rootMap.find(point); if ( i == rootMap.end() ) { rootMap[point] = point; return point; } df::coord parent = (*i).second; if ( parent == point ) return parent; df::coord root = getRoot(parent, rootMap); rootMap[point] = root; return root; }