diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..8b077fbd74 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +changelog.txt merge=union diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..e682b17afc --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1 @@ +If this PR makes an externally-visible change in behavior, please add an appropriate line to `changelog.txt`. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a41c5365aa..4982b36b06 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,63 +3,27 @@ name: Build on: [push, pull_request] jobs: + test: + uses: DFHack/dfhack/.github/workflows/test.yml@develop + with: + scripts_repo: ${{ github.repository }} + scripts_ref: ${{ github.ref }} + secrets: inherit + docs: - runs-on: ubuntu-18.04 - steps: - - name: Set up Python 3 - uses: actions/setup-python@v2 - with: - python-version: 3 - - name: Install dependencies - run: | - pip install 'sphinx<4.4.0' - - name: Clone scripts - uses: actions/checkout@v1 - - name: Set up DFHack - run: | - git clone https://github.com/DFHack/dfhack.git $HOME/dfhack --depth 1 --branch develop - git -C $HOME/dfhack submodule update --init --depth 1 --remote plugins/stonesense library/xml - rmdir $HOME/dfhack/scripts - ln -sv $(pwd) $HOME/dfhack/scripts - - name: Build docs - run: | - sphinx-build -W --keep-going -j3 --color $HOME/dfhack html - - name: Check for missing docs - if: success() || failure() - run: python $HOME/dfhack/ci/script-docs.py . - - name: Upload docs - if: success() || failure() - uses: actions/upload-artifact@master - with: - name: docs - path: html + uses: DFHack/dfhack/.github/workflows/build-linux.yml@develop + with: + scripts_repo: ${{ github.repository }} + scripts_ref: ${{ github.ref }} + artifact-name: docs + platform-files: false + common-files: false + docs: true + secrets: inherit + lint: - runs-on: ubuntu-18.04 - steps: - - name: Set up Python 3 - uses: actions/setup-python@v2 - with: - python-version: 3 - - name: Set up Ruby 2.7 - uses: actions/setup-ruby@v1 - with: - ruby-version: 2.7 - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install lua5.3 - - name: Clone scripts - uses: actions/checkout@v1 - - name: Set up DFHack - run: | - git clone https://github.com/DFHack/dfhack.git $HOME/dfhack --depth 1 --branch develop - rmdir $HOME/dfhack/scripts - ln -sv $(pwd) $HOME/dfhack/scripts - - name: Check whitespace - run: python $HOME/dfhack/travis/lint.py - - name: Check Lua syntax - if: success() || failure() - run: python $HOME/dfhack/travis/script-syntax.py --ext=lua --cmd="luac5.3 -p" --github-actions - - name: Check Ruby syntax - if: success() || failure() - run: python $HOME/dfhack/travis/script-syntax.py --ext=rb --cmd="ruby -c" --github-actions + uses: DFHack/dfhack/.github/workflows/lint.yml@develop + with: + scripts_repo: ${{ github.repository }} + scripts_ref: ${{ github.ref }} + secrets: inherit diff --git a/.github/workflows/clean-cache.yml b/.github/workflows/clean-cache.yml new file mode 100644 index 0000000000..3439019d01 --- /dev/null +++ b/.github/workflows/clean-cache.yml @@ -0,0 +1,11 @@ +name: Clean up PR caches + +on: + pull_request_target: + types: + - closed + +jobs: + cleanup: + uses: DFHack/dfhack/.github/workflows/clean-cache.yml@develop + secrets: inherit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 486e1a5b81..c5694a6f43 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: # shared across repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v6.0.0 hooks: - id: check-added-large-files - id: check-case-conflict @@ -20,11 +20,11 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.18.2 + rev: 0.37.4 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.3.1 + rev: v1.5.6 hooks: - id: forbid-tabs exclude_types: @@ -34,6 +34,6 @@ repos: - json # specific to scripts: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v6.0.0 hooks: - id: forbid-new-submodules diff --git a/CMakeLists.txt b/CMakeLists.txt index 3631626adc..a673d8298a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,11 @@ install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DESTINATION ${DFHACK_DATA_DESTINATION} FILES_MATCHING PATTERN "*.lua" - PATTERN "*.rb" PATTERN "*.json" - PATTERN "3rdparty" EXCLUDE + PATTERN "scripts/docs" EXCLUDE PATTERN "scripts/test" EXCLUDE + PATTERN ".github" EXCLUDE + PATTERN ".vscode" EXCLUDE ) if(BUILD_TESTS) diff --git a/adaptation.lua b/adaptation.lua new file mode 100644 index 0000000000..221fa7e317 --- /dev/null +++ b/adaptation.lua @@ -0,0 +1,64 @@ +local argparse = require('argparse') + +local function print_color(color, s) + dfhack.color(color) + dfhack.print(s) + dfhack.color(COLOR_RESET) +end + +local function show_one(unit) + local t = dfhack.units.getMiscTrait(unit, df.misc_trait_type.CaveAdapt) + local val = t and t.value or 0 + print_color(COLOR_RESET, ('%s has an adaptation level of '): + format(dfhack.units.getReadableName(unit))) + if val <= 399999 then + print_color(COLOR_GREEN, ('%d\n'):format(val)) + elseif val <= 599999 then + print_color(COLOR_YELLOW, ('%d\n'):format(val)) + else + print_color(COLOR_RED, ('%d\n'):format(val)) + end +end + +local function set_one(unit, value) + local t = dfhack.units.getMiscTrait(unit, df.misc_trait_type.CaveAdapt, true) + print(('%s has changed from an adaptation level of %d to %d'): + format(dfhack.units.getReadableName(unit), t.value, value)) + t.value = value +end + +local function get_units(all) + local units = all and dfhack.units.getCitizens() or {dfhack.gui.getSelectedUnit(true)} + if #units == 0 then + qerror('Please select a unit or specify the --all option') + end + return units +end + +local help, all = false, false +local positionals = argparse.processArgsGetopt({...}, { + {'a', 'all', handler=function() all = true end}, + {'h', 'help', handler=function() help = true end} +}) + +if help then + print(dfhack.script_help()) + return +end + +if not positionals[1] or positionals[1] == 'show' then + for _, unit in ipairs(get_units(all)) do + show_one(unit) + end +elseif positionals[1] == 'set' then + local value = argparse.nonnegativeInt(positionals[2], 'value') + if value > 800000 then + dfhack.printerr('clamping value to 800,000') + value = 800000 + end + for _, unit in ipairs(get_units(all)) do + set_one(unit, value) + end +else + qerror('unknown command: ' .. positionals[1]) +end diff --git a/adaptation.rb b/adaptation.rb deleted file mode 100644 index aea6dcfbf1..0000000000 --- a/adaptation.rb +++ /dev/null @@ -1,122 +0,0 @@ -# View or set cavern adaptation levels -# based on removebadthoughts.rb -=begin - -adaptation -========== -View or set level of cavern adaptation for the selected unit or the whole fort. - -Usage:: - - adaptation [value] - -The ``value`` must be between 0 and 800,000 (inclusive). - -=end - -# Color constants, values mapped to color_value enum in include/ColorText.h -COLOR_RESET = -1 -COLOR_GREEN = 2 -COLOR_RED = 4 -COLOR_YELLOW = 14 - -def usage(s) - if nil != s - puts(s) - end - puts "Usage: adaptation [value]" - throw :script_finished -end - -mode = $script_args[0] || 'help' -who = $script_args[1] -value = $script_args[2] - -if 'help' == mode - usage(nil) -elsif 'show' != mode && 'set' != mode - usage("Invalid mode '#{mode}': must be either 'show' or 'set'") -end - -if nil == who - usage("Target not specified") -elsif 'him' != who && 'all' != who - usage("Invalid target '#{who}'") -end - -if 'set' == mode - if nil == value - usage("Value not specified") - elsif !/[[:digit:]]/.match(value) - usage("Invalid value '#{value}'") - end - - if 0 > value.to_i || 800000 < value.to_i - usage("Value must be between 0 and 800000") - end - value = value.to_i -end - -num_set = 0 - -set_adaptation_value = lambda { |u,v| - next if !df.unit_iscitizen(u) - next if u.flags2.killed - trait_found = false - u.status.misc_traits.each { |t| - if t.id == :CaveAdapt - if mode == 'show' - if df.respond_to?(:print_color) - df.print_color(COLOR_RESET, "Unit #{u.id} (#{u.name}) has an adaptation of ") - case t.value - when 0..399999 - df.print_color(COLOR_GREEN, "#{t.value}\n") - when 400000..599999 - df.print_color(COLOR_YELLOW, "#{t.value}\n") - else - df.print_color(COLOR_RED, "#{t.value}\n") - end - else - puts "Unit #{u.id} (#{u.name}) has an adaptation of #{t.value}" - end - elsif mode == 'set' - puts "Unit #{u.id} (#{u.name}) changed from #{t.value} to #{v}" - t.value = v - num_set += 1 - end - #return # Doesn't work on Ruby 1.8 - trait_found = true - end - } - - if !trait_found - if mode == 'show' - df.print_color(COLOR_RESET, "Unit #{u.id} (#{u.name}) has an adaptation of ") - df.print_color(COLOR_GREEN, "0\n") - elsif mode == 'set' - new_trait = DFHack::UnitMiscTrait.cpp_new - new_trait.id = :CaveAdapt - new_trait.value = v - num_set += 1 - u.status.misc_traits.push(new_trait) - puts "Unit #{u.id} (#{u.name}) changed from 0 to #{v}" - end - end -} - -case who -when 'him' - if u = df.unit_find - set_adaptation_value[u,value] - else - puts 'Please select a dwarf ingame' - end -when 'all' - df.unit_citizens.each { |uu| - set_adaptation_value[uu,value] - } -end - -if 'set' == mode - puts "#{num_set} unit#{'s' if num_set != 1} updated." -end diff --git a/add-recipe.lua b/add-recipe.lua index c2f50e28dd..fe7fd63576 100644 --- a/add-recipe.lua +++ b/add-recipe.lua @@ -1,29 +1,8 @@ -- Script to add unknown crafting recipes to the player's civ. ---[====[ -add-recipe -========== -Adds unknown weapon and armor crafting recipes to your civ. -E.g. some civilizations never learn to craft high boots. This script can -help with that, and more. Only weapons, armor, and tools are currently supported; -things such as instruments are not. Available options: - -* ``add-recipe all`` adds *all* available weapons and armor, including exotic items - like blowguns, two-handed swords, and capes. - -* ``add-recipe native`` adds only native (but unknown) crafting recipes. Civilizations - pick randomly from a pool of possible recipes, which means not all civs get - high boots, for instance. This command gives you all the recipes your - civilisation could have gotten. - -* ``add-recipe single `` adds a single item by the given - item token. For example:: - - add-recipe single SHOES:ITEM_SHOES_BOOTS -]====] local itemDefs = df.global.world.raws.itemdefs -local resources = df.historical_entity.find(df.global.ui.civ_id).resources -local civ = df.historical_entity.find(df.global.ui.civ_id).entity_raw +local resources = df.historical_entity.find(df.global.plotinfo.civ_id).resources +local civ = df.historical_entity.find(df.global.plotinfo.civ_id).entity_raw if (resources == nil) then qerror("Could not find entity resources") @@ -84,7 +63,7 @@ function addItems(category, exotic) --category: the category of items we're adding --exotic: whether to add exotic items --returns: list of item objects that were added - local known = category[1] + local known_category = category[1] local native = category[2] local all = category[3] local added = {} --as:df.itemdef[] @@ -93,16 +72,20 @@ function addItems(category, exotic) local item = item --as:df.itemdef_weaponst local subtype = item.subtype local itemOk = false + local known = known_category + -- digging implements are seperate from weapons in entity resources. + if (df.itemdef_weaponst:is_instance(item) and item.skill_melee == df.job_skill.MINING) then + known = diggers + end --check if it's a training weapon local t1, t2 = pcall(function () return item.flags.TRAINING == false end) local training = not(not t1 or t2) - --we don't want procedural items with adjectives such as "wavy spears" - --(because they don't seem to be craftable even if added) + --excludes procedural items, eg: "wavy spears" for divine origins --nor do we want known items or training items (because adding training --items seems to allow them to be made out of metals) - if (item.adjective == "" and not training and not checkKnown(known, subtype)) then + if (not item.base_flags.GENERATED and not training and not checkKnown(known, subtype)) then itemOk = true end @@ -110,10 +93,12 @@ function addItems(category, exotic) itemOk = false end - --check that the weapon we're adding is not already known to the civ as - --a digging implement so picks don't get duplicated - if (checkKnown(diggers, subtype)) then - itemOk = false + --if the weapon we're adding is a digging implement, add to diggers instead of weapons + --prevents picks from being duplicated, and puts great picks in correct category + if (df.itemdef_weaponst:is_instance(item) and item.skill_melee == df.job_skill.MINING) then + if (checkKnown(diggers, subtype)) then + itemOk = false + end end if (itemOk) then @@ -125,7 +110,6 @@ function addItems(category, exotic) return added end - function printItems(itemList) for _, v in ipairs(itemList) do local v = v --as:df.itemdef_weaponst @@ -192,10 +176,5 @@ elseif (cmd == "native") then elseif (cmd == "single") then addSingleItem(args[2]) else - print("Available options:\n" - .."all: adds all supported crafting recipes.\n" - .."native: adds only unknown native recipes (eg. high boots for " - .."some dwarves)\n" - .."single: adds a specific item by itemstring (eg. " - .."SHOES:ITEM_SHOES_BOOTS)") + print(dfhack.script_help()) end diff --git a/add-thought.lua b/add-thought.lua index 91919532d8..b68916d30b 100644 --- a/add-thought.lua +++ b/add-thought.lua @@ -1,47 +1,41 @@ -- Adds emotions to creatures. --@ module = true ---[====[ - -add-thought -=========== -Adds a thought or emotion to the selected unit. Can be used by other scripts, -or the gui invoked by running ``add-thought -gui`` with a unit selected. - -]====] - -local utils=require('utils') +local script = require('gui.script') +local utils = require('utils') function addEmotionToUnit(unit,thought,emotion,severity,strength,subthought) - local emotions=unit.status.current_soul.personality.emotions - if not (tonumber(emotion)) then - emotion=df.emotion_type[emotion] --luacheck: retype + local personality = unit.status.current_soul.personality + local emotions = personality.emotions + if not tonumber(emotion) then + emotion = df.emotion_type[emotion] --luacheck: retype end + severity = tonumber(severity) or 0 local properThought = tonumber(thought) or df.unit_thought_type[thought] local properSubthought = tonumber(subthought) if not properThought or not df.unit_thought_type[properThought] then - for k,syn in ipairs(df.global.world.raws.syndromes.all) do - if syn.syn_name==thought then + for _,syn in ipairs(df.global.world.raws.mat_table.syndromes.all) do + if syn.syn_name == thought then properThought = df.unit_thought_type.Syndrome properSubthought = syn.id break end end end - emotions:insert('#',{new=df.unit_personality.T_emotions, - type=tonumber(emotion), - unk2=1, - strength=tonumber(strength), - thought=properThought, - subthought=properSubthought, - severity=tonumber(severity), - unk7=0, - year=df.global.cur_year, - year_tick=df.global.cur_year_tick + emotions:insert('#', { + new=df.personality_moodst, + type=emotion, + strength=1, + relative_strength=tonumber(strength), + thought=properThought, + subthought=properSubthought, + severity=severity, + year=df.global.cur_year, + year_tick=df.global.cur_year_tick }) local divider=df.emotion_type.attrs[emotion].divider - if divider~=0 then - unit.status.current_soul.personality.stress_level=unit.status.current_soul.personality.stress_level+math.ceil(severity/df.emotion_type.attrs[emotion].divider) + if divider ~= 0 then + personality.stress = personality.stress + math.ceil(severity/df.emotion_type.attrs[emotion].divider) end end @@ -56,48 +50,37 @@ local validArgs = utils.invert({ }) function tablify(iterableObject) - local t={} + local t = {} for k,v in ipairs(iterableObject) do t[k] = v~=nil and v or 'nil' end return t end -if moduleMode then - return +if dfhack_flags.module then + return end local args = utils.processArgs({...}, validArgs) local unit = args.unit and df.unit.find(tonumber(args.unit)) or dfhack.gui.getSelectedUnit(true) - if not unit then qerror('A unit must be specified or selected.') end + if args.gui then - local script=require('gui.script') - script.start(function() - local tok,thought=script.showListPrompt('emotions','Which thought?',COLOR_WHITE,tablify(df.unit_thought_type),10,true) - if tok then - local eok,emotion=script.showListPrompt('emotions','Which emotion?',COLOR_WHITE,tablify(df.emotion_type),10,true) - if eok then - local sok,severity=script.showInputPrompt('emotions','At what severity?',COLOR_WHITE,'0') - if sok then - local stok,strength=script.showInputPrompt('emotions','At what strength?',COLOR_WHITE,'0') - if stok then - addEmotionToUnit(unit,thought,emotion,severity,strength,0) - end - end - end - end - end) + script.start(function() + local tok,thought = script.showListPrompt('emotions','Which thought?',COLOR_WHITE,tablify(df.unit_thought_type),10,true) + if not tok then return end + local eok,emotion = script.showListPrompt('emotions','Which emotion?',COLOR_WHITE,tablify(df.emotion_type),10,true) + if not eok then return end + local stok,strength = script.showInputPrompt('emotions','At what strength? 1 (Slight), 2 (Moderate), 5 (Strong), 10 (Intense).',COLOR_WHITE,'0') + if not stok then return end + addEmotionToUnit(unit,thought,emotion,0,strength,0) + end) else - local thought = args.thought or 180 - + local thought = args.thought or df.unit_thought_type.NeedsUnfulfilled local emotion = args.emotion or -1 - local severity = args.severity or 0 - local subthought = args.subthought or 0 - local strength = args.strength or 0 addEmotionToUnit(unit,thought,emotion,severity,strength,subthought) diff --git a/adv-fix-sleepers.lua b/adv-fix-sleepers.lua deleted file mode 100644 index e92a01e0e7..0000000000 --- a/adv-fix-sleepers.lua +++ /dev/null @@ -1,45 +0,0 @@ ---Fixes all local bugged sleepers in adventure mode. ---[====[ - -adv-fix-sleepers -================ -Fixes :bug:`6798`. This bug is characterized by sleeping units who refuse to -awaken in adventure mode regardless of talking to them, hitting them, or waiting -so long you die of thirst. If you come accross one or more bugged sleepers in -adventure mode, simply run the script and all nearby sleepers will be cured. - -Usage:: - - adv-fix-sleepers - - -]====] - ---======================== --- Author: ArrowThunder on bay12 & reddit --- Version: 1.1 ---======================= - --- get the list of all the active units currently loaded -local active_units = df.global.world.units.active -- get all active units - --- check every active unit for the bug -local num_fixed = 0 -- this is the number of army controllers fixed, not units - -- I've found that often, multiple sleepers share a bugged army controller -for k, unit in pairs(active_units) do - if unit then - local army_controller = df.army_controller.find(unit.enemy.army_controller_id) - if army_controller and army_controller.type == 4 then -- sleeping code is possible - if army_controller.unk_64.t4.unk_2.not_sleeping == false then - army_controller.unk_64.t4.unk_2.not_sleeping = true -- fix bug - num_fixed = num_fixed + 1 - end - end - end -end - -if num_fixed == 0 then - print ("No sleepers with the fixable bug were found, sorry.") -else - print ("Fixed " .. num_fixed .. " bugged army_controller(s).") -end diff --git a/adv-rumors.lua b/adv-rumors.lua deleted file mode 100644 index 1099df621b..0000000000 --- a/adv-rumors.lua +++ /dev/null @@ -1,83 +0,0 @@ --- Improve "Bring up specific incident or rumor" menu in Adventure mode ---@ module = true ---[====[ - -adv-rumors -========== -Improves the "Bring up specific incident or rumor" menu in Adventure mode. - -- Moves entries into one line -- Adds a "slew" keyword for filtering, making it easy to find your kills and not your companions' -- Trims repetitive words - -]====] - ---======================== --- Author : 1337G4mer on bay12 and reddit --- Version : 0.2 --- Description : A small utility based on dfhack to improve the rumor UI in adventure mode. --- --- In game when you want to boast about your kill to someone. Start conversation and choose --- the menu "Bring up specific incident or rumor" --- type rumors in dfhack window and hit enter. Or do the below keybind and use that directly from DF window. --- --- Prior Configuration: (you can skip this if you want) --- Set the three boolean values below and play around with the script as to how you like --- improveReadability = will move everything in one line --- addKeywordSlew = will add a keyword for filtering using slew, making it easy to find your kills and not your companion's --- shortenString = will further shorten the line to = slew "XYZ" ( "n time" ago in " Region") ---======================= - -local utils = require "utils" - -local names_blacklist = utils.invert{"a", "an", "you", "attacked", "slew", "was", "slain", "by"} - -function condenseChoiceTitle(choice) - while #choice.title > 1 do - choice.title[0].value = choice.title[0].value .. ' ' .. choice.title[1].value - choice.title:erase(1) - end -end - -function addKeyword(choice, keyword) - local keyword_ptr = df.new('string') - keyword_ptr.value = keyword - choice.keywords:insert('#', keyword_ptr) -end - -function rumorUpdate() - local improveReadability = true - local addKeywordSlew = true - local shortenString = true - local addKeywordNames = true - - for i, choice in ipairs(df.global.ui_advmode.conversation.choices) do - if choice.choice.type == df.talk_choice_type.SummarizeConflict then - if improveReadability then - condenseChoiceTitle(choice) - end - if shortenString then - condenseChoiceTitle(choice) - choice.title[0].value = choice.title[0].value - :gsub("Summarize the conflict in which +", "") - :gsub("This occurred +", "") - end - if addKeywordSlew then - if string.find(choice.title[0].value, "slew") then - addKeyword(choice, 'slew') - end - end - if addKeywordNames then - local title = choice.title[0].value - for keyword in title:sub(1, title:find('%(') - 1):gmatch('%w+') do - keyword = dfhack.utf2df(dfhack.df2utf(keyword):lower()) - if not names_blacklist[keyword] then - addKeyword(choice, keyword) - end - end - end - end - end -end - -rumorUpdate() diff --git a/advtools.lua b/advtools.lua new file mode 100644 index 0000000000..9f4d2ec3b3 --- /dev/null +++ b/advtools.lua @@ -0,0 +1,32 @@ +--@ module=true + +local convo = reqscript('internal/advtools/convo') +local fastcombat = reqscript('internal/advtools/fastcombat') +local party = reqscript('internal/advtools/party') + +OVERLAY_WIDGETS = { + conversation=convo.AdvRumorsOverlay, + fastcombat=fastcombat.AdvCombatOverlay, +} + +if dfhack_flags.module then + return +end + +local commands = { + party=party.run, +} + +local args = {...} +local command = table.remove(args, 1) + +if not command or command == 'help' or not commands[command] then + print(dfhack.script_help()) + return +end + +-- since these are "advtools", maybe don't let them run outside adventure mode. +if not dfhack.world.isAdventureMode() then + qerror("This script can only be used during adventure mode!") +end +commands[command](args) diff --git a/agitation-rebalance.lua b/agitation-rebalance.lua new file mode 100644 index 0000000000..ae22593a67 --- /dev/null +++ b/agitation-rebalance.lua @@ -0,0 +1,790 @@ +--@module = true +--@enable = true + +local eventful = require('plugins.eventful') +local exterminate = reqscript('exterminate') +local gui = require('gui') +local overlay = require('plugins.overlay') +local utils = require('utils') +local widgets = require('gui.widgets') + +local GLOBAL_KEY = 'agitation-rebalance' +local UNIT_EVENT_FREQ = 5 + +local presets = { + casual={ + wild_irritate_min=100000, + wild_sens=100000, + wild_irritate_decay=100000, + cavern_dweller_max_attackers=0, + }, + lenient={ + wild_irritate_min=10000, + wild_sens=10000, + wild_irritate_decay=5000, + cavern_dweller_max_attackers=20, + }, + strict={ + wild_irritate_min=2500, + wild_sens=500, + wild_irritate_decay=1000, + cavern_dweller_max_attackers=50, + }, + insane={ + wild_irritate_min=600, + wild_sens=200, + wild_irritate_decay=200, + cavern_dweller_max_attackers=100, + }, +} + +local vanilla_presets = { + casual={ + wild_irritate_min=2000, + wild_sens=10000, + wild_irritate_decay=500, + cavern_dweller_max_attackers=0, + }, + lenient={ + wild_irritate_min=2000, + wild_sens=10000, + wild_irritate_decay=500, + cavern_dweller_max_attackers=50, + }, + strict={ + wild_irritate_min=0, + wild_sens=10000, + wild_irritate_decay=100, + cavern_dweller_max_attackers=75, + }, +} + +local function get_default_state() + return { + enabled=false, + features={ + auto_preset=true, + surface=true, + cavern=true, + cap_invaders=true, + }, + caverns={ + last_invasion_id=-1, + last_year_roll=-1, + last_season_roll=-1, + baseline=0, + player_visible_baseline=0, + }, + stats={ + surface_attacks=0, + cavern_attacks=0, + invasions_diverted=0, + invaders_vaporized=0, + }, + } +end + +state = state or get_default_state() +new_unit_min_frame_counter = new_unit_min_frame_counter or -1 +num_cavern_invaders = num_cavern_invaders or 0 +num_cavern_invaders_frame_counter = num_cavern_invaders_frame_counter or -1 + +function isEnabled() + return state.enabled +end + +local function get_stat(stat) + return ensure_key(state, 'stats')[stat] or 0 +end + +local function inc_stat(stat) + local cur_val = get_stat(stat) + state.stats[stat] = cur_val + 1 +end + +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) +end + +local world = df.global.world +local map_features = world.features.map_features +local plotinfo = df.global.plotinfo +local custom_difficulty = plotinfo.main.custom_difficulty + +local function on_surface_attack() + if plotinfo.outdoor_irritation > custom_difficulty.wild_irritate_min then + plotinfo.outdoor_irritation = custom_difficulty.wild_irritate_min + inc_stat('surface_attacks') + persist_state() + end +end + +local function get_cumulative_irritation() + local irritation = 0 + for _, map_feature in ipairs(map_features) do + if df.feature_init_subterranean_from_layerst:is_instance(map_feature) then + irritation = irritation + map_feature.feature.irritation_level + end + end + return irritation +end + +local function get_cavern_irritation(which) + for _,map_feature in ipairs(map_features) do + if not df.feature_init_subterranean_from_layerst:is_instance(map_feature) then + goto continue + end + if map_feature.start_depth == which then + return map_feature.feature.irritation_level + end + ::continue:: + end +end + +-- returns the minimum irritation level that will max out chances of +-- both cavern invasions and forgotten beasts +local function get_normalized_irritation(which) + local irritation = get_cavern_irritation(which) + if not irritation then return 0 end + local wealth_rating = plotinfo.tasks.wealth.total // custom_difficulty.forgotten_wealth_div + local irritation_min = custom_difficulty.forgotten_irritate_min + return math.max(10000, irritation_min - wealth_rating + custom_difficulty.forgotten_sens) +end + +local function get_cavern_sens() + return (custom_difficulty.wild_irritate_min + custom_difficulty.wild_sens)//2 +end + +local function on_cavern_attack(invasion_id) + state.caverns.last_invasion_id = invasion_id + for _,map_feature in ipairs(map_features) do + if not df.feature_init_subterranean_from_layerst:is_instance(map_feature) then + goto continue + end + local normalized_irritation = get_normalized_irritation(map_feature.start_depth) + map_feature.feature.irritation_level = math.min( + map_feature.feature.irritation_level, + 100000-get_cavern_sens(), -- values above this are too close to max limit + normalized_irritation) -- values above this are effectively the same + ::continue:: + end + state.caverns.baseline = get_cumulative_irritation() + inc_stat('cavern_attacks') + persist_state() +end + +local function is_unkilled(unit) + return not dfhack.units.isKilled(unit) and + unit.animal.vanish_countdown <= 0 -- not yet exterminated +end + +local function is_cavern_invader(unit) + local invasion = df.invasion_info.find(unit.invasion_id) + return invasion and + invasion.origin_master_army_controller_id == -1 and + not unit.flags1.caged and + not dfhack.units.isTame(unit) +end + +local function on_cavern_invader_over_max() + -- process units from the end of the active units first so we tend to + -- preserve animal person invaders over the war animals they bring + for i=#world.units.active-1,0,-1 do + local unit = world.units.active[i] + if not is_cavern_invader(unit) or not is_unkilled(unit) then + goto continue + end + exterminate.killUnit(unit, exterminate.killMethod.DISINTEGRATE) + num_cavern_invaders = num_cavern_invaders - 1 + inc_stat('invaders_vaporized') + if num_cavern_invaders <= custom_difficulty.cavern_dweller_max_attackers then + break + end + ::continue:: + end + persist_state() +end + +local function get_cavern_invaders() + local invaders = {} + for _, unit in ipairs(world.units.active) do + if is_unkilled(unit) and is_cavern_invader(unit) then + table.insert(invaders, unit) + end + end + return invaders +end + +local function get_num_cavern_invaders(slack) + slack = slack or 0 + if num_cavern_invaders_frame_counter + slack < world.frame_counter then + num_cavern_invaders = #get_cavern_invaders() + num_cavern_invaders_frame_counter = world.frame_counter + if num_cavern_invaders == 0 and + state.caverns.baseline ~= state.caverns.player_visible_baseline + then + state.caverns.player_visible_baseline = state.caverns.baseline + persist_state() + end + end + return num_cavern_invaders +end + +local function get_agitated_units() + local agitators = {} + for _, unit in ipairs(world.units.active) do + if is_unkilled(unit) and dfhack.units.isAgitated(unit) then + table.insert(agitators, unit) + end + end + return agitators +end + +local function check_new_unit(unit_id) + -- when just enabling, ignore the first batch of "new" units so we + -- don't react to existing agitated units or cavern invaders + if new_unit_min_frame_counter >= world.frame_counter then return end + local unit = df.unit.find(unit_id) + if not unit or not is_unkilled(unit) then return end + if state.features.surface and dfhack.units.isAgitated(unit) then + on_surface_attack() + return + end + if not state.features.cap_invaders or not is_cavern_invader(unit) then + return + end + if state.caverns.last_invasion_id ~= unit.invasion_id then + on_cavern_attack(unit.invasion_id) + end + if state.features.cap_invaders and + get_num_cavern_invaders() > custom_difficulty.cavern_dweller_max_attackers + then + on_cavern_invader_over_max() + end +end + +local function cull_invaders() + if not state.features.cap_invaders then return end + if get_num_cavern_invaders() > custom_difficulty.cavern_dweller_max_attackers then + on_cavern_invader_over_max() + end +end + +local function get_cavern_attack_independent_natural_chance(which) + return math.min(1, (get_cavern_irritation(which) or 0) / 10000) +end + +local function get_cavern_attack_natural_chances() + local cavern_1_chance = get_cavern_attack_independent_natural_chance(df.layer_type.Cavern1) + local cavern_2_chance = get_cavern_attack_independent_natural_chance(df.layer_type.Cavern2) + local cavern_3_chance = get_cavern_attack_independent_natural_chance(df.layer_type.Cavern3) + return cavern_1_chance, + (1-cavern_1_chance) * cavern_2_chance, + (1-cavern_1_chance) * (1-cavern_2_chance) * cavern_3_chance +end + +local function cavern_attack_passes_roll() + local irritation = get_cumulative_irritation() - state.caverns.baseline + local irr_max = get_cavern_sens() + if state.caverns.baseline == 0 then + -- normalize chances if irritation < 10000 + local c1, c2, c3 = get_cavern_attack_natural_chances() + irr_max = math.floor(irr_max * (c1 + c2 + c3)) + end + if irritation >= irr_max then return true end + return math.random(1, irr_max) <= irritation +end + +local function throttle_invasions() + if not state.features.cavern then return end + if state.caverns.last_year_roll == df.global.cur_year and + state.caverns.last_season_roll >= df.global.cur_season or + state.caverns.last_year_roll >= df.global.cur_year + then + -- only roll once per season + return + end + local over_cap = state.features.cap_invaders and + get_num_cavern_invaders() >= custom_difficulty.cavern_dweller_max_attackers + for idx=#df.global.timed_events-1,0,-1 do + local ev = df.global.timed_events[idx] + if ev.type ~= df.timed_event_type.FeatureAttack then goto continue end + local civ = ev.entity + if not civ then goto continue end + if over_cap or not cavern_attack_passes_roll() then + inc_stat('invasions_diverted') + df.global.timed_events:erase(idx) + ev:delete() + end + ::continue:: + end + state.caverns.last_year_roll = df.global.cur_year + state.caverns.last_season_roll = df.global.cur_season + persist_state() +end + +local function do_preset(preset_name) + local preset = presets[preset_name] + if not preset then + qerror(('preset not found: "%s"'):format(preset_name)) + end + utils.assign(custom_difficulty, preset) + print('agitation-rebalance: preset applied: ' .. preset_name) +end + +local TICKS_PER_DAY = 1200 +local TICKS_PER_MONTH = 28 * TICKS_PER_DAY +local TICKS_PER_SEASON = 3 * TICKS_PER_MONTH + +local function seasons_cleaning() + if not state.enabled then return end + cull_invaders() + throttle_invasions() + local ticks_until_next_season = TICKS_PER_SEASON - df.global.cur_season_tick + 1 + dfhack.timeout(ticks_until_next_season, 'ticks', seasons_cleaning) +end + +local function check_preset() + for preset_name,vanilla_settings in pairs(vanilla_presets) do + local matched = true + for k,v in pairs(vanilla_settings) do + if custom_difficulty[k] ~= v then + matched = false + break + end + end + if matched then + do_preset(preset_name) + break + end + end +end + +local function do_enable() + state.enabled = true + new_unit_min_frame_counter = world.frame_counter + UNIT_EVENT_FREQ + 1 + num_cavern_invaders_frame_counter = -(UNIT_EVENT_FREQ+1) + eventful.enableEvent(eventful.eventType.UNIT_NEW_ACTIVE, UNIT_EVENT_FREQ) + eventful.onUnitNewActive[GLOBAL_KEY] = check_new_unit + if state.features.auto_preset then check_preset() end + seasons_cleaning() +end + +local function do_disable() + state.enabled = false + eventful.onUnitNewActive[GLOBAL_KEY] = nil +end + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + do_disable() + return + end + if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then + return + end + state = get_default_state() + utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) + num_cavern_invaders = num_cavern_invaders or 0 + num_cavern_invaders_frame_counter = -(UNIT_EVENT_FREQ+1) + if state.enabled then + do_enable() + end +end + +----------------------------------- +-- IrritationOverlay +-- + +IrritationOverlay = defclass(IrritationOverlay, overlay.OverlayWidget) +IrritationOverlay.ATTRS{ + desc='Monitors irritation and shows chances of invasion.', + default_pos={x=-32,y=5}, + viewscreens='dwarfmode/Default', + overlay_onupdate_max_freq_seconds=5, + frame={w=24, h=13}, +} + +local function get_savagery() + -- need to check at (or about) ground level since biome data may be missing or incorrect + -- in the extreme top or bottom levels of the map + local ground_level = (world.map.z_count-2) - world.worldgen.worldgen_parms.levels_above_ground + local rgnX, rgnY + for z=ground_level,0,-1 do + rgnX, rgnY = dfhack.maps.getTileBiomeRgn(0, 0, z) + if rgnX then break end + end + local biome = dfhack.maps.getRegionBiome(rgnX, rgnY) + return biome and biome.savagery or 0 +end + +-- returns chance for next wildlife group +local function get_surface_attack_chance() + local adjusted_irritation = plotinfo.outdoor_irritation - custom_difficulty.wild_irritate_min + if adjusted_irritation <= 0 or get_savagery() <= 65 then return 0 end + return custom_difficulty.wild_sens <= 0 and 100 or + math.min(100, (adjusted_irritation*100)//custom_difficulty.wild_sens) +end + +-- returns chance for next season +local function get_fb_attack_chance(which) + local irritation = get_cavern_irritation(which) + if not irritation then return 0 end + local wealth_rating = plotinfo.tasks.wealth.total // custom_difficulty.forgotten_wealth_div + local irritation_min = custom_difficulty.forgotten_irritate_min + local adjusted_irritation = wealth_rating + irritation - irritation_min + if adjusted_irritation < 0 then return 0 end + return custom_difficulty.forgotten_sens <= 0 and 33 or + math.min(33, (adjusted_irritation*33)//custom_difficulty.forgotten_sens) +end + +local function get_cavern_attack_natural_chance(which) + local c1, c2, c3 = get_cavern_attack_natural_chances() + if which == df.layer_type.Cavern1 then + return math.floor(c1 * 100) + elseif which == df.layer_type.Cavern2 then + return math.floor(c2 * 100) + elseif which == df.layer_type.Cavern3 then + return math.floor(c3 * 100) + else + return math.floor((c1+c2+c3) * 100) + end +end + +local function get_cavern_invasion_chance(which) + if not state.enabled then + return get_cavern_attack_natural_chance(which) + end + + -- don't divilge new lowered chances until the current crop of invaders is gone + local baseline = num_cavern_invaders == 0 and + state.caverns.baseline or state.caverns.player_visible_baseline + local irritation = get_cumulative_irritation() - baseline + local irr_max = get_cavern_sens() + local c1, c2, c3 = get_cavern_attack_natural_chances() + local natural_chance = c1 + c2 + c3 + if state.caverns.baseline == 0 then + -- normalize chances if we've never had an attack + irr_max = math.floor(irr_max * natural_chance) + end + local overall_chance = math.min(1, irritation * natural_chance / irr_max) + + if which == df.layer_type.Cavern1 then + return math.floor(c1 * 100 * overall_chance) + elseif which == df.layer_type.Cavern2 then + return math.floor(c2 * 100 * overall_chance) + elseif which == df.layer_type.Cavern3 then + return math.floor(c3 * 100 * overall_chance) + else + return math.floor(natural_chance * 100 * overall_chance) + end +end + +local function get_chance_color(chance_fn, chance_arg) + local chance = chance_fn(chance_arg) + if chance < 1 then + return COLOR_GREEN + elseif chance < 33 then + return COLOR_YELLOW + elseif chance < 51 then + return COLOR_LIGHTRED + end + return COLOR_RED +end + +local function obfuscate_chance(chance_fn, chance_arg) + local chance = chance_fn(chance_arg) + if chance < 1 then + return 'None' + elseif chance < 33 then + return 'Low' + elseif chance < 51 then + return 'Med' + end + return 'High' +end + +local function get_invader_color() + if num_cavern_invaders <= 0 then + return COLOR_GREEN + elseif num_cavern_invaders < custom_difficulty.cavern_dweller_max_attackers then + return COLOR_YELLOW + else + return COLOR_RED + end +end + +-- set to true with :lua reqscript('agitation-rebalance').monitor_debug=true +-- to see more information on the monitor panel +monitor_debug = monitor_debug or false + +function IrritationOverlay:init() + local panel = widgets.Panel{ + frame_style=gui.FRAME_MEDIUM, + frame_background=gui.CLEAR_PEN, + frame={t=0, r=0, w=15, h=5}, + visible=function() return not monitor_debug end, + } + panel:addviews{ + widgets.Label{ + frame={t=0}, + text='Irrit. Threat', + auto_width=true, + }, + widgets.Label{ + frame={t=1, l=0}, + text={ + 'Surface:', + {gap=1, text=curry(obfuscate_chance, get_surface_attack_chance)}, + }, + text_pen=curry(get_chance_color, get_surface_attack_chance), + }, + widgets.Label{ + frame={t=2, l=0}, + text={ + 'Caverns:', + {gap=1, text=curry(obfuscate_chance, get_cavern_invasion_chance)}, + }, + text_pen=curry(get_chance_color, get_cavern_invasion_chance), + }, + } + + local debug_panel = widgets.Panel{ + frame_style=gui.FRAME_MEDIUM, + frame_background=gui.CLEAR_PEN, + visible=function() return monitor_debug end, + } + debug_panel:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text='Attack chance', + }, + widgets.Label{ + frame={t=1, l=0}, + text={ + ' Surface:', + {gap=1, text=get_surface_attack_chance, width=3, rjustify=true}, + '%', + }, + text_pen=curry(get_chance_color, get_surface_attack_chance), + }, + widgets.Label{ + frame={t=2, l=0}, + text={ + 'Caverns:', + {gap=2, text='FBs:'}, + }, + }, + widgets.Label{ + frame={t=3, l=0}, + text={ + '1:', + {gap=2, text=curry(get_cavern_invasion_chance, df.layer_type.Cavern1), width=3, rjustify=true}, + '%', + }, + text_pen=curry(get_chance_color, get_cavern_invasion_chance, df.layer_type.Cavern1), + }, + widgets.Label{ + frame={t=3, l=10}, + text={ + {text=curry(get_fb_attack_chance, df.layer_type.Cavern1), width=3, rjustify=true}, + '%', + }, + text_pen=curry(get_chance_color, get_fb_attack_chance, df.layer_type.Cavern1), + }, + widgets.Label{ + frame={t=4, l=0}, + text={ + '2:', + {gap=2, text=curry(get_cavern_invasion_chance, df.layer_type.Cavern2), width=3, rjustify=true}, + '%', + }, + text_pen=curry(get_chance_color, get_cavern_invasion_chance, df.layer_type.Cavern2), + }, + widgets.Label{ + frame={t=4, l=10}, + text={ + {text=curry(get_fb_attack_chance, df.layer_type.Cavern2), width=3, rjustify=true}, + '%', + }, + text_pen=curry(get_chance_color, get_fb_attack_chance, df.layer_type.Cavern2), + }, + widgets.Label{ + frame={t=5, l=0}, + text={ + '3:', + {gap=2, text=curry(get_cavern_invasion_chance, df.layer_type.Cavern3), width=3, rjustify=true}, + '%', + }, + text_pen=curry(get_chance_color, get_cavern_invasion_chance, df.layer_type.Cavern3), + }, + widgets.Label{ + frame={t=5, l=10}, + text={ + {text=curry(get_fb_attack_chance, df.layer_type.Cavern3), width=3, rjustify=true}, + '%', + }, + text_pen=curry(get_chance_color, get_fb_attack_chance, df.layer_type.Cavern3), + }, + widgets.Label{ + frame={t=0, r=0}, + text='Irrit', + auto_width=true, + }, + widgets.Label{ + frame={t=1, r=0}, + text={{text=function() return plotinfo.outdoor_irritation end, width=6, rjustify=true}}, + text_pen=curry(get_chance_color, get_surface_attack_chance), + auto_width=true, + }, + widgets.Label{ + frame={t=3, r=0}, + text={{text=function() return get_cavern_irritation(df.layer_type.Cavern1) end, width=6, rjustify=true}}, + text_pen=curry(get_chance_color, get_cavern_invasion_chance, df.layer_type.Cavern1), + auto_width=true, + }, + widgets.Label{ + frame={t=4, r=0}, + text={{text=function() return get_cavern_irritation(df.layer_type.Cavern2) end, width=6, rjustify=true}}, + text_pen=curry(get_chance_color, get_cavern_invasion_chance, df.layer_type.Cavern2), + auto_width=true, + }, + widgets.Label{ + frame={t=5, r=0}, + text={{text=function() return get_cavern_irritation(df.layer_type.Cavern3) end, width=6, rjustify=true}}, + text_pen=curry(get_chance_color, get_cavern_invasion_chance, df.layer_type.Cavern3), + auto_width=true, + }, + widgets.Label{ + frame={t=6, l=0}, + text={ + 'Invaders:', + {gap=1, text=function() return num_cavern_invaders end, width=4, rjustify=true}, + '/', + {text=function() return custom_difficulty.cavern_dweller_max_attackers end}, + }, + text_pen=function() return get_invader_color() end, + }, + widgets.Label{ + frame={t=7, l=0}, + text={ + 'Surface attacks:', + {gap=1, text=function() return get_stat('surface_attacks') end, width=5, rjustify=true}, + }, + }, + widgets.Label{ + frame={t=8, l=0}, + text={ + ' Cavern attacks:', + {gap=1, text=function() return get_stat('cavern_attacks') end, width=5, rjustify=true}, + }, + }, + widgets.Label{ + frame={t=9, l=0}, + text={ + 'Invasions erased:', + {gap=1, text=function() return get_stat('invasions_diverted') end, width=4, rjustify=true}, + }, + }, + widgets.Label{ + frame={t=10, l=0}, + text={ + 'Invaders culled:', + {gap=1, text=function() return get_stat('invaders_vaporized') end, width=5, rjustify=true}, + }, + }, + } + + self:addviews{ + panel, + debug_panel, + widgets.HelpButton{command='agitation-rebalance'} + } +end + +function IrritationOverlay:overlay_onupdate() + get_num_cavern_invaders(UNIT_EVENT_FREQ) +end + +OVERLAY_WIDGETS = {monitor=IrritationOverlay} + +----------------------------------- +-- CLI +-- + +if dfhack_flags.module then + return +end + +if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then + qerror('needs a loaded fortress map to work') +end + +local WIDGET_NAME = dfhack.current_script_name() .. '.monitor' + +local function print_status() + print(GLOBAL_KEY .. ' is ' .. (state.enabled and 'enabled' or 'not enabled')) + print() + print('features:') + for k,v in pairs(state.features) do + print((' %15s: %s'):format(k, v)) + end + print((' %15s: %s'):format('monitor', overlay.isOverlayEnabled(WIDGET_NAME) or 'false')) + print() + print('difficulty settings:') + print((' Wilderness irritation minimum: %d (about %d tree(s) until initial attacks are possible)'):format( + custom_difficulty.wild_irritate_min, custom_difficulty.wild_irritate_min // 100)) + print((' Wilderness sensitivity: %d (each tree past the miniumum makes an attack %.2f%% more likely)'):format( + custom_difficulty.wild_sens, 10000 / custom_difficulty.wild_sens)) + print((' Wilderness irritation decay: %d (about %d additional tree(s) allowed per year)'):format( + custom_difficulty.wild_irritate_decay, custom_difficulty.wild_irritate_decay // 100)) + print((' Cavern dweller maximum attackers: %d (maximum allowed across all caverns)'):format( + custom_difficulty.cavern_dweller_max_attackers)) + print() + local unhidden_invaders = {} + for _, unit in ipairs(get_cavern_invaders()) do + if not dfhack.units.isHidden(unit) then + table.insert(unhidden_invaders, unit) + end + end + print(('current agitated wildlife: %5d'):format(#get_agitated_units())) + print(('current known cavern invaders: %5d'):format(#unhidden_invaders)) + print() + print('current chances for an upcoming attack:') + print((' Surface: %s'):format(obfuscate_chance(get_surface_attack_chance))) + print((' Caverns: %s'):format(obfuscate_chance(get_cavern_invasion_chance))) +end + +local function enable_feature(which, enabled) + if which == 'monitor' then + dfhack.run_command('overlay', enabled and 'enable' or 'disable', WIDGET_NAME) + return + end + local feature = state.features[which] + if feature == nil then + qerror(('feature not found: "%s"'):format(which)) + end + state.features[which] = enabled + print(('feature %sabled: %s'):format(enabled and 'en' or 'dis', which)) +end + +local args = {...} +local command = table.remove(args, 1) + +if dfhack_flags and dfhack_flags.enable then + if dfhack_flags.enable_state then do_enable() + else do_disable() + end +elseif command == 'preset' then + do_preset(args[1] or '') +elseif command == 'enable' or command == 'disable' then + enable_feature(args[1] or '', command == 'enable') +elseif not command or command == 'status' then + print_status() + return +else + print(dfhack.script_help()) + return +end + +persist_state() diff --git a/allneeds.lua b/allneeds.lua new file mode 100644 index 0000000000..a3fa117cf1 --- /dev/null +++ b/allneeds.lua @@ -0,0 +1,84 @@ +-- Prints the sum of all citizens' needs. + +local argparse = require('argparse') + +local sorts = { + id=function(a,b) return a.id < b.id end, + strength=function(a,b) return a.strength > b.strength end, + focus=function(a,b) return a.focus < b.focus end, + freq=function(a,b) return a.freq > b.freq end, +} + +local sort = 'focus' + +argparse.processArgsGetopt({...}, { + {'s', 'sort', hasArg=true, handler=function(optarg) sort = optarg end} +}) + +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) +if units then + print(('Summarizing needs for %s:'):format(dfhack.units.getReadableName(units))) + units = {units} +else + print('Summarizing needs for all (sane) citizens and residents:') + units = dfhack.units.getCitizens() +end +print() + +for _, unit in ipairs(units) do + local mind = unit.status.current_soul.personality.needs + -- sum need_level and focus_level for each need + for _,need in ipairs(mind) do + local needs = ensure_key(fort_needs, need.id) + 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 + +local sorted_fort_needs = {} +for id, need in pairs(fort_needs) do + table.insert(sorted_fort_needs, { + id=df.need_type[id], + 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 %35s' +print(fmt:format("Need", "Strength", "Focus Impact", "Frequency", "Num. Unfettered -> Badly distracted")) +print(fmt:format("----", "--------", "------------", "---------", "-----------------------------------")) +for _, need in ipairs(sorted_fort_needs) do + 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/animal-control.lua b/animal-control.lua index aab1ffa8ef..65c3364e81 100644 --- a/animal-control.lua +++ b/animal-control.lua @@ -19,79 +19,6 @@ local validArgs = utils.invert({ 'help', }) local args = utils.processArgs({...}, validArgs) -local help = [====[ - -animal-control -============== -Animal control is a script useful for deciding what animals to butcher and geld. - -While not as powerful as Dwarf Therapist in managing animals - in so far as -DT allows you to sort by various stats and flags - this script does provide -many options for filtering animals. Additionally you can mark animals for -slaughter or gelding, you can even do so enmasse if you so choose. - -Examples:: - - animal-control -race DOG - animal-control -race DOG -male -notgelded -showstats - animal-control -markfor gelding -id 1988 - animal-control -markfor slaughter -id 1988 - animal-control -gelded -markedfor slaughter -unmarkfor slaughter - -**Selection options:** - -These options are used to specify what animals you want or do not want to select. - -``-all``: Selects all units. - Note: cannot be used in conjunction with other - selection options. - -``-id ``: Selects the unit with the specified id value provided. - -``-race ``: Selects units which match the race value provided. - -``-markedfor ``: Selects units which have been marked for the action provided. - Valid actions: ``slaughter``, ``gelding`` - -``-notmarkedfor ``: Selects units which have not been marked for the action provided. - Valid actions: ``slaughter``, ``gelding`` - -``-gelded``: Selects units which have already been gelded. - -``-notgelded``: Selects units which have not been gelded. - -``-male``: Selects units which are male. - -``-female``: Selects units which are female. - -**Command options:** - -- ``-showstats``: Displays physical attributes of the selected animals. - -- ``-markfor ``: Marks selected animals for the action provided. - Valid actions: ``slaughter``, ``gelding`` - -- ``-unmarkfor ``: Unmarks selected animals for the action provided. - Valid actions: ``slaughter``, ``gelding`` - -**Other options:** - -- ``-help``: Displays this information - -**Column abbreviations** - -Due to space constraints, the names of some output columns are abbreviated -as follows: - -- ``str``: strength -- ``agi``: agility -- ``tgh``: toughness -- ``endur``: endurance -- ``recup``: recuperation -- ``disres``: disease resistance - -]====] - header_format = "%-20s %-9s %-9s %-5s %-22s %-8s %-25s" row_format = "%-20s %-9d %-9d %-5s %-22s %-8s %-25s" @@ -125,7 +52,7 @@ bcommands = (args.showstats or args.markfor or args.unmarkfor) bvalid = (args.all and not bfilters) or (not args.all and (bfilters or bcommands)) if args.help or not bvalid then - print(help) + print(dfhack.script_help()) else count=0 if args.showstats then @@ -134,7 +61,7 @@ else print(header) end for _,v in ipairs(df.global.world.units.active) do - if v.civ_id == df.global.ui.civ_id and v.flags1.tame then + if v.civ_id == df.global.plotinfo.civ_id and v.flags1.tame then if not (args.male or args.female) or args.male and v.sex == 1 or args.female and v.sex == 0 then if not args.race or tonumber(args.race) == v.race then if not args.markedfor or (args.markedfor == "slaughter" and v.flags2.slaughter) or (args.markedfor == "gelding" and v.flags3.marked_for_gelding) then diff --git a/armoks-blessing.lua b/armoks-blessing.lua index eedd5ee637..6d57ab34eb 100644 --- a/armoks-blessing.lua +++ b/armoks-blessing.lua @@ -1,59 +1,9 @@ -- Adjust all attributes of all dwarves to an ideal -- by vjek ---[====[ -armoks-blessing -=============== -Runs the equivalent of `rejuvenate`, `elevate-physical`, `elevate-mental`, and -`brainwash` on all dwarves currently on the map. This is an extreme change, -which sets every stat and trait to an ideal easy-to-satisfy preference. +local rejuvenate = reqscript('rejuvenate') +local utils = require('utils') -Without providing arguments, only attributes, age, and personalities will be adjusted. -Adding arguments allows for skills or classes to be adjusted to legendary (maximum). - -Arguments: - -- ``list`` - Prints list of all skills - -- ``classes`` - Prints list of all classes - -- ``all`` - Set all skills, for all Dwarves, to legendary - -- ```` - Set a specific skill, for all Dwarves, to legendary - - example: ``armoks-blessing RANGED_COMBAT`` - - All Dwarves become a Legendary Archer - -- ```` - Set a specific class (group of skills), for all Dwarves, to legendary - - example: ``armoks-blessing Medical`` - - All Dwarves will have all medical related skills set to legendary - -]====] -local utils = require 'utils' -function rejuvenate(unit) - if unit==nil then - print ("No unit available! Aborting with extreme prejudice.") - return - end - - local current_year=df.global.cur_year - local newbirthyear=current_year - 20 - if unit.birth_year < newbirthyear then - unit.birth_year=newbirthyear - end - if unit.old_year < current_year+100 then - unit.old_year=current_year+100 - end - -end -- --------------------------------------------------------------------------- function brainwash_unit(unit) if unit==nil then @@ -73,7 +23,7 @@ function brainwash_unit(unit) unit.status.current_soul.personality.traits.GREED = 25 unit.status.current_soul.personality.traits.IMMODERATION = 25 unit.status.current_soul.personality.traits.VIOLENT = 50 - unit.status.current_soul.personality.traits.PERSEVERENCE = 75 + unit.status.current_soul.personality.traits.PERSEVERANCE = 75 unit.status.current_soul.personality.traits.WASTEFULNESS = 50 unit.status.current_soul.personality.traits.DISCORD = 25 unit.status.current_soul.personality.traits.FRIENDLINESS = 75 @@ -130,7 +80,7 @@ function brainwash_unit(unit) [df.value_type.HARD_WORK]=41, [df.value_type.SACRIFICE]=41, [df.value_type.COMPETITION]=-41, - [df.value_type.PERSEVERENCE]=41, + [df.value_type.PERSEVERANCE]=41, [df.value_type.LEISURE_TIME]=-11, [df.value_type.COMMERCE]=41, [df.value_type.ROMANCE]=41, @@ -239,22 +189,26 @@ function BreathOfArmok(unit) print ("The breath of Armok has engulfed "..unit.name.first_name) end -- --------------------------------------------------------------------------- -function LegendaryByClass(skilltype,v) - local unit=v - if unit==nil then +local function get_skill_desc(skill_idx) + return df.job_skill.attrs[skill_idx].caption or df.job_skill[skill_idx] or ("(unnamed skill %d)"):format(skill_idx) +end + +function LegendaryByClass(skilltype, unit) + if not unit then print ("No unit available! Aborting with extreme prejudice.") return end - local i - local skillclass local count_max = count_this(df.job_skill) for i=0, count_max do - skillclass = df.job_skill_class[df.job_skill.attrs[i].type] + if df.job_skill[i]:startswith('UNUSED') then goto continue end + local skillclass = df.job_skill_class[df.job_skill.attrs[i].type] if skilltype == skillclass then - print ("Skill "..df.job_skill.attrs[i].caption.." is type: "..skillclass.." and is now Legendary for "..unit.name.first_name) + local skillname = get_skill_desc(i) + print ("Skill "..skillname.." is type: "..skillclass.." and is now Legendary for "..unit.name.first_name) utils.insert_or_update(unit.status.current_soul.skills, { new = true, id = i, rating = 20 }, 'id') end + ::continue:: end end -- --------------------------------------------------------------------------- @@ -262,7 +216,7 @@ function PrintSkillList() local count_max = count_this(df.job_skill) local i for i=0, count_max do - print("'"..df.job_skill.attrs[i].caption.."' "..df.job_skill[i].." Type: "..df.job_skill_class[df.job_skill.attrs[i].type]) + print("'"..get_skill_desc(i).."' "..df.job_skill[i].." Type: "..df.job_skill_class[df.job_skill.attrs[i].type]) end print ("Provide the UPPER CASE argument, for example: PROCESSPLANTS rather than Threshing") end @@ -278,20 +232,18 @@ function PrintSkillClassList() end -- --------------------------------------------------------------------------- function adjust_all_dwarves(skillname) - for _,v in ipairs(df.global.world.units.all) do - if v.race == df.global.ui.race_id and v.status.current_soul then - print("Adjusting "..dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(v)))) - brainwash_unit(v) - elevate_attributes(v) - rejuvenate(v) - if skillname then - if df.job_skill_class[skillname] then - LegendaryByClass(skillname,v) - elseif skillname=="all" then - BreathOfArmok(v) - else - make_legendary(skillname,v) - end + for _,v in ipairs(dfhack.units.getCitizens()) do + print("Adjusting "..dfhack.df2console(dfhack.units.getReadableName(v))) + brainwash_unit(v) + elevate_attributes(v) + rejuvenate.rejuvenate(v, true) + if skillname then + if df.job_skill_class[skillname] then + LegendaryByClass(skillname,v) + elseif skillname=="all" then + BreathOfArmok(v) + else + make_legendary(skillname,v) end end end diff --git a/assign-beliefs.lua b/assign-beliefs.lua index 2a0d92cdb4..cc0de2c282 100644 --- a/assign-beliefs.lua +++ b/assign-beliefs.lua @@ -134,10 +134,6 @@ function assign(beliefs, unit, reset) for belief, level in pairs(beliefs) do assert(type(level) == "number") belief = belief:upper() - -- there's a typo in the game data - if belief == "PERSEVERANCE" then - belief = "PERSEVERENCE" - end if df.value_type[belief] then if level >= -3 and level <= 3 then local belief_value = calculate_random_belief_value(level) diff --git a/assign-facets.lua b/assign-facets.lua index 3e77b3e2e0..c91670f07c 100644 --- a/assign-facets.lua +++ b/assign-facets.lua @@ -134,10 +134,6 @@ function assign(facets, unit, reset) for facet, level in pairs(facets) do assert(type(level) == "number") facet = facet:upper() - -- there's a typo in the game data - if facet == "PERSEVERANCE" then - facet = "PERSEVERENCE" - end if df.personality_facet_type[facet] then if level >= -3 and level <= 3 then local facet_strength = calculate_random_facet_strength(level) diff --git a/assign-minecarts.lua b/assign-minecarts.lua index 9c859450d6..53e498d2b7 100644 --- a/assign-minecarts.lua +++ b/assign-minecarts.lua @@ -1,69 +1,30 @@ -- assigns minecarts to hauling routes --@ module = true ---[====[ - -assign-minecarts -================ -This script allows you to assign minecarts to hauling routes without having to -use the in-game interface. - -Usage:: - - assign-minecarts list|all| [-q|--quiet] - -:list: will show you information about your hauling routes, including whether - they have minecarts assigned to them. -:all: will automatically assign a free minecart to all hauling routes that don't - have a minecart assigned to them. - -If you specifiy a route id, only that route will get a minecart assigned to it -(if it doesn't already have one and there is a free minecart available). - -Add ``-q`` or ``--quiet`` to suppress informational output. - -Note that a hauling route must have at least one stop defined before a minecart -can be assigned to it. -]====] local argparse = require('argparse') -local quickfort = reqscript('quickfort') - --- ensures the list of available minecarts has been calculated by the game -local function refresh_ui_hauling_vehicles() - local qfdata - if #df.global.ui.hauling.routes > 0 then - -- if there is an existing route, move to the vehicle screen and back - -- out to force the game to scan for assignable minecarts - qfdata = 'hv^^' - else - -- if no current routes, create a route, move to the vehicle screen, - -- back out, and remove the route. The extra "px" is in the string in - -- case the user has the confirm plugin enabled. "p" pauses the plugin - -- and "x" retries the route deletion. - qfdata = 'hrv^xpx^' - end - quickfort.apply_blueprint{mode='config', data=qfdata} -end +local utils = require('utils') function get_free_vehicles() - refresh_ui_hauling_vehicles() local free_vehicles = {} - for _,minecart in ipairs(df.global.ui.hauling.vehicles) do - if minecart and minecart.route_id == -1 then - table.insert(free_vehicles, minecart) + for _,vehicle in ipairs(df.global.world.vehicles.active) do + if vehicle and vehicle.route_id == -1 then + table.insert(free_vehicles, vehicle) end end return free_vehicles end -local function has_minecart(route) - return #route.vehicle_ids > 0 -end - local function has_stops(route) return #route.stops > 0 end +local function get_minecart(route) + if #route.vehicle_ids == 0 then return end + local vehicle = utils.binsearch(df.global.world.vehicles.active, route.vehicle_ids[0], 'id') + if not vehicle then return end + return df.item.find(vehicle.item_id) +end + local function get_name(route) return route.name and #route.name > 0 and route.name or ('Route '..route.id) end @@ -73,8 +34,9 @@ local function get_id_and_name(route) end local function assign_minecart_to_route(route, quiet, minecart) - if has_minecart(route) then - return true + local assigned_minecart = get_minecart(route) + if assigned_minecart then + return assigned_minecart end if not has_stops(route) then if not quiet then @@ -93,6 +55,12 @@ local function assign_minecart_to_route(route, quiet, minecart) return false end end + for _,vehicle_id in ipairs(route.vehicle_ids) do + local vehicle = utils.binsearch(df.global.world.vehicles.all, vehicle_id, 'id') + if vehicle then vehicle.route_id = -1 end + end + route.vehicle_ids:resize(0) + route.vehicle_stops:resize(0) route.vehicle_ids:insert('#', minecart.id) route.vehicle_stops:insert('#', 0) minecart.route_id = route.id @@ -100,13 +68,13 @@ local function assign_minecart_to_route(route, quiet, minecart) print(('Assigned a minecart to route %s.') :format(get_id_and_name(route))) end - return true + return df.item.find(minecart.item_id) end -- assign first free minecart to the most recently-created route --- returns whether route now has a minecart assigned +-- returns assigned minecart (or nil if assignment failed) function assign_minecart_to_last_route(quiet) - local routes = df.global.ui.hauling.routes + local routes = df.global.plotinfo.hauling.routes local route_idx = #routes - 1 if route_idx < 0 then return false @@ -116,7 +84,7 @@ function assign_minecart_to_last_route(quiet) end local function get_route_by_id(route_id) - for _,route in ipairs(df.global.ui.hauling.routes) do + for _,route in ipairs(df.global.plotinfo.hauling.routes) do if route.id == route_id then return route end @@ -124,7 +92,7 @@ local function get_route_by_id(route_id) end local function list() - local routes = df.global.ui.hauling.routes + local routes = df.global.plotinfo.hauling.routes if 0 == #routes then print('No hauling routes defined.') else @@ -135,7 +103,7 @@ local function list() for _,route in ipairs(routes) do print(('%-8d %-9s %-9s %s') :format(route.id, - has_minecart(route) and 'yes' or 'NO', + get_minecart(route) and 'yes' or 'NO', has_stops(route) and 'yes' or 'NO', get_name(route))) end @@ -147,9 +115,9 @@ end local function all(quiet) local minecarts, idx = get_free_vehicles(), 1 - local routes = df.global.ui.hauling.routes + local routes = df.global.plotinfo.hauling.routes for _,route in ipairs(routes) do - if has_minecart(route) then + if get_minecart(route) then goto continue end if not assign_minecart_to_route(route, quiet, minecarts[idx]) then @@ -160,7 +128,7 @@ local function all(quiet) end end -local function do_help() +local function do_help(_) print(dfhack.script_help()) end @@ -184,7 +152,7 @@ local function main(args) local route = get_route_by_id(requested_route_id) if not route then dfhack.printerr('route id not found: '..requested_route_id) - elseif has_minecart(route) then + elseif get_minecart(route) then if not quiet then print(('Route %s already has a minecart assigned.') :format(get_id_and_name(route))) diff --git a/assign-preferences.lua b/assign-preferences.lua index 3eedaf30b6..9a8c1bdc6a 100644 --- a/assign-preferences.lua +++ b/assign-preferences.lua @@ -1,193 +1,12 @@ -- Change the preferences of a unit. --@ module = true -local help = [====[ - -assign-preferences -================== -A script to change the preferences of a unit. - -Preferences are classified into 12 types. The first 9 are: - -* like material; -* like creature; -* like food; -* hate creature; -* like item; -* like plant; -* like tree; -* like colour; -* like shape. - -These can be changed using this script. - -The remaining three are not currently managed by this script, -and are: like poetic form, like musical form, like dance form. - -To produce the correct description in the "thoughts and preferences" -page, you must specify the particular type of preference. For -each type, a description is provided in the section below. - -You will need to know the token of the object you want your dwarf to like. -You can find them in the wiki, otherwise in the folder "/raw/objects/" under -the main DF directory you will find all the raws defined in the game. - -For more information: -https://dwarffortresswiki.org/index.php/DF2014:Preferences - -Usage: - -``-help``: - print the help page. - -``-unit ``: - set the target unit ID. If not present, the - currently selected unit will be the target. - -``-likematerial [ <...> ]``: - usually a type of stone, a type of metal and a type - of gem, plus it can also be a type of wood, a type of - glass, a type of leather, a type of horn, a type of - pearl, a type of ivory, a decoration material - coral - or amber, a type of bone, a type of shell, a type - of silk, a type of yarn, or a type of plant cloth. - Write the full tokens. - There must be a space before and after each square - bracket. - -``-likecreature [ <...> ]``: - one or more creatures liked by the unit. You can - just list the species: the creature token will be - something similar to ``CREATURE:SPARROW:SKIN``, - so the name of the species will be ``SPARROW``. Nothing - will stop you to write the full token, if you want: the - script will just ignore the first and the last parts. - There must be a space before and after each square - bracket. - -``-likefood [ <...> ]``: - usually a type of alcohol, plus it can be a type of - meat, a type of fish, a type of cheese, a type of edible - plant, a cookable plant/creature extract, a cookable - mill powder, a cookable plant seed or a cookable plant - leaf. Write the full tokens. - There must be a space before and after each square - bracket. - -``-hatecreature [ <...> ]``: - works the same way as ``-likecreature``, but this time - it's one or more creatures that the unit detests. They - should be a type of ``HATEABLE`` vermin which isn't already - explicitly liked, but no check is performed about this. - Like before, you can just list the creature species. - There must be a space before and after each square - bracket. - -``-likeitem [ <...> ]``: - a kind of weapon, a kind of ammo, a kind of piece of - armor, a piece of clothing (including backpacks or - quivers), a type of furniture (doors, floodgates, beds, - chairs, windows, cages, barrels, tables, coffins, - statues, boxes, armor stands, weapon racks, cabinets, - bins, hatch covers, grates, querns, millstones, traction - benches, or slabs), a kind of craft (figurines, amulets, - scepters, crowns, rings, earrings, bracelets, or large - gems), or a kind of miscellaneous item (catapult parts, - ballista parts, a type of siege ammo, a trap component, - coins, anvils, totems, chains, flasks, goblets, - buckets, animal traps, an instrument, a toy, splints, - crutches, or a tool). The item tokens can be found here: - https://dwarffortresswiki.org/index.php/DF2014:Item_token - If you want to specify an item subtype, look into the files - listed under the column "Subtype" of the wiki page (they are - in the "/raw/ojects/" folder), then specify the items using - the full tokens found in those files (see examples below). - There must be a space before and after each square - bracket. - -``-likeplant [ <...> ]``: - works in a similar way as ``-likecreature``, this time - with plants. You can just List the plant species (the - middle part of the token). - There must be a space before and after each square - bracket. - -``-liketree [ <...> ]``: - works exactly as ``-likeplant``. I think this - preference type is here for backward compatibility (?). - You can still use it, however. As before, - you can just list the tree (plant) species. - There must be a space before and after each square - bracket. - -``-likecolor [ <...> ]``: - you can find the color tokens here: - https://dwarffortresswiki.org/index.php/DF2014:Color#Color_tokens - or inside the "descriptor_color_standard.txt" file - (in the "/raw/ojects/" folder). You can use the full token or - just the color name. - There must be a space before and after each square - bracket. - -``-likeshape [ <...> ]``: - I couldn't find a list of shape tokens in the wiki, but you - can find them inside the "descriptor_shape_standard.txt" - file (in the "/raw/ojects/" folder). You can - use the full token or just the shape name. - There must be a space before and after each square - bracket. - -``-reset``: - clear all preferences. If the script is called - with both this option and one or more preferences, - first all the unit preferences will be cleared - and then the listed preferences will be added. - -Examples: - -* "likes alabaster and willow wood":: - - assign-preferences -reset -likematerial [ INORGANIC:ALABASTER PLANT:WILLOW:WOOD ] - -* "likes sparrows for their ...":: - - assign-preferences -reset -likecreature SPARROW - -* "prefers to consume dwarven wine and olives":: - - assign-preferences -reset -likefood [ PLANT:MUSHROOM_HELMET_PLUMP:DRINK PLANT:OLIVE:FRUIT ] - -* "absolutely detests jumping spiders:: - - assign-preferences -reset -hatecreature SPIDER_JUMPING - -* "likes logs and battle axes":: - - assign-preferences -reset -likeitem [ WOOD ITEM_WEAPON:ITEM_WEAPON_AXE_BATTLE ] - -* "likes straberry plants for their ...":: - - assign-preferences -reset -likeplant BERRIES_STRAW - -* "likes oaks for their ...":: - - assign-preferences -reset -liketree OAK - -* "likes the color aqua":: - - assign-preferences -reset -likecolor AQUA - -* "likes stars":: - - assign-preferences -reset -likeshape STAR - -]====] - local utils = require("utils") local valid_args = utils.invert({ 'help', 'unit', + 'show', 'likematerial', 'likecreature', 'likefood', @@ -207,6 +26,38 @@ local function print_yellow(text) dfhack.color(-1) end +local function format_preference(pref, index) + print(string.format("Preference #%d:", index)) + + local pref_type = df.unitpref_type[pref.type] + local description = "" + if pref_type == "LikeMaterial" then + description = "Likes material: " .. dfhack.matinfo.getToken(pref.mattype, pref.matindex) + elseif pref_type == "LikeFood" then + description = "Likes food: " .. dfhack.matinfo.getToken(pref.mattype, pref.matindex) + elseif pref_type == "LikeItem" then + description = "Likes item type: " .. tostring(pref.item_type) + elseif pref_type == "LikePlant" then + description = "Likes plant: " .. dfhack.matinfo.getToken(pref.mattype, pref.matindex) + elseif pref_type == "HateCreature" then + description = "Hates creature: " .. df.global.world.raws.creatures.all[pref.creature_id].creature_id + elseif pref_type == "LikeColor" then + description = "Likes color: " .. df.global.world.raws.descriptors.colors[pref.color_id].id + elseif pref_type == "LikeShape" then + description = "Likes shape: " .. df.global.world.raws.descriptors.shapes[pref.shape_id].id + elseif pref_type == "LikePoeticForm" then + description = "Likes poetic form: " .. dfhack.translation.translateName(df.global.world.poetic_forms.all[pref.poetic_form_id].name, true) + elseif pref_type == "LikeMusicalForm" then + description = "Likes musical form: " .. dfhack.translation.translateName(df.global.world.musical_forms.all[pref.musical_form_id].name, true) + elseif pref_type == "LikeDanceForm" then + description = "Likes dance form: " .. dfhack.translation.translateName(df.global.world.dance_forms.all[pref.dance_form_id].name, true) + else + description = "Unknown preference type: " .. tostring(pref.type) + end + + print(description) +end + -- initialise random number generator local rng = dfhack.random.new() @@ -225,7 +76,7 @@ local preference_functions = { local ret = {} if mat_info then ret = { --luacheck:retype - type = df.unit_preference.T_type.LikeMaterial, + type = df.unitpref_type.LikeMaterial, item_type = -1, creature_id = -1, color_id = -1, @@ -238,7 +89,7 @@ local preference_functions = { mattype = mat_info.type, matindex = mat_info.index, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } else @@ -258,7 +109,7 @@ local preference_functions = { local index = utils.linear_index(df.global.world.raws.creatures.all, creature_id, "creature_id") if index then return { - type = df.unit_preference.T_type.LikeCreature, + type = df.unitpref_type.LikeCreature, item_type = index, creature_id = index, color_id = index, @@ -271,7 +122,7 @@ local preference_functions = { mattype = -1, matindex = -1, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } else @@ -305,7 +156,7 @@ local preference_functions = { item_type = df.item_type.POWDER_MISC elseif food_mat_index.CookableSeed > -1 then item_type = df.item_type.SEEDS - elseif food_mat_index.CookableLeaf > -1 then + elseif food_mat_index.CookablePlantGrowth > -1 then --[[ In case of plant growths, "mat_info" stores the item type as a specific subtype ("FLOWER", or "FRUIT", etc.) instead of the generic "PLANT_GROWTH" item type. Also, the IDs of the different types of growths @@ -335,7 +186,7 @@ local preference_functions = { if item_type then return { - type = df.unit_preference.T_type.LikeFood, + type = df.unitpref_type.LikeFood, item_type = item_type, creature_id = item_type, color_id = item_type, @@ -348,7 +199,7 @@ local preference_functions = { mattype = mat_info.type, matindex = mat_info.index, mat_state = 1, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } end @@ -370,7 +221,7 @@ local preference_functions = { local index = utils.linear_index(df.global.world.raws.creatures.all, creature_id, "creature_id") if index then return { - type = df.unit_preference.T_type.HateCreature, + type = df.unitpref_type.HateCreature, item_type = index, creature_id = index, color_id = index, @@ -383,7 +234,7 @@ local preference_functions = { mattype = -1, matindex = -1, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } else @@ -411,7 +262,7 @@ local preference_functions = { do if item_type then return { - type = df.unit_preference.T_type.LikeItem, + type = df.unitpref_type.LikeItem, item_type = item_type, creature_id = item_type, color_id = item_type, @@ -424,7 +275,7 @@ local preference_functions = { mattype = -1, matindex = -1, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } end @@ -446,7 +297,7 @@ local preference_functions = { local index = utils.linear_index(df.global.world.raws.plants.all, plant_id, "id") if index then return { - type = df.unit_preference.T_type.LikePlant, + type = df.unitpref_type.LikePlant, item_type = index, creature_id = index, color_id = index, @@ -459,7 +310,7 @@ local preference_functions = { mattype = -1, matindex = -1, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } else @@ -479,7 +330,7 @@ local preference_functions = { local index = utils.linear_index(df.global.world.raws.plants.all, plant_id, "id") if index then return { - type = df.unit_preference.T_type.LikeTree, + type = df.unitpref_type.LikeTree, item_type = index, creature_id = index, color_id = index, @@ -492,7 +343,7 @@ local preference_functions = { mattype = -1, matindex = -1, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } else @@ -513,7 +364,7 @@ local preference_functions = { local _, found, index = utils.binsearch(df.global.world.raws.descriptors.colors, color_name, "id") if found then return { - type = df.unit_preference.T_type.LikeColor, + type = df.unitpref_type.LikeColor, item_type = index, creature_id = index, color_id = index, @@ -526,7 +377,7 @@ local preference_functions = { mattype = -1, matindex = -1, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } else @@ -546,7 +397,7 @@ local preference_functions = { local index, _ = utils.linear_index(df.global.world.raws.descriptors.shapes, shape_name, "id") if index then return { - type = df.unit_preference.T_type.LikeShape, + type = df.unitpref_type.LikeShape, item_type = index, creature_id = index, color_id = index, @@ -559,7 +410,7 @@ local preference_functions = { mattype = -1, matindex = -1, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } else @@ -624,12 +475,25 @@ function assign(preferences, unit, reset) end end +-- ----------------------------------------------- SHOW PREF UTILITY ------------------------------------------------ -- +local function showPreferences(unit) + assert(not unit or type(unit) == "number" or df.unit:is_instance(unit)) + unit = unit or dfhack.gui.getSelectedUnit(true) + if not unit then + qerror("No unit found.") + end + + for i, pref in ipairs(unit.status.current_soul.preferences) do + format_preference(pref, i) + end +end + -- ------------------------------------------------------ MAIN ------------------------------------------------------ -- local function main(...) local args = utils.processArgs({ ... }, valid_args) if args.help then - print(help) + print(dfhack.script_help()) return end @@ -646,9 +510,15 @@ local function main(...) reset = true end + if args.show then + showPreferences(unit) + return + end + -- parse preferences args.unit = nil -- remove from args table args.reset = nil -- remove from args table + args.show = nil -- remove from args table local preferences = {} utils.assign(preferences, args) diff --git a/assign-profile.lua b/assign-profile.lua index a996c46135..70ce2ac600 100644 --- a/assign-profile.lua +++ b/assign-profile.lua @@ -92,7 +92,7 @@ local scripts = { FACETS = reqscript("assign-facets"), } -local default_filename = "/hack/scripts/dwarf_profiles.json" +local default_filename = dfhack.getHackPath().."/scripts/dwarf_profiles.json" -- ------------------------------------------------- APPLY PROFILE -------------------------------------------------- -- --- Apply the given profile to a unit, erasing or resetting the unit characteristics as requested. @@ -179,7 +179,7 @@ local function main(...) end local profile = load_profile(profile_name, filename) - apply_profile(profile, unit_id, reset) + apply_profile(profile, unit, reset) end if not dfhack_flags.module then diff --git a/autocheese.lua b/autocheese.lua new file mode 100644 index 0000000000..e9bdc146ae --- /dev/null +++ b/autocheese.lua @@ -0,0 +1,173 @@ +--@module = true + +---make cheese using a specific barrel and workshop +---@param barrel df.item +---@param workshop df.building_workshopst +---@return df.job +function makeCheese(barrel, workshop) + ---@type df.job + local job = dfhack.job.createLinked() + job.job_type = df.job_type.MakeCheese + + local jitem = df.job_item:new() + jitem.quantity = 0 + jitem.vector_id = df.job_item_vector_id.ANY_COOKABLE + jitem.flags1.unrotten = true + jitem.flags1.milk = true + job.job_items.elements:insert('#', jitem) + + if not dfhack.job.attachJobItem(job, barrel, df.job_role_type.Reagent, 0, -1) then + dfhack.error('could not attach item') + end + + dfhack.job.assignToWorkshop(job, workshop) + return job +end + +---checks that unit can path to workshop +---@param unit df.unit +---@param workshop df.building_workshopst +---@return boolean +function canAccessWorkshop(unit, workshop) + local workshop_position = xyz2pos(workshop.centerx, workshop.centery, workshop.z) + return dfhack.maps.canWalkBetween(unit.pos, workshop_position) +end + +---check if unit can perform labor at workshop +---@param unit df.unit +---@param unit_labor df.unit_labor +---@param workshop df.building +---@return boolean +function availableLaborer(unit, unit_labor, workshop) + return unit.status.labors[unit_labor] + and dfhack.units.isJobAvailable(unit) + and canAccessWorkshop(unit, workshop) +end + +---find unit with a particular labor enabled +---@param unit_labor df.unit_labor +---@param job_skill df.job_skill +---@param workshop df.building +---@return df.unit|nil +---@return integer|nil + function findAvailableLaborer(unit_labor, job_skill, workshop) + local max_unit = nil + local max_skill = -1 + for _, unit in ipairs(dfhack.units.getCitizens(true, false)) do + if + availableLaborer(unit, unit_labor, workshop) + then + local unit_skill = dfhack.units.getNominalSkill(unit, job_skill, true) + if unit_skill > max_skill then + max_unit = unit + max_skill = unit_skill + end + end + end + return max_unit, max_skill +end + +local function findMilkBarrel(min_liquids) + for _, container in ipairs(df.global.world.items.other.FOOD_STORAGE) do + if + not (container.flags.in_job or container.flags.forbid) and + container.flags.container and #container.general_refs >= min_liquids + then + local content_reference = dfhack.items.getGeneralRef(container, df.general_ref_type.CONTAINS_ITEM) + local contained_item = df.item.find(content_reference and content_reference.item_id or -1) + if contained_item then + local mat_info = dfhack.matinfo.decode(contained_item) + if mat_info:matches { milk = true } then + return container + end + end + end + end +end + +---find a workshop to which the barrel can be brought +---if the workshop has a master, only return workshop and master if the master is available +---@param pos df.coord +---@return df.building_workshopst? +---@return df.unit? +function findWorkshop(pos) + for _,workshop in ipairs(df.global.world.buildings.other.WORKSHOP_FARMER) do + if + dfhack.maps.canWalkBetween(pos, xyz2pos(workshop.centerx, workshop.centery, workshop.z)) and + not workshop.profile.blocked_labors[df.unit_labor.MAKE_CHEESE] and + #workshop.jobs == 0 + then + if #workshop.profile.permitted_workers == 0 then + -- immediately return workshop without master + return workshop, nil + else + unit = df.unit.find(workshop.profile.permitted_workers[0]) + if + unit and availableLaborer(unit, df.unit_labor.MAKE_CHEESE, workshop) + then + -- return workshop and master, if master is available + return workshop, unit + else + print("autocheese: Skipping farmer's workshop with unavailable master") + end + end + end + end +end + +if dfhack_flags.module then + return +end + +-- actual script action + +local argparse = require('argparse') + +local min_number = 50 + +local _ = argparse.processArgsGetopt({...}, +{ + { 'm', 'min-milk', hasArg = true, + handler = function(min) + min_number = argparse.nonnegativeInt(min, 'min-milk') + end } +}) + + +local reagent = findMilkBarrel(min_number) + +if not reagent then + -- print('autocheese: no sufficiently full barrel found') + return +end + +local workshop, worker = findWorkshop(xyz2pos(dfhack.items.getPosition(reagent))) + +if not workshop then + print("autocheese: no Farmer's Workshop available") + return +end + +-- try to find laborer for workshop without master +if not worker then + worker, _ = findAvailableLaborer(df.unit_labor.MAKE_CHEESE, df.job_skill.CHEESEMAKING, workshop) +end + +if not worker then + print('autocheese: no cheesemaker available') + return +end +local job = makeCheese(reagent, workshop) + +print(('autocheese: dispatching cheesemaking job for %s (%d milk) to %s'):format( + dfhack.df2console(dfhack.items.getReadableDescription(reagent)), + #reagent.general_refs, + dfhack.df2console(dfhack.units.getReadableName(worker)) +)) + + +-- assign a worker and send it to fetch the barrel +dfhack.job.addWorker(job, worker) +dfhack.units.setPathGoal(worker, reagent.pos, df.unit_path_goal.GrabJobResources) +job.items[0].flags.is_fetching = true +job.flags.fetching = true diff --git a/autofish.lua b/autofish.lua new file mode 100644 index 0000000000..d743175ca8 --- /dev/null +++ b/autofish.lua @@ -0,0 +1,257 @@ +-- handles automatic fishing jobs to limit the number of fish the fortress keeps on hand +-- autofish [enable | disable] [min] [] + +--@ enable=true +--@ module=true + +local argparse = require("argparse") +local repeatutil = require("repeat-util") + +local GLOBAL_KEY = "autofish" + +-- set default enabled state +enabled = enabled or false +s_maxFish = s_maxFish or 100 +s_minFish = s_minFish or 75 +s_useRaw = s_useRaw or true +isFishing = isFishing or true + +--- Check if the script is enabled. +-- @return true if the script is enabled, otherwise false. +function isEnabled() + return enabled +end + +--- Save the current state of the script +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, { + enabled=enabled, + s_maxFish=s_maxFish, + s_minFish=s_minFish, + s_useRaw=s_useRaw, + isFishing=isFishing, + }) +end + +--- Load the saved state of the script +local function load_state() + -- load persistent data + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) + enabled = persisted_data.enabled or false + s_maxFish = persisted_data.s_maxFish or 100 + s_minFish = persisted_data.s_minFish or 75 + s_useRaw = persisted_data.s_useRaw or (persisted_data.s_useRaw == nil) + isFishing = persisted_data.isFishing or (persisted_data.isFishing == nil) +end + +--- Set the maximum fish threshold. +-- @param val The value to set s_maxFish to. (number) +function set_minFish(val) + s_minFish = val + -- min fish cannot exceed max fish + if s_minFish >= s_maxFish then s_minFish = s_maxFish end + persist_state() +end + +--- Set the minimum fish threshold. +-- @param val The value to set s_minFish to. (number) +function set_maxFish(val) + s_maxFish = val + -- max fish cannot be lower than min fish + if s_maxFish <= s_minFish then s_minFish = s_maxFish end + persist_state() +end + +--- Set the raw fish toggle +-- @param val The value to set s_useRaw to. (boolean) +function set_useRaw(val) + s_useRaw = val + persist_state() +end + +--- Toggle all work details (and fishing labours) +-- @param state What state to toggle the labours to. +function toggle_fishing_labour(state) + -- pass true to state to turn on, otherwise disable + -- find all work details that have fishing enabled: + local work_details = df.global.plotinfo.labor_info.work_details + for _,v in pairs(work_details) do + if v.allowed_labors.FISH then + v.flags.mode = state and + df.work_detail_mode.OnlySelectedDoesThis or df.work_detail_mode.NobodyDoesThis + + -- since the work details are not actually applied unless a button + -- is clicked on the work details screen, we have to manually set + -- unit labours + for _,v2 in ipairs(v.assigned_units) do + -- find unit by ID and toggle fishing + local unit = df.unit.find(v2) + if unit then + unit.status.labors.FISH = state + end + end + end + end + isFishing = state -- save current state + + -- let the user know we've got enough, or run out of fish + if isFishing then + print("autofish: Re-enabling fishing, fallen below minimum.") + else + print("autofish: Disabling fishing, reached desired quota.") + end +end + +--- Checks several item flags to see if a given item should be considered good +-- @param item: a valid dwarf fortress item. +function isValidItem(item) + local flags = item.flags + if flags.rotten or flags.trader or flags.hostile or flags.forbid + or flags.dump or flags.on_fire or flags.garbage_collect or flags.owned + or flags.removed or flags.encased or flags.spider_web then + return false + end + return true +end + + +--- Counts the number of available fish in a fortress +-- @return prepared (number): count of prepared fish available. +-- @return raw (number): count of raw fish available. +function count_fish() + local world = df.global.world + + -- count the number of valid fish we have. (not rotten, forbidden, on fire, dumping...) + local prepared, raw = 0, 0 + for k,v in pairs(world.items.other[df.items_other_id.IN_PLAY]) do + if v:getType() == df.item_type.FISH and isValidItem(v) then + prepared = prepared + v:getStackSize() + end + if (v:getType() == df.item_type.FISH_RAW and isValidItem(v)) and s_useRaw then + raw = raw + v:getStackSize() + end + end + return prepared, raw +end + +--- The main event loop of the script. +function event_loop() + if not enabled then return end + + local prepared, raw = count_fish() + + -- handle pausing/resuming labour + local numFish = s_useRaw and (prepared+raw) or prepared + if isFishing and (numFish >= s_maxFish) then + toggle_fishing_labour(false) + elseif not isFishing and (numFish < s_minFish) then + toggle_fishing_labour(true) + end + + persist_state() + + -- check weekly + repeatutil.scheduleUnlessAlreadyScheduled(GLOBAL_KEY, 7, "days", event_loop) +end + + +--- Print a status output, showing the current state of the script and options. +local function print_status() + print(string.format("autofish is currently %s.\n", (enabled and "enabled" or "disabled"))) + if enabled then + local rfs + rfs = s_useRaw and "raw & prepared" or "prepared" + + print(string.format("Stopping at %s %s fish.", s_maxFish, rfs)) + print(string.format("Restarting at %s %s fish.", s_minFish, rfs)) + if isFishing then + print("\nCurrently allowing fishing.") + else + print("\nCurrently not allowing fishing.") + end + end +end + +--- Handles automatic loading +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + -- unload with game + if sc == SC_MAP_UNLOADED then + enabled = false + return + end + + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return + end + + load_state() + + -- run the main code + event_loop() +end + + +-- sanity checks? +if dfhack_flags.module then + return +end + +if df.global.gamemode ~= df.game_mode.DWARF or not dfhack.isMapLoaded() then + dfhack.printerr("autofish needs a loaded fortress to work") + return +end + +-- argument handling +local args = {...} +if dfhack_flags and dfhack_flags.enable then + args = {dfhack_flags.enable_state and "enable" or "disable"} +end + +-- handle options flags +local positionals = argparse.processArgsGetopt(args, + {{"r", "raw", hasArg=true, + handler=function(optArg) + local val = argparse.boolean(optArg, "raw") + set_useRaw(val) + end} +}) + +load_state() +-- handle the rest of the arguments +if positionals[1] == "enable" then + enabled = true + +elseif positionals[1] == "disable" then + enabled = false + persist_state() + repeatutil.cancel(GLOBAL_KEY) + return + +elseif positionals[1] == "status" then + print_status() + return + +-- positionals is an empty table if no positional arguments are set +elseif positionals ~= nil then + -- check to see if passed args are numbers + if positionals[1] and tonumber(positionals[1]) then + -- assume we're changing setting: + local newval = tonumber(positionals[1]) + set_maxFish(newval) + if not positionals[2] then + set_minFish(math.floor(newval * 0.75)) + end + end + + if positionals[2] and tonumber(positionals[2]) then + set_minFish(tonumber(positionals[2])) + end + + -- a setting probably changed, save & show the updated settings. + persist_state() + print_status() + return +end + +event_loop() +persist_state() diff --git a/autonick.lua b/autonick.lua index 2ea3cb4032..ee1cd01326 100644 --- a/autonick.lua +++ b/autonick.lua @@ -1,33 +1,4 @@ -- gives dwarves unique nicknames ---[====[ - -autonick -======== -Gives dwarves unique nicknames chosen randomly from ``dfhack-config/autonick.txt``. - -One nickname per line. -Empty lines, lines beginning with ``#`` and repeat entries are discarded. - -Dwarves with manually set nicknames are ignored. - -If there are fewer available nicknames than dwarves, the remaining -dwarves will go un-nicknamed. - -You may wish to use this script with the "repeat" command, e.g: -``repeat -name autonick -time 3 -timeUnits months -command [ autonick all ]`` - -Usage: - - autonick all [] - autonick help - -Options: - -:``-h``, ``--help``: - Show this text. -:``-q``, ``--quiet``: - Do not report how many dwarves were given nicknames. -]====] local options = {} @@ -48,9 +19,8 @@ end local seen = {} --check current nicknames -for _,unit in ipairs(df.global.world.units.active) do - if dfhack.units.isCitizen(unit) and - unit.name.nickname ~= "" then +for _,unit in ipairs(dfhack.units.getCitizens()) do + if unit.name.nickname ~= "" then seen[unit.name.nickname] = true end end @@ -70,16 +40,15 @@ end --assign names local count = 0 -for _,unit in ipairs(df.global.world.units.active) do +for _,unit in ipairs(dfhack.units.getCitizens()) do if (#names == 0) then if options.quiet ~= true then - print("no free names left in dfhack-config/autonick.txt") + print("not enough unique names in dfhack-config/autonick.txt") end break end --if there are any names left - if dfhack.units.isCitizen(unit) and - unit.name.nickname == "" then + if unit.name.nickname == "" then newnameIndex = math.random (#names) dfhack.units.setNickname(unit, names[newnameIndex]) table.remove(names, newnameIndex) diff --git a/autotraining.lua b/autotraining.lua new file mode 100644 index 0000000000..386ab6bb0a --- /dev/null +++ b/autotraining.lua @@ -0,0 +1,296 @@ +-- Based on the original code by RNGStrategist (who also got some help from Uncle Danny) +--@ enable = true +--@ module = true + +local repeatUtil = require('repeat-util') +local utils=require('utils') + +local GLOBAL_KEY = "autotraining" +local MartialTraining = df.need_type['MartialTraining'] +local ignore_count = 0 + +local function get_default_state() + return { + enabled=false, + threshold=-5000, + ignored={}, + ignored_nobles={}, + training_squads = {}, + } +end + +state = state or get_default_state() + +function isEnabled() + return state.enabled +end + +-- persisting a table with numeric keys results in a json array with a huge number of null entries +-- therefore, we convert the keys to strings for persistence +local function to_persist(persistable) + local persistable_ignored = {} + for k, v in pairs(persistable) do + persistable_ignored[tostring(k)] = v + end + return persistable_ignored +end + +-- loads both from the older array format and the new string table format +local function from_persist(persistable) + if not persistable then + return + end + local ret = {} + for k, v in pairs(persistable) do + ret[tonumber(k)] = v + end + return ret +end + +function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, { + enabled=state.enabled, + threshold=state.threshold, + ignored=to_persist(state.ignored), + ignored_nobles=state.ignored_nobles, + training_squads=to_persist(state.training_squads) + }) +end + +--- Load the saved state of the script +local function load_state() + -- load persistent data + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) + state.enabled = persisted_data.enabled or state.enabled + state.threshold = persisted_data.threshold or state.threshold + state.ignored = from_persist(persisted_data.ignored) or state.ignored + state.ignored_nobles = persisted_data.ignored_nobles or state.ignored_nobles + state.training_squads = from_persist(persisted_data.training_squads) or state.training_squads + return state +end + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + state.enabled = false + return + end + -- the state changed, is a map loaded and is that map in fort mode? + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + -- no its isnt, so bail + return + end + -- yes it was, so: + + -- retrieve state saved in game. merge with default state so config + -- saved from previous versions can pick up newer defaults. + load_state() + if state.enabled then + start() + end + persist_state() +end + + +--###### +--Functions +--###### +local function isIgnoredNoble(unit) + local noblePos = dfhack.units.getNoblePositions(unit) + if noblePos ~= nil then + for _, position in ipairs(noblePos) do + if state.ignored_nobles[position.position.code] then + return true + end + end + end + return false +end + +---@return table +function getTrainingCandidates() + local ret = {} + ignore_count = 0 + for _, unit in ipairs(dfhack.units.getCitizens(true)) do + if not dfhack.units.isAdult(unit) then + goto next_unit + end + local need = getTrainingNeed(unit) + if not need or need.focus_level >= state.threshold then + goto next_unit + end + -- ignored units are those that would like to train but are forbidden from doing so + if state.ignored[unit.id] then + ignore_count = ignore_count + 1 + goto next_unit + end + if isIgnoredNoble(unit) then + ignore_count = ignore_count + 1 + goto next_unit + end + if unit.military.squad_id ~= -1 then + goto next_unit + end + table.insert(ret, { unit = unit, need = need.focus_level }) + ::next_unit:: + end + table.sort(ret, function (a, b) return a.need < b.need end) + return ret +end + +function getTrainingSquads() + local squads = {} + for squad_id, active in pairs(state.training_squads) do + local squad = df.squad.find(squad_id) + if active and squad then + table.insert(squads, squad) + else + -- setting to nil during iteration is permitted by lua + state.training_squads[squad_id] = nil + end + end + return squads +end + +function getTrainingNeed(unit) + if unit == nil then return nil end + local needs = unit.status.current_soul.personality.needs + for _, need in ipairs(needs) do + if need.id == MartialTraining then + return need + end + end + return nil +end + +--###### +--Main +--###### + +-- Find all training squads +-- Abort if no squads found +function checkSquads() + local squads = {} + for _, squad in ipairs(getTrainingSquads()) do + if squad.entity_id == df.global.plotinfo.group_id then + local leader = squad.positions[0].occupant + if leader ~= -1 then + table.insert(squads,squad) + end + end + end + + if #squads == 0 then + return nil + end + + return squads +end + +function addTraining(unit,good_squads) + if unit.military.squad_id ~= -1 then + for _, squad in ipairs(good_squads) do + if unit.military.squad_id == squad.id then + return true + end + end + return false + end + for _, squad in ipairs(good_squads) do + for i=1,9,1 do + if squad.positions[i].occupant == -1 then + return dfhack.military.addToSquad(unit.id,squad.id,i) + end + end + end + + return false +end + +function removeAll() + if state.training_squads == nil then return end + for _, squad in ipairs(getTrainingSquads()) do + for i=1,9,1 do + local hf = df.historical_figure.find(squad.positions[i].occupant) + if hf ~= nil then + dfhack.military.removeFromSquad(hf.unit_id) + end + end + end +end + + +function check() + local squads = checkSquads() + local intraining_count = 0 + local inque_count = 0 + if squads == nil then return end + for _,squad in ipairs(squads) do + for i=1,9,1 do + if squad.positions[i].occupant ~= -1 then + local hf = df.historical_figure.find(squad.positions[i].occupant) + if hf ~= nil then + local unit = df.unit.find(hf.unit_id) + local training_need = getTrainingNeed(unit) + if not training_need or training_need.focus_level >= state.threshold then + dfhack.military.removeFromSquad(unit.id) + end + end + end + end + end + for _, p in ipairs(getTrainingCandidates()) do + local added = addTraining(p.unit, squads) + if added then + intraining_count = intraining_count +1 + else + inque_count = inque_count +1 + end + end + print(("%s: %d training, %d waiting, and %d excluded units with training needs"): + format(GLOBAL_KEY, intraining_count, inque_count, ignore_count)) +end + +function start() + repeatUtil.scheduleEvery(GLOBAL_KEY, 1, 'days', check) +end + +function stop() + repeatUtil.cancel(GLOBAL_KEY) +end + +function enable() + state.enabled = true + persist_state() + start() +end + +function disable() + state.enabled = false + persist_state() + stop() + removeAll() +end + +if dfhack_flags.module then + return +end + +validArgs = utils.invert({ + 't' +}) + +local args = utils.processArgs({...}, validArgs) + +if dfhack_flags.enable then + if dfhack_flags.enable_state then + enable() + else + disable() + end +else + -- called on the command-line + if args.t then + state.threshold = 0-tonumber(args.t) + end + print(("autotraining is %s"):format(state.enabled and "enabled" or "disabled")) +end diff --git a/autounsuspend.lua b/autounsuspend.lua deleted file mode 100644 index 90b3801f88..0000000000 --- a/autounsuspend.lua +++ /dev/null @@ -1,36 +0,0 @@ --- automate periodic running of the unsuspend script ---[====[ - -autounsuspend -============= -Periodically check construction jobs and keep them unsuspended with the -`unsuspend` script. -]====] - -local repeatUtil = require 'repeat-util' - -local job_name = '__autounsuspend' - -local function help() - print('syntax: autounsuspend [start|stop]') -end - -local function stop() - repeatUtil.cancel(job_name) - print('autounsuspend Stopped.') -end - -local function start() - local unsuspend_fn = function() dfhack.run_script('unsuspend') end - repeatUtil.scheduleEvery(job_name, '1', 'days', unsuspend_fn) - print('autounsuspend Running.') -end - -local action_switch = { - start=start, - stop=stop, -} -setmetatable(action_switch, {__index=function() return help end}) - -local args = {...} -action_switch[args[1] or 'help']() diff --git a/ban-cooking.lua b/ban-cooking.lua new file mode 100644 index 0000000000..2254e116dd --- /dev/null +++ b/ban-cooking.lua @@ -0,0 +1,310 @@ +-- convenient way to ban cooking categories of food +-- based on ban-cooking.rb by Putnam: https://github.com/DFHack/scripts/pull/427/files +-- Putnams work completed by TBSTeun + +local argparse = require('argparse') + +local kitchen = df.global.plotinfo.kitchen + +local options = {} +local banned = {} +local count = 0 + +local function make_key(mat_type, mat_index, type, subtype) + return ('%s:%s:%s:%s'):format(mat_type, mat_index, type, subtype) +end + +local function ban_cooking(print_name, mat_type, mat_index, type, subtype) + local key = make_key(mat_type, mat_index, type, subtype) + -- Skip adding a new entry further below if there's nothing to do + if (banned[key] and not options.unban) or (not banned[key] and options.unban) then + return + end + -- The item hasn't already been (un)banned, so we do that here by appending/removing + -- its values to/from the various arrays + count = count + 1 + if options.verbose then + print(print_name .. ' has been ' .. (options.unban and 'un' or '') .. 'banned!') + end + + if options.unban then + dfhack.kitchen.removeExclusion({Cook=true}, type, subtype, mat_type, mat_index) + banned[key] = nil + else + dfhack.kitchen.addExclusion({Cook=true}, type, subtype, mat_type, mat_index) + banned[key] = { + mat_type=mat_type, + mat_index=mat_index, + type=type, + subtype=subtype, + } + end +end + +local function init_banned() + -- Iterate over the elements of the kitchen.item_types list + for i in ipairs(kitchen.item_types) do + if kitchen.exc_types[i].Cook then + local key = make_key(kitchen.mat_types[i], kitchen.mat_indices[i], kitchen.item_types[i], kitchen.item_subtypes[i]) + if not banned[key] then + banned[key] = { + mat_type=kitchen.mat_types[i], + mat_index=kitchen.mat_indices[i], + type=kitchen.item_types[i], + subtype=kitchen.item_subtypes[i], + } + end + end + end +end + +local funcs = {} + +funcs.booze = function() + for _, p in ipairs(df.global.world.raws.plants.all) do + for _, m in ipairs(p.material) do + if m.flags.ALCOHOL 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.DRINK, -1) + end + end + end + for _, c in ipairs(df.global.world.raws.creatures.all) do + for _, m in ipairs(c.material) do + if m.flags.ALCOHOL and m.flags.EDIBLE_COOKED then + local matinfo = dfhack.matinfo.find(c.creature_id, m.id) + ban_cooking(c.name[2] .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.DRINK, -1) + end + end + end +end + +funcs.honey = function() + for _, c in ipairs(df.global.world.raws.creatures.all) do + for _, m in ipairs(c.material) do + 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(c.creature_id, m.id) + ban_cooking(c.name[2] .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.LIQUID_MISC, -1) + break + end + end + end + end + end +end + +funcs.tallow = function() + for _, c in ipairs(df.global.world.raws.creatures.all) do + for _, m in ipairs(c.material) do + if m.flags.EDIBLE_COOKED then + for _, s in ipairs(m.reaction_product.id) do + if s.value == "SOAP_MAT" then + local matinfo = dfhack.matinfo.find(c.creature_id, m.id) + ban_cooking(c.name[2] .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.GLOB, -1) + break + end + end + end + end + end +end + +funcs.milk = function() + for _, c in ipairs(df.global.world.raws.creatures.all) do + for _, m in ipairs(c.material) do + if m.flags.EDIBLE_COOKED then + for _, s in ipairs(m.reaction_product.id) do + if s.value == "CHEESE_MAT" then + local matinfo = dfhack.matinfo.find(c.creature_id, m.id) + ban_cooking(c.name[2] .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.LIQUID_MISC, -1) + break + end + end + end + end + end +end + +funcs.oil = function() + for _, p in ipairs(df.global.world.raws.plants.all) do + for _, m in ipairs(p.material) do + if m.flags.EDIBLE_COOKED then + for _, s in ipairs(m.reaction_product.id) do + if s.value == "SOAP_MAT" then + local matinfo = dfhack.matinfo.find(p.id, m.id) + ban_cooking(p.name .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.LIQUID_MISC, -1) + break + end + end + end + end + end +end + +funcs.seeds = function() + for _, p in ipairs(df.global.world.raws.plants.all) do + 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" 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_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 + ban_cooking(p.name .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.PLANT_GROWTH, k) + end + end + end + + ::continue:: + end +end + +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" 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 + local matinfo = dfhack.matinfo.decode(g) + local m = matinfo.material + if m.flags.EDIBLE_COOKED then + for _, s in ipairs(m.reaction_product.id) do + if s.value == "DRINK_MAT" then + ban_cooking(p.name .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.PLANT_GROWTH, k) + break + end + end + end + end + + ::continue:: + end +end + +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" 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 + end +end + +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" 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 + local matinfo = dfhack.matinfo.decode(g) + local m = matinfo.material + if m.flags.EDIBLE_COOKED then + for _, s in ipairs(m.reaction_product.id) do + if s.value == "THREAD" then + ban_cooking(p.name .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.PLANT_GROWTH, k) + break + end + end + end + end + + ::continue:: + end +end + +funcs.fruit = function() + for _, p in ipairs(df.global.world.raws.plants.all) do + for k, g in ipairs(p.growths) do + local matinfo = dfhack.matinfo.decode(g) + local m = matinfo.material + if m.id == "FRUIT" and m.flags.EDIBLE_COOKED and m.flags.STOCKPILE_PLANT_GROWTH then + for _, s in ipairs(m.reaction_product.id) do + if s.value == "DRINK_MAT" then + ban_cooking(p.name .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.PLANT_GROWTH, k) + break + end + end + end + end + end +end + +local classes = argparse.processArgsGetopt({...}, { + {'h', 'help', handler=function() options.help = true end}, + {'u', 'unban', handler=function() options.unban = true end}, + {'v', 'verbose', handler=function() options.verbose = true end}, +}) + +if options.help == true then + print(dfhack.script_help()) + return +end + +init_banned() + +if classes[1] == 'all' then + for _, func in pairs(funcs) do + func() + end +else + for _, v in ipairs(classes) do + if funcs[v] then + funcs[v]() + end + end +end + +print((options.unban and 'un' or '') .. 'banned ' .. count .. ' types.') diff --git a/ban-cooking.rb b/ban-cooking.rb deleted file mode 100644 index 5b45c285e1..0000000000 --- a/ban-cooking.rb +++ /dev/null @@ -1,358 +0,0 @@ -# convenient way to ban cooking categories of food -=begin - -ban-cooking -=========== -A more convenient way to ban cooking various categories of foods than the -kitchen interface. Usage: ``ban-cooking ``. Valid types are ``booze``, -``honey``, ``tallow``, ``oil``, ``seeds`` (non-tree plants with seeds), -``brew``, ``fruit``, ``mill``, ``thread``, and ``milk``. - -=end - -# Create a dictionary/hash table to store what items are already banned. -already_banned = {} - -# Just create a shorthand reference to the kitchen object -kitchen = df.ui.kitchen - -# Store our list of banned items in the dictionary/hash table -kitchen.item_types.length.times { |i| - if kitchen.exc_types[i] == :Cook - already_banned[[kitchen.mat_types[i], kitchen.mat_indices[i], kitchen.item_types[i], kitchen.item_subtypes[i]]] = true - end -} - -# The function for actually banning cooking of an item. -# -- subtype was added to the arguments list from the original script, as -# the original script defaulted subtype to -1, which doesn't support tree -# fruit items -# -- item names was added to the front of the arguments list, as the -# original script ran silently, and during debugging it was found to be -# more useful to print the banned item names than picking through the -# kitchen menu in game -ban_cooking = lambda { |print_name, mat_type, mat_index, type, subtype| - key = [mat_type, mat_index, type, subtype] - # Skip adding a new entry further below, if the item is already banned. - if already_banned[key] - return - end - # The item hasn't already been banned, so we do that here by appending its values to the various arrays - puts(print_name + ' has been banned!') - # grab the length of the array now, before it's appended to, so that we don't have to subtract one for the index value to be correct after appending is done. - length = df.ui.kitchen.mat_types.length - df.ui.kitchen.mat_types << mat_type - df.ui.kitchen.mat_indices << mat_index - df.ui.kitchen.item_types << type - df.ui.kitchen.item_subtypes << subtype - df.ui.kitchen.exc_types << :Cook - already_banned[key] = true -} - -$script_args.each do |arg| - case arg - # ban the cooking of plant based alcohol - # -- targets creature based alcohol, of which I'm not sure if any exists, but it should be banned too if it does, I guess (forgotten beasts maybe?) - when 'booze' - df.world.raws.plants.all.each_with_index do |p, i| - p.material.each_with_index do |m, j| - if m.flags[:ALCOHOL] and m.flags[:EDIBLE_COOKED] - ban_cooking[p.name + ' ' + m.id, j + DFHack::MaterialInfo::PLANT_BASE, i, :DRINK, -1] - end - end - end - df.world.raws.creatures.all.each_with_index do |c, i| - c.material.each_with_index do |m, j| - if m.flags[:ALCOHOL] and m.flags[:EDIBLE_COOKED] - ban_cooking[c.name[0] + ' ' + m.id, j + DFHack::MaterialInfo::CREATURE_BASE, i, :DRINK, -1] - end - end - end - - # Mmmm.... mead. For those days when you want to savor the labor of thousands of semi-willingly enslaved workers. - # Bans only honey bee honey... technically dwarves could collect bumble bee honey from wild nests, I think... - when 'honey' - # hard-coded in the raws of the mead reaction - honey = df.decode_mat('CREATURE:HONEY_BEE:HONEY') - ban_cooking['honey bee honey', honey.mat_type, honey.mat_index, :LIQUID_MISC, -1] - - # Gotta have that cat soap somehow... - # Just wait until explosives are implemented... - # Bans all tallow from creatures - when 'tallow' - df.world.raws.creatures.all.each_with_index do |c, i| - c.material.each_with_index do |m, j| - if m.flags[:EDIBLE_COOKED] and m.reaction_product.id.include?('SOAP_MAT') - ban_cooking[c.name[0] + ' ' + m.id, j + DFHack::MaterialInfo::CREATURE_BASE, i, :GLOB, -1] - end - end - end - - # Too bad adding this to meals doesn't alter the bone fracture mechanics (both healing and damage taking) - # Ban milk from cooking, so that cheese can be produced - # -- Not the best of ideas, as currently milk lasts forever, and cheese rots. - # -- Technically hard cheeses never go "bad", they just grow a nasty mold layer that can be cut off - when 'milk' - df.world.raws.creatures.all.each_with_index do |c, i| - c.material.each_with_index do |m, j| - if m.flags[:EDIBLE_COOKED] and m.reaction_product.id.include?('CHEESE_MAT') - ban_cooking[c.name[0] + ' ' + m.id, j + DFHack::MaterialInfo::CREATURE_BASE, i, :LIQUID_MISC, -1] - end - end - end - - # Don't be an elf... - # Ban all plant based oils from cooking - when 'oil' - df.world.raws.plants.all.each_with_index do |p, i| - p.material.each_with_index do |m, j| - if m.flags[:EDIBLE_COOKED] and m.reaction_product.id.include?('SOAP_MAT') - ban_cooking[p.name + ' ' + m.id, j + DFHack::MaterialInfo::PLANT_BASE, i, :LIQUID_MISC, -1] - end - end - end - - # Ban seeds, and the plant parts that produce the seeds from being cooked - # -- Doesn't ban seeds that can't be farmed (trees), as well as those that can't be brewed, - # as gaining seeds from dwarves eating the food raw is too time consuming. - when 'seeds' - df.world.raws.plants.all.each_with_index do |p, i| - # skip over plants without seeds and tree seeds (as you can't currently farm trees with their seeds) - if p.material_defs.type[:Seed] != -1 and p.material_defs.idx[:Seed] != -1 and not p.flags.inspect.include?('TREE') - # Bans the seeds themselves - ban_cooking[p.name + ' seeds', p.material_defs.type[:Seed], p.material_defs.idx[:Seed], :SEEDS, -1] - # This section handles banning the structural plant parts that produce seeds. - # -- There's no guarantee I can find that the STRUCTURAL material will be array item zero in the materials array - # thus I'm playing it safe with a possibly wasteful loop here - p.material.each_with_index do |m, j| - # only operate here on STRUCTURAL materials, as the rest will be :PLANT_GROWTH, instead of just :PLANT - # which then means that the subtype won't be -1 - if m.id == "STRUCTURAL" and m.flags[:EDIBLE_COOKED] and m.reaction_product.id.include?('SEED_MAT') and m.reaction_product.id.include?('DRINK_MAT') - ban_cooking[p.name + ' ' + m.id, j + DFHack::MaterialInfo::PLANT_BASE, i, :PLANT, -1] - end - end - # This section handles banning the plant growths that produce seeds - p.growths.each_with_index do |g, r| - m = df.decode_mat(g).material - if m.flags[:EDIBLE_COOKED] and m.reaction_product.id.include?('SEED_MAT') and m.reaction_product.id.include?('DRINK_MAT') - p.material.each_with_index do |s, j| - if m.id == s.id - ban_cooking[p.name + ' ' + m.id, j + DFHack::MaterialInfo::PLANT_BASE, i, :PLANT_GROWTH, r] - end - end - end - end - end - end - - # Bans cooking of alcohol producing plant parts - when 'brew' - df.world.raws.plants.all.each_with_index do |p, i| - # skip over any plants that don't have an alcohol listed - if p.material_defs.type[:Drink] != -1 and p.material_defs.idx[:Drink] != -1 - p.material.each_with_index do |m, j| - # only operate here on STRUCTURAL materials, as the rest will be :PLANT_GROWTH, instead of just :PLANT - # which then means that the subtype won't be -1 - if m.id == "STRUCTURAL" and m.flags[:EDIBLE_COOKED] and m.reaction_product.id.include?('DRINK_MAT') - ban_cooking[p.name + ' ' + m.id, j + DFHack::MaterialInfo::PLANT_BASE, i, :PLANT, -1] - end - end - # This section handles banning the plant growths that produce alcohol - p.growths.each_with_index do |g, r| - m = df.decode_mat(g).material - if m.flags[:EDIBLE_COOKED] and m.reaction_product.id.include?('DRINK_MAT') - p.material.each_with_index do |s, j| - if m.id == s.id - ban_cooking[p.name + ' ' + m.id, j + DFHack::MaterialInfo::PLANT_BASE, i, :PLANT_GROWTH, r] - end - end - end - end - end - end - - # Should work, but I don't think there are any millable plants that are cookable - when 'mill' - df.world.raws.plants.all.each_with_index do |p, i| - # skip over plants that don't have a millable part listed - if p.material_defs.idx[:Mill] != -1 - p.material.each_with_index do |m, j| - if m.id == "STRUCTURAL" and m.flags[:EDIBLE_COOKED] - ban_cooking[p.name + ' ' + m.id, j + DFHack::MaterialInfo::PLANT_BASE, i, :PLANT, -1] - end - end - # No plant growths are targeted for milling, as I can't find a flag that would indicate that a growth - # was used for milling. Thus, I can only assume that only the STRUCTURAL plant object can be used - # in the milling process. - end - end - - # Should work, but I don't think there are any thread convertable plants that are cookable - when 'thread' - df.world.raws.plants.all.each_with_index do |p, i| - # skip over plants that don't have a threadable part listed - if p.material_defs.idx[:Thread] != -1 - p.material.each_with_index do |m, j| - # only operate here on STRUCTURAL materials, as the rest will be :PLANT_GROWTH, instead of just :PLANT - # which then means that the subtype won't be -1 - if m.id == "STRUCTURAL" and m.flags[:EDIBLE_COOKED] and m.reaction_product.str.include?('THREAD') - ban_cooking[p.name + ' ' + m.id, j + DFHack::MaterialInfo::PLANT_BASE, i, :PLANT, -1] - end - end - # This section handles banning the plant growths that produce thread... not that there are any now, that I'm aware of... - p.growths.each_with_index do |g, r| - m = df.decode_mat(g).material - if m.flags[:EDIBLE_COOKED] and m.reaction_product.str.include?('THREAD') - p.material.each_with_index do |s, j| - if m.id == s.id - ban_cooking[p.name + ' ' + m.id, j + DFHack::MaterialInfo::PLANT_BASE, i, :PLANT_GROWTH, r] - end - end - end - end - end - end - - # Bans fruits that produce alcohol - when 'fruit' - df.world.raws.plants.all.each_with_index do |p, i| - p.growths.each_with_index do |g, r| - # Get the material item from the growth data - m = df.decode_mat(g).material - # ensure that we're only targetting fruits that can be cooked as solids (that's the :LEAF_MAT flag) - # in the kitchen, which can also be brewed into alcohol - if m.id == "FRUIT" and m.flags[:EDIBLE_COOKED] and m.flags[:LEAF_MAT] and m.reaction_product.id.include?('DRINK_MAT') - p.material.each_with_index do |s, j| - if m.id == s.id - ban_cooking[p.name + ' ' + m.id, j + DFHack::MaterialInfo::PLANT_BASE, i, :PLANT_GROWTH, r] - end - end - end - end - end - - # The below function outputs a pipe seperated list of the banned cooking ingredients - # The list isn't intended to be readable from the console, as I used it for validating - # the methods I was using to select, and ban cooking items. Mostly this was for the - # tree fruit items, as the item subtype number wasn't immediately obvious to be used - # as a reference pointer to the growths array. - when 'show' - # First put together a dictionary/hash table - type_list = {} - # cycle through all plants - df.world.raws.plants.all.each_with_index do |p, i| - # The below three if statements initialize the dictionary/hash tables for their respective (cookable) plant/drink/seed - # And yes, this will create and then overwrite an entry when there is no (cookable) plant/drink/seed item for a specific plant, - # but since the -1 type and -1 index can't be added to the ban list, it's inconsequential to check for non-existent (cookable) plant/drink/seed items here - if not type_list[[p.material_defs.type[:BasicMat], p.material_defs.idx[:BasicMat]]] - type_list[[p.material_defs.type[:BasicMat], p.material_defs.idx[:BasicMat]]] = {} - end - if not type_list[[p.material_defs.type[:Drink], p.material_defs.idx[:Drink]]] - type_list[[p.material_defs.type[:Drink], p.material_defs.idx[:Drink]]] = {} - end - if not type_list[[p.material_defs.type[:Seed], p.material_defs.idx[:Seed]]] - type_list[[p.material_defs.type[:Seed], p.material_defs.idx[:Seed]]] = {} - end - type_list[[p.material_defs.type[:BasicMat], p.material_defs.idx[:BasicMat]]]['text'] = p.name + ' basic' - # basic materials for plants always appear to use the :PLANT item type tag - type_list[[p.material_defs.type[:BasicMat], p.material_defs.idx[:BasicMat]]]['type'] = :PLANT - # item subtype of :PLANT types appears to always be -1, as there is no growth array entry for the :PLANT - type_list[[p.material_defs.type[:BasicMat], p.material_defs.idx[:BasicMat]]]['subtype'] = -1 - type_list[[p.material_defs.type[:Drink], p.material_defs.idx[:Drink]]]['text'] = p.name + ' drink' - # drink materials for plants always appear to use the :DRINK item type tag - type_list[[p.material_defs.type[:Drink], p.material_defs.idx[:Drink]]]['type'] = :DRINK - # item subtype of :DRINK types appears to always be -1, as there is no growth array entry for the :DRINK - type_list[[p.material_defs.type[:Drink], p.material_defs.idx[:Drink]]]['subtype'] = -1 - type_list[[p.material_defs.type[:Seed], p.material_defs.idx[:Seed]]]['text'] = p.name + ' seed' - # seed materials for plants always appear to use the :SEEDS item type tag - type_list[[p.material_defs.type[:Seed], p.material_defs.idx[:Seed]]]['type'] = :SEEDS - # item subtype of :SEEDS types appears to always be -1, as there is no growth array entry for the :SEEDS - type_list[[p.material_defs.type[:Seed], p.material_defs.idx[:Seed]]]['subtype'] = -1 - p.growths.each_with_index do |g, r| - m = df.decode_mat(g).material - # Search only growths that are cookable (:EDIBLE_COOKED), and listed as :LEAF_MAT, - # as that appears to be the tag required to allow cooking as a solid/non-liquid item in the kitchen - if m.flags[:EDIBLE_COOKED] and m.flags[:LEAF_MAT] - # Sift through the materials array to find the matching entry for our growths array entry - p.material.each_with_index do |s, j| - if m.id == s.id - if not type_list[[j + DFHack::MaterialInfo::PLANT_BASE, i]] - type_list[[j + DFHack::MaterialInfo::PLANT_BASE, i]] = {} - end - type_list[[j + DFHack::MaterialInfo::PLANT_BASE, i]]['text'] = p.name + ' ' + m.id + ' growth' - # item type for plant materials listed in the growths array appear to always use the :PLANT_GROWTH item type tag - type_list[[j + DFHack::MaterialInfo::PLANT_BASE, i]]['type'] = :PLANT_GROWTH - # item subtype is equal to the array index of the cookable item in the growths table - type_list[[j + DFHack::MaterialInfo::PLANT_BASE, i]]['subtype'] = r - end - end - end - end - end - # cycle through all creatures - df.world.raws.creatures.all.each_with_index do |c, i| - c.material.each_with_index do |m, j| - if m.reaction_product and m.reaction_product.id and m.reaction_product.id.include?('CHEESE_MAT') - if not type_list[[j + DFHack::MaterialInfo::CREATURE_BASE, i]] - type_list[[j + DFHack::MaterialInfo::CREATURE_BASE, i]] = {} - end - type_list[[j + DFHack::MaterialInfo::CREATURE_BASE, i]]['text'] = c.name[0] + ' milk' - # item type for milk appears to use the :LIQUID_MISC tag - type_list[[j + DFHack::MaterialInfo::CREATURE_BASE, i]]['type'] = :LIQUID_MISC - type_list[[j + DFHack::MaterialInfo::CREATURE_BASE, i]]['subtype'] = -1 - end - if m.reaction_product and m.reaction_product.id and m.reaction_product.id.include?('SOAP_MAT') - if not type_list[[j + DFHack::MaterialInfo::CREATURE_BASE, i]] - type_list[[j + DFHack::MaterialInfo::CREATURE_BASE, i]] = {} - end - type_list[[j + DFHack::MaterialInfo::CREATURE_BASE, i]]['text'] = c.name[0] + ' tallow' - # item type for tallow appears to use the :GLOB tag - type_list[[j + DFHack::MaterialInfo::CREATURE_BASE, i]]['type'] = :GLOB - type_list[[j + DFHack::MaterialInfo::CREATURE_BASE, i]]['subtype'] = -1 - end - end - end - already_banned.each_with_index do |b, i| - # initialize our output string with the array entry position (largely stays the same for each item on successive runs, except when items are added/removed) - output = i.inspect + ': ' - # initialize our key for accessing our stored items info - key = [b[0][0], b[0][1]] - # It shouldn't be possible for there to not be a matching key entry by this point, but we'll be kinda safe here - if type_list[key] - # Add the item name to the first part of the string - output += '|' + type_list[key]['text'] + ' |type ' - if type_list[key]['type'] == b[0][2] - # item type expected vs. actual is a match, so we print that it's a match, as well as the item type - output += 'match: ' + type_list[key]['type'].inspect - else - # Aw crap. The item type we EXpected doesn't match up with the ACtual item type. - output += 'error: ex;' + type_list[key]['type'].inspect + '/ac;' + b[0][2].inspect - end - output += '|subtype ' - if type_list[key]['subtype'] == b[0][3] - # item sub type is a match, so we print that it's a match, as well as the item subtype index number (-1 means there is no subtype for this item) - output += 'match: ' + type_list[key]['subtype'].inspect - else - # Something went wrong, and the EXpected item subtype index value doesn't match the ACtual index value - output += 'error: ex;' + type_list[key]['subtype'].inspect + '/ac;' + b[0][3].inspect - end - else - # There's no entry for this item in our calculated list of cookable items. So, it's not a plant, alcohol, tallow, or milk. It's likely that it's a meat that has been banned. - output += '|"' + key.inspect + ' unknown banned material type (meat?) " ' + '|item type: "' + b[0][2].inspect + '"|item subtype: "' + b[0][3].inspect - end - puts output - end - else - puts "ban-cooking booze - bans cooking of drinks" - puts "ban-cooking honey - bans cooking of honey bee honey" - puts "ban-cooking tallow - bans cooking of tallow" - puts "ban-cooking milk - bans cooking of creature liquids that can be turned into cheese" - puts "ban-cooking oil - bans cooking of oil" - puts "ban-cooking seeds - bans cooking of plants that have farmable seeds and that can be brewed into alcohol (eating raw plants to get seeds is rather slow)" - puts "ban-cooking brew - bans cooking of all plants (fruits too) that can be brewed into alcohol" - puts "ban-cooking fruit - bans cooking of only fruits that can be brewed into alcohol" - puts "ban-cooking mill - bans cooking of plants that can be milled into powder -- should any actually exist" - puts "ban-cooking thread - bans cooking of plants that can be spun into thread -- should any actually exist" - puts "ban-cooking show - list known items that are banned in a pipe seperated format (if you ban meat(s) or fish(es) you'll get unknown listings!)" - end -end diff --git a/bodyswap.lua b/bodyswap.lua index a278a6d848..e64f43f783 100644 --- a/bodyswap.lua +++ b/bodyswap.lua @@ -1,199 +1,144 @@ --- Shifts player control over to another unit in adventure mode. --- author: Atomic Chicken --- based on "assumecontrol.lua" by maxthyme, as well as the defunct advtools plugin "adv-bodyswap" --- calls "modtools/create-unit" for nemesis and histfig creation - --@ module = true local utils = require 'utils' local validArgs = utils.invert({ - 'unit', - 'help' + 'unit', + 'help' }) -local args = utils.processArgs({...}, validArgs) - -local usage = [====[ - -bodyswap -======== -This script allows the player to take direct control of any unit present in -adventure mode whilst giving up control of their current player character. - -To specify the target unit, simply select it in the user interface, -such as by opening the unit's status screen or viewing its description, -and enter "bodyswap" in the DFHack console. - -Alternatively, the target unit can be specified by its unit id as shown below. - -Arguments:: - - -unit id - replace "id" with the unit id of your target - example: - bodyswap -unit 42 - -]====] +local args = utils.processArgs({ ... }, validArgs) if args.help then - print(usage) - return + print(dfhack.script_help()) + return end function setNewAdvNemFlags(nem) - nem.flags.ACTIVE_ADVENTURER = true - nem.flags.RETIRED_ADVENTURER = false - nem.flags.ADVENTURER = true + nem.flags.ACTIVE_ADVENTURER = true + nem.flags.ADVENTURER = true end + function setOldAdvNemFlags(nem) - nem.flags.ACTIVE_ADVENTURER = false + nem.flags.ACTIVE_ADVENTURER = false end function clearNemesisFromLinkedSites(nem) --- omitting this step results in duplication of the unit entry in df.global.world.units.active when the site to which the historical figure is linked is reloaded with said figure present as a member of the player party --- this can be observed as part of the normal recruitment process when the player adds a site-linked historical figure to their party - if not nem.figure then - return - end - for _,link in ipairs(nem.figure.site_links) do - local site = df.world_site.find(link.site) - for i = #site.unk_1.nemesis-1, 0, -1 do - if site.unk_1.nemesis[i] == nem.id then - site.unk_1.nemesis:erase(i) - end + -- omitting this step results in duplication of the unit entry in df.global.world.units.active when the site to which the historical figure is linked is reloaded with said figure present as a member of the player party + -- this can be observed as part of the normal recruitment process when the player adds a site-linked historical figure to their party + if not nem.figure then + return + end + for _, link in ipairs(nem.figure.site_links) do + local site = df.world_site.find(link.site) + utils.erase_sorted(site.populace.nemesis, nem.id) end - end end function createNemesis(unit) - local nemesis = reqscript('modtools/create-unit').createNemesis(unit,unit.civ_id) - nemesis.figure.flags.never_cull = true - return nemesis + local nemesis = unit:create_nemesis(1, 1) + nemesis.figure.flags.never_cull = true + return nemesis end function isPet(nemesis) - if nemesis.unit then - if nemesis.unit.relationship_ids.Pet ~= -1 then - return true + if nemesis.unit then + if nemesis.unit.relationship_ids.PetOwner ~= -1 then + return true + end + elseif nemesis.figure then -- in case the unit is offloaded + for _, link in ipairs(nemesis.figure.histfig_links) do + if link._type == df.histfig_hf_link_pet_ownerst then + return true + end + end end - elseif nemesis.figure then -- in case the unit is offloaded - for _, link in ipairs(nemesis.figure.histfig_links) do - if link._type == df.histfig_hf_link_pet_ownerst then - return true - end - end - end - return false + return false end function processNemesisParty(nemesis, targetUnitID, alreadyProcessed) --- configures the target and any leaders/companions to behave as cohesive adventure mode party members - local alreadyProcessed = alreadyProcessed or {} - alreadyProcessed[tostring(nemesis.id)] = true - - local nemUnit = nemesis.unit - if nemesis.unit_id == targetUnitID then -- the target you're bodyswapping into - df.global.ui_advmode.interactions.party_core_members:insert('#', nemesis.figure.id) - nemUnit.relationship_ids.GroupLeader = -1 - elseif isPet(nemesis) then -- pets belonging to the target or to their companions - df.global.ui_advmode.interactions.party_pets:insert('#', nemesis.figure.id) - else - df.global.ui_advmode.interactions.party_core_members:insert('#', nemesis.figure.id) -- placing all non-pet companions into the core party list to enable tactical mode swapping - nemesis.flags.ADVENTURER = true - if nemUnit then -- check in case the companion is offloaded - nemUnit.relationship_ids.GroupLeader = targetUnitID + -- configures the target and any leaders/companions to behave as cohesive adventure mode party members + local alreadyProcessed = alreadyProcessed or {} + alreadyProcessed[tostring(nemesis.id)] = true + + local nemUnit = nemesis.unit + if nemesis.unit_id == targetUnitID then -- the target you're bodyswapping into + df.global.adventure.interactions.party_core_members:insert('#', nemesis.figure.id) + nemUnit.relationship_ids.GroupLeader = -1 + elseif isPet(nemesis) then -- pets belonging to the target or to their companions + df.global.adventure.interactions.party_pets:insert('#', nemesis.figure.id) + else + df.global.adventure.interactions.party_core_members:insert('#', nemesis.figure.id) -- placing all non-pet companions into the core party list to enable tactical mode swapping + nemesis.flags.ADVENTURER = true + if nemUnit then -- check in case the companion is offloaded + nemUnit.relationship_ids.GroupLeader = targetUnitID + end end - end --- the hierarchy of nemesis-level leader/companion relationships appears to be left untouched when the player character is changed using the inbuilt "tactical mode" party system + -- the hierarchy of nemesis-level leader/companion relationships appears to be left untouched when the player character is changed using the inbuilt "tactical mode" party system - clearNemesisFromLinkedSites(nemesis) + clearNemesisFromLinkedSites(nemesis) - if nemesis.group_leader_id ~= -1 and not alreadyProcessed[tostring(nemesis.group_leader_id)] then - local leader = df.nemesis_record.find(nemesis.group_leader_id) - if leader then - processNemesisParty(leader, targetUnitID, alreadyProcessed) + if nemesis.group_leader_id ~= -1 and not alreadyProcessed[tostring(nemesis.group_leader_id)] then + local leader = df.nemesis_record.find(nemesis.group_leader_id) + if leader then + processNemesisParty(leader, targetUnitID, alreadyProcessed) + end end - end - for _, id in ipairs(nemesis.companions) do - if not alreadyProcessed[tostring(id)] then - local companion = df.nemesis_record.find(id) - if companion then - processNemesisParty(companion, targetUnitID, alreadyProcessed) - end + for _, id in ipairs(nemesis.companions) do + if not alreadyProcessed[tostring(id)] then + local companion = df.nemesis_record.find(id) + if companion then + processNemesisParty(companion, targetUnitID, alreadyProcessed) + end + end end - end end function configureAdvParty(targetNemesis) - local party = df.global.ui_advmode.interactions - party.party_core_members:resize(0) - party.party_pets:resize(0) - party.party_extra_members:resize(0) - processNemesisParty(targetNemesis, targetNemesis.unit_id) + local party = df.global.adventure.interactions + party.party_core_members:resize(0) + party.party_pets:resize(0) + party.party_extra_members:resize(0) + processNemesisParty(targetNemesis, targetNemesis.unit_id) end function swapAdvUnit(newUnit) + if not newUnit then + qerror('Target unit not specified!') + end - if not newUnit then - qerror('Target unit not specified!') - end - - local oldNem = df.nemesis_record.find(df.global.ui_advmode.player_id) - local oldUnit = oldNem.unit - if newUnit == oldUnit then - return - end - - local activeUnits = df.global.world.units.active - local oldUnitIndex - if activeUnits[0] == oldUnit then - oldUnitIndex = 0 - else -- unlikely; this is just in case - for i,u in ipairs(activeUnits) do - if u == oldUnit then - oldUnitIndex = i - break - end + local oldNem = df.nemesis_record.find(df.global.adventure.player_id) + local oldUnit = oldNem.unit + if newUnit == oldUnit then + return end - end - local newUnitIndex - for i,u in ipairs(activeUnits) do - if u == newUnit then - newUnitIndex = i - break + + local newNem = dfhack.units.getNemesis(newUnit) or createNemesis(newUnit) + if not newNem then + qerror("Failed to obtain target nemesis!") end - end - - if not newUnitIndex then - qerror("Target unit index not found!") - end - - local newNem = dfhack.units.getNemesis(newUnit) or createNemesis(newUnit) - if not newNem then - qerror("Failed to obtain target nemesis!") - end - - setOldAdvNemFlags(oldNem) - setNewAdvNemFlags(newNem) - configureAdvParty(newNem) - df.global.ui_advmode.player_id = newNem.id - activeUnits[newUnitIndex] = oldUnit - activeUnits[oldUnitIndex] = newUnit - oldUnit.idle_area:assign(oldUnit.pos) + + setOldAdvNemFlags(oldNem) + setNewAdvNemFlags(newNem) + configureAdvParty(newNem) + df.global.adventure.player_id = newNem.id + df.global.world.units.adv_unit = newUnit + oldUnit.idle_area:assign(oldUnit.pos) + + dfhack.gui.revealInDwarfmodeMap(xyz2pos(dfhack.units.getPosition(newUnit)), true) end if not dfhack_flags.module then - if df.global.gamemode ~= df.game_mode.ADVENTURE then - qerror("This script can only be used in adventure mode!") - end - - local unit = args.unit and df.unit.find(tonumber(args.unit)) or dfhack.gui.getSelectedUnit() - if not unit then - print("Enter the following if you require assistance: bodyswap -help") - if args.unit then - qerror("Invalid unit id: "..args.unit) - else - qerror("Target unit not specified!") + if df.global.gamemode ~= df.game_mode.ADVENTURE then + qerror("This script can only be used in adventure mode!") + end + + local unit = args.unit and df.unit.find(tonumber(args.unit)) or dfhack.gui.getSelectedUnit() + if not unit then + print("Enter the following if you require assistance: help bodyswap") + if args.unit then + qerror("Invalid unit id: " .. args.unit) + else + qerror("Target unit not specified!") + end end - end - swapAdvUnit(unit) + swapAdvUnit(unit) end diff --git a/brainwash.lua b/brainwash.lua index 12776de283..1cbdc29355 100644 --- a/brainwash.lua +++ b/brainwash.lua @@ -27,7 +27,7 @@ function brainwash_unit(profile) return end - unit_name=dfhack.TranslateName(dfhack.units.getVisibleName(unit)) + unit_name = dfhack.df2console(dfhack.units.getReadableName(unit)) print("Previous personality values for "..unit_name) printall(unit.status.current_soul.personality.traits) diff --git a/break-dance.lua b/break-dance.lua index 3a992510ba..0d4de1c61c 100644 --- a/break-dance.lua +++ b/break-dance.lua @@ -9,7 +9,7 @@ can't find a partner. ]====] local unit if dfhack.world.isAdventureMode() then - unit = df.global.world.units.active[0] + unit = dfhack.world.getAdventurer() else unit = dfhack.gui.getSelectedUnit(true) or qerror('No unit selected') end diff --git a/build-now.lua b/build-now.lua index d2407f9ed4..91e362aeac 100644 --- a/build-now.lua +++ b/build-now.lua @@ -1,50 +1,15 @@ -- instantly completes unsuspended building construction jobs ---[====[ - -build-now -========= - -Instantly completes unsuspended building construction jobs. By default, all -buildings on the map are completed, but the area of effect is configurable. - -Note that no units will get architecture experience for any buildings that -require that skill to construct. - -Usage:: - - build-now [ []] [] - -Where the optional ```` pair can be used to specify the coordinate bounds -within which ``build-now`` will operate. If they are not specified, -``build-now`` will scan the entire map. If only one ```` is specified, only -the building at that coordinate is built. - -The ```` parameters can either be an ``,,`` triple (e.g. -``35,12,150``) or the string ``here``, which means the position of the active -game cursor. - -Examples: - -``build-now`` - Completes all unsuspended construction jobs on the map. - -``build-now here`` - Builds the unsuspended, unconstructed building under the cursor. - -Options: - -:``-h``, ``--help``: - Show help text. -:``-q``, ``--quiet``: - Suppress informational output (error messages are still printed). -]====] local argparse = require('argparse') -local buildingplan = require('plugins.buildingplan') -local dig_now = require('plugins.dig-now') local gui = require('gui') +local suspendmanager = require('plugins.suspendmanager') local utils = require('utils') +local ok, buildingplan = pcall(require, 'plugins.buildingplan') +if not ok then + buildingplan = nil +end + local function min_to_max(...) local args = {...} table.sort(args, function(a, b) return a < b end) @@ -56,6 +21,7 @@ local function parse_commandline(args) local positionals = argparse.processArgsGetopt(args, { {'h', 'help', handler=function() opts.help = true end}, {'q', 'quiet', handler=function() opts.quiet = true end}, + {'z', 'zlevel', handler=function() opts.zlevel = true end}, }) if positionals[1] == 'help' then opts.help = true end @@ -77,6 +43,10 @@ local function parse_commandline(args) local x, y, z = dfhack.maps.getTileSize() opts['end'] = xyz2pos(x-1, y-1, z-1) end + if opts.zlevel then + opts.start.z = df.global.window_z + opts['end'].z = df.global.window_z + end return opts end @@ -96,7 +66,7 @@ local function get_jobs(opts) -- job_items are not items, they're filters that describe the kinds of -- items that need to be attached. - for _,job_item in ipairs(job.job_items) do + for _,job_item in ipairs(job.job_items.elements) do -- we have to check for quantity != 0 instead of just the existence -- of the job_item since buildingplan leaves 0-quantity job_items in -- place to protect against persistence errors. @@ -113,7 +83,7 @@ local function get_jobs(opts) goto continue end - -- accept building if if any part is within the processing area + -- accept building if any part is within the processing area if bld.z < opts.start.z or bld.z > opts['end'].z or bld.x2 < opts.start.x or bld.x1 > opts['end'].x or bld.y2 < opts.start.y or bld.y1 > opts['end'].y then @@ -130,7 +100,7 @@ local function get_jobs(opts) :format(num_suspended, num_suspended ~= 1 and 's' or '')) end if num_incomplete > 0 then - print(('Skipped %d building%s with missing items') + print(('Skipped %d building%s with pending items') :format(num_incomplete, num_incomplete ~= 1 and 's' or '')) end if num_clipped > 0 then @@ -205,11 +175,8 @@ local function is_good_dump_pos(pos) local shape_attrs = df.tiletype_shape.attrs[attrs.shape] -- reject hidden tiles if flags.hidden then return false, false end - -- reject unwalkable or open tiles + -- reject unwalkable tiles if not shape_attrs.walkable then return false, false end - if shape_attrs.basic_shape == df.tiletype_shape_basic.Open then - return false, false - end -- reject footprints within other buildings. this could potentially be -- relaxed a bit since we can technically dump items on passable tiles -- within other buildings, but that would look messy. @@ -264,10 +231,8 @@ local function get_dump_pos(bld) if dump_pos then return dump_pos end - for _,unit in ipairs(df.global.world.units.active) do - if dfhack.units.isCitizen(unit) then - return unit.pos - end + for _,unit in ipairs(dfhack.units.getCitizens(true)) do + return unit.pos end -- fall back to position of first active unit return df.global.world.units.active[0].pos @@ -295,42 +260,6 @@ local function get_items(job) return items end --- disconnect item from the workshop that it is cluttering, if any -local function disconnect_clutter(item) - local bld = dfhack.items.getHolderBuilding(item) - if not bld then return true end - -- remove from contained items list, fail if not found - local found = false - for i,contained_item in ipairs(bld.contained_items) do - if contained_item.item == item then - bld.contained_items:erase(i) - found = true - break - end - end - if not found then - dfhack.printerr('failed to find clutter item in expected building') - return false - end - -- remove building ref from item and move item into containing map block - -- we do this manually instead of calling dfhack.items.moveToGround() - -- because that function will cowardly refuse to work with items with - -- BUILDING_HOLDER references (because it could crash the game). However, - -- we know that this particular setup is safe to work with. - for i,ref in ipairs(item.general_refs) do - if ref:getType() == df.general_ref_type.BUILDING_HOLDER then - item.general_refs:erase(i) - -- this call can return failure, but it always succeeds in setting - -- the required item flags and adding the item to the map block, - -- which is all we care about here. dfhack.items.moveToBuilding() - -- will fix things up later. - item:moveToGround(item.pos.x, item.pos.y, item.pos.z) - return true - end - end - return false -end - -- teleport any items that are not already part of the building to the building -- center and mark them as part of the building. this handles both partially- -- built buildings and items that are being carried to the building correctly. @@ -338,9 +267,6 @@ local function attach_items(bld, items) for _,item in ipairs(items) do -- skip items that have already been brought to the building if item.flags.in_building then goto continue end - -- ensure we have no more holder building references so moveToBuilding - -- can succeed - if not disconnect_clutter(item) then return false end -- 2 means "make part of bld" (which causes constructions to crash on -- deconstruct) local use = bld:getType() == df.building_type.Construction and 0 or 2 @@ -350,139 +276,6 @@ local function attach_items(bld, items) return true end --- from observation of vectors sorted by the DF, pos sorting seems to be by x, --- then by y, then by z -local function pos_cmp(a, b) - local xcmp = utils.compare(a.x, b.x) - if xcmp ~= 0 then return xcmp end - local ycmp = utils.compare(a.y, b.y) - if ycmp ~= 0 then return ycmp end - return utils.compare(a.z, b.z) -end - -local function get_original_tiletype(pos) - -- TODO: this is not always exactly the existing tile type. for example, - -- tracks are ignored - return dfhack.maps.getTileType(pos) -end - -local function reuse_construction(construction, item) - construction.item_type = item:getType() - construction.item_subtype = item:getSubtype() - construction.mat_type = item:getMaterial() - construction.mat_index = item:getMaterialIndex() - construction.flags.top_of_wall = false - construction.flags.no_build_item = true -end - -local function create_and_link_construction(pos, item, top_of_wall) - local construction = df.construction:new() - utils.assign(construction.pos, pos) - construction.item_type = item:getType() - construction.item_subtype = item:getSubtype() - construction.mat_type = item:getMaterial() - construction.mat_index = item:getMaterialIndex() - construction.flags.top_of_wall = top_of_wall - construction.flags.no_build_item = not top_of_wall - construction.original_tile = get_original_tiletype(pos) - utils.insert_sorted(df.global.world.constructions, construction, - 'pos', pos_cmp) -end - --- maps construction_type to the resulting tiletype -local const_to_tile = { - [df.construction_type.Fortification] = df.tiletype.ConstructedFortification, - [df.construction_type.Wall] = df.tiletype.ConstructedPillar, - [df.construction_type.Floor] = df.tiletype.ConstructedFloor, - [df.construction_type.UpStair] = df.tiletype.ConstructedStairU, - [df.construction_type.DownStair] = df.tiletype.ConstructedStairD, - [df.construction_type.UpDownStair] = df.tiletype.ConstructedStairUD, - [df.construction_type.Ramp] = df.tiletype.ConstructedRamp, -} --- fill in all the track mappings, which have nice consistent naming conventions -for i,v in ipairs(df.construction_type) do - if type(v) ~= 'string' then goto continue end - local _, _, base, dir = v:find('^(TrackR?a?m?p?)([NSEW]+)') - if base == 'Track' then - const_to_tile[i] = df.tiletype['ConstructedFloorTrack'..dir] - elseif base == 'TrackRamp' then - const_to_tile[i] = df.tiletype['ConstructedRampTrack'..dir] - end - ::continue:: -end - -local function set_tiletype(pos, tt) - local block = dfhack.maps.ensureTileBlock(pos) - block.tiletype[pos.x%16][pos.y%16] = tt - if tt == df.tiletype.ConstructedPillar then - block.designation[pos.x%16][pos.y%16].outside = 0 - end - -- all tiles below this one are now "inside" - for z = pos.z-1,0,-1 do - block = dfhack.maps.ensureTileBlock(pos.x, pos.y, z) - if not block or block.designation[pos.x%16][pos.y%16].outside == 0 then - return - end - block.designation[pos.x%16][pos.y%16].outside = 0 - end -end - -local function adjust_tile_above(pos_above, item, construction_type) - if not dfhack.maps.ensureTileBlock(pos_above) then return end - local tt_above = dfhack.maps.getTileType(pos_above) - local shape_above = df.tiletype.attrs[tt_above].shape - if shape_above ~= df.tiletype_shape.EMPTY - and shape_above == df.tiletype_shape.RAMP_TOP then - return - end - if construction_type == df.construction_type.Wall then - create_and_link_construction(pos_above, item, true) - set_tiletype(pos_above, df.tiletype.ConstructedFloor) - elseif df.construction_type[construction_type]:find('Ramp') then - set_tiletype(pos_above, df.tiletype.RampTop) - end -end - --- add new construction to the world list and manage tiletype conversion -local function build_construction(bld) - -- remember required metadata and get rid of building used for designation - local item = bld.contained_items[0].item - local pos = copyall(item.pos) - local construction_type = bld.type - dfhack.buildings.deconstruct(bld) - - -- check if we're building on a construction (i.e. building a construction on top of a wall) - local tiletype = dfhack.maps.getTileType(pos) - local tileattrs = df.tiletype.attrs[tiletype] - if tileattrs.material == df.tiletype_material.CONSTRUCTION then - -- modify the construction to the new type - local construction, found = utils.binsearch(df.global.world.constructions, pos, 'pos', pos_cmp) - if not found then - error('Could not find construction entry for construction tile at ' .. pos.x .. ', ' .. pos.y .. ', ' .. pos.z) - end - reuse_construction(construction, item) - else - -- add entry to df.global.world.constructions - create_and_link_construction(pos, item, false) - end - -- adjust tiletypes for the construction itself - set_tiletype(pos, const_to_tile[construction_type]) - if construction_type == df.construction_type.Wall then - dig_now.link_adjacent_smooth_walls(pos) - end - - -- for walls and ramps with empty space above, adjust the tile above - if construction_type == df.construction_type.Wall - or df.construction_type[construction_type]:find('Ramp') then - adjust_tile_above(xyz2pos(pos.x, pos.y, pos.z+1), item, - construction_type) - end - - -- a duplicate item will get created on deconstruction due to the - -- no_build_item flag set in create_and_link_construction; destroy this item - dfhack.items.remove(item) -end - -- complete architecture, if required, and perform the adjustments the game -- normally does when a building is built. this logic is reverse engineered from -- observing game behavior and may be incomplete. @@ -491,23 +284,12 @@ local function build_building(bld) -- unlike "natural" builds, we don't set the architect or builder unit -- id. however, this doesn't seem to have any in-game effect. local design = bld.design - design.flags.designed = true design.flags.built = true design.hitpoints = 80640 design.max_hitpoints = 80640 end bld:setBuildStage(bld:getMaxBuildStage()) - bld.flags.exists = true - -- update occupancy flags - for x = bld.x1,bld.x2 do - for y = bld.y1,bld.y2 do - bld:updateOccupancy(x, y) - end - end - -- doors link to adjacent smooth walls - if bld:getType() == df.building_type.Door then - dig_now.link_adjacent_smooth_walls(bld.centerx, bld.centery, bld.z) - end + dfhack.buildings.completeBuild(bld) end local function throw(bld, msg) @@ -523,7 +305,13 @@ if opts.help then print(dfhack.script_help()) return end -- ensure buildingplan is up to date so we don't skip buildings just because -- buildingplan hasn't scanned them yet -buildingplan.doCycle() +if buildingplan then + buildingplan.doCycle() +end + +if suspendmanager.isEnabled() then + dfhack.run_command('unsuspend') +end local num_jobs = 0 for _,job in ipairs(get_jobs(opts)) do @@ -532,6 +320,14 @@ for _,job in ipairs(get_jobs(opts)) do -- retrieve the items attached to the job before we destroy the references local items = get_items(job) + local bld_type = bld:getType() + if #items == 0 and bld_type ~= df.building_type.RoadDirt + and bld_type ~= df.building_type.FarmPlot then + print(('skipping building with no items attached at'.. + ' (%d, %d, %d)'):format(bld.centerx, bld.centery, bld.z)) + goto continue + end + -- skip jobs whose attached items are already owned by the target building -- but are not already part of the building. They are actively being used to -- construct the building and we can't safely change the building's state. @@ -555,9 +351,7 @@ for _,job in ipairs(get_jobs(opts)) do goto continue end - -- remove job data and clean up ref links. we do this first because - -- dfhack.items.moveToBuilding() refuses to work with items that already - -- hold references to buildings. + -- remove job data and attach items to building. if not dfhack.job.removeJob(job) then throw(bld, 'failed to remove job; job state may be inconsistent') end @@ -567,11 +361,7 @@ for _,job in ipairs(get_jobs(opts)) do 'failed to attach items to building; state may be inconsistent') end - if bld:getType() == df.building_type.Construction then - build_construction(bld) - else - build_building(bld) - end + build_building(bld) num_jobs = num_jobs + 1 ::continue:: diff --git a/burial.lua b/burial.lua index 3f75c50b46..d6ce884014 100644 --- a/burial.lua +++ b/burial.lua @@ -1,27 +1,31 @@ --- allows burial in unowned coffins --- by Putnam https://gist.github.com/Putnam3145/e7031588f4d9b24b9dda ---[====[ +-- Allows burial in unowned coffins. +-- Based on Putnam's work (https://gist.github.com/Putnam3145/e7031588f4d9b24b9dda) -burial -====== -Sets all unowned coffins to allow burial. ``burial -pets`` also allows burial -of pets. +local argparse = require('argparse') +local quickfort = reqscript('quickfort') -]====] - -local utils=require('utils') - -local validArgs = utils.invert({ - 'pets' +local cur_zlevel, citizens, pets = false, true, true +argparse.processArgsGetopt({...}, { + {'z', 'cur-zlevel', handler=function() cur_zlevel = true end}, + {'c', 'citizens-only', handler=function() pets = false end}, + {'p', 'pets-only', handler=function() citizens = false end}, }) +local tomb_blueprint = { + mode = 'zone', + pos = nil, + -- Don't pass properties with default values to avoid 'unhandled property' warning + data = ('T{%s %s}'):format(citizens and '' or 'citizens=false', pets and 'pets=true' or ''), +} -local args = utils.processArgs({...}, validArgs) - -for k,v in ipairs(df.global.world.buildings.other.COFFIN) do --as:df.building_coffinst - if v.owner_id==-1 then - v.burial_mode.allow_burial=true - if not args.pets then - v.burial_mode.no_pets=true - end +local tomb_count = 0 +for _, coffin in pairs(df.global.world.buildings.other.COFFIN) do + if #coffin.relations > 0 or cur_zlevel and coffin.z ~= df.global.window_z then + goto skip end + tomb_blueprint.pos = xyz2pos(coffin.x1, coffin.y1, coffin.z) + quickfort.apply_blueprint(tomb_blueprint) + tomb_count = tomb_count + 1 + ::skip:: end + +print(('Created %s tomb(s).'):format(tomb_count)) diff --git a/cannibalism.lua b/cannibalism.lua index bf5fd407d0..00ca47b1f3 100644 --- a/cannibalism.lua +++ b/cannibalism.lua @@ -1,13 +1,3 @@ ---Allows consumption of sapient corpses. ---[====[ - -cannibalism -=========== -Allows consumption of sapient corpses. Use from an adventurer's inventory screen -or an individual item's detail screen. - -]====] - function unmark_inventory(inventory) for _, entry in ipairs(inventory) do entry.item.flags.dead_dwarf = false @@ -19,8 +9,8 @@ if df.viewscreen_itemst:is_instance(scrn) then scrn.item.flags.dead_dwarf = false --hint:df.viewscreen_itemst elseif df.viewscreen_dungeon_monsterstatusst:is_instance(scrn) then unmark_inventory(scrn.inventory) --hint:df.viewscreen_dungeon_monsterstatusst -elseif df.global.ui_advmode.menu == df.ui_advmode_menu.Inventory then - unmark_inventory(df.global.world.units.active[0].inventory) +elseif df.global.adventure.menu == df.ui_advmode_menu.Inventory then + unmark_inventory(dfhack.world.getAdventurer().inventory) else qerror('Unsupported context') end diff --git a/caravan.lua b/caravan.lua index c429cf7bbd..0dd2cf6d89 100644 --- a/caravan.lua +++ b/caravan.lua @@ -1,31 +1,30 @@ --- Adjusts properties of caravans ---[====[ - -caravan -======= - -Adjusts properties of caravans on the map. See also `force` to create caravans. - -This script has multiple subcommands. Commands listed with the argument -``[IDS]`` can take multiple caravan IDs (see ``caravan list``). If no IDs are -specified, then the commands apply to all caravans on the map. - -**Subcommands:** - -- ``list``: lists IDs and information about all caravans on the map. -- ``extend [DAYS] [IDS]``: extends the time that caravans stay at the depot by - the specified number of days (defaults to 7 if not specified). Also causes - caravans to return to the depot if applicable. -- ``happy [IDS]``: makes caravans willing to trade again (after seizing goods, - annoying merchants, etc.). Also causes caravans to return to the depot if - applicable. -- ``leave [IDS]``: makes caravans pack up and leave immediately. -- ``unload``: fixes endless unloading at the depot. Run this if merchant pack - animals were startled and now refuse to come to the trade depot. +-- Adjusts properties of caravans and provides overlays for enhanced trading +--@ module = true -]====] +local movegoods = reqscript('internal/caravan/movegoods') +local pedestal = reqscript('internal/caravan/pedestal') +local trade = reqscript('internal/caravan/trade') +local tradeagreement = reqscript('internal/caravan/tradeagreement') + +dfhack.onStateChange.caravanTradeOverlay = function(code) + if code == SC_WORLD_UNLOADED then + trade.trader_selected_state = {} + trade.broker_selected_state = {} + trade.handle_ctrl_click_on_render = false + trade.handle_shift_click_on_render = false + end +end ---@ module = true +OVERLAY_WIDGETS = { + trade=trade.TradeOverlay, + tradebanner=trade.TradeBannerOverlay, + tradeethics=trade.TradeEthicsWarningOverlay, + tradeagreement=tradeagreement.TradeAgreementOverlay, + movegoods=movegoods.MoveGoodsOverlay, + movegoods_hider=movegoods.MoveGoodsHiderOverlay, + assigntrade=movegoods.AssignTradeOverlay, + displayitemselector=pedestal.PedestalOverlay, +} INTERESTING_FLAGS = { casualty = 'Casualty', @@ -33,7 +32,7 @@ INTERESTING_FLAGS = { seized = 'Goods seized', offended = 'Offended' } -local caravans = df.global.ui.caravans +local caravans = df.global.plotinfo.caravans local function caravans_from_ids(ids) if not ids or #ids == 0 then @@ -42,7 +41,7 @@ local function caravans_from_ids(ids) local c = {} --as:df.caravan_state[] for _,id in ipairs(ids) do - local id = tonumber(id) + id = tonumber(id) if id then c[id] = caravans[id] end @@ -63,9 +62,9 @@ function commands.list() print(dfhack.df2console(('%d: %s caravan from %s'):format( id, df.creature_raw.find(df.historical_entity.find(car.entity).race).name[2], -- adjective - dfhack.TranslateName(df.historical_entity.find(car.entity).name) + dfhack.translation.translateName(df.historical_entity.find(car.entity).name) ))) - print(' ' .. (df.caravan_state.T_trade_state[car.trade_state] or 'Unknown state: ' .. car.trade_state)) + print(' ' .. (df.caravan_state.T_trade_state[car.trade_state] or ('Unknown state: ' .. car.trade_state))) print((' %d day(s) remaining'):format(math.floor(car.time_remaining / 120))) for flag, msg in pairs(INTERESTING_FLAGS) do if car.flags[flag] then @@ -95,6 +94,26 @@ function commands.leave(...) for id, car in pairs(caravans_from_ids{...}) do car.trade_state = df.caravan_state.T_trade_state.Leaving end + local still_needs_broker = false + for _,car in ipairs(caravans) do + if car.trade_state == df.caravan_state.T_trade_state.Approaching or + car.trade_state == df.caravan_state.T_trade_state.AtDepot + then + still_needs_broker = true + break + end + end + if not still_needs_broker then + for _,depot in ipairs(df.global.world.buildings.other.TRADE_DEPOT) do + depot.trade_flags.trader_requested = false + for _, job in ipairs(depot.jobs) do + if job.job_type == df.job_type.TradeAtDepot then + dfhack.job.removeJob(job) + break + end + end + end + end end local function isDisconnectedPackAnimal(unit) @@ -107,24 +126,15 @@ local function isDisconnectedPackAnimal(unit) end end -local function getPrintableUnitName(unit) - local visible_name = dfhack.units.getVisibleName(unit) - local profession_name = dfhack.units.getProfessionName(unit) - if visible_name.has_name then - return ('%s (%s)'):format(dfhack.TranslateName(visible_name), profession_name) - end - return profession_name -- for unnamed animals -end - local function rejoin_pack_animals() print('Reconnecting disconnected pack animals...') local found = false - for _, unit in pairs(df.global.world.units.active) do + for _, unit in ipairs(df.global.world.units.active) do if unit.flags1.merchant and isDisconnectedPackAnimal(unit) then local dragger = unit.following print((' %s <-> %s'):format( - dfhack.df2console(getPrintableUnitName(unit)), - dfhack.df2console(getPrintableUnitName(dragger)) + dfhack.df2console(dfhack.units.getReadableName(unit)), + dfhack.df2console(dfhack.units.getReadableName(dragger)) )) unit.relationship_ids[ df.unit_relationship_type.Dragger ] = dragger.id dragger.relationship_ids[ df.unit_relationship_type.Draggee ] = unit.id @@ -138,7 +148,7 @@ local function rejoin_pack_animals() end end -function commands.unload(...) +function commands.unload() rejoin_pack_animals() end @@ -146,21 +156,17 @@ function commands.help() print(dfhack.script_help()) end -function main(...) - local args = {...} - local command = table.remove(args, 1) +function main(args) + local command = table.remove(args, 1) or 'list' if commands[command] then commands[command](table.unpack(args)) else commands.help() - if command then - qerror("No such subcommand: " .. command) - else - qerror("Missing subcommand") - end + print() + qerror("No such command: " .. command) end end if not dfhack_flags.module then - main(...) + main{...} end diff --git a/catsplosion.lua b/catsplosion.lua index d6c03825bb..367798fd94 100644 --- a/catsplosion.lua +++ b/catsplosion.lua @@ -1,21 +1,3 @@ --- Make cats just /multiply/. ---[====[ - -catsplosion -=========== -Makes cats (and other animals) just *multiply*. It is not a good idea to run this -more than once or twice. - -Usage: - -:catsplosion: Make all cats pregnant -:catsplosion list: List IDs of all animals on the map -:catsplosion ID ...: Make animals with given ID(s) pregnant - -Animals will give birth within two in-game hours (100 ticks or fewer). - -]====] - local world = df.global.world if not dfhack.isWorldLoaded() then @@ -45,11 +27,19 @@ local total_created = 0 local males = {} --as:df.unit[][] local females = {} --as:df.unit[][] -for _, unit in pairs(world.units.all) do +for _, unit in pairs(world.units.active) do + if not dfhack.units.isActive(unit) or + dfhack.units.isDead(unit) or + dfhack.units.isBaby(unit) or + dfhack.units.isChild(unit) + then + goto continue + end local id = world.raws.creatures.all[unit.race].creature_id males[id] = males[id] or {} females[id] = females[id] or {} table.insert((dfhack.units.isFemale(unit) and females or males)[id], unit) + ::continue:: end if list_only then @@ -67,8 +57,9 @@ if list_only then end for id in pairs(creatures) do - total = total + #(females[id] or {}) - for _, female in pairs(females[id]) do + local female_list = females[id] or {} + total = total + #female_list + for _, female in pairs(female_list) do if female.pregnancy_timer ~= 0 then female.pregnancy_timer = math.random(1, 100) total_changed = total_changed + 1 diff --git a/changelog.txt b/changelog.txt index 9524188ccb..cf07906e0c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,7 +1,7 @@ ===[[[ This file contains changes specific to the scripts repo. See docs/changelog.txt in the dfhack repo for a full description, or -https://docs.dfhack.org/en/latest/docs/Documentation.html#building-the-changelogs +https://docs.dfhack.org/en/latest/docs/dev/Documentation.html#building-the-changelogs NOTE: currently, gen_changelog.py expects a "Future" section to exist at the top of this file (even if no changes are listed under it), or you will get a @@ -9,383 +9,1304 @@ top of this file (even if no changes are listed under it), or you will get a changelogs when making a new release, docs/changelog.txt in the dfhack repo must have the new release listed in the right place, even if no changes were made in that repo. + +Template for new versions: + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Removed + ]]] # Future -## New Scripts -- `gui/kitchen-info`: adds more info to the Kitchen screen -- `gui/workorder-details`: adjusts work orders' input item, material, traits -- `warn-stealers`: warn when creatures that may steal your food, drinks, or items become visible +## New Tools + +## New Features ## Fixes -- `devel/query`: fixed error when --tile is specified -- `gui/unit-info-viewer`: fix logic for displaying undead creature names -- `gui/workflow`: restore functionality to the add/remove/order hotkeys on the workflow status screen -- `emigration`: fix emigrant logic so unhappy dwarves leave as designed -- `gui/gm-unit`: allow ``+`` and ``-`` to adjust skill values as intended instead of letting the filter intercept the characters -- `dwarf-op`: fixed error when applying the Miner job to dwarves +- `fix/loyaltycascade`: guard against citizens that are not historical figures and emit a warning. +- `gui/siegemanager`: fix nil index if there are no siege engines on the map ## Misc Improvements -- `devel/query`: inform the user when a query has been truncated due to ``--maxlength`` being hit. -- `devel/query`: increased default maxlength value from 257 to 2048 -- `gui/gm-unit`: don't clear the list filter when you adjust a skill value -- `gui/quickfort`: better formatting for the generated manager orders report -- `gui/quickfort`: display an error message when the blueprints directory cannot be found -- `quickfort`: library blueprints are now included by default in ``quickfort list`` output. Use the new ``--useronly`` (or just ``-u``) option to filter out library bluerpints. -- `quickfort`: better error message when the blueprints directory cannot be found -- `dwarf-op`: replaces [ a b c ] option lists with a,b,c option lists +- `caravan`: the ``Bring goods to depot``, ``Trade``, and ``Assign items for display`` overlays now allow searching for items with non-ASCII characters in their description +- `caravan`: the ``Trade`` overlay will use the trader's appraisal skill instead of the broker's to round/obfuscate the value of items ## Removed -- `fix/build-location`: The corresponding DF bug (5991) was fixed in DF 0.40.05 -- `fix/diplomats`: DF bug 3295 fixed in 0.40.05 -- `fix/fat-dwarves`: DF bug 5971 fixed in 0.40.05 -- `fix/feeding-timers`: DF bug 2606 is fixed in 0.40.12 -- `fix/merchants`: DF bug that prevents humans from making trade agreements has been fixed -- `gui/assign-rack`: No longer useful in current DF versions -- `gui/hack-wish`: Replaced by `gui/create-item` -- `gui/no-dfhack-init`: No longer useful since players don't have to create their own ``dfhack.init`` files anymore -# 0.47.05-r6 +# 53.15-r2 -## New Scripts +## New Tools + +## New Features + +## Fixes +- `combine`: stopgap fix for incorrectly combining dyes, no longer affects dyes +- `gui/rename`: fix script error that sometimes caused the script to malfunction when started from the Launcher. + +## Misc Improvements + +## Removed + +# 53.15-r1 + +## New Tools +- `machine-toggle`: interface for toggling gear assemblies, as well as pressure plates (previously in `trackstop`) (available only if ``armok`` tools are shown) + +## New Features + +## Fixes +- `add-recipe`: fix to include incorrectly excluded recipes +- `gui/control-panel`: fixed incorrect description of deteriorate commands +- `gui/petitions`: fix deity display + +## Misc Improvements +- `trackstop`: ``pressureplate`` overlay moved to `machine-toggle` as an ``armok`` tool + +## Removed + +# 53.14-r2 + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Removed +- ``gui/logcleaner``: Removed + +# 53.14-r1 + +## New Tools + +## New Features + +## Fixes +- `caravan`: fix ethics warning for wooden weapons +- `gui/control-panel`: fixed inconsistent space in command help ("tweak" -> "tweak ", like with other commands) +- `gui/siegemanager`: consistently set the initial filter to "All" +- `immortal-cravings`: also take care of immortal units with alcohol dependence + +## Misc Improvements + +## Removed + +# 53.11-r2 + +## New Tools + +## New Features -- `assign-minecarts`: automatically assign minecarts to hauling routes that don't have one -- `deteriorate`: combines, replaces, and extends previous `deteriorateclothes`, `deterioratecorpses`, and `deterioratefood` scripts. -- `gui/petitions`: shows petitions. now you can see which guildhall/temple you agreed to build! -- `gui/quantum`: point-and-click tool for creating quantum stockpiles -- `gui/quickfort`: shows blueprint previews on the live map so you can apply them interactively -- `modtools/fire-rate`: allows modders to adjust the rate of fire for ranged attacks +## Fixes +- `gui/rename`: skip ``NONE`` when iterating through language name options +- `quickfort`: work orders will no longer be created with a repetition frequency of ``NONE`` + +## Misc Improvements + +## Removed + +# 53.10-r2 + +## New Tools +- `gui/logcleaner`: graphical overlay for configuring the logcleaner plugin with enable and filter toggles. + +## New Features +- `trackstop`: can now modify pressure plates; permits minecart and creature triggers to be set beyond normal sensitivity + +## Fixes +- `gui/rename`: added check for entity_id input in get_target function +- `prioritize`: Fix the overlay appearing where it should not when following a unit + +## Misc Improvements +- `gui/notify`: reduced severity of the missing nemesis records warning if no units on the map are affected. clarified wording. + +## Removed + +# 53.10-r1 + +## New Tools + +## New Features +- `gui/quickcmd`: added custom command names and option to display command output +- `gui/notify`: new notification type: missing nemesis records; displays a warning message about game corruption. + +## Fixes + +## Misc Improvements + +## Removed + +# 53.07-r1 + +## New Tools +- `fix/codex-pages`: add pages to written content that have unspecified page counts. +- `gui/keybinds`: gui for managing and saving custom keybindings + +## New Features ## Fixes -- `build-now`: walls built above other walls can now be deconstructed like regularly-built walls -- `gui/dfstatus`: no longer count items owned by traders -- `gui/unit-info-viewer`: fix calculation/labeling of unit size +- `empty-bin`: renamed ``--liquids`` parameter to ``--force`` and made emptying of containers (bags) with powders contingent on that parameter. Previously powders would just always get disposed. ## Misc Improvements -- `build-now`: buildings that were just designated with `buildingplan` are now built immediately (as long as there are items available to build the buildings with) instead of being skipped until buildingplan gets around to doing its regular scan -- `caravan`: new ``unload`` command, fixes endless unloading at the depot by reconnecting merchant pack animals that were disconnected from their owners -- `deteriorate`: new ``now`` command immediately deteriorates items of the specified types -- `list-agreements`: now displays translated guild names, worshipped deities, petition age, and race-appropriate professions (e.g. "Craftsdwarf" instead of "Craftsman") -- `workorder`: a manager is no longer required for orders to be created (matching bevavior in the game itself) +- `combine`: try harder to find the currently-selected stockpile ## Removed -- `devel/unforbidall`: please use `unforbid` instead. You can silence the output with ``unforbid all --quiet`` -- `deteriorateclothes`: please use ``deteriorate --types=clothes`` instead -- `deterioratecorpses`: please use ``deteriorate --types=corpses`` instead -- `deterioratefood`: please use ``deteriorate --types=food`` instead -# 0.47.05-r5 +# 53.06-r1 + +## New Tools + +## New Features + +## Fixes +- `gui/design`: designating a single-level stair construction now properly follows the selected stair type. +- `gui/design`: adjusted conflicting keybinds, diagonal line reverse becoming ``R`` and bottom stair type becoming ``g``. +- `modtools/set-personality`: use correct caste trait ranges; fixes `gui/gm-unit` being unable to correctly randomize traits or set traits to caste average + +## Misc Improvements + +## Removed + +# 53.04-r1 + +## New Tools +- `gui/siegemanager`: manage your siege engines at a glance. + +## New Features +- `item`: new ``--total-quality`` option for use in conjunction with ``--min-quality`` or ``--max-quality`` to filter items according to their total quality + +## Fixes + +## Misc Improvements +- `gui/design`: can now construct reinforced walls +- `quickfort`: support for reinforced walls and bolt throwers + +## Removed + +# 53.01-r1 + +## New Tools +- `fix/symbol-unstick`: unstick noble symbols that cannot be re-designated. +- `resize-armor`: resize armor or clothing item to any creature size. + +## New Features + +## Fixes +- `autotraining`: squads once used for training then disabled now properly are treated as disabled. + +## Misc Improvements + +## Removed +- `fix/archery-practice`: removed from the control panel's bug fixes tab. + +# 52.05-r2 + +## New Tools + +## New Features + +## Fixes +- `fix/archery-practice`: now splits instead of combining ammo items in quivers, and moves quivers to end of unit's inventory list + +## Misc Improvements + +## Removed + +# 52.05-r1 + +## New Tools +- `fix/archery-practice`: combine ammo items in units' quivers to fix 'Soldier (no item)' issue +- `gui/adv-finder`: UI for tracking historical figures and artifacts in adventure mode +- `store-owned`: task owned items to be stored in the owner's room furniture + +## New Features + +## Fixes +- `ban-cooking`: bans honey added by creatures other than vanilla honey bee +- `uniform-unstick`: added quivers, backpacks, and flasks/waterskins to uniform analysis +- `uniform-unstick`: the ``--drop`` option now only evaluates clothing as possible items to drop +- `uniform-unstick`: the ``--free`` option no longer redundantly reports an improperly assigned item when that item is removed from a uniform +- `uniform-unstick`: the ``--drop`` and ``--free`` options now only drop items which are actually in a unit's inventory +- `uniform-unstick`: the ``--all`` and ``--drop`` options, when used together, now print the separator line between each unit's report in the proper place + +## Misc Improvements +- adapt Lua tools to use new API functionality for creating and assigning jobs +- `idle-crafting`: properly interrupt interruptible (i.e. "green") social activities + +## Removed + +# 52.03-r2 + +## New Tools +- `autotraining`: new tool to assign citizens to a military squad when they need Martial Training +- `gui/autotraining`: configuration tool for autotraining +- `entomb`: allow any unit that has a corpse or body parts to be assigned a tomb zone +- `husbandry`: Automatically milk and shear animals at nearby farmer's workshops + +## New Features +- `deathcause`: added functionality to this script to fetch cause of death programatically + +## Fixes +- `ban-cooking`: will not fail trying to ban honey if the world has no honey +- `confirm`: only show pause option for pausable confirmations +- `confirm`: when editing a uniform, confirm discard of changes when exiting with Escape +- `confirm`: when removing a manager order, show correct order description when using non-100% interface setting +- `confirm`: when removing a manager order, show correct order description after prior order removal or window resize (when scrolled to bottom of order list) +- `confirm`: when removing a manager order, show specific item/job type for ammo, shield, helm, gloves, shoes, trap component, and meal orders +- `confirm`: the pause option now pauses individual confirmation types, allowing multiple different confirmations to be paused independently +- `immortal-cravings`: prioritize high-value meals, properly split of portions, and don't go eating or drinking on a full stomach +- `uniform-unstick`: no longer causes units to equip multiples of assigned items +- `caravan`: in the pedestal item assignment dialog, add new items at the end of the list of displayed items instead of at a random position +- `caravan`: in the pedestal item assignment dialog, consistently remove items from the list of displayed items + +## Misc Improvements +- `devel/hello-world`: updated to show off the new Slider widget + +## Removed + +# 52.03-r1 + +## New Tools + +## New Features + +## Fixes +- `make-legendary`: ``make-legendary all`` will no longer corrupt souls + +## Misc Improvements + +## Removed + +# 52.02-r2 + +## New Tools + +## New Features +- `gui/mod-manager`: now supports arena mode + +## Fixes +- `gui/mod-manager`: gracefully handle vanilla mods with different versions from the user's preset +- `gui/mod-manager`: hide other versions of loaded mods and unhides them when unloaded + +## Misc Improvements + +## Removed + +# 52.02-r1 + +## New Tools + +## New Features + +## Fixes +- ``embark-anyone``: validate viewscreen before using, avoids a crash + +## Misc Improvements + +## Removed + +# 52.01-r1 + +## New Tools + +## New Features + +## Fixes +- fixed references to removed ``unit.curse`` compound +- `gui/gm-unit`: remove reference to ``think_counter``, removed in v51.12 +- `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed +- `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files +- `uniform-unstick`: resolve overlap with new buttons in 51.13 + +## Misc Improvements + +## Removed + +# 51.12-r1 + +## New Tools +- `deteriorate`: (reinstated) allow corpses, body parts, food, and/or damaged clothes to rot away +- `modtools/moddable-gods`: (reinstated) create new deities from scratch + +## New Features +- `gui/spectate`: added "Prefer nicknamed" to the list of options +- `gui/mod-manager`: when run in a loaded world, shows a list of active mods -- click to export the list to the clipboard for easy sharing or posting +- `gui/blueprint`: now records zone designations +- `gui/design`: add option to draw N-point stars, hollow or filled or inverted, and change the main axis to orient in any direction + +## Fixes +- `starvingdead`: properly restore to correct enabled state when loading a new game that is different from the first game loaded in this session +- `starvingdead`: ensure undead decay does not happen faster than the declared decay rate when saving and loading the game +- `gui/design`: prevent line thickness from extending outside the map boundary + +## Misc Improvements +- `remove-stress`: also applied to long-term stress, immediately removing stressed and haggard statuses + +# 51.11-r1 + +## Fixes +- `list-agreements`: fix date math when determining petition age +- `gui/petitions`: fix date math when determining petition age +- `gui/rename`: fix commandline processing when manually specifying target ids +- `gui/sandbox`: restore metal equipment options when spawning units + +## Misc Improvements +- `fix/loyaltycascade`: now also breaks up brawls and other intra-fort conflicts that *look* like loyalty cascades +- `makeown`: remove selected unit from any current conflicts so they don't just start attacking other citizens when you make them a citizen of your fort + +# 51.09-r1 + +## New Features +- `gui/mass-remove`: add a button to the bottom toolbar when eraser mode is active for launching `gui/mass-remove` +- `idle-crafting`: default to only considering happy and ecstatic units for the highest need threshold +- `gui/sitemap`: add a button to the toolbar at the bottom left corner of the screen for launching `gui/sitemap` + +## Fixes +- `idle-crafting`: check that units still have crafting needs before creating a job for them +- `gui/journal`: prevent pause/unpause events from leaking through the UI when keys are mashed + +# 51.07-r1 + +## New Tools +- `devel/export-map`: export map tile data to a JSON file +- `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk +- `gui/spectate`: interactive UI for configuring `spectate` +- `gui/notes`: UI for adding and managing notes attached to tiles on the map +- `launch`: (reinstated) new adventurer fighting move: thrash your enemies with a flying suplex +- `putontable`: (reinstated) make an item appear on a table + +## New Features +- `advtools`: ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys +- `gui/journal`: now working in adventure mode -- journal is per-adventurer, so if you unretire an adventurer, you get the same journal +- `emigration`: ``nobles`` command for sending freeloader barons back to the sites that they rule over +- `toggle-kbd-cursor`: support adventure mode (Alt-k keybinding now toggles Look mode) + +## Fixes +- `hfs-pit`: use correct wall types when making pits with walls +- `gui/liquids`: don't add liquids to wall tiles +- `gui/liquids`: using the remove tool with magma selected will no longer create unexpected unpathable tiles +- `idle-crafting`: do not assign crafting jobs to nobles holding meetings (avoids dangling jobs) +- `rejuvenate`: update unit portrait and sprite when aging up babies and children +- `rejuvenate`: recalculate labor assignments for unit when aging up babies and children (so they can start accepting jobs) + +## Misc Improvements +- `hide-tutorials`: handle tutorial popups for adventure mode +- `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game (in case you hid them all and now want them back) +- `gui/notify`: moody dwarf notification turns red when they can't reach workshop or items +- `gui/notify`: save reminder now appears in adventure mode +- `gui/notify`: save reminder changes color to yellow at 30 minutes and to orange at 60 minutes +- `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete +- `gui/create-item`: now accepts a ``pos`` argument of where to spawn items +- `modtools/create-item`: exported ``hackWish`` function now supports ``opts.pos`` for determining spawn location +- `hfs-pit`: improve placement of stairs w/r/t eerie pits and ramp tops +- `position`: add adventurer tile position +- `position`: add global site position +- `position`: when a tile is selected, display relevant map block and intra-block offset +- `gui/sitemap`: shift click to start following the selected unit or artifact +- `prioritize`: when prioritizing jobs of a specified type, also output how many of those jobs were already prioritized before you ran the command +- `prioritize`: don't include already-prioritized jobs in the output of ``prioritize -j`` +- `gui/design`: only display vanilla dimensions tooltip if the DFHack dimensions tooltip is disabled +- `devel/query`: support adventure mode +- `devel/tree-info`: support adventure mode +- `hfs-pit`: support adventure mode +- `colonies`: support adventure mode +- `position`: report position of the adventure mode look cursor, if active + +# 51.04-r1.1 + +## Fixes +- `advtools`: fix dfhack-added conversation options not appearing in the ask whereabouts conversation tree +- `gui/rename`: fix error when changing the language of a unit's name + +## Misc Improvements +- `assign-preferences`: new ``--show`` option to display the preferences of the selected unit +- `pref-adjust`: new ``show`` command to display the preferences of the selected unit + +## Removed +- `gui/control-panel`: removed ``craft-age-wear`` tweak for Windows users; the tweak doesn't currently load on Windows + +# 51.02-r1 + +## Fixes +- `deathcause`: fix error when retrieving the name of a historical figure + +# 50.15-r2 + +## New Tools +- `fix/stuck-squad`: allow squads and messengers returning from missions to rescue squads that have gotten stuck on the world map +- `gui/rename`: (reinstated) give new in-game language-based names to anything that can be named (units, governments, fortresses, the world, etc.) + +## New Features +- `gui/settings-manager`: new overlay on the Labor -> Standing Orders tab for configuring the number of barrels to reserve for job use (so you can brew alcohol and not have all your barrels claimed by stockpiles for container storage) +- `gui/settings-manager`: standing orders save/load now includes the reserved barrels setting +- `gui/rename`: add overlay to worldgen screen allowing you to rename the world before the new world is saved +- `gui/rename`: add overlay to the "Prepare carefully" embark screen that transparently fixes a DF bug where you can't give units nicknames or custom professions +- `gui/notify`: new notification type: save reminder; appears if you have gone more than 15 minutes without saving; click to autosave + +## Fixes +- `fix/dry-buckets`: don't empty buckets for wells that are actively in use +- `gui/unit-info-viewer`: skill progress bars now show correct XP thresholds for skills past Legendary+5 +- `caravan`: no longer incorrectly identify wood-based plant items and plant-based soaps as being ethically unsuitable for trading with the elves +- `gui/design`: don't require an extra right click on the first cancel of building area designations +- `gui/gm-unit`: refresh unit sprite when profession is changed + +## Misc Improvements +- `immortal-cravings`: goblins and other naturally non-eating/non-drinking races will now also satisfy their needs for eating and drinking +- `caravan`: add filter for written works in display furniture assignment dialog +- `fix/wildlife`: don't vaporize stuck wildlife that is onscreen -- kill them instead (as if they died from old age) +- `gui/sitemap`: show primary group affiliation for visitors and invaders (e.g. civilization name or performance troupe) + +# 50.14-r2 + +## New Tools +- `fix/wildlife`: prevent wildlife from getting stuck when trying to exit the map. This fix needs to be enabled manually in `gui/control-panel` on the Bug Fixes tab since not all players want this bug to be fixed (you can intentionally stall wildlife incursions by trapping wildlife in an enclosed area so they are not caged but still cannot escape). +- `immortal-cravings`: allow immortals to satisfy their cravings for food and drink +- `justice`: pardon a criminal's prison sentence + +## New Features +- `force`: add support for a ``Wildlife`` event to allow additional wildlife to enter the map + +## Fixes +- `gui/quickfort`: only print a help blueprint's text once even if the repeat setting is enabled +- `makeown`: quell any active enemy or conflict relationships with converted creatures +- `makeown`: halt any hostile jobs the unit may be engaged in, like kidnapping +- `fix/loyaltycascade`: allow the fix to work on non-dwarven citizens +- `control-panel`: fix error when setting numeric preferences from the commandline +- `gui/quickfort`: fix build mode evaluation rules to allow placement of furniture and constructions on tiles with stair shapes or without orthagonal floors +- `emigration`: save-and-reload no longer resets the emigration cycle timeout +- `geld`, `ungeld`: save-and-reload no longer loses changes done by `geld` and `ungeld` for units who are historical figures +- `rejuvenate`: fix error when specifying ``--age`` parameter +- `gui/notify`: don't classify (peacefully) visiting night creatures as hostile +- `exportlegends`: ensure historical figure race filter is usable after re-entering legends mode with a different loaded world + +## Misc Improvements +- `idle-crafting`: also support making shell crafts for workshops with linked input stockpiles +- `gui/gm-editor`: automatically resolve and display names for ``language_name`` fields +- `fix/stuck-worship`: reduced console output by default. Added ``--verbose`` and ``--quiet`` options. +- `gui/design`: add dimensions tooltip to vanilla zone painting interface +- `necronomicon`: new ``--world`` option to list all secret-containing items in the entire world +- `gui/design`: new ``gui/design.rightclick`` overlay that allows you to cancel out of partially drawn box and minecart designations without canceling completely out of drawing mode + +## Removed +- `modtools/force`: merged into `force` + +# 50.13-r5 + +## New Tools +- `embark-anyone`: allows you to embark as any civilization, including dead and non-dwarven civs +- `idle-crafting`: allow dwarves to independently satisfy their need to craft objects +- `gui/family-affairs`: (reinstated) inspect or meddle with pregnancies, marriages, or lover relationships +- `notes`: attach notes to locations on a fort map + +## New Features +- `caravan`: DFHack dialogs for trade screens (both ``Bring goods to depot`` and the ``Trade`` barter screen) can now filter by item origins (foreign vs. fort-made) and can filter bins by whether they have a mix of ethically acceptable and unacceptable items in them +- `caravan`: If you have managed to select an item that is ethically unacceptable to the merchant, an "Ethics warning" badge will now appear next to the "Trade" button. Clicking on the badge will show you which items that you have selected are problematic. The dialog has a button that you can click to deselect the problematic items in the trade list. +- `confirm`: If you have ethically unacceptable items selected for trade, the "Are you sure you want to trade" confirmation will warn you about them +- `quickfort`: ``#zone`` blueprints now integrated with `preserve-rooms` so you can create a zone and automatically assign it to a noble or administrative role +- `exportlegends`: option to filter by race on historical figures page + +## Fixes +- `timestream`: ensure child growth events (that is, a child's transition to adulthood) are not skipped; existing "overage" children will be automatically fixed within a year +- `empty-bin`: ``--liquids`` option now correctly empties containers filled with LIQUID_MISC (like lye) +- `gui/design`: don't overcount "affected tiles" for Line & Freeform drawing tools +- `deep-embark`: fix error when embarking where there is no land to stand on (e.g. when embarking in the ocean with `gui/embark-anywhere`) +- `deep-embark`: fix failure to transport units and items when embarking where there is no room to spawn the starting wagon +- `gui/create-item`, `modtools/create-item`: items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing" and will now stack correctly +- `rejuvenate`: don't set a lifespan limit for creatures that are immortal (e.g. elves, goblins) +- `rejuvenate`: properly disconnect babies from mothers when aging babies up to adults + +## Misc Improvements +- `gui/sitemap`: show whether a unit is friendly, hostile, or wild +- `gui/sitemap`: show whether a unit is caged +- `gui/control-panel`: include option for turning off dumping of old clothes for `tailor`, for players who have magma pit dumps and want to save old clothes from being dumped into the magma +- `position`: report current historical era (e.g., "Age of Myth"), site/adventurer world coords, and mouse map tile coords +- `position`: option to copy keyboard cursor position to the clipboard +- `assign-minecarts`: reassign vehicles to routes where the vehicle has been destroyed (or has otherwise gone missing) +- `fix/dry-buckets`: prompt DF to recheck requests for aid (e.g. "bring water" jobs) when a bucket is unclogged and becomes available for use +- `exterminate`: show descriptive names for the listed races in addition to their IDs +- `exterminate`: show actual names for unique creatures such as forgotten beasts and titans +- `fix/ownership`: now also checks and fixes room ownership links + +## Documentation +- `gui/embark-anywhere`: add information about how the game determines world tile pathability and instructions for bridging two landmasses + +# 50.13-r4 + +## New Features +- `gui/journal`: new automatic table of contents. add lines that start with "# ", like "# Entry for 502-04-02", to add hyperlinked headers to the table of contents + +## Fixes +- `full-heal`: fix ``-r --all_citizens`` option combination not resurrecting citizens +- `open-legends`: don't intercept text bound for vanilla legends mode search widgets +- `gui/unit-info-viewer`: correctly display skill levels when rust is involved +- `timestream`: fix dwarves spending too long eating and drinking +- `timestream`: fix jobs not being created at a sufficient rate, leading to dwarves standing around doing nothing +- `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 restoring of saved settings on Windows +- `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 areas +- `gui/autodump`: properly turn items into projectiles when they are teleported into mid-air + +## 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 (e.g. ``list-waves 0`` to identify your starting 7 dwarves) +- `allneeds`: display distribution of needs by how severely they are affecting the dwarf + +# 50.13-r3 + +## New Tools +- `advtools`: collection of useful commands and overlays for adventure mode +- `advtools`: added an overlay that automatically fixes corrupt throwing/shooting state, preventing save/load crashes +- `advtools`: advtools party - promotes one of your companions to become a controllable adventurer +- `advtools`: advtools pets - fixes pets you gift in adventure mode. +- `pop-control`: (reinstated) limit the maximum size of migrant waves +- `bodyswap`: (reinstated) take control of another unit in adventure mode +- `gui/sitemap`: list and zoom to people, locations, and artifacts +- `devel/tree-info`: print a technical visualization of tree data +- `gui/tiletypes`: interface for modifying map tiles and tile properties +- `fix/population-cap`: fixes the situation where you continue to get migrant waves even when you are above your configured population cap +- `fix/occupancy`: fixes issues where you can't build somewhere because the game tells you an item/unit/building is in the way but there's nothing there +- `fix/sleepers`: (reinstated) fixes sleeping units belonging to a camp that never wake up. +- `timestream`: (reinstated) keep the game running quickly even when there are large numbers of units on the map +- `gui/journal`: fort journal with a multi-line text editor +- `devel/luacov`: (reinstated) add Lua script coverage reporting for use in testing and performance analysis + +## New Features +- `buildingplan`: dimension tooltip is now displayed for constructions and buildings that are designated over an area, like bridges and farm plots +- `gui/notify`: new notification type: injured citizens; click to zoom to injured units; also displays a warning if your hospital is not functional (or if you have no hospital) +- `gui/notify`: new notification type: drowning and suffocation progress bars for adventure mode +- `prioritize`: new info panel on under-construction buildings showing if the construction job has been taken and by whom. click to zoom to builder; toggle high priority status for job if it's not yet taken and you need it to be built ASAP +- `gui/unit-info-viewer`: new overlay for displaying progress bars for skills on the unit info sheet +- `gui/pathable`: new "Depot" mode that shows whether wagons can path to your trade depot +- `advtools`: automatically add a conversation option to "ask whereabouts of" for all your relationships (before, you could only ask whereabouts of people involved in rumors) +- `gui/design`: all-new visually-driven UI for much improved usability + +## Fixes +- `assign-profile`: fix handling of ``unit`` option for setting target unit id +- `gui/gm-unit`: correctly display skill levels above Legendary+5 +- `gui/gm-unit`: fix errors when editing/randomizing colors and body appearance +- `quickfort`: fix incorrect handling of stockpiles that are split into multiple separate areas but are given the same label (indicating that they should be part of the same stockpile) +- `makeown`: set animals to tame and domesticated +- `gui/sandbox`: spawned citizens can now be useful military squad members +- `gui/sandbox`: spawned undead now have a purple shade (only after save and reload, though) +- `caravan`: fix errors in trade dialog if all fort items are traded away while the trade dialog is showing fort items and the `confirm` trade confirmation is shown +- `control-panel`: restore non-default values of per-save enabled/disabled settings for repeat-based commands +- `confirm`: fix confirmation prompt behavior when overwriting a hotkey zoom location +- `quickfort`: allow farm plots to be built on muddy stone (as per vanilla behavior) +- `suspend`: remove broken ``--onlyblocking`` option; restore functionality to ``suspend all`` +- `gui/create-item`: allow creation of adamantine thread, wool, and yarn +- `gui/notify`: the notification panel no longer responds to the Enter key so Enter key is passed through to the vanilla UI +- `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" +- `gui/launcher`: "space space to toggle pause" behavior is skipped if the game was paused when `gui/launcher` came up to prevent accidental unpausing +- `gui/unit-info-viewer`: add precise unit size in cc (cubic centimeters) for comparison against the wiki values. you can set your preferred number format for large numbers like this in the preferences of `control-panel` or `gui/control-panel` +- `gui/unit-info-viewer`: now displays a unit's weight relative to a similarly-sized well-known creature (dwarves, elephants, or cats) +- `gui/unit-info-viewer`: shows a unit's size compared to the average for the unit's race +- `caravan`: optional overlay to hide vanilla "bring trade goods to depot" button (if you prefer to always use the DFHack version and don't want to accidentally click on the vanilla button). enable ``caravan.movegoods_hider`` in `gui/control-panel` UI Overlays tab to use. +- `caravan`: bring goods to depot screen now shows (approximate) distance from item to depot +- `gui/design`: circles are more circular (now matches more pleasing shape generated by ``digcircle``) +- `gui/quickfort`: you can now delete your blueprints from the blueprint load dialog +- `caravan`: remember filter settings for pedestal item assignment dialog +- `quickfort`: new ``delete`` command for deleting player-owned blueprints (library and mod-added blueprints cannot be deleted) +- `quickfort`: support enabling `logistics` features for autoforbid and autoclaim on stockpiles +- `gui/quickfort`: allow farm plots, dirt roads, and paved roads to be designated around partial obstructions without calling it an error, matching vanilla behavior +- `gui/launcher`: refresh default tag filter when mortal mode is toggled in `gui/control-panel` so changes to which tools autocomplete take effect immediately +- `gui/civ-alert`: you can now register multiple burrows as civilian alert safe spaces +- `exterminate`: add ``all`` target for convenient scorched earth tactics +- `empty-bin`: select a stockpile, tile, or building to empty all containers in the stockpile, tile, or building +- `exterminate`: add ``--limit`` option to limit number of exterminated creatures +- `exterminate`: add ``knockout`` and ``traumatize`` method for non-lethal incapacitation +- `caravan`: add shortcut to the trade request screen for selecting item types by value (e.g. so you can quickly select expensive gems or cheap leather) +- `gui/notify`: notification panel extended to apply to adventure mode +- `gui/control-panel`: highlight preferences that have been changed from the defaults +- `gui/quickfort`: buildings can now be constructed in a "high priority" state, giving them first dibs on `buildingplan` materials and setting their construction jobs to the highest priority +- `prioritize`: add ``ButcherAnimal`` to the default prioritization list (``SlaughterAnimal`` was already there, but ``ButcherAnimal`` -- which is different -- was missing) +- `prioritize`: list both unclaimed and total counts for current jobs when the --jobs option is specified +- `prioritize`: boost performance of script by not tracking number of times a job type was prioritized +- `gui/unit-syndromes`: make werecreature syndromes easier to search for + +## Removed +- `max-wave`: merged into `pop-control` +- `devel/find-offsets`, `devel/find-twbt`, `devel/prepare-save`: remove development scripts that are no longer useful +- `fix/item-occupancy`, `fix/tile-occupancy`: merged into `fix/occupancy` +- `adv-fix-sleepers`: renamed to `fix/sleepers` +- `adv-rumors`: merged into `advtools` + +# 50.13-r2 + +## New Tools +- Updated for adventure mode: `gui/sandbox`, `gui/create-item`, `gui/reveal` +- `ghostly`: (reinstated) allow your adventurer to phase through walls +- `markdown`: (reinstated) export description of selected unit or item to a text file +- `adaptation`: (reinstated) inspect or set unit cave adaptation levels +- `fix/engravings`: fix corrupt engraving tiles +- `unretire-anyone`: (reinstated) choose anybody in the world as an adventurer +- `reveal-adv-map`: (reinstated) reveal (or hide) the adventure map +- `resurrect-adv`: (reinstated) allow your adventurer to recover from death +- `flashstep`: (reinstated) teleport your adventurer to the mouse cursor + +## New Features +- `instruments`: new subcommand ``instruments order`` for creating instrument work orders + +## Fixes +- `modtools/create-item`: now functions properly when the ``reaction-gloves`` tweak is active +- `quickfort`: don't designate multiple tiles of the same tree for chopping when applying a tree chopping blueprint to a multi-tile tree +- `gui/quantum`: fix processing when creating a quantum dump instead of a quantum stockpile +- `caravan`: don't include undiscovered divine artifacts in the goods list +- `quickfort`: fix detection of valid tiles for wells +- `combine`: respect container volume limits + +## Misc Improvements +- `gui/autobutcher`: add shortcuts for butchering/unbutchering all animals +- `combine`: reduce combined drink sizes to 25 +- `gui/launcher`: add button for copying output to the system clipboard +- `deathcause`: automatically find and choose a corpse when a pile of mixed items is selected +- `gui/quantum`: add option for whether a minecart automatically gets ordered and/or attached +- `gui/quantum`: when attaching a minecart, show which minecart was attached +- `gui/quantum`: allow multiple feeder stockpiles to be linked to the minecart route +- `prioritize`: add PutItemOnDisplay jobs to the default prioritization list -- when these kinds of jobs are requested by the player, they generally want them done ASAP + +# 50.13-r1.1 + +## Fixes +- `gui/quantum`: accept all item types in the output stockpile as intended +- `deathcause`: fix error on run + +# 50.13-r1 + +## New Tools +- `gui/unit-info-viewer`: (reinstated) give detailed information on a unit, such as egg laying behavior, body size, birth date, age, and information about their afterlife behavior (if a ghost) +- `gui/quantum`: (reinstated) point and click interface for creating quantum stockpiles or quantum dumps + +## Fixes +- `open-legends`: don't interfere with the dragging of vanilla list scrollbars +- `gui/create-item`: properly restrict bags to bag materials by default +- `gui/create-item`: allow gloves and shoes to be made out of textiles by default +- `exterminate`: don't classify dangerous non-invader units as friendly (e.g. snatchers) + +## Misc Improvements +- `open-legends`: allow player to cancel the "DF will now exit" dialog and continue browsing +- `gui/gm-unit`: changes to unit appearance will now immediately be reflected in the unit portrait + +# 50.12-r3 + +## New Tools +- `gui/aquifer`: interactive aquifer visualization and editing +- `open-legends`: (reinstated) open legends mode directly from a loaded fort + +## New Features +- `quickfort`: add options for setting warm/damp dig markers when applying blueprints +- `gui/quickfort`: add options for setting warm/damp dig markers when applying blueprints +- `gui/reveal`: new "aquifer only" mode to only see hidden aquifers but not reveal any tiles +- `gui/notify`: optional notification for general wildlife (not on by default) + +## Fixes +- `fix/loyaltycascade`: fix edge case where loyalties of renegade units were not being fixed +- `quickfort`: reject tiles for building that contain magma or deep water +- `armoks-blessing`: fix error when making "Normal" attributes legendary +- `emigration`: remove units from burrows when they emigrate +- `agitation-rebalance`: fix calculated percent chance of cavern invasion +- `gui/launcher`: don't pop up a result dialog if a command run from minimal mode has no output + +## Misc Improvements +- `gui/reveal`: show aquifers even when not in mining mode +- `gui/control-panel`: add alternate "nodump" version for `cleanowned` that does not cause citizens to toss their old clothes in the dump. this is useful for players who would rather sell old clothes than incinerate them +- `agitation-rebalance`: when more than the maximum allowed cavern invaders are trying to enter the map, prefer keeping the animal people invaders instead of their war animals + +## Removed +- `drain-aquifer`: replaced by ``aquifer drain --all``; an alias now exists so ``drain-aquifer`` will automatically run the new command + +# 50.12-r2.1 + +## Fixes +- `fix/noexert-exhaustion`: fix typo in control panel registry entry which prevented the fix from being run when enabled +- `gui/suspendmanager`: fix script startup errors +- `control-panel`: properly auto-enable newly added bugfixes + +## Misc Improvements +- `gui/unit-syndromes`: make syndromes searchable by their display names (e.g. "necromancer") + +# 50.12-r2 + +## New Tools +- `agitation-rebalance`: alter mechanics of irritation-related attacks so they are less constant and are more responsive to recent player behavior +- `fix/ownership`: fix instances of multiple citizens claiming the same items, resulting in "Store owned item" job loops +- `fix/stuck-worship`: fix prayer so units don't get stuck in uninterruptible "Worship!" states +- `instruments`: provides information on how to craft the instruments used by the player civilization +- `modtools/item-trigger`: (reinstated) modder's resource for triggering scripted content when specific items are used +- `modtools/if-entity`: (reinstated) modder's resource for triggering scripted content depending on the race of the loaded fort +- `devel/block-borders`: (reinstated) highlights boundaries of map blocks or embark tile blocks +- `fix/noexert-exhaustion`: fix "Tired" NOEXERT units. Enabling via `gui/control-panel` prevents NOEXERT units from getting stuck in a "Tired" state + +## New Features +- `gui/settings-manager`: add import, export, and autoload for work details +- `exterminate`: new "disintegrate" kill method that additionally destroys carried items +- `quickfort`: allow setting of workshop profile properties (e.g. labor, skill restrictions) from build blueprints + +## Fixes +- `gui/launcher`: fix history scanning (Up/Down arrow keys) being slow to respond when in minimal mode +- `control-panel`: fix filtering not filtering when running the ``list`` command +- `gui/notify`: don't zoom to forbidden depots for merchants ready to trade notification +- `catsplosion`: only cause pregnancies in adults + +## Misc Improvements +- `gui/launcher`: add interface for browsing and filtering commands by tags +- `gui/launcher`: add support for history search (Alt-s hotkey) when in minimal mode +- `gui/launcher`: add support for the ``clear`` command and clearing the scrollback buffer +- `control-panel`: enable tweaks quietly on fort load so we don't spam the console +- `devel/tile-browser`: simplify interface now that SDL automatically normalizes texture scale +- `exterminate`: make race name matching case and space insensitive +- `gui/gm-editor`: support opening engraved art for inspection +- `gui/notify`: Shift click or Shift Enter on a zoomable notification to zoom to previous target +- `allneeds`: select a dwarf in the UI to see a summary of needs for just that dwarf +- `allneeds`: provide options for sorting the cumulative needs by different criteria +- `prioritize`: print out custom reaction and hauling jobs in the same format that is used for ``prioritize`` command arguments so the player can just copy and paste + +# 50.12-r1 + +## Fixes +- `gui/notify`: persist notification settings when toggled in the UI + +## Misc Improvements +- `gui/launcher`: developer mode hotkey restored to Ctrl-D + +# 50.11-r7 + +## New Tools +- `undump-buildings`: (reinstated) remove dump designation from in-use building materials +- `gui/petitions`: (reinstated) show outstanding (or all historical) petition agreements for guildhalls and temples +- `gui/notify`: display important notifications that vanilla doesn't support yet and provide quick zoom links to notification targets. +- `list-waves`: (reinstated) show migration wave information +- `make-legendary`: (reinstated) make a dwarf legendary in specified skills +- `combat-harden`: (reinstated) set a dwarf's resistance to being affected by visible corpses +- `add-thought`: (reinstated) add custom thoughts to a dwarf +- `devel/input-monitor`: interactive UI for debugging input issues + +## Fixes +- `gui/design`: clicking the center point when there is a design mark behind it will no longer simultaneously enter both mark dragging and center dragging modes. Now you can click once to move the shape, and click twice to move only the mark behind the center point. +- `fix/retrieve-units`: prevent pulling in duplicate units from offscreen +- `warn-stranded`: when there was at least one truly stuck unit and miners were actively mining, the miners were also confusingly shown in the stuck units list +- `source`: fix issue where removing sources would make some other sources inactive +- `caravan`: display book and scroll titles in the goods and trade dialogs instead of generic scroll descriptions +- `item`: avoid error when scanning items that have no quality rating (like bars and other construction materials) +- `gui/blueprint`: changed hotkey for setting blueprint origin tile so it doesn't conflict with default map movement keys +- `gui/control-panel`: fix error when toggling autostart settings + +## Misc Improvements +- `exportlegends`: make progress increase smoothly over the entire export and increase precision of progress percentage +- `gui/autobutcher`: ask for confirmation before zeroing out targets for all races +- `caravan`: move goods to trade depot dialog now allocates more space for the display of the value of very expensive items +- `extinguish`: allow selecting units/items/buildings in the UI to target them for extinguishing; keyboard cursor is only required for extinguishing map tiles that cannot be selected any other way +- `item`: change syntax so descriptions can be searched for without indicating the ``--description`` option. e.g. it's now ``item count royal`` instead of ``item count --description royal`` +- `item`: add ``--verbose`` option to print each item as it is matched +- `gui/mod-manager`: will automatically unmark the default mod profile from being the default if it fails to load (due to missing or incompatible mods) +- `gui/quickfort`: can now dynamically adjust the dig priority of tiles designated by dig blueprints +- `gui/quickfort`: can now opt to apply dig blueprints in marker mode + +## Removed +- `gui/manager-quantity`: the vanilla UI can now modify manager order quantities after creation +- `gui/create-tree`: replaced by `gui/sandbox` +- `warn-starving`: combined into `gui/notify` +- `warn-stealers`: combined into `gui/notify` + +# 50.11-r6 + +## Fixes +- `makeown`: fix error when adopting units that need a historical figure to be created +- `item`: fix missing item categories when using ``--by-type`` + +# 50.11-r5 + +## New Tools +- `control-panel`: new commandline interface for control panel functions +- `uniform-unstick`: (reinstated) force squad members to drop items that they picked up in the wrong order so they can get everything equipped properly +- `gui/reveal`: temporarily unhide terrain and then automatically hide it again when you're ready to unpause +- `gui/teleport`: mouse-driven interface for selecting and teleporting units +- `gui/biomes`: visualize and inspect biome regions on the map +- `gui/embark-anywhere`: bypass those pesky warnings and embark anywhere you want to +- `item`: perform bulk operations on groups of items. + +## New Features +- `uniform-unstick`: add overlay to the squad equipment screen to show a equipment conflict report and give you a one-click button to (attempt to) fix +- `gui/settings-manager`: save and load embark difficulty settings and standing orders; options for auto-load on new embark + +## Fixes +- `source`: water and magma sources and sinks now persist with fort across saves and loads +- `gui/design`: fix incorrect dimensions being shown when you're placing a stockpile, but a start coordinate hasn't been selected yet +- `warn-stranded`: don't warn for citizens who are only transiently stranded, like those on stepladders gathering plants or digging themselves out of a hole +- `ban-cooking`: fix banning creature alcohols resulting in error +- `confirm`: properly detect clicks on the remove zone button even when the unit selection screen is also open (e.g. the vanilla assign animal to pasture panel) +- `caravan`: ensure items are marked for trade when the move trade goods dialog is closed even when they were selected and then the list filters were changed such that the items were no longer actively shown +- `quickfort`: if a blueprint specifies an up/down stair, but the tile the blueprint is applied to cannot make an up stair (e.g. it has already been dug out), still designate a down stair if possible +- `suspendmanager`: correctly handle building collisions with smoothing designations when the building is on the edge of the map +- `empty-bin`: now correctly sends ammunition in carried quivers to the tile underneath the unit instead of teleporting them to an invalid (or possibly just far away) location + +## Misc Improvements +- `warn-stranded`: center the screen on the unit when you select one in the list +- `gui/control-panel`: reduce frequency for `warn-stranded` check to once every 2 days +- `gui/control-panel`: tools are now organized by type: automation, bugfix, and gameplay +- `confirm`: updated confirmation dialogs to use clickable widgets and draggable windows +- `confirm`: added confirmation prompt for right clicking out of the trade agreement screen (so your trade agreement selections aren't lost) +- `confirm`: added confirmation prompts for irreversible actions on the trade screen +- `confirm`: added confirmation prompt for deleting a uniform +- `confirm`: added confirmation prompt for convicting a criminal +- `confirm`: added confirmation prompt for re-running the embark site finder +- `confirm`: added confirmation prompt for reassigning or clearing zoom hotkeys +- `confirm`: added confirmation prompt for exiting the uniform customization page without saving +- `gui/autobutcher`: interface redesigned to better support mouse control +- `gui/launcher`: now persists the most recent 32KB of command output even if you close it and bring it back up +- `gui/quickcmd`: clickable buttons for command add/remove/edit operations +- `uniform-unstick`: warn if a unit belongs to a squad from a different site (can happen with migrants from previous forts) +- `gui/mass-remove`: can now differentiate planned constructions, stockpiles, and regular buildings +- `gui/mass-remove`: can now remove zones +- `gui/mass-remove`: can now cancel removal for buildings and constructions +- `fix/stuck-instruments`: now handles instruments that are left in the "in job" state but that don't have any actual jobs associated with them +- `gui/launcher`: make autocomplete case insensitive + +# 50.11-r4 + +## New Tools +- `build-now`: (reinstated) instantly complete unsuspended buildings that are ready to be built + +## Fixes +- `combine`: prevent stack sizes from growing beyond quantities that you would normally see in vanilla gameplay +- `gui/design`: Center dragging shapes now track the mouse correctly + +## Misc Improvements +- `caravan`: enable searching within containers in trade screen when in "trade bin with contents" mode + +# 50.11-r3 + +## New Tools +- `sync-windmills`: synchronize or randomize movement of active windmills +- `trackstop`: (reimplemented) integrated overlay for changing track stop and roller settings after construction + +## New Features +- `gui/design`: show selected dimensions next to the mouse cursor when designating with vanilla tools, for example when painting a burrow or designating digging +- `quickfort`: new ``burrow`` blueprint mode for designating or manipulating burrows +- `unforbid`: now ignores worn and tattered items by default (X/XX), use -X to bypass +- `fix/dead-units`: gained ability to scrub dead units from burrow membership lists + +## Fixes +- `gui/unit-syndromes`: show the syndrome names properly in the UI +- `emigration`: fix clearing of work details assigned to units that leave the fort + +## Misc Improvements +- `warn-stranded`: don't warn for units that are temporarily on unwalkable tiles (e.g. as they pass under a waterfall) + +## Removed +- `gui/control-panel`: removed always-on system services from the ``System`` tab: `buildingplan`, `confirm`, `logistics`, and `overlay`. The base services should not be turned off by the player. Individual confirmation prompts can be managed via `gui/confirm`, and overlays (including those for `buildingplan` and `logistics`) are managed on the control panel ``Overlays`` tab. +- `gui/control-panel`: removed `autolabor` from the ``Fort`` and ``Autostart`` tabs. The tool does not function correctly with the new labor types, and is causing confusion. You can still enable `autolabor` from the commandline with ``enable autolabor`` if you understand and accept its limitations. + +# 50.11-r2 + +## New Tools +- `add-recipe`: (reinstated) add reactions to your civ (e.g. for high boots if your civ didn't start with the ability to make high boots) +- `fix/corrupt-jobs`: prevents crashes by automatically removing corrupted jobs +- `burial`: (reinstated) create tomb zones for unzoned coffins + +## New Features +- `burial`: new options to configure automatic burial and limit scope to the current z-level +- `drain-aquifer`: gained ability to drain just above or below a certain z-level +- `drain-aquifer`: new option to drain all layers except for the first N aquifer layers, in case you want some aquifer layers but not too many +- `gui/control-panel`: ``drain-aquifer --top 2`` added as an autostart option + +## New Scripts +- `warn-stranded`: new repeatable maintenance script to check for stranded units, similar to `warn-starving` + +## Fixes +- `suspendmanager`: fix errors when constructing near the map edge +- `gui/sandbox`: fix scrollbar moving double distance on click +- `hide-tutorials`: fix the embark tutorial prompt sometimes not being skipped +- `full-heal`: fix removal of corpse after resurrection +- `toggle-kbd-cursor`: clear the cursor position when disabling, preventing the game from sometimes jumping the viewport around when cursor keys are hit + +## Misc Improvements +- `prioritize`: refuse to automatically prioritize dig and smooth/carve job types since it can break the DF job scheduler; instead, print a suggestion that the player use specialized units and vanilla designation priorities +- `gui/overlay`: filter overlays by current context so there are fewer on the screen at once and you can more easily click on the one you want to reposition +- `quickfort`: now allows constructions to be built on top of constructed floors and ramps, just like vanilla. however, to allow blueprints to be safely reapplied to the same area, for example to fill in buildings whose constructions were canceled due to lost items, floors will not be rebuilt on top of floors and ramps will not be rebuilt on top of ramps +- `gui/gm-editor`: for fields with primitive types, change from click to edit to click to select, double-click to edit. this should help prevent accidental modifications to the data and make hotkeys easier to use (since you have to click on a data item to use a hotkey on it) + +# 50.11-r1 + +## New Tools +- `startdwarf`: (reinstated) set number of starting dwarves + +## New Features +- `startdwarf`: overlay scrollbar so you can scroll through your starting dwarves if they don't all fit on the screen +- A new searchable, sortable, filterable dialog for selecting items for display on pedestals and display cases + +## Fixes +- `suspendmanager`: fixed a bug where floor grates, bars, bridges etc. wouldn't be recognised as walkable, leading to unnecessary suspensions in certain cases. + +## Misc Improvements +- `devel/inspect-screen`: display total grid size for UI and map layers +- `suspendmanager`: now suspends constructions that would cave-in immediately on completion + +# 50.10-r1 + +## Fixes +- 'fix/general-strike: fix issue where too many seeds were getting planted in farm plots + +# 50.09-r4 + +## Misc Improvements +- `autofish`: changed ``--raw`` argument format to allow explicit setting to on or off +- `caravan`: move goods to depot screen can now see/search/trade items inside of barrels and pots +- `gui/launcher`: show tagged tools in the autocomplete list when a tag name is typed + +# 50.09-r3 + +## New Tools +- `devel/scan-vtables`: scan and dump likely vtable addresses (for memory research) +- `hide-interface`: hide the vanilla UI elements for clean screenshots or laid-back fortress observing +- `hide-tutorials`: hide the DF tutorial popups; enable in the System tab of `gui/control-panel` +- `set-orientation`: tinker with romantic inclinations (reinstated from back catalog of tools) + +## New Features +- `exportlegends`: new overlay that integrates with the vanilla "Export XML" button. Now you can generate both the vanilla export and the extended data export with a single click! + +## Fixes +- `suspendmanager`: Fix the overlay enabling/disabling `suspendmanager` unexpectedly +- `caravan`: correct price adjustment values in trade agreement details screen +- `caravan`: apply both import and export trade agreement price adjustments to items being both bought or sold to align with how vanilla DF calculates prices +- `caravan`: cancel any active TradeAtDepot jobs if all caravans are instructed to leave +- `emigration`: fix errors loading forts after dwarves assigned to work details or workshops have emigrated +- `emigration`: fix citizens sometimes "emigrating" to the fortress site +- `suspendmanager`: improve the detection on "T" and "+" shaped high walls +- `starvingdead`: ensure sieges end properly when undead siegers starve +- `fix/retrieve-units`: fix retrieved units sometimes becoming duplicated on the map +- `quickfort`: cancel old dig jobs that point to a tile when a new designation is applied to the tile +- `gui/launcher`, `gui/gm-editor`: recover gracefully when the saved frame position is now offscreen +- `gui/sandbox`: correctly load equipment materials in modded games that categorize non-wood plants as wood + +## Misc Improvements +- `devel/lsmem`: added support for filtering by memory addresses and filenames +- `gui/gm-editor`: hold down shift and right click to exit, regardless of how many substructures deep you are +- `quickfort`: linked stockpiles and workshops can now be specified by ID instead of only by name. this is mostly useful when dynamically generating blueprints and applying them via the `quickfort` API +- `gui/quickfort`: blueprint details screen can now be closed with Ctrl-D (the same hotkey used to open the details) +- `suspendmanager`: display a different color for jobs suspended by suspendmanager +- `caravan`: optionally display items within bins in bring goods to depot screen +- `gui/gm-editor`: display in the title bar whether the editor window is scanning for live updates +- `gui/design`: change "auto commit" hotkey from ``c`` to ``Alt-c`` to avoid conflict with the default keybinding for z-level down +- `gui/liquids`: support removing river sources by converting them into stone floors + +# 50.09-r2 ## New Scripts -- `devel/eventful-client`: useful for testing eventful events +- `caravan`: new trade screen UI replacements for bringing goods to trade depot and trading +- `fix/empty-wheelbarrows`: new script to empty stuck rocks from all wheelbarrows on the map ## Fixes -- `devel/query`: fixed a problem printing parents when the starting path had lua pattern special characters in it -- `devel/query`: fixed a crash when trying to iterate over linked lists -- `gui/advfort`: encrust and stud jobs no longer consume reagents without actually improving the target item -- `quickfort`: contructions and bridges are now properly placed over natural ramps -- `setfps`: keep internal ratio of processing FPS to graphics FPS in sync when updating FPS +- `gui/autodump`: when "include items claimed by jobs" is on, actually cancel the job so the item can be teleported +- `gui/gm-unit`: fix commandline processing when a unit id is specified +- `suspendmanager`: take in account already built blocking buildings +- `suspendmanager`: don't consider tree branches as a suitable access path to a building + +## Misc Improvements +- `gui/unit-syndromes`: make lists searchable +- `suspendmanager`: display the suspension reason when viewing a suspended building +- `quickfort`: blueprint libraries are now moddable -- add a ``blueprints/`` directory to your mod and they'll show up in `quickfort` and `gui/quickfort`! + +# 50.09-r1 ## Misc Improvements -- `autonick`: now displays help instead of modifying dwarf nicknames when run without parameters. use ``autonick all`` to rename all dwarves. -- `autonick`: added ``--quiet`` and ``--help`` options -- `gui/blueprint`: support new `blueprint` options and phases -- `quickfort`: support transformations for blueprints that use expansion syntax -- `quickfort`: adjust direction affinity when transforming buildings (e.g. bridges that open to the north now open to the south when rotated 180 degrees) -- `quickfort`: automatically adjust cursor movements on the map screen in ``#query`` and ``#config`` modes when the blueprint is transformed. e.g. ``{Up}`` will be played back as ``{Right}`` when the blueprint is rotated clockwise and the direction key would move the map cursor -- `quickfort`: new blueprint mode: ``#config``; for playing back key sequences that don't involve the map cursor (like configuring hotkeys, changing standing orders, or modifying military uniforms) -- `quickfort`: API function ``apply_blueprint`` can now take ``data`` parameters that are simple strings instead of coordinate maps. This allows easier application of blueprints that are just one cell. +- `caravan`: new overlay for selecting all/none on trade request screen +- `suspendmanager`: don't suspend constructions that are built over open space -# 0.47.05-r4 +# 50.08-r4 ## Fixes -- `quickfort`: produce a useful error message instead of a code error when a bad query blueprint key sequence leaves the game in a mode that does not have an active cursor -- `quickfort`: restore functionality to the ``--verbose`` commandline flag -- `quickfort`: don't designate tiles for digging if they are within the bounds of a planned or constructed building -- `quickfort`: allow grates, bars, and hatches to be built on flat floor (like DF itself allows) -- `quickfort`: allow tracks to be built on hard, natural rock ramps -- `quickfort`: allow dig priority to be properly set for track designations -- `quickfort`: fix incorrect directions for tracks that extend south or east from a track segment pair specified with expansion syntax (e.g. T(4x4)) -- `quickfort`: fix parsing of multi-part extended zone configs (e.g. when you set custom supply limits for hospital zones AND set custom flags for a pond) -- `quickfort`: fix error when attempting to set a custom limit for plaster powder in a hospital zone -- `exportlegends`: fix issue where birth year was outputted as birth seconds +- `gui/create-item`: allow blocks to be made out of wood when using the restrictive filters +- `emigration`: reassign home site for emigrating units so they don't just come right back to the fort +- `gui/sandbox`: allow creatures that have separate caste-based graphics to be spawned (like ewes/rams) +- `gui/liquids`: ensure tile temperature is set correctly when painting water or magma +- `workorder`: prevent ``autoMilkCreature`` from over-counting milkable animals, which was leading to cancellation spam for the MilkCreature job +- `gui/quickfort`: allow traffic designations to be applied over buildings +- `gui/quickfort`: protect against meta blueprints recursing infinitely if they include themselves ## Misc Improvements -- `gui/blueprint`: support the new ``--splitby`` and ``--format`` options for `blueprint` -- `gui/blueprint`: hide help text when the screen is too short to display it -- `quickfort`: add ``quickfort.apply_blueprint()`` API function that can be called directly by other scripts -- `quickfort`: by default, don't designate tiles for digging that have masterwork engravings on them. quality level to preserve is configurable with the new ``--preserve-engravings`` param -- `quickfort`: implement single-tile track aliases so engraved tracks can be specified tile-by-tile just like constructed tracks -- `quickfort`: allow blueprints to jump up or down multiple z-levels with a single command (e.g. ``#>5`` goes down 5 levels) -- `quickfort`: blueprints can now be repeated up and down a specified number of z-levels via ``repeat`` markers in meta blueprints or the ``--repeat`` commandline option -- `quickfort`: blueprints can now be rotated, flipped, and shifted via ``transform`` and ``shift`` markers in meta blueprints or the corresponding commandline options +- `gui/control-panel`: add some popular startup configuration commands for `autobutcher` and `autofarm` +- `gui/control-panel`: add option for running `fix/blood-del` on new forts (enabled by default) +- `gui/sandbox`: when creating citizens, give them names appropriate for their races +- `gui/autodump`: add option to clear the ``trader`` flag from teleported items, allowing you to reclaim items dropped by merchants +- `quickfort`: significant rewrite for DF v50! now handles zones, locations, stockpile configuration, hauling routes, and more +- `suspendmanager`: now suspends construction jobs on top of floor designations, protecting the designations from being erased +- `prioritize`: add wild animal management tasks and lever pulling to the default list of prioritized job types +- `suspendmanager`: suspend blocking jobs when building high walls or filling corridors +- `workorder`: reduce existing orders for automatic shearing and milking jobs when animals are no longer available +- `gui/quickfort`: adapt "cursor lock" to mouse controls so it's easier to see the full preview for multi-level blueprints before you apply them +- `gui/quickfort`: only display post-blueprint messages once when repeating the blueprint up or down z-levels +- `combine`: reduce max different stacks in containers to 30 to prevent containers from getting overfull ## Removed -- `digfort`: please use `quickfort` instead +- `gui/automelt`: replaced by an overlay panel that appears when you click on a stockpile -# 0.47.05-r3 +# 50.08-r2 ## New Scripts -- `autonick`: gives dwarves unique nicknames -- `build-now`: instantly completes planned building constructions -- `do-job-now`: makes a job involving current selection high priority -- `prioritize`: automatically boosts the priority of current and/or future jobs of specified types, such as hauling food, tanning hides, or pulling levers -- `reveal-adv-map`: exposes/hides all world map tiles in adventure mode - -## Fixes -- `devel/export-dt-ini`: fixed incorrect vtable address on Windows -- `quickfort`: allow machines (e.g. screw pumps) to be built on ramps just like DF allows -- `quickfort`: fix error message when the requested label is not found in the blueprint file - -## Misc Improvements -- `assign-beliefs`, `assign-facets`: now update needs of units that were changed -- `devel/query`: updated script to v3.2 (i.e. major rewrite for maintainability/readability) -- `devel/query`: merged options ``-query`` and ``-querykeys`` into ``-search`` -- `devel/query`: merged options ``-depth`` and ``-keydepth`` into ``-maxdepth`` -- `devel/query`: replaced option ``-safer`` with ``-excludetypes`` and ``-excludekinds`` -- `devel/query`: improved how tile data is dealt with identification, iteration, and searching -- `devel/query`: added option ``-findvalue`` -- `devel/query`: added option ``-showpaths`` to print full data paths instead of nested fields -- `devel/query`: added option ``-nopointers`` to disable printing values with memory addresses -- `devel/query`: added option ``-alignto`` to set the value column's alignment -- `devel/query`: added options ``-oneline`` and alias ``-1`` to avoid using two lines for fields with metadata -- `devel/query`: added support for matching multiple patterns -- `devel/query`: added support for selecting the highlighted job, plant, building, and map block data -- `devel/query`: added support for selecting a Lua script (e.g. `dorf_tables`) -- `devel/query`: added support for selecting a Json file (e.g. dwarf_profiles.json) -- `devel/query`: removed options ``-listall``, ``-listfields``, and ``-listkeys`` - these are now simply default behaviour -- `devel/query`: ``-table`` now accepts the same abbreviations (global names, ``unit``, ``screen``, etc.) as `lua` and `gui/gm-editor` -- `dorf_tables`: integrated `devel/query` to show the table definitions when requested with ``-list`` -- `geld`: fixed ``-help`` option -- `gui/gm-editor`: made search case-insensitive -- `quickfort`: accept multiple commands, list numbers, and/or blueprint lables on a single commandline -- `unretire-anyone`: replaced the 'undead' descriptor with 'reanimated' to make it more mod-friendly -- `warn-starving`: added an option to only check sane dwarves - -## Internals -- Install tests in the scripts repo into hack/scripts/test/scripts when the CMake variable BUILD_TESTS is defined - -# 0.47.05-r2 +- `diplomacy`: view or alter diplomatic relationships +- `exportlegends`: (reinstated) export extended legends information for external browsing +- `modtools/create-item`: (reinstated) commandline and API interface for creating items +- `light-aquifers-only`: (reinstated) convert heavy aquifers to light +- `necronomicon`: search fort for items containing the secrets of life and death +- `fix/stuck-instruments`: fix instruments that are attached to invalid jobs, making them unusable. turn on automatic fixing in `gui/control-panel` in the ``Maintenance`` tab. +- `gui/mod-manager`: automatically restore your list of active mods when generating new worlds +- `gui/autodump`: point and click item teleportation and destruction interface (available only if ``armok`` tools are shown) +- `gui/sandbox`: creation interface for units, trees, and items (available only if ``armok`` tools are shown) +- `assign-minecarts`: (reinstated) quickly assign minecarts to hauling routes + +## Fixes +- `quickfort`: properly allow dwarves to smooth, engrave, and carve beneath walkable tiles of buildings +- `deathcause`: fix incorrect weapon sometimes being reported +- `gui/create-item`: allow armor to be made out of leather when using the restrictive filters +- `gui/design`: Fix building and stairs designation +- `quickfort`: fixed detection of tiles where machines are allowed (e.g. water wheels *can* be built on stairs if there is a machine support nearby) +- `quickfort`: fixed rotation of blueprints with carved track tiles + +## Misc Improvements +- `gui/quickfort`: blueprints that designate items for dumping/forbidding/etc. no longer show an error highlight for tiles that have no items on them +- `gui/quickfort`: place (stockpile layout) mode is now supported. note that detailed stockpile configurations were part of query mode and are not yet supported +- `gui/quickfort`: you can now generate manager orders for items required to complete blueprints +- `gui/create-item`: ask for number of items to spawn by default +- `light-aquifers-only`: now available as a fort Autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. +- `gui/gm-editor`: when passing the ``--freeze`` option, further ensure that the game is frozen by halting all rendering (other than for DFHack tool windows) +- `gui/gm-editor`: Alt-A now enables auto-update mode, where you can watch values change live when the game is unpaused + +# 50.08-r1 + +## Fixes +- `deteriorate`: ensure remains of enemy dwarves are properly deteriorated +- `suspendmanager`: Fix over-aggressive suspension of jobs that could still possibly be done (e.g. jobs that are partially submerged in water) + +## Misc Improvements +- `combine`: Now supports ammo, parts, powders, and seeds, and combines into containers +- `deteriorate`: add option to exclude useable parts from deterioration +- `gui/gm-editor`: press ``g`` to move the map to the currently selected item/unit/building +- `gui/gm-editor`: press ``Ctrl-D`` to toggle read-only mode to protect from accidental changes; this state persists across sessions +- `gui/gm-editor`: new ``--freeze`` option for ensuring the game doesn't change while you're inspecting it +- `gui/launcher`: DFHack version now shown in the default help text +- `gui/prerelease-warning`: widgets are now clickable + +# 50.07-r1 + +## Fixes +-@ `caravan`: fix trade good list sometimes disappearing when you collapse a bin +-@ `gui/gm-editor`: no longer nudges last open window when opening a new one +- `warn-starving`: no longer warns for dead units +-@ `gui/control-panel`: the config UI for `automelt` is no longer offered when not in fortress mode + +## Misc Improvements +- `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` +- `gui/gm-editor`: the key column now auto-fits to the widest key +- `prioritize`: revise and simplify the default list of prioritized jobs -- be sure to tell us if your forts are running noticeably better (or worse!) +-@ `gui/control-panel`: add `faststart` to the system services + +# 50.07-beta2 ## New Scripts -- `clear-webs`: removes all webs on the map and/or frees any webbed creatures -- `devel/block-borders`: overlay that displays map block borders -- `devel/luacov`: generate code test coverage reports for script development. Define the ``DFHACK_ENABLE_LUACOV=1`` environment variable to start gathering coverage metrics. -- `fix/drop-webs`: causes floating webs to fall to the ground -- `gui/blueprint`: interactive frontend for the `blueprint` plugin (with mouse support!) -- `gui/mass-remove`: mass removal/suspension tool for buildings and constructions -- `reveal-hidden-sites`: exposes all undiscovered sites -- `set-timeskip-duration`: changes the duration of the "Updating World" process preceding the start of a new game, enabling you to jump in earlier or later than usual - -## Fixes -- `bodyswap`: stopped prior party members from tagging along after bodyswapping and reloading the map -- `bodyswap`: made companions of bodyswapping targets get added to the adventurer party - they can now be viewed using the in-game party system -- `color-schemes`: fixed an error in the ``register`` subcommand when the DF path contains certain punctuation characters -- `gui/advfort`: fixed an issue where starting a workshop job while not standing at the center of the workshop required advancing time manually -- `gui/unit-info-viewer`: fixed size description displaying unrelated values instead of size -- `quickfort`: comments in blueprint cells no longer prevent the rest of the row from being read. A cell with a single '#' marker in it, though, will still stop the parser from reading further in the row. -- `quickfort`: fixed an off-by-one line number accounting in blueprints with implicit ``#dig`` modelines -- `quickfort`: changed to properly detect and report an error on sub-alias params with no values instead of just failing to apply the alias later (if you really want an empty value, use ``{Empty}`` instead) -- `quickfort`: improved handling of non-rectangular and non-solid extent-based structures (like fancy-shaped stockpiles and farm plots) -- `quickfort`: fixed conversion of numbers to DF keycodes in ``#query`` blueprints -- `quickfort`: fixed various errors with cropping across the map edge -- `quickfort`: properly reset config to default values in ``quickfort reset`` even if if the ``dfhack-config/quickfort/quickfort.txt`` config file doesn't mention all config vars. Also now works even if the config file doesn't exist. - -## Misc Improvements -- `devel/annc-monitor`: added ``report enable|disable`` subcommand to filter combat reports -- `gui/advfort`: added workshop name to workshop UI -- `quickfort`: added the ``--cursor`` option for running a blueprint at specific coordinates instead of starting at the game cursor position -- `quickfort`: added more helpful error messages for invalid modeline markers -- `quickfort`: added support for extra space characters in blueprints -- `quickfort`: added a warning when an invalid alias is encountered instead of silently ignoring it -- `quickfort`: made more quiet when the ``--quiet`` parameter is specified -- `setfps`: improved error handling -- `unretire-anyone`: the historical figure selection list now includes the ``SYN_NAME`` (necromancer, vampire, etc) of figures where applicable - -# 0.47.05-r1 - -## Misc Improvements -- `gui/no-dfhack-init`: clarified how to dismiss dialog that displays when no ``dfhack.init`` file is found -- `quickfort`: an active cursor is no longer required for running #notes blueprints (like the dreamfort walkthrough) -- `quickfort`: you can now be in any mode with an active cursor when running ``#query`` blueprints (before you could only be in a few "approved" modes, like look, query, or place) -- `quickfort`: refined ``#query`` blueprint sanity checks: cursor should still be on target tile at end of configuration, and it's ok for the screen ID to change if you are destroying (or canceling destruction of) a building -- `quickfort`: now reports how many work orders were added when generating manager orders from blueprints in the gui dialog -- `quickfort`: added ``--dry-run`` option to process blueprints but not change any game state -- `quickfort`: you can now specify the number of desired barrels, bins, and wheelbarrows for individual stockpiles when placing them -- `quickfort`: ``quickfort orders`` on a ``#place`` blueprint will now enqueue manager orders for barrels, bins, or wheelbarrows that are explicitly set in the blueprint. -- `quickfort`: you can now add alias definitions directly to your blueprint files instead of having to put them in a separate aliases.txt file. makes sharing blueprints with custom alias definitions much easier. +- `fix/general-strike`: fix known causes of the general strike bug (contributed by Putnam) +- `gui/seedwatch`: GUI config and status panel interface for `seedwatch` +- `gui/civ-alert`: configure and trigger civilian alerts -## Documentation -- `digfort`: added deprecation warnings - digfort has been replaced by `quickfort` +## Fixes +-@ `caravan`: item list length now correct when expanding and collapsing containers +-@ `prioritize`: fixed all watched job type names showing as ``nil`` after a game load +-@ `suspendmanager`: does not suspend non-blocking jobs such as floor bars or bridges anymore +-@ `suspendmanager`: fix occasional bad identification of buildingplan jobs +- `warn-starving`: no longer warns for enemy and neutral units + +## Misc Improvements +- `gui/control-panel`: Now detects overlays from scripts named with capital letters +- `gui/cp437-table`: now has larger key buttons and clickable backspace/submit/cancel buttons, making it fully usable on the Steam Deck and other systems that don't have an accessible keyboard +-@ `gui/design`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. +- `exterminate`: add support for ``vaporize`` kill method for when you don't want to leave a corpse +- `combine`: you can select a target stockpile in the UI instead of having to use the keyboard cursor +- `combine`: added ``--quiet`` option for no output when there are no changes +- `stripcaged`: added ``--skip-forbidden`` option for greater control over which items are marked for dumping +- `stripcaged`: items that are marked for dumping are now automatically unforbidden (unless ``--skip-forbidden`` is set) +-@ `gui/control-panel`: added ``combine all`` maintenance option for automatic combining of partial stacks in stockpiles +-@ `gui/control-panel`: added ``general-strike`` maintenance option for automatic fixing of (at least one cause of) the general strike bug +- `gui/cp437-table`: dialog is now fully controllable with the mouse, including highlighting which key you are hovering over and adding a clickable backspace button + +## Removed +- `autounsuspend`: replaced by `suspendmanager` +-@ `gui/dig`: renamed to `gui/design` -# 0.47.05-beta1 +# 50.07-beta1 + +## New Scripts +- `suspendmanager`: automatic job suspension management (replaces `autounsuspend`) +- `gui/suspendmanager`: graphical configuration interface for `suspendmanager` +- `suspend`: suspends building construction jobs ## Fixes -- `quickfort`: raw numeric `quickfort-dig-priorities` (e.g. ``3``, which is a valid shorthand for ``d3``) now works when used in .xlsx blueprints +-@ `quicksave`: now reliably triggers an autosave, even if one has been performed recently +- `gui/launcher`: tab characters in command output now appear as a space instead of a code page 437 "blob" ## Misc Improvements -- `quickfort`: new commandline options for setting the initial state of the gui dialog. for example: ``quickfort gui -l dreamfort notes`` will start the dialog filtered for the dreamfort walkthrough blueprints +- `quickfort`: now reads player-created blueprints from ``dfhack-config/blueprints/`` instead of the old ``blueprints/`` directory. Be sure to move over your personal blueprints to the new directory! +- `gui/gm-editor`: can now open the selected stockpile if run without parameters + +# 50.07-alpha3 -# 0.47.04-r5 +## Fixes +-@ `gui/create-item`: fix generic corpsepiece spawning + +## Misc Improvements +- `gui/create-item`: added ability to spawn 'whole' corpsepieces (every layer of a part) +-@ `gui/dig`: Allow placing an extra point (curve) while still placing the second main point +-@ `gui/dig`: Allow placing n-point shapes, shape rotation/mirroring +-@ `gui/dig`: Allow second bezier point, mirror-mode for freeform shapes, symmetry mode + +# 50.07-alpha2 ## New Scripts -- `gui/quickfort`: fast access to the quickfort interactive dialog -- `workorder-recheck`: resets the selected work order to the ``Checking`` state +- `combine`: combines stacks of food and plant items. ## Fixes -- `quickfort`: zones are now created in the active state by default -- `quickfort`: solve rare crash when changing UI modes +-@ `troubleshoot-item`: fix printing of job details for chosen item +-@ `makeown`: fixes errors caused by using makeown on an invader +-@ `gui/blueprint`: correctly use setting presets passed on the commandline +-@ `gui/quickfort`: correctly use settings presets passed on the commandline +- `devel/query`: can now properly index vectors in the --table argument +-@ `forbid`: fix detection of unreachable items for items in containers +-@ `unforbid`: fix detection of unreachable items for items in containers ## Misc Improvements -- `quickfort`: new blueprint mode: ``#ignore``, useful for scratch space or personal notes -- `quickfort`: implement ``{Empty}`` keycode for use in quickfort aliases; useful for defining blank-by-default alias values -- `quickfort`: more flexible commandline parsing allowing for more natural parameter ordering (e.g. where you used to have to write ``quickfort list dreamfort -l`` you can now write ``quickfort list -l dreamfort``) -- `quickfort`: print out blueprint names that a ``#meta`` blueprint is applying so it's easier to understand what meta blueprints are doing -- `quickfort`: whitespace is now allowed between a marker name and the opening parenthesis in blueprint modelines. for example, ``#dig start (5; 5)`` is now valid (you used to be required to write ``#dig start(5; 5)``) +- `troubleshoot-item`: output as bullet point list with indenting, with item description and ID at top +- `troubleshoot-item`: reports on items that are hidden, artifacts, in containers, and held by a unit +- `troubleshoot-item`: reports on the contents of containers with counts for each contained item type +- `devel/visualize-structure`: now automatically inspects the contents of most pointer fields, rather than inspecting the pointers themselves +- `devel/query`: will now search for jobs at the map coordinate highlighted, if no explicit job is highlighted and there is a map tile highlighted +- `caravan`: add trade screen overlay that assists with selecting groups of items and collapsing groups in the UI +- `gui/gm-editor`: will now inspect a selected building itself if the building has no current jobs + +## Removed +- `combine-drinks`: replaced by `combine` +- `combine-plants`: replaced by `combine` -# 0.47.04-r4 +# 50.07-alpha1 ## New Scripts -- `fix/corrupt-equipment`: fixes some military equipment-related corruption issues that can cause DF crashes - -## Fixes -- `adaptation`: fixed handling of units with no cave adaptation suffered yet -- `assign-goals`: fixed error preventing new goals from being created -- `assign-preferences`: fixed handling of preferences for flour -- `deathcause`: fixed an error when inspecting certain corpses -- `quickfort`: fixed handling of modifier keys (e.g. ``{Ctrl}`` or ``{Alt}``) in query blueprints -- `quickfort`: fixed misconfiguration of nest boxes, hives, and slabs that were preventing them from being built from build blueprints -- `quickfort`: fixed valid placement detection for floor hatches, floor grates, and floor bars (they were erroneously being rejected from open spaces and staircase tops) -- `quickfort`: fixed query blueprint statistics being added to the wrong metric when both a query and a zone blueprint are run by the same meta blueprint -- `quickfort`: added missing blueprint labels in gui dialog list -- `quickfort`: fixed occupancy settings for extent-based structures so that stockpiles can be placed within other stockpiles (e.g. in a checkerboard or bullseye pattern) -- `unsuspend`: now leaves buildingplan-managed buildings alone and doesn't unsuspend underwater tasks - -## Misc Improvements -- `devel/export-dt-ini`: updated for Dwarf Therapist 41.2.0 -- `gui/advfort`: added support for linking to hatches and pressure plates with mechanisms -- `modtools/add-syndrome`: added support for specifying syndrome IDs instead of names -- `quickfort`: query blueprint aliases can now accept parameters for dynamic expansion - see dfhack-config/quickfort/aliases.txt for details -- `quickfort`: alias names can now include dashes and underscores (in addition to letters and numbers) -- `quickfort`: improved speed of first call to ``quickfort list`` significantly, especially for large blueprint libraries -- `quickfort`: added ``query_unsafe`` setting to disable query blueprint error checking - useful for query blueprints that send unusual key sequences -- `quickfort`: added support for bookcases, display cases, and offering places (altars) -- `quickfort`: added configuration support for zone pit/pond, gather, and hospital sub-menus in zone blueprints -- `quickfort`: removed ``buildings_use_blocks`` setting and replaced it with more flexible functionality in `buildingplan` -- `quickfort`: added support for creating uninitialized stockpiles with :kbd:`c` - -# 0.47.04-r3 +- `gui/design`: digging and construction designation tool with shapes and patterns +- `makeown`: makes the selected unit a citizen of your fortress + +## Fixes +-@ `gui/unit-syndromes`: allow the window widgets to be interacted with +-@ `fix/protect-nicks`: now works by setting the historical figure nickname +-@ `gui/liquids`: fixed issues with unit pathing after adding/removing liquids +-@ `gui/dig`: Fix for 'continuing' auto-stair designation. Avoid nil index issue for tile_type + +## Misc Improvements +- `gui/gm-editor`: now supports multiple independent data inspection windows +- `gui/gm-editor`: now prints out contents of coordinate vars instead of just the type +- `rejuvenate`: now takes an --age parameter to choose a desired age. +-@ `gui/dig` : Added 'Line' shape that also can draw curves, added draggable center handle + +# 50.05-alpha3.1 + +## Fixes +-@ `gui/launcher`: no longer resets to the Help tab on every keystroke + +# 50.05-alpha3 ## New Scripts -- `quickfort`: DFHack-native implementation of quickfort with many new features and integrations - see the `quickfort-user-guide` for details -- `timestream`: controls the speed of the calendar and creatures -- `uniform-unstick`: prompts units to reevaluate their uniform, by removing/dropping potentially conflicting worn items +- `autofish`: auto-manage fishing labors to control your stock of fish +- `gui/autofish`: GUI config and status panel interface for autofish +- `gui/automelt`: GUI config and status panel interface for automelt +- `gui/control-panel`: quick access to DFHack configuration +- `fix/civil-war`: removes negative relations with own government +- `fix/protect-nicks`: restore nicknames when DF loses them +- `forbid`: forbid and list forbidden items on the map +- `gui/unit-syndromes`: browser for syndrome information ## Fixes -- `ban-cooking`: fixed an error in several subcommands +- `build-now`: now correctly avoids adjusting non-empty tiles above constructions that it builds +- `catsplosion`: now only affects live, active units +- `quickfort`: allow floor bars, floor grates, and hatches to be placed over all stair types like vanilla allows ## Misc Improvements -- `unretire-anyone`: made undead creature names appear in the historical figure list +- `ban-cooking`: ban announcements are now hidden by default; use new option ``--verbose`` to show them. +- `ban-cooking`: report number of items banned. +- `build-now`: now handles dirt roads and initializes farm plots properly +- `devel/click-monitor`: report on middle mouse button actions +-@ `gui/autochop`: hide uninteresting burrows by default +-@ `gui/blueprint`: allow map movement with the keyboard while the UI is open +- `gui/create-item`: support spawning corpse pieces (e.g. shells) under "body part" +- `gui/create-item`: added search and filter capabilities to the selection lists +- `gui/launcher`: make command output scrollback separate from the help text so players can continue to see the output of the previous command as they type the next one +- `gui/launcher`: allow double spacebar to pause/unpause the game, even while typing a command +- `gui/launcher`: clarify what is being shown in the autocomplete list (all commands, autocompletion of partially typed command, or commands related to typed command) +- `gui/launcher`: support running commands directly from the autocomplete list via double-clicking +- `gui/liquids`: interface overhaul, also now allows spawning river sources, setting/adding/removing liquid levels, and cleaning water from being salty or stagnant +- `gui/overlay`: now focuses on repositioning overlay widgets; enabling, disabling, and getting help for overlay widgets has moved to the new `gui/control-panel` +-@ `gui/quickcmd`: now acts like a regular window instead of a modal dialog +- `gui/quickfort`: don't close the window when applying a blueprint so players can apply the same blueprint multiple times more easily +- `locate-ore`: now only searches revealed tiles by default +- `modtools/spawn-liquid`: sets tile temperature to stable levels when spawning water or magma +-@ `prioritize`: pushing minecarts is now included in the default prioritization list +- `prioritize`: now automatically starts boosting the default list of job types when enabled +- `unforbid`: avoids unforbidding unreachable and underwater items by default +- `gui/create-item`: added whole corpse spawning alongside corpsepieces. (under "corpse") + +## Removed +- `show-unit-syndromes`: replaced by `gui/unit-syndromes`; html export is no longer supported + +# 50.05-alpha2 + +## Fixes +-@ `gui/gm-editor`: fix errors displayed while viewing help screen +- `build-now`: don't error on constructions that do not have an item attached + +## Removed +- `create-items`: replaced by `gui/create-item` ``--multi`` -# 0.47.04-r2 +# 50.05-alpha1 ## New Scripts -- `animal-control`: helps manage the butchery and gelding of animals -- `devel/kill-hf`: kills a historical figure -- `geld`: gelds or ungelds animals -- `list-agreements`: lists all guildhall and temple agreements -- `list-waves`: displays migration wave information for citizens/units -- `ungeld`: ungelds animals (wrapper around `geld`) - -## Fixes -- `digfort`: fixed y-line tracking when .csv files contain lines with only commas -- `digfort`: fixed an issue causing blueprints touching the southern or eastern edges of the map to be rejected (northern and western edges were already allowed). This allows blueprints that span the entire embark area. -- `exportlegends`: fixed an issue where two different ```` tags could be included in a ```` -- `exportlegends`: stopped including some tags with ``-1`` values which don't provide useful information -- `gui/advfort`: fixed "operate pump" job -- `gui/load-screen`: fixed an issue causing longer timezones to be cut off -- `names`: fixed an error preventing the script from working -- `names`: fixed an issue causing renamed units to display their old name in legends mode and some other places -- `modtools/moddable-gods`: fixed an error when creating the historical figure -- `modtools/moddable-gods`: removed unused ``-domain`` and ``-description`` arguments -- `modtools/moddable-gods`: made ``-depictedAs`` argument work -- `pref-adjust`: fixed some compatibility issues and a potential crash - -## Misc Improvements -- `add-recipe`: added tool recipes (minecarts, wheelbarrows, stepladders, etc.) -- `add-recipe`: added a command explanation or error message when entering an invalid command -- `armoks-blessing`: added adjustments to values and needs -- `devel/query`: added many new query options -- `digfort`: handled double quotes (") at the start of a string, allowing .csv files exported from spreadsheets to work without manual modification -- `digfort`: documented that removing ramps, cutting trees, and gathering plants are indeed supported -- `digfort`: added a ``force`` option to truncate blueprints if the full blueprint would extend off the edge of the map -- `dwarf-op`: added ability to select dwarves based on migration wave -- `dwarf-op`: added ability to protect dwarves based on symbols in their custom professions -- `exportlegends`: changed some flags to be represented by self-closing tags instead of true/false strings (e.g. ````) - note that this may require changes to other XML-parsing utilities -- `exportlegends`: changed some enum values from numbers to their string representations -- `exportlegends`: added ability to save all files to a subfolder, named after the region folder and date by default -- `gui/advfort`: added support for specifying the entity used to determine available resources -- `gui/gm-editor`: added support for automatically following ref-targets when pressing the ``i`` key -- `modtools/moddable-gods`: added support for ``neuter`` gender -- `pref-adjust`: added support for adjusting just the selected dwarf -- `pref-adjust`: added a new ``goth`` profile -- `remove-stress`: added a ``-value`` argument to enable setting stress level directly -- `workorder`: changed default frequency from "Daily" to "OneTime" - -# 0.47.04-r1 - -## Fixes -- `catsplosion`: fixed error when handling races with only one caste (e.g. harpies) --@ `exportlegends`: fixed error when exporting maps -- `spawnunit`: fixed an error when forwarding some arguments but not a location to `modtools/create-unit` - -## Misc Improvements -- `exportlegends`: - - made interaction export more robust and human-readable - - removed empty ```` and ```` tags -- `modtools/create-unit`: - - added ``-equip`` option to equip created units - - added ``-skills`` option to give skills to units - - added ``-profession`` and ``-customProfession`` options to adjust unit professions - -# 0.47.04-beta1 - -## New scripts -- `color-schemes`: manages color schemes -- `devel/print-event`: prints the description of an event by ID or index -- `gui/color-schemes`: an in-game interface for `color-schemes` -- `light-aquifers-only`: changes heavy aquifers to light aquifers -- `on-new-fortress`: runs DFHack commands only in a new fortress -- `once-per-save`: runs DFHack commands unless already run in the current save -- `resurrect-adv`: brings your adventurer back to life -- `reveal-hidden-units`: exposes all sneaking units -- `workorder`: allows queuing manager jobs; smart about shear and milk creature jobs - -## Fixes -- `devel/visualize-structure`: fixed padding detection for globals -- `exportlegends`: - - added UTF-8 encoding and XML escaping for more fields - - added checking for unhandled structures to avoid generating invalid XML - - fixed missing fields in ``history_event_assume_identityst`` export -- `full-heal`: - - when resurrected by specifying a corpse, units now appear at the location of the corpse rather than their location of death - - resurrected units now have their tile occupancy set (and are placed in the prone position to facilitate this) - -## Misc Improvements -- `devel/export-dt-ini`: updated some field names for DT for 0.47 -- `devel/visualize-structure`: added human-readable lengths to containers -- `exportlegends`: - - added evilness and force IDs to regions - - added profession and weapon info to relevant entities - - added support for many new history events in 0.47 - - added historical event relationships and supplementary data -- `full-heal`: - - made resurrection produce a historical event viewable in Legends mode - - made error messages more explanatory -- `install-info`: added DFHack build ID to report -- `modtools/create-item`: added ``-matchingGloves`` and ``-matchingShoes`` arguments -- `modtools/create-unit`: - - added ``-duration`` argument to make the unit vanish after some time - - added ``-locationRange`` argument to allow spawning in a random position within a defined area - - added ``-locationType`` argument to specify the type of location to spawn in +- `gui/autochop`: configuration frontend and status monitor for the `autochop` plugin +- `devel/tile-browser`: page through available textures and see their texture ids +- `allneeds`: list all unmet needs sorted by how many dwarves suffer from them. + +## Fixes +- `make-legendary`: "MilitaryUnarmed" option now functional + +## Misc Improvements +- `autounsuspend`: now saves its state with your fort +- `emigration`: now saves its state with your fort +- `prioritize`: now saves its state with your fort +- `unsuspend`: overlay now displays different letters for different suspend states so they can be differentiated in graphics mode (P=planning, x=suspended, X=repeatedly suspended) +- `unsuspend`: overlay now shows a marker all the time when in graphics mode. ascii mode still only shows when paused so that you can see what's underneath. +- `gui/gm-editor`: converted to a movable, resizable, mouse-enabled window +- `gui/launcher`: now supports a smaller, minimal mode. click the toggle in the launcher UI or start in minimal mode via the ``Ctrl-Shift-P`` keybinding +- `gui/launcher`: can now be dragged from anywhere on the window body +- `gui/launcher`: now remembers its size and position between invocations +- `gui/gm-unit`: converted to a movable, resizable, mouse-enabled window +- `gui/cp437-table`: converted to a movable, mouse-enabled window +- `gui/quickcmd`: converted to a movable, resizable, mouse-enabled window +- `gui/quickcmd`: commands are now stored globally so you don't have to recreate commands for every fort +- `devel/inspect-screen`: updated for new rendering semantics and can now also inspect map textures +- `exterminate`: added drown method. magma and drown methods will now clean up liquids automatically. + +## Documentation +- `devel/hello-world`: updated to be a better example from which to start new gui scripts diff --git a/clear-smoke.lua b/clear-smoke.lua index 987ab40365..ee3b82c017 100644 --- a/clear-smoke.lua +++ b/clear-smoke.lua @@ -1,25 +1,39 @@ --- Removes all smoke from the map +--@module = true ---[====[ - -clear-smoke -=========== - -Removes all smoke from the map. Note that this can leak memory and should be -used sparingly. +function removeFlow(flow) --have DF remove the flow + if not flow then + return + end + flow.flags.DEAD = true -]====] + local block = dfhack.maps.getTileBlock(flow.pos) + if block then + block.flow_pool.flags.active = true + else + df.global.world.orphaned_flow_pool.flags.active = true + end +end -function clearSmoke(flows) - for i = #flows - 1, 0, -1 do - if flows[i].type == df.flow_type.Smoke then - flows:erase(i) +function removeFlows(flow_type) --remove all if flow_type is nil + local count = 0 + for _,flow in ipairs(df.global.flows) do + if not flow.flags.DEAD and (flow_type == nil or flow.type == flow_type) then + removeFlow(flow) + count = count + 1 end end + + return count end -clearSmoke(df.global.flows) +function clearSmoke() + if dfhack.isWorldLoaded() then + print(('%d smoke flows removed.'):format(removeFlows(df.flow_type.Smoke))) + else + qerror('World not loaded!') + end +end -for _, block in pairs(df.global.world.map.map_blocks) do - clearSmoke(block.flows) +if not dfhack_flags.module then + clearSmoke() end diff --git a/clear-webs.lua b/clear-webs.lua index 37f1939da3..bab8086da5 100644 --- a/clear-webs.lua +++ b/clear-webs.lua @@ -1,36 +1,7 @@ -- Removes webs and frees webbed units. -- Author: Atomic Chicken -local usage = [====[ - -clear-webs -========== -This script removes all webs that are currently on the map, -and also frees any creatures who have been caught in one. - -Note that it does not affect sprayed webs until -they settle on the ground. - -Usable in both fortress and adventurer mode. - -Web removal and unit release happen together by default. -The following may be used to isolate one of these actions: - -Arguments:: - - -unitsOnly - Include this if you want to free all units from webs - without removing any webs - - -websOnly - Include this if you want to remove all webs - without freeing any units - -See also `fix/drop-webs`. - -]====] - -local utils = require 'utils' +local utils = require('utils') local validArgs = utils.invert({ 'unitsOnly', 'websOnly', @@ -39,12 +10,12 @@ local validArgs = utils.invert({ local args = utils.processArgs({...}, validArgs) if args.help then - print(usage) + print(dfhack.script_help()) return end if args.unitsOnly and args.websOnly then - qerror("You have specified both -unitsOnly and -websOnly. These cannot be used together.") + qerror("You have specified both --unitsOnly and --websOnly. These cannot be used together.") end local webCount = 0 @@ -57,7 +28,7 @@ end local unitCount = 0 if not args.websOnly then - for _, unit in ipairs(df.global.world.units.all) do + for _, unit in ipairs(df.global.world.units.active) do if unit.counters.webbed > 0 and not unit.flags2.killed and not unit.flags1.inactive then -- the webbed status is retained in death unitCount = unitCount + 1 unit.counters.webbed = 0 diff --git a/colonies.lua b/colonies.lua index 93d7bff3e9..2672b66ab9 100644 --- a/colonies.lua +++ b/colonies.lua @@ -1,24 +1,7 @@ -- List, create, or change wild colonies (eg honey bees) -- By PeridexisErrant and Warmist -local help = [====[ - -colonies -======== -List vermin colonies, place honey bees, or convert all vermin -to honey bees. Usage: - -:colonies: List all vermin colonies on the map. -:colonies place: Place a honey bee colony under the cursor. -:colonies convert: Convert all existing colonies to honey bees. - -The ``place`` and ``convert`` subcommands by default create or -convert to honey bees, as this is the most commonly useful. -However both accept an optional flag to use a different vermin -type, for example ``colonies place ANT`` creates an ant colony -and ``colonies convert TERMITE`` ends your beekeeping industry. - -]====] +local guidm = require('gui.dwarfmode') function findVermin(target_verm) for k,v in ipairs(df.global.world.raws.creatures.all) do @@ -30,7 +13,7 @@ function findVermin(target_verm) end function list_colonies() - for idx, col in pairs(df.global.world.vermin.colonies) do + for idx, col in pairs(df.global.world.event.vermin_colonies) do local race = df.global.world.raws.creatures.all[col.race].creature_id print(race..' at '..col.pos.x..', '..col.pos.y..', '..col.pos.z) end @@ -39,7 +22,7 @@ end function convert_vermin_to(target_verm) local vermin_id = findVermin(target_verm) local changed = 0 - for _, verm in pairs(df.global.world.vermin.colonies) do + for _, verm in pairs(df.global.world.event.vermin_colonies) do verm.race = vermin_id verm.caste = -1 -- check for queen bee? verm.amount = 18826 @@ -50,8 +33,8 @@ function convert_vermin_to(target_verm) end function place_vermin(target_verm) - local pos = copyall(df.global.cursor) - if pos.x == -30000 then + local pos = guidm.getCursorPos() + if not pos then qerror("Cursor must be pointing somewhere") end local verm = df.vermin:new() @@ -61,21 +44,21 @@ function place_vermin(target_verm) verm.amount = 18826 verm.visible = true verm.pos:assign(pos) - df.global.world.vermin.colonies:insert("#", verm) - df.global.world.vermin.all:insert("#", verm) + df.global.world.event.vermin_colonies:insert("#", verm) + df.global.world.event.vermin:insert("#", verm) end local args = {...} local target_verm = args[2] or "HONEY_BEE" if args[1] == 'help' or args[1] == '?' then - print(help) + print(dfhack.script_help()) elseif args[1] == 'convert' then convert_vermin_to(target_verm) elseif args[1] == 'place' then place_vermin(target_verm) else - if #df.global.world.vermin.colonies < 1 then + if #df.global.world.event.vermin_colonies < 1 then dfhack.printerr('There are no colonies on the map.') end list_colonies() diff --git a/combat-harden.lua b/combat-harden.lua index 39cdef7a83..8010ae14b8 100644 --- a/combat-harden.lua +++ b/combat-harden.lua @@ -1,41 +1,6 @@ --- Sets a unit's combat-hardened value to a given percent --@ module = true -local help = [====[ - -combat-harden -============= -Sets the combat-hardened value on a unit, making them care more/less about seeing corpses. -Requires a value and a target. - -Valid values: - -:``-value <0-100>``: - A percent value to set combat hardened to. -:``-tier <1-4>``: - Choose a tier of hardenedness to set it to. - 1 = No hardenedness. - 2 = "is getting used to tragedy" - 3 = "is a hardened individual" - 4 = "doesn't really care about anything anymore" (max) - -If neither are provided, the script defaults to using a value of 100. - -Valid targets: - -:``-all``: - All active units will be affected. -:``-citizens``: - All (sane) citizens of your fort will be affected. Will do nothing in adventure mode. -:``-unit ``: - The given unit will be affected. - -If no target is given, the provided unit can't be found, or no unit id is given with the unit -argument, the script will try and default to targeting the currently selected unit. - -]====] - -local utils = require 'utils' +local utils = require('utils') local validArgs = utils.invert({ 'help', @@ -49,83 +14,81 @@ local validArgs = utils.invert({ local tiers = {0, 33, 67, 100} function setUnitCombatHardened(unit, value) - if unit.status.current_soul ~= nil then - -- Ensure value is in the bounds of 0-100 - local value = math.max(0, math.min(100, value)) + if not unit.status.current_soul then return end - unit.status.current_soul.personality.combat_hardened = value - end + -- Ensure value is in the bounds of 0-100 + value = math.max(0, math.min(100, value)) + unit.status.current_soul.personality.combat_hardened = value + + print(('set hardness value for %s to %d'):format( + dfhack.df2console(dfhack.units.getReadableName(unit)), + value)) end -function main(...) - local args = utils.processArgs({...}, validArgs) +function main(args) + local opts = utils.processArgs(args, validArgs) - if args.help then - print(help) + if opts.help then + print(dfhack.script_help()) return end local value - if not args.tier and not args.value then + if not opts.tier and not opts.value then -- Default to 100 value = 100 - elseif args.tier then + elseif opts.tier then -- Bound between 1-4 - local tierNum = math.max(1, math.min(4, tonumber(args.tier))) + local tierNum = math.max(1, math.min(4, tonumber(opts.tier))) value = tiers[tierNum] - elseif args.value then + elseif opts.value then -- Function ensures value is bound, so no need to bother here -- Will check it's a number, though - value = tonumber(args.value) or 100 + value = tonumber(opts.value) or 100 end local unitsList = {} --as:df.unit[] - if not args.all and not args.citizens then + if not opts.all and not opts.citizens then -- Assume trying to target a unit local unit - if args.unit then - if tonumber(args.unit) then - unit = df.unit.find(args.unit) + if opts.unit then + if tonumber(opts.unit) then + unit = df.unit.find(opts.unit) end end -- If unit ID wasn't provided / unit couldn't be found, -- Try getting selected unit - if unit == nil then + if not unit then unit = dfhack.gui.getSelectedUnit(true) end - if unit == nil then - qerror("Couldn't find unit. If you don't want to target a specific unit, use -all or -citizens.") + if not unit then + qerror("Couldn't find unit. If you don't want to target a specific unit, use --all or --citizens.") else table.insert(unitsList, unit) end - elseif args.all then + elseif opts.all then for _, unit in pairs(df.global.world.units.active) do table.insert(unitsList, unit) end - elseif args.citizens then - -- Technically this will exclude insane citizens, but this is the - -- easiest thing that dfhack provides - + elseif opts.citizens then -- Abort if not in Fort mode if not dfhack.world.isFortressMode() then - qerror('-citizens requires fortress mode') + qerror('--citizens requires fortress mode') end - for _, unit in pairs(df.global.world.units.active) do - if dfhack.units.isCitizen(unit) then - table.insert(unitsList, unit) - end + for _, unit in ipairs(dfhack.units.getCitizens()) do + table.insert(unitsList, unit) end end - for index, unit in ipairs(unitsList) do + for _, unit in ipairs(unitsList) do setUnitCombatHardened(unit, value) end end if not dfhack_flags.module then - main(...) + main{...} end diff --git a/combine-drinks.lua b/combine-drinks.lua deleted file mode 100644 index 0a909aa0b5..0000000000 --- a/combine-drinks.lua +++ /dev/null @@ -1,122 +0,0 @@ --- Merge drink stacks in the selected stockpile ---[====[ - -combine-drinks -============== -Merge stacks of drinks in the selected stockpile. - -]====] -local utils = require 'utils' - -local validArgs = utils.invert({ 'max', 'stockpile' }) -local args = utils.processArgs({...}, validArgs) - -local max = 30 -if args.max then max = tonumber(args.max) end - -local stockpile = nil -if args.stockpile then stockpile = df.building.find(tonumber(args.stockpile)) end - -local function itemsCompatible(item0, item1) - return item0:getType() == item1:getType() - and item0.mat_type == item1.mat_type - and item0.mat_index == item1.mat_index -end - -local function getDrinks(items, drinks, index) - for i,d in ipairs(items) do - local foundDrink = nil - - -- Skip items currently tasked - if #d.specific_refs == 0 then - - if d:getType() == df.item_type.DRINK then - foundDrink = d - else - --print(d.id) - local containedItems = dfhack.items.getContainedItems(d) - -- Drink containers only contain one item - if #containedItems == 1 then - local possibleDrink = containedItems[1] - - if #possibleDrink.specific_refs == 0 and possibleDrink:getType() == df.item_type.DRINK then - foundDrink = possibleDrink - end - end - end - end - - if foundDrink ~= nil then - drinks[index] = foundDrink - index = index + 1 - end - end - - return index -end - -local building = stockpile or dfhack.gui.getSelectedBuilding(true) -if building ~= nil and building:getType() ~= 29 then building = nil end - -if building == nil then - qerror("Select a stockpile") - -else - local rootItems = dfhack.buildings.getStockpileContents(building) - - if #rootItems == 0 then - qerror("Select a non-empty stockpile") - - else - local drinks = { } --as:df.item_drinkst[] - local drinkCount = getDrinks(rootItems, drinks, 0) - - --for i,p in ipairs(drinks) do - -- print(i .. ': ' .. dfhack.items.getDescription(p, p:getType())) - --end - - local removedDrinks = {} --as:bool[] - - for i=0,(drinkCount-2) do - local currentDrink = drinks[i] - local itemsNeeded = max - currentDrink.stack_size - --print('processing ' .. dfhack.items.getDescription(currentDrink, currentDrink:getType()) .. ' needs ' .. itemsNeeded) - if removedDrinks[currentDrink.id] == nil and itemsNeeded > 0 then - for j=(i+1),(drinkCount-1) do - local sourceDrink = drinks[j] - --print('\ttrying ' .. dfhack.items.getDescription(sourceDrink, sourceDrink:getType())) - - if removedDrinks[sourceDrink.id] == nil and itemsCompatible(currentDrink, sourceDrink) then - local amountToMove = math.min(itemsNeeded, sourceDrink.stack_size) - --print('\tmoving ' .. amountToMove) - itemsNeeded = itemsNeeded - amountToMove - currentDrink.stack_size = currentDrink.stack_size + amountToMove - - if sourceDrink.stack_size == amountToMove then - --print('\tadding remove id ' .. sourceDrink.id) - removedDrinks[sourceDrink.id] = true - else - sourceDrink.stack_size = sourceDrink.stack_size - amountToMove - end - end - end - end - end - - local removedDrinkCount = 0 - for id,removed in pairs(removedDrinks) do - if removed then - local removedDrink = df.item.find(id) - removedDrinkCount = removedDrinkCount + 1 - --print('remove id=' .. id .. ' drink=' .. dfhack.items.getDescription(removedDrink, removedDrink:getType())) - dfhack.items.remove(removedDrink) - end - end - print('found ' .. drinkCount .. ' drinks') - print('removed ' .. removedDrinkCount .. ' drinks') - end ---elseif item:getType() == 68 then - --handleDrink(item) ---else --- qerror("Select a drink or a drink.") -end diff --git a/combine-plants.lua b/combine-plants.lua deleted file mode 100644 index d32907433e..0000000000 --- a/combine-plants.lua +++ /dev/null @@ -1,118 +0,0 @@ --- Merge plant stacks in the selected container or stockpile ---[====[ - -combine-plants -============== -Merge stacks of plants or plant growths in the selected container or stockpile. - -]====] -local utils = require 'utils' - -local validArgs = utils.invert({ 'max', 'stockpile', 'container' }) -local args = utils.processArgs({...}, validArgs) - -local max = 12 -if args.max then max = tonumber(args.max) end - -local stockpile = nil -if args.stockpile then stockpile = df.building.find(tonumber(args.stockpile)) end - -local container = nil -if args.container then container = df.item.find(tonumber(args.container)) end - -function itemsCompatible(item0, item1) - return item0:getType() == item1:getType() - and item0.mat_type == item1.mat_type --hint:df.item_plantst - and item0.mat_index == item1.mat_index --hint:df.item_plantst -end - -function getPlants(items, plants, index) - repeat - local nextBatch = {} - for _,v in pairs(items) do - -- Skip items currently tasked - if #v.specific_refs == 0 then - if v:getType() == df.item_type.PLANT or v:getType() == df.item_type.PLANT_GROWTH or v:getType() == df.item_type.CHEESE then - plants[index] = v - index = index + 1 - - else - local containedItems = dfhack.items.getContainedItems(v) - if #containedItems > 0 then - for _,w in pairs(containedItems) do - table.insert(nextBatch, w) - end - end - end - end - end - items = nextBatch - until #items == 0 - - return index -end - -local item = container or dfhack.gui.getSelectedItem(true) -local building = stockpile or dfhack.gui.getSelectedBuilding(true) -if building ~= nil and building:getType() ~= 29 then building = nil end -if item == nil and building == nil then - qerror("Select an item or building") - -else - local rootItems - if building then - rootItems = dfhack.buildings.getStockpileContents(building) - else - rootItems = dfhack.items.getContainedItems(item) - end - - if #rootItems == 0 then - qerror("Select a non-empty container") - - else - local plants = { } --as:df.item_actual[] - local plantCount = getPlants(rootItems, plants, 0) - print("found " .. plantCount .. " plants") - - local removedPlants = { } --as:bool[] - - for i=0,(plantCount-2) do - local currentPlant = plants[i] --as:df.item_plantst - local itemsNeeded = max - currentPlant.stack_size - - if removedPlants[currentPlant.id] == nil and itemsNeeded > 0 then - local j = i+1 - local last = plantCount - repeat - local sourcePlant = plants[j] - - if removedPlants[sourcePlant.id] == nil and itemsCompatible(currentPlant, sourcePlant) then - local amountToMove = math.min(itemsNeeded, sourcePlant.stack_size) - itemsNeeded = itemsNeeded - amountToMove - currentPlant.stack_size = currentPlant.stack_size + amountToMove - - if sourcePlant.stack_size == amountToMove then - removedPlants[sourcePlant.id] = true - sourcePlant.stack_size = 1 - else - sourcePlant.stack_size = sourcePlant.stack_size - amountToMove - end --- else print("failed") - end - - j = j + 1 - until j == plantCount or itemsNeeded == 0 - end - end - - local removedCount = 0 - for id,removed in pairs(removedPlants) do - if removed then - removedCount = removedCount + 1 - local removedPlant = df.item.find(id) - dfhack.items.remove(removedPlant) - end - end - print("removed " .. removedCount .. " plants") - end -end diff --git a/combine.lua b/combine.lua new file mode 100644 index 0000000000..cd9e2522f0 --- /dev/null +++ b/combine.lua @@ -0,0 +1,867 @@ +local argparse = require('argparse') +local utils = require('utils') + +local opts, args = { + help = false, + all = nil, + here = nil, + dry_run = false, + types = nil, + quiet = false, + verbose = 0, + }, {...} + +-- TODO: +-- - Combine non-plantable seeds (seed combining currently commented out since we don't want to combine plantable seeds) +-- - Combine items inside built containers. +-- - Combine cloth, quality of cloth. +-- - Combine partial bars in smelters. +-- - Combine thread, quality of thread. +-- - Quality for food, currently ignoring. +-- - Override stack size; armok option. +-- - Override container limits; quantum containers armok option. + +-- list of types that use race and caste +local typesThatUseCreatures = utils.invert{'REMAINS', 'FISH', 'FISH_RAW', 'VERMIN', 'PET', 'EGG', 'CORPSE', 'CORPSEPIECE'} +local typesThatUseMaterial = utils.invert{'CORPSEPIECE'} + +-- list of valid item types for merging +-- Notes: 1. mergeable stacks are ones with the same type_id+race+caste or type_id+mat_type+mat_index +-- 2. even though powders are specified, sand and plaster types items are excluded from merging. +-- 3. seeds cannot be combined in stacks > 1. +local valid_types_map = { + all = { }, + ammo = {[df.item_type.AMMO] ={type_id=df.item_type.AMMO, max_stack_qty=25, max_mat_amt=1}}, + parts = {[df.item_type.CORPSEPIECE] ={type_id=df.item_type.CORPSEPIECE, max_stack_qty=1, max_mat_amt=30}}, + drink = {[df.item_type.DRINK] ={type_id=df.item_type.DRINK, max_stack_qty=25, max_mat_amt=1}}, + fat = {[df.item_type.GLOB] ={type_id=df.item_type.GLOB, max_stack_qty=5, max_mat_amt=1}, + [df.item_type.CHEESE] ={type_id=df.item_type.CHEESE, max_stack_qty=5, max_mat_amt=1}}, + fish = {[df.item_type.FISH] ={type_id=df.item_type.FISH, max_stack_qty=5, max_mat_amt=1}, + [df.item_type.FISH_RAW] ={type_id=df.item_type.FISH_RAW, max_stack_qty=5, max_mat_amt=1}, + [df.item_type.EGG] ={type_id=df.item_type.EGG, max_stack_qty=5, max_mat_amt=1}}, + food = {[df.item_type.FOOD] ={type_id=df.item_type.FOOD, max_stack_qty=20, max_mat_amt=1}}, + meat = {[df.item_type.MEAT] ={type_id=df.item_type.MEAT, max_stack_qty=5, max_mat_amt=1}}, + plant = {[df.item_type.PLANT] ={type_id=df.item_type.PLANT, max_stack_qty=5, max_mat_amt=1}, + [df.item_type.PLANT_GROWTH]={type_id=df.item_type.PLANT_GROWTH, max_stack_qty=5, max_mat_amt=1}}, + powder= {[df.item_type.POWDER_MISC] ={type_id=df.item_type.POWDER_MISC, max_stack_qty=10, max_mat_amt=1}}, +-- seed = {[df.item_type.SEEDS] ={type_id=df.item_type.SEEDS, max_stack_qty=1, max_mat_amt=1}}, +} + +-- populate all types entry +for k1,v1 in pairs(valid_types_map) do + if k1 == 'all' then goto continue end + for k2,v2 in pairs(v1) do + local elem = ensure_key(valid_types_map.all, k2) + for k3,v3 in pairs(v2) do + elem[k3] = v3 + end + end + ::continue:: +end + +local function log(level, ...) + -- if verbose is specified, then print the arguments, or don't. + if not opts.quiet and opts.verbose >= level then + print(dfhack.df2console(string.format(...))) + end +end + +-- CList class +-- generic list class used for key value pairs. +local CList = { } + +function CList:new(o) + -- key, value pair table structure. __len allows # to be used for table count. + o = o or { } + setmetatable(o, self) + self.__index = self + self.__len = function(t) local n = 0 for _ in pairs(t) do n = n + 1 end return n end + return o +end + +local function comp_item_new(comp_key, stack_type) + -- create a new comp_item entry to be added to a comp_items table. + local comp_item = {} + if not comp_key then qerror('new_comp_item: comp_key is nil') end + comp_item.comp_key = comp_key -- key used to index comparable items for merging + comp_item.description = '' -- description of the comp item for output + comp_item.max_stack_qty = stack_type.max_stack_qty -- how many of a comp item can be in one stack + -- item info + comp_item.items = CList:new() -- key:item.id, + -- val:{item, + -- before_size, after_size, before_cont_id, after_cont_id, + -- stockpile_id, stockpile_name, + -- before_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} + -- after_mat_amt {Leather, Bone, Shell, Tooth, Horn, HairWool, Yarn} + -- } + comp_item.item_qty = 0 -- total quantity of items + comp_item.material_amt = 0 -- total amount of materials + comp_item.max_mat_amt = stack_type.max_mat_amt -- max amount of materials in one stack + + comp_item.before_stacks = 0 -- the number of stacks of the items before... + comp_item.after_stacks = 0 -- ...and after the merge + --container info + comp_item.before_cont_ids = CList:new() -- key:container.id, val:container.id + comp_item.after_cont_ids = CList:new() -- key:container.id, val:container.id + return comp_item +end + +local function comp_item_add_item(stockpile, stack_type, comp_item, item, container) + -- add an item into the comp_items table, setting the comp_item attributes. + if not comp_item.items[item.id] then + comp_item.item_qty = comp_item.item_qty + item.stack_size + comp_item.before_stacks = comp_item.before_stacks + 1 + comp_item.description = utils.getItemDescription(item, 1) + + if item.stack_size > comp_item.max_stack_qty then + comp_item.max_stack_qty = item.stack_size + end + + local new_item = {} + new_item.item = item + new_item.before_size = item.stack_size + + new_item.stockpile_id = stockpile.id + new_item.stockpile_name = stockpile.name + + -- material amount info + new_item.before_mat_amt = {} + new_item.before_mat_amt.Qty = 0 + new_item.after_mat_amt = {} + new_item.after_mat_amt.Qty = 0 + + -- material amount used? + if typesThatUseMaterial[df.item_type[stack_type.type_id]] then + new_item.before_mat_amt.Leather = item.material_amount.Leather + new_item.before_mat_amt.Bone = item.material_amount.Bone + new_item.before_mat_amt.Shell = item.material_amount.Shell + new_item.before_mat_amt.Tooth = item.material_amount.Tooth + new_item.before_mat_amt.Horn = item.material_amount.Horn + new_item.before_mat_amt.HairWool = item.material_amount.HairWool + new_item.before_mat_amt.Yarn = item.material_amount.Yarn + for _, v in pairs(new_item.before_mat_amt) do if new_item.before_mat_amt.Qty < v then new_item.before_mat_amt.Qty = v end end + + comp_item.material_amt = comp_item.material_amt + new_item.before_mat_amt.Qty + if new_item.before_mat_amt.Qty > comp_item.max_mat_amt then comp_item.max_mat_amt = new_item.before_mat_amt.Qty end + end + + -- item is in a container + if container then + new_item.before_cont_id = container.id + comp_item.before_cont_ids[container.id] = container.id + end + + comp_item.items[item.id] = new_item + return comp_item.items[item.id] + else + -- this case should not happen, unless an item id is duplicated. + -- in which case, only allow one instance for the merge. + return nil + end +end + +local function stack_type_new(type_vals) + -- create a new stack type entry to be added to the stacks table. + local stack_type = {} + + -- attributes from the type val table + for k,v in pairs(type_vals) do + stack_type[k] = v + end + + -- item info + stack_type.comp_items = CList:new() -- key:comp_key, val:comp_item + stack_type.item_qty = 0 -- total quantity of items types + stack_type.material_amt = 0 -- total amount of materials + stack_type.before_stacks = 0 -- the number of stacks of the item types before ... + stack_type.after_stacks = 0 -- ...and after the merge + + --container info + stack_type.before_cont_ids = CList:new() -- key:container.id, val:container.id + stack_type.after_cont_ids = CList:new() -- key:container.id, val:container.id + return stack_type +end + +local function isDye(item) + -- Dyes should not be combined as this will cause bugs when mixing them together + if item:getType() ~= df.item_type.POWDER_MISC then return false end + -- pcall guards items/materials that can't be decoded or lack the flag + local ok, is_dye = pcall(function() + local mat = dfhack.matinfo.decode(item.mat_type, item.mat_index) + return mat and mat.material.flags.IS_DYE or false + end) + return ok and is_dye or false +end + +local function stacks_add_item(stockpile, stacks, stack_type, item, container) + -- add an item to the matching comp_items table; based on comp_key. + local comp_key = '' + + if typesThatUseCreatures[df.item_type[stack_type.type_id]] then + if not typesThatUseMaterial[df.item_type[stack_type.type_id]] then + comp_key = ('%s+%s+%s'):format(stack_type.type_id, item.race, item.caste) + else + comp_key = ('%s+%s+%s+%s+%s'):format(stack_type.type_id, item.race, item.caste, item:getActualMaterial(), item:getActualMaterialIndex()) + end + elseif item:isCrafted() then + if item:getQuality() == df.item_quality.Masterful then + comp_key = ('%s+%s+%s+%s+%s'):format(stack_type.type_id, item.mat_type, item.mat_index, item:getQuality(), item:getMaker()) + else + comp_key = ('%s+%s+%s+%s'):format(stack_type.type_id, item.mat_type, item.mat_index, item:getQuality()) + end + else + comp_key = ('%s+%s+%s'):format(stack_type.type_id, item.mat_type, item.mat_index) + end + + if not stack_type.comp_items[comp_key] then + stack_type.comp_items[comp_key] = comp_item_new(comp_key, stack_type) + end + + local new_comp_item_item = comp_item_add_item(stockpile, stack_type, stack_type.comp_items[comp_key], item, container) + if new_comp_item_item then + stack_type.before_stacks = stack_type.before_stacks + 1 + stack_type.item_qty = stack_type.item_qty + item.stack_size + stack_type.material_amt = stack_type.material_amt + new_comp_item_item.before_mat_amt.Qty + + stacks.before_stacks = stacks.before_stacks + 1 + stacks.item_qty = stacks.item_qty + item.stack_size + stacks.material_amt = stacks.material_amt + new_comp_item_item.before_mat_amt.Qty + + if item.stack_size > stack_type.max_stack_qty then + stack_type.max_stack_qty = item.stack_size + end + + -- item is in a container + if container then + + -- add it to the stack type list + stack_type.before_cont_ids[container.id] = container.id + + -- add it to the before stacks container list + stacks.before_cont_ids[container.id] = container.id + end + end +end + +local function sorted_items_qty(tab) + -- used to sort the comp_items by contained, then size. Important for combining containers. + local sorted = {} + for id, val in pairs(tab) do + table.insert(sorted, { + id=id, + before_cont_id=val.before_cont_id, + before_size=val.before_size, + }) + end + + table.sort(sorted, + function(a, b) + if not a.before_cont_id and not b.before_cont_id or a.before_cont_id and b.before_cont_id then + return a.before_size > b.before_size + else + return a.before_cont_id and not b.before_cont_id + end + end + ) + + local i = 0 + local iter = + function() + i = i + 1 + if sorted[i] == nil then + return nil + else + return sorted[i].id, tab[sorted[i].id] + end + end + return iter +end + +local function sorted_items_mat(tab) + -- used to sort the comp_items by mat amt. + local sorted = {} + for id, val in pairs(tab) do + table.insert(sorted, { + id=id, + before_qty=val.before_mat_amt.Qty, + }) + end + + table.sort(sorted, + function(a, b) + return a.before_qty > b.before_qty + end + ) + + local i = 0 + local iter = + function() + i = i + 1 + if sorted[i] == nil then + return nil + else + return sorted[i].id, tab[sorted[i].id] + end + end + return iter +end + +local function sorted_desc(tab, ids) + -- used to sort the lists by description + local sorted = {} + for id, val in pairs(tab) do + if ids[id] then + table.insert(sorted, { + id=id, + description=val.description, + }) + end + end + + table.sort(sorted, function(a, b) return a.description < b.description end) + + local i = 0 + local iter = + function() + i = i + 1 + if sorted[i] == nil then + return nil + else + return sorted[i].id, tab[sorted[i].id] + end + end + return iter +end + +local function print_stacks_details(stacks, quiet) + -- print stacks details + if quiet then return end + if #stacks.containers > 0 then + log(1, 'Summary:') + log(1, 'Containers:%5d before:%5d after:%5d', #stacks.containers, #stacks.before_cont_ids, #stacks.after_cont_ids) + for cont_id, cont in sorted_desc(stacks.containers, stacks.before_cont_ids) do + log(2, (' Cont: %50s <%6d> bef:%5d aft:%5d'):format(cont.description, cont_id, cont.before_vol, cont.after_vol)) + end + end + if stacks.item_qty > 0 then + log(1, ('Items: #Qty: %6d sizes: bef:%5d aft:%5d Mat amt:%6d'):format(stacks.item_qty, stacks.before_stacks, stacks.after_stacks, stacks.material_amt)) + for key, stack_type in pairs(stacks.stack_types) do + if stack_type.item_qty > 0 then + log(1, (' Type: %12s <%d> #Qty:%6d sizes: max:%5d bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d'):format( + df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_stack_qty, stack_type.before_stacks, + stack_type.after_stacks, #stack_type.before_cont_ids, #stack_type.after_cont_ids, stack_type.material_amt)) + for _, comp_item in sorted_desc(stack_type.comp_items, stack_type.comp_items) do + if comp_item.item_qty > 0 then + log(2, (' Comp item:%40s <%12s> #Qty:%6d #stacks:%5d max:%5d bef:%6d aft:%6d Cont: bef:%5d aft:%5d Mat amt:%6d'):format( + comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_stack_qty, comp_item.before_stacks, + comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids, comp_item.material_amt)) + for _, item in sorted_items_qty(comp_item.items) do + log(3, (' Item:%40s <%6d> Qty: bef:%6d aft:%6d Cont: bef:<%5d> aft:<%5d> Mat Amt: bef: %6d aft:%6d stockpile:%s'):format( + utils.getItemDescription(item.item), item.item.id, item.before_size or 0, item.after_size or 0, item.before_cont_id or 0, + item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0, item.stockpile_name)) + log(4, (' stackable: %s'):format(df.item_type.attrs[stack_type.type_id].is_stackable)) + end + end + end + end + end + end +end + +local function print_stacks_summary(stacks, quiet, dry_run) + -- print stacks summary to the console + local printed = 0 + for _, s in pairs(stacks.stack_types) do + if s.before_stacks ~= s.after_stacks then + printed = printed + 1 + local str = '' + if dry_run then str = 'will combine' else str ='combined' end + print(('%s %d %s items from %d stacks into %d') + :format(str, s.item_qty, df.item_type[s.type_id], s.before_stacks, s.after_stacks)) + end + end + if printed == 0 and not quiet then + print('All stacks already optimally combined.') + end +end + +local function stacks_new() + local stacks = {} + + stacks.stack_types = CList:new() -- key=type_id, val=stack_type + stacks.containers = CList:new() -- key=container.id, val={container, description, before_vol, after_vol} + stacks.before_cont_ids = CList:new() -- key=container.id, val=container.id + stacks.after_cont_ids = CList:new() -- key=container.id, val=container.id + stacks.item_qty = 0 + stacks.material_amt = 0 -- total amount of materials - used for CORPSEPIECEs + stacks.before_stacks = 0 + stacks.after_stacks = 0 + + return stacks +end + +local function isRestrictedItem(item) + -- is the item restricted from merging? + local flags = item.flags + return flags.rotten or flags.trader or flags.hostile or flags.forbid + or flags.dump or flags.on_fire or flags.garbage_collect or flags.owned + or flags.removed or flags.encased or flags.spider_web or flags.melt + or #item.specific_refs > 0 +end + +local function isValidPart(item) + return item:getMaterial() >= 0 or + (not item.corpse_flags.unbutchered and ( + item.material_amount.Leather > 0 or + item.material_amount.Bone > 0 or + item.material_amount.Shell > 0 or + item.material_amount.Tooth > 0 or + item.material_amount.Horn > 0 or + item.material_amount.HairWool > 0 or + item.material_amount.Yarn > 0)) +end + +local function getCapacity(container, item) + if item:getType() == df.item_type.DRINK then + -- artificially reduce the capacity of barrels for drinks since 100 is just too many + return 60 * valid_types_map.drink[df.item_type.DRINK].max_stack_qty + end + return dfhack.items.getCapacity(container) +end + +local function getVolume(items) + local vol = 0 + for _, item in ipairs(items) do + vol = vol + item:getVolume() + end + return vol +end + +local function stacks_add_items(stockpile, stacks, items, container, ind) +-- loop through each item and add it to the matching stack[type_id].comp_items table +-- recursively calls itself to add contained items + if not ind then ind = '' end + + for _, item in pairs(items) do + local type_id = item:getType() + local stack_type = stacks.stack_types[type_id] + + -- item type in list of included types? + if stack_type and not item:isSand() and not item:isPlaster() and not isDye(item) and isValidPart(item) then + if not isRestrictedItem(item) and item.stack_size <= stack_type.max_stack_qty then + + stacks_add_item(stockpile, stacks, stack_type, item, container) + + if typesThatUseCreatures[df.item_type[type_id]] then + local raceRaw = df.global.world.raws.creatures.all[item.race] + local casteRaw = raceRaw.caste[item.caste] + log(4, (' %sitem:%40s <%6d> is incl, type:%d, race:%s, caste:%s'):format( + ind, utils.getItemDescription(item), item.id, type_id, raceRaw.creature_id, casteRaw.caste_id)) + elseif item:isCrafted() then + local mat_info = dfhack.matinfo.decode(item.mat_type, item.mat_index) + log(4, (' %sitem:%40s <%6d> is incl, type:%d, info:%s, quality:%d, maker:%d'):format( + ind, utils.getItemDescription(item), item.id, type_id, mat_info:toString(), item:getQuality(), item:getMaker())) + else + local mat_info = dfhack.matinfo.decode(item.mat_type, item.mat_index) + log(4, (' %sitem:%40s <%6d> is incl, type:%d, info:%s, sand:%s, plasterplaster:%s quality:%d ovl quality:%d'):format( + ind, utils.getItemDescription(item), item.id, type_id, mat_info:toString(), item:isSand(), item:isPlaster(), + item:getQuality(), item:getOverallQuality())) + end + + else + -- restricted; such as marked for action or dump. + log(4, (' %sitem:%40s <%6d> is restricted'):format(ind, utils.getItemDescription(item), item.id)) + end + + -- add contained items + elseif dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINS_ITEM) then + local contained_items = dfhack.items.getContainedItems(item) + local count = #contained_items + local volume = getVolume(contained_items) + stacks.containers[item.id] = {} + stacks.containers[item.id].container = item + stacks.containers[item.id].before_vol = volume + stacks.containers[item.id].description = utils.getItemDescription(item, 1) + log(4, (' %sContainer:%s <%6d> #items:%5d volume:%5d'):format( + ind, utils.getItemDescription(item), item.id, count, volume)) + stacks_add_items(stockpile, stacks, contained_items, item, ind .. ' ') + + -- excluded item types + else + log(5, (' %sitem:%40s <%6d> is excl, type %d, sand:%s plaster:%s'):format( + ind, utils.getItemDescription(item), item.id, type_id, item:isSand(), item:isPlaster())) + end + end +end + +local function populate_stacks(stacks, stockpiles, types) + -- 1. loop through the specified types and add them to the stacks table. stacks[type_id] + -- 2. loop through the table of stockpiles, get each item in the stockpile, then add them to stacks if the type_id matches + -- an item is stored at the bottom of the structure: stacks[type_id].comp_items[comp_key].item + -- comp_key is a compound key comprised of type_id+race+caste or type_id+mat_type+mat_index + log(4, 'Populating phase') + + -- iterate across the types + log(4, 'stack types') + for type_id, type_vals in pairs(types) do + if not stacks.stack_types[type_id] then + stacks.stack_types[type_id] = stack_type_new(type_vals) + local stack_type = stacks.stack_types[type_id] + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d bef:%5d aft:%5d'):format( + df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_stack_qty, + stack_type.before_stacks, stack_type.after_stacks)) + end + end + + -- iterate across the stockpiles, get the list of items and call the add function to check/add as needed + log(4, ('stockpiles')) + for _, stockpile in ipairs(stockpiles) do + + local items = dfhack.buildings.getStockpileContents(stockpile) + log(4, (' stockpile:%30s <%6d> pos:(%3d,%3d,%3d) #items:%5d'):format( + stockpile.name, stockpile.id, stockpile.centerx, stockpile.centery, stockpile.z, #items)) + + if #items > 0 then + stacks_add_items(stockpile, stacks, items) + else + log(4, ' skipping stockpile: no items') + end + end +end + +local function preview_stacks(stacks) + -- calculate the stacks sizes and store in after_item_stack_size + -- the max stack size for each comp item is determined as the maximum stack size for its type + log(4, 'Preview phase') + + for _, stack_type in pairs(stacks.stack_types) do + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d bef:%5d aft:%5d'):format( + df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_stack_qty, + stack_type.before_stacks, stack_type.after_stacks)) + + for _, comp_item in pairs(stack_type.comp_items) do + log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d bef:%5d aft:%5d Cont: bef:%5d aft:%5d'):format( + comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_stack_qty, + comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + + -- item qty used? + if not typesThatUseMaterial[df.item_type[stack_type.type_id]] then + + -- max size comparison + if stack_type.max_stack_qty > comp_item.max_stack_qty then + comp_item.max_stack_qty = stack_type.max_stack_qty + end + + -- how many stacks are needed? + local stacks_needed = comp_item.item_qty // comp_item.max_stack_qty + + -- how many items are left over after the max stacks are allocated? + local stack_remainder = comp_item.item_qty - stacks_needed * comp_item.max_stack_qty + + if stack_remainder > 0 then + comp_item.after_stacks = stacks_needed + 1 + else + comp_item.after_stacks = stacks_needed + end + + stack_type.after_stacks = stack_type.after_stacks + comp_item.after_stacks + stacks.after_stacks = stacks.after_stacks + comp_item.after_stacks + + -- Update the after stack sizes. + for _, item in sorted_items_qty(comp_item.items) do + if stacks_needed > 0 then + stacks_needed = stacks_needed - 1 + item.after_size = comp_item.max_stack_qty + elseif stack_remainder > 0 then + item.after_size = stack_remainder + stack_remainder = 0 + else + item.after_size = 0 + end + end + + -- material amount used. + else + local stacks_needed = comp_item.material_amt // comp_item.max_mat_amt + local stack_remainder = comp_item.material_amt - stacks_needed * comp_item.max_mat_amt + + if stack_remainder > 0 then + comp_item.after_stacks = stacks_needed + 1 + else + comp_item.after_stacks = stacks_needed + end + + stack_type.after_stacks = stack_type.after_stacks + comp_item.after_stacks + stacks.after_stacks = stacks.after_stacks + comp_item.after_stacks + + for _, item in sorted_items_mat(comp_item.items) do + item.after_mat_amt = {} + if stacks_needed > 0 then + stacks_needed = stacks_needed - 1 + item.after_size = item.before_size + for k2, v in pairs(item.before_mat_amt) do + if v > 0 then + item.after_mat_amt[k2] = comp_item.max_mat_amt + else + item.after_mat_amt[k2] = 0 + end + end + elseif stack_remainder > 0 then + item.after_size = item.before_size + for k2, v in pairs(item.before_mat_amt) do + if v > 0 then + item.after_mat_amt[k2] = stack_remainder + else + item.after_mat_amt[k2] = 0 + end + end + stack_remainder = 0 + else + for k2, v in pairs(item.before_mat_amt) do + item.after_mat_amt[k2] = 0 + end + item.after_size = 0 + end + end + end + + -- Container loop; combine item stacks in containers. + local curr_cont = nil + local curr_cap = nil + local curr_vol = 0 + + for _, item in sorted_items_qty(comp_item.items) do + local vol = item.item:getVolume() + + -- non-zero quantity? + if item.after_size > 0 then + -- in a container before merge? + if item.before_cont_id then + local before_cont = stacks.containers[item.before_cont_id] + + -- first contained item or current container full? + if not curr_cont or curr_vol + vol > curr_cap then + curr_cont = before_cont + curr_cap = getCapacity(curr_cont.container, item.item) + curr_vol = curr_cont.before_vol + stacks.after_cont_ids[item.before_cont_id] = item.before_cont_id + stack_type.after_cont_ids[item.before_cont_id] = item.before_cont_id + comp_item.after_cont_ids[item.before_cont_id] = item.before_cont_id + + -- enough room in current container + else + curr_vol = curr_vol + vol + before_cont.after_vol = (before_cont.after_vol or before_cont.before_vol) - vol + end + + curr_cont.after_vol = curr_vol + item.after_cont_id = curr_cont.container.id + + -- not in a container before merge, container exists, and has space + elseif curr_cont and curr_vol + vol <= curr_cap then + curr_vol = curr_vol + vol + curr_cont.after_vol = curr_vol + item.after_cont_id = curr_cont.container.id + + -- not in a container, no container exists or no space in container + else + -- do nothing + end + + -- zero after size, reduce the number of stacks in the container + elseif item.before_cont_id then + local before_cont = stacks.containers[item.before_cont_id] + before_cont.after_vol = (before_cont.after_vol or before_cont.before_vol) - vol + end + end + log(4, (' comp item:%40s <%12s> #qty:%5d #stacks:%5d sizes: max:%5d bef:%5d aft:%5d cont: bef:%5d aft:%5d'):format( + comp_item.description, comp_item.comp_key, comp_item.item_qty, #comp_item.items, comp_item.max_stack_qty, + comp_item.before_stacks, comp_item.after_stacks, #comp_item.before_cont_ids, #comp_item.after_cont_ids)) + end + log(4, (' type: <%12s> <%d> #item_qty:%5d stack sizes: max: %5d bef:%5d aft:%5d'):format( + df.item_type[stack_type.type_id], stack_type.type_id, stack_type.item_qty, stack_type.max_stack_qty, + stack_type.before_stacks, stack_type.after_stacks)) + end +end + +local function merge_stacks(stacks) + -- apply the stack size changes in the after_item_stack_size + -- if the after_item_stack_size is zero, then remove the item + log(4, 'Merge phase') + for _, stack_type in pairs(stacks.stack_types) do + for comp_key, comp_item in pairs(stack_type.comp_items) do + + for item_id, item in pairs(comp_item.items) do + log(4, (' item amt:%40s <%6d> bef:%5d aft:%5d cont: bef:<%5d> aft:<%5d> mat: bef:%5d aft:%5d'):format( + comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, + item.before_cont_id or 0, item.after_cont_id or 0, item.before_mat_amt.Qty or 0, item.after_mat_amt.Qty or 0)) + + -- no items left in stack? + if item.after_size == 0 then + log(4, ' removing') + dfhack.items.remove(item.item) + + -- some items left in stack + elseif not typesThatUseMaterial[df.item_type[stack_type.type_id]] and item.before_size ~= item.after_size then + log(4, ' updating qty') + item.item.stack_size = item.after_size + + elseif typesThatUseMaterial[df.item_type[stack_type.type_id]] and item.before_mat_amt.Qty ~= item.after_mat_amt.Qty then + log(4, ' updating material') + item.item.material_amount.Leather = item.after_mat_amt.Leather + item.item.material_amount.Bone = item.after_mat_amt.Bone + item.item.material_amount.Shell = item.after_mat_amt.Shell + item.item.material_amount.Tooth = item.after_mat_amt.Tooth + item.item.material_amount.Horn = item.after_mat_amt.Horn + item.item.material_amount.HairWool = item.after_mat_amt.HairWool + item.item.material_amount.Yarn = item.after_mat_amt.Yarn + else + log(4, ' no change') + end + + -- move to a container? + if item.after_cont_id then + if (item.before_cont_id or 0) ~= item.after_cont_id then + log(4, (' moving item:%40s <%6d> bef:%5d aft:%5d cont: bef:<%5d> aft:<%5d>'):format( + comp_item.description, item.item.id, item.before_size or 0, item.after_size or 0, + item.before_cont_id or 0, item.after_cont_id or 0)) + dfhack.items.moveToContainer(item.item, stacks.containers[item.after_cont_id].container) + end + end + end + end + end +end + +local function get_stockpile_all() + -- returns the stockpiles vector + local stockpiles = df.global.world.buildings.other.STOCKPILE + if opts.verbose > 0 then + print(('Stockpile(all): %d found'):format(#stockpiles)) + end + return stockpiles +end + +local function get_stockpile_here() + -- attempt to get the selected stockpile, or exit with error + -- return the stockpile as a table + local stockpiles = {} + local building = dfhack.gui.getSelectedStockpile(true) + + -- try finding the stockpile by viewed item or first item in itemlist viewsheet. + if building == nil then + local item = nil + if dfhack.gui.getSelectedItem(true) ~= nil then + item = dfhack.gui.getSelectedItem(true) + elseif tonumber(dfhack.DF_VERSION:match("^0*%.*(%d+%.%d+)")) >= 50.07 -- matchFocusString() in Commit a770a4c + and dfhack.gui.matchFocusString("dwarfmode/ViewSheets/ITEM_LIST", dfhack.gui.getDFViewscreen()) + and df.global.game.main_interface.view_sheets.open == true + and df.global.game.main_interface.view_sheets.active_sheet == df.view_sheet_type.ITEM_LIST + and #df.global.game.main_interface.view_sheets.viewing_itid > 0 + then + local itemid = df.global.game.main_interface.view_sheets.viewing_itid[0] + item = df.item.find(itemid) + end + local pos = (item) and xyz2pos(dfhack.items.getPosition(item)) or nil + building = (pos) and dfhack.buildings.findAtTile(pos) or nil + building = (df.building_stockpilest:is_instance(building)) and building or nil + end + + if not building then qerror('Please select a stockpile.') end + table.insert(stockpiles, building) + if opts.verbose > 0 then + print(('Stockpile(here): %s <%d> #items:%d'):format(building.name, building.id, + #dfhack.buildings.getStockpileContents(building))) + end + return stockpiles +end + +local function parse_types_opts(arg) + -- check the types specified on the command line, or exit with error + -- return the selected types as a table + local types = {} + local div = '' + local types_output = '' + + if not arg then + qerror('Expected: comma separated list of types') + end + + for _, t in pairs(argparse.stringList(arg)) do + if not valid_types_map[t] then + qerror(('Unknown type: %s'):format(t)) + end + + for k2, v2 in pairs(valid_types_map[t]) do + if not types[k2] then + types[k2]={} + for k3, v3 in pairs(v2) do + types[k2][k3]=v3 + end + types_output = types_output .. div .. df.item_type[types[k2].type_id] + div=', ' + else + qerror(('Expected: only one value for %s'):format(t)) + end + end + end + return types +end + +local function parse_commandline(opts, args) + -- check the command line/exit on error, and set the defaults + local positionals = argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() opts.help = true end}, + {'t', 'types', hasArg=true, handler=function(optarg) opts.types=parse_types_opts(optarg) end}, + {'d', 'dry-run', handler=function() opts.dry_run = true end}, + {'q', 'quiet', handler=function() opts.quiet = true end}, + {'v', 'verbose', hasArg=true, handler=function(optarg) opts.verbose = math.tointeger(optarg) or 0 end}, + }) + + -- if stockpile option is not specificed, then default to all + if positionals[1] == 'all' then + opts.all=get_stockpile_all() + elseif positionals[1] == 'here' then + opts.here=get_stockpile_here() + else + opts.help = true + end + + -- if types option is not specified, then default to all + if not opts.types then + opts.types = valid_types_map['all'] + end +end + +-- main program starts here +local function main() + + if df.global.gamemode ~= df.game_mode.DWARF or not dfhack.isMapLoaded() then + qerror('combine needs a loaded fortress map to work') + end + + parse_commandline(opts, args) + + if opts.help then + print(dfhack.script_help()) + return + end + + local stacks = stacks_new() + + populate_stacks(stacks, opts.all or opts.here, opts.types) + + preview_stacks(stacks) + + if not opts.dry_run then + merge_stacks(stacks) + end + + print_stacks_details(stacks) + print_stacks_summary(stacks, opts.quiet, opts.dry_run) + +end + +if not dfhack_flags.module then + main() +end diff --git a/confirm.lua b/confirm.lua new file mode 100644 index 0000000000..5142013d1f --- /dev/null +++ b/confirm.lua @@ -0,0 +1,210 @@ +--@ module = true + +local dialogs = require('gui.dialogs') +local gui = require('gui') +local overlay = require('plugins.overlay') +local specs = reqscript('internal/confirm/specs') +local utils = require('utils') +local widgets = require("gui.widgets") + +------------------------ +-- API + +function get_state() + return specs.config.data +end + +function set_enabled(id, enabled) + for _, conf in pairs(specs.config.data) do + if conf.id == id then + if conf.enabled ~= enabled then + conf.enabled = enabled + specs.config:write() + end + break + end + end +end + +------------------------ +-- Overlay + +local function get_contexts() + local contexts, contexts_set = {}, {} + for id, conf in pairs(specs.REGISTRY) do + if not contexts_set[id] then + contexts_set[id] = true + table.insert(contexts, conf.context) + end + end + return contexts +end + +ConfirmOverlay = defclass(ConfirmOverlay, overlay.OverlayWidget) +ConfirmOverlay.ATTRS{ + desc='Detects dangerous actions and prompts with confirmation dialogs.', + default_pos={x=1,y=1}, + default_enabled=true, + full_interface=true, -- not player-repositionable + hotspot=true, -- need to reset pause when we're not in target contexts + overlay_onupdate_max_freq_seconds=300, + viewscreens=get_contexts(), +} + +function ConfirmOverlay:init() + for id, conf in pairs(specs.REGISTRY) do + if conf.intercept_frame then + self:addviews{ + widgets.Panel{ + view_id=id, + frame=copyall(conf.intercept_frame), + frame_style=conf.debug_frame and gui.FRAME_INTERIOR or nil, + } + } + end + end + self.paused_confs = {} +end + +function ConfirmOverlay:preUpdateLayout() + local interface_rect = gui.get_interface_rect() + self.frame.w, self.frame.h = interface_rect.width, interface_rect.height + -- reset frames if any of them have been pushed out of position + for id, conf in pairs(specs.REGISTRY) do + if conf.intercept_frame then + self.subviews[id].frame = copyall(conf.intercept_frame) + end + end +end + +function ConfirmOverlay:overlay_onupdate() + for conf in pairs(self.paused_confs) do + if not dfhack.gui.matchFocusString(conf.context, + dfhack.gui.getDFViewscreen(true)) + then + self.paused_confs[conf] = nil + end + end + if not next(self.paused_confs) then + self.overlay_onupdate_max_freq_seconds = 300 + end +end + +function ConfirmOverlay:matches_conf(conf, keys, scr) + local matched_keys = false + for _, key in ipairs(conf.intercept_keys) do + if keys[key] then + matched_keys = true + break + end + end + if not matched_keys then return false end + local mouse_offset + if keys._MOUSE_L and conf.intercept_frame then + local mousex, mousey = self.subviews[conf.id]:getMouseFramePos() + if not mousex then + return false + end + mouse_offset = xy2pos(mousex, mousey) + end + if not dfhack.gui.matchFocusString(conf.context, scr) then return false end + return not conf.predicate or conf.predicate(keys, mouse_offset) +end + +function ConfirmOverlay:onInput(keys) + if self.simulating then + return false + end + local scr = dfhack.gui.getDFViewscreen(true) + for id, conf in pairs(specs.REGISTRY) do + if specs.config.data[id].enabled and self:matches_conf(conf, keys, scr) then + if self.paused_confs[conf] then + return false + end + local mouse_pos = xy2pos(dfhack.screen.getMousePos()) + local propagate_fn = function(pause) + if conf.on_propagate then + conf.on_propagate() + end + if pause then + self.paused_confs[conf] = true + self.overlay_onupdate_max_freq_seconds = 0 + end + if keys._MOUSE_L then + df.global.gps.mouse_x = mouse_pos.x + df.global.gps.mouse_y = mouse_pos.y + end + self.simulating = true + gui.simulateInput(scr, keys) + self.simulating = false + end + local pause_fn = conf.pausable and curry(propagate_fn, true) or nil + dialogs.showYesNoPrompt(conf.title, utils.getval(conf.message):wrap(45), COLOR_YELLOW, + propagate_fn, nil, pause_fn, curry(dfhack.run_script, 'gui/confirm', tostring(conf.id))) + return true + end + end +end + +function ConfirmOverlay:render(dc) + if gui.blink_visible(500) then + return + end + ConfirmOverlay.super.render(self, dc) +end + +OVERLAY_WIDGETS = { + overlay=ConfirmOverlay, +} + +------------------------ +-- CLI + +local function do_list() + print('Available confirmation prompts:') + local confs, max_len = {}, 10 + for id, conf in pairs(specs.REGISTRY) do + max_len = math.max(max_len, #id) + table.insert(confs, conf) + end + table.sort(confs, function(a,b) return a.id < b.id end) + for _, conf in ipairs(confs) do + local fmt = '%' .. tostring(max_len) .. 's: %s %s' + print((fmt):format(conf.id, + specs.config.data[conf.id].enabled and '(enabled) ' or '(disabled)', + conf.title)) + end +end + +local function do_enable_disable(args, enable) + if args[1] == 'all' then + for id in pairs(specs.REGISTRY) do + set_enabled(id, enable) + end + else + for _, id in ipairs(args) do + if not specs.REGISTRY[id] then + qerror('confirmation prompt id not found: ' .. tostring(id)) + end + set_enabled(id, enable) + end + end +end + +local function main(args) + local command = table.remove(args, 1) + + if not command or command == 'list' then + do_list() + elseif command == 'enable' or command == 'disable' then + do_enable_disable(args, command == 'enable') + elseif command == 'help' then + print(dfhack.script_help()) + else + dfhack.printerr('unknown command: ' .. tostring(command)) + end +end + +if not dfhack_flags.module then + main{...} +end diff --git a/control-panel.lua b/control-panel.lua new file mode 100644 index 0000000000..7579db0261 --- /dev/null +++ b/control-panel.lua @@ -0,0 +1,242 @@ +--@module = true + +local argparse = require('argparse') +local common = reqscript('internal/control-panel/common') +local registry = reqscript('internal/control-panel/registry') +local utils = require('utils') + +local GLOBAL_KEY = 'control-panel' + +-- state change hooks + +local function apply_system_config() + local enabled_map = common.get_enabled_map() + for _, data in ipairs(registry.COMMANDS_BY_IDX) do + if data.mode == 'system_enable' or data.mode == 'tweak' then + common.apply_command(data, enabled_map) + end + end + for _, data in ipairs(registry.PREFERENCES_BY_IDX) do + local value = safe_index(common.config.data.preferences, data.name, 'val') + if value ~= nil then + data.set_fn(value) + end + end +end + +local function apply_autostart_config() + local enabled_map =common.get_enabled_map() + for _, data in ipairs(registry.COMMANDS_BY_IDX) do + if data.mode == 'enable' or data.mode == 'run' or data.mode == 'repeat' then + common.apply_command(data, enabled_map) + end + end +end + +local function apply_fort_loaded_config() + local state = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) + if not state.autostart_done then + apply_autostart_config() + dfhack.persistent.saveSiteData(GLOBAL_KEY, {autostart_done=true}) + end + local enabled_map = common.get_enabled_map() + local enabled_repeats = dfhack.persistent.getSiteData(common.REPEATS_GLOBAL_KEY, {}) + for _, data in ipairs(registry.COMMANDS_BY_IDX) do + if data.mode == 'repeat' then + common.apply_command(data, enabled_map, enabled_repeats[data.command]) + end + end +end + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_CORE_INITIALIZED then + apply_system_config() + elseif sc == SC_MAP_LOADED and dfhack.world.isFortressMode() then + apply_fort_loaded_config() + end +end + +local function get_command_data(name_or_idx) + if type(name_or_idx) == 'number' then + return registry.COMMANDS_BY_IDX[name_or_idx] + end + return registry.COMMANDS_BY_NAME[name_or_idx] +end + +local function get_autostart_internal(data) + local default_value = not not data.default + local current_value = safe_index(common.config.data.commands, data.command, 'autostart') + if current_value == nil then + current_value = default_value + end + return current_value, default_value +end + +-- API + +-- returns current, default +function get_autostart(command) + local data = get_command_data(command) + if not data then return end + return get_autostart_internal(data) +end + +-- CLI + +local function print_header(header) + print() + print(header) + print(('-'):rep(#header)) +end + +local function list_command_group(group, filter_strs, enabled_map) + local header = ('Group: %s'):format(group) + for idx, data in ipairs(registry.COMMANDS_BY_IDX) do + if not common.command_passes_filters(data, group, filter_strs) then + goto continue + end + if header then + print_header(header) + ---@diagnostic disable-next-line: cast-local-type + header = nil + end + local extra = '' + if data.mode == 'system_enable' or data.mode == 'tweak' then + extra = ' (global)' + end + print(('%d) %s%s'):format(idx, data.command, extra)) + local desc = common.get_description(data) + if #desc > 0 then + print((' %s'):format(desc)) + end + print((' autostart enabled: %s (default: %s)'):format(get_autostart_internal(data))) + if enabled_map[data.command] ~= nil then + print((' currently enabled: %s'):format(enabled_map[data.command])) + end + print() + ::continue:: + end + if not header then + end +end + +local function list_preferences(filter_strs) + local header = 'Preferences' + for _, data in ipairs(registry.PREFERENCES_BY_IDX) do + local search_key = ('%s %s %s'):format(data.name, data.label, data.desc) + if not utils.search_text(search_key, filter_strs) then goto continue end + if header then + print_header(header) + ---@diagnostic disable-next-line: cast-local-type + header = nil + end + print(('%s) %s'):format(data.name, data.label)) + print((' %s'):format(data.desc)) + print((' current: %s (default: %s)'):format(data.get_fn(), data.default)) + if data.min then + print((' minimum: %s'):format(data.min)) + end + print() + ::continue:: + end +end + +local function do_list(filter_strs) + local enabled_map = common.get_enabled_map() + list_command_group('automation', filter_strs, enabled_map) + list_command_group('bugfix', filter_strs, enabled_map) + list_command_group('gameplay', filter_strs, enabled_map) + list_preferences(filter_strs) +end + +local function do_enable_disable(which, entries) + local enabled_map =common.get_enabled_map() + for _, entry in ipairs(entries) do + local data = get_command_data(entry) + if data.mode ~= 'system_enable' and not dfhack.world.isFortressMode() then + qerror('must have a loaded fortress to enable '..data.name) + end + if common.apply_command(data, enabled_map, which == 'en') then + print(('%sabled %s'):format(which, entry)) + end + end +end + +local function do_enable(entries) + do_enable_disable('en', entries) +end + +local function do_disable(entries) + do_enable_disable('dis', entries) +end + +local function do_autostart_noautostart(which, entries) + for _, entry in ipairs(entries) do + local data = get_command_data(entry) + if not data then + qerror(('autostart command or index not found: "%s"'):format(entry)) + else + common.set_autostart(data, which == 'en') + print(('%sabled autostart for: %s'):format(which, entry)) + end + end + common.config:write() +end + +local function do_autostart(entries) + do_autostart_noautostart('en', entries) +end + +local function do_noautostart(entries) + do_autostart_noautostart('dis', entries) +end + +local function do_set(params) + local name, value = params[1], params[2] + local data = registry.PREFERENCES_BY_NAME[name] + if not data then + qerror(('preference name not found: "%s"'):format(name)) + end + common.set_preference(data, value) + common.config:write() +end + +local function do_reset(params) + local name = params[1] + local data = registry.PREFERENCES_BY_NAME[name] + if not data then + qerror(('preference name not found: "%s"'):format(name)) + end + common.set_preference(data, data.default) + common.config:write() +end + +local command_switch = { + list=do_list, + enable=do_enable, + disable=do_disable, + autostart=do_autostart, + noautostart=do_noautostart, + set=do_set, + reset=do_reset, +} + +local function main(args) + local help = false + + local positionals = argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() help = true end}, + }) + + local command = table.remove(positionals, 1) + if help or not command or not command_switch[command] then + print(dfhack.script_help()) + return + end + + command_switch[command](positionals) +end + +if not dfhack_flags.module then + main{...} +end diff --git a/create-items.rb b/create-items.rb deleted file mode 100644 index dae0995f33..0000000000 --- a/create-items.rb +++ /dev/null @@ -1,207 +0,0 @@ -# create first necessity items under cursor -=begin - -create-items -============ -Spawn items under the cursor, to get your fortress started. - -The first argument gives the item category, the second gives the material, -and the optional third gives the number of items to create (defaults to 20). - -Currently supported item categories: ``boulder``, ``bar``, ``plant``, ``log``, -``web``. - -Instead of material, using ``list`` makes the script list eligible materials. - -The ``web`` item category will create an uncollected cobweb on the floor. - -Note that the script does not enforce anything, and will let you create -boulders of toad blood and stuff like that. -However the ``list`` mode will only show 'normal' materials. - -Examples:: - - create-items boulders COAL_BITUMINOUS 12 - create-items plant tail_pig - create-items log list - create-items web CREATURE:SPIDER_CAVE_GIANT:SILK - create-items bar CREATURE:CAT:SOAP - create-items bar adamantine - -=end - -category = $script_args[0] || 'help' -mat_raw = $script_args[1] || 'list' -count = $script_args[2] - - -category = df.match_rawname(category, ['help', 'bars', 'boulders', 'plants', 'logs', 'webs', 'anvils']) || 'help' - -if category == 'help' - puts < 5 - df.curview.feed_keys(:CURSOR_DOWN_Z) - df.curview.feed_keys(:CURSOR_UP_Z) -else - df.curview.feed_keys(:CURSOR_UP_Z) - df.curview.feed_keys(:CURSOR_DOWN_Z) -end diff --git a/deathcause.lua b/deathcause.lua new file mode 100644 index 0000000000..3fd62fd115 --- /dev/null +++ b/deathcause.lua @@ -0,0 +1,177 @@ +-- show death cause of a creature +--@ module = true + +local DEATH_TYPES = reqscript('gui/unit-info-viewer').DEATH_TYPES + +-- Gets the first corpse item at the given location +local function getItemAtPosition(pos) + for _, item in ipairs(df.global.world.items.other.ANY_CORPSE) do + -- could this maybe be `if same_xyz(pos, item.pos) then`? + if item.pos.x == pos.x and item.pos.y == pos.y and item.pos.z == pos.z then + print("Automatically chose first corpse at the selected location.") + return item + end + end +end + +local function getRaceNameSingular(race_id) + return df.creature_raw.find(race_id).name[0] +end + +local function getDeathStringFromCause(cause) + if cause == -1 then + return "died" + else + return DEATH_TYPES[cause]:trim() + end +end + +-- Returns a cause of death given a unit +local function getDeathCauseFromUnit(unit) + local str = unit.name.has_name and '' or 'The ' + str = str .. dfhack.units.getReadableName(unit) + + if not dfhack.units.isDead(unit) then + return str .. " is not dead yet!" + end + + str = str .. (" %s"):format(getDeathStringFromCause(unit.counters.death_cause)) + + local incident = df.incident.find(unit.counters.death_id) + if incident then + str = str .. (" in year %d"):format(incident.event_year) + + if incident.criminal then + local killer = df.unit.find(incident.criminal) + if killer then + str = str .. (", killed by the %s"):format(getRaceNameSingular(killer.race)) + if killer.name.has_name then + str = str .. (" %s"):format(dfhack.translation.translateName(dfhack.units.getVisibleName(killer))) + end + end + end + end + + return str .. '.' +end + +-- returns the item description if the item still exists; otherwise +-- returns the weapon name +local function getWeaponName(item_id, subtype) + local item = df.item.find(item_id) + if not item then + return df.global.world.raws.itemdefs.weapons[subtype].name + end + return dfhack.items.getDescription(item, 0, false) +end + +local function getDeathEventHistFigUnit(histfig_unit, event) + local str = ("The %s %s %s in year %d"):format( + getRaceNameSingular(histfig_unit.race), + dfhack.translation.translateName(dfhack.units.getVisibleName(histfig_unit)), + getDeathStringFromCause(event.death_cause), + event.year + ) + + local slayer_histfig = df.historical_figure.find(event.slayer_hf) + if slayer_histfig then + str = str .. (", killed by the %s %s"):format( + getRaceNameSingular(slayer_histfig.race), + dfhack.translation.translateName(dfhack.units.getVisibleName(slayer_histfig)) + ) + end + + if event.weapon then + if event.weapon.item_type == df.item_type.WEAPON then + str = str .. (", using a %s"):format(getWeaponName(event.weapon.item, event.weapon.item_subtype)) + elseif event.weapon.shooter_item_type == df.item_type.WEAPON then + str = str .. (", shot by a %s"):format(getWeaponName(event.weapon.shooter_item, event.weapon.shooter_item_subtype)) + end + end + + return str .. '.' +end + +-- Returns the death event for the given histfig or nil if not found +local function getDeathEventForHistFig(histfig_id) + for i = #df.global.world.history.events - 1, 0, -1 do + local event = df.global.world.history.events[i] + if event:getType() == df.history_event_type.HIST_FIGURE_DIED then + if event.victim_hf == histfig_id then + return event + end + end + end +end + +-- Returns the cause of death given a histfig +local function getDeathCauseFromHistFig(histfig) + local histfig_unit = df.unit.find(histfig.unit_id) + if not histfig_unit then + qerror("Cause of death not available") + end + + if not dfhack.units.isDead(histfig_unit) then + return ("%s is not dead yet!"):format(dfhack.units.getReadableName(histfig_unit)) + else + local death_event = getDeathEventForHistFig(histfig.id) + return getDeathEventHistFigUnit(histfig_unit, death_event) + end +end + +local function is_corpse_item(item) + if not item then return false end + local itype = item:getType() + return itype == df.item_type.CORPSE or itype == df.item_type.CORPSEPIECE +end + +local view_sheets = df.global.game.main_interface.view_sheets + +local function get_target() + local selected_unit = dfhack.gui.getSelectedUnit(true) + if selected_unit then + return selected_unit.hist_figure_id, selected_unit + end + local selected_item = dfhack.gui.getSelectedItem(true) + if not selected_item and + dfhack.gui.matchFocusString('dwarfmode/ViewSheets/ITEM_LIST', dfhack.gui.getDFViewscreen(true)) and + #view_sheets.viewing_itid > 0 + then + local pos = xyz2pos(dfhack.items.getPosition(df.item.find(view_sheets.viewing_itid[0]))) + selected_item = getItemAtPosition(pos) + end + if not is_corpse_item(selected_item) then + if df.item_remainsst:is_instance(selected_item) then + print(("The %s died."):format(getRaceNameSingular(selected_item.race))) + return + end + qerror("Please select a unit, a corpse, or a body part") + end + return selected_item.hist_figure_id, df.unit.find(selected_item.unit_id) +end + +-- wrapper function to take either a unit or a histfig and get the death cause +function getDeathCause(target) + if df.unit:is_instance(target) then + return getDeathCauseFromUnit(target) + else + return getDeathCauseFromHistFig(target) + end +end + +if dfhack_flags.module then + return +end + +local hist_figure_id, selected_unit = get_target() + +if not hist_figure_id then + qerror("Cause of death not available") +elseif hist_figure_id == -1 then + if not selected_unit then + qerror("Cause of death not available") + end + print(dfhack.df2console(getDeathCause(selected_unit))) +else + print(dfhack.df2console(getDeathCause(df.historical_figure.find(hist_figure_id)))) +end diff --git a/deathcause.rb b/deathcause.rb deleted file mode 100644 index c80ddf034c..0000000000 --- a/deathcause.rb +++ /dev/null @@ -1,83 +0,0 @@ -# show death cause of a creature -=begin - -deathcause -========== -Select a body part ingame, or a unit from the :kbd:`u` unit list, and this -script will display the cause of death of the creature. - -=end - -def display_death_event(e) - str = "The #{e.victim_hf_tg.race_tg.name[0]} #{e.victim_hf_tg.name} died in year #{e.year}" - str << " (cause: #{e.death_cause.to_s.downcase})," - str << " killed by the #{e.slayer_race_tg.name[0]} #{e.slayer_hf_tg.name}" if e.slayer_hf != -1 - str << " using a #{df.world.raws.itemdefs.weapons[e.weapon.item_subtype].name}" if e.weapon.item_type == :WEAPON - str << ", shot by a #{df.world.raws.itemdefs.weapons[e.weapon.shooter_item_subtype].name}" if e.weapon.shooter_item_type == :WEAPON - - puts str.chomp(',') + '.' -end - -def display_death_unit(u) - str = "The #{u.race_tg.name[0]}" - str << " #{u.name}" if u.name.has_name - - if not u.flags2.killed and not u.flags3.ghostly - str << " is not dead yet !" - - puts str.chomp(',') - else - death_info = u.counters.death_tg - killer = death_info.criminal_tg if death_info - - str << " died" if !u.flags2.slaughter - str << " was slaughtered" if u.flags2.slaughter - - str << " in year #{death_info.event_year}" if death_info - str << " (cause: #{u.counters.death_cause.to_s.downcase})," if u.counters.death_cause != -1 - str << " killed by the #{killer.race_tg.name[0]} #{killer.name}" if killer - - puts str.chomp(',') + '.' - end -end - -item = df.item_find(:selected) -unit = df.unit_find(:selected) - -if !unit and (!item or !item.kind_of?(DFHack::ItemBodyComponent)) - item = df.world.items.other[:ANY_CORPSE].find { |i| df.at_cursor?(i) } -end - -if item and item.kind_of?(DFHack::ItemBodyComponent) - hf = item.hist_figure_id -elsif unit - hf = unit.hist_figure_id -end - -if not hf - puts "Please select a corpse in the loo'k' menu, or an unit in the 'u'nitlist screen" - -elsif hf == -1 - if unit ||= item.unit_tg - display_death_unit(unit) - else - puts "Not a historical figure, cannot find death info" - end - -else - histfig = df.world.history.figures.binsearch(hf) - unit = histfig ? df.unit_find(histfig.unit_id) : nil - if unit and not unit.flags2.killed and not unit.flags3.ghostly - puts "#{unit.name} is not dead yet !" - - else - events = df.world.history.events - (0...events.length).reverse_each { |i| - e = events[i] - if e.kind_of?(DFHack::HistoryEventHistFigureDiedst) and e.victim_hf == hf - display_death_event(e) - break - end - } - end -end diff --git a/deep-embark.lua b/deep-embark.lua index 3e853cfd4b..6738bd532f 100644 --- a/deep-embark.lua +++ b/deep-embark.lua @@ -1,406 +1,360 @@ --- Embark underground. --- author: Atomic Chicken - --@ module = true -local usage = [====[ - -deep-embark -=========== -Moves the starting units and equipment to -a specific underground region upon embarking. - -This script can be run directly from the console -at any point whilst setting up an embark. - -Alternatively, create a file called "onLoad.init" -in the DF raw folder (if one does not exist already) -and enter the script command within it. Doing so will -cause the script to run automatically and should hence -be especially useful for modders who want their mod -to include underground embarks by default. - -Example:: - - deep-embark -depth CAVERN_2 - -Usage:: - - -depth X - (obligatory) - replace "X" with one of the following: - CAVERN_1 - CAVERN_2 - CAVERN_3 - UNDERWORLD - - -blockDemons - including this arg will prevent demon surges - in the context of breached underworld spires - (intended mainly for UNDERWORLD embarks) - ("wildlife" demon spawning will be unaffected) - - -atReclaim - if the script is being run from onLoad.init, - including this arg will enable deep embarks - when reclaiming sites too - (there's no need to specify this if running - the script directly from the console) - - -clear - re-enable normal surface embarks - -]====] - -local utils = require 'utils' +local dlg = require('gui.dialogs') +local utils = require('utils') function getFeatureID(cavernType) - local features = df.global.world.features - local map_features = features.map_features - if cavernType == 'CAVERN_1' then - for i, feature in ipairs(map_features) do - if feature._type == df.feature_init_subterranean_from_layerst - and feature.start_depth == 0 then - return features.feature_global_idx[i] - end - end - elseif cavernType == 'CAVERN_2' then - for i, feature in ipairs(map_features) do - if feature._type == df.feature_init_subterranean_from_layerst - and feature.start_depth == 1 then - return features.feature_global_idx[i] - end - end - elseif cavernType == 'CAVERN_3' then - for i, feature in ipairs(map_features) do - if feature._type == df.feature_init_subterranean_from_layerst - and feature.start_depth == 2 then - return features.feature_global_idx[i] - end - end - elseif cavernType == 'UNDERWORLD' then - for i, feature in ipairs(map_features) do - if feature._type == df.feature_init_underworld_from_layerst - and feature.start_depth == 4 then - return features.feature_global_idx[i] - end + local features = df.global.world.features + local map_features = features.map_features + if cavernType == 'CAVERN_1' then + for i, feature in ipairs(map_features) do + if feature._type == df.feature_init_subterranean_from_layerst and feature.start_depth == 0 then + return features.feature_global_idx[i] + end + end + elseif cavernType == 'CAVERN_2' then + for i, feature in ipairs(map_features) do + if feature._type == df.feature_init_subterranean_from_layerst and feature.start_depth == 1 then + return features.feature_global_idx[i] + end + end + elseif cavernType == 'CAVERN_3' then + for i, feature in ipairs(map_features) do + if feature._type == df.feature_init_subterranean_from_layerst and feature.start_depth == 2 then + return features.feature_global_idx[i] + end + end + elseif cavernType == 'UNDERWORLD' then + for i, feature in ipairs(map_features) do + if feature._type == df.feature_init_underworld_from_layerst and feature.start_depth == 4 then + return features.feature_global_idx[i] + end + end end - end end function getFeatureBlocks(featureID) - local featureBlocks = {} --as:number[] - for i,block in ipairs(df.global.world.map.map_blocks) do - if block.global_feature == featureID and block.local_feature == -1 then - table.insert(featureBlocks, i) + local featureBlocks = {} --as:number[] + for i, block in ipairs(df.global.world.map.map_blocks) do + if block.global_feature == featureID and block.local_feature == -1 then + table.insert(featureBlocks, i) + end end - end - return featureBlocks + return featureBlocks end function isValidTiletype(tiletype) - local tiletype = df.tiletype[tiletype] - local tiletypeAttrs = df.tiletype.attrs[tiletype] - local material = tiletypeAttrs.material - local forbiddenMaterials = { - df.tiletype_material.TREE, -- so as not to embark stranded on top of a tree - df.tiletype_material.MUSHROOM, - df.tiletype_material.FIRE, - df.tiletype_material.CAMPFIRE - } - for _,forbidden in ipairs(forbiddenMaterials) do - if material == forbidden then - return false - end - end - local shapeAttrs = df.tiletype_shape.attrs[tiletypeAttrs.shape] - if shapeAttrs.walkable and shapeAttrs.basic_shape ~= df.tiletype_shape_basic.Open then -- downward ramps are walkable but open; units placed here would fall - return true - else - return false - end + local tt = df.tiletype[tiletype] + local tiletypeAttrs = df.tiletype.attrs[tt] + local material = tiletypeAttrs.material + local forbiddenMaterials = utils.invert{ + df.tiletype_material.TREE, -- so as not to embark stranded on top of a tree + df.tiletype_material.MUSHROOM, + df.tiletype_material.FIRE, + df.tiletype_material.CAMPFIRE + } + if forbiddenMaterials[material] then return false end + local shapeAttrs = df.tiletype_shape.attrs[tiletypeAttrs.shape] + return shapeAttrs.walkable end function getValidEmbarkTiles(block) - local validTiles = {} --as:{_type:table,x:number,y:number,z:number}[] - for xi = 0,15 do - for yi = 0,15 do - if block.designation[xi][yi].flow_size == 0 - and isValidTiletype(block.tiletype[xi][yi]) then - table.insert(validTiles, {x = block.map_pos.x + xi, y = block.map_pos.y + yi, z = block.map_pos.z}) - end + local validTiles = {} --as:{_type:table,x:number,y:number,z:number}[] + for xi = 0, 15 do + for yi = 0, 15 do + if block.designation[xi][yi].flow_size == 0 + and isValidTiletype(block.tiletype[xi][yi]) + then + table.insert(validTiles, { x = block.map_pos.x + xi, y = block.map_pos.y + yi, z = block.map_pos.z }) + end + end end - end - return validTiles + return validTiles end function blockGlowingBarrierAnnouncements(recenter) --- temporarily disables the "glowing barrier has disappeared" announcement --- announcement settings are restored after 1 tick --- setting recenter to true enables recentering of game view to the announcement position - local announcementFlags = df.global.d_init.announcements.flags.ENDGAME_EVENT_1 -- glowing barrier disappearance announcement - local oldFlags = df.global.d_init.announcements.flags.ENDGAME_EVENT_1:new() -- backup announcement settings - announcementFlags.DO_MEGA = false - announcementFlags.PAUSE = false - announcementFlags.RECENTER = recenter and true or false - announcementFlags.A_DISPLAY = false - announcementFlags.D_DISPLAY = recenter and true or false -- an actual announcement is required for recentering to occur - dfhack.timeout(1,'ticks', function() -- barrier disappears after 1 tick - announcementFlags:assign(oldFlags) -- restore announcement settings - if recenter then --- Remove glowing barrier notifications: - local status = df.global.world.status - local announcements = status.announcements - for i = #announcements-1, 0, -1 do - if string.find(announcements[i].text,"glowing barrier has disappeared") then - announcements:erase(i) - break - end - end - local reports = status.reports - for i = #reports-1, 0, -1 do - if string.find(reports[i].text,"glowing barrier has disappeared") then - reports:erase(i) - break + -- temporarily disables the "glowing barrier has disappeared" announcement + -- announcement settings are restored after 1 tick + -- setting recenter to true enables recentering of game view to the announcement position + -- glowing barrier disappearance announcement + local announcementFlags = df.global.d_init.announcements.flags.ENDGAME_EVENT_1 + local oldFlags = df.global.d_init.announcements.flags.ENDGAME_EVENT_1:new() -- backup announcement settings + announcementFlags.DO_MEGA = false + announcementFlags.PAUSE = false + announcementFlags.RECENTER = recenter and true or false + announcementFlags.A_DISPLAY = false + announcementFlags.D_DISPLAY = recenter and true or false -- an actual announcement is required for recentering to occur + dfhack.timeout(1, 'ticks', function() -- barrier disappears after 1 tick + announcementFlags:assign(oldFlags) -- restore announcement settings + if recenter then + -- Remove glowing barrier notifications: + local status = df.global.world.status + local announcements = status.announcements + for i = #announcements - 1, 0, -1 do + if string.find(announcements[i].text, "glowing barrier has disappeared") then + announcements:erase(i) + break + end + end + local reports = status.reports + for i = #reports - 1, 0, -1 do + if string.find(reports[i].text, "glowing barrier has disappeared") then + reports:erase(i) + break + end + end + status.display_timer = 0 -- to avoid displaying other announcements end - end - status.display_timer = 0 -- to avoid displaying other announcements - end - end) + end) end function reveal(pos) --- creates an unbound glowing barrier at the target location --- so as to trigger tile revelation when it disappears 1 tick later (fortress mode only) --- should be run in conjunction with blockGlowingBarrierAnnouncements() - local x,y,z = pos2xyz(pos) - local block = dfhack.maps.getTileBlock(x,y,z) - local tiletype = block.tiletype[x%16][y%16] - if tiletype ~= df.tiletype.GlowingBarrier then -- to avoid multiple instances - block.tiletype[x%16][y%16] = df.tiletype.GlowingBarrier - local barriers = df.global.world.glowing_barriers + -- creates an unbound glowing barrier at the target location + -- so as to trigger tile revelation when it disappears 1 tick later (fortress mode only) + -- should be run in conjunction with blockGlowingBarrierAnnouncements() + local x, y, z = pos2xyz(pos) + local block = dfhack.maps.getTileBlock(x, y, z) + local tiletype = block.tiletype[x % 16][y % 16] + if tiletype == df.tiletype.GlowingBarrier then -- to avoid multiple instances + return + end + block.tiletype[x % 16][y % 16] = df.tiletype.GlowingBarrier + local barriers = df.global.world.event.glowing_barriers local barrier = df.glowing_barrier:new() - barrier.buildings:insert('#',-1) -- being unbound to a building makes the barrier disappear immediately + barrier.buildings:insert('#', -1) -- being unbound to a building makes the barrier disappear immediately barrier.pos:assign(pos) - barriers:insert('#',barrier) + barriers:insert('#', barrier) local hfs = df.glowing_barrier:new() - hfs.triggered = true -- this prevents HFS events (which can otherwise be triggered by the barrier disappearing) - barriers:insert('#',hfs) - dfhack.timeout(1,'ticks', function() -- barrier tiletype disappears after 1 tick - block.tiletype[x%16][y%16] = tiletype -- restore old tiletype - barriers:erase(#barriers-1) -- remove hfs blocker - barriers:erase(#barriers-1) -- remove revelation barrier + hfs.triggered = 1 -- this prevents HFS events (which can otherwise be triggered by the barrier disappearing) + barriers:insert('#', hfs) + dfhack.timeout(1, 'ticks', function() -- barrier tiletype disappears after 1 tick + block.tiletype[x % 16][y % 16] = tiletype -- restore old tiletype + barriers:erase(#barriers - 1) -- remove hfs blocker + barriers:erase(#barriers - 1) -- remove revelation barrier end) - end end function moveEmbarkStuff(selectedBlock, embarkTiles) - local spawnPosCentre - for _, hotkey in ipairs(df.global.ui.main.hotkeys) do - if hotkey.name == "Gate" then -- the preset hotkey is centred around the spawn point - spawnPosCentre = xyz2pos(hotkey.x, hotkey.y, hotkey.z) - hotkey:assign(embarkTiles[math.random(1, #embarkTiles)]) -- set the hotkey to the new spawn point - break + local spawnPosCentre + for _, hotkey in ipairs(df.global.plotinfo.main.hotkeys) do + if hotkey.cmd == df.hotkey_type.Zoom then -- the preset hotkey is centred around the spawn point + spawnPosCentre = xyz2pos(hotkey.x, hotkey.y, hotkey.z) + hotkey:assign(embarkTiles[math.random(1, #embarkTiles)]) -- set the hotkey to the new spawn point + break + end end - end --- only target things within this zone to help avoid teleporting non-embark stuff: --- the following values might need to be modified - local x1 = spawnPosCentre.x - 15 - local x2 = spawnPosCentre.x + 15 - local y1 = spawnPosCentre.y - 15 - local y2 = spawnPosCentre.y + 15 - local z1 = spawnPosCentre.z - 3 -- units can be spread across multiple z-levels when embarking on a mountain - local z2 = spawnPosCentre.z + 3 + if not spawnPosCentre then -- no place for the wagon; use the position of the first unit + spawnPosCentre = xyz2pos(dfhack.units.getPosition(dfhack.units.getCitizens()[1])) + end --- Move citizens and pets: - local unitsAtSpawn = dfhack.units.getUnitsInBox(x1,y1,z1,x2,y2,z2) - local movedUnit = false - for i, unit in ipairs(unitsAtSpawn) do - if unit.civ_id == df.global.ui.civ_id and not unit.flags1.inactive and not unit.flags2.killed then - local pos = embarkTiles[math.random(1, #embarkTiles)] - dfhack.units.teleport(unit, pos) - reveal(pos) - movedUnit = true + -- only target things within this zone to help avoid teleporting non-embark stuff: + -- the following values might need to be modified + local x1 = spawnPosCentre.x - 15 + local x2 = spawnPosCentre.x + 15 + local y1 = spawnPosCentre.y - 15 + local y2 = spawnPosCentre.y + 15 + local z1 = spawnPosCentre.z - 3 -- units can be spread across multiple z-levels when embarking on a mountain + local z2 = spawnPosCentre.z + 3 + + -- Move citizens and pets: + local unitsAtSpawn = dfhack.units.getUnitsInBox(x1, y1, z1, x2, y2, z2) + local movedUnit = false + for i, unit in ipairs(unitsAtSpawn) do + if unit.civ_id == df.global.plotinfo.civ_id and not unit.flags1.inactive and not unit.flags2.killed then + local pos = embarkTiles[math.random(1, #embarkTiles)] + dfhack.units.teleport(unit, pos) + reveal(pos) + movedUnit = true + end + end + if movedUnit then + blockGlowingBarrierAnnouncements(true) -- this is separate from the reveal() function as it only needs to be called once per tick, regardless of how many times reveal() has been run end - end - if movedUnit then - blockGlowingBarrierAnnouncements(true) -- this is separate from the reveal() function as it only needs to be called once per tick, regardless of how many times reveal() has been run - end --- Move wagon contents: - local wagonFound = false - for _, wagon in ipairs(df.global.world.buildings.other.WAGON) do --as:df.building_wagonst - if wagon.age == 0 then -- just in case there's an older wagon present for some reason - local contained = wagon.contained_items - for i = #contained-1, 0, -1 do - if contained[i].use_mode == 0 then -- actual contents (as opposed to building components) - local item = contained[i].item --- dfhack.items.moveToGround() does not handle items within buildings, so do this manually: - contained:erase(i) - for k = #item.general_refs-1, 0, -1 do - if item.general_refs[k]._type == df.general_ref_building_holderst then - item.general_refs:erase(k) + -- Move wagon contents: + local wagonFound = false + for _, wagon in ipairs(df.global.world.buildings.other.WAGON) do --as:df.building_wagonst + if wagon.age == 0 then -- just in case there's an older wagon present for some reason + local contained = wagon.contained_items + for i = #contained - 1, 0, -1 do + if contained[i].use_mode == df.building_item_role_type.TEMP then -- actual contents (as opposed to building components) + local item = contained[i].item + -- dfhack.items.moveToGround() does not handle items within buildings, so do this manually: + contained:erase(i) + for k = #item.general_refs - 1, 0, -1 do + if item.general_refs[k]._type == df.general_ref_building_holderst then + item.general_refs:erase(k) + end + end + item.flags.in_building = false + item.flags.on_ground = true + local pos = embarkTiles[math.random(1, #embarkTiles)] + item.pos:assign(pos) + selectedBlock.items:insert('#', item.id) + selectedBlock.occupancy[pos.x % 16][pos.y % 16].item = true + end end - end - item.flags.in_building = false - item.flags.on_ground = true - local pos = embarkTiles[math.random(1, #embarkTiles)] - item.pos:assign(pos) - selectedBlock.items:insert('#', item.id) - selectedBlock.occupancy[pos.x%16][pos.y%16].item = true + dfhack.buildings.deconstruct(wagon) + wagon.flags.almost_deleted = true -- wagon vanishes a tick later + wagonFound = true + break end - end - dfhack.buildings.deconstruct(wagon) - wagon.flags.almost_deleted = true -- wagon vanishes a tick later - wagonFound = true - break end - end --- Move items scattered around the spawn point if there's no wagon: - if not wagonFound then - for _, item in ipairs(df.global.world.items.other.IN_PLAY) do - local flags = item.flags - if item.age == 0 -- embark equipment consists of newly created items - and item.pos.x >= x1 and item.pos.x <= x2 - and item.pos.y >= y1 and item.pos.y <= y2 - and item.pos.z >= z1 and item.pos.z <= z2 - and flags.on_ground - and not flags.in_inventory - and not flags.in_building - and not flags.in_chest - and not flags.construction - and not flags.spider_web - and not flags.encased then - dfhack.items.moveToGround(item, embarkTiles[math.random(1, #embarkTiles)]) - end + -- Move items scattered around the spawn point if there's no wagon: + if not wagonFound then + for _, item in ipairs(df.global.world.items.other.IN_PLAY) do + local flags = item.flags + local item_pos = xyz2pos(dfhack.items.getPosition(item)) + -- items spawned into mid-air incorrectly have the `in_job` flag set + if 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 + flags.in_job = false + end + if item.age == 0 -- embark equipment consists of newly created items + and item_pos.x >= x1 and item_pos.x <= x2 + and item_pos.y >= y1 and item_pos.y <= y2 + and item_pos.z >= z1 and item_pos.z <= z2 + and not flags.in_inventory + and not flags.in_building + and not flags.construction + and not flags.spider_web + and not flags.encased + then + dfhack.items.moveToGround(item, embarkTiles[math.random(1, #embarkTiles)]) + end + end end - end + + dlg.showMessage('deep-embark', 'Please unpause to zoom to your deep embark.', COLOR_WHITE) end function deepEmbark(cavernType, blockDemons) - if not cavernType then - qerror('Cavern type not specified!') - end + if not cavernType then + qerror('Cavern type not specified!') + end - local cavernBlocks = getFeatureBlocks(getFeatureID(cavernType)) - if #cavernBlocks == 0 then - qerror(cavernType .. " not found!") - end + local cavernBlocks = getFeatureBlocks(getFeatureID(cavernType)) + if #cavernBlocks == 0 then + qerror(cavernType .. " not found!") + end - local moved = false - for n = 1, #cavernBlocks do - local i = math.random(1, #cavernBlocks) - local selectedBlock = df.global.world.map.map_blocks[cavernBlocks[i]] - local embarkTiles = getValidEmbarkTiles(selectedBlock) - if #embarkTiles >= 20 then -- value chosen arbitrarily; might want to increase/decrease (determines how cramped the embark spot is allowed to be) - moveEmbarkStuff(selectedBlock, embarkTiles) - moved = true - break + local moved = false + for n = 1, #cavernBlocks do + local i = math.random(1, #cavernBlocks) + local selectedBlock = df.global.world.map.map_blocks[cavernBlocks[i]] + local embarkTiles = getValidEmbarkTiles(selectedBlock) + if #embarkTiles >= 20 then -- value chosen arbitrarily; might want to increase/decrease (determines how cramped the embark spot is allowed to be) + moveEmbarkStuff(selectedBlock, embarkTiles) + moved = true + break + end + table.remove(cavernBlocks, i) + end + if not moved then + qerror('Insufficient space at ' .. cavernType) end - table.remove(cavernBlocks, i) - end - if not moved then - qerror('Insufficient space at ' .. cavernType) - end - if blockDemons then - disableSpireDemons() - end + if blockDemons then + disableSpireDemons() + end end function disableSpireDemons() --- marks underworld spires on the map as having been breached already, preventing HFS events - for _, spire in ipairs(df.global.world.deep_vein_hollows) do - spire.triggered = true - end + -- marks underworld spires on the map as having been breached already, preventing HFS events + for _, spire in ipairs(df.global.world.event.deep_vein_hollows) do + spire.triggered = true + end end function inEmbarkMode() - if df.global.gametype ~= df.game_type.DWARF_MAIN then -- is always set at fortress mode setup - return false - end - local embarkViewScreens = { - df.viewscreen_adopt_regionst, -- onLoad.init kicks in early; this is the viewscreen present at this stage (the 'loading world' viewscreen is also present at adventure mode setup and legends mode, hence the game_type check above) - df.viewscreen_choose_start_sitest, - df.viewscreen_setupdwarfgamest - } - local view = dfhack.gui.getCurViewscreen() - for _, valid in ipairs(embarkViewScreens) do - if view._type == valid or view.parent._type == valid and view._type ~= df.viewscreen_textviewerst then -- df.viewscreen_textviewerst is present right after embarking (displays the embark message) and has .parent._type == df.viewscreen_setupdwarfgamest - return true + if df.global.gametype ~= df.game_type.DWARF_MAIN then -- is always set at fortress mode setup + return false + end + local embarkViewScreens = { + df.viewscreen_adopt_regionst, -- onLoad.init kicks in early; this is the viewscreen present at this stage (the 'loading world' viewscreen is also present at adventure mode setup and legends mode, hence the game_type check above) + df.viewscreen_choose_start_sitest, + df.viewscreen_setupdwarfgamest + } + local view = dfhack.gui.getCurViewscreen() + for _, valid in ipairs(embarkViewScreens) do + if view._type == valid then + return true + end end - end - return false + return false end local validArgs = utils.invert({ - 'depth', - 'atReclaim', - 'blockDemons', - 'clear', - 'help' + 'depth', + 'atReclaim', + 'blockDemons', + 'clear', + 'help' }) -local args = utils.processArgs({...}, validArgs) +local args = utils.processArgs({ ... }, validArgs) if moduleMode then - return + return end if args.help then - print(usage) - return + print(dfhack.script_help()) + return end if args.clear then - dfhack.onStateChange.DeepEmbarkMonitor = nil - print("Cleared settings; now embarking normally.") - return + dfhack.onStateChange.DeepEmbarkMonitor = nil + print("Cleared settings; now embarking normally.") + return end if not args.depth then - qerror('Depth not specified! Enter "deep-embark -help" for more information.') + qerror('Depth not specified! Enter "help deep-embark" for more information.') end local validDepths = { - ["CAVERN_1"] = true, - ["CAVERN_2"] = true, - ["CAVERN_3"] = true, - ["UNDERWORLD"] = true + ["CAVERN_1"] = true, + ["CAVERN_2"] = true, + ["CAVERN_3"] = true, + ["UNDERWORLD"] = true } if not validDepths[args.depth] then - qerror("Invalid depth: " .. args.depth) + qerror("Invalid depth: " .. args.depth) end local consoleMode = dfhack.is_interactive() -- true if the script has been called directly from the DFHack console, false if called from onLoad.init -if not inEmbarkMode() then - if consoleMode then - qerror('This script must be run prior to embarking! Enter "deep-embark -help" for more information.') - else - return -- terminate silently to prevent unwanted error messages every time onLoad.init is run in non-embark scenarios - end +if consoleMode and not inEmbarkMode() then + -- if running from the console (not onLoad.init), abort if not currently in an embark viewscreen. + qerror( + 'When run from the command line, this script should be run during the embark setup screens. Enter "help deep-embark" for more information.') end if consoleMode then - print("Embarking at: " .. tostring(args.depth)) + print("Embarking at: " .. tostring(args.depth)) end dfhack.onStateChange.DeepEmbarkMonitor = function(event) - if event == SC_VIEWSCREEN_CHANGED then -- I initially tried using SC_MAP_LOADED, but the map appears to be loaded too early when reclaiming sites - if dfhack.gui.getCurViewscreen()._type ~= df.viewscreen_textviewerst then -- embark message; map should have been loaded by the time this is presented - return - end - if not consoleMode and not args.atReclaim and df.global.gametype == df.game_type.DWARF_RECLAIM then -- it's assumed that a player who chooses to run the script from console whilst reclaiming knows what they're doing, so there's no need to check for -atReclaim in this scenario - dfhack.onStateChange.DeepEmbarkMonitor = nil -- stop monitoring - return -- don't deepEmbark if running from onLoad.init and in reclaim mode without -atReclaim - else - deepEmbark(args.depth, args.blockDemons) - dfhack.onStateChange.DeepEmbarkMonitor = nil + if event == SC_VIEWSCREEN_CHANGED then -- I initially tried using SC_MAP_LOADED, but the map appears to be loaded too early when reclaiming sites + local view = dfhack.gui.getCurViewscreen() + if not consoleMode and not args.atReclaim and df.global.gametype == df.game_type.DWARF_RECLAIM then -- it's assumed that a player who chooses to run the script from console whilst reclaiming knows what they're doing, so there's no need to check for -atReclaim in this scenario + dfhack.onStateChange.DeepEmbarkMonitor = nil -- stop monitoring + return -- don't deepEmbark if running from onLoad.init and in reclaim mode without -atReclaim + elseif view._type == df.viewscreen_choose_start_sitest then -- on embark screen + if view.choosing_embark or view.choosing_reclaim then -- on a fresh embark, or on a reclaim + deepEmbark(args.depth, args.blockDemons) + dfhack.onStateChange.DeepEmbarkMonitor = nil + end + elseif view._type == df.viewscreen_dwarfmodest then -- we're in game. If we got here then we never got an embark screen, so this is loading a save and we abort. + dfhack.onStateChange.DeepEmbarkMonitor = nil + end + elseif event == SC_WORLD_UNLOADED then -- embark aborted + dfhack.onStateChange.DeepEmbarkMonitor = nil end - elseif event == SC_WORLD_UNLOADED then -- embark aborted - dfhack.onStateChange.DeepEmbarkMonitor = nil - end end diff --git a/deteriorate.lua b/deteriorate.lua index df71cf7251..42c0498aad 100644 --- a/deteriorate.lua +++ b/deteriorate.lua @@ -1,128 +1,159 @@ -- Cause selected item types to quickly rot away --@module = true ---[====[ +--@enable = true -deteriorate -=========== - -Causes the selected item types to rot away. By default, items disappear after a -few months, but you can choose to slow this down or even make things rot away -instantly! - -Now all those slightly worn wool shoes that dwarves scatter all over the place -or the toes, teeth, fingers, and limbs from the last undead siege will -deteriorate at a greatly increased rate, and eventually just crumble into -nothing. As warm and fuzzy as a dining room full of used socks makes your -dwarves feel, your FPS does not like it! - -To always have deteriorate running in your forts, add a line like this to your -``onMapLoad.init`` file (use your preferred options, of course):: - - deteriorate start --types=corpses - -Usage:: - - deteriorate [] - -**** is one of: - -:start: Starts deteriorating items while you play. -:stop: Stops running. -:status: Shows the item types that are currently being monitored and their - deterioration frequencies. -:now: Causes all items (of the specified item types) to rot away within a - few ticks. - -You can control which item types are being monitored and their rotting rates by -running the command multiple times with different options. - -**** are: - -``-f``, ``--freq``, ``--frequency [,]`` - How often to increment the wear counters. ```` can be one of - ``days``, ``months``, or ``years`` and defaults to ``days`` if not - specified. The default frequency of 1 day will result in items disappearing - after several months. The number does not need to be a whole number. E.g. - ``--freq=0.5,days`` is perfectly valid. -``-q``, ``--quiet`` - Silence non-error output. -``-t``, ``--types `` - The item types to affect. This option is required for ``start``, ``stop``, - and ``now`` commands. See below for valid types. +local argparse = require('argparse') +local utils = require('utils') -**** is any of: +-------------------- +-- state -:clothes: All clothing types that have an armor rating of 0, are on the ground, - and are already starting to show signs of wear. -:corpses: All non-dwarf corpses and body parts. This includes potentially - useful remains such as hair, wool, hooves, bones, and skulls. Use - them before you lose them! -:food: All food and plants, regardles of whether they are in barrels or - stockpiles. Seeds are left untouched. +local GLOBAL_KEY = 'deteriorate' -You can specify multiple types by separating them with commas, e.g. -``deteriorate start --types=clothes,food``. +local categories = { + 'clothes', + 'food', + 'corpses', + 'usable-parts', + 'unusable-parts', +} -Examples: +local aliases = { + parts={'usable-parts', 'unusable-parts'}, + all=categories, +} -* Deteriorate corpses at twice the default rate:: +local function get_default_state() + local default_state = { + enabled=false, + categories={}, + } + for _,category in ipairs(categories) do + local default_enabled = category == 'corpses' or category == 'unusable-parts' + default_state.categories[category] = { + enabled=default_enabled, + frequency=1, + last_cycle_tick=0, + } + end + return default_state +end - deteriorate start --types=corpses --freq=0.5,days +state = state or get_default_state() -* Deteriorate corpses quickly but food slowly:: +function isEnabled() + return state.enabled +end - deteriorate start -tcorpses -f0.1 - deteriorate start -tfood -f3,months -]====] +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) +end -local argparse = require('argparse') -local utils = require('utils') +----------------------- +-- deterioration logic local function get_clothes_vectors() - return {df.global.world.items.other.GLOVES, - df.global.world.items.other.ARMOR, - df.global.world.items.other.SHOES, - df.global.world.items.other.PANTS, - df.global.world.items.other.HELM} + return { + df.global.world.items.other.GLOVES, + df.global.world.items.other.ARMOR, + df.global.world.items.other.SHOES, + df.global.world.items.other.PANTS, + df.global.world.items.other.HELM, + } end -local function get_corpse_vectors() - return {df.global.world.items.other.ANY_CORPSE} +local function get_food_vectors() + return { + df.global.world.items.other.FISH, + df.global.world.items.other.FISH_RAW, + df.global.world.items.other.EGG, + df.global.world.items.other.CHEESE, + df.global.world.items.other.PLANT, + df.global.world.items.other.PLANT_GROWTH, + df.global.world.items.other.FOOD, + df.global.world.items.other.MEAT, + df.global.world.items.other.LIQUID_MISC, + } end -local function get_remains_vectors() - return {df.global.world.items.other.REMAINS} +local function get_corpse_vectors() + return { + df.global.world.items.other.CORPSE, + df.global.world.items.other.REMAINS, + } end -local function get_food_vectors() - return {df.global.world.items.other.FISH, - df.global.world.items.other.FISH_RAW, - df.global.world.items.other.EGG, - df.global.world.items.other.CHEESE, - df.global.world.items.other.PLANT, - df.global.world.items.other.PLANT_GROWTH, - df.global.world.items.other.FOOD} +local function get_parts_vectors() + return { + df.global.world.items.other.CORPSEPIECE, + } end local function is_valid_clothing(item) + -- includes discarded owned clothes return item.subtype.armorlevel == 0 and item.flags.on_ground and item.wear > 0 end +local function is_valid_food(item) + if not df.item_liquid_miscst:is_instance(item) then + return true + end + local mi = dfhack.matinfo.decode(item) + return mi:getToken():endswith(':MILK') +end + +-- TODO: is just checking in_building sufficient, or do we need to validate +-- that the building it is in is a coffin? +local function is_entombed(item) + return item.flags.in_building +end + local function is_valid_corpse(item) - return not item.flags.dead_dwarf + return not is_entombed(item) end -local function is_valid_food(item) - return true +local usable_types = { + 'plant', + 'silk', + 'leather', + 'bone', + 'shell', + 'wood', + 'soap', + 'tooth', + 'horn', + 'pearl', + 'skull', + 'hair_wool', + 'yarn', +} + +local function is_usable_corpse_piece(item) + if item.flags.dead_dwarf or item.corpse_flags.unbutchered then + return false + end + for _,flag in ipairs(usable_types) do + if item.corpse_flags[flag] then return true end + end + return false +end + +local function is_valid_usable_corpse_piece(item) + return not is_entombed(item) and is_usable_corpse_piece(item) end +local function is_valid_unusable_corpse_piece(item) + return not is_entombed(item) and not is_usable_corpse_piece(item) +end + +-- different algorithm for clothes so they rot away when they become tattered local function increment_clothes_wear(item) item.wear_timer = math.ceil(item.wear_timer * (item.wear + 0.5)) return item.wear > 2 end -local function increment_generic_wear(item, threshold) +local function increment_wear(threshold, item) item.wear_timer = item.wear_timer + 1 if item.wear_timer > threshold then item.wear_timer = 0 @@ -131,210 +162,231 @@ local function increment_generic_wear(item, threshold) return item.wear > 3 end -local function increment_corpse_wear(item) - return increment_generic_wear(item, 24) -end - -local function increment_remains_wear(item) - return increment_generic_wear(item, 6) -end - -local function increment_food_wear(item) - return increment_generic_wear(item, 24) -end - -local function deteriorate(get_item_vectors_fn, is_valid_fn, increment_wear_fn) - local count = 0 +local function deteriorate_items(now, get_item_vectors_fn, is_valid_fn, increment_wear_fn) + local items_to_remove = {} for _,v in ipairs(get_item_vectors_fn()) do for _,item in ipairs(v) do - if is_valid_fn(item) and increment_wear_fn(item) - and not item.flags.garbage_collect then - item.flags.garbage_collect = true - item.flags.hidden = true - count = count + 1 + if is_valid_fn(item) and (now or increment_wear_fn(item)) and not item.flags.garbage_collect then + table.insert(items_to_remove, item) end end end - return count + for _,item in ipairs(items_to_remove) do + print(('deteriorate: %s crumbles away to dust'):format(dfhack.items.getReadableDescription(item))) + dfhack.items.remove(item) + end + return #items_to_remove end -local function always_worn() - return true +local function mk_deteriorate_fn(get_item_vectors_fn, is_valid_fn, increment_wear_fn) + return function(now) + return deteriorate_items(now, get_item_vectors_fn, is_valid_fn, increment_wear_fn) + end end -local function deteriorate_clothes(now) - return deteriorate(get_clothes_vectors, is_valid_clothing, - now and always_worn or increment_clothes_wear) -end +local category_fns = { + clothes=mk_deteriorate_fn(get_clothes_vectors, is_valid_clothing, increment_clothes_wear), + food=mk_deteriorate_fn(get_food_vectors, is_valid_food, curry(increment_wear, 24)), + corpses=mk_deteriorate_fn(get_corpse_vectors, is_valid_corpse, curry(increment_wear, 24)), + ['usable-parts']=mk_deteriorate_fn(get_parts_vectors, is_valid_usable_corpse_piece, curry(increment_wear, 24)), + ['unusable-parts']=mk_deteriorate_fn(get_parts_vectors, is_valid_unusable_corpse_piece, curry(increment_wear, 24)), +} -local function deteriorate_corpses(now) - return deteriorate(get_corpse_vectors, is_valid_corpse, - now and always_worn or increment_corpse_wear) - + deteriorate(get_remains_vectors, is_valid_corpse, - now and always_worn or increment_remains_wear) -end +---------------------------- +-- cycle and timer logic + +local TICKS_PER_DAY = 1200 +local TICKS_PER_MONTH = 28 * TICKS_PER_DAY +local TICKS_PER_YEAR = 12 * TICKS_PER_MONTH -local function deteriorate_food(now) - return deteriorate(get_food_vectors, is_valid_food, - now and always_worn or increment_food_wear) +local function get_normalized_tick() + return dfhack.world.ReadCurrentTick() + TICKS_PER_YEAR * dfhack.world.ReadCurrentYear() end -local type_fns = { - clothes=deteriorate_clothes, - corpses=deteriorate_corpses, - food=deteriorate_food, -} +timeout_ids = timeout_ids or {} --- maps the type string to {id=int, time=int, timeunit=string} -timeout_ids = timeout_ids or { - clothes={}, - corpses={}, - food={}, -} +local function event_loop(category) + local category_data = state.categories[category] + if not state.enabled or not category_data.enabled then return end -local function _stop(item_type) - local timeout_id = timeout_ids[item_type].id + local current_tick = get_normalized_tick() + local ticks_per_cycle = math.max(1, math.floor(TICKS_PER_DAY * category_data.frequency)) + local timeout_ticks = ticks_per_cycle + + if current_tick - category_data.last_cycle_tick < ticks_per_cycle then + timeout_ticks = category_data.last_cycle_tick - current_tick + ticks_per_cycle + else + category_fns[category](false) + category_data.last_cycle_tick = current_tick + persist_state() + end + timeout_ids[category] = dfhack.timeout(timeout_ticks, 'ticks', curry(event_loop, category)) +end + +-- launches timer. first cycle will be after the configured frequency +local function start_category(category, category_data, current_tick) + category_data = category_data or state.categories[category] + category_data.last_cycle_tick = current_tick or get_normalized_tick() + event_loop(category) +end + +local function stop_category(category) + local timeout_id = timeout_ids[category] if timeout_id then dfhack.timeout_active(timeout_id, nil) -- cancel callback - timeout_ids[item_type].id = nil - return true + timeout_ids[category] = nil end end -local function make_timeout_cb(item_type, opts) - local fn - fn = function(first_time) - local timeout_data = timeout_ids[item_type] - timeout_data.time, timeout_data.mode = opts.time, opts.mode - timeout_data.id = dfhack.timeout(opts.time, opts.mode, fn) - if not timeout_ids[item_type].id then - print('Map has been unloaded; stopping deteriorate') - for k in pairs(type_fns) do - _stop(k) - end - return - end - if not first_time then - local count = type_fns[item_type]() - if count > 0 then - print(('Deteriorated %d %s'):format(count, item_type)) - end +local function do_enable() + if state.enabled then return end + + state.enabled = true + local current_tick = get_normalized_tick() + for _,category in ipairs(categories) do + local category_data = state.categories[category] + if category_data.enabled then + start_category(category, category_data, current_tick) end end - return fn end -local function start(opts) - for _,v in ipairs(opts.types) do - _stop(v) - if not opts.quiet then - print(('Deterioration of %s commencing...'):format(v)) - end - -- create a callback and call it to make it register itself - make_timeout_cb(v, opts)(true) +local function do_disable() + if not state.enabled then return end + + state.enabled = false + for _,category in ipairs(categories) do + stop_category(category) end end -local function stop(opts) - for _,v in ipairs(opts.types) do - if _stop(v) and not opts.quiet then - print('Stopped deteriorating ' .. v) - end +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + do_disable() + return end -end -local function status() - for k in pairs(type_fns) do - local timeout_data = timeout_ids[k] - local status_str = 'Stopped' - if timeout_data.id then - local time, mode = timeout_data.time, timeout_data.mode - if time == 1 then - mode = mode:sub(1, #mode - 1) -- make singular - end - status_str = ('Running (every %s %s)') :format(time, mode) - end - print(('%7s:\t%s'):format(k, status_str)) + if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then + return end -end -local function now(opts) - for _,v in ipairs(opts.types) do - local count = type_fns[v](true) - if not opts.quiet then - print(('Deteriorated %d %s'):format(count, v)) - end + state = get_default_state() + utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) + + for _,category in ipairs(categories) do + event_loop(category) end end -local function help() - print(dfhack.script_help()) -end +--------------------- +-- CLI if dfhack_flags.module then return end -if not dfhack.isMapLoaded() then - qerror('deteriorate needs a fortress map to be loaded.') +if dfhack_flags.enable then + if dfhack_flags.enable_state then + do_enable() + else + do_disable() + end end -local command_switch = { - start=start, - stop=stop, - status=status, - now=now, -} - -local valid_timeunits = utils.invert{'days', 'months', 'years'} - -local function parse_freq(arg) - local elems = argparse.stringList(arg) - local num = tonumber(elems[1]) - if not num or num <= 0 then - qerror('number parameter for --freq option must be greater than 0') +local function parse_categories(arg) + local list = {} + for _,v in ipairs(argparse.stringList(arg)) do + if aliases[v] then + for _,alias in ipairs(aliases[v]) do + table.insert(list, alias) + end + elseif category_fns[v] then + table.insert(list, v) + else + qerror(('unrecognized category: "%s"'):format(v)) + end end - if #elems == 1 then - return num, 'days' + if #list == 0 then + qerror('no categories specified') end - local timeunit = elems[2]:lower() - if valid_timeunits[timeunit] then return num, timeunit end - timeunit = timeunit .. 's' -- it's ok if the user specified a singular - if valid_timeunits[timeunit] then return num, timeunit end - qerror(('invalid time unit: "%s"'):format(elems[2])) + return list end -local function parse_types(arg) - local types = argparse.stringList(arg) - for _,v in ipairs(types) do - if not type_fns[v] then - qerror(('unrecognized type: "%s"'):format(v)) +local function status() + local running_str = state.enabled and 'Running' or 'Would run' + print(('deteriorate is %s'):format(state.enabled and 'enabled' or 'disabled')) + print() + for _,category in ipairs(categories) do + local status_str = 'Stopped' + local category_data = state.categories[category] + if category_data.enabled then + status_str = ('%s every %s day%s') :format(running_str, + category_data.frequency, category_data.frequency == 1 and '' or 's') end + print(('%18s: %s'):format(category, status_str)) end - return types end -local opts = { - time = 1, - mode = 'days', - quiet = false, - types = {}, - help = false, -} +local help = false -local nonoptions = argparse.processArgsGetopt({...}, { - {'f', 'freq', 'frequency', hasArg=true, - handler=function(optarg) opts.time,opts.mode = parse_freq(optarg) end}, - {'h', 'help', handler=function() opts.help = true end}, - {'q', 'quiet', handler=function() opts.quiet = true end}, - {'t', 'types', hasArg=true, - handler=function(optarg) opts.types = parse_types(optarg) end}}) +local positionals = argparse.processArgsGetopt({...}, { + {'h', 'help', handler=function() help = true end}, +}) -local command = nonoptions[1] -if not command or not command_switch[command] then opts.help = true end +local command = table.remove(positionals, 1) +if command == 'help' or help then + print(dfhack.script_help()) + return +end -if not opts.help and command ~= 'status' and #opts.types == 0 then - qerror('no item types specified! try adding a --types parameter.') +if not command or command == 'status' then + status() +elseif command == 'enable' then + local cats = parse_categories(positionals[1]) + for _,v in ipairs(cats) do + if state.categories[v].enabled then + goto continue + end + state.categories[v].enabled = true + if state.enabled then + start_category(v) + end + ::continue:: + end +elseif command == 'disable' then + local cats = parse_categories(positionals[1]) + for _,v in ipairs(cats) do + if not state.categories[v].enabled then + goto continue + end + state.categories[v].enabled = false + if state.enabled then + stop_category(v) + end + ::continue:: + end +elseif command == 'frequency' or command == 'freq' then + local freq = tonumber(positionals[1]) + if not freq or freq <= 0 then + qerror('frequency must be greater than 0') + end + local cats = parse_categories(positionals[2]) + for _,v in ipairs(cats) do + state.categories[v].frequency = freq + if state.enabled then + stop_category(v) + start_category(v) + end + end +elseif command == 'now' then + local cats = parse_categories(positionals[1]) + local count = 0 + for _,v in ipairs(cats) do + count = count + category_fns[v](true) + end + print(('Deteriorated %d item%s'):format(count, count == 1 and '' or 's')) +else + qerror('unrecognized command: "' .. command .. '"') end -(command_switch[command] or help)(opts) +persist_state() diff --git a/devel/all-bob.lua b/devel/all-bob.lua index ef4dc32d0a..59a7fe09d4 100644 --- a/devel/all-bob.lua +++ b/devel/all-bob.lua @@ -1,15 +1,6 @@ -- Changes the first name of all units to "Bob" --author expwnent --- ---[====[ -devel/all-bob -============= -Changes the first name of all units to "Bob". -Useful for testing `modtools/interaction-trigger` events. - -]====] - -for _,v in ipairs(df.global.world.units.all) do +for _,v in ipairs(df.global.world.units.active) do v.name.first_name = "Bob" end diff --git a/devel/block-borders.lua b/devel/block-borders.lua index 5e5b1b5ea5..445bdcaab5 100644 --- a/devel/block-borders.lua +++ b/devel/block-borders.lua @@ -1,19 +1,8 @@ -- overlay that displays map block borders ---[====[ - -devel/block-borders -=================== - -An overlay that draws borders of map blocks. See :doc:`/docs/api/Maps` for -details on map blocks. - -]====] - -local gui = require "gui" +local gui = require('gui') local guidm = require "gui.dwarfmode" - -local ui = df.global.ui +local widgets = require('gui.widgets') local DRAW_CHARS = { ns = string.char(179), @@ -23,64 +12,39 @@ local DRAW_CHARS = { se = string.char(218), sw = string.char(191), } --- persist across script runs -color = color or COLOR_LIGHTCYAN -BlockBordersOverlay = defclass(BlockBordersOverlay, guidm.MenuOverlay) -BlockBordersOverlay.ATTRS{ - block_size = 16, - draw_borders = true, +BlockBorders = defclass(BlockBorders, widgets.Window) +BlockBorders.ATTRS { + frame_title='Block Borders', + frame={t=20, r=3, w=29, h=7}, + autoarrange_subviews=true, + autoarrange_gap=1, } -function BlockBordersOverlay:onInput(keys) - if keys.LEAVESCREEN then - self:dismiss() - elseif keys.D_PAUSE then - self.draw_borders = not self.draw_borders - elseif keys.CUSTOM_B then - self.block_size = self.block_size == 16 and 48 or 16 - elseif keys.CUSTOM_C then - color = color + 1 - if color > 15 then - color = 1 - end - elseif keys.CUSTOM_SHIFT_C then - color = color - 1 - if color < 1 then - color = 15 - end - elseif keys.D_LOOK then - self:sendInputToParent(ui.main.mode == df.ui_sidebar_mode.LookAround and 'LEAVESCREEN' or 'D_LOOK') - else - self:propagateMoveKeys(keys) - end -end - -function BlockBordersOverlay:onRenderBody(dc) - dc = dc:viewport(1, 1, dc.width - 2, dc.height - 2) - dc:key_string('D_PAUSE', 'Toggle borders') - :newline() - dc:key_string('CUSTOM_B', self.block_size == 16 and '1 block (16 tiles)' or '3 blocks (48 tiles)') - :newline() - dc:key('CUSTOM_C') - :string(', ') - :key_string('CUSTOM_SHIFT_C', 'Color: ') - :string('Example', color) - :newline() - dc:key_string('D_LOOK', 'Toggle cursor') - :newline() - - self:renderOverlay() +function BlockBorders:init() + self:addviews{ + widgets.ToggleHotkeyLabel{ + view_id='draw', + key='CUSTOM_CTRL_D', + label='Draw borders:', + initial_option=true, + }, + widgets.CycleHotkeyLabel{ + view_id='size', + key='CUSTOM_CTRL_B', + label=' Block size:', + options={16, 48}, + }, + } end -function BlockBordersOverlay:renderOverlay() - if not self.draw_borders then return end - - local block_end = self.block_size - 1 - self:renderMapOverlay(function(pos, is_cursor) +function BlockBorders:render_overlay() + local block_size = self.subviews.size:getOptionValue() + local block_end = block_size - 1 + guidm.renderMapOverlay(function(pos, is_cursor) if is_cursor then return end - local block_x = pos.x % self.block_size - local block_y = pos.y % self.block_size + local block_x = pos.x % block_size + local block_y = pos.y % block_size local key if block_x == 0 and block_y == 0 then key = 'se' @@ -95,16 +59,34 @@ function BlockBordersOverlay:renderOverlay() elseif block_y == 0 or block_y == block_end then key = 'ew' end - return DRAW_CHARS[key], color or COLOR_LIGHTCYAN + if not key then return nil end + return COLOR_LIGHTCYAN, DRAW_CHARS[key] end) end +function BlockBorders:onRenderFrame(dc, rect) + if self.subviews.draw:getOptionValue() then + self:render_overlay() + end + BlockBorders.super.onRenderFrame(self, dc, rect) +end + +BlockBordersScreen = defclass(BlockBordersScreen, gui.ZScreen) +BlockBordersScreen.ATTRS { + focus_path='block-borders', + pass_movement_keys=true, +} + +function BlockBordersScreen:init() + self:addviews{BlockBorders{}} +end + +function BlockBordersScreen:onDismiss() + view = nil +end + if not dfhack.isMapLoaded() then qerror('This script requires a fortress map to be loaded') end --- we can work both with a cursor and without one. start in a mode that mirrors --- the current game state -local is_cursor = not not guidm.getCursorPos() -local sidebar_mode = df.ui_sidebar_mode[is_cursor and 'LookAround' or 'Default'] -BlockBordersOverlay{sidebar_mode=sidebar_mode}:show() +view = view and view:raise() or BlockBordersScreen{}:show() diff --git a/devel/check-release.lua b/devel/check-release.lua index 96cf66838a..b690ac1a30 100644 --- a/devel/check-release.lua +++ b/devel/check-release.lua @@ -1,9 +1,4 @@ -- basic check for release readiness ---[====[ -devel/check-release -=================== -Basic checks for release readiness -]====] local ok = true function err(s) diff --git a/devel/click-monitor.lua b/devel/click-monitor.lua index 0bace7d436..685a21fb91 100644 --- a/devel/click-monitor.lua +++ b/devel/click-monitor.lua @@ -1,28 +1,7 @@ -- Displays the mouse (grid) coordinates when the mouse is clicked --@ enable = true ---[====[ - -devel/click-monitor -=================== -Displays the grid coordinates of mouse clicks in the console. -Useful for plugin/script development. - -Usage: ``devel/click-monitor start|stop`` - -]====] - -VERSION = '0.2' - active = active or false -function usage() - print [[ -Usage: - click-monitor enable|start: Begin monitoring - click-monitor disable|stop: End monitoring -]] -end - function set_timeout() dfhack.timeout(1, 'frames', check_click) end @@ -42,8 +21,8 @@ end function check_click() local s = '' local color = COLOR_RESET - for _, attr in pairs({'mouse_lbut', 'mouse_rbut', 'mouse_lbut_down', - 'mouse_rbut_down', 'mouse_lbut_lift', 'mouse_rbut_lift'}) do + for _, attr in pairs({'mouse_lbut', 'mouse_rbut', 'mouse_mbut', 'mouse_lbut_down', + 'mouse_rbut_down', 'mouse_mbut_down', 'mouse_lbut_lift', 'mouse_rbut_lift', 'mouse_mbut_lift'}) do local enabler = df.global.enabler --as:number[] if enabler[attr] ~= 0 then s = s .. '[' .. attr:sub(7) .. '] ' @@ -74,8 +53,8 @@ if #args == 1 then elseif args[1] == 'stop' or args[1] == 'disable' then active = false else - usage() + print(dfhack.script_help()) end else - usage() + print(dfhack.script_help()) end diff --git a/devel/dump-offsets.lua b/devel/dump-offsets.lua index 1676115a9a..5f49ce1662 100644 --- a/devel/dump-offsets.lua +++ b/devel/dump-offsets.lua @@ -1,173 +1,9 @@ -- Dump all global addresses ---[====[ - -devel/dump-offsets -================== - -.. warning:: - - THIS SCRIPT IS STRICTLY FOR DFHACK DEVELOPERS. - - Running this script on a new DF version will NOT - MAKE IT RUN CORRECTLY if any data structures - changed, thus possibly leading to CRASHES AND/OR - PERMANENT SAVE CORRUPTION. - -This dumps the contents of the table of global addresses (new in 0.44.01). - -Passing global names as arguments calls setAddress() to set those globals' -addresses in-game. Passing "all" does this for all globals. - -]====] - -GLOBALS = { - version = "version", - min_load_version = "min_load_version", - movie_version = "movie_version", - basic_seed = "basic_seed", - enabler = "enabler", - cursor = "cursor", - point = "selection_rect", - gamemode = "gamemode", - gametype = "gametype", - menuposition = "ui_menu_width", - itemmade = "created_item_type", - itemmade_subtype = "created_item_subtype", - itemmade_subcat1 = "created_item_mattype", - itemmade_subcat2 = "created_item_matindex", - itemmade_number = "created_item_count", - mainview = "map_renderer", - d_init = "d_init", - title = "title", - title2 = "title_spaced", - event_flow = "flows", - gps = "gps", - gview = "gview", - init = "init", - texture = "texture", - plot_event = "timed_events", - plotinfo = "ui", - adventure = "ui_advmode", - buildreq = "ui_build_selector", - buildjob_type = "ui_building_assign_type", - buildjob_selected = "ui_building_assign_is_marked", - buildjob_unit = "ui_building_assign_units", - buildjob_item = "ui_building_assign_items", - looklist = "ui_look_list", - game = "ui_sidebar_menus", - world = "world", - year = "cur_year", - season_count = "cur_year_tick", - precise_phase = "cur_year_tick_advmode", - season_timer = "cur_season_tick", - season = "cur_season", - cur_weather = "current_weather", - assignbuildingjobs = "process_jobs", - assigndesjobs = "process_dig", - paused = "pause_state", - modeunit = "ui_selected_unit", - modeview = "ui_unit_view_mode", - modepage = "ui_look_cursor", - modeitem = "ui_building_item_cursor", - addingtask = "ui_workshop_in_add", - modejob = "ui_workshop_job_cursor", - buildjob_assignroom = "ui_building_in_assign", - buildjob_sizeroom = "ui_building_in_resize", - addingtask_sub = "ui_lever_target_type", - buildjob_sizerad = "ui_building_resize_radius", - scrollx = "window_x", - scrolly = "window_y", - scrollz = "window_z", - DEBUG_CONTINUOUS = "debug_nopause", - DEBUG_NOMOOD = "debug_nomoods", - DEBUG_SAFEDWARVES = "debug_combat", - DEBUG_NOANIMALS = "debug_wildlife", - DEBUG_NOTHIRST = "debug_nodrink", - DEBUG_NOHUNGER = "debug_noeat", - DEBUG_NOSLEEP = "debug_nosleep", - DEBUG_VISIBLEAMBUSHERS = "debug_showambush", - DEBUG_QUICKMODE_MINING = "debug_fastmining", - DEBUG_NEVERBERSERK = "debug_noberserk", - DEBUG_MEGAFAST = "debug_turbospeed", - gamemode_cansave = "save_on_exit", - standingorder_butcher = "standing_orders_auto_butcher", - standingorder_collect_web = "standing_orders_auto_collect_webs", - standingorder_fishery = "standing_orders_auto_fishery", - standingorder_kiln = "standing_orders_auto_kiln", - standingorder_kitchen = "standing_orders_auto_kitchen", - standingorder_loom = "standing_orders_auto_loom", - standingorder_other = "standing_orders_auto_other", - standingorder_slaughter = "standing_orders_auto_slaughter", - standingorder_smelter = "standing_orders_auto_smelter", - standingorder_tan = "standing_orders_auto_tan", - standingorder_gatherrefuse_chasm_bones = "standing_orders_dump_bones", - standingorder_gatherrefuse_chasm_corpses = "standing_orders_dump_corpses", - standingorder_gatherrefuse_chasm_strand_tissue = "standing_orders_dump_hair", - standingorder_gatherrefuse_chasm_othernonmetal = "standing_orders_dump_other", - standingorder_gatherrefuse_chasm_shell = "standing_orders_dump_shells", - standingorder_gatherrefuse_chasm_skins = "standing_orders_dump_skins", - standingorder_gatherrefuse_chasm_skulls = "standing_orders_dump_skulls", - standingorder_allharvest = "standing_orders_farmer_harvest", - standingorder_autoforbid_other_items = "standing_orders_forbid_other_dead_items", - standingorder_autoforbid_other_corpse = "standing_orders_forbid_other_nohunt", - standingorder_autoforbid_your_corpse = "standing_orders_forbid_own_dead", - standingorder_autoforbid_your_items = "standing_orders_forbid_own_dead_items", - standingorder_autoforbid_projectile = "standing_orders_forbid_used_ammo", - standingorder_gatheranimals = "standing_orders_gather_animals", - standingorder_gatherbodies = "standing_orders_gather_bodies", - standingorder_gatherfood = "standing_orders_gather_food", - standingorder_gatherfurniture = "standing_orders_gather_furniture", - standingorder_gatherstone = "standing_orders_gather_minerals", - standingorder_gatherrefuse = "standing_orders_gather_refuse", - standingorder_gatherrefuse_outside = "standing_orders_gather_refuse_outside", - standingorder_gatherrefuse_outside_vermin = "standing_orders_gather_vermin_remains", - standingorder_gatherwood = "standing_orders_gather_wood", - option_exceptions = "standing_orders_job_cancel_announce", - standingorder_mixfoods = "standing_orders_mix_food", - standingorder_dyed_clothes = "standing_orders_use_dyed_cloth", - standingorder_zone_drinking = "standing_orders_zoneonly_drink", - standingorder_zone_fishing = "standing_orders_zoneonly_fish", - next_activity_global_id = "activity_next_id", - next_agreement_global_id = "agreement_next_id", - next_army_controller_global_id = "army_controller_next_id", - next_army_global_id = "army_next_id", - next_army_tracking_info_global_id = "army_tracking_info_next_id", - next_art_imagechunk_global_id = "art_image_chunk_next_id", - next_artifact_global_id = "artifact_next_id", - next_belief_system_global_id = "belief_system_next_id", - next_building_global_id = "building_next_id", - next_crime_global_id = "crime_next_id", - next_cultural_identity_global_id = "cultural_identity_next_id", - next_dance_form_global_id = "dance_form_next_id", - next_civ_global_id = "entity_next_id", - next_flow_guide_global_id = "flow_guide_next_id", - next_formation_global_id = "formation_next_id", - next_histeventcol_global_id = "hist_event_collection_next_id", - next_histevent_global_id = "hist_event_next_id", - next_histfig_global_id = "hist_figure_next_id", - next_identity_global_id = "identity_next_id", - next_incident_global_id = "incident_next_id", - next_interaction_instance_global_id = "interaction_instance_next_id", - next_item_global_id = "item_next_id", - next_job_global_id = "job_next_id", - next_machine_global_id = "machine_next_id", - next_musical_form_global_id = "musical_form_next_id", - next_nem_global_id = "nemesis_next_id", - next_occupation_global_id = "occupation_next_id", - next_poetic_form_global_id = "poetic_form_next_id", - next_proj_global_id = "proj_next_id", - next_rhythm_global_id = "rhythm_next_id", - next_scale_global_id = "scale_next_id", - next_schedule_global_id = "schedule_next_id", - next_soul_global_id = "soul_next_id", - next_squad_global_id = "squad_next_id", - next_task_global_id = "task_next_id", - next_unitchunk_global_id = "unit_chunk_next_id", - next_unit_global_id = "unit_next_id", - next_vehicle_global_id = "vehicle_next_id", - next_written_content_global_id = "written_content_next_id", -} +GLOBALS = {} +for k, v in pairs(df.global._fields) do + GLOBALS[v.original_name] = k +end function read_cstr(addr) local s = '' @@ -188,29 +24,44 @@ local data = ms.get_data_segment() or qerror('Could not find data segment') local search if dfhack.getArchitecture() == 64 then - search = {0x1234567812345678, 0x8765432187654321} + search = {0x1234567812345678, 0x8765432187654321, 0x89abcdef89abcdef} else - search = {0x12345678, 0x87654321} + search = {0x12345678, 0x87654321, 0x89abcdef} end -local addrs = {} function save_addr(name, addr) print((""):format(name, addr)) if iargs[name] or iargs.all then ms.found_offset(name, addr) end - addrs[name] = addr end -local start = data.intptr_t:find_one(search) +local extended = false +local start, start_addr = data.intptr_t:find_one(search) +if start then + extended = true +else + -- try searching for a non-extended table + table.remove(search, #search) + start = data.intptr_t:find_one(search) +end +if not start then + qerror('Could not find global table header') +end + +if extended then + -- structures only has types for an extended global table + save_addr('global_table', start_addr + (#search * data.intptr_t.esize)) +end local index = 1 +local entry_size = (extended and 3 or 2) while true do - local p_name = data.intptr_t[start + (index * 2)] + local p_name = data.intptr_t[start + (index * entry_size)] if p_name == 0 then break end - local g_addr = data.intptr_t[start + (index * 2) + 1] + local g_addr = data.intptr_t[start + (index * entry_size) + 1] local df_name = read_cstr(p_name) local g_name = GLOBALS[df_name] if df_name:find('^index[12]_') then @@ -224,3 +75,5 @@ while true do end index = index + 1 end + +print('global table length:', index) diff --git a/devel/dump-tooltip-ids.lua b/devel/dump-tooltip-ids.lua new file mode 100644 index 0000000000..5a5c044d72 --- /dev/null +++ b/devel/dump-tooltip-ids.lua @@ -0,0 +1,49 @@ +df_captions = {} -- bimap: DF ID <> DF caption text +dfhack_captions = {} -- bimap: DFHack ID <> DF caption text +hover_instruction = df.global.game.main_interface.hover_instruction + +function xmlescape(s) + return s:gsub("'", '''):gsub('<', '<'):gsub('>', '>') +end + +for i, lines in ipairs(hover_instruction) do + local text = '' + for _, line in ipairs(lines) do + text = text .. ' ' .. line.value + end + text = text:trim():gsub('%s+', ' ') + df_captions[i] = text + df_captions[text] = i +end + +for i in ipairs(df.main_hover_instruction) do + local text = df.main_hover_instruction.attrs[i].caption + dfhack_captions[i] = text + dfhack_captions[text] = i +end + +print(" ") +print(" generated by devel/dump-tooltip-ids") +print(" ") +print("") +for i in ipairs(hover_instruction) do + if i % 10 == 0 then + print(" " .. i) + end + local dfhack_name = nil + if dfhack_captions[df_captions[i]] then + -- known caption, use the enum item name that DFHack has for it + dfhack_name = df.main_hover_instruction[dfhack_captions[df_captions[i]]] + end + + print((" "):format(dfhack_name and (" name='%s'"):format(xmlescape(dfhack_name)) or '')) + print((" "):format(xmlescape(df_captions[i]))) + print(" ") +end +print(" ") + +for k, id in pairs(dfhack_captions) do + if type(k) == 'string' and not df_captions[k] then + dfhack.printerr(('Unmatched caption: %s: was ID %d, key %s'):format(k, id, df.main_hover_instruction[id])) + end +end diff --git a/devel/eventful-client.lua b/devel/eventful-client.lua index 280a28011e..0fd6b83c67 100644 --- a/devel/eventful-client.lua +++ b/devel/eventful-client.lua @@ -62,8 +62,10 @@ local function help() end end -local function make_handler_fn(registry_entry, freq) +local function make_handler_fn(registry_entry, handler_name, freq) return function(...) + local handler = handlers[handler_name] + handler.count = handler.count + 1 print(('eventful-client: %s received %s event (freq=%d)') :format(os.date("%X"), registry_entry.etype, freq)) print(' params:', ...) @@ -80,7 +82,7 @@ local function add_one(registry_entry, freq) end eventful.enableEvent(eventful.eventType[registry_entry.etype], freq) local handler_name = 'eventful-client.'..tostring(rng:random()) - local handler = make_handler_fn(registry_entry, freq) + local handler = make_handler_fn(registry_entry, handler_name, freq) print(('eventful-client registering new %s handler at freq %d: %s') :format(registry_entry.etype, freq, handler_name)) eventful[registry_entry.fn][handler_name] = handler diff --git a/devel/export-dt-ini.lua b/devel/export-dt-ini.lua index 6cc522ead6..5bf5eadd6c 100644 --- a/devel/export-dt-ini.lua +++ b/devel/export-dt-ini.lua @@ -1,18 +1,11 @@ -- Exports an ini file for Dwarf Therapist. ---[====[ -devel/export-dt-ini -=================== -Exports an ini file containing memory addresses for Dwarf Therapist. -]====] -local utils = require 'utils' local ms = require 'memscan' -- Utility functions local globals = df.global local global_addr = dfhack.internal.getAddress -local os_type = dfhack.getOSType() local rdelta = dfhack.internal.getRebaseDelta() local lines = {} --as:string[] local complete = true @@ -72,9 +65,9 @@ end header('addresses') address('cur_year_tick',globals,'cur_year_tick') address('current_year',globals,'cur_year') -address('dwarf_civ_index',globals,'ui','civ_id') -address('dwarf_race_index',globals,'ui','race_id') -address('fortress_entity',globals,'ui','main','fortress_entity') +address('dwarf_civ_index',globals,'plotinfo','civ_id') +address('dwarf_race_index',globals,'plotinfo','race_id') +address('fortress_entity',globals,'plotinfo','main','fortress_entity') address('historical_entities_vector',globals,'world','entities','all') address('creature_vector',globals,'world','units','all') address('active_creature_vector',globals,'world','units','active') @@ -99,7 +92,7 @@ address('musical_forms_vector',globals,'world','musical_forms','all') address('dance_forms_vector',globals,'world','dance_forms','all') address('occupations_vector',globals,'world','occupations','all') address('world_data',globals,'world','world_data') -address('material_templates_vector',globals,'world','raws','material_templates') +address('material_templates_vector',globals,'world','raws','material_templates','all') address('inorganics_vector',globals,'world','raws','inorganics') address('plants_vector',globals,'world','raws','plants','all') address('races_vector',globals,'world','raws','creatures','all') @@ -123,12 +116,14 @@ address('colors_vector',globals,'world','raws','descriptors','colors') address('shapes_vector',globals,'world','raws','descriptors','shapes') address('reactions_vector',globals,'world','raws','reactions') address('base_materials',globals,'world','raws','mat_table','builtin') -address('all_syndromes_vector',globals,'world','raws','syndromes','all') +address('all_syndromes_vector',globals,'world','raws','mat_table','syndromes','all') address('events_vector',globals,'world','history','events') address('historical_figures_vector',globals,'world','history','figures') address('world_site_type',df.world_site,'type') address('active_sites_vector',df.world_data,'active_site') address('gview',globals,'gview') +address('external_flag',globals,'game','external_flag') +address('global_equipment_update',globals,'plotinfo','equipment','update') vtable('viewscreen_setupdwarfgame_vtable','viewscreen_setupdwarfgamest') header('offsets') @@ -174,7 +169,7 @@ address('tissues_vector',df.creature_raw,'tissue') header('caste_offsets') address('caste_name',df.caste_raw,'caste_name') address('caste_descr',df.caste_raw,'description') -address('caste_trait_ranges',df.caste_raw,'personality','a') +address('caste_trait_ranges',df.caste_raw,'personality','min') address('caste_phys_att_ranges',df.caste_raw,'attributes','phys_att_range') address('baby_age',df.caste_raw,'misc','baby_age') address('child_age',df.caste_raw,'misc','child_age') @@ -206,7 +201,7 @@ address('hist_name',df.historical_figure,'name') address('id',df.historical_figure,'id') address('hist_fig_info',df.historical_figure,'info') address('reputation',df.historical_figure_info,'reputation') -address('current_ident',df.historical_figure_info.T_reputation,'cur_identity') +address('current_ident',df.reputation_profilest,'cur_identity') address('fake_name',df.identity,'name') address('fake_birth_year',df.identity,'birth_year') address('fake_birth_time',df.identity,'birth_second') @@ -226,10 +221,10 @@ address('id',df.item,'id') address('general_refs',df.item,'general_refs') address('stack_size',df.item_actual,'stack_size') address('wear',df.item_actual,'wear') -address('mat_type',df.item_crafted,'mat_type') -address('mat_index',df.item_crafted,'mat_index') -address('maker_race',df.item_crafted,'maker_race') -address('quality',df.item_crafted,'quality') +address('mat_type',df.item_constructed,'mat_type') -- should be item_crafted +address('mat_index',df.item_constructed,'mat_index') -- should be item_crafted +address('maker_race',df.item_constructed,'maker_race') -- should be item_crafted +address('quality',df.item_constructed,'quality') -- should be item_crafted address('artifact_id',df.artifact_record,'id') address('artifact_name',df.artifact_record,'name') @@ -242,12 +237,6 @@ address('adjective',df.itemdef_armorst,'name_preplural') address('tool_flags',df.itemdef_toolst,'flags') address('tool_adjective',df.itemdef_toolst,'adjective') -header('item_filter_offsets') -address('item_subtype',df.item_filter_spec,'item_subtype') -address('mat_class',df.item_filter_spec,'material_class') -address('mat_type',df.item_filter_spec,'mattype') -address('mat_index',df.item_filter_spec,'matindex') - header('weapon_subtype_offsets') address('single_size',df.itemdef_weaponst,'two_handed') address('multi_size',df.itemdef_weaponst,'minimum_size') @@ -266,14 +255,14 @@ address('pants_armor_properties',df.itemdef_pantsst,'props') address('other_armor_properties',df.itemdef_helmst,'props') header('material_offsets') -address('solid_name',df.material_common,'state_name','Solid') -address('liquid_name',df.material_common,'state_name','Liquid') -address('gas_name',df.material_common,'state_name','Gas') -address('powder_name',df.material_common,'state_name','Powder') -address('paste_name',df.material_common,'state_name','Paste') -address('pressed_name',df.material_common,'state_name','Pressed') -address('flags',df.material_common,'flags') -address('reaction_class',df.material_common,'reaction_class') +address('solid_name',df.material,'state_name','Solid') +address('liquid_name',df.material,'state_name','Liquid') +address('gas_name',df.material,'state_name','Gas') +address('powder_name',df.material,'state_name','Powder') +address('paste_name',df.material,'state_name','Paste') +address('pressed_name',df.material,'state_name','Pressed') +address('flags',df.material,'flags') +address('reaction_class',df.material,'reaction_class') address('prefix',df.material,'prefix') address('inorganic_materials_vector',df.inorganic_raw,'material') address('inorganic_flags',df.inorganic_raw,'flags') @@ -319,19 +308,19 @@ address('civ',df.unit,'civ_id') address('specific_refs',df.unit,'specific_refs') address('squad_id',df.unit,'military','squad_id') address('squad_position',df.unit,'military','squad_position') -address('recheck_equipment',df.unit,'military','pickup_flags') +address('recheck_equipment',df.unit,'uniform','pickup_flags') address('mood',df.unit,'mood') address('birth_year',df.unit,'birth_year') address('birth_time',df.unit,'birth_time') -address('pet_owner_id',df.unit,'relationship_ids',df.unit_relationship_type.Pet) +address('pet_owner_id',df.unit,'relationship_ids',df.unit_relationship_type.PetOwner) address('current_job',df.unit,'job','current_job') address('physical_attrs',df.unit,'body','physical_attrs') address('body_size',df.unit,'appearance','body_modifiers') address('size_info',df.unit,'body','size_info','size_cur') address('size_base',df.unit,'body','size_info','size_base') -address('curse',df.unit,'curse','name') -address('curse_add_flags1',df.unit,'curse','add_tags1') -address('turn_count',df.unit,'curse','time_on_site') +address('curse',df.unit,'uwss_display_name_sing') +address('curse_add_flags1',df.unit,'uwss_add_caste_flag') +address('turn_count',df.unit,'usable_interaction','time_on_site') address('souls',df.unit,'status','souls') address('states',df.unit,'status','misc_traits') address('labors',df.unit,'status','labors') @@ -347,7 +336,7 @@ address('counters3',df.unit, 'counters2','paralysis') address('limb_counters',df.unit,'status2','limbs_stand_max') address('blood',df.unit,'body','blood_max') address('body_component_info',df.unit,'body','components') -address('layer_status_vector',df.body_component_info,'layer_status') +address('layer_status_vector',df.unit.T_body.T_components,'layer_status') address('wounds_vector',df.unit,'body','wounds') address('mood_skill',df.unit,'job','mood_skill') address('used_items_vector',df.unit,'used_items') @@ -367,16 +356,16 @@ address('trans_race_vec',df.creature_interaction_effect_body_transformationst,'r header('unit_wound_offsets') address('parts',df.unit_wound,'parts') -address('id',df.unit_wound.T_parts,'body_part_id') -address('layer',df.unit_wound.T_parts,'layer_idx') +address('id',df.unit_wound_layerst,'body_part_id') +address('layer',df.unit_wound_layerst,'layer_idx') address('general_flags',df.unit_wound,'flags') -address('flags1',df.unit_wound.T_parts,'flags1') -address('flags2',df.unit_wound.T_parts,'flags2') -address('effects_vector',df.unit_wound.T_parts,'effect_type') -address('bleeding',df.unit_wound.T_parts,'bleeding') -address('pain',df.unit_wound.T_parts,'pain') -address('cur_pen',df.unit_wound.T_parts,'cur_penetration_perc') -address('max_pen',df.unit_wound.T_parts,'max_penetration_perc') +address('flags1',df.unit_wound_layerst,'flags1') +address('flags2',df.unit_wound_layerst,'flags2') +address('effects_vector',df.unit_wound_layerst,'effect_type') +address('bleeding',df.unit_wound_layerst,'bleeding') +address('pain',df.unit_wound_layerst,'pain') +address('cur_pen',df.unit_wound_layerst,'cur_penetration_perc') +address('max_pen',df.unit_wound_layerst,'max_penetration_perc') header('soul_details') address('name',df.unit_soul,'name') @@ -388,9 +377,9 @@ address('personality',df.unit_soul,'personality') address('beliefs',df.unit_personality,'values') address('emotions',df.unit_personality,'emotions') address('goals',df.unit_personality,'dreams') -address('goal_realized',df.unit_personality.T_dreams,'flags') +address('goal_realized',df.personality_goalst,'flags') address('traits',df.unit_personality,'traits') -address('stress_level',df.unit_personality,'stress_level') +address('stress_level',df.unit_personality,'stress') address('needs',df.unit_personality,'needs') address('current_focus',df.unit_personality,'current_focus') address('undistracted_focus',df.unit_personality,'undistracted_focus') @@ -398,19 +387,19 @@ address('combat_hardened',df.unit_personality,'combat_hardened') address('likes_outdoors',df.unit_personality,'likes_outdoors') header('need_offsets') -address('id',df.unit_personality.T_needs,'id') -address('deity_id',df.unit_personality.T_needs,'deity_id') -address('focus_level',df.unit_personality.T_needs,'focus_level') -address('need_level',df.unit_personality.T_needs,'need_level') +address('id',df.personality_needst,'id') +address('deity_id',df.personality_needst,'deity_id') +address('focus_level',df.personality_needst,'focus_level') +address('need_level',df.personality_needst,'need_level') header('emotion_offsets') -address('emotion_type',df.unit_personality.T_emotions,'type') -address('strength',df.unit_personality.T_emotions,'strength') -address('thought_id',df.unit_personality.T_emotions,'thought') -address('sub_id',df.unit_personality.T_emotions,'subthought') -address('level',df.unit_personality.T_emotions,'severity') -address('year',df.unit_personality.T_emotions,'year') -address('year_tick',df.unit_personality.T_emotions,'year_tick') +address('emotion_type',df.personality_moodst,'type') +address('strength',df.personality_moodst,'relative_strength') +address('thought_id',df.personality_moodst,'thought') +address('sub_id',df.personality_moodst,'subthought') +address('level',df.personality_moodst,'severity') +address('year',df.personality_moodst,'year') +address('year_tick',df.personality_moodst,'year_tick') header('job_details') address('id',df.job,'job_type') @@ -431,23 +420,28 @@ address('schedules',df.squad,'schedule') value('sched_size',df.squad_schedule_entry:sizeof()) address('sched_orders',df.squad_schedule_entry,'orders') address('sched_assign',df.squad_schedule_entry,'order_assignments') -address('alert',df.squad,'cur_alert_idx') -address('carry_food',df.squad,'carry_food') -address('carry_water',df.squad,'carry_water') -address('ammunition',df.squad,'ammunition') +address('alert',df.squad,'cur_routine_idx') +address('carry_food',df.squad,'supplies','carry_food') +address('carry_water',df.squad,'supplies','carry_water') +address('ammunition',df.squad,'ammo','ammunition') address('ammunition_qty',df.squad_ammo_spec,'amount') -address('quiver',df.squad_position,'quiver') -address('backpack',df.squad_position,'backpack') -address('flask',df.squad_position,'flask') -address('armor_vector',df.squad_position,'uniform','body') -address('helm_vector',df.squad_position,'uniform','head') -address('pants_vector',df.squad_position,'uniform','pants') -address('gloves_vector',df.squad_position,'uniform','gloves') -address('shoes_vector',df.squad_position,'uniform','shoes') -address('shield_vector',df.squad_position,'uniform','shield') -address('weapon_vector',df.squad_position,'uniform','weapon') -address('uniform_item_filter',df.squad_uniform_spec,'item_filter') +address('quiver',df.squad_position,'equipment','quiver') +address('backpack',df.squad_position,'equipment','backpack') +address('flask',df.squad_position,'equipment','flask') +address('armor_vector',df.squad_position,'equipment','uniform','body') +address('helm_vector',df.squad_position,'equipment','uniform','head') +address('pants_vector',df.squad_position,'equipment','uniform','pants') +address('gloves_vector',df.squad_position,'equipment','uniform','gloves') +address('shoes_vector',df.squad_position,'equipment','uniform','shoes') +address('shield_vector',df.squad_position,'equipment','uniform','shield') +address('weapon_vector',df.squad_position,'equipment','uniform','weapon') +address('uniform_spec_item_type',df.squad_uniform_spec,'item_type') +address('uniform_spec_item_subtype',df.squad_uniform_spec,'item_subtype') +address('uniform_spec_mat_class',df.squad_uniform_spec,'material_class') +address('uniform_spec_mat_type',df.squad_uniform_spec,'mattype') +address('uniform_spec_mat_index',df.squad_uniform_spec,'matindex') address('uniform_indiv_choice',df.squad_uniform_spec,'indiv_choice') +address('equipment_update',df.squad,'ammo','update') header('activity_offsets') address('activity_type',df.activity_entry,'type') @@ -458,11 +452,11 @@ address('sq_skill',df.activity_event_skill_demonstrationst,'skill') address('sq_train_rounds',df.activity_event_skill_demonstrationst,'train_rounds') address('pray_deity',df.activity_event_prayerst,'histfig_id') address('pray_sphere',df.activity_event_prayerst,'topic') -address('knowledge_category',df.activity_event_ponder_topicst,'knowledge','flag_type') -address('knowledge_flag',df.activity_event_ponder_topicst,'knowledge','flag_data') +address('knowledge_category',df.activity_event_ponder_topicst,'topic','research','flag_type') +address('knowledge_flag',df.activity_event_ponder_topicst,'topic','research','flag_data') address('perf_type',df.activity_event_performancest,'type') address('perf_participants',df.activity_event_performancest,'participant_actions') -address('perf_histfig',df.activity_event_performancest.T_participant_actions,'histfig_id') +address('perf_histfig',df.performance_rolest,'histfig_id') header('art_offsets') address('name',df.poetic_form,'name') @@ -470,7 +464,7 @@ address('name',df.poetic_form,'name') header('viewscreen_offsets') address('view',df.interfacest,'view') address('child',df.viewscreen,'child') -address('setupdwarfgame_units',df.viewscreen_setupdwarfgamest,'units') +address('setupdwarfgame_units',df.viewscreen_setupdwarfgamest,'s_unit') -- Final creation of the file @@ -509,7 +503,6 @@ end write_flags('valid_flags_2', {}) write_flags('invalid_flags_1', { - { 'a skeleton', { df.unit_flags1.skeleton } }, { 'a merchant', { df.unit_flags1.merchant } }, { 'outpost liaison, diplomat, or artifact requesting visitor', { df.unit_flags1.diplomat } }, { 'an invader or hostile', { df.unit_flags1.active_invader } }, diff --git a/devel/export-map.lua b/devel/export-map.lua new file mode 100644 index 0000000000..d01844a54b --- /dev/null +++ b/devel/export-map.lua @@ -0,0 +1,334 @@ +-- Export fortress map tile data to a JSON file +-- based on export-map.lua by mikerenfro: +-- https://github.com/mikerenfro/df-map-export/blob/main/export-map.lua +-- redux version by timothymtorres + +local tm = require('tile-material') +local utils = require('utils') +local json = require('json') +local argparse = require('argparse') + +local underworld_z +local underworld +local evilness + +-- the layer of the underworld +for _, feature in ipairs(df.global.world.features.map_features) do + if feature:getType() == df.feature_type.underworld_from_layer then + underworld_z = feature.layer + end +end + +-- right now the only tile_liquids are Water and Magma +local liquid_list = {} +for id, liquid in ipairs(df.tile_liquid) do + liquid_list[id] = string.upper(liquid) +end + +-- copied from agitation-rebalance.lua +-- check only one tile at the center of the map at ground lvl +-- (this ignores different biomes on the edges of the map) +local function get_evilness() + -- check around ground level + + local lvls_above + lvls_above = df.global.world.worldgen.worldgen_parms.levels_above_ground + local ground_z = (df.global.world.map.z_count - 2) - lvls_above + local xmax, ymax = dfhack.maps.getTileSize() + local center_x, center_y = math.floor(xmax/2), math.floor(ymax/2) + local rgnX, rgnY = dfhack.maps.getTileBiomeRgn(center_x, center_y, ground_z) + local biome = dfhack.maps.getRegionBiome(rgnX, rgnY) + + return biome and biome.evilness or 0 +end + +local function classify_tile(options, x, y, z) + -- if your map happens to cross a region boundary and different regions are + -- different depths, the last z-levels of hell MIGHT shrink their x/y size + -- so if your map is 190x190, the last hell z-levels can end up being 90x90 + + if dfhack.maps.getTileType(x, y, z) == nil then + return nil -- Designating the non-tiles of hell to be nil + end + + local tileattrs = df.tiletype.attrs[dfhack.maps.getTileType(x, y, z)] + local tileflags, tile_occupancy = dfhack.maps.getTileFlags(x, y, z) + + local tile_data = {} + + for map_option, position in pairs(options) do + if(map_option == "tiletype") then + tile_data[position] = tileattrs.material + elseif(map_option == "shape") then + tile_data[position] = tileattrs.shape + elseif(map_option == "special") then + tile_data[position] = tileattrs.special + elseif(map_option == "variant") then + tile_data[position] = tileattrs.variant + elseif(map_option == "hidden") then + tile_data[position] = tileflags.hidden + elseif(map_option == "light") then + tile_data[position] = tileflags.light + elseif(map_option == "subterranean") then + tile_data[position] = tileflags.subterranean + elseif(map_option == "outside") then + tile_data[position] = tileflags.outside + elseif(map_option == "liquid") then + if(tileflags.flow_size > 0) then + -- liquid_type is a boolean (true=Magma, false=Water) + -- converting it to a number for easy reference in key table + tile_data[position] = tileflags.liquid_type and 1 or 0 + else + tile_data[position] = nil + end + elseif(map_option == "flow") then + tile_data[position] = tileflags.flow_size + elseif(map_option == "aquifer") then + -- hardcoding these values bc they are not directly in a list + if(tileflags.water_table and tile_occupancy.heavy_aquifer) then + tile_data[position] = 2 + elseif(tileflags.water_table) then + tile_data[position] = 1 + else + tile_data[position] = 0 + end + elseif(map_option == "material") then + if(tileattrs.material >= 8 and tileattrs.material <= 11) then + -- grass material IDs [8-11] will throw an error so we skip them + tile_data[position] = nil + else + local material = tm.GetTileMat(x, y, z) + tile_data[position] = material and material.index or nil + end + end + end + + return tile_data +end + +local function setup_keys(options) + local KEYS = {} + + if(options.tiletype) then + KEYS.TILETYPE = {} + for id, material in ipairs(df.tiletype_material) do + KEYS.TILETYPE[id] = material + end + end + + if(options.shape) then + KEYS.SHAPE = {} + for id, shape in ipairs(df.tiletype_shape) do + KEYS.SHAPE[id] = shape + end + end + + if(options.special) then + KEYS.SPECIAL = {} + for id, special in ipairs(df.tiletype_special) do + KEYS.SPECIAL[id] = special + end + end + + if(options.variant) then + KEYS.VARIANT = {} + for id, variant in ipairs(df.tiletype_variant) do + KEYS.VARIANT[id] = variant + end + end + + if(options.aquifer) then + -- We are hardcoding since this info is not easily listed anywhere + KEYS.AQUIFER = { + [0] = "NONE", + [1] = "LIGHT", + [2] = "HEAVY", + } + end + + if(options.material) then + KEYS.MATERIAL = {} + KEYS.MATERIAL.PLANT = {} + for id, plant in ipairs(df.global.world.raws.plants.all) do + KEYS.MATERIAL.PLANT[id] = plant.id + end + + KEYS.MATERIAL.SOLID = {} -- everything but plants (stones, gems, metals) + KEYS.MATERIAL.METAL = {} + KEYS.MATERIAL.STONE = {} + KEYS.MATERIAL.GEM = {} + + for id, rock in ipairs(df.global.world.raws.inorganics.all) do + local material = rock.material + local name = material.state_adj.Solid + KEYS.MATERIAL.SOLID[id] = name +-- cant sort by key see +-- https://stackoverflow.com/questions/26160327/sorting-a-lua-table-by-key + KEYS.MATERIAL.STONE[id] = material.flags.IS_STONE and name or false + KEYS.MATERIAL.GEM[id] = material.flags.IS_GEM and name or false + KEYS.MATERIAL.METAL[id] = material.flags.IS_METAL and name or false + end + end + + if(options.liquid) then + KEYS.LIQUID = liquid_list + end + + if(options.flow) then + KEYS.FLOW = {} + for i=0, 7 do + KEYS.FLOW[i] = i + end + end + + return KEYS +end + +local function export_all_z_levels(fortress_name, folder, options) + local xmax, ymax, zmax = dfhack.maps.getTileSize() + local filename = string.format("%s/%s.json", folder, fortress_name) + + if dfhack.filesystem.exists(filename) then + qerror('Destination file ' .. filename .. ' already exists!') + return false + end + + local data = {} + + data.ARGUMENT_OPTION_ORDER = options + data.MAP_SIZE = { + x = xmax, + y = ymax, + -- subtract underworld levels if excluded from options + z = underworld and zmax or (zmax - underworld_z), + underworld_z_level = underworld and underworld_z or nil, + evilness = evilness and get_evilness() or nil, + } + data.KEYS = setup_keys(options) + + data.map = {} + + local zmin = 0 + if not underworld then -- skips all z-levels in the underworld + zmin = underworld_z + end + + -- start from bottom z-level (underworld) to top z-level (sky) + for z = zmin, zmax do + local level_data = {} + for y = 0, ymax - 1 do + local row_data = {} + for x = 0, xmax - 1 do + local classification = classify_tile(options, x, y, z) + table.insert(row_data, classification) + end + table.insert(level_data, row_data) + end + table.insert(data.map, level_data) + end + + local f = assert(io.open(filename, 'w')) + f:write(json.encode(data)) + f:close() + print("File created in Dwarf Fortress folder under " .. filename) +end + + +local function export_fortress_map(options) + local fortress_name = dfhack.TranslateName( + df.global.world.world_data.active_site[0].name + ) + local export_path = "map-exports/" .. fortress_name + dfhack.filesystem.mkdir_recursive(export_path) + export_all_z_levels(fortress_name, export_path, options) +end + +if not dfhack.isMapLoaded() then + qerror('This script requires a map to be loaded') +end + +local options, args = { + help = false, + tiletype = false, + shape = false, + special = false, + variant = false, + hidden = false, + light = false, + subterranean = false, + outside = false, + aquifer = false, + material = false, + flow = false, + liquid = false, + underworld = false, + evilness = false, +}, {...} + +local positionals = argparse.processArgsGetopt(args, { + {'', 'help', handler=function() options.help = true end}, + {'t', 'tiletype', handler=function() options.tiletype = true end}, + {'s', 'shape', handler=function() options.shape = true end}, + {'p', 'special', handler=function() options.special = true end}, + {'v', 'variant', handler=function() options.variant = true end}, + {'h', 'hidden', handler=function() options.hidden = true end}, + {'l', 'light', handler=function() options.light = true end}, + {'b', 'subterranean', handler=function() options.subterranean = true end}, + {'o', 'outside', handler=function() options.outside = true end}, + {'a', 'aquifer', handler=function() options.aquifer = true end}, + {'m', 'material', handler=function() options.material = true end}, + {'f', 'flow', handler=function() options.flow = true end}, + {'q', 'liquid', handler=function() options.liquid = true end}, + {'u', 'underworld', handler=function() options.underworld = true end}, + {'e', 'evilness', handler=function() options.evilness = true end}, +}) + +if positionals[1] == "help" or options.help then + print(dfhack.script_help()) + return false +elseif positionals[1] == "include" then + -- no need to change anything +elseif positionals[1] == "exclude" then + for setting in pairs(options) do + options[setting] = not options[setting] + end +else -- include everything + for setting in pairs(options) do + options[setting] = true + end +end + +local ordered_options = { + "tiletype", + "shape", + "special", + "variant", + "hidden", + "light", + "subterranean", + "outside", + "aquifer", + "material", + "flow", + "liquid", +} + +-- these get omitted from ordered_options since this data goes directly into the +-- JSON object for MAP_SIZE and doesn't need to be parsed into every tile +underworld = options.underworld +evilness = options.evilness + +-- reorganize ordered options based on selected options via argparse +-- this is so ARGUMENT_OPTION_ORDER has the correct order with no gaps +for setting in pairs(options) do + if not options[setting] then + for pos, json_setting in ipairs(ordered_options) do + if setting == json_setting then + table.remove(ordered_options, pos) + end + end + end +end + +ordered_options = utils.invert(ordered_options) +export_fortress_map(ordered_options) diff --git a/devel/find-offsets.lua b/devel/find-offsets.lua deleted file mode 100644 index 92045b85f8..0000000000 --- a/devel/find-offsets.lua +++ /dev/null @@ -1,1963 +0,0 @@ --- Find some global addresses ---luacheck:skip-entirely ---[====[ - -devel/find-offsets -================== - -.. warning:: - - THIS SCRIPT IS STRICTLY FOR DFHACK DEVELOPERS. - - Running this script on a new DF version will NOT - MAKE IT RUN CORRECTLY if any data structures - changed, thus possibly leading to CRASHES AND/OR - PERMANENT SAVE CORRUPTION. - -Finding the first few globals requires this script to be -started immediately after loading the game, WITHOUT -first loading a world. The rest expect a loaded save, -not a fresh embark. Finding current_weather requires -a special save previously processed with `devel/prepare-save` -on a DF version with working dfhack. - -The script expects vanilla game configuration, without -any custom tilesets or init file changes. Never unpause -the game unless instructed. When done, quit the game -without saving using 'die'. - -Arguments: - -* global names to force finding them -* ``all`` to force all globals -* ``nofeed`` to block automated fake input searches -* ``nozoom`` to disable neighboring object heuristics - -]====] - ---luacheck-flags: strictsubtype - -local utils = require 'utils' -local ms = require 'memscan' -local gui = require 'gui' - -local is_known = dfhack.internal.getAddress - -local os_type = dfhack.getOSType() - -local force_scan = {} --as:bool[] -for _,v in ipairs({...}) do - force_scan[v] = true -end - -PTR_SIZE = (function() - local tmp = df.new('uintptr_t') - local size = tmp:sizeof() - tmp:delete() - return size -end)() - -collectgarbage() - -function prompt_proceed(indent) - if not indent then indent = 0 end - return utils.prompt_yes_no(string.rep(' ', indent) .. 'Proceed?', true) -end - -print[[ -WARNING: THIS SCRIPT IS STRICTLY FOR DFHACK DEVELOPERS. - -Running this script on a new DF version will NOT -MAKE IT RUN CORRECTLY if any data structures -changed, thus possibly leading to CRASHES AND/OR -PERMANENT SAVE CORRUPTION. - -Finding the first few globals requires this script to be -started immediately after loading the game, WITHOUT -first loading a world. The rest expect a loaded save, -not a fresh embark. Finding current_weather requires -a special save previously processed with devel/prepare-save -on a DF version with working dfhack. - -The script expects vanilla game configuration, without -any custom tilesets or init file changes. Never unpause -the game unless instructed. When done, quit the game -without saving using 'die'. -]] - -if not utils.prompt_yes_no('Proceed?') then - return -end - --- Data segment location - -local data = ms.get_data_segment() -if not data then - qerror('Could not find data segment') -end - -print('\nData section: '..tostring(data)) -if data.size < 5000000 then - qerror('Data segment too short.') -end - -local searcher = ms.DiffSearcher.new(data) - -local function get_screen(class, prompt) - if not is_known('gview') then - print('Please navigate to '..prompt) - if not prompt_proceed() then - return nil, false - end - return nil, true - end - - while true do - local cs = dfhack.gui.getCurViewscreen(true) - if not df.is_instance(class, cs) then - print('Please navigate to '..prompt) - if not prompt_proceed() then - return nil, false - end - else - return cs, true - end - end -end - -local function screen_title() - return get_screen(df.viewscreen_titlest, 'the title screen') -end -local function screen_dwarfmode() - return get_screen(df.viewscreen_dwarfmodest, 'the main dwarf mode screen') -end - -local function validate_offset(name,validator,addr,tname,...) - local obj = data:object_by_field(addr,tname,...) - if obj and not validator(obj) then - obj = nil - end - ms.found_offset(name,obj) -end - -local function zoomed_searcher(startn, end_or_sz, bidirectional) - if force_scan.nozoom then - return nil - end - local sv = is_known(startn) - if not sv then - return nil - end - local ev - if type(end_or_sz) == 'number' then - ev = sv + end_or_sz - if end_or_sz < 0 then - sv, ev = ev, sv - end - else - ev = is_known(end_or_sz) - if not ev then - return nil - end - end - if bidirectional then - sv = sv - (ev - sv) - end - sv = sv - (sv % 4) - ev = ev + 3 - ev = ev - (ev % 4) - if data:contains_range(sv, ev-sv) then - return ms.DiffSearcher.new(ms.MemoryArea.new(sv,ev)) - end -end - -local finder_searches = {} --as:string[] -local function exec_finder(finder, names, validators) - local validators = validators --as:{_type:function,_node:bool}[] - if type(names) ~= 'table' then - names = { names } --luacheck: retype - end - if type(validators) ~= 'table' then --luacheck: skip - validators = { validators } - end - local search = force_scan['all'] - for k,v in ipairs(names) do - if force_scan[v] or not is_known(v) then - table.insert(finder_searches, v) - search = true - elseif validators[k] then - if not validators[k](df.global[v]) then --luacheck: skip - dfhack.printerr('Validation failed for '..v..', will try to find again') - table.insert(finder_searches, v) - search = true - end - end - end - if search then - local ok, err = dfhack.safecall(finder) - if not ok then - if tostring(err):find('abort') or not utils.prompt_yes_no('Proceed with the rest of the script?') then - searcher:reset() - qerror('Quit') - end - end - else - print('Already known: '..table.concat(names,', ')) - end -end - -local ordinal_names = { - [0] = '1st entry', - [1] = '2nd entry', - [2] = '3rd entry' -} -setmetatable(ordinal_names, { - __index = function(self,idx) return (idx+1)..'th entry' end -}) - -local function list_index_choices(length_func) - return function(id) - if id > 0 then - local ok, len = pcall(length_func) - if not ok then - len = 5 - elseif len > 10 then - len = 10 - end - return id % len - else - return 0 - end - end -end - -local function can_feed() - return not force_scan['nofeed'] and is_known 'gview' -end - -local function dwarfmode_feed_input(...) - local screen = screen_dwarfmode() - if not df.isvalid(screen) then - qerror('could not retrieve dwarfmode screen') - end - try_save_cursor() - for _,v in ipairs({...}) do - gui.simulateInput(screen, v) - end -end - -local function dwarfmode_step_frames(count) - local screen = screen_dwarfmode() - if not df.isvalid(screen) then - qerror('could not retrieve dwarfmode screen') - end - - for i = 1,(count or 1) do - gui.simulateInput(screen, 'D_ONESTEP') - if screen.keyRepeat ~= 1 then - qerror('Could not step one frame: . did not work') - end - screen:logic() - end -end - -local function dwarfmode_to_top() - if not can_feed() then - return false - end - - local screen = screen_dwarfmode() - if not df.isvalid(screen) then - return false - end - - for i=0,10 do - if is_known 'ui' and df.global.ui.main.mode == df.ui_sidebar_mode.Default then - break - end - gui.simulateInput(screen, 'LEAVESCREEN') - end - - -- force pause just in case - screen.keyRepeat = 1 - return true -end - -local prev_cursor = df.global.T_cursor:new() -prev_cursor.x = -30000 -function try_save_cursor() - if not dfhack.internal.getAddress('cursor') then return end - for _, v in pairs(df.global.cursor) do - if v < 0 then - return - end - end - prev_cursor:assign(df.global.cursor) -end - -function try_restore_cursor() - if not dfhack.internal.getAddress('cursor') then return end - if prev_cursor.x >= 0 then - df.global.cursor:assign(prev_cursor) - dfhack.gui.refreshSidebar() - end -end - -local function feed_menu_choice(catnames,catkeys,enum,enter_seq,exit_seq,prompt) - return function (idx) - if idx == 0 and prompt and not prompt_proceed(2) then - return false - end - if idx > 0 then - dwarfmode_feed_input(table.unpack(exit_seq or {})) - end - idx = idx % #catnames + 1 - dwarfmode_feed_input(table.unpack(enter_seq or {})) - dwarfmode_feed_input(catkeys[idx]) - if enum then - return true, enum[catnames[idx]] - else - return true, catnames[idx] - end - end -end - -local function feed_list_choice(count,upkey,downkey) - return function(idx) - if idx > 0 then - local ok, len - if type(count) == 'number' then - ok, len = true, count - else - ok, len = pcall(count) - end - if not ok then - len = 5 - elseif len > 10 then - len = 10 - end - - local hcnt = len-1 - local rix = 1 + (idx-1) % (hcnt*2) - - if rix >= hcnt then - dwarfmode_feed_input(upkey or 'SECONDSCROLL_UP') - return true, hcnt*2 - rix - else - dwarfmode_feed_input(donwkey or 'SECONDSCROLL_DOWN') - return true, rix - end - else - print(' Please select the first list item.') - if not prompt_proceed(2) then - return false - end - return true, 0 - end - end -end - -local function feed_menu_bool(enter_seq, exit_seq) - return function(idx) - if idx == 0 then - if not prompt_proceed(2) then - return false - end - return true, 0 - end - if idx == 5 then - print(' Please resize the game window.') - if not prompt_proceed(2) then - return false - end - end - if idx%2 == 1 then - dwarfmode_feed_input(table.unpack(enter_seq)) - return true, 1 - else - dwarfmode_feed_input(table.unpack(exit_seq)) - return true, 0 - end - end -end - --- --- Cursor group --- - -local function find_cursor() - local _, ok = screen_title() - if not ok then - return false - end - - -- Unpadded version - local idx, addr = data.int32_t:find_one{ - -30000, -30000, -30000, - -30000, -30000, -30000, -30000, -30000, -30000, - df.game_mode.NONE, df.game_type.NONE - } - if idx then - ms.found_offset('cursor', addr) - ms.found_offset('selection_rect', addr + 12) - ms.found_offset('gamemode', addr + 12 + 24) - ms.found_offset('gametype', addr + 12 + 24 + 4) - return true - end - - -- Padded version - idx, addr = data.int32_t:find_one{ - -30000, -30000, -30000, 0, - -30000, -30000, -30000, -30000, -30000, -30000, 0, 0, - df.game_mode.NONE, 0, 0, 0, df.game_type.NONE - } - if idx then - ms.found_offset('cursor', addr) - ms.found_offset('selection_rect', addr + 0x10) - ms.found_offset('gamemode', addr + 0x30) - ms.found_offset('gametype', addr + 0x40) - return true - end - - -- New in 0.43.05 x64 - idx, addr = data.int32_t:find_one{ - -30000, -30000, -30000, 0, 0, - -30000, -30000, -30000, -30000, -30000, -30000, - df.game_mode.NONE, df.game_type.NONE - } - if idx then - ms.found_offset('cursor', addr) - ms.found_offset('selection_rect', addr + 0x14) - ms.found_offset('gamemode', addr + 0x2C) - ms.found_offset('gametype', addr + 0x30) - return true - end - - -- New in 0.43.05 x64 Linux - if os_type == 'linux' then - idx, addr = data.int32_t:find_one{ - df.game_type.NONE, 0, 0, 0, - df.game_mode.NONE, 0, 0, 0, - -30000, -30000, -30000, -30000, - -30000, -30000, 0, 0, - -30000, -30000, -30000 - } - if idx then - ms.found_offset('cursor', addr + 0x40) - ms.found_offset('selection_rect', addr + 0x20) - ms.found_offset('gamemode', addr + 0x10) - ms.found_offset('gametype', addr) - return true - end - end - - dfhack.printerr('Could not find cursor.') - return false -end - --- --- d_init --- - -local function is_valid_d_init(di) - if di.sky_tile ~= 178 then - print('Sky tile expected 178, found: '..di.sky_tile) - if not utils.prompt_yes_no('Ignore?') then - return false - end - end - - return true -end - -local function find_d_init() - local idx, addr = data.int16_t:find_one{ - 1,0, 2,0, 5,0, 25,0, -- path_cost - 4,4, -- embark_rect - 20,1000,1000,1000,1000 -- store_dist - } - if idx then - validate_offset('d_init', is_valid_d_init, addr, df.d_init, 'path_cost') - return - end - - dfhack.printerr('Could not find d_init') -end - --- --- gview --- - -local function find_gview() - local vs_vtable = dfhack.internal.getVTable('viewscreenst') - if not vs_vtable then - dfhack.printerr('Cannot search for gview - no viewscreenst vtable.') - return - end - - local idx, addr = data.uintptr_t:find_one{0, vs_vtable} - if idx then - ms.found_offset('gview', addr) - return - end - - idx, addr = data.uintptr_t:find_one{100, vs_vtable} - if idx then - ms.found_offset('gview', addr) - return - end - - dfhack.printerr('Could not find gview') -end - --- --- enabler --- - -local function lookup_colors() - local f = io.open('data/init/colors.txt', 'r') or error('failed to open file') - local text = f:read('*all') - f:close() - local colors = {} - for _, color in pairs({'BLACK', 'BLUE', 'GREEN', 'CYAN', 'RED', 'MAGENTA', - 'BROWN', 'LGRAY', 'DGRAY', 'LBLUE', 'LGREEN', 'LCYAN', 'LRED', - 'LMAGENTA', 'YELLOW', 'WHITE'}) do - for _, part in pairs({'R', 'G', 'B'}) do - local opt = color .. '_' .. part - table.insert(colors, tonumber(text:match(opt .. ':(%d+)') or error('missing from colors.txt: ' .. opt))) - end - end - return colors -end - -local function is_valid_enabler(e) - if not ms.is_valid_vector(e.textures.raws, PTR_SIZE) - or not ms.is_valid_vector(e.text_system, PTR_SIZE) - then - dfhack.printerr('Vector layout check failed.') - return false - end - - return true -end - -local function find_enabler() - -- Data from data/init/colors.txt - local default_colors = { - 0, 0, 0, 0, 0, 128, 0, 128, 0, - 0, 128, 128, 128, 0, 0, 128, 0, 128, - 128, 128, 0, 192, 192, 192, 128, 128, 128, - 0, 0, 255, 0, 255, 0, 0, 255, 255, - 255, 0, 0, 255, 0, 255, 255, 255, 0, - 255, 255, 255 - } - local colors - local ok, ret = pcall(lookup_colors) - if not ok then - dfhack.printerr('Failed to look up colors, using defaults: \n' .. ret) - colors = default_colors - else - colors = ret - end - - for i = 1,#colors do colors[i] = colors[i]/255 end - - local idx, addr = data.float:find_one(colors) - if not idx then - idx, addr = data.float:find_one(default_colors) - end - if idx then - validate_offset('enabler', is_valid_enabler, addr, df.enabler, 'ccolor') - return - end - - dfhack.printerr('Could not find enabler') -end - --- --- gps --- - -local function is_valid_gps(g) - if g.clipx[0] < 0 or g.clipx[0] > g.clipx[1] or g.clipx[1] >= g.dimx then - dfhack.printerr('Invalid clipx: ', g.clipx[0], g.clipx[1], g.dimx) - end - if g.clipy[0] < 0 or g.clipy[0] > g.clipy[1] or g.clipy[1] >= g.dimy then - dfhack.printerr('Invalid clipy: ', g.clipy[0], g.clipy[1], g.dimy) - end - - return true -end - -local function find_gps() - print('\nPlease ensure the mouse cursor is not over the game window.') - if not prompt_proceed() then - return - end - - local zone - if os_type == 'windows' or os_type == 'linux' then - zone = zoomed_searcher('cursor', 0x1000) - elseif os_type == 'darwin' then - zone = zoomed_searcher('enabler', 0x1000) - end - zone = zone or searcher - - local w,h = ms.get_screen_size() - - local idx, addr = zone.area.int32_t:find_one{w, h, -1, -1} - if not idx then - idx, addr = data.int32_t:find_one{w, h, -1, -1} - end - if idx then - validate_offset('gps', is_valid_gps, addr, df.graphic, 'dimx') - return - end - - dfhack.printerr('Could not find gps') -end - --- --- World --- - -local function is_valid_world(world) - if not ms.is_valid_vector(world.units.all, PTR_SIZE) - or not ms.is_valid_vector(world.units.active, PTR_SIZE) - or not ms.is_valid_vector(world.units.bad, PTR_SIZE) - or not ms.is_valid_vector(world.history.figures, PTR_SIZE) - or not ms.is_valid_vector(world.features.map_features, PTR_SIZE) - then - dfhack.printerr('Vector layout check failed.') - return false - end - - if #world.units.all == 0 or #world.units.all ~= #world.units.bad then - print('Different or zero size of units.all and units.bad:'..#world.units.all..' vs '..#world.units.bad) - if not utils.prompt_yes_no('Ignore?') then - return false - end - end - - return true -end - -local function find_world() - local catnames = { - 'Corpses', 'Refuse', 'Stone', 'Wood', 'Gems', 'Bars', 'Cloth', 'Leather', 'Ammo', 'Coins' - } - local catkeys = { - 'STOCKPILE_GRAVEYARD', 'STOCKPILE_REFUSE', 'STOCKPILE_STONE', 'STOCKPILE_WOOD', - 'STOCKPILE_GEM', 'STOCKPILE_BARBLOCK', 'STOCKPILE_CLOTH', 'STOCKPILE_LEATHER', - 'STOCKPILE_AMMO', 'STOCKPILE_COINS' - } - local addr - - if dwarfmode_to_top() then - dwarfmode_feed_input('D_STOCKPILES') - - addr = searcher:find_interactive( - 'Auto-searching for world.', - 'int32_t', - feed_menu_choice(catnames, catkeys, df.stockpile_category), - 20 - ) - end - - if not addr then - addr = searcher:find_menu_cursor([[ -Searching for world. Please open the stockpile creation -menu, and select different types as instructed below:]], - 'int32_t', catnames, df.stockpile_category - ) - end - - validate_offset('world', is_valid_world, addr, df.world, 'selected_stockpile_type') -end - --- --- UI --- - -local function is_valid_ui(ui) - if not ms.is_valid_vector(ui.economic_stone, 1) - or not ms.is_valid_vector(ui.dipscripts, PTR_SIZE) - then - dfhack.printerr('Vector layout check failed.') - return false - end - - if ui.follow_item ~= -1 or ui.follow_unit ~= -1 then - print('Invalid follow state: '..ui.follow_item..', '..ui.follow_unit) - return false - end - - return true -end - -local function find_ui() - local catnames = { - 'DesignateMine', 'DesignateChannel', 'DesignateRemoveRamps', - 'DesignateUpStair', 'DesignateDownStair', 'DesignateUpDownStair', - 'DesignateUpRamp', 'DesignateChopTrees' - } - local catkeys = { - 'DESIGNATE_DIG', 'DESIGNATE_CHANNEL', 'DESIGNATE_DIG_REMOVE_STAIRS_RAMPS', - 'DESIGNATE_STAIR_UP', 'DESIGNATE_STAIR_DOWN', 'DESIGNATE_STAIR_UPDOWN', - 'DESIGNATE_RAMP', 'DESIGNATE_CHOP' - } - local addr - - if dwarfmode_to_top() then - dwarfmode_feed_input('D_DESIGNATE') - - addr = searcher:find_interactive( - 'Auto-searching for ui.', - 'int16_t', - feed_menu_choice(catnames, catkeys, df.ui_sidebar_mode), - 20 - ) - end - - if not addr then - addr = searcher:find_menu_cursor([[ -Searching for ui. Please open the designation -menu, and switch modes as instructed below:]], - 'int16_t', catnames, df.ui_sidebar_mode - ) - end - - validate_offset('ui', is_valid_ui, addr, df.ui, 'main', 'mode') -end - --- --- ui_sidebar_menus --- - -local function is_valid_ui_sidebar_menus(usm) - if not ms.is_valid_vector(usm.workshop_job.choices_all, 4) - or not ms.is_valid_vector(usm.workshop_job.choices_visible, 4) - then - dfhack.printerr('Vector layout check failed.') - return false - end - - if #usm.workshop_job.choices_all == 0 - or #usm.workshop_job.choices_all ~= #usm.workshop_job.choices_visible then - print('Different or zero size of visible and all choices:'.. - #usm.workshop_job.choices_all..' vs '..#usm.workshop_job.choices_visible) - if not utils.prompt_yes_no('Ignore?') then - return false - end - end - - return true -end - -local function find_ui_sidebar_menus() - local addr - - if dwarfmode_to_top() then - dwarfmode_feed_input('D_BUILDJOB') - - addr = searcher:find_interactive([[ -Auto-searching for ui_sidebar_menus. Please select a Mason's, -Craftsdwarf's, or Carpenter's workshop:]], - 'int32_t', - function(idx) - if idx == 0 then - prompt_proceed(2) - -- ensure that the job list isn't full - dwarfmode_feed_input('BUILDJOB_CANCEL', 'BUILDJOB_ADD') - return true, 0 - end - return feed_list_choice(7)(idx) - end, - 20 - ) - end - - if not addr then - addr = searcher:find_menu_cursor([[ -Searching for ui_sidebar_menus. Please switch to 'q' mode, -select a Mason, Craftsdwarfs, or Carpenters workshop, open -the Add Job menu, and move the cursor within:]], - 'int32_t', - { 0, 1, 2, 3, 4, 5, 6 }, - ordinal_names - ) - end - - validate_offset('ui_sidebar_menus', is_valid_ui_sidebar_menus, - addr, df.ui_sidebar_menus, 'workshop_job', 'cursor') -end - --- --- ui_build_selector --- - -local function is_valid_ui_build_selector(ubs) - if not ms.is_valid_vector(ubs.requirements, 4) - or not ms.is_valid_vector(ubs.choices, 4) - then - dfhack.printerr('Vector layout check failed.') - return false - end - - if ubs.building_type ~= df.building_type.Trap - or ubs.building_subtype ~= df.trap_type.PressurePlate then - print('Invalid building type and subtype:'..ubs.building_type..','..ubs.building_subtype) - return false - end - - return true -end - -local function find_ui_build_selector() - local addr - - if dwarfmode_to_top() then - addr = searcher:find_interactive([[ -Auto-searching for ui_build_selector. This requires mechanisms.]], - 'int32_t', - function(idx) - if idx == 0 then - dwarfmode_to_top() - dwarfmode_feed_input( - 'D_BUILDING', - 'HOTKEY_BUILDING_TRAP', - 'HOTKEY_BUILDING_TRAP_TRIGGER', - 'BUILDING_TRIGGER_ENABLE_CREATURE' - ) - else - dwarfmode_feed_input('BUILDING_TRIGGER_MIN_SIZE_UP') - end - return true, 5000 + 1000*idx - end, - 20 - ) - end - - if not addr then - addr = searcher:find_menu_cursor([[ -Searching for ui_build_selector. Please start constructing -a pressure plate, and enable creatures. Then change the min -weight as requested, knowing that the ui shows 5000 as 5K:]], - 'int32_t', - { 5000, 6000, 7000, 8000, 9000, 10000, 11000 } - ) - end - - validate_offset('ui_build_selector', is_valid_ui_build_selector, - addr, df.ui_build_selector, 'plate_info', 'unit_min') -end - --- --- init --- - -local function is_valid_init(i) - -- derived from curses_*.png image sizes presumably - if i.font.small_font_dispx ~= 8 or i.font.small_font_dispy ~= 12 or - i.font.large_font_dispx ~= 10 or i.font.large_font_dispy ~= 12 then - print('Unexpected font sizes: ', - i.font.small_font_dispx, i.font.small_font_dispy, - i.font.large_font_dispx, i.font.large_font_dispy) - if not utils.prompt_yes_no('Ignore?') then - return false - end - end - - return true -end - -local function find_init() - local zone - --[[if os_type == 'windows' then - zone = zoomed_searcher('ui_build_selector', 0x3000) - elseif os_type == 'linux' or os_type == 'darwin' then - zone = zoomed_searcher('d_init', -0x2000) - end]] - zone = zone or searcher - - local idx, addr = zone.area.long:find_one{250, 150, 15, 0} - if idx then - validate_offset('init', is_valid_init, addr, df.init, 'input', 'hold_time') - return - end - - local w,h = ms.get_screen_size() - - local idx, addr = zone.area.int32_t:find_one{w, h} - if idx then - validate_offset('init', is_valid_init, addr, df.init, 'display', 'grid_x') - return - end - - dfhack.printerr('Could not find init') -end - --- --- current_weather --- - -local function find_current_weather() - local zone - if os_type == 'windows' then - zone = zoomed_searcher('crime_next_id', 512) - elseif os_type == 'darwin' then - zone = zoomed_searcher('cursor', 128, true) - elseif os_type == 'linux' then - zone = zoomed_searcher('ui_selected_unit', 512) - end - zone = zone or searcher - - local wbytes = { - 2, 1, 0, 2, 0, - 1, 2, 1, 0, 0, - 2, 0, 2, 1, 2, - 1, 2, 0, 1, 1, - 2, 0, 1, 0, 2 - } - - local idx, addr = zone.area.int8_t:find_one(wbytes) - if not idx then - idx, addr = data.int8_t:find_one(wbytes) - end - if idx then - ms.found_offset('current_weather', addr) - return - end - - dfhack.printerr('Could not find current_weather - must be a wrong save.') -end - --- --- ui_menu_width --- - -local function find_ui_menu_width() - local addr - - if dwarfmode_to_top() then - addr = searcher:find_interactive('Auto-searching for ui_menu_width', 'int8_t', function(idx) - local val = (idx % 3) + 1 - if idx == 0 then - print('Switch to the default [map][menu][map] layout (with Tab)') - if not prompt_proceed(2) then return false end - else - dwarfmode_feed_input('CHANGETAB', val ~= 3 and 'CHANGETAB') - end - return true, val - end) - end - - if not addr then - addr = searcher:find_menu_cursor([[ -Searching for ui_menu_width. Please exit to the main -dwarfmode menu, then use Tab to do as instructed below:]], - 'int8_t', - { 2, 3, 1 }, - { [2] = 'switch to the most usual [mapmap][menu] layout', - [3] = 'hide the menu completely', - [1] = 'switch to the default [map][menu][map] layout' } - ) - end - - ms.found_offset('ui_menu_width', addr) - - -- reset to make sure view is small enough for window_x/y scan on small maps - df.global.ui_menu_width[0] = 2 - df.global.ui_menu_width[1] = 3 -end - --- --- ui_selected_unit --- - -local function find_ui_selected_unit() - if not is_known 'world' then - dfhack.printerr('Cannot search for ui_selected_unit: no world') - return - end - - for i,unit in ipairs(df.global.world.units.active) do - -- This function does a lot of things and accesses histfigs, souls and so on: - --dfhack.units.setNickname(unit, i) - - -- Instead use just a simple bit of code that only requires the start of the - -- unit to be valid. It may not work properly with vampires or reset later - -- if unpaused, but is sufficient for this script and won't crash: - unit.name.nickname = tostring(i) - unit.name.has_name = true - end - - local addr = searcher:find_menu_cursor([[ -Searching for ui_selected_unit. Please activate the 'v' -mode, point it at units, and enter their numeric nickname -into the prompts below:]], - 'int32_t', - function() - return utils.prompt_input(' Enter index: ', utils.check_number) - end, - 'noprompt' - ) - ms.found_offset('ui_selected_unit', addr) -end - --- --- ui_unit_view_mode --- - -local function find_ui_unit_view_mode() - local catnames = { 'General', 'Inventory', 'Preferences', 'Wounds' } - local catkeys = { 'UNITVIEW_GEN', 'UNITVIEW_INV', 'UNITVIEW_PRF', 'UNITVIEW_WND' } - local addr - - if dwarfmode_to_top() and is_known('ui_selected_unit') then - dwarfmode_feed_input('D_VIEWUNIT') - - if df.global.ui_selected_unit < 0 then - df.global.ui_selected_unit = 0 - end - - addr = searcher:find_interactive( - 'Auto-searching for ui_unit_view_mode.', - 'int32_t', - feed_menu_choice(catnames, catkeys, df.ui_unit_view_mode.T_value), - 10 - ) - end - - if not addr then - addr = searcher:find_menu_cursor([[ -Searching for ui_unit_view_mode. Having selected a unit -with 'v', switch the pages as requested:]], - 'int32_t', catnames, df.ui_unit_view_mode.T_value - ) - end - - ms.found_offset('ui_unit_view_mode', addr) -end - --- --- ui_look_cursor --- - -local function look_item_list_count() - return #df.global.ui_look_list.items -end - -local function find_ui_look_cursor() - local addr - - if dwarfmode_to_top() then - dwarfmode_feed_input('D_LOOK') - - addr = searcher:find_interactive([[ -Auto-searching for ui_look_cursor. Please select a tile -with at least 5 items or units on the ground, and move -the cursor as instructed:]], - 'int32_t', - feed_list_choice(look_item_list_count), - 20 - ) - end - - if not addr then - addr = searcher:find_menu_cursor([[ -Searching for ui_look_cursor. Please activate the 'k' -mode, find a tile with many items or units on the ground, -and select list entries as instructed:]], - 'int32_t', - list_index_choices(look_item_list_count), - ordinal_names - ) - end - - ms.found_offset('ui_look_cursor', addr) -end - --- --- ui_building_item_cursor --- - -local function building_item_list_count() - return #df.global.world.selected_building.contained_items --hint:df.building_actual -end - -local function find_ui_building_item_cursor() - local addr - - if dwarfmode_to_top() then - dwarfmode_feed_input('D_BUILDITEM') - - addr = searcher:find_interactive([[ -Auto-searching for ui_building_item_cursor. Please highlight a -workshop, trade depot or other building with at least 5 contained -items, and select as instructed:]], - 'int32_t', - feed_list_choice(building_item_list_count), - 20 - ) - end - - if not addr then - addr = searcher:find_menu_cursor([[ -Searching for ui_building_item_cursor. Please activate the 't' -mode, find a cluttered workshop, trade depot, or other building -with many contained items, and select as instructed:]], - 'int32_t', - list_index_choices(building_item_list_count), - ordinal_names - ) - end - - ms.found_offset('ui_building_item_cursor', addr) -end - --- --- ui_workshop_in_add --- - -local function find_ui_workshop_in_add() - local addr - - if dwarfmode_to_top() then - dwarfmode_feed_input('D_BUILDJOB') - - addr = searcher:find_interactive([[ -Auto-searching for ui_workshop_in_add. Please select a -workshop, e.g. Carpenters or Masons.]], - 'int8_t', - feed_menu_bool( - { 'BUILDJOB_CANCEL', 'BUILDJOB_ADD' }, - { 'SELECT', 'SELECT', 'SELECT', 'SELECT', 'SELECT' } - ), - 20 - ) - end - - if not addr then - addr = searcher:find_menu_cursor([[ -Searching for ui_workshop_in_add. Please activate the 'q' -mode, find a workshop without jobs (or delete jobs), -and do as instructed below. - -NOTE: If not done after first 3-4 steps, resize the game window.]], - 'int8_t', - { 1, 0 }, - { [1] = 'enter the add job menu', - [0] = 'add job, thus exiting the menu' } - ) - end - - ms.found_offset('ui_workshop_in_add', addr) -end - --- --- ui_workshop_job_cursor --- - -local function workshop_job_list_count() - return #df.global.world.selected_building.jobs -end - -local function find_ui_workshop_job_cursor() - local addr - - if dwarfmode_to_top() then - dwarfmode_feed_input('D_BUILDJOB') - addr = searcher:find_interactive([[ -Auto-searching for ui_workshop_job_cursor. Please highlight a -Mason's or Carpenter's workshop, or any building with a job -selection interface navigable with just "Enter":]], - 'int32_t', - function(idx) - if idx == 0 then prompt_proceed(2) end - for i = 1, 10 - workshop_job_list_count() do - dwarfmode_feed_input('BUILDJOB_ADD', 'SELECT', 'SELECT', 'SELECT', 'SELECT', 'SELECT') - end - dwarfmode_feed_input('SECONDSCROLL_DOWN') - -- adding jobs resets the cursor position, so it is difficult to determine here - return true - end, - 20 - ) - end - - if not addr then - addr = searcher:find_menu_cursor([[ -Searching for ui_workshop_job_cursor. Please activate the 'q' -mode, find a workshop with many jobs, and select as instructed:]], - 'int32_t', - list_index_choices(workshop_job_list_count), - ordinal_names - ) - end - - ms.found_offset('ui_workshop_job_cursor', addr) -end - --- --- ui_building_in_assign --- - -local function find_ui_building_in_assign() - local addr - - if dwarfmode_to_top() then - dwarfmode_feed_input('D_BUILDJOB') - try_restore_cursor() - - addr = searcher:find_interactive([[ -Auto-searching for ui_building_in_assign. Please select a room, -i.e. a bedroom, tomb, office, dining room or statue garden.]], - 'int8_t', - feed_menu_bool( - { { 'BUILDJOB_STATUE_ASSIGN', 'BUILDJOB_COFFIN_ASSIGN', - 'BUILDJOB_CHAIR_ASSIGN', 'BUILDJOB_TABLE_ASSIGN', - 'BUILDJOB_BED_ASSIGN' } }, - { 'LEAVESCREEN' } - ), - 20 - ) - end - - if not addr then - addr = searcher:find_menu_cursor([[ -Searching for ui_building_in_assign. Please activate -the 'q' mode, select a room building (e.g. a bedroom) -and do as instructed below. - -NOTE: If not done after first 3-4 steps, resize the game window.]], - 'int8_t', - { 1, 0 }, - { [1] = 'enter the Assign owner menu', - [0] = 'press Esc to exit assign' } - ) - end - - ms.found_offset('ui_building_in_assign', addr) -end - --- --- ui_building_in_resize --- - -local function find_ui_building_in_resize() - local addr - - if dwarfmode_to_top() then - dwarfmode_feed_input('D_BUILDJOB') - try_restore_cursor() - - addr = searcher:find_interactive([[ -Auto-searching for ui_building_in_resize. Please select a room, -i.e. a bedroom, tomb, office, dining room or statue garden.]], - 'int8_t', - feed_menu_bool( - { { 'BUILDJOB_STATUE_SIZE', 'BUILDJOB_COFFIN_SIZE', - 'BUILDJOB_CHAIR_SIZE', 'BUILDJOB_TABLE_SIZE', - 'BUILDJOB_BED_SIZE' } }, - { 'LEAVESCREEN' } - ), - 20 - ) - end - - if not addr then - addr = searcher:find_menu_cursor([[ -Searching for ui_building_in_resize. Please activate -the 'q' mode, select a room building (e.g. a bedroom) -and do as instructed below. - -NOTE: If not done after first 3-4 steps, resize the game window.]], - 'int8_t', - { 1, 0 }, - { [1] = 'enter the Resize room mode', - [0] = 'press Esc to exit resize' } - ) - end - - ms.found_offset('ui_building_in_resize', addr) -end - --- --- ui_lever_target_type --- -local function find_ui_lever_target_type() - local catnames = { - 'Bridge', 'Door', 'Floodgate', - 'Cage', 'Chain', 'TrackStop', - 'GearAssembly', - } - local catkeys = { - 'HOTKEY_TRAP_BRIDGE', 'HOTKEY_TRAP_DOOR', 'HOTKEY_TRAP_FLOODGATE', - 'HOTKEY_TRAP_CAGE', 'HOTKEY_TRAP_CHAIN', 'HOTKEY_TRAP_TRACK_STOP', - 'HOTKEY_TRAP_GEAR_ASSEMBLY', - } - local addr - - if dwarfmode_to_top() then - dwarfmode_feed_input('D_BUILDJOB') - - addr = searcher:find_interactive( - 'Auto-searching for ui_lever_target_type. Please select a lever:', - 'int8_t', - feed_menu_choice(catnames, catkeys, df.lever_target_type, - {'BUILDJOB_ADD'}, - {'LEAVESCREEN', 'LEAVESCREEN'}, - true -- prompt - ), - 20 - ) - end - - if not addr then - addr = searcher:find_menu_cursor([[ -Searching for ui_lever_target_type. Please select a lever with -'q' and enter the "add task" menu with 'a':]], - 'int8_t', catnames, df.lever_target_type - ) - end - - ms.found_offset('ui_lever_target_type', addr) -end - --- --- window_x --- - -local function feed_window_xyz(dec,inc,step) - return function(idx) - if idx == 0 then - for i = 1,30 do dwarfmode_feed_input(dec) end - else - dwarfmode_feed_input(inc) - end - return true, nil, step - end -end - -local function find_window_x() - local addr - - if dwarfmode_to_top() then - addr = searcher:find_interactive( - 'Auto-searching for window_x.', - 'int32_t', - feed_window_xyz('CURSOR_LEFT_FAST', 'CURSOR_RIGHT', 10), - 20 - ) - - dwarfmode_feed_input('D_HOTKEY1') - end - - if not addr then - addr = searcher:find_counter([[ -Searching for window_x. Please exit to main dwarfmode menu, -scroll to the LEFT edge, then do as instructed:]], - 'int32_t', 10, - 'Please press Right to scroll right one step.' - ) - end - - ms.found_offset('window_x', addr) -end - --- --- window_y --- - -local function find_window_y() - local addr - - if dwarfmode_to_top() then - addr = searcher:find_interactive( - 'Auto-searching for window_y.', - 'int32_t', - feed_window_xyz('CURSOR_UP_FAST', 'CURSOR_DOWN', 10), - 20 - ) - - dwarfmode_feed_input('D_HOTKEY1') - end - - if not addr then - addr = searcher:find_counter([[ -Searching for window_y. Please exit to main dwarfmode menu, -scroll to the TOP edge, then do as instructed:]], - 'int32_t', 10, - 'Please press Down to scroll down one step.' - ) - end - - ms.found_offset('window_y', addr) -end - --- --- window_z --- - -local function find_window_z() - local addr - - if dwarfmode_to_top() then - addr = searcher:find_interactive( - 'Auto-searching for window_z.', - 'int32_t', - feed_window_xyz('CURSOR_UP_Z', 'CURSOR_DOWN_Z', -1), - 30 - ) - - dwarfmode_feed_input('D_HOTKEY1') - end - - if not addr then - addr = searcher:find_counter([[ -Searching for window_z. Please exit to main dwarfmode menu, -scroll to a Z level near surface, then do as instructed below. - -NOTE: If not done after first 3-4 steps, resize the game window.]], - 'int32_t', -1, - "Please press '>' to scroll one Z level down." - ) - end - - ms.found_offset('window_z', addr) -end - --- --- cur_year --- - -local function find_cur_year() - local zone - if os_type == 'windows' then - zone = zoomed_searcher('formation_next_id', 32) - elseif os_type == 'darwin' then - zone = zoomed_searcher('cursor', -32) - elseif os_type == 'linux' then - zone = zoomed_searcher('current_weather', -512) - end - if not zone then - dfhack.printerr('Cannot search for cur_year - prerequisites missing.') - return - end - - local yvalue = utils.prompt_input('Please enter current in-game year: ', utils.check_number) - local idx, addr = zone.area.int32_t:find_one{yvalue} - if idx then - ms.found_offset('cur_year', addr) - return - end - - dfhack.printerr('Could not find cur_year') -end - --- --- cur_year_tick --- - -function stop_autosave() - if is_known 'd_init' then - local f = df.global.d_init.flags4 - if f.AUTOSAVE_SEASONAL or f.AUTOSAVE_YEARLY then - f.AUTOSAVE_SEASONAL = false - f.AUTOSAVE_YEARLY = false - print('Disabled seasonal and yearly autosave.') - end - else - dfhack.printerr('Could not disable autosave!') - end -end - ---luacheck: in=number,df.viewscreen_dwarfmodest -function step_n_frames(cnt, feed) - local world = df.global.world - local ctick = world.frame_counter - - if feed then - print(" Auto-stepping "..cnt.." frames.") - dwarfmode_step_frames(cnt) - return world.frame_counter-ctick - end - - local more = '' - while world.frame_counter-ctick < cnt do - print(" Please step the game "..(cnt-world.frame_counter+ctick)..more.." frames.") - more = ' more' - if not prompt_proceed(2) then - return nil - end - end - return world.frame_counter-ctick -end - -local function find_cur_year_tick() - local zone - if os_type == 'windows' then - zone = zoomed_searcher('ui_unit_view_mode', 0x200) - else - zone = zoomed_searcher('cur_year', 128, true) - end - if not zone then - dfhack.printerr('Cannot search for cur_year_tick - prerequisites missing.') - return - end - - stop_autosave() - - local feed = dwarfmode_to_top() - local addr = zone:find_interactive( - 'Searching for cur_year_tick.', - 'int32_t', - function(idx) - if idx > 0 then - if not step_n_frames(1, feed) then - return false - end - end - return true, nil, 1 - end, - 20 - ) - - ms.found_offset('cur_year_tick', addr) -end - -local function find_cur_year_tick_advmode() - stop_autosave() - - local feed = dwarfmode_to_top() - local addr = searcher:find_interactive( - 'Searching for cur_year_tick_advmode.', - 'int32_t', - function(idx) - if idx > 0 then - if not step_n_frames(1, feed) then - return false - end - end - return true, nil, 144 - end, - 20 - ) - - ms.found_offset('cur_year_tick_advmode', addr) -end - --- --- cur_season_tick --- - -local function find_cur_season_tick() - if not (is_known 'cur_year_tick') then - dfhack.printerr('Cannot search for cur_season_tick - prerequisites missing.') - return - end - - stop_autosave() - - local feed = dwarfmode_to_top() - local addr = searcher:find_interactive([[ -Searching for cur_season_tick. Please exit to main dwarfmode -menu, then do as instructed below:]], - 'int32_t', - function(ccursor) - if ccursor > 0 then - if not step_n_frames(10, feed) then - return false - end - end - return true, math.floor((df.global.cur_year_tick%100800)/10) - end - ) - ms.found_offset('cur_season_tick', addr) -end - --- --- cur_season --- - -local function find_cur_season() - if not (is_known 'cur_year_tick' and is_known 'cur_season_tick') then - dfhack.printerr('Cannot search for cur_season - prerequisites missing.') - return - end - - stop_autosave() - - local feed = dwarfmode_to_top() - local addr = searcher:find_interactive([[ -Searching for cur_season. Please exit to main dwarfmode -menu, then do as instructed below:]], - 'int8_t', - function(ccursor) - if ccursor > 0 then - local cst = df.global.cur_season_tick - df.global.cur_season_tick = 10079 - df.global.cur_year_tick = df.global.cur_year_tick + (10079-cst)*10 - if not step_n_frames(10, feed) then - return false - end - end - return true, math.floor(df.global.cur_year_tick/100800)%4 - end - ) - ms.found_offset('cur_season', addr) -end - --- --- process_jobs --- - -local function get_process_zone() - if os_type == 'windows' then - return zoomed_searcher('ui_workshop_job_cursor', 'ui_building_in_resize') - elseif os_type == 'linux' or os_type == 'darwin' then - return zoomed_searcher('cur_year', 'cur_year_tick') - end -end - -local function find_process_jobs() - local zone = get_process_zone() or searcher - local addr - - stop_autosave() - - if dwarfmode_to_top() and dfhack.internal.getAddress('cursor') then - local cursor = df.global.T_cursor:new() - addr = zone:find_interactive([[ -Searching for process_jobs. Please position the cursor to the left -of at least 10 vacant natural floor tiles.]], - 'int8_t', - function(idx) - if idx == 0 then - dwarfmode_feed_input('D_LOOK') - if not prompt_proceed(2) then return false end - cursor:assign(df.global.cursor) - elseif idx == 6 then - print(' Please resize the game window.') - if not prompt_proceed(2) then return false end - end - dwarfmode_to_top() - dwarfmode_step_frames(1) - if idx % 2 == 0 then - dwarfmode_feed_input( - 'D_BUILDING', - 'HOTKEY_BUILDING_CONSTRUCTION', - 'HOTKEY_BUILDING_CONSTRUCTION_WALL' - ) - df.global.cursor:assign(cursor) - df.global.cursor.x = df.global.cursor.x + math.floor(idx / 2) - dwarfmode_feed_input('CURSOR_RIGHT', 'CURSOR_LEFT', 'SELECT', 'SELECT') - return true, 1 - else - return true, 0 - end - end, - 20) - end - - if not addr then - addr = zone:find_menu_cursor([[ -Searching for process_jobs. Please do as instructed below:]], - 'int8_t', - { 1, 0 }, - { [1] = 'designate a building to be constructed, e.g a bed or a wall', - [0] = 'step or unpause the game to reset the flag' } - ) - end - ms.found_offset('process_jobs', addr) -end - --- --- process_dig --- - -local function find_process_dig() - local zone = get_process_zone() or searcher - local addr - - stop_autosave() - - if dwarfmode_to_top() and dfhack.internal.getAddress('cursor') then - local cursor = df.global.T_cursor:new() - addr = zone:find_interactive([[ -Searching for process_dig. Please position the cursor to the left -of at least 10 unmined, unrevealed tiles.]], - 'int8_t', - function(idx) - if idx == 0 then - dwarfmode_feed_input('D_LOOK') - if not prompt_proceed(2) then return false end - cursor:assign(df.global.cursor) - elseif idx == 6 then - print(' Please resize the game window.') - if not prompt_proceed(2) then return false end - end - dwarfmode_to_top() - dwarfmode_step_frames(1) - if idx % 2 == 0 then - dwarfmode_feed_input('D_DESIGNATE', 'DESIGNATE_DIG') - df.global.cursor:assign(cursor) - df.global.cursor.x = df.global.cursor.x + math.floor(idx / 2) - dwarfmode_feed_input('SELECT', 'SELECT') - return true, 1 - else - return true, 0 - end - end, - 20) - end - - if not addr then - addr = zone:find_menu_cursor([[ -Searching for process_dig. Please do as instructed below:]], - 'int8_t', - { 1, 0 }, - { [1] = 'designate a tile to be mined out', - [0] = 'step or unpause the game to reset the flag' } - ) - end - ms.found_offset('process_dig', addr) -end - --- --- pause_state --- - -local function find_pause_state() - local zone, addr - if os_type == 'linux' or os_type == 'darwin' then - zone = zoomed_searcher('ui_look_cursor', 32, true) - elseif os_type == 'windows' then - zone = zoomed_searcher('ui_workshop_job_cursor', 80) - end - zone = zone or searcher - - stop_autosave() - - if dwarfmode_to_top() then - addr = zone:find_interactive( - 'Auto-searching for pause_state', - 'int8_t', - function(idx) - if idx%2 == 0 then - dwarfmode_feed_input('D_ONESTEP') - return true, 0 - else - screen_dwarfmode():logic() - return true, 1 - end - end, - 20 - ) - end - - if not addr then - addr = zone:find_menu_cursor([[ -Searching for pause_state. Please do as instructed below:]], - 'int8_t', - { 1, 0 }, - { [1] = 'PAUSE the game', - [0] = 'UNPAUSE the game' } - ) - end - - ms.found_offset('pause_state', addr) -end - --- --- standing orders --- - -local function find_standing_orders(gname, seq, depends) - if type(seq) ~= 'table' then seq = {seq} end - for k, v in pairs(depends) do - if not dfhack.internal.getAddress(k) then - dfhack.printerr(('Cannot locate %s: %s not found'):format(gname, k)) - return - end - df.global[k] = v - end - local addr - if dwarfmode_to_top() then - addr = searcher:find_interactive( - 'Auto-searching for ' .. gname, - 'uint8_t', - function(idx) - dwarfmode_feed_input('D_ORDERS') - dwarfmode_feed_input(table.unpack(seq)) - return true - end - ) - else - dfhack.printerr("Won't scan for standing orders global manually: " .. gname) - return - end - - ms.found_offset(gname, addr) -end - -local function exec_finder_so(gname, seq, _depends) - local depends = {} --as:number[] - local _depends = _depends or {} --as:number[] - for k, v in pairs(_depends) do - if k:find('standing_orders_') ~= 1 then - k = 'standing_orders_' .. k - end - depends[k] = v - end - if force_scan['standing_orders'] then - force_scan[gname] = true - end - exec_finder(function() - return find_standing_orders(gname, seq, depends) - end, gname) -end - --- --- MAIN FLOW --- - -print('\nInitial globals (need title screen):\n') - -exec_finder(find_gview, 'gview') -exec_finder(find_cursor, { 'cursor', 'selection_rect', 'gamemode', 'gametype' }) -exec_finder(find_d_init, 'd_init', is_valid_d_init) -exec_finder(find_enabler, 'enabler', is_valid_enabler) -exec_finder(find_gps, 'gps', is_valid_gps) - -print('\nCompound globals (need loaded world):\n') - -print('\nPlease load the save previously processed with prepare-save.') -if not prompt_proceed() then - searcher:reset() - return -end - -exec_finder(find_world, 'world', is_valid_world) -exec_finder(find_ui, 'ui', is_valid_ui) -exec_finder(find_ui_sidebar_menus, 'ui_sidebar_menus') -exec_finder(find_ui_build_selector, 'ui_build_selector') -exec_finder(find_init, 'init', is_valid_init) - -print('\nPrimitive globals:\n') - -exec_finder(find_ui_menu_width, 'ui_menu_width') -exec_finder(find_ui_selected_unit, 'ui_selected_unit') -exec_finder(find_ui_unit_view_mode, 'ui_unit_view_mode') -exec_finder(find_ui_look_cursor, 'ui_look_cursor') -exec_finder(find_ui_building_item_cursor, 'ui_building_item_cursor') -exec_finder(find_ui_workshop_in_add, 'ui_workshop_in_add') -exec_finder(find_ui_workshop_job_cursor, 'ui_workshop_job_cursor') -exec_finder(find_ui_building_in_assign, 'ui_building_in_assign') -exec_finder(find_ui_building_in_resize, 'ui_building_in_resize') -exec_finder(find_ui_lever_target_type, 'ui_lever_target_type') -exec_finder(find_window_x, 'window_x') -exec_finder(find_window_y, 'window_y') -exec_finder(find_window_z, 'window_z') -exec_finder(find_current_weather, 'current_weather') - -print('\nUnpausing globals:\n') - -exec_finder(find_cur_year, 'cur_year') -exec_finder(find_cur_year_tick, 'cur_year_tick') -exec_finder(find_cur_year_tick_advmode, 'cur_year_tick_advmode') -exec_finder(find_cur_season_tick, 'cur_season_tick') -exec_finder(find_cur_season, 'cur_season') -exec_finder(find_process_jobs, 'process_jobs') -exec_finder(find_process_dig, 'process_dig') -exec_finder(find_pause_state, 'pause_state') - -print('\nStanding orders:\n') - -exec_finder_so('standing_orders_gather_animals', 'ORDERS_GATHER_ANIMALS') -exec_finder_so('standing_orders_gather_bodies', 'ORDERS_GATHER_BODIES') -exec_finder_so('standing_orders_gather_food', 'ORDERS_GATHER_FOOD') -exec_finder_so('standing_orders_gather_furniture', 'ORDERS_GATHER_FURNITURE') -exec_finder_so('standing_orders_gather_minerals', 'ORDERS_GATHER_STONE') -exec_finder_so('standing_orders_gather_wood', 'ORDERS_GATHER_WOOD') - -exec_finder_so('standing_orders_gather_refuse', - {'ORDERS_REFUSE', 'ORDERS_REFUSE_GATHER'}) -exec_finder_so('standing_orders_gather_refuse_outside', - {'ORDERS_REFUSE', 'ORDERS_REFUSE_OUTSIDE'}, {gather_refuse=1}) -exec_finder_so('standing_orders_gather_vermin_remains', - {'ORDERS_REFUSE', 'ORDERS_REFUSE_OUTSIDE_VERMIN'}, {gather_refuse=1, gather_refuse_outside=1}) -exec_finder_so('standing_orders_dump_bones', - {'ORDERS_REFUSE', 'ORDERS_REFUSE_DUMP_BONE'}, {gather_refuse=1}) -exec_finder_so('standing_orders_dump_corpses', - {'ORDERS_REFUSE', 'ORDERS_REFUSE_DUMP_CORPSE'}, {gather_refuse=1}) -exec_finder_so('standing_orders_dump_hair', - {'ORDERS_REFUSE', 'ORDERS_REFUSE_DUMP_STRAND_TISSUE'}, {gather_refuse=1}) -exec_finder_so('standing_orders_dump_other', - {'ORDERS_REFUSE', 'ORDERS_REFUSE_DUMP_OTHER'}, {gather_refuse=1}) -exec_finder_so('standing_orders_dump_shells', - {'ORDERS_REFUSE', 'ORDERS_REFUSE_DUMP_SHELL'}, {gather_refuse=1}) -exec_finder_so('standing_orders_dump_skins', - {'ORDERS_REFUSE', 'ORDERS_REFUSE_DUMP_SKIN'}, {gather_refuse=1}) -exec_finder_so('standing_orders_dump_skulls', - {'ORDERS_REFUSE', 'ORDERS_REFUSE_DUMP_SKULL'}, {gather_refuse=1}) - - -exec_finder_so('standing_orders_auto_butcher', - {'ORDERS_WORKSHOP', 'ORDERS_BUTCHER'}) -exec_finder_so('standing_orders_auto_collect_webs', - {'ORDERS_WORKSHOP', 'ORDERS_COLLECT_WEB'}) -exec_finder_so('standing_orders_auto_fishery', - {'ORDERS_WORKSHOP', 'ORDERS_AUTO_FISHERY'}) -exec_finder_so('standing_orders_auto_kiln', - {'ORDERS_WORKSHOP', 'ORDERS_AUTO_KILN'}) -exec_finder_so('standing_orders_auto_kitchen', - {'ORDERS_WORKSHOP', 'ORDERS_AUTO_KITCHEN'}) -exec_finder_so('standing_orders_auto_loom', - {'ORDERS_WORKSHOP', 'ORDERS_LOOM'}) -exec_finder_so('standing_orders_auto_other', - {'ORDERS_WORKSHOP', 'ORDERS_AUTO_OTHER'}) -exec_finder_so('standing_orders_auto_slaughter', - {'ORDERS_WORKSHOP', 'ORDERS_SLAUGHTER'}) -exec_finder_so('standing_orders_auto_smelter', - {'ORDERS_WORKSHOP', 'ORDERS_AUTO_SMELTER'}) -exec_finder_so('standing_orders_auto_tan', - {'ORDERS_WORKSHOP', 'ORDERS_TAN'}) -exec_finder_so('standing_orders_use_dyed_cloth', - {'ORDERS_WORKSHOP', 'ORDERS_DYED_CLOTH'}) - -exec_finder_so('standing_orders_forbid_other_dead_items', - {'ORDERS_AUTOFORBID', 'ORDERS_FORBID_OTHER_ITEMS'}) -exec_finder_so('standing_orders_forbid_other_nohunt', - {'ORDERS_AUTOFORBID', 'ORDERS_FORBID_OTHER_CORPSE'}) -exec_finder_so('standing_orders_forbid_own_dead', - {'ORDERS_AUTOFORBID', 'ORDERS_FORBID_YOUR_CORPSE'}) -exec_finder_so('standing_orders_forbid_own_dead_items', - {'ORDERS_AUTOFORBID', 'ORDERS_FORBID_YOUR_ITEMS'}) -exec_finder_so('standing_orders_forbid_used_ammo', - {'ORDERS_AUTOFORBID', 'ORDERS_FORBID_PROJECTILE'}) - -exec_finder_so('standing_orders_farmer_harvest', 'ORDERS_ALL_HARVEST') -exec_finder_so('standing_orders_job_cancel_announce', 'ORDERS_EXCEPTIONS') -exec_finder_so('standing_orders_mix_food', 'ORDERS_MIXFOODS') - -exec_finder_so('standing_orders_zoneonly_drink', - {'ORDERS_ZONE', 'ORDERS_ZONE_DRINKING'}) -exec_finder_so('standing_orders_zoneonly_fish', - {'ORDERS_ZONE', 'ORDERS_ZONE_FISHING'}) - -dwarfmode_to_top() -print('\nDone. Now exit the game with the die command and add\n'.. - 'the newly-found globals to symbols.xml. You can find them\n'.. - 'in stdout.log or here:\n') - -for _, global in ipairs(finder_searches) do - local addr = dfhack.internal.getAddress(global) - if addr ~= nil then - local ival = addr - dfhack.internal.getRebaseDelta() - print(string.format("", global, ival)) - end -end - -searcher:reset() diff --git a/devel/find-twbt.lua b/devel/find-twbt.lua deleted file mode 100644 index 61c75b3787..0000000000 --- a/devel/find-twbt.lua +++ /dev/null @@ -1,83 +0,0 @@ --- Find some TWBT-related offsets ---luacheck:skip-entirely ---[====[ - -devel/find-twbt -=============== - -Finds some TWBT-related offsets - currently just ``twbt_render_map``. - -]====] -local ms = require('memscan') -local cs = ms.get_code_segment() - -function get_ptr_size() - local v = df.new('uintptr_t') - local ret = df.sizeof(v) - df.delete(v) - return ret -end - -function print_off(name, off) - print(string.format("", name, off - dfhack.internal.getRebaseDelta())) -end - -local ptr_size = get_ptr_size() - -local vtoff = dfhack.internal.getVTable('viewscreen_dwarfmodest') --- print_off("Vtable:", vtoff) - -local vtable = ms.CheckedArray.new('uintptr_t', vtoff, vtoff + 3 * ptr_size) - -local render_method = vtable[2] --third method aka render --- print_off("render", render_method) - -function list_all_possible_calls(start, len) - local func = ms.CheckedArray.new('uint8_t', start, start + len) -- should be near - local possible_calls = {} - - for i = 0, #func - 1 do - if func[i] == 0xe8 then - table.insert(possible_calls, i) - end - end - return possible_calls -end -function get_call_target(offset) - local call_offset = df.reinterpret_cast('int32_t', offset + 1) - local ret = call_offset.value + offset + 5 - if cs:contains_range(ret, 1) then - return ret - else - return nil - end -end -function list_all_valid_calls(start, len) - local all_calls = list_all_possible_calls(start, len) - local ret = {} - for i, v in ipairs(all_calls) do - local ct = get_call_target(start + v) - if ct then - table.insert(ret, ct) - end - end - return ret -end --- TODO(warmist): this is probably stupid, but without disassembler ( sad face ) i can't say for sure --- if it's part of instruction or an arg, so we just hope to find only one? - -local possible_calls = list_all_valid_calls(render_method, 50) - -if #possible_calls == 0 then - qerror("Failed to find call instruction in render vmethod") -elseif #possible_calls > 1 then - qerror("Found multiple 0xe8 (call) in render vmethod start") -end - -local dwarfmode_render_main = possible_calls[1] -print_off("twbt_render_map", dwarfmode_render_main) - ---[[ other not used offset(s) -local dfrender = list_all_valid_calls(dwarfmode_render_main, 200)[2] --this could be further -print_off("A_RENDER_MAP", dfrender) -]] diff --git a/devel/hello-world.lua b/devel/hello-world.lua index d16045d703..2af576f815 100644 --- a/devel/hello-world.lua +++ b/devel/hello-world.lua @@ -1,31 +1,92 @@ --- Test lua viewscreens. ---[====[ +-- A basic example to start your own gui script from. +--@ module = true -devel/hello-world -================= -A basic example for testing, or to start your own script from. +local gui = require('gui') +local widgets = require('gui.widgets') -]====] -local gui = require 'gui' +local HIGHLIGHT_PEN = dfhack.pen.parse{ + ch=string.byte(' '), + fg=COLOR_LIGHTGREEN, + bg=COLOR_LIGHTGREEN, +} + +HelloWorldWindow = defclass(HelloWorldWindow, widgets.Window) +HelloWorldWindow.ATTRS{ + frame={w=25, h=25}, + frame_title='Hello World', + autoarrange_subviews=true, + autoarrange_gap=2, + resizable=true, + resize_min={w=25, h=25}, +} -local text = 'Woohoo, lua viewscreen :)' +function HelloWorldWindow:init() + local LEVEL_OPTIONS = { + {label='Low', value=1}, + {label='Medium', value=2}, + {label='High', value=3}, + {label='Pro', value=4}, + {label='Insane', value=5}, + } + + self:addviews{ + widgets.Label{text={{text='Hello, world!', pen=COLOR_LIGHTGREEN}}}, + widgets.HotkeyLabel{ + frame={l=0}, + label='Click me', + key='CUSTOM_CTRL_A', + on_activate=self:callback('toggleHighlight'), + }, + widgets.Panel{ + view_id='highlight', + frame={w=10, h=5}, + frame_style=gui.INTERIOR_FRAME, + }, + widgets.Divider{ + frame={h=1}, + frame_style_l=false, + frame_style_r=false, + }, + widgets.CycleHotkeyLabel{ + view_id='level', + frame={l=0, w=20}, + label='Level:', + key_back='CUSTOM_SHIFT_C', + key='CUSTOM_SHIFT_V', + options=LEVEL_OPTIONS, + initial_option=LEVEL_OPTIONS[1].value, + }, + widgets.Slider{ + frame={l=1}, + num_stops=#LEVEL_OPTIONS, + get_idx_fn=function() + return self.subviews.level:getOptionValue() + end, + on_change=function(idx) self.subviews.level:setOption(idx) end, + }, + } +end -local screen = gui.FramedScreen{ - frame_style = gui.GREY_LINE_FRAME, - frame_title = 'Hello World', - frame_width = #text, - frame_height = 1, - frame_inset = 1, +function HelloWorldWindow:toggleHighlight() + local panel = self.subviews.highlight + panel.frame_background = not panel.frame_background and HIGHLIGHT_PEN or nil +end + +HelloWorldScreen = defclass(HelloWorldScreen, gui.ZScreen) +HelloWorldScreen.ATTRS{ + focus_path='hello-world', } -function screen:onRenderBody(dc) - dc:string(text, COLOR_LIGHTGREEN) +function HelloWorldScreen:init() + self:addviews{HelloWorldWindow{}} +end + +function HelloWorldScreen:onDismiss() + view = nil end -function screen:onInput(keys) - if keys.LEAVESCREEN or keys.SELECT then - self:dismiss() - end +if dfhack_flags.module then + return end -screen:show() +view = view and view:raise() or HelloWorldScreen{}:show() diff --git a/devel/inject-raws.lua b/devel/inject-raws.lua index 446baaadb3..62b4403c12 100644 --- a/devel/inject-raws.lua +++ b/devel/inject-raws.lua @@ -134,7 +134,7 @@ function add_to_dwarf_civs(btype, id) end for _,entity in ipairs(df.global.world.entities.all) do - if entity.race == df.global.ui.race_id then + if entity.race == df.global.plotinfo.race_id then local bvec = typeinfo[3] --as:string add_to_civ(entity, bvec, id) end diff --git a/devel/input-monitor.lua b/devel/input-monitor.lua new file mode 100644 index 0000000000..7b1c20fad6 --- /dev/null +++ b/devel/input-monitor.lua @@ -0,0 +1,146 @@ +local gui = require('gui') +local widgets = require('gui.widgets') + +----------------------- +-- InputMonitorWindow +-- + +InputMonitorWindow = defclass(InputMonitorWindow, widgets.Window) +InputMonitorWindow.ATTRS{ + frame={w=51, h=50}, + frame_title='Input Monitor', + resizable=true, + resize_min={h=20}, +} + +local function getModifierPen(which) + return dfhack.internal.getModifiers()[which] and + COLOR_WHITE or COLOR_GRAY +end + +local function getButtonPen(which) + which = ('mouse_%s_down'):format(which) + return df.global.enabler[which] == 1 and + COLOR_WHITE or COLOR_GRAY +end + +function InputMonitorWindow:init() + self:addviews{ + widgets.Label{ + frame={l=0, t=0}, + text={ + 'Modifier keys:', + {gap=1, text='Shift', pen=function() return getModifierPen('shift') end}, + {gap=1, text='Ctrl', pen=function() return getModifierPen('ctrl') end}, + {gap=1, text='Alt', pen=function() return getModifierPen('alt') end}, + }, + }, + widgets.Label{ + frame={l=0, t=2}, + text={ + 'Mouse buttons:', + {gap=1, text='Lbut', pen=function() return getButtonPen('lbut') end}, + {gap=1, text='Mbut', pen=function() return getButtonPen('mbut') end}, + {gap=1, text='Rbut', pen=function() return getButtonPen('rbut') end}, + }, + }, + widgets.Panel{ + view_id='streampanel', + frame={t=4, b=2, l=0, r=0}, + frame_style=gui.INTERIOR_FRAME, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text='Input stream (newest at bottom):', + }, + widgets.Label{ + view_id='streamlog', + frame={t=1, l=2, b=0}, + auto_height=false, + text={}, + }, + }, + }, + widgets.HotkeyLabel{ + frame={b=0}, + key='LEAVESCREEN', + label='Hit ESC twice or click here twice to close', + text_pen=function() + return self.escape_armed and COLOR_LIGHTRED or COLOR_WHITE + end, + auto_width=true, + on_activate=function() + if self.escape_armed then + self.parent_view:dismiss() + end + self.escape_armed = true + end, + }, + } +end + +function InputMonitorWindow:onInput(keys) + local streamlog = self.subviews.streamlog + local stream = streamlog.text + if #stream > 0 then + table.insert(stream, NEWLINE) + table.insert(stream, NEWLINE) + end + for key in pairs(keys) do + if key == '_STRING' then + table.insert(stream, + ('_STRING="%s" (%d)'):format(keys._STRING == 0 and '' or string.char(keys._STRING), keys._STRING)) + else + table.insert(stream, key) + end + print(stream[#stream]) + table.insert(stream, NEWLINE) + end + print() + local newstream = {} + local num_lines = self.subviews.streampanel.frame_rect.height - 2 + for idx=#stream,1,-1 do + local elem = stream[idx] + if elem == NEWLINE then + num_lines = num_lines - 1 + if num_lines <= 0 then + break + end + end + table.insert(newstream, elem) + end + for idx=1,#newstream//2 do + local mirror_idx = #newstream-idx+1 + newstream[idx], newstream[mirror_idx] = newstream[mirror_idx], newstream[idx] + end + streamlog:setText(newstream) + + InputMonitorWindow.super.onInput(self, keys) + if not keys._MOUSE_L and not keys._MOUSE_L_DOWN and not keys.LEAVESCREEN then + self.escape_armed = false + end + return true +end + +----------------------- +-- InputMonitorScreen +-- + +InputMonitorScreen = defclass(InputMonitorScreen, gui.ZScreen) +InputMonitorScreen.ATTRS{ + focus_path='input-monitor', +} + +function InputMonitorScreen:init() + self:addviews{InputMonitorWindow{}} +end + +function InputMonitorScreen:onDismiss() + view = nil +end + +if dfhack_flags.module then + return +end + +view = view and view:raise() or InputMonitorScreen{}:show() diff --git a/devel/inspect-screen.lua b/devel/inspect-screen.lua index 62b5b35798..c92f4b5f15 100644 --- a/devel/inspect-screen.lua +++ b/devel/inspect-screen.lua @@ -1,110 +1,354 @@ -- Read from the screen and display info about the tiles ---[====[ -devel/inspect-screen -==================== -Read the tiles from the screen and display info about them. +local gui = require('gui') +local guidm = require('gui.dwarfmode') +local widgets = require('gui.widgets') +local overlay = require('plugins.overlay') -]====] +Inspect = defclass(Inspect, widgets.Window) +Inspect.ATTRS{ + frame={w=40, h=20}, + resizable=true, + frame_title='Screen Inspector', +} -local utils = require 'utils' -local gui = require 'gui' +function Inspect:init() + local scr_name = overlay.simplify_viewscreen_name( + getmetatable(dfhack.gui.getDFViewscreen(true))) -InspectScreen = defclass(InspectScreen, gui.Screen) + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text={'Current screen: ', {text=scr_name, pen=COLOR_CYAN}}, + }, + widgets.CycleHotkeyLabel{ + view_id='layer', + frame={t=2, l=0}, + key='CUSTOM_CTRL_A', + label='Inspect layer:', + options={{label='UI', value='ui'}, 'map'}, + enabled=self:callback('is_unfrozen'), + }, + widgets.CycleHotkeyLabel{ + view_id='empties', + frame={t=3, l=0}, + key='CUSTOM_CTRL_E', + label='Empty elements:', + options={'hide', 'show'}, + }, + widgets.ToggleHotkeyLabel{ + view_id='freeze', + frame={t=4, l=0}, + key='CUSTOM_CTRL_F', + label='Freeze current tile:', + initial_option=false, + }, + widgets.Label{ + frame={t=6}, + text={{text=self:callback('get_grid_size')}}, + }, + widgets.Label{ + frame={t=7}, + text={{text=self:callback('get_mouse_pos')}}, + }, + widgets.Label{ + view_id='report', + frame={t=9}, + }, + } +end -function InspectScreen:init(args) - local w,h = dfhack.screen.getWindowSize() - self.cursor_x = math.floor(w/2) - self.cursor_y = math.floor(h/2) +function Inspect:is_unfrozen() + return not self.subviews.freeze:getOptionValue() end -function InspectScreen:computeFrame(parent_rect) - local sw, sh = parent_rect.width, parent_rect.height - self.cursor_x = math.max(0, math.min(self.cursor_x, sw-1)) - self.cursor_y = math.max(0, math.min(self.cursor_y, sh-1)) +function Inspect:do_refresh() + return self:is_unfrozen() and not self:getMouseFramePos() +end - local frame = { w = 14, r = 1, h = 10, t = 1 } - if self.cursor_x > sw/2 then - frame = { w = 14, l = 1, h = 10, t = 1 } +function Inspect:get_grid_size() + if self.subviews.layer:getOptionValue() == 'ui' then + local width, height = dfhack.screen.getWindowSize() + return ('UI grid size: %d x %d'):format(width, height) + end + local layout = guidm.getPanelLayout() + return ('Map grid size: %d x %d'):format(layout.map.width, layout.map.height) +end + +local cur_mouse_pos = {x=-1, y=-1} +function Inspect:get_mouse_pos() + local pos, text = cur_mouse_pos, '' + if self.subviews.layer:getOptionValue() == 'ui' then + if self:do_refresh() then + pos = xy2pos(dfhack.screen.getMousePos()) + end + text = ('UI grid coords: %s, %s'):format(pos.x, pos.y) + else + if self:do_refresh() then + pos = dfhack.gui.getMousePos() + end + if pos then + text = ('Map coords: %s, %s, %s'):format(pos.x, pos.y, pos.z) + else + text = 'Mouse is not on the map' + end end + cur_mouse_pos = pos or cur_mouse_pos + return text +end - return gui.compute_frame_body(sw, sh, frame, 1, 0, false) +local function add_screen_report_line(report, base, vec, index, show_empty) + local ch = base[vec][index] + if ch <= 0 and not show_empty then return end + table.insert(report, '[' .. string.char(ch) .. ']') + table.insert(report, (' (%s) - %s'):format(ch, vec)) + table.insert(report, NEWLINE) + table.insert(report, (' fg: %d, %d, %d') + :format(base[vec][index+1], base[vec][index+2], base[vec][index+3])) + table.insert(report, NEWLINE) + table.insert(report, (' bg: %d, %d, %d') + :format(base[vec][index+4], base[vec][index+5], base[vec][index+6])) + table.insert(report, NEWLINE) end -function InspectScreen:onRenderFrame(dc, rect) - self:renderParent() - self.cursor_pen = dfhack.screen.readTile(self.cursor_x, self.cursor_y) - if gui.blink_visible(100) then - dfhack.screen.paintTile({ch='X',fg=COLOR_LIGHTGREEN}, self.cursor_x, self.cursor_y) +local function add_texpos_report_line(report, base, vec, index, show_empty) + local texpos = base[vec][index] + if texpos <= 0 and not show_empty then return end + table.insert(report, '[') + table.insert(report, {tile=texpos}) + table.insert(report, ']') + table.insert(report, (' (%s) - %s'):format(texpos, vec)) + table.insert(report, NEWLINE) +end + +local function pop_flag(report, flag, name, show_empty, dim) + dim = dim or 2 + local bit = flag % dim + if bit ~= 0 or show_empty then + table.insert(report, (' %s = %d'):format(name, bit)) + table.insert(report, NEWLINE) end - dc:fill(rect, {ch=' ',fg=COLOR_WHITE,bg=COLOR_CYAN}) + return flag // dim end -local FG_PEN = {fg=COLOR_WHITE,bg=COLOR_BLACK,tile_color=true} -local BG_PEN = {fg=COLOR_BLACK,bg=COLOR_WHITE,tile_color=true} -local TXT_PEN = {fg=COLOR_WHITE} - -function InspectScreen:onRenderBody(dc) - dc:pen(COLOR_WHITE, COLOR_CYAN) - if self.cursor_pen then - local info = self.cursor_pen - dc:string('CH: '):char(info.ch, FG_PEN):char(info.ch, BG_PEN):string(' '):string(''..info.ch,TXT_PEN):newline() - local fgcolor = info.fg - local fgstr = tostring(info.fg) - if info.bold then - fgcolor = (fgcolor+8)%16 - fgstr = fgstr..'+8' - end - dc:string('FG: '):string('NN',{fg=fgcolor}):string(' '):string(''..fgstr,TXT_PEN) - dc:seek(dc.width-1):char(info.ch,{fg=info.fg,bold=info.bold}):newline() - dc:string('BG: '):string('NN',{fg=info.bg}):string(' '):string(''..info.bg,TXT_PEN) - dc:seek(dc.width-1):char(info.ch,{fg=COLOR_BLACK,bg=info.bg}):newline() - local bstring = 'false' - if info.bold then bstring = 'true' end - dc:string('Bold: '..bstring):newline():newline() - - if info.tile and gui.USE_GRAPHICS then - dc:string('TL: '):tile(' ', info.tile, FG_PEN):tile(' ', info.tile, BG_PEN):string(' '..info.tile):newline() - if info.tile_color then - dc:string('Color: true') - elseif info.tile_fg then - dc:string('FG: '):string('NN',{fg=info.tile_fg}):string(' '):string(''..info.tile_fg,TXT_PEN):newline() - dc:string('BG: '):string('NN',{fg=info.tile_bg}):string(' '):string(''..info.tile_bg,TXT_PEN):newline() - end - end - else - dc:string('Invalid', COLOR_LIGHTRED) +local function add_texpos_flag_report_line(report, base, vec, index, flags, show_empty) + local val = base[vec][index] + if val == 0 and not show_empty then return end + table.insert(report, vec) + table.insert(report, NEWLINE) + for _,f in ipairs(flags or {}) do + val = pop_flag(report, val, f.name, show_empty, f.dim) end end -local MOVEMENT_KEYS = { - CURSOR_UP = { 0, -1, 0 }, CURSOR_DOWN = { 0, 1, 0 }, - CURSOR_LEFT = { -1, 0, 0 }, CURSOR_RIGHT = { 1, 0, 0 }, - CURSOR_UPLEFT = { -1, -1, 0 }, CURSOR_UPRIGHT = { 1, -1, 0 }, - CURSOR_DOWNLEFT = { -1, 1, 0 }, CURSOR_DOWNRIGHT = { 1, 1, 0 }, - CURSOR_UP_FAST = { 0, -1, 0, true }, CURSOR_DOWN_FAST = { 0, 1, 0, true }, - CURSOR_LEFT_FAST = { -1, 0, 0, true }, CURSOR_RIGHT_FAST = { 1, 0, 0, true }, - CURSOR_UPLEFT_FAST = { -1, -1, 0, true }, CURSOR_UPRIGHT_FAST = { 1, -1, 0, true }, - CURSOR_DOWNLEFT_FAST = { -1, 1, 0, true }, CURSOR_DOWNRIGHT_FAST = { 1, 1, 0, true }, +local gps = df.global.gps + +local screentexpos_flags = { + {name='grayscale'}, + {name='addcolor'}, + {name='anchor_subordinate'}, + {name='top_of_text'}, + {name='bottom_of_text'}, + {name='anchor_use_screen_color'}, + {name='anchor_x_coord', dim=64}, + {name='anchor_y_coord', dim=64}, } -function InspectScreen:onInput(keys) - if keys.LEAVESCREEN then - self:dismiss() - else - for k,v in pairs(MOVEMENT_KEYS) do - if keys[k] then - local delta = 1 - if v[4] then - delta = 10 - end - self.cursor_x = self.cursor_x + delta*v[1] - self.cursor_y = self.cursor_y + delta*v[2] - self:updateLayout() - return - end - end +local function get_ui_report(show_empty) + local report = {} + + local pos = cur_mouse_pos + if pos.x < 0 or pos.y < 0 then return report end + + local index = (pos.x * gps.dimy) + pos.y + + add_screen_report_line(report, gps, 'screen', index*8, show_empty) + add_texpos_report_line(report, gps, 'screentexpos_lower', index, show_empty) + add_texpos_report_line(report, gps, 'screentexpos', index, show_empty) + add_texpos_report_line(report, gps, 'screentexpos_anchored', index, show_empty) + add_texpos_report_line(report, gps, 'screentexpos_anchored_x', index, show_empty) + add_texpos_report_line(report, gps, 'screentexpos_anchored_y', index, show_empty) + add_texpos_flag_report_line(report, gps, 'screentexpos_flag', index, + screentexpos_flags, show_empty) + + if gps.top_in_use then + add_screen_report_line(report, gps, 'screen_top', index*8, show_empty) + add_texpos_report_line(report, gps, 'screentexpos_top_lower', index, show_empty) + add_texpos_report_line(report, gps, 'screentexpos_top', index, show_empty) + add_texpos_report_line(report, gps, 'screentexpos_top_anchored', index, show_empty) + add_texpos_report_line(report, gps, 'screentexpos_top_anchored_x', index, show_empty) + add_texpos_report_line(report, gps, 'screentexpos_top_anchored_y', index, show_empty) + add_texpos_flag_report_line(report, gps, 'screentexpos_top_flag', index, + screentexpos_flags, show_empty) + end + + return report +end + +local viewport_floor_flags = { + {name='s_edging', dim=256}, + {name='w_edging', dim=256}, + {name='e_edging', dim=256}, + {name='n_edging', dim=256}, + {name='special_texture', dim=8}, +} + +local viewport_liquid_flags = { + {name='center_animation', dim=4}, + {name='center_level', dim=8}, + {name='center_type', dim=16}, + {name='n_edge_type', dim=16}, + {name='s_edge_type', dim=16}, + {name='w_edge_type', dim=16}, + {name='e_edge_type', dim=16}, +} + +local viewport_spatter_flags = { + {name='shape_type', dim=64}, + {name='material_type', dim=32}, + {name='color_index', dim=256}, + {name='derived_plus', dim=512}, + {name='fire', dim=8}, + {name='accepts_spatter'}, +} + +local viewport_ramp_flags = { + {name='type', dim=256}, + {name='wall_n'}, + {name='wall_w'}, + {name='wall_e'}, + {name='wall_s'}, + {name='wall_nw'}, + {name='wall_ne'}, + {name='wall_sw'}, + {name='wall_se'}, + {name='n_is_dark_corner'}, + {name='s_is_dark_corner'}, + {name='w_is_dark_corner'}, + {name='e_is_dark_corner'}, + {name='n_is_open_air'}, + {name='s_is_open_air'}, + {name='w_is_open_air'}, + {name='e_is_open_air'}, + {name='show_up_arrow'}, + {name='show_down_arrow'}, + {name='color_index', dim=256}, +} + +local viewport_shadow_flags = { + {name='accepts_shadow'}, + {name='is_shadow_wall'}, + {name='shadow_wall_to_n'}, + {name='shadow_wall_to_s'}, + {name='shadow_wall_to_w'}, + {name='shadow_wall_to_e'}, + {name='shadow_wall_to_nw'}, + {name='shadow_wall_to_ne'}, + {name='shadow_wall_to_sw'}, + {name='shadow_wall_to_se'}, + {name='ramp_shadow_on_floor_nw_of_corner_se'}, + {name='ramp_shadow_on_floor_n_of_corner_se'}, + {name='ramp_shadow_on_floor_n_of_s'}, + {name='ramp_shadow_on_floor_n_of_corner_sw'}, + {name='ramp_shadow_on_floor_ne_of_corner_sw'}, + {name='ramp_shadow_on_floor_w_of_corner_se'}, + {name='ramp_shadow_on_floor_e_of_corner_sw'}, + {name='ramp_shadow_on_floor_w_of_e'}, + {name='ramp_shadow_on_floor_e_of_w'}, + {name='ramp_shadow_on_floor_w_of_corner_ne'}, + {name='ramp_shadow_on_floor_e_of_corner_nw'}, + {name='ramp_shadow_on_floor_sw_of_corner_ne'}, + {name='ramp_shadow_on_floor_s_of_corner_ne'}, + {name='ramp_shadow_on_floor_s_of_n'}, + {name='ramp_shadow_on_floor_s_of_corner_nw'}, + {name='ramp_shadow_on_floor_se_of_corner_nw'}, +} + +local function get_map_report(show_empty) + local report = {} + + local pos = cur_mouse_pos + if not pos or not pos.z then + return report + end + + if not dfhack.screen.inGraphicsMode() then + table.insert(report, 'Please inspect the UI layer') + table.insert(report, NEWLINE) + table.insert(report, 'when not in graphics mode') + return report + end + + local vp = gps.main_viewport + local index = ((pos.x - df.global.window_x) * vp.dim_y) + pos.y - df.global.window_y + + add_texpos_report_line(report, vp, 'screentexpos_background', index, show_empty) + add_texpos_flag_report_line(report, vp, 'screentexpos_floor_flag', index, + viewport_floor_flags, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_background_two', index, show_empty) + add_texpos_flag_report_line(report, vp, 'screentexpos_liquid_flag', index, + viewport_liquid_flags, show_empty) + add_texpos_flag_report_line(report, vp, 'screentexpos_spatter_flag', index, + viewport_spatter_flags, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_spatter', index, show_empty) + add_texpos_flag_report_line(report, vp, 'screentexpos_ramp_flag', index, + viewport_ramp_flags, show_empty) + add_texpos_flag_report_line(report, vp, 'screentexpos_shadow_flag', index, + viewport_shadow_flags, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_building_one', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_item', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_vehicle', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_vermin', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_left_creature', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_right_creature', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_building_two', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_projectile', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_high_flow', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_top_shadow', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_signpost', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_upleft_creature', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_up_creature', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_upright_creature', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_designation', index, show_empty) + add_texpos_report_line(report, vp, 'screentexpos_interface', index, show_empty) + + return report +end + +function Inspect:onRenderBody() + if not self:do_refresh() then return end + local show_empty = self.subviews.empties:getOptionValue() == 'show' + local report = self.subviews.layer:getOptionValue() == 'ui' and + get_ui_report(show_empty) or get_map_report(show_empty) + self.subviews.report:setText(report) + self:updateLayout() +end + +function Inspect:onInput(keys) + if Inspect.super.onInput(self, keys) then + return true + end + if keys._MOUSE_L and not self:getMouseFramePos() then + self.subviews.freeze:cycle() + return true end end -InspectScreen{}:show() +InspectScreen = defclass(InspectScreen, gui.ZScreenModal) +InspectScreen.ATTRS{ + focus_string='inspect-screen', +} + +function InspectScreen:init() + -- prevent hotspot widgets from reacting + overlay.register_trigger_lock_screen(self) + + self:addviews{Inspect{}} +end + +function InspectScreen:onDismiss() + view = nil +end + +view = view and view:raise() or InspectScreen{}:show() diff --git a/devel/kill-hf.lua b/devel/kill-hf.lua index e2cf5f7215..63b8083988 100644 --- a/devel/kill-hf.lua +++ b/devel/kill-hf.lua @@ -1,31 +1,5 @@ -- Kills the specified historical figure ---[====[ - -devel/kill-hf -============= - -Kills the specified historical figure, even if off-site, or terminates a -pregnancy. Useful for working around :bug:`11549`. - -Usage:: - - devel/kill-hf [-p|--pregnancy] [-n|--dry-run] HISTFIG_ID - -Arguments: - -``histfig_id``: - the ID of the historical figure to target - -``-p``, ``--pregnancy``: - if specified, and if the historical figure is pregnant, terminate the - pregnancy instead of killing the historical figure - -``-n``, ``--dry-run``: - if specified, only print the name of the historical figure - -]====] - local target_hf = -1 local target_pregnancy = false local dry_run = false @@ -44,7 +18,7 @@ end local hf = df.historical_figure.find(target_hf) or qerror('histfig not found: ' .. target_hf) -local hf_name = dfhack.df2console(dfhack.TranslateName(hf.name)) +local hf_name = dfhack.df2console(dfhack.translation.translateName(hf.name)) local hf_desc = ('%i: %s (%s)'):format(target_hf, hf_name, dfhack.units.getRaceNameById(hf.race)) if dry_run then diff --git a/devel/light.lua b/devel/light.lua index aa741f4d07..ff56e34c30 100644 --- a/devel/light.lua +++ b/devel/light.lua @@ -1,16 +1,5 @@ -- an experimental lighting engine ---[====[ -devel/light -=========== -An experimental lighting engine for DF, using the `rendermax` plugin. - -Call ``devel/light static`` to not recalculate lighting when in game. -Press :kbd:`~` to recalculate lighting. Press :kbd:`\`` to exit. - -]====] - -local gui = require 'gui' local guidm = require 'gui.dwarfmode' local render = require 'plugins.rendermax' @@ -27,12 +16,6 @@ function setCell(x,y,cell) cell.bo=cell.bo or {r=0,g=0,b=0} render.setCell(x,y,cell) end -function getCursorPos() - local g_cursor=df.global.cursor - if g_cursor.x ~= -30000 then - return copyall(g_cursor) - end -end --luacheck: skip function falloff(color,sqDist,maxdist) local v1=1/(sqDist/maxdist+1) @@ -273,7 +256,7 @@ function LightOverlay:calculateLightSun() end end function LightOverlay:calculateLightCursor() - local c=getCursorPos() + local c=guidm.getCursorPos() if c then diff --git a/devel/list-filters.lua b/devel/list-filters.lua index 9c4ebaa888..c9aa9245aa 100644 --- a/devel/list-filters.lua +++ b/devel/list-filters.lua @@ -54,7 +54,7 @@ end local function dump(name) local out = {} - for i,v in ipairs(df.global.ui_build_selector.requirements) do + for i,v in ipairs(df.global.buildreq.requirements) do out[#out+1] = clone_filter(v.filter, v.count_required) end @@ -64,8 +64,8 @@ local function dump(name) print(fmt) end -local itype = df.global.ui_build_selector.building_type -local stype = df.global.ui_build_selector.building_subtype +local itype = df.global.buildreq.building_type +local stype = df.global.buildreq.building_subtype if itype == df.building_type.Workshop then dump(' [df.workshop_type.'..df.workshop_type[stype]..'] = ') diff --git a/devel/lsmem.lua b/devel/lsmem.lua index dc9100e2f4..286a5b3f2b 100644 --- a/devel/lsmem.lua +++ b/devel/lsmem.lua @@ -8,6 +8,39 @@ valid, whether a certain library/plugin is loaded, etc. ]====] +function range_contains_any(range, addrs) + for _, a in ipairs(addrs) do + if a >= range.start_addr and a < range.end_addr then + return true + end + end + return false +end + +function range_name_match_any(range, names) + for _, n in ipairs(names) do + if range.name:lower():find(n, 1, true) or range.name:lower():find(n) then + return true + end + end + return false +end + +local args = {...} +local filter_addrs = {} +local filter_names = {} +for _, arg in ipairs(args) do + if arg:lower():startswith('0x') then + arg = arg:sub(3) + end + local addr = tonumber(arg, 16) + if addr then + table.insert(filter_addrs, addr) + else + table.insert(filter_names, arg:lower()) + end +end + for _,v in ipairs(dfhack.internal.getMemRanges()) do local access = { '-', '-', '-', 'p' } if v.read then access[1] = 'r' end @@ -18,5 +51,9 @@ for _,v in ipairs(dfhack.internal.getMemRanges()) do elseif v.shared then access[4] = 's' end - print(string.format('%08x-%08x %s %s', v.start_addr, v.end_addr, table.concat(access), v.name)) + if (#filter_addrs == 0 or range_contains_any(v, filter_addrs)) and + (#filter_names == 0 or range_name_match_any(v, filter_names)) + then + print(string.format('%08x-%08x %s %s', v.start_addr, v.end_addr, table.concat(access), v.name)) + end end diff --git a/devel/make-dt.pl b/devel/make-dt.pl index e1198ae9c1..9d8ba2c433 100755 --- a/devel/make-dt.pl +++ b/devel/make-dt.pl @@ -84,17 +84,17 @@ ($$$$) emit_addr 'language_vector',%globals,'world','world.raws.language.words'; emit_addr 'creature_vector',%globals,'world','world.units.all'; emit_addr 'active_creature_vector',%globals,'world','world.units.active'; - emit_addr 'dwarf_race_index',%globals,'ui','ui.race_id'; + emit_addr 'dwarf_race_index',%globals,'plotinfo','plotinfo.race_id'; emit_addr 'squad_vector',%globals,'world','world.squads.all'; emit_addr 'current_year',%globals,'cur_year','cur_year'; emit_addr 'cur_year_tick',%globals,'cur_year_tick','cur_year_tick'; - emit_addr 'dwarf_civ_index',%globals,'ui','ui.civ_id'; + emit_addr 'dwarf_civ_index',%globals,'plotinfo','plotinfo.civ_id'; emit_addr 'races_vector',%globals,'world','world.raws.creatures.all'; emit_addr 'reactions_vector',%globals,'world','world.raws.reactions'; emit_addr 'events_vector',%globals,'world','world.history.events'; emit_addr 'historical_figures_vector',%globals,'world','world.history.figures'; emit_addr 'fake_identities_vector',%globals,'world','world.identities.all'; - emit_addr 'fortress_entity',%globals,'ui','ui.main.fortress_entity'; + emit_addr 'fortress_entity',%globals,'plotinfo','plotinfo.main.fortress_entity'; emit_addr 'historical_entities_vector',%globals,'world','world.entities.all'; emit_addr 'itemdef_weapons_vector',%globals,'world','world.raws.itemdefs.weapons'; emit_addr 'itemdef_trap_vector',%globals,'world','world.raws.itemdefs.trapcomps'; @@ -316,7 +316,7 @@ ($$$$) emit_addr 'specific_refs',%all,'unit','specific_refs'; emit_addr 'squad_id',%all,'unit','military.squad_id'; emit_addr 'squad_position',%all,'unit','military.squad_position'; - emit_addr 'recheck_equipment',%all,'unit','military.pickup_flags'; + emit_addr 'recheck_equipment',%all,'unit','uniform.pickup_flags'; emit_addr 'mood',%all,'unit','mood'; emit_addr 'birth_year',%all,'unit','birth_year'; emit_addr 'birth_time',%all,'unit','birth_time'; @@ -325,9 +325,9 @@ ($$$$) emit_addr 'physical_attrs',%all,'unit','body.physical_attrs'; emit_addr 'body_size',%all,'unit','appearance.body_modifiers'; emit_addr 'size_info',%all,'unit','body.size_info'; - emit_addr 'curse',%all,'unit','curse.name'; - emit_addr 'curse_add_flags1',%all,'unit','curse.add_tags1'; - emit_addr 'turn_count',%all,'unit','curse.time_on_site'; + emit_addr 'curse',%all,'unit','uwss_display_name_sing'; + emit_addr 'curse_add_flags1',%all,'unit','uwss_add_caste_flag'; + emit_addr 'turn_count',%all,'unit','usable_interaction.time_on_site'; emit_addr 'souls',%all,'unit','status.souls'; emit_addr 'states',%all,'unit','status.misc_traits'; emit_addr 'labors',%all,'unit','status.labors'; @@ -386,7 +386,7 @@ ($$$$) emit_addr 'goals',%all,'unit_personality','dreams'; emit_addr 'goal_realized',%all,'unit_personality::anon5','unk8'; emit_addr 'traits',%all,'unit_personality','traits'; - emit_addr 'stress_level',%all,'unit_personality','stress_level'; + emit_addr 'stress',%all,'unit_personality','stress'; emit_header 'emotion_offsets'; emit_addr 'emotion_type',%all,'unit_personality::anon4','type'; diff --git a/devel/modstate-monitor.lua b/devel/modstate-monitor.lua index 160bd08a36..11961f35c0 100644 --- a/devel/modstate-monitor.lua +++ b/devel/modstate-monitor.lua @@ -1,19 +1,5 @@ -- Displays changes in the key modifier state --@ enable = true ---[====[ - -devel/modstate-monitor -====================== -Display changes in key modifier state, i.e. :kbd:`Ctrl`/:kbd:`Alt`/:kbd:`Shift`. - -Arguments: - -:enable|start: Begin monitoring -:disable|stop: End monitoring - -]====] - -VERSION = '0.1' active = active or false @@ -22,11 +8,7 @@ if dfhack.internal.getModstate == nil or dfhack.internal.getModifiers == nil the end function usage() - print [[ -Usage: - modstate-monitor enable|start: Begin monitoring - modstate-monitor disable|stop: End monitoring -]] + print(dfhack.script_help()) end function set_timeout() diff --git a/devel/nuke-items.lua b/devel/nuke-items.lua index 36d623b8a6..82ce37977a 100644 --- a/devel/nuke-items.lua +++ b/devel/nuke-items.lua @@ -1,21 +1,12 @@ -- Delete ALL items not held by units, buildings or jobs ---[====[ - -devel/nuke-items -================ -Deletes ALL items not held by units, buildings or jobs. -Intended solely for lag investigation. - -]====] local count = 0 for _,v in ipairs(df.global.world.items.all) do if not (v.flags.in_building or v.flags.construction or v.flags.in_job - or dfhack.items.getGeneralRef(v,df.general_ref_type.UNIT_HOLDER)) then + or dfhack.items.getGeneralRef(v,df.general_ref_type.UNIT_HOLDER)) then count = count + 1 - v.flags.forbid = true - v.flags.garbage_collect = true + dfhack.items.remove(v) end end diff --git a/devel/pop-screen.lua b/devel/pop-screen.lua index ef320ab090..8b90620854 100644 --- a/devel/pop-screen.lua +++ b/devel/pop-screen.lua @@ -1,24 +1,3 @@ -- Forcibly closes the current screen ---[====[ - -devel/pop-screen -================ -Forcibly closes the current screen. This is usually equivalent to pressing -:kbd:`Esc` (``LEAVESCREEN``), but will bypass the screen's input handling. This is -intended primarily for development, if you have created a screen whose input -handling throws an error before it handles :kbd:`Esc` (or if you have forgotten -to handle :kbd:`Esc` entirely). - -.. warning:: - - If you run this script when the current screen does not have a parent, - this will cause DF to exit **immediately**. These screens include: - - * The main fortress mode screen (``viewscreen_dwarfmodest``) - * The main adventure mode screen (``viewscreen_dungeonmodest``) - * The main legends mode screen (``viewscreen_legendsst``) - * The title screen (``viewscreen_titlest``) - -]====] dfhack.screen.dismiss(dfhack.gui.getCurViewscreen()) diff --git a/devel/prepare-save.lua b/devel/prepare-save.lua deleted file mode 100644 index eb3d33671b..0000000000 --- a/devel/prepare-save.lua +++ /dev/null @@ -1,100 +0,0 @@ --- Prepare the current save for devel/find-offsets ---[====[ - -devel/prepare-save -================== - -.. warning:: - - THIS SCRIPT IS STRICTLY FOR DFHACK DEVELOPERS. - -This script prepares the current savegame to be used -with `devel/find-offsets`. It CHANGES THE GAME STATE -to predefined values, and initiates an immediate -`quicksave`, thus PERMANENTLY MODIFYING the save. - -]====] - -local utils = require 'utils' - -df.global.pause_state = true - -print[[ -WARNING: THIS SCRIPT IS STRICTLY FOR DFHACK DEVELOPERS. - -This script prepares the current savegame to be used -with devel/find-offsets. It CHANGES THE GAME STATE -to predefined values, and initiates an immediate -quicksave, thus PERMANENTLY MODIFYING the save. -]] - -if not utils.prompt_yes_no('Proceed?') then - return -end - ---[[print('Placing anchor...') - -do - local wp = df.global.ui.waypoints - - for _,pt in ipairs(wp.points) do - if pt.name == 'dfhack_anchor' then - print('Already placed.') - goto found - end - end - - local x,y,z = pos2xyz(df.global.cursor) - - if not x then - error("Place cursor at your preferred anchor point.") - end - - local id = wp.next_point_id - wp.next_point_id = id + 1 - - wp.points:insert('#',{ - new = true, id = id, name = 'dfhack_anchor', - comment=(x..','..y..','..z), - tile = string.byte('!'), fg_color = COLOR_LIGHTRED, bg_color = COLOR_BLUE, - pos = xyz2pos(x,y,z) - }) - -::found:: -end]] - -print('Nicknaming units...') - -for i,unit in ipairs(df.global.world.units.active) do - dfhack.units.setNickname(unit, i..':'..unit.id) -end - -print('Setting weather...') - -local wbytes = { - 2, 1, 0, 2, 0, - 1, 2, 1, 0, 0, - 2, 0, 2, 1, 2, - 1, 2, 0, 1, 1, - 2, 0, 1, 0, 2 -} - -for i=0,4 do - for j = 0,4 do - df.global.current_weather[i][j] = (wbytes[i*5+j+1] or 2) - end -end - -local yearstr = df.global.cur_year..','..df.global.cur_year_tick - -print('Cur year and tick: '..yearstr) - -dfhack.persistent.save{ - key='prepare-save/cur_year', - value=yearstr, - ints={df.global.cur_year, df.global.cur_year_tick} -} - --- Save - -dfhack.run_script('quicksave') diff --git a/devel/print-event.lua b/devel/print-event.lua index f2d9a54aad..771ac55299 100644 --- a/devel/print-event.lua +++ b/devel/print-event.lua @@ -34,7 +34,7 @@ end local function print_event(event) local str = df.new("string") local ctx = df.history_event_context:new() - event:getSentence(str, ctx) + event:getSentence(str, ctx, true, false) ctx:delete() print(str.value) str:delete() diff --git a/devel/query.lua b/devel/query.lua index 4348f104bb..610003873c 100644 --- a/devel/query.lua +++ b/devel/query.lua @@ -2,7 +2,8 @@ -- Written by Josh Cooper(cppcooper) on 2017-12-21, last modified: 2021-06-13 -- Version: 3.2 --luacheck:skip-entirely -local utils=require('utils') +local guidm = require('gui.dwarfmode') +local utils = require('utils') local validArgs = utils.invert({ 'help', @@ -50,176 +51,6 @@ local tiley = nil local bToggle = true local bool_flags = {} -local help = [====[ - -devel/query -=========== -Query is a script useful for finding and reading values of data structure -fields. Purposes will likely be exclusive to writing lua script code, -possibly C++. - -This script takes your data selection eg.{table,unit,item,tile,etc.} then -recursively iterates through it, outputting names and values of what it finds. - -As it iterates you can have it do other things, like search for a specific -structure pattern (see lua patterns) or set the value of fields matching the -selection and any search pattern specified. - -.. Note:: - - This is a recursive search function. The data structures are also recursive. - So there are a few things that must be considered (in order): - - - Is the search depth too high? (Default: 7) - - Is the data capable of being iterated, or does it only have a value? - - How can the data be iterated? - - Is the iteration count for the data too high? (Default: 257) - - Does the user want to exclude the data's type? - - Is the data recursively indexing (eg. A.B.C.A.*)? - - Does the data match the search pattern? - -.. Warning:: - - This is a recursive script that's primary use is to search recursive data - structures. You can, fairly easily, cause an infinite loop. You can even - more easily run a query that simply requires an inordinate amount of time - to complete. - -.. Tip:: - - Should the need arise, you can kill the command from another shell with - `kill-lua`, e.g. by running it with `dfhack-run` from another terminal. - -Usage examples:: - - devel/query -unit -getfield id - devel/query -unit -search STRENGTH - devel/query -unit -search physical_attrs -maxdepth 2 - devel/query -tile -search dig - devel/query -tile -search "occup.*carv" - devel/query -table df -maxdepth 2 - devel/query -table df -maxdepth 2 -excludekinds s -excludetypes fsu -oneline - devel/query -table df.profession -findvalue FISH - devel/query -table df.global.ui.main -maxdepth 0 - devel/query -table df.global.ui.main -maxdepth 0 -oneline - devel/query -table df.global.ui.main -maxdepth 0 -1 - -**Selection options:** - -``-tile`` - Selects the highlighted tile's block, and then - uses the tile's local position to index the 2D data. - -``-block`` - Selects the highlighted tile's block. - -``-unit`` - Selects the highlighted unit - -``-item`` - Selects the highlighted item. - -``-plant`` - Selects the highlighted plant. - -``-building`` - Selects the highlighted building. - -``-job`` - Selects the highlighted job. - -``-script