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