FieldLocalWetness = {}
FieldLocalWetness.name = g_currentModName or "FieldLocalWetness"
FieldLocalWetness.path = g_currentModDirectory or ""

addModEventListener(FieldLocalWetness)

FieldLocalWetness.enabled = false
FieldLocalWetness.debug = false

FieldLocalWetness.cellSize = 18.0
FieldLocalWetness.sampleRadius = 24.0
FieldLocalWetness.lowlandStrength = 0.55
FieldLocalWetness.slopeDrainStrength = 0.35
FieldLocalWetness.effectStrength = 0.80
FieldLocalWetness.permaChanceStrength = 1.25
FieldLocalWetness.permaChanceMinWetness = 0.35
FieldLocalWetness.radiusEffectStrength = 0.75

FieldLocalWetness.rainGainPerHour = 0.82
FieldLocalWetness.snowGainPerHour = 0.18
FieldLocalWetness.hailGainPerHour = 0.34
FieldLocalWetness.dryLossPerHour = 0.16
FieldLocalWetness.baseMoisture = 0.10
FieldLocalWetness.minWetness = 0.04
FieldLocalWetness.maxWetness = 1.00

FieldLocalWetness.scanCellsPerUpdate = 96
FieldLocalWetness.scanOnLoad = true

FieldLocalWetness.DEFAULTS = {
    effectStrength = 0.80,
    cellSize = 18.0,
    lowlandStrength = 0.55,
    permaChanceStrength = 1.25,
    permaChanceMinWetness = 0.35
}

FieldLocalWetness._cells = {}
FieldLocalWetness._weatherMemory = 0
FieldLocalWetness._scan = nil
FieldLocalWetness._lastGlobalUpdateMs = 0
FieldLocalWetness._gameDayOffsetMs = 0
FieldLocalWetness._lastGameDayTimeMs = nil
FieldLocalWetness._lastWeatherGameTimeMs = nil

local function flwClamp(v, mn, mx)
    v = tonumber(v) or 0
    if v < mn then return mn end
    if v > mx then return mx end
    return v
end

local function flwLerp(a, b, t)
    return a + (b - a) * flwClamp(t, 0, 1)
end

local function flwSmoothstep(edge0, edge1, x)
    local t = flwClamp((x - edge0) / math.max(0.0001, edge1 - edge0), 0, 1)
    return t * t * (3 - 2 * t)
end

local function flwActivePrecipScale(v)
    v = flwClamp(v or 0, 0, 1)
    if v <= 0.01 then
        return 0
    end

    -- FS can report long rain as a small rainfall scale. Once precipitation is active,
    -- keep a realistic soaking floor so local wetness does not crawl upward for hours.
    return math.max(v, 0.34 + flwSmoothstep(0.03, 0.45, v) * 0.46)
end

local function flwTableCount(t)
    local count = 0
    if t ~= nil then
        for _ in pairs(t) do
            count = count + 1
        end
    end
    return count
end

local function flwGetGameTimeMs()
    local env = g_currentMission ~= nil and g_currentMission.environment or nil
    local dayTime = env ~= nil and tonumber(env.dayTime) or nil
    if dayTime == nil and env ~= nil and env.weather ~= nil and env.weather.owner ~= nil then
        dayTime = tonumber(env.weather.owner.dayTime)
    end

    if dayTime == nil then
        return g_time or 0
    end

    dayTime = flwClamp(dayTime, 0, 86400000)

    local last = FieldLocalWetness._lastGameDayTimeMs
    if last ~= nil and dayTime < last - 43200000 then
        FieldLocalWetness._gameDayOffsetMs = (FieldLocalWetness._gameDayOffsetMs or 0) + 86400000
    elseif last ~= nil and dayTime > last + 43200000 then
        FieldLocalWetness._gameDayOffsetMs = math.max(0, (FieldLocalWetness._gameDayOffsetMs or 0) - 86400000)
    end

    FieldLocalWetness._lastGameDayTimeMs = dayTime
    return (FieldLocalWetness._gameDayOffsetMs or 0) + dayTime
end

local function flwGetMissionTerrainSize()
    if g_currentMission ~= nil and tonumber(g_currentMission.terrainSize) ~= nil then
        return tonumber(g_currentMission.terrainSize)
    end
    if tonumber(g_terrainSize) ~= nil then
        return tonumber(g_terrainSize)
    end
    return 2048
end

