#include #include #include #include #include #include #include "Console.h" #include "Core.h" #include "Export.h" #include "MiscUtils.h" #include "PluginManager.h" #include "TileTypes.h" #include "VTableInterpose.h" #include "modules/Gui.h" #include "modules/Maps.h" #include "modules/Screen.h" #include "modules/World.h" #include "df/building_def_workshopst.h" #include "df/building_drawbuffer.h" #include "df/building_workshopst.h" #include "df/buildings_other_id.h" #include "df/builtin_mats.h" #include "df/flow_info.h" #include "df/graphic.h" #include "df/item_liquid_miscst.h" #include "df/job.h" #include "df/machine.h" #include "df/power_info.h" #include "df/report.h" #include "df/tile_designation.h" #include "df/plotinfost.h" #include "df/buildreq.h" #include "df/viewscreen_dwarfmodest.h" #include "df/workshop_type.h" #include "df/world.h" using std::vector; using std::string; using std::stack; using std::set; using namespace DFHack; using namespace df::enums; DFHACK_PLUGIN("steam-engine"); REQUIRE_GLOBAL(gps); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(ui_build_selector); REQUIRE_GLOBAL(cursor); /* * List of known steam engine workshop raws. */ struct steam_engine_workshop { int id; df::building_def_workshopst *def; // Cached properties bool is_magma; int max_power, max_capacity; int wear_temp; // Special tiles (relative position) std::vector gear_tiles; df::coord2d hearth_tile; df::coord2d water_tile; df::coord2d magma_tile; }; std::vector engines; steam_engine_workshop *find_steam_engine(int id) { for (size_t i = 0; i < engines.size(); i++) if (engines[i].id == id) return &engines[i]; return NULL; } /* * Misc utilities. */ static const int hearth_colors[6][2] = { { COLOR_BLACK, 1 }, { COLOR_BROWN, 0 }, { COLOR_RED, 0 }, { COLOR_RED, 1 }, { COLOR_BROWN, 1 }, { COLOR_GREY, 1 } }; void enable_updates_at(df::coord pos, bool flow, bool temp) { static const int delta[4][2] = { { -1, -1 }, { 1, -1 }, { -1, 1 }, { 1, 1 } }; for (int i = 0; i < 4; i++) { auto blk = Maps::getTileBlock(pos.x+delta[i][0], pos.y+delta[i][1], pos.z); Maps::enableBlockUpdates(blk, flow, temp); } } void decrement_flow(df::coord pos, int amount) { auto pldes = Maps::getTileDesignation(pos); if (!pldes) return; int nsize = std::max(0, int(pldes->bits.flow_size - amount)); pldes->bits.flow_size = nsize; pldes->bits.flow_forbid = (nsize > 3 || pldes->bits.liquid_type == tile_liquid::Magma); enable_updates_at(pos, true, false); } void make_explosion(df::coord center, int power) { static const int bias[9] = { 60, 30, 60, 30, 0, 30, 60, 30, 60 }; int mat_type = builtin_mats::WATER, mat_index = -1; int i = 0; for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { int size = power - bias[i++]; auto pos = center + df::coord(dx,dy,0); if (size > 0) Maps::spawnFlow(pos, flow_type::MaterialDust, mat_type, mat_index, size); } } Gui::showAutoAnnouncement( announcement_type::CAVE_COLLAPSE, center, "A boiler has exploded!", COLOR_RED, true ); } static const int WEAR_TICKS = 806400; bool add_wear_nodestroy(df::item_actual *item, int rate) { if (item->incWearTimer(rate)) { while (item->wear_timer >= WEAR_TICKS) { item->wear_timer -= WEAR_TICKS; item->wear++; } } return item->wear > 3; } /* * Hook for the liquid item. Implements a special 'boiling' * matter state with a modified description and temperature * locked at boiling-1. */ struct liquid_hook : df::item_liquid_miscst { typedef df::item_liquid_miscst interpose_base; static const uint32_t BOILING_FLAG = 0x80000000U; DEFINE_VMETHOD_INTERPOSE(void, getItemDescription, (std::string *buf, int8_t mode)) { if (mat_state.whole & BOILING_FLAG) buf->append("boiling "); INTERPOSE_NEXT(getItemDescription)(buf, mode); } DEFINE_VMETHOD_INTERPOSE(bool, adjustTemperature, (uint16_t temp, int32_t unk)) { if (mat_state.whole & BOILING_FLAG) temp = std::max(int(temp), getBoilingPoint()-1); return INTERPOSE_NEXT(adjustTemperature)(temp, unk); } DEFINE_VMETHOD_INTERPOSE(bool, checkTemperatureDamage, ()) { if (mat_state.whole & BOILING_FLAG) temperature.whole = std::max(int(temperature.whole), getBoilingPoint()-1); return INTERPOSE_NEXT(checkTemperatureDamage)(); } }; IMPLEMENT_VMETHOD_INTERPOSE(liquid_hook, getItemDescription); IMPLEMENT_VMETHOD_INTERPOSE(liquid_hook, adjustTemperature); IMPLEMENT_VMETHOD_INTERPOSE(liquid_hook, checkTemperatureDamage); /* * Hook for the workshop itself. Implements core logic. */ struct workshop_hook : df::building_workshopst { typedef df::building_workshopst interpose_base; // Engine detection steam_engine_workshop *get_steam_engine() { if (type == workshop_type::Custom) return find_steam_engine(custom_type); return NULL; } inline bool is_fully_built() { return getBuildStage() >= getMaxBuildStage(); } // Use high bits of flags to store current steam amount. // This is necessary for consistency if items disappear unexpectedly. int get_steam_amount() { return (flags.whole >> 28) & 15; } void set_steam_amount(int count) { flags.whole = (flags.whole & 0x0FFFFFFFU) | uint32_t((count & 15) << 28); } // Find liquids to consume below the engine. bool find_liquids(df::coord *pwater, df::coord *pmagma, bool is_magma, int min_level) { if (!is_magma) pmagma = NULL; for (int x = x1; x <= x2; x++) { for (int y = y1; y <= y2; y++) { auto ptile = Maps::getTileType(x,y,z); if (!ptile || !FlowPassableDown(*ptile)) continue; auto pltile = Maps::getTileType(x,y,z-1); if (!pltile || !FlowPassable(*pltile)) continue; auto pldes = Maps::getTileDesignation(x,y,z-1); if (!pldes || pldes->bits.flow_size < min_level) continue; if (pldes->bits.liquid_type == tile_liquid::Magma) { if (pmagma) *pmagma = df::coord(x,y,z-1); if (pwater->isValid()) return true; } else { *pwater = df::coord(x,y,z-1); if (!pmagma || pmagma->isValid()) return true; } } } return false; } // Absorbs a water item produced by stoke reaction into the engine. bool absorb_unit(steam_engine_workshop *engine, df::item_liquid_miscst *liquid) { // Consume liquid inputs df::coord water, magma; if (!find_liquids(&water, &magma, engine->is_magma, 1)) { // Destroy the item with enormous wear amount. liquid->addWear(WEAR_TICKS*5, true, false); return false; } decrement_flow(water, 1); if (engine->is_magma) decrement_flow(magma, 1); // Update flags liquid->flags.bits.in_building = true; liquid->mat_state.whole |= liquid_hook::BOILING_FLAG; liquid->temperature.whole = liquid->getBoilingPoint()-1; liquid->temperature.fraction = 0; // This affects where the steam appears to come from if (engine->hearth_tile.isValid()) liquid->pos = df::coord(x1+engine->hearth_tile.x, y1+engine->hearth_tile.y, z); // Enable block temperature updates enable_updates_at(liquid->pos, false, true); return true; } bool boil_unit(df::item_liquid_miscst *liquid) { liquid->wear = 4; liquid->flags.bits.in_building = false; liquid->temperature.whole = liquid->getBoilingPoint() + 10; return liquid->checkMeltBoil(); } void suspend_jobs(bool suspend) { for (size_t i = 0; i < jobs.size(); i++) if (jobs[i]->job_type == job_type::CustomReaction) jobs[i]->flags.bits.suspend = suspend; } // Scan contained items for boiled steam to absorb. df::item_liquid_miscst *collect_steam(steam_engine_workshop *engine, int *count) { df::item_liquid_miscst *first = NULL; *count = 0; for (int i = contained_items.size()-1; i >= 0; i--) { auto item = contained_items[i]; if (item->use_mode != 0) continue; auto liquid = strict_virtual_cast(item->item); if (!liquid) continue; if (!liquid->flags.bits.in_building) { if (liquid->mat_type != builtin_mats::WATER || liquid->age > 1 || liquid->wear != 0) continue; // This may destroy the item if (!absorb_unit(engine, liquid)) continue; } if (*count < engine->max_capacity) { first = liquid; ++*count; } else { // Overpressure valve boil_unit(liquid); suspend_jobs(true); } } return first; } void random_boil() { int cnt = 0; for (int i = contained_items.size()-1; i >= 0; i--) { auto item = contained_items[i]; if (item->use_mode != 0 || !item->item->flags.bits.in_building) continue; auto liquid = strict_virtual_cast(item->item); if (!liquid) continue; if (cnt == 0 || rand() < RAND_MAX/2) { cnt++; boil_unit(liquid); } } } int classify_component(df::building_actual::T_contained_items *item) { if (item->use_mode != 2 || item->item->isBuildMat()) return -1; switch (item->item->getType()) { case item_type::TRAPPARTS: case item_type::CHAIN: return 0; case item_type::BARREL: return 2; default: return 1; } } bool check_component_wear(steam_engine_workshop *engine, int count, int power) { int coeffs[3] = { 0, power, count }; for (int i = contained_items.size()-1; i >= 0; i--) { int type = classify_component(contained_items[i]); if (type < 0) continue; df::item *item = contained_items[i]->item; int melt_temp = item->getMeltingPoint(); if (coeffs[type] == 0 || melt_temp >= engine->wear_temp) continue; // let 500 degree delta at 4 pressure work 1 season float ticks = coeffs[type]*(engine->wear_temp - melt_temp)*3.0f/500.0f/4.0f; if (item->addWear(int(8*(1 + ticks)), true, true)) return true; } return false; } float get_component_quality(int use_type) { float sum = 0, cnt = 0; for (size_t i = 0; i < contained_items.size(); i++) { int type = classify_component(contained_items[i]); if (type != use_type) continue; sum += contained_items[i]->item->getQuality(); cnt += 1; } return (cnt > 0 ? sum/cnt : 0); } int get_steam_use_rate(steam_engine_workshop *engine, int dimension, int power_level) { // total ticks to wear off completely float ticks = WEAR_TICKS * 4.0f; // dimension == days it lasts * 100 ticks /= 1200.0f * dimension / 100.0f; // true power use float power_rate = 1.0f; // check the actual load if (auto mptr = df::machine::find(machine.machine_id)) { if (mptr->cur_power >= mptr->min_power) power_rate = float(mptr->min_power) / mptr->cur_power; else power_rate = 0.0f; } // waste rate: 1-10% depending on piston assembly quality float piston_qual = get_component_quality(1); float waste = 0.1f - 0.016f * 0.5f * (piston_qual + get_component_quality(2)); float efficiency_coeff = 1.0f - 0.02f * piston_qual; // apply rate and waste factor ticks *= (waste + 0.9f*power_rate)*power_level*efficiency_coeff; // end result return std::max(1, int(ticks)); } void update_under_construction(steam_engine_workshop *engine) { if (machine.machine_id != -1) return; int cur_count = 0; if (auto first = collect_steam(engine, &cur_count)) { if (add_wear_nodestroy(first, WEAR_TICKS*4/10)) { boil_unit(first); cur_count--; } } set_steam_amount(cur_count); } void update_working(steam_engine_workshop *engine) { int old_count = get_steam_amount(); int old_power = std::min(engine->max_power, old_count); int cur_count = 0; if (auto first = collect_steam(engine, &cur_count)) { int rate = get_steam_use_rate(engine, first->dimension, old_power); if (add_wear_nodestroy(first, rate)) { boil_unit(first); cur_count--; } if (check_component_wear(engine, old_count, old_power)) return; } if (old_count < engine->max_capacity && cur_count == engine->max_capacity) suspend_jobs(true); else if (cur_count <= engine->max_power+1 && old_count > engine->max_power+1) suspend_jobs(false); set_steam_amount(cur_count); int cur_power = std::min(engine->max_power, cur_count); if (cur_power != old_power) { auto mptr = df::machine::find(machine.machine_id); if (mptr) mptr->cur_power += (cur_power - old_power)*100; } } // Furnaces need architecture, and this is a workshop // only because furnaces cannot connect to machines. DEFINE_VMETHOD_INTERPOSE(bool, needsDesign, ()) { if (get_steam_engine()) return true; return INTERPOSE_NEXT(needsDesign)(); } // Machine interface DEFINE_VMETHOD_INTERPOSE(void, getPowerInfo, (df::power_info *info)) { if (auto engine = get_steam_engine()) { info->produced = std::min(engine->max_power, get_steam_amount())*100; info->consumed = 10 - int(get_component_quality(0)); return; } INTERPOSE_NEXT(getPowerInfo)(info); } DEFINE_VMETHOD_INTERPOSE(df::machine_info*, getMachineInfo, ()) { if (get_steam_engine()) return &machine; return INTERPOSE_NEXT(getMachineInfo)(); } DEFINE_VMETHOD_INTERPOSE(bool, isPowerSource, ()) { if (get_steam_engine()) return true; return INTERPOSE_NEXT(isPowerSource)(); } DEFINE_VMETHOD_INTERPOSE(void, categorize, (bool free)) { if (get_steam_engine()) { auto &vec = world->buildings.other[buildings_other_id::ANY_MACHINE]; insert_into_vector(vec, &df::building::id, (df::building*)this); } INTERPOSE_NEXT(categorize)(free); } DEFINE_VMETHOD_INTERPOSE(void, uncategorize, ()) { if (get_steam_engine()) { auto &vec = world->buildings.other[buildings_other_id::ANY_MACHINE]; erase_from_vector(vec, &df::building::id, id); } INTERPOSE_NEXT(uncategorize)(); } DEFINE_VMETHOD_INTERPOSE(bool, canConnectToMachine, (df::machine_tile_set *info)) { if (auto engine = get_steam_engine()) { int real_cx = centerx, real_cy = centery; bool ok = false; for (size_t i = 0; i < engine->gear_tiles.size(); i++) { // the original function connects to the center tile centerx = x1 + engine->gear_tiles[i].x; centery = y1 + engine->gear_tiles[i].y; if (!INTERPOSE_NEXT(canConnectToMachine)(info)) continue; ok = true; break; } centerx = real_cx; centery = real_cy; return ok; } else return INTERPOSE_NEXT(canConnectToMachine)(info); } // Operation logic DEFINE_VMETHOD_INTERPOSE(bool, isUnpowered, ()) { if (auto engine = get_steam_engine()) { df::coord water, magma; return !find_liquids(&water, &magma, engine->is_magma, 3); } return INTERPOSE_NEXT(isUnpowered)(); } DEFINE_VMETHOD_INTERPOSE(void, updateAction, ()) { if (auto engine = get_steam_engine()) { if (is_fully_built()) update_working(engine); else update_under_construction(engine); if (flags.bits.almost_deleted) return; } INTERPOSE_NEXT(updateAction)(); } DEFINE_VMETHOD_INTERPOSE(void, drawBuilding, (df::building_drawbuffer *db, int16_t unk)) { INTERPOSE_NEXT(drawBuilding)(db, unk); if (auto engine = get_steam_engine()) { if (!is_fully_built()) return; // If machine is running, tweak gear assemblies auto mptr = df::machine::find(machine.machine_id); if (mptr && (mptr->visual_phase & 1) != 0) { for (size_t i = 0; i < engine->gear_tiles.size(); i++) { auto pos = engine->gear_tiles[i]; db->tile[pos.x][pos.y] = 42; } } // Use the hearth color to display power level if (engine->hearth_tile.isValid()) { auto pos = engine->hearth_tile; int power = std::min(engine->max_power, get_steam_amount()); db->fore[pos.x][pos.y] = hearth_colors[power][0]; db->bright[pos.x][pos.y] = hearth_colors[power][1]; } // Set liquid indicator state if (engine->water_tile.isValid() || engine->magma_tile.isValid()) { df::coord water, magma; find_liquids(&water, &magma, engine->is_magma, 3); df::coord dwater, dmagma; find_liquids(&dwater, &dmagma, engine->is_magma, 5); if (engine->water_tile.isValid()) { if (!water.isValid()) db->fore[engine->water_tile.x][engine->water_tile.y] = 0; else if (!dwater.isValid()) db->bright[engine->water_tile.x][engine->water_tile.y] = 0; } if (engine->magma_tile.isValid() && engine->is_magma) { if (!magma.isValid()) db->fore[engine->magma_tile.x][engine->magma_tile.y] = 0; else if (!dmagma.isValid()) db->bright[engine->magma_tile.x][engine->magma_tile.y] = 0; } } } } DEFINE_VMETHOD_INTERPOSE(void, deconstructItems, (bool noscatter, bool lost)) { if (get_steam_engine()) { // Explode if any steam left if (int amount = get_steam_amount()) { make_explosion( df::coord((x1+x2)/2, (y1+y2)/2, z), 40 + amount * 20 ); random_boil(); } } INTERPOSE_NEXT(deconstructItems)(noscatter, lost); } }; IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, needsDesign); IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, getPowerInfo); IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, getMachineInfo); IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, isPowerSource); IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, categorize); IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, uncategorize); IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, canConnectToMachine); IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, isUnpowered); IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, updateAction); IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, drawBuilding); IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, deconstructItems); /* * Hook for the dwarfmode screen. Tweaks the build menu * behavior to suit the steam engine building more. */ struct dwarfmode_hook : df::viewscreen_dwarfmodest { typedef df::viewscreen_dwarfmodest interpose_base; steam_engine_workshop *get_steam_engine() { if (plotinfo->main.mode == ui_sidebar_mode::Build && ui_build_selector->stage == 1 && ui_build_selector->building_type == building_type::Workshop && ui_build_selector->building_subtype == workshop_type::Custom) { return find_steam_engine(ui_build_selector->custom_type); } return NULL; } void check_hanging_tiles(steam_engine_workshop *engine) { if (!engine) return; bool error = false; int x1 = cursor->x - engine->def->workloc_x; int y1 = cursor->y - engine->def->workloc_y; for (int x = 0; x < engine->def->dim_x; x++) { for (int y = 0; y < engine->def->dim_y; y++) { if (ui_build_selector->tiles[x][y] >= 5) continue; auto ptile = Maps::getTileType(x1+x,y1+y,cursor->z); if (ptile && !isOpenTerrain(*ptile)) continue; ui_build_selector->tiles[x][y] = 6; error = true; } } if (error) { const char *msg = "Hanging - cover channels with down stairs."; ui_build_selector->errors.push_back(new std::string(msg)); } } DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) { steam_engine_workshop *engine = get_steam_engine(); // Selector insists that workshops cannot be placed hanging // unless they require magma, so pretend we always do. if (engine) engine->def->needs_magma = true; INTERPOSE_NEXT(feed)(input); // Restore the flag if (engine) engine->def->needs_magma = engine->is_magma; // And now, check for open space. Since these workshops // are machines, they will collapse over true open space. check_hanging_tiles(get_steam_engine()); } }; IMPLEMENT_VMETHOD_INTERPOSE(dwarfmode_hook, feed); /* * Scan raws for matching workshop buildings. */ static bool find_engines(color_ostream &out) { engines.clear(); auto &wslist = world->raws.buildings.workshops; for (size_t i = 0; i < wslist.size(); i++) { if (strstr(wslist[i]->code.c_str(), "STEAM_ENGINE") == NULL) continue; steam_engine_workshop ws; ws.def = wslist[i]; ws.id = ws.def->id; int bs = ws.def->build_stages; for (int x = 0; x < ws.def->dim_x; x++) { for (int y = 0; y < ws.def->dim_y; y++) { switch (ws.def->tile[bs][x][y]) { case 15: ws.gear_tiles.push_back(df::coord2d(x,y)); break; case 19: ws.hearth_tile = df::coord2d(x,y); break; } if (ws.def->tile_color[2][bs][x][y]) { switch (ws.def->tile_color[0][bs][x][y]) { case 1: ws.water_tile = df::coord2d(x,y); break; case 4: ws.magma_tile = df::coord2d(x,y); break; } } } } ws.is_magma = ws.def->needs_magma; ws.max_power = ws.is_magma ? 5 : 3; ws.max_capacity = ws.is_magma ? 10 : 6; ws.wear_temp = ws.is_magma ? 12000 : 11000; if (!ws.gear_tiles.empty()) engines.push_back(ws); else out.printerr("%s has no gear tiles - ignoring.\n", wslist[i]->code.c_str()); } return !engines.empty(); } DFHACK_PLUGIN_IS_ENABLED(is_enabled); static void enable_hooks(bool enable) { is_enabled = enable; INTERPOSE_HOOK(liquid_hook, getItemDescription).apply(enable); INTERPOSE_HOOK(liquid_hook, adjustTemperature).apply(enable); INTERPOSE_HOOK(liquid_hook, checkTemperatureDamage).apply(enable); INTERPOSE_HOOK(workshop_hook, needsDesign).apply(enable); INTERPOSE_HOOK(workshop_hook, getPowerInfo).apply(enable); INTERPOSE_HOOK(workshop_hook, getMachineInfo).apply(enable); INTERPOSE_HOOK(workshop_hook, isPowerSource).apply(enable); INTERPOSE_HOOK(workshop_hook, categorize).apply(enable); INTERPOSE_HOOK(workshop_hook, uncategorize).apply(enable); INTERPOSE_HOOK(workshop_hook, canConnectToMachine).apply(enable); INTERPOSE_HOOK(workshop_hook, isUnpowered).apply(enable); INTERPOSE_HOOK(workshop_hook, updateAction).apply(enable); INTERPOSE_HOOK(workshop_hook, drawBuilding).apply(enable); INTERPOSE_HOOK(workshop_hook, deconstructItems).apply(enable); INTERPOSE_HOOK(dwarfmode_hook, feed).apply(enable); } DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { switch (event) { case SC_MAP_LOADED: if (World::isFortressMode() && find_engines(out)) { out.print("Detected steam engine workshops - enabling plugin.\n"); enable_hooks(true); } else enable_hooks(false); break; case SC_MAP_UNLOADED: enable_hooks(false); engines.clear(); break; default: break; } return CR_OK; } DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { if (Core::getInstance().isWorldLoaded()) plugin_onstatechange(out, SC_WORLD_LOADED); return CR_OK; } DFhackCExport command_result plugin_shutdown ( color_ostream &out ) { enable_hooks(false); return CR_OK; }