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 1: Line 1:
-- Module:SouthernStraitCorridorOps
-- Module:SouthernStraitCorridorOps
-- Southern Strait-only daily operations wrapper.
-- Southern Strait corridor display.
-- Shows only Southern Strait / Haifan Bassaridian corridor entries from:
-- Pulls from:
-- 1) WarLeagueDivisionOpsBVR
--   Module:WarLeagueDivisionOpsBVR
-- 2) HatchMinistryDailyOps
--   Module:MissionaryDailyOpsBVR
-- 3) MissionaryDailyOpsBVR
--   Module:HatchMinistryDailyOps
--
--
-- Adds row-level columns:
-- This module intentionally overrides the displayed city/node to Mylecia
-- 4) Locality focus
-- for all rendered War League, Temple Bank, and Hatch Ministry corridor rows.
-- 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 = {}


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Helpers
-- Configuration
--------------------------------------------------------------------------------
 
local SOUTHERN_STRAIT_CITY = "Mylecia"
local SOUTHERN_STRAIT_CITY_FLAG = "MyleciaFlabB.jpeg"
local CITY_PAGE = "List_of_cities_in_Bassaridia_Vaeringheim"
 
local DEFAULT_LIMIT = 24
 
local HAIFAN_STAGING_CITIES = {
["Keybir-Aviv"] = true,
["Tel-Amin"] = true,
["Diamandis"] = true,
["Jogi"] = true,
["Lewisburg"] = true,
["Thermosalem"] = true,
["Akróstadium"] = true,
["Sufriya"] = true,
["Lykopolis"] = true,
["Mylecia"] = true,
}
 
--------------------------------------------------------------------------------
-- Basic helpers
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


local function yn(value, default)
local function trim(s)
local v = tostring(value or ""):lower()
return mw.text.trim(tostring(s or ""))
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
end


local function makeFrame(args)
local function lower(s)
return { args = args or {} }
return mw.ustring.lower(tostring(s or ""))
end
end


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


local function safeCall(moduleName, funcName, args)
local function flagIcon(fileName, size)
local mod, err = safeRequire(moduleName)
size = tonumber(size) or 20
if not mod then
if not fileName or fileName == "" then
return false, "''" .. err .. ".''"
return ""
end
end
return string.format("[[File:%s|%dpx]]", fileName, size)
end
local function myleciaCell(flagSize)
return string.format(
"%s [[%s#Mylecia|Mylecia]]",
flagIcon(SOUTHERN_STRAIT_CITY_FLAG, flagSize),
CITY_PAGE
)
end
local function stripCellMarkup(s)
s = tostring(s or "")
s = s:gsub("<br%s*/?>", " ")
s = s:gsub("%[%[File:[^%]]+%]%]", "")
s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
s = s:gsub("%[%[([^%]]+)%]%]", "%1")
s = s:gsub("'''", "")
s = s:gsub("''", "")
return trim(s)
end


if type(mod[funcName]) ~= "function" then
local function rowContainsSouthernStrait(text)
return false, "''" .. tostring(moduleName) .. " has no public function named " .. tostring(funcName) .. ".''"
local t = lower(stripCellMarkup(text))
end


local ok, result = pcall(function()
if t:find("southern strait", 1, true) then return true end
return mod[funcName](makeFrame(args))
if t:find("southern straits", 1, true) then return true end
end)
if t:find("former maritime markets", 1, true) then return true 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


if not ok then
for city in pairs(HAIFAN_STAGING_CITIES) do
return false, "''Error while invoking " .. tostring(moduleName) .. "." .. tostring(funcName) .. ": " .. tostring(result) .. "''"
if t:find(lower(city), 1, true) then
return true
end
end
end


return true, tostring(result or "")
return false
end
end


local function doyToMonthDom(doy)
local function splitWikiCells(line)
doy = tonumber(doy) or 1
line = tostring(line or "")
if doy < 1 then doy = 1 end
line = line:gsub("^|%s*", "")
if doy > 183 then doy = 183 end
local parts = mw.text.split(line, "%s*||%s*")
 