local function flwGetGrid()
    local size = math.max(6.0, tonumber(FieldLocalWetness.cellSize) or 18.0)
    local terrainSize = math.max(size, flwGetMissionTerrainSize())
    local half = terrainSize * 0.5
    local cols = math.max(1, math.ceil(terrainSize / size))
    return size, -half, half, cols
end

local function flwGetProfileAt(x, z)
    if FieldGroundMudPhysics ~= nil and FieldGroundMudPhysics.getProfileAtWorldPos ~= nil then
        local _, _, prof = FieldGroundMudPhysics:getProfileAtWorldPos(x, z)
        return prof
    end
    return nil
end

local function flwGetProfileWetnessMul(prof)
    if prof == nil then
        return 1.0
    end

    local mud = flwClamp(tonumber(prof.mud) or 0, 0, 1)
    local wetMul = tonumber(prof.wetMul) or 1.0
    local sinkMul = tonumber(prof.sinkMul) or 5.4
    local dirtMul = tonumber(prof.dirtMul) or 1.0

    local mul = 0.86
        + mud * 0.22
        + (wetMul - 1.0) * 0.22
        + (sinkMul - 5.4) * 0.035
        + (dirtMul - 1.0) * 0.06

    if prof.permaStuck == true then
        mul = mul + 0.05
    else
        mul = mul - 0.16
    end

    return flwClamp(mul, 0.62, 1.34)
end

local function flwGetProfilePermaMul(prof)
    if prof == nil then
        return 1.0
    end
    if prof.permaStuck ~= true then
        return 0.35
    end

    local mud = flwClamp(tonumber(prof.mud) or 0, 0, 1)
    local wetMul = tonumber(prof.wetMul) or 1.0
    local sinkMul = tonumber(prof.sinkMul) or 5.4
    local brakeMul = tonumber(prof.brakeMul) or 4.0

    local mul = 0.82
        + mud * 0.30
        + (wetMul - 1.0) * 0.18
        + (sinkMul - 5.4) * 0.065
        + (brakeMul - 4.0) * 0.035

    return flwClamp(mul, 0.70, 1.55)
end

local function flwGetWeather()
    if g_currentMission ~= nil
        and g_currentMission.environment ~= nil
        and g_currentMission.environment.weather ~= nil then
        return g_currentMission.environment.weather
    end
    return nil
end

local function flwGetWeatherState()
    local weather = flwGetWeather()
    if weather == nil then
        return 0, 0, 0, nil, 0
    end

    local rain = 0
    if weather.getRainFallScale ~= nil then
        rain = weather:getRainFallScale() or 0
    end

    local snow = 0
    if weather.getSnowFallScale ~= nil then
        snow = weather:getSnowFallScale() or 0
    end

    local hail = 0
    if weather.getHailFallScale ~= nil then
        hail = weather:getHailFallScale() or 0
    end

    local temp = nil
    if weather.getCurrentTemperature ~= nil then
        temp = weather:getCurrentTemperature()
    end

    local cloud = 0
    if weather.cloudUpdater ~= nil and weather.cloudUpdater.currentClouds ~= nil then
        local c = weather.cloudUpdater.currentClouds
        local precip = tonumber(c.precipitation) or 0
        local cirrus = tonumber(c.cirrusCoverage) or 0
        local dens = tonumber(c.densityScale) or 0
        cloud = math.max(precip, flwClamp(dens * 35.0, 0, 1), flwClamp(cirrus * 1.5, 0, 1))
    end

    return flwActivePrecipScale(rain), flwActivePrecipScale(snow) * 0.72, flwActivePrecipScale(hail), temp, flwClamp(cloud, 0, 1)
end

local function flwTerrainHeight(x, z)
    local terrainNode = g_terrainNode
    if (terrainNode == nil or terrainNode == 0) and g_currentMission ~= nil then
        terrainNode = g_currentMission.terrainRootNode
    end
    if terrainNode == nil or terrainNode == 0 or getTerrainHeightAtWorldPos == nil then
        return 0
    end
    return getTerrainHeightAtWorldPos(terrainNode, x, 0, z) or 0
end

local function flwHash01(ix, iz)
    local n = math.sin(ix * 12.9898 + iz * 78.233 + 37.719) * 43758.5453
    return n - math.floor(n)
end

