-- 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 -- Butikskategori: markredskaber kun bag traktorer (tractorsS/M/L …) — ikke lastbil/Unimog, læssere o.l. -- Tømmevogne må alle trække. Ukendt kategori tillades (intet må gå i stå på manglende data). ADPool.ANY_PULLER_ROLES = {unloader = true} function ADPool.categoryOk(category, role) if category == nil or ADPool.ANY_PULLER_ROLES[role] then return true end return tostring(category):lower():find("^tractors") ~= nil end local function suits(tractor, implement, role) return ADPool.powerOk(tractor.hp, implement.neededHp) and ADPool.categoryOk(tractor.category, role) 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 -- Når opgaven slutter, sættes puljeredskabet af — også når AutoDrive ellers selv ville parkere (påfyldte -- sæt): AD parkerer ved redskabets plads med redskabet på. Aldrig ved manuel transport. function ADPool.shouldReturnHome(assignment) return not assignment.manual end -- Kun maskiner uden egen arbejdsrolle (almindelige traktorer) henter puljeredskaber — ikke mejetærskere, -- selvkørende sprøjter o.l., selv om de har koblingspunkter. function ADPool.canPull(role) return role == nil end -- Hvem fik koblingen til at fejle? Kom traktoren ikke frem, er det traktoren (ingen AD-rute), ellers redskabet. function ADPool.failureBlames(reason) if reason == "stoppedAway" then return "tractor" end return "implement" end -- Efter koblingen: refill = markarbejdets påfyldningsplan (nil = intet at fylde). function ADPool.nextDrive(refill) if refill == nil or not refill.empty then return "drive" end if refill.refillId ~= nil then return "load" end return "empty" end -- Puljen bruges til opgaver med ét sæt (også tømmevogne ved høst), ikke når marken har egne faste maskiner -- til rollen. function ADPool.mayUsePool(decision, hasOwnForRole) return (decision.count or 1) == 1 and not hasOwnForRole end -- Kan puljevognen bære varen? (implement.carries = {[fillType] = true}; nil vare = intet krav) function ADPool.canCarry(implement, fillType) if fillType == nil then return true end return implement.carries ~= nil and implement.carries[fillType] == true end -- Status til flådefanen. s = {parkName, hasConnect, attachedTo, quarantineMin, reserved} -> nøgle, argument function ADPool.implementStatus(s) if s.parkName == nil then return "noPark" end if not s.hasConnect then return "noConnect", s.parkName end if s.attachedTo ~= nil then return "attached", s.attachedTo end if s.flowName ~= nil then return "flow", s.flowName end if (s.quarantineMin or 0) > 0 then return "quarantine", s.quarantineMin end if s.reserved then return "reserved" end return "parked", s.parkName end -- s = {hasPark, enabled, controlled, returning, fieldId, busy, offNetwork, hitched} -> nøgle, argument function ADPool.tractorStatus(s) if not s.hasPark then return "noPark" end if s.controlled then return "player" end if s.returning then return "returning" end if s.fieldId ~= nil then return "job", s.fieldId end if s.busy then return "busy" end if not s.enabled then return "notInFleet" end if s.offNetwork then return "offNetwork" end if s.hitched then return "hitched" end return "free" 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] and not tractor.offNetwork 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 suits(t, implement, role) end) if tractor ~= nil then return {implement = implement, tractor = tractor} end if weakest == nil or (implement.neededHp or 0) < (weakest.neededHp or 0) then weakest = implement end end return nil, "tooWeak", {weakest ~= nil and weakest.neededHp or 0, weakest ~= nil and weakest.name or "?"} end -- Alle frie puljeredskaber med rollen, hver parret med den nærmeste endnu ubrugte traktor med nok hk -- (marksilo-flowet planlægger over mange vogne på én gang). function ADPool.pairAll(role, implements, tractors, blocked) blocked = blocked or {} local used, result = {}, {} for _, implement in ipairs(implements or {}) do if hasRole(implement, role) and not blocked[implement.id] then local tractor = nearest(tractors or {}, implement.x, implement.z, function(t) return not used[t.id] and not blocked[t.id] and not t.offNetwork and suits(t, implement, role) end) if tractor ~= nil then used[tractor.id] = true table.insert(result, {implement = implement, tractor = tractor}) end end end return result end -- Ventende marker prøver igen på fælles taktslag (ikke hver sin 30 s fra eget forsøg): så udløber de i samme -- tick, og køens rækkefølge (længst ventende først) afgør hvem der får den næste ledige traktor. function ADPool.alignedRetry(nowMs, periodMs) return (math.floor(nowMs / periodMs) + 1) * periodMs 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.quarantineMinutes(id, nowMs) local untilMs = quarantined[id] if untilMs == nil or nowMs >= untilMs then return 0 end return math.ceil((untilMs - nowMs) / 60000) end function ADPool.isReserved(id) for _, ids in pairs(reservations) do for _, reserved in ipairs(ids) do if reserved == id then return true end end end return false 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 -- Traktorer på hjemtur (sætter redskab af, kører hjem): optaget for markarbejdet til de er hjemme. local returning = setmetatable({}, {__mode = "k"}) function ADPool.setReturning(vehicle, isOn) returning[vehicle] = isOn and true or nil end function ADPool.isReturning(vehicle) return returning[vehicle] == true end -- ---- spil ---- local function parkMarker(adEnv, vehicle) local index = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil if type(index) ~= "number" or index < 1 then return nil end return adEnv.ADGraphManager:getMapMarkerById(index) end local function positionOf(vehicle) local x, _, z = getWorldTranslation(vehicle.rootNode) return x, z end -- Butiksdata: redskabets krævede effekt (hk). specs indlæses først, når butikken har vist varen. local neededHpCache = {} -- configFileName -> hk eller false (butiksdata ændrer sig ikke i et spil) local function neededHpOf(vehicle) if g_storeManager == nil or vehicle.configFileName == nil then return nil end local cached = neededHpCache[vehicle.configFileName] if cached ~= nil then return cached or nil end local value = ADPool.readNeededHp(vehicle) neededHpCache[vehicle.configFileName] = value or false return value end function ADPool.readNeededHp(vehicle) local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName) if item == nil then return nil end if item.specs == nil and StoreItemUtil ~= nil and StoreItemUtil.loadSpecsFromXML ~= nil then pcall(StoreItemUtil.loadSpecsFromXML, item) end return item.specs ~= nil and ADPool.numberOf(item.specs.neededPower) or nil end local tractorHpCache = setmetatable({}, {__mode = "k"}) -- vehicle -> hk eller false -- Traktorens effekt i hk: motorens topeffekt (kW) omregnet; ellers butiksdata. Gemmes pr. traktor. local function tractorHpOf(vehicle) local cached = tractorHpCache[vehicle] if cached ~= nil then return cached or nil end local value = ADPool.readTractorHp(vehicle) tractorHpCache[vehicle] = value or false return value end function ADPool.readTractorHp(vehicle) local motor = vehicle.spec_motorized ~= nil and vehicle.spec_motorized.motor or nil local kw = motor ~= nil and motor.peakMotorPower or nil if type(kw) == "number" and kw > 0 then return kw * 1.35962 end if g_storeManager == nil or vehicle.configFileName == nil then return nil end local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName) return item ~= nil and item.specs ~= nil and ADPool.numberOf(item.specs.power) or nil end -- Butikskategori (fx "TRACTORSM", "TRUCKS") eller nil. local function categoryOf(vehicle) if g_storeManager == nil or vehicle.configFileName == nil then return nil end local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName) return item ~= nil and item.categoryName or nil end -- Varer og samlet kapacitet i redskabets fyldenheder (tømmevogne). function ADPool.cargoOf(vehicle) local carries, capacity = {}, 0 local units = vehicle.spec_fillUnit ~= nil and vehicle.spec_fillUnit.fillUnits or {} for index in ipairs(units) do capacity = capacity + (vehicle:getFillUnitCapacity(index) or 0) for fillType in pairs(vehicle:getFillUnitSupportedFillTypes(index) or {}) do carries[fillType] = true end end return carries, capacity end -- Ikke bundet af et kørende flow (traktorer i stoppede flows er ledige). function ADPool.isFreeFromFlows(vehicle) if ADFlowWagons ~= nil then return ADFlowWagons.isFreeTractor(vehicle) end return ADRunsController.getRunForVehicle(vehicle) == nil end local function isAttached(vehicle) return vehicle.getAttacherVehicle ~= nil and vehicle:getAttacherVehicle() ~= nil end -- Traktorens egen rolle uden de redskaber der hænger på (mejetærsker/selvkørende har en, en traktor ikke). function ADPool.ownRole(vehicle) local specs = {} for _, name in ipairs(ADCpBridge.SPECS) do if vehicle[name] ~= nil then specs[name] = true end end return ADFieldFleet.roleFromSpecs(specs) end -- Puljeredskaber (frakoblet, parkering "X", markør "Connect X") og puljetraktorer. -- isBusy(vehicle): markarbejdets optaget-regel (opgave, AD/CP kører, lånt af marksilo, på hjemtur). function ADPool.collect(adEnv, isBusy) local markers = adEnv.ADGraphManager:getMapMarkers() local farmId = g_currentMission:getFarmId() local implements, tractors = {}, {} -- spillets egen liste: frakoblede redskaber er ikke nødvendigvis i AutoDrive's køretøjsliste for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles) do local owned = vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId if owned and vehicle.spec_motorized == nil and vehicle.spec_attachable ~= nil and not isAttached(vehicle) then local park = parkMarker(adEnv, vehicle) local connect = park ~= nil and ADPool.connectFor(park.name, markers) or nil local specs = connect ~= nil and ADCpBridge.getSpecs(vehicle) or nil local role = specs ~= nil and ADPool.roleOf(specs) or nil local held = role ~= nil and ADFlowWagons ~= nil and ADFlowWagons.isHeldWagon(ADRunsController.getVehicleId(vehicle)) if role ~= nil and not held then local x, z = positionOf(vehicle) local carries, capacity = ADPool.cargoOf(vehicle) table.insert(implements, {carries = carries, capacity = capacity, id = ADRunsController.getVehicleId(vehicle), vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle), role = role, roles = specs.sprayRoles or ADFieldFleet.rolesFor(role), x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle), neededHp = neededHpOf(vehicle), connectId = connect.id, parkId = park.id, }) end end end local state = ADFieldJobs.state for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do local id = ADRunsController.getVehicleId(vehicle) local park = parkMarker(adEnv, vehicle) local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() if id ~= nil and park ~= nil and vehicle.spec_attacherJoints ~= nil and #ADHitch.rearImplementsOf(vehicle) == 0 and ADPool.isFreeFromFlows(vehicle) and not controlled and not isBusy(vehicle) and not ADPool.isReturning(vehicle) and ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id) and ADPool.canPull(ADPool.ownRole(vehicle)) then local x, z = positionOf(vehicle) -- uden for AD-vejnettet: den kan ikke køre ud efter redskabet (ville sende redskaber i karantæne) local networkDistance = ADFieldWork.distanceToNetwork(adEnv, x, z) table.insert(tractors, {id = id, vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle), x = x, z = z, hp = tractorHpOf(vehicle), category = categoryOf(vehicle), parkId = park.id, offNetwork = networkDistance ~= nil and networkDistance > ADFieldWork.MAX_OFF_NETWORK}) end end return implements, tractors end -- Oversigt til flådefanen: alle redskaber med en markrolle (også dem der mangler parkering/Connect, så -- spilleren kan se hvorfor) og alle traktorer uden egen arbejdsrolle, der ikke hører til et flow. function ADPool.overview(adEnv) local markers = adEnv.ADGraphManager:getMapMarkers() local farmId = g_currentMission:getFarmId() local nowMs = ADFieldJobs.now or 0 local implements, tractors = {}, {} for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles) do local owned = vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId if owned and vehicle.spec_motorized == nil and vehicle.spec_attachable ~= nil then local role = ADPool.roleOf(ADCpBridge.getSpecs(vehicle)) if role ~= nil then local id = ADRunsController.getVehicleId(vehicle) local park = parkMarker(adEnv, vehicle) local attacher = isAttached(vehicle) and vehicle:getAttacherVehicle() or nil local key, arg = ADPool.implementStatus({ parkName = park ~= nil and park.name or nil, hasConnect = park ~= nil and ADPool.connectFor(park.name, markers) ~= nil, attachedTo = attacher ~= nil and ADRuns.vehicleLabel(attacher) or nil, quarantineMin = id ~= nil and ADPool.quarantineMinutes(id, nowMs) or 0, reserved = id ~= nil and ADPool.isReserved(id), flowName = id ~= nil and ADFlowWagons ~= nil and ADFlowWagons.isHeldWagon(id) and tostring(ADFlowWagons.ownerOf(ADRunsController.store.runs, id).name) or nil, }) table.insert(implements, {id = id, label = ADRuns.vehicleLabel(vehicle), role = role, status = key, arg = arg}) end end end local state = ADFieldJobs.state for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do local id = ADRunsController.getVehicleId(vehicle) -- en traktor med et redskab der giver den en markrolle, står allerede i flåden som kombination if id ~= nil and vehicle.spec_attacherJoints ~= nil and ADPool.isFreeFromFlows(vehicle) and ADPool.canPull(ADPool.ownRole(vehicle)) and ADFieldFleet.roleFromSpecs(ADCpBridge.getSpecs(vehicle)) == nil then local x, z = positionOf(vehicle) local distance = ADFieldWork.distanceToNetwork(adEnv, x, z) local assignment = state.assignments[id] local rear = ADHitch.rearImplementsOf(vehicle) local key, arg = ADPool.tractorStatus({ hasPark = parkMarker(adEnv, vehicle) ~= nil, enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id), controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled(), returning = ADPool.isReturning(vehicle), fieldId = assignment ~= nil and assignment.fieldId or nil, busy = ADFieldWork.isBusyVehicle(vehicle, id), offNetwork = distance ~= nil and distance > ADFieldWork.MAX_OFF_NETWORK, hitched = #rear > 0, }) table.insert(tractors, {id = id, label = ADRuns.vehicleLabel(vehicle), hp = tractorHpOf(vehicle), status = key, arg = arg, enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id)}) end end return implements, tractors end