Module:SouthernStraitCorridorOps: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
No edit summary
NewZimiaGov (talk | contribs)
No edit summary
Tag: Manual revert
Line 1: Line 1:
-- Module:SouthernStraitCorridorOps
-- Module:SouthernStraitCorridorOps
-- Southern Strait corridor display.
-- Southern Strait-only daily operations wrapper.
-- Pulls from:
-- Shows only Southern Strait / Haifan Bassaridian corridor entries from:
--   Module:WarLeagueDivisionOpsBVR
-- 1) WarLeagueDivisionOpsBVR
--   Module:MissionaryDailyOpsBVR
-- 2) HatchMinistryDailyOps
--   Module:HatchMinistryDailyOps
-- 3) MissionaryDailyOpsBVR
--
--
-- This module intentionally overrides the displayed city/node to Mylecia
-- Adds row-level columns:
-- for all rendered War League, Temple Bank, and Hatch Ministry corridor rows.
-- 4) Locality focus
-- 5) Trade agreement
-- 6) Rebellion alert
--
-- Trade-agreement behavior:
-- Trade agreements generate ONLY under Temple Bank / missionary rows.
-- War League and Hatch Ministry rows show "-" in the Trade agreement column.
--
-- Economic design note:
-- Southern Strait and Eastern Mykonos localities are written as grounded
-- successor/satellite economies influenced by the historic port economies of
-- Blore Heath, Jogi, and Mylecia, without copying those ports directly.


local p = {}
local p = {}


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Configuration
-- Helpers
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


local SOUTHERN_STRAIT_CITY = "Mylecia"
local function yn(value, default)
local SOUTHERN_STRAIT_CITY_FLAG = "MyleciaFlabB.jpeg"
local v = tostring(value or ""):lower()
local CITY_PAGE = "List_of_cities_in_Bassaridia_Vaeringheim"
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 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


local DEFAULT_LIMIT = 24
if type(mod[funcName]) ~= "function" then
return false, "''" .. tostring(moduleName) .. " has no public function named " .. tostring(funcName) .. ".''"
end


local HAIFAN_STAGING_CITIES = {
local ok, result = pcall(function()
["Keybir-Aviv"] = true,
return mod[funcName](makeFrame(args))
["Tel-Amin"] = true,
end)
["Diamandis"] = true,
["Jogi"] = true,
["Lewisburg"] = true,
["Thermosalem"] = true,
["Akróstadium"] = true,
["Sufriya"] = true,
["Lykopolis"] = true,
["Mylecia"] = true,
}


--------------------------------------------------------------------------------
if not ok then
-- Basic helpers
return false, "''Error while invoking " .. tostring(moduleName) .. "." .. tostring(funcName) .. ": " .. tostring(result) .. "''"
--------------------------------------------------------------------------------
end


local function trim(s)
return true, tostring(result or "")
return mw.text.trim(tostring(s or ""))
end
end


local function lower(s)
local function doyToMonthDom(doy)
return mw.ustring.lower(tostring(s or ""))
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
end


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


local function flagIcon(fileName, size)
local function section(title, body, collapsible, collapsed)
size = tonumber(size) or 20
local out = {}
if not fileName or fileName == "" then
local safeTitle = tostring(title or "Operations")
return ""
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
end
return string.format("[[File:%s|%dpx]]", fileName, size)
 
local collapseClass = collapsed and "mw-collapsible mw-collapsed" or "mw-collapsible"
table.insert(out, '<div class="' .. collapseClass .. '" style="border:1px solid #a2a9b1; margin:0.6em 0; background:#f8f9fa;">')
table.insert(out, '<div style="font-weight:bold; padding:0.35em 0.6em; background:#eaecf0;">' .. safeTitle .. '</div>')
table.insert(out, '<div style="padding:0.5em 0.6em;">')
table.insert(out, content)
table.insert(out, '</div>')
table.insert(out, '</div>')
 
return table.concat(out, "\n")
end
end


local function myleciaCell(flagSize)
local function hr()
return string.format(
return "\n----\n"
"%s [[%s#Mylecia|Mylecia]]",
flagIcon(SOUTHERN_STRAIT_CITY_FLAG, flagSize),
CITY_PAGE
)
end
end


