• 🏆 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!

StackNSplit

  • Like
Reactions: deepstrasz and Vinz
Easy item charges stacking and splitting.
Used in Island Troll Tribes and Night of the Dead: Aftermath for stackable handling.

StackNSplit, the name gives it all:
- specify max amount of charges
- number of charges lost per split action

Embraces "containers" idea provided by my brother, Spinnaker.
- set item types as containers for other items
- specify different stack and split values

You can order units to pickup stackable items despite their inventory being full.

Available in vJass and Wurst versions. Checkout code documentation for more info and play with demo map to get a general idea of how powerful this tool is.

Visit main repository if you want to know full history of the library. For setting Wurst-only dependency point, please use wurst repository.

Testing vJass:
- simply download the demo map and launch it
- test map contains all dependancies and optional requirements listed in Trigger Editor

Testing Wurst:
- clone repository using git and uncomment demo (InitDemos.wurst) for system you wish to test
- use 'Run a Wurst map' VSCode task for running the map

StackNSplit (vJass):
JASS:
/*****************************************************************************
*
*    StackNSplit v1.1.2.2
*       by Bannar
*
*    Easy item charges stacking and splitting.
*
*    Thanks to Dangerb0y for original system.
*    Container idea provided by Spinnaker.
*
******************************************************************************
*
*    Requirements:
*
*       ListT by Bannar
*          hiveworkshop.com/threads/containers-list-t.249011/
*
*       InventoryEvent by Bannar
*          hiveworkshop.com/threads/snippet-inventoryevent.287084/
*
*       RegisterPlayerUnitEvent by Bannar
*          hiveworkshop.com/threads/snippet-registerevent-pack.250266/
*
*
*    Optional requirement:
*
*       SmoothItemPickup by Bannar
*          hiveworkshop.com/threads/smoothitempickup.306016/
*
******************************************************************************
*
*    Event API:
*
*       integer EVENT_ITEM_CHARGES_ADDED
*       integer EVENT_ITEM_CHARGES_REMOVED
*
*       Use RegisterNativeEvent or RegisterIndexNativeEvent for event registration.
*       GetNativeEventTrigger and GetIndexNativeEventTrigger provide access to trigger handles.
*
*
*       function GetItemStackingUnit takes nothing returns unit
*          Returns unit which manupilated event item.
*
*       function GetItemStackingItem takes nothing returns item
*          Returns manipulated event item.
*
*       function GetItemStackingCharges takes nothing returns integer
*          Returns number of charges that has been added or removed.
*
******************************************************************************
*
*    Containers - idea behind:
*
*       Each item type is allowed to have another item type assigned as its container,
*       and becoming an element at the same time.
*
*       Example:
*
*        | element - Gold Coin
*        | container - Endless Sack of Gold
*
*       Any item type can become container for another as long as it is not a stackable item type.
*       Containers cannot have thier own containers assigned.
*       Containers may declare different maximum stack and split count values.
*       Container is always prioritized over element type when item charges are being redistributed.
*       Each element can have multiple item types assigned as its containers.
*
******************************************************************************
*
*    Functions:
*
*       function IsUnitItemFullyStacked takes unit whichUnit, integer itemTypeId returns boolean
*          Checks if specified unit has hold any additional charges of provided item.
*
*       function UnitStackItem takes unit whichUnit, item whichItem returns boolean
*          Attempts to stack provided item for specified unit.
*
*       function UnitSplitItem takes unit whichUnit, item whichItem returns boolean
*          Attempts to split provided item for specified unit.
*
*
*    Functions (Container):
*
*       function IsItemContainer takes integer containerType returns boolean
*          Returns value indicating whether specifed item is stackable or not.
*
*       function GetItemContainerMaxStacks takes integer containerType returns integer
*          Returns maximum number of charges for specified container.
*
*       function GetItemContainerSplitCount takes integer containerType returns integer
*          Returns number of charges lost by specified container per split.
*
*       function GetItemContainerItem takes integer containerType returns integer
*          Returns item type assigned to specified container as its elements.
*
*       function GetItemContainerMinCharges takes integer containerType returns integer
*          Number of charges that container cannot go below during split operation.
*
*       function UnsetItemContainer takes integer elementType returns nothing
*          Unsets any container related data related to specified element item type.
*
*       function SetItemContainer takes integer elementType, integer containerType, integer stacks, integer splits, integer minCharges returns boolean
*          Sets specified containerType item type as container for item type elementType.
*          Argument minCharges specifies number of charges that container item cannot
*          go below during split operation.
*
*
*    Functions (Element):
*
*       function IsItemStackable takes integer elementType returns boolean
*          Returns value indicating whether specifed item is stackable or not.
*
*       function GetItemMaxStacks takes integer elementType returns integer
*          Returns maximum number of charges for specified item.
*
*       function GetItemSplitCount takes integer elementType returns integer
*          Returns number of charges lost by specified item per split.
*
*       function ItemHasContainer takes integer elementType returns boolean
*          Indicates if specifed element type has container assigned to it.
*
*       function GetItemContainers takes integer elementType returns IntegerList
*          Returns list of item types assigned to specified element as its containers.
*
*       function MakeItemUnstackable takes integer elementType returns nothing
*          Unregisters specified item from being stackable.
*
*       function MakeItemStackable takes integer elementType, integer stacks, integer splits returns boolean
*          Registers specified item as stackable.
*
*****************************************************************************/
library StackNSplit requires /*
                    */ ListT /*
                    */ InventoryEvent /*
                    */ RegisterPlayerUnitEvent /*
                    */ optional SmoothItemPickup

