parseAreaMap - extract positions from "map" table

Discussion in 'Mods & Lua Functions' started by Colandus, Dec 26, 2016.

  1. Colandus

    Colandus Well-Known Member

    Joined:
    Jun 6, 2007
    Messages:
    2,291
    This function will convert a table like this:
    Code:
    local areaMap = {
        {1,2,0,2,1},
        {2,0,0,0,2},
        {0,0,3,0,0},
        {2,0,0,0,2},
        {1,2,0,2,1}
    }
    
    Into this:
    Code:
    {
       [1] = {
          {x=-2, y=-2},
          {x=2, y=-2},
          {x=2, y=2},
          {x=-2, y=2}
       },
       [2] = {
          {x=-2, y=-1},
          {x=-2, y=-1},
          .... ETC (all coordinates where "2" is, relative to center "3").
       },
    }
    
    (Note: you will not get the center position into the table.)

    It's also possible to set center position coordinates, which would instead return a table with real positions, and not positions relative to the area maps center.

    You can define the number which represents the center position on your own. Also, to use numbers above 10, I recommend using variables such as A, B, C etc... Or even starting from 1, considering it might make your area map seem clearer.



    Usage:

    parseAreaMap(areaMapTable[, centerVal=2])
    parseAreaMap(areaMapTable[, centerPos[, centerVal=2] ])

    Parameters:
    areaMapTable: A multi-dimensional table consisting of values (preferably numbers).
    centerPos: [OPTIONAL]: The center position (e.g. a creature or tile position)
    centerVal: [OPTIONAL]: The number representing center position in the area map (default 2)

    Return value: Table consisting of tables with Position objects.



    Example:
    Code:
    local areaMap = {
        {1,2,0,2,1},
        {2,0,0,0,2},
        {0,0,3,0,0},
        {2,0,0,0,2},
        {1,2,0,2,1}
    }
    
    local positions = parseAreaMap(areaMap, player:getPosition(), 3) -- setting 3 as center
    local firstPositions = positions[1]
    local secondPositions = positions[2]
    
    for _, position in ipairs(firstPositions) do
        position:sendMagicEffect(CONST_ME_MAGIC_BLUE)
        -- and some more code here
    end
    
    for _, position in ipairs(secondPositions) do
        position:sendMagicEffect(CONST_ME_MAGIC_GREEN)
        -- some other code here.....
    end
    
    Or, you might loop all areas at the same time:
    Code:
    local positionEffects = {[1] = CONST_ME_MAGIC_BLUE, [2] = CONST_ME_MAGIC_GREEN}
    
    local areaPositions = parseAreaMap(areaMap, player:getPosition(), 3)
    for i, positions in ipairs(areaPositions) do
        for _, position in ipairs(positions) do
            position:sendMagicEffect(positionEffects[i]) -- send magic effect based on value on area map...
        end
    end
    

    The function:
    Code:
    function parseAreaMap(t, ...)
        if type(t) ~= 'table' then
            return false, error("Error: table expected for 'parseAreaMap', got " .. type(t), 2)
        end
    
        local args = {...}
        local centerVal, centerPos
        if type(args[1]) == 'table' then
            centerPos = args[1]
            centerVal = args[2]
        end
    
        centerVal = centerVal or (not centerPos and args[1]) or 2
    
        local rawValues, values, center = {}, {}
    
        for iy, yt in ipairs(t) do
            for ix, v in ipairs(yt) do
                if v ~= 0 then
                    if v == centerVal then
                        if center then
                            return error("Error: Duplicate center positions!", 2)
                        end
    
                        center = Position(-ix, -iy, 0)
                        if(centerPos) then
                            center = centerPos + center
                        end
    
                        -- With center pos just discovered, correct the raw values and insert into values table.
                        for _, raw in ipairs(rawValues) do
                            values[raw.value] = values[raw.value] or {}
                            table.insert(values[raw.value], Position({x = center.x + raw.x, y = center.y + raw.y, z = center.z}))
                        end
                    elseif center then
                        -- We have a center pos, place directly into values.
                        values[v] = values[v] or {}
                        table.insert(values[v], Position({x = center.x + ix, y = center.y + iy, z = center.z}))
                    else
                        -- We don't have a center pos yet. Store raw.
                        table.insert(rawValues, {value = v, x = ix, y = iy})
                    end
                end
            end
        end
    
        if not center then
            return false, error("Error: no center position found!", 2)
        end
    
        return values
    end
    


    Any questions, bugs, suggestions, please reply.
     
    Last edited: Jan 13, 2017
    ahmed30, Printer, divegia and 3 others like this.
  2. Colandus

    Colandus Well-Known Member

    Joined:
    Jun 6, 2007
    Messages:
    2,291
    This function does the same as this guy charges 20€ for over here :eek: Maybe I should make a business of selling overpriced functions? :D I would be rich!

    And this would be an almost complete conversion of his code (working, but missing a function, so it will only show effect but do no damage):
    Code:
    local n = 0
    local areaT = {
        {n,n,4,4,4,n,n},
        {n,4,3,3,3,4,n},
        {4,3,2,2,2,3,4},
        {4,3,2,1,2,3,4},
        {4,3,2,2,2,3,4},
        {n,4,3,3,3,4,n},
        {n,n,4,4,4,n,n},
    }
    
    function touchExplosiveBarrel(barrel)
        local barrelPos = barrel:getPosition()
        local area = parseAreaMap(areaT, barrelPos, 1)
        local timer = 5000
    
        -- good job expensive scripter filling the queue stack with events!!
        for i, posT in pairs(area) do
            for _, pos in pairs(posT) do
                addEvent(Position.sendMagicEffect, timer + (i-1)*150, pos, CONST_ME_FIREAREA)
            end
        end
    
        for msgDelay = 1000, timer, 1000 do
            local secondsLeft = (timer - msgDelay) /1000
            local msg =  secondsLeft.." seconds left"
       
            if secondsLeft == 0 then msg = "Kaboom!" end
            addEvent(text, msgDelay, msg, barrelPos)
        end
    end
    
    function text(msg, pos)
        local players = Game.getSpectators(pos, false, true)
        for _, player in ipairs(players) do
            player:say(msg, TALKTYPE_MONSTER_SAY, false, player, pos)
        end
    end
    
    Anyone who cares could make the "dealDamagePos" function as seen in his original code! This was solely for demonstration and to let you know you have profited 20€ :rolleyes: You're welcome.
     
    Last edited: Dec 27, 2016
  3. tokenzz

    tokenzz :thinking:

    Joined:
    Feb 2, 2013
    Messages:
    703
    replace all table.inserts with regular table inserts, e.g:

    Code:
    local t = {1, 2, 3}
    t[#t + 1] = 4
    
    this would improve performance
     
    Last edited: Dec 27, 2016
  4. cbrm

    cbrm Just another mod Staff Member Global Moderator

    Joined:
    Jan 6, 2009
    Messages:
    6,496
    really?
     
  5. Colandus

    Colandus Well-Known Member

    Joined:
    Jun 6, 2007
    Messages:
    2,291
    Yes I know. I just feel using inserts are cleaner. The difference is too minimal to pay any great attention to imo.
     
    StreamSide likes this.
  6. tokenzz

    tokenzz :thinking:

    Joined:
    Feb 2, 2013
    Messages:
    703
    I'd say it looks much cleaner without the function calls. But it's just a personal preference I guess. Also, it'd require a little refactoring changing it.

    Performance-wise it doesn't really matter anyhow I guess since you don't even have to think about performance in terms of Lua scripts like these (as far as you don't do anything wrong) . However, regular table inserts are faster than using function calls with table.insert(). This is what I meant @cbrm . Thanks for the harsh comment though :)

    good job anyhow
     
  7. Colandus

    Colandus Well-Known Member

    Joined:
    Jun 6, 2007
    Messages:
    2,291
    Yes, it's certainly a personal preference. I used to be the other way first, hated table.insert. Then I started liking them. I guess I don't like the fact I have to repeat the table name twice (longerTableName[#longerTableName + 1]). If table names are shorter (as in your case, t), I usually go for the inline expression.

    Also, I didn't see anyone being harsh. I don't suppose you meant me. I believe cbrm asked out of sincere curiosity.

    All feedback is appreciated though. So, thank you.
     
    Last edited: Dec 27, 2016
    tokenzz likes this.
  8. tokenzz

    tokenzz :thinking:

    Joined:
    Feb 2, 2013
    Messages:
    703
    dunno, just got the general impression of sarcasm from his comment "really?". It didn't feel like a question or sense of surprise at all. If it was, I'm sorry for the misunderstanding.
     
  9. cbrm

    cbrm Just another mod Staff Member Global Moderator

    Joined:
    Jan 6, 2009
    Messages:
    6,496
    I just didn't find any basis for that, I also prefer using table.insert over the other one.
    Anyways, not a big deal for the script performance, imo. Sorry if it sounded harsh.
     
  10. whitevo

    whitevo Feeling good, thats what I do.

    Joined:
    Jan 2, 2015
    Messages:
    3,134
    Your parse function can give same final result as getAreaPos(), but it not the same thing.
    getAreaPos() in other words is sequence mapping with few features not shown in the example code.
    Yours formats the areaT to position map.

    Why act like you know my functions and say something like this?
    I charge per hour, not per script/function. I guess it only took you max 1 hour to do the above code, which in my world would be 5€
    Let customers/donators/collaborators decide am I overpriced or not.

    Other than that, your code looks good to me.

    suggestions:
    make area rotateable

    questions:

    What do you mean by that?
    If I'm doing something wrong, care to explain and improve instead of stacking events yourself xD
     
  11. Colandus

    Colandus Well-Known Member

    Joined:
    Jun 6, 2007
    Messages:
    2,291
    You should only call 1 event and inside the event callback call the event again. No it took me max 30 min. Rotateable is easy. I will add it later. No I still won't need 4h to make what you did. But that's not important. Regardless I wouldn't charge someone for 4h for a function. Doesn't make much sense.

    Whatever.
     
    Last edited: Dec 28, 2016
  12. MatheusMkalo

    MatheusMkalo ボーカロイド

    Joined:
    Jun 1, 2011
    Messages:
    965
    As you asked for suggestions:

    Here:
    Code:
    for areaVal, posT in pairs(rawValues) do
        values[areaVal] = {}
        for _, pos in ipairs(posT) do
            table.insert(values[areaVal], Position({x = center.x + pos.x, y = center.y + pos.y, z = center.z}))
        end
    end
    
    Instead of storing the raw position in a table indexed by the value, you store a table that has the raw and the value so you will need only 1 for to parse the raw values.

    Here:
    Code:
    if not values[v] then values[v] = {} end
    More readable:
    Code:
    values[v] = values[v] or {}
    And for me it's silly to have a function that loops through a table, to generate another table that will also be looped to do something, instead you could make a function that you pass the map and a table looking something like:
    Code:
    {
        [1] = function(pos) dosomethingwithpos(pos) end,
        [2] = function(pos) doshitwithpos(pos) end,
    }
    Then for every 1 and 2 values in the table those functions are going to be called passing the position as parameter.

    It would make it easier to use imo.

    @cbrm https://springrts.com/wiki/Lua_Perf...ng_Table_Items_.28table.insert_vs._.5B_.5D.29
     
    tokenzz likes this.
  13. Colandus

    Colandus Well-Known Member

    Joined:
    Jun 6, 2007
    Messages:
    2,291
    1. I would argue that an if-statement is faster (which I have no proof of though), but then I'd be contradictory (considering my statement on a previous post), so I will agree with you on that.

    2. Yes, certainly right. The reason I did so is because I initially made the raw values code before I did the other one, thus I did not consider it.

    3. Yes, that would be convenient, however it wouldn't always be preferred. Should definitely be an option though. The reason is because I initially made this function to use them as static positions (basically a tile with some pillars around it) instead of having to write the positions manually. My main concern is also that people will find it too complex to use a callback. OT scripters aren't used to that type of coding. Also it's not always you want entirely different functions for the values, as in the second example. Perhaps an option to return an iterator instead?

    Thanks for your feedback, will update once I get home.

    Update: Fixed 1 and 2. I am not sure about 3. Need to figure what options to make. Input, @MatheusMkalo?
     
    Last edited: Dec 28, 2016
  14. whitevo

    whitevo Feeling good, thats what I do.

    Joined:
    Jan 2, 2015
    Messages:
    3,134
    Once again you assume you know everything what does getAreaPos() do. Yeah your function can be used for one of the things what getAreaPos() does, it's still not the same thing.
    Just leave it be, don't compare cog with clock.

    About events. I still don't understand.
    Why would I make event what loops trough positions and puts events in sequence instead of what I did before (loop trough positions and sequence the events)

    Or you mean that if 2 or more different positions should execute at the same time, I should group them into 1 addEvent?
    In that case I just prefer handling every position 1 by 1, code looks better and less complex + I haven't found a problem in performance by making addEvent for each position.
     
  15. ahmed30

    ahmed30 Active Member

    Joined:
    Jun 7, 2011
    Messages:
    221
    Awesome release @Colandus as always, but the code is kinda sophisticated, it could be much more cleaner by finding the centerPos before generating the positions. For example:
    Code:
    do
       local function findCenterPos(t, centerValue)
            for iy, yt in ipairs(t) do
                for ix, v in ipairs(yt) do
                    if v == centerValue then
                        return iy, ix
                    end
                end
            end
            return nil
        end
    
        function parseAreaMap(t, centerValue)
            local positions = {}
            local centerX, centerY = findCenterPos(t, centerValue)
            for iy, yt in ipairs(t) do
                for ix, v in ipairs(yt) do
                    if v ~= 0 and v ~= centerValue then
                        local pos = Position(centerX-ix, centerY-iy, 0)
                        if not positions[v] then
                            positions[v] = {}
                        end
                        table.insert(positions[v], pos)
                    end
                end
            end
            return positions
        end
    end
    
    Personally, i don't see how ternary operator would be more readable than an if statement in this case.

    @Colandus provided a useful function that reduces alot of code complexity and fits the majority of people. Unfortunately, creating a code that fit everyone's needs and preference too is impossible. So, you can always modify the function or override it to fit your needs and/or preference. For example:
    Code:
    do
        local oldParseAreaMap = parseAreaMap
        function parseAreaMap(t, centerValue, posFuncs)
            local areaPositions = oldParseAreaMap(t, centerValue)
            for i, positions in ipairs(areaPositions) do
                for _, pos in ipairs(positions) do
                    local func = posFuncs[i]
                    if func then
                        func(pos)
                    end
                end
            end
        end
    end
    
    And, if you want to handle raised errors by yourself i suggest encapsulating the code and use pcall instead of direct function call.
     
    Last edited: Jan 10, 2017
    Colandus likes this.
  16. Colandus

    Colandus Well-Known Member

    Joined:
    Jun 6, 2007
    Messages:
    2,291
    Thanks. I just didn't want to loop twice just to find the center. I'm not sure how important it is that the function is cleaner to that level. Handling the optional parameters could be prettier though.
     
    Last edited: Jan 10, 2017

Share This Page