Skip to content

Commit

Permalink
Improve World error messages
Browse files Browse the repository at this point in the history
Closes #8
  • Loading branch information
evaera committed Jun 4, 2022
1 parent 6cedc31 commit 4581a4f
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- `queryChanged` behavior has changed slightly: If an entity's storage was changed multiple times since your system last observed it, the `old` field in the `ChangeRecord` will be the last value your system observed the entity as having for that component, rather than what it was most recently changed from.
- World and Loop types are now exported (#9)
- Matter now uses both `__iter` and `__call` for iteration over `QueryResult`.
- Improved many error messages from World methods, including passing nil values or passing a Component instead of a Component instance.

### Fixed
- System error stack traces are now displayed properly (#12)
Expand Down
35 changes: 35 additions & 0 deletions lib/Component.lua
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ local Llama = require(script.Parent.Parent.Llama)
```
]=]

-- This is a special value we set inside the component's metatable that will allow us to detect when
-- a Component is accidentally inserted as a Component Instance.
-- It should not be accessible through indexing into a component instance directly.
local DIAGNOSTIC_COMPONENT_MARKER = {}

local function newComponent(name)
name = name or debug.info(2, "s") .. "@" .. debug.info(2, "l")

Expand Down Expand Up @@ -93,11 +98,41 @@ local function newComponent(name)
__tostring = function()
return name
end,
[DIAGNOSTIC_COMPONENT_MARKER] = true,
})

return component
end

local function assertValidComponent(value, position)
if typeof(value) ~= "table" then
error(string.format("Component #%d is invalid: not a table", position), 3)
end

local metatable = getmetatable(value)

if metatable == nil then
error(string.format("Component #%d is invalid: has no metatable", position), 3)
end
end

local function assertValidComponentInstance(value, position)
assertValidComponent(value, position)

if getmetatable(value)[DIAGNOSTIC_COMPONENT_MARKER] ~= nil then
error(
string.format(
"Component #%d is invalid: passed a Component instead of a Component instance; "
.. "did you forget to call it as a function?",
position
),
3
)
end
end

return {
newComponent = newComponent,
assertValidComponentInstance = assertValidComponentInstance,
assertValidComponent = assertValidComponent,
}
21 changes: 21 additions & 0 deletions lib/Component.spec.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
local Component = require(script.Parent.Component)
local None = require(script.Parent.Parent.Llama).None
local component = Component.newComponent
local assertValidComponentInstance = Component.assertValidComponentInstance

return function()
describe("Component", function()
Expand Down Expand Up @@ -39,4 +40,24 @@ return function()
expect(a2.baz).to.equal("qux")
end)
end)

describe("assertValidComponentInstance", function()
it("should throw on invalid components", function()
expect(function()
assertValidComponentInstance({})
end).to.throw()

expect(function()
assertValidComponentInstance(55)
end).to.throw()

expect(function()
assertValidComponentInstance(component())
end).to.throw()

expect(function()
assertValidComponentInstance(component().new())
end).never.to.throw()
end)
end)
end
22 changes: 21 additions & 1 deletion lib/World.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
local Archetype = require(script.Parent.Archetype)
local TopoRuntime = require(script.Parent.TopoRuntime)
local Component = require(script.Parent.Component)

local assertValidComponentInstance = Component.assertValidComponentInstance
local assertValidComponent = Component.assertValidComponent
local archetypeOf = Archetype.archetypeOf
local areArchetypesCompatible = Archetype.areArchetypesCompatible

Expand Down Expand Up @@ -61,6 +64,9 @@ function World:spawn(...)

for i = 1, select("#", ...) do
local newComponent = select(i, ...)

assertValidComponentInstance(newComponent, i)

local metatable = getmetatable(newComponent)

if components[metatable] then
Expand Down Expand Up @@ -150,6 +156,9 @@ function World:replace(id, ...)

for i = 1, select("#", ...) do
local newComponent = select(i, ...)

assertValidComponentInstance(newComponent, i)

local metatable = getmetatable(newComponent)

if components[metatable] then
Expand Down Expand Up @@ -234,12 +243,15 @@ function World:get(id, ...)
local length = select("#", ...)

if length == 1 then
assertValidComponent((...), 1)
return entity[...]
end

local components = {}
for i = 1, length do
components[i] = entity[select(i, ...)]
local metatable = select(i, ...)
assertValidComponent(metatable, i)
components[i] = entity[metatable]
end

return unpack(components, 1, length)
Expand Down Expand Up @@ -383,6 +395,8 @@ end
]=]
function World:query(...)
debug.profilebegin("World:query")
assertValidComponent((...), 1)

local metatables = { ... }
local queryLength = select("#", ...)

Expand Down Expand Up @@ -642,7 +656,11 @@ function World:insert(id, ...)
local wasNew = false
for i = 1, select("#", ...) do
local newComponent = select(i, ...)

assertValidComponentInstance(newComponent, i)

local metatable = getmetatable(newComponent)

local oldComponent = existingComponents[metatable]

if not oldComponent then
Expand Down Expand Up @@ -687,6 +705,8 @@ function World:remove(id, ...)
for i = 1, length do
local metatable = select(i, ...)

assertValidComponent(metatable, i)

local oldComponent = existingComponents[metatable]

removed[i] = oldComponent
Expand Down
25 changes: 23 additions & 2 deletions lib/World.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ return function()
)

world:spawn( -- Spawn something we don't want to get back
component(),
component()
component()(),
component()()
)

local two = world:spawn(
Expand Down Expand Up @@ -342,5 +342,26 @@ return function()

infrequentBindable:Fire()
end)

it("should error when passing nil to query", function()
expect(function()
World.new():query(nil)
end).to.throw()
end)

it("should error when passing an invalid table", function()
local world = World.new()
local id = world:spawn()

expect(function()
world:insert(id, {})
end).to.throw()
end)

it("should error when passing a Component instead of Component instance", function()
expect(function()
World.new():spawn(component())
end).to.throw()
end)
end)
end

0 comments on commit 4581a4f

Please sign in to comment.