globals
    integer EVENT_ITEM_CHARGES_ADDED
    integer EVENT_ITEM_CHARGES_REMOVED
endglobals

globals
    private unit eventUnit = null
    private item eventItem = null
    private integer eventCharges = -1
    private TableArray table = 0
endglobals

function GetItemStackingUnit takes nothing returns unit
    return eventUnit
endfunction

function GetItemStackingItem takes nothing returns item
    return eventItem
endfunction

function GetItemStackingCharges takes nothing returns integer
    return eventCharges
endfunction

function IsItemContainer takes integer containerType returns boolean
    return table[3].has(containerType)
endfunction

function GetItemContainerMaxStacks takes integer containerType returns integer
    if IsItemContainer(containerType) then
        return table[0][containerType]
    endif
    return -1
endfunction

function GetItemContainerSplitCount takes integer containerType returns integer
    if IsItemContainer(containerType) then
        return table[1][containerType]
    endif
    return -1
endfunction

function GetItemContainerItem takes integer containerType returns integer
    if IsItemContainer(containerType) then
        return table[3][containerType]
    endif
    return 0
endfunction

function GetItemContainerMinCharges takes integer containerType returns integer
    if IsItemContainer(containerType) then
        return table[4][containerType]
    endif
    return -1
endfunction

function IsItemStackable takes integer elementType returns boolean
    return not IsItemContainer(elementType) and table[0].has(elementType)
endfunction

function GetItemMaxStacks takes integer elementType returns integer
    if IsItemStackable(elementType) then
        return table[0][elementType]
    endif
    return -1
endfunction

function GetItemSplitCount takes integer elementType returns integer
    if IsItemStackable(elementType) then
        return table[1][elementType]
    endif
    return -1
endfunction

function ItemHasContainer takes integer elementType returns boolean
    return table[2].has(elementType)
endfunction

function GetItemContainers takes integer elementType returns IntegerList
    if ItemHasContainer(elementType) then
        return table[2][elementType]
    endif
    return 0
endfunction

function MakeItemUnstackable takes integer elementType returns nothing
    if IsItemStackable(elementType) then
        call table[0].remove(elementType)
        call table[1].remove(elementType)
    endif
endfunction

function MakeItemStackable takes integer elementType, integer stacks, integer splits returns boolean
    if not IsItemContainer(elementType) and stacks > 0 then
        set table[0][elementType] = stacks
        set table[1][elementType] = IMaxBJ(splits, 1)
        return true
    endif
    return false
endfunction

function UnsetItemContainer takes integer containerType returns nothing
    local integer elementType = GetItemContainerItem(containerType)
    local IntegerList containers

    if elementType != 0 then
        call table[0].remove(containerType)
        call table[1].remove(containerType)
        call table[3].remove(containerType)
        call table[4].remove(containerType)

        // Remove containerType from containers list
        set containers = GetItemContainers(elementType)
        call containers.removeElem(containerType)
        if containers.empty() then
            call containers.destroy()
            call table[2].remove(elementType)
        endif
    endif
endfunction

function SetItemContainer takes integer elementType, integer containerType, integer stacks, integer splits, integer minCharges returns boolean
    local IntegerList containers

    if elementType == 0 or containerType == 0 then
        return false
    elseif stacks <= 0 or elementType == containerType then
        return false
    elseif IsItemContainer(elementType) or IsItemContainer(containerType) then
        return false
    elseif IsItemStackable(containerType) then
        return false
    endif

    set containers = GetItemContainers(elementType)
    if containers == 0 then
        set containers = IntegerList.create()
        set table[2][elementType] = containers
    endif
    call containers.push(containerType)

    set table[0][containerType] = stacks
    set table[1][containerType] = IMaxBJ(splits, 1)
    set table[3][containerType] = elementType
    set table[4][containerType] = IMaxBJ(minCharges, 0)
    return true
endfunction

function IsUnitItemFullyStacked takes unit whichUnit, integer itemTypeId returns boolean
    local boolean result = true
    local integer max
    local item itm
    local integer size
    local integer slot = 0
    local IntegerListItem iter

    if not IsUnitInventoryFull(whichUnit) then
        return false
    elseif IsItemContainer(itemTypeId) then
        return result
    endif

    set size = UnitInventorySize(whichUnit)
    if ItemHasContainer(itemTypeId) then
        set iter = GetItemContainers(itemTypeId).first
        loop
            exitwhen iter == 0
            set max = GetItemContainerMaxStacks(iter.data)

            if max > 0 then
                loop
                    exitwhen slot >= size
                    set itm = UnitItemInSlot(whichUnit, slot)
                    if GetItemTypeId(itm) == iter.data and GetItemCharges(itm) < max then
                        set result = false
                        exitwhen true
                    endif
                    set slot = slot + 1
                endloop
            endif
            set iter = iter.next
        endloop
    endif

    if result and IsItemStackable(itemTypeId) then
        set max = GetItemMaxStacks(itemTypeId)
        if max > 0 then
            set slot = 0
            loop
                exitwhen slot >= size
                set itm = UnitItemInSlot(whichUnit, slot)
                if GetItemTypeId(itm) == itemTypeId and GetItemCharges(itm) < max then
                    set result = false
                    exitwhen true
                endif
                set slot = slot + 1
            endloop
        endif
    endif

    set itm = null
    return result
