diff --git a/data/init/dfhack.tools.init b/data/init/dfhack.tools.init index 50272cfa25..2b9ea6404f 100644 --- a/data/init/dfhack.tools.init +++ b/data/init/dfhack.tools.init @@ -98,7 +98,8 @@ enable automelt # Other interface improvement tools enable \ - confirm + confirm \ + zone # dwarfmonitor \ # mousequery \ # autogems \ @@ -107,7 +108,6 @@ enable \ # autotrade \ # buildingplan \ # trackstop \ -# zone \ # stocks \ # autochop \ # stockpiles diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 7940c39942..df79d98f4f 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -169,7 +169,7 @@ dfhack_plugin(tiletypes tiletypes.cpp Brushes.h LINK_LIBRARIES lua) #dfhack_plugin(workflow workflow.cpp LINK_LIBRARIES lua) #dfhack_plugin(workNow workNow.cpp) dfhack_plugin(xlsxreader xlsxreader.cpp LINK_LIBRARIES lua xlsxio_read_STATIC zip expat) -#dfhack_plugin(zone zone.cpp) +dfhack_plugin(zone zone.cpp) # If you are adding a plugin that you do not intend to commit to the DFHack repo, # see instructions for adding "external" plugins at the end of this file. diff --git a/plugins/lua/zone.lua b/plugins/lua/zone.lua new file mode 100644 index 0000000000..4c1e19688d --- /dev/null +++ b/plugins/lua/zone.lua @@ -0,0 +1,542 @@ +local _ENV = mkmodule('plugins.zone') +local gui = require('gui') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +ZoneOverlay = defclass(ZoneOverlay, overlay.OverlayWidget) +ZoneOverlay.ATTRS{ + default_pos={x=46,y=39}, + default_enabled=true, + viewscreens='dwarfmode', + frame={w=42, h=15}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +local function zone_assign_is_active() + return dfhack.gui.matchFocusString('dwarfmode/UnitSelector') and (df.global.game.main_interface.unit_selector.context == 0 or df.global.game.main_interface.unit_selector.context == 1) +end + +local function canShear(unit) + return #df.global.world.raws.creatures.all[unit.race].caste[unit.caste].shearable_tissue_layer > 0 +end + +local function copyTable(table_to_copy) + local new_table = {} + + for _, v in ipairs(table_to_copy) do + table.insert(new_table, v) + end + + return new_table +end + +local function createSearchKeyForUnit(unit) + local race_name = df.global.world.raws.creatures.all[unit.race].caste[unit.caste].caste_name[0] + local species = df.global.world.raws.creatures.all[unit.race].name[0] + local unit_actual_name = string.lower(dfhack.toSearchNormalized(dfhack.TranslateName(dfhack.units.getVisibleName(unit), false))) + local is_stray = not dfhack.units.isPet(unit) + local unit_profession = unit.profession + local is_child = dfhack.units.isChild(unit) + local child_name = df.global.world.raws.creatures.all[unit.race].general_child_name + local baby_name = df.global.world.raws.creatures.all[unit.race].general_baby_name + + if child_name then + child_name = child_name[0] + end + + if baby_name then + baby_name = baby_name[0] + end + + local search_name = race_name + if is_baby then search_name = baby_name end + if is_child then search_name = child_name end + + local trained_war = false + local trained_hunter = false + + if unit_profession == 98 then + trained_hunter = true + elseif unit_profession == 99 then + trained_war = true + end + + return search_name .. ' ' .. unit_actual_name .. (is_stray and ' stray ' or '') .. ' (tame) ' .. + (trained_war and ' war ' or '') .. (trained_hunter and ' hunting ' or '') .. ' species=' .. species +end + +function ZoneOverlay:init() + self.screen = nil + self:setDirty(false) + self.search_focused = false + + -- used to store + self.original_unit_table = nil + self.original_selected_table = nil + + -- used to update original_unit_table when a filter is applied + self.selected_table_before_click = nil + self.check_selected_on_render = false + self.has_rendered = false + + -- filters + self.non_grazing = true + self.non_egg_laying = true + self.non_milkable = true + self.non_shearable = true + self.not_caged = true + self.currently_pastured = true + self.female = true + self.male = true + + self:addviews{ + widgets.HotkeyLabel{ + view_id='toggle_all', + frame={t=0,l=0}, + label='Assign/unassign all', + key='CUSTOM_ALT_Q', + on_activate=self:callback('toggleAllUnits'), + }, + widgets.ToggleHotkeyLabel{ + view_id='non_grazing', + frame={t=2,l=0}, + label='Non-Grazing', + key='CUSTOM_ALT_G', + on_change=self:callback('toggleNonGrazing'), + }, + widgets.ToggleHotkeyLabel{ + view_id='non_egg_laying', + frame={t=3,l=0}, + label='Non-Egg Laying', + key='CUSTOM_ALT_E', + on_change=self:callback('toggleNonEggLaying'), + }, + widgets.ToggleHotkeyLabel{ + view_id='non_milkable', + frame={t=4,l=0}, + label='Non-Milkable', + key='CUSTOM_ALT_L', + on_change=self:callback('toggleNonMilkable'), + }, + widgets.ToggleHotkeyLabel{ + view_id='non_shearable', + frame={t=5,l=0}, + label='Non-Shearable', + key='CUSTOM_ALT_A', + on_change=self:callback('toggleNonShearable'), + }, + widgets.ToggleHotkeyLabel{ + view_id='not_caged', + frame={t=6,l=0}, + label='Not Caged', + key='CUSTOM_ALT_C', + on_change=self:callback('toggleNotCaged'), + }, + widgets.ToggleHotkeyLabel{ + view_id='currently_pastured', + frame={t=7,l=0}, + label='Currently Pastured', + key='CUSTOM_ALT_P', + on_change=self:callback('toggleCurrentlyPastured'), + }, + widgets.ToggleHotkeyLabel{ + view_id='female', + frame={t=8,l=0}, + label='Female', + key='CUSTOM_ALT_F', + on_change=self:callback('toggleFemale'), + }, + widgets.ToggleHotkeyLabel{ + view_id='male', + frame={t=9,l=0}, + label='Male', + key='CUSTOM_ALT_M', + on_change=self:callback('toggleMale'), + }, + widgets.HotkeyLabel{ + frame={t=11, l=0}, + label='Search', + key='CUSTOM_ALT_S', + on_activate=self:callback('startSearch'), + }, + widgets.EditField{ + view_id='search', + frame={t=12,l=0}, + on_submit=function() self:setSearchFocus(false) end, + on_change=self:callback('doSearch'), + }, + } + + self:setSearchFocus(false) +end + +function ZoneOverlay:toggleAllUnits() + local current_civzone = dfhack.gui.getSelectedCivZone() + local current_civzone_id + if current_civzone then current_civzone_id = current_civzone.id end + if not current_civzone_id then return end + + self.selected_table_before_click = copyTable(df.global.game.main_interface.unit_selector.selected) + + self.dirty = true + local switching_to = 1 + + for k, unit_id in ipairs(df.global.game.main_interface.unit_selector.unid) do + local unit_selected_val = df.global.game.main_interface.unit_selector.selected[k] + -- switch everything to the opposite of whatever the first element is set to + if k == 0 then + if unit_selected_val == 1 then + switching_to = 0 + end + end + + if df.global.game.main_interface.unit_selector.selected[k] ~= switching_to then + df.global.game.main_interface.unit_selector.selected[k] = switching_to + + local unit = df.unit.find(unit_id) + if switching_to == 0 then + -- switching to unpastured, remove pastured ref + for k2, ref in ipairs(unit.general_refs) do + if df.general_ref_building_civzone_assignedst:is_instance(ref) and ref.building_id == current_civzone_id then + unit.general_refs:erase(k2) + break + end + end + + for k2, assigned_unit in ipairs(current_civzone.assigned_units) do + if assigned_unit == unit_id then + current_civzone.assigned_units:erase(k2) + break + end + end + else + -- switching to pastured here, create pastured ref, remove any not for this pasture + for k2, ref in ipairs(unit.general_refs) do + local civzone_to_unassign_from + + if df.general_ref_building_civzone_assignedst:is_instance(ref) and ref.building_id ~= current_civzone_id then + civzone_to_unassign_from = df.building.find(ref.building_id) + + unit.general_refs:erase(k2) + + if civzone_to_unassign_from then + for k3, assigned_unit in ipairs(civzone_to_unassign_from.assigned_units) do + if assigned_unit == unit_id then + civzone_to_unassign_from.assigned_units:erase(k3) + break + end + end + end + + break + end + end + local new_ref = df.general_ref_building_civzone_assignedst:new() + new_ref.building_id = current_civzone_id + unit.general_refs:insert('#', new_ref) + current_civzone.assigned_units:insert('#', unit_id) + end + end + end + + self.check_selected_on_render = true +end + +function ZoneOverlay:toggleNonGrazing() + self.dirty = true + self.non_grazing = not self.non_grazing + self:doSearch() +end + +function ZoneOverlay:toggleNonEggLaying() + self.dirty = true + self.non_egg_laying = not self.non_egg_laying + self:doSearch() +end + +function ZoneOverlay:toggleNonMilkable() + self.dirty = true + self.non_milkable = not self.non_milkable + self:doSearch() +end + +function ZoneOverlay:toggleNonShearable() + self.dirty = true + self.non_shearable = not self.non_shearable + self:doSearch() +end + +function ZoneOverlay:toggleNotCaged() + self.dirty = true + self.not_caged = not self.not_caged + self:doSearch() +end + +function ZoneOverlay:toggleCurrentlyPastured() + self.dirty = true + self.currently_pastured = not self.currently_pastured + self:doSearch() +end + +function ZoneOverlay:toggleFemale() + self.dirty = true + self.female = not self.female + self:doSearch() +end + +function ZoneOverlay:toggleMale() + self.dirty = true + self.male = not self.male + self:doSearch() +end + +function ZoneOverlay:onInput(keys) + if not (isEnabled() and zone_assign_is_active()) then return false end + + if keys._MOUSE_L then + if self.original_unit_table ~= nil and df.global.game.main_interface.unit_selector.selected ~= nil then + self.selected_table_before_click = copyTable(df.global.game.main_interface.unit_selector.selected) + self.check_selected_on_render = true + end + elseif keys.LEAVESCREEN or keys._MOUSE_R then + if self.subviews.search.focus then + self:setSearchFocus(false) + return true + end + end + + local handled_by_super = ZoneOverlay.super.onInput(self, keys) + + if keys._MOUSE_L and handled_by_super then + df.global.enabler.mouse_lbut = 0 + end + + return handled_by_super +end + +function ZoneOverlay:checkAndHandleSelectedChanges() + if not self.original_unit_table then return end + if not self.selected_table_before_click then return end + + for i, selected in ipairs(df.global.game.main_interface.unit_selector.selected) do + if selected ~= self.selected_table_before_click[i+1] then + self:updateOriginalSelectedTable(i, selected) + end + end + self.selected_table_before_click = nil +end + +function ZoneOverlay:updateOriginalSelectedTable(selected_index, selected_state) + local unit_id = df.global.game.main_interface.unit_selector.unid[selected_index] + + for i, original_unit_id in ipairs(self.original_unit_table) do + if unit_id == original_unit_id then + self.original_selected_table[i] = selected_state + break + end + end +end + +function ZoneOverlay:restoreUnitSelectorTables() + if self.original_unit_table then + df.global.game.main_interface.unit_selector.unid = copyTable(self.original_unit_table) + end + + if self.original_selected_table then + df.global.game.main_interface.unit_selector.selected = copyTable(self.original_selected_table) + end +end + +function ZoneOverlay:unitValidForFilters(unit) + if not self.non_grazing then + if not dfhack.units.isGrazer(unit) then + return false + end + end + + if not self.non_egg_laying then + if not dfhack.units.isEggLayer(unit) then + return false + end + end + + if not self.non_milkable then + if not dfhack.units.isMilkable(unit) then + return false + end + end + + if not self.non_shearable then + if not canShear(unit) then + return false + end + end + + if not self.not_caged then + -- TODO: make method + if not dfhack.units.getGeneralRef(unit, df.general_ref_type.CONTAINED_IN_ITEM) then + return false + end + end + + if not self.currently_pastured then + -- TODO: make method + if dfhack.units.getGeneralRef(unit, df.general_ref_type.BUILDING_CIVZONE_ASSIGNED) then + return false + end + end + + if not self.male then + if dfhack.units.isMale(unit) then + return false + end + end + + if not self.female then + if dfhack.units.isFemale(unit) then + return false + end + end + + return true +end + +function ZoneOverlay:anyFiltersActive() + return not self.non_grazing or not self.non_egg_laying or not self.non_milkable or not self.not_caged or + not self.currently_pastured or not self.female or not self.male or not self.non_shearable or false +end + +function ZoneOverlay:disableAllFilters() + self.non_grazing = true + self.subviews.non_grazing:setOption(1) + self.non_egg_laying = true + self.subviews.non_egg_laying:setOption(1) + self.non_milkable = true + self.subviews.non_milkable:setOption(1) + self.non_shearable = true + self.subviews.non_shearable:setOption(1) + self.not_caged = true + self.subviews.not_caged:setOption(1) + self.currently_pastured = true + self.subviews.currently_pastured:setOption(1) + self.female = true + self.subviews.female:setOption(1) + self.male = true + self.subviews.male:setOption(1) +end + +function ZoneOverlay:doSearch() + self:setDirty(true) + local filter = self.subviews.search.text + + if filter == '' and not self:anyFiltersActive() then + self:restoreUnitSelectorTables() + return + end + + filter = string.lower(filter) + local tokens = filter:split() + + local new_unid_table = {} + local new_selected_table = {} + + for idx, unit_id in ipairs(self.original_unit_table) do + local unit = df.unit.find(unit_id) + + if not self:unitValidForFilters(unit) then + goto skip_unit + end + + local search_key = createSearchKeyForUnit(unit) + + local ok = true + for _,key in ipairs(tokens) do + key = key:escape_pattern() + local invert = key:sub(1, 1) == '!' + if invert then + key = key:sub(2) + end + + -- key will be blank if the token was only "!" + if key ~= '' then + local match = search_key:match('%f[^%s\x00]'..key) + if (not invert and not match) or (invert and match) then + ok = false + break + end + end + end + + if ok then + table.insert(new_unid_table, unit_id) + table.insert(new_selected_table, self.original_selected_table[idx]) + end + + ::skip_unit:: + end + + df.global.game.main_interface.unit_selector.unid = new_unid_table + df.global.game.main_interface.unit_selector.selected = new_selected_table +end + +function ZoneOverlay:setSearchFocus(focus) + self.subviews.search:setFocus(focus) + self.search_focused = focus +end + +function ZoneOverlay:startSearch() + self:setDirty(true) + self:setSearchFocus(true) +end + +function ZoneOverlay:setSearch(search, do_search) + self.subviews.search.text = search + + if do_search == nil or do_search then + self:doSearch() + end +end + +function ZoneOverlay:setDirty(dirty) + self.dirty = dirty +end + +function ZoneOverlay:render(dc) + if not (isEnabled() and zone_assign_is_active()) then + if self.dirty then + self:setDirty(false) + self.original_unit_table = nil + self.original_selected_table = nil + self:setSearch('', false) + self:setSearchFocus(false) + self:disableAllFilters() + self.has_rendered = false + end + + return false + end + + -- first render after becoming enabled + if not self.has_rendered then + self.has_rendered = true + self.dirty = true + -- keep track of the original tables so we can mutate those as we change things in the modified tables, and then restore + -- them when no longer searching + self.original_unit_table = copyTable(df.global.game.main_interface.unit_selector.unid) + self.original_selected_table = copyTable(df.global.game.main_interface.unit_selector.selected) + end + + if self.check_selected_on_render then + self.check_selected_on_render = false + self:checkAndHandleSelectedChanges() + end + + ZoneOverlay.super.render(self, dc) +end + +OVERLAY_WIDGETS = { + overlay=ZoneOverlay, +} + +return _ENV diff --git a/plugins/zone.cpp b/plugins/zone.cpp index 878014bb00..b44df2bb8e 100644 --- a/plugins/zone.cpp +++ b/plugins/zone.cpp @@ -57,14 +57,8 @@ DFHACK_PLUGIN_IS_ENABLED(is_enabled); REQUIRE_GLOBAL(cursor); REQUIRE_GLOBAL(gps); REQUIRE_GLOBAL(plotinfo); -REQUIRE_GLOBAL(ui_building_item_cursor); -REQUIRE_GLOBAL(ui_building_assign_type); -REQUIRE_GLOBAL(ui_building_assign_is_marked); -REQUIRE_GLOBAL(ui_building_assign_units); -REQUIRE_GLOBAL(ui_building_assign_items); -REQUIRE_GLOBAL(ui_building_in_assign); -REQUIRE_GLOBAL(ui_menu_width); REQUIRE_GLOBAL(world); +REQUIRE_GLOBAL(game); static void doMarkForSlaughter(df::unit* unit) { @@ -346,8 +340,9 @@ static bool isInBuiltCageRoom(df::unit* unit) // !!! building->isRoom() returns true if the building can be made a room but currently isn't // !!! except for coffins/tombs which always return false // !!! using the bool is_room however gives the correct state/value - if(!building->is_room) - continue; + // TODO: fixme + /*if(!building->is_room) + continue;*/ if(building->getType() == df::building_type::Cage) { @@ -736,15 +731,16 @@ static void zoneInfo(color_ostream & out, df::building* building, bool verbose) else out << "not active"; - if(civ->zone_flags.bits.pen_pasture) + if(civ->type == df::civzone_type::Pen) out << ", pen/pasture"; - else if (civ->zone_flags.bits.pit_pond) + else if (civ->type == df::civzone_type::AnimalPit) { - out << " (pit flags:" << civ->pit_flags.whole << ")"; - if(civ->pit_flags.bits.is_pond) - out << ", pond"; - else - out << ", pit"; + // TODO: fixme + //out << " (pit flags:" << civ->pit_flags.whole << ")"; + out << ", pit"; + } + else if (civ-> type == df::civzone_type::Pond) { + out << ", pond"; } out << endl; out << "x1:" <x1 @@ -840,38 +836,39 @@ static void chainInfo(color_ostream & out, df::building* building, bool list_ref } } -static df::building* getAssignableBuildingAtCursor(color_ostream& out) +static df::building* getSelectedAssignableBuilding(color_ostream& out) { - // set building at cursor position to be new target building - if (cursor->x == -30000) - { - out.printerr("No cursor; place cursor over activity zone, pen," - " pasture, pit, pond, chain, or cage.\n"); - return NULL; - } - - auto building_at_tile = Buildings::findAtTile(Gui::getCursorPos()); - // cagezone wants a pen/pit as starting point - if (isCage(building_at_tile)) - { - out << "Target building type: cage." << endl; - return building_at_tile; - } - else - { - auto zone_at_tile = Buildings::findPenPitAt(Gui::getCursorPos()); - if(!zone_at_tile) - { - out << "No pen/pasture, pit, or cage under cursor!" << endl; + df::building *selected_building = Gui::getSelectedBuilding(out, true); + if (selected_building) { + if (isCage(selected_building)) { + out << "Target building type: cage.\n"; + return selected_building; + } + else { + out << "No cage selected. \n"; return NULL; } - else - { - out << "Target building type: pen/pasture or pit." << endl; - return zone_at_tile; + } + + df::building_civzonest *selected_civzone = Gui::getSelectedCivZone(out, true); + if(selected_civzone) { + if (Buildings::isPitPond((df::building*)selected_civzone)) { + out << "Target zone type: pit/pond.\n"; + return selected_civzone; + } + else if(Buildings::isPenPasture((df::building*)selected_civzone)) { + out << "Target zone type: pen/pasture.\n"; + return selected_civzone; + } + else { + out << "No pit/pond or pen/pasture selected. \n"; + return NULL; } } + + out.printerr("No pen/pasture, pit, or cage selected!\n"); + return NULL; } // ZONE FILTERS (as in, filters used by 'zone') @@ -927,7 +924,7 @@ static pair> createRaceFilter(vector & return make_pair( "race of " + race, [race](df::unit *unit) -> bool { - return Units::getRaceName(unit) == race; + return toLower(Units::getRaceName(unit)) == toLower(race); } ); } @@ -1043,6 +1040,7 @@ static command_result df_zone(color_ostream &out, vector & parameters) int target_count = 0; + bool zone_info = false; bool unit_info = false; bool unit_slaughter = false; bool building_assign = false; @@ -1066,7 +1064,9 @@ static command_result df_zone(color_ostream &out, vector & parameters) } else if(p0 == "zinfo") { - if (cursor->x == -30000) { + df::building* civzone = Gui::getSelectedCivZone(out); + + if (!civzone && cursor->x == -30000) { out.color(COLOR_RED); out << "No cursor; place cursor over activity zone, chain, or cage." << endl; out.reset_color(); @@ -1077,18 +1077,28 @@ static command_result df_zone(color_ostream &out, vector & parameters) // give info on zone(s), chain or cage under cursor // (doesn't use the findXyzAtCursor() methods because zones might // overlap and contain a cage or chain) - vector zones; - Buildings::findCivzonesAt(&zones, Gui::getCursorPos()); - for (auto zone = zones.begin(); zone != zones.end(); ++zone) - zoneInfo(out, *zone, verbose); - df::building* building = Buildings::findAtTile(Gui::getCursorPos()); - chainInfo(out, building, verbose); - cageInfo(out, building, verbose); - return CR_OK; + if(!civzone) { + vector zones; + Buildings::findCivzonesAt(&zones, Gui::getCursorPos()); + for (auto zone = zones.begin(); zone != zones.end(); ++zone) + zoneInfo(out, *zone, verbose); + df::building* building = Buildings::findAtTile(Gui::getCursorPos()); + chainInfo(out, building, verbose); + cageInfo(out, building, verbose); + zone_info = true; + return CR_OK; + } + else { + zoneInfo(out, civzone, verbose); + chainInfo(out, civzone, verbose); + cageInfo(out, civzone, verbose); + zone_info = true; + return CR_OK; + } } else if(p0 == "set") { - target_building = getAssignableBuildingAtCursor(out); + target_building = getSelectedAssignableBuilding(out); if (target_building) { out.color(COLOR_BLUE); out << "Current building set to #" @@ -1154,15 +1164,26 @@ static command_result df_zone(color_ostream &out, vector & parameters) } if (!target_building_given) { - if(target_building) { - out << "No building id specified. Will use #" - << target_building->id << endl; - } else { - out.color(COLOR_RED); - out << "No building id specified and current one is invalid!" << endl; - out.reset_color(); + df::building_civzonest *selected_building = Gui::getSelectedCivZone(out); + + if(selected_building) { + out << "No building id specified and none set. Will use UI selected building #" + << selected_building->id << endl; + building_assign = true; + start_index = 1; + target_building = (df::building*)selected_building; + } + else { + if(target_building) { + out << "No building id specified. Will use #" + << target_building->id << endl; + } else { + out.color(COLOR_RED); + out << "No building id specified and current one is invalid!" << endl; + out.reset_color(); - return CR_WRONG_USAGE; + return CR_WRONG_USAGE; + } } } @@ -1203,18 +1224,30 @@ static command_result df_zone(color_ostream &out, vector & parameters) } if(!target_building_given) { - if(target_building) { - out << "No " << building_type << " id specified. Will use #" - << target_building->id << endl; + df::building_civzonest *selected_building = Gui::getSelectedCivZone(out); + + if(selected_building) { + out << "No " << building_type << " id specified and none set. Will use UI selected #" + << selected_building->id << endl; building_assign = true; start_index = 1; - } else { - out.color(COLOR_RED); - out << "No " << building_type - << " id specified and current one is invalid!" << endl; - out.reset_color(); + target_building = (df::building*)selected_building; + } + else { + if(target_building) { + out << "No " << building_type << " id specified. Will use #" + << target_building->id << endl; + building_assign = true; + start_index = 1; + } + else { + out.color(COLOR_RED); + out << "No " << building_type + << " id specified and current one is invalid!" << endl; + out.reset_color(); - return CR_WRONG_USAGE; + return CR_WRONG_USAGE; + } } } @@ -1553,7 +1586,7 @@ static command_result df_zone(color_ostream &out, vector & parameters) ); } - if (target_count == 0) + if (target_count == 0 && !unit_info && !zone_info) { out.color(COLOR_RED); out << "No target count! 'zone " << parameters[0] @@ -1763,7 +1796,7 @@ static command_result df_zone(color_ostream &out, vector & parameters) df::unit *unit = Gui::getSelectedUnit(out, true); if (!unit) { out.color(COLOR_RED); - out << "Error: no unit selected!" << endl; + out << "Error: no unit selected! Must provide count/filter if no unit is selected." << endl; out.reset_color(); return CR_WRONG_USAGE; @@ -1788,390 +1821,8 @@ static command_result df_zone(color_ostream &out, vector & parameters) return CR_OK; } -//START zone filters - -class zone_filter -{ -public: - zone_filter() - { - initialized = false; - } - - void initialize(const df::ui_sidebar_mode &mode) - { - if (!initialized) - { - this->mode = mode; - saved_ui_building_assign_type.clear(); - saved_ui_building_assign_units.clear(); - saved_ui_building_assign_items.clear(); - saved_ui_building_assign_is_marked.clear(); - saved_indexes.clear(); - - for (size_t i = 0; i < ui_building_assign_units->size(); i++) - { - saved_ui_building_assign_type.push_back(ui_building_assign_type->at(i)); - saved_ui_building_assign_units.push_back(ui_building_assign_units->at(i)); - saved_ui_building_assign_items.push_back(ui_building_assign_items->at(i)); - saved_ui_building_assign_is_marked.push_back(ui_building_assign_is_marked->at(i)); - } - - search_string.clear(); - show_non_grazers = show_pastured = show_noncaged = show_male = show_female = show_other_zones = true; - entry_mode = false; - - initialized = true; - } - } - - void deinitialize() - { - initialized = false; - } - - void apply_filters() - { - if (saved_indexes.size() > 0) - { - bool list_has_been_sorted = (ui_building_assign_units->size() == reference_list.size() - && *ui_building_assign_units != reference_list); - - for (size_t i = 0; i < saved_indexes.size(); i++) - { - int adjusted_item_index = i; - if (list_has_been_sorted) - { - for (size_t j = 0; j < ui_building_assign_units->size(); j++) - { - if (ui_building_assign_units->at(j) == reference_list[i]) - { - adjusted_item_index = j; - break; - } - } - } - - saved_ui_building_assign_is_marked[saved_indexes[i]] = ui_building_assign_is_marked->at(adjusted_item_index); - } - } - - string search_string_l = toLower(search_string); - saved_indexes.clear(); - ui_building_assign_type->clear(); - ui_building_assign_is_marked->clear(); - ui_building_assign_units->clear(); - ui_building_assign_items->clear(); - - for (size_t i = 0; i < saved_ui_building_assign_units.size(); i++) - { - df::unit *curr_unit = saved_ui_building_assign_units[i]; - - if (!curr_unit) - continue; - - if (!show_non_grazers && !Units::isGrazer(curr_unit)) - continue; - - if (!show_pastured && isAssignedToZone(curr_unit)) - continue; - - if (!show_noncaged) - { - // must be in a container - if(!isContainedInItem(curr_unit)) - continue; - // but exclude built cages (zoos, traps, ...) to avoid "accidental" pitting of creatures you'd prefer to keep - if (isInBuiltCage(curr_unit)) - continue; - } - - if (!show_male && Units::isMale(curr_unit)) - continue; - - if (!show_female && Units::isFemale(curr_unit)) - continue; - - if (!search_string_l.empty()) - { - string desc = Translation::TranslateName( - Units::getVisibleName(curr_unit), false); - - desc += Units::getProfessionName(curr_unit); - desc = toLower(desc); - - if (desc.find(search_string_l) == string::npos) - continue; - } - - ui_building_assign_type->push_back(saved_ui_building_assign_type[i]); - ui_building_assign_units->push_back(curr_unit); - ui_building_assign_items->push_back(saved_ui_building_assign_items[i]); - ui_building_assign_is_marked->push_back(saved_ui_building_assign_is_marked[i]); - - saved_indexes.push_back(i); // Used to map filtered indexes back to original, if needed - } - - reference_list = *ui_building_assign_units; - *ui_building_item_cursor = 0; - } - - bool handle_input(const set *input) - { - if (!initialized) - return false; - - bool key_processed = true; - - if (entry_mode) - { - // Query typing mode - - if (input->count(interface_key::SECONDSCROLL_UP) || input->count(interface_key::SECONDSCROLL_DOWN) || - input->count(interface_key::SECONDSCROLL_PAGEUP) || input->count(interface_key::SECONDSCROLL_PAGEDOWN)) - { - // Arrow key pressed. Leave entry mode and allow screen to process key - entry_mode = false; - return false; - } - - df::interface_key last_token = get_string_key(input); - int charcode = Screen::keyToChar(last_token); - if (charcode >= 32 && charcode <= 126) - { - // Standard character - search_string += char(charcode); - apply_filters(); - } - else if (last_token == interface_key::STRING_A000) - { - // Backspace - if (search_string.length() > 0) - { - search_string.erase(search_string.length()-1); - apply_filters(); - } - } - else if (input->count(interface_key::SELECT) || input->count(interface_key::LEAVESCREEN)) - { - // ENTER or ESC: leave typing mode - entry_mode = false; - } - } - // Not in query typing mode - else if (input->count(interface_key::CUSTOM_SHIFT_G) && - (mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::QueryBuilding)) - { - show_non_grazers = !show_non_grazers; - apply_filters(); - } - else if (input->count(interface_key::CUSTOM_SHIFT_C) && - (mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::ZonesPitInfo || mode == ui_sidebar_mode::QueryBuilding)) - { - show_noncaged = !show_noncaged; - apply_filters(); - } - else if (input->count(interface_key::CUSTOM_SHIFT_P) && - (mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::ZonesPitInfo || mode == ui_sidebar_mode::QueryBuilding)) - { - show_pastured = !show_pastured; - apply_filters(); - } - else if (input->count(interface_key::CUSTOM_SHIFT_M) && - (mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::ZonesPitInfo || mode == ui_sidebar_mode::QueryBuilding)) - { - show_male = !show_male; - apply_filters(); - } - else if (input->count(interface_key::CUSTOM_SHIFT_F) && - (mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::ZonesPitInfo || mode == ui_sidebar_mode::QueryBuilding)) - { - show_female = !show_female; - apply_filters(); - } - else if (input->count(interface_key::CUSTOM_S)) - { - // Hotkey pressed, enter typing mode - entry_mode = true; - } - else if (input->count(interface_key::CUSTOM_SHIFT_S)) - { - // Shift + Hotkey pressed, clear query - search_string.clear(); - apply_filters(); - } - else - { - // Not a key for us, pass it on to the screen - key_processed = false; - } - - return key_processed || entry_mode; // Only pass unrecognized keys down if not in typing mode - } - - void do_render() - { - if (!initialized) - return; - - int left_margin = gps->dimx - 30; - int8_t a = (*ui_menu_width)[0]; - int8_t b = (*ui_menu_width)[1]; - if ((a == 1 && b > 1) || (a == 2 && b == 2)) - left_margin -= 24; - - int x = left_margin; - int y = 24; - - OutputString(COLOR_BROWN, x, y, "DFHack Filtering"); - x = left_margin; - ++y; - OutputString(COLOR_LIGHTGREEN, x, y, "s"); - OutputString(COLOR_WHITE, x, y, ": Search"); - if (!search_string.empty() || entry_mode) - { - OutputString(COLOR_WHITE, x, y, ": "); - if (!search_string.empty()) - OutputString(COLOR_WHITE, x, y, search_string); - if (entry_mode) - OutputString(COLOR_LIGHTGREEN, x, y, "_"); - } - - if (mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::QueryBuilding) - { - x = left_margin; - y += 2; - OutputString(COLOR_LIGHTGREEN, x, y, "G"); - OutputString(COLOR_WHITE, x, y, ": "); - OutputString((show_non_grazers) ? COLOR_WHITE : COLOR_GREY, x, y, "Non-Grazing"); - - x = left_margin; - ++y; - OutputString(COLOR_LIGHTGREEN, x, y, "C"); - OutputString(COLOR_WHITE, x, y, ": "); - OutputString((show_noncaged) ? COLOR_WHITE : COLOR_GREY, x, y, "Not Caged"); - - x = left_margin; - ++y; - OutputString(COLOR_LIGHTGREEN, x, y, "P"); - OutputString(COLOR_WHITE, x, y, ": "); - OutputString((show_pastured) ? COLOR_WHITE : COLOR_GREY, x, y, "Currently Pastured"); - - x = left_margin; - ++y; - OutputString(COLOR_LIGHTGREEN, x, y, "F"); - OutputString(COLOR_WHITE, x, y, ": "); - OutputString((show_female) ? COLOR_WHITE : COLOR_GREY, x, y, "Female"); - - x = left_margin; - ++y; - OutputString(COLOR_LIGHTGREEN, x, y, "M"); - OutputString(COLOR_WHITE, x, y, ": "); - OutputString((show_male) ? COLOR_WHITE : COLOR_GREY, x, y, "Male"); - } - - // pits don't have grazer filter because it seems pointless - if (mode == ui_sidebar_mode::ZonesPitInfo) - { - x = left_margin; - y += 2; - OutputString(COLOR_LIGHTGREEN, x, y, "C"); - OutputString(COLOR_WHITE, x, y, ": "); - OutputString((show_noncaged) ? COLOR_WHITE : COLOR_GREY, x, y, "Not Caged"); - - x = left_margin; - ++y; - OutputString(COLOR_LIGHTGREEN, x, y, "P"); - OutputString(COLOR_WHITE, x, y, ": "); - OutputString((show_pastured) ? COLOR_WHITE : COLOR_GREY, x, y, "Currently Pastured"); - - x = left_margin; - ++y; - OutputString(COLOR_LIGHTGREEN, x, y, "F"); - OutputString(COLOR_WHITE, x, y, ": "); - OutputString((show_female) ? COLOR_WHITE : COLOR_GREY, x, y, "Female"); - - x = left_margin; - ++y; - OutputString(COLOR_LIGHTGREEN, x, y, "M"); - OutputString(COLOR_WHITE, x, y, ": "); - OutputString((show_male) ? COLOR_WHITE : COLOR_GREY, x, y, "Male"); - } - } - -private: - df::ui_sidebar_mode mode; - string search_string; - bool initialized; - bool entry_mode; - bool show_non_grazers, show_pastured, show_noncaged, show_male, show_female, show_other_zones; - - std::vector saved_ui_building_assign_type; - std::vector saved_ui_building_assign_units, reference_list; - std::vector saved_ui_building_assign_items; - std::vector saved_ui_building_assign_is_marked; - - vector saved_indexes; - -}; - -struct zone_hook : public df::viewscreen_dwarfmodest -{ - typedef df::viewscreen_dwarfmodest interpose_base; - static zone_filter filter; - - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!filter.handle_input(input)) - INTERPOSE_NEXT(feed)(input); - } - - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - if ( ( (plotinfo->main.mode == ui_sidebar_mode::ZonesPenInfo || plotinfo->main.mode == ui_sidebar_mode::ZonesPitInfo) && - ui_building_assign_type && ui_building_assign_units && - ui_building_assign_is_marked && ui_building_assign_items && - ui_building_assign_type->size() == ui_building_assign_units->size() && - ui_building_item_cursor) - // allow mode QueryBuilding, but only for cages (bedrooms will crash DF with this code, chains don't work either etc) - || - ( plotinfo->main.mode == ui_sidebar_mode::QueryBuilding && - ui_building_in_assign && *ui_building_in_assign && - ui_building_assign_type && ui_building_assign_units && - ui_building_assign_type->size() == ui_building_assign_units->size() && - ui_building_assign_type->size() == ui_building_assign_items->size() && - ui_building_assign_type->size() == ui_building_assign_is_marked->size() && - ui_building_item_cursor && - world->selected_building && isCage(world->selected_building) ) - ) - { - if (vector_get(*ui_building_assign_units, *ui_building_item_cursor)) - filter.initialize(plotinfo->main.mode); - } - else - { - filter.deinitialize(); - } - - INTERPOSE_NEXT(render)(); - - filter.do_render(); - - } -}; - -zone_filter zone_hook::filter; - -IMPLEMENT_VMETHOD_INTERPOSE(zone_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(zone_hook, render); -//END zone filters - DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { - if (enable != is_enabled) { - if (!INTERPOSE_HOOK(zone_hook, feed).apply(enable) || - !INTERPOSE_HOOK(zone_hook, render).apply(enable)) - return CR_FAILURE; - + if (is_enabled != enable) { is_enabled = enable; }