diff --git a/FS25_ADSmartPickup/adPool.lua b/FS25_ADSmartPickup/adPool.lua new file mode 100644 index 0000000..3aae9b9 --- /dev/null +++ b/FS25_ADSmartPickup/adPool.lua @@ -0,0 +1,135 @@ +-- AD Pool +-- Maskinpulje: plove, kultivatorer, såmaskiner … holder frakoblet på deres AD-parkering "X", og en ledig +-- solo-traktor henter dem via markøren "Connect X", når markarbejdet mangler en færdig kombination. +-- Ren logik øverst (testes med luajit), spil-adapter nederst. + +ADPool = {} +ADPool.LOG_PREFIX = "ADSmartPickup: pulje: " +ADPool.QUARANTINE_MS = 600000 -- redskab der ikke kunne kobles på, springes over så længe + +-- ---- ren logik ---- + +-- Rolle for et frakoblet redskab ud fra dets egne specs (samme prioritet som markflåden); en ren vogn med +-- tip/snegl er tømmevogn. +function ADPool.roleOf(specs) + local role = ADFieldFleet.roleFromSpecs(specs) + if role ~= nil and role ~= "unloader" then return role end + if specs.spec_trailer and specs.spec_dischargeable then return "unloader" end + return role +end + +-- Butiksdata er enten et tal eller en tabel pr. konfiguration: største tal, ellers nil. +function ADPool.numberOf(value) + if type(value) == "number" then return value end + if type(value) ~= "table" then return nil end + local best = nil + for _, entry in pairs(value) do + if type(entry) == "number" and (best == nil or entry > best) then best = entry end + end + return best +end + +-- Mangler et af tallene, tillades det (intet må gå i stå på manglende data). +function ADPool.powerOk(tractorHp, neededHp) + if tractorHp == nil or neededHp == nil or tractorHp <= 0 or neededHp <= 0 then return true end + return tractorHp >= neededHp +end + +-- Markøren "Connect " for en parkering (navneregel). +function ADPool.connectFor(parkName, markers) + if type(parkName) ~= "string" then return nil end + local wanted = parkName:lower() + for _, marker in pairs(markers or {}) do + local action, target = ADHitch.parseMarker(marker.name) + if action == "attach" and target ~= nil and target:lower() == wanted then return marker end + end + return nil +end + +local function hasRole(item, role) + return item.role == role or (item.roles ~= nil and item.roles[role] == true) +end + +local function distanceSq(ax, az, bx, bz) + return (ax - bx) ^ 2 + (az - bz) ^ 2 +end + +local function nearest(items, x, z, keep) + local best, bestDistance = nil, math.huge + for _, item in ipairs(items) do + if keep(item) then + local distance = distanceSq(item.x, item.z, x, z) + if distance < bestDistance then best, bestDistance = item, distance end + end + end + return best +end + +-- Redskab med rollen nærmest marken, derefter nærmeste ledige traktor med nok hk til redskabet. +-- blocked = {[id] = true} (reserverede og redskaber i karantæne). accept (valgfri): implement -> bool. +function ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, blocked, accept) + blocked = blocked or {} + local free = {} + for _, tractor in ipairs(tractors or {}) do + if not blocked[tractor.id] then table.insert(free, tractor) end + end + local candidates = {} + for _, implement in ipairs(implements or {}) do + if hasRole(implement, role) and not blocked[implement.id] and (accept == nil or accept(implement)) then + table.insert(candidates, implement) + end + end + if #candidates == 0 then return nil, "noPoolImplement" end + if #free == 0 then return nil, "noPoolTractor" end + table.sort(candidates, function(a, b) + return distanceSq(a.x, a.z, fieldX, fieldZ) < distanceSq(b.x, b.z, fieldX, fieldZ) + end) + local weakest = nil + for _, implement in ipairs(candidates) do + local tractor = nearest(free, implement.x, implement.z, function(t) return ADPool.powerOk(t.hp, implement.neededHp) end) + if tractor ~= nil then return {implement = implement, tractor = tractor} end + if weakest == nil or (implement.neededHp or 0) < weakest then weakest = implement.neededHp end + end + return nil, "tooWeak", {weakest or 0} +end + +-- Ventende marker først (længst ventende først, lige -> lavest id); resten bagefter i oprindelig rækkefølge. +function ADPool.queueOrder(infos, waitingSince) + local waiting, rest = {}, {} + for _, info in ipairs(infos or {}) do + if waitingSince ~= nil and waitingSince[info.id] ~= nil then table.insert(waiting, info) else table.insert(rest, info) end + end + table.sort(waiting, function(a, b) + local wa, wb = waitingSince[a.id], waitingSince[b.id] + if wa ~= wb then return wa < wb end + return a.id < b.id + end) + for _, info in ipairs(rest) do table.insert(waiting, info) end + return waiting +end + +-- Reservationer (i hukommelsen): owner -> {id, ...}. Karantæne: id -> udløbstid. +local reservations, quarantined = {}, {} + +function ADPool.reserve(owner, ids) + reservations[owner] = ids +end + +function ADPool.release(owner) + reservations[owner] = nil +end + +function ADPool.quarantine(id, nowMs) + quarantined[id] = nowMs + ADPool.QUARANTINE_MS +end + +function ADPool.blocked(nowMs) + local result = {} + for _, ids in pairs(reservations) do + for _, id in ipairs(ids) do result[id] = true end + end + for id, untilMs in pairs(quarantined) do + if nowMs < untilMs then result[id] = true end + end + return result +end diff --git a/tests/test_adPool.lua b/tests/test_adPool.lua new file mode 100644 index 0000000..b705ece --- /dev/null +++ b/tests/test_adPool.lua @@ -0,0 +1,76 @@ +-- Kør: luajit tests/test_adPool.lua (fra repo-roden) +dofile("FS25_ADSmartPickup/adFieldFleet.lua") +dofile("FS25_ADSmartPickup/adHitch.lua") +dofile("FS25_ADSmartPickup/adPool.lua") +local P = ADPool +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 +-- rolle ud fra redskabets egne specs +check("rolle: plov", P.roleOf({spec_plow = true}), "plow") +check("rolle: kultivator", P.roleOf({spec_cultivator = true}), "cultivator") +check("rolle: vogn med tip", P.roleOf({spec_trailer = true, spec_dischargeable = true}), "unloader") +check("rolle: vogn der også er læssevogn er læssevogn", P.roleOf({spec_trailer = true, spec_dischargeable = true, spec_forageWagon = true}), "collector") +check("rolle: intet", P.roleOf({}), nil) +-- hk fra butiksdata: tal eller tabel (konfigurationer) +check("tal", P.numberOf(180), 180) +check("tabel: største", P.numberOf({base = 120, config = 150}), 150) +check("tom tabel", P.numberOf({}), nil) +check("nil", P.numberOf(nil), nil) +check("hk: nok", P.powerOk(200, 180), true) +check("hk: for lidt", P.powerOk(150, 180), false) +check("hk: redskab uden data", P.powerOk(150, nil), true) +check("hk: traktor uden data", P.powerOk(nil, 180), true) +-- Connect-markør til en parkering +local markers = {{name = "Start", id = 1}, {name = "Bredal", id = 4}, {name = "Connect Bredal", id = 9}, {name = "connect plov 1", id = 12}} +check("connect: Bredal", P.connectFor("Bredal", markers).id, 9) +check("connect: små bogstaver", P.connectFor("Plov 1", markers).id, 12) +check("connect: ingen", P.connectFor("Start", markers), nil) +check("connect: uden parkering", P.connectFor(nil, markers), nil) +-- parring: redskab nærmest marken, så traktor nærmest redskabet +local implements = { + {id = "plovFar", name = "Plov far", role = "plow", x = 500, z = 0, connectId = 20, parkId = 21}, + {id = "plovNear", name = "Plov nær", role = "plow", x = 100, z = 0, neededHp = 180, connectId = 30, parkId = 31}, + {id = "kult", name = "Kultivator", role = "cultivator", x = 0, z = 0, connectId = 40, parkId = 41}, +} +local tractors = { + {id = "small", name = "Lille", x = 110, z = 0, hp = 120, parkId = 2}, + {id = "big", name = "Stor", x = 300, z = 0, hp = 250, parkId = 3}, + {id = "mid", name = "Mellem", x = 150, z = 0, hp = 190, parkId = 5}, +} +local pair = P.pairFor("plow", 0, 0, implements, tractors, {}) +check("par: nærmeste plov til marken", pair.implement.id, "plovNear") +check("par: nærmeste stærke traktor til ploven", pair.tractor.id, "mid") +local blockedPair = P.pairFor("plow", 0, 0, implements, tractors, {plovNear = true}) +check("par: reserveret plov springes over", blockedPair.implement.id, "plovFar") +check("par: plov uden hk-krav tager nærmeste traktor", blockedPair.tractor.id, "big") +local none, why = P.pairFor("seeder", 0, 0, implements, tractors, {}) +check("par: ingen såmaskine", none, nil); check("par: grund", why, "noPoolImplement") +local weak, weakWhy, weakArgs = P.pairFor("plow", 0, 0, {implements[2]}, {tractors[1]}, {}) +check("par: for svag", weak, nil); check("par: grund tooWeak", weakWhy, "tooWeak"); check("par: hk i args", weakArgs[1], 180) +local noTractor, noTractorWhy = P.pairFor("plow", 0, 0, implements, {}, {}) +check("par: ingen traktor", noTractor, nil); check("par: grund noPoolTractor", noTractorWhy, "noPoolTractor") +local rejected, rejectedWhy = P.pairFor("plow", 0, 0, implements, tractors, {}, function() return false end) +check("par: accept afviser alle", rejected, nil); check("par: grund", rejectedWhy, "noPoolImplement") +local multi = P.pairFor("fertilizer", 0, 0, {{id = "spreder", role = "fertilizer", roles = {fertilizer = true, liming = true}, x = 0, z = 0}}, tractors, {}) +check("par: redskab med flere roller", multi.implement.id, "spreder") +-- reservationer og karantæne +P.reserve("F12", {"plovNear", "mid"}) +check("reserveret: redskab", P.blocked(0).plovNear, true) +check("reserveret: traktor", P.blocked(0).mid, true) +P.release("F12") +check("frigivet", P.blocked(0).plovNear, nil) +P.quarantine("plovNear", 1000) +check("karantæne: aktiv", P.blocked(1000 + P.QUARANTINE_MS - 1).plovNear, true) +check("karantæne: udløbet", P.blocked(1000 + P.QUARANTINE_MS).plovNear, nil) +-- kø: længst ventende først, lige -> lavest mark-id, ikke-ventende bagerst i oprindelig rækkefølge +local infos = {{id = 5}, {id = 3}, {id = 9}, {id = 1}} +local ordered = P.queueOrder(infos, {[9] = 100, [3] = 50, [1] = 50}) +check("kø: 1.", ordered[1].id, 1); check("kø: 2.", ordered[2].id, 3); check("kø: 3.", ordered[3].id, 9); check("kø: 4.", ordered[4].id, 5) +check("kø: rører ikke input", infos[1].id, 5) +print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) +os.exit(failures == 0 and 0 or 1)