diff --git a/FS25_TaxMod.zip b/FS25_TaxMod.zip new file mode 100644 index 0000000..0c8dba8 Binary files /dev/null and b/FS25_TaxMod.zip differ diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..ea3ece8 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,35 @@ +# Roadmap: FS25_TaxMod + +> Ecosystem role: **Markets and Economy** · Part of the Realistic Farming connected suite +> Status: FILLED from the ecosystem audit/baseline. +> Forward-looking only. Shipped history lives in CHANGELOG.md and the releases. + +## How to use this file +- Populate the milestones below from the audit baseline once it lands. +- Each item should be small enough to map to a `TODO.md` entry. +- Keep it honest: near-term is committed, mid-term is intended, long-term is aspirational. + +## Current baseline +- Version at baseline: v1.1.3.0 +- Audit reference: ecosystem-dev-tracking Point 1-5 (FS25_TaxMod, 2026-06-30) +- Baseline date: 2026-06-30 + +## Near-term (next release cycle) +- [x] MP tax bug (F15): DONE on development (commit 8ff9989d). Annual-tax `addMoney` gated on `getIsServer`; `applyDailyTax` only accumulates. Re-verify the release asset matches dev HEAD (asset modDesc was mislabeled 1.1.4.1). +- [x] StateLedger `TaxMod_Data` bridge live (delegate-when-present); `modSettings/` save kept as the safety copy. Shipped v1.1.5.0. (Fully retiring the FS22 path is a later cleanup.) +- [~] SettingsHub: 5 settings registered (selfPersisted), shipped v1.1.5.0; removing TaxSettingsUI.lua + UIHelper.lua (ESC injection) still open. + +## Mid-term (this season) +- [ ] NetworkSync channel `TaxMod_Sync` for settings broadcast on admin change. Not built yet. +- [x] MasterHUD `TaxMod_HUD` registered (delegate-when-present; TaxHUD keeps its own draw/update/mouse logic). Shipped v1.1.5.0. +- [ ] Decide the fate of `returnPercentage` (vestigial in the annual model). + +## Long-term / aspirational +- [ ] Richer tax model (brackets, deductions, or period options) without breaking MP correctness. + +## Cross-mod / ecosystem dependencies +- [~] Bedrock migrations: 3/4 done (StateLedger + MasterHUD + SettingsHub, v1.1.5.0); NetworkSync remaining. +- [ ] FarmTablet TaxApp (reads `g_currentMission.taxManager`). + +## Deferred / parked +- Consuming a peer earnings API instead of reading `farm.money` directly: parked; direct read is correct and simpler for v1. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..50362da --- /dev/null +++ b/TODO.md @@ -0,0 +1,32 @@ +# TODO: FS25_TaxMod + +> Ecosystem role: **Markets and Economy** · Part of the Realistic Farming connected suite +> Status: FILLED from the ecosystem audit/baseline, kept current. +> Convention: `[ ]` open · `[~]` in progress · `[x]` done · `[!]` blocked. Newest at the top of each section. + +## From the ecosystem audit (Arissani) +- [x] StateLedger `TaxMod_Data` bridge live (delegate-when-present); own `modSettings/` save kept as the safety copy per the ecosystem pattern. Shipped v1.1.5.0. (Fully retiring the FS22 createXMLFile path is a later cleanup.) +- [~] SettingsHub bridged (selfPersisted, v1.1.5.0); removing TaxSettingsUI.lua + UIHelper.lua (ESC injection) is still open (ESC retained as the standalone fallback meanwhile). +- [ ] Decide: keep or remove `returnPercentage`. + +## Bugs +- [x] MP tax bug (F15): RESOLVED on development (commit 8ff9989d, 2026-07-01). The annual-tax `addMoney` is gated on `getIsServer` (main.lua:226); `applyDailyTax` only accumulates and moves no money. Note: the mislabeled pre-release asset (modDesc 1.1.4.1) predates this, so re-verify against dev HEAD before release. + +## Features / enhancements +- [~] Bedrock 3/4 built (delegate-when-present): StateLedger + MasterHUD + SettingsHub (v1.1.5.0). NetworkSync (`TaxMod_Sync`) still open. + +## Cross-mod integration +- [x] SCS-011 irrigation-expense mirror: DONE + IN-GAME VERIFIED 2026-07-17 (`src/integrations/CropStressIrrigationExpense.lua`, commit 9b19058). Reads the SCS B3.2b facade (`getIrrigationSystems` / `getIrrigationCostsEnabled`) once per in-game day and records ONE aggregated "Irrigation Operations" debit via `recordExpense` (bookkeeping only, no money moved; SCS already charges it, so no double-charge). Attributes to `g_currentMission:getFarmId()` (the same local player farm SCS's FinanceIntegration debits; corrects the brief's pivot-ownership OI-1). Depreciation wired but neutral at 0 (OI-2: needs a capital value + useful life from the balance pass). Verified in-game: -€40 Irrigation Operations debit fired on the day change, no errors. Deployed, not released (batched). Soft follow-ups: confirm the amount equals SCS's actual daily deduction; check the `M0` month stamp in the ledger display. +- [ ] TaxMod has NO self-test harness (verify gap). SCS-011 was verified via Lua 5.1 parse + the SCS-side getter test + in-game; stand up a `tools/test/` harness (mirror SoilFertilizer/SCS) before the next real logic build. +- [x] StateLedger: `TaxMod_Data` bridge live (delegate-when-present). Shipped v1.1.5.0. +- [ ] NetworkSync: `TaxMod_Sync` (settings broadcast). Not built yet. +- [x] MasterHUD: `TaxMod_HUD` registered (delegate-when-present). Shipped v1.1.5.0. +- [x] SettingsHub: 5 settings registered (selfPersisted). Shipped v1.1.5.0. + +## Docs / localization +- [ ] Keep all 26 languages in step for any new setting. +- [ ] Update README/version on each release. + +## Blocked / waiting on +- [~] Bedrock migrations: 3/4 adopted (StateLedger/MasterHUD/SettingsHub, v1.1.5.0); NetworkSync remaining. +- [!] MasterHUD GUI-visibility suppression policy (waits on: audit answer on whether MasterHUD or each panel guards GUI visibility). diff --git a/VISION.md b/VISION.md new file mode 100644 index 0000000..98063f7 --- /dev/null +++ b/VISION.md @@ -0,0 +1,41 @@ +# Vision: FS25_TaxMod + +> Ecosystem role: **Markets and Economy** · Part of the Realistic Farming connected suite +> Status: FILLED from the ecosystem audit (Point 1-5, ecosystem-map, notes). +> Last updated: 2026-07-08 + +## 1. One-line purpose +Recurring farm taxation: an annual (and optional daily) tax charged against the farm's finances, so profit carries an ongoing liability instead of just accumulating. + +## 2. Problem it solves +FS25 has no taxation. Money accrues with zero ongoing cost, so once a farm is profitable there is nothing pulling against the balance. TaxMod adds a realistic recurring tax so wealth management and reinvestment actually matter. + +## 3. Design pillars +- **Multiplayer-correct.** Tax deductions are server-authoritative. A period's tax is charged once for a farm, never once per connected client. +- **Finance-based.** The tax scales off the farm's own money/finances, read from base-game farm data. +- **Configurable rates.** Tax rate and cadence are settings; defaults stay reasonable. +- **Standard persistence.** State lives in the standard save surface, not a non-standard subfolder. + +## 4. Role in the ecosystem +- Public handle on `g_currentMission.taxManager` (getfenv alias `g_TaxManager`), set in `Mission00.load`. +- Reads from (consumes): base-game farm finances via `g_farmManager:getFarmById(...).money` directly. It does NOT consume a peer economy API (it is not a confirmed consumer of getEarningsForFarm/getFertilizerSpendForFarm; that was architectural intent, not v1 behaviour). +- Read by (consumers): FarmTablet TaxApp, via the mission handle read contract. +- Core-API registration status (specced in Point 1-5, not yet wired): + - StateLedger (save/load): planned, module `TaxMod_Data`. Replaces the non-standard `modSettings/FS25_TaxMod.xml` and the FS22 createXMLFile/loadXMLFile usage. + - NetworkSync (MP state): planned, channel `TaxMod_Sync` (settings broadcast on admin change). + - MasterHUD (overlays): planned, `TaxMod_HUD` (TaxHUD already owns its draw/update/mouse logic). + - SettingsHub (admin settings): planned, 5 settings. Removes TaxSettingsUI.lua + UIHelper.lua (the ESC-menu injection). + +## 5. Explicit non-goals +- Does not consume peer economy mods for its base. It reads farm money directly rather than another mod's earnings API. +- Not a subsidy or income system (that is IncomeMod). TaxMod only takes money. +- The HUD layout stays client-local. + +## 6. Success criteria +- A period's tax is deducted exactly once per farm, correct in multiplayer. +- Persistence moves to the standard save surface; no reliance on a `modSettings/` subfolder that may not exist. +- Settings are admin-gated with no ESC-menu injection remaining after the SettingsHub move. + +## 7. Open questions for the audit +- `returnPercentage` is still in the settings table and save surface but no longer drives the annual tax model. Keep for backward compat or remove? +- MasterHUD: TaxHUD already guards on `g_gui` visibility itself. Should MasterHUD suppress draws when the GUI is visible, or does each panel keep handling that? diff --git a/main.lua b/main.lua index 1679be4..16571c5 100644 --- a/main.lua +++ b/main.lua @@ -13,11 +13,15 @@ local modName = g_currentModName source(modDirectory .. "src/settings/UIHelper.lua") source(modDirectory .. "src/settings/SettingsUI.lua") source(modDirectory .. "src/ui/TaxHUD.lua") +source(modDirectory .. "src/settings/SettingsHubBridge.lua") +source(modDirectory .. "src/integrations/TaxStateLedgerBridge.lua") -- bedrock: optional StateLedger state bridge +source(modDirectory .. "src/integrations/TaxMasterHUDBridge.lua") -- bedrock: optional MasterHUD draw bridge +source(modDirectory .. "src/integrations/CropStressIrrigationExpense.lua") -- SCS-011: mirror SCS irrigation operating cost as a deductible expense FS25TaxMod = {} FS25TaxMod.modDir = modDirectory FS25TaxMod.modName = modName -FS25TaxMod.version = "1.1.3.0" +FS25TaxMod.version = "1.1.5.0" FS25TaxMod.Debug = false local settings = { @@ -45,6 +49,13 @@ local stats = { } FS25TaxMod.stats = stats +-- Companion ledger: tracked money adjustments recorded by other mods via +-- g_TaxManager.recordExpense(). Pure bookkeeping, surfaced in stats/HUD. +-- It never moves money and never enters any tax computation. +local LEDGER_MAX_ENTRIES = 10 +local ledger = { farms = {} } +FS25TaxMod.ledger = ledger + local lastDay = -1 local lastMonth = -1 local lastMinuteCheck = -1 @@ -100,6 +111,14 @@ local function saveSettings() setXMLInt(xmlFile, "settings.stats.lastTaxYear", stats.lastTaxYear or 0) -- New setXMLInt(xmlFile, "settings.lastDay", tonumber(lastDay) or 1) setXMLInt(xmlFile, "settings.lastMonth", tonumber(lastMonth) or 1) + local farmIndex = 0 + for farmId, farmLedger in pairs(ledger.farms) do + local farmKey = string.format("settings.ledger.farm(%d)", farmIndex) + setXMLInt(xmlFile, farmKey .. "#farmId", farmId) + setXMLFloat(xmlFile, farmKey .. "#creditTotal", farmLedger.creditTotal or 0) + setXMLFloat(xmlFile, farmKey .. "#debitTotal", farmLedger.debitTotal or 0) + farmIndex = farmIndex + 1 + end saveXMLFile(xmlFile) delete(xmlFile) end @@ -132,10 +151,115 @@ local function loadSettings() stats.lastTaxYear = Utils.getNoNil(getXMLInt(xmlFile, "settings.stats.lastTaxYear"), 0) -- New lastDay = Utils.getNoNil(getXMLInt(xmlFile, "settings.lastDay"), lastDay) lastMonth = Utils.getNoNil(getXMLInt(xmlFile, "settings.lastMonth"), lastMonth) + ledger.farms = {} + local farmIndex = 0 + while true do + local farmKey = string.format("settings.ledger.farm(%d)", farmIndex) + local farmId = getXMLInt(xmlFile, farmKey .. "#farmId") + if farmId == nil then break end + ledger.farms[farmId] = { + creditTotal = Utils.getNoNil(getXMLFloat(xmlFile, farmKey .. "#creditTotal"), 0), + debitTotal = Utils.getNoNil(getXMLFloat(xmlFile, farmKey .. "#debitTotal"), 0), + entries = {}, + } + farmIndex = farmIndex + 1 + end delete(xmlFile) log("Settings loaded", 2) end +-- ===================== +-- STATE SERIALIZATION (StateLedger bridge) +-- ===================== +-- serializeState/applyState are the in-memory twins of the state portion of +-- saveSettings/loadSettings (stats + the recordExpense ledger + the day/month +-- cursors). They carry STATE only, never settings (SettingsHub owns those). +-- The own XML stays the standalone safety copy; these feed the optional +-- StateLedger module TaxMod_Data when bedrock is present. Plain-table in/out so +-- StateLedger's generic serializer can round-trip them (number keys included). + +function FS25TaxMod.serializeState() + local farms = {} + for farmId, fl in pairs(ledger.farms) do + local entries = {} + if type(fl.entries) == "table" then + for i, e in ipairs(fl.entries) do + entries[i] = { amount = e.amount, label = e.label, day = e.day, month = e.month } + end + end + farms[farmId] = { + creditTotal = fl.creditTotal or 0, + debitTotal = fl.debitTotal or 0, + entries = entries, + } + end + return { + version = 1, + stats = { + totalTaxesPaid = stats.totalTaxesPaid or 0, + totalTaxesReturned = stats.totalTaxesReturned or 0, + taxesThisMonth = stats.taxesThisMonth or 0, + daysTaxed = stats.daysTaxed or 0, + monthsReturned = stats.monthsReturned or 0, + taxesAccumulatedAnnual = stats.taxesAccumulatedAnnual or 0, + lastTaxYear = stats.lastTaxYear or 0, + }, + lastDay = lastDay, + lastMonth = lastMonth, + ledger = { farms = farms }, + } +end + +function FS25TaxMod.applyState(data) + if type(data) ~= "table" then return false end + + local st = data.stats + if type(st) == "table" then + stats.totalTaxesPaid = tonumber(st.totalTaxesPaid) or stats.totalTaxesPaid or 0 + stats.totalTaxesReturned = tonumber(st.totalTaxesReturned) or stats.totalTaxesReturned or 0 + stats.taxesThisMonth = tonumber(st.taxesThisMonth) or stats.taxesThisMonth or 0 + stats.daysTaxed = tonumber(st.daysTaxed) or stats.daysTaxed or 0 + stats.monthsReturned = tonumber(st.monthsReturned) or stats.monthsReturned or 0 + stats.taxesAccumulatedAnnual = tonumber(st.taxesAccumulatedAnnual) or stats.taxesAccumulatedAnnual or 0 + stats.lastTaxYear = tonumber(st.lastTaxYear) or stats.lastTaxYear or 0 + end + + if data.lastDay ~= nil then lastDay = tonumber(data.lastDay) or lastDay end + if data.lastMonth ~= nil then lastMonth = tonumber(data.lastMonth) or lastMonth end + + -- Rebuild ledger.farms IN PLACE so the table identity FS25TaxMod.ledger (and + -- the HUD) already holds stays valid. + for k in pairs(ledger.farms) do ledger.farms[k] = nil end + local df = data.ledger and data.ledger.farms + if type(df) == "table" then + for farmId, fl in pairs(df) do + local fid = tonumber(farmId) + if fid ~= nil and type(fl) == "table" then + local entries = {} + if type(fl.entries) == "table" then + for _, e in ipairs(fl.entries) do + if type(e) == "table" then + entries[#entries + 1] = { + amount = tonumber(e.amount) or 0, + label = tostring(e.label or ""), + day = tonumber(e.day) or 0, + month = tonumber(e.month) or 0, + } + end + end + end + ledger.farms[fid] = { + creditTotal = tonumber(fl.creditTotal) or 0, + debitTotal = tonumber(fl.debitTotal) or 0, + entries = entries, + } + end + end + end + + return true +end + local function applyDailyTax() if not settings.enabled or not g_currentMission then return end local farmId = g_currentMission:getFarmId() @@ -191,7 +315,12 @@ local function applyAnnualTax() return end - g_currentMission:addMoney(-taxAmount, farmId, MoneyType.OTHER, true) + -- Only the server may move money; the engine syncs the new balance to clients. + -- Guard only the addMoney (not the whole function): TaxMod has no MP stat sync, so + -- every machine must still reset its own accumulation below, or client HUDs drift. + if g_currentMission:getIsServer() then + g_currentMission:addMoney(-taxAmount, farmId, MoneyType.OTHER, true) + end stats.totalTaxesPaid = stats.totalTaxesPaid + taxAmount -- Update total paid with annual tax stats.taxesAccumulatedAnnual = 0 -- Reset annual accumulation stats.lastTaxYear = currentYear -- Mark tax as paid for this year @@ -252,6 +381,12 @@ local function createUpdateable() if env.currentDay ~= lastDay then lastDay = env.currentDay applyDailyTax() + -- SCS-011: mirror one day of SCS irrigation operating cost + -- into the ledger (read-only, server-only, neutral when SCS + -- is absent or costs are off). + if CropStressIrrigationExpense then + CropStressIrrigationExpense.accrueDaily(FS25TaxMod) + end end -- Monthly checks for annual tax events @@ -275,6 +410,69 @@ local function createUpdateable() } end +-- ===================== +-- PUBLIC WRITE API (companion mods) +-- ===================== +-- g_TaxManager.recordExpense(farmId, amount, label) -> boolean success +-- Records a tracked money adjustment in the running per-farm credit/debit +-- ledger. Positive amount = credit (money the farm received, e.g. a rebate), +-- negative amount = debit (money the farm paid out, e.g. a wage). +-- Bookkeeping only: it never moves money and never changes any tax +-- computation (TaxMod taxes balance, not income). Callers are expected to be +-- server-side money code; on a non-server peer the call is rejected. +function FS25TaxMod.recordExpense(a, b, c, d) + -- Tolerate colon-style calls (g_TaxManager:recordExpense(...)) + local farmId, amount, label = a, b, c + if a == FS25TaxMod then + farmId, amount, label = b, c, d + end + + if g_currentMission == nil or not g_currentMission:getIsServer() then + Logging.warning("[%s] recordExpense: server-only API called on a non-server peer, ignoring", modName) + return false + end + if type(farmId) ~= "number" or farmId <= 0 then + Logging.warning("[%s] recordExpense: invalid farmId '%s' (positive number expected)", modName, tostring(farmId)) + return false + end + if type(amount) ~= "number" or amount ~= amount or amount == math.huge or amount == -math.huge or amount == 0 then + Logging.warning("[%s] recordExpense: invalid amount '%s' (finite non-zero number expected)", modName, tostring(amount)) + return false + end + if label ~= nil and type(label) ~= "string" then + Logging.warning("[%s] recordExpense: invalid label '%s' (string expected)", modName, tostring(label)) + return false + end + label = label or "Companion expense" + + local farmLedger = ledger.farms[farmId] + if farmLedger == nil then + farmLedger = { creditTotal = 0, debitTotal = 0, entries = {} } + ledger.farms[farmId] = farmLedger + end + + if amount > 0 then + farmLedger.creditTotal = farmLedger.creditTotal + amount + else + farmLedger.debitTotal = farmLedger.debitTotal - amount + end + + local env = g_currentMission.environment + table.insert(farmLedger.entries, 1, { + amount = amount, + label = label, + day = env and env.currentDay or 0, + month = env and env.currentMonth or 0, + }) + while #farmLedger.entries > LEDGER_MAX_ENTRIES do + table.remove(farmLedger.entries) + end + + log(string.format("recordExpense: farm %d %s%s (%s)", farmId, + amount > 0 and "+" or "", tostring(amount), label), 2) + return true +end + -- ===================== -- CONSOLE COMMANDS -- ===================== @@ -356,6 +554,21 @@ function FS25TaxMod:consoleTaxStatistics() if stats.daysTaxed > 0 then print("Avg daily tax: " .. formatMoney(math.floor(stats.totalTaxesPaid / stats.daysTaxed))) end + print("=== Companion Ledger (recordExpense) ===") + local farmCount = 0 + for farmId, farmLedger in pairs(ledger.farms) do + farmCount = farmCount + 1 + print(string.format("Farm %d: credits +%s | debits -%s", + farmId, formatMoney(farmLedger.creditTotal), formatMoney(farmLedger.debitTotal))) + for i = 1, math.min(#farmLedger.entries, 3) do + local entry = farmLedger.entries[i] + print(string.format(" M%d D%d %s%s (%s)", entry.month, entry.day, + entry.amount > 0 and "+" or "-", formatMoney(math.abs(entry.amount)), entry.label)) + end + end + if farmCount == 0 then + print("No tracked entries yet") + end end function FS25TaxMod:consoleTaxSimulate() @@ -433,16 +646,39 @@ local function onLoad(mission) if ok and id then FS25TaxMod.toggleHUDEventId = id g_inputBinding:setActionEventTextPriority(id, GS_PRIO_NORMAL) - log("HUD toggle registered", 2) else log("HUD toggle registration failed", 1) end + + -- HUD drag (enter/exit edit mode) — PLAYER context + local dragOk, dragId = g_inputBinding:registerActionEvent( + InputAction.TM_HUD_DRAG, + FS25TaxMod, + FS25TaxMod.onHUDDragInput, + false, true, false, true + ) + if dragOk and dragId then + FS25TaxMod.hudDragEventId = dragId + g_inputBinding:setActionEventText(dragId, g_i18n:getText("input_TM_HUD_DRAG") or "Tax HUD Edit Mode") + log("HUD drag registered", 2) + end + g_inputBinding:endActionEventsModification() end end end +function FS25TaxMod:onHUDDragInput() + if not taxHUD then return end + if not taxHUD.visible then return end + if taxHUD.editMode then + taxHUD:exitEditMode() + else + taxHUD:enterEditMode() + end +end + function FS25TaxMod:onToggleHUDInput() if taxHUD then taxHUD:toggleVisibility() @@ -458,6 +694,24 @@ local function onMissionLoaded(mission, node) loadSettings() + -- Register with SettingsHub (if installed) so FarmTablet's System Settings + -- app can list Tax Mod's settings. No-ops safely if SettingsHub is absent. + TaxSettingsHubBridge.register(FS25TaxMod) + + -- Register with StateLedger (if installed) as the state load source of truth. + -- No-ops safely if StateLedger is absent (own XML stays primary). Runs after + -- loadSettings so, when the shared ledger is present, it overrides the state + -- portion imported from the own XML. + TaxStateLedgerBridge.register(FS25TaxMod) + if TaxStateLedgerBridge.hasState() then + TaxStateLedgerBridge.applyState(FS25TaxMod) + end + + -- Register the tax HUD with MasterHUD (if installed) so it owns the single + -- suspend-aware draw loop. No-ops safely if MasterHUD is absent; the own + -- FSBaseMission.draw hook below stands down when this is active. + TaxMasterHUDBridge.register(FS25TaxMod) + local env = g_currentMission.environment if env then lastDay = tonumber(env.currentDay) or 1 @@ -506,6 +760,10 @@ local function onUnload() g_inputBinding:removeActionEvent(FS25TaxMod.toggleHUDEventId) FS25TaxMod.toggleHUDEventId = nil end + if FS25TaxMod.hudDragEventId and g_inputBinding then + g_inputBinding:removeActionEvent(FS25TaxMod.hudDragEventId) + FS25TaxMod.hudDragEventId = nil + end if FS25TaxMod._inputHookOriginal and PlayerInputComponent then PlayerInputComponent.registerActionEvents = FS25TaxMod._inputHookOriginal FS25TaxMod._inputHookOriginal = nil @@ -525,8 +783,17 @@ FSBaseMission.update = Utils.appendedFunction(FSBaseMission.update, function(mis if taxHUD then taxHUD:update(dt) end end) +-- When MasterHUD is present it drives TaxMasterHUDBridge.drawStack in its own +-- suspend-aware loop, so this fallback hook stands down to avoid a double draw. +-- The draw body (the taxHUD + settings.showHUD guard) lives in drawStack so the +-- two paths can never diverge. FSBaseMission.draw = Utils.appendedFunction(FSBaseMission.draw, function(mission) - if taxHUD and settings.showHUD then taxHUD:draw() end + if TaxMasterHUDBridge and TaxMasterHUDBridge.active then return end + if TaxMasterHUDBridge then + TaxMasterHUDBridge.drawStack() + elseif taxHUD and settings.showHUD then + taxHUD:draw() + end end) Mission00.saveToXMLFile = Utils.appendedFunction(Mission00.saveToXMLFile, function(mission, xmlFilename) diff --git a/modDesc.xml b/modDesc.xml index bb03055..c1aa079 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -4,7 +4,7 @@ xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/NMC-TBone/xml-schema/main/modDesc.xsd"> TisonK - 1.1.4.0 + 1.1.5.0 <en>Tax Mod</en> @@ -127,6 +127,14 @@ Funkcje: </extraSourceFiles> <l10n> + <!-- SCS-011 irrigation expense ledger labels (companion expense lines) --> + <text name="tm_irrigation_operations"> + <en><![CDATA[Irrigation Operations]]></en> + </text> + <text name="tm_irrigation_assets"> + <en><![CDATA[Irrigation Assets]]></en> + </text> + <!-- Section Header --> <text name="tm_section"> <en><![CDATA[Tax Mod]]></en> @@ -140,7 +148,7 @@ Funkcje: <cz>[EN] <![CDATA[Tax Mod]]></cz> <br>[EN] <![CDATA[Tax Mod]]></br> <ru>[EN] <![CDATA[Tax Mod]]></ru> - <da>[EN] <![CDATA[Tax Mod]]></da> + <da><![CDATA[Skattebaseret Mod]]></da> </text> <!-- Enable --> @@ -156,7 +164,7 @@ Funkcje: <cz>[EN] <![CDATA[Enable Tax Mod]]></cz> <br>[EN] <![CDATA[Enable Tax Mod]]></br> <ru>[EN] <![CDATA[Enable Tax Mod]]></ru> - <da>[EN] <![CDATA[Enable Tax Mod]]></da> + <da><![CDATA[Aktiver Skattebaseret Mod]]></da> </text> <text name="tm_enabled_long"> <en><![CDATA[Enable or disable daily tax deductions and monthly returns]]></en> @@ -170,7 +178,7 @@ Funkcje: <cz>[EN] <![CDATA[Enable or disable daily tax deductions and monthly returns]]></cz> <br>[EN] <![CDATA[Enable or disable daily tax deductions and monthly returns]]></br> <ru>[EN] <![CDATA[Enable or disable daily tax deductions and monthly returns]]></ru> - <da>[EN] <![CDATA[Enable or disable daily tax deductions and monthly returns]]></da> + <da><![CDATA[Aktivér eller deaktiver daglige skattefradrag og månedlige skatterefusioner]]></da> </text> <!-- Tax Rate --> @@ -186,7 +194,7 @@ Funkcje: <cz>[EN] <![CDATA[Tax Rate]]></cz> <br>[EN] <![CDATA[Tax Rate]]></br> <ru>[EN] <![CDATA[Tax Rate]]></ru> - <da>[EN] <![CDATA[Tax Rate]]></da> + <da><![CDATA[Skattesats]]></da> </text> <text name="tm_taxrate_long"> <en><![CDATA[Daily tax rate applied to your farm balance: Low=1%, Medium=2%, High=3%]]></en> @@ -200,25 +208,25 @@ Funkcje: <cz>[EN] <![CDATA[Daily tax rate applied to your farm balance: Low=1%, Medium=2%, High=3%]]></cz> <br>[EN] <![CDATA[Daily tax rate applied to your farm balance: Low=1%, Medium=2%, High=3%]]></br> <ru>[EN] <![CDATA[Daily tax rate applied to your farm balance: Low=1%, Medium=2%, High=3%]]></ru> - <da>[EN] <![CDATA[Daily tax rate applied to your farm balance: Low=1%, Medium=2%, High=3%]]></da> + <da><![CDATA[Daglig skattesats anvendt på din gårds saldo: Lav=1 %, Mellem=2 %, Høj=3 %]]></da> </text> <text name="tm_taxrate_1"><en><![CDATA[Low (1%)]]></en><de><![CDATA[Niedrig (1%)]]></de><fr><![CDATA[Faible (1%)]]></fr><es><![CDATA[Baja (1%)]]></es><it><![CDATA[Bassa (1%)]]></it><pl><![CDATA[Niska (1%)]]></pl><uk><![CDATA[Низька (1%)]]></uk> <cz>[EN] <![CDATA[Low (1%)]]></cz> <br>[EN] <![CDATA[Low (1%)]]></br> <ru>[EN] <![CDATA[Low (1%)]]></ru> - <da>[EN] <![CDATA[Low (1%)]]></da> + <da><![CDATA[Lav (1 %)]]></da> </text> <text name="tm_taxrate_2"><en><![CDATA[Medium (2%)]]></en><de><![CDATA[Mittel (2%)]]></de><fr><![CDATA[Moyen (2%)]]></fr><es><![CDATA[Media (2%)]]></es><it><![CDATA[Media (2%)]]></it><pl><![CDATA[Średnia (2%)]]></pl><uk><![CDATA[Середня (2%)]]></uk> <cz>[EN] <![CDATA[Medium (2%)]]></cz> <br>[EN] <![CDATA[Medium (2%)]]></br> <ru>[EN] <![CDATA[Medium (2%)]]></ru> - <da>[EN] <![CDATA[Medium (2%)]]></da> + <da><![CDATA[Mellem (2 %)]]></da> </text> <text name="tm_taxrate_3"><en><![CDATA[High (3%)]]></en><de><![CDATA[Hoch (3%)]]></de><fr><![CDATA[Élevé (3%)]]></fr><es><![CDATA[Alta (3%)]]></es><it><![CDATA[Alta (3%)]]></it><pl><![CDATA[Wysoka (3%)]]></pl><uk><![CDATA[Висока (3%)]]></uk> <cz>[EN] <![CDATA[High (3%)]]></cz> <br>[EN] <![CDATA[High (3%)]]></br> <ru>[EN] <![CDATA[High (3%)]]></ru> - <da>[EN] <![CDATA[High (3%)]]></da> + <da><![CDATA[Høj (3 %)]]></da> </text> <!-- Annual Tax Rate --> @@ -234,7 +242,7 @@ Funkcje: <cz>[EN] <![CDATA[Annual Tax Rate]]></cz> <br>[EN] <![CDATA[Annual Tax Rate]]></br> <ru>[EN] <![CDATA[Annual Tax Rate]]></ru> - <da>[EN] <![CDATA[Annual Tax Rate]]></da> + <da><![CDATA[Årlig skattesats]]></da> </text> <text name="tm_annualrate_long"> <en><![CDATA[Percentage of accumulated taxes paid each March: Low=2%, Medium=5%, High=10%]]></en> @@ -248,25 +256,25 @@ Funkcje: <cz>[EN] <![CDATA[Percentage of accumulated taxes paid each March: Low=2%, Medium=5%, High=10%]]></cz> <br>[EN] <![CDATA[Percentage of accumulated taxes paid each March: Low=2%, Medium=5%, High=10%]]></br> <ru>[EN] <![CDATA[Percentage of accumulated taxes paid each March: Low=2%, Medium=5%, High=10%]]></ru> - <da>[EN] <![CDATA[Percentage of accumulated taxes paid each March: Low=2%, Medium=5%, High=10%]]></da> + <da><![CDATA[Procentdel af de akkumulerede skatter, der udbetales hver marts: Lav=2 %, Mellem=5 %, Høj=10 %]]></da> </text> <text name="tm_annualrate_1"><en><![CDATA[Low (2%)]]></en><de><![CDATA[Niedrig (2%)]]></de><fr><![CDATA[Faible (2%)]]></fr><es><![CDATA[Baja (2%)]]></es><it><![CDATA[Bassa (2%)]]></it><pl><![CDATA[Niski (2%)]]></pl><uk><![CDATA[Низький (2%)]]></uk> <cz>[EN] <![CDATA[Low (2%)]]></cz> <br>[EN] <![CDATA[Low (2%)]]></br> <ru>[EN] <![CDATA[Low (2%)]]></ru> - <da>[EN] <![CDATA[Low (2%)]]></da> + <da><![CDATA[Lav (2 %)]]></da> </text> <text name="tm_annualrate_2"><en><![CDATA[Medium (5%)]]></en><de><![CDATA[Mittel (5%)]]></de><fr><![CDATA[Moyen (5%)]]></fr><es><![CDATA[Media (5%)]]></es><it><![CDATA[Media (5%)]]></it><pl><![CDATA[Średni (5%)]]></pl><uk><![CDATA[Середній (5%)]]></uk> <cz>[EN] <![CDATA[Medium (5%)]]></cz> <br>[EN] <![CDATA[Medium (5%)]]></br> <ru>[EN] <![CDATA[Medium (5%)]]></ru> - <da>[EN] <![CDATA[Medium (5%)]]></da> + <da><![CDATA[Mellem (5 %)]]></da> </text> <text name="tm_annualrate_3"><en><![CDATA[High (10%)]]></en><de><![CDATA[Hoch (10%)]]></de><fr><![CDATA[Élevé (10%)]]></fr><es><![CDATA[Alta (10%)]]></es><it><![CDATA[Alta (10%)]]></it><pl><![CDATA[Wysoki (10%)]]></pl><uk><![CDATA[Високий (10%)]]></uk> <cz>[EN] <![CDATA[High (10%)]]></cz> <br>[EN] <![CDATA[High (10%)]]></br> <ru>[EN] <![CDATA[High (10%)]]></ru> - <da>[EN] <![CDATA[High (10%)]]></da> + <da><![CDATA[Høj (10 %)]]></da> </text> <!-- Return Percentage (kept for save compatibility) --> @@ -282,7 +290,7 @@ Funkcje: <cz>[EN] <![CDATA[Tax Return %]]></cz> <br>[EN] <![CDATA[Tax Return %]]></br> <ru>[EN] <![CDATA[Tax Return %]]></ru> - <da>[EN] <![CDATA[Tax Return %]]></da> + <da><![CDATA[Skatterefusion %]]></da> </text> <text name="tm_return_long"> <en><![CDATA[Percentage of monthly taxes returned at the start of each new month: Low=10%, Medium=20%, High=30%]]></en> @@ -296,25 +304,25 @@ Funkcje: <cz>[EN] <![CDATA[Percentage of monthly taxes returned at the start of each new month: Low=10%, Medium=20%, High=30%]]></cz> <br>[EN] <![CDATA[Percentage of monthly taxes returned at the start of each new month: Low=10%, Medium=20%, High=30%]]></br> <ru>[EN] <![CDATA[Percentage of monthly taxes returned at the start of each new month: Low=10%, Medium=20%, High=30%]]></ru> - <da>[EN] <![CDATA[Percentage of monthly taxes returned at the start of each new month: Low=10%, Medium=20%, High=30%]]></da> + <da><![CDATA[Procentdel af de månedlige skatter, der tilbagebetales ved starten af hver ny måned: Lav=10 %, Mellem=20 %, Høj=30 %]]></da> </text> <text name="tm_return_1"><en><![CDATA[Low (10%)]]></en><de><![CDATA[Niedrig (10%)]]></de><fr><![CDATA[Faible (10%)]]></fr><es><![CDATA[Baja (10%)]]></es><it><![CDATA[Bassa (10%)]]></it><pl><![CDATA[Niski (10%)]]></pl><uk><![CDATA[Низький (10%)]]></uk> <cz>[EN] <![CDATA[Low (10%)]]></cz> <br>[EN] <![CDATA[Low (10%)]]></br> <ru>[EN] <![CDATA[Low (10%)]]></ru> - <da>[EN] <![CDATA[Low (10%)]]></da> + <da><![CDATA[Lav (10 %)]]></da> </text> <text name="tm_return_2"><en><![CDATA[Medium (20%)]]></en><de><![CDATA[Mittel (20%)]]></de><fr><![CDATA[Moyen (20%)]]></fr><es><![CDATA[Media (20%)]]></es><it><![CDATA[Media (20%)]]></it><pl><![CDATA[Średni (20%)]]></pl><uk><![CDATA[Середній (20%)]]></uk> <cz>[EN] <![CDATA[Medium (20%)]]></cz> <br>[EN] <![CDATA[Medium (20%)]]></br> <ru>[EN] <![CDATA[Medium (20%)]]></ru> - <da>[EN] <![CDATA[Medium (20%)]]></da> + <da><![CDATA[Mellem (20 %)]]></da> </text> <text name="tm_return_3"><en><![CDATA[High (30%)]]></en><de><![CDATA[Hoch (30%)]]></de><fr><![CDATA[Élevé (30%)]]></fr><es><![CDATA[Alta (30%)]]></es><it><![CDATA[Alta (30%)]]></it><pl><![CDATA[Wysoki (30%)]]></pl><uk><![CDATA[Високий (30%)]]></uk> <cz>[EN] <![CDATA[High (30%)]]></cz> <br>[EN] <![CDATA[High (30%)]]></br> <ru>[EN] <![CDATA[High (30%)]]></ru> - <da>[EN] <![CDATA[High (30%)]]></da> + <da><![CDATA[Høj (30 %)]]></da> </text> <!-- Notifications --> @@ -330,7 +338,7 @@ Funkcje: <cz>[EN] <![CDATA[Notifications]]></cz> <br>[EN] <![CDATA[Notifications]]></br> <ru>[EN] <![CDATA[Notifications]]></ru> - <da>[EN] <![CDATA[Notifications]]></da> + <da><![CDATA[Notifikationer]]></da> </text> <text name="tm_notifications_long"> <en><![CDATA[Show in-game notifications when taxes are deducted or returns are paid]]></en> @@ -344,7 +352,7 @@ Funkcje: <cz>[EN] <![CDATA[Show in-game notifications when taxes are deducted or returns are paid]]></cz> <br>[EN] <![CDATA[Show in-game notifications when taxes are deducted or returns are paid]]></br> <ru>[EN] <![CDATA[Show in-game notifications when taxes are deducted or returns are paid]]></ru> - <da>[EN] <![CDATA[Show in-game notifications when taxes are deducted or returns are paid]]></da> + <da><![CDATA[Vis notifikationer i spillet, når skatter fratrækkes eller refusioner udbetales]]></da> </text> <!-- Show HUD --> @@ -360,7 +368,7 @@ Funkcje: <cz>[EN] <![CDATA[Show Tax HUD]]></cz> <br>[EN] <![CDATA[Show Tax HUD]]></br> <ru>[EN] <![CDATA[Show Tax HUD]]></ru> - <da>[EN] <![CDATA[Show Tax HUD]]></da> + <da><![CDATA[Vis skatte-HUD]]></da> </text> <text name="tm_show_hud_long"> <en><![CDATA[Show the Tax HUD overlay with current rate, statistics and recent activity (toggle with T key)]]></en> @@ -374,7 +382,7 @@ Funkcje: <cz>[EN] <![CDATA[Show the Tax HUD overlay with current rate, statistics and recent activity (toggle with T key)]]></cz> <br>[EN] <![CDATA[Show the Tax HUD overlay with current rate, statistics and recent activity (toggle with T key)]]></br> <ru>[EN] <![CDATA[Show the Tax HUD overlay with current rate, statistics and recent activity (toggle with T key)]]></ru> - <da>[EN] <![CDATA[Show the Tax HUD overlay with current rate, statistics and recent activity (toggle with T key)]]></da> + <da><![CDATA[Vis skatte-HUD-overlayet med aktuel sats, statistik og seneste aktivitet (slå til/fra med T-tasten)]]></da> </text> <!-- HUD toggle key binding display name --> @@ -386,22 +394,36 @@ Funkcje: <it><![CDATA[Attiva/disattiva HUD fiscale]]></it> <pl><![CDATA[Przełącz HUD podatkowy]]></pl> <uk><![CDATA[Перемкнути HUD податків]]></uk> - <cz>[EN] <![CDATA[Toggle Tax HUD]]></cz> <br>[EN] <![CDATA[Toggle Tax HUD]]></br> <ru>[EN] <![CDATA[Toggle Tax HUD]]></ru> - <da>[EN] <![CDATA[Toggle Tax HUD]]></da> + <da><![CDATA[Slå skatte-HUD til/fra]]></da> + </text> + <text name="input_TM_HUD_DRAG"> + <en>Tax HUD Edit Mode</en> + <de>Steuer-HUD-Bearbeitungsmodus</de> + <fr>[EN] Tax HUD Edit Mode</fr> + <es>[EN] Tax HUD Edit Mode</es> + <it>[EN] Tax HUD Edit Mode</it> + <pl>[EN] Tax HUD Edit Mode</pl> + <uk>[EN] Tax HUD Edit Mode</uk> + <cz>[EN] Tax HUD Edit Mode</cz> + <br>[EN] Tax HUD Edit Mode</br> + <ru>[EN] Tax HUD Edit Mode</ru> + <da>Skat HUD-redigeringstilstand</da> </text> </l10n> <actions> <action name="TM_TOGGLE_HUD" category="ONFOOT" /> + <action name="TM_HUD_DRAG" category="ONFOOT" /> </actions> <inputBinding> <actionBinding action="TM_TOGGLE_HUD"> <binding device="KB_MOUSE_DEFAULT" input="KEY_T" /> </actionBinding> + <actionBinding action="TM_HUD_DRAG" /> </inputBinding> </modDesc> \ No newline at end of file diff --git a/src/integrations/CropStressIrrigationExpense.lua b/src/integrations/CropStressIrrigationExpense.lua new file mode 100644 index 0000000..c894fa0 --- /dev/null +++ b/src/integrations/CropStressIrrigationExpense.lua @@ -0,0 +1,119 @@ +-- ===================================================================== +-- CropStressIrrigationExpense.lua (SCS-011) +-- Mirror SeasonalCropStress irrigation operating cost into the TaxMod +-- ledger as a deductible expense line, once per in-game day. +-- +-- READ-ONLY over the SCS B3.2b companion facade (getIrrigationSystems / +-- getIrrigationCostsEnabled on g_currentMission.cropStressManager). It moves +-- NO money: recordExpense is bookkeeping only. SCS itself already deducts the +-- operating cost each active hour (FinanceIntegration), so this line reflects a +-- charge the player is already paying -- it never adds a second one, so there +-- is no double-charge (the CC-1 decision). +-- +-- Run-time is DERIVED from each system's schedule (there is no runtime counter +-- in the snapshot), matching the same day-of-week + window logic SCS uses to +-- flip a system active (IrrigationManager:hourlyScheduleCheck). This is an +-- accepted approximation for a bookkeeping mirror: it does not see a system +-- that lost its water source or was placed mid-day. Gated on SCS's own +-- costsEnabled so it never reports a cost the player has switched off. +-- ===================================================================== + +CropStressIrrigationExpense = {} + +-- Optional daily depreciation per scheduled system. 0 = off. A real schedule +-- needs a per-system capital value + useful life that the facade snapshot does +-- not carry, and the number is a balance-pass call, so it ships neutral (0) +-- and wired, not fabricated (brief OI-2 / OI-3). +CropStressIrrigationExpense.DAILY_DEPRECIATION_PER_SYSTEM = 0 + +-- Day-of-week index 1..7, matching SCS IrrigationManager:hourlyScheduleCheck +-- (env.currentDayInPeriod, fallback (currentDay % 7) + 1, clamped) so our +-- accrual lands on the same days SCS actually runs a system. +local function dayOfWeekIndex(env) + if env == nil then return 1 end + local dow = env.currentDayInPeriod + if dow == nil then + local currentDay = env.currentDay or env.currentMonotonicDay or 0 + dow = (currentDay % 7) + 1 + end + if dow < 1 or dow > 7 then dow = 1 end + return dow +end + +-- Scheduled active hours for one system today, from its schedule window, using +-- the same wrap-around math as SCS. 0 when the system does not run today. +local function scheduledHoursToday(system, dow) + local sched = system.schedule + if sched == nil or sched.activeDays == nil then return 0 end + if sched.activeDays[dow] ~= true then return 0 end + local s = sched.startHour + local e = sched.endHour + if type(s) ~= "number" or type(e) ~= "number" then return 0 end + if s <= e then return e - s end + return (24 - s) + e -- wrap-around (e.g. 23 -> 2) +end + +local function resolveLabel(key, fallback) + if g_i18n ~= nil then + local t = g_i18n:getText(key) + if t ~= nil and t ~= key then return t end + end + return fallback +end + +-- Accrue one in-game day of irrigation operating cost into the TaxMod ledger. +-- Called once per day-change from main.lua's daily tick. Server-only, neutral +-- (records nothing) whenever SCS is absent, costs are off, or no system runs. +function CropStressIrrigationExpense.accrueDaily(taxManager) + if taxManager == nil then return end + if g_currentMission == nil or not g_currentMission:getIsServer() then return end + + -- SCS facade handle (mission-bound; never the bare global as the primary). + local csm = g_currentMission.cropStressManager or g_cropStressManager + if csm == nil or type(csm.getIrrigationSystems) ~= "function" then return end + + -- Respect SCS's own irrigation-costs toggle: if the player disabled it, SCS + -- charges nothing, so we must mirror nothing. Getter is optional (older SCS + -- builds lack it); default to enabled when it is not present. + if type(csm.getIrrigationCostsEnabled) == "function" then + local ok, enabled = pcall(csm.getIrrigationCostsEnabled, csm) + if ok and enabled == false then return end + end + + local ok, systems = pcall(csm.getIrrigationSystems, csm) + if not ok or type(systems) ~= "table" then return end + + local env = g_currentMission.environment + local dow = dayOfWeekIndex(env) + + local operating = 0 + local scheduledSystems = 0 + for _, sys in ipairs(systems) do + local hours = scheduledHoursToday(sys, dow) + if hours > 0 then + scheduledSystems = scheduledSystems + 1 + if type(sys.operationalCostPerHour) == "number" then + operating = operating + sys.operationalCostPerHour * hours + end + end + end + + if operating <= 0 and scheduledSystems == 0 then return end + + -- Attribute to the local player's farm -- the same farm SCS's + -- FinanceIntegration charges (it deducts from the local player's owner farm), + -- via TaxMod's own idiom so the mirror never disagrees with what SCS moved. + local farmId = g_currentMission.getFarmId and g_currentMission:getFarmId() + if type(farmId) ~= "number" or farmId <= 0 then return end + + if operating > 0 then + taxManager.recordExpense(farmId, -operating, + resolveLabel("tm_irrigation_operations", "Irrigation Operations")) + end + + local depr = CropStressIrrigationExpense.DAILY_DEPRECIATION_PER_SYSTEM + if depr > 0 and scheduledSystems > 0 then + taxManager.recordExpense(farmId, -(depr * scheduledSystems), + resolveLabel("tm_irrigation_assets", "Irrigation Assets")) + end +end diff --git a/src/integrations/TaxMasterHUDBridge.lua b/src/integrations/TaxMasterHUDBridge.lua new file mode 100644 index 0000000..641f3d1 --- /dev/null +++ b/src/integrations/TaxMasterHUDBridge.lua @@ -0,0 +1,63 @@ +-- ========================================================= +-- FS25 Tax Mod - TaxMasterHUDBridge +-- ========================================================= +-- Author: TisonK +-- ========================================================= +-- Optional bridge to FS25_MasterHUD (bedrock). Strictly delegate-when-present, +-- mirroring SoilFertilizer / IncomeMod / WorkerCosts: +-- * MasterHUD installed -> the tax HUD overlay registers as a subscribe() +-- element so MasterHUD owns the single suspend-aware draw loop and cross-mod +-- ordering; the mod's own FSBaseMission.draw hook stands down when active. +-- * MasterHUD absent -> the mod's own draw hook runs the exact same body. +-- +-- drawStack() is the single source of the draw body, shared with the fallback +-- hook so the two paths can never diverge. It reproduces the own hook's guard +-- (settings.showHUD) and then calls TaxHUD:draw(), which itself self-guards on +-- getIsClient / GUI-visible / large-map / visible, so calling it every frame is +-- a no-op when hidden. +-- +-- Mouse routing stays on the mod's own addModEventListener handler (MasterHUD +-- owns draw ordering + suspend, not input). The settings UI injected into the +-- General Settings page is g_gui-managed and stays there. +-- +-- The cross-mod handle is g_currentMission.masterHUD (published in Mission00.load). +-- ========================================================= + +TaxMasterHUDBridge = {} + +TaxMasterHUDBridge.HUD_ID = "TaxMod_HUD" +TaxMasterHUDBridge.active = false -- MasterHUD present and we registered + +-- The tax HUD draw body. Resolves the manager from the canonical global so it +-- can be driven either by MasterHUD's loop or by the mod's own draw hook. +-- Reproduces the own hook's `taxHUD and settings.showHUD` guard. +function TaxMasterHUDBridge.drawStack() + local tm = g_TaxManager + if tm ~= nil and tm.taxHUD ~= nil and tm.settings ~= nil and tm.settings.showHUD then + tm.taxHUD:draw() + end +end + +-- Register with MasterHUD if present. Called from onMissionLoaded. +function TaxMasterHUDBridge.register(tm) + TaxMasterHUDBridge.active = false + + local hud = (g_currentMission ~= nil and g_currentMission.masterHUD) or g_masterHUD + if hud == nil then + Logging.info("Tax Mod: MasterHUD not detected; tax HUD uses its own draw hook") + return + end + + local ok, err = pcall(function() + hud:subscribe(TaxMasterHUDBridge.HUD_ID, { + draw = TaxMasterHUDBridge.drawStack, + }) + end) + + if ok then + TaxMasterHUDBridge.active = true + Logging.info("Tax Mod: Registered tax HUD with MasterHUD (single draw loop + menu-suspend)") + else + Logging.warning("Tax Mod: MasterHUD registration failed: %s (using own draw hook)", tostring(err)) + end +end diff --git a/src/integrations/TaxStateLedgerBridge.lua b/src/integrations/TaxStateLedgerBridge.lua new file mode 100644 index 0000000..a6473c1 --- /dev/null +++ b/src/integrations/TaxStateLedgerBridge.lua @@ -0,0 +1,98 @@ +-- ========================================================= +-- FS25 Tax Mod - TaxStateLedgerBridge +-- ========================================================= +-- Author: TisonK +-- ========================================================= +-- Optional bridge to FS25_StateLedger (bedrock). Strictly +-- delegate-when-present, mirroring SoilFertilizer / IncomeMod / WorkerCosts: +-- +-- * StateLedger installed -> the shared master save file is the LOAD source +-- of truth for the Tax Mod STATE: the running stats (totals, days taxed, +-- the annual accumulator + last-paid year) and the companion recordExpense +-- ledger (per-farm credit/debit + recent entries). The mod's own +-- modSettings/FS25_TaxMod.xml is still written every save as the standalone +-- safety copy, so removing the ledger later never loses data. +-- * StateLedger absent -> nothing changes; the own XML is primary. +-- +-- ONE module: TaxMod_Data. Settings are NOT a StateLedger module here. +-- Settings persistence + MP sync are SettingsHub's domain, and the mod keeps +-- its own XML as the settings fallback; a third settings copy in a per-mod +-- ledger module would just be a conflicting duplicate. This matches +-- SoilFertilizer / IncomeMod, whose ledger modules carry state, not settings. +-- +-- Timing note (same as IncomeMod / WorkerCosts): the mod loads its state in +-- onMissionLoaded (= loadMission00Finished), the same phase StateLedger parses +-- its file in. So register() force-triggers the idempotent parseFile after +-- registering, guaranteeing deserialize has delivered before the caller reads +-- hasState(), independent of mod load order. +-- +-- The cross-mod handle is g_currentMission.stateLedger (published in Mission00.load). +-- ========================================================= + +TaxStateLedgerBridge = {} + +-- LOCKED persistence key. Never renamed after first persist. +TaxStateLedgerBridge.MODULE_ID = "TaxMod_Data" + +TaxStateLedgerBridge.active = false -- ledger present and we registered +TaxStateLedgerBridge.delivered = false -- deserialize has fired (once) +TaxStateLedgerBridge.pendingState = nil -- cached table (nil = new save / no block) + +-- True when the ledger is the state load source of truth this session. +function TaxStateLedgerBridge.hasState() + return TaxStateLedgerBridge.active + and TaxStateLedgerBridge.delivered + and TaxStateLedgerBridge.pendingState ~= nil +end + +-- Apply the cached state block into the tax mod. Returns true on apply. +function TaxStateLedgerBridge.applyState(tm) + if tm == nil or type(tm.applyState) ~= "function" then return false end + if type(TaxStateLedgerBridge.pendingState) ~= "table" then return false end + return tm.applyState(TaxStateLedgerBridge.pendingState) +end + +-- Register with StateLedger if present. Called from onMissionLoaded, after +-- loadSettings() has imported the own-XML safety copy. +function TaxStateLedgerBridge.register(tm) + TaxStateLedgerBridge.active = false + TaxStateLedgerBridge.delivered = false + TaxStateLedgerBridge.pendingState = nil + + local ledger = (g_currentMission ~= nil and g_currentMission.stateLedger) or g_stateLedger + if ledger == nil then + Logging.info("Tax Mod: StateLedger not detected; state uses its own FS25_TaxMod.xml") + return + end + if tm == nil or type(tm.serializeState) ~= "function" then return end + + local ok, err = pcall(function() + ledger:registerModule(TaxStateLedgerBridge.MODULE_ID, { + serialize = function() + return tm.serializeState() + end, + deserialize = function(data) + TaxStateLedgerBridge.delivered = true + TaxStateLedgerBridge.pendingState = data -- nil on a brand-new save + end, + }) + end) + + if not ok then + Logging.warning("Tax Mod: StateLedger registration failed: %s (falling back to own XML)", tostring(err)) + return + end + + TaxStateLedgerBridge.active = true + + -- Force the (idempotent) parse now so deserialize has delivered before the + -- caller checks hasState() a few lines later, independent of which mod's + -- loadMission00Finished handler ran first. A no-op if StateLedger already + -- parsed (it loaded first). + if ledger.parseFile ~= nil then + pcall(function() ledger:parseFile() end) + end + + Logging.info("Tax Mod: Registered with StateLedger as '%s' (FS25_TaxMod.xml kept as safety copy)", + TaxStateLedgerBridge.MODULE_ID) +end diff --git a/src/settings/SettingsHubBridge.lua b/src/settings/SettingsHubBridge.lua new file mode 100644 index 0000000..326d195 --- /dev/null +++ b/src/settings/SettingsHubBridge.lua @@ -0,0 +1,64 @@ +-- ========================================================= +-- FS25 Tax Mod - SettingsHub bridge +-- ========================================================= +-- Author: TisonK +-- ========================================================= +-- Optional bridge to FS25_SettingsHub. Safe if SettingsHub is not +-- installed (register() just no-ops). Purpose: let the FarmTablet +-- System Settings app list Tax Mod's settings. +-- +-- FS25TaxMod.settings (a plain table, persisted by the mod's own save +-- path) stays the source of truth. This mirrors current values into +-- SettingsHub for display; the tablet app is read-only for now, so +-- applyChange only sets the value back on that table. +-- ========================================================= + +TaxSettingsHubBridge = {} + +local function applyChange(key, value) + local tm = g_TaxManager + if tm == nil or tm.settings == nil then return end + tm.settings[key] = value + if type(tm.saveSettings) == "function" then tm.saveSettings() end +end + +function TaxSettingsHubBridge.register(tm) + -- The reliable cross-mod handle is g_currentMission.settingsHub (the same one + -- FarmTablet reads). The bare g_settingsHub global is only visible inside + -- SettingsHub's own mod environment, so it reads back nil from here. + local hub = (g_currentMission ~= nil and g_currentMission.settingsHub) or g_settingsHub + if hub == nil then return end + if tm == nil or tm.settings == nil then return end + local s = tm.settings + + local defs = { + { id = "enabled", type = "bool", default = s.enabled, adminOnly = true, label = "Tax Mod Enabled" }, + { id = "taxRate", type = "enum", default = s.taxRate, adminOnly = true, values = { "low", "medium", "high" }, label = "Tax Rate (low/medium/high)" }, + { id = "annualTaxRate", type = "float", default = s.annualTaxRate, adminOnly = true, min = 0, max = 1, label = "Annual Tax Rate" }, + { id = "returnPercentage", type = "int", default = s.returnPercentage, adminOnly = true, min = 0, max = 100, label = "Return Percentage" }, + { id = "minimumBalance", type = "int", default = s.minimumBalance, adminOnly = true, min = 0, max = 100000000, label = "Minimum Balance" }, + { id = "showNotification", type = "bool", default = s.showNotification, adminOnly = false, label = "Show Notifications" }, + { id = "showStatistics", type = "bool", default = s.showStatistics, adminOnly = false, label = "Show Statistics" }, + { id = "showHUD", type = "bool", default = s.showHUD, adminOnly = false, label = "Show HUD" }, + { id = "debugLevel", type = "int", default = s.debugLevel, adminOnly = false, min = 0, max = 3, label = "Debug Level" }, + } + + local ok, err = pcall(function() + hub:registerModule("TaxMod", { + adminSettings = defs, + onChange = function(key, value, playerId) applyChange(key, value) end, + -- We own our persistence (the mod's own save path writes FS25_TaxMod.xml) and + -- load it before this registration runs, so the hub must mirror-for-display + -- only: never restore its own stale copy and replay it back through onChange on + -- load. Without this the hub can push a stale value over our real setting every + -- load (the SoilFertilizer enabled=false reset-on-load bug class). + selfPersisted = true, + }) + end) + + if ok then + Logging.info("Tax Mod: Registered with SettingsHub (%d setting(s))", #defs) + else + Logging.warning("Tax Mod: SettingsHub registration failed: %s", tostring(err)) + end +end diff --git a/src/ui/TaxHUD.lua b/src/ui/TaxHUD.lua index 9031263..13b4e89 100644 --- a/src/ui/TaxHUD.lua +++ b/src/ui/TaxHUD.lua @@ -252,13 +252,15 @@ end function TaxHUD:onMouseEvent(posX, posY, isDown, isUp, button, eventUsed) if not self.visible then return false end + -- RMB: exit edit mode only. Edit mode is entered exclusively via the + -- TM_HUD_DRAG key binding — never via right-click — so RMB is never + -- consumed during normal play, preserving CoursePlay, AutoDrive, etc. if isDown and button == 3 then if self.editMode then self:exitEditMode() - elseif self:isPointerOverHUD(posX, posY) then - self:enterEditMode() + return true end - return true + return false end if not self.editMode then return false end @@ -383,8 +385,8 @@ function TaxHUD:drawPanel() local histCount = math.min(#self.taxHistory, TaxHUD.MAX_HISTORY_ROWS) - -- Row count: title + rate + min-balance + annual-accumulated + projected + next-event + totals(2) + history-header + hist-rows + hint = 10 fixed + max(histCount,1) variable - local nRows = 11 + math.max(histCount - 1, 0) + -- Row count: title + rate + min-balance + annual-accumulated + projected + next-event + totals(2) + ledger + history-header + hist-rows + hint = 11 fixed + max(histCount,1) variable + local nRows = 12 + math.max(histCount - 1, 0) local nDividers = 3 local bgH = pad * 2 + nRows * lh + nDividers * (0.004 * sc) local bgX = x - pad @@ -537,6 +539,24 @@ function TaxHUD:drawPanel() renderText(x + w, cy - tsSmall, tsSmall, "+" .. retFmt) cy = cy - lh + -- Companion ledger (recordExpense credits/debits for the local farm) + local ledgerCredit, ledgerDebit = 0, 0 + local ledgerFarms = taxMod.ledger and taxMod.ledger.farms + if ledgerFarms ~= nil and g_currentMission ~= nil and g_currentMission.getFarmId ~= nil then + local farmLedger = ledgerFarms[g_currentMission:getFarmId()] + if farmLedger ~= nil then + ledgerCredit = farmLedger.creditTotal or 0 + ledgerDebit = farmLedger.debitTotal or 0 + end + end + setTextAlignment(RenderText.ALIGN_LEFT) + setTextColor(self.COLORS.LABEL[1], self.COLORS.LABEL[2], self.COLORS.LABEL[3], self.COLORS.LABEL[4]) + renderText(x, cy - tsSmall, tsSmall, "Tracked credits/debits:") + setTextAlignment(RenderText.ALIGN_RIGHT) + setTextColor(self.COLORS.VALUE[1], self.COLORS.VALUE[2], self.COLORS.VALUE[3], self.COLORS.VALUE[4]) + renderText(x + w, cy - tsSmall, tsSmall, "+" .. self:formatMoney(ledgerCredit) .. " / -" .. self:formatMoney(ledgerDebit)) + cy = cy - lh + -- Divider self:divider(bgX, cy + lh * 0.35, bgW, sc) cy = cy - 0.004 * sc