v50 Steam/Premium information for editors
- v50 information can now be added to pages in the main namespace. v0.47 information can still be found in the DF2014 namespace. See here for more details on the new versioning policy.
- Use this page to report any issues related to the migration.
This notice may be cached—the current version can be found here.
User:Fleeting Frames/relationsindicator
< User:Fleeting Frames
Jump to navigation
Jump to search
Revision as of 05:04, 13 December 2020 by Fleeting Frames (talk | contribs) (Fixed 47.04 historical gelding flag, added trust checking)
local helptext = [=[
relations-indicator
===================
v1.17
Displays the pregnancy, fertility and romantic status of unit(s).
For singular unit, in (v)iew-(g)eneral mode.
For multiple units, in Citizens, Pets/Livestock and Others lists.
By default, displays just pregnancies in those,
as well as binding itself to be called there via keybinding plugin.
Can be called with following arguments
Relating to instructions
-help, help, ?, -?
Displays this text and exits
explain colors
Explains the meanings of various colors used and exits
bindings
Lists the keybindings relations-indicator uses with bind and exits
bind
adds context-specific keybindings
unbind
removes context-specific keybindings and exits]=]
-- ======================================== --
-- Color shades --
-- ======================================== --
local COLOR_DARKGRAY = 8 -- British spelling
local COLOR_LIGHTGRAY = 7 -- ditto
local straightMaleShade = COLOR_LIGHTCYAN -- feels like this should be pregnant color, but tweak uses green
local straightFemaleShade = COLOR_LIGHTMAGENTA -- DT colors. Could alternatively flicker with symbol.
local gayMaleShade = COLOR_YELLOW -- Blue makes more sense, but dark blue on black is hard to see.
local gayFemaleShade = COLOR_LIGHTRED -- originally love or straight female color.
local pregnantColor = COLOR_LIGHTGREEN -- base tweak color, might wap with yellow
local infertileColor = COLOR_LIGHTGRAY
local offsiteShade = COLOR_WHITE -- issue: orientation-offsite? Blinking can solve this, ofc.
local deadColor = COLOR_DARKGRAY -- still, should avoid blinking with symbols if possible.
-- 8 shades, but I only have 7 available. Plus default blue is not so great.
local function colorExplanation()
function printlnc(text, color)
dfhack.color(color)
dfhack.println(text)
dfhack.color(COLOR_RESET)
end
dfhack.println("relations-indicator marks the following traits with following colour:")
dfhack.print("Straight Male ") printlnc("Cyan", straightMaleShade)
dfhack.print("Gay Male ") printlnc("Yellow", gayMaleShade)
dfhack.print("Straight Female ") printlnc("Magenta", straightFemaleShade)
dfhack.print("Gay Female ") printlnc("Red", gayFemaleShade)
dfhack.print("Pregnant ") printlnc("Green", pregnantColor)
dfhack.println("")
dfhack.println("For the first four, darker shade indicates unwillingness to marry.")
dfhack.println("")
dfhack.println("The below three by default replace the first four in top-down hiearchy:")
dfhack.println("")
dfhack.print("Dead partner ") printlnc("Dark gray ", deadColor)
dfhack.print("Infertile/aromantic or infertile/aromantic hetero partner ") printlnc("Light gray", infertileColor)
dfhack.print("Offsite partner ") printlnc("White", offsiteShade)
end
-- ======================================== --
-- Indicator Symbols --
-- ======================================== --
local loversSymbol = "\148"
local marriedSymbol = "\3"
local singleMaleSymbol = "\11"
local singleFemaleSymbol = "\12"
local fertileBirdSymbol = "\8" -- used only in unitlist
local pregnantCreatureSymbol = "\20" -- also only in unitlist
local diedS, diedE = "\197", "\197" --Used just for viewing a single unit.
local offsite = "..." --Also for just a single unit only.
local blinkingdelay = 650
--How often does it blink between colors and/or symbols.
-- ======================================== --
-- Affection thresholds --
-- ======================================== --
local friendlinessthreshold = 0
local loversthreshold = 14
local marriagethreshold = -40
-- ======================================== --
-- Keybindings used --
-- ======================================== --
local keybinding_list = {}
table.insert(keybinding_list, "U@dwarfmode/Default relations-indicator")
table.insert(keybinding_list, "V@dwarfmode/Default relations-indicator")
table.insert(keybinding_list, "Z@unitlist/Citizens relations-indicator")
table.insert(keybinding_list, "Z@dfhack/unitlabors relations-indicator")
table.insert(keybinding_list, "Z@unitlist/Livestock relations-indicator")
table.insert(keybinding_list, "Z@unitlist/Citizens relations-indicator")
table.insert(keybinding_list, "Z@unitlist/Others relations-indicator")
table.insert(keybinding_list, "Z@layer_unit_relationship relations-indicator")
local function relationsIndicatorPowerState(enable)
for bindingi=1, #keybinding_list do
dfhack.run_command_silent("keybinding " .. (enable and "add " or "clear ") .. keybinding_list[bindingi])
end
end
local args = {...}
local argsline = table.concat(args, " ")
--utils.processArgs is neat but requires - and not necessary here
if argsline:find("help") or
argsline:find("?") then
dfhack.println(helptext)
qerror("")
--Not, strictly speaking, a proper exit but it'll do
end
if argsline:find("explain colors") then colorExplanation() qerror("") end
if argsline:find("bindings") then
for bindingi=1, #keybinding_list do
dfhack.println(keybinding_list[bindingi])
end
dfhack.println("(Internal) Z@dfhack/lua/manipulator relations-indicator")
qerror("")
end
if argsline:find("unbind") then relationsIndicatorPowerState(false) qerror("") end
if argsline:find("bind") then
relationsIndicatorPowerState(true)
end
-- ======================================== --
-- Loading required modules --
-- ======================================== --
local gui = require 'gui'
local screenconstructor = dfhack.script_environment("gui/indicator_screen")
-- ======================================== --
-- Utility functions --
-- ======================================== --
local function getLen(data)
-- Can't # a hashed table, must use pairs.
local len = 0
for i, val in pairs(data) do
len = len +1
end
return len
end
function blinkergenerator(tableofblinkers)
-- takes numerically indexed table of values
-- returns either single value if table has a single value, or function that alternates which one it returns
-- local manyblinkers = #tableofblinkers
if #tableofblinkers == 1 then
return tableofblinkers[1]
else
function blinkingfunction()
local blinkertable = tableofblinkers
local blinkernr = #tableofblinkers
return blinkertable[1+math.floor(dfhack.getTickCount()/blinkingdelay) % blinkernr]
end
return blinkingfunction
end
end
function getFlag(object, flag)
-- Utility function for safely requesting info from userdata
-- Returns nil if the object doesn't have flag attribute, else returns it's value
-- Because well, ordinarily, {}[flag] returns nil.
-- However, if object is unit - or some other type, it may instead throw an error
local a = {}
if not object or not flag then return nil end
--Crash is still possible for attempting to pairs a nil
for index, value in pairs(object) do
a[index] = value
end
local returnvalue = a[flag]
a = nil --lua automatically garbage cleans tables without variable that links to them.
return returnvalue
end
function getBottomMostViewscreenWithFocus(text, targetscreen)
--Finds and returns the screen closest to root screen whose path includes text
--duplicated from getScreen, admittedly.
if targetscreen and
dfhack.gui.getFocusString(targetscreen):find(text) then
return targetscreen
elseif targetscreen and targetscreen.child then --Attempting to call nil.child will crash this
return getBottomMostViewscreenWithFocus(text, targetscreen.child)
end
-- if there's no child, it didn't find a screen with text in focus and returns nil
end
function writeoverTable(modifiedtable, copiedtable)
--Takes two tables as input
--Removes all the values in first table
--Then places all the values in second table into it
--Returns the first table after that
for index, value in pairs(modifiedtable) do
modifiedtable[index] = nil
end
for index, value in pairs(copiedtable) do
modifiedtable[index] = value
end
return modifiedtable
end
function hasKeyOverlap (searchtable, searchingtable)
-- Looks for a key in searchtable whose name contains the name of a key in searchtable
-- returns true if finds it.
for aindex, avalue in pairs(searchingtable) do
for bindex, bvalue in pairs(searchtable) do
if tostring(bindex):find(tostring(aindex)) then
return true
end
end
end
end
-- ======================================== --
-- Tier 1 (df) functions --
-- ======================================== --
function isItBird(unit)
return (df.global.world.raws.creatures.all[unit.race].caste[0].flags.LAYS_EGGS)
--because it lists all available flags as true or false doesn't need to invoke getFlag
end
function isItSmart(unit)
if (df.global.world.raws.creatures.all[unit.race].caste[0].flags.CAN_LEARN and
not df.global.world.raws.creatures.all[unit.race].caste[0].flags.SLOW_LEARNER) then
return true
else
return false
end
end
function getGenderInInfertileColor(unit)
local symbolColor = {}
symbolColor.text = unit.sex == 1 and
singleMaleSymbol or
( unit.sex == 0 and
singleFemaleSymbol or "")
symbolColor.color = infertileColor
return symbolColor
end
-- ======================================== --
-- 43.05 vs earlier structure differences --
-- ======================================== --
function getPregnancyTimer(unit)
-- Takes local unit, returns time until birth in steps.
-- In case of historical unit always returns -1; don't know about their pregnancy structures.
-- utilizes getFlag
if getFlag(unit, "info") then return -1 end -- so assume they're always not pregnant. They usually aren't
return (getFlag(unit, "pregnancy_timer") or getFlag(unit.relations, "pregnancy_timer"))
--Differences between 43.05 and earlier dfhack.
end
function getLocalRelationship(unit, SpouseOrLover)
--Takes local unit, boolean
-- returns spouse id in the case of true for second value,
-- lover in the case of false or if nil is used and spouse isn't present.
-- utilizes getFlag
if getFlag(unit, "info") then return nil end
--Not intended to be used on historical figure structure, as that is different.
--Also, using nil when number is expected will throw an error, so this points out those mistakes
local is4305p = getFlag(unit, "relationship_ids") and true or false
local relationships = is4305p and unit.relationship_ids or unit.relations
local spousevalue = is4305p and relationships.Spouse or relationships.spouse_id
local lovervalue = is4305p and relationships.Lover or relationships.lover_id
--Again, differences between 43.05 and earlier dfhack.
--Further issue: 43.03 uses spouse_id, 43.05 uses Spouse
--This is not as extensible, but since I only get spouse or lover for now...
if SpouseOrLover == true then return spousevalue end
if SpouseOrLover == false then return lovervalue end
if spousevalue > -1 then
return spousevalue
else
return lovervalue
end
end
-- ======================================== --
-- Tier 2 functions --
-- ======================================== --
function isItGelded(unit)
-- Either local or historical unit
-- returns true or nil
-- utilizes getFlag
if getFlag(unit, "status") then
--local unit
if unit.flags3.gelded then
--usually only pets and guys are gelded, but can be set on anyone
return true
elseif unit.curse.add_tags2.STERILE then
--occurs for vampires and husks
return true
elseif not getFlag(unit.status, "current_soul") then
--occurs for animated corpses
--Could also use unit.curse.add_tags1.OPPOSED_TO_LIFE
--Though I'm not certain you need a soul to breed, lack of soul messes up checking personality
return true
end
elseif getFlag(unit, "info") then
--historical unit
if (getFlag(unit.info, "wounds") and getFlag(unit.info, "wounds") and (getFlag(unit.info.wounds,"geld") ==1 or getFlag(unit.info.wounds,"unk_flags")[0])) then
--suspected gelding flag. 0 is dead, -1 is default?
return true
elseif (getFlag(unit.info,"curse") and getFlag(unit.info.curse, "active_interactions") ) then
for i, interaction in pairs(unit.info.curse.active_interactions) do
--Here, could just check that it's name has VAMPIRE in it in vanilla, but could have modded vamps
--Interestingly, soul is not mentioned in historical unit data. Presumably it is hiding. Fallback plan
for j, text in pairs(interaction.str) do
if text.value:find("CE_ADD_TAG") and (text.value:find("STERILE") or
text.value:find("OPPOSED_TO_LIFE")) then
--side effect: False positive on syndromes that remove those tags.
--ex: modded-in gonads that remove sterility from otherwise-sterile creature
--TODO: fix
return true
end
end
end
end
end
return nil
end
function getPregnancyText(unit)
-- Takes local unit, returns single line string that is "" for no pregnancy
-- utilizes getFlag, getPregnancyTimer
if not getFlag(unit, "status") or
unit.caste > 0 then
return ""
else
local howfar = getPregnancyTimer(unit)
local pregnancy_duration = df.global.world.raws.creatures.all[unit.race].caste[0].flags.CAN_LEARN and 9 or 6
if howfar < 1 then return "" else
local returnstring = ""
if isItBird(unit) then
if (howfar/33600)>1 then returnstring = "Was wooed " .. tostring(math.floor(pregnancy_duration-howfar/33600)) .. " months ago" end
if (howfar/33600)<1 then returnstring = "Last seed withers in " .. tostring(math.floor(howfar/1200)) .. " days " end
if (howfar/33600)>(pregnancy_duration-1) then returnstring = "Was wooed recently" end
else
if (howfar/33600)>0 then returnstring = "Sick in the morning recently" end
if (howfar/33600)<(pregnancy_duration-1) then returnstring = "Missed a period" end
if (howfar/33600)<(pregnancy_duration-2) then returnstring = tostring(math.floor(pregnancy_duration-howfar/33600)) .. " months pregnant" end
if (howfar/33600)<1 then returnstring = "Will give birth in " .. tostring(math.floor(howfar/1200)) .. " days" end
end
return returnstring;
end
end
end
function getSpouseAndLover(unit)
--Takes a local unit, returns either local or historical spouse and lover of that unit if present
--if the spouse/lover is historically set but culled (single parents who can marry), returns false for those
--else, returns for that value
-- utilizes getLocalRelationship, df.historical_figure.find
local historical_unit = df.historical_figure.find(unit.hist_figure_id)
local spouse,lover, unithistoricalrelations
local spouseid = getLocalRelationship(unit, true)
local loverid = getLocalRelationship(unit, false)
if spouseid > -1 then
spouse = df.unit.find(spouseid)
--shortcut
end
if not spouse and --shortcut failed due spouse never arriving, or having left the site
historical_unit then --pets brought on embark aren't historical
--got to dig into historical unit values
unithistoricalrelations = historical_unit.histfig_links
for index, relation in pairs(unithistoricalrelations) do
--no local spouse? Mark both global spouse and lover
if (relation._type == df.histfig_hf_link_spousest) then
spouse=df.historical_figure.find(relation.target_hf)
if not spouse then spouse = false
--there was an id, but there wasn't a histfig with that id (due culling)
elseif spouse.unit_id > -1 and getFlag(spouse,"info") and getFlag(spouse.info, "whereabouts") then
if df.global.ui.site_id==spouse.info.whereabouts.site then
spouseid = spouse.unit_id
spouse = df.unit.find(spouseid)
if not spouse then spouse = false end
end
end
-- small distinction between nil and false: is it better to have loved and lost, or never loved?
elseif (relation._type == df.histfig_hf_link_loverst) then
lover=df.historical_figure.find(relation.target_hf)
if not lover then lover = false
elseif lover.unit_id > -1 and getFlag(lover,"info") and getFlag(lover.info, "whereabouts") then
if df.global.ui.site_id==lover.info.whereabouts.site then
loverid = lover.unit_id
end
end
end
end
end
if loverid > -1 then
lover=df.unit.find(loverid) --can be nil for offsite lover
if not lover then lover = false end --false instead of nil to indicate having checked
end
if not lover and historical_unit then
--No local lover? Maybe lover is global
unithistoricalrelations = historical_unit.histfig_links
for index, relation in pairs(unithistoricalrelations) do
if (relation._type == df.histfig_hf_link_loverst) then
lover=df.historical_figure.find(relation.target_hf)
if not lover then lover = false
elseif lover.unit_id > -1 and getFlag(lover,"info") and getFlag(lover.info, "whereabouts") then
if df.global.ui.site_id==lover.info.whereabouts.site then
loverid = lover.unit_id
end
end
end
end
end
return spouse, lover
end
function areCompatible(unitA,unitB)
--Checks if two local units make compatible pair and returns true if they are, false if not
--Utilizes getFlag, requires them to be historical to check relationships
-- Lets check if one of them is married.
-- If they are, can do hanky panky with spouse alone
local spouseA, loverA = getSpouseAndLover(unitA)
local spouseB, loverB = getSpouseAndLover(unitB)
if spouseA or loverA or
spouseB or loverB then
if spouseA == unitB or
loverA == unitB then
return true
else
return false
end
end
-- Do I check if one is a child?
-- I think not. Arranged marriages can be planned a decade before they happen.
-- Still, age is most common disqualifying factor
if unitA.race ~= unitB.race then return false end
--multi-racial fortress are nice, but humans still can't into dwarves; not like this.
local is4305p = getFlag(unitA, "relationship_ids") and true or false
local relationshipsA = is4305p and unitA or unitA.relations
local relationshipsB = is4305p and unitB or unitB.relations
--age is stored in relations for 4303 but on base level in 43.05, so...
local curYear=df.global.cur_year+df.global.cur_year_tick/403200
local ageA = relationshipsA.birth_year+relationshipsA.birth_time/403200-curYear
local ageB = relationshipsB.birth_year+relationshipsB.birth_time/403200-curYear
--exact age matters
local is47plus = tonumber(dfhack.DF_VERSION:sub(3,4))>46
if (ageA-ageB) > 10 or (ageB-ageA) > 10 then
--over 10 year age difference
if is47plus then --over50% age difference
if ageA/ageB > 1.5 or ageB/ageA > 1.5 then return false end
else
return false
end
end
--Lets check if they have compatible orientations for marrying each other.
local attractionA = getAttraction(unitA)
local attractionB = getAttraction(unitB)
if not ((2 == attractionA[(unitB.sex == 1 and "guy" or "girl")]) and
(2 == attractionB[(unitA.sex == 1 and "guy" or "girl")])) then
-- Admittedly, this means that someone who only romances has no suitable pairings.
return false
end
--Lets check if personalities are compatible.
local unwillingToFriendA, unwillingToLoveA, unwillingToMarryA = getAromantism(unitA)
local unwillingToFriendB, unwillingToLoveB, unwillingToMarryB = getAromantism(unitB)
if unwillingToFriendA or unwillingToLoveA or unwillingToMarryA or
unwillingToFriendB or unwillingToLoveB or unwillingToMarryB then
--If either one as baggage about progressing through a relationship, no babies
return false
end
--Checking for relationships requires digging into historical unit values.
local hfA = unitA.hist_figure_id > -1 and df.historical_figure.find(unitA.hist_figure_id) or nil
local hfB = unitB.hist_figure_id > -1 and df.historical_figure.find(unitB.hist_figure_id) or nil
if hfA and hfB then --basic sanity check.
-- Function to check for being a sibling.
-- Half-siblings...Possible with hacking, and I bet they block
function gethfParent(hfunit, retrieveMother)
--Returns historical mother or father of a historical unit if possible
--otherwise returns nil
for index, relationship_link in pairs(hfunit.histfig_links) do
if retrieveMother and relationship_link._type == df.histfig_hf_link_motherst or
(not retrieveMother and relationship_link._type == df.histfig_hf_link_fatherst) then
return df.historical_figure.find(relationship_link.target_hf)
end
end
end
local momA = gethfParent(hfA, true)
local momB = gethfParent(hfB, true)
local dadA = gethfParent(hfA)
local dadB = gethfParent(hfB)
if momA and momB and momA == momB or --existence of moms must be checked since nil == nil
(dadA and dadB and dadA == dadB) then
--siblings or half-siblings are not allowed
return false
end
--Function to check for grudge:
-- (As it is not used outside parent function, not encapsulating elsewhere despite size)
-- temporarily disabled in 47
if (hfB.info.relationships and (getFlag(hfB.info.relationships,"list") or getFlag(hfB.info.relationships,"hf_visual") )) or (hfA.info.relationships and (getFlag(hfA.info.relationships,"list") or getFlag(hfA.info.relationships,"hf_visual"))) then
function hasGrudgeTowards(hfUnitChecked, hfUnitFuckThisCreatureInParticular)
-- print("Checking for grudge between " .. dfhack.TranslateName(hfUnitChecked.name) .. " and " .. dfhack.TranslateName(hfUnitFuckThisCreatureInParticular.name))
-- Triple-loops checking info.relationships.list[#].anon_3[#].
-- Admittedly, doing this repeatedly for every unit is inefficient.
-- Better would be finding all grudges in fortress at start and cross-checking that.
if hfUnitChecked.info.relationships then
--Invaders, for instance, may have it absent
--Though I wonder if it is even possible to marry off invaders, even after peace settlement
local relationshipList = getFlag(hfUnitChecked.info.relationships, "list") or getFlag(hfUnitChecked.info.relationships, "hf_visual") or {}
for index, relationship in pairs (relationshipList) do
if hfUnitFuckThisCreatureInParticular.id == relationship.histfig_id then
--Found a relationship between the two units. Now for grudge!
if getFlag(relationship, "love") then if (relationship.love < 0 or relationship.trust < 0) then return true else return false end end
local attitude
if getFlag(relationship,'anon_3') ~= nil then attitude = relationship.anon_3 else attitude = relationship.attitude end
for feelingindex, feelingtype in pairs(attitude) do
--A dwarf can have multiple feelings/relationship types with someone.
if feelingtype == 2 then
--[[List of options I've noticed with changing that value:
0: Hero
1: Friend
2: Grudge
3: Bonded
6: Good for Business
7: Friendly Terms? (unsure)
10: Comrade
17: Loyal Soldier
18: Considers Monster (hm, could be interesting RP-wise)
26: Protector of the Weak
Others seemed to default to Friendly terms
with just few points on 7 as second relation.
Perhaps anon_1 and anon_5 may also matter.
--]]
return true
end
end
--Found unit without grudge.
attitude = nil
return false
end
end
local vagueRelationships = getFlag(getFlag(hfUnitChecked, "vague_relationships"),"hfid") or {}
for index, hfid in pairs(vagueRelationships) do
if hfid==hfUnitFuckThisCreatureInParticular.id then
local attitude = vagueRelationships.relationship[index]
if attitude == 2 --can jealous obsession be valid romance target? I guess no for now
or attitude == 3
or attitude == 11
or attitude == 12
or attitude == 13
or attitude == 14
then
return true
else
return false
end
end
end
end
end
if hasGrudgeTowards(hfA, hfB) or
hasGrudgeTowards(hfB, hfA) then
--Either one having a grudge? Welp, no marriage for you two.
return false
end
end
end
-- No other disqualifing factors? It's a go.
return true
end
function getInclinationIfPresent(unit, inclinationnumber)
--takes ensouled unit and numerical index of inclination
--returns the value of inclination or 0 or -1000 in case of divorce from local and seeing the world.
-- utilizes getFlag
local values
if getFlag(unit,"status") then
values = unit.status.current_soul.personality.values
elseif getFlag(unit.info,"personality") then
--can be nil for local units who have never updated their hfunit
--comes up in the case of divorce.
values = getFlag(unit.info.personality, "values") or getFlag(unit.info.personality, "personality").values or getFlag(unit.info.personality, "anon_1").values
else
return -1000 --buggy placeholder: divorced partners are super-incapable of progressing their relationship.
end
-- Do need to check hfunits, since both parties of a ship must be willing to embark on the waters of marriage
for index, value in pairs(values) do
if value.type == inclinationnumber then
return value.strength
end
end
return 0
end
function getAttraction(unit)
--unit can't be nil. The nothingness where there should be something doesn't have sex, y'see.
--Outputs a table of levels of sexual attraction an unit has.
-- utilizes getFlag
local attraction = {guy = 0, girl = 0}
local orientation
if unit.sex~=-1 then
if getFlag(unit, "status") then
--local unit
orientation= getFlag(unit.status, "current_soul") and unit.status.current_soul.orientation_flags or false
--alas, creatures can be soulless.
else
--historical unit
orientation = unit.orientation_flags
end
end
if orientation then
if orientation.romance_male then
attraction.guy = 1
elseif orientation.marry_male then
attraction.guy = 2
end
if orientation.romance_female then
attraction.girl = 1
elseif orientation.marry_female then
attraction.girl = 2
end
end
return attraction
end
-- ======================================== --
-- Tier 3 functions --
-- ======================================== --
function getAromantism(unit)
--Takes local unit
--returns following series of values
local unwillingToFriend, unwillingToLove, unwillingToMarry
--utilizes getFlag, getIncinationIfPresent
--utilizes these internally:
local smittedness, friendly, bonding
--failure conditions : hating friendship and having no eligble friends (not certain, might be enough to just have one-sided relation), hating romance, hating marriage.
-- unit.status.current_soul.personality.values.
-- Type 29: romance, type 2: family, type 3: friendship, type 18: harmony.
-- hfunit.info.personality.values - nil for embark dwarves, present on visitors, like visitors can have nil spouse relation but histfig value set
-- poults born on-site from embark turkeys don't have hfid or more than 0 values.
-- unit.status.current_soul.personality.traits .LOVE_PROPENSITY (if this is high and romance is low can still marry, Putnam suggests from 2015 suggests it is almost the only trait that matters)
-- unknown: Type 3 - friendship, .LUST_PROPENSITY, FRIENDLINESS
-- unknown: how much traits must differ in general for marriage to happen instead of a grudge.
-- also, grudges could be prevented or perhaps later removed by social skills, shared skills and preferences.
--local smittedness, friendly, bonding, unwillingToFriend, unWillingToLove, unwillingToMarry
smittedness = unit.status.current_soul.personality.traits.LOVE_PROPENSITY
-- again, always present on units that are on-site; but obviously histfigs will have to be handled differently
friendly =unit.status.current_soul.personality.traits.FRIENDLINESS
--FRIENDLINESS. I think I've seen ever dyed-in-the-wool quarrels have friends.
bonding = unit.status.current_soul.personality.traits.EMOTIONALLY_OBSESSIVE -- how easily they make connections
-- per Sarias's research in 2013, dwarves must be friends before becoming lovers.
-- local cheerfulness -- happier dwarves make relationships more easily, buuut everyone is at -99999 anyway
-- local lustfulness -- science required to see if it affects whether relationship can form
-- local trust -- relationships should be about trust, but lying scumbags can get married too irl. Who knows?
-- Eventually, for specific units would have to check how well they match up and return granual value.
unwillingToFriend =(getInclinationIfPresent(unit, 3)+friendly+bonding) < friendlinessthreshold and true or false
-- avg 100, min -50, max 250
-- currently requires ~roughly two lowest possible and 1 second lowest possible values.
-- Starting seven do have friends, which can override this.
-- While I've failed to friend off dwarves due personality,
-- those dwarves have managed friendships with at least 1 other person,
-- and several times have managed a marriage with someone else.
-- 18-year old Erush Shoduksǎkzul has 3 friends, having 1 lower FRIENDSHIP, bonding
unwillingToLove = (getInclinationIfPresent(unit, 29)+smittedness) < loversthreshold and true or false
--not using bonding, maybe should. They already have emotional bond with others, though.
--50 is average. 14 might be too low, allowing second lowest value on 1 with other average
--20 is maximum for non-mentioned propensity and hating even the idea of romance, but can sometimes prevent two 1 worse than average values
-- What numercal indicators can I fit into 1 tile, anyway? Blinking, I guess. TWBT would enable gradual color
-- blinking has problems at low fps, but viewing unit and unit list have game paused.
-- Tests should be done with 50 fps/10 gfps, since those are lowest maximums I know of.
-- 30 GFPS can blend-ish, but 10 is blinky. Maybe use -blinking_at input with default value only_if_above_29
-- should check if it blinks on returning to low fps.
unwillingToMarry = getInclinationIfPresent(unit, 2) < marriagethreshold and true or false
--as long as they don't find family loathsome, it's a-ok.
return unwillingToFriend, unwillingToLove, unwillingToMarry
end
function getSpouseAndLoverLink(unit)
-- Currently takes local unit only
-- Returns eight values: spouse and lover names, spouse and lover sites, spouse and lover aliveness, spouse and lover gender
-- Doesn't check the hf relationships if the hf_only unit doesn't have a spouse in histfig links
-- might be an issue if a visitor comes on map, romances someone, and then leaves. Never heard of that happening, but hey
-- utilizes getFlag, getSpouseAndLover, dfhack.units.isAlive, dfhack.TranslateName, df.historical_figure.find
local spouse,lover
local spousesite, loversite = df.global.ui.site_id, df.global.ui.site_id
--blanket current site, unless I assign them differently later
--this is fine as I have indicator for off-site spouse, not on-site spouse
local spouselives, loverlives
spouse, lover = getSpouseAndLover(unit)
if spouse and getFlag(spouse,"info") then spousesite = getFlag(spouse.info, "whereabouts") and getFlag(spouse.info, "whereabouts").site or spouse.info.unk_14.site end
if lover and getFlag(lover,"info") then loversite = getFlag(lover.info, "whereabouts") and getFlag(lover.info, "whereabouts").site or lover.info.unk_14.site end
local spousename, lovername
if spouse == false then
for index, relation in pairs(df.historical_figure.find(unit.hist_figure_id).histfig_links) do
--Spouse has been culled? Maybe they're single parent.
if (relation._type == df.histfig_hf_link_childst) then
spousename = "Single parent"
spouselives = true --More like that they're not dead. Visual thing.
end
end
elseif spouse then
spousename = dfhack.TranslateName(spouse.name)
-- here, as spouse without name doesn't have gender either
if getFlag(spouse, "flags1") then --local unit
spouselives = dfhack.units.isAlive(spouse)
else
spouselives = spouse.died_year < 0
end
end
if lover then
lovername = dfhack.TranslateName(lover.name)
if getFlag(lover, "flags1") then --local unit
loverlives = dfhack.units.isAlive(lover)
else
loverlives = lover.died_year < 0
end
end
-- lovers can't have children, so it's entirely pointless to speak of lost love.
return spousename, lovername, spousesite, loversite, spouselives, loverlives, spouse, lover
end
function getSymbolizedAnimalPreference(unit, unwrapped)
--Returns symbolized pregnancy if animal is pregnant.
--Else, returns gender symbol, color: string and number
--color is a function whose return value blinks between modes if appropriate.
--unwrapped doesn't pass the colors through blinkergenerator.
--utilizes getAttraction, isItBird, isItGelded
local attraction = getAttraction(unit)
local symbolColor = {text, color}
local prefColorTable
if getPregnancyTimer(unit) > 0 then
symbolColor.text = isItBird(unit) and fertileBirdSymbol or pregnantCreatureSymbol
symbolColor.color = pregnantColor
else
symbolColor.text = unit.sex == 1 and
singleMaleSymbol or
( unit.sex == 0 and
singleFemaleSymbol or "")
if unit.sex == -1 or --flesh balls
(attraction.guy == 0 and attraction.girl == 0) or --asexual
isItGelded(unit) then --some tomcats
symbolColor.color = infertileColor
return symbolColor
--strictly speaking, not necessary, due light gray being default color
end
prefColorTable = {}
if unit.sex == 0 then
if attraction.guy > 0 then table.insert(prefColorTable, straightFemaleShade) end
if attraction.girl > 0 then table.insert(prefColorTable, gayFemaleShade) end
else
if attraction.girl > 0 then table.insert(prefColorTable, straightMaleShade) end
if attraction.guy > 0 then table.insert(prefColorTable, gayMaleShade) end
end
end
if unwrapped then
if prefColorTable and #prefColorTable > 0 then
symbolColor.color = prefColorTable
end
return symbolColor
else
if prefColorTable then symbolColor.color = blinkergenerator(prefColorTable) end
if getPregnancyTimer(unit) > 0 then
symbolColor.onhovertext = {
color = pregnantColor,
text = tostring(math.floor((isItSmart(unit) and 9 or 6) -getPregnancyTimer(unit)/33600))
}
end
return symbolColor
end
end
function getSymbolizedSpouse(unit)
-- Currently takes local unit only
-- Returns {} with text and color which are string or function and number or function
-- utilizes getSpouseAndLoverLink, getAttraction, getInclinationIfPresent, isItSmart,isItBird, isItGelded
local spousename, lovername, spousesite, loversite, spouselives, loverlives, spouse, lover = getSpouseAndLoverLink(unit)
local symbolColor = {text, color}
local attraction = getAttraction(unit)
--plain sexual attraction table
--it'd be more compact to code if instead of guy and girl values would use 0 and 1 indices
--could call attraction[unit.sex] == 2 to see if they're willing to engage in gay marriage, for example
--however, code is more self-explanatory with variables having names that explain what they're for
local unwillingToFriend, unwillingToLove, unwillingToMarry
if (attraction.guy+attraction.girl) == 0 then
unwillingToFriend, unwillingToLove, unwillingToMarry = true, true, true
else
unwillingToFriend, unwillingToLove, unwillingToMarry = getAromantism(unit)
end
--series of disqualifying boolean values; though if orientation is already zero better not check
local symbolTable = {}
local colorTable = {}
if getPregnancyTimer(unit) > 0 and
isItSmart(unit) then --necessary due otherwise doubling up on the indicator with animal preferences.
--Normally, would lose nothing by having pregnancy highest hiearchy, could just check first and skip the above
--However, in cases of modding or father dying in battle (such as in fucduck's elven outpost) info is lost
if isItBird(unit) then
--possible with bird-women adventurers joining the fortress
table.insert(symbolTable,fertileBirdSymbol)
else
table.insert(symbolTable,pregnantCreatureSymbol)
end
table.insert(colorTable,pregnantColor)
end
if not isItSmart(unit) then --it's an animal
local animalprefs = getSymbolizedAnimalPreference(unit,true)
table.insert(symbolTable, animalprefs.text)
if type(animalprefs.color) == "table" then
table.insert(colorTable, animalprefs.color[0])
table.insert(colorTable, animalprefs.color[1]) --two gender prefs at most.
table.insert(symbolTable, animalprefs.text) --going to mess up timing otherwise.
else
table.insert(colorTable, animalprefs.color)
end
end
if not lovername and
(not spousename or spousename == "Single parent") then
if isItSmart(unit) then --otherwise already handled earlier, don't need to add anything.
table.insert(symbolTable,
((unit.sex == 0) and singleFemaleSymbol or (unit.sex == 1 and singleMaleSymbol or "")))
--creatures without gender don't get a symbol.
if (attraction.guy == 0 and attraction.girl == 0) or --asexual
isItGelded(unit) or -- gelded. Aw.
unwillingToLove or --aromantic. Requires soul.
sex == -1 then --creatures like iron men and bronze colossi indicate that genderless creatures can't breed
table.insert(colorTable, infertileColor)
else
if unit.sex == 0 then
if attraction.girl > 0 then
table.insert(colorTable,
(gayFemaleShade - 8*( (unwillingToMarry or attraction.girl == 1) and 1 or 0 )))
--darker shade for lover-only relationships
end
if attraction.guy > 0 then
table.insert(colorTable,
(straightFemaleShade - 8*( (unwillingToMarry or attraction.guy == 1) and 1 or 0 )))
end
else
if attraction.girl > 0 then
table.insert(colorTable,
(straightMaleShade - 8*( (unwillingToMarry or attraction.girl == 1) and 1 or 0 )))
end
if attraction.guy > 0 then
table.insert(colorTable,
(gayMaleShade - 8*( (unwillingToMarry or attraction.guy == 1) and 1 or 0 )))
end
end
if #colorTable>#symbolTable and --Our table has more colors than symbols. Noprobs,
#symbolTable>1 then --unless there's pregnant single present
table.insert(symbolTable, symbolTable[#symbolTable])
--Pregnant singles screw up timing, unless we double up on last symbol.
end
end
end
else
if spousename and spousename ~= "Single parent" then
table.insert(symbolTable, marriedSymbol)
--table for on-site, alive, not infertile spouse
-- Using hiearchy:
-- Dead > Infertile (unless gay) > Offsite > Normal bright color.
local spousecolor = (unit.sex == 0) and
(spouse.sex==1 and straightFemaleShade or gayFemaleShade) or
(spouse.sex==0 and straightMaleShade or gayMaleShade)
-- live marriage
spousecolor = df.global.ui.site_id == spousesite and spousecolor or offsiteShade
--spouse is offsite. Can have problems with divorcing.
spousecolor = (isItGelded(unit) or isItGelded(spouse)) and
not (unit.sex == spouse.sex)
and infertileColor or spousecolor
--spouse is infertile and not gay-only marriage
spousecolor = spouselives and spousecolor or deadColor
table.insert(colorTable, spousecolor)
end
if lovername then
table.insert(symbolTable, loversSymbol)
-- Two types of lovers: ones willing to progress to marriage, ones unwilling
-- The unwilling ones get darker shade
-- Both parties must be willing
-- Lovers and spouses may also be off-site, dead or infertile.
-- While can display all with blinky things, should minimize needless UI churn.
-- Dead > Infertile (unless gay) > Offsite > Unwilling to progress to marriage > Normal bright color.
local lovercolor = (unit.sex == 0) and
(lover.sex==0 and gayFemaleShade or straightFemaleShade) or
(lover.sex==0 and straightMaleShade or gayMaleShade)
--baseline is willing to marry
lovercolor = (attraction[((lover.sex==1) and "guy" or "girl")] < 2 or
getAttraction(lover)[((unit.sex==1) and "guy" or "girl")] < 2 or
unwillingToMarry or
(getInclinationIfPresent(lover, 2) < marriagethreshold and true or false)) and
(lovercolor - 8) or lovercolor
-- if the unit or their lover has personality or attraction failure, the relationship will not progress
lovercolor = df.global.ui.site_id == loversite and lovercolor or offsiteShade
--lover is offsite. Can have problems with divorcing.
--Happens mostly in case of visitors or married migrants.
--Issue: Either a possible false hope of offsite lover eventually arriving.
-- or a false indicator of lover being on-site.
--Blinking and blurring could solve this, but not hiearchy.
lovercolor = (isItGelded(unit) or isItGelded(lover)) and
not (unit.sex == lover.sex)
and infertileColor or lovercolor
--lover is infertile and not gay-only marriage
lovercolor = loverlives and lovercolor or deadColor
--lover is dead
table.insert(colorTable, lovercolor)
end
end
symbolColor.text = blinkergenerator(symbolTable)
symbolColor.color = blinkergenerator(colorTable)
if getPregnancyTimer(unit) > 0 then
symbolColor.onhovertext = {
color = pregnantColor,
text = tostring(math.floor((isItSmart(unit) and 9 or 6) -getPregnancyTimer(unit)/33600))
--Needs to be converted to string since #text is called for width
}
end
return symbolColor
end
function getSpouseText(unit)
-- Takes local unit, returns single line string that is "" if there's no romantic relation
local spousename, lovername, spousesite, loversite, spouselives, loverlives = getSpouseAndLoverLink(unit)
--An unit can have both lover and spouse in vanilla with retirement shenanigans
local returnstring = ""
if spousename then
--spouse matters more so goes first.
returnstring = returnstring .. marriedSymbol
if spousesite ~= df.global.ui.site_id then
returnstring = returnstring .. offsite
end
if spouselives then
returnstring = returnstring .. spousename
else
returnstring = returnstring .. diedS .. spousename ..diedE
end
end
if lovername then
returnstring = returnstring .. loversSymbol
if loversite ~= df.global.ui.site_id then
returnstring = returnstring .. offsite
end
if loverlives then
returnstring = returnstring .. lovername
else
returnstring = returnstring .. diedS .. lovername ..diedE
end
end
return returnstring
end
function getSuitableMatches(unit, unitlist, joblist)
--Takes a local unit, local unitlist and local joblist
--Returns an unitlist that includes unit and then all it's suitable candidates.
--And joblist that has only those same indices, as unitlist viewscreen uses that data.
--utilizes areCompatible
local matchlist, jobmatchlist = {}, {}
--The unit we've checking always comes first
for index=0, #unitlist-1 do
if (unit == unitlist[index]) or -- self
areCompatible(unit, unitlist[index]) then --suitable match
matchlist[index] = true
jobmatchlist[index] = true
end
end
return matchlist, jobmatchlist
end
-- ======================================== --
-- Dynamic text {} output functions --
-- ======================================== --
function getViewUnitPairs(unit)
local returnpairs = {}
local pregnantRichText = {}
pregnantRichText.text = getPregnancyText(unit)
--bit of fluff for pregnancy
pregnantRichText.color = pregnantColor
table.insert(returnpairs, pregnantRichText)
--First line is for pregnancy, second line is for spouse/lover
local spouseRichText = {}
--Also gets lover text
spouseRichText.text = getSpouseText(unit)
function nabNonPregnantColor(unit)
--I want spouse text to be coloured appropriately for the relationship.
local previouscolor, returncolor
local basecolor = getSymbolizedSpouse(unit).color
if type(basecolor) == "number" then
return basecolor
else
previouscolor = getSymbolizedSpouse(unit).color()
function returnfunction()
--In case the romantic relationship is blinky - typically that means pregnancy and/or lover.
local unit = unit
returncolor = getSymbolizedSpouse(unit).color()
--Might as well use code already in place
if returncolor == pregnantColor then
--Of course, if the unit is pregnant, that's shown above, not here.
--Visual bug: Can still start out as pregnant color.
return previouscolor
else
previouscolor = returncolor
return returncolor
end
end
return returnfunction
end
end
if spouseRichText.text then
spouseRichText.color = nabNonPregnantColor(unit)
end
table.insert(returnpairs, spouseRichText)
return returnpairs
end
local HavePrinted, oldCitizens, oldJobs, oldIndices = false
function showUnitPairs(unit)
local unitscreen = getBottomMostViewscreenWithFocus("unitlist", df.global.gview.view.child.child)
if not HavePrinted then
oldCitizens, oldJobs, oldIndices = {}, {}, {}
local index = 0
while getFlag(unitscreen.units.Citizens, index) do
if (unit == unitscreen.units.Citizens[index]) or -- self
areCompatible(unit, unitscreen.units.Citizens[index]) then
index = 1+index
else
table.insert(oldCitizens, unitscreen.units.Citizens[index])
table.insert(oldIndices, index)
oldJobs[#oldIndices] = unitscreen.jobs.Citizens[index]
unitscreen.units.Citizens:erase(index)
unitscreen.jobs.Citizens:erase(index)
end
end
HavePrinted = true
for ci = 0, #unitscreen.units.Citizens -1 do
if (unit == unitscreen.units.Citizens[ci]) then
unitscreen.cursor_pos.Citizens = ci
break;
end
end
end
end
function hideUnitPairs()
if HavePrinted then
local unitscreen = getBottomMostViewscreenWithFocus("unitlist", df.global.gview.view.child.child)
for i=#oldCitizens, 1, -1 do
unitscreen.units.Citizens:insert(oldIndices[i], oldCitizens[i])
unitscreen.jobs.Citizens:insert(oldIndices[i], oldJobs[i])
end
HavePrinted = false
end
end
local pagelength, currentpage, visitorpopupdims, symbolizedList = df.global.gps.dimy - 9, 0, {x = -30, y = 4}
-- pagelength needs to be exposed due manipulators having two lines shorter pages than standard view.
-- Also due resizing.
-- currentpage needs to be exposed due traversing the lists.
function getUnitListPairs()
local unitscreen = getBottomMostViewscreenWithFocus("unitlist", df.global.gview.view.child.child)
--note: Counts from 0, unlike lua tables
local returntable = {}
local cursorposition, unitlist, iter
if unitscreen.page == 0 then --Citizen list
cursorposition = unitscreen.cursor_pos.Citizens
currentpage = math.floor(cursorposition / pagelength)
cursorposition = cursorposition % pagelength --cursor position within a page
unitlist = unitscreen.units.Citizens
for iter = (0+currentpage*pagelength),
( (((1+currentpage)*pagelength-1)<(#unitlist -1)) and
((1+currentpage)*pagelength-1) or
(#unitlist -1)) do
table.insert(returntable, getSymbolizedSpouse(unitlist[iter]))
returntable[#returntable].onclick = function()
local tile = nil
if dfhack.gui.getCurFocus():find("unitlist") then tile = dfhack.screen.readTile(39,df.global.gps.dimy-2) end
--search plugin support
if not tile or (tile and tile.ch == 95 and tile.fg == 2 or tile.ch == 0) then
if HavePrinted then hideUnitPairs() else
showUnitPairs(unitlist[iter]) end
writeoverTable(symbolizedList, getUnitListPairs())
end
end
end
elseif unitscreen.page == 1 or unitscreen.page == 2 then --Livestock or Others
local pageName = (unitscreen.page == 1) and "Livestock" or "Others"
cursorposition = unitscreen.cursor_pos[pageName]
currentpage = math.floor(cursorposition / pagelength)
cursorposition = cursorposition % pagelength --cursor position within a page
unitlist = unitscreen.units[pageName]
for iter = (0+currentpage*pagelength),
( (((1+currentpage)*pagelength-1)<(#unitlist -1)) and
((1+currentpage)*pagelength-1) or
(#unitlist -1)) do
local unit = unitlist[iter]
-- What goes on with combination of pet and intelligent?
-- Tests reveals failure to bear children and love for even histfigged intelligent dogs
-- Perhaps only dumb pets of non-your civ can screw.
-- Of course, might want accurate indicator then anyway, as you might make them your civ members
--Near as I can tell, for historical figures:
-- pet 1 0
-- smart 1 - Fertile
-- 0 Fer Fertile(trogs, trolls)
if isItSmart(unit) then
if (df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET or
df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET_EXOTIC)
-- Intelligent pets seem unable to breed
or unit.hist_figure_id < 0 then
--I think marriage requires being historical,
-- collaborated by historical turkeys being able to marry during retirement
table.insert(returntable,getGenderInInfertileColor(unit))
end
if unit.hist_figure_id > -1 then
table.insert(returntable, getSymbolizedSpouse(unit))
returntable[#returntable].onclick = function()
--Something to display visitor dating pool
--creates several tables per call, but one doesn't usually call it.
local fortressmatches = getSuitableMatches(unit, unitscreen.units.Citizens, unitscreen.jobs.Others)
--Lets find the candidates for a given visitor
-- this is a table of index, true values, not units.
-- print("entered onclick " .. getLen(fortressmatches))
if getLen(fortressmatches) > 0 then
local fortressmatchlist = {}
for index, value in pairs(fortressmatches) do
table.insert(fortressmatchlist, getSymbolizedSpouse(unitscreen.units.Citizens[index]))
fortressmatchlist[#fortressmatchlist].notEndOfLine = true
table.insert(fortressmatchlist, {text = dfhack.TranslateName(unitscreen.units.Citizens[index].name)})
--Lets convert the unit to nicely colored name to display.
end
--print(#fortressmatchlist)
local popupscreen = screenconstructor.getScreen(
fortressmatchlist,
{x = math.floor((df.global.gps.dimx-screenconstructor.getLongestLength(fortressmatchlist,"text"))/2),
y = math.floor((df.global.gps.dimy-screenconstructor.getHeight(fortressmatchlist))/2)},
{x = math.floor((df.global.gps.dimx-screenconstructor.getLongestLength(fortressmatchlist,"text"))/2 -1),
y = math.floor((df.global.gps.dimy-screenconstructor.getHeight(fortressmatchlist))/2 -1),
width = 2+ screenconstructor.getLongestLength(fortressmatchlist,"text"),
height = 2+ screenconstructor.getHeight(fortressmatchlist)}
)
popupscreen:show()
popupscreen.onInput = function() popupscreen:dismiss() end
--There's no input on which I wont want to dismiss the screen.
end
end
end
else
--It doesn't matter if a pet/troglodyte has killed someone or not, they'll breed either way.
if unit.hist_figure_id > -1 then
--Nonetheless, historical pets can marry in retired forts
table.insert(returntable, getSymbolizedSpouse(unit))
--getSymbolizedSpouse calls on below functions for not smart creatures
else
table.insert(returntable, getSymbolizedAnimalPreference(unit))
end
end
--[[ Deprecated logic based on previously believed data.
if unit.hist_figure_id > 0 then
--Can be married. Visitor, murderer, pet...
if isItSmart(unit) and
(df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET or
df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET_EXOTIC) then
--
table.insert(returntable,getGenderInInfertileColor(unit))
end
--Distinction between being smart or not (aka free love) is handled inside.
table.insert(returntable, getSymbolizedSpouse(unit))
else
if isItSmart(unit) then
--Wild animal men and non-historical gremlins
--Infertile until they're able to marry - like after having killed someone important
table.insert(returntable,getGenderInInfertileColor(unit))
else
--normal turkeys brought on embark and wild animals.
table.insert(returntable, getSymbolizedAnimalPreference(unit))
end
end]]--
end
end
return returntable
end
-- ======================================== --
-- Initialization and placement --
-- ======================================== --
local unitscreen = getBottomMostViewscreenWithFocus("unitlist",df.global.gview.view.child.child)
--gets unitlist viewscreen
local viewscreen
if unitscreen then
symbolizedList = getUnitListPairs()
local list_rect = {x = 1, y = 4, width = 1, height = pagelength}
local newscreen = screenconstructor.getScreen(symbolizedList, list_rect, nil)
newscreen:show()
local listtable = {}
listtable[0] = "Citizens"
listtable[1] = "Livestock"
listtable[2] = "Others"
listtable[3] = "Dead"
local oldwhichlist,lenlist, doNotUseBase, manitimeout, manicursorpos = listtable[unitscreen.page]
local manipulatorkeytable = {}
local upkeys = {CURSOR_UP = true}
local downkeys = {CURSOR_DOWN = true}
newscreen.onclick = function()
local tile = nil
if dfhack.gui.getCurFocus():find("unitlist") then tile = dfhack.screen.readTile(39,df.global.gps.dimy-2) end
--search plugin support
--if prevents failing to work in manipulator/main
if not tile or (tile and tile.ch == 95 and tile.fg == 2 or tile.ch == 0) then
if HavePrinted then hideUnitPairs() end --restoring units on random clicks
writeoverTable(symbolizedList, getUnitListPairs()) --restoring appearance too
end
end
local baseonInput = newscreen.onInput
local function onListInput(self, keys)
lenlist = #(unitscreen.units[oldwhichlist])
if keys.LEAVESCREEN and 2 == dfhack.screen.readTile(29,df.global.gps.dimy-2).fg then
--Search plugin support. Tbh, would have been ultimately easier to disable dismiss_on_zoom
local dismissfunc = self.dismiss
self.dismiss = function () end
dfhack.timeout(1, "frames", function() self.dismiss = dismissfunc end )
end
if not doNotUseBase then baseonInput(self, keys) end
local manipulatorscript = getBottomMostViewscreenWithFocus("dfhack/lua/manipulator",df.global.gview.view.child.child)
--Lua manipulator viewscreen is present
local whichlist = listtable[unitscreen.page]
--duplicated from indicator_screen (ugh). Feels like there should be a way to better determine this.
if (currentpage ~= math.floor(unitscreen.cursor_pos[whichlist]/ pagelength) and whichlist ~= "Dead") or
--up-down paging
(oldwhichlist and oldwhichlist ~= whichlist) or --left-right paging
(lenlist ~= #(unitscreen.units[oldwhichlist])) then --search plugin support
oldwhichlist = whichlist
doNotUseBase = true
lenlist = #(unitscreen.units[oldwhichlist])
writeoverTable(symbolizedList, getUnitListPairs())
dfhack.timeout(2, "frames", function() doNotUseBase = false end)
--Something weird happens with writeoverTable here, where it sometimes parses input twice.
--In the absence of other solutions, merely avoid relaying input for two frames.
end
function mv_cursor(keys)
-- Function for the purpose of moving cursor alongside the manipulator.
-- They don't do this natively - a bugging disrepacy.
if keys.CURSOR_UP or keys.CURSOR_DOWN then
unitscreen.cursor_pos[whichlist] = --set new cursor position
(unitscreen.cursor_pos[whichlist] +(keys.CURSOR_DOWN and 1 or -1)) --to 1 up or down from previous
% #(unitscreen.units[whichlist]) --with overflow accounted for.
end
end
--manipulator/main.lua scrolls differently than default interface, got to handle it
--TODO: fix the mess with numpad keys and manipulator/main.lua
if manipulatorscript and manipulatorscript.breakdown_level ~= 2 then
--Finds manipulator here on both Alt-L and Escape back out
--breakdown level check prevents that.
if #(unitscreen.units[whichlist]) > (df.global.gps.dimy -11) then
--multi-page manipulator unitlist handling
if pagelength ~= lenlist then
--Instead of using a sublist that is refreshed manipulator/main uses whole list that is moved
pagelength = lenlist
writeoverTable(symbolizedList, getUnitListPairs())
self:adjustDims(true,nil,nil,nil, pagelength)
self.frame_body.clip_y2 = df.global.gps.dimy - 8
manicursorpos = unitscreen.cursor_pos[whichlist] > (df.global.gps.dimy - 12) and
(df.global.gps.dimy - 12) or unitscreen.cursor_pos[whichlist]
self.frame_body.y1 = 4 - (unitscreen.cursor_pos[whichlist] > (df.global.gps.dimy - 12) and
unitscreen.cursor_pos[whichlist] - (df.global.gps.dimy - 12) or
0)
end
--scrolling occurs if manipulator's cursor position has reached the edge and tries to keep going.
--Manipulator's initial cursor position is either current cursor position or bottom, whichever is smaller
--Successive changes can divorce the two, so need to have internal check.
--Two functions to follow the cursor position of manipulator
function manidown()
if manicursorpos ~= (df.global.gps.dimy - 12) then
manicursorpos = 1 + manicursorpos
elseif manicursorpos == (df.global.gps.dimy - 12) then
if (unitscreen.cursor_pos[whichlist] +1 ) ~= #(unitscreen.units[whichlist]) then
self.frame_body.y1 = -1 + self.frame_body.y1
else
self.frame_body.y1 = 4
manicursorpos = 0
end
end
end
function maniup()
if manicursorpos ~= 0 then
manicursorpos = -1 + manicursorpos
elseif manicursorpos == 0 then
if unitscreen.cursor_pos[whichlist] ~= 0 then
self.frame_body.y1 = 1 + self.frame_body.y1
else
self.frame_body.y1 = (df.global.gps.dimy -7) - #(unitscreen.units[whichlist])
manicursorpos = (df.global.gps.dimy - 12)
end
end
end
--manipulator/main allows shift+up/down scrolling, which has unique behaviour
if hasKeyOverlap(keys, downkeys) then
if not hasKeyOverlap(keys, {["_FAST"] = true}) then
manidown()
else
for i=1, 10 do
if (unitscreen.cursor_pos[whichlist] +1 ) ~= #(unitscreen.units[whichlist]) then
manidown()
mv_cursor(downkeys)
else
if i == 1 then
manidown()
mv_cursor(downkeys)
end
break;
end
end
end
end
if hasKeyOverlap(keys, upkeys) then
if not hasKeyOverlap(keys, {["_FAST"] = true}) then
maniup()
else
for i=1, 10 do
if unitscreen.cursor_pos[whichlist] ~= 0 then
maniup()
mv_cursor(upkeys)
else
if i == 1 then
maniup()
mv_cursor(upkeys)
end
break;
end
end
end
end
end
mv_cursor(keys) --adjust outside cursor, does nothing on shift-scrolling
if df.global.gps.mouse_x == 1 and --clicked on the indicator line
(keys._MOUSE_L or keys._MOUSE_L_DOWN) then
if manipulatorscript and not manitimeout then
manitimeout = true
--timeout necessary due otherwise causing errors with multiple rapid commands
self._native.parent.breakdown_level = 2
self._native.parent.parent.child = self._native.parent.child
self._native.parent = self._native.parent.parent
dfhack.timeout(2, "frames", function()
dfhack.run_command("gui/indicator_screen execute_hook manipulator/main")
end)
dfhack.timeout(2, "frames", function() manitimeout = false end)
end
end
elseif manipulatorscript and manipulatorscript.breakdown_level == 2 then
if keys.LEAVESCREEN then
hideUnitPairs()
dfhack.run_command("relations-indicator")
end
if keys.UNITJOB_ZOOM_CRE then
dfhack.timeout(4, "frames", function() dfhack.run_command("relations-indicator") end)
end
end
end
newscreen.onInput = onListInput
local baseOnResize = newscreen.onResize
function onListResize(self)
-- Unlike with View-unit, the data might change depending on the size of the window.
baseOnResize(self)
if pagelength ~= (df.global.gps.dimy - 9) then
--If window length changed, better refresh the data.
pagelength = df.global.gps.dimy - 9
writeoverTable(symbolizedList, getUnitListPairs())
self:adjustDims(true,list_rect.x, list_rect.y,list_rect.width, pagelength)
--Not adjusting height here would result in situation where making screen shorter works, but taller not.
end
end
newscreen.onResize = onListResize
-- ======================================== --
-- View-unit section --
-- ======================================== --
else
local function viewunit()
viewscreen = dfhack.gui.getCurViewscreen()
local unit = dfhack.gui.getSelectedUnit()
local symbolizedSingle = getViewUnitPairs(unit)
local view_rect = {x = (-30 -(screenconstructor.isWideView() and 24 or 0)), y = 17, width = 28, height = 2}
local newscreen = screenconstructor.getScreen(symbolizedSingle, view_rect, nil)
newscreen:show()
if not dfhack.gui.getFocusString(viewscreen)
:find("dwarfmode/ViewUnits/Some/General") then
--Can enter in inventory view with v if one previously exited inventory view
newscreen:removeFromView() --Gotta hide in that case
end
local baseonInput = newscreen.onInput
local function onViewInput(self, keys)
--handling changing the menu width and units:
--Capturing the state before it changes:
local oldUnit, oldScreen
local sameUnit, sameScreen = true, true
--Tab changing menu width is handled below.
--storing pre-keypress identifiers for unit and viewscreen state
if not keys.CHANGETAB then
oldUnit = df.global.ui_selected_unit
oldScreen = dfhack.gui.getFocusString(viewscreen)
--Merely checking viewscreen match will not work, given that sideview only modifies existing screen
end
baseonInput(self,keys) --Doing baseline housekeeping and passing input to parent
--Finding out if anything changed after parent got the input
if not keys.CHANGETAB then
sameUnit = (oldUnit == df.global.ui_selected_unit)
--could also use dfhack.gui.getSelectedUnit()
sameScreen = (oldScreen == dfhack.gui.getFocusString(viewscreen))
end
if keys.CHANGETAB then
--Tabbing moves around the sideview, so got to readjust screen position.
view_rect.x = -30 -(screenconstructor.isWideView() and 24 or 0)
self:adjustDims(true, view_rect.x)
--unlike text tables, position tables aren't dynamic, to allow them to be incomplete
end
--If unit changed, got to replace the indicator text
if not sameUnit then
writeoverTable(symbolizedSingle,getViewUnitPairs(df.global.world.units.active[df.global.ui_selected_unit]))
elseif not sameScreen then
--Don't want to display the screen if there isn't an unit present, but don't want to spam blinks either
if not dfhack.gui.getFocusString(viewscreen)
:find("dwarfmode/ViewUnits/Some/General") then
--Different screen doesn't mean it's different in same way - need to check here too.
self:removeFromView()
else
--It's general, so better fix it...Thoug well - should change mostly nothing
self:adjustDims(true, view_rect.x, view_rect.y, view_rect.width, view_rect.height)
self.signature = true
end
end
end
newscreen.onInput = onViewInput
end
if dfhack.gui.getCurFocus():find("dwarfmode/ViewUnits/Some")
then viewunit()
else dfhack.timeout(2, "frames", function()
if getBottomMostViewscreenWithFocus("dwarfmode/ViewUnits/Some", df.global.gview.view.child) then
viewunit()
end
end)
end
end