for i, v in ipairs(parts) do
if doy <= 61 then
parts[i] = trim(v)
return 1, doy
elseif doy <= 122 then
return 2, doy - 61
else
return 3, doy - 122
end
end
return parts
end
end


local function dateLabel(args)
local function firstNonEmpty(...)
local y = tonumber(args.year or args.psscyear)
for _, v in ipairs({ ... }) do
local d = tonumber(args.day or args.dayofyear)
v = trim(v)
 
if v ~= "" then
if y and d then
return v
local m, dom = doyToMonthDom(d)
end
return string.format("%d/%d/%d PSSC", dom, m, y)
end
end
 
return "—"
return "current PSSC date"
end
end


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


if not collapsible then
if s:find("critical", 1, true) or s:find("red", 1, true) or s:find("closed", 1, true) or s:find("no%-go") then
table.insert(out, "===" .. safeTitle .. "===")
return ' style="background-color:#f8d7da;"'
table.insert(out, content)
return table.concat(out, "\n")
end
end


local collapseClass = collapsed and "mw-collapsible mw-collapsed" or "mw-collapsible"
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
table.insert(out, '<div class="' .. collapseClass .. '" style="border:1px solid #a2a9b1; margin:0.6em 0; background:#f8f9fa;">')
return ' style="background-color:#fff3cd;"'
table.insert(out, '<div style="font-weight:bold; padding:0.35em 0.6em; background:#eaecf0;">' .. safeTitle .. '</div>')
end
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")
if s:find("yellow", 1, true) or s:find("medium", 1, true) or s:find("slow", 1, true) then
end
return ' style="background-color:#fff3cd;"'
 
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
end
return h
end


local function pick(list, seed, salt)
return ' style="background-color:#d4edda;"'
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
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Extended Southern Strait / Eastern Mykonos / Snowholme locality list
-- Source parsers
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