local function stripCellMarkup(s)
local function hashString(s)
s = tostring(s or "")
s = tostring(s or "")
s = s:gsub("<br%s*/?>", " ")
local h = 0
s = s:gsub("%[%[File:[^%]]+%]%]", "")
for i = 1, #s do
s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
h = (h * 31 + string.byte(s, i)) % 1000003
s = s:gsub("%[%[([^%]]+)%]%]", "%1")
end
s = s:gsub("'''", "")
return h
s = s:gsub("''", "")
return trim(s)
end
end


local function rowContainsSouthernStrait(text)
local function pick(list, seed, salt)
local t = lower(stripCellMarkup(text))
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


if t:find("southern strait", 1, true) then return true end
local function pctFromSeed(seed, salt)
if t:find("southern straits", 1, true) then return true end
return hashString(tostring(seed or "") .. ":" .. tostring(salt or "")) % 100
if t:find("former maritime markets", 1, true) then return true end
end
if t:find("maritime markets", 1, true) then return true end
if t:find("straits compliance", 1, true) then return true end
if t:find("strait compliance", 1, true) then return true end
if t:find("prize%-court") then return true end
if t:find("ferry lane") then return true end
if t:find("ferry%-lane") then return true end
if t:find("inland transfer") then return true end
if t:find("pilgrimage%-route") then return true end
if t:find("harbor%-to%-road") then return true end


for city in pairs(HAIFAN_STAGING_CITIES) do
--------------------------------------------------------------------------------
if t:find(lower(city), 1, true) then
-- Extended Southern Strait / Eastern Mykonos / Snowholme locality list
return true
--------------------------------------------------------------------------------
end
end


return false
local southernTradeCities = {
end
{
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"
}
}


local function splitWikiCells(line)
--------------------------------------------------------------------------------
line = tostring(line or "")
-- Row-level locality focus helper
line = line:gsub("^|%s*", "")
--------------------------------------------------------------------------------
local parts = mw.text.split(line, "%s*||%s*")
for i, v in ipairs(parts) do
parts[i] = trim(v)
end
return parts
end


local function firstNonEmpty(...)
local function rowLocalityData(args, channel, rowNumber)
for _, v in ipairs({ ... }) do
if not southernTradeCities or #southernTradeCities == 0 then
v = trim(v)
return nil
if v ~= "" then
return v
end
end
end
return "—"
end


local function statusStyle(statusText)
local year = tonumber(args.year or args.psscyear) or 0
local s = lower(statusText)
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)


if s:find("critical", 1, true) or s:find("red", 1, true) or s:find("closed", 1, true) or s:find("no%-go") then
local offset = hashString(seed) % #southernTradeCities
return ' style="background-color:#f8d7da;"'
local idx = ((tonumber(rowNumber) or 1) - 1 + offset) % #southernTradeCities + 1
end


if s:find("high", 1, true) or s:find("orange", 1, true) or s:find("restricted", 1, true) or s:find("limited", 1, true) then
return southernTradeCities[idx]
return ' style="background-color:#fff3cd;"'
end
end


if s:find("yellow", 1, true) or s:find("medium", 1, true) or s:find("slow", 1, true) then
local function rowLocalityText(loc)
return ' style="background-color:#fff3cd;"'
if not loc then
return "Southern Strait corridor"
end
end
 
return loc.city .. "<br><small>" .. loc.region .. "</small>"
return ' style="background-color:#d4edda;"'
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Source parsers
-- Cult sign-off list for Temple Bank trade agreements
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


