Iterators

Talk about creating Grimrock 1 levels and mods here. Warning: forum contains spoilers!
Post Reply
Marble Mouth
Posts: 52
Joined: Sun Feb 10, 2013 12:46 am
Location: Dorchester, MA, USA

Iterators

Post by Marble Mouth »

Hi everyone. Previously, I wrote a redefinition for allEntities to avoid a certain bug that was identified with the standard definition. I have also written some non-standard iterators for situations that I see discussed frequently on here:

Edit: There are some flaws with the code in this post, but this documentation remains accurate. Please see replies in this thread for details of flaws and updated code.

allEntities( level ) - Just another equivalent redefinition of the same thing.

entitiesWithin( level , corner1x , corner1y , corner2x , corner2y ) - You specify the coordinates for a bounding box (inclusive) and it gives you the entitiesAt each tile within that box.

carriedItems( champion ) - champion may be either a number or a champion returned by party:getChampion(slot) . If it is a number, then the code just calls party:getChampion(champion) to convert it. It gives you all the items carried by that champion in any equipment or backpack slot, and all the items that are contained within items carried by that champion.

allCarriedItems() - It gives you all the items carried by any champion in any equipment or backpack slot, and all the items that are contained within items carried by any champion, and the mouse item, and all items that are contained within the mouse item.

Code: Select all

allEntities = function( level )
	return entitiesWithin( level , 0 , 0 , 31 , 31 )
end

entitiesWithin = function( level , corner1x , corner1y , corner2x , corner2y )
--The arguments should specify two corners of a rectangle,
--diagonally across from one another

	if corner1x > corner2x then
		corner1x , corner2x = corner2x , corner1x
	end
	if corner1y > corner2y then
		corner1y , corner2y = corner2y , corner1y
	end

	local state = {}
	state["offset"] = 0
	local count = 0
	
	for i = corner1x , corner2x do
		for j = corner1y , corner2y do
			for k in entitiesAt( level , i , j ) do
				count = count + 1
				state[count] = k
			end
		end
	end

	return advance , state , nil
end

carriedItems = function( champion )
--champion may be an actual champion, or a number

	if type( champion ) == "number" then
		champion = party:getChampion( champion )
	end
	if not champion then
		return advance , { offset = 0 } , nil
	end

	local state = {}
	state["offset"] = 0
	local count = 0
	
	for i = 1 , 31 do
		local item = champion:getItem( i )
		if item then
			for containedItem in item:containedItems() do
				count = count + 1
				state[count] = containedItem
			end
			count = count + 1
			state[count] = item
		end
	end

	return advance , state , nil
end

allCarriedItems = function()

	local state = {}
	state["offset"] = 0
	local count = 0
	
	for i = 1 , 4 do
		for item in carriedItems( i ) do
			count = count + 1
			state[count] = item
		end
	end
	
	local mouseItem = getMouseItem()
	if mouseItem then
		for containedItem in mouseItem:containedItems() do
			count = count + 1
			state[count] = containedItem
		end
		count = count + 1
		state[count] = mouseItem
	end
	
	return advance , state , nil
end

------------------------------------------
--[[Functions below this line are intended
for internal use only.
--]]

advance = function ( state , _ )

	local offset = state["offset"] + 1
	state["offset"] = offset

	return state[offset]
end
If you'd like to use any of these iterators, I would recommend pasting this code into a script_entity ( hereafter referred to as "iteratorScript" ) and then in your other script_entity where you want to use the iterators, follow either of these approaches:

Code: Select all

for item in iteratorScript.allCarriedItems() do
   if item.name == "torch" then
      --do something with that item
   end
end
or

Code: Select all

--At the top of your script_entity
allCarriedItems = iteratorScript.allCarriedItems

--other code here

for item in allCarriedItems() do
   if item.name == "torch" then
      --do something with that item
   end
end
As always, if you find any bugs or want to request any additional features, don't hesitate to post it here :)

Edit: allCarriedItems now includes items contained inside the mouse item.
Last edited by Marble Mouth on Fri Mar 22, 2013 2:14 am, edited 2 times in total.
User avatar
Diarmuid
Posts: 807
Joined: Thu Nov 22, 2012 6:59 am
Location: Montreal, Canada
Contact:

Re: Iterators

Post by Diarmuid »

That's really cool, thanks.

I didn't know you could override/hide global functions of the lua state, but now I think about it it makes sense, as functions work like variables.

I know Xanathar's grimq offered already a lot of this in a linq like format, but thats a good addition as easier to read/use for less advances coders.
Grimfan
Posts: 369
Joined: Wed Jan 02, 2013 7:48 am

Re: Iterators

Post by Grimfan »

Thanks for writing these Marble Mouth. Even if I don't use them in the dungeon I'm currently working on it's good to know that these codes are out there if I ever do need to use them in the future.

Much respect. :)
Marble Mouth
Posts: 52
Joined: Sun Feb 10, 2013 12:46 am
Location: Dorchester, MA, USA

Re: Iterators

Post by Marble Mouth »

