|
3 | 3 | local dlg = require('gui.dialogs') |
4 | 4 | local gui = require('gui') |
5 | 5 | local json = require('json') |
| 6 | +local utils = require('utils') |
6 | 7 | local list_agreements = reqscript('list-agreements') |
7 | 8 | local repeat_util = require('repeat-util') |
8 | 9 | local stuck_squad = reqscript('fix/stuck-squad') |
@@ -334,8 +335,121 @@ local function save_popup() |
334 | 335 | end |
335 | 336 | end |
336 | 337 |
|
| 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 | + |
337 | 394 | -- the order of this list controls the order the notifications will appear in the overlay |
338 | 395 | 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 | + }, |
339 | 453 | { |
340 | 454 | name='stuck_squad', |
341 | 455 | desc='Notifies when a squad is stuck on the world map.', |
@@ -612,3 +726,9 @@ local function get_config() |
612 | 726 | end |
613 | 727 |
|
614 | 728 | 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