local function parseWarLeagueRows(text, args)
local tradeSigningCults = {
local rows = {}
{
local flagSize = tonumber(args.flagsize) or 20
code = "UMB",
local filterRows = tostring(args.filter or "yes"):lower() ~= "no"
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 currentStyle = ""
local tradeOutcomes = {
for line in tostring(text or ""):gmatch("[^\r\n]+") do
"Cleared for General Port ledger review.",
local trimmed = trim(line)
"Provisional admission recommended.",
"Conditional trade window approved.",
"Company charter accepted for corridor listing.",
"Trade agreement filed through ordinary registry channels."
}


if trimmed:match("^|%-") then
local function tradeAgreementCell(args, channel, rowNumber, loc)
currentStyle = trimmed:gsub("^|%-", "")
-- Trade agreements belong only under Temple Bank / missionary rows.
elseif trimmed:match("^|") and not trimmed:match("^|}") then
-- War League and Hatch Ministry rows keep the column but show a dash.
local cells = splitWikiCells(trimmed)
if tostring(channel or "") ~= "missionary" then
return "-"
end


-- WarLeagueDivisionOpsBVR expected:
if not loc then
-- Date, Division, City / Node, Daily event trigger, Urgency,
return "-"
-- Operation, Ground Forces, Naval Forces, Aerial Forces, Oversight,
-- Compliance / Notes
if #cells >= 10 and lower(cells[1]) ~= "date" then
local combined = table.concat(cells, " ")
if (not filterRows) or rowContainsSouthernStrait(combined) then
local division = firstNonEmpty(cells[2])
local urgency = firstNonEmpty(cells[5])
local operation = firstNonEmpty(cells[6])
local forces = table.concat({
"'''Ground:''' " .. firstNonEmpty(cells[7]),
"'''Naval:''' " .. firstNonEmpty(cells[8]),
"'''Aerial:''' " .. firstNonEmpty(cells[9])
}, "<br>")
local notes = table.concat({
firstNonEmpty(cells[4]),
firstNonEmpty(cells[10]),
firstNonEmpty(cells[11])
}, "<br>")
 
table.insert(rows, {
style = currentStyle ~= "" and currentStyle or statusStyle(urgency .. " " .. notes),
source = "'''War League'''",
date = firstNonEmpty(cells[1]),
unit = division,
city = myleciaCell(flagSize),
status = urgency,
tasking = operation .. "<br>" .. forces,
notes = notes,
})
end
end
end
end
end


return rows
-- Low default likelihood. Use tradechance=100 for testing.
end
local chance = tonumber(args.tradechance or args.tradeChance) or 3
if chance < 0 then chance = 0 end
if chance > 100 then chance = 100 end


local function parseTempleRows(text, args)
local year = tonumber(args.year or args.psscyear) or 0
local rows = {}
local day  = tonumber(args.day or args.dayofyear) or 0
local flagSize = tonumber(args.flagsize) or 20
local seed = tostring(args.seed or "") ..
local filterRows = tostring(args.filter or "yes"):lower() ~= "no"
":SS-TEMPLE-TRADE-COL:" .. tostring(rowNumber or 0) ..
":" .. tostring(loc.code or "GEN") ..
":" .. tostring(year) ..
":" .. tostring(day)


local currentStyle = ""
local roll = pctFromSeed(seed, "roll") + 1
for line in tostring(text or ""):gmatch("[^\r\n]+") do
if roll > chance then
local trimmed = trim(line)
if yn(args.showTradeNoEvent or args.showtradenoevent, false) then
return "No sign-off"
end
return "-"
end


if trimmed:match("^|%-") then
local cult = pick(tradeSigningCults, seed, "cult")
currentStyle = trimmed:gsub("^|%-", "")
local outcome = pick(tradeOutcomes, seed, "outcome")
elseif trimmed:match("^|") and not trimmed:match("^|}") then
if not cult then
local cells = splitWikiCells(trimmed)
return "-"
end


-- MissionaryDailyOpsBVR expected:
local seq = (pctFromSeed(seed, "seq") % 20) + 1
-- Date, City, Region, Ops Alert, Mystery (Cult), Unit,
local tradeId
-- Tasking, Temple Bank Notes, optional Threat Watch, Severity
if #cells >= 9 and lower(cells[1]) ~= "date" then
local combined = table.concat(cells, " ")
if (not filterRows) or rowContainsSouthernStrait(combined) then
local severity = cells[#cells]
local threat = (#cells >= 10) and cells[#cells - 1] or ""
local bankNotes = (#cells >= 10) and cells[#cells - 2] or cells[8]


table.insert(rows, {
if year > 0 and day > 0 then
style = currentStyle ~= "" and currentStyle or statusStyle(cells[4] .. " " .. severity),
tradeId = string.format("GP-SS-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
source = "'''Temple Bank'''",
else
date = firstNonEmpty(cells[1]),
tradeId = string.format("GP-SS-%s-%02d", loc.code or "GEN", seq)
unit = firstNonEmpty(cells[5], cells[6]),
city = myleciaCell(flagSize),
status = firstNonEmpty(cells[4], severity),
tasking = "'''Unit:''' " .. firstNonEmpty(cells[6]) .. "<br>" .. firstNonEmpty(cells[7]),
notes = firstNonEmpty(bankNotes) .. (threat ~= "" and ("<br>'''Threat Watch:''' " .. threat) or ""),
})
end
end
end
end
end


return rows
local signCode = string.format("%s-TA%02d", cult.code or "CUL", seq)
 
return "'''Trade sign-off'''<br>" ..
"<small>" .. tradeId .. "</small><br>" ..
(cult.label or "Cult sign-off recorded") .. "<br>" ..
"<small>Code: '''" .. signCode .. "'''<br>" .. outcome .. "</small>"
end
end


local function parseHatchRows(text, args)
--------------------------------------------------------------------------------
local rows = {}
-- Rebellion / corridor unrest row column
local flagSize = tonumber(args.flagsize) or 20
--------------------------------------------------------------------------------
local filterRows = tostring(args.filter or "yes"):lower() ~= "no"


local currentStyle = ""
local rebellionProfiles = {
for line in tostring(text or ""):gmatch("[^\r\n]+") do
{
local trimmed = trim(line)
label = "Confederacy-adjacent courier screen",
description = "False-route couriers testing inspection gaps."
},
{
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."
},
{
label = "False-proclamation circle",
description = "Pamphlet readers circulating forged civic orders."
},
{
label = "Route-barricade committee",
description = "Agitators obstructing lawful inspection lanes."
},
{
label = "Dispossessed-sympathetic quay cell",
description = "Waterfront cell copying Confederacy-style courier habits."
}
}


if trimmed:match("^|%-") then
local rebellionTriggers = {
currentStyle = trimmed:gsub("^|%-", "")
"Forged levy notice.",
elseif trimmed:match("^|") and not trimmed:match("^|}") then
"Masked speakers redirected traffic.",
local cells = splitWikiCells(trimmed)
"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."
}


-- HatchMinistryDailyOps expected:
local rebellionActions = {
-- Privateer Flag, Captain, Home City, Current Station,
"CKD cordon and evidence hold.",
-- Ops Alert, Rotation, Today's Activity
"Temple rumor-control desk opened.",
if #cells >= 7 and lower(cells[2]) ~= "captain" then
"Ledgers and copied seals sealed.",
local combined = table.concat(cells, " ")
"Privateer stand-off posture.",
if (not filterRows) or rowContainsSouthernStrait(combined) then
"Pass papers reissued.",
table.insert(rows, {
"Lawful cargo separated.",
style = currentStyle ~= "" and currentStyle or statusStyle(cells[5] .. " " .. cells[7]),
"Missionary mediation opened.",
source = "'''Hatch Ministry'''",
"Corrected signal notice issued.",
date = firstNonEmpty(cells[6]),
"Temporary General Port audit lane opened.",
unit = firstNonEmpty(cells[2]),
"Incident classified as contained unrest."
city = myleciaCell(flagSize),
}
status = firstNonEmpty(cells[5]),
tasking = firstNonEmpty(cells[7]),
notes = "'''Home city:''' " .. firstNonEmpty(cells[3]) .. "<br>'''Current station:''' " .. firstNonEmpty(cells[4]),
})
end
end
end
end


return rows
local rebellionRisks = {
end
"Low",
"Moderate",
"High",
"Contained",
"Escalating"
}


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Source loaders
-- Compliance observed / minor positive row column
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


local function getWarLeagueRows(args)
local complianceProfiles = {
local wl = safeRequire("Module:WarLeagueDivisionOpsBVR")
{
if not wl or type(wl.makeTable) ~= "function" then
label = "Inspection cooperation",
return {}
description = "Local carriers complied with inspection sequencing without requiring escalation."
},
{
label = "Ledger correction",
description = "Route clerks corrected incomplete or inconsistent paperwork before formal penalty."
},
{
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
end


local year = tostring(args.year or "")
-- Minor positive events should be more common than full trade agreements,
local day = tostring(args.day or "")
-- but still uncommon enough to remain meaningful.
local seed = tostring(args.seed or "")
local chance = tonumber(args.compliancechance or args.complianceChance or args.observedchance or args.observedChance) or 8
if chance < 0 then chance = 0 end
if chance > 100 then chance = 100 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-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 callArgs = {
local profile = pick(complianceProfiles, seed, "profile")
rows = tostring(args.warRows or args.rows or "48"),
local outcome = pick(complianceOutcomes, seed, "outcome")
showlegend = "no",
}


if seed ~= "" then
if not profile then
callArgs.seed = seed
return "-"
elseif year ~= "" and day ~= "" then
callArgs.seed = tostring((tonumber(year) or 0) * 1000 + (tonumber(day) or 0))
end
end


local ok, result = pcall(wl.makeTable, { args = callArgs })
local seq = (pctFromSeed(seed, "seq") % 20) + 1
if not ok then
local eventId
return {}
 
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
end


return parseWarLeagueRows(result, args)
return "'''Compliance observed'''<br>" ..
"<small>" .. eventId .. "</small><br>" ..
"'''Profile:''' " .. profile.label .. "<br>" ..
"<small>" .. profile.description .. "<br>" ..
"'''Outcome:''' " .. outcome .. "</small>"
end
end


local function getTempleRows(args)
local function rebellionAlertCell(args, channel, rowNumber, loc)
local miss = safeRequire("Module:MissionaryDailyOpsBVR")
if not loc then
if not miss or type(miss.makeTable) ~= "function" then
return "-"
return {}
end
end


local callArgs = {
local chance = tonumber(args.rebellionchance or args.rebellionChance or args.unrestchance or args.unrestChance) or 6
region = tostring(args.templeRegion or args.region or "Haifan Bassaridia"),
if chance < 0 then chance = 0 end
includeThreat = tostring(args.includeThreat or "no"),
if chance > 100 then chance = 100 end
showEcon = "no",
showlegend = "no",
limit = tostring(args.templeLimit or "80"),
}


if args.year then callArgs.year = tostring(args.year) end
local year = tonumber(args.year or args.psscyear) or 0
if args.day then callArgs.day = tostring(args.day) end
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 ok, result = pcall(miss.makeTable, { args = callArgs })
local roll = pctFromSeed(seed, "roll") + 1
if not ok then
if roll > chance then
return {}
if yn(args.showRebellionNoEvent or args.showrebellionnoevent or args.showUnrestNoEvent or args.showunrestnoevent, false) then
return "No unrest"
end
return "-"
end
end


return parseTempleRows(result, args)
local profile = pick(rebellionProfiles, seed, "profile")
end
local trigger = pick(rebellionTriggers, seed, "trigger")
local action = pick(rebellionActions, seed, "action")
local risk = pick(rebellionRisks, seed, "risk")


local function getHatchRows(args)
if not profile then
local hatch = safeRequire("Module:HatchMinistryDailyOps")
return "-"
if not hatch or type(hatch.dailyReport) ~= "function" then
return {}
end
end


local callArgs = {
local seq = (pctFromSeed(seed, "seq") % 20) + 1
legend = "no",
local eventId
}


if args.year then callArgs.year = tostring(args.year) end
if year > 0 and day > 0 then
if args.day then callArgs.day = tostring(args.day) end
eventId = string.format("SS-RB-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
if args.captain then callArgs.captain = tostring(args.captain) end
else
 
eventId = string.format("SS-RB-%s-%02d", loc.code or "GEN", seq)
local ok, result = pcall(hatch.dailyReport, { args = callArgs })
if not ok then
return {}
end
end


return parseHatchRows(result, args)
return "'''Unrest alert'''<br>" ..
"<small>" .. eventId .. "</small><br>" ..
"'''Profile:''' " .. profile.label .. "<br>" ..
"<small>" .. profile.description .. "<br>" ..
"'''Trigger:''' " .. trigger .. "<br>" ..
"'''Risk:''' " .. risk .. "<br>" ..
"'''Response:''' " .. action .. "</small>"
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Rendering
-- Row-column insertion helper
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


local function addRows(target, sourceRows, limit)
local function addCorridorColumns(body, args, channel)
for _, row in ipairs(sourceRows) do
body = tostring(body or "")
if #target >= limit then
 
return
if not body:find("{|", 1, true) then
end
return body
table.insert(target, row)
end
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
end


local function renderTable(rows)
local lines = mw.text.split(body, "\n", true)
local out = {}
local out = {}


table.insert(out, "{| class='wikitable sortable' style='width:100%;'")
local inTable = false
table.insert(out, "! Source !! Date / Rotation !! Unit !! City !! Status !! Tasking !! Notes")
local headerInserted = false
local rowNumber = 0
 
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)
 
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)


if #rows == 0 then
else
table.insert(out, "|-")
table.insert(out, line)
table.insert(out, "| colspan='7' style='text-align:center;' | ''No Southern Strait corridor rows were returned by the source modules for this invocation.''")
else
for _, row in ipairs(rows) do
table.insert(out, "|-" .. (row.style or ""))
table.insert(out, string.format(
"| %s || %s || %s || %s || %s || %s || %s",
firstNonEmpty(row.source),
firstNonEmpty(row.date),
firstNonEmpty(row.unit),
firstNonEmpty(row.city),
firstNonEmpty(row.status),
firstNonEmpty(row.tasking),
firstNonEmpty(row.notes)
))
end
end
end
end


table.insert(out, "|}")
return table.concat(out, "\n")
return table.concat(out, "\n")
end
local function renderLegend()
return table.concat({
"<div style='margin-top:0.5em; font-size:90%;'>",
"<b>Display note:</b> Southern Strait corridor rows are shown through ",
myleciaCell(20),
". Source-module tasking, Op-IDs, captains, units, and compliance notes are preserved; only the displayed corridor city is normalized for this page.",
"</div>"
}, "")
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Public entry points
-- Main Southern Strait-only view
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


function p.makeTable(frame)
function p.makeTable(frame)
local args = (frame and frame.args) or {}
local args = (frame and frame.args) or {}
local limit = tonumber(args.limit) or DEFAULT_LIMIT
local showLegend = tostring(args.showlegend or args.legend or "yes"):lower() ~= "no"
local showHeader = tostring(args.header or "no"):lower() == "yes"


local rows = {}
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 includeWar = tostring(args.war or "yes"):lower() ~= "no"
local collapseSections = yn(args.collapsible, true)
local includeTemple = tostring(args.temple or "yes"):lower() ~= "no"
local startCollapsed = yn(args.collapsed, true)
local includeHatch = tostring(args.hatch or "yes"):lower() ~= "no"
 
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 Haifan Bassaridian / former Maritime Markets 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, and row-level rebellion-type unrest alerts while excluding unrelated regional rows.")
end
 
----------------------------------------------------------------------------
-- War League: Southern Strait-only
----------------------------------------------------------------------------


if includeWar then
if includeWar then
addRows(rows, getWarLeagueRows(args), limit)
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",
 
-- Southern Strait Corridor wrapper shows only Council of Kings
-- Division rows for War League deployments.
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 = addCorridorColumns(body, args, "war")
 
table.insert(out, hr())
table.insert(out, section("Council of Kings Southern Strait deployments", body, collapseSections, startCollapsed))
end
end


if includeTemple then
----------------------------------------------------------------------------
addRows(rows, getTempleRows(args), limit)
-- Hatch Ministry: Southern Strait-only
end
----------------------------------------------------------------------------


if includeHatch then
if includeHatch then
addRows(rows, getHatchRows(args), limit)
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 = addCorridorColumns(body, args, "hatch")
 
table.insert(out, hr())
table.insert(out, section("Hatch Ministry Southern Strait privateer operations", body, collapseSections, startCollapsed))
end
end


local out = {}
----------------------------------------------------------------------------
-- Temple Bank / Missionary Operations: Haifan only
----------------------------------------------------------------------------
 
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 = addCorridorColumns(body, args, "missionary")


if showHeader then
table.insert(out, hr())
table.insert(out, "'''Southern Strait Corridor Daily Operations'''")
table.insert(out, section("Temple Bank Southern Strait missionary operations", body, collapseSections, startCollapsed))
table.insert(out, "")
end
end


table.insert(out, renderTable(rows))
----------------------------------------------------------------------------
-- Combined legend
----------------------------------------------------------------------------


if showLegend then
if showLegend then
table.insert(out, renderLegend())
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 Haifan home-port privateers only.",
"* '''Temple Bank Southern Strait missionary operations''' show Haifan Bassaridian missionary rows only.",
"* '''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.",
"* '''Trade agreement''' columns generate actual trade sign-offs only in the Temple Bank section; War League and Hatch Ministry rows display '''-''' in that column.",
"* '''Trade sign-off''' is the key phrase indicating that a Temple Bank row generated a cult-backed trade agreement.",
"* '''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.",
"* Use '''tradechance=100''' and '''rebellionchance=100''' for testing; return to low values such as '''tradechance=3''' and '''rebellionchance=6''' for ordinary use."
}, "\n")
 
table.insert(out, hr())
table.insert(out, section("Combined legend", legendBody, collapseSections, startCollapsed))
end
end


Line 457: Line 785:
end
end


function p.dailyReport(frame)
--------------------------------------------------------------------------------
return p.makeTable(frame)
-- Shortcut views
end
--------------------------------------------------------------------------------


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


function p.cityCell(frame)
function p.warOnly(frame)
local args = (frame and frame.args) or {}
local args = (frame and frame.args) or {}
return myleciaCell(args.flagsize or 20)
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
end


 
function p.hatchOnly(frame)
function p.dailyReport(frame)
local args = (frame and frame.args) or {}
return p.makeTable(frame)
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
end


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


return p
return p

Revision as of 19:55, 5 June 2026

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


-- Module:SouthernStraitCorridorOps
-- Southern Strait-only daily operations wrapper.
-- Shows only Southern Strait / Haifan Bassaridian corridor entries from:
-- 1) WarLeagueDivisionOpsBVR
-- 2) HatchMinistryDailyOps
-- 3) MissionaryDailyOpsBVR
--
-- Adds row-level columns:
-- 4) Locality focus
-- 5) Trade agreement
-- 6) Rebellion alert
--
-- Trade-agreement behavior:
-- Trade agreements generate ONLY under Temple Bank / missionary rows.
-- War League and Hatch Ministry rows show "-" in the Trade agreement column.
--
-- Economic design note:
-- Southern Strait and Eastern Mykonos localities are written as grounded
-- successor/satellite economies influenced by the historic port economies of
-- Blore Heath, Jogi, and Mylecia, without copying those ports directly.

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

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

