Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Template for new versions:
## Fixes

## Misc Improvements
- `gui/notify`: reduced severity of the missing nemesis records warning if no units on the map are affected. clarified wording.

## Removed

Expand Down
104 changes: 73 additions & 31 deletions internal/notify/notifications.lua
Original file line number Diff line number Diff line change
Expand Up @@ -335,24 +335,25 @@ local function save_popup()
end
end

---@return string[]
local function get_active_units_with_missing_nemesis_records()
local namelist = {}
for _, unit in ipairs(df.global.world.units.active) do
---@return df.unit.id[]
local function _get_active_unit_ids_with_missing_nemesis_records()
local list = {}
for _, unit in ipairs(units.active) do
local ref = dfhack.units.getGeneralRef(unit, df.general_ref_type.IS_NEMESIS)
if ref then
local nrec = ref:getNemesis()
if nrec == nil then
table.insert(namelist, dfhack.units.getReadableName(unit))
table.insert(list, unit.id)
end
end
end
return namelist
return list
end

---@param vector any[] # a df vector or array, or a Lua list.
---@generic T
---@param vector `T`[] # a df vector or array, or a Lua list (not tested yet).
---@param field string? # nil, or the field name to sort on.
---@param comparator fun(a:any, b:any):integer|nil
---@param comparator fun(a:T, b:T):integer|nil
--- # an optional comparator that returns -1,0,1 per utils.compare_* .
--- # nil falls back to utils.compare or utils.compare_field.
--- # if a comparator is given, the field parameter is ignored.
Expand All @@ -378,17 +379,46 @@ local function verify_vector_is_sorted(vector, field, comparator)
return sorted
end

local cache_nemesis_all_is_sorted = {}
---only verifies if the vector length has changed.
---@return boolean
local function verify_nemesis_all_is_sorted()
local vector = df.global.world.nemesis.all
if #vector == cache_nemesis_all_is_sorted.length then
return cache_nemesis_all_is_sorted.sorted
local Cache_nemesis_all = defclass(Cache_nemesis_all, nil) -- singleton, don't need to instantiate.
Cache_nemesis_all.ATTRS{}

function Cache_nemesis_all:invalidate()
self.ATTRS.cached_on_year = -1
end

---@param force boolean? # true to force updating the cached data
function Cache_nemesis_all:populate(force)
local recheck_after = (self.ATTRS.cached_on_tick or 0) + 1200
if force ~= true
and dfhack.world.ReadCurrentYear() == self.ATTRS.cached_on_year
and dfhack.world.ReadCurrentTick() < recheck_after
and self.ATTRS.nemesis_all_length == #df.global.world.nemesis.all
and self.ATTRS.units_active_length == #df.global.world.units.active
then
return
end
cache_nemesis_all_is_sorted.length = #vector
cache_nemesis_all_is_sorted.sorted = verify_vector_is_sorted(vector, 'id')
return cache_nemesis_all_is_sorted.sorted
self.ATTRS.cached_on_year = dfhack.world.ReadCurrentYear()
self.ATTRS.cached_on_tick = dfhack.world.ReadCurrentTick()
self.ATTRS.nemesis_all_length = #df.global.world.nemesis.all
self.ATTRS.units_active_length = #df.global.world.units.active
self.ATTRS.nemesis_all_is_sorted = verify_vector_is_sorted(df.global.world.nemesis.all, 'id')
self.ATTRS.affected_unit_ids = _get_active_unit_ids_with_missing_nemesis_records()
end

function Cache_nemesis_all:init()
self:invalidate()
end

---@return boolean
function Cache_nemesis_all:is_sorted()
self:populate()
return self.ATTRS.nemesis_all_is_sorted
end

---@return df.unit[]
function Cache_nemesis_all:get_affected_unit_ids()
self:populate()
return self.ATTRS.affected_unit_ids
end

