diff --git a/DOCS.md b/DOCS.md index fb3b7db..34ae85e 100644 --- a/DOCS.md +++ b/DOCS.md @@ -1,17 +1,18 @@ # Magic Compass DOCS ### Create new locations -Every icon in the menu is a location. Locations must be declared in a .txt document inside the `locations` folder like so: +Every icon in the menu is a location. Locations must be declared in one or more .yml files inside the `locations` folder like so: +```yml +ID: # must be a number, don't write "ID" + description: Red Forest # the name you want to show in the menu when hovering the icon + icon: magiccompass_redforest.png # the associated texture + teleport_to: 90.5, 20.0, -30.5 # where the player will be teleported + cooldown: 5 # (optional) cooldown before being able to use it again. None by default + requires: fast # (optional) privileges required in order to use it. Use ", " to separate them or it won't work + hidden_by_default: true # (optional) whether to hide the icon to players who don't have the required privileges ``` -Red Forest -- the name you want to show in the menu when hovering the icon -magiccompass_redforest.png -- the associated texture --3.5, 5.0, -20.5 -- where the player will be teleported -5 -- (optional) cooldown before being able to use it again. Leave empty or put -1 for none -interact, myrpg_lv10 -- (optional) privileges required in order to use it. Use ", " to separate them or it won't work -HIDE -- (optional) whether to hide the icon to players who don't have the required privileges -``` -The file name is important too, as it must start with a number followed by an underscore like `5_whatever name.txt`. -The number indicates the position of the associated item in the grid (which scales according to the highest number declared), and empty spaces are generated automatically if the numbers of the items don't represent a full sequence. + +The ID indicates the position of the associated item in the grid (which scales according to the highest number declared), and empty spaces are generated automatically if the numbers of the items don't represent a full sequence. ### Callbacks If you want to run additional code from an external mod of yours, there a few callbacks coming in handy: diff --git a/locations/10_(this is a hidden one, hehe) b/locations/10_(this is a hidden one, hehe) deleted file mode 100644 index 09eedd6..0000000 --- a/locations/10_(this is a hidden one, hehe) +++ /dev/null @@ -1,6 +0,0 @@ -Do you see me? That's because you have the 'fast' privilege. People who don't, they simply can't! -magiccompass_example6.png -90.5, 20.0, -30.5 --1 -fast -HIDE diff --git a/locations/18_(and this can hide an entire section!) b/locations/18_(and this can hide an entire section!) deleted file mode 100644 index e919e22..0000000 --- a/locations/18_(and this can hide an entire section!) +++ /dev/null @@ -1,6 +0,0 @@ -And if you can't see ME (you need the password privilege), being the only one in the row, you won't see this row nor all the empty rows before it. THIS IS THE TRUE POWER OF ADMINS, CAN YOU FEEL IT?! -magiccompass_example6.png -150.5, 100.0, -90.5 --1 -password -HIDE diff --git a/locations/1_whatevername.txt b/locations/1_whatevername.txt deleted file mode 100644 index e473a47..0000000 --- a/locations/1_whatevername.txt +++ /dev/null @@ -1,3 +0,0 @@ -Check the Locations folder inside the mod to edit us! -magiccompass_example.png -10.5, 30.0, 20.5 diff --git a/locations/28_(finally, this would appear in the fourth row if you can't see #18 but you can see this - settime priv needed) b/locations/28_(finally, this would appear in the fourth row if you can't see #18 but you can see this - settime priv needed) deleted file mode 100644 index afe1dc3..0000000 --- a/locations/28_(finally, this would appear in the fourth row if you can't see #18 but you can see this - settime priv needed) +++ /dev/null @@ -1,6 +0,0 @@ -So as you can see we can create empty rows too, not declaring anything in a line and having something LIKE ME after -magiccompass_example7.png --180.5, 210.0, -20.5 --1 -settime -HIDE diff --git a/locations/3_seriously_wh4t3v3r.txt b/locations/3_seriously_wh4t3v3r.txt deleted file mode 100644 index b2bea3e..0000000 --- a/locations/3_seriously_wh4t3v3r.txt +++ /dev/null @@ -1,4 +0,0 @@ -Every single icon can have a cooldown. Like me, you can spam-click how much you want, I dare you -magiccompass_example2.png --3.5, 5.0, -20.5 -10 diff --git a/locations/5_just keep the number (WITH THE UNDERSCORE) to declare an order.txt b/locations/5_just keep the number (WITH THE UNDERSCORE) to declare an order.txt deleted file mode 100644 index b21c09e..0000000 --- a/locations/5_just keep the number (WITH THE UNDERSCORE) to declare an order.txt +++ /dev/null @@ -1,3 +0,0 @@ -And now some cute animals in Italian to please you with a search on the internet: basettino, pangolino, coniglio ariete. Don't thank me -magiccompass_example3.png -60.5, 70.0, 10.5 diff --git a/locations/7_the compass window also automatically resizes itself according to the max number declared in these files b/locations/7_the compass window also automatically resizes itself according to the max number declared in these files deleted file mode 100644 index 90cf891..0000000 --- a/locations/7_the compass window also automatically resizes itself according to the max number declared in these files +++ /dev/null @@ -1,3 +0,0 @@ -Also remember to change the textures in Textures -magiccompass_example4.png -55.5, 30.0, -40.5 diff --git a/locations/9_WACK b/locations/9_WACK deleted file mode 100644 index abe72f6..0000000 --- a/locations/9_WACK +++ /dev/null @@ -1,5 +0,0 @@ -And how about privileges? We have those too. Like this, which doesn't work without the 'fly' and 'shout' privilege -magiccompass_example5.png -40.5, 40.0, 1.5 --1 -fly, shout diff --git a/locations/locations.yml b/locations/locations.yml new file mode 100644 index 0000000..2aae18d --- /dev/null +++ b/locations/locations.yml @@ -0,0 +1,51 @@ +1: + description: Check the Locations folder inside the mod to edit us! + icon: magiccompass_example.png + teleport_to: 10.5, 30.0, 20.5 + +3: + description: Every single icon can have a cooldown. Like me, you can spam-click how much you want, I dare you + icon: magiccompass_example2.png + teleport_to: -3.5, 5.0, -20.5 + cooldown: 10 + +5: + description: And now some cute animals in Italian to please you with a search on the internet: basettino, pangolino, coniglio ariete. Don't thank me + icon: magiccompass_example3.png + teleport_to: 60.5, 70.0, 10.5 + +7: + description: Also remember to change the textures in Textures + icon: magiccompass_example4.png + teleport_to: 55.5, 30.0, -40.5 + +9: + description: And how about privileges? We have those too. Like this, which doesn't work without the 'fly' and 'shout' privilege + icon: magiccompass_example5.png + teleport_to: 40.5, 40.0, 1.5 + requires: fly, shout + +10: + description: Do you see me? That's because you have the 'fast' privilege. People who don't, they simply can't! + icon: magiccompass_example6.png + teleport_to: 90.5, 20.0, -30.5 + requires: fast + hidden_by_default: true + +# ------------ # +# admins only +# ------------ # + +18: + description: And if you can't see ME (you need the password privilege), being the only one in the row, you won't see this row nor all the empty rows before it. THIS IS THE TRUE POWER OF ADMINS, CAN YOU FEEL IT?! + icon: magiccompass_example6.png + teleport_to: 150.5, 100.0, -90.5 + requires: password + hidden_by_default: true + +28: + description: So as you can see we can create empty rows too, like the one before mine + icon: magiccompass_example7.png + teleport_to: -180.5, 210.0, -20.5 + requires: settime + hidden_by_default: true diff --git a/src/deserializer.lua b/src/deserializer.lua index b12e406..747828e 100644 --- a/src/deserializer.lua +++ b/src/deserializer.lua @@ -1,47 +1,40 @@ magic_compass.items = {} +local yaml = dofile(minetest.get_modpath("magic_compass") .. "/src/yaml_parser.lua") local S = minetest.get_translator("magic_compass") -local locations_dir = minetest.get_modpath("magic_compass") .. "/locations" -local locations_content = minetest.get_dir_list(locations_dir) -for _, file_name in pairs(locations_content) do - -- estrapolo le info dal file - local file = io.open(locations_dir .. "/" .. file_name, "r") - local data = string.split(file:read("*all"), "\n") - file:close() +local function load_locations() - local i_ID = string.match(file_name, "(%d+)_") - local i_desc = data[1] - local i_texture = data[2] - local i_pos = data[3] - local i_cooldown - local i_privileges - local i_hide + local dir = minetest.get_modpath("magic_compass") .. "/locations" + local content = minetest.get_dir_list(dir) - if data[4] and tonumber(data[4]) ~= -1 then - i_cooldown = tonumber(data[4]) + for _, f_name in pairs(content) do + if f_name:sub(-4) == ".yml" or f_name:sub(-5) == ".yaml" then + local file = io.open(dir .. "/" .. f_name, "r") + local locs = yaml.parse(file:read("*all")) + + for ID, loc in pairs(locs) do + + assert(type(ID) == "number", "[MAGIC_COMPASS] Invalid location ID '" .. ID .. "': numbers only!") + assert(loc.description, "[MAGIC_COMPASS] Location #" .. ID .. " has no description!") + assert(loc.icon, "[MAGIC_COMPASS] Location #" .. ID .. " has no icon!") + assert(loc.teleport_to, "[MAGIC_COMPASS] Location #" .. ID .. " has no teleport coordinates!") + + minetest.register_tool("magic_compass:" .. ID, { + description = S(loc.description), + inventory_image = loc.icon, + groups = {not_in_creative_inventory = 1, oddly_breakable_by_hand = 2} + }) + + magic_compass.items[ID] = {desc = loc.description, pos = loc.teleport_to, cooldown = loc.cooldown, privs = loc.requires, hide = loc.hidden_by_default} + end + + file:close() + end end - - if data[5] then - i_privileges = data[5] - end - - if data[6] and data[6] == "HIDE" then - i_hide = true - end - - -- creo l'oggetto - minetest.register_tool("magic_compass:" .. i_ID, { - - description = S(i_desc), - inventory_image = i_texture, - groups = {not_in_creative_inventory = 1, oddly_breakable_by_hand = 2} - - }) - - magic_compass.items[tonumber(i_ID)] = {desc = i_desc, pos = i_pos, cooldown = i_cooldown, privs = i_privileges, hide = i_hide} - end + +load_locations() diff --git a/src/formspec.lua b/src/formspec.lua index 89cfab5..40b455d 100644 --- a/src/formspec.lua +++ b/src/formspec.lua @@ -60,8 +60,6 @@ function magic_compass.get_formspec(p_name) last_pointed_row = i end - - end local shown_rows = rows - hidden_rows diff --git a/src/yaml_parser.lua b/src/yaml_parser.lua new file mode 100644 index 0000000..dd306cc --- /dev/null +++ b/src/yaml_parser.lua @@ -0,0 +1,475 @@ +-- YAMLParserLite = class("YAMLParserLite") + +-- function YAMLParserLite:initialize() +-- end + +-- function YAMLParserLite:parse(yaml) +-- local lines = {} +-- for line in string.gmatch(yaml..'\n', '(.-)\n') do +-- table.insert(lines, line) +-- end + +-- local docs = parse_documents(lines) +-- if #docs == 1 then +-- return docs[1] +-- end +-- return docs +-- end + +-- 以上是为了 配合个人已有结构 + +local schar = string.char +local ssub, gsub = string.sub, string.gsub +local sfind, smatch = string.find, string.match +local tinsert, tremove = table.insert, table.remove + +local UNESCAPES = { + ['0'] = "\x00", z = "\x00", N = "\x85", + a = "\x07", b = "\x08", t = "\x09", + n = "\x0a", v = "\x0b", f = "\x0c", + r = "\x0d", e = "\x1b", ['\\'] = '\\', +} + +-- help function +local function select(list, pred) + local selected = {} + for i = 0, #list do + local v = list[i] + if v and pred(v, i) then + tinsert(selected, v) + end + end + return selected +end + +-- return: indent_count, left_string +local function count_indent(line) + local _, j = sfind(line, '^%s+') + if not j then + return 0, line + end + return j, ssub(line, j+1) +end + +local function trim(str) + return string.gsub(str, "^%s*(.-)%s*$", "%1") +end + +local function ltrim(str) + return smatch(str, "^%s*(.-)$") +end + +local function rtrim(str) + return smatch(str, "^(.-)%s*$") +end + +local function isemptyline(line) + return line == '' or sfind(line, '^%s*$') or sfind(line, '^%s*#') +end + +local function startswith(haystack, needle) + return ssub(haystack, 1, #needle) == needle +end + +local function startswithline(line, needle) + return startswith(line, needle) and isemptyline(ssub(line, #needle+1)) +end + +-- class +local class = {__meta={}} +function class.__meta.__call(cls, ...) + local self = setmetatable({}, cls) + if cls.__init then + cls.__init(self, ...) + end + return self +end + +function class.def(base, type, cls) + base = base or class + local mt = {__metatable=base, __index=base} + for k, v in pairs(base.__meta) do mt[k] = v end + cls = setmetatable(cls or {}, mt) + cls.__index = cls + cls.__metatable = cls + cls.__type = type + cls.__meta = mt + return cls +end + +local types = { + null = class:def('null'), + map = class:def('map'), + seq = class:def('seq'), +} + +local Null = types.null +function Null.__tostring() return 'yaml.null' end +function Null.isnull(v) + if v == nil then return true end + if type(v) == 'table' and getmetatable(v) == Null then return true end + return false +end +local null = Null() + +-- implement function +local function parse_string(line, stopper) + + stopper = stopper or '' + local q = ssub(line, 1, 1) + if q == ' ' or q == '\t' then + return parse_string(ssub(line, 2)) + end + + if q == "'" then + local i = sfind(line, "'", 2, true) + if not i then + return nil, line + end + return ssub(line, 2, i-1), ssub(line, i+1) + end + + if q == '"' then + local i, buf = 2, '' + while i < #line do + local c = ssub(line, i, i) + if c == '\\' then + local n = ssub(line, i+1, i+1) + if UNESCAPES[n] ~= nil then + buf = buf..UNESCAPES[n] + elseif n == 'x' then + local h = ssub(i+2,i+3) + if sfind(h, '^[0-9a-fA-F]$') then + buf = buf..schar(tonumber(h, 16)) + i = i + 2 + else + buf = buf..'x' + end + else + buf = buf..n + end + i = i + 1 + elseif c == q then + break + else + buf = buf..c + end + i = i + 1 + end + return buf, ssub(line, i+1) + end + + if q == '-' or q == ':' then + if ssub(line, 2, 2) == ' ' or #line == 1 then + return nil, line + end + end + + local buf = '' + while #line > 0 do + local c = ssub(line, 1, 1) + if sfind(stopper, c, 1, true) then + break + elseif c == ':' and (ssub(line, 2, 2) == ' ' or #line == 1) then + break + elseif c == '#' and (ssub(buf, #buf, #buf) == ' ') then + break + else + buf = buf..c + end + line = ssub(line, 2) + end + return rtrim(buf), line +end + +local function parse_flowstyle(line, lines) + local stack = {} + while true do + if #line == 0 then + if #lines == 0 then + break + else + line = tremove(lines, 1) + end + end + local c = ssub(line, 1, 1) + if c == '#' then + line = '' + elseif c == ' ' or c == '\t' or c == '\r' or c == '\n' then + line = ssub(line, 2) + elseif c == '{' or c == '[' then + tinsert(stack, {v={},t=c}) + line = ssub(line, 2) + elseif c == ':' then + local s = tremove(stack) + tinsert(stack, {v=s.v, t=':'}) + line = ssub(line, 2) + elseif c == ',' then + local value = tremove(stack) + if value.t == ':' or value.t == '{' or value.t == '[' then error() end + if stack[#stack].t == ':' then + -- map + local key = tremove(stack) + stack[#stack].v[key.v] = value.v + elseif stack[#stack].t == '{' then + -- set + stack[#stack].v[value.v] = true + elseif stack[#stack].t == '[' then + -- seq + tinsert(stack[#stack].v, value.v) + end + line = ssub(line, 2) + elseif c == '}' then + if stack[#stack].t == '{' then + if #stack == 1 then break end + stack[#stack].t = '}' + line = ssub(line, 2) + else + line = ','..line + end + elseif c == ']' then + if stack[#stack].t == '[' then + if #stack == 1 then break end + stack[#stack].t = ']' + line = ssub(line, 2) + else + line = ','..line + end + else + local s, rest = parse_string(line, ',{}[]') + if not s then + error('invalid flowstyle line: '..line) + end + tinsert(stack, {v=s, t='s'}) + line = rest + end + end + return stack[1].v, line +end + +local function parse_scalar(line, lines) + + line = ltrim(line) + line = gsub(line, '%s*#.*$', '') + + if line == '' or line == '~' then + return null + end + + if startswith(line, '{') or startswith(line, '[') then + return parse_flowstyle(line, lines) + end + + local s, _ = parse_string(line) + if s and s ~= line then + return s + end + + -- Special cases + if sfind('\'"!$', ssub(line, 1, 1), 1, true) then + error('unsupported line: '..line) + end + + if startswithline(line, '{}') then + return {} + end + if startswithline(line, '[]') then + return {} + end + + -- Regular unquoted string + local v = line + if v == 'null' or v == 'Null' or v == 'NULL'then + return null + elseif v == 'true' or v == 'True' or v == 'TRUE' then + return true + elseif v == 'false' or v == 'False' or v == 'FALSE' then + return false + elseif v == '.inf' or v == '.Inf' or v == '.INF' then + return math.huge + elseif v == '+.inf' or v == '+.Inf' or v == '+.INF' then + return math.huge + elseif v == '-.inf' or v == '-.Inf' or v == '-.INF' then + return -math.huge + elseif v == '.nan' or v == '.NaN' or v == '.NAN' then + return 0 / 0 + elseif sfind(v, '^[%+%-]?[0-9]+$') or sfind(v, '^[%+%-]?[0-9]+%.$')then + return tonumber(v) + elseif sfind(v, '^[%+%-]?[0-9]+%.[0-9]+$') then + return tonumber(v) + end + return v +end + +local parse_map + +local function parse_seq(line, lines, indent) + local seq = setmetatable({}, types.seq) + if line ~= '' then + error() + end + while #lines > 0 do + line = lines[1] + + local level = count_indent(line) + if level < indent and indent ~= -1 then + return seq + elseif level > indent and indent ~= -1 then + error("found bad indenting in line: ".. line) + end + + local i, j = sfind(line, '%-%s+') + if not i then + i, j = sfind(line, '%-$') + if not i then + return seq + end + end + local rest = ssub(line, j+1) + + if sfind(rest, '^[^\'\"%s]*:') then + local indent2 = j + lines[1] = string.rep(' ', indent2)..rest + tinsert(seq, parse_map('', lines, indent2)) + elseif isemptyline(rest) then + tremove(lines, 1) + if #lines == 0 then + tinsert(seq, null) + return seq + end + if sfind(lines[1], '^%s*%-') then + local nextline = lines[1] + local indent2 = count_indent(nextline) + if indent2 == indent then + tinsert(seq, null) + else + tinsert(seq, parse_seq('', lines, indent2)) + end + else + local nextline = lines[1] + local indent2 = count_indent(nextline) + tinsert(seq, parse_map('', lines, indent2)) + end + elseif rest then + tremove(lines, 1) + local tmp = parse_scalar(rest, lines) + tinsert(seq, tmp) + end + end + return seq +end + +function parse_map(line, lines, indent) + if not isemptyline(line) then + error('not map line: '..line) + end + local map = setmetatable({}, types.map) + while #lines > 0 do + line = lines[1] + + local level, _ = count_indent(line) + if level < indent then + return map + elseif level > indent then + error("found bad indenting in line: ".. line) + end + + local key + local s, rest = parse_string(line) + if s and startswith(rest, ':') then + local sc = parse_scalar(s, {}) + if sc and type(sc) ~= 'string' then + key = sc + else + key = s + end + line = ssub(rest, 2) + else + error("failed to classify line: "..line) + end + + if map[key] ~= nil then + print("found a duplicate key '"..key.."' in line: "..line) + local suffix = 1 + while map[key..'__'..suffix] do + suffix = suffix + 1 + end + key = key ..'_'..suffix + end + + line = ltrim(line) + + if not isemptyline(line) then + tremove(lines, 1) + line = ltrim(line) + map[key] = parse_scalar(line, lines) + else + tremove(lines, 1) + if #lines == 0 then + map[key] = null + return map; + end + if sfind(lines[1], '^%s*%-') then + local indent2 = count_indent(lines[1]) + map[key] = parse_seq('', lines, indent2) + else + local indent2 = count_indent(lines[1]) + if indent >= indent2 then + map[key] = null + else + map[key] = parse_map('', lines, indent2) + end + end + end + end + return map +end + +local function parse_documents(lines) + lines = select(lines, function(s) return not isemptyline(s) end) + + if #lines == 1 and not sfind(lines[1], '^%s*%-') then + local line = lines[1] + line = ltrim(line) + return parse_scalar(line, lines) + end + + local root = {} + while #lines > 0 do + local line = lines[1] + if sfind(line, '^%s*%-') then + tinsert(root, parse_seq('', lines, -1)) + elseif sfind(line, '^%s*[^%s]') then + local level = count_indent(line) + tinsert(root, parse_map('', lines, level)) + else + error('parse error: '..line) + end + end + + if #root > 1 and Null.isnull(root[1]) then + tremove(root, 1) + return root + end + + return root + +end + +local function parse(yaml) + local lines = {} + for line in string.gmatch(yaml..'\n', '(.-)\n') do + table.insert(lines, line) + end + + local docs = parse_documents(lines) + if #docs == 1 then + return docs[1] + end + return docs +end + +return { + null = null, + parse = parse, +}