endfunction

private function FireEvent takes integer evt, unit u, item itm, integer charges returns nothing
    local unit prevUnit = eventUnit
    local item prevItem = eventItem
    local integer prevCharges = eventCharges
    local integer playerId = GetPlayerId(GetOwningPlayer(u))

    set eventUnit = u
    set eventItem = itm
    set eventCharges = charges

    call TriggerEvaluate(GetNativeEventTrigger(evt))
    if IsNativeEventRegistered(playerId, evt) then
        call TriggerEvaluate(GetIndexNativeEventTrigger(playerId, evt))
    endif

    set eventUnit = prevUnit
    set eventItem = prevItem
    set eventCharges = prevCharges

    set prevUnit = null
    set prevItem = null
endfunction

private function StackItem takes unit u, item itm, item ignored, integer withTypeId, integer max returns integer
    local integer charges = GetItemCharges(itm)
    local integer slot = 0
    local integer size = UnitInventorySize(u)
    local item with
    local integer withCharges
    local integer diff

    loop
        exitwhen slot >= size
        set with = UnitItemInSlot(u, slot)

        if with != ignored and GetItemTypeId(with) == withTypeId then
            set withCharges = GetItemCharges(with)
            if withCharges < max then
                set diff = max - withCharges

                if diff >= charges then
                    call SetItemCharges(with, withCharges + charges)
                    call RemoveItem(itm)
                    call FireEvent(EVENT_ITEM_CHARGES_ADDED, u, with, charges)
                    set charges = 0
                    exitwhen true
                else
                    set charges = charges - diff
                    call SetItemCharges(with, max)
                    call SetItemCharges(itm, charges)
                    call FireEvent(EVENT_ITEM_CHARGES_REMOVED, u, itm, diff)
                    call FireEvent(EVENT_ITEM_CHARGES_ADDED, u, with, diff)
                endif
            endif
        endif

        set slot = slot + 1
    endloop

    set with = null
    return charges
endfunction

function UnitStackItem takes unit whichUnit, item whichItem returns boolean
    local integer charges = GetItemCharges(whichItem)
    local integer itemTypeId = GetItemTypeId(whichItem)
    local integer containerType
    local integer max
    local boolean result = false
    local IntegerListItem iter

    if whichUnit == null or charges == 0 then
        return result
    endif

    if not IsItemContainer(itemTypeId) then
        if ItemHasContainer(itemTypeId) then
            set iter = GetItemContainers(itemTypeId).first
            loop
                exitwhen iter == 0
                set containerType = iter.data

                set max = GetItemContainerMaxStacks(containerType)
                set charges = StackItem(whichUnit, whichItem, whichItem, containerType, max)
                exitwhen charges == 0
                set iter = iter.next
            endloop
            set result = true
        endif

        if IsItemStackable(itemTypeId) and charges > 0 then
            set max = GetItemMaxStacks(itemTypeId)
            call StackItem(whichUnit, whichItem, whichItem, itemTypeId, max)
            set result = true
        endif
    else
        set max = GetItemContainerMaxStacks(itemTypeId)
        call StackItem(whichUnit, whichItem, whichItem, GetItemContainerItem(itemTypeId), max)
        set result = true
    endif
    return result
endfunction

function UnitSplitItem takes unit whichUnit, item whichItem returns boolean
    local integer charges = GetItemCharges(whichItem)
    local integer itemTypeId = GetItemTypeId(whichItem)
    local integer toSplit
    local integer elementType
    local IntegerListItem iter
    local integer containerType
    local item with
    local trigger t
    local integer minCharges = 1

    if IsItemContainer(itemTypeId) then
        set minCharges = GetItemContainerMinCharges(itemTypeId)
        if charges <= minCharges then
            return false
        endif

        set elementType = GetItemContainerItem(itemTypeId)
        set toSplit = GetItemContainerSplitCount(itemTypeId)
    elseif IsItemStackable(itemTypeId) and charges > minCharges then
        set elementType = itemTypeId
        set toSplit = GetItemSplitCount(itemTypeId)
    else
        return false
    endif

    if toSplit >= charges then
        set toSplit = charges - minCharges
    endif
    call SetItemCharges(whichItem, charges - toSplit)
    call FireEvent(EVENT_ITEM_CHARGES_REMOVED, whichUnit, whichItem, toSplit)

    set with = CreateItem(elementType, GetUnitX(whichUnit), GetUnitY(whichUnit))
    call SetItemCharges(with, toSplit)
    // Redistribute splitted stacks if possible
    if ItemHasContainer(elementType) then
        set iter = GetItemContainers(elementType).first
        loop
            exitwhen iter == 0
            set containerType = iter.data

            set toSplit = StackItem(whichUnit, with, whichItem, containerType, GetItemContainerMaxStacks(containerType))
            exitwhen toSplit == 0
            set iter = iter.next
        endloop
    endif
    if IsItemStackable(elementType) and toSplit > 0 then
        set toSplit = StackItem(whichUnit, with, whichItem, elementType, GetItemMaxStacks(elementType))
    endif

    if toSplit > 0 then // something is left
        set t = GetAnyPlayerUnitEventTrigger(EVENT_PLAYER_UNIT_PICKUP_ITEM)
        call DisableTrigger(t)
        call UnitAddItem(whichUnit, with)
        call EnableTrigger(t)
        set t = null
    endif

    set with = null
    return true