local function flwBuildCellFromSamples(ix, iz, size, minCoord)
    local cx = minCoord + (ix + 0.5) * size
    local cz = minCoord + (iz + 0.5) * size
    local r = math.max(size * 0.75, tonumber(FieldLocalWetness.sampleRadius) or 24.0)

    local h = flwTerrainHeight(cx, cz)
    local hN = flwTerrainHeight(cx, cz + r)
    local hS = flwTerrainHeight(cx, cz - r)
    local hE = flwTerrainHeight(cx + r, cz)
    local hW = flwTerrainHeight(cx - r, cz)
    local hNE = flwTerrainHeight(cx + r, cz + r)
    local hNW = flwTerrainHeight(cx - r, cz + r)
    local hSE = flwTerrainHeight(cx + r, cz - r)
    local hSW = flwTerrainHeight(cx - r, cz - r)

    local avgCardinal = (hN + hS + hE + hW) * 0.25
    local avgAll = (hN + hS + hE + hW + hNE + hNW + hSE + hSW) * 0.125
    local high = math.max(hN, hS, hE, hW, hNE, hNW, hSE, hSW)
    local low = math.min(hN, hS, hE, hW, hNE, hNW, hSE, hSW)
    local relief = math.max(0.01, high - low)

    local lowland = flwClamp((avgAll - h) / math.max(0.45, relief), 0, 1)
    local pocket = flwClamp((avgCardinal - h) / math.max(0.28, relief * 0.75), 0, 1)
    local ridge = flwClamp((h - avgAll) / math.max(0.45, relief), 0, 1)
    local slope = flwClamp(relief / math.max(1.0, r * 0.22), 0, 1)
    local noise = flwHash01(ix, iz)

    local retention = 1.0
        + lowland * (tonumber(FieldLocalWetness.lowlandStrength) or 0.55)
        + pocket * 0.42
        - ridge * 0.30
        - slope * (tonumber(FieldLocalWetness.slopeDrainStrength) or 0.35)
        + (noise - 0.5) * 0.14

    retention = flwClamp(retention, 0.42, 2.05)

    local startMoisture = (tonumber(FieldLocalWetness.baseMoisture) or 0.10) * retention
    startMoisture = startMoisture + (FieldLocalWetness._weatherMemory or 0) * retention

    return {
        ix = ix,
        iz = iz,
        x = cx,
        z = cz,
        height = h,
        moisture = flwClamp(startMoisture, 0, 1),
        retention = retention,
        lowland = flwClamp(math.max(lowland, pocket * 0.85), 0, 1),
        pocket = pocket,
        ridge = ridge,
        slope = slope,
        lastGameTimeMs = flwGetGameTimeMs(),
        lastTimeMs = g_time or 0
    }
end

function FieldLocalWetness:getCellKeyFromIndex(ix, iz)
    return tostring(ix) .. ":" .. tostring(iz)
end

function FieldLocalWetness:getCellIndex(x, z)
    local size, minCoord, _, cols = flwGetGrid()
    local ix = math.floor(((x or 0) - minCoord) / size)
    local iz = math.floor(((z or 0) - minCoord) / size)
    ix = flwClamp(ix, 0, cols - 1)
    iz = flwClamp(iz, 0, cols - 1)
    return ix, iz, size, minCoord
end

function FieldLocalWetness:getCellKey(x, z)
    local ix, iz, size = self:getCellIndex(x, z)
    return self:getCellKeyFromIndex(ix, iz), ix, iz, size
end

function FieldLocalWetness:buildCell(ix, iz, size, minCoord)
    return flwBuildCellFromSamples(ix, iz, size, minCoord)
end

function FieldLocalWetness:getCell(x, z)
    local key, ix, iz, size = self:getCellKey(x, z)
    local cell = self._cells[key]
    if cell == nil then
        local _, minCoord = flwGetGrid()
        cell = self:buildCell(ix, iz, size, minCoord)
        self._cells[key] = cell
    end
    return cell
end

function FieldLocalWetness:startTerrainScan()
    local size, minCoord, _, cols = flwGetGrid()
    self._cells = {}
    self._scan = {
        size = size,
        minCoord = minCoord,
        cols = cols,
        ix = 0,
        iz = 0,
        done = false,
        startedMs = g_time or 0
    }
end

