Module:SouthernStraitCorridorOps: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
No edit summary
NewZimiaGov (talk | contribs)
No edit summary
Line 3: Line 3:


-- Helpers
-- Helpers
local function yn(value, default)
local function yn(value, default)
local v = tostring(value or ""):lower()
local v = tostring(value or ""):lower()
Line 10: Line 11:
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 })
Line 19: Line 22:
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 < 1 then dayOfYear = 1 end
if dayOfYear > 183 then dayOfYear = 183 end
if dayOfYear > 183 then dayOfYear = 183 end


return psscYear, dayOfYear
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
for k, v in pairs(args or {}) do
out[k] = v
out[k] = v
Line 47: Line 55:


return out
return out


end
end
local function safeRequire(moduleName)
local function safeRequire(moduleName)
local ok, mod = pcall(require, moduleName)
local ok, mod = pcall(require, moduleName)
Line 55: Line 65:
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
if not mod then
return false, "''" .. err .. ".''"
return false, "''" .. err .. ".''"
end
end
if type(mod[funcName]) ~= "function" then
if type(mod[funcName]) ~= "function" then
return false, "''" .. tostring(moduleName) .. " has no public function named " .. tostring(funcName) .. ".''"
return false, "''" .. tostring(moduleName) .. " has no public function named " .. tostring(funcName) .. ".''"
Line 75: Line 88:


return true, tostring(result or "")
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
if doy <= 61 then
return 1, doy
return 1, doy
Line 88: Line 105:
return 3, doy - 122
return 3, doy - 122
end
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
if y and d then
local m, dom = doyToMonthDom(d)
local m, dom = doyToMonthDom(d)
Line 99: Line 120:


return "current PSSC date"
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
if not collapsible then
table.insert(out, "===" .. safeTitle .. "===")
table.insert(out, "===" .. safeTitle .. "===")
Line 121: Line 146:


return table.concat(out, "\n")
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
for i = 1, #s do
h = (h * 31 + string.byte(s, i)) % 1000003
h = (h * 31 + string.byte(s, i)) % 1000003
Line 134: Line 164:


return h
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
Line 151: Line 184:
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
Line 156: Line 190:
startYear = tonumber(startYear) or 0
startYear = tonumber(startYear) or 0
startDay = tonumber(startDay) or 0
startDay = tonumber(startDay) or 0
if year > startYear then
if year > startYear then
return true
return true
Line 165: Line 201:


return false
return false


end
end


