• 🏆 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!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Baradé's Item Unstack System 1.9

This system adds a missing item unstack feature to Warcraft III's native item stack system.
Items can be unstacked by right double clicking an item in the inventory with the help of this system.
The system was inspired by the Easy ItemStack 'n Split system.

Requirements:
  • Warcraft III: Reforged supporting the native item stacking.
  • Item types with maximum stacks specified.
  • vJass/JassHelper.

Advantages:
  • Uses the max stacks values from the object editor instead of the item level like Easy ItemStack 'n Split system.
  • Keeps item properties such as invulnerable, pawnable, player etc. when the item is unstacked which is necessary when the stacked item has been added and modified with a trigger. It is only useful if you place a stacked unique item somewhere with these exact properties since Warcraft's native stacking system will always use the properties of the target item which is stacked and hence different properties can get lost on item stacking.
The code contains a comment usage instructions.
Copy the code of the following two libraries into your map script or a custom trigger converted into code:
JASS:
// Define this function, to return custom values. It is only used if CHECK_MAX_STACKS is false.
constant function ItemUnstackItemGetMaxStacks takes integer itemTypeId returns integer
    return 1000
endfunction

library ItemUnstackSystem initializer Init requires optional MaxItemStacks
// Baradé's Item Unstack System 1.9
//
// Supports the missing Warcraft III feature of unstacking stacked items in your inventory.
//
// Usage:
// - Enable Warcraft's native stack system in the "Advanced - Gameplay Constants - Inventory - Enable Item Stacking".
// - Give certain item types a maximum stack value: "Stats - Max Stacks". Note that some of them like Wards do already have specified some values greater than 0 here.
// - Copy this code into your map script or a trigger converted into code.
// - Optional: Adapt the values of the constants for your map.
// - Optional: Redefine ItemUnstackItemGetMaxStacks if necessary.
// - Optional (recommended): Change the "Advanced - Game Interface - Text - General" from "|cff808080Drop item on shop to sell|R" into "|cff808080Drop item on shop to sell|NDouble right click item to unstack|R" to guide the player.
//
// Download:
// https://www.hiveworkshop.com/threads/barad%C3%A9s-item-unstack-system-1-1.339109/
//
// API:
//
// function SetItemUnstackEnabled takes unit whichUnit, boolean enabled returns nothing
//
// Enables item unstack for the given unit or disables it. It is enabled by default.
//
// function IsItemUnstackEnabled takes unit whichUnit returns boolean
//
// Returns if item unstack is enabled for the given unit which should be true by default.

globals
    // The number of charges which are unstacked at maximum if available.
    private constant integer MAX_UNSTACKED_CHARGES = 10
    // Overwrites the previous value if set to true. It always unstacks the half of charges. For uneven numbers it will unstack the lower value. For example, for 3 it will unstack 1 charge.
    private constant boolean UNSTACK_HALF_CHARGES = false
    // Unstacking an item can be moved to the next slot if it has the same item type and stack with it.
    private constant boolean ALLOW_STACKING_NEXT_ITEM = true
    // This option only has an effect is ALLOW_STACKING_NEXT_ITEM is true. The unstacked item might be stacked to an item in a previous slot in the inventory if this option is true.
    private constant boolean STACKING_NEXT_ITEM_FROM_START = true
    // Checks for the maximum possible stacks for every item type. Otherwise, ItemUnstackItemGetMaxStacks is used.
    private constant boolean CHECK_MAX_STACKS = true

    private trigger orderTrigger = CreateTrigger()
    private group disabledUnits = CreateGroup()
endglobals

function SetItemUnstackEnabled takes unit whichUnit, boolean enabled returns nothing
    if (enabled) then
        if (IsUnitInGroup(whichUnit, disabledUnits)) then
            call GroupRemoveUnit(disabledUnits, whichUnit)
        endif
    else
        if (not IsUnitInGroup(whichUnit, disabledUnits)) then
            call GroupAddUnit(disabledUnits, whichUnit)
        endif
    endif
endfunction

function IsItemUnstackEnabled takes unit whichUnit returns boolean
    return not IsUnitInGroup(whichUnit, disabledUnits)
endfunction

private function GetMaxStacksByItemTypeIdIntern takes integer itemTypeId returns integer
static if (CHECK_MAX_STACKS) then
    return GetMaxStacksByItemTypeId(itemTypeId)
else
    return ItemUnstackItemGetMaxStacks(itemTypeId)
endif
endfunction

private function CopyItemProps takes item sourceItem, item targetItem returns nothing
    // some seem broken
    //call BlzSetItemName(targetItem, GetItemName(sourceItem))
    call BlzSetItemDescription(targetItem, BlzGetItemDescription(sourceItem))
    //call BlzSetItemTooltip(targetItem, BlzGetItemTooltip(sourceItem))
    call BlzSetItemExtendedTooltip(targetItem, BlzGetItemExtendedTooltip(sourceItem))
    //call BlzSetItemIconPath(targetItem, BlzGetItemIconPath(sourceItem))
    call SetItemPawnable(targetItem, IsItemPawnable(sourceItem))
    call SetItemInvulnerable(targetItem, IsItemInvulnerable(sourceItem))
    if (GetItemPlayer(sourceItem) != null) then
        call SetItemPlayer(targetItem, GetItemPlayer(sourceItem), false)
    endif
endfunction

private function GetItemSlot takes unit hero, item whichItem returns integer
    local integer sourceSlot = -1
    local integer i = 0
    loop
        if (UnitItemInSlot(hero, i) == whichItem) then
            set sourceSlot = i
        endif
        set i = i + 1
        exitwhen (sourceSlot != -1 or i >= UnitInventorySize(hero))
    endloop
    return sourceSlot
endfunction

