mirror of
https://github.com/sbrl/Minetest-WorldEditAdditions.git
synced 2024-05-31 20:12:57 +00:00
325 lines
14 KiB
Lua
325 lines
14 KiB
Lua
local weac = worldeditadditions_core
|
|
local Vector3
|
|
local parse_json, split
|
|
if worldeditadditions_core then
|
|
Vector3 = weac.Vector3
|
|
parse_json = dofile(weac.modpath.."/utils/parse/json.lua")
|
|
split = weac.split
|
|
else
|
|
Vector3 = require("worldeditadditions_core.utils.vector3")
|
|
parse_json = require("worldeditadditions_core.utils.parse.json")
|
|
split = require("worldeditadditions_core.utils.strings.split")
|
|
end
|
|
|
|
|
|
--- Library for parsing .weaschem WorldEditAdditions Schematic files.
|
|
--
|
|
-- Most of these functions return 3 values rather than the usual 2. This might seem overcomplicated, but it enables a higher level of validation in automated testing. These 3 return values take the following form:
|
|
--
|
|
-- 1. bool: A success/failure bool. `true` means success, and `false`, means failure.
|
|
-- 2. string: The error code. "SUCCESS" if #1 (the bool) is `true`. Otherwise set to a code to indicate exactly which error ocurred.
|
|
-- 3. any|string: EITHER the expected return value, OR a string with a human-readable error message if `bool=false`.
|
|
-- @namespace worldeditadditions_core.parse.file.weaschem
|
|
local weaschem = {}
|
|
|
|
--- Validates and converts a raw table into a Vector3 instance.
|
|
-- @param source_obj table The table to convert.
|
|
-- @returns bool,string,Vector3|string A success bool, then an error code, then finally EITHER the resulting Vector3 instance (if bool=true) OR an error message as a string.
|
|
function weaschem.parse_vector3(source_obj)
|
|
if type(source_obj) ~= "table" then return false, "VECTOR3_INVALID_TYPE", "Vector3: Expected a table, but got a value of type '"..type(source_obj).."'." end
|
|
|
|
if source_obj.x == nil then return false, "VECTOR3_NO_X", "Vector3: No value for the x coordinate was found." end
|
|
if source_obj.y == nil then return false, "VECTOR3_NO_Y", "Vector3: No value for the y coordinate was found." end
|
|
if source_obj.z == nil then return false, "VECTOR3_NO_Z", "Vector3: No value for the z coordinate was found." end
|
|
if type(source_obj.x) ~= "number" then return false, "VECTOR3_INVALID_X", "Vector3: The type of the x coordinate was expected to be number, but found '"..type(source_obj.x).."'." end
|
|
if type(source_obj.y) ~= "number" then return false, "VECTOR3_INVALID_Y", "Vector3: The type of the y coordinate was expected to be number, but found '"..type(source_obj.y).."'." end
|
|
if type(source_obj.z) ~= "number" then return false, "VECTOR3_INVALID_Z", "Vector3: The type of the z coordinate was expected to be number, but found '"..type(source_obj.z).."'." end
|
|
|
|
local result = Vector3.clone(source_obj)
|
|
|
|
local rounded = result:floor()
|
|
if math.abs(result.x - rounded.x) > 0.0000001 then return false, "VECTOR3_X_FLOAT", "The x coordinate appears to be a floating-point number and not an integer. Only integer values in schematics are supported." end
|
|
if math.abs(result.y - rounded.y) > 0.0000001 then return false, "VECTOR3_Y_FLOAT", "The y coordinate appears to be a floating-point number and not an integer. Only integer values in schematics are supported." end
|
|
if math.abs(result.z - rounded.z) > 0.0000001 then return false, "VECTOR3_Z_FLOAT", "The z coordinate appears to be a floating-point number and not an integer. Only integer values in schematics are supported." end
|
|
|
|
return true, "SUCCESS", result
|
|
end
|
|
|
|
|
|
--- Parses the header out of the given source string.
|
|
-- @param source string The source string to parse.
|
|
-- @returns bool,string,table|string 1. Success bool (`true` == success)
|
|
-- 2. Error code. "SUCCESS" is passed if the operation was successful.
|
|
-- 3. Either the resulting Lua table if parsing was successful, or an error message as a string if not.
|
|
function weaschem.parse_header(source)
|
|
local raw_obj = parse_json(source)
|
|
if raw_obj == nil then return false, "HEADER_INVALID_JSON", "The header is invalid JSON." end
|
|
|
|
local header = {}
|
|
|
|
|
|
if raw_obj["name"] == nil then return false, "HEADER_NO_NAME", "No name property found in header."
|
|
else
|
|
header["name"] = raw_obj["name"]
|
|
if type(header["name"]) ~= "string" then
|
|
return false, "HEADER_NAME_INVALID",
|
|
"Invalid name in header: expected string, but found value of type '"..type(raw_obj["name"]).."'."
|
|
end
|
|
end
|
|
if raw_obj["description"] ~= nil then
|
|
header["description"] = raw_obj["description"]
|
|
if type(header["description"]) ~= "string" then
|
|
return false, "HEADER_DESCRIPTION_INVALID",
|
|
"Invalid description in header: expected string, but found value of type '"..type(raw_obj["description"]).."'."
|
|
end
|
|
end
|
|
|
|
|
|
if raw_obj["offset"] == nil then
|
|
return false, "HEADER_NO_OFFSET", "No offset property found in header."
|
|
else
|
|
local success, code, result = weaschem.parse_vector3(raw_obj["offset"])
|
|
if not success then return success, code, result end
|
|
header["offset"] = result
|
|
end
|
|
if raw_obj["size"] == nil then return false, "HEADER_NO_SIZE", "No size property found in header."
|
|
else
|
|
local success, code, result = weaschem.parse_vector3(raw_obj["size"])
|
|
if not success then return success, code, result end
|
|
header["size"] = result
|
|
end
|
|
|
|
|
|
if raw_obj["type"] == nil then
|
|
return false, "HEADER_NO_TYPE", "No type property found in header."
|
|
else
|
|
header["type"] = raw_obj["type"]
|
|
if type(header["type"]) ~= "string" then
|
|
return false, "HEADER_TYPE_INVALID",
|
|
"Invalid type in header: expected string, but found value of type '" .. type(raw_obj["type"]).."'."
|
|
end
|
|
if header["type"] ~= "full" and header["type"] ~= "delta" then
|
|
return false, "HEADER_TYPE_INVALID",
|
|
"The value of the header field type was expected to be 'full' or 'delta', but was found to be '" ..
|
|
tostring(header["type"]) .. "'."
|
|
end
|
|
end
|
|
|
|
|
|
if raw_obj["generator"] == nil then
|
|
return false, "HEADER_NO_GENERATOR", "No generator property found in header."
|
|
else
|
|
header["generator"] = raw_obj["generator"]
|
|
if type(header["generator"]) ~= "string" then
|
|
return false, "HEADER_GENERATOR_INVALID",
|
|
"Invalid generator in header: expected string, but found value of generator '"..type(raw_obj["generator"]).."'."
|
|
end
|
|
end
|
|
|
|
|
|
return true, "SUCCESS", header
|
|
end
|
|
|
|
|
|
|
|
--- Parses the ID map out of the given source string.
|
|
-- @param source string The source string to parse.
|
|
-- @returns bool,string,table|string 1. Success bool (`true` == success)
|
|
-- 2. Error code. "SUCCESS" is passed if the operation was successful.
|
|
-- 3. Either the resulting Lua table if parsing was successful, or an error message as a string if not.
|
|
function weaschem.parse_id_map(source)
|
|
local raw_obj = parse_json(source)
|
|
if raw_obj == nil then return false, "ID_MAP_INVALID_JSON", "The node id map is invalid JSON." end
|
|
|
|
local result = {}
|
|
for id_str, node_name in pairs(raw_obj) do
|
|
local id = tonumber(id_str)
|
|
if id == nil then
|
|
return false, "ID_MAP_INVALID_ID", "A node id in the node id map is not parsable as a number."
|
|
end
|
|
if string.find(node_name, ":") == nil then
|
|
return false, "ID_MAP_RELATIVE_NODE_NAME", "A node name does not contain a colon, suggesting it is a relative node id. Relative node ids are not supported."
|
|
end
|
|
if id - math.floor(id) > 0.0000001 then
|
|
return false, "ID_MAP_FLOATING_POINT_NODE_NAME", "Error: All node ids in the node id map must be integers."
|
|
end
|
|
if id < 0 then
|
|
return false, "ID_MAP_NEGATIVE_NODE_ID", "Error: All node ids in the node id map must be integers greater than or equal to 0."
|
|
end
|
|
|
|
if result[id] ~= nil then
|
|
return false, "ID_MAP_DUPLICATE", "Multiple node ids in the node id map parse to the same number."
|
|
end
|
|
|
|
result[id] = node_name
|
|
end
|
|
|
|
return true, "SUCCESS", result
|
|
end
|
|
|
|
--- Parses a data table string into a Lua table.
|
|
-- @internal
|
|
-- @param source string The source string to parse.
|
|
-- @param is_delta bool Whether the provided data table is of type delta or not. Used for validation.
|
|
-- @returns bool,string,table|string 1. Success bool (`true` == success)
|
|
-- 2. Error code. "SUCCESS" is passed if the operation was successful.
|
|
-- 3. Either the resulting Lua table if parsing was successful, or an error message as a string if not.
|
|
function weaschem.parse_data_table(source, is_delta)
|
|
local data_table = {}
|
|
local values = split(source, ",", true)
|
|
local i = 0
|
|
for _, next_val in pairs(values) do
|
|
if next_val:find("x") ~= nil then
|
|
local multi_count, multi_node_id = string.match(next_val, "^(%d+)x(-?[%d]+)$")
|
|
-- print("DEBUG:parse_data_table next_val", next_val, "multi_count", multi_count, "multi_node_id", multi_node_id)
|
|
|
|
if multi_count == nil or multi_node_id == nil then
|
|
return false, "DATA_TABLE_INVALID_VALUE", "Error: Encountered an invalid node id / count pair at position '"..tostring(i).."'."
|
|
end
|
|
-- These tonumber() calls are guaranteed to work since we only pass a number here
|
|
multi_count = tonumber(multi_count)
|
|
multi_node_id = tonumber(multi_node_id)
|
|
|
|
if is_delta then
|
|
if multi_node_id < -2 then
|
|
return false, "DATA_TABLE_INVALID_NODE_ID", "Error: When type=delta, then node ids must not be less then -2."
|
|
end
|
|
else
|
|
if multi_node_id < -1 then
|
|
return false, "DATA_TABLE_INVALID_NODE_ID", "Error: When type=delta, then node ids must not be less then -1."
|
|
end
|
|
end
|
|
|
|
for _ = 1,multi_count do
|
|
data_table[i] = multi_node_id
|
|
i = i + 1
|
|
end
|
|
else
|
|
local node_id = tonumber(next_val)
|
|
if type(node_id) ~= "number" then
|
|
return false, "DATA_TABLE_INVALID_NODE_ID",
|
|
"Encountered node id value in data table at position '" .. tostring(i) .. "'."
|
|
end
|
|
if is_delta then
|
|
if node_id < -2 then
|
|
return false, "DATA_TABLE_INVALID_NODE_ID",
|
|
"Error: When type=delta, then node ids must not be less then -2."
|
|
end
|
|
else
|
|
if node_id < -1 then
|
|
return false, "DATA_TABLE_INVALID_NODE_ID",
|
|
"Error: When type=delta, then node ids must not be less then -1."
|
|
end
|
|
end
|
|
|
|
data_table[i] = node_id
|
|
i = i + 1
|
|
end
|
|
end
|
|
|
|
return true, "SUCCESS", data_table
|
|
end
|
|
|
|
--- Parses the WorldEditAdditions schematic file from the given handle.
|
|
-- This requires a file handle, as significant optimisations can be made in the case of invalid files.
|
|
-- @param handle File A Lua file handle to read from.
|
|
-- @param [delta_which=both] string If the schematic file is of type delta (i.e. as opposed to full), then this indicates which state is desired. Useful to significantly optimise both CPU and memory usage by avoiding parsing more than necessary if only one state is desired. Possible values: both (default; read both the previous and current states), prev (read only the previous state), current (read only the current state).
|
|
-- @returns bool,string,table|string 1. Success bool (`true` == success)
|
|
-- 2. Error code. "SUCCESS" is passed if the operation was successful.
|
|
-- 3. Either the resulting Lua table if parsing was successful, or an error message as a string if not.
|
|
function weaschem.parse(handle, delta_which)
|
|
if delta_which == nil then delta_which = "both" end
|
|
if delta_which ~= "both" and delta_which ~= "prev" and delta_which ~= "current" then
|
|
return false, "INVALID_DELTA_WHICH", "Invalid delta_which argument. Possible values: both [default], prev, current."
|
|
end
|
|
|
|
local temp, temp2 = handle:read(8, 1)
|
|
if temp ~= "WEASCHEM" then
|
|
return false, "INVALID_MAGIC_BYTES", "The magic bytes 'WEASCHEM' were not found, so the input is an invalid .weaschem schematic."
|
|
end
|
|
if temp2 ~= " " then
|
|
return false, "INVALID_MAGIC_SPACE", "Invalid character after magic bytes (expected a single U+20 space)"
|
|
end
|
|
|
|
local version = handle:read("*n")
|
|
if version ~= 1 then
|
|
return false, "INVALID_VERSION", "Invalid schematic version. Expected 1, but found '"..tostring(version).."'."
|
|
end
|
|
|
|
temp = handle:read(1)
|
|
if temp ~= "\n" then
|
|
return false, "UNEXPECTED_TOKEN", "Invalid character present after schematic version number (expected a new line character U+0A)"
|
|
end
|
|
|
|
temp = handle:read("*l")
|
|
|
|
local success, code, header = weaschem.parse_header(temp)
|
|
if not success then return success, code, header end
|
|
|
|
temp = handle:read("*l")
|
|
if temp == nil then return false, "NO_ID_MAP", "Unable to read the id map from the file." end
|
|
local id_map
|
|
success, code, id_map = weaschem.parse_id_map(temp)
|
|
if not success then return success, code, id_map end
|
|
|
|
local data_tables = {}
|
|
|
|
if header["type"] == "full" then
|
|
temp = handle:read("*l")
|
|
if temp == nil then return false, "NO_DATA_TABLE", "Unable to read the data table full:data from the file." end
|
|
success, code, temp2 = weaschem.parse_data_table(temp, false)
|
|
if not success then return success, code, temp2 end
|
|
data_tables["data"] = temp2
|
|
|
|
temp = handle:read("*l")
|
|
if temp == nil then return false, "NO_DATA_TABLE", "Unable to read the data table full:param2 from the file." end
|
|
success, code, temp2 = weaschem.parse_data_table(temp, false)
|
|
if not success then return success, code, temp2 end
|
|
data_tables["param2"] = temp2
|
|
else
|
|
temp = handle:read("*l")
|
|
if temp == nil then return false, "NO_DATA_TABLE", "Unable to read the data table delta:data_previous from the file." end
|
|
-- delta_which="current" is the only state in which we would NOT want to parse the previous state. Regardless, we still need to read the line in though even if we immediately discard it otherwise we'll be out of sync.
|
|
if delta_which ~= "current" then
|
|
success, code, temp2 = weaschem.parse_data_table(temp, true)
|
|
if not success then return success, code, temp2 end
|
|
data_tables["data_prev"] = temp2
|
|
end
|
|
|
|
temp = handle:read("*l")
|
|
if temp == nil then return false, "NO_DATA_TABLE", "Unable to read the data table delta:param2_previous from the file." end
|
|
if delta_which ~= "current" then
|
|
success, code, temp2 = weaschem.parse_data_table(temp, true)
|
|
if not success then return success, code, temp2 end
|
|
data_tables["param2_prev"] = temp2
|
|
end
|
|
|
|
temp = handle:read("*l")
|
|
if temp == nil then return false, "NO_DATA_TABLE", "Unable to read the data table delta:data_current from the file." end
|
|
if delta_which ~= "prev" then
|
|
success, code, temp2 = weaschem.parse_data_table(temp, true)
|
|
if not success then return success, code, temp2 end
|
|
data_tables["data_current"] = temp2
|
|
end
|
|
|
|
temp = handle:read("*l")
|
|
if temp == nil then return false, "NO_DATA_TABLE", "Unable to read the data table delta:param2_current from the file." end
|
|
if delta_which ~= "prev" then
|
|
success, code, temp2 = weaschem.parse_data_table(temp, true)
|
|
if not success then return success, code, temp2 end
|
|
data_tables["param2_current"] = temp2
|
|
end
|
|
end
|
|
|
|
|
|
local result = {
|
|
header = header,
|
|
id_map = id_map,
|
|
data_tables = data_tables
|
|
}
|
|
|
|
return true, "SUCCESS", result
|
|
end
|
|
|
|
|
|
return weaschem |