function FieldLocalWetness:updateTerrainScan(maxCells)
    local scan = self._scan
    if scan == nil or scan.done == true then
        return
    end

    local limit = math.max(1, math.floor(tonumber(maxCells) or self.scanCellsPerUpdate or 96))
    local built = 0
    while built < limit and scan.done ~= true do
        local key = self:getCellKeyFromIndex(scan.ix, scan.iz)
        if self._cells[key] == nil then
            self._cells[key] = self:buildCell(scan.ix, scan.iz, scan.size, scan.minCoord)
        end

        built = built + 1
        scan.ix = scan.ix + 1
        if scan.ix >= scan.cols then
            scan.ix = 0
            scan.iz = scan.iz + 1
            if scan.iz >= scan.cols then
                scan.done = true
                if self.debug then
                    print(string.format("[FieldLocalWetness] terrain scan complete: cells=%d cellSize=%.1f", flwTableCount(self._cells), scan.size))
                end
            end
        end
    end
end

function FieldLocalWetness:updateCell(cell)
    if cell == nil then return nil end

    local now = flwGetGameTimeMs()
    local last = cell.lastGameTimeMs or now
    local dtH = math.max(0, now - last) / 3600000
    if dtH <= 0 then
        return cell.moisture or 0
    end
    if dtH > 2.0 then
        dtH = 2.0
    end

    local rain, snow, hail, temp, cloud = flwGetWeatherState()
    local precipGain =
        rain * (tonumber(self.rainGainPerHour) or 0.82)
        + snow * (tonumber(self.snowGainPerHour) or 0.18)
        + hail * (tonumber(self.hailGainPerHour) or 0.34)

    local tempDryMul = 1.0
    if temp ~= nil then
        if temp <= 0 then
            tempDryMul = 0.22
        elseif temp >= 30 then
            tempDryMul = 1.28
        else
            tempDryMul = flwLerp(0.45, 1.12, flwClamp(temp / 30, 0, 1))
        end
    end

    local cloudDryMul = flwLerp(1.00, 0.45, cloud)
    local memoryDryMul = flwLerp(1.00, 0.55, flwClamp((self._weatherMemory or 0) / 0.50, 0, 1))
    local retention = flwClamp(cell.retention or 1.0, 0.42, 2.05)
    local lowland = flwClamp(cell.lowland or 0, 0, 1)
    local pocket = flwClamp(cell.pocket or 0, 0, 1)
    local ridge = flwClamp(cell.ridge or 0, 0, 1)
    local slope = flwClamp(cell.slope or 0, 0, 1)

    local rainK = flwClamp(rain + snow * 0.45 + hail * 0.65, 0, 1)
    local soakMul = 0.78 + retention * 0.45 + lowland * 0.95 + pocket * 0.55
    soakMul = soakMul * flwLerp(1.0, 1.18 + lowland * 0.26 + pocket * 0.16, rainK)
    local drainMul = flwClamp(1.44 - retention * 0.55 - lowland * 0.24 + ridge * 0.10 + slope * 0.06, 0.28, 1.02)

    local gain = precipGain * soakMul * dtH
    local precipDryMul = flwLerp(1.0, 0.18, rainK)
    local dry = (tonumber(self.dryLossPerHour) or 0.16) * tempDryMul * cloudDryMul * memoryDryMul * drainMul * precipDryMul * dtH

    local moisture = (cell.moisture or 0) + gain - dry
    cell.moisture = flwClamp(moisture, tonumber(self.minWetness) or 0.04, tonumber(self.maxWetness) or 1.0)
    cell.lastGameTimeMs = now
    cell.lastTimeMs = g_time or 0

    return cell.moisture
end

function FieldLocalWetness:getWetnessAt(x, z, dt)
    if self.enabled ~= true or x == nil or z == nil then
        return nil
    end

    local cell = self:getCell(x, z)
    return self:updateCell(cell), cell
end

function FieldLocalWetness:getEffectiveWetnessAt(x, z, fallbackWetness, dt, prof)
    if self.enabled ~= true then
        return fallbackWetness or 0
    end

    local localWetness = self:getWetnessAt(x, z, dt)
    if localWetness == nil then
        return fallbackWetness or 0
    end

    prof = prof or flwGetProfileAt(x, z)
    local strength = flwClamp(tonumber(self.effectStrength) or 0.80, 0, 1)
    local profileMul = flwLerp(1.0, flwGetProfileWetnessMul(prof), strength)
    return flwClamp(localWetness * profileMul, 0, 1)
end

