Skip to content
This repository has been archived by the owner on Jul 16, 2024. It is now read-only.

Return chainable iterator after QueryResult:without() #90

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9a84577
Add check for config
Ukendio Dec 20, 2023
89c3f2c
Prototyped implementation
Ukendio Dec 20, 2023
3b8d5d7
Remove whitspace
Ukendio Dec 20, 2023
28e1c9e
Added unit tests
Ukendio Dec 20, 2023
7a93fd7
Preserve order of query traversal
Ukendio Dec 20, 2023
a46a97a
Merge branch 'main' of https://github.com/evaera/matter into views
Ukendio Dec 21, 2023
aa3d0f4
Adding comments
Ukendio Dec 21, 2023
0a2f6df
Merge branch 'main' of https://github.com/evaera/matter into views
Ukendio Dec 21, 2023
4868ba6
Remove entity param from view
Ukendio Dec 21, 2023
7f67d1d
Oops.
Ukendio Dec 21, 2023
e70a590
Add "_" to unused variables
Ukendio Dec 21, 2023
84e1c36
Change otherRoot name in tests
Ukendio Dec 21, 2023
79822a7
Merge branch 'main' of https://github.com/evaera/matter into views
Ukendio Dec 21, 2023
817d1df
Put keys in a linked list
Ukendio Dec 21, 2023
e7889b9
Add checks for views:get()
Ukendio Dec 21, 2023
09bd251
Initial Commit
Ukendio Dec 22, 2023
ad24560
iterate self._filter instead of entityData
Ukendio Dec 22, 2023
ebb1f85
Create a NOOP
Ukendio Dec 22, 2023
03a31b5
Remove unused function
Ukendio Dec 22, 2023
e0ed459
Moved functions under corresponding comments
Ukendio Dec 22, 2023
bdae1c4
Initial commit
Ukendio Dec 22, 2023
fe4ec1d
Expand should be a closure for inlining
Ukendio Dec 22, 2023
0c97ff7
Merge branch 'query-iter' of https://github.com/evaera/matter into ar…
Ukendio Dec 22, 2023
fad7489
Make :_next into a closure instead
Ukendio Dec 23, 2023
4e9eecf
Merge branch 'query-iter' of https://github.com/evaera/matter into ar…
Ukendio Dec 23, 2023
a7fe37a
remove topoRuntime import
Ukendio Dec 23, 2023
1cc5144
Make struct smaller
Ukendio Dec 23, 2023
ac1d737
Merge branch 'query-iter' of https://github.com/evaera/matter into ar…
Ukendio Dec 23, 2023
a53ce69
Change split to ||
Ukendio Dec 23, 2023
ef5d8fe
Merge branch 'views' of https://github.com/evaera/matter into archety…
Ukendio Dec 23, 2023
4e9a28b
Moving back some of the query logic
Ukendio Dec 24, 2023
c744828
Return static noopQuery
Ukendio Dec 24, 2023
e794dd6
Port changes from archetype negation branch
Ukendio Dec 24, 2023
cc73650
Initial commit
Ukendio Dec 24, 2023
70d74c9
Revert "Initial commit"
Ukendio Dec 24, 2023
2ae303a
Revert "Revert "Initial commit""
Ukendio Dec 24, 2023
9cd23e5
Revert "Port changes from archetype negation branch"
Ukendio Dec 24, 2023
5009e3b
Revert "Revert "Port changes from archetype negation branch""
Ukendio Dec 24, 2023
d7aecf8
Revert "Initial commit"
Ukendio Dec 24, 2023
d605f67
Iterable NOOP
Ukendio Dec 24, 2023
aac0f7c
noop:without returns noop
Ukendio Dec 24, 2023
7f7e8e1
Return self
Ukendio Dec 24, 2023
87e0cac
Update storage index on empty storage
Ukendio Dec 24, 2023
c8e89d7
Merge branch 'main' of https://github.com/evaera/matter into query-iter
Ukendio Dec 31, 2023
5c3a342
Separate changes from archetype-negation
Ukendio Dec 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 3 additions & 256 deletions lib/World.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ local assertValidComponent = Component.assertValidComponent
local archetypeOf = archetypeModule.archetypeOf
local areArchetypesCompatible = archetypeModule.areArchetypesCompatible

