diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index f7586f03d..2ad1dfe81 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -136,6 +136,7 @@ if (BUILD_SUPPORTED) DFHACK_PLUGIN(misery misery.cpp) DFHACK_PLUGIN(mode mode.cpp) DFHACK_PLUGIN(mousequery mousequery.cpp) + DFHACK_PLUGIN(orders orders.cpp LINK_LIBRARIES jsoncpp) DFHACK_PLUGIN(petcapRemover petcapRemover.cpp) DFHACK_PLUGIN(plants plants.cpp) DFHACK_PLUGIN(probe probe.cpp) diff --git a/plugins/orders.cpp b/plugins/orders.cpp new file mode 100644 index 000000000..344f00e29 --- /dev/null +++ b/plugins/orders.cpp @@ -0,0 +1,788 @@ +#include "Console.h" +#include "Core.h" +#include "DataDefs.h" +#include "Export.h" +#include "PluginManager.h" + +#include "modules/Filesystem.h" +#include "modules/Materials.h" + +#include "jsoncpp.h" + +#include "df/building.h" +#include "df/historical_figure.h" +#include "df/itemdef_ammost.h" +#include "df/itemdef_armorst.h" +#include "df/itemdef_foodst.h" +#include "df/itemdef_glovesst.h" +#include "df/itemdef_helmst.h" +#include "df/itemdef_instrumentst.h" +#include "df/itemdef_pantsst.h" +#include "df/itemdef_shieldst.h" +#include "df/itemdef_shoesst.h" +#include "df/itemdef_siegeammost.h" +#include "df/itemdef_toolst.h" +#include "df/itemdef_toyst.h" +#include "df/itemdef_trapcompst.h" +#include "df/itemdef_weaponst.h" +#include "df/job_item.h" +#include "df/manager_order.h" +#include "df/manager_order_condition_item.h" +#include "df/manager_order_condition_order.h" +#include "df/world.h" + +using namespace DFHack; +using namespace df::enums; + +DFHACK_PLUGIN("orders"); + +REQUIRE_GLOBAL(world); + +static command_result orders_command(color_ostream & out, std::vector & parameters); + +DFhackCExport command_result plugin_init(color_ostream & out, std::vector & commands) +{ + commands.push_back(PluginCommand( + "orders", + "Manipulate manager orders.", + orders_command, + false, + "orders - Manipulate manager orders\n" + " orders export [name]\n" + " Exports the current list of manager orders to a file named dfhack-config/orders/[name].json.\n" + " orders import [name]\n" + " Imports manager orders from a file named dfhack-config/orders/[name].json.\n" + )); + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown(color_ostream & out) +{ + return CR_OK; +} + +static command_result orders_export_command(color_ostream & out, const std::string & name); +static command_result orders_import_command(color_ostream & out, const std::string & name); + +static command_result orders_command(color_ostream & out, std::vector & parameters) +{ + class color_ostream_resetter + { + color_ostream & out; + + public: + color_ostream_resetter(color_ostream & out) : out(out) {} + ~color_ostream_resetter() { out.reset_color(); } + } resetter(out); + + if (parameters.empty()) + { + return CR_WRONG_USAGE; + } + + if (parameters[0] == "export" && parameters.size() == 2) + { + return orders_export_command(out, parameters[1]); + } + + if (parameters[0] == "import" && parameters.size() == 2) + { + return orders_import_command(out, parameters[1]); + } + + return CR_WRONG_USAGE; +} + +static bool is_safe_filename(color_ostream & out, const std::string & name) +{ + if (name.empty()) + { + out << COLOR_LIGHTRED << "Missing filename!" << std::endl; + return false; + } + + for (auto it = name.begin(); it != name.end(); it++) + { + if (isalnum(*it)) + { + continue; + } + + if (*it != ' ' && *it != '.' && *it != '-' && *it != '_') + { + out << COLOR_LIGHTRED << "Invalid symbol in name: " << *it << std::endl; + return false; + } + } + + return true; +} + +template +static void bitfield_to_json_array(Json::Value & out, B bits) +{ + std::vector names; + bitfield_to_string(&names, bits); + + for (auto it = names.begin(); it != names.end(); it++) + { + out.append(*it); + } +} + +template +static void json_array_to_bitfield(B & bits, Json::Value & arr) +{ + if (arr.size() == 0) + { + return; + } + + for (Json::ArrayIndex i = arr.size() - 1; i != 0; i--) + { + int current; + if (get_bitfield_field(¤t, bits, arr[i].asString())) + { + if (!current && set_bitfield_field(&bits, arr[i].asString(), 1)) + { + Json::Value removed; + arr.removeIndex(i, &removed); + } + } + } +} + +template +static df::itemdef *get_itemdef(int16_t subtype) +{ + return D::find(subtype); +} + +template +static df::itemdef *get_itemdef(const std::string & subtype) +{ + for (auto it = D::get_vector().begin(); it != D::get_vector().end(); it++) + { + if ((*it)->id == subtype) + { + return *it; + } + } + return nullptr; +} + +template +static df::itemdef *get_itemdef(color_ostream & out, df::item_type type, ST subtype) +{ + switch (type) + { + case item_type::AMMO: + return get_itemdef(subtype); + case item_type::ARMOR: + return get_itemdef(subtype); + case item_type::FOOD: + return get_itemdef(subtype); + case item_type::GLOVES: + return get_itemdef(subtype); + case item_type::HELM: + return get_itemdef(subtype); + case item_type::INSTRUMENT: + return get_itemdef(subtype); + case item_type::PANTS: + return get_itemdef(subtype); + case item_type::SHIELD: + return get_itemdef(subtype); + case item_type::SHOES: + return get_itemdef(subtype); + case item_type::SIEGEAMMO: + return get_itemdef(subtype); + case item_type::TOOL: + return get_itemdef(subtype); + case item_type::TOY: + return get_itemdef(subtype); + case item_type::TRAPCOMP: + return get_itemdef(subtype); + case item_type::WEAPON: + return get_itemdef(subtype); + default: + out << COLOR_YELLOW << "Unhandled raw item type in manager order: " << enum_item_key(type) << "! Please report this bug to DFHack." << std::endl; + return nullptr; + } +} + +static command_result orders_export_command(color_ostream & out, const std::string & name) +{ + if (!is_safe_filename(out, name)) + { + return CR_WRONG_USAGE; + } + + Json::Value orders(Json::arrayValue); + + { + CoreSuspender suspend; + + for (auto it = world->manager_orders.begin(); it != world->manager_orders.end(); it++) + { + Json::Value order(Json::objectValue); + + order["id"] = (*it)->id; + order["job"] = enum_item_key((*it)->job_type); + if (!(*it)->reaction_name.empty()) + { + order["reaction"] = (*it)->reaction_name; + } + + if ((*it)->item_type != item_type::NONE) + { + order["item_type"] = enum_item_key((*it)->item_type); + } + if ((*it)->item_subtype != -1) + { + df::itemdef *def = get_itemdef(out, (*it)->item_type == item_type::NONE ? ENUM_ATTR(job_type, item, (*it)->job_type) : (*it)->item_type, (*it)->item_subtype); + + if (def) + { + order["item_subtype"] = def->id; + } + } + + if ((*it)->job_type == job_type::PrepareMeal) + { + order["meal_ingredients"] = (*it)->mat_type; + } + else if ((*it)->mat_type != -1 || (*it)->mat_index != -1) + { + order["material"] = MaterialInfo(*it).getToken(); + } + + if ((*it)->item_category.whole != 0) + { + bitfield_to_json_array(order["item_category"], (*it)->item_category); + } + + if ((*it)->hist_figure_id != -1) + { + order["hist_figure"] = (*it)->hist_figure_id; + } + + if ((*it)->material_category.whole != 0) + { + bitfield_to_json_array(order["material_category"], (*it)->material_category); + } + + if ((*it)->art_spec.type != df::job_art_specification::None) + { + Json::Value art(Json::objectValue); + + art["type"] = enum_item_key((*it)->art_spec.type); + art["id"] = (*it)->art_spec.id; + if ((*it)->art_spec.subid != -1) + { + art["subid"] = (*it)->art_spec.subid; + } + + order["art"] = art; + } + + order["amount_left"] = (*it)->amount_left; + order["amount_total"] = (*it)->amount_total; + order["is_validated"] = bool((*it)->status.bits.validated); + order["is_active"] = bool((*it)->status.bits.active); + + order["frequency"] = enum_item_key((*it)->frequency); + + // TODO: finished_year, finished_year_tick + + if ((*it)->workshop_id != -1) + { + order["workshop_id"] = (*it)->workshop_id; + } + + if ((*it)->max_workshops != 0) + { + order["max_workshops"] = (*it)->max_workshops; + } + + if (!(*it)->item_conditions.empty()) + { + Json::Value conditions(Json::arrayValue); + + for (auto it2 = (*it)->item_conditions.begin(); it2 != (*it)->item_conditions.end(); it2++) + { + Json::Value condition(Json::objectValue); + + condition["condition"] = enum_item_key((*it2)->compare_type); + condition["value"] = (*it2)->compare_val; + + if ((*it2)->flags1.whole != 0 || (*it2)->flags2.whole != 0 || (*it2)->flags3.whole != 0) + { + bitfield_to_json_array(condition["flags"], (*it2)->flags1); + bitfield_to_json_array(condition["flags"], (*it2)->flags2); + bitfield_to_json_array(condition["flags"], (*it2)->flags3); + // TODO: flags4, flags5 + } + + if ((*it2)->item_type != item_type::NONE) + { + condition["item_type"] = enum_item_key((*it2)->item_type); + } + if ((*it2)->item_subtype != -1) + { + df::itemdef *def = get_itemdef(out, (*it2)->item_type, (*it2)->item_subtype); + + if (def) + { + condition["item_subtype"] = def->id; + } + } + + if ((*it2)->mat_type != -1 || (*it2)->mat_index != -1) + { + condition["material"] = MaterialInfo(*it2).getToken(); + } + + if ((*it2)->inorganic_bearing != -1) + { + condition["bearing"] = df::inorganic_raw::find((*it2)->inorganic_bearing)->id; + } + + if (!(*it2)->reaction_class.empty()) + { + condition["reaction_class"] = (*it2)->reaction_class; + } + + if (!(*it2)->has_material_reaction_product.empty()) + { + condition["reaction_product"] = (*it2)->has_material_reaction_product; + } + + if ((*it2)->has_tool_use != tool_uses::NONE) + { + condition["tool"] = enum_item_key((*it2)->has_tool_use); + } + + // TODO: anon_1, anon_2, anon_3 + + conditions.append(condition); + } + + order["item_conditions"] = conditions; + } + + if (!(*it)->order_conditions.empty()) + { + Json::Value conditions(Json::arrayValue); + + for (auto it2 = (*it)->order_conditions.begin(); it2 != (*it)->order_conditions.end(); it2++) + { + Json::Value condition(Json::objectValue); + + condition["order"] = (*it2)->order_id; + condition["condition"] = enum_item_key((*it2)->condition); + + // TODO: anon_1 + + conditions.append(condition); + } + + order["order_conditions"] = conditions; + } + + // TODO: anon_1 + + orders.append(order); + } + } + + Filesystem::mkdir("dfhack-config/orders"); + + std::ofstream file("dfhack-config/orders/" + name + ".json"); + + file << orders << std::endl; + + return file.good() ? CR_OK : CR_FAILURE; +} + +static command_result orders_import_command(color_ostream & out, const std::string & name) +{ + if (!is_safe_filename(out, name)) + { + return CR_WRONG_USAGE; + } + + Json::Value orders; + + { + std::ifstream file("dfhack-config/orders/" + name + ".json"); + + if (!file.good()) + { + out << COLOR_LIGHTRED << "Cannot find orders file." << std::endl; + return CR_FAILURE; + } + + file >> orders; + + if (!file.good()) + { + out << COLOR_LIGHTRED << "Error reading orders file." << std::endl; + return CR_FAILURE; + } + } + + if (orders.type() != Json::arrayValue) + { + out << COLOR_LIGHTRED << "Invalid orders file: expected array" << std::endl; + return CR_FAILURE; + } + + CoreSuspender suspend; + + for (auto it = world->manager_orders.begin(); it != world->manager_orders.end(); it++) + { + for (auto it2 = (*it)->item_conditions.begin(); it2 != (*it)->item_conditions.end(); it2++) + { + delete *it2; + } + for (auto it2 = (*it)->order_conditions.begin(); it2 != (*it)->order_conditions.end(); it2++) + { + delete *it2; + } + if ((*it)->anon_1) + { + for (auto it2 = (*it)->anon_1->begin(); it2 != (*it)->anon_1->end(); it2++) + { + delete *it2; + } + delete (*it)->anon_1; + } + + delete *it; + } + world->manager_orders.clear(); + world->manager_order_next_id = 0; + + for (auto it = orders.begin(); it != orders.end(); it++) + { + df::manager_order *order = new df::manager_order(); + + order->id = (*it)["id"].asInt(); + world->manager_order_next_id = order->id + 1; + + if (!find_enum_item(&order->job_type, (*it)["job"].asString())) + { + delete order; + + out << COLOR_LIGHTRED << "Invalid job type for imported manager order: " << (*it)["job"].asString() << std::endl; + + return CR_FAILURE; + } + + if (it->isMember("reaction")) + { + order->reaction_name = (*it)["reaction"].asString(); + } + + if (it->isMember("item_type")) + { + if (!find_enum_item(&order->item_type, (*it)["item_type"].asString()) || order->item_type == item_type::NONE) + { + delete order; + + out << COLOR_LIGHTRED << "Invalid item type for imported manager order: " << (*it)["item_type"].asString() << std::endl; + + return CR_FAILURE; + } + } + if (it->isMember("item_subtype")) + { + df::itemdef *def = get_itemdef(out, order->item_type == item_type::NONE ? ENUM_ATTR(job_type, item, order->job_type) : order->item_type, (*it)["item_subtype"].asString()); + + if (def) + { + order->item_subtype = def->subtype; + } + else + { + delete order; + + out << COLOR_LIGHTRED << "Invalid item subtype for imported manager order: " << enum_item_key(order->item_type) << ":" << (*it)["item_subtype"].asString() << std::endl; + + return CR_FAILURE; + } + } + + if (it->isMember("meal_ingredients")) + { + order->mat_type = (*it)["meal_ingredients"].asInt(); + order->mat_index = -1; + } + else if (it->isMember("material")) + { + MaterialInfo mat; + if (!mat.find((*it)["material"].asString())) + { + delete order; + + out << COLOR_LIGHTRED << "Invalid material for imported manager order: " << (*it)["material"].asString() << std::endl; + + return CR_FAILURE; + } + order->mat_type = mat.type; + order->mat_index = mat.index; + } + + if (it->isMember("item_category")) + { + json_array_to_bitfield(order->item_category, (*it)["item_category"]); + if (!(*it)["item_category"].empty()) + { + delete order; + + out << COLOR_LIGHTRED << "Invalid item_category value for imported manager order: " << (*it)["item_category"] << std::endl; + + return CR_FAILURE; + } + } + + if (it->isMember("hist_figure")) + { + if (!df::historical_figure::find((*it)["hist_figure"].asInt())) + { + delete order; + + out << COLOR_YELLOW << "Missing historical figure for imported manager order: " << (*it)["hist_figure"].asInt() << std::endl; + + continue; + } + + order->hist_figure_id = (*it)["hist_figure"].asInt(); + } + + if (it->isMember("material_category")) + { + json_array_to_bitfield(order->material_category, (*it)["material_category"]); + if (!(*it)["material_category"].empty()) + { + delete order; + + out << COLOR_LIGHTRED << "Invalid material_category value for imported manager order: " << (*it)["material_category"] << std::endl; + + return CR_FAILURE; + } + } + + if (it->isMember("art")) + { + if (!find_enum_item(&order->art_spec.type, (*it)["art"]["type"].asString())) + { + delete order; + + out << COLOR_LIGHTRED << "Invalid art type value for imported manager order: " << (*it)["art"]["type"].asString() << std::endl; + + return CR_FAILURE; + } + + order->art_spec.id = (*it)["art"]["id"].asInt(); + if ((*it)["art"].isMember("subid")) + { + order->art_spec.subid = (*it)["art"]["subid"].asInt(); + } + } + + order->amount_left = (*it)["amount_left"].asInt(); + order->amount_total = (*it)["amount_total"].asInt(); + order->status.bits.validated = (*it)["is_validated"].asBool(); + order->status.bits.active = (*it)["is_active"].asBool(); + + if (!find_enum_item(&order->frequency, (*it)["frequency"].asString())) + { + delete order; + + out << COLOR_LIGHTRED << "Invalid frequency value for imported manager order: " << (*it)["frequency"].asString() << std::endl; + + return CR_FAILURE; + } + + // TODO: finished_year, finished_year_tick + + if (it->isMember("workshop_id")) + { + if (!df::building::find((*it)["workshop_id"].asInt())) + { + delete order; + + out << COLOR_YELLOW << "Missing workshop for imported manager order: " << (*it)["workshop_id"].asInt() << std::endl; + + continue; + } + + order->workshop_id = (*it)["workshop_id"].asInt(); + } + + if (it->isMember("max_workshops")) + { + order->max_workshops = (*it)["max_workshops"].asInt(); + } + + if (it->isMember("item_conditions")) + { + for (auto it2 = (*it)["item_conditions"].begin(); it2 != (*it)["item_conditions"].end(); it2++) + { + df::manager_order_condition_item *condition = new df::manager_order_condition_item(); + + if (!find_enum_item(&condition->compare_type, (*it2)["condition"].asString())) + { + delete condition; + + out << COLOR_YELLOW << "Invalid item condition condition for imported manager order: " << (*it2)["condition"].asString() << std::endl; + + continue; + } + + condition->compare_val = (*it2)["value"].asInt(); + + if (it2->isMember("flags")) + { + json_array_to_bitfield(condition->flags1, (*it2)["flags"]); + json_array_to_bitfield(condition->flags2, (*it2)["flags"]); + json_array_to_bitfield(condition->flags3, (*it2)["flags"]); + // TODO: flags4, flags5 + + if (!(*it2)["flags"].empty()) + { + delete condition; + + out << COLOR_YELLOW << "Invalid item condition flags for imported manager order: " << (*it2)["flags"] << std::endl; + + continue; + } + } + + if (it2->isMember("item_type")) + { + if (!find_enum_item(&condition->item_type, (*it2)["item_type"].asString()) || condition->item_type == item_type::NONE) + { + delete condition; + + out << COLOR_YELLOW << "Invalid item condition item type for imported manager order: " << (*it2)["item_type"].asString() << std::endl; + + continue; + } + } + if (it2->isMember("item_subtype")) + { + df::itemdef *def = get_itemdef(out, condition->item_type, (*it2)["item_subtype"].asString()); + + if (def) + { + condition->item_subtype = def->subtype; + } + else + { + delete condition; + + out << COLOR_YELLOW << "Invalid item condition item subtype for imported manager order: " << enum_item_key(condition->item_type) << ":" << (*it2)["item_subtype"].asString() << std::endl; + + continue; + } + } + + if (it2->isMember("material")) + { + MaterialInfo mat; + if (!mat.find((*it2)["material"].asString())) + { + delete condition; + + out << COLOR_YELLOW << "Invalid item condition material for imported manager order: " << (*it2)["material"].asString() << std::endl; + + continue; + } + condition->mat_type = mat.type; + condition->mat_index = mat.index; + } + + if (it2->isMember("bearing")) + { + std::string bearing((*it2)["bearing"].asString()); + auto found = std::find_if(world->raws.inorganics.begin(), world->raws.inorganics.end(), [bearing](df::inorganic_raw *raw) -> bool { return raw->id == bearing; }); + if (found == world->raws.inorganics.end()) + { + delete condition; + + out << COLOR_YELLOW << "Invalid item condition inorganic bearing type for imported manager order: " << (*it2)["bearing"].asString() << std::endl; + + continue; + } + condition->inorganic_bearing = found - world->raws.inorganics.begin(); + } + + if (it2->isMember("reaction_class")) + { + condition->reaction_class = (*it2)["reaction_class"].asString(); + } + + if (it2->isMember("reaction_product")) + { + condition->has_material_reaction_product = (*it2)["reaction_product"].asString(); + } + + if (it2->isMember("tool")) + { + if (!find_enum_item(&condition->has_tool_use, (*it2)["tool"].asString()) || condition->has_tool_use == tool_uses::NONE) + { + delete condition; + + out << COLOR_YELLOW << "Invalid item condition tool use for imported manager order: " << (*it2)["tool"].asString() << std::endl; + + continue; + } + } + + // TODO: anon_1, anon_2, anon_3 + + order->item_conditions.push_back(condition); + } + } + + if (it->isMember("order_conditions")) + { + for (auto it2 = (*it)["order_conditions"].begin(); it2 != (*it)["order_conditions"].end(); it2++) + { + df::manager_order_condition_order *condition = new df::manager_order_condition_order(); + + int32_t id = (*it2)["order"].asInt(); + if (id == order->id || std::find_if(orders.begin(), orders.end(), [id](const Json::Value & o) -> bool { return o["id"].asInt() == id; }) == orders.end()) + { + delete condition; + + out << COLOR_YELLOW << "Missing order condition target for imported manager order: " << (*it2)["order"].asInt() << std::endl; + + continue; + } + condition->order_id = id; + + if (!find_enum_item(&condition->condition, (*it2)["condition"].asString())) + { + delete condition; + + out << COLOR_YELLOW << "Invalid order condition type for imported manager order: " << (*it2)["condition"].asString() << std::endl; + + continue; + } + + // TODO: anon_1 + + order->order_conditions.push_back(condition); + } + } + + // TODO: anon_1 + + world->manager_orders.push_back(order); + } + + return CR_OK; +}