mardi 28 janvier 2014

[Coding] SkillProfiler (Beta) topic




Do you avoid a different playstyle because respeccing is such a terrible chore? Or are you the kind who just does it anyway because they're so crazy about chopping and changing?

Well, here's the solution to your problems: SkillProfiler.

Once again I'd like to reach out to OVERKILL and present this as yet another example of how allowing Lua coding can permit actually constructive mods that enhance gameplay without impacting anybody else's experience.


Code:


-- This is SkillProfiler. It allows you to save and load skill trees in PayDay 2.

-- The skilltrees are saved to a flat file in JSON format using your
-- Steam ID as part of the filename. You can edit it, but breaking the JSON
-- may well make LUA get upset as I had to remove a pcall() in the JSON parsing
-- code. With that said, I did store the profile names in a separate array, so it
-- should be relatively difficult to break the JSON if all you wish to do is rename
-- the profiles. Additionally, there is a fake profile created called Bootup Skills;
-- you can restore to this profile if you find that all your others are somehow bad.

-- Also note that restoring a profile with more skillpoints than you actually have
-- will only restore as many as it can until you have run out of points. This is
-- by design. You can however choose between making it free to switch profiles or not.

-- Relevant code ends at line 153, everything further is just utilities.
--
-- Pay attention to the variables below however and tune them as desired.
--
-- You can rename the profiles by editing the JSON file AFTER it has been saved.
-- It will only take effect upon the next start of the game.

-- If you want to bundle this code, the "entrypoint" function
-- is managers.skilltree:profileMain() which gets called on the very last line of this file.

-- TL;DR:
-- 1) CHECK IF YOU ARE HAPPY WITH THE DEFAULTS BELOW
-- 2) IT WILL ONLY PARTIALLY LOAD PROFILES IF YOU LACK THE POINTS
-- 3) PROFILES ARE SAVED IN A JSON FILE USING YOUR STEAM ID AS PART OF THE FILE NAME
-- 4) THERE IS A PROFILE CALLED "Bootup Skills" THAT IS CREATED FROM THE FIRST RUN OF THE PROFILER,
--      IT WILL NOT PERSIST BETWEEN MISSIONS.
-- 5) YOU MAY NOT REPOST WITHOUT CREDITING "Harfatus" EXPLICITLY AT THE TOP OF THE POST, IN AT LEAST
--      EQUAL FONT SIZE TO THE LARGEST TEXT IN THE POST BODY.