private function AddUnstackedItem takes unit hero, integer itemTypeId, integer charges, integer sourceSlot, item sourceItem returns nothing
    local item itemInNextSlot = null
    local item itemInPreviousSlot = null
    local integer inventorySize = UnitInventorySize(hero)
    local integer slotItemCharges = 0
    local boolean addedToFreeSlot = false
    // we need to specify the target slot explicitly to prevent stacking the items again
    // we prefer empty slots next to the unstacked item
    local item unstackedItem = null
    local integer remainingCharges = charges
    local integer unstackedCharges = 0
    local integer maxCharges = 0
    local integer i = sourceSlot + 1
    local integer j = sourceSlot - 1
    // check for a slot with an item with the same type and free stacks
    if (ALLOW_STACKING_NEXT_ITEM) then
        set maxCharges = GetMaxStacksByItemTypeIdIntern(itemTypeId)
        loop
            set itemInNextSlot = UnitItemInSlot(hero, i)
            if (i < inventorySize) then
                if (itemInNextSlot == null) then
                    set addedToFreeSlot = true
                    call UnitAddItemToSlotById(hero, itemTypeId, i)
                    set unstackedItem = UnitItemInSlot(hero, i)
                    set unstackedCharges = IMinBJ(maxCharges, remainingCharges)
                    call SetItemCharges(unstackedItem, unstackedCharges)
                    set remainingCharges = remainingCharges - unstackedCharges
                elseif (GetItemTypeId(itemInNextSlot) == itemTypeId) then
                    set slotItemCharges = GetItemCharges(itemInNextSlot)
                    if (slotItemCharges < maxCharges) then
                        set unstackedItem = itemInNextSlot
                        set unstackedCharges = IMinBJ(maxCharges - GetItemCharges(itemInNextSlot), remainingCharges)
                        call SetItemCharges(unstackedItem, slotItemCharges + unstackedCharges)
                        set remainingCharges = remainingCharges - unstackedCharges
                    endif
                endif
            endif
            set i = i + 1
            exitwhen (remainingCharges == 0 or i >= inventorySize)
        endloop
        if (STACKING_NEXT_ITEM_FROM_START and remainingCharges > 0 and sourceSlot > 0) then
            set i = 0
            loop
                set itemInNextSlot = UnitItemInSlot(hero, i)
                if (i < sourceSlot) then
                    if (itemInNextSlot == null) then
                        set addedToFreeSlot = true
                        call UnitAddItemToSlotById(hero, itemTypeId, i)
                        set unstackedItem = UnitItemInSlot(hero, i)
                        set unstackedCharges = IMinBJ(maxCharges, remainingCharges)
                        call SetItemCharges(unstackedItem, unstackedCharges)
                        set remainingCharges = remainingCharges - unstackedCharges
                    elseif (GetItemTypeId(itemInNextSlot) == itemTypeId) then
                        set slotItemCharges = GetItemCharges(itemInNextSlot)
                        if (slotItemCharges < maxCharges) then
                            set unstackedItem = itemInNextSlot
                            set unstackedCharges = IMinBJ(maxCharges - GetItemCharges(itemInNextSlot), remainingCharges)
                            call SetItemCharges(unstackedItem, slotItemCharges + unstackedCharges)
                            set remainingCharges = remainingCharges - unstackedCharges
                        endif
                    endif
                endif
                set i = i + 1
                exitwhen (remainingCharges == 0 or i >= sourceSlot)
            endloop
        endif
    endif

    // check for a free slot
    if (remainingCharges > 0) then
        set i = sourceSlot + 1
        set j = sourceSlot - 1
        loop
            set itemInNextSlot = UnitItemInSlot(hero, i)
            set itemInPreviousSlot = UnitItemInSlot(hero, j)
            if (i < inventorySize and itemInNextSlot == null) then
                set addedToFreeSlot = true
                call UnitAddItemToSlotById(hero, itemTypeId, i)
                set unstackedItem = UnitItemInSlot(hero, i)
                call CopyItemProps(sourceItem, unstackedItem)
                set unstackedCharges = IMinBJ(maxCharges, remainingCharges)
                call SetItemCharges(unstackedItem, unstackedCharges)
                set remainingCharges = remainingCharges - unstackedCharges
            elseif (j >= 0 and itemInPreviousSlot == null) then
                set addedToFreeSlot = true
                call UnitAddItemToSlotById(hero, itemTypeId, j)
                set unstackedItem = UnitItemInSlot(hero, j)
                call CopyItemProps(sourceItem, unstackedItem)
                set unstackedCharges = IMinBJ(maxCharges, remainingCharges)
                call SetItemCharges(unstackedItem, unstackedCharges)
                set remainingCharges = remainingCharges - unstackedCharges
            endif
            set i = i + 1
            set j = j - 1
            exitwhen (remainingCharges == 0 or (i >= inventorySize and j < 0))
        endloop
    endif

    // create the item for the hero with one slot if all slots are used
    loop
        exitwhen (remainingCharges == 0)
        set unstackedItem = CreateItem(itemTypeId, GetUnitX(hero), GetUnitY(hero))
        set unstackedCharges = IMinBJ(maxCharges, remainingCharges)
        call SetItemCharges(unstackedItem, unstackedCharges)
        call CopyItemProps(sourceItem, unstackedItem)
        set remainingCharges = remainingCharges - unstackedCharges
    endloop

    set unstackedItem = null
endfunction

private function TriggerConditionOrderUnstack takes nothing returns boolean
    local boolean isEnabled = IsItemUnstackEnabled(GetTriggerUnit())
    local integer orderId = GetIssuedOrderId()
    local item targetItem = GetOrderTargetItem()
    local integer maxStacks = GetMaxStacksByItemTypeIdIntern(GetItemTypeId(targetItem))
    local integer charges = GetItemCharges(targetItem)
    local boolean result = isEnabled and orderId >= 852002 and orderId <= 852007 and targetItem != null and maxStacks > 0 and charges > 1 and GetItemSlot(GetTriggerUnit(), targetItem) == orderId - 852002
    set targetItem = null
    return result
endfunction

private function TriggerActionOrderUnstack takes nothing returns nothing
    local unit hero = GetTriggerUnit()
    local item sourceItem = GetOrderTargetItem()
    local integer sourceItemTypeId = GetItemTypeId(sourceItem)
    local integer sourceSlot = GetItemSlot(hero, sourceItem)
    local integer charges = 1
    // wait for completing the order or the item is not at the target slot
    call TriggerSleepAction(0.0)
    // item does still exist and was dropped on its previous slot
    // we are not sure if this works when the item is removed via triggers since the value of the variable becomes an invalid reference
    if (sourceItem != null and GetWidgetLife(sourceItem) > 0.0 and GetItemCharges(sourceItem) > 0 and UnitItemInSlot(hero, sourceSlot) == sourceItem) then
        if (UNSTACK_HALF_CHARGES) then
            set charges = IMaxBJ(GetItemCharges(sourceItem) / 2, 1)
        else
            set charges = IMinBJ(GetItemCharges(sourceItem) - 1, MAX_UNSTACKED_CHARGES)
        endif
        if (charges > 0) then
            call SetItemCharges(sourceItem, GetItemCharges(sourceItem) - charges)
            call AddUnstackedItem(hero, sourceItemTypeId, charges, sourceSlot, sourceItem)
        endif
    endif

    set hero = null
    set sourceItem = null
endfunction

private function Init takes nothing returns nothing
    call TriggerRegisterAnyUnitEventBJ(orderTrigger, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER)
    call TriggerAddCondition(orderTrigger, Condition(function TriggerConditionOrderUnstack))
    call TriggerAddAction(orderTrigger, function TriggerActionOrderUnstack)
    
    call SetItemUnstackEnabled(MaxItemStacks_GetStackItemDummy(), false)
endfunction

// Change Log:
//
// 1.9 2023-01-10:
// - Fix checking for the charges maximums of the corresponding item types when unstacking items.
// - Store more information in local variables to improve the performance.
// - Add function SetItemUnstackEnabled.
// - Add function IsItemUnstackEnabled.
// - Revise tooltips, stocks and number for items in example map.
// - Give players gold and lumber in example map.
//
// 1.8 2022-12-18:
// - Unstack item charges -1 at maximum instead of all item charges which would be like moving the item.
// - Do never unstack charges equal to or less than 0 to prevent bugs.
//
// 1.7 2022-10-29:
// - Simplify TriggerConditionOrderUnstack by using more local variables.
// - Add option STACKING_NEXT_ITEM_FROM_START.
//
// 1.6 2022-07-15:
// - Add separate library MaxItemStacks.
//
// 1.5 2022-07-10:
// - Use vJass since we have two triggers now, can use the initializer and use static ifs.
// - Use constants instead of constant functions for the options of the system.
// - Calculate the max stacks per item type with the help of a dummy if enabled.
// - Increase the max stacks possible to 1000 since this is the maximum possible value in the object editor.
// - Add option UNSTACK_HALF_CHARGES.
//
// 1.4 2022-07-09:
// - ItemUnstackMaximumCharges allows changing the number of unstacked charges.
// - ItemUnstackAllowStackingNextItem allows stacking the unstacked item to the next item with the same type instead of only using a free slot.
// - ItemUnstackItemGetMaxStacks to check if the item is even stackable and to make sure item charges will not be over the maximum.
//
// 1.3 2022-04-16:
// - Refactor function names.
//
// 1.2 2022-04-13:
// - Use UnitInventorySize instead of bj_MAX_INVENTORY to support different inventory sizes.
// - Place Footmen with unit inventories to check different inventory sizes.
//
// 1.1 2022-04-11:
// - Split into multiple functions.
// - Do not apply item name, tooltip and icon path on unstacking since it is broken.
// - Prefer the nearest empty slot on unstacking.
// - Add a line break to the unstacking hint.
// - Add some items with custom names and descriptions to test stacking/unstacking them.
// - Move system code into converted trigger.
// - Update preview image.
endlibrary

JASS:
library MaxItemStacks initializer Init
// Baradé's Max Item Stacks System 1.1
//
// Supports the missing Warcraft III feature of calculating the value of 'ista' per item type ID.
//
// Usage:
// - Copy this code into your map script or a trigger converted into code.
// - Use the function GetMaxStacksByItemTypeId to get the value of 'ista' per item type ID.
//
// Download:
// https://www.hiveworkshop.com/threads/barad%C3%A9s-item-unstack-system-1-1.339109/

