this is a second prototype, with a much more complex system and some other improvements. (the gist of what this about: making feeding your fort (much) harder and more interesting by giving food different properties and making units eat more.)
not only item types but materials are now considered. item types have a base value, materials are composed of different types of nutrition (fat, protein, complex carbohydrates, sugar, fiber, water, alcohol).
each of these types modfiy the base value and have different effects on the unit's hunger, thirst, stomach content/food and body fat values. for example, a PLANT_GROWTH of base value 8000 that is composed soleley of fiber will reduce hunger by about half that amount, but actually burn fat, and put a lot of useless material in the stomach. an egg with the same base value will decrease hunger by about 6000, while taking up less space in the stomach(but adding more nutrition to it). a sugary treat of the same size will cause the unit to put on some extra weight, and put enormous amounts of nutrition into the stomach, while taking up only little room.
materials are ordered into groups tied to several plant/creature tokens, such that a group of species shares nutrition values for their materials. example:
using this system, no raw modfification at all is necessary, all the data is read from the table. in addition there is a set of default values for common material tokens (LEAF, FRUIT, MUSCLE, BRAIN, DRINK, ...) so this will work with pretty much any set of raws.
no longer causes dwarves to eat more often, instead units will eat several items in a row until their hunger threshold is reached. to facilitate that, the hunger/thirst counters are stored and only written into memory after several jobs were completed.
immoderation has a strong effect on the unit's subjective feeling of hunger. dwarves will eat up to 33% less or 88% more than their hunger would dictate. this, together with the above changes, will lead dwarves to go on massive binges, some more so than others. immoderate dwarves will grow fat and lardy (this requires further testing tho).
this doesnt quite work so well yet with drinks, mostly because the alcohol syndrome isnt made for it (and the base value for drinks is fairly low, a beer gives about 1/5th of the normal value). dwarves will frequently drink until theyre unconscious, and then immediately drink again when they wake up.
- in future versions, there will be more counters that need to be stored and updated, like vitamins, etc. currently im using persist-storage to track jobs, but i am not sure it will be fast enough to update ~200 units and their counters every tick?
- is editing syndrome severity in memory feasible? do alcohol effects from multiple sources stack? i could base the alcohol effects on the alcohol content of the drink directly, if i edit the syndome after it is applied. this way there is no need for multiple alcohol templates, and it is more granual.
- stomach_content doesnt seem to do much, it just goes up to 300000 but doesnt seem to stop dwarves from eating. its conceivable that eating a lot of fibrous stuff without any nuttrition actually is dangerous.
-- introduces nutrition values to drinks/foods & their materials. increases food and drink consumption greatly(fort mode)
---------
local eventful = require 'plugins.eventful'
local persistTable = require 'persist-table'
-- we have a default value for each material token that might be edible, basically
-- this table will contain the data from matLookup_init in the form of REALM_TOKEN:SPECIES_TOKEN:MAT_TOKEN
-- (CREATURE:DWARF:SKIN, PLANT:PLUMP_HELMET:STRUCTURAL)
-- each material table may default to a material from the same table by using a "default = <string>" entry
-- for example, if you look up the material "JUICE" in the category "grains_normal",
-- it will default to the values for "SEED"
-- materials from unspecified creatures or plants will use the defaults below, as will known creatures/plants
-- that lack a "default" entry, when an unspecified material is requested
local matLookup = {
PLANT = {
default = { -- this stuff will be average to terrible, but gets rarely used, if ever
members = {},
materials = {
STRUCTURAL = {.05, .1, .15, .05, .4, .25, 0},
LEAF = {.05, .1, .15, .05, .4, .25, 0},
FLOWER = {0, 0, .1, 0, .6, .3, 0},
SEED = {.1, .05, .1, 0, .6, .2, 0},
FRUIT = {0, 0, .3, .1, .3, .3, 0},
BERRY = {0, 0, .3, .1, .3, .3, 0},
BUD = {0, 0, .3, .1, .3, .3, 0},
POD = {0, .2, .2, 0, .3, .3, 0},
MUSHROOM = {0, .05, .25, 0, .3, .4, 0},
POWDER = {0, .15, .45, 0, .3, 0, 0},
PRESS = {.1, .05, .2, 0, .6, .05, 0},
PASTE = {.25, .1, .25, 0, .15, .25, 0},
EXTRACT = {.1, .05, .2, .1, .05, .5, 0},
JUICE = {.1, .05, .2, .1, .05, .5, 0},
DRINK = {0, .0, .1, .15, 0, .65, .1},
OIL = {.9, 0, 0, 0, .1, 0, 0},
MILK = {.05, .4, 0, .05, 0, .5, 0},
CHEESE = {.1, .6, .1, 0, 0, .2, 0},
SYRUP = {0, 0, 0, .8, 0, .2, 0},
SUGAR = {0, 0, .05, .9, 0.5, 0, 0},
default = 'STRUCTURAL',
},
},
grains_normal = { -- high carb, little protein, some fiber
members = {'SINGLE-GRAIN_WHEAT', 'TWO-GRAIN_WHEAT', 'SOFT_WHEAT', 'HARD_WHEAT', 'SPELT',
'BARLEY', 'BUCKWHEAT', 'OATS', 'RYE'},
materials = {
SEED = {0, .1, .65, 0, .2, .05, 0},
MILL = {0, .15, .75, 0, .1, 0, 0},
DRINK = {0, 0, .1, .1, 0, .7, .1},
default = 'SEED',
},
},
grains_leaves = { -- grain more fiber than normal, leaves also high fiber
members = {'PENDANT_AMARANTH', 'BLOOD_AMARANTH', 'PURPLE_AMARANTH', 'RED_SPINACH',
'ELEPHANT-HEAD_AMARANTH', 'QUINOA', 'KANIWA'},
materials = {
SEED = {0, .1, .5, 0, .35, .05, 0},
MILL = {0, .15, .65, 0, .2, 0, 0},
DRINK = {0, 0, .15, .05, 0, .75, .05},
FRESH_LEAF = {.05, .1, 0, .1, .4, .35, 0},
default = 'SEED',
},
},
grains_small = { -- grain has more protein, but more fiber
members = {'PEARL_MILLET', 'WHITE_MILLET', 'FINGER_MILLET', 'FOXTAIL_MILLET', 'FONIO',
'TEFF', 'SORGHUM'},
materials = {
SEED = {0, .3, .3, 0, .4, 0, 0}, -- lots of minerals
MILL = {0, .35, .4, 0, .25, 0, 0},
DRINK = {0, .05, .1, .1, 0, .7, .05},
default = 'SEED',
},
},
grains_sweet = { -- contains sugar too
members = {'MAIZE', 'GRASS_LONGLAND', 'VINE_WHIP'},
materials = {
SEED = {.1, .2, .2, .3, .1, .1, 0},
MILL = {.15, .25, .25, .35, 0, 0, 0},
DRINK = {0, 0, .05, .25, 0, .6, 0.15},
default = 'SEED',
},
},
nuts_fatty = {
members = {'HEMP', 'FLAX', 'COTTON', 'KENAF', 'NUTGRASS', 'PEANUT', 'PINE', 'KAPOK', 'CANDLENUT',
'PARADISE_NUT', 'HAZEL', 'PECAN', 'MACADAMIA', 'WALNUT', 'CASHEW', 'OLIVE'},
materials = {
FRUIT = {.5, .1, .1, 0, .1, .2, 0}, -- olive
NUT = {.5, .1, .25, 0, .1, .05, 0},
SEED = {.5, .1, .25, 0, .1, .05, 0},
PASTE = {.5, .1, .25, 0, .1, .05, 0},
OIL = {.9, .1, 0, 0, 0, 0, 0},
PRESS = {.1, .05, .5, 0, .35, 0, 0},
default = 'SEED',
},
},
nuts_protein = { -- beans are here too
members = {'SOYBEAN', 'CHICKPEA', 'COWPEA', 'LENTIL', 'MUNG_BEAN', 'PEA', 'RED_BEAN', 'URAD_BEAN',
'CHESTNUT', 'CACTUS_MILK'},
materials = {
FRUIT = {.05, .55, .25, 0, .05, .1, 0},
NUT = {.05, .55, .25, 0, .05, .1, 0}, -- trees use this
SEED = {.05, .55, .25, 0, .05, .1, 0},
MILL = {.1, .6, .3, 0, 0, 0, 0},
PASTE = {.05, .55, .25, 0, .5, .1, 0},
PRESS = {0, .1, .05, 0, .85, 0, 0},
MILK = {.05, .4, 0, .05, 0, .5, 0},
CHEESE = {.1, .6, .1, 0, 0, .2, 0},
default = 'SEED',
},
},
nuts_carbo = {
members = {'HAUSA_GROUNDNUT', 'BAMBARA_GROUNDNUT', 'CHICKPEA', 'OAK', 'PALM'},
materials = {
NUT = {.05, .1, .6, .1, .05, .1, 0}, -- trees use this
SEED = {.05, .1, .6, .1, .05, .1, 0},
MILL = {.1, .1, .65, .15, 0, 0, 0},
PASTE = {.05, .1, .6, .1, .05, .1, 0},
default = 'SEED',
},
},
root_stringy = {
members = {'RADISH', 'WILD_CARROT', 'TURNIP', 'PARSNIP', 'ONION'},
materials = {
ROOT = {0, .1, .2, .1, .2, .4, 0},
FRUIT = {0, .1, .2, .1, .2, .4, 0},
-- LEAF = use defaults
default = 'FRUIT',
},
},
root_starchy = {
members = {'RADISH', 'WILD_CARROT', 'LONG_YAM', 'TURNIP', 'PARSNIP', 'ONION', 'SWEET_POTATO',
'POTATO', 'TARO', 'CASSAVA', 'LESSER_YAM', 'PURPLE_YAM', 'WHITE_YAM'},
materials = {
ROOT = {0, .2, .5, 0, 0, .3, 0},
FRUIT = {0, .2, .5, 0, 0, .3, 0},
DRINK = {0, 0, .15, .15, 0, .6, .1},
MILL = {0, .2, .75, 0, .05, 0, 0},
-- LEAF = use defaults
default = 'FRUIT',
},
},
fruit_stringy = {
members = {'EGGPLANT', 'CELERY', 'CHICORY', 'GARDEN_CRESS', 'LEEK', 'LETTUCE', 'SPINACH'},
materials = {
LEAF = {.05, .25, .1, .05, .3, .25, 0},
FRUIT = {.05, .25, .1, .05, .3, .25, 0},
BERRY = {.05, .25, .1, .05, .3, .25, 0}, -- unused
PRESS = {0, .35, .15, 0, .45, .05, 0}, -- unused
EXTRACT = {.15, .05, .1, .15, .05, .5, 0}, -- unused
JUICE = {.15, .05, .1, .15, .05, .5, 0}, -- unused
DRINK = {0.05, .05, .05, .1, .05, .65, .05}, -- unused
default = 'FRUIT',
},
},
fruit_sweet = { -- lots of sugar but also water. could be split into more groups but then need data
members = {'HORNED_MELON', 'MUSKMELON', 'WATERMELON', 'WINTER_MELON', 'PEPPER_SWEET_RED',
'PEPPER_SWEET_GREEN', 'GRAPE', 'CRANBERRY', 'BILBERRY', 'BLUEBERRY', 'BLACKBERRY', 'RASPBERRY',
'TOMATO', 'TOMATILLO', 'BERRIES_STRAW', 'BERRIES_PRICKLE', 'BERRIES_FISHER', 'PINEAPPLE',
'PASSION_FRUIT', 'SAGUARO', 'MANGO', 'CARAMBOLA', 'PEACH', 'PEAR', 'PERSIMMON', 'PLUM',
'APPLE', 'APRICOT', 'BAYBERRY', 'CHERRY', 'POMEGRANATE', 'CUSTARD_APPLE', 'DATE_PALM',
'LYCHEE', 'GUAVA', 'RAMBUTAN', 'SAND_PEAR'},
materials = {
BERRY = {0, 0, .1, .35, .15, .35, .05},
FRUIT = {0, 0, .1, .35, .15, .35, .05},
PRESS = {0, 0, .25, .05, .6, .1, 0},
EXTRACT = {0, 0, .05, .45, 0, .5, .05},
JUICE = {0, 0, .05, .45, 0, .5, .05},
DRINK = {0, 0, 0, .3, 0, .55, .15},
WINE = {0, 0, 0, .3, 0, .55, .15},
default = 'FRUIT',
--LEAF = {}, -- use default
},
},
fruit_other = { -- mixed, some of everything, mostly starch
members = {'SQUASH', 'STRING_BEAN', 'BROAD_BEAN', 'BITTER_MELON', 'CABBAGE', 'CUCUMBER', 'BANANA',
'DURIAN', 'AVOCADO'},
materials = {
POD = {0, .45, .2, 0, .25, .1, 0}, -- beans
LEAF = {.05, .1, .3, .1, .15, .3, 0},
FRUIT = {.05, .1, .3, .1, .15, .3, 0},
BERRY = {.05, .1, .3, .1, .15, .3, 0}, -- unused
PRESS = {.1, .3, .15, 0, .5, .05, 0}, -- unused
EXTRACT = {0, .05, .4, .15, 0, .4, 0}, -- unused
JUICE = {0, .05, .4, .15, 0, .4, 0}, -- unused
DRINK = {0, .05, .2, .15, 0, .5, .1}, -- banana
default = 'FRUIT',
},
},
supersweet = { -- sugar producers and special
members = {'BEET', 'CANE_SUGAR', 'ROOT_MUCK', 'TUBER_BLOATED', 'BERRY_SUN', 'BLOOD_THORN', 'POD_SWEET'},
materials = {
FRUIT = {0, 0, .1, .55, .1, .2, .05},
ROOT = {0, 0, .1, .55, .1, .2, .05},
PRESS = {0, 0, .2, .1, .7, 0, 0},
EXTRACT = {0, 0, .05, .55, 0, .35, .05},
JUICE = {0, 0, .05, .55, 0, .35, .05},
DRINK = {0, 0, 0, .5, 0, .3, .2},
WINE = {0, 0, 0, .5, 0, .3, .2},
SPIRIT = {0, 0, 0, .3, 0, .3, .4},
SUGAR = {0, 0, .05, .9, 0.5, 0, 0},
SYRUP = {0, 0, 0, .8, 0, .15, .05},
default = 'FRUIT',
},
},
},
CREATURE = {
default = { -- this is used for each and every creature unless specified otherwise
members = {},
materials = {
MEAT = {.4, .2, 0, 0, .2, .2, 0},
MUSCLE = {.4, .2, 0, 0, .2, .2, 0},
LIVER = {.5, .15, 0, 0, .1, .25, 0},
GUT = {.5, .15, 0, 0, .1, .25, 0},
STOMACH = {.5, .15, 0, 0, .1, .25, 0},
GIZZARD = {.5, .15, 0, 0, .1, .25, 0},
PANCREAS = {.5, .15, 0, 0, .1, .25, 0},
SPLEEN = {.5, .15, 0, 0, .1, .25, 0},
KIDNEY = {.5, .15, 0, 0, .1, .25, 0},
LUNG = {.5, .15, 0, 0, .1, .25, 0},
HEART = {.3, .3, 0, 0, .2, .2, 0},
EYE = {.3, .4, 0, 0, 0, .3, 0},
BRAIN = {.2, .5, 0, 0, 0, .3, 0}, -- yum
FAT = {.4, .05, 0, 0, .05, .5, 0},
TALLOW = {.8, .05, 0, 0, .05, .1, 0},
BLOOD = {.1, .1, 0, 3, 0, .5, 0},
EGGSHELL = {0, 0, 0, 0, 1, 0, 0},
EGG_WHITE = {0, .5, 0, 0, 0, .5, 0}, -- its actually more like 90% water but that would make eggs bad
EGG_YOLK = {.3, .2, 0, 0, 0, .5, 0},
MILK = {.15, .05, 0, .3, 0, .5, 0}, -- 90% water
CHEESE = {.4, .3, 0, .1, 0, .2, 0},
EXTRACT = {.15, .05, 0, .3, 0, .5, 0},
DRINK = {0, 0, 0, .35, 0, .5, .15},
BONE = {.1, .1, 0, 0, .7, .1, 0},
CARTILAGE = {.1, .1, 0, 0, .7, .1, 0},
SINEW = {.1, .1, 0, 0, .7, .1, 0},
SKIN = {.1, .1, 0, 0, .7, .1, 0},
SCALE = {.1, .1, 0, 0, .7, .1, 0},
NERVE = {.1, .1, 0, 0, .7, .1, 0},
SPONGE = {.1, .1, 0, 0, .7, .1, 0},
default = 'MEAT',
},
},
brains = { -- everyone likes brains
members = {'DWARF'},
materials = {
MEAT = {.4, .2, .05, 0, .05, .3, 0},
LIVER = {.7, .1, 0, 0, 0, .1, .1},
BRAIN = {.1, .8, 0, 0, 0, .1, 0},
default = 'MEAT', -- all dwarf materials will be nutritious
},
},
gremlins = {
members = {'GREMLIN'},
materials = {
TEARS = {0, 0, 0, .3, 0, .2, .5},
},
},
milk_special = { -- suppose they got fattier milk
members = {'YAK', 'MUSKOX'},
materials = {
MILK = {.3, .2, 0, .1, 0, .4, 0},
CHEESE = {.4, .3, 0, .2, 0, .1, 0},
},
},
},
}
-- here we install an entry for each plant/creature in the realm table, using the data from the groups.
-- metamethods are used to redirect to the original groups' material table as well as for default values
-- "realm" being either 'CREATURE', or 'PLANT'
local function initMatLookupTable(realm)
local temp = {}
for group_id, group in pairs(matLookup[realm]) do
if group.members then -- if its not a newly added item
local fallback = group.materials.default
if fallback then
setmetatable(group.materials, {
__index = function(t, k)
return group.materials[fallback]
end,
})
else
setmetatable(group.materials, {
__index = function(t, k)
return matLookup[realm].default.materials[k]
end,
})
end
for _, k in pairs(group.members) do
temp[k] = {
materials = group.materials,
group_id = group_id,
}
end
end
end
for k, v in pairs(temp) do
matLookup[realm][k] = v
end
local fallback = matLookup[realm].default
setmetatable(matLookup[realm], {
__index = function(t, k)
return fallback
end,
})
setmetatable(fallback.materials, {
__index = function(t, k)
return fallback.materials[fallback.materials.default]
end,
})
end
initMatLookupTable('PLANT')
initMatLookupTable('CREATURE')
-- foods are composed of varying amounts of these primary constituents, with different impact on various counters.
-- counters are, in that order:
-- hunger_timer, stomach_content, stomach_food, stored_fat, thirst_timer
local nutVectors = {
[1] = {1.1, 1.5, 1.3, 0.1, 0}, -- fat
[2] = {1.4, .7, 1.2, 0, 0}, -- protein
[3] = {1, 1.2, .8, -0.1, -0.1}, -- complex carbohyrates
[4] = {.5, .1, 2, 0.2, 0}, -- sugar
[5] = {.5, 2, .1, -0.2, -0.2}, -- fiber
[6] = {.1, 1, 0, 0, 1}, -- water
[7] = {.2, .1, 1, 0.1, 2}, --alcohol
}
-- base nutrition values by item type
local baseValues = {}
local fooValues = {
MEAT = 12000,
FISH = 12000,
FISH_RAW = 10000,
CHEESE = 14000,
PLANT = 8000,
PLANT_GROWTH = 8000,
SEEDS = 4000,
GLOB = 8000,
LIQUID_MISC = 8000,
POWDER_MISC = 8000,
EGG = 8000,
VERMIN = 5000,
REMAINS = 5000,
DRINK = 12000,
FOOD = 16000,
}
for k, v in pairs(fooValues) do
baseValues[df.item_type[k]] = v
end
setmetatable(baseValues, {
__index = function(t, k)
print("unknown item type: ", k)
return 8000
end,
})
local JOBTYPES = {}
local EATJOB_TYPE = df.job_type['Eat']
local DRINKBOOZE_TYPE = df.job_type['Drink2']
--local DRINKWATER_TYPE = df.job_type['Drink']
local GIVEFOOD_TYPE = df.job_type['GiveFood']
local GIVEFOOD2_TYPE = df.job_type['GiveFood2']
--local GIVEWATER_TYPE = df.job_type['GiveWater']
--local GIVEWATER2_TYPE = df.job_type['GiveWater2']
-- give food 2
-- give water 2
JOBTYPES[EATJOB_TYPE] = true
JOBTYPES[DRINKBOOZE_TYPE] = true
--JOBTYPES[DRINKWATER_TYPE] = true
JOBTYPES[GIVEFOOD_TYPE] = true
JOBTYPES[GIVEFOOD2_TYPE] = true
--JOBTYPES[GIVEWATER_TYPE] = true
--JOBTYPES[GIVEWATER2_TYPE] = true
local VALUEMULT = .1
local FOOD_WHILE_EATING = 1000
-- dwarves will eat/drink until their counters drop below these values
local HUNGER_THRESHOLD = 10000
local THIRST_THRESHOLD = 10000
-- finished eat/drink jobs will wait this long for a new job of the same kind before considering the binge done
local EXPIRE_TICKS = 30
---------
local PreJobCallin
local PostJobCallin
local DoUpdate
local pt -- persistant storage
local trackedJobs = {}
local PT_Decode = function(str)
local _, _, v1, v2, v3, v4, v5 = string.find(str, '(%S+)%s(%S+)%s(%S+)%s(%S+)%s(%S+)')
return {tonumber(v1), tonumber(v2), tonumber(v3), tonumber(v4), tonumber(v5)}
end
local PT_Encode = function(v)--(v1, v2, v3, v4)
return v[1].." "..v[2].." "..v[3].." "..v[4].." "..v[5]
end
---------
-- find the nutriotion values for one item of food or drink. used by all supported job types
local function GetNutritionVectors(job, jobdata)
local item = job.items[0].item
local itype = item:getType()
local matinfo = dfhack.matinfo.decode(item.mat_type, item.mat_index)
assert(matinfo, 'no material information for job: '..job.id)
local matValue = matinfo.material.material_value
local baseValue = baseValues[itype] * (.9 + matValue * .1)
local matComposition, gid
if matinfo.mode == 'plant' then
gid = matLookup.PLANT[matinfo.plant.id].group_id
matComposition = matLookup.PLANT[matinfo.plant.id].materials[matinfo.material.id]
elseif matinfo.mode == 'creature' then
gid = matLookup.CREATURE[matinfo.creature.creature_id].group_id
matComposition = matLookup.CREATURE[matinfo.creature.creature_id].materials[matinfo.material.id]
else
return {0, 0, 0, 0, 1, 0, 0} -- assume its entirely made of fiber
end
local result = {0, 0, 0, 0, 0}
for i = 1, 7 do
for j = 1, 5 do
result[j] = result[j] + (nutVectors[i][j] * matComposition[i] * baseValue)
end
end
return result
end
-- give food lacks a callin for completion so this function gets used instead
-- it just sets the target units' counters
local function CompleteGiveJob(job, jobdata)
local result = jobdata.result
local before = jobdata.before
local c2 = jobdata.target.counters2
c2.stomach_content = before[2] + result[2]
c2.stomach_food = before[3] + result[3]
c2.stored_fat = before[4] + result[4]
local hunger = before[1] - result[1]
c2.hunger_timer = hunger > -1 and hunger or 0
local thirst = before[5] - result[5]
c2.thirst_timer = thirst > -1 and thirst or 0
end
-- gets called by eventful when any job completes, but some are not supported
PostJobCallin = function(job)
if trackedJobs[job.id] then
local jobdata = trackedJobs[job.id]
local jobtype = job.job_type
if jobtype == EATJOB_TYPE or jobtype == DRINKBOOZE_TYPE then
local jobdata = trackedJobs[job.id]
local before = jobdata.before
local result = GetNutritionVectors(job, jobdata)
local eatenSoFar = tonumber(pt.units[jobdata.uid].eatenSoFar) + result[1]
local drunkenSoFar = tonumber(pt.units[jobdata.uid].drunkenSoFar) + result[5]
local moderation = jobdata.unit.status.current_soul.personality.traits.IMMODERATION
local modMult = math.abs((((moderation - 25) * 1.2) / 100) + 1)
local hunger_real = before[1] - eatenSoFar
local thirst_real = before[5] - drunkenSoFar
local c2 = jobdata.unit.counters2
c2.stomach_content = before[2] + result[2]
c2.stomach_food = before[3] + result[3]
c2.stored_fat = before[4] + result[4]
-- stomach content as a break condition needs to be considered
if jobdata.jobtype == DRINKBOOZE_TYPE then
local hunger = before[1] - result[1]
c2.hunger_timer = hunger > -1 and hunger or 0
local thirst_mod = before[5] - (drunkenSoFar / modMult)
if thirst_mod < THIRST_THRESHOLD then -- we are done drinking
c2.thirst_timer = thirst_real > -1 and thirst_real or 0
pt.units[jobdata.uid].drunkenSoFar = '0'
else -- more booze!
c2.thirst_timer = before[5]
pt.units[jobdata.uid].drunkenSoFar = tostring(drunkenSoFar)
trackedJobs[job.id].waitTimer = EXPIRE_TICKS
trackedJobs[job.id].ptr.waitTimer = tostring(EXPIRE_TICKS)
end
else
local thirst = before[5] - result[5]
c2.thirst_timer = thirst > -1 and thirst or 0
local hunger_mod = before[1] - (eatenSoFar / modMult)
if hunger_mod < HUNGER_THRESHOLD then -- we are done eating
c2.hunger_timer = hunger_real > -1 and hunger_real or 0
pt.units[jobdata.uid].eatenSoFar = '0'
else -- we must keep going
c2.hunger_timer = before[1]
pt.units[jobdata.uid].eatenSoFar = tostring(eatenSoFar)
trackedJobs[job.id].waitTimer = EXPIRE_TICKS
trackedJobs[job.id].ptr.waitTimer = tostring(EXPIRE_TICKS)
end
end
-- we may be keeping this job's data around for a bit to see if a job of the same kind is going
-- to happen. we do need to remove the actual job from the table tho because the adress will be re-used
-- by df for another job
trackedJobs[job.id].job = nil
end
end
end
-- we create a table for every job that we are interested in and start to track it, which happens in DoUpdate()
PreJobCallin = function(job)
local jobtype = job.job_type
if JOBTYPES[jobtype] then
if jobtype == GIVEFOOD_TYPE or jobtype == GIVEFOOD2_TYPE then
-- for give jobs, we get the target ref first, as the job only later aquires a worker
local jid = job.id
local tid = job.general_refs[0].unit_id
local target = df.unit.find(tid)
local ptr
pt.jobs[jid] = {}
ptr = pt.jobs[jid]
ptr.tid = tostring(tid)
ptr.jobtype = tostring(job.job_type)
trackedJobs[jid] = {
set = function(self, data)
self.before = data
self.ptr.before = PT_Encode(data)
end,
compare = function(self, data)
return self.before[1] > data[1] and self.before[1] - data[1] or nil
end,
before = {},
target = target,
tid = tid,
job = job,
jobtype = job.job_type,
ptr = ptr,
}
else -- normal eat or drink job
local jid = job.id
local uid = job.general_refs[0].unit_id
local unit = df.unit.find(uid)
if not pt.units[uid] then -- we will need to store this data over multiple jobs
pt.units[uid] = {}
pt.units[uid].eatenSoFar = '0'
pt.units[uid].drunkenSoFar = '0'
end
local ptr
pt.jobs[jid] = {}
ptr = pt.jobs[jid]
ptr.uid = tostring(uid)
ptr.jobtype = tostring(job.job_type)
trackedJobs[jid] = {
set = function(self, data)
self.before = data
self.ptr.before = PT_Encode(data)
end,
before = {},
unit = unit,
uid = uid,
job = job,
jobtype = job.job_type,
ptr = ptr,
waitTimer = -1,
}
end
end
end
DoUpdate = function()
for id, j in pairs(trackedJobs) do
local uid, unit = j.uid, j.unit
local canceled, finished
if j.job then
if j.jobtype == GIVEFOOD_TYPE or j.jobtype == GIVEFOOD2_TYPE then
if not j.result and #j.job.items > 0 then
local result = GetNutritionVectors(j.job, j)
j.result = result
j.ptr.result = PT_Encode(result)
end
-- we dont actually need the worker unit's data to complete the job, i think
if #j.job.general_refs > 1 then -- we have a worker
if j.unit and j.unit.job.current_job and j.unit.job.current_job.job_type ~= j.jobtype then
-- if we have a worker but he decides to take another job, without removing the gen_ref
-- cannot happen
do end
elseif not j.uid then -- if we dont have the workers data in table yet
j.uid = j.job.general_refs[1].unit_id
j.unit = df.unit.find(j.uid)
j.ptr.uid = tostring(j.uid)
local c2 = j.target.counters2
-- this is the latest we can set the data unless we want to continually poll until completion
j:set({c2.hunger_timer, c2.stomach_content, c2.stomach_food, c2.stored_fat, c2.thirst_timer})
end
elseif #j.job.general_refs < 1 then -- we have either a success or cancelation
-- at this point, the job is done(and garbage collected)
-- lacking a completion timer, we decide wether a give job was successful by comparing the
-- recipient's counters before and after the gen_refs disappeared
local c2 = j.target.counters2
local margin = j:compare({c2.hunger_timer, c2.stomach_content, c2.stomach_food, c2.stored_fat, c2.thirst_timer})
if margin then
-- the data we are using here is slightly stale, giving the food recipient an extra
-- few hundred food units
CompleteGiveJob(j.job, j)
finished = true
else
canceled = true
end
end
elseif j.job.completion_timer == 0 then -- tick before job completion callin. we remember the unit's stats
local c2 = j.unit.counters2
j:set({c2.hunger_timer, c2.stomach_content, c2.stomach_food, c2.stored_fat, c2.thirst_timer})
elseif #j.job.general_refs < 1 then -- if the job lost its worker, its canceled
canceled = true
end
elseif j.waitTimer > -1 then
-- at this point we already know that the job completed because the job link is gone.
-- if the timer is running, we might have a new job or a cancelation,
-- if it isnt running, the unit is no longer hungry or thirsty
if unit.job.current_job then
if unit.job.current_job.job_type == j.jobtype then -- new eat/drink job
-- we are going to keep the values in the unit's table(how much it already ate/drank)
-- but we are going to delete the job entry
if unit.job.current_job.id ~= id then
finished = true
-- elseif j.waitTimer == 0 then -- debug
end
else -- took a different job. write the unit data into memory and kill the job entry
canceled = true
end
elseif j.waitTimer == 0 then -- if unit doesnt take a new job in 30 ticks, cancel
canceled = true
end
j.waitTimer = j.waitTimer - 1
else
finished = true
end
if canceled then
if j.jobtype == EATJOB_TYPE then
local ht = unit.counters2.hunger_timer - tonumber(pt.units[uid].eatenSoFar)
unit.counters2.hunger_timer = ht > -1 and ht or 0
pt.units[uid].eatenSoFar = '0'
elseif j.jobtype == DRINKBOOZE_TYPE then
local tt = unit.counters2.thirst_timer - tonumber(pt.units[uid].drunkenSoFar)
unit.counters2.thirst_timer = tt > -1 and tt or 0
pt.units[uid].drunkenSoFar = '0'
--elseif j.jobtype == GIVEFOOD_TYPE or j.jobtype == GIVEFOOD2_TYPE then
-- we already wrote the data, dont need to anything here
end
pt.jobs[tostring(id)] = nil
trackedJobs[id] = nil
end
if finished then
if j.jobtype == GIVEFOOD_TYPE or j.jobtype == GIVEFOOD2_TYPE then
pt.jobs[tostring(id)] = nil
trackedJobs[id] = nil
else
pt.jobs[tostring(id)] = nil
trackedJobs[id] = nil
end
end
end
timer = dfhack.timeout(1, 'ticks', DoUpdate)
end
local function findHangingJobs()
for _, id in pairs(pt.jobs._children) do
local data = pt.jobs[id]
trackedJobs[tonumber(id)] = {}
local j = trackedJobs[tonumber(id)]
j.before = data.before and PT_Decode(data.before) or {}
j.jobtype = tonumber(data.jobtype)
if j.jobtype == GIVEFOOD_TYPE or j.jobtype == GIVEFOOD2_TYPE then
j.tid = tonumber(data.tid)
j.target = df.unit.find(j.tid)
j.result = data.result and PT_Decode(data.result) or nil
end
if data.uid then
j.uid = tonumber(data.uid)
j.unit = df.unit.find(j.uid)
-- only get the unit's current job if we dont have a timer
j.waitTimer = tonumber(data.waitTimer) or -1
if j.waitTimer == - 1 then j.job = j.unit.job.current_job end
end
j.ptr = pt.jobs[id]
j.set = function(self, data)
self.before = data
self.ptr.before = PT_Encode(data)
end
end
end
dfhack.onStateChange.hungrydwarves = function(state)
if state == 2 then
print("Hungry Dwarves enabled!")
eventful.enableEvent(eventful.eventType.JOB_INITIATED, 1)
eventful.enableEvent(eventful.eventType.JOB_COMPLETED, 1)
eventful.onJobInitiated.hungrydwarves = PreJobCallin
eventful.onJobCompleted.hungrydwarves = PostJobCallin
if not persistTable.GlobalTable.hungrydwarves then
persistTable.GlobalTable.hungrydwarves = {}
persistTable.GlobalTable.hungrydwarves.jobs = {}
persistTable.GlobalTable.hungrydwarves.units = {}
pt = persistTable.GlobalTable.hungrydwarves
else
pt = persistTable.GlobalTable.hungrydwarves
findHangingJobs()
end
timer = dfhack.timeout(1, 'ticks', DoUpdate)
elseif state == 3 then
print("Hungry Dwarves disabled!")
timer = dfhack.timeout_active(timer, nil)
persistTable.GlobalTable.hungrydwarves = nil
end
end