diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 828ae4954..7359d842c 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -73,8 +73,9 @@ struct BuildingTypeKeyHash { static PersistentDataItem config; // for use in counting available materials for the UI -static unordered_map, BuildingTypeKeyHash> job_item_repo; +static unordered_map, BuildingTypeKeyHash> job_item_cache; static unordered_map cur_heat_safety; +static unordered_map, BuildingTypeKeyHash> cur_item_filters; // building id -> PlannedBuilding static unordered_map planned_buildings; // vector id -> filter bucket -> queue of (building id, job_item index) @@ -96,6 +97,87 @@ void PlannedBuilding::remove(color_ostream &out) { static const int32_t CYCLE_TICKS = 600; // twice per game day static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle +static bool call_buildingplan_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(status).print("calling buildingplan lua function: '%s'\n", fn_name); + + CoreSuspender guard; + + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + if (!out) + out = &Core::getInstance().getConsole(); + + return Lua::CallLuaModuleFunction(*out, L, "plugins.buildingplan", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); +} + +static int get_num_filters(color_ostream &out, BuildingTypeKey key) { + int num_filters = 0; + if (!call_buildingplan_lua(&out, "get_num_filters", 3, 1, + [&](lua_State *L) { + Lua::Push(L, std::get<0>(key)); + Lua::Push(L, std::get<1>(key)); + Lua::Push(L, std::get<2>(key)); + }, + [&](lua_State *L) { + num_filters = lua_tonumber(L, -1); + })) { + return 0; + } + return num_filters; +} + +static vector & get_job_items(color_ostream &out, BuildingTypeKey key) { + if (job_item_cache.count(key)) + return job_item_cache[key]; + const int num_filters = get_num_filters(out, key); + auto &jitems = job_item_cache[key]; + for (int index = 0; index < num_filters; ++index) { + bool failed = false; + if (!call_buildingplan_lua(&out, "get_job_item", 4, 1, + [&](lua_State *L) { + Lua::Push(L, std::get<0>(key)); + Lua::Push(L, std::get<1>(key)); + Lua::Push(L, std::get<2>(key)); + Lua::Push(L, index+1); + }, + [&](lua_State *L) { + df::job_item *jitem = Lua::GetDFObject(L, -1); + DEBUG(status,out).print("retrieving job_item for (%d, %d, %d) index=%d: %p\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), index, jitem); + if (!jitem) + failed = true; + else + jitems.emplace_back(jitem); + }) || failed) { + jitems.clear(); + break; + } + } + return jitems; +} + +static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { + if (cur_heat_safety.count(key)) + return cur_heat_safety.at(key); + return HEAT_SAFETY_ANY; +} + +static vector & get_item_filters(color_ostream &out, const BuildingTypeKey &key) { + if (cur_item_filters.count(key)) + return cur_item_filters[key]; + + vector &filters = cur_item_filters[key]; + filters.resize(get_job_items(out, key).size()); + return filters; +} + static command_result do_command(color_ostream &out, vector ¶meters); void buildingplan_cycle(color_ostream &out, Tasks &tasks, unordered_map &planned_buildings); @@ -149,37 +231,19 @@ static void validate_config(color_ostream &out, bool verbose = false) { set_config_bool(config, CONFIG_BARS, false); } -static bool call_buildingplan_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(status).print("calling buildingplan lua function: '%s'\n", fn_name); - - CoreSuspender guard; - - auto L = Lua::Core::State; - Lua::StackUnwinder top(L); - - if (!out) - out = &Core::getInstance().getConsole(); - - return Lua::CallLuaModuleFunction(*out, L, "plugins.buildingplan", fn_name, - nargs, nres, - std::forward(args_lambda), - std::forward(res_lambda)); -} - static void clear_state(color_ostream &out) { call_buildingplan_lua(&out, "signal_reset"); call_buildingplan_lua(&out, "reload_cursors"); planned_buildings.clear(); tasks.clear(); - for (auto &entry : job_item_repo) { + cur_heat_safety.clear(); + cur_item_filters.clear(); + for (auto &entry : job_item_cache ) { for (auto &jitem : entry.second) { delete jitem; } } - job_item_repo.clear(); + job_item_cache.clear(); } DFhackCExport command_result plugin_load_data (color_ostream &out) { @@ -199,7 +263,15 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { const size_t num_building_configs = building_configs.size(); for (size_t idx = 0; idx < num_building_configs; ++idx) { PlannedBuilding pb(out, building_configs[idx]); - registerPlannedBuilding(out, pb); + df::building *bld = df::building::find(pb.id); + if (!bld) { + WARN(status).print("cannot find building %d; halting load\n", pb.id); + } + BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); + if (pb.item_filters.size() != get_item_filters(out, key).size()) + WARN(status).print("loaded state for building %d doesn't match world\n", pb.id); + else + registerPlannedBuilding(out, pb); } return CR_OK; @@ -438,30 +510,12 @@ static bool setSetting(color_ostream &out, string name, bool value) { static bool isPlannableBuilding(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom) { DEBUG(status,out).print("entering isPlannableBuilding\n"); - int num_filters = 0; - if (!call_buildingplan_lua(&out, "get_num_filters", 3, 1, - [&](lua_State *L) { - Lua::Push(L, type); - Lua::Push(L, subtype); - Lua::Push(L, custom); - }, - [&](lua_State *L) { - num_filters = lua_tonumber(L, -1); - })) { - return false; - } - return num_filters >= 1; + return get_num_filters(out, BuildingTypeKey(type, subtype, custom)) >= 1; } static bool isPlannedBuilding(color_ostream &out, df::building *bld) { TRACE(status,out).print("entering isPlannedBuilding\n"); - return bld && planned_buildings.count(bld->id) > 0; -} - -static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { - if (cur_heat_safety.count(key)) - return cur_heat_safety.at(key); - return HEAT_SAFETY_ANY; + return bld && planned_buildings.count(bld->id); } static bool addPlannedBuilding(color_ostream &out, df::building *bld) { @@ -471,7 +525,7 @@ static bool addPlannedBuilding(color_ostream &out, df::building *bld) { bld->getCustomType())) return false; BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); - PlannedBuilding pb(out, bld, get_heat_safety_filter(key)); + PlannedBuilding pb(out, bld, get_heat_safety_filter(key), get_item_filters(out, key)); return registerPlannedBuilding(out, pb); } @@ -492,30 +546,10 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ type, subtype, custom, index); BuildingTypeKey key(type, subtype, custom); HeatSafety heat = get_heat_safety_filter(key); - auto &job_items = job_item_repo[key]; - if (index >= (int)job_items.size()) { - for (int i = job_items.size(); i <= index; ++i) { - bool failed = false; - if (!call_buildingplan_lua(&out, "get_job_item", 4, 1, - [&](lua_State *L) { - Lua::Push(L, type); - Lua::Push(L, subtype); - Lua::Push(L, custom); - Lua::Push(L, index+1); - }, - [&](lua_State *L) { - df::job_item *jitem = Lua::GetDFObject(L, -1); - DEBUG(status,out).print("retrieving job_item for index=%d: %p\n", - index, jitem); - if (!jitem) - failed = true; - else - job_items.emplace_back(jitem); - }) || failed) { - return 0; - } - } - } + auto &job_items = get_job_items(out, key); + if (job_items.size() <= index) + return 0; + auto &item_filters = get_item_filters(out, key); auto &jitem = job_items[index]; auto vector_ids = getVectorIds(out, jitem); @@ -524,7 +558,7 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ for (auto vector_id : vector_ids) { auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); for (auto &item : df::global::world->items.other[other_id]) { - if (itemPassesScreen(item) && matchesFilters(item, jitem, heat)) { + if (itemPassesScreen(item) && matchesFilters(item, jitem, heat, item_filters[index])) { if (item_ids) item_ids->emplace_back(item->id); ++count; diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index 787987586..4f0d374e7 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -1,5 +1,7 @@ #pragma once +#include "itemfilter.h" + #include "modules/Persistence.h" #include "df/building.h" @@ -38,6 +40,6 @@ void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); std::vector getVectorIds(DFHack::color_ostream &out, df::job_item *job_item); bool itemPassesScreen(df::item * item); -bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat); +bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter); bool isJobReady(DFHack::color_ostream &out, const std::vector &jitems); void finalizeBuilding(DFHack::color_ostream &out, df::building *bld); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 703bab9b0..a904bc5a8 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -46,7 +46,7 @@ bool itemPassesScreen(df::item * item) { && !item->isAssignedToStockpile(); } -bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat) { +bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter) { // check the properties that are not checked by Job::isSuitableItem() if (job_item->item_type > -1 && job_item->item_type != item->getType()) return false; @@ -76,7 +76,8 @@ bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat) { &jitem, item->getType(), item->getSubtype()) && Job::isSuitableMaterial( &jitem, item->getMaterial(), item->getMaterialIndex(), - item->getType()); + item->getType()) + && item_filter.matches(item); } bool isJobReady(color_ostream &out, const std::vector &jitems) { @@ -180,7 +181,9 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, auto id = task.first; auto job = bld->jobs[0]; auto filter_idx = task.second; - if (matchesFilters(item, job->job_items[filter_idx], planned_buildings.at(id).heat_safety) + auto &pb = planned_buildings.at(id); + if (matchesFilters(item, job->job_items[filter_idx], pb.heat_safety, + pb.item_filters[filter_idx]) && Job::attachJobItem(job, item, df::job_item_ref::Hauled, filter_idx)) { diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp index e69de29bb..bd35c848a 100644 --- a/plugins/buildingplan/itemfilter.cpp +++ b/plugins/buildingplan/itemfilter.cpp @@ -0,0 +1,189 @@ +#include "itemfilter.h" + +#include "Debug.h" + +#include "df/item.h" + +using namespace DFHack; + +namespace DFHack { + DBG_EXTERN(buildingplan, status); +} + +ItemFilter::ItemFilter() { + clear(); +} + +void ItemFilter::clear() { + min_quality = df::item_quality::Ordinary; + max_quality = df::item_quality::Masterful; + decorated_only = false; + mat_mask.whole = 0; + materials.clear(); +} + +bool ItemFilter::isEmpty() { + return min_quality == df::item_quality::Ordinary + && max_quality == df::item_quality::Masterful + && !decorated_only + && !mat_mask.whole + && materials.empty(); +} + +static bool deserializeMaterialMask(std::string ser, df::dfhack_material_category mat_mask) { + if (ser.empty()) + return true; + + if (!parseJobMaterialCategory(&mat_mask, ser)) { + DEBUG(status).print("invalid job material category serialization: '%s'", ser.c_str()); + return false; + } + return true; +} + +static bool deserializeMaterials(std::string ser, std::vector &materials) { + if (ser.empty()) + return true; + + std::vector mat_names; + split_string(&mat_names, ser, ","); + for (auto m = mat_names.begin(); m != mat_names.end(); m++) { + DFHack::MaterialInfo material; + if (!material.find(*m) || !material.isValid()) { + DEBUG(status).print("invalid material name serialization: '%s'", ser.c_str()); + return false; + } + materials.push_back(material); + } + return true; +} + +ItemFilter::ItemFilter(std::string serialized) { + clear(); + + std::vector tokens; + split_string(&tokens, serialized, "/"); + if (tokens.size() != 5) { + DEBUG(status).print("invalid ItemFilter serialization: '%s'", serialized.c_str()); + return; + } + + if (!deserializeMaterialMask(tokens[0], mat_mask) || !deserializeMaterials(tokens[1], materials)) + return; + + setMinQuality(atoi(tokens[2].c_str())); + setMaxQuality(atoi(tokens[3].c_str())); + decorated_only = static_cast(atoi(tokens[4].c_str())); +} + +// format: mat,mask,elements/materials,list/minq/maxq/decorated +std::string ItemFilter::serialize() const { + std::ostringstream ser; + ser << bitfield_to_string(mat_mask, ",") << "/"; + if (!materials.empty()) { + ser << materials[0].getToken(); + for (size_t i = 1; i < materials.size(); ++i) + ser << "," << materials[i].getToken(); + } + ser << "/" << static_cast(min_quality); + ser << "/" << static_cast(max_quality); + ser << "/" << static_cast(decorated_only); + return ser.str(); +} + +static void clampItemQuality(df::item_quality *quality) { + if (*quality > df::item_quality::Artifact) { + DEBUG(status).print("clamping quality to Artifact"); + *quality = df::item_quality::Artifact; + } + if (*quality < df::item_quality::Ordinary) { + DEBUG(status).print("clamping quality to Ordinary"); + *quality = df::item_quality::Ordinary; + } +} + +void ItemFilter::setMinQuality(int quality) { + min_quality = static_cast(quality); + clampItemQuality(&min_quality); + if (max_quality < min_quality) + max_quality = min_quality; +} + +void ItemFilter::setMaxQuality(int quality) { + max_quality = static_cast(quality); + clampItemQuality(&max_quality); + if (max_quality < min_quality) + min_quality = max_quality; +} + +void ItemFilter::setDecoratedOnly(bool decorated) { + decorated_only = decorated; +} + +void ItemFilter::setMaterialMask(uint32_t mask) { + mat_mask.whole = mask; +} + +void ItemFilter::setMaterials(const std::vector &materials) { + this->materials = materials; +} + +std::string ItemFilter::getMinQuality() const { + return ENUM_KEY_STR(item_quality, min_quality); +} + +std::string ItemFilter::getMaxQuality() const { + return ENUM_KEY_STR(item_quality, max_quality); +} + +bool ItemFilter::getDecoratedOnly() const { + return decorated_only; +} + +uint32_t ItemFilter::getMaterialMask() const { + return mat_mask.whole; +} + +static std::string material_to_string_fn(const MaterialInfo &m) { return m.toString(); } + +std::vector ItemFilter::getMaterials() const { + std::vector descriptions; + transform_(materials, descriptions, material_to_string_fn); + + if (descriptions.size() == 0) + bitfield_to_string(&descriptions, mat_mask); + + if (descriptions.size() == 0) + descriptions.push_back("any"); + + return descriptions; +} + +static bool matchesMask(DFHack::MaterialInfo &mat, df::dfhack_material_category mat_mask) { + return mat_mask.whole ? mat.matches(mat_mask) : true; +} + +bool ItemFilter::matches(df::dfhack_material_category mask) const { + return mask.whole & mat_mask.whole; +} + +bool ItemFilter::matches(DFHack::MaterialInfo &material) const { + for (auto it = materials.begin(); it != materials.end(); ++it) + if (material.matches(*it)) + return true; + return false; +} + +bool ItemFilter::matches(df::item *item) const { + if (item->getQuality() < min_quality || item->getQuality() > max_quality) + return false; + + if (decorated_only && !item->hasImprovements()) + return false; + + auto imattype = item->getActualMaterial(); + auto imatindex = item->getActualMaterialIndex(); + auto item_mat = DFHack::MaterialInfo(imattype, imatindex); + + return (materials.size() == 0) ? matchesMask(item_mat, mat_mask) : matches(item_mat); +} diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h index 6f70f09be..134d3b249 100644 --- a/plugins/buildingplan/itemfilter.h +++ b/plugins/buildingplan/itemfilter.h @@ -1 +1,39 @@ #pragma once + +#include "modules/Materials.h" + +#include "df/dfhack_material_category.h" +#include "df/item_quality.h" + +class ItemFilter { +public: + ItemFilter(); + ItemFilter(std::string serialized); + + void clear(); + bool isEmpty(); + std::string serialize() const; + + void setMinQuality(int quality); + void setMaxQuality(int quality); + void setDecoratedOnly(bool decorated); + void setMaterialMask(uint32_t mask); + void setMaterials(const std::vector &materials); + + std::string getMinQuality() const; + std::string getMaxQuality() const; + bool getDecoratedOnly() const; + uint32_t getMaterialMask() const; + std::vector getMaterials() const; + + bool matches(df::dfhack_material_category mask) const; + bool matches(DFHack::MaterialInfo &material) const; + bool matches(df::item *item) const; + +private: + df::item_quality min_quality; + df::item_quality max_quality; + bool decorated_only; + df::dfhack_material_category mat_mask; + std::vector materials; +}; diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp index eb55a95b4..c68d668bf 100644 --- a/plugins/buildingplan/plannedbuilding.cpp +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -31,14 +31,18 @@ static vector> get_vector_ids(color_ostream &out, return ret; } -static vector> deserialize(color_ostream &out, PersistentDataItem &bld_config) { +static vector> deserialize_vector_ids(color_ostream &out, PersistentDataItem &bld_config) { vector> ret; - DEBUG(status,out).print("deserializing state for building %d: %s\n", - get_config_val(bld_config, BLD_CONFIG_ID), bld_config.val().c_str()); + vector rawstrs; + split_string(&rawstrs, bld_config.val(), "|"); + const string &serialized = rawstrs[0]; + + DEBUG(status,out).print("deserializing vector ids for building %d: %s\n", + get_config_val(bld_config, BLD_CONFIG_ID), serialized.c_str()); vector joined; - split_string(&joined, bld_config.val(), "|"); + split_string(&joined, serialized, ";"); for (auto &str : joined) { vector lst; split_string(&lst, str, ","); @@ -54,28 +58,60 @@ static vector> deserialize(color_ostream &out, Pe return ret; } -static string serialize(const vector> &vector_ids) { +static std::vector deserialize_item_filters(color_ostream &out, PersistentDataItem &bld_config) { + std::vector ret; + + vector rawstrs; + split_string(&rawstrs, bld_config.val(), "|"); + if (rawstrs.size() < 2) + return ret; + const string &serialized = rawstrs[1]; + + DEBUG(status,out).print("deserializing item filters for building %d: %s\n", + get_config_val(bld_config, BLD_CONFIG_ID), serialized.c_str()); + + vector filterstrs; + split_string(&filterstrs, serialized, ";"); + for (auto &str : filterstrs) { + ret.emplace_back(str); + } + + return ret; +} + +static string serialize(const vector> &vector_ids, const vector &item_filters) { vector joined; for (auto &vec_list : vector_ids) { joined.emplace_back(join_strings(",", vec_list)); } - return join_strings("|", joined); + std::ostringstream out; + out << join_strings(";", joined) << "|"; + + joined.clear(); + for (auto &filter : item_filters) { + joined.emplace_back(filter.serialize()); + } + out << join_strings(";", joined); + + return out.str(); } -PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafety heat) - : id(bld->id), vector_ids(get_vector_ids(out, id)), heat_safety(heat) { +PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafety heat, const vector &item_filters) + : id(bld->id), vector_ids(get_vector_ids(out, id)), heat_safety(heat), + item_filters(item_filters) { DEBUG(status,out).print("creating persistent data for building %d\n", id); bld_config = World::AddPersistentData(BLD_CONFIG_KEY); set_config_val(bld_config, BLD_CONFIG_ID, id); set_config_val(bld_config, BLD_CONFIG_HEAT, heat_safety); - bld_config.val() = serialize(vector_ids); + bld_config.val() = serialize(vector_ids, item_filters); DEBUG(status,out).print("serialized state for building %d: %s\n", id, bld_config.val().c_str()); } PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_config) : id(get_config_val(bld_config, BLD_CONFIG_ID)), - vector_ids(deserialize(out, bld_config)), + vector_ids(deserialize_vector_ids(out, bld_config)), heat_safety((HeatSafety)get_config_val(bld_config, BLD_CONFIG_HEAT)), + item_filters(deserialize_item_filters(out, bld_config)), bld_config(bld_config) { } // Ensure the building still exists and is in a valid state. It can disappear diff --git a/plugins/buildingplan/plannedbuilding.h b/plugins/buildingplan/plannedbuilding.h index 0a67e0edc..5bd09ba5a 100644 --- a/plugins/buildingplan/plannedbuilding.h +++ b/plugins/buildingplan/plannedbuilding.h @@ -1,6 +1,7 @@ #pragma once #include "buildingplan.h" +#include "itemfilter.h" #include "Core.h" @@ -18,7 +19,9 @@ public: const HeatSafety heat_safety; - PlannedBuilding(DFHack::color_ostream &out, df::building *bld, HeatSafety heat); + const std::vector item_filters; + + PlannedBuilding(DFHack::color_ostream &out, df::building *bld, HeatSafety heat, const std::vector &item_filters); PlannedBuilding(DFHack::color_ostream &out, DFHack::PersistentDataItem &bld_config); void remove(DFHack::color_ostream &out); diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 9d8eb463b..b6cbb383b 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -1292,7 +1292,7 @@ InspectorOverlay.ATTRS{ default_pos={x=-41,y=14}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING', - frame={w=30, h=14}, + frame={w=30, h=15}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } @@ -1308,12 +1308,12 @@ function InspectorOverlay:init() InspectorLine{view_id='item3', frame={t=6, l=0}, idx=3}, InspectorLine{view_id='item4', frame={t=8, l=0}, idx=4}, widgets.HotkeyLabel{ - frame={t=10, l=0}, + frame={t=11, l=0}, label='adjust filters', key='CUSTOM_CTRL_F', }, widgets.HotkeyLabel{ - frame={t=11, l=0}, + frame={t=12, l=0}, label='make top priority', key='CUSTOM_CTRL_T', on_activate=self:callback('make_top_priority'),