diff --git a/allneeds.lua b/allneeds.lua index f23ab95351..a3fa117cf1 100644 --- a/allneeds.lua +++ b/allneeds.lua @@ -19,6 +19,18 @@ if not sorts[sort] then qerror(('unknown sort: "%s"'):format(sort)) end +local fulfillment_threshold = + { 300, 200, 100, -999, -9999, -99999 } + +local function getFulfillment(focus_level) + for i = 1, 6 do + if focus_level >= fulfillment_threshold[i] then + return i + end + end + return 7 +end + local fort_needs = {} local units = dfhack.gui.getSelectedUnit(true) @@ -39,6 +51,10 @@ for _, unit in ipairs(units) do needs.strength = (needs.strength or 0) + need.need_level needs.focus = (needs.focus or 0) + need.focus_level needs.freq = (needs.freq or 0) + 1 + + local level = getFulfillment(need.focus_level) + ensure_key(needs, 'fulfillment', {0, 0, 0, 0, 0, 0, 0}) + needs.fulfillment[level] = needs.fulfillment[level] + 1 end end @@ -49,15 +65,20 @@ for id, need in pairs(fort_needs) do strength=need.strength, focus=need.focus, freq=need.freq, + fulfillment=need.fulfillment }) end table.sort(sorted_fort_needs, sorts[sort]) -- Print sorted output -local fmt = '%20s %8s %12s %9s' -print(fmt:format("Need", "Strength", "Focus Impact", "Frequency")) -print(fmt:format("----", "--------", "------------", "---------")) +local fmt = '%20s %8s %12s %9s %35s' +print(fmt:format("Need", "Strength", "Focus Impact", "Frequency", "Num. Unfettered -> Badly distracted")) +print(fmt:format("----", "--------", "------------", "---------", "-----------------------------------")) for _, need in ipairs(sorted_fort_needs) do - print(fmt:format(need.id, need.strength, need.focus, need.freq)) + local res = "" + for i = 1, 7 do + res = res..(('%5d'):format(need.fulfillment[i])) + end + print(fmt:format(need.id, need.strength, need.focus, need.freq, res)) end diff --git a/ban-cooking.lua b/ban-cooking.lua index 2c309a8293..fdb59fbe05 100644 --- a/ban-cooking.lua +++ b/ban-cooking.lua @@ -137,30 +137,29 @@ funcs.seeds = function() if p.material_defs.type.seed == -1 or p.material_defs.idx.seed == -1 or p.flags.TREE then goto continue end ban_cooking(p.name .. ' seeds', p.material_defs.type.seed, p.material_defs.idx.seed, df.item_type.SEEDS, -1) for _, m in ipairs(p.material) do - if m.id == "STRUCTURAL" and m.flags.EDIBLE_COOKED then - local has_drink = false - local has_seed = false - for _, s in ipairs(m.reaction_product.id) do - has_seed = has_seed or s.value == "SEED_MAT" - has_drink = has_drink or s.value == "DRINK_MAT" - end - if has_seed and has_drink then - local matinfo = dfhack.matinfo.find(p.id, m.id) - ban_cooking(p.name .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.PLANT, -1) + if m.id == "STRUCTURAL" then + if m.flags.EDIBLE_COOKED then + local has_seed = false + for _, s in ipairs(m.reaction_product.id) do + has_seed = has_seed or s.value == "SEED_MAT" + end + if has_seed then + local matinfo = dfhack.matinfo.find(p.id, m.id) + ban_cooking(p.name .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.PLANT, -1) + end end + break end end for k, g in ipairs(p.growths) do local matinfo = dfhack.matinfo.decode(g) local m = matinfo.material if m.flags.EDIBLE_COOKED then - local has_drink = false local has_seed = false for _, s in ipairs(m.reaction_product.id) do has_seed = has_seed or s.value == "SEED_MAT" - has_drink = has_drink or s.value == "DRINK_MAT" end - if has_seed and has_drink then + if has_seed then ban_cooking(p.name .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.PLANT_GROWTH, k) end end @@ -174,14 +173,18 @@ funcs.brew = function() for _, p in ipairs(df.global.world.raws.plants.all) do if p.material_defs.type.drink == -1 or p.material_defs.idx.drink == -1 then goto continue end for _, m in ipairs(p.material) do - if m.id == "STRUCTURAL" and m.flags.EDIBLE_COOKED then - for _, s in ipairs(m.reaction_product.id) do - if s.value == "DRINK_MAT" then - local matinfo = dfhack.matinfo.find(p.id, m.id) - ban_cooking(p.name .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.PLANT, -1) - break + if m.id == "STRUCTURAL" then + if m.flags.EDIBLE_COOKED then + for _, s in ipairs(m.reaction_product.id) do + if s.value == "DRINK_MAT" then + local matinfo = dfhack.matinfo.find(p.id, m.id) + ban_cooking(p.name .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.PLANT, -1) + break + end end end + -- Stop iterating materials since there is only one STRUCTURAL + break end end for k, g in ipairs(p.growths) do @@ -205,9 +208,12 @@ funcs.mill = function() for _, p in ipairs(df.global.world.raws.plants.all) do if p.material_defs.idx.mill ~= -1 then for _, m in ipairs(p.material) do - if m.id == "STRUCTURAL" and m.flags.EDIBLE_COOKED then - local matinfo = dfhack.matinfo.find(p.id, m.id) - ban_cooking(p.name .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.PLANT, -1) + if m.id == "STRUCTURAL" then + if m.flags.EDIBLE_COOKED then + local matinfo = dfhack.matinfo.find(p.id, m.id) + ban_cooking(p.name .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.PLANT, -1) + end + break end end end @@ -218,14 +224,17 @@ funcs.thread = function() for _, p in ipairs(df.global.world.raws.plants.all) do if p.material_defs.idx.thread == -1 then goto continue end for _, m in ipairs(p.material) do - if m.id == "STRUCTURAL" and m.flags.EDIBLE_COOKED then - for _, s in ipairs(m.reaction_product.id) do - if s.value == "THREAD" then - local matinfo = dfhack.matinfo.find(p.id, m.id) - ban_cooking(p.name .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.PLANT, -1) - break + if m.id == "STRUCTURAL" then + if m.flags.EDIBLE_COOKED then + for _, s in ipairs(m.reaction_product.id) do + if s.value == "THREAD" then + local matinfo = dfhack.matinfo.find(p.id, m.id) + ban_cooking(p.name .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.PLANT, -1) + break + end end end + break end end for k, g in ipairs(p.growths) do diff --git a/changelog.txt b/changelog.txt index 288bd442d0..d0f1ac59a3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,15 +34,22 @@ Template for new versions: - `full-heal`: fix ``-r --all_citizens`` option combination not resurrecting citizens - `open-legends`: don't intercept text bound for vanilla search widgets - `gui/unit-info-viewer`: correctly display skill levels when rust is involved +- `timestream`: fix dwarves spending too long eating and drinking +- `locate-ore`: fix sometimes selecting an incorrect tile when there are multiple mineral veins in a single map block +- `gui/settings-manager`: fix position of "settings restored" message on embark when the player has no saved embark profiles - `build-now`: fix error when building buildings that (in previous DF versions) required the architecture labor - `prioritize`: fix incorrect loading of persisted data on some OS types - `list-waves`: no longer gets confused by units that leave the map and then return (e.g. squads who go out on raids) - `fix/dead-units`: fix error when removing dead units from burrows and the unit with the greatest ID was dead +- `makeown`: ensure names given to adopted units (or units created with `gui/sandbox`) are respected later in legends mode +- `gui/autodump`: prevent dumping into walls or invalid map area, as well as selecting in unallocated blocks +- `gui/autodump`: set proper projectile flags to prevent items from being destroyed ## Misc Improvements - `build-now`: if `suspendmanager` is running, run an unsuspend cycle immediately before scanning for buildings to build - `list-waves`: now outputs the names of the dwarves in each migration wave - `list-waves`: can now display information about specific migration waves (like identifying your starting 7 dwarves) +- `allneeds`: display counts of fulfillment degrees as a separate column ## Removed @@ -93,6 +100,7 @@ Template for new versions: - `clear-smoke`: properly tag smoke flows for garbage collection to avoid memory leak - `warn-stranded`: don't warn for babies carried by mothers who happen to be gathering fruit from trees - `prioritize`: also boost priority of already-claimed jobs when boosting priority of a job type so those jobs are not interrupted +- `ban-cooking`: ban all seed producing items from being cooked when 'seeds' is chosen instead of just brewable seed producing items ## Misc Improvements - `item`: option for ignoring uncollected spider webs when you search for "silk" diff --git a/docs/ban-cooking.rst b/docs/ban-cooking.rst index 0c53799b95..38d2ba2b1f 100644 --- a/docs/ban-cooking.rst +++ b/docs/ban-cooking.rst @@ -45,7 +45,7 @@ Valid types are: - ``milk`` - ``mill`` (millable plants) - ``oil`` -- ``seeds`` (plantable seeds) +- ``seeds`` (plantable seeds and items producing seeds) - ``tallow`` - ``thread`` diff --git a/docs/empty-bin.rst b/docs/empty-bin.rst index 1cd6b9bb29..1d45eb81aa 100644 --- a/docs/empty-bin.rst +++ b/docs/empty-bin.rst @@ -9,9 +9,6 @@ This tool can quickly empty the contents of the selected container (bin, barrel, pot, wineskin, quiver, etc.) onto the floor, allowing you to access individual items that might otherwise be hard to get to. -Note that if there are liquids in the container, they will empty onto the floor -and become unusable. - If you instead select a stockpile or building, running `empty-bin` will empty *all* containers in the stockpile or building. Likewise, if you select a tile that has many items and the UI is showing the list of items, all containers on @@ -22,4 +19,24 @@ Usage :: - empty-bin + empty-bin [] + +Examples +-------- + +``empty-bin`` + Empty the contents of selected containers or all containers in the selected stockpile or building, except containers with liquids, onto the floor. + +``empty-bin --liquids`` + Empty the contents of selected containers or all containers in the selected stockpile or building, including containers with liquids, onto the floor. + +``empty-bin --recursive --liquids`` + Empty the contents of selected containers or all containers in the selected stockpile or building, including containers with liquids and containers contents that are containers, such as a bags of seeds or filled waterskins, onto the floor. + +Options +-------------- + +``-r``, ``--recursive`` + Recursively empty containers. +``-l``, ``--liquids`` + Move contained liquids (DRINK and LIQUID_MISC) to the floor, making them unusable. diff --git a/docs/gui/autodump.rst b/docs/gui/autodump.rst index d20ca0021f..cad8c4f4eb 100644 --- a/docs/gui/autodump.rst +++ b/docs/gui/autodump.rst @@ -9,8 +9,9 @@ This is a general point and click interface for teleporting or destroying items. By default, it will teleport items you have marked for dumping, but if you draw boxes around items on the map, it will act on the selected items instead. Double-click anywhere on the map to teleport the items there. Be wary -(or excited) that if you teleport the items into an unsupported position (e.g. -mid-air), then they will become projectiles and fall. +(or excited) that if you teleport the items into an unsupported position +(e.g., mid-air), then they will become projectiles and fall. Items may not be +teleported into walls or hidden tiles. There are options to include or exclude forbidden items, items that are currently tagged as being used by an active job, and items dropped by traders. diff --git a/docs/locate-ore.rst b/docs/locate-ore.rst index c25a1c880f..d2fbf814b2 100644 --- a/docs/locate-ore.rst +++ b/docs/locate-ore.rst @@ -6,17 +6,21 @@ locate-ore :tags: fort armok productivity map This tool finds and designates for digging one tile of a specific metal ore. If -you want to dig **all** tiles of that kind of ore, select that tile with the -cursor and run `digtype `. +you want to dig **all** tiles of that kind of ore, highlight that tile with the +keyboard cursor and run `digtype `. -By default, the tool only searches for visible ore veins. +By default, the tool only searches ore veins that your dwarves have discovered. + +Note that looking for a particular metal might find an ore that contains that +metal along with other metals. For example, locating silver may find +tetrahedrite, which contains silver and copper. Usage ----- -``locate-ore list`` +``locate-ore [list] []`` List metal ores available on the map. -``locate-ore `` +``locate-ore []`` Finds a tile of the specified ore type, zooms the screen so that tile is visible, and designates that tile for digging. @@ -24,17 +28,16 @@ Options ------- ``-a``, ``--all`` - Allow undiscovered ore veins to be marked. + Also search undiscovered ore veins. Examples -------- +``locate-ore`` + List discovered + :: locate-ore hematite locate-ore iron locate-ore silver --all - -Note that looking for a particular metal might find an ore that contains that -metal along with other metals. For example, locating silver may find -tetrahedrite, which contains silver and copper. diff --git a/empty-bin.lua b/empty-bin.lua index 3f10225e65..f94a140816 100644 --- a/empty-bin.lua +++ b/empty-bin.lua @@ -1,30 +1,53 @@ -- Empty a bin onto the floor --- Based on "emptybin" by StoneToad +-- Based on 'emptybin' by StoneToad -- https://gist.github.com/stonetoad/11129025 -- http://dwarffortresswiki.org/index.php/DF2014_Talk:Bin -local function moveItem(item, to_pos) - print(' ' .. dfhack.items.getReadableDescription(item)) - dfhack.items.moveToGround(item, to_pos) -end +local argparse = require('argparse') + +local options, args = { + help = false, + recursive = false, + liquids = false + }, + {...} local function emptyContainer(container) local items = dfhack.items.getContainedItems(container) - if #items > 0 then print('Emptying ' .. dfhack.items.getReadableDescription(container)) local pos = xyz2pos(dfhack.items.getPosition(container)) for _, item in ipairs(items) do - moveItem(item, pos) + local skip_liquid = item:getType() == df.item_type.LIQUID_MISC or item:getType() == df.item_type.DRINK and not options.liquids + if skip_liquid then + print(' ' .. dfhack.items.getReadableDescription(item) .. ' was skipped because the --liquids flag was not provided') + else + print(' ' .. dfhack.items.getReadableDescription(item)) + dfhack.items.moveToGround(item, pos) + if options.recursive then + emptyContainer(item) + end + end end end end -local viewsheets = df.global.game.main_interface.view_sheets +argparse.processArgsGetopt(args,{ + { 'h', 'help', handler = function() options.help = true end }, + { 'r', 'recursive', handler = function() options.recursive = true end }, + { 'l', 'liquids', handler = function() options.liquids = true end } + }) +if options.help then + print(dfhack.script_help()) + return +end + +local viewsheets = df.global.game.main_interface.view_sheets local stockpile = dfhack.gui.getSelectedStockpile(true) local selectedItem = dfhack.gui.getSelectedItem(true) local selectedBuilding = dfhack.gui.getSelectedBuilding(true) + if stockpile then local contents = dfhack.buildings.getStockpileContents(stockpile) for _, container in ipairs(contents) do @@ -33,7 +56,9 @@ if stockpile then elseif selectedItem then emptyContainer(selectedItem) elseif selectedBuilding then - if not df.building_actual:is_instance(selectedBuilding) then return end + if not selectedBuilding:isActual() then + return + end for _, contained in ipairs(selectedBuilding.contained_items) do if contained.use_mode == df.building_item_role_type.TEMP then emptyContainer(contained.item) @@ -45,5 +70,5 @@ elseif viewsheets.open then emptyContainer(item) end else - qerror("Please select a container, building, stockpile, or tile with a list of items.") + qerror('Please select a container, building, stockpile, or tile with a list of items.') end diff --git a/fix/empty-wheelbarrows.lua b/fix/empty-wheelbarrows.lua index b3c0234832..00c00a9bfe 100644 --- a/fix/empty-wheelbarrows.lua +++ b/fix/empty-wheelbarrows.lua @@ -1,15 +1,12 @@ ---checks all wheelbarrows on map for rocks stuck in them. If a wheelbarrow isn't in use for a job (hauling) then there should be no rocks in them ---rocks will occasionally get stuck in wheelbarrows, and accumulate if the wheelbarrow gets used. ---this script empties all wheelbarrows which have rocks stuck in them. +-- checks all wheelbarrows on map for rocks stuck in them and empties such rocks onto the ground. If a wheelbarrow +-- isn't in use for a job (hauling) then there should be no rocks in them. local argparse = require("argparse") -local args = {...} - local quiet = false local dryrun = false -local cmds = argparse.processArgsGetopt(args, { +argparse.processArgsGetopt({...}, { {'q', 'quiet', handler=function() quiet = true end}, {'d', 'dry-run', handler=function() dryrun = true end}, }) @@ -17,24 +14,31 @@ local cmds = argparse.processArgsGetopt(args, { local i_count = 0 local e_count = 0 -local function emptyContainedItems(e, outputCallback) - local items = dfhack.items.getContainedItems(e) - if #items > 0 then - outputCallback('Emptying wheelbarrow: ' .. dfhack.items.getDescription(e, 0)) - e_count = e_count + 1 - for _,i in ipairs(items) do - outputCallback(' ' .. dfhack.items.getDescription(i, 0)) - if (not dryrun) then dfhack.items.moveToGround(i, e.pos) end - i_count = i_count + 1 +local function emptyContainedItems(wheelbarrow, outputCallback) + local items = dfhack.items.getContainedItems(wheelbarrow) + if #items == 0 then return end + outputCallback('Emptying wheelbarrow: ' .. dfhack.items.getReadableDescription(wheelbarrow)) + e_count = e_count + 1 + for _,item in ipairs(items) do + outputCallback(' ' .. dfhack.items.getReadableDescription(item)) + if not dryrun then + if item.flags.in_job then + local job_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + if job_ref then + dfhack.job.removeJob(job_ref.data.job) + end + end + dfhack.items.moveToGround(item, wheelbarrow.pos) end + i_count = i_count + 1 end end local function emptyWheelbarrows(outputCallback) - for _,e in ipairs(df.global.world.items.other.TOOL) do + for _,item in ipairs(df.global.world.items.other.TOOL) do -- wheelbarrow must be on ground and not in a job - if ((not e.flags.in_job) and e.flags.on_ground and e:isWheelbarrow()) then - emptyContainedItems(e, outputCallback) + if ((not item.flags.in_job) and item.flags.on_ground and item:isWheelbarrow()) then + emptyContainedItems(item, outputCallback) end end end @@ -44,6 +48,7 @@ if (quiet) then output = (function(...) end) else output = print end emptyWheelbarrows(output) -if (i_count > 0 or (not quiet)) then - print(("fix/empty-wheelbarrows - removed %d items from %d wheelbarrows."):format(i_count, e_count)) +if i_count > 0 or not quiet then + local action = dryrun and 'would remove' or 'removed' + print(("fix/empty-wheelbarrows - %s %d item(s) from %d wheelbarrow(s)."):format(action, i_count, e_count)) end diff --git a/gui/autodump.lua b/gui/autodump.lua index 21a08c0e3c..d3ec16c840 100644 --- a/gui/autodump.lua +++ b/gui/autodump.lua @@ -229,7 +229,7 @@ function Autodump:select_box(bounds) for x=bounds.x1,bounds.x2 do local block = dfhack.maps.getTileBlock(xyz2pos(x, y, z)) local block_str = tostring(block) - if not seen_blocks[block_str] then + if block and not seen_blocks[block_str] then seen_blocks[block_str] = true self:select_items_in_block(block, bounds) end @@ -308,12 +308,26 @@ end function Autodump:do_dump(pos) pos = pos or dfhack.gui.getMousePos() - if not pos then return end - local tileattrs = df.tiletype.attrs[dfhack.maps.getTileType(pos)] - local basic_shape = df.tiletype_shape.attrs[tileattrs.shape].basic_shape - local on_ground = basic_shape == df.tiletype_shape_basic.Floor or - basic_shape == df.tiletype_shape_basic.Stair or - basic_shape == df.tiletype_shape_basic.Ramp + if not pos then --We check this before calling + qerror('Autodump:do_dump called with bad pos!') + end + + local tt = dfhack.maps.getTileType(pos) + if not (tt and dfhack.maps.isTileVisible(pos)) then + dfhack.printerr('Dump tile not visible! Must be in a revealed area of map.') + return + end + + local on_ground + local shape = df.tiletype.attrs[tt].shape + local shape_attrs = df.tiletype_shape.attrs[shape] + if shape_attrs.walkable or shape == df.tiletype_shape.FORTIFICATION then + on_ground = true --Floor, stair, ramp, or fortification + elseif shape_attrs.basic_shape == df.tiletype_shape_basic.Wall then + dfhack.printerr('Dump tile blocked! Can\'t dump inside walls.') --Wall or brook bed + return + end + local items = #self.selected_items.list > 0 and self.selected_items.list or self.dump_items local mark_as_forbidden = self.subviews.mark_as_forbidden:getOptionValue() print(('teleporting %d items'):format(#items)) @@ -331,7 +345,15 @@ function Autodump:do_dump(pos) item.flags.forbid = true end if not on_ground then - dfhack.items.makeProjectile(item) + local proj = dfhack.items.makeProjectile(item) + if proj then + proj.flags.no_impact_destroy = true + proj.flags.bouncing = true + proj.flags.piercing = true + proj.flags.parabolic = true + proj.flags.no_adv_pause = true + proj.flags.no_collide = true + end end else print(('Could not move item: %s from (%d, %d, %d)'):format( diff --git a/gui/companion-order.lua b/gui/companion-order.lua index 9f5449d3c1..9659c79785 100644 --- a/gui/companion-order.lua +++ b/gui/companion-order.lua @@ -62,13 +62,8 @@ function getxyz() -- this will return pointers x,y and z coordinates. return x,y,z -- return the coords end -function GetCaste(race_id,caste_id) - local race=df.creature_raw.find(race_id) - return race.caste[caste_id] -end - function EnumBodyEquipable(race_id,caste_id) - local caste=GetCaste(race_id,caste_id) + local caste=dfhack.units.getCasteRaw(race_id,caste_id) local bps=caste.body_info.body_parts local ret={} for k,v in pairs(bps) do @@ -149,7 +144,7 @@ function AddIfFits(body_equip,unit,item) return false end function EnumGrasps(race_id,caste_id) - local caste=GetCaste(race_id,caste_id) + local caste=dfhack.units.getCasteRaw(race_id,caste_id) local bps=caste.body_info.body_parts local ret={} for k,v in pairs(bps) do diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index c5c478f9ea..f138c50d87 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -58,13 +58,6 @@ end -- DimsPanel -- -local function get_dims(pos1, pos2) - local width, height, depth = math.abs(pos1.x - pos2.x) + 1, - math.abs(pos1.y - pos2.y) + 1, - math.abs(pos1.z - pos2.z) + 1 - return width, height, depth -end - DimsPanel = defclass(DimsPanel, widgets.ResizingPanel) DimsPanel.ATTRS{ get_mark_fn=DEFAULT_NIL, diff --git a/gui/settings-manager.lua b/gui/settings-manager.lua index 12a8a2e35e..1a7e468bf5 100644 --- a/gui/settings-manager.lua +++ b/gui/settings-manager.lua @@ -173,13 +173,13 @@ DifficultyEmbarkNotificationOverlay.ATTRS { default_pos={x=75, y=18}, viewscreens='setupdwarfgame/Default', default_enabled=true, - frame={w=23, h=3}, + frame={w=25, h=3}, } function DifficultyEmbarkNotificationOverlay:init() self:addviews{ widgets.Panel{ - frame={t=0, w=25}, + frame={h=3, b=0, w=25}, frame_style=gui.FRAME_MEDIUM, frame_background=gui.CLEAR_PEN, subviews={ @@ -197,6 +197,12 @@ function DifficultyEmbarkNotificationOverlay:preUpdateLayout(parent_rect) self.frame.w = parent_rect.width - (self.frame.l or (self.default_pos.x - 1)) end +function DifficultyEmbarkNotificationOverlay:render(dc) + local scr = dfhack.gui.getDFViewscreen(true) + self.frame.h = #scr.embark_profile == 0 and 11 or 3 + DifficultyEmbarkNotificationOverlay.super.render(self, dc) +end + local last_scr_type dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc ~= SC_VIEWSCREEN_CHANGED then return end diff --git a/gui/unit-info-viewer.lua b/gui/unit-info-viewer.lua index 56751e9fd1..ed677453a9 100644 --- a/gui/unit-info-viewer.lua +++ b/gui/unit-info-viewer.lua @@ -168,10 +168,6 @@ local function get_death_type(death_cause) return DEATH_TYPES[death_cause] or ' died of unknown causes' end -local function get_caste_data(unit) - return df.global.world.raws.creatures.all[unit.race].caste[unit.caste] -end - local function get_creature_data(unit) return df.global.world.raws.creatures.all[unit.race] end @@ -190,7 +186,7 @@ local function get_translated_name_chunk(unit) end local function get_description_chunk(unit) - local desc = get_caste_data(unit).description + local desc = dfhack.units.getCasteRaw(unit).description if #desc == 0 then return end return {text=desc, pen=COLOR_WHITE} end @@ -271,7 +267,7 @@ end local function get_max_age_chunk(unit) if not dfhack.units.isAlive(unit) then return end - local caste = get_caste_data(unit) + local caste = dfhack.units.getCasteRaw(unit) local blurb if caste.misc.maxage_min == -1 then blurb = ' only die of unnatural causes.' @@ -395,7 +391,7 @@ end local function get_grazer_chunk(unit) if not dfhack.units.isGrazer(unit) then return end - local caste = get_caste_data(unit) + local caste = dfhack.units.getCasteRaw(unit) local blurb = 'Grazing satisfies ' .. tostring(caste.misc.grazer) .. ' units of hunger.' return {text=blurb, pen=COLOR_LIGHTGREEN} end @@ -403,7 +399,7 @@ end local function get_milkable_chunk(unit) if not dfhack.units.isAlive(unit) or not dfhack.units.isMilkable(unit) then return end if not dfhack.units.isAnimal(unit) then return end - local caste = get_caste_data(unit) + local caste = dfhack.units.getCasteRaw(unit) local milk = dfhack.matinfo.decode(caste.extracts.milkable_mat, caste.extracts.milkable_matidx) if not milk then return end local days, seconds = math.modf(caste.misc.milkable / TU_PER_DAY) @@ -419,7 +415,7 @@ end local function get_shearable_chunk(unit) if not dfhack.units.isAlive(unit) then return end if not dfhack.units.isAnimal(unit) then return end - local caste = get_caste_data(unit) + local caste = dfhack.units.getCasteRaw(unit) local mat_types = caste.body_info.materials.mat_type local mat_idxs = caste.body_info.materials.mat_index for idx, mat_type in ipairs(mat_types) do @@ -438,7 +434,7 @@ end local function get_egg_layer_chunk(unit) if not dfhack.units.isAlive(unit) or not dfhack.units.isEggLayer(unit) then return end - local caste = get_caste_data(unit) + local caste = dfhack.units.getCasteRaw(unit) local clutch = (caste.misc.clutch_size_max + caste.misc.clutch_size_min) // 2 local blurb = ('She lays clutches of about %d egg%s.'):format(clutch, clutch == 1 and '' or 's') return {text=blurb, pen=COLOR_GREEN} diff --git a/gui/unit-syndromes.lua b/gui/unit-syndromes.lua index b8ea3a37ee..b789bfd97d 100644 --- a/gui/unit-syndromes.lua +++ b/gui/unit-syndromes.lua @@ -306,7 +306,7 @@ local function getLivestock() local units = {} for _, unit in pairs(df.global.world.units.active) do - local caste_flags = unit.caste and df.global.world.raws.creatures.all[unit.race].caste[unit.caste].flags + local caste_flags = dfhack.units.getCasteRaw(unit).flags if dfhack.units.isFortControlled(unit) and caste_flags and (caste_flags.PET or caste_flags.PET_EXOTIC) then table.insert(units, unit) diff --git a/internal/gm-unit/editor_colors.lua b/internal/gm-unit/editor_colors.lua index 57a7c9fde5..decebda005 100644 --- a/internal/gm-unit/editor_colors.lua +++ b/internal/gm-unit/editor_colors.lua @@ -64,7 +64,7 @@ end function Editor_Colors:random() local featureChoiceIndex, featureChoice = self.subviews.features:getSelected() -- This is the part / feature that's selected - local caste = df.creature_raw.find(self.target_unit.race).caste[self.target_unit.caste] + local caste = dfhack.units.getCasteRaw(self.target_unit) -- Nil check in case there are no features if featureChoiceIndex == nil then @@ -136,7 +136,7 @@ function Editor_Colors:featureSelected(index, choice) end function Editor_Colors:updateChoices() - local caste = df.creature_raw.find(self.target_unit.race).caste[self.target_unit.caste] + local caste = dfhack.units.getCasteRaw(self.target_unit) local choices = {} for index, colorMod in ipairs(caste.color_modifiers) do table.insert(choices, {text = colorMod.part:gsub("^%l", string.upper), mod = colorMod, index = index}) diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index 5273d83b8e..b06f139d90 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -105,10 +105,8 @@ local function for_moody(fn, reverse) end, fn, reverse) end -local races = df.global.world.raws.creatures.all - local function is_stealer(unit) - local casteFlags = races[unit.race].caste[unit.caste].flags + local casteFlags = dfhack.units.getCasteRaw(unit).flags if casteFlags.CURIOUS_BEAST_EATER or casteFlags.CURIOUS_BEAST_GUZZLER or casteFlags.CURIOUS_BEAST_ITEM @@ -224,7 +222,7 @@ end local function summarize_units(for_fn) local counts = {} for_fn(function(unit) - local names = races[unit.race].caste[unit.caste].caste_name + local names = dfhack.units.getCasteRaw(unit).caste_name local record = ensure_key(counts, names[0], {count=0, plural=names[1]}) record.count = record.count + 1 end) diff --git a/locate-ore.lua b/locate-ore.lua index 6fbf9c67d5..a9e027fbbf 100644 --- a/locate-ore.lua +++ b/locate-ore.lua @@ -2,15 +2,11 @@ local argparse = require('argparse') -local tile_attrs = df.tiletype.attrs - local function extractKeys(target_table) local keyset = {} - for k, _ in pairs(target_table) do table.insert(keyset, k) end - return keyset end @@ -40,24 +36,6 @@ local function getRandomFromTable(target_table) return target_table[key] end -local function randomSort(target_table) - local rnd = {} - table.sort( target_table, - function ( a, b) - rnd[a] = rnd[a] or math.random() - rnd[b] = rnd[b] or math.random() - return rnd[a] > rnd[b] - end ) -end - -local function sequence(min, max) - local tbl = {} - for i=min,max do - table.insert(tbl, i) - end - return tbl -end - local function sortTableBy(tbl, sort_func) local sorted = {} for _, value in pairs(tbl) do @@ -80,14 +58,21 @@ local function matchesMetalOreById(mat_indices, target_ore) return false end -local function findOreVeins(target_ore, show_undiscovered) - if target_ore then - target_ore = string.lower(target_ore) - end +local tile_attrs = df.tiletype.attrs - local ore_veins = {} - for _, block in pairs(df.global.world.map.map_blocks) do - for _, bevent in pairs(block.block_events) do +local function isValidMineralTile(opts, pos, check_designation) + if not opts.all and not dfhack.maps.isTileVisible(pos) then return false end + local tt = dfhack.maps.getTileType(pos) + if not tt then return false end + return tile_attrs[tt].material == df.tiletype_material.MINERAL and + (not check_designation or dfhack.maps.getTileFlags(pos).dig == df.tile_dig_designation.No) and + tile_attrs[tt].shape == df.tiletype_shape.WALL +end + +local function findOres(opts, check_designation, target_ore) + local ore_types = {} + for _, block in ipairs(df.global.world.map.map_blocks) do + for _, bevent in ipairs(block.block_events) do if bevent:getType() ~= df.block_square_event_type.mineral then goto skipevent end @@ -97,130 +82,99 @@ local function findOreVeins(target_ore, show_undiscovered) goto skipevent end - if not show_undiscovered and not bevent.flags.discovered then + if not opts.all and not bevent.flags.discovered then goto skipevent end local lower_raw = string.lower(ino_raw.id) if not target_ore or lower_raw == target_ore or matchesMetalOreById(ino_raw.metal_ore.mat_index, target_ore) then - if not ore_veins[bevent.inorganic_mat] then - local vein_info = { + local positions = ensure_key(ore_types, bevent.inorganic_mat, { inorganic_id = ino_raw.id, inorganic_mat = bevent.inorganic_mat, metal_ore = ino_raw.metal_ore, positions = {} - } - ore_veins[bevent.inorganic_mat] = vein_info + }).positions + local block_pos = block.map_pos + for y=0,15 do + local row = bevent.tile_bitmask.bits[y] + for x=0,15 do + if row & (1 << x) == 1 then + local pos = xyz2pos(block_pos.x + x, block_pos.y + y, block_pos.z) + if isValidMineralTile(opts, pos, check_designation) then + table.insert(positions, pos) + end + end + end end - - table.insert(ore_veins[bevent.inorganic_mat].positions, block.map_pos) end - :: skipevent :: end end - return ore_veins + -- trim veins with zero valid tiles + for key,vein in pairs(ore_types) do + if #vein.positions == 0 then + ore_types[key] = nil + end + end + + return ore_types end local function designateDig(pos) local designation = dfhack.maps.getTileFlags(pos) designation.dig = df.tile_dig_designation.Default + dfhack.maps.getTileBlock(pos).flags.designated = true end -local function getOreDescription(ore) - local str = ("%s ("):format(string.lower(tostring(ore.inorganic_id))) - for _, mat_index in ipairs(ore.metal_ore.mat_index) do +local function getOreDescription(opts, vein) + local visible = opts.all and '' or 'visible ' + local str = ('%5d %stile(s) of %s ('):format(#vein.positions, visible, tostring(vein.inorganic_id):lower()) + for _, mat_index in ipairs(vein.metal_ore.mat_index) do local metal_raw = df.global.world.raws.inorganics[mat_index] - str = ("%s%s, "):format(str, string.lower(metal_raw.id)) + str = ('%s%s, '):format(str, string.lower(metal_raw.id)) end - str = str:gsub(", %s*$", "") .. ')' + str = str:gsub(', %s*$', '') .. ')' return str end -local options, args = { - help = false, - show_undiscovered = false -}, {...} +local function selectOreTile(opts, target_ore) + local ore_types = findOres(opts, true, target_ore) + local target_vein = getRandomFromTable(ore_types) + if target_vein == nil then + local visible = opts.all and '' or 'visible ' + qerror('Cannot find any undesignated ' .. visible .. target_ore) + end + local target_pos = target_vein.positions[math.random(#target_vein.positions)] + dfhack.gui.revealInDwarfmodeMap(target_pos, true, true) + designateDig(target_pos) + print(('Here is some %s'):format(target_vein.inorganic_id)) +end + +local opts = { + all=false, + help=false, +} -local positionals = argparse.processArgsGetopt(args, { - {'h', 'help', handler=function() options.help = true end}, - {'a', 'all', handler=function() options.show_undiscovered = true end}, +local positionals = argparse.processArgsGetopt({...}, { + {'a', 'all', handler=function() opts.all = true end}, + {'h', 'help', handler=function() opts.help = true end}, }) -if positionals[1] == "help" or options.help then +local target_ore = positionals[1] +if target_ore == 'help' or opts.help then print(dfhack.script_help()) return end -if positionals[1] == nil or positionals[1] == "list" then - print(dfhack.script_help()) - local veins = findOreVeins(nil, options.show_undiscovered) - local sorted = sortTableBy(veins, function(a, b) return #a.positions < #b.positions end) +if not target_ore or target_ore == 'list' then + local ore_types = findOres(opts, false) + local sorted = sortTableBy(ore_types, function(a, b) return #a.positions < #b.positions end) - for _, vein in ipairs(sorted) do - print(" " .. getOreDescription(vein)) + for _,ore_type in ipairs(sorted) do + print(' ' .. getOreDescription(opts, ore_type)) end - return else - local veins = findOreVeins(positionals[1], options.show_undiscovered) - local vein_keys = extractKeys(veins) - - if #vein_keys == 0 then - qerror("Cannot find unmined " .. positionals[1]) - end - - local target_vein = getRandomFromTable(veins) - if target_vein == nil then - -- really shouldn't happen at this point - qerror("Failed to choose vein from available choices") - end - - local pos_keyset = extractKeys(target_vein.positions) - local dxs = sequence(0, 15) - local dys = sequence(0, 15) - - randomSort(pos_keyset) - randomSort(dxs) - randomSort(dys) - - local target_pos = nil - for _, k in pairs(pos_keyset) do - local block_pos = target_vein.positions[k] - for _, dx in pairs(dxs) do - for _, dy in pairs(dys) do - local pos = { x = block_pos.x + dx, y = block_pos.y + dy, z = block_pos.z } - -- Enforce world boundaries - if pos.x <= 0 or pos.x >= df.global.world.map.x_count or pos.y <= 0 or pos.y >= df.global.world.map.y_count then - goto skip_pos - end - - if not options.show_undiscovered and not dfhack.maps.isTileVisible(pos) then - goto skip_pos - end - - local tile_type = dfhack.maps.getTileType(pos) - local tile_mat = tile_attrs[tile_type].material - local shape = tile_attrs[tile_type].shape - local designation = dfhack.maps.getTileFlags(pos) - if tile_mat == df.tiletype_material.MINERAL and designation.dig == df.tile_dig_designation.No and shape == df.tiletype_shape.WALL then - target_pos = pos - goto complete - end - - :: skip_pos :: - end - end - end - - :: complete :: - - if target_pos ~= nil then - dfhack.gui.pauseRecenter(target_pos) - designateDig(target_pos) - print(("Here is some %s at (%d, %d, %d)"):format(target_vein.inorganic_id, target_pos.x, target_pos.y, target_pos.z)) - else - qerror("Cannot find unmined " .. positionals[1]) - end + selectOreTile(opts, positionals[1]:lower()) end diff --git a/makeown.lua b/makeown.lua index cbfe3433aa..565b7c6856 100644 --- a/makeown.lua +++ b/makeown.lua @@ -32,6 +32,10 @@ function name_unit(unit) unit.name.parts_of_speech.RearCompound = df.part_of_speech.Verb3rdPerson unit.name.type = df.language_name_type.Figure unit.name.has_name = true + + local hf = df.historical_figure.find(unit.hist_figure_id) + if not hf then return end + hf.name:assign(unit.name) end local function fix_clothing_ownership(unit) diff --git a/modtools/set-personality.lua b/modtools/set-personality.lua index c0136c56a5..29c966e25c 100644 --- a/modtools/set-personality.lua +++ b/modtools/set-personality.lua @@ -165,7 +165,7 @@ end -- Gets the range of the unit caste's min, average, and max value for a trait, as defined in the PERSONALITY creature tokens. function getUnitCasteTraitRange(unit, trait) - local caste = df.creature_raw.find(unit.race).caste[unit.caste] + local caste = dfhack.units.getCasteRaw(unit) local range = {} range.min = caste.personality.a[df.personality_facet_type[trait]] diff --git a/set-orientation.lua b/set-orientation.lua index 19583f7913..6b821b8fcf 100644 --- a/set-orientation.lua +++ b/set-orientation.lua @@ -155,7 +155,7 @@ function randomiseOrientation(unit, sex) return end - local caste = df.creature_raw.find(unit.race).caste[unit.caste] + local caste = dfhack.units.getCasteRaw(unit) -- Build a weighted table for use in the weighted roll function local sexname = getSexString(sex) diff --git a/timestream.lua b/timestream.lua index c520f696be..e41143e05d 100644 --- a/timestream.lua +++ b/timestream.lua @@ -135,10 +135,29 @@ local function adjust_unit_counters(unit, timeskip) decrement_counter(c2, 'stomach_content', timeskip * 5) decrement_counter(c2, 'stomach_food', timeskip * 5) decrement_counter(c2, 'vomit_timeout', timeskip) - -- stored_fat wanders about based on other state; we can probably leave it alone + -- stored_fat wanders about based on other state; we can likely leave it alone and + -- not materially affect gameplay +end + +-- need to manually adjust job completion_timer values for jobs that are controlled by unit actions +-- with a timer of 1, which are destroyed immediately after they are created. longer-lived unit +-- actions are already sufficiently handled by dfhack.units.subtractGroupActionTimers(). +-- this will also decrement timers for jobs with actions that have just expired, but on average, this +-- should balance out to be correct, since we're losing time when we subtract from the action timers +-- and cap the value so it never drops below 1. +local function adjust_job_counter(unit, timeskip) + local job = unit.job.current_job + if not job then return end + for _,action in ipairs(unit.actions) do + if action.type == df.unit_action_type.Job or action.type == df.unit_action_type.JobRecover then + return + end + end + decrement_counter(job, 'completion_timer', timeskip) end -- unit needs appear to be incremented on season ticks, so we don't need to worry about those +-- since the TICK_TRIGGERS check makes sure that we never skip season ticks local function adjust_units(timeskip) for _, unit in ipairs(df.global.world.units.active) do if not dfhack.units.isActive(unit) then goto continue end @@ -146,6 +165,7 @@ local function adjust_units(timeskip) dfhack.units.subtractGroupActionTimers(unit, timeskip, df.unit_action_type_group.All) if not dfhack.units.isOwnGroup(unit) then goto continue end adjust_unit_counters(unit, timeskip) + adjust_job_counter(unit, timeskip) ::continue:: end end @@ -176,14 +196,17 @@ local function adjust_activities(timeskip) -- countdown appears to never move from 0 decrement_counter(ev, 'countdown', timeskip) elseif df.activity_event_harassmentst:is_instance(ev) then - -- TODO: counter behavior not yet analyzed - -- print(i) + if DEBUG then + print('activity_event_harassmentst ready for analysis at index', i) + end elseif df.activity_event_encounterst:is_instance(ev) then - -- TODO: counter behavior not yet analyzed - -- print(i) + if DEBUG then + print('activity_event_encounterst ready for analysis at index', i) + end elseif df.activity_event_reunionst:is_instance(ev) then - -- TODO: counter behavior not yet analyzed - -- print(i) + if DEBUG then + print('activity_event_reunionst ready for analysis at index', i) + end elseif df.activity_event_conversationst:is_instance(ev) then increment_counter(ev, 'pause', timeskip) elseif df.activity_event_guardst:is_instance(ev) then @@ -221,8 +244,9 @@ local function adjust_activities(timeskip) elseif df.activity_event_performancest:is_instance(ev) then increment_counter(ev, 'current_position', timeskip) elseif df.activity_event_store_objectst:is_instance(ev) then - -- TODO: counter behavior not yet analyzed - -- print(i) + if DEBUG then + print('activity_event_store_objectst ready for analysis at index', i) + end end end end @@ -253,7 +277,9 @@ local function on_tick() -- don't let our deficit grow unbounded if we can never catch up timeskip_deficit = math.min(desired_timeskip - timeskip, 100.0) - if DEBUG then print(('timeskip (%d, +%.2f)'):format(timeskip, timeskip_deficit)) end + if DEBUG and (tonumber(DEBUG) or 0) >= 2 then + print(('timeskip (%d, +%.2f)'):format(timeskip, timeskip_deficit)) + end if timeskip <= 0 then return end local desired_calendar_timeskip = (timeskip * state.settings.calendar_rate) + calendar_timeskip_deficit @@ -261,6 +287,7 @@ local function on_tick() calendar_timeskip_deficit = math.max(0, desired_calendar_timeskip - calendar_timeskip) df.global.cur_year_tick = df.global.cur_year_tick + calendar_timeskip + df.global.cur_year_tick_advmode = df.global.cur_year_tick_advmode + calendar_timeskip*144 adjust_units(timeskip) adjust_activities(timeskip) diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 9c578bc18c..af86aa8006 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -50,7 +50,7 @@ end local function bodyparts_that_can_wear(unit, item) local bodyparts = {} - local unitparts = df.creature_raw.find(unit.race).caste[unit.caste].body_info.body_parts + local unitparts = dfhack.units.getCasteRaw(unit).body_info.body_parts if item._type == df.item_helmst then for index, part in ipairs(unitparts) do diff --git a/unretire-anyone.lua b/unretire-anyone.lua index c977ebdfd5..0e79fccaba 100644 --- a/unretire-anyone.lua +++ b/unretire-anyone.lua @@ -50,7 +50,7 @@ function showNemesisPrompt(advSetUpScreen) not histFlags.deity and not histFlags.force then - local creature = df.creature_raw.find(histFig.race).caste[histFig.caste] + local creature = dfhack.units.getCasteRaw(histFig.race, histFig.caste) local name = creature.caste_name[0] if histFig.info and histFig.info.curse then local curse = histFig.info.curse diff --git a/workorder.lua b/workorder.lua index 7c58e785b5..c72ec99981 100644 --- a/workorder.lua +++ b/workorder.lua @@ -594,10 +594,7 @@ end -- true/false or nil if no shearable_tissue_layer with length > 0. local function canShearCreature(u) - local stls = world.raws.creatures - .all[u.race] - .caste[u.caste] - .shearable_tissue_layer + local stls = dfhack.units.getCasteRaw(u).shearable_tissue_layer local any for _, stl in ipairs(stls) do