From 3226ef155357283cefdaab1fc1f9b49319295289 Mon Sep 17 00:00:00 2001 From: Alex Rodin Date: Thu, 14 Oct 2021 19:17:15 -0300 Subject: [PATCH] Versioning --- ECS.lua | 9 +- ECS_concat.lua | 293 ++++++++++++++++++++++++++-------------- build.lua | 7 +- src/Component.lua | 126 +++++++++-------- src/ECS.lua | 5 +- src/Entity.lua | 91 +++++++++++-- test/test_Component.lua | 6 +- test/test_Entity.lua | 174 ++++++++++++++++++++++++ 8 files changed, 530 insertions(+), 181 deletions(-) diff --git a/ECS.lua b/ECS.lua index 4b4b414..9721897 100644 --- a/ECS.lua +++ b/ECS.lua @@ -1,10 +1,9 @@ --[[ - ECS-Lua v2.0.0 [2021-10-02 17:25] + ECS Lua v2.0.0 [2021-10-14 18:00] - ECS-Lua is a tiny and easy to use ECS (Entity Component System) engine for - game development + ECS Lua is a lua ECS (Entity Component System) library used for game developments. - This is a minified version of ECS-Lua, to see the full source code visit + This is a minified version of ECS Lua, to see the full source code visit https://github.com/nidorx/ecs-lua ------------------------------------------------------------------------------ @@ -31,4 +30,4 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] -local a,b={},{}local function c(d)if(not a[d])then a[d]={r=b[d]()}end return a[d].r end b["Archetype"]=function()local d={}local e={}local f={}local g=0 local h={}h.__index=h function h.Of(i)local j={}local k={}for m,n in ipairs(i)do if(n.IsCType and not n.isComponent)then if n.IsQualifier then if k[n]==nil then k[n]=true table.insert(j,n.Id)end n=n.SuperClass end if k[n]==nil then k[n]=true table.insert(j,n.Id)end end end table.sort(j)local l='_'..table.concat(j,'_')if d[l]==nil then d[l]=setmetatable({id=l,_components=k},h)g=g+1 end return d[l]end function h.Version()return g end function h:Has(i)return(self._components[i]==true)end function h:With(i)if self._components[i]==true then return self end local j=e[self]if not j then j={}e[self]=j end local k=j[i]if k==nil then local l={i}for m,n in pairs(self._components)do table.insert(l,m)end k=h.Of(l)j[i]=k end return k end function h:WithAll(i)local j={}for k,l in pairs(self._components)do table.insert(j,k)end for k,l in ipairs(i)do if self._components[l]==nil then table.insert(j,l)end end return h.Of(j)end function h:Without(i)if self._components[i]==nil then return self end local j=f[self]if not j then j={}f[self]=j end local k=j[i]if k==nil then local l={}for m,n in pairs(self._components)do if m~=i then table.insert(l,m)end end k=h.Of(l)j[i]=k end return k end function h:WithoutAll(i)local j={}for l,m in ipairs(i)do j[m]=true end local k={}for l,m in pairs(self._components)do if j[l]==nil then table.insert(k,l)end end return h.Of(k)end h.EMPTY=h.Of({})return h end b["Component"]=function()local d=c("Utility")local e=c("ComponentFSM")local f=d.copyDeep local g=d.mergeDeep local h=0 local function i(l,m)h=h+1 local n={Id=h,IsCType=true,SuperClass=m}n.__index=n if m==nil then m=n m._Qualifiers={["Primary"]=n}m._Initializers={}else n.IsQualifier=true end local o=m._Qualifiers setmetatable(n,{__call=function(p,q)return n.New(q)end,__index=function(p,q)if(q=='States')then return m.__States end if(q=='Case'or q=='StateInitial')then return rawget(m,q)end end,__newindex=function(p,q,r)if(q=='Case'or q=='States'or q=='StateInitial')then if n==m then if(q=='States')then if not m.IsFSM then e.AddCapability(m,r)for s,t in pairs(o)do if t~=m then e.AddMethods(m,t)end end end else rawset(p,q,r)end end else rawset(p,q,r)end end})if m.IsFSM then e.AddMethods(m,n)end function n.Qualifier(p)if type(p)~="string"then for r,s in pairs(o)do if s==p then return p end end return nil end local q=o[p]if q==nil then q=i(l,m)o[p]=q end return q end function n.Qualifiers(...)local p={}local q={...}if#q==0 then for r,s in pairs(o)do table.insert(p,s)end else local r={}for s,t in ipairs({...})do local u=n.Qualifier(t)if u and r[u]==nil then r[u]=true table.insert(p,u)end end end return p end function n.New(p)if(p~=nil and type(p)~='table')then p={value=p}end local q=setmetatable(l(p)or{},n)for r,s in ipairs(m._Initializers)do s(q)end q.isComponent=true q._qualifiers={[n]=q}return q end function n:GetType()return n end function n:Is(p)return p==n or p==m end function n:Primary()return self._qualifiers[m]end function n:Qualified(p)return self._qualifiers[n.Qualifier(p)]end function n:QualifiedAll()local p={}for q,r in pairs(o)do p[q]=self._qualifiers[r]end return p end function n:Merge(p)if self==p then return end if self._qualifiers==p._qualifiers then return end if not p:Is(m)then return end local q=n local r=p:GetType()local s if q==m then s=self._qualifiers elseif r==m then s=p._qualifiers elseif self._qualifiers[m]~=nil then s=self._qualifiers[m]._qualifiers elseif p._qualifiers[m]~=nil then s=p._qualifiers[m]._qualifiers end if s~=nil then if self._qualifiers~=s then for t,u in pairs(self._qualifiers)do if m~=t then s[t]=u u._qualifiers=s end end end if p._qualifiers~=s then for t,u in pairs(p._qualifiers)do if m~=t then s[t]=u u._qualifiers=s end end end else for t,u in pairs(p._qualifiers)do if q~=t then self._qualifiers[t]=u u._qualifiers=self._qualifiers end end end end return n end local function j(l)return l or{}end local k={}function k.Create(l)local m=j if l~=nil then local n=type(l)if(n=='function')then m=l else if(n~='table')then l={value=l}end m=function(o)local p=f(l)if(o~=nil)then g(p,o)end return p end end end return i(m,nil)end return k end b["ComponentFSM"]=function()local function d(f,g)local h=g.States local i=g.IsSuperClass local j=g.ComponentClass if i then local k=j.Qualifiers()for l,m in ipairs(k)do local n=f[m]if(n~=nil and h[n:GetState()]==true)then return true end end return false else local k=f[j]if k==nil then return false end return h[k:GetState()]==true end end local e={}function e.AddCapability(f,g)f.IsFSM=true local h=setmetatable({},{__newindex=function(i,j,k)if(type(k)~="table")then k={k}end if table.find(k,'*')then rawset(i,j,'*')else local l=table.find(k,j)if l~=nil then table.remove(k,l)if#k==0 then k='*'end end rawset(i,j,k)end end})rawset(f,'__States',h)for i,j in pairs(g)do if f.StateInitial==nil then f.StateInitial=i end h[i]=j end e.AddMethods(f,f)table.insert(f._Initializers,function(i)i:SetState(f.StateInitial)end)end function e.AddMethods(f,g)local h=f.States function g.In(...)local i={}local j=0 for k,l in ipairs({...})do if(h[l]~=nil and i[l]==nil)then j=j+1 i[l]=true end end if j==0 then return{Components={g},}end return{Filter=d,Components={g},Config={States=i,IsSuperClass=(g==f),ComponentClass=g,}}end function g:SetState(i)if(i==nil or h[i]==nil)then return end local j=self:GetState()if(j==i)then return end if(j~=nil)then local l=h[j]if(l~='*'and table.find(l,i)==nil)then return end end self._state=i self._statePrev=j self._stateTime=os.clock()local k=f.Case and f.Case[i]if k then k(self,j)end end function g:GetState()return self._state or f.StateInitial end function g:GetPrevState()return self._statePrev or nil end function g:GetStateTime()return self._stateTime or 0 end end return e end b["ECS"]=function()local d=c("Query")local e=c("World")local f=c("System")local g=c("Archetype")local h=c("Component")local function i(k)e.LoopManager=k end pcall(function()if(game and game.ClassName=='DataModel')then i(c("RobloxLoopManager")())end end)local j={Query=d,World=e.New,System=f.Create,Archetype=g,Component=h.Create,SetLoopManager=i}if _G.ECS==nil then _G.ECS=j else local k=_G.warn or print k("ECS Lua was not registered in the global variables, there is already another object registered.")end return j end b["Entity"]=function()local d=c("Archetype")local e=0 local function f(k,...)local l={...}local m=k._data if(#l==1)then local o=l[1]if(o.IsCType and not o.isComponent)then return m[o]else return nil end end local n={}for o,p in ipairs(l)do if(p.IsCType and not p.isComponent)then table.insert(n,m[p])end end return table.unpack(n)end local function g(k,...)local l={...}local m=k._data local n=k.archetype local o=n local p=l[1]if(p and p.IsCType and not p.isComponent)then local q=l[2]if q==nil then m[p]=nil o=o:Without(p)elseif q.isComponent then p=q:GetType()m[p]=q o=o:With(p)else m[p]=p(q)o=o:With(p)end else for q,r in ipairs(l)do if(r.isComponent)then local s=r:GetType()m[s]=r o=o:With(s)end end end if(n~=o)then k.archetype=o k._onChange:Fire(k,n)end end local function h(k,...)local l=k._data local m=k.archetype local n=m for o,p in ipairs({...})do if p.isComponent then local q=p:GetType()l[q]=nil n=n:Without(q)elseif p.IsCType then l[p]=nil n=n:Without(p)end end if k.archetype~=n then k.archetype=n k._onChange:Fire(k,m)end end local function i(k,l)local m=k._data local n={}if(l~=nil and l.IsCType and not l.isComponent)then local o=l.Qualifiers()for p,q in ipairs(o)do local r=m[q]if r then table.insert(n,r)end end else for o,p in pairs(m)do table.insert(n,p)end end return n end local j={__index=function(k,l)if(type(l)=="table")then return f(k,l)end end,__newindex=function(k,l,m)local n=true if(type(l)=="table"and(l.IsCType and not l.isComponent))then g(k,l,m)else rawset(k,l,m)end end}function j.New(k,l)local m=d.EMPTY local n={}if(l~=nil and#l>0)then local o={}for p,q in ipairs(l)do local r=q:GetType()table.insert(o,r)n[r]=q end m=d.Of(o)end e=e+1 return setmetatable({_data=n,_onChange=k,id=e,isAlive=false,archetype=m,Get=f,Set=g,Unset=h,GetAll=i,},j)end return j end b["EntityRepository"]=function()local d={}d.__index=d function d.New()return setmetatable({_archetypes={},_entitiesArchetype={},},d)end function d:Insert(e)if(self._entitiesArchetype[e]==nil)then local f=e.archetype local g=self._archetypes[f]if(g==nil)then g={Count=0,Entities={}}self._archetypes[f]=g end g.Entities[e]=true g.Count=g.Count+1 self._entitiesArchetype[e]=f else self:Update(e)end end function d:Remove(e)local f=self._entitiesArchetype[e]if f==nil then return end self._entitiesArchetype[e]=nil local g=self._archetypes[f]if(g~=nil and g.Entities[e]==true)then g.Entities[e]=nil g.Count=g.Count-1 if(g.Count==0)then self._archetypes[f]=nil end end end function d:Update(e)local f=self._entitiesArchetype[e]if(f==nil or f==e.archetype)then return end self:Remove(e)self:Insert(e)end function d:Query(e)local f={}for g,h in pairs(self._archetypes)do if e:Match(g)then table.insert(f,h.Entities)end end return e:Result(f)end return d end b["Event"]=function()local d={}d.__index=d function d.New(f,g)return setmetatable({_Event=f,_Handler=g},d)end function d:Disconnect()local f=self._Event if(f and not f.destroyed)then local g=table.find(f._handlers,self._Handler)if g~=nil then table.remove(f._handlers,g)end end setmetatable(self,nil)end local e={}e.__index=e function e.New()return setmetatable({_handlers={}},e)end function e:Connect(f)if(type(f)=="function")then table.insert(self._handlers,f)return d.New(self,f)end error(("Event:Connect(%s)"):format(typeof(f)),2)end function e:Fire(...)if not self.destroyed then for f,g in ipairs(self._handlers)do g(table.unpack({...}))end end end function e:Destroy()setmetatable(self,nil)self._handlers=nil self.destroyed=true end return e end b["Query"]=function()local d=c("QueryResult")local e={}local f={}f.__index=f setmetatable(f,{__call=function(i,j,k,l)return f.New(j,k,l)end,})local function g(i,j,k)local l={}local m={}local n={}for o,p in ipairs(i)do if(l[p]==nil)then if(p.IsCType and not p.isComponent)then l[p]=true table.insert(m,p)table.insert(n,p.Id)else if p.Components then l[p]=true for q,r in ipairs(p.Components)do if(not l[r]and r.IsCType and not r.isComponent)then l[r]=true table.insert(m,r)table.insert(n,p.Id)end end end if p.Filter then l[p]=true p[j]=true table.insert(k,p)end end end end if#m>0 then table.sort(n)local o='_'..table.concat(n,'_')return m,o end end function f.New(i,j,k)local l={}local m,n,o if(j~=nil)then j,m=g(j,"IsAnyFilter",l)end if(i~=nil)then i,n=g(i,"IsAllFilter",l)end if(k~=nil)then k,o=g(k,"IsNoneFilter",l)end return setmetatable({isQuery=true,_any=j,_all=i,_none=k,_anyKey=m,_allKey=n,_noneKey=o,_cache={},_clauses=#l>0 and l or nil,},f)end function f:Result(i)return d.New(i,self._clauses)end function f:Match(i)local j=self._cache local k=j[i]if k~=nil then return k else local l=e[i]if(l==nil)then l={Any={},All={},None={}}e[i]=l end local m=self._noneKey if m then local p=l.None[m]if(p==nil)then p=true for q,r in ipairs(self._none)do if i:Has(r)then p=false break end end l.None[m]=p end if(p==false)then j[i]=false return false end end local n=self._anyKey if n then local p=l.Any[n]if(p==nil)then p=false if(l.All[n]==true)then p=true else for q,r in ipairs(self._any)do if i:Has(r)then p=true break end end end l.Any[n]=p end if(p==false)then j[i]=false return false end end local o=self._allKey if o then local p=l.All[o]if(p==nil)then local q=true for r,s in ipairs(self._all)do if(not i:Has(s))then q=false break end end if q then p=true else p=false end l.All[o]=p end j[i]=p return p end j[i]=true return true end end local function h()local i={isQueryBuilder=true}function i.All(...)i._all={...}return i end function i.Any(...)i._any={...}return i end function i.None(...)i._none={...}return i end function i.Build()return f.New(i._all,i._any,i._none)end return i end function f.All(...)return h().All(...)end function f.Any(...)return h().Any(...)end function f.None(...)return h().None(...)end return f end b["QueryResult"]=function()local function d(l,m,n)return m,(l(m)==true),true end local function e(l,m,n)return l(m),true,true end local function f(l,m,n)local o=(n<=l)return m,o,o end local function g(l,m,n)local o=true for p,q in ipairs(l)do if(q.Filter(m,q.Config)==true)then o=false break end end return m,o,true end local function h(l,m,n)local o=true for p,q in ipairs(l)do if(q.Filter(m,q.Config)==false)then o=false break end end return m,o,true end local function i(l,m,n)local o=false for p,q in ipairs(l)do if(q.Filter(m,q.Config)==true)then o=true break end end return m,o,true end local j={}local k={}k.__index=k function k.New(l,m)local n=j if(m and#m>0)then local o={}local p={}local q={}n={}for r,s in ipairs(m)do if s.IsNoneFilter then table.insert(q,s)elseif s.IsAnyFilter then table.insert(p,s)else table.insert(o,s)end end if(#q>0)then table.insert(n,{g,q})end if(#o>0)then table.insert(n,{h,o})end if(#p>0)then table.insert(n,{i,p})end end return setmetatable({chunks=l,_pipeline=n,},k)end function k:With(l,m)local n={}for o,p in ipairs(self._pipeline)do table.insert(n,p)end table.insert(n,{l,m})return setmetatable({chunks=self.chunks,_pipeline=n,},k)end function k:Filter(l)return self:With(d,l)end function k:Map(l)return self:With(e,l)end function k:Limit(l)return self:With(f,l)end function k:AnyMatch(l)local m=false self:Run(function(n)if l(n)then m=true end return m end)return m end function k:AllMatch(l)local m=true self:Run(function(n)if(not l(n))then m=false end return m==false end)return m end function k:FindAny()local l self:Run(function(m)l=m return true end)return l end function k:ForEach(l)self:Run(function(m)return l(m)==true end)end function k:ToArray()local l={}self:Run(function(m)table.insert(l,m)end)return l end function k:Iterator()local l=coroutine.create(function()self:Run(function(m,n)coroutine.yield(m,n)end)end)return function()local m,n,o=coroutine.resume(l)return o,n end end function k:Run(l)local m=1 local n=self._pipeline local o=#n>0 if(not o)then for p,q in ipairs(self.chunks)do for r,s in pairs(q)do if(l(r,m)==true)then return end m=m+1 end end else for p,q in ipairs(self.chunks)do for r,s in pairs(q)do local t=false local u=true local v=r if(u and o)then for w,x in ipairs(n)do local y,z,A=x[1](x[2],v,m)if(not A)then t=true end if z then v=y else u=false break end end end if u then if(l(v,m)==true)then return end m=m+1 end if t then return end end end end end return k end b["RobloxLoopManager"]=function()local function d()local e=game:GetService('RunService')return{Register=function(f)local g=e.Stepped:Connect(function()f:Update('process',os.clock())end)local h=e.Heartbeat:Connect(function()f:Update('transform',os.clock())end)local i if(not e:IsServer())then i=e.RenderStepped:Connect(function()f:Update('render',os.clock())end)end return function()g:Disconnect()g:Disconnect()g:Disconnect()end end}end return d end b["System"]=function()local d=0 local e={'task','render','process','transform'}local f={}function f.Create(g,h,i,j)if(g==nil or not table.find(e,g))then error('The "step" parameter must one of ',table.concat(e,', '))end if type(h)=="function"then j=h h=nil end if(h==nil or h<0)then h=50 end if type(i)=="function"then j=i i=nil end if(i and i.isQueryBuilder)then i=i.Build()end d=d+1 local k=d local l={Id=k,Step=g,Order=h,Query=i,}l.__index=l function l.New(m,n)local o=setmetatable({version=0,_world=m,_config=n,},l)if o.Initialize then o:Initialize(n)end return o end function l:GetType()return l end function l:Result(m)return self._world:Exec(m or l.Query)end function l:Destroy()if self.OnDestroy then self.OnDestroy()end setmetatable(self,nil)for m,n in pairs(self)do self[m]=nil end end if j and type(j)=="function"then l.Update=j end return l end return f end b["SystemExecutor"]=function()local function d(i)local j={}local k={}for l,m in ipairs(i)do local n=m:GetType()if(m._TaskState==nil)then m._TaskState="suspended"end if not k[n]then local o={Type=n,System=m,Depends={}}k[n]=o table.insert(j,o)end end for l,m in ipairs(j)do local n=m.Type.Before if(n~=nil and#n>0)then for p,q in ipairs(n)do local r=k[q]if r then r.Depends[m]=true end end end local o=m.Type.After if(o~=nil and#o>0)then for p,q in ipairs(o)do local r=k[q]if r then m.Depends[r]=true end end end end return j end local function e(i,j)return i.Order0 do local j=false local k,l=0,#self._schedulers-1 while k<=l do k=k+1 local m=self._schedulers[k]local n,o=m.Resume(i)if o then j=true end i=i-(n+0.00001)if(i<=0)then break end end if not j then return end end end local function h(i,j,k,l)local m=i.System m._TaskState="running"if(m.ShouldUpdate==nil or m.ShouldUpdate(j))then k.version=k.version+1 m:Update(j)m.version=k.version end m._TaskState="suspended"l(i)end function f:ScheduleTasks(i)local j=self._world local k={}local l={}local m={}local n={}local o={}local p,q=0,#self._task-1 while p<=q do p=p+1 local s=self._task[p]if(s.System._TaskState=="suspended")then s.System._TaskState="scheduled"local t=false for u,v in pairs(s.Depends)do t=true if o[u]==nil then o[u]={}end table.insert(o[u],s)end if(not t)then table.insert(k,s)end m[s]=true end end local function r(s)s.Thread=nil s.LastExecTime=nil n[s]=true if o[s]then local t=o[s]local u,v=0,#t-1 while u<=v do u=u+1 local w=t[u]if m[w]then local x=true for y,z in pairs(w.Depends)do if n[y]~=true then x=false break end end if x then m[w]=nil w.LastExecTime=0 w.Thread=coroutine.create(h)table.insert(l,w)end end end end end if#k>0 then local s,t=0,#k-1 while s<=t do s=s+1 local v=k[s]m[v]=nil v.LastExecTime=0 v.Thread=coroutine.create(h)table.insert(l,v)end local u u={Resume=function(v)table.sort(l,function(A,B)return A.LastExecTimev)then break end end end for A,B in ipairs(l)do if B.Thread==nil then local C=table.find(l,B)if C~=nil then table.remove(l,C)end end end local z=#l>0 if(not z)then local A=table.find(self._schedulers,u)if A~=nil then table.remove(self._schedulers,A)end end return w,z end}table.insert(self._schedulers,u)end end return f end b["Timer"]=function()local d=4 local e={}e.__index=e local f={}f.__index=f function f.New(g)local h=setmetatable({Time=setmetatable({Now=0,NowReal=0,Frame=0,FrameReal=0,Process=0,Delta=0,DeltaFixed=0,Interpolation=0},e),Frequency=0,LastFrame=0,ProcessOld=0,FirstUpdate=0,},f)h:SetFrequency(g)return h end function f:SetFrequency(g)if g==nil then g=30 end local h=math.floor(math.abs(g)/2)*2 if h<2 then h=2 end if g~=h then g=h print(string.format(">>> ATTENTION! The execution frequency of world has been changed to %d <<<",h))end self.Frequency=g self.Time.DeltaFixed=1000/g/1000 end function f:Update(g,h,i)if(self.FirstUpdate==0)then self.FirstUpdate=g end local j=g g=g-self.FirstUpdate local k=self.Time k.Now=g k.NowReal=j if h=='process'then local l=k.Process k.Frame=g k.FrameReal=j if self.LastFrame==0 then self.LastFrame=k.Frame end if k.Process==0 then k.Process=k.Frame self.ProcessOld=k.Frame end k.Delta=k.Frame-self.LastFrame k.Interpolation=1 local m=0 local n=false while(k.Process<=k.Frame and m0)then local p={}for q,r in ipairs(m)do local s=r:GetType()table.insert(p,s)o[s]=r end n=d.Of(p)end e=e+1 return setmetatable({_data=o,_onChange=l,id=e,isAlive=false,archetype=n,Get=f,Set=h,Unset=i,GetAll=j,},k)end return k end b["EntityRepository"]=function()local d={}d.__index=d function d.New()return setmetatable({_archetypes={},_entitiesArchetype={},},d)end function d:Insert(e)if(self._entitiesArchetype[e]==nil)then local f=e.archetype local g=self._archetypes[f]if(g==nil)then g={Count=0,Entities={}}self._archetypes[f]=g end g.Entities[e]=true g.Count=g.Count+1 self._entitiesArchetype[e]=f else self:Update(e)end end function d:Remove(e)local f=self._entitiesArchetype[e]if f==nil then return end self._entitiesArchetype[e]=nil local g=self._archetypes[f]if(g~=nil and g.Entities[e]==true)then g.Entities[e]=nil g.Count=g.Count-1 if(g.Count==0)then self._archetypes[f]=nil end end end function d:Update(e)local f=self._entitiesArchetype[e]if(f==nil or f==e.archetype)then return end self:Remove(e)self:Insert(e)end function d:Query(e)local f={}for g,h in pairs(self._archetypes)do if e:Match(g)then table.insert(f,h.Entities)end end return e:Result(f)end return d end b["Event"]=function()local d={}d.__index=d function d.New(f,g)return setmetatable({_Event=f,_Handler=g},d)end function d:Disconnect()local f=self._Event if(f and not f.destroyed)then local g=table.find(f._handlers,self._Handler)if g~=nil then table.remove(f._handlers,g)end end setmetatable(self,nil)end local e={}e.__index=e function e.New()return setmetatable({_handlers={}},e)end function e:Connect(f)if(type(f)=="function")then table.insert(self._handlers,f)return d.New(self,f)end error(("Event:Connect(%s)"):format(typeof(f)),2)end function e:Fire(...)if not self.destroyed then for f,g in ipairs(self._handlers)do g(table.unpack({...}))end end end function e:Destroy()setmetatable(self,nil)self._handlers=nil self.destroyed=true end return e end b["Query"]=function()local d=c("QueryResult")local e={}local f={}f.__index=f setmetatable(f,{__call=function(i,j,k,l)return f.New(j,k,l)end,})local function g(i,j,k)local l={}local m={}local n={}for o,p in ipairs(i)do if(l[p]==nil)then if(p.IsCType and not p.isComponent)then l[p]=true table.insert(m,p)table.insert(n,p.Id)else if p.Components then l[p]=true for q,r in ipairs(p.Components)do if(not l[r]and r.IsCType and not r.isComponent)then l[r]=true table.insert(m,r)table.insert(n,p.Id)end end end if p.Filter then l[p]=true p[j]=true table.insert(k,p)end end end end if#m>0 then table.sort(n)local o="_"..table.concat(n,"_")return m,o end end function f.New(i,j,k)local l={}local m,n,o if(j~=nil)then j,m=g(j,"IsAnyFilter",l)end if(i~=nil)then i,n=g(i,"IsAllFilter",l)end if(k~=nil)then k,o=g(k,"IsNoneFilter",l)end return setmetatable({isQuery=true,_any=j,_all=i,_none=k,_anyKey=m,_allKey=n,_noneKey=o,_cache={},_clauses=#l>0 and l or nil,},f)end function f:Result(i)return d.New(i,self._clauses)end function f:Match(i)local j=self._cache local k=j[i]if k~=nil then return k else local l=e[i]if(l==nil)then l={Any={},All={},None={}}e[i]=l end local m=self._noneKey if m then local p=l.None[m]if(p==nil)then p=true for q,r in ipairs(self._none)do if i:Has(r)then p=false break end end l.None[m]=p end if(p==false)then j[i]=false return false end end local n=self._anyKey if n then local p=l.Any[n]if(p==nil)then p=false if(l.All[n]==true)then p=true else for q,r in ipairs(self._any)do if i:Has(r)then p=true break end end end l.Any[n]=p end if(p==false)then j[i]=false return false end end local o=self._allKey if o then local p=l.All[o]if(p==nil)then local q=true for r,s in ipairs(self._all)do if(not i:Has(s))then q=false break end end if q then p=true else p=false end l.All[o]=p end j[i]=p return p end j[i]=true return true end end local function h()local i={isQueryBuilder=true}function i.All(...)i._all={...}return i end function i.Any(...)i._any={...}return i end function i.None(...)i._none={...}return i end function i.Build()return f.New(i._all,i._any,i._none)end return i end function f.All(...)return h().All(...)end function f.Any(...)return h().Any(...)end function f.None(...)return h().None(...)end return f end b["QueryResult"]=function()local function d(l,m,n)return m,(l(m)==true),true end local function e(l,m,n)return l(m),true,true end local function f(l,m,n)local o=(n<=l)return m,o,o end local function g(l,m,n)local o=true for p,q in ipairs(l)do if(q.Filter(m,q.Config)==true)then o=false break end end return m,o,true end local function h(l,m,n)local o=true for p,q in ipairs(l)do if(q.Filter(m,q.Config)==false)then o=false break end end return m,o,true end local function i(l,m,n)local o=false for p,q in ipairs(l)do if(q.Filter(m,q.Config)==true)then o=true break end end return m,o,true end local j={}local k={}k.__index=k function k.New(l,m)local n=j if(m and#m>0)then local o={}local p={}local q={}n={}for r,s in ipairs(m)do if s.IsNoneFilter then table.insert(q,s)elseif s.IsAnyFilter then table.insert(p,s)else table.insert(o,s)end end if(#q>0)then table.insert(n,{g,q})end if(#o>0)then table.insert(n,{h,o})end if(#p>0)then table.insert(n,{i,p})end end return setmetatable({chunks=l,_pipeline=n,},k)end function k:With(l,m)local n={}for o,p in ipairs(self._pipeline)do table.insert(n,p)end table.insert(n,{l,m})return setmetatable({chunks=self.chunks,_pipeline=n,},k)end function k:Filter(l)return self:With(d,l)end function k:Map(l)return self:With(e,l)end function k:Limit(l)return self:With(f,l)end function k:AnyMatch(l)local m=false self:Run(function(n)if l(n)then m=true end return m end)return m end function k:AllMatch(l)local m=true self:Run(function(n)if(not l(n))then m=false end return m==false end)return m end function k:FindAny()local l self:Run(function(m)l=m return true end)return l end function k:ForEach(l)self:Run(function(m)return l(m)==true end)end function k:ToArray()local l={}self:Run(function(m)table.insert(l,m)end)return l end function k:Iterator()local l=coroutine.create(function()self:Run(function(m,n)coroutine.yield(m,n)end)end)return function()local m,n,o=coroutine.resume(l)return o,n end end function k:Run(l)local m=1 local n=self._pipeline local o=#n>0 if(not o)then for p,q in ipairs(self.chunks)do for r,s in pairs(q)do if(l(r,m)==true)then return end m=m+1 end end else for p,q in ipairs(self.chunks)do for r,s in pairs(q)do local t=false local u=true local v=r if(u and o)then for w,x in ipairs(n)do local y,z,A=x[1](x[2],v,m)if(not A)then t=true end if z then v=y else u=false break end end end if u then if(l(v,m)==true)then return end m=m+1 end if t then return end end end end end return k end b["RobloxLoopManager"]=function()local function d()local e=game:GetService("RunService")return{Register=function(f)local g=e.Stepped:Connect(function()f:Update("process",os.clock())end)local h=e.Heartbeat:Connect(function()f:Update("transform",os.clock())end)local i if(not e:IsServer())then i=e.RenderStepped:Connect(function()f:Update("render",os.clock())end)end return function()g:Disconnect()g:Disconnect()g:Disconnect()end end}end return d end b["System"]=function()local d=0 local e={"task","render","process","transform"}local f={}function f.Create(g,h,i,j)if(g==nil or not table.find(e,g))then error("The step parameter must one of ",table.concat(e,", "))end if type(h)=="function"then j=h h=nil end if(h==nil or h<0)then h=50 end if type(i)=="function"then j=i i=nil end if(i and i.isQueryBuilder)then i=i.Build()end d=d+1 local k=d local l={Id=k,Step=g,Order=h,Query=i,}l.__index=l function l.New(m,n)local o=setmetatable({version=0,_world=m,_config=n,},l)if o.Initialize then o:Initialize(n)end return o end function l:GetType()return l end function l:Result(m)return self._world:Exec(m or l.Query)end function l:Destroy()if self.OnDestroy then self.OnDestroy()end setmetatable(self,nil)for m,n in pairs(self)do self[m]=nil end end if j and type(j)=="function"then l.Update=j end return l end return f end b["SystemExecutor"]=function()local function d(i)local j={}local k={}for l,m in ipairs(i)do local n=m:GetType()if(m._TaskState==nil)then m._TaskState="suspended"end if not k[n]then local o={Type=n,System=m,Depends={}}k[n]=o table.insert(j,o)end end for l,m in ipairs(j)do local n=m.Type.Before if(n~=nil and#n>0)then for p,q in ipairs(n)do local r=k[q]if r then r.Depends[m]=true end end end local o=m.Type.After if(o~=nil and#o>0)then for p,q in ipairs(o)do local r=k[q]if r then m.Depends[r]=true end end end end return j end local function e(i,j)return i.Order0 do local j=false local k,l=0,#self._schedulers-1 while k<=l do k=k+1 local m=self._schedulers[k]local n,o=m.Resume(i)if o then j=true end i=i-(n+0.00001)if(i<=0)then break end end if not j then return end end end local function h(i,j,k,l)local m=i.System m._TaskState="running"if(m.ShouldUpdate==nil or m.ShouldUpdate(j))then k.version=k.version+1 m:Update(j)m.version=k.version end m._TaskState="suspended"l(i)end function f:ScheduleTasks(i)local j=self._world local k={}local l={}local m={}local n={}local o={}local p,q=0,#self._task-1 while p<=q do p=p+1 local s=self._task[p]if(s.System._TaskState=="suspended")then s.System._TaskState="scheduled"local t=false for u,v in pairs(s.Depends)do t=true if o[u]==nil then o[u]={}end table.insert(o[u],s)end if(not t)then table.insert(k,s)end m[s]=true end end local function r(s)s.Thread=nil s.LastExecTime=nil n[s]=true if o[s]then local t=o[s]local u,v=0,#t-1 while u<=v do u=u+1 local w=t[u]if m[w]then local x=true for y,z in pairs(w.Depends)do if n[y]~=true then x=false break end end if x then m[w]=nil w.LastExecTime=0 w.Thread=coroutine.create(h)table.insert(l,w)end end end end end if#k>0 then local s,t=0,#k-1 while s<=t do s=s+1 local v=k[s]m[v]=nil v.LastExecTime=0 v.Thread=coroutine.create(h)table.insert(l,v)end local u u={Resume=function(v)table.sort(l,function(A,B)return A.LastExecTimev)then break end end end for A,B in ipairs(l)do if B.Thread==nil then local C=table.find(l,B)if C~=nil then table.remove(l,C)end end end local z=#l>0 if(not z)then local A=table.find(self._schedulers,u)if A~=nil then table.remove(self._schedulers,A)end end return w,z end}table.insert(self._schedulers,u)end end return f end b["Timer"]=function()local d=4 local e={}e.__index=e local f={}f.__index=f function f.New(g)local h=setmetatable({Time=setmetatable({Now=0,NowReal=0,Frame=0,FrameReal=0,Process=0,Delta=0,DeltaFixed=0,Interpolation=0},e),Frequency=0,LastFrame=0,ProcessOld=0,FirstUpdate=0,},f)h:SetFrequency(g)return h end function f:SetFrequency(g)if g==nil then g=30 end local h=math.floor(math.abs(g)/2)*2 if h<2 then h=2 end if g~=h then g=h end self.Frequency=g self.Time.DeltaFixed=1000/g/1000 end function f:Update(g,h,i)if(self.FirstUpdate==0)then self.FirstUpdate=g end local j=g g=g-self.FirstUpdate local k=self.Time k.Now=g k.NowReal=j if h=="process"then local l=k.Process k.Frame=g k.FrameReal=j if self.LastFrame==0 then self.LastFrame=k.Frame end if k.Process==0 then k.Process=k.Frame self.ProcessOld=k.Frame end k.Delta=k.Frame-self.LastFrame k.Interpolation=1 local m=0 local n=false while(k.Process<=k.Frame and m 0 then table.sort(cTypeIds) - local cTypesKey = '_' .. table.concat(cTypeIds, '_') + local cTypesKey = "_" .. table.concat(cTypeIds, "_") return cTypes, cTypesKey end end @@ -1896,24 +1986,24 @@ end __F__["RobloxLoopManager"] = function() -- src/RobloxLoopManager.lua local function InitManager() - local RunService = game:GetService('RunService') + local RunService = game:GetService("RunService") return { Register = function(world) -- if not RunService:IsRunning() then -- return -- end local processConn = RunService.Stepped:Connect(function() - world:Update('process', os.clock()) + world:Update("process", os.clock()) end) local transformConn = RunService.Heartbeat:Connect(function() - world:Update('transform', os.clock()) + world:Update("transform", os.clock()) end) local renderConn if (not RunService:IsServer()) then renderConn = RunService.RenderStepped:Connect(function() - world:Update('render', os.clock()) + world:Update("render", os.clock()) end) end @@ -1935,7 +2025,7 @@ __F__["System"] = function() local SYSTEM_ID_SEQ = 0 - local STEPS = { 'task', 'render', 'process', 'transform' } + local STEPS = { "task", "render", "process", "transform" } local System = {} @@ -1950,7 +2040,7 @@ __F__["System"] = function() function System.Create(step, order, query, updateFn) if (step == nil or not table.find(STEPS, step)) then - error('The "step" parameter must one of ', table.concat(STEPS, ', ')) + error("The step parameter must one of ", table.concat(STEPS, ", ")) end if type(order) == "function" then @@ -2125,22 +2215,22 @@ __F__["SystemExecutor"] = function() for _, system in pairs(systems) do local step = system.Step if system.Update then - if step == 'task' then + if step == "task" then table.insert(updateTask, system) - elseif step == 'process' then + elseif step == "process" then table.insert(updateProcess, system) - elseif step == 'transform' then + elseif step == "transform" then table.insert(updateTransform, system) - elseif step == 'render' then + elseif step == "render" then table.insert(updateRender, system) end end - if (system.Query and system.Query.isQuery and step ~= 'task') then + if (system.Query and system.Query.isQuery and step ~= "task") then if system.OnExit then table.insert(onExit, system) end @@ -2576,7 +2666,6 @@ __F__["Timer"] = function() if frequency ~= safeFrequency then frequency = safeFrequency - print(string.format(">>> ATTENTION! The execution frequency of world has been changed to %d <<<", safeFrequency)) end self.Frequency = frequency @@ -2597,7 +2686,7 @@ __F__["Timer"] = function() Time.Now = now Time.NowReal = nowReal - if step == 'process' then + if step == "process" then local processOldTmp = Time.Process -- first step, initialize current frame time @@ -2635,8 +2724,6 @@ __F__["Timer"] = function() -- Fixed time is updated in regular intervals (equal to DeltaFixed) until time property is reached. while (Time.Process <= Time.Frame and nLoops < MAX_SKIP_FRAMES) do - -- debugF('Update') - updated = true callback(Time) @@ -2659,7 +2746,7 @@ __F__["Timer"] = function() callback(Time) - if step == 'render' then + if step == "render" then -- last step, save last frame time self.LastFrame = Time.Frame end @@ -2907,10 +2994,10 @@ __F__["World"] = function() ]] self._timer:Update(now, step, function(Time) - if step == 'process' then + if step == "process" then self._executor:ScheduleTasks(Time) self._executor:ExecProcess(Time) - elseif step == 'transform' then + elseif step == "transform" then self._executor:ExecTransform(Time) else self._executor:ExecRender(Time) diff --git a/build.lua b/build.lua index fbfe88a..da50059 100644 --- a/build.lua +++ b/build.lua @@ -23,12 +23,11 @@ local SRC_FILES = { } local HEADER = [[ - ECS-Lua v2.0.0 [2021-10-02 17:25] + ECS Lua v2.0.0 [2021-10-14 18:00] - ECS-Lua is a tiny and easy to use ECS (Entity Component System) engine for - game development + ECS Lua is a lua ECS (Entity Component System) library used for game developments. - This is a minified version of ECS-Lua, to see the full source code visit + This is a minified version of ECS Lua, to see the full source code visit https://github.com/nidorx/ecs-lua ------------------------------------------------------------------------------ diff --git a/src/Component.lua b/src/Component.lua index 33df6a8..4dbf7da 100644 --- a/src/Component.lua +++ b/src/Component.lua @@ -25,12 +25,15 @@ local function createComponentClass(initializer, superClass) if superClass == nil then superClass = ComponentClass superClass._Qualifiers = { ["Primary"] = ComponentClass } + superClass._QualifiersArr = { ComponentClass } superClass._Initializers = {} else + superClass.HasQualifier = true ComponentClass.IsQualifier = true end local Qualifiers = superClass._Qualifiers + local QualifiersArr = superClass._QualifiersArr setmetatable(ComponentClass, { __call = function(t, value) @@ -80,7 +83,7 @@ local function createComponentClass(initializer, superClass) ]] function ComponentClass.Qualifier(qualifier) if type(qualifier) ~= "string" then - for _, qualifiedClass in pairs(Qualifiers) do + for _, qualifiedClass in ipairs(QualifiersArr) do if qualifiedClass == qualifier then return qualifier end @@ -92,6 +95,7 @@ local function createComponentClass(initializer, superClass) if qualifiedClass == nil then qualifiedClass = createComponentClass(initializer, superClass) Qualifiers[qualifier] = qualifiedClass + table.insert(QualifiersArr, qualifiedClass) end return qualifiedClass end @@ -103,15 +107,11 @@ local function createComponentClass(initializer, superClass) @return ComponentClass[] ]] function ComponentClass.Qualifiers(...) - - local qualifiers = {} - local filter = {...} if #filter == 0 then - for _, qualifiedClass in pairs(Qualifiers) do - table.insert(qualifiers, qualifiedClass) - end + return QualifiersArr else + local qualifiers = {} local cTypes = {} for _,qualifier in ipairs({...}) do local qualifiedClass = ComponentClass.Qualifier(qualifier) @@ -120,9 +120,8 @@ local function createComponentClass(initializer, superClass) table.insert(qualifiers, qualifiedClass) end end + return qualifiers end - - return qualifiers end --[[ @@ -197,63 +196,80 @@ local function createComponentClass(initializer, superClass) end --[[ - Merges data from the other component into the current component. This method should not be invoked, it is used - by the entity to ensure correct retrieval of a component's qualifiers. - - @param other {Component} + Unlink this component with the other qualifiers ]] - function ComponentClass:Merge(other) - if self == other then + function ComponentClass:Detach() + if not superClass.HasQualifier then return end - if self._qualifiers == other._qualifiers then - return - end + -- remove old unlink + self._qualifiers[ComponentClass] = nil - if not other:Is(superClass) then - return - end + -- new link + self._qualifiers = { [ComponentClass] = self } + end - local selfClass = ComponentClass - local otherClass = other:GetType() - - -- does anyone know the reference to the primary entity? - local primaryQualifiers - if selfClass == superClass then - primaryQualifiers = self._qualifiers - elseif otherClass == superClass then - primaryQualifiers = other._qualifiers - elseif self._qualifiers[superClass] ~= nil then - primaryQualifiers = self._qualifiers[superClass]._qualifiers - elseif other._qualifiers[superClass] ~= nil then - primaryQualifiers = other._qualifiers[superClass]._qualifiers - end + --[[ + Merges data from the other component into the current component. This method should not be invoked, it is used + by the entity to ensure correct retrieval of a component's qualifiers. - if primaryQualifiers ~= nil then - if self._qualifiers ~= primaryQualifiers then - for qualifiedClass, component in pairs(self._qualifiers) do - if superClass ~= qualifiedClass then - primaryQualifiers[qualifiedClass] = component - component._qualifiers = primaryQualifiers + @param other {Component} + ]] + function ComponentClass:Merge(other) + if superClass.HasQualifier then + if self == other then + return + end + + if self._qualifiers == other._qualifiers then + return + end + + if not other:Is(superClass) then + return + end + + local selfClass = ComponentClass + local otherClass = other:GetType() + + -- does anyone know the reference to the primary entity? + local primaryQualifiers + if selfClass == superClass then + primaryQualifiers = self._qualifiers + elseif otherClass == superClass then + primaryQualifiers = other._qualifiers + elseif self._qualifiers[superClass] ~= nil then + primaryQualifiers = self._qualifiers[superClass]._qualifiers + elseif other._qualifiers[superClass] ~= nil then + primaryQualifiers = other._qualifiers[superClass]._qualifiers + end + + if primaryQualifiers ~= nil then + if self._qualifiers ~= primaryQualifiers then + for qualifiedClass, component in pairs(self._qualifiers) do + if superClass ~= qualifiedClass then + primaryQualifiers[qualifiedClass] = component + component._qualifiers = primaryQualifiers + end end end - end - - if other._qualifiers ~= primaryQualifiers then - for qualifiedClass, component in pairs(other._qualifiers) do - if superClass ~= qualifiedClass then - primaryQualifiers[qualifiedClass] = component - component._qualifiers = primaryQualifiers + + if other._qualifiers ~= primaryQualifiers then + for qualifiedClass, component in pairs(other._qualifiers) do + if superClass ~= qualifiedClass then + primaryQualifiers[qualifiedClass] = component + component._qualifiers = primaryQualifiers + end end end - end - else - -- none of the instances know the Primary, use the current object reference - for qualifiedClass, component in pairs(other._qualifiers) do - if selfClass ~= qualifiedClass then - self._qualifiers[qualifiedClass] = component - component._qualifiers = self._qualifiers + else + -- none of the instances know the Primary, use the current object reference + for qualifiedClass, component in pairs(other._qualifiers) do + if selfClass ~= qualifiedClass then + self._qualifiers[qualifiedClass] = component + component._qualifiers = self._qualifiers + end end end end diff --git a/src/ECS.lua b/src/ECS.lua index aa2b425..0bd771b 100644 --- a/src/ECS.lua +++ b/src/ECS.lua @@ -1,8 +1,7 @@ --[[ - ECS-Lua v2.0.0 [2021-10-02 17:25] + ECS Lua v2.0.0 [2021-10-14 18:00] - Roblox-ECS is a tiny and easy to use ECS (Entity Component System) engine for - game development on the Roblox platform + ECS Lua is a lua ECS (Entity Component System) library used for game developments. https://github.com/nidorx/ecs-lua diff --git a/src/Entity.lua b/src/Entity.lua index b70a78f..e5bfb2e 100644 --- a/src/Entity.lua +++ b/src/Entity.lua @@ -40,6 +40,31 @@ local function getComponent(entity, ...) return table.unpack(components) end +--[[ + Merges the qualifiers of a new added component. + + @param entity {Entity} + @param newComponent {Component} + @param newComponentClass {ComponentClass} +]] +local function mergeComponents(entity, newComponent, newComponentClass) + local data = entity._data + local otherComponent + -- get first instance + for _,oCType in ipairs(newComponentClass.Qualifiers()) do + if oCType ~= newComponentClass then + otherComponent = data[oCType] + if otherComponent then + break + end + end + end + + if otherComponent then + otherComponent:Merge(newComponent) + end +end + --[[ [SET] 01) entity[CompType1] = nil @@ -56,34 +81,76 @@ local function setComponent(entity, ...) local archetypeOld = entity.archetype local archetypeNew = archetypeOld + local toMerge = {} + local cType = values[1] - if (cType and cType.IsCType and not cType.isComponent) then + if (cType and cType.IsCType and not cType.isComponent) then local value = values[2] + local component -- 01) entity[CompType1] = nil -- 02) entity[CompType1] = value -- 03) entity:Set(CompType1, nil) -- 04) entity:Set(CompType1, value) if value == nil then + local old = data[cType] + if old then + old:Detach() + end + data[cType] = nil archetypeNew = archetypeNew:Without(cType) elseif value.isComponent then - cType = value:GetType() - data[cType] = value - archetypeNew = archetypeNew:With(cType) + local old = data[cType] + if (old ~= value) then + if old then + old:Detach() + end + + cType = value:GetType() + data[cType] = value + archetypeNew = archetypeNew:With(cType) + -- merge components + if (cType.HasQualifier or cType.IsQualifier) then + mergeComponents(entity, value, cType) + end + end else - data[cType] = cType(value) + local old = data[cType] + if old then + old:Detach() + end + + local component = cType(value) + data[cType] = component archetypeNew = archetypeNew:With(cType) + + -- merge components + if (cType.HasQualifier or cType.IsQualifier) then + mergeComponents(entity, component, cType) + end end else -- 05) entity:Set(comp1) -- 06) entity:Set(comp1, comp2, ...) for i,component in ipairs(values) do if (component.isComponent) then - local ctype = component:GetType() - data[ctype] = component - archetypeNew = archetypeNew:With(ctype) + local cType = component:GetType() + local old = data[cType] + if (old ~= component) then + if old then + old:Detach() + end + + data[cType] = component + archetypeNew = archetypeNew:With(cType) + + -- merge components + if (cType.HasQualifier or cType.IsQualifier) then + mergeComponents(entity, component, cType) + end + end end end end @@ -113,6 +180,10 @@ local function unsetComponent(entity, ...) -- 01) enity:Unset(comp1) -- 04) enity:Unset(comp1, comp1, ...) local cType = value:GetType() + local old = data[cType] + if old then + old:Detach() + end data[cType] = nil archetypeNew = archetypeNew:Without(cType) @@ -120,6 +191,10 @@ local function unsetComponent(entity, ...) -- 02) entity[CompType1] = nil -- 03) enity:Unset(CompType1) -- 05) enity:Unset(CompType1, CompType2, ...) + local old = data[value] + if old then + old:Detach() + end data[value] = nil archetypeNew = archetypeNew:Without(value) end diff --git a/test/test_Component.lua b/test/test_Component.lua index abff64f..83ddeac 100644 --- a/test/test_Component.lua +++ b/test/test_Component.lua @@ -163,13 +163,13 @@ function TestComponent:test_Should_CreateQualifier() lu.assertEquals(buffMission:Qualified("Mission"), buffMission) -- all - lu.assertEquals(buff:QualifiedAll("Mission"), { + lu.assertEquals(buff:QualifiedAll(), { ["Primary"] = buff, ["Level"] = buffLevel, ["Mission"] = buffMission }) - lu.assertEquals(buffLevel:QualifiedAll("Mission"), { + lu.assertEquals(buffLevel:QualifiedAll(), { ["Primary"] = buff, ["Level"] = buffLevel, ["Mission"] = buffMission }) - lu.assertEquals(buffMission:QualifiedAll("Mission"), { + lu.assertEquals(buffMission:QualifiedAll(), { ["Primary"] = buff, ["Level"] = buffLevel, ["Mission"] = buffMission }) end diff --git a/test/test_Entity.lua b/test/test_Entity.lua index 99ae73b..b4b5b2c 100644 --- a/test/test_Entity.lua +++ b/test/test_Entity.lua @@ -392,3 +392,177 @@ function TestEntity:test_RawSet() lu.assertEquals(entity[Object], "XPTO") lu.assertEquals(entity.archetype, Archetype.EMPTY) end + +function TestEntity:test_Qualifiers() + + local event = Event.New() + + local HealthBuff = Component.Create({ percent = 0 }) + local HealthBuffLevel = HealthBuff.Qualifier("Level") + local HealthBuffMission = HealthBuff.Qualifier("Mission") + local OtherComp = Component.Create({ value = 0 }) + + + local function doTest(instanciate) + + local entity = Entity.New(event) + + local buff, buffLevel, buffMission, other = instanciate(entity) + + lu.assertItemsEquals(entity:GetAll(HealthBuff), {buff, buffLevel, buffMission}) + lu.assertItemsEquals(entity:GetAll(HealthBuffLevel), {buff, buffLevel, buffMission}) + lu.assertItemsEquals(entity:GetAll(HealthBuffMission), {buff, buffLevel, buffMission}) + lu.assertItemsEquals(entity:GetAll(OtherComp), {other}) + lu.assertItemsEquals(entity:GetAll(), {other, buff, buffLevel, buffMission}) + + + -- get primary + lu.assertEquals(buff:Primary(), buff) + lu.assertEquals(buffLevel:Primary(), buff) + lu.assertEquals(buffMission:Primary(), buff) + + -- get qualified + lu.assertEquals(buff:Qualified("Primary"), buff) + lu.assertEquals(buffLevel:Qualified("Primary"), buff) + lu.assertEquals(buffMission:Qualified("Primary"), buff) + + lu.assertEquals(buff:Qualified("Level"), buffLevel) + lu.assertEquals(buffLevel:Qualified("Level"), buffLevel) + lu.assertEquals(buffMission:Qualified("Level"), buffLevel) + + lu.assertEquals(buff:Qualified("Mission"), buffMission) + lu.assertEquals(buffLevel:Qualified("Mission"), buffMission) + lu.assertEquals(buffMission:Qualified("Mission"), buffMission) + + -- all + lu.assertEquals(buff:QualifiedAll(), { + ["Primary"] = buff, ["Level"] = buffLevel, ["Mission"] = buffMission + }) + lu.assertEquals(buffLevel:QualifiedAll(), { + ["Primary"] = buff, ["Level"] = buffLevel, ["Mission"] = buffMission + }) + lu.assertEquals(buffMission:QualifiedAll(), { + ["Primary"] = buff, ["Level"] = buffLevel, ["Mission"] = buffMission + }) + + -- unset + entity[HealthBuffLevel] = nil + lu.assertItemsEquals(entity:GetAll(HealthBuff), {buff, buffMission}) + lu.assertItemsEquals(entity:GetAll(HealthBuffLevel), {buff, buffMission}) + lu.assertItemsEquals(entity:GetAll(HealthBuffMission), {buff, buffMission}) + lu.assertItemsEquals(entity:GetAll(OtherComp), {other}) + lu.assertItemsEquals(entity:GetAll(), {other, buff, buffMission}) + lu.assertEquals(buff:QualifiedAll(), { + ["Primary"] = buff, ["Mission"] = buffMission + }) + lu.assertEquals(buffLevel:QualifiedAll(), { + ["Level"] = buffLevel + }) + lu.assertEquals(buffMission:QualifiedAll(), { + ["Primary"] = buff, ["Mission"] = buffMission + }) + + -- unset + entity:Unset(HealthBuff) + lu.assertItemsEquals(entity:GetAll(HealthBuff), {buffMission}) + lu.assertItemsEquals(entity:GetAll(HealthBuffLevel), {buffMission}) + lu.assertItemsEquals(entity:GetAll(HealthBuffMission), {buffMission}) + lu.assertItemsEquals(entity:GetAll(OtherComp), {other}) + lu.assertItemsEquals(entity:GetAll(), {other, buffMission}) + lu.assertEquals(buff:QualifiedAll(), { + ["Primary"] = buff + }) + lu.assertEquals(buffLevel:QualifiedAll(), { + ["Level"] = buffLevel + }) + lu.assertEquals(buffMission:QualifiedAll(), { + ["Mission"] = buffMission + }) + + -- again + local buff, buffLevel, buffMission, other = instanciate(entity) + -- all + lu.assertEquals(buff:QualifiedAll(), { + ["Primary"] = buff, ["Level"] = buffLevel, ["Mission"] = buffMission + }) + lu.assertEquals(buffLevel:QualifiedAll(), { + ["Primary"] = buff, ["Level"] = buffLevel, ["Mission"] = buffMission + }) + lu.assertEquals(buffMission:QualifiedAll(), { + ["Primary"] = buff, ["Level"] = buffLevel, ["Mission"] = buffMission + }) + + -- unset + entity[HealthBuffLevel] = nil + lu.assertItemsEquals(entity:GetAll(HealthBuff), {buff, buffMission}) + lu.assertItemsEquals(entity:GetAll(HealthBuffLevel), {buff, buffMission}) + lu.assertItemsEquals(entity:GetAll(HealthBuffMission), {buff, buffMission}) + lu.assertItemsEquals(entity:GetAll(OtherComp), {other}) + lu.assertItemsEquals(entity:GetAll(), {other, buff, buffMission}) + lu.assertEquals(buff:QualifiedAll(), { + ["Primary"] = buff, ["Mission"] = buffMission + }) + lu.assertEquals(buffLevel:QualifiedAll(), { + ["Level"] = buffLevel + }) + lu.assertEquals(buffMission:QualifiedAll(), { + ["Primary"] = buff, ["Mission"] = buffMission + }) + + -- unset + entity:Unset(HealthBuff) + lu.assertItemsEquals(entity:GetAll(HealthBuff), {buffMission}) + lu.assertItemsEquals(entity:GetAll(HealthBuffLevel), {buffMission}) + lu.assertItemsEquals(entity:GetAll(HealthBuffMission), {buffMission}) + lu.assertItemsEquals(entity:GetAll(OtherComp), {other}) + lu.assertItemsEquals(entity:GetAll(), {other, buffMission}) + lu.assertEquals(buff:QualifiedAll(), { + ["Primary"] = buff + }) + lu.assertEquals(buffLevel:QualifiedAll(), { + ["Level"] = buffLevel + }) + lu.assertEquals(buffMission:QualifiedAll(), { + ["Mission"] = buffMission + }) + end + + doTest(function(entity) + local buff = HealthBuff() + local buffLevel = HealthBuffLevel() + local buffMission = HealthBuffMission() + local other = OtherComp() + + entity:Set(buff) + entity:Set(buffLevel) + entity:Set(buffMission) + entity:Set(other) + + return buff, buffLevel, buffMission, other + end) + + doTest(function(entity) + local buff = HealthBuff() + local buffLevel = HealthBuffLevel() + local buffMission = HealthBuffMission() + local other = OtherComp() + + entity[HealthBuff] = buff + entity[HealthBuffLevel] = buffLevel + entity[HealthBuffMission] = buffMission + entity[OtherComp] = other + + return buff, buffLevel, buffMission, other + end) + + doTest(function(entity) + + entity[HealthBuff] = {} + entity[HealthBuffLevel] = {} + entity[HealthBuffMission] = {} + entity[OtherComp] = {} + + return entity[HealthBuff], entity[HealthBuffLevel] , entity[HealthBuffMission], entity[OtherComp] + end) + +end