Compare commits

...
Sign in to create a new pull request.

5 commits
master ... dev

26 changed files with 993 additions and 756 deletions

View file

@ -1,16 +1,13 @@
-- default argument
if arg[1] == nil then
arg[1] = "https://snips.sh/f/_U5-XctEVE?r=1"
end
-- sync lovr/love positional cli arguments -- sync lovr/love positional cli arguments
if arg[0] == nil and arg[1] ~= nil then arg[0] = arg[1] end if arg[0] == nil and arg[1] ~= nil then arg[0] = arg[1] end
package.path = package.path .. ';' .. package.path = package.path .. ';' .. '?.lua;' .. 'lib/?.lua'
'?.lua;' ..
'?/init.lua;' ..
'lib/?.lua;'
local runtime = require("runtime/detect") 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") require( runtime.path .. "/conf")

View file

@ -1,4 +1,4 @@
local ecs = require("tiny-ecs") local ecs = require("lib/tiny-ecs")
ecs.init = function(api) ecs.init = function(api)
print("[i] loading ecs") print("[i] loading ecs")
@ -30,4 +30,14 @@ ecs.clear = function()
api.world.commit() api.world.commit()
end end
ecs.extension = {
name = "ecs",
enabled = true,
index = 300,
init = function(api) ecs.init(api) end,
draw = function(pass)
ecs.update( api.world, pass, ecs.filterDraw )
end
}
return ecs return ecs

View file

