MatthewC529

CC Config API v1.0.0

Jul 23rd, 2015
281
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 37.15 KB | None | 0 0
  1. --[[
  2.     CC Config API v1.0.0 by MatthewC529 (Matt529@GitHub, [email protected])
  3.  
  4.     An API port of the RoboLib Configuration API that is heavily inspired by MinecraftForge's
  5.     Configuration implementation, faithful to it's style. The API is meant to be a bit more stripped down but
  6.     just as functional.
  7.  
  8.     The API revolves around the Configuration object which is created calling cc_config.new, all organization is handled
  9.     internally, although the ConfigValue objects can be accessed using the Configuration:get method. The GET methods also
  10.     act to SET the value to the default if the value does not yet exist.
  11.  
  12.     It is worth note that the default parent directory (if none is provided as the second parameter) is /.config-tmp/VERSION
  13.     (i.e. /.config-tmp/1.0.0 for v1.0.0 of this API).
  14. ]]--
  15.  
  16. _API_NAME = 'CC Config'
  17. _API_VERSION = '1.0.0'
  18. _API_DEBUG_FLAG = false
  19.  
  20. -- Bit API unused since these can be used with floating point and the vlaues ARE greater than 2^32 - 1
  21. _MAX_NUMBER = math.pow(2, 53)
  22. _MIN_NUMBER = -1 * _MAX_NUMBER
  23.  
  24. local infotag, warntag, errtag = 'INFO', 'WARN', 'ERR'
  25.  
  26. LogLevels = {
  27.     INFO = infotag,
  28.     info = infotag,
  29.     INFORMATION = infotag,
  30.     information = infotag,
  31.     WARN = warntag,
  32.     warn = warntag,
  33.     WARNING = warntag,
  34.     warning = warntag,
  35.     ERR = errtag,
  36.     err = errtag,
  37.     ERROR = errtag,
  38.     error = errtag
  39. }
  40.  
  41. local function toboolean(x)
  42.     if type(x) == 'boolean' then return x end
  43.     assert(x and (type(x) == 'string' or type(x) == 'number'), 'toboolean parameter must be a string or number!')
  44.  
  45.     if type(x) == 'string' then
  46.        return x:lower() == 'true'
  47.     else
  48.         return x > 0
  49.     end
  50. end
  51.  
  52. -- Initial Type Declarations
  53. local Configuration = {}
  54. local ConfigCategory = {}
  55. local ConfigValue = {
  56.     Type = {
  57.         STRING  = {id = function() return 'S' end,
  58.                     typename = function() return 'string' end,
  59.                     cast = function(x) return tostring(x) end
  60.                     },
  61.         NUMBER  = {id = function() return 'N' end,
  62.                     typename = function() return 'number' end,
  63.                     cast = function(x) return tonumber(x) end
  64.                     },
  65.         BOOLEAN = {id = function() return 'B' end,
  66.                     typename = function() return 'boolean' end,
  67.                     cast = function(x) return toboolean(x) end
  68.                     }
  69.     }
  70. }
  71.  
  72. -- Metatables
  73. local c_meta = {__index = Configuration}
  74. local cc_meta = {__index = ConfigCategory}
  75. local cv_meta = {__index = ConfigValue}
  76.  
  77. -- Constants
  78. local GLOBALS_LOADED_NAME = '__cc_config_globals_loaded'
  79. local CONFIG_SEPARATOR = '##############################################################'
  80. local DEFAULT_HOME = '/.config-tmp/'.._API_VERSION
  81. local LOG_PATH = DEFAULT_HOME..'/log.txt'
  82. local SUFFIX = '.cfg'
  83. local ALLOWED_CHARS = '._-'
  84.  
  85. -- Helper Functions
  86.  
  87. function version()
  88.     return _API_NAME..' v'.._API_VERSION
  89. end
  90.  
  91. function _CC_API_VERSION()
  92.     return _CC_API_VERSION
  93. end
  94.  
  95. function _IS_DEBUG()
  96.     return _API_DEBUG_FLAG
  97. end
  98.  
  99. function _INIT_DEBUG(logtag)
  100.     -- Reserved for future usage for any reason,
  101.     -- such as initializing a mutlishell tab for debugging
  102.     _API_DEBUG_FLAG = true
  103.  
  104.     if logtag then
  105.         cc_config_log_tag(logtag)
  106.     end
  107. end
  108.  
  109. -- Internal Use, Prints Entire table. Extensive prints all sub-tables as well.
  110. local function _debug_print_table(tbl, extensive)
  111.     assert(tbl and type(tbl) == 'table', 'not table', 2)
  112.  
  113.     extensive = extensive or false
  114.     assert(type(extensive) == 'boolean', 'not boolean', 2)
  115.  
  116.     local res
  117.     if extensive then
  118.         res = '[ '
  119.         for k, v in pairs(tbl) do
  120.             if res ~= '[ ' then
  121.                 res = res..'. '
  122.             end
  123.  
  124.             if type(v) == 'table' then
  125.                 res = res..string.format('%s: %s', tostring(k), _debug_print_table(v, true))
  126.             else
  127.                 res = res..string.format('%s: %s', tostring(k), tostring(v))
  128.             end
  129.         end
  130.         res = res..' ]'
  131.     else
  132.         res = '['..table.concat(tbl, ', ')..']'
  133.     end
  134.  
  135.     return res
  136. end
  137.  
  138. -- Split string into parts delimited by 'sep' or whitespace if'sep' is undefined
  139. local function split_str(self, sep)
  140.     if not sep then sep = '%s' end
  141.  
  142.     local parts = {}
  143.     for s in self:gmatch('([^'..sep..']+)') do
  144.         table.insert(parts, s)
  145.     end
  146.  
  147.     return parts
  148. end
  149.  
  150. -- Log Only Variables
  151. local _internal_caller = _API_NAME:upper()
  152. local _default_caller = 'USER'
  153. local log_opened = false
  154. local log_time = os.clock()
  155. local log_caller_max_width = #_internal_caller > 8 and #_internal_caller or 8
  156. local log_caller_padding = log_caller_max_width
  157.  
  158. -- Print a Formatted Log Message to the log file
  159. local function _log(f, msg, caller, level)
  160.     caller = caller or _internal_caller
  161.     level = level or LogLevels.INFO
  162.  
  163.     if #caller > log_caller_max_width then
  164.         caller = caller:sub(1, log_caller_max_width)
  165.     end
  166.  
  167.     while #caller < log_caller_padding do
  168.         caller = caller ..' '
  169.     end
  170.  
  171.     f:write(string.format('[%s][ %4d s ]: %s: %s\n', caller:upper(), os.clock() - log_time, level:upper(), msg))   
  172. end
  173.  
  174. -- Either retuns the pre-existing loaded log file, or initializes a new file
  175. local function openLogFile()
  176.     local mode = log_opened and 'a' or 'w'
  177.     local f = io.open(LOG_PATH, mode)
  178.  
  179.     if not log_opened then
  180.         -- Print System Information
  181.         local comp_type = pocket and 'Pocket Computer' or (commands and 'Command Computer' or 'Computer')
  182.         local tech_type = (not commands and colors) and 'an Advanced ' or 'a '
  183.         local mc_version = _G['_MC_VERSION'] or '[undefined]'
  184.         local cc_version = _G['_CC_VERSION'] or '[undefined]'
  185.         local luaj_version = _G['_LUAJ_VERSION'] or '[undefined]'
  186.  
  187.         f:write('# Log File for '.._API_NAME..' v'.._API_VERSION..'\n')
  188.         f:write('# by MatthewC529 (Matt529@GitHub, [email protected])\n')
  189.         f:write(string.format('# Using LuaJ %s in CC %s for MC %s\n', luaj_version, cc_version, mc_version))
  190.         f:write(string.format('# This is %s%s running %s | %s ID %d <-- Sanity Check\n\n', tech_type, comp_type, os.version(), os.getComputerLabel(), os.getComputerID()))
  191.         _log(f, 'Opened log file', _internal_caller)
  192.         f:flush()
  193.         log_opened = true
  194.     end
  195.  
  196.     return f
  197. end
  198.  
  199.  
  200. -- Log a message to desired debug
  201. local do_close = true
  202. local function _debug(opts)
  203.     if not _API_DEBUG_FLAG then
  204.         return
  205.     end
  206.  
  207.     -- Options:
  208.     -- __plog: internally used to provide a log file for recursive use, should NOT be specified
  209.     --
  210.     -- B:test: [REQUIRED] whether or not to print
  211.     -- S:msg: [REQUIRED OR msgf w/ fvars] message to log
  212.     -- S:msgf: [REQUIRED w/ fvars or msg] message format to log
  213.     -- T:fvars: [REQUIRED w/msgf or msg] Format Data for msgf
  214.     -- T:lines: [OPTIONAL] a table of Debug Options (as defined above), must be non-mapped
  215.  
  216.     local lf = (not do_close) and opts.__plog or openLogFile()
  217.  
  218.     if not opts.lines then
  219.         assert(opts.msg or (opts.msgf and opts.fvars), 'Missing Error Message'..((opts.msgf and not opts.fvars) and ' - fvars not provided!' or ''))
  220.  
  221.         if opts.msgf then
  222.             opts.msg = string.format(opts.msgf, unpack(opts.fvars))
  223.         end
  224.  
  225.         opts.caller = opts.caller or _internal_caller
  226.         opts.level = opts.level or 'INFO'
  227.  
  228.         _log(lf, opts.msg, opts.caller, opts.level)
  229.     else
  230.         local prev_do_close = do_close
  231.         do_close = false
  232.  
  233.         for i, v in ipairs(opts.lines) do
  234.             v.__plog = lf
  235.             _debug(v)
  236.         end
  237.  
  238.         do_close = prev_do_close
  239.     end
  240.  
  241.     if do_close then
  242.         lf:flush()
  243.         lf:close()
  244.     end
  245. end
  246.  
  247. local function _error(message, level, caller)
  248.     _debug({ msg=table.concat(split_str(message, '\r?\n'), ' | '), caller=caller, level=LogLevels.ERROR})
  249.     error(message, level)
  250. end
  251.  
  252. -- Trim Leading and Trailing whitespace
  253. local function trim_str(s)
  254.   return s:match'^()%s*$' and '' or s:match'^%s*(.*%S)'
  255. end
  256.  
  257. -- Returns true if is alphanumeric or one of the allowed characters
  258. local function isValidChar(c)
  259.     return c:match('%w') or ALLOWED_CHARS:find(c, 1, true)
  260. end
  261.  
  262. -- Returns true if all characters pass the test used in isValidChar
  263. local function isValid(str)
  264.  
  265.     for s in str:gmatch('.') do
  266.         if not isValidChar(s) then
  267.             return false
  268.         end
  269.     end
  270.  
  271.     return true
  272. end
  273.  
  274. -- Gets true length of table (as opposed to index-based when using the '#' operator)
  275. local function len(table)
  276.     local count = 0
  277.  
  278.     for k,_ in pairs(table) do
  279.         if table[k] ~= nil then
  280.             count = count + 1
  281.         end
  282.     end
  283.  
  284.     return count
  285. end
  286.  
  287. --[[
  288.     Applies a function to each value of a table, if a value is returned by this
  289.     function, that value is returned and the iteration stops. A function that returns nothing
  290.     WILL apply to ALL elements.
  291.  
  292.     This function uses pairs() since only the value matters and the key is ignored, therefore
  293.     this works for all tables. Even tables with non-numeric indices.
  294. ]]--
  295. local function foreach(table, func, ...)
  296.     local args = {...}
  297.     local val = nil
  298.  
  299.     for _, v in pairs(table) do
  300.         if len(args) > 0 then
  301.             val = func(v, unpack(args))
  302.         else
  303.             val = func(v)
  304.         end
  305.  
  306.         if val ~= nil then
  307.             break
  308.         end
  309.     end
  310.  
  311.     return val
  312. end
  313.  
  314. --[[
  315.     Applies a function to each value of a table, if a value is returned by
  316.     this function, the index of the element on which the function returned a value
  317.     will be returned. Otherwise -1 is returned and the function applies to ALL elements.
  318.  
  319.     This function only works for tables where ipairs works properly.
  320. ]]--
  321. local function iforeach(table, func, ...)
  322.     local args = {...}
  323.     local idx
  324.  
  325.     for i, v in ipairs(table) do
  326.         if len(args) > 0 and func(v, unpack(args)) then
  327.             idx = i
  328.         elseif  len(args) == 0 and func(v, unpack(args)) then
  329.             idx = i
  330.         end
  331.  
  332.         if idx then
  333.             break
  334.         end
  335.     end
  336.  
  337.     return idx or -1
  338. end
  339.  
  340. -- Returns true if the table contains a key-value pair that uses the given key
  341. local function containsKey(table, key)
  342.     assert(table and key, 'missing parameter', 2)
  343.     assert(type(table) == 'table' and type(key) == 'string', 'type mismatch', 2)
  344.  
  345.     for k, v in pairs(table) do
  346.         if k == key and not not v then
  347.             return true
  348.         end
  349.     end
  350.  
  351.     return false
  352. end
  353.  
  354. -- Simply Opens and Closes a file, ensures creation
  355. local function touch(path)
  356.     io.open(path, 'w'):close()
  357. end
  358.  
  359. -- Returns the remainder of division of a by b.
  360. local function mod(a, b)
  361.     return a - math.floor(a/b)*b
  362. end
  363.  
  364. -- Determines the ConfigValue.Type value represented by id
  365. local function getTypeFor(id)
  366.     return foreach(ConfigValue.Type, function(val) return val.id() == id and val or nil end)
  367. end
  368.  
  369. -- Determines if 'values' is of the given type. If table, checks all elements.
  370. local function isType(typename, values)
  371.     assert(type(typename) == 'string', 'isType\'s typename parameter MUST be a string!', 2)
  372.     assert(values ~= nil, 'values must NOT be nil!', 2)
  373.  
  374.     if type(values) == 'table' then
  375.         for i=1,#values do
  376.             if type(values[i]) ~= typename then
  377.                 return false
  378.             end
  379.         end
  380.  
  381.         return true
  382.     else
  383.         return type(values) == typename
  384.     end
  385. end
  386.  
  387. -- Checks if two objects are of the same type, delegates to isType
  388. local function typeEquals(x, y)
  389.     return type(x) == type(y)
  390. end
  391.  
  392. -- Checks if ALL parameters are of type 'table'
  393. local function isTable(...)
  394.     return isType('table', {...})
  395. end
  396.  
  397. -- Checks if ALL parameters are of type 'string'
  398. local function isString(...)
  399.     return isType('string', {...})
  400. end
  401.  
  402. -- Checks if ALL parameters are of type 'number'
  403. local function isNumber(...)
  404.     return isType('number', {...})
  405. end
  406.  
  407. -- Checks if ALL parameters are a number (via isNumber) and whole.
  408. local function isInteger(...)
  409.     local params = {...}
  410.  
  411.     if not isNumber(unpack(params)) then
  412.         return false
  413.     end
  414.  
  415.     for i=1,#params do
  416.         if mod(params[i], 1) ~= 0 then
  417.             return false
  418.         end
  419.     end
  420.  
  421.     return true
  422. end
  423.  
  424. local function table_to_string(tbl, sep, deep)
  425.     local first = true
  426.     local res
  427.  
  428.     for _, v in ipairs(tbl) do
  429.         local cur
  430.         if deep and isTable(v) then
  431.             cur = '[ '..table_to_string(v, sep, deep)..' ]'
  432.         else
  433.             cur = tostring(v)
  434.         end
  435.  
  436.         res = first and cur or (res..', '..cur)
  437.         first = false
  438.     end
  439.  
  440.     if #tbl == 0 then
  441.         res = ''
  442.     end
  443.  
  444.     return res
  445. end
  446.  
  447. -- Returns a shallow copy of a table, sub-tables are not copied.
  448. local function shallow_copy(orig)
  449.     local copy
  450.     if type(orig) == 'table' then
  451.         copy = {}
  452.         for k, v in pairs(orig) do
  453.             copy[k] = v
  454.         end
  455.     else
  456.         copy = orig
  457.     end
  458.  
  459.     return copy
  460. end
  461.  
  462. -- Returns a deep copy of a table, sub-tables ARE copied
  463. local function deep_copy(orig)
  464.     local copy
  465.     if type(orig) == 'table' then
  466.         copy = {}
  467.         for k, v in pairs(orig) do
  468.             copy[deep_copy(k)] = deep_copy(v)
  469.         end
  470.     else
  471.         copy = orig
  472.     end
  473.  
  474.     return copy
  475. end
  476.  
  477. -- A map of tables to read-only proxies (views)
  478. local _proxies = setmetatable({}, {__mode = 'k'})
  479.  
  480. -- API Functions
  481.  
  482. --[[
  483.     Returns the value type related to the descriptor as found in the ids of ConfigValue.Type tables.
  484.  
  485.     'string'  or 's' or 1 = ConfigValue.Type.STRING
  486.     'number'  or 'n' or 2 = ConfigValue.Type.NUMBER
  487.     'boolean' or 'b' or 3 = ConfigValue.Type.BOOLEAN
  488. ]]--
  489. function getValueType(descriptor)
  490.     local res
  491.  
  492.     if isString(descriptor) then
  493.  
  494.         -- 1 char id
  495.         if #descriptor == 1 then
  496.             for _, v in pairs(ConfigValue.Type) do
  497.                 if descriptor:upper() == v.id() then
  498.                     res = v
  499.                 end
  500.             end
  501.         else
  502.             res = ConfigValue.Type[descriptor:upper()]
  503.         end
  504.     elseif isInteger(descriptor) then
  505.         local idx = mod(descriptor, len(ConfigValue.Type))
  506.         local tmp = 1
  507.  
  508.         for _, v in pairs(ConfigValue.Type) do
  509.             if tmp == idx then
  510.                 res = v
  511.                 break
  512.             end
  513.  
  514.             tmp = tmp + 1
  515.         end
  516.     end
  517.  
  518.     assert(res, 'Invalid Descriptor! Must either be a typename of string, number or boolean or must be an integer.', 2)
  519.     return res
  520. end
  521.  
  522. function new(name, homeDirectory, fileHeaderDescription)
  523.  
  524.     local args = {name, homeDirectory, fileHeaderDescription}
  525.  
  526.     name = table.remove(args, 1)
  527.     fileHeaderDescription = table.remove(args)
  528.     homeDirectory = #args > 0 and table.remove(args) or DEFAULT_HOME
  529.  
  530.     local obj = {
  531.         filename = name..SUFFIX,
  532.         path = string.format('%s/%s', homeDirectory, name..SUFFIX),
  533.         categories = {},
  534.         changed = false,
  535.         _p_file_name = nil,
  536.         _header_desc = fileHeaderDescription or '',
  537.         _cfg_log_id = name:upper()
  538.     }
  539.  
  540.     setmetatable(obj, c_meta)
  541.     obj:load()
  542.  
  543.     _debug({msgf='Config File \'%s\' [%s]', fvars={name, obj.path}})
  544.     return obj
  545. end
  546.  
  547. local function determineValueType(val)
  548.     if not isTable(val) then
  549.         _debug({ msgf='Determining Identifier of "%s" to \'%s\'', fvars={tostring(val), type(val):sub(1,1)}})
  550.         return type(val):sub(1,1)
  551.     else
  552.         _debug({ msg='Determining Identifier for table: [ '..table_to_string(val, ', ')..' ]'})
  553.         local t
  554.         for _, v in pairs(val) do
  555.             if isTable(v) then
  556.                 t = determineValueType(v)
  557.             else
  558.                 t = type(v):sub(1,1)
  559.             end
  560.  
  561.             if t then return t end
  562.         end
  563.     end
  564.  
  565.     return nil
  566. end
  567.  
  568. function Configuration:getName()
  569.     return self.filename:sub(1, #self.filename - 4)
  570. end
  571.  
  572. function Configuration:getFileName()
  573.     return self.filename
  574. end
  575.  
  576. function Configuration:_logid()
  577.     return self._cfg_log_id
  578. end
  579.  
  580. function Configuration:get(category, key, defaultValue, comment, min, max, valids)
  581.     assert(category and key and defaultValue ~= nil, 'Settings require at least a Category, Key/Name and default value')
  582.  
  583.     category = category:lower()
  584.  
  585.     local v_type = determineValueType(defaultValue)
  586.     local cat = self:getCategory(category)
  587.    
  588.     if not isTable(valids) then
  589.         valdis = not not valids and {v_vals} or nil
  590.     end
  591.  
  592.     local prop
  593.     if cat:containsKey(key) then
  594.         prop = cat:get(key)
  595.  
  596.         if not prop:getType() then
  597.             prop = _cvalue(key, v_type, prop:getRaw(), prop:getValidValues())
  598.             cat:put(key, prop)
  599.         end
  600.     else
  601.         prop = _cvalue(key, v_type, defaultValue, valids)
  602.        
  603.         prop:setValue(defaultValue)
  604.         cat:put(key, prop)
  605.     end
  606.  
  607.     prop:setDefault(defaultValue)
  608.  
  609.     if comment then
  610.         prop:setComment(comment)
  611.     end
  612.  
  613.     if min then prop:setMin(min) end
  614.     if max then prop:setMax(max) end
  615.  
  616.  
  617.     if _API_DEBUG_FLAG then
  618.         if prop:isListValue() then
  619.             _debug({msgf=self:_logid()..' GET [Cat: %s, Key: %s, val: %s]', fvars={category, key, prop:getString()}})
  620.         end
  621.     end
  622.  
  623.     return prop
  624. end
  625.  
  626. function Configuration:resetChanged()
  627.     self.changed = false
  628.     foreach(self.categories, function(cat) cat:_reset_changed() end)
  629. end
  630.  
  631. function Configuration:hasChanged()
  632.     if self.changed then return true end
  633.  
  634.     for _, v in pairs(self.categories) do
  635.         if v:hasChanged() then
  636.             return true
  637.         end
  638.     end
  639.  
  640.     return false
  641. end
  642.  
  643. local function matchAndFirst(s, pattern)
  644.     local res
  645.     local matches = 0
  646.  
  647.     for match in s:gmatch(pattern) do
  648.         if not res then
  649.             res = match
  650.         end
  651.  
  652.         matches = matches + 1
  653.     end
  654.  
  655.     return not not res, res, matches
  656. end
  657.  
  658. local function getQualifiedName(name, parentNode)
  659.     return parentNode and getQualifiedName(parentNode.id, parentNode.parent)..'.'..name or name
  660. end
  661.  
  662. function Configuration:load()
  663.     if not fs.exists(self.path) then
  664.         self.categories = {}
  665.         touch(self.path)
  666.  
  667.         return
  668.     end
  669.  
  670.     local out = io.open(self.path, 'r')
  671.  
  672.     -- Parsing
  673.     local cur_cat
  674.     local cur_type
  675.     local cur_name
  676.     local tmp_list
  677.     local line_num = 0
  678.  
  679.     for cline in out:lines() do
  680.         line_num = line_num + 1
  681.  
  682.         local s_success, s_match, s_count = matchAndFirst(cline, 'START: \"([^\"]+)\"')
  683.         local e_success, e_match, e_count = matchAndFirst(cline, 'END: \"([^\"]+)\"')
  684.  
  685.         if s_success then
  686.             self._p_file_name = s_match
  687.             self.categories = {}
  688.         elseif e_success then
  689.             self._p_file_name = e_match
  690.         else
  691.  
  692.             local n_start = -1
  693.             local n_end = -1
  694.             local skip = false
  695.             local quoted = false
  696.             local is_first_nonwhitespace = true
  697.  
  698.             local i = 0
  699.             for c in cline:gmatch('.') do
  700.                 i = i + 1
  701.                 if isValidChar(c) or (quoted and c ~= '"') then
  702.                     if n_start == -1 then
  703.                         n_start = i
  704.                     end
  705.  
  706.                     n_end = i
  707.                     is_first_nonwhitespace = false
  708.                 elseif c:match('%s+') then
  709.  
  710.                 else
  711.                     if c == '#' then
  712.                         if not tmp_list then
  713.                             skip = true
  714.                         end
  715.                     elseif c == '"' then
  716.                         if not tmp_list then
  717.                             if quoted then
  718.                                 quoted = false
  719.                             end
  720.  
  721.                             if not quoted and n_start == -1 then
  722.                                 quoted = true
  723.                             end
  724.                         end
  725.                     elseif c == '{' then
  726.                         if not tmp_list then
  727.                             cur_name = cline:sub(n_start, n_end)
  728.                             local qual_name = getQualifiedName(cur_name, cur_cat)
  729.  
  730.                             local tmp_cat = self.categories[qual_name]
  731.                             if not tmp_cat then
  732.                                 cur_cat = _ccategory(cur_name, cur_cat)
  733.                                 self.categories[qual_name] = cur_cat
  734.                             else
  735.                                 cur_cat = cat
  736.                             end
  737.                             cur_name = nil
  738.                         end
  739.                     elseif c == '}' then
  740.                         if not tmp_list then
  741.                             if not cur_cat then
  742.                                 out:close()
  743.                                 _error(string.format('Corrupt Config! Attempt to close too many categories: %s:%s', self._p_file_name, tostring(line_num)), 2)
  744.                             end
  745.  
  746.                             cur_cat = cur_cat.parent
  747.                         end
  748.  
  749.                     elseif c == '=' then
  750.                         if not tmp_list then
  751.                             cur_name = cline:sub(n_start, n_end)
  752.  
  753.                             if not cur_cat then
  754.                                 out:close()
  755.                                 _error(string.format("'%s' has no scope in '%s:%s'", cur_name, self._p_file_name, tostring(line_num)), 2)
  756.                             end
  757.  
  758.                             local prop = _cvalue(cur_name, cur_type, cline:sub(i + 1))
  759.                             cur_cat:put(cur_name, prop)
  760.                             break
  761.                         end
  762.                     elseif c == ':' then
  763.                         if not tmp_list then
  764.                             cur_type = getTypeFor(cline:sub(n_start, n_end):sub(1, 1))
  765.                             n_start = -1
  766.                             n_end = -1
  767.                         end
  768.                     elseif c == '<' then
  769.                         if (tmp_list and i == #cline) or (not tmp_list and i ~= #cline) then
  770.                             out:close()
  771.                             _error(string.format('Malformed list "%s:%s"', self._p_file_name, tostring(line_num)), 2)
  772.                         elseif i == #cline then
  773.                             cur_name = cline:sub(n_start, n_end)
  774.  
  775.                             if not cur_cat then
  776.                                 out:close()
  777.                                 _error(string.format("'%s' has no scope in '%s:%s'", cur_name, self._p_file_name, tostring(line_num)), 2)
  778.                             end
  779.  
  780.                             tmp_list = {}
  781.                             skip = true
  782.                         end
  783.  
  784.                     elseif c == '>' then
  785.                         if not tmp_list then
  786.                             out:close()
  787.                             _error(string.format('Malformed list "%s:%s"', self._p_file_name, tostring(line_num)), 2)
  788.                         end
  789.  
  790.                         if is_first_nonwhitespace then
  791.                             cur_cat:put(cur_name, _cvalue(cur_name, cur_type, deep_copy(tmp_list)))
  792.                             cur_name = nil
  793.                             tmp_list = nil
  794.                             cur_type = nil
  795.                         end
  796.                     elseif c == '~' then
  797.                    
  798.                     else
  799.                         if not tmp_list then
  800.                             out:close()
  801.                             _error(string.format('Unknown character "%s" in %s:%s', c, self._p_file_name, tostring(line_num)), 2)
  802.                         end
  803.                     end
  804.  
  805.                     is_first_nonwhitespace = false
  806.                 end
  807.  
  808.                 if skip then break end
  809.             end
  810.  
  811.             if quoted then
  812.                 out:close()
  813.                 _error(string.format('Unmatched quote in %s:%s', self._p_file_name, tostring(line_num)), 2)
  814.             elseif tmp_list and not skip then
  815.                 table.insert(tmp_list, trim_str(cline))
  816.             end
  817.  
  818.         end
  819.     end
  820.  
  821.     self:resetChanged()
  822.     out:close()
  823. end
  824.  
  825. function Configuration:_save(output)
  826.     for k, v in pairs(self.categories) do
  827.         if not v:isChild() then
  828.             v:writeOut(output, 0)
  829.             v:write('\n')
  830.         end
  831.     end
  832. end
  833.  
  834. function Configuration:save()
  835.     local out = io.open(self.path, 'w')
  836.  
  837.     out:write('# '..self._header_desc..' Configuration File\n\n')
  838.     self:_save(out)
  839.  
  840.     out:close()
  841. end
  842.  
  843. local function contains_str(str, c)
  844.     for ch in str:gmatch('.') do
  845.         if ch == c then
  846.             return true
  847.         end
  848.     end
  849.  
  850.     return false
  851. end
  852.  
  853. function Configuration:getCategory(category)
  854.     local res = self.categories[category]
  855.  
  856.     if not res then
  857.         if contains_str(category, '.') then
  858.             local hierarchy = split_str(category, '\\.')
  859.             local parent = self.categories[hierarchy[1]]
  860.  
  861.             if not parent then
  862.                 parent = _ccategory(hierarchy[1])
  863.                 self.categories[parent:getQualifiedName()] = parent
  864.                 self.changed = true
  865.             end
  866.  
  867.             for i=2,#hierarchy do
  868.                 local n = getQualifiedName(hierarchy[i], parent)
  869.                 local nchild = self.categories[n]
  870.                
  871.                 if not nchild then
  872.                     nchild = _ccategory(hierarchy[i], parent)
  873.                     self.categories[n] = nchild
  874.                     self.changed = true
  875.                 end
  876.  
  877.                 res = nchild
  878.                 parent = nchild
  879.             end
  880.         else
  881.             res = _ccategory(category)
  882.             self.categories[category] = res
  883.             changed = true
  884.         end
  885.     end
  886.  
  887.     return res
  888. end
  889.  
  890. function Configuration:removeCategory(category_obj)
  891.     local _self = self
  892.     foreach(category_obj:getChildren(), function(c) _self:removeCategory(c) end)
  893.  
  894.     if containsKey(self.categories, category_obj:getQualifiedName()) then
  895.         self.categories[category_obj:getQualifiedName()] = nil
  896.  
  897.         if category_obj.parent then
  898.             category_obj.parent:removeChild(category_obj)
  899.         end
  900.  
  901.         changed = true
  902.     end
  903. end
  904.  
  905. function Configuration:setCategoryComment(category, comment)
  906.     self.categories[category]:setComment(comment)
  907. end
  908.  
  909. function Configuration:getCategoryNames()
  910.     local names = {}
  911.  
  912.     for k, _ in pairs(self.categories) do
  913.         table.insert(names, k)
  914.     end
  915.  
  916.     return names
  917. end
  918.  
  919. function Configuration:getString(name, category, defaultValue, comment, v_vals)
  920.     local prop = self:get(category, name, defaultValue)
  921.  
  922.     if not isTable(v_vals) then
  923.         v_vals = not not v_vals and {v_vals} or nil
  924.     end
  925.  
  926.     comment = comment or ''
  927.  
  928.     prop:setValids(v_vals)
  929.     prop:setComment(comment..' [default: '..defaultValue..']')
  930.  
  931.     local _p_valids = prop:getValidValues()
  932.  
  933.     if isTable(_p_valids) then
  934.         _p_valids = '< '..table.concat(_p_valids, ', ')..' >'
  935.     else
  936.         _p_valids = '< '..tostring(_p_valids)..' >'
  937.     end
  938.  
  939.  
  940.     _debug({msgf=self:_logid()..' GET Valid Values: < %s >',fvars={(_p_valids == '<  >' or _p_valids == '< nil >') and 'any' or _p_valids}})
  941.  
  942.     return prop:getString()
  943. end
  944.  
  945. function Configuration:getNumber(name, category, defaultValue, comment, min, max)
  946.     local prop = self:get(category, name, defaultValue)
  947.    
  948.     if not isTable(v_vals) then
  949.         v_vals = not not v_vals and {v_vals} or nil
  950.     end
  951.  
  952.     comment = comment or ''
  953.  
  954.     prop:setComment(comment..' [default: '..tostring(defaultValue)..']')
  955.  
  956.     if max then prop:setMax(max) end
  957.     if min then prop:setMin(min) end
  958.  
  959.     _debug({msgf=self:_logid()..' GET [Cat: %s, Key: %s, val: %d, range: [%f, %f]]', fvars={category, name, prop:getNumber(), prop:getMin(), prop:getMax()}})
  960.  
  961.     return prop:getNumber()
  962. end
  963.  
  964. function Configuration:getBoolean(name, category, defaultValue, comment)
  965.     local prop = self:get(category, name, defaultValue)
  966.    
  967.     if not isTable(v_vals) then
  968.         v_vals = not not v_vals and {v_vals} or nil
  969.     end
  970.  
  971.     comment = comment or ''
  972.  
  973.     prop:setComment(comment..' [default: '..tostring(defaultValue)..']')
  974.  
  975.     _debug({msgf=self:_logid()..' GET [Cat: %s, Key: %s, val: %s]', fvars={category, name, prop:getString()}})
  976.  
  977.     return prop:getBoolean()
  978. end
  979.  
  980. function Configuration:getList(name, category, defaultValue, comment, v_vals)
  981.     local prop = self:get(category, name, defaultValue)
  982.    
  983.     if not isTable(v_vals) then
  984.         v_vals = not not v_vals and {v_vals} or nil
  985.     end
  986.  
  987.     comment = comment or ''
  988.  
  989.     local default_str = '[ '
  990.  
  991.     for i, v in ipairs(defaultValue) do
  992.         if i ~= 1 then
  993.             default_str = default_str..', '
  994.         end
  995.  
  996.         default_str = default_str..tostring(v)
  997.     end
  998.     default_str = default_str..' ]'
  999.  
  1000.     prop:setValids(v_vals)
  1001.     prop:setComment(comment..' [default: '..default_str..']')
  1002.  
  1003.     local _p_valids = prop:getValidValues()
  1004.  
  1005.     if isTable(_p_valids) then
  1006.         _p_valids = '< '..table_to_string(_p_valids, ', ')..' >'
  1007.     else
  1008.         _p_valids = '< '..tostring(_p_valids)..' >'
  1009.     end
  1010.  
  1011.     _debug({msgf=self:_logid()..' GET Valid Values: %s',fvars={(_p_valids == '<  >' or _p_valids == '< nil >') and 'any' or _p_valids}})
  1012.  
  1013.     return prop:getList()
  1014. end
  1015.  
  1016. function Configuration:getPath()
  1017.     return self.path
  1018. end
  1019.  
  1020. -- Config Category Declarations
  1021.  
  1022. function _ccategory(name, parent)
  1023.     assert(name, 'Config Category MUST have a name!')
  1024.  
  1025.     local cc = {
  1026.         id = name,
  1027.         parent = parent,
  1028.         comment = nil,
  1029.         children = {},
  1030.         settings = {},
  1031.         s_count = 0,
  1032.         changed = false
  1033.     }
  1034.  
  1035.     if parent then
  1036.         table.insert(parent.children, cc)
  1037.     end
  1038.  
  1039.     setmetatable(cc, cc_meta)
  1040.     return cc
  1041. end
  1042.  
  1043. local function getRoot(cur)
  1044.     return cur.parent and getRoot(cur.parent) or cur
  1045. end
  1046.  
  1047. local function indent(i)
  1048.     local res = ''
  1049.     for s=1,i do
  1050.         res = res..'   '
  1051.     end
  1052.  
  1053.     return res
  1054. end
  1055.  
  1056. function ConfigCategory:setComment(comment)
  1057.     assert(isString(comment), 'Comment must be a string!', 2)
  1058.  
  1059.     self.comment = comment
  1060.     return self
  1061. end
  1062.  
  1063. function ConfigCategory:getName()
  1064.     return self.id
  1065. end
  1066.  
  1067. function ConfigCategory:getComment()
  1068.     return self.comment
  1069. end
  1070.  
  1071. function ConfigCategory:getQualifiedName()
  1072.     return getQualifiedName(self.id, self.parent)
  1073. end
  1074.  
  1075. function ConfigCategory:getRoot()
  1076.     return getRoot(self)
  1077. end
  1078.  
  1079. function ConfigCategory:isChild()
  1080.     return self.parent ~= nil
  1081. end
  1082.  
  1083. function ConfigCategory:getValues()
  1084.     return shallow_copy(self.settings)
  1085. end
  1086.  
  1087. function ConfigCategory:get(key)
  1088.     return self.settings[key]
  1089. end
  1090.  
  1091. function ConfigCategory:containsKey(key)
  1092.     return containsKey(self.settings, key)
  1093. end
  1094.  
  1095. function ConfigCategory:write(file, newLine, ...)
  1096.     assert(file, 'File must not be null!', 2)
  1097.  
  1098.     local data = {...}
  1099.     for _, v in pairs(data) do
  1100.         file:write(tostring(v))
  1101.     end
  1102.  
  1103.     if newLine then
  1104.         file:write('\n')
  1105.     end
  1106. end
  1107.  
  1108. function ConfigCategory:writeOut(file, indentation)
  1109.     local p0 = indent(indentation)
  1110.     local p1 = indent(indentation + 1)
  1111.     local p2 = indent(indentation + 2)
  1112.  
  1113.     if self.comment and #self.comment ~= 0 then
  1114.         self:write(file, true, p0, CONFIG_SEPARATOR)
  1115.         self:write(file, true, p0, '# ', self:getName())
  1116.         self:write(file, true, p0, '#------------------------------------------------------------#')
  1117.  
  1118.         foreach(split_str(self.comment, '\r?\n'), function(str)
  1119.             self:write(file, true, p0, '# ', str)
  1120.         end)
  1121.  
  1122.         self:write(file, true, p0, CONFIG_SEPARATOR, '\n')
  1123.     end
  1124.  
  1125.     local name = self.id
  1126.     if not isValid(name) then
  1127.         name = '"'..name..'"'
  1128.     end
  1129.  
  1130.     self:write(file, true, p0, name, ' {')
  1131.     for _, v in pairs(self.settings) do
  1132.         local val = v
  1133.  
  1134.         if val.comment then
  1135.             if i ~= 1 then
  1136.                 self:write(file, true)
  1137.             end
  1138.  
  1139.             for i, line in ipairs(split_str(val.comment, '\r?\n')) do
  1140.                 self:write(file, true, p1, '# ', line)
  1141.             end
  1142.         end
  1143.  
  1144.         local val_name = val:getName()
  1145.         if not isValid(val_name) then
  1146.             val_name = '"'..val_name..'"'
  1147.         end
  1148.  
  1149.         if val:isListValue() then
  1150.             self:write(file, true, p1, val:getType().id(), ':', val_name, ' <')
  1151.  
  1152.             for _, v in pairs(val:getList()) do
  1153.                 self:write(file, true, p2, v)
  1154.             end
  1155.  
  1156.             self:write(file, true, p1, ' >')
  1157.         elseif not val:getType() then
  1158.             self:write(file, true, p1, val_name, '=', val:getString())
  1159.         else
  1160.             self:write(file, true, p1, val:getType().id(), ':', val_name, '=', val:getString())
  1161.         end
  1162.     end
  1163.  
  1164.     if #self.children > 0 then
  1165.         self:write(file, true)
  1166.     end
  1167.  
  1168.     for _, child in pairs(self.children) do child:writeOut(file, indentation + 1) end
  1169.     self:write(file, true, p0, '}', '\n')
  1170. end
  1171.  
  1172. function ConfigCategory:hasChanged()
  1173.     if self.changed then return true end
  1174.  
  1175.     for _, v in pairs(self.settings) do
  1176.         if v:hasChanged() then
  1177.             return true
  1178.         end
  1179.     end
  1180.  
  1181.     return false
  1182. end
  1183.  
  1184. function ConfigCategory:_reset_changed()
  1185.     self.changed = false
  1186.  
  1187.     for _, v in pairs(self.settings) do
  1188.         v:resetChanged()
  1189.     end
  1190. end
  1191.  
  1192. function ConfigCategory:put(key, setting)
  1193.     assert(isString(key), 'Key must be a string!', 2)
  1194.  
  1195.     self.changed = true
  1196.     self.settings[key] = setting
  1197. end
  1198.  
  1199. function ConfigCategory:remove(key)
  1200.     self.changed = true
  1201.  
  1202.     local idx = 1
  1203.     local val
  1204.     for k, v in pairs(self.settings) do
  1205.         if k == key then
  1206.             val = self.settings[k]
  1207.             self.settings[k] = nil
  1208.             break
  1209.         end
  1210.  
  1211.         idx = idx + 1
  1212.     end
  1213.  
  1214.     return val
  1215. end
  1216.  
  1217. function ConfigCategory:clear()
  1218.     self.changed = true
  1219.  
  1220.     while len(self.settings) > 0 do
  1221.         table.remove(self.settings)
  1222.     end
  1223. end
  1224.  
  1225. function ConfigCategory:contains(setting)
  1226.     assert(setting, 'Setting is Nil', 2)
  1227.    
  1228.     for _,v in pairs(self.settings) do
  1229.         if v == setting then
  1230.             return true
  1231.         end
  1232.     end
  1233.  
  1234.     return false
  1235. end
  1236.  
  1237. function ConfigCategory:keys()
  1238.     local _keys = {}
  1239.  
  1240.     for k, _ in pairs(self.settings) do
  1241.         table.insert(_keys, k)
  1242.     end
  1243.  
  1244.     return _keys
  1245. end
  1246.  
  1247. function ConfigCategory:settings()
  1248.     local _entries = {}
  1249.  
  1250.     for k, v in pairs(self.settings) do
  1251.         table.insert(_entires, {k, v})
  1252.     end
  1253.  
  1254.     return _entries
  1255. end
  1256.  
  1257. function ConfigCategory:getChildren()
  1258.     return deep_copy(self.children)
  1259. end
  1260.  
  1261. function ConfigCategory:removeChild(child)
  1262.     local idx = iforeach(self.children, function(v) return v == child end)
  1263.  
  1264.     if idx > 0 then
  1265.         table.remove(self.children, idx)
  1266.         self.changed = true
  1267.     end
  1268. end
  1269.  
  1270. -- Config Value Declarations
  1271.  
  1272. function _cvalue(name, valueType, values, validValues)
  1273.     assert(name and valueType and values ~= nil, 'A ConfigValue must start with AT LEAST a name, type and value!', 2)
  1274.  
  1275.     if isString(valueType) or isNumber(valueType) then
  1276.         valueType = getValueType(valueType)
  1277.     end
  1278.  
  1279.     assert(isTable(valueType), 'ValueType must be a valid ConfigValue.Type table or a typename... (e.g. \'string\', \'number\' or \'boolean\'', 2)
  1280.  
  1281.     validValues = validValues or {}
  1282.  
  1283.     if not isTable(validValues) then
  1284.         validValues = {validValues}
  1285.     end
  1286.  
  1287.     local cv = {
  1288.         id = name,
  1289.         comment = nil,
  1290.         value = values,
  1291.         default = deep_copy(values),
  1292.         value_type = valueType,
  1293.         is_list = isTable(values),
  1294.         valid_values = shallow_copy(validValues),
  1295.         min_val = _MIN_NUMBER,
  1296.         max_val = _MAX_NUMBER,
  1297.         changed = false
  1298.     }
  1299.  
  1300.     setmetatable(cv, cv_meta)
  1301.     return cv
  1302. end
  1303.  
  1304. function ConfigValue:setName(new_name)
  1305.     self.id = new_name
  1306.  
  1307.     return self
  1308. end
  1309.  
  1310. function ConfigValue:setComment(comment)
  1311.     self.comment = comment
  1312.  
  1313.     return self
  1314. end
  1315.  
  1316. function ConfigValue:setValue(value)
  1317.     if typeEquals(self.value, value) then
  1318.         if self.is_list then
  1319.             self.value = deep_copy(value)
  1320.         else
  1321.             self.value = value
  1322.         end
  1323.     else
  1324.         self.value = self.value_type.cast(value)
  1325.     end
  1326.  
  1327.     self.changed = true
  1328.  
  1329.     return self
  1330. end
  1331.  
  1332. function ConfigValue:set(value)
  1333.     self:setValue(value)
  1334. end
  1335.  
  1336. function ConfigValue:setDefault(new_default)
  1337.     assert(new_default ~= nil, 'new default value must be non-nil', 2)
  1338.     assert(self:isListValue() or isType(self.value_type.typename(), new_default), 'new default vlaue must be of the appropriate type: '..self.value_type.typename(), 2)
  1339.  
  1340.     if isTable(new_default) then
  1341.         self.default = deep_copy(new_default)
  1342.     else
  1343.         self.default = new_default
  1344.     end
  1345.  
  1346.     return self
  1347. end
  1348.  
  1349. function ConfigValue:setToDefault()
  1350.     if self.is_list then
  1351.         self.value = shallow_copy(self.default)
  1352.     else
  1353.         self.value = self.default
  1354.     end
  1355.  
  1356.     return self
  1357. end
  1358.  
  1359. function ConfigValue:isDefault()
  1360.     if self.is_list then
  1361.         for key, val in pair(self.default) do
  1362.             if val ~= self.value[key] then
  1363.                 return false
  1364.             end
  1365.         end
  1366.  
  1367.         return true
  1368.     else
  1369.         return self.default == self.value
  1370.     end
  1371. end
  1372.  
  1373. function ConfigValue:hasChanged()
  1374.     return self.changed
  1375. end
  1376.  
  1377. function ConfigValue:setMin(minimum)
  1378.     assert(isNumber(minimum), 'min and max MUST be numbers!', 2)
  1379.  
  1380.     self.min_val = minimum
  1381.     return self
  1382. end
  1383.  
  1384. function ConfigValue:setMax(maximum)
  1385.     assert(isNumber(minimum), 'min and max MUST be numbers!', 2)
  1386.  
  1387.     self.max_val = maximum
  1388.     return self
  1389. end
  1390.  
  1391. function ConfigValue:setValids(valids)
  1392.     if not valids or (isTable(valids) and #valids == 0) then return end
  1393.  
  1394.     self.valid_values = isTable(valids) and valids or {valids}
  1395.  
  1396.     return self
  1397. end
  1398.  
  1399. function ConfigValue:getName()
  1400.     return self.id
  1401. end
  1402.  
  1403. function ConfigValue:getMin()
  1404.     return self.min_val
  1405. end
  1406.  
  1407. function ConfigValue:getMax()
  1408.     return self.max_val
  1409. end
  1410.  
  1411. function ConfigValue:getValidValues()
  1412.     return deep_copy(self.valid_values)
  1413. end
  1414.  
  1415. function ConfigValue:isListValue()
  1416.     return self.is_list
  1417. end
  1418.  
  1419. function ConfigValue:getRaw()
  1420.     if self.valid_values and len(self.valid_values) > 0 then
  1421.         local success = false
  1422.         for i, v in ipairs(self.valid_values) do
  1423.             if v == self.value then
  1424.                 success = true
  1425.                 break
  1426.             end
  1427.         end
  1428.  
  1429.         if not success then
  1430.             _error(string.format('"%s" is not a valid value for option \'%s\':\n\nValid Values:\n[ %s ]', self.value, self.id, table.concat(self.valid_values, ',\n  ')), 2)
  1431.         end
  1432.     end
  1433.  
  1434.     return self.value
  1435. end
  1436.  
  1437. function ConfigValue:getString()
  1438.     local err, val
  1439.  
  1440.     if self.is_list then
  1441.         err, val = pcall(table_to_string, self:getRaw(), ', ')
  1442.     else
  1443.         err, val = pcall(tostring, self:getRaw())
  1444.     end
  1445.  
  1446.     assert(err, val, 2)
  1447.  
  1448.     return self.is_list and '< '..val..' >' or val
  1449. end
  1450.  
  1451. function ConfigValue:inRange(num)
  1452.     return num >= self:getMin() and num <= self:getMax()
  1453. end
  1454.  
  1455. function ConfigValue:getNumber()
  1456.     local err, val = pcall(tonumber, self:getRaw())
  1457.     assert(err and val ~= nil, 'Cannot get \''..self:getRaw()..'\' as', 2)
  1458.  
  1459.     if not self:inRange(val) then
  1460.         _error(string.format('number value \'%f\' is not within range for \'%s\'\n\nValue Range: [%f, %f]',
  1461.             val,
  1462.             self:getName(),
  1463.             self:getMin(),
  1464.             self:getMax()), 2)
  1465.     end
  1466.  
  1467.     return val
  1468. end
  1469.  
  1470. function ConfigValue:getBoolean()
  1471.     local err, val = pcall(toboolean, self:getRaw())
  1472.     assert(err, val, 2)
  1473.  
  1474.     return val
  1475. end
  1476.  
  1477. function ConfigValue:getList()
  1478.     assert(self.is_list, 'value is not a list!', 2)
  1479.  
  1480.     if self.valid_values and len(self.valid_values) > 0 then
  1481.         for _, v in ipairs(self.value) do
  1482.             local success = false
  1483.             for _2, v2 in ipairs(self.valid_values) do
  1484.                 if tostring(v) == tostring(v2) then
  1485.                     success = true
  1486.                     break
  1487.                 end
  1488.             end
  1489.  
  1490.             if not success then
  1491.                 _error(string.format('"%s" is not a valid value in list \'%s\':\n\n Valid Values: [ %s ]', v, self.id, table_to_string(self.valid_values, ', ')), 2)
  1492.             end
  1493.         end
  1494.     end
  1495.  
  1496.     return self.value
  1497. end
  1498.  
  1499. function ConfigValue:getType()
  1500.     return self.value_type
  1501. end
  1502.  
  1503. function ConfigValue:getTypename()
  1504.     return self.value_type.typename()
  1505. end
  1506.  
  1507. function ConfigValue:resetChanged()
  1508.     self.changed = false
  1509. end
  1510.  
  1511.  
  1512. -- Dirty Global Stuff and Function Overriding
  1513.  
  1514.  
  1515. local _log_tag_func = 'cc_config_log_tag'
  1516. local _log_debug_func = 'cc_API_DEBUG_FLAG'
  1517. local _log_error_func = 'cc_config_error'
  1518. local _log_debug_enable_func = 'cc_config_init_debug'
  1519. local _log_levels_tbl = 'LogLevels'
  1520.  
  1521. _G[_log_levels_tbl] = LogLevels
  1522.  
  1523. _G[_log_debug_enable_func] = _INIT_DEBUG
  1524.  
  1525. _G[_log_tag_func] = function(new_caller)
  1526.     assert(new_caller, 'Please provide a log tag to be used! Nil was provided...')
  1527.     assert(isString(new_caller), 'Please provide a log tag to be used! Must be string, '..type(new_caller)..' was provided...')
  1528.  
  1529.     _default_caller = new_caller
  1530. end
  1531.  
  1532. _G[_log_debug_func] = function(level, message, ...)
  1533.     if #{...} ~= 0 then
  1534.         _debug({ msgf=message, fvars={...}, caller=_default_caller, level=level})
  1535.     else
  1536.         _debug({ msg=message, caller=_default_caller, level=level})
  1537.     end
  1538. end
  1539.  
  1540. _G[_log_error_func] = function(message, ...)
  1541.     if #{...} ~= 0 then
  1542.         message = string.format(message, ...)
  1543.     end
  1544.  
  1545.     _error(message, 2, _default_caller)
  1546. end
  1547.  
  1548. -- Unloading override to unload our globals. Restore on unload.
  1549. if not _G[GLOBALS_LOADED_NAME] then
  1550.  
  1551.     local nativeUnloadAPI = os.unloadAPI
  1552.     local nativeConcat = table.concat
  1553.     os.unloadAPI = function(api)
  1554.         if not _G[GLOBALS_LOADED_NAME] then nativeUnloadAPI(api) end
  1555.         if not _G[api] then return end
  1556.  
  1557.         local obj = _G[api]
  1558.         if obj['_API_NAME'] and obj._API_NAME == _API_NAME then
  1559.             _debug({msg='Unloading Global APIs...'})
  1560.  
  1561.             _G[_log_levels_tbl] = nil
  1562.             _G[_log_debug_enable_func] = nil
  1563.             _G[_log_tag_func] = nil
  1564.             _G[_log_debug_func] = nil
  1565.             _G[_log_error_func] = nil
  1566.  
  1567.             _debug({msg='Globals Unloaded! Restoring original os.unloadAPI...'})
  1568.             os.unloadAPI = nativeUnloadAPI
  1569.             table.concat = nativeConcat
  1570.             _G[GLOBALS_LOADED_NAME] = false
  1571.         end
  1572.  
  1573.         nativeUnloadAPI(api)
  1574.     end
  1575.  
  1576.     table.concat = function(tbl, sep)
  1577.         local success, val = pcall(table_to_string, tbl, sep, false)
  1578.  
  1579.         if not success then
  1580.             _debug({msgf='Failed to concatenate table using table_to_string! [tostring(tbl): %s, sep: %s]', fvars={tostring(tbl), tostring(sep)}, level=LogLevels.WARN})
  1581.             val = table.concat(tbl, sep)
  1582.         end
  1583.  
  1584.         return val
  1585.     end
  1586.  
  1587.     _G[GLOBALS_LOADED_NAME] = true
  1588.  
  1589. end
Advertisement
Add Comment
Please, Sign In to add comment