local southernTradeCities = {
local function parseWarLeagueRows(text, args)
{
local rows = {}
code = "MIR",
local flagSize = tonumber(args.flagsize) or 20
city = "[[Mirovia]]",
local filterRows = tostring(args.filter or "yes"):lower() ~= "no"
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 currentStyle = ""
-- Row-level locality focus helper
for line in tostring(text or ""):gmatch("[^\r\n]+") do
--------------------------------------------------------------------------------
local trimmed = trim(line)


local function rowLocalityData(args, channel, rowNumber)
if trimmed:match("^|%-") then
if not southernTradeCities or #southernTradeCities == 0 then
currentStyle = trimmed:gsub("^|%-", "")
return nil
elseif trimmed:match("^|") and not trimmed:match("^|}") then
end
local cells = splitWikiCells(trimmed)


local year = tonumber(args.year or args.psscyear) or 0
-- WarLeagueDivisionOpsBVR expected:
local day  = tonumber(args.day or args.dayofyear) or 0
-- Date, Division, City / Node, Daily event trigger, Urgency,
local seed = tostring(args.seed or "") .. ":SS-ROW-LOCALITY:" .. tostring(channel or "") .. ":" .. tostring(year) .. ":" .. tostring(day)
-- 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>")


local offset = hashString(seed) % #southernTradeCities
table.insert(rows, {
local idx = ((tonumber(rowNumber) or 1) - 1 + offset) % #southernTradeCities + 1
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


return southernTradeCities[idx]
return rows
end
end


local function rowLocalityText(loc)
local function parseTempleRows(text, args)
if not loc then
local rows = {}
return "Southern Strait corridor"
local flagSize = tonumber(args.flagsize) or 20
end
local filterRows = tostring(args.filter or "yes"):lower() ~= "no"
return loc.city .. "<br><small>" .. loc.region .. "</small>"
end


--------------------------------------------------------------------------------
local currentStyle = ""
-- Cult sign-off list for Temple Bank trade agreements
for line in tostring(text or ""):gmatch("[^\r\n]+") do
--------------------------------------------------------------------------------
local trimmed = trim(line)


local tradeSigningCults = {
if trimmed:match("^|%-") then
{
currentStyle = trimmed:gsub("^|%-", "")
code = "UMB",
elseif trimmed:match("^|") and not trimmed:match("^|}") then
label = "[[Noctis (Reformed Stripping Path)#Order of the Umbral Oracle|Order of the Umbral Oracle]]"
local cells = splitWikiCells(trimmed)
},
{
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 = {
-- MissionaryDailyOpsBVR expected:
"Cleared for General Port ledger review.",
-- Date, City, Region, Ops Alert, Mystery (Cult), Unit,
"Provisional admission recommended.",
-- Tasking, Temple Bank Notes, optional Threat Watch, Severity
"Conditional trade window approved.",
if #cells >= 9 and lower(cells[1]) ~= "date" then
"Company charter accepted for corridor listing.",
local combined = table.concat(cells, " ")
"Trade agreement filed through ordinary registry channels."
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]


local function tradeAgreementCell(args, channel, rowNumber, loc)
table.insert(rows, {
-- Trade agreements belong only under Temple Bank / missionary rows.
style = currentStyle ~= "" and currentStyle or statusStyle(cells[4] .. " " .. severity),
-- War League and Hatch Ministry rows keep the column but show a dash.
source = "'''Temple Bank'''",
if tostring(channel or "") ~= "missionary" then
date = firstNonEmpty(cells[1]),
return "-"
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


if not loc then
return rows
return "-"
end
end
 
local function parseHatchRows(text, args)
local rows = {}
local flagSize = tonumber(args.flagsize) or 20
local filterRows = tostring(args.filter or "yes"):lower() ~= "no"


-- Low default likelihood. Use tradechance=100 for testing.
local currentStyle = ""
local chance = tonumber(args.tradechance or args.tradeChance) or 3
for line in tostring(text or ""):gmatch("[^\r\n]+") do
if chance < 0 then chance = 0 end
local trimmed = trim(line)
if chance > 100 then chance = 100 end


local year = tonumber(args.year or args.psscyear) or 0
if trimmed:match("^|%-") then
local day  = tonumber(args.day or args.dayofyear) or 0
currentStyle = trimmed:gsub("^|%-", "")
local seed = tostring(args.seed or "") ..
elseif trimmed:match("^|") and not trimmed:match("^|}") then
":SS-TEMPLE-TRADE-COL:" .. tostring(rowNumber or 0) ..
local cells = splitWikiCells(trimmed)
":" .. tostring(loc.code or "GEN") ..
":" .. tostring(year) ..
":" .. tostring(day)


local roll = pctFromSeed(seed, "roll") + 1
-- HatchMinistryDailyOps expected:
if roll > chance then
-- Privateer Flag, Captain, Home City, Current Station,
if yn(args.showTradeNoEvent or args.showtradenoevent, false) then
-- Ops Alert, Rotation, Today's Activity
return "No sign-off"
if #cells >= 7 and lower(cells[2]) ~= "captain" then
local combined = table.concat(cells, " ")
if (not filterRows) or rowContainsSouthernStrait(combined) then
table.insert(rows, {
style = currentStyle ~= "" and currentStyle or statusStyle(cells[5] .. " " .. cells[7]),
source = "'''Hatch Ministry'''",
date = firstNonEmpty(cells[6]),
unit = firstNonEmpty(cells[2]),
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 "-"
end
local cult = pick(tradeSigningCults, seed, "cult")
local outcome = pick(tradeOutcomes, seed, "outcome")
if not cult then
return "-"
end
end


local seq = (pctFromSeed(seed, "seq") % 20) + 1
return rows
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
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Rebellion / corridor unrest row column
-- Source loaders
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


local rebellionProfiles = {
local function getWarLeagueRows(args)
{
local wl = safeRequire("Module:WarLeagueDivisionOpsBVR")
label = "Confederacy-adjacent courier screen",
if not wl or type(wl.makeTable) ~= "function" then
description = "False-route couriers testing inspection gaps."
return {}
},
end
{
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 = {
local year = tostring(args.year or "")
"Forged levy notice.",
local day = tostring(args.day or "")
"Masked speakers redirected traffic.",
local seed = tostring(args.seed or "")
"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 = {
local callArgs = {
"CKD cordon and evidence hold.",
rows = tostring(args.warRows or args.rows or "48"),
"Temple rumor-control desk opened.",
showlegend = "no",
"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 = {
if seed ~= "" then
"Minor positive compliance noted.",
callArgs.seed = seed
"Recorded as a stabilizing local action.",
elseif year ~= "" and day ~= "" then
"Accepted as offsetting evidence against wider noncompliance.",
callArgs.seed = tostring((tonumber(year) or 0) * 1000 + (tonumber(day) or 0))
"Filed as a documented good-faith response.",
end
"Preserved for future compliance comparison.",
"Marked as below trade-agreement threshold but operationally helpful."
}


local function complianceObservedCell(args, channel, rowNumber, loc)
local ok, result = pcall(wl.makeTable, { args = callArgs })
if not loc then
if not ok then
return "-"
return {}
end
end


-- Minor positive events should be more common than full trade agreements,
return parseWarLeagueRows(result, args)
-- but still uncommon enough to remain meaningful.
end
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 function getTempleRows(args)
local day  = tonumber(args.day or args.dayofyear) or 0
local miss = safeRequire("Module:MissionaryDailyOpsBVR")
local seed = tostring(args.seed or "") ..
if not miss or type(miss.makeTable) ~= "function" then
":SS-COMPLIANCE-COL:" .. tostring(channel or "") ..
return {}
":" .. 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
end


local profile = pick(complianceProfiles, seed, "profile")
local callArgs = {
local outcome = pick(complianceOutcomes, seed, "outcome")
region = tostring(args.templeRegion or args.region or "Haifan Bassaridia"),
includeThreat = tostring(args.includeThreat or "no"),
showEcon = "no",
showlegend = "no",
limit = tostring(args.templeLimit or "80"),
}


if not profile then
if args.year then callArgs.year = tostring(args.year) end
return "-"
if args.day then callArgs.day = tostring(args.day) end
end


local seq = (pctFromSeed(seed, "seq") % 20) + 1
local ok, result = pcall(miss.makeTable, { args = callArgs })
local eventId
if not ok then
 
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 "'''Compliance observed'''<br>" ..
return parseTempleRows(result, args)
"<small>" .. eventId .. "</small><br>" ..
"'''Profile:''' " .. profile.label .. "<br>" ..
"<small>" .. profile.description .. "<br>" ..
"'''Outcome:''' " .. outcome .. "</small>"
end
end


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


local chance = tonumber(args.rebellionchance or args.rebellionChance or args.unrestchance or args.unrestChance) or 6
local callArgs = {
if chance < 0 then chance = 0 end
legend = "no",
if chance > 100 then chance = 100 end
}


local year = tonumber(args.year or args.psscyear) or 0
if args.year then callArgs.year = tostring(args.year) end
local day  = tonumber(args.day or args.dayofyear) or 0
if args.day then callArgs.day = tostring(args.day) end
local seed = tostring(args.seed or "") ..
if args.captain then callArgs.captain = tostring(args.captain) end
":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
local ok, result = pcall(hatch.dailyReport, { args = callArgs })
if roll > chance then
if not ok then
if yn(args.showRebellionNoEvent or args.showrebellionnoevent or args.showUnrestNoEvent or args.showunrestnoevent, false) then
return {}
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
end


local seq = (pctFromSeed(seed, "seq") % 20) + 1
return parseHatchRows(result, args)
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
end


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


local function addCorridorColumns(body, args, channel)
local function addRows(target, sourceRows, limit)
body = tostring(body or "")
for _, row in ipairs(sourceRows) do
 
if #target >= limit then
if not body:find("{|", 1, true) then
return
return body
end
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 lines = mw.text.split(body, "\n", true)
local function renderTable(rows)
local out = {}
local out = {}


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


for _, line in ipairs(lines) do
if #rows == 0 then
if line:match("^%s*{|") then
table.insert(out, "|-")
inTable = true
table.insert(out, "| colspan='7' style='text-align:center;' | ''No Southern Strait corridor rows were returned by the source modules for this invocation.''")
headerInserted = false
else
table.insert(out, line)
for _, row in ipairs(rows) do
 
table.insert(out, "|-" .. (row.style or ""))
elseif inTable and not headerInserted and line:match("^%s*!") then
table.insert(out, string.format(
if showLocalityColumn then
"| %s || %s || %s || %s || %s || %s || %s",
table.insert(out, "! Locality focus")
firstNonEmpty(row.source),
end
firstNonEmpty(row.date),
if showTradeColumn then
firstNonEmpty(row.unit),
table.insert(out, "! Trade agreement")
firstNonEmpty(row.city),
end
firstNonEmpty(row.status),
if showComplianceColumn then
firstNonEmpty(row.tasking),
table.insert(out, "! Compliance observed")
firstNonEmpty(row.notes)
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
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


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


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 showIntro  = yn(args.intro, false)
local rows = {}
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 includeWar = tostring(args.war or "yes"):lower() ~= "no"
local startCollapsed = yn(args.collapsed, true)
local includeTemple = tostring(args.temple or "yes"):lower() ~= "no"
local includeHatch = tostring(args.hatch or "yes"):lower() ~= "no"


local includeWar     = yn(args.war or args.warleague, true)
if includeWar then
local includeHatch  = yn(args.hatch or args.hatchministry, true)
addRows(rows, getWarLeagueRows(args), limit)
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
end


if showIntro then
if includeTemple then
table.insert(out, string.format(
addRows(rows, getTempleRows(args), limit)
"''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
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
if includeHatch then
local hatchArgs = {
addRows(rows, getHatchRows(args), limit)
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
if showHeader then
local missionaryArgs = {
table.insert(out, "'''Southern Strait Corridor Daily Operations'''")
year = year,
table.insert(out, "")
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
end


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


if showLegend then
if showLegend then
local legendBody = table.concat({
table.insert(out, renderLegend())
"* '''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 785: Line 457:
end
end


--------------------------------------------------------------------------------
function p.dailyReport(frame)
-- Shortcut views
return p.makeTable(frame)
--------------------------------------------------------------------------------
 
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
end


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


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


return p
return p

Revision as of 19:52, 5 June 2026

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

-- Module:SouthernStraitCorridorOps
-- Southern Strait corridor display.
-- Pulls from:
--   Module:WarLeagueDivisionOpsBVR
--   Module:MissionaryDailyOpsBVR
--   Module:HatchMinistryDailyOps
--
-- This module intentionally overrides the displayed city/node to Mylecia
-- for all rendered War League, Temple Bank, and Hatch Ministry corridor rows.

local p = {}

--------------------------------------------------------------------------------
-- Configuration
--------------------------------------------------------------------------------

local SOUTHERN_STRAIT_CITY = "Mylecia"
local SOUTHERN_STRAIT_CITY_FLAG = "MyleciaFlabB.jpeg"
local CITY_PAGE = "List_of_cities_in_Bassaridia_Vaeringheim"

local DEFAULT_LIMIT = 24

local HAIFAN_STAGING_CITIES = {
	["Keybir-Aviv"] = true,
	["Tel-Amin"] = true,
	["Diamandis"] = true,
	["Jogi"] = true,
	["Lewisburg"] = true,
	["Thermosalem"] = true,
	["Akróstadium"] = true,
	["Sufriya"] = true,
	["Lykopolis"] = true,
	["Mylecia"] = true,
}

--------------------------------------------------------------------------------
-- Basic helpers
--------------------------------------------------------------------------------

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

local function lower(s)
	return mw.ustring.lower(tostring(s or ""))
end

local function safeRequire(name)
	local ok, mod = pcall(require, name)
	if ok and mod then
		return mod
	end
	return nil
end

local function flagIcon(fileName, size)
	size = tonumber(size) or 20
	if not fileName or fileName == "" then
		return ""
	end
	return string.format("[[File:%s|%dpx]]", fileName, size)
end

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

local function stripCellMarkup(s)
	s = tostring(s or "")
	s = s:gsub("<br%s*/?>", " ")
	s = s:gsub("%[%[File:[^%]]+%]%]", "")
	s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
	s = s:gsub("%[%[([^%]]+)%]%]", "%1")
	s = s:gsub("'''", "")
	s = s:gsub("''", "")
	return trim(s)
end

local function rowContainsSouthernStrait(text)
	local t = lower(stripCellMarkup(text))

	if t:find("southern strait", 1, true) then return true end
	if t:find("southern straits", 1, true) then return true end
	if t:find("former maritime markets", 1, true) then return true 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
			return true
		end
	end

	return false
end

local function splitWikiCells(line)
	line = tostring(line or "")
	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(...)
	for _, v in ipairs({ ... }) do
		v = trim(v)
		if v ~= "" then
			return v
		end
	end
	return "—"
end

local function statusStyle(statusText)
	local s = lower(statusText)

	if s:find("critical", 1, true) or s:find("red", 1, true) or s:find("closed", 1, true) or s:find("no%-go") then
		return ' style="background-color:#f8d7da;"'
	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 ' style="background-color:#fff3cd;"'
	end

	if s:find("yellow", 1, true) or s:find("medium", 1, true) or s:find("slow", 1, true) then
		return ' style="background-color:#fff3cd;"'
	end

	return ' style="background-color:#d4edda;"'
end

--------------------------------------------------------------------------------
-- Source parsers
--------------------------------------------------------------------------------

local function parseWarLeagueRows(text, args)
	local rows = {}
	local flagSize = tonumber(args.flagsize) or 20
	local filterRows = tostring(args.filter or "yes"):lower() ~= "no"

	local currentStyle = ""
	for line in tostring(text or ""):gmatch("[^\r\n]+") do
		local trimmed = trim(line)

		if trimmed:match("^|%-") then
			currentStyle = trimmed:gsub("^|%-", "")
		elseif trimmed:match("^|") and not trimmed:match("^|}") then
			local cells = splitWikiCells(trimmed)

			-- WarLeagueDivisionOpsBVR expected:
			-- Date, Division, City / Node, Daily event trigger, Urgency,
			-- 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

	return rows
end

local function parseTempleRows(text, args)
	local rows = {}
	local flagSize = tonumber(args.flagsize) or 20
	local filterRows = tostring(args.filter or "yes"):lower() ~= "no"

	local currentStyle = ""
	for line in tostring(text or ""):gmatch("[^\r\n]+") do
		local trimmed = trim(line)

		if trimmed:match("^|%-") then
			currentStyle = trimmed:gsub("^|%-", "")
		elseif trimmed:match("^|") and not trimmed:match("^|}") then
			local cells = splitWikiCells(trimmed)

			-- MissionaryDailyOpsBVR expected:
			-- Date, City, Region, Ops Alert, Mystery (Cult), Unit,
			-- 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, {
						style = currentStyle ~= "" and currentStyle or statusStyle(cells[4] .. " " .. severity),
						source = "'''Temple Bank'''",
						date = firstNonEmpty(cells[1]),
						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

	return rows
end

local function parseHatchRows(text, args)
	local rows = {}
	local flagSize = tonumber(args.flagsize) or 20
	local filterRows = tostring(args.filter or "yes"):lower() ~= "no"

	local currentStyle = ""
	for line in tostring(text or ""):gmatch("[^\r\n]+") do
		local trimmed = trim(line)

		if trimmed:match("^|%-") then
			currentStyle = trimmed:gsub("^|%-", "")
		elseif trimmed:match("^|") and not trimmed:match("^|}") then
			local cells = splitWikiCells(trimmed)

			-- HatchMinistryDailyOps expected:
			-- Privateer Flag, Captain, Home City, Current Station,
			-- Ops Alert, Rotation, Today's Activity
			if #cells >= 7 and lower(cells[2]) ~= "captain" then
				local combined = table.concat(cells, " ")
				if (not filterRows) or rowContainsSouthernStrait(combined) then
					table.insert(rows, {
						style = currentStyle ~= "" and currentStyle or statusStyle(cells[5] .. " " .. cells[7]),
						source = "'''Hatch Ministry'''",
						date = firstNonEmpty(cells[6]),
						unit = firstNonEmpty(cells[2]),
						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
end

--------------------------------------------------------------------------------
-- Source loaders
--------------------------------------------------------------------------------

local function getWarLeagueRows(args)
	local wl = safeRequire("Module:WarLeagueDivisionOpsBVR")
	if not wl or type(wl.makeTable) ~= "function" then
		return {}
	end

	local year = tostring(args.year or "")
	local day = tostring(args.day or "")
	local seed = tostring(args.seed or "")

	local callArgs = {
		rows = tostring(args.warRows or args.rows or "48"),
		showlegend = "no",
	}

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

	local ok, result = pcall(wl.makeTable, { args = callArgs })
	if not ok then
		return {}
	end

	return parseWarLeagueRows(result, args)
end

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

	local callArgs = {
		region = tostring(args.templeRegion or args.region or "Haifan Bassaridia"),
		includeThreat = tostring(args.includeThreat or "no"),
		showEcon = "no",
		showlegend = "no",
		limit = tostring(args.templeLimit or "80"),
	}

	if args.year then callArgs.year = tostring(args.year) end
	if args.day then callArgs.day = tostring(args.day) end

	local ok, result = pcall(miss.makeTable, { args = callArgs })
	if not ok then
		return {}
	end

	return parseTempleRows(result, args)
end

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

	local callArgs = {
		legend = "no",
	}

	if args.year then callArgs.year = tostring(args.year) end
	if args.day then callArgs.day = tostring(args.day) end
	if args.captain then callArgs.captain = tostring(args.captain) end

	local ok, result = pcall(hatch.dailyReport, { args = callArgs })
	if not ok then
		return {}
	end

	return parseHatchRows(result, args)
end

--------------------------------------------------------------------------------
-- Rendering
--------------------------------------------------------------------------------

local function addRows(target, sourceRows, limit)
	for _, row in ipairs(sourceRows) do
		if #target >= limit then
			return
		end
		table.insert(target, row)
	end
end

local function renderTable(rows)
	local out = {}

	table.insert(out, "{| class='wikitable sortable' style='width:100%;'")
	table.insert(out, "! Source !! Date / Rotation !! Unit !! City !! Status !! Tasking !! Notes")

	if #rows == 0 then
		table.insert(out, "|-")
		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

	table.insert(out, "|}")
	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

--------------------------------------------------------------------------------
-- Public entry points
--------------------------------------------------------------------------------

function p.makeTable(frame)
	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 includeWar = tostring(args.war or "yes"):lower() ~= "no"
	local includeTemple = tostring(args.temple or "yes"):lower() ~= "no"
	local includeHatch = tostring(args.hatch or "yes"):lower() ~= "no"

	if includeWar then
		addRows(rows, getWarLeagueRows(args), limit)
	end

	if includeTemple then
		addRows(rows, getTempleRows(args), limit)
	end

	if includeHatch then
		addRows(rows, getHatchRows(args), limit)
	end

	local out = {}

	if showHeader then
		table.insert(out, "'''Southern Strait Corridor Daily Operations'''")
		table.insert(out, "")
	end

	table.insert(out, renderTable(rows))

	if showLegend then
		table.insert(out, renderLegend())
	end

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

function p.dailyReport(frame)
	return p.makeTable(frame)
end

function p.generateTable(frame)
	return p.makeTable(frame)
end

function p.cityCell(frame)
	local args = (frame and frame.args) or {}
	return myleciaCell(args.flagsize or 20)
end

return p