Bay 12 Games Forum

Please login or register.

Login with username, password and session length
Advanced search  

Author Topic: dfhack script to show citizens' current (encumbered) speeds  (Read 4366 times)

Bo-Rufus CMVII

  • Bay Watcher
    • View Profile
dfhack script to show citizens' current (encumbered) speeds
« on: February 27, 2014, 08:23:35 pm »

Edit 18-March-2014: Another substantial revision.  New screenshots, description, and code are below.
  • Added columns to show how much overloaded each dorf is (including or excluding anything they're currently hauling.)
  • Added support for sorting by any column.

Default output (sorts by descending speed):


Notice the column headers above, and the key below.  Most zeros are left out to reduce clutter.

Red numbers in the 'E' column indicate encumbrance; green numbers indicate spare capacity.

The 'E-H' colum is the same thing, except calculations are made without the current weight of hauling jobs.

Pen/Pasture Large Animal is another common cause of sloth.  Others are still unexplained.  (E.g., sixth from bottom.)

Here is the help output:


And here is the output sorted by the E-H column (how much encumbered or free if not hauling anything):


I think I'm done with it now, except for bug fixes.

Put it in hack/scripts/

Tested with dfhack r3 on Windows and Linux.
Should work with r4 as well, but I have not tested it.
  • Let us know what happens if you try it.

Post a reply if you spot a bug or numbers that look suspicious.

Some of the characters in the names may not display correctly on Linux.

show-speeds.lua -
Code: [Select]
-- Shows current speed and encumbrance for each citizen.
--
local Version = 2.1
--    Fixed to work with either dfhack r3 or r4.
--    Added columns for excess weight and excess weight without hauling.
--    Made sortable by any column.
--    Trimmed width of text columns.
--    Changed display color scheme.
--
-- Version 2.0
--    Completely reworked, to provide information that (hopefully) explains low speeds.
--
-- Thanks to Putnum, Warmist, and Meph for help and encouragement, and to the writers
-- of all the scripts, .cpp, and .xml that I looked at or copied from.


-- Change these if necessary to improve contrast:
-- (My Windows and Linux machines don't use the same background!)
local overloadColor    = COLOR_RED
local underloadColor   = COLOR_GREEN
local informationColor = COLOR_DARKGREY
-- Make these match any changes above:
local overloadColorName  = 'Red'
local underloadColorName = 'Green'

local args  = {...}
local dorfs = {}

local showHelp         = false
local showVersion      = false

local sortReversed     = false
local itemizeOther     = false

local sortBySpeed      = true -- default
local sortByHauling    = false
local sortByWeapon     = false
local sortByArmor      = false
local sortByOther      = false
local sortByTotal      = false
local sortByMaximum    = false
local sortByExcess     = false
local sortByDifference = false

local sortByName       = false
local sortByProfession = false
local sortByJob        = false


-- sanity checks:
if not dfhack.isMapLoaded() then
    qerror('You must load a map to use this.')
end

-- process arguments:
if #args > 0 then
    for a = 1, #args, 1 do
        args[a] = args[a]:lower()

        if args[a] == 'help' then
            showHelp = true
        elseif args[a] == '-v' then
            showVersion = true

        elseif args[a] == '-r' then
            sortReversed = true
        elseif args[a] == '-i' then
            itemizeOther = true

        elseif args[a] == 's' then -- not needed; provided for completeness
            sortBySpeed = true
        elseif args[a] == 'h' then
            sortByHauling = true
        elseif args[a] == 'w' then
            sortByWeapon = true
        elseif args[a] == 'a' then
            sortByArmor = true
        elseif args[a] == 'o' then
            sortByOther = true
        elseif args[a] == 't' then
            sortByTotal = true
        elseif args[a] == 'm' then
            sortByMaximum = true
        elseif args[a] == 'e' then
            sortByExcess = true
        elseif args[a] == '-' then
            sortByDifference = true

        elseif args[a] == 'j' then
            sortByJob = true
        elseif args[a] == 'n' then
            sortByName = true
        elseif args[a] == 'p' then
            sortByProfession = true

        else
            print('Invalid argument: ', args[a])
            return
        end
    end
end

-- show help and exit:
if showHelp then
    print()
    print('Shows current speed, encumbrance, name, profession, and job for all citizens.')
    print()
    print('Usage:')
    print()
    print('   show-speeds            lists citizens sorted by speed (high to low)')
    print()
    print('   show-speeds X          if X is a column label, sorts by the indicated column')
    print('                           o use minus sign (show-speeds -) for the E-H column')
    print('                           o use the first letter (only) for the text columns')
    print()
    print('   show-speeds -r         reverses sort order')
    print('                           o compatible with any numeric column argument')
    print()
    print('   show-speeds -i         itemize gross weights of any worn containers')
    print('                           o compatible with any column argument')
    print()
    print('   show-speeds help       show this help text and exit')
    print('   show-speeds -v         show script version number and exit')
    print()
    print('Arguments are not case sensitive.')
    print()
    print('Notes:')
    print(' o Pen/Pasture Large Animal can have either much or little effect, apparently')
    print('   depending on the animal.  Animals being led do not show up in the weights.')
    print(' o Store Item will not show a weight hauled if en route to pick up the item.')
    print(' o If you load a game and run the script before unpausing, most of the')
    print('   information needed by the script will not be available.  Unpause the')
    print('   game for at least a few seconds before using the script.')
    print(' o Some low speeds appear to be transient.  If no explanation for a low speed')
    print('   can be determined from the output, unpause the game for a few seconds and')
    print('   then run the script again.')
    print()
    return
end

-- show version and exit
if showVersion then
    print()
    print('   show-speeds version',Version)
    print()
    return
end


-- *** supporting functions ***

-- comparator for sorting units by speed:
function compSpeeds(a,b)
    if sortReversed then -- naughty me!
        return (a.speed < b.speed)
    else
        return (a.speed > b.speed)
    end
end

-- comparator for sorting by hauling weight:
function compHauling(a,b)
    if sortReversed then
        return (a.haulingWeight < b.haulingWeight)
    else
        return (a.haulingWeight > b.haulingWeight)
    end
end

-- comparator for sorting by weapon weight:
function compWeapons(a,b)
    if sortReversed then
        return (a.weaponWeight < b.weaponWeight)
    else
        return (a.weaponWeight > b.weaponWeight)
    end
end

-- comparator for sorting by armorWeight:
function compArmor(a,b)
    if sortReversed then
        return (a.armorWeight < b.armorWeight)
    else
        return (a.armorWeight > b.armorWeight)
    end
end

-- comparator for sorting by other item weight:
function compOther(a,b)
    if sortReversed then
        return (a.otherWeight < b.otherWeight)
    else
        return (a.otherWeight > b.otherWeight)
    end
end

-- comparator for sorting by total load:
function compTotals(a,b)
    if sortReversed then
        return (a.totalWeight < b.totalWeight)
    else
        return (a.totalWeight > b.totalWeight)
    end
end

-- comparator for sorting by maximum unencumbered load:
function compMaximums(a,b)
    if sortReversed then
        return (a.loadCapacity < b.loadCapacity)
    else
        return (a.loadCapacity > b.loadCapacity)
    end
end

-- comparator for sorting by excess load:
function compExcessWeight(a,b)
    if sortReversed then
        return (a.excessWeight < b.excessWeight)
    else
        return (a.excessWeight > b.excessWeight)
    end
end

-- comparator for sorting by excess load, ignoring hauling load:
function compModifiedExcessWeight(a,b)
    if sortReversed then
        return (a.excessWeightWithoutHauling < b.excessWeightWithoutHauling)
    else
        return (a.excessWeightWithoutHauling > b.excessWeightWithoutHauling)
    end
end

-- comparator for sorting units by name:
function compNames(a,b)
    return a.name < b.name
end

-- comparator for sorting units by profession:
function compProfessions(a,b)
    return a.profession < b.profession
end

-- comparator for sorting units by job:
function compJobs(a,b)
    if a.job == nil then
        return false
    elseif b.job == nil then
        return true
    end
    return a.job_name < b.job_name
end

-- round a positive number to an integer:
function round(n)
    return math.floor(n+0.5)
end

-- dfhack.print a string truncated or padded to a specified field width;
-- if the string is too long, end with a slash to indicate the truncation
function printStringInField(s,w)
    local sLength = s:len()
    if sLength > w then
        dfhack.print(s:sub(1,w-1)..'/')
    else
        dfhack.print(s)
        for i = w - sLength-1, 0, -1 do
            dfhack.print(' ')
        end
    end
end

-- dfhack.print a positive number rounded to an integer and right-justified in a 3-wide field;
-- leave field blank if number is zero, or print a dot if it is greater than zero but rounds to zero
function printNumber(n)
    local rounded
    if n == 0 then
        dfhack.print('   ')
    else
        rounded = round(n)
        if rounded == 0 then
            dfhack.print('  .') -- indicates fractional value rounded to zero
        else
            if rounded < 100 then
                dfhack.print(' ')
            end
            if rounded < 10 then
                dfhack.print(' ')
            end
            dfhack.print(rounded)
        end
    end
end

-- is the unit hiding a curse?
-- from isHidingCurse in Units.cpp, which is not exported for lua
function isHidingCurse(v)
    if not v.job.hunt_target then -- presumably not "hiding curse" while hunting
        local identity = dfhack.units.getIdentity(v) -- finds assumed identity
        if identity then
            if identity.unk_4c == 0 then
                return true
            end
        end
    end
    return false
end

-- get a unit's strength:
-- from getPhysicalAttrValue in Units.cpp, which is not exported for lua
function getStrength(v)
    local attributeObject = v.body.physical_attrs[df.physical_attribute_type.STRENGTH]
    local strength = attributeObject.value - attributeObject.soft_demotion
    local modifier = v.curse.attr_change
    local modifiedValue
    if modifier then
        modifiedValue = strength * modifier.phys_att_perc[df.physical_attribute_type.STRENGTH] / 100
        modifiedValue = modifiedValue + modifier.phys_att_add[df.physical_attribute_type.STRENGTH]
        if isHidingCurse(v) then
            strength = math.min(strength,modifiedValue)
        else
            strength = modifiedValue
        end
    end
    return math.max(0,strength)
end

-- get a unit's size:
function getSize(v)
    local bodySize
    if dfhack.VERSION == "0.34.11-r3" then
        bodySize = v.body.physical_attr_tissues.STRENGTH -- *** I think this is actually .size_info.size_cur ***
    else                                                 -- *** for now, assume r4 ***
        bodySize = v.body.size_info.size_cur
    end
    return bodySize
end

-- *** end supporting functions ***



-- build a list of citizens and their relevant properties:
for _,v in ipairs(df.global.world.units.active) do

    if dfhack.units.isCitizen(v) then

        local d = {}

        -- look up some stuff:
        d.name = dfhack.TranslateName(dfhack.units.getVisibleName(v))
        d.profession = dfhack.units.getProfessionName(v)
        d.job = v.job.current_job
        if d.job == nil then
            -- do nothing (the negation or the test doesn't work on Windows)
        else
            d.jobtype = v.job.current_job.job_type
            d.job_name = df.job_type.attrs[d.jobtype].caption -- *** substitute acronyms for long job names ***
        end

        d.speed = round(100000/dfhack.units.computeMovementSpeed(v))

        -- calculate unencumbered load capacity:   (logic from computeMovementSpeed in Units.cpp)
        d.size = getSize(v)
        d.strength = getStrength(v)
        d.loadCapacity = math.max(1, d.size/10 + d.strength*3)
        d.loadCapacity = d.loadCapacity/100 -- since my weights are 1/100 of the Units.cpp logic

        -- calculate armor-user skill multiplier:   (logic from calcInventoryWeight in Units.cpp)
        d.armorSkill = math.min(15,dfhack.units.getEffectiveSkill(v,df.job_skill.ARMOR))
        d.armorMultiplier = 15 - d.armorSkill

        -- process the inventory:

        d.haulingWeight = 0
        d.otherWeight   = 0
        d.weaponWeight  = 0
        d.armorWeight   = 0
        d.otherItemType = -1
        d.flask         = false
        d.backpack      = false
        d.quiver        = false
        d.mysteryItem   = false
        d.mysteryItemWeight = 0

        for i = 0, #v.inventory-1, 1 do

            -- find the item weight in dorf-pounds
            if not v.inventory[i].item.flags['weight_computed'] then
                v.inventory[i].item:calculateWeight()
            end
            itemWeight = v.inventory[i].item.weight + v.inventory[i].item.weight_fraction / 1000000
   
            -- break the inventory weights into categories:
            if v.inventory[i].mode == 0 then -- hauled item (see df.units.xml) -- *** figure out how to replace the hard-coded 0 with the enumerated mode name ***
                d.haulingWeight = d.haulingWeight + itemWeight
            elseif v.inventory[i].item:isWeapon() then
                d.weaponWeight = d.weaponWeight + itemWeight
            elseif v.inventory[i].item:isArmor() then
                if d.armorSkill > 1 then
                    itemWeight = itemWeight * d.armorMultiplier / 16 -- per calcInventoryWeight in Units.cpp
                end
                d.armorWeight = d.armorWeight + itemWeight
            else
                d.otherWeight = d.otherWeight + itemWeight
                -- break out the items for later, if the itemize option is was used
                if itemizeOther then
                    otherItemType = v.inventory[i].item:getType()
                    if otherItemType == 11 then -- *** fix the hard-coding ***
                        d.flask = true
                        d.flaskWeight = itemWeight
                    elseif otherItemType == 60 then
                        d.pack = true
                        d.packWeight = itemWeight
                    elseif otherItemType == 61 then
                        d.quiver = true
                        d.quiverWeight = itemWeight
                    else -- no other types currently itemized
                        d.mysteryItem = true
                        d.mysteryItemWeight = d.mysteryItemWeight + itemWeight -- possibly more than one
                    end
                end
            end
        end

        -- analyze the load:
        d.totalWeight = d.weaponWeight+d.armorWeight+d.haulingWeight+d.otherWeight
        d.excessWeight = d.totalWeight - d.loadCapacity
        d.overloaded = (d.excessWeight > 0)
        d.excessWeightWithoutHauling = d.excessWeight - d.haulingWeight

        -- store for sorting:
        table.insert(dorfs,d)

    end
end

-- sort the list according to any argument:
if sortByHauling then
    table.sort(dorfs,compHauling)
elseif sortByWeapon then
    table.sort(dorfs,compWeapons)
elseif sortByArmor then
    table.sort(dorfs,compArmor)
elseif sortByOther then
    table.sort(dorfs,compOther)
elseif sortByTotal then
    table.sort(dorfs,compTotals)
elseif sortByMaximum then
    table.sort(dorfs,compMaximums)
elseif sortByExcess then
    table.sort(dorfs,compExcessWeight)
elseif sortByDifference then
    table.sort(dorfs,compModifiedExcessWeight)
-- text fields:
elseif sortByName then
    table.sort(dorfs,compNames)
elseif sortByProfession then
    table.sort(dorfs,compProfessions)
elseif sortByJob then
    table.sort(dorfs,compJobs)
-- default to speed:
else
    table.sort(dorfs,compSpeeds)
end


-- show the list:
print()
dfhack.color(informationColor)
print(" S   H   W   A   O   T   M   E  E-H Name           Profession     Job")
dfhack.color(nil)

-- output loop:
for _,d in ipairs(dorfs) do

    -- speed:
    printNumber(d.speed)

    -- weight hauled:
    dfhack.print(' ')
    printNumber(d.haulingWeight)

    -- weight of weapons
    dfhack.print(' ')
    printNumber(d.weaponWeight)

    -- weight of armor
    dfhack.print(' ')
    printNumber(d.armorWeight)

    -- weight of everything else
    dfhack.print(' ')
    printNumber(d.otherWeight)

    -- total weight equipped/carried:
    dfhack.print(' ')
    printNumber(d.totalWeight)

    -- unencumbered capacity:
    dfhack.print(' ')
    printNumber(d.loadCapacity)

   -- excessWeight load:
    dfhack.print(' ')
    if d.overloaded then
        dfhack.color(overloadColor)
        printNumber(d.excessWeight)
    else
        dfhack.color(underloadColor)
        printNumber(-d.excessWeight)
    end
    dfhack.color(nil)

    -- excess load without any hauling:
    dfhack.print(' ')
    if d.excessWeightWithoutHauling > 0 then
        dfhack.color(overloadColor)
        printNumber(d.excessWeightWithoutHauling)
    else
        dfhack.color(underloadColor)
        printNumber(-d.excessWeightWithoutHauling)
    end
    dfhack.color(nil)

    -- name:
    dfhack.print(' ')
    printStringInField(d.name,13)

    -- profession
    dfhack.print('  ')
    printStringInField(d.profession,13)

    -- current job:
    dfhack.print('  ')
    if d.job == nil then
        print(' -')
    else
        printStringInField(d.job_name,13)
        print()
   end

    -- itemized containers:
    if itemizeOther then
        dfhack.color(informationColor)
        if d.flask then
            dfhack.print('                ')
            printNumber(d.flaskWeight)
            print(' flask')
        end
        if d.pack then
            dfhack.print('                ')
            printNumber(d.packWeight)
            print(' backpack')
        end
        if d.quiver then
            dfhack.print('                ')
            printNumber(d.quiverWeight)
            print(' quiver')
        end
        if d.mysteryItem then
            dfhack.print('                ')
            printNumber(d.mysteryItemWeight)
            print(' other items')
        end
        dfhack.color(nil)
    end

end -- main loop

print()
dfhack.color(informationColor)
print('S : Current relative speed (nominal average is 100)')
print('H : Weight of any hauled items')
print('W : Weight of any weapons')
print('A : Weight of any armor and clothes (possibly reduced by armor-wearer skill)')
print('O : Weight of other items carried or worn (quivers, backpacks, flasks, etc.)')
print('T : Total weight carried or worn')
print('M : Maximum allowable weight without encumbrance')
--
dfhack.print('E : ')
dfhack.color(overloadColor)
dfhack.print(overloadColorName..':')
dfhack.color(informationColor)
dfhack.print(' Encumbering Weight (T-M); ')
dfhack.color(underloadColor)
dfhack.print(underloadColorName..':')
dfhack.color(informationColor)
print(' Spare Capacity (M-T)')
--
print('E-H : As E, but without current hauling')
print()
print('Notes:')
print(' o Rounding may prevent numbers from appearing to add up correctly.')
print(' o Period in numeric column indicates non-zero weight rounded to zero.')
print()
dfhack.print('Use ')
dfhack.color(nil)
dfhack.print('show-speeds help')
dfhack.color(informationColor)
print(' for more information.')
dfhack.color(nil)
print() -- otherwise color does not reset before the next dfhack prompt
« Last Edit: March 18, 2014, 03:33:49 am by Bo-Rufus CMVII »
Logged

Putnam

  • Bay Watcher
  • DAT WIZARD
    • View Profile
Re: dfhack script to show citizens' current (encumbered) speeds
« Reply #1 on: February 28, 2014, 10:56:13 am »

The variable "name" only shows up in for block near the bottom--I think it'd probably be faster to declare it right there, if my understanding of the speed of getting local variables is correct.

Bo-Rufus CMVII

  • Bay Watcher
    • View Profile
Re: dfhack script to show citizens' current (encumbered) speeds
« Reply #2 on: February 28, 2014, 07:57:44 pm »

Yeah, I know exactly diddly about lua.  I guess I ought to read up on scope and such.
Logged

fricy

  • Bay Watcher
  • [DFHACK:ZEALOT]
    • View Profile
Re: dfhack script to show citizens' current (encumbered) speeds
« Reply #3 on: March 15, 2014, 06:12:13 am »

Tested on mac: with dfhack-r3 it works correctly, but with the unoffical r4 I get the following error:

Spoiler (click to show/hide)

Bo-Rufus CMVII

  • Bay Watcher
    • View Profile
Re: dfhack script to show citizens' current (encumbered) speeds
« Reply #4 on: March 15, 2014, 12:25:43 pm »

Thanks.  I think that's due to the renaming of some fields in the data.

If you want to experiment, try changing line 269 from:

Code: [Select]
    bodySize = v.body.physical_attr_tissues.STRENGTH
to:

Code: [Select]
    bodySize = v.body.size_info.size_cur
Logged

Bo-Rufus CMVII

  • Bay Watcher
    • View Profile
Re: dfhack script to show citizens' current (encumbered) speeds
« Reply #5 on: March 18, 2014, 03:35:00 am »

I just posted a very substantial update, with tools to help you analyze encumbrance.

Screenshots and code are at the first post.
Logged

fricy

  • Bay Watcher
  • [DFHACK:ZEALOT]
    • View Profile
Re: dfhack script to show citizens' current (encumbered) speeds
« Reply #6 on: March 31, 2014, 12:30:26 pm »

I just posted a very substantial update, with tools to help you analyze encumbrance.

Screenshots and code are at the first post.

Tested on mac, works with r3 and r4. Thx!