feat(status): statusvindue på skærmen slås til med en tast
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user