local nk = { } local function codePointToUTF8(codePoint) if codePoint <= 0x7F then return string.char(codePoint) elseif codePoint <= 0x07FF then return string.char(0xC0 + bit.rshift(codePoint, 6), 0x80 + bit.band(codePoint, 0x3F)) elseif codePoint <= 0xFFFF then return string.char(0xE0 + bit.rshift(codePoint, 12), 0x80 + bit.band(bit.rshift(codePoint, 6), 0x3F), 0x80 + bit.band(codePoint, 0x3f)) else return "?" end end local function convertUTF16to8(text) local out = { } local highSurr for i = 1, #text - 1, 2 do local codeUnit = text:byte(i) + text:byte(i+1) * 256 if codeUnit >= 0xD800 and codeUnit <= 0xDBFF then highSurr = codeUnit - 0xD800 elseif codeUnit >= 0xDC00 and codeUnit <= 0xDFFF then if highSurr then table.insert(out, codePointToUTF8(highSurr * 1024 + codeUnit - 0xDC00 + 0x010000)) highSurr = nil end else table.insert(out, codePointToUTF8(codeUnit)) end end return table.concat(out) end local statDescriptor local statDescriptors = { } function loadStatFile(fileName) if statDescriptors[fileName] then statDescriptor = statDescriptors[fileName] return end statDescriptor = { } statDescriptors[fileName] = statDescriptor local curLang local curDescriptor = { } local order = 1 local function processLine(line) local include = line:match('include "Metadata/StatDescriptions/(.+)"$') if include then for line in io.lines(include) do processLine(line) end return end local noDesc = line:match("no_description ([%w_%+%-%%]+)") if noDesc then statDescriptor[noDesc] = { order = 0 } elseif line:match("description") then curLang = { } curDescriptor = { lang = { ["English"] = curLang }, order = order } order = order + 1 elseif not curDescriptor.stats then local stats = line:match("%d+%s+([%w_%+%-%% ]+)") if stats then curDescriptor.stats = { } for stat in stats:gmatch("[%w_%+%-%%]+") do table.insert(curDescriptor.stats, stat) statDescriptor[stat] = curDescriptor end end else local langName = line:match('lang ".+"') if langName then curLang = { } curDescriptor.lang[langName] = curLang else local statLimits, text, special = line:match('([%d%-#| ]+) "(.-)"%s*(.*)') if statLimits then local desc = { text = text, special = { }, limit = { } } for statLimit in statLimits:gmatch("[%d%-#|]+") do local limit = { } if statLimit == "#" then limit.min = "#" limit.max = "#" elseif statLimit:match("^%d+$") then limit.min = tonumber(statLimit) limit.max = tonumber(statLimit) else limit.min, limit.max = statLimit:match("([%d%-#]+)|([%d%-#]+)") limit.min = tonumber(limit.min) or limit.min limit.max = tonumber(limit.max) or limit.max end table.insert(desc.limit, limit) end for k, v in special:gmatch("([%w%%_]+) (%w+)") do table.insert(desc.special, { k = k, v = tonumber(v) or v, }) nk[k] = v end table.insert(curLang, desc) end end end end local f = io.open(fileName, "rb") local text = convertUTF16to8(f:read("*a")) f:close() for line in text:gmatch("[^\r\n]+") do processLine(line) end print(fileName.. " loaded. ("..order.." stats)") end for k, v in pairs(nk) do --print("'"..k.."' = '"..v.."'") end local function matchLimit(lang, val) for _, desc in ipairs(lang) do local match = true for i, limit in ipairs(desc.limit) do if (limit.max ~= "#" and val[i].min > limit.max) or (limit.min ~= "#" and val[i].min < limit.min) then match = false break end end if match then return desc end end end function describeStats(stats) local out = { } local orders = { } local descriptors = { } for s, v in pairs(stats) do if (v.min ~= 0 or v.max ~= 0) and statDescriptor[s] and statDescriptor[s].stats then descriptors[statDescriptor[s]] = true end end local descOrdered = { } for descriptor in pairs(descriptors) do table.insert(descOrdered, descriptor) end table.sort(descOrdered, function(a, b) return a.order < b.order end) for _, descriptor in ipairs(descOrdered) do local val = { } for i, s in ipairs(descriptor.stats) do val[i] = stats[s] or { min = 0, max = 0 } val[i].fmt = "d" end local desc = matchLimit(descriptor.lang["English"], val) if desc then for _, spec in ipairs(desc.special) do if spec.k == "negate" then val[spec.v].max, val[spec.v].min = -val[spec.v].min, -val[spec.v].max elseif spec.k == "divide_by_one_hundred" then val[spec.v].min = round(val[spec.v].min / 100, 1) val[spec.v].max = round(val[spec.v].max / 100, 1) val[spec.v].fmt = "g" elseif spec.k == "divide_by_one_hundred_2dp" then val[spec.v].min = round(val[spec.v].min / 100, 2) val[spec.v].max = round(val[spec.v].max / 100, 2) val[spec.v].fmt = "g" elseif spec.k == "per_minute_to_per_second" then val[spec.v].min = round(val[spec.v].min / 60, 1) val[spec.v].max = round(val[spec.v].max / 60, 1) val[spec.v].fmt = "g" elseif spec.k == "per_minute_to_per_second_2dp_if_required" then val[spec.v].min = round(val[spec.v].min / 60, 2) val[spec.v].max = round(val[spec.v].max / 60, 2) val[spec.v].fmt = "g" elseif spec.k == "per_minute_to_per_second_0dp" then val[spec.v].min = val[spec.v].min / 60 val[spec.v].max = val[spec.v].max / 60 elseif spec.k == "milliseconds_to_seconds" then val[spec.v].min = val[spec.v].min / 1000 val[spec.v].max = val[spec.v].max / 1000 val[spec.v].fmt = "g" elseif spec.k == "milliseconds_to_seconds_0dp" then val[spec.v].min = val[spec.v].min / 1000 val[spec.v].max = val[spec.v].max / 1000 elseif spec.k == "deciseconds_to_seconds" then val[spec.v].min = val[spec.v].min / 10 val[spec.v].max = val[spec.v].max / 10 val[spec.v].fmt = ".2f" elseif spec.k == "60%_of_value" then val[spec.v].min = val[spec.v].min * 0.6 val[spec.v].max = val[spec.v].max * 0.6 elseif spec.k == "mod_value_to_item_class" then val[spec.v].min = ItemClasses[val[spec.v].min].Name val[spec.v].max = ItemClasses[val[spec.v].max].Name val[spec.v].fmt = "s" elseif spec.k == "multiplicative_damage_modifier" then val[spec.v].min = 100 + val[spec.v].min val[spec.v].max = 100 + val[spec.v].max end end local statDesc = desc.text:gsub("%%(%d)%%", function(n) local v = val[tonumber(n)] if v.min == v.max then return string.format("%"..v.fmt, v.min) else return string.format("(%"..v.fmt.."-%"..v.fmt..")", v.min, v.max) end end):gsub("%%d", function() local v = val[1] if v.min == v.max then return string.format("%"..v.fmt, v.min) else return string.format("(%"..v.fmt.."-%"..v.fmt..")", v.min, v.max) end end):gsub("%%(%d)$(%+?)d", function(n, fmt) local v = val[tonumber(n)] if v.min == v.max then return string.format("%"..fmt..v.fmt, v.min) elseif fmt == "+" then if v.max < 0 then return string.format("-(%d-%d)", -v.min, -v.max) else return string.format("+(%d-%d)", v.min, v.max) end else return string.format("(%"..fmt..v.fmt.."-%"..fmt..v.fmt..")", v.min, v.max) end end):gsub("%%%%","%%") local order = descriptor.order for line in (statDesc.."\\n"):gmatch("([^\\]+)\\n") do table.insert(out, line) table.insert(orders, order) order = order + 0.1 end end end return out, orders end function describeMod(mod) local stats = { } for i = 1, 5 do if mod["StatsKey"..i] then stats[Stats[mod["StatsKey"..i]].Id] = { min = mod["Stat"..i.."Min"], max = mod["Stat"..i.."Max"] } end end return describeStats(stats) end