Precomputed, Synchronized Height Map

This bundle is marked as high quality. It exceeds standards and is highly desirable.
Using terrain height to perform actions that must be synced between players is dangerous because the natives such as GetLocationZ and BlzGetUnitZ are not guaranteed to return the same value for all players. This resource generates 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 your functions with the GetLocZ and GetUnitZ functions provided in this library. 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. The height map will remain 99.9% (Δ < 1) accurate.

The GetLocZ function is about twice as fast as the traditional function defined as:
Lua:
    function GetLocZ(x, y)
        MoveLocation(moveableLoc, x, y)
        return GetLocationZ(moveableLoc)
    end

PrecomputedHeightMap also includes the GetTerrainZ function, which returns the exact height of the terrain geometry, which is actually slightly different than what the GetLocationZ function returns.

Lua:
if Debug then Debug.beginFile "PrecomputedHeightMap" end ---@diagnostic disable: param-type-mismatch
do
    --[[
    ===============================================================================================================================================================
                                                                    Precomputed Height Map
                                                                        by Antares
    ===============================================================================================================================================================
   
    GetLocZ(x, y)                               Returns the same value as GetLocationZ(x, y).
    GetTerrainZ(x, y)                           Returns the exact height of the terrain geometry.
    GetUnitZ(whichUnit)                         Returns the same value as BlzGetUnitZ(whichUnit).
    GetUnitCoordinates(whichUnit)               Returns x, y, and z-coordinates of a unit.
    ===============================================================================================================================================================
    Computes the terrain height of your map on map initialization for later use. The function GetLocZ replaces the traditional GetLocZ, defined as:
    function GetLocZ(x, y)
        MoveLocation(moveableLoc, x, y)
        return GetLocationZ(moveableLoc)
    end
    The function provided in this library cannot cause desyncs and is approximately twice as fast. GetTerrainZ is a variation of GetLocZ that returns the exact height
    of the terrain geometry (around cliffs, it has to approximate).
    Note: PrecomputedHeightMap initializes OnitInit.final, because otherwise walkable doodads would not be registered.
    ===============================================================================================================================================================
    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.
    --=============================================================================================================================================================
                                                                          C O N F I G
    --=============================================================================================================================================================
    ]]
    local SUBFOLDER                         = "PrecomputedHeightMap"
    --Where to store data when exporting height map.
    local STORE_CLIFF_DATA                  = true
    --If set to false, GetTerrainZ will be less accurate around cliffs, but slightly faster.
    local STORE_WATER_DATA                  = true
    --Set to true if you have water cliffs and have STORE_CLIFF_DATA enabled.
    local WRITE_HEIGHT_MAP                  = false
    --Write height map to file?
    local VALIDATE_HEIGHT_MAP               = true
    --Check if height map read from string is accurate.
    local VISUALIZE_HEIGHT_MAP              = false
    --Create a special effect at each grid point to double-check if the height map is correct.
    --=============================================================================================================================================================
    local heightMap                         = {}        ---@type table[]
    local terrainHasCliffs                  = {}        ---@type table[]
    local terrainCliffLevel                 = {}        ---@type table[]
    local terrainHasWater                   = {}        ---@type table[]
    local moveableLoc                       = nil       ---@type location
    local MINIMUM_Z                         = -2048     ---@type number
    local CLIFF_HEIGHT                      = 128       ---@type number
    local worldMinX
    local worldMinY
    local worldMaxX
    local worldMaxY
    local iMax
    local jMax
    local chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!$&()[]=?:;,._#*~/{}<>^"
    local NUMBER_OF_CHARS = string.len(chars)
    ---@param x number
    ---@param y number
    ---@return number
    function GetLocZ(x, y)
        MoveLocation(moveableLoc, x, y)
        return GetLocationZ(moveableLoc)
    end
   
    GetTerrainZ = GetLocZ
    ---@param whichUnit unit
    ---@return number
    function GetUnitZ(whichUnit)
        return GetLocZ(GetUnitX(whichUnit), GetUnitY(whichUnit)) + GetUnitFlyHeight(whichUnit)
    end
    ---@param whichUnit unit
    ---@return number, number, number
    function GetUnitCoordinates(whichUnit)
        local x = GetUnitX(whichUnit)
        local y = GetUnitY(whichUnit)
        return x, y, GetLocZ(x, y) + GetUnitFlyHeight(whichUnit)
    end
    local function OverwriteHeightFunctions()
        ---@param x number
        ---@param y number
        ---@return number
        GetLocZ = function(x, y)
            local rx = (x - worldMinX)*0.0078125 + 1
            local ry = (y - worldMinY)*0.0078125 + 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
            local heightMapI = heightMap[i]
            local heightMapIplus1 = heightMap[i+1]
            return (1 - ry)*((1 - rx)*heightMapI[j] + rx*heightMapIplus1[j]) + ry*((1 - rx)*heightMapI[j+1] + rx*heightMapIplus1[j+1])
        end
        if STORE_CLIFF_DATA then
            ---@param x number
            ---@param y number
            ---@return number
            GetTerrainZ = function(x, y)
                local rx = (x - worldMinX)*0.0078125 + 1
                local ry = (y - worldMinY)*0.0078125 + 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
                            if STORE_WATER_DATA and terrainHasWater[i][j] then
                                return heightMap[i][j]
                            else
                                return (1 - rx - ry)*heightMap[i][j] + (rx*(heightMap[i+1][j] - CLIFF_HEIGHT*(terrainCliffLevel[i+1][j] - terrainCliffLevel[i][j])) + ry*(heightMap[i][j+1] - CLIFF_HEIGHT*(terrainCliffLevel[i][j+1] - terrainCliffLevel[i][j])))
                            end
                        elseif STORE_WATER_DATA and terrainHasWater[i][j] then
                            return heightMap[i][j+1]
                        elseif rx + ry > 1 then
                            return (rx + ry - 1)*(heightMap[i+1][j+1] - CLIFF_HEIGHT*(terrainCliffLevel[i+1][j+1] - terrainCliffLevel[i][j+1])) + ((1 - rx)*heightMap[i][j+1] + (1 - ry)*(heightMap[i+1][j] - CLIFF_HEIGHT*(terrainCliffLevel[i+1][j] - terrainCliffLevel[i][j+1])))
                        else
                            return (1 - rx - ry)*(heightMap[i][j] - CLIFF_HEIGHT*(terrainCliffLevel[i][j] - terrainCliffLevel[i][j+1])) + (rx*(heightMap[i+1][j] - CLIFF_HEIGHT*(terrainCliffLevel[i+1][j] - terrainCliffLevel[i][j+1])) + ry*heightMap[i][j+1])
                        end
                    elseif ry < 0.5 then
                        if STORE_WATER_DATA and terrainHasWater[i][j] then
                            return heightMap[i+1][j]
                        elseif rx + ry > 1 then
                            return (rx + ry - 1)*(heightMap[i+1][j+1] - CLIFF_HEIGHT*(terrainCliffLevel[i+1][j+1] - terrainCliffLevel[i+1][j])) + ((1 - rx)*(heightMap[i][j+1] - CLIFF_HEIGHT*(terrainCliffLevel[i][j+1] - terrainCliffLevel[i+1][j])) + (1 - ry)*heightMap[i+1][j])
                        else
                            return (1 - rx - ry)*(heightMap[i][j] - CLIFF_HEIGHT*(terrainCliffLevel[i][j] - terrainCliffLevel[i+1][j])) + (rx*heightMap[i+1][j] + ry*(heightMap[i][j+1] - CLIFF_HEIGHT*(terrainCliffLevel[i][j+1] - terrainCliffLevel[i+1][j])))
                        end
                    elseif STORE_WATER_DATA and terrainHasWater[i][j] then
                        return heightMap[i+1][j+1]
                    else
                        return (rx + ry - 1)*heightMap[i+1][j+1] + ((1 - rx)*(heightMap[i][j+1] - CLIFF_HEIGHT*(terrainCliffLevel[i][j+1] - terrainCliffLevel[i+1][j+1])) + (1 - ry)*(heightMap[i+1][j] - CLIFF_HEIGHT*(terrainCliffLevel[i+1][j] - terrainCliffLevel[i+1][j+1])))
                    end
                else
                    if rx + ry > 1 then --In top-right triangle
                        local heightMapIplus1 = heightMap[i+1]
                        return (rx + ry - 1)*heightMapIplus1[j+1] + ((1 - rx)*heightMap[i][j+1] + (1 - ry)*heightMapIplus1[j])
                    else
                        local heightMapI = heightMap[i]
                        return (1 - rx - ry)*heightMapI[j] + (rx*heightMap[i+1][j] + ry*heightMapI[j+1])
                    end
                end
            end
        else
            ---@param x number
            ---@param y number
            ---@return number
            GetTerrainZ = function(x, y)
                local rx = (x - worldMinX)*0.0078125 + 1
                local ry = (y - worldMinY)*0.0078125 + 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
                    local heightMapIplus1 = heightMap[i+1]
                    return (rx + ry - 1)*heightMapIplus1[j+1] + ((1 - rx)*heightMap[i][j+1] + (1 - ry)*heightMapIplus1[j])
                else
                    local heightMapI = heightMap[i]
                    return (1 - rx - ry)*heightMapI[j] + (rx*heightMap[i+1][j] + ry*heightMapI[j+1])
                end
            end
        end
    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] = {}
            if STORE_CLIFF_DATA then
                terrainHasCliffs[i] = {}
                terrainCliffLevel[i] = {}
                if STORE_WATER_DATA then
                    terrainHasWater[i] = {}
                end
            end
            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 + 128, y + 128)
                    if level1 ~= level2 or level1 ~= level3 or level1 ~= level4 then
                        terrainHasCliffs[i][j] = true
                    end
                    terrainCliffLevel[i][j] = level1
                    if STORE_WATER_DATA then
                        terrainHasWater[i][j] = not IsTerrainPathable(x, y, PATHING_TYPE_FLOATABILITY)
                        or not IsTerrainPathable(x, y + 128, PATHING_TYPE_FLOATABILITY)
                        or not IsTerrainPathable(x + 128, y, PATHING_TYPE_FLOATABILITY)
                        or not IsTerrainPathable(x + 128, y + 128, PATHING_TYPE_FLOATABILITY)
                    end
                end
                j = j + 1
                y = y + 128
            end
            i = i + 1
            x = x + 128
        end
        iMax = i - 2
        jMax = j - 2
    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, NUMBER_OF_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] = {}
            if STORE_CLIFF_DATA then
                terrainHasCliffs[i] = {}
                terrainCliffLevel[i] = {}
                if STORE_WATER_DATA then
                    terrainHasWater[i] = {}
                end
            end
            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 + 128, y + 128)
                    if level1 ~= level2 or level1 ~= level3 or level1 ~= level4 then
                        terrainHasCliffs[i][j] = true
                    end
                    terrainCliffLevel[i][j] = level1
                    if STORE_WATER_DATA then
                        terrainHasWater[i][j] = not IsTerrainPathable(x, y, PATHING_TYPE_FLOATABILITY)
                        or not IsTerrainPathable(x, y + 128, PATHING_TYPE_FLOATABILITY)
                        or not IsTerrainPathable(x + 128, y, PATHING_TYPE_FLOATABILITY)
                        or not IsTerrainPathable(x + 128, y + 128, PATHING_TYPE_FLOATABILITY)
                    end
                end
                j = j + 1
                y = y + 128
            end
            i = i + 1
            x = x + 128
        end
        iMax = i - 2
        jMax = j - 2
    end
    OnInit.final("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 bj_isSinglePlayer and VALIDATE_HEIGHT_MAP then
                ValidateHeightMap()
            end
        else
            CreateHeightMap()
            if WRITE_HEIGHT_MAP then
                WriteHeightMap(SUBFOLDER)
            end
        end
        OverwriteHeightFunctions()
    end)