@ -14,14 +14,25 @@ return {
obj.root = false obj.root = false
if obj.URI.target == '_top' then if obj.URI.target == '_top' then
obj.root = true obj.root = true
if obj.URI.protocol ~= 'xrf' then -- xrf:// should not update toplevel URI
api.ext.URI.current = obj.URL -- set current url api.ext.URI.current = obj.URL -- set current url
end end
end
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.x = obj.x or 0
obj.y = obj.y or 0 obj.y = obj.y or 0
obj.z = obj.z or 0 obj.z = obj.z or 0
obj.scale = obj.scale or 1 obj.scale = obj.scale or 1
end
if obj.URL.protocol == 'file' then if obj.URL.protocol == 'file' then
obj.model = api.graphics.newModel( obj.URL.string ) obj.model = api.graphics.newModel( obj.URL.file )
else else
obj.model = api.graphics.newModel( api.data.newBlob( obj.URLResponse.data) ) obj.model = api.graphics.newModel( api.data.newBlob( obj.URLResponse.data) )
end end

View file

@ -0,0 +1,106 @@
local api = ...
local ecs = api.ecs
local gctl
gctl = {
name = "gazecontrol",
enabled = (lovr ~= nil),
index = api.ext.max * 0.9, -- important: render last! (renderOrder)
texture = {},
material = nil,
dtmax = 1,
dt = 0,
visor = {
rotate = { x = 0, y = 0, z = 0},
scaleTo = 0.1,
scale = 0.1,
scaleSpeed = 0.005,
walkingSpeed = 2,
texture = false,
textures = {}
},
init = function()
--lovr.mouse.setRelativeMode(true)
gctl.initVisor()
gctl.extendECS()
api.player.matrix = lovr.math.newMat4()
end,
initVisor = function()
local visor = gctl.visor
visor.textures.idle = lovr.graphics.newTexture("ext/gazecontrol/reticle_idle.png")
visor.textures.hover = lovr.graphics.newTexture("ext/gazecontrol/reticle_hover.png")
visor.texture = visor.textures.idle
end,
update = function(dt, input)
local visor = gctl.visor
if input.pointer.hover then
visor.texture = visor.textures.hover
if visor.scale == visor.scaleTo then
visor.scale = 0.05
end
else
visor.texture = visor.textures.idle
visor.scale = visor.scaleTo
end
if input.pointer.wheel then -- move forward/backward via scrollwheel
print( input.pointer.wheel.y)
local direction = vector(lovr.headset.getDirection('head'))
if not api.player.flying then direction = vector(direction.x, 0, direction.z) end
-- move forward/backward via scrollwheel
api.player.matrix:translate(direction * (input.pointer.wheel.y * visor.walkingSpeed) )
end
end,
draw = function(pass,dt)
-- *TODO* render this last (as it needs to be rendered after skybox & floor)
local pos = vector( lovr.headset.getPosition('head') ) + vector( api.player.matrix:getPosition() )
local dirx, diry, dirz = lovr.headset.getDirection('head')
local distance = 1
local x = pos.x + distance * dirx
local y = pos.y + distance * diry
local z = pos.z + distance * dirz
local angle, ax, ay, az = lovr.headset.getOrientation(device)
local visor = gctl.visor
local rot = visor.rotate
local scale = visor.scale
if scale < visor.scaleTo then
scale = scale + visor.scaleSpeed
visor.scale = scale
end
pass:setMaterial(visor.texture)
pass:setColor(1, 1, 1, 1)
pass:setCullMode('back')
--pass:setFaceCull()
--pass:setDepthWrite(true)
pass:setBlendMode("alpha", "alphamultiply")
--pass:setColorWrite(true)
pass:plane(x, y, z, scale, scale, angle, ax, ay, az)
end,
-- ECS API docs: https://bakpakin.github.io/tiny-ecs/doc
extendECS = function()
local visorSystem = ecs.processingSystem({
updatethread = true,
filter = ecs.requireAll('mycomponent'),
onAdd = function(self,obj) print_r(obj) end,
onRemove = function(self,obj) end,
process = function(self,obj) end,
})
ecs.addSystem( api.world, visorSystem )
ecs.add( api.world, { visor = gctl.visor } )
--gctl.visor.commit() -- notify other systems to update entitycache
end
}
return gctl

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -7,7 +7,12 @@ return {
enabled = true, enabled = true,
onURI = function(obj) onURI = function(obj)
if obj.URL ~= nil and obj.URLResponse ~= nil and obj.URLResponse.ok then 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 local res = obj.URLResponse
-- JML heuristic -- JML heuristic
if res.data:lower():match("<fireboxroom>") or res.data:lower():match("<room[ >]") then if res.data:lower():match("<fireboxroom>") or res.data:lower():match("<room[ >]") then
@ -20,6 +25,10 @@ return {
JML0 = JML0:gsub("</[Rr]oom>.*","</room>") JML0 = JML0:gsub("</[Rr]oom>.*","</room>")
api.ext.janusxr.loadXML( JML0, res, obj ) api.ext.janusxr.loadXML( JML0, res, obj )
end end
else
print("[janusxr] error: could not load " .. obj.URI.url .. ": ")
print_r(obj.URLResponse)
end
end end
end, end,

View file

@ -9,21 +9,9 @@ return {
--api.ext.skeleton.extendECS() --api.ext.skeleton.extendECS()
end, end,
update = function() update = function(dt,input) end,
local iui = api.iui
local backend = api.backend
local mainWindow = api.mainWindow
end,
load = function() end, load = function() end,
draw = function(pass,dt) end,
draw = function(pass)
if api.iui.idiom == "vr" then
else
end
end,
-- ECS API docs: https://bakpakin.github.io/tiny-ecs/doc -- ECS API docs: https://bakpakin.github.io/tiny-ecs/doc
extendECS = function() extendECS = function()

View file

@ -5,6 +5,7 @@ local shader
return { return {
name = "startupscene", name = "startupscene",
enabled = (lovr ~= nil), enabled = (lovr ~= nil),
index = api.ext.max * 0.1,
init = function() end, init = function() end,
@ -45,25 +46,30 @@ return {
-- skybox -- skybox
pass:setCullMode('back') pass:setCullMode('back')
pass:setBlendMode() pass:setBlendMode()
pass:setShader()
pass:setColor(1,1,1,1)
pass:skybox(api.world.skybox) pass:skybox(api.world.skybox)
pass:setColor(1, 1, 1) pass:setColor(1, 1, 1)
pass:setMaterial(envTex) pass:setMaterial(envTex)
pass:setDepthWrite(false)
pass:draw(envTex, 0, 0, 0, 512, math.pi * 0.5, 1, 0, 0) pass:draw(envTex, 0, 0, 0, 512, math.pi * 0.5, 1, 0, 0)
-- floor -- floor
--pass:setMaterial() pass:setMaterial()
pass:setCullMode('front') pass:setDepthWrite(true)
pass:setBlendMode('alpha') pass:setShader()
--pass:setCullMode('front')
--pass:setBlendMode('alpha')
pass:setMaterial() pass:setMaterial()
pass:setColor(0.8,0.8,0.8,1) pass:setColor(0.8,0.8,0.8,1)
pass:plane(0, 0, 0, 500, 500, math.pi * 0.5, 1, 0, 0, "fill", 5, 5) pass:plane(0, 0, 0, 500, 500, math.pi * 0.5, 1, 0, 0, "fill", 5, 5)
pass:setColor( 0, 0, 0, 0.75 ) pass:setColor( 0, 0, 0, 0.75 )
pass:plane(0, 0.01, 0, 500, 500, math.pi * 0.5, 1, 0, 0, "line", 100, 100) pass:plane(0, 0.01, 0, 500, 500, math.pi * 0.5, 1, 0, 0, "line", 100, 100)
pass:setShader( lovr.shader.pbr ) --pass:setShader( lovr.shader.pbr )
pass:send('cubemap', api.world.environmentMap) --pass:send('cubemap', api.world.environmentMap)
pass:send('sphericalHarmonics', api.world.sphericalHarmonics) --pass:send('sphericalHarmonics', api.world.sphericalHarmonics)
end end
end end

View file

@ -84,7 +84,7 @@ unixy = {
stderr_file = stderr_file, stderr_file = stderr_file,
callback = callback callback = callback
}) })
trace("[unixy] start job with pid " .. pid .. " => 1> " .. stdout_file .. " 2> " .. stderr_file) trace("[unixy] start job with pid " .. pid .. ": " .. cmd)
end, end,
check_jobs = function() check_jobs = 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. scale[3] * (d + 0.01) -- planes e.g.
) )
print("making node " .. node['name'] .. " clickable: " .. node.extras.href ) 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
end end

View file

@ -11,6 +11,7 @@ xrfimpl = {
init = function() end, init = function() end,
on3DFile = function(obj) on3DFile = function(obj)
local referer = obj.URI.url
if lovr ~= nil then if lovr ~= nil then
xrf.traverseNodesContaining('href', obj, xrf.traverseNodesContaining('href', obj,
xrf.makeClickable( api.ecs.worldPhysics, xrf.makeClickable( api.ecs.worldPhysics,
@ -18,10 +19,9 @@ xrfimpl = {
local href = obj.node.extras.href local href = obj.node.extras.href
trace("\n[xrf] href was clicked: " .. href) trace("\n[xrf] href was clicked: " .. href)
api.ext.exec("onClick", obj, collider) api.ext.exec("onClick", obj, collider)
local ok = xrf.level2.load( href, obj, api.ext.URI.current, xrfimpl.onLoad) local ok = xrf.level2.load( href, obj, api.ext.URI.current, xrfimpl.onLoad)
or or
xrf.level4.import( href, obj, api.ext.URI.current, xrfimpl.onImport) xrf.level4.import( href, obj, referer, xrfimpl.onImport)
end end
) )
@ -51,10 +51,12 @@ xrfimpl = {
local filter = function(o) local filter = function(o)
return (o.URL ~= nil and o.URL.URN:gsub("#.*","") == absoluteHref:gsub("#.*","")) return (o.URL ~= nil and o.URL.URN:gsub("#.*","") == absoluteHref:gsub("#.*",""))
end end
local obj = api.world.get(filter) local newobj = api.world.get(filter)
if obj then if newobj then
obj.URL = url.parse(absoluteHref) -- re-parse to detect media fragment change if newobj.URL.audio ~= nil then
newobj.URL = url.parse(absoluteHref) -- re-parse to detect media fragment change
xrfimpl.onAudioFile(obj) xrfimpl.onAudioFile(obj)
end
else else
api.ecs.add( api.world, { URI = { url = absoluteHref, method = 'GET', target = obj } }) api.ecs.add( api.world, { URI = { url = absoluteHref, method = 'GET', target = obj } })
end 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 system.modified = true
local tmpEntity = ses[#ses] local tmpEntity = ses[#ses]
ses[index] = tmpEntity ses[index] = tmpEntity
if tmpEntity ~= nil then
seis[tmpEntity] = index seis[tmpEntity] = index
end
seis[entity] = nil seis[entity] = nil
ses[#ses] = nil ses[#ses] = nil
local onRemove = system.onRemove 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 return node
end end
return {
newNode = newNode,
newParser = newParser
}

View file

@ -9,11 +9,11 @@
-- ./lovr xurfer https://janusxr.org/index.html' -- ./lovr xurfer https://janusxr.org/index.html'
-- ./lovr xurfer https://snips.sh/f/rHFLg-cewi?r=1' -- cube -- ./lovr xurfer https://snips.sh/f/rHFLg-cewi?r=1' -- cube
-- ./lovr xurfer https://snips.sh/f/_U5-XctEVE?r=1' -- cube, monkey, scene -- ./lovr xurfer https://snips.sh/f/_U5-XctEVE?r=1' -- cube, monkey, scene
--
print("\n X U R F Ξ R") print("\n X U R F Ξ R")
print(" ⠭ ⠥ ⠗ ⠋ ⠑ ⠗\n") print(" ⠭ ⠥ ⠗ ⠋ ⠑ ⠗\n")
package.path = package.path .. ';' .. '?.lua;' package.path = package.path .. ';' .. '?.lua'
require('conf') require('conf')
local runtime = require("runtime/detect") local runtime = require("runtime/detect")
@ -31,20 +31,21 @@ api = {
ecs = ecs, ecs = ecs,
world = ecs.world(), world = ecs.world(),
protocol = {}, protocol = {},
player = { x = 0, y = 0, z = 0, ox = 0, oy = 1, oz = 0, oangle = 0, flying = true },
ext = { -- all extensions are loaded here from disk at runtime ext = { -- all extensions are loaded here from disk at runtime
ecs = ecs.extension,
max = 10000, -- max plugins (used for sorting)
exec = function(...) util.exec(api.ext,...) end -- util function to call func on each extension exec = function(...) util.exec(api.ext,...) end -- util function to call func on each extension
} },
i = 1
} }
api = util.merge( api, runtime.api ) -- overlay api onto lovr/love-compatible runtime api api = util.merge( api, runtime.api ) -- overlay api onto lovr/love-compatible runtime api
util.loaddir( "ext", api, api.ext )
require( runtime.path .. "/main") require( runtime.path .. "/main")
util.loaddir( "ext", api, api.ext ) api.ext.exec('init', api)
util.loaddir( "media", api, api.media )
ecs.init(api)
api.ext.exec('init')
-- load urls passed on the cli -- load urls passed on the cli
foreach( arg, function(k,uri) foreach( arg, function(k,uri)

View file

@ -1,5 +1,31 @@
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;' ..
lovr.filesystem.setRequirePath(
lovr.filesystem.getRequirePath() .. ";" ..
'runtime/lovr/?.lua;' ..
'runtime/lovr/?/init.lua;' ..
'lib/?.lua;' ..
'?.lua'
)
print(lovr.filesystem.getRequirePath())
local util = require("util") local util = require("util")
local launch = require "launch" local launch = require "runtime/lovr/launch"
local function isVR() local function isVR()
return launch.mode == "vr" or util.match(arg,"--desktop") == false return launch.mode == "vr" or util.match(arg,"--desktop") == false

View file

@ -5,6 +5,8 @@ end
local util = require "util" local util = require "util"
local launch = require "launch" local launch = require "launch"
require('shaders').create(api)
local iui = require "lib.iui" local iui = require "lib.iui"
local backend = require "lib.lovr-iui" local backend = require "lib.lovr-iui"
@ -26,9 +28,30 @@ local ecs = api.ecs
ecs.filterDraw = ecs.requireAll('drawthread') ecs.filterDraw = ecs.requireAll('drawthread')
ecs.filterUpdate = ecs.requireAll('updatethread') ecs.filterUpdate = ecs.requireAll('updatethread')
-- sort drawcalls of extensions
api.ext.lovr = {
drawcalls = {},
enabled = true,
name = "lovr",
index = api.ext.max-1,
init = function()
api.ext.lovr.sortDrawCalls()
end,
sortDrawCalls = function()
local drawcalls = api.ext.lovr.drawcalls
foreach( api.ext, function(k,v)
if type(v) == 'table' and v.draw ~= nil then
table.insert(drawcalls, v)
end
end)
table.sort( drawcalls, function(a,b) return a.index < b.index end)
end
}
-- ecs systems -- ecs systems
local interactionsUpdater local interactions
--- @type Texture --- @type Texture
local envTex local envTex
@ -38,8 +61,7 @@ local mainWindow
function lovr.load() function lovr.load()
if launch.mode == "desktop" then if launch.mode == "desktop" then
backend.mouse = require "lib.lovr-mouse" --backend.mouse = require "lib.lovr-mouse"
lovr.system.setKeyRepeat(true) lovr.system.setKeyRepeat(true)
end end
@ -48,7 +70,10 @@ function lovr.load()
api.ext.exec("load") api.ext.exec("load")
if iui.idiom == "vr" then if iui.idiom == "vr" then
lovr.headset.setPassthrough("opaque") local modes = lovr.headset.getPassthroughModes()
local mode = "opaque"
if modes.blend then mode = "blend" end
lovr.headset.setPassthrough( mode )
mainWindow = backend.worldWindow.new() mainWindow = backend.worldWindow.new()
api.mainWindow = mainWindow api.mainWindow = mainWindow
@ -64,7 +89,7 @@ function lovr.update(dt)
-- In desktop mode, we use IUI's standard window API to fill the screen. -- In desktop mode, we use IUI's standard window API to fill the screen.
iui.beginWindow(lovr.system.getWindowDimensions()) iui.beginWindow(lovr.system.getWindowDimensions())
api.ext.exec("update", dt) api.ext.exec("update", dt, interactions.event )
iui.endWindow() iui.endWindow()
@ -74,7 +99,7 @@ function lovr.update(dt)
if mainWindow:beginFrame() then if mainWindow:beginFrame() then
iui.beginWindow(mainWindow.w, mainWindow.h) iui.beginWindow(mainWindow.w, mainWindow.h)
api.ext.exec("update", dt) api.ext.exec("update", dt, interactions.event )
iui.endWindow() iui.endWindow()
@ -86,120 +111,152 @@ function lovr.update(dt)
end end
function lovr.draw(pass) function lovr.draw(pass)
--api.player.matrix:setPosition( api.player.x, api.player.y, api.player.z )
--api.player.matrix.setOrientation( api.player.x, api.player.y, api.player.z )
pass:transform(mat4(api.player.matrix):invert())
if iui.idiom == "desktop" then if iui.idiom == "desktop" then
backend.graphics.pass = pass backend.graphics.pass = pass
iui.draw() iui.draw()
end end
if api.iui.idiom == "vr" then if api.iui.idiom == "vr" then
pass:setClear(0.5, 0.5, 0.5) pass:setClear(0.2, 0.2, 0.2)
end end
api.ext.exec("draw",pass) -- call draw() function of extensions (sorted by .index)
ecs.update( api.world, pass, ecs.filterDraw ) foreach( api.ext.lovr.drawcalls, function(k,ext) ext.draw(pass) end)
-- Dot
if interactionsUpdater.selectedBox then
pass:setColor(0, 0, 1)
pass:sphere( interactionsUpdater.hitpoint, .01)
end
-- Laser pointer -- -- Dot
local hand = vec3(lovr.headset.getPosition('hand/left/point')) --if interactions.selectedBox then
local direction = quat(lovr.headset.getOrientation('hand/left/point')):direction() -- pass:setColor(0, 0, 1)
pass:setColor(1, 1, 1) -- pass:sphere( interactions.hitpoint, .01)
pass:line(hand, interactionsUpdater.selectedBox and interactionsUpdater.hitpoint or (hand + direction * 50)) --end
--
---- Laser pointers
--local hands = {"left","right"}
--foreach( hands, function(k,side)
-- local hand = vec3(lovr.headset.getPosition('hand/' .. side .. '/point'))
-- local direction = quat(lovr.headset.getOrientation('hand/' .. side .. '/point')):direction()
-- pass:setColor(1, 1, 1)
-- pass:line(hand, interactions.selectedBox and interactions.hitpoint or (hand + direction * 50))
--end)
if api.mainWindow then --if api.mainWindow then
api.mainWindow:draw(pass) -- api.mainWindow:draw(pass)
end --end
return false return false
end end
function lovr.recenter() function lovr.recenter()
if iui.idiom == "vr" then if iui.idiom == "vr" and lovr.headset.isActive() then
mainWindow:recenter() mainWindow:recenter()
end end
end end
if launch.mode == "desktop" then
function lovr.mousemoved(x, y, dx, dy) function lovr.mousemoved(x, y, dx, dy)
backend.mousemoved(x, y, dx, dy) backend.mousemoved(x, y, dx, dy)
interactionsUpdater.mouse.moved = {x=x, y=y} interactions.pointer.moved = {x=x, y=y}
end end
function lovr.mousepressed(x, y, button) function lovr.mousepressed(x, y, button)
backend.mousepressed(x, y, button) backend.mousepressed(x, y, button)
interactionsUpdater.mouse.pressed = {x=x, y=y, button=button} interactions.pointer.pressed = {x=x, y=y, button=button}
end end
function lovr.mousereleased(x, y, button) function lovr.mousereleased(x, y, button)
backend.mousereleased(x, y, button) backend.mousereleased(x, y, button)
interactionsUpdater.mouse.released = {x=x, y=y, button=button} interactions.pointer.released = {x=x, y=y, button=button}
print("mouserelease")
end end
function lovr.wheelmoved(x, y) function lovr.wheelmoved(x, y)
backend.wheelmoved(x, y) backend.wheelmoved(x, y)
interactionsUpdater.mouse.wheel = {x=x, y=y} interactions.pointer.wheel = {x=x, y=y}
print_r(interactions.pointer.wheel)
end end
function lovr.keypressed(key, scancode, isRepeat) function lovr.keypressed(key, scancode, isRepeat)
backend.keypressed(key, scancode, isRepeat) backend.keypressed(key, scancode, isRepeat)
interactionsUpdater.key.pressed = {key=key,scancode=scancode,isRepeat=isRepeat} interactions.key.pressed = {key=key,scancode=scancode,isRepeat=isRepeat}
end end
function lovr.keyreleased(key, scancode) function lovr.keyreleased(key, scancode)
backend.keyreleased(key, scancode) backend.keyreleased(key, scancode)
interactionsUpdater.key.released = {key=key,scancode=scancode} interactions.key.released = {key=key,scancode=scancode}
end end
function lovr.textinput(text) function lovr.textinput(text)
backend.textinput(text) backend.textinput(text)
interactionsUpdater.input = text interactions.input = text
end
end end
function initInteractions(ecs) function initInteractions(ecs)
ecs.worldPhysics = lovr.physics.newWorld(0, 0, 0) ecs.worldPhysics = lovr.physics.newWorld(0, 0, 0)
interactionsUpdater = ecs.processingSystem() interactions = ecs.processingSystem()
interactionsUpdater.updatethread = true interactions.updatethread = true
interactionsUpdater.filter = function(obj) return true end -- always run interactions.filter = function(obj) return true end -- always run
-- collider vars -- collider vars
interactionsUpdater.mouse = { released = false, pressed = false, moved = false, wheel = false} interactions.pointer = { released = false, pressed = false, moved = false, hover = false, wheel = false}
interactionsUpdater.key = { released = false, pressed = false } interactions.key = { released = false, pressed = false }
interactionsUpdater.input = "" interactions.input = ""
interactionsUpdater.hitpoint = lovr.math.newVec3() interactions.hitpoint = vector()
interactionsUpdater.selectedBox = nil interactions.selectedBox = nil
interactionsUpdater.clear = function(o) foreach( o, function(k,v) o[k] = false end ) end interactions.clearTable = function(o) foreach( o, function(k,v) o[k] = false end ) end
interactions.clear = function()
interactions.clearTable( interactions.pointer )
interactions.clearTable( interactions.key )
interactions.input = false
end
function interactionsUpdater:process(obj, dt) function interactions:process(obj, dt)
ecs.worldPhysics:update(dt) ecs.worldPhysics:update(dt)
local ox, oy, oz = lovr.headset.getPosition('hand/left/point') local castsrc = 'hand/left/point'
local dx, dy, dz = quat(lovr.headset.getOrientation('hand/left/point')):direction():mul(50):unpack() local castsrc = 'head'
local ox, oy, oz = lovr.headset.getPosition(castsrc)
local direction = quat(lovr.headset.getOrientation(castsrc)):direction()
if direction ~= nil then
local dx, dy, dz = vector( direction * 50 ):unpack()
local collider, shape, x, y, z = ecs.worldPhysics:raycast(ox, oy, oz, ox + dx, oy + dy, oz + dz) local collider, shape, x, y, z = ecs.worldPhysics:raycast(ox, oy, oz, ox + dx, oy + dy, oz + dz)
if collider then if collider then
interactions.pointer.hover = true
local node = collider:getUserData()
interactions.selectedBox = collider
interactions.hitpoint.x = x
interactions.hitpoint.y = y
interactions.hitpoint.z = z
for i, hand in ipairs(lovr.headset.getHands()) do for i, hand in ipairs(lovr.headset.getHands()) do
if lovr.headset.isDown(hand, 'trigger') then if lovr.headset.isDown(hand, 'trigger') then
print("buttondown") interactions.pointer.pressed = true
end end
if lovr.headset.wasReleased(hand, 'trigger') or interactionsUpdater['mouse']['released'] then if lovr.headset.wasReleased(hand, 'trigger') then
local node = collider:getUserData() interactions.pointer.released = true
interactionsUpdater.selectedBox = collider end
interactionsUpdater.hitpoint:set(x, y, z) end
if node.onclick ~= nil then node.onclick(node,collider) end if interactions.pointer.released and node.onclick ~= nil then
node.onclick(node,collider)
end end
end end
end end
-- reset stuff -- backup & reset stuff
interactionsUpdater.clear( interactionsUpdater.mouse ) interactions.event = {
interactionsUpdater.clear( interactionsUpdater.key ) input = util.clone(interactions.input),
interactionsUpdater.input = false pointer = util.clone(interactions.pointer),
key = util.clone(interactions.key),
hitpoint = { x = interactions.hitpoint.x, y = interactions.hitpoint.y, z = interactions.z },
--head = { x = ox, y = oy, z = oz, direction = direction},
collider = collider
}
-- clear interactions
interactions.clear()
end end
ecs.addSystem( api.world, interactionsUpdater ) ecs.addSystem( api.world, interactions )
end end
function initCleanups(ecs) function initCleanups(ecs)

View file

@ -7,8 +7,6 @@ modelRenderer = {
init = function(api) init = function(api)
local ecs = api.ecs local ecs = api.ecs
api.world.shader = { pbr = false } api.world.shader = { pbr = false }
lovr.shader = lovr.shader or {}
lovr.shader['pbr'] = modelRenderer.initShaderPBR()
renderer = ecs.processingSystem() renderer = ecs.processingSystem()
renderer.filter = ecs.requireAll('model') renderer.filter = ecs.requireAll('model')
@ -36,38 +34,6 @@ modelRenderer = {
end end
end end
ecs.addSystem( api.world, renderer ) ecs.addSystem( api.world, renderer )
end,
initShaderPBR = function()
return lovr.graphics.newShader([[
vec4 lovrmain() {
return DefaultPosition;
}
]], [[
uniform textureCube cubemap;
uniform sphericalHarmonics { vec3 sh[9]; };
vec4 lovrmain() {
Surface surface;
initSurface(surface);
vec3 color = vec3(0);
vec3 lightDirection = vec3(-1, -1, -1);
vec4 lightColorAndBrightness = vec4(1, 1, 1, 3);
float visibility = 1.;
color += getLighting(surface, lightDirection, lightColorAndBrightness, visibility);
color += getIndirectLighting(surface, cubemap, sh);
return vec4(color, 1);
}
]], {
flags = {
glow = true,
normalMap = true,
vertexTangents = false, -- DamagedHelmet doesn't have vertex tangents
tonemap = true
}
})
end end
} }

View file

@ -0,0 +1,34 @@
return {
create = function(api)
lovr.shader = {}
lovr.shader.pbr = lovr.graphics.newShader([[
vec4 lovrmain() {
return DefaultPosition;
}
]], [[
uniform textureCube cubemap;
uniform sphericalHarmonics { vec3 sh[9]; };
vec4 lovrmain() {
Surface surface;
initSurface(surface);
vec3 color = vec3(0);
vec3 lightDirection = vec3(-1, -1, -1);
vec4 lightColorAndBrightness = vec4(1, 1, 1, 3);
float visibility = 1.;
color += getLighting(surface, lightDirection, lightColorAndBrightness, visibility);
color += getIndirectLighting(surface, cubemap, sh);
return vec4(color, 1);
}
]], {
flags = {
glow = true,
normalMap = true,
vertexTangents = false, -- DamagedHelmet doesn't have vertex tangents
tonemap = true
}
})
end
}

View file

@ -11,13 +11,6 @@ api.ext.lua = {
api.ecs.filterUpdate = api.ecs.requireAll('updatethread') api.ecs.filterUpdate = api.ecs.requireAll('updatethread')
api.ecs.filterDraw = api.ecs.requireAll('drawthread') api.ecs.filterDraw = api.ecs.requireAll('drawthread')
api.protocol.http.request( "https://2wa.isvery.ninja", {}, function(status,data,headers)
print("request done")
--print_r(status)
--print_r(headers)
--print_r(data)
end)
while true do while true do
local current_time = os.clock() local current_time = os.clock()
local dt = current_time - last_time local dt = current_time - last_time
@ -38,11 +31,11 @@ api.ext.lua = {
-- via wget -- via wget
api.protocol.http.request = api.ext.unixy.proxyCLI( api.protocol.http.request = api.ext.unixy.proxyCLI(
"wget", "wget",
function(url,opts, cb) return string.format( "out=$(mktemp); wget -O $out https://2wa.isvery.ninja; cat $out; rm $out", url ) end, function(url,opts, cb) return string.format( "out=$(mktemp); wget -O $out %s; cat $out; rm $out", url ) end,
function( stdout, stderr, retcode, url, opts, cb) function( stdout, stderr, retcode, url, opts, cb)
local status = 200 local status = 200
if retcode ~= 0 then status = retcode end if retcode ~= 'exit' then status = 500 end
cb( retcode, stdout, {string = stderr}) -- todo: parse string into key/values cb( status, stdout, {string = stderr, retcode = retcode})
end end
) )
@ -52,8 +45,8 @@ api.ext.lua = {
function(url,opts, cb) return string.format( "curl -v -s %s", url ) end, function(url,opts, cb) return string.format( "curl -v -s %s", url ) end,
function( stdout, stderr, retcode, url, opts, cb) function( stdout, stderr, retcode, url, opts, cb)
local status = 200 local status = 200
if retcode ~= 0 then status = retcode end if retcode ~= 'exit' then status = 500 end
cb( retcode, stdout, {string = stderr}) -- todo: parse string into key/values cb( status, stdout, {string = stderr, retcode = retcode})
end end
) )

1
xurfer/test.localhost.jml0 Symbolic link
View file

@ -0,0 +1 @@
../test.localhost.jml0

View file

@ -1,5 +1,13 @@
local util = { ext = {} } 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) util.loaddir = function( path, api, target)
local dir = api.filesystem.getDirectoryItems(path) local dir = api.filesystem.getDirectoryItems(path)
foreach( dir, function(k,v) foreach( dir, function(k,v)
@ -7,10 +15,26 @@ util.loaddir = function( path, api, target)
if api.filesystem.isFile(ext) then if api.filesystem.isFile(ext) then
print("[i] loading '" .. v .. "'") print("[i] loading '" .. v .. "'")
target[ v ] = api.filesystem.load( ext )(api) target[ v ] = api.filesystem.load( ext )(api)
if target[ v ].index == nil then target[v].index = api.ext.max / 2 end
end end
end) end)
end end
util.clone = function(orig)
local orig_type = type(orig)
local copy
if orig_type == 'table' then
copy = {}
for orig_key, orig_value in next, orig, nil do
copy[util.clone(orig_key)] = util.clone(orig_value)
end
setmetatable(copy, util.clone(getmetatable(orig)))
else -- number, string, boolean, etc
copy = orig
end
return copy
end
util.cmdExist = function(cmd) util.cmdExist = function(cmd)
if api.runtime.os == 'unixy' then cmd = cmd .. " 1>/dev/null 2>/dev/null" end if api.runtime.os == 'unixy' then cmd = cmd .. " 1>/dev/null 2>/dev/null" end
local stdout, retstr, retcode = os.execute(cmd) local stdout, retstr, retcode = os.execute(cmd)
@ -153,8 +177,8 @@ end
-- entity.commit() -- this recalculates the filtercache of systems -- entity.commit() -- this recalculates the filtercache of systems
util.commit = function(world, obj, api) util.commit = function(world, obj, api)
return function(extCb) return function(extCb)
for j = 1, #world.entities do for i = 1, #world.systems do
local system = world.systems[j] local system = world.systems[i]
system.entities = {} system.entities = {}
for j = 1, #world.entities do for j = 1, #world.entities do
local entity = world.entities[j] local entity = world.entities[j]
@ -190,9 +214,9 @@ end
-- LUA SYNTAX SUGAR (from here on ) -- LUA SYNTAX SUGAR (from here on )
-- --
function foreach(table, f) function foreach(t, f, sort)
if table ~= nil then if t ~= nil then
for k, v in pairs(table) do f(k, v) end for k, v in pairs(t) do f(k, v) end
end end
end end