-- Extended Southern Strait / Eastern Mykonos / Snowholme locality list
-- Extended Southern Strait / Eastern Mykonos / Snowholme locality list
local southernTradeCities = {
local southernTradeCities = {
{
{
Line 238: Line 276:
-- MIR, SAR, and THQ are capped at one displayed unit per section/channel.
-- MIR, SAR, and THQ are capped at one displayed unit per section/channel.
-- QSE is targeted for up to three displayed units per section/channel by default.
-- QSE is targeted for up to three displayed units per section/channel by default.
local cappedPresenceLocalities = {
local cappedPresenceLocalities = {
MIR = true,
MIR = true,
Line 243: Line 282:
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
for _, loc in ipairs(southernTradeCities or {}) do
if tostring(loc.code or "") == code then
if tostring(loc.code or "") == code then
Line 252: Line 294:


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
local code
if type(locOrCode) == "table" then
if type(locOrCode) == "table" then
code = tostring(locOrCode.code or "")
code = tostring(locOrCode.code or "")
Line 264: Line 311:


return tonumber(state.counts[code]) or 0
return tonumber(state.counts[code]) or 0


end
end
local function incrementLocalityCount(state, loc)
local function incrementLocalityCount(state, loc)
if not state or not loc then return end
if not state or not loc then return end
local code = tostring(loc.code or "")
local code = tostring(loc.code or "")
if code == "" then return end
if code == "" then return end


state.counts[code] = (tonumber(state.counts[code]) or 0) + 1
state.counts[code] = (tonumber(state.counts[code]) or 0) + 1


end
end
local function qhapaqSelenPresenceTarget(args)
local function qhapaqSelenPresenceTarget(args)
local v = tonumber(
local v = tonumber(
Line 282: Line 335:
args.qhapagselentarget
args.qhapagselentarget
)
)
if v == nil then v = 3 end
if v == nil then v = 3 end
if v < 0 then v = 0 end
if v < 0 then v = 0 end


return math.floor(v)
return math.floor(v)


end
end
local function qhapaqSelenPresenceInterval(args)
local function qhapaqSelenPresenceInterval(args)
local v = tonumber(
local v = tonumber(
Line 296: Line 353:
args.qhapagseleninterval
args.qhapagseleninterval
)
)
if v == nil then v = 3 end
if v == nil then v = 3 end
if v < 1 then v = 1 end
if v < 1 then v = 1 end


return math.floor(v)
return math.floor(v)


end
end
local function qhapaqSelenAllowOverTarget(args)
local function qhapaqSelenAllowOverTarget(args)
return yn(
return yn(
Line 312: Line 373:
)
)
end
end
local function newLocalityAssignmentState(args, channel)
local function newLocalityAssignmentState(args, channel)
return {
return {
Line 321: Line 383:
}
}
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 "")
local code = tostring(loc.code or "")


Line 336: Line 401:


return true
return true


end
end
local function shouldAssignQhapaqSelen(args, channel, rowNumber, state)
local function shouldAssignQhapaqSelen(args, channel, rowNumber, state)
if not state then return false end
if not state then return false end
local target = tonumber(state.qseTarget) or 3
local target = tonumber(state.qseTarget) or 3
if target <= 0 then return false end
if target <= 0 then return false end
Line 376: Line 445:


return false
return false


end
end


-- Row-level locality focus helper
-- 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
return nil
end
end
state = state or newLocalityAssignmentState(args, channel)
 
 
state = state or newLocalityAssignmentState(args, channel)


local qse = findLocalityByCode("QSE")
local qse = findLocalityByCode("QSE")
if qse and shouldAssignQhapaqSelen(args, channel, rowNumber, state) and canAssignLocality(args, qse, state) then
if qse and shouldAssignQhapaqSelen(args, channel, rowNumber, state) and canAssignLocality(args, qse, state) then
incrementLocalityCount(state, qse)
incrementLocalityCount(state, qse)
Line 393: Line 467:


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


local seed = tostring(args.seed or "") ..
local seed = tostring(args.seed or "") ..
Line 416: Line 490:
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 ~= "QSE" then
if not cappedPresenceLocalities[code] and code ~= "QSE" then
incrementLocalityCount(state, loc)
incrementLocalityCount(state, loc)
Line 423: Line 498:


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"
return "Southern Strait corridor"
end
end
return loc.city .. "<br><small>" .. loc.region .. "</small>"
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
-- 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,
Line 442: Line 524:
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
return false
end
end
local year = tonumber(args.year or args.psscyear) or 0
local year = tonumber(args.year or args.psscyear) or 0
local day = tonumber(args.day or args.dayofyear) or 0
local day = tonumber(args.day or args.dayofyear) or 0
Line 455: Line 541:


return isOnOrAfterPSSCDate(year, day, startYear, startDay)
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
return false
end
end
return loc and imperialFederationSupportLocalities[tostring(loc.code or "")] == true
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
if hasImperialFederationSupport(args, loc) then
chance = chance + (tonumber(boost) or 0)
chance = chance + (tonumber(boost) or 0)
Line 471: Line 565:


return clampChance(chance)
return clampChance(chance)


end
end
local function imperialFederationSupportNotice(args)
local function imperialFederationSupportNotice(args)
if not imperialSupportEffective(args) then
if not imperialSupportEffective(args) then
return ""
return ""
end
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 and +3 percentage points to Temple Bank trade sign-offs from 112/53 PSSC onward.</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
Line 484: Line 583:
-- Publicly acknowledged by the Council of Kings Division on 55/2/53 PSSC.
-- Publicly acknowledged by the Council of Kings Division on 55/2/53 PSSC.
-- 55/2/53 PSSC = day-of-year 116.
-- 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,
Line 493: Line 594:
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
return false
end
end
local year = tonumber(args.year or args.psscyear) or 0
local year = tonumber(args.year or args.psscyear) or 0
local day = tonumber(args.day or args.dayofyear) or 0
local day = tonumber(args.day or args.dayofyear) or 0
Line 506: Line 611:


return isOnOrAfterPSSCDate(year, day, startYear, startDay)
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
return false
end
end
return loc ~= nil
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
return false
end
end
return loc and majorBassaridianMissionLocalities[tostring(loc.code or "")] == true
return loc and majorBassaridianMissionLocalities[tostring(loc.code or "")] == true


end
end
local function qhapaqSelenComplianceBoost(args)
local function qhapaqSelenComplianceBoost(args)
local v = tonumber(
local v = tonumber(
Line 530: Line 645:
args.qhapagselencomplianceboost
args.qhapagselencomplianceboost
)
)
if v == nil then v = 5 end
if v == nil then v = 5 end


return v
return v


end
end
local function qhapaqSelenUnrestBoost(args)
local function qhapaqSelenUnrestBoost(args)
local v = tonumber(
local v = tonumber(
Line 543: Line 662:
args.qhapagselenunrestboost
args.qhapagselenunrestboost
)
)
if v == nil then v = 4 end
if v == nil then v = 4 end


return v
return v


end
end
local function qhapaqSelenAdjustedComplianceChance(args, loc, baseChance)
local function qhapaqSelenAdjustedComplianceChance(args, loc, baseChance)
local chance = clampChance(baseChance)
local chance = clampChance(baseChance)
if loc and tostring(loc.code or "") == "QSE" then
if loc and tostring(loc.code or "") == "QSE" then
chance = chance + qhapaqSelenComplianceBoost(args)
chance = chance + qhapaqSelenComplianceBoost(args)
Line 555: Line 680:


return clampChance(chance)
return clampChance(chance)


end
end
local function qhapaqSelenAdjustedUnrestChance(args, loc, baseChance)
local function qhapaqSelenAdjustedUnrestChance(args, loc, baseChance)
local chance = clampChance(baseChance)
local chance = clampChance(baseChance)
if loc and tostring(loc.code or "") == "QSE" then
if loc and tostring(loc.code or "") == "QSE" then
chance = chance + qhapaqSelenUnrestBoost(args)
chance = chance + qhapaqSelenUnrestBoost(args)
Line 564: Line 693:


return clampChance(chance)
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
if hasBassaridianMission(args, loc) then
chance = chance + (tonumber(args.missioncomplianceboost or args.missionComplianceBoost) or 1)
chance = chance + (tonumber(args.missioncomplianceboost or args.missionComplianceBoost) or 1)
Line 577: Line 710:


return clampChance(chance)
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
if hasBassaridianMission(args, loc) then
chance = chance - (tonumber(args.missionunrestreduction or args.missionUnrestReduction) or 1)
chance = chance - (tonumber(args.missionunrestreduction or args.missionUnrestReduction) or 1)
Line 586: Line 723:


return clampChance(chance)
return clampChance(chance)


end
end
local function bassaridianMissionsNotice(args)
local function bassaridianMissionsNotice(args)
if not bassaridianMissionsEffective(args) then
if not bassaridianMissionsEffective(args) then
return ""
return ""
end
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]], [[Ayllu Nysara]], [[Thal Qencha]], and [[Qhapaq Selen]].<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. Qhapaq Selen 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>"
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]], [[Ayllu Nysara]], [[Thal Qencha]], and [[Qhapaq Selen]].<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. Qhapaq Selen 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>"


end
end


-- Forced City/Node display
-- Recurring adversarial-node tracking
local forcedCityNodeText = "[[File:MyleciaFlabB.jpeg|20px]] [[Mylecia]]"
-- Temple Bank report context begins 60/2/53 PSSC.
local function cleanHeaderLabel(line)
-- 60/2/53 PSSC = day-of-year 121.
local s = tostring(line or "")
 
s = s:gsub("^%s*!+%s*", "")
local ADVERSARY_NODES_START_YEAR = 53
if s:find("|", 1, true) then
local ADVERSARY_NODES_START_DAY = 121
local parts = mw.text.split(s, "|", true)
 
s = parts[#parts] or s
local recurringAdversaries = {
KHADIR = {
name = "[[Khadir Veyran]]",
epithet = "Keeper of the Ash Ledger",
localities = {
QSE = true,
AYS = true,
RUK = true,
HAY = true
},
notes = {
"Ash 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 Ash Ledger are suspected of turning routine paperwork into a jurisdictional dispute."
},
complianceNotes = {
"Temple Bank auditors noted that this cooperation weakens the Ash 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 Ash 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 Ash 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
end


s = s:gsub("<.->", "")
local function adversaryNodesEffective(args)
s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
if not adversaryNodesEnabled(args) then
s = s:gsub("%[%[([^%]]+)%]%]", "%1")
return false
s = s:gsub("&nbsp;", " ")
end
s = s:gsub("%s*/%s*", "/")
 
s = s:gsub("^%s+", ""):gsub("%s+$", "")
 
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 "")


return s:lower()


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
end
local cityNodeHeaderLabels = {
 
["city"] = true,
return clampChance(tonumber(args.adversarychance or args.adversaryChance or args.unrestadversarychance or args.unrestAdversaryChance) or 35)
["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
end


if not body:find("{|", 1, true) then
local function localityAllowsAdversary(loc, adv)
return body
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
end


local forcedText = args.forcecitytext or args.forceCityText or forcedCityNodeText
local function candidateAdversaryCodes(profile, loc)
local lines = mw.text.split(body, "\n", true)
local out = {}
local out = {}


local inTable = false
local cityCol = nil
local col = 0


for _, line in ipairs(lines) do
if profile and type(profile.adversaries) == "table" then
if line:match("^%s*{|") then
for _, code in ipairs(profile.adversaries) do
inTable = true
local adv = recurringAdversaries[code]
cityCol = nil
if adv and localityAllowsAdversary(loc, adv) then
col = 0
table.insert(out, code)
table.insert(out, line)
end
end
end


elseif inTable and line:match("^%s*!") then
if #out == 0 then
local raw = line:gsub("^%s*!+%s*", "")
for code, adv in pairs(recurringAdversaries) do
local headers = mw.text.split(raw, "!!", true)
if localityAllowsAdversary(loc, adv) then
 
table.insert(out, code)
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
end
end
end


table.insert(out, line)
return out


elseif inTable and line:match("^%s*|%-") then
col = 0
table.insert(out, line)


elseif inTable and cityCol and line:match("^%s*|[^}%-%+]") then
end
local raw = line:gsub("^%s*|%s*", "")
local cells = mw.text.split(raw, "||", true)


if #cells > 1 then
local function adversaryNotePart(args, kind, seed, profile, loc)
for i = 1, #cells do
if not adversaryNodesEffective(args) then
if i == cityCol then
return ""
cells[i] = " " .. forcedText .. " "
end
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
local chance = adversaryNoteChance(args, kind)
inTable = false
local roll = pctFromSeed(seed, "adv-note-roll-" .. tostring(kind or "general")) + 1
table.insert(out, line)


else
if roll > chance then
table.insert(out, line)
return ""
end
end
end


return table.concat(out, "\n")
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
end


-- Hatch Ministry Home city / Current station swap
local notes = adv.notes
local function swapHatchHomeCityCurrentStation(body, args)
 
body = tostring(body or "")
if tostring(kind or "") == "trade" and adv.tradeNotes then
if yn(args.swaphatchstation or args.swapHatchStation or args.swaphatchhomecity or args.swapHatchHomeCity, true) == false then
notes = adv.tradeNotes
return body
elseif tostring(kind or "") == "compliance" and adv.complianceNotes then
notes = adv.complianceNotes
end
end


if not body:find("{|", 1, true) then
local note = pick(notes, seed, "adv-note-text-" .. tostring(kind or "general"))
return body
if not note then
return ""
end
end


local lines = mw.text.split(body, "\n", true)
return "<br>'''Adversarial note:''' " .. adv.name .. " (''" .. adv.epithet .. "'') — " .. note
local out = {}


local inTable = false
local homeCol = nil
local stationCol = nil
local col = 0


for _, line in ipairs(lines) do
end
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 function adversarialNodesNotice(args)
local raw = line:gsub("^%s*!+%s*", "")
if not adversaryNodesEffective(args) then
local headers = mw.text.split(raw, "!!", true)
return ""
end


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
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 Ash 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>"
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)
end


elseif inTable and line:match("^%s*|%-") then
-- Forced City/Node display
col = 0
table.insert(out, line)


elseif inTable and homeCol and stationCol and line:match("^%s*|[^}%-%+]") then
local forcedCityNodeText = "[[File:MyleciaFlabB.jpeg|20px]] [[Mylecia]]"
local raw = line:gsub("^%s*|%s*", "")
local cells = mw.text.split(raw, "||", true)


if #cells > 1 then
local function cleanHeaderLabel(line)
if cells[homeCol] and cells[stationCol] then
local s = tostring(line or "")
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
s = s:gsub("^%s*!+%s*", "")
table.insert(out, line)
 
end
if s:find("|", 1, true) then
end
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()


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


end
end


-- Cult sign-off list for Temple Bank trade agreements
local cityNodeHeaderLabels = {
local tradeSigningCults = {
["city"] = true,
{ code = "UMB", label = "[[Noctis (Reformed Stripping Path)#Order of the Umbral Oracle|Order of the Umbral Oracle]]" },
["node"] = true,
{ code = "GGS", label = "[[Chrysos (Reformed Stripping Path)#Guild of Golden Shadows|Guild of Golden Shadows]]" },
["city/node"] = true,
{ code = "AZS", label = "[[Thalassa (Reformed Stripping Path)#Azure Sentinel Sect|Azure Sentinel Sect]]" },
["city / node"] = true,
{ code = "HAR", label = "[[Micras (Reformed Stripping Path)#Harmony Sanctum|Harmony Sanctum]]" },
["city or node"] = true,
{ code = "VIT", label = "[[Plateau (Reformed Stripping Path)#Sanctum Vitalis|Sanctum Vitalis]]" },
["home port"] = true,
{ code = "AUR", label = "[[Eos (Reformed Stripping Path)#Order of Aurora Mystica|Order of Aurora Mystica]]" },
["homeport"] = true,
{ code = "DEL", label = "[[Sisera (Reformed Stripping Path)#Sanctum Delphica|Sanctum Delphica]]" },
["home city"] = true,
{ code = "CAT", label = "[[Cato (Reformed Stripping Path)#Rex Catonis|Rex Catonis]]" },
["base city"] = true,
{ code = "CON", label = "[[Lukedu (Reformed Stripping Path)#Accord Concordia|Accord Concordia]]" },
["staging city"] = true
{ 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


local chance = imperialSupportBoostedChance(args, loc, tonumber(args.tradechance or args.tradeChance) or 3, 3)
local function forceMyleciaCityNodeColumn(body, args)
body = tostring(body or "")


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 "") ..
if yn(args.forcemylecia or args.forceMylecia or args.myleciacity or args.myleciaCity, true) == false then
":SS-TEMPLE-TRADE-COL:" .. tostring(rowNumber or 0) ..
return body
":" .. tostring(loc.code or "GEN") ..
end
":" .. tostring(year) ..
":" .. tostring(day)


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


local cult = pick(tradeSigningCults, seed, "cult")
local forcedText = args.forcecitytext or args.forceCityText or forcedCityNodeText
local outcome = pick(tradeOutcomes, seed, "outcome")
local lines = mw.text.split(body, "\n", true)
local out = {}


if not cult then
local inTable = false
return "-"
local cityCol = nil
end
local col = 0


local seq = (pctFromSeed(seed, "seq") % 20) + 1
for _, line in ipairs(lines) do
local tradeId
if line:match("^%s*{|") then
inTable = true
cityCol = nil
col = 0
table.insert(out, line)


if year > 0 and day > 0 then
elseif inTable and line:match("^%s*!") then
tradeId = string.format("GP-SS-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
local raw = line:gsub("^%s*!+%s*", "")
else
local headers = mw.text.split(raw, "!!", true)
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)
if #headers > 1 then
 
for _, h in ipairs(headers) do
return "'''Trade sign-off'''<br>" ..
col = col + 1
"<small>" .. tradeId .. "</small><br>" ..
if cityNodeHeaderLabels[cleanHeaderLabel("! " .. h)] then
(cult.label or "Cult sign-off recorded") .. "<br>" ..
cityCol = col
"<small>Code: '''" .. signCode .. "'''<br>" .. outcome .. "</small>"
end
end
else
col = col + 1
if cityNodeHeaderLabels[cleanHeaderLabel(line)] then
cityCol = col
end
end
 
table.insert(out, line)


end
elseif inTable and line:match("^%s*|%-") then
col = 0
table.insert(out, line)


-- Rebellion / corridor unrest row column
elseif inTable and cityCol and line:match("^%s*|[^}%-%+]") then
local rebellionProfiles = {
local raw = line:gsub("^%s*|%s*", "")
{ label = "Confederacy-adjacent courier screen", description = "False-route couriers testing inspection gaps." },
local cells = mw.text.split(raw, "||", true)
{ label = "Former Maritime Markets broker faction", description = "Old broker-house retainers resisting General Port paperwork." },
 
{ label = "Anti-registry dock syndicate", description = "Warehouse intermediaries opposing voucher and ledger discipline." },
if #cells > 1 then
{ label = "False-proclamation circle", description = "Pamphlet readers circulating forged civic orders." },
for i = 1, #cells do
{ label = "Route-barricade committee", description = "Agitators obstructing lawful inspection lanes." },
if i == cityCol then
{ label = "Dispossessed-sympathetic quay cell", description = "Waterfront cell copying Confederacy-style courier habits." }
cells[i] = " " .. forcedText .. " "
}
end
local rebellionTriggers = {
end
"Forged levy notice.",
table.insert(out, "| " .. table.concat(cells, " || "))
"Masked speakers redirected traffic.",
else
"Missing seals and altered registry slips.",
col = col + 1
"Crowd pressure over bonded cargo.",
if col == cityCol then
"Tampered signal schedule.",
table.insert(out, "| " .. forcedText)
"Conflicting route orders.",
else
"Coordinated paper strike.",
table.insert(out, line)
"Anti-Bassaridian chant movement.",
end
"Repeated courier handoffs.",
end
"Matched false manifests."
 
}
elseif inTable and line:match("^%s*|}") then
local rebellionActions = {
inTable = false
"CKD cordon and evidence hold.",
table.insert(out, line)
"Temple rumor-control desk opened.",
 
"Ledgers and copied seals sealed.",
else
"Privateer stand-off posture.",
table.insert(out, line)
"Pass papers reissued.",
end
"Lawful cargo separated.",
"Missionary mediation opened.",
"Corrected signal notice issued.",
"Temporary General Port audit lane opened.",
"Incident classified as contained unrest."
}
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
end
return rebellionAlertExcludedLocalities[tostring(loc.code or "")] == true


end
return table.concat(out, "\n")
local function rebellionAlertCell(args, channel, rowNumber, loc)
 
if not loc then
 
return "-"
end
if isRebellionAlertExcluded(loc) then
return "-"
end
end


local baseChance = tonumber(args.rebellionchance or args.rebellionChance or args.unrestchance or args.unrestChance) or 6
-- Hatch Ministry Home city / Current station swap
local chance = missionAdjustedUnrestChance(args, loc, baseChance)
chance = qhapaqSelenAdjustedUnrestChance(args, loc, chance)


local year = tonumber(args.year or args.psscyear) or 0
local function swapHatchHomeCityCurrentStation(body, args)
local day  = tonumber(args.day or args.dayofyear) or 0
body = tostring(body or "")
 
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 "-"
if yn(args.swaphatchstation or args.swapHatchStation or args.swaphatchhomecity or args.swapHatchHomeCity, true) == false then
return body
end
end


local profile = pick(rebellionProfiles, seed, "profile")
if not body:find("{|", 1, true) then
local trigger = pick(rebellionTriggers, seed, "trigger")
return body
local action = pick(rebellionActions, seed, "action")
local risk = pick(rebellionRisks, seed, "risk")
 
if not profile then
return "-"
end
end


local seq = (pctFromSeed(seed, "seq") % 20) + 1
local lines = mw.text.split(body, "\n", true)
local eventId
local out = {}


if year > 0 and day > 0 then
local inTable = false
eventId = string.format("SS-RB-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
local homeCol = nil
else
local stationCol = nil
eventId = string.format("SS-RB-%s-%02d", loc.code or "GEN", seq)
local col = 0
end


return "'''Unrest alert'''<br>" ..
for _, line in ipairs(lines) do
"<small>" .. eventId .. "</small><br>" ..
if line:match("^%s*{|") then
"'''Profile:''' " .. profile.label .. "<br>" ..
inTable = true
"<small>" .. profile.description .. "<br>" ..
homeCol = nil
"'''Trigger:''' " .. trigger .. "<br>" ..
stationCol = nil
"'''Risk:''' " .. risk .. "<br>" ..
col = 0
"'''Response:''' " .. action .. "</small>"
table.insert(out, line)


end
elseif inTable and line:match("^%s*!") then
local raw = line:gsub("^%s*!+%s*", "")
local headers = mw.text.split(raw, "!!", true)


-- Compliance observed / minor positive row column
if #headers > 1 then
local complianceProfiles = {
for _, h in ipairs(headers) do
{ label = "Inspection cooperation", description = "Local carriers complied with inspection sequencing without requiring escalation." },
col = col + 1
{ label = "Ledger correction", description = "Route clerks corrected incomplete or inconsistent paperwork before formal penalty." },
local label = cleanHeaderLabel("! " .. h)
{ label = "Crowd dispersal", description = "Local stewards dispersed a gathering before it became an obstruction." },
{ label = "Guide assistance", description = "Licensed guides helped separate lawful traffic from suspect courier movement." },
{ label = "Cargo separation", description = "Bonded cargo was voluntarily separated for review without disrupting ordinary passage." },
{ label = "Rumor-control compliance", description = "Temple notices were accepted locally and hostile rumors lost momentum." },
{ label = "Privateer signal compliance", description = "Vessels accepted stand-off or hold-and-release signals without resistance." },
{ label = "Registry cooperation", description = "Local registrars preserved names, seals, and witness records for later review." }
}
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 = qhapaqSelenAdjustedComplianceChance(args, loc, chance)


local year = tonumber(args.year or args.psscyear) or 0
if label == "home city" or label == "home" then
local day  = tonumber(args.day or args.dayofyear) or 0
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


local seed = tostring(args.seed or "") ..
table.insert(out, line)
":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
elseif inTable and line:match("^%s*|%-") then
if roll > chance then
col = 0
if yn(args.showComplianceNoEvent or args.showcompliancenoevent, false) then
table.insert(out, line)
return "No observation"
end


return "-"
elseif inTable and homeCol and stationCol and line:match("^%s*|[^}%-%+]") then
end
local raw = line:gsub("^%s*|%s*", "")
local cells = mw.text.split(raw, "||", true)


local profile = pick(complianceProfiles, seed, "profile")
if #cells > 1 then
local outcome = pick(complianceOutcomes, seed, "outcome")
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


if not profile then
elseif inTable and line:match("^%s*|}") then
return "-"
inTable = false
table.insert(out, line)
 
else
table.insert(out, line)
end
end
end


local seq = (pctFromSeed(seed, "seq") % 20) + 1
return table.concat(out, "\n")
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


return "'''Compliance observed'''<br>" ..
"<small>" .. eventId .. "</small><br>" ..
"'''Profile:''' " .. profile.label .. "<br>" ..
"<small>" .. profile.description .. "<br>" ..
"'''Outcome:''' " .. outcome .. "</small>"


end
end


-- Row-column insertion helper
-- Cult sign-off list for Temple Bank trade agreements
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 tradeSigningCults = {
local showTradeColumn = yn(args.tradecolumn or args.tradecolumns or args.showtradecolumn, true)
{ code = "UMB", label = "[[Noctis (Reformed Stripping Path)#Order of the Umbral Oracle|Order of the Umbral Oracle]]" },
local showComplianceColumn = yn(args.compliancecolumn or args.compliancecolumns or args.observedcolumn or args.showcompliancecolumn, true)
{ code = "GGS", label = "[[Chrysos (Reformed Stripping Path)#Guild of Golden Shadows|Guild of Golden Shadows]]" },
local showRebellionColumn = yn(args.rebellioncolumn or args.rebellioncolumns or args.unrestcolumn or args.showrebellioncolumn, true)
{ 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]]" }
}


if not showLocalityColumn and not showTradeColumn and not showComplianceColumn and not showRebellionColumn then
local tradeOutcomes = {
return body
"Cleared for General Port ledger review.",
end
"Provisional admission recommended.",
"Conditional trade window approved.",
"Company charter accepted for corridor listing.",
"Trade agreement filed through ordinary registry channels."
}


local lines = mw.text.split(body, "\n", true)
local function tradeAgreementCell(args, channel, rowNumber, loc)
local out = {}
if tostring(channel or "") ~= "missionary" then
return "-"
end


local inTable = false
local headerInserted = false
local rowNumber = 0
local localityState = newLocalityAssignmentState(args, channel)


for _, line in ipairs(lines) do
if not loc then
if line:match("^%s*{|") then
return "-"
inTable = true
end
headerInserted = false
table.insert(out, line)


elseif inTable and not headerInserted and line:match("^%s*!") then
local chance = imperialSupportBoostedChance(args, loc, tonumber(args.tradechance or args.tradeChance) or 3, 3)
if showLocalityColumn then
table.insert(out, "! Locality focus")
end


if showTradeColumn then
local year = tonumber(args.year or args.psscyear) or 0
table.insert(out, "! Trade agreement")
local day = tonumber(args.day or args.dayofyear) or 0
end


if showComplianceColumn then
local seed = tostring(args.seed or "") ..
table.insert(out, "! Compliance observed")
":SS-TEMPLE-TRADE-COL:" .. tostring(rowNumber or 0) ..
end
":" .. tostring(loc.code or "GEN") ..
":" .. tostring(year) ..
":" .. tostring(day)


if showRebellionColumn then
local roll = pctFromSeed(seed, "roll") + 1
table.insert(out, "! Rebellion alert")
end


headerInserted = true
if roll > chance then
table.insert(out, line)
if yn(args.showTradeNoEvent or args.showtradenoevent, false) then
return "No sign-off"
end


elseif inTable and line:match("^%s*|%-") then
return "-"
rowNumber = rowNumber + 1
end
local loc = rowLocalityData(args, channel, rowNumber, localityState)


table.insert(out, line)
local cult = pick(tradeSigningCults, seed, "cult")
local outcome = pick(tradeOutcomes, seed, "outcome")


if showLocalityColumn then
if not cult then
table.insert(out, "| " .. rowLocalityText(loc))
return "-"
end
end


if showTradeColumn then
local seq = (pctFromSeed(seed, "seq") % 20) + 1
table.insert(out, "| " .. tradeAgreementCell(args, channel, rowNumber, loc))
local tradeId
end


if showComplianceColumn then
if year > 0 and day > 0 then
table.insert(out, "| " .. complianceObservedCell(args, channel, rowNumber, loc))
tradeId = string.format("GP-SS-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
end
else
tradeId = string.format("GP-SS-%s-%02d", loc.code or "GEN", seq)
end


if showRebellionColumn then
local signCode = string.format("%s-TA%02d", cult.code or "CUL", seq)
table.insert(out, "| " .. rebellionAlertCell(args, channel, rowNumber, loc))
local advNote = adversaryNotePart(args, "trade", seed, nil, loc)
end


elseif inTable and line:match("^%s*|}") then
return "'''Trade sign-off'''<br>" ..
inTable = false
"<small>" .. tradeId .. "</small><br>" ..
table.insert(out, line)
(cult.label or "Cult sign-off recorded") .. "<br>" ..
"<small>Code: '''" .. signCode .. "'''<br>" .. outcome .. advNote .. "</small>"


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


end
end


-- Main Southern Strait-only view
-- Rebellion / corridor unrest row column
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 rebellionProfiles = {
local includeHatch  = yn(args.hatch or args.hatchministry, true)
{
local includeMission = yn(args.missionary or args.templebank, true)
label = "Confederacy-adjacent courier screen",
 
description = "False-route couriers testing inspection gaps.",
local year = args.year or args.psscyear
adversaries = { "SUYANA" }
local day  = args.day or args.dayofyear
},
local title = args.title or "Southern Strait Corridor Daily Operations"
{
local label = dateLabel(args)
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 = "Ash Ledger witness screen",
description = "Broker-linked witnesses preserving conflicting older records against General Port review.",
adversaries = { "KHADIR" }
}
}


local out = {}
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."
}


if showTitle then
local rebellionActions = {
table.insert(out, "==" .. title .. "==")
"CKD cordon and evidence hold.",
end
"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."
}


if showIntro then
local rebellionRisks = {
table.insert(out, string.format(
"Low",
"''Southern Strait-only operational display for %s, limited to Mylecian Southern Strait corridor entries.''",
"Moderate",
label
"High",
))
"Contained",
table.insert(out, "")
"Escalating"
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, and Qhapaq Selen concentration modifiers are applied where effective.")
}
 
local rebellionAlertExcludedLocalities = {
MIR = true,
SAR = true,
THQ = true
}
 
local function isRebellionAlertExcluded(loc)
if not loc then
return false
end
end


local ifSupportNotice = imperialFederationSupportNotice(args)
 
if ifSupportNotice ~= "" then
return rebellionAlertExcludedLocalities[tostring(loc.code or "")] == true
table.insert(out, ifSupportNotice)
 
table.insert(out, "")
 
end
end


local missionNotice = bassaridianMissionsNotice(args)
local function rebellionAlertCell(args, channel, rowNumber, loc)
if missionNotice ~= "" then
if not loc then
table.insert(out, missionNotice)
return "-"
table.insert(out, "")
end
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
if isRebellionAlertExcluded(loc) then
warArgs.seed = args.seed
return "-"
end
end


local ok, body = safeCall("Module:WarLeagueDivisionOpsBVR", "makeTable", warArgs)
local baseChance = tonumber(args.rebellionchance or args.rebellionChance or args.unrestchance or args.unrestChance) or 6
body = forceMyleciaCityNodeColumn(body, args)
local chance = missionAdjustedUnrestChance(args, loc, baseChance)
body = addCorridorColumns(body, args, "war")
chance = qhapaqSelenAdjustedUnrestChance(args, loc, chance)


table.insert(out, hr())
local year = tonumber(args.year or args.psscyear) or 0
table.insert(out, section("Council of Kings Southern Strait deployments", body, collapseSections, startCollapsed))
local day = tonumber(args.day or args.dayofyear) or 0
end


if includeHatch then
local seed = tostring(args.seed or "") ..
local hatchArgs = {
":SS-REBELLION-COL:" .. tostring(channel or "") ..
year = year,
":" .. tostring(rowNumber or 0) ..
day = day,
":" .. tostring(loc.code or "GEN") ..
legend = showLegend and "yes" or "no",
":" .. tostring(year) ..
zone = "SouthernStrait"
":" .. tostring(day)
}
 
local roll = pctFromSeed(seed, "roll") + 1


if args.captain and tostring(args.captain) ~= "" then
if roll > chance then
hatchArgs.captain = args.captain
if yn(args.showRebellionNoEvent or args.showrebellionnoevent or args.showUnrestNoEvent or args.showunrestnoevent, false) then
return "No unrest"
end
end


local ok, body = safeCall("Module:HatchMinistryDailyOps", "dailyReport", hatchArgs)
return "-"
body = forceMyleciaCityNodeColumn(body, args)
end
body = swapHatchHomeCityCurrentStation(body, args)
 
body = addCorridorColumns(body, args, "hatch")
local profile = pick(rebellionProfiles, seed, "profile")
local trigger = pick(rebellionTriggers, seed, "trigger")
local action = pick(rebellionActions, seed, "action")
local risk = pick(rebellionRisks, seed, "risk")


table.insert(out, hr())
if not profile then
table.insert(out, section("Hatch Ministry Southern Strait privateer operations", body, collapseSections, startCollapsed))
return "-"
end
end


if includeMission then
local seq = (pctFromSeed(seed, "seq") % 20) + 1
local missionaryArgs = {
local eventId
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
if year > 0 and day > 0 then
missionaryArgs.limit = args.missionarylimit
eventId = string.format("SS-RB-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
elseif args.limit and tostring(args.limit) ~= "" then
else
missionaryArgs.limit = args.limit
eventId = string.format("SS-RB-%s-%02d", loc.code or "GEN", seq)
end
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>"


local ok, 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
end


if showLegend then
-- Compliance observed / minor positive row column
local legendBody = table.concat({
 
"* '''Council of Kings Southern Strait deployments''' show only Council of Kings Division War League rows for the Southern Strait corridor.",
local complianceProfiles = {
"* '''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.",
label = "Inspection cooperation",
"* '''City/Node display''' is forced to [[File:MyleciaFlabB.jpeg|20px]] [[Mylecia]] for War League, Hatch Ministry, and Temple Bank rows.",
description = "Local carriers complied with inspection sequencing without requiring escalation.",
"* '''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.",
adversaries = { "SUYANA" }
"* '''Capped presence localities''' are [[Mirovia]], [[Saruq Thyrsa]], and [[Thal Qencha]]. 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.",
},
"* '''Qhapaq Selen 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.",
{
"* '''Qhapaq Selen chance modifiers''' add +4 percentage points to unrest-alert likelihood and +5 percentage points to compliance-observation likelihood by default.",
label = "Ledger correction",
"* '''Trade agreement''' columns generate actual trade sign-offs only in the Temple Bank section; War League and Hatch Ministry rows display '''-''' in that column.",
description = "Route clerks corrected incomplete or inconsistent paperwork before formal penalty.",
"* '''Compliance observed''' columns show minor positive local behavior below the level of a formal trade agreement.",
adversaries = { "KHADIR" }
"* '''Rebellion alert''' columns show rare row-level contained unrest events such as forged proclamations, broker-cell activity, route barricades, false orders, or Dispossessed-adjacent courier behavior.",
},
"* '''Mirovia''', '''Saruq Thyrsa''', and '''Thal Qencha''' are hard-excluded from Rebellion Alert results; their Rebellion Alert cells always display '''-''', even when '''rebellionchance=100''' is used for testing.",
{
"* '''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.",
label = "Crowd dispersal",
"* '''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.",
description = "Local stewards dispersed a gathering before it became an obstruction.",
"* '''Major Bassaridian Missions''' at Saruq Thyrsa, Mirovia, Ayllu Nysara, Thal Qencha, and Qhapaq Selen receive an additional +2 percentage points to compliance observations.",
adversaries = { "SUYANA" }
"* Use '''tradechance=100''', '''compliancechance=100''', and '''rebellionchance=100''' for testing.",
},
"* Use '''qhapaqselentarget=3''' to change the default Qhapaq Selen section target.",
{
"* Use '''qhapaqselenoverfill=yes''' to allow Qhapaq Selen to exceed its default three-row section target.",
label = "Guide assistance",
"* Use '''qhapaqselenunrestboost=4''' and '''qhapaqselencomplianceboost=5''' to change the default Qhapaq Selen chance modifiers.",
description = "Licensed guides helped separate lawful traffic from suspect courier movement.",
"* Use '''missionsupport=no''' to disable Bassaridian Missions modifiers for testing.",
adversaries = { "SUYANA" }
"* 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")
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" }
}
}


table.insert(out, hr())
local complianceOutcomes = {
table.insert(out, section("Combined legend", legendBody, collapseSections, startCollapsed))
"Minor positive compliance noted.",
end
"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."
}


return table.concat(out, "\n")
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 = qhapaqSelenAdjustedComplianceChance(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)


end
local roll = pctFromSeed(seed, "roll") + 1


-- Shortcut views
if roll > chance then
function p.southernStrait(frame)
if yn(args.showComplianceNoEvent or args.showcompliancenoevent, false) then
local args = (frame and frame.args) or {}
return "No observation"
args.title = args.title or "Southern Strait Corridor Daily Operations"
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, 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, Qhapaq Selen 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 ok, 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 ok, 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 ok, 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.",
"* '''Capped presence localities''' are [[Mirovia]], [[Saruq Thyrsa]], and [[Thal Qencha]]. 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.",
"* '''Qhapaq Selen 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.",
"* '''Qhapaq Selen 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 Qencha''' 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, Ash 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, Ayllu Nysara, Thal Qencha, and Qhapaq Selen 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 Qhapaq Selen section target.",
"* Use '''qhapaqselenoverfill=yes''' to allow Qhapaq Selen to exceed its default three-row section target.",
"* Use '''qhapaqselenunrestboost=4''' and '''qhapaqselencomplianceboost=5''' to change the default Qhapaq Selen 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
 
-- 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 })
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 })
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 })
return p.makeTable({ args = args })
end
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
return p

Revision as of 14:32, 24 June 2026

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

-- Module:SouthernStraitCorridorOps
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 = "[[Ayllu Nysara]]",
region = "Eastern foothills of the [[Snowholme Range]]",
category = "Agriculture",
company = "[[Nysaran Terrace Farmers' Ayllu]]",
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 Qencha]]",
region = "Eastern foothills of the [[Snowholme Range]]",
category = "Agriculture",
company = "[[Thal Qencha 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 = "[[Qhapaq Selen]]",
region = "Eastern foothills of the [[Snowholme Range]]",
category = "Services",
company = "[[Qhapaq Selen 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
-- MIR, SAR, and THQ are capped at one displayed unit per section/channel.
-- QSE is targeted for up to three displayed units per section/channel by default.

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 qhapaqSelenPresenceTarget(args)
local v = tonumber(
args.qhapaqselentarget or
args.qhapaqSelenTarget or
args.qseTarget or
args.qsetarget or
args.qhapagselentarget
)


if v == nil then v = 3 end
if v < 0 then v = 0 end

return math.floor(v)


end

local function qhapaqSelenPresenceInterval(args)
local v = tonumber(
args.qhapaqseleninterval or
args.qhapaqSelenInterval or
args.qseInterval or
args.qseinterval or
args.qhapagseleninterval
)


if v == nil then v = 3 end
if v < 1 then v = 1 end

return math.floor(v)


end

local function qhapaqSelenAllowOverTarget(args)
return yn(
args.qhapaqselenoverfill or
args.qhapaqSelenOverfill or
args.qseoverfill or
args.qseOverfill or
args.qhapagselenoverfill,
false
)
end

local function newLocalityAssignmentState(args, channel)
return {
channel = tostring(channel or ""),
counts = {},
qseTarget = qhapaqSelenPresenceTarget(args),
qseInterval = qhapaqSelenPresenceInterval(args),
qseAllowOverTarget = qhapaqSelenAllowOverTarget(args)
}
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 == "QSE" and state and not state.qseAllowOverTarget then
	if localityCount(state, code) >= (tonumber(state.qseTarget) or 3) then
		return false
	end
end

return true


end

local function shouldAssignQhapaqSelen(args, channel, rowNumber, state)
if not state then return false 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
end

local targetWindow = target * interval
local remainingRowsInWindow = targetWindow - row + 1
local needed = target - already

if remainingRowsInWindow <= needed then
	return true
end

return false


end

-- Row-level locality focus helper

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


state = state or newLocalityAssignmentState(args, channel)

local qse = findLocalityByCode("QSE")

if qse and shouldAssignQhapaqSelen(args, channel, rowNumber, state) and canAssignLocality(args, qse, state) then
	incrementLocalityCount(state, qse)
	return qse
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 ~= "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
-- Effective from 112/53 PSSC only.

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 and +3 percentage points to Temple Bank trade sign-offs from 112/53 PSSC onward.</small>"


end

-- 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_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

local function qhapaqSelenComplianceBoost(args)
local v = tonumber(
args.qhapaqselencomplianceboost or
args.qhapaqSelenComplianceBoost or
args.qsecomplianceboost or
args.qseComplianceBoost or
args.qhapagselencomplianceboost
)


if v == nil then v = 5 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


end

local function qhapaqSelenAdjustedComplianceChance(args, loc, baseChance)
local chance = clampChance(baseChance)


if loc and tostring(loc.code or "") == "QSE" then
	chance = chance + qhapaqSelenComplianceBoost(args)
end

return clampChance(chance)


end

local function qhapaqSelenAdjustedUnrestChance(args, loc, baseChance)
local chance = clampChance(baseChance)


if loc and tostring(loc.code or "") == "QSE" then
	chance = chance + qhapaqSelenUnrestBoost(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]], [[Ayllu Nysara]], [[Thal Qencha]], and [[Qhapaq Selen]].<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. Qhapaq Selen 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>"


