diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index ab7d6434e..ebd82ce19 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1998,6 +1998,11 @@ Low-level building creation functions: Destroys the building, or queues a deconstruction job. Returns *true* if the building was destroyed and deallocated immediately. +* ``dfhack.buildings.notifyCivzoneModified(building)`` + + Rebuilds the civzone <-> overlapping building association mapping. + Call after changing extents or modifying size in some fashion + * ``dfhack.buildings.markedForRemoval(building)`` Returns *true* if the building is marked for removal (with :kbd:`x`), *false* diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 9f3d590dd..84612f790 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2239,6 +2239,7 @@ static const LuaWrapper::FunctionReg dfhack_buildings_module[] = { WRAPM(Buildings, constructWithItems), WRAPM(Buildings, constructWithFilters), WRAPM(Buildings, deconstruct), + WRAPM(Buildings, notifyCivzoneModified), WRAPM(Buildings, markedForRemoval), WRAPM(Buildings, getRoomDescription), WRAPM(Buildings, isActivityZone), diff --git a/library/include/modules/Buildings.h b/library/include/modules/Buildings.h index aa539c02a..f4e8f37be 100644 --- a/library/include/modules/Buildings.h +++ b/library/include/modules/Buildings.h @@ -202,6 +202,11 @@ DFHACK_EXPORT bool deconstruct(df::building *bld); */ DFHACK_EXPORT bool markedForRemoval(df::building *bld); +/** + * Rebuilds a civzones building associations after it has been modified +*/ +DFHACK_EXPORT void notifyCivzoneModified(df::building* bld); + void updateBuildings(color_ostream& out, void* ptr); void clearBuildings(color_ostream& out); diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index 2e7143533..653f8bccf 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -102,7 +102,6 @@ struct CoordHash { }; static unordered_map locationToBuilding; -static unordered_map, CoordHash> locationToCivzones; static df::building_extents_type *getExtentTile(df::building_extents &extent, df::coord2d tile) { @@ -167,6 +166,163 @@ void buildings_onUpdate(color_ostream &out) } } +static void building_into_zone_unidir(df::building* bld, df::building_civzonest* zone) +{ + for (size_t bid = 0; bid < zone->contained_buildings.size(); bid++) + { + if (zone->contained_buildings[bid] == bld) + return; + } + + zone->contained_buildings.push_back(bld); + + std::sort(zone->contained_buildings.begin(), zone->contained_buildings.end(), [](df::building* b1, df::building* b2) + { + return b1->id < b2->id; + }); +} + +static void zone_into_building_unidir(df::building* bld, df::building_civzonest* zone) +{ + for (size_t bid = 0; bid < bld->relations.size(); bid++) + { + if (bld->relations[bid] == zone) + return; + } + + bld->relations.push_back(zone); + + std::sort(bld->relations.begin(), bld->relations.end(), [](df::building* b1, df::building* b2) + { + return b1->id < b2->id; + }); +} + +static bool is_suitable_building_for_zoning(df::building* bld) +{ + return bld->canMakeRoom(); +} + +static void add_building_to_zone(df::building* bld, df::building_civzonest* zone) +{ + if (!is_suitable_building_for_zoning(bld)) + return; + + building_into_zone_unidir(bld, zone); + zone_into_building_unidir(bld, zone); +} + +static void add_building_to_all_zones(df::building* bld) +{ + if (!is_suitable_building_for_zoning(bld)) + return; + + df::coord coord(bld->centerx, bld->centery, bld->z); + + std::vector cv; + Buildings::findCivzonesAt(&cv, coord); + + for (size_t i=0; i < cv.size(); i++) + { + add_building_to_zone(bld, cv[i]); + } +} + +static void add_zone_to_all_buildings(df::building* zone_as_building) +{ + if (zone_as_building->getType() != building_type::Civzone) + return; + + auto zone = strict_virtual_cast(zone_as_building); + + if (zone == nullptr) + return; + + auto& vec = world->buildings.other[buildings_other_id::IN_PLAY]; + + for (size_t i = 0; i < vec.size(); i++) + { + auto against = vec[i]; + + if (zone->z != against->z) + continue; + + if (!is_suitable_building_for_zoning(against)) + continue; + + int32_t cx = against->centerx; + int32_t cy = against->centery; + + df::coord2d coord(cx, cy); + + //can a zone without extents exist? + if (zone->room.extents && zone->isExtentShaped()) + { + auto etile = getExtentTile(zone->room, coord); + if (!etile || !*etile) + continue; + + add_building_to_zone(against, zone); + } + } +} + +static void remove_building_from_zone(df::building* bld, df::building_civzonest* zone) +{ + for (int bid = 0; bid < (int)zone->contained_buildings.size(); bid++) + { + if (zone->contained_buildings[bid] == bld) + { + zone->contained_buildings.erase(zone->contained_buildings.begin() + bid); + bid--; + } + } + + for (int bid = 0; bid < (int)bld->relations.size(); bid++) + { + if (bld->relations[bid] == zone) + { + bld->relations.erase(bld->relations.begin() + bid); + bid--; + } + } +} + +static void remove_building_from_all_zones(df::building* bld) +{ + df::coord coord(bld->centerx, bld->centery, bld->z); + + std::vector cv; + Buildings::findCivzonesAt(&cv, coord); + + for (size_t i=0; i < cv.size(); i++) + { + remove_building_from_zone(bld, cv[i]); + } +} + +static void remove_zone_from_all_buildings(df::building* zone_as_building) +{ + if (zone_as_building->getType() != building_type::Civzone) + return; + + auto zone = strict_virtual_cast(zone_as_building); + + if (zone == nullptr) + return; + + auto& vec = world->buildings.other[buildings_other_id::IN_PLAY]; + + //this is a paranoid check and slower than it could be. Zones contain a list of children + //good for fixing potentially bad game states when deleting a zone + for (size_t i = 0; i < vec.size(); i++) + { + df::building* bld = vec[i]; + + remove_building_from_zone(bld, zone); + } +} + uint32_t Buildings::getNumBuildings() { return world->buildings.all.size(); @@ -325,78 +481,30 @@ static void cacheBuilding(df::building *building, bool is_civzone) { for (int32_t y = p1.y; y <= p2.y; y++) { df::coord pt(x, y, building->z); if (Buildings::containsTile(building, pt, is_civzone)) { - if (is_civzone) - locationToCivzones[pt].push_back(id); - else + if (!is_civzone) locationToBuilding[pt] = id; } } } } -static int32_t nextCivzone = 0; -static void cacheNewCivzones() { - if (!world || !building_next_id) - return; - - int32_t nextBuildingId = *building_next_id; - for (int32_t id = nextCivzone; id < nextBuildingId; ++id) { - auto &vec = world->buildings.other[buildings_other_id::ANY_ZONE]; - int32_t idx = df::building::binsearch_index(vec, id); - if (idx > -1) - cacheBuilding(vec[idx], true); - } - nextCivzone = nextBuildingId; -} - bool Buildings::findCivzonesAt(std::vector *pvec, df::coord pos) { pvec->clear(); - // Tiles have an occupancy->bits.building flag to quickly determine if it is - // covered by a buildling, but there is no such flag for civzones. - // Therefore, we need to make sure that our cache is authoratative. - // Otherwise, we would have to fall back to linearly scanning the list of - // all civzones on a cache miss. - // - // Since we guarantee our cache contains *at least* all tiles that are - // currently covered by civzones, we can conclude that if a tile is not in - // the cache, there is no civzone there. Civzones *can* be dynamically - // shrunk, so we still need to verify that civzones that once covered this - // tile continue to cover this tile. - cacheNewCivzones(); - - auto civzones_it = locationToCivzones.find(pos); - if (civzones_it == locationToCivzones.end()) - return false; - - set ids_to_remove; - auto &civzones = civzones_it->second; - for (int32_t id : civzones) { - int32_t idx = df::building::binsearch_index( - world->buildings.other[buildings_other_id::ANY_ZONE], id); - df::building_civzonest *civzone = NULL; - if (idx > -1) - civzone = world->buildings.other.ANY_ZONE[idx]; - if (!civzone || civzone->z != pos.z || - !containsTile(civzone, pos, true)) { - ids_to_remove.insert(id); + for (df::building_civzonest* zone : world->buildings.other.ACTIVITY_ZONE) + { + if (pos.z != zone->z) continue; - } - pvec->push_back(civzone); - } - // civzone no longer occupies this tile; update the cache - if (!ids_to_remove.empty()) { - for (auto it = civzones.begin(); it != civzones.end(); ) { - if (ids_to_remove.count(*it)) { - it = civzones.erase(it); + if (zone->room.extents && zone->isExtentShaped()) + { + auto etile = getExtentTile(zone->room, pos); + if (!etile || !*etile) continue; - } - ++it; + + pvec->push_back(zone); } - if (civzones.empty()) - locationToCivzones.erase(pos); } return !pvec->empty(); @@ -1053,7 +1161,6 @@ static int getMaxStockpileId() return max_id; } -/* TODO: understand how this changes for v50 static int getMaxCivzoneId() { auto &vec = world->buildings.other[buildings_other_id::ANY_ZONE]; @@ -1068,7 +1175,6 @@ static int getMaxCivzoneId() return max_id; } -*/ bool Buildings::constructAbstract(df::building *bld) { @@ -1087,12 +1193,14 @@ bool Buildings::constructAbstract(df::building *bld) stock->stockpile_number = getMaxStockpileId() + 1; break; -/* TODO: understand how this changes for v50 case building_type::Civzone: if (auto zone = strict_virtual_cast(bld)) + { zone->zone_num = getMaxCivzoneId() + 1; + + add_zone_to_all_buildings(zone); + } break; -*/ default: break; @@ -1186,6 +1294,8 @@ bool Buildings::constructWithItems(df::building *bld, std::vector ite bld->mat_index = items[i]->getMaterialIndex(); } + add_building_to_all_zones(bld); + createDesign(bld, rough); return true; } @@ -1232,6 +1342,8 @@ bool Buildings::constructWithFilters(df::building *bld, std::vectoruncategorize(); + + remove_building_from_all_zones(bld); + remove_zone_from_all_buildings(bld); + delete bld; if (world->selected_building == bld) @@ -1310,12 +1426,20 @@ bool Buildings::markedForRemoval(df::building *bld) return false; } +void Buildings::notifyCivzoneModified(df::building* bld) +{ + if (bld->getType() != building_type::Civzone) + return; + + //remove zone here needs to be the slow method + remove_zone_from_all_buildings(bld); + add_zone_to_all_buildings(bld); +} + void Buildings::clearBuildings(color_ostream& out) { corner1.clear(); corner2.clear(); locationToBuilding.clear(); - locationToCivzones.clear(); - nextCivzone = 0; } void Buildings::updateBuildings(color_ostream&, void* ptr)