function FieldLocalWetness:getVehicleWetness(vehicle, fallbackWetness, dt)
    if self.enabled ~= true or vehicle == nil or vehicle.spec_wheels == nil or vehicle.spec_wheels.wheels == nil then
        return fallbackWetness or 0
    end

    local sum, count = 0, 0
    for _, wheel in ipairs(vehicle.spec_wheels.wheels) do
        if wheel ~= nil and FieldGroundMudPhysics ~= nil and FieldGroundMudPhysics.getWheelContactPos ~= nil then
            local x, _, z = FieldGroundMudPhysics:getWheelContactPos(wheel)
            if x ~= nil then
                local prof = nil
                if FieldGroundMudPhysics.getWheelGroundProfile ~= nil then
                    local _mud, _gt
                    _mud, _gt, prof = FieldGroundMudPhysics:getWheelGroundProfile(wheel)
                else
                    prof = flwGetProfileAt(x, z)
                end

                local wet = self:getWetnessAt(x, z, dt)
                if wet ~= nil then
                    local strength = flwClamp(tonumber(self.effectStrength) or 0.80, 0, 1)
                    wet = flwClamp(wet * flwLerp(1.0, flwGetProfileWetnessMul(prof), strength), 0, 1)
                    sum = sum + wet
                    count = count + 1
                end
            end
        end
    end

    if count <= 0 then
        return fallbackWetness or 0
    end

    return flwClamp(sum / count, 0, 1)
end

function FieldLocalWetness:getPermaChanceMultiplierAt(x, z, wetnessEff, dt, prof)
    if self.enabled ~= true then
        return 1.0
    end

    local wet, cell = self:getWetnessAt(x, z, dt)
    if wet == nil then
        wet = wetnessEff or 0
    end

    prof = prof or flwGetProfileAt(x, z)
    wet = flwClamp(wet * flwGetProfileWetnessMul(prof), 0, 1)

    local minWet = flwClamp(tonumber(self.permaChanceMinWetness) or 0.35, 0, 1)
    local wet01 = flwClamp((wet - minWet) / math.max(0.01, 1 - minWet), 0, 1)
    local lowland = (cell ~= nil and cell.lowland) or 0
    local pocket = (cell ~= nil and cell.pocket) or 0
    local ridge = (cell ~= nil and cell.ridge) or 0
    local slope = (cell ~= nil and cell.slope) or 0
    local strength = math.max(0, tonumber(self.permaChanceStrength) or 1.25)

    local landscape = 0.28 + lowland * 1.18 + pocket * 0.60 - ridge * 0.35 - slope * 0.12
    local mult = 1.0 + wet01 * strength * math.max(0.05, landscape) * flwGetProfilePermaMul(prof)
    mult = mult - ridge * 0.20 * wet01
    return flwClamp(mult, 0.25, 4.75)
end

function FieldLocalWetness:getRadiusEffectMultiplierAt(x, z, wetnessEff, dt, prof)
    if self.enabled ~= true then
        return 1.0
    end

    local wet, cell = self:getWetnessAt(x, z, dt)
    if wet == nil then
        wet = wetnessEff or 0
    end

    prof = prof or flwGetProfileAt(x, z)
    wet = flwClamp(wet * flwGetProfileWetnessMul(prof), 0, 1)

    local lowland = (cell ~= nil and cell.lowland) or 0
    local pocket = (cell ~= nil and cell.pocket) or 0
    local ridge = (cell ~= nil and cell.ridge) or 0
    local slope = (cell ~= nil and cell.slope) or 0
    local strength = flwClamp(tonumber(self.radiusEffectStrength) or 0.75, 0, 2.0)

    local wet01 = flwClamp((wet - 0.12) / 0.88, 0, 1)
    local terrainK = 0.70 + lowland * 0.65 + pocket * 0.45 - ridge * 0.20 - slope * 0.10
    local mult = 1.0 + wet01 * strength * flwClamp(terrainK, 0.35, 1.95)

    return flwClamp(mult, 1.0, 2.35)
end

function FieldLocalWetness:onSettingsChanged()
    local looksLikeOldNilSave =
        (tonumber(self.effectStrength) or 0) <= 0
        and (tonumber(self.lowlandStrength) or 0) <= 0
        and (tonumber(self.permaChanceStrength) or 0) <= 0
        and (tonumber(self.permaChanceMinWetness) or 0) <= 0
        and (tonumber(self.cellSize) or 0) <= 6.0

    if looksLikeOldNilSave then
        for key, value in pairs(self.DEFAULTS or {}) do
            self[key] = value
        end
    end

    local oldCellSize = self._lastCellSize
    self.cellSize = flwClamp(self.cellSize, 6.0, 64.0)
    self.sampleRadius = flwClamp(self.sampleRadius, 8.0, 90.0)
    self.lowlandStrength = flwClamp(self.lowlandStrength, 0, 2.0)
    self.slopeDrainStrength = flwClamp(self.slopeDrainStrength, 0, 2.0)
    self.effectStrength = flwClamp(self.effectStrength, 0, 1)
    self.permaChanceStrength = flwClamp(self.permaChanceStrength, 0, 4.0)
    self.permaChanceMinWetness = flwClamp(self.permaChanceMinWetness, 0, 1)
    self.radiusEffectStrength = flwClamp(self.radiusEffectStrength, 0, 2.0)
    self._lastCellSize = self.cellSize

    if oldCellSize ~= nil and math.abs(oldCellSize - self.cellSize) > 0.01 then
        self:startTerrainScan()
    end
end

function FieldLocalWetness:updateWeatherMemory(dt)
    local now = flwGetGameTimeMs()
    local last = self._lastWeatherGameTimeMs or now
    local dtH = math.max(0, now - last) / 3600000
    self._lastWeatherGameTimeMs = now

    if dtH <= 0 then
        return
    end
    if dtH > 2.0 then
        dtH = 2.0
    end

    local rain, snow, hail, temp, cloud = flwGetWeatherState()
    local precipGain =
        rain * (tonumber(self.rainGainPerHour) or 0.82)
        + snow * (tonumber(self.snowGainPerHour) or 0.18)
        + hail * (tonumber(self.hailGainPerHour) or 0.34)

    local tempDryMul = 1.0
    if temp ~= nil then
        if temp <= 0 then
            tempDryMul = 0.25
        elseif temp >= 30 then
            tempDryMul = 1.45
        else
            tempDryMul = flwLerp(0.60, 1.20, flwClamp(temp / 30, 0, 1))
        end
    end

    local cloudDryMul = flwLerp(1.10, 0.50, cloud)
    local rainK = flwClamp(rain + snow * 0.45 + hail * 0.65, 0, 1)
    local dry = (tonumber(self.dryLossPerHour) or 0.16) * tempDryMul * cloudDryMul * flwLerp(1.0, 0.22, rainK) * dtH
    local gain = precipGain * flwLerp(1.0, 1.28, rainK) * dtH

    self._weatherMemory = flwClamp((self._weatherMemory or 0) + gain - dry, 0, 0.75)
end

function FieldLocalWetness:update(dt)
    if self.enabled ~= true then
        return
    end

    self:updateTerrainScan(self.scanCellsPerUpdate)
    self:updateWeatherMemory(dt)
end

function FieldLocalWetness:loadMap()
    self._cells = {}
    self._weatherMemory = 0
    self._gameDayOffsetMs = 0
    self._lastGameDayTimeMs = nil
    self._lastWeatherGameTimeMs = flwGetGameTimeMs()
    self:onSettingsChanged()
    if self.scanOnLoad == true then
        self:startTerrainScan()
    end
    addConsoleCommand("gsFieldWetness", "Field local wetness on/off/debug/reset/scan/status", "consoleCommand", self)
end

function FieldLocalWetness:deleteMap()
    removeConsoleCommand("gsFieldWetness")
    self._cells = {}
    self._scan = nil
    self._lastWeatherGameTimeMs = nil
end

function FieldLocalWetness:consoleCommand(arg)
    arg = tostring(arg or "")
    if arg == "on" then
        self.enabled = true
    elseif arg == "off" then
        self.enabled = false
    elseif arg == "debug" then
        self.debug = not self.debug
    elseif arg == "reset" then
        self:startTerrainScan()
    elseif arg == "scan" then
        self:startTerrainScan()
        self:updateTerrainScan(1000000000)
    end

    local scan = self._scan
    local scanText = "none"
    if scan ~= nil then
        local done = scan.done == true
        local doneCells = (scan.iz or 0) * (scan.cols or 0) + (scan.ix or 0)
        local total = (scan.cols or 0) * (scan.cols or 0)
        scanText = string.format("%s %d/%d", tostring(done), doneCells, total)
    end

    local rain, snow, hail = flwGetWeatherState()

    return string.format("FieldLocalWetness enabled=%s debug=%s cells=%d scan=%s rain=%.2f snow=%.2f hail=%.2f memory=%.2f",
        tostring(self.enabled),
        tostring(self.debug),
        flwTableCount(self._cells),
        scanText,
        rain or 0,
        snow or 0,
        hail or 0,
        self._weatherMemory or 0)
end