end

-- 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_DAY = 121

local recurringAdversaries = {
KHADIR = {
name = "[[Khadir Veyran]]",
epithet = "Keeper of the Ash Ledger",
localities = {
QSE = true,
AYS = true,
RUK = true,
HAY = true
},
notes = {
"Ash 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 Ash Ledger are suspected of turning routine paperwork into a jurisdictional dispute."
},
complianceNotes = {
"Temple Bank auditors noted that this cooperation weakens the Ash 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 Ash 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 Ash 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 Ash 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

-- 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

-- 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

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

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 = "Ash 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 = qhapaqSelenAdjustedUnrestChance(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 = qhapaqSelenAdjustedComplianceChance(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, 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, Qhapaq Selen 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 ok, 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 ok, 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 ok, 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.",
		"* '''Capped presence localities''' are [[Mirovia]], [[Saruq Thyrsa]], and [[Thal Qencha]]. 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.",
		"* '''Qhapaq Selen 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.",
		"* '''Qhapaq Selen 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 Qencha''' 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, Ash 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, Ayllu Nysara, Thal Qencha, and Qhapaq Selen 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 Qhapaq Selen section target.",
		"* Use '''qhapaqselenoverfill=yes''' to allow Qhapaq Selen to exceed its default three-row section target.",
		"* Use '''qhapaqselenunrestboost=4''' and '''qhapaqselencomplianceboost=5''' to change the default Qhapaq Selen 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

-- 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