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
2 changes: 2 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Template for new versions:

## New Features

- `gui/notify`: new `mandates_expiring_early` notification warns when a production mandate is within 3 months of expiring (disabled by default)

## Fixes
- `bodyswap`: fix "invalid argument count" when the target unit has no nemesis record
- `fix/loyaltycascade`: guard against citizens that are not historical figures and emit a warning.
Expand Down
39 changes: 31 additions & 8 deletions internal/notify/notifications.lua
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,19 @@ function Cache_nemesis_all:get_affected_unit_ids()
return self.ATTRS.affected_unit_ids
end

-- mandate.timeout_counter increments once per 10 frames
local function count_expiring_mandates(remaining)
local count = 0
for _, mandate in ipairs(df.global.world.mandates.all) do
if mandate.mode == df.mandate_type.Make and
mandate.timeout_limit - mandate.timeout_counter < remaining
then
count = count + 1
end
end
return count
end

-- the order of this list controls the order the notifications will appear in the overlay
NOTIFICATIONS_BY_IDX = {
{
Expand Down Expand Up @@ -585,14 +598,7 @@ NOTIFICATIONS_BY_IDX = {
desc='Notifies when a production mandate is within 1 month of expiring.',
default=true,
dwarf_fn=function()
local count = 0
for _, mandate in ipairs(df.global.world.mandates.all) do
if mandate.mode == df.mandate_type.Make and
mandate.timeout_limit - mandate.timeout_counter < 2500
then
count = count + 1
end
end
local count = count_expiring_mandates(2500)
if count > 0 then
return ('%d production mandate%s near deadline'):format(
count,
Expand All @@ -604,6 +610,23 @@ NOTIFICATIONS_BY_IDX = {
gui.simulateInput(dfhack.gui.getDFViewscreen(), 'D_NOBLES')
end,
},
{
name='mandates_expiring_early',
desc='Notifies when a production mandate is within 3 months of expiring.',
default=false,
dwarf_fn=function()
local count = count_expiring_mandates(10080)
if count > 0 then
return ('%d production mandate%s expire within 3 months'):format(
count,
count == 1 and '' or 's'
)
end
end,
on_click=function()
gui.simulateInput(dfhack.gui.getDFViewscreen(), 'D_NOBLES')
end,
},
{
name='petitions_agreed',
desc='Notifies when you have agreed to build (but have not yet built) a guildhall or temple.',
Expand Down
77 changes: 77 additions & 0 deletions test/notify.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
config = {
target = 'gui/notify',
mode = 'fortress',
}

local notifications = reqscript('internal/notify/notifications')

local all_mandates = df.global.world.mandates.all

-- mandate.timeout_counter increments once per 10 frames
local NEAR_THRESHOLD = 2500 -- ~3 weeks
local EARLY_THRESHOLD = 10080 -- 3 months

local function add_mandate(remaining)
local m = df.mandate:new()
m.mode = df.mandate_type.Make
m.timeout_counter = 0
m.timeout_limit = remaining
all_mandates:insert('#', m)
end

local function remove_added_mandates(orig_size)
for i = #all_mandates - 1, orig_size, -1 do
all_mandates:erase(i)
end
end

local function window_count(remaining)
local count = 0
for _, m in ipairs(all_mandates) do
if m.mode == df.mandate_type.Make and
m.timeout_limit - m.timeout_counter < remaining
then
count = count + 1
end
end
return count
end

local function expected_msg(count, suffix)
if count == 0 then return nil end
return ('%d production mandate%s %s'):format(
count, count == 1 and '' or 's', suffix)
end

local function expect_mandate_msgs(near_count, early_count)
local near = notifications.NOTIFICATIONS_BY_NAME.mandates_expiring
local early = notifications.NOTIFICATIONS_BY_NAME.mandates_expiring_early
expect.eq(expected_msg(near_count, 'near deadline'), near.dwarf_fn())
expect.eq(expected_msg(early_count, 'expire within 3 months'),
early.dwarf_fn())
end

function test.mandates_expiring()
local orig_size = #all_mandates
return dfhack.with_finalize(
function() remove_added_mandates(orig_size) end,
function()
-- the fort may already have real mandates; measure baselines
local base_near = window_count(NEAR_THRESHOLD)
local base_early = window_count(EARLY_THRESHOLD)

expect_mandate_msgs(base_near, base_early)

-- ~1.5 months remaining: early notification only
add_mandate(5000)
expect_mandate_msgs(base_near, base_early + 1)

-- ~8 days remaining: both notifications
add_mandate(1000)
expect_mandate_msgs(base_near + 1, base_early + 2)

-- ~4.5 months remaining: neither notification
add_mandate(15000)
expect_mandate_msgs(base_near + 1, base_early + 2)
end)
end
Loading