local SwitchProfilesForFree = true -- Set to false and switching profiles works just like manually respeccing.
local TotalSkillProfiles = 3 -- Number of skill profiles to setup at first run (does nothing after that)
local SubDir = "skilltrees" -- If you want to store profiles in a dir under PD2, create the dir FIRST, then edit this.
---BEGIN CODE, DO NOT EDIT BELOW THIS LINE.
managers.skilltree._switchProfilesForFree = SwitchProfilesForFree
managers.skilltree._totalSkillProfiles = TotalSkillProfiles
managers.skilltree._profileFile = (string.len(SubDir) > 0 and (SubDir .. "\") or "") .. tostring(Steam:userid()) .. "-skilltrees.json"

managers.skilltree.profileInit = managers.skilltree.profileInit or function(self)
    local file = io.open(self._profileFile, "r")
    if file then
        self._skillProfiles = JSON:decode(file:read("*all"))
        file:close()
        if type(self._skillProfiles) ~= "table" then
            io.stderr:write("Warning: JSON was found to be invalid, reiniting profiles")
            self._skillProfiles = nil
        end
    end
    if not self._skillProfiles or #self._skillProfiles.names == 0 then
        io.stderr:write("Initializing all profiles with your current skill tree\n")
        self._skillProfiles = { names = {} }
        self._bootupProfile = deep_clone(self._global.skills)
        for i = 1, self._totalSkillProfiles do
            self._skillProfiles[ tostring(i) ] = deep_clone(self._global.skills)
            self._skillProfiles.names[i] = "Profile " .. tostring(i)
        end
    end
   
    self._treeSkillFinder = {}
    for treeid,tiers in pairs(tweak_data.skilltree.trees) do
        for _,skills in pairs(tiers.tiers) do
            for _,skill in pairs(skills) do
                self._treeSkillFinder[skill] = treeid
            end
        end
    end
    self._profilerInited = true
end

managers.skilltree.profileMain = managers.skilltree.profileMain or function(self)
    if not self._profilerInited then
        self:profileInit()
    end
        local mainMenuOpts = {
        { text = "Load", callback = managers.skilltree.profileChange, data = "Load", Class = managers.skilltree },
        { text = "Save", callback = managers.skilltree.profileChange, data = "Save", Class = managers.skilltree },
        { text = "Cancel", is_cancel_button = true }
    }
   
    local skillTreeMain = SimpleMenu:new("Skill Tree Profiler", "Select whether you wish to load a skill tree or save your current one", mainMenuOpts)
    skillTreeMain:show()
end

managers.skilltree.profileChange = managers.skilltree.profileChange or function(self, action)
    local changeMenuOpts = action == "Load" and { {text = "Bootup Skills", callback = managers.skilltree.doProfileOp,
                                                  data = { profile = -1, action = action }, Class = managers.skilltree } } or {}
    for i,name in ipairs(self._skillProfiles.names) do
        table.insert(changeMenuOpts, { text = name, callback = managers.skilltree.doProfileOp,
                                      data = { profile = tostring(i), action = action }, Class = managers.skilltree }
                    )
    end
    table.insert(changeMenuOpts, { text = "Cancel", is_cancel_button = true })
    local skillTreeProfiler = SimpleMenu:new("Skill Tree Profiler", "Select Profile to " .. action, changeMenuOpts)
    skillTreeProfiler:show()
end

managers.skilltree.doProfileOp = managers.skilltree.doProfileOp or function(self, data)
    local profileID = data.profile
    if data.action == "Save" then
        self:saveProfile(profileID)
    elseif data.action == "Load" then
        self:loadProfile(profileID == -1 and self.bootupProfile or self._skillProfiles[profileID])
    end
end

managers.skilltree.saveProfile = managers.skilltree.saveProfile or function(self, profileID)
    local file = io.open(self._profileFile, "w")
    if not file then
        io.stderr:write("Failed writing JSON\n")
        return
    end
    self._skillProfiles[profileID] = deep_clone(self._global.skills)
    file:write(JSON:encode_pretty(self._skillProfiles))
    file:close()
end
   

managers.skilltree.loadProfile = managers.skilltree.loadProfile or function(self, profile)
    local origMoneyRespec = managers.money.on_respec_skilltree
    local origMoneySpend = managers.money.on_skillpoint_spent
    if self._switchProfilesForFree then
        managers.money.on_respec_skilltree = function(...) end
        managers.money.on_skillpoint_spent = function(...) end
    end
    -- Reset all skill trees:
    for i = 1, #tweak_data.skilltree.trees do
        self:on_respec_tree(i, false)
    end
    for tree_id,tree in pairs(tweak_data.skilltree.trees) do
        if profile[tree.skill] and profile[tree.skill].unlocked > 0 then
            self:unlock_tree(tree_id) -- Apply base skilltree skills
           
            for _,tier in pairs(tree.tiers) do
                for _,skill in pairs(tier) do
                    for i = 1, profile[skill].unlocked do -- Handles regular and Aced.
                        self:unlock(tree_id, skill)
                    end
                end
            end
        end
    end
    managers.money.on_respec_skilltree = origMoneyRespec
    managers.money.on_skillpoint_spend = origMoneySpend
    -- Update GUI items and reload the Skilltree GUI if we're in there:
    managers.menu_component:refresh_player_profile_gui()
    if managers.menu_component._skilltree_gui then
        managers.menu_component:close_skilltree_gui()
        managers.menu_component:_create_skilltree_gui()
    end
end
--RELEVANT CODE END

if not SimpleMenu then
    SimpleMenu = class()

    function SimpleMenu:init(title, message, options)
        self.dialog_data = { title = title, text = message, button_list = {},
                            id = tostring(math.random(0,0xFFFFFFFF)) }
        self.visible = false
        for _,opt in ipairs(options) do
            local elem = {}
            elem.text = opt.text
            opt.data = opt.data or nil
            opt.callback = opt.callback or nil
            elem.callback_func = callback(self, self, "_do_callback",
                                          { data = opt.data,
                                            callback = opt.callback,
                                            Class = opt.Class or nil})
            elem.cancel_button = opt.is_cancel_button or false
            if opt.is_focused_button then
                self.dialog_data.focus_button = #self.dialog_data.button_list+1
            end
            table.insert(self.dialog_data.button_list, elem)
        end
        return self
    end

    function SimpleMenu:_do_callback(info)
        if info.callback then
            if info.data then
                if info.Class then
                    info.callback(info.Class, info.data)
                else
                    info.callback(info.data)
                end
            else
                if info.Class then
                    info.callback(info.Class)
                else
                    info.callback()
                end
            end
        end
        self.visible = false
    end

    function SimpleMenu:show()
        if self.visible then
            return
        end
        self.visible = true
        managers.system_menu:show(self.dialog_data)
    end

    function SimpleMenu:hide()
        if self.visible then
            managers.system_menu:close(self.dialog_data.id)
            self.visible = false
            return
        end
    end
   
    patched_update_input = patched_update_input or function (self, t, dt )
        if self._data.no_buttons then
            return
        end
       
        local dir, move_time
        local move = self._controller:get_input_axis( "menu_move" )

        if( self._controller:get_input_bool( "menu_down" )) then
            dir = 1
        elseif( self._controller:get_input_bool( "menu_up" )) then
            dir = -1
        end
       
        if dir == nil then
            if move.y > self.MOVE_AXIS_LIMIT then
                dir = 1
            elseif move.y < -self.MOVE_AXIS_LIMIT then
                dir = -1
            end
        end

        if dir ~= nil then
            if( ( self._move_button_dir == dir ) and self._move_button_time and ( t < self._move_button_time + self.MOVE_AXIS_DELAY ) ) then
                move_time = self._move_button_time or t
            else
                self._panel_script:change_focus_button( dir )
                move_time = t
            end
        end

        self._move_button_dir = dir
        self._move_button_time = move_time
       
        local scroll = self._controller:get_input_axis( "menu_scroll" )
        if( scroll.y > self.MOVE_AXIS_LIMIT ) then
            self._panel_script:scroll_up()
        elseif( scroll.y < -self.MOVE_AXIS_LIMIT ) then
            self._panel_script:scroll_down()
        end
    end
    managers.system_menu.DIALOG_CLASS.update_input = patched_update_input
    managers.system_menu.GENERIC_DIALOG_CLASS.update_input = patched_update_input
end
if not JSONParser then
    local VERSION = 20140116.10  -- version history at end of file
    local OBJDEF = { VERSION = VERSION }
    local author = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json), version " .. tostring(VERSION) .. " ]-"
    local isArray  = { __tostring = function() return "JSON array"  end }    isArray.__index  = isArray
    local isObject = { __tostring = function() return "JSON object" end }    isObject.__index = isObject


    function OBJDEF:newArray(tbl)
      return setmetatable(tbl or {}, isArray)
    end

    function OBJDEF:newObject(tbl)
      return setmetatable(tbl or {}, isObject)
    end

    local function unicode_codepoint_as_utf8(codepoint)
      --
      -- codepoint is a number
      --
      if codepoint <= 127 then
          return string.char(codepoint)

      elseif codepoint <= 2047 then
          --
          -- 110yyyxx 10xxxxxx        <-- useful notation from http://en.wikipedia.org/wiki/Utf8
          --
          local highpart = math.floor(codepoint / 0x40)
          local lowpart  = codepoint - (0x40 * highpart)
          return string.char(0xC0 + highpart,
                            0x80 + lowpart)

      elseif codepoint <= 65535 then
          --
          -- 1110yyyy 10yyyyxx 10xxxxxx
          --
          local highpart  = math.floor(codepoint / 0x1000)
          local remainder = codepoint - 0x1000 * highpart
          local midpart  = math.floor(remainder / 0x40)
          local lowpart  = remainder - 0x40 * midpart

          highpart = 0xE0 + highpart
          midpart  = 0x80 + midpart
          lowpart  = 0x80 + lowpart

          --
          -- Check for an invalid character (thanks Andy R. at Adobe).
          -- See table 3.7, page 93, in http://www.unicode.org/versions/Unicode5.2.0/ch03.pdf#G28070
          --
          if ( highpart == 0xE0 and midpart < 0xA0 ) or
            ( highpart == 0xED and midpart > 0x9F ) or
            ( highpart == 0xF0 and midpart < 0x90 ) or
            ( highpart == 0xF4 and midpart > 0x8F )
          then
            return "?"
          else
            return string.char(highpart,
                                midpart,
                                lowpart)
          end

      else
          --
          -- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx
          --
          local highpart  = math.floor(codepoint / 0x40000)
          local remainder = codepoint - 0x40000 * highpart
          local midA      = math.floor(remainder / 0x1000)
          remainder      = remainder - 0x1000 * midA
          local midB      = math.floor(remainder / 0x40)
          local lowpart  = remainder - 0x40 * midB

          return string.char(0xF0 + highpart,
                            0x80 + midA,
                            0x80 + midB,
                            0x80 + lowpart)
      end
    end

    function OBJDEF:onDecodeError(message, text, location, etc)
      if text then
          if location then
            message = string.format("%s at char %d of: %s", message, location, text)
          else
            message = string.format("%s: %s", message, text)
          end
      end

      if etc ~= nil then
          message = message .. " (" .. OBJDEF:encode(etc) .. ")"
      end

      if self.assert then
          self.assert(false, message)
      else
          assert(false, message)
      end
    end

    OBJDEF.onDecodeOfNilError  = OBJDEF.onDecodeError
    OBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeError

    function OBJDEF:onEncodeError(message, etc)
      if etc ~= nil then
          message = message .. " (" .. OBJDEF:encode(etc) .. ")"
      end

      if self.assert then
          self.assert(false, message)
      else
          assert(false, message)
      end
    end

    local function grok_number(self, text, start, etc)
      --
      -- Grab the integer part
      --
      local integer_part = text:match('^-?[1-9]%d*', start)
                        or text:match("^-?0",        start)

      if not integer_part then
          self:onDecodeError("expected number", text, start, etc)
      end

      local i = start + integer_part:len()

      --
      -- Grab an optional decimal part
      --
      local decimal_part = text:match('^%.%d+', i) or ""

      i = i + decimal_part:len()

      --
      -- Grab an optional exponential part
      --
      local exponent_part = text:match('^[eE][-+]?%d+', i) or ""

      i = i + exponent_part:len()

      local full_number_text = integer_part .. decimal_part .. exponent_part
      local as_number = tonumber(full_number_text)

      if not as_number then
          self:onDecodeError("bad number", text, start, etc)
      end

      return as_number, i
    end


    local function grok_string(self, text, start, etc)

      if text:sub(start,start) ~= '"' then
          self:onDecodeError("expected string's opening quote", text, start, etc)
      end

      local i = start + 1 -- +1 to bypass the initial quote
      local text_len = text:len()
      local VALUE = ""
      while i <= text_len do
          local c = text:sub(i,i)
          if c == '"' then
            return VALUE, i + 1
          end
          if c ~= '\' then
            VALUE = VALUE .. c
            i = i + 1
          elseif text:match('^\b', i) then
            VALUE = VALUE .. "\b"
            i = i + 2
          elseif text:match('^\f', i) then
            VALUE = VALUE .. "\f"
            i = i + 2
          elseif text:match('^\n', i) then
            VALUE = VALUE .. "\n"
            i = i + 2
          elseif text:match('^\r', i) then
            VALUE = VALUE .. "\r"
            i = i + 2
          elseif text:match('^\t', i) then
            VALUE = VALUE .. "\t"
            i = i + 2
          else
            local hex = text:match('^\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i)
            if hex then
                i = i + 6 -- bypass what we just read

                -- We have a Unicode codepoint. It could be standalone, or if in the proper range and
                -- followed by another in a specific range, it'll be a two-code surrogate pair.
                local codepoint = tonumber(hex, 16)
                if codepoint >= 0xD800 and codepoint <= 0xDBFF then
                  -- it's a hi surrogate... see whether we have a following low
                  local lo_surrogate = text:match('^\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i)
                  if lo_surrogate then
                      i = i + 6 -- bypass the low surrogate we just read
                      codepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16)
                  else
                      -- not a proper low, so we'll just leave the first codepoint as is and spit it out.
                  end
                end
                VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint)

            else

                -- just pass through what's escaped
                VALUE = VALUE .. text:match('^\(.)', i)
                i = i + 2
            end
          end
      end

      self:onDecodeError("unclosed string", text, start, etc)
    end

    local function skip_whitespace(text, start)

      local match_start, match_end = text:find("^[ \n\r\t]+", start) -- [http://www.ietf.org/rfc/rfc4627.txt] Section 2
      if match_end then
          return match_end + 1
      else
          return start
      end
    end

    local grok_one -- assigned later

    local function grok_object(self, text, start, etc)
      if not text:sub(start,start) == '{' then
          self:onDecodeError("expected '{'", text, start, etc)
      end

      local i = skip_whitespace(text, start + 1) -- +1 to skip the '{'

      local VALUE = self.strictTypes and self:newObject { } or { }

      if text:sub(i,i) == '}' then
          return VALUE, i + 1
      end
      local text_len = text:len()
      while i <= text_len do
          local key, new_i = grok_string(self, text, i, etc)

          i = skip_whitespace(text, new_i)

          if text:sub(i, i) ~= ':' then
            self:onDecodeError("expected colon", text, i, etc)
          end

          i = skip_whitespace(text, i + 1)

          local val, new_i = grok_one(self, text, i)

          VALUE[key] = val

          --
          -- Expect now either '}' to end things, or a ',' to allow us to continue.
          --
          i = skip_whitespace(text, new_i)

          local c = text:sub(i,i)

          if c == '}' then
            return VALUE, i + 1
          end

          if text:sub(i, i) ~= ',' then
            self:onDecodeError("expected comma or '}'", text, i, etc)
          end

          i = skip_whitespace(text, i + 1)
      end

      self:onDecodeError("unclosed '{'", text, start, etc)
    end

    local function grok_array(self, text, start, etc)
      if not text:sub(start,start) == '[' then
          self:onDecodeError("expected '['", text, start, etc)
      end

      local i = skip_whitespace(text, start + 1) -- +1 to skip the '['
      local VALUE = self.strictTypes and self:newArray { } or { }
      if text:sub(i,i) == ']' then
          return VALUE, i + 1
      end

      local text_len = text:len()
      while i <= text_len do
          local val, new_i = grok_one(self, text, i)

          table.insert(VALUE, val)

          i = skip_whitespace(text, new_i)

          --
          -- Expect now either ']' to end things, or a ',' to allow us to continue.
          --
          local c = text:sub(i,i)
          if c == ']' then
            return VALUE, i + 1
          end
          if text:sub(i, i) ~= ',' then
            self:onDecodeError("expected comma or '['", text, i, etc)
          end
          i = skip_whitespace(text, i + 1)
      end
      self:onDecodeError("unclosed '['", text, start, etc)
    end


    grok_one = function(self, text, start, etc)
      -- Skip any whitespace
      start = skip_whitespace(text, start)

      if start > text:len() then
          self:onDecodeError("unexpected end of string", text, nil, etc)
      end

      if text:find('^"', start) then
          return grok_string(self, text, start, etc)

      elseif text:find('^[-0123456789 ]', start) then
          return grok_number(self, text, start, etc)

      elseif text:find('^%{', start) then
          return grok_object(self, text, start, etc)

      elseif text:find('^%[', start) then
          return grok_array(self, text, start, etc)

      elseif text:find('^true', start) then
          return true, start + 4

      elseif text:find('^false', start) then
          return false, start + 5

      elseif text:find('^null', start) then
          return nil, start + 4

      else
          self:onDecodeError("can't parse JSON", text, start, etc)
      end
    end

    function OBJDEF:decode(text, etc)
      if type(self) ~= 'table' or self.__index ~= OBJDEF then
          OBJDEF:onDecodeError("JSON:decode must be called in method format", nil, nil, etc)
      end

      if text == nil then
          self:onDecodeOfNilError(string.format("nil passed to JSON:decode()"), nil, nil, etc)
      elseif type(text) ~= 'string' then
          self:onDecodeError(string.format("expected string argument to JSON:decode(), got %s", type(text)), nil, nil, etc)
      end

      if text:match('^%s*$') then
          return nil
      end

      if text:match('^%s*<') then
          -- Can't be JSON... we'll assume it's HTML
          self:onDecodeOfHTMLError(string.format("html passed to JSON:decode()"), text, nil, etc)
      end

      --
      -- Ensure that it's not UTF-32 or UTF-16.
      -- Those are perfectly valid encodings for JSON (as per RFC 4627 section 3),
      -- but this package can't handle them.
      --
      if text:sub(1,1):byte() == 0 or (text:len() >= 2 and text:sub(2,2):byte() == 0) then
          self:onDecodeError("JSON package groks only UTF-8, sorry", text, nil, etc)
      end

      return grok_one(self, text, 1, etc)
    end

    local function backslash_replacement_function(c)
      if c == "\n" then
          return "\n"
      elseif c == "\r" then
          return "\r"
      elseif c == "\t" then
          return "\t"
      elseif c == "\b" then
          return "\b"
      elseif c == "\f" then
          return "\f"
      elseif c == '"' then
          return '\"'
      elseif c == '\' then
          return '\\'
      else
          return string.format("\u%04x", c:byte())
      end
    end

    local chars_to_be_escaped_in_JSON_string
      = '['
      ..    '"'    -- class sub-pattern to match a double quote
      ..    '%\'  -- class sub-pattern to match a backslash
      ..    '%z'  -- class sub-pattern to match a null
      ..    '1' .. '-' .. '1' -- class sub-pattern to match control characters
      .. ']'

    local function json_string_literal(value)
      local newval = value:gsub(chars_to_be_escaped_in_JSON_string, backslash_replacement_function)
      return '"' .. newval .. '"'
    end

    local function object_or_array(self, T, etc)
      --
      -- We need to inspect all the keys... if there are any strings, we'll convert to a JSON
      -- object. If there are only numbers, it's a JSON array.
      --
      -- If we'll be converting to a JSON object, we'll want to sort the keys so that the
      -- end result is deterministic.
      --
      local string_keys = { }
      local number_keys = { }
      local number_keys_must_be_strings = false
      local maximum_number_key

      for key in pairs(T) do
          if type(key) == 'string' then
            table.insert(string_keys, key)
          elseif type(key) == 'number' then
            table.insert(number_keys, key)
            if key <= 0 or key >= math.huge then
                number_keys_must_be_strings = true
            elseif not maximum_number_key or key > maximum_number_key then
                maximum_number_key = key
            end
          else
            self:onEncodeError("can't encode table with a key of type " .. type(key), etc)
          end
      end

      if #string_keys == 0 and not number_keys_must_be_strings then
          --
          -- An empty table, or a numeric-only array
          --
          if #number_keys > 0 then
            return nil, maximum_number_key -- an array
          elseif tostring(T) == "JSON array" then
            return nil
          elseif tostring(T) == "JSON object" then
            return { }
          else
            -- have to guess, so we'll pick array, since empty arrays are likely more common than empty objects
            return nil
          end
      end

      table.sort(string_keys)

      local map
      if #number_keys > 0 then
          --
          -- If we're here then we have either mixed string/number keys, or numbers inappropriate for a JSON array
          -- It's not ideal, but we'll turn the numbers into strings so that we can at least create a JSON object.
          --

          if JSON.noKeyConversion then
            self:onEncodeError("a table with both numeric and string keys could be an object or array; aborting", etc)
          end

          --
          -- Have to make a shallow copy of the source table so we can remap the numeric keys to be strings
          --
          map = { }
          for key, val in pairs(T) do
            map[key] = val
          end

          table.sort(number_keys)

          --
          -- Throw numeric keys in there as strings
          --
          for _, number_key in ipairs(number_keys) do
            local string_key = tostring(number_key)
            if map[string_key] == nil then
                table.insert(string_keys , string_key)
                map[string_key] = T[number_key]
            else
                self:onEncodeError("conflict converting table with mixed-type keys into a JSON object: key " .. number_key .. " exists both as a string and a number.", etc)
            end
          end
      end

      return string_keys, nil, map
    end

    --
    -- Encode
    --
    local encode_value -- must predeclare because it calls itself
    function encode_value(self, value, parents, etc, indent) -- non-nil indent means pretty-printing

      if value == nil then
          return 'null'

      elseif type(value) == 'string' then
          return json_string_literal(value)

      elseif type(value) == 'number' then
          if value ~= value then
            --
            -- NaN (Not a Number).
            -- JSON has no NaN, so we have to fudge the best we can. This should really be a package option.
            --
            return "null"
          elseif value >= math.huge then
            --
            -- Positive infinity. JSON has no INF, so we have to fudge the best we can. This should
            -- really be a package option. Note: at least with some implementations, positive infinity
            -- is both ">= math.huge" and "<= -math.huge", which makes no sense but that's how it is.
            -- Negative infinity is properly "<= -math.huge". So, we must be sure to check the ">="
            -- case first.
            --
            return "1e+9999"
          elseif value <= -math.huge then
            --
            -- Negative infinity.
            -- JSON has no INF, so we have to fudge the best we can. This should really be a package option.
            --
            return "-1e+9999"
          else
            return tostring(value)
          end

      elseif type(value) == 'boolean' then
          return tostring(value)

      elseif type(value) ~= 'table' then
          self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc)

      else
          --
          -- A table to be converted to either a JSON object or array.
          --
          local T = value

          if parents[T] then
            self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc)
          else
            parents[T] = true
          end

          local result_value

          local object_keys, maximum_number_key, map = object_or_array(self, T, etc)
          if maximum_number_key then
            --
            -- An array...
            --
            local ITEMS = { }
            for i = 1, maximum_number_key do
                table.insert(ITEMS, encode_value(self, T[i], parents, etc, indent))
            end

            if indent then
                result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]"
            else
                result_value = "[" .. table.concat(ITEMS, ",") .. "]"
            end

          elseif object_keys then
            --
            -- An object
            --
            local TT = map or T

            if indent then

                local KEYS = { }
                local max_key_length = 0
                for _, key in ipairs(object_keys) do
                  local encoded = encode_value(self, tostring(key), parents, etc, "")
                  max_key_length = math.max(max_key_length, #encoded)
                  table.insert(KEYS, encoded)
                end
                local key_indent = indent .. "    "
                local subtable_indent = indent .. string.rep(" ", max_key_length + 2 + 4)
                local FORMAT = "%s%" .. string.format("%d", max_key_length) .. "s: %s"

                local COMBINED_PARTS = { }
                for i, key in ipairs(object_keys) do
                  local encoded_val = encode_value(self, TT[key], parents, etc, subtable_indent)
                  table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val))
                end
                result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}"

            else

                local PARTS = { }
                for _, key in ipairs(object_keys) do
                  local encoded_val = encode_value(self, TT[key],      parents, etc, indent)
                  local encoded_key = encode_value(self, tostring(key), parents, etc, indent)
                  table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val))
                end
                result_value = "{" .. table.concat(PARTS, ",") .. "}"

            end
          else
            --
            -- An empty array/object... we'll treat it as an array, though it should really be an option
            --
            result_value = "[]"
          end

          parents[T] = false
          return result_value
      end
    end


    function OBJDEF:encode(value, etc)
      if type(self) ~= 'table' or self.__index ~= OBJDEF then
          OBJDEF:onEncodeError("JSON:encode must be called in method format", etc)
      end
      return encode_value(self, value, {}, etc, nil)
    end

    function OBJDEF:encode_pretty(value, etc)
      if type(self) ~= 'table' or self.__index ~= OBJDEF then
          OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc)
      end
      return encode_value(self, value, {}, etc, "")
    end

    function OBJDEF.__tostring()
      return "JSON encode/decode package"
    end

    OBJDEF.__index = OBJDEF

    function OBJDEF:new(args)
      local new = { }

      if args then
          for key, val in pairs(args) do
            new[key] = val
          end
      end

      return setmetatable(new, OBJDEF)
    end

    JSON = OBJDEF:new()
end

managers.skilltree:profileMain()







Aucun commentaire:

Enregistrer un commentaire