feat(status): statusvindue på skærmen slås til med en tast

This commit is contained in:
2026-09-30 14:52:35 +02:00
parent 131b1ad10c
commit ba2a3e94f1
11 changed files with 702 additions and 1 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ står i [docs/BRUGERMANUAL.md](docs/BRUGERMANUAL.md) (dansk) og [docs/USER_MANUA
## 1.61 — Produktionskæder
- **1.61.0** Produktionskæder: slå et produkt til i en fabrik, og moden holder det kørende helt tilbage til marken. Kæden regner baglæns gennem dine egne fabrikker og stalde til afgrøderne og ser 12 måneder frem. Ny fane **Kæder** med træ pr. rod-produktion, farvet status, fold ud/ind og **[Pause]**/**[Start]**. Marker markeret **Fri til kæden** skifter afgrøde automatisk (højst 2 i minuttet); andre marker får forslag med **[Godkend]**/**[Afvis]** (højst 3 pr. afgrøde) i fanen og som ét samlet punkt i **Kræver handling**. En afgrøde, du selv har valgt, ændres aldrig. Mellemled tændes og slukkes kun igen, hvis kæden tændte dem. Flows **Kæde: <vare> → <bygning>** oprettes med en ledig pool-traktor og tipvogn (ellers stoppet: *mangler traktor/vogn*; paller altid stoppet) og stoppes, ikke slettes, når kæden slås fra. Dine egne tændte produktioner bruges som kilde først; slukker du en produktion, tænder kæden den ikke igen, før du tænder en ny. Stoppede kædeflows slippes af kæden. Ingen handlinger de første 2 minutter, ingen markskift før alle marker er scannet, og intet slukkes ved delvise læsefejl. Begrænsninger: intet køb, ingen dyrekøb, flerårige afgrøder og græsmarker skiftes aldrig, grundudbytte uden gødningsbonus. **Vigtigt:** hver produktion, du tænder i en fabrik, prøver moden at forsyne — tænd kun det, du vil have produceret; sluk produktionen eller tryk [Pause] for at stoppe kæden. Manglende led forklares i klart sprog med råd (*Ingen af dine fabrikker laver Mel — byg en eller køb*, *Mølle kan kun lave 800 af 1.0k/md*, *Dine stalde leverer ikke nok Mælk …*, *… er slukket af dig*, *Kredsløb omkring …*): som rød tekst under varen i fanen Kæder, som én rød linje pr. kæde i **Kræver handling** (højst 2; klik åbner kæden foldet ud) og som engangsbesked med kædens navn foran, når et nyt led mangler (ikke i opstartsfristen eller mens markerne scannes). Manual og ordliste opdateret.
- **1.61.0** Produktionskæder: slå et produkt til i en fabrik, og moden holder det kørende helt tilbage til marken. Kæden regner baglæns gennem dine egne fabrikker og stalde til afgrøderne og ser 12 måneder frem. Ny fane **Kæder** med træ pr. rod-produktion, farvet status, fold ud/ind og **[Pause]**/**[Start]**. Marker markeret **Fri til kæden** skifter afgrøde automatisk (højst 2 i minuttet); andre marker får forslag med **[Godkend]**/**[Afvis]** (højst 3 pr. afgrøde) i fanen og som ét samlet punkt i **Kræver handling**. En afgrøde, du selv har valgt, ændres aldrig. Mellemled tændes og slukkes kun igen, hvis kæden tændte dem. Flows **Kæde: <vare> → <bygning>** oprettes med en ledig pool-traktor og tipvogn (ellers stoppet: *mangler traktor/vogn*; paller altid stoppet) og stoppes, ikke slettes, når kæden slås fra. Dine egne tændte produktioner bruges som kilde først; slukker du en produktion, tænder kæden den ikke igen, før du tænder en ny. Stoppede kædeflows slippes af kæden. Ingen handlinger de første 2 minutter, ingen markskift før alle marker er scannet, og intet slukkes ved delvise læsefejl. Begrænsninger: intet køb, ingen dyrekøb, flerårige afgrøder og græsmarker skiftes aldrig, grundudbytte uden gødningsbonus. **Vigtigt:** hver produktion, du tænder i en fabrik, prøver moden at forsyne — tænd kun det, du vil have produceret; sluk produktionen eller tryk [Pause] for at stoppe kæden. Manglende led forklares i klart sprog med råd (*Ingen af dine fabrikker laver Mel — byg en eller køb*, *Mølle kan kun lave 800 af 1.0k/md*, *Dine stalde leverer ikke nok Mælk …*, *… er slukket af dig*, *Kredsløb omkring …*): som rød tekst under varen i fanen Kæder, som én rød linje pr. kæde i **Kræver handling** (højst 2; klik åbner kæden foldet ud) og som engangsbesked med kædens navn foran, når et nyt led mangler (ikke i opstartsfristen eller mens markerne scannes). Nyt **statusvindue** på skærmen (i stil med Simple Inspector), som slås til/fra med **venstre Alt + S** (kan ændres under Styring; virker til fods og i køretøjer): øverst til højre under info-boksen vises flow-traktorernes handling og fyld, markarbejde i gang, kæder med manglende led og advarsler (køretøjer der har stået stille over 2 min, ventende kædeforslag) — højst 18 linjer, opdateres hvert sekund, skjules ved menu/dialog, skjult HUD og søvn; tændt/slukket gemmes pr. bruger i `modSettings/FS25_ADSmartPickup/status.xml`. Manual og ordliste opdateret.
## 1.60 — "Kræver handling" → flow, menuplacering, flow-vogne i guiden
+9
View File
@@ -562,6 +562,15 @@ function ADFlowWagons.isEmptying(vehicle)
return emptying[vehicle] ~= nil
end
-- Kun læsning (statusvinduet): hvad flowets vogn-logik har traktoren i gang med -> "fetching"|"waiting"|"emptying"|"homing"|nil
function ADFlowWagons.statusOf(vehicle)
if fetching[vehicle] ~= nil then return "fetching" end
if waiting[vehicle] ~= nil then return "waiting" end
if emptying[vehicle] ~= nil then return "emptying" end
if homing[vehicle] ~= nil then return "homing" end
return nil
end
function ADFlowWagons.sendHome(adEnv, run)
for _, vehicleId in ipairs(run.vehicleIds or {}) do
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
+10
View File
@@ -1335,6 +1335,16 @@ function ADSmartPickup:update(dt)
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke gendanne varevalg: %s", tostring(err))
supplyState = setmetatable({}, {__mode = "k"})
end
if ADStatusHud ~= nil then pcall(ADStatusHud.update, dt, getAutoDriveEnv()) end
end
-- Statusvinduet på skærmen (adStatusHud.lua): tegner kun den cachede tekst.
function ADSmartPickup:draw()
if ADStatusHud ~= nil then pcall(ADStatusHud.draw) end
end
function ADSmartPickup:deleteMap()
if ADStatusHud ~= nil then pcall(ADStatusHud.delete) end
end
addModEventListener(ADSmartPickup)
+447
View File
@@ -0,0 +1,447 @@
-- AD Status Hud
-- Statusvinduet på skærmen (i stil med Simple Inspector): slås til/fra med input-handlingen SPU_TOGGLE_STATUS
-- (standard venstre Alt + S). Øjebliksbilledet bygges af modens egne statusfunktioner (samme som fanerne) højst
-- hvert sekund i update() og kun mens vinduet er tændt; draw() tegner kun de cachede linjer og bredder.
-- Synligheden gemmes pr. bruger i modSettings/FS25_ADSmartPickup/status.xml (standard skjult).
-- Kører kun på en klient (ikke dedikeret server). Alle spilkald er beskyttet; fejl logges én gang pr. nøgle.
ADStatusHud = {}
ADStatusHud.LOG_PREFIX = "ADSmartPickup: status: "
ADStatusHud.REFRESH_MS = 1000
ADStatusHud.STUCK_WARN_MS = 120000 -- stået stille så længe (uden at Smart Pickup holder den) = advarsel
ADStatusHud.TEXT_SIZE_PX = 14
ADStatusHud.MARGIN_PX = 8
ADStatusHud.SCREEN_OFFSET_PX = 16 -- afstand til skærmens højre kant
ADStatusHud.TOP_Y = 1 - 0.12 -- øverste kant (normaliseret); under spillets info-boks
ADStatusHud.SETTINGS_DIR = "modSettings/FS25_ADSmartPickup/"
ADStatusHud.SETTINGS_FILE = "status.xml"
ADStatusHud.SETTINGS_ROOT = "adSmartPickupStatus"
ADStatusHud.HEADER_COLOR = {1, 1, 1, 1}
ADStatusHud.visible = false
ADStatusHud.lines = {} -- {{text, level, header, color, width}}
ADStatusHud.layout = nil -- {x, y, width, height, textSize, marginX, marginY, padding}
local timerMs = 0
local settingsLoaded = false
local overlays = nil
local loggedErrors = {}
local registerToggleActionRef -- sat nederst (tastens handler til krogen)
-- ---- hjælpere ----
local function logOnce(key, err)
if loggedErrors[key] then return end
loggedErrors[key] = true
if Logging ~= nil and Logging.warning ~= nil then
Logging.warning(ADStatusHud.LOG_PREFIX .. "%s fejlede: %s", tostring(key), tostring(err))
end
end
local function guarded(key, fn, ...)
local ok, result = pcall(fn, ...)
if not ok then
logOnce(key, result)
return nil
end
return result
end
local function textOf(key)
if g_i18n == nil then return key end
local ok, text = pcall(g_i18n.getText, g_i18n, key)
return ok and text or key
end
function ADStatusHud.isClient()
local mission = g_currentMission
if mission == nil or mission.getIsClient == nil then return false end
local ok, isClient = pcall(mission.getIsClient, mission)
return ok and isClient == true
end
-- Fanernes egne tekst-/statusfunktioner ligger som metoder på SmartPickupFrame og bruger kun self.i18n:
-- en let "frame" med spillets i18n genbruger dem uden at røre menuen.
local function frameProxy()
if SmartPickupFrame == nil then return nil end
return setmetatable({i18n = g_i18n, lastStartFailed = {}}, {__index = SmartPickupFrame})
end
local function fillTitle(fillType)
if fillType == nil or g_fillTypeManager == nil then return "?" end
local title = g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(fillType)
or g_fillTypeManager:getFillTypeNameByIndex(fillType)
return tostring(title or "?")
end
-- ---- øjebliksbillede ----
local WAGON_ACTIONS = {
fetching = {key = "spu_status_act_fetchWagon", level = "waiting"},
waiting = {key = "spu_status_act_waitWagon", level = "waiting"},
emptying = {key = "spu_status_act_emptying", level = "running"},
homing = {key = "spu_status_act_homing", level = "running"},
}
-- Traktorens handling som kort tekst + niveau (ADRunsController.getVehicleActivity som Flows-fanen).
local function vehicleAction(adEnv, vehicle)
local wagonState = ADFlowWagons ~= nil and ADFlowWagons.statusOf ~= nil and ADFlowWagons.statusOf(vehicle) or nil
if wagonState ~= nil and WAGON_ACTIONS[wagonState] ~= nil then
return textOf(WAGON_ACTIONS[wagonState].key), WAGON_ACTIONS[wagonState].level
end
local activity = ADRunsController.getVehicleActivity(adEnv, vehicle)
local place = tostring(activity.place or "?")
if activity.kind == "stopped" then
return textOf("spu_activity_stopped"), "stopped"
elseif activity.kind == "waiting" then
return string.format(textOf("spu_status_act_waiting"), place), "waiting"
elseif activity.kind == "holding" then
return string.format(textOf("spu_status_act_holding"), place), "waiting"
elseif activity.kind == "empty" and activity.fillType ~= nil then
return string.format(textOf("spu_status_act_fetch"), fillTitle(activity.fillType), place), "running"
end
return string.format(textOf("spu_status_act_driving"), place), "running"
end
-- Fyld i % af hele vogntoget (AutoDrives egne opgørelser, som kildevalget bruger); nil uden kapacitet.
local function fillPercent(adEnv, vehicle)
local autoDrive = adEnv.AutoDrive
if autoDrive == nil or autoDrive.getAllUnits == nil or autoDrive.getAllFillLevels == nil then return nil end
local level, capacity = autoDrive.getAllFillLevels(autoDrive.getAllUnits(vehicle))
if type(level) ~= "number" or type(capacity) ~= "number" or capacity <= 0 then return nil end
return math.max(0, math.min(100, level / capacity * 100))
end
local function farmVehiclesById(adEnv)
local byId = {}
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
local id = ADRunsController.getVehicleId(vehicle)
if id ~= nil then byId[id] = vehicle end
end
return byId
end
-- Én linje pr. traktor i et kørende flow (flows der kan stoppes: startet, eller holder vogn/henter).
function ADStatusHud.flowEntries(adEnv, proxy)
local entries = {}
if ADRunsController == nil or ADRunsController.store == nil or ADFlowView == nil then return entries end
local byId = farmVehiclesById(adEnv)
for _, run in ipairs(ADFlowView.orderedRuns(ADRunsController.store.runs or {})) do
local status = ADRunsController.getRunStatus(adEnv, run)
if status.canStop then
local runLevel = proxy ~= nil and proxy.flowLevel ~= nil and guarded("flowLevel", proxy.flowLevel, proxy, adEnv, run) or nil
for _, vehicleId in ipairs(run.vehicleIds or {}) do
local vehicle = byId[vehicleId]
if vehicle ~= nil then
local action, level = vehicleAction(adEnv, vehicle)
if runLevel == "error" then level = "error" end
table.insert(entries, {vehicle = tostring(ADRuns.vehicleLabel(vehicle)), action = action,
fillPercent = guarded("fillPercent", fillPercent, adEnv, vehicle), level = level})
end
end
end
end
return entries
end
-- Én linje pr. mark med aktivt markarbejde (samme statustekst og farve som Marker-fanen).
function ADStatusHud.fieldEntries(proxy)
local entries = {}
if ADFields == nil or ADFieldJobs == nil or ADFieldModel == nil or proxy == nil then return entries end
for _, info in ipairs(ADFields.list()) do
local field = ADFieldJobs.getField(info.id)
if field ~= nil and (ADFieldModel.isWorking(field) or ADFieldJobs.awaitingPlayer(info.id) ~= nil) then
local names = {}
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(info.id)) do
table.insert(names, ADRuns.vehicleLabel(assignment.vehicle))
end
table.insert(entries, {
fieldId = info.id,
work = proxy:fieldStatusText(field, info, true),
vehicle = #names > 0 and ADFlowView.joinCounted(names, ", ", nil) or nil,
progress = SmartPickupFrame.monthCpProgress ~= nil and guarded("fieldProgress", SmartPickupFrame.monthCpProgress, info.id) or nil,
level = proxy:fieldLevel(info.id),
})
end
end
return entries
end
-- Kæder med manglende led + ventende forslag (samme punkter og tekster som "Kræver handling").
function ADStatusHud.chainEntries(proxy)
local chains, suggestions = {}, {}
local result = {chains = chains, suggestions = suggestions}
if ADChain == nil or ADChain.last == nil or ADStorageStats == nil or ADStorageStats.chainEntries == nil or proxy == nil then
return result
end
for _, entry in ipairs(ADStorageStats.chainEntries(ADChain.last)) do
if entry.kind == "chainProblem" then
table.insert(chains, {text = proxy:alertText(entry), level = "error"})
elseif entry.kind == "chainSuggest" then
table.insert(suggestions, {text = proxy:alertText(entry), level = "waiting"})
end
end
return result
end
-- Gårdens køretøjer der har stået stille over grænsen, uden at Smart Pickup selv holder dem (Wait/udkørsel).
function ADStatusHud.stuckEntries()
local entries = {}
if ADStuckDiag == nil or ADStuckDiag.stillVehicles == nil then return entries end
local farmId = g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or nil
for _, still in ipairs(ADStuckDiag.stillVehicles(ADStatusHud.STUCK_WARN_MS)) do
local vehicle = still.vehicle
local owned = farmId == nil or (vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId)
local held = (ADUnloadWait ~= nil and ADUnloadWait.getWaitFor ~= nil and ADUnloadWait.getWaitFor(vehicle) ~= nil)
or (ADOutbound ~= nil and ADOutbound.getHoldFor ~= nil and ADOutbound.getHoldFor(vehicle) ~= nil)
if owned and not held then
table.insert(entries, {text = string.format(textOf("spu_status_stuck"), tostring(ADRuns.vehicleLabel(vehicle)),
math.floor(still.ms / 60000)), level = "error"})
end
end
return entries
end
function ADStatusHud.snapshot(adEnv)
local proxy = frameProxy()
local snapshot = {flows = {}, fields = {}, chains = {}, warnings = {}}
if adEnv ~= nil then
snapshot.flows = guarded("flows", ADStatusHud.flowEntries, adEnv, proxy) or {}
end
snapshot.fields = guarded("fields", ADStatusHud.fieldEntries, proxy) or {}
local chainResult = guarded("chains", ADStatusHud.chainEntries, proxy) or {}
snapshot.chains = chainResult.chains or {}
for _, entry in ipairs(guarded("stuck", ADStatusHud.stuckEntries) or {}) do table.insert(snapshot.warnings, entry) end
for _, entry in ipairs(chainResult.suggestions or {}) do table.insert(snapshot.warnings, entry) end
return snapshot
end
-- ---- layout og cache (kun i update) ----
local function gameInfoDisplay()
local hud = g_currentMission ~= nil and g_currentMission.hud or nil
return hud ~= nil and hud.gameInfoDisplay or nil
end
-- Størrelser i skærmkoordinater, skaleret som spillets egen HUD (Simple Inspector-mønster).
local function measureUnits()
local display = gameInfoDisplay()
if display == nil or display.scalePixelToScreenHeight == nil or display.scalePixelToScreenVector == nil then return nil end
local textSize = display:scalePixelToScreenHeight(ADStatusHud.TEXT_SIZE_PX)
local marginX, marginY = display:scalePixelToScreenVector({ADStatusHud.MARGIN_PX, ADStatusHud.MARGIN_PX})
local offsetX = display:scalePixelToScreenVector({ADStatusHud.SCREEN_OFFSET_PX, 0})
return textSize, marginX, marginY, offsetX
end
-- Øverste kant: fast under info-boksen; ligger info-boksens position lavere, rykkes vinduet ned under den.
local function topY(marginY)
local top = ADStatusHud.TOP_Y
local display = gameInfoDisplay()
if display ~= nil and display.getPosition ~= nil then
local ok, _, y = pcall(display.getPosition, display)
if ok and type(y) == "number" and y > 0.5 and y < 1 then top = math.min(top, y - marginY) end
end
return top
end
function ADStatusHud.rebuild(adEnv)
local lines = ADStatusView.lines(ADStatusHud.snapshot(adEnv), textOf, ADStatusView.MAX_LINES)
local textSize, marginX, marginY, offsetX = measureUnits()
if textSize == nil then
ADStatusHud.lines, ADStatusHud.layout = {}, nil
return
end
local maxWidth = 0
for _, line in ipairs(lines) do
setTextBold(line.header == true)
line.width = getTextWidth(textSize, line.text)
line.color = line.header and ADStatusHud.HEADER_COLOR or (ADFlowView.LEVEL_COLORS[line.level] or ADStatusHud.HEADER_COLOR)
if line.width > maxWidth then maxWidth = line.width end
end
setTextBold(false)
local width = maxWidth + 2 * marginX
local height = #lines * textSize + 2 * marginY
local top = topY(marginY)
ADStatusHud.lines = lines
ADStatusHud.layout = {x = 1 - offsetX - width, y = top - height, top = top, width = width, height = height,
textSize = textSize, marginX = marginX, marginY = marginY, padding = marginX * 0.5}
end
-- ---- overlays ----
local function createOverlays()
if overlays ~= nil then return overlays end
if g_overlayManager == nil or HUD == nil or HUD.COLOR == nil or HUD.COLOR.BACKGROUND == nil then return nil end
local r, g, b, a = unpack(HUD.COLOR.BACKGROUND)
local created = {
middle = g_overlayManager:createOverlay("gui.gameInfo_middle", 0, 0, 0, 0),
left = g_overlayManager:createOverlay("gui.gameInfo_left", 0, 0, 0, 0),
right = g_overlayManager:createOverlay("gui.gameInfo_right", 0, 0, 0, 0),
}
for _, overlay in pairs(created) do overlay:setColor(r, g, b, a) end
overlays = created
return overlays
end
local function deleteOverlays()
if overlays == nil then return end
for _, overlay in pairs(overlays) do
if overlay.delete ~= nil then pcall(overlay.delete, overlay) end
end
overlays = nil
end
-- ---- indstillinger (pr. bruger, ikke i savegamet) ----
local function settingsPath()
if getUserProfileAppPath == nil then return nil end
return getUserProfileAppPath() .. ADStatusHud.SETTINGS_DIR .. ADStatusHud.SETTINGS_FILE
end
function ADStatusHud.loadSettings()
settingsLoaded = true
local path = settingsPath()
if path == nil or fileExists == nil or not fileExists(path) then return end
local xmlFile = XMLFile.load("ADSmartPickupStatus", path)
if xmlFile == nil then return end
ADStatusHud.visible = xmlFile:getBool(ADStatusHud.SETTINGS_ROOT .. "#visible", false) == true
xmlFile:delete()
end
function ADStatusHud.saveSettings()
local path = settingsPath()
if path == nil then return end
if createFolder ~= nil then
createFolder(getUserProfileAppPath() .. "modSettings/")
createFolder(getUserProfileAppPath() .. ADStatusHud.SETTINGS_DIR)
end
local xmlFile = XMLFile.create("ADSmartPickupStatus", path, ADStatusHud.SETTINGS_ROOT)
if xmlFile == nil then
logOnce("saveSettings", "kunne ikke oprette " .. path)
return
end
xmlFile:setBool(ADStatusHud.SETTINGS_ROOT .. "#visible", ADStatusHud.visible == true)
xmlFile:save()
xmlFile:delete()
end
-- ---- offentlige kald fra moden ----
function ADStatusHud.toggle()
ADStatusHud.visible = not ADStatusHud.visible
timerMs = ADStatusHud.REFRESH_MS -- byg straks ved næste update
if not ADStatusHud.visible then ADStatusHud.lines, ADStatusHud.layout = {}, nil end
guarded("saveSettings", ADStatusHud.saveSettings)
end
-- Action-event-callback (target = ADStatusHud).
function ADStatusHud.onToggleAction()
ADStatusHud.toggle()
end
function ADStatusHud.update(dt, adEnv)
if not ADStatusHud.isClient() then return end
if not settingsLoaded then
guarded("loadSettings", ADStatusHud.loadSettings)
timerMs = ADStatusHud.REFRESH_MS
end
if not ADStatusHud.visible then return end
timerMs = timerMs + (dt or 0)
if timerMs < ADStatusHud.REFRESH_MS then return end
timerMs = 0
guarded("rebuild", ADStatusHud.rebuild, adEnv)
end
-- Tegnes ikke ved åben menu/dialog, skjult HUD, søvn eller åben chat.
local function shouldSkipDraw()
if g_gui ~= nil and g_gui:getIsGuiVisible() then return true end
if g_noHudModeEnabled then return true end
local mission = g_currentMission
local hud = mission ~= nil and mission.hud or nil
if hud == nil or hud.isVisible == false then return true end
if g_sleepManager ~= nil and g_sleepManager.getIsSleeping ~= nil and g_sleepManager:getIsSleeping() then return true end
if hud.chatDisplay ~= nil and hud.chatDisplay.getVisible ~= nil and hud.chatDisplay:getVisible() then return true end
return false
end
local function render(layout, bg)
bg.middle:setDimension(layout.width, layout.height)
bg.middle:setPosition(layout.x, layout.y)
bg.middle:render()
bg.left:setDimension(layout.padding, layout.height)
bg.left:setPosition(layout.x - layout.padding, layout.y)
bg.left:render()
bg.right:setDimension(layout.padding, layout.height)
bg.right:setPosition(layout.x + layout.width, layout.y)
bg.right:render()
setTextAlignment(RenderText.ALIGN_LEFT)
setTextVerticalAlignment(RenderText.VERTICAL_ALIGN_TOP)
local textX, textY = layout.x + layout.marginX, layout.top - layout.marginY
for _, line in ipairs(ADStatusHud.lines) do
local color = line.color
setTextBold(line.header == true)
setTextColor(color[1], color[2], color[3], color[4])
renderText(textX, textY, layout.textSize, line.text)
textY = textY - layout.textSize
end
end
function ADStatusHud.draw()
if not ADStatusHud.visible or ADStatusHud.layout == nil or #ADStatusHud.lines == 0 then return end
if shouldSkipDraw() then return end
local bg = createOverlays()
if bg == nil then return end
local ok, err = pcall(render, ADStatusHud.layout, bg)
-- tekststate nulstilles altid, også efter en fejl (andre mods/spillet tegner videre med den)
setTextColor(1, 1, 1, 1)
setTextAlignment(RenderText.ALIGN_LEFT)
setTextVerticalAlignment(RenderText.VERTICAL_ALIGN_BASELINE)
setTextBold(false)
if not ok then logOnce("draw", err) end
end
function ADStatusHud.delete()
deleteOverlays()
if PlayerInputComponent ~= nil and PlayerInputComponent.spuStatusToggleHandler == registerToggleActionRef then
PlayerInputComponent.spuStatusToggleHandler = nil
end
ADStatusHud.lines, ADStatusHud.layout = {}, nil
settingsLoaded = false
timerMs = 0
end
-- ---- tast: global spiller-handling (til fods og i køretøjer) ----
-- PlayerInputComponent.registerGlobalPlayerActionEvents kaldes ved hvert kontekstskift (til fods / i køretøj);
-- samme mønster som FS25_Courseplay (CP_OPEN_INGAME_MENU) og FS25_EasyDevControls. Hjælpeteksten skjules i F1.
local function registerToggleAction(component)
if not ADStatusHud.isClient() then return end
local player = component ~= nil and component.player or nil
if player ~= nil and player.isOwner == false then return end
if g_inputBinding == nil or InputAction == nil or InputAction.SPU_TOGGLE_STATUS == nil then return end
local _, eventId = g_inputBinding:registerActionEvent(InputAction.SPU_TOGGLE_STATUS, ADStatusHud, ADStatusHud.onToggleAction,
false, true, false, true)
if eventId ~= nil then g_inputBinding:setActionEventTextVisibility(eventId, false) end
end
registerToggleActionRef = function(component) guarded("registerAction", registerToggleAction, component) end
-- Spillets klasse overlever en genindlæsning af savegamet, moden gør ikke: krogen lægges kun én gang (markør på
-- klassen) og kalder den aktuelle indlæsnings handler, så en gammel ADStatusHud aldrig registrerer tasten igen.
function ADStatusHud.installInputHook()
if PlayerInputComponent == nil or PlayerInputComponent.registerGlobalPlayerActionEvents == nil or Utils == nil then return false end
PlayerInputComponent.spuStatusToggleHandler = registerToggleActionRef
if PlayerInputComponent.spuStatusToggleHooked then return true end
PlayerInputComponent.registerGlobalPlayerActionEvents = Utils.appendedFunction(PlayerInputComponent.registerGlobalPlayerActionEvents,
function(component, ...)
local handler = PlayerInputComponent.spuStatusToggleHandler
if handler ~= nil then handler(component) end
end)
PlayerInputComponent.spuStatusToggleHooked = true
return true
end
if PlayerInputComponent ~= nil and Utils ~= nil then
ADStatusHud.installInputHook()
end
+143
View File
@@ -0,0 +1,143 @@
-- AD Status View
-- Ren logik for statusvinduet på skærmen (slås til/fra med en tast, se adStatusHud.lua): øjebliksbilledet
-- -> færdige linjer {text, level, header}. Sektioner i fast rækkefølge Flows/Marker/Kæder/Advarsler, kun med
-- indhold; højst maxLines linjer (resten "+k mere"); hver tekst højst MAX_CHARS tegn (UTF-8-sikkert, "..").
--
-- snapshot = {
-- flows = {{vehicle, action, fillPercent (0-100|nil), level}},
-- fields = {{fieldId, work, vehicle|nil, progress (0-1|nil), level}},
-- chains = {{text, level}}, -- standard rød
-- warnings = {{text, level}}, -- standard gul
-- }
-- level = "running"|"waiting"|"error"|"stopped" (farver: ADFlowView.LEVEL_COLORS); overskrifter har level "header".
ADStatusView = {}
ADStatusView.MAX_LINES = 18
ADStatusView.MAX_CHARS = 48
ADStatusView.VEHICLE_CHARS = 18 -- traktornavnet må ikke æde hele linjen
ADStatusView.MIN_FLEX_CHARS = 6
ADStatusView.SEPARATOR = " · "
ADStatusView.ELLIPSIS = ".."
local LEVELS = {running = true, waiting = true, error = true, stopped = true}
local SECTIONS = {
{key = "flows", header = "spu_status_flows", defaultLevel = "running"},
{key = "fields", header = "spu_status_fields", defaultLevel = "running"},
{key = "chains", header = "spu_status_chains", defaultLevel = "error"},
{key = "warnings", header = "spu_status_warnings", defaultLevel = "waiting"},
}
-- ---- UTF-8-sikker længde/afkortning (æøå er flere bytes) ----
function ADStatusView.textLength(text)
local count = 0
for _ in tostring(text):gmatch("[^\128-\191]") do count = count + 1 end
return count
end
local function utf8Prefix(text, chars)
local result, count = {}, 0
for char in text:gmatch("[^\128-\191][\128-\191]*") do
if count >= chars then break end
table.insert(result, char)
count = count + 1
end
return table.concat(result)
end
function ADStatusView.fit(text, chars)
text = tostring(text or "")
if ADStatusView.textLength(text) <= chars then return text end
local ellipsis = ADStatusView.ELLIPSIS
return utf8Prefix(text, math.max(0, chars - ADStatusView.textLength(ellipsis))) .. ellipsis
end
-- Dele samlet med " · "; delen på flexIndex får den plads der er tilbage, så fx fyldprocenten bevares.
local function joinFitted(parts, flexIndex)
local sep = ADStatusView.SEPARATOR
local fixed = ADStatusView.textLength(sep) * (#parts - 1)
for index, part in ipairs(parts) do
if index ~= flexIndex then fixed = fixed + ADStatusView.textLength(part) end
end
local budget = math.max(ADStatusView.MIN_FLEX_CHARS, ADStatusView.MAX_CHARS - fixed)
local fitted = {}
for index, part in ipairs(parts) do
fitted[index] = index == flexIndex and ADStatusView.fit(part, budget) or part
end
return ADStatusView.fit(table.concat(fitted, sep), ADStatusView.MAX_CHARS)
end
local function percentText(value)
return string.format("%d %%", math.floor(value + 0.5))
end
-- ---- linjer pr. sektion ----
local function flowText(entry)
local parts = {ADStatusView.fit(entry.vehicle or "?", ADStatusView.VEHICLE_CHARS), tostring(entry.action or "")}
if type(entry.fillPercent) == "number" then table.insert(parts, percentText(entry.fillPercent)) end
return joinFitted(parts, 2)
end
local function fieldText(entry)
local parts = {string.format("F%s %s", tostring(entry.fieldId or "?"), tostring(entry.work or ""))}
if entry.vehicle ~= nil and entry.vehicle ~= "" then
table.insert(parts, ADStatusView.fit(entry.vehicle, ADStatusView.VEHICLE_CHARS))
end
if type(entry.progress) == "number" then table.insert(parts, percentText(entry.progress * 100)) end
return joinFitted(parts, 1)
end
local function plainText(entry)
return ADStatusView.fit(entry.text or "", ADStatusView.MAX_CHARS)
end
local TEXT_OF_SECTION = {flows = flowText, fields = fieldText, chains = plainText, warnings = plainText}
local function levelOf(entry, default)
return LEVELS[entry.level] and entry.level or default
end
-- Alle linjer uden loft.
local function allLines(snapshot, textOf)
local lines = {}
for _, section in ipairs(SECTIONS) do
local entries = snapshot[section.key] or {}
if #entries > 0 then
table.insert(lines, {text = ADStatusView.fit(textOf(section.header), ADStatusView.MAX_CHARS), level = "header", header = true})
for _, entry in ipairs(entries) do
table.insert(lines, {text = TEXT_OF_SECTION[section.key](entry), level = levelOf(entry, section.defaultLevel), header = false})
end
end
end
return lines
end
-- Afkort til maxLines: sidste plads bliver "+k mere" (k = skjulte indholdslinjer); en overskrift uden
-- indhold lige før afkortningen droppes.
local function capLines(lines, maxLines, textOf)
if #lines <= maxLines then return lines end
local kept = {}
for index = 1, math.max(0, maxLines - 1) do kept[index] = lines[index] end
while #kept > 0 and kept[#kept].header do table.remove(kept) end
local hidden = 0
for index = #kept + 1, #lines do
if not lines[index].header then hidden = hidden + 1 end
end
local ok, text = pcall(string.format, textOf("spu_status_more"), hidden)
table.insert(kept, {text = ADStatusView.fit(ok and text or ("+" .. hidden), ADStatusView.MAX_CHARS), level = "stopped", header = false})
return kept
end
function ADStatusView.lines(snapshot, textOf, maxLines)
snapshot = snapshot or {}
textOf = textOf or function(key) return key end
maxLines = math.max(1, math.floor(maxLines or ADStatusView.MAX_LINES))
local lines = allLines(snapshot, textOf)
if #lines == 0 then
return {{text = ADStatusView.fit(textOf("spu_status_idle"), ADStatusView.MAX_CHARS), level = "stopped", header = false}}
end
return capLines(lines, maxLines, textOf)
end
+10
View File
@@ -53,6 +53,16 @@ function ADStuckDiag.describe(vehicle)
return table.concat(parts, " ")
end
-- Kun læsning (statusvinduet): køretøjer der har holdt stille mindst minMs -> {{vehicle, ms}}, længst først.
function ADStuckDiag.stillVehicles(minMs)
local list = {}
for vehicle, ms in pairs(stillMs) do
if ms >= (minMs or ADStuckDiag.STILL_MS) then table.insert(list, {vehicle = vehicle, ms = ms}) end
end
table.sort(list, function(a, b) return a.ms > b.ms end)
return list
end
function ADStuckDiag.tick(dt)
timerMs = timerMs + (dt or 0)
if timerMs < ADStuckDiag.CHECK_INTERVAL_MS then return end
+16
View File
@@ -616,5 +616,21 @@
<text name="spu_chain_problem_off" text="%s (%s) er slukket af dig — tænd den eller sæt kæden på pause"/>
<text name="spu_chain_problem_cycle" text="Kredsløb omkring %s — kæden kan ikke beregnes"/>
<text name="spu_attention_chainProblem" text="%s mangler et led: %s"/>
<text name="input_SPU_TOGGLE_STATUS" text="AD Smart Pickup: statusvindue"/>
<text name="spu_status_flows" text="Flows"/>
<text name="spu_status_fields" text="Marker"/>
<text name="spu_status_chains" text="Kæder"/>
<text name="spu_status_warnings" text="Advarsler"/>
<text name="spu_status_more" text="+%d mere"/>
<text name="spu_status_idle" text="Smart Pickup: intet kører lige nu"/>
<text name="spu_status_act_fetch" text="henter %s ved %s"/>
<text name="spu_status_act_driving" text="kører til %s"/>
<text name="spu_status_act_waiting" text="venter ved %s"/>
<text name="spu_status_act_holding" text="holder ved %s"/>
<text name="spu_status_act_fetchWagon" text="henter vogn"/>
<text name="spu_status_act_waitWagon" text="venter på vogn"/>
<text name="spu_status_act_emptying" text="tømmer vognen"/>
<text name="spu_status_act_homing" text="på vej hjem"/>
<text name="spu_status_stuck" text="%s står stille (%d min)"/>
</texts>
</l10n>
+16
View File
@@ -616,5 +616,21 @@
<text name="spu_chain_problem_off" text="%s (%s) was turned off by you — turn it on or pause the chain"/>
<text name="spu_chain_problem_cycle" text="Loop around %s — the chain cannot be calculated"/>
<text name="spu_attention_chainProblem" text="%s is missing a link: %s"/>
<text name="input_SPU_TOGGLE_STATUS" text="AD Smart Pickup: status window"/>
<text name="spu_status_flows" text="Flows"/>
<text name="spu_status_fields" text="Fields"/>
<text name="spu_status_chains" text="Chains"/>
<text name="spu_status_warnings" text="Warnings"/>
<text name="spu_status_more" text="+%d more"/>
<text name="spu_status_idle" text="Smart Pickup: nothing running right now"/>
<text name="spu_status_act_fetch" text="fetching %s at %s"/>
<text name="spu_status_act_driving" text="driving to %s"/>
<text name="spu_status_act_waiting" text="waiting at %s"/>
<text name="spu_status_act_holding" text="holding at %s"/>
<text name="spu_status_act_fetchWagon" text="fetching wagon"/>
<text name="spu_status_act_waitWagon" text="waiting for wagon"/>
<text name="spu_status_act_emptying" text="emptying the wagon"/>
<text name="spu_status_act_homing" text="heading home"/>
<text name="spu_status_stuck" text="%s standing still (%d min)"/>
</texts>
</l10n>
+10
View File
@@ -62,6 +62,16 @@
<sourceFile filename="adChain.lua"/>
<sourceFile filename="adChainRun.lua"/>
<sourceFile filename="adChainView.lua"/>
<sourceFile filename="adStatusView.lua"/>
<sourceFile filename="adStatusHud.lua"/>
<sourceFile filename="adSmartPickup.lua"/>
</extraSourceFiles>
<actions>
<action name="SPU_TOGGLE_STATUS"/>
</actions>
<inputBinding>
<actionBinding action="SPU_TOGGLE_STATUS">
<binding device="KB_MOUSE_DEFAULT" input="KEY_lalt KEY_s"/>
</actionBinding>
</inputBinding>
</modDesc>
+19
View File
@@ -333,6 +333,25 @@ Sæt **Ja** ud for de bygninger, flowet skal bruge. Bygninger uden markør er sk
**Vis alle bygninger** til. Listen er sorteret efter navn, så *Silo 9* kommer før *Silo 10*. En bygning,
du har solgt, står som *Fjernet bygning (findes ikke mere) — sæt NEJ*.
### Statusvindue
Tryk **venstre Alt + S** for at slå et lille statusvindue til eller fra øverst til højre på skærmen, lige under
spillets info-boks. Tasten virker både til fods og i et køretøj og kan ændres under *Indstillinger → Styring*
(*AD Smart Pickup: statusvindue*). Vinduet opdateres hvert sekund og viser kun de afsnit, der har indhold:
- **Flows** — én linje pr. traktor i et kørende flow: *traktor · handling · fyld %* (fx *Milk Collect 1 ·
henter Mælk ved Kostald · 45 %*). Grøn = kører, gul = venter (Wait, vogn), rød = flowet har en advarsel,
grå = stoppet.
- **Marker** — én linje pr. mark med markarbejde i gang: *F92 Høstes · Roeoptager · 45 %* (farve som i Marker-fanen).
- **Kæder** — kun kæder med et manglende led: *Brød mangler et led: Mel*.
- **Advarsler** — køretøjer der har stået stille i over 2 minutter (uden at Smart Pickup selv holder dem ved
Wait/udkørsel) og ventende kædeforslag (*Kæde: 2 forslag*).
Højst 18 linjer; resten samles i *+k mere*. Vinduet skjules, mens en menu eller dialog er åben, når HUD'en er
skjult, og når du sover. Om vinduet er tændt, gemmes pr. bruger i
`Dokumenter/My Games/FarmingSimulator2025/modSettings/FS25_ADSmartPickup/status.xml` (ikke i savegamet);
standard er slukket.
---
## 5. Forsyning: hold stalde og fabrikker fyldt
+21
View File
@@ -336,6 +336,27 @@ Set **Yes** for the buildings the flow should use. Buildings without a marker ar
on **Show all buildings**. The list is sorted by name, so *Silo 9* comes before *Silo 10*. A building you
have sold appears as *Removed building (no longer exists) — set NO*.
### Status window
Press **left Alt + S** to toggle a small status window in the top right corner of the screen, just below
the game's info box. The key works both on foot and in a vehicle and can be changed under *Settings →
Controls* (*AD Smart Pickup: status window*). The window refreshes every second and only shows sections
that have something in them:
- **Flows** — one line per tractor in a running flow: *tractor · action · fill %* (e.g. *Milk Collect 1 ·
fetching Milk at Cow Barn · 45 %*). Green = running, yellow = waiting (Wait, trailer), red = the flow has
a warning, grey = stopped.
- **Fields** — one line per field with field work in progress: *F92 Harvesting · Beet harvester · 45 %*
(colours as in the Fields tab).
- **Chains** — only chains with a missing link: *Bread is missing a link: Flour*.
- **Warnings** — vehicles that have stood still for more than 2 minutes (without Smart Pickup holding them at
Wait/outbound) and pending chain suggestions (*Chain: 2 suggestions*).
At most 18 lines; the rest is summed up as *+k more*. The window is hidden while a menu or dialog is open,
when the HUD is hidden and while you sleep. Whether it is on is saved per user in
`Documents/My Games/FarmingSimulator2025/modSettings/FS25_ADSmartPickup/status.xml` (not in the savegame);
it is off by default.
---
## 5. Supply: keep barns and factories filled