/* https://github.com/peterix/dfhack Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. */ #include "Internal.h" #include #include #include using namespace std; #include "VersionInfo.h" #include "MemAccess.h" #include "Types.h" #include "Error.h" #include "modules/Buildings.h" #include "modules/Maps.h" #include "modules/Job.h" #include "ModuleFactory.h" #include "Core.h" #include "TileTypes.h" #include "MiscUtils.h" using namespace DFHack; #include "DataDefs.h" #include "df/world.h" #include "df/ui.h" #include "df/ui_look_list.h" #include "df/d_init.h" #include "df/item.h" #include "df/unit.h" #include "df/job.h" #include "df/job_item.h" #include "df/general_ref_building_holderst.h" #include "df/buildings_other_id.h" #include "df/building_design.h" #include "df/building_def.h" #include "df/building_axle_horizontalst.h" #include "df/building_trapst.h" #include "df/building_bridgest.h" #include "df/building_coffinst.h" #include "df/building_civzonest.h" #include "df/building_stockpilest.h" #include "df/building_furnacest.h" #include "df/building_workshopst.h" #include "df/building_screw_pumpst.h" #include "df/building_water_wheelst.h" #include "df/building_wellst.h" #include "df/building_rollersst.h" using namespace df::enums; using df::global::ui; using df::global::world; using df::global::d_init; using df::global::building_next_id; using df::global::process_jobs; using df::building_def; static uint8_t *getExtentTile(df::building_extents &extent, df::coord2d tile) { if (!extent.extents) return NULL; int dx = tile.x - extent.x; int dy = tile.y - extent.y; if (dx < 0 || dy < 0 || dx >= extent.width || dy >= extent.height) return NULL; return &extent.extents[dx + dy*extent.width]; } /* * A monitor to work around this bug, in its application to buildings: * * http://www.bay12games.com/dwarves/mantisbt/view.php?id=1416 */ bool buildings_do_onupdate = false; void buildings_onStateChange(color_ostream &out, state_change_event event) { switch (event) { case SC_MAP_LOADED: buildings_do_onupdate = true; break; case SC_MAP_UNLOADED: buildings_do_onupdate = false; break; default: break; } } void buildings_onUpdate(color_ostream &out) { buildings_do_onupdate = false; df::job_list_link *link = world->job_list.next; for (; link; link = link->next) { df::job *job = link->item; if (job->job_type != job_type::ConstructBuilding) continue; if (job->job_items.empty()) continue; buildings_do_onupdate = true; for (size_t i = 0; i < job->items.size(); i++) { df::job_item_ref *iref = job->items[i]; if (iref->role != df::job_item_ref::Reagent) continue; df::job_item *item = vector_get(job->job_items, iref->job_item_idx); if (!item) continue; // Convert Reagent to Hauled, while decrementing quantity item->quantity = std::max(0, item->quantity-1); iref->role = df::job_item_ref::Hauled; iref->job_item_idx = -1; } } } uint32_t Buildings::getNumBuildings() { return world->buildings.all.size(); } bool Buildings::Read (const uint32_t index, t_building & building) { Core & c = Core::getInstance(); df::building *bld = world->buildings.all[index]; building.x1 = bld->x1; building.x2 = bld->x2; building.y1 = bld->y1; building.y2 = bld->y2; building.z = bld->z; building.material.index = bld->mat_index; building.material.type = bld->mat_type; building.type = bld->getType(); building.subtype = bld->getSubtype(); building.custom_type = bld->getCustomType(); building.origin = bld; return true; } bool Buildings::ReadCustomWorkshopTypes(map & btypes) { vector & bld_def = world->raws.buildings.all; uint32_t size = bld_def.size(); btypes.clear(); for (auto iter = bld_def.begin(); iter != bld_def.end();iter++) { building_def * temp = *iter; btypes[temp->id] = temp->code; } return true; } bool Buildings::setOwner(df::building *bld, df::unit *unit) { CHECK_NULL_POINTER(bld); if (!bld->is_room) return false; if (bld->owner == unit) return true; if (bld->owner) { auto &blist = bld->owner->owned_buildings; vector_erase_at(blist, linear_index(blist, bld)); if (auto spouse = df::unit::find(bld->owner->relations.spouse_id)) { auto &blist = spouse->owned_buildings; vector_erase_at(blist, linear_index(blist, bld)); } } bld->owner = unit; if (unit) { unit->owned_buildings.push_back(bld); if (auto spouse = df::unit::find(unit->relations.spouse_id)) { auto &blist = spouse->owned_buildings; if (bld->canUseSpouseRoom() && linear_index(blist, bld) < 0) blist.push_back(bld); } } return true; } df::building *Buildings::findAtTile(df::coord pos) { auto occ = Maps::getTileOccupancy(pos); if (!occ || !occ->bits.building) return NULL; auto &vec = df::building::get_vector(); for (size_t i = 0; i < vec.size(); i++) { auto bld = vec[i]; if (pos.z != bld->z || pos.x < bld->x1 || pos.x > bld->x2 || pos.y < bld->y1 || pos.y > bld->y2) continue; if (!bld->isSettingOccupancy()) continue; if (bld->room.extents && bld->isExtentShaped()) { auto etile = getExtentTile(bld->room, pos); if (!etile || !*etile) continue; } return bld; } return NULL; } bool Buildings::findCivzonesAt(std::vector *pvec, df::coord pos) { pvec->clear(); auto &vec = world->buildings.other[buildings_other_id::ANY_ZONE]; for (size_t i = 0; i < vec.size(); i++) { auto bld = strict_virtual_cast(vec[i]); if (!bld || bld->z != pos.z || !containsTile(bld, pos)) continue; pvec->push_back(bld); } return !pvec->empty(); } df::building *Buildings::allocInstance(df::coord pos, df::building_type type, int subtype, int custom) { if (!building_next_id) return NULL; // Allocate object const char *classname = ENUM_ATTR(building_type, classname, type); if (!classname) return NULL; auto id = virtual_identity::find(classname); if (!id) return NULL; df::building *bld = (df::building*)id->allocate(); if (!bld) return NULL; // Init base fields bld->x1 = bld->x2 = bld->centerx = pos.x; bld->y1 = bld->y2 = bld->centery = pos.y; bld->z = pos.z; bld->race = ui->race_id; if (subtype != -1) bld->setSubtype(subtype); if (custom != -1) bld->setCustomType(custom); bld->setMaterialAmount(1); // Type specific init switch (type) { case building_type::Well: { auto obj = (df::building_wellst*)bld; obj->bucket_z = bld->z; break; } case building_type::Furnace: { auto obj = (df::building_furnacest*)bld; obj->melt_remainder.resize(df::inorganic_raw::get_vector().size(), 0); break; } case building_type::Coffin: { auto obj = (df::building_coffinst*)bld; obj->initBurialFlags(); // DF has this copy&pasted break; } case building_type::Trap: { auto obj = (df::building_trapst*)bld; if (obj->trap_type == trap_type::PressurePlate) obj->ready_timeout = 500; break; } default: break; } return bld; } static void makeOneDim(df::coord2d &size, df::coord2d ¢er, bool vertical) { if (vertical) size.x = 1; else size.y = 1; center = size/2; } bool Buildings::getCorrectSize(df::coord2d &size, df::coord2d ¢er, df::building_type type, int subtype, int custom, int direction) { using namespace df::enums::building_type; if (size.x <= 0) size.x = 1; if (size.y <= 0) size.y = 1; switch (type) { case FarmPlot: case Bridge: case RoadDirt: case RoadPaved: case Stockpile: case Civzone: center = size/2; return true; case TradeDepot: case Shop: size = df::coord2d(5,5); center = df::coord2d(2,2); return false; case SiegeEngine: case Windmill: case Wagon: size = df::coord2d(3,3); center = df::coord2d(1,1); return false; case AxleHorizontal: makeOneDim(size, center, direction); return true; case Rollers: makeOneDim(size, center, (direction&1) == 0); return true; case WaterWheel: size = df::coord2d(3,3); makeOneDim(size, center, direction); return false; case Workshop: { using namespace df::enums::workshop_type; switch ((df::workshop_type)subtype) { case Quern: case Millstone: case Tool: size = df::coord2d(1,1); center = df::coord2d(0,0); break; case Siege: case Kennels: size = df::coord2d(5,5); center = df::coord2d(2,2); break; case Custom: if (auto def = df::building_def::find(custom)) { size = df::coord2d(def->dim_x, def->dim_y); center = df::coord2d(def->workloc_x, def->workloc_y); break; } default: size = df::coord2d(3,3); center = df::coord2d(1,1); } return false; } case Furnace: { using namespace df::enums::furnace_type; switch ((df::furnace_type)subtype) { case Custom: if (auto def = df::building_def::find(custom)) { size = df::coord2d(def->dim_x, def->dim_y); center = df::coord2d(def->workloc_x, def->workloc_y); break; } default: size = df::coord2d(3,3); center = df::coord2d(1,1); } return false; } case ScrewPump: { using namespace df::enums::screw_pump_direction; switch ((df::screw_pump_direction)direction) { case FromEast: size = df::coord2d(2,1); center = df::coord2d(1,0); break; case FromSouth: size = df::coord2d(1,2); center = df::coord2d(0,1); break; case FromWest: size = df::coord2d(2,1); center = df::coord2d(0,0); break; default: size = df::coord2d(1,2); center = df::coord2d(0,0); } return false; } default: size = df::coord2d(1,1); center = df::coord2d(0,0); return false; } } bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, df::building_extents *ext, bool create_ext, bool allow_occupied) { bool found_any = false; for (int dx = 0; dx < size.x; dx++) { for (int dy = 0; dy < size.y; dy++) { df::coord tile = pos + df::coord(dx,dy,0); uint8_t *etile = NULL; // Exclude using extents if (ext && ext->extents) { etile = getExtentTile(*ext, tile); if (!etile || !*etile) continue; } // Look up map block df::map_block *block = Maps::getTileBlock(tile); if (!block) return false; df::coord2d btile = df::coord2d(tile) & 15; bool allowed = true; // Check occupancy and tile type if (!allow_occupied && block->occupancy[btile.x][btile.y].bits.building) allowed = false; else { auto tile = block->tiletype[btile.x][btile.y]; if (!HighPassable(tile)) allowed = false; } // Create extents if requested if (allowed) found_any = true; else { if (!ext || !create_ext) return false; if (!ext->extents) { ext->extents = new uint8_t[size.x*size.y]; ext->x = pos.x; ext->y = pos.y; ext->width = size.x; ext->height = size.y; memset(ext->extents, 1, size.x*size.y); etile = getExtentTile(*ext, tile); } if (!etile) return false; *etile = 0; } } } return found_any; } std::pair Buildings::getSize(df::building *bld) { CHECK_NULL_POINTER(bld); df::coord pos(bld->x1,bld->y1,bld->z); return std::pair(pos, df::coord2d(bld->x2+1,bld->y2+1) - pos); } static bool checkBuildingTiles(df::building *bld, bool can_change) { auto psize = Buildings::getSize(bld); return Buildings::checkFreeTiles(psize.first, psize.second, &bld->room, can_change && bld->isExtentShaped(), !bld->isSettingOccupancy()); } int Buildings::countExtentTiles(df::building_extents *ext, int defval) { if (!ext || !ext->extents) return defval; int cnt = 0; for (int i = 0; i < ext->width * ext->height; i++) if (ext->extents[i]) cnt++; return cnt; } bool Buildings::containsTile(df::building *bld, df::coord2d tile, bool room) { CHECK_NULL_POINTER(bld); if (room) { if (!bld->is_room || !bld->room.extents) return false; } else { if (tile.x < bld->x1 || tile.x > bld->x2 || tile.y < bld->y1 || tile.y >= bld->y2) return false; } if (bld->room.extents && (room || bld->isExtentShaped())) { uint8_t *etile = getExtentTile(bld->room, tile); if (!etile || !*etile) return false; } return true; } bool Buildings::hasSupport(df::coord pos, df::coord2d size) { for (int dx = -1; dx <= size.x; dx++) { for (int dy = -1; dy <= size.y; dy++) { // skip corners if ((dx < 0 || dx == size.x) && (dy < 0 || dy == size.y)) continue; df::coord tile = pos + df::coord(dx,dy,0); df::map_block *block = Maps::getTileBlock(tile); if (!block) continue; df::coord2d btile = df::coord2d(tile) & 15; if (!isOpenTerrain(block->tiletype[btile.x][btile.y])) return true; } } return false; } static int computeMaterialAmount(df::building *bld) { auto size = Buildings::getSize(bld).second; int cnt = size.x * size.y; if (bld->room.extents && bld->isExtentShaped()) cnt = Buildings::countExtentTiles(&bld->room, cnt); return cnt/4 + 1; } bool Buildings::setSize(df::building *bld, df::coord2d size, int direction) { CHECK_NULL_POINTER(bld); CHECK_INVALID_ARGUMENT(bld->id == -1); // Delete old extents if (bld->room.extents) { delete[] bld->room.extents; bld->room.extents = NULL; } // Compute correct size and apply it df::coord2d center; getCorrectSize(size, center, bld->getType(), bld->getSubtype(), bld->getCustomType(), direction); bld->x2 = bld->x1 + size.x - 1; bld->y2 = bld->y1 + size.y - 1; bld->centerx = bld->x1 + center.x; bld->centery = bld->y1 + center.y; auto type = bld->getType(); using namespace df::enums::building_type; switch (type) { case WaterWheel: { auto obj = (df::building_water_wheelst*)bld; obj->is_vertical = !!direction; break; } case AxleHorizontal: { auto obj = (df::building_axle_horizontalst*)bld; obj->is_vertical = !!direction; break; } case ScrewPump: { auto obj = (df::building_screw_pumpst*)bld; obj->direction = (df::screw_pump_direction)direction; break; } case Rollers: { auto obj = (df::building_rollersst*)bld; obj->direction = (df::screw_pump_direction)direction; break; } case Bridge: { auto obj = (df::building_bridgest*)bld; auto psize = getSize(bld); obj->gate_flags.bits.has_support = hasSupport(psize.first, psize.second); obj->direction = (df::building_bridgest::T_direction)direction; break; } default: break; } bool ok = checkBuildingTiles(bld, true); if (type != Construction) bld->setMaterialAmount(computeMaterialAmount(bld)); return ok; } static void markBuildingTiles(df::building *bld, bool remove) { bool use_extents = bld->room.extents && bld->isExtentShaped(); bool stockpile = (bld->getType() == building_type::Stockpile); bool complete = (bld->getBuildStage() >= bld->getMaxBuildStage()); if (remove) stockpile = complete = false; for (int tx = bld->x1; tx <= bld->x2; tx++) { for (int ty = bld->y1; ty <= bld->y2; ty++) { df::coord tile(tx,ty,bld->z); if (use_extents) { uint8_t *etile = getExtentTile(bld->room, tile); if (!etile || !*etile) continue; } df::map_block *block = Maps::getTileBlock(tile); df::coord2d btile = df::coord2d(tile) & 15; auto &des = block->designation[btile.x][btile.y]; des.bits.pile = stockpile; if (!remove) des.bits.dig = tile_dig_designation::No; if (complete) bld->updateOccupancy(tx, ty); else { auto &occ = block->occupancy[btile.x][btile.y]; if (remove) occ.bits.building = tile_building_occ::None; else occ.bits.building = tile_building_occ::Planned; } } } } static void linkRooms(df::building *bld) { auto &vec = world->buildings.other[buildings_other_id::IN_PLAY]; bool changed = false; for (size_t i = 0; i < vec.size(); i++) { auto room = vec[i]; if (!room->is_room || room->z != bld->z) continue; uint8_t *pext = getExtentTile(room->room, df::coord2d(bld->x1, bld->y1)); if (!pext || !*pext) continue; changed = true; room->children.push_back(bld); bld->parents.push_back(room); // TODO: the game updates room rent here if economy is enabled } if (changed) df::global::ui->equipment.update.bits.buildings = true; } static void unlinkRooms(df::building *bld) { for (size_t i = 0; i < bld->parents.size(); i++) { auto parent = bld->parents[i]; int idx = linear_index(parent->children, bld); vector_erase_at(parent->children, idx); } bld->parents.clear(); } static void linkBuilding(df::building *bld) { bld->id = (*building_next_id)++; world->buildings.all.push_back(bld); bld->categorize(true); if (bld->isSettingOccupancy()) markBuildingTiles(bld, false); linkRooms(bld); Job::checkBuildingsNow(); } static void createDesign(df::building *bld, bool rough) { auto job = bld->jobs[0]; job->mat_type = bld->mat_type; job->mat_index = bld->mat_index; if (bld->needsDesign()) { auto act = (df::building_actual*)bld; act->design = new df::building_design(); act->design->flags.bits.rough = rough; } } static int getMaxStockpileId() { auto &vec = world->buildings.other[buildings_other_id::STOCKPILE]; int max_id = 0; for (size_t i = 0; i < vec.size(); i++) { auto bld = strict_virtual_cast(vec[i]); if (bld) max_id = std::max(max_id, bld->stockpile_number); } return max_id; } bool Buildings::constructAbstract(df::building *bld) { CHECK_NULL_POINTER(bld); CHECK_INVALID_ARGUMENT(bld->id == -1); CHECK_INVALID_ARGUMENT(!bld->isActual()); if (!checkBuildingTiles(bld, false)) return false; switch (bld->getType()) { case building_type::Stockpile: if (auto stock = strict_virtual_cast(bld)) stock->stockpile_number = getMaxStockpileId() + 1; break; default: break; } linkBuilding(bld); if (!bld->flags.bits.exists) { bld->flags.bits.exists = true; bld->initFarmSeasons(); } return true; } static bool linkForConstruct(df::job* &job, df::building *bld) { if (!checkBuildingTiles(bld, false)) return false; auto ref = df::allocate(); if (!ref) { Core::printerr("Could not allocate general_ref_building_holderst\n"); return false; } linkBuilding(bld); ref->building_id = bld->id; job = new df::job(); job->job_type = df::job_type::ConstructBuilding; job->pos = df::coord(bld->centerx, bld->centery, bld->z); job->references.push_back(ref); bld->jobs.push_back(job); Job::linkIntoWorld(job); return true; } static bool needsItems(df::building *bld) { if (!bld->isActual()) return false; switch (bld->getType()) { case building_type::FarmPlot: case building_type::RoadDirt: return false; default: return true; } } bool Buildings::constructWithItems(df::building *bld, std::vector items) { CHECK_NULL_POINTER(bld); CHECK_INVALID_ARGUMENT(bld->id == -1); CHECK_INVALID_ARGUMENT(bld->isActual()); CHECK_INVALID_ARGUMENT(!items.empty() == needsItems(bld)); for (size_t i = 0; i < items.size(); i++) { CHECK_NULL_POINTER(items[i]); if (items[i]->flags.bits.in_job) return false; } df::job *job = NULL; if (!linkForConstruct(job, bld)) return false; bool rough = false; for (size_t i = 0; i < items.size(); i++) { Job::attachJobItem(job, items[i], df::job_item_ref::Hauled); if (items[i]->getType() == item_type::BOULDER) rough = true; if (bld->mat_type == -1) bld->mat_type = items[i]->getMaterial(); if (bld->mat_index == -1) bld->mat_index = items[i]->getMaterialIndex(); } createDesign(bld, rough); return true; } bool Buildings::constructWithFilters(df::building *bld, std::vector items) { CHECK_NULL_POINTER(bld); CHECK_INVALID_ARGUMENT(bld->id == -1); CHECK_INVALID_ARGUMENT(bld->isActual()); CHECK_INVALID_ARGUMENT(!items.empty() == needsItems(bld)); for (size_t i = 0; i < items.size(); i++) CHECK_NULL_POINTER(items[i]); df::job *job = NULL; if (!linkForConstruct(job, bld)) { for (size_t i = 0; i < items.size(); i++) delete items[i]; return false; } bool rough = false; for (size_t i = 0; i < items.size(); i++) { if (items[i]->quantity < 0) items[i]->quantity = computeMaterialAmount(bld); /* The game picks up explicitly listed items in reverse * order, but processes filters straight. This reverses * the order of filters so as to produce the same final * contained_items ordering as the normal ui way. */ vector_insert_at(job->job_items, 0, items[i]); if (items[i]->item_type == item_type::BOULDER) rough = true; if (bld->mat_type == -1) bld->mat_type = items[i]->mat_type; if (bld->mat_index == -1) bld->mat_index = items[i]->mat_index; } buildings_do_onupdate = true; createDesign(bld, rough); return true; } bool Buildings::deconstruct(df::building *bld) { using df::global::ui; using df::global::world; using df::global::ui_look_list; CHECK_NULL_POINTER(bld); if (bld->isActual() && bld->getBuildStage() > 0) { bld->queueDestroy(); return false; } return deconstructImmediately(bld); } bool Buildings::deconstructImmediately(df::building *bld) { using df::global::ui; using df::global::world; using df::global::ui_look_list; CHECK_NULL_POINTER(bld); /* Immediate destruction code path. Should only happen for abstract and unconstructed buildings.*/ if (bld->isSettingOccupancy()) { markBuildingTiles(bld, true); bld->cleanupMap(); } bld->removeUses(false, false); // Assume: no parties. unlinkRooms(bld); // Assume: not unit destroy target vector_erase_at(ui->tax_collection.rooms, linear_index(ui->tax_collection.rooms, bld)); // Assume: not used in punishment // Assume: not used in non-own jobs // Assume: does not affect pathfinding bld->deconstructItems(false, false); // Don't clear arrows. bld->uncategorize(); delete bld; if (world->selected_building == bld) { world->selected_building = NULL; world->update_selected_building = true; } for (int i = ui_look_list->items.size()-1; i >= 0; i--) { auto item = ui_look_list->items[i]; if (item->type == df::ui_look_list::T_items::Building && item->building == bld) { vector_erase_at(ui_look_list->items, i); delete item; } } Job::checkBuildingsNow(); Job::checkDesignationsNow(); return true; }