--------------------------------------------------------------------------------
-- Row-level locality focus helper
--------------------------------------------------------------------------------

local function rowLocalityData(args, channel, rowNumber)
	if not southernTradeCities or #southernTradeCities == 0 then
		return nil
	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 idx = ((tonumber(rowNumber) or 1) - 1 + offset) % #southernTradeCities + 1

	return southernTradeCities[idx]
end

local function rowLocalityText(loc)
	if not loc then
		return "Southern Strait corridor"
	end
	return loc.city .. "<br><small>" .. loc.region .. "</small>"
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)
	-- Trade agreements belong only under Temple Bank / missionary rows.
	-- War League and Hatch Ministry rows keep the column but show a dash.
	if tostring(channel or "") ~= "missionary" then
		return "-"
	end

	if not loc then
		return "-"
	end

	-- Low default likelihood. Use tradechance=100 for testing.
	local chance = tonumber(args.tradechance or args.tradeChance) or 3
	if chance < 0 then chance = 0 end
	if chance > 100 then chance = 100 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-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)

	return "'''Trade sign-off'''<br>" ..
		"<small>" .. tradeId .. "</small><br>" ..
		(cult.label or "Cult sign-off recorded") .. "<br>" ..
		"<small>Code: '''" .. signCode .. "'''<br>" .. outcome .. "</small>"
