From 5c84d180011c06f4f4f36032f023984953aa0044 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 6 Feb 2023 18:38:16 -0800 Subject: [PATCH] update tailor, persist state, use best practices --- docs/plugins/tailor.rst | 28 ++- plugins/CMakeLists.txt | 2 +- plugins/lua/tailor.lua | 56 +++++ plugins/tailor.cpp | 527 +++++++++++++++++++++------------------- 4 files changed, 350 insertions(+), 263 deletions(-) create mode 100644 plugins/lua/tailor.lua diff --git a/docs/plugins/tailor.rst b/docs/plugins/tailor.rst index 4dc4f53a4..0e8980948 100644 --- a/docs/plugins/tailor.rst +++ b/docs/plugins/tailor.rst @@ -5,16 +5,15 @@ tailor :summary: Automatically keep your dwarves in fresh clothing. :tags: fort auto workorders -Whenever the bookkeeper updates stockpile records, this plugin will scan the -fort. If there are fresh cloths available, dwarves who are wearing tattered -clothing will have their rags confiscated (in the same manner as the -`cleanowned` tool) so that they'll reequip with replacement clothes. +Once a day, this plugin will scan the clothing situation in the fort. If there +are fresh cloths available, dwarves who are wearing tattered clothing will have +their rags confiscated (in the same manner as the `cleanowned` tool) so that +they'll reequip with replacement clothes. -If there are not enough clothes available, manager orders will be generated -to manufacture some more. ``tailor`` will intelligently create orders using -raw materials that you have on hand in the fort. For example, if you have -lots of silk, but no cloth, then ``tailor`` will order only silk clothing to -be made. +If there are not enough clothes available, manager orders will be generated to +manufacture some more. ``tailor`` will intelligently create orders using raw +materials that you have on hand in the fort. For example, if you have lots of +silk, but no cloth, then ``tailor`` will order only silk clothing to be made. Usage ----- @@ -22,7 +21,8 @@ Usage :: enable tailor - tailor status + tailor [status] + tailor now tailor materials [ ...] By default, ``tailor`` will prefer using materials in this order:: @@ -32,12 +32,16 @@ By default, ``tailor`` will prefer using materials in this order:: but you can use the ``tailor materials`` command to restrict which materials are used, and in what order. -Example -------- +Examples +-------- ``enable tailor`` Start replacing tattered clothes with default settings. +``tailor now`` + Run a scan and order cycle right now, regardless of whether the plugin is + enabled. + ``tailor materials silk cloth yarn`` Restrict the materials used for automatically manufacturing clothing to silk, cloth, and yarn, preferred in that order. This saves leather for diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index df49c3d59..7e8258aeb 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -159,7 +159,7 @@ dfhack_plugin(showmood showmood.cpp) #add_subdirectory(stockpiles) #dfhack_plugin(stocks stocks.cpp) #dfhack_plugin(strangemood strangemood.cpp) -dfhack_plugin(tailor tailor.cpp) +dfhack_plugin(tailor tailor.cpp LINK_LIBRARIES lua) dfhack_plugin(tiletypes tiletypes.cpp Brushes.h LINK_LIBRARIES lua) #dfhack_plugin(title-folder title-folder.cpp) #dfhack_plugin(title-version title-version.cpp) diff --git a/plugins/lua/tailor.lua b/plugins/lua/tailor.lua new file mode 100644 index 000000000..5c748fbfa --- /dev/null +++ b/plugins/lua/tailor.lua @@ -0,0 +1,56 @@ +local _ENV = mkmodule('plugins.tailor') + +local argparse = require('argparse') +local utils = require('utils') + +local function process_args(opts, args) + if args[1] == 'help' then + opts.help = true + return + end + + return argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() opts.help = true end}, + }) +end + +function status() + print(('tailor is %s'):format(enabled and "enabled" or "disabled")) + print('materials preference order:') + for _,name in ipairs(tailor_getMaterialPreferences()) do + print((' %s'):format(name)) + end +end + +function setMaterials(names) + local idxs = utils.invert(names) + tailor_setMaterialPreferences( + idxs.silk or -1, + idxs.cloth or -1, + idxs.yarn or -1, + idxs.leather or -1) +end + +function parse_commandline(...) + local args, opts = {...}, {} + local positionals = process_args(opts, args) + + if opts.help then + return false + end + + local command = table.remove(positionals, 1) + if not command or command == 'status' then + status() + elseif command == 'now' then + tailor_doCycle() + elseif command == 'materials' then + setMaterials(positionals) + else + return false + end + + return true +end + +return _ENV diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index 2cca8a2c8..b35f434b8 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -1,136 +1,161 @@ /* * Tailor plugin. Automatically manages keeping your dorfs clothed. - * For best effect, place "tailor enable" in your dfhack.init configuration, - * or set AUTOENABLE to true. */ -#include "Core.h" -#include "DataDefs.h" -#include "Debug.h" -#include "PluginManager.h" +#include +#include +#include #include "df/creature_raw.h" -#include "df/global_objects.h" #include "df/historical_entity.h" +#include "df/item.h" +#include "df/item_flags.h" #include "df/itemdef_armorst.h" #include "df/itemdef_glovesst.h" #include "df/itemdef_helmst.h" #include "df/itemdef_pantsst.h" #include "df/itemdef_shoesst.h" #include "df/items_other_id.h" -#include "df/job.h" -#include "df/job_type.h" #include "df/manager_order.h" #include "df/plotinfost.h" #include "df/world.h" -#include "modules/Maps.h" -#include "modules/Units.h" +#include "Core.h" +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" + +#include "modules/Materials.h" +#include "modules/Persistence.h" #include "modules/Translation.h" +#include "modules/Units.h" #include "modules/World.h" -using namespace DFHack; +using std::string; +using std::vector; -using df::global::world; -using df::global::plotinfo; +using namespace DFHack; DFHACK_PLUGIN("tailor"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); -#define AUTOENABLE false -DFHACK_PLUGIN_IS_ENABLED(enabled); - -REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(standing_orders_use_dyed_cloth); +REQUIRE_GLOBAL(world); namespace DFHack { DBG_DECLARE(tailor, cycle, DebugCategory::LINFO); DBG_DECLARE(tailor, config, DebugCategory::LINFO); } -class Tailor { - // ARMOR, SHOES, HELM, GLOVES, PANTS +static const string CONFIG_KEY = string(plugin_name) + "/config"; +static PersistentDataItem config; - // ah, if only STL had a bimap +enum ConfigValues { + CONFIG_IS_ENABLED = 0, + CONFIG_SILK_IDX = 1, + CONFIG_CLOTH_IDX = 2, + CONFIG_YARN_IDX = 3, + CONFIG_LEATHER_IDX = 4, +}; -private: +static int get_config_val(PersistentDataItem &c, int index) { + if (!c.isValid()) + return -1; + return c.ival(index); +} +static bool get_config_bool(PersistentDataItem &c, int index) { + return get_config_val(c, index) == 1; +} +static void set_config_val(PersistentDataItem &c, int index, int value) { + if (c.isValid()) + c.ival(index) = value; +} +static void set_config_bool(PersistentDataItem &c, int index, bool value) { + set_config_val(c, index, value ? 1 : 0); +} - const std::map jobTypeMap = { - { df::job_type::MakeArmor, df::item_type::ARMOR }, - { df::job_type::MakePants, df::item_type::PANTS }, - { df::job_type::MakeHelm, df::item_type::HELM }, - { df::job_type::MakeGloves, df::item_type::GLOVES }, - { df::job_type::MakeShoes, df::item_type::SHOES } - }; - - const std::map itemTypeMap = { - { df::item_type::ARMOR, df::job_type::MakeArmor }, - { df::item_type::PANTS, df::job_type::MakePants }, - { df::item_type::HELM, df::job_type::MakeHelm }, - { df::item_type::GLOVES, df::job_type::MakeGloves }, - { df::item_type::SHOES, df::job_type::MakeShoes } - }; - -#define F(x) df::item_flags::mask_##x - const df::item_flags bad_flags = { - ( - F(dump) | F(forbid) | F(garbage_collect) | - F(hostile) | F(on_fire) | F(rotten) | F(trader) | - F(in_building) | F(construction) | F(owned) - ) - #undef F - }; - - class MatType { - - public: - std::string name; - df::job_material_category job_material; - df::armor_general_flags armor_flag; - - bool operator==(const MatType& m) const - { - return name == m.name; - } +static const int32_t CYCLE_TICKS = 1200; // one day +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle - // operator< is required to use this as a std::map key - bool operator<(const MatType& m) const - { - return name < m.name; - } +// ah, if only STL had a bimap +static const std::map jobTypeMap = { + { df::job_type::MakeArmor, df::item_type::ARMOR }, + { df::job_type::MakePants, df::item_type::PANTS }, + { df::job_type::MakeHelm, df::item_type::HELM }, + { df::job_type::MakeGloves, df::item_type::GLOVES }, + { df::job_type::MakeShoes, df::item_type::SHOES } +}; - MatType(std::string& n, df::job_material_category jm, df::armor_general_flags af) - : name(n), job_material(jm), armor_flag(af) {}; - MatType(const char* n, df::job_material_category jm, df::armor_general_flags af) - : name(std::string(n)), job_material(jm), armor_flag(af) {}; +static const std::map itemTypeMap = { + { df::item_type::ARMOR, df::job_type::MakeArmor }, + { df::item_type::PANTS, df::job_type::MakePants }, + { df::item_type::HELM, df::job_type::MakeHelm }, + { df::item_type::GLOVES, df::job_type::MakeGloves }, + { df::item_type::SHOES, df::job_type::MakeShoes } +}; + +class MatType { +public: + const std::string name; + const df::job_material_category job_material; + const df::armor_general_flags armor_flag; + + bool operator==(const MatType& m) const { + return name == m.name; + } - }; + // operator< is required to use this as a std::map key + bool operator<(const MatType& m) const { + return name < m.name; + } - const MatType - M_SILK = MatType("silk", df::job_material_category::mask_silk, df::armor_general_flags::SOFT), - M_CLOTH = MatType("cloth", df::job_material_category::mask_cloth, df::armor_general_flags::SOFT), - M_YARN = MatType("yarn", df::job_material_category::mask_yarn, df::armor_general_flags::SOFT), - M_LEATHER = MatType("leather", df::job_material_category::mask_leather, df::armor_general_flags::LEATHER); + MatType(std::string& n, df::job_material_category jm, df::armor_general_flags af) + : name(n), job_material(jm), armor_flag(af) {}; + MatType(const char* n, df::job_material_category jm, df::armor_general_flags af) + : name(std::string(n)), job_material(jm), armor_flag(af) {}; +}; - std::list all_materials = { M_SILK, M_CLOTH, M_YARN, M_LEATHER }; +static const MatType + M_SILK = MatType("silk", df::job_material_category::mask_silk, df::armor_general_flags::SOFT), + M_CLOTH = MatType("cloth", df::job_material_category::mask_cloth, df::armor_general_flags::SOFT), + M_YARN = MatType("yarn", df::job_material_category::mask_yarn, df::armor_general_flags::SOFT), + M_LEATHER = MatType("leather", df::job_material_category::mask_leather, df::armor_general_flags::LEATHER); + +static const std::list all_materials = { M_SILK, M_CLOTH, M_YARN, M_LEATHER }; +static std::list material_order = all_materials; + +static struct BadFlags { + uint32_t whole; + + BadFlags() { + df::item_flags flags; + #define F(x) flags.bits.x = true; + F(dump); F(forbid); F(garbage_collect); + F(hostile); F(on_fire); F(rotten); F(trader); + F(in_building); F(construction); F(owned); + F(in_chest); F(removed); F(encased); + F(spider_web); + #undef F + whole = flags.whole; + } +} badFlags; +class Tailor { +private: std::map, int> available; // key is item type & size std::map, int> needed; // same std::map, int> queued; // same std::map sizes; // this maps body size to races - std::map, int> orders; // key is item type, item subtype, size std::map supply; - - color_ostream* out; - - std::list material_order = { M_SILK, M_CLOTH, M_YARN, M_LEATHER }; std::map reserves; int default_reserve = 10; +public: void reset() { available.clear(); @@ -145,9 +170,7 @@ private: { for (auto i : world->items.other[df::items_other_id::ANY_GENERIC37]) // GENERIC37 is "clothing" { - if (i->flags.whole & bad_flags.whole) - continue; - if (i->flags.bits.owned) + if (i->flags.whole & badFlags.whole) continue; if (i->getWear() >= 1) continue; @@ -164,7 +187,7 @@ private: for (auto i : world->items.other[df::items_other_id::CLOTH]) { - if (i->flags.whole & bad_flags.whole) + if (i->flags.whole & badFlags.whole) continue; if (require_dyed && !i->hasImprovements()) @@ -197,7 +220,7 @@ private: for (auto i : world->items.other[df::items_other_id::SKIN_TANNED]) { - if (i->flags.whole & bad_flags.whole) + if (i->flags.whole & badFlags.whole) continue; supply[M_LEATHER] += i->getStackSize(); } @@ -369,8 +392,9 @@ private: } - void place_orders() + int place_orders() { + int ordered = 0; auto entity = world->entities.all[plotinfo->civ_id]; for (auto& o : orders) @@ -477,6 +501,7 @@ private: ); count -= c; + ordered += c; } else { @@ -486,215 +511,217 @@ private: } } } + return ordered; } +}; -public: - void do_scan(color_ostream& o) - { - out = &o; - - reset(); - - // scan for useable clothing +static std::unique_ptr tailor_instance; - scan_clothing(); +static command_result do_command(color_ostream &out, vector ¶meters); +static int do_cycle(color_ostream &out); - // scan for clothing raw materials +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(config,out).print("initializing %s\n", plugin_name); - scan_materials(); + tailor_instance = dts::make_unique(); - // scan for units who need replacement clothing + // provide a configuration interface for the plugin + commands.push_back(PluginCommand( + plugin_name, + "Automatically keep your dwarves in fresh clothing.", + do_command)); - scan_replacements(); + return CR_OK; +} - // create new orders +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot enable %s without a loaded world.\n", plugin_name); + return CR_FAILURE; + } - create_orders(); + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(config,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); + if (enable) + do_cycle(out); + } else { + DEBUG(config,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); + } + return CR_OK; +} - // scan existing orders and subtract +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(config,out).print("shutting down %s\n", plugin_name); - scan_existing_orders(); + tailor_instance.release(); - // place orders + return CR_OK; +} - place_orders(); +static void set_material_order() { + material_order.clear(); + for (int i = 0; i < all_materials.size(); ++i) { + if (i == get_config_val(config, CONFIG_SILK_IDX)) + material_order.push_back(M_SILK); + else if (i == get_config_val(config, CONFIG_CLOTH_IDX)) + material_order.push_back(M_CLOTH); + else if (i == get_config_val(config, CONFIG_YARN_IDX)) + material_order.push_back(M_YARN); + else if (i == get_config_val(config, CONFIG_LEATHER_IDX)) + material_order.push_back(M_LEATHER); } + if (!material_order.size()) + std::copy(all_materials.begin(), all_materials.end(), std::back_inserter(material_order)); +} -public: - command_result set_materials(color_ostream& out, std::vector& parameters) - { - std::list newmat; - newmat.clear(); - - for (auto m = parameters.begin() + 1; m != parameters.end(); m++) - { - auto nameMatch = [m](MatType& m1) { return *m == m1.name; }; - auto mm = std::find_if(all_materials.begin(), all_materials.end(), nameMatch); - if (mm == all_materials.end()) - { - WARN(config,out).print("tailor: material %s not recognized\n", m->c_str()); - return CR_WRONG_USAGE; - } - else { - newmat.push_back(*mm); - } - } - - material_order = newmat; - INFO(config,out).print("tailor: material list set to %s\n", get_material_list().c_str()); +DFhackCExport command_result plugin_load_data (color_ostream &out) { + cycle_timestamp = 0; + config = World::GetPersistentData(CONFIG_KEY); - return CR_OK; + if (!config.isValid()) { + DEBUG(config,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); + set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); } -public: - std::string get_material_list() - { - std::string s; - for (const auto& m : material_order) - { - if (!s.empty()) s += ", "; - s += m.name; - } - return s; - } + is_enabled = get_config_bool(config, CONFIG_IS_ENABLED); + DEBUG(config,out).print("loading persisted enabled state: %s\n", + is_enabled ? "true" : "false"); + set_material_order(); -public: - void process(color_ostream& out) - { - bool found = false; + return CR_OK; +} - for (df::job_list_link* link = &world->jobs.list; link != NULL; link = link->next) - { - if (link->item == NULL) continue; - if (link->item->job_type == df::enums::job_type::UpdateStockpileRecords) - { - found = true; - break; - } +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED) { + if (is_enabled) { + DEBUG(config,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; } + } + return CR_OK; +} - if (found) - { - do_scan(out); - } +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (is_enabled && world->frame_counter - cycle_timestamp >= CYCLE_TICKS) { + int ordered = do_cycle(out); + if (0 < ordered) + out.print("tailor: ordered %d items of clothing\n", ordered); } -}; + return CR_OK; +} -static std::unique_ptr tailor_instance; +static bool call_tailor_lua(color_ostream *out, const char *fn_name, + int nargs = 0, int nres = 0, + Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, + Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { + DEBUG(config).print("calling tailor lua function: '%s'\n", fn_name); -#define DELTA_TICKS 50 + CoreSuspender guard; -DFhackCExport command_result plugin_onupdate(color_ostream& out) -{ - if (!enabled || !tailor_instance) - return CR_OK; + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); - if (!Maps::IsValid()) - return CR_OK; + if (!out) + out = &Core::getInstance().getConsole(); - if (DFHack::World::ReadPauseState()) - return CR_OK; + return Lua::CallLuaModuleFunction(*out, L, "plugins.tailor", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); +} - if (world->frame_counter % DELTA_TICKS != 0) - return CR_OK; +static command_result do_command(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; - { - CoreSuspender suspend; - tailor_instance->process(out); + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot run %s without a loaded world.\n", plugin_name); + return CR_FAILURE; } - return CR_OK; + bool show_help = false; + if (!call_tailor_lua(&out, "parse_commandline", parameters.size(), 1, + [&](lua_State *L) { + for (const string ¶m : parameters) + Lua::Push(L, param); + }, + [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; + } + + return show_help ? CR_WRONG_USAGE : CR_OK; } -static command_result tailor_cmd(color_ostream& out, std::vector & parameters) { - bool desired = enabled; - if (parameters.size() == 1 && (parameters[0] == "enable" || parameters[0] == "on" || parameters[0] == "1")) - { - desired = true; - } - else if (parameters.size() == 1 && (parameters[0] == "disable" || parameters[0] == "off" || parameters[0] == "0")) - { - desired = false; - } - else if (parameters.size() == 1 && (parameters[0] == "usage" || parameters[0] == "help" || parameters[0] == "?")) - { - return CR_WRONG_USAGE; - } - else if (parameters.size() == 1 && parameters[0] == "test") - { - if (tailor_instance) - { - tailor_instance->do_scan(out); - return CR_OK; - } - else - { - out.print("%s: not instantiated\n", plugin_name); - return CR_FAILURE; - } - } - else if (parameters.size() > 1 && parameters[0] == "materials") - { - if (tailor_instance) - { - return tailor_instance->set_materials(out, parameters); - } - else - { - out.print("%s: not instantiated\n", plugin_name); - return CR_FAILURE; - } - } - else if (parameters.size() == 1 && parameters[0] != "status") - { - return CR_WRONG_USAGE; - } +///////////////////////////////////////////////////// +// cycle logic +// - out.print("Tailor is %s %s.\n", (desired == enabled) ? "currently" : "now", desired ? "enabled" : "disabled"); - if (tailor_instance) - { - out.print("Material list is: %s\n", tailor_instance->get_material_list().c_str()); - } - else - { - out.print("%s: not instantiated\n", plugin_name); - } +static int do_cycle(color_ostream &out) { + // mark that we have recently run + cycle_timestamp = world->frame_counter; - enabled = desired; + DEBUG(cycle,out).print("running %s cycle\n", plugin_name); - return CR_OK; + tailor_instance->reset(); + tailor_instance->scan_clothing(); + tailor_instance->scan_materials(); + tailor_instance->scan_replacements(); + tailor_instance->create_orders(); + tailor_instance->scan_existing_orders(); + return tailor_instance->place_orders(); } +///////////////////////////////////////////////////// +// Lua API +// -DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event event) -{ - return CR_OK; +static void tailor_doCycle(color_ostream &out) { + DEBUG(config,out).print("entering tailor_doCycle\n"); + out.print("ordered %d items of clothing\n", do_cycle(out)); } -DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) -{ - enabled = enable; - return CR_OK; -} +// remember, these are ONE-based indices from Lua +static void tailor_setMaterialPreferences(color_ostream &out, int32_t silkIdx, + int32_t clothIdx, int32_t yarnIdx, int32_t leatherIdx) { + DEBUG(config,out).print("entering tailor_setMaterialPreferences\n"); -DFhackCExport command_result plugin_init(color_ostream& out, std::vector & commands) -{ - tailor_instance = std::move(dts::make_unique()); + // it doesn't really matter if these are invalid. set_material_order will do + // the right thing. + set_config_val(config, CONFIG_SILK_IDX, silkIdx); + set_config_val(config, CONFIG_CLOTH_IDX, clothIdx); + set_config_val(config, CONFIG_YARN_IDX, yarnIdx); + set_config_val(config, CONFIG_LEATHER_IDX, leatherIdx); - if (AUTOENABLE) { - enabled = true; - } + set_material_order(); +} - commands.push_back(PluginCommand( - plugin_name, - "Automatically keep your dwarves in fresh clothing.", - tailor_cmd)); - return CR_OK; +static int tailor_getMaterialPreferences(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(config,*out).print("entering tailor_getMaterialPreferences\n"); + vector names; + for (const auto& m : material_order) + names.emplace_back(m.name); + Lua::PushVector(L, names); + return 1; } -DFhackCExport command_result plugin_shutdown(color_ostream& out) -{ - tailor_instance.release(); +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(tailor_doCycle), + DFHACK_LUA_FUNCTION(tailor_setMaterialPreferences), + DFHACK_LUA_END +}; - return plugin_enable(out, false); -} +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(tailor_getMaterialPreferences), + DFHACK_LUA_END +};