endfunction

private function PickupItem takes unit u, item itm returns nothing
    local integer itemTypeId = GetItemTypeId(itm)
    local integer charges
    local integer elementType
    local integer max
    local item with
    local integer withCharges
    local integer diff
    local integer slot = 0
    local integer size

    if IsItemContainer(itemTypeId) then
        set max = GetItemContainerMaxStacks(itemTypeId)
        set elementType = GetItemContainerItem(itemTypeId)
        set charges = GetItemCharges(itm)
        set size = UnitInventorySize(u)
        loop
            exitwhen charges >= max
            exitwhen slot >= size
            set with = UnitItemInSlot(u, slot)
            set withCharges = GetItemCharges(with)

            if with != itm and withCharges > 0 and GetItemTypeId(with) == elementType then
                if charges + withCharges > max then
                    set diff = max - charges
                    call SetItemCharges(itm, max)
                    call SetItemCharges(with, withCharges - diff)
                    call FireEvent(EVENT_ITEM_CHARGES_REMOVED, u, with, diff)
                    call FireEvent(EVENT_ITEM_CHARGES_ADDED, u, itm, diff)
                    exitwhen true
                else
                    set charges = charges + withCharges
                    call SetItemCharges(itm, charges)
                    call RemoveItem(with)
                    call FireEvent(EVENT_ITEM_CHARGES_ADDED, u, itm, withCharges)
                endif
            endif

            set slot = slot + 1
        endloop
    else
        call UnitStackItem(u, itm)
    endif
endfunction

private function OnPickup takes nothing returns nothing
    call PickupItem(GetTriggerUnit(), GetManipulatedItem())
endfunction

private function OnMoved takes nothing returns nothing
    local unit u = GetInventoryManipulatingUnit()
    local item itm = GetInventoryManipulatedItem()
    local integer slotFrom = GetInventorySlotFrom()
    local integer itemTypeId = GetItemTypeId(itm)
    local integer charges
    local item swapped
    local integer swappedTypeId
    local integer swappedCharges
    local integer max = 0
    local integer total
    local integer diff
    local trigger t

    if slotFrom == GetInventorySlotTo() then // splitting
        call UnitSplitItem(u, itm)
    elseif not IsItemContainer(itemTypeId) then
        set charges = GetItemCharges(itm)
        set swapped = GetInventorySwappedItem()
        set swappedTypeId = GetItemTypeId(swapped)
        set swappedCharges = GetItemCharges(swapped)

        if charges > 0 then
            if swappedTypeId == itemTypeId and swappedCharges > 0 then
                set max = GetItemMaxStacks(itemTypeId)
            elseif GetItemContainerItem(swappedTypeId) == itemTypeId then
                set max = GetItemContainerMaxStacks(swappedTypeId)
            endif
        endif

        if max > 0 then
            set total = charges + swappedCharges
            if total > max then
                if swappedCharges < max then // if not met, allow for standard replacement action
                    set t = GetAnyPlayerUnitEventTrigger(EVENT_PLAYER_UNIT_DROP_ITEM)
                    call DisableTrigger(t)
                    call RemoveItem(itm) // Remove the item to prevent item swap from occurring
                    call EnableTrigger(t)
                    set t = GetAnyPlayerUnitEventTrigger(EVENT_PLAYER_UNIT_PICKUP_ITEM)
                    call DisableTrigger(t)
                    call UnitAddItemToSlotById(u, itemTypeId, slotFrom) // Create and add new item replacing removed one
                    call EnableTrigger(t)
                    set t = null

                    set itm = UnitItemInSlot(u, slotFrom)
                    call SetItemCharges(itm, total - max)
                    call SetItemCharges(swapped, max)
                    set diff = max - charges
                    call FireEvent(EVENT_ITEM_CHARGES_REMOVED, u, itm, diff)
                    call FireEvent(EVENT_ITEM_CHARGES_ADDED, u, swapped, diff)
                endif
            else
                call SetItemCharges(swapped, total)
                call RemoveItem(itm)
                call FireEvent(EVENT_ITEM_CHARGES_ADDED, u, swapped, charges)
            endif
        endif
        set swapped = null
    endif

    set u = null
    set itm = null
