diff --git a/docs/changelog.txt b/docs/changelog.txt index bc862e311..1d94f45fc 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -43,6 +43,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Misc Improvements - `buildingplan`: now displays which items are attached and which items are still missing for planned buildings +- `orders`: support importing and exporting reaction-specific item conditions, like "lye-containing" for soap production orders - `tiletypes-here`, `tiletypes-here-point`: add --cursor and --quiet options to support non-interactive use cases - `quickfort`: Dreamfort blueprint set improvements: extensive revision based on playtesting and feedback. includes updated ``onMapLoad.init`` settings file and enhanced automation orders. see full changelog at https://github.com/DFHack/dfhack/pull/1921 diff --git a/plugins/orders.cpp b/plugins/orders.cpp index 629fee225..2a90e7e69 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -29,6 +29,8 @@ #include "df/manager_order.h" #include "df/manager_order_condition_item.h" #include "df/manager_order_condition_order.h" +#include "df/reaction.h" +#include "df/reaction_reagent.h" #include "df/world.h" using namespace DFHack; @@ -377,7 +379,26 @@ static command_result orders_export_command(color_ostream & out, const std::stri condition["tool"] = enum_item_key(it2->has_tool_use); } - // TODO: anon_1, anon_2, anon_3 + if (it2->min_dimension != -1) + { + condition["min_dimension"] = it2->min_dimension; + } + + if (it2->reaction_id != -1) + { + df::reaction *reaction = world->raws.reactions.reactions[it2->reaction_id]; + condition["reaction_id"] = reaction->code; + + if (!it2->contains.empty()) + { + Json::Value contains(Json::arrayValue); + for (int32_t contains_val : it2->contains) + { + contains.append(reaction->reagents[contains_val]->code); + } + condition["contains"] = contains; + } + } conditions.append(condition); } @@ -705,7 +726,72 @@ static command_result orders_import(color_ostream &out, Json::Value &orders) } } - // TODO: anon_1, anon_2, anon_3 + if (it2.isMember("min_dimension")) + { + condition->min_dimension = it2["min_dimension"].asInt(); + } + + if (it2.isMember("reaction_id")) + { + std::string reaction_code = it2["reaction_id"].asString(); + df::reaction *reaction = NULL; + int32_t reaction_id = -1; + size_t num_reactions = world->raws.reactions.reactions.size(); + for (size_t idx = 0; idx < num_reactions; ++idx) + { + reaction = world->raws.reactions.reactions[idx]; + if (reaction->code == reaction_code) + { + reaction_id = idx; + break; + } + } + if (reaction_id < 0) + { + delete condition; + + out << COLOR_YELLOW << "Reaction code not found for imported manager order: " << reaction_code << std::endl; + + continue; + } + + condition->reaction_id = reaction_id; + + if (it2.isMember("contains")) + { + size_t num_reagents = reaction->reagents.size(); + std::string bad_reagent_code; + for (Json::Value & contains_val : it2["contains"]) + { + std::string reagent_code = contains_val.asString(); + bool reagent_found = false; + for (size_t idx = 0; idx < num_reagents; ++idx) + { + df::reaction_reagent *reagent = reaction->reagents[idx]; + if (reagent->code == reagent_code) + { + condition->contains.push_back(idx); + reagent_found = true; + break; + } + } + + if (!reagent_found) + { + bad_reagent_code = reagent_code; + break; + } + } + if (!bad_reagent_code.empty()) + { + delete condition; + + out << COLOR_YELLOW << "Invalid reagent code for imported manager order: " << bad_reagent_code << std::endl; + + continue; + } + } + } order->item_conditions.push_back(condition); } diff --git a/test/plugins/orders.lua b/test/plugins/orders.lua index 53b084f18..95fe186d2 100644 --- a/test/plugins/orders.lua +++ b/test/plugins/orders.lua @@ -1,26 +1,70 @@ config.mode = 'fortress' -TMP_FILE_NAME = 'tmp-test' -TMP_FILE_PATH = ('dfhack-config/orders/%s.json'):format(TMP_FILE_NAME) +local FILE_PATH_PATTERN = 'dfhack-config/orders/%s.json' -function run_orders_import(file_content) - local f = io.open(TMP_FILE_PATH, 'w') - f:write(file_content) - f:close() +local BACKUP_FILE_NAME = 'tmp-backup' +local BACKUP_FILE_PATH = FILE_PATH_PATTERN:format(BACKUP_FILE_NAME) +local TMP_FILE_NAME = 'tmp-test' +local TMP_FILE_PATH = FILE_PATH_PATTERN:format(TMP_FILE_NAME) + +local function test_wrapper(test_fn) + -- backup and clear active orders + dfhack.run_command_silent{'orders', 'export', BACKUP_FILE_NAME} + dfhack.run_command_silent{'orders', 'clear'} + df.global.world.manager_order_next_id = 0 return dfhack.with_finalize( function() + -- clear test orders, restore original orders, remove temp files + dfhack.run_command_silent{'orders', 'clear'} + df.global.world.manager_order_next_id = 0 + dfhack.run_command_silent{'orders', 'import', BACKUP_FILE_NAME} + df.global.world.manager_order_next_id = + #df.global.world.manager_orders + os.remove(BACKUP_FILE_PATH) os.remove(TMP_FILE_PATH) end, - function() - return dfhack.run_command_silent{'orders', 'import', TMP_FILE_NAME} - end - ) + test_fn) end +config.wrapper = test_wrapper -function check_import_success(file_content) - local output, result = run_orders_import(file_content) +-- returns export command result and exported file content +function run_orders_export() + local _, result = dfhack.run_command_silent{'orders', 'export', + TMP_FILE_NAME} + local f = io.open(TMP_FILE_PATH, 'r') + return dfhack.with_finalize( + function() f:close() end, + function() return result, f:read('*all') end) +end + +function run_orders_import(file_content) + local f = io.open(TMP_FILE_PATH, 'w') + f:write(file_content) + f:close() + return dfhack.run_command_silent{'orders', 'import', TMP_FILE_NAME} +end + +local function normalize_whitespace(str) + return str:gsub('%s+', ' '):trim() +end + +function check_export_success(expected_file_content) + local result, file_content = run_orders_export() expect.eq(result, CR_OK) + + -- ignore whitespace (otherwise the expected file content is impossible to + -- format properly in this file) + expect.eq(normalize_whitespace(expected_file_content), + normalize_whitespace(file_content)) +end + +function check_import_success(file_content, comment, num_expected_orders) + local prev_num_orders = #df.global.world.manager_orders + local output, result = run_orders_import(file_content) + expect.eq(result, CR_OK, comment) + expect.eq(prev_num_orders + num_expected_orders, + #df.global.world.manager_orders, comment) end function check_import_fail(file_content, comment, prefix) @@ -35,7 +79,7 @@ function check_import_fail(file_content, comment, prefix) end function test.import_empty() - check_import_success('[]') + check_import_success('[]', 'empty input', 0) end function test.import_non_array() @@ -121,3 +165,96 @@ function test.import_valid_and_invalid_orders() ] ]], 'empty order before valid order') end + +function test.import_export_reaction_condition() + local file_content = [[ + [ + { + "amount_left" : 1, + "amount_total" : 1, + "frequency" : "Daily", + "id" : 0, + "is_active" : false, + "is_validated" : false, + "item_conditions" : + [ + { + "condition" : "AtLeast", + "contains" : + [ + "lye" + ], + "reaction_id" : "MAKE_SOAP_FROM_TALLOW", + "value" : 5 + } + ], + "job" : "CustomReaction", + "reaction" : "MAKE_SOAP_FROM_TALLOW" + } + ] + ]] + check_import_success(file_content, 'valid reaction condition', 1) + check_export_success(file_content) +end + +local function get_last_order() + return df.global.world.manager_orders[#df.global.world.manager_orders-1] +end + +function test.import_invalid_reaction_conditions() + check_import_success([[ + [ + { + "amount_left" : 1, + "amount_total" : 1, + "frequency" : "OneTime", + "id" : 0, + "is_active" : false, + "is_validated" : true, + "item_conditions" : + [ + { + "condition" : "AtLeast", + "contains" : + [ + "lye" + ], + "reaction_id" : "MAKE_SOAP_FROM_TALLOW_xxx", + "value" : 5 + } + ], + "job" : "CustomReaction", + "reaction" : "MAKE_SOAP_FROM_TALLOW" + } + ] + ]], 'condition ignored for bad reaction id', 1) + expect.eq(0, #get_last_order().item_conditions) + + check_import_success([[ + [ + { + "amount_left" : 1, + "amount_total" : 1, + "frequency" : "OneTime", + "id" : 0, + "is_active" : false, + "is_validated" : true, + "item_conditions" : + [ + { + "condition" : "AtLeast", + "contains" : + [ + "lye_xxx" + ], + "reaction_id" : "MAKE_SOAP_FROM_TALLOW", + "value" : 5 + } + ], + "job" : "CustomReaction", + "reaction" : "MAKE_SOAP_FROM_TALLOW" + } + ] + ]], 'condition ignored for bad reagent name', 1) + expect.eq(0, #get_last_order().item_conditions) +end