-- 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 -- 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 kun til hovedopgaver med ét sæt, og ikke når marken har egne faste maskiner til rollen. function ADPool.mayUsePool(decision, hasOwnForRole) return not decision.helper and (decision.count or 1) == 1 and not hasOwnForRole 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.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 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.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 -- 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 -- Traktorens effekt i hk: motorens topeffekt (kW) omregnet; ellers butiksdata. local function tractorHpOf(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 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 if role ~= nil then local x, z = positionOf(vehicle) table.insert(implements, { 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 ADRunsController.getRunForVehicle(vehicle) == nil 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), 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), }) 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) if id ~= nil and vehicle.spec_attacherJoints ~= nil and ADRunsController.getRunForVehicle(vehicle) == nil and ADPool.canPull(ADPool.ownRole(vehicle)) 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