end
Contents

PrecomputedHeightMap (Map)

PrecomputedHeightMap (Binary)

Reviews
Wrda
local chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" local NUMBER_OF_CHARS = 52 You can just do #chars to get the length of the string, it's equivalent to string.len(str). local function ReadHeightMap() local charPos = 0 local...
Level 2
Joined
Sep 3, 2023
Messages
8
Nice system, but since this precomputes the heights, can this work with in-game terrain deformation spells?
 
Update:
  • Improved the accuracy of GetCliffAdjustedZ.
  • Added a new function:
    Lua:
        ---@param whichUnit unit
        ---@return number, number, number
        function GetUnitCoordinates(whichUnit)
            local x = GetUnitX(whichUnit)
            local y = GetUnitY(whichUnit)
            return x, y, GetTerrainZ(x, y) + GetUnitFlyHeight(whichUnit)
        end
 
Last edited:
Update:
  • Returned initialization from OnInit.global to OnInit.final, because walkable doodads do not get registered otherwise.
  • Added new GetLocZ function, which returns the exact same value as GetLocationZ. GetTerrainZ returns the exact height of the terrain geometry.
  • GetTerrainZ now either accounts for cliffs or not based on the config. GetCliffAdjustedZ has been removed.

Update 2:
  • Added the STORE_WATER_DATA option to increase accuracy of GetTerrainZ around water cliffs.
 
