diff --git a/src/conf.lua b/src/conf.lua index ac743fb..838b6bd 100644 --- a/src/conf.lua +++ b/src/conf.lua @@ -1,14 +1,15 @@ -- sync lovr/love positional cli arguments if arg[0] == nil and arg[1] ~= nil then arg[0] = arg[1] end -package.path = package.path .. ';' .. - arg[0] .. '/lovr/?.lua;' .. - arg[0] .. '/lovr/?/init.lua;' .. - arg[0] .. '/lib/?.lua' - local runtime = "" if lovr ~= nil then runtime = "lovr" end if love ~= nil then runtime = "love" end +package.path = package.path .. ';' .. + arg[0] .. '/' .. runtime .. '/?.lua;' .. + arg[0] .. '/' .. runtime .. '/?/init.lua;' .. + arg[0] .. '/lib/?.lua' + + require( runtime .. "/conf") diff --git a/src/ecs.lua b/src/ecs.lua index dc13dc8..8acd3f8 100644 --- a/src/ecs.lua +++ b/src/ecs.lua @@ -1,37 +1,14 @@ -local api = ... local ecs = require("tiny-ecs") -ecs.initers = { - renderer = function() - local renderer = ecs.processingSystem() - renderer.filter = ecs.requireAll('type') -- *TODO* might need filter later - function renderer:process(obj, pass) - local type = obj:type() - if type == "Model" then - pass:setMaterial() - pass:setCullMode('none') - pass:draw(obj, 0, 0, 0, 1, 0, 1, 0, 0, 1) - end - end - ecs.addSystem( ecs.world, renderer ) - end -} - -ecs.enabled = true -ecs.world = ecs.world() -ecs.world.update = ecs.update -- swap with extension update-func - ecs.init = function() - ecs.initers.renderer() -end - -ecs.update = function(dt) - --ecs.world.update( ecs.world, dt ) -- *TODO* different filter -end - -ecs.draw = function(pass) - local opts = { - ecs.world.update( ecs.world, pass ) + print("[i] loading ecs") + baseEntify = ecs.processingSystem() + baseEntify.filter = ecs.rejectAll('commit') + baseEntify.updatethread = true + function baseEntify:onAdd(obj) + obj.commit = api.util.commit( api.world, api.ecs.requireAll('allowcommit') ) + end + ecs.addSystem( api.world, baseEntify ) end return ecs diff --git a/src/ext/3DFile/main.lua b/src/ext/3DFile/main.lua index cdb362f..3ba9de4 100644 --- a/src/ext/3DFile/main.lua +++ b/src/ext/3DFile/main.lua @@ -5,28 +5,22 @@ return { name = "3DFile", enabled = true, - init = function() - modelLoader = ecs.processingSystem() - modelLoader.updatesystem = true - modelLoader.filter = ecs.requireAll('URL', 'method', 'data', 'ok') - function modelLoader:process(req, dt) - if req.ok and req.model == nil then - if req.URL.extension == 'OBJ' or - req.URL.extension == 'GLB' or - req.URL.extension == 'GLTF' then - - req.model = api.graphics.newModel( api.data.newBlob(req.data) ) - api.world.add({ - x = 0, - y = 0, - z = 0, - model = req.model, - req = req - }) - end + loadasset = function(obj) + if( obj.URL ~= nil and obj.URLResponse ~= nil and + (obj.URL.extension == 'GLB' or + obj.URL.extension == 'GLTF' or + obj.URL.extension == 'OBJ')) then + if obj.URLResponse.ok then + obj.model = api.graphics.newModel( api.data.newBlob( obj.URLResponse.data) ) + obj.root = (obj.URI.target == '_top') + obj.x = 0 + obj.y = 0 + obj.z = 0 + obj.commit() -- notify systems + else + print("[3DFile] error: could not load " .. obj.URL.string ) end end - api.world.add(modelLoader) end } diff --git a/src/ext/URI/main.lua b/src/ext/URI/main.lua new file mode 100644 index 0000000..d86a363 --- /dev/null +++ b/src/ext/URI/main.lua @@ -0,0 +1,42 @@ +local api = ... +local ecs = api.ecs + +return { + name = "URI", + enabled = true, + + init = function() + local urlListener + urlListener = ecs.processingSystem({ + updatethread = true, + filter = ecs.requireAll('URI', ecs.rejectAll('URLResponse') ), + onAdd = function(self,obj) + api.ext.URI.to(obj.URI, nil, obj) + end + }) + ecs.addSystem( api.world, urlListener ) + end, + + to = function(URI,refererer, obj) + obj = obj or {} + print("surfing to " .. URI.url) + obj.URL = api.url.parse(URI.url) + obj.URLResponse = {} + foreach( api.protocol, function(name, p) + if obj.URL.protocol:match("^"..name) then + protocol = p + end + end) + if protocol then + local status, data, headers = protocol.request(URI.url) + obj.URLResponse = { + ok = (status >= 200 and status < 300), + status = status, + data = data, + headers = headers + } + api.exec( api.ext, 'loadasset', obj) + end + end + +} diff --git a/src/ext/browser/main.lua b/src/ext/browser/main.lua deleted file mode 100644 index 9c99a8f..0000000 --- a/src/ext/browser/main.lua +++ /dev/null @@ -1,38 +0,0 @@ -local api = ... -local ecs = api.ecs - -return { - name = "browser", - enabled = true, - - init = function() - urlListener = ecs.processingSystem({ - updatesystem = true, - filter = ecs.requireAll('url', 'method'), - onAdd = function(self,obj) - api.ext.browser.to(obj.url, nil, obj) - end - }) - api.world.add(urlListener) - end, - - to = function(url,refererer, opts) - opts = opts or {method = 'GET' } - print("surfing to " .. url) - local URL = api.url.parse(url) - foreach( api.protocol, function(name, p) - if URL.protocol:match("^"..name) then - protocol = p - end - end) - if protocol then - local status, data, headers = protocol.request(url) - opts.ok = (status >= 200 and status < 300) - opts.status = status - opts.data = data - opts.headers = headers - opts.URL = URL - end - end - -} diff --git a/src/ext/janusxr/main.lua b/src/ext/janusxr/main.lua index acfab83..5798ad1 100644 --- a/src/ext/janusxr/main.lua +++ b/src/ext/janusxr/main.lua @@ -8,7 +8,7 @@ return { init = function() JMLLoader = ecs.processingSystem() - JMLLoader.updatesystem = true + JMLLoader.updatethread = true JMLLoader.filter = ecs.requireAll('URL', 'method', 'data', 'ok') function JMLLoader:process(req, dt) if req.ok and req.xml == nil then @@ -37,7 +37,7 @@ return { end end end - api.world.add(JMLLoader) + ecs.addSystem(api.world,JMLLoader) end, } diff --git a/src/ext/xrfragments/main.lua b/src/ext/xrfragments/main.lua index efc096c..530c888 100644 --- a/src/ext/xrfragments/main.lua +++ b/src/ext/xrfragments/main.lua @@ -9,7 +9,9 @@ return { local ecs = api.ecs xrfsystem = ecs.system({ - filter = ecs.filter('x', 'y', 'z', 'model'), + filter = ecs.requireAll('x', 'y', 'z', 'model'), + nocache = true, + filterDynamic = true, onAdd = function(self,obj) diff --git a/src/lib/lust.lua b/src/lib/lust.lua new file mode 100644 index 0000000..7a5d28a --- /dev/null +++ b/src/lib/lust.lua @@ -0,0 +1,272 @@ +-- lust v0.2.0 - Lua test framework +-- https://github.com/bjornbytes/lust +-- MIT LICENSE + +local lust = {} +lust.level = 0 +lust.passes = 0 +lust.errors = 0 +lust.befores = {} +lust.afters = {} + +local red = string.char(27) .. '[31m' +local green = string.char(27) .. '[32m' +local normal = string.char(27) .. '[0m' +local function indent(level) return string.rep('\t', level or lust.level) end + +function lust.nocolor() + red, green, normal = '', '', '' + return lust +end + +function lust.describe(name, fn) + print(indent() .. name) + lust.level = lust.level + 1 + fn() + lust.befores[lust.level] = {} + lust.afters[lust.level] = {} + lust.level = lust.level - 1 +end + +function lust.it(name, fn) + for level = 1, lust.level do + if lust.befores[level] then + for i = 1, #lust.befores[level] do + lust.befores[level][i](name) + end + end + end + + local success, err = pcall(fn) + if success then lust.passes = lust.passes + 1 + else lust.errors = lust.errors + 1 end + local color = success and green or red + local label = success and 'PASS' or 'FAIL' + print(indent() .. color .. label .. normal .. ' ' .. name) + if err then + print(indent(lust.level + 1) .. red .. tostring(err) .. normal) + end + + for level = 1, lust.level do + if lust.afters[level] then + for i = 1, #lust.afters[level] do + lust.afters[level][i](name) + end + end + end +end + +function lust.before(fn) + lust.befores[lust.level] = lust.befores[lust.level] or {} + table.insert(lust.befores[lust.level], fn) +end + +function lust.after(fn) + lust.afters[lust.level] = lust.afters[lust.level] or {} + table.insert(lust.afters[lust.level], fn) +end + +-- Assertions +local function isa(v, x) + if type(x) == 'string' then + return type(v) == x, + 'expected ' .. tostring(v) .. ' to be a ' .. x, + 'expected ' .. tostring(v) .. ' to not be a ' .. x + elseif type(x) == 'table' then + if type(v) ~= 'table' then + return false, + 'expected ' .. tostring(v) .. ' to be a ' .. tostring(x), + 'expected ' .. tostring(v) .. ' to not be a ' .. tostring(x) + end + + local seen = {} + local meta = v + while meta and not seen[meta] do + if meta == x then return true end + seen[meta] = true + meta = getmetatable(meta) and getmetatable(meta).__index + end + + return false, + 'expected ' .. tostring(v) .. ' to be a ' .. tostring(x), + 'expected ' .. tostring(v) .. ' to not be a ' .. tostring(x) + end + + error('invalid type ' .. tostring(x)) +end + +local function has(t, x) + for k, v in pairs(t) do + if v == x then return true end + end + return false +end + +local function eq(t1, t2, eps) + if type(t1) ~= type(t2) then return false end + if type(t1) == 'number' then return math.abs(t1 - t2) <= (eps or 0) end + if type(t1) ~= 'table' then return t1 == t2 end + for k, _ in pairs(t1) do + if not eq(t1[k], t2[k], eps) then return false end + end + for k, _ in pairs(t2) do + if not eq(t2[k], t1[k], eps) then return false end + end + return true +end + +local function stringify(t) + if type(t) == 'string' then return "'" .. tostring(t) .. "'" end + if type(t) ~= 'table' or getmetatable(t) and getmetatable(t).__tostring then return tostring(t) end + local strings = {} + for i, v in ipairs(t) do + strings[#strings + 1] = stringify(v) + end + for k, v in pairs(t) do + if type(k) ~= 'number' or k > #t or k < 1 then + strings[#strings + 1] = ('[%s] = %s'):format(stringify(k), stringify(v)) + end + end + return '{ ' .. table.concat(strings, ', ') .. ' }' +end + +local paths = { + [''] = { 'to', 'to_not' }, + to = { 'have', 'equal', 'be', 'exist', 'fail', 'match' }, + to_not = { 'have', 'equal', 'be', 'exist', 'fail', 'match', chain = function(a) a.negate = not a.negate end }, + a = { test = isa }, + an = { test = isa }, + be = { 'a', 'an', 'truthy', + test = function(v, x) + return v == x, + 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to be the same', + 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to not be the same' + end + }, + exist = { + test = function(v) + return v ~= nil, + 'expected ' .. tostring(v) .. ' to exist', + 'expected ' .. tostring(v) .. ' to not exist' + end + }, + truthy = { + test = function(v) + return v, + 'expected ' .. tostring(v) .. ' to be truthy', + 'expected ' .. tostring(v) .. ' to not be truthy' + end + }, + equal = { + test = function(v, x, eps) + local comparison = '' + local equal = eq(v, x, eps) + + if not equal and (type(v) == 'table' or type(x) == 'table') then + comparison = comparison .. '\n' .. indent(lust.level + 1) .. 'LHS: ' .. stringify(v) + comparison = comparison .. '\n' .. indent(lust.level + 1) .. 'RHS: ' .. stringify(x) + end + + return equal, + 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to be equal' .. comparison, + 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to not be equal' + end + }, + have = { + test = function(v, x) + if type(v) ~= 'table' then + error('expected ' .. tostring(v) .. ' to be a table') + end + + return has(v, x), + 'expected ' .. tostring(v) .. ' to contain ' .. tostring(x), + 'expected ' .. tostring(v) .. ' to not contain ' .. tostring(x) + end + }, + fail = { 'with', + test = function(v) + return not pcall(v), + 'expected ' .. tostring(v) .. ' to fail', + 'expected ' .. tostring(v) .. ' to not fail' + end + }, + with = { + test = function(v, pattern) + local ok, message = pcall(v) + return not ok and message:match(pattern), + 'expected ' .. tostring(v) .. ' to fail with error matching "' .. pattern .. '"', + 'expected ' .. tostring(v) .. ' to not fail with error matching "' .. pattern .. '"' + end + }, + match = { + test = function(v, p) + if type(v) ~= 'string' then v = tostring(v) end + local result = string.find(v, p) + return result ~= nil, + 'expected ' .. v .. ' to match pattern [[' .. p .. ']]', + 'expected ' .. v .. ' to not match pattern [[' .. p .. ']]' + end + } +} + +function lust.expect(v) + local assertion = {} + assertion.val = v + assertion.action = '' + assertion.negate = false + + setmetatable(assertion, { + __index = function(t, k) + if has(paths[rawget(t, 'action')], k) then + rawset(t, 'action', k) + local chain = paths[rawget(t, 'action')].chain + if chain then chain(t) end + return t + end + return rawget(t, k) + end, + __call = function(t, ...) + if paths[t.action].test then + local res, err, nerr = paths[t.action].test(t.val, ...) + if assertion.negate then + res = not res + err = nerr or err + end + if not res then + error(err or 'unknown failure', 2) + end + end + end + }) + + return assertion +end + +function lust.spy(target, name, run) + local spy = {} + local subject + + local function capture(...) + table.insert(spy, {...}) + return subject(...) + end + + if type(target) == 'table' then + subject = target[name] + target[name] = capture + else + run = name + subject = target or function() end + end + + setmetatable(spy, {__call = function(_, ...) return capture(...) end}) + + if run then run() end + + return spy +end + +lust.test = lust.it +lust.paths = paths + +return lust diff --git a/src/lovr/main.lua b/src/lovr/main.lua index a2824e4..1785121 100644 --- a/src/lovr/main.lua +++ b/src/lovr/main.lua @@ -16,6 +16,8 @@ api.backend = backend -- decorate api api.protocol.http = require('http') local ecs = api.ecs +ecs.filterDraw = ecs.requireAll('drawthread') +ecs.filterUpdate = ecs.requireAll('updatethread') -- ecs systems @@ -48,11 +50,8 @@ function lovr.load() end function lovr.update(dt) - if lovr.system.isKeyDown("escape") then - lovr.event.quit() - end - ecs.update( api.world, dt, ecs.filter('updatesystem') ) + ecs.update( api.world, dt, ecs.filterUpdate ) iui.beginFrame(dt) @@ -92,7 +91,7 @@ function lovr.draw(pass) end api.exec(api.ext, "draw",pass) - ecs.update( api.world, pass, ecs.filter('drawsystem') ) + ecs.update( api.world, pass, ecs.filterDraw ) -- Dot if selectedBox then @@ -154,12 +153,12 @@ if launch.mode == "desktop" then end local initECS = function(ecs) - -- lovr.draw => ecs.drawsystem (render-logic thread) + -- lovr.draw => ecs.draw (render-logic thread) renderer = ecs.processingSystem() - renderer.filter = ecs.requireAny('model') - renderer.drawsystem = true + renderer.filter = ecs.requireAll('model') + renderer.drawthread = true function renderer:process(obj, pass) - if obj['model'] ~= nil then + if obj.model ~= nil and obj.root ~= nil then pass:setMaterial() pass:setCullMode('none') pass:draw( @@ -172,22 +171,13 @@ local initECS = function(ecs) end ecs.addSystem( api.world, renderer ) - -- lovr.update => ecs updatesystem (game-logic thread) - local updater = ecs.processingSystem() - updater.updatesystem = true - updater.filter = ecs.requireAll('update') - function updater:process(obj, dt) - print_r(obj['data']) - end - ecs.addSystem( api.world, updater ) - end function initInteractions(ecs) ecs.worldPhysics = lovr.physics.newWorld(0, 0, 0) interactionsUpdater = ecs.processingSystem() - interactionsUpdater.updatesystem = true + interactionsUpdater.updatethread = true interactionsUpdater.filter = ecs.requireAll('collider') -- collider vars interactionsUpdater.mouse = { released = false} diff --git a/src/main.lua b/src/main.lua index 164a677..f8ab809 100644 --- a/src/main.lua +++ b/src/main.lua @@ -1,5 +1,5 @@ local util = require("util") -local ecs = require("tiny-ecs") +local ecs = require("ecs") api = { parser = { @@ -15,10 +15,6 @@ api = { exec = function(...) util.exec(...) end -- calls function on each table item } --- convenience wrappers -api.world.add = function(...) api.ecs.add( api.world, ...) end -api.world.remove = function(...) api.ecs.remove( api.world, ...) end - local runtime local runtimepath @@ -26,11 +22,13 @@ if lovr ~= nil then runtime = { path = "lovr", api = lovr } end if love ~= nil then runtime = { path = "love", api = love } end api = util.merge( api, runtime.api ) + +require( runtime.path .. "/main") + util.loaddir( "ext", api, api.ext ) util.loaddir( "media", api, api.media ) - +ecs.init() api.exec( api.ext, 'init') -require( runtime.path .. "/main") local url='https://coderofsalvation.codeberg.page/xrfragment-haxe/example/assets/example.glb?bar=1&f=2#foo' --local url = 'https://codeberg.org/coderofsalvation/xrfragment/raw/branch/main/assets/template/website/website.glb' @@ -38,4 +36,4 @@ local url='https://coderofsalvation.codeberg.page/xrfragment-haxe/example/assets --local url = 'https://janusxr.org/index.html' -api.world.add({ url = url, method = 'GET' }) +api.ecs.add( api.world, { URI = { url = url, method = 'GET', target = '_top' } }) diff --git a/src/test/main.lua b/src/test/main.lua new file mode 100644 index 0000000..5627be4 --- /dev/null +++ b/src/test/main.lua @@ -0,0 +1,19 @@ +local lust = require 'lust' +local describe, it, expect = lust.describe, lust.it, lust.expect + +describe('my project', function() + lust.before(function() + -- This gets run before every test. + end) + + describe('module1', function() -- Can be nested + it('feature1', function() + expect(1).to.be.a('number') -- Pass + expect('astring').to.equal('astring') -- Pass + end) + + it('feature2', function() + expect(nil).to.exist() -- Fail + end) + end) +end) diff --git a/src/util.lua b/src/util.lua index 200bc66..7ee6bf5 100644 --- a/src/util.lua +++ b/src/util.lua @@ -151,7 +151,6 @@ function print_r(t) print( util.dump(t) ) end - util.match = function(list,k,v) local ret = false foreach( list, function(K,V) @@ -160,4 +159,22 @@ util.match = function(list,k,v) return ret end +-- recalculate filters for tiny-ecs systems +-- usage: entity.commit = util.commit(api.world) -- once +-- entity.commit() -- this recalculates the filtercache of systems +util.commit = function(world) + return function() + for j = 1, #world.entities do + local system = world.systems[j] + system.entities = {} + for j = 1, #world.entities do + local entity = world.entities[j] + if system.filter(system, entity) then + table.insert( system.entities, entity) + end + end + end + end +end + return util