• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!

CG Inventory Stacking

Level 14
Joined
Nov 20, 2005
Messages
1,156
So, stacking inventory, pretty simple, huh? But what if you want it to stack properly even if you have a full inventory? I set out to solve that, and got a big improvement. Not quite totally fixed, due to an insane bit of stupidity on Blizzard's part (basically, if a hero tries to pass something to another hero that has a full inventory, then the system can't kick in as no order is given, but the hero tries to put the object at the other hero's feet; no way to solve that that I can see).

Without further ado, here's the system - it's a wee bit less efficient than it could be, but is pretty efficient. The instructions are in the code. Does NOT require vJASS.

EDIT: Updated. Improved a few things, changed it to use a stack for the indexes, and made the check implementation non-lame (now just cycles up; this is much safer, unless you do 2^31 orders, which'll never happen). Changed config a bit, made it so that the game cache would not conflict if you have another game cache (presuming it is inited on initialization).

JASS:
function TimerAttach takes timer t, real dur, real val, code func returns nothing
    call TimerStart(t, val, false, null)
    call PauseTimer(t)
    call TimerStart(t, dur, false, func)
endfunction

function H2I takes handle h returns integer
    return h
    return 0
endfunction

// In order to install this system, simply:
// - Copy and paste in the code below into a trigger called "CG Inv"
// - Include the H2I and TimerAttach functions above this code (if you already have them, no need to add them again)
// - Change the full inventory message in gameinterface (under advanced) to be " " (nothing won't work; use shift enter)
// - Change the warning sound for inventory full to something that has no sound (I used 'null' for testing; use shift enter)
// - Add in the data into the config funcs below
// - If you have NewGen, that globals block will do you; if not, you have to add them to the variable editor (without the udg_ prefix)
// - Recommended that the pick up distance variable is 60 more than the one in gameplay constants
// - The gamecache will be inited here ONLY if it is null to start with, so no worries if you already init it elsewhere.

// This system very well, and without extra work, at allowing heroes to pick up items and add them to stack, even if
// their inventory is full.

// HOWEVER, while it works fine if heroes give an item to them and they haven't got a full inventory, but if they have,
// then the item will be dropped on the floor, even if it could be stacked in.

// As soon as you try to pass an item to someone with a full inventory, WC3 tells them to put it on the floor.
// These orders, incidently, set off no events. Due to this silliness, this small 'bug' cannot be fixed.

// Picking up items is fine, but if you pick up and stack an item with full inventory, then queued orders will get messed up (it'll still stack the item fine).

// If you are giving items to stack to units at game init / before 0.01 seconds has elapsed, then this system won't have kicked in yet.
// If you know JASS, you know how to fix it, otherwise just use GUI and manually setup the gamecache (see example).

// CREDITS:
// - Vexorian, SimError, adapted for this to a specific ability and sound.

globals
    item array udg_CGINV_target
    unit array udg_CGINV_trigger
    real array udg_CGINV_x
    real array udg_CGINV_y
    item array udg_CGINV_slot
    integer array udg_CGINV_check
    
    integer array udg_CGINV_linkIndex
    integer udg_CGINV_nextIndex = 1
    
    trigger udg_CGINV_trigAquire = null
    trigger udg_CGINV_trigOrder = null
    trigger udg_CGINV_trigExecute = null
    real udg_CGINV_PICK_UP_DISTANCE = 210. // NB: This must be slightly (~50+ at least) more than that in gamplay constants
    // The actual different will be smaller, as I think that the game's own system uses the edge of the unit, this uses
    // the centre of the unit.
    
    sound udg_CGINV_inventoryFull = null
    
    gamecache udg_gc = null
endglobals

//this could be placed in the map script header and then called from elsewhere. Do this if you are going to use the GUI storage method, or access it from elsewhere.
function CGINV_SetItemTypeStack takes integer id, integer maxstack returns nothing
    call StoreInteger(udg_gc, "CGINVitem"+I2S(id), "maxstack", maxstack)
endfunction

// CONFIG FUNCS ==================================================================================

// Here you can add items to stack; note that the number of charges MUST BE MORE THAN 0 in the object editor
// Oh, and don't put waits in this function, it'll kill it.
function CGINV_SetupEx takes nothing returns nothing
    call CGINV_SetItemTypeStack('ckng', 5)
endfunction

// This function can be edited, eg: you might want all of the power up type items to stack up to 3 times, etc.
// Normally you'll just want to leave this alone unless you know what you are doing.
function CGINV_GetMaxStack takes item i returns integer
    return GetStoredInteger(udg_gc, "CGINVitem"+I2S(GetItemTypeId(i)), "maxstack")
endfunction

// END CONFIG FUNCS ==============================================================================

function CGINV_Setup takes nothing returns nothing
    if udg_gc == null then
        call FlushGameCache(InitGameCache( "MapName.w3v" ))
        set udg_gc = InitGameCache( "MapName.w3v" )
    endif
    call CGINV_SetupEx()
    call DestroyTimer(GetExpiredTimer())
endfunction

function CGINV_InventoryFull takes unit u returns nothing // Adapted from - SimError credits to Vexorian.
    if GetLocalPlayer() == GetOwningPlayer(u) then
        call StartSound(udg_CGINV_inventoryFull)
        call DisplayTimedTextToPlayer( GetLocalPlayer(), 0.52, -1.00, 2.00, "|cffffcc00Inventory is full.|r" )
    endif
endfunction

function CGINV_ItemToStack takes unit u, item i returns nothing
    local integer max = CGINV_GetMaxStack(i)
    local integer li = 0
    local integer id = GetItemTypeId(i)
    local integer size = UnitInventorySize(u)
    local item temp = null
    local integer charges = GetItemCharges(i)
    local integer tempCharges
    
    loop
        set temp = UnitItemInSlot(u, li)
        if temp != null then
            if GetItemTypeId(temp) == id then
                set tempCharges = GetItemCharges(temp)
                if tempCharges + charges <= max then
                    call SetItemCharges(temp, tempCharges + charges)
                    call RemoveItem(i)
                    set temp = null
                    return
                elseif tempCharges < max then
                    set charges = charges - max + tempCharges
                    call SetItemCharges(temp, max)
                endif
            endif
        endif
        set li = li + 1
        exitwhen li >= size
    endloop
    
    call SetItemCharges(i, charges)
    
    set temp = null
    
    call DisableTrigger(udg_CGINV_trigAquire)
    if not(UnitAddItem(u, i)) then
        call CGINV_InventoryFull(u)
    endif
    call EnableTrigger(udg_CGINV_trigAquire)
endfunction

function CGINV_Orders_Condition takes nothing returns boolean
    if not(UnitInventorySize(GetTriggerUnit()) == 0) then // This may need work to make sure it doesn't fail on stuff like beserk
        call FlushStoredInteger(udg_gc, "CGINV"+I2S(H2I(GetTriggerUnit())), "check")
    endif
    return false
endfunction

function CGINV_Aquired takes nothing returns nothing
    local item i = GetManipulatedItem()
    if CGINV_GetMaxStack(i) > 1 then
        call SetItemPosition(i, 0., 0.)
        call CGINV_ItemToStack(GetTriggerUnit(), i)
    endif
    set i = null
endfunction

function CGINV_CanStackItem takes unit u, item i, integer size returns boolean
    local integer id = GetItemTypeId(i)
    local integer li = 0
    local item temp = null
    local integer max = CGINV_GetMaxStack(i)
    if max <= 1 then
        return false
    endif
    loop
        set temp = UnitItemInSlot(u, li)
        if GetItemTypeId(temp) == id and GetItemCharges(temp) < max then
            set temp = null
            return true
        endif
        set li = li + 1
        exitwhen li >= size
    endloop
    set temp = null
    return false
endfunction

function CGINV_IncCheck takes unit u returns nothing
    local string s = "CGINV"+I2S(H2I(GetTriggerUnit()))
    local integer check = GetStoredInteger(udg_gc, s, "check")
    if check != 0 then // no need to increment it if it has never fired off the action
        call StoreInteger(udg_gc, "CGINV"+I2S(H2I(GetTriggerUnit())), "check", check+1)
    endif
endfunction

function CGINV_Direct_Condition takes nothing returns boolean
    local integer i = UnitInventorySize(GetTriggerUnit())
    local integer check
    local string s
    if i != 0 then
        if GetItemTypeId(GetOrderTargetItem()) != 0 and GetIssuedOrderId() == 851971 then // order smart
            if UnitInventoryCount(GetTriggerUnit()) < i then
                call CGINV_IncCheck(GetTriggerUnit())
                return false
            elseif CGINV_CanStackItem(GetTriggerUnit(), GetOrderTargetItem(), i) then
                return true
            endif
            // fake full inventory here, as you need to disable normal messages
            call CGINV_InventoryFull(GetTriggerUnit())
        endif
        call CGINV_IncCheck(GetTriggerUnit())
    endif
    return false
endfunction

function CGINV_Direct_Ex2 takes nothing returns nothing
    local integer ti = R2I(TimerGetRemaining(GetExpiredTimer())+0.9)
    local item i = udg_CGINV_target[ti]
    local unit u = udg_CGINV_trigger[ti]
    local real x = udg_CGINV_x[ti]
    local real y = udg_CGINV_y[ti]
    local string hs = "CGINV"+I2S(H2I(u))
    local integer check = udg_CGINV_check[ti]
    
    call DisableTrigger(udg_CGINV_trigAquire)
    call UnitAddItem(u, udg_CGINV_slot[ti])
    call EnableTrigger(udg_CGINV_trigAquire)
    
    call DestroyTimer(GetExpiredTimer())
    
    set udg_CGINV_slot[ti] = null
    set udg_CGINV_trigger[ti] = null
    set udg_CGINV_target[ti] = null
    
    set udg_CGINV_linkIndex[ti] = udg_CGINV_nextIndex
    set udg_CGINV_nextIndex = ti
    
    loop
        exitwhen (x-GetUnitX(u))*(x-GetUnitX(u)) + (y-GetUnitY(u))*(y-GetUnitY(u)) <= udg_CGINV_PICK_UP_DISTANCE*udg_CGINV_PICK_UP_DISTANCE
        call TriggerSleepAction(0.)
        if check != GetStoredInteger(udg_gc, hs, "check") or not(x==GetItemX(i)) then
            set u = null
            set i = null
            return
        endif
    endloop
    
    //call IssueImmediateOrder(u, "stop")
    call CGINV_ItemToStack(u, i)
    
    set u = null
    set i = null
endfunction

function CGINV_Direct_Ex takes nothing returns nothing
    call TriggerExecute(udg_CGINV_trigExecute)
endfunction

function CGINV_Direct_Action takes nothing returns nothing
    local item i = GetOrderTargetItem()
    local unit u = GetTriggerUnit()
    local real x = GetItemX(i)
    local real y = GetItemY(i)
    local item slot = UnitItemInSlot(u, 0)
    local string hs = "CGINV"+I2S(H2I(u))
    local integer check = GetStoredInteger(udg_gc, hs, "check") + 1 // increments the check; if this gets to 2^31, well, the world will end first.
    local integer index = udg_CGINV_nextIndex
    
    if (x-GetUnitX(u))*(x-GetUnitX(u)) + (y-GetUnitY(u))*(y-GetUnitY(u)) <= udg_CGINV_PICK_UP_DISTANCE*udg_CGINV_PICK_UP_DISTANCE then
        call CGINV_ItemToStack(u, i)
        set i = null
        set u = null
        set slot = null
        return
    endif
    
    set udg_CGINV_nextIndex = udg_CGINV_linkIndex[index]
    if udg_CGINV_nextIndex == 0 then
        set udg_CGINV_nextIndex = index+1
    endif
    
    set udg_CGINV_target[index] = i
    set udg_CGINV_trigger[index] = u
    set udg_CGINV_x[index] = x
    set udg_CGINV_y[index] = y
    set udg_CGINV_slot[index] = slot
    set udg_CGINV_check[index] = check
    
    //call DisableTrigger(udg_CGINV_trigOrder)
    //call IssuePointOrder(u, "move", x, y)
    //call EnableTrigger(udg_CGINV_trigOrder)
    
    call StoreInteger(udg_gc, hs, "check", check)
    call SetItemPosition(slot, 0., 0.)
    call TimerAttach(CreateTimer(), 0., index, function CGINV_Direct_Ex)
    
    set u = null
    set i = null
    set slot = null
endfunction

//===========================================================================
function InitTrig_CG_Inv takes nothing returns nothing
    local trigger t = CreateTrigger(  )
    call TriggerRegisterAnyUnitEventBJ( t, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER )
    call TriggerAddCondition( t, Condition( function CGINV_Direct_Condition ) )
    call TriggerAddAction( t, function CGINV_Direct_Action )
    
    set udg_CGINV_trigAquire = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ( udg_CGINV_trigAquire, EVENT_PLAYER_UNIT_PICKUP_ITEM )
    call TriggerAddAction(udg_CGINV_trigAquire, function CGINV_Aquired )
    
    set udg_CGINV_trigOrder = CreateTrigger(  )
    call TriggerRegisterAnyUnitEventBJ( udg_CGINV_trigOrder, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER )
    call TriggerRegisterAnyUnitEventBJ( udg_CGINV_trigOrder, EVENT_PLAYER_UNIT_ISSUED_ORDER )
    call TriggerAddCondition( udg_CGINV_trigOrder, Condition( function CGINV_Orders_Condition ) )
    
    set udg_CGINV_trigExecute = CreateTrigger()
    call TriggerAddAction(udg_CGINV_trigExecute, function CGINV_Direct_Ex2)
    
    set udg_CGINV_inventoryFull = CreateSound("Sound\\Interface\\Warning\\Human\\KnightInventoryFull1.wav", false, false, false, 10, 10, "")
    
    call TimerStart(CreateTimer(), 0.01, false, function CGINV_Setup)
endfunction

  • Example
    • Events
      • Map initialization
    • Conditions
    • Actions
      • Game Cache - Create a game cache from MapName.w3v
      • Set gc = (Last created game cache)
      • -------- Obviously you need these variables. --------
      • Set CGINV_itemType = Health Stone
      • Set CGINV_maxStack = 3
      • -------- Call this script each time after setting the variables; it'll handle all the work for you. NEEDS THE FUNCTION IN THE SCRIPT AREA, NOT A TRIGGER --------
      • Custom script: call CGINV_SetItemTypeStack(udg_CGINV_itemType, udg_CGINV_maxStack)
 
Last edited:
Level 9
Joined
Mar 25, 2005
Messages
252
Acquired has a "c" in it.

[jass=quoted]// - Change the warning sound for inventory full to something that has no sound (I used 'null' for testing)[/code]I could not find 'null'. I tested with SubGroupSelectionChange instead and couldn't hear a thing (though I got some music playing on the background).

The order id of "smart" is 851971. Credits to the ConvOrder tool by Blade.dk @ wc3c.

Imo in CGINV_Direct_Condition you should first check GetItemTypeId(GetOrderTargetItem()) != 0 because that rules out all orders that don't target items, which is the majority of them (bigger than the amount of non-smart orders and orders given to units without inventories, which you check first).

This sys is otherwise nice but it screws up all shift-orders given to a unit that is ordered to pick up an item that can be stacked with something in his inventory when it is full. You give the unit a move order that is the cause of this. The unit will still move to the item even if you don't give that order. The unit wont come in range of 150, but atleast 300, so by increasing that range constant this should work without the move order (I tested this and it worked). There is also some stop order in there that imo you could just get rid of...
 
Level 14
Joined
Nov 20, 2005
Messages
1,156
Acquired has a "c" in it.

Blargh.

[jass=quoted]// - Change the warning sound for inventory full to something that has no sound (I used 'null' for testing)[/code]I could not find 'null'. I tested with SubGroupSelectionChange instead and couldn't hear a thing (though I got some music playing on the background).

Shift-enter. null isn't there, hence why I used it, as it has no sound.

The order id of "smart" is 851971. Credits to the ConvOrder tool by Blade.dk @ wc3c.

Meh, a tiny optimisation given the rest of it.

Imo in CGINV_Direct_Condition you should first check GetItemTypeId(GetOrderTargetItem()) != 0 because that rules out all orders that don't target items, which is the majority of them (bigger than the amount of non-smart orders and orders given to units without inventories, which you check first).

Hmm...probably a good idea.

This sys is otherwise nice but it screws up all shift-orders given to a unit that is ordered to pick up an item that can be stacked with something in his inventory when it is full. You give the unit a move order that is the cause of this. The unit will still move to the item even if you don't give that order. The unit wont come in range of 150, but atleast 300, so by increasing that range constant this should work without the move order (I tested this and it worked). There is also some stop order in there that imo you could just get rid of...

Oooh, I didn't think of queued orders. Yea, if you set the range a bit higher, then both the move order and the stop order become unnecessary.
 
Level 14
Joined
Nov 20, 2005
Messages
1,156
Changes made, except for:

Imo in CGINV_Direct_Condition you should first check GetItemTypeId(GetOrderTargetItem()) != 0 because that rules out all orders that don't target items, which is the majority of them (bigger than the amount of non-smart orders and orders given to units without inventories, which you check first).

That has to be checked against all to know if I should set its check to zero (show it has been given another order).

Behaviour's a wee bit different with queued orders to what you'd see with the blizzard version (which runs except when it wouldn't work), but nothing you'd really notice or would have an effect on gameplay - it no longer interrupts them too.

And I might an optimisation short cut for where the item was within range already.
 
Level 40
Joined
Dec 14, 2005
Messages
10,532
CGINV_Direct_Condition said:
call StoreInteger(udg_gc, "CGINV"+I2S(H2I(GetTriggerUnit())), "check", 0)
It would make more sense to just flush the integer; also, I'm not sure that StoreInteger with a null value actually clears the memory (though it may very well).



JASS:
// This function can be edited, eg: you might want all of the power up type items to stack up to 3 times, etc.
function CGINV_GetMaxStack takes item i returns integer
return GetStoredInteger(udg_gc, "CGINVitem"+I2S(GetItemTypeId(i)), "maxstack")
endfunction
It would make more sense to advise users not to edit it: maxstack will already store the info they need, and leaving this single-lined allows it to be inlined by Vex's Optimizer.


You could always make udg_CGInv_Loop behave like a stack, to waste needless allocation of arrays.


JASS:
function CGINV_CanStackItem takes unit u, item i, integer size returns boolean
    local integer id = GetItemTypeId(i)
    local integer li = 0
    local item temp = null
    local integer max = CGINV_GetMaxStack(i)
    if max <= 1 then
        return false
    endif
    loop
        set temp = UnitItemInSlot(u, li)
        if GetItemTypeId(temp) == id and GetItemCharges(temp) < max then
            set temp = null
            return true
        endif
        set li = li + 1
        exitwhen li >= size
    endloop
    set temp = null
    return false
endfunction
All the mcuking about with temp leaves me inclined to believe it would be better to just call UnitItemInSlot twice. Even if you don't want to, though, I would assuming a bit of manipulation would allow you to remove the last set temp = null, unless WarCraft crashes for invalid slots, which I highly doubt (Though you never know). The loop could be similarly edited in CGINV_ItemToStack.

I can't find too much to be picky about, though I haven't pounded it though testing yet.
 
Level 14
Joined
Nov 20, 2005
Messages
1,156
Updated. Uses a stack, made the check method non-sucky and non-buggy, and also improved a few things (mainly non-collision with existing gamecaches of the same name). Now has examples on how to use in GUI, with just a CnP import and moving a function to the map script section, and out of the trigger.
 
Level 19
Joined
Aug 24, 2007
Messages
2,888
Can you include a vjass version pls ?
and does this display Inventory is full and make a sound if inventory is full and item you trying to pickup doesnt stack with anything ?
if not make that depending on a boolean variable which I can set true/false in config
 
Level 14
Joined
Nov 20, 2005
Messages
1,156
Can you include a vjass version pls ?
and does this display Inventory is full and make a sound if inventory is full and item you trying to pickup doesnt stack with anything ?
if not make that depending on a boolean variable which I can set true/false in config

I could make a vJASS version, but meh, it's easier to support just one version. Just put it in a script and forget about it is the idea, really.

And yes, it has a modified SimError.
 
Level 19
Joined
Aug 24, 2007
Messages
2,888
Bah I can make it vjass when Im gonna use anyway :/
vJass is much better coz u can put globals in trigger and dont spam the X etc etc
 
Top