Last edited:

Wrda

Spell Reviewer
Level 28
Joined
Nov 18, 2012
Messages
2,010
Lua:
local chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
local NUMBER_OF_CHARS = 52
You can just do #chars to get the length of the string, it's equivalent to string.len(str).

Lua:
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
Perfect place to use NUMBER_OF_CHARS instead of string.len since you already declared it before :p.

I didn't find any problems while testing the file, reading, writing and validating. Just make a note that the test map has 2 outdated locations, even though it looks intentional, it seemed a bit weird.

This is rather an excellent approach for anyone who likes to deal with the Z axis. Now we can truly do "3D" maps without traumatic headaches of desyncs!

Approved
 
Lua:
local chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
local NUMBER_OF_CHARS = 52
You can just do #chars to get the length of the string, it's equivalent to string.len(str).

Lua:
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
Perfect place to use NUMBER_OF_CHARS instead of string.len since you already declared it before :p.
You definitely are thorough. :prazz:

I didn't find any problems while testing the file, reading, writing and validating. Just make a note that the test map has 2 outdated locations, even though it looks intentional, it seemed a bit weird.
It actually wasn't intentional :plol:. I think I changed the terrain once to test something, then reverted it not correctly. But it's actually a good idea to show off that functionality.

This is rather an excellent approach for anyone who likes to deal with the Z axis. Now we can truly do "3D" maps without traumatic headaches of desyncs!