endfunction

static if LIBRARY_SmoothItemPickup then
private function OnSmoothPickup takes nothing returns nothing
    call PickupItem(GetSmoothItemPickupUnit(), GetSmoothItemPickupItem())
endfunction

private struct SmoothChargesStack extends array
    static method canPickup takes unit whichUnit, item whichItem returns boolean
        local integer itemTypeId = GetItemTypeId(whichItem)

        if IsItemContainer(itemTypeId) then
            return not IsUnitItemFullyStacked(whichUnit, GetItemContainerItem(itemTypeId))
        elseif IsItemStackable(itemTypeId) then
            return not IsUnitItemFullyStacked(whichUnit, itemTypeId)
        endif

        return false
    endmethod

    implement optional SmoothPickupPredicateModule
endstruct
endif

private module StackNSplitInit
    private static method onInit takes nothing returns nothing
        set EVENT_ITEM_CHARGES_ADDED = CreateNativeEvent()
        set EVENT_ITEM_CHARGES_REMOVED = CreateNativeEvent()

        set table = TableArray[5]

        call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_PICKUP_ITEM, function OnPickup)
        call RegisterNativeEvent(EVENT_ITEM_INVENTORY_MOVE, function OnMoved)
static if LIBRARY_SmoothItemPickup then
        call RegisterNativeEvent(EVENT_ITEM_SMOOTH_PICKUP, function OnSmoothPickup)
        call AddSmoothItemPickupCondition(SmoothChargesStack.create())
endif
    endmethod
endmodule

private struct StackNSplit extends array
    implement StackNSplitInit
endstruct

endlibrary
Demo code:
JASS:
scope StackNSplitDemo initializer Init

globals
    string description = "Potion of Healing stacks up to 10 times and splits for 2.\n" + /*
    */ "Healing Salve is a container for Potion of Healing. Holds up to 20 charges, splits for 5. Can be emptied.\n" + /*
    */ "Tiny Farm stacks up to 2 times and splits for 1.\n" + /*
    */ "Tine Castly is a container for Tiny Farm. Holds up to 7 charges, splits for 1. Has to have at least 1 charge to allow for splitting.\n" + /*
    */ "\nYou can order hero to stack up item charges despite their inventory being full."
endglobals

private function Init takes nothing returns nothing
    call MakeItemStackable('phea', 10, 2)
    call SetItemContainer('phea', 'hslv', 20, 5, 0)
    call MakeItemStackable('tfar', 2, 1)
    call SetItemContainer('tfar', 'tcas', 7, 1, 1)

    call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 45, description)
endfunction

endscope
StackNSplit (Wurst):
Wurst:
/*
*  StackNSplit v1.1.1.9
*     by Bannar
*
*  Easy item charges stacking and splitting.
*/
package StackNSplit
import RegisterEvents
import InventoryEvent
import SmoothItemPickup
import LinkedList

tuple eventInfo(unit u, item itm, int charges)

var eventState = eventInfo(null, null, -1)
constant eventAddedTrigger = CreateTrigger()
constant eventRemovedTrigger = CreateTrigger()

constant table = InitHashtable()

public enum EVENT_ITEM_CHARGES
    ADDED
    REMOVED

/** Returns unit which manupilated event item. */
public function getItemStackingUnit() returns unit
    return eventState.u

/** Returns manipulated event item. */
public function getItemStackingItem() returns item
    return eventState.itm

/** Returns number of charges that has been added or removed. */
public function getItemStackingCharges() returns int
    return eventState.charges

/** Returns trigger handle associated with specified item stacking event. */
public function getItemStackingEventTrigger(EVENT_ITEM_CHARGES whichEvent) returns trigger
    trigger result = null

    switch whichEvent
        case EVENT_ITEM_CHARGES.ADDED
            result = eventAddedTrigger
        case EVENT_ITEM_CHARGES.REMOVED
            result = eventRemovedTrigger
    return result

/** Registers new event handler for specified item stacking event. */
public function registerItemStackingEvent(EVENT_ITEM_CHARGES whichEvent, code func)
    switch whichEvent
        case EVENT_ITEM_CHARGES.ADDED
            eventAddedTrigger.addCondition(Condition(func))
        case EVENT_ITEM_CHARGES.REMOVED
            eventRemovedTrigger.addCondition(Condition(func))

/** Returns value indicating whether specifed item is stackable or not. */
public function isItemContainer(int containerType) returns boolean
    return table.hasInt(3, containerType)

/** Returns maximum number of charges for specified container. */
public function getItemContainerMaxStacks(int containerType) returns int
    var result = -1
    if isItemContainer(containerType)
        result = table.loadInt(0, containerType)
    return result

/** Returns item type assigned to specified container as its elements. */
public function getItemContainerSplitCount(int containerType) returns int
    var result = -1
    if isItemContainer(containerType)
        result = table.loadInt(1, containerType)
    return result

/** Returns item type assigned to specified container as its elements. */
public function getItemContainerItem(int containerType) returns int
    var result = 0
    if isItemContainer(containerType)
        result = table.loadInt(3, containerType)
    return result

