commit 81c12b7867cffd3ac94c76bc23da14077c8e21f2 Author: Tangent Date: Mon Dec 22 18:22:05 2025 -0700 initial area a day late to commit .. or 2 ? but whatever diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/ReadMe.md b/ReadMe.md new file mode 100644 index 0000000..7cf91e4 --- /dev/null +++ b/ReadMe.md @@ -0,0 +1,60 @@ +# Immortality-Bae-travelmap +Immortality-Bae is a private family server. + +## travelmap +I made a little program for displaying a simplistic graph of nodes and edges to +make a travel network with calculated distances... specifically for my Minecraft +World. + +Data must be in a map.json file. `nodes` is an object with named arrays of info. +Each node is an `x`, `y`, and `z` position. The optional 4th item is `true` to +display the node's name, a string to display *that* as the node name, or `false` +to completely hide a node. `edges` is an array of arrays of two names of nodes. +An optional 3rd item can be set to `false` to hide an edge, or a string for a +custom label. `edges` can be nonexistent. + +Pan with WASD or arrow keys. +Zoom with +/- keys. +`r` to reload the default map. + +You can drag and drop a JSON file to load it. + +Both nodes and edges can have an extra object with metadata, some of which is +used by this program. An array called `offset` will adjust the positioning of +labels. An array called `color` will set the color via 0 to 1 RGBA values. + +Added `areas` which are exactly like nodes except that they always draw a white +label and can have a custom radius specified. Drawn between edges and nodes. +Use transparency. 25% works well. +- `box` can be an array of width/height to draw a rectangular shape instead +- `radius` can be an array of x/z radii to draw an ellipse instead of a circle +- `mode` can be "line" to draw an outline instead of filling +- `image` can be a file path, in which case `size` must be an array of desired + width/height + - (path relative to the JSON file? or relative to the executable?) + +Images are only loaded if they will be displayed. + +## Example JSON + +```json +{ + "nodes": { + "TestA": [100, 0, 100], + "TestB": [150, 0, 200], + "TestC": [300, 0, -200, true], + "TestE": [1200, 0, 200, "Displayed Name"], + "TestFarLeft": [-200, 5000, 50], + "TestFarBottomRight": [1200, 0, 2000, true], + "Hidden Node": [0, 0, 0, false] + }, + "edges": [ + ["TestA", "TestB"], + ["TestA", "TestC"], + ["TestB", "TestC"], + ["TestC", "TestE"], + ["TestE", "TestFarLeft"], + ["Hidden Node", "TestC", false] + ] +} +``` diff --git a/Spawn Area.json b/Spawn Area.json new file mode 100644 index 0000000..46b9143 --- /dev/null +++ b/Spawn Area.json @@ -0,0 +1,74 @@ +{ + "nodes": { + "zombie dungeon": [116, -29, -32, true], + "Rail Factory": [30, 66, -14, true, {"offset": [1, -1]}], + "di'Luna's original boat": [26, 64, -36, true], + "original mine entrance": [16, 63, -13, true, {"offset": [-103, -10]}], + "Ky's houseboat": [-25, 63, -30, true], + "path origin": [16, 63, -18, false], + "factory turn": [28, 64, -19, false], + "b-side di'Luna's underground base": [44, 62, -51, false], + "di'Luna's underground base": [52, 58, -51, true], + "path beside supersmelter": [41, 63, -92, false], + "supersmelter": [61, 59, -92, true], + "back entrance to Battle of Day 194 Monument": [7, 62, -142, false], + "Battle of Day 194 Monument": [-4, 62, -133, true], + "front entrance to Battle of Day 194 Monument": [-17, 62, -123, false], + "non-dock to Battle of Day 194 Monument": [-12, 62, -110, false], + "tiny waterside farm": [-43, 62, -99, true], + "library path split": [-87, 65, -91, false], + "Ky's iron vein mine": [-75, 63, -64, true], + "Bookcase Library": [-97, 65, -91, true], + "first path split": [22, 64, -17, false], + "Birdie Bridge path split": [-14, 69, 17, false], + "original supersmelter": [-11, 66, 37, true, {"offset": [-90, 0]}], + "sugarcane farm": [14, 68, 8, true], + "iron farm": [21, 68, 47, true], + "mob farm (AFK spot)": [62, 183, 43, "mob farm"], + "Tangent's farm area": [-90, 64, 18, true], + "Tangent's iron vein mine (surface)": [-88, 69, 88, "Tangent's iron vein mine"], + "Tangent's original chicken coop": [104, 70, 236, true], + "coop path deadend": [52, 63, 123, false], + "east rail origin": [40, 66, -22, true], + "east rail deadend": [1707, 66, 31, false], + "Ky's 2nd iron vein mine": [-102, -23, -43, true], + "nether portal": [-131, 65, -35, true], + "Ky's 3rd iron vein mine": [-160, 70, -160, true], + "Staircase of Moria": [-33, 65, 64, true, {"offset": [-7, 2]}], + "Slimeless Barrens": [-304, -58, 64, true], + "end of original mineshaft": [15, -61, 112, "bottom of original mine"], + "trial chamber entrance": [-15, 63, 4, true, {"offset": [-133, -18]}], + "trial chamber (top)": [70, -24, 4, "trial chamber", {"offset": [40, -1]}] + }, + "edges": [ + ["original mine entrance", "path origin", ""], + ["path origin", "first path split", ""], + ["first path split", "factory turn", ""], + ["factory turn", "Rail Factory", ""], + ["factory turn", "b-side di'Luna's underground base"], + ["b-side di'Luna's underground base", "di'Luna's underground base"], + ["b-side di'Luna's underground base", "path beside supersmelter"], + ["path beside supersmelter", "supersmelter"], + ["path beside supersmelter", "back entrance to Battle of Day 194 Monument"], + ["front entrance to Battle of Day 194 Monument", "back entrance to Battle of Day 194 Monument", null, {"offset": [-10, -2]}], + ["front entrance to Battle of Day 194 Monument", "non-dock to Battle of Day 194 Monument", ""], + ["front entrance to Battle of Day 194 Monument", "tiny waterside farm", null, {"offset": [-10, 0]}], + ["tiny waterside farm", "library path split"], + ["library path split", "Ky's iron vein mine"], + ["library path split", "Bookcase Library"], + ["first path split", "Birdie Bridge path split", true, {"offset": [-16, 3]}], + ["original supersmelter", "Birdie Bridge path split"], + ["Tangent's farm area", "Birdie Bridge path split"], + ["Tangent's farm area", "Tangent's iron vein mine (surface)"], + ["Tangent's original chicken coop", "coop path deadend"], + ["east rail origin", "east rail deadend", false], + ["Staircase of Moria", "Slimeless Barrens"], + ["original mine entrance", "end of original mineshaft", true, {"offset": [1, 50]}], + ["Ky's iron vein mine", "Ky's 2nd iron vein mine"], + ["trial chamber entrance", "trial chamber (top)", true, {"offset": [40, -1]}] + ], + "areas": { + "spawn village": [44, 67, 55, true, {"radius": 54, "offset": [25, 50]}], + "Tangent's farm area": [-90, 64, 18, null, {"radius": 22}] + } +} diff --git a/Spawn Area.png b/Spawn Area.png new file mode 100644 index 0000000..0ef2bb3 Binary files /dev/null and b/Spawn Area.png differ diff --git a/map.json b/map.json new file mode 100644 index 0000000..1dc29f2 --- /dev/null +++ b/map.json @@ -0,0 +1,78 @@ +{ + "nodes": { + "zombie dungeon": [116, -29, -32, true], + "Rail Factory": [30, 66, -14, true, {"offset": [1, -1]}], + "di'Luna's original boat": [26, 64, -36, true], + "original mine entrance": [16, 63, -13, true, {"offset": [-103, -10]}], + "Ky's houseboat": [-25, 63, -30, true], + "path origin": [16, 63, -18, false], + "factory turn": [28, 64, -19, false], + "b-side di'Luna's underground base": [44, 62, -51, false], + "di'Luna's underground base": [52, 58, -51, true], + "path beside supersmelter": [41, 63, -92, false], + "supersmelter": [61, 59, -92, true], + "back entrance to Battle of Day 194 Monument": [7, 62, -142, false], + "Battle of Day 194 Monument": [-4, 62, -133, true], + "front entrance to Battle of Day 194 Monument": [-17, 62, -123, false], + "non-dock to Battle of Day 194 Monument": [-12, 62, -110, false], + "tiny waterside farm": [-43, 62, -99, true], + "library path split": [-87, 65, -91, false], + "Ky's iron vein mine": [-75, 63, -64, true], + "Bookcase Library": [-97, 65, -91, true], + "first path split": [22, 64, -17, false], + "Birdie Bridge path split": [-14, 69, 17, false], + "original supersmelter": [-11, 66, 37, true, {"offset": [-90, 0]}], + "sugarcane farm": [14, 68, 8, true], + "iron farm": [21, 68, 47, true], + "mob farm (AFK spot)": [62, 183, 43, "mob farm"], + "Tangent's farm area": [-90, 64, 18, true], + "Tangent's iron vein mine (surface)": [-88, 69, 88, "Tangent's iron vein mine"], + "Tangent's original chicken coop": [104, 70, 236, true], + "coop path deadend": [52, 63, 123, false], + "east rail origin": [40, 66, -22, true], + "east rail deadend": [1707, 66, 31, true], + "Ky's 2nd iron vein mine": [-102, -23, -43, true], + "nether portal": [-131, 65, -35, true], + "Ky's 3rd iron vein mine": [-160, 70, -160, true], + "Staircase of Moria": [-33, 65, 64, true, {"offset": [-7, 2]}], + "Slimeless Barrens": [-304, -58, 64, true], + "end of original mineshaft": [15, -61, 112, "bottom of original mine"], + "trial chamber entrance": [-15, 63, 4, true, {"offset": [-133, -18]}], + "trial chamber (top)": [70, -24, 4, "trial chamber", {"offset": [40, -1]}], + "ruined nether portal": [867, 66, 2, true], + "east rail ice point": [1067, 64, -1, true], + "east rail mountain point": [1651, 66, -1, false] + }, + "edges": [ + ["original mine entrance", "path origin", ""], + ["path origin", "first path split", ""], + ["first path split", "factory turn", ""], + ["factory turn", "Rail Factory", ""], + ["factory turn", "b-side di'Luna's underground base"], + ["b-side di'Luna's underground base", "di'Luna's underground base"], + ["b-side di'Luna's underground base", "path beside supersmelter"], + ["path beside supersmelter", "supersmelter"], + ["path beside supersmelter", "back entrance to Battle of Day 194 Monument"], + ["front entrance to Battle of Day 194 Monument", "back entrance to Battle of Day 194 Monument", null, {"offset": [-10, -2]}], + ["front entrance to Battle of Day 194 Monument", "non-dock to Battle of Day 194 Monument", ""], + ["front entrance to Battle of Day 194 Monument", "tiny waterside farm", null, {"offset": [-10, 0]}], + ["tiny waterside farm", "library path split"], + ["library path split", "Ky's iron vein mine"], + ["library path split", "Bookcase Library"], + ["first path split", "Birdie Bridge path split", true, {"offset": [-16, 3]}], + ["original supersmelter", "Birdie Bridge path split"], + ["Tangent's farm area", "Birdie Bridge path split"], + ["Tangent's farm area", "Tangent's iron vein mine (surface)"], + ["Tangent's original chicken coop", "coop path deadend"], + ["east rail origin", "east rail mountain point", true, {"offset": [0, -12]}], + ["east rail mountain point", "east rail deadend"], + ["Staircase of Moria", "Slimeless Barrens"], + ["original mine entrance", "end of original mineshaft", true, {"offset": [1, 50]}], + ["Ky's iron vein mine", "Ky's 2nd iron vein mine"], + ["trial chamber entrance", "trial chamber (top)", true, {"offset": [40, -1]}] + ], + "areas": { + "spawn village": [44, 67, 55, true, {"radius": 54, "offset": [25, 50]}], + "Tangent's farm area": [-90, 64, 18, null, {"radius": 22}] + } +} diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..6ec5e02 --- /dev/null +++ b/run.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env zsh +source ~/.zshrc # why is this having to be done manually? +cd "$(dirname "$0")" + +love src diff --git a/src/lib/dkjson.lua b/src/lib/dkjson.lua new file mode 100644 index 0000000..3bfbec2 --- /dev/null +++ b/src/lib/dkjson.lua @@ -0,0 +1,752 @@ +-- Module options: +local always_use_lpeg = false +local register_global_module_table = false +local global_module_name = 'json' + +--[==[ + +David Kolf's JSON module for Lua 5.1 - 5.4 + +Version 2.8 + + +For the documentation see the corresponding readme.txt or visit +. + +You can contact the author by sending an e-mail to 'david' at the +domain 'dkolf.de'. + + +Copyright (C) 2010-2024 David Heiko Kolf + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--]==] + +-- global dependencies: +local pairs, type, tostring, tonumber, getmetatable, setmetatable = + pairs, type, tostring, tonumber, getmetatable, setmetatable +local error, require, pcall, select = error, require, pcall, select +local floor, huge = math.floor, math.huge +local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat = + string.rep, string.gsub, string.sub, string.byte, string.char, + string.find, string.len, string.format +local strmatch = string.match +local concat = table.concat + +local json = { version = "dkjson 2.8" } + +local jsonlpeg = {} + +if register_global_module_table then + if always_use_lpeg then + _G[global_module_name] = jsonlpeg + else + _G[global_module_name] = json + end +end + +local _ENV = nil -- blocking globals in Lua 5.2 and later + +pcall (function() + -- Enable access to blocked metatables. + -- Don't worry, this module doesn't change anything in them. + local debmeta = require "debug".getmetatable + if debmeta then getmetatable = debmeta end +end) + +json.null = setmetatable ({}, { + __tojson = function () return "null" end +}) + +local function isarray (tbl) + local max, n, arraylen = 0, 0, 0 + for k,v in pairs (tbl) do + if k == 'n' and type(v) == 'number' then + arraylen = v + if v > max then + max = v + end + else + if type(k) ~= 'number' or k < 1 or floor(k) ~= k then + return false + end + if k > max then + max = k + end + n = n + 1 + end + end + if max > 10 and max > arraylen and max > n * 2 then + return false -- don't create an array with too many holes + end + return true, max +end + +local escapecodes = { + ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", + ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t" +} + +local function escapeutf8 (uchar) + local value = escapecodes[uchar] + if value then + return value + end + local a, b, c, d = strbyte (uchar, 1, 4) + a, b, c, d = a or 0, b or 0, c or 0, d or 0 + if a <= 0x7f then + value = a + elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then + value = (a - 0xc0) * 0x40 + b - 0x80 + elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then + value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80 + elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then + value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80 + else + return "" + end + if value <= 0xffff then + return strformat ("\\u%.4x", value) + elseif value <= 0x10ffff then + -- encode as UTF-16 surrogate pair + value = value - 0x10000 + local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400) + return strformat ("\\u%.4x\\u%.4x", highsur, lowsur) + else + return "" + end +end + +local function fsub (str, pattern, repl) + -- gsub always builds a new string in a buffer, even when no match + -- exists. First using find should be more efficient when most strings + -- don't contain the pattern. + if strfind (str, pattern) then + return gsub (str, pattern, repl) + else + return str + end +end + +local function quotestring (value) + -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js + value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8) + if strfind (value, "[\194\216\220\225\226\239]") then + value = fsub (value, "\194[\128-\159\173]", escapeutf8) + value = fsub (value, "\216[\128-\132]", escapeutf8) + value = fsub (value, "\220\143", escapeutf8) + value = fsub (value, "\225\158[\180\181]", escapeutf8) + value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8) + value = fsub (value, "\226\129[\160-\175]", escapeutf8) + value = fsub (value, "\239\187\191", escapeutf8) + value = fsub (value, "\239\191[\176-\191]", escapeutf8) + end + return "\"" .. value .. "\"" +end +json.quotestring = quotestring + +local function replace(str, o, n) + local i, j = strfind (str, o, 1, true) + if i then + return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1) + else + return str + end +end + +-- locale independent num2str and str2num functions +local decpoint, numfilter + +local function updatedecpoint () + decpoint = strmatch(tostring(0.5), "([^05+])") + -- build a filter that can be used to remove group separators + numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+" +end + +updatedecpoint() + +local function num2str (num) + return replace(fsub(tostring(num), numfilter, ""), decpoint, ".") +end + +local function str2num (str) + local num = tonumber(replace(str, ".", decpoint)) + if not num then + updatedecpoint() + num = tonumber(replace(str, ".", decpoint)) + end + return num +end + +local function addnewline2 (level, buffer, buflen) + buffer[buflen+1] = "\n" + buffer[buflen+2] = strrep (" ", level) + buflen = buflen + 2 + return buflen +end + +function json.addnewline (state) + if state.indent then + state.bufferlen = addnewline2 (state.level or 0, + state.buffer, state.bufferlen or #(state.buffer)) + end +end + +local encode2 -- forward declaration + +local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state) + local kt = type (key) + if kt ~= 'string' and kt ~= 'number' then + return nil, "type '" .. kt .. "' is not supported as a key by JSON." + end + if prev then + buflen = buflen + 1 + buffer[buflen] = "," + end + if indent then + buflen = addnewline2 (level, buffer, buflen) + end + -- When Lua is compiled with LUA_NOCVTN2S this will fail when + -- numbers are mixed into the keys of the table. JSON keys are always + -- strings, so this would be an implicit conversion too and the failure + -- is intentional. + buffer[buflen+1] = quotestring (key) + buffer[buflen+2] = ":" + return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state) +end + +local function appendcustom(res, buffer, state) + local buflen = state.bufferlen + if type (res) == 'string' then + buflen = buflen + 1 + buffer[buflen] = res + end + return buflen +end + +local function exception(reason, value, state, buffer, buflen, defaultmessage) + defaultmessage = defaultmessage or reason + local handler = state.exception + if not handler then + return nil, defaultmessage + else + state.bufferlen = buflen + local ret, msg = handler (reason, value, state, defaultmessage) + if not ret then return nil, msg or defaultmessage end + return appendcustom(ret, buffer, state) + end +end + +function json.encodeexception(reason, value, state, defaultmessage) + return quotestring("<" .. defaultmessage .. ">") +end + +encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state) + local valtype = type (value) + local valmeta = getmetatable (value) + valmeta = type (valmeta) == 'table' and valmeta -- only tables + local valtojson = valmeta and valmeta.__tojson + if valtojson then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + state.bufferlen = buflen + local ret, msg = valtojson (value, state) + if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end + tables[value] = nil + buflen = appendcustom(ret, buffer, state) + elseif value == nil then + buflen = buflen + 1 + buffer[buflen] = "null" + elseif valtype == 'number' then + local s + if value ~= value or value >= huge or -value >= huge then + -- This is the behaviour of the original JSON implementation. + s = "null" + else + s = num2str (value) + end + buflen = buflen + 1 + buffer[buflen] = s + elseif valtype == 'boolean' then + buflen = buflen + 1 + buffer[buflen] = value and "true" or "false" + elseif valtype == 'string' then + buflen = buflen + 1 + buffer[buflen] = quotestring (value) + elseif valtype == 'table' then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + level = level + 1 + local isa, n = isarray (value) + if n == 0 and valmeta and valmeta.__jsontype == 'object' then + isa = false + end + local msg + if isa then -- JSON array + buflen = buflen + 1 + buffer[buflen] = "[" + for i = 1, n do + buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + if i < n then + buflen = buflen + 1 + buffer[buflen] = "," + end + end + buflen = buflen + 1 + buffer[buflen] = "]" + else -- JSON object + local prev = false + buflen = buflen + 1 + buffer[buflen] = "{" + local order = valmeta and valmeta.__jsonorder or globalorder + if order then + local used = {} + n = #order + for i = 1, n do + local k = order[i] + local v = value[k] + if v ~= nil then + used[k] = true + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + for k,v in pairs (value) do + if not used[k] then + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + else -- unordered + for k,v in pairs (value) do + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + if indent then + buflen = addnewline2 (level - 1, buffer, buflen) + end + buflen = buflen + 1 + buffer[buflen] = "}" + end + tables[value] = nil + else + return exception ('unsupported type', value, state, buffer, buflen, + "type '" .. valtype .. "' is not supported by JSON.") + end + return buflen +end + +function json.encode (value, state) + state = state or {} + local oldbuffer = state.buffer + local buffer = oldbuffer or {} + state.buffer = buffer + updatedecpoint() + local ret, msg = encode2 (value, state.indent, state.level or 0, + buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state) + if not ret then + error (msg, 2) + elseif oldbuffer == buffer then + state.bufferlen = ret + return true + else + state.bufferlen = nil + state.buffer = nil + return concat (buffer) + end +end + +local function loc (str, where) + local line, pos, linepos = 1, 1, 0 + while true do + pos = strfind (str, "\n", pos, true) + if pos and pos < where then + line = line + 1 + linepos = pos + pos = pos + 1 + else + break + end + end + return strformat ("line %d, column %d", line, where - linepos) +end + +local function unterminated (str, what, where) + return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where) +end + +local function scanwhite (str, pos) + while true do + pos = strfind (str, "%S", pos) + if not pos then return nil end + local sub2 = strsub (str, pos, pos + 1) + if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then + -- UTF-8 Byte Order Mark + pos = pos + 3 + elseif sub2 == "//" then + pos = strfind (str, "[\n\r]", pos + 2) + if not pos then return nil end + elseif sub2 == "/*" then + pos = strfind (str, "*/", pos + 2) + if not pos then return nil end + pos = pos + 2 + else + return pos + end + end +end + +local escapechars = { + ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f", + ["n"] = "\n", ["r"] = "\r", ["t"] = "\t" +} + +local function unichar (value) + if value < 0 then + return nil + elseif value <= 0x007f then + return strchar (value) + elseif value <= 0x07ff then + return strchar (0xc0 + floor(value/0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0xffff then + return strchar (0xe0 + floor(value/0x1000), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0x10ffff then + return strchar (0xf0 + floor(value/0x40000), + 0x80 + (floor(value/0x1000) % 0x40), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + else + return nil + end +end + +local function scanstring (str, pos) + local lastpos = pos + 1 + local buffer, n = {}, 0 + while true do + local nextpos = strfind (str, "[\"\\]", lastpos) + if not nextpos then + return unterminated (str, "string", pos) + end + if nextpos > lastpos then + n = n + 1 + buffer[n] = strsub (str, lastpos, nextpos - 1) + end + if strsub (str, nextpos, nextpos) == "\"" then + lastpos = nextpos + 1 + break + else + local escchar = strsub (str, nextpos + 1, nextpos + 1) + local value + if escchar == "u" then + value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16) + if value then + local value2 + if 0xD800 <= value and value <= 0xDBff then + -- we have the high surrogate of UTF-16. Check if there is a + -- low surrogate escaped nearby to combine them. + if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then + value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16) + if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then + value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000 + else + value2 = nil -- in case it was out of range for a low surrogate + end + end + end + value = value and unichar (value) + if value then + if value2 then + lastpos = nextpos + 12 + else + lastpos = nextpos + 6 + end + end + end + end + if not value then + value = escapechars[escchar] or escchar + lastpos = nextpos + 2 + end + n = n + 1 + buffer[n] = value + end + end + if n == 1 then + return buffer[1], lastpos + elseif n > 1 then + return concat (buffer), lastpos + else + return "", lastpos + end +end + +local scanvalue -- forward declaration + +local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta) + local tbl, n = {}, 0 + local pos = startpos + 1 + if what == 'object' then + setmetatable (tbl, objectmeta) + else + setmetatable (tbl, arraymeta) + end + while true do + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + local char = strsub (str, pos, pos) + if char == closechar then + return tbl, pos + 1 + end + local val1, err + val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + if char == ":" then + if val1 == nil then + return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")" + end + pos = scanwhite (str, pos + 1) + if not pos then return unterminated (str, what, startpos) end + local val2 + val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + tbl[val1] = val2 + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + else + n = n + 1 + tbl[n] = val1 + end + if char == "," then + pos = pos + 1 + end + end +end + +scanvalue = function (str, pos, nullval, objectmeta, arraymeta) + pos = pos or 1 + pos = scanwhite (str, pos) + if not pos then + return nil, strlen (str) + 1, "no valid JSON value (reached the end)" + end + local char = strsub (str, pos, pos) + if char == "{" then + return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta) + elseif char == "[" then + return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta) + elseif char == "\"" then + return scanstring (str, pos) + else + local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos) + if pstart then + local number = str2num (strsub (str, pstart, pend)) + if number then + return number, pend + 1 + end + end + pstart, pend = strfind (str, "^%a%w*", pos) + if pstart then + local name = strsub (str, pstart, pend) + if name == "true" then + return true, pend + 1 + elseif name == "false" then + return false, pend + 1 + elseif name == "null" then + return nullval, pend + 1 + end + end + return nil, pos, "no valid JSON value at " .. loc (str, pos) + end +end + +local function optionalmetatables(...) + if select("#", ...) > 0 then + return ... + else + return {__jsontype = 'object'}, {__jsontype = 'array'} + end +end + +function json.decode (str, pos, nullval, ...) + local objectmeta, arraymeta = optionalmetatables(...) + return scanvalue (str, pos, nullval, objectmeta, arraymeta) +end + +function json.use_lpeg () + local g = require ("lpeg") + + if type(g.version) == 'function' and g.version() == "0.11" then + error "due to a bug in LPeg 0.11, it cannot be used for JSON matching" + end + + local pegmatch = g.match + local P, S, R = g.P, g.S, g.R + + local function ErrorCall (str, pos, msg, state) + if not state.msg then + state.msg = msg .. " at " .. loc (str, pos) + state.pos = pos + end + return false + end + + local function Err (msg) + return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall) + end + + local function ErrorUnterminatedCall (str, pos, what, state) + return ErrorCall (str, pos - 1, "unterminated " .. what, state) + end + + local SingleLineComment = P"//" * (1 - S"\n\r")^0 + local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/" + local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0 + + local function ErrUnterminated (what) + return g.Cmt (g.Cc (what) * g.Carg (2), ErrorUnterminatedCall) + end + + local PlainChar = 1 - S"\"\\\n\r" + local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars + local HexDigit = R("09", "af", "AF") + local function UTF16Surrogate (match, pos, high, low) + high, low = tonumber (high, 16), tonumber (low, 16) + if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then + return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000) + else + return false + end + end + local function UTF16BMP (hex) + return unichar (tonumber (hex, 16)) + end + local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit)) + local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP + local Char = UnicodeEscape + EscapeSequence + PlainChar + local String = P"\"" * (g.Cs (Char ^ 0) * P"\"" + ErrUnterminated "string") + local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0)) + local Fractal = P"." * R"09"^0 + local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1 + local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num + local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1) + local SimpleValue = Number + String + Constant + local ArrayContent, ObjectContent + + -- The functions parsearray and parseobject parse only a single value/pair + -- at a time and store them directly to avoid hitting the LPeg limits. + local function parsearray (str, pos, nullval, state) + local obj, cont + local start = pos + local npos + local t, nt = {}, 0 + repeat + obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state) + if cont == 'end' then + return ErrorUnterminatedCall (str, start, "array", state) + end + pos = npos + if cont == 'cont' or cont == 'last' then + nt = nt + 1 + t[nt] = obj + end + until cont ~= 'cont' + return pos, setmetatable (t, state.arraymeta) + end + + local function parseobject (str, pos, nullval, state) + local obj, key, cont + local start = pos + local npos + local t = {} + repeat + key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state) + if cont == 'end' then + return ErrorUnterminatedCall (str, start, "object", state) + end + pos = npos + if cont == 'cont' or cont == 'last' then + t[key] = obj + end + until cont ~= 'cont' + return pos, setmetatable (t, state.objectmeta) + end + + local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) + local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) + local Value = Space * (Array + Object + SimpleValue) + local ExpectedValue = Value + Space * Err "value expected" + local ExpectedKey = String + Err "key expected" + local End = P(-1) * g.Cc'end' + local ErrInvalid = Err "invalid JSON" + ArrayContent = (Value * Space * (P"," * g.Cc'cont' + P"]" * g.Cc'last'+ End + ErrInvalid) + g.Cc(nil) * (P"]" * g.Cc'empty' + End + ErrInvalid)) * g.Cp() + local Pair = g.Cg (Space * ExpectedKey * Space * (P":" + Err "colon expected") * ExpectedValue) + ObjectContent = (g.Cc(nil) * g.Cc(nil) * P"}" * g.Cc'empty' + End + (Pair * Space * (P"," * g.Cc'cont' + P"}" * g.Cc'last' + End + ErrInvalid) + ErrInvalid)) * g.Cp() + local DecodeValue = ExpectedValue * g.Cp () + + jsonlpeg.version = json.version + jsonlpeg.encode = json.encode + jsonlpeg.null = json.null + jsonlpeg.quotestring = json.quotestring + jsonlpeg.addnewline = json.addnewline + jsonlpeg.encodeexception = json.encodeexception + jsonlpeg.using_lpeg = true + + function jsonlpeg.decode (str, pos, nullval, ...) + local state = {} + state.objectmeta, state.arraymeta = optionalmetatables(...) + local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state) + if state.msg then + return nil, state.pos, state.msg + else + return obj, retpos + end + end + + -- cache result of this function: + json.use_lpeg = function () return jsonlpeg end + jsonlpeg.use_lpeg = json.use_lpeg + + return jsonlpeg +end + +if always_use_lpeg then + return json.use_lpeg() +end + +return json + diff --git a/src/main.lua b/src/main.lua new file mode 100644 index 0000000..a433404 --- /dev/null +++ b/src/main.lua @@ -0,0 +1,240 @@ +local json = require "lib.dkjson" + +love.window.setMode(1600, 900) +local screen_width, screen_height = love.graphics.getDimensions() +local node_radius = 2 +local safe_zone = 12 + +-- panning / scaling control tuning +local scale_adjust_speed = 2 +local translation_adjust_speed = 600 +local scale_fix_translation_adjustment = 300 -- this is wrong, and the wrong way to do this, but I don't know what the correct version is + + + +local nodes, edges, areas, minimums, maximums, scale, translation +local function load_map(map_file_name) + local file, text + if type(map_file_name) == "string" then + file = io.open(map_file_name, "r") + if not file then + nodes = {} + edges = {} + areas = {} + minimums = {0, 0} + maximums = {100, 100} + scale = 1 + translation = {0, 0} + return + end + text = file:read("*all") + else + file = map_file_name + file:open("r") + text = file:read("string") + end + + local map = json.decode(text) + nodes = map.nodes or {} + edges = map.edges or {} + areas = map.areas or {} + for _, area in pairs(map.areas) do + local image_file_name = (area[4] or area[4] == nil) and area[5] and area[5].image + if image_file_name then + local file = io.open(image_file_name, "rb") + local data = file:read("*all") + file:close() + local byte_data = love.data.newByteData(data) + area[5].image_object = love.graphics.newImage(byte_data) + end + end + + -- find map size + minimums = {math.huge, math.huge} -- x, z + maximums = {-math.huge, -math.huge} -- x, z + for _, node in pairs(nodes) do + if node[1] > maximums[1] and node[4] ~= false then + maximums[1] = node[1] + end + if node[1] < minimums[1] and node[4] ~= false then + minimums[1] = node[1] + end + if node[3] > maximums[2] and node[4] ~= false then + maximums[2] = node[3] + end + if node[3] < minimums[2] and node[4] ~= false then + minimums[2] = node[3] + end + end + + -- find scale factor, translate + local distance_x = maximums[1] - minimums[1] + local distance_z = maximums[2] - minimums[2] + scale = math.min((screen_width - safe_zone * 2) / distance_x, (screen_height - safe_zone * 2) / distance_z) + translation = {-minimums[1] + safe_zone / scale, -minimums[2] + safe_zone / scale} + + -- TEMP displaying scale to debug + -- scale = 1 +end +load_map("map.json") + + + +local font = love.graphics.getFont() +function love.draw() + love.graphics.scale(scale) + love.graphics.translate(unpack(translation)) + + -- draw edges below nodes + for index, edge in ipairs(edges) do + local node_a = nodes[edge[1]] + local node_b = nodes[edge[2]] + if (not node_a) or (not node_b) then + error("Edge " .. index .. " names nonexistent node(s): " .. edge[1] .. "->" .. node_a .. ", " .. edge[2] .. "->" .. node_b) + end + + if not (edge[3] == false) then + local color = edge[4] and edge[4].color or {0.33, 0.33, 0.33, 1} + love.graphics.setColor(color) + love.graphics.line(node_a[1], node_a[3], node_b[1], node_b[3]) + + local center_x = node_a[1] + (node_b[1] - node_a[1]) / 2 + local center_z = node_a[3] + (node_b[3] - node_a[3]) / 2 + local distance = math.floor(math.sqrt((node_b[3] - node_a[3])^2 + (node_b[2] - node_a[2])^2 + (node_b[1] - node_a[1])^2)) .. "m" + if type(edge[3]) == "string" then + distance = edge[3] + end + + local color = edge[4] and edge[4].color or {0.67, 0.67, 0.67, 1} + love.graphics.setColor(color) + if edge[4] and edge[4].offset then + -- these actually work better not scale-adjusted + center_x = center_x + edge[4].offset[1] + center_z = center_z + edge[4].offset[2] + end + love.graphics.print(distance, center_x, center_z - font:getHeight() / 2, 0, 1 / scale, 1 / scale) + end + end + + -- areas sit between edges and nodes + for name, area in pairs(areas) do + local color = area[5] and area[5].color or {1, 1, 1, 0.25} + love.graphics.setColor(color) + if not (area[4] == false) then + local mode = area[5] and area[5].mode or "fill" + local box = area[5] and area[5].box + local image = area[5] and area[5].image_object + + if image then + local size = area[5].size + local image_width, image_height = image:getWidth(), image:getHeight() + love.graphics.draw(image, area[1], area[3], 0, size[1] / image_width, size[2] / image_height, image_width / 2, image_height / 2) + elseif box then + love.graphics.rectangle(mode, area[1] - box[1] / 2, area[3] - box[2] / 2, box[1], box[2]) + else + local radius = area[5] and area[5].radius or node_radius + if type(radius) == "number" then + love.graphics.circle(mode, area[1], area[3], radius) + else + love.graphics.ellipse(mode, area[1], area[3], radius[1], radius[2]) + end + end + end + + if area[4] then + if type(area[4]) == "string" then + name = area[4] + end + + local text_x, text_z = area[1] + node_radius / scale, area[3] + node_radius / scale + + local difference = area[1] + font:getWidth(name) - maximums[1] + if difference > 0 then + text_x = area[1] - (node_radius + difference) / scale + end + + local difference = area[3] + font:getHeight() - maximums[2] + if difference > 0 then + text_z = area[3] - (node_radius + difference) / scale + end + + if area[5] and area[5].offset then + text_x = text_x + area[5].offset[1] / scale + text_z = text_z + area[5].offset[2] / scale + end + love.graphics.setColor(1, 1, 1, 1) + love.graphics.print(name, text_x, text_z, 0, 1 / scale, 1 / scale) + end + end + + -- draw nodes and label them + for name, node in pairs(nodes) do + local color = node[5] and node[5].color or {1, 1, 1, 1} + love.graphics.setColor(color) + + if not (node[4] == false) then + love.graphics.circle("fill", node[1], node[3], node_radius / scale) + end + + if node[4] then + if type(node[4]) == "string" then + name = node[4] + end + + local text_x, text_z = node[1] + node_radius / scale, node[3] + node_radius / scale + + local difference = node[1] + font:getWidth(name) - maximums[1] + if difference > 0 then + text_x = node[1] - (node_radius + difference) / scale + end + + local difference = node[3] + font:getHeight() - maximums[2] + if difference > 0 then + text_z = node[3] - (node_radius + difference) / scale + end + + if node[5] and node[5].offset then + text_x = text_x + node[5].offset[1] / scale + text_z = text_z + node[5].offset[2] / scale + end + love.graphics.print(name, text_x, text_z, 0, 1 / scale, 1 / scale) + end + end +end + +function love.update(dt) + if love.keyboard.isDown("w") or love.keyboard.isDown("up") then + translation[2] = translation[2] + translation_adjust_speed / scale * dt + end + if love.keyboard.isDown("s") or love.keyboard.isDown("down") then + translation[2] = translation[2] - translation_adjust_speed / scale * dt + end + if love.keyboard.isDown("a") or love.keyboard.isDown("left") then + translation[1] = translation[1] + translation_adjust_speed / scale * dt + end + if love.keyboard.isDown("d") or love.keyboard.isDown("right") then + translation[1] = translation[1] - translation_adjust_speed / scale * dt + end + if love.keyboard.isDown("=") then + scale = scale + scale * scale_adjust_speed * dt + translation[1] = translation[1] - scale_fix_translation_adjustment * scale_adjust_speed * dt + translation[2] = translation[2] - scale_fix_translation_adjustment * scale_adjust_speed * dt + end + if love.keyboard.isDown("-") then + scale = scale - scale * scale_adjust_speed * dt + translation[1] = translation[1] + scale_fix_translation_adjustment * scale_adjust_speed * dt + translation[2] = translation[2] + scale_fix_translation_adjustment * scale_adjust_speed * dt + end +end + +function love.filedropped(file) + load_map(file) +end + +function love.keypressed(key) + if key == "escape" then + love.event.quit() + elseif key == "r" then + load_map("map.json") + end +end