globals
    // This dummy is created and hidden once only if CHECK_MAX_STACKS is set to true. It requires an inventory with at least 2 slots.
    private constant integer DUMMY_UNIT_TYPE_MAX_CHECKS = 'Hpal'
    // Warcraft III has a limit of number of stacks for the field "Stats - Max Stacks" ('ista').
    private constant integer MAX_STACKS_ALLOWED = 1000
    private constant real DUMMY_X = 0.0
    private constant real DUMMY_Y = 0.0

    private integer stackCounter = 0
    private hashtable stackHashTable = InitHashtable()
    private unit stackItemDummy = null
    private trigger stackItemTrigger = CreateTrigger()
endglobals

public function GetStackItemDummy takes nothing returns unit
    return stackItemDummy
endfunction

function GetMaxStacksByItemTypeIdFresh takes integer itemTypeId returns integer
    local integer i = 0
    local item tmpItem = CreateItem(itemTypeId, 0.0, 0.0)
    set stackCounter = 1
    call SetItemCharges(tmpItem, 1)
    call UnitAddItem(stackItemDummy, tmpItem)
    set i = 1
    loop
        set tmpItem = CreateItem(itemTypeId, 0.0, 0.0)
        call SetItemCharges(tmpItem, 1)
        call UnitAddItem(stackItemDummy, tmpItem)
        exitwhen (stackCounter <= i)
        set i = i + 1
        exitwhen (i >= MAX_STACKS_ALLOWED)
    endloop
    if (UnitItemInSlot(stackItemDummy, 0) != null) then
        call RemoveItem(UnitItemInSlot(stackItemDummy, 0))
    endif
    if (UnitItemInSlot(stackItemDummy, 1) != null) then
        call RemoveItem(UnitItemInSlot(stackItemDummy, 1))
    endif
    call SaveInteger(stackHashTable, itemTypeId, 0, stackCounter)
    return stackCounter
endfunction

function GetMaxStacksByItemTypeId takes integer itemTypeId returns integer
    if (HaveSavedInteger(stackHashTable, itemTypeId, 0)) then
        return LoadInteger(stackHashTable, itemTypeId, 0)
    endif
   
    return GetMaxStacksByItemTypeIdFresh(itemTypeId)
endfunction

private function TriggerConditionStack takes nothing returns boolean
    set stackCounter = stackCounter + 1
    return false
endfunction

private function Init takes nothing returns nothing
    set stackItemDummy = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMY_UNIT_TYPE_MAX_CHECKS, DUMMY_X, DUMMY_Y, 0.0)
    call SetUnitInvulnerable(stackItemDummy, true)
    if (IsUnitType(stackItemDummy, UNIT_TYPE_HERO)) then
        call SuspendHeroXP(stackItemDummy, true)
    endif
    call SetUnitUseFood(stackItemDummy, false)
    call ShowUnit(stackItemDummy, false)
    call BlzSetUnitWeaponBooleanField(stackItemDummy, UNIT_WEAPON_BF_ATTACKS_ENABLED, 0, false)
    call BlzSetUnitWeaponBooleanField(stackItemDummy, UNIT_WEAPON_BF_ATTACKS_ENABLED, 1, false)
    call SetUnitPathing(stackItemDummy, false)
    call SetUnitMoveSpeed(stackItemDummy, 0.0)
    call TriggerRegisterUnitEvent(stackItemTrigger, stackItemDummy, EVENT_UNIT_STACK_ITEM)
    call TriggerAddCondition(stackItemTrigger, Condition(function TriggerConditionStack))
endfunction

// Change Log:
//
// 1.1 2022-12-18:
// - Add function GetMaxStacksByItemTypeIdFresh to move locals and allow avoiding caching.
endlibrary
Contents

Baradé's Item Unstack System 1.9 (Map)

Reviews
Wrda
A good system that completes the unstacking mechanic that blizzard's system lacks. Approved.
Level 18
Joined
Oct 17, 2012
Messages
818
Since this system is for the latest patch, you could use an library initializer to eliminate the need to initialize the system on the user end. The system will then be automatic without the user ever touching the code.

AFAIK, setting name and tooltip does not work on the latest patch. Extended tooltip and description are changeable in game, however. Though, if you were to use the new skin system, you could change all item properties during runtime. However, it is not dynamic as in you would need to create new items in the object editor and changing one property on demand is next to impossible, or at the very least, not feasible with the skin system.

I would not recommend placing code in the map header. There is this strange bug, where the code in the map header is replaced with code from another trigger.
 
Last edited:
Level 25
Joined
Feb 2, 2006
Messages
1,667
I wanted to avoid using vJass and keep it minimalistic, so you can save without JassHelper enabled which is a bit faster.

Hmm I never had that bug. I have always used the map script like in the classic Blizzard maps but I can move it into a trigger.

I can disable some of the set function calls like tooltip and description. I don't know how the skin system works?
I can add an item with dynmically changed properties which stacks as proof of concept.

The only concern I had was the TriggerSleepAction. The item could theoretically have been sold/removed during this sleep. I hope the widget life check is sufficient but I might have to detect it in a different way.
I could also start a timer with a timeout of 0 seconds and attach the necessary data using a hashtable but that would increase the code.

edit:
I can create a vJass and GUI version like for the refered Easy Item Stack 'n Split system.
 
Last edited:
Level 18
Joined
Oct 17, 2012
Messages
818
If that is what you are going for, you could use the InitTrig function of an converted GUI trigger. These are ran automatically.

You don't need that second loop to search for a free item slot. Just add the item to the unit via UnitAddItemById. The item added will automatically drop to the ground if the unit inventory is full.

The TriggerSleepAction is unnecessary. The system functions well without it.
 
Level 25
Joined
Feb 2, 2006
Messages
1,667
Thx for the hints. I will adapt everything later.

The sleep is there to wait until the order has been completed. I had some bad experience when the item wasn't moved to the slot but I can try it differently.

edit:
Yes it only works with a sleep. It seems that the native stacking does take the custom descriptions and settings of the item already in the inventory so applying the item properties on unstacking is kind of useless except for stacked items which are given to heroes with triggers and set to invulnerable etc.

edit2:
The second loop is necessary to prevent Warcraft's native stacking. If I just add the item it will stack again!

edit3:
Release 1.1:
  • Split into multiple functions.
  • Do not apply item name, tooltip and icon path on unstacking since it is broken.
  • Prefer the nearest empty slot on unstacking.
  • Add a line break to the unstacking hint.
  • Add some items with custom names and descriptions to test stacking/unstacking them.
Now the unstacking does not use the first free slots counting from 1 to 6 but the first free one next to the current slot preferring the slots after the current one.
The only concern I have with this system is that the item could be removed via triggers during the sleep and then the variable will have some invalid reference which theoretically could be used by another item in the same slot which would lead to some weird unstacking of another replaced item. The chances of this happening would be almost 0.

Since Warcraft's native stacking does always keep the properties of the item which receives additional stacks, keeping the properties on unstacking is only helpful if the stacked item has originally been created with some triggers and is unique in the whole map with these properties.

edit4:

Release 1.2 2022-04-13:

  • Use UnitInventorySize instead of bj_MAX_INVENTORY to support different inventory sizes.
  • Place Footmen with unit inventories to check different inventory sizes.
 
Last edited:

Wrda

Spell Reviewer
Level 25
Joined
Nov 18, 2012
Messages
1,864
I wanted to avoid using vJass and keep it minimalistic, so you can save without JassHelper enabled which is a bit faster.
This doesn't really make much sense, because since this is made for the latest patches, you just need one VJASS code or system or whatever for it to be required to run JassHelper. It doesn't really bother anyone for +3 seconds of saving, it's not like you're constantly trying to save the map every minute. You shouldn't think this in a minimalistic way, but rather a pratical way, with the things that are required.

JASS:
function ItemUnstackApplyItemProperties takes item sourceItem, item targetItem returns nothing
This looks either too long or borderline acceptable length of a function, a better length would be
"ItemUnstackApplyProps".

You could shorten this
JASS:
function ItemUnstackGetItemSlot takes unit hero, item whichItem returns integer
    local integer sourceSlot = -1
    local integer i = 0
    loop
        if (UnitItemInSlot(hero, i) == whichItem) then
            set sourceSlot = i
        endif
        set i = i + 1
        exitwhen (sourceSlot != -1 or i >= UnitInventorySize(hero))
    endloop
    return sourceSlot