/** Number of charges that container cannot go below during split operation. */
public function getItemContainerMinCharges(int containerType) returns int
    var result = -1
    if isItemContainer(containerType)
        result = table.loadInt(4, containerType)
    return result

/** Returns value indicating whether specifed item is stackable or not. */
public function isItemStackable(int elementType) returns boolean
    return not isItemContainer(elementType) and table.hasInt(0, elementType)

/** Retrieves maximum amount of stacks for specified item. */
public function getItemMaxStacks(int elementType) returns int
    var result = -1
    if isItemStackable(elementType)
        result = table.loadInt(0, elementType)
    return result

/** Returns number of charges lost by specified item per split. */
public function getItemSplitCount(int elementType) returns int
    var result = -1
    if isItemStackable(elementType)
        result = table.loadInt(1, elementType)
    return result

/** Indicates if specifed element type has container assigned to it. */
public function itemHasContainer(int elementType) returns boolean
    return table.hasInt(2, elementType)

/** Returns list of item types assigned to specified element as its containers. */
public function getItemContainers(int elementType) returns LinkedList<int>
    LinkedList<int> result = null
    if itemHasContainer(elementType)
        result = table.loadInt(2, elementType) castTo LinkedList<int>
    return result

/** Unregisters specified item from being stackable. */
public function makeItemUnstackable(int elementType)
    if isItemStackable(elementType)
        table.removeInt(0, elementType)
        table.removeInt(1, elementType)

/** Registers specified item as stackable. */
public function makeItemStackable(int elementType, int stacks, int splits) returns boolean
    var result = false
    if not isItemContainer(elementType) and stacks > 0
        table.saveInt(0, elementType, stacks)
        table.saveInt(1, elementType, max(splits, 1))
        result = true
    return result

/** Registers specified item as stackable. */
public function makeItemStackable(int elementType, int stacks) returns boolean
    return makeItemStackable(elementType, stacks, 1)

/** Unsets any container related data related to specified element item type. */
public function unsetItemContainer(int containerType)
    var elementType = getItemContainerItem(containerType)
    if elementType != 0
        table.removeInt(0, containerType)
        table.removeInt(1, containerType)
        table.removeInt(3, containerType)
        table.removeInt(4, containerType)

        // Remove containerType from containers list
        var containers = getItemContainers(elementType)
        containers.remove(containerType)
        if containers.isEmpty()
            destroy containers
            table.removeInt(2, elementType)

/** Sets specified containerType item type as container for item type elementType.
    Argument minCharges specifies number of charges that container item cannot
    go below during split operation. */
public function setItemContainer(int elementType, int containerType, int stacks, int splits, int minCharges) returns boolean
    if elementType == 0 or containerType == 0
        return false
    else if stacks <= 0 or elementType == containerType
        return false
    else if isItemContainer(elementType) or isItemContainer(containerType)
        return false
    else if isItemStackable(containerType)
        return false

    var containers = getItemContainers(elementType)
    if containers == null
        containers = new LinkedList<int>()
        table.saveInt(2, elementType, containers castTo int)
    containers.push(containerType)

    table.saveInt(0, containerType, stacks)
    table.saveInt(1, containerType, max(splits, 1))
    table.saveInt(3, containerType, elementType)
    table.saveInt(4, containerType, max(minCharges, 0))
    return true

/** Sets specified containerType item type as container for item type elementType. */
public function setItemContainer(int elementType, int containerType, int stacks) returns boolean
    return setItemContainer(elementType, containerType, stacks, 1, 0)

/** Checks if unit inventory is fully stacked and no charges can be added. */
public function unit.isItemFullyStacked(int itemTypeId) returns boolean
    if not this.isInventoryFull()
        return false
    else if isItemContainer(itemTypeId)
        return true

    var result = true
    var last = this.inventorySize() - 1
    if itemHasContainer(itemTypeId)
        for containerType in getItemContainers(itemTypeId)
            var max = getItemContainerMaxStacks(containerType)
            if max > 0
                for slot = 0 to last
                    var itm = this.itemInSlot(slot)
                    if itm.getTypeId() == containerType and itm.getCharges() < max
                        result = false
                        break

    if result and isItemStackable(itemTypeId)
        var max = getItemMaxStacks(itemTypeId)
        if max > 0
            for slot = 0 to last
                var itm = this.itemInSlot(slot)
                if itm.getTypeId() == itemTypeId and itm.getCharges() < max
                    result = false
                    break
    return result

function fireEvent(trigger evt, eventInfo currState)
    var prevState = eventState
    eventState = currState
    evt.evaluate()
    eventState = prevState

function stackItem(unit u, item itm, item ignored, int withTypeId, int max) returns int
    var charges = itm.getCharges()
    var last = u.inventorySize() - 1

    for slot = 0 to last
        var with = u.itemInSlot(slot)
        if with != ignored and with.getTypeId() == withTypeId
            var withCharges = with.getCharges()

            if withCharges < max
                var diff = max - withCharges
                if diff >= charges
                    with.setCharges(withCharges + charges)
                    itm.remove()
                    fireEvent(eventAddedTrigger, eventInfo(u, with, charges))
                    charges = 0
                    break
                else
                    charges -= diff
                    with.setCharges(max)
                    itm.setCharges(charges)
                    fireEvent(eventRemovedTrigger, eventInfo(u, itm, diff))
                    fireEvent(eventAddedTrigger, eventInfo(u, with, diff))
    return charges

