LogUtils

This bundle is marked as pending. It has not been reviewed by a staff member yet.
LogUtils v1.0.4 by Tomotz

This library provides a log file for the game.

Information

LogUtils

PrettyString

HandleType


Features beyond what FileIO offers:
- Log is saved to a memory buffer, allowing you to write to the file over and over again without losing the old data.
- Writing lines to the log only when the game is in replay to reduce performance impact
- Maximum log length - when reached, will open a new log file (and memory buffer) to avoid long writes
- Adds the player id to the log name to allow multiple logs from a single game on the same computer (for map testing)
- print like interface for arguments
- Supports pretty printing tables and wc3 handles

Interface:
LogWrite(...)
Appends data to the current log file (also flushes everything in the memory buffer to the file). Acts like print (converts the arguments to strings and concatenates them)
LogWriteNoFlush(...)
Appends data to the memory buffer without writing it to the file.
This should only be used if you know you will eventually use a LogWrite to flush this line
LogWriteNoFlushReplay(...)
LogWriteReplay(...)
These functions are the same as the ones above, except they will only write the line when a replay of the game is played.
Note that the normal variants of these functions will write in both replay and normal modes.
These functions should be used when you worry about game performance impact. If you write many/very long lines to
the log, it might hurt performance

Installation instructions:
Copy the code to your map. For Pretty prints of tables and handles, also include PrettyString and HandleType.

Credits:
TriggerHappy GameStatus (Replay Detection) [vJASS] - GameStatus (Replay Detection)
* A big block of code was taken from there to allow detecting that the game is running in replay mode.

Requires:
FileIO (lua) by Trokkin - [Lua] - FileIO (Lua-optimized)
TotalInitialization by Bribe - [Lua] - Total Initialization

Updated:
Nov 2025

Change Log:
1.0.1: Fixed bug with log names for multiplayer.
1.0.2: Support print like interface (support multiple arguments, convert arguments to strings and concatenate them)
1.0.4: Add PrettyPrint for table and wc3 handles
Required helper functions that you can put with the system code or anywhere else. Those are needed if you want to have a timestamp on each log line, and to tell if the game is online/offline/replay
Lua:
-- Useful globals that tells you if the game is offline (single player I think), online or replay.
-- The first 3 are constants and should not be changed
GAME_STATUS_OFFLINE = 0
GAME_STATUS_ONLINE = 1
GAME_STATUS_REPLAY = 2
GameStatus = 0 -- one of GAME_STATUS_OFFLINE, GAME_STATUS_ONLINE, GAME_STATUS_REPLAY
-- sets GameStatus variable which decides if game is online, offline or replay
local function SetGameStatus()
    local firstPlayer ---@type player
    local u ---@type unit
    local selected ---@type boolean
    -- find an actual player
    firstPlayer = Player(0)
    while not ((GetPlayerController(firstPlayer) == MAP_CONTROL_USER and GetPlayerSlotState(firstPlayer) == PLAYER_SLOT_STATE_PLAYING)) do
        firstPlayer = Player(GetPlayerId(firstPlayer) + 1)
    end
    -- force the player to select a dummy unit
    u = CreateUnit(firstPlayer, FourCC('hfoo'), 0, 0, 0)
    SelectUnit(u, true)
    selected = IsUnitSelected(u, firstPlayer)
    RemoveUnit(u)
    if (selected) then
        -- detect if replay or offline game
        if (ReloadGameCachesFromDisk()) then
            GameStatus = GAME_STATUS_OFFLINE
        else
            GameStatus = GAME_STATUS_REPLAY
        end
    else
        -- if the unit wasn't selected instantly, the game is online
        GameStatus = GAME_STATUS_ONLINE
    end
end
OnInit.global(SetGameStatus)

local gametime_initialized = false
local gameStartTimer ---@type timer
---@return real
function GetElapsedGameTime()
    if not gametime_initialized then return 0 end
    return TimerGetElapsed(gameStartTimer)
end
OnInit.global(function()
    gametime_initialized = true
    gameStartTimer = CreateTimer()
    TimerStart(gameStartTimer, 0xF4240, false, nil)
end)

