Skip to content

Commit 0b78162

Browse files
authored
Merge pull request #1529 from SilasD/notify-nemesis
Add notification case: missing nemesis records
2 parents 8359ac6 + 14ad7b9 commit 0b78162

2 files changed

Lines changed: 121 additions & 0 deletions

File tree

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Template for new versions:
2929
## New Tools
3030

3131
## New Features
32+
- `gui/notify`: new notification type: missing nemesis records; displays a warning message about game corruption.
3233

3334
## Fixes
3435

internal/notify/notifications.lua

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
local dlg = require('gui.dialogs')
44
local gui = require('gui')
55
local json = require('json')
6+
local utils = require('utils')
67
local list_agreements = reqscript('list-agreements')
78
local repeat_util = require('repeat-util')
89
local stuck_squad = reqscript('fix/stuck-squad')
@@ -334,8 +335,121 @@ local function save_popup()
334335
end
335336
end
336337

338+
---@return string[]
339+
local function get_active_units_with_missing_nemesis_records()
340+
local namelist = {}
341+
for _, unit in ipairs(df.global.world.units.active) do
342+
local ref = dfhack.units.getGeneralRef(unit, df.general_ref_type.IS_NEMESIS)
343+
if ref then
344+
local nrec = ref:getNemesis()
345+
if nrec == nil then
346+
table.insert(namelist, dfhack.units.getReadableName(unit))
347+
end
348+
end
349+
end
350+
return namelist
351+
end
352+
353+
---@param vector any[] # a df vector or array, or a Lua list.
354+
---@param field string? # nil, or the field name to sort on.
355+
---@param comparator fun(a:any, b:any):integer|nil
356+
--- # an optional comparator that returns -1,0,1 per utils.compare_* .
357+
--- # nil falls back to utils.compare or utils.compare_field.
358+
--- # if a comparator is given, the field parameter is ignored.
359+
---@return boolean
360+
local function verify_vector_is_sorted(vector, field, comparator)
361+
assert(type(vector) == 'table' or utils.is_container(vector))
362+
assert(type(field) == 'string' or field == nil)
363+
assert(type(comparator) == 'function' or comparator == nil)
364+
comparator = comparator or utils.compare_field(field)
365+
local lo, hi
366+
if type(vector) == 'table' then
367+
lo, hi = 1, #vector
368+
else
369+
lo, hi = 0, #vector-1
370+
end
371+
local sorted = true
372+
for i = lo, hi-1 do
373+
if comparator(vector[i], vector[i+1]) ~= -1 then
374+
sorted = false
375+
break
376+
end
377+
end
378+
return sorted
379+
end
380+
381+
local cache_nemesis_all_is_sorted = {}
382+
---only verifies if the vector length has changed.
383+
---@return boolean
384+
local function verify_nemesis_all_is_sorted()
385+
local vector = df.global.world.nemesis.all
386+
if #vector == cache_nemesis_all_is_sorted.length then
387+
return cache_nemesis_all_is_sorted.sorted
388+
end
389+
cache_nemesis_all_is_sorted.length = #vector
390+
cache_nemesis_all_is_sorted.sorted = verify_vector_is_sorted(vector, 'id')
391+
return cache_nemesis_all_is_sorted.sorted
392+
end
393+
337394
-- the order of this list controls the order the notifications will appear in the overlay
338395
NOTIFICATIONS_BY_IDX = {
396+
{
397+
name='missing_nemesis',
398+
desc='Reports missing nemesis records, indicating savegame corruption.',
399+
default=true,
400+
fn = function()
401+
if not verify_nemesis_all_is_sorted() then
402+
return { {
403+
pen = COLOR_LIGHTRED,
404+
text = 'nemesis vector not sorted'
405+
} }
406+
end
407+
local count = df.global.nemesis_next_id - #df.global.world.nemesis.all
408+
if count == 0 then return end
409+
return { {
410+
pen = COLOR_LIGHTRED,
411+
text = ('missing %d nemesis record%s'):format(count, count == 1 and '' or 's')
412+
} }
413+
end,
414+
on_click=function()
415+
if not verify_nemesis_all_is_sorted() then
416+
local message =
417+
'This save game is corrupt.\n\nThe world.nemesis.global vector\n' ..
418+
'of this savegame is not sorted.\n\nSome attempts to lookup the\n' ..
419+
'nemesis record for a unit or\nhistorical figure will fail.\n\n' ..
420+
'This should be reported to\nBay 12 Games as a bug.\n'
421+
dlg.showMessage('nemesis vector not sorted', message, COLOR_RED)
422+
return
423+
end
424+
local message = {
425+
{ pen = COLOR_RED, text = 'This save game may be corrupt.' }, NEWLINE,
426+
NEWLINE,
427+
{ pen = COLOR_WHITE, text = 'This save game contains units which are missing' }, NEWLINE,
428+
{ pen = COLOR_WHITE, text = 'their assigned nemesis records.' }, NEWLINE,
429+
NEWLINE,
430+
{ pen = COLOR_WHITE, text = 'Missing nemesis records have been known to cause' }, NEWLINE,
431+
{ pen = COLOR_WHITE, text = 'crashes during game save and when retiring forts.' }, NEWLINE,
432+
NEWLINE,
433+
{ pen = COLOR_WHITE, text = 'Units with missing nemesis records will' }, NEWLINE,
434+
{ pen = COLOR_RED, text = 'permanently disappear' },
435+
{ pen = COLOR_WHITE, text = ' if they leave the map or' }, NEWLINE,
436+
{ pen = COLOR_WHITE, text = 'if the fort is retired.' }, NEWLINE,
437+
NEWLINE,
438+
}
439+
local redtext = get_active_units_with_missing_nemesis_records()
440+
if #redtext > 0 then
441+
table.insert(message, { pen = COLOR_RED,
442+
text = 'These active units are missing their nemesis records:' })
443+
table.insert(message, NEWLINE)
444+
for _, line in ipairs(redtext) do
445+
table.insert(message, { pen = COLOR_LIGHTRED, text = ' ' .. line })
446+
table.insert(message, NEWLINE)
447+
end
448+
end
449+
dlg.showMessage((#redtext > 0 and 'Active units are' or 'This world is')
450+
.. ' missing nemesis records',message, COLOR_WHITE)
451+
end,
452+
},
339453
{
340454
name='stuck_squad',
341455
desc='Notifies when a squad is stuck on the world map.',
@@ -612,3 +726,9 @@ local function get_config()
612726
end
613727

614728
config = get_config()
729+
730+
dfhack.onStateChange['internal/notify/notifications'] = function(event)
731+
if event == SC_WORLD_LOADED or event == SC_WORLD_UNLOADED then
732+
cache_nemesis_all_is_sorted = {}
733+
end
734+
end

0 commit comments

Comments
 (0)