Advertisement
Guest User

[Dwarf Fortress] Fleeting Frames' relationsindicator v 1.11

a guest
Jul 5th, 2018
352
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. local helptext = [=[
  2. relations-indicator
  3. ===================
  4.        v1.1
  5. Displays the pregnancy, fertility and romantic status of unit(s).
  6.     For singular unit, in (v)iew-(g)eneral mode.
  7.     For multiple units, in Citizens, Pets/Livestock and Others lists.
  8.     By default, displays just pregnancies in those,
  9.     as well as binding itself to be called there via keybinding plugin.
  10. Can be called with following arguments
  11.             Relating to instructions
  12.   -help, help, ?, -?
  13.     Displays this text and exits
  14.   explain colors
  15.     Explains the meanings of various colors used and exits
  16.   bindings
  17.     Lists the keybindings relations-indicator uses with bind and exits
  18.   bind
  19.     adds context-specific keybindings
  20.   unbind
  21.     removes context-specific keybindings and exits]=]
  22.  
  23. -- ======================================== --
  24. --                Color shades              --
  25. -- ======================================== --
  26.  
  27. local COLOR_DARKGRAY = 8 --  British spelling
  28. local COLOR_LIGHTGRAY = 7 -- ditto
  29.  
  30. local straightMaleShade = COLOR_LIGHTCYAN -- feels like this should be pregnant color, but tweak uses green
  31. local straightFemaleShade = COLOR_LIGHTMAGENTA -- DT colors. Could alternatively flicker with symbol.
  32. local gayMaleShade = COLOR_YELLOW         -- Blue makes more sense, but dark blue on black is hard to see.
  33. local gayFemaleShade = COLOR_LIGHTRED     -- originally love or straight female color.
  34. local pregnantColor = COLOR_LIGHTGREEN    -- base tweak color, might wap with yellow
  35. local infertileColor = COLOR_LIGHTGRAY
  36. local offsiteShade = COLOR_WHITE -- issue: orientation-offsite? Blinking can solve this, ofc.
  37. local deadColor = COLOR_DARKGRAY --        still, should avoid blinking with symbols if possible.
  38. -- 8 shades, but I only have 7 available. Plus default blue is not so great.
  39.  
  40.  
  41. local function colorExplanation()
  42.     function printlnc(text, color)
  43.         dfhack.color(color)
  44.             dfhack.println(text)
  45.         dfhack.color(COLOR_RESET)
  46.     end
  47.     dfhack.println("relations-indicator marks the following traits with following colour:")
  48.     dfhack.print("Straight Male ") printlnc("Cyan", straightMaleShade)
  49.     dfhack.print("Gay Male ") printlnc("Yellow", gayMaleShade)
  50.     dfhack.print("Straight Female ") printlnc("Magenta", straightFemaleShade)
  51.     dfhack.print("Gay Female ") printlnc("Red", gayFemaleShade)
  52.     dfhack.print("Pregnant ") printlnc("Green", pregnantColor)
  53.     dfhack.println("")
  54.     dfhack.println("For the first four, darker shade indicates unwillingness to marry.")
  55.     dfhack.println("")
  56.     dfhack.println("The below three by default replace the first four in top-down hiearchy:")
  57.     dfhack.println("")
  58.     dfhack.print("Dead partner ") printlnc("Dark gray ", deadColor)
  59.     dfhack.print("Infertile/aromantic or infertile/aromantic hetero partner ") printlnc("Light gray", infertileColor)
  60.     dfhack.print("Offsite partner ") printlnc("White", offsiteShade)
  61. end
  62.  
  63. -- ======================================== --
  64. --                Indicator Symbols         --
  65. -- ======================================== --
  66.  
  67. local loversSymbol = "\148"
  68. local marriedSymbol = "\3"
  69. local singleMaleSymbol = "\11"
  70. local singleFemaleSymbol = "\12"
  71. local fertileBirdSymbol = "\8" -- used only in unitlist
  72. local pregnantCreatureSymbol = "\20" -- also only in unitlist
  73. local diedS, diedE = "\197", "\197" --Used just for viewing a single unit.
  74. local offsite = "..."                --Also for just a single unit only.
  75. local blinkingdelay = 650
  76.     --How often does it blink between colors and/or symbols.
  77.  
  78. -- ======================================== --
  79. --              Affection thresholds        --
  80. -- ======================================== --
  81.  
  82. local friendlinessthreshold = 0
  83. local loversthreshold = 14
  84. local marriagethreshold = -40
  85.  
  86. -- ======================================== --
  87. --                Keybindings used          --
  88. -- ======================================== --
  89.  
  90. local keybinding_list = {}
  91. table.insert(keybinding_list, "U@dwarfmode/Default relations-indicator")
  92.  
  93. table.insert(keybinding_list, "V@dwarfmode/Default relations-indicator")
  94. table.insert(keybinding_list, "Z@unitlist/Citizens relations-indicator")
  95. table.insert(keybinding_list, "Z@dfhack/unitlabors relations-indicator")
  96. table.insert(keybinding_list, "Z@unitlist/Livestock relations-indicator")
  97. table.insert(keybinding_list, "Z@unitlist/Citizens relations-indicator")
  98. table.insert(keybinding_list, "Z@unitlist/Others relations-indicator")
  99. table.insert(keybinding_list, "Z@layer_unit_relationship relations-indicator")
  100.  
  101.  
  102. local function relationsIndicatorPowerState(enable)
  103.     for bindingi=1, #keybinding_list do
  104.         dfhack.run_command_silent("keybinding " .. (enable and "add " or "clear ")  .. keybinding_list[bindingi])
  105.     end
  106. end
  107.  
  108. local args = {...}
  109.  
  110. local argsline = table.concat(args, " ")
  111.     --utils.processArgs is neat but requires - and not necessary here
  112. if  argsline:find("help") or
  113.     argsline:find("?") then
  114.     dfhack.println(helptext)
  115.     qerror("")
  116.         --Not, strictly speaking, a proper exit but it'll do
  117.     end
  118. if argsline:find("explain colors") then colorExplanation() qerror("") end
  119.  
  120. if argsline:find("bindings") then
  121.    for bindingi=1, #keybinding_list do
  122.        dfhack.println(keybinding_list[bindingi])
  123.    end
  124.    dfhack.println("(Internal) Z@dfhack/lua/manipulator relations-indicator")
  125.    qerror("")
  126.  end
  127.  
  128. if argsline:find("unbind") then relationsIndicatorPowerState(false) qerror("") end
  129. if argsline:find("bind") then
  130.     relationsIndicatorPowerState(true)
  131. end
  132.  
  133. -- ======================================== --
  134. --           Loading required modules       --
  135. -- ======================================== --
  136.  
  137. local gui = require 'gui'
  138.  
  139. local screenconstructor = dfhack.script_environment("gui/indicator_screen")
  140.  
  141. -- ======================================== --
  142. --                Utility functions            --
  143. -- ======================================== --
  144.  
  145.  
  146. local function getLen(data)
  147.   -- Can't # a hashed table, must use pairs.
  148.   local len = 0
  149.   for i, val in pairs(data) do
  150.     len = len +1
  151.   end
  152.   return len
  153. end
  154.  
  155. function blinkergenerator(tableofblinkers)
  156.     -- takes numerically indexed table of values
  157.     -- returns either single value if table has a single value, or function that alternates which one it returns
  158.     -- local manyblinkers = #tableofblinkers
  159.     if #tableofblinkers == 1 then
  160.         return tableofblinkers[1]
  161.     else
  162.         function blinkingfunction()
  163.             local blinkertable = tableofblinkers
  164.             local blinkernr = #tableofblinkers
  165.  
  166.             return blinkertable[1+math.floor(dfhack.getTickCount()/blinkingdelay) % blinkernr]
  167.         end
  168.         return blinkingfunction
  169.     end
  170. end
  171.  
  172. function getFlag(object, flag)
  173.     -- Utility function for safely requesting info from userdata
  174.     -- Returns nil if the object doesn't have flag attribute, else returns it's value
  175.     -- Because well, ordinarily, {}[flag] returns nil.
  176.     -- However, if object is unit - or some other type, it may instead throw an error
  177.     local a = {}
  178.     if not object or not flag then return nil end
  179.         --Crash is still possible for attempting to pairs a nil
  180.     for index, value in pairs(object) do
  181.         a[index] = value
  182.     end
  183.     local returnvalue = a[flag]
  184.     a = nil --lua automatically garbage cleans tables without variable that links to them.
  185.     return returnvalue
  186. end
  187.  
  188. function getBottomMostViewscreenWithFocus(text, targetscreen)
  189.     --Finds and returns the screen closest to root screen whose path includes text
  190.     --duplicated from getScreen, admittedly.
  191.  if targetscreen and
  192.     dfhack.gui.getFocusString(targetscreen):find(text) then
  193.     return targetscreen
  194.  elseif targetscreen and targetscreen.child then --Attempting to call nil.child will crash this
  195.     return getBottomMostViewscreenWithFocus(text, targetscreen.child)
  196.  end
  197.  -- if there's no child, it didn't find a screen with text in focus and returns nil
  198. end
  199.  
  200. function writeoverTable(modifiedtable, copiedtable)
  201.     --Takes two tables as input
  202.     --Removes all the values in first table
  203.     --Then places all the values in second table into it
  204.     --Returns the first table after that
  205.     for index, value in pairs(modifiedtable) do
  206.         modifiedtable[index] = nil
  207.     end
  208.     for index, value in pairs(copiedtable) do
  209.         modifiedtable[index] = value
  210.     end
  211.     return modifiedtable
  212. end
  213.  
  214. function hasKeyOverlap (searchtable, searchingtable)
  215.     -- Looks for a key in searchtable whose name contains the name of a key in searchtable
  216.     -- returns true if finds it.
  217.     for aindex, avalue in pairs(searchingtable) do
  218.         for bindex, bvalue in pairs(searchtable) do
  219.             if tostring(bindex):find(tostring(aindex)) then
  220.                 return true
  221.             end
  222.         end
  223.     end
  224. end
  225.  
  226. -- ======================================== --
  227. --            Tier 1 (df) functions         --
  228. -- ======================================== --
  229.  
  230. function isItBird(unit)
  231.     return (df.global.world.raws.creatures.all[unit.race].caste[0].flags.LAYS_EGGS)
  232.     --because it lists all available flags as true or false doesn't need to invoke getFlag
  233. end
  234.  
  235. function isItSmart(unit)
  236. if (df.global.world.raws.creatures.all[unit.race].caste[0].flags.CAN_LEARN and
  237. not df.global.world.raws.creatures.all[unit.race].caste[0].flags.SLOW_LEARNER) then
  238.     return true
  239. else
  240. return false
  241. end
  242.  
  243. end
  244.  
  245. function getGenderInInfertileColor(unit)
  246.     local symbolColor = {}
  247.     symbolColor.text = unit.sex == 1 and
  248.         singleMaleSymbol or
  249.         ( unit.sex == 0 and
  250.         singleFemaleSymbol or "")
  251.     symbolColor.color = infertileColor
  252.     return symbolColor
  253. end
  254. -- ======================================== --
  255. --  43.05 vs earlier structure differences  --
  256. -- ======================================== --
  257.  
  258. function getPregnancyTimer(unit)
  259.     -- Takes local unit, returns time until birth in steps.
  260.     -- In case of historical unit always returns -1; don't know about their pregnancy structures.
  261.     -- utilizes getFlag
  262.     if getFlag(unit, "info") then return -1 end -- so assume they're always not pregnant. They usually aren't
  263.     return (getFlag(unit, "pregnancy_timer") or getFlag(unit.relations, "pregnancy_timer"))
  264.     --Differences between 43.05 and earlier dfhack.
  265. end
  266.  
  267. function getLocalRelationship(unit, SpouseOrLover)
  268.     --Takes local unit, boolean
  269.     -- returns spouse id in the case of true for second value,
  270.     -- lover in the case of false or if nil is used and spouse isn't present.
  271.     -- utilizes getFlag
  272.     if getFlag(unit, "info") then return nil end
  273.     --Not intended to be used on historical figure structure, as that is different.
  274.     --Also, using nil when number is expected will throw an error, so this points out those mistakes
  275.     local is4305p = getFlag(unit, "relationship_ids") and true or false
  276.     local relationships = is4305p and unit.relationship_ids or unit.relations
  277.     local spousevalue = is4305p and relationships.Spouse or relationships.spouse_id
  278.     local lovervalue = is4305p and relationships.Lover or relationships.lover_id
  279.     --Again, differences between 43.05 and earlier dfhack.
  280.     --Further issue: 43.03 uses spouse_id, 43.05 uses Spouse
  281.     --This is not as extensible, but since I only get spouse or lover for now...
  282.     if SpouseOrLover == true then return spousevalue end
  283.     if SpouseOrLover == false then return lovervalue end
  284.     if spousevalue > -1 then
  285.         return spousevalue
  286.     else
  287.         return lovervalue
  288.     end
  289. end
  290.  
  291. -- ======================================== --
  292. --              Tier 2 functions            --
  293. -- ======================================== --
  294.  
  295. function isItGelded(unit)
  296.     -- Either local or historical unit
  297.     -- returns true or nil
  298.     -- utilizes getFlag
  299.     if getFlag(unit, "status") then
  300.         --local unit
  301.         if unit.flags3.gelded then
  302.             --usually only pets and guys are gelded, but can be set on anyone
  303.             return true
  304.         elseif unit.curse.add_tags2.STERILE then
  305.             --occurs for vampires and husks
  306.             return true
  307.         elseif not getFlag(unit.status, "current_soul") then
  308.             --occurs for animated corpses
  309.             --Could also use unit.curse.add_tags1.OPPOSED_TO_LIFE
  310.             --Though I'm not certain you need a soul to breed, lack of soul messes up checking personality
  311.             return true
  312.         end
  313.     elseif getFlag(unit, "info") then
  314.             --historical unit
  315.         if (getFlag(unit.info, "wounds") and getFlag(unit.info, "wounds").anon_3 ==1 ) then
  316.             --suspected gelding flag. 0 is dead, -1 is default?
  317.             return true
  318.         elseif (getFlag(unit.info,"curse") and getFlag(unit.info.curse, "active_interactions") ) then
  319.             for i, interaction in pairs(unit.info.curse.active_interactions) do
  320.             --Here, could just check that it's name has VAMPIRE in it in vanilla, but could have modded vamps
  321.             --Interestingly, soul is not mentioned in historical unit data. Presumably it is hiding. Fallback plan
  322.                 for j, text in pairs(interaction.str) do
  323.                     if text:find("STERILE") or
  324.                        text:find("OPPOSED_TO_LIFE") then
  325.                        --side effect: False positive on syndromes that remove those tags.
  326.                        --ex: modded-in gonads that remove sterility from otherwise-sterile creature
  327.                        --TODO: fix
  328.                         return true
  329.                     end
  330.                 end
  331.             end
  332.         end
  333.     end
  334.     return nil
  335. end
  336.  
  337. function getPregnancyText(unit)
  338.     -- Takes local unit, returns single line string that is "" for no pregnancy
  339.     -- utilizes getFlag, getPregnancyTimer
  340.  if not getFlag(unit, "status") or
  341.     unit.caste > 0 then
  342.     return ""
  343.  else
  344.      local howfar = getPregnancyTimer(unit)
  345.  
  346.      local pregnancy_duration = df.global.world.raws.creatures.all[unit.race].caste[0].flags.CAN_LEARN and 9 or 6
  347.      if howfar < 1 then return "" else
  348.          local returnstring = ""
  349.          if isItBird(unit) then
  350.              if (howfar/33600)>1 then returnstring = "Was wooed " .. tostring(math.floor(pregnancy_duration-howfar/33600)) .. " months ago" end
  351.              if (howfar/33600)<1 then returnstring = "Last seed withers in " .. tostring(math.floor(howfar/1200)) .. " days " end
  352.              if (howfar/33600)>(pregnancy_duration-1) then returnstring = "Was wooed recently" end
  353.  
  354.          else
  355.              if (howfar/33600)>0 then returnstring = "Sick in the morning recently" end
  356.              if (howfar/33600)<(pregnancy_duration-1) then returnstring = "Missed a period" end
  357.              if (howfar/33600)<(pregnancy_duration-2) then returnstring = tostring(math.floor(pregnancy_duration-howfar/33600)) .. " months pregnant" end
  358.              if (howfar/33600)<1 then returnstring = "Will give birth in " .. tostring(math.floor(howfar/1200)) .. " days" end
  359.         end
  360.         return returnstring;
  361.     end
  362.  end
  363. end
  364.  
  365. function getSpouseAndLover(unit)
  366.     --Takes a local unit, returns either local or historical spouse and lover of that unit if present
  367.     --if the spouse/lover is historically set but culled (single parents who can marry), returns false for those
  368.     --else, returns for that value
  369.     -- utilizes getLocalRelationship, df.historical_figure.find
  370.     local historical_unit = df.historical_figure.find(unit.hist_figure_id)
  371.     local spouse,lover, unithistoricalrelations
  372.     local spouseid = getLocalRelationship(unit, true)
  373.     local loverid = getLocalRelationship(unit, false)
  374.     if spouseid > -1 then
  375.         spouse = df.unit.find(spouseid)
  376.         --shortcut
  377.     end
  378.     if not spouse and --shortcut failed due spouse never arriving, or having left the site
  379.        historical_unit then --pets brought on embark aren't historical
  380.         --got to dig into historical unit values
  381.         unithistoricalrelations = historical_unit.histfig_links
  382.         for index, relation in pairs(unithistoricalrelations) do
  383.             --no local spouse? Mark both global spouse and lover
  384.             if (relation._type == df.histfig_hf_link_spousest) then
  385.                 spouse=df.historical_figure.find(relation.target_hf)
  386.                 if not spouse then spouse = false
  387.                     --there was an id, but there wasn't a histfig with that id (due culling)
  388.                 end
  389.                 -- small distinction between nil and false: is it better to have loved and lost, or never loved?
  390.             elseif (relation._type == df.histfig_hf_link_loverst) then
  391.                 lover=df.historical_figure.find(relation.target_hf)
  392.                 if not lover then lover = false
  393.                 end
  394.             end
  395.         end
  396.     end
  397.     if loverid > -1 then
  398.         lover=df.unit.find(loverid) --can be nil for offsite lover
  399.         if not lover then lover = false end --false instead of nil to indicate having checked
  400.     end
  401.     if not lover and historical_unit then
  402.         --No local lover? Maybe lover is global
  403.         unithistoricalrelations = historical_unit.histfig_links
  404.         for index, relation in pairs(unithistoricalrelations) do
  405.             if (relation._type == df.histfig_hf_link_loverst) then
  406.                 lover=df.historical_figure.find(relation.target_hf)
  407.                 if not lover then lover = false end
  408.             end
  409.         end
  410.     end
  411.     return spouse, lover
  412. end
  413.  
  414. function areCompatible(unitA,unitB)
  415.     --Checks if two local units make compatible pair and returns true if they are, false if not
  416.     --Utilizes getFlag, requires them to be historical to check relationships
  417.  
  418.     -- Do I check if one is a child?
  419.     -- I think not. Arranged marriages can be planned a decade before they happen.
  420.     -- Still, age is most common disqualifying factor
  421.     if unitA.race ~= unitB.race then return false end
  422.         --multi-racial fortress are nice, but humans still can't into dwarves; not like this.
  423.     local is4305p = getFlag(unitA, "relationship_ids") and true or false
  424.     local relationshipsA = is4305p and unitA or unitA.relations
  425.     local relationshipsB = is4305p and unitB or unitB.relations
  426.         --age is stored in relations for 4303 but on base level in 43.05, so...
  427.     local ageA = relationshipsA.birth_year+relationshipsA.birth_time/403200
  428.     local ageB = relationshipsB.birth_year+relationshipsB.birth_time/403200
  429.         --exact age matters
  430.     if (ageA-ageB) > 10 or (ageB-ageA) > 10 then
  431.             --over 10 year age difference
  432.         return false
  433.     end
  434.  
  435.     -- Lets check if one of them is married.
  436.     -- If they are, can do hanky panky with spouse alone
  437.     local spouseA, loverA = getSpouseAndLover(unitA)
  438.     local spouseB, loverB = getSpouseAndLover(unitB)
  439.     if spouseA or loverA or
  440.        spouseB or loverB then
  441.        if spouseA == unitB or
  442.           loverA == unitB then
  443.           return true
  444.        else
  445.           return false
  446.        end
  447.     end
  448.  
  449.     --Lets check if they have compatible orientations for marrying each other.
  450.     local attractionA = getAttraction(unitA)
  451.     local attractionB = getAttraction(unitB)
  452.     if not ((2 == attractionA[(unitB.sex == 1 and "guy" or "girl")]) and
  453.             (2 == attractionB[(unitA.sex == 1 and "guy" or "girl")])) then
  454.             -- Admittedly, this means that someone who only romances has no suitable pairings.
  455.         return false
  456.     end
  457.  
  458.     --Lets check if personalities are compatible.
  459.     local unwillingToFriendA, unwillingToLoveA, unwillingToMarryA = getAromantism(unitA)
  460.     local unwillingToFriendB, unwillingToLoveB, unwillingToMarryB = getAromantism(unitB)
  461.     if  unwillingToFriendA or unwillingToLoveA or unwillingToMarryA or
  462.         unwillingToFriendB or unwillingToLoveB or unwillingToMarryB then
  463.         --If either one as baggage about progressing through a relationship, no babies
  464.       return false
  465.     end
  466.     --Checking for relationships requires digging into historical unit values.
  467.     local hfA = unitA.hist_figure_id > -1 and df.historical_figure.find(unitA.hist_figure_id) or nil
  468.     local hfB = unitB.hist_figure_id > -1 and df.historical_figure.find(unitB.hist_figure_id) or nil
  469.     if hfA and hfB then --basic sanity check.
  470.         -- Function to check for being a sibling.
  471.         -- Half-siblings...Possible with hacking, and I bet they block
  472.         function gethfParent(hfunit, retrieveMother)
  473.             --Returns historical mother or father of a historical unit if possible
  474.             --otherwise returns nil
  475.             for index, relationship_link in pairs(hfunit.histfig_links) do
  476.                 if retrieveMother and relationship_link._type == df.histfig_hf_link_motherst or
  477.                     (not retrieveMother and relationship_link._type == df.histfig_hf_link_fatherst) then
  478.                     return df.historical_figure.find(relationship_link.target_hf)
  479.                 end
  480.             end
  481.         end
  482.  
  483.         local momA = gethfParent(hfA, true)
  484.         local momB = gethfParent(hfB, true)
  485.         local dadA = gethfParent(hfA)
  486.         local dadB = gethfParent(hfB)
  487.         if     momA and momB and momA == momB or --existence of moms must be checked since nil == nil
  488.             (dadA and dadB and dadA == dadB) then
  489.             --siblings or half-siblings are not allowed
  490.             return false
  491.         end
  492.  
  493.         --Function to check for grudge:
  494.         -- (As it is not used outside parent function, not encapsulating elsewhere despite size)
  495.         function hasGrudgeTowards(hfUnitChecked, hfUnitFuckThisCreatureInParticular)
  496.             -- print("Checking for grudge between " .. dfhack.TranslateName(hfUnitChecked.name) .. " and " .. dfhack.TranslateName(hfUnitFuckThisCreatureInParticular.name))
  497.             -- Triple-loops checking info.relationships.list[#].anon_3[#].
  498.             -- Admittedly, doing this repeatedly for every unit is inefficient.
  499.             -- Better would be finding all grudges in fortress at start and cross-checking that.
  500.           if hfUnitChecked.info.relationships then
  501.             --Invaders, for instance, may have it absent
  502.             --Though I wonder if it is even possible to marry off invaders, even after peace settlement
  503.             for index, relationship in pairs (hfUnitChecked.info.relationships.list) do
  504.                 if hfUnitFuckThisCreatureInParticular.id == relationship.histfig_id then
  505.                     --Found a relationship between the two units. Now for grudge!
  506.                     local attitude
  507.                     if getFlag(relationship,'anon3') ~= nil then attitude = relationship.anon3 else attitude = relationship.attitude end
  508.                     for feelingindex, feelingtype in pairs(attitude) do
  509.                         --A dwarf can have multiple feelings/relationship types with someone.
  510.                         if feelingtype == 2 then
  511.                             --[[List of options I've noticed with changing that value:
  512.                                 0: Hero
  513.                                 1: Friend
  514.                                 2: Grudge
  515.                                 3: Bonded
  516.                                 6: Good for Business
  517.                                 7: Friendly Terms? (unsure)
  518.                                 10: Comrade
  519.                                 17: Loyal Soldier
  520.                                 18: Considers Monster (hm, could be interesting RP-wise)
  521.                                 26: Protector of the Weak
  522.  
  523.                                 Others seemed to default to Friendly terms
  524.                                 with just few points on 7 as second relation.
  525.  
  526.                                 Perhaps anon_1 and anon_5 may also matter.
  527.                                 --]]
  528.                             return true
  529.                         end
  530.                     end
  531.                     --Found unit without grudge.
  532.                     attitude = nil
  533.                     return false
  534.                 end
  535.             end
  536.           end
  537.         end
  538.  
  539.         if hasGrudgeTowards(hfA, hfB) or
  540.             hasGrudgeTowards(hfB, hfA) then
  541.             --Either one having a grudge? Welp, no marriage for you two.
  542.             return false
  543.         end
  544.     end
  545.     -- No other disqualifing factors? It's a go.
  546.  
  547.     return true
  548. end
  549.  
  550. function getInclinationIfPresent(unit, inclinationnumber)
  551.     --takes ensouled unit and numerical index of inclination
  552.     --returns the value of inclination or 0 or -1000 in case of divorce from local and seeing the world.
  553.     -- utilizes getFlag
  554.     local values
  555.     if getFlag(unit,"status") then
  556.         values = unit.status.current_soul.personality.values
  557.     elseif getFlag(unit.info,"personality") then
  558.         --can be nil for local units who have never updated their hfunit
  559.         --comes up in the case of divorce.
  560.         values = unit.info.personality.values
  561.     else
  562.         return -1000 --buggy placeholder: divorced partners are super-incapable of progressing their relationship.
  563.     end
  564.     -- Do need to check hfunits, since both parties of a ship must be willing to embark on the waters of marriage
  565.     for index, value in pairs(values) do
  566.         if value.type == inclinationnumber then
  567.             return value.strength
  568.         end
  569.     end
  570.     return 0
  571. end
  572.  
  573. function getAttraction(unit)
  574.     --unit can't be nil. The nothingness where there should be something doesn't have sex, y'see.
  575.     --Outputs a table of levels of sexual attraction an unit has.
  576.     -- utilizes getFlag
  577.  local attraction = {guy = 0, girl = 0}
  578.  local orientation
  579.  if unit.sex~=-1 then
  580.    if getFlag(unit, "status") then
  581.     --local unit
  582.     orientation= getFlag(unit.status, "current_soul") and unit.status.current_soul.orientation_flags or false
  583.         --alas, creatures can be soulless.
  584.   else
  585.     --historical unit
  586.     orientation = unit.orientation_flags
  587.  
  588.   end
  589.  end
  590.  if orientation then
  591.   if orientation.romance_male then
  592.     attraction.guy = 1
  593.   elseif orientation.marry_male then
  594.     attraction.guy = 2
  595.   end
  596.   if orientation.romance_female then
  597.     attraction.girl = 1
  598.   elseif orientation.marry_female then
  599.   attraction.girl = 2
  600.   end
  601.  end
  602.  return attraction
  603. end
  604.  
  605. -- ======================================== --
  606. --               Tier 3  functions          --
  607. -- ======================================== --
  608.  
  609. function getAromantism(unit)
  610.     --Takes local unit
  611.     --returns following series of values
  612. local unwillingToFriend, unwillingToLove, unwillingToMarry
  613.     --utilizes getFlag, getIncinationIfPresent
  614.     --utilizes these internally:
  615. local smittedness, friendly, bonding
  616.  
  617. --failure conditions : hating friendship and having no eligble friends (not certain, might be enough to just have one-sided relation), hating romance, hating marriage.
  618. -- unit.status.current_soul.personality.values.
  619. -- Type 29: romance, type 2: family, type 3: friendship, type 18: harmony.
  620. -- hfunit.info.personality.values - nil for embark dwarves, present on visitors, like visitors can have nil spouse relation but histfig value set
  621. -- poults born on-site from embark turkeys don't have hfid or more than 0 values.
  622. -- 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)
  623. -- unknown: Type 3 - friendship, .LUST_PROPENSITY, FRIENDLINESS
  624. -- unknown: how much traits must differ in general for marriage to happen instead of a grudge.
  625. -- also, grudges could be prevented or perhaps later removed by social skills, shared skills and preferences.
  626.  
  627.  
  628. --local smittedness, friendly, bonding, unwillingToFriend, unWillingToLove, unwillingToMarry
  629.  
  630. smittedness = unit.status.current_soul.personality.traits.LOVE_PROPENSITY
  631.     -- again, always present on units that are on-site; but obviously histfigs will have to be handled differently
  632.  
  633. friendly =unit.status.current_soul.personality.traits.FRIENDLINESS
  634. --FRIENDLINESS. I think I've seen ever dyed-in-the-wool quarrels have friends.
  635.  
  636. bonding = unit.status.current_soul.personality.traits.EMOTIONALLY_OBSESSIVE -- how easily they make connections
  637. -- per Sarias's research in 2013, dwarves must be friends before becoming lovers.
  638. -- local cheerfulness -- happier dwarves make relationships more easily, buuut everyone is at -99999 anyway
  639. -- local lustfulness -- science required to see if it affects whether relationship can form
  640. -- local trust -- relationships should be about trust, but lying scumbags can get married too irl. Who knows?
  641. --     Eventually, for specific units would have to check how well they match up and return granual value.
  642.  
  643. unwillingToFriend =(getInclinationIfPresent(unit, 3)+friendly+bonding) < friendlinessthreshold and true or false
  644. -- avg 100, min -50, max 250
  645. -- currently requires ~roughly two lowest possible and 1 second lowest possible values.
  646. -- Starting seven do have friends, which can override this.
  647.  
  648. -- While I've failed to friend off dwarves due personality,
  649. -- those dwarves have managed friendships with at least 1 other person,
  650. -- and several times have managed a marriage with someone else.
  651. -- 18-year old Erush Shoduksǎkzul has 3 friends, having 1 lower FRIENDSHIP, bonding
  652.  
  653. unwillingToLove = (getInclinationIfPresent(unit, 29)+smittedness) < loversthreshold and true or false
  654.     --not using bonding, maybe should. They already have emotional bond with others, though.
  655.     --50 is average. 14 might be too low, allowing second lowest value on 1 with other average
  656.     --20 is maximum for non-mentioned propensity and hating even the idea of romance, but can sometimes prevent two 1 worse than average values
  657.  
  658.     -- What numercal indicators can I fit into 1 tile, anyway? Blinking, I guess. TWBT would enable gradual color
  659.     -- blinking has problems at low fps, but viewing unit and unit list have game paused.
  660.     -- Tests should be done with 50 fps/10 gfps, since those are lowest maximums I know of.
  661.     -- 30 GFPS can blend-ish, but 10 is blinky. Maybe use -blinking_at input with default value only_if_above_29
  662.     -- should check if it blinks on returning to low fps.
  663. unwillingToMarry = getInclinationIfPresent(unit, 2) < marriagethreshold and true or false
  664.     --as long as they don't find family loathsome, it's a-ok.
  665.  
  666. return unwillingToFriend, unwillingToLove, unwillingToMarry
  667. end
  668.  
  669. function getSpouseAndLoverLink(unit)
  670.     -- Currently takes local unit only
  671.     -- Returns eight values: spouse and lover names, spouse and lover sites, spouse and lover aliveness, spouse and lover gender
  672.     -- Doesn't check the hf relationships if the hf_only unit doesn't have a spouse in histfig links
  673.     -- might be an issue if a visitor comes on map, romances someone, and then leaves. Never heard of that happening, but hey
  674.     -- utilizes getFlag, getSpouseAndLover, dfhack.units.isAlive, dfhack.TranslateName, df.historical_figure.find
  675.     local spouse,lover
  676.     local spousesite, loversite = df.global.ui.site_id, df.global.ui.site_id
  677.         --blanket current site, unless I assign them differently later
  678.         --this is fine as I have indicator for off-site spouse, not on-site spouse
  679.     local spouselives, loverlives
  680.     spouse, lover = getSpouseAndLover(unit)
  681.     if spouse and getFlag(spouse,"info") then spousesite = spouse.info.unk_14.site end
  682.     if lover and getFlag(lover,"info") then loversite = lover.info.unk_14.site  end
  683.     local spousename, lovername
  684.     if spouse == false then
  685.         for index, relation in pairs(df.historical_figure.find(unit.hist_figure_id).histfig_links) do
  686.             --Spouse has been culled? Maybe they're single parent.
  687.             if (relation._type == df.histfig_hf_link_childst) then
  688.                 spousename = "Single parent"
  689.                 spouselives = true    --More like that they're not dead. Visual thing.
  690.             end
  691.         end
  692.     elseif spouse then
  693.         spousename = dfhack.TranslateName(spouse.name)
  694.         -- here, as spouse without name doesn't have gender either
  695.         if getFlag(spouse, "flags1") then --local unit
  696.             spouselives = dfhack.units.isAlive(spouse)
  697.         else
  698.             spouselives = spouse.died_year < 0
  699.         end
  700.     end
  701.     if lover then
  702.         lovername = dfhack.TranslateName(lover.name)
  703.         if getFlag(lover, "flags1") then --local unit
  704.             loverlives = dfhack.units.isAlive(lover)
  705.         else
  706.             loverlives = lover.died_year < 0
  707.         end
  708.     end
  709.     -- lovers can't have children, so it's entirely pointless to speak of lost love.
  710.     return spousename, lovername, spousesite, loversite, spouselives, loverlives, spouse, lover
  711. end
  712.  
  713. function getSymbolizedAnimalPreference(unit, unwrapped)
  714.     --Returns symbolized pregnancy if animal is pregnant.
  715.     --Else, returns gender symbol, color: string and number
  716.         --color is a function whose return value blinks between modes if appropriate.
  717.     --unwrapped doesn't pass the colors through blinkergenerator.
  718.     --utilizes getAttraction, isItBird, isItGelded
  719.     local attraction = getAttraction(unit)
  720.     local symbolColor = {text, color}
  721.     local prefColorTable
  722.     if getPregnancyTimer(unit) > 0 then
  723.         symbolColor.text = isItBird(unit) and fertileBirdSymbol or pregnantCreatureSymbol
  724.         symbolColor.color = pregnantColor
  725.     else
  726.         symbolColor.text = unit.sex == 1 and
  727.                             singleMaleSymbol or
  728.                             ( unit.sex == 0 and
  729.                             singleFemaleSymbol or "")
  730.         if unit.sex == -1 or --flesh balls
  731.            (attraction.guy == 0 and attraction.girl == 0) or --asexual
  732.            isItGelded(unit) then --some tomcats
  733.             symbolColor.color = infertileColor
  734.             return symbolColor
  735.             --strictly speaking, not necessary, due light gray being default color
  736.         end
  737.         prefColorTable = {}
  738.         if unit.sex == 0 then
  739.             if attraction.guy > 0 then table.insert(prefColorTable, straightFemaleShade) end
  740.             if attraction.girl > 0 then table.insert(prefColorTable, gayFemaleShade) end
  741.         else
  742.             if attraction.girl > 0 then table.insert(prefColorTable, straightMaleShade) end
  743.             if attraction.guy > 0 then table.insert(prefColorTable, gayMaleShade) end
  744.         end
  745.     end
  746.     if unwrapped then
  747.         if prefColorTable and #prefColorTable > 0 then
  748.         symbolColor.color = prefColorTable
  749.         end
  750.     return symbolColor
  751.     else
  752.     if prefColorTable then symbolColor.color = blinkergenerator(prefColorTable) end
  753.     if getPregnancyTimer(unit) > 0 then
  754.         symbolColor.onhovertext = {
  755.         color = pregnantColor,
  756.         text = tostring(math.floor((isItSmart(unit) and 9 or 6) -getPregnancyTimer(unit)/33600))
  757.         }
  758.     end
  759.     return symbolColor
  760.     end
  761. end
  762.  
  763. function getSymbolizedSpouse(unit)
  764. -- Currently takes local unit only
  765. -- Returns {} with text and color which are string or function and number or function
  766. -- utilizes getSpouseAndLoverLink, getAttraction, getInclinationIfPresent, isItSmart,isItBird, isItGelded
  767. local spousename, lovername, spousesite, loversite, spouselives, loverlives, spouse, lover = getSpouseAndLoverLink(unit)
  768. local symbolColor = {text, color}
  769.  
  770.  
  771. local attraction = getAttraction(unit)
  772.     --plain sexual attraction table
  773.     --it'd be more compact to code if instead of guy and girl values would use 0 and 1 indices
  774.     --could call attraction[unit.sex] == 2 to see if they're willing to engage in gay marriage, for example
  775.     --however, code is more self-explanatory with variables having names that explain what they're for
  776. local unwillingToFriend, unwillingToLove, unwillingToMarry
  777. if (attraction.guy+attraction.girl) == 0 then
  778.   unwillingToFriend, unwillingToLove, unwillingToMarry = true, true, true
  779. else
  780.   unwillingToFriend, unwillingToLove, unwillingToMarry = getAromantism(unit)
  781. end
  782.     --series of disqualifying boolean values; though if orientation is already zero better not check
  783.  
  784. local symbolTable = {}
  785. local colorTable = {}
  786. if getPregnancyTimer(unit) > 0 and
  787.     isItSmart(unit) then --necessary due otherwise doubling up on the indicator with animal preferences.
  788.     --Normally, would lose nothing by having pregnancy highest hiearchy, could just check first and skip the above
  789.     --However, in cases of modding or father dying in battle (such as in fucduck's elven outpost) info is lost
  790.     if isItBird(unit) then
  791.         --possible with bird-women adventurers joining the fortress
  792.     table.insert(symbolTable,fertileBirdSymbol)
  793.     else
  794.     table.insert(symbolTable,pregnantCreatureSymbol)
  795.     end
  796.     table.insert(colorTable,pregnantColor)
  797. end
  798.  
  799. if not isItSmart(unit) then --it's an animal
  800.     local animalprefs = getSymbolizedAnimalPreference(unit,true)
  801.     table.insert(symbolTable, animalprefs.text)
  802.     if type(animalprefs.color) == "table" then
  803.     table.insert(colorTable, animalprefs.color[0])
  804.     table.insert(colorTable, animalprefs.color[1]) --two gender prefs at most.
  805.     table.insert(symbolTable, animalprefs.text) --going to mess up timing otherwise.
  806.     else
  807.     table.insert(colorTable, animalprefs.color)
  808.     end
  809. end
  810. if not lovername and
  811.     (not spousename or spousename == "Single parent") then
  812.     if isItSmart(unit) then --otherwise already handled earlier, don't need to add anything.
  813.         table.insert(symbolTable,
  814.         ((unit.sex == 0) and singleFemaleSymbol or (unit.sex == 1 and singleMaleSymbol or "")))
  815.         --creatures without gender don't get a symbol.
  816.         if (attraction.guy == 0 and attraction.girl == 0) or --asexual
  817.             isItGelded(unit) or -- gelded. Aw.
  818.             unwillingToLove or --aromantic. Requires soul.
  819.             sex == -1 then --creatures like iron men and bronze colossi indicate that genderless creatures can't breed
  820.  
  821.             table.insert(colorTable, infertileColor)
  822.  
  823.         else
  824.             if unit.sex == 0 then
  825.                 if attraction.girl > 0 then
  826.                      table.insert(colorTable,
  827.                      (gayFemaleShade - 8*( (unwillingToMarry or attraction.girl == 1) and 1 or 0  )))
  828.                     --darker shade for lover-only relationships
  829.                 end
  830.                 if attraction.guy > 0 then
  831.                      table.insert(colorTable,
  832.                      (straightFemaleShade - 8*( (unwillingToMarry or attraction.guy == 1) and 1 or 0  )))
  833.                 end
  834.             else
  835.                 if attraction.girl > 0 then
  836.                      table.insert(colorTable,
  837.                      (straightMaleShade - 8*( (unwillingToMarry or attraction.girl == 1) and 1 or 0  )))
  838.                 end
  839.                 if attraction.guy > 0 then
  840.                      table.insert(colorTable,
  841.                      (gayMaleShade - 8*( (unwillingToMarry or attraction.guy == 1) and 1 or 0  )))
  842.                 end
  843.             end
  844.              if #colorTable>#symbolTable and    --Our table has more colors than symbols. Noprobs,
  845.                 #symbolTable>1 then                --unless there's pregnant single present
  846.                 table.insert(symbolTable, symbolTable[#symbolTable])
  847.                 --Pregnant singles screw up timing, unless we double up on last symbol.
  848.             end
  849.         end
  850.     end
  851. else
  852.     if spousename and spousename ~= "Single parent" then
  853.         table.insert(symbolTable, marriedSymbol)
  854.         --table for on-site, alive, not infertile spouse
  855.         -- Using hiearchy:
  856.         -- Dead > Infertile (unless gay) > Offsite > Normal bright color.
  857.         local spousecolor = (unit.sex == 0) and
  858.                             (spouse.sex==1 and straightFemaleShade or gayFemaleShade) or
  859.                             (spouse.sex==0 and straightMaleShade or gayMaleShade)
  860.                             -- live marriage
  861.         spousecolor = df.global.ui.site_id == spousesite and spousecolor or offsiteShade
  862.                             --spouse is offsite. Can have problems with divorcing.
  863.         spousecolor = (isItGelded(unit) or isItGelded(spouse)) and
  864.                         not (unit.sex == spouse.sex)
  865.                         and infertileColor or spousecolor
  866.                         --spouse is infertile and not gay-only marriage
  867.         spousecolor = spouselives and spousecolor or deadColor
  868.         table.insert(colorTable, spousecolor)
  869.     end
  870.     if lovername then
  871.         table.insert(symbolTable, loversSymbol)
  872.         -- Two types of lovers: ones willing to progress to marriage, ones unwilling
  873.         -- The unwilling ones get darker shade
  874.         -- Both parties must be willing
  875.         -- Lovers and spouses may also be off-site, dead or infertile.
  876.         -- While can display all with blinky things, should minimize needless UI churn.
  877.         -- Dead > Infertile (unless gay) > Offsite > Unwilling to progress to marriage > Normal bright color.
  878.         local lovercolor =  (unit.sex == 0) and
  879.                             (lover.sex==0 and gayFemaleShade or straightFemaleShade) or
  880.                             (lover.sex==0 and straightMaleShade or gayMaleShade)
  881.         --baseline is willing to marry
  882.         lovercolor = (attraction[((lover.sex==1) and "guy" or "girl")] < 2 or
  883.               getAttraction(lover)[((unit.sex==1) and "guy" or "girl")] < 2 or
  884.               unwillingToMarry or
  885.               (getInclinationIfPresent(lover, 2) < marriagethreshold and true or false)) and
  886.               (lovercolor - 8) or lovercolor
  887.         -- if the unit or their lover has personality or attraction failure, the relationship will not progress
  888.         lovercolor = df.global.ui.site_id == loversite and lovercolor or offsiteShade
  889.             --lover is offsite. Can have problems with divorcing.
  890.             --Happens mostly in case of visitors or married migrants.
  891.             --Issue: Either a possible false hope of offsite lover eventually arriving.
  892.             --        or a false indicator of lover being on-site.
  893.             --Blinking and blurring could solve this, but not hiearchy.
  894.         lovercolor = (isItGelded(unit) or isItGelded(lover)) and
  895.                 not (unit.sex == lover.sex)
  896.                 and infertileColor or lovercolor
  897.                 --lover is infertile and not gay-only marriage
  898.         lovercolor = loverlives and lovercolor or deadColor
  899.                 --lover is dead
  900.         table.insert(colorTable, lovercolor)
  901.     end
  902.  
  903. end
  904.  
  905. symbolColor.text = blinkergenerator(symbolTable)
  906.  
  907. symbolColor.color = blinkergenerator(colorTable)
  908.  
  909. if getPregnancyTimer(unit) > 0 then
  910.     symbolColor.onhovertext = {
  911.     color = pregnantColor,
  912.     text = tostring(math.floor((isItSmart(unit) and 9 or 6) -getPregnancyTimer(unit)/33600))
  913.     --Needs to be converted to string since #text is called for width
  914.     }
  915. end
  916.  
  917. return symbolColor
  918.  
  919. end
  920.  
  921. function getSpouseText(unit)
  922.     -- Takes local unit, returns single line string that is "" if there's no romantic relation
  923.     local spousename, lovername, spousesite, loversite, spouselives, loverlives = getSpouseAndLoverLink(unit)
  924.    --An unit can have both lover and spouse in vanilla with retirement shenanigans
  925.     local returnstring = ""
  926.     if spousename then
  927.         --spouse matters more so goes first.
  928.         returnstring = returnstring .. marriedSymbol
  929.         if spousesite ~= df.global.ui.site_id then
  930.             returnstring = returnstring .. offsite
  931.         end
  932.         if spouselives then
  933.         returnstring = returnstring .. spousename
  934.         else
  935.         returnstring = returnstring .. diedS .. spousename ..diedE
  936.         end
  937.     end
  938.     if lovername then
  939.         returnstring = returnstring .. loversSymbol
  940.         if loversite ~= df.global.ui.site_id then
  941.             returnstring = returnstring .. offsite
  942.         end
  943.         if loverlives then
  944.         returnstring = returnstring .. lovername
  945.         else
  946.         returnstring = returnstring .. diedS .. lovername ..diedE
  947.         end
  948.     end
  949.  
  950.     return returnstring
  951. end
  952.  
  953. function getSuitableMatches(unit, unitlist, joblist)
  954.     --Takes a local unit, local unitlist and local joblist
  955.     --Returns an unitlist that includes unit and then all it's suitable candidates.
  956.     --And joblist that has only those same indices, as unitlist viewscreen uses that data.
  957.     --utilizes areCompatible
  958. local matchlist, jobmatchlist = {}, {}
  959.     --The unit we've checking always comes first
  960.     for index=0, #unitlist-1 do
  961.         if  (unit == unitlist[index]) or -- self
  962.             areCompatible(unit, unitlist[index]) then --suitable match
  963.           matchlist[index] = true
  964.           jobmatchlist[index] = true
  965.         end
  966.     end
  967.     return matchlist, jobmatchlist
  968. end
  969.  
  970. -- ======================================== --
  971. --      Dynamic text {} output functions    --
  972. -- ======================================== --
  973.  
  974.  
  975. function getViewUnitPairs(unit)
  976.     local returnpairs = {}
  977.     local pregnantRichText = {}
  978.     pregnantRichText.text = getPregnancyText(unit)
  979.     --bit of fluff for pregnancy
  980.     pregnantRichText.color = pregnantColor
  981.     table.insert(returnpairs, pregnantRichText)
  982.     --First line is for pregnancy, second line is for spouse/lover
  983.     local spouseRichText = {}
  984.         --Also gets lover text
  985.     spouseRichText.text = getSpouseText(unit)
  986.     function nabNonPregnantColor(unit)
  987.         --I want spouse text to be coloured appropriately for the relationship.
  988.         local previouscolor, returncolor
  989.         local basecolor = getSymbolizedSpouse(unit).color
  990.         if type(basecolor) == "number" then
  991.             return basecolor
  992.         else
  993.             previouscolor = getSymbolizedSpouse(unit).color()
  994.             function returnfunction()
  995.                 --In case the romantic relationship is blinky - typically that means pregnancy and/or lover.
  996.                 local unit = unit
  997.                 returncolor = getSymbolizedSpouse(unit).color()
  998.                     --Might as well use code already in place
  999.                 if returncolor == pregnantColor then
  1000.                     --Of course, if the unit is pregnant, that's shown above, not here.
  1001.                     --Visual bug: Can still start out as pregnant color.
  1002.                     return previouscolor
  1003.                 else
  1004.                     previouscolor = returncolor
  1005.                     return returncolor
  1006.                 end
  1007.             end
  1008.         return returnfunction
  1009.         end
  1010.     end
  1011.  
  1012.     if spouseRichText.text then
  1013.         spouseRichText.color = nabNonPregnantColor(unit)
  1014.     end
  1015.     table.insert(returnpairs, spouseRichText)
  1016.     return returnpairs
  1017. end
  1018.  
  1019.     local HavePrinted, oldCitizens, oldJobs, oldIndices = false
  1020. function showUnitPairs(unit)
  1021.     local unitscreen = getBottomMostViewscreenWithFocus("unitlist", df.global.gview.view.child.child)
  1022.   if not HavePrinted then
  1023.     oldCitizens, oldJobs, oldIndices = {}, {}, {}
  1024.     local index = 0
  1025.     while getFlag(unitscreen.units.Citizens, index) do
  1026.         if  (unit == unitscreen.units.Citizens[index]) or -- self
  1027.             areCompatible(unit, unitscreen.units.Citizens[index]) then
  1028.             index = 1+index
  1029.         else
  1030.             table.insert(oldCitizens, unitscreen.units.Citizens[index])
  1031.             table.insert(oldIndices, index)
  1032.             oldJobs[#oldIndices] = unitscreen.jobs.Citizens[index]
  1033.             unitscreen.units.Citizens:erase(index)
  1034.             unitscreen.jobs.Citizens:erase(index)
  1035.         end
  1036.     end
  1037.     HavePrinted = true
  1038.     for ci = 0, #unitscreen.units.Citizens -1 do
  1039.         if (unit == unitscreen.units.Citizens[ci]) then
  1040.             unitscreen.cursor_pos.Citizens = ci
  1041.             break;
  1042.         end
  1043.     end
  1044.   end
  1045. end
  1046.  
  1047. function hideUnitPairs()
  1048.   if HavePrinted then
  1049.     local unitscreen = getBottomMostViewscreenWithFocus("unitlist", df.global.gview.view.child.child)
  1050.     for i=#oldCitizens, 1, -1 do
  1051.         unitscreen.units.Citizens:insert(oldIndices[i], oldCitizens[i])
  1052.         unitscreen.jobs.Citizens:insert(oldIndices[i], oldJobs[i])
  1053.     end
  1054.     HavePrinted = false
  1055.   end
  1056. end
  1057.  
  1058.  
  1059.     local pagelength, currentpage, visitorpopupdims, symbolizedList = df.global.gps.dimy - 9, 0, {x = -30, y = 4}
  1060.     -- pagelength needs to be exposed due manipulators having two lines shorter pages than standard view.
  1061.     -- Also due resizing.
  1062.     -- currentpage needs to be exposed due traversing the lists.
  1063. function getUnitListPairs()
  1064.     local unitscreen = getBottomMostViewscreenWithFocus("unitlist", df.global.gview.view.child.child)
  1065.         --note: Counts from 0, unlike lua tables
  1066.     local returntable = {}
  1067.     local cursorposition, unitlist, iter
  1068.     if unitscreen.page == 0 then --Citizen list
  1069.         cursorposition = unitscreen.cursor_pos.Citizens
  1070.         currentpage = math.floor(cursorposition / pagelength)
  1071.         cursorposition = cursorposition % pagelength --cursor position within a page
  1072.         unitlist = unitscreen.units.Citizens
  1073.         for iter = (0+currentpage*pagelength),
  1074.                 ( (((1+currentpage)*pagelength-1)<(#unitlist -1)) and
  1075.                 ((1+currentpage)*pagelength-1) or
  1076.                 (#unitlist -1)) do
  1077.             table.insert(returntable, getSymbolizedSpouse(unitlist[iter]))
  1078.             returntable[#returntable].onclick = function()
  1079.                   local tile = nil
  1080.                   if dfhack.gui.getCurFocus():find("unitlist") then tile = dfhack.screen.readTile(39,df.global.gps.dimy-2) end
  1081.                     --search plugin support
  1082.                   if not tile or (tile and tile.ch == 95 and tile.fg == 2 or tile.ch == 0) then
  1083.                     if HavePrinted then hideUnitPairs() else
  1084.                         showUnitPairs(unitlist[iter]) end
  1085.                     writeoverTable(symbolizedList, getUnitListPairs())
  1086.                   end
  1087.                 end
  1088.         end
  1089.     elseif unitscreen.page == 1 or unitscreen.page == 2 then --Livestock or Others
  1090.         local pageName = (unitscreen.page == 1) and "Livestock" or "Others"
  1091.         cursorposition = unitscreen.cursor_pos[pageName]
  1092.         currentpage = math.floor(cursorposition / pagelength)
  1093.         cursorposition = cursorposition % pagelength --cursor position within a page
  1094.         unitlist = unitscreen.units[pageName]
  1095.         for iter = (0+currentpage*pagelength),
  1096.                 ( (((1+currentpage)*pagelength-1)<(#unitlist -1)) and
  1097.                 ((1+currentpage)*pagelength-1) or
  1098.                 (#unitlist -1)) do
  1099.             local unit = unitlist[iter]
  1100.             -- What goes on with combination of pet and intelligent?
  1101.             -- Tests reveals failure to bear children and love for even histfigged intelligent dogs
  1102.             -- Perhaps only dumb pets of non-your civ can screw.
  1103.             -- Of course, might want accurate indicator then anyway, as you might make them your civ members
  1104.  
  1105.             --Near as I can tell, for historical figures:
  1106.             --            pet    1    0
  1107.             --    smart    1    -    Fertile
  1108.             --            0    Fer    Fertile(trogs, trolls)
  1109.  
  1110.             if isItSmart(unit) then
  1111.                 if (df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET or
  1112.                     df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET_EXOTIC)
  1113.                     -- Intelligent pets seem unable to breed
  1114.                     or unit.hist_figure_id < 0 then
  1115.                         --I think marriage requires being historical,
  1116.                         -- collaborated by historical turkeys being able to marry during retirement
  1117.                         table.insert(returntable,getGenderInInfertileColor(unit))
  1118.                 end
  1119.                 if unit.hist_figure_id > -1 then
  1120.                   table.insert(returntable, getSymbolizedSpouse(unit))
  1121.                   returntable[#returntable].onclick = function()
  1122.                     --Something to display visitor dating pool
  1123.                     --creates several tables per call, but one doesn't usually call it.
  1124.                     local fortressmatches = getSuitableMatches(unit, unitscreen.units.Citizens, unitscreen.jobs.Others)
  1125.                     --Lets find the candidates for a given visitor
  1126.                     -- this is a table of index, true values, not units.
  1127.                     --    print("entered onclick " .. getLen(fortressmatches))
  1128.                     if getLen(fortressmatches) > 0 then
  1129.                       local fortressmatchlist = {}
  1130.                       for index, value in pairs(fortressmatches) do
  1131.                         table.insert(fortressmatchlist, getSymbolizedSpouse(unitscreen.units.Citizens[index]))
  1132.                         fortressmatchlist[#fortressmatchlist].notEndOfLine = true
  1133.                         table.insert(fortressmatchlist, {text = dfhack.TranslateName(unitscreen.units.Citizens[index].name)})
  1134.                             --Lets convert the unit to nicely colored name to display.
  1135.                       end
  1136.                       print(#fortressmatchlist)
  1137.                       local popupscreen = screenconstructor.getScreen(
  1138.                         fortressmatchlist,
  1139.                         {x = math.floor((df.global.gps.dimx-screenconstructor.getLongestLength(fortressmatchlist,"text"))/2),
  1140.                          y = math.floor((df.global.gps.dimy-screenconstructor.getHeight(fortressmatchlist))/2)},
  1141.                         {x = math.floor((df.global.gps.dimx-screenconstructor.getLongestLength(fortressmatchlist,"text"))/2 -1),
  1142.                          y = math.floor((df.global.gps.dimy-screenconstructor.getHeight(fortressmatchlist))/2 -1),
  1143.                          width = 2+ screenconstructor.getLongestLength(fortressmatchlist,"text"),
  1144.                          height = 2+ screenconstructor.getHeight(fortressmatchlist)}
  1145.                          )
  1146.                       popupscreen:show()
  1147.                       popupscreen.onInput = function() popupscreen:dismiss() end
  1148.                          --There's no input on which I wont want to dismiss the screen.
  1149.                     end
  1150.                     end
  1151.                 end
  1152.             else
  1153.                 --It doesn't matter if a pet/troglodyte has killed someone or not, they'll breed either way.
  1154.                 if unit.hist_figure_id > -1 then
  1155.                     --Nonetheless, historical pets can marry in retired forts
  1156.                     table.insert(returntable, getSymbolizedSpouse(unit))
  1157.                     --getSymbolizedSpouse calls on below functions for not smart creatures
  1158.                 else
  1159.                 table.insert(returntable, getSymbolizedAnimalPreference(unit))
  1160.                 end
  1161.             end
  1162.  
  1163.             --[[ Deprecated logic based on previously believed data.
  1164.             if unit.hist_figure_id > 0 then
  1165.                 --Can be married. Visitor, murderer, pet...
  1166.                 if isItSmart(unit) and
  1167.                     (df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET or
  1168.                     df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET_EXOTIC) then
  1169.                     --
  1170.                         table.insert(returntable,getGenderInInfertileColor(unit))
  1171.  
  1172.                 end
  1173.                 --Distinction between being smart or not (aka free love) is handled inside.
  1174.                 table.insert(returntable, getSymbolizedSpouse(unit))
  1175.             else
  1176.                 if isItSmart(unit) then
  1177.                         --Wild animal men and non-historical gremlins
  1178.                         --Infertile until they're able to marry - like after having killed someone important
  1179.                         table.insert(returntable,getGenderInInfertileColor(unit))
  1180.                     else
  1181.                         --normal turkeys brought on embark and wild animals.
  1182.                         table.insert(returntable, getSymbolizedAnimalPreference(unit))
  1183.  
  1184.                 end
  1185.             end]]--
  1186.         end
  1187.  
  1188.     end
  1189.     return returntable
  1190. end
  1191.  
  1192.  
  1193. -- ======================================== --
  1194. --        Initialization and placement      --
  1195. -- ======================================== --
  1196.  
  1197. local unitscreen = getBottomMostViewscreenWithFocus("unitlist",df.global.gview.view.child.child)
  1198.                     --gets unitlist viewscreen
  1199. local viewscreen
  1200.     if unitscreen then
  1201. symbolizedList = getUnitListPairs()
  1202. local list_rect = {x = 1, y = 4, width = 1, height = pagelength}
  1203. local newscreen = screenconstructor.getScreen(symbolizedList, list_rect, nil)
  1204. newscreen:show()
  1205. local listtable = {}
  1206. listtable[0] = "Citizens"
  1207. listtable[1] = "Livestock"
  1208. listtable[2] = "Others"
  1209. listtable[3] = "Dead"
  1210. local oldwhichlist,lenlist, doNotUseBase, manitimeout, manicursorpos = listtable[unitscreen.page]
  1211.  
  1212. local manipulatorkeytable = {}
  1213. local upkeys = {CURSOR_UP = true}
  1214. local downkeys = {CURSOR_DOWN = true}
  1215. newscreen.onclick = function()
  1216.   local tile = nil
  1217.   if dfhack.gui.getCurFocus():find("unitlist") then tile = dfhack.screen.readTile(39,df.global.gps.dimy-2) end
  1218.     --search plugin support
  1219.     --if prevents failing to work in manipulator/main
  1220.   if not tile or (tile and tile.ch == 95 and tile.fg == 2 or tile.ch == 0) then
  1221.     if HavePrinted then hideUnitPairs() end --restoring units on random clicks
  1222.     writeoverTable(symbolizedList, getUnitListPairs()) --restoring appearance too
  1223.   end
  1224. end
  1225. local baseonInput = newscreen.onInput
  1226. local function onListInput(self, keys)
  1227.   lenlist = #(unitscreen.units[oldwhichlist])
  1228.  
  1229.     if keys.LEAVESCREEN and 2 == dfhack.screen.readTile(29,df.global.gps.dimy-2).fg then
  1230.         --Search plugin support. Tbh, would have been ultimately easier to disable dismiss_on_zoom
  1231.         local dismissfunc = self.dismiss
  1232.         self.dismiss = function () end
  1233.         dfhack.timeout(1, "frames", function() self.dismiss = dismissfunc end )
  1234.     end
  1235.  
  1236.   if not doNotUseBase then baseonInput(self, keys) end
  1237.     local manipulatorscript = getBottomMostViewscreenWithFocus("dfhack/lua/manipulator",df.global.gview.view.child.child)
  1238.     --Lua manipulator viewscreen is present
  1239.     local whichlist = listtable[unitscreen.page]
  1240.             --duplicated from indicator_screen (ugh). Feels like there should be a way to better determine this.
  1241.  
  1242.     if (currentpage ~= math.floor(unitscreen.cursor_pos[whichlist]/ pagelength) and whichlist ~= "Dead") or
  1243.         --up-down paging
  1244.         (oldwhichlist and oldwhichlist ~= whichlist) or --left-right paging
  1245.         (lenlist ~= #(unitscreen.units[oldwhichlist])) then --search plugin support
  1246.         oldwhichlist = whichlist
  1247.         doNotUseBase = true
  1248.         lenlist = #(unitscreen.units[oldwhichlist])
  1249.         writeoverTable(symbolizedList, getUnitListPairs())
  1250.         dfhack.timeout(2, "frames", function() doNotUseBase = false end)
  1251.             --Something weird happens with writeoverTable here, where it sometimes parses input twice.
  1252.             --In the absence of other solutions, merely avoid relaying input for two frames.
  1253.     end
  1254.  
  1255.     function mv_cursor(keys)
  1256.         -- Function for the purpose of moving cursor alongside the manipulator.
  1257.         -- They don't do this natively - a bugging disrepacy.
  1258.         if keys.CURSOR_UP or keys.CURSOR_DOWN then
  1259.             unitscreen.cursor_pos[whichlist] = --set new cursor position
  1260.                 (unitscreen.cursor_pos[whichlist] +(keys.CURSOR_DOWN and 1 or -1)) --to 1 up or down from previous
  1261.                 % #(unitscreen.units[whichlist])    --with overflow accounted for.
  1262.         end
  1263.     end
  1264.  
  1265.         --manipulator/main.lua scrolls differently than default interface, got to handle it
  1266.         --TODO: fix the mess with numpad keys and manipulator/main.lua
  1267.     if manipulatorscript and manipulatorscript.breakdown_level ~= 2 then
  1268.         --Finds manipulator here on both Alt-L and Escape back out
  1269.         --breakdown level check prevents that.
  1270.       if #(unitscreen.units[whichlist]) > (df.global.gps.dimy -11) then
  1271.       --multi-page manipulator unitlist handling
  1272.         if pagelength ~= lenlist then
  1273.         --Instead of using a sublist that is refreshed manipulator/main uses whole list that is moved
  1274.           pagelength = lenlist
  1275.           writeoverTable(symbolizedList, getUnitListPairs())
  1276.           self:adjustDims(true,nil,nil,nil, pagelength)
  1277.           self.frame_body.clip_y2 = df.global.gps.dimy - 8
  1278.           manicursorpos = unitscreen.cursor_pos[whichlist] > (df.global.gps.dimy - 12) and
  1279.                                 (df.global.gps.dimy - 12) or unitscreen.cursor_pos[whichlist]
  1280.           self.frame_body.y1 = 4 - (unitscreen.cursor_pos[whichlist] > (df.global.gps.dimy - 12) and
  1281.                                     unitscreen.cursor_pos[whichlist] - (df.global.gps.dimy - 12) or
  1282.                                     0)
  1283.         end
  1284.         --scrolling occurs if manipulator's cursor position has reached the edge and tries to keep going.
  1285.         --Manipulator's initial cursor position is either current cursor position or bottom, whichever is smaller
  1286.         --Successive changes can divorce the two, so need to have internal check.
  1287.  
  1288.         --Two functions to follow the cursor position of manipulator
  1289.         function manidown()
  1290.             if manicursorpos ~= (df.global.gps.dimy - 12) then
  1291.                 manicursorpos = 1 + manicursorpos
  1292.             elseif manicursorpos == (df.global.gps.dimy - 12) then
  1293.                 if (unitscreen.cursor_pos[whichlist] +1 ) ~= #(unitscreen.units[whichlist]) then
  1294.                 self.frame_body.y1 = -1 + self.frame_body.y1
  1295.                 else
  1296.                 self.frame_body.y1 = 4
  1297.                 manicursorpos = 0
  1298.                 end
  1299.             end
  1300.         end
  1301.         function maniup()
  1302.             if manicursorpos ~= 0 then
  1303.                 manicursorpos = -1 + manicursorpos
  1304.             elseif manicursorpos == 0 then
  1305.                 if unitscreen.cursor_pos[whichlist] ~= 0 then
  1306.                     self.frame_body.y1 = 1 + self.frame_body.y1
  1307.                 else
  1308.                     self.frame_body.y1 =  (df.global.gps.dimy -7) - #(unitscreen.units[whichlist])
  1309.                     manicursorpos = (df.global.gps.dimy - 12)
  1310.                 end
  1311.             end
  1312.         end
  1313.  
  1314.         --manipulator/main allows shift+up/down scrolling, which has unique behaviour
  1315.         if hasKeyOverlap(keys, downkeys) then
  1316.             if not hasKeyOverlap(keys, {["_FAST"] = true}) then
  1317.                 manidown()
  1318.             else
  1319.                 for i=1, 10 do
  1320.                     if (unitscreen.cursor_pos[whichlist] +1 ) ~= #(unitscreen.units[whichlist]) then
  1321.                         manidown()
  1322.                         mv_cursor(downkeys)
  1323.                     else
  1324.                         if i == 1 then
  1325.                             manidown()
  1326.                             mv_cursor(downkeys)
  1327.                         end
  1328.                         break;
  1329.                     end
  1330.                 end
  1331.             end
  1332.         end
  1333.         if hasKeyOverlap(keys, upkeys) then
  1334.             if not hasKeyOverlap(keys, {["_FAST"] = true}) then
  1335.                 maniup()
  1336.             else
  1337.                 for i=1, 10 do
  1338.                     if unitscreen.cursor_pos[whichlist] ~= 0 then
  1339.                         maniup()
  1340.                         mv_cursor(upkeys)
  1341.                     else
  1342.                         if i == 1 then
  1343.                             maniup()
  1344.                             mv_cursor(upkeys)
  1345.                         end
  1346.                         break;
  1347.                     end
  1348.                 end
  1349.             end
  1350.         end
  1351.       end
  1352.             mv_cursor(keys) --adjust outside cursor, does nothing on shift-scrolling
  1353.         if df.global.gps.mouse_x == 1 and --clicked on the indicator line
  1354.             (keys._MOUSE_L or keys._MOUSE_L_DOWN) then
  1355.                 if manipulatorscript and not manitimeout then
  1356.                     manitimeout = true
  1357.                     --timeout necessary due otherwise causing errors with multiple rapid commands
  1358.                     self._native.parent.breakdown_level = 2
  1359.                     self._native.parent.parent.child = self._native.parent.child
  1360.                     self._native.parent = self._native.parent.parent
  1361.                     dfhack.timeout(2, "frames", function()
  1362.                     dfhack.run_command("gui/indicator_screen execute_hook manipulator/main")
  1363.                     end)
  1364.                     dfhack.timeout(2, "frames", function() manitimeout = false end)
  1365.                 end
  1366.         end
  1367.     elseif  manipulatorscript and manipulatorscript.breakdown_level == 2 then
  1368.         if keys.LEAVESCREEN then
  1369.             hideUnitPairs()
  1370.             dfhack.run_command("relations-indicator")
  1371.         end
  1372.         if keys.UNITJOB_ZOOM_CRE then
  1373.         dfhack.timeout(4, "frames", function() dfhack.run_command("relations-indicator") end)
  1374.         end
  1375.     end
  1376. end
  1377. newscreen.onInput = onListInput
  1378.  
  1379. local baseOnResize = newscreen.onResize
  1380.  
  1381. function onListResize(self)
  1382.     -- Unlike with View-unit, the data might change depending on the size of the window.
  1383.     baseOnResize(self)
  1384.     if pagelength ~= (df.global.gps.dimy - 9) then
  1385.         --If window length changed, better refresh the data.
  1386.         pagelength = df.global.gps.dimy - 9
  1387.         writeoverTable(symbolizedList, getUnitListPairs())
  1388.         self:adjustDims(true,list_rect.x, list_rect.y,list_rect.width, pagelength)
  1389.         --Not adjusting height here would result in situation where making screen shorter works, but taller not.
  1390.     end
  1391. end
  1392.  
  1393. newscreen.onResize = onListResize
  1394.  
  1395. -- ======================================== --
  1396. --              View-unit section           --
  1397. -- ======================================== --
  1398.  
  1399.  
  1400.     else
  1401. local function viewunit()
  1402. viewscreen = dfhack.gui.getCurViewscreen()
  1403. local unit = dfhack.gui.getSelectedUnit()
  1404. local symbolizedSingle = getViewUnitPairs(unit)
  1405. local view_rect = {x = (-30 -(screenconstructor.isWideView() and 24 or 0)), y = 17, width = 28, height = 2}
  1406. local newscreen = screenconstructor.getScreen(symbolizedSingle, view_rect, nil)
  1407. newscreen:show()
  1408. if not dfhack.gui.getFocusString(viewscreen)
  1409.         :find("dwarfmode/ViewUnits/Some/General") then
  1410.     --Can enter in inventory view with v if one previously exited inventory view
  1411.     newscreen:removeFromView() --Gotta hide in that case
  1412. end
  1413. local baseonInput = newscreen.onInput
  1414. local function onViewInput(self, keys)
  1415.     --handling changing the menu width and units:
  1416.     --Capturing the state before it changes:
  1417.     local oldUnit, oldScreen
  1418.     local sameUnit, sameScreen = true, true
  1419.     --Tab changing menu width is handled below.
  1420.     --storing pre-keypress identifiers for unit and viewscreen state
  1421.     if not keys.CHANGETAB then
  1422.         oldUnit = df.global.ui_selected_unit
  1423.         oldScreen = dfhack.gui.getFocusString(viewscreen)
  1424.         --Merely checking viewscreen match will not work, given that sideview only modifies existing screen
  1425.     end
  1426.  
  1427.     baseonInput(self,keys) --Doing baseline housekeeping and passing input to parent
  1428.  
  1429.     --Finding out if anything changed after parent got the input
  1430.     if not keys.CHANGETAB then
  1431.         sameUnit = (oldUnit == df.global.ui_selected_unit)
  1432.         --could also use dfhack.gui.getSelectedUnit()
  1433.         sameScreen = (oldScreen == dfhack.gui.getFocusString(viewscreen))
  1434.     end
  1435.  
  1436.     if keys.CHANGETAB then
  1437.         --Tabbing moves around the sideview, so got to readjust screen position.
  1438.         view_rect.x = -30 -(screenconstructor.isWideView() and 24 or 0)
  1439.         self:adjustDims(true, view_rect.x)
  1440.         --unlike text tables, position tables aren't dynamic, to allow them to be incomplete
  1441.     end
  1442.  
  1443.     --If unit changed, got to replace the indicator text
  1444.     if not sameUnit then
  1445.         writeoverTable(symbolizedSingle,getViewUnitPairs(df.global.world.units.active[df.global.ui_selected_unit]))
  1446.     elseif not sameScreen then
  1447.         --Don't want to display the screen if there isn't an unit present, but don't want to spam blinks either
  1448.         if not dfhack.gui.getFocusString(viewscreen)
  1449.                 :find("dwarfmode/ViewUnits/Some/General") then
  1450.             --Different screen doesn't mean it's different in same way - need to check here too.
  1451.             self:removeFromView()
  1452.         else
  1453.             --It's general, so better fix it...Thoug well - should change mostly nothing
  1454.             self:adjustDims(true, view_rect.x, view_rect.y, view_rect.width, view_rect.height)
  1455.             self.signature = true
  1456.         end
  1457.     end
  1458.  
  1459. end
  1460.  
  1461. newscreen.onInput = onViewInput
  1462.  
  1463.     end
  1464. if dfhack.gui.getCurFocus():find("dwarfmode/ViewUnits/Some")
  1465.     then viewunit()
  1466.     else dfhack.timeout(2, "frames", function()
  1467.          if getBottomMostViewscreenWithFocus("dwarfmode/ViewUnits/Some", df.global.gview.view.child) then
  1468.              viewunit()
  1469.          end
  1470.     end)
  1471.     end
  1472.     end
Advertisement
RAW Paste Data Copied
Advertisement