-- the order of this list controls the order the notifications will appear in the overlay
Expand All @@ -398,7 +428,7 @@ NOTIFICATIONS_BY_IDX = {
desc='Reports missing nemesis records, indicating savegame corruption.',
default=true,
fn = function()
if not verify_nemesis_all_is_sorted() then
if not Cache_nemesis_all:is_sorted() then
return { {
pen = COLOR_LIGHTRED,
text = 'nemesis vector not sorted'
Expand All @@ -407,12 +437,13 @@ NOTIFICATIONS_BY_IDX = {
local count = df.global.nemesis_next_id - #df.global.world.nemesis.all
if count == 0 then return end
return { {
pen = COLOR_LIGHTRED,
pen = #Cache_nemesis_all:get_affected_unit_ids() > 0
and COLOR_LIGHTRED or COLOR_YELLOW,
text = ('missing %d nemesis record%s'):format(count, count == 1 and '' or 's')
} }
end,
on_click=function()
if not verify_nemesis_all_is_sorted() then
if not Cache_nemesis_all:is_sorted() then
local message =
'This save game is corrupt.\n\nThe world.nemesis.global vector\n' ..
'of this savegame is not sorted.\n\nSome attempts to lookup the\n' ..
Expand All @@ -421,6 +452,7 @@ NOTIFICATIONS_BY_IDX = {
dlg.showMessage('nemesis vector not sorted', message, COLOR_RED)
return
end
local list = Cache_nemesis_all:get_affected_unit_ids()
local message = {
{ pen = COLOR_RED, text = 'This save game may be corrupt.' }, NEWLINE,
NEWLINE,
Expand All @@ -431,23 +463,31 @@ NOTIFICATIONS_BY_IDX = {
{ pen = COLOR_WHITE, text = 'crashes during game save and when retiring forts.' }, NEWLINE,
NEWLINE,
{ pen = COLOR_WHITE, text = 'Units with missing nemesis records will' }, NEWLINE,
{ pen = COLOR_RED, text = 'permanently disappear' },
{ pen = #list > 0 and COLOR_RED or COLOR_WHITE,
text = 'permanently disappear' },
{ pen = COLOR_WHITE, text = ' if they leave the map or' }, NEWLINE,
{ pen = COLOR_WHITE, text = 'if the fort is retired.' }, NEWLINE,
NEWLINE,
}
local redtext = get_active_units_with_missing_nemesis_records()
if #redtext > 0 then
if #list > 0 then
table.insert(message, { pen = COLOR_RED,
text = 'These active units are missing their nemesis records:' })
text = 'These units on the map are missing their nemesis records:' })
table.insert(message, NEWLINE)
for _, line in ipairs(redtext) do
table.insert(message, { pen = COLOR_LIGHTRED, text = ' ' .. line })
for _, unit_id in ipairs(list) do
local unit = df.unit.find(unit_id)
local text = unit and dfhack.units.getReadableName(unit)
or "missing unit for unit id " .. unit_id
if #text > 55 then text = dfhack.units.getReadableName(unit, true); end
table.insert(message, { pen = COLOR_LIGHTRED, text = text })
table.insert(message, NEWLINE)
end
else
table.insert(message, { pen = COLOR_YELLOW,
text = 'No units on the map are missing their nemesis records.' })
table.insert(message, NEWLINE)
end
dlg.showMessage((#redtext > 0 and 'Active units are' or 'This world is')
.. ' missing nemesis records',message, COLOR_WHITE)
dlg.showMessage((#list > 0 and 'Units on the map are' or 'This world is')
.. ' missing nemesis records', message, COLOR_WHITE)
end,
},
{
Expand Down Expand Up @@ -728,7 +768,9 @@ end
config = get_config()

dfhack.onStateChange['internal/notify/notifications'] = function(event)
if event == SC_WORLD_LOADED or event == SC_WORLD_UNLOADED then
cache_nemesis_all_is_sorted = {}
if event == SC_WORLD_LOADED or event == SC_WORLD_UNLOADED
or event == SC_MAP_LOADED or event == SC_MAP_UNLOADED
then
Cache_nemesis_all:invalidate()
end
end
Loading