- Joined
- Jun 30, 2021
- Messages
- 56
This Lua library provides a small, self-contained API for reading, writing, and manipulating Warcraft III
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
There is no obvious hard limit on icon count beyond
.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.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
}








