View difference between Paste ID: xEc03xSe and XcaHdtL8
SHOW: | | - or go back to the newest paste.
1
local helptext = [=[
2
relations-indicator
3
===================
4-
       v1.0
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-
                    for feelingindex, feelingtype in pairs(relationship.anon_3) do
504+
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 unittable,jobstable = getSuitableMatches(unit, unitscreen.units.Citizens,unitscreen.jobs.Citizens)
1018+
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-
                        print("entered onclick " .. getLen(fortressmatches))
1123+
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