Approved

Thank you!
 
Any plans to port this into vJass?
Otherwise I might try doing a port
You would have to get around two restrictions in JASS. One is the array size limit of 32768, which means the map can't be larger than sqrt(32768) = 181. The other is the string size limit, which means you can't read the entire string at once and have to store it in an array.

I'm not really motivated to do this myself, but I can help you out if you wanna give it a shot.
 
Actually the vJass array limit is 409550 so it should be doable. I'll tinker a bit myself and reach out if I am stuck
Where do you have that number from? If you do,
vJASS:
scope Test initializer Init
	globals
		integer array testArray
	endglobals

	private function Init takes nothing returns nothing
		set testArray[32767] = 1
		call BJDebugMsg(I2S(testArray[32767]))
		set testArray[32768] = 2
		call BJDebugMsg(I2S(testArray[32768]))
		set testArray[100000] = 3
		call BJDebugMsg(I2S(testArray[100000]))
		set testArray[409549] = 4
		call BJDebugMsg(I2S(testArray[409549]))
	endfunction
endscope
it will print 1, 0, 0, 0, so I'm relatively sure that the 32768 number is correct.
 

Uncle

Warcraft Moderator
Level 73
Joined
Aug 10, 2018
Messages
7,866
Actually the vJass array limit is 409550 so it should be doable. I'll tinker a bit myself and reach out if I am stuck
Last change was 8,192 -> 32,768
 

SHBlade

Hosted Project GR
Level 14
Joined
May 1, 2013
Messages
224
Where do you have that number from? If you do,
vJASS:
scope Test initializer Init
    globals
        integer array testArray
    endglobals

    private function Init takes nothing returns nothing
        set testArray[32767] = 1
        call BJDebugMsg(I2S(testArray[32767]))
        set testArray[32768] = 2
        call BJDebugMsg(I2S(testArray[32768]))
        set testArray[100000] = 3
        call BJDebugMsg(I2S(testArray[100000]))
        set testArray[409549] = 4
        call BJDebugMsg(I2S(testArray[409549]))
    endfunction
endscope
it will print 1, 0, 0, 0, so I'm relatively sure that the 32768 number is correct.

Last change was 8,192 -> 32,768
Let me rephrase, yes you are right the max array size is there. However vJass supports 2d arrays, so everything up to [640][639] (408960 elements) is legal.

EDIT:
Yeah I managed to get the CreateMap function to work, however this part
JASS:
                   set level1 = GetTerrainCliffLevel(x, y)
                   set level2 = GetTerrainCliffLevel(x, y + 128)
                   set level3 = GetTerrainCliffLevel(x + 128, y)
                   set level4 = GetTerrainCliffLevel(x + 128, y + 128)
crashes the map when put inside a loop so the vJass version might be limited
 
Last edited:
Let me rephrase, yes you are right the max array size is there. However vJass supports 2d arrays, so everything up to [640][639] (408960 elements) is legal.
Oh wow, I didn't know that vJASS did that. So, it automatically creates multiple arrays when you declare a 2D array above the array size limit. If you look at the j-file, though, that is a monstrosity. It still uses the outdated array size limit of 8192, so it has to create a lot of arrays. Might be worth trying to manage that manually to optimize it, once the system works. Otherwise, you're talking about a ~100-200x performance drop compared to the Lua version, if I had to take a guess.

EDIT:
Yeah I managed to get the CreateMap function to work, however this part
JASS:
                   set level1 = GetTerrainCliffLevel(x, y)
                   set level2 = GetTerrainCliffLevel(x, y + 128)
                   set level3 = GetTerrainCliffLevel(x + 128, y)
                   set level4 = GetTerrainCliffLevel(x + 128, y + 128)
crashes the map when put inside a loop so the vJass version might be limited
Are you hitting the OP limit during initialization, maybe? Maybe have a 0.001 second timer that computes each column separately.
 
Last edited:
Wonder the odds of turning this into scanning a maps terrain then porting it into a hashtable look up for jass...
Would certainly be nice to have jass heightmaps finally a visible reality without the cost of how long it takes to generate in jass.
How are you envisioning this? You want to generate a script that has a hard-coded number via a hashtable entry for each map tile? For a 256x256 map, this would be ~62,500*28 = 1.75 million characters, so 1.75MB. Reading in the script would take ~62,500*500ns = ~30 milliseconds. So, if you're ok with the increase in map size, it seems doable.
 
Top