local QueryResult = require(script.Parent.query)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why the file was named "query" instead of "QueryResult"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly because I am conflicted on what to name. I would much prefer calling the file query, but that might mean we have to rename the struct to query as well which I am not sure how I feel about that yet.

What do you suggest?


local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed"

--[=[
Expand Down Expand Up @@ -370,169 +372,6 @@ function World:get(id, ...)
return unpack(components, 1, length)
end

--[=[
@class QueryResult

A result from the [`World:query`](/api/World#query) function.

Calling the table or the `next` method allows iteration over the results. Once all results have been returned, the
QueryResult is exhausted and is no longer useful.

```lua
for id, enemy, charge, model in world:query(Enemy, Charge, Model) do
-- Do something
end
```
]=]
local QueryResult = {}
QueryResult.__index = QueryResult

function QueryResult:__call()
return self._expand(self._next())
end

function QueryResult:__iter()
return function()
return self._expand(self._next())
end
end

--[=[
Returns the next set of values from the query result. Once all results have been returned, the
QueryResult is exhausted and is no longer useful.

:::info
This function is equivalent to calling the QueryResult as a function. When used in a for loop, this is implicitly
done by the language itself.
:::

```lua
-- Using world:query in this position will make Lua invoke the table as a function. This is conventional.
for id, enemy, charge, model in world:query(Enemy, Charge, Model) do
-- Do something
end
```

If you wanted to iterate over the QueryResult without a for loop, it's recommended that you call `next` directly
instead of calling the QueryResult as a function.
```lua
local id, enemy, charge, model = world:query(Enemy, Charge, Model):next()
local id, enemy, charge, model = world:query(Enemy, Charge, Model)() -- Possible, but unconventional
```

@return id -- Entity ID
@return ...ComponentInstance -- The requested component values
]=]
function QueryResult:next()
return self._expand(self._next())
end

local snapshot = {
__iter = function(self): any
local i = 0
return function()
i += 1

local data = self[i]

if data then
return unpack(data, 1, data.n)
end
return
end
end,
}

--[=[
Creates a "snapshot" of this query, draining this QueryResult and returning a list containing all of its results.

By default, iterating over a QueryResult happens in "real time": it iterates over the actual data in the ECS, so
changes that occur during the iteration will affect future results.

By contrast, `QueryResult:snapshot()` creates a list of all of the results of this query at the moment it is called,
so changes made while iterating over the result of `QueryResult:snapshot` do not affect future results of the
iteration.

Of course, this comes with a cost: we must allocate a new list and iterate over everything returned from the
QueryResult in advance, so using this method is slower than iterating over a QueryResult directly.

The table returned from this method has a custom `__iter` method, which lets you use it as you would use QueryResult
directly:

```lua
for entityId, health, player in world:query(Health, Player):snapshot() do

end
```

However, the table itself is just a list of sub-tables structured like `{entityId, component1, component2, ...etc}`.

@return {{entityId: number, component: ComponentInstance, component: ComponentInstance, component: ComponentInstance, ...}}
]=]
function QueryResult:snapshot()
local list = setmetatable({}, snapshot)

local function iter()
return self._next()
end

for entityId, entityData in iter do
if entityId then
table.insert(list, table.pack(self._expand(entityId, entityData)))
end
end

return list
end

--[=[
Returns an iterator that will skip any entities that also have the given components.

:::tip
This is essentially equivalent to querying normally, using `World:get` to check if a component is present,
and using Lua's `continue` keyword to skip this iteration (though, using `:without` is faster).

This means that you should avoid queries that return a very large amount of results only to filter them down
to a few with `:without`. If you can, always prefer adding components and making your query more specific.
:::

@param ... Component -- The component types to filter against.
@return () -> (id, ...ComponentInstance) -- Iterator of entity ID followed by the requested component values

```lua
for id in world:query(Target):without(Model) do
-- Do something
end
```
]=]
function QueryResult:without(...)
local metatables = { ... }
return function(): any
while true do
local entityId, entityData = self._next()

if not entityId then
break
end

local skip = false
for _, metatable in ipairs(metatables) do
if entityData[metatable] then
skip = true
break
end
end

if skip then
continue
end

return self._expand(entityId, entityData)
end
return
end
end

--[=[
Performs a query against the entities in this World. Returns a [QueryResult](/api/QueryResult), which iterates over
the results of the query.
Expand All @@ -553,99 +392,7 @@ end
@return QueryResult -- See [QueryResult](/api/QueryResult) docs.
]=]
function World:query(...)
debug.profilebegin("World:query")
assertValidComponent((...), 1)

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

local archetype = archetypeOf(...)

if self._queryCache[archetype] == nil then
self:_newQueryArchetype(archetype)
end

local compatibleArchetypes = self._queryCache[archetype]

debug.profileend()

if next(compatibleArchetypes) == nil then
-- If there are no compatible storages avoid creating our complicated iterator
return setmetatable({
_expand = function() end,
_next = function() end,
}, QueryResult)
end

local queryOutput = table.create(queryLength)

local function expand(entityId, entityData)
if not entityId then
return
end

for i, metatable in ipairs(metatables) do
queryOutput[i] = entityData[metatable]
end

return entityId, unpack(queryOutput, 1, queryLength)
end

local currentCompatibleArchetype = next(compatibleArchetypes)
local lastEntityId
local storageIndex = 1

if self._pristineStorage == self._storages[1] then
self:_markStorageDirty()
end

local seenEntities = {}

local function nextItem()
local entityId, entityData

repeat
if self._storages[storageIndex][currentCompatibleArchetype] then
entityId, entityData = next(self._storages[storageIndex][currentCompatibleArchetype], lastEntityId)
end

while entityId == nil do
currentCompatibleArchetype = next(compatibleArchetypes, currentCompatibleArchetype)

if currentCompatibleArchetype == nil then
storageIndex += 1

local nextStorage = self._storages[storageIndex]

if nextStorage == nil or next(nextStorage) == nil then
return
end

currentCompatibleArchetype = nil

if self._pristineStorage == nextStorage then
self:_markStorageDirty()
end

continue
elseif self._storages[storageIndex][currentCompatibleArchetype] == nil then
continue
end

entityId, entityData = next(self._storages[storageIndex][currentCompatibleArchetype])
end
lastEntityId = entityId

until seenEntities[entityId] == nil

seenEntities[entityId] = true
return entityId, entityData
end

return setmetatable({
_expand = expand,
_next = nextItem,
}, QueryResult)
return QueryResult.new(self, ...)
end

local function cleanupQueryChanged(hookState)
Expand Down
7 changes: 5 additions & 2 deletions lib/World.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -505,9 +505,10 @@ return function()
})
)

local snapshot = world:query(Health, Player):snapshot()
local query = world:query(Health, Player)
local snapshot = query:snapshot()

for entityId, health, player in world:query(Health, Player):snapshot() do
for entityId, health, player in snapshot do
expect(type(entityId)).to.equal("number")
expect(type(player.name)).to.equal("string")
expect(type(health.value)).to.equal("number")
Expand All @@ -521,6 +522,8 @@ return function()
else
expect(snapshot[2][1]).to.equal(1)
end

expect(#world:query(Player):without(Poison):snapshot()).to.equal(1)
end)

it("should not invalidate iterators", function()
Expand Down
Loading