From 382191cce80e4bb2a0735bc50e8385b6622127df Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 9 May 2025 15:43:39 -0700 Subject: [PATCH] record stockpile properties in blueprint --- docs/changelog.txt | 2 + plugins/blueprint.cpp | 347 ++++++++++++++++++++++---------------- plugins/logistics.cpp | 22 ++- plugins/lua/blueprint.lua | 16 ++ 4 files changed, 231 insertions(+), 156 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 0b824ae90d..abd1b845fc 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -69,6 +69,8 @@ Template for new versions: - All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. - `dig`: ASCII overlay now displays priority of digging designations - `spectate`: added prefer nicknamed units +- `blueprint`: support for recording zones +- `blueprint`: support for recording stockpile properties like names and stockpile links; does not yet support recording detailed contents configuration ## Documentation diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 7d2b656886..26caf25816 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -182,7 +182,12 @@ struct blueprint_processor { get_tile(get_tile), init_ctx(init_ctx) { } }; -// global caches, cleared when the string cache is cleared +// global caches, lazily initialized and cleared at the end of each blueprint +// this assumes that no two blueprints are being generated at the same time, +// which is currently ensured by the higher-level DFHack command handling code. +// if this assumption ever becomes untrue, we'll need to protect the caches +// with thread synchronization primitives or make the caches per-blueprint. +static std::set string_cache; static std::unordered_map engravings_cache; static std::unordered_map dig_job_cache; static PersistentDataItem warm_config, damp_config; @@ -214,6 +219,14 @@ static void init_caches(DFHack::color_ostream &out, bool cache_engravings) { }); } +static void clear_caches() { + string_cache.clear(); + engravings_cache.clear(); + dig_job_cache.clear(); + warm_config = PersistentDataItem(); + damp_config = PersistentDataItem(); +} + // We use const char * throughout this code instead of std::string to avoid // having to allocate memory for all the small string literals. This // significantly speeds up processing and allows us to handle very large maps @@ -222,19 +235,9 @@ static void init_caches(DFHack::color_ostream &out, bool cache_engravings) { // allocated until we write out the blueprints at the end. // If NULL is passed as the str, the cache is cleared. static const char * cache(const char *str) { - // this local static assumes that no two blueprints are being generated at - // the same time, which is currently ensured by the higher-level DFHack - // command handling code. if this assumption ever becomes untrue, we'll - // need to protect the cache with thread synchronization primitives or make - // the cache per-blueprint. - static std::set _cache; - if (!str) { - _cache.clear(); - engravings_cache.clear(); - dig_job_cache.clear(); + if (!str) return NULL; - } - return _cache.emplace(str).first->c_str(); + return string_cache.emplace(str).first->c_str(); } // Convenience wrapper for std::string. @@ -1024,21 +1027,18 @@ static const char * add_expansion_syntax(const df::building *bld, return cache(s); } +static void add_expansion_syntax(const df::building *bld, ostringstream &keys) { + if (keys.str().empty()) + return; + pair size = get_building_size(bld); + keys << "(" << size.first << "x" << size.second << ")"; +} + static const char * add_expansion_syntax(const tile_context &ctx, const char *keys) { return add_expansion_syntax(ctx.b, keys); } -static const char * add_label(const tile_context &ctx, const char *keys) { - if (!keys) - return "~"; - auto bld = ctx.b; - ostringstream s; - // use building's id as the unique label - s << keys << "/" << "bld_" << bld->id; - return cache(s); -} - static const char * get_tile_build(color_ostream &out, const df::coord &pos, const tile_context &ctx) { if (!ctx.b || ctx.b->getType() == building_type::Stockpile) { return NULL; @@ -1052,51 +1052,152 @@ static const char * get_tile_build(color_ostream &out, const df::coord &pos, con return add_expansion_syntax(ctx, keys); } -static const char * get_place_keys(const tile_context &ctx) { - df::building_stockpilest* sp = - virtual_cast(ctx.b); - if (!sp) { - return NULL; - } +static string quotify_inner(const string &s) { + if (s.find_first_of(" ,") == string::npos) + return s; + ostringstream buf; + buf << "\"\"" << s << "\"\""; + return buf.str(); +} + +static string quotify_outer(const string &s) { + if (s.find_first_of("\",") == string::npos) + return s; + ostringstream buf; + buf << "\"" << s << "\""; + return buf.str(); +} - string keys; +static string quotify_outer(const ostringstream &s) { + return quotify_outer(s.str()); +} + +static void get_place_keys(color_ostream &out, ostringstream &keys, df::building_stockpilest* sp, bool add_label, bool add_properties) { df::stockpile_group_set &flags = sp->settings.flags; - if (flags.bits.animals) keys += 'a'; - if (flags.bits.food) keys += 'f'; - if (flags.bits.furniture) keys += 'u'; - if (flags.bits.coins) keys += 'n'; - if (flags.bits.corpses) keys += 'y'; - if (flags.bits.refuse) keys += 'r'; - if (flags.bits.stone) keys += 's'; - if (flags.bits.wood) keys += 'w'; - if (flags.bits.gems) keys += 'e'; - if (flags.bits.bars_blocks) keys += 'b'; - if (flags.bits.cloth) keys += 'h'; - if (flags.bits.leather) keys += 'l'; - if (flags.bits.ammo) keys += 'z'; - if (flags.bits.sheet) keys += 'S'; - if (flags.bits.finished_goods) keys += 'g'; - if (flags.bits.weapons) keys += 'p'; - if (flags.bits.armor) keys += 'd'; - - if (keys.empty()) - return "c"; - return cache(keys); + if (flags.bits.animals) keys << 'a'; + if (flags.bits.food) keys << 'f'; + if (flags.bits.furniture) keys << 'u'; + if (flags.bits.coins) keys << 'n'; + if (flags.bits.corpses) keys << 'y'; + if (flags.bits.refuse) keys << 'r'; + if (flags.bits.stone) keys << 's'; + if (flags.bits.wood) keys << 'w'; + if (flags.bits.gems) keys << 'e'; + if (flags.bits.bars_blocks) keys << 'b'; + if (flags.bits.cloth) keys << 'h'; + if (flags.bits.leather) keys << 'l'; + if (flags.bits.ammo) keys << 'z'; + if (flags.bits.sheet) keys << 'S'; + if (flags.bits.finished_goods) keys << 'g'; + if (flags.bits.weapons) keys << 'p'; + if (flags.bits.armor) keys << 'd'; + + if (keys.str().empty()) + keys << 'c'; + + if (!add_label && !add_properties) + return; + + if (add_label) + keys << "/" << "sp_" << sp->id; + + if (!add_properties) + return; + + vector properties; + + if (!sp->name.empty()) + properties.push_back("name=" + quotify_inner(sp->name)); + + // only include take_from and give_to targets if they are named + vector take_from, give_to; + for (auto & target : sp->links.take_from_pile) { + if (target->name.empty()) + continue; + take_from.push_back(target->name); + } + for (auto & target : sp->links.take_from_workshop) { + if (target->name.empty()) + continue; + take_from.push_back(target->name); + } + for (auto & target : sp->links.give_to_pile) { + if (target->name.empty()) + continue; + give_to.push_back(target->name); + } + for (auto & target : sp->links.give_to_workshop) { + if (target->name.empty()) + continue; + give_to.push_back(target->name); + } + if (!take_from.empty()) + properties.push_back("take_from=" + quotify_inner(join_strings(",", take_from))); + if (!give_to.empty()) + properties.push_back("give_to=" + quotify_inner(join_strings(",", give_to))); + + if (sp->stockpile_flag.bits.use_links_only) + properties.push_back("links_only=true"); + + // simplify implementation; always record container counts, even if they are set to default values + if (!sp->storage.max_barrels && !sp->storage.max_bins && !sp->storage.max_wheelbarrows) + properties.push_back("containers=0"); + else { + properties.push_back("barrels=" + int_to_string(sp->storage.max_barrels)); + properties.push_back("bins=" + int_to_string(sp->storage.max_bins)); + properties.push_back("wheelbarrows=" + int_to_string(sp->storage.max_wheelbarrows)); + } + + // logistics features + Lua::CallLuaModuleFunction(out, "plugins.blueprint", "get_logistics_settings", + std::make_tuple(sp->stockpile_number), 6, [&](lua_State *L) { + if (lua_toboolean(L, -6)) properties.push_back("automelt=true"); + if (lua_toboolean(L, -5)) properties.push_back("autotrade=true"); + if (lua_toboolean(L, -4)) properties.push_back("autodump=true"); + if (lua_toboolean(L, -3)) properties.push_back("autotrain=true"); + if (lua_toboolean(L, -2)) properties.push_back("autoforbid=true"); + if (lua_toboolean(L, -1)) properties.push_back("autoclaim=true"); + }); + + if (!properties.empty()) + keys << '{' << join_strings(" ", properties) << '}'; +} + +static df::coord get_first_tile(df::building *bld) { + df::coord first_pos; + cuboid bld_area(bld->x1, bld->y1, bld->z, bld->x2, bld->y2, bld->z); + bld_area.forCoord([&](const df::coord &pos) { + if (Buildings::containsTile(bld, pos)) { + first_pos = pos; + return false; + } + return true; + }, true); + + return first_pos; } static const char * get_tile_place(color_ostream &out, const df::coord &pos, const tile_context &ctx) { - if (!ctx.b || ctx.b->getType() != building_type::Stockpile) + df::building_stockpilest* sp = virtual_cast(ctx.b); + + if (!sp || sp->getType() != building_type::Stockpile) return NULL; - if (!is_rectangular(ctx)) - return add_label(ctx, get_place_keys(ctx)); + bool rectangular = is_rectangular(sp); + bool is_first_tile = pos == get_first_tile(sp); + ostringstream keys; - if (ctx.b->x1 != static_cast(pos.x) - || ctx.b->y1 != static_cast(pos.y)) { - return if_pretty(ctx, "`"); + if (!rectangular){ + get_place_keys(out, keys, sp, true, is_first_tile); + return cache(quotify_outer(keys)); } - return add_expansion_syntax(ctx, get_place_keys(ctx)); + if (!is_first_tile) + return if_pretty(ctx, "`"); + + get_place_keys(out, keys, sp, false, true); + add_expansion_syntax(sp, keys); + return cache(quotify_outer(keys)); } static string get_reservation(color_ostream &out, df::building_civzonest *zone) { @@ -1110,12 +1211,11 @@ static string get_reservation(color_ostream &out, df::building_civzonest *zone) } // TODO: handle locations -static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zone, bool add_label, bool add_properties) { - const char * symbol = NULL; +static void get_zone_keys(color_ostream &out, ostringstream &keys, df::building_civzonest *zone, bool add_label, bool add_properties) { vector properties; if (!zone->name.empty()) - properties.push_back("name=" + zone->name); + properties.push_back(quotify_inner("name=" + zone->name)); if (!zone->spec_sub_flag.bits.active) properties.push_back("active=false"); if (auto reserved_for = get_reservation(out, zone); !reserved_for.empty()) { @@ -1125,26 +1225,26 @@ static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zo // in DFHack docs order switch (zone->type) { using namespace df::enums::civzone_type; - case MeetingHall: symbol = "m"; break; - case Bedroom: symbol = "b"; break; - case DiningHall: symbol = "h"; break; - case Pen: symbol = "n"; break; + case MeetingHall: keys << "m"; break; + case Bedroom: keys << "b"; break; + case DiningHall: keys << "h"; break; + case Pen: keys << "n"; break; case Pond: - symbol = "p"; + keys << "p"; { if (zone->zone_settings.pond.flag.bits.keep_filled) properties.push_back("pond=true"); } break; - case WaterSource: symbol = "w"; break; - case Dungeon: symbol = "j"; break; - case FishingArea: symbol = "f"; break; - case SandCollection: symbol = "s"; break; - case Office: symbol = "o"; break; - case Dormitory: symbol = "D"; break; - case Barracks: symbol = "B"; break; + case WaterSource: keys << "w"; break; + case Dungeon: keys << "j"; break; + case FishingArea: keys << "f"; break; + case SandCollection: keys << "s"; break; + case Office: keys << "o"; break; + case Dormitory: keys << "D"; break; + case Barracks: keys << "B"; break; case ArcheryRange: - symbol = "a"; + keys << "a"; { auto & archery = zone->zone_settings.archery; if (archery.dir_x == 1 && archery.dir_y == 0) @@ -1155,14 +1255,16 @@ static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zo properties.push_back("shoot_from=north"); else if (archery.dir_x == 0 && archery.dir_y == -1) properties.push_back("shoot_from=south"); - else - return NULL; // invalid direction + else { + keys.clear(); + return; // invalid direction + } } break; - case Dump: symbol = "d"; break; - case AnimalTraining: symbol = "t"; break; + case Dump: keys << "d"; break; + case AnimalTraining: keys << "t"; break; case Tomb: - symbol = "T"; + keys << "T"; { auto & tomb = zone->zone_settings.tomb; if (!tomb.flags.bits.no_pets) @@ -1172,7 +1274,7 @@ static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zo } break; case PlantGathering: - symbol = "g"; + keys << "g"; { auto & gather = zone->zone_settings.gather; if (!gather.flags.bits.pick_trees) @@ -1183,73 +1285,18 @@ static const char * get_zone_keys(color_ostream &out, df::building_civzonest *zo properties.push_back("gather_fallen=false"); } break; - case ClayCollection: symbol = "c"; break; + case ClayCollection: keys << "c"; break; default: - return NULL; + return; } if (!add_label && (!add_properties || properties.empty())) - return symbol; + return; - ostringstream keys; - keys << symbol; if (add_label) keys << "/" << "zone_" << zone->id; - if (add_properties) + if (add_properties && !properties.empty()) keys << '{' << join_strings(" ", properties) << '}'; - return cache(keys.str()); -} - -static df::coord get_first_tile(df::building_civzonest *zone) { - df::coord first_pos; - cuboid zone_area(zone->x1, zone->y1, zone->z, zone->x2, zone->y2, zone->z); - zone_area.forCoord([&](const df::coord &pos) { - if (Buildings::containsTile(zone, pos)) { - first_pos = pos; - return false; - } - return true; - }, true); - - return first_pos; -} - -static int32_t get_flood_size(const df::building::T_room &room, int32_t start_x, int32_t start_y) { - if (!room.extents) - return 0; - std::unordered_set visited; - std::queue to_visit; - to_visit.push(df::coord(start_x, start_y, 0)); - while (!to_visit.empty()) { - df::coord pos = to_visit.front(); - to_visit.pop(); - if (visited.count(pos)) - continue; - visited.insert(pos); - for (int32_t y = -1; y <= 1; ++y) { - for (int32_t x = -1; x <= 1; ++x) { - if (x == 0 && y == 0) - continue; - int32_t nx = pos.x + x; - int32_t ny = pos.y + y; - if (nx < 0 || ny < 0 || nx >= room.width || ny >= room.height) - continue; - if (room.extents[ny * room.width + nx]) - to_visit.push(df::coord(nx, ny, 0)); - } - } - } - // flood size is the number of tiles we visited - return (int32_t)visited.size(); -} - -static bool is_disjoint(df::building *bld, const df::coord &first_tile) { - const df::building::T_room &room = bld->room; - if (!room.extents) - return false; - - int32_t flood_size = get_flood_size(room, first_tile.x - bld->x1, first_tile.y - bld->y1); - return flood_size != Buildings::countExtentTiles(bld); } static const char * get_tile_zone(color_ostream &out, const df::coord &pos, const tile_context &ctx) { @@ -1263,9 +1310,9 @@ static const char * get_tile_zone(color_ostream &out, const df::coord &pos, cons // -- no two non-rectangular zones overlap // for a first implementation, we will only handle overlapping zones if they - // are rectangular. if this is the upper left corner of a rectangular zone, - // we will output for that zone. otherwise, if this pos is interior to - // all zones, then it doesn't matter which we choose. + // are rectangular and have different upper-left corners. if this is the upper + // left corner of a rectangular zone, we will output for that zone. otherwise, + // if this pos is interior to all zones, then it doesn't matter which we choose. df::building_civzonest * primary_zone = civzones[0]; df::coord upper_left_corner; @@ -1283,16 +1330,21 @@ static const char * get_tile_zone(color_ostream &out, const df::coord &pos, cons } } - bool disjoint = is_disjoint(primary_zone, upper_left_corner); + bool rectangular = is_rectangular(primary_zone); bool is_first_tile = pos == upper_left_corner; + ostringstream keys; - if (!is_rectangular(primary_zone)) - return get_zone_keys(out, primary_zone, disjoint, is_first_tile); + if (!rectangular) { + get_zone_keys(out, keys, primary_zone, true, is_first_tile); + return cache(quotify_outer(keys)); + } if (!is_first_tile) return if_pretty(ctx, "`"); - return add_expansion_syntax(primary_zone, get_zone_keys(out, primary_zone, disjoint, true)); + get_zone_keys(out, keys, primary_zone, false, true); + add_expansion_syntax(primary_zone, keys); + return cache(quotify_outer(keys)); } static bool create_output_dir(color_ostream &out, @@ -1647,8 +1699,7 @@ static command_result do_blueprint(color_ostream &out, bool ok = do_transform(out, start, end, options, files); - // clear caches - cache(NULL); + clear_caches(); return ok ? CR_OK : CR_FAILURE; } diff --git a/plugins/logistics.cpp b/plugins/logistics.cpp index bcb59b7ad5..90b31b3509 100644 --- a/plugins/logistics.cpp +++ b/plugins/logistics.cpp @@ -59,6 +59,12 @@ enum StockpileConfigValues { STOCKPILE_CONFIG_FORBID = 6, }; +enum StockpileConfigForbidValues { + STOCKPILE_CONFIG_FORBID_OFF = 0, + STOCKPILE_CONFIG_FORBID_FORBID = 1, + STOCKPILE_CONFIG_FORBID_CLAIM = 2, +}; + static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stockpile_number) { TRACE(control, out).print("ensuring stockpile config stockpile_number=%d\n", stockpile_number); if (watched_stockpiles.count(stockpile_number)) { @@ -76,7 +82,7 @@ static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stock c.set_bool(STOCKPILE_CONFIG_DUMP, false); c.set_bool(STOCKPILE_CONFIG_TRAIN, false); c.set_bool(STOCKPILE_CONFIG_MELT_MASTERWORKS, false); - c.set_int(STOCKPILE_CONFIG_FORBID, 0); + c.set_int(STOCKPILE_CONFIG_FORBID, STOCKPILE_CONFIG_FORBID_OFF); return c; } @@ -132,7 +138,7 @@ static void validate_stockpile_configs(color_ostream& out, !c.get_bool(STOCKPILE_CONFIG_TRADE) && !c.get_bool(STOCKPILE_CONFIG_DUMP) && !c.get_bool(STOCKPILE_CONFIG_TRAIN) && - !(c.get_int(STOCKPILE_CONFIG_FORBID) > 0))) { + !(c.get_int(STOCKPILE_CONFIG_FORBID) > STOCKPILE_CONFIG_FORBID_OFF))) { to_remove.push_back(stockpile_number); continue; } @@ -180,7 +186,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { if (c.key() == CONFIG_KEY) continue; if (c.get_int(STOCKPILE_CONFIG_FORBID) == -1) // remove this once saves from 51.01 are no longer compatible - c.set_int(STOCKPILE_CONFIG_FORBID, 0); + c.set_int(STOCKPILE_CONFIG_FORBID, STOCKPILE_CONFIG_FORBID_OFF); watched_stockpiles.emplace(c.get_int(STOCKPILE_CONFIG_STOCKPILE_NUMBER), c); } migrate_old_keys(out); @@ -560,8 +566,8 @@ static void do_cycle(color_ostream& out, bool trade = c.get_bool(STOCKPILE_CONFIG_TRADE); bool dump = c.get_bool(STOCKPILE_CONFIG_DUMP); bool train = c.get_bool(STOCKPILE_CONFIG_TRAIN); - bool forbid = 1 == c.get_int(STOCKPILE_CONFIG_FORBID); - bool claim = 2 == c.get_int(STOCKPILE_CONFIG_FORBID); + bool forbid = STOCKPILE_CONFIG_FORBID_FORBID == c.get_int(STOCKPILE_CONFIG_FORBID); + bool claim = STOCKPILE_CONFIG_FORBID_CLAIM == c.get_int(STOCKPILE_CONFIG_FORBID); MeltStockProcessor melt_stock_processor(stockpile_number, melt, melt_stats, melt_masterworks); TradeStockProcessor trade_stock_processor(stockpile_number, trade, trade_stats); @@ -645,8 +651,8 @@ static int logistics_getStockpileData(lua_State *L) { bool trade = c.get_bool(STOCKPILE_CONFIG_TRADE); bool dump = c.get_bool(STOCKPILE_CONFIG_DUMP); bool train = c.get_bool(STOCKPILE_CONFIG_TRAIN); - bool forbid = 1 == c.get_int(STOCKPILE_CONFIG_FORBID); - bool claim = 2 == c.get_int(STOCKPILE_CONFIG_FORBID); + bool forbid = STOCKPILE_CONFIG_FORBID_FORBID == c.get_int(STOCKPILE_CONFIG_FORBID); + bool claim = STOCKPILE_CONFIG_FORBID_CLAIM == c.get_int(STOCKPILE_CONFIG_FORBID); unordered_map sconfig; sconfig.emplace("melt", melt ? "true" : "false"); @@ -719,7 +725,7 @@ static unordered_map get_stockpile_config(int32_t stockpile_number) stockpile_config.emplace("trade", false); stockpile_config.emplace("dump", false); stockpile_config.emplace("train", false); - stockpile_config.emplace("forbid", 0); + stockpile_config.emplace("forbid", STOCKPILE_CONFIG_FORBID_OFF); } return stockpile_config; } diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index e43ce5f5f6..8c765d563f 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -1,6 +1,7 @@ local _ENV = mkmodule('plugins.blueprint') local argparse = require('argparse') +local logistics = require('plugins.logistics') local utils = require('utils') local valid_phase_list = { @@ -221,6 +222,21 @@ function get_filename(opts, phase, ordinal) return ('%s-%d-%s.csv'):format(fullname, ordinal, phase) end +function get_logistics_settings(stockpile_number) + local automelt, autotrade, autodump, autotrain, autoforbid, autoclaim = false, false, false, false, false, false + local configs = logistics.logistics_getStockpileConfigs(stockpile_number) + if configs and #configs == 1 then + local config = configs[1] + automelt = config.melt ~= 0 + autotrade = config.trade ~= 0 + autodump = config.dump ~= 0 + autotrain = config.train ~= 0 + autoforbid = config.forbid == 1 + autoclaim = config.forbid == 2 + end + return automelt, autotrade, autodump, autotrain, autoforbid, autoclaim +end + -- compatibility with old exported API. local function do_phase(start_pos, end_pos, name, phase) local width = math.abs(start_pos.x - end_pos.x) + 1