Lua:
if Debug and Debug.beginFile then Debug.beginFile("LogUtils") end
--[[
LogUtils v1.0.2 by Tomotz
This library provides a log file for the game.
Features beyond what FileIO offers:
 - Log is saved to a memory buffer, allowing you to write to the file over and over again without losing the old data.
 - Writing lines to the log only when the game is in replay to reduce performance impact
 - Maximum log length - when reached, will open a new log file (and memory buffer) to avoid long writes
 - Adds the player id to the log name to allow multiple logs from a single game on the same computer (for map testing)
 - `print` like interface for arguments
Interface:
    LogWrite(...)
        Appends data to the current log file (also flushes everything in the memory buffer to the file). Acts like print
    LogWriteNoFlush(...)
        Appends data to the memory buffer without writing it to the file.
        This should only be used if you know you will eventually use a LogWrite to flush this line
    LogWriteNoFlushReplay(...)
    LogWriteReplay(...)
        These functions are the same as the ones above, except they will only write the line when a replay of the game is played.
        Note that the normal variants of these functions will write in both replay and normal modes.
        These functions should be used when you worry about game performace impact. If you write many/very long lines to
        the log, it might hurt performance
Installation instructions:
Copy the code to your map.
Credits:
TriggerHappy GameStatus (Replay Detection) https://www.hiveworkshop.com/threads/gamestatus-replay-detection.293176/
 * A big block of code was taken from there to allow detecting that the game is running in replay mode.
Requires:
FileIO (lua) by Trokkin - https://www.hiveworkshop.com/threads/fileio-lua-optimized.347049/
TotalInitialization by Bribe - https://www.hiveworkshop.com/threads/total-initialization.317099/
Updated: 28 Oct 2025
--]]
do
    LIBRARY_LogUtils   = true
    local SCOPE_PREFIX = "LogUtils_" ---@type string requires TotalInitialization, FileIO
    -- Configurations:

    -- Base name for the log files.
    -- The logs are saved to Documents\Warcraft III\CustomMapData
    -- the relative path where the log files are kept. Should end with \\
    local RAW_LOG_PATH    = "Savegames\\TestMap\\"
    -- Raw name for the log file. Will append the player index to the start on any mode, and "replay" to the end on replay mode.
    -- Will also add the log index (in case there are multiple log files in the same run) and .txt to the end
    local RAW_LOG_NAME    = "last_game_log"
    -- The maximum length of the memory buffer. If too long, writes to the log file can take a long time
    -- (as the whole buffer is flushed on every write) and hinder performances
    local MAX_BUFF_LEN    = 50000 ---@type integer -- Theoretically, FileIO supports 999999
    -- Maximum number of log files that will be created in a single game (will stop logging if threshold was reached)
    local MAX_FILES       = 10 ---@type integer
    -- Same as MAX_FILES, but for replay logs
    local MAX_FILESREPLAY = 50 ---@type integer
    -- If true, will try writing the map name to the long on init (using TRIGSTR_001)
    local WRITE_MAP_NAME  = true ---@type boolean
    -- Useful globals that tells you if the game is offline (single player I think), online or replay.
    -- The first 3 are constants and should not be changed
    GAME_STATUS_OFFLINE   = 0 ---@type integer
    GAME_STATUS_ONLINE    = 1 ---@type integer
    GAME_STATUS_REPLAY    = 2 ---@type integer
    GameStatus            = 0 ---@type integer -- one of GAME_STATUS_OFFLINE, GAME_STATUS_ONLINE, GAME_STATUS_REPLAY
    -- Local variables for internal use
    local LogFileName ---@type string
    local WriteBuffer     = "\n" ---@type string
    local FileIdx         = 0 ---@type integer
    local IsFlashed       = true ---@type boolean
    ---@param text string
    local function WriteAndFlush(text)
        if (FileIdx < MAX_FILES) or ((GameStatus == GAME_STATUS_REPLAY) and (FileIdx < MAX_FILESREPLAY)) then
            FileIO.Save(LogFileName, text)
        end
    end
    local function CreateNewLogFile()
        LogFileName = RAW_LOG_PATH .. GetPlayerId(GetLocalPlayer()) .. "_" .. RAW_LOG_NAME .. "_"
        if GameStatus == GAME_STATUS_REPLAY then
            LogFileName = LogFileName .. "replay_"
        end
        LogFileName = LogFileName .. tostring(FileIdx) .. ".txt"
        FileIdx = FileIdx + 1
        WriteAndFlush("\nLog file is empty")
    end
    -- sets GameStatus variable which decides if game is online, offline or replay
    local function SetGameStatus()
        local firstPlayer ---@type player
        local u ---@type unit
        local selected ---@type boolean
        -- find an actual player
        firstPlayer = Player(0)
        while not ((GetPlayerController(firstPlayer) == MAP_CONTROL_USER and GetPlayerSlotState(firstPlayer) == PLAYER_SLOT_STATE_PLAYING)) do
            firstPlayer = Player(GetPlayerId(firstPlayer) + 1)
        end
        -- force the player to select a dummy unit
        u = CreateUnit(firstPlayer, FourCC('hfoo'), 0, 0, 0)
        SelectUnit(u, true)
        selected = IsUnitSelected(u, firstPlayer)
        RemoveUnit(u)
        if (selected) then
            -- detect if replay or offline game
            if (ReloadGameCachesFromDisk()) then
                GameStatus = GAME_STATUS_OFFLINE
            else
                GameStatus = GAME_STATUS_REPLAY
            end
        else
            -- if the unit wasn't selected instantly, the game is online
            GameStatus = GAME_STATUS_ONLINE
        end
    end
    local function init()
        CreateNewLogFile()
        -- write map name to log
        if WRITE_MAP_NAME then
            LogWrite(GetLocalizedString("TRIGSTR_001"))
        end
    end

    -- Writes a line to the WriteBuffer to be logged
    function LogWriteNoFlush(...)
        local args = {...}
        if #args > 1 then
            for i = 1, #args - 1 do
                args[i] = tostring(args[i]) .. " " -- Convert each argument to a string
            end
        end
        args[#args] = tostring(args[#args])
        local full_line = table.concat(args)  .. "\n" ---@type string
        if StringLength(WriteBuffer) + StringLength(full_line) > MAX_BUFF_LEN then
            if not IsFlashed then
                WriteAndFlush(WriteBuffer)
            end
            CreateNewLogFile()
            WriteBuffer = full_line
        else
            WriteBuffer = WriteBuffer .. full_line
        end
        IsFlashed = false
    end
    -- Writes a line to the WriteBuffer to be logged only in replay mode
    function LogWriteNoFlushReplay(...)
        if GameStatus == GAME_STATUS_REPLAY then
            LogWriteNoFlush(...)
        end
    end
    -- Writes a line to the current log
    function LogWrite(...)
        LogWriteNoFlush(tostring(timeCounter) .. ":", ...)
        WriteAndFlush(WriteBuffer)
        IsFlashed = true
    end
    -- Writes a line to the log only in replay mode
    function LogWriteReplay(...)
        if GameStatus == GAME_STATUS_REPLAY then
            LogWrite(...)
        end
    end
    OnInit(init)
end
if Debug and Debug.endFile then Debug.endFile() end

PrettyString v1.0.0 by Tomotz

Provides a PrettyString function that can convert tables and wc3 handles to a nice string representation.

Optionaly requires:
HandleType by Antares - Get Handle Type - to parse the handles (Use my version to have GetObjectTypeId function).
Hook by Bribe - [Lua] - Hook - if you want to hook print and IngameConsole to use PrettyString automatically.

Lua:
if Debug then Debug.beginFile("PrettyString") end
do
--[[
PrettyString v1.0.0 by Tomotz
    Provides a PrettyString function that can convert tables and wc3 handles to a nice string representation.
    Useful for logging and debugging.
Optionaly requires:
    HandleType by Antares - https://www.hiveworkshop.com/threads/get-handle-type.354436/ - to parse the handles (Use my version to have GetObjectTypeId function).
    Hook by Bribe - https://www.hiveworkshop.com/threads/hook.339153/ - if you want to hook print and IngameConsole to use PrettyString automatically.
--]]
---@param num integer
---@return string
function FourCC2Str(num)
    return string.pack('>I4', num)
end
---@param tbl table
---@return string -- note that `pairs` is used, so the result is not synced between players
function TableToStr(tbl)
    local out = {}
    for k, v in pairs(tbl) do
        table.insert(out, PrettyString(k) .. ": " .. PrettyString(v))
    end
    local full = table.concat(out, ", ")
    return "{" .. full .. "}"
end
local initDone = false
OnInit.final(function() initDone = true end)
---@param arg any
---@return string -- returns a pretty string representation of the argument -- note that `pairs` is used, so the result is not synced between players
function PrettyString(arg)
    if type(arg) == "table" then
        return TableToStr(arg)
    elseif type(arg) == "userdata" then
        local type = HandleType and HandleType[arg] or ""
        if type == "" then
            return tostring(arg)
        end
        local id = GetObjectTypeId(arg)
        local name = initDone and GetObjectName(id) or "" -- using GetObjectName during init can cause crashes
        return type .. ": " .. name .. "('" .. FourCC2Str(id) .. "')"
    end
    return tostring(arg)
end
OnInit.final(function()
    if Hook then
        Hook.add("print", function(hook, ...)
            local args = {...}
            for i = 1, #args do
                args[i] = PrettyString(args[i])
            end
            hook.next(table.unpack(args))
        end)
        -- Ingame console runs over the print function, so we need to hook it's functions
        if IngameConsole and IngameConsole.originalPrint ~= nil then
            IngameConsole.originalPrint = print
            Hook.add("out", function(hook, a, b, c, ...)
                local args = {...}
                for i = 1, #args do
                    args[i] = PrettyString(args[i])
                end
                hook.next(a, b, c, table.unpack(args))
            end, 0, IngameConsole)
        end
    end
end)
end
if Debug then Debug.endFile() end


Handle Type by Antares
Determine the type of a Wacraft 3 object (handle). The result is stored in a table on the first execution to increase performance.
Lua:
if Debug then Debug.beginFile("HandleType") end
do
    --[[
    https://www.hiveworkshop.com/threads/get-handle-type.354436/
    ===============================================================================================================================================================
                                                                        Handle Type
                                                                        by Antares
    ===============================================================================================================================================================
    Determine the type of a Wacraft 3 object (handle). The result is stored in a table on the first execution to increase performance.
    HandleType[whichHandle]     -> string           Returns an empty string if variable is not a handle.
    IsHandle[whichHandle]       -> boolean
    IsWidget[whichHandle]       -> boolean
    IsUnit[whichHandle]         -> boolean
    These can also be called as a function, which has a nil-check, but is slower than the table-lookup
    ===============================================================================================================================================================
    ]]
    local widgetTypes = {
        unit = true,
        destructable = true,
        item = true
    }
    HandleType = setmetatable({}, {
        __mode = "k",
        __index = function(self, key)
            if type(key) == "userdata" then
                local str = tostring(key)
                self[key] = str:sub(1, (str:find(":", nil, true) or 0) - 1)
                return self[key]
            else
                self[key] = ""
                return ""
            end
        end,
        __call = function(self, key)
            if key then
                return self[key]
            else
                return ""
            end
        end
    })
    IsHandle = setmetatable({}, {
        __mode = "k",
        __index = function(self, key)
            self[key] = HandleType[key] ~= ""
            return self[key]
        end,
        __call = function(self, key)
            if key then
                return self[key]
            else
                return false
            end
        end
    })
    IsWidget = setmetatable({}, {
        __mode = "k",
        __index = function(self, key)
            self[key] = widgetTypes[HandleType[key]] == true
            return self[key]
        end,
        __call = function(self, key)
            if key then
                return self[key]
            else
                return false
            end
        end
    })
    IsUnit = setmetatable({}, {
        __mode = "k",
        __index = function(self, key)
            self[key] = HandleType[key] == "unit"
            return self[key]
        end,
        __call = function(self, key)
            if key then
                return self[key]
            else
                return false
            end
        end
    })
    local typeIdFuncs = {unit=GetUnitTypeId, item=GetItemTypeId, destructable=GetDestructableTypeId, ability=BlzGetAbilityId}
    ---@param handle userdata
    ---@return integer
    function GetObjectTypeId(handle)
        local handleType = HandleType[handle]
        local func = typeIdFuncs[handleType]
        if func then
            return func(handle)
        else
            return 0
        end
    end
end
if Debug then Debug.endFile() end
Previews
Contents

testMap_006 (Map)

Back
Top