• Check out the results of the Techtree Contest #19!
  • Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.
  • Create a void inspired texture for Warcraft 3 and enter Hive's 34th Texturing Contest: Void! Click here to enter!
  • The Hive's 22nd Icon Contest: Creep Abilities is now concluded, time to vote for your favourite set of icons! Click here to vote!

Lua .mmp API

This Lua library provides a small, self-contained API for reading, writing, and manipulating Warcraft III .mmp files. An MMP file defines the set of minimap preview overlay icons such as start locations, gold mines, and creep camps (see pic below), including their position, color, and transparency on a 256×256 minimap canvas.

1767723721439.png



The API is EmmyLua-friendly, IDE-friendly, and politely pretends to be OOP-friendly, complete with icon objects, methods, and a faint illusion of encapsulation. If you don’t enjoy that sort of thing, feel free to ignore all of that and just operate on MMPData.icons directly, like a normal Lua person.

Practical notes​

Icons appear to be clipped 8 pixels from the minimap border, so the predictable drawing area is closer to [8, 247] than [0, 255].
There is no obvious hard limit on icon count beyond int32, although dumping thousands of icons may briefly turn the minimap completely black while the game catches up.

Lua:
MMPIconType (Enum)
    Gold = 0
    NeutralBuilding = 1
    StartLoc = 2
    CreepCampSmall = 3
    CreepCampLarge = 4

MMPData (Class)
    MMPData:new() -> MMPData
    MMPData:newFromFile(filePath: string) -> MMPData

    MMPData:write(filePath: string)

    MMPData:countIcons() -> integer
    MMPData:addIcon(icon: MMPIcon, pos?: integer) -> integer|nil
    MMPData:removeIconByNumber(n: integer) -> MMPIcon|nil
    MMPData:removeIconInstance(icon: MMPIcon) -> MMPIcon
    MMPData:enumerateIcons(fn: fun(icon: MMPIcon, index: integer))
    MMPData:newIcon(iconType?: MMPIconType|integer, x?: integer, y?: integer, b?: integer, g?: integer, r?: integer, a?: integer) -> MMPIcon

    MMPData:print()

MMPIcon (Class)
    MMPIcon:setColorBGRA(b: integer, g: integer, r: integer, a: integer)
    MMPIcon:getColorBGRA() -> b: integer, g: integer, r: integer, a: integer
    MMPIcon:adjustColorBGRA(db?: integer, dg?: integer, dr?: integer, da?: integer)
    MMPIcon:setAlpha(a: integer)
    MMPIcon:getAlpha() -> a: integer

    MMPIcon:setPosition(x: integer, y: integer)
    MMPIcon:getPosition() -> x: integer, y: integer
    MMPIcon:translate(dx?: integer, dy?: integer)

    MMPIcon:setType(iconType: MMPIconType|integer)
    MMPIcon:getType() -> MMPIconType|integer
    MMPIcon:getFileName() -> fileName: string

    MMPIcon:print(iconId?: integer)
Lua:
-- Import
local MMPLib = require("mmp_lib")
local MMPData, IconType = MMPLib.MMPData, MMPLib.MMPIconType

-- Read file
local testFile = "path\\to\\war3map.mmp"
local mmpData = MMPData:newFromFile(testFile)

-- Debug file
mmpData:print()

-- Make all icons red
mmpData:enumerateIcons(function(icon)
    icon:setColorBGRA(0, 0, 255, 255)
end)

-- Write file
mmpData:write(testFile)

-- Create new mmp
mmpData = MMPData:new()

-- Add new icon
local icon = mmpData:newIcon(IconType.CreepCampLarge)
icon:setPosition(125, 125)
icon:setColorBGRA(0, 255, 255, 255)
mmpData:addIcon(icon)

-- Edit icon
icon:setType(IconType.StartLoc)
icon:adjustColorBGRA(25)
icon:translate(-10, 20)