/** Attempts to stack provided item for specified unit. */
public function unit.stackItem(item whichItem) returns boolean
    var charges = whichItem.getCharges()
    if charges == 0
        return false

    var result = false
    var itemTypeId = whichItem.getTypeId()
    if not isItemContainer(itemTypeId)
        if itemHasContainer(itemTypeId)
            for containerType in getItemContainers(itemTypeId)
                charges = stackItem(this, whichItem, whichItem, containerType, getItemContainerMaxStacks(containerType))
                if charges == 0
                    break
            result = true

        if isItemStackable(itemTypeId) and charges > 0
            stackItem(this, whichItem, whichItem, itemTypeId, getItemMaxStacks(itemTypeId))
            result = true
    else
        stackItem(this, whichItem, whichItem, getItemContainerItem(itemTypeId), getItemContainerMaxStacks(itemTypeId))
        result = true
    return result

/** Attempts to split provided item for specified unit. */
public function unit.splitItem(item whichItem) returns boolean
    var charges = whichItem.getCharges()
    var itemTypeId = whichItem.getTypeId()
    int elementType
    int toSplit
    var minCharges = 1

    if isItemContainer(itemTypeId)
        minCharges = getItemContainerMinCharges(itemTypeId)
        if charges <= minCharges
            return false

        elementType = getItemContainerItem(itemTypeId)
        toSplit = getItemContainerSplitCount(itemTypeId)
    else if isItemStackable(itemTypeId) and charges > minCharges
        elementType = itemTypeId
        toSplit = getItemSplitCount(itemTypeId)
    else
        return false

    if toSplit >= charges
        toSplit = charges - minCharges
    whichItem.setCharges(charges - toSplit)
    fireEvent(eventRemovedTrigger, eventInfo(this, whichItem, toSplit))

    var with = createItem(elementType, this.getPos())
    with.setCharges(toSplit)
    // Redistribute splitted stacks if possible
    if itemHasContainer(elementType)
        for containerType in getItemContainers(elementType)
            toSplit = stackItem(this, with, whichItem, containerType, getItemContainerMaxStacks(containerType))
            if toSplit == 0
                break
    if isItemStackable(elementType) and toSplit > 0
        toSplit = stackItem(this, with, whichItem, elementType, getItemMaxStacks(elementType))

    if toSplit > 0 // something is left
        var t = getPlayerUnitEventTrigger(EVENT_PLAYER_UNIT_PICKUP_ITEM)
        t.disable()
        this.addItemHandle(with)
        t.enable()
    return true

function pickupItem(unit u, item itm)
    var itemTypeId = itm.getTypeId()

    if isItemContainer(itemTypeId)
        var max = getItemContainerMaxStacks(itemTypeId)
        var elementType = getItemContainerItem(itemTypeId)
        var charges = itm.getCharges()
        var last = u.inventorySize() - 1

        for slot = 0 to last
            if charges >= max
                break
            var with = u.itemInSlot(slot)
            var withCharges = with.getCharges()

            if with != itm and withCharges > 0 and with.getTypeId() == elementType
                if charges + withCharges > max
                    int diff = max - charges
                    itm.setCharges(max)
                    with.setCharges(withCharges - diff)
                    fireEvent(eventRemovedTrigger, eventInfo(u, with, diff))
                    fireEvent(eventAddedTrigger, eventInfo(u, itm, diff))
                    break
                else
                    charges += withCharges
                    itm.setCharges(charges)
                    with.remove()
                    fireEvent(eventAddedTrigger, eventInfo(u, itm, withCharges))
    else
        u.stackItem(itm)

function onPickup()
    pickupItem(GetTriggerUnit(), GetManipulatedItem())

function onMoved()
    var slotFrom = getInventorySlotFrom()
    if slotFrom == getInventorySlotTo() // splitting
        getInventoryManipulatingUnit().splitItem(getInventoryManipulatedItem())
        return

    var u = getInventoryManipulatingUnit()
    var itm = getInventoryManipulatedItem()
    var itemTypeId = itm.getTypeId()
    if not isItemContainer(itemTypeId)
        var charges = itm.getCharges()
        var swapped = getInventorySwappedItem()
        var swappedTypeId = swapped.getTypeId()
        var swappedCharges = swapped.getCharges()
        var max = 0

        if charges > 0
            if swappedTypeId == itemTypeId and swappedCharges > 0
                max = getItemMaxStacks(itemTypeId)
            else if getItemContainerItem(swappedTypeId) == itemTypeId
                max = getItemContainerMaxStacks(swappedTypeId)

        if max > 0
            var total = charges + swappedCharges
            if total > max
                if swappedCharges < max // if not met, allow for standard replacement action
                    var t = getPlayerUnitEventTrigger(EVENT_PLAYER_UNIT_DROP_ITEM)
                    t.disable()
                    itm.remove() // Remove the item to prevent item swap from occurring
                    t.enable()
                    t = getPlayerUnitEventTrigger(EVENT_PLAYER_UNIT_PICKUP_ITEM)
                    t.disable()
                    u.addItemToSlot(itemTypeId, slotFrom) // Create and add new item replacing removed one
                    t.enable()

                    itm = u.itemInSlot(slotFrom)
                    itm.setCharges(total - max)
                    swapped.setCharges(max)
                    var diff = max - charges
                    fireEvent(eventRemovedTrigger, eventInfo(u, itm, diff))
                    fireEvent(eventAddedTrigger, eventInfo(u, swapped, diff))
            else
                swapped.setCharges(total)
                itm.remove()
                fireEvent(eventAddedTrigger, eventInfo(u, swapped, charges))