I did find an... issue. I don't know if it's a bug. Suppose you have a crafting item (e.g. a mortar.) And that crafting items contains all the ingredients for a valid recipe(e.g. a flask and a tar_bead.) And now you can see the ingredients at the top, and the result (e.g. a Healing Potion) at the bottom. If you click the result, the ingredients disappear. But you could also just leave it like that without clicking the result to complete the crafting. In this scenario, if you do:

Code: Select all

for item in mortar_1:containedItems() do
   print(item.name)
end
then you'll see:
flask
tar_bead
potion_healing

which kind of makes sense, I suppose. At any rate, this is the behavior of item:containedItems(), which my code relies upon. Just keep this possibility in mind if you're going to use item:containedItems() with or without these iterators.
Diarmuid wrote: it makes sense, as functions work like variables.
Technically, functions are values. Function is a type of value, just like number, string, or table. Variables in lua can always hold a value of any type, including function. For example, there is a variable called "hudPrint" which normally contains a function that prints strings to the HUD. But you could do this:

Code: Select all

hudPrint = print
which makes that variable "hudPrint" contain a different function. If you then did this:

Code: Select all

hudPrint("hello")
you would see "hello" printed to the console, not the HUD. You could even do this:

Code: Select all

hudPrint = 12
without lua complaining. But then if you did this:

Code: Select all

hudPrint("hello")
you would get this error: "attempt to call global hudPrint (a number value)". You get this error because you can call functions, and functions are the only thing you can call. Side note: hudPrint can only accept a string, and it only accepts one argument at a time. print can accept an argument of any type, and it can accept multiple arguments.

Also, here's some more code that uses one of these iterators:

Code: Select all

findEntityAnywhere = function( id )

	local entity = findEntity( id )
	if entity then
		return entity
	end
	
	for item in iteratorScript.allCarriedItems() do
		if item.id == id then
			return item
		end
	end
end
findEntity will not actually find items in the inventory. This function will. It still returns nil when the item couldn't be found anywhere.
User avatar
Xanathar
Posts: 629
Joined: Sun Apr 15, 2012 10:19 am
Location: Torino, Italy
Contact:

Re: Iterators

Post by Xanathar »

Just a couple of notes as I walked that minefield before :o
The containers have some weird issues. The biggest one is that the mortar can be put into a container. This has two effects: first of all 2 levels of recursion, second you can leave the outmost container behind and still have the window of the mortar open (game bug).
Btw: I think (IIRC) that findEntityAnywhere does not find items inside containers left in the world or in alcoves.

My current version (forgive me the grimq syntax - consider it pseudocode as far as this is concerned) is:

Code: Select all

function find(id)
	local entity = findEntity(id)
	if (entity ~= nil) then	return entity; end
	
	entity = fromPartyInventory(true, inventory.all, true):where("id", id):first()
	if (entity ~= nil) then	return entity; end
	
	local containers = fromAllEntitiesInWorld(isItem)
				:selectMany(function(i) return from(i:containedItems()):toArray(); end)
	
	entity = containers
		:where(function(ii) return ii.id == id; end)
		:first()
	
	if (entity ~= nil) then	return entity; end
		
	entity = containers
		:selectMany(function(i) return from(i:containedItems()):toArray(); end)
		:where(function(ii) return ii.id == id; end)
		:first()
		
	return entity
end
Edit: btw - finding an item anywhere is a major pain :)
Waking Violet (Steam, PS4, PSVita, Switch) : http://www.wakingviolet.com

The Sunset Gate [MOD]: viewtopic.php?f=14&t=5563

My preciousss: http://www.moonsharp.org
Marble Mouth
Posts: 52
Joined: Sun Feb 10, 2013 12:46 am
Location: Dorchester, MA, USA

Re: Iterators

Post by Marble Mouth »

Ah, thanks Xanathar. You're right, the version of findEntityAnywhere that I posted above won't find items inside containers on the ground, but it will find items in alcoves. Also, I didn't account for nested containers because originally I only tested with wooden boxes, which don't nest. Oh well, I won't be deterred.

Here's some better code:

Code: Select all

allEntities = function( level )
	return entitiesWithin( level , 0 , 0 , 31 , 31 )
end

entitiesWithin = function( level , corner1x , corner1y , corner2x , corner2y )

	if corner1x > corner2x then
		corner1x , corner2x = corner2x , corner1x
	end
	if corner1y > corner2y then
		corner1y , corner2y = corner2y , corner1y
	end

	local state = {}
	state["offset"] = 0
	local count = 0
	
	for i = corner1x , corner2x do
		for j = corner1y , corner2y do
			for k in entitiesAt( level , i , j ) do
				count = count + 1
				state[count] = k
			end
		end
	end

	return advance , state , nil
end

allContainedItems = function( item )
	
	local state = {}
	state["offset"] = 0
	local count = 0
	
	if not item.containedItems then
		return advance , { offset = 0 } , nil
	end
	
	for containedItem in item:containedItems() do
		count = count + 1
		state[count] = containedItem
		for nestedItem in allContainedItems( containedItem ) do
			count = count + 1
			state[count] = nestedItem
		end
	end
	
	return advance , state , nil
end

carriedItems = function( champion )
--champion may be an actual champion, or a number

	if type( champion ) == "number" then
		champion = party:getChampion( champion )
	end
	if not champion then
		return advance , { offset = 0 } , nil
	end

	local state = {}
	state["offset"] = 0
	local count = 0
	
	for i = 1 , 31 do
		local item = champion:getItem( i )
		if item then
			for containedItem in allContainedItems(item) do
				count = count + 1
				state[count] = containedItem
			end
			count = count + 1
			state[count] = item
		end
	end

	return advance , state , nil
end

allCarriedItems = function()

	local state = {}
	state["offset"] = 0
	local count = 0
	
	for i = 1 , 4 do
		for item in carriedItems( i ) do
			count = count + 1
			state[count] = item
		end
	end
	
	local mouseItem = getMouseItem()
	if mouseItem then
		for containedItem in allContainedItems(mouseItem) do
			count = count + 1
			state[count] = containedItem
		end
		count = count + 1
		state[count] = mouseItem
	end
	
	return advance , state , nil
end

absolutelyAllItems = function()
	
	local state = {}
	state["offset"] = 0
	local count = 0
	
	for item in allCarriedItems() do
		count = count + 1
		state[count] = item
	end
	
	for level = 1 , getMaxLevels() do
		for item in allEntities( level ) do
			count = count + 1
			state[count] = item
			if item.class == "Item" then
			--Don't look inside alcoves, because their
			--contents are already found by allEntities
				for containedItem in allContainedItems( item ) do
					count = count + 1
					state[count] = containedItem
				end
			end
		end
	end
	
	return advance , state , nil
end

findEntityAnywhere = function( id )	
	for item in absolutelyAllItems() do
		if item.id == id then
			return item
		end
	end
end

------------------------------------------
--[[Functions below this line are intended
for internal use only.
--]]

advance = function ( state , _ )

	local offset = state["offset"] + 1
	state["offset"] = offset

	return state[offset]
end
Have I missed anything else?
The documentation from the OP still applies to several of the functions in the code above. Functions contained here, but not documented in the OP:

allContainedItems( item ) - Similar to the standard item:containedItems() , except that it finds items inside nested containers.

absolutelyAllItems() - This will (should) give you every item, everywhere.

findEntityAnywhere( id ) - This is not an iterator. It is used similarly to findEntity( id ). It calls absolutelyAllItems to find an item with an id matching the argument. In my testing, this function successfully found a flask inside a mortar, inside a wooden box, in an alcove.
Xanathar wrote: you can leave the outmost container behind and still have the window of the mortar open (game bug).
That bug is quite exploitable :shock: . It allows you to walk around with a mortar-of-holding. It goes away when you close the champion's inventory, and comes back when you open that same champion's inventory. The items in the mortar don't count against your carrying Load, although it does only have six slots.

And by the way, I have lots of respect for what you've done with grimq, but I don't use it because I have never learned linq. One of my goals in writing tools is to make the learning curve as shallow as possible. I think that if a modder has figured out how to use allEntities , they should be able to use allCarriedItems similarly. I design pretty much everything from the outside in: I start with how I expect it to be used, and that influences how it will work internally.
User avatar
Xanathar
Posts: 629
Joined: Sun Apr 15, 2012 10:19 am
Location: Torino, Italy
Contact:

Re: Iterators

Post by Xanathar »

And by the way, I have lots of respect for what you've done with grimq
I don't :) A library with just 3 users (author included) is quite a failure :(
One of my goals in writing tools is to make the learning curve as shallow as possible.
That's a very good point, and I think it is the reason of the failure of grimq - it is perceived to be too difficult. For example the carriedItems() iterator would be grimq.fromPartyInventory():toIterator() which is longer but not really anymore difficult than the carriedItems() function, but the whole syntax and introduction does move the user out of his comfort zone and thus the library discarded. So, go on with this :D

BTW there is a philosophical difference between this and grimq in that grimq uses temporary structures to work while yours uses only iterators; I'd guess your implementation to be slightly faster too - one day I might convert grimq to iterators but given the number of users and the fact that in my mod I do many world-wide queries with no visible performance impact, I don't think I'll ever do that really (at the very least I should first release the current version with some tons of bugs fixed :D )
Waking Violet (Steam, PS4, PSVita, Switch) : http://www.wakingviolet.com

The Sunset Gate [MOD]: viewtopic.php?f=14&t=5563

My preciousss: http://www.moonsharp.org
User avatar
Diarmuid
Posts: 807
Joined: Thu Nov 22, 2012 6:59 am
Location: Montreal, Canada
Contact:

Re: Iterators

Post by Diarmuid »

Hey, I DID use grimq...

But I agree about clarity and the points you make.
User avatar
Xanathar
Posts: 629
Joined: Sun Apr 15, 2012 10:19 am
Location: Torino, Italy
Contact:

Re: Iterators

Post by Xanathar »

Hey, I DID use grimq...
I counted you in the 3 :D
Waking Violet (Steam, PS4, PSVita, Switch) : http://www.wakingviolet.com

The Sunset Gate [MOD]: viewtopic.php?f=14&t=5563

My preciousss: http://www.moonsharp.org
Post Reply