-- Debug icon
icon:print()
Lua:
--- Utils ---
---@param val number
---@param strict boolean? when true, error on values outside [0, 255]
---@return integer -- value clamped to [0, 255] unless strict
local function clampByte(val, strict)
    local v = math.tointeger(val)
    if not v then error(tostring(val) .. " is not an integer value") end
    if strict then
        if v < 0 or v > 255 then
            error(tostring(v) .. " is out of byte range")
        end
        return v
    end
    return (v < 0 and 0) or (v > 255 and 255) or v
end
---@param b integer -- [0, 255]
---@param g integer -- [0, 255]
---@param r integer -- [0, 255]
---@param a integer -- [0, 255]
local function colorBGRA(b, g, r, a)
    return {
        Red = clampByte(r),
        Green = clampByte(g),
        Blue = clampByte(b),
        Alpha = clampByte(a),
    }
end
---@param x integer -- [0, 255]
---@param y integer -- [0, 255]
local function position(x, y)
    return {
        X = clampByte(x),
        Y = clampByte(y)
    }
end
local iconFileDict = {
    "MinimapIconNeutralBuilding",
    "MinimapIconStartLoc",
    "MinimapIconCreepCampSmall",
    "MinimapIconCreepCampLarge",
    [0] = "MinimapIconGold"
}
---@param iconType MMPIconType|integer
---@return string fileName
local function getIconFile(iconType)
    local iconFile = iconFileDict[iconType]
    if not iconFile then error(tostring(iconType) .. " is not a valid icon type") end
    return iconFile
end
---@enum MMPIconType
local IconType = {
    --- UI\MiniMap\MinimapIcon\MinimapIconGold.blp
    Gold = 0,
    --- UI\MiniMap\MinimapIcon\MinimapIconNeutralBuilding.blp
    NeutralBuilding = 1,
    --- UI\MiniMap\MinimapIcon\MinimapIconStartLoc.blp
    StartLoc = 2,
    --- UI\Minimap\MinimapIconCreepLoc.blp
    CreepCampSmall = 3,
    --- UI\Minimap\MinimapIconCreepLoc2.blp
    CreepCampLarge = 4,
}
--- Icon Data ---
---@class MMPIcon
---@field private type integer
---@field private pos table
---@field private color table
---@field private __index table
local BinaryIcon = {}
BinaryIcon.__index = BinaryIcon
---@param iconType MMPIconType|integer type of icon [0, 4]
---@param pos table {X = x, Y = y}
---@param color table {Red = r, Green = g, Blue = b, Alpha = a}
---@return MMPIcon
---@private
function BinaryIcon:new(iconType, pos, color)
    local icon = setmetatable({}, BinaryIcon)
    icon:setType(iconType or 0)
    icon.pos = pos
    icon.color = color
    return icon
end
---@return string data icon packed to bytes
---@package
function BinaryIcon:pack()
    local c = self.color
    return string.pack("<i4i4i4BBBB", self.type, self.pos.X, self.pos.Y, c.Blue, c.Green, c.Red, c.Alpha)
end
--- Icon API ---
---@param b integer -- [0, 255]
---@param g integer -- [0, 255]
---@param r integer -- [0, 255]
---@param a integer -- [0, 255]
---@public
function BinaryIcon:setColorBGRA(b, g, r, a)
    local c = self.color
    c.Red = clampByte(r)
    c.Green = clampByte(g)
    c.Blue = clampByte(b)
    c.Alpha = clampByte(a)
end
---@return integer b -- [0, 255]
---@return integer g -- [0, 255]
---@return integer r -- [0, 255]
---@return integer a -- [0, 255]
---@public
function BinaryIcon:getColorBGRA()
    local c = self.color
    return c.Blue, c.Green, c.Red, c.Alpha
end
---@param db integer? delta [-255, 255]
---@param dg integer? delta [-255, 255]
---@param dr integer? delta [-255, 255]
---@param da integer? delta [-255, 255]
---@public
function BinaryIcon:adjustColorBGRA(db, dg, dr, da)
    local r, g, b, a = self:getColorBGRA()
    self:setColorBGRA(b + (db or 0), g + (dg or 0), r + (dr or 0), a + (da or 0))