function onSmoothPickup()
    pickupItem(getSmoothItemPickupUnit(), getSmoothItemPickupItem())

class SmoothChargesStack implements SmoothPickupPredicate
    function canPickup(unit whichUnit, item whichItem) returns boolean
        var itemTypeId = whichItem.getTypeId()
        var result = false

        if isItemContainer(itemTypeId)
            result = not whichUnit.isItemFullyStacked(getItemContainerItem(itemTypeId))
        else if isItemStackable(itemTypeId)
            result = not whichUnit.isItemFullyStacked(itemTypeId)
        return result

init
    registerPlayerUnitEvent(EVENT_PLAYER_UNIT_PICKUP_ITEM, () -> onPickup())
    registerInventoryEvent(EVENT_ITEM_INVENTORY.MOVE, () -> onMoved())
    registerSmoothItemPickupEvent(() -> onSmoothPickup())
    addSmoothItemPickupCondition(new SmoothChargesStack())
Demo code:
Wurst:
package StackNSplitDemo
import StackNSplit

string description = "Potion of Healing stacks up to 10 times and splits for 2.\n" +
    "Healing Salve is a container for Potion of Healing. Holds up to 20 charges, splits for 5. Can be emptied.\n" +
    "Tiny Farm stacks up to 2 times and splits for 1.\n" +
    "Tine Castly is a container for Tiny Farm. Holds up to 7 charges, splits for 1. Has to have at least 1 charge to allow for splitting.\n" +
    "\nYou can order hero to stack up item charges despite their inventory being full."

function createAllItems()
    CreateItem('ckng', - 402.8, 548.5)
    CreateItem('modt', - 521.4, 434.9)
    CreateItem('ratf', - 524.1, 552.4)
    CreateItem('rde4', - 404.8, 435.1)
    CreateItem('hslv', - 150.9, 367.1)
    CreateItem('hslv', - 207.9, 267.3)
    CreateItem('hslv', - 262.7, 367.5)
    CreateItem('phea', - 55.3, 108.0).setCharges(3)
    CreateItem('phea', - 138.6, 107.1).setCharges(3)
    CreateItem('phea', - 236.8, 96.9).setCharges(3)
    CreateItem('phea', - 324.2, 93.8).setCharges(3)
    CreateItem('phea', - 59.8, 23.2).setCharges(3)
    CreateItem('phea', - 135.4, 27.6).setCharges(3)
    CreateItem('phea', - 226.7, 17.6).setCharges(3)
    CreateItem('phea', - 311.4, 10.7).setCharges(3)
    CreateItem('tcas', - 938.9, - 54.9)
    CreateItem('tcas', - 937.4, 63.4)
    CreateItem('tfar', - 794.6, 157.8)
    CreateItem('tfar', - 796.7, 57.9)
    CreateItem('tfar', - 798.0, - 39.2)
    CreateItem('tfar', - 799.0, - 140.1)
    CreateItem('tfar', - 703.2, 173.7)
    CreateItem('tfar', - 703.0, 67.6)
    CreateItem('tfar', - 695.6, - 33.0)
    CreateItem('tfar', - 694.6, - 140.7)

public function initStackNSplitDemo()
    createAllItems()

    makeItemStackable('phea', 10, 2)
    setItemContainer('phea', 'hslv', 20, 5, 0)
    makeItemStackable('tfar', 2, 1)
    setItemContainer('tfar', 'tcas', 7, 1, 1)

    printTimed(description, 45)
Contents

Test Map (Map)

Reviews
MyPad
This works surprisingly well, after some testing. The optional requirements do not impede the performance of the system. Status: Approved

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
I find it immature for reviewer to set resource out of front page without stating the reason for it. This lowers the amount of downloads and possible feedback (because hey, most ppl don't select "Awaiting Update" flag).
I welcome everyone's feedback - not just one from reviewers - thus, it'd be much better to leave it be until you post your objections and thoughts so we can get constructive discussion going as soon as possible.
 
Level 18
Joined
Oct 17, 2012
Messages
818
I am not sure this was a limitation of the game or system or intentional. I had a full inventory with two Healing Potions stacked to the max (10). I could not pick up its container, the Healing Salve.
 
Last edited:
Top