Module:SouthernStraitCorridorOps: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
No edit summary
NewZimiaGov (talk | contribs)
No edit summary
 
(3 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:SouthernStraitCorridorOps
-- Module:SouthernStraitCorridorOps
-- CKD reinforcement at Nysara Velai and Selenarion
-- Post-agreement Southern Strait operations:
--  +6 percentage points to Nysara Velai Compliance Observed
--  -6 percentage points to Nysara Velai Rebellion Alert
--  historical trade sign-offs preserved through 27/3/53 PSSC
--  all future trade-agreement generation disabled from 28/3/53 PSSC onward
local p = {}
local p = {}


-- Helpers
-- Helpers
local function yn(value, default)
local function yn(value, default)
local v = tostring(value or ""):lower()
local v = tostring(value or ""):lower()
if v == "" then return default end
if v == "" then return default end
if v == "no" or v == "n" or v == "0" or v == "false" then return false end
if v == "no" or v == "n" or v == "0" or v == "false" then return false end
if v == "yes" or v == "y" or v == "1" or v == "true" then return true end
if v == "yes" or v == "y" or v == "1" or v == "true" then return true end
return default
return default
end
end


local function makeFrame(args)
local function makeFrame(args)
return { args = args or {} }
return { args = args or {} }
end
end


local function getCurrentPSSCDate()
local function getCurrentPSSCDate()
local startDate = os.time({ year = 1999, month = 8, day = 6, hour = 0 })
local startDate = os.time({ year = 1999, month = 8, day = 6, hour = 0 })
local secondsInDay = 86400
local secondsInDay = 86400
local daysPerYear = 183
local daysPerYear = 183
local currentDate = os.time()
local currentDate = os.time()
local totalDaysElapsed = math.floor((currentDate - startDate) / secondsInDay)
local totalDaysElapsed = math.floor((currentDate - startDate) / secondsInDay)
local psscYear = math.floor(totalDaysElapsed / daysPerYear)
local psscYear = math.floor(totalDaysElapsed / daysPerYear)
local dayOfYear = (totalDaysElapsed % daysPerYear) + 1
local dayOfYear = (totalDaysElapsed % daysPerYear) + 1
 
if dayOfYear < 1 then dayOfYear = 1 end
 
if dayOfYear > 183 then dayOfYear = 183 end
if dayOfYear < 1 then dayOfYear = 1 end
return psscYear, dayOfYear
if dayOfYear > 183 then dayOfYear = 183 end
 
return psscYear, dayOfYear
 
 
end
end


local function normalizeArgsDate(args)
local function normalizeArgsDate(args)
local out = {}
local out = {}
 
for k, v in pairs(args or {}) do
 
out[k] = v
for k, v in pairs(args or {}) do
end
out[k] = v
end
 
local y = tonumber(out.year or out.psscyear)
local d = tonumber(out.day or out.dayofyear)
 
if not y or not d then
y, d = getCurrentPSSCDate()
end
 
out.year = tostring(y)
out.day = tostring(d)
out.psscyear = tostring(y)
out.dayofyear = tostring(d)
 
return out


local y = tonumber(out.year or out.psscyear)
local d = tonumber(out.day or out.dayofyear)
if not y or not d then
y, d = getCurrentPSSCDate()
end


out.year = tostring(y)
out.day = tostring(d)
out.psscyear = tostring(y)
out.dayofyear = tostring(d)
return out
end
end


local function safeRequire(moduleName)
local function safeRequire(moduleName)
local ok, mod = pcall(require, moduleName)
local ok, mod = pcall(require, moduleName)
if ok and mod then
if ok and mod then
return mod, nil
return mod, nil
end
end
return nil, "Could not load " .. tostring(moduleName)
return nil, "Could not load " .. tostring(moduleName)
end
end


local function safeCall(moduleName, funcName, args)
local function safeCall(moduleName, funcName, args)
local mod, err = safeRequire(moduleName)
local mod, err = safeRequire(moduleName)
 
if not mod then
 
return false, "''" .. err .. ".''"
if not mod then
end
return false, "''" .. err .. ".''"
if type(mod[funcName]) ~= "function" then
end
return false, "''" .. tostring(moduleName) .. " has no public function named " .. tostring(funcName) .. ".''"
 
end
if type(mod[funcName]) ~= "function" then
return false, "''" .. tostring(moduleName) .. " has no public function named " .. tostring(funcName) .. ".''"
end
 
local ok, result = pcall(function()
return mod[funcName](makeFrame(args))
end)
 
if not ok then
return false, "''Error while invoking " .. tostring(moduleName) .. "." .. tostring(funcName) .. ": " .. tostring(result) .. "''"
end
 
return true, tostring(result or "")
 


local ok, result = pcall(function()
return mod[funcName](makeFrame(args))
end)
if not ok then
return false, "''Error while invoking " .. tostring(moduleName) .. "." .. tostring(funcName) .. ": " .. tostring(result) .. "''"
end
return true, tostring(result or "")
end
end


local function doyToMonthDom(doy)
local function doyToMonthDom(doy)
doy = tonumber(doy) or 1
doy = tonumber(doy) or 1
if doy < 1 then doy = 1 end
if doy < 1 then doy = 1 end
if doy > 183 then doy = 183 end
if doy > 183 then doy = 183 end
 
 
if doy <= 61 then
return 1, doy
elseif doy <= 122 then
return 2, doy - 61
else
return 3, doy - 122
end
 


if doy <= 61 then
return 1, doy
elseif doy <= 122 then
return 2, doy - 61
else
return 3, doy - 122
end
end
end


local function dateLabel(args)
local function dateLabel(args)
local y = tonumber(args.year or args.psscyear)
local y = tonumber(args.year or args.psscyear)
local d = tonumber(args.day or args.dayofyear)
local d = tonumber(args.day or args.dayofyear)
 
if y and d then
 
local m, dom = doyToMonthDom(d)
if y and d then
return string.format("%d/%d/%d PSSC", dom, m, y)
local m, dom = doyToMonthDom(d)
end
return string.format("%d/%d/%d PSSC", dom, m, y)
return "current PSSC date"
end
 
return "current PSSC date"
 
 
end
end


local function section(title, body, collapsible, collapsed)
local function section(title, body, collapsible, collapsed)
local out = {}
local out = {}
local safeTitle = tostring(title or "Operations")
local safeTitle = tostring(title or "Operations")
local content = tostring(body or "''No output.''")
local content = tostring(body or "''No output.''")


if not collapsible then
table.insert(out, "===" .. safeTitle .. "===")
table.insert(out, content)
return table.concat(out, "\n")
end


if not collapsible then
local collapseClass = collapsed and "mw-collapsible mw-collapsed" or "mw-collapsible"
table.insert(out, "===" .. safeTitle .. "===")
table.insert(out, '<div class="' .. collapseClass .. '" style="border:1px solid #a2a9b1; margin:0.6em 0; background:#f8f9fa;">')
table.insert(out, '<div style="font-weight:bold; padding:0.35em 0.6em; background:#eaecf0;">' .. safeTitle .. '</div>')
table.insert(out, '<div style="padding:0.5em 0.6em;">')
table.insert(out, content)
table.insert(out, content)
table.insert(out, '</div>')
table.insert(out, '</div>')
return table.concat(out, "\n")
return table.concat(out, "\n")
end
local collapseClass = collapsed and "mw-collapsible mw-collapsed" or "mw-collapsible"
table.insert(out, '<div class="' .. collapseClass .. '" style="border:1px solid #a2a9b1; margin:0.6em 0; background:#f8f9fa;">')
table.insert(out, '<div style="font-weight:bold; padding:0.35em 0.6em; background:#eaecf0;">' .. safeTitle .. '</div>')
table.insert(out, '<div style="padding:0.5em 0.6em;">')
table.insert(out, content)
table.insert(out, '</div>')
table.insert(out, '</div>')
return table.concat(out, "\n")
end
end


local function hr()
local function hr()
return "\n----\n"
return "\n----\n"
end
end


local function hashString(s)
local function hashString(s)
s = tostring(s or "")
s = tostring(s or "")
local h = 0
local h = 0
 
for i = 1, #s do
 
h = (h * 31 + string.byte(s, i)) % 1000003
for i = 1, #s do
end
h = (h * 31 + string.byte(s, i)) % 1000003
return h
end
 
return h
 
 
end
end


local function pick(list, seed, salt)
local function pick(list, seed, salt)
if not list or #list == 0 then return nil end
if not list or #list == 0 then return nil end
local idx = (hashString(tostring(seed or "") .. ":" .. tostring(salt or "")) % #list) + 1
local idx = (hashString(tostring(seed or "") .. ":" .. tostring(salt or "")) % #list) + 1
return list[idx]
return list[idx]
end
end


local function pctFromSeed(seed, salt)
local function pctFromSeed(seed, salt)
return hashString(tostring(seed or "") .. ":" .. tostring(salt or "")) % 100
return hashString(tostring(seed or "") .. ":" .. tostring(salt or "")) % 100
end
end


local function clampChance(value)
local function clampChance(value)
value = tonumber(value) or 0
value = tonumber(value) or 0
if value < 0 then return 0 end
if value < 0 then return 0 end
if value > 100 then return 100 end
if value > 100 then return 100 end
return value
return value
end
end


local function isOnOrAfterPSSCDate(year, day, startYear, startDay)
local function isOnOrAfterPSSCDate(year, day, startYear, startDay)
year = tonumber(year) or 0
year = tonumber(year) or 0
day = tonumber(day) or 0
day = tonumber(day) or 0
startYear = tonumber(startYear) or 0
startYear = tonumber(startYear) or 0
startDay = tonumber(startDay) or 0
startDay = tonumber(startDay) or 0
 
if year > startYear then return true end
 
if year == startYear and day >= startDay then return true end
if year > startYear then
return false
return true
end
 
if year == startYear and day >= startDay then
return true
end
 
return false
 
 
end
end


-- Extended Southern Strait / Eastern Mykonos / Snowholme locality list
-- Extended Southern Strait / Eastern Mykonos / Snowholme locality list
local southernTradeCities = {
local southernTradeCities = {
{
{
code = "MIR",
code = "MIR",
city = "[[Mirovia]]",
city = "[[Mirovia]]",
region = "[[Eastern Mykonos]] Desert",
region = "[[Eastern Mykonos]] Desert",
category = "Agriculture",
category = "Agriculture",
company = "[[Mirovian Cistern Growers' Syndicate]]",
company = "[[Mirovian Cistern Growers' Syndicate]]",
resource = "cistern-grown melons, dryland cucumbers, preserved desert fruit, seed oil, packed caravan rations, and water-accounted produce consignments",
resource = "cistern-grown melons, dryland cucumbers, preserved desert fruit, seed oil, packed caravan rations, and water-accounted produce consignments",
lineage = "Desert provisioning economy influenced by [[Jogi]]'s food-market role and [[Mylecia]]'s caravan-facing trade networks; adapted to cistern agriculture, water claims, and dry-route supply rather than beer, grain, or ritual crops"
lineage = "Desert provisioning economy influenced by [[Jogi]]'s food-market role and [[Mylecia]]'s caravan-facing trade networks; adapted to cistern agriculture, water claims, and dry-route supply rather than beer, grain, or ritual crops"
},
},
{
{
code = "SAR",
code = "SAR",
city = "[[Saruq Thyrsa]]",
city = "[[Saruq Thyrsa]]",
region = "Southern Strait coast",
region = "Southern Strait coast",
category = "Agriculture",
category = "Agriculture",
company = "[[Saruq Thyrsa Reedcutters' Guild]]",
company = "[[Saruq Thyrsa Reedcutters' Guild]]",
resource = "coastal reed, cane fiber, packing straw, ferry mats, cargo-padding material, rope backing, warehouse thatch, and crate-lining bundles",
resource = "coastal reed, cane fiber, packing straw, ferry mats, cargo-padding material, rope backing, warehouse thatch, and crate-lining bundles",
lineage = "Harbor-materials economy influenced by [[Blore Heath]]'s shipping brokerage and [[Jogi]]'s quay-market supply system; focused on practical cargo handling, ferry shelters, and packing materials rather than piracy or cult goods"
lineage = "Harbor-materials economy influenced by [[Blore Heath]]'s shipping brokerage and [[Jogi]]'s quay-market supply system; focused on practical cargo handling, ferry shelters, and packing materials rather than piracy or cult goods"
},
},
{
{
code = "AYS",
code = "AYS",
city = "[[Nysara Velai]]",
city = "[[Nysara Velai]]",
region = "Eastern foothills of the [[Snowholme Range]]",
region = "Eastern foothills of the [[Snowholme Range]]",
category = "Agriculture",
category = "Agriculture",
company = "[[Nysaran Terrace Farmers' Ayllu]]",
company = "[[Nysaran Terrace Farmers' Cooperative]]",
resource = "terrace beans, cold-weather tubers, sheep wool, highland fodder, pack-animal feed, preserved root stores, and mule-train provisions",
resource = "terrace beans, cold-weather tubers, sheep wool, highland fodder, pack-animal feed, preserved root stores, and mule-train provisions",
lineage = "Foothill supply economy influenced only indirectly by the Maritime Markets; tied more closely to Snowholme terrace farming, upland food storage, wool traffic, and pass-road provisioning"
lineage = "Foothill supply economy influenced only indirectly by the Maritime Markets; tied more closely to Snowholme terrace farming, upland food storage, wool traffic, and pass-road provisioning"
},
},
{
{
code = "THQ",
code = "THQ",
city = "[[Thal Lenaia]]",
city = "[[Thal Lenaia]]",
region = "Eastern foothills of the [[Snowholme Range]]",
region = "Eastern foothills of the [[Snowholme Range]]",
category = "Agriculture",
category = "Agriculture",
company = "[[Thal Lenaia Springkeepers' Cooperative]]",
company = "[[Thal Lenaia Springkeepers' Cooperative]]",
resource = "spring-fed watercress, trout ponds, smoked fish, cold-storage greens, inspected mountain food shipments, packed ice-box produce, and road-market fish consignments",
resource = "spring-fed watercress, trout ponds, smoked fish, cold-storage greens, inspected mountain food shipments, packed ice-box produce, and road-market fish consignments",
lineage = "Foothill water-and-food economy tied to Snowholme springs and pass roads; comparable to older Maritime Markets food distribution only in its role as a regulated supplier to travelers, markets, and route offices"
lineage = "Foothill water-and-food economy tied to Snowholme springs and pass roads; comparable to older Maritime Markets food distribution only in its role as a regulated supplier to travelers, markets, and route offices"
},
},
{
{
code = "RUK",
code = "RUK",
city = "[[Rumi-Kefaya]]",
city = "[[Rumi-Kefaya]]",
region = "[[Eastern Mykonos]] Desert",
region = "[[Eastern Mykonos]] Desert",
category = "Mining / Manufacturing / Energy",
category = "Mining / Manufacturing / Energy",
company = "[[Rumi-Kefaya Stone and Lime Works]]",
company = "[[Rumi-Kefaya Stone and Lime Works]]",
resource = "basalt, limestone, paving stone, railway ballast, drainage tile, lime mortar, culvert blocks, harbor repair stone, quay coping, and storm-route masonry",
resource = "basalt, limestone, paving stone, railway ballast, drainage tile, lime mortar, culvert blocks, harbor repair stone, quay coping, and storm-route masonry",
lineage = "Infrastructure economy influenced by [[Mylecia]]'s coal-and-works tradition and Southern Strait coastal preservation activity; focused on roads, quays, culverts, retaining walls, and storm-damage repair rather than extractive wealth"
lineage = "Infrastructure economy influenced by [[Mylecia]]'s coal-and-works tradition and Southern Strait coastal preservation activity; focused on roads, quays, culverts, retaining walls, and storm-damage repair rather than extractive wealth"
},
},
{
{
code = "HAY",
code = "HAY",
city = "[[Hayr Chaskal]]",
city = "[[Hayr Chaskal]]",
region = "Southern Strait coast",
region = "Southern Strait coast",
category = "Mining / Manufacturing / Energy",
category = "Mining / Manufacturing / Energy",
company = "[[Hayr Chaskal Glass and Signal Works]]",
company = "[[Hayr Chaskal Glass and Signal Works]]",
resource = "beacon glass, telegraph insulators, lighthouse lenses, lamp glass, storm panes, signal mirrors, buoy lenses, and weatherproof signal housings",
resource = "beacon glass, telegraph insulators, lighthouse lenses, lamp glass, storm panes, signal mirrors, buoy lenses, and weatherproof signal housings",
lineage = "Signal-and-navigation economy influenced by [[Blore Heath]]'s shipping control and [[Mylecia]]'s coastal works; supports ferry warnings, lighthouse maintenance, telegraph lines, and Hatch Ministry watch posts without copying older pirate or brokerage firms"
lineage = "Signal-and-navigation economy influenced by [[Blore Heath]]'s shipping control and [[Mylecia]]'s coastal works; supports ferry warnings, lighthouse maintenance, telegraph lines, and Hatch Ministry watch posts without copying older pirate or brokerage firms"
},
},
{
{
code = "QSE",
code = "QSE",
city = "[[Selenarion]]",
city = "[[Selenarion]]",
region = "Eastern foothills of the [[Snowholme Range]]",
region = "Eastern foothills of the [[Snowholme Range]]",
category = "Services",
category = "Services",
company = "[[Selenarion Route Registry Office]]",
company = "[[Selenarion Route Registry Office]]",
resource = "night-road registry, caravan lodging, pass permits, route insurance, guide licensing, cargo-route papers, missing-traveler filings, and bonded transit records",
resource = "night-road registry, caravan lodging, pass permits, route insurance, guide licensing, cargo-route papers, missing-traveler filings, and bonded transit records",
lineage = "Foothill route-service economy influenced by [[Mylecia]]'s caravan-facing commerce and [[Jogi]]'s intelligence-and-paperwork traditions; adapted to Snowholme pass roads, guide houses, lodging records, and insurance claims"
lineage = "Foothill route-service economy influenced by [[Mylecia]]'s caravan-facing commerce and [[Jogi]]'s intelligence-and-paperwork traditions; adapted to Snowholme pass roads, guide houses, lodging records, and insurance claims"
}
}
}
}


-- Locality presence controls
-- Locality presence controls
-- MIR, SAR, and THQ are capped at one displayed unit per section/channel.
-- In the CKD / War League section, AYS receives six rows and QSE receives two.
-- QSE is targeted for up to three displayed units per section/channel by default.
-- Hatch Ministry and Temple Bank retain three AYS rows and one QSE row.
 
local cappedPresenceLocalities = {
local cappedPresenceLocalities = {
MIR = true,
MIR = true,
SAR = true,
SAR = true,
THQ = true
THQ = true
}
}


local function findLocalityByCode(code)
local function findLocalityByCode(code)
code = tostring(code or "")
code = tostring(code or "")
 
for _, loc in ipairs(southernTradeCities or {}) do
 
if tostring(loc.code or "") == code then
for _, loc in ipairs(southernTradeCities or {}) do
return loc
if tostring(loc.code or "") == code then
end
return loc
end
end
end
return nil
 
return nil
 
 
end
end


local function localityCount(state, locOrCode)
local function localityCount(state, locOrCode)
if not state or not state.counts then return 0 end
if not state or not state.counts then return 0 end
 
local code
 
if type(locOrCode) == "table" then
local code
code = tostring(locOrCode.code or "")
 
else
if type(locOrCode) == "table" then
code = tostring(locOrCode or "")
code = tostring(locOrCode.code or "")
end
else
return tonumber(state.counts[code]) or 0
code = tostring(locOrCode or "")
end
end


return tonumber(state.counts[code]) or 0
local function incrementLocalityCount(state, loc)
 
if not state or not loc then return end
 
local code = tostring(loc.code or "")
if code == "" then return end
state.counts[code] = (tonumber(state.counts[code]) or 0) + 1
end
end


local function incrementLocalityCount(state, loc)
local function nysaraVelaiPresenceTarget(args, channel)
if not state or not loc then return end
local isWar = tostring(channel or "") == "war"
 
local v
 
if isWar then
local code = tostring(loc.code or "")
v = tonumber(
if code == "" then return end
args.warnysaravelaitarget or
 
args.warNysaraVelaiTarget or
state.counts[code] = (tonumber(state.counts[code]) or 0) + 1
args.warnysaratarget or
 
args.warNysaraTarget or
 
args.waraystarget or
args.warAysTarget
)
end
if v == nil then
v = tonumber(
args.nysaravelaitarget or
args.nysaraVelaiTarget or
args.nysaratarget or
args.nysaraTarget or
args.aystarget or
args.aysTarget
)
end
if v == nil then v = isWar and 6 or 3 end
if v < 0 then v = 0 end
return math.floor(v)
end
end


local function qhapaqSelenPresenceTarget(args)
local function selenarionPresenceTarget(args, channel)
local v = tonumber(
local isWar = tostring(channel or "") == "war"
args.qhapaqselentarget or
local v
args.qhapaqSelenTarget or
if isWar then
args.qseTarget or
v = tonumber(
args.qsetarget or
args.warselenariontarget or
args.qhapagselentarget
args.warSelenarionTarget or
)
args.warqsetarget or
 
args.warQseTarget
 
)
if v == nil then v = 3 end
end
if v < 0 then v = 0 end
if v == nil then
 
v = tonumber(
return math.floor(v)
args.selenariontarget or
 
args.selenarionTarget or
 
args.qsetarget or
args.qseTarget
)
end
if v == nil then v = isWar and 2 or 1 end
if v < 0 then v = 0 end
return math.floor(v)
end
end


local function qhapaqSelenPresenceInterval(args)
local function nysaraVelaiAllowOverTarget(args)
local v = tonumber(
return yn(
args.qhapaqseleninterval or
args.nysaravelaioverfill or
args.qhapaqSelenInterval or
args.nysaraVelaiOverfill or
args.qseInterval or
args.nysaraoverfill or
args.qseinterval or
args.nysaraOverfill or
args.qhapagseleninterval
args.aysoverfill or
)
args.aysOverfill,
 
false
 
)
if v == nil then v = 3 end
if v < 1 then v = 1 end
 
return math.floor(v)
 
 
end
end


local function qhapaqSelenAllowOverTarget(args)
local function buildPriorityLocalityPlan(args, channel)
return yn(
local plan = {}
args.qhapaqselenoverfill or
local nysaraTarget = nysaraVelaiPresenceTarget(args, channel)
args.qhapaqSelenOverfill or
local selenarionTarget = selenarionPresenceTarget(args, channel)
args.qseoverfill or
for _ = 1, nysaraTarget do table.insert(plan, "AYS") end
args.qseOverfill or
for _ = 1, selenarionTarget do table.insert(plan, "QSE") end
args.qhapagselenoverfill,
return plan
false
)
end
end


local function newLocalityAssignmentState(args, channel)
local function newLocalityAssignmentState(args, channel)
return {
return {
channel = tostring(channel or ""),
channel = tostring(channel or ""),
counts = {},
counts = {},
qseTarget = qhapaqSelenPresenceTarget(args),
aysTarget = nysaraVelaiPresenceTarget(args, channel),
qseInterval = qhapaqSelenPresenceInterval(args),
aysAllowOverTarget = nysaraVelaiAllowOverTarget(args),
qseAllowOverTarget = qhapaqSelenAllowOverTarget(args)
qseTarget = selenarionPresenceTarget(args, channel),
}
priorityPlan = buildPriorityLocalityPlan(args, channel)
}
end
end


local function canAssignLocality(args, loc, state)
local function canAssignLocality(args, loc, state)
if not loc then return false end
if not loc then return false end
 
local code = tostring(loc.code or "")
 
if cappedPresenceLocalities[code] and localityCount(state, code) >= 1 then
local code = tostring(loc.code or "")
 
if cappedPresenceLocalities[code] and localityCount(state, code) >= 1 then
return false
end
 
if code == "QSE" and state and not state.qseAllowOverTarget then
if localityCount(state, code) >= (tonumber(state.qseTarget) or 3) then
return false
return false
end
end
end
if code == "AYS" and state and not state.aysAllowOverTarget then
 
if localityCount(state, code) >= (tonumber(state.aysTarget) or 3) then
return true
return false
 
end
 
end
end
if code == "QSE" and state then
 
if localityCount(state, code) >= (tonumber(state.qseTarget) or 1) then
local function shouldAssignQhapaqSelen(args, channel, rowNumber, state)
return false
if not state then return false end
end
 
end
 
local target = tonumber(state.qseTarget) or 3
if target <= 0 then return false end
 
local already = localityCount(state, "QSE")
if already >= target then return false end
 
local interval = tonumber(state.qseInterval) or 3
if interval < 1 then interval = 1 end
 
local row = tonumber(rowNumber) or 1
if row < 1 then row = 1 end
 
local year = tonumber(args.year or args.psscyear) or 0
local day = tonumber(args.day or args.dayofyear) or 0
 
local seed = tostring(args.seed or "") ..
":SS-QSE-PRESENCE-SLOT:" ..
tostring(channel or "") ..
":" .. tostring(year) ..
":" .. tostring(day)
 
local phase = hashString(seed) % interval
 
if ((row + phase) % interval) == 0 then
return true
return true
end
end


local targetWindow = target * interval
local function priorityLocalityForRow(state, rowNumber)
local remainingRowsInWindow = targetWindow - row + 1
if not state or not state.priorityPlan then return nil end
local needed = target - already
local row = tonumber(rowNumber) or 0
 
local code = state.priorityPlan[row]
if remainingRowsInWindow <= needed then
if not code then return nil end
return true
return findLocalityByCode(code)
end
end
return false
end
-- Row-level locality focus helper


local function rowLocalityData(args, channel, rowNumber, state)
local function rowLocalityData(args, channel, rowNumber, state)
if not southernTradeCities or #southernTradeCities == 0 then
if not southernTradeCities or #southernTradeCities == 0 then return nil end
return nil
state = state or newLocalityAssignmentState(args, channel)
end


local priorityLoc = priorityLocalityForRow(state, rowNumber)
if priorityLoc and canAssignLocality(args, priorityLoc, state) then
incrementLocalityCount(state, priorityLoc)
return priorityLoc
end


state = state or newLocalityAssignmentState(args, channel)
local year = tonumber(args.year or args.psscyear) or 0
local day = tonumber(args.day or args.dayofyear) or 0
local seed = tostring(args.seed or "") ..
":SS-ROW-LOCALITY:" .. tostring(channel or "") ..
":" .. tostring(year) ..
":" .. tostring(day)


local qse = findLocalityByCode("QSE")
local offset = hashString(seed) % #southernTradeCities
 
local baseRow = tonumber(rowNumber) or 1
if qse and shouldAssignQhapaqSelen(args, channel, rowNumber, state) and canAssignLocality(args, qse, state) then
for attempt = 0, #southernTradeCities - 1 do
incrementLocalityCount(state, qse)
local idx = ((baseRow - 1 + offset + attempt) % #southernTradeCities) + 1
return qse
local loc = southernTradeCities[idx]
end
if canAssignLocality(args, loc, state) then
 
incrementLocalityCount(state, loc)
local year = tonumber(args.year or args.psscyear) or 0
return loc
local day = tonumber(args.day or args.dayofyear) or 0
end
 
local seed = tostring(args.seed or "") ..
":SS-ROW-LOCALITY:" ..
tostring(channel or "") ..
":" .. tostring(year) ..
":" .. tostring(day)
 
local offset = hashString(seed) % #southernTradeCities
local baseRow = tonumber(rowNumber) or 1
 
for attempt = 0, #southernTradeCities - 1 do
local idx = ((baseRow - 1 + offset + attempt) % #southernTradeCities) + 1
local loc = southernTradeCities[idx]
 
if canAssignLocality(args, loc, state) then
incrementLocalityCount(state, loc)
return loc
end
end
end


for _, loc in ipairs(southernTradeCities) do
for _, loc in ipairs(southernTradeCities) do
local code = tostring(loc.code or "")
local code = tostring(loc.code or "")
 
if not cappedPresenceLocalities[code] and code ~= "AYS" and code ~= "QSE" then
if not cappedPresenceLocalities[code] and code ~= "QSE" then
incrementLocalityCount(state, loc)
incrementLocalityCount(state, loc)
return loc
return loc
end
end
end
end
return nil
 
return nil
 
 
end
end


local function rowLocalityText(loc)
local function rowLocalityText(loc)
if not loc then
if not loc then return "Southern Strait corridor" end
return "Southern Strait corridor"
return loc.city .. "<br><small>" .. loc.region .. "</small>"
end
 
 
return loc.city .. "<br><small>" .. loc.region .. "</small>"
 
 
end
end


-- Imperial Federation support marker and locality boosts
-- Imperial Federation support marker and locality boosts
-- Effective from 112/53 PSSC only.
local IMPERIAL_SUPPORT_START_YEAR = 53
local IMPERIAL_SUPPORT_START_YEAR = 53
local IMPERIAL_SUPPORT_START_DAY = 112
local IMPERIAL_SUPPORT_START_DAY = 112
local imperialFederationSupportLocalities = {
local imperialFederationSupportLocalities = {
MIR = true,
MIR = true,
SAR = true,
SAR = true,
RUK = true
RUK = true
}
}


local function imperialSupportEnabled(args)
local function imperialSupportEnabled(args)
return yn(args.imperialsupport or args.imperialSupport or args.ifsupport or args.ifSupport, true)
return yn(args.imperialsupport or args.imperialSupport or args.ifsupport or args.ifSupport, true)
end
end


local function imperialSupportEffective(args)
local function imperialSupportEffective(args)
if not imperialSupportEnabled(args) then
if not imperialSupportEnabled(args) then return false end
return false
local year = tonumber(args.year or args.psscyear) or 0
end
local day = tonumber(args.day or args.dayofyear) or 0
 
local startYear = tonumber(args.imperialstartyear or args.imperialStartYear) or IMPERIAL_SUPPORT_START_YEAR
 
local startDay = tonumber(args.imperialstartday or args.imperialStartDay) or IMPERIAL_SUPPORT_START_DAY
local year = tonumber(args.year or args.psscyear) or 0
return isOnOrAfterPSSCDate(year, day, startYear, startDay)
local day = tonumber(args.day or args.dayofyear) or 0
local startYear = tonumber(args.imperialstartyear or args.imperialStartYear) or IMPERIAL_SUPPORT_START_YEAR
local startDay = tonumber(args.imperialstartday or args.imperialStartDay) or IMPERIAL_SUPPORT_START_DAY
 
return isOnOrAfterPSSCDate(year, day, startYear, startDay)
 
 
end
end


local function hasImperialFederationSupport(args, loc)
local function hasImperialFederationSupport(args, loc)
if not imperialSupportEffective(args) then
if not imperialSupportEffective(args) then return false end
return false
return loc and imperialFederationSupportLocalities[tostring(loc.code or "")] == true
end
 
 
return loc and imperialFederationSupportLocalities[tostring(loc.code or "")] == true
 
 
end
end


local function imperialSupportBoostedChance(args, loc, baseChance, boost)
local function imperialSupportBoostedChance(args, loc, baseChance, boost)
local chance = clampChance(baseChance)
local chance = clampChance(baseChance)
 
if hasImperialFederationSupport(args, loc) then
 
chance = chance + (tonumber(boost) or 0)
if hasImperialFederationSupport(args, loc) then
end
chance = chance + (tonumber(boost) or 0)
return clampChance(chance)
end
 
return clampChance(chance)
 
 
end
end


local function imperialFederationSupportNotice(args)
local function imperialFederationSupportNotice(args)
if not imperialSupportEffective(args) then
if not imperialSupportEffective(args) then return "" end
return ""
return "'''Imperial Federation support present:''' [[2nd Brigade, 25th Division of the Imperial Marines]] deployed in allied support of Bassaridian Southern Strait corridor activity around [[Saruq Thyrsa]], [[Mirovia]], and [[Rumi-Kefaya]].<br>" ..
end
"<small>Mechanized, mixed-capability support of approximately 4,000 soldiers reinforces route security, inspection cordons, convoy spacing, engineering liaison, and lawful corridor stabilization. " ..
 
"Locality rows for Saruq Thyrsa, Mirovia, and Rumi-Kefaya receive +10 percentage points to compliance observations. The former +3-point Temple Bank trade-signoff modifier remains relevant only when reproducing historical dates through 27/3/53 PSSC; all later trade-agreement generation is closed.</small>"
 
return "'''Imperial Federation support present:''' [[2nd Brigade, 25th Division of the Imperial Marines]] deployed in allied support of Bassaridian Southern Strait corridor activity around [[Saruq Thyrsa]], [[Mirovia]], and [[Rumi-Kefaya]].<br><small>Mechanized, mixed-capability support of approximately 4,000 soldiers reinforces route security, inspection cordons, convoy spacing, engineering liaison, and lawful corridor stabilization. Locality rows for Saruq Thyrsa, Mirovia, and Rumi-Kefaya receive +10 percentage points to compliance observations and +3 percentage points to Temple Bank trade sign-offs from 112/53 PSSC onward.</small>"
 
 
end
end


-- Bassaridian Missions support marker and locality adjustments
-- Bassaridian Missions support marker and locality adjustments
-- Publicly acknowledged by the Council of Kings Division on 55/2/53 PSSC.
-- 55/2/53 PSSC = day-of-year 116.
local BASSARIDIAN_MISSIONS_START_YEAR = 53
local BASSARIDIAN_MISSIONS_START_YEAR = 53
local BASSARIDIAN_MISSIONS_START_DAY = 116
local BASSARIDIAN_MISSIONS_START_DAY = 116
local majorBassaridianMissionLocalities = {
local majorBassaridianMissionLocalities = {
SAR = true,
SAR = true,
MIR = true,
MIR = true,
AYS = true,
AYS = true,
THQ = true,
THQ = true,
QSE = true
QSE = true
}
}


local function bassaridianMissionsEnabled(args)
local function bassaridianMissionsEnabled(args)
return yn(args.missionsupport or args.missionSupport or args.bassaridianmissions or args.bassaridianMissions, true)
return yn(args.missionsupport or args.missionSupport or args.bassaridianmissions or args.bassaridianMissions, true)
end
end


local function bassaridianMissionsEffective(args)
local function bassaridianMissionsEffective(args)
if not bassaridianMissionsEnabled(args) then
if not bassaridianMissionsEnabled(args) then return false end
return false
local year = tonumber(args.year or args.psscyear) or 0
end
local day = tonumber(args.day or args.dayofyear) or 0
 
local startYear = tonumber(args.missionstartyear or args.missionStartYear) or BASSARIDIAN_MISSIONS_START_YEAR
 
local startDay = tonumber(args.missionstartday or args.missionStartDay) or BASSARIDIAN_MISSIONS_START_DAY
local year = tonumber(args.year or args.psscyear) or 0
return isOnOrAfterPSSCDate(year, day, startYear, startDay)
local day = tonumber(args.day or args.dayofyear) or 0
local startYear = tonumber(args.missionstartyear or args.missionStartYear) or BASSARIDIAN_MISSIONS_START_YEAR
local startDay = tonumber(args.missionstartday or args.missionStartDay) or BASSARIDIAN_MISSIONS_START_DAY
 
return isOnOrAfterPSSCDate(year, day, startYear, startDay)
 
 
end
end


local function hasBassaridianMission(args, loc)
local function hasBassaridianMission(args, loc)
if not bassaridianMissionsEffective(args) then
if not bassaridianMissionsEffective(args) then return false end
return false
return loc ~= nil
end
 
 
return loc ~= nil
 
 
end
end


local function hasMajorBassaridianMission(args, loc)
local function hasMajorBassaridianMission(args, loc)
if not bassaridianMissionsEffective(args) then
if not bassaridianMissionsEffective(args) then return false end
return false
return loc and majorBassaridianMissionLocalities[tostring(loc.code or "")] == true
end
end


 
-- Nysara Velai-specific probability modifiers
return loc and majorBassaridianMissionLocalities[tostring(loc.code or "")] == true
local function nysaraVelaiComplianceBoost(args)
 
local v = tonumber(
 
args.nysaravelaicomplianceboost or
args.nysaraVelaiComplianceBoost or
args.nysaracomplianceboost or
args.nysaraComplianceBoost or
args.ayscomplianceboost or
args.aysComplianceBoost
)
if v == nil then v = 6 end
return v
end
end


local function qhapaqSelenComplianceBoost(args)
local function nysaraVelaiUnrestAdjustment(args)
local v = tonumber(
local reduction = tonumber(
args.qhapaqselencomplianceboost or
args.nysaravelaiunrestreduction or
args.qhapaqSelenComplianceBoost or
args.nysaraVelaiUnrestReduction or
args.qsecomplianceboost or
args.nysaraunrestreduction or
args.qseComplianceBoost or
args.nysaraUnrestReduction or
args.qhapagselencomplianceboost
args.aysunrestreduction or
)
args.aysUnrestReduction
 
)
 
if reduction ~= nil then
if v == nil then v = 5 end
return -math.abs(reduction)
 
end
return v
 
 
end
 
local function qhapaqSelenUnrestBoost(args)
local v = tonumber(
args.qhapaqselenunrestboost or
args.qhapaqSelenUnrestBoost or
args.qseunrestboost or
args.qseUnrestBoost or
args.qhapagselenunrestboost
)
 
 
if v == nil then v = 4 end
 
return v
 


-- Legacy additive override retained for compatibility.
local legacyBoost = tonumber(
args.nysaravelaiunrestboost or
args.nysaraVelaiUnrestBoost or
args.nysaraunrestboost or
args.nysaraUnrestBoost or
args.aysunrestboost or
args.aysUnrestBoost
)
if legacyBoost ~= nil then
return legacyBoost
end
return -6
end
end


local function qhapaqSelenAdjustedComplianceChance(args, loc, baseChance)
local function nysaraVelaiTradeBoost(args)
local chance = clampChance(baseChance)
local v = tonumber(
 
args.nysaravelaitradeboost or
 
args.nysaraVelaiTradeBoost or
if loc and tostring(loc.code or "") == "QSE" then
args.nysaratradeboost or
chance = chance + qhapaqSelenComplianceBoost(args)
args.nysaraTradeBoost or
args.aystradeboost or
args.aysTradeBoost
)
if v == nil then v = 3 end
return v
end
end


return clampChance(chance)
local function nysaraVelaiAdjustedComplianceChance(args, loc, baseChance)
 
local chance = clampChance(baseChance)
 
if loc and tostring(loc.code or "") == "AYS" then
chance = chance + nysaraVelaiComplianceBoost(args)
end
return clampChance(chance)
end
end


local function qhapaqSelenAdjustedUnrestChance(args, loc, baseChance)
local function nysaraVelaiAdjustedUnrestChance(args, loc, baseChance)
local chance = clampChance(baseChance)
local chance = clampChance(baseChance)
 
if loc and tostring(loc.code or "") == "AYS" then
 
chance = chance + nysaraVelaiUnrestAdjustment(args)
if loc and tostring(loc.code or "") == "QSE" then
end
chance = chance + qhapaqSelenUnrestBoost(args)
return clampChance(chance)
end
end


return clampChance(chance)
local function nysaraVelaiAdjustedTradeChance(args, loc, baseChance)
 
local chance = clampChance(baseChance)
 
if loc and tostring(loc.code or "") == "AYS" then
chance = chance + nysaraVelaiTradeBoost(args)
end
return clampChance(chance)
end
end


local function missionAdjustedComplianceChance(args, loc, baseChance)
local function missionAdjustedComplianceChance(args, loc, baseChance)
local chance = clampChance(baseChance)
local chance = clampChance(baseChance)
 
if hasBassaridianMission(args, loc) then
 
chance = chance + (tonumber(args.missioncomplianceboost or args.missionComplianceBoost) or 1)
if hasBassaridianMission(args, loc) then
end
chance = chance + (tonumber(args.missioncomplianceboost or args.missionComplianceBoost) or 1)
if hasMajorBassaridianMission(args, loc) then
end
chance = chance + (tonumber(args.majormissioncomplianceboost or args.majorMissionComplianceBoost) or 2)
 
end
if hasMajorBassaridianMission(args, loc) then
return clampChance(chance)
chance = chance + (tonumber(args.majormissioncomplianceboost or args.majorMissionComplianceBoost) or 2)
end
 
return clampChance(chance)
 
 
end
end


local function missionAdjustedUnrestChance(args, loc, baseChance)
local function missionAdjustedUnrestChance(args, loc, baseChance)
local chance = clampChance(baseChance)
local chance = clampChance(baseChance)
 
if hasBassaridianMission(args, loc) then
 
chance = chance - (tonumber(args.missionunrestreduction or args.missionUnrestReduction) or 1)
if hasBassaridianMission(args, loc) then
end
chance = chance - (tonumber(args.missionunrestreduction or args.missionUnrestReduction) or 1)
return clampChance(chance)
end
 
return clampChance(chance)
 
 
end
end


local function bassaridianMissionsNotice(args)
local function bassaridianMissionsNotice(args)
if not bassaridianMissionsEffective(args) then
if not bassaridianMissionsEffective(args) then return "" end
return ""
return "'''Bassaridian Missions acknowledged:''' [[Temple Bank of the Reformed Stripping Path|Temple Bank]]-run fortified mission compounds operate across the Southern Strait locality system, with major mission houses at [[Saruq Thyrsa]], [[Mirovia]], [[Nysara Velai]], [[Thal Lenaia]], and [[Selenarion]].<br>" ..
end
"<small>From 55/2/53 PSSC onward, all locality rows receive -1 percentage point to unrest alerts and +1 percentage point to compliance observations. " ..
 
"Major mission localities receive an additional +2 percentage points to compliance observations. " ..
 
"Nysara Velai separately receives +6 percentage points to compliance-observation likelihood and -6 percentage points to unrest-alert likelihood, reflecting the stabilizing effect of the enlarged CKD presence, strengthened missionary institutions, protected terrace routes, and improving administrative cooperation across the eastern Snowholme approaches. Its former +3-point trade-signoff modifier is retained only for historical reproduction through 27/3/53 PSSC; no new agreements may be generated afterward. " ..
return "'''Bassaridian Missions acknowledged:''' [[Temple Bank of the Reformed Stripping Path|Temple Bank]]-run fortified mission compounds operate across the Southern Strait locality system, with major mission houses at [[Saruq Thyrsa]], [[Mirovia]], [[Nysara Velai]], [[Thal Lenaia]], and [[Selenarion]].<br><small>From 55/2/53 PSSC onward, all locality rows receive -1 percentage point to unrest alerts and +1 percentage point to compliance observations. Major mission localities receive an additional +2 percentage points to compliance observations. Selenarion receives an additional +4 percentage points to unrest-alert likelihood and +5 percentage points to compliance-observation likelihood, reflecting intensified contestation and intensified Bassaridian administrative presence along the route registry corridor. Mission guards are drawn from local Baratar recruits and Council of Kings Division rotations, while deployed units continue to route through [[Mylecia]] before staging locally.</small>"
"Mission guards are drawn from local Baratar recruits and Council of Kings Division rotations, while deployed units continue to route through [[Mylecia]] before staging locally.</small>"
 
 
end
end


-- Recurring adversarial-node tracking
-- Recurring adversarial-node tracking
-- Temple Bank report context begins 60/2/53 PSSC.
-- 60/2/53 PSSC = day-of-year 121.
local ADVERSARY_NODES_START_YEAR = 53
local ADVERSARY_NODES_START_YEAR = 53
local ADVERSARY_NODES_START_DAY = 121
local ADVERSARY_NODES_START_DAY = 121


local recurringAdversaries = {
local recurringAdversaries = {
KHADIR = {
KHADIR = {
name = "[[Khadir Veyran]]",
name = "[[Khadir Veyran]]",
epithet = "Keeper of the Bromian Ledger",
epithet = "Keeper of the Bromian Ledger",
localities = {
localities = { QSE = true, AYS = true, RUK = true, HAY = true },
QSE = true,
notes = {
AYS = true,
"Bromian Ledger intermediaries are suspected of delaying countersignatures without openly refusing Bassaridian review.",
RUK = true,
"Veyran-linked witnesses preserve older Maritime Markets custom as a rival source of commercial legitimacy.",
HAY = true
"Broker-house clerks appear to be keeping disputed cargo identity ambiguous until General Port auditors withdraw.",
},
"Warehouse notaries tied to the Bromian Ledger are suspected of turning routine paperwork into a jurisdictional dispute."
notes = {
},
"Bromian Ledger intermediaries are suspected of delaying countersignatures without openly refusing Bassaridian review.",
complianceNotes = {
"Veyran-linked witnesses preserve older Maritime Markets custom as a rival source of commercial legitimacy.",
"Temple Bank auditors noted that this cooperation weakens the Bromian Ledger's claim that older broker custom remains the only trusted record.",
"Broker-house clerks appear to be keeping disputed cargo identity ambiguous until General Port auditors withdraw.",
"Local registry compliance was treated as useful evidence against Veyran-linked delay tactics.",
"Warehouse notaries tied to the Bromian Ledger are suspected of turning routine paperwork into a jurisdictional dispute."
"The observation was preserved as a small but meaningful challenge to Bromian Ledger influence in the pass-road economy."
},
},
complianceNotes = {
tradeNotes = {
"Temple Bank auditors noted that this cooperation weakens the Bromian Ledger's claim that older broker custom remains the only trusted record.",
"Temple Bank clerks flagged the sign-off as a possible counterweight to Khadir Veyran's older broker-credit network.",
"Local registry compliance was treated as useful evidence against Veyran-linked delay tactics.",
"The filing gives General Port auditors a lawful route around Bromian Ledger intermediaries.",
"The observation was preserved as a small but meaningful challenge to Bromian Ledger influence in the pass-road economy."
"The agreement was preserved as evidence that local commercial authority could be recorded without Veyran's countersignature."
},
}
tradeNotes = {
},
"Temple Bank clerks flagged the sign-off as a possible counterweight to Khadir Veyran's older broker-credit network.",
SUYANA = {
"The filing gives General Port auditors a lawful route around Bromian Ledger intermediaries.",
name = "[[Suyana Amaruq]]",
"The agreement was preserved as evidence that local commercial authority could be recorded without Veyran's countersignature."
epithet = "Black-Thread Suyana",
}
localities = { QSE = true, AYS = true, THQ = true, HAY = true },
},
notes = {
SUYANA = {
"Black-Thread couriers are suspected of using ordinary household movement to mask route intelligence.",
name = "[[Suyana Amaruq]]",
"Suyana-linked runners may have tested night-road inspection gaps before disappearing into ordinary market traffic.",
epithet = "Black-Thread Suyana",
"False pilgrims and guide-house messengers appear to have carried warning signals ahead of the audit lane.",
localities = {
"Temple Bank officers treated the incident as route-intelligence obstruction rather than ordinary smuggling."
QSE = true,
},
AYS = true,
complianceNotes = {
THQ = true,
"The observation was treated as a useful interruption of Black-Thread courier habits.",
HAY = true
"Licensed guides helped separate ordinary passage from Suyana-linked courier movement.",
},
"Temple Bank staff preserved the record as evidence that local mobility could be protected without unregistered courier screens."
notes = {
},
"Black-Thread couriers are suspected of using ordinary household movement to mask route intelligence.",
tradeNotes = {
"Suyana-linked runners may have tested night-road inspection gaps before disappearing into ordinary market traffic.",
"Temple Bank notes warned that Black-Thread couriers may attempt to test the new trade window through false-route traffic.",
"False pilgrims and guide-house messengers appear to have carried warning signals ahead of the audit lane.",
"The sign-off was paired with route-watch instructions because Suyana-linked couriers remained active near the upland approaches.",
"Temple Bank officers treated the incident as route-intelligence obstruction rather than ordinary smuggling."
"Mission clerks treated the filing as useful, but not sufficient to end Black-Thread influence over informal passage."
},
}
complianceNotes = {
}
"The observation was treated as a useful interruption of Black-Thread courier habits.",
"Licensed guides helped separate ordinary passage from Suyana-linked courier movement.",
"Temple Bank staff preserved the record as evidence that local mobility could be protected without unregistered courier screens."
},
tradeNotes = {
"Temple Bank notes warned that Black-Thread couriers may attempt to test the new trade window through false-route traffic.",
"The sign-off was paired with route-watch instructions because Suyana-linked couriers remained active near the upland approaches.",
"Mission clerks treated the filing as useful, but not sufficient to end Black-Thread influence over informal passage."
}
}
}
}


local function adversaryNodesEnabled(args)
local function adversaryNodesEnabled(args)
return yn(
return yn(
args.adversarynodes or
args.adversarynodes or
args.adversaryNodes or
args.adversaryNodes or
args.namedadversaries or
args.namedadversaries or
args.namedAdversaries or
args.namedAdversaries or
args.adversarynotes or
args.adversarynotes or
args.adversaryNotes,
args.adversaryNotes,
true
true
)
)
end
end


local function adversaryNodesEffective(args)
local function adversaryNodesEffective(args)
if not adversaryNodesEnabled(args) then
if not adversaryNodesEnabled(args) then return false end
return false
local year = tonumber(args.year or args.psscyear) or 0
end
local day = tonumber(args.day or args.dayofyear) or 0
 
local startYear = tonumber(args.adversarystartyear or args.adversaryStartYear) or ADVERSARY_NODES_START_YEAR
 
local startDay = tonumber(args.adversarystartday or args.adversaryStartDay) or ADVERSARY_NODES_START_DAY
local year = tonumber(args.year or args.psscyear) or 0
return isOnOrAfterPSSCDate(year, day, startYear, startDay)
local day = tonumber(args.day or args.dayofyear) or 0
local startYear = tonumber(args.adversarystartyear or args.adversaryStartYear) or ADVERSARY_NODES_START_YEAR
local startDay = tonumber(args.adversarystartday or args.adversaryStartDay) or ADVERSARY_NODES_START_DAY
 
return isOnOrAfterPSSCDate(year, day, startYear, startDay)
 
 
end
end


local function adversaryNoteChance(args, kind)
local function adversaryNoteChance(args, kind)
kind = tostring(kind or "")
kind = tostring(kind or "")
 
if kind == "trade" then
 
return clampChance(tonumber(args.tradeadversarychance or args.tradeAdversaryChance) or 22)
if kind == "trade" then
elseif kind == "compliance" then
return clampChance(tonumber(args.tradeadversarychance or args.tradeAdversaryChance) or 22)
return clampChance(tonumber(args.complianceadversarychance or args.complianceAdversaryChance) or 18)
elseif kind == "compliance" then
end
return clampChance(tonumber(args.complianceadversarychance or args.complianceAdversaryChance) or 18)
return clampChance(tonumber(
end
args.adversarychance or
 
args.adversaryChance or
return clampChance(tonumber(args.adversarychance or args.adversaryChance or args.unrestadversarychance or args.unrestAdversaryChance) or 35)
args.unrestadversarychance or
 
args.unrestAdversaryChance
 
) or 35)
end
end


local function localityAllowsAdversary(loc, adv)
local function localityAllowsAdversary(loc, adv)
if not adv then return false end
if not adv then return false end
if not loc then return true end
if not loc then return true end
 
local localities = adv.localities
 
if not localities then return true end
local localities = adv.localities
return localities[tostring(loc.code or "")] == true
if not localities then return true end
 
return localities[tostring(loc.code or "")] == true
 
 
end
end


local function candidateAdversaryCodes(profile, loc)
local function candidateAdversaryCodes(profile, loc)
local out = {}
local out = {}
 
if profile and type(profile.adversaries) == "table" then
 
for _, code in ipairs(profile.adversaries) do
if profile and type(profile.adversaries) == "table" then
local adv = recurringAdversaries[code]
for _, code in ipairs(profile.adversaries) do
if adv and localityAllowsAdversary(loc, adv) then
local adv = recurringAdversaries[code]
table.insert(out, code)
if adv and localityAllowsAdversary(loc, adv) then
end
table.insert(out, code)
end
end
end
end
end
if #out == 0 then
 
for code, adv in pairs(recurringAdversaries) do
if #out == 0 then
if localityAllowsAdversary(loc, adv) then
for code, adv in pairs(recurringAdversaries) do
table.insert(out, code)
if localityAllowsAdversary(loc, adv) then
end
table.insert(out, code)
end
end
end
end
end
return out
 
return out
 
 
end
end


local function adversaryNotePart(args, kind, seed, profile, loc)
local function adversaryNotePart(args, kind, seed, profile, loc)
if not adversaryNodesEffective(args) then
if not adversaryNodesEffective(args) then return "" end
return ""
local chance = adversaryNoteChance(args, kind)
end
local roll = pctFromSeed(seed, "adv-note-roll-" .. tostring(kind or "general")) + 1
if roll > chance then return "" end


local candidates = candidateAdversaryCodes(profile, loc)
local code = pick(candidates, seed, "adv-note-code-" .. tostring(kind or "general"))
local adv = recurringAdversaries[code]
if not adv then return "" end


local chance = adversaryNoteChance(args, kind)
local notes = adv.notes
local roll = pctFromSeed(seed, "adv-note-roll-" .. tostring(kind or "general")) + 1
if tostring(kind or "") == "trade" and adv.tradeNotes then
 
notes = adv.tradeNotes
if roll > chance then
elseif tostring(kind or "") == "compliance" and adv.complianceNotes then
return ""
notes = adv.complianceNotes
end
end
 
local candidates = candidateAdversaryCodes(profile, loc)
local code = pick(candidates, seed, "adv-note-code-" .. tostring(kind or "general"))
local adv = recurringAdversaries[code]
 
if not adv then
return ""
end
 
local notes = adv.notes
 
if tostring(kind or "") == "trade" and adv.tradeNotes then
notes = adv.tradeNotes
elseif tostring(kind or "") == "compliance" and adv.complianceNotes then
notes = adv.complianceNotes
end
 
local note = pick(notes, seed, "adv-note-text-" .. tostring(kind or "general"))
if not note then
return ""
end
 
return "<br>'''Adversarial note:''' " .. adv.name .. " (''" .. adv.epithet .. "'') — " .. note
 


local note = pick(notes, seed, "adv-note-text-" .. tostring(kind or "general"))
if not note then return "" end
return "<br>'''Adversarial note:''' " .. adv.name .. " (''" .. adv.epithet .. "'') — " .. note
end
end


local function adversarialNodesNotice(args)
local function adversarialNodesNotice(args)
if not adversaryNodesEffective(args) then
if not adversaryNodesEffective(args) then return "" end
return ""
return "'''Temple Bank adversarial-node tracking active:''' from 60/2/53 PSSC onward, occasional row-level notes may identify suspected influence associated with [[Khadir Veyran]] (''Keeper of the Bromian Ledger'') or [[Suyana Amaruq]] (''Black-Thread Suyana'').<br>" ..
end
"<small>These notes are seeded and probabilistic. They do not appear in every unrest or compliance row; trade-signoff notes remain available only when reproducing historical dates through 27/3/53 PSSC. " ..
 
"Use '''adversarynodes=no''' to disable them, '''adversarychance=35''' to adjust unrest-note frequency, '''complianceadversarychance=18''' to adjust compliance-note frequency, and '''tradeadversarychance=22''' only for historical trade-signoff rows.</small>"
 
return "'''Temple Bank adversarial-node tracking active:''' from 60/2/53 PSSC onward, occasional row-level notes may identify suspected influence associated with [[Khadir Veyran]] (''Keeper of the Bromian Ledger'') or [[Suyana Amaruq]] (''Black-Thread Suyana'').<br><small>These notes are seeded and probabilistic. They do not appear in every unrest, compliance, or trade-signoff row. Use '''adversarynodes=no''' to disable them, '''adversarychance=35''' to adjust unrest-note frequency, '''complianceadversarychance=18''' to adjust compliance-note frequency, and '''tradeadversarychance=22''' to adjust Temple Bank trade-signoff note frequency.</small>"
 
 
end
end


-- Forced City/Node display
-- Forced City/Node display
local forcedCityNodeText = "[[File:MyleciaFlabB.jpeg|20px]] [[Mylecia]]"
local forcedCityNodeText = "[[File:MyleciaFlabB.jpeg|20px]] [[Mylecia]]"


local function cleanHeaderLabel(line)
local function cleanHeaderLabel(line)
local s = tostring(line or "")
local s = tostring(line or "")
 
s = s:gsub("^%s*!+%s*", "")
 
if s:find("|", 1, true) then
s = s:gsub("^%s*!+%s*", "")
local parts = mw.text.split(s, "|", true)
 
s = parts[#parts] or s
if s:find("|", 1, true) then
end
local parts = mw.text.split(s, "|", true)
s = s:gsub("<.->", "")
s = parts[#parts] or s
s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
end
s = s:gsub("%[%[([^%]]+)%]%]", "%1")
 
s = s:gsub("&nbsp;", " ")
s = s:gsub("<.->", "")
s = s:gsub("%s*/%s*", "/")
s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
s = s:gsub("^%s+", ""):gsub("%s+$", "")
s = s:gsub("%[%[([^%]]+)%]%]", "%1")
return s:lower()
s = s:gsub("&nbsp;", " ")
s = s:gsub("%s*/%s*", "/")
s = s:gsub("^%s+", ""):gsub("%s+$", "")
 
return s:lower()
 
 
end
end


local cityNodeHeaderLabels = {
local cityNodeHeaderLabels = {
["city"] = true,
["city"] = true,
["node"] = true,
["node"] = true,
["city/node"] = true,
["city/node"] = true,
["city / node"] = true,
["city / node"] = true,
["city or node"] = true,
["city or node"] = true,
["home port"] = true,
["home port"] = true,
["homeport"] = true,
["homeport"] = true,
["home city"] = true,
["home city"] = true,
["base city"] = true,
["base city"] = true,
["staging city"] = true
["staging city"] = true
}
}


local function forceMyleciaCityNodeColumn(body, args)
local function forceMyleciaCityNodeColumn(body, args)
body = tostring(body or "")
body = tostring(body or "")
if yn(args.forcemylecia or args.forceMylecia or args.myleciacity or args.myleciaCity, true) == false then
return body
end
if not body:find("{|", 1, true) then return body end


local forcedText = args.forcecitytext or args.forceCityText or forcedCityNodeText
local lines = mw.text.split(body, "\n", true)
local out = {}
local inTable = false
local cityCol = nil
local col = 0


if yn(args.forcemylecia or args.forceMylecia or args.myleciacity or args.myleciaCity, true) == false then
for _, line in ipairs(lines) do
return body
if line:match("^%s*{|") then
end
inTable = true
 
cityCol = nil
if not body:find("{|", 1, true) then
col = 0
return body
table.insert(out, line)
end
elseif inTable and line:match("^%s*!") then
 
local raw = line:gsub("^%s*!+%s*", "")
local forcedText = args.forcecitytext or args.forceCityText or forcedCityNodeText
local headers = mw.text.split(raw, "!!", true)
local lines = mw.text.split(body, "\n", true)
if #headers > 1 then
local out = {}
for _, h in ipairs(headers) do
 
col = col + 1
local inTable = false
if cityNodeHeaderLabels[cleanHeaderLabel("! " .. h)] then
local cityCol = nil
cityCol = col
local col = 0
end
 
end
for _, line in ipairs(lines) do
else
if line:match("^%s*{|") then
inTable = true
cityCol = nil
col = 0
table.insert(out, line)
 
elseif inTable and line:match("^%s*!") then
local raw = line:gsub("^%s*!+%s*", "")
local headers = mw.text.split(raw, "!!", true)
 
if #headers > 1 then
for _, h in ipairs(headers) do
col = col + 1
col = col + 1
if cityNodeHeaderLabels[cleanHeaderLabel("! " .. h)] then
if cityNodeHeaderLabels[cleanHeaderLabel(line)] then
cityCol = col
cityCol = col
end
end
end
end
else
table.insert(out, line)
col = col + 1
elseif inTable and line:match("^%s*|%-") then
if cityNodeHeaderLabels[cleanHeaderLabel(line)] then
col = 0
cityCol = col
table.insert(out, line)
end
elseif inTable and cityCol and line:match("^%s*|[^}%-%+]") then
end
local raw = line:gsub("^%s*|%s*", "")
 
local cells = mw.text.split(raw, "||", true)
table.insert(out, line)
if #cells > 1 then
 
for i = 1, #cells do
elseif inTable and line:match("^%s*|%-") then
if i == cityCol then cells[i] = " " .. forcedText .. " " end
col = 0
end
table.insert(out, line)
table.insert(out, "| " .. table.concat(cells, " || "))
 
else
elseif inTable and cityCol and line:match("^%s*|[^}%-%+]") then
col = col + 1
local raw = line:gsub("^%s*|%s*", "")
if col == cityCol then
local cells = mw.text.split(raw, "||", true)
table.insert(out, "| " .. forcedText)
 
else
if #cells > 1 then
table.insert(out, line)
for i = 1, #cells do
if i == cityCol then
cells[i] = " " .. forcedText .. " "
end
end
end
end
table.insert(out, "| " .. table.concat(cells, " || "))
elseif inTable and line:match("^%s*|}") then
inTable = false
table.insert(out, line)
else
else
col = col + 1
table.insert(out, line)
if col == cityCol then
table.insert(out, "| " .. forcedText)
else
table.insert(out, line)
end
end
end
elseif inTable and line:match("^%s*|}") then
inTable = false
table.insert(out, line)
else
table.insert(out, line)
end
end
end
return table.concat(out, "\n")
 
return table.concat(out, "\n")
 
 
end
end


-- Hatch Ministry Home city / Current station swap
-- Hatch Ministry Home city / Current station swap
local function swapHatchHomeCityCurrentStation(body, args)
local function swapHatchHomeCityCurrentStation(body, args)
body = tostring(body or "")
body = tostring(body or "")
if yn(args.swaphatchstation or args.swapHatchStation or args.swaphatchhomecity or args.swapHatchHomeCity, true) == false then
return body
end
if not body:find("{|", 1, true) then return body end


local lines = mw.text.split(body, "\n", true)
local out = {}
local inTable = false
local homeCol = nil
local stationCol = nil
local col = 0


if yn(args.swaphatchstation or args.swapHatchStation or args.swaphatchhomecity or args.swapHatchHomeCity, true) == false then
for _, line in ipairs(lines) do
return body
if line:match("^%s*{|") then
end
inTable = true
 
homeCol = nil
if not body:find("{|", 1, true) then
stationCol = nil
return body
col = 0
end
table.insert(out, line)
 
elseif inTable and line:match("^%s*!") then
local lines = mw.text.split(body, "\n", true)
local raw = line:gsub("^%s*!+%s*", "")
local out = {}
local headers = mw.text.split(raw, "!!", true)
 
if #headers > 1 then
local inTable = false
for _, h in ipairs(headers) do
local homeCol = nil
col = col + 1
local stationCol = nil
local label = cleanHeaderLabel("! " .. h)
local col = 0
if label == "home city" or label == "home" then
 
homeCol = col
for _, line in ipairs(lines) do
elseif label == "current station" or label == "station" then
if line:match("^%s*{|") then
stationCol = col
inTable = true
end
homeCol = nil
end
stationCol = nil
else
col = 0
table.insert(out, line)
 
elseif inTable and line:match("^%s*!") then
local raw = line:gsub("^%s*!+%s*", "")
local headers = mw.text.split(raw, "!!", true)
 
if #headers > 1 then
for _, h in ipairs(headers) do
col = col + 1
col = col + 1
local label = cleanHeaderLabel("! " .. h)
local label = cleanHeaderLabel(line)
 
if label == "home city" or label == "home" then
if label == "home city" or label == "home" then
homeCol = col
homeCol = col
Line 1,102: Line 891:
end
end
end
end
else
table.insert(out, line)
col = col + 1
elseif inTable and line:match("^%s*|%-") then
local label = cleanHeaderLabel(line)
col = 0
 
table.insert(out, line)
if label == "home city" or label == "home" then
elseif inTable and homeCol and stationCol and line:match("^%s*|[^}%-%+]") then
homeCol = col
local raw = line:gsub("^%s*|%s*", "")
elseif label == "current station" or label == "station" then
local cells = mw.text.split(raw, "||", true)
stationCol = col
if #cells > 1 then
if cells[homeCol] and cells[stationCol] then
cells[homeCol], cells[stationCol] = cells[stationCol], cells[homeCol]
end
table.insert(out, "| " .. table.concat(cells, " || "))
else
col = col + 1
table.insert(out, line)
end
end
end
elseif inTable and line:match("^%s*|}") then
 
inTable = false
table.insert(out, line)
table.insert(out, line)
 
elseif inTable and line:match("^%s*|%-") then
col = 0
table.insert(out, line)
 
elseif inTable and homeCol and stationCol and line:match("^%s*|[^}%-%+]") then
local raw = line:gsub("^%s*|%s*", "")
local cells = mw.text.split(raw, "||", true)
 
if #cells > 1 then
if cells[homeCol] and cells[stationCol] then
cells[homeCol], cells[stationCol] = cells[stationCol], cells[homeCol]
end
table.insert(out, "| " .. table.concat(cells, " || "))
else
else
col = col + 1
table.insert(out, line)
table.insert(out, line)
end
end
elseif inTable and line:match("^%s*|}") then
inTable = false
table.insert(out, line)
else
table.insert(out, line)
end
end
return table.concat(out, "\n")
end
end


return table.concat(out, "\n")
-- Trade-agreement generation cutoff
-- The Nysara Velai sign-off on 27/3/53 PSSC concluded the locality-alignment phase.
-- Historical output through that date remains reproducible, but all dates from
-- 28/3/53 PSSC onward hard-return '-' in the Trade agreement column.
local TRADE_SIGNOFF_FINAL_YEAR = 53
local TRADE_SIGNOFF_FINAL_DAY = 149 -- 27/3/53 PSSC


local function tradeAgreementGenerationClosed(args)
local year = tonumber(args.year or args.psscyear) or 0
local day = tonumber(args.day or args.dayofyear) or 0


if year > TRADE_SIGNOFF_FINAL_YEAR then
return true
end
if year == TRADE_SIGNOFF_FINAL_YEAR and day > TRADE_SIGNOFF_FINAL_DAY then
return true
end
return false
end
end


-- Cult sign-off list for Temple Bank trade agreements
-- Cult sign-off list for Temple Bank trade agreements
local tradeSigningCults = {
local tradeSigningCults = {
{ code = "UMB", label = "[[Noctis (Reformed Stripping Path)#Order of the Umbral Oracle|Order of the Umbral Oracle]]" },
{ code = "UMB", label = "[[Noctis (Reformed Stripping Path)#Order of the Umbral Oracle|Order of the Umbral Oracle]]" },
{ code = "GGS", label = "[[Chrysos (Reformed Stripping Path)#Guild of Golden Shadows|Guild of Golden Shadows]]" },
{ code = "GGS", label = "[[Chrysos (Reformed Stripping Path)#Guild of Golden Shadows|Guild of Golden Shadows]]" },
{ code = "AZS", label = "[[Thalassa (Reformed Stripping Path)#Azure Sentinel Sect|Azure Sentinel Sect]]" },
{ code = "AZS", label = "[[Thalassa (Reformed Stripping Path)#Azure Sentinel Sect|Azure Sentinel Sect]]" },
{ code = "HAR", label = "[[Micras (Reformed Stripping Path)#Harmony Sanctum|Harmony Sanctum]]" },
{ code = "HAR", label = "[[Micras (Reformed Stripping Path)#Harmony Sanctum|Harmony Sanctum]]" },
{ code = "VIT", label = "[[Plateau (Reformed Stripping Path)#Sanctum Vitalis|Sanctum Vitalis]]" },
{ code = "VIT", label = "[[Plateau (Reformed Stripping Path)#Sanctum Vitalis|Sanctum Vitalis]]" },
{ code = "AUR", label = "[[Eos (Reformed Stripping Path)#Order of Aurora Mystica|Order of Aurora Mystica]]" },
{ code = "AUR", label = "[[Eos (Reformed Stripping Path)#Order of Aurora Mystica|Order of Aurora Mystica]]" },
{ code = "DEL", label = "[[Sisera (Reformed Stripping Path)#Sanctum Delphica|Sanctum Delphica]]" },
{ code = "DEL", label = "[[Sisera (Reformed Stripping Path)#Sanctum Delphica|Sanctum Delphica]]" },
{ code = "CAT", label = "[[Cato (Reformed Stripping Path)#Rex Catonis|Rex Catonis]]" },
{ code = "CAT", label = "[[Cato (Reformed Stripping Path)#Rex Catonis|Rex Catonis]]" },
{ code = "CON", label = "[[Lukedu (Reformed Stripping Path)#Accord Concordia|Accord Concordia]]" },
{ code = "CON", label = "[[Lukedu (Reformed Stripping Path)#Accord Concordia|Accord Concordia]]" },
{ code = "STY", label = "[[Styx (Reformed Stripping Path)#Mystery of the Stygian Veil|Mystery of the Stygian Veil]]" }
{ code = "STY", label = "[[Styx (Reformed Stripping Path)#Mystery of the Stygian Veil|Mystery of the Stygian Veil]]" }
}
}


local tradeOutcomes = {
local tradeOutcomes = {
"Cleared for General Port ledger review.",
"Cleared for General Port ledger review.",
"Provisional admission recommended.",
"Provisional admission recommended.",
"Conditional trade window approved.",
"Conditional trade window approved.",
"Company charter accepted for corridor listing.",
"Company charter accepted for corridor listing.",
"Trade agreement filed through ordinary registry channels."
"Trade agreement filed through ordinary registry channels."
}
}


local function tradeAgreementCell(args, channel, rowNumber, loc)
local function tradeAgreementCell(args, channel, rowNumber, loc)
if tostring(channel or "") ~= "missionary" then
if tostring(channel or "") ~= "missionary" then return "-" end
return "-"
if not loc then return "-" end
end
if tradeAgreementGenerationClosed(args) then return "-" end


local chance = imperialSupportBoostedChance(
args,
loc,
tonumber(args.tradechance or args.tradeChance) or 3,
3
)
chance = nysaraVelaiAdjustedTradeChance(args, loc, chance)


if not loc then
local year = tonumber(args.year or args.psscyear) or 0
return "-"
local day = tonumber(args.day or args.dayofyear) or 0
end
local seed = tostring(args.seed or "") ..
":SS-TEMPLE-TRADE-COL:" .. tostring(rowNumber or 0) ..
":" .. tostring(loc.code or "GEN") ..
":" .. tostring(year) ..
":" .. tostring(day)


local chance = imperialSupportBoostedChance(args, loc, tonumber(args.tradechance or args.tradeChance) or 3, 3)
local roll = pctFromSeed(seed, "roll") + 1
if roll > chance then
if yn(args.showTradeNoEvent or args.showtradenoevent, false) then return "No sign-off" end
return "-"
end


local year = tonumber(args.year or args.psscyear) or 0
local cult = pick(tradeSigningCults, seed, "cult")
local day = tonumber(args.day or args.dayofyear) or 0
local outcome = pick(tradeOutcomes, seed, "outcome")
if not cult then return "-" end


local seed = tostring(args.seed or "") ..
local seq = (pctFromSeed(seed, "seq") % 20) + 1
":SS-TEMPLE-TRADE-COL:" .. tostring(rowNumber or 0) ..
local tradeId
":" .. tostring(loc.code or "GEN") ..
if year > 0 and day > 0 then
":" .. tostring(year) ..
tradeId = string.format("GP-SS-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
":" .. tostring(day)
else
 
tradeId = string.format("GP-SS-%s-%02d", loc.code or "GEN", seq)
local roll = pctFromSeed(seed, "roll") + 1
 
if roll > chance then
if yn(args.showTradeNoEvent or args.showtradenoevent, false) then
return "No sign-off"
end
end
 
local signCode = string.format("%s-TA%02d", cult.code or "CUL", seq)
return "-"
local advNote = adversaryNotePart(args, "trade", seed, nil, loc)
end
return "'''Trade sign-off'''<br>" ..
 
"<small>" .. tradeId .. "</small><br>" ..
local cult = pick(tradeSigningCults, seed, "cult")
(cult.label or "Cult sign-off recorded") .. "<br>" ..
local outcome = pick(tradeOutcomes, seed, "outcome")
"<small>Code: '''" .. signCode .. "'''<br>" .. outcome .. advNote .. "</small>"
 
if not cult then
return "-"
end
 
local seq = (pctFromSeed(seed, "seq") % 20) + 1
local tradeId
 
if year > 0 and day > 0 then
tradeId = string.format("GP-SS-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
else
tradeId = string.format("GP-SS-%s-%02d", loc.code or "GEN", seq)
end
 
local signCode = string.format("%s-TA%02d", cult.code or "CUL", seq)
local advNote = adversaryNotePart(args, "trade", seed, nil, loc)
 
return "'''Trade sign-off'''<br>" ..
"<small>" .. tradeId .. "</small><br>" ..
(cult.label or "Cult sign-off recorded") .. "<br>" ..
"<small>Code: '''" .. signCode .. "'''<br>" .. outcome .. advNote .. "</small>"
 
 
end
end


-- Rebellion / corridor unrest row column
-- Rebellion / corridor unrest row column
local rebellionProfiles = {
local rebellionProfiles = {
{
{ label = "Confederacy-adjacent courier screen", description = "False-route couriers testing inspection gaps.", adversaries = { "SUYANA" } },
label = "Confederacy-adjacent courier screen",
{ label = "Former Maritime Markets broker faction", description = "Old broker-house retainers resisting General Port paperwork.", adversaries = { "KHADIR" } },
description = "False-route couriers testing inspection gaps.",
{ label = "Anti-registry dock syndicate", description = "Warehouse intermediaries opposing voucher and ledger discipline.", adversaries = { "KHADIR" } },
adversaries = { "SUYANA" }
{ label = "False-proclamation circle", description = "Pamphlet readers circulating forged civic orders.", adversaries = { "KHADIR", "SUYANA" } },
},
{ label = "Route-barricade committee", description = "Agitators obstructing lawful inspection lanes.", adversaries = { "SUYANA" } },
{
{ label = "Dispossessed-sympathetic quay cell", description = "Waterfront cell copying Confederacy-style courier habits.", adversaries = { "SUYANA" } },
label = "Former Maritime Markets broker faction",
{ label = "Illicit ritual-cargo and medical-transfer exploiters", description = "Handlers using illness, mourning, ritual cargo, or shrine protection to delay inspection.", adversaries = { "SUYANA" } },
description = "Old broker-house retainers resisting General Port paperwork.",
{ label = "Bromian Ledger witness screen", description = "Broker-linked witnesses preserving conflicting older records against General Port review.", adversaries = { "KHADIR" } }
adversaries = { "KHADIR" }
},
{
label = "Anti-registry dock syndicate",
description = "Warehouse intermediaries opposing voucher and ledger discipline.",
adversaries = { "KHADIR" }
},
{
label = "False-proclamation circle",
description = "Pamphlet readers circulating forged civic orders.",
adversaries = { "KHADIR", "SUYANA" }
},
{
label = "Route-barricade committee",
description = "Agitators obstructing lawful inspection lanes.",
adversaries = { "SUYANA" }
},
{
label = "Dispossessed-sympathetic quay cell",
description = "Waterfront cell copying Confederacy-style courier habits.",
adversaries = { "SUYANA" }
},
{
label = "Illicit ritual-cargo and medical-transfer exploiters",
description = "Handlers using illness, mourning, ritual cargo, or shrine protection to delay inspection.",
adversaries = { "SUYANA" }
},
{
label = "Bromian Ledger witness screen",
description = "Broker-linked witnesses preserving conflicting older records against General Port review.",
adversaries = { "KHADIR" }
}
}
}


local rebellionTriggers = {
local rebellionTriggers = {
"Forged levy notice.",
"Forged levy notice.",
"Masked speakers redirected traffic.",
"Masked speakers redirected traffic.",
"Missing seals and altered registry slips.",
"Missing seals and altered registry slips.",
"Crowd pressure over bonded cargo.",
"Crowd pressure over bonded cargo.",
"Tampered signal schedule.",
"Tampered signal schedule.",
"Conflicting route orders.",
"Conflicting route orders.",
"Coordinated paper strike.",
"Coordinated paper strike.",
"Anti-Bassaridian chant movement.",
"Anti-Bassaridian chant movement.",
"Repeated courier handoffs.",
"Repeated courier handoffs.",
"Matched false manifests.",
"Matched false manifests.",
"False medical-transfer exemption.",
"False medical-transfer exemption.",
"Unreconciled caravan lodging roll.",
"Unreconciled caravan lodging roll.",
"Guide-house witness contradiction.",
"Guide-house witness contradiction.",
"Older Maritime Markets clearance invoked."
"Older Maritime Markets clearance invoked."
}
}


local rebellionActions = {
local rebellionActions = {
"CKD cordon and evidence hold.",
"CKD cordon and evidence hold.",
"Temple rumor-control desk opened.",
"Temple rumor-control desk opened.",
"Ledgers and copied seals sealed.",
"Ledgers and copied seals sealed.",
"Privateer stand-off posture.",
"Privateer stand-off posture.",
"Pass papers reissued.",
"Pass papers reissued.",
"Lawful cargo separated.",
"Lawful cargo separated.",
"Missionary mediation opened.",
"Missionary mediation opened.",
"Corrected signal notice issued.",
"Corrected signal notice issued.",
"Temporary General Port audit lane opened.",
"Temporary General Port audit lane opened.",
"Incident classified as contained unrest.",
"Incident classified as contained unrest.",
"Guide-license review opened.",
"Guide-license review opened.",
"Medical-transfer custody ledger corrected.",
"Medical-transfer custody ledger corrected.",
"Broker-house witnesses summoned for countersignature.",
"Broker-house witnesses summoned for countersignature.",
"Night-road registry temporarily frozen."
"Night-road registry temporarily frozen."
}
}


local rebellionRisks = {
local rebellionRisks = { "Low", "Moderate", "High", "Contained", "Escalating" }
"Low",
local rebellionAlertExcludedLocalities = { MIR = true, SAR = true, THQ = true }
"Moderate",
"High",
"Contained",
"Escalating"
}
 
local rebellionAlertExcludedLocalities = {
MIR = true,
SAR = true,
THQ = true
}


local function isRebellionAlertExcluded(loc)
local function isRebellionAlertExcluded(loc)
if not loc then
if not loc then return false end
return false
return rebellionAlertExcludedLocalities[tostring(loc.code or "")] == true
end
 
 
return rebellionAlertExcludedLocalities[tostring(loc.code or "")] == true
 
 
end
end


local function rebellionAlertCell(args, channel, rowNumber, loc)
local function rebellionAlertCell(args, channel, rowNumber, loc)
if not loc then
if not loc then return "-" end
return "-"
if isRebellionAlertExcluded(loc) then return "-" end
end


local baseChance = tonumber(args.rebellionchance or args.rebellionChance or args.unrestchance or args.unrestChance) or 6
local chance = missionAdjustedUnrestChance(args, loc, baseChance)
chance = nysaraVelaiAdjustedUnrestChance(args, loc, chance)


if isRebellionAlertExcluded(loc) then
local year = tonumber(args.year or args.psscyear) or 0
return "-"
local day = tonumber(args.day or args.dayofyear) or 0
end
local seed = tostring(args.seed or "") ..
":SS-REBELLION-COL:" .. tostring(channel or "") ..
":" .. tostring(rowNumber or 0) ..
":" .. tostring(loc.code or "GEN") ..
":" .. tostring(year) ..
":" .. tostring(day)


local baseChance = tonumber(args.rebellionchance or args.rebellionChance or args.unrestchance or args.unrestChance) or 6
local roll = pctFromSeed(seed, "roll") + 1
local chance = missionAdjustedUnrestChance(args, loc, baseChance)
if roll > chance then
chance = qhapaqSelenAdjustedUnrestChance(args, loc, chance)
if yn(args.showRebellionNoEvent or args.showrebellionnoevent or args.showUnrestNoEvent or args.showunrestnoevent, false) then
return "No unrest"
end
return "-"
end


local year = tonumber(args.year or args.psscyear) or 0
local profile = pick(rebellionProfiles, seed, "profile")
local day = tonumber(args.day or args.dayofyear) or 0
local trigger = pick(rebellionTriggers, seed, "trigger")
local action = pick(rebellionActions, seed, "action")
local risk = pick(rebellionRisks, seed, "risk")
if not profile then return "-" end


local seed = tostring(args.seed or "") ..
local seq = (pctFromSeed(seed, "seq") % 20) + 1
":SS-REBELLION-COL:" .. tostring(channel or "") ..
local eventId
":" .. tostring(rowNumber or 0) ..
if year > 0 and day > 0 then
":" .. tostring(loc.code or "GEN") ..
eventId = string.format("SS-RB-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
":" .. tostring(year) ..
else
":" .. tostring(day)
eventId = string.format("SS-RB-%s-%02d", loc.code or "GEN", seq)
 
local roll = pctFromSeed(seed, "roll") + 1
 
if roll > chance then
if yn(args.showRebellionNoEvent or args.showrebellionnoevent or args.showUnrestNoEvent or args.showunrestnoevent, false) then
return "No unrest"
end
end
 
local advNote = adversaryNotePart(args, "unrest", seed, profile, loc)
return "-"
return "'''Unrest alert'''<br>" ..
end
"<small>" .. eventId .. "</small><br>" ..
 
"'''Profile:''' " .. profile.label .. "<br>" ..
local profile = pick(rebellionProfiles, seed, "profile")
"<small>" .. profile.description .. "<br>" ..
local trigger = pick(rebellionTriggers, seed, "trigger")
"'''Trigger:''' " .. trigger .. "<br>" ..
local action = pick(rebellionActions, seed, "action")
"'''Risk:''' " .. risk .. "<br>" ..
local risk = pick(rebellionRisks, seed, "risk")
"'''Response:''' " .. action .. advNote .. "</small>"
 
if not profile then
return "-"
end
 
local seq = (pctFromSeed(seed, "seq") % 20) + 1
local eventId
 
if year > 0 and day > 0 then
eventId = string.format("SS-RB-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
else
eventId = string.format("SS-RB-%s-%02d", loc.code or "GEN", seq)
end
 
local advNote = adversaryNotePart(args, "unrest", seed, profile, loc)
 
return "'''Unrest alert'''<br>" ..
"<small>" .. eventId .. "</small><br>" ..
"'''Profile:''' " .. profile.label .. "<br>" ..
"<small>" .. profile.description .. "<br>" ..
"'''Trigger:''' " .. trigger .. "<br>" ..
"'''Risk:''' " .. risk .. "<br>" ..
"'''Response:''' " .. action .. advNote .. "</small>"
 
 
end
end


-- Compliance observed / minor positive row column
-- Compliance observed / minor positive row column
local complianceProfiles = {
local complianceProfiles = {
{
{ label = "Inspection cooperation", description = "Local carriers complied with inspection sequencing without requiring escalation.", adversaries = { "SUYANA" } },
label = "Inspection cooperation",
{ label = "Ledger correction", description = "Route clerks corrected incomplete or inconsistent paperwork before formal penalty.", adversaries = { "KHADIR" } },
description = "Local carriers complied with inspection sequencing without requiring escalation.",
{ label = "Crowd dispersal", description = "Local stewards dispersed a gathering before it became an obstruction.", adversaries = { "SUYANA" } },
adversaries = { "SUYANA" }
{ label = "Guide assistance", description = "Licensed guides helped separate lawful traffic from suspect courier movement.", adversaries = { "SUYANA" } },
},
{ label = "Cargo separation", description = "Bonded cargo was voluntarily separated for review without disrupting ordinary passage.", adversaries = { "KHADIR" } },
{
{ label = "Rumor-control compliance", description = "Temple notices were accepted locally and hostile rumors lost momentum.", adversaries = { "KHADIR", "SUYANA" } },
label = "Ledger correction",
{ label = "Privateer signal compliance", description = "Vessels accepted stand-off or hold-and-release signals without resistance.", adversaries = { "SUYANA" } },
description = "Route clerks corrected incomplete or inconsistent paperwork before formal penalty.",
{ label = "Registry cooperation", description = "Local registrars preserved names, seals, and witness records for later review.", adversaries = { "KHADIR" } },
adversaries = { "KHADIR" }
{ label = "Medical-transfer ledger cooperation", description = "Clinic escorts preserved patient custody records without accepting false sacred exemptions.", adversaries = { "SUYANA" } }
},
{
label = "Crowd dispersal",
description = "Local stewards dispersed a gathering before it became an obstruction.",
adversaries = { "SUYANA" }
},
{
label = "Guide assistance",
description = "Licensed guides helped separate lawful traffic from suspect courier movement.",
adversaries = { "SUYANA" }
},
{
label = "Cargo separation",
description = "Bonded cargo was voluntarily separated for review without disrupting ordinary passage.",
adversaries = { "KHADIR" }
},
{
label = "Rumor-control compliance",
description = "Temple notices were accepted locally and hostile rumors lost momentum.",
adversaries = { "KHADIR", "SUYANA" }
},
{
label = "Privateer signal compliance",
description = "Vessels accepted stand-off or hold-and-release signals without resistance.",
adversaries = { "SUYANA" }
},
{
label = "Registry cooperation",
description = "Local registrars preserved names, seals, and witness records for later review.",
adversaries = { "KHADIR" }
},
{
label = "Medical-transfer ledger cooperation",
description = "Clinic escorts preserved patient custody records without accepting false sacred exemptions.",
adversaries = { "SUYANA" }
}
}
}


local complianceOutcomes = {
local complianceOutcomes = {
"Minor positive compliance noted.",
"Minor positive compliance noted.",
"Recorded as a stabilizing local action.",
"Recorded as a stabilizing local action.",
"Accepted as offsetting evidence against wider noncompliance.",
"Accepted as offsetting evidence against wider noncompliance.",
"Filed as a documented good-faith response.",
"Filed as a documented good-faith response.",
"Preserved for future compliance comparison.",
"Preserved for future compliance comparison.",
"Marked as below trade-agreement threshold but operationally helpful."
"Marked as below trade-agreement threshold but operationally helpful."
}
}


local function complianceObservedCell(args, channel, rowNumber, loc)
local function complianceObservedCell(args, channel, rowNumber, loc)
if not loc then
if not loc then return "-" end
return "-"
local baseChance = tonumber(args.compliancechance or args.complianceChance or args.observedchance or args.observedChance) or 8
end
local chance = missionAdjustedComplianceChance(args, loc, baseChance)
chance = imperialSupportBoostedChance(args, loc, chance, 10)
chance = nysaraVelaiAdjustedComplianceChance(args, loc, chance)


local year = tonumber(args.year or args.psscyear) or 0
local day = tonumber(args.day or args.dayofyear) or 0
local seed = tostring(args.seed or "") ..
":SS-COMPLIANCE-COL:" .. tostring(channel or "") ..
":" .. tostring(rowNumber or 0) ..
":" .. tostring(loc.code or "GEN") ..
":" .. tostring(year) ..
":" .. tostring(day)


local baseChance = tonumber(args.compliancechance or args.complianceChance or args.observedchance or args.observedChance) or 8
local roll = pctFromSeed(seed, "roll") + 1
local chance = missionAdjustedComplianceChance(args, loc, baseChance)
if roll > chance then
chance = imperialSupportBoostedChance(args, loc, chance, 10)
if yn(args.showComplianceNoEvent or args.showcompliancenoevent, false) then return "No observation" end
chance = qhapaqSelenAdjustedComplianceChance(args, loc, chance)
return "-"
end


local year = tonumber(args.year or args.psscyear) or 0
local profile = pick(complianceProfiles, seed, "profile")
local day = tonumber(args.day or args.dayofyear) or 0
local outcome = pick(complianceOutcomes, seed, "outcome")
if not profile then return "-" end


local seed = tostring(args.seed or "") ..
local seq = (pctFromSeed(seed, "seq") % 20) + 1
":SS-COMPLIANCE-COL:" .. tostring(channel or "") ..
local eventId
":" .. tostring(rowNumber or 0) ..
if year > 0 and day > 0 then
":" .. tostring(loc.code or "GEN") ..
eventId = string.format("SS-CO-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
":" .. tostring(year) ..
else
":" .. tostring(day)
eventId = string.format("SS-CO-%s-%02d", loc.code or "GEN", seq)
 
local roll = pctFromSeed(seed, "roll") + 1
 
if roll > chance then
if yn(args.showComplianceNoEvent or args.showcompliancenoevent, false) then
return "No observation"
end
end
 
local advNote = adversaryNotePart(args, "compliance", seed, profile, loc)
return "-"
return "'''Compliance observed'''<br>" ..
end
"<small>" .. eventId .. "</small><br>" ..
 
"'''Profile:''' " .. profile.label .. "<br>" ..
local profile = pick(complianceProfiles, seed, "profile")
"<small>" .. profile.description .. "<br>" ..
local outcome = pick(complianceOutcomes, seed, "outcome")
"'''Outcome:''' " .. outcome .. advNote .. "</small>"
 
if not profile then
return "-"
end
 
local seq = (pctFromSeed(seed, "seq") % 20) + 1
local eventId
 
if year > 0 and day > 0 then
eventId = string.format("SS-CO-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
else
eventId = string.format("SS-CO-%s-%02d", loc.code or "GEN", seq)
end
 
local advNote = adversaryNotePart(args, "compliance", seed, profile, loc)
 
return "'''Compliance observed'''<br>" ..
"<small>" .. eventId .. "</small><br>" ..
"'''Profile:''' " .. profile.label .. "<br>" ..
"<small>" .. profile.description .. "<br>" ..
"'''Outcome:''' " .. outcome .. advNote .. "</small>"
 
 
end
end


-- Row-column insertion helper
-- Row-column insertion helper
local function addCorridorColumns(body, args, channel)
local function addCorridorColumns(body, args, channel)
body = tostring(body or "")
body = tostring(body or "")
if not body:find("{|", 1, true) then return body end


local showLocalityColumn = yn(args.rowlocalities or args.localities or args.showlocalities, true)
local showTradeColumn = yn(args.tradecolumn or args.tradecolumns or args.showtradecolumn, true)
local showComplianceColumn = yn(args.compliancecolumn or args.compliancecolumns or args.observedcolumn or args.showcompliancecolumn, true)
local showRebellionColumn = yn(args.rebellioncolumn or args.rebellioncolumns or args.unrestcolumn or args.showrebellioncolumn, true)
if not showLocalityColumn and not showTradeColumn and not showComplianceColumn and not showRebellionColumn then
return body
end


if not body:find("{|", 1, true) then
local lines = mw.text.split(body, "\n", true)
return body
local out = {}
end
local inTable = false
local headerInserted = false
local rowNumber = 0
local localityState = newLocalityAssignmentState(args, channel)


local showLocalityColumn = yn(args.rowlocalities or args.localities or args.showlocalities, true)
for _, line in ipairs(lines) do
local showTradeColumn = yn(args.tradecolumn or args.tradecolumns or args.showtradecolumn, true)
if line:match("^%s*{|") then
local showComplianceColumn = yn(args.compliancecolumn or args.compliancecolumns or args.observedcolumn or args.showcompliancecolumn, true)
inTable = true
local showRebellionColumn = yn(args.rebellioncolumn or args.rebellioncolumns or args.unrestcolumn or args.showrebellioncolumn, true)
headerInserted = false
 
table.insert(out, line)
if not showLocalityColumn and not showTradeColumn and not showComplianceColumn and not showRebellionColumn then
elseif inTable and not headerInserted and line:match("^%s*!") then
return body
if showLocalityColumn then table.insert(out, "! Locality focus") end
end
if showTradeColumn then table.insert(out, "! Trade agreement") end
 
if showComplianceColumn then table.insert(out, "! Compliance observed") end
local lines = mw.text.split(body, "\n", true)
if showRebellionColumn then table.insert(out, "! Rebellion alert") end
local out = {}
headerInserted = true
 
table.insert(out, line)
local inTable = false
elseif inTable and line:match("^%s*|%-") then
local headerInserted = false
rowNumber = rowNumber + 1
local rowNumber = 0
local loc = rowLocalityData(args, channel, rowNumber, localityState)
local localityState = newLocalityAssignmentState(args, channel)
table.insert(out, line)
 
if showLocalityColumn then table.insert(out, "| " .. rowLocalityText(loc)) end
for _, line in ipairs(lines) do
if showTradeColumn then table.insert(out, "| " .. tradeAgreementCell(args, channel, rowNumber, loc)) end
if line:match("^%s*{|") then
if showComplianceColumn then table.insert(out, "| " .. complianceObservedCell(args, channel, rowNumber, loc)) end
inTable = true
if showRebellionColumn then table.insert(out, "| " .. rebellionAlertCell(args, channel, rowNumber, loc)) end
headerInserted = false
elseif inTable and line:match("^%s*|}") then
table.insert(out, line)
inTable = false
 
table.insert(out, line)
elseif inTable and not headerInserted and line:match("^%s*!") then
else
if showLocalityColumn then
table.insert(out, line)
table.insert(out, "! Locality focus")
end
end
if showTradeColumn then
table.insert(out, "! Trade agreement")
end
if showComplianceColumn then
table.insert(out, "! Compliance observed")
end
if showRebellionColumn then
table.insert(out, "! Rebellion alert")
end
headerInserted = true
table.insert(out, line)
elseif inTable and line:match("^%s*|%-") then
rowNumber = rowNumber + 1
local loc = rowLocalityData(args, channel, rowNumber, localityState)
table.insert(out, line)
if showLocalityColumn then
table.insert(out, "| " .. rowLocalityText(loc))
end
if showTradeColumn then
table.insert(out, "| " .. tradeAgreementCell(args, channel, rowNumber, loc))
end
if showComplianceColumn then
table.insert(out, "| " .. complianceObservedCell(args, channel, rowNumber, loc))
end
if showRebellionColumn then
table.insert(out, "| " .. rebellionAlertCell(args, channel, rowNumber, loc))
end
elseif inTable and line:match("^%s*|}") then
inTable = false
table.insert(out, line)
else
table.insert(out, line)
end
end
end
return table.concat(out, "\n")
 
return table.concat(out, "\n")
 
 
end
end


-- Main Southern Strait-only view
-- Main Southern Strait-only view
function p.makeTable(frame)
function p.makeTable(frame)
local args = normalizeArgsDate((frame and frame.args) or {})
local args = normalizeArgsDate((frame and frame.args) or {})
local showIntro = yn(args.intro, false)
local showTitle = yn(args.showtitle or args.titleline, false)
local showLegend = yn(args.legend or args.showlegend, false)
local collapseSections = yn(args.collapsible, true)
local startCollapsed = yn(args.collapsed, true)
local includeWar = yn(args.war or args.warleague, true)
local includeHatch = yn(args.hatch or args.hatchministry, true)
local includeMission = yn(args.missionary or args.templebank, true)
local year = args.year or args.psscyear
local day = args.day or args.dayofyear
local title = args.title or "Southern Strait Corridor Daily Operations"
local label = dateLabel(args)
local out = {}


if showTitle then table.insert(out, "==" .. title .. "==") end
if showIntro then
table.insert(out, string.format(
"''Southern Strait-only operational display for %s, limited to Mylecian Southern Strait corridor entries.''",
label
))
table.insert(out, "")
table.insert(out,
"This display combines Council of Kings deployments, Hatch Ministry privateer activity, Temple Bank missionary operations, row-level Southern Strait locality focus columns, row-level compliance observations, and row-level rebellion-type unrest alerts while displaying the operational City/Node as Mylecia. Historical Temple Bank trade sign-offs remain reproducible through 27/3/53 PSSC, but all later Trade agreement cells display '-'. Imperial Federation, Bassaridian Mission, capped-locality, CKD reinforcement concentration, and Temple Bank adversarial-node modifiers are applied where effective."
)
end


local showIntro = yn(args.intro, false)
local ifSupportNotice = imperialFederationSupportNotice(args)
local showTitle = yn(args.showtitle or args.titleline, false)
if ifSupportNotice ~= "" then
local showLegend = yn(args.legend or args.showlegend, false)
table.insert(out, ifSupportNotice)
local collapseSections = yn(args.collapsible, true)
table.insert(out, "")
local startCollapsed = yn(args.collapsed, true)
end
local includeWar = yn(args.war or args.warleague, true)
local includeHatch = yn(args.hatch or args.hatchministry, true)
local includeMission = yn(args.missionary or args.templebank, true)


local year = args.year or args.psscyear
local missionNotice = bassaridianMissionsNotice(args)
local day = args.day or args.dayofyear
if missionNotice ~= "" then
local title = args.title or "Southern Strait Corridor Daily Operations"
table.insert(out, missionNotice)
local label = dateLabel(args)
table.insert(out, "")
end


local out = {}
local adversaryNotice = adversarialNodesNotice(args)
if adversaryNotice ~= "" then
table.insert(out, adversaryNotice)
table.insert(out, "")
end


if showTitle then
if includeWar then
table.insert(out, "==" .. title .. "==")
local warArgs = {
end
year = year,
day = day,
rows = args.warrows or args.rows or "24",
scanrows = args.warscanrows or args.scanrows or "160",
showlegend = showLegend and "yes" or "no",
flagsize = args.flagsize or "20",
zone = "SouthernStrait",
division = "Council of Kings Division"
}
if args.seed and tostring(args.seed) ~= "" then warArgs.seed = args.seed end
local _, body = safeCall("Module:WarLeagueDivisionOpsBVR", "makeTable", warArgs)
body = forceMyleciaCityNodeColumn(body, args)
body = addCorridorColumns(body, args, "war")
table.insert(out, hr())
table.insert(out, section("Council of Kings Southern Strait deployments", body, collapseSections, startCollapsed))
end


if showIntro then
if includeHatch then
table.insert(out, string.format(
local hatchArgs = {
"''Southern Strait-only operational display for %s, limited to Mylecian Southern Strait corridor entries.''",
year = year,
label
day = day,
))
legend = showLegend and "yes" or "no",
table.insert(out, "")
zone = "SouthernStrait"
table.insert(out, "This display combines Council of Kings deployments, Hatch Ministry privateer activity, Temple Bank missionary operations, row-level Southern Strait locality focus columns, Temple Bank-only trade-agreement sign-offs, row-level compliance observations, and row-level rebellion-type unrest alerts while displaying the operational City/Node as Mylecia. Imperial Federation, Bassaridian Mission, capped-locality, Selenarion concentration, and Temple Bank adversarial-node modifiers are applied where effective.")
}
end
if args.captain and tostring(args.captain) ~= "" then hatchArgs.captain = args.captain end
 
local _, body = safeCall("Module:HatchMinistryDailyOps", "dailyReport", hatchArgs)
local ifSupportNotice = imperialFederationSupportNotice(args)
body = forceMyleciaCityNodeColumn(body, args)
if ifSupportNotice ~= "" then
body = swapHatchHomeCityCurrentStation(body, args)
table.insert(out, ifSupportNotice)
body = addCorridorColumns(body, args, "hatch")
table.insert(out, "")
table.insert(out, hr())
end
table.insert(out, section("Hatch Ministry Southern Strait privateer operations", body, collapseSections, startCollapsed))
 
local missionNotice = bassaridianMissionsNotice(args)
if missionNotice ~= "" then
table.insert(out, missionNotice)
table.insert(out, "")
end
 
local adversaryNotice = adversarialNodesNotice(args)
if adversaryNotice ~= "" then
table.insert(out, adversaryNotice)
table.insert(out, "")
end
 
if includeWar then
local warArgs = {
year = year,
day = day,
rows = args.warrows or args.rows or "24",
scanrows = args.warscanrows or args.scanrows or "160",
showlegend = showLegend and "yes" or "no",
flagsize = args.flagsize or "20",
zone = "SouthernStrait",
division = "Council of Kings Division"
}
 
if args.seed and tostring(args.seed) ~= "" then
warArgs.seed = args.seed
end
end


local ok, body = safeCall("Module:WarLeagueDivisionOpsBVR", "makeTable", warArgs)
if includeMission then
body = forceMyleciaCityNodeColumn(body, args)
local missionaryArgs = {
body = addCorridorColumns(body, args, "war")
year = year,
 
day = day,
table.insert(out, hr())
region = "Haifan",
table.insert(out, section("Council of Kings Southern Strait deployments", body, collapseSections, startCollapsed))
showlegend = showLegend and "yes" or "no",
end
includeThreat = args.includeThreat or args.includethreat or "no",
 
showEcon = args.showEcon or args.showecon or "no"
if includeHatch then
}
local hatchArgs = {
if args.missionarylimit and tostring(args.missionarylimit) ~= "" then
year = year,
missionaryArgs.limit = args.missionarylimit
day = day,
elseif args.limit and tostring(args.limit) ~= "" then
legend = showLegend and "yes" or "no",
missionaryArgs.limit = args.limit
zone = "SouthernStrait"
end
}
local _, body = safeCall("Module:MissionaryDailyOpsBVR", "makeTable", missionaryArgs)
 
body = forceMyleciaCityNodeColumn(body, args)
if args.captain and tostring(args.captain) ~= "" then
body = addCorridorColumns(body, args, "missionary")
hatchArgs.captain = args.captain
table.insert(out, hr())
table.insert(out, section("Temple Bank Southern Strait missionary operations", body, collapseSections, startCollapsed))
end
end


local ok, body = safeCall("Module:HatchMinistryDailyOps", "dailyReport", hatchArgs)
if showLegend then
body = forceMyleciaCityNodeColumn(body, args)
local legendBody = table.concat({
body = swapHatchHomeCityCurrentStation(body, args)
"* '''Council of Kings Southern Strait deployments''' show only Council of Kings Division War League rows for the Southern Strait corridor.",
body = addCorridorColumns(body, args, "hatch")
"* '''Hatch Ministry Southern Strait privateer operations''' show Southern Strait privateer rows.",
 
"* '''Temple Bank Southern Strait missionary operations''' show Haifan Bassaridian missionary rows, but the displayed City/Node is forced to Mylecia in this wrapper.",
table.insert(out, hr())
"* '''City/Node display''' is forced to [[File:MyleciaFlabB.jpeg|20px]] [[Mylecia]] for War League, Hatch Ministry, and Temple Bank rows.",
table.insert(out, section("Hatch Ministry Southern Strait privateer operations", body, collapseSections, startCollapsed))
"* '''Locality focus''' columns assign one extended Southern Strait, Eastern Mykonos Desert, or eastern Snowholme foothill settlement to each generated row when row-level localities are enabled.",
end
"* '''CKD reinforcement at Nysara Velai''' assigns six War League locality rows there by default, representing the earlier three deployments plus three additional CKD deployments.",
 
"* '''CKD reinforcement at Selenarion''' assigns two War League locality rows there by default, representing the earlier deployment plus one additional CKD deployment.",
if includeMission then
"* '''Hatch Ministry and Temple Bank locality targets''' remain at three Nysara Velai rows and one Selenarion row per section by default; the CKD reinforcement does not automatically increase those services.",
local missionaryArgs = {
"* '''Other capped presence localities''' are [[Mirovia]], [[Saruq Thyrsa]], and [[Thal Lenaia]]. Each may appear no more than once per section/channel.",
year = year,
"* '''Nysara Velai chance modifiers''' add +6 percentage points to compliance-observation likelihood and subtract 6 percentage points from unrest-alert likelihood. Its former +3-point trade-signoff modifier applies only to historical dates through 27/3/53 PSSC.",
day = day,
"* '''Trade agreement generation is closed''' from 28/3/53 PSSC onward. All channels display '''-''' in that column after the final Nysara Velai agreement; Temple Bank sign-offs remain reproducible only for historical dates through 27/3/53 PSSC.",
region = "Haifan",
"* '''Compliance observed''' columns show minor positive local behavior below the level of a formal trade agreement.",
showlegend = showLegend and "yes" or "no",
"* '''Rebellion alert''' columns show rare row-level contained unrest events such as forged proclamations, broker-cell activity, route barricades, false orders, ritual-cargo abuse, medical-transfer exploitation, or Dispossessed-adjacent courier behavior.",
includeThreat = args.includeThreat or args.includethreat or "no",
"* '''Mirovia''', '''Saruq Thyrsa''', and '''Thal Lenaia''' are hard-excluded from Rebellion Alert results; their Rebellion Alert cells always display '''-''', even when '''rebellionchance=100''' is used for testing.",
showEcon = args.showEcon or args.showecon or "no"
"* '''Recurring adversarial nodes''' apply from 60/2/53 PSSC by default. Eligible rows may occasionally mention [[Khadir Veyran]] or [[Suyana Amaruq]] as suspected influence, but the notes are not guaranteed to appear.",
}
"* '''Khadir Veyran''' notes are weighted toward broker factions, Bromian Ledger witness screens, anti-registry dock activity, cargo ambiguity, ledger correction, and trade-signoff counterweight language.",
 
"* '''Suyana Amaruq''' notes are weighted toward courier screens, route barricades, false-pilgrim or guide-house movement, medical-transfer exploitation, and Black-Thread route-intelligence behavior.",
if args.missionarylimit and tostring(args.missionarylimit) ~= "" then
"* '''Adversarial note frequency''' defaults to 35 percent for unrest alerts and 18 percent for compliance observations. The 22-percent trade-signoff note rate applies only to historical trade rows through 27/3/53 PSSC.",
missionaryArgs.limit = args.missionarylimit
"* '''Imperial Federation support''' applies from 112/53 PSSC by default and adds +10 percentage points to compliance observations for Saruq Thyrsa, Mirovia, and Rumi-Kefaya. Its former +3-point trade-signoff effect is historical only and cannot generate agreements after 27/3/53 PSSC.",
elseif args.limit and tostring(args.limit) ~= "" then
"* '''Bassaridian Missions support''' applies from 55/2/53 PSSC by default. It reduces unrest-alert chance by 1 percentage point in all locality rows and increases compliance-observation chance by 1 percentage point in all locality rows.",
missionaryArgs.limit = args.limit
"* '''Major Bassaridian Missions''' at Saruq Thyrsa, Mirovia, Nysara Velai, Thal Lenaia, and Selenarion receive an additional +2 percentage points to compliance observations.",
"* Use '''compliancechance=100''' and '''rebellionchance=100''' for testing. '''tradechance=100''' affects only historical dates through 27/3/53 PSSC and cannot override the post-agreement cutoff.",
"* Use '''warnysaravelaitarget=6''' to change only the War League / CKD Nysara Velai target.",
"* Use '''warselenariontarget=2''' to change only the War League / CKD Selenarion target.",
"* Use '''nysaravelaitarget=3''' or '''selenariontarget=1''' to override the corresponding target in every section/channel.",
"* Use '''nysaravelaioverfill=yes''' to allow Nysara Velai to exceed its section target after the priority rows are assigned.",
"* Use '''nysaravelaicomplianceboost=6''' and '''nysaravelaiunrestreduction=6''' to change the active Nysara Velai modifiers. '''nysaravelaitradeboost=3''' is retained solely for historical reproduction through 27/3/53 PSSC. The older '''nysaravelaiunrestboost=''' parameter remains available as an additive legacy override.",
"* Use '''adversarynodes=no''' to disable named adversarial notes.",
"* Use '''adversarychance=35''' to change the default unrest-alert adversarial note chance.",
"* Use '''complianceadversarychance=18''' to change the default compliance-observation adversarial note chance.",
"* Use '''tradeadversarychance=22''' only to change adversarial-note frequency in historical Temple Bank trade-signoff rows through 27/3/53 PSSC.",
"* Use '''adversarystartyear=53''' and '''adversarystartday=121''' to change the default 60/2/53 PSSC start date for named adversarial-node tracking.",
"* Use '''missionsupport=no''' to disable Bassaridian Missions modifiers for testing.",
"* Use '''imperialsupport=no''' to disable Imperial Federation support modifiers for testing.",
"* Use '''forcemylecia=no''' only if the wrapper should temporarily show the original source-module City/Node values."
}, "\n")
table.insert(out, hr())
table.insert(out, section("Combined legend", legendBody, collapseSections, startCollapsed))
end
end


local ok, body = safeCall("Module:MissionaryDailyOpsBVR", "makeTable", missionaryArgs)
return table.concat(out, "\n")
body = forceMyleciaCityNodeColumn(body, args)
body = addCorridorColumns(body, args, "missionary")
 
table.insert(out, hr())
table.insert(out, section("Temple Bank Southern Strait missionary operations", body, collapseSections, startCollapsed))
end
 
if showLegend then
local legendBody = table.concat({
"* '''Council of Kings Southern Strait deployments''' show only Council of Kings Division War League rows for the Southern Strait corridor.",
"* '''Hatch Ministry Southern Strait privateer operations''' show Southern Strait privateer rows.",
"* '''Temple Bank Southern Strait missionary operations''' show Haifan Bassaridian missionary rows, but the displayed City/Node is forced to Mylecia in this wrapper.",
"* '''City/Node display''' is forced to [[File:MyleciaFlabB.jpeg|20px]] [[Mylecia]] for War League, Hatch Ministry, and Temple Bank rows.",
"* '''Locality focus''' columns assign one extended Southern Strait, Eastern Mykonos Desert, or eastern Snowholme foothill settlement to each generated row when row-level localities are enabled.",
"* '''Capped presence localities''' are [[Mirovia]], [[Saruq Thyrsa]], and [[Thal Lenaia]]. Each may appear no more than once per section/channel: one War League row, one Hatch Ministry row, and one Temple Bank row by default.",
"* '''Selenarion concentration''' targets up to three locality rows per section/channel by default: three War League rows, three Hatch Ministry rows, and three Temple Bank rows when enough rows are available.",
"* '''Selenarion chance modifiers''' add +4 percentage points to unrest-alert likelihood and +5 percentage points to compliance-observation likelihood by default.",
"* '''Trade agreement''' columns generate actual trade sign-offs only in the Temple Bank section; War League and Hatch Ministry rows display '''-''' in that column.",
"* '''Compliance observed''' columns show minor positive local behavior below the level of a formal trade agreement.",
"* '''Rebellion alert''' columns show rare row-level contained unrest events such as forged proclamations, broker-cell activity, route barricades, false orders, ritual-cargo abuse, medical-transfer exploitation, or Dispossessed-adjacent courier behavior.",
"* '''Mirovia''', '''Saruq Thyrsa''', and '''Thal Lenaia''' are hard-excluded from Rebellion Alert results; their Rebellion Alert cells always display '''-''', even when '''rebellionchance=100''' is used for testing.",
"* '''Recurring adversarial nodes''' apply from 60/2/53 PSSC by default. Eligible rows may occasionally mention [[Khadir Veyran]] or [[Suyana Amaruq]] as suspected influence, but the notes are not guaranteed to appear.",
"* '''Khadir Veyran''' notes are weighted toward broker factions, Bromian Ledger witness screens, anti-registry dock activity, cargo ambiguity, ledger correction, and trade-signoff counterweight language.",
"* '''Suyana Amaruq''' notes are weighted toward courier screens, route barricades, false-pilgrim or guide-house movement, medical-transfer exploitation, and Black-Thread route-intelligence behavior.",
"* '''Adversarial note frequency''' defaults to 35 percent for unrest alerts, 18 percent for compliance observations, and 22 percent for Temple Bank trade sign-offs after 60/2/53 PSSC.",
"* '''Imperial Federation support''' applies from 112/53 PSSC by default. It adds +10 percentage points to compliance observations and +3 percentage points to Temple Bank trade sign-offs for Saruq Thyrsa, Mirovia, and Rumi-Kefaya.",
"* '''Bassaridian Missions support''' applies from 55/2/53 PSSC by default. It reduces unrest-alert chance by 1 percentage point in all locality rows and increases compliance-observation chance by 1 percentage point in all locality rows.",
"* '''Major Bassaridian Missions''' at Saruq Thyrsa, Mirovia, Nysara Velai, Thal Lenaia, and Selenarion receive an additional +2 percentage points to compliance observations.",
"* Use '''tradechance=100''', '''compliancechance=100''', and '''rebellionchance=100''' for testing.",
"* Use '''qhapaqselentarget=3''' to change the default Selenarion section target.",
"* Use '''qhapaqselenoverfill=yes''' to allow Selenarion to exceed its default three-row section target.",
"* Use '''qhapaqselenunrestboost=4''' and '''qhapaqselencomplianceboost=5''' to change the default Selenarion chance modifiers.",
"* Use '''adversarynodes=no''' to disable named adversarial notes.",
"* Use '''adversarychance=35''' to change the default unrest-alert adversarial note chance.",
"* Use '''complianceadversarychance=18''' to change the default compliance-observation adversarial note chance.",
"* Use '''tradeadversarychance=22''' to change the default Temple Bank trade-signoff adversarial note chance.",
"* Use '''adversarystartyear=53''' and '''adversarystartday=121''' to change the default 60/2/53 PSSC start date for named adversarial-node tracking.",
"* Use '''missionsupport=no''' to disable Bassaridian Missions modifiers for testing.",
"* Use '''imperialsupport=no''' to disable Imperial Federation support modifiers for testing.",
"* Use '''forcemylecia=no''' only if the wrapper should temporarily show the original source-module City/Node values."
}, "\n")
 
table.insert(out, hr())
table.insert(out, section("Combined legend", legendBody, collapseSections, startCollapsed))
end
 
return table.concat(out, "\n")
 
 
end
end


-- Shortcut views
-- Shortcut views
function p.southernStrait(frame)
function p.southernStrait(frame)
local args = (frame and frame.args) or {}
local args = (frame and frame.args) or {}
args.title = args.title or "Southern Strait Corridor Daily Operations"
args.title = args.title or "Southern Strait Corridor Daily Operations"
return p.makeTable({ args = args })
return p.makeTable({ args = args })
end
end


function p.warOnly(frame)
function p.warOnly(frame)
local args = (frame and frame.args) or {}
local args = (frame and frame.args) or {}
args.hatch = "no"
args.hatch = "no"
args.missionary = "no"
args.missionary = "no"
args.war = "yes"
args.war = "yes"
args.title = args.title or "War League Southern Strait Daily Operations"
args.title = args.title or "War League Southern Strait Daily Operations"
return p.makeTable({ args = args })
return p.makeTable({ args = args })
end
end


function p.hatchOnly(frame)
function p.hatchOnly(frame)
local args = (frame and frame.args) or {}
local args = (frame and frame.args) or {}
args.war = "no"
args.war = "no"
args.missionary = "no"
args.missionary = "no"
args.hatch = "yes"
args.hatch = "yes"
args.title = args.title or "Hatch Ministry Southern Strait Daily Operations"
args.title = args.title or "Hatch Ministry Southern Strait Daily Operations"
return p.makeTable({ args = args })
return p.makeTable({ args = args })
end
end


function p.missionaryOnly(frame)
function p.missionaryOnly(frame)
local args = (frame and frame.args) or {}
local args = (frame and frame.args) or {}
args.war = "no"
args.war = "no"
args.hatch = "no"
args.hatch = "no"
args.missionary = "yes"
args.missionary = "yes"
args.title = args.title or "Temple Bank Southern Strait Missionary Daily Operations"
args.title = args.title or "Temple Bank Southern Strait Missionary Daily Operations"
return p.makeTable({ args = args })
return p.makeTable({ args = args })
end
end


return p
return p

Latest revision as of 16:34, 22 July 2026

Documentation for this module may be created at Module:SouthernStraitCorridorOps/doc

-- Module:SouthernStraitCorridorOps
-- CKD reinforcement at Nysara Velai and Selenarion
-- Post-agreement Southern Strait operations:
--   +6 percentage points to Nysara Velai Compliance Observed
--   -6 percentage points to Nysara Velai Rebellion Alert
--   historical trade sign-offs preserved through 27/3/53 PSSC
--   all future trade-agreement generation disabled from 28/3/53 PSSC onward

local p = {}

-- Helpers
local function yn(value, default)
	local v = tostring(value or ""):lower()
	if v == "" then return default end
	if v == "no" or v == "n" or v == "0" or v == "false" then return false end
	if v == "yes" or v == "y" or v == "1" or v == "true" then return true end
	return default
end

local function makeFrame(args)
	return { args = args or {} }
end

local function getCurrentPSSCDate()
	local startDate = os.time({ year = 1999, month = 8, day = 6, hour = 0 })
	local secondsInDay = 86400
	local daysPerYear = 183
	local currentDate = os.time()
	local totalDaysElapsed = math.floor((currentDate - startDate) / secondsInDay)
	local psscYear = math.floor(totalDaysElapsed / daysPerYear)
	local dayOfYear = (totalDaysElapsed % daysPerYear) + 1
	if dayOfYear < 1 then dayOfYear = 1 end
	if dayOfYear > 183 then dayOfYear = 183 end
	return psscYear, dayOfYear
end

local function normalizeArgsDate(args)
	local out = {}
	for k, v in pairs(args or {}) do
		out[k] = v
	end

	local y = tonumber(out.year or out.psscyear)
	local d = tonumber(out.day or out.dayofyear)
	if not y or not d then
		y, d = getCurrentPSSCDate()
	end

	out.year = tostring(y)
	out.day = tostring(d)
	out.psscyear = tostring(y)
	out.dayofyear = tostring(d)
	return out
end

local function safeRequire(moduleName)
	local ok, mod = pcall(require, moduleName)
	if ok and mod then
		return mod, nil
	end
	return nil, "Could not load " .. tostring(moduleName)
end

local function safeCall(moduleName, funcName, args)
	local mod, err = safeRequire(moduleName)
	if not mod then
		return false, "''" .. err .. ".''"
	end
	if type(mod[funcName]) ~= "function" then
		return false, "''" .. tostring(moduleName) .. " has no public function named " .. tostring(funcName) .. ".''"
	end

	local ok, result = pcall(function()
		return mod[funcName](makeFrame(args))
	end)
	if not ok then
		return false, "''Error while invoking " .. tostring(moduleName) .. "." .. tostring(funcName) .. ": " .. tostring(result) .. "''"
	end
	return true, tostring(result or "")
end

local function doyToMonthDom(doy)
	doy = tonumber(doy) or 1
	if doy < 1 then doy = 1 end
	if doy > 183 then doy = 183 end

	if doy <= 61 then
		return 1, doy
	elseif doy <= 122 then
		return 2, doy - 61
	else
		return 3, doy - 122
	end
end

local function dateLabel(args)
	local y = tonumber(args.year or args.psscyear)
	local d = tonumber(args.day or args.dayofyear)
	if y and d then
		local m, dom = doyToMonthDom(d)
		return string.format("%d/%d/%d PSSC", dom, m, y)
	end
	return "current PSSC date"
end

local function section(title, body, collapsible, collapsed)
	local out = {}
	local safeTitle = tostring(title or "Operations")
	local content = tostring(body or "''No output.''")

	if not collapsible then
		table.insert(out, "===" .. safeTitle .. "===")
		table.insert(out, content)
		return table.concat(out, "\n")
	end

	local collapseClass = collapsed and "mw-collapsible mw-collapsed" or "mw-collapsible"
	table.insert(out, '<div class="' .. collapseClass .. '" style="border:1px solid #a2a9b1; margin:0.6em 0; background:#f8f9fa;">')
	table.insert(out, '<div style="font-weight:bold; padding:0.35em 0.6em; background:#eaecf0;">' .. safeTitle .. '</div>')
	table.insert(out, '<div style="padding:0.5em 0.6em;">')
	table.insert(out, content)
	table.insert(out, '</div>')
	table.insert(out, '</div>')
	return table.concat(out, "\n")
end

local function hr()
	return "\n----\n"
end

local function hashString(s)
	s = tostring(s or "")
	local h = 0
	for i = 1, #s do
		h = (h * 31 + string.byte(s, i)) % 1000003
	end
	return h
end

local function pick(list, seed, salt)
	if not list or #list == 0 then return nil end
	local idx = (hashString(tostring(seed or "") .. ":" .. tostring(salt or "")) % #list) + 1
	return list[idx]
end

local function pctFromSeed(seed, salt)
	return hashString(tostring(seed or "") .. ":" .. tostring(salt or "")) % 100
end

local function clampChance(value)
	value = tonumber(value) or 0
	if value < 0 then return 0 end
	if value > 100 then return 100 end
	return value
end

local function isOnOrAfterPSSCDate(year, day, startYear, startDay)
	year = tonumber(year) or 0
	day = tonumber(day) or 0
	startYear = tonumber(startYear) or 0
	startDay = tonumber(startDay) or 0
	if year > startYear then return true end
	if year == startYear and day >= startDay then return true end
	return false
end

-- Extended Southern Strait / Eastern Mykonos / Snowholme locality list
local southernTradeCities = {
	{
		code = "MIR",
		city = "[[Mirovia]]",
		region = "[[Eastern Mykonos]] Desert",
		category = "Agriculture",
		company = "[[Mirovian Cistern Growers' Syndicate]]",
		resource = "cistern-grown melons, dryland cucumbers, preserved desert fruit, seed oil, packed caravan rations, and water-accounted produce consignments",
		lineage = "Desert provisioning economy influenced by [[Jogi]]'s food-market role and [[Mylecia]]'s caravan-facing trade networks; adapted to cistern agriculture, water claims, and dry-route supply rather than beer, grain, or ritual crops"
	},
	{
		code = "SAR",
		city = "[[Saruq Thyrsa]]",
		region = "Southern Strait coast",
		category = "Agriculture",
		company = "[[Saruq Thyrsa Reedcutters' Guild]]",
		resource = "coastal reed, cane fiber, packing straw, ferry mats, cargo-padding material, rope backing, warehouse thatch, and crate-lining bundles",
		lineage = "Harbor-materials economy influenced by [[Blore Heath]]'s shipping brokerage and [[Jogi]]'s quay-market supply system; focused on practical cargo handling, ferry shelters, and packing materials rather than piracy or cult goods"
	},
	{
		code = "AYS",
		city = "[[Nysara Velai]]",
		region = "Eastern foothills of the [[Snowholme Range]]",
		category = "Agriculture",
		company = "[[Nysaran Terrace Farmers' Cooperative]]",
		resource = "terrace beans, cold-weather tubers, sheep wool, highland fodder, pack-animal feed, preserved root stores, and mule-train provisions",
		lineage = "Foothill supply economy influenced only indirectly by the Maritime Markets; tied more closely to Snowholme terrace farming, upland food storage, wool traffic, and pass-road provisioning"
	},
	{
		code = "THQ",
		city = "[[Thal Lenaia]]",
		region = "Eastern foothills of the [[Snowholme Range]]",
		category = "Agriculture",
		company = "[[Thal Lenaia Springkeepers' Cooperative]]",
		resource = "spring-fed watercress, trout ponds, smoked fish, cold-storage greens, inspected mountain food shipments, packed ice-box produce, and road-market fish consignments",
		lineage = "Foothill water-and-food economy tied to Snowholme springs and pass roads; comparable to older Maritime Markets food distribution only in its role as a regulated supplier to travelers, markets, and route offices"
	},
	{
		code = "RUK",
		city = "[[Rumi-Kefaya]]",
		region = "[[Eastern Mykonos]] Desert",
		category = "Mining / Manufacturing / Energy",
		company = "[[Rumi-Kefaya Stone and Lime Works]]",
		resource = "basalt, limestone, paving stone, railway ballast, drainage tile, lime mortar, culvert blocks, harbor repair stone, quay coping, and storm-route masonry",
		lineage = "Infrastructure economy influenced by [[Mylecia]]'s coal-and-works tradition and Southern Strait coastal preservation activity; focused on roads, quays, culverts, retaining walls, and storm-damage repair rather than extractive wealth"
	},
	{
		code = "HAY",
		city = "[[Hayr Chaskal]]",
		region = "Southern Strait coast",
		category = "Mining / Manufacturing / Energy",
		company = "[[Hayr Chaskal Glass and Signal Works]]",
		resource = "beacon glass, telegraph insulators, lighthouse lenses, lamp glass, storm panes, signal mirrors, buoy lenses, and weatherproof signal housings",
		lineage = "Signal-and-navigation economy influenced by [[Blore Heath]]'s shipping control and [[Mylecia]]'s coastal works; supports ferry warnings, lighthouse maintenance, telegraph lines, and Hatch Ministry watch posts without copying older pirate or brokerage firms"
	},
	{
		code = "QSE",
		city = "[[Selenarion]]",
		region = "Eastern foothills of the [[Snowholme Range]]",
		category = "Services",
		company = "[[Selenarion Route Registry Office]]",
		resource = "night-road registry, caravan lodging, pass permits, route insurance, guide licensing, cargo-route papers, missing-traveler filings, and bonded transit records",
		lineage = "Foothill route-service economy influenced by [[Mylecia]]'s caravan-facing commerce and [[Jogi]]'s intelligence-and-paperwork traditions; adapted to Snowholme pass roads, guide houses, lodging records, and insurance claims"
	}
}

-- Locality presence controls
-- In the CKD / War League section, AYS receives six rows and QSE receives two.
-- Hatch Ministry and Temple Bank retain three AYS rows and one QSE row.
local cappedPresenceLocalities = {
	MIR = true,
	SAR = true,
	THQ = true
}

local function findLocalityByCode(code)
	code = tostring(code or "")
	for _, loc in ipairs(southernTradeCities or {}) do
		if tostring(loc.code or "") == code then
			return loc
		end
	end
	return nil
end

local function localityCount(state, locOrCode)
	if not state or not state.counts then return 0 end
	local code
	if type(locOrCode) == "table" then
		code = tostring(locOrCode.code or "")
	else
		code = tostring(locOrCode or "")
	end
	return tonumber(state.counts[code]) or 0
end

local function incrementLocalityCount(state, loc)
	if not state or not loc then return end
	local code = tostring(loc.code or "")
	if code == "" then return end
	state.counts[code] = (tonumber(state.counts[code]) or 0) + 1
end

local function nysaraVelaiPresenceTarget(args, channel)
	local isWar = tostring(channel or "") == "war"
	local v
	if isWar then
		v = tonumber(
			args.warnysaravelaitarget or
			args.warNysaraVelaiTarget or
			args.warnysaratarget or
			args.warNysaraTarget or
			args.waraystarget or
			args.warAysTarget
		)
	end
	if v == nil then
		v = tonumber(
			args.nysaravelaitarget or
			args.nysaraVelaiTarget or
			args.nysaratarget or
			args.nysaraTarget or
			args.aystarget or
			args.aysTarget
		)
	end
	if v == nil then v = isWar and 6 or 3 end
	if v < 0 then v = 0 end
	return math.floor(v)
end

local function selenarionPresenceTarget(args, channel)
	local isWar = tostring(channel or "") == "war"
	local v
	if isWar then
		v = tonumber(
			args.warselenariontarget or
			args.warSelenarionTarget or
			args.warqsetarget or
			args.warQseTarget
		)
	end
	if v == nil then
		v = tonumber(
			args.selenariontarget or
			args.selenarionTarget or
			args.qsetarget or
			args.qseTarget
		)
	end
	if v == nil then v = isWar and 2 or 1 end
	if v < 0 then v = 0 end
	return math.floor(v)
end

local function nysaraVelaiAllowOverTarget(args)
	return yn(
		args.nysaravelaioverfill or
		args.nysaraVelaiOverfill or
		args.nysaraoverfill or
		args.nysaraOverfill or
		args.aysoverfill or
		args.aysOverfill,
		false
	)
end

local function buildPriorityLocalityPlan(args, channel)
	local plan = {}
	local nysaraTarget = nysaraVelaiPresenceTarget(args, channel)
	local selenarionTarget = selenarionPresenceTarget(args, channel)
	for _ = 1, nysaraTarget do table.insert(plan, "AYS") end
	for _ = 1, selenarionTarget do table.insert(plan, "QSE") end
	return plan
end

local function newLocalityAssignmentState(args, channel)
	return {
		channel = tostring(channel or ""),
		counts = {},
		aysTarget = nysaraVelaiPresenceTarget(args, channel),
		aysAllowOverTarget = nysaraVelaiAllowOverTarget(args),
		qseTarget = selenarionPresenceTarget(args, channel),
		priorityPlan = buildPriorityLocalityPlan(args, channel)
	}
end

local function canAssignLocality(args, loc, state)
	if not loc then return false end
	local code = tostring(loc.code or "")
	if cappedPresenceLocalities[code] and localityCount(state, code) >= 1 then
		return false
	end
	if code == "AYS" and state and not state.aysAllowOverTarget then
		if localityCount(state, code) >= (tonumber(state.aysTarget) or 3) then
			return false
		end
	end
	if code == "QSE" and state then
		if localityCount(state, code) >= (tonumber(state.qseTarget) or 1) then
			return false
		end
	end
	return true
end

local function priorityLocalityForRow(state, rowNumber)
	if not state or not state.priorityPlan then return nil end
	local row = tonumber(rowNumber) or 0
	local code = state.priorityPlan[row]
	if not code then return nil end
	return findLocalityByCode(code)
end

local function rowLocalityData(args, channel, rowNumber, state)
	if not southernTradeCities or #southernTradeCities == 0 then return nil end
	state = state or newLocalityAssignmentState(args, channel)

	local priorityLoc = priorityLocalityForRow(state, rowNumber)
	if priorityLoc and canAssignLocality(args, priorityLoc, state) then
		incrementLocalityCount(state, priorityLoc)
		return priorityLoc
	end

	local year = tonumber(args.year or args.psscyear) or 0
	local day = tonumber(args.day or args.dayofyear) or 0
	local seed = tostring(args.seed or "") ..
		":SS-ROW-LOCALITY:" .. tostring(channel or "") ..
		":" .. tostring(year) ..
		":" .. tostring(day)

	local offset = hashString(seed) % #southernTradeCities
	local baseRow = tonumber(rowNumber) or 1
	for attempt = 0, #southernTradeCities - 1 do
		local idx = ((baseRow - 1 + offset + attempt) % #southernTradeCities) + 1
		local loc = southernTradeCities[idx]
		if canAssignLocality(args, loc, state) then
			incrementLocalityCount(state, loc)
			return loc
		end
	end

	for _, loc in ipairs(southernTradeCities) do
		local code = tostring(loc.code or "")
		if not cappedPresenceLocalities[code] and code ~= "AYS" and code ~= "QSE" then
			incrementLocalityCount(state, loc)
			return loc
		end
	end
	return nil
end

local function rowLocalityText(loc)
	if not loc then return "Southern Strait corridor" end
	return loc.city .. "<br><small>" .. loc.region .. "</small>"
end

-- Imperial Federation support marker and locality boosts
local IMPERIAL_SUPPORT_START_YEAR = 53
local IMPERIAL_SUPPORT_START_DAY = 112
local imperialFederationSupportLocalities = {
	MIR = true,
	SAR = true,
	RUK = true
}

local function imperialSupportEnabled(args)
	return yn(args.imperialsupport or args.imperialSupport or args.ifsupport or args.ifSupport, true)
end

local function imperialSupportEffective(args)
	if not imperialSupportEnabled(args) then return false end
	local year = tonumber(args.year or args.psscyear) or 0
	local day = tonumber(args.day or args.dayofyear) or 0
	local startYear = tonumber(args.imperialstartyear or args.imperialStartYear) or IMPERIAL_SUPPORT_START_YEAR
	local startDay = tonumber(args.imperialstartday or args.imperialStartDay) or IMPERIAL_SUPPORT_START_DAY
	return isOnOrAfterPSSCDate(year, day, startYear, startDay)
end

local function hasImperialFederationSupport(args, loc)
	if not imperialSupportEffective(args) then return false end
	return loc and imperialFederationSupportLocalities[tostring(loc.code or "")] == true
end

local function imperialSupportBoostedChance(args, loc, baseChance, boost)
	local chance = clampChance(baseChance)
	if hasImperialFederationSupport(args, loc) then
		chance = chance + (tonumber(boost) or 0)
	end
	return clampChance(chance)
end

local function imperialFederationSupportNotice(args)
	if not imperialSupportEffective(args) then return "" end
	return "'''Imperial Federation support present:''' [[2nd Brigade, 25th Division of the Imperial Marines]] deployed in allied support of Bassaridian Southern Strait corridor activity around [[Saruq Thyrsa]], [[Mirovia]], and [[Rumi-Kefaya]].<br>" ..
		"<small>Mechanized, mixed-capability support of approximately 4,000 soldiers reinforces route security, inspection cordons, convoy spacing, engineering liaison, and lawful corridor stabilization. " ..
		"Locality rows for Saruq Thyrsa, Mirovia, and Rumi-Kefaya receive +10 percentage points to compliance observations. The former +3-point Temple Bank trade-signoff modifier remains relevant only when reproducing historical dates through 27/3/53 PSSC; all later trade-agreement generation is closed.</small>"
end

-- Bassaridian Missions support marker and locality adjustments
local BASSARIDIAN_MISSIONS_START_YEAR = 53
local BASSARIDIAN_MISSIONS_START_DAY = 116
local majorBassaridianMissionLocalities = {
	SAR = true,
	MIR = true,
	AYS = true,
	THQ = true,
	QSE = true
}

local function bassaridianMissionsEnabled(args)
	return yn(args.missionsupport or args.missionSupport or args.bassaridianmissions or args.bassaridianMissions, true)
end

local function bassaridianMissionsEffective(args)
	if not bassaridianMissionsEnabled(args) then return false end
	local year = tonumber(args.year or args.psscyear) or 0
	local day = tonumber(args.day or args.dayofyear) or 0
	local startYear = tonumber(args.missionstartyear or args.missionStartYear) or BASSARIDIAN_MISSIONS_START_YEAR
	local startDay = tonumber(args.missionstartday or args.missionStartDay) or BASSARIDIAN_MISSIONS_START_DAY
	return isOnOrAfterPSSCDate(year, day, startYear, startDay)
end

local function hasBassaridianMission(args, loc)
	if not bassaridianMissionsEffective(args) then return false end
	return loc ~= nil
end

local function hasMajorBassaridianMission(args, loc)
	if not bassaridianMissionsEffective(args) then return false end
	return loc and majorBassaridianMissionLocalities[tostring(loc.code or "")] == true
end

-- Nysara Velai-specific probability modifiers
local function nysaraVelaiComplianceBoost(args)
	local v = tonumber(
		args.nysaravelaicomplianceboost or
		args.nysaraVelaiComplianceBoost or
		args.nysaracomplianceboost or
		args.nysaraComplianceBoost or
		args.ayscomplianceboost or
		args.aysComplianceBoost
	)
	if v == nil then v = 6 end
	return v
end

local function nysaraVelaiUnrestAdjustment(args)
	local reduction = tonumber(
		args.nysaravelaiunrestreduction or
		args.nysaraVelaiUnrestReduction or
		args.nysaraunrestreduction or
		args.nysaraUnrestReduction or
		args.aysunrestreduction or
		args.aysUnrestReduction
	)
	if reduction ~= nil then
		return -math.abs(reduction)
	end

	-- Legacy additive override retained for compatibility.
	local legacyBoost = tonumber(
		args.nysaravelaiunrestboost or
		args.nysaraVelaiUnrestBoost or
		args.nysaraunrestboost or
		args.nysaraUnrestBoost or
		args.aysunrestboost or
		args.aysUnrestBoost
	)
	if legacyBoost ~= nil then
		return legacyBoost
	end
	return -6
end

local function nysaraVelaiTradeBoost(args)
	local v = tonumber(
		args.nysaravelaitradeboost or
		args.nysaraVelaiTradeBoost or
		args.nysaratradeboost or
		args.nysaraTradeBoost or
		args.aystradeboost or
		args.aysTradeBoost
	)
	if v == nil then v = 3 end
	return v
end

local function nysaraVelaiAdjustedComplianceChance(args, loc, baseChance)
	local chance = clampChance(baseChance)
	if loc and tostring(loc.code or "") == "AYS" then
		chance = chance + nysaraVelaiComplianceBoost(args)
	end
	return clampChance(chance)
end

local function nysaraVelaiAdjustedUnrestChance(args, loc, baseChance)
	local chance = clampChance(baseChance)
	if loc and tostring(loc.code or "") == "AYS" then
		chance = chance + nysaraVelaiUnrestAdjustment(args)
	end
	return clampChance(chance)
end

local function nysaraVelaiAdjustedTradeChance(args, loc, baseChance)
	local chance = clampChance(baseChance)
	if loc and tostring(loc.code or "") == "AYS" then
		chance = chance + nysaraVelaiTradeBoost(args)
	end
	return clampChance(chance)
end

local function missionAdjustedComplianceChance(args, loc, baseChance)
	local chance = clampChance(baseChance)
	if hasBassaridianMission(args, loc) then
		chance = chance + (tonumber(args.missioncomplianceboost or args.missionComplianceBoost) or 1)
	end
	if hasMajorBassaridianMission(args, loc) then
		chance = chance + (tonumber(args.majormissioncomplianceboost or args.majorMissionComplianceBoost) or 2)
	end
	return clampChance(chance)
end

local function missionAdjustedUnrestChance(args, loc, baseChance)
	local chance = clampChance(baseChance)
	if hasBassaridianMission(args, loc) then
		chance = chance - (tonumber(args.missionunrestreduction or args.missionUnrestReduction) or 1)
	end
	return clampChance(chance)
end

local function bassaridianMissionsNotice(args)
	if not bassaridianMissionsEffective(args) then return "" end
	return "'''Bassaridian Missions acknowledged:''' [[Temple Bank of the Reformed Stripping Path|Temple Bank]]-run fortified mission compounds operate across the Southern Strait locality system, with major mission houses at [[Saruq Thyrsa]], [[Mirovia]], [[Nysara Velai]], [[Thal Lenaia]], and [[Selenarion]].<br>" ..
		"<small>From 55/2/53 PSSC onward, all locality rows receive -1 percentage point to unrest alerts and +1 percentage point to compliance observations. " ..
		"Major mission localities receive an additional +2 percentage points to compliance observations. " ..
		"Nysara Velai separately receives +6 percentage points to compliance-observation likelihood and -6 percentage points to unrest-alert likelihood, reflecting the stabilizing effect of the enlarged CKD presence, strengthened missionary institutions, protected terrace routes, and improving administrative cooperation across the eastern Snowholme approaches. Its former +3-point trade-signoff modifier is retained only for historical reproduction through 27/3/53 PSSC; no new agreements may be generated afterward. " ..
		"Mission guards are drawn from local Baratar recruits and Council of Kings Division rotations, while deployed units continue to route through [[Mylecia]] before staging locally.</small>"
end

-- Recurring adversarial-node tracking
local ADVERSARY_NODES_START_YEAR = 53
local ADVERSARY_NODES_START_DAY = 121

local recurringAdversaries = {
	KHADIR = {
		name = "[[Khadir Veyran]]",
		epithet = "Keeper of the Bromian Ledger",
		localities = { QSE = true, AYS = true, RUK = true, HAY = true },
		notes = {
			"Bromian Ledger intermediaries are suspected of delaying countersignatures without openly refusing Bassaridian review.",
			"Veyran-linked witnesses preserve older Maritime Markets custom as a rival source of commercial legitimacy.",
			"Broker-house clerks appear to be keeping disputed cargo identity ambiguous until General Port auditors withdraw.",
			"Warehouse notaries tied to the Bromian Ledger are suspected of turning routine paperwork into a jurisdictional dispute."
		},
		complianceNotes = {
			"Temple Bank auditors noted that this cooperation weakens the Bromian Ledger's claim that older broker custom remains the only trusted record.",
			"Local registry compliance was treated as useful evidence against Veyran-linked delay tactics.",
			"The observation was preserved as a small but meaningful challenge to Bromian Ledger influence in the pass-road economy."
		},
		tradeNotes = {
			"Temple Bank clerks flagged the sign-off as a possible counterweight to Khadir Veyran's older broker-credit network.",
			"The filing gives General Port auditors a lawful route around Bromian Ledger intermediaries.",
			"The agreement was preserved as evidence that local commercial authority could be recorded without Veyran's countersignature."
		}
	},
	SUYANA = {
		name = "[[Suyana Amaruq]]",
		epithet = "Black-Thread Suyana",
		localities = { QSE = true, AYS = true, THQ = true, HAY = true },
		notes = {
			"Black-Thread couriers are suspected of using ordinary household movement to mask route intelligence.",
			"Suyana-linked runners may have tested night-road inspection gaps before disappearing into ordinary market traffic.",
			"False pilgrims and guide-house messengers appear to have carried warning signals ahead of the audit lane.",
			"Temple Bank officers treated the incident as route-intelligence obstruction rather than ordinary smuggling."
		},
		complianceNotes = {
			"The observation was treated as a useful interruption of Black-Thread courier habits.",
			"Licensed guides helped separate ordinary passage from Suyana-linked courier movement.",
			"Temple Bank staff preserved the record as evidence that local mobility could be protected without unregistered courier screens."
		},
		tradeNotes = {
			"Temple Bank notes warned that Black-Thread couriers may attempt to test the new trade window through false-route traffic.",
			"The sign-off was paired with route-watch instructions because Suyana-linked couriers remained active near the upland approaches.",
			"Mission clerks treated the filing as useful, but not sufficient to end Black-Thread influence over informal passage."
		}
	}
}

local function adversaryNodesEnabled(args)
	return yn(
		args.adversarynodes or
		args.adversaryNodes or
		args.namedadversaries or
		args.namedAdversaries or
		args.adversarynotes or
		args.adversaryNotes,
		true
	)
end

local function adversaryNodesEffective(args)
	if not adversaryNodesEnabled(args) then return false end
	local year = tonumber(args.year or args.psscyear) or 0
	local day = tonumber(args.day or args.dayofyear) or 0
	local startYear = tonumber(args.adversarystartyear or args.adversaryStartYear) or ADVERSARY_NODES_START_YEAR
	local startDay = tonumber(args.adversarystartday or args.adversaryStartDay) or ADVERSARY_NODES_START_DAY
	return isOnOrAfterPSSCDate(year, day, startYear, startDay)
end

local function adversaryNoteChance(args, kind)
	kind = tostring(kind or "")
	if kind == "trade" then
		return clampChance(tonumber(args.tradeadversarychance or args.tradeAdversaryChance) or 22)
	elseif kind == "compliance" then
		return clampChance(tonumber(args.complianceadversarychance or args.complianceAdversaryChance) or 18)
	end
	return clampChance(tonumber(
		args.adversarychance or
		args.adversaryChance or
		args.unrestadversarychance or
		args.unrestAdversaryChance
	) or 35)
end

local function localityAllowsAdversary(loc, adv)
	if not adv then return false end
	if not loc then return true end
	local localities = adv.localities
	if not localities then return true end
	return localities[tostring(loc.code or "")] == true
end

local function candidateAdversaryCodes(profile, loc)
	local out = {}
	if profile and type(profile.adversaries) == "table" then
		for _, code in ipairs(profile.adversaries) do
			local adv = recurringAdversaries[code]
			if adv and localityAllowsAdversary(loc, adv) then
				table.insert(out, code)
			end
		end
	end
	if #out == 0 then
		for code, adv in pairs(recurringAdversaries) do
			if localityAllowsAdversary(loc, adv) then
				table.insert(out, code)
			end
		end
	end
	return out
end

local function adversaryNotePart(args, kind, seed, profile, loc)
	if not adversaryNodesEffective(args) then return "" end
	local chance = adversaryNoteChance(args, kind)
	local roll = pctFromSeed(seed, "adv-note-roll-" .. tostring(kind or "general")) + 1
	if roll > chance then return "" end

	local candidates = candidateAdversaryCodes(profile, loc)
	local code = pick(candidates, seed, "adv-note-code-" .. tostring(kind or "general"))
	local adv = recurringAdversaries[code]
	if not adv then return "" end

	local notes = adv.notes
	if tostring(kind or "") == "trade" and adv.tradeNotes then
		notes = adv.tradeNotes
	elseif tostring(kind or "") == "compliance" and adv.complianceNotes then
		notes = adv.complianceNotes
	end

	local note = pick(notes, seed, "adv-note-text-" .. tostring(kind or "general"))
	if not note then return "" end
	return "<br>'''Adversarial note:''' " .. adv.name .. " (''" .. adv.epithet .. "'') — " .. note
end

local function adversarialNodesNotice(args)
	if not adversaryNodesEffective(args) then return "" end
	return "'''Temple Bank adversarial-node tracking active:''' from 60/2/53 PSSC onward, occasional row-level notes may identify suspected influence associated with [[Khadir Veyran]] (''Keeper of the Bromian Ledger'') or [[Suyana Amaruq]] (''Black-Thread Suyana'').<br>" ..
		"<small>These notes are seeded and probabilistic. They do not appear in every unrest or compliance row; trade-signoff notes remain available only when reproducing historical dates through 27/3/53 PSSC. " ..
		"Use '''adversarynodes=no''' to disable them, '''adversarychance=35''' to adjust unrest-note frequency, '''complianceadversarychance=18''' to adjust compliance-note frequency, and '''tradeadversarychance=22''' only for historical trade-signoff rows.</small>"
end

-- Forced City/Node display
local forcedCityNodeText = "[[File:MyleciaFlabB.jpeg|20px]] [[Mylecia]]"

local function cleanHeaderLabel(line)
	local s = tostring(line or "")
	s = s:gsub("^%s*!+%s*", "")
	if s:find("|", 1, true) then
		local parts = mw.text.split(s, "|", true)
		s = parts[#parts] or s
	end
	s = s:gsub("<.->", "")
	s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
	s = s:gsub("%[%[([^%]]+)%]%]", "%1")
	s = s:gsub("&nbsp;", " ")
	s = s:gsub("%s*/%s*", "/")
	s = s:gsub("^%s+", ""):gsub("%s+$", "")
	return s:lower()
end

local cityNodeHeaderLabels = {
	["city"] = true,
	["node"] = true,
	["city/node"] = true,
	["city / node"] = true,
	["city or node"] = true,
	["home port"] = true,
	["homeport"] = true,
	["home city"] = true,
	["base city"] = true,
	["staging city"] = true
}

local function forceMyleciaCityNodeColumn(body, args)
	body = tostring(body or "")
	if yn(args.forcemylecia or args.forceMylecia or args.myleciacity or args.myleciaCity, true) == false then
		return body
	end
	if not body:find("{|", 1, true) then return body end

	local forcedText = args.forcecitytext or args.forceCityText or forcedCityNodeText
	local lines = mw.text.split(body, "\n", true)
	local out = {}
	local inTable = false
	local cityCol = nil
	local col = 0

	for _, line in ipairs(lines) do
		if line:match("^%s*{|") then
			inTable = true
			cityCol = nil
			col = 0
			table.insert(out, line)
		elseif inTable and line:match("^%s*!") then
			local raw = line:gsub("^%s*!+%s*", "")
			local headers = mw.text.split(raw, "!!", true)
			if #headers > 1 then
				for _, h in ipairs(headers) do
					col = col + 1
					if cityNodeHeaderLabels[cleanHeaderLabel("! " .. h)] then
						cityCol = col
					end
				end
			else
				col = col + 1
				if cityNodeHeaderLabels[cleanHeaderLabel(line)] then
					cityCol = col
				end
			end
			table.insert(out, line)
		elseif inTable and line:match("^%s*|%-") then
			col = 0
			table.insert(out, line)
		elseif inTable and cityCol and line:match("^%s*|[^}%-%+]") then
			local raw = line:gsub("^%s*|%s*", "")
			local cells = mw.text.split(raw, "||", true)
			if #cells > 1 then
				for i = 1, #cells do
					if i == cityCol then cells[i] = " " .. forcedText .. " " end
				end
				table.insert(out, "| " .. table.concat(cells, " || "))
			else
				col = col + 1
				if col == cityCol then
					table.insert(out, "| " .. forcedText)
				else
					table.insert(out, line)
				end
			end
		elseif inTable and line:match("^%s*|}") then
			inTable = false
			table.insert(out, line)
		else
			table.insert(out, line)
		end
	end
	return table.concat(out, "\n")
end

-- Hatch Ministry Home city / Current station swap
local function swapHatchHomeCityCurrentStation(body, args)
	body = tostring(body or "")
	if yn(args.swaphatchstation or args.swapHatchStation or args.swaphatchhomecity or args.swapHatchHomeCity, true) == false then
		return body
	end
	if not body:find("{|", 1, true) then return body end

	local lines = mw.text.split(body, "\n", true)
	local out = {}
	local inTable = false
	local homeCol = nil
	local stationCol = nil
	local col = 0

	for _, line in ipairs(lines) do
		if line:match("^%s*{|") then
			inTable = true
			homeCol = nil
			stationCol = nil
			col = 0
			table.insert(out, line)
		elseif inTable and line:match("^%s*!") then
			local raw = line:gsub("^%s*!+%s*", "")
			local headers = mw.text.split(raw, "!!", true)
			if #headers > 1 then
				for _, h in ipairs(headers) do
					col = col + 1
					local label = cleanHeaderLabel("! " .. h)
					if label == "home city" or label == "home" then
						homeCol = col
					elseif label == "current station" or label == "station" then
						stationCol = col
					end
				end
			else
				col = col + 1
				local label = cleanHeaderLabel(line)
				if label == "home city" or label == "home" then
					homeCol = col
				elseif label == "current station" or label == "station" then
					stationCol = col
				end
			end
			table.insert(out, line)
		elseif inTable and line:match("^%s*|%-") then
			col = 0
			table.insert(out, line)
		elseif inTable and homeCol and stationCol and line:match("^%s*|[^}%-%+]") then
			local raw = line:gsub("^%s*|%s*", "")
			local cells = mw.text.split(raw, "||", true)
			if #cells > 1 then
				if cells[homeCol] and cells[stationCol] then
					cells[homeCol], cells[stationCol] = cells[stationCol], cells[homeCol]
				end
				table.insert(out, "| " .. table.concat(cells, " || "))
			else
				col = col + 1
				table.insert(out, line)
			end
		elseif inTable and line:match("^%s*|}") then
			inTable = false
			table.insert(out, line)
		else
			table.insert(out, line)
		end
	end
	return table.concat(out, "\n")
end

-- Trade-agreement generation cutoff
-- The Nysara Velai sign-off on 27/3/53 PSSC concluded the locality-alignment phase.
-- Historical output through that date remains reproducible, but all dates from
-- 28/3/53 PSSC onward hard-return '-' in the Trade agreement column.
local TRADE_SIGNOFF_FINAL_YEAR = 53
local TRADE_SIGNOFF_FINAL_DAY = 149 -- 27/3/53 PSSC

local function tradeAgreementGenerationClosed(args)
	local year = tonumber(args.year or args.psscyear) or 0
	local day = tonumber(args.day or args.dayofyear) or 0

	if year > TRADE_SIGNOFF_FINAL_YEAR then
		return true
	end
	if year == TRADE_SIGNOFF_FINAL_YEAR and day > TRADE_SIGNOFF_FINAL_DAY then
		return true
	end
	return false
end

-- Cult sign-off list for Temple Bank trade agreements
local tradeSigningCults = {
	{ code = "UMB", label = "[[Noctis (Reformed Stripping Path)#Order of the Umbral Oracle|Order of the Umbral Oracle]]" },
	{ code = "GGS", label = "[[Chrysos (Reformed Stripping Path)#Guild of Golden Shadows|Guild of Golden Shadows]]" },
	{ code = "AZS", label = "[[Thalassa (Reformed Stripping Path)#Azure Sentinel Sect|Azure Sentinel Sect]]" },
	{ code = "HAR", label = "[[Micras (Reformed Stripping Path)#Harmony Sanctum|Harmony Sanctum]]" },
	{ code = "VIT", label = "[[Plateau (Reformed Stripping Path)#Sanctum Vitalis|Sanctum Vitalis]]" },
	{ code = "AUR", label = "[[Eos (Reformed Stripping Path)#Order of Aurora Mystica|Order of Aurora Mystica]]" },
	{ code = "DEL", label = "[[Sisera (Reformed Stripping Path)#Sanctum Delphica|Sanctum Delphica]]" },
	{ code = "CAT", label = "[[Cato (Reformed Stripping Path)#Rex Catonis|Rex Catonis]]" },
	{ code = "CON", label = "[[Lukedu (Reformed Stripping Path)#Accord Concordia|Accord Concordia]]" },
	{ code = "STY", label = "[[Styx (Reformed Stripping Path)#Mystery of the Stygian Veil|Mystery of the Stygian Veil]]" }
}

local tradeOutcomes = {
	"Cleared for General Port ledger review.",
	"Provisional admission recommended.",
	"Conditional trade window approved.",
	"Company charter accepted for corridor listing.",
	"Trade agreement filed through ordinary registry channels."
}

local function tradeAgreementCell(args, channel, rowNumber, loc)
	if tostring(channel or "") ~= "missionary" then return "-" end
	if not loc then return "-" end
	if tradeAgreementGenerationClosed(args) then return "-" end

	local chance = imperialSupportBoostedChance(
		args,
		loc,
		tonumber(args.tradechance or args.tradeChance) or 3,
		3
	)
	chance = nysaraVelaiAdjustedTradeChance(args, loc, chance)

	local year = tonumber(args.year or args.psscyear) or 0
	local day = tonumber(args.day or args.dayofyear) or 0
	local seed = tostring(args.seed or "") ..
		":SS-TEMPLE-TRADE-COL:" .. tostring(rowNumber or 0) ..
		":" .. tostring(loc.code or "GEN") ..
		":" .. tostring(year) ..
		":" .. tostring(day)

	local roll = pctFromSeed(seed, "roll") + 1
	if roll > chance then
		if yn(args.showTradeNoEvent or args.showtradenoevent, false) then return "No sign-off" end
		return "-"
	end

	local cult = pick(tradeSigningCults, seed, "cult")
	local outcome = pick(tradeOutcomes, seed, "outcome")
	if not cult then return "-" end

	local seq = (pctFromSeed(seed, "seq") % 20) + 1
	local tradeId
	if year > 0 and day > 0 then
		tradeId = string.format("GP-SS-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
	else
		tradeId = string.format("GP-SS-%s-%02d", loc.code or "GEN", seq)
	end
	local signCode = string.format("%s-TA%02d", cult.code or "CUL", seq)
	local advNote = adversaryNotePart(args, "trade", seed, nil, loc)
	return "'''Trade sign-off'''<br>" ..
		"<small>" .. tradeId .. "</small><br>" ..
		(cult.label or "Cult sign-off recorded") .. "<br>" ..
		"<small>Code: '''" .. signCode .. "'''<br>" .. outcome .. advNote .. "</small>"
end

-- Rebellion / corridor unrest row column
local rebellionProfiles = {
	{ label = "Confederacy-adjacent courier screen", description = "False-route couriers testing inspection gaps.", adversaries = { "SUYANA" } },
	{ label = "Former Maritime Markets broker faction", description = "Old broker-house retainers resisting General Port paperwork.", adversaries = { "KHADIR" } },
	{ label = "Anti-registry dock syndicate", description = "Warehouse intermediaries opposing voucher and ledger discipline.", adversaries = { "KHADIR" } },
	{ label = "False-proclamation circle", description = "Pamphlet readers circulating forged civic orders.", adversaries = { "KHADIR", "SUYANA" } },
	{ label = "Route-barricade committee", description = "Agitators obstructing lawful inspection lanes.", adversaries = { "SUYANA" } },
	{ label = "Dispossessed-sympathetic quay cell", description = "Waterfront cell copying Confederacy-style courier habits.", adversaries = { "SUYANA" } },
	{ label = "Illicit ritual-cargo and medical-transfer exploiters", description = "Handlers using illness, mourning, ritual cargo, or shrine protection to delay inspection.", adversaries = { "SUYANA" } },
	{ label = "Bromian Ledger witness screen", description = "Broker-linked witnesses preserving conflicting older records against General Port review.", adversaries = { "KHADIR" } }
}

local rebellionTriggers = {
	"Forged levy notice.",
	"Masked speakers redirected traffic.",
	"Missing seals and altered registry slips.",
	"Crowd pressure over bonded cargo.",
	"Tampered signal schedule.",
	"Conflicting route orders.",
	"Coordinated paper strike.",
	"Anti-Bassaridian chant movement.",
	"Repeated courier handoffs.",
	"Matched false manifests.",
	"False medical-transfer exemption.",
	"Unreconciled caravan lodging roll.",
	"Guide-house witness contradiction.",
	"Older Maritime Markets clearance invoked."
}

local rebellionActions = {
	"CKD cordon and evidence hold.",
	"Temple rumor-control desk opened.",
	"Ledgers and copied seals sealed.",
	"Privateer stand-off posture.",
	"Pass papers reissued.",
	"Lawful cargo separated.",
	"Missionary mediation opened.",
	"Corrected signal notice issued.",
	"Temporary General Port audit lane opened.",
	"Incident classified as contained unrest.",
	"Guide-license review opened.",
	"Medical-transfer custody ledger corrected.",
	"Broker-house witnesses summoned for countersignature.",
	"Night-road registry temporarily frozen."
}

local rebellionRisks = { "Low", "Moderate", "High", "Contained", "Escalating" }
local rebellionAlertExcludedLocalities = { MIR = true, SAR = true, THQ = true }

local function isRebellionAlertExcluded(loc)
	if not loc then return false end
	return rebellionAlertExcludedLocalities[tostring(loc.code or "")] == true
end

local function rebellionAlertCell(args, channel, rowNumber, loc)
	if not loc then return "-" end
	if isRebellionAlertExcluded(loc) then return "-" end

	local baseChance = tonumber(args.rebellionchance or args.rebellionChance or args.unrestchance or args.unrestChance) or 6
	local chance = missionAdjustedUnrestChance(args, loc, baseChance)
	chance = nysaraVelaiAdjustedUnrestChance(args, loc, chance)

	local year = tonumber(args.year or args.psscyear) or 0
	local day = tonumber(args.day or args.dayofyear) or 0
	local seed = tostring(args.seed or "") ..
		":SS-REBELLION-COL:" .. tostring(channel or "") ..
		":" .. tostring(rowNumber or 0) ..
		":" .. tostring(loc.code or "GEN") ..
		":" .. tostring(year) ..
		":" .. tostring(day)

	local roll = pctFromSeed(seed, "roll") + 1
	if roll > chance then
		if yn(args.showRebellionNoEvent or args.showrebellionnoevent or args.showUnrestNoEvent or args.showunrestnoevent, false) then
			return "No unrest"
		end
		return "-"
	end

	local profile = pick(rebellionProfiles, seed, "profile")
	local trigger = pick(rebellionTriggers, seed, "trigger")
	local action = pick(rebellionActions, seed, "action")
	local risk = pick(rebellionRisks, seed, "risk")
	if not profile then return "-" end

	local seq = (pctFromSeed(seed, "seq") % 20) + 1
	local eventId
	if year > 0 and day > 0 then
		eventId = string.format("SS-RB-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
	else
		eventId = string.format("SS-RB-%s-%02d", loc.code or "GEN", seq)
	end
	local advNote = adversaryNotePart(args, "unrest", seed, profile, loc)
	return "'''Unrest alert'''<br>" ..
		"<small>" .. eventId .. "</small><br>" ..
		"'''Profile:''' " .. profile.label .. "<br>" ..
		"<small>" .. profile.description .. "<br>" ..
		"'''Trigger:''' " .. trigger .. "<br>" ..
		"'''Risk:''' " .. risk .. "<br>" ..
		"'''Response:''' " .. action .. advNote .. "</small>"
end

-- Compliance observed / minor positive row column
local complianceProfiles = {
	{ label = "Inspection cooperation", description = "Local carriers complied with inspection sequencing without requiring escalation.", adversaries = { "SUYANA" } },
	{ label = "Ledger correction", description = "Route clerks corrected incomplete or inconsistent paperwork before formal penalty.", adversaries = { "KHADIR" } },
	{ label = "Crowd dispersal", description = "Local stewards dispersed a gathering before it became an obstruction.", adversaries = { "SUYANA" } },
	{ label = "Guide assistance", description = "Licensed guides helped separate lawful traffic from suspect courier movement.", adversaries = { "SUYANA" } },
	{ label = "Cargo separation", description = "Bonded cargo was voluntarily separated for review without disrupting ordinary passage.", adversaries = { "KHADIR" } },
	{ label = "Rumor-control compliance", description = "Temple notices were accepted locally and hostile rumors lost momentum.", adversaries = { "KHADIR", "SUYANA" } },
	{ label = "Privateer signal compliance", description = "Vessels accepted stand-off or hold-and-release signals without resistance.", adversaries = { "SUYANA" } },
	{ label = "Registry cooperation", description = "Local registrars preserved names, seals, and witness records for later review.", adversaries = { "KHADIR" } },
	{ label = "Medical-transfer ledger cooperation", description = "Clinic escorts preserved patient custody records without accepting false sacred exemptions.", adversaries = { "SUYANA" } }
}

local complianceOutcomes = {
	"Minor positive compliance noted.",
	"Recorded as a stabilizing local action.",
	"Accepted as offsetting evidence against wider noncompliance.",
	"Filed as a documented good-faith response.",
	"Preserved for future compliance comparison.",
	"Marked as below trade-agreement threshold but operationally helpful."
}

local function complianceObservedCell(args, channel, rowNumber, loc)
	if not loc then return "-" end
	local baseChance = tonumber(args.compliancechance or args.complianceChance or args.observedchance or args.observedChance) or 8
	local chance = missionAdjustedComplianceChance(args, loc, baseChance)
	chance = imperialSupportBoostedChance(args, loc, chance, 10)
	chance = nysaraVelaiAdjustedComplianceChance(args, loc, chance)

	local year = tonumber(args.year or args.psscyear) or 0
	local day = tonumber(args.day or args.dayofyear) or 0
	local seed = tostring(args.seed or "") ..
		":SS-COMPLIANCE-COL:" .. tostring(channel or "") ..
		":" .. tostring(rowNumber or 0) ..
		":" .. tostring(loc.code or "GEN") ..
		":" .. tostring(year) ..
		":" .. tostring(day)

	local roll = pctFromSeed(seed, "roll") + 1
	if roll > chance then
		if yn(args.showComplianceNoEvent or args.showcompliancenoevent, false) then return "No observation" end
		return "-"
	end

	local profile = pick(complianceProfiles, seed, "profile")
	local outcome = pick(complianceOutcomes, seed, "outcome")
	if not profile then return "-" end

	local seq = (pctFromSeed(seed, "seq") % 20) + 1
	local eventId
	if year > 0 and day > 0 then
		eventId = string.format("SS-CO-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
	else
		eventId = string.format("SS-CO-%s-%02d", loc.code or "GEN", seq)
	end
	local advNote = adversaryNotePart(args, "compliance", seed, profile, loc)
	return "'''Compliance observed'''<br>" ..
		"<small>" .. eventId .. "</small><br>" ..
		"'''Profile:''' " .. profile.label .. "<br>" ..
		"<small>" .. profile.description .. "<br>" ..
		"'''Outcome:''' " .. outcome .. advNote .. "</small>"
end

-- Row-column insertion helper
local function addCorridorColumns(body, args, channel)
	body = tostring(body or "")
	if not body:find("{|", 1, true) then return body end

	local showLocalityColumn = yn(args.rowlocalities or args.localities or args.showlocalities, true)
	local showTradeColumn = yn(args.tradecolumn or args.tradecolumns or args.showtradecolumn, true)
	local showComplianceColumn = yn(args.compliancecolumn or args.compliancecolumns or args.observedcolumn or args.showcompliancecolumn, true)
	local showRebellionColumn = yn(args.rebellioncolumn or args.rebellioncolumns or args.unrestcolumn or args.showrebellioncolumn, true)
	if not showLocalityColumn and not showTradeColumn and not showComplianceColumn and not showRebellionColumn then
		return body
	end

	local lines = mw.text.split(body, "\n", true)
	local out = {}
	local inTable = false
	local headerInserted = false
	local rowNumber = 0
	local localityState = newLocalityAssignmentState(args, channel)

	for _, line in ipairs(lines) do
		if line:match("^%s*{|") then
			inTable = true
			headerInserted = false
			table.insert(out, line)
		elseif inTable and not headerInserted and line:match("^%s*!") then
			if showLocalityColumn then table.insert(out, "! Locality focus") end
			if showTradeColumn then table.insert(out, "! Trade agreement") end
			if showComplianceColumn then table.insert(out, "! Compliance observed") end
			if showRebellionColumn then table.insert(out, "! Rebellion alert") end
			headerInserted = true
			table.insert(out, line)
		elseif inTable and line:match("^%s*|%-") then
			rowNumber = rowNumber + 1
			local loc = rowLocalityData(args, channel, rowNumber, localityState)
			table.insert(out, line)
			if showLocalityColumn then table.insert(out, "| " .. rowLocalityText(loc)) end
			if showTradeColumn then table.insert(out, "| " .. tradeAgreementCell(args, channel, rowNumber, loc)) end
			if showComplianceColumn then table.insert(out, "| " .. complianceObservedCell(args, channel, rowNumber, loc)) end
			if showRebellionColumn then table.insert(out, "| " .. rebellionAlertCell(args, channel, rowNumber, loc)) end
		elseif inTable and line:match("^%s*|}") then
			inTable = false
			table.insert(out, line)
		else
			table.insert(out, line)
		end
	end
	return table.concat(out, "\n")
end

-- Main Southern Strait-only view
function p.makeTable(frame)
	local args = normalizeArgsDate((frame and frame.args) or {})
	local showIntro = yn(args.intro, false)
	local showTitle = yn(args.showtitle or args.titleline, false)
	local showLegend = yn(args.legend or args.showlegend, false)
	local collapseSections = yn(args.collapsible, true)
	local startCollapsed = yn(args.collapsed, true)
	local includeWar = yn(args.war or args.warleague, true)
	local includeHatch = yn(args.hatch or args.hatchministry, true)
	local includeMission = yn(args.missionary or args.templebank, true)
	local year = args.year or args.psscyear
	local day = args.day or args.dayofyear
	local title = args.title or "Southern Strait Corridor Daily Operations"
	local label = dateLabel(args)
	local out = {}

	if showTitle then table.insert(out, "==" .. title .. "==") end
	if showIntro then
		table.insert(out, string.format(
			"''Southern Strait-only operational display for %s, limited to Mylecian Southern Strait corridor entries.''",
			label
		))
		table.insert(out, "")
		table.insert(out,
			"This display combines Council of Kings deployments, Hatch Ministry privateer activity, Temple Bank missionary operations, row-level Southern Strait locality focus columns, row-level compliance observations, and row-level rebellion-type unrest alerts while displaying the operational City/Node as Mylecia. Historical Temple Bank trade sign-offs remain reproducible through 27/3/53 PSSC, but all later Trade agreement cells display '-'. Imperial Federation, Bassaridian Mission, capped-locality, CKD reinforcement concentration, and Temple Bank adversarial-node modifiers are applied where effective."
		)
	end

	local ifSupportNotice = imperialFederationSupportNotice(args)
	if ifSupportNotice ~= "" then
		table.insert(out, ifSupportNotice)
		table.insert(out, "")
	end

	local missionNotice = bassaridianMissionsNotice(args)
	if missionNotice ~= "" then
		table.insert(out, missionNotice)
		table.insert(out, "")
	end

	local adversaryNotice = adversarialNodesNotice(args)
	if adversaryNotice ~= "" then
		table.insert(out, adversaryNotice)
		table.insert(out, "")
	end

	if includeWar then
		local warArgs = {
			year = year,
			day = day,
			rows = args.warrows or args.rows or "24",
			scanrows = args.warscanrows or args.scanrows or "160",
			showlegend = showLegend and "yes" or "no",
			flagsize = args.flagsize or "20",
			zone = "SouthernStrait",
			division = "Council of Kings Division"
		}
		if args.seed and tostring(args.seed) ~= "" then warArgs.seed = args.seed end
		local _, body = safeCall("Module:WarLeagueDivisionOpsBVR", "makeTable", warArgs)
		body = forceMyleciaCityNodeColumn(body, args)
		body = addCorridorColumns(body, args, "war")
		table.insert(out, hr())
		table.insert(out, section("Council of Kings Southern Strait deployments", body, collapseSections, startCollapsed))
	end

	if includeHatch then
		local hatchArgs = {
			year = year,
			day = day,
			legend = showLegend and "yes" or "no",
			zone = "SouthernStrait"
		}
		if args.captain and tostring(args.captain) ~= "" then hatchArgs.captain = args.captain end
		local _, body = safeCall("Module:HatchMinistryDailyOps", "dailyReport", hatchArgs)
		body = forceMyleciaCityNodeColumn(body, args)
		body = swapHatchHomeCityCurrentStation(body, args)
		body = addCorridorColumns(body, args, "hatch")
		table.insert(out, hr())
		table.insert(out, section("Hatch Ministry Southern Strait privateer operations", body, collapseSections, startCollapsed))
	end

	if includeMission then
		local missionaryArgs = {
			year = year,
			day = day,
			region = "Haifan",
			showlegend = showLegend and "yes" or "no",
			includeThreat = args.includeThreat or args.includethreat or "no",
			showEcon = args.showEcon or args.showecon or "no"
		}
		if args.missionarylimit and tostring(args.missionarylimit) ~= "" then
			missionaryArgs.limit = args.missionarylimit
		elseif args.limit and tostring(args.limit) ~= "" then
			missionaryArgs.limit = args.limit
		end
		local _, body = safeCall("Module:MissionaryDailyOpsBVR", "makeTable", missionaryArgs)
		body = forceMyleciaCityNodeColumn(body, args)
		body = addCorridorColumns(body, args, "missionary")
		table.insert(out, hr())
		table.insert(out, section("Temple Bank Southern Strait missionary operations", body, collapseSections, startCollapsed))
	end

	if showLegend then
		local legendBody = table.concat({
			"* '''Council of Kings Southern Strait deployments''' show only Council of Kings Division War League rows for the Southern Strait corridor.",
			"* '''Hatch Ministry Southern Strait privateer operations''' show Southern Strait privateer rows.",
			"* '''Temple Bank Southern Strait missionary operations''' show Haifan Bassaridian missionary rows, but the displayed City/Node is forced to Mylecia in this wrapper.",
			"* '''City/Node display''' is forced to [[File:MyleciaFlabB.jpeg|20px]] [[Mylecia]] for War League, Hatch Ministry, and Temple Bank rows.",
			"* '''Locality focus''' columns assign one extended Southern Strait, Eastern Mykonos Desert, or eastern Snowholme foothill settlement to each generated row when row-level localities are enabled.",
			"* '''CKD reinforcement at Nysara Velai''' assigns six War League locality rows there by default, representing the earlier three deployments plus three additional CKD deployments.",
			"* '''CKD reinforcement at Selenarion''' assigns two War League locality rows there by default, representing the earlier deployment plus one additional CKD deployment.",
			"* '''Hatch Ministry and Temple Bank locality targets''' remain at three Nysara Velai rows and one Selenarion row per section by default; the CKD reinforcement does not automatically increase those services.",
			"* '''Other capped presence localities''' are [[Mirovia]], [[Saruq Thyrsa]], and [[Thal Lenaia]]. Each may appear no more than once per section/channel.",
			"* '''Nysara Velai chance modifiers''' add +6 percentage points to compliance-observation likelihood and subtract 6 percentage points from unrest-alert likelihood. Its former +3-point trade-signoff modifier applies only to historical dates through 27/3/53 PSSC.",
			"* '''Trade agreement generation is closed''' from 28/3/53 PSSC onward. All channels display '''-''' in that column after the final Nysara Velai agreement; Temple Bank sign-offs remain reproducible only for historical dates through 27/3/53 PSSC.",
			"* '''Compliance observed''' columns show minor positive local behavior below the level of a formal trade agreement.",
			"* '''Rebellion alert''' columns show rare row-level contained unrest events such as forged proclamations, broker-cell activity, route barricades, false orders, ritual-cargo abuse, medical-transfer exploitation, or Dispossessed-adjacent courier behavior.",
			"* '''Mirovia''', '''Saruq Thyrsa''', and '''Thal Lenaia''' are hard-excluded from Rebellion Alert results; their Rebellion Alert cells always display '''-''', even when '''rebellionchance=100''' is used for testing.",
			"* '''Recurring adversarial nodes''' apply from 60/2/53 PSSC by default. Eligible rows may occasionally mention [[Khadir Veyran]] or [[Suyana Amaruq]] as suspected influence, but the notes are not guaranteed to appear.",
			"* '''Khadir Veyran''' notes are weighted toward broker factions, Bromian Ledger witness screens, anti-registry dock activity, cargo ambiguity, ledger correction, and trade-signoff counterweight language.",
			"* '''Suyana Amaruq''' notes are weighted toward courier screens, route barricades, false-pilgrim or guide-house movement, medical-transfer exploitation, and Black-Thread route-intelligence behavior.",
			"* '''Adversarial note frequency''' defaults to 35 percent for unrest alerts and 18 percent for compliance observations. The 22-percent trade-signoff note rate applies only to historical trade rows through 27/3/53 PSSC.",
			"* '''Imperial Federation support''' applies from 112/53 PSSC by default and adds +10 percentage points to compliance observations for Saruq Thyrsa, Mirovia, and Rumi-Kefaya. Its former +3-point trade-signoff effect is historical only and cannot generate agreements after 27/3/53 PSSC.",
			"* '''Bassaridian Missions support''' applies from 55/2/53 PSSC by default. It reduces unrest-alert chance by 1 percentage point in all locality rows and increases compliance-observation chance by 1 percentage point in all locality rows.",
			"* '''Major Bassaridian Missions''' at Saruq Thyrsa, Mirovia, Nysara Velai, Thal Lenaia, and Selenarion receive an additional +2 percentage points to compliance observations.",
			"* Use '''compliancechance=100''' and '''rebellionchance=100''' for testing. '''tradechance=100''' affects only historical dates through 27/3/53 PSSC and cannot override the post-agreement cutoff.",
			"* Use '''warnysaravelaitarget=6''' to change only the War League / CKD Nysara Velai target.",
			"* Use '''warselenariontarget=2''' to change only the War League / CKD Selenarion target.",
			"* Use '''nysaravelaitarget=3''' or '''selenariontarget=1''' to override the corresponding target in every section/channel.",
			"* Use '''nysaravelaioverfill=yes''' to allow Nysara Velai to exceed its section target after the priority rows are assigned.",
			"* Use '''nysaravelaicomplianceboost=6''' and '''nysaravelaiunrestreduction=6''' to change the active Nysara Velai modifiers. '''nysaravelaitradeboost=3''' is retained solely for historical reproduction through 27/3/53 PSSC. The older '''nysaravelaiunrestboost=''' parameter remains available as an additive legacy override.",
			"* Use '''adversarynodes=no''' to disable named adversarial notes.",
			"* Use '''adversarychance=35''' to change the default unrest-alert adversarial note chance.",
			"* Use '''complianceadversarychance=18''' to change the default compliance-observation adversarial note chance.",
			"* Use '''tradeadversarychance=22''' only to change adversarial-note frequency in historical Temple Bank trade-signoff rows through 27/3/53 PSSC.",
			"* Use '''adversarystartyear=53''' and '''adversarystartday=121''' to change the default 60/2/53 PSSC start date for named adversarial-node tracking.",
			"* Use '''missionsupport=no''' to disable Bassaridian Missions modifiers for testing.",
			"* Use '''imperialsupport=no''' to disable Imperial Federation support modifiers for testing.",
			"* Use '''forcemylecia=no''' only if the wrapper should temporarily show the original source-module City/Node values."
		}, "\n")
		table.insert(out, hr())
		table.insert(out, section("Combined legend", legendBody, collapseSections, startCollapsed))
	end

	return table.concat(out, "\n")
end

-- Shortcut views
function p.southernStrait(frame)
	local args = (frame and frame.args) or {}
	args.title = args.title or "Southern Strait Corridor Daily Operations"
	return p.makeTable({ args = args })
end

function p.warOnly(frame)
	local args = (frame and frame.args) or {}
	args.hatch = "no"
	args.missionary = "no"
	args.war = "yes"
	args.title = args.title or "War League Southern Strait Daily Operations"
	return p.makeTable({ args = args })
end

function p.hatchOnly(frame)
	local args = (frame and frame.args) or {}
	args.war = "no"
	args.missionary = "no"
	args.hatch = "yes"
	args.title = args.title or "Hatch Ministry Southern Strait Daily Operations"
	return p.makeTable({ args = args })
end

function p.missionaryOnly(frame)
	local args = (frame and frame.args) or {}
	args.war = "no"
	args.hatch = "no"
	args.missionary = "yes"
	args.title = args.title or "Temple Bank Southern Strait Missionary Daily Operations"
	return p.makeTable({ args = args })
end

return p