Name | Type | is_array | initial_value |
HASH_TEST | hashtable | No | |
mlnl_LoggingPlayer | player | Yes | |
mlnl_MakeProxyTable | string | Yes | |
mlnl_Modes | string | Yes | |
mlnl_NIL_player | player | Yes | |
mlnl_WillSaveThis | string | Yes | |
SelectedUnit | unit | No | |
UNIT_TEST | unit | No | UnitNull |
VAR_TEST_INT | integer | No | |
VAR_TEST_REAL | real | No | |
VAR_TEST_STR | string | No |
do; local _, codeLoc = pcall(error, "", 2) --get line number where DebugUtils begins.
--[[
-------------------------
-- | Debug Utils 2.2 | --
-------------------------
--> https://www.hiveworkshop.com/threads/lua-debug-utils-incl-ingame-console.353720/
- by Eikonium, with special thanks to:
- @Bribe, for pretty table print, showing that xpcall's message handler executes before the stack unwinds and useful suggestions like name caching and stack trace improvements.
- @Jampion, for useful suggestions like print caching and applying Debug.try to all code entry points
- @Luashine, for useful feedback and building "WC3 Debug Console Paste Helper?" (https://github.com/Luashine/wc3-debug-console-paste-helper#readme)
- @HerlySQR, for showing a way to get a stack trace in Wc3 (https://www.hiveworkshop.com/threads/lua-getstacktrace.340841/)
- @Macadamia, for showing a way to print warnings upon accessing nil globals, where this all started with (https://www.hiveworkshop.com/threads/lua-very-simply-trick-to-help-lua-users-track-syntax-errors.326266/)
-----------------------------------------------------------------------------------------------------------------------------
| Provides debugging utility for Wc3-maps using Lua. |
| |
| Including: |
| 1. Automatic ingame error messages upon running erroneous code from triggers or timers. |
| 2. Ingame Console that allows you to execute code via Wc3 ingame chat. |
| 3. Automatic warnings upon reading nil globals (which also triggers after misspelling globals) |
| 4. Debug-Library functions for manual error handling. |
| 5. Caching of loading screen print messages until game start (which simplifies error handling during loading screen) |
| 6. Overwritten tostring/print-functions to show the actual string-name of an object instead of the memory position. |
| 7. Conversion of war3map.lua-error messages to local file error messages. |
| 8. Other useful debug utility (table.print and Debug.wc3Type) |
-----------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Installation: |
| |
| 1. Copy the code (DebugUtils.lua, StringWidth.lua and IngameConsole.lua) into your map. Use script files (Ctrl+U) in your trigger editor, not text-based triggers! |
| 2. Order the files: DebugUtils above StringWidth above IngameConsole. Make sure they are above ALL other scripts (crucial for local line number feature). |
| 3. Adjust the settings in the settings-section further below to receive the debug environment that fits your needs. |
| |
| Deinstallation: |
| |
| - Debug Utils is meant to provide debugging utility and as such, shall be removed or invalidated from the map closely before release. |
| - Optimally delete the whole Debug library. If that isn't suitable (because you have used library functions at too many places), you can instead replace Debug Utils |
| by the following line of code that will invalidate all Debug functionality (without breaking your code): |
| Debug = setmetatable({try = function(...) return select(2,pcall(...)) end}, {__index = function(t,k) return DoNothing end}); try = Debug.try |
| - If that is also not suitable for you (because your systems rely on the Debug functionality to some degree), at least set ALLOW_INGAME_CODE_EXECUTION to false. |
| - Be sure to test your map thoroughly after removing Debug Utils. |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
* Documentation and API-Functions:
*
* - All automatic functionality provided by Debug Utils can be deactivated using the settings directly below the documentation.
*
* -------------------------
* | Ingame Code Execution |
* -------------------------
* - Debug Utils provides the ability to run code via chat command from within Wc3, if you have conducted step 3 from the installation section.
* - You can either open the ingame console by typing "-console" into the chat, or directly execute code by typing "-exec <code>".
* - See IngameConsole script for further documentation.
*
* ------------------
* | Error Handling |
* ------------------
* - Debug Utils automatically applies error handling (i.e. Debug.try) to code executed by your triggers and timers (error handling means that error messages are printed on screen, if anything doesn't run properly).
* - You can still use the below library functions for manual debugging.
*
* Debug.try(funcToExecute, ...) -> ...
* - Help function for debugging another function (funcToExecute) that prints error messages on screen, if funcToExecute fails to execute.
* - DebugUtils will automatically apply this to all code run by your triggers and timers, so you rarely need to apply this manually (maybe on code run in the Lua root).
* - Calls funcToExecute with the specified parameters (...) in protected mode (which means that following code will continue to run even if funcToExecute fails to execute).
* - If the call is successful, returns the specified function's original return values (so p1 = Debug.try(Player, 0) will work fine).
* - If the call is unsuccessful, prints an error message on screen (including stack trace and parameters you have potentially logged before the error occured)
* - By default, the error message consists of a line-reference to war3map.lua (which you can look into by forcing a syntax error in WE or by exporting it from your map via File -> Export Script).
* You can get more helpful references to local script files instead, see section about "Local script references".
* - Example: Assume you have a code line like "func(param1,param2)", which doesn't work and you want to know why.
* Option 1: Change it to "Debug.try(func, param1, param2)", i.e. separate the function from the parameters.
* Option 2: Change it to "Debug.try(function() return func(param1, param2) end)", i.e. pack it into an anonymous function (optionally skip the return statement).
* Debug.log(...)
* - Logs the specified parameters to the Debug-log. The Debug-log will be printed upon the next error being catched by Debug.try, Debug.assert or Debug.throwError.
* - The Debug-log will only hold one set of parameters per code-location. That means, if you call Debug.log() inside any function, only the params saved within the latest call of that function will be kept.
* Debug.throwError(...)
* - Prints an error message including document, line number, stack trace, previously logged parameters and all specified parameters on screen. Parameters can have any type.
* - In contrast to Lua's native error function, this can be called outside of protected mode and doesn't halt code execution.
* Debug.assert(condition:boolean, errorMsg:string, ...) -> ...
* - Prints the specified error message including document, line number, stack trace and previously logged parameters on screen, IF the specified condition fails (i.e. resolves to false/nil).
* - Returns ..., IF the specified condition holds.
* - This works exactly like Lua's native assert, except that it also works outside of protected mode and does not halt code execution.
* Debug.traceback() -> string
* - Returns the stack trace at the position where this is called. You need to manually print it.
* Debug.getLine([depth: integer]) -> integer?
* - Returns the line in war3map.lua, where this function is executed.
* - You can specify a depth d >= 1 to instead return the line, where the d-th function in the stack trace was called. I.e. depth = 2 will return the line of execution of the function that calls Debug.getLine.
* - Due to Wc3's limited stack trace ability, this might sometimes return nil for depth >= 3, so better apply nil-checks on the result.
* Debug.getLocalErrorMsg(errorMsg:string) -> string
* - Takes an error message containing a file and a linenumber and converts war3map.lua-lines to local document lines as defined by uses of Debug.beginFile() and Debug.endFile().
* - Error Msg must be formatted like "<document>:<linenumber><Rest>".
*
* ----------------------------
* | Warnings for nil-globals |
* ----------------------------
* - DebugUtils will print warnings on screen, if you read any global variable in your code that contains nil.
* - This feature is meant to spot any case where you forgot to initialize a variable with a value or misspelled a variable or function name, such as calling CraeteUnit instead of CreateUnit.
* - By default, warnings are disabled for globals that have been initialized with any value (including nil). I.e. you can disable nil-warnings by explicitly setting MyGlobalVariable = nil. This behaviour can be changed in the settings.
*
* Debug.disableNilWarningsFor(variableName:string)
* - Manually disables nil-warnings for the specified global variable.
* - Variable must be inputted as string, e.g. Debug.disableNilWarningsFor("MyGlobalVariable").
*
* -----------------
* | Print Caching |
* -----------------
* - DebugUtils caches print()-calls occuring during loading screen and delays them to after game start.
* - This also applies to loading screen error messages, so you can wrap erroneous parts of your Lua root in Debug.try-blocks and see the message after game start.
*
* -------------------------
* | Local File Stacktrace |
* -------------------------
* - By default, error messages and stack traces printed by the error handling functionality of Debug Utils contain references to war3map.lua (a big file just appending all your local scripts).
* - The Debug-library provides the two functions below to index your local scripts, activating local file names and line numbers (matching those in your IDE) instead of the war3map.lua ones.
* - This allows you to inspect errors within your IDE (VSCode) instead of the World Editor.
*
* Debug.beginFile(fileName: string [, depth: integer])
* - Tells the Debug library that the specified file begins exactly here (i.e. in the line, where this is called).
* - Using this improves stack traces of error messages. "war3map.lua"-references between <here> and the next Debug.endFile() will be converted to file-specific references.
* - All war3map.lua-lines located between the call of Debug.beginFile(fileName) and the next call of Debug.beginFile OR Debug.endFile are treated to be part of "fileName".
* - !!! To be called in the Lua root in Line 1 of every document you wish to track. Line 1 means exactly line 1, before any comment! This way, the line shown in the trace will exactly match your IDE.
* - Depth can be ignored, except if you want to use a custom wrapper around Debug.beginFile(), in which case you need to set the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.beginFile().
* Debug.endFile([depth: integer])
* - Ends the current file that was previously begun by using Debug.beginFile(). War3map.lua-lines after this will not be converted until the next instance of Debug.beginFile().
* - The next call of Debug.beginFile() will also end the previous one, so using Debug.endFile() is optional. Mainly recommended to use, if you prefer to have war3map.lua-references in a certain part of your script (such as within GUI triggers).
* - Depth can be ignored, except if you want to use a custom wrapper around Debug.endFile(), you need to increase the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.endFile().
*
* ----------------
* | Name Caching |
* ----------------
* - DebugUtils overwrites the tostring-function so that it prints the name of a non-primitive object (if available) instead of its memory position. The same applies to print().
* - For instance, print(CreateUnit) will show "function: CreateUnit" on screen instead of "function: 0063A698".
* - The table holding all those names is referred to as "Name Cache".
* - All names of objects in global scope will automatically be added to the Name Cache both within Lua root and again at game start (to get names for overwritten natives and your own objects).
* - New names entering global scope will also automatically be added, even after game start. The same applies to subtables of _G up to a depth of Debug.settings.NAME_CACHE_DEPTH.
* - Objects within subtables will be named after their parent tables and keys. For instance, the name of the function within T = {{bla = function() end}} is "T[1].bla".
* - The automatic adding doesn't work for objects saved into existing variables/keys after game start (because it's based on __newindex metamethod which simply doesn't trigger)
* - You can manually add names to the name cache by using the following API-functions:
*
* Debug.registerName(whichObject:any, name:string)
* - Adds the specified object under the specified name to the name cache, letting tostring and print output "<type>: <name>" going foward.
* - The object must be non-primitive, i.e. this won't work on strings, numbers and booleans.
* - This will overwrite existing names for the specified object with the specified name.
* Debug.registerNamesFrom(parentTable:table [, parentTableName:string] [, depth])
* - Adds names for all values from within the specified parentTable to the name cache.
* - Names for entries will be like "<parentTableName>.<key>" or "<parentTableName>[<key>]" (depending on the key type), using the existing name of the parentTable from the name cache.
* - You can optionally specify a parentTableName to use that for the entry naming instead of the existing name. Doing so will also register that name for the parentTable, if it doesn't already has one.
* - Specifying the empty string as parentTableName will suppress it in the naming and just register all values as "<key>". Note that only string keys will be considered this way.
* - In contrast to Debug.registerName(), this function will NOT overwrite existing names, but just add names for new objects.
* Debug.oldTostring(object:any) -> string
* - The old tostring-function in case you still need outputs like "function: 0063A698".
*
* -----------------
* | Other Utility |
* -----------------
*
* Debug.wc3Type(object:any) -> string
* - Returns the Warcraft3-type of the input object. E.g. Debug.wc3Type(Player(0)) will return "player".
* - Returns type(object), if used on Lua-objects.
* table.tostring(whichTable [, depth:integer] [, pretty_yn:boolean])
* - Creates a list of all (key,value)-pairs from the specified table. Also lists subtable entries up to the specified depth (unlimited, if not specified).
* - E.g. for T = {"a", 5, {7}}, table.tostring(T) would output '{(1, "a"), (2, 5), (3, {(1, 7)})}' (if using concise style, i.e. pretty_yn being nil or false).
* - Not specifying a depth can potentially lead to a stack overflow for self-referential tables (e.g X = {}; X[1] = X). Choose a sensible depth to prevent this (in doubt start with 1 and test upwards).
* - Supports pretty style by setting pretty_yn to true. Pretty style is linebreak-separated, uses indentations and has other visual improvements. Use it on small tables only, because Wc3 can't show that many linebreaks at once.
* - All of the following is valid syntax: table.tostring(T), table.tostring(T, depth), table.tostring(T, pretty_yn) or table.tostring(T, depth, pretty_yn).
* - table.tostring is not multiplayer-synced.
* table.print(whichTable [, depth:integer] [, pretty_yn:boolean])
* - Prints table.tostring(...).
*
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------]]
-- disable sumneko extension warnings for imported resource
---@diagnostic disable
----------------
--| Settings |--
----------------
Debug = {
--BEGIN OF SETTINGS--
settings = {
--Ingame Error Messages
SHOW_TRACE_ON_ERROR = true ---Set to true to show a stack trace on every error in addition to the regular message (msg sources: automatic error handling, Debug.try, Debug.throwError, ...)
, INCLUDE_DEBUGUTILS_INTO_TRACE = true ---Set to true to include lines from Debug Utils into the stack trace. Those show the source of error handling, which you might consider redundant.
, USE_TRY_ON_TRIGGERADDACTION = true ---Set to true for automatic error handling on TriggerAddAction (applies Debug.try on every trigger action).
, USE_TRY_ON_CONDITION = true ---Set to true for automatic error handling on boolexpressions created via Condition() or Filter() (essentially applies Debug.try on every trigger condition).
, USE_TRY_ON_TIMERSTART = true ---Set to true for automatic error handling on TimerStart (applies Debug.try on every timer callback).
, USE_TRY_ON_ENUMFUNCS = true ---Set to true for automatic error handling on ForGroup, ForForce, EnumItemsInRect and EnumDestructablesInRect (applies Debug.try on every enum callback)
, USE_TRY_ON_COROUTINES = true ---Set to true for improved stack traces on errors within coroutines (applies Debug.try on coroutine.create and coroutine.wrap). This lets stack traces point to the erroneous function executed within the coroutine (instead of the function creating the coroutine).
--Ingame Console and -exec
, ALLOW_INGAME_CODE_EXECUTION = true ---Set to true to enable IngameConsole and -exec command.
--Warnings for nil globals
, WARNING_FOR_NIL_GLOBALS = false ---Set to true to print warnings upon accessing nil-globals (i.e. globals containing no value).
, SHOW_TRACE_FOR_NIL_WARNINGS = false ---Set to true to include a stack trace into nil-warnings.
, EXCLUDE_BJ_GLOBALS_FROM_NIL_WARNINGS = true ---Set to true to exclude bj_ variables from nil-warnings.
, EXCLUDE_INITIALIZED_GLOBALS_FROM_NIL_WARNINGS = true ---Set to true to disable warnings for initialized globals, (i.e. nil globals that held a value at some point will be treated intentionally nilled and no longer prompt warnings).
--Print Caching
, USE_PRINT_CACHE = true ---Set to true to let print()-calls during loading screen be cached until the game starts.
, PRINT_DURATION = nil ---Adjust the duration in seconds that values printed by print() last on screen. Set to nil to use default duration (which depends on string length).
--Name Caching
, USE_NAME_CACHE = true ---Set to true to let tostring/print output the string-name of an object instead of its memory location (except for booleans/numbers/strings). E.g. print(CreateUnit) will output "function: CreateUnit" instead of "function: 0063A698".
, AUTO_REGISTER_NEW_NAMES = true ---Automatically adds new names from global scope (and subtables of _G up to NAME_CACHE_DEPTH) to the name cache by adding metatables with the __newindex metamethod to ALL tables accessible from global scope.
, NAME_CACHE_DEPTH = 4 ---Set to 0 to only affect globals. Experimental feature: Set to an integer > 0 to also cache names for subtables of _G (up to the specified depth). Warning: This will alter the __newindex metamethod of subtables of _G (but not break existing functionality).
}
--END OF SETTINGS--
--START OF CODE--
, data = {
nameCache = {} ---@type table<any,string> contains the string names of any object in global scope (random for objects that have multiple names)
, nameCacheMirror = {} ---@type table<string,any> contains the (name,object)-pairs of all objects in the name cache. Used to prevent name duplicates that might otherwise occur upon reassigning globals.
, nameDepths = {} ---@type table<any,integer> contains the depth of the name used by by any object in the name cache (i.e. the depth within the parentTable).
, autoIndexedTables = {} ---@type table<table,boolean> contains (t,true), if DebugUtils already set a __newindex metamethod for name caching in t. Prevents double application.
, paramLog = {} ---@type table<string,string> saves logged information per code location. to be filled by Debug.log(), to be printed by Debug.try()
, sourceMap = {{firstLine= 1,file='DebugUtils'}} ---@type table<integer,{firstLine:integer,file:string,lastLine?:integer}> saves lines and file names of all documents registered via Debug.beginFile().
, printCache = {n=0} ---@type string[] contains the strings that were attempted to print during loading screen.
, globalWarningExclusions = {} ---@type table<string,boolean> contains global variable names that should be excluded from warnings.
}
}
--localization
local settings, paramLog, nameCache, nameDepths, autoIndexedTables, nameCacheMirror, sourceMap, printCache = Debug.settings, Debug.data.paramLog, Debug.data.nameCache, Debug.data.nameDepths, Debug.data.autoIndexedTables, Debug.data.nameCacheMirror, Debug.data.sourceMap, Debug.data.printCache
--Write DebugUtils first line number to sourceMap:
---@diagnostic disable-next-line
Debug.data.sourceMap[1].firstLine = tonumber(codeLoc:match(":\x25d+"):sub(2,-1))
-------------------------------------------------
--| File Indexing for local Error Msg Support |--
-------------------------------------------------
-- Functions for war3map.lua -> local file conversion for error messages.
---Returns the line number in war3map.lua, where this is called (for depth = 0).
---Choose a depth d > 0 to instead return the line, where the d-th function in the stack leading to this call is executed.
---@param depth? integer default: 0.
---@return number?
function Debug.getLine(depth)
depth = depth or 0
local _, location = pcall(error, "", depth + 3) ---@diagnostic disable-next-line
local line = location:match(":\x25d+") --extracts ":1000" from "war3map.lua:1000:..."
return tonumber(line and line:sub(2,-1)) --check if line is nil before applying string.sub to prevent errors (nil can result from string.match above, although it should never do so in our case)
end
---Tells the Debug library that the specified file begins exactly here (i.e. in the line, where this is called).
---
---Using this improves stack traces of error messages. Stack trace will have "war3map.lua"-references between this and the next Debug.endFile() converted to file-specific references.
---
---To be called in the Lua root in Line 1 of every file you wish to track! Line 1 means exactly line 1, before any comment! This way, the line shown in the trace will exactly match your IDE.
---
---If you want to use a custom wrapper around Debug.beginFile(), you need to increase the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.beginFile().
---@param fileName string
---@param depth? integer default: 0. Set to 1, if you call this from a wrapper (and use the wrapper in line 1 of every document).
---@param lastLine? integer Ignore this. For compatibility with Total Initialization.
function Debug.beginFile(fileName, depth, lastLine)
depth, fileName = depth or 0, fileName or '' --filename is not actually optional, we just default to '' to prevent crashes.
local line = Debug.getLine(depth + 1)
if line then --for safety reasons. we don't want to add a non-existing line to the sourceMap
table.insert(sourceMap, {firstLine = line, file = fileName, lastLine = lastLine}) --automatically sorted list, because calls of Debug.beginFile happen logically in the order of the map script.
end
end
---Tells the Debug library that the file previously started with Debug.beginFile() ends here.
---This is in theory optional to use, as the next call of Debug.beginFile will also end the previous. Still good practice to always use this in the last line of every file.
---If you want to use a custom wrapper around Debug.endFile(), you need to increase the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.endFile().
---@param depth? integer
function Debug.endFile(depth)
depth = depth or 0
local line = Debug.getLine(depth + 1)
sourceMap[#sourceMap].lastLine = line
end
---Takes an error message containing a file and a linenumber and converts both to local file and line as saved to Debug.sourceMap.
---@param errorMsg string must be formatted like "<document>:<linenumber><RestOfMsg>".
---@return string convertedMsg a string of the form "<localDocument>:<localLinenumber><RestOfMsg>"
function Debug.getLocalErrorMsg(errorMsg)
local startPos, endPos = errorMsg:find(":\x25d*") --start and end position of line number. The part before that is the document, part after the error msg.
if startPos and endPos then --can be nil, if input string was not of the desired form "<document>:<linenumber><RestOfMsg>".
local document, line, rest = errorMsg:sub(1, startPos), tonumber(errorMsg:sub(startPos+1, endPos)), errorMsg:sub(endPos+1, -1) --get error line in war3map.lua
if document == 'war3map.lua:' and line then --only convert war3map.lua-references to local position. Other files such as Blizzard.j.lua are not converted (obiously).
for i = #sourceMap, 1, -1 do --find local file containing the war3map.lua error line.
if line >= sourceMap[i].firstLine then --war3map.lua line is part of sourceMap[i].file
if not sourceMap[i].lastLine or line <= sourceMap[i].lastLine then --if lastLine is given, we must also check for it
return sourceMap[i].file .. ":" .. (line - sourceMap[i].firstLine + 1) .. rest
else --if line is larger than firstLine and lastLine of sourceMap[i], it is not part of a tracked file -> return global war3map.lua position.
break --prevent return within next step of the loop ("line >= sourceMap[i].firstLine" would be true again, but wrong file)
end
end
end
end
end
return errorMsg
end
local convertToLocalErrorMsg = Debug.getLocalErrorMsg
----------------------
--| Error Handling |--
----------------------
local concat
---Applies tostring() on all input params and concatenates them 4-space-separated.
---@param firstParam any
---@param ... any
---@return string
concat = function(firstParam, ...)
if select('#', ...) == 0 then
return tostring(firstParam)
end
return tostring(firstParam) .. ' ' .. concat(...)
end
---Returns the stack trace between the specified startDepth and endDepth.
---The trace lists file names and line numbers. File name is only listed, if it has changed from the previous traced line.
---The previous file can also be specified as an input parameter to suppress the first file name in case it's identical.
---@param startDepth integer
---@param endDepth integer
---@return string trace
local function getStackTrace(startDepth, endDepth)
local trace, separator = "", ""
local _, currentFile, lastFile, tracePiece, lastTracePiece
for loopDepth = startDepth, endDepth do --get trace on different depth level
_, tracePiece = pcall(error, "", loopDepth) ---@type boolean, string
tracePiece = convertToLocalErrorMsg(tracePiece)
if #tracePiece > 0 and lastTracePiece ~= tracePiece then --some trace pieces can be empty, but there can still be valid ones beyond that
currentFile = tracePiece:match("^.-:")
--Hide DebugUtils in the stack trace (except main reference), if settings.INCLUDE_DEBUGUTILS_INTO_TRACE is set to true.
if settings.INCLUDE_DEBUGUTILS_INTO_TRACE or (loopDepth == startDepth) or currentFile ~= "DebugUtils:" then
trace = trace .. separator .. ((currentFile == lastFile) and tracePiece:match(":\x25d+"):sub(2,-1) or tracePiece:match("^.-:\x25d+"))
lastFile, lastTracePiece, separator = currentFile, tracePiece, " <- "
end
end
end
return trace
end
---Message Handler to be used by the try-function below.
---Adds stack trace plus formatting to the message and prints it.
---@param errorMsg string
---@param startDepth? integer default: 4 for use in xpcall
local function errorHandler(errorMsg, startDepth)
startDepth = startDepth or 4 --xpcall doesn't specify this param, so it must default to 4 for this case
errorMsg = convertToLocalErrorMsg(errorMsg)
--Print original error message and stack trace.
print("|cffff5555ERROR at " .. errorMsg .. "|r")
if settings.SHOW_TRACE_ON_ERROR then
print("|cffff5555Traceback (most recent call first):|r")
print("|cffff5555" .. getStackTrace(startDepth,200) .. "|r")
end
--Also print entries from param log, if there are any.
for location, loggedParams in pairs(paramLog) do
print("|cff888888Logged at " .. convertToLocalErrorMsg(location) .. loggedParams .. "|r")
paramLog[location] = nil
end
end
---Tries to execute the specified function with the specified parameters in protected mode and prints an error message (including stack trace), if unsuccessful.
---
---Example use: Assume you have a code line like "CreateUnit(0,1,2)", which doesn't work and you want to know why.
---* Option 1: Change it to "Debug.try(CreateUnit, 0, 1, 2)", i.e. separate the function from the parameters.
---* Option 2: Change it to "Debug.try(function() return CreateUnit(0,1,2) end)", i.e. pack it into an anonymous function. You can skip the "return", if you don't need the return values.
---When no error occured, the try-function will return all values returned by the input function.
---When an error occurs, try will print the resulting error and stack trace.
---@param funcToExecute function the function to call in protected mode
---@param ... any params for the input-function
---@return ... any
function Debug.try(funcToExecute, ...)
return select(2, xpcall(funcToExecute, errorHandler,...))
end
---@diagnostic disable-next-line lowercase-global
try = Debug.try
---Prints "ERROR:" and the specified error objects on the Screen. Also prints the stack trace leading to the error. You can specify as many arguments as you wish.
---
---In contrast to Lua's native error function, this can be called outside of protected mode and doesn't halt code execution.
---@param ... any objects/errormessages to be printed (doesn't have to be strings)
function Debug.throwError(...)
errorHandler(getStackTrace(4,4) .. ": " .. concat(...), 5)
end
---Prints the specified error message, if the specified condition fails (i.e. if it resolves to false or nil).
---
---Returns all specified arguments after the errorMsg, if the condition holds.
---
---In contrast to Lua's native assert function, this can be called outside of protected mode and doesn't halt code execution (even in case of condition failure).
---@param condition any actually a boolean, but you can use any object as a boolean.
---@param errorMsg string the message to be printed, if the condition fails
---@param ... any will be returned, if the condition holds
function Debug.assert(condition, errorMsg, ...)
if condition then
return ...
else
errorHandler(getStackTrace(4,4) .. ": " .. errorMsg, 5)
end
end
---Returns the stack trace at the code position where this function is called.
---The returned string includes war3map.lua/blizzard.j.lua code positions of all functions from the stack trace in the order of execution (most recent call last). It does NOT include function names.
---@return string
function Debug.traceback()
return getStackTrace(3,200)
end
---Saves the specified parameters to the debug log at the location where this function is called. The Debug-log will be printed for all affected locations upon the try-function catching an error.
---The log is unique per code location: Parameters logged at code line x will overwrite the previous ones logged at x. Parameters logged at different locations will all persist and be printed.
---@param ... any save any information, for instance the parameters of the function call that you are logging.
function Debug.log(...)
local _, location = pcall(error, "", 3) ---@diagnostic disable-next-line: need-check-nil
paramLog[location or ''] = concat(...)
end
----------------------------------
--| Undeclared Global Warnings |--
----------------------------------
--Utility function here. _G metatable behaviour is defined in Gamestart section further below.
local globalWarningExclusions = Debug.data.globalWarningExclusions
---Disables nil-warnings for the specified variable.
---@param variableName string
function Debug.disableNilWarningsFor(variableName)
globalWarningExclusions[variableName] = true
end
------------------------------------
--| Name Caching (API-functions) |--
------------------------------------
--Help-table. The registerName-functions below shall not work on call-by-value-types, i.e. booleans, strings and numbers (renaming a value of any primitive type doesn't make sense).
local skipType = {boolean = true, string = true, number = true, ['nil'] = true}
--Set weak keys to nameCache and nameDepths and weak values for nameCacheMirror to prevent garbage collection issues
setmetatable(nameCache, {__mode = 'k'})
setmetatable(nameDepths, getmetatable(nameCache))
setmetatable(nameCacheMirror, {__mode = 'v'})
---Removes the name from the name cache, if already used for any object (freeing it for the new object). This makes sure that a name is always unique.
---This doesn't solve the
---@param name string
local function removeNameIfNecessary(name)
if nameCacheMirror[name] then
nameCache[nameCacheMirror[name]] = nil
nameCacheMirror[name] = nil
end
end
---Registers a name for the specified object, which will be the future output for tostring(whichObject).
---You can overwrite existing names for whichObject by using this.
---@param whichObject any
---@param name string
function Debug.registerName(whichObject, name)
if not skipType[type(whichObject)] then
removeNameIfNecessary(name)
nameCache[whichObject] = name
nameCacheMirror[name] = whichObject
nameDepths[name] = 0
end
end
---Registers a new name to the nameCache as either just <key> (if parentTableName is the empty string), <table>.<key> (if parentTableName is given and string key doesn't contain whitespace) or <name>[<key>] notation (for other keys in existing tables).
---Only string keys without whitespace support <key>- and <table>.<key>-notation. All other keys require a parentTableName.
---@param parentTableName string | '""' empty string suppresses <table>-affix.
---@param key any
---@param object any only call-be-ref types allowed
---@param parentTableDepth? integer
local function addNameToCache(parentTableName, key, object, parentTableDepth)
parentTableDepth = parentTableDepth or -1
--Don't overwrite existing names for the same object, don't add names for primitive types.
if nameCache[object] or skipType[type(object)] then
return
end
local name
--apply dot-syntax for string keys without whitespace
if type(key) == 'string' and not string.find(key, "\x25s") then
if parentTableName == "" then
name = key
nameDepths[object] = 0
else
name = parentTableName .. "." .. key
nameDepths[object] = parentTableDepth + 1
end
--apply bracket-syntax for all other keys. This requires a parentTableName.
elseif parentTableName ~= "" then
name = type(key) == 'string' and ('"' .. key .. '"') or key
name = parentTableName .. "[" .. tostring(name) .. "]"
nameDepths[object] = parentTableDepth + 1
end
--Stop in cases without valid name (like parentTableName = "" and key = [1])
if name then
removeNameIfNecessary(name)
nameCache[object] = name
nameCacheMirror[name] = object
end
end
---Registers all call-by-reference objects in the given parentTable to the nameCache.
---Automatically filters out primitive objects and already registed Objects.
---@param parentTable table
---@param parentTableName? string
local function registerAllObjectsInTable(parentTable, parentTableName)
parentTableName = parentTableName or nameCache[parentTable] or ""
--Register all call-by-ref-objects in parentTable
for key, object in pairs(parentTable) do
addNameToCache(parentTableName, key, object, nameDepths[parentTable])
end
end
---Adds names for all values of the specified parentTable to the name cache. Names will be "<parentTableName>.<key>" or "<parentTableName>[<key>]", depending on the key type.
---
---Example: Given a table T = {f = function() end, [1] = {}}, tostring(T.f) and tostring(T[1]) will output "function: T.f" and "table: T[1]" respectively after running Debug.registerNamesFrom(T).
---The name of T itself must either be specified as an input parameter OR have previously been registered. It can also be suppressed by inputting the empty string (so objects will just display by their own names).
---The names of objects in global scope are automatically registered during loading screen.
---@param parentTable table base table of which all entries shall be registered (in the Form parentTableName.objectName).
---@param parentTableName? string|'""' Nil: takes <parentTableName> as previously registered. Empty String: Skips <parentTableName> completely. String <s>: Objects will show up as "<s>.<objectName>".
---@param depth? integer objects within sub-tables up to the specified depth will also be added. Default: 1 (only elements of whichTable). Must be >= 1.
---@overload fun(parentTable:table, depth:integer)
function Debug.registerNamesFrom(parentTable, parentTableName, depth)
--Support overloaded definition fun(parentTable:table, depth:integer)
if type(parentTableName) == 'number' then
depth = parentTableName
parentTableName = nil
end
--Apply default values
depth = depth or 1
parentTableName = parentTableName or nameCache[parentTable] or ""
--add name of T in case it hasn't already
if not nameCache[parentTable] and parentTableName ~= "" then
Debug.registerName(parentTable, parentTableName)
end
--Register all call-by-ref-objects in parentTable. To be preferred over simple recursive approach to ensure that top level names are preferred.
registerAllObjectsInTable(parentTable, parentTableName)
--if depth > 1 was specified, also register Names from subtables.
if depth > 1 then
for _, object in pairs(parentTable) do
if type(object) == 'table' then
Debug.registerNamesFrom(object, nil, depth - 1)
end
end
end
end
-------------------------------------------
--| Name Caching (Loading Screen setup) |--
-------------------------------------------
---Registers all existing object names from global scope and Lua incorporated libraries to be used by tostring() overwrite below.
local function registerNamesFromGlobalScope()
--Add all names from global scope to the name cache.
Debug.registerNamesFrom(_G, "")
--Add all names of Warcraft-enabled Lua libraries as well:
--Could instead add a depth to the function call above, but we want to ensure that these libraries are added even if the user has chosen depth 0.
for _, lib in ipairs({coroutine, math, os, string, table, utf8, Debug}) do
Debug.registerNamesFrom(lib)
end
--Add further names that are not accessible from global scope:
--Player(i)
for i = 0, GetBJMaxPlayerSlots() - 1 do
Debug.registerName(Player(i), "Player(" .. i .. ")")
end
end
--Set empty metatable to _G. __index is added when game starts (for "attempt to read nil-global"-errors), __newindex is added right below (for building the name cache).
setmetatable(_G, getmetatable(_G) or {}) --getmetatable(_G) should always return nil provided that DebugUtils is the topmost script file in the trigger editor, but we still include this for safety-
-- Save old tostring into Debug Library before overwriting it.
Debug.oldTostring = tostring
if settings.USE_NAME_CACHE then
local oldTostring = tostring
tostring = function(obj) --new tostring(CreateUnit) prints "function: CreateUnit"
--tostring of non-primitive object is NOT guaranteed to be like "<type>:<hex>", because it might have been changed by some __tostring-metamethod.
if settings.USE_NAME_CACHE then --return names from name cache only if setting is enabled. This allows turning it off during runtime (via Ingame Console) to revert to old tostring.
return nameCache[obj] and ((oldTostring(obj):match("^.-: ") or (oldTostring(obj) .. ": ")) .. nameCache[obj]) or oldTostring(obj)
end
return Debug.oldTostring(obj)
end
--Add names to Debug.data.objectNames within Lua root. Called below the other Debug-stuff to get the overwritten versions instead of the original ones.
registerNamesFromGlobalScope()
--Prepare __newindex-metamethod to automatically add new names to the name cache
if settings.AUTO_REGISTER_NEW_NAMES then
local nameRegisterNewIndex
---__newindex to be used for _G (and subtables up to a certain depth) to automatically register new names to the nameCache.
---Tables in global scope will use their own name. Subtables of them will use <parentName>.<childName> syntax.
---Global names don't support container[key]-notation (because "_G[...]" is probably not desired), so we only register string type keys instead of using prettyTostring.
---@param t table
---@param k any
---@param v any
---@param skipRawset? boolean set this to true when combined with another __newindex. Suppresses rawset(t,k,v) (because the other __newindex is responsible for that).
nameRegisterNewIndex = function(t,k,v, skipRawset)
local parentDepth = nameDepths[t] or 0
--Make sure the parent table has an existing name before using it as part of the child name
if t == _G or nameCache[t] then
local existingName = nameCache[v]
if not existingName then
addNameToCache((t == _G and "") or nameCache[t], k, v, parentDepth)
end
--If v is a table and the parent table has a valid name, inherit __newindex to v's existing metatable (or create a new one), if that wasn't already done.
if type(v) == 'table' and nameDepths[v] < settings.NAME_CACHE_DEPTH then
if not existingName then
--If v didn't have a name before, also add names for elements contained in v by construction (like v = {x = function() end} ).
Debug.registerNamesFrom(v, settings.NAME_CACHE_DEPTH - nameDepths[v])
end
--Apply __newindex to new tables.
if not autoIndexedTables[v] then
autoIndexedTables[v] = true
local mt = getmetatable(v)
if not mt then
mt = {}
setmetatable(v, mt) --only use setmetatable when we are sure there wasn't any before to prevent issues with "__metatable"-metamethod.
end
---@diagnostic disable-next-line: assign-type-mismatch
local existingNewIndex = mt.__newindex
local isTable_yn = (type(existingNewIndex) == 'table')
--If mt has an existing __newindex, add the name-register effect to it (effectively create a new __newindex using the old)
if existingNewIndex then
mt.__newindex = function(t,k,v)
nameRegisterNewIndex(t,k,v, true) --setting t[k] = v might not be desired in case of existing newindex. Skip it and let existingNewIndex make the decision.
if isTable_yn then
existingNewIndex[k] = v
else
return existingNewIndex(t,k,v)
end
end
else
--If mt doesn't have an existing __newindex, add one that adds the object to the name cache.
mt.__newindex = nameRegisterNewIndex
end
end
end
end
--Set t[k] = v.
if not skipRawset then
rawset(t,k,v)
end
end
--Apply metamethod to _G.
local existingNewIndex = getmetatable(_G).__newindex --should always be nil provided that DebugUtils is the topmost script in your trigger editor. Still included for safety.
local isTable_yn = (type(existingNewIndex) == 'table')
if existingNewIndex then
getmetatable(_G).__newindex = function(t,k,v)
nameRegisterNewIndex(t,k,v, true)
if isTable_yn then
existingNewIndex[k] = v
else
existingNewIndex(t,k,v)
end
end
else
getmetatable(_G).__newindex = nameRegisterNewIndex
end
end
end
------------------------------------------------------
--| Native Overwrite for Automatic Error Handling |--
------------------------------------------------------
--A table to store the try-wrapper for each function. This avoids endless re-creation of wrapper functions within the hooks below.
--Weak keys ensure that garbage collection continues as normal.
local tryWrappers = setmetatable({}, {__mode = 'k'}) ---@type table<function,function>
local try = Debug.try
---Takes a function and returns a wrapper executing the same function within Debug.try.
---Wrappers are permanently stored (until the original function is garbage collected) to ensure that they don't have to be created twice for the same function.
---@param func? function
---@return function
local function getTryWrapper(func)
if func then
tryWrappers[func] = tryWrappers[func] or function(...) return try(func, ...) end
end
return tryWrappers[func] --returns nil for func = nil (important for TimerStart overwrite below)
end
--Overwrite TriggerAddAction, TimerStart, Condition, Filter and Enum natives to let them automatically apply Debug.try.
--Also overwrites coroutine.create and coroutine.wrap to let stack traces point to the function executed within instead of the function creating the coroutine.
if settings.USE_TRY_ON_TRIGGERADDACTION then
local originalTriggerAddAction = TriggerAddAction
TriggerAddAction = function(whichTrigger, actionFunc)
return originalTriggerAddAction(whichTrigger, getTryWrapper(actionFunc))
end
end
if settings.USE_TRY_ON_TIMERSTART then
local originalTimerStart = TimerStart
TimerStart = function(whichTimer, timeout, periodic, handlerFunc)
originalTimerStart(whichTimer, timeout, periodic, getTryWrapper(handlerFunc))
end
end
if settings.USE_TRY_ON_CONDITION then
local originalCondition = Condition
Condition = function(func)
return originalCondition(getTryWrapper(func))
end
Filter = Condition
end
if settings.USE_TRY_ON_ENUMFUNCS then
local originalForGroup = ForGroup
ForGroup = function(whichGroup, callback)
originalForGroup(whichGroup, getTryWrapper(callback))
end
local originalForForce = ForForce
ForForce = function(whichForce, callback)
originalForForce(whichForce, getTryWrapper(callback))
end
local originalEnumItemsInRect = EnumItemsInRect
EnumItemsInRect = function(r, filter, actionfunc)
originalEnumItemsInRect(r, filter, getTryWrapper(actionfunc))
end
local originalEnumDestructablesInRect = EnumDestructablesInRect
EnumDestructablesInRect = function(r, filter, actionFunc)
originalEnumDestructablesInRect(r, filter, getTryWrapper(actionFunc))
end
end
if settings.USE_TRY_ON_COROUTINES then
local originalCoroutineCreate = coroutine.create
---@diagnostic disable-next-line: duplicate-set-field
coroutine.create = function(f)
return originalCoroutineCreate(getTryWrapper(f))
end
local originalCoroutineWrap = coroutine.wrap
---@diagnostic disable-next-line: duplicate-set-field
coroutine.wrap = function(f)
return originalCoroutineWrap(getTryWrapper(f))
end
end
------------------------------------------
--| Cache prints during Loading Screen |--
------------------------------------------
-- Apply the duration as specified in the settings.
if settings.PRINT_DURATION then
local display, getLocalPlayer, dur = DisplayTimedTextToPlayer, GetLocalPlayer, settings.PRINT_DURATION
print = function(...) ---@diagnostic disable-next-line: param-type-mismatch
display(getLocalPlayer(), 0, 0, dur, concat(...))
end
end
-- Delay loading screen prints to after game start.
if settings.USE_PRINT_CACHE then
local oldPrint = print
--loading screen print will write the values into the printCache
print = function(...)
if bj_gameStarted then
oldPrint(...)
else --during loading screen only: concatenate input arguments 4-space-separated, implicitely apply tostring on each, cache to table
---@diagnostic disable-next-line
printCache.n = printCache.n + 1
printCache[printCache.n] = concat(...)
end
end
end
-------------------------
--| Modify Game Start |--
-------------------------
local originalMarkGameStarted = MarkGameStarted
--Hook certain actions into the start of the game.
MarkGameStarted = function()
originalMarkGameStarted()
if settings.WARNING_FOR_NIL_GLOBALS then
local existingIndex = getmetatable(_G).__index
local isTable_yn = (type(existingIndex) == 'table')
getmetatable(_G).__index = function(t, k) --we made sure that _G has a metatable further above.
--Don't show warning, if the variable name has been actively excluded or if it's a bj_ variable (and those are excluded).
if (not globalWarningExclusions[k]) and ((not settings.EXCLUDE_BJ_GLOBALS_FROM_NIL_WARNINGS) or string.sub(tostring(k),1,3) ~= 'bj_') then --prevents intentionally nilled bj-variables from triggering the check within Blizzard.j-functions, like bj_cineFadeFinishTimer.
print("Trying to read nil global at " .. getStackTrace(4,4) .. ": " .. tostring(k)
.. (settings.SHOW_TRACE_FOR_NIL_WARNINGS and "\nTraceback (most recent call first):\n" .. getStackTrace(4,200) or ""))
end
if existingIndex then
if isTable_yn then
return existingIndex[k]
end
return existingIndex(t,k)
end
return rawget(t,k)
end
if settings.EXCLUDE_INITIALIZED_GLOBALS_FROM_NIL_WARNINGS then
local existingNewIndex = getmetatable(_G).__newindex
local isTable_yn = (type(existingNewIndex) == 'table')
getmetatable(_G).__newindex = function(t,k,v)
--First, exclude the initialized global from future warnings
globalWarningExclusions[k] = true
--Second, execute existing newindex, if there is one in place.
if existingNewIndex then
if isTable_yn then
existingNewIndex[k] = v
else
existingNewIndex(t,k,v)
end
else
rawset(t,k,v)
end
end
end
end
--Add names to Debug.data.objectNames again to ensure that overwritten natives also make it to the name cache.
--Overwritten natives have a new value, but the old key, so __newindex didn't trigger. But we can be sure that objectNames[v] doesn't yet exist, so adding again is safe.
if settings.USE_NAME_CACHE then
for _,v in pairs(_G) do
nameCache[v] = nil
end
registerNamesFromGlobalScope()
end
--Print messages that have been cached during loading screen.
if settings.USE_PRINT_CACHE then
--Note that we don't restore the old print. The overwritten variant only applies caching behaviour to loading screen prints anyway and "unhooking" always adds other risks.
for _, str in ipairs(printCache) do
print(str)
end ---@diagnostic disable-next-line: cast-local-type
printCache = nil --frees reference for the garbage collector
end
--Create triggers listening to "-console" and "-exec" chat input.
if settings.ALLOW_INGAME_CODE_EXECUTION and IngameConsole then
IngameConsole.createTriggers()
end
end
---------------------
--| Other Utility |--
---------------------
do
---Returns the type of a warcraft object as string, e.g. "unit" upon inputting a unit.
---@param input any
---@return string
function Debug.wc3Type(input)
local typeString = type(input)
if typeString == 'userdata' then
typeString = tostring(input) --tostring returns the warcraft type plus a colon and some hashstuff.
return typeString:sub(1, (typeString:find(":", nil, true) or 0) -1) --string.find returns nil, if the argument is not found, which would break string.sub. So we need to replace by 0.
else
return typeString
end
end
Wc3Type = Debug.wc3Type --for backwards compatibility
local conciseTostring, prettyTostring
---Translates a table into a comma-separated list of its (key,value)-pairs. Also translates subtables up to the specified depth.
---E.g. {"a", 5, {7}} will display as '{(1, "a"), (2, 5), (3, {(1, 7)})}'.
---@param object any
---@param depth? integer default: unlimited. Unlimited depth will throw a stack overflow error on self-referential tables.
---@return string
conciseTostring = function (object, depth)
depth = depth or -1
if type(object) == 'string' then
return '"' .. object .. '"'
elseif depth ~= 0 and type(object) == 'table' then
local elementArray = {}
local keyAsString
for k,v in pairs(object) do
keyAsString = type(k) == 'string' and ('"' .. tostring(k) .. '"') or tostring(k)
table.insert(elementArray, '(' .. keyAsString .. ', ' .. conciseTostring(v, depth -1) .. ')')
end
return '{' .. table.concat(elementArray, ', ') .. '}'
end
return tostring(object)
end
---Creates a list of all (key,value)-pairs from the specified table. Also lists subtable entries up to the specified depth.
---Major differences to concise print are:
--- * Format: Linebreak-formatted instead of one-liner, uses "[key] = value" instead of "(key,value)"
--- * Will also unpack tables used as keys
--- * Also includes the table's memory position as returned by tostring(table).
--- * Tables referenced multiple times will only be unpacked upon first encounter and abbreviated on subsequent encounters
--- * As a consequence, pretty version can be executed with unlimited depth on self-referential tables.
---@param object any
---@param depth? integer default: unlimited.
---@param constTable table
---@param indent string
---@return string
prettyTostring = function(object, depth, constTable, indent)
depth = depth or -1
local objType = type(object)
if objType == "string" then
return '"'..object..'"' --wrap the string in quotes.
elseif objType == 'table' and depth ~= 0 then
if not constTable[object] then
constTable[object] = tostring(object):gsub(":","")
if next(object)==nil then
return constTable[object]..": {}"
else
local mappedKV = {}
for k,v in pairs(object) do
table.insert(mappedKV, '\n ' .. indent ..'[' .. prettyTostring(k, depth - 1, constTable, indent .. " ") .. '] = ' .. prettyTostring(v, depth - 1, constTable, indent .. " "))
end
return constTable[object]..': {'.. table.concat(mappedKV, ',') .. '\n'..indent..'}'
end
end
end
return constTable[object] or tostring(object)
end
---Creates a list of all (key,value)-pairs from the specified table. Also lists subtable entries up to the specified depth.
---Supports concise style and pretty style.
---Concise will display {"a", 5, {7}} as '{(1, "a"), (2, 5), (3, {(1, 7)})}'.
---Pretty is linebreak-separated, so consider table size before converting. Pretty also abbreviates tables referenced multiple times.
---Can be called like table.tostring(T), table.tostring(T, depth), table.tostring(T, pretty_yn) or table.tostring(T, depth, pretty_yn).
---table.tostring is not multiplayer-synced.
---@param whichTable table
---@param depth? integer default: unlimited
---@param pretty_yn? boolean default: false (concise)
---@return string
---@overload fun(whichTable:table, pretty_yn?:boolean):string
function table.tostring(whichTable, depth, pretty_yn)
--reassign input params, if function was called as table.tostring(whichTable, pretty_yn)
if type(depth) == 'boolean' then
pretty_yn = depth
depth = -1
end
return pretty_yn and prettyTostring(whichTable, depth, {}, "") or conciseTostring(whichTable, depth)
end
---Prints a list of (key,value)-pairs contained in the specified table and its subtables up to the specified depth.
---Supports concise style and pretty style. Pretty is linebreak-separated, so consider table size before printing.
---Can be called like table.print(T), table.print(T, depth), table.print(T, pretty_yn) or table.print(T, depth, pretty_yn).
---@param whichTable table
---@param depth? integer default: unlimited
---@param pretty_yn? boolean default: false (concise)
---@overload fun(whichTable:table, pretty_yn?:boolean)
function table.print(whichTable, depth, pretty_yn)
print(table.tostring(whichTable, depth, pretty_yn))
end
end
end
Debug.endFile()
if Debug and Debug.beginFile then Debug.beginFile("IngameConsole") end
--[[
--------------------------
----| Ingame Console |----
--------------------------
/**********************************************
* Allows you to use the following ingame commands:
* "-exec <code>" to execute any code ingame.
* "-console" to start an ingame console interpreting any further chat input as code and showing both return values of function calls and error messages. Furthermore, the print function will print
* directly to the console after it got started. You can still look up all print messages in the F12-log.
***********************
* -------------------
* |Using the console|
* -------------------
* Any (well, most) chat input by any player after starting the console is interpreted as code and directly executed. You can enter terms (like 4+5 or just any variable name), function calls (like print("bla"))
* and set-statements (like y = 5). If the code has any return values, all of them are printed to the console. Erroneous code will print an error message.
* Chat input starting with a hyphen is being ignored by the console, i.e. neither executed as code nor printed to the console. This allows you to still use other chat commands like "-exec" without prompting errors.
***********************
* ------------------
* |Multiline-Inputs|
* ------------------
* You can prevent a chat input from being immediately executed by preceeding it with the '>' character. All lines entered this way are halted, until any line not starting with '>' is being entered.
* The first input without '>' will execute all halted lines (and itself) in one chunk.
* Example of a chat input (the console will add an additional '>' to every line):
* >function a(x)
* >return x
* end
***********************
* Note that multiline inputs don't accept pure term evaluations, e.g. the following input is not supported and will prompt an error, while the same lines would have worked as two single-line inputs:
* >x = 5
* x
***********************
* -------------------
* |Reserved Keywords|
* -------------------
* The following keywords have a reserved functionality, i.e. are direct commands for the console and will not be interpreted as code:
* - 'help' - will show a list of all reserved keywords along very short explanations.
* - 'exit' - will shut down the console
* - 'share' - will share the players console with every other player, allowing others to read and write into it. Will force-close other players consoles, if they have one active.
* - 'clear' - will clear all text from the console, except the word 'clear'
* - 'lasttrace' - will show the stack trace of the latest error that occured within IngameConsole
* - 'show' - will show the console, after it was accidently hidden (you can accidently hide it by showing another multiboard, while the console functionality is still up and running).
* - 'printtochat' - will let the print function return to normal behaviour (i.e. print to the chat instead of the console).
* - 'printtoconsole'- will let the print function print to the console (which is default behaviour).
* - 'autosize on' - will enable automatic console resize depending on the longest string in the display. This is turned on by default.
* - 'autosize off' - will disable automatic console resize and instead linebreak long strings into multiple lines.
* - 'textlang eng' - lets the console use english Wc3 text language font size to compute linebreaks (look in your Blizzard launcher settings to find out)
* - 'textlang ger' - lets the console use german Wc3 text language font size to compute linebreaks (look in your Blizzard launcher settings to find out)
***********************
* --------------
* |Paste Helper|
* --------------
* @Luashine has created a tool that simplifies pasting multiple lines of code from outside Wc3 into the IngameConsole.
* This is particularly useful, when you want to execute a large chunk of testcode containing several linebreaks.
* Goto: https://github.com/Luashine/wc3-debug-console-paste-helper#readme
*
*************************************************/
--]]
----------------
--| Settings |--
----------------
---@class IngameConsole
IngameConsole = {
--Settings
numRows = 20 ---@type integer Number of Rows of the console (multiboard), excluding the title row. So putting 20 here will show 21 rows, first being the title row.
, autosize = true ---@type boolean Defines, whether the width of the main Column automatically adjusts with the longest string in the display.
, currentWidth = 0.5 ---@type number Current and starting Screen Share of the console main column.
, mainColMinWidth = 0.3 ---@type number Minimum Screen share of the console main column.
, mainColMaxWidth = 0.8 ---@type number Maximum Scren share of the console main column.
, tsColumnWidth = 0.06 ---@type number Screen Share of the Timestamp Column
, linebreakBuffer = 0.008 ---@type number Screen Share that is added to longest string in display to calculate the screen share for the console main column. Compensates for the small inaccuracy of the String Width function.
, maxLinebreaks = 8 ---@type integer Defines the maximum amount of linebreaks, before the remaining output string will be cut and not further displayed.
, printToConsole = true ---@type boolean defines, if the print function should print to the console or to the chat
, sharedConsole = false ---@type boolean defines, if the console is displayed to each player at the same time (accepting all players input) or if all players much start their own console.
, showTraceOnError = false ---@type boolean defines, if the console shows a trace upon printing errors. Usually not too useful within console, because you have just initiated the erroneous call.
, textLanguage = 'eng' ---@type string text language of your Wc3 installation, which influences font size (look in the settings of your Blizzard launcher). Currently only supports 'eng' and 'ger'.
, colors = {
timestamp = "bbbbbb" ---@type string Timestamp Color
, singleLineInput = "ffffaa" ---@type string Color to be applied to single line console inputs
, multiLineInput = "ffcc55" ---@type string Color to be applied to multi line console inputs
, returnValue = "00ffff" ---@type string Color applied to return values
, error = "ff5555" ---@type string Color to be applied to errors resulting of function calls
, keywordInput = "ff00ff" ---@type string Color to be applied to reserved keyword inputs (console reserved keywords)
, info = "bbbbbb" ---@type string Color to be applied to info messages from the console itself (for instance after creation or after printrestore)
}
--Privates
, numCols = 2 ---@type integer Number of Columns of the console (multiboard). Adjusting this requires further changes on code base.
, player = nil ---@type player player for whom the console is being created
, currentLine = 0 ---@type integer Current Output Line of the console.
, inputload = '' ---@type string Input Holder for multi-line-inputs
, output = {} ---@type string[] Array of all output strings
, outputTimestamps = {} ---@type string[] Array of all output string timestamps
, outputWidths = {} ---@type number[] remembers all string widths to allow for multiboard resize
, trigger = nil ---@type trigger trigger processing all inputs during console lifetime
, multiboard = nil ---@type multiboard
, timer = nil ---@type timer gets started upon console creation to measure timestamps
, errorHandler = nil ---@type fun(errorMsg:string):string error handler to be used within xpcall. We create one per console to make it compatible with console-specific settings.
, lastTrace = '' ---@type string trace of last error occured within console. To be printed via reserved keyword "lasttrace"
--Statics
, keywords = {} ---@type table<string,function> saves functions to be executed for all reserved keywords
, playerConsoles = {} ---@type table<player,IngameConsole> Consoles currently being active. up to one per player.
, originalPrint = print ---@type function original print function to restore, after the console gets closed.
}
IngameConsole.__index = IngameConsole
IngameConsole.__name = 'IngameConsole'
------------------------
--| Console Creation |--
------------------------
---Creates and opens up a new console.
---@param consolePlayer player player for whom the console is being created
---@return IngameConsole
function IngameConsole.create(consolePlayer)
local new = {} ---@type IngameConsole
setmetatable(new, IngameConsole)
---setup Object data
new.player = consolePlayer
new.output = {}
new.outputTimestamps = {}
new.outputWidths = {}
--Timer
new.timer = CreateTimer()
TimerStart(new.timer, 3600., true, nil) --just to get TimeElapsed for printing Timestamps.
--Trigger to be created after short delay, because otherwise it would fire on "-console" input immediately and lead to stack overflow.
new:setupTrigger()
--Multiboard
new:setupMultiboard()
--Create own error handler per console to be compatible with console-specific settings
new:setupErrorHandler()
--Share, if settings say so
if IngameConsole.sharedConsole then
new:makeShared() --we don't have to exit other players consoles, because we look for the setting directly in the class and there just logically can't be other active consoles.
end
--Welcome Message
new:out('info', 0, false, "Console started. Any further chat input will be executed as code, except when beginning with \x22-\x22.")
return new
end
---Creates the multiboard used for console display.
function IngameConsole:setupMultiboard()
self.multiboard = CreateMultiboard()
MultiboardSetRowCount(self.multiboard, self.numRows + 1) --title row adds 1
MultiboardSetColumnCount(self.multiboard, self.numCols)
MultiboardSetTitleText(self.multiboard, "Console")
local mbitem
for col = 1, self.numCols do
for row = 1, self.numRows + 1 do --Title row adds 1
mbitem = MultiboardGetItem(self.multiboard, row -1, col -1)
MultiboardSetItemStyle(mbitem, true, false)
MultiboardSetItemValueColor(mbitem, 255, 255, 255, 255) -- Colors get applied via text color code
MultiboardSetItemWidth(mbitem, (col == 1 and self.tsColumnWidth) or self.currentWidth )
MultiboardReleaseItem(mbitem)
end
end
mbitem = MultiboardGetItem(self.multiboard, 0, 0)
MultiboardSetItemValue(mbitem, "|cffffcc00Timestamp|r")
MultiboardReleaseItem(mbitem)
mbitem = MultiboardGetItem(self.multiboard, 0, 1)
MultiboardSetItemValue(mbitem, "|cffffcc00Line|r")
MultiboardReleaseItem(mbitem)
self:showToOwners()
end
---Creates the trigger that responds to chat events.
function IngameConsole:setupTrigger()
self.trigger = CreateTrigger()
TriggerRegisterPlayerChatEvent(self.trigger, self.player, "", false) --triggers on any input of self.player
TriggerAddCondition(self.trigger, Condition(function() return string.sub(GetEventPlayerChatString(),1,1) ~= '-' end)) --console will not react to entered stuff starting with '-'. This still allows to use other chat orders like "-exec".
TriggerAddAction(self.trigger, function() self:processInput(GetEventPlayerChatString()) end)
end
---Creates an Error Handler to be used by xpcall below.
---Adds stack trace plus formatting to the message.
function IngameConsole:setupErrorHandler()
self.errorHandler = function(errorMsg)
errorMsg = Debug.getLocalErrorMsg(errorMsg)
local _, tracePiece, lastFile = nil, "", errorMsg:match("^.-:") or "<unknown>" -- errors on objects created within Ingame Console don't have a file and linenumber. Consider "x = {}; x[nil] = 5".
local fullMsg = errorMsg .. "\nTraceback (most recent call first):\n" .. (errorMsg:match("^.-:\x25d+") or "<unknown>")
--Get Stack Trace. Starting at depth 5 ensures that "error", "messageHandler", "xpcall" and the input error message are not included.
for loopDepth = 5, 50 do --get trace on depth levels up to 50
---@diagnostic disable-next-line: cast-local-type, assign-type-mismatch
_, tracePiece = pcall(error, "", loopDepth) ---@type boolean, string
tracePiece = Debug.getLocalErrorMsg(tracePiece)
if #tracePiece > 0 then --some trace pieces can be empty, but there can still be valid ones beyond that
fullMsg = fullMsg .. " <- " .. ((tracePiece:match("^.-:") == lastFile) and tracePiece:match(":\x25d+"):sub(2,-1) or tracePiece:match("^.-:\x25d+"))
lastFile = tracePiece:match("^.-:")
end
end
self.lastTrace = fullMsg
return "ERROR: " .. (self.showTraceOnError and fullMsg or errorMsg)
end
end
---Shares this console with all players.
function IngameConsole:makeShared()
local player
for i = 0, GetBJMaxPlayers() -1 do
player = Player(i)
if (GetPlayerSlotState(player) == PLAYER_SLOT_STATE_PLAYING) and (IngameConsole.playerConsoles[player] ~= self) then --second condition ensures that the player chat event is not added twice for the same player.
IngameConsole.playerConsoles[player] = self
TriggerRegisterPlayerChatEvent(self.trigger, player, "", false) --triggers on any input
end
end
self.sharedConsole = true
end
---------------------
--| In |--
---------------------
---Processes a chat string. Each input will be printed. Incomplete multiline-inputs will be halted until completion. Completed inputs will be converted to a function and executed. If they have an output, it will be printed.
---@param inputString string
function IngameConsole:processInput(inputString)
--if the input is a reserved keyword, conduct respective actions and skip remaining actions.
if IngameConsole.keywords[inputString] then --if the input string is a reserved keyword
self:out('keywordInput', 1, false, inputString)
IngameConsole.keywords[inputString](self) --then call the method with the same name. IngameConsole.keywords["exit"](self) is just self.keywords:exit().
return
end
--if the input is a multi-line-input, queue it into the string buffer (inputLoad), but don't yet execute anything
if string.sub(inputString, 1, 1) == '>' then --multiLineInput
inputString = string.sub(inputString, 2, -1)
self:out('multiLineInput',2, false, inputString)
self.inputload = self.inputload .. inputString .. '\n'
else --if the input is either singleLineInput OR the last line of multiLineInput, execute the whole thing.
self:out(self.inputload == '' and 'singleLineInput' or 'multiLineInput', 1, false, inputString)
self.inputload = self.inputload .. inputString
local loadedFunc, errorMsg = load("return " .. self.inputload) --adds return statements, if possible (works for term statements)
if loadedFunc == nil then
loadedFunc, errorMsg = load(self.inputload)
end
self.inputload = '' --empty inputload before execution of pcall. pcall can break (rare case, can for example be provoked with metatable.__tostring = {}), which would corrupt future console inputs.
--manually catch case, where the input did not define a proper Lua statement (i.e. loadfunc is nil)
local results = loadedFunc and table.pack(xpcall(loadedFunc, self.errorHandler)) or {false, "Input is not a valid Lua-statement: " .. errorMsg}
--output error message (unsuccessful case) or return values (successful case)
if not results[1] then --results[1] is the error status that pcall always returns. False stands for: error occured.
self:out('error', 0, true, results[2]) -- second result of pcall is the error message in case an error occured
elseif results.n > 1 then --Check, if there was at least one valid output argument. We check results.n instead of results[2], because we also get nil as a proper return value this way.
self:out('returnValue', 0, true, table.unpack(results, 2, results.n))
end
end
end
----------------------
--| Out |--
----------------------
-- split color codes, split linebreaks, print lines separately, print load-errors, update string width, update text, error handling with stack trace.
---Duplicates Color coding around linebreaks to make each line printable separately.
---Operates incorrectly on lookalike color codes invalidated by preceeding escaped vertical bar (like "||cffffcc00bla|r").
---Also operates incorrectly on multiple color codes, where the first is missing the end sequence (like "|cffffcc00Hello |cff0000ffWorld|r")
---@param inputString string
---@return string, integer
function IngameConsole.spreadColorCodes(inputString)
local replacementTable = {} --remembers all substrings to be replaced and their replacements.
for foundInstance, color in inputString:gmatch("((|c\x25x\x25x\x25x\x25x\x25x\x25x\x25x\x25x).-|r)") do
replacementTable[foundInstance] = foundInstance:gsub("(\r?\n)", "|r\x251" .. color)
end
return inputString:gsub("((|c\x25x\x25x\x25x\x25x\x25x\x25x\x25x\x25x).-|r)", replacementTable)
end
---Concatenates all inputs to one string, spreads color codes around line breaks and prints each line to the console separately.
---@param colorTheme? '"timestamp"'| '"singleLineInput"' | '"multiLineInput"' | '"result"' | '"keywordInput"' | '"info"' | '"error"' | '"returnValue"' Decides about the color to be applied. Currently accepted: 'timestamp', 'singleLineInput', 'multiLineInput', 'result', nil. (nil equals no colorTheme, i.e. white color)
---@param numIndentations integer Number of '>' chars that shall preceed the output
---@param hideTimestamp boolean Set to false to hide the timestamp column and instead show a "->" symbol.
---@param ... any the things to be printed in the console.
function IngameConsole:out(colorTheme, numIndentations, hideTimestamp, ...)
local inputs = table.pack(...)
for i = 1, inputs.n do
inputs[i] = tostring(inputs[i]) --apply tostring on every input param in preparation for table.concat
end
--Concatenate all inputs (4-space-separated)
local printOutput = table.concat(inputs, ' ', 1, inputs.n)
printOutput = printOutput:find("(\r?\n)") and IngameConsole.spreadColorCodes(printOutput) or printOutput
local substrStart, substrEnd = 1, 1
local numLinebreaks, completePrint = 0, true
repeat
substrEnd = (printOutput:find("(\r?\n)", substrStart) or 0) - 1
numLinebreaks, completePrint = self:lineOut(colorTheme, numIndentations, hideTimestamp, numLinebreaks, printOutput:sub(substrStart, substrEnd))
hideTimestamp = true
substrStart = substrEnd + 2
until substrEnd == -1 or numLinebreaks > self.maxLinebreaks
if substrEnd ~= -1 or not completePrint then
self:lineOut('info', 0, false, 0, "Previous value not entirely printed after exceeding maximum number of linebreaks. Consider adjusting 'IngameConsole.maxLinebreaks'.")
end
self:updateMultiboard()
end
---Prints the given string to the console with the specified colorTheme and the specified number of indentations.
---Only supports one-liners (no \n) due to how multiboards work. Will add linebreaks though, if the one-liner doesn't fit into the given multiboard space.
---@param colorTheme? '"timestamp"'| '"singleLineInput"' | '"multiLineInput"' | '"result"' | '"keywordInput"' | '"info"' | '"error"' | '"returnValue"' Decides about the color to be applied. Currently accepted: 'timestamp', 'singleLineInput', 'multiLineInput', 'result', nil. (nil equals no colorTheme, i.e. white color)
---@param numIndentations integer Number of greater '>' chars that shall preceed the output
---@param hideTimestamp boolean Set to false to hide the timestamp column and instead show a "->" symbol.
---@param numLinebreaks integer
---@param printOutput string the line to be printed in the console.
---@return integer numLinebreaks, boolean hasPrintedEverything returns true, if everything could be printed. Returns false otherwise (can happen for very long strings).
function IngameConsole:lineOut(colorTheme, numIndentations, hideTimestamp, numLinebreaks, printOutput)
--add preceeding greater chars
printOutput = ('>'):rep(numIndentations) .. printOutput
--Print a space instead of the empty string. This allows the console to identify, if the string has already been fully printed (see while-loop below).
if printOutput == '' then
printOutput = ' '
end
--Compute Linebreaks.
local linebreakWidth = ((self.autosize and self.mainColMaxWidth) or self.currentWidth )
local partialOutput = nil
local maxPrintableCharPosition
local printWidth
while string.len(printOutput) > 0 and numLinebreaks <= self.maxLinebreaks do --break, if the input string has reached length 0 OR when the maximum number of linebreaks would be surpassed.
--compute max printable substring (in one multiboard line)
maxPrintableCharPosition, printWidth = IngameConsole.getLinebreakData(printOutput, linebreakWidth - self.linebreakBuffer, self.textLanguage)
--adds timestamp to the first line of any output
if numLinebreaks == 0 then
partialOutput = printOutput:sub(1, numIndentations) .. ((IngameConsole.colors[colorTheme] and "|cff" .. IngameConsole.colors[colorTheme] .. printOutput:sub(numIndentations + 1, maxPrintableCharPosition) .. "|r") or printOutput:sub(numIndentations + 1, maxPrintableCharPosition)) --Colorize the output string, if a color theme was specified. IngameConsole.colors[colorTheme] can be nil.
table.insert(self.outputTimestamps, "|cff" .. IngameConsole.colors['timestamp'] .. ((hideTimestamp and ' ->') or IngameConsole.formatTimerElapsed(TimerGetElapsed(self.timer))) .. "|r")
else
partialOutput = (IngameConsole.colors[colorTheme] and "|cff" .. IngameConsole.colors[colorTheme] .. printOutput:sub(1, maxPrintableCharPosition) .. "|r") or printOutput:sub(1, maxPrintableCharPosition) --Colorize the output string, if a color theme was specified. IngameConsole.colors[colorTheme] can be nil.
table.insert(self.outputTimestamps, ' ..') --need a dummy entry in the timestamp list to make it line-progress with the normal output.
end
numLinebreaks = numLinebreaks + 1
--writes output string and width to the console tables.
table.insert(self.output, partialOutput)
table.insert(self.outputWidths, printWidth + self.linebreakBuffer) --remember the Width of this printed string to adjust the multiboard size in case. 0.5 percent is added to avoid the case, where the multiboard width is too small by a tiny bit, thus not showing some string without spaces.
--compute remaining string to print
printOutput = string.sub(printOutput, maxPrintableCharPosition + 1, -1) --remaining string until the end. Returns empty string, if there is nothing left
end
self.currentLine = #self.output
return numLinebreaks, string.len(printOutput) == 0 --printOutput is the empty string, if and only if everything has been printed
end
---Lets the multiboard show the recently printed lines.
function IngameConsole:updateMultiboard()
local startIndex = math.max(self.currentLine - self.numRows, 0) --to be added to loop counter to get to the index of output table to print
local outputIndex = 0
local maxWidth = 0.
local mbitem
for i = 1, self.numRows do --doesn't include title row (index 0)
outputIndex = i + startIndex
mbitem = MultiboardGetItem(self.multiboard, i, 0)
MultiboardSetItemValue(mbitem, self.outputTimestamps[outputIndex] or '')
MultiboardReleaseItem(mbitem)
mbitem = MultiboardGetItem(self.multiboard, i, 1)
MultiboardSetItemValue(mbitem, self.output[outputIndex] or '')
MultiboardReleaseItem(mbitem)
maxWidth = math.max(maxWidth, self.outputWidths[outputIndex] or 0.) --looping through non-defined widths, so need to coalesce with 0
end
--Adjust Multiboard Width, if necessary.
maxWidth = math.min(math.max(maxWidth, self.mainColMinWidth), self.mainColMaxWidth)
if self.autosize and self.currentWidth ~= maxWidth then
self.currentWidth = maxWidth
for i = 1, self.numRows +1 do
mbitem = MultiboardGetItem(self.multiboard, i-1, 1)
MultiboardSetItemWidth(mbitem, maxWidth)
MultiboardReleaseItem(mbitem)
end
self:showToOwners() --reshow multiboard to update item widths on the frontend
end
end
---Shows the multiboard to all owners (one or all players)
function IngameConsole:showToOwners()
if self.sharedConsole or GetLocalPlayer() == self.player then
MultiboardDisplay(self.multiboard, true)
MultiboardMinimize(self.multiboard, false)
end
end
---Formats the elapsed time as "mm: ss. hh" (h being a hundreds of a sec)
function IngameConsole.formatTimerElapsed(elapsedInSeconds)
return string.format("\x2502d: \x2502.f. \x2502.f", elapsedInSeconds // 60, math.fmod(elapsedInSeconds, 60.) // 1, math.fmod(elapsedInSeconds, 1) * 100)
end
---Computes the max printable substring for a given string and a given linebreakWidth (regarding a single line of console).
---Returns both the substrings last char position and its total width in the multiboard.
---@param stringToPrint string the string supposed to be printed in the multiboard console.
---@param linebreakWidth number the maximum allowed width in one line of the console, before a string must linebreak
---@param textLanguage string 'ger' or 'eng'
---@return integer maxPrintableCharPosition, number printWidth
function IngameConsole.getLinebreakData(stringToPrint, linebreakWidth, textLanguage)
local loopWidth = 0.
local bytecodes = table.pack(string.byte(stringToPrint, 1, -1))
for i = 1, bytecodes.n do
loopWidth = loopWidth + string.charMultiboardWidth(bytecodes[i], textLanguage)
if loopWidth > linebreakWidth then
return i-1, loopWidth - string.charMultiboardWidth(bytecodes[i], textLanguage)
end
end
return bytecodes.n, loopWidth
end
-------------------------
--| Reserved Keywords |--
-------------------------
---Exits the Console
---@param self IngameConsole
function IngameConsole.keywords.exit(self)
DestroyMultiboard(self.multiboard)
DestroyTrigger(self.trigger)
DestroyTimer(self.timer)
IngameConsole.playerConsoles[self.player] = nil
if next(IngameConsole.playerConsoles) == nil then --set print function back to original, when no one has an active console left.
print = IngameConsole.originalPrint
end
end
---Lets the console print to chat
---@param self IngameConsole
function IngameConsole.keywords.printtochat(self)
self.printToConsole = false
self:out('info', 0, false, "The print function will print to the normal chat.")
end
---Lets the console print to itself (default)
---@param self IngameConsole
function IngameConsole.keywords.printtoconsole(self)
self.printToConsole = true
self:out('info', 0, false, "The print function will print to the console.")
end
---Shows the console in case it was hidden by another multiboard before
---@param self IngameConsole
function IngameConsole.keywords.show(self)
self:showToOwners() --might be necessary to do, if another multiboard has shown up and thereby hidden the console.
self:out('info', 0, false, "Console is showing.")
end
---Prints all available reserved keywords plus explanations.
---@param self IngameConsole
function IngameConsole.keywords.help(self)
self:out('info', 0, false, "The Console currently reserves the following keywords:")
self:out('info', 0, false, "'help' shows the text you are currently reading.")
self:out('info', 0, false, "'exit' closes the console.")
self:out('info', 0, false, "'lasttrace' shows the stack trace of the latest error that occured within IngameConsole.")
self:out('info', 0, false, "'share' allows other players to read and write into your console, but also force-closes their own consoles.")
self:out('info', 0, false, "'clear' clears all text from the console.")
self:out('info', 0, false, "'show' shows the console. Sensible to use, when displaced by another multiboard.")
self:out('info', 0, false, "'printtochat' lets Wc3 print text to normal chat again.")
self:out('info', 0, false, "'printtoconsole' lets Wc3 print text to the console (default).")
self:out('info', 0, false, "'autosize on' enables automatic console resize depending on the longest line in the display.")
self:out('info', 0, false, "'autosize off' retains the current console size.")
self:out('info', 0, false, "'textlang eng' will use english text installation font size to compute linebreaks (default).")
self:out('info', 0, false, "'textlang ger' will use german text installation font size to compute linebreaks.")
self:out('info', 0, false, "Preceeding a line with '>' prevents immediate execution, until a line not starting with '>' has been entered.")
end
---Clears the display of the console.
---@param self IngameConsole
function IngameConsole.keywords.clear(self)
self.output = {}
self.outputTimestamps = {}
self.outputWidths = {}
self.currentLine = 0
self:out('keywordInput', 1, false, 'clear') --we print 'clear' again. The keyword was already printed by self:processInput, but cleared immediately after.
end
---Shares the console with other players in the same game.
---@param self IngameConsole
function IngameConsole.keywords.share(self)
for _, console in pairs(IngameConsole.playerConsoles) do
if console ~= self then
IngameConsole.keywords['exit'](console) --share was triggered during console runtime, so there potentially are active consoles of others players that need to exit.
end
end
self:makeShared()
self:showToOwners() --showing it to the other players.
self:out('info', 0,false, "The console of player " .. GetConvertedPlayerId(self.player) .. " is now shared with all players.")
end
---Enables auto-sizing of console (will grow and shrink together with text size)
---@param self IngameConsole
IngameConsole.keywords["autosize on"] = function(self)
self.autosize = true
self:out('info', 0,false, "The console will now change size depending on its content.")
end
---Disables auto-sizing of console
---@param self IngameConsole
IngameConsole.keywords["autosize off"] = function(self)
self.autosize = false
self:out('info', 0,false, "The console will retain the width that it currently has.")
end
---Lets linebreaks be computed by german font size
---@param self IngameConsole
IngameConsole.keywords["textlang ger"] = function(self)
self.textLanguage = 'ger'
self:out('info', 0,false, "Linebreaks will now compute with respect to german text installation font size.")
end
---Lets linebreaks be computed by english font size
---@param self IngameConsole
IngameConsole.keywords["textlang eng"] = function(self)
self.textLanguage = 'eng'
self:out('info', 0,false, "Linebreaks will now compute with respect to english text installation font size.")
end
---Prints the stack trace of the latest error that occured within IngameConsole.
---@param self IngameConsole
IngameConsole.keywords["lasttrace"] = function(self)
self:out('error', 0,false, self.lastTrace)
end
--------------------
--| Main Trigger |--
--------------------
do
--Actions to be executed upon typing -exec
local function execCommand_Actions()
local input = string.sub(GetEventPlayerChatString(),7,-1)
print("Executing input: |cffffff44" .. input .. "|r")
--try preceeding the input by a return statement (preparation for printing below)
local loadedFunc, errorMsg = load("return ".. input)
if not loadedFunc then --if that doesn't produce valid code, try without return statement
loadedFunc, errorMsg = load(input)
end
--execute loaded function in case the string defined a valid function. Otherwise print error.
if errorMsg then
print("|cffff5555Invalid Lua-statement: " .. Debug.getLocalErrorMsg(errorMsg) .. "|r")
else
---@diagnostic disable-next-line: param-type-mismatch
local results = table.pack(Debug.try(loadedFunc))
if results[1] ~= nil or results.n > 1 then
for i = 1, results.n do
results[i] = tostring(results[i])
end
--concatenate all function return values to one colorized string
print("|cff00ffff" .. table.concat(results, ' ', 1, results.n) .. "|r")
end
end
end
local function execCommand_Condition()
return string.sub(GetEventPlayerChatString(), 1, 6) == "-exec "
end
function IngameConsole.ForceStart()
local p = GetLocalPlayer();
if IngameConsole.playerConsoles[p] then
IngameConsole.playerConsoles[p]:showToOwners()
return
end
--create Ingame Console object
IngameConsole.playerConsoles[p] = IngameConsole.create(p)
--overwrite print function
print = function(...)
IngameConsole.originalPrint(...) --the new print function will also print "normally", but clear the text immediately after. This is to add the message to the F12-log.
if IngameConsole.playerConsoles[GetLocalPlayer()] and IngameConsole.playerConsoles[GetLocalPlayer()].printToConsole then
ClearTextMessages() --clear text messages for all players having an active console
end
for player, console in pairs(IngameConsole.playerConsoles) do
if console.printToConsole and (player == console.player) then --player == console.player ensures that the console only prints once, even if the console was shared among all players
console:out(nil, 0, false, ...)
end
end
end
end
local function startIngameConsole()
--if the triggering player already has a console, show that console and stop executing further actions
if IngameConsole.playerConsoles[GetTriggerPlayer()] then
IngameConsole.playerConsoles[GetTriggerPlayer()]:showToOwners()
return
end
--create Ingame Console object
IngameConsole.playerConsoles[GetTriggerPlayer()] = IngameConsole.create(GetTriggerPlayer())
--overwrite print function
print = function(...)
IngameConsole.originalPrint(...) --the new print function will also print "normally", but clear the text immediately after. This is to add the message to the F12-log.
if IngameConsole.playerConsoles[GetLocalPlayer()] and IngameConsole.playerConsoles[GetLocalPlayer()].printToConsole then
ClearTextMessages() --clear text messages for all players having an active console
end
for player, console in pairs(IngameConsole.playerConsoles) do
if console.printToConsole and (player == console.player) then --player == console.player ensures that the console only prints once, even if the console was shared among all players
console:out(nil, 0, false, ...)
end
end
end
end
---Creates the triggers listening to "-console" and "-exec" chat input.
---Being executed within DebugUtils (MarkGameStart overwrite).
function IngameConsole.createTriggers()
--Exec
local execTrigger = CreateTrigger()
TriggerAddCondition(execTrigger, Condition(execCommand_Condition))
TriggerAddAction(execTrigger, execCommand_Actions)
--Real Console
local consoleTrigger = CreateTrigger()
TriggerAddAction(consoleTrigger, startIngameConsole)
--Events
for i = 0, GetBJMaxPlayers() -1 do
TriggerRegisterPlayerChatEvent(execTrigger, Player(i), "-exec ", false)
TriggerRegisterPlayerChatEvent(consoleTrigger, Player(i), "-console", true)
end
end
end
--[[
used by Ingame Console to determine multiboard size
every unknown char will be treated as having default width (see constants below)
--]]
do
----------------------------
----| String Width API |----
----------------------------
local multiboardCharTable = {} ---@type table -- saves the width in screen percent (on 1920 pixel width resolutions) that each char takes up, when displayed in a multiboard.
local DEFAULT_MULTIBOARD_CHAR_WIDTH = 1. / 128. ---@type number -- used for unknown chars (where we didn't define a width in the char table)
local MULTIBOARD_TO_PRINT_FACTOR = 1. / 36. ---@type number -- 36 is actually the lower border (longest width of a non-breaking string only consisting of the letter "i")
---Returns the width of a char in a multiboard, when inputting a char (string of length 1) and 0 otherwise.
---also returns 0 for non-recorded chars (like ` and ´ and ß and § and €)
---@param char string | integer integer bytecode representations of chars are also allowed, i.e. the results of string.byte().
---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@return number
function string.charMultiboardWidth(char, textlanguage)
return multiboardCharTable[textlanguage or 'eng'][char] or DEFAULT_MULTIBOARD_CHAR_WIDTH
end
---returns the width of a string in a multiboard (i.e. output is in screen percent)
---unknown chars will be measured with default width (see constants above)
---@param multichar string
---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@return number
function string.multiboardWidth(multichar, textlanguage)
local chartable = table.pack(multichar:byte(1,-1)) --packs all bytecode char representations into a table
local charWidth = 0.
for i = 1, chartable.n do
charWidth = charWidth + string.charMultiboardWidth(chartable[i], textlanguage)
end
return charWidth
end
---The function should match the following criteria: If the value returned by this function is smaller than 1.0, than the string fits into a single line on screen.
---The opposite is not necessarily true (but should be true in the majority of cases): If the function returns bigger than 1.0, the string doesn't necessarily break.
---@param char string | integer integer bytecode representations of chars are also allowed, i.e. the results of string.byte().
---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@return number
function string.charPrintWidth(char, textlanguage)
return string.charMultiboardWidth(char, textlanguage) * MULTIBOARD_TO_PRINT_FACTOR
end
---The function should match the following criteria: If the value returned by this function is smaller than 1.0, than the string fits into a single line on screen.
---The opposite is not necessarily true (but should be true in the majority of cases): If the function returns bigger than 1.0, the string doesn't necessarily break.
---@param multichar string
---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@return number
function string.printWidth(multichar, textlanguage)
return string.multiboardWidth(multichar, textlanguage) * MULTIBOARD_TO_PRINT_FACTOR
end
----------------------------------
----| String Width Internals |----
----------------------------------
---@param charset '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@param char string|integer either the char or its bytecode
---@param lengthInScreenWidth number
local function setMultiboardCharWidth(charset, char, lengthInScreenWidth)
multiboardCharTable[charset] = multiboardCharTable[charset] or {}
multiboardCharTable[charset][char] = lengthInScreenWidth
end
---numberPlacements says how often the char can be placed in a multiboard column, before reaching into the right bound.
---@param charset '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@param char string|integer either the char or its bytecode
---@param numberPlacements integer
local function setMultiboardCharWidthBase80(charset, char, numberPlacements)
setMultiboardCharWidth(charset, char, 0.8 / numberPlacements) --1-based measure. 80./numberPlacements would result in Screen Percent.
setMultiboardCharWidth(charset, char:byte(1,-1), 0.8 / numberPlacements)
end
-- Set Char Width for all printable ascii chars in screen width (1920 pixels). Measured on a 80percent screen width multiboard column by counting the number of chars that fit into it.
-- Font size differs by text install language and patch (1.32- vs. 1.33+)
if BlzGetUnitOrderCount then --identifies patch 1.33+
--German font size for patch 1.33+
setMultiboardCharWidthBase80('ger', "a", 144)
setMultiboardCharWidthBase80('ger', "b", 131)
setMultiboardCharWidthBase80('ger', "c", 144)
setMultiboardCharWidthBase80('ger', "d", 120)
setMultiboardCharWidthBase80('ger', "e", 131)
setMultiboardCharWidthBase80('ger', "f", 240)
setMultiboardCharWidthBase80('ger', "g", 120)
setMultiboardCharWidthBase80('ger', "h", 131)
setMultiboardCharWidthBase80('ger', "i", 288)
setMultiboardCharWidthBase80('ger', "j", 288)
setMultiboardCharWidthBase80('ger', "k", 144)
setMultiboardCharWidthBase80('ger', "l", 288)
setMultiboardCharWidthBase80('ger', "m", 85)
setMultiboardCharWidthBase80('ger', "n", 131)
setMultiboardCharWidthBase80('ger', "o", 120)
setMultiboardCharWidthBase80('ger', "p", 120)
setMultiboardCharWidthBase80('ger', "q", 120)
setMultiboardCharWidthBase80('ger', "r", 206)
setMultiboardCharWidthBase80('ger', "s", 160)
setMultiboardCharWidthBase80('ger', "t", 206)
setMultiboardCharWidthBase80('ger', "u", 131)
setMultiboardCharWidthBase80('ger', "v", 131)
setMultiboardCharWidthBase80('ger', "w", 96)
setMultiboardCharWidthBase80('ger', "x", 144)
setMultiboardCharWidthBase80('ger', "y", 131)
setMultiboardCharWidthBase80('ger', "z", 144)
setMultiboardCharWidthBase80('ger', "A", 103)
setMultiboardCharWidthBase80('ger', "B", 120)
setMultiboardCharWidthBase80('ger', "C", 111)
setMultiboardCharWidthBase80('ger', "D", 103)
setMultiboardCharWidthBase80('ger', "E", 144)
setMultiboardCharWidthBase80('ger', "F", 160)
setMultiboardCharWidthBase80('ger', "G", 96)
setMultiboardCharWidthBase80('ger', "H", 96)
setMultiboardCharWidthBase80('ger', "I", 240)
setMultiboardCharWidthBase80('ger', "J", 240)
setMultiboardCharWidthBase80('ger', "K", 120)
setMultiboardCharWidthBase80('ger', "L", 144)
setMultiboardCharWidthBase80('ger', "M", 76)
setMultiboardCharWidthBase80('ger', "N", 96)
setMultiboardCharWidthBase80('ger', "O", 90)
setMultiboardCharWidthBase80('ger', "P", 131)
setMultiboardCharWidthBase80('ger', "Q", 90)
setMultiboardCharWidthBase80('ger', "R", 120)
setMultiboardCharWidthBase80('ger', "S", 131)
setMultiboardCharWidthBase80('ger', "T", 144)
setMultiboardCharWidthBase80('ger', "U", 103)
setMultiboardCharWidthBase80('ger', "V", 120)
setMultiboardCharWidthBase80('ger', "W", 76)
setMultiboardCharWidthBase80('ger', "X", 111)
setMultiboardCharWidthBase80('ger', "Y", 120)
setMultiboardCharWidthBase80('ger', "Z", 120)
setMultiboardCharWidthBase80('ger', "1", 144)
setMultiboardCharWidthBase80('ger', "2", 120)
setMultiboardCharWidthBase80('ger', "3", 120)
setMultiboardCharWidthBase80('ger', "4", 120)
setMultiboardCharWidthBase80('ger', "5", 120)
setMultiboardCharWidthBase80('ger', "6", 120)
setMultiboardCharWidthBase80('ger', "7", 131)
setMultiboardCharWidthBase80('ger', "8", 120)
setMultiboardCharWidthBase80('ger', "9", 120)
setMultiboardCharWidthBase80('ger', "0", 120)
setMultiboardCharWidthBase80('ger', ":", 288)
setMultiboardCharWidthBase80('ger', ";", 288)
setMultiboardCharWidthBase80('ger', ".", 288)
setMultiboardCharWidthBase80('ger', "#", 120)
setMultiboardCharWidthBase80('ger', ",", 288)
setMultiboardCharWidthBase80('ger', " ", 286) --space
setMultiboardCharWidthBase80('ger', "'", 180)
setMultiboardCharWidthBase80('ger', "!", 180)
setMultiboardCharWidthBase80('ger', "$", 131)
setMultiboardCharWidthBase80('ger', "&", 90)
setMultiboardCharWidthBase80('ger', "/", 180)
setMultiboardCharWidthBase80('ger', "(", 240)
setMultiboardCharWidthBase80('ger', ")", 240)
setMultiboardCharWidthBase80('ger', "=", 120)
setMultiboardCharWidthBase80('ger', "?", 144)
setMultiboardCharWidthBase80('ger', "^", 144)
setMultiboardCharWidthBase80('ger', "<", 144)
setMultiboardCharWidthBase80('ger', ">", 144)
setMultiboardCharWidthBase80('ger', "-", 180)
setMultiboardCharWidthBase80('ger', "+", 120)
setMultiboardCharWidthBase80('ger', "*", 180)
setMultiboardCharWidthBase80('ger', "|", 287) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
setMultiboardCharWidthBase80('ger', "~", 111)
setMultiboardCharWidthBase80('ger', "{", 240)
setMultiboardCharWidthBase80('ger', "}", 240)
setMultiboardCharWidthBase80('ger', "[", 240)
setMultiboardCharWidthBase80('ger', "]", 240)
setMultiboardCharWidthBase80('ger', "_", 144)
setMultiboardCharWidthBase80('ger', "\x25", 103) --percent
setMultiboardCharWidthBase80('ger', "\x5C", 205) --backslash
setMultiboardCharWidthBase80('ger', "\x22", 120) --double quotation mark
setMultiboardCharWidthBase80('ger', "\x40", 90) --at sign
setMultiboardCharWidthBase80('ger', "\x60", 144) --Gravis (Accent)
--English font size for patch 1.33+
setMultiboardCharWidthBase80('eng', "a", 144)
setMultiboardCharWidthBase80('eng', "b", 120)
setMultiboardCharWidthBase80('eng', "c", 131)
setMultiboardCharWidthBase80('eng', "d", 120)
setMultiboardCharWidthBase80('eng', "e", 120)
setMultiboardCharWidthBase80('eng', "f", 240)
setMultiboardCharWidthBase80('eng', "g", 120)
setMultiboardCharWidthBase80('eng', "h", 120)
setMultiboardCharWidthBase80('eng', "i", 288)
setMultiboardCharWidthBase80('eng', "j", 288)
setMultiboardCharWidthBase80('eng', "k", 144)
setMultiboardCharWidthBase80('eng', "l", 288)
setMultiboardCharWidthBase80('eng', "m", 80)
setMultiboardCharWidthBase80('eng', "n", 120)
setMultiboardCharWidthBase80('eng', "o", 111)
setMultiboardCharWidthBase80('eng', "p", 111)
setMultiboardCharWidthBase80('eng', "q", 111)
setMultiboardCharWidthBase80('eng', "r", 206)
setMultiboardCharWidthBase80('eng', "s", 160)
setMultiboardCharWidthBase80('eng', "t", 206)
setMultiboardCharWidthBase80('eng', "u", 120)
setMultiboardCharWidthBase80('eng', "v", 144)
setMultiboardCharWidthBase80('eng', "w", 90)
setMultiboardCharWidthBase80('eng', "x", 131)
setMultiboardCharWidthBase80('eng', "y", 144)
setMultiboardCharWidthBase80('eng', "z", 144)
setMultiboardCharWidthBase80('eng', "A", 103)
setMultiboardCharWidthBase80('eng', "B", 120)
setMultiboardCharWidthBase80('eng', "C", 103)
setMultiboardCharWidthBase80('eng', "D", 96)
setMultiboardCharWidthBase80('eng', "E", 131)
setMultiboardCharWidthBase80('eng', "F", 160)
setMultiboardCharWidthBase80('eng', "G", 96)
setMultiboardCharWidthBase80('eng', "H", 90)
setMultiboardCharWidthBase80('eng', "I", 240)
setMultiboardCharWidthBase80('eng', "J", 240)
setMultiboardCharWidthBase80('eng', "K", 120)
setMultiboardCharWidthBase80('eng', "L", 131)
setMultiboardCharWidthBase80('eng', "M", 76)
setMultiboardCharWidthBase80('eng', "N", 90)
setMultiboardCharWidthBase80('eng', "O", 85)
setMultiboardCharWidthBase80('eng', "P", 120)
setMultiboardCharWidthBase80('eng', "Q", 85)
setMultiboardCharWidthBase80('eng', "R", 120)
setMultiboardCharWidthBase80('eng', "S", 131)
setMultiboardCharWidthBase80('eng', "T", 144)
setMultiboardCharWidthBase80('eng', "U", 96)
setMultiboardCharWidthBase80('eng', "V", 120)
setMultiboardCharWidthBase80('eng', "W", 76)
setMultiboardCharWidthBase80('eng', "X", 111)
setMultiboardCharWidthBase80('eng', "Y", 120)
setMultiboardCharWidthBase80('eng', "Z", 111)
setMultiboardCharWidthBase80('eng', "1", 103)
setMultiboardCharWidthBase80('eng', "2", 111)
setMultiboardCharWidthBase80('eng', "3", 111)
setMultiboardCharWidthBase80('eng', "4", 111)
setMultiboardCharWidthBase80('eng', "5", 111)
setMultiboardCharWidthBase80('eng', "6", 111)
setMultiboardCharWidthBase80('eng', "7", 111)
setMultiboardCharWidthBase80('eng', "8", 111)
setMultiboardCharWidthBase80('eng', "9", 111)
setMultiboardCharWidthBase80('eng', "0", 111)
setMultiboardCharWidthBase80('eng', ":", 288)
setMultiboardCharWidthBase80('eng', ";", 288)
setMultiboardCharWidthBase80('eng', ".", 288)
setMultiboardCharWidthBase80('eng', "#", 103)
setMultiboardCharWidthBase80('eng', ",", 288)
setMultiboardCharWidthBase80('eng', " ", 286) --space
setMultiboardCharWidthBase80('eng', "'", 360)
setMultiboardCharWidthBase80('eng', "!", 288)
setMultiboardCharWidthBase80('eng', "$", 131)
setMultiboardCharWidthBase80('eng', "&", 120)
setMultiboardCharWidthBase80('eng', "/", 180)
setMultiboardCharWidthBase80('eng', "(", 206)
setMultiboardCharWidthBase80('eng', ")", 206)
setMultiboardCharWidthBase80('eng', "=", 111)
setMultiboardCharWidthBase80('eng', "?", 180)
setMultiboardCharWidthBase80('eng', "^", 144)
setMultiboardCharWidthBase80('eng', "<", 111)
setMultiboardCharWidthBase80('eng', ">", 111)
setMultiboardCharWidthBase80('eng', "-", 160)
setMultiboardCharWidthBase80('eng', "+", 111)
setMultiboardCharWidthBase80('eng', "*", 144)
setMultiboardCharWidthBase80('eng', "|", 479) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
setMultiboardCharWidthBase80('eng', "~", 144)
setMultiboardCharWidthBase80('eng', "{", 160)
setMultiboardCharWidthBase80('eng', "}", 160)
setMultiboardCharWidthBase80('eng', "[", 206)
setMultiboardCharWidthBase80('eng', "]", 206)
setMultiboardCharWidthBase80('eng', "_", 120)
setMultiboardCharWidthBase80('eng', "\x25", 103) --percent
setMultiboardCharWidthBase80('eng', "\x5C", 180) --backslash
setMultiboardCharWidthBase80('eng', "\x22", 180) --double quotation mark
setMultiboardCharWidthBase80('eng', "\x40", 85) --at sign
setMultiboardCharWidthBase80('eng', "\x60", 206) --Gravis (Accent)
else
--German font size up to patch 1.32
setMultiboardCharWidthBase80('ger', "a", 144)
setMultiboardCharWidthBase80('ger', "b", 144)
setMultiboardCharWidthBase80('ger', "c", 144)
setMultiboardCharWidthBase80('ger', "d", 131)
setMultiboardCharWidthBase80('ger', "e", 144)
setMultiboardCharWidthBase80('ger', "f", 240)
setMultiboardCharWidthBase80('ger', "g", 120)
setMultiboardCharWidthBase80('ger', "h", 144)
setMultiboardCharWidthBase80('ger', "i", 360)
setMultiboardCharWidthBase80('ger', "j", 288)
setMultiboardCharWidthBase80('ger', "k", 144)
setMultiboardCharWidthBase80('ger', "l", 360)
setMultiboardCharWidthBase80('ger', "m", 90)
setMultiboardCharWidthBase80('ger', "n", 144)
setMultiboardCharWidthBase80('ger', "o", 131)
setMultiboardCharWidthBase80('ger', "p", 131)
setMultiboardCharWidthBase80('ger', "q", 131)
setMultiboardCharWidthBase80('ger', "r", 206)
setMultiboardCharWidthBase80('ger', "s", 180)
setMultiboardCharWidthBase80('ger', "t", 206)
setMultiboardCharWidthBase80('ger', "u", 144)
setMultiboardCharWidthBase80('ger', "v", 131)
setMultiboardCharWidthBase80('ger', "w", 96)
setMultiboardCharWidthBase80('ger', "x", 144)
setMultiboardCharWidthBase80('ger', "y", 131)
setMultiboardCharWidthBase80('ger', "z", 144)
setMultiboardCharWidthBase80('ger', "A", 103)
setMultiboardCharWidthBase80('ger', "B", 131)
setMultiboardCharWidthBase80('ger', "C", 120)
setMultiboardCharWidthBase80('ger', "D", 111)
setMultiboardCharWidthBase80('ger', "E", 144)
setMultiboardCharWidthBase80('ger', "F", 180)
setMultiboardCharWidthBase80('ger', "G", 103)
setMultiboardCharWidthBase80('ger', "H", 103)
setMultiboardCharWidthBase80('ger', "I", 288)
setMultiboardCharWidthBase80('ger', "J", 240)
setMultiboardCharWidthBase80('ger', "K", 120)
setMultiboardCharWidthBase80('ger', "L", 144)
setMultiboardCharWidthBase80('ger', "M", 80)
setMultiboardCharWidthBase80('ger', "N", 103)
setMultiboardCharWidthBase80('ger', "O", 96)
setMultiboardCharWidthBase80('ger', "P", 144)
setMultiboardCharWidthBase80('ger', "Q", 90)
setMultiboardCharWidthBase80('ger', "R", 120)
setMultiboardCharWidthBase80('ger', "S", 144)
setMultiboardCharWidthBase80('ger', "T", 144)
setMultiboardCharWidthBase80('ger', "U", 111)
setMultiboardCharWidthBase80('ger', "V", 120)
setMultiboardCharWidthBase80('ger', "W", 76)
setMultiboardCharWidthBase80('ger', "X", 111)
setMultiboardCharWidthBase80('ger', "Y", 120)
setMultiboardCharWidthBase80('ger', "Z", 120)
setMultiboardCharWidthBase80('ger', "1", 288)
setMultiboardCharWidthBase80('ger', "2", 131)
setMultiboardCharWidthBase80('ger', "3", 144)
setMultiboardCharWidthBase80('ger', "4", 120)
setMultiboardCharWidthBase80('ger', "5", 144)
setMultiboardCharWidthBase80('ger', "6", 131)
setMultiboardCharWidthBase80('ger', "7", 144)
setMultiboardCharWidthBase80('ger', "8", 131)
setMultiboardCharWidthBase80('ger', "9", 131)
setMultiboardCharWidthBase80('ger', "0", 131)
setMultiboardCharWidthBase80('ger', ":", 480)
setMultiboardCharWidthBase80('ger', ";", 360)
setMultiboardCharWidthBase80('ger', ".", 480)
setMultiboardCharWidthBase80('ger', "#", 120)
setMultiboardCharWidthBase80('ger', ",", 360)
setMultiboardCharWidthBase80('ger', " ", 288) --space
setMultiboardCharWidthBase80('ger', "'", 480)
setMultiboardCharWidthBase80('ger', "!", 360)
setMultiboardCharWidthBase80('ger', "$", 160)
setMultiboardCharWidthBase80('ger', "&", 96)
setMultiboardCharWidthBase80('ger', "/", 180)
setMultiboardCharWidthBase80('ger', "(", 288)
setMultiboardCharWidthBase80('ger', ")", 288)
setMultiboardCharWidthBase80('ger', "=", 160)
setMultiboardCharWidthBase80('ger', "?", 180)
setMultiboardCharWidthBase80('ger', "^", 144)
setMultiboardCharWidthBase80('ger', "<", 160)
setMultiboardCharWidthBase80('ger', ">", 160)
setMultiboardCharWidthBase80('ger', "-", 144)
setMultiboardCharWidthBase80('ger', "+", 160)
setMultiboardCharWidthBase80('ger', "*", 206)
setMultiboardCharWidthBase80('ger', "|", 480) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
setMultiboardCharWidthBase80('ger', "~", 144)
setMultiboardCharWidthBase80('ger', "{", 240)
setMultiboardCharWidthBase80('ger', "}", 240)
setMultiboardCharWidthBase80('ger', "[", 240)
setMultiboardCharWidthBase80('ger', "]", 288)
setMultiboardCharWidthBase80('ger', "_", 144)
setMultiboardCharWidthBase80('ger', "\x25", 111) --percent
setMultiboardCharWidthBase80('ger', "\x5C", 206) --backslash
setMultiboardCharWidthBase80('ger', "\x22", 240) --double quotation mark
setMultiboardCharWidthBase80('ger', "\x40", 103) --at sign
setMultiboardCharWidthBase80('ger', "\x60", 240) --Gravis (Accent)
--English Font size up to patch 1.32
setMultiboardCharWidthBase80('eng', "a", 144)
setMultiboardCharWidthBase80('eng', "b", 120)
setMultiboardCharWidthBase80('eng', "c", 131)
setMultiboardCharWidthBase80('eng', "d", 120)
setMultiboardCharWidthBase80('eng', "e", 131)
setMultiboardCharWidthBase80('eng', "f", 240)
setMultiboardCharWidthBase80('eng', "g", 120)
setMultiboardCharWidthBase80('eng', "h", 131)
setMultiboardCharWidthBase80('eng', "i", 360)
setMultiboardCharWidthBase80('eng', "j", 288)
setMultiboardCharWidthBase80('eng', "k", 144)
setMultiboardCharWidthBase80('eng', "l", 360)
setMultiboardCharWidthBase80('eng', "m", 80)
setMultiboardCharWidthBase80('eng', "n", 131)
setMultiboardCharWidthBase80('eng', "o", 120)
setMultiboardCharWidthBase80('eng', "p", 120)
setMultiboardCharWidthBase80('eng', "q", 120)
setMultiboardCharWidthBase80('eng', "r", 206)
setMultiboardCharWidthBase80('eng', "s", 160)
setMultiboardCharWidthBase80('eng', "t", 206)
setMultiboardCharWidthBase80('eng', "u", 131)
setMultiboardCharWidthBase80('eng', "v", 144)
setMultiboardCharWidthBase80('eng', "w", 90)
setMultiboardCharWidthBase80('eng', "x", 131)
setMultiboardCharWidthBase80('eng', "y", 144)
setMultiboardCharWidthBase80('eng', "z", 144)
setMultiboardCharWidthBase80('eng', "A", 103)
setMultiboardCharWidthBase80('eng', "B", 120)
setMultiboardCharWidthBase80('eng', "C", 103)
setMultiboardCharWidthBase80('eng', "D", 103)
setMultiboardCharWidthBase80('eng', "E", 131)
setMultiboardCharWidthBase80('eng', "F", 160)
setMultiboardCharWidthBase80('eng', "G", 103)
setMultiboardCharWidthBase80('eng', "H", 96)
setMultiboardCharWidthBase80('eng', "I", 288)
setMultiboardCharWidthBase80('eng', "J", 240)
setMultiboardCharWidthBase80('eng', "K", 120)
setMultiboardCharWidthBase80('eng', "L", 131)
setMultiboardCharWidthBase80('eng', "M", 76)
setMultiboardCharWidthBase80('eng', "N", 96)
setMultiboardCharWidthBase80('eng', "O", 85)
setMultiboardCharWidthBase80('eng', "P", 131)
setMultiboardCharWidthBase80('eng', "Q", 85)
setMultiboardCharWidthBase80('eng', "R", 120)
setMultiboardCharWidthBase80('eng', "S", 131)
setMultiboardCharWidthBase80('eng', "T", 144)
setMultiboardCharWidthBase80('eng', "U", 103)
setMultiboardCharWidthBase80('eng', "V", 120)
setMultiboardCharWidthBase80('eng', "W", 76)
setMultiboardCharWidthBase80('eng', "X", 111)
setMultiboardCharWidthBase80('eng', "Y", 120)
setMultiboardCharWidthBase80('eng', "Z", 111)
setMultiboardCharWidthBase80('eng', "1", 206)
setMultiboardCharWidthBase80('eng', "2", 131)
setMultiboardCharWidthBase80('eng', "3", 131)
setMultiboardCharWidthBase80('eng', "4", 111)
setMultiboardCharWidthBase80('eng', "5", 131)
setMultiboardCharWidthBase80('eng', "6", 120)
setMultiboardCharWidthBase80('eng', "7", 131)
setMultiboardCharWidthBase80('eng', "8", 111)
setMultiboardCharWidthBase80('eng', "9", 120)
setMultiboardCharWidthBase80('eng', "0", 111)
setMultiboardCharWidthBase80('eng', ":", 360)
setMultiboardCharWidthBase80('eng', ";", 360)
setMultiboardCharWidthBase80('eng', ".", 360)
setMultiboardCharWidthBase80('eng', "#", 103)
setMultiboardCharWidthBase80('eng', ",", 360)
setMultiboardCharWidthBase80('eng', " ", 288) --space
setMultiboardCharWidthBase80('eng', "'", 480)
setMultiboardCharWidthBase80('eng', "!", 360)
setMultiboardCharWidthBase80('eng', "$", 131)
setMultiboardCharWidthBase80('eng', "&", 120)
setMultiboardCharWidthBase80('eng', "/", 180)
setMultiboardCharWidthBase80('eng', "(", 240)
setMultiboardCharWidthBase80('eng', ")", 240)
setMultiboardCharWidthBase80('eng', "=", 111)
setMultiboardCharWidthBase80('eng', "?", 180)
setMultiboardCharWidthBase80('eng', "^", 144)
setMultiboardCharWidthBase80('eng', "<", 131)
setMultiboardCharWidthBase80('eng', ">", 131)
setMultiboardCharWidthBase80('eng', "-", 180)
setMultiboardCharWidthBase80('eng', "+", 111)
setMultiboardCharWidthBase80('eng', "*", 180)
setMultiboardCharWidthBase80('eng', "|", 480) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
setMultiboardCharWidthBase80('eng', "~", 144)
setMultiboardCharWidthBase80('eng', "{", 240)
setMultiboardCharWidthBase80('eng', "}", 240)
setMultiboardCharWidthBase80('eng', "[", 240)
setMultiboardCharWidthBase80('eng', "]", 240)
setMultiboardCharWidthBase80('eng', "_", 120)
setMultiboardCharWidthBase80('eng', "\x25", 103) --percent
setMultiboardCharWidthBase80('eng', "\x5C", 180) --backslash
setMultiboardCharWidthBase80('eng', "\x22", 206) --double quotation mark
setMultiboardCharWidthBase80('eng', "\x40", 96) --at sign
setMultiboardCharWidthBase80('eng', "\x60", 206) --Gravis (Accent)
end
end
if Debug and Debug.endFile then Debug.endFile() end
if Debug and Debug.beginFile then Debug.beginFile('MLNL_Config') end
--[[
Friendly configuration file used by the MagiLogNLoad system.
]]
do
MagiLogNLoad = {configVersion = 1100};
MagiLogNLoad.SAVE_FOLDER_PATH = 'MagiLogNLoad/'; -- needs to end with / if you want a folder
-- Necessary abilities for the system to operate correctly.
-- This can be any ability. Used by FileIO.
MagiLogNLoad.FILEIO_ABIL = FourCC('AZIO');
-- Needs to be a copy of the 'Agho' ability.
-- Necessary for loading into transports.
MagiLogNLoad.LOAD_GHOST_ID = FourCC('AFFZ');
-- Needs to be a copy of the 'Adri' ability.
-- Necessary for unloading units.
MagiLogNLoad.UNLOAD_INSTA_ID = FourCC('AUNL');
-- List all <Cargo Hold> and <Load> ability ids in your map so that transports can be saved and loaded properly.
MagiLogNLoad.ALL_TRANSP_ABILS = {
LOAD = {FourCC('Aloa'), FourCC('Sloa'), FourCC('Slo2'), FourCC('Slo3'), FourCC('Aenc')},
CARGO = {FourCC('Sch3'), FourCC('Sch4'), FourCC('Sch5'), FourCC('Achd'), FourCC('Abun'), FourCC('Achl')}
};
-- Enforceable limits. Recommended for multiplayer maps without a tight-knit community.
MagiLogNLoad.LOADING_TIME_SECONDS_TO_WARNING = 30; -- use 0 or -1 to disable it.
MagiLogNLoad.LOADING_TIME_SECONDS_TO_ABORT = 120; -- use 0 or -1 to disable it.
MagiLogNLoad.MAX_LOADS_PER_PLAYER = 10; -- amount of times a player can load a file.
-- Save-files that exceed these parameters will be truncated.
MagiLogNLoad.MAX_TERRAIN_PER_FILE = 10240;
-- Read MAX_DESTRS_PER_FILE as: the max amount of destructables recorded in a save-file.
MagiLogNLoad.MAX_DESTRS_PER_FILE = 5120;
-- Read MAX_ENTRIES_PER_DESTR as: the max amount of properties each destr can record in a save-file.
MagiLogNLoad.MAX_ENTRIES_PER_DESTR = 160;
MagiLogNLoad.MAX_RESEARCH_PER_FILE = 5120;
-- Most saved research contain only a couple of entries, so we dont need the extra config.
-- Just extrapolate from the MAX_DESTRS_PER_FILE and MAX_ENTRIES_PER_DESTR explanations.
MagiLogNLoad.MAX_UNITS_PER_FILE = 5120;
MagiLogNLoad.MAX_ENTRIES_PER_UNIT = 1280;
MagiLogNLoad.MAX_ITEMS_PER_FILE = 2560;
MagiLogNLoad.MAX_ENTRIES_PER_ITEM = 80;
-- Some data-types are not grouped by instance and instead spread out as sequences of entries across the save-file.
MagiLogNLoad.MAX_HASHTABLE_ENTRIES_PER_FILE = 5120;
MagiLogNLoad.MAX_VARIABLE_ENTRIES_PER_FILE = 5120;
MagiLogNLoad.MAX_PROXYTABLE_ENTRIES_PER_FILE = 2560;
MagiLogNLoad.MAX_EXTRAS_PER_FILE = 2560;
-- Allows the system to replace type ids in the save-file with new ones.
-- Use this to support save-files created in older versions of your map that might
-- not have the same stuff as the new versions.
MagiLogNLoad.oldTypeId2NewTypeId = {
--['hpea'] = 'hkni',
--['hfoo'] = 'hmkg',
--['hsor'] = 'harc'
};
-- These strings are used to configure the modes when using GUI to initialize the system.
-- Please refer to the provided test map GUI triggers for an example of how it works.
MagiLogNLoad.MODE_GUI_STRS = {
-- Enable Debug prints. Very recommended.
DEBUG = 'DEBUG',
-- Save units created at runtime if they are owned by the saving player.
-- Units will be saved with their items, unless the items are pre-placed and SAVE_PREPLACED_ITEMS is disabled.
-- Does not save summoned or dead units.
SAVE_NEW_UNITS_OF_PLAYER = 'SAVE_NEW_UNITS_OF_PLAYER',
-- Save changes to pre-placed units. Might lead to conflicts in multiplayer.
-- Defining a logging player can be done through MagiLogNLoad.SetLoggingPlayer or mlnl_LoggingPlayer.
SAVE_PREPLACED_UNITS_OF_PLAYER = 'SAVE_PREPLACED_UNITS_OF_PLAYER',
-- Save destruction/removal of pre-placed units. Might lead to conflicts in multiplayer.
-- [IMPORTANT] Destruction/removals are only saved if a logging player is defined when they happen.
-- Defining a logging player can be done through MagiLogNLoad.SetLoggingPlayer or mlnl_LoggingPlayer.
SAVE_DESTROYED_PREPLACED_UNITS = 'SAVE_DESTROYED_PREPLACED_UNITS',
-- Save changes to pre-placed items, even if units are holding them. Might lead to conflicts in multiplayer.
-- If the item is destroyed or removed, its destruction will be recorded and reproduced when loaded.
-- [IMPORTANT] Changes are only saved if a logging player is defined when the change happens.
-- Defining a logging player can be done through MagiLogNLoad.SetLoggingPlayer or mlnl_LoggingPlayer.
SAVE_PREPLACED_ITEMS = 'SAVE_PREPLACED_ITEMS',
-- Save changes to pre-placed trees/gates/destructables.
-- [IMPORTANT] AS A SPECIAL CASE, this mode saves destructables destroyed or removed
-- > by explicit calls to Kill/RemoveDestructable().
-- Might lead to conflicts in multiplayer.
-- [IMPORTANT] Changes are only saved if a logging player is defined when the change happens.
-- Defining a logging player can be done through MagiLogNLoad.SetLoggingPlayer or mlnl_LoggingPlayer.
SAVE_STANDING_PREPLACED_DESTRS = 'SAVE_STANDING_PREPLACED_DESTRS',
-- Save the destruction/removal of pre-placed trees/gates/destructables.
-- Might lead to conflicts in multiplayer.
-- If the map has too many destructables, the performance will suffer.
-- [IMPORTANT] Destruction/Removals are only saved if a logging player is defined when they happen.
-- Defining a logging player can be done through MagiLogNLoad.SetLoggingPlayer or mlnl_LoggingPlayer.
SAVE_DESTROYED_PREPLACED_DESTRS = 'SAVE_DESTROYED_PREPLACED_DESTRS',
-- Save units disowned by a player if the owner at the time of saving is one of the Neutral players.
SAVE_UNITS_DISOWNED_BY_PLAYERS = 'SAVE_UNITS_DISOWNED_BY_PLAYERS',
-- Save items on the ground only if a player's unit dropped them.
-- Won't save items dropped by creeps or Neutral player's units.
SAVE_ITEMS_DROPPED_MANUALLY = 'SAVE_ITEMS_DROPPED_MANUALLY',
-- Adds all items on the ground to the saving player's save-file.
SAVE_ALL_ITEMS_ON_GROUND = 'SAVE_ALL_ITEMS_ON_GROUND',
};
MagiLogNLoad.SAVE_CMD_PREFIX = '-save'; -- use <nil> or an empty string to disable the command.
MagiLogNLoad.LOAD_CMD_PREFIX = '-load'; -- use <nil> or an empty string to disable the command.
-- Async. Doing cursed things here will curse your map.
MagiLogNLoad.onMaxLoadsError = function(_player)
print('|cffff5500MLNL Error! You have already loaded too many save-files this game!|r');
end
-- Async. Doing cursed things here will curse your map.
MagiLogNLoad.onAlreadyLoadedWarning = function(_player, _fileName)
print('|cffff9900MLNL Warning! You have already loaded this save-file this game!|r')
print('|cffff9900Any changes made might not load correctly until the map is restarted!|r');
end
-- Sync.
---@return boolean @if false then it interrupts the saving
MagiLogNLoad.onSaveStarted = function(_player, _fileName)
if _player == GetLocalPlayer() then
print('|cff00fa33Saving file <|r', _fileName, '|cff00fa33>...|r');
end
return true;
end
-- Async. Doing cursed things here will curse your map.
MagiLogNLoad.onSaveFinished = function(_player, _fileName)
print('|cff00fa33File <|r', _fileName, '|cff00fa33> saved successfully!|r');
MagiLogNLoad.willPrintTallyNext = true;
end
-- Sync.
---@return boolean @if false then it interrupts the loading
MagiLogNLoad.onLoadStarted = function(_player)
print('|cffffdd00', GetPlayerName(_player), 'has started loading a save-file. Please wait!|r');
return true;
end
-- Sync.
MagiLogNLoad.onLoadFinished = function(_player, _fileName)
print('|cff00fa33', GetPlayerName(_player), 'has loaded their save-file successfully!|r');
MagiLogNLoad.willPrintTallyNext = true;
end
-- Sync.
---@param _progress integer @value within [0,100]
---@param _checkpointTracker table @Tracks if a checkpoint has been reached
MagiLogNLoad.onSyncProgressCheckpoint = function(_progress, _checkpointTracker)
if not _checkpointTracker[75] and _progress >= 75 then
_checkpointTracker[75] = true;
print('|cffffff33Loading is','75\x25','done. Please wait!|r');
elseif not _checkpointTracker[50] and _progress >= 50 then
_checkpointTracker[50] = true;
print('|cffffff33Loading is','50\x25','done. Please wait!|r');
elseif not _checkpointTracker[25] and _progress >= 25 then
_checkpointTracker[25] = true;
print('|cffffff33Loading is','25\x25','done. Please wait!|r');
end
end
--[[
MagiLogNLoad.onSaveStarted = nil;
MagiLogNLoad.onSaveFinished = nil;
MagiLogNLoad.onLoadStarted = nil;
MagiLogNLoad.onLoadFinished = nil;
MagiLogNLoad.onSyncProgressCheckpoint = nil;
MagiLogNLoad.onAlreadyLoadedWarning = nil;
MagiLogNLoad.onMaxLoadsError = nil;
]]
end
if Debug and Debug.endFile then Debug.endFile() end
if Debug and Debug.beginFile then Debug.beginFile('FileIO') end
-- Adaptation of Trokkin @ HiveWorkshop.com's FileIO, by ModdieMads @ HiveWorkshop.com
do
FileIO = {};
local RAW_PREFIX = ']]i([['
local RAW_SUFFIX = ']])--[['
local RAW_SIZE = 256 - #RAW_PREFIX - #RAW_SUFFIX
local FILEIO_ABIL = MagiLogNLoad.FILEIO_ABIL or FourCC('ANdc');
local LOAD_EMPTY_KEY = 'empty'
local lastFileName = nil;
local function OpenFile(fileName)
lastFileName = fileName;
PreloadGenClear();
Preload('")\nendfunction\n//!beginusercode\nlocal p={} local i=function(s) table.insert(p,s) end--[[');
end
local function WriteFile(str)
for i = 1, #str, RAW_SIZE do
Preload(RAW_PREFIX .. str:sub(i, i + RAW_SIZE - 1) .. RAW_SUFFIX);
end
end
local function CloseFile()
Preload(']]BlzSetAbilityTooltip(' .. FILEIO_ABIL .. ', table.concat(p), 0)\n//!endusercode\nfunction a takes nothing returns nothing\n//');
PreloadGenEnd(lastFileName);
lastFileName = nil;
end
---@param fileName string
---@return string?
function FileIO.LoadFile(fileName)
BlzSetAbilityTooltip(FILEIO_ABIL, LOAD_EMPTY_KEY, 0);
Preloader(fileName);
local loaded = BlzGetAbilityTooltip(FILEIO_ABIL, 0);
if loaded == LOAD_EMPTY_KEY then
return nil;
end
return loaded;
end
---@param fileName string
---@param data string
---@param onFail function?
---@return boolean
function FileIO.SaveFile(fileName, data, onFail)
OpenFile(fileName);
WriteFile(data);
CloseFile();
end
end
if Debug and Debug.endFile then Debug.endFile() end
if Debug and Debug.beginFile then Debug.beginFile('LibDeflate') end
--[[
Adaptation of LibDeflate v1.08
(C) ModdieMads @ https://www.hiveworkshop.com/members/moddiemads.310879/
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
LibDeflate
Pure Lua compressor and decompressor with high compression ratio using
DEFLATE/zlib format.
(C) 2018-2021 Haoqian He
zlib License
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
License History:
1. GNU General Public License Version 3 in v1.0.0 and earlier versions.
2. GNU Lesser General Public License Version 3 in v1.0.1
3. the zlib License since v1.0.2
]]
do
LibDeflate = {}
-- localize Lua api for faster access.
-- this is highly unecessary, but here we go...
local assert = assert
local error = error
local pairs = pairs
local string_byte = string.byte
local string_char = string.char
local string_sub = string.sub
local table_concat = table.concat
local table_sort = table.sort
local math_fmod = math.fmod;
local fmod = function(v,mod) return (math_fmod(v,mod) + .5)//1 end;
-- Converts i to 2^i
-- This is used to implement bit left shift and bit right shift.
-- "x << y" in C: "x*_pow2[y]" in Lua
local _pow2 = {}
-- Converts any byte to a character, (0<=byte<=255)
local _byte_to_char = {}
-- _reverseBitsTbl[len][val] stores the bit reverse of
-- the number with bit length "len" and value "val"
-- For example, decimal number 6 with bits length 5 is binary 00110
-- It's reverse is binary 01100,
-- which is decimal 12 and 12 == _reverseBitsTbl[5][6]
-- 1<=len<=9, 0<=val<=2^len-1
-- The reason for 1<=len<=9 is that the max of min bitlen of huffman code
-- of a huffman alphabet is 9?
local _reverse_bits_tbl = {}
-- Convert a LZ77 length (3<=len<=258) to
-- a deflate literal,LZ77_length code (257<=code<=285)
local _length_to_deflate_code = {}
-- convert a LZ77 length (3<=len<=258) to
-- a deflate literal,LZ77_length code extra bits.
local _length_to_deflate_extra_bits = {}
-- Convert a LZ77 length (3<=len<=258) to
-- a deflate literal,LZ77_length code extra bit length.
local _length_to_deflate_extra_bitlen = {}
-- Convert a small LZ77 distance (1<=dist<=256) to a deflate code.
local _dist256_to_deflate_code = {}
-- Convert a small LZ77 distance (1<=dist<=256) to
-- a deflate distance code extra bits.
local _dist256_to_deflate_extra_bits = {}
-- Convert a small LZ77 distance (1<=dist<=256) to
-- a deflate distance code extra bit length.
local _dist256_to_deflate_extra_bitlen = {}
-- Convert a literal,LZ77_length deflate code to LZ77 base length
-- The key of the table is (code - 256), 257<=code<=285
local _literal_deflate_code_to_base_len =
{
3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67,
83, 99, 115, 131, 163, 195, 227, 258
}
-- Convert a literal,LZ77_length deflate code to base LZ77 length extra bits
-- The key of the table is (code - 256), 257<=code<=285
local _literal_deflate_code_to_extra_bitlen =
{
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5,
5, 5, 5, 0
}
-- Convert a distance deflate code to base LZ77 distance. (0<=code<=29)
local _dist_deflate_code_to_base_dist = {
[0] = 1,
2,
3,
4,
5,
7,
9,
13,
17,
25,
33,
49,
65,
97,
129,
193,
257,
385,
513,
769,
1025,
1537,
2049,
3073,
4097,
6145,
8193,
12289,
16385,
24577
}
-- Convert a distance deflate code to LZ77 bits length. (0<=code<=29)
local _dist_deflate_code_to_extra_bitlen =
{
[0] = 0,
0,
0,
0,
1,
1,
2,
2,
3,
3,
4,
4,
5,
5,
6,
6,
7,
7,
8,
8,
9,
9,
10,
10,
11,
11,
12,
12,
13,
13
}
-- The code order of the first huffman header in the dynamic deflate block.
-- See the page 12 of RFC1951
local _rle_codes_huffman_bitlen_order = {
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
}
-- The following tables are used by fixed deflate block.
-- The value of these tables are assigned at the bottom of the source.
-- The huffman code of the literal,LZ77_length deflate codes,
-- in fixed deflate block.
local _fix_block_literal_huffman_code
-- Convert huffman code of the literal,LZ77_length to deflate codes,
-- in fixed deflate block.
local _fix_block_literal_huffman_to_deflate_code
-- The bit length of the huffman code of literal,LZ77_length deflate codes,
-- in fixed deflate block.
local _fix_block_literal_huffman_bitlen
-- The count of each bit length of the literal,LZ77_length deflate codes,
-- in fixed deflate block.
local _fix_block_literal_huffman_bitlen_count
-- The huffman code of the distance deflate codes,
-- in fixed deflate block.
local _fix_block_dist_huffman_code
-- Convert huffman code of the distance to deflate codes,
-- in fixed deflate block.
local _fix_block_dist_huffman_to_deflate_code
-- The bit length of the huffman code of the distance deflate codes,
-- in fixed deflate block.
local _fix_block_dist_huffman_bitlen
-- The count of each bit length of the huffman code of
-- the distance deflate codes,
-- in fixed deflate block.
local _fix_block_dist_huffman_bitlen_count
--- Calculate the Adler-32 checksum of the string. <br>
-- definition of Adler-32 checksum.
-- @param str [string] the input string to calcuate its Adler-32 checksum.
-- @return [integer] The Adler-32 checksum, which is greater or equal to 0,
-- and less than 2^32 (4294967296).
function LibDeflate.Adler32(str)
-- This function is loop unrolled by better performance.
--
-- Here is the minimum code:
local strlen = #str
local i = 1
local a = 1
local b = 0
while i <= strlen - 15 do
local x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16 = string_byte(str, i, i + 15)
b = fmod(b + 16 * a + 16 * x1 + 15 * x2 + 14 * x3 + 13 * x4 + 12 * x5 + 11 * x6 +
10 * x7 + 9 * x8 + 8 * x9 + 7 * x10 + 6 * x11 + 5 * x12 + 4 * x13 + 3 *
x14 + 2 * x15 + x16, 65521);
a = fmod(a + x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 + x10 + x11 + x12 + x13 +
x14 + x15 + x16, 65521);
i = i + 16
end
while (i <= strlen) do
local x = string_byte(str, i, i)
a = fmod((a + x), 65521)
b = fmod((b + a), 65521)
i = i + 1
end
return fmod((b * 65536 + a), 4294967296)
end
-- Compare adler32 checksum.
-- adler32 should be compared with a mod to avoid sign problem
-- 4072834167 (unsigned) is the same adler32 as -222133129
function LibDeflate.IsEqualAdler32(actual, expected)
return fmod(actual, 4294967296) == fmod(expected, 4294967296)
end
local _compression_level_configs = {
[0] = {false, nil, 0, 0, 0}, -- level 0, no compression
[1] = {false, nil, 4, 8, 4}, -- level 1, similar to zlib level 1
[2] = {false, nil, 5, 18, 8}, -- level 2, similar to zlib level 2
[3] = {false, nil, 6, 32, 32}, -- level 3, similar to zlib level 3
[4] = {true, 4, 4, 16, 16}, -- level 4, similar to zlib level 4
[5] = {true, 8, 16, 32, 32}, -- level 5, similar to zlib level 5
[6] = {true, 8, 16, 128, 128}, -- level 6, similar to zlib level 6
[7] = {true, 8, 32, 128, 256}, -- (SLOW) level 7, similar to zlib level 7
[8] = {true, 32, 128, 258, 1024}, -- (SLOW) level 8,similar to zlib level 8
[9] = {true, 32, 258, 258, 4096}
-- (VERY SLOW) level 9, similar to zlib level 9
}
-- partial flush to save memory
local _FLUSH_MODE_MEMORY_CLEANUP = 0
-- full flush with partial bytes
local _FLUSH_MODE_OUTPUT = 1
-- write bytes to get to byte boundary
local _FLUSH_MODE_BYTE_BOUNDARY = 2
-- no flush, just get num of bits written so far
local _FLUSH_MODE_NO_FLUSH = 3
--[[
Create an empty writer to easily write stuffs as the unit of bits.
Return values:
1. WriteBits(code, bitlen):
2. WriteString(str):
3. Flush(mode):
--]]
local function CreateWriter()
local buffer_size = 0
local cache = 0
local cache_bitlen = 0
local total_bitlen = 0
local buffer = {}
-- When buffer is big enough, flush into result_buffer to save memory.
local result_buffer = {}
-- Write bits with value "value" and bit length of "bitlen" into writer.
-- @param value: The value being written
-- @param bitlen: The bit length of "value"
-- @return nil
local function WriteBits(value, bitlen)
cache = cache + value * _pow2[cache_bitlen]
cache_bitlen = cache_bitlen + bitlen
total_bitlen = total_bitlen + bitlen
-- Only bulk to buffer every 4 bytes. This is quicker.
if cache_bitlen >= 16 then
buffer_size = buffer_size + 1
buffer[buffer_size] = _byte_to_char[cache&255] .. _byte_to_char[(((cache - (cache&255))) >> 8)&255]
local rshift_mask = _pow2[16 - cache_bitlen + bitlen]
cache = (value - (value&(rshift_mask-1))) >> (16 - cache_bitlen + bitlen)
cache_bitlen = cache_bitlen - 16
end
end
-- Write the entire string into the writer.
-- @param str The string being written
-- @return nil
local function WriteString(str)
for _ = 1, cache_bitlen, 8 do
buffer_size = buffer_size + 1
buffer[buffer_size] = string_char((cache&255))
cache = (cache - (cache&255)) >> 8;
end
cache_bitlen = 0
buffer_size = buffer_size + 1
buffer[buffer_size] = str
total_bitlen = total_bitlen + #str * 8
end
-- Flush current stuffs in the writer and return it.
-- This operation will free most of the memory.
-- @param mode See the descrtion of the constant and the source code.
-- @return The total number of bits stored in the writer right now.
-- for byte boundary mode, it includes the padding bits.
-- for output mode, it does not include padding bits.
-- @return Return the outputs if mode is output.
local function FlushWriter(mode)
if mode == _FLUSH_MODE_NO_FLUSH then return total_bitlen end
if mode == _FLUSH_MODE_OUTPUT or mode == _FLUSH_MODE_BYTE_BOUNDARY then
-- Full flush, also output cache.
-- Need to pad some bits if cache_bitlen is not multiple of 8.
local padding_bitlen = ((8 - (cache_bitlen&7))&7)
if cache_bitlen > 0 then
-- padding with all 1 bits, mainly because "\000" is not
-- good to be tranmitted. I do this so "\000" is a little bit
-- less frequent.
cache = cache - _pow2[cache_bitlen] + _pow2[cache_bitlen + padding_bitlen]
for _ = 1, cache_bitlen, 8 do
buffer_size = buffer_size + 1
buffer[buffer_size] = _byte_to_char[(cache&255)];
cache = (cache - (cache&255)) >> 8;
end
cache = 0
cache_bitlen = 0
end
if mode == _FLUSH_MODE_BYTE_BOUNDARY then
total_bitlen = total_bitlen + padding_bitlen
return total_bitlen
end
end
local flushed = table_concat(buffer)
buffer = {}
buffer_size = 0
result_buffer[#result_buffer + 1] = flushed
if mode == _FLUSH_MODE_MEMORY_CLEANUP then
return total_bitlen
else
return total_bitlen, table_concat(result_buffer)
end
end
return WriteBits, WriteString, FlushWriter
end
-- Push an element into a max heap
-- @param heap A max heap whose max element is at index 1.
-- @param e The element to be pushed. Assume element "e" is a table
-- and comparison is done via its first entry e[1]
-- @param heap_size current number of elements in the heap.
-- NOTE: There may be some garbage stored in
-- heap[heap_size+1], heap[heap_size+2], etc..
-- @return nil
local function MinHeapPush(heap, e, heap_size)
heap_size = heap_size + 1
heap[heap_size] = e
local value = e[1]
local pos = heap_size
local parent_pos = (pos - (pos&1)) >> 1
while (parent_pos >= 1 and heap[parent_pos][1] > value) do
local t = heap[parent_pos]
heap[parent_pos] = e
heap[pos] = t
pos = parent_pos
parent_pos = (parent_pos - (parent_pos&1)) >> 1
end
end
-- Pop an element from a max heap
-- @param heap A max heap whose max element is at index 1.
-- @param heap_size current number of elements in the heap.
-- @return the poped element
-- Note: This function does not change table size of "heap" to save CPU time.
local function MinHeapPop(heap, heap_size)
local top = heap[1]
local e = heap[heap_size]
local value = e[1]
heap[1] = e
heap[heap_size] = top
heap_size = heap_size - 1
local pos = 1
local left_child_pos = pos * 2
local right_child_pos = left_child_pos + 1
while (left_child_pos <= heap_size) do
local left_child = heap[left_child_pos]
if (right_child_pos <= heap_size and heap[right_child_pos][1] < left_child[1]) then
local right_child = heap[right_child_pos]
if right_child[1] < value then
heap[right_child_pos] = e
heap[pos] = right_child
pos = right_child_pos
left_child_pos = pos * 2
right_child_pos = left_child_pos + 1
else
break
end
else
if left_child[1] < value then
heap[left_child_pos] = e
heap[pos] = left_child
pos = left_child_pos
left_child_pos = pos * 2
right_child_pos = left_child_pos + 1
else
break
end
end
end
return top
end
-- Deflate defines a special huffman tree, which is unique once the bit length
-- of huffman code of all symbols are known.
-- @param bitlen_count Number of symbols with a specific bitlen
-- @param symbol_bitlen The bit length of a symbol
-- @param max_symbol The max symbol among all symbols,
-- which is (number of symbols - 1)
-- @param max_bitlen The max huffman bit length among all symbols.
-- @return The huffman code of all symbols.
local function GetHuffmanCodeFromBitlen(bitlen_counts, symbol_bitlens, max_symbol, max_bitlen)
local huffman_code = 0
local next_codes = {}
local symbol_huffman_codes = {}
for bitlen = 1, max_bitlen do
huffman_code = (huffman_code + (bitlen_counts[bitlen - 1] or 0)) * 2
next_codes[bitlen] = huffman_code
end
for symbol = 0, max_symbol do
local bitlen = symbol_bitlens[symbol]
if bitlen then
huffman_code = next_codes[bitlen]
next_codes[bitlen] = huffman_code + 1
-- Reverse the bits of huffman code,
-- because most signifant bits of huffman code
-- is stored first into the compressed data.
-- @see RFC1951 Page5 Section 3.1.1
if bitlen <= 9 then -- Have cached reverse for small bitlen.
symbol_huffman_codes[symbol] = _reverse_bits_tbl[bitlen][huffman_code]
else
local reverse = 0
for _ = 1, bitlen do
reverse = reverse - (reverse&1) + ((((reverse&1) == 1) or ((huffman_code&1)) == 1) and 1 or 0)
huffman_code = (huffman_code - (huffman_code&1)) >> 1
reverse = reverse * 2
end
symbol_huffman_codes[symbol] = (reverse - (reverse&1)) >> 1
end
end
end
return symbol_huffman_codes
end
-- A helper function to sort heap elements
-- a[1], b[1] is the huffman frequency
-- a[2], b[2] is the symbol value.
local function SortByFirstThenSecond(a, b)
return a[1] < b[1] or (a[1] == b[1] and a[2] < b[2])
end
-- Calculate the huffman bit length and huffman code.
-- @param symbol_count: A table whose table key is the symbol, and table value
-- is the symbol frenquency (nil means 0 frequency).
-- @param max_bitlen: See description of return value.
-- @param max_symbol: The maximum symbol
-- @return a table whose key is the symbol, and the value is the huffman bit
-- bit length. We guarantee that all bit length <= max_bitlen.
-- For 0<=symbol<=max_symbol, table value could be nil if the frequency
-- of the symbol is 0 or nil.
-- @return a table whose key is the symbol, and the value is the huffman code.
-- @return a number indicating the maximum symbol whose bitlen is not 0.
local function GetHuffmanBitlenAndCode(symbol_counts, max_bitlen, max_symbol)
local heap_size
local max_non_zero_bitlen_symbol = -1
local leafs = {}
local heap = {}
local symbol_bitlens = {}
local symbol_codes = {}
local bitlen_counts = {}
--[[
tree[1]: weight, temporarily used as parent and bitLengths
tree[2]: symbol
tree[3]: left child
tree[4]: right child
--]]
local number_unique_symbols = 0
for symbol, count in pairs(symbol_counts) do
number_unique_symbols = number_unique_symbols + 1
leafs[number_unique_symbols] = {count, symbol}
end
if (number_unique_symbols == 0) then
-- no code.
return {}, {}, -1
elseif (number_unique_symbols == 1) then
-- Only one code. In this case, its huffman code
-- needs to be assigned as 0, and bit length is 1.
-- This is the only case that the return result
-- represents an imcomplete huffman tree.
local symbol = leafs[1][2]
symbol_bitlens[symbol] = 1
symbol_codes[symbol] = 0
return symbol_bitlens, symbol_codes, symbol
else
table_sort(leafs, SortByFirstThenSecond)
heap_size = number_unique_symbols
for i = 1, heap_size do heap[i] = leafs[i] end
while (heap_size > 1) do
-- Note: pop does not change table size of heap
local leftChild = MinHeapPop(heap, heap_size)
heap_size = heap_size - 1
local rightChild = MinHeapPop(heap, heap_size)
heap_size = heap_size - 1
local newNode = {leftChild[1] + rightChild[1], -1, leftChild, rightChild}
MinHeapPush(heap, newNode, heap_size)
heap_size = heap_size + 1
end
-- Number of leafs whose bit length is greater than max_len.
local number_bitlen_overflow = 0
-- Calculate bit length of all nodes
local fifo = {heap[1], 0, 0, 0} -- preallocate some spaces.
local fifo_size = 1
local index = 1
heap[1][1] = 0
while (index <= fifo_size) do -- Breath first search
local e = fifo[index]
local bitlen = e[1]
local symbol = e[2]
local left_child = e[3]
local right_child = e[4]
if left_child then
fifo_size = fifo_size + 1
fifo[fifo_size] = left_child
left_child[1] = bitlen + 1
end
if right_child then
fifo_size = fifo_size + 1
fifo[fifo_size] = right_child
right_child[1] = bitlen + 1
end
index = index + 1
if (bitlen > max_bitlen) then
number_bitlen_overflow = number_bitlen_overflow + 1
bitlen = max_bitlen
end
if symbol >= 0 then
symbol_bitlens[symbol] = bitlen
max_non_zero_bitlen_symbol = (symbol > max_non_zero_bitlen_symbol) and symbol or max_non_zero_bitlen_symbol
bitlen_counts[bitlen] = (bitlen_counts[bitlen] or 0) + 1
end
end
-- Resolve bit length overflow
-- @see ZLib/trees.c:gen_bitlen(s, desc), for reference
if (number_bitlen_overflow > 0) then
repeat
local bitlen = max_bitlen - 1
while ((bitlen_counts[bitlen] or 0) == 0) do bitlen = bitlen - 1 end
-- move one leaf down the tree
bitlen_counts[bitlen] = bitlen_counts[bitlen] - 1
-- move one overflow item as its brother
bitlen_counts[bitlen + 1] = (bitlen_counts[bitlen + 1] or 0) + 2
bitlen_counts[max_bitlen] = bitlen_counts[max_bitlen] - 1
number_bitlen_overflow = number_bitlen_overflow - 2
until (number_bitlen_overflow <= 0)
index = 1
for bitlen = max_bitlen, 1, -1 do
local n = bitlen_counts[bitlen] or 0
while (n > 0) do
local symbol = leafs[index][2]
symbol_bitlens[symbol] = bitlen
n = n - 1
index = index + 1
end
end
end
symbol_codes = GetHuffmanCodeFromBitlen(bitlen_counts, symbol_bitlens, max_symbol, max_bitlen)
return symbol_bitlens, symbol_codes, max_non_zero_bitlen_symbol
end
end
-- Calculate the first huffman header in the dynamic huffman block
-- @see RFC1951 Page 12
-- @param lcode_bitlen: The huffman bit length of literal,LZ77_length.
-- @param max_non_zero_bitlen_lcode: The maximum literal,LZ77_length symbol
-- whose huffman bit length is not zero.
-- @param dcode_bitlen: The huffman bit length of LZ77 distance.
-- @param max_non_zero_bitlen_dcode: The maximum LZ77 distance symbol
-- whose huffman bit length is not zero.
-- @return The run length encoded codes.
-- @return The extra bits. One entry for each rle code that needs extra bits.
-- (code == 16 or 17 or 18).
-- @return The count of appearance of each rle codes.
local function RunLengthEncodeHuffmanBitlen(lcode_bitlens, max_non_zero_bitlen_lcode, dcode_bitlens, max_non_zero_bitlen_dcode)
local rle_code_tblsize = 0
local rle_codes = {}
local rle_code_counts = {}
local rle_extra_bits_tblsize = 0
local rle_extra_bits = {}
local prev = nil
local count = 0
-- If there is no distance code, assume one distance code of bit length 0.
-- RFC1951: One distance code of zero bits means that
-- there are no distance codes used at all (the data is all literals).
max_non_zero_bitlen_dcode = (max_non_zero_bitlen_dcode < 0) and 0 or max_non_zero_bitlen_dcode
local max_code = max_non_zero_bitlen_lcode + max_non_zero_bitlen_dcode + 1
for code = 0, max_code + 1 do
local len = (code <= max_non_zero_bitlen_lcode) and
(lcode_bitlens[code] or 0) or ((code <= max_code) and
(dcode_bitlens[code - max_non_zero_bitlen_lcode - 1] or 0) or
nil)
if len == prev then
count = count + 1
if len ~= 0 and count == 6 then
rle_code_tblsize = rle_code_tblsize + 1
rle_codes[rle_code_tblsize] = 16
rle_extra_bits_tblsize = rle_extra_bits_tblsize + 1
rle_extra_bits[rle_extra_bits_tblsize] = 3
rle_code_counts[16] = (rle_code_counts[16] or 0) + 1
count = 0
elseif len == 0 and count == 138 then
rle_code_tblsize = rle_code_tblsize + 1
rle_codes[rle_code_tblsize] = 18
rle_extra_bits_tblsize = rle_extra_bits_tblsize + 1
rle_extra_bits[rle_extra_bits_tblsize] = 127
rle_code_counts[18] = (rle_code_counts[18] or 0) + 1
count = 0
end
else
if count == 1 then
rle_code_tblsize = rle_code_tblsize + 1
rle_codes[rle_code_tblsize] = prev
rle_code_counts[prev] = (rle_code_counts[prev] or 0) + 1
elseif count == 2 then
rle_code_tblsize = rle_code_tblsize + 1
rle_codes[rle_code_tblsize] = prev
rle_code_tblsize = rle_code_tblsize + 1
rle_codes[rle_code_tblsize] = prev
rle_code_counts[prev] = (rle_code_counts[prev] or 0) + 2
elseif count >= 3 then
rle_code_tblsize = rle_code_tblsize + 1
local rleCode = (prev ~= 0) and 16 or (count <= 10 and 17 or 18)
rle_codes[rle_code_tblsize] = rleCode
rle_code_counts[rleCode] = (rle_code_counts[rleCode] or 0) + 1
rle_extra_bits_tblsize = rle_extra_bits_tblsize + 1
rle_extra_bits[rle_extra_bits_tblsize] =
(count <= 10) and (count - 3) or (count - 11)
end
prev = len
if len and len ~= 0 then
rle_code_tblsize = rle_code_tblsize + 1
rle_codes[rle_code_tblsize] = len
rle_code_counts[len] = (rle_code_counts[len] or 0) + 1
count = 0
else
count = 1
end
end
end
return rle_codes, rle_extra_bits, rle_code_counts
end
-- Load the string into a table, in order to speed up LZ77.
-- Loop unrolled 16 times to speed this function up.
-- @param str The string to be loaded.
-- @param t The load destination
-- @param start str[index] will be the first character to be loaded.
-- @param end str[index] will be the last character to be loaded
-- @param offset str[index] will be loaded into t[index-offset]
-- @return t
local function LoadStringToTable(str, t, start, stop, offset)
local i = start - offset
while i <= stop - 15 - offset do
t[i], t[i + 1], t[i + 2], t[i + 3], t[i + 4], t[i + 5], t[i + 6], t[i + 7], t[i + 8], t[i + 9], t[i + 10],
t[i + 11], t[i + 12], t[i + 13], t[i + 14], t[i +15] = string_byte(str, i + offset, i + 15 + offset)
i = i + 16
end
while (i <= stop - offset) do
t[i] = string_byte(str, i + offset, i + offset)
i = i + 1
end
return t
end
-- Do LZ77 process. This function uses the majority of the CPU time.
-- @see zlib/deflate.c:deflate_fast(), zlib/deflate.c:deflate_slow()
-- This function uses the algorithms used above. You should read the
-- algorithm.txt above to understand what is the hash function and the
-- lazy evaluation.
--
-- The special optimization used here is hash functions used here.
-- The hash function is just the multiplication of the three consective
-- characters. So if the hash matches, it guarantees 3 characters are matched.
-- This optimization can be implemented because Lua table is a hash table.
--
-- @param level integer that describes compression level.
-- @param string_table table that stores the value of string to be compressed.
-- The index of this table starts from 1.
-- The caller needs to make sure all values needed by this function
-- are loaded.
-- Assume "str" is the origin input string into the compressor
-- str[block_start]..str[block_end+3] needs to be loaded into
-- string_table[block_start-offset]..string_table[block_end-offset]
-- If dictionary is presented, the last 258 bytes of the dictionary
-- needs to be loaded into sing_table[-257..0]
-- (See more in the description of offset.)
-- @param hash_tables. The table key is the hash value (0<=hash<=16777216=256^3)
-- The table value is an array0 that stores the indexes of the
-- input data string to be compressed, such that
-- hash == str[index]*str[index+1]*str[index+2]
-- Indexes are ordered in this array.
-- @param block_start The indexes of the input data string to be compressed.
-- that starts the LZ77 block.
-- @param block_end The indexes of the input data string to be compressed.
-- that stores the LZ77 block.
-- @param offset str[index] is stored in string_table[index-offset],
-- This offset is mainly an optimization to limit the index
-- of string_table, so lua can access this table quicker.
-- @param dictionary See LibDeflate:CreateDictionary
-- @return literal,LZ77_length deflate codes.
-- @return the extra bits of literal,LZ77_length deflate codes.
-- @return the count of each literal,LZ77 deflate code.
-- @return LZ77 distance deflate codes.
-- @return the extra bits of LZ77 distance deflate codes.
-- @return the count of each LZ77 distance deflate code.
local function GetBlockLZ77Result(level, string_table, hash_tables, block_start, block_end, offset)--, dictionary)
local config = _compression_level_configs[level]
local config_use_lazy, config_good_prev_length, config_max_lazy_match,
config_nice_length, config_max_hash_chain = config[1], config[2], config[3], config[4], config[5]
local config_max_insert_length = (not config_use_lazy) and config_max_lazy_match or 2147483646
local config_good_hash_chain = (config_max_hash_chain - ((config_max_hash_chain&3) >> 2))
local hash
local dict_hash_tables
local dict_string_table
local dict_string_len_plus3 = 3
hash = (string_table[block_start - offset] or 0) * 256 + (string_table[block_start + 1 - offset] or 0)
local lcodes = {}
local lcode_tblsize = 0
local lcodes_counts = {}
local dcodes = {}
local dcodes_tblsize = 0
local dcodes_counts = {}
local lextra_bits = {}
local lextra_bits_tblsize = 0
local dextra_bits = {}
local dextra_bits_tblsize = 0
local match_available = false
local prev_len
local prev_dist
local cur_len = 0
local cur_dist = 0
local index = block_start
local index_end = block_end + (config_use_lazy and 1 or 0)
-- the zlib source code writes separate code for lazy evaluation and
-- not lazy evaluation, which is easier to understand.
-- I put them together, so it is a bit harder to understand.
-- because I think this is easier for me to maintain it.
while (index <= index_end) do
local string_table_index = index - offset
local offset_minus_three = offset - 3
prev_len = cur_len
prev_dist = cur_dist
cur_len = 0
hash = (hash * 256 + (string_table[string_table_index + 2] or 0)) & 16777215
local chain_index
local cur_chain
local hash_chain = hash_tables[hash]
local chain_old_size
if not hash_chain then
chain_old_size = 0
hash_chain = {}
hash_tables[hash] = hash_chain
if dict_hash_tables then
cur_chain = dict_hash_tables[hash]
chain_index = cur_chain and #cur_chain or 0
else
chain_index = 0
end
else
chain_old_size = #hash_chain
cur_chain = hash_chain
chain_index = chain_old_size
end
if index <= block_end then hash_chain[chain_old_size + 1] = index end
if (chain_index > 0 and index + 2 <= block_end and (not config_use_lazy or prev_len < config_max_lazy_match)) then
local depth = (config_use_lazy and prev_len >= config_good_prev_length) and config_good_hash_chain or config_max_hash_chain
local max_len_minus_one = block_end - index
max_len_minus_one = (max_len_minus_one >= 257) and 257 or max_len_minus_one
max_len_minus_one = max_len_minus_one + string_table_index
local string_table_index_plus_three = string_table_index + 3
while chain_index >= 1 and depth > 0 do
local prev = cur_chain[chain_index]
if index - prev > 32768 then break end
if prev < index then
local sj = string_table_index_plus_three
if prev >= -257 then
local pj = prev - offset_minus_three
while (sj <= max_len_minus_one and string_table[pj] == string_table[sj]) do
sj = sj + 1
pj = pj + 1
end
else
local pj = dict_string_len_plus3 + prev
end
local j = sj - string_table_index
if j > cur_len then
cur_len = j
cur_dist = index - prev
end
if cur_len >= config_nice_length then break end
end
chain_index = chain_index - 1
depth = depth - 1
if chain_index == 0 and prev > 0 and dict_hash_tables then
cur_chain = dict_hash_tables[hash]
chain_index = cur_chain and #cur_chain or 0
end
end
end
if not config_use_lazy then prev_len, prev_dist = cur_len, cur_dist end
if ((not config_use_lazy or match_available) and (prev_len > 3 or (prev_len == 3 and prev_dist < 4096)) and cur_len <= prev_len) then
local code = _length_to_deflate_code[prev_len]
local length_extra_bits_bitlen = _length_to_deflate_extra_bitlen[prev_len]
local dist_code, dist_extra_bits_bitlen, dist_extra_bits
if prev_dist <= 256 then -- have cached code for small distance.
dist_code = _dist256_to_deflate_code[prev_dist]
dist_extra_bits = _dist256_to_deflate_extra_bits[prev_dist]
dist_extra_bits_bitlen = _dist256_to_deflate_extra_bitlen[prev_dist]
else
dist_code = 16
dist_extra_bits_bitlen = 7
local a = 384
local b = 512
while true do
if prev_dist <= a then
dist_extra_bits = (prev_dist - (b >> 1) - 1) & ((b >> 2)-1)
break
elseif prev_dist <= b then
dist_extra_bits = (prev_dist - (b >> 1) - 1) & ((b >> 2)-1)
dist_code = dist_code + 1
break
else
dist_code = dist_code + 2
dist_extra_bits_bitlen = dist_extra_bits_bitlen + 1
a = a * 2
b = b * 2
end
end
end
lcode_tblsize = lcode_tblsize + 1
lcodes[lcode_tblsize] = code
lcodes_counts[code] = (lcodes_counts[code] or 0) + 1
dcodes_tblsize = dcodes_tblsize + 1
dcodes[dcodes_tblsize] = dist_code
dcodes_counts[dist_code] = (dcodes_counts[dist_code] or 0) + 1
if length_extra_bits_bitlen > 0 then
local lenExtraBits = _length_to_deflate_extra_bits[prev_len]
lextra_bits_tblsize = lextra_bits_tblsize + 1
lextra_bits[lextra_bits_tblsize] = lenExtraBits
end
if dist_extra_bits_bitlen > 0 then
dextra_bits_tblsize = dextra_bits_tblsize + 1
dextra_bits[dextra_bits_tblsize] = dist_extra_bits
end
for i = index + 1, index + prev_len - (config_use_lazy and 2 or 1) do
hash = (hash * 256 + (string_table[i - offset + 2] or 0)) & 16777215
if prev_len <= config_max_insert_length then
hash_chain = hash_tables[hash]
if not hash_chain then
hash_chain = {}
hash_tables[hash] = hash_chain
end
hash_chain[#hash_chain + 1] = i
end
end
index = index + prev_len - (config_use_lazy and 1 or 0)
match_available = false
elseif (not config_use_lazy) or match_available then
local code = string_table[config_use_lazy and (string_table_index - 1) or string_table_index]
lcode_tblsize = lcode_tblsize + 1
lcodes[lcode_tblsize] = code
lcodes_counts[code] = (lcodes_counts[code] or 0) + 1
index = index + 1
else
match_available = true
index = index + 1
end
end
-- Write "end of block" symbol
lcode_tblsize = lcode_tblsize + 1
lcodes[lcode_tblsize] = 256
lcodes_counts[256] = (lcodes_counts[256] or 0) + 1
return lcodes, lextra_bits, lcodes_counts, dcodes, dextra_bits, dcodes_counts
end
-- Get the header data of dynamic block.
-- @param lcodes_count The count of each literal,LZ77_length codes.
-- @param dcodes_count The count of each Lz77 distance codes.
-- @return a lots of stuffs.
-- @see RFC1951 Page 12
local function GetBlockDynamicHuffmanHeader(lcodes_counts, dcodes_counts)
local lcodes_huffman_bitlens, lcodes_huffman_codes, max_non_zero_bitlen_lcode = GetHuffmanBitlenAndCode(lcodes_counts, 15, 285)
local dcodes_huffman_bitlens, dcodes_huffman_codes, max_non_zero_bitlen_dcode = GetHuffmanBitlenAndCode(dcodes_counts, 15, 29)
local rle_deflate_codes, rle_extra_bits, rle_codes_counts =
RunLengthEncodeHuffmanBitlen(lcodes_huffman_bitlens,
max_non_zero_bitlen_lcode,
dcodes_huffman_bitlens,
max_non_zero_bitlen_dcode)
local rle_codes_huffman_bitlens, rle_codes_huffman_codes = GetHuffmanBitlenAndCode(rle_codes_counts, 7, 18)
local HCLEN = 0
for i = 1, 19 do
local symbol = _rle_codes_huffman_bitlen_order[i]
local length = rle_codes_huffman_bitlens[symbol] or 0
if length ~= 0 then HCLEN = i end
end
HCLEN = HCLEN - 4
local HLIT = max_non_zero_bitlen_lcode + 1 - 257
local HDIST = max_non_zero_bitlen_dcode + 1 - 1
if HDIST < 0 then HDIST = 0 end
return HLIT, HDIST, HCLEN, rle_codes_huffman_bitlens, rle_codes_huffman_codes,
rle_deflate_codes, rle_extra_bits, lcodes_huffman_bitlens,
lcodes_huffman_codes, dcodes_huffman_bitlens, dcodes_huffman_codes
end
-- Get the size of dynamic block without writing any bits into the writer.
-- @param ... Read the source code of GetBlockDynamicHuffmanHeader()
-- @return the bit length of the dynamic block
local function GetDynamicHuffmanBlockSize(lcodes, dcodes, HCLEN,
rle_codes_huffman_bitlens,
rle_deflate_codes,
lcodes_huffman_bitlens,
dcodes_huffman_bitlens)
local block_bitlen = 17 -- 1+2+5+5+4
block_bitlen = block_bitlen + (HCLEN + 4) * 3
for i = 1, #rle_deflate_codes do
local code = rle_deflate_codes[i]
block_bitlen = block_bitlen + rle_codes_huffman_bitlens[code]
if code >= 16 then
block_bitlen = block_bitlen + ((code == 16) and 2 or (code == 17 and 3 or 7))
end
end
local length_code_count = 0
for i = 1, #lcodes do
local code = lcodes[i]
local huffman_bitlen = lcodes_huffman_bitlens[code]
block_bitlen = block_bitlen + huffman_bitlen
if code > 256 then -- Length code
length_code_count = length_code_count + 1
if code > 264 and code < 285 then -- Length code with extra bits
local extra_bits_bitlen = _literal_deflate_code_to_extra_bitlen[code - 256]
block_bitlen = block_bitlen + extra_bits_bitlen
end
local dist_code = dcodes[length_code_count]
local dist_huffman_bitlen = dcodes_huffman_bitlens[dist_code]
block_bitlen = block_bitlen + dist_huffman_bitlen
if dist_code > 3 then -- dist code with extra bits
local dist_extra_bits_bitlen = ((dist_code - (dist_code&1)) >> 1) - 1
block_bitlen = block_bitlen + dist_extra_bits_bitlen
end
end
end
return block_bitlen
end
-- Write dynamic block.
-- @param ... Read the source code of GetBlockDynamicHuffmanHeader()
local function CompressDynamicHuffmanBlock(WriteBits, is_last_block, lcodes,
lextra_bits, dcodes, dextra_bits,
HLIT, HDIST, HCLEN,
rle_codes_huffman_bitlens,
rle_codes_huffman_codes,
rle_deflate_codes, rle_extra_bits,
lcodes_huffman_bitlens,
lcodes_huffman_codes,
dcodes_huffman_bitlens,
dcodes_huffman_codes)
WriteBits(is_last_block and 1 or 0, 1) -- Last block identifier
WriteBits(2, 2) -- Dynamic Huffman block identifier
WriteBits(HLIT, 5)
WriteBits(HDIST, 5)
WriteBits(HCLEN, 4)
for i = 1, HCLEN + 4 do
local symbol = _rle_codes_huffman_bitlen_order[i]
local length = rle_codes_huffman_bitlens[symbol] or 0
WriteBits(length, 3)
end
local rleExtraBitsIndex = 1
for i = 1, #rle_deflate_codes do
local code = rle_deflate_codes[i]
WriteBits(rle_codes_huffman_codes[code], rle_codes_huffman_bitlens[code])
if code >= 16 then
local extraBits = rle_extra_bits[rleExtraBitsIndex]
WriteBits(extraBits, (code == 16) and 2 or (code == 17 and 3 or 7))
rleExtraBitsIndex = rleExtraBitsIndex + 1
end
end
local length_code_count = 0
local length_code_with_extra_count = 0
local dist_code_with_extra_count = 0
for i = 1, #lcodes do
local deflate_codee = lcodes[i]
local huffman_code = lcodes_huffman_codes[deflate_codee]
local huffman_bitlen = lcodes_huffman_bitlens[deflate_codee]
WriteBits(huffman_code, huffman_bitlen)
if deflate_codee > 256 then -- Length code
length_code_count = length_code_count + 1
if deflate_codee > 264 and deflate_codee < 285 then
-- Length code with extra bits
length_code_with_extra_count = length_code_with_extra_count + 1
local extra_bits = lextra_bits[length_code_with_extra_count]
local extra_bits_bitlen = _literal_deflate_code_to_extra_bitlen[deflate_codee - 256]
WriteBits(extra_bits, extra_bits_bitlen)
end
-- Write distance code
local dist_deflate_code = dcodes[length_code_count]
local dist_huffman_code = dcodes_huffman_codes[dist_deflate_code]
local dist_huffman_bitlen = dcodes_huffman_bitlens[dist_deflate_code]
WriteBits(dist_huffman_code, dist_huffman_bitlen)
if dist_deflate_code > 3 then -- dist code with extra bits
dist_code_with_extra_count = dist_code_with_extra_count + 1
local dist_extra_bits = dextra_bits[dist_code_with_extra_count]
local dist_extra_bits_bitlen = ((dist_deflate_code - (dist_deflate_code&1)) >> 1) - 1
WriteBits(dist_extra_bits, dist_extra_bits_bitlen)
end
end
end
end
-- Get the size of fixed block without writing any bits into the writer.
-- @param lcodes literal,LZ77_length deflate codes
-- @param decodes LZ77 distance deflate codes
-- @return the bit length of the fixed block
local function GetFixedHuffmanBlockSize(lcodes, dcodes)
local block_bitlen = 3
local length_code_count = 0
for i = 1, #lcodes do
local code = lcodes[i]
local huffman_bitlen = _fix_block_literal_huffman_bitlen[code]
block_bitlen = block_bitlen + huffman_bitlen
if code > 256 then -- Length code
length_code_count = length_code_count + 1
if code > 264 and code < 285 then -- Length code with extra bits
local extra_bits_bitlen = _literal_deflate_code_to_extra_bitlen[code - 256]
block_bitlen = block_bitlen + extra_bits_bitlen
end
local dist_code = dcodes[length_code_count]
block_bitlen = block_bitlen + 5
if dist_code > 3 then -- dist code with extra bits
local dist_extra_bits_bitlen = ((dist_code - (dist_code&1)) >> 1) - 1
block_bitlen = block_bitlen + dist_extra_bits_bitlen
end
end
end
return block_bitlen
end
-- Get the size of store block without writing any bits into the writer.
-- @param block_start The start index of the origin input string
-- @param block_end The end index of the origin input string
-- @param Total bit lens had been written into the compressed result before,
-- because store block needs to shift to byte boundary.
-- @return the bit length of the fixed block
local function GetStoreBlockSize(block_start, block_end, total_bitlen)
assert(block_end - block_start + 1 <= 65535)
local block_bitlen = 3
total_bitlen = total_bitlen + 3
local padding_bitlen = ((8 - (total_bitlen&7))&7)
block_bitlen = block_bitlen + padding_bitlen
block_bitlen = block_bitlen + 16
block_bitlen = block_bitlen + (block_end - block_start + 1) * 8
return block_bitlen
end
-- Do the deflate
-- Currently using a simple way to determine the block size
-- (This is why the compression ratio is little bit worse than zlib when
-- the input size is very large
-- The first block is 64KB, the following block is 32KB.
-- After each block, there is a memory cleanup operation.
-- This is not a fast operation, but it is needed to save memory usage, so
-- the memory usage does not grow unboundly. If the data size is less than
-- 64KB, then memory cleanup won't happen.
-- This function determines whether to use store,fixed,dynamic blocks by
-- calculating the block size of each block type and chooses the smallest one.
local function Deflate( WriteBits, WriteString, FlushWriter, str)
local string_table = {}
local hash_tables = {}
local is_last_block = nil
local block_start
local block_end
local bitlen_written
local total_bitlen = FlushWriter(_FLUSH_MODE_NO_FLUSH)
local strlen = #str
local offset
local level = strlen < 2048 and 7 or 3;
while not is_last_block do
if not block_start then
block_start = 1
block_end = 32 * 1024 - 1
offset = 0
else
block_start = block_end + 1
block_end = block_end + 16 * 1024
offset = block_start - 16 * 1024 - 1
end
if block_end >= strlen then
block_end = strlen
is_last_block = true
else
is_last_block = false
end
local lcodes, lextra_bits, lcodes_counts, dcodes, dextra_bits, dcodes_counts
local HLIT, HDIST, HCLEN, rle_codes_huffman_bitlens,
rle_codes_huffman_codes, rle_deflate_codes, rle_extra_bits,
lcodes_huffman_bitlens, lcodes_huffman_codes, dcodes_huffman_bitlens,
dcodes_huffman_codes
local dynamic_block_bitlen
local fixed_block_bitlen
local store_block_bitlen
if level ~= 0 then
-- GetBlockLZ77 needs block_start to block_end+3 to be loaded.
LoadStringToTable(str, string_table, block_start, block_end + 3, offset)
lcodes, lextra_bits, lcodes_counts, dcodes, dextra_bits, dcodes_counts =
GetBlockLZ77Result(level, string_table, hash_tables, block_start, block_end, offset)
-- LuaFormatter off
HLIT, HDIST, HCLEN, rle_codes_huffman_bitlens, rle_codes_huffman_codes, rle_deflate_codes,
rle_extra_bits, lcodes_huffman_bitlens, lcodes_huffman_codes, dcodes_huffman_bitlens, dcodes_huffman_codes
= GetBlockDynamicHuffmanHeader(lcodes_counts, dcodes_counts)
dynamic_block_bitlen = GetDynamicHuffmanBlockSize(lcodes, dcodes, HCLEN,
rle_codes_huffman_bitlens,
rle_deflate_codes,
lcodes_huffman_bitlens,
dcodes_huffman_bitlens)
fixed_block_bitlen = GetFixedHuffmanBlockSize(lcodes, dcodes)
end
store_block_bitlen = GetStoreBlockSize(block_start, block_end, total_bitlen)
local min_bitlen = store_block_bitlen
min_bitlen = (fixed_block_bitlen and fixed_block_bitlen < min_bitlen) and fixed_block_bitlen or min_bitlen
min_bitlen = (dynamic_block_bitlen and dynamic_block_bitlen < min_bitlen) and dynamic_block_bitlen or min_bitlen
CompressDynamicHuffmanBlock(WriteBits, is_last_block, lcodes, lextra_bits,
dcodes, dextra_bits, HLIT, HDIST, HCLEN,
rle_codes_huffman_bitlens,
rle_codes_huffman_codes, rle_deflate_codes,
rle_extra_bits, lcodes_huffman_bitlens,
lcodes_huffman_codes, dcodes_huffman_bitlens,
dcodes_huffman_codes);
total_bitlen = total_bitlen + dynamic_block_bitlen;
if is_last_block then
bitlen_written = FlushWriter(_FLUSH_MODE_NO_FLUSH)
else
bitlen_written = FlushWriter(_FLUSH_MODE_MEMORY_CLEANUP)
end
assert(bitlen_written == total_bitlen)
-- Memory clean up, so memory consumption does not always grow linearly,
-- even if input string is > 64K.
-- Not a very efficient operation, but this operation won't happen
-- when the input data size is less than 64K.
if not is_last_block then
local j
j = 1
for i = block_end - 32767, block_end do
string_table[j] = string_table[i - offset]
j = j + 1
end
for k, t in pairs(hash_tables) do
local tSize = #t
if tSize > 0 and block_end + 1 - t[1] > 32768 then
if tSize == 1 then
hash_tables[k] = nil
else
local new = {}
local newSize = 0
for i = 2, tSize do
j = t[i]
if block_end + 1 - j <= 32768 then
newSize = newSize + 1
new[newSize] = j
end
end
hash_tables[k] = new
end
end
end
end
end
end
--[[ --------------------------------------------------------------------------
Decompress code
--]] --------------------------------------------------------------------------
--[[
Create a reader to easily reader stuffs as the unit of bits.
Return values:
1. ReadBits(bitlen)
2. ReadBytes(bytelen, buffer, buffer_size)
3. Decode(huffman_bitlen_count, huffman_symbol, min_bitlen)
4. ReaderBitlenLeft()
5. SkipToByteBoundary()
--]]
local function CreateReader(input_string)
local input = input_string
local input_strlen = #input_string
local input_next_byte_pos = 1
local cache_bitlen = 0
local cache = 0
-- Read some bits.
-- To improve speed, this function does not
-- check if the input has been exhausted.
-- Use ReaderBitlenLeft() < 0 to check it.
-- @param bitlen the number of bits to read
-- @return the data is read.
local function ReadBits(bitlen)
local rshift_mask = _pow2[bitlen]
local code = 0
if bitlen <= cache_bitlen then
code = (cache&(rshift_mask-1));
cache = (cache - code) >> bitlen;
cache_bitlen = cache_bitlen - bitlen;
else -- Whether input has been exhausted is not checked.
local lshift_mask = _pow2[cache_bitlen]
local byte1, byte2 = string_byte(input, input_next_byte_pos, input_next_byte_pos + 1)
cache = cache + ((byte1 or 0) + (byte2 or 0) * 256) * lshift_mask
input_next_byte_pos = input_next_byte_pos + 2
cache_bitlen = cache_bitlen + 16 - bitlen
code = (cache&(rshift_mask-1))
cache = (cache - code) >> bitlen
end
return code
end
-- Read some bytes from the reader.
-- Assume reader is on the byte boundary.
-- @param bytelen The number of bytes to be read.
-- @param buffer The byte read will be stored into this buffer.
-- @param buffer_size The buffer will be modified starting from
-- buffer[buffer_size+1], ending at buffer[buffer_size+bytelen-1]
-- @return the new buffer_size
local function ReadBytes(bytelen, buffer, buffer_size)
assert((cache_bitlen&7) == 0)
local byte_from_cache = ((cache_bitlen >> 3) < bytelen) and (cache_bitlen >> 3) or bytelen
for _ = 1, byte_from_cache do
local byte = ((cache&255))
buffer_size = buffer_size + 1
buffer[buffer_size] = string_char(byte)
cache = (cache - byte) >> 8
end
cache_bitlen = cache_bitlen - byte_from_cache * 8
bytelen = bytelen - byte_from_cache
if (input_strlen - input_next_byte_pos - bytelen + 1) * 8 + cache_bitlen < 0 then
return -1 -- out of input
end
for i = input_next_byte_pos, input_next_byte_pos + bytelen - 1 do
buffer_size = buffer_size + 1
buffer[buffer_size] = string_sub(input, i, i)
end
input_next_byte_pos = input_next_byte_pos + bytelen
return buffer_size
end
-- Decode huffman code
-- To improve speed, this function does not check
-- if the input has been exhausted.
-- Use ReaderBitlenLeft() < 0 to check it.
-- Credits for Mark Adler. This code is from puff:Decode()
-- @see puff:Decode(...)
-- @param huffman_bitlen_count
-- @param huffman_symbol
-- @param min_bitlen The minimum huffman bit length of all symbols
-- @return The decoded deflate code.
-- Negative value is returned if decoding fails.
local function Decode(huffman_bitlen_counts, huffman_symbols, min_bitlen)
local code = 0
local first = 0
local index = 0
local count
if min_bitlen > 0 then
if cache_bitlen < 15 and input then
local lshift_mask = _pow2[cache_bitlen]
local byte1, byte2 = string_byte(input, input_next_byte_pos, input_next_byte_pos + 1)
-- This requires lua number to be at least double ()
cache = cache + ((byte1 or 0) + (byte2 or 0) * 256) * lshift_mask
input_next_byte_pos = input_next_byte_pos + 2
cache_bitlen = cache_bitlen + 16
end
local rshift_mask = _pow2[min_bitlen]
cache_bitlen = cache_bitlen - min_bitlen
code = (cache&(rshift_mask-1))
cache = (cache - code) >> min_bitlen
-- Reverse the bits
code = _reverse_bits_tbl[min_bitlen][code]
count = huffman_bitlen_counts[min_bitlen]
if code < count then return huffman_symbols[code] end
index = count
first = count * 2
code = code * 2
end
for bitlen = min_bitlen + 1, 15 do
local bit
bit = (cache&1)
cache = (cache - bit) >> 1
cache_bitlen = cache_bitlen - 1
code = (bit == 1) and (code + 1 - (code&1)) or code
count = huffman_bitlen_counts[bitlen] or 0
local diff = code - first
if diff < count then return huffman_symbols[index + diff] end
index = index + count
first = first + count
first = first * 2
code = code * 2
end
-- invalid literal,length or distance code
-- in fixed or dynamic block (run out of code)
return -10
end
local function ReaderBitlenLeft()
return (input_strlen - input_next_byte_pos + 1) * 8 + cache_bitlen
end
local function SkipToByteBoundary()
local skipped_bitlen = (cache_bitlen&7)
local rshift_mask = _pow2[skipped_bitlen]
cache_bitlen = cache_bitlen - skipped_bitlen
cache = (cache - (cache&(rshift_mask-1))) >> skipped_bitlen
end
return ReadBits, ReadBytes, Decode, ReaderBitlenLeft, SkipToByteBoundary
end
-- Create a deflate state, so I can pass in less arguments to functions.
-- @param str the whole string to be decompressed.
-- @param dictionary The preset dictionary. nil if not provided.
-- This dictionary should be produced by LibDeflate:CreateDictionary(str)
-- @return The decomrpess state.
local function CreateDecompressState(str)
local ReadBits, ReadBytes, Decode, ReaderBitlenLeft, SkipToByteBoundary = CreateReader(str)
local state = {
ReadBits = ReadBits,
ReadBytes = ReadBytes,
Decode = Decode,
ReaderBitlenLeft = ReaderBitlenLeft,
SkipToByteBoundary = SkipToByteBoundary,
buffer_size = 0,
buffer = {},
result_buffer = {},
}
return state
end
-- Get the stuffs needed to decode huffman codes
-- @see puff.c:construct(...)
-- @param huffman_bitlen The huffman bit length of the huffman codes.
-- @param max_symbol The maximum symbol
-- @param max_bitlen The min huffman bit length of all codes
-- @return zero or positive for success, negative for failure.
-- @return The count of each huffman bit length.
-- @return A table to convert huffman codes to deflate codes.
-- @return The minimum huffman bit length.
local function GetHuffmanForDecode(huffman_bitlens, max_symbol, max_bitlen)
local huffman_bitlen_counts = {}
local min_bitlen = max_bitlen
for symbol = 0, max_symbol do
local bitlen = huffman_bitlens[symbol] or 0
min_bitlen = (bitlen > 0 and bitlen < min_bitlen) and bitlen or min_bitlen
huffman_bitlen_counts[bitlen] = (huffman_bitlen_counts[bitlen] or 0) + 1
end
if huffman_bitlen_counts[0] == max_symbol + 1 then -- No Codes
return 0, huffman_bitlen_counts, {}, 0 -- Complete, but decode will fail
end
local left = 1
for len = 1, max_bitlen do
left = left * 2
left = left - (huffman_bitlen_counts[len] or 0)
if left < 0 then
return left -- Over-subscribed, return negative
end
end
-- Generate offsets info symbol table for each length for sorting
local offsets = {}
offsets[1] = 0
for len = 1, max_bitlen - 1 do
offsets[len + 1] = offsets[len] + (huffman_bitlen_counts[len] or 0)
end
local huffman_symbols = {}
for symbol = 0, max_symbol do
local bitlen = huffman_bitlens[symbol] or 0
if bitlen ~= 0 then
local offset = offsets[bitlen]
huffman_symbols[offset] = symbol
offsets[bitlen] = offsets[bitlen] + 1
end
end
-- Return zero for complete set, positive for incomplete set.
return left, huffman_bitlen_counts, huffman_symbols, min_bitlen
end
-- Decode a fixed or dynamic huffman blocks, excluding last block identifier
-- and block type identifer.
-- @see puff.c:codes()
-- @param state decompression state that will be modified by this function.
-- @see CreateDecompressState
-- @param ... Read the source code
-- @return 0 on success, other value on failure.
local function DecodeUntilEndOfBlock(state, lcodes_huffman_bitlens,
lcodes_huffman_symbols,
lcodes_huffman_min_bitlen,
dcodes_huffman_bitlens,
dcodes_huffman_symbols,
dcodes_huffman_min_bitlen)
local buffer, buffer_size, ReadBits, Decode, ReaderBitlenLeft, result_buffer =
state.buffer, state.buffer_size, state.ReadBits, state.Decode,
state.ReaderBitlenLeft, state.result_buffer
local dict_string_table
local dict_strlen
local buffer_end = 1
repeat
local symbol = Decode(lcodes_huffman_bitlens, lcodes_huffman_symbols,
lcodes_huffman_min_bitlen)
if symbol < 0 or symbol > 285 then
-- invalid literal,length or distance code in fixed or dynamic block
return -10
elseif symbol < 256 then -- Literal
buffer_size = buffer_size + 1
buffer[buffer_size] = _byte_to_char[symbol]
elseif symbol > 256 then -- Length code
symbol = symbol - 256
local bitlen = _literal_deflate_code_to_base_len[symbol]
bitlen = (symbol >= 8) and (bitlen + ReadBits(_literal_deflate_code_to_extra_bitlen[symbol])) or bitlen
symbol = Decode(dcodes_huffman_bitlens, dcodes_huffman_symbols, dcodes_huffman_min_bitlen)
if symbol < 0 or symbol > 29 then
-- invalid literal,length or distance code in fixed or dynamic block
return -10
end
local dist = _dist_deflate_code_to_base_dist[symbol]
dist = (dist > 4) and (dist + ReadBits(_dist_deflate_code_to_extra_bitlen[symbol])) or dist
local char_buffer_index = buffer_size - dist + 1
if char_buffer_index < buffer_end then
-- distance is too far back in fixed or dynamic block
return -11
end
if char_buffer_index >= -257 then
for _ = 1, bitlen do
buffer_size = buffer_size + 1
buffer[buffer_size] = buffer[char_buffer_index]
char_buffer_index = char_buffer_index + 1
end
else
char_buffer_index = dict_strlen + char_buffer_index
for _ = 1, bitlen do
buffer_size = buffer_size + 1
buffer[buffer_size] = _byte_to_char[dict_string_table[char_buffer_index]]
char_buffer_index = char_buffer_index + 1
end
end
end
if ReaderBitlenLeft() < 0 then
return 2 -- available inflate data did not terminate
end
if buffer_size >= 65536 then
result_buffer[#result_buffer + 1] = table_concat(buffer, "", 1, 32768)
for i = 32769, buffer_size do buffer[i - 32768] = buffer[i] end
buffer_size = buffer_size - 32768
buffer[buffer_size + 1] = nil
-- NOTE: buffer[32769..end] and buffer[-257..0] are not cleared.
-- This is why "buffer_size" variable is needed.
end
until symbol == 256
state.buffer_size = buffer_size
return 0
end
-- Decompress a store block
-- @param state decompression state that will be modified by this function.
-- @return 0 if succeeds, other value if fails.
local function DecompressStoreBlock(state)
local buffer, buffer_size, ReadBits, ReadBytes, ReaderBitlenLeft,
SkipToByteBoundary, result_buffer = state.buffer, state.buffer_size,
state.ReadBits, state.ReadBytes,
state.ReaderBitlenLeft,
state.SkipToByteBoundary,
state.result_buffer
SkipToByteBoundary()
local bytelen = ReadBits(16)
if ReaderBitlenLeft() < 0 then
return 2 -- available inflate data did not terminate
end
local bytelenComp = ReadBits(16)
if ReaderBitlenLeft() < 0 then
return 2 -- available inflate data did not terminate
end
if (bytelen&255) + (bytelenComp&255) ~= 255 then
return -2 -- Not one's complement
end
if ((bytelen - (bytelen&255)) >> 8) + ((bytelenComp - (bytelenComp&255)) >> 8) ~= 255 then
return -2 -- Not one's complement
end
-- Note that ReadBytes will skip to the next byte boundary first.
buffer_size = ReadBytes(bytelen, buffer, buffer_size)
if buffer_size < 0 then
return 2 -- available inflate data did not terminate
end
-- memory clean up when there are enough bytes in the buffer.
if buffer_size >= 65536 then
result_buffer[#result_buffer + 1] = table_concat(buffer, "", 1, 32768)
for i = 32769, buffer_size do buffer[i - 32768] = buffer[i] end
buffer_size = buffer_size - 32768
buffer[buffer_size + 1] = nil
end
state.buffer_size = buffer_size
return 0
end
-- Decompress a fixed block
-- @param state decompression state that will be modified by this function.
-- @return 0 if succeeds other value if fails.
local function DecompressFixBlock(state)
return DecodeUntilEndOfBlock(state, _fix_block_literal_huffman_bitlen_count,
_fix_block_literal_huffman_to_deflate_code, 7,
_fix_block_dist_huffman_bitlen_count,
_fix_block_dist_huffman_to_deflate_code, 5)
end
-- Decompress a dynamic block
-- @param state decompression state that will be modified by this function.
-- @return 0 if success, other value if fails.
local function DecompressDynamicBlock(state)
local ReadBits, Decode = state.ReadBits, state.Decode
local nlen = ReadBits(5) + 257
local ndist = ReadBits(5) + 1
local ncode = ReadBits(4) + 4
if nlen > 286 or ndist > 30 then
-- dynamic block code description: too many length or distance codes
return -3
end
local rle_codes_huffman_bitlens = {}
for i = 1, ncode do
rle_codes_huffman_bitlens[_rle_codes_huffman_bitlen_order[i]] = ReadBits(3)
end
local rle_codes_err, rle_codes_huffman_bitlen_counts, rle_codes_huffman_symbols, rle_codes_huffman_min_bitlen = GetHuffmanForDecode(rle_codes_huffman_bitlens, 18, 7)
if rle_codes_err ~= 0 then -- Require complete code set here
-- dynamic block code description: code lengths codes incomplete
return -4
end
local lcodes_huffman_bitlens = {}
local dcodes_huffman_bitlens = {}
-- Read length,literal and distance code length tables
local index = 0
while index < nlen + ndist do
local symbol -- Decoded value
local bitlen -- Last length to repeat
symbol = Decode(rle_codes_huffman_bitlen_counts, rle_codes_huffman_symbols, rle_codes_huffman_min_bitlen)
if symbol < 0 then
return symbol -- Invalid symbol
elseif symbol < 16 then
if index < nlen then
lcodes_huffman_bitlens[index] = symbol
else
dcodes_huffman_bitlens[index - nlen] = symbol
end
index = index + 1
else
bitlen = 0
if symbol == 16 then
if index == 0 then
-- dynamic block code description: repeat lengths
-- with no first length
return -5
end
if index - 1 < nlen then
bitlen = lcodes_huffman_bitlens[index - 1]
else
bitlen = dcodes_huffman_bitlens[index - nlen - 1]
end
symbol = 3 + ReadBits(2)
elseif symbol == 17 then -- Repeat zero 3..10 times
symbol = 3 + ReadBits(3)
else -- == 18, repeat zero 11.138 times
symbol = 11 + ReadBits(7)
end
if index + symbol > nlen + ndist then
-- dynamic block code description:
-- repeat more than specified lengths
return -6
end
while symbol > 0 do -- Repeat last or zero symbol times
symbol = symbol - 1
if index < nlen then
lcodes_huffman_bitlens[index] = bitlen
else
dcodes_huffman_bitlens[index - nlen] = bitlen
end
index = index + 1
end
end
end
if (lcodes_huffman_bitlens[256] or 0) == 0 then
-- dynamic block code description: missing end-of-block code
return -9
end
local lcodes_err, lcodes_huffman_bitlen_counts, lcodes_huffman_symbols, lcodes_huffman_min_bitlen = GetHuffmanForDecode(lcodes_huffman_bitlens, nlen - 1, 15)
-- dynamic block code description: invalid literal,length code lengths,
-- Incomplete code ok only for single length 1 code
if (lcodes_err ~= 0 and (lcodes_err < 0 or nlen ~= (lcodes_huffman_bitlen_counts[0] or 0) + (lcodes_huffman_bitlen_counts[1] or 0))) then
return -7
end
local dcodes_err, dcodes_huffman_bitlen_counts, dcodes_huffman_symbols, dcodes_huffman_min_bitlen = GetHuffmanForDecode(dcodes_huffman_bitlens, ndist - 1, 15)
-- dynamic block code description: invalid distance code lengths,
-- Incomplete code ok only for single length 1 code
if (dcodes_err ~= 0 and (dcodes_err < 0 or ndist ~= (dcodes_huffman_bitlen_counts[0] or 0) + (dcodes_huffman_bitlen_counts[1] or 0))) then
return -8
end
-- Build buffman table for literal,length codes
return DecodeUntilEndOfBlock(state, lcodes_huffman_bitlen_counts,
lcodes_huffman_symbols,
lcodes_huffman_min_bitlen,
dcodes_huffman_bitlen_counts,
dcodes_huffman_symbols, dcodes_huffman_min_bitlen)
end
-- Decompress a deflate stream
-- @param state: a decompression state
-- @return the decompressed string if succeeds. nil if fails.
local function Inflate(state)
local ReadBits = state.ReadBits
local is_last_block
while not is_last_block do
is_last_block = (ReadBits(1) == 1)
local block_type = ReadBits(2)
local status
if block_type == 0 then
status = DecompressStoreBlock(state)
elseif block_type == 1 then
status = DecompressFixBlock(state)
elseif block_type == 2 then
status = DecompressDynamicBlock(state)
else
return nil, -1 -- invalid block type (type == 3)
end
if status ~= 0 then return nil, status end
end
state.result_buffer[#state.result_buffer + 1] = table_concat(state.buffer, "", 1, state.buffer_size)
local result = table_concat(state.result_buffer)
return result
end
function LibDeflate.DecompressDeflate(str)
local state = CreateDecompressState(str)
local result, status = Inflate(state)
if not result then return nil, status end
local bitlen_left = state.ReaderBitlenLeft()
local bytelen_left = (bitlen_left - (bitlen_left&7)) >> 3
return result, bytelen_left
end
function LibDeflate.CompressDeflate(str)
local WriteBits, WriteString, FlushWriter = CreateWriter();
Deflate(WriteBits, WriteString, FlushWriter, str);
local total_bitlen, result = FlushWriter(_FLUSH_MODE_OUTPUT)
local padding_bitlen = ((8 - (total_bitlen&7))&7);
return result, padding_bitlen;
end
function LibDeflate.InitCompressor()
for i = 0, 255 do _byte_to_char[i] = string_char(i) end
local pow = 1
for i = 0, 24 do
_pow2[i] = pow
pow = pow * 2
end
for i = 1, 9 do
_reverse_bits_tbl[i] = {}
for j = 0, _pow2[i + 1] - 1 do
local reverse = 0
local value = j
for _ = 1, i do
reverse = reverse - (reverse&1) + ((((reverse&1) == 1) or ((value&1)) == 1) and 1 or 0)
value = (value - (value&1)) >> 1
reverse = reverse << 1
end
_reverse_bits_tbl[i][j] = (reverse - (reverse&1)) >> 1
end
end
-- The source code is written according to the pattern in the numbers
-- in RFC1951 Page10.
local a = 18
local b = 16
local c = 265
local bitlen = 1
for len = 3, 258 do
if len <= 10 then
_length_to_deflate_code[len] = len + 254
_length_to_deflate_extra_bitlen[len] = 0
elseif len == 258 then
_length_to_deflate_code[len] = 285
_length_to_deflate_extra_bitlen[len] = 0
else
if len > a then
a = a + b
b = b * 2
c = c + 4
bitlen = bitlen + 1
end
local t = len - a - 1 + (b >> 1)
_length_to_deflate_code[len] = (t - (t&((b >> 3)-1))) // (b >> 3) + c
_length_to_deflate_extra_bitlen[len] = bitlen
_length_to_deflate_extra_bits[len] = (t&((b >> 3)-1))
end
end
-- The source code is written according to the pattern in the numbers
-- in RFC1951 Page11.
_dist256_to_deflate_code[1] = 0
_dist256_to_deflate_code[2] = 1
_dist256_to_deflate_extra_bitlen[1] = 0
_dist256_to_deflate_extra_bitlen[2] = 0
local a = 3
local b = 4
local code = 2
local bitlen = 0
for dist = 3, 256 do
if dist > b then
a = a * 2
b = b * 2
code = code + 2
bitlen = bitlen + 1
end
_dist256_to_deflate_code[dist] = (dist <= a) and code or (code + 1)
_dist256_to_deflate_extra_bitlen[dist] = (bitlen < 0) and 0 or bitlen
if b >= 8 then
_dist256_to_deflate_extra_bits[dist] = ((dist - (b >> 1) - 1)&((b >> 2)-1))
end
end
_fix_block_literal_huffman_bitlen = {}
for sym = 0, 143 do _fix_block_literal_huffman_bitlen[sym] = 8 end
for sym = 144, 255 do _fix_block_literal_huffman_bitlen[sym] = 9 end
for sym = 256, 279 do _fix_block_literal_huffman_bitlen[sym] = 7 end
for sym = 280, 287 do _fix_block_literal_huffman_bitlen[sym] = 8 end
_fix_block_dist_huffman_bitlen = {}
for dist = 0, 31 do _fix_block_dist_huffman_bitlen[dist] = 5 end
local status
status, _fix_block_literal_huffman_bitlen_count, _fix_block_literal_huffman_to_deflate_code = GetHuffmanForDecode(_fix_block_literal_huffman_bitlen, 287, 9)
assert(status == 0)
status, _fix_block_dist_huffman_bitlen_count, _fix_block_dist_huffman_to_deflate_code = GetHuffmanForDecode(_fix_block_dist_huffman_bitlen, 31, 5)
assert(status == 0)
_fix_block_literal_huffman_code = GetHuffmanCodeFromBitlen(_fix_block_literal_huffman_bitlen_count, _fix_block_literal_huffman_bitlen, 287, 9)
_fix_block_dist_huffman_code = GetHuffmanCodeFromBitlen(_fix_block_dist_huffman_bitlen_count, _fix_block_dist_huffman_bitlen, 31, 5)
end
end
if Debug and Debug.endFile then Debug.endFile() end
if Debug and Debug.beginFile then Debug.beginFile('MagiLogNLoad') end
--[[
Magi Log 'n Load v1.1
A preload-based save-load system for WC3!
(C) ModdieMads
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Permission is granted to anyone to use this software for personal purposes,
excluding any commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation and public
profiles is required.
2. The Software and any modifications made to it may not be used for the purpose
of training or improving machine learning algorithms, including but not
limited to artificial intelligence, natural language processing, or data
mining. This condition applies to any derivatives, modifications, or updates
based on the Software code. Any usage of the Software in an AI-training
dataset is considered a breach of this License.
3. The Software may not be included in any dataset used for training or improving
machine learning algorithms, including but not limited to artificial
intelligence, natural language processing, or data mining.
3. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
4. This notice may not be removed or altered from any source distribution.
]]
--[[
Documentation has been kept to a minimum following feedback from @Wrda and @Antares.
Further explanation of the system will be provided by Discord messages.
Hit me up on HiveWorkshop's Discord server! @ModdieMads!
--------------------------------
-- | Magi Log 'N Load v1.1 |--
-------------------------------
--> By ModdieMads @ https://www.hiveworkshop.com/members/moddiemads.310879/
- Special thanks to:
- @Adiniz/Imemi, for the opportunity! Check their map: https://www.hiveworkshop.com/threads/azeroth-roleplay.357579/
- @Wrda, for the pioneering work in Lua save-load systems!
- @Trokkin, for the FileIO code!
- @Bribe and @Tasyen, for the Hashtable to Lua table converter!
- @Eikonium, for the invaluable DebugUtils! And the template for this header...
- Haoqian He, for the original LibDeflate!
-------------------------------------------------------------------------------------------------------------------+
| Provides logging and save-loading functionalities. |
| |
| Feature Overview: |
| 1. Save units, items, destructables, terrain tiles, variables, hashtables and more with a single command! |
| 2. The fastest syncing of all available save-load systems, powered by LibDeflate and COBS streams! |
| 3. The fastest game state reconstruction of all available save-load systems, powered by Lua! |
| 4. Save and load transports with units inside, cosmetic changes and per-player table and hashtable entries! |
-------------------------------------------------------------------------------------------------------------------+
--------------------------------------------------------------------------------------------------------------+
| Installation: |
| |
| 1. Open the provided map, and copy-paste the Trigger Editor's MagiLogNLoad folder into your map. |
| 2. Order the script files from top to bottom: MLNL Config, MLNL FileIO, MLNL LibDeflate, MagiLogNload |
| 3. Adjust the settings in the MLNL Config script to fit your needs. |
| 4. Call MagiLogNLoad.Init() JUST AFTER the map has been initialized. |
| |
--------------------------------------------------------------------------------------------------------------+
--------------------------------------------------------------------------------------------------------------------------------------------------------
* Documentation and API-Functions:
*
* - All automatic functionality provided by MagiLogNLoad can be deactivated by disabling the script files.
*
* -------------------------
* | Commands |
* -------------------------
* - "-save <FILE PATH>" creates a save-file at the provided file path. Folders will be created as necessary.
* > Powered by MagiLogNLoad.UpdateSaveableVarsForPlayerId() and MagiLogNLoad.CreatePlayerSaveFile().
* > Can be altered in the config file.
*
* - "-load <FILE NAME>" loads a save-file located at the provided file path.
* > Powered by MagiLogNLoad.QueuePlayerLoadCommand() and MagiLogNLoad.SyncLocalSaveFile().
* > Can be altered in the config file.
*
* ----------------------------
* | GUI Variables |
* ----------------------------
* - Check the provided map (MagiLogNLoad1010.w3x) for more information.
*
* - udg_mlnl_Modes < String Array >
* > Powered by MagiLogNLoad.DefineModes()
*
* - udg_mlnl_MakeProxyTable < String Array >
* > Powered by MakeProxyTable()
*
* - udg_mlnl_WillSaveThis < String Array >
* > Powered by MagiLogNLoad.WillSaveThis()
*
* - udg_mlnl_LoggingPlayer < Player Array >
* > Powered by MagiLogNLoad.SetLoggingPlayer()
*
* ------------------
* | API |
* ------------------
*
* MagiLogNLoad.Init(_debug, _skipInitGUI)
* - Initialization function. Required to make the system operational.
* - Args ->
* > _debug: Enables the system to initialize with the Debug mode enabled.
* > _skipInitGUI: Skip initializing the GUI functions. Use this if your map doesn't use any GUI triggers.
* - [IMPORTANT] When initializing the GUI functions, the system overrides the following natives:
* > _G.StringHashBJ, _G.StringHash
* > _G.GetHandleIdBJ, _G.GetHandleId
* > All Hashtable natives
*
* MagiLogNLoad.CreatePlayerSaveFile(p, fileName) < SYNC + ASYNC >
* - Updates all referenced values (sync) then starts the process of saving a file if <p> is the local player (async).
* - Args ->
* > fileName: name of the file that will be created.
* > player: player whose data will be saved.
* - Step 1: Creates and serializes all necessary logs.
* - Step 2: Compresses the serialized logs using LibDeflate.
* - Step 3: The compressed byte-stream within the string is encoded with Constant Overhead Byte Stuffing.
* - Step 4: The encoded string is converted to uft8's ideogram space.
* - Step 5: The utf8 string is written to the save-file by FileIO.
*
* MagiLogNLoad.SyncLocalSaveFile(fileName) < ASYNC >
* - Begins the process of loading a file for the local player. Only call this function inside a GetLocalPlayer() block.
* - Warning! Calling this while someone is syncing a file might lead to a system failure. Consider MagiLogNLoad.QueuePlayerLoadCommand(p, fileName).
* - Step 1: Reads the save-file and parses it as a byte-stream within a string.byte
* - Step 2: Creates the sync-stream struct to track the syncing process, then initiates it.
* - Step 3: After all chunks are received by the players, calls MagiLogNLoad.LoadPlayerSaveFromString()
*
* MagiLogNLoad.LoadPlayerSaveFromString(p, argStr, startTime)
* - Reconstructs the game state according to the instructions encoded in the passed string.
* - Step 1: Decodes the string using COBS
* - Step 2: Decompresses the string using LibDeflate
* - Step 3: Sanitizes the string to mitigate attacks (shoutouts to Ozzzymaniac)
* - Step 4: Deserializes the string back into log tables.
* - Step 5: Traverses the logs and reconstructs the game state along the way.
* - Step 6: If there are commands in the loading queue, execute the next one on the queue.
* - Args -> self explanatory
*
* MagiLogNLoad.QueuePlayerLoadCommand(p, fileName)
* - Queues a loading command. Use to prevent network jamming.
* - The queue is processed automatically.
* - Args -> self explanatory
*
* MagiLogNLoad.UpdateSaveableVarsForPlayerId(pid)
* - Updates the log entries concerning variables marked as saveable by MagiLogNLoad.WillSaveThis.
* - When saving a file, call this first in a sync context (outside a GetLocalPlayer() block).
*
* MagiLogNLoad.WillSaveThis(argName, willSave)
* - Registers a global/public variable as saveable/non-saveable. This registry is used when executing a save command.
* - Variables registered this way are saved regardless of the modes enabled. Great for exceptions and overrides!
* - If the variable contains a (hash)table, they will use a proxy to record each change made to them as individual log entries.
* - This means that properties in a (hash)table can be singled out (per player) and saved without needing to save the whole table.
* - Args ->
* - argName: Name of the variable. Must be the string that returns the intended variable when used in _G[argName].
* - willSave: true means the variable will be saved, false means that it won't.
* - Changing a (hash)table from unsaveable to saveable will still save the modifications made to the table when it was saveable.
* - To erase all log entries about a (hash)table, change the value assigned to the variable to something else (nil).
*
* MagiLogNLoad.HashtableSaveInto(value, childKey, parentKey, whichHashTable)
* - Saves a value into a GUI hashtable. Necessary due to the usage of Bribe and Tasyen's Hashtable to Lua table converter.
* - Args -> self explanatory
*
* MagiLogNLoad.DefineModes(_modes)
* - Define the modes used by the system.
* - Available modes:
* - debug
* > Enable Debug prints. Very recommended.
* - saveNewUnitsOfPlayer
* > Save units created at runtime if they are owned by the saving player.
* - savePreplacedUnitsOfPlayer
* > Save changes to pre-placed units owned by the saving player. Might lead to conflicts in multiplayer.
* - saveUnitsDisownedByPlayers
* > Save units disowned by a player if the owner at the time of saving is one of the Neutral players.
* - saveStandingPreplacedDestrs
* > Save pre-placed trees/destructables. Might lead to conflicts in multiplayer.
* > For the sake of good performance, the destruction of destrs is only saved if caused by the use of Remove/KillDestructable().
* - saveDestroyedPreplacedDestrs
* > Save trees/destructables that are killed by attacks or spells.
* > Needs to have a loggingPid set for the logging to occur.
* > If the map has too many trees/destrs, its performance might suffer.
* - savePreplacedItems
* > Save changes to pre-placed items. Might lead to conflicts in multiplayer.
* > If the item is destroyed or removed, its destruction will be recorded and reproduced when loaded.
* - saveItemsDroppedManually
* > Save items on the ground ONLY if a player's unit dropped them.
* > Won't save items dropped by creeps or Neutral player's units.
* - saveAllItemsOnGround
* > Adds all items on the ground to the saving player's save-file.
* - Args ->
* - _modes: Table of the format { modeName1 = true, modeName2 = true, ...}
* - Example: { savePreplacedUnitsOfPlayer = true, saveNewUnitsOfPlayer = true }
*
* MagiLogNLoad.SetLoggingPlayer(p), MagiLogNLoad.ResetLoggingPlayer(), MagiLogNLoad.GetLoggingPlayerId()
* - Defines, resets and returns the PlayerId that will be used by the system to determine in which player's logs the incoming changes will be entered.
* - This only applies to changes that cannot be automatically attributed to a player. Examples:
* - Remove/CreateDestructable(), destructable deaths
* - Remove/CreateItem(), item deaths
* - Modifying global variables.
* - And many more...
* - In single-player maps, it's recommended to just call MagiLogNLoad.SetLoggingPlayer(GetLocalPlayer()) at the beginning and forget about it.
* - In multiplayer maps, you will need to manually set and reset the Logging Player when necessary to keep the logs accurate.
*
* MagiLogNLoad.HashtableLoadFrom(childKey, parentKey, whichHashTable, default)
* - Loads a value from a GUI hashtable. Necessary due to the usage of Bribe and Tasyen's Hashtable to Lua table converter.
* - Args -> self explanatory
*
* MagiLogNLoad.HashtableLoadFrom(childKey, parentKey, whichHashTable, default)
* - Loads a value from a GUI hashtable. Necessary due to the usage of Bribe and Tasyen's Hashtable to Lua table converter.
* - Args -> self explanatory
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
* CHANGELOG:
* 1.10
* - [FEATURE] MagiLogNLoad can now unload units from transports seamlessly when loading a save-file.
* > Maps using this system require the custom ability 'AUNL' (customized 'Adri').
* > Please refer to the provided test map and the MLNL_Config file.
*
* - [BREAKING] MagiLogNLoad.CreatePlayerSaveFile has replaced MagiLogNLoad.SaveLocalPlayer.
* > YOU MUST UPDATE ALL YOUR <MagiLogNLoad.SaveLocalPlayer> FUNCTION CALLS.
* > It's now sync while updating references, then it becomes async when saving the actual file.
*
* - [BREAKING] Mode <savePreplacedUnits> has been renamed to <savePreplacedUnitsOfPlayer>
* > GUI config string has been changed to SAVE_PREPLACED_UNITS_OF_PLAYER. Please update your triggers.
* > Functionality unchanged.
*
* - [BREAKING] Mode <saveAllUnitsOfPlayer> has been renamed to <saveNewUnitsOfPlayer>.
* > GUI config string has been changed to SAVE_NEW_UNITS_OF_PLAYER. Please update your triggers.
* > Enabling this will only saved units that have been created at runtime.
*
* - [BREAKING] Mode <saveDestrsKilledByUnits> has been renamed to <saveDestroyedPreplacedDestrs>.
* > GUI config string has been changed to SAVE_DESTROYED_PREPLACED_DESTRS. Please update your triggers.
* > Functionality unchanged.
*
* - [BREAKING] MagiLogNLoad.ALL_CARGO_ABILS has been replaced by MagiLogNLoad.ALL_TRANSP_ABILS
* > It now contains 2 arrays: CARGO, with all <Cargo Hold> abils in the map; and LOAD, with all <Load> abils in the map.
* > All units are assumed to have just one abil of each category.
*
* - Added the mode <saveDestroyedPreplacedUnits>.
* > Save ALL destroyed pre-placed units as long as a logging player is defined when the destruction happens.
* - Added a way to change the commands "-save" and "-load" to arbitrary strings in the config file.
* - At the request of @Antares, added the following callback functions to ease integration and message editing:
* > MagiLogNLoad.onSaveStarted
* > MagiLogNLoad.onSaveFinished
* > MagiLogNLoad.onLoadStarted
* > MagiLogNLoad.onLoadFinished
* > MagiLogNLoad.onSyncProgressCheckpoint
* > MagiLogNLoad.onAlreadyLoadedWarning
* > MagiLogNLoad.onMaxLoadsError
*
* 1.09
* - Fixed a bunch of linting bugs thanks to @Tomotz
* - Fixed a bug preventing huge reals from being saved correctly. S2R and R2S are still unstable.
*
* 1.08
* - Added parallel bursting sync streams. Load times can be up to 50x faster.
* - Added filename validation to prevent users from shooting themselves in the foot.
* - Added internal state monitoring capabilities to the system. Errors should be much easier to track.
* - Added the option for the map-maker to set a time limit for loading and syncing. The loading will fail in case it's exceeded.
* - Added progress prints for file loading. They should show up at 25, 50 and 75% loading progress.
* - Changed the default behavior for MagiLogNLoad.WillSaveThis. Ommitting the second argument will default to <true>.
* - Fixed a possible jamming desync when multiple players are syncing at the same time.
* - Fixed a bug involving some weird Blizzard functions, like SetUnitColor.
* - Fixed a bug involving unit groups that could cause saves to fail.
* - Fixed a bug involving proxied tables that could cause saves to fail.
* - Fixed a bug causing real variables to sometimes save as 10x their normal value.
* - Fixed a bug causing research/upgrade entries to sometimes not load correctly.
----------------------------------------------------------------------------------------------------------------------------------------------
]]
do
MagiLogNLoad = MagiLogNLoad or {};
MagiLogNLoad.engineVersion = 1100;
local unpack = table.unpack;
local concat = table.concat;
local remove = table.remove;
local string_byte = string.byte;
local string_char = string.char;
local math_floor = math.floor;
local function TableToConsole(tab, indent) --
indent = indent or 0;
local toprint = '{\r\n';
indent = indent + 2 ;
for k, v in pairs(tab) do
toprint = toprint .. string.rep(' ', indent);
if (type(k) == 'number') then
toprint = toprint .. '[' .. k .. '] = ';
elseif (type(k) == 'string') then
toprint = toprint .. k .. ' = ';
end
if (type(v) == 'number') then
toprint = toprint .. v .. ',\r\n';
elseif (type(v) == 'string') then
toprint = toprint .. '\'' .. v .. '\',\r\n';
elseif (type(v) == 'table') then
toprint = toprint .. TableToConsole(v, indent + 2) .. ',\r\n';
else
toprint = toprint .. '\'' .. tostring(v) .. '\',\r\n';
end
end
toprint = toprint .. string.rep(' ', indent-2) .. '}';
return toprint;
end
local function Nil()
return nil;
end
local function B2I(val)
return val and 1 or 0;
end
local function I2B(val)
return val ~= 0;
end
local function UTF8Codes2Real(...)
return tonumber(utf8.char(...));
end
local function NakedReturn(a) return a end
local function Round(val)
return math_floor(val + .5);
end
local function FourCC2Str(num)
return string.pack('>I4', num);
end
local function Range(i0,i1)
local ans = {};
local n = 0;
for i=i0,i1 do
n = n + 1;
ans[n] = i;
end
return ans;
end
local function Map(arr, f, ind0, ind1)
ind0 = ind0 or 1;
ind1 = ind1 and (ind1 <= 0 and #arr + ind1 or ind1) or #arr;
ind1 = ind1 < ind0 and ind0 or ind1;
local ans = {};
for i=ind0,ind1 do
ans[#ans+1] = f(arr[i], i);
end
return ans;
end
local function Concat7BitPair(a, b)
return ((a-1) << 7) | (b-1);
end
local function Lerp(v0, v1, t)
return v0 + t*(v1-v0);
end
local function Terp(v0, v1, v)
return (v-v0)/(v1-v0);
end
local function Clamp(v, v0, v1)
return v < v0 and v0 or (v > v1 and v1 or v);
end
local function Vec2Add_inPlace(v0, v1)
v0[1] = v0[1]+v1[1];
v0[2] = v0[2]+v1[2];
return v0;
end
local function ArrayIndexOfPlucked(arr, val, pluckInd)
for i,v in ipairs(arr) do
if v[pluckInd] == val then return i end;
end
return -1;
end
local function PluckArray(arr, propName)
local ans = {};
for i,v in ipairs(arr) do
ans[i] = v[propName];
end
return ans;
end
local function Array2Hash(arr, fromInd, toInd, nilVal)
fromInd = fromInd or 1;
toInd = toInd or #arr;
local ans = {};
for i=fromInd,toInd do
if arr[i] ~= nil and arr[i] ~= nilVal then
ans[arr[i] ] = i;
end
end
return ans;
end
local function TableInvert(tab)
local ans = {};
for k,v in pairs(tab) do
ans[v] = k;
end
return ans;
end
local int2Function = {};
local PERC = utf8.char(37);
local neutralPIds = {
[PLAYER_NEUTRAL_AGGRESSIVE] = true,
[PLAYER_NEUTRAL_PASSIVE] = true,
[bj_PLAYER_NEUTRAL_VICTIM] = true,
[bj_PLAYER_NEUTRAL_EXTRA] = true
};
local ENUM_RECT = {};
local enumSingleX = 1.0;
local enumSingleY = 1.0;
local enumSingleMinDist = 1e20;
local enumSingleId = 0;
local enumSingle;
local BOARD_ORDER_ID = 852043;
local UNLOAD_INSTA_ORDER_ID = 852049;
local MAX_FOURCC = 2139062143;
local MIN_FOURCC = 16843009;
local sanitizer = '';
local fileNameSanitizer = '';
local TICK_DUR = .035;
local mainTimer = {};
local mainTick = 1;
local onLaterTickFuncs = {};
local WORLD_BOUNDS = {};
MagiLogNLoad.ALL_TRANSP_ABILS = MagiLogNLoad.ALL_TRANSP_ABILS or {
LOAD = {FourCC('Aloa'), FourCC('Sloa'), FourCC('Slo2'), FourCC('Slo3'), FourCC('Aenc')},
CARGO = {FourCC('Sch3'), FourCC('Sch4'), FourCC('Sch5'), FourCC('Achd'), FourCC('Abun'), FourCC('Achl')}
};
MagiLogNLoad.BASE_STATES = {
OFF = 0,
STANDBY = 1,
CLEANING_TABLES = 2
};
MagiLogNLoad.SAVE_STATES = {
SAVE_CMD_ENTERED = 100,
UPDATE_SAVEABLE_VARS = 110,
CREATE_PLAYER_SAVEFILE = 120,
CREATE_UNITS_OF_PLAYER_LOG = 130,
CREATE_ITEMS_OF_PLAYER_LOG = 140,
CREATE_EXTRAS_OF_PLAYER_LOG = 150,
CREATE_SERIAL_LOGS = 160,
COMPRESSING_LOGS = 170,
FILE_IO_SAVE_FILE = 180,
PRINT_TALLY = 190
};
MagiLogNLoad.LOAD_STATES = {
LOAD_CMD_ENTERED = 500,
QUEUE_LOAD = 510,
SYNC_LOCAL_SAVE_FILE = 520,
SETTING_START_OSCLOCK = 530,
FILE_IO_LOAD_FILE = 540,
GET_LOG_STR = 550,
CREATE_UPLOADING_STREAM = 560,
CREATE_MESSAGES = 570,
SEND_SYNC_DATA = 580,
SYNC_EVENT = 590,
GETTING_MANIFESTS = 600,
LOADING_LOGS = 605,
SUCCESFUL_LOADING = 610,
PRINT_TALLY = 620
};
MagiLogNLoad.stateOfPlayer = {};
local word2CodeHash = {};
local code2WordHash = {};
local word2LoadLogFuncHash = {};
local fCodes = {};
local logEntry2ReferencedHash = {};
local typeStr2TypeIdGetter;
local typeStr2GetterFCode;
local typeStr2ReferencedArraysHash;
local fCodeFilters = {};
local tempGroup, tempPlayerId;
local varName2ProxyTableHash = {};
local saveables = {
groups = {},
vars = {},
hashtables = {}
};
local loggingPid = -1;
local playerLoadInfo = {};
local function GetHandleTypeStr(handle)
local str = tostring(handle);
return str:sub(1, (str:find(':', nil, true) or 0) - 1);
end
local StringTrim_CONST0 = '^'..PERC..'s*(.-)'..PERC..'s*$';
local StringTrim_CONST1 = PERC..'1';
local function StringTrim(s)
return s:gsub(StringTrim_CONST0, StringTrim_CONST1);
end
local function GetUTF8Codes(str)
local ans = {};
local n = 0;
for _, c in utf8.codes(str) do
n = n+1;
ans[n] = c;
end
return ans;
end
local function GetCharCodesFromUTF8(str)
local ans = {};
local ind = 0;
for _, c in utf8.codes(str) do
ind = ind + 1;
ans[ind] = c >> 8;
ind = ind + 1;
ans[ind] = c&255;
end
ans[ind-ans[ind]] = nil;
ans[ind-1] = nil;
ans[ind] = nil;
return ans;
end
local function String2SafeUTF8(str)
local utf8_char = utf8.char;
local strlen = #str;
local arr = {string_byte(str, 1, strlen)};
local ans = {};
for i=0,(strlen >> 1)+(strlen&1)-1 do
ans[i+1] = utf8_char((arr[i+i+1] << 8) + (arr[i+i+2] or 255));
end
ans[#ans+1] = utf8_char((strlen&1)+1);
return concat(ans);
end
local function COBSEscape(str)
local STR0 = string_char(0);
local STR255 = string_char(255);
local len = #str;
local ind0 = 1;
local ans = {};
local ansN = 0;
while ind0 <= len do
local ind1 = ind0+253 > len and len - ind0 + 1 or 254;
local substr = str:sub(ind0, ind0 + ind1);
ind0 = ind0 + ind1;
local lastHit = 0;
local hitInd = substr:find(STR0, lastHit + 1, true);
while hitInd do
ansN = ansN + 1;
ans[ansN] = string_char(hitInd - lastHit);
if hitInd - lastHit > 1 then
ansN = ansN + 1;
ans[ansN] = substr:sub(lastHit+1, hitInd-1);
end
lastHit = hitInd;
hitInd = lastHit < ind1 and substr:find(STR0, lastHit + 1, true) or nil;
end
if lastHit <= ind1 then
ansN = ansN + 1;
ans[ansN] = STR255;
ansN = ansN + 1;
ans[ansN] = substr:sub(lastHit+1, ind1);
end
end
return concat(ans);
end
local function COBSDescape(str)
local STR0 = string_char(0);
local len = #str;
local ind = 1;
local ans = {};
local ansN = 0;
while ind <= len do
local substrLen = ind+254 > len and len - ind + 1 or 255;
local substr = str:sub(ind, ind + substrLen);
ind = ind + substrLen;
local lastHit = 1;
local hitInd = string_byte(substr, lastHit, lastHit);
while lastHit + hitInd <= substrLen do
ansN = ansN + 1;
ans[ansN] = substr:sub(lastHit+1, lastHit+hitInd-1);
lastHit = lastHit + hitInd;
ansN = ansN + 1;
ans[ansN] = STR0;
hitInd = string_byte(substr, lastHit, lastHit);
end
if lastHit < substrLen then
ansN = ansN + 1;
ans[ansN] = substr:sub(lastHit+1, substrLen);
end
end
return concat(ans);
end
local oriG = {};
local function PrependFunction(funcName, prepend)
local upfunc = _G[funcName];
oriG[funcName] = upfunc;
_G[funcName] = function(...)
prepend(...);
return upfunc(...);
end
end
local function WrapFunction(funcName, wrapF)
local upfunc = _G[funcName];
oriG[funcName] = upfunc;
_G[funcName] = function(...)
return wrapF(upfunc(...), ...);
end
end
local serializeIntTableFuncs = {};
local function SerializeIntTable(x)
return serializeIntTableFuncs[type(x)](x);
end
serializeIntTableFuncs = {
['number'] = function(v)
return tostring(v);
end,
['table'] = function(t)
local rtn = {};
local rtnN = 0;
for _,v in ipairs(t) do
rtnN = rtnN + 1;
rtn[rtnN] = SerializeIntTable(v);
end
return '{' .. concat(rtn, ',') .. '}';
end
};
local function Deserialize(str)
return load('return ' .. str)();
end
local deconvertHash = {
[PLAYER_COLOR_RED] = 0,
[PLAYER_COLOR_BLUE] = 1,
[PLAYER_COLOR_CYAN] = 2,
[PLAYER_COLOR_PURPLE] = 3,
[PLAYER_COLOR_YELLOW] = 4,
[PLAYER_COLOR_ORANGE] = 5,
[PLAYER_COLOR_GREEN ] = 6,
[PLAYER_COLOR_PINK] = 7,
[PLAYER_COLOR_LIGHT_GRAY] = 8,
[PLAYER_COLOR_LIGHT_BLUE] = 9,
[PLAYER_COLOR_AQUA] = 10,
[PLAYER_COLOR_BROWN ] = 11,
[PLAYER_COLOR_MAROON] = 12,
[PLAYER_COLOR_NAVY] = 13,
[PLAYER_COLOR_TURQUOISE ] = 14,
[PLAYER_COLOR_VIOLET] = 15,
[PLAYER_COLOR_WHEAT ] = 16,
[PLAYER_COLOR_PEACH ] = 17,
[PLAYER_COLOR_MINT] = 18,
[PLAYER_COLOR_LAVENDER] = 19,
[PLAYER_COLOR_COAL] = 20,
[PLAYER_COLOR_SNOW] = 21,
[PLAYER_COLOR_EMERALD] = 22,
[PLAYER_COLOR_PEANUT] = 23,
[UNIT_STATE_LIFE] = 0,
[UNIT_STATE_MAX_LIFE] = 1,
[UNIT_STATE_MANA] = 2,
[UNIT_STATE_MAX_MANA] = 3,
[RARITY_FREQUENT] = 0,
[RARITY_RARE] = 1,
[PLAYER_STATE_RESOURCE_GOLD] = 1,
[PLAYER_STATE_RESOURCE_LUMBER] = 2
};
local logGen = {
terrain = 0,
destrs = 0,
units = 0,
items = 0,
res = 0,
proxy = 0,
groups = 0,
extras = 256,
vars = 0,
hashtable = 0,
streams = 1
};
local magiLog = {
terrainOfPlayer = {},
researchOfPlayer = {},
proxyTablesOfPlayer = {},
hashtablesOfPlayer = {},
varsOfPlayer = {},
destrsOfPlayer = {},
referencedDestrsOfPlayer = {},
items = {},
itemsOfPlayer = {},
referencedItemsOfPlayer = {},
units = {},
unitsOfPlayer = {},
formerUnits = {},
referencedUnitsOfPlayer = {},
extrasOfPlayer = {}
};
local tabKeyCleaner, tabKeyCleanerSize, lastCleanedTab;
-- Example values. These must always be overriden during initialization.
MagiLogNLoad.modes = {
debug = true,
savePreplacedUnitsOfPlayer = false,
saveDestroyedPreplacedUnits = false,
saveStandingPreplacedDestrs = true,
savePreplacedItems = false,
saveNewUnitsOfPlayer = true,
saveUnitsDisownedByPlayers = true,
saveItemsDroppedManually = true,
saveAllItemsOnGround = false,
saveDestroyedPreplacedDestrs = false
};
local function PrintDebug(...)
if MagiLogNLoad.modes.debug then
print(...);
end
end
local guiModes;
local modalTriggers = {};
local issuedWarningChecks = {
saveInPreplacedTransp = false,
saveTransformedPreplacedUnit = false,
tabKeyCleanerPanic = false,
initPanic = false,
loadTimeWarning = false,
syncProgressCheckpoints = {}
};
local signals = {
abortLoop = nil
};
local preplacedWidgets;
local unit2TransportHash = {};
local unit2HiddenStat = {};
local tempIsGateHash = setmetatable({}, { __mode = 'k' });
local streamsOfPlayer = {};
local uploadingStream = nil;
local downloadingStreamFrom;
local loadingQueue = {};
local logItems;
local logItemsN = 0;
local function GetLoggingItem(ind)
return ind and logItems[ind] or logItems[logItemsN];
end
local logDestrs;
local logDestrsN = 0;
local function GetLoggingDestr(ind)
return ind and logDestrs[ind] or logDestrs[logDestrsN];
end
local logUnits;
local logUnitsN = 0;
local function GetLoggingUnit(ind)
return ind and logUnits[ind] or logUnits[logUnitsN];
end
local function GetLoggingUnitSafe0(ind)
local u = ind and logUnits[ind] or logUnits[logUnitsN];
return (u or 0);
end
local function GetLoggingUnitsSafe0(...)
return Map({...}, GetLoggingUnitSafe0);
end
local function Div10000(val)
return val/10000.;
end
local function XY2Index30(x, y)
return (Clamp(math_floor(Terp(WORLD_BOUNDS.minX, WORLD_BOUNDS.maxX, x)*32767.), 0, 32767) << 15) |
(Clamp(math_floor(Terp(WORLD_BOUNDS.minY, WORLD_BOUNDS.maxY, y)*32767.), 0, 32767));
end
local function XYZ2Index30(x, y, z)
return (Clamp(math_floor(Terp(WORLD_BOUNDS.minX, WORLD_BOUNDS.maxX, x)*1023.), 0, 1023) << 20) |
(Clamp(math_floor(Terp(WORLD_BOUNDS.minY, WORLD_BOUNDS.maxY, y)*1023.), 0, 1023) << 10) |
(Clamp(math_floor(Terp(WORLD_BOUNDS.minX, WORLD_BOUNDS.maxX, z)*1023.), 0, 1023));
end
local function XYZW2Index32(x, y, z, w)
return (Clamp(math_floor(Terp(WORLD_BOUNDS.minX, WORLD_BOUNDS.maxX, x)*255.), 0, 255) << 24) |
(Clamp(math_floor(Terp(WORLD_BOUNDS.minY, WORLD_BOUNDS.maxY, y)*255.), 0, 255) << 16) |
(Clamp(math_floor(Terp(WORLD_BOUNDS.minX, WORLD_BOUNDS.maxX, z)*255.), 0, 255) << 8) |
(Clamp(math_floor(Terp(WORLD_BOUNDS.minY, WORLD_BOUNDS.maxY, w)*255.), 0, 255));
end
local function InvertHash(hash)
local ans = {};
for k,v in pairs(hash) do
if v ~= nil then
ans[v] = k;
end
end
return ans;
end
local function TableGetDeep(tab, map)
local pt = tab;
for _,v in ipairs(map) do
pt = pt[v];
if pt == nil then return nil end;
end
return pt;
end
local function TableSetDeep(tab, val, map)
local pt = tab;
local N = #map;
for i=1,N-1 do
local v = map[i];
if not pt[v] then
if val == nil then return end;
pt[v] = {};
end
pt = pt[v];
end
pt[map[N]] = val;
end
local function TableSetManyPaired(keys, vals, tab)
local ans = tab or {};
for i, v in ipairs(keys) do
ans[v] = vals[i];
end
return ans;
end
local function SortLog(a, b)
return a[1] < b[1] or (a[1] == b[1] and a[4] and b[4] and a[4] < b[4]); -- because of LoadGroupAddUnit entries
end
local function IsUnitGone(u)
return GetUnitTypeId(u) == 0 or IsUnitType(u, UNIT_TYPE_DEAD);
end
local function LogBaseStatsByType(u)
-- Legacy function. Only here to not break things.
end
local function ResetTransportAfterLoad(transp, oriCastRanges, cargoUnits, oriMoveSpeeds, oriFacing)
local j = 0;
for _,v in pairs(MagiLogNLoad.ALL_TRANSP_ABILS) do
for _,abilid in ipairs(v) do
local lvl = GetUnitAbilityLevel(transp, abilid);
if lvl > 0 then
lvl = lvl -1;
j = j + 1;
BlzSetAbilityRealLevelField(BlzGetUnitAbility(transp, abilid), ABILITY_RLF_CAST_RANGE, lvl, oriCastRanges[j]);
break;
end
end
end
for i, v in ipairs(cargoUnits) do
oriG.SetUnitMoveSpeed(v, oriMoveSpeeds[i] or 298);
local s = unit2HiddenStat[v] and unit2HiddenStat[v].scale or BlzGetUnitRealField(v, UNIT_RF_SCALING_VALUE);
oriG.SetUnitScale(v, s,s,s);
if GetUnitAbilityLevel(v, MagiLogNLoad.LOAD_GHOST_ID) > 0 then
oriG.UnitRemoveAbility(v, MagiLogNLoad.LOAD_GHOST_ID);
end
end
BlzSetUnitFacingEx(transp, oriFacing or 0);
end
local function ForceLoadUnits(transp, units)
if transp == nil or IsUnitGone(transp) then
PrintDebug('|cffff5500MLNL Error:ForceLoadUnits!', 'Invalid transport unit detected when trying to load!');
return;
end
local oriCastRanges = {};
for _,v in pairs(MagiLogNLoad.ALL_TRANSP_ABILS) do
for _,abilid in ipairs(v) do
local lvl = GetUnitAbilityLevel(transp, abilid);
if lvl > 0 then
local abil = BlzGetUnitAbility(transp, abilid);
lvl = lvl-1;
oriCastRanges[#oriCastRanges+1] = BlzGetAbilityRealLevelField(abil, ABILITY_RLF_CAST_RANGE, lvl)
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_CAST_RANGE, lvl, 99999999.);
break;
end
end
end
local oriFacing = GetUnitFacing(transp);
local transpX, transpY = GetUnitX(transp), GetUnitY(transp);
local oriMoveSpeeds = {};
for i,v in ipairs(units) do
if v ~= 0 then
oriMoveSpeeds[i] = GetUnitMoveSpeed(v);
oriG.SetUnitMoveSpeed(v, 0);
if not unit2HiddenStat[v] then
unit2HiddenStat[v] = {};
end
if not unit2HiddenStat[v].scale then
unit2HiddenStat[v].scale = BlzGetUnitRealField(v, UNIT_RF_SCALING_VALUE)
end
oriG.SetUnitScale(v, 0,0,0);
SetUnitX(v, transpX);
SetUnitY(v, transpY);
IssueTargetOrderById(v, BOARD_ORDER_ID, transp);
BlzQueueTargetOrderById(v, BOARD_ORDER_ID, transp);
BlzQueueTargetOrderById(v, BOARD_ORDER_ID, transp);
BlzQueueTargetOrderById(v, BOARD_ORDER_ID, transp);
end
end
onLaterTickFuncs[#onLaterTickFuncs+1] = {mainTick + 30, ResetTransportAfterLoad, {transp, oriCastRanges, units, oriMoveSpeeds, oriFacing}};
end
local function UpdateHeroStats(hero, str, agi, int)
local val = GetHeroStr(hero, false);
if val ~= str then
SetHeroStr(hero, str, true);
end
val = GetHeroAgi(hero, false);
if val ~= agi then
SetHeroAgi(hero, agi, true);
end
val = GetHeroInt(hero, false);
if val ~= int then
SetHeroInt(hero, int, true);
end
end
local function UpdateUnitStats(u, maxhp, hp, maxMana, mana, baseDmg)
if BlzGetUnitMaxHP(u) ~= maxhp then
BlzSetUnitMaxHP(u, maxhp);
end
SetUnitState(u, UNIT_STATE_LIFE, hp);
if maxMana > 0 then
if BlzGetUnitMaxMana(u) ~= maxMana then
BlzSetUnitMaxMana(u, maxMana);
end
SetUnitState(u, UNIT_STATE_MANA, mana);
end
if BlzGetUnitBaseDamage(u, 0) ~= baseDmg then
BlzSetUnitBaseDamage(u, baseDmg, 0);
end
end
local function LoadSetUnitFlyHeight(u, height, rate)
local Amrf_4CC = FourCC('Amrf');
if BlzGetUnitMovementType(u) ~= 2 and GetUnitAbilityLevel(u, Amrf_4CC) <= 0 then
oriG.UnitAddAbility(u, Amrf_4CC);
oriG.UnitRemoveAbility(u, Amrf_4CC);
end
local isBuilding = BlzGetUnitBooleanField(u, UNIT_BF_IS_A_BUILDING);
if isBuilding then
BlzSetUnitBooleanField(u, UNIT_BF_IS_A_BUILDING, false);
end
SetUnitFlyHeight(u, height, rate);
SetUnitPosition(u, GetUnitX(u), GetUnitY(u));
if isBuilding then
BlzSetUnitBooleanField(u, UNIT_BF_IS_A_BUILDING, true);
end
end
local function LoadSetUnitArmor(u, armorDif)
BlzSetUnitArmor(u, (BlzGetUnitArmor(u)*10 + armorDif)*.1);
end
local function LoadSetUnitMoveSpeed(u, speedDif)
SetUnitMoveSpeed(u, speedDif);
end
local function LoadSetWaygate(u, x, y, isActive)
WaygateSetDestination(u, x, y);
WaygateActivate(u, isActive);
end
local function UnpackLogEntry(tab)
local ans = {};
local ansN = 0;
for _,v in ipairs(tab) do
if type(v) == 'table' then
ansN = ansN + 1;
ans[ansN] = #v == 1 and int2Function[v[1]]() or int2Function[v[1]](unpack(v[2]));
else
ansN = ansN + 1;
ans[ansN] = v;
end
end
return unpack(ans);
end
local function LoadItem(log)
local entriesN = 0;
local lastId = 0;
for i,v in ipairs(log) do
local curId = v[1];
if lastId > curId then
PrintDebug('|cffff5500MLNL Error:LoadItem!', 'Bad save-file detected while loading ITEM #',i,'!|r');
return false;
end
lastId = curId;
int2Function[v[2]](UnpackLogEntry(v[3]));
if signals.abortLoop == LoadItem then
signals.abortLoop = nil;
break;
end
entriesN = entriesN + 1;
end
return entriesN;
end
local function LoadCreateItem(itemid, x, y, charges, pid)
local item = CreateItem(itemid, x, y);
if not item then
PrintDebug('|cffff5500MLNL Error:LoadCreateItem!', 'Failed to create item with id:',FourCC2Str(itemid),'!|r');
signals.abortLoop = LoadItem;
return nil;
end
if GetItemCharges(item) ~= charges then
SetItemCharges(item, charges);
end
magiLog.items[item] = {pid};
end
local function LoadSetPlayerState(p, goldVal, lumberVal)
SetPlayerState(p, PLAYER_STATE_RESOURCE_GOLD, goldVal);
SetPlayerState(p, PLAYER_STATE_RESOURCE_LUMBER, lumberVal);
end
local function GetPreplacedUnit(...)
return TableGetDeep(preplacedWidgets.unitMap, {...});
end
local function GetPreplacedItem(itemid, slot, x, y, unitid, unitOwnerId)
if slot == -1 then
return TableGetDeep(preplacedWidgets.itemMap, {itemid, slot, x, y});
end
local u = GetPreplacedUnit(x,y,unitid,unitOwnerId);
if not u then return nil end;
local item = UnitItemInSlot(u, slot);
return itemid == GetItemTypeId(item) and item or nil;
end
local function EnumGetSingleDestr()
local destr = GetEnumDestructable();
local x = GetDestructableX(destr) - enumSingleX;
local y = GetDestructableY(destr) - enumSingleY;
local dist = x*x + y*y;
if dist < enumSingleMinDist then
enumSingleMinDist = dist;
enumSingle = destr;
end
destr = nil;
end
local function GetDestructableByXY(x,y)
MoveRectTo(ENUM_RECT, x, y);
enumSingleMinDist = 1e20;
enumSingleX = x;
enumSingleY = y;
enumSingle = nil;
EnumDestructablesInRect(ENUM_RECT, nil, EnumGetSingleDestr);
return enumSingle;
end
local function LoadUnit(log)
local entriesN = 0;
local lastId = 0;
for i,v in ipairs(log) do
local curId = v[1];
if lastId > curId then
PrintDebug('|cffff5500MLNL Error:LoadUnit!', 'Bad save-file detected while loading UNIT #',i,'!|r');
return false;
end
lastId = curId;
int2Function[v[2]](UnpackLogEntry(v[3]));
if signals.abortLoop == LoadUnit then
signals.abortLoop = nil;
break;
end
entriesN = entriesN + 1;
end
return entriesN;
end
local function GetLoggingPlayer()
return loggingPid > -1 and Player(loggingPid) or nil;
end
local function LoadRemoveUnit(u)
if not u then
PrintDebug('|cffff5500MLNL Error:LoadRemoveUnit!', 'Passed unit is nil!|r');
return;
end
ShowUnit(u, true);
RemoveUnit(u);
end
local function LoadCreateUnit(p, unitid, x, y, face)
local u = CreateUnit(p, unitid, x, y, face);
if not u then
if GetLoggingPlayer() == GetLocalPlayer() then
print('|cffff5500MLNL Error! Failed to create unit with id:',FourCC2Str(unitid),'!|r');
end
signals.abortLoop = LoadUnit;
return nil;
end
logUnits[logUnitsN] = u;
if not BlzGetUnitBooleanField(u, UNIT_BF_IS_A_BUILDING) then
SetUnitX(u, x);
SetUnitY(u, y);
end
if GetUnitMoveSpeed(u) <= 0. then
ShowUnit(u, false);
ShowUnit(u, true);
end
return u;
end
local function LoadPreplacedUnit(p, prepX, prepY, preplacedUId, preplacedPId, x, y, face)
local u = GetPreplacedUnit(prepX, prepY, preplacedUId, preplacedPId);
if not u then
PrintDebug('|cffff5500MLNL Error:LoadPreplacedUnit!', 'Failed to find pre-placed unit at (',prepX, prepY,
'), id:', FourCC2Str(preplacedUId),'.|r|cffff9900Creating a new unit...|r');
return LoadCreateUnit(p, preplacedUId, x, y, face);
end
--if not u then return nil end;
logUnits[logUnitsN] = u;
if GetPlayerId(p) ~= preplacedPId then
SetUnitOwner(u, p);
end
local transp = unit2TransportHash[u];
if transp and IsUnitLoaded(u) and not IsUnitGone(transp) then
local abils = {};
local j = 0;
for _,v in pairs(MagiLogNLoad.ALL_TRANSP_ABILS) do
for _,abilid in ipairs(v) do
local lvl = GetUnitAbilityLevel(transp, abilid);
if lvl > 0 then
lvl = lvl -1;
local abil = BlzGetUnitAbility(transp, abilid);
j = j + 1;
abils[j] = {
abil,
lvl,
BlzGetAbilityRealLevelField(abil, ABILITY_RLF_AREA_OF_EFFECT, lvl),
BlzGetAbilityRealLevelField(abil, ABILITY_RLF_DURATION_HERO, lvl),
BlzGetAbilityRealLevelField(abil, ABILITY_RLF_DURATION_NORMAL, lvl)
};
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_AREA_OF_EFFECT, lvl, 99999);
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_DURATION_HERO, lvl, 0);
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_DURATION_NORMAL, lvl, 0);
break;
end
end
end
local transpX,transpY = GetUnitX(transp), GetUnitY(transp);
UnitAddAbility(transp, MagiLogNLoad.UNLOAD_INSTA_ID);
SetUnitPosition(transp, x, y);
IssueImmediateOrderById(transp,UNLOAD_INSTA_ORDER_ID);
SetUnitPosition(transp, transpX, transpY);
UnitRemoveAbility(transp, MagiLogNLoad.UNLOAD_INSTA_ID);
for i,obj in ipairs(abils) do
local abil = obj[1];
local lvl = obj[2];
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_AREA_OF_EFFECT, lvl, obj[3]);
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_DURATION_HERO, lvl, obj[4]);
BlzSetAbilityRealLevelField(abil, ABILITY_RLF_DURATION_NORMAL, lvl, obj[5]);
end
end
SetUnitX(u, x);
SetUnitY(u, y);
BlzSetUnitFacingEx(u, face);
if GetUnitMoveSpeed(u) <= 0. then
ShowUnit(u, false);
ShowUnit(u, true);
end
return u;
end
local function LoadUnitAddItemToSlotById(u, itemid, slotid, charges)
if not u then
PrintDebug('|cffff5500MLNL Error:LoadUnitAddItemToSlotById!', 'Passed unit is nil!|r');
return false;
end
local item = UnitItemInSlot(u, slotid);
if item then
if preplacedWidgets.items[item] then
UnitRemoveItem(u, item);
else
RemoveItem(item);
end
end
local v = UnitAddItemToSlotById(u, itemid, slotid);
if not v then
return false;
end
item = UnitItemInSlot(u, slotid);
logItems[logItemsN] = item;
if charges and GetItemCharges(item) ~= charges then
SetItemCharges(item, charges);
end
return true;
end
local function LoadUnitAddPreplacedItem(u, item, itemid, slotid, charges)
if not item then
return LoadUnitAddItemToSlotById(u, itemid, slotid, charges);
end
logItems[logItemsN] = item;
UnitAddItem(u, item);
if charges and GetItemCharges(item) ~= charges then
SetItemCharges(item, charges);
end
end
local function LoadGroupAddUnit(gname, u)
if not u then return end;
if not _G[gname] then
_G[gname] = CreateGroup();
end
return GroupAddUnit(_G[gname], u);
end
local function IsUnitSaveable(u, skipPreplacedCheck)
--#PROD
return (
(skipPreplacedCheck or
(
MagiLogNLoad.modes.savePreplacedUnitsOfPlayer and
preplacedWidgets.units[u] and
GetUnitTypeId(u) == preplacedWidgets.units[u][3]
) or (
MagiLogNLoad.modes.saveNewUnitsOfPlayer and
not preplacedWidgets.units[u] and
not (preplacedWidgets.units[unit2TransportHash[u]] and IsUnitLoaded(u))
)
) and
not IsUnitGone(u) and
not IsUnitType(u, UNIT_TYPE_SUMMONED)
);
end
local function LogUnit(u, forceSaving, ownerId)
if not IsUnitSaveable(u, forceSaving) then
issuedWarningChecks.saveInPreplacedTransp = issuedWarningChecks.saveInPreplacedTransp or
(not MagiLogNLoad.modes.savePreplacedUnitsOfPlayer and preplacedWidgets.units[unit2TransportHash[u]] and IsUnitLoaded(u));
issuedWarningChecks.saveTransformedPreplacedUnit = issuedWarningChecks.saveTransformedPreplacedUnit or
(MagiLogNLoad.modes.savePreplacedUnitsOfPlayer and preplacedWidgets.units[u] and GetUnitTypeId(u) ~= preplacedWidgets.units[u][3]);
return false;
end
local uid = GetUnitTypeId(u);
local float, x, y, tab, str;
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
local logEntries = log[u];
if preplacedWidgets.units[u] then
tab = preplacedWidgets.units[u];
logEntries[fCodes.LoadPreplacedUnit] = {
1, fCodes.LoadPreplacedUnit, {
ownerId and {fCodes.Player, {ownerId}} or {fCodes.GetLoggingPlayer},
tab[1], tab[2], tab[3], tab[4], Round(GetUnitX(u)), Round(GetUnitY(u)), Round(GetUnitFacing(u))
}
};
else
logEntries[fCodes.LoadCreateUnit] = {
1, fCodes.LoadCreateUnit, {
ownerId and {fCodes.Player, {ownerId}} or {fCodes.GetLoggingPlayer},
uid, Round(GetUnitX(u)), Round(GetUnitY(u)), Round(GetUnitFacing(u))
}
};
end
--Legacy
--logEntries[fCodes.LogBaseStatsByType] = {2, fCodes.LogBaseStatsByType, {{fCodes.GetLoggingUnit}}};
if IsUnitLoaded(u) and GetUnitAbilityLevel(u, MagiLogNLoad.LOAD_GHOST_ID) <= 0 then
if logEntries[MagiLogNLoad.LOAD_GHOST_ID] == nil then
logEntries[MagiLogNLoad.LOAD_GHOST_ID] = {};
end
logEntries[MagiLogNLoad.LOAD_GHOST_ID][fCodes.UnitAddAbility] = {3, fCodes.UnitAddAbility, {{fCodes.GetLoggingUnit}, MagiLogNLoad.LOAD_GHOST_ID}};
end
float = GetUnitFlyHeight(u);
if float > 0.0 or float < 0 then
logEntries[fCodes.LoadSetUnitFlyHeight] = {21, fCodes.LoadSetUnitFlyHeight, {{fCodes.GetLoggingUnit}, Round(float), 0}};
end
if UnitInventorySize(u) > 0 then
for i=0,5 do
local item = UnitItemInSlot(u, i);
if item then
if MagiLogNLoad.modes.savePreplacedItems and preplacedWidgets.items[item] then
tab = preplacedWidgets.items[item];
logEntries[(fCodes.LoadUnitAddPreplacedItem << 8) | i] = {30+2*i, fCodes.LoadUnitAddPreplacedItem, {
{fCodes.GetLoggingUnit},
{fCodes.GetPreplacedItem, {tab[1], tab[2], tab[3], tab[4], tab[5] or 0, tab[6] or 0}},
GetItemTypeId(item),
i,
GetItemCharges(item)
}};
else
logEntries[(fCodes.LoadUnitAddItemToSlotById << 8) | i] = {30+2*i, fCodes.LoadUnitAddItemToSlotById, {
{fCodes.GetLoggingUnit}, GetItemTypeId(item), i, GetItemCharges(item)
}};
end
end
end
end
if unit2HiddenStat[u] and unit2HiddenStat[u].name then
str = GetUnitName(u);
if str ~= unit2HiddenStat[u].name then
logEntries[fCodes.BlzSetUnitName] = {55, fCodes.BlzSetUnitName, {{fCodes.GetLoggingUnit}, {fCodes.utf8char, GetUTF8Codes(str)}}};
end
end
if IsHeroUnitId(uid) then
str = GetHeroProperName(u);
if str and str ~= '' then
logEntries[fCodes.BlzSetHeroProperName] = {61, fCodes.BlzSetHeroProperName, {{fCodes.GetLoggingUnit}, {fCodes.utf8char, GetUTF8Codes(str)}}};
end
logEntries[fCodes.SetHeroXP] = {62, fCodes.SetHeroXP, {{fCodes.GetLoggingUnit}, GetHeroXP(u), {fCodes.I2B, {B2I(false)}}}};
local skillCounter = 0;
for i = 0,255 do
local abil = BlzGetUnitAbilityByIndex(u, i);
if not abil then break end;
if BlzGetAbilityBooleanField(abil, ABILITY_BF_HERO_ABILITY) then
local abilid = BlzGetAbilityId(abil);
for j=1,GetUnitAbilityLevel(u, abilid) do
skillCounter = skillCounter + 1;
logEntries[(fCodes.SelectHeroSkill << 8) | skillCounter] = {63, fCodes.SelectHeroSkill, {{fCodes.GetLoggingUnit}, abilid}};
end
end
abil = nil;
end
logEntries[fCodes.UpdateHeroStats] = {64, fCodes.UpdateHeroStats, {{fCodes.GetLoggingUnit}, GetHeroStr(u,false), GetHeroAgi(u,false), GetHeroInt(u,false)}};
end
logEntries[fCodes.UpdateUnitStats] = {100, fCodes.UpdateUnitStats, {{fCodes.GetLoggingUnit},
BlzGetUnitMaxHP(u), -- max hp
math_floor(GetWidgetLife(u))+1, -- hp
BlzGetUnitMaxMana(u), -- max mana
math_floor(GetUnitState(u, UNIT_STATE_MANA)), -- mana
BlzGetUnitBaseDamage(u, 0) -- base dmg
}};
x = WaygateGetDestinationX(u);
y = WaygateGetDestinationY(u);
if x ~= 0. or y ~= 0. then
logEntries[fCodes.LoadSetWaygate] = {120, fCodes.LoadSetWaygate, {{fCodes.GetLoggingUnit}, Round(x), Round(y), {fCodes.I2B, {B2I(WaygateIsActive(u))}}}};
end
magiLog.unitsOfPlayer[tempPlayerId][u] = logEntries;
end
local FilterEnumLogUnit = Filter(function()
LogUnit(GetFilterUnit());
return false;
end);
local function CreateUnitsOfPlayerLog(p)
tempPlayerId = GetPlayerId(p);
local createdLog = {};
magiLog.unitsOfPlayer[tempPlayerId] = createdLog;
issuedWarningChecks.saveInPreplacedTransp = false;
issuedWarningChecks.saveTransformedPreplacedUnit = false;
if MagiLogNLoad.modes.saveNewUnitsOfPlayer or MagiLogNLoad.modes.savePreplacedUnitsOfPlayer then
GroupEnumUnitsOfPlayer(tempGroup, p, FilterEnumLogUnit);
end
if MagiLogNLoad.modes.saveUnitsDisownedByPlayers then
for k,v in pairs(magiLog.formerUnits) do
if v == tempPlayerId and IsUnitSaveable(k) and neutralPIds[GetPlayerId(GetOwningPlayer(k))] then
LogUnit(k);
end
end
end
local tab = magiLog.referencedUnitsOfPlayer[tempPlayerId];
if tab then
for u,_ in pairs(tab) do
if not createdLog[u] then
local pid = GetPlayerId(GetOwningPlayer(u));
LogUnit(u, true, pid ~= tempPlayerId and pid or nil);
end
end
end
if issuedWarningChecks.saveInPreplacedTransp then
--#AZZY
--print('|cffff9900MLNL Warning! Some units are loaded into the Roleplaying Circle and cannot be saved!|r');
print('|cffff9900MLNL Warning! Some units are loaded into an unsaved transport and will not be saved!|r');
end
if issuedWarningChecks.saveTransformedPreplacedUnit then
print('|cffff9900MLNL Warning! Some pre-placed units have been transformed and will not be saved!|r');
end
issuedWarningChecks.saveInPreplacedTransp = false;
issuedWarningChecks.saveTransformedPreplacedUnit = false;
end
local function LoadPreplacedItem(x, y, charges, pid, prepItemId, prepSlot, prepX, prepY, prepUnitId, prepUnitOwnerId)
local item = GetPreplacedItem(prepItemId, prepSlot, prepX, prepY, prepUnitId, prepUnitOwnerId);
if not item then
PrintDebug(
'|cffff5500MLNL Error:LoadPreplacedItem!', 'Failed to find pre-placed item at with id:', FourCC2Str(prepItemId),
'! Creating identical item...|r'
);
return LoadCreateItem(prepItemId, x, y, charges, pid);
end
SetItemPosition(item, x, y);
if GetItemCharges(item) ~= charges then
SetItemCharges(item, charges);
end
magiLog.items[item] = {pid};
end
local function LogItemOnGround(item, pid, forceSaving)
if not item or GetItemTypeId(item) == 0 or GetWidgetLife(item) <= 0. then return end;
local itemEntry = magiLog.items[item];
if (forceSaving or MagiLogNLoad.modes.savePreplacedItems or MagiLogNLoad.modes.saveAllItemsOnGround) and preplacedWidgets.items[item] then
local tab = preplacedWidgets.items[item];
logGen.items = logGen.items + 1;
magiLog.itemsOfPlayer[pid][item] = { [fCodes.LoadPreplacedItem] = {logGen.items, fCodes.LoadPreplacedItem, {
Round(GetItemX(item)), Round(GetItemY(item)), GetItemCharges(item), pid,
tab[1], tab[2], tab[3] or 0, tab[4] or 0, tab[5], tab[6] or 0
}}};
elseif MagiLogNLoad.modes.saveAllItemsOnGround or (MagiLogNLoad.modes.saveItemsDroppedManually and itemEntry and itemEntry[1] == pid) then
logGen.items = logGen.items + 1;
magiLog.itemsOfPlayer[pid][item] = { [fCodes.LoadCreateItem] = {logGen.items, fCodes.LoadCreateItem, {
GetItemTypeId(item), Round(GetItemX(item)), Round(GetItemY(item)), GetItemCharges(item), pid
}}};
end
end
local function EnumLogItemOnGround()
LogItemOnGround(GetEnumItem(), tempPlayerId);
end
local function CreateItemsOfPlayerLog(p)
local pid = GetPlayerId(p);
tempPlayerId = pid;
local createdLog = {};
magiLog.itemsOfPlayer[pid] = createdLog;
EnumItemsInRect(WORLD_BOUNDS.rect, nil, EnumLogItemOnGround);
local tab = magiLog.referencedItemsOfPlayer[pid];
if tab then
for item,_ in pairs(tab) do
if not createdLog[item] then
LogItemOnGround(item, pid, true);
end
end
end
if MagiLogNLoad.modes.savePreplacedItems then
for item,v in pairs(magiLog.items) do
if not createdLog[item] and v[1] == pid and v[2] and preplacedWidgets.items[item] then
logGen.items = logGen.items + 1;
createdLog[item] = v[2];
end
end
end
tempPlayerId = -1;
end
local function CreateExtrasOfPlayerLog(p)
local pid = GetPlayerId(p);
local log = magiLog.extrasOfPlayer[pid];
if not log then
log = {};
magiLog.extrasOfPlayer[pid] = log;
end
log[fCodes.LoadSetPlayerState] = {[fCodes.LoadSetPlayerState] = {
1, fCodes.LoadSetPlayerState, {
{fCodes.GetLoggingPlayer},
GetPlayerState(p, PLAYER_STATE_RESOURCE_GOLD),
GetPlayerState(p, PLAYER_STATE_RESOURCE_LUMBER),
}
}};
end
local function CompileUnitsLog(log, fCodeFilter)
if not log then return {{}, 'unit', {0,0}} end;
local table_sort = table.sort;
local maxLen = MagiLogNLoad.MAX_UNITS_PER_FILE;
local maxEntries = MagiLogNLoad.MAX_ENTRIES_PER_UNIT;
local entriesN = 0;
local ans = {};
local ansN = 0;
local u2i = {};
local i2u = {};
for u,v in pairs(log) do
if ansN >= maxLen then
print('|cffff9900MLNL Warning!', 'Your save-file has too many UNIT entries! Not all will be saved!|r');
break;
end
local curAns = {};
local curAnsN = 0;
for k2,v2 in pairs(v) do
--[[
if k2 == fCodes.KillUnit and (not fCodeFilter or fCodeFilter[k2]) then
local entry = v2;
if GetUnitTypeId(u) == 0 then
curAnsN = curAnsN + 1;
curAns[curAnsN] = {v2[1], fCodes.LoadRemoveUnit, v2[3]};
elseif IsUnitType(u, UNIT_TYPE_DEAD) then
curAnsN = curAnsN + 1;
curAns[curAnsN] = v2;
end
else]]
if k2 == fCodes.SetUnitAnimation then
local entry = v2[GetUnitCurrentOrder(u)];
if entry and (not fCodeFilter or fCodeFilter[entry[2]]) then
curAnsN = curAnsN + 1;
curAns[curAnsN] = entry;
end
else
local check = true;
if v2[1] == nil or type(v2[1]) == 'table' then
for _,v3 in pairs(v2) do
if type(v3) ~= 'table' or next(v3) == nil then break end;
check = false;
if not fCodeFilter or fCodeFilter[v3[2]] then
curAnsN = curAnsN + 1;
curAns[curAnsN] = v3;
end
end
end
if check and type(v2) == 'table' and next(v2) ~= nil and (not fCodeFilter or fCodeFilter[v2[2]]) then
curAnsN = curAnsN + 1;
curAns[curAnsN] = v2;
end
end
if curAnsN >= maxEntries then
print('|cffff9900MLNL Warning!', 'Some UNITS have too much data! Not all will be saved!|r');
break;
end
end
if curAnsN > 0 then
if curAnsN > 1 then
table_sort(curAns, SortLog);
end
entriesN = entriesN + curAnsN;
ansN = ansN + 1;
ans[ansN] = curAns;
u2i[u] = ansN;
i2u[ansN] = u;
end
end
local transport2UnitsHash = setmetatable({}, {
__index = function(t,k)
if rawget(t,k) == nil then
rawset(t, k, {});
end
return rawget(t,k);
end
});
for u,_ in pairs(log) do
local transp = unit2TransportHash[u];
if IsUnitLoaded(u) and not IsUnitGone(transp) and (MagiLogNLoad.modes.savePreplacedUnitsOfPlayer or not preplacedWidgets.units[transp]) then
local hash = transport2UnitsHash[transp];
hash[#hash+1] = u;
else
unit2TransportHash[u] = nil;
end
end
local lastInd = #ans;
for transp, units in pairs(transport2UnitsHash) do
local ind = u2i[transp];
if ind and #units > 0 then
ans[ind], ans[lastInd], u2i[transp], u2i[i2u[lastInd]], i2u[ind], i2u[lastInd] = ans[lastInd], ans[ind], lastInd, ind, i2u[lastInd], i2u[ind];
lastInd = lastInd - 1;
ind = u2i[transp];
local curLog = ans[ind];
curLog[#curLog+1] = {200, fCodes.ForceLoadUnits, {
{fCodes.GetLoggingUnit, {ind}}, {fCodes.GetLoggingUnitsSafe0, Map(units, function(v) return u2i[v] or -1 end)}
}};
entriesN = entriesN + 1;
end
end
for i,v in ipairs(ans) do
v[#v+1] = i2u[i];
end
-- Maybe it helps untangle the GC?
u2i = nil;
i2u = nil;
return {ans, 'unit', {entriesN, ansN}};
end
local function CompileDestrsLog(log, fCodeFilter)
if not log then return {{}, 'destructable', {0,0}} end;
local table_sort = table.sort;
local maxLen = MagiLogNLoad.MAX_DESTRS_PER_FILE;
local maxEntries = MagiLogNLoad.MAX_ENTRIES_PER_DESTR;
local entriesN = 0;
local ans = {};
local ansN = 0;
for destr, v in pairs(log) do
if v[fCodes.LoadCreateDestructable] or MagiLogNLoad.modes.saveStandingPreplacedDestrs then
local curAns = {};
local curAnsN = 0;
if ansN >= maxLen then
print('|cffff9900MLNL Warning!', 'Your save-file has too many DESTRUCTABLE entries! Not all will be saved!|r');
break;
end
for _,v2 in pairs(v) do
if type(v2) == 'table' and next(v2) ~= nil and (not fCodeFilter or fCodeFilter[v2[2]]) then
curAnsN = curAnsN + 1;
curAns[curAnsN] = v2;
if curAnsN >= maxEntries then
print('|cffff9900MLNL Warning!', 'Some DESTRUCTABLES have too much data! Not all of it will be saved!|r');
break;
end
end
end
if curAnsN > 0 then
if curAnsN > 1 then
table_sort(curAns, SortLog);
end
entriesN = entriesN + curAnsN;
curAnsN = curAnsN+1;
curAns[curAnsN] = destr;
ansN = ansN + 1;
ans[ansN] = curAns;
end
end
end
return {ans, 'destructable', {entriesN, ansN}};
end
local function CompileItemsLog(log, fCodeFilter)
if not log then return {{}, 'item', {0,0}} end;
local table_sort = table.sort;
local maxLen = MagiLogNLoad.MAX_ITEMS_PER_FILE;
local maxEntries = MagiLogNLoad.MAX_ENTRIES_PER_ITEM;
local entriesN = 0;
local ans = {};
local ansN = 0;
for item, v in pairs(log) do
local curAns = {};
local curAnsN = 0;
if ansN >= maxLen then
print('|cffff9900MLNL Warning!', 'Your save-file has too many ITEM entries! Not all will be saved!|r');
break;
end
for _,v2 in pairs(v) do
if type(v2) == 'table' and next(v2) ~= nil and (not fCodeFilter or fCodeFilter[v2[2]]) then
curAnsN = curAnsN + 1;
curAns[curAnsN] = v2;
if curAnsN >= maxEntries then
print('|cffff9900MLNL Warning!', 'Some ITEMS have too much data! Not all of it will be saved!|r');
break;
end
end
end
if curAnsN > 0 then
if curAnsN > 1 then
table_sort(curAns, SortLog);
end
entriesN = entriesN + curAnsN;
curAnsN = curAnsN+1;
curAns[curAnsN] = item;
ansN = ansN + 1;
ans[ansN] = curAns;
end
end
return {ans, 'item', {entriesN, ansN}};
end
local function CompileTerrainLog(log, fCodeFilter)
if not log then return {{}, 'terrain', {0,0}} end;
local maxLen = MagiLogNLoad.MAX_TERRAIN_PER_FILE;
local ans = {}
local ansN = 0;
for _,v in pairs(log) do
if type(v) == 'table' and next(v) ~= nil and (not fCodeFilter or fCodeFilter[v[2]]) then
ansN = ansN + 1;
ans[ansN] = v;
if ansN >= maxLen then
print('|cffff9900MLNL Warning!', 'Your save-file has too many TERRAIN entries! Not all will be saved!|r');
break;
end
end
end
if ansN > 1 then
table.sort(ans, SortLog);
end
return {ans, 'terrain', {ansN, 0}};
end
local function CompileResearchLog(log, fCodeFilter)
if not log then return {{}, 'research', {0, 0}} end;
local maxLen = MagiLogNLoad.MAX_RESEARCH_PER_FILE;
local ans = {}
local ansN = 0;
for _,v in pairs(log) do
if type(v) == 'table' and next(v) ~= nil and (not fCodeFilter or fCodeFilter[v[2]]) then
ansN = ansN + 1;
ans[ansN] = v;
if ansN >= maxLen then
print('|cffff9900MLNL Warning!', 'Your save-file has too many RESEARCH entries! Not all will be saved!|r');
break;
end
end
end
if ansN > 1 then
table.sort(ans, SortLog);
end
return {ans, 'research', {ansN, 0}};
end
local function CompileExtrasLog(log, fCodeFilter)
if not log then return {{}, 'extra', {0,0}} end;
local maxLen = MagiLogNLoad.MAX_EXTRAS_PER_FILE;
local ans = {}
local ansN = 0;
for _,entryCol in pairs(log) do
for _,entry in pairs(entryCol) do
if type(entry) == 'table' and next(entry) ~= nil and (not fCodeFilter or fCodeFilter[entry[2]]) then
ansN = ansN + 1;
ans[ansN] = entry;
if ansN >= maxLen then
print('|cffff9900MLNL Warning!', 'Your save-file has too many EXTRA entries! Not all will be saved!|r');
break;
end
end
end
end
if ansN > 1 then
table.sort(ans, SortLog);
end
return {ans, 'extra', {ansN,0}};
end
local function FindInLogsEnds(logs, val)
local offset = 0;
for _,log in ipairs(logs) do
for i,elemLog in ipairs(log) do
if elemLog[#elemLog] == val then
return i+offset;
end
end
offset = offset + #log;
end
return -1;
end
local function Handle2LogGetter(val, typeStr2LogHash)
if val == nil then return nil end;
local valType = type(val);
if valType == 'number' then
return (val > 2147483647 or val < -2147483647) and {fCodes.UTF8Codes2Real, GetUTF8Codes(tostring(val))} or
(val == math_floor(val) or val > 2147483647/10000. or val < -2147483647/10000.) and Round(val) or
{fCodes.Div10000, {Round(10000*val)}};
end
if valType == 'boolean' then return {fCodes.I2B, {B2I(val)}} end;
if valType == 'string' then return {fCodes.utf8char, GetUTF8Codes(val)} end;
valType = GetHandleTypeStr(val);
local logs = typeStr2LogHash[valType];
if logs then
local ind = FindInLogsEnds(logs, val);
if ind ~= -1 then
return {typeStr2GetterFCode[valType], {ind}};
end
end
PrintDebug('|cffff9900MLNL Warning:Handle2LogGetter!', 'Cannot create getter for value of type',valType,'!|r');
return nil;
end
local function CompileProxyTablesLog(log, fullLog)
if not log then return {{}, 'proxy', {0,0}} end;
local typeStr2LogHash = {};
for _,v in ipairs(fullLog) do
local typeName = v[2];
if typeStr2LogHash[typeName] then
local cur = typeStr2LogHash[typeName];
cur[#cur+1] = v[1];
else
typeStr2LogHash[typeName] = {v[1]};
end
end
local maxLen = MagiLogNLoad.MAX_PROXYTABLE_ENTRIES_PER_FILE;
local breakCheck = false;
local warningCheck = true;
local ans = {};
local ansN = 0;
for varName,v in pairs(log) do
for actualVar,v2 in pairs(v) do
if _G[varName] == actualVar then
for key,logEntry in pairs(v2) do
local tkv = logEntry[3];
local getterKey = Handle2LogGetter(tkv[2], typeStr2LogHash);
if getterKey ~= nil then
local getterVal = Handle2LogGetter(tkv[3], typeStr2LogHash);
if getterVal ~= nil or tkv[3] == nil then
ansN = ansN + 1;
ans[ansN] = {logEntry[1], logEntry[2], { {fCodes.utf8char, GetUTF8Codes(tkv[1])}, getterKey, getterVal ~= nil and getterVal or {fCodes.Nil}}};
if ansN >= maxLen then
PrintDebug('|cffff9900MLNL Warning:CompileProxyTablesLog!', 'Your save-file has too many PROXY TABLE entries! Not all will be saved!|r');
breakCheck = true;
break;
end
end
elseif warningCheck then
warningCheck = false;
PrintDebug('|cffff9900MLNL Warning:CompileProxyTablesLog!', 'Some key getters are nil and will be skipped!|r');
end
end
end
if breakCheck then break end;
end
if breakCheck then break end;
end
if ansN > 1 then
table.sort(ans, SortLog);
end
return {ans, 'proxy', {ansN, 0}};
end
local function CompileHashtablesLog(log, fullLog)
if not log then return {{}, 'hashtable', {0,0}} end;
local typeStr2LogHash = {};
for _,v in ipairs(fullLog) do
local typeName = v[2];
if typeStr2LogHash[typeName] then
local cur = typeStr2LogHash[typeName];
cur[#cur+1] = v[1];
else
typeStr2LogHash[typeName] = {v[1]};
end
end
local maxLen = MagiLogNLoad.MAX_HASHTABLE_ENTRIES_PER_FILE;
local breakCheck = false;
local warningCheck = true;
local ans = {};
local ansN = 0;
for name,v in pairs(log) do
for actualHt,v2 in pairs(v) do
if actualHt == _G[name] then
for parentKey,v3 in pairs(v2) do
for childKey,logEntry in pairs(v3) do
local logArgs = logEntry[3]; --{hashtableName, value, childKey, parentKey}};
local getterParentKey = Handle2LogGetter(logArgs[4], typeStr2LogHash);
local getterChildKey = Handle2LogGetter(logArgs[3], typeStr2LogHash);
if getterParentKey ~= nil and getterChildKey ~= nil then
local getterVal = Handle2LogGetter(logArgs[2], typeStr2LogHash);
if getterVal ~= nil or logArgs[2] == nil then
getterVal = getterVal ~= nil and getterVal or {fCodes.Nil};
ansN = ansN + 1;
ans[ansN] = {logEntry[1], logEntry[2], { {fCodes.utf8char, GetUTF8Codes(logArgs[1])}, getterVal, getterChildKey, getterParentKey}};
if ansN >= maxLen then
PrintDebug('|cffff9900MLNL Warning:CompileHashtablesLog!', 'Your save-file has too many PROXY TABLE entries! Not all will be saved!|r');
breakCheck = true;
break;
end
end
elseif warningCheck then
warningCheck = false;
PrintDebug('|cffff5500MLNL Error:CompileHashtablesLog!', 'Some key getters are nil and will be skipped!|r');
end
end
if breakCheck then break end;
end
if breakCheck then break end;
end
end
if breakCheck then break end;
end
if ansN > 1 then
table.sort(ans, SortLog);
end
return {ans, 'hashtable', {ansN,0}};
end
local function CompileVarsLog(log, fullLog)
if not log then return {{}, 'var', {0,0}} end;
local typeStr2LogHash = {};
for _,v in ipairs(fullLog) do
local typeName = v[2];
if typeStr2LogHash[typeName] then
local cur = typeStr2LogHash[typeName];
cur[#cur+1] = v[1];
else
typeStr2LogHash[typeName] = {v[1]};
end
end
local maxLen = MagiLogNLoad.MAX_VARIABLE_ENTRIES_PER_FILE;
local ans = {};
local ansN = 0;
for name,entry in pairs(log) do
local getterVal = Handle2LogGetter(entry[3][2], typeStr2LogHash);-- = {logGen.vars, fCodes.SaveIntoNamedVar, {{fCodes.utf8char, GetUTF8Codes(name)}, curVal}};
if getterVal ~= nil or entry[3][2] == nil then
ansN = ansN + 1;
ans[ansN] = {entry[1], entry[2], {{fCodes.utf8char, GetUTF8Codes(entry[3][1])}, getterVal ~= nil and getterVal or {fCodes.Nil}}};
if ansN >= maxLen then
PrintDebug('|cffff9900MLNL Warning:CompileVarsLog!', 'Your save-file has too many VARIABLE entries! Not all will be saved!|r');
break;
end
end
end
if ansN > 1 then
table.sort(ans, SortLog);
end
return {ans, 'var', {ansN,0}};
end
local function TrimLogs(logList)
for _,log in ipairs(logList) do
if type(log) == 'table' and type(log[1]) == 'table' then
for _,v in ipairs(log) do
if type(v[#v]) ~= 'table' then
v[#v] = nil;
end
end
end
end
end
local function CreateSerialLogs(p)
local pid = GetPlayerId(p);
local n = 0;
local fullLog = {};
local tally = {};
for k,_ in pairs(word2LoadLogFuncHash) do
tally[k] = {0,0};
end
local log;
n = n + 1;
log = CompileTerrainLog(magiLog.terrainOfPlayer[pid]);
fullLog[n] = log;
Vec2Add_inPlace(tally[log[2]], log[3]);
n = n + 1;
log = CompileDestrsLog(magiLog.destrsOfPlayer[pid], fCodeFilters.destrs[1]);
fullLog[n] = log;
Vec2Add_inPlace(tally[log[2]], log[3]);
n = n + 1;
log = CompileUnitsLog(magiLog.unitsOfPlayer[pid]);
fullLog[n] = log;
Vec2Add_inPlace(tally[log[2]], log[3]);
n = n + 1;
log = CompileItemsLog(magiLog.itemsOfPlayer[pid]);
fullLog[n] = log;
Vec2Add_inPlace(tally[log[2]], log[3]);
n = n + 1;
log = CompileDestrsLog(magiLog.destrsOfPlayer[pid], fCodeFilters.destrs[2]);
fullLog[n] = log;
Vec2Add_inPlace(tally[log[2]], log[3]);
n = n + 1;
log = CompileResearchLog(magiLog.researchOfPlayer[pid]);
fullLog[n] = log;
Vec2Add_inPlace(tally[log[2]], log[3]);
n = n + 1;
log = CompileExtrasLog(magiLog.extrasOfPlayer[pid]);
fullLog[n] = log;
Vec2Add_inPlace(tally[log[2]], log[3]);
n = n + 1;
log = CompileHashtablesLog(magiLog.hashtablesOfPlayer[pid], fullLog);
fullLog[n] = log;
Vec2Add_inPlace(tally[log[2]], log[3]);
n = n + 1;
log = CompileProxyTablesLog(magiLog.proxyTablesOfPlayer[pid], fullLog);
fullLog[n] = log;
Vec2Add_inPlace(tally[log[2]], log[3]);
n = n + 1;
log = CompileVarsLog(magiLog.varsOfPlayer[pid], fullLog);
fullLog[n] = log;
Vec2Add_inPlace(tally[log[2]], log[3]);
local manifest = {MagiLogNLoad.engineVersion, Map(PluckArray(fullLog, 2), function(v) return word2CodeHash[v] or 0 end)};
fullLog = PluckArray(fullLog, 1);
TrimLogs(fullLog);
fullLog = {manifest, fullLog};
local str = SerializeIntTable(fullLog);
magiLog.unitsOfPlayer[pid] = nil;
magiLog.itemsOfPlayer[pid] = nil;
return str, tally;
end
function MagiLogNLoad.TryLoadNextInQueue()
if downloadingStreamFrom == nil and #loadingQueue > 0 then
local queued = loadingQueue[1];
if queued then
remove(loadingQueue, 1);
if queued[1] == GetLocalPlayer() then
MagiLogNLoad.SyncLocalSaveFile(queued[2]);
end
return true;
end
end
return false;
end
local function AbortCurrentLoading()
if downloadingStreamFrom then
local stream = streamsOfPlayer[GetPlayerId(downloadingStreamFrom)];
stream = stream and stream[logGen.streams] or nil;
if stream then
stream.aborted = true;
end
end
downloadingStreamFrom = nil;
uploadingStream = nil;
end
local function CheckLoadingProg()
if not downloadingStreamFrom then return end;
local stream = streamsOfPlayer[GetPlayerId(downloadingStreamFrom)];
stream = stream and stream[logGen.streams] or nil;
if not stream or stream.aborted or not stream.streamStartTick then return end;
local streamStartTick = stream.streamStartTick;
local loadingTime = (mainTick - streamStartTick)*TICK_DUR;
if not issuedWarningChecks.loadTimeWarning and
MagiLogNLoad.LOADING_TIME_SECONDS_TO_WARNING > 0 and
loadingTime > MagiLogNLoad.LOADING_TIME_SECONDS_TO_WARNING then
issuedWarningChecks.loadTimeWarning = true;
print('|cffff9900MLNL Warning! The current save-file loading has lasted for more than', MagiLogNLoad.LOADING_TIME_SECONDS_TO_WARNING, 'seconds!|r');
if MagiLogNLoad.LOADING_TIME_SECONDS_TO_WARNING < MagiLogNLoad.LOADING_TIME_SECONDS_TO_ABORT then
print(
'|cffff9900It will be aborted in',
MagiLogNLoad.LOADING_TIME_SECONDS_TO_ABORT - MagiLogNLoad.LOADING_TIME_SECONDS_TO_WARNING,
'seconds from now!|r'
);
end
elseif loadingTime > MagiLogNLoad.LOADING_TIME_SECONDS_TO_ABORT then
AbortCurrentLoading();
print('|cffff5500The current save-file loading has been aborted!|r');
elseif mainTick - stream.streamLastTick > 250 then
AbortCurrentLoading();
print('|cffff5500The currently loading save-file appears to have stalled and has been aborted!|r');
end
end
local function ResetReferencedLogging(entry)
if not logEntry2ReferencedHash[entry] then return end;
for k,v in pairs(logEntry2ReferencedHash[entry]) do
v[k] = nil;
end
logEntry2ReferencedHash[entry] = nil;
end
local function TimerTick()
mainTick = mainTick + 1;
local localPid = GetPlayerId(GetLocalPlayer());
if MagiLogNLoad.stateOfPlayer[localPid] ~= MagiLogNLoad.BASE_STATES.STANDBY then
local state = MagiLogNLoad.stateOfPlayer[localPid];
if state == MagiLogNLoad.BASE_STATES.OFF and not issuedWarningChecks.initPanic then
print('|cffff5500MLNL Error! Something went horribly wrong while MagiLogNLoad was initializing!|r');
print('|cffff5500Please find |r|cffaaff00@ModdieMads|r|cffff5500 and report this error code:|r|cffaaff00', 'BASE_STATES',state,'!|r');
issuedWarningChecks.initPanic = true;
elseif state == MagiLogNLoad.BASE_STATES.CLEANING_TABLES and not issuedWarningChecks.tabKeyCleanerPanic then
print('|cffff9900MLNL Warning! Something went mildly wrong while cleaning up memory in MagiLogNLoad!|r');
print('|cffff5500Please find |r|cffaaff00@ModdieMads|r|cffff5500 and report this error code:|r|cffaaff00', 'BASE_STATES',state,'!|r');
issuedWarningChecks.tabKeyCleanerPanic = true;
end
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
if MagiLogNLoad.SAVE_STATES.HASH[state] then
print('|cffff5500MLNL Error! Something went horribly wrong while trying to create a save-file!|r');
print('|cffff5500Please find |r|cffaaff00@ModdieMads|r|cffff5500 and report this error code:|r|cffaaff00', 'SAVE_STATES',state,'!|r');
local dumpFileName = MagiLogNLoad.SAVE_FOLDER_PATH .. 'magilognload_dump.pld';
print('|cffff9900Trying to dump data into', dumpFileName, '!|r');
FileIO.SaveFile(dumpFileName, '--SAVE_STATES '..state..'..\r\n'..TableToConsole({
['proxyTablesOfPlayer']=magiLog.proxyTablesOfPlayer[localPid],
['hashtablesOfPlayer']=magiLog.hashtablesOfPlayer[localPid],
['unitsOfPlayer']=magiLog.unitsOfPlayer[localPid],
['referencedUnitsOfPlayer']=magiLog.referencedUnitsOfPlayer[localPid],
['destrsOfPlayer']=magiLog.destrsOfPlayer[localPid],
['referencedDestrsOfPlayer']=magiLog.referencedDestrsOfPlayer[localPid],
['itemsOfPlayer']=magiLog.itemsOfPlayer[localPid],
['referencedItemsOfPlayer']=magiLog.referencedItemsOfPlayer[localPid]
}));
print('|cffaaaaaaDid it work? I think it did... Done!|r');
elseif MagiLogNLoad.LOAD_STATES.HASH[state] then
print('|cffff5500MLNL Error! Something went horribly wrong while trying to load a save-file!|r');
print('|cffff5500Please find |r|cffaaff00@ModdieMads|r|cffff5500 and report this error code:|r|cffaaff00', 'LOAD_STATES',state,'!|r');
end
end
local len = #onLaterTickFuncs;
if len > 0 then
local i = 1;
while i <= len do
local fobj = onLaterTickFuncs[i];
if mainTick >= fobj[1] then
remove(onLaterTickFuncs,i);
i = i-1;
len = len-1;
if fobj[3] then
fobj[2](unpack(fobj[3]));
fobj[3] = nil;
else
fobj[2]();
end
end
i = i+1;
end
end
CheckLoadingProg();
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.LOAD_STATES.SEND_SYNC_DATA;
if uploadingStream then
local msgsN = uploadingStream.messagesN;
local msgsSent = uploadingStream.messagesSent;
if msgsSent < msgsN then
local nextMsg = uploadingStream.messagesSent + 1;
for n=1,(uploadingStream.chunksSynced > 2 and 50 or 1) do
if not BlzSendSyncData('MLNL', uploadingStream.messages[nextMsg]) then
break;
end
uploadingStream.messagesSent = nextMsg;
nextMsg = nextMsg+1;
if nextMsg > msgsN then
break;
end
end
end
end
MagiLogNLoad.TryLoadNextInQueue();
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.CLEANING_TABLES;
--#DEBUG
if (mainTick&1023) == 0 then
local tab = tabKeyCleaner[((mainTick >> 10)&tabKeyCleanerSize)+1];
if tab then
if tab.arr then
local ind = (mainTick >> 10)&31;
if ind == 31 then lastCleanedTab = nil end;
local innerTab = nil;
repeat
ind = ind + 1;
innerTab = tab.arr[ind];
until innerTab ~= nil or ind >= 31;
if innerTab and lastCleanedTab ~= innerTab and next(innerTab) then
lastCleanedTab = innerTab;
if tab.referenceLogger then
for refName,v in pairs(innerTab) do
for ref,v2 in pairs(v) do
if _G[refName] ~= ref then
ResetReferencedLogging(v2);
v[ref] = nil;
end
end
end
else
local typeIdFunc = typeStr2TypeIdGetter[GetHandleTypeStr(next(innerTab))];
local chk = tab.chk;
local willClean = true;
for k,v in pairs(innerTab) do
if typeIdFunc(k) == 0 then
if chk then
for _,code in ipairs(chk) do
if v[code] then
willClean = false;
break;
end
end
end
if willClean then
innerTab[k] = nil;
end
end
end
end
end
else
local map;
if tab.key then
tab = tab.key;
map = tab.map;
end
if tab and tab ~= lastCleanedTab and next(tab) then
lastCleanedTab = tab;
local typeIdFunc = typeStr2TypeIdGetter[GetHandleTypeStr(next(tab))];
for k,v in pairs(tab) do
if typeIdFunc(k) == 0 then
tab[k] = nil;
if map then
TableSetDeep(map, nil, v);
end
end
end
end
end
end
end
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
signals.abortLoop = nil;
end
local function LoadTerrainLog(log)
local lastId = 0;
local entriesN = 0;
for i,v in ipairs(log) do
local curId = v[1];
if lastId > curId then
PrintDebug('|cffff5500MLNL Error:LoadTerrainLog!', 'Bad save-file detected while loading TERRAIN entry #',i,'!|r');
return false;
end
lastId = curId;
entriesN = entriesN + 1;
int2Function[v[2]](UnpackLogEntry(v[3]));
end
return {entriesN,0};
end
local function LoadResearchLog(log)
local entriesN = 0;
local lastId = 0;
for i,v in ipairs(log) do
local curId = v[1];
if lastId > curId then
PrintDebug('|cffff5500MLNL Error:LoadResearchLog!', 'Bad save-file detected while loading RESEARCH entry #',i,'!|r');
return false;
end
lastId = curId;
int2Function[v[2]](UnpackLogEntry(v[3]));
entriesN = entriesN + 1;
end
return {entriesN,0};
end
local function LoadExtrasLog(log)
local entriesN = 0;
local lastId = 0;
for i,v in ipairs(log) do
local curId = v[1];
if lastId > curId then
PrintDebug('|cffff5500MLNL Error:LoadExtrasLog!', 'Bad save-file detected while loading EXTRAS entry #',i,'!|r');
return false;
end
lastId = curId;
int2Function[v[2]](UnpackLogEntry(v[3]));
entriesN = entriesN + 1;
end
return {entriesN,0};
end
local function LoadDestr(log)
local lastId = 0;
local entriesN = 0;
for i,v in ipairs(log) do
local curId = v[1];
if lastId > curId then
PrintDebug('|cffff5500MLNL Error:LoadDestr!', 'Bad save-file detected while loading DESTRUCTABLE #',i,'!|r');
return false;
end
lastId = curId;
int2Function[v[2]](UnpackLogEntry(v[3]));
if signals.abortLoop == LoadDestr then
signals.abortLoop = nil;
break;
end
entriesN = entriesN + 1;
end
return entriesN;
end
local function LoadDestrsLog(log)
local destrsN, destrEntriesN = 0,0;
for i,v in ipairs(log) do
destrsN = destrsN + 1;
logDestrsN = logDestrsN + 1;
destrEntriesN = destrEntriesN + LoadDestr(v);
end
return {destrEntriesN, destrsN};
end
local function LoadUnitsLog(log)
local unitsN, unitEntriesN = 0, 0;
for i,v in ipairs(log) do
unitsN = unitsN + 1;
logUnitsN = logUnitsN + 1;
unitEntriesN = unitEntriesN + LoadUnit(v);
end
return {unitEntriesN, unitsN};
end
local function LoadItemsLog(log)
local itemsN, itemEntriesN = 0,0;
for i,v in ipairs(log) do
logItemsN = logItemsN+1;
itemEntriesN = itemEntriesN + LoadItem(v);
itemsN = itemsN + 1;
end
return {itemEntriesN, itemsN};
end
local function LoadProxyTablesLog(log)
local entriesN = 0;
local lastId = 0;
for i,v in ipairs(log) do
local curId = v[1];
if lastId > curId then
PrintDebug('|cffff5500MLNL Error:LoadProxyTablesLog!', 'Bad save-file detected while loading PROXY TABLE entry #',i,'!|r');
return false;
end
lastId = curId;
int2Function[v[2]](UnpackLogEntry(v[3]));
entriesN = entriesN + 1;
end
return {entriesN,0};
end
local function LoadHashtablesLog(log)
local entriesN = 0;
local lastId = 0;
for i,v in ipairs(log) do
local curId = v[1];
if lastId > curId then
PrintDebug('|cffff5500MLNL Error:LoadHashtablesLog!', 'Bad save-file detected while loading HASHTABLE entry #',i,'!|r');
return false;
end
lastId = curId;
int2Function[v[2]](UnpackLogEntry(v[3]));
entriesN = entriesN + 1;
end
return {entriesN,0};
end
local function LoadVarsLog(log)
local entriesN = 0;
local lastId = 0;
for i,v in ipairs(log) do
local curId = v[1];
if lastId > curId then
PrintDebug('|cffff5500MLNL Error:LoadVarsLog!', 'Bad save-file detected while loading VARIABLE entry #',i,'!|r');
return false;
end
lastId = curId;
int2Function[v[2]](UnpackLogEntry(v[3]));
entriesN = entriesN + 1;
end
return {entriesN,0};
end
local function PrintTally(totalTime, tally, prefix)
if totalTime then
print('Total '..prefix..'ing Time:', totalTime, 'seconds.');
end
if tally['terrain'] and tally['terrain'][1] > 0 then
print(prefix..'ed',tally['terrain'][1], 'terrain entries!')
end
if tally['destructable'] and tally['destructable'][1] > 0 then
print(prefix..'ed',tally['destructable'][2], 'destructables with a total of', tally['destructable'][1], 'properties!');
end
if tally['unit'] and tally['unit'][1] > 0 then
print(prefix..'ed',tally['unit'][2], 'units with a total of', tally['unit'][1], 'properties!')
end
if tally['item'] and tally['item'][1] > 0 then
print(prefix..'ed',tally['item'][2], 'items with a total of', tally['item'][1], 'properties!')
end
if tally['research'] and tally['research'][1] > 0 then
print(prefix..'ed',tally['research'][1], 'research entries!')
end
if tally['extra'] and tally['extra'][1] > 0 then
print(prefix..'ed', tally['extra'][1], 'extras entries!')
end
if tally['hashtable'] and tally['hashtable'][1] > 0 then
print(prefix..'ed', tally['hashtable'][1], 'hashtable entries!')
end
if tally['proxy'] and tally['proxy'][1] > 0 then
print(prefix..'ed', tally['proxy'][1], 'proxy table entries!')
end
if tally['var'] and tally['var'][1] > 0 then
print(prefix..'ed', tally['var'][1], 'variable entries!')
end
end
local function LoadCreateDestructable(destrid, argX, argY, face, sca, vari)
local destr = CreateDestructable(destrid, argX, argY, face, sca, vari);
if not destr then
PrintDebug('|cffff5500MLNL Error:LoadCreateDestructable!', 'Failed to create destructable with id:',FourCC2Str(destrid),'!|r');
signals.abortLoop = LoadDestr;
return nil;
end
logDestrs[logDestrsN] = destr;
return destr;
end
--#AZZY2
function MagiLogNLoad.LoadPlayerSaveFromString(p, argStr, startTime)
local pid = GetPlayerId(p);
local localPid = GetPlayerId(GetLocalPlayer());
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.LOAD_STATES.LOAD_SAVE_FROM_STRING;
downloadingStreamFrom = nil;
--#PROD
local str = LibDeflate.DecompressDeflate(COBSDescape(argStr));
if not str then
print('|cffff5500MLNL Error!', 'Bad save-file! Cannot decompress.|r');
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
return false;
end
--#DEBUG
if MagiLogNLoad.modes.debug then
FileIO.SaveFile('mlnl_debug.pld', str);
end
str = str:gsub(sanitizer, '');
if not str then
print('|cffff5500MLNL Error!', 'Bad save-file! Aborting loading...|r');
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
return false;
end
if MagiLogNLoad.oldTypeId2NewTypeId and next(MagiLogNLoad.oldTypeId2NewTypeId) then
PrintDebug('|cffff9900MLNL Warning:MagiLogNLoad.LoadPlayerSaveFromString!', 'Replacing old type ids with new ones in MagiLogNLoad.oldTypeId2NewTypeId...|r');
str = str:gsub(PERC..'-?'..PERC..'d+', MagiLogNLoad.oldTypeId2NewTypeId);
end
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.LOAD_STATES.DESERIALIZE;
local logs = Deserialize(str);
if not logs then
print('|cffff5500MLNL Error!', 'Bad save-file! Cannot deserialize.|r');
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
return false;
end
--#AZZY1
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.LOAD_STATES.GETTING_MANIFESTS;
local manifest = logs[1];
if not manifest then
print('|cffff5500MLNL Error!', 'Bad save-file! Manifest is missing.|r');
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
return false;
end
if not manifest[1] or manifest[1] < MagiLogNLoad.engineVersion then
PrintDebug('|cffff9900MLNL Warning:MagiLogNLoad.LoadPlayerSaveFromString!', 'Outdated save-file version detected! Trying to load it anyway...|r');
end
logs = logs[2];
local tally = {};
for k,_ in pairs(word2LoadLogFuncHash) do
tally[k] = {0,0};
end
logUnits = {};
logUnitsN = 0;
logDestrs = {};
logDestrsN = 0;
logItems = {};
logItemsN = 0;
local oldLoggingPId = loggingPid;
MagiLogNLoad.SetLoggingPlayer(p);
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.LOAD_STATES.LOADING_LOGS;
for i,v in ipairs(manifest[2]) do
if v ~= 0 then
local word = code2WordHash[v];
Vec2Add_inPlace(tally[word], word2LoadLogFuncHash[word](logs[i]));
end
end
logUnits = nil;
logUnitsN = 0;
logDestrs = nil;
logDestrsN = 0;
logItems = nil;
logItemsN = 0;
print('|cffffcc00Loading is done!|r |cffaaaaaaMagiLogNLoad is cooling down and collecting reports...|r');
local totalTime = startTime and os.clock() - startTime or nil;
onLaterTickFuncs[#onLaterTickFuncs+1] = {mainTick + 100,
function()
downloadingStreamFrom = nil;
MagiLogNLoad.stateOfPlayer[pid] = MagiLogNLoad.LOAD_STATES.SUCCESFUL_LOADING;
local localPid = GetPlayerId(GetLocalPlayer());
if MagiLogNLoad.onLoadFinished then
MagiLogNLoad.onLoadFinished(p);
end
if MagiLogNLoad.willPrintTallyNext then
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.LOAD_STATES.PRINT_TALLY;
PrintTally(totalTime, tally, 'Load');
MagiLogNLoad.willPrintTallyNext = false;
end
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
end
};
playerLoadInfo[loggingPid].count = playerLoadInfo[loggingPid].count - 1;
loggingPid = oldLoggingPId;
downloadingStreamFrom = p;
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
return true;
end
function MagiLogNLoad.SaveLocalPlayer()
print(
'|cffff5500MLNL Error! MagiLogNLoad.SaveLocalPlayer has been deprecated!',
'Please use MagiLogNLoad.CreatePlayerSaveFile instead.|r'
);
end
function MagiLogNLoad.CreatePlayerSaveFile(p, fileName)
local lp = GetLocalPlayer();
local localPid = GetPlayerId(lp);
if not p then
PrintDebug('|cffff5500MLNL Error:MagiLogNLoad.CreatePlayerSaveFile!', 'Passed player <p> argument is nil!|r');
return false;
end
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.SAVE_STATES.CREATE_PLAYER_SAVEFILE;
fileName = fileName:gsub(fileNameSanitizer, '');
if fileName == nil or #fileName < 1 or #fileName > 250 or (fileName:sub(1,1)):find(PERC..'a') == nil then
if p == lp then
print('|cffff5500MLNL Error! <|r',fileName, '|cffff5500> is not a valid save-file name!|r');
end
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
return false;
end
if fileName:sub(-4,-1) ~= '.pld' then
fileName = fileName..'.pld';
end
if MagiLogNLoad.onSaveStarted and MagiLogNLoad.onSaveStarted(p, fileName) == false then
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
return false;
end
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.SAVE_STATES.UPDATE_SAVEABLE_VARS;
MagiLogNLoad.UpdateSaveableVarsForPlayerId(GetPlayerId(p));
if lp == p then
local t0 = os.clock();
fileName = MagiLogNLoad.SAVE_FOLDER_PATH .. fileName;
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.SAVE_STATES.CREATE_UNITS_OF_PLAYER_LOG;
CreateUnitsOfPlayerLog(p);
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.SAVE_STATES.CREATE_ITEMS_OF_PLAYER_LOG;
CreateItemsOfPlayerLog(p);
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.SAVE_STATES.CREATE_EXTRAS_OF_PLAYER_LOG;
CreateExtrasOfPlayerLog(p);
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.SAVE_STATES.CREATE_SERIAL_LOGS;
local logs, tally = CreateSerialLogs(p);
--#PROD
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.SAVE_STATES.COMPRESSING_LOGS;
local str = String2SafeUTF8(COBSEscape(LibDeflate.CompressDeflate(logs)));
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.SAVE_STATES.FILE_IO_SAVE_FILE;
FileIO.SaveFile(fileName, str);
if MagiLogNLoad.onSaveFinished then
MagiLogNLoad.onSaveFinished(p, fileName);
if MagiLogNLoad.willPrintTallyNext then
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.SAVE_STATES.PRINT_TALLY;
PrintTally(os.clock() - t0, tally, 'Sav');
MagiLogNLoad.willPrintTallyNext = false;
end
end
end
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
return true;
end
function MagiLogNLoad.SyncLocalSaveFile(fileName)
local localPid = GetPlayerId(GetLocalPlayer());
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.LOAD_STATES.SYNC_LOCAL_SAVE_FILE;
fileName = fileName:gsub(fileNameSanitizer, '');
if fileName:sub(-4,-1) ~= '.pld' then
fileName = fileName..'.pld';
end
local filePath = MagiLogNLoad.SAVE_FOLDER_PATH .. fileName;
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.LOAD_STATES.SETTING_START_OSCLOCK;
local startTime = os.clock();
issuedWarningChecks.loadTimeWarning = false;
if MagiLogNLoad.onSyncProgressCheckpoint then
issuedWarningChecks.syncProgressCheckpoints = {};
end
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.LOAD_STATES.FILE_IO_LOAD_FILE;
local logStr = FileIO.LoadFile(filePath);
if not logStr then
print('|cffff5500MLNL Error!','Missing save-file or unable to read it! Path:|r',filePath);
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
return false;
end
--#PROD
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.LOAD_STATES.GET_LOG_STR;
logStr = string_char(unpack(GetCharCodesFromUTF8(logStr)));
local logStrLen = #logStr;
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.LOAD_STATES.CREATE_UPLOADING_STREAM;
local tally = streamsOfPlayer[localPid];
if not tally then
tally = {};
streamsOfPlayer[localPid] = tally;
end
logGen.streams = logGen.streams + 1;
uploadingStream = {
localFileName = fileName,
--chunksN = 0,
--chunksSynced = 0,
localChunks = {},
messages = {},
--messagesN = 0,
--messagesSent = 0,
startTime = startTime,
streamStartTick = mainTick,
streamLastTick = mainTick
};
tally[logGen.streams] = uploadingStream;
local arr = tally[logGen.streams].messages;
local arrN = 0;
local ind0 = 1;
local ind1 = 1;
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.LOAD_STATES.CREATE_MESSAGES;
local msgLen = 250-1;
local totalChunks = 1+math_floor(logStrLen/250);
local totalChunksB0 = (totalChunks>>7)+1;
local totalChunksB1 = (totalChunks&127)+1;
while ind0 < logStrLen do
ind1 = ind0 + msgLen;
ind1 = ind1 > logStrLen and logStrLen or ind1;
arrN = arrN + 1;
arr[arrN] = string_char(logGen.streams, (arrN >> 7)+1, (arrN&127)+1, totalChunksB0, totalChunksB1) .. logStr:sub(ind0, ind1);
ind0 = ind1 + 1;
end
uploadingStream.chunksN = arrN;
uploadingStream.chunksSynced = 0;
uploadingStream.messagesN = arrN;
uploadingStream.messagesSent = 0;
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
return true;
end
function MagiLogNLoad.QueuePlayerLoadCommand(p, fileName)
if downloadingStreamFrom == p then
if p == GetLocalPlayer() then
print('|cffff5500MLNL Error! You are already loading a save-file!|r');
end
return;
end
local ind = ArrayIndexOfPlucked(loadingQueue, p, 1);
loadingQueue[ind == -1 and (#loadingQueue+1) or ind] = {p, fileName};
if p == GetLocalPlayer() then
print('|cffffdd00Another player is currently loading a save-file!|r');
print('|cffffdd00Please wait! Your save-file|r', fileName,'|cffffdd00will soon load automatically.|r');
end
end
local function TrySetReferencedLogging(val, pid, entry)
if val == nil then return false end;
local arr = typeStr2ReferencedArraysHash[GetHandleTypeStr(val)];
if not arr then return false end;
if not arr[pid] then
arr[pid] = {};
end
arr = arr[pid];
arr[val] = entry;
if not logEntry2ReferencedHash[entry] then
logEntry2ReferencedHash[entry] = {};
end
logEntry2ReferencedHash[entry][val] = arr;
return true;
end
function MagiLogNLoad.UpdateSaveableVarsForPlayerId(pid)
local log = magiLog.varsOfPlayer[pid];
if not log then
log = {};
magiLog.varsOfPlayer[pid] = log;
end
for name,_ in pairs(saveables.vars) do
local curVal = _G[name];
if curVal == nil and name:sub(1,4) ~= 'udg_' and _G['udg_'..name] ~= nil then
saveables.vars['udg_'..name] = saveables.vars[name];
saveables.vars[name] = nil;
end
end
for name,vals in pairs(saveables.vars) do
local curVal = _G[name];
if curVal and (type(curVal) == 'table' or GetHandleTypeStr(curVal) == 'group') then
if log[name] then
ResetReferencedLogging(log[name]);
end
log[name] = nil;
saveables.vars[name] = nil;
MagiLogNLoad.WillSaveThis(name, true);
elseif curVal ~= vals[1] then
if log[name] then
ResetReferencedLogging(log[name]);
end
if curVal ~= vals[2] then
logGen.vars = logGen.vars+1;
local entry = {logGen.vars, fCodes.SaveIntoNamedVar, {name, curVal}};
log[name] = entry;
TrySetReferencedLogging(curVal, pid, entry);
else
log[name] = nil;
end
vals[1] = curVal;
end
end
end
local function InitAllTriggers()
local trig = CreateTrigger();
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_LOADED);
TriggerAddAction(trig, (function()
local u = GetTriggerUnit();
local transp = GetTransportUnit();
unit2TransportHash[u] = transp;
end));
trig = CreateTrigger();
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_DROP_ITEM);
TriggerAddAction(trig, (function()
local item = GetManipulatedItem();
local log = magiLog.items;
log[item] = {GetPlayerId(GetOwningPlayer(GetManipulatingUnit()))};
end));
trig = CreateTrigger();
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_RESEARCH_FINISH);
TriggerAddAction(trig, (function()
local resId = GetResearched();
if resId == 0 then return end;
local pid = GetPlayerId(GetOwningPlayer(GetResearchingUnit()));
local log = magiLog.researchOfPlayer[pid];
if not log then
log = {};
magiLog.researchOfPlayer[pid] = log;
end
logGen.res = logGen.res + 1;
log[logGen.res] = {logGen.res, fCodes.AddPlayerTechResearched, {{fCodes.GetLoggingPlayer}, resId, 1}};
end));
if MagiLogNLoad.SAVE_CMD_PREFIX and MagiLogNLoad.SAVE_CMD_PREFIX ~= '' then
trig = CreateTrigger();
for i=0,bj_MAX_PLAYER_SLOTS-1 do
TriggerRegisterPlayerChatEvent(trig, Player(i), MagiLogNLoad.SAVE_CMD_PREFIX..' ', false);
end
TriggerAddAction(trig, function()
local p = GetTriggerPlayer();
local pid = GetPlayerId(p);
local localPid = GetPlayerId(GetLocalPlayer());
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.SAVE_STATES.SAVE_CMD_ENTERED;
local fileName = GetEventPlayerChatString();
MagiLogNLoad.CreatePlayerSaveFile(p, StringTrim(fileName:sub(fileName:find(' ')+1, -1)):gsub(fileNameSanitizer, ''));
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
end);
end
if MagiLogNLoad.LOAD_CMD_PREFIX and MagiLogNLoad.LOAD_CMD_PREFIX ~= '' then
trig = CreateTrigger();
for i=0,bj_MAX_PLAYER_SLOTS-1 do
TriggerRegisterPlayerChatEvent(trig, Player(i), MagiLogNLoad.LOAD_CMD_PREFIX..' ', false);
end
TriggerAddAction(trig, (function()
local p = GetTriggerPlayer();
local pid = GetPlayerId(p);
local localPid = GetPlayerId(GetLocalPlayer());
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.LOAD_STATES.LOAD_CMD_ENTERED;
if playerLoadInfo[pid].count <= 0 then
if p == GetLocalPlayer() and MagiLogNLoad.onMaxLoadsError then
MagiLogNLoad.onMaxLoadsError(p);
end
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
return;
end
local fileName = GetEventPlayerChatString();
fileName = StringTrim(fileName:sub(fileName:find(' ')+1, -1));
if p == GetLocalPlayer() and playerLoadInfo[pid].fileNameHash[fileName] and MagiLogNLoad.onAlreadyLoadedWarning then
MagiLogNLoad.onAlreadyLoadedWarning(p, fileName);
end
if p == GetLocalPlayer() then
if downloadingStreamFrom then
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.LOAD_STATES.QUEUE_LOAD;
MagiLogNLoad.QueuePlayerLoadCommand(p, fileName);
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
return;
end
MagiLogNLoad.SyncLocalSaveFile(fileName);
end
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
end));
end
trig = CreateTrigger();
for i=0,bj_MAX_PLAYER_SLOTS-1 do
TriggerRegisterPlayerChatEvent(trig, Player(i), '-credits', true);
end
TriggerAddAction(trig, (function()
if GetTriggerPlayer() == GetLocalPlayer() then
print('|cffffdd00:: MagiLogNLoad v1.1 by ModdieMads. Get it at HiveWorkshop.com!|r');
print('::>> Generously commissioned by the folks @ AzerothRoleplay!');
end
end));
trig = CreateTrigger()
for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
BlzTriggerRegisterPlayerSyncEvent(trig, Player(i), 'MLNL', false);
end
TriggerAddAction(trig, function()
local p = GetTriggerPlayer();
local pid = GetPlayerId(p);
local localPid = GetPlayerId(GetLocalPlayer());
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.LOAD_STATES.SYNC_EVENT;
local str = BlzGetTriggerSyncData();
local streamId = string_byte(str, 1, 1);
if streamId > logGen.streams then
logGen.streams = streamId;
end
local stream = streamsOfPlayer[pid];
if stream and stream[streamId] and stream[streamId].aborted then
PrintDebug('|cffff9900MLNL Warning:PlayerSyncEvent!','Message received from aborted stream!|r');
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
return;
elseif not downloadingStreamFrom then
if MagiLogNLoad.onLoadStarted and MagiLogNLoad.onLoadStarted(p) == false then
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
return;
end
elseif downloadingStreamFrom ~= p then
if p == GetLocalPlayer() and uploadingStream then
print('|cffff5500MLNL Error! You have jammed the network!|r');
MagiLogNLoad.QueuePlayerLoadCommand(p, uploadingStream.localFileName);
uploadingStream = nil;
end
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
return;
end
downloadingStreamFrom = p;
local chunkId = Concat7BitPair(string_byte(str, 2, 3));
local chunksN = Concat7BitPair(string_byte(str, 4, 5));
if not stream then
stream = {};
streamsOfPlayer[pid] = stream;
end
if not stream[streamId] then
stream[streamId] = {
chunksN = chunksN,
chunksSynced = 0,
localChunks = {},
streamStartTick = mainTick
};
end
stream = stream[streamId];
stream.streamLastTick = mainTick;
stream.localChunks[chunkId] = str:sub(6, #str);
stream.chunksSynced = stream.chunksSynced + 1;
--stream.chunksN = stream.chunksN - 1;
if stream.chunksSynced >= stream.chunksN then
str = concat(stream.localChunks);
stream.localChunks = {};
local startTime;
if uploadingStream then
startTime = uploadingStream.startTime;
playerLoadInfo[pid].fileNameHash[uploadingStream.localFileName] = true;
uploadingStream = nil;
end
MagiLogNLoad.LoadPlayerSaveFromString(p, str, startTime);
return;
elseif MagiLogNLoad.onSyncProgressCheckpoint then
MagiLogNLoad.onSyncProgressCheckpoint(math_floor(100*stream.chunksSynced/stream.chunksN),issuedWarningChecks.syncProgressCheckpoints);
end
MagiLogNLoad.stateOfPlayer[localPid] = MagiLogNLoad.BASE_STATES.STANDBY;
end);
end
local function MapPreplacedWidgets()
preplacedWidgets = {
units = {},
unitMap = {},
items = {},
itemMap = {}
};
for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
GroupEnumUnitsOfPlayer(tempGroup, Player(i), Filter(function()
local u = GetFilterUnit();
local params = {Round(GetUnitX(u)), Round(GetUnitY(u)), GetUnitTypeId(u), GetPlayerId(GetOwningPlayer(u))};
preplacedWidgets.units[u] = params;
TableSetDeep(preplacedWidgets.unitMap, u, params);
if UnitInventorySize(u) > 0 then
for i2=0,5 do
local item = UnitItemInSlot(u, i2);
if item then
preplacedWidgets.items[item] = {GetItemTypeId(item), i2, unpack(params)};
end
end
end
return false;
end));
end
EnumItemsInRect(WORLD_BOUNDS.rect, nil, function()
local item = GetEnumItem();
local params = {GetItemTypeId(item), -1, Round(GetItemX(item)), Round(GetItemY(item))};
preplacedWidgets.items[item] = params;
TableSetDeep(preplacedWidgets.itemMap, item, params);
end);
end
local function SaveIntoNamedVar(varName, val)
if varName == nil then
PrintDebug('|cffff5500MLNL Error:SaveIntoNamedVar!', 'Variable name is NIL!|r');
return;
end
_G[varName] = val;
end
-- Proxy tables are expected to NOT have their metatables changed outside of this system.
-- Proxy tables save changes made to them on an individual basis.
-- This allows for only some changes to be saved, or for changes to be saved differently for each player.
local function MakeProxyTable(argName, willSaveChanges)
if argName == nil then
PrintDebug('|cffff5500MLNL Error:MakeProxyTable!','Name passed to MakeProxyTable is NIL!|r');
return false;
end
if type(argName) ~= 'string' then
PrintDebug('|cffff5500MLNL Error:MakeProxyTable!','Name passed to MakeProxyTable must be a string! It is of type:', type(argName),'!|r');
return false;
end
local varName = argName;
local var = _G[varName];
if var == nil then
varName = 'udg_'..argName;
var = _G[varName];
if var == nil or type(var) ~= 'table' then
PrintDebug('|cffff5500MLNL Error:MakeProxyTable!','Name passed must be the name of a GUI Array or Table-type variable!|r');
return false;
end
end
if varName2ProxyTableHash[varName] == nil then
local mt = getmetatable(var);
if mt and (mt.__newindex or mt.__index) then
PrintDebug('|cffff9900MLNL Warning:MakeProxyTable!', 'Metatable of', varName, 'is being overwritten to make it saveable!|r');
end
varName2ProxyTableHash[varName] = {};
end
if rawget(var,0) ~= nil or rawget(var,1) ~= nil then
PrintDebug('|cffff9900MLNL Warning:MakeProxyTable!', 'Assuming passed variable is a <jarray>. Values at index 0 and 1 will be nilled!|r');
end
local tab = varName2ProxyTableHash[varName];
if willSaveChanges then
setmetatable(var, {
__index = function(t, k)
return tab[k];
end,
__newindex = function(t, k, v)
if loggingPid >= 0 then
local log = magiLog.proxyTablesOfPlayer;
if not log[loggingPid] then
log[loggingPid] = {};
end
log = log[loggingPid];
if not log[varName] then
log[varName] = {};
end
log = log[varName];
if not log[var] then
log[var] = {};
end
log = log[var];
if v == nil then
if log[k] ~= nil then
ResetReferencedLogging(log[k]);
end
log[k] = nil;
else
logGen.proxy = logGen.proxy + 1;
local entry = {logGen.proxy, fCodes.SaveIntoNamedTable, {varName, k, v}};
if log[k] then
ResetReferencedLogging(log[k]);
end
log[k] = entry;
TrySetReferencedLogging(k, loggingPid, entry);
TrySetReferencedLogging(v, loggingPid, entry);
end
end
tab[k] = v;
end
});
else
setmetatable(var, {
__index = function(t, k)
return tab[k];
end,
__newindex = function(t, k, v)
tab[k] = v;
end
});
end
rawset(var,0,nil);
rawset(var,1,nil);
return true;
end
local function SaveIntoNamedTable(varName, key, val)
if key == nil or varName == nil or varName == '' then
PrintDebug('|cffff5500MLNL Error:SaveIntoNamedTable!', 'Passed arguments are NIL!|r');
return;
end
local var = _G[varName];
if not var then
PrintDebug('|cffff9900MLNL Warning:SaveIntoNamedTable!', 'Table', varName, 'cannot be found! Creating a new table...|r');
var = {};
_G[varName] = var;
end
if not varName2ProxyTableHash[varName] then
PrintDebug('|cffff9900MLNL Warning:SaveIntoNamedTable!', 'Table', varName, 'is not proxied! Making a proxy table for it...|r');
MakeProxyTable(varName, true);
end
var[key] = val;
end
function MagiLogNLoad.WillSaveThis(argName, willSave)
if argName == nil or argName == '' then
PrintDebug('|cffff5500MLNL Error:MagiLogNLoad.WillSaveThis!', 'Passed name of variable is NIL! It must be a string.|r');
return;
end
if type(argName) == 'table' then
PrintDebug('|cffff5500MLNL Error:MagiLogNLoad.WillSaveThis!', 'Passed name of variable is a table! It must be a string.|r');
return;
end
willSave = willSave == nil and true or willSave;
local varName = argName;
local obj = _G[varName];
if obj == nil then
varName = 'udg_'..argName;
obj = _G[varName];
if obj == nil then
varName = argName;
end
end
if obj == nil then
PrintDebug('|cffff9900MLNL Warning:MagiLogNLoad.WillSaveThis!', 'Variable:', varName, 'is NOT initialized.',
'If that variable is supposed to hold a table, initialize it BEFORE making it saveable.|r');
else
if type(obj) == 'table' then
if next(obj) ~= nil then
PrintDebug('|cffff9900MLNL Warning:MagiLogNLoad.WillSaveThis!', 'Variable', varName,
"is NOT an empty table. Only new key-value pairs will be saved!|r");
end
local mt = getmetatable(obj);
if mt and mt.isHashtable then
if willSave then
saveables.hashtables[obj] = varName;
else
saveables.hashtables[obj] = nil;
end
return true;
end
return MakeProxyTable(varName, willSave);
elseif GetHandleTypeStr(obj) == 'group' then
if willSave then
saveables.groups[obj] = varName;
else
saveables.groups[obj] = nil;
end
return true;
end
end
if willSave then
saveables.vars[varName] = {obj, obj};
else
saveables.vars[varName] = nil;
end
return true;
end
local function HashtableGet(whichHashTable, parentKey)
if GetHandleTypeStr(whichHashTable) == 'hashtable' then
PrintDebug('|cffff5500MLNL Error:HashtableGet', 'Non-proxied hashtable detected! ALL hashtables MUST be created/initialized after MagiLogNLoad.Init()!|r');
end
local index = whichHashTable[parentKey];
if not index then
local tab = {};
index = setmetatable({},{
__index = function(t,k)
return tab[k];
end,
__newindex = function(t,k,v)
if loggingPid >= 0 and saveables.hashtables[whichHashTable] then
local hashtableName = saveables.hashtables[whichHashTable];
local log = magiLog.hashtablesOfPlayer;
if not log[loggingPid] then
log[loggingPid] = {};
end
log = log[loggingPid];
if not log[hashtableName] then
log[hashtableName] = {};
end
log = log[hashtableName];
if not log[whichHashTable] then
log[whichHashTable] = {};
end
log = log[whichHashTable];
if v == nil then
if log[parentKey] and log[parentKey][k] ~= nil then
ResetReferencedLogging(log[parentKey][k]);
log[parentKey][k] = nil;
end
else
if not log[parentKey] then
log[parentKey] = {};
end
log = log[parentKey];
logGen.hashtable = logGen.hashtable + 1;
local entry = {logGen.hashtable, fCodes.SaveIntoNamedHashtable, {hashtableName, v, k, parentKey}};
if log[k] then
ResetReferencedLogging(log[k]);
end
log[k] = entry;
TrySetReferencedLogging(parentKey, loggingPid, entry);
TrySetReferencedLogging(k, loggingPid, entry);
TrySetReferencedLogging(v, loggingPid, entry);
end
end
tab[k] = v;
end
});
whichHashTable[parentKey] = index;
end
return index;
end
function MagiLogNLoad.HashtableSaveInto(value, childKey, parentKey, whichHashTable)
if whichHashTable == nil then
PrintDebug('|cffff5500MLNL Error:MagiLogNLoad.HashtableSaveInto!', 'Passed hashtable argument is NIL!|r');
return false;
end
if childKey == nil or parentKey == nil then
PrintDebug('|cffff5500MLNL Error:MagiLogNLoad.HashtableSaveInto!', 'Passed key arguments are NIL!|r');
return false;
end
HashtableGet(whichHashTable, parentKey)[childKey] = value;
end
function MagiLogNLoad.HashtableLoadFrom(childKey, parentKey, whichHashTable, default)
if whichHashTable == nil then
PrintDebug('|cffff9900MLNL Warning:MagiLogNLoad.HashtableLoadFrom!', 'Passed hashtable argument is NIL! Returning default...|r');
return default;
end
if childKey == nil or parentKey == nil then
PrintDebug('|cffff9900MLNL Warning:MagiLogNLoad.HashtableLoadFrom!', 'Passed key arguments is NIL! Returning default...|r');
return default;
end
local val = HashtableGet(whichHashTable, parentKey)[childKey];
return val ~= nil and val or default;
end
local function SaveIntoNamedHashtable(hashtableName, value, childKey, parentKey)
if not _G[hashtableName] then
PrintDebug('|cffff9900MLNL Warning:SaveIntoNamedHashtable!', 'Hashtable', hashtableName, 'cannot be found! Creating new hashtable...|r');
_G[hashtableName] = InitHashtableBJ();
end
MagiLogNLoad.HashtableSaveInto(value, childKey, parentKey, _G[hashtableName]);
end
local function LogKillDestructable(destr)
if loggingPid < 0 or destr == nil or tempIsGateHash[destr] then return end;
local log = magiLog.destrsOfPlayer;
local pid = loggingPid;
if not log[pid] then
log[pid] = {};
end
log = log[pid];
if not log[destr] then
log[destr] = {};
end
if log[destr][fCodes.LoadCreateDestructable] then
log[destr] = nil;
else
log[destr][fCodes.KillDestructable] = {10, fCodes.KillDestructable, {
{fCodes.GetDestructableByXY, {Round(GetDestructableX(destr)), Round(GetDestructableY(destr))}}
}};
end
end
function MagiLogNLoad.DefineModes(_modes)
if _modes then
MagiLogNLoad.modes = _modes;
elseif guiModes then
local guiModesHash = Array2Hash(guiModes, 0, 15, '');
MagiLogNLoad.modes.debug = guiModesHash[MagiLogNLoad.MODE_GUI_STRS.DEBUG] ~= nil;
MagiLogNLoad.modes.savePreplacedUnitsOfPlayer = guiModesHash[MagiLogNLoad.MODE_GUI_STRS.SAVE_PREPLACED_UNITS_OF_PLAYER] ~= nil;
MagiLogNLoad.modes.saveDestroyedPreplacedUnits = guiModesHash[MagiLogNLoad.MODE_GUI_STRS.SAVE_DESTROYED_PREPLACED_UNITS] ~= nil;
MagiLogNLoad.modes.saveNewUnitsOfPlayer = guiModesHash[MagiLogNLoad.MODE_GUI_STRS.SAVE_NEW_UNITS_OF_PLAYER] ~= nil;
MagiLogNLoad.modes.saveUnitsDisownedByPlayers = guiModesHash[MagiLogNLoad.MODE_GUI_STRS.SAVE_UNITS_DISOWNED_BY_PLAYERS] ~= nil;
MagiLogNLoad.modes.saveStandingPreplacedDestrs = guiModesHash[MagiLogNLoad.MODE_GUI_STRS.SAVE_STANDING_PREPLACED_DESTRS] ~= nil;
MagiLogNLoad.modes.saveDestroyedPreplacedDestrs = guiModesHash[MagiLogNLoad.MODE_GUI_STRS.SAVE_DESTROYED_PREPLACED_DESTRS] ~= nil;
MagiLogNLoad.modes.savePreplacedItems = guiModesHash[MagiLogNLoad.MODE_GUI_STRS.SAVE_PREPLACED_ITEMS] ~= nil;
MagiLogNLoad.modes.saveItemsDroppedManually = guiModesHash[MagiLogNLoad.MODE_GUI_STRS.SAVE_ITEMS_DROPPED_MANUALLY] ~= nil;
MagiLogNLoad.modes.saveAllItemsOnGround = guiModesHash[MagiLogNLoad.MODE_GUI_STRS.SAVE_ALL_ITEMS_ON_GROUND] ~= nil;
end
if MagiLogNLoad.modes.saveDestroyedPreplacedUnits then
if not modalTriggers.saveDestroyedPreplacedUnits then
local trig = CreateTrigger();
modalTriggers.saveDestroyedPreplacedUnits = trig;
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_DEATH);
TriggerAddAction(trig, function()
local u = GetTriggerUnit();
local prepMap = preplacedWidgets.units[u];
if loggingPid < 0 or not prepMap then return end;
local log = magiLog.extrasOfPlayer;
if not log[loggingPid] then
log[loggingPid] = {};
end
log = log[loggingPid];
if not log[fCodes.LoadRemoveUnit] then
log[fCodes.LoadRemoveUnit] = {};
end
log = log[fCodes.LoadRemoveUnit];
local index = XYZW2Index32(
prepMap[1],
prepMap[2],
Lerp(WORLD_BOUNDS.minX, WORLD_BOUNDS.maxX, Terp(MIN_FOURCC, MAX_FOURCC, prepMap[3])),
Lerp(WORLD_BOUNDS.minX, WORLD_BOUNDS.maxX, prepMap[4]/31)
);
if not log[index] then
log[index] = {logGen.extras, fCodes.LoadRemoveUnit, {{fCodes.GetPreplacedUnit, prepMap}}};
end
end);
end
if not IsTriggerEnabled(modalTriggers.saveDestroyedPreplacedUnits) then
EnableTrigger(modalTriggers.saveDestroyedPreplacedUnits);
end
else
if IsTriggerEnabled(modalTriggers.saveDestroyedPreplacedUnits) then
DisableTrigger(modalTriggers.saveDestroyedPreplacedUnits);
end
end
if MagiLogNLoad.modes.saveDestroyedPreplacedDestrs then
if not modalTriggers.saveDestroyedPreplacedDestrs then
local trig = CreateTrigger();
modalTriggers.saveDestroyedPreplacedDestrs = trig;
EnumDestructablesInRect(WORLD_BOUNDS.rect, nil, function()
TriggerRegisterDeathEvent(trig, GetEnumDestructable());
end);
TriggerAddAction(trig, function()
LogKillDestructable(GetTriggerDestructable());
end);
end
if not IsTriggerEnabled(modalTriggers.saveDestroyedPreplacedDestrs) then
EnableTrigger(modalTriggers.saveDestroyedPreplacedDestrs);
end
else
if IsTriggerEnabled(modalTriggers.saveDestroyedPreplacedDestrs) then
DisableTrigger(modalTriggers.saveDestroyedPreplacedDestrs);
end
end
if MagiLogNLoad.modes.savePreplacedItems then
if not modalTriggers.savePreplacedItems then
local trig = CreateTrigger();
modalTriggers.savePreplacedItems = trig;
EnumItemsInRect(WORLD_BOUNDS.rect, nil, function()
TriggerRegisterDeathEvent(trig, GetEnumItem());
end);
TriggerAddAction(trig, function()
local widget = GetTriggerWidget();
local item = nil;
for k,v in pairs(preplacedWidgets.items) do
if k == widget and type(v) == 'table' and next(v) ~= nil then
item = k;
break;
end
end
if loggingPid < 0 or item == nil then return end;
logGen.items = logGen.items + 1;
magiLog.items[item] = {loggingPid, {[fCodes.RemoveItem] = {logGen.items, fCodes.RemoveItem, {{fCodes.GetPreplacedItem, preplacedWidgets.items[item]}}}}};
end);
end
if not IsTriggerEnabled(modalTriggers.savePreplacedItems) then
EnableTrigger(modalTriggers.savePreplacedItems);
end
else
if IsTriggerEnabled(modalTriggers.savePreplacedItems) then
DisableTrigger(modalTriggers.savePreplacedItems);
end
end
if MagiLogNLoad.modes.debug then
print('--- MLNL MODES ---');
for k,v in pairs(MagiLogNLoad.modes) do
print(k, ':', v);
end
print('--- ---------- ---');
end
end
local function InitGUI()
if udg_mlnl_MakeProxyTable then
if type(udg_mlnl_MakeProxyTable) ~= 'table' then
PrintDebug('|cffff9900MLNL Warning:InitGui!','GUI variable mlnl_MakeProxyTable must be an array! Its functionality has been disabled.|r');
else
setmetatable(udg_mlnl_MakeProxyTable, {
__index = function(t, k)
return nil;
end,
__newindex = function(t, k, v)
MakeProxyTable(v);
end
});
rawset(udg_mlnl_MakeProxyTable,0,nil);
rawset(udg_mlnl_MakeProxyTable,1,nil);
end
end
if udg_mlnl_Modes then
if type(udg_mlnl_Modes) ~= 'table' then
PrintDebug('|cffff9900MLNL Warning:InitGui!','GUI variable mlnl_Modes must be an array! Its functionality has been disabled.|r');
else
guiModes = {};
setmetatable(udg_mlnl_Modes, {
__index = function(t, k)
return guiModes[k];
end,
__newindex = function(t, k, v)
guiModes[k] = v;
MagiLogNLoad.DefineModes();
end
});
rawset(udg_mlnl_Modes,0,nil);
rawset(udg_mlnl_Modes,1,nil);
end
end
if udg_mlnl_WillSaveThis then
if type(udg_mlnl_WillSaveThis) ~= 'table' then
PrintDebug('|cffff9900MLNL Warning:InitGui!','GUI variable mlnl_WillSaveThis must be an array! Its functionality has been disabled.|r');
else
local tab = {};
setmetatable(udg_mlnl_WillSaveThis, {
__index = function(t, k)
return tab[k];
end,
__newindex = function(t, k, v)
v = v ~= '' and v or nil;
if tab[k] ~= nil and v ~= tab[k] then
MagiLogNLoad.WillSaveThis(tab[k], false);
end
if v ~= nil then
MagiLogNLoad.WillSaveThis(v, true);
end
tab[k] = v;
end
});
rawset(udg_mlnl_WillSaveThis,0,nil);
rawset(udg_mlnl_WillSaveThis,1,nil);
end
end
if udg_mlnl_LoggingPlayer then
if type(udg_mlnl_LoggingPlayer) ~= 'table' then
PrintDebug('|cffff9900MLNL Warning:InitGui!','GUI variable mlnl_LoggingPlayer must be an array! Its functionality has been disabled.|r');
else
setmetatable(udg_mlnl_LoggingPlayer, {
__index = function(t, k)
return loggingPid > -1 and Player(loggingPid) or nil;
end,
__newindex = function(t, k, v)
MagiLogNLoad.SetLoggingPlayer(v);
end
});
rawset(udg_mlnl_LoggingPlayer,0,nil);
rawset(udg_mlnl_LoggingPlayer,1,nil);
end
end
--[[
Adaptation of "GUI hashtable converter by Tasyen and Bribe" by ModdieMads
Converts GUI hashtables API into Lua Tables, overrides StringHashBJ and GetHandleIdBJ to permit
typecasting, bypasses the 256 hashtable limit by avoiding hashtables.
]]
_G.StringHashBJ = NakedReturn;
_G.StringHash = NakedReturn;
_G.GetHandleIdBJ = NakedReturn;
_G.GetHandleId = NakedReturn;
local last;
_G.GetLastCreatedHashtableBJ = function() return last end;
_G.InitHashtableBJ = function()
last = setmetatable({}, {isHashtable=true});
return last;
end
_G.SaveIntegerBJ = MagiLogNLoad.HashtableSaveInto;
_G.SaveRealBJ = MagiLogNLoad.HashtableSaveInto;
_G.SaveBooleanBJ = MagiLogNLoad.HashtableSaveInto;
_G.SaveStringBJ = MagiLogNLoad.HashtableSaveInto;
local function createDefault(default)
return function(childKey, parentKey, whichHashTable)
return MagiLogNLoad.HashtableLoadFrom(childKey, parentKey, whichHashTable, default)
end
end
local loadNumber = createDefault(0)
_G.LoadIntegerBJ = loadNumber
_G.LoadRealBJ = loadNumber
_G.LoadBooleanBJ = createDefault(false)
_G.LoadStringBJ = createDefault("")
local saveHandleFuncs = {
'SavePlayerHandleBJ', 'SaveWidgetHandleBJ', 'SaveDestructableHandleBJ', 'SaveItemHandleBJ', 'SaveUnitHandleBJ', 'SaveAbilityHandleBJ',
'SaveTimerHandleBJ', 'SaveTriggerHandleBJ', 'SaveTriggerConditionHandleBJ','SaveTriggerActionHandleBJ','SaveTriggerEventHandleBJ',
'SaveForceHandleBJ','SaveGroupHandleBJ','SaveLocationHandleBJ','SaveRectHandleBJ','SaveBooleanExprHandleBJ','SaveSoundHandleBJ',
'SaveEffectHandleBJ', 'SaveUnitPoolHandleBJ','SaveItemPoolHandleBJ','SaveQuestHandleBJ','SaveQuestItemHandleBJ','SaveDefeatConditionHandleBJ',
'SaveTimerDialogHandleBJ','SaveLeaderboardHandleBJ','SaveMultiboardHandleBJ','SaveTrackableHandleBJ','SaveDialogHandleBJ','SaveButtonHandleBJ',
'SaveTextTagHandleBJ','SaveLightningHandleBJ','SaveImageHandleBJ','SaveUbersplatHandleBJ','SaveRegionHandleBJ','SaveFogStateHandleBJ','SaveFogModifierHandleBJ',
'SaveAgentHandleBJ','SaveHashtableHandleBJ'
};
local loadHandleFuncs = {
'LoadPlayerHandleBJ', 'LoadWidgetHandleBJ', 'LoadDestructableHandleBJ', 'LoadItemHandleBJ', 'LoadUnitHandleBJ', 'LoadAbilityHandleBJ',
'LoadTimerHandleBJ', 'LoadTriggerHandleBJ', 'LoadTriggerConditionHandleBJ','LoadTriggerActionHandleBJ','LoadTriggerEventHandleBJ',
'LoadForceHandleBJ','LoadGroupHandleBJ','LoadLocationHandleBJ','LoadRectHandleBJ','LoadBooleanExprHandleBJ','LoadSoundHandleBJ',
'LoadEffectHandleBJ', 'LoadUnitPoolHandleBJ','LoadItemPoolHandleBJ','LoadQuestHandleBJ','LoadQuestItemHandleBJ','LoadDefeatConditionHandleBJ',
'LoadTimerDialogHandleBJ','LoadLeaderboardHandleBJ','LoadMultiboardHandleBJ','LoadTrackableHandleBJ','LoadDialogHandleBJ','LoadButtonHandleBJ',
'LoadTextTagHandleBJ','LoadLightningHandleBJ','LoadImageHandleBJ','LoadUbersplatHandleBJ','LoadRegionHandleBJ','LoadFogStateHandleBJ','LoadFogModifierHandleBJ',
'LoadAgentHandleBJ','LoadHashtableHandleBJ'
};
for _,v in ipairs(saveHandleFuncs) do
_G[v] = MagiLogNLoad.HashtableSaveInto;
end
for _,v in ipairs(loadHandleFuncs) do
_G[v] = MagiLogNLoad.HashtableLoadFrom;
end
saveHandleFuncs = nil;
loadHandleFuncs = nil;
_G.HaveSavedValue = function(childKey, _, parentKey, whichHashTable)
return HashtableGet(whichHashTable, parentKey)[childKey] ~= nil;
end
local flushChildren = function(whichHashTable, parentKey)
if GetHandleTypeStr(whichHashTable) == 'hashtable' then
PrintDebug(
'|cffff5500MLNL Error:Flush__HashtableBJ',
'Non-proxied hashtable detected! All Hashtables must be created/init after MagiLogNLoad.Init()!|r'
);
end
if whichHashTable and parentKey ~= nil then
local tab = whichHashTable[parentKey];
if tab then
for k,_ in pairs(tab) do
tab[k] = nil;
end
whichHashTable[parentKey] = nil;
end
end
end
_G.FlushChildHashtableBJ = flushChildren;
_G.FlushParentHashtableBJ = function(whichHashTable)
for key,_ in pairs(whichHashTable) do
flushChildren(whichHashTable[key]);
whichHashTable[key] = nil;
end
end
end
local function InitAllFunctions()
-- -=-==-= QoL Overrides -=-==-=
oriG.ConvertPlayerColor = _G.ConvertPlayerColor;
_G.ConvertPlayerColor = function(pid)
return oriG.ConvertPlayerColor(pid or 0);
end
-- -=-==-= UNIT LOGGING HOOKS -=-==-=
PrependFunction('SetUnitColor', function(whichUnit, whichColor)
if not whichUnit or not whichColor or not deconvertHash[whichColor] then return end;
local log = magiLog.units;
if not log[whichUnit] then
log[whichUnit] = {};
end
local logEntries = log[whichUnit];
logEntries[fCodes.SetUnitColor] = {50, fCodes.SetUnitColor, {{fCodes.GetLoggingUnit}, {fCodes.ConvertPlayerColor, {deconvertHash[whichColor]}}}};
end);
PrependFunction('SetUnitVertexColor', function(u, r, g, b, a)
if not u or not r or not g or not b then return end;
a = a or 255;
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log[u][fCodes.SetUnitVertexColor] = {51, fCodes.SetUnitVertexColor, {{fCodes.GetLoggingUnit}, r, g, b, a}};
end);
PrependFunction('SetUnitTimeScale', function(u, sca)
if not u then return end;
sca = sca or 1;
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log[u][fCodes.SetUnitTimeScale] = {52, fCodes.SetUnitTimeScale, {{fCodes.GetLoggingUnit}, {fCodes.Div10000, {Round(10000*sca)}}}};
end);
PrependFunction('SetUnitScale', function(u, sx, sy, sz)
if not u or not sx then return end;
sy = sy or sx;
sz = sz or sx;
if not unit2HiddenStat[u] then
unit2HiddenStat[u] = {};
end
unit2HiddenStat[u].scale = sx;
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log[u][fCodes.SetUnitScale] = {54, fCodes.SetUnitScale, {
{fCodes.GetLoggingUnit}, {fCodes.Div10000, {Round(10000*sx)}}, {fCodes.Div10000, {Round(10000*sy)}}, {fCodes.Div10000, {Round(10000*sz)}}
}};
end);
PrependFunction('SetUnitAnimation', function(u, str)
if not u or not str then return end;
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log = log[u];
log[fCodes.SetUnitAnimation] = {[GetUnitCurrentOrder(u)] = {53, fCodes.SetUnitAnimation, {{fCodes.GetLoggingUnit}, {fCodes.utf8char, GetUTF8Codes(str)}}}};
end);
PrependFunction('SetUnitAnimationByIndex', function(u, int)
if not u or not int then return end;
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log = log[u];
log[fCodes.SetUnitAnimation] = {[GetUnitCurrentOrder(u)] = {53, fCodes.SetUnitAnimationByIndex, {{fCodes.GetLoggingUnit}, int}}};
end);
PrependFunction('SetUnitAnimationWithRarity', function(u, str, rarity)
if not u or not str or not rarity then return end;
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log = log[u];
log[fCodes.SetUnitAnimation] = {
[GetUnitCurrentOrder(u)] = {
53, fCodes.SetUnitAnimationWithRarity, {
{fCodes.GetLoggingUnit}, {fCodes.utf8char, GetUTF8Codes(str)}, {fCodes.ConvertRarityControl, {deconvertHash[rarity] or deconvertHash[RARITY_RARE]}}
}
}
};
end);
PrependFunction('UnitAddAbility', function(u, abilid)
if not u or not abilid then return end;
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log = log[u];
if not log[abilid] then
log[abilid] = {};
end
log[abilid][fCodes.UnitAddAbility] = {70, fCodes.UnitAddAbility, {{fCodes.GetLoggingUnit}, abilid}};
end);
PrependFunction('UnitMakeAbilityPermanent', function(u, permanent, abilid)
if not u or not abilid then return end;
if permanent == nil then permanent = true end;
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log = log[u];
if not log[abilid] then
log[abilid] = {};
end
log[abilid][fCodes.UnitMakeAbilityPermanent] = {71, fCodes.UnitMakeAbilityPermanent, {{fCodes.GetLoggingUnit}, permanent, abilid}};
end);
PrependFunction('SetUnitAbilityLevel', function(u, abilid, level)
if not u or not abilid or not level then return end;
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log = log[u];
if not log[abilid] then
log[abilid] = {};
end
log[abilid][fCodes.SetUnitAbilityLevel] = {72, fCodes.SetUnitAbilityLevel, {{fCodes.GetLoggingUnit}, abilid, level}};
end);
PrependFunction('UnitRemoveAbility', function(u, abilid)
if not u or not abilid then return end;
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log = log[u];
if not log[abilid] then
log[abilid] = {};
end
if log[abilid][fCodes.UnitAddAbility] then
log[abilid] = nil;
else
log[abilid] = {[fCodes.UnitRemoveAbility] = {73, fCodes.UnitRemoveAbility, {{fCodes.GetLoggingUnit}, abilid}}};
end
end);
PrependFunction('BlzSetUnitArmor', function(u, armor)
if not u or not armor then return end;
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log[u][fCodes.LoadSetUnitArmor] = {110, fCodes.LoadSetUnitArmor, {{fCodes.GetLoggingUnit}, math_floor((armor-BlzGetUnitArmor(u)+.7)*10)}};
end);
PrependFunction('BlzSetUnitName', function(u, str)
if not u or not str then return end;
if not unit2HiddenStat[u] then
unit2HiddenStat[u] = {};
end
unit2HiddenStat[u].name = str;
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log[u][fCodes.BlzSetUnitName] = {55, fCodes.BlzSetUnitName, {{fCodes.GetLoggingUnit}, {fCodes.utf8char, GetUTF8Codes(str)}}};
end);
PrependFunction('SetUnitMoveSpeed', function(u, speed)
if not u or not speed then return end;
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log[u][fCodes.LoadSetUnitMoveSpeed] = {111, fCodes.LoadSetUnitMoveSpeed, {{fCodes.GetLoggingUnit}, math_floor(speed + .5)}};
end);
PrependFunction('GroupAddUnit', function (g, u)
if not g or not u or not saveables.groups[g] then return end;
local gname = saveables.groups[g];
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log = log[u];
if not log[fCodes.LoadGroupAddUnit] then
log[fCodes.LoadGroupAddUnit] = {};
end
log = log[fCodes.LoadGroupAddUnit];
logGen.groups = logGen.groups+1;
log[gname] = {190, fCodes.LoadGroupAddUnit, {{fCodes.utf8char, GetUTF8Codes(gname)}, {fCodes.GetLoggingUnit}}, logGen.groups};
end);
PrependFunction('GroupRemoveUnit', function (g, u)
if not g or not saveables.groups[g] or not u then return end;
local gname = saveables.groups[g];
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log = log[u];
if log[fCodes.LoadGroupAddUnit] then
log[fCodes.LoadGroupAddUnit][gname] = nil;
end
end);
PrependFunction('SetUnitOwner', function(u, p, changeColor)
if not u or not p then return end;
if changeColor == nil then changeColor = true end;
local newPid = GetPlayerId(p);
local oldPid = GetPlayerId(GetOwningPlayer(u));
if not neutralPIds[newPid] then
if neutralPIds[oldPid] then
magiLog.formerUnits[u] = nil;
end
return;
end
magiLog.formerUnits[u] = oldPid;
local log = magiLog.units;
if not log[u] then
log[u] = {};
end
log[u][fCodes.SetUnitOwner] = {130, fCodes.SetUnitOwner, {{fCodes.GetLoggingUnit}, {fCodes.Player, {newPid}}, {fCodes.I2B, {B2I(changeColor)}}}};
end);
-- -=-==-= DESTRUCTABLE LOGGING HOOKS -=-==-=
WrapFunction('CreateDestructable', function(destr, destrid, __x, __y, face, sca, vari)
if loggingPid < 0 or destr == nil or not destrid or destrid == 0 then return end;
face = face or 0;
sca = sca or 1;
vari = vari or -1;
local log = magiLog.destrsOfPlayer;
local pid = loggingPid;
if not log[pid] then
log[pid] = {};
end
log = log[pid];
local x = GetDestructableX(destr);
local y = GetDestructableY(destr);
if not log[destr] then
log[destr] = {};
end
log[destr] = {[fCodes.LoadCreateDestructable] = {1, fCodes.LoadCreateDestructable, {
destrid, Round(x), Round(y), Round(face), {fCodes.Div10000, {Round(10000*sca)}}, vari
}}};
return destr;
end);
PrependFunction('RemoveDestructable', function(destr)
if loggingPid < 0 or destr == nil then return end;
local log = magiLog.destrsOfPlayer;
local pid = loggingPid;
if not log[pid] then
log[pid] = {};
end
log = log[pid];
if log[destr] and log[destr][fCodes.LoadCreateDestructable] then
log[destr] = nil;
else
--[[
log = magiLog.extrasOfPlayer;
if not log[loggingPid] then
log[loggingPid] = {};
end
log = log[loggingPid];
if not log[fCodes.RemoveDestructable] then
log[fCodes.RemoveDestructable] = {};
end
log = log[fCodes.RemoveDestructable];
local x, y = Round(GetDestructableX(destr)), Round(GetDestructableY(destr));
local index = XY2Index30(x, y);
log[index] = {
logGen.extras, fCodes.RemoveDestructable, {{fCodes.GetDestructableByXY, {x, y}}}
};
]]
log[destr][fCodes.RemoveDestructable] = {90, fCodes.RemoveDestructable, {
{fCodes.GetDestructableByXY, {Round(GetDestructableX(destr)), Round(GetDestructableY(destr))}}
}};
end
end);
PrependFunction('KillDestructable', LogKillDestructable);
PrependFunction('DestructableRestoreLife', function (destr, life, birth)
if loggingPid < 0 or destr == nil or GetDestructableTypeId(destr) == 0 or not life or tempIsGateHash[destr] then return end;
local log = magiLog.destrsOfPlayer;
local pid = loggingPid;
if not log[pid] then
log[pid] = {};
end
log = log[pid];
if not log[destr] then
log[destr] = {};
end
if log[destr][fCodes.KillDestructable] then
log[destr][fCodes.KillDestructable] = nil;
else
log[destr][fCodes.DestructableRestoreLife] = {15, fCodes.DestructableRestoreLife, {
{fCodes.GetDestructableByXY, {Round(GetDestructableX(destr)), Round(GetDestructableY(destr))}}, Round(life), {fCodes.I2B, {B2I(birth)}}
}};
end
end);
PrependFunction('ModifyGateBJ', function (op, destr)
if loggingPid < 0 or not destr or GetDestructableTypeId(destr) == 0 or not op then return end;
tempIsGateHash[destr] = true;
local log = magiLog.destrsOfPlayer;
local pid = loggingPid;
if not log[pid] then
log[pid] = {};
end
log = log[pid];
if not log[destr] then
log[destr] = {};
end
log[destr][fCodes.ModifyGateBJ] = {30, fCodes.ModifyGateBJ, {
op, {fCodes.GetDestructableByXY, {Round(GetDestructableX(destr)), Round(GetDestructableY(destr))}}
}};
end);
PrependFunction('SetDestructableInvulnerable', function (destr, flag)
if loggingPid < 0 or not destr or GetDestructableTypeId(destr) == 0 or flag == nil then return end;
local log = magiLog.destrsOfPlayer;
local pid = loggingPid;
if not log[pid] then
log[pid] = {};
end
log = log[pid];
if not log[destr] then
log[destr] = {};
end
log[destr][fCodes.SetDestructableInvulnerable] = {40, fCodes.SetDestructableInvulnerable, {
{fCodes.GetDestructableByXY, {Round(GetDestructableX(destr)), Round(GetDestructableY(destr))}}, {fCodes.I2B, {B2I(flag)}}
}};
end);
-- -=-==-= TERRAIN LOGGING HOOKS -=-==-=
PrependFunction('SetTerrainType', function(x, y, terrainType, variation, area, shape)
if loggingPid < 0 or not x or not y or not terrainType then return end;
variation = variation or -1;
shape = shape or 0;
area = area or 1;
local log = magiLog.terrainOfPlayer;
if not log[loggingPid] then
log[loggingPid] = {};
end
log = log[loggingPid];
x = math_floor((.5 + x // 64.) * 64);
y = math_floor((.5 + y // 64.) * 64);
logGen.terrain = logGen.terrain + 1;
log[XY2Index30(x,y)] = {logGen.terrain, fCodes.SetTerrainType, {x, y, terrainType, variation, area, shape}};
end);
-- -=-==-= RESEARCH LOGGING HOOKS -=-==-=
PrependFunction('AddPlayerTechResearched', function(p, resId, levels)
if not p or not resId or resId == 0 or not levels then return end;
local pid = GetPlayerId(p);
local log = magiLog.researchOfPlayer[pid];
if not log then
log = {};
magiLog.researchOfPlayer[pid] = log;
end
logGen.res = logGen.res + 1;
log[logGen.res] = {logGen.res, fCodes.AddPlayerTechResearched, {{fCodes.GetLoggingPlayer}, resId, levels}};
end);
PrependFunction('SetPlayerTechResearched', function(p, resId, levels)
if not p or not resId or resId == 0 or not levels then return end;
local pid = GetPlayerId(p);
local log = magiLog.researchOfPlayer[pid];
if not log then
log = {};
magiLog.researchOfPlayer[pid] = log;
end
logGen.res = logGen.res + 1;
log[logGen.res] = {logGen.res, fCodes.SetPlayerTechResearched, {{fCodes.GetLoggingPlayer}, resId, levels}};
end);
-- -=-==-= EXTRAS LOGGING HOOKS -=-==-=
PrependFunction('RemoveUnit', function(u)
if loggingPid < 0 or not u or not preplacedWidgets.units[u] then return end;
local log = magiLog.extrasOfPlayer;
if not log[loggingPid] then
log[loggingPid] = {};
end
log = log[loggingPid];
if not log[fCodes.LoadRemoveUnit] then
log[fCodes.LoadRemoveUnit] = {};
end
log = log[fCodes.LoadRemoveUnit];
local tab = preplacedWidgets.units[u];
local index = XYZW2Index32(
tab[1],
tab[2],
Lerp(WORLD_BOUNDS.minX, WORLD_BOUNDS.maxX, Terp(MIN_FOURCC, MAX_FOURCC, tab[3])),
Lerp(WORLD_BOUNDS.minX, WORLD_BOUNDS.maxX, tab[4]/31)
);
log[index] = {
logGen.extras, fCodes.LoadRemoveUnit, {{fCodes.GetPreplacedUnit, tab}}
};
end);
PrependFunction('SetBlightRect', function (p, r, addBlight)
if not r or not p then return end;
local pid = GetPlayerId(p);
local log = magiLog.extrasOfPlayer;
if not log[pid] then
log[pid] = {};
end
log = log[pid];
if not log[fCodes.SetBlightRect] then
log[fCodes.SetBlightRect] = {};
end
log = log[fCodes.SetBlightRect];
local minx, miny, maxx, maxy = Round(GetRectMinX(r)), Round(GetRectMinY(r)), Round(GetRectMaxX(r)), Round(GetRectMaxY(r));
logGen.extras = logGen.extras + 1;
log[XYZW2Index32(minx, miny, maxx, maxy)] = {
logGen.extras, fCodes.SetBlightRect, {
{fCodes.GetLoggingPlayer},
{fCodes.Rect, {minx, miny, maxx, maxy}},
{fCodes.I2B, {B2I(addBlight)}}
}
};
end);
PrependFunction('SetBlightPoint', function (p, x, y, addBlight)
if not p or not x or not y then return end;
local pid = GetPlayerId(p);
local log = magiLog.extrasOfPlayer;
if not log[pid] then
log[pid] = {};
end
log = log[pid];
if not log[fCodes.SetBlightPoint] then
log[fCodes.SetBlightPoint] = {};
end
log = log[fCodes.SetBlightPoint];
x = Round(x);
y = Round(y);
logGen.extras = logGen.extras + 1
log[XY2Index30(x,y)] = {
logGen.extras, fCodes.SetBlightPoint, {
{fCodes.GetLoggingPlayer}, x, y, {fCodes.I2B, {B2I(addBlight)}}
}
};
end);
local SetBlight = function(p, x, y, radius, addBlight)
if not p or not x or not y or not radius then return end;
local pid = GetPlayerId(p);
local log = magiLog.extrasOfPlayer;
if not log[pid] then
log[pid] = {};
end
log = log[pid];
if not log[fCodes.SetBlight] then
log[fCodes.SetBlight] = {};
end
log = log[fCodes.SetBlight];
logGen.extras = logGen.extras + 1
log[XYZ2Index30(x, y, radius)] = {
logGen.extras, fCodes.SetBlight, {
{fCodes.GetLoggingPlayer},
Round(x),
Round(y),
Round(radius),
{fCodes.I2B, {B2I(addBlight)}}
}
};
end;
PrependFunction('SetBlight', SetBlight);
PrependFunction('SetBlightLoc', function (p, loc, radius, addBlight)
if not p or not loc or not radius then return end;
SetBlight(p, GetLocationX(loc), GetLocationY(loc), radius, addBlight);
end);
local words = {'terrain', 'unit', 'destructable', 'item', 'research', 'extra', 'hashtable', 'proxy', 'var'};
word2CodeHash = TableSetManyPaired(words, Range(1, #words));
code2WordHash = InvertHash(word2CodeHash);
word2LoadLogFuncHash = {
['terrain'] = LoadTerrainLog,
['unit'] = LoadUnitsLog,
['destructable'] = LoadDestrsLog,
['item'] = LoadItemsLog,
['research'] = LoadResearchLog,
['extra'] = LoadExtrasLog,
['hashtable'] = LoadHashtablesLog,
['proxy'] = LoadProxyTablesLog,
['var'] = LoadVarsLog
};
fCodes.BlzSetHeroProperName, fCodes.BlzSetUnitName, fCodes.ConvertPlayerColor, fCodes.ConvertUnitState, fCodes.LoadCreateUnit, fCodes.GetLoggingItem,
fCodes.GetLoggingUnit, fCodes.GetLoggingPlayer, fCodes.SelectHeroSkill, fCodes.SetHeroXP, fCodes.SetItemCharges, fCodes.SetTerrainType, fCodes.SetUnitAnimation,
fCodes.SetUnitColor, fCodes.LoadSetUnitFlyHeight, fCodes.SetUnitState, fCodes.SetUnitTimeScale, fCodes.SetUnitVertexColor, fCodes.SetWidgetLife, fCodes.BlzSetUnitMaxHP,
fCodes.BlzSetUnitMaxMana, fCodes.UnitAddAbility, fCodes.LoadUnitAddItemToSlotById, fCodes.UnitRemoveAbility, fCodes.ForceLoadUnits, fCodes.GetLoggingUnitsSafe0,
fCodes.SetUnitPathing, fCodes.UpdateHeroStats, fCodes.UpdateUnitStats, fCodes.LoadSetUnitArmor, fCodes.LoadSetUnitMoveSpeed, fCodes.LogBaseStatsByType,
fCodes.LoadCreateDestructable, fCodes.GetDestructableByXY, fCodes.UnitMakeAbilityPermanent, fCodes.LoadCreateItem, fCodes.utf8char, fCodes.ModifyGateBJ,
fCodes.RemoveDestructable, fCodes.AddPlayerTechResearched, fCodes.I2B, fCodes.GetLoggingDestr, fCodes.SaveIntoNamedTable, fCodes.SetUnitAnimationByIndex,
fCodes.SetUnitAnimationWithRarity, fCodes.ConvertRarityControl, fCodes.LoadGroupAddUnit, fCodes.GroupRemoveUnit, fCodes.SetUnitPosition, fCodes.Player, fCodes.Rect,
fCodes.LoadSetPlayerState, fCodes.ConvertPlayerState, fCodes.LoadSetWaygate, fCodes.SetUnitOwner, fCodes.SetUnitScale, fCodes.SaveIntoNamedHashtable,
fCodes.SetBlightPoint, fCodes.SetBlight, fCodes.SetBlightRect, fCodes.SetUnitAbilityLevel, fCodes.LoadPreplacedUnit, fCodes.KillDestructable, fCodes.Div10000,
fCodes.LoadUnitAddPreplacedItem, fCodes.GetPreplacedItem, fCodes.Nil, fCodes.SaveIntoNamedVar, fCodes.ShowUnit, fCodes.LoadPreplacedItem,
fCodes.DestructableRestoreLife,fCodes.RemoveItem, fCodes.SetPlayerTechResearched, fCodes.SetDestructableInvulnerable, fCodes.UTF8Codes2Real,
fCodes.KillUnit, fCodes.LoadRemoveUnit, fCodes.GetPreplacedUnit = unpack(Range(1,128));
int2Function = TableSetManyPaired(
{
fCodes.BlzSetHeroProperName, fCodes.BlzSetUnitName, fCodes.ConvertPlayerColor, fCodes.ConvertUnitState, fCodes.LoadCreateUnit, fCodes.GetLoggingItem,
fCodes.GetLoggingUnit, fCodes.GetLoggingPlayer, fCodes.SelectHeroSkill, fCodes.SetHeroXP, fCodes.SetItemCharges, fCodes.SetTerrainType, fCodes.SetUnitAnimation,
fCodes.SetUnitColor, fCodes.LoadSetUnitFlyHeight, fCodes.SetUnitState, fCodes.SetUnitTimeScale, fCodes.SetUnitVertexColor, fCodes.SetWidgetLife, fCodes.BlzSetUnitMaxHP,
fCodes.BlzSetUnitMaxMana, fCodes.UnitAddAbility, fCodes.LoadUnitAddItemToSlotById,fCodes.UnitRemoveAbility,fCodes.ForceLoadUnits, fCodes.GetLoggingUnitsSafe0,
fCodes.SetUnitPathing, fCodes.UpdateHeroStats, fCodes.UpdateUnitStats, fCodes.LoadSetUnitArmor, fCodes.LoadSetUnitMoveSpeed, fCodes.LogBaseStatsByType,
fCodes.LoadCreateDestructable, fCodes.GetDestructableByXY, fCodes.UnitMakeAbilityPermanent, fCodes.LoadCreateItem,fCodes.utf8char, fCodes.ModifyGateBJ,
fCodes.RemoveDestructable, fCodes.AddPlayerTechResearched, fCodes.I2B, fCodes.GetLoggingDestr, fCodes.SaveIntoNamedTable, fCodes.SetUnitAnimationByIndex,
fCodes.SetUnitAnimationWithRarity, fCodes.ConvertRarityControl, fCodes.LoadGroupAddUnit, fCodes.GroupRemoveUnit, fCodes.SetUnitPosition, fCodes.Player, fCodes.Rect,
fCodes.LoadSetPlayerState, fCodes.ConvertPlayerState, fCodes.LoadSetWaygate, fCodes.SetUnitOwner, fCodes.SetUnitScale,fCodes.SaveIntoNamedHashtable,
fCodes.SetBlightPoint, fCodes.SetBlight, fCodes.SetBlightRect, fCodes.SetUnitAbilityLevel, fCodes.LoadPreplacedUnit, fCodes.KillDestructable,fCodes.Div10000,
fCodes.LoadUnitAddPreplacedItem, fCodes.GetPreplacedItem, fCodes.Nil, fCodes.SaveIntoNamedVar, fCodes.ShowUnit, fCodes.LoadPreplacedItem,
fCodes.DestructableRestoreLife, fCodes.RemoveItem, fCodes.SetPlayerTechResearched, fCodes.SetDestructableInvulnerable, fCodes.UTF8Codes2Real,
fCodes.KillUnit, fCodes.LoadRemoveUnit, fCodes.GetPreplacedUnit
}
,
{
BlzSetHeroProperName, BlzSetUnitName, ConvertPlayerColor, ConvertUnitState, LoadCreateUnit, GetLoggingItem,
GetLoggingUnit, GetLoggingPlayer, SelectHeroSkill, SetHeroXP, SetItemCharges, SetTerrainType, SetUnitAnimation,
SetUnitColor, LoadSetUnitFlyHeight, SetUnitState, SetUnitTimeScale, SetUnitVertexColor, SetWidgetLife, BlzSetUnitMaxHP,
BlzSetUnitMaxMana, UnitAddAbility, LoadUnitAddItemToSlotById, UnitRemoveAbility, ForceLoadUnits, GetLoggingUnitsSafe0,
SetUnitPathing, UpdateHeroStats, UpdateUnitStats, LoadSetUnitArmor, LoadSetUnitMoveSpeed, LogBaseStatsByType,
LoadCreateDestructable, GetDestructableByXY, UnitMakeAbilityPermanent, LoadCreateItem, utf8.char, ModifyGateBJ,
RemoveDestructable, AddPlayerTechResearched, I2B, GetLoggingDestr, SaveIntoNamedTable, SetUnitAnimationByIndex,
SetUnitAnimationWithRarity, ConvertRarityControl, LoadGroupAddUnit, GroupRemoveUnit, SetUnitPosition, Player, Rect,
LoadSetPlayerState, ConvertPlayerState, LoadSetWaygate, SetUnitOwner, SetUnitScale, SaveIntoNamedHashtable,
SetBlightPoint, SetBlight, SetBlightRect, SetUnitAbilityLevel, LoadPreplacedUnit, KillDestructable, Div10000,
LoadUnitAddPreplacedItem, GetPreplacedItem, Nil, SaveIntoNamedVar, ShowUnit, LoadPreplacedItem,
DestructableRestoreLife, RemoveItem, SetPlayerTechResearched, SetDestructableInvulnerable, UTF8Codes2Real,
KillUnit, LoadRemoveUnit, GetPreplacedUnit
}
);
fCodeFilters.destrs = {
{[fCodes.KillDestructable]=true, [fCodes.RemoveDestructable]=true},
{[fCodes.LoadCreateDestructable]=true, [fCodes.ModifyGateBJ]=true, [fCodes.DestructableRestoreLife]=true}
};
typeStr2GetterFCode = {
['unit'] = fCodes.GetLoggingUnit,
['item'] = fCodes.GetLoggingItem,
['destructable'] = fCodes.GetLoggingDestr
};
typeStr2TypeIdGetter = {
['unit'] = GetUnitTypeId,
['item'] = GetItemTypeId,
['destructable'] = GetDestructableTypeId
};
typeStr2ReferencedArraysHash = {
['unit'] = magiLog.referencedUnitsOfPlayer,
['item'] = magiLog.referencedItemsOfPlayer,
['destructable'] = magiLog.referencedDestrsOfPlayer
};
fileNameSanitizer = concat({'[^',PERC,'a',PERC,'d',PERC,'.',PERC,'_',PERC,'-]'});
sanitizer = concat({'[^',PERC,'{',PERC,'}',PERC,'d',PERC,',',PERC,'-]'});
end
local function InitAllRects()
WORLD_BOUNDS = GetWorldBounds();
WORLD_BOUNDS = {
rect = WORLD_BOUNDS,
minX = GetRectMinX(WORLD_BOUNDS),
minY = GetRectMinY(WORLD_BOUNDS),
maxX = GetRectMaxX(WORLD_BOUNDS),
maxY = GetRectMaxY(WORLD_BOUNDS)
};
ENUM_RECT = Rect(-64.0, -64.0, 64.0, 64.0);
end
local function InitAllTables()
MagiLogNLoad.BASE_STATES.HASH = TableInvert(MagiLogNLoad.BASE_STATES);
MagiLogNLoad.SAVE_STATES.HASH = TableInvert(MagiLogNLoad.SAVE_STATES);
MagiLogNLoad.LOAD_STATES.HASH = TableInvert(MagiLogNLoad.LOAD_STATES);
for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
playerLoadInfo[i] = {
count = MagiLogNLoad.MAX_LOADS_PER_PLAYER,
fileNameHash = {}
};
end
if MagiLogNLoad.oldTypeId2NewTypeId and next(MagiLogNLoad.oldTypeId2NewTypeId) then
local temp = {};
for k,v in pairs(MagiLogNLoad.oldTypeId2NewTypeId) do
temp[tostring(type(k) == 'string' and FourCC(k) or k)] = tostring(type(v) == 'string' and FourCC(v) or v);
end
MagiLogNLoad.oldTypeId2NewTypeId = temp;
end
end
function MagiLogNLoad.SetLoggingPlayer(pOrId)
local pid;
if not pOrId then
pid = -1;
elseif GetHandleTypeStr(pOrId) == 'player' then
pid = GetPlayerId(pOrId);
else
local int = math.tointeger(pOrId);
if int and Player(int) then
pid = int;
else
PrintDebug('|cffff5500MLNL Error:SetLoggingPlayer!', 'Argument <pOrId> passed is NOT a player nor a player ID!|r');
return;
end
end
--[[
if pid ~= loggingPid and loggingPid >= 0 then
MagiLogNLoad.UpdateSaveableVarsForPlayerId(loggingPid);
end
]]
loggingPid = pid;
return loggingPid;
end
function MagiLogNLoad.ResetLoggingPlayer()
MagiLogNLoad.SetLoggingPlayer(nil)
end
function MagiLogNLoad.GetLoggingPlayerId()
return loggingPid;
end
function MagiLogNLoad.Init(_debug, _skipInitGUI)
if MagiLogNLoad.configVersion == nil then
PrintDebug('|cffff5500MLNL Error:Init!', 'Missing config script!');
return false;
end
for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
MagiLogNLoad.stateOfPlayer[i] = MagiLogNLoad.BASE_STATES.OFF;
end
tempGroup = CreateGroup();
InitAllRects();
InitAllFunctions();
MagiLogNLoad.DefineModes({debug=_debug});
InitAllTables();
InitAllTriggers();
MapPreplacedWidgets();
LibDeflate.InitCompressor();
if not _skipInitGUI then
InitGUI();
end
tabKeyCleaner = {
{key=preplacedWidgets.units, map=preplacedWidgets.unitMap},
{key=preplacedWidgets.items, map=preplacedWidgets.itemMap},
{arr=magiLog.proxyTablesOfPlayer, referenceLogger=true},
{arr=magiLog.hashtablesOfPlayer, referenceLogger=true},
unit2TransportHash,
unit2HiddenStat,
magiLog.units,
magiLog.formerUnits,
{arr=magiLog.unitsOfPlayer},
{arr=magiLog.referencedUnitsOfPlayer},
{arr=magiLog.destrsOfPlayer, chk={fCodes.KillDestructable, fCodes.RemoveDestructable}},
{arr=magiLog.referencedDestrsOfPlayer},
magiLog.items,
{arr=magiLog.itemsOfPlayer},
{arr=magiLog.referencedItemsOfPlayer}
};
tabKeyCleanerSize = 31;
mainTimer = CreateTimer();
TimerStart(mainTimer, TICK_DUR, true, TimerTick);
for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
MagiLogNLoad.stateOfPlayer[i] = MagiLogNLoad.BASE_STATES.STANDBY;
end
PrintDebug(
'|cffff9900MLNL Warning! This update (v1.1) contains [BREAKING] changes!',
'Please refer to the CHANGELOG and adapt your map as needed.|r'
);
MagiLogNLoad.Init = DoNothing;
return true;
end
end
if Debug and Debug.endFile then Debug.endFile() end
-- This is just an illustrative example of what a init function equivalent to the GUI
-- trigger above might look like in Lua.
function InitMagiLogNLoadTest()
MagiLogNLoad.Init(false, false);
MagiLogNLoad.DefineModes({
debug = false,
savePreplacedUnits = true,
savePreplacedItems = true,
savePreplacedDestrs = true,
saveAllUnitsOfPlayer = true,
saveDestrsKilledByUnits = true,
saveAllItemsOnGround = true,
--this mode is overriden by the saveAllItemsOnGround when that is set to <true>.
--saveItemsDroppedManually = false,
saveUnitsDisownedByPlayers = false
});
MagiLogNLoad.SetLoggingPlayer(Player(0));
MagiLogNLoad.WillSaveThis('udg_VAR_TEST_INT');
MagiLogNLoad.WillSaveThis('udg_VAR_TEST_REAL');
MagiLogNLoad.WillSaveThis('udg_VAR_TEST_STR');
MagiLogNLoad.WillSaveThis('udg_UNIT_TEST');
InitHashtableBJ();
udg_HASHTEST = GetLastCreatedHashtableBJ()
MagiLogNLoad.WillSaveThis('udg_HASH_TEST');
end