Advertisement
Guest User

[Dwarf Fortress] Fleeting Frames' relationsindicator v 1.1

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