end

--------------------------------------------------------------------------------
-- Rebellion / corridor unrest row column
--------------------------------------------------------------------------------

local rebellionProfiles = {
	{
		label = "Confederacy-adjacent courier screen",
		description = "False-route couriers testing inspection gaps."
	},
	{
		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."
	},
	{
		label = "False-proclamation circle",
		description = "Pamphlet readers circulating forged civic orders."
	},
	{
		label = "Route-barricade committee",
		description = "Agitators obstructing lawful inspection lanes."
	},
	{
		label = "Dispossessed-sympathetic quay cell",
		description = "Waterfront cell copying Confederacy-style courier habits."
	}
}

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."
}

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."
}

local rebellionRisks = {
	"Low",
	"Moderate",
	"High",
	"Contained",
	"Escalating"
}

--------------------------------------------------------------------------------
-- Compliance observed / minor positive row column
--------------------------------------------------------------------------------

local complianceProfiles = {
	{
		label = "Inspection cooperation",
		description = "Local carriers complied with inspection sequencing without requiring escalation."
	},
	{
		label = "Ledger correction",
		description = "Route clerks corrected incomplete or inconsistent paperwork before formal penalty."
	},
	{
		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

	-- Minor positive events should be more common than full trade agreements,
	-- but still uncommon enough to remain meaningful.
	local chance = tonumber(args.compliancechance or args.complianceChance or args.observedchance or args.observedChance) or 8
	if chance < 0 then chance = 0 end
	if chance > 100 then chance = 100 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-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

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

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

	local chance = tonumber(args.rebellionchance or args.rebellionChance or args.unrestchance or args.unrestChance) or 6
	if chance < 0 then chance = 0 end
	if chance > 100 then chance = 100 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-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

	return "'''Unrest alert'''<br>" ..
		"<small>" .. eventId .. "</small><br>" ..
		"'''Profile:''' " .. profile.label .. "<br>" ..
		"<small>" .. profile.description .. "<br>" ..
		"'''Trigger:''' " .. trigger .. "<br>" ..
		"'''Risk:''' " .. risk .. "<br>" ..
		"'''Response:''' " .. action .. "</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

	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)

			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 = (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 Haifan Bassaridian / former Maritime Markets 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, and row-level rebellion-type unrest alerts while excluding unrelated regional rows.")
	end

	----------------------------------------------------------------------------
	-- War League: Southern Strait-only
	----------------------------------------------------------------------------

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

			-- Southern Strait Corridor wrapper shows only Council of Kings
			-- Division rows for War League deployments.
			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 = addCorridorColumns(body, args, "war")

		table.insert(out, hr())
		table.insert(out, section("Council of Kings Southern Strait deployments", body, collapseSections, startCollapsed))
	end

	----------------------------------------------------------------------------
	-- Hatch Ministry: Southern Strait-only
	----------------------------------------------------------------------------

	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 = addCorridorColumns(body, args, "hatch")

		table.insert(out, hr())
		table.insert(out, section("Hatch Ministry Southern Strait privateer operations", body, collapseSections, startCollapsed))
	end

	----------------------------------------------------------------------------
	-- Temple Bank / Missionary Operations: Haifan only
	----------------------------------------------------------------------------

	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 = addCorridorColumns(body, args, "missionary")

		table.insert(out, hr())
		table.insert(out, section("Temple Bank Southern Strait missionary operations", body, collapseSections, startCollapsed))
	end

	----------------------------------------------------------------------------
	-- Combined legend
	----------------------------------------------------------------------------

	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 Haifan home-port privateers only.",
			"* '''Temple Bank Southern Strait missionary operations''' show Haifan Bassaridian missionary rows only.",
			"* '''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.",
			"* '''Trade agreement''' columns generate actual trade sign-offs only in the Temple Bank section; War League and Hatch Ministry rows display '''-''' in that column.",
			"* '''Trade sign-off''' is the key phrase indicating that a Temple Bank row generated a cult-backed trade agreement.",
			"* '''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.",
			"* Use '''tradechance=100''' and '''rebellionchance=100''' for testing; return to low values such as '''tradechance=3''' and '''rebellionchance=6''' for ordinary use."
		}, "\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