From 14500b44a2f5709214212bf8ce25de15ae0e0a6d Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Mon, 29 Jun 2026 16:16:28 +0200 Subject: [PATCH] improved xrfragment imports + initial commit for gazecontrol extension --- xurfer/conf.lua | 10 +- xurfer/ext/3DFile/main.lua | 19 +- xurfer/ext/gazecontrol/main.lua | 35 +++ xurfer/ext/janusxr/main.lua | 33 ++- xurfer/ext/skeleton/main.lua | 14 +- xurfer/ext/xrfragments/json.lua | 386 --------------------------- xurfer/ext/xrfragments/lovr-xrf.lua | 2 +- xurfer/ext/xrfragments/main.lua | 14 +- xurfer/ext/xrfragments/url.lua | 166 ------------ xurfer/lib/json.lua | 387 +++++++++++++++++++++++++++- xurfer/lib/tiny-ecs.lua | 4 +- xurfer/lib/url.lua | 168 +++++++++++- xurfer/lib/xmlSimple.lua | 5 + xurfer/runtime/lovr/conf.lua | 19 +- xurfer/util.lua | 12 +- 15 files changed, 671 insertions(+), 603 deletions(-) create mode 100644 xurfer/ext/gazecontrol/main.lua delete mode 100644 xurfer/ext/xrfragments/json.lua delete mode 100644 xurfer/ext/xrfragments/url.lua mode change 120000 => 100644 xurfer/lib/json.lua mode change 120000 => 100644 xurfer/lib/url.lua diff --git a/xurfer/conf.lua b/xurfer/conf.lua index 986f8f9..f8bfa01 100644 --- a/xurfer/conf.lua +++ b/xurfer/conf.lua @@ -1,16 +1,8 @@ -- sync lovr/love positional cli arguments if arg[0] == nil and arg[1] ~= nil then arg[0] = arg[1] end -package.path = package.path .. ';' .. - '?.lua;' .. - '?/init.lua;' .. - 'lib/?.lua;' +package.path = package.path .. ';' .. '?.lua;' .. 'lib/?.lua' local runtime = require("runtime/detect") -package.path = package.path .. ';' .. - arg[0] .. '/' .. runtime.path .. '/?.lua;' .. - arg[0] .. '/' .. runtime.path .. '/?/init.lua;' .. - arg[0] .. '/lib/?.lua' - require( runtime.path .. "/conf") diff --git a/xurfer/ext/3DFile/main.lua b/xurfer/ext/3DFile/main.lua index 06bc3ad..b1cff6b 100644 --- a/xurfer/ext/3DFile/main.lua +++ b/xurfer/ext/3DFile/main.lua @@ -16,12 +16,21 @@ return { obj.root = true api.ext.URI.current = obj.URL -- set current url end - obj.x = obj.x or 0 - obj.y = obj.y or 0 - obj.z = obj.z or 0 - obj.scale = obj.scale or 1 + if type(obj.URI.target) == 'table' and obj.URI.target.obj ~= nil then + local parent = obj.URI.target.obj + -- TODO: the runtime/lovr/render/model code should calculate parent offset + obj.x = parent.x + obj.y = parent.y + obj.z = parent.z + obj.scale = parent.scale + else + obj.x = obj.x or 0 + obj.y = obj.y or 0 + obj.z = obj.z or 0 + obj.scale = obj.scale or 1 + end if obj.URL.protocol == 'file' then - obj.model = api.graphics.newModel( obj.URL.string ) + obj.model = api.graphics.newModel( obj.URL.file ) else obj.model = api.graphics.newModel( api.data.newBlob( obj.URLResponse.data) ) end diff --git a/xurfer/ext/gazecontrol/main.lua b/xurfer/ext/gazecontrol/main.lua new file mode 100644 index 0000000..4931473 --- /dev/null +++ b/xurfer/ext/gazecontrol/main.lua @@ -0,0 +1,35 @@ +local api = ... +local ecs = api.ecs + +local gctl + +gctl = { + + name = "gctl", + enabled = true, + dtmax = 1, + dt = 0, + + init = function() + lovr.mouse.setRelativeMode(true) + + camera = { + transform = lovr.math.newMat4(), + position = vector(), + movespeed = 10, + pitch = 0, + yaw = 0 + } + end, + + update = function(dt) + end, + + load = function() end, + + draw = function(pass,dt) + end, + +} + +return gctl diff --git a/xurfer/ext/janusxr/main.lua b/xurfer/ext/janusxr/main.lua index 98311ce..e3996e7 100644 --- a/xurfer/ext/janusxr/main.lua +++ b/xurfer/ext/janusxr/main.lua @@ -7,18 +7,27 @@ return { enabled = true, onURI = function(obj) - if obj.URL ~= nil and obj.URLResponse ~= nil and obj.URLResponse.ok then - local res = obj.URLResponse - -- JML heuristic - if res.data:lower():match("") or res.data:lower():match("]") then - local JML0 - local JML - res.xml = api.parser.xml.newParser() - local xmlstr = res.data - -- cleanup JML0 - local JML0 = xmlstr:gsub(".*<[Rr]oom",".*","") - api.ext.janusxr.loadXML( JML0, res, obj ) + if obj.URL ~= nil and (obj.URLResponse ~= nil or obj.URL.protocol == 'file') then + if obj.URL.protocol == 'file' and obj.URLResponse.ok == nil then + local data = util.readfile(obj.URL.file) + obj.URLResponse = { data = data, ok = (data ~= nil) } + end + if obj.URLResponse.ok then + local res = obj.URLResponse + -- JML heuristic + if res.data:lower():match("") or res.data:lower():match("]") then + local JML0 + local JML + print_r(api.parser) + res.xml = api.parser.xml.newParser() + local xmlstr = res.data + -- cleanup JML0 + local JML0 = xmlstr:gsub(".*<[Rr]oom",".*","") + api.ext.janusxr.loadXML( JML0, res, obj ) + end + else + print("[janusxr] error: could not load " .. obj.URI.url .. ": " .. (obj.URLResponse.data or '') ) end end end, diff --git a/xurfer/ext/skeleton/main.lua b/xurfer/ext/skeleton/main.lua index b457f85..4137ef2 100644 --- a/xurfer/ext/skeleton/main.lua +++ b/xurfer/ext/skeleton/main.lua @@ -9,21 +9,11 @@ return { --api.ext.skeleton.extendECS() end, - update = function() - local iui = api.iui - local backend = api.backend - local mainWindow = api.mainWindow - end, + update = function(dt) end, load = function() end, - draw = function(pass) - if api.iui.idiom == "vr" then - - else - - end - end, + draw = function(pass,dt) end, -- ECS API docs: https://bakpakin.github.io/tiny-ecs/doc extendECS = function() diff --git a/xurfer/ext/xrfragments/json.lua b/xurfer/ext/xrfragments/json.lua deleted file mode 100644 index 190fe20..0000000 --- a/xurfer/ext/xrfragments/json.lua +++ /dev/null @@ -1,386 +0,0 @@ --- --- json.lua --- --- Copyright (c) 2020 rxi --- --- 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. --- - -local json = { _version = "0.1.2" } - -------------------------------------------------------------------------------- --- Encode -------------------------------------------------------------------------------- - -local encode - -local escape_char_map = { - [ "\\" ] = "\\", - [ "\"" ] = "\"", - [ "\b" ] = "b", - [ "\f" ] = "f", - [ "\n" ] = "n", - [ "\r" ] = "r", - [ "\t" ] = "t", -} - -local escape_char_map_inv = { [ "/" ] = "/" } -for k, v in pairs(escape_char_map) do - escape_char_map_inv[v] = k -end - - -local function escape_char(c) - return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) -end - - -local function encode_nil(val) - return "null" -end - - -local function encode_table(val, stack) - local res = {} - stack = stack or {} - - -- Circular reference? - if stack[val] then error("circular reference") end - - stack[val] = true - - if rawget(val, 1) ~= nil or next(val) == nil then - -- Treat as array -- check keys are valid and it is not sparse - local n = 0 - for k in pairs(val) do - if type(k) ~= "number" then - error("invalid table: mixed or invalid key types") - end - n = n + 1 - end - if n ~= #val then - error("invalid table: sparse array") - end - -- Encode - for i, v in ipairs(val) do - table.insert(res, encode(v, stack)) - end - stack[val] = nil - return "[" .. table.concat(res, ",") .. "]" - - else - -- Treat as an object - for k, v in pairs(val) do - if type(k) ~= "string" then - error("invalid table: mixed or invalid key types") - end - table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) - end - stack[val] = nil - return "{" .. table.concat(res, ",") .. "}" - end -end - - -local function encode_string(val) - return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' -end - - -local function encode_number(val) - -- Check for NaN, -inf and inf - if val ~= val or val <= -math.huge or val >= math.huge then - error("unexpected number value '" .. tostring(val) .. "'") - end - return string.format("%.14g", val) -end - - -local type_func_map = { - [ "nil" ] = encode_nil, - [ "table" ] = encode_table, - [ "string" ] = encode_string, - [ "number" ] = encode_number, - [ "boolean" ] = tostring, -} - - -encode = function(val, stack) - local t = type(val) - local f = type_func_map[t] - if f then - return f(val, stack) - end - error("unexpected type '" .. t .. "'") -end - - -function json.encode(val) - return ( encode(val) ) -end - -------------------------------------------------------------------------------- --- Decode -------------------------------------------------------------------------------- - -local parse - -local function create_set(...) - local res = {} - for i = 1, select("#", ...) do - res[ select(i, ...) ] = true - end - return res -end - -local space_chars = create_set(" ", "\t", "\r", "\n") -local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") -local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") -local literals = create_set("true", "false", "null") - -local literal_map = { - [ "true" ] = true, - [ "false" ] = false, - [ "null" ] = nil, -} - - -local function next_char(str, idx, set, negate) - for i = idx, #str do - if set[str:sub(i, i)] ~= negate then - return i - end - end - return #str + 1 -end - - -local function decode_error(str, idx, msg) - local line_count = 1 - local col_count = 1 - for i = 1, idx - 1 do - col_count = col_count + 1 - if str:sub(i, i) == "\n" then - line_count = line_count + 1 - col_count = 1 - end - end - error( string.format("%s at line %d col %d", msg, line_count, col_count) ) -end - - -local function codepoint_to_utf8(n) - -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa - local f = math.floor - if n <= 0x7f then - return string.char(n) - elseif n <= 0x7ff then - return string.char(f(n / 64) + 192, n % 64 + 128) - elseif n <= 0xffff then - return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) - elseif n <= 0x10ffff then - return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, - f(n % 4096 / 64) + 128, n % 64 + 128) - end - error( string.format("invalid unicode codepoint '%x'", n) ) -end - - -local function parse_unicode_escape(s) - local n1 = tonumber( s:sub(1, 4), 16 ) - local n2 = tonumber( s:sub(7, 10), 16 ) - -- Surrogate pair? - if n2 then - return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) - else - return codepoint_to_utf8(n1) - end -end - - -local function parse_string(str, i) - local res = "" - local j = i + 1 - local k = j - - while j <= #str do - local x = str:byte(j) - - if x < 32 then - decode_error(str, j, "control character in string") - - elseif x == 92 then -- `\`: Escape - res = res .. str:sub(k, j - 1) - j = j + 1 - local c = str:sub(j, j) - if c == "u" then - local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) - or str:match("^%x%x%x%x", j + 1) - or decode_error(str, j - 1, "invalid unicode escape in string") - res = res .. parse_unicode_escape(hex) - j = j + #hex - else - if not escape_chars[c] then - decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") - end - res = res .. escape_char_map_inv[c] - end - k = j + 1 - - elseif x == 34 then -- `"`: End of string - res = res .. str:sub(k, j - 1) - return res, j + 1 - end - - j = j + 1 - end - - decode_error(str, i, "expected closing quote for string") -end - - -local function parse_number(str, i) - local x = next_char(str, i, delim_chars) - local s = str:sub(i, x - 1) - local n = tonumber(s) - if not n then - decode_error(str, i, "invalid number '" .. s .. "'") - end - return n, x -end - - -local function parse_literal(str, i) - local x = next_char(str, i, delim_chars) - local word = str:sub(i, x - 1) - if not literals[word] then - decode_error(str, i, "invalid literal '" .. word .. "'") - end - return literal_map[word], x -end - - -local function parse_array(str, i) - local res = {} - local n = 1 - i = i + 1 - while 1 do - local x - i = next_char(str, i, space_chars, true) - -- Empty / end of array? - if str:sub(i, i) == "]" then - i = i + 1 - break - end - -- Read token - x, i = parse(str, i) - res[n] = x - n = n + 1 - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "]" then break end - if chr ~= "," then decode_error(str, i, "expected ']' or ','") end - end - return res, i -end - - -local function parse_object(str, i) - local res = {} - i = i + 1 - while 1 do - local key, val - i = next_char(str, i, space_chars, true) - -- Empty / end of object? - if str:sub(i, i) == "}" then - i = i + 1 - break - end - -- Read key - if str:sub(i, i) ~= '"' then - decode_error(str, i, "expected string for key") - end - key, i = parse(str, i) - -- Read ':' delimiter - i = next_char(str, i, space_chars, true) - if str:sub(i, i) ~= ":" then - decode_error(str, i, "expected ':' after key") - end - i = next_char(str, i + 1, space_chars, true) - -- Read value - val, i = parse(str, i) - -- Set - res[key] = val - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "}" then break end - if chr ~= "," then decode_error(str, i, "expected '}' or ','") end - end - return res, i -end - - -local char_func_map = { - [ '"' ] = parse_string, - [ "0" ] = parse_number, - [ "1" ] = parse_number, - [ "2" ] = parse_number, - [ "3" ] = parse_number, - [ "4" ] = parse_number, - [ "5" ] = parse_number, - [ "6" ] = parse_number, - [ "7" ] = parse_number, - [ "8" ] = parse_number, - [ "9" ] = parse_number, - [ "-" ] = parse_number, - [ "t" ] = parse_literal, - [ "f" ] = parse_literal, - [ "n" ] = parse_literal, - [ "[" ] = parse_array, - [ "{" ] = parse_object, -} - - -parse = function(str, idx) - local chr = str:sub(idx, idx) - local f = char_func_map[chr] - if f then - return f(str, idx) - end - decode_error(str, idx, "unexpected character '" .. chr .. "'") -end - - -function json.decode(str) - if type(str) ~= "string" then - error("expected argument of type string, got " .. type(str)) - end - local res, idx = parse(str, next_char(str, 1, space_chars, true)) - idx = next_char(str, idx, space_chars, true) - if idx <= #str then - decode_error(str, idx, "trailing garbage") - end - return res -end - -return json diff --git a/xurfer/ext/xrfragments/lovr-xrf.lua b/xurfer/ext/xrfragments/lovr-xrf.lua index 8c9ecac..c004a09 100644 --- a/xurfer/ext/xrfragments/lovr-xrf.lua +++ b/xurfer/ext/xrfragments/lovr-xrf.lua @@ -107,7 +107,7 @@ xrf.makeClickable = function( physicsWorld, cb) scale[3] * (d + 0.01) -- planes e.g. ) print("making node " .. node['name'] .. " clickable: " .. node.extras.href ) - collider:setUserData( {name = node['name'], node = node, model = model, mesh = mesh, onclick = cb }) + collider:setUserData( {name = node['name'], node = node, model = model, mesh = mesh, onclick = cb, obj = obj }) end end diff --git a/xurfer/ext/xrfragments/main.lua b/xurfer/ext/xrfragments/main.lua index e5a8ad3..1db9a4c 100644 --- a/xurfer/ext/xrfragments/main.lua +++ b/xurfer/ext/xrfragments/main.lua @@ -11,6 +11,7 @@ xrfimpl = { init = function() end, on3DFile = function(obj) + local referer = obj.URI.url if lovr ~= nil then xrf.traverseNodesContaining('href', obj, xrf.makeClickable( api.ecs.worldPhysics, @@ -18,10 +19,9 @@ xrfimpl = { local href = obj.node.extras.href trace("\n[xrf] href was clicked: " .. href) api.ext.exec("onClick", obj, collider) - local ok = xrf.level2.load( href, obj, api.ext.URI.current, xrfimpl.onLoad) or - xrf.level4.import( href, obj, api.ext.URI.current, xrfimpl.onImport) + xrf.level4.import( href, obj, referer, xrfimpl.onImport) end ) @@ -51,10 +51,12 @@ xrfimpl = { local filter = function(o) return (o.URL ~= nil and o.URL.URN:gsub("#.*","") == absoluteHref:gsub("#.*","")) end - local obj = api.world.get(filter) - if obj then - obj.URL = url.parse(absoluteHref) -- re-parse to detect media fragment change - xrfimpl.onAudioFile(obj) + local newobj = api.world.get(filter) + if newobj then + if newobj.URL.audio ~= nil then + newobj.URL = url.parse(absoluteHref) -- re-parse to detect media fragment change + xrfimpl.onAudioFile(obj) + end else api.ecs.add( api.world, { URI = { url = absoluteHref, method = 'GET', target = obj } }) end diff --git a/xurfer/ext/xrfragments/url.lua b/xurfer/ext/xrfragments/url.lua deleted file mode 100644 index a0fd7d6..0000000 --- a/xurfer/ext/xrfragments/url.lua +++ /dev/null @@ -1,166 +0,0 @@ --- Copyright 2026 Leon van Kammen / coderofsalvation. All rights reserved. --- --- Redistribution and use in source and binary forms, with or without modification, are --- permitted provided that the following conditions are met: --- --- 1. Redistributions of source code must retain the above copyright notice, this list of --- conditions and the following disclaimer. --- --- 2. Redistributions in binary form must reproduce the above copyright notice, this list --- of conditions and the following disclaimer in the documentation and/or other materials --- provided with the distribution. --- --- THIS SOFTWARE IS PROVIDED BY AS IS'' AND ANY EXPRESS OR IMPLIED --- WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND --- FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OR --- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR --- CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR --- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON --- ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING --- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF --- --- ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --- --- The views and conclusions contained in the software and documentation are those of the --- authors and should not be interpreted as representing official policies, either expressed --- or implied, of - --- below is a lua adaptation of Godot's URI.gd --- https://gist.github.com/coderofsalvation/b2b111a2631fbdc8e76d6cab3bea8f17 - -local url = {} - --- Helper function to split a string by a delimiter -local function split(str, delimiter) - local result = {} - if delimiter == "" then return {str} end - local pattern = string.format("([^%s]+)", delimiter) - for match in string.gmatch(str, pattern) do - table.insert(result, match) - end - return result -end - --- Helper for guess_type (converts numbers/booleans from strings) -local function guess_type(val) - if tonumber(val) then return tonumber(val) end - if val == "true" then return true end - if val == "false" then return false end - return val -end - --- Helper to parse query parameters and fragments -local function parseArgs(fragment) - local ARG = {} - if not fragment or fragment == "" then return ARG end - - local items = split(fragment, "&") - for _, item in ipairs(items) do - local key_value = split(item, "=") - if #key_value > 1 then - local k = key_value[1] - ARG[k] = guess_type(key_value[2]) -- xr fragment operators preserved - ARG[k:gsub("[^%w]","") ] = guess_type(key_value[2]) -- alphanumeric only - elseif #key_value == 1 then - ARG[key_value[1]] = "" - end - end - return ARG -end - --- Main URL Parser function attached to the module object -function url.parse(url_string) - if type(url_string) == 'table' then return url_string end -- already parsed - -- Setup initial URI table structure - local URI = { - domain = "", - fragment = {}, - file = "", - extension = "", - string = url_string, - protocol = "", - path = "", - query = {}, - hash = "", - URN = "", - isLocal = false - } - local remaining = url_string - local protocol = string.match(remaining, "^(%w+://)") -- protocol - if protocol then - URI.protocol = protocol:gsub("://", "") - remaining = remaining:sub(#protocol + 1) - else - URI.protocol = "file" - end - local hash = string.match(remaining, "(#.*)$") -- fragment - if hash then - URI.hash = hash - remaining = remaining:sub(1, #remaining - #hash) - end - local query = string.match(remaining, "(%?[^#]+)$") - if query then - URI.query = query - remaining = remaining:sub(1, #remaining - #query) - end - URI.path = remaining - -- Process Path, Domain, and File - if URI.path and URI.path ~= "" then - local pathParts = split(URI.path, "/") - - if #pathParts > 1 then - local firstPart = pathParts[1] - -- Check if the first part looks like a domain (contains '.' or ':') - if string.find(firstPart, "%.") or string.find(firstPart, ":") then - URI.domain = table.remove(pathParts, 1) - end - end - -- Reconstruct path - URI.path = table.concat(pathParts, "/") - -- Extract file if present - if #pathParts > 0 then - local lastPart = pathParts[#pathParts] - if string.find(lastPart, "%.") then - URI.file = lastPart - URI.extension = URI.file:match("%.([^%.]+)$"):upper() - end - end - end - if URI.hash and URI.hash ~= "" then - URI.fragment = parseArgs(URI.hash:sub(2)) -- sub(2) skips the '#' - else - URI.fragment = {} - end - - if URI.query and type(URI.query) == "string" and URI.query ~= "" then - URI.query = parseArgs(URI.query:sub(2)) -- sub(2) skips the '?' - else - URI.query = {} - end - if URI.domain ~= "" then - URI.URN = URI.string:gsub("%?.*", "") - else - URI.URN = "" - end - URI.isLocal = (URI.domain == "") - return URI -end - -url.getAbsolute = function(href,referer,keephash) - local URL = url.parse(href) - local URLref = url.parse(referer) - if referer ~= nil and URL.protocol == 'file' and URLref.protocol ~= 'file' then - URL.protocol = URLref.protocol - URL.domain = URLref.domain - URL.string = URLref.protocol .. '://' .. URLref.domain .. '/' .. URL.path .. URL.hash - if keephash then - URL.URN = URL.string - else - URL.URN = URL.string:gsub("#.*", "") - end - end - return URL -end - --- Return the module table so it can be assigned via require() -return url diff --git a/xurfer/lib/json.lua b/xurfer/lib/json.lua deleted file mode 120000 index b8f72c7..0000000 --- a/xurfer/lib/json.lua +++ /dev/null @@ -1 +0,0 @@ -../ext/xrfragments/json.lua \ No newline at end of file diff --git a/xurfer/lib/json.lua b/xurfer/lib/json.lua new file mode 100644 index 0000000..190fe20 --- /dev/null +++ b/xurfer/lib/json.lua @@ -0,0 +1,386 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- 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. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + +return json diff --git a/xurfer/lib/tiny-ecs.lua b/xurfer/lib/tiny-ecs.lua index d09f6c8..28c60be 100644 --- a/xurfer/lib/tiny-ecs.lua +++ b/xurfer/lib/tiny-ecs.lua @@ -695,7 +695,9 @@ function tiny_manageEntities(world) system.modified = true local tmpEntity = ses[#ses] ses[index] = tmpEntity - seis[tmpEntity] = index + if tmpEntity ~= nil then + seis[tmpEntity] = index + end seis[entity] = nil ses[#ses] = nil local onRemove = system.onRemove diff --git a/xurfer/lib/url.lua b/xurfer/lib/url.lua deleted file mode 120000 index 71a2c45..0000000 --- a/xurfer/lib/url.lua +++ /dev/null @@ -1 +0,0 @@ -../ext/xrfragments/url.lua \ No newline at end of file diff --git a/xurfer/lib/url.lua b/xurfer/lib/url.lua new file mode 100644 index 0000000..4a9ecc1 --- /dev/null +++ b/xurfer/lib/url.lua @@ -0,0 +1,167 @@ +-- Copyright 2026 Leon van Kammen / coderofsalvation. All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without modification, are +-- permitted provided that the following conditions are met: +-- +-- 1. Redistributions of source code must retain the above copyright notice, this list of +-- conditions and the following disclaimer. +-- +-- 2. Redistributions in binary form must reproduce the above copyright notice, this list +-- of conditions and the following disclaimer in the documentation and/or other materials +-- provided with the distribution. +-- +-- THIS SOFTWARE IS PROVIDED BY AS IS'' AND ANY EXPRESS OR IMPLIED +-- WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +-- FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OR +-- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +-- CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +-- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +-- ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +-- +-- ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-- +-- The views and conclusions contained in the software and documentation are those of the +-- authors and should not be interpreted as representing official policies, either expressed +-- or implied, of + +-- below is a lua adaptation of Godot's URI.gd +-- https://gist.github.com/coderofsalvation/b2b111a2631fbdc8e76d6cab3bea8f17 + +local url = {} + +-- Helper function to split a string by a delimiter +local function split(str, delimiter) + local result = {} + if delimiter == "" then return {str} end + local pattern = string.format("([^%s]+)", delimiter) + for match in string.gmatch(str, pattern) do + table.insert(result, match) + end + return result +end + +-- Helper for guess_type (converts numbers/booleans from strings) +local function guess_type(val) + if tonumber(val) then return tonumber(val) end + if val == "true" then return true end + if val == "false" then return false end + return val +end + +-- Helper to parse query parameters and fragments +local function parseArgs(fragment) + local ARG = {} + if not fragment or fragment == "" then return ARG end + + local items = split(fragment, "&") + for _, item in ipairs(items) do + local key_value = split(item, "=") + if #key_value > 1 then + local k = key_value[1] + ARG[k] = guess_type(key_value[2]) -- xr fragment operators preserved + ARG[k:gsub("[^%w]","") ] = guess_type(key_value[2]) -- alphanumeric only + elseif #key_value == 1 then + ARG[key_value[1]] = "" + end + end + return ARG +end + +-- Main URL Parser function attached to the module object +function url.parse(url_string) + if type(url_string) == 'table' then return url_string end -- already parsed + -- Setup initial URI table structure + local URI = { + domain = "", + fragment = {}, + file = "", + extension = "", + string = url_string, + protocol = "", + path = "", + query = {}, + hash = "", + URN = "", + isLocal = false + } + local remaining = url_string + local protocol = string.match(remaining, "^(%w+://)") -- protocol + if protocol then + URI.protocol = protocol:gsub("://", "") + remaining = remaining:sub(#protocol + 1) + else + URI.protocol = "file" + end + local hash = string.match(remaining, "(#.*)$") -- fragment + if hash then + URI.hash = hash + remaining = remaining:sub(1, #remaining - #hash) + end + local query = string.match(remaining, "(%?[^#]+)$") + if query then + URI.query = query + remaining = remaining:sub(1, #remaining - #query) + end + URI.path = remaining + -- Process Path, Domain, and File + if URI.path and URI.path ~= "" then + local pathParts = split(URI.path, "/") + + if #pathParts > 1 then + local firstPart = pathParts[1] + -- Check if the first part looks like a domain (contains '.' or ':') + if string.find(firstPart, "%.") or string.find(firstPart, ":") then + URI.domain = table.remove(pathParts, 1) + end + end + -- Reconstruct path + URI.path = table.concat(pathParts, "/") + -- Extract file if present + if #pathParts > 0 then + local lastPart = pathParts[#pathParts] + if string.find(lastPart, "%.") then + URI.file = lastPart + URI.extension = URI.file:match("%.([^%.]+)$"):upper() + end + end + end + if URI.hash and URI.hash ~= "" then + URI.fragment = parseArgs(URI.hash:sub(2)) -- sub(2) skips the '#' + else + URI.fragment = {} + end + + if URI.query and type(URI.query) == "string" and URI.query ~= "" then + URI.query = parseArgs(URI.query:sub(2)) -- sub(2) skips the '?' + else + URI.query = {} + end + if URI.domain ~= "" then + URI.URN = URI.string:gsub("%?.*", "") + else + URI.URN = "" + end + URI.isLocal = (URI.domain == "") + return URI +end + +url.getAbsolute = function(href,referer,keephash) + local URL = url.parse(href) + local URLref = url.parse(referer) + if referer ~= nil and URL.protocol == 'file' and URLref.protocol ~= 'file' then + URL.protocol = URLref.protocol or '' + URL.domain = URLref.domain or '' + URL.string = URL.protocol .. '://' .. URL.domain .. '/' .. URL.path .. URL.hash + if keephash then + URL.URN = URL.string + else + URL.URN = URL.string:gsub("#.*", "") + end + end + URL.referer = referer + return URL +end + +-- Return the module table so it can be assigned via require() +return url diff --git a/xurfer/lib/xmlSimple.lua b/xurfer/lib/xmlSimple.lua index 576ae72..6780e25 100644 --- a/xurfer/lib/xmlSimple.lua +++ b/xurfer/lib/xmlSimple.lua @@ -167,3 +167,8 @@ function newNode(name) return node end + +return { + newNode = newNode, + newParser = newParser +} diff --git a/xurfer/runtime/lovr/conf.lua b/xurfer/runtime/lovr/conf.lua index 64de556..db04940 100644 --- a/xurfer/runtime/lovr/conf.lua +++ b/xurfer/runtime/lovr/conf.lua @@ -1,5 +1,22 @@ +local runtime = require("runtime/detect") +-- sync lovr/love positional cli arguments +if arg[0] == nil and arg[1] ~= nil then arg[0] = arg[1] end + +local runtime = require("runtime/detect") + +-- setup path +print("!!!!!!!") +package.path = package.path .. ';' .. + './runtime/lovr/?.lua;' .. + './runtime/lovr/?/init.lua;' .. + './lib/?.lua;' .. + './?.lua' + --arg[0] .. 'runtime/lovr/?.lua;' .. + --arg[0] .. 'runtime/lovr/?/init.lua;' .. + --arg[0] .. 'lib/?.lua;' .. + local util = require("util") -local launch = require "launch" +local launch = require "runtime/lovr/launch" local function isVR() return launch.mode == "vr" or util.match(arg,"--desktop") == false diff --git a/xurfer/util.lua b/xurfer/util.lua index 392c62e..ae8724e 100644 --- a/xurfer/util.lua +++ b/xurfer/util.lua @@ -1,5 +1,13 @@ local util = { ext = {} } +util.readfile = function(filename, mode) + local file, err = io.open(filename, mode or "r") + if not file then return print("Error opening file:", err) end + local content = file:read("*a") + file:close() + return content +end + util.loaddir = function( path, api, target) local dir = api.filesystem.getDirectoryItems(path) foreach( dir, function(k,v) @@ -153,8 +161,8 @@ end -- entity.commit() -- this recalculates the filtercache of systems util.commit = function(world, obj, api) return function(extCb) - for j = 1, #world.entities do - local system = world.systems[j] + for i = 1, #world.systems do + local system = world.systems[i] system.entities = {} for j = 1, #world.entities do local entity = world.entities[j]