end
---@param a integer -- [0, 255]
---@public
function BinaryIcon:setAlpha(a)
    self.color.Alpha = clampByte(a)
end
---@return integer a -- [0, 255]
---@public
function BinaryIcon:getAlpha()
    return self.color.Alpha
end
--- Sets the icon center position on the 256 x 256 minimap.
--- The icon size is 16x16 pixels.
---
--- Recommended coordinate ranges:
--- - Safe (no clipping): [8, 247]
--- - Visually optimal for grids (edge-aligned): [8, 248]
---
--- Values outside these ranges are allowed, but may cause partial clipping
--- and visual asymmetry near the image borders.
---@param x integer -- [0, 255]
---@param y integer -- [0, 255]
---@public
function BinaryIcon:setPosition(x, y)
    self.pos.X = clampByte(x)
    self.pos.Y = clampByte(y)
end
---@return integer x -- [0, 255]
---@return integer y -- [0, 255]
---@public
function BinaryIcon:getPosition()
    return self.pos.X, self.pos.Y
end
--- See `MMPIcon:setPosition()` docstring for grid coordinate limits
---@param dx integer? delta [-255, 255]
---@param dy integer? delta [-255, 255]
---@public
function BinaryIcon:translate(dx, dy)
    local x, y = self:getPosition()
    self:setPosition(x + (dx or 0), y + (dy or 0))
end
---@param iconType MMPIconType|integer -- [0, 4]
---@public
function BinaryIcon:setType(iconType)
    getIconFile(iconType)
    self.type = iconType
end
---@return integer|MMPIconType type -- [0, 4]
---@public
function BinaryIcon:getType()
    return self.type
end
---@return string fileName
---@public
function BinaryIcon:getFileName()
    return getIconFile(self.type)
end
---@param iconId integer?
---@public
function BinaryIcon:print(iconId)
    print("Icon " .. (iconId or "") .. ": Type: " .. self.type .. " (\"" .. getIconFile(self.type) .. "\")")
    local p = self.pos
    print("\tPosition:", p.X, p.Y)
    local c = self.color
    print("\tColorBGRA:", c.Blue, c.Green, c.Red, c.Alpha)
end
--- File Data ---
---@class MMPData
---@field private icons MMPIcon[]
---@field private fileData string
---@field private size integer
---@field private offset integer
---@field private DEFAULT_VERSION number
---@field private ICON_SIZE_IN_BYTES integer
---@field private __index table
local BinaryData = {}
BinaryData.__index = BinaryData
BinaryData.DEFAULT_VERSION = 0
BinaryData.ICON_SIZE_IN_BYTES = 4 + 4 + 4 + 4
---@return MMPData MMP data instance
---@public
function BinaryData:new()
    local binaryData = setmetatable({}, BinaryData)
    binaryData.version = self.DEFAULT_VERSION
    binaryData.icons = {}
    return binaryData
end
---@param filePath string Path to .mmp file
---@return MMPData MMP data instance
---@public
function BinaryData:newFromFile(filePath)
    local binaryData = self:new()
    local input = assert(io.open(filePath, "rb"))
    local fileData = input:read("*all")
    input:close()
    binaryData.fileData = fileData
    binaryData.size = #fileData
    binaryData.offset = 1
    binaryData:parse()
    binaryData.fileData = nil
    binaryData.size = nil
    binaryData.offset = nil
    return binaryData
end
---@private
function BinaryData:parse()
    local version = self:getNumber("<i4", 4)
    if version ~= self.DEFAULT_VERSION then error(tostring(version) .. " is an unknown MMP version") end
    local iconNumber = self:getNumber("<i4", 4)
    if iconNumber < 0 then
        error("Invalid icon number, got " .. tostring(iconNumber))
    end
    if self.offset + iconNumber * self.ICON_SIZE_IN_BYTES ~= self.size + 1 then
        error("Unexpected file size, expected " .. self.offset + iconNumber * self.ICON_SIZE_IN_BYTES .. ", got " .. self.size)
    end
    local icons = {}
    for _ = 1, iconNumber do
        table.insert(icons, self:readIcon())
    end
    self.version = version
    self.icons = icons
