Advertisement
Guest User

[Dwarf Fortress] Fleeting Frames' relationsindicator v 1.15

a guest
Aug 2nd, 2020
343
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 69.03 KB | None | 0 0
  1. local helptext = [=[
  2. relations-indicator
  3. ===================
  4.        v1.15
  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.value:find("CE_ADD_TAG") and (text.value:find("STERILE") or
  324.                        text.value: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.                     elseif spouse.unit_id > -1 and getFlag(spouse,"info") and getFlag(spouse.info, "whereabouts") then
  389.                     if df.global.ui.site_id==spouse.info.whereabouts.site then
  390.                     spouseid = spouse.unit_id
  391.                     spouse = df.unit.find(spouseid)
  392.                     if not spouse then spouse = false end
  393.                     end
  394.                    
  395.                 end
  396.                 -- small distinction between nil and false: is it better to have loved and lost, or never loved?
  397.             elseif (relation._type == df.histfig_hf_link_loverst) then
  398.                 lover=df.historical_figure.find(relation.target_hf)
  399.                 if not lover then lover = false
  400.                 elseif lover.unit_id > -1 and getFlag(lover,"info") and getFlag(lover.info, "whereabouts") then
  401.                     if df.global.ui.site_id==lover.info.whereabouts.site then
  402.                     loverid = lover.unit_id
  403.                     end
  404.                 end
  405.             end
  406.         end
  407.     end
  408.     if loverid > -1 then
  409.         lover=df.unit.find(loverid) --can be nil for offsite lover
  410.         if not lover then lover = false end --false instead of nil to indicate having checked
  411.     end
  412.     if not lover and historical_unit then
  413.         --No local lover? Maybe lover is global
  414.         unithistoricalrelations = historical_unit.histfig_links
  415.         for index, relation in pairs(unithistoricalrelations) do
  416.             if (relation._type == df.histfig_hf_link_loverst) then
  417.                 lover=df.historical_figure.find(relation.target_hf)
  418.                 if not lover then lover = false
  419.                     elseif lover.unit_id > -1 and getFlag(lover,"info") and getFlag(lover.info, "whereabouts") then
  420.                     if df.global.ui.site_id==lover.info.whereabouts.site then
  421.                     loverid = lover.unit_id
  422.                     end
  423.                 end
  424.             end
  425.         end
  426.     end
  427.     return spouse, lover
  428. end
  429.  
  430. function areCompatible(unitA,unitB)
  431.     --Checks if two local units make compatible pair and returns true if they are, false if not
  432.     --Utilizes getFlag, requires them to be historical to check relationships
  433.  
  434.     -- Lets check if one of them is married.
  435.     -- If they are, can do hanky panky with spouse alone
  436.     local spouseA, loverA = getSpouseAndLover(unitA)
  437.     local spouseB, loverB = getSpouseAndLover(unitB)
  438.     if spouseA or loverA or
  439.        spouseB or loverB then
  440.        if spouseA == unitB or
  441.           loverA == unitB then
  442.           return true
  443.        else
  444.           return false
  445.        end
  446.     end
  447.  
  448.     -- Do I check if one is a child?
  449.     -- I think not. Arranged marriages can be planned a decade before they happen.
  450.     -- Still, age is most common disqualifying factor
  451.     if unitA.race ~= unitB.race then return false end
  452.         --multi-racial fortress are nice, but humans still can't into dwarves; not like this.
  453.     local is4305p = getFlag(unitA, "relationship_ids") and true or false
  454.     local relationshipsA = is4305p and unitA or unitA.relations
  455.     local relationshipsB = is4305p and unitB or unitB.relations
  456.         --age is stored in relations for 4303 but on base level in 43.05, so...
  457.     local ageA = relationshipsA.birth_year+relationshipsA.birth_time/403200
  458.     local ageB = relationshipsB.birth_year+relationshipsB.birth_time/403200
  459.         --exact age matters
  460.     if (ageA-ageB) > 10 or (ageB-ageA) > 10 then
  461.             --over 10 year age difference
  462.         return false
  463.     end
  464.  
  465.     --Lets check if they have compatible orientations for marrying each other.
  466.     local attractionA = getAttraction(unitA)
  467.     local attractionB = getAttraction(unitB)
  468.     if not ((2 == attractionA[(unitB.sex == 1 and "guy" or "girl")]) and
  469.             (2 == attractionB[(unitA.sex == 1 and "guy" or "girl")])) then
  470.             -- Admittedly, this means that someone who only romances has no suitable pairings.
  471.         return false
  472.     end
  473.  
  474.     --Lets check if personalities are compatible.
  475.     local unwillingToFriendA, unwillingToLoveA, unwillingToMarryA = getAromantism(unitA)
  476.     local unwillingToFriendB, unwillingToLoveB, unwillingToMarryB = getAromantism(unitB)
  477.     if  unwillingToFriendA or unwillingToLoveA or unwillingToMarryA or
  478.         unwillingToFriendB or unwillingToLoveB or unwillingToMarryB then
  479.         --If either one as baggage about progressing through a relationship, no babies
  480.       return false
  481.     end
  482.     --Checking for relationships requires digging into historical unit values.
  483.     local hfA = unitA.hist_figure_id > -1 and df.historical_figure.find(unitA.hist_figure_id) or nil
  484.     local hfB = unitB.hist_figure_id > -1 and df.historical_figure.find(unitB.hist_figure_id) or nil
  485.     if hfA and hfB then --basic sanity check.
  486.         -- Function to check for being a sibling.
  487.         -- Half-siblings...Possible with hacking, and I bet they block
  488.         function gethfParent(hfunit, retrieveMother)
  489.             --Returns historical mother or father of a historical unit if possible
  490.             --otherwise returns nil
  491.             for index, relationship_link in pairs(hfunit.histfig_links) do
  492.                 if retrieveMother and relationship_link._type == df.histfig_hf_link_motherst or
  493.                     (not retrieveMother and relationship_link._type == df.histfig_hf_link_fatherst) then
  494.                     return df.historical_figure.find(relationship_link.target_hf)
  495.                 end
  496.             end
  497.         end
  498.  
  499.         local momA = gethfParent(hfA, true)
  500.         local momB = gethfParent(hfB, true)
  501.         local dadA = gethfParent(hfA)
  502.         local dadB = gethfParent(hfB)
  503.         if     momA and momB and momA == momB or --existence of moms must be checked since nil == nil
  504.             (dadA and dadB and dadA == dadB) then
  505.             --siblings or half-siblings are not allowed
  506.             return false
  507.         end
  508.  
  509.         --Function to check for grudge:
  510.         -- (As it is not used outside parent function, not encapsulating elsewhere despite size)
  511.         -- temporarily disabled in 47
  512.        if (hfB.info.relationships and getFlag(hfB.info.relationships,"list")) or (hfA.info.relationships and getFlag(hfA.info.relationships,"list")) then
  513.         function hasGrudgeTowards(hfUnitChecked, hfUnitFuckThisCreatureInParticular)
  514.             -- print("Checking for grudge between " .. dfhack.TranslateName(hfUnitChecked.name) .. " and " .. dfhack.TranslateName(hfUnitFuckThisCreatureInParticular.name))
  515.             -- Triple-loops checking info.relationships.list[#].anon_3[#].
  516.             -- Admittedly, doing this repeatedly for every unit is inefficient.
  517.             -- Better would be finding all grudges in fortress at start and cross-checking that.
  518.           if hfUnitChecked.info.relationships then
  519.             --Invaders, for instance, may have it absent
  520.             --Though I wonder if it is even possible to marry off invaders, even after peace settlement
  521.             for index, relationship in pairs (hfUnitChecked.info.relationships.list) do
  522.                 if hfUnitFuckThisCreatureInParticular.id == relationship.histfig_id then
  523.                     --Found a relationship between the two units. Now for grudge!
  524.                     local attitude
  525.                     if getFlag(relationship,'anon_3') ~= nil then attitude = relationship.anon_3 else attitude = relationship.attitude end
  526.                     for feelingindex, feelingtype in pairs(attitude) do
  527.                         --A dwarf can have multiple feelings/relationship types with someone.
  528.                         if feelingtype == 2 then
  529.                             --[[List of options I've noticed with changing that value:
  530.                                 0: Hero
  531.                                 1: Friend
  532.                                 2: Grudge
  533.                                 3: Bonded
  534.                                 6: Good for Business
  535.                                 7: Friendly Terms? (unsure)
  536.                                 10: Comrade
  537.                                 17: Loyal Soldier
  538.                                 18: Considers Monster (hm, could be interesting RP-wise)
  539.                                 26: Protector of the Weak
  540.  
  541.                                 Others seemed to default to Friendly terms
  542.                                 with just few points on 7 as second relation.
  543.  
  544.                                 Perhaps anon_1 and anon_5 may also matter.
  545.                                 --]]
  546.                             return true
  547.                         end
  548.                     end
  549.                     --Found unit without grudge.
  550.                     attitude = nil
  551.                     return false
  552.                 end
  553.             end
  554.           end
  555.         end
  556.  
  557.         if hasGrudgeTowards(hfA, hfB) or
  558.             hasGrudgeTowards(hfB, hfA) then
  559.             --Either one having a grudge? Welp, no marriage for you two.
  560.             return false
  561.         end
  562.        end
  563.     end
  564.     -- No other disqualifing factors? It's a go.
  565.  
  566.     return true
  567. end
  568.  
  569. function getInclinationIfPresent(unit, inclinationnumber)
  570.     --takes ensouled unit and numerical index of inclination
  571.     --returns the value of inclination or 0 or -1000 in case of divorce from local and seeing the world.
  572.     -- utilizes getFlag
  573.     local values
  574.     if getFlag(unit,"status") then
  575.         values = unit.status.current_soul.personality.values
  576.     elseif getFlag(unit.info,"personality") then
  577.         --can be nil for local units who have never updated their hfunit
  578.         --comes up in the case of divorce.
  579.         values = unit.info.personality.values
  580.     else
  581.         return -1000 --buggy placeholder: divorced partners are super-incapable of progressing their relationship.
  582.     end
  583.     -- Do need to check hfunits, since both parties of a ship must be willing to embark on the waters of marriage
  584.     for index, value in pairs(values) do
  585.         if value.type == inclinationnumber then
  586.             return value.strength
  587.         end
  588.     end
  589.     return 0
  590. end
  591.  
  592. function getAttraction(unit)
  593.     --unit can't be nil. The nothingness where there should be something doesn't have sex, y'see.
  594.     --Outputs a table of levels of sexual attraction an unit has.
  595.     -- utilizes getFlag
  596.  local attraction = {guy = 0, girl = 0}
  597.  local orientation
  598.  if unit.sex~=-1 then
  599.    if getFlag(unit, "status") then
  600.     --local unit
  601.     orientation= getFlag(unit.status, "current_soul") and unit.status.current_soul.orientation_flags or false
  602.         --alas, creatures can be soulless.
  603.   else
  604.     --historical unit
  605.     orientation = unit.orientation_flags
  606.  
  607.   end
  608.  end
  609.  if orientation then
  610.   if orientation.romance_male then
  611.     attraction.guy = 1
  612.   elseif orientation.marry_male then
  613.     attraction.guy = 2
  614.   end
  615.   if orientation.romance_female then
  616.     attraction.girl = 1
  617.   elseif orientation.marry_female then
  618.   attraction.girl = 2
  619.   end
  620.  end
  621.  return attraction
  622. end
  623.  
  624. -- ======================================== --
  625. --               Tier 3  functions          --
  626. -- ======================================== --
  627.  
  628. function getAromantism(unit)
  629.     --Takes local unit
  630.     --returns following series of values
  631. local unwillingToFriend, unwillingToLove, unwillingToMarry
  632.     --utilizes getFlag, getIncinationIfPresent
  633.     --utilizes these internally:
  634. local smittedness, friendly, bonding
  635.  
  636. --failure conditions : hating friendship and having no eligble friends (not certain, might be enough to just have one-sided relation), hating romance, hating marriage.
  637. -- unit.status.current_soul.personality.values.
  638. -- Type 29: romance, type 2: family, type 3: friendship, type 18: harmony.
  639. -- hfunit.info.personality.values - nil for embark dwarves, present on visitors, like visitors can have nil spouse relation but histfig value set
  640. -- poults born on-site from embark turkeys don't have hfid or more than 0 values.
  641. -- 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)
  642. -- unknown: Type 3 - friendship, .LUST_PROPENSITY, FRIENDLINESS
  643. -- unknown: how much traits must differ in general for marriage to happen instead of a grudge.
  644. -- also, grudges could be prevented or perhaps later removed by social skills, shared skills and preferences.
  645.  
  646.  
  647. --local smittedness, friendly, bonding, unwillingToFriend, unWillingToLove, unwillingToMarry
  648.  
  649. smittedness = unit.status.current_soul.personality.traits.LOVE_PROPENSITY
  650.     -- again, always present on units that are on-site; but obviously histfigs will have to be handled differently
  651.  
  652. friendly =unit.status.current_soul.personality.traits.FRIENDLINESS
  653. --FRIENDLINESS. I think I've seen ever dyed-in-the-wool quarrels have friends.
  654.  
  655. bonding = unit.status.current_soul.personality.traits.EMOTIONALLY_OBSESSIVE -- how easily they make connections
  656. -- per Sarias's research in 2013, dwarves must be friends before becoming lovers.
  657. -- local cheerfulness -- happier dwarves make relationships more easily, buuut everyone is at -99999 anyway
  658. -- local lustfulness -- science required to see if it affects whether relationship can form
  659. -- local trust -- relationships should be about trust, but lying scumbags can get married too irl. Who knows?
  660. --     Eventually, for specific units would have to check how well they match up and return granual value.
  661.  
  662. unwillingToFriend =(getInclinationIfPresent(unit, 3)+friendly+bonding) < friendlinessthreshold and true or false
  663. -- avg 100, min -50, max 250
  664. -- currently requires ~roughly two lowest possible and 1 second lowest possible values.
  665. -- Starting seven do have friends, which can override this.
  666.  
  667. -- While I've failed to friend off dwarves due personality,
  668. -- those dwarves have managed friendships with at least 1 other person,
  669. -- and several times have managed a marriage with someone else.
  670. -- 18-year old Erush ShoduksÇŽkzul has 3 friends, having 1 lower FRIENDSHIP, bonding
  671.  
  672. unwillingToLove = (getInclinationIfPresent(unit, 29)+smittedness) < loversthreshold and true or false
  673.     --not using bonding, maybe should. They already have emotional bond with others, though.
  674.     --50 is average. 14 might be too low, allowing second lowest value on 1 with other average
  675.     --20 is maximum for non-mentioned propensity and hating even the idea of romance, but can sometimes prevent two 1 worse than average values
  676.  
  677.     -- What numercal indicators can I fit into 1 tile, anyway? Blinking, I guess. TWBT would enable gradual color
  678.     -- blinking has problems at low fps, but viewing unit and unit list have game paused.
  679.     -- Tests should be done with 50 fps/10 gfps, since those are lowest maximums I know of.
  680.     -- 30 GFPS can blend-ish, but 10 is blinky. Maybe use -blinking_at input with default value only_if_above_29
  681.     -- should check if it blinks on returning to low fps.
  682. unwillingToMarry = getInclinationIfPresent(unit, 2) < marriagethreshold and true or false
  683.     --as long as they don't find family loathsome, it's a-ok.
  684.  
  685. return unwillingToFriend, unwillingToLove, unwillingToMarry
  686. end
  687.  
  688. function getSpouseAndLoverLink(unit)
  689.     -- Currently takes local unit only
  690.     -- Returns eight values: spouse and lover names, spouse and lover sites, spouse and lover aliveness, spouse and lover gender
  691.     -- Doesn't check the hf relationships if the hf_only unit doesn't have a spouse in histfig links
  692.     -- might be an issue if a visitor comes on map, romances someone, and then leaves. Never heard of that happening, but hey
  693.     -- utilizes getFlag, getSpouseAndLover, dfhack.units.isAlive, dfhack.TranslateName, df.historical_figure.find
  694.     local spouse,lover
  695.     local spousesite, loversite = df.global.ui.site_id, df.global.ui.site_id
  696.         --blanket current site, unless I assign them differently later
  697.         --this is fine as I have indicator for off-site spouse, not on-site spouse
  698.     local spouselives, loverlives
  699.     spouse, lover = getSpouseAndLover(unit)
  700.     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
  701.     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
  702.     local spousename, lovername
  703.     if spouse == false then
  704.         for index, relation in pairs(df.historical_figure.find(unit.hist_figure_id).histfig_links) do
  705.             --Spouse has been culled? Maybe they're single parent.
  706.             if (relation._type == df.histfig_hf_link_childst) then
  707.                 spousename = "Single parent"
  708.                 spouselives = true    --More like that they're not dead. Visual thing.
  709.             end
  710.         end
  711.     elseif spouse then
  712.         spousename = dfhack.TranslateName(spouse.name)
  713.         -- here, as spouse without name doesn't have gender either
  714.         if getFlag(spouse, "flags1") then --local unit
  715.             spouselives = dfhack.units.isAlive(spouse)
  716.         else
  717.             spouselives = spouse.died_year < 0
  718.         end
  719.     end
  720.     if lover then
  721.         lovername = dfhack.TranslateName(lover.name)
  722.         if getFlag(lover, "flags1") then --local unit
  723.             loverlives = dfhack.units.isAlive(lover)
  724.         else
  725.             loverlives = lover.died_year < 0
  726.         end
  727.     end
  728.     -- lovers can't have children, so it's entirely pointless to speak of lost love.
  729.     return spousename, lovername, spousesite, loversite, spouselives, loverlives, spouse, lover
  730. end
  731.  
  732. function getSymbolizedAnimalPreference(unit, unwrapped)
  733.     --Returns symbolized pregnancy if animal is pregnant.
  734.     --Else, returns gender symbol, color: string and number
  735.         --color is a function whose return value blinks between modes if appropriate.
  736.     --unwrapped doesn't pass the colors through blinkergenerator.
  737.     --utilizes getAttraction, isItBird, isItGelded
  738.     local attraction = getAttraction(unit)
  739.     local symbolColor = {text, color}
  740.     local prefColorTable
  741.     if getPregnancyTimer(unit) > 0 then
  742.         symbolColor.text = isItBird(unit) and fertileBirdSymbol or pregnantCreatureSymbol
  743.         symbolColor.color = pregnantColor
  744.     else
  745.         symbolColor.text = unit.sex == 1 and
  746.                             singleMaleSymbol or
  747.                             ( unit.sex == 0 and
  748.                             singleFemaleSymbol or "")
  749.         if unit.sex == -1 or --flesh balls
  750.            (attraction.guy == 0 and attraction.girl == 0) or --asexual
  751.            isItGelded(unit) then --some tomcats
  752.             symbolColor.color = infertileColor
  753.             return symbolColor
  754.             --strictly speaking, not necessary, due light gray being default color
  755.         end
  756.         prefColorTable = {}
  757.         if unit.sex == 0 then
  758.             if attraction.guy > 0 then table.insert(prefColorTable, straightFemaleShade) end
  759.             if attraction.girl > 0 then table.insert(prefColorTable, gayFemaleShade) end
  760.         else
  761.             if attraction.girl > 0 then table.insert(prefColorTable, straightMaleShade) end
  762.             if attraction.guy > 0 then table.insert(prefColorTable, gayMaleShade) end
  763.         end
  764.     end
  765.     if unwrapped then
  766.         if prefColorTable and #prefColorTable > 0 then
  767.         symbolColor.color = prefColorTable
  768.         end
  769.     return symbolColor
  770.     else
  771.     if prefColorTable then symbolColor.color = blinkergenerator(prefColorTable) end
  772.     if getPregnancyTimer(unit) > 0 then
  773.         symbolColor.onhovertext = {
  774.         color = pregnantColor,
  775.         text = tostring(math.floor((isItSmart(unit) and 9 or 6) -getPregnancyTimer(unit)/33600))
  776.         }
  777.     end
  778.     return symbolColor
  779.     end
  780. end
  781.  
  782. function getSymbolizedSpouse(unit)
  783. -- Currently takes local unit only
  784. -- Returns {} with text and color which are string or function and number or function
  785. -- utilizes getSpouseAndLoverLink, getAttraction, getInclinationIfPresent, isItSmart,isItBird, isItGelded
  786. local spousename, lovername, spousesite, loversite, spouselives, loverlives, spouse, lover = getSpouseAndLoverLink(unit)
  787. local symbolColor = {text, color}
  788.  
  789.  
  790. local attraction = getAttraction(unit)
  791.     --plain sexual attraction table
  792.     --it'd be more compact to code if instead of guy and girl values would use 0 and 1 indices
  793.     --could call attraction[unit.sex] == 2 to see if they're willing to engage in gay marriage, for example
  794.     --however, code is more self-explanatory with variables having names that explain what they're for
  795. local unwillingToFriend, unwillingToLove, unwillingToMarry
  796. if (attraction.guy+attraction.girl) == 0 then
  797.   unwillingToFriend, unwillingToLove, unwillingToMarry = true, true, true
  798. else
  799.   unwillingToFriend, unwillingToLove, unwillingToMarry = getAromantism(unit)
  800. end
  801.     --series of disqualifying boolean values; though if orientation is already zero better not check
  802.  
  803. local symbolTable = {}
  804. local colorTable = {}
  805. if getPregnancyTimer(unit) > 0 and
  806.     isItSmart(unit) then --necessary due otherwise doubling up on the indicator with animal preferences.
  807.     --Normally, would lose nothing by having pregnancy highest hiearchy, could just check first and skip the above
  808.     --However, in cases of modding or father dying in battle (such as in fucduck's elven outpost) info is lost
  809.     if isItBird(unit) then
  810.         --possible with bird-women adventurers joining the fortress
  811.     table.insert(symbolTable,fertileBirdSymbol)
  812.     else
  813.     table.insert(symbolTable,pregnantCreatureSymbol)
  814.     end
  815.     table.insert(colorTable,pregnantColor)
  816. end
  817.  
  818. if not isItSmart(unit) then --it's an animal
  819.     local animalprefs = getSymbolizedAnimalPreference(unit,true)
  820.     table.insert(symbolTable, animalprefs.text)
  821.     if type(animalprefs.color) == "table" then
  822.     table.insert(colorTable, animalprefs.color[0])
  823.     table.insert(colorTable, animalprefs.color[1]) --two gender prefs at most.
  824.     table.insert(symbolTable, animalprefs.text) --going to mess up timing otherwise.
  825.     else
  826.     table.insert(colorTable, animalprefs.color)
  827.     end
  828. end
  829. if not lovername and
  830.     (not spousename or spousename == "Single parent") then
  831.     if isItSmart(unit) then --otherwise already handled earlier, don't need to add anything.
  832.         table.insert(symbolTable,
  833.         ((unit.sex == 0) and singleFemaleSymbol or (unit.sex == 1 and singleMaleSymbol or "")))
  834.         --creatures without gender don't get a symbol.
  835.         if (attraction.guy == 0 and attraction.girl == 0) or --asexual
  836.             isItGelded(unit) or -- gelded. Aw.
  837.             unwillingToLove or --aromantic. Requires soul.
  838.             sex == -1 then --creatures like iron men and bronze colossi indicate that genderless creatures can't breed
  839.  
  840.             table.insert(colorTable, infertileColor)
  841.  
  842.         else
  843.             if unit.sex == 0 then
  844.                 if attraction.girl > 0 then
  845.                      table.insert(colorTable,
  846.                      (gayFemaleShade - 8*( (unwillingToMarry or attraction.girl == 1) and 1 or 0  )))
  847.                     --darker shade for lover-only relationships
  848.                 end
  849.                 if attraction.guy > 0 then
  850.                      table.insert(colorTable,
  851.                      (straightFemaleShade - 8*( (unwillingToMarry or attraction.guy == 1) and 1 or 0  )))
  852.                 end
  853.             else
  854.                 if attraction.girl > 0 then
  855.                      table.insert(colorTable,
  856.                      (straightMaleShade - 8*( (unwillingToMarry or attraction.girl == 1) and 1 or 0  )))
  857.                 end
  858.                 if attraction.guy > 0 then
  859.                      table.insert(colorTable,
  860.                      (gayMaleShade - 8*( (unwillingToMarry or attraction.guy == 1) and 1 or 0  )))
  861.                 end
  862.             end
  863.              if #colorTable>#symbolTable and    --Our table has more colors than symbols. Noprobs,
  864.                 #symbolTable>1 then                --unless there's pregnant single present
  865.                 table.insert(symbolTable, symbolTable[#symbolTable])
  866.                 --Pregnant singles screw up timing, unless we double up on last symbol.
  867.             end
  868.         end
  869.     end
  870. else
  871.     if spousename and spousename ~= "Single parent" then
  872.         table.insert(symbolTable, marriedSymbol)
  873.         --table for on-site, alive, not infertile spouse
  874.         -- Using hiearchy:
  875.         -- Dead > Infertile (unless gay) > Offsite > Normal bright color.
  876.         local spousecolor = (unit.sex == 0) and
  877.                             (spouse.sex==1 and straightFemaleShade or gayFemaleShade) or
  878.                             (spouse.sex==0 and straightMaleShade or gayMaleShade)
  879.                             -- live marriage
  880.         spousecolor = df.global.ui.site_id == spousesite and spousecolor or offsiteShade
  881.                             --spouse is offsite. Can have problems with divorcing.
  882.         spousecolor = (isItGelded(unit) or isItGelded(spouse)) and
  883.                         not (unit.sex == spouse.sex)
  884.                         and infertileColor or spousecolor
  885.                         --spouse is infertile and not gay-only marriage
  886.         spousecolor = spouselives and spousecolor or deadColor
  887.         table.insert(colorTable, spousecolor)
  888.     end
  889.     if lovername then
  890.         table.insert(symbolTable, loversSymbol)
  891.         -- Two types of lovers: ones willing to progress to marriage, ones unwilling
  892.         -- The unwilling ones get darker shade
  893.         -- Both parties must be willing
  894.         -- Lovers and spouses may also be off-site, dead or infertile.
  895.         -- While can display all with blinky things, should minimize needless UI churn.
  896.         -- Dead > Infertile (unless gay) > Offsite > Unwilling to progress to marriage > Normal bright color.
  897.         local lovercolor =  (unit.sex == 0) and
  898.                             (lover.sex==0 and gayFemaleShade or straightFemaleShade) or
  899.                             (lover.sex==0 and straightMaleShade or gayMaleShade)
  900.         --baseline is willing to marry
  901.         lovercolor = (attraction[((lover.sex==1) and "guy" or "girl")] < 2 or
  902.               getAttraction(lover)[((unit.sex==1) and "guy" or "girl")] < 2 or
  903.               unwillingToMarry or
  904.               (getInclinationIfPresent(lover, 2) < marriagethreshold and true or false)) and
  905.               (lovercolor - 8) or lovercolor
  906.         -- if the unit or their lover has personality or attraction failure, the relationship will not progress
  907.         lovercolor = df.global.ui.site_id == loversite and lovercolor or offsiteShade
  908.             --lover is offsite. Can have problems with divorcing.
  909.             --Happens mostly in case of visitors or married migrants.
  910.             --Issue: Either a possible false hope of offsite lover eventually arriving.
  911.             --        or a false indicator of lover being on-site.
  912.             --Blinking and blurring could solve this, but not hiearchy.
  913.         lovercolor = (isItGelded(unit) or isItGelded(lover)) and
  914.                 not (unit.sex == lover.sex)
  915.                 and infertileColor or lovercolor
  916.                 --lover is infertile and not gay-only marriage
  917.         lovercolor = loverlives and lovercolor or deadColor
  918.                 --lover is dead
  919.         table.insert(colorTable, lovercolor)
  920.     end
  921.  
  922. end
  923.  
  924. symbolColor.text = blinkergenerator(symbolTable)
  925.  
  926. symbolColor.color = blinkergenerator(colorTable)
  927.  
  928. if getPregnancyTimer(unit) > 0 then
  929.     symbolColor.onhovertext = {
  930.     color = pregnantColor,
  931.     text = tostring(math.floor((isItSmart(unit) and 9 or 6) -getPregnancyTimer(unit)/33600))
  932.     --Needs to be converted to string since #text is called for width
  933.     }
  934. end
  935.  
  936. return symbolColor
  937.  
  938. end
  939.  
  940. function getSpouseText(unit)
  941.     -- Takes local unit, returns single line string that is "" if there's no romantic relation
  942.     local spousename, lovername, spousesite, loversite, spouselives, loverlives = getSpouseAndLoverLink(unit)
  943.    --An unit can have both lover and spouse in vanilla with retirement shenanigans
  944.     local returnstring = ""
  945.     if spousename then
  946.         --spouse matters more so goes first.
  947.         returnstring = returnstring .. marriedSymbol
  948.         if spousesite ~= df.global.ui.site_id then
  949.             returnstring = returnstring .. offsite
  950.         end
  951.         if spouselives then
  952.         returnstring = returnstring .. spousename
  953.         else
  954.         returnstring = returnstring .. diedS .. spousename ..diedE
  955.         end
  956.     end
  957.     if lovername then
  958.         returnstring = returnstring .. loversSymbol
  959.         if loversite ~= df.global.ui.site_id then
  960.             returnstring = returnstring .. offsite
  961.         end
  962.         if loverlives then
  963.         returnstring = returnstring .. lovername
  964.         else
  965.         returnstring = returnstring .. diedS .. lovername ..diedE
  966.         end
  967.     end
  968.  
  969.     return returnstring
  970. end
  971.  
  972. function getSuitableMatches(unit, unitlist, joblist)
  973.     --Takes a local unit, local unitlist and local joblist
  974.     --Returns an unitlist that includes unit and then all it's suitable candidates.
  975.     --And joblist that has only those same indices, as unitlist viewscreen uses that data.
  976.     --utilizes areCompatible
  977. local matchlist, jobmatchlist = {}, {}
  978.     --The unit we've checking always comes first
  979.     for index=0, #unitlist-1 do
  980.         if  (unit == unitlist[index]) or -- self
  981.             areCompatible(unit, unitlist[index]) then --suitable match
  982.           matchlist[index] = true
  983.           jobmatchlist[index] = true
  984.         end
  985.     end
  986.     return matchlist, jobmatchlist
  987. end
  988.  
  989. -- ======================================== --
  990. --      Dynamic text {} output functions    --
  991. -- ======================================== --
  992.  
  993.  
  994. function getViewUnitPairs(unit)
  995.     local returnpairs = {}
  996.     local pregnantRichText = {}
  997.     pregnantRichText.text = getPregnancyText(unit)
  998.     --bit of fluff for pregnancy
  999.     pregnantRichText.color = pregnantColor
  1000.     table.insert(returnpairs, pregnantRichText)
  1001.     --First line is for pregnancy, second line is for spouse/lover
  1002.     local spouseRichText = {}
  1003.         --Also gets lover text
  1004.     spouseRichText.text = getSpouseText(unit)
  1005.     function nabNonPregnantColor(unit)
  1006.         --I want spouse text to be coloured appropriately for the relationship.
  1007.         local previouscolor, returncolor
  1008.         local basecolor = getSymbolizedSpouse(unit).color
  1009.         if type(basecolor) == "number" then
  1010.             return basecolor
  1011.         else
  1012.             previouscolor = getSymbolizedSpouse(unit).color()
  1013.             function returnfunction()
  1014.                 --In case the romantic relationship is blinky - typically that means pregnancy and/or lover.
  1015.                 local unit = unit
  1016.                 returncolor = getSymbolizedSpouse(unit).color()
  1017.                     --Might as well use code already in place
  1018.                 if returncolor == pregnantColor then
  1019.                     --Of course, if the unit is pregnant, that's shown above, not here.
  1020.                     --Visual bug: Can still start out as pregnant color.
  1021.                     return previouscolor
  1022.                 else
  1023.                     previouscolor = returncolor
  1024.                     return returncolor
  1025.                 end
  1026.             end
  1027.         return returnfunction
  1028.         end
  1029.     end
  1030.  
  1031.     if spouseRichText.text then
  1032.         spouseRichText.color = nabNonPregnantColor(unit)
  1033.     end
  1034.     table.insert(returnpairs, spouseRichText)
  1035.     return returnpairs
  1036. end
  1037.  
  1038.     local HavePrinted, oldCitizens, oldJobs, oldIndices = false
  1039. function showUnitPairs(unit)
  1040.     local unitscreen = getBottomMostViewscreenWithFocus("unitlist", df.global.gview.view.child.child)
  1041.   if not HavePrinted then
  1042.     oldCitizens, oldJobs, oldIndices = {}, {}, {}
  1043.     local index = 0
  1044.     while getFlag(unitscreen.units.Citizens, index) do
  1045.         if  (unit == unitscreen.units.Citizens[index]) or -- self
  1046.             areCompatible(unit, unitscreen.units.Citizens[index]) then
  1047.             index = 1+index
  1048.         else
  1049.             table.insert(oldCitizens, unitscreen.units.Citizens[index])
  1050.             table.insert(oldIndices, index)
  1051.             oldJobs[#oldIndices] = unitscreen.jobs.Citizens[index]
  1052.             unitscreen.units.Citizens:erase(index)
  1053.             unitscreen.jobs.Citizens:erase(index)
  1054.         end
  1055.     end
  1056.     HavePrinted = true
  1057.     for ci = 0, #unitscreen.units.Citizens -1 do
  1058.         if (unit == unitscreen.units.Citizens[ci]) then
  1059.             unitscreen.cursor_pos.Citizens = ci
  1060.             break;
  1061.         end
  1062.     end
  1063.   end
  1064. end
  1065.  
  1066. function hideUnitPairs()
  1067.   if HavePrinted then
  1068.     local unitscreen = getBottomMostViewscreenWithFocus("unitlist", df.global.gview.view.child.child)
  1069.     for i=#oldCitizens, 1, -1 do
  1070.         unitscreen.units.Citizens:insert(oldIndices[i], oldCitizens[i])
  1071.         unitscreen.jobs.Citizens:insert(oldIndices[i], oldJobs[i])
  1072.     end
  1073.     HavePrinted = false
  1074.   end
  1075. end
  1076.  
  1077.  
  1078.     local pagelength, currentpage, visitorpopupdims, symbolizedList = df.global.gps.dimy - 9, 0, {x = -30, y = 4}
  1079.     -- pagelength needs to be exposed due manipulators having two lines shorter pages than standard view.
  1080.     -- Also due resizing.
  1081.     -- currentpage needs to be exposed due traversing the lists.
  1082. function getUnitListPairs()
  1083.     local unitscreen = getBottomMostViewscreenWithFocus("unitlist", df.global.gview.view.child.child)
  1084.         --note: Counts from 0, unlike lua tables
  1085.     local returntable = {}
  1086.     local cursorposition, unitlist, iter
  1087.     if unitscreen.page == 0 then --Citizen list
  1088.         cursorposition = unitscreen.cursor_pos.Citizens
  1089.         currentpage = math.floor(cursorposition / pagelength)
  1090.         cursorposition = cursorposition % pagelength --cursor position within a page
  1091.         unitlist = unitscreen.units.Citizens
  1092.         for iter = (0+currentpage*pagelength),
  1093.                 ( (((1+currentpage)*pagelength-1)<(#unitlist -1)) and
  1094.                 ((1+currentpage)*pagelength-1) or
  1095.                 (#unitlist -1)) do
  1096.             table.insert(returntable, getSymbolizedSpouse(unitlist[iter]))
  1097.             returntable[#returntable].onclick = function()
  1098.                   local tile = nil
  1099.                   if dfhack.gui.getCurFocus():find("unitlist") then tile = dfhack.screen.readTile(39,df.global.gps.dimy-2) end
  1100.                     --search plugin support
  1101.                   if not tile or (tile and tile.ch == 95 and tile.fg == 2 or tile.ch == 0) then
  1102.                     if HavePrinted then hideUnitPairs() else
  1103.                         showUnitPairs(unitlist[iter]) end
  1104.                     writeoverTable(symbolizedList, getUnitListPairs())
  1105.                   end
  1106.                 end
  1107.         end
  1108.     elseif unitscreen.page == 1 or unitscreen.page == 2 then --Livestock or Others
  1109.         local pageName = (unitscreen.page == 1) and "Livestock" or "Others"
  1110.         cursorposition = unitscreen.cursor_pos[pageName]
  1111.         currentpage = math.floor(cursorposition / pagelength)
  1112.         cursorposition = cursorposition % pagelength --cursor position within a page
  1113.         unitlist = unitscreen.units[pageName]
  1114.         for iter = (0+currentpage*pagelength),
  1115.                 ( (((1+currentpage)*pagelength-1)<(#unitlist -1)) and
  1116.                 ((1+currentpage)*pagelength-1) or
  1117.                 (#unitlist -1)) do
  1118.             local unit = unitlist[iter]
  1119.             -- What goes on with combination of pet and intelligent?
  1120.             -- Tests reveals failure to bear children and love for even histfigged intelligent dogs
  1121.             -- Perhaps only dumb pets of non-your civ can screw.
  1122.             -- Of course, might want accurate indicator then anyway, as you might make them your civ members
  1123.  
  1124.             --Near as I can tell, for historical figures:
  1125.             --            pet    1    0
  1126.             --    smart    1    -    Fertile
  1127.             --            0    Fer    Fertile(trogs, trolls)
  1128.  
  1129.             if isItSmart(unit) then
  1130.                 if (df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET or
  1131.                     df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET_EXOTIC)
  1132.                     -- Intelligent pets seem unable to breed
  1133.                     or unit.hist_figure_id < 0 then
  1134.                         --I think marriage requires being historical,
  1135.                         -- collaborated by historical turkeys being able to marry during retirement
  1136.                         table.insert(returntable,getGenderInInfertileColor(unit))
  1137.                 end
  1138.                 if unit.hist_figure_id > -1 then
  1139.                   table.insert(returntable, getSymbolizedSpouse(unit))
  1140.                   returntable[#returntable].onclick = function()
  1141.                     --Something to display visitor dating pool
  1142.                     --creates several tables per call, but one doesn't usually call it.
  1143.                     local fortressmatches = getSuitableMatches(unit, unitscreen.units.Citizens, unitscreen.jobs.Others)
  1144.                     --Lets find the candidates for a given visitor
  1145.                     -- this is a table of index, true values, not units.
  1146.                     --    print("entered onclick " .. getLen(fortressmatches))
  1147.                     if getLen(fortressmatches) > 0 then
  1148.                       local fortressmatchlist = {}
  1149.                       for index, value in pairs(fortressmatches) do
  1150.                         table.insert(fortressmatchlist, getSymbolizedSpouse(unitscreen.units.Citizens[index]))
  1151.                         fortressmatchlist[#fortressmatchlist].notEndOfLine = true
  1152.                         table.insert(fortressmatchlist, {text = dfhack.TranslateName(unitscreen.units.Citizens[index].name)})
  1153.                             --Lets convert the unit to nicely colored name to display.
  1154.                       end
  1155.                       --print(#fortressmatchlist)
  1156.                       local popupscreen = screenconstructor.getScreen(
  1157.                         fortressmatchlist,
  1158.                         {x = math.floor((df.global.gps.dimx-screenconstructor.getLongestLength(fortressmatchlist,"text"))/2),
  1159.                          y = math.floor((df.global.gps.dimy-screenconstructor.getHeight(fortressmatchlist))/2)},
  1160.                         {x = math.floor((df.global.gps.dimx-screenconstructor.getLongestLength(fortressmatchlist,"text"))/2 -1),
  1161.                          y = math.floor((df.global.gps.dimy-screenconstructor.getHeight(fortressmatchlist))/2 -1),
  1162.                          width = 2+ screenconstructor.getLongestLength(fortressmatchlist,"text"),
  1163.                          height = 2+ screenconstructor.getHeight(fortressmatchlist)}
  1164.                          )
  1165.                       popupscreen:show()
  1166.                       popupscreen.onInput = function() popupscreen:dismiss() end
  1167.                          --There's no input on which I wont want to dismiss the screen.
  1168.                     end
  1169.                     end
  1170.                 end
  1171.             else
  1172.                 --It doesn't matter if a pet/troglodyte has killed someone or not, they'll breed either way.
  1173.                 if unit.hist_figure_id > -1 then
  1174.                     --Nonetheless, historical pets can marry in retired forts
  1175.                     table.insert(returntable, getSymbolizedSpouse(unit))
  1176.                     --getSymbolizedSpouse calls on below functions for not smart creatures
  1177.                 else
  1178.                 table.insert(returntable, getSymbolizedAnimalPreference(unit))
  1179.                 end
  1180.             end
  1181.  
  1182.             --[[ Deprecated logic based on previously believed data.
  1183.             if unit.hist_figure_id > 0 then
  1184.                 --Can be married. Visitor, murderer, pet...
  1185.                 if isItSmart(unit) and
  1186.                     (df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET or
  1187.                     df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET_EXOTIC) then
  1188.                     --
  1189.                         table.insert(returntable,getGenderInInfertileColor(unit))
  1190.  
  1191.                 end
  1192.                 --Distinction between being smart or not (aka free love) is handled inside.
  1193.                 table.insert(returntable, getSymbolizedSpouse(unit))
  1194.             else
  1195.                 if isItSmart(unit) then
  1196.                         --Wild animal men and non-historical gremlins
  1197.                         --Infertile until they're able to marry - like after having killed someone important
  1198.                         table.insert(returntable,getGenderInInfertileColor(unit))
  1199.                     else
  1200.                         --normal turkeys brought on embark and wild animals.
  1201.                         table.insert(returntable, getSymbolizedAnimalPreference(unit))
  1202.  
  1203.                 end
  1204.             end]]--
  1205.         end
  1206.  
  1207.     end
  1208.     return returntable
  1209. end
  1210.  
  1211.  
  1212. -- ======================================== --
  1213. --        Initialization and placement      --
  1214. -- ======================================== --
  1215.  
  1216. local unitscreen = getBottomMostViewscreenWithFocus("unitlist",df.global.gview.view.child.child)
  1217.                     --gets unitlist viewscreen
  1218. local viewscreen
  1219.     if unitscreen then
  1220. symbolizedList = getUnitListPairs()
  1221. local list_rect = {x = 1, y = 4, width = 1, height = pagelength}
  1222. local newscreen = screenconstructor.getScreen(symbolizedList, list_rect, nil)
  1223. newscreen:show()
  1224. local listtable = {}
  1225. listtable[0] = "Citizens"
  1226. listtable[1] = "Livestock"
  1227. listtable[2] = "Others"
  1228. listtable[3] = "Dead"
  1229. local oldwhichlist,lenlist, doNotUseBase, manitimeout, manicursorpos = listtable[unitscreen.page]
  1230.  
  1231. local manipulatorkeytable = {}
  1232. local upkeys = {CURSOR_UP = true}
  1233. local downkeys = {CURSOR_DOWN = true}
  1234. newscreen.onclick = function()
  1235.   local tile = nil
  1236.   if dfhack.gui.getCurFocus():find("unitlist") then tile = dfhack.screen.readTile(39,df.global.gps.dimy-2) end
  1237.     --search plugin support
  1238.     --if prevents failing to work in manipulator/main
  1239.   if not tile or (tile and tile.ch == 95 and tile.fg == 2 or tile.ch == 0) then
  1240.     if HavePrinted then hideUnitPairs() end --restoring units on random clicks
  1241.     writeoverTable(symbolizedList, getUnitListPairs()) --restoring appearance too
  1242.   end
  1243. end
  1244. local baseonInput = newscreen.onInput
  1245. local function onListInput(self, keys)
  1246.   lenlist = #(unitscreen.units[oldwhichlist])
  1247.  
  1248.     if keys.LEAVESCREEN and 2 == dfhack.screen.readTile(29,df.global.gps.dimy-2).fg then
  1249.         --Search plugin support. Tbh, would have been ultimately easier to disable dismiss_on_zoom
  1250.         local dismissfunc = self.dismiss
  1251.         self.dismiss = function () end
  1252.         dfhack.timeout(1, "frames", function() self.dismiss = dismissfunc end )
  1253.     end
  1254.  
  1255.   if not doNotUseBase then baseonInput(self, keys) end
  1256.     local manipulatorscript = getBottomMostViewscreenWithFocus("dfhack/lua/manipulator",df.global.gview.view.child.child)
  1257.     --Lua manipulator viewscreen is present
  1258.     local whichlist = listtable[unitscreen.page]
  1259.             --duplicated from indicator_screen (ugh). Feels like there should be a way to better determine this.
  1260.  
  1261.     if (currentpage ~= math.floor(unitscreen.cursor_pos[whichlist]/ pagelength) and whichlist ~= "Dead") or
  1262.         --up-down paging
  1263.         (oldwhichlist and oldwhichlist ~= whichlist) or --left-right paging
  1264.         (lenlist ~= #(unitscreen.units[oldwhichlist])) then --search plugin support
  1265.         oldwhichlist = whichlist
  1266.         doNotUseBase = true
  1267.         lenlist = #(unitscreen.units[oldwhichlist])
  1268.         writeoverTable(symbolizedList, getUnitListPairs())
  1269.         dfhack.timeout(2, "frames", function() doNotUseBase = false end)
  1270.             --Something weird happens with writeoverTable here, where it sometimes parses input twice.
  1271.             --In the absence of other solutions, merely avoid relaying input for two frames.
  1272.     end
  1273.  
  1274.     function mv_cursor(keys)
  1275.         -- Function for the purpose of moving cursor alongside the manipulator.
  1276.         -- They don't do this natively - a bugging disrepacy.
  1277.         if keys.CURSOR_UP or keys.CURSOR_DOWN then
  1278.             unitscreen.cursor_pos[whichlist] = --set new cursor position
  1279.                 (unitscreen.cursor_pos[whichlist] +(keys.CURSOR_DOWN and 1 or -1)) --to 1 up or down from previous
  1280.                 % #(unitscreen.units[whichlist])    --with overflow accounted for.
  1281.         end
  1282.     end
  1283.  
  1284.         --manipulator/main.lua scrolls differently than default interface, got to handle it
  1285.         --TODO: fix the mess with numpad keys and manipulator/main.lua
  1286.     if manipulatorscript and manipulatorscript.breakdown_level ~= 2 then
  1287.         --Finds manipulator here on both Alt-L and Escape back out
  1288.         --breakdown level check prevents that.
  1289.       if #(unitscreen.units[whichlist]) > (df.global.gps.dimy -11) then
  1290.       --multi-page manipulator unitlist handling
  1291.         if pagelength ~= lenlist then
  1292.         --Instead of using a sublist that is refreshed manipulator/main uses whole list that is moved
  1293.           pagelength = lenlist
  1294.           writeoverTable(symbolizedList, getUnitListPairs())
  1295.           self:adjustDims(true,nil,nil,nil, pagelength)
  1296.           self.frame_body.clip_y2 = df.global.gps.dimy - 8
  1297.           manicursorpos = unitscreen.cursor_pos[whichlist] > (df.global.gps.dimy - 12) and
  1298.                                 (df.global.gps.dimy - 12) or unitscreen.cursor_pos[whichlist]
  1299.           self.frame_body.y1 = 4 - (unitscreen.cursor_pos[whichlist] > (df.global.gps.dimy - 12) and
  1300.                                     unitscreen.cursor_pos[whichlist] - (df.global.gps.dimy - 12) or
  1301.                                     0)
  1302.         end
  1303.         --scrolling occurs if manipulator's cursor position has reached the edge and tries to keep going.
  1304.         --Manipulator's initial cursor position is either current cursor position or bottom, whichever is smaller
  1305.         --Successive changes can divorce the two, so need to have internal check.
  1306.  
  1307.         --Two functions to follow the cursor position of manipulator
  1308.         function manidown()
  1309.             if manicursorpos ~= (df.global.gps.dimy - 12) then
  1310.                 manicursorpos = 1 + manicursorpos
  1311.             elseif manicursorpos == (df.global.gps.dimy - 12) then
  1312.                 if (unitscreen.cursor_pos[whichlist] +1 ) ~= #(unitscreen.units[whichlist]) then
  1313.                 self.frame_body.y1 = -1 + self.frame_body.y1
  1314.                 else
  1315.                 self.frame_body.y1 = 4
  1316.                 manicursorpos = 0
  1317.                 end
  1318.             end
  1319.         end
  1320.         function maniup()
  1321.             if manicursorpos ~= 0 then
  1322.                 manicursorpos = -1 + manicursorpos
  1323.             elseif manicursorpos == 0 then
  1324.                 if unitscreen.cursor_pos[whichlist] ~= 0 then
  1325.                     self.frame_body.y1 = 1 + self.frame_body.y1
  1326.                 else
  1327.                     self.frame_body.y1 =  (df.global.gps.dimy -7) - #(unitscreen.units[whichlist])
  1328.                     manicursorpos = (df.global.gps.dimy - 12)
  1329.                 end
  1330.             end
  1331.         end
  1332.  
  1333.         --manipulator/main allows shift+up/down scrolling, which has unique behaviour
  1334.         if hasKeyOverlap(keys, downkeys) then
  1335.             if not hasKeyOverlap(keys, {["_FAST"] = true}) then
  1336.                 manidown()
  1337.             else
  1338.                 for i=1, 10 do
  1339.                     if (unitscreen.cursor_pos[whichlist] +1 ) ~= #(unitscreen.units[whichlist]) then
  1340.                         manidown()
  1341.                         mv_cursor(downkeys)
  1342.                     else
  1343.                         if i == 1 then
  1344.                             manidown()
  1345.                             mv_cursor(downkeys)
  1346.                         end
  1347.                         break;
  1348.                     end
  1349.                 end
  1350.             end
  1351.         end
  1352.         if hasKeyOverlap(keys, upkeys) then
  1353.             if not hasKeyOverlap(keys, {["_FAST"] = true}) then
  1354.                 maniup()
  1355.             else
  1356.                 for i=1, 10 do
  1357.                     if unitscreen.cursor_pos[whichlist] ~= 0 then
  1358.                         maniup()
  1359.                         mv_cursor(upkeys)
  1360.                     else
  1361.                         if i == 1 then
  1362.                             maniup()
  1363.                             mv_cursor(upkeys)
  1364.                         end
  1365.                         break;
  1366.                     end
  1367.                 end
  1368.             end
  1369.         end
  1370.       end
  1371.             mv_cursor(keys) --adjust outside cursor, does nothing on shift-scrolling
  1372.         if df.global.gps.mouse_x == 1 and --clicked on the indicator line
  1373.             (keys._MOUSE_L or keys._MOUSE_L_DOWN) then
  1374.                 if manipulatorscript and not manitimeout then
  1375.                     manitimeout = true
  1376.                     --timeout necessary due otherwise causing errors with multiple rapid commands
  1377.                     self._native.parent.breakdown_level = 2
  1378.                     self._native.parent.parent.child = self._native.parent.child
  1379.                     self._native.parent = self._native.parent.parent
  1380.                     dfhack.timeout(2, "frames", function()
  1381.                     dfhack.run_command("gui/indicator_screen execute_hook manipulator/main")
  1382.                     end)
  1383.                     dfhack.timeout(2, "frames", function() manitimeout = false end)
  1384.                 end
  1385.         end
  1386.     elseif  manipulatorscript and manipulatorscript.breakdown_level == 2 then
  1387.         if keys.LEAVESCREEN then
  1388.             hideUnitPairs()
  1389.             dfhack.run_command("relations-indicator")
  1390.         end
  1391.         if keys.UNITJOB_ZOOM_CRE then
  1392.         dfhack.timeout(4, "frames", function() dfhack.run_command("relations-indicator") end)
  1393.         end
  1394.     end
  1395. end
  1396. newscreen.onInput = onListInput
  1397.  
  1398. local baseOnResize = newscreen.onResize
  1399.  
  1400. function onListResize(self)
  1401.     -- Unlike with View-unit, the data might change depending on the size of the window.
  1402.     baseOnResize(self)
  1403.     if pagelength ~= (df.global.gps.dimy - 9) then
  1404.         --If window length changed, better refresh the data.
  1405.         pagelength = df.global.gps.dimy - 9
  1406.         writeoverTable(symbolizedList, getUnitListPairs())
  1407.         self:adjustDims(true,list_rect.x, list_rect.y,list_rect.width, pagelength)
  1408.         --Not adjusting height here would result in situation where making screen shorter works, but taller not.
  1409.     end
  1410. end
  1411.  
  1412. newscreen.onResize = onListResize
  1413.  
  1414. -- ======================================== --
  1415. --              View-unit section           --
  1416. -- ======================================== --
  1417.  
  1418.  
  1419.     else
  1420. local function viewunit()
  1421. viewscreen = dfhack.gui.getCurViewscreen()
  1422. local unit = dfhack.gui.getSelectedUnit()
  1423. local symbolizedSingle = getViewUnitPairs(unit)
  1424. local view_rect = {x = (-30 -(screenconstructor.isWideView() and 24 or 0)), y = 17, width = 28, height = 2}
  1425. local newscreen = screenconstructor.getScreen(symbolizedSingle, view_rect, nil)
  1426. newscreen:show()
  1427. if not dfhack.gui.getFocusString(viewscreen)
  1428.         :find("dwarfmode/ViewUnits/Some/General") then
  1429.     --Can enter in inventory view with v if one previously exited inventory view
  1430.     newscreen:removeFromView() --Gotta hide in that case
  1431. end
  1432. local baseonInput = newscreen.onInput
  1433. local function onViewInput(self, keys)
  1434.     --handling changing the menu width and units:
  1435.     --Capturing the state before it changes:
  1436.     local oldUnit, oldScreen
  1437.     local sameUnit, sameScreen = true, true
  1438.     --Tab changing menu width is handled below.
  1439.     --storing pre-keypress identifiers for unit and viewscreen state
  1440.     if not keys.CHANGETAB then
  1441.         oldUnit = df.global.ui_selected_unit
  1442.         oldScreen = dfhack.gui.getFocusString(viewscreen)
  1443.         --Merely checking viewscreen match will not work, given that sideview only modifies existing screen
  1444.     end
  1445.  
  1446.     baseonInput(self,keys) --Doing baseline housekeeping and passing input to parent
  1447.  
  1448.     --Finding out if anything changed after parent got the input
  1449.     if not keys.CHANGETAB then
  1450.         sameUnit = (oldUnit == df.global.ui_selected_unit)
  1451.         --could also use dfhack.gui.getSelectedUnit()
  1452.         sameScreen = (oldScreen == dfhack.gui.getFocusString(viewscreen))
  1453.     end
  1454.  
  1455.     if keys.CHANGETAB then
  1456.         --Tabbing moves around the sideview, so got to readjust screen position.
  1457.         view_rect.x = -30 -(screenconstructor.isWideView() and 24 or 0)
  1458.         self:adjustDims(true, view_rect.x)
  1459.         --unlike text tables, position tables aren't dynamic, to allow them to be incomplete
  1460.     end
  1461.  
  1462.     --If unit changed, got to replace the indicator text
  1463.     if not sameUnit then
  1464.         writeoverTable(symbolizedSingle,getViewUnitPairs(df.global.world.units.active[df.global.ui_selected_unit]))
  1465.     elseif not sameScreen then
  1466.         --Don't want to display the screen if there isn't an unit present, but don't want to spam blinks either
  1467.         if not dfhack.gui.getFocusString(viewscreen)
  1468.                 :find("dwarfmode/ViewUnits/Some/General") then
  1469.             --Different screen doesn't mean it's different in same way - need to check here too.
  1470.             self:removeFromView()
  1471.         else
  1472.             --It's general, so better fix it...Thoug well - should change mostly nothing
  1473.             self:adjustDims(true, view_rect.x, view_rect.y, view_rect.width, view_rect.height)
  1474.             self.signature = true
  1475.         end
  1476.     end
  1477.  
  1478. end
  1479.  
  1480. newscreen.onInput = onViewInput
  1481.  
  1482.     end
  1483. if dfhack.gui.getCurFocus():find("dwarfmode/ViewUnits/Some")
  1484.     then viewunit()
  1485.     else dfhack.timeout(2, "frames", function()
  1486.          if getBottomMostViewscreenWithFocus("dwarfmode/ViewUnits/Some", df.global.gview.view.child) then
  1487.              viewunit()
  1488.          end
  1489.     end)
  1490.     end
  1491.     end
  1492.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement