• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!

Precomputed, Synchronized Height Map

This bundle is marked as pending. It has not been reviewed by a staff member yet.
Generate a terrain height map that makes using z-coordinates desync safe without the need to synchronize it between players via the sync natives. To get a relatively desync-safe height map, simply copy this library into your map and replace GetLocZ with GetTerrainZ, and BlzGetUnitZ with GetUnitZ. This might suffice for most applications. To be extra safe, you then have the ability to write the height map to a file and reimport it. Now, whenever your map is started, the height map will be read from the imported data, making it guaranteed to be synced between players.

GetTerrainZ is 99.99% (Δ < 1) accurate and about twice as fast as the traditional GetLocZ function:
Lua:
    function GetLocZ(x, y)
        MoveLocation(moveableLoc, x, y)
        return GetLocationZ(moveableLoc)
    end

The height map requires ~1MB of RAM on a 192x192 map and the string size on a map of that size is ~40kB.

This library also includes the GetCliffAdjustedZ function, which returns a different value around cliffs, which is more in line with the visual representation.

A JASS translation is, in general, possible, but the speed advantage will evaporate and there are a lot of annoying limitations of JASS that need to be worked around.

Lua:
if Debug then Debug.beginFile "PrecomputedHeightMap" end
do LIBRARY_PrecomputedHeightMap = true
    --[[
    ===============================================================================================================================================================
                                                                    Precomputed Height Map
                                                                        by Antares
    ===============================================================================================================================================================
    
    GetTerrainZ(x, y)                           Replaces GetLocZ(x, y).
    GetUnitZ(whichUnit)                         Replaces BlzGetUnitZ(whichUnit).
    GetCliffAdjustedZ(x, y)                     Returns a value in tiles neighboring cliffs that is more aligned with the visual effect. Requires STORE_CLIFF_DATA.

    ===============================================================================================================================================================

    Computes the terrain height of your map on map initialization for later use. To get the terrain height at any location, use GetTerrainZ, replacing GetLocZ:

    function GetLocZ(x, y)
        MoveLocation(moveableLoc, x, y)
        return GetLocationZ(moveableLoc)
    end

    GetTerrainZ is less prone to cause desyncs and is approximately twice as fast.

    You have the option to save the height map to a file on map initialization. You can then reimport the data into the map to load the height map from that data.
    This will make the use of Z-coordinates completely safe, as all clients are guaranteed to use exactly the same data. It is recommended to do this once for the
    release version of your map.

    To do this, set the flag for WRITE_HEIGHT_MAP and launch your map. The terrain height map will be generated on map initialization and saved to a file in your
    Warcraft III\CustomMapData\ folder. Open that file in a text editor, then remove all occurances of

    	call Preload( "
    " )
    
    with find and replace (including the quotation marks and tab space). Then, remove

    function PreloadFiles takes nothing returns nothing
	    call PreloadStart()

    at the beginning of the file and

        call PreloadEnd( 0.0 )
    endfunction

    at the end of the file. Finally, remove all line breaks by removing \n and \r. The result should be something like

    HeightMapCode = "|pk44mM-b+b1-dr|krjdhWcy1aa1|eWcyaa"

    except much longer.

    Copy the entire string and paste it anywhere into the Lua root in your map, for example into the Config section of this library. Now, every time your map is
    launched, the height map will be read from the string instead of being generated, making it guaranteed to be synced.

    To check if the code has been generated correctly, launch your map one more time in single-player. The height map generated from the code will be checked against
    one generated in the traditional way.

    --=============================================================================================================================================================
                                                                            Config
    --=============================================================================================================================================================
    ]]

    local SUBFOLDER                         = "PrecomputedHeightMap"
    local STORE_CLIFF_DATA                  = true
    local WRITE_HEIGHT_MAP                  = false
    local VALIDATE_HEIGHT_MAP               = true
    local VISUALIZE_HEIGHT_MAP              = true

    --=============================================================================================================================================================

    local heightMap                         = {}        ---@type table[]
    local terrainHasCliffs                  = {}        ---@type table[]
    local moveableLoc                       = nil       ---@type location

    local MINIMUM_Z                         = -256      ---@type number

    local worldMinX
    local worldMinY
    local worldMaxX
    local worldMaxY

    local iMax
    local jMax

    local chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    local NUMBER_OF_CHARS = 52

    local function GetLocZ(x, y)
        MoveLocation(moveableLoc, x, y)
        return GetLocationZ(moveableLoc)
    end
	
	GetTerrainZ = GetLocZ

    function GetUnitZ(whichUnit)
        return GetTerrainZ(GetUnitX(whichUnit), GetUnitY(whichUnit)) + GetUnitFlyHeight(whichUnit)
    end

    local function CreateHeightMap()
        local xMin = (worldMinX // 128)*128
        local yMin = (worldMinY // 128)*128
        local xMax = (worldMaxX // 128)*128 + 1
        local yMax = (worldMaxY // 128)*128 + 1

        local x = xMin
        local y
        local i = 1
        local j
        while x <= xMax do
            heightMap[i] = {}
            terrainHasCliffs[i] = {}
            y = yMin
            j = 1
            while y <= yMax do
                heightMap[i][j] = GetLocZ(x,y)
                if VISUALIZE_HEIGHT_MAP then
                    BlzSetSpecialEffectZ(AddSpecialEffect("Doodads\\Cinematic\\GlowingRunes\\GlowingRunes0", x, y), heightMap[i][j] - 40)
                end
                if STORE_CLIFF_DATA then
                    local level1 = GetTerrainCliffLevel(x, y)
                    local level2 = GetTerrainCliffLevel(x, y + 128)
                    local level3 = GetTerrainCliffLevel(x + 128, y)
                    local level4 = GetTerrainCliffLevel(x, y + 128)
                    if level1 ~= level2 or level1 ~= level3 or level1 ~= level4 then
                        terrainHasCliffs[i][j] = true
                    end
                end
                j = j + 1
                y = y + 128
            end
            i = i + 1
            x = x + 128
        end

        iMax = i - 2
        jMax = j - 2
		
    	---@param x number
		---@param y number
		---@return number
        GetTerrainZ = function(x, y)
            local rx = (x - worldMinX)/128 + 1
            local ry = (y - worldMinY)/128 + 1
            local i = rx // 1
            local j = ry // 1
            rx = rx - i
            ry = ry - j
            if i < 1 then
                i = 1
                rx = 0
            elseif i > iMax then
                i = iMax
                rx = 1
            end
            if j < 1 then
                j = 1
                ry = 0
            elseif j > jMax then
                j = jMax
                ry = 1
            end

            if rx + ry > 1 then --In top-right triangle
                return (rx + ry - 1)*heightMap[i+1][j+1] + ((1 - rx)*heightMap[i][j+1] + (1 - ry)*heightMap[i+1][j])
            else
                return (1 - rx - ry)*heightMap[i][j] + (rx*heightMap[i+1][j] + ry*heightMap[i][j+1])
            end
        end
    end

    ---@param x number
    ---@param y number
    ---@return number
    function GetCliffAdjustedZ(x, y)
        local rx = (x - worldMinX)/128 + 1
        local ry = (y - worldMinY)/128 + 1
        local i = rx // 1
        local j = ry // 1
        rx = rx - i
        ry = ry - j
        if i < 1 then
            i = 1
            rx = 0
        elseif i > iMax then
            i = iMax
            rx = 1
        end
        if j < 1 then
            j = 1
            ry = 0
        elseif j > jMax then
            j = jMax
            ry = 1
        end

        if terrainHasCliffs[i][j] then
            if rx < 0.5 then
                if ry < 0.5 then
                    return heightMap[i][j]
                else
                    return heightMap[i][j+1]
                end
            elseif ry < 0.5 then
                return heightMap[i+1][j]
            else
                return heightMap[i+1][j+1]
            end
        else
            if rx + ry > 1 then --In top-right triangle
                return (rx + ry - 1)*heightMap[i+1][j+1] + ((1 - rx)*heightMap[i][j+1] + (1 - ry)*heightMap[i+1][j])
            else
                return (1 - rx - ry)*heightMap[i][j] + (rx*heightMap[i+1][j] + ry*heightMap[i][j+1])
            end
        end
    end

    local function ValidateHeightMap()
        local xMin = (worldMinX // 128)*128
        local yMin = (worldMinY // 128)*128
        local xMax = (worldMaxX // 128)*128 + 1
        local yMax = (worldMaxY // 128)*128 + 1

        local numOutdated = 0

        local x = xMin
        local y
        local i = 1
        local j
        while x <= xMax do
            y = yMin
            j = 1
            while y <= yMax do
                if heightMap[i][j] then
                    if VISUALIZE_HEIGHT_MAP then
                        BlzSetSpecialEffectZ(AddSpecialEffect("Doodads\\Cinematic\\GlowingRunes\\GlowingRunes0", x, y), heightMap[i][j] - 40)
                    end
                    if bj_isSinglePlayer and math.abs(heightMap[i][j] - GetLocZ(x, y)) > 1 then
                        numOutdated = numOutdated + 1
                    end
                else
                    print("Height Map nil at x = " .. x .. ", y = " .. y)
                end
                j = j + 1
                y = y + 128
            end
            i = i + 1
            x = x + 128
        end
        
        if numOutdated > 0 then
            print("|cffff0000Warning:|r Height Map is outdated at " .. numOutdated .. " locations...")
        end
    end

    local function ReadHeightMap()
        local charPos = 0
        local numRepetitions = 0
        local charValues = {}
    
        for i = 1, string.len(chars) do
            charValues[string.sub(chars, i, i)] = i - 1
        end
    
        local firstChar = nil
    
        local PLUS = 0
        local MINUS = 1
        local ABS = 2
        local segmentType = ABS
    
        for i = 1, #heightMap do
            for j = 1, #heightMap[i] do
                if numRepetitions > 0 then
                    heightMap[i][j] = heightMap[i][j-1]
                    numRepetitions = numRepetitions - 1
                else
                    local valueDetermined = false
                    while not valueDetermined do
                        charPos = charPos + 1
                        local char = string.sub(HeightMapCode, charPos, charPos)
                        if char == "+" then
                            segmentType = PLUS
                            charPos = charPos + 1
                            char = string.sub(HeightMapCode, charPos, charPos)
                        elseif char == "-" then
                            segmentType = MINUS
                            charPos = charPos + 1
                            char = string.sub(HeightMapCode, charPos, charPos)
                        elseif char == "|" then
                            segmentType = ABS
                            charPos = charPos + 1
                            char = string.sub(HeightMapCode, charPos, charPos)
                        end
                        if tonumber(char) then
                            local k = 0
                            while tonumber(string.sub(HeightMapCode, charPos + k + 1, charPos + k + 1)) do
                                k = k + 1
                            end
                            numRepetitions = tonumber(string.sub(HeightMapCode, charPos, charPos + k)) - 1
                            charPos = charPos + k
                            valueDetermined = true
                            heightMap[i][j] = heightMap[i][j-1]
                        else
                            if segmentType == PLUS then
                                heightMap[i][j] = heightMap[i][j-1] + charValues[char]
                                valueDetermined = true
                            elseif segmentType == MINUS then
                                heightMap[i][j] = heightMap[i][j-1] - charValues[char]
                                valueDetermined = true
                            elseif firstChar then
                                if charValues[firstChar] and charValues[char] then
                                    heightMap[i][j] = charValues[firstChar]*NUMBER_OF_CHARS + charValues[char] + MINIMUM_Z
                                else
                                    heightMap[i][j] = 0
                                end
                                firstChar = nil
                                valueDetermined = true
                            else
                                firstChar = char
                            end
                        end
                    end
                end
            end
        end
        HeightMapCode = nil
    end

    local function WriteHeightMap(subfolder)
        PreloadGenClear()
        PreloadGenStart()
    
        local numRepetitions = 0
        local firstChar
        local secondChar
        local stringLength = 0
        local lastValue = 0
    
        local PLUS = 0
        local MINUS = 1
        local ABS = 2
        local segmentType = ABS
        local preloadString = {'HeightMapCode = "'}

        for i = 1, #heightMap do
            for j = 1, #heightMap[i] do
                if j > 1 then
                    local diff = (heightMap[i][j] - lastValue)//1
                    if diff == 0 then
                        numRepetitions = numRepetitions + 1
                    else
                        if numRepetitions > 0 then
                            table.insert(preloadString, numRepetitions)
                        end
                        numRepetitions = 0

                        if diff > 0 and diff < NUMBER_OF_CHARS then
                            if segmentType ~= PLUS then
                                segmentType = PLUS
                                table.insert(preloadString, "+")
                            end
                        elseif diff < 0 and diff > -NUMBER_OF_CHARS then
                            if segmentType ~= MINUS then
                                segmentType = MINUS
                                table.insert(preloadString, "-")
                            end
                        else
                            if segmentType ~= ABS then
                                segmentType = ABS
                                table.insert(preloadString, "|")
                            end
                        end
    
                        if segmentType == ABS then
                            firstChar = (heightMap[i][j] - MINIMUM_Z) // NUMBER_OF_CHARS + 1
                            secondChar = heightMap[i][j]//1 - MINIMUM_Z - (heightMap[i][j]//1 - MINIMUM_Z)//NUMBER_OF_CHARS*NUMBER_OF_CHARS + 1
                            table.insert(preloadString, string.sub(chars, firstChar, firstChar) .. string.sub(chars, secondChar, secondChar))
                        elseif segmentType == PLUS then
                            firstChar = diff//1 + 1
                            table.insert(preloadString, string.sub(chars, firstChar, firstChar))
                        elseif segmentType == MINUS then
                            firstChar = -diff//1 + 1
                            table.insert(preloadString, string.sub(chars, firstChar, firstChar))
                        end
                    end
                else
                    if numRepetitions > 0 then
                        table.insert(preloadString, numRepetitions)
                    end
                    segmentType = ABS
                    table.insert(preloadString, "|")
                    numRepetitions = 0
                    firstChar = (heightMap[i][j] - MINIMUM_Z) // NUMBER_OF_CHARS + 1
                    secondChar = heightMap[i][j]//1 - MINIMUM_Z - (heightMap[i][j]//1 - MINIMUM_Z)//NUMBER_OF_CHARS*NUMBER_OF_CHARS + 1
                    table.insert(preloadString, string.sub(chars, firstChar, firstChar) .. string.sub(chars, secondChar, secondChar))
                end
    
                lastValue = heightMap[i][j]//1
    
                stringLength = stringLength + 1
                if stringLength == 100 then
                    Preload(table.concat(preloadString))
                    stringLength = 0
                    for k, __ in ipairs(preloadString) do
                        preloadString[k] = nil
                    end
                end
            end
        end
    
        if numRepetitions > 0 then
            table.insert(preloadString, numRepetitions)
        end
    
        table.insert(preloadString, '"')
        Preload(table.concat(preloadString))
    
        PreloadGenEnd(subfolder .. "\\heightMap.txt")
    
        print("Written Height Map to CustomMapData\\" .. subfolder .. "\\heightMap.txt")
    end

    local function InitHeightMap()
        local xMin = (worldMinX // 128)*128
        local yMin = (worldMinY // 128)*128
        local xMax = (worldMaxX // 128)*128 + 1
        local yMax = (worldMaxY // 128)*128 + 1

        local x = xMin
        local y
        local i = 1
        local j
        while x <= xMax do
            heightMap[i] = {}
            terrainHasCliffs[i] = {}
            y = yMin
            j = 1
            while y <= yMax do
                heightMap[i][j] = 0
                if STORE_CLIFF_DATA then
                    local level1 = GetTerrainCliffLevel(x, y)
                    local level2 = GetTerrainCliffLevel(x, y + 128)
                    local level3 = GetTerrainCliffLevel(x + 128, y)
                    local level4 = GetTerrainCliffLevel(x, y + 128)
                    if level1 ~= level2 or level1 ~= level3 or level1 ~= level4 then
                        terrainHasCliffs[i][j] = true
                    end
                end
                j = j + 1
                y = y + 128
            end
            i = i + 1
            x = x + 128
        end
    end

    OnInit.main("PrecomputedHeightMap", function()
        local worldBounds = GetWorldBounds()
        worldMinX = GetRectMinX(worldBounds)
        worldMinY = GetRectMinY(worldBounds)
        worldMaxX = GetRectMaxX(worldBounds)
        worldMaxY = GetRectMaxY(worldBounds)

        moveableLoc = Location(0, 0)

        if HeightMapCode then
            InitHeightMap()
			ReadHeightMap()
            if VALIDATE_HEIGHT_MAP then
                ValidateHeightMap()
            end
        else
            CreateHeightMap()
            if WRITE_HEIGHT_MAP then
                WriteHeightMap(SUBFOLDER)
            end
        end
    end)
end
Contents

PrecomputedHeightMap (Map)

Level 2
Joined
Sep 3, 2023
Messages
6
Nice system, but since this precomputes the heights, can this work with in-game terrain deformation spells?
 
Top