diff --git a/Hardcore.lua b/Hardcore.lua index 8c91778a..543def62 100644 --- a/Hardcore.lua +++ b/Hardcore.lua @@ -118,6 +118,7 @@ local speedrun_levels = { [45] = 1, [50] = 1, [60] = 1, + [80] = 1, } local last_received_xguild_chat = "" local debug = false diff --git a/Hardcore_Classic.toc b/Hardcore_Classic.toc index 96fbfab0..508e8a72 100644 --- a/Hardcore_Classic.toc +++ b/Hardcore_Classic.toc @@ -16,6 +16,8 @@ embeds.xml CustomLayouts.lua utils.lua Security.lua +JsonConverter.lua +Base64.lua Hardcore.xml #Tokens diff --git a/Hardcore_Wrath.toc b/Hardcore_Wrath.toc index 8ea81a28..6eed882d 100644 --- a/Hardcore_Wrath.toc +++ b/Hardcore_Wrath.toc @@ -16,6 +16,8 @@ embeds.xml CustomLayouts.lua utils.lua Security.lua +JsonConverter.lua +Base64.lua Hardcore.xml #Tokens diff --git a/JsonConverter.lua b/JsonConverter.lua new file mode 100644 index 00000000..b486df0c --- /dev/null +++ b/JsonConverter.lua @@ -0,0 +1,184 @@ +--[[ json.lua +A compact pure-Lua JSON library. +The main functions are: json.stringify, json.parse. +## json.stringify: +This expects the following to be true of any tables being encoded: + * They only have string or number keys. Number keys must be represented as + strings in json; this is part of the json spec. + * They are not recursive. Such a structure cannot be specified in json. +A Lua table is considered to be an array if and only if its set of keys is a +consecutive sequence of positive integers starting at 1. Arrays are encoded like +so: `[2, 3, false, "hi"]`. Any other type of Lua table is encoded as a json +object, encoded like so: `{"key1": 2, "key2": false}`. +Because the Lua nil value cannot be a key, and as a table value is considerd +equivalent to a missing key, there is no way to express the json "null" value in +a Lua table. The only way this will output "null" is if your entire input obj is +nil itself. +An empty Lua table, {}, could be considered either a json object or array - +it's an ambiguous edge case. We choose to treat this as an object as it is the +more general type. +To be clear, none of the above considerations is a limitation of this code. +Rather, it is what we get when we completely observe the json specification for +as arbitrary a Lua object as json is capable of expressing. +## json.parse: +This function parses json, with the exception that it does not pay attention to +\u-escaped unicode code points in strings. +It is difficult for Lua to return null as a value. In order to prevent the loss +of keys with a null value in a json string, this function uses the one-off +table value json.null (which is just an empty table) to indicate null values. +This way you can check if a value is null with the conditional +`val == json.null`. +If you have control over the data and are using Lua, I would recommend just +avoiding null values in your data to begin with. +--]] + + +function Hardcore_GetJSONConverter() + local json = {} + + + -- Internal functions. + + local function kind_of(obj) + if type(obj) ~= 'table' then return type(obj) end + local i = 1 + for _ in pairs(obj) do + if obj[i] ~= nil then i = i + 1 else return 'table' end + end + if i == 1 then return 'table' else return 'array' end + end + + local function escape_str(s) + local in_char = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'} + local out_char = {'\\', '"', '/', 'b', 'f', 'n', 'r', 't'} + for i, c in ipairs(in_char) do + s = s:gsub(c, '\\' .. out_char[i]) + end + return s + end + + -- Returns pos, did_find; there are two cases: + -- 1. Delimiter found: pos = pos after leading space + delim; did_find = true. + -- 2. Delimiter not found: pos = pos after leading space; did_find = false. + -- This throws an error if err_if_missing is true and the delim is not found. + local function skip_delim(str, pos, delim, err_if_missing) + pos = pos + #str:match('^%s*', pos) + if str:sub(pos, pos) ~= delim then + if err_if_missing then + error('Expected ' .. delim .. ' near position ' .. pos) + end + return pos, false + end + return pos + 1, true + end + + -- Expects the given pos to be the first character after the opening quote. + -- Returns val, pos; the returned pos is after the closing quote character. + local function parse_str_val(str, pos, val) + val = val or '' + local early_end_error = 'End of input found while parsing string.' + if pos > #str then error(early_end_error) end + local c = str:sub(pos, pos) + if c == '"' then return val, pos + 1 end + if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end + -- We must have a \ character. + local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} + local nextc = str:sub(pos + 1, pos + 1) + if not nextc then error(early_end_error) end + return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc)) + end + + -- Returns val, pos; the returned pos is after the number's final character. + local function parse_num_val(str, pos) + local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos) + local val = tonumber(num_str) + if not val then error('Error parsing number at position ' .. pos .. '.') end + return val, pos + #num_str + end + + + -- Public values and functions. + + function json.stringify(obj, as_key) + local s = {} -- We'll build the string as an array of strings to be concatenated. + local kind = kind_of(obj) -- This is 'array' if it's an array or type(obj) otherwise. + if kind == 'array' then + if as_key then error('Can\'t encode array as key.') end + s[#s + 1] = '[' + for i, val in ipairs(obj) do + if i > 1 then s[#s + 1] = ', ' end + s[#s + 1] = json.stringify(val) + end + s[#s + 1] = ']' + elseif kind == 'table' then + if as_key then error('Can\'t encode table as key.') end + s[#s + 1] = '{' + for k, v in pairs(obj) do + if #s > 1 then s[#s + 1] = ', ' end + s[#s + 1] = json.stringify(k, true) + s[#s + 1] = ':' + s[#s + 1] = json.stringify(v) + end + s[#s + 1] = '}' + elseif kind == 'string' then + return '"' .. escape_str(obj) .. '"' + elseif kind == 'number' then + if as_key then return '"' .. tostring(obj) .. '"' end + return tostring(obj) + elseif kind == 'boolean' then + return tostring(obj) + elseif kind == 'nil' then + return 'null' + else + error('Unjsonifiable type: ' .. kind .. '.') + end + return table.concat(s) + end + + json.null = {} -- This is a one-off table to represent the null value. + + function json.parse(str, pos, end_delim) + pos = pos or 1 + if pos > #str then error('Reached unexpected end of input.') end + local pos = pos + #str:match('^%s*', pos) -- Skip whitespace. + local first = str:sub(pos, pos) + if first == '{' then -- Parse an object. + local obj, key, delim_found = {}, true, true + pos = pos + 1 + while true do + key, pos = json.parse(str, pos, '}') + if key == nil then return obj, pos end + if not delim_found then error('Comma missing between object items.') end + pos = skip_delim(str, pos, ':', true) -- true -> error if missing. + obj[key], pos = json.parse(str, pos) + pos, delim_found = skip_delim(str, pos, ',') + end + elseif first == '[' then -- Parse an array. + local arr, val, delim_found = {}, true, true + pos = pos + 1 + while true do + val, pos = json.parse(str, pos, ']') + if val == nil then return arr, pos end + if not delim_found then error('Comma missing between array items.') end + arr[#arr + 1] = val + pos, delim_found = skip_delim(str, pos, ',') + end + elseif first == '"' then -- Parse a string. + return parse_str_val(str, pos + 1) + elseif first == '-' or first:match('%d') then -- Parse a number. + return parse_num_val(str, pos) + elseif first == end_delim then -- End of an object or array. + return nil, pos + 1 + else -- Parse true, false, or null. + local literals = {['true'] = true, ['false'] = false, ['null'] = json.null} + for lit_str, lit_val in pairs(literals) do + local lit_end = pos + #lit_str - 1 + if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end + end + local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10) + error('Invalid json syntax starting at ' .. pos_info_str) + end + end + + return json +end diff --git a/MainMenu.lua b/MainMenu.lua index 771d2805..757c977d 100644 --- a/MainMenu.lua +++ b/MainMenu.lua @@ -609,44 +609,63 @@ end local function DrawVerifyTab(container, _hardcore_character) local ATTRIBUTE_SEPARATOR = "_" local function GenerateVerificationString() - local version = GetAddOnMetadata("Hardcore", "Version") local _, class, _, race, _, name = GetPlayerInfoByGUID(UnitGUID("player")) + local version = GetAddOnMetadata("Hardcore", "Version") local realm = GetRealmName() local level = UnitLevel("player") - local tradePartners = Hardcore_join(_hardcore_character.trade_partners, ",") local converted_successfully = "FALSE" if _hardcore_character.converted_successfully then converted_successfully = "TRUE" end + local game_version_checker = _hardcore_character.game_version or { _G["HardcoreBuildLabel"] } - local baseVerificationData = { - version, - _hardcore_character.guid, - realm, - race, - class, - name, - level, - _hardcore_character.time_played, - _hardcore_character.time_tracked, - #_hardcore_character.deaths, - tradePartners, - _hardcore_character.sacrificed_at, - converted_successfully, - game_version_checker, - } - local baseVerificationString = - Hardcore_join(Hardcore_map(baseVerificationData, Hardcore_stringOrNumberToUnicode), ATTRIBUTE_SEPARATOR) - local bubbleHearthIncidentsVerificationString = - Hardcore_tableToUnicode(_hardcore_character.bubble_hearth_incidents) - local playedtimeGapsVerificationString = Hardcore_tableToUnicode(_hardcore_character.played_time_gap_warnings) - return Hardcore_join({ - baseVerificationString, - bubbleHearthIncidentsVerificationString, - playedtimeGapsVerificationString, - }, ATTRIBUTE_SEPARATOR) + local data_to_encode = {} + data_to_encode["race"] = race + data_to_encode["version"] = version + data_to_encode["class"] = class + data_to_encode["realm"] = realm + data_to_encode["level"] = level + data_to_encode["trade_parters"] = _hardcore_character.trade_partners + + local achievement_nums = {} + for _,v in ipairs(_hardcore_character.achievements) do + if _G.a_id[v] ~= nil then + achievement_nums[#achievement_nums+1] = _G.a_id[v] + end + end + data_to_encode["achievements"] = achievement_nums + + local passive_achievement_nums = {} + for _,v in ipairs(_hardcore_character.passive_achievements) do + if _G.pa_id[v] ~= nil then + passive_achievement_nums[#passive_achievement_nums+1] = _G.pa_id[v] + end + end + data_to_encode["passive_achievements"] = passive_achievement_nums + data_to_encode["converted_successfully"] = converted_successfully + data_to_encode["game_version_checker"] = game_version_checker + data_to_encode["guid"] = _hardcore_character.guid + data_to_encode["name"] = name + data_to_encode["time_played"] = _hardcore_character.time_played + data_to_encode["time_tracked"] = _hardcore_character.time_tracked + data_to_encode["deaths"] = #_hardcore_character.deaths + data_to_encode["sacrificed_at"] = _hardcore_character.sacrificed_at + data_to_encode["bubble_hearth_incidents"] = _hardcore_character.bubble_hearth_incidents + data_to_encode["played_time_gap_warnings"] = _hardcore_character.played_time_gap_warnings + data_to_encode["adjusted_time10"] = _hardcore_character.adjusted_time10 + data_to_encode["adjusted_time15"] = _hardcore_character.adjusted_time15 + data_to_encode["adjusted_time20"] = _hardcore_character.adjusted_time20 + data_to_encode["adjusted_time30"] = _hardcore_character.adjusted_time30 + data_to_encode["adjusted_time40"] = _hardcore_character.adjusted_time40 + data_to_encode["adjusted_time50"] = _hardcore_character.adjusted_time50 + data_to_encode["adjusted_time60"] = _hardcore_character.adjusted_time60 + data_to_encode["adjusted_time80"] = _hardcore_character.adjusted_time80 + + local json = Hardcore_GetJSONConverter() + local base64 = Hardcore_Base64() + return base64.encode(json.stringify(data_to_encode)) end local version = GetAddOnMetadata("Hardcore", "Version") @@ -701,7 +720,7 @@ local function DrawVerifyTab(container, _hardcore_character) local extra_lines = "" - if UnitLevel("player") < max_level then + if UnitLevel("player") < max_level and false then -- local general_rules_description = AceGUI:Create("Label") -- general_rules_description:SetWidth(_menu_width) -- general_rules_description:SetText("\n\nYou must be max level for your chosen expansion (60 or 80) to get a verification string your character.") diff --git a/base64.lua b/base64.lua new file mode 100644 index 00000000..8eafd345 --- /dev/null +++ b/base64.lua @@ -0,0 +1,196 @@ +--[[ + base64 -- v1.5.3 public domain Lua base64 encoder/decoder + no warranty implied; use at your own risk + Needs bit32.extract function. If not present it's implemented using BitOp + or Lua 5.3 native bit operators. For Lua 5.1 fallbacks to pure Lua + implementation inspired by Rici Lake's post: + http://ricilake.blogspot.co.uk/2007/10/iterating-bits-in-lua.html + author: Ilya Kolbin (iskolbin@gmail.com) + url: github.com/iskolbin/lbase64 + COMPATIBILITY + Lua 5.1+, LuaJIT + LICENSE + See end of file for license information. +--]] + + +function Hardcore_Base64() +local base64 = {} + +local extract = _G.bit32 and _G.bit32.extract -- Lua 5.2/Lua 5.3 in compatibility mode +if not extract then + if _G.bit then -- LuaJIT + local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band + extract = function( v, from, width ) + return band( shr( v, from ), shl( 1, width ) - 1 ) + end + elseif _G._VERSION == "Lua 5.1" then + extract = function( v, from, width ) + local w = 0 + local flag = 2^from + for i = 0, width-1 do + local flag2 = flag + flag + if v % flag2 >= flag then + w = w + 2^i + end + flag = flag2 + end + return w + end + else -- Lua 5.3+ + extract = load[[return function( v, from, width ) + return ( v >> from ) & ((1 << width) - 1) + end]]() + end +end + + +function base64.makeencoder( s62, s63, spad ) + local encoder = {} + for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J', + 'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y', + 'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n', + 'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2', + '3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do + encoder[b64code] = char:byte() + end + return encoder +end + +function base64.makedecoder( s62, s63, spad ) + local decoder = {} + for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do + decoder[charcode] = b64code + end + return decoder +end + +local DEFAULT_ENCODER = base64.makeencoder() +local DEFAULT_DECODER = base64.makedecoder() + +local char, concat = string.char, table.concat + +function base64.encode( str, encoder, usecaching ) + encoder = encoder or DEFAULT_ENCODER + local t, k, n = {}, 1, #str + local lastn = n % 3 + local cache = {} + for i = 1, n-lastn, 3 do + local a, b, c = str:byte( i, i+2 ) + local v = a*0x10000 + b*0x100 + c + local s + if usecaching then + s = cache[v] + if not s then + s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)]) + cache[v] = s + end + else + s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)]) + end + t[k] = s + k = k + 1 + end + if lastn == 2 then + local a, b = str:byte( n-1, n ) + local v = a*0x10000 + b*0x100 + t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64]) + elseif lastn == 1 then + local v = str:byte( n )*0x10000 + t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64]) + end + return concat( t ) +end + +function base64.decode( b64, decoder, usecaching ) + decoder = decoder or DEFAULT_DECODER + local pattern = '[^%w%+%/%=]' + if decoder then + local s62, s63 + for charcode, b64code in pairs( decoder ) do + if b64code == 62 then s62 = charcode + elseif b64code == 63 then s63 = charcode + end + end + pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) ) + end + b64 = b64:gsub( pattern, '' ) + local cache = usecaching and {} + local t, k = {}, 1 + local n = #b64 + local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0 + for i = 1, padding > 0 and n-4 or n, 4 do + local a, b, c, d = b64:byte( i, i+3 ) + local s + if usecaching then + local v0 = a*0x1000000 + b*0x10000 + c*0x100 + d + s = cache[v0] + if not s then + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d] + s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8)) + cache[v0] = s + end + else + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d] + s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8)) + end + t[k] = s + k = k + 1 + end + if padding == 1 then + local a, b, c = b64:byte( n-3, n-1 ) + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + t[k] = char( extract(v,16,8), extract(v,8,8)) + elseif padding == 2 then + local a, b = b64:byte( n-3, n-2 ) + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + t[k] = char( extract(v,16,8)) + end + return concat( t ) +end + +return base64 + +end + +--[[ +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2018 Ilya Kolbin +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +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. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +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 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. +------------------------------------------------------------------------------ +--]]