diff --git a/plugins/devel/building_zsteam_engine.txt b/plugins/devel/building_zsteam_engine.txt index f76b237d5..572eb4074 100644 --- a/plugins/devel/building_zsteam_engine.txt +++ b/plugins/devel/building_zsteam_engine.txt @@ -30,9 +30,11 @@ building_zsteam_engine [COLOR:2:1:6:0:0:0:0:0:7:0:0] [COLOR:2:2:7:0:0:0:0:0:6:0:0] [COLOR:2:3:7:0:0:MAT:7:0:0] + Tile 15 marks places where machines can connect: [TILE:3:1:15:246:15] [TILE:3:2:'\':19:'/'] [TILE:3:3:7:' ':7] + Color 0:?:1 marks hearth, 1:?:1 water indicator, 4:?:1 magma indicator: [COLOR:3:1:6:0:0:6:0:0:6:0:0] [COLOR:3:2:6:7:0:0:0:1:6:7:0] [COLOR:3:3:1:7:1:0:0:0:4:7:1] diff --git a/plugins/devel/reaction_zsteam_engine.txt b/plugins/devel/reaction_zsteam_engine.txt index b8267cf55..1018510f4 100644 --- a/plugins/devel/reaction_zsteam_engine.txt +++ b/plugins/devel/reaction_zsteam_engine.txt @@ -7,6 +7,7 @@ reaction_other [BUILDING:STEAM_ENGINE:CUSTOM_S] [BUILDING:MAGMA_STEAM_ENGINE:CUSTOM_S] [FUEL] - [PRODUCT:100:1:LIQUID_MISC:NONE:WATER][PRODUCT_DIMENSION:333] [SKILL:SMELT] + Dimension is the number of days it can produce 100 power * 100. + [PRODUCT:100:1:LIQUID_MISC:NONE:WATER][PRODUCT_DIMENSION:1500] diff --git a/plugins/devel/steam-engine.cpp b/plugins/devel/steam-engine.cpp index 23af12177..5a26fb246 100644 --- a/plugins/devel/steam-engine.cpp +++ b/plugins/devel/steam-engine.cpp @@ -22,6 +22,8 @@ #include "df/world.h" #include "df/buildings_other_id.h" #include "df/machine.h" +#include "df/job.h" +#include "df/building_drawbuffer.h" #include "MiscUtils.h" @@ -40,6 +42,8 @@ DFHACK_PLUGIN("steam-engine"); struct steam_engine_workshop { int id; df::building_def_workshopst *def; + bool is_magma; + int max_power, max_capacity; std::vector gear_tiles; df::coord2d hearth_tile; df::coord2d water_tile; @@ -48,42 +52,94 @@ struct steam_engine_workshop { 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; +} + +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 } +}; + +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); + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(liquid_hook, getItemDescription); + struct workshop_hook : df::building_workshopst { typedef df::building_workshopst interpose_base; steam_engine_workshop *get_steam_engine() { if (type == workshop_type::Custom) - for (size_t i = 0; i < engines.size(); i++) - if (engines[i].id == custom_type) - return &engines[i]; + return find_steam_engine(custom_type); return NULL; } + // Use high bits of flags to store current steam amount. + // This is necessary for consistency if items disappear unexpectedly. + int get_steam_amount() { - int cnt = 0; + return (flags.whole >> 28) & 15; + } - for (size_t i = 0; i < contained_items.size(); i++) - { - if (contained_items[i]->use_mode == 0 && - contained_items[i]->item->flags.bits.in_building) - cnt++; - } + void set_steam_amount(int count) + { + flags.whole = (flags.whole & 0x0FFFFFFFU) | uint32_t((count & 15) << 28); + } + + void absorb_unit(steam_engine_workshop *engine, df::item_liquid_miscst *liquid) + { + liquid->flags.bits.in_building = true; + liquid->mat_state.whole |= liquid_hook::BOILING_FLAG; - return cnt; + // 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); } - int get_power_output(steam_engine_workshop *engine) + bool boil_unit(df::item_liquid_miscst *liquid) { - int maxv = engine->def->needs_magma ? 5 : 3; - return std::min(get_steam_amount(), maxv)*100; + liquid->wear = 4; + liquid->flags.bits.in_building = false; + liquid->temperature = 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; } - df::item_liquid_miscst *collect_steam() + 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--) { @@ -98,19 +154,54 @@ struct workshop_hook : df::building_workshopst { if (!liquid->flags.bits.in_building) { if (liquid->mat_type != builtin_mats::WATER || - liquid->dimension != 333 || + liquid->age > 1 || liquid->wear != 0) continue; - liquid->flags.bits.in_building = true; + absorb_unit(engine, liquid); } - first = liquid; + if (*count < engine->max_capacity) + { + first = liquid; + ++*count; + } + else + { + // Overpressure valve + boil_unit(liquid); + } } return first; } + static const int WEAR_TICKS = 806400; + + 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; + } + // apply rate; 10% steam is wasted anyway + ticks *= (0.1f + 0.9f*power_rate)*power_level; + // end result + return std::max(1, int(ticks)); + } + + // Furnaces need architecture, and this is a workshop + // only because furnaces cannot connect to machines. DEFINE_VMETHOD_INTERPOSE(bool, needsDesign, ()) { if (get_steam_engine()) @@ -119,11 +210,12 @@ struct workshop_hook : df::building_workshopst { return INTERPOSE_NEXT(needsDesign)(); } + // Machine interface DEFINE_VMETHOD_INTERPOSE(void, getPowerInfo, (df::power_info *info)) { if (auto engine = get_steam_engine()) { - info->produced = get_power_output(engine); + info->produced = std::min(engine->max_power, get_steam_amount())*100; info->consumed = 10; return; } @@ -178,6 +270,7 @@ struct workshop_hook : df::building_workshopst { 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; @@ -199,37 +292,76 @@ struct workshop_hook : df::building_workshopst { { if (auto engine = get_steam_engine()) { - int output = get_power_output(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()) + if (auto first = collect_steam(engine, &cur_count)) { - if (first->incWearTimer(output)) + int rate = get_steam_use_rate(engine, first->dimension, old_power); + + if (first->incWearTimer(rate)) { - while (first->wear_timer >= 806400) + while (first->wear_timer >= WEAR_TICKS) { - first->wear_timer -= 806400; + first->wear_timer -= WEAR_TICKS; first->wear++; } if (first->wear > 3) { - first->flags.bits.in_building = 0; - first->temperature = first->getBoilingPoint()+50; + boil_unit(first); + cur_count--; } } } - int new_out = get_power_output(engine); - if (new_out != output) + 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 += (new_out - output); + mptr->cur_power += (cur_power - old_power)*100; } } INTERPOSE_NEXT(updateAction)(); } + + DEFINE_VMETHOD_INTERPOSE(void, drawBuilding, (df::building_drawbuffer *db, void *unk)) + { + INTERPOSE_NEXT(drawBuilding)(db, unk); + + if (auto engine = get_steam_engine()) + { + // 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]; + } + } + } }; IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, needsDesign); @@ -240,8 +372,9 @@ IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, categorize); IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, uncategorize); IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, canConnectToMachine); IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, updateAction); +IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, drawBuilding); -static void find_engines() +static bool find_engines() { engines.clear(); @@ -284,47 +417,46 @@ static void find_engines() } } - engines.push_back(ws); + ws.is_magma = ws.def->needs_magma; + ws.max_power = ws.is_magma ? 5 : 3; + ws.max_capacity = ws.is_magma ? 10 : 6; + + if (!ws.gear_tiles.empty()) + engines.push_back(ws); } -} -static void enable_hooks() -{ - INTERPOSE_HOOK(workshop_hook, needsDesign).apply(); - INTERPOSE_HOOK(workshop_hook, getPowerInfo).apply(); - INTERPOSE_HOOK(workshop_hook, getMachineInfo).apply(); - INTERPOSE_HOOK(workshop_hook, isPowerSource).apply(); - INTERPOSE_HOOK(workshop_hook, categorize).apply(); - INTERPOSE_HOOK(workshop_hook, uncategorize).apply(); - INTERPOSE_HOOK(workshop_hook, canConnectToMachine).apply(); - INTERPOSE_HOOK(workshop_hook, updateAction).apply(); + return !engines.empty(); } -static void disable_hooks() +static void enable_hooks(bool enable) { - INTERPOSE_HOOK(workshop_hook, needsDesign).remove(); - INTERPOSE_HOOK(workshop_hook, getPowerInfo).remove(); - INTERPOSE_HOOK(workshop_hook, getMachineInfo).remove(); - INTERPOSE_HOOK(workshop_hook, isPowerSource).remove(); - INTERPOSE_HOOK(workshop_hook, categorize).remove(); - INTERPOSE_HOOK(workshop_hook, uncategorize).remove(); - INTERPOSE_HOOK(workshop_hook, canConnectToMachine).remove(); - INTERPOSE_HOOK(workshop_hook, updateAction).remove(); + INTERPOSE_HOOK(liquid_hook, getItemDescription).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, updateAction).apply(enable); + INTERPOSE_HOOK(workshop_hook, drawBuilding).apply(enable); } DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { switch (event) { case SC_MAP_LOADED: - find_engines(); - if (!engines.empty()) + if (find_engines()) { out.print("Detected steam engine workshops - enabling plugin.\n"); - enable_hooks(); + enable_hooks(true); } + else + enable_hooks(false); break; case SC_MAP_UNLOADED: - disable_hooks(); + enable_hooks(false); engines.clear(); break; default: @@ -344,6 +476,6 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector