Making list of inventory

Discussion in 'General Modification' started by marc1967, Mar 4, 2015.

Remove all ads!
  1. marc1967

    marc1967 Established Member

    Joined:
    Jan 19, 2014
    Messages:
    578
    Likes Received:
    60
    Is there a smooth way to get a complete list of inventory for an NPC or other container?

    It seems that the only way to do this is by using brute force to cycle through every possible proto (4000 - 13000) , and then using obj.item_find_by_proto() repeatedly to see what's there.

    This method was used in clearing out the inventory in InventoryRespawn.py by Cerulean the Blue shown here:

    Code:
    def ClearInv(attachee):
    # Removes all inventory from attachee.
    	for num in range(4000, 13000):
    		item = attachee.item_find_by_proto(num)
    		while (item != OBJ_HANDLE_NULL):
    			item.destroy()
    			item = attachee.item_find_by_proto(num)
    	return

    So I assume there is NOT a convenient existing call like:

    Code:
    list = attachee.inventory()     # This call does not really exist
     
  2. Shiningted

    Shiningted I want my goat back Administrator

    Joined:
    Oct 23, 2004
    Messages:
    12,654
    Likes Received:
    352
    I don't think so: Darmagon tried it before Blue, and in the end it had to be done the hard way.

    One possible work around is to use equip_best_all() (or whatever its called) and check by slot: you can get exact figures by slot. Then destroy that object, rinse and repeat until it returns a null. Livonya used something similar for her strategy thing (Livonya.py) to flag what weapons/shields NPCs were carrying and change between melee / reach / range as appropriate. It won't pick up inventory stuff though (coins, gems, wands etc) only what is equippable. And although inventory stuff technically lives in a slot, I have not had any luck picking such things up by slot in scripts, though I have manipulated mobs that way.
     
  3. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,613
    Likes Received:
    537
    Tragically, no. Similar to the merchant inventory thing, the inventory is a non-32bit field (obj_f_critter_inventory_list_idx) and is thus inaccessible with any Python script that I know of. As with that, I hope to one day write an extension to the obj_get_int / obj_set_int scripts to enable reading from / writing to these fields. I'm actually cautiously optimistic that it shouldn't be that complicated to do.

    BTW, regarding item_wield_best_all() - it turns out that the function actually accepts an optional PyObj_Handle argument. Haven't tested what it does exactly, but perhaps it's a way to force the game to switch to a specific weapon, e.g. item_wield_best_all( <sword handle> ).

    Edit: Good news! I was able to patch the function item_worn_at() so it now also retrieves your non-worn inventory! Attached is the necessary change, highlighted in red.

    [​IMG]
    Note that the hex address starts at 650D0 - the left column cuts off one digit of the address for some reason.

    Usage:
    To access worn items, use obj.item_worn_at(#) as usual - with # between 0 and 15.

    To access inventory, type
    Code:
    obj.item_worn_at( # + 100)
    Where # denotes the inventory slot, starting from 0 (top left slot) and increasing up to 23 (bottom right slot).

    Explicitly, the indexing is
    Code:
    0   1   2   3    4   5
    6   7   8   9   10  11
    12 13 14 15  16  17
    18 19 20 21  22  23
    Using this, you can get the entire inventory directly with a for loop.

    This also works for containers, btw, except that the order is
    Code:
    0 1
    2 3
    4 5
    
    And so on.

    In case you're curious, the normal item_worn_at code simply looks for items of index 200-215, which must stand for the worn items. I surmise that this is also the reason why overfull containers cause trouble - going over 200 will probably cause the game to think a chest is trying to equip items.
     

    Attached Files:

    Last edited: Mar 5, 2015
  4. marc1967

    marc1967 Established Member

    Joined:
    Jan 19, 2014
    Messages:
    578
    Likes Received:
    60
    Wow, well that was quick , awesome! I'm thinking Sitra Achara must mean "challenge accepted" in Latin or something. Seriously, how do you find this stuff? :blink: Nevermind, I must not start digging into DLL for my own sanity.

    OK, so with this change, it's just a matter of making a function that loops thru each slot and returns a list of full inventory. The only concern is the quantity of stacked items, like potions and gems.
     
  5. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,613
    Likes Received:
    537
    Sitra Achara means "the dark side", which is a proper description for the places you reach when plunging into the bottom of ToEE's cthulhuesque DLL :p

    Fortunately I had already mapped out item_worn_at, so it wasn't so difficult, and the change is a relatively simple alteration of a switch case statement.

    Also, I'd recommend writing a "get_inventory" function that uses the above for implementation, so that if/when I create a less hacky method it could be easily replaced.

    As for stacked items, they are in fact a single item with a "quantity" property (obj_f_item_quantity). For once, this is a 32 bit field ;) so you can read/write with obj_get/set_int.

    Oh, another thing that could be helpful - you can read the amount of items in the inventory with obj_f_critter_inventory_num ( or obj_f_container_inventory_num for containers). This includes worn items and counts an item stack as one item.
     
    Last edited: Mar 5, 2015
  6. Shiningted

    Shiningted I want my goat back Administrator

    Joined:
    Oct 23, 2004
    Messages:
    12,654
    Likes Received:
    352
    Nice work sir :clap: But I for one forget why I wanted it to read inventory slots :p Glad it helped Marc.
     
  7. DarkStorm

    DarkStorm Established Member

    Joined:
    Oct 2, 2003
    Messages:
    514
    Likes Received:
    3
    > As with that, I hope to one day write an extension to the obj_get_int / obj_set_int scripts to enable reading from / writing to these fields.

    We should create issues for that in the github repo (I need to recreate that btw. You accidentally pushed the C# code again hehe).

    Also a plan: Exchange pyToee22.dll with a newer version of Python allowing for more modern constructs (and performance yay). We should probably use the names of all functions found in the python-tables to name the corresponding C function that gets called. Or did you already do that?
     
  8. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,613
    Likes Received:
    537
    @Ted: With the smell, comes the appetite ;)

    @DS: Yeah I just suggested that in the other thread :) I haven't comprehensively named all the functions, but I did go over all the PyObj methods (see the Py_Obj_* functions). Also you can go ingame and type in the console dir(<PyObj>) to get a list of the methods.
     
  9. marc1967

    marc1967 Established Member

    Joined:
    Jan 19, 2014
    Messages:
    578
    Likes Received:
    60
    There's so much to process and respond to from your posts.

    After braving the hex editor and putting in Sitra's changes to temple.dll, everything is working great, and all the slots can be accessed individually by calling obj.item_worn_at(#). And the call to obj_get_int(obj_f_item_quantity) works perfectly for finding the quantity of stacked items.

    Here's a quick function that will return a list of the objects from any container:

    Code:
    def get_inv(obj):
    	
    	slots = []
    	if obj.type in [obj_t_npc,obj_t_pc]:
    		slots = range(0,16) + range(100,124)
    	elif obj.type == obj_t_container:
    		slots = range(100,219)
    
    	inv = []
    	for s in slots:
    		item = obj.item_worn_at(s)
    		if item != OBJ_HANDLE_NULL:
    			inv.append(item)
    	return inv
    With this function, deleting the inventory is as simple as:

    Code:
    def del_inv(container):
    	for item in get_inv(container):
    		item.destroy()
    
    The main purpose for pursuing this was to be more efficient in clearing inventory for merchant restocking. Instead of looping thru 10,000+ protos and 10,000 calls to item_find_by_proto(num), I can now just check a small amount of container slots individually.

    But in addition, I find it much easier when writing scripts to have a handy list of any inventory to process and examine, instead of having to cleverly call item_find_by_proto(num) with loops when dealing with an inventory for whatever reason. The list is actually a list of the object handles instead of just "names" or proto numbers, which makes it more powerful to use, but also can get you into more trouble.

    A few quick examples, and there are probably better ones, but I'm short on time:

    - calculating to total value of gems in an inventory
    - rearranging an inventory into a particular order
    - removing all normal weapons from an inventory and replacing with masterwork
    - transferring all potions from one pc to another
    - checking how many total slots are available when receiving items like in the Thrommel reward encounter, and asking to reopen if there aren't enough.
    - magically transferring all items from one pc to an npc. (all your gear are belong to us!)

    Code:
    def give_inv(pc,npc):
    	for i in get_inv(pc):
    		npc.item_get(i)

    I think most of those examples could be done with existing means, but having the list makes it so much easier, and the code becomes very concise and easier to program and read.

    Thanks again Sitra for that hack, you made my week. :)
     
  10. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,613
    Likes Received:
    537
    Cool!

    Although, I think it's recommended to be careful when dealing with equipped items. I'd include an optional mode_select in get_inv, where the default is non-equipped inventory only.

    Code:
    def get_inv(obj, mode_select = 0):
    	
    	slots = []
    	if obj.type in [obj_t_npc,obj_t_pc]:
    		if mode_select == 0:
    			slots = range(100,124)
    		elif mode_select == 1:
    			slots = range(0,16) + range(100,124)
    		elif mode_select == 2:
    			slots = range(0,16)
    	elif obj.type == obj_t_container:
    		slots = range(100,219) ## why 219? is the actual limit 119 items?
    
    	inv = []
    	for s in slots:
    		item = obj.item_worn_at(s)
    		if item != OBJ_HANDLE_NULL:
    			inv.append(item)
    	return inv
    
     
  11. marc1967

    marc1967 Established Member

    Joined:
    Jan 19, 2014
    Messages:
    578
    Likes Received:
    60
    That's a really nice option to be able to select between equipped, non-equipped, or both, but is there a danger in just making a list of the equipped items? Or is this a warning that messing with them afterward can cause problems?

    That should be range(100,220). From what I've experimented with, which is 3 different chests, lareth's dresser, and something else I can't recall, they all filled up at 120 items when inserting from the trade during the game. I'm not sure yet if you can add more than that from code (still exploring), I'll post here if I find out more details.
     
  12. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,613
    Likes Received:
    537
    Looking at your list of examples, I can think of a couple of problem scenarios:
    1. Directly destroying equipped items - when used on an NPC - IIRC directly doing that has some side effects
    2. If someone has, say, 20 inventory items and 10 equipped, and you transfer them over all at once to another NPC, some of them can end up spilling over due to the 24 item limit.

    Otherwise it's a great addition, I should probably retrofit it into the Extraplanar Chest to eliminate that small stutter when summoning/banishing it.
     
  13. Shiningted

    Shiningted I want my goat back Administrator

    Joined:
    Oct 23, 2004
    Messages:
    12,654
    Likes Received:
    352
    Since we're discussing it...

    Can anyone think of a way to check for weight? I've tried the obvious ones and they don't work. I want to make a material component pouch a la the extraplanar chest but only for items of >=1lb.
     
  14. marc1967

    marc1967 Established Member

    Joined:
    Jan 19, 2014
    Messages:
    578
    Likes Received:
    60
    This should find the weight:

    weight = item.obj_get_int(obj_f_item_weight)
     
  15. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,613
    Likes Received:
    537
    I think you mean <=1lb

    Have you tried obj_get_int(obj_f_item_weight)? Works for me.
     
Our Host!