end
---@private
function BinaryData:readIcon()
    return BinaryIcon:new(
            self:getNumber("<i4", 4),
            position(
                    clampByte(self:getNumber("<i4", 4), true),
                    clampByte(self:getNumber("<i4", 4), true)
            ),
            colorBGRA(
                    self:getNumber("B", 1),
                    self:getNumber("B", 1),
                    self:getNumber("B", 1),
                    self:getNumber("B", 1)
            )
    )
end
---@private
function BinaryData:getNumber(fmt, size)
    if not (self.offset + size - 1 <= self.size) then
        error("Error while reading number! Expected " .. tostring(size) .. "bytes, got " .. tostring(self.size - self.offset + 1))
    end
    local number = string.unpack(fmt, self.fileData, self.offset)
    self.offset = self.offset + size
    return number
end
---@public
function BinaryData:write(filePath)
    local iconNumber = self:countIcons()
    local data = string.pack("<i4i4", self.version, iconNumber)
    local icons = {}
    self:enumerateIcons(function(icon)
        table.insert(icons, icon:pack())
    end)
    data = data .. table.concat(icons)
    local output = assert(io.open(filePath, "wb"))
    output:write(data)
    output:close()
end
--- MMP API ---
---@return integer
---@public
function BinaryData:countIcons()
    return #self.icons
end
---If icon arg is a valid icon instance, works same as table.insert(), else returns nil
---@param icon MMPIcon icon instance
---@param pos integer? insert position
---@return integer|nil
---@public
function BinaryData:addIcon(icon, pos)
    if type(icon) == "table" and icon.pack ~= nil then
        if pos == nil then
            return table.insert(self.icons, icon)
        else
            return table.insert(self.icons, pos, icon)
        end
    end
    return nil
end
---Works same as table.remove()
---@param n integer
---@return MMPIcon|nil icon instance
---@public
function BinaryData:removeIconByNumber(n)
    if n > 0 and n <= #self.icons then
        return table.remove(self.icons, n)
    end
    return nil
end
---@param icon MMPIcon icon instance
---@return MMPIcon
---@public
function BinaryData:removeIconInstance(icon)
    for k, v in ipairs(self.icons) do
        if v == icon then
            return self:removeIconByNumber(k)
        end
    end
    return icon
end
---@param fn fun(icon: MMPIcon, index: integer): nil
function BinaryData:enumerateIcons(fn)
    for i, icon in ipairs(self.icons) do
        fn(icon, i)
    end
end
--- Icon factory
---
--- See `MMPIcon:setPosition()` docstring for grid coordinate limits
---@param iconType MMPIconType|integer? type of icon [0, 4]
---@param x integer? [0, 255]
---@param y integer? [0, 255]
---@param b integer? [0, 255]
---@param g integer? [0, 255]
---@param r integer? [0, 255]
---@param a integer? [0, 255]
---@return MMPIcon
---@public
function BinaryData:newIcon(iconType, x, y, b, g, r, a)
    return BinaryIcon:new(
            iconType or 0,
            position(x or 0, y or 0),
            colorBGRA(b or 255, g or 255, r or 255, a or 255)
    )
end
---@public
function BinaryData:print()
    print("MMP FILE: VERSION " .. self.version, "ICON NUMBER: " .. #self.icons)
    self:enumerateIcons(function(iconInstance, id)
        iconInstance:print(id)
    end)
end
return {
    MMPData = BinaryData,
    MMPIconType = IconType
}

Examples of use (procedurally generated mmp files)​

random.pngcrossbycrosses.pnggoldminegradient.pngLissajous.pngradialrays_depth.pngrainbowspiralalpha_v2.pngvortex.pngwaves.png

 

Attachments

Back
Top