feat(marker): vejr (vent på tørvejr), påfyldning via AutoDrive LOAD+CP, beskeder i spillet; Start nu virker på bare marker
This commit is contained in:
@@ -159,6 +159,23 @@ function ADCpBridge.isPlantableNow(fruitIndex)
|
|||||||
return ADCpBridge.isPlantableIn(fruit, environment ~= nil and environment.currentPeriod or 0)
|
return ADCpBridge.isPlantableIn(fruit, environment ~= nil and environment.currentPeriod or 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Hvilken vare sættet bruger og kan få fyldt på af AutoDrive: frø (såmaskine) eller sprøjtens/sprederens
|
||||||
|
-- nuværende fyld (gødning, kalk, sprøjtemiddel). nil = intet at fylde på (strigle, plov ...).
|
||||||
|
function ADCpBridge.getRefillFillType(vehicle)
|
||||||
|
for _, object in ipairs(collect(vehicle)) do
|
||||||
|
if object.spec_sowingMachine ~= nil and g_fillTypeManager ~= nil then
|
||||||
|
return g_fillTypeManager:getFillTypeIndexByName("SEEDS")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, object in ipairs(collect(vehicle)) do
|
||||||
|
if object.spec_sprayer ~= nil and object.getSprayerFillUnitIndex ~= nil then
|
||||||
|
local ok, fillType = pcall(function() return object:getFillUnitFillType(object:getSprayerFillUnitIndex()) end)
|
||||||
|
if ok and fillType ~= nil and fillType ~= 0 then return fillType end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
-- Stop køretøjets AI-job (Courseplay/hjælper).
|
-- Stop køretøjets AI-job (Courseplay/hjælper).
|
||||||
function ADCpBridge.stop(vehicle)
|
function ADCpBridge.stop(vehicle)
|
||||||
pcall(function()
|
pcall(function()
|
||||||
|
|||||||
@@ -31,12 +31,19 @@ end
|
|||||||
|
|
||||||
-- Beslutning for én mark. assignments = sæt på marken ({phase, stage, helper}); kun sæt på det aktuelle
|
-- Beslutning for én mark. assignments = sæt på marken ({phase, stage, helper}); kun sæt på det aktuelle
|
||||||
-- trin tæller (en aflæsningsvogn på vej hjem fra høsten forstyrrer ikke kultiveringen).
|
-- trin tæller (en aflæsningsvogn på vej hjem fra høsten forstyrrer ikke kultiveringen).
|
||||||
-- Svar: {action = "start"|"care"|"advance"|"dispatch"|"wait"|"none", role, count, helper, stage, care}.
|
-- env = {raining = bool}: vejrfølsomme trin (høst/slå/vend/riv/hø) sendes ikke ud i regn.
|
||||||
function ADFieldJobs.decide(field, info, assignments, now)
|
-- Svar: {action = "start"|"care"|"advance"|"dispatch"|"wait"|"none", role, count, helper, stage, care, reason}.
|
||||||
|
function ADFieldJobs.decide(field, info, assignments, now, env)
|
||||||
local hasMarker = info ~= nil and info.markerId ~= nil
|
local hasMarker = info ~= nil and info.markerId ~= nil
|
||||||
|
local raining = env ~= nil and env.raining == true
|
||||||
|
local kind = field.kind or (info ~= nil and info.kind) or "mowable"
|
||||||
if field.stage == "ready" then
|
if field.stage == "ready" then
|
||||||
if not ADFieldModel.shouldStart(field) then return {action = "none"} end
|
if not ADFieldModel.shouldStart(field) then return {action = "none"} end
|
||||||
if not hasMarker then return {action = "wait"} end
|
if not hasMarker then return {action = "wait"} end
|
||||||
|
local first = ADFieldModel.workStages(field.plan, info.kind, info.needs)[1]
|
||||||
|
if raining and first ~= nil and ADFieldModel.isWeatherSensitive(first, field.plan, info.kind) then
|
||||||
|
return {action = "wait", reason = "rain"}
|
||||||
|
end
|
||||||
return {action = "start"}
|
return {action = "start"}
|
||||||
end
|
end
|
||||||
if not ADFieldModel.isWorking(field) then
|
if not ADFieldModel.isWorking(field) then
|
||||||
@@ -68,6 +75,9 @@ function ADFieldJobs.decide(field, info, assignments, now)
|
|||||||
end
|
end
|
||||||
if not hasMarker then return {action = "wait"} end
|
if not hasMarker then return {action = "wait"} end
|
||||||
if (field.retryAt or 0) > now then return {action = "none"} end
|
if (field.retryAt or 0) > now then return {action = "none"} end
|
||||||
|
if raining and ADFieldModel.isWeatherSensitive(field.stage, field.plan, kind) then
|
||||||
|
return {action = "wait", reason = "rain"}
|
||||||
|
end
|
||||||
return {action = "dispatch", role = ADFieldModel.roleFor(field.stage), count = ADFieldModel.maxRigs(field.stage, field.plan), helper = false}
|
return {action = "dispatch", role = ADFieldModel.roleFor(field.stage), count = ADFieldModel.maxRigs(field.stage, field.plan), helper = false}
|
||||||
end
|
end
|
||||||
-- hovedsættene arbejder: fyld op med hjælpere (aflæsningsvogne) en ad gangen
|
-- hovedsættene arbejder: fyld op med hjælpere (aflæsningsvogne) en ad gangen
|
||||||
@@ -166,6 +176,25 @@ function ADFieldJobs.stopField(adEnv, id)
|
|||||||
ADFieldJobs.log(id, "stoppet fra menuen")
|
ADFieldJobs.log(id, "stoppet fra menuen")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Besked i spillet (højre side). critical = rød. Samme tekst vises kun én gang i træk.
|
||||||
|
function ADFieldJobs.notify(text, critical)
|
||||||
|
if ADFieldJobs.lastNotice == text then return end
|
||||||
|
ADFieldJobs.lastNotice = text
|
||||||
|
pcall(function()
|
||||||
|
local kind = critical and FSBaseMission.INGAME_NOTIFICATION_CRITICAL or FSBaseMission.INGAME_NOTIFICATION_OK
|
||||||
|
g_currentMission:addIngameNotification(kind, "Smart Pickup: " .. text)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Vejret lige nu (regn), læst en gang pr. tick.
|
||||||
|
function ADFieldJobs.environment()
|
||||||
|
local ok, raining = pcall(function()
|
||||||
|
local weather = g_currentMission.environment.weather
|
||||||
|
return weather ~= nil and weather.getIsRaining ~= nil and weather:getIsRaining() or false
|
||||||
|
end)
|
||||||
|
return {raining = ok and raining == true}
|
||||||
|
end
|
||||||
|
|
||||||
-- Seneste beslutning pr. mark (GUI viser pleje-behov og grunde).
|
-- Seneste beslutning pr. mark (GUI viser pleje-behov og grunde).
|
||||||
ADFieldJobs.lastDecision = {}
|
ADFieldJobs.lastDecision = {}
|
||||||
|
|
||||||
@@ -174,7 +203,7 @@ local function stepField(adEnv, info)
|
|||||||
ADFieldJobs.setField(field)
|
ADFieldJobs.setField(field)
|
||||||
local assignments = ADFieldJobs.assignmentsFor(info.id)
|
local assignments = ADFieldJobs.assignmentsFor(info.id)
|
||||||
for _, done in ipairs(finishedFor(info.id)) do table.insert(assignments, done) end
|
for _, done in ipairs(finishedFor(info.id)) do table.insert(assignments, done) end
|
||||||
local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now)
|
local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now, ADFieldJobs.environment())
|
||||||
ADFieldJobs.lastDecision[info.id] = decision
|
ADFieldJobs.lastDecision[info.id] = decision
|
||||||
if decision.action == "start" then
|
if decision.action == "start" then
|
||||||
local started = ADFieldModel.start(field, info.kind, info.needs)
|
local started = ADFieldModel.start(field, info.kind, info.needs)
|
||||||
@@ -192,7 +221,14 @@ local function stepField(adEnv, info)
|
|||||||
ADFieldJobs.forgetFinished(info.id)
|
ADFieldJobs.forgetFinished(info.id)
|
||||||
ADFieldWork.windDownHelpers(adEnv, info.id, field.stage)
|
ADFieldWork.windDownHelpers(adEnv, info.id, field.stage)
|
||||||
ADFieldJobs.log(info.id, "%s -> %s", field.stage, nextField.stage)
|
ADFieldJobs.log(info.id, "%s -> %s", field.stage, nextField.stage)
|
||||||
|
if nextField.stage == "done" or (ADFieldModel.isCare(field.stage) and nextField.stage == "idle") then
|
||||||
|
ADFieldJobs.notify(string.format("F%d %s", info.id, g_i18n ~= nil and g_i18n:getText("spu_field_notice_done") or "done"), false)
|
||||||
|
end
|
||||||
ADFieldJobs.save()
|
ADFieldJobs.save()
|
||||||
|
elseif decision.action == "wait" and decision.reason == "rain" then
|
||||||
|
if field.reason == nil or field.reason.key ~= "spu_field_reason_rain" then
|
||||||
|
ADFieldJobs.note(info.id, {reason = {key = "spu_field_reason_rain"}})
|
||||||
|
end
|
||||||
elseif decision.action == "dispatch" then
|
elseif decision.action == "dispatch" then
|
||||||
ADFieldWork.dispatch(adEnv, field, info, decision)
|
ADFieldWork.dispatch(adEnv, field, info, decision)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -178,7 +178,8 @@ function ADFieldModel.onReadiness(field, ratio)
|
|||||||
local copy = copyField(field)
|
local copy = copyField(field)
|
||||||
if (field.stage == "idle" or field.stage == "done") and ratio >= ADFieldModel.READY_RATIO then
|
if (field.stage == "idle" or field.stage == "done") and ratio >= ADFieldModel.READY_RATIO then
|
||||||
copy.stage = "ready"
|
copy.stage = "ready"
|
||||||
elseif field.stage == "ready" and ratio < ADFieldModel.CUT_RATIO then
|
elseif field.stage == "ready" and ratio < ADFieldModel.CUT_RATIO and not field.startRequested then
|
||||||
|
-- (en Start nu-forespørgsel holder marken klar: bare marker har klar-andel 0)
|
||||||
copy.stage = "idle"
|
copy.stage = "idle"
|
||||||
copy.startRequested = false
|
copy.startRequested = false
|
||||||
end
|
end
|
||||||
@@ -256,6 +257,14 @@ function ADFieldModel.stop(field)
|
|||||||
return copy
|
return copy
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Vejrfølsomme trin: afgrøde og skår skal være tørre (høst, slåning, vending, rivning, hø-opsamling).
|
||||||
|
-- Jordbehandling, såning, pleje og ensilage-opsamling kan køre i regn.
|
||||||
|
local WEATHER_STAGES = {harvesting = true, mowing = true, tedding = true, raking = true}
|
||||||
|
function ADFieldModel.isWeatherSensitive(stage, plan, kind)
|
||||||
|
if WEATHER_STAGES[stage] then return true end
|
||||||
|
return stage == "collecting" and ADFieldModel.product(plan, kind or "mowable") == "hay"
|
||||||
|
end
|
||||||
|
|
||||||
-- Pleje under vækst: hvilket pleje-trin marken trænger til nu (eller nil). info fra adFields:
|
-- Pleje under vækst: hvilket pleje-trin marken trænger til nu (eller nil). info fra adFields:
|
||||||
-- {kind, growing, fert, fertMax, lime, weed}. Højst MAX_CARE_RUNS pr. pleje pr. cyklus.
|
-- {kind, growing, fert, fertMax, lime, weed}. Højst MAX_CARE_RUNS pr. pleje pr. cyklus.
|
||||||
function ADFieldModel.careNeed(field, info)
|
function ADFieldModel.careNeed(field, info)
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ end
|
|||||||
local function fail(fieldId, text)
|
local function fail(fieldId, text)
|
||||||
note(fieldId, {retryAt = now() + ADFieldModel.RETRY_MS, reason = {text = tostring(text)}})
|
note(fieldId, {retryAt = now() + ADFieldModel.RETRY_MS, reason = {text = tostring(text)}})
|
||||||
log(fieldId, "%s", tostring(text))
|
log(fieldId, "%s", tostring(text))
|
||||||
|
ADFieldJobs.notify(string.format("F%s: %s", tostring(fieldId), tostring(text)), true)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Hvad et sæt skal kunne ud over rollen (afgrøde, frø, ukrudtets størrelse).
|
-- Hvad et sæt skal kunne ud over rollen (afgrøde, frø, ukrudtets størrelse).
|
||||||
@@ -339,6 +340,32 @@ local function prepareCollector(adEnv, field, info, assignment)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Påfyldning undervejs (såmaskine/sprøjte/spreder): AutoDrive LOAD med automatisk påfyldningsmål og
|
||||||
|
-- hjælper = Courseplay. Når CP melder tom (onCpEmpty), kører AD til nærmeste påfyldning af varen, fylder
|
||||||
|
-- op, kører tilbage til F-markøren og genstarter CP. Courseplay og AutoDrive er bygget til netop dette.
|
||||||
|
local function prepareRefill(adEnv, field, info, assignment)
|
||||||
|
local vehicle = assignment.vehicle
|
||||||
|
local fillType = ADCpBridge.getRefillFillType(vehicle)
|
||||||
|
if fillType == nil then return end
|
||||||
|
local module = stateModule(vehicle)
|
||||||
|
local ok = pcall(function()
|
||||||
|
module:setMode(adEnv.AutoDrive.MODE_LOAD)
|
||||||
|
module:setFirstMarkerByWayPointId(info.markerId)
|
||||||
|
module:setSecondMarkerByWayPointId(info.markerId)
|
||||||
|
module:setAutomaticPickupTarget(true)
|
||||||
|
module:setFillType(fillType)
|
||||||
|
module:setStartHelper(true)
|
||||||
|
module.usedHelper = helperCp(adEnv)
|
||||||
|
enableParkAtJobFinished(vehicle)
|
||||||
|
module:raiseDirtyFlag()
|
||||||
|
end)
|
||||||
|
if ok then
|
||||||
|
assignment.adHandlesEnd = true
|
||||||
|
local name = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
|
||||||
|
log(field.id, "'%s' fyldes op med %s af AutoDrive når den er tom", ADRuns.vehicleLabel(vehicle), tostring(name))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Aflæsningsvogn under høst: Courseplays combine-unloader med stationen fra silovalget.
|
-- Aflæsningsvogn under høst: Courseplays combine-unloader med stationen fra silovalget.
|
||||||
local function beginUnloader(adEnv, field, info, assignment)
|
local function beginUnloader(adEnv, field, info, assignment)
|
||||||
local vehicle = assignment.vehicle
|
local vehicle = assignment.vehicle
|
||||||
@@ -407,7 +434,12 @@ local function beginWork(adEnv, field, info, assignment)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if assignment.role == "collector" and not prepareCollector(adEnv, field, info, assignment) then return end
|
if assignment.role == "collector" then
|
||||||
|
if not prepareCollector(adEnv, field, info, assignment) then return end
|
||||||
|
assignment.adHandlesEnd = true
|
||||||
|
elseif assignment.role == "seeder" or assignment.role == "fertilizer" or assignment.role == "limer" or assignment.role == "weeder" then
|
||||||
|
prepareRefill(adEnv, field, info, assignment)
|
||||||
|
end
|
||||||
if assignment.role == "seeder" then
|
if assignment.role == "seeder" then
|
||||||
local okSeed, seedError = ADCpBridge.setSeedFruit(vehicle, ADFieldWork.sowFruitIndex(field))
|
local okSeed, seedError = ADCpBridge.setSeedFruit(vehicle, ADFieldWork.sowFruitIndex(field))
|
||||||
if not okSeed then
|
if not okSeed then
|
||||||
@@ -462,8 +494,8 @@ end
|
|||||||
local function finishAssignment(adEnv, assignment)
|
local function finishAssignment(adEnv, assignment)
|
||||||
local vehicle = assignment.vehicle
|
local vehicle = assignment.vehicle
|
||||||
log(assignment.fieldId, "%s '%s' færdig", assignment.role, ADRuns.vehicleLabel(vehicle))
|
log(assignment.fieldId, "%s '%s' færdig", assignment.role, ADRuns.vehicleLabel(vehicle))
|
||||||
-- læssevognen parkeres af AutoDrive selv (onCpFinished med parkering sat)
|
-- læssevogn/påfyldte sæt parkeres af AutoDrive selv (onCpFinished med parkering sat)
|
||||||
local park = assignment.role ~= "collector" and parkMarkerId(adEnv, vehicle) or nil
|
local park = not assignment.adHandlesEnd and parkMarkerId(adEnv, vehicle) or nil
|
||||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||||
restoreToolOffset(assignment)
|
restoreToolOffset(assignment)
|
||||||
assignment.phase = "finished"
|
assignment.phase = "finished"
|
||||||
|
|||||||
@@ -151,6 +151,8 @@
|
|||||||
<text name="spu_field_stage_weeding" text="Ukrudt bekæmpes"/>
|
<text name="spu_field_stage_weeding" text="Ukrudt bekæmpes"/>
|
||||||
<text name="spu_field_unloaders" text="Aflæsningsvogne under høst"/>
|
<text name="spu_field_unloaders" text="Aflæsningsvogne under høst"/>
|
||||||
<text name="spu_field_weed" text="Bekæmp ukrudt"/>
|
<text name="spu_field_weed" text="Bekæmp ukrudt"/>
|
||||||
|
<text name="spu_field_notice_done" text="er færdig"/>
|
||||||
|
<text name="spu_field_reason_rain" text="venter på tørvejr"/>
|
||||||
<text name="spu_button_start" text="Start run"/>
|
<text name="spu_button_start" text="Start run"/>
|
||||||
<text name="spu_button_stop" text="Stop run"/>
|
<text name="spu_button_stop" text="Stop run"/>
|
||||||
<text name="spu_button_new" text="Nyt run"/>
|
<text name="spu_button_new" text="Nyt run"/>
|
||||||
|
|||||||
@@ -151,6 +151,8 @@
|
|||||||
<text name="spu_field_stage_weeding" text="Weeding"/>
|
<text name="spu_field_stage_weeding" text="Weeding"/>
|
||||||
<text name="spu_field_unloaders" text="Unloading trailers during harvest"/>
|
<text name="spu_field_unloaders" text="Unloading trailers during harvest"/>
|
||||||
<text name="spu_field_weed" text="Control weeds"/>
|
<text name="spu_field_weed" text="Control weeds"/>
|
||||||
|
<text name="spu_field_notice_done" text="is done"/>
|
||||||
|
<text name="spu_field_reason_rain" text="waiting for dry weather"/>
|
||||||
<text name="spu_button_start" text="Start run"/>
|
<text name="spu_button_start" text="Start run"/>
|
||||||
<text name="spu_button_stop" text="Stop run"/>
|
<text name="spu_button_stop" text="Stop run"/>
|
||||||
<text name="spu_button_new" text="New run"/>
|
<text name="spu_button_new" text="New run"/>
|
||||||
|
|||||||
@@ -75,5 +75,19 @@ check("J10 manuel -> intet, men behov vist", manualCare.action, "none"); check("
|
|||||||
check("J10 Start nu -> pleje", J.decide(M.with(M.newField(60), {startRequested = true}), growingInfo, {}, 0).action, "care")
|
check("J10 Start nu -> pleje", J.decide(M.with(M.newField(60), {startRequested = true}), growingInfo, {}, 0).action, "care")
|
||||||
check("J10 uden markør -> vent", J.decide(autoField, {id = 60, kind = "harvest", growing = true, fert = 0, fertMax = 2}, {}, 0).action, "wait")
|
check("J10 uden markør -> vent", J.decide(autoField, {id = 60, kind = "harvest", growing = true, fert = 0, fertMax = 2}, {}, 0).action, "wait")
|
||||||
|
|
||||||
|
-- J11: regn — vejrfølsomme trin sendes ikke ud (kørende sæt fortsætter); klargøring/pleje kører videre
|
||||||
|
local rain = {raining = true}
|
||||||
|
local dRain = J.decide(mowing, info, {}, 0, rain)
|
||||||
|
check("J11 slåning venter i regn", dRain.action, "wait"); check("J11 grund", dRain.reason, "rain")
|
||||||
|
check("J11 kørende slåmaskine fortsætter", J.decide(mowing, info, {{phase = "working"}}, 0, rain).action, "none")
|
||||||
|
check("J11 høst venter", J.decide(harvesting, harvestInfo, {}, 0, rain).reason, "rain")
|
||||||
|
check("J11 kultivering kører i regn", J.decide(cultivating, afterInfo, {}, 0, rain).role, "cultivator")
|
||||||
|
check("J11 start af høst venter", J.decide(M.with(M.onReadiness(M.newField(51), 1), {startRequested = true, kind = "harvest"}), harvestInfo, {}, 0, rain).reason, "rain")
|
||||||
|
local silageCollect = M.advance(mowing)
|
||||||
|
check("J11 ensilage-opsamling kører i regn", J.decide(silageCollect, info, {}, 0, rain).action, "dispatch")
|
||||||
|
local hayCollect = M.with(silageCollect, {plan = {product = "hay"}})
|
||||||
|
check("J11 hø-opsamling venter", J.decide(hayCollect, info, {}, 0, rain).reason, "rain")
|
||||||
|
check("J11 tørvejr", J.decide(mowing, info, {}, 0, {raining = false}).action, "dispatch")
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||||
os.exit(failures == 0 and 0 or 1)
|
os.exit(failures == 0 and 0 or 1)
|
||||||
|
|||||||
@@ -227,5 +227,12 @@ check("M17 etape 1-post: hø", legacy.plan.product, "hay")
|
|||||||
check("M17 etape 1-post: standard kalk til", legacy.plan.lime, true)
|
check("M17 etape 1-post: standard kalk til", legacy.plan.lime, true)
|
||||||
check("M17 etape 1-post: 1 sæt", legacy.plan.rigs, 1)
|
check("M17 etape 1-post: 1 sæt", legacy.plan.rigs, 1)
|
||||||
|
|
||||||
|
-- M18: Start nu på en bar mark (klar-andel 0) må ikke nulstilles af vækst-reglen før den starter
|
||||||
|
local bare = M.with(M.forceReady(M.newField(70)), {startRequested = true})
|
||||||
|
check("M18 bar mark forbliver klar", M.onReadiness(bare, 0).stage, "ready")
|
||||||
|
check("M18 uden forespørgsel -> venter", M.onReadiness(M.forceReady(M.newField(71)), 0).stage, "idle")
|
||||||
|
check("M18 vejrfølsom: høst", M.isWeatherSensitive("harvesting", {}, "harvest"), true)
|
||||||
|
check("M18 vejrfølsom: kultivering ikke", M.isWeatherSensitive("cultivating", {}, "harvest"), false)
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||||
os.exit(failures == 0 and 0 or 1)
|
os.exit(failures == 0 and 0 or 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user