Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -----------------------------------------------------------------------------
- -- JSON4Lua: JSON encoding / decoding / traversing support for the Lua language.
- -- json Module.
- -- Authors: Craig Mason-Jones, Egor Skriptunoff
- -- Version: 1.2.1
- -- 2017-05-10
- -- This module is released under the MIT License (MIT).
- -- Please see LICENCE.txt for details:
- -- https://github.com/craigmj/json4lua/blob/master/doc/LICENCE.txt
- --
- -- USAGE:
- -- This module exposes three functions:
- -- json.encode(obj)
- -- Accepts Lua value (table/string/boolean/number/nil/json.null/json.empty) and returns JSON string.
- -- json.decode(s)
- -- Accepts JSON (as string or as loader function) and returns Lua object.
- -- json.traverse(s, callback)
- -- Accepts JSON (as string or as loader function) and user-supplied callback function, returns nothing
- -- Traverses the JSON, sends each item to callback function, no memory-consuming Lua objects are being created.
- --
- -- REQUIREMENTS:
- -- Lua 5.1, Lua 5.2, Lua 5.3 or LuaJIT
- --
- -- CHANGELOG
- -- 1.2.1 Now you can partially decode JSON while traversing it (callback function should return true).
- -- 1.2.0 Some improvements made to be able to use this module on RAM restricted devices:
- -- To read large JSONs, you can now provide "loader function" instead of preloading whole JSON as Lua string.
- -- Added json.traverse() to traverse JSON using callback function (without creating arrays/objects in Lua).
- -- Now, instead of decoding whole JSON, you can decode its arbitrary element (e.g, array or object)
- -- by specifying the position where this element starts.
- -- In order to do that, at first you have to traverse JSON to get all positions you need.
- -- Most of the code rewritten to improve performance.
- -- Decoder now understands extended syntax beyond strict JSON standard:
- -- In arrays:
- -- trailing comma is ignored: [1,2,3,] -> [1,2,3]
- -- missing values are nulls: [,,42,,] -> [null,null,42,null]
- -- In objects:
- -- missing values are ignored: {,,"a":42,,} -> {"a":42}
- -- unquoted identifiers are valid keys: {a$b_5:42} -> {"a$b_5":42}
- -- Encoder now accepts both 0-based and 1-based Lua arrays (but decoder always converts JSON arrays to 1-based Lua arrays).
- -- Some minor bugs fixed.
- -- 1.1.0 Modifications made by Egor Skriptunoff, based on version 1.0.0 taken from
- -- https://github.com/craigmj/json4lua/blob/40fb13b0ec4a70e36f88812848511c5867bed857/json/json.lua.
- -- Added Lua 5.2 and Lua 5.3 compatibility.
- -- Removed Lua 5.0 compatibility.
- -- Introduced json.empty (Lua counterpart for empty JSON object)
- -- Bugs fixed:
- -- Attempt to encode Lua table {[10^9]=0} raises an out-of-memory error.
- -- Zero bytes '\0' in Lua strings are not escaped by encoder.
- -- JSON numbers with capital "E" (as in 1E+100) are not accepted by decoder.
- -- All nulls in a JSON arrays are skipped by decoder, sparse arrays could not be loaded correctly.
- -- UTF-16 surrogate pairs in JSON strings are not recognised by decoder.
- -- 1.0.0 Merged Amr Hassan's changes
- -- 0.9.30 Changed to MIT Licence.
- -- 0.9.20 Introduction of local Lua functions for private functions (removed _ function prefix).
- -- Fixed Lua 5.1 compatibility issues.
- -- Introduced json.null to have null values in associative arrays.
- -- json.encode() performance improvement (more than 50%) through table_concat rather than ..
- -- Introduced decode ability to ignore /**/ comments in the JSON string.
- -- 0.9.10 Fix to array encoding / decoding to correctly manage nil/null values in arrays.
- -- 0.9.00 First release
- --
- -----------------------------------------------------------------------------
- -----------------------------------------------------------------------------
- -- Module declaration
- -----------------------------------------------------------------------------
- local json = {}
- do
- -----------------------------------------------------------------------------
- -- Imports and dependencies
- -----------------------------------------------------------------------------
- local math, string, table = require'math', require'string', require'table'
- local math_floor, math_max, math_type = math.floor, math.max, math.type or function() end
- local string_char, string_sub, string_find, string_match, string_gsub, string_format
- = string.char, string.sub, string.find, string.match, string.gsub, string.format
- local table_insert, table_remove, table_concat = table.insert, table.remove, table.concat
- local type, tostring, pairs, assert, error = type, tostring, pairs, assert, error
- local loadstring = loadstring or load
- -----------------------------------------------------------------------------
- -- Public functions
- -----------------------------------------------------------------------------
- -- function json.encode(obj) encodes Lua value to JSON, returns JSON as string.
- -- function json.decode(s, pos) decodes JSON, returns the decoded result as Lua value (may be very memory-consuming).
- -- Both functions json.encode() and json.decode() work with "special" Lua values json.null and json.empty
- -- special Lua value json.null = JSON value null
- -- special Lua value json.empty = JSON value {} (empty JSON object)
- -- regular Lua empty table = JSON value [] (empty JSON array)
- -- Empty JSON objects and JSON nulls require special handling upon sending (encoding).
- -- Please make sure that you send empty JSON objects as json.empty (instead of empty Lua table).
- -- Empty Lua tables will be encoded as empty JSON arrays, not as empty JSON objects!
- -- json.encode( {empt_obj = json.empty, empt_arr = {}} ) --> {"empt_obj":{},"empt_arr":[]}
- -- Also make sure you send JSON nulls as json.null (instead of nil).
- -- json.encode( {correct = json.null, incorrect = nil} ) --> {"correct":null}
- -- Empty JSON objects and JSON nulls require special handling upon receiving (decoding).
- -- After receiving the result of decoding, every Lua table returned (including nested tables) should firstly
- -- be compared with special Lua values json.empty/json.null prior to making operations on these values.
- -- If you don't need to distinguish between empty JSON objects and empty JSON arrays,
- -- json.empty may be replaced with newly created regular empty Lua table.
- -- v = (v == json.empty) and {} or v
- -- If you don't need special handling of JSON nulls, you may replace json.null with nil to make them disappear.
- -- if v == json.null then v = nil end
- -- Function json.traverse(s, callback, pos) traverses JSON using user-supplied callback function, returns nothing.
- -- Traverse is useful to reduce memory usage: no memory-consuming objects are being created in Lua while traversing.
- -- Each item found inside JSON will be sent to callback function passing the following arguments:
- -- (path, json_type, value, pos, pos_last)
- -- path is array of nested JSON identifiers/indices, "path" is empty for root JSON element
- -- json_type is one of "null"/"boolean"/"number"/"string"/"array"/"object"
- -- value is defined when json_type is "null"/"boolean"/"number"/"string", value == nil for "object"/"array"
- -- pos is 1-based index of first character of current JSON element
- -- pos_last is 1-based index of last character of current JSON element (defined only when "value" ~= nil)
- -- "path" table reference is the same on each callback invocation, but its content differs every time.
- -- Do not modify "path" array inside your callback function, use it as read-only.
- -- Do not save reference to "path" for future use (create shallow table copy instead).
- -- callback function should return a value, when it is invoked with argument "value" == nil
- -- a truthy value means user wants to decode this JSON object/array and create its Lua counterpart (this may be memory-consuming)
- -- a falsy value (or no value returned) means user wants to traverse through this JSON object/array
- -- (returned value is ignored when callback function is invoked with value ~= nil)
- -- Traverse examples:
- -- json.traverse([[ 42 ]], callback)
- -- will invoke callback 1 time:
- -- path json_type value pos pos_last
- -- ---------- --------- -------------- --- --------
- -- callback( {}, "number", 42, 2, 3 )
- --
- -- json.traverse([[ {"a":true, "b":null, "c":["one","two"], "d":{ "e":{}, "f":[] } } ]], callback)
- -- will invoke callback 9 times:
- -- path json_type value pos pos_last
- -- ---------- --------- -------------- --- --------
- -- callback( {}, "object", nil, 2, nil )
- -- callback( {"a"}, "boolean", true, 7, 10 )
- -- callback( {"b"}, "null", json.null, 17, 20 ) -- special Lua value for JSON null
- -- callback( {"c"}, "array", nil, 27, nil )
- -- callback( {"c", 1}, "string", "one", 28, 32 )
- -- callback( {"c", 2}, "string", "two", 34, 38 )
- -- callback( {"d"}, "object", nil, 46, nil )
- -- callback( {"d", "e"}, "object", nil, 52, nil )
- -- callback( {"d", "f"}, "array", nil, 60, nil )
- --
- -- json.traverse([[ {"a":true, "b":null, "c":["one","two"], "d":{ "e":{}, "f":[] } } ]], callback)
- -- will invoke callback 9 times if callback returns true when invoked for array "c" and object "e":
- -- path json_type value pos pos_last
- -- ---------- --------- -------------- --- --------
- -- callback( {}, "object", nil, 2, nil )
- -- callback( {"a"}, "boolean", true, 7, 10 )
- -- callback( {"b"}, "null", json.null, 17, 20 )
- -- callback( {"c"}, "array", nil, 27, nil ) -- this callback returned true (user wants to decode this array)
- -- callback( {"c"}, "array", {"one", "two"}, 27, 39 ) -- the next invocation brings the result of decoding
- -- callback( {"d"}, "object", nil, 46, nil )
- -- callback( {"d", "e"}, "object", nil, 52, nil ) -- this callback returned true (user wants to decode this object)
- -- callback( {"d", "e"}, "object", json.empty, 52, 53 ) -- the next invocation brings the result of decoding (special Lua value for empty JSON object)
- -- callback( {"d", "f"}, "array", nil, 60, nil )
- -- Both decoder functions json.decode(s) and json.traverse(s, callback) can accept JSON (argument s)
- -- as a "loader function" instead of a string.
- -- This function will be called repeatedly to return next parts (substrings) of JSON.
- -- An empty string, nil, or no value returned from "loader function" means the end of JSON.
- -- This may be useful for low-memory devices or for traversing huge JSON files.
- --- The json.null table allows one to specify a null value in an associative array (which is otherwise
- -- discarded if you set the value with 'nil' in Lua. Simply set t = { first=json.null }
- local null = {"This Lua table is used to designate JSON null value, compare your values with json.null to determine JSON nulls"}
- json.null = setmetatable(null, {
- __tostring = function() return 'null' end
- })
- --- The json.empty table allows one to specify an empty JSON object.
- -- To encode empty JSON array use usual empty Lua table.
- -- Example: t = { empty_object=json.empty, empty_array={} }
- local empty = {}
- json.empty = setmetatable(empty, {
- __tostring = function() return '{}' end,
- __newindex = function() error("json.empty is an read-only Lua table", 2) end
- })
- -----------------------------------------------------------------------------
- -- Private functions
- -----------------------------------------------------------------------------
- local decode
- local decode_scanArray
- local decode_scanConstant
- local decode_scanNumber
- local decode_scanObject
- local decode_scanString
- local decode_scanIdentifier
- local decode_scanWhitespace
- local encodeString
- local isArray
- local isEncodable
- local isConvertibleToString
- local isRegularNumber
- -----------------------------------------------------------------------------
- -- PUBLIC FUNCTIONS
- -----------------------------------------------------------------------------
- --- Encodes an arbitrary Lua object / variable.
- -- @param obj Lua value (table/string/boolean/number/nil/json.null/json.empty) to be JSON-encoded.
- -- @return string String containing the JSON encoding.
- function json.encode(obj)
- -- Handle nil and null values
- if obj == nil or obj == null then
- return 'null'
- end
- -- Handle empty JSON object
- if obj == empty then
- return '{}'
- end
- local obj_type = type(obj)
- -- Handle strings
- if obj_type == 'string' then
- return '"'..encodeString(obj)..'"'
- end
- -- Handle booleans
- if obj_type == 'boolean' then
- return tostring(obj)
- end
- -- Handle numbers
- if obj_type == 'number' then
- assert(isRegularNumber(obj), 'numeric values Inf and NaN are unsupported')
- return math_type(obj) == 'integer' and tostring(obj) or string_format('%.17g', obj)
- end
- -- Handle tables
- if obj_type == 'table' then
- local rval = {}
- -- Consider arrays separately
- local bArray, maxCount = isArray(obj)
- if bArray then
- for i = obj[0] ~= nil and 0 or 1, maxCount do
- table_insert(rval, json.encode(obj[i]))
- end
- else -- An object, not an array
- for i, j in pairs(obj) do
- if isConvertibleToString(i) and isEncodable(j) then
- table_insert(rval, '"'..encodeString(i)..'":'..json.encode(j))
- end
- end
- end
- if bArray then
- return '['..table_concat(rval, ',')..']'
- else
- return '{'..table_concat(rval, ',')..'}'
- end
- end
- error('Unable to JSON-encode Lua value of unsupported type "'..obj_type..'": '..tostring(obj))
- end
- local function create_state(s)
- -- Argument s may be "whole JSON string" or "JSON loader function"
- -- Returns "state" object which holds current state of reading long JSON:
- -- part = current part (substring of long JSON string)
- -- disp = number of bytes before current part inside long JSON
- -- more = function to load next substring (more == nil if all substrings are already read)
- local state = {disp = 0}
- if type(s) == "string" then
- -- s is whole JSON string
- state.part = s
- else
- -- s is loader function
- state.part = ""
- state.more = s
- end
- return state
- end
- --- Decodes a JSON string and returns the decoded value as a Lua data structure / value.
- -- @param s The string to scan (or "loader function" for getting next substring).
- -- @param pos (optional) The position inside s to start scan, default = 1.
- -- @return Lua object The object that was scanned, as a Lua table / string / number / boolean / json.null / json.empty.
- function json.decode(s, pos)
- return (decode(create_state(s), pos or 1))
- end
- --- Traverses a JSON string, sends everything to user-supplied callback function, returns nothing
- -- @param s The string to scan (or "loader function" for getting next substring).
- -- @param callback The user-supplied callback function which accepts arguments (path, json_type, value, pos, pos_last).
- -- @param pos (optional) The position inside s to start scan, default = 1.
- function json.traverse(s, callback, pos)
- decode(create_state(s), pos or 1, {path = {}, callback = callback})
- end
- local function read_ahead(state, startPos)
- -- Make sure there are at least 32 bytes read ahead
- local endPos = startPos + 31
- local part = state.part -- current part (substring of "whole JSON" string)
- local disp = state.disp -- number of bytes before current part inside "whole JSON" string
- local more = state.more -- function to load next substring
- assert(startPos > disp)
- while more and disp + #part < endPos do
- -- (disp + 1) ... (disp + #part) - we already have this segment now
- -- startPos ... endPos - we need to have this segment
- local next_substr = more()
- if not next_substr or next_substr == "" then
- more = nil
- else
- disp, part = disp + #part, string_sub(part, startPos - disp)
- disp, part = disp - #part, part..next_substr
- end
- end
- state.disp, state.part, state.more = disp, part, more
- end
- local function get_word(state, startPos, length)
- -- 1 <= length <= 32
- if state.more then read_ahead(state, startPos) end
- local idx = startPos - state.disp
- return string_sub(state.part, idx, idx + length - 1)
- end
- local function skip_until_word(state, startPos, word)
- -- #word < 30
- -- returns position after that word (nil if not found)
- repeat
- if state.more then read_ahead(state, startPos) end
- local part, disp = state.part, state.disp
- local b, e = string_find(part, word, startPos - disp, true)
- if b then
- return disp + e + 1
- end
- startPos = disp + #part + 2 - #word
- until not state.more
- end
- local function match_with_pattern(state, startPos, pattern, operation)
- -- pattern must be
- -- "^[some set of chars]+"
- -- returns
- -- matched_string, endPos for operation "read" (matched_string == "" if no match found)
- -- endPos for operation "skip"
- if operation == "read" then
- local t = {}
- repeat
- if state.more then read_ahead(state, startPos) end
- local part, disp = state.part, state.disp
- local str = string_match(part, pattern, startPos - disp)
- if str then
- table_insert(t, str)
- startPos = startPos + #str
- end
- until not str or startPos <= disp + #part
- return table_concat(t), startPos
- elseif operation == "skip" then
- repeat
- if state.more then read_ahead(state, startPos) end
- local part, disp = state.part, state.disp
- local b, e = string_find(part, pattern, startPos - disp)
- if b then
- startPos = startPos + e - b + 1
- end
- until not b or startPos <= disp + #part
- return startPos
- else
- error("Wrong operation name")
- end
- end
- --- Decodes a JSON string and returns the decoded value as a Lua data structure / value.
- -- @param state The state of JSON reader.
- -- @param startPos Starting position where the JSON string is located.
- -- @param traverse (optional) table with fields "path" and "callback" for traversing JSON.
- -- @param decode_key (optional) boolean flag for decoding key inside JSON object.
- -- @return Lua_object,int The object that was scanned, as a Lua table / string / number / boolean / json.null / json.empty,
- -- and the position of the first character after the scanned JSON object.
- function decode(state, startPos, traverse, decode_key)
- local curChar, value, nextPos
- startPos, curChar = decode_scanWhitespace(state, startPos)
- if curChar == '{' and not decode_key then
- -- Object
- if traverse and traverse.callback(traverse.path, "object", nil, startPos, nil) then
- -- user wants to decode this JSON object (and get it as Lua value) while traversing
- local object, endPos = decode_scanObject(state, startPos)
- traverse.callback(traverse.path, "object", object, startPos, endPos - 1)
- return false, endPos
- end
- return decode_scanObject(state, startPos, traverse)
- elseif curChar == '[' and not decode_key then
- -- Array
- if traverse and traverse.callback(traverse.path, "array", nil, startPos, nil) then
- -- user wants to decode this JSON array (and get it as Lua value) while traversing
- local array, endPos = decode_scanArray(state, startPos)
- traverse.callback(traverse.path, "array", array, startPos, endPos - 1)
- return false, endPos
- end
- return decode_scanArray(state, startPos, traverse)
- elseif curChar == '"' then
- -- String
- value, nextPos = decode_scanString(state, startPos)
- if traverse then
- traverse.callback(traverse.path, "string", value, startPos, nextPos - 1)
- end
- elseif decode_key then
- -- Unquoted string as key name
- return decode_scanIdentifier(state, startPos)
- elseif string_find(curChar, "^[%d%-]") then
- -- Number
- value, nextPos = decode_scanNumber(state, startPos)
- if traverse then
- traverse.callback(traverse.path, "number", value, startPos, nextPos - 1)
- end
- else
- -- Otherwise, it must be a constant
- value, nextPos = decode_scanConstant(state, startPos)
- if traverse then
- traverse.callback(traverse.path, value == null and "null" or "boolean", value, startPos, nextPos - 1)
- end
- end
- return value, nextPos
- end
- -----------------------------------------------------------------------------
- -- Internal, PRIVATE functions.
- -- Following a Python-like convention, I have prefixed all these 'PRIVATE'
- -- functions with an underscore.
- -----------------------------------------------------------------------------
- --- Scans an array from JSON into a Lua object
- -- startPos begins at the start of the array.
- -- Returns the array and the next starting position
- -- @param state The state of JSON reader.
- -- @param startPos The starting position for the scan.
- -- @param traverse (optional) table with fields "path" and "callback" for traversing JSON.
- -- @return table,int The scanned array as a table, and the position of the next character to scan.
- function decode_scanArray(state, startPos, traverse)
- local array = not traverse and {} -- The return value
- local elem_index, elem_ready, object = 1
- startPos = startPos + 1
- -- Infinite loop for array elements
- while true do
- repeat
- local curChar
- startPos, curChar = decode_scanWhitespace(state, startPos)
- if curChar == ']' then
- return array, startPos + 1
- elseif curChar == ',' then
- if not elem_ready then
- -- missing value in JSON array
- if traverse then
- table_insert(traverse.path, elem_index)
- traverse.callback(traverse.path, "null", null, startPos, startPos - 1) -- empty substring: pos_last = pos - 1
- table_remove(traverse.path)
- else
- array[elem_index] = null
- end
- end
- elem_ready = false
- elem_index = elem_index + 1
- startPos = startPos + 1
- end
- until curChar ~= ','
- if elem_ready then
- error('Comma is missing in JSON array at position '..startPos)
- end
- if traverse then
- table_insert(traverse.path, elem_index)
- end
- object, startPos = decode(state, startPos, traverse)
- if traverse then
- table_remove(traverse.path)
- else
- array[elem_index] = object
- end
- elem_ready = true
- end
- end
- --- Scans for given constants: true, false or null
- -- Returns the appropriate Lua type, and the position of the next character to read.
- -- @param state The state of JSON reader.
- -- @param startPos The position in the string at which to start scanning.
- -- @return object, int The object (true, false or json.null) and the position at which the next character should be scanned.
- function decode_scanConstant(state, startPos)
- local w5 = get_word(state, startPos, 5)
- local w4 = string_sub(w5, 1, 4)
- if w5 == "false" then
- return false, startPos + 5
- elseif w4 == "true" then
- return true, startPos + 4
- elseif w4 == "null" then
- return null, startPos + 4
- end
- error('Failed to parse JSON at position '..startPos)
- end
- --- Scans a number from the JSON encoded string.
- -- (in fact, also is able to scan numeric +- eqns, which is not in the JSON spec.)
- -- Returns the number, and the position of the next character after the number.
- -- @param state The state of JSON reader.
- -- @param startPos The position at which to start scanning.
- -- @return number,int The extracted number and the position of the next character to scan.
- function decode_scanNumber(state, startPos)
- local stringValue, endPos = match_with_pattern(state, startPos, '^[%+%-%d%.eE]+', "read")
- local stringEval = loadstring('return '..stringValue)
- if not stringEval then
- error('Failed to scan number '..stringValue..' in JSON string at position '..startPos)
- end
- return stringEval(), endPos
- end
- --- Scans a JSON object into a Lua object.
- -- startPos begins at the start of the object.
- -- Returns the object and the next starting position.
- -- @param state The state of JSON reader.
- -- @param startPos The starting position of the scan.
- -- @param traverse (optional) table with fields "path" and "callback" for traversing JSON
- -- @return table,int The scanned object as a table and the position of the next character to scan.
- function decode_scanObject(state, startPos, traverse)
- local object, elem_ready = not traverse and empty
- startPos = startPos + 1
- while true do
- repeat
- local curChar
- startPos, curChar = decode_scanWhitespace(state, startPos)
- if curChar == '}' then
- return object, startPos + 1
- elseif curChar == ',' then
- startPos = startPos + 1
- elem_ready = false
- end
- until curChar ~= ','
- if elem_ready then
- error('Comma is missing in JSON object at '..startPos)
- end
- -- Scan the key as string or unquoted identifier such as in {"a":1,b:2}
- local key, value
- key, startPos = decode(state, startPos, nil, true)
- local colon
- startPos, colon = decode_scanWhitespace(state, startPos)
- if colon ~= ':' then
- error('JSON object key-value assignment mal-formed at '..startPos)
- end
- startPos = decode_scanWhitespace(state, startPos + 1)
- if traverse then
- table_insert(traverse.path, key)
- end
- value, startPos = decode(state, startPos, traverse)
- if traverse then
- table_remove(traverse.path)
- else
- if object == empty then
- object = {}
- end
- object[key] = value
- end
- elem_ready = true
- end -- infinite loop while key-value pairs are found
- end
- --- Scans JSON string for an identifier (unquoted key name inside object)
- -- Returns the string extracted as a Lua string, and the position after the closing quote.
- -- @param state The state of JSON reader.
- -- @param startPos The starting position of the scan.
- -- @return string,int The extracted string as a Lua string, and the next character to parse.
- function decode_scanIdentifier(state, startPos)
- local identifier, idx = match_with_pattern(state, startPos, '^[%w_%-%$]+', "read")
- if identifier == "" then
- error('JSON String decoding failed: missing key name at position '..startPos)
- end
- return identifier, idx
- end
- -- START SoniEx2
- -- Initialize some things used by decode_scanString
- -- You know, for efficiency
- local escapeSequences = { t = "\t", f = "\f", r = "\r", n = "\n", b = "\b" }
- -- END SoniEx2
- --- Scans a JSON string from the opening quote to the end of the string.
- -- Returns the string extracted as a Lua string, and the position after the closing quote.
- -- @param state The state of JSON reader.
- -- @param startPos The starting position of the scan.
- -- @return string,int The extracted string as a Lua string, and the next character to parse.
- function decode_scanString(state, startPos)
- local t, idx, surrogate_pair_started, regular_part = {}, startPos + 1
- while true do
- regular_part, idx = match_with_pattern(state, idx, '^[^"\\]+', "read")
- table_insert(t, regular_part)
- local w6 = get_word(state, idx, 6)
- local c = string_sub(w6, 1, 1)
- if c == '"' then
- return table_concat(t), idx + 1
- elseif c == '\\' then
- local esc = string_sub(w6, 2, 2)
- if esc == "u" then
- local n = tonumber(string_sub(w6, 3), 16)
- if not n then
- error("String decoding failed: bad Unicode escape "..w6.." at position "..idx)
- end
- -- Handling of UTF-16 surrogate pairs
- if n >= 0xD800 and n < 0xDC00 then
- surrogate_pair_started, n = n
- elseif n >= 0xDC00 and n < 0xE000 then
- n, surrogate_pair_started = surrogate_pair_started and (surrogate_pair_started - 0xD800) * 0x400 + (n - 0xDC00) + 0x10000
- end
- if n then
- -- Convert unicode codepoint n (0..0x10FFFF) to UTF-8 string
- local x
- if n < 0x80 then
- x = string_char(n % 0x80)
- elseif n < 0x800 then
- -- [110x xxxx] [10xx xxxx]
- x = string_char(0xC0 + (math_floor(n/64) % 0x20), 0x80 + (n % 0x40))
- elseif n < 0x10000 then
- -- [1110 xxxx] [10xx xxxx] [10xx xxxx]
- x = string_char(0xE0 + (math_floor(n/64/64) % 0x10), 0x80 + (math_floor(n/64) % 0x40), 0x80 + (n % 0x40))
- else
- -- [1111 0xxx] [10xx xxxx] [10xx xxxx] [10xx xxxx]
- x = string_char(0xF0 + (math_floor(n/64/64/64) % 8), 0x80 + (math_floor(n/64/64) % 0x40), 0x80 + (math_floor(n/64) % 0x40), 0x80 + (n % 0x40))
- end
- table_insert(t, x)
- end
- idx = idx + 6
- else
- table_insert(t, escapeSequences[esc] or esc)
- idx = idx + 2
- end
- else
- error('String decoding failed: missing closing " for string at position '..startPos)
- end
- end
- end
- --- Scans a JSON string skipping all whitespace from the current start position.
- -- Returns the position of the first non-whitespace character.
- -- @param state The state of JSON reader.
- -- @param startPos The starting position where we should begin removing whitespace.
- -- @return int,char The first position where non-whitespace was encountered, non-whitespace char.
- function decode_scanWhitespace(state, startPos)
- while true do
- startPos = match_with_pattern(state, startPos, '^[ \n\r\t]+', "skip")
- local w2 = get_word(state, startPos, 2)
- if w2 == '/*' then
- local endPos = skip_until_word(state, startPos + 2, '*/')
- if not endPos then
- error("Unterminated comment in JSON string at "..startPos)
- end
- startPos = endPos
- else
- local next_char = string_sub(w2, 1, 1)
- if next_char == '' then
- error('Unexpected end of JSON')
- end
- return startPos, next_char
- end
- end
- end
- --- Encodes a string to be JSON-compatible.
- -- This just involves backslash-escaping of quotes, slashes and control codes
- -- @param s The string to return as a JSON encoded (i.e. backquoted string)
- -- @return string The string appropriately escaped.
- local escapeList = {
- ['"'] = '\\"',
- ['\\'] = '\\\\',
- ['/'] = '\\/',
- ['\b'] = '\\b',
- ['\f'] = '\\f',
- ['\n'] = '\\n',
- ['\r'] = '\\r',
- ['\t'] = '\\t',
- ['\127'] = '\\u007F'
- }
- function encodeString(s)
- if type(s) == 'number' then
- s = math_type(s) == 'integer' and tostring(s) or string_format('%.f', s)
- end
- return string_gsub(s, ".", function(c) return escapeList[c] or c:byte() < 32 and string_format('\\u%04X', c:byte()) end)
- end
- -- Determines whether the given Lua type is an array or a table / dictionary.
- -- We consider any table an array if it has indexes 1..n for its n items, and no other data in the table.
- -- I think this method is currently a little 'flaky', but can't think of a good way around it yet...
- -- @param t The table to evaluate as an array
- -- @return boolean,number True if the table can be represented as an array, false otherwise.
- -- If true, the second returned value is the maximum number of indexed elements in the array.
- function isArray(t)
- -- Next we count all the elements, ensuring that any non-indexed elements are not-encodable
- -- (with the possible exception of 'n')
- local maxIndex = 0
- for k, v in pairs(t) do
- if type(k) == 'number' and math_floor(k) == k and 0 <= k and k <= 1e6 then -- k,v is an indexed pair
- if not isEncodable(v) then -- All array elements must be encodable
- return false
- end
- maxIndex = math_max(maxIndex, k)
- elseif not (k == 'n' and v == #t) then -- if it is n, then n does not hold the number of elements
- if isConvertibleToString(k) and isEncodable(v) then
- return false
- end
- end -- End of k,v not an indexed pair
- end -- End of loop across all pairs
- return true, maxIndex
- end
- --- Determines whether the given Lua object / table / value can be JSON encoded.
- -- The only types that are JSON encodable are: string, boolean, number, nil, table and special tables json.null and json.empty.
- -- @param o The object to examine.
- -- @return boolean True if the object should be JSON encoded, false if it should be ignored.
- function isEncodable(o)
- local t = type(o)
- return t == 'string' or t == 'boolean' or t == 'number' and isRegularNumber(o) or t == 'nil' or t == 'table'
- end
- --- Determines whether the given Lua object / table / variable can be a JSON key.
- -- Integer Lua numbers are allowed to be considered as valid string keys in JSON.
- -- @param o The object to examine.
- -- @return boolean True if the object can be converted to a string, false if it should be ignored.
- function isConvertibleToString(o)
- local t = type(o)
- return t == 'string' or t == 'number' and isRegularNumber(o) and (math_type(o) == 'integer' or math_floor(o) == o)
- end
- local is_Inf_or_NaN = {[tostring(1/0)]=true, [tostring(-1/0)]=true, [tostring(0/0)]=true, [tostring(-(0/0))]=true}
- --- Determines whether the given Lua number is a regular number or Inf/Nan.
- -- @param v The number to examine.
- -- @return boolean True if the number is a regular number which may be encoded in JSON.
- function isRegularNumber(v)
- return not is_Inf_or_NaN[tostring(v)]
- end
- end
- -- RSWarehouse.lua
- -- Author: Scott Adkins <[email protected]> (Zucanthor)
- -- Published: 2021-09-21
- --
- -- This program monitors work requests for the Minecolonies Warehouse and
- -- tries to fulfill requests from the Refined Storage network. If the
- -- RS network doesn't have enough items and a crafting pattern exists, a
- -- crafting job is scheduled to restock the items in order to fulfill the
- -- work request. The script will continuously loop, monitoring for new
- -- requests and checking on crafting jobs to fulfill previous requests.
- -- The following is required for setup:
- -- * 1 ComputerCraft Computer
- -- * 1 or more ComputerCraft Monitors (recommend 3x3 monitors)
- -- * 1 Advanced Peripheral Colony Integrator
- -- * 1 Advanced Peripheral RS Bridge
- -- * 1 Chest or other storage container
- -- Attach an RS Cable from the RS network to the RS Bridge. Connect the
- -- storage container to the Minecolonies Warehouse Hut block. One idea is
- -- to set up a second RS network attached to the Warehouse Hut using an
- -- External Storage connector and then attach an Importer for that network
- -- to the storage container.
- -- THINGS YOU CAN CUSTOMIZE IN THIS PROGRAM:
- -- Line 59: Specify the side storage container is at.
- -- Line 66: Name of log file for storing JSON data of all open requests.
- -- Lines 231+: Any items you find that should be manually provided.
- -- Line 373: Time in seconds between work order scans.
- ----------------------------------------------------------------------------
- -- INITIALIZATION
- ----------------------------------------------------------------------------
- -- Initialize Monitor
- -- A future update may allow for multiple monitors. This would allow one
- -- monitor to be used for logging and another to be used for work requests.
- local monitor = peripheral.find("monitor")
- if not monitor then error("Monitor not found.") end
- monitor.setTextScale(0.5)
- monitor.clear()
- monitor.setCursorPos(1, 1)
- monitor.setCursorBlink(false)
- print("Monitor initialized.")
- -- Initialize RS Bridge
- local bridge = peripheral.find("meBridge")
- if not bridge then error("ME Bridge not found.") end
- print("RS Bridge initialized.")
- -- Initialize Colony Integrator
- local colony = peripheral.find("colonyIntegrator")
- if not colony then error("Colony Integrator not found.") end
- if not colony.isInColony then error("Colony Integrator is not in a colony.") end
- print("Colony Integrator initialized.")
- -- Point to location of chest or storage container
- -- A future update may autodetect where the storage container is and error
- -- out if no storage container is found.
- --local storage = peripheral.find("minecraft:chest")
- --if not storage then error("Storage not found") end
- storage = "up"
- print("Storage initialized.")
- -- Name of log file to capture JSON data from the open requests. The log can
- -- be too big to edit within CC, which may require a "pastebin put" if you want
- -- to look at it. Logging could be improved to only capture Skipped items,
- -- which in turn will make log files smaller and edittable in CC directly.
- local logFile = "RSWarehouse.log"
- ----------------------------------------------------------------------------
- -- FUNCTIONS
- ----------------------------------------------------------------------------
- -- Prints to the screen one row after another, scrolling the screen when
- -- reaching the bottom. Acts as a normal display where text is printed in
- -- a standard way. Long lines are not wrapped and newlines are printed as
- -- spaces, both to be addressed in a future update.
- -- NOTE: No longer used in this program.
- function mPrintScrollable(mon, ...)
- w, h = mon.getSize()
- x, y = mon.getCursorPos()
- -- Blink the cursor like a normal display.
- mon.setCursorBlink(true)
- -- For multiple strings, append them with a space between each.
- for i = 2, #arg do t = t.." "..arg[i] end
- mon.write(arg[1])
- if y >= h then
- mon.scroll(1)
- mon.setCursorPos(1, y)
- else
- mon.setCursorPos(1, y+1)
- end
- end
- -- Prints strings left, centered, or right justified at a specific row and
- -- specific foreground/background color.
- function mPrintRowJustified(mon, y, pos, text, ...)
- w, h = mon.getSize()
- fg = mon.getTextColor()
- bg = mon.getBackgroundColor()
- if pos == "left" then x = 1 end
- if pos == "center" then x = math.floor((w - #text) / 2) end
- if pos == "right" then x = w - #text end
- if #arg > 0 then mon.setTextColor(arg[1]) end
- if #arg > 1 then mon.setBackgroundColor(arg[2]) end
- mon.setCursorPos(x, y)
- mon.write(text)
- mon.setTextColor(fg)
- mon.setBackgroundColor(bg)
- end
- -- Utility function that returns true if the provided character is a digit.
- -- Yes, this is a hack and there are better ways to do this. Clearly.
- function isdigit(c)
- if c == "0" then return true end
- if c == "1" then return true end
- if c == "2" then return true end
- if c == "3" then return true end
- if c == "4" then return true end
- if c == "5" then return true end
- if c == "6" then return true end
- if c == "7" then return true end
- if c == "8" then return true end
- if c == "9" then return true end
- return false
- end
- -- Utility function that displays current time and remaining time on timer.
- -- For time of day, yellow is day, orange is sunset/sunrise, and red is night.
- -- The countdown timer is orange over 15s, yellow under 15s, and red under 5s.
- -- At night, the countdown timer is red and shows PAUSED insted of a time.
- function displayTimer(mon, t)
- now = os.time()
- cycle = "day"
- cycle_color = colors.orange
- if now >= 4 and now < 6 then
- cycle = "sunrise"
- cycle_color = colors.orange
- elseif now >= 6 and now < 18 then
- cycle = "day"
- cycle_color = colors.yellow
- elseif now >= 18 and now < 19.5 then
- cycle = "sunset"
- cycle_color = colors.orange
- elseif now >= 19.5 or now < 5 then
- cycle = "night"
- cycle_color = colors.red
- end
- timer_color = colors.orange
- if t < 15 then timer_color = colors.yellow end
- if t < 5 then timer_color = colors.red end
- mPrintRowJustified(mon, 1, "left", string.format("Time: %s [%s] ", textutils.formatTime(now, false), cycle), cycle_color)
- if cycle ~= "night" then mPrintRowJustified(mon, 1, "right", string.format(" Remaining: %ss", t), timer_color)
- else mPrintRowJustified(mon, 1, "right", " Remaining: PAUSED", colors.red) end
- end
- -- Scan all open work requests from the Warehouse and attempt to satisfy those
- -- requests. Display all activity on the monitor, including time of day and the
- -- countdown timer before next scan. This function is not called at night to
- -- save on some ticks, as the colonists are in bed anyways. Items in red mean
- -- work order can't be satisfied by Refined Storage (lack of pattern or lack of
- -- required crafting ingredients). Yellow means order partially filled and a
- -- crafting job was scheduled for the rest. Green means order fully filled.
- -- Blue means the Player needs to manually fill the work order. This includes
- -- equipment (Tools of Class), NBT items like armor, weapons and tools, as well
- -- as generic requests ike Compostables, Fuel, Food, Flowers, etc.
- function scanWorkRequests(mon, rs, chest)
- -- Before we do anything, prep the log file for this scan.
- -- The log file is truncated each time this function is called.
- file = fs.open(logFile, "w")
- print("\nScan starting at", textutils.formatTime(os.time(), false) .. " (" .. os.time() ..").")
- -- We want to keep three different lists so that they can be
- -- displayed on the monitor in a more intelligent way. The first
- -- list is for the Builder requests. The second list is for the
- -- non-Builder requests. The third list is for any armor, tools
- -- and weapons requested by the colonists.
- builder_list = {}
- nonbuilder_list = {}
- equipment_list = {}
- -- Scan RS for all items in its network. Ignore items with NBT data.
- -- If a Builder needs any items with NBT data, this function will need
- -- to be updated to not ignore those items.
- items = rs.listItems()
- item_array = {}
- for index, item in ipairs(items) do
- item_array[item.name] = {amount = item.amount, nbt = json.encode(item.nbt)}
- end
- end
- -- Scan the Warehouse for all open work requests. For each item, try to
- -- provide as much as possible from RS, then craft whatever is needed
- -- after that. Green means item was provided entirely. Yellow means item
- -- is being crafted. Red means item is missing crafting recipe.
- workRequests = colony.getRequests()
- file.write(textutils.serialize(workRequests))
- for w in pairs(workRequests) do
- name = workRequests[w].name
- item = workRequests[w].items[1].name
- nbtt = workRequests[w].items[1].nbt
- target = workRequests[w].target
- desc = workRequests[w].desc
- needed = workRequests[w].count
- provided = 0
- target_words = {}
- target_length = 0
- for word in target:gmatch("%S+") do
- table.insert(target_words, word)
- target_length = target_length + 1
- end
- if target_length >= 3 then target_name = target_words[target_length-2] .. " " .. target_words[target_length]
- else target_name = target end
- target_type = ""
- target_count = 1
- repeat
- if target_type ~= "" then target_type = target_type .. " " end
- target_type = target_type .. target_words[target_count]
- target_count = target_count + 1
- until target_count > target_length - 3
- useRS = 1
- if string.find(desc, "Tool of class") then useRS = 0 end
- if string.find(name, "Hoe") then useRS = 0 end
- if string.find(name, "Shovel") then useRS = 0 end
- if string.find(name, "Axe") then useRS = 0 end
- if string.find(name, "Pickaxe") then useRS = 0 end
- if string.find(name, "Bow") then useRS = 0 end
- if string.find(name, "Sword") then useRS = 0 end
- if string.find(name, "Shield") then useRS = 0 end
- if string.find(name, "Helmet") then useRS = 0 end
- if string.find(name, "Leather Cap") then useRS = 0 end
- if string.find(name, "Chestplate") then useRS = 0 end
- if string.find(name, "Tunic") then useRS = 0 end
- if string.find(name, "Pants") then useRS = 0 end
- if string.find(name, "Leggings") then useRS = 0 end
- if string.find(name, "Boots") then useRS = 0 end
- if string.find(item, "domum_ornamentum") then useRS = 0 end
- if name == "Rallying Banner" then useRS = 0 end --bugged in alpha versions
- if name == "Crafter" then useRS = 0 end
- if name == "Compostable" then useRS = 0 end
- if name == "Fertilizer" then useRS = 0 end
- if name == "Flowers" then useRS = 0 end
- if name == "Food" then useRS = 0 end
- if name == "Fuel" then item = "minecraft:coal" end
- if name == "Smeltable Ore" then useRS = 0 end
- if name == "Stack List" then useRS = 0 end
- color = colors.blue
- if useRS == 1 then
- provided = 0
- if item_array[item].amount > 0 then
- provided = rs.exportItem({name=item, count=needed}, chest)
- end
- color = colors.green
- if provided < needed then
- if rs.isItemCrafting({name=item}) then
- color = colors.yellow
- print("[Crafting]", item)
- else
- if rs.craftItem({name=item, count=needed}) then
- color = colors.yellow
- print("[Scheduled]", needed, "x", item)
- else
- color = colors.red
- print("[Failed]", item)
- end
- end
- end
- else
- nameString = name .. " [" .. target .. "]"
- print("[Skipped]", nameString)
- end
- if string.find(desc, "of class") then
- level = "Any Level"
- if string.find(desc, "with maximal level:Leather") then level = "Leather" end
- if string.find(desc, "with maximal level:Gold") then level = "Gold" end
- if string.find(desc, "with maximal level:Chain") then level = "Chain" end
- if string.find(desc, "with maximal level:Wood or Gold") then level = "Wood or Gold" end
- if string.find(desc, "with maximal level:Stone") then level = "Stone" end
- if string.find(desc, "with maximal level:Iron") then level = "Iron" end
- if string.find(desc, "with maximal level:Diamond") then level = "Diamond" end
- new_name = level .. " " .. name
- if level == "Any Level" then new_name = name .. " of any level" end
- new_target = target_type .. " " .. target_name
- equipment = { name=new_name, target=new_target, needed=needed, provided=provided, color=color}
- table.insert(equipment_list, equipment)
- elseif string.find(target, "Builder") then
- builder = { name=name, item=item, target=target_name, needed=needed, provided=provided, color=color }
- table.insert(builder_list, builder)
- else
- new_target = target_type .. " " .. target_name
- if target_length < 3 then
- new_target = target
- end
- nonbuilder = { name=name, target=new_target, needed=needed, provided=provided, color=color }
- table.insert(nonbuilder_list, nonbuilder)
- end
- end
- -- Show the various lists on the attached monitor.
- row = 3
- mon.clear()
- header_shown = 0
- for e in pairs(equipment_list) do
- equipment = equipment_list[e]
- if header_shown == 0 then
- mPrintRowJustified(mon, row, "center", "Equipment")
- header_shown = 1
- row = row + 1
- end
- text = string.format("%d %s", equipment.needed, equipment.name)
- mPrintRowJustified(mon, row, "left", text, equipment.color)
- mPrintRowJustified(mon, row, "right", " " .. equipment.target, equipment.color)
- row = row + 1
- end
- header_shown = 0
- for b in pairs(builder_list) do
- builder = builder_list[b]
- if header_shown == 0 then
- if row > 1 then row = row + 1 end
- mPrintRowJustified(mon, row, "center", "Builder Requests")
- header_shown = 1
- row = row + 1
- end
- text = string.format("%d/%s", builder.provided, builder.name)
- mPrintRowJustified(mon, row, "left", text, builder.color)
- mPrintRowJustified(mon, row, "right", " " .. builder.target, builder.color)
- row = row + 1
- end
- header_shown = 0
- for n in pairs(nonbuilder_list) do
- nonbuilder = nonbuilder_list[n]
- if header_shown == 0 then
- if row > 1 then row = row + 1 end
- mPrintRowJustified(mon, row, "center", "Nonbuilder Requests")
- header_shown = 1
- row = row + 1
- end
- text = string.format("%d %s", nonbuilder.needed, nonbuilder.name)
- if isdigit(nonbuilder.name:sub(1,1)) then
- text = string.format("%d/%s", nonbuilder.provided, nonbuilder.name)
- end
- mPrintRowJustified(mon, row, "left", text, nonbuilder.color)
- mPrintRowJustified(mon, row, "right", " " .. nonbuilder.target, nonbuilder.color)
- row = row + 1
- end
- if row == 3 then mPrintRowJustified(mon, row, "center", "No Open Requests") end
- print("Scan completed at", textutils.formatTime(os.time(), false) .. " (" .. os.time() ..").")
- file.close()
- end
- ----------------------------------------------------------------------------
- -- MAIN
- ----------------------------------------------------------------------------
- -- Scan for requests periodically. This will catch any updates that were
- -- triggered from the previous scan. Right-clicking on the monitor will
- -- trigger an immediate scan and reset the timer. Unfortunately, there is
- -- no way to capture left-clicks on the monitor.
- local time_between_runs = 30
- local current_run = time_between_runs
- scanWorkRequests(monitor, bridge, storage)
- displayTimer(monitor, current_run)
- local TIMER = os.startTimer(1)
- while true do
- local e = {os.pullEvent()}
- if e[1] == "timer" and e[2] == TIMER then
- now = os.time()
- if now >= 5 and now < 19.5 then
- current_run = current_run - 1
- if current_run <= 0 then
- scanWorkRequests(monitor, bridge, storage)
- current_run = time_between_runs
- end
- end
- displayTimer(monitor, current_run)
- TIMER = os.startTimer(1)
- elseif e[1] == "monitor_touch" then
- os.cancelTimer(TIMER)
- scanWorkRequests(monitor, bridge, storage)
- current_run = time_between_runs
- displayTimer(monitor, current_run)
- TIMER = os.startTimer(1)
- end
- end
Advertisement
Add Comment
Please, Sign In to add comment