feat: AD Smart Pickup — vare-bevidst mappe-rotation (mindst fyldte silo først)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
*.zip
|
||||
@@ -0,0 +1,186 @@
|
||||
-- AD Smart Pickup
|
||||
-- Gør AutoDrives mappe-rotation for pålæsning vare-bevidst: i stedet for
|
||||
-- "næste markør i mappen" vælges den markør, hvis silo har den valgte vare —
|
||||
-- og blandt dem den med MINDST i (så rester tømmes først og siloer frigives).
|
||||
-- Findes ingen silo med varen, bruges AutoDrives originale rotation uændret.
|
||||
|
||||
ADSmartPickup = {}
|
||||
|
||||
-- Markøren er dér hvor trækkerens næse stopper; læssetriggeren ligger et
|
||||
-- vogntog bagved. Siloer tættere end dette regnes som "markørens silo".
|
||||
ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE = 40
|
||||
-- Under dette niveau regnes siloen som tom (undgår ture efter støv-rester).
|
||||
ADSmartPickup.MIN_USEFUL_LITERS = 1
|
||||
ADSmartPickup.LOG_PREFIX = "ADSmartPickup: "
|
||||
|
||||
local isHooked = false
|
||||
|
||||
-- Cross-mod adgang: FS25-mods er sandboxede; AutoDrives globals ligger
|
||||
-- i den globale tabel FS25_AutoDrive (zip-navnet).
|
||||
local function getAutoDriveEnv()
|
||||
if FS25_AutoDrive ~= nil and FS25_AutoDrive.ADMultipleTargetsManager ~= nil then
|
||||
return FS25_AutoDrive
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function getFarmId(vehicle)
|
||||
if vehicle.getOwnerFarmId ~= nil then
|
||||
local farmId = vehicle:getOwnerFarmId()
|
||||
if farmId ~= nil and farmId > 0 then
|
||||
return farmId
|
||||
end
|
||||
end
|
||||
if vehicle.spec_aiVehicle ~= nil and vehicle.spec_aiVehicle.startedFarmId ~= nil and vehicle.spec_aiVehicle.startedFarmId > 0 then
|
||||
return vehicle.spec_aiVehicle.startedFarmId
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function getWantedFillTypes(driver)
|
||||
local wanted = {}
|
||||
local selected = driver.ad.stateModule:getSelectedFillTypes()
|
||||
if type(selected) == "table" then
|
||||
for _, fillType in pairs(selected) do
|
||||
table.insert(wanted, fillType)
|
||||
end
|
||||
end
|
||||
if #wanted == 0 then
|
||||
local fillType = driver.ad.stateModule:getFillType()
|
||||
if fillType ~= nil then
|
||||
table.insert(wanted, fillType)
|
||||
end
|
||||
end
|
||||
return wanted
|
||||
end
|
||||
|
||||
local function getDistanceToStation(station, x, z)
|
||||
local closest = math.huge
|
||||
for _, loadTrigger in pairs(station.loadTriggers or {}) do
|
||||
local node = loadTrigger.triggerNode
|
||||
if node ~= nil and entityExists(node) then
|
||||
local tx, _, tz = getWorldTranslation(node)
|
||||
closest = math.min(closest, MathUtil.vector2Length(x - tx, z - tz))
|
||||
end
|
||||
end
|
||||
return closest
|
||||
end
|
||||
|
||||
local function getStationAtMarker(wayPoint, farmId)
|
||||
local bestStation = nil
|
||||
local bestDistance = ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE
|
||||
for _, station in pairs(g_currentMission.storageSystem:getLoadingStations()) do
|
||||
if g_currentMission.accessHandler:canFarmAccess(farmId, station) then
|
||||
local distance = getDistanceToStation(station, wayPoint.x, wayPoint.z)
|
||||
if distance < bestDistance then
|
||||
bestDistance = distance
|
||||
bestStation = station
|
||||
end
|
||||
end
|
||||
end
|
||||
return bestStation
|
||||
end
|
||||
|
||||
-- Siloens EGET indhold. Udvidelses-siloer (isExtension) deler lager med naboer
|
||||
-- inden for storageRadius, så stationens niveau er en nabo-sum — eget lager er
|
||||
-- det tal spilleren ser på siloen og giver en præcis "mindst først"-rangering.
|
||||
local function getOwnStorageLevel(station, fillTypes)
|
||||
local placeable = station.owningPlaceable
|
||||
local storages = placeable ~= nil and placeable.spec_silo ~= nil and placeable.spec_silo.storages or nil
|
||||
if storages == nil then
|
||||
return 0
|
||||
end
|
||||
local total = 0
|
||||
for _, storage in pairs(storages) do
|
||||
for _, fillType in ipairs(fillTypes) do
|
||||
total = total + (storage:getFillLevel(fillType) or 0)
|
||||
end
|
||||
end
|
||||
return total
|
||||
end
|
||||
|
||||
-- Returnerer niveau + om det er siloens eget lager (true) eller kun nabo-lagre (false).
|
||||
local function getStationFillLevel(station, fillTypes, farmId)
|
||||
local ownLevel = getOwnStorageLevel(station, fillTypes)
|
||||
if ownLevel >= ADSmartPickup.MIN_USEFUL_LITERS then
|
||||
return ownLevel, true
|
||||
end
|
||||
-- intet i eget lager: stationen kan stadig læsse fra nabo-lagre
|
||||
local total = 0
|
||||
for _, fillType in ipairs(fillTypes) do
|
||||
total = total + (station:getFillLevel(fillType, farmId) or 0)
|
||||
end
|
||||
return total, false
|
||||
end
|
||||
|
||||
-- Returnerer markerIndex for mappens mindst fyldte silo der har varen, ellers nil.
|
||||
function ADSmartPickup.findLeastFilledPickup(adEnv, driver)
|
||||
local firstMarker = driver.ad.stateModule:getFirstMarker()
|
||||
if firstMarker == nil or firstMarker.group == nil or firstMarker.group == "All" then
|
||||
return nil
|
||||
end
|
||||
local farmId = getFarmId(driver)
|
||||
local fillTypes = getWantedFillTypes(driver)
|
||||
if farmId == nil or #fillTypes == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- siloer med varen i EGET lager går altid forud for dem der kun kan låne fra naboer
|
||||
local best = {[true] = {level = math.huge}, [false] = {level = math.huge}}
|
||||
for _, marker in ipairs(adEnv.ADGraphManager:getMapMarkersInGroup(firstMarker.group)) do
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
||||
local station = wayPoint ~= nil and getStationAtMarker(wayPoint, farmId) or nil
|
||||
if station ~= nil then
|
||||
local level, isOwn = getStationFillLevel(station, fillTypes, farmId)
|
||||
if level >= ADSmartPickup.MIN_USEFUL_LITERS and level < best[isOwn].level then
|
||||
best[isOwn] = {level = level, marker = marker}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local choice = best[true].marker ~= nil and best[true] or best[false]
|
||||
if choice.marker == nil then
|
||||
return nil
|
||||
end
|
||||
return choice.marker.markerIndex, choice.marker.name, choice.level
|
||||
end
|
||||
|
||||
local function installHook(adEnv)
|
||||
local manager = adEnv.ADMultipleTargetsManager
|
||||
local originalGetNextPickup = manager.getNextPickup
|
||||
|
||||
manager.getNextPickup = function(self, driver, forcedSkip)
|
||||
local ok, markerIndex, markerName, level = pcall(ADSmartPickup.findLeastFilledPickup, adEnv, driver)
|
||||
if not ok then
|
||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "fejl i silo-opslag, bruger AutoDrives rotation: %s", tostring(markerIndex))
|
||||
return originalGetNextPickup(self, driver, forcedSkip)
|
||||
end
|
||||
if markerIndex == nil then
|
||||
return originalGetNextPickup(self, driver, forcedSkip)
|
||||
end
|
||||
|
||||
-- hold AutoDrives egen rotations-tilstand konsistent med vores valg
|
||||
local groupName = driver.ad.stateModule:getFirstMarker().group
|
||||
self.pickups[groupName] = self.pickups[groupName] or {}
|
||||
self.pickups[groupName].lastTarget = markerIndex
|
||||
self.pickups[groupName].lastVehicle = driver
|
||||
|
||||
Logging.info(ADSmartPickup.LOG_PREFIX .. "'%s' -> %s (%.0f l)", tostring(driver:getName()), tostring(markerName), level)
|
||||
return markerIndex
|
||||
end
|
||||
|
||||
Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen vælges i mappe-rotation)")
|
||||
end
|
||||
|
||||
function ADSmartPickup:update(dt)
|
||||
if isHooked then
|
||||
return
|
||||
end
|
||||
local adEnv = getAutoDriveEnv()
|
||||
if adEnv ~= nil and g_currentMission ~= nil and g_currentMission.storageSystem ~= nil then
|
||||
installHook(adEnv)
|
||||
isHooked = true
|
||||
end
|
||||
end
|
||||
|
||||
addModEventListener(ADSmartPickup)
|
||||
Binary file not shown.
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<modDesc descVersion="107">
|
||||
<author>masterdraco</author>
|
||||
<version>1.0.0.0</version>
|
||||
<title>
|
||||
<en>AD Smart Pickup</en>
|
||||
</title>
|
||||
<description>
|
||||
<en><![CDATA[AutoDrive-tillæg: når pålæsningsmål roteres i en mappe, køres der direkte til den silo i mappen, der har den valgte vare (mindst fyldte først), i stedet for at prøve markørerne én efter én. Kræver FS25_AutoDrive med "Brug mapper" + "Rotér mål: pålæsning".]]></en>
|
||||
</description>
|
||||
<iconFilename>icon.dds</iconFilename>
|
||||
<multiplayer supported="true"/>
|
||||
<extraSourceFiles>
|
||||
<sourceFile filename="adSmartPickup.lua"/>
|
||||
</extraSourceFiles>
|
||||
</modDesc>
|
||||
@@ -0,0 +1,12 @@
|
||||
# FS25_ADSmartPickup
|
||||
|
||||
AutoDrive-tillæg til Farming Simulator 25: når pålæsningsmål roteres i en AD-mappe, køres der
|
||||
direkte til den silo i mappen, der har den valgte vare — **mindst fyldte først** — i stedet for
|
||||
at prøve markørerne én efter én.
|
||||
|
||||
- Wrapper `ADMultipleTargetsManager.getNextPickup` (via den globale `FS25_AutoDrive`-tabel); rører ikke AutoDrive-zip'en.
|
||||
- Siloens eget lager rangeres før nabo-lån (udvidelses-siloer deler lager inden for `storageRadius`).
|
||||
- Finder den ingen silo med varen, eller fejler et spil-API, bruges AutoDrives originale rotation.
|
||||
- Kræver i AutoDrive: "Brug mapper" + "Rotér mål: pålæsning" på køretøjet.
|
||||
|
||||
Test: `luajit tests/test_adSmartPickup.lua` · Byg: zip indholdet af `FS25_ADSmartPickup/` til `FS25_ADSmartPickup.zip`.
|
||||
@@ -0,0 +1,138 @@
|
||||
-- Kør: luajit tests/test_adSmartPickup.lua (fra repo-roden)
|
||||
-- Mock'er de få FS25-/AutoDrive-API'er moden rører og tester valg-logikken.
|
||||
|
||||
local SUGARBEET, WHEAT = 10, 11
|
||||
local logLines = {}
|
||||
|
||||
-- ---- FS25-mocks ----
|
||||
MathUtil = {vector2Length = function(x, z) return math.sqrt(x * x + z * z) end}
|
||||
Logging = {
|
||||
info = function(fmt, ...) table.insert(logLines, string.format(fmt, ...)) end,
|
||||
warning = function(fmt, ...) table.insert(logLines, "WARN " .. string.format(fmt, ...)) end,
|
||||
}
|
||||
local nodePositions = {}
|
||||
function entityExists(node) return nodePositions[node] ~= nil end
|
||||
function getWorldTranslation(node) local p = nodePositions[node]; return p.x, 0, p.z end
|
||||
local listener
|
||||
function addModEventListener(l) listener = l end
|
||||
|
||||
local function makeStation(node, x, z, levels, isBroken, ownLevels)
|
||||
nodePositions[node] = {x = x, z = z}
|
||||
local placeable = ownLevels ~= nil and {spec_silo = {storages = {
|
||||
{getFillLevel = function(_, fillType) return ownLevels[fillType] or 0 end},
|
||||
}}} or nil
|
||||
return {
|
||||
owningPlaceable = placeable,
|
||||
loadTriggers = {{triggerNode = node}},
|
||||
getFillLevel = function(_, fillType)
|
||||
if isBroken then error("boom") end
|
||||
return levels[fillType] or 0
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local stations = {}
|
||||
g_currentMission = {
|
||||
storageSystem = {getLoadingStations = function() return stations end},
|
||||
accessHandler = {canFarmAccess = function() return true end},
|
||||
}
|
||||
|
||||
-- ---- AutoDrive-mocks ----
|
||||
local markers, wayPoints = {}, {}
|
||||
local function addMarker(index, name, group, x, z)
|
||||
wayPoints[index] = {x = x, z = z}
|
||||
table.insert(markers, {id = index, markerIndex = index, name = name, group = group})
|
||||
end
|
||||
|
||||
local function resetWorld()
|
||||
stations, markers, wayPoints, nodePositions, logLines = {}, {}, {}, {}, {}
|
||||
FS25_AutoDrive.ADMultipleTargetsManager.pickups = {}
|
||||
end
|
||||
|
||||
FS25_AutoDrive = {
|
||||
ADMultipleTargetsManager = {pickups = {}, getNextPickup = function() return "ORIGINAL" end},
|
||||
ADGraphManager = {
|
||||
getMapMarkersInGroup = function(_, group)
|
||||
local result = {}
|
||||
for _, m in ipairs(markers) do if m.group == group then table.insert(result, m) end end
|
||||
return result
|
||||
end,
|
||||
getWayPointById = function(_, id) return wayPoints[id] end,
|
||||
},
|
||||
}
|
||||
|
||||
local function makeDriver(firstMarker, fillTypes)
|
||||
return {
|
||||
getName = function() return "Testtraktor" end,
|
||||
getOwnerFarmId = function() return 1 end,
|
||||
ad = {stateModule = {
|
||||
getFirstMarker = function() return firstMarker end,
|
||||
getSelectedFillTypes = function() return fillTypes end,
|
||||
getFillType = function() return fillTypes[1] end,
|
||||
}},
|
||||
}
|
||||
end
|
||||
|
||||
dofile("FS25_ADSmartPickup/adSmartPickup.lua")
|
||||
listener:update(16) -- installerer hook'en
|
||||
local manager = FS25_AutoDrive.ADMultipleTargetsManager
|
||||
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then
|
||||
print("OK " .. name)
|
||||
else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
|
||||
-- 1: vælger mindst fyldte silo med varen; springer tomme og forkert vare over
|
||||
resetWorld()
|
||||
addMarker(1, "Silo 1", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[WHEAT] = 900000}))
|
||||
addMarker(2, "Silo 2", "Siloer", 500, 0); table.insert(stations, makeStation(102, 500, 15, {[SUGARBEET] = 800000}))
|
||||
addMarker(3, "Silo 3", "Siloer", 1000, 0); table.insert(stations, makeStation(103, 1000, 15, {[SUGARBEET] = 12000}))
|
||||
addMarker(4, "Silo 4", "Siloer", 1500, 0); table.insert(stations, makeStation(104, 1500, 15, {[SUGARBEET] = 0}))
|
||||
addMarker(5, "Fabrik", "Andet", 1000, 5); -- anden mappe, skal ignoreres
|
||||
check("mindst fyldte med varen vælges", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), 3)
|
||||
check("AD-rotationstilstand opdateret", manager.pickups["Siloer"].lastTarget, 3)
|
||||
|
||||
-- 2: ingen silo har varen -> AutoDrives originale rotation
|
||||
check("fallback når varen ikke findes", manager:getNextPickup(makeDriver(markers[1], {99}), false), "ORIGINAL")
|
||||
|
||||
-- 3: markør uden silo i nærheden ignoreres
|
||||
resetWorld()
|
||||
addMarker(1, "Langt væk", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 200, {[SUGARBEET] = 5}))
|
||||
addMarker(2, "Silo 2", "Siloer", 500, 0); table.insert(stations, makeStation(102, 500, 15, {[SUGARBEET] = 700}))
|
||||
check("markør uden nær silo ignoreres", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), 2)
|
||||
|
||||
-- 4: flere valgte varer lægges sammen
|
||||
resetWorld()
|
||||
addMarker(1, "Silo 1", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[SUGARBEET] = 100, [WHEAT] = 5000}))
|
||||
addMarker(2, "Silo 2", "Siloer", 500, 0); table.insert(stations, makeStation(102, 500, 15, {[SUGARBEET] = 3000}))
|
||||
check("flere varetyper summeres", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET, WHEAT}), false), 2)
|
||||
|
||||
-- 4b: klynge med delt lager — eget indhold slår nabo-sum, også når nabo-summen er mindre
|
||||
resetWorld()
|
||||
addMarker(1, "Nabo", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[SUGARBEET] = 800}, false, {}))
|
||||
addMarker(2, "Ejer stor", "Siloer", 14, 0); table.insert(stations, makeStation(102, 14, 15, {[SUGARBEET] = 900000}, false, {[SUGARBEET] = 500000}))
|
||||
addMarker(3, "Ejer lille", "Siloer", 28, 0);table.insert(stations, makeStation(103, 28, 15, {[SUGARBEET] = 900000}, false, {[SUGARBEET] = 20000}))
|
||||
check("eget lager foran nabo-lån, mindst først", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), 3)
|
||||
|
||||
-- 4c: kun nabo-lån muligt -> bruges stadig
|
||||
resetWorld()
|
||||
addMarker(1, "Nabo", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[SUGARBEET] = 800}, false, {}))
|
||||
check("nabo-lån bruges når ingen ejer varen", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), 1)
|
||||
|
||||
-- 5: fejl i spil-API -> fallback + advarsel, aldrig crash
|
||||
resetWorld()
|
||||
addMarker(1, "Silo 1", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {}, true))
|
||||
check("fejl giver fallback", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), "ORIGINAL")
|
||||
check("fejl logges som advarsel", logLines[#logLines]:sub(1, 4), "WARN")
|
||||
|
||||
-- 6: gruppen "All" og manglende første markør røres ikke
|
||||
check("gruppe All -> fallback", manager:getNextPickup(makeDriver({group = "All", markerIndex = 1}, {SUGARBEET}), false), "ORIGINAL")
|
||||
check("ingen første markør -> fallback", manager:getNextPickup(makeDriver(nil, {SUGARBEET}), false), "ORIGINAL")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
Reference in New Issue
Block a user