endfunction
To this:
JASS:
function ItemUnstackGetItemSlot takes unit hero, item whichItem returns integer
    local integer sourceSlot = -1
    local integer i = 0
    loop
        if (UnitItemInSlot(hero, i) == whichItem) then
            set sourceSlot = i
            exitwhen true
        endif
        set i = i + 1
        exitwhen i >= UnitInventorySize(hero))
    endloop
    return sourceSlot
endfunction
ItemUnstackAddItemToNearestFreeSlot function should also follow this logic


JASS:
function ItemUnstackAddItemToNearestFreeSlot takes unit hero, integer itemTypeId, integer sourceSlot returns item
function ItemUnstackAddItemToNearestFreeSlotOrGround takes unit hero, integer itemTypeId, integer sourceSlot returns item
function ItemUnstackAddItemToNearestFreeSlotOrGroundUnstack takes unit hero, integer itemTypeId, integer sourceSlot, item sourceItem returns nothing
DUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUDE, are we playing who reads the whold functions fast? This is too long. I'm sorry but "wtf is this" was my first reaction, lol.

You could merge ItemUnstackAddItemToNearestFreeSlotOrGround and ItemUnstackAddItemToNearestFreeSlotOrGroundUnstack.
JASS:
function ItemUnstackAddItemToNearestFreeSlotOrGroundUnstack takes unit hero, integer itemTypeId, integer sourceSlot, item sourceItem returns nothing //change function name for god's sake
    // search for a free slot to unstack the item
    local item unstackedItem = ItemUnstackAddItemToNearestFreeSlot(hero, itemTypeId, sourceSlot) //change this too please
    // create the item for the hero with one slot if all slots are used
    if (unstackedItem == null) then
        set unstackedItem = CreateItem(itemTypeId, GetUnitX(hero), GetUnitY(hero))
    endif
    call SetItemCharges(unstackedItem, 1)
    call ItemUnstackApplyItemProperties(sourceItem, unstackedItem)
    set unstackedItem = null
endfunction

Why name these functions TriggerConditionItemUnstack and TriggerActionsItemUnstack, when all the other functions start with "ItemUnstack"? InitItemStackSystem follows the convention of an init function, so it is an exception. This isn't a big deal, but it shows the inconsistencies. ItemUnstackCondition(s), ItemUnstack_Condition(s), Trig_ItemUnstackCondition(s), TrigItemUnstackCondition(s), Trig_ItemUnstack_Condition(s) are more suitable names.

The only concern I had was the TriggerSleepAction. The item could theoretically have been sold/removed during this sleep. I hope the widget life check is sufficient but I might have to detect it in a different way.
But isn't the item null if it has already been sold/removed?
I can create a vJass and GUI version like for the refered Easy Item Stack 'n Split system.
GUI version makes sense, vJass not, since you should integrate it here already to make things simpler, ahem,
keep it minimalistic
yes, making function names minimalistic ;)

Aside from all the ranting, this resource proves to be certainly useful with no doubt.
 
Level 25
Joined
Feb 2, 2006
Messages
1,667
Wow this is quite a rant about minor things like function names of functions you do not even call since it is no API.

The local item variable does not become null automatically I think. It might have a reference to a removed item. I would have to hook the RemoveItem call I think and the null my variable. If the reference is not null but not the one from any item in the inventory it is no issue.

I can adapt all the function names if you have an issue with them.
You do not even call them from anywhere. I can start them all with ItemUnstack.

I learned to define all conditions inside one exitwhen instead of use multiple ones (it's like multiple breaks instead of defining one single loop condition).
Hence, changing ItemUnstackGetItemSlot would save one single operation but lead to one more line of code and two exitwhens.

I think these are really minor things.

About vJass: Not all maps use vJass code and JassHelper can be quite annoying. Generating a map script with it takes some time.
If your map does not use vJass why should you use vJass for this one simple system? It doesn't even have global variables.
I myself have one big project which uses plenty of vJass but other maps which only use GUI triggers with some JASS functions.
I would totally disable the JassHelper/vJass if I did not define some free global variables.

edit:
I have refactored the code a bit with all functions starting with ItemUnstack and two functions became one now.
I think that should be sufficient. You do not have to call anything to use this system. Just copy the trigger from the example map or call the Init function yourself.
vJass is really not required with the name prefixes.
 
Last edited:
Level 4
Joined
Apr 22, 2022
Messages
31
Nice system @Barade! I have some questions:

How do you get it to unstack more than 1 at a time? Example: I have a stack of 50 potions and I want to unstack 5 at a time, what do I change so it unstacks 5 at a time instead of 1?

Is it possible to make consecutively unstacked items stack onto themselves? I start unstacking my 50 potions and it just creates stacks of 1 and fills up the inventory instead of stacking on top of each other.

Thanks!
 
Level 25
Joined
Feb 2, 2006
Messages
1,667
Nice system @Barade! I have some questions:

How do you get it to unstack more than 1 at a time? Example: I have a stack of 50 potions and I want to unstack 5 at a time, what do I change so it unstacks 5 at a time instead of 1?

Is it possible to make consecutively unstacked items stack onto themselves? I start unstacking my 50 potions and it just creates stacks of 1 and fills up the inventory instead of stacking on top of each other.

Thanks!
Unstacking more than 1 at a time requires you to change the "- 1" and to set the charges in ItemUnstackAddItemToNearestFreeSlotOrGround to a higher value than 1. I could add a constant function returns this value, so it be easily adapted.

Do you mean if you unstack an item and in the next slot there is an item of the same type, it should be stacked onto this item instead of filling another slot?
Yes, this would be possible. I should probably add more options like this.
 
Level 38
Joined
Feb 27, 2007
Messages
4,951
JASS:
function ItemUnstackAddItemToNearestFreeSlot takes unit hero, integer itemTypeId, integer sourceSlot returns item
function ItemUnstackAddItemToNearestFreeSlotOrGround takes unit hero, integer itemTypeId, integer sourceSlot returns item
function ItemUnstackAddItemToNearestFreeSlotOrGroundUnstack takes unit hero, integer itemTypeId, integer sourceSlot, item sourceItem returns nothing
DUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUDE, are we playing who reads the whold functions fast? This is too long. I'm sorry but "wtf is this" was my first reaction, lol.

You could merge ItemUnstackAddItemToNearestFreeSlotOrGround and ItemUnstackAddItemToNearestFreeSlotOrGroundUnstack.
set the charges in ItemUnstackAddItemToNearestFreeSlotOrGround to a higher value
Wrda is absolutely right here but it's hilarious to me that the exact specific function he pointed out was actually the relevant function to answer the first question in this thread about the system's usage.
 
Level 25
Joined
Feb 2, 2006
Messages
1,667
Release 1.4 is here!


@Pyrogasm
No he is not right and certainly not absolutely. The function is not used from outside. In 1.4 I add functions which return these constants. Even if you change the value in the function, it is never called from outside, so the name does not matter. In fact only the function
JASS:
ItemUnstackSystemInit
is called from outside. This is not an API. He was just raging which is pretty useless and annoying since it has no benefit for anyone or anything. The long function names are due to the ItemUnstack prefix since the system does not require vJass. The only important thing is that the name does not collide with any other functions since there is no library or scope.
If you have an issue with the names or that it does not use vJass, you can write your own system. I think that JassHelper adds an annoying overhead even to the save time because it is rather slow. I cannot understand how people react that hostile because of function names of functions which are NOT called from outside. Please stop discussing this stuff now and concentrate on the functionality.



I have added the trequested features but my main issue is that I cannot extract the field:

JASS:
function ItemUnstackItemGetMaxStacks takes item whichItem returns integer
    return BlzGetItemIntegerField(whichItem, ConvertItemIntegerField('ista'))
endfunction

I would need it to check if an item can be stacked at all which is currently not checked and probably allows unstacking all items even the ones which are not stackable. I would also need this information if you unstack an item and it should be stacked to the next slot with an item of the same type.
Either there is some workaround to get this information or you will still need to register this information manually which would be really bad.
The only fields defined in common.j are these ones:

JASS:
    constant itemintegerfield ITEM_IF_LEVEL                 = ConvertItemIntegerField('ilev')
    constant itemintegerfield ITEM_IF_NUMBER_OF_CHARGES     = ConvertItemIntegerField('iuse')
    constant itemintegerfield ITEM_IF_COOLDOWN_GROUP        = ConvertItemIntegerField('icid')
    constant itemintegerfield ITEM_IF_MAX_HIT_POINTS        = ConvertItemIntegerField('ihtp')
    constant itemintegerfield ITEM_IF_HIT_POINTS            = ConvertItemIntegerField('ihpc')
    constant itemintegerfield ITEM_IF_PRIORITY              = ConvertItemIntegerField('ipri')
    constant itemintegerfield ITEM_IF_ARMOR_TYPE            = ConvertItemIntegerField('iarm')
    constant itemintegerfield ITEM_IF_TINTING_COLOR_RED     = ConvertItemIntegerField('iclr')
    constant itemintegerfield ITEM_IF_TINTING_COLOR_GREEN   = ConvertItemIntegerField('iclg')
    constant itemintegerfield ITEM_IF_TINTING_COLOR_BLUE    = ConvertItemIntegerField('iclb')
    constant itemintegerfield ITEM_IF_TINTING_COLOR_ALPHA   = ConvertItemIntegerField('ical')


This is the code for version 1.4 in which I added 3 functions which can be customized to change the effects of the system.
Again, they are never called from outside, so you only have to change the return values ...


JASS:
// Baradé's Item Unstack System 1.4
//
// Supports the missing Warcraft III feature of unstacking stacked items in your inventory.
//
// Usage:
// - Enable Warcraft's native stack system in the "Advanced - Gameplay Constants - Inventory - Enable Item Stacking".
// - Give certain item types a maximum stack value: "Stats - Max Stacks". Note that some of them like Wards do already have specified some values greater than 0 here.
// - Copy this code into your map script.
// - Call the function ItemUnstackSystemInit during the map initialization.
// - Customize the system by changing the values returned by ItemUnstackMaximumCharges and ItemUnstackAllowStackingNextItem.
//
// Recommended (optional):
// - Change the "Advanced - Game Interface - Text - General" from "|cff808080Drop item on shop to sell|R" into "|cff808080Drop item on shop to sell|NDouble right click item to unstack|R" to guide the player.
//
// Download:
// https://www.hiveworkshop.com/threads/barad%C3%A9s-item-unstack-system-1-1.339109/
//
// Change Log:
//
// 1.4 2022-07-09:
// - ItemUnstackMaximumCharges allows changing the number of unstacked charges.
// - ItemUnstackAllowStackingNextItem allows stacking the unstacked item to the next item with the same type instead of only using a free slot.
// - ItemUnstackItemGetMaxStacks to check if the item is even stackable and to make sure item charges will not be over the maximum.
//
// 1.3 2022-04-16:
// - Refactor function names.
//
// 1.2 2022-04-13:
// - Use UnitInventorySize instead of bj_MAX_INVENTORY to support different inventory sizes.
// - Place Footmen with unit inventories to check different inventory sizes.
//
// 1.1 2022-04-11:
// - Split into multiple functions.
// - Do not apply item name, tooltip and icon path on unstacking since it is broken.
// - Prefer the nearest empty slot on unstacking.
// - Add a line break to the unstacking hint.
// - Add some items with custom names and descriptions to test stacking/unstacking them.
// - Move system code into converted trigger.
// - Update preview image.

constant function ItemUnstackMaximumCharges takes nothing returns integer
    return 1
endfunction

constant function ItemUnstackAllowStackingNextItem takes nothing returns boolean
    return true
endfunction

function ItemUnstackItemGetMaxStacks takes item whichItem returns integer
    // TODO 'ista' cannot be extracted.
    //return BlzGetItemIntegerField(whichItem, ConvertItemIntegerField('ista'))
    return 100
endfunction

function ItemUnstackCopyItemProps takes item sourceItem, item targetItem returns nothing
    // some seem broken
    //call BlzSetItemName(targetItem, GetItemName(sourceItem))
    call BlzSetItemDescription(targetItem, BlzGetItemDescription(sourceItem))
    //call BlzSetItemTooltip(targetItem, BlzGetItemTooltip(sourceItem))
    call BlzSetItemExtendedTooltip(targetItem, BlzGetItemExtendedTooltip(sourceItem))
    //call BlzSetItemIconPath(targetItem, BlzGetItemIconPath(sourceItem))
    call SetItemPawnable(targetItem, IsItemPawnable(sourceItem))
    call SetItemInvulnerable(targetItem, IsItemInvulnerable(sourceItem))
    if (GetItemPlayer(sourceItem) != null) then
        call SetItemPlayer(targetItem, GetItemPlayer(sourceItem), false)
    endif
endfunction

function ItemUnstackGetItemSlot takes unit hero, item whichItem returns integer
    local integer sourceSlot = -1
    local integer i = 0
    loop
        if (UnitItemInSlot(hero, i) == whichItem) then
            set sourceSlot = i
        endif
        set i = i + 1
        exitwhen (sourceSlot != -1 or i >= UnitInventorySize(hero))
    endloop
    return sourceSlot
endfunction

function ItemUnstackAddItemToNearestFreeSlot takes unit hero, integer itemTypeId, integer charges, integer sourceSlot, item sourceItem returns item
    local item itemInNextSlot = null
    local item itemInPreviousSlot = null
    local boolean addedToFreeSlot = false
    // we need to specify the target slot explicitly to prevent stacking the items again
    // we prefer empty slots next to the unstacked item
    local item unstackedItem = null
    local integer i = sourceSlot + 1
    local integer j = sourceSlot - 1
    // check for a slot with an item with the same type and free stacks
    if (ItemUnstackAllowStackingNextItem()) then
        loop
            set itemInNextSlot = UnitItemInSlot(hero, i)
            if (i < UnitInventorySize(hero)) then
                if (itemInNextSlot == null) then
                    set addedToFreeSlot = true
                    call UnitAddItemToSlotById(hero, itemTypeId, i)
                    set unstackedItem = UnitItemInSlot(hero, i)
                elseif (GetItemTypeId(itemInNextSlot) == itemTypeId and GetItemCharges(itemInNextSlot) < ItemUnstackItemGetMaxStacks(itemInNextSlot)) then
                    set unstackedItem = itemInNextSlot
                endif
            endif
            set i = i + 1
            exitwhen (unstackedItem != null or i >= UnitInventorySize(hero))
        endloop
    endif

    // check for a free slot
    if (unstackedItem == null) then
        set i = sourceSlot + 1
        set j = sourceSlot - 1
        loop
            set itemInNextSlot = UnitItemInSlot(hero, i)
            set itemInPreviousSlot = UnitItemInSlot(hero, j)
            if (i < UnitInventorySize(hero) and itemInNextSlot == null) then
                set addedToFreeSlot = true
                call UnitAddItemToSlotById(hero, itemTypeId, i)
                set unstackedItem = UnitItemInSlot(hero, i)
            elseif (j >= 0 and itemInPreviousSlot == null) then
                set addedToFreeSlot = true
                call UnitAddItemToSlotById(hero, itemTypeId, j)
                set unstackedItem = UnitItemInSlot(hero, j)
            endif
            set i = i + 1
            set j = j - 1
            exitwhen (unstackedItem != null or (i >= UnitInventorySize(hero) and j < 0))
        endloop
    endif

    if (addedToFreeSlot) then
        call SetItemCharges(unstackedItem, charges)
        call ItemUnstackCopyItemProps(sourceItem, unstackedItem)
    else
        call SetItemCharges(unstackedItem, GetItemCharges(unstackedItem) + charges)
    endif

    return unstackedItem
endfunction

function ItemUnstackAddItemToNearestFreeSlotOrGround takes unit hero, integer itemTypeId, integer charges, integer sourceSlot, item sourceItem returns nothing
    // search for a free slot to unstack the item
    local item unstackedItem = ItemUnstackAddItemToNearestFreeSlot(hero, itemTypeId, charges, sourceSlot, sourceItem)
    // create the item for the hero with one slot if all slots are used
    if (unstackedItem == null) then
        set unstackedItem = CreateItem(itemTypeId, GetUnitX(hero), GetUnitY(hero))
        call SetItemCharges(unstackedItem, charges)
        call ItemUnstackCopyItemProps(sourceItem, unstackedItem)
    endif

    set unstackedItem = null
endfunction

function ItemUnstackTriggerCondition takes nothing returns boolean
    return GetIssuedOrderId() >= 852002 and GetIssuedOrderId() <= 852007 and GetOrderTargetItem() != null and ItemUnstackItemGetMaxStacks(GetOrderTargetItem()) > 0 and GetItemCharges(GetOrderTargetItem()) > 1 and ItemUnstackGetItemSlot(GetTriggerUnit(), GetOrderTargetItem()) == GetIssuedOrderId() - 852002
endfunction

function ItemUnstackTriggerAction takes nothing returns nothing
    local unit hero = GetTriggerUnit()
    local item sourceItem = GetOrderTargetItem()
    local integer sourceItemTypeId = GetItemTypeId(sourceItem)
    local integer sourceSlot = ItemUnstackGetItemSlot(hero, sourceItem)
    local integer charges = 1
    // wait for completing the order or the item is not at the target slot
    call TriggerSleepAction(0.0)
    // item does still exist and was dropped on its previous slot
    // we are not sure if this works when the item is removed via triggers since the value of the variable becomes an invalid reference
    if (sourceItem != null and GetWidgetLife(sourceItem) > 0.0 and GetItemCharges(sourceItem) > 0 and UnitItemInSlot(hero, sourceSlot) == sourceItem) then
        set charges = IMinBJ(GetItemCharges(sourceItem), ItemUnstackMaximumCharges())
        call SetItemCharges(sourceItem, GetItemCharges(sourceItem) - charges)
        call ItemUnstackAddItemToNearestFreeSlotOrGround(hero, sourceItemTypeId, charges, sourceSlot, sourceItem)
    endif

    set hero = null
    set sourceItem = null
endfunction

function ItemUnstackSystemInit takes nothing returns nothing
    local trigger whichTrigger = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(whichTrigger, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER)
    call TriggerAddCondition(whichTrigger, Condition(function ItemUnstackTriggerCondition))
    call TriggerAddAction(whichTrigger, function ItemUnstackTriggerAction)
endfunction
 
Last edited:

Wrda

Spell Reviewer
Level 25
Joined
Nov 18, 2012
Messages
1,864
I think these are really minor things.
Yes, they were minor issues, but they add up and can affect the overall appearance, therefore they're still important.
About vJass: Not all maps use vJass code and JassHelper can be quite annoying. Generating a map script with it takes some time.
If your map does not use vJass why should you use vJass for this one simple system? It doesn't even have global variables.
I myself have one big project which uses plenty of vJass but other maps which only use GUI triggers with some JASS functions.
I would totally disable the JassHelper/vJass if I did not define some free global variables.
You could apply this to any map that STILL doesn't use vJass, probably will use and here you have your answer.
vJass is really not required with the name prefixes.
And you wouldn't have name prefixes at all if you used it.
The long function names are due to the ItemUnstack prefix since the system does not require vJass. The only important thing is that the name does not collide with any other functions since there is no library or scope.
If you have an issue with the names or that it does not use vJass, you can write your own system. I think that JassHelper adds an annoying overhead even to the save time because it is rather slow.
All your reasons you gave to use jass were not to use VJass, these are not valid arguments. You could have simply answered "I want this system to be in Jass" and it would be cleared that you've aimed for that. But that's simply not what you said. Yes, you could create a VJass version, but that's on you to decide, I'm not going to force you on that, I've only marked this point because it wasn't clear what you wanted.
I cannot understand how people react that hostile because of function names of functions which are NOT called from outside. Please stop discussing this stuff now and concentrate on the functionality.
This is not an API. He was just raging which is pretty useless and annoying since it has no benefit for anyone or anything.
Even if you change the value in the function, it is never called from outside, so the name does not matter. In fact only the function
JASS:
ItemUnstackSystemInit
is called from outside.
So, you're telling me that if I go to a clothing store and check the men's section and see there's trousers, shorts, shirts, and women's clothes mixed randomly along side I shouldn't criticize to the staff and the manager about this nonsensical situation and just concentrate if the clothes are pretty and fit me? Are you kidding me right now? You've uploaded this to the people, for us to use, obviously we're going to talk about every aspect of it, regardless if you think it's important or not, you don't decide that.
Also, it's immature of you not understanding why we're talking about this. First of all, an interesting resource, even if it is simple or not, makes people curious and to how it was achieved internally. Second of all, one might download this resource and then one day wants to extend its functionality for whatever reason, and has to read ItemUnstackAddItemToNearestFreeSlotOrGroundOrSomethingVeryLongForUsToReadPleaseReduceThisFunctionHereThisHurtsMyEyes and he will have a somewhat hard time... Thirdly, this even might happen to yourself later, like months later.
You throwing random words such as "raging", "annoying", "useless" and this "perceived" hostilety make no sense. It seems pretty clear to me that my post had some satire and hyperbole, it's supposed to be taken lightly and have comedic value, even this crazy function name. If those words are how you think at first sight, it's very impulsive... and rather sad.
Besides that, you even changed some of the function names that were inconsistent with one another, so that even makes me more confused as to why you're even talking about this again.
An unwarranted reaction, must I say.
I have added the trequested features but my main issue is that I cannot extract the field:

JASS:
function ItemUnstackItemGetMaxStacks takes item whichItem returns integer
    return BlzGetItemIntegerField(whichItem, ConvertItemIntegerField('ista'))
endfunction

I would need it to check if an item can be stacked at all which is currently not checked and probably allows unstacking all items even the ones which are not stackable. I would also need this information if you unstack an item and it should be stacked to the next slot with an item of the same type.
Either there is some workaround to get this information or you will still need to register this information manually which would be really bad.
The only fields defined in common.j are these ones:

JASS:
    constant itemintegerfield ITEM_IF_LEVEL                 = ConvertItemIntegerField('ilev')
    constant itemintegerfield ITEM_IF_NUMBER_OF_CHARGES     = ConvertItemIntegerField('iuse')
    constant itemintegerfield ITEM_IF_COOLDOWN_GROUP        = ConvertItemIntegerField('icid')
    constant itemintegerfield ITEM_IF_MAX_HIT_POINTS        = ConvertItemIntegerField('ihtp')
    constant itemintegerfield ITEM_IF_HIT_POINTS            = ConvertItemIntegerField('ihpc')
    constant itemintegerfield ITEM_IF_PRIORITY              = ConvertItemIntegerField('ipri')
    constant itemintegerfield ITEM_IF_ARMOR_TYPE            = ConvertItemIntegerField('iarm')
    constant itemintegerfield ITEM_IF_TINTING_COLOR_RED     = ConvertItemIntegerField('iclr')
    constant itemintegerfield ITEM_IF_TINTING_COLOR_GREEN   = ConvertItemIntegerField('iclg')
    constant itemintegerfield ITEM_IF_TINTING_COLOR_BLUE    = ConvertItemIntegerField('iclb')
    constant itemintegerfield ITEM_IF_TINTING_COLOR_ALPHA   = ConvertItemIntegerField('ical')
Unfortunately this is Blizzard and their incompleteness...
This is the code for version 1.4 in which I added 3 functions which can be customized to change the effects of the system.
Again, they are never called from outside, so you only have to change the return values ...
You mean they're not supposed to be called by the user.
JASS:
// Baradé's Item Unstack System 1.4
//
// Supports the missing Warcraft III feature of unstacking stacked items in your inventory.
//
// Usage:
// - Enable Warcraft's native stack system in the "Advanced - Gameplay Constants - Inventory - Enable Item Stacking".
// - Give certain item types a maximum stack value: "Stats - Max Stacks". Note that some of them like Wards do already have specified some values greater than 0 here.
// - Copy this code into your map script.
// - Call the function ItemUnstackSystemInit during the map initialization.
// - Customize the system by changing the values returned by ItemUnstackMaximumCharges and ItemUnstackAllowStackingNextItem.
//
// Recommended (optional):
// - Change the "Advanced - Game Interface - Text - General" from "|cff808080Drop item on shop to sell|R" into "|cff808080Drop item on shop to sell|NDouble right click item to unstack|R" to guide the player.
//
// Download:
// https://www.hiveworkshop.com/threads/barad%C3%A9s-item-unstack-system-1-1.339109/
//
// Change Log:
//
// 1.4 2022-07-09:
// - ItemUnstackMaximumCharges allows changing the number of unstacked charges.
// - ItemUnstackAllowStackingNextItem allows stacking the unstacked item to the next item with the same type instead of only using a free slot.
// - ItemUnstackItemGetMaxStacks to check if the item is even stackable and to make sure item charges will not be over the maximum.
//
// 1.3 2022-04-16:
// - Refactor function names.
//
// 1.2 2022-04-13:
// - Use UnitInventorySize instead of bj_MAX_INVENTORY to support different inventory sizes.
// - Place Footmen with unit inventories to check different inventory sizes.
//
// 1.1 2022-04-11:
// - Split into multiple functions.
// - Do not apply item name, tooltip and icon path on unstacking since it is broken.
// - Prefer the nearest empty slot on unstacking.
// - Add a line break to the unstacking hint.
// - Add some items with custom names and descriptions to test stacking/unstacking them.
// - Move system code into converted trigger.
// - Update preview image.

constant function ItemUnstackMaximumCharges takes nothing returns integer
    return 1
endfunction

constant function ItemUnstackAllowStackingNextItem takes nothing returns boolean
    return true
endfunction

function ItemUnstackItemGetMaxStacks takes item whichItem returns integer
    // TODO 'ista' cannot be extracted.
    //return BlzGetItemIntegerField(whichItem, ConvertItemIntegerField('ista'))
    return 100
endfunction

function ItemUnstackCopyItemProps takes item sourceItem, item targetItem returns nothing
    // some seem broken
    //call BlzSetItemName(targetItem, GetItemName(sourceItem))
    call BlzSetItemDescription(targetItem, BlzGetItemDescription(sourceItem))
    //call BlzSetItemTooltip(targetItem, BlzGetItemTooltip(sourceItem))
    call BlzSetItemExtendedTooltip(targetItem, BlzGetItemExtendedTooltip(sourceItem))
    //call BlzSetItemIconPath(targetItem, BlzGetItemIconPath(sourceItem))
    call SetItemPawnable(targetItem, IsItemPawnable(sourceItem))
    call SetItemInvulnerable(targetItem, IsItemInvulnerable(sourceItem))
    if (GetItemPlayer(sourceItem) != null) then
        call SetItemPlayer(targetItem, GetItemPlayer(sourceItem), false)
    endif
endfunction

function ItemUnstackGetItemSlot takes unit hero, item whichItem returns integer
    local integer sourceSlot = -1
    local integer i = 0
    loop
        if (UnitItemInSlot(hero, i) == whichItem) then
            set sourceSlot = i
        endif
        set i = i + 1
        exitwhen (sourceSlot != -1 or i >= UnitInventorySize(hero))
    endloop
    return sourceSlot
endfunction

function ItemUnstackAddItemToNearestFreeSlot takes unit hero, integer itemTypeId, integer charges, integer sourceSlot, item sourceItem returns item
    local item itemInNextSlot = null
    local item itemInPreviousSlot = null
    local boolean addedToFreeSlot = false
    // we need to specify the target slot explicitly to prevent stacking the items again
    // we prefer empty slots next to the unstacked item
    local item unstackedItem = null
    local integer i = sourceSlot + 1
    local integer j = sourceSlot - 1
    // check for a slot with an item with the same type and free stacks
    if (ItemUnstackAllowStackingNextItem()) then
        loop
            set itemInNextSlot = UnitItemInSlot(hero, i)
            if (i < UnitInventorySize(hero)) then
                if (itemInNextSlot == null) then
                    set addedToFreeSlot = true
                    call UnitAddItemToSlotById(hero, itemTypeId, i)
                    set unstackedItem = UnitItemInSlot(hero, i)
                elseif (GetItemTypeId(itemInNextSlot) == itemTypeId and GetItemCharges(itemInNextSlot) < ItemUnstackItemGetMaxStacks(itemInNextSlot)) then
                    set unstackedItem = itemInNextSlot
                endif
            endif
            set i = i + 1
            exitwhen (unstackedItem != null or i >= UnitInventorySize(hero))
        endloop
    endif

    // check for a free slot
    if (unstackedItem == null) then
        set i = sourceSlot + 1
        set j = sourceSlot - 1
        loop
            set itemInNextSlot = UnitItemInSlot(hero, i)
            set itemInPreviousSlot = UnitItemInSlot(hero, j)
            if (i < UnitInventorySize(hero) and itemInNextSlot == null) then
                set addedToFreeSlot = true
                call UnitAddItemToSlotById(hero, itemTypeId, i)
                set unstackedItem = UnitItemInSlot(hero, i)
            elseif (j >= 0 and itemInPreviousSlot == null) then
                set addedToFreeSlot = true
                call UnitAddItemToSlotById(hero, itemTypeId, j)
                set unstackedItem = UnitItemInSlot(hero, j)
            endif
            set i = i + 1
            set j = j - 1
            exitwhen (unstackedItem != null or (i >= UnitInventorySize(hero) and j < 0))
        endloop
    endif

    if (addedToFreeSlot) then
        call SetItemCharges(unstackedItem, charges)
        call ItemUnstackCopyItemProps(sourceItem, unstackedItem)
    else
        call SetItemCharges(unstackedItem, GetItemCharges(unstackedItem) + charges)
    endif

    return unstackedItem
endfunction

function ItemUnstackAddItemToNearestFreeSlotOrGround takes unit hero, integer itemTypeId, integer charges, integer sourceSlot, item sourceItem returns nothing
    // search for a free slot to unstack the item
    local item unstackedItem = ItemUnstackAddItemToNearestFreeSlot(hero, itemTypeId, charges, sourceSlot, sourceItem)
    // create the item for the hero with one slot if all slots are used
    if (unstackedItem == null) then
        set unstackedItem = CreateItem(itemTypeId, GetUnitX(hero), GetUnitY(hero))
        call SetItemCharges(unstackedItem, charges)
        call ItemUnstackCopyItemProps(sourceItem, unstackedItem)
    endif

    set unstackedItem = null
endfunction

function ItemUnstackTriggerCondition takes nothing returns boolean
    return GetIssuedOrderId() >= 852002 and GetIssuedOrderId() <= 852007 and GetOrderTargetItem() != null and ItemUnstackItemGetMaxStacks(GetOrderTargetItem()) > 0 and GetItemCharges(GetOrderTargetItem()) > 1 and ItemUnstackGetItemSlot(GetTriggerUnit(), GetOrderTargetItem()) == GetIssuedOrderId() - 852002
endfunction

function ItemUnstackTriggerAction takes nothing returns nothing
    local unit hero = GetTriggerUnit()
    local item sourceItem = GetOrderTargetItem()
    local integer sourceItemTypeId = GetItemTypeId(sourceItem)
    local integer sourceSlot = ItemUnstackGetItemSlot(hero, sourceItem)
    local integer charges = 1
    // wait for completing the order or the item is not at the target slot
    call TriggerSleepAction(0.0)
    // item does still exist and was dropped on its previous slot
    // we are not sure if this works when the item is removed via triggers since the value of the variable becomes an invalid reference
    if (sourceItem != null and GetWidgetLife(sourceItem) > 0.0 and GetItemCharges(sourceItem) > 0 and UnitItemInSlot(hero, sourceSlot) == sourceItem) then
        set charges = IMinBJ(GetItemCharges(sourceItem), ItemUnstackMaximumCharges())
        call SetItemCharges(sourceItem, GetItemCharges(sourceItem) - charges)
        call ItemUnstackAddItemToNearestFreeSlotOrGround(hero, sourceItemTypeId, charges, sourceSlot, sourceItem)
    endif

    set hero = null
    set sourceItem = null
endfunction

function ItemUnstackSystemInit takes nothing returns nothing
    local trigger whichTrigger = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(whichTrigger, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER)
    call TriggerAddCondition(whichTrigger, Condition(function ItemUnstackTriggerCondition))
    call TriggerAddAction(whichTrigger, function ItemUnstackTriggerAction)
endfunction
I would argue that the changelog should be in another separate trigger as a comment, rather in the same script as it just bloats.
Also the comments about what ItemUnstackMaximumCharges does and the other 2 functions should be next to the functions themselves, I think it's more clear that way, than just scrolling up and down.
 
Level 25
Joined
Feb 2, 2006
Messages
1,667
Sorry I won't go into details in this discussion. I have no interest in discussing function names. You are free to upload your own system. I am not the impulsive one here. You started with comments like "DUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUDE, are we playing who reads the whold functions fast? This is too long. I'm sorry but "wtf is this" was my first reaction, lol." and now you are trying to turn it around.
Discussing function names and minor stuff like comments is a waste of time.

The only issue I have now is that I am not able to extract the field 'ista'.

As long as the ChangeLog does not get too long, it should be fine. You can copy and paste this system very easily into one trigger.
The other comments could be moved.
 
Level 38
Joined
Feb 27, 2007
Messages
4,951
No he is not right and certainly not absolutely.
Sure thing, man. Him specifically noticing a ridiculously named function, laughing about it, and then using that specific function as an example of poor code practices is 100% wrong. Right. How could I be so stupid?

I'm saying he's simply right to laugh at it because it is ridiculous. Doesn't matter if it's never intended to be called by the user. It's like laughing at the CD changer for a car being in the trunk just fucking because. "It can store 5 cds so it doesn't matter that you can't put a new cd in while you're driving." "Okay but it's still a laughably detached design choice."

I'm not shitting on you... I'm noting the irony that Wrda commented on a function, your response was "functions aren't called by the user so it doesn't matter" and then in your next reply you specifically had to tell someone to go interact with that exact function (not call it, no, but interact with it). The one you said users don't need to interact with. That is what's funny.



And if you want me to be constructive: the three functions I quoted Wrda quoting could all be combined into a single function (with the shortest name) if you gave it 2 additional boolean arguments for toGround and doUnstack.

Or you could, you know, use a prefix like IU_ for all your functions instead.
 
Last edited:
Level 25
Joined
Feb 2, 2006
Messages
1,667
Release 1.5:

- Use vJass since we have two triggers now, can use the initializer and use static ifs.
- Use constants instead of constant functions for the options of the system.
- Calculate the max stacks per item type with the help of a dummy if enabled.
- Increase the max stacks possible to 1000 since this is the maximum possible value in the object editor.
- Add option UNSTACK_HALF_CHARGES.


I hope you are happy now. I had to use vJass because of the number of triggers and static ifs etc. to determine the maximum stacks of an item type.
This is actually required mainly to prevent unstacking non-stackable item types and stacking them onto next items with max charges already stacked (this would not be a major issue I guess since it would be added to the next free slot automatically).
 

Wrda

Spell Reviewer
Level 25
Joined
Nov 18, 2012
Messages
1,864
A good system that completes the unstacking mechanic that blizzard's system lacks.
Approved.

A bit odd you're using standard unit type for a dummy. Usually a custom one is used. But I digress.

JASS:
public function GetStackItemDummy takes nothing returns unit
    return stackItemDummy
endfunction
Making the unit public would be enough, a function looks redundant.
JASS:
private function TriggerConditionOrderUnstack takes nothing returns boolean
static if (CHECK_MAX_STACKS) then
    local boolean isNotStackDummy = GetTriggerUnit() != MaxItemStacks_GetStackItemDummy()
else
    local boolean isNotStackDummy = true
endif
    return isNotStackDummy and GetIssuedOrderId() >= 852002 and GetIssuedOrderId() <= 852007 and GetOrderTargetItem() != null and GetMaxStacksByItemTypeIdIntern(GetItemTypeId(GetOrderTargetItem())) > 0 and GetItemCharges(GetOrderTargetItem()) > 1 and GetItemSlot(GetTriggerUnit(), GetOrderTargetItem()) == GetIssuedOrderId() - 852002
endfunction
While not a big deal, usually you'd want to avoid really wide lines (255 character limit afaik). Storing GetIssuedOrderId would be enough.

When inventory is full, unstacking the last item slot will drop a charge on the unit's feet, even if one of the items in earlier slots still doesn't have max charges. Just making sure, is this intended?
 
Level 25
Joined
Feb 2, 2006
Messages
1,667
A good system that completes the unstacking mechanic that blizzard's system lacks.
Approved.

A bit odd you're using standard unit type for a dummy. Usually a custom one is used. But I digress.

JASS:
public function GetStackItemDummy takes nothing returns unit
    return stackItemDummy
endfunction
Making the unit public would be enough, a function looks redundant.
JASS:
private function TriggerConditionOrderUnstack takes nothing returns boolean
static if (CHECK_MAX_STACKS) then
    local boolean isNotStackDummy = GetTriggerUnit() != MaxItemStacks_GetStackItemDummy()
else
    local boolean isNotStackDummy = true
endif
    return isNotStackDummy and GetIssuedOrderId() >= 852002 and GetIssuedOrderId() <= 852007 and GetOrderTargetItem() != null and GetMaxStacksByItemTypeIdIntern(GetItemTypeId(GetOrderTargetItem())) > 0 and GetItemCharges(GetOrderTargetItem()) > 1 and GetItemSlot(GetTriggerUnit(), GetOrderTargetItem()) == GetIssuedOrderId() - 852002
endfunction
While not a big deal, usually you'd want to avoid really wide lines (255 character limit afaik). Storing GetIssuedOrderId would be enough.

When inventory is full, unstacking the last item slot will drop a charge on the unit's feet, even if one of the items in earlier slots still doesn't have max charges. Just making sure, is this intended?
hmm not sure if it should start from the beginning again on unstacking. If it does this and you have a full inventory of stacked items, you can never drop an item with a single charge right? Whenever you unstack one item it will stack onto another one.
I could add an option for this and then adapt the following code to start all over again until reaching the slot with the unstacked item:

JASS:
// check for a slot with an item with the same type and free stacks
    if (ALLOW_STACKING_NEXT_ITEM) then
        loop
            set itemInNextSlot = UnitItemInSlot(hero, i)
            if (i < UnitInventorySize(hero)) then
                if (itemInNextSlot == null) then
                    set addedToFreeSlot = true
                    call UnitAddItemToSlotById(hero, itemTypeId, i)
                    set unstackedItem = UnitItemInSlot(hero, i)
                elseif (GetItemTypeId(itemInNextSlot) == itemTypeId and GetItemCharges(itemInNextSlot) < GetMaxStacksByItemTypeIdIntern(itemTypeId)) then
                    set unstackedItem = itemInNextSlot
                endif
            endif
            set i = i + 1
            exitwhen (unstackedItem != null or i >= UnitInventorySize(hero))
        endloop
    endif

The dummy getter exists, so you cannot overwrite the dummy like if it is a public global variable. The ID can be changed by the user in the constant.

I can store the issued order ID for the next release.

edit:
Release 1.7:
  • New option STACKING_NEXT_ITEM_FROM_START enabled by default.
  • Use local variables in trigger condition to reduce the length of the long line.
  • Move system comments into the library.
  • Split the code in the description.
 
Last edited:
Level 4
Joined
Apr 22, 2022
Messages
31
Potential bug: It seems like if you set MAX_UNSTACKED_CHARGES = something above 1 for example 10, it will pull 10 out of a stack just fine, but if you try to unstack 10 it will just keep it as 10 and create another item of that type with 0 charges. It should just be pulling 9 out and leaving 1 behind from a stack of 10.
 
Level 4
Joined
Apr 22, 2022
Messages
31
Found a bug with the ALLOW_STACKING_NEXT_ITEM part of this system when it is set to true. If you have a full inventory, but have an open stack and double right click another item of same type it will take out the max unstacked charges and place them onto the open stack, fully ignoring that items maximum stacking limit. In this example I had MAX_UNSTACKED_CHARGES set to 10 and the max stack size of the potion is 20 and CHECK_MAX_STACKS is set to true.

Pic1.png Pic2.png
 
Last edited:
Level 25
Joined
Feb 2, 2006
Messages
1,667
thx probably simply forgot a check. I guess it should unstack 4 there and try the remaining 6 at every other slot and then coming back to the slot before the last slot and keep them there.

edit:
Updated the system. Can you check again? I tried it with maximum of 100 for healing potions and for me the remainings dropped on the ground. I could also prevent that and just let them in the source item if that would be better.

You can disable unstack items for specific units now.
 
Last edited:
Top