User:Fleeting Frames/relationsindicator

From Dwarf Fortress Wiki
Jump to navigation Jump to search
local helptext = [=[
relations-indicator
===================
       v1.16
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").anon_3 ==1 ) 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) 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