improved xrfragment imports + initial commit for gazecontrol extension

This commit is contained in:
Leon van Kammen 2026-06-29 16:16:28 +02:00
parent 4c72581493
commit 14500b44a2
15 changed files with 671 additions and 603 deletions

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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("<fireboxroom>") or res.data:lower():match("<room[ >]") then
local JML0
local JML
res.xml = api.parser.xml.newParser()
local xmlstr = res.data
-- cleanup JML0
local JML0 = xmlstr:gsub(".*<[Rr]oom","<room")
JML0 = JML0:gsub("</[Rr]oom>.*","</room>")
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("<fireboxroom>") or res.data:lower():match("<room[ >]") 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","<room")
JML0 = JML0:gsub("</[Rr]oom>.*","</room>")
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,

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1 +0,0 @@
../ext/xrfragments/json.lua

386
xurfer/lib/json.lua Normal file
View file

@ -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

View file

@ -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

View file

@ -1 +0,0 @@
../ext/xrfragments/url.lua

167
xurfer/lib/url.lua Normal file
View file

@ -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

View file

@ -167,3 +167,8 @@ function newNode(name)
return node
end
return {
newNode = newNode,
newParser = newParser
}

View file

@ -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

View file

@ -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]