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

ItemRecipe

  • Like
Reactions: deepstrasz and AGD
Powerful item recipe creator, one you haven't seen before.
Used in Island Troll Tribes for recipe handling.

Supports:
- unordered or ordered recipes types
- onpickup or on ability cast
- option for disassembling result items to their ingredients
- batches, fill several items into same spot
- arbitrary unit requirements e.g.: at least one spider alive on the map
- assembly of recipes despite unit's 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

ItemRecipe (vJass):
JASS:
/*****************************************************************************
*
*    ItemRecipe v1.1.2.3
*       by Bannar
*
*    Powerful item recipe creator.
*
******************************************************************************
*
*    Requirements:
*
*       ListT by Bannar
*          hiveworkshop.com/threads/containers-list-t.249011/
*
*       VectorT by Bannar
*          hiveworkshop.com/threads/containers-vector-t.248942/
*
*       RegisterPlayerUnitEvent by Bannar
*          hiveworkshop.com/threads/snippet-registerevent-pack.250266/
*
*
*    Optional requirements:
*
*       InventoryEvent by Bannar
*          hiveworkshop.com/threads/snippet-inventoryevent.287084/
*
*       ItemRestriction by Bannar
*          hiveworkshop.com/threads/itemrestriction.306012/
*
*       SmoothItemPickup by Bannar
*          hiveworkshop.com/threads/smoothitempickup.306016/
*
******************************************************************************
*
*    Event API:
*
*       integer EVENT_ITEM_RECIPE_ASSEMBLE
*
*       Use RegisterNativeEvent or RegisterIndexNativeEvent for event registration.
*       GetNativeEventTrigger and GetIndexNativeEventTrigger provide access to trigger handles.
*
*
*       function GetEventItemRecipe takes nothing returns ItemRecipe
*          Returns triggering item recipe.
*
*       function GetEventItemRecipeUnit takes nothing returns unit
*          Returns recipe triggering unit.
*
*       function GetEventItemRecipeItem takes nothing returns item
*          Returns reward item for triggering recipe.
*
*       function GetEventItemRecipeIngredients takes nothing returns RecipeIngredientVector
*          Returns collection of ingredients chosen to assemble the reward item,
*          where each index corresponds to triggering unit inventory slot.
*
******************************************************************************
*
*    struct RecipeIngredient:
*
*       Fields:
*
*        | IntegerList itemTypeId
*        |    Item type of this ingredient.
*        |
*        | integer perishable
*        |    Whether ingredient is destroyed during assembly.
*        |
*        | integer charges
*        |    Number of charges required by ingredient.
*        |
*        | integer index
*        |    Indicates the slot which given ingredient occupies.
*
*
*    struct ItemRecipe:
*
*       Fields:
*
*        | readonly integer reward
*        |    Reward item type.
*        |
*        | integer charges
*        |    Number of charges to assign to reward item.
*        |
*        | boolean ordered
*        |    Whether recipe is ordered or unordered item inventory slot wise.
*        |
*        | boolean permanent
*        |    Determines if recipe can be disassembled.
*        |
*        | boolean pickupable
*        |    Whether recipe can be assembled when picking up items.
*        |
*        | optional UnitRequirement requirement
*        |    Criteria that unit needs to meet to assemble the recipe.
*        |
*        | readonly integer count
*        |    Total number of items required by the recipe.
*
*
*       General:
*
*        | static method create takes integer reward, integer charges, boolean ordered, boolean permanent, boolean pickupable returns thistype
*        |    Creates new instance of ItemRecipe struct.
*        |
*        | method destroy takes nothing returns nothing
*        |    Releases all resources this instance occupies.
*        |
*        | static method operator [] takes thistype other returns thistype
*        |    Copy contructor.
*
*
*       Access and modifiers:
*
*        | static method getRecipesForReward takes integer itemTypeId returns ItemRecipeList
*        |    Returns recipes which reward matches specified item type.
*        |
*        | static method getRecipesWithIngredient takes integer itemTypeId returns ItemRecipeList
*        |    Returns recipes that specified item is part of.
*        |
*        | static method getRecipesWithAbility takes integer abilityId returns ItemRecipeList
*        |    Returns recipes that can be assembled by casting specified ability.
*        |
*        | method getIngredients takes nothing returns RecipeIngredientList
*        |    Returns shallow copy of item recipe data.
*        |
*        | method isIngredient takes integer itemTypeId returns boolean
*        |    Whether specified item type is a part of the recipe.
*        |
*        | method getAbility takes nothing returns integer
*        |    Retrieves id of ability thats triggers assembly of this recipe.
*        |
*        | method setAbility takes integer abilityId returns thistype
*        |    Sets or removes specified ability from triggering recipe assembly.
*        |
*        | method startBatch takes nothing returns thistype
*        |    Starts single-reference counted batch. Allows to assign multiple items to the same item slot.
*        |
*        | method endBatch takes nothing returns thistype
*        |    Closes current batch.
*        |
*        | method removeItem takes integer itemTypeId returns thistype
*        |    Removes all entries that match specified item type from recipe ingredient list.
*        |
*        | method addItem takes integer itemTypeId, boolean perishable, integer charges returns thistype
*        |    Adds new entry to recipe ingredient list.
*        |
*        | method addItemEx takes integer itemTypeId returns thistype
*        |    Adds new entry to recipe ingredient list.
*
*
*    Assembly & disassembly:
*
*        | method test takes unit whichUnit, ItemVector items returns RecipeIngredientVector
*        |    Checks if recipe can be assembled for specified unit given the ingredients list.
*        |
*        | method testEx takes unit whichUnit returns RecipeIngredientVector
*        |    Checks if recipe can be assembled for specified unit.
*        |
*        | method assemble takes unit whichUnit, ItemVector items returns boolean
*        |    Attempts to assemble recipe for specified unit given the ingredients list.
*        |
*        | method assembleEx takes unit whichUnit returns boolean
*        |    Attempts to assemble recipe for specified unit.
*        |
*        | method disassemble takes unit whichUnit returns boolean
*        |    Reverts the assembly, removing the reward item and returning all ingredients to specified unit.
*
*
******************************************************************************
*
*    Functions:
*
*       function UnitAssembleItem takes unit whichUnit, integer itemTypeId returns boolean
*          Attempts to assemble specified item type for provided unit.
*
*       function UnitDisassembleItem takes unit whichUnit, item whichItem returns boolean
*          Reverts the assembly, removing the reward item and returning all ingredients to specified unit.
*
*****************************************************************************/
library ItemRecipe requires /*
                   */ ListT /*
                   */ VectorT /*
                   */ RegisterPlayerUnitEvent /*
                   */ optional InventoryEvent /*
                   */ optional ItemRestriction /*
                   */ optional SmoothItemPickup

globals
    integer EVENT_ITEM_RECIPE_ASSEMBLE
endglobals

globals
    private ItemRecipe eventRecipe = 0
    private unit eventUnit = null
    private item eventItem = null
    private ItemVector eventIngredients = 0

    private Table instanceTable = 0
endglobals

function GetEventItemRecipe takes nothing returns ItemRecipe
    return eventRecipe
endfunction

function GetEventItemRecipeUnit takes nothing returns unit
    return eventUnit
endfunction

function GetEventItemRecipeItem takes nothing returns item
    return eventItem
endfunction

function GetEventItemRecipeIngredients takes nothing returns ItemVector
    return eventIngredients
endfunction

// ItemVector definition can be moved directly to the VectorT library file
//! runtextmacro DEFINE_VECTOR("", "ItemVector", "item")
//! runtextmacro DEFINE_STRUCT_VECTOR("", "RecipeIngredientVector", "RecipeIngredient")
//! runtextmacro DEFINE_STRUCT_LIST("", "RecipeIngredientList", "RecipeIngredient")
//! runtextmacro DEFINE_STRUCT_LIST("", "ItemRecipeList", "ItemRecipe")

private function GetUnitItemVector takes unit whichUnit returns ItemVector
    local integer slot = 0
    local integer size = UnitInventorySize(whichUnit)
    local ItemVector result = ItemVector.create()

    loop
        exitwhen slot >= size
        call result.push(UnitItemInSlot(whichUnit, slot))
        set slot = slot + 1
    endloop
    return result
endfunction

private function FireEvent takes ItemRecipe recipe, unit u, item it, ItemVector ingredients returns nothing
    local ItemRecipe prevRecipe = eventRecipe
    local unit prevUnit = eventUnit
    local item prevItem = eventItem
    local ItemVector prevIngredients = eventIngredients
    local integer playerId = GetPlayerId(GetOwningPlayer(u))

    set eventRecipe = recipe
    set eventUnit = u
    set eventItem = it
    set eventIngredients = ingredients

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

    set eventRecipe = prevRecipe
    set eventUnit = prevUnit
    set eventItem = prevItem
    set eventIngredients = prevIngredients

    set prevUnit = null
    set prevItem = null
endfunction

struct RecipeIngredient extends array
    integer itemTypeId
    boolean perishable
    integer charges
    // If non 0, then it is part of a batch i.e multiple items can fill its spot
    integer index

    implement Alloc

    static method create takes integer itemTypeId, boolean perishable, integer charges, integer index returns thistype
        local thistype this = allocate()
        set this.itemTypeId = itemTypeId
        set this.perishable = perishable
        set this.charges = charges
        set this.index = index
        return this
    endmethod

    method destroy takes nothing returns nothing
        set itemTypeId = 0
        set perishable = false
        set charges = 0
        set index = 0
        call deallocate()
    endmethod

    static method operator [] takes thistype other returns thistype
        return create(other.itemTypeId, other.perishable, other.charges, other.index)
    endmethod
endstruct

struct ItemRecipe extends array
    integer charges
    boolean ordered
    boolean permanent
    boolean pickupable
static if LIBRARY_ItemRestriction then
    UnitRequirement requirement
endif
    readonly integer reward
    readonly integer count
    private RecipeIngredientList ingredients
    private integer abilityId
    private boolean batch

    implement Alloc

    private static method saveRecipe takes integer index, ItemRecipe recipe returns nothing
        local ItemRecipeList recipes
        if not instanceTable.has(index) then
            set instanceTable[index] = ItemRecipeList.create()
        endif
        set recipes = instanceTable[index]
        if recipes.find(recipe) == 0 then
            call recipes.push(recipe)
        endif
    endmethod

    private static method flushRecipe takes integer index, ItemRecipe recipe returns nothing
        local ItemRecipeList recipes = instanceTable[index]
        call recipes.erase(recipes.find(recipe))
        if recipes.empty() then
            call recipes.destroy()
            call instanceTable.remove(index)
        endif
    endmethod

    private static method getRecipes takes integer index returns ItemRecipeList
        if instanceTable.has(index) then
            return instanceTable[index]
        endif
        return 0
    endmethod

    static method getRecipesForReward takes integer itemTypeId returns ItemRecipeList
        return getRecipes(-itemTypeId)
    endmethod

    static method getRecipesWithIngredient takes integer itemTypeId returns ItemRecipeList
        return getRecipes(itemTypeId)
    endmethod

    static method getRecipesWithAbility takes integer abilityId returns ItemRecipeList
        return getRecipes(abilityId)
    endmethod

    static method create takes integer reward, integer charges, boolean ordered, boolean permanent, boolean pickupable returns thistype
        local thistype this = allocate()
        local ItemRecipeList recipes

        set ingredients = RecipeIngredientList.create()
        set this.reward = reward
        set this.charges = charges
        set this.ordered = ordered
        set this.permanent = permanent
        set this.pickupable = pickupable
static if LIBRARY_ItemRestriction then
        set requirement = 0
endif
        set abilityId = 0
        set count = 0
        set batch = false
        call saveRecipe(-reward, this)

        return this
    endmethod

    method getAbility takes nothing returns integer
        return abilityId
    endmethod

    method setAbility takes integer abilityId returns thistype
        if this.abilityId != abilityId then
            if this.abilityId != 0 then
                call flushRecipe(abilityId, this)
            endif
            if abilityId > 0 then
                set this.abilityId = abilityId
                call saveRecipe(abilityId, this)
            endif
        endif

        return this
    endmethod

    method destroy takes nothing returns nothing
        local RecipeIngredientListItem iter = ingredients.first
        local ItemRecipeList recipes
        local integer itemTypeId

        call setAbility(0)
        loop
            exitwhen iter == 0
            call flushRecipe(iter.data.itemTypeId, this)
            set iter = iter.next
        endloop
        call flushRecipe(-reward, this)

        call ingredients.destroy()
        call deallocate()
    endmethod

    static method operator [] takes thistype other returns thistype
        local thistype this = create(other.reward, other.charges, other.ordered, other.permanent, other.pickupable)
        local RecipeIngredientListItem iter = other.ingredients.first

        loop
            exitwhen iter == 0
            call this.ingredients.push(RecipeIngredient[iter.data])
            set iter = iter.next
        endloop

        set this.count = other.count
        call this.setAbility(other.abilityId)
        set this.batch = other.batch
        return this
    endmethod

    method getIngredients takes nothing returns RecipeIngredientList
        return RecipeIngredientList[ingredients]
    endmethod

    method isIngredient takes integer itemTypeId returns boolean
        local RecipeIngredientListItem iter = ingredients.first

        loop
            exitwhen iter == 0
            if iter.data.itemTypeId == itemTypeId then
                return true
            endif
            set iter = iter.next
        endloop
        return false
    endmethod

    method startBatch takes nothing returns thistype
        if not batch then
            set batch = true
        debug else
            debug call DisplayTimedTextFromPlayer(GetLocalPlayer(),0,0,60,"ItemRecipe::startBatch failed. Batch is already started.")
        endif
        return this
    endmethod

    method endBatch takes nothing returns thistype
        if batch then
            set batch = false
            set count = count + 1
        debug else
            debug call DisplayTimedTextFromPlayer(GetLocalPlayer(),0,0,60,"ItemRecipe::endBatch failed. No batch has been started.")
        endif
        return this
    endmethod

    method removeItem takes integer itemTypeId returns thistype
        local RecipeIngredientListItem iter = ingredients.first
        local boolean found = false
        local RecipeIngredient ingredient

        if batch then // removing item when batch is ongoing is forbidden
            return this
        endif
        loop
            exitwhen iter == 0
            if iter.data.itemTypeId == itemTypeId then
                set ingredient = iter.data

                // Decrement count only if this item is not part of any batch
                if (iter.prev == 0 or iter.prev.data.index != ingredient.index) and /*
                */ (iter.next == 0 or iter.next.data.index != ingredient.index) then
                    set count = count - 1
                endif

                call ingredients.erase(iter)
                set found = true
            endif
            set iter = iter.next
        endloop

        if found then
            call flushRecipe(itemTypeId, this)
        endif
        return this
    endmethod

    method addItem takes integer itemTypeId, boolean perishable, integer charges returns thistype
        local RecipeIngredient ingredient

        if itemTypeId != reward then
            set ingredient = RecipeIngredient.create(itemTypeId, perishable, IMaxBJ(charges, 0), count)
            call ingredients.push(ingredient)
            if not batch then
                set count = count + 1
            endif
            call saveRecipe(itemTypeId, this)
        endif

        return this
    endmethod

    method addItemEx takes integer itemTypeId returns thistype
        return addItem(itemTypeId, true, 0)
    endmethod

    private method orderedSearch takes ItemVector items returns RecipeIngredientVector
        local integer slot = 0
        local boolean found
        local item itm
        local integer charges
        local RecipeIngredient ingredient
        local integer idx
        local RecipeIngredientVector result = RecipeIngredientVector.create()
        local RecipeIngredientListItem iter = ingredients.first
        call result.assign(items.size(), 0)

        loop
            exitwhen iter == 0 // test() validated this.count against items size already
            set found = false
            set itm = items[slot]
            set charges = GetItemCharges(itm)
            set ingredient = iter.data
            set idx = ingredient.index

            loop
                exitwhen ingredient.index != idx // treats each part of recipe as possible batch
                if GetItemTypeId(itm) == ingredient.itemTypeId and charges >= ingredient.charges then
                    set result[slot] = RecipeIngredient[ingredient]
                    set found = true
                    exitwhen true
                endif

                set iter = iter.next
                exitwhen iter == 0
                set ingredient = iter.data
            endloop

            if not found then
                call result.destroy()
                set result = 0
                exitwhen true
            endif
            // Seek node which is not part of this batch
            loop
                set iter = iter.next
                exitwhen iter == 0 or iter.data.index != idx
            endloop
            set slot = slot + 1
        endloop

        set itm = null
        return result
    endmethod

    private method unorderedSearch takes ItemVector items returns RecipeIngredientVector
        local boolean found
        local integer idx
        local integer slot = 0
        local integer size = items.size()
        local item itm
        local integer itemTypeId
        local integer charges
        local RecipeIngredient ingredient
        local RecipeIngredientVector result = RecipeIngredientVector.create()
        local RecipeIngredientListItem iter = ingredients.first
        local RecipeIngredientListItem head
        call result.assign(size, 0)

        loop
            exitwhen iter == 0
            set found = false
            set idx = iter.data.index
            set head = iter // save head of current batch
            set slot = 0

            loop
                exitwhen slot >= size
                // Attempt to find any matching items from given batch within items collection
                if result[slot] == 0 then
                    set itm = items[slot]
                    set itemTypeId = GetItemTypeId(itm)
                    set charges = GetItemCharges(itm)
                    set ingredient = iter.data

                    loop
                        exitwhen ingredient.index != idx
                        if GetItemTypeId(itm) == ingredient.itemTypeId and charges >= ingredient.charges then
                            set result[slot] = RecipeIngredient[ingredient]
                            set found = true
                            exitwhen true
                        endif

                        set iter = iter.next
                        exitwhen iter == 0
                        set ingredient = iter.data
                    endloop

                    exitwhen found
                    set iter = head
                endif
                set slot = slot + 1
            endloop

            if not found then
                call result.destroy()
                set result = 0
                exitwhen true
            endif
            // Seek node which is not part of this batch
            loop
                set iter = iter.next
                exitwhen iter == 0 or iter.data.index != idx
            endloop
        endloop

        set itm = null
        return result
    endmethod

    method test takes unit whichUnit, ItemVector items returns RecipeIngredientVector
        if items == 0 or items.size() < count then
            return 0
        endif
static if LIBRARY_ItemRestriction then
        if requirement != 0 and not requirement.filter(whichUnit) then
            return 0
        endif
endif

        if ordered then
            return orderedSearch(items)
        endif
        return unorderedSearch(items)
    endmethod

    method testEx takes unit whichUnit returns RecipeIngredientVector
        local ItemVector items = GetUnitItemVector(whichUnit)
        local RecipeIngredientVector result = test(whichUnit, items)
        call items.destroy()
        return result
    endmethod

    method assemble takes unit whichUnit, ItemVector fromItems returns boolean
        local integer i = 0
        local integer size = fromItems.size()
        local item rewardItm
        local item itm
        local integer chrgs
        local RecipeIngredient ingredient
        local RecipeIngredientVector fromIngredients = test(whichUnit, fromItems)
        local ItemVector usedItems

        if fromIngredients == 0 then
            return false
        endif
        set rewardItm = CreateItem(reward, GetUnitX(whichUnit), GetUnitY(whichUnit))
        if charges > 0 then
            call SetItemCharges(rewardItm, charges)
        endif

        set usedItems = ItemVector.create()
        loop
            exitwhen i >= size
            if fromIngredients[i] != 0 then
                call usedItems.push(fromItems[i])
            endif
            set i = i + 1
        endloop

        call FireEvent(this, whichUnit, rewardItm, usedItems)

        set i = 0
        loop
            exitwhen i >= size
            set ingredient = fromIngredients[i]
            if ingredient != 0 then
                set itm = fromItems[i]

                if ingredient.charges > 0 then
                    set chrgs = GetItemCharges(itm)
                    if chrgs > ingredient.charges and not ingredient.perishable then
                        call SetItemCharges(itm, chrgs - ingredient.charges)
                    else
                        call RemoveItem(itm)
                    endif
                elseif ingredient.perishable then
                    call RemoveItem(itm)
                endif
            endif

            set i = i + 1
        endloop
        call UnitAddItem(whichUnit, rewardItm)

        call fromIngredients.destroy()
        call usedItems.destroy()
        set rewardItm = null
        set itm = null
        return true
    endmethod

    method assembleEx takes unit whichUnit returns boolean
        local ItemVector items = GetUnitItemVector(whichUnit)
        local boolean result = assemble(whichUnit, items)
        call items.destroy()
        return result
    endmethod

    method disassemble takes unit whichUnit returns boolean
        local integer slot = 0
        local integer size = UnitInventorySize(whichUnit)
        local boolean result = false
        local item itm
        local RecipeIngredientListItem iter
        local RecipeIngredient ingredient

        if permanent then
            return false
        endif

        loop
            exitwhen slot >= size
            set itm = UnitItemInSlot(whichUnit, slot)
            if GetItemTypeId(itm) == reward then
                call RemoveItem(itm)

                set iter = ingredients.first
                loop
                    exitwhen iter == 0
                    set ingredient = iter.data
                    if ingredient.perishable then
                        set itm = CreateItem(ingredient.itemTypeId, GetUnitX(whichUnit), GetUnitY(whichUnit))
                        if ingredient.charges > 0 then
                            call SetItemCharges(itm , ingredient.charges)
                        endif
                        call UnitAddItem(whichUnit, itm)
                    endif

                    set iter = iter.next
                endloop

                set result = true
                exitwhen true
            endif
            set slot = slot + 1
        endloop

        set itm = null
        return result
    endmethod
endstruct

function UnitAssembleItem takes unit whichUnit, integer itemTypeId returns boolean
    local ItemRecipeList recipes = ItemRecipe.getRecipesForReward(itemTypeId)
    local ItemRecipeListItem iter

    if recipes != 0 then
        set iter = recipes.first
        loop
            exitwhen iter == 0
            if iter.data.assembleEx(whichUnit) then
                return true
            endif
            set iter = iter.next
        endloop
    endif
    return false
endfunction

function UnitDisassembleItem takes unit whichUnit, item whichItem returns boolean
    local ItemRecipeList recipes

    if UnitHasItem(whichUnit, whichItem) then
        set recipes = ItemRecipe.getRecipesForReward(GetItemTypeId(whichItem))
        // Disassembling item with multiple recipe variants is ambiguous
        if recipes != 0 and recipes.size() == 1 then
            return recipes.front().disassemble(whichUnit)
        endif
    endif
    return false
endfunction

private function OnPickup takes nothing returns nothing
    local unit u
    local integer itemTypeId = GetItemTypeId(GetManipulatedItem())
    local ItemRecipeList recipes = ItemRecipe.getRecipesWithIngredient(itemTypeId)
    local ItemRecipeListItem iter
    local ItemRecipe recipe

    if recipes != 0 then
        set u = GetTriggerUnit()
        set iter = recipes.first

        loop
            exitwhen iter == 0
            set recipe = iter.data
            if recipe.pickupable and recipe.assembleEx(u) then
                exitwhen true
            endif
            set iter = iter.next
        endloop
        set u = null
    endif
endfunction

static if LIBRARY_InventoryEvent then
private function OnMoved takes nothing returns nothing
    local unit u
    local item itm = GetInventoryManipulatedItem()
    local ItemRecipeList recipes = ItemRecipe.getRecipesWithIngredient(GetItemTypeId(itm))
    local ItemRecipeListItem iter
    local ItemRecipe recipe
    local ItemVector items

    if recipes != 0 then
        set u = GetInventoryManipulatingUnit()
        set items = GetUnitItemVector(u)
        set items[GetInventorySlotFrom()] = GetInventorySwappedItem()
        set items[GetInventorySlotTo()] = itm

        set iter = recipes.first
        loop
            exitwhen iter == 0
            set recipe = iter.data
            if recipe.pickupable and recipe.assemble(u, items) then
                exitwhen true
            endif
            set iter = iter.next
        endloop

        call items.destroy()
        set u = null
    endif
    set itm = null
endfunction
endif

private function OnCast takes nothing returns nothing
    local unit u
    local ItemRecipeList recipes = ItemRecipe.getRecipesWithAbility(GetSpellAbilityId())
    local ItemRecipeListItem iter

    if recipes != 0 then
        set u = GetTriggerUnit()
        set iter = recipes.first

        loop
            exitwhen iter == 0
            if iter.data.assembleEx(u) then
                exitwhen true
            endif
            set iter = iter.next
        endloop
        set u = null
    endif
endfunction

static if LIBRARY_SmoothItemPickup then
private function GetCheatRecipe takes unit u, item itm returns ItemRecipe
    local ItemRecipeList recipes = ItemRecipe.getRecipesWithIngredient(GetItemTypeId(itm))
    local ItemRecipeListItem iter
    local ItemRecipe recipe
    local ItemRecipe result = 0
    local ItemVector items
    local RecipeIngredientList ingredients
    local RecipeIngredientListItem ingrIter

    if recipes == 0 then
        return 0
    endif

    set items = GetUnitItemVector(u).push(itm)
    set iter = recipes.first
    loop
        exitwhen iter == 0 or result != 0
        set recipe = iter.data

        if recipe.pickupable and not recipe.ordered then
            set ingredients = recipe.getIngredients()
            set ingrIter = ingredients.first
            loop
                exitwhen ingrIter == 0
                // At least one item has to removed, in order to fit recipe reward in
                if ingrIter.data.perishable and recipe.test(u, items) != 0 then
                    set result = recipe
                    exitwhen true
                endif
                set ingrIter = ingrIter.next
            endloop
        endif

        set iter = iter.next
    endloop

    call items.destroy()
    return result
endfunction

private function OnSmoothPickup takes nothing returns nothing
    local unit u = GetSmoothItemPickupUnit()
    local item itm = GetSmoothItemPickupItem()
    local ItemRecipe recipe = GetCheatRecipe(u, itm)
    local ItemVector items

    if recipe != 0 then
        set items = GetUnitItemVector(u).push(itm)
        call recipe.assemble(u, items)
        call items.destroy()
    endif

    set u = null
    set itm = null
endfunction

private struct SmoothRecipeAssembly extends array
    static method canPickup takes unit whichUnit, item whichItem returns boolean
        return GetCheatRecipe(whichUnit, whichItem) != 0
    endmethod

    implement optional SmoothPickupPredicateModule
endstruct
endif

private module Init
    private static method onInit takes nothing returns nothing
        set EVENT_ITEM_RECIPE_ASSEMBLE = CreateNativeEvent()
        set instanceTable = Table.create()

        call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_PICKUP_ITEM, function OnPickup)
        call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_SPELL_EFFECT, function OnCast)
static if LIBRARY_InventoryEvent then
        call RegisterNativeEvent(EVENT_ITEM_INVENTORY_MOVE, function OnMoved)
endif
static if LIBRARY_SmoothItemPickup then
        // Allow for smooth pickup for pickup-type unordered recipes
        call RegisterNativeEvent(EVENT_ITEM_SMOOTH_PICKUP, function OnSmoothPickup)
        call AddSmoothItemPickupCondition(SmoothRecipeAssembly.create())
endif
    endmethod
endmodule

private struct StructInit extends array
    implement Init
endstruct

endlibrary
Demo code:
JASS:
scope ItemRecipeDemo initializer Init

native UnitAlive takes unit whichUnit returns boolean

struct NoTrollsAlive extends array
    static method filter takes nothing returns boolean
        return GetUnitTypeId(GetFilterUnit()) == 'ndtb' and UnitAlive(GetFilterUnit()) // Dark Troll Berserker
    endmethod

    static method isMet takes unit whichUnit returns string
        local group g = CreateGroup()
        local string result = null

        call GroupEnumUnitsInRect(g, bj_mapInitialPlayableArea, Filter(function thistype.filter))
        if FirstOfGroup(g) != null then
            set result = "There are still some trolls alive on the map. Kill them first."
        endif

        call DestroyGroup(g)
        set g = null
        return result
    endmethod

    implement UnitRequirementPredicateModule
endstruct

globals
    string description = "1) Blood Key + Ghost Key = Magic Chain Key (ordered, onpickup)\n" + /*
    */ "2) Any key +  Cheese (catalyst) = Mechanical Critter (unordered, onpickup)\n" + /*
    */ "3) Helm of Valor + Hood of Cunning + Medallion of Courage = Crown of Kings\n" + /*
    */ "                (ordered, onpickup, special condition: no trolls alive)\n" + /*
    */ "4) All four Orbs = Shadow Orb (unordered, assembled only via Thunder Clap)\n" + /*
    */ "5) All Wands + Glyph (catalyst) + Empty Vial = Full Vial (unordered, onpickup)\n" + /*
    */ "\nYou can order hero to assemble unordered recipes despite their inventory being full." + /*
    */ " Inventory manipulation e.g. item double click can trigger ordered recipe assembly."
endglobals

private function Init takes nothing returns nothing
    local ItemRecipe magicKeyChain
    local ItemRecipe critter
    local ItemRecipe crown
    local ItemRecipe shadowOrb
    local ItemRecipe fullVial

    local UnitRequirement noTrollsAlive = UnitRequirement.create("No trolls alive")
    call noTrollsAlive.addCondition(NoTrollsAlive.create())

    set magicKeyChain = ItemRecipe.create('mgtk', 0, false, true, true)
    call magicKeyChain.addItemEx('kybl')
    call magicKeyChain.addItemEx('kygh')

    set critter = ItemRecipe.create('mcri', 2, false, true, true)
    call critter.startBatch() /*
      */ .addItemEx('kybl') /*
      */ .addItemEx('kygh') /*
      */ .addItemEx('kymn') /*
      */ .addItemEx('kysn')
    call critter.endBatch()
    call critter.addItem('ches', false, 0) // Cheese is just a catalyst

    set crown = ItemRecipe.create('ckng', 0, true, false, true)
    call crown.addItemEx('hval')
    call crown.addItemEx('hcun')
    call crown.addItemEx('mcou')
    set crown.requirement = noTrollsAlive

    set shadowOrb = ItemRecipe.create('sora', 0, false, true, false)
    call shadowOrb.addItemEx('oli2')
    call shadowOrb.addItemEx('ofir')
    call shadowOrb.addItemEx('oven')
    call shadowOrb.addItemEx('ocor')
    call shadowOrb.setAbility('AHtc')

    set fullVial = ItemRecipe.create('bzbf', 0, false, true, true)
    call fullVial.addItemEx('bzbe')
    call fullVial.addItemEx('wlsd')
    call fullVial.addItemEx('woms')
    call fullVial.addItemEx('wshs')
    call fullVial.addItemEx('wcyc')
    call fullVial.addItem('gopr', false, 0) // Glyph of Purification is just a catalyst

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

endscope
ItemRecipe (Wurst):
Wurst:
/*
*  ItemRecipe v1.0.3.7
*     by Bannar
*
*  Powerful item recipe creator.
*/
package ItemRecipe
import HashMap
import HashList
import LinkedList
import RegisterEvents
import ItemRestriction
import InventoryEvent
import SmoothItemPickup

tuple eventInfo(ItemRecipe recipe, unit u, item itm, LinkedList<item> ingredients)

var eventState = eventInfo(null, null, null, null)
constant eventTrigger = CreateTrigger()

/** Returns triggering item recipe. */
public function getEventItemRecipe() returns ItemRecipe
    return eventState.recipe

/** Returns recipe triggering unit. */
public function getEventItemRecipeUnit() returns unit
    return eventState.u

/** Returns reward item for triggering recipe. */
public function getEventItemRecipeItem() returns item
    return eventState.itm

/** Returns collection of ingredients chosen to assemble the reward item,
    where each index corresponds to triggering unit inventory slot. */
public function getEventItemRecipeIngredients() returns LinkedList<item>
    return eventState.ingredients

/** Registers new event handler for item recipe event. */
public function registerItemRecipeEvent(code func)
    eventTrigger.addCondition(Condition(func))

/** Returns trigger handle associated with recipe event. */
public function getItemRecipeEventTrigger() returns trigger
    return eventTrigger

function unit.getItemList() returns LinkedList<item>
    var last = this.inventorySize() - 1
    var items = new LinkedList<item>()

    for slot = 0 to last
        items.push(this.itemInSlot(slot))
    return items

function fireEvent(eventInfo currState)
    var prevState = eventState
    eventState = currState
    eventTrigger.evaluate()
    eventState = prevState

/** Stores information about item required for recipe assembly. */
public class RecipeIngredient
    int itemTypeId
    boolean perishable
    int charges
    // If non 0, then it is part of a batch i.e multiple items can fill its spot
    int index

    construct(int itemTypeId, boolean perishable, int charges, int index)
        this.itemTypeId = itemTypeId
        this.perishable = perishable
        this.charges = charges
        this.index = index

    construct(thistype base)
        this.itemTypeId = base.itemTypeId
        this.perishable = base.perishable
        this.charges = base.charges
        this.index = base.index

/** Item recipe, collection of items that can be combined for powerful rewards. */
public class ItemRecipe
    int charges
    boolean ordered
    boolean permanent
    boolean pickupable
    UnitRequirement requirement = null

    private constant ingredients = new LinkedList<RecipeIngredient>()
    private int reward
    private var abilityId = 0
    private var count = 0
    private var batch = false
    // Global containers for associated recipe list retrieval
    private static constant instances = new HashMap<int, LinkedList<ItemRecipe>>()

    private static function saveRecipe(int index, ItemRecipe recipe)
        if not instances.has(index)
            instances.put(index, new LinkedList<ItemRecipe>())
        var recipes = instances.get(index)
        if not recipes.contains(recipe)
            recipes.push(recipe)

    private static function flushRecipe(int index, ItemRecipe recipe)
        var recipes = instances.get(index)
        recipes.remove(recipe)
        if recipes.isEmpty()
            destroy recipes
            instances.remove(index)

    private static function getRecipes(int index) returns LinkedList<ItemRecipe>
        LinkedList<ItemRecipe> result = null
        if instances.has(index)
            result = instances.get(index)
        return result

    /** Returns recipes which reward matches specified item type. */
    static function getRecipesForReward(int itemTypeId) returns LinkedList<ItemRecipe>
        return getRecipes(-itemTypeId)

    /** Returns recipes that specified item is part of. */
    static function getRecipesWithIngredient(int itemTypeId) returns LinkedList<ItemRecipe>
        return getRecipes(itemTypeId)

    /** Returns recipes that can be assembled by casting specified ability. */
    static function getRecipesWithAbility(int abilityId) returns LinkedList<ItemRecipe>
        return getRecipes(abilityId)

    construct(int reward, int charges, boolean ordered, boolean permanent, boolean pickupable)
        this.reward = reward
        this.charges = charges
        this.ordered = ordered
        this.permanent = permanent
        this.pickupable = pickupable
        saveRecipe(-reward, this)

    ondestroy
        setAbility(0)
        ingredients.forEach(e -> flushRecipe(e.itemTypeId, this))
        flushRecipe(-reward, this)
        destroy ingredients

    /** Returns reward item type. */
    function getReward() returns int
        return reward

    /** Number of items required for the recipe. */
    function count() returns int
        return count

    /** Returns shallow copy of item recipe data. */
    function getIngredients() returns LinkedList<RecipeIngredient>
        return ingredients.copy()

    /** Whether specified item type is a part of the recipe. */
    function isIngredient(int itemTypeId) returns boolean
        var result = false
        for ingredient from ingredients.staticItr()
            if ingredient.itemTypeId == itemTypeId
                result = true
                break
        return result

    /** Retrieves id of ability thats triggers assembly of this recipe. */
    function getAbility() returns int
        return abilityId

    /** Sets or removes specified ability from triggering recipe assembly. */
    function setAbility(int abilityId)
        if this.abilityId != abilityId
            if this.abilityId != 0
                flushRecipe(abilityId, this)
            if abilityId > 0
                this.abilityId = abilityId
                saveRecipe(abilityId, this)

    /** Starts single-reference counted batch. Allows to assign multiple items to the same item slot. */
    function startBatch()
        if not batch
            batch = true

    /** Closes current batch. */
    function endBatch()
        if batch
            batch = false
            count++

    /** Removes all entries that match specified item type from recipe ingredient list. */
    function removeItem(int itemTypeId)
        if not batch // removing item when batch is ongoing is forbidden
            var iter = ingredients.iterator()
            var found = false

            for ingredient from iter
                if ingredient.itemTypeId == itemTypeId
                    var entry = iter.current
                    // Decrement count only if this item is not part of any batch
                    if (entry.prev == iter.dummy or entry.prev.elem.index != ingredient.index) and
                       (entry.next == iter.dummy or entry.next.elem.index != ingredient.index)
                        count--
                    iter.remove()
                    found = true
            iter.close()
            if found
                flushRecipe(itemTypeId, this)

    /** Adds new entry to recipe ingredient list. */
    function addItem(int itemTypeId, boolean perishable, int charges)
        if itemTypeId != reward
            var ingredient = new RecipeIngredient(itemTypeId, perishable, max(charges, 0), count)
            ingredients.push(ingredient)
            if not batch
                count++
            saveRecipe(itemTypeId, this)

    /** Adds new entry to recipe ingredient list. */
    function addItem(int itemTypeId)
        addItem(itemTypeId, true, 0)

    private function orderedSearch(LinkedList<item> items) returns HashList<RecipeIngredient>
        var result = new HashList<RecipeIngredient>
        var iter = ingredients.iterator()
        var slot = 0

        while iter.hasNext() // test() validated this.count against items size already
            var found = false
            var ingredient = iter.next()
            var idx = ingredient.index
            var itm = items.get(slot)
            var charges = itm.getCharges()

            while ingredient.index == idx // treats each part of recipe as possible batch
                if itm.getTypeId() == ingredient.itemTypeId and charges >= ingredient.charges
                    result.set(slot, new RecipeIngredient(ingredient))
                    found = true
                    break
                if not iter.hasNext()
                    break
                ingredient = iter.next()

            if not found
                destroy result
                result = null
                break
            // Seek node which is not part of this batch
            while iter.hasNext()
                if iter.lookahead().index != idx
                    break
                iter.next()
            slot++
        iter.close()
        return result

    private function unorderedSearch(LinkedList<item> items) returns HashList<RecipeIngredient>
        var result = new HashList<RecipeIngredient>()
        var iter = ingredients.iterator()
        var last = items.size() - 1

        while iter.hasNext()
            var found = false
            var idx = iter.next().index
            var head = iter.current // save head of current batch

            for slot = 0 to last
                // Attempt to find any matching item from given batch within items collection
                if result.get(slot) == null
                    var itm = items.get(slot)
                    var itemTypeId = itm.getTypeId()
                    var charges = itm.getCharges()
                    var ingredient = head.elem

                    while ingredient.index == idx
                        if itemTypeId == ingredient.itemTypeId and charges >= ingredient.charges
                            result.set(slot, new RecipeIngredient(ingredient))
                            found = true
                            break
                        if not iter.hasNext()
                            break
                        ingredient = iter.next()
                    if found
                        break
                    iter.current = head

            if not found
                destroy result
                result = null
                break
            // Seek node which is not part of this batch
            while iter.hasNext()
                if iter.lookahead().index != idx
                    break
                iter.next()
        iter.close()
        return result

    /** Checks if recipe can be assembled for specified unit given the ingredients list. */
    function test(unit whichUnit, LinkedList<item> items) returns HashList<RecipeIngredient>
        HashList<RecipeIngredient> result = null

        if items != null and items.size() >= count
            if requirement == null or requirement.filter(whichUnit)
                if ordered
                    result = orderedSearch(items)
                else
                    result = unorderedSearch(items)
        return result

    /** Checks if recipe can be assembled for specified unit. */
    function test(unit whichUnit) returns HashList<RecipeIngredient>
        var items = whichUnit.getItemList()
        var result = test(whichUnit, items)
        destroy items
        return result

    /** Attempts to assemble recipe for specified unit given the ingredients list. */
    function assemble(unit whichUnit, LinkedList<item> fromItems) returns boolean
        var fromIngredients = test(whichUnit, fromItems)
        if fromIngredients == null
            return false

        var rewardItm = createItem(reward, whichUnit.getPos())
        if charges > 0
            rewardItm.setCharges(charges)

        var usedItems = new LinkedList<item>()
        var last = fromItems.size() - 1
        for i = 0 to last
            if fromIngredients.get(i) != null
                usedItems.push(fromItems.get(i))

        fireEvent(eventInfo(this, whichUnit, rewardItm, usedItems))

        for i = 0 to last
            var ingredient = fromIngredients.get(i)
            if ingredient != null
                var itm = fromItems.get(i)
                if ingredient.charges > 0
                    if itm.getCharges() > ingredient.charges and not ingredient.perishable
                        itm.setCharges(itm.getCharges() - ingredient.charges)
                    else
                        itm.remove()
                else if ingredient.perishable
                    itm.remove()
        whichUnit.addItemHandle(rewardItm)

        destroy fromIngredients
        destroy usedItems
        return true

    /** Attempts to assemble recipe for specified unit. */
    function assemble(unit whichUnit) returns boolean
        var items = whichUnit.getItemList()
        var result = assemble(whichUnit, items)
        destroy items
        return result

    /** Reverts the assembly, removing the reward item and returning all ingredients to specified unit. */
    function disassemble(unit whichUnit) returns boolean      
        if permanent
            return false
        var last = whichUnit.inventorySize() - 1
        var result = false

        for slot = 0 to last
            var itm = whichUnit.itemInSlot(slot)
            if itm.getTypeId() == reward
                itm.remove()

                for ingredient in ingredients
                    if ingredient.perishable
                        itm = createItem(ingredient.itemTypeId, whichUnit.getPos())
                        if ingredient.charges > 0
                            itm.setCharges(ingredient.charges)
                        whichUnit.addItemHandle(itm)
                result = true
                break
        return result

/** Attempts to assemble specified item type for provided unit. */
public function unit.assembleItem(int itemTypeId) returns boolean
    var result = false
    var recipes = ItemRecipe.getRecipesForReward(itemTypeId)
    if recipes != null
        for recipe in recipes
            if recipe.assemble(this)
                result = true
                break
    return result

/** Reverts the assembly, removing the reward item and returning all ingredients to specified unit. */
public function unit.disassembleItem(item whichItem) returns boolean
    var result = false
    if this.hasItem(whichItem)
        var recipes = ItemRecipe.getRecipesForReward(whichItem.getTypeId())
        // Disassembling item with multiple recipe variants is ambiguous
        if recipes != null and recipes.size() == 1
            result = recipes.getFirst().disassemble(this)
    return result

function onPickup()
    var itemTypeId = GetManipulatedItem().getTypeId()
    var recipes = ItemRecipe.getRecipesWithIngredient(itemTypeId)

    if recipes != null
        var u = GetTriggerUnit()
        for recipe in recipes
            if recipe.pickupable and recipe.assemble(u)
                break

function onMoved()
    var itm = getInventoryManipulatedItem()
    var recipes = ItemRecipe.getRecipesWithIngredient(itm.getTypeId())

    if recipes != null
        var u = getInventoryManipulatingUnit()
        var items = u.getItemList()
        items.set(getInventorySlotFrom(), getInventorySwappedItem())
        items.set(getInventorySlotTo(), itm)

        for recipe in recipes
            if recipe.pickupable and recipe.assemble(u, items)
                break
        destroy items

function onCast()
    var recipes = ItemRecipe.getRecipesWithAbility(GetSpellAbilityId())
    if recipes != null
        var u = GetTriggerUnit()
        for recipe in recipes
            if recipe.assemble(u)
                break

function getCheatRecipe(unit u, item itm) returns ItemRecipe
    var recipes = ItemRecipe.getRecipesWithIngredient(itm.getTypeId())
    if recipes == null
        return null

    ItemRecipe result = null
    var items = u.getItemList()
    items.push(itm)
    var iter = recipes.iterator()

    while iter.hasNext() and result == null
        var recipe = iter.next()
        if recipe.pickupable and not recipe.ordered
            for ingredient in recipe.getIngredients()
                // At least one item has to removed, in order to fit recipe reward in
                if ingredient.perishable and recipe.test(u, items) != null
                    result = recipe
                    break
    iter.close()
    destroy items
    return result

function onSmoothPickup()
    var u = getSmoothItemPickupUnit()
    var itm = getSmoothItemPickupItem()
    var recipe = getCheatRecipe(u, itm)

    if recipe != null
        var items = u.getItemList()
        items.push(itm)
        recipe.assemble(u, items)
        destroy items

class SmoothRecipeAssembly implements SmoothPickupPredicate
    function canPickup(unit whichUnit, item whichItem) returns boolean
        return getCheatRecipe(whichUnit, whichItem) != null

init
    registerPlayerUnitEvent(EVENT_PLAYER_UNIT_PICKUP_ITEM, () -> onPickup())
    registerPlayerUnitEvent(EVENT_PLAYER_UNIT_SPELL_EFFECT, () -> onCast())
    registerInventoryEvent(EVENT_ITEM_INVENTORY.MOVE, () -> onMoved())

    // Allow for smooth pickup for pickup-type unordered recipes
    registerSmoothItemPickupEvent(() -> onSmoothPickup())
    addSmoothItemPickupCondition(new SmoothRecipeAssembly())
Demo code:
Wurst:
package ItemRecipeDemo
import ItemRecipe
import ItemRestriction

class NoTrollsAlive implements UnitRequirementPredicate
    static function filter() returns boolean
        return GetFilterUnit().getTypeId() == 'ndtb' and GetFilterUnit().isAlive() // Spitting Spider

    function isMet(unit _whichUnit) returns string
        var g = CreateGroup()
        string result = null

        g.enumUnitsInRect(bj_mapInitialPlayableArea, Filter(function filter))
        if g.hasNext()
            result = "There are still some trolls alive on the map. Kill them first."

        g.destr()
        return result

string description = "1) Blood Key + Ghost Key = Magic Chain Key (ordered, onpickup)\n" +
    "2) Any key +  Cheese (catalyst) = Mechanical Critter (unordered, onpickup)\n" +
    "3) Helm of Valor + Hood of Cunning + Medallion of Courage = Crown of Kings\n" +
    "                (ordered, onpickup, special condition: no trolls alive)\n" +
    "4) All four Orbs = Shadow Orb (unordered, assembled only via Thunder Clap)\n" +
    "5) All Wands + Glyph (catalyst) + Empty Vial = Full Vial (unordered, onpickup)\n" +
    "\nYou can order hero to assemble unordered recipes despite their inventory being full." +
    " Inventory manipulation e.g. item double click can trigger ordered recipe assembly."

function createAllItems()
    CreateItem('bzbe', 134.4, - 69.5)
    CreateItem('ches', - 243.8, 268.7)
    CreateItem('ches', - 358.4, 273.3)
    CreateItem('ches', - 101.7, 273.9)
    CreateItem('gopr', 131.9, 36.3)
    CreateItem('hcun', - 885.0, 132.7)
    CreateItem('hcun', - 883.7, 4.3)
    CreateItem('hval', - 750.3, 12.5)
    CreateItem('hval', - 753.3, 145.7)
    CreateItem('kybl', - 307.5, 496.9)
    CreateItem('kybl', - 305.3, 384.8)
    CreateItem('kygh', - 169.4, 372.5)
    CreateItem('kygh', - 167.6, 506.7)
    CreateItem('kymn', - 435.0, 387.8)
    CreateItem('kymn', - 436.4, 497.3)
    CreateItem('kysn', - 30.2, 496.8)
    CreateItem('kysn', - 37.3, 371.8)
    CreateItem('mcou', - 1003.8, 131.5)
    CreateItem('mcou', - 1008.5, - 4.4)
    CreateItem('ocor', - 483.0, - 93.2)
    CreateItem('ofir', - 375.6, - 83.2)
    CreateItem('oli2', - 488.8, 11.0)
    CreateItem('oven', - 374.0, 21.0)
    CreateItem('wcyc', - 115.4, 56.1)
    CreateItem('wlsd', - 112.1, - 71.3)
    CreateItem('woms', 12.4, - 80.6)
    CreateItem('wshs', 13.3, 46.5)

public function initItemRecipeDemo()
    createAllItems()

    var noTrollsAlive = new UnitRequirement("No trolls alive")
    noTrollsAlive.addCondition(new NoTrollsAlive())

    var magicKeyChain = new ItemRecipe('mgtk', 0, false, true, true)
    magicKeyChain.addItem('kybl')
    magicKeyChain.addItem('kygh')

    var critter = new ItemRecipe('mcri', 2, false, true, true)
    critter..startBatch()
        ..addItem('kybl')
        ..addItem('kygh')
        ..addItem('kymn')
        ..addItem('kysn')
    critter.endBatch()
    critter.addItem('ches', false, 0) // Cheese is just a catalyst

    var crown = new ItemRecipe('ckng', 0, true, false, true)
    crown.addItem('hval')
    crown.addItem('hcun')
    crown.addItem('mcou')
    crown.requirement = noTrollsAlive

    var shadowOrb = new ItemRecipe('sora', 0, false, true, false)
    shadowOrb.addItem('oli2')
    shadowOrb.addItem('ofir')
    shadowOrb.addItem('oven')
    shadowOrb.addItem('ocor')
    shadowOrb.setAbility('AHtc')

    var fullVial = new ItemRecipe('bzbf', 0, false, true, true)
    fullVial.addItem('bzbe')
    fullVial.addItem('wlsd')
    fullVial.addItem('woms')
    fullVial.addItem('wshs')
    fullVial.addItem('wcyc')
    fullVial.addItem('gopr', false, 0) // Glyph of Purification is just a catalyst

    printTimed(description, 45)
Contents

Test Map (Map)

Reviews
MyPad
EDIT: Removed Quote. Remarks: I must agree with @Chaosy here. This is complete overkill, but a good kind of overkill. (Little to no need for an update in features, that is, due to anticipatory approach in coding) I cannot help but try to approve...
Level 18
Joined
Oct 17, 2012
Messages
818
What about other arbitrary requirements, such as a specific region a unit must enter or stand in for each recipe? Why not let the user define special requirements for each recipe or for the system as a whole?
 

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
What about other arbitrary requirements, such as a specific region a unit must enter or stand in for each recipe? Why not let the user define special requirements for each recipe or for the system as a whole?
Have you read the documentation? Did you check the demo code? Because what you are asking about is implemented in the system.
You are provided with UnitRequirementPredicate interface (Wurst) or UnitRequirementPredicateMdoule (vJass). Checkout the demo map, in there I posted example of usage of such:
- Crown of Kings can only be assembled if no trolls alive are present on the map.

In your case, recipe assembly could be allowed only if certain units are present or absent in given region - simply modify method "isMet". Word "arbitrary" is used here not by mistake, my friend.

Another Recipe System already exists in the Spells database.

What differentiates this from the Recipe System by Chaosy?
Had you even look into the code and documentation?

On hive there is no ItemRecipe that could stand against this one, feature wise. This, together with ItemRestriction, StackNSplit and sneaky SmoothItemPickup enable one to take control over entire item handling in the map. Customizations are in plenty - your imagination is the limit here.
Think about Island Troll Tribes. I'll give you a glimpse.
Boots in ITT require item of type 'hide' in order to construct. However, you are not limited to just a single hide.. it can be Elk, Wolf or Bear hide. Thus "batches" have been introduced. You are permitted to set several items for the exact same spot.
You can declare ordered and unordered recipes - one that do account for inventory item order and others that don't.
You can easily disassemble result items if need be.
Inventory events can and will take part in recipe assembly process, not just item pickup event.
Allows for arbitrary conditions for unit to assembly recipe, e.g.: a mammoth has to be alive in order to assembly specific item.
Assemble recipes despite unit inventory being full.
And so on..

MyPad, you are a Spell Reviewer, and hive reviewers are expected to spend a while validating the system before moving forward.
 

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
This resource has been created in 2014, some for StackNSplit and ItemRestriction - that's why those are not released as 1.0.0.0. I've recently posted them on github, and the changelog is rich.
Real life has its own plans as we all know, thus many devs have not been releasing as much stuff as they would like or had to even quit this hobby. This includes me too : ) For three years I've been out of scope.

Disclaimer: this is not posted for anyone to copy from - in any case, it deprecates previous libs.
This is a community resource, if you have questions or feedback, post it here so we can make better resources together. I'm including everyone who gave their solid input into credits. Library is not posted in Chaosy thread because it's completely different being, much greater and more powerful. Would be a complete overhaul / rewrite of his lib and in fact, would not be his own lib any longer.

Whatmore, it's not an overkill - many libs are declared as optional requirements. When you think of items, there are many things that can come into mind.
ItemRecipe has been enriched in all of these and has been granted a breeze of arbitrariness.
DotA item handling melts at the mere sight of ItemRecipe.
 
Last edited:
Level 13
Joined
Nov 7, 2014
Messages
571
It seems to me that a big chunk of the functionality of this library can be implemented in a dozen or so functions, with 0 dependencies, much much less code and more efficiently.

JASS:
library inv initializer init uses libassert

globals
    private integer array inv
    private integer array slots
    private integer g_s1
    private integer g_s2
    private integer g_s3
    private integer g_s4
    private integer g_s5
    private integer g_s6
endglobals

private function inv_load takes unit u returns nothing
    local integer aa
    local integer a = 0
    local item it
    local integer tmp

    set aa = UnitInventorySize(u)
    loop
        exitwhen a == aa
        set it = UnitItemInSlot(u, a)
        if it == null then
            set inv[a] = 0x7FFFFFFF
        else
            set inv[a] = GetItemTypeId(it)
        endif
        set a = a + 1
    endloop

    loop
        exitwhen a == 6
        set inv[a] = 0x7FFFFFFF
        set a = a + 1
    endloop

    set slots[0] = 0
    set slots[1] = 1
    set slots[2] = 2
    set slots[3] = 3
    set slots[4] = 4
    set slots[5] = 5

    // sort the inv array using a sorting network for N = 6;
    // remember the items' original slots
    //
    //! textmacro SWAP takes a, b
        if inv[$a$] > inv[$b$] then
            set tmp = inv[$a$]
            set inv[$a$] = inv[$b$]
            set inv[$b$] = tmp

            set tmp = slots[$a$]
            set slots[$a$] = slots[$b$]
            set slots[$b$] = tmp
        endif
     //! endtextmacro

    //! runtextmacro SWAP("1", "2")
    //! runtextmacro SWAP("0", "2")
    //! runtextmacro SWAP("0", "1")
    //! runtextmacro SWAP("4", "5")
    //! runtextmacro SWAP("3", "5")
    //! runtextmacro SWAP("3", "4")
    //! runtextmacro SWAP("0", "3")
    //! runtextmacro SWAP("1", "4")
    //! runtextmacro SWAP("2", "5")
    //! runtextmacro SWAP("2", "4")
    //! runtextmacro SWAP("1", "3")
    //! runtextmacro SWAP("2", "3")
endfunction

 //! textmacro MATCH takes n
    set m = false
    loop
        exitwhen a == 6
        if x$n$ == inv[a] then
            set m = true
            set g_s$n$ = slots[a]
            exitwhen true
        endif
        set a = a + 1
    endloop
    if not m then
        return false
    endif
    set a = a + 1
//! endtextmacro

private function inv_test2 takes unit u, integer x1, integer x2, boolean flag returns boolean
    local integer a
    local boolean m

static if DEBUG_MODE then
    call assert("[inv_test2] x1 <= x2", /*
        */ x1 <= x2)
endif

    set a = 0
    //! runtextmacro MATCH("1")
    //! runtextmacro MATCH("2")

    if flag then
        call RemoveItem(UnitItemInSlot(u, g_s1))
        call RemoveItem(UnitItemInSlot(u, g_s2))
    endif
    return true
endfunction
private function inv_match2 takes unit u, integer x1, integer x2 returns boolean
    return inv_test2(u, x1, x2, true)
endfunction
private function inv_accept2 takes unit u, integer x1, integer x2 returns boolean
    return inv_test2(u, x1, x2, false)
endfunction
private function inv_remove_items2 takes unit u returns nothing
    call RemoveItem(UnitItemInSlot(u, g_s1))
    call RemoveItem(UnitItemInSlot(u, g_s2))
endfunction

private function inv_test3 takes unit u, integer x1, integer x2, integer x3, boolean flag returns boolean
    local integer a
    local boolean m

static if DEBUG_MODE then
    call assert("[inv_test3] x1 <= x2 and x2 <= x3", /*
        */ x1 <= x2 and x2 <= x3)
endif

    set a = 0
    //! runtextmacro MATCH("1")
    //! runtextmacro MATCH("2")
    //! runtextmacro MATCH("3")

    if flag then
        call RemoveItem(UnitItemInSlot(u, g_s1))
        call RemoveItem(UnitItemInSlot(u, g_s2))
        call RemoveItem(UnitItemInSlot(u, g_s3))
    endif
    return true
endfunction
private function inv_match3 takes unit u, integer x1, integer x2, integer x3 returns boolean
    return inv_test3(u, x1, x2, x3, true)
endfunction
private function inv_accept3 takes unit u, integer x1, integer x2, integer x3 returns boolean
    return inv_test3(u, x1, x2, x3, false)
endfunction
private function inv_remove_items3 takes unit u returns nothing
    call RemoveItem(UnitItemInSlot(u, g_s1))
    call RemoveItem(UnitItemInSlot(u, g_s2))
    call RemoveItem(UnitItemInSlot(u, g_s3))
endfunction

private function inv_test4 takes unit u, integer x1, integer x2, integer x3, integer x4, boolean flag returns boolean
    local integer a
    local boolean m

static if DEBUG_MODE then
    call assert("[inv_test4] x1 <= x2 and x2 <= x3 and x3 <= x4", /*
        */ x1 <= x2 and x2 <= x3 and x3 <= x4)
endif

    set a = 0
    //! runtextmacro MATCH("1")
    //! runtextmacro MATCH("2")
    //! runtextmacro MATCH("3")
    //! runtextmacro MATCH("4")

    if flag then
        call RemoveItem(UnitItemInSlot(u, g_s1))
        call RemoveItem(UnitItemInSlot(u, g_s2))
        call RemoveItem(UnitItemInSlot(u, g_s3))
        call RemoveItem(UnitItemInSlot(u, g_s4))
    endif
    return true
endfunction
private function inv_match4 takes unit u, integer x1, integer x2, integer x3, integer x4 returns boolean
    return inv_test4(u, x1, x2, x3, x4, true)
endfunction
private function inv_accept4 takes unit u, integer x1, integer x2, integer x3, integer x4 returns boolean
    return inv_test4(u, x1, x2, x3, x4, false)
endfunction
private function inv_remove_items4 takes unit u returns nothing
    call RemoveItem(UnitItemInSlot(u, g_s1))
    call RemoveItem(UnitItemInSlot(u, g_s2))
    call RemoveItem(UnitItemInSlot(u, g_s3))
    call RemoveItem(UnitItemInSlot(u, g_s4))
endfunction

private function inv_test5 takes unit u, integer x1, integer x2, integer x3, integer x4, integer x5, boolean flag returns boolean
    local integer a
    local boolean m

static if DEBUG_MODE then
    call assert("[inv_test5] x1 <= x2 and x2 <= x3 and x3 <= x4 and x4 <= x5", /*
        */ x1 <= x2 and x2 <= x3 and x3 <= x4 and x4 <= x5)
endif

    set a = 0
    //! runtextmacro MATCH("1")
    //! runtextmacro MATCH("2")
    //! runtextmacro MATCH("3")
    //! runtextmacro MATCH("4")
    //! runtextmacro MATCH("5")

    if flag then
        call RemoveItem(UnitItemInSlot(u, g_s1))
        call RemoveItem(UnitItemInSlot(u, g_s2))
        call RemoveItem(UnitItemInSlot(u, g_s3))
        call RemoveItem(UnitItemInSlot(u, g_s4))
        call RemoveItem(UnitItemInSlot(u, g_s5))
    endif
    return true
endfunction
private function inv_match5 takes unit u, integer x1, integer x2, integer x3, integer x4, integer x5 returns boolean
    return inv_test5(u, x1, x2, x3, x4, x5, true)
endfunction
private function inv_accept5 takes unit u, integer x1, integer x2, integer x3, integer x4, integer x5 returns boolean
    return inv_test5(u, x1, x2, x3, x4, x5, false)
endfunction
private function inv_remove_items5 takes unit u returns nothing
    call RemoveItem(UnitItemInSlot(u, g_s1))
    call RemoveItem(UnitItemInSlot(u, g_s2))
    call RemoveItem(UnitItemInSlot(u, g_s3))
    call RemoveItem(UnitItemInSlot(u, g_s4))
    call RemoveItem(UnitItemInSlot(u, g_s5))
endfunction

private function inv_test6 takes unit u, integer x1, integer x2, integer x3, integer x4, integer x5, integer x6, boolean flag returns boolean
    local integer a
    local boolean m

static if DEBUG then
    call assert("[inv_test6] x1 <= x2 and x2 <= x3 and x3 <= x4 and x4 <= x5 and x5 <= x6", /*
        */ x1 <= x2 and x2 <= x3 and x3 <= x4 and x4 <= x5 and x5 <= x6)
endif

    set a = 0
    //! runtextmacro MATCH("1")
    //! runtextmacro MATCH("2")
    //! runtextmacro MATCH("3")
    //! runtextmacro MATCH("4")
    //! runtextmacro MATCH("5")
    //! runtextmacro MATCH("6")

    if flag then
        call RemoveItem(UnitItemInSlot(u, g_s1))
        call RemoveItem(UnitItemInSlot(u, g_s2))
        call RemoveItem(UnitItemInSlot(u, g_s3))
        call RemoveItem(UnitItemInSlot(u, g_s4))
        call RemoveItem(UnitItemInSlot(u, g_s5))
        call RemoveItem(UnitItemInSlot(u, g_s6))
    endif
    return true
endfunction
private function inv_match6 takes unit u, integer x1, integer x2, integer x3, integer x4, integer x5, integer x6 returns boolean
    return inv_test6(u, x1, x2, x3, x4, x5, x6, true)
endfunction
private function inv_accept6 takes unit u, integer x1, integer x2, integer x3, integer x4, integer x5, integer x6 returns boolean
    return inv_test6(u, x1, x2, x3, x4, x5, x6, false)
endfunction
private function inv_remove_items6 takes unit u returns nothing
    call RemoveItem(UnitItemInSlot(u, g_s1))
    call RemoveItem(UnitItemInSlot(u, g_s2))
    call RemoveItem(UnitItemInSlot(u, g_s3))
    call RemoveItem(UnitItemInSlot(u, g_s4))
    call RemoveItem(UnitItemInSlot(u, g_s5))
    call RemoveItem(UnitItemInSlot(u, g_s6))
endfunction

private function inv_remove_slot1 takes unit u returns nothing
    call RemoveItem(UnitItemInSlot(u, g_s1))
endfunction
private function inv_remove_slot2 takes unit u returns nothing
    call RemoveItem(UnitItemInSlot(u, g_s2))
endfunction
private function inv_remove_slot3 takes unit u returns nothing
    call RemoveItem(UnitItemInSlot(u, g_s3))
endfunction
private function inv_remove_slot4 takes unit u returns nothing
    call RemoveItem(UnitItemInSlot(u, g_s4))
endfunction
private function inv_remove_slot5 takes unit u returns nothing
    call RemoveItem(UnitItemInSlot(u, g_s5))
endfunction
private function inv_remove_slot6 takes unit u returns nothing
    call RemoveItem(UnitItemInSlot(u, g_s6))
endfunction


private function unit_add_item_by_id takes unit u, integer item_id returns nothing
    call UnitAddItemById(u, item_id)
endfunction

private function unit_add_item_by_id_charges takes unit u, integer item_id, integer charges returns nothing
    local item it = UnitAddItemById(u, item_id)
    call SetItemCharges(it, charges)
    set it = null
endfunction

native UnitAlive takes unit u returns boolean
globals
    private group g_group = CreateGroup()
endglobals

private function no_trolls_alive takes nothing returns boolean
    local unit u
    local boolean res = true

    call GroupEnumUnitsInRect(g_group, bj_mapInitialPlayableArea, null)
    loop
        set u = FirstOfGroup(g_group)
        exitwhen u == null
        call GroupRemoveUnit(g_group, u)
        if GetUnitTypeId(u) == 'ndtb' and UnitAlive(u) then
            set res = false
            exitwhen true
        endif
    endloop

    call GroupClear(g_group)
    set u = null
    return res
endfunction

private function unit_try_match_recipes takes unit u returns nothing
    call inv_load(u)

    if inv_match2(u, 'kybl', 'kygh') then
        call unit_add_item_by_id(u, 'mgtk')

    elseif inv_accept2(u, 'ches', 'kybl') or inv_accept2(u, 'ches', 'kygh') /*
    */ or inv_accept2(u, 'ches', 'kymn') or inv_accept2(u, 'ches', 'kysn') then
        call inv_remove_slot2(u)
        call unit_add_item_by_id_charges(u, 'mcri', 2)

    elseif inv_accept3(u, 'hcun', 'hval', 'mcou') then
        if no_trolls_alive() then
            call inv_remove_items3(u)
            call unit_add_item_by_id(u, 'ckng')

        else
            call writeln("There are still some trolls alive on the map. Kill them first.")
        endif

    elseif inv_accept6(u, 'bzbe', 'gopr', 'wcyc', 'wlsd', 'woms', 'wshs') then
        call inv_remove_slot1(u)
        call inv_remove_slot3(u)
        call inv_remove_slot4(u)
        call inv_remove_slot5(u)
        call inv_remove_slot6(u)
        call unit_add_item_by_id(u, 'bzbf')
    endif
endfunction

private function on_item_pickup takes unit u returns nothing
    call unit_try_match_recipes(u)
endfunction
private function on_item_pickup_action takes nothing returns nothing
    call on_item_pickup(GetTriggerUnit())
endfunction

private function on_unit_death takes unit d, unit k returns nothing
    if k != null then
        call unit_try_match_recipes(k)
    endif
endfunction
private function on_unit_death_action takes nothing returns nothing
    call on_unit_death(GetTriggerUnit(), GetKillingUnit())
endfunction

private function on_spell_effect takes unit u returns nothing
    local integer sid = GetSpellAbilityId()

    if sid != 'AHtc' then
        return
    endif
    call inv_load(u)

    if sid == 'AHtc' and inv_match4(u, 'ocor', 'ofir', 'oli2', 'oven') then
        call unit_add_item_by_id(u, 'sora')
    endif
endfunction
private function on_spell_effect_action takes nothing returns nothing
    call on_spell_effect(GetTriggerUnit())
endfunction

private function init takes nothing returns nothing
    local trigger t

    set t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_PICKUP_ITEM)
    call TriggerAddAction(t, function on_item_pickup_action)

    set t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_DEATH)
    call TriggerAddAction(t, function on_unit_death_action)

    set t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddAction(t, function on_spell_effect_action)
endfunction

endlibrary // inv


library libassert

function assert takes string s, boolean b returns nothing
    if not b then
        call BJDebugMsg("|cffFF0000assert failed: |r" + s)
        call I2S(1/0)
    endif
endfunction

function writeln takes string s returns nothing
    call BJDebugMsg(s)
endfunction

endlibrary

PS: in jass strings can span multiple lines
JASS:
globals
    string description = "1) Blood Key + Ghost Key = Magic Chain Key (ordered, onpickup)\n" + /*
    */ "2) Any key +  Cheese (catalyst) = Mechanical Critter (unordered, onpickup)\n" + /*
    */ "3) Helm of Valor + Hood of Cunning + Medallion of Courage = Crown of Kings\n" + /*
    */ "                (ordered, onpickup, special condition: no trolls alive)\n" + /*
    */ "4) All four Orbs = Shadow Orb (unordered, assembled only via Thunder Clap)\n" + /*
    */ "5) All Wands + Glyph (catalyst) + Empty Vial = Full Vial (unordered, onpickup)\n" + /*
    */ "\nYou can order hero to assemble unordered recipes despite their inventory being full." + /*
    */ " Inventory manipulation e.g. item double click can trigger ordered recipe assembly."
endglobals

// =>

globals
    string description = "1) Blood Key + Ghost Key = Magic Chain Key (ordered, onpickup)
2) Any key +  Cheese (catalyst) = Mechanical Critter (unordered, onpickup)
3) Helm of Valor + Hood of Cunning + Medallion of Courage = Crown of Kings
                (ordered, onpickup, special condition: no trolls alive)
4) All four Orbs = Shadow Orb (unordered, assembled only via Thunder Clap)
5) All Wands + Glyph (catalyst) + Empty Vial = Full Vial (unordered, onpickup)
You can order hero to assemble unordered recipes despite their inventory being full.
Inventory manipulation e.g. item double click can trigger ordered recipe assembly."
endglobals
 
EDIT:

Removed Quote.

Remarks:

  • I must agree with @Chaosy here. This is complete overkill, but a good kind of overkill. (Little to no need for an update in features, that is, due to anticipatory approach in coding)
  • I cannot help but try to approve this once I get back on mapping.

Nitpicks:

Just in case, these are relatively minor, and may also contain suggestions.​
  • In spell importing, I would suggest to make it a bit more descriptive than just plug-and-play.
  • Why not rename UnitAssemblyItem to UnitAssembleItem?
  • This should be called an Item Recipe System, not just Item Recipe.

Quirks:

  • The documentation is crisp and concise, along with the details of the capability of the resource.

As of now, I would like to get a further look on this.

Status:

  • Pending, from Awaiting Update
 
Last edited:

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
Thank you for feedback guys!

@Aniki what you posted doesn't follow the submission rules (Jass convention).
Now to the things that really matter.. The reason it's coupled the way it is, is to make it intuitive in usage, class instance-methods API that is a very familiar concept for most vJassers. Interface that could be taken out of struct into standalone functions without generating additional code during jasshelper compile, have been moved. On top of that, delegates has been used for module interface to unsure compiled code is clean and again, with no unnecessary methods generated during compile time.
Your code does not account for fact that item recipe could be ordered or unordered type.
Your code does not account for arbitrary recipes (not talking about arbitrary requirements here). I permit user to assembly recipes from vector rather than unit inventory which always limited by the size of 6.
Your code does not account properly for in and out status of units inventory when assembling recipe (what happens to each item in given unit's inventory).
And so on.. I guess you see the point now.

Security is implemented where is it needed and nowhere else, but without any security your code is like a fresh meal for bugs. We don't want bugs.

Thanks for the multiline point tho, I'm used to not being able to use newlines after "." operator (i.e. cascading the invocations) thus I mirrored the string concatenation in the same fashion.

Nitpicks:

Just in case, these are relatively minor, and may also contain suggestions.​

  • In spell importing, I would suggest to make it a bit more descriptive than just plug-and-play.
  • Why not rename UnitAssemblyItem to UnitAssembleItem?
  • This should be called an Item Recipe System, not just Item Recipe.
Could you elaborate on "descriptive"?
The rename of said function seems reasonable.
The "Item Recipe System", you are talking about thread or library itself? I bet it's the former. I agree!

About "overkill" again, I urge you guys to think about Island Troll Tribes recipes, how advanced, how flexible, how dynamic they are. If you ever played that game, you will know what I speak of. If you did not, and perhaps DotA is the only recipe related map you have in mind, know this: troll's recipes are miles away in complexity.

ItemRecipe is an ultimate solution, accounts for basic and advanced needs. Since it and its friends (my other item libs which I've already mentioned) have been accepted by NotD: Aftermath and Island Troll Tribes communities I don't think I followed the wrong path by giving people - read: mappers - tools to do whatever they want with items in their map.
People wanted control. I gave them control.
People wanted power. I gave them power.
 
Last edited:

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
The reason for no special effects is because I wanted this to be as generic as possible. Arbitrary content and size of item recipe ingredient list. Arbitrary unit requirements. Arbitrary response on item recipe assembly: EVENT_ITEM_RECIPE_ASSEMBLE. User can register to that event and play whatever effects he or she wants : )

Edit:
As requested, function names have been updated:

ItemRecipe::assembly method has been renamed to assemble.
ItemRecipe::disassembly method has been renamed to disassemble.

ItemRecipe (vJass):
- UnitAssemblyItem function has been renamed to UnitAssembleItem
- UnitDisassemblyItem function has been renamed to UnitDisassembleItem

ItemRecipe (Wurst):
- unit.assemblyItem extension method has been renamed to unit.assembleItem
- unit.diassemblyItem extension method has been renamed to unit.disassembleItem
 
Last edited:

Cokemonkey11

Code Reviewer
Level 29
Joined
May 9, 2006
Messages
3,516
This review is for the wurst code only.

- I seem to be missing a high-level API documentation. From reading the code it seems there are lots of public methods that aren't needed in the demo - and thus I think you need a structured set of documentation (A page or so for high-level stuff, and a page or so for lower level (but public stuff)
- "Returns collection of ingredients chosen to assemble the reward item, where each index corresponds to triggering unit inventory slot." - I think this is a good use case for something like a smallvec ( smallvec::smallvec - Rust ) rather than a linked list. In other words, if your LinkedList is always of size 6, may as well use a tupletype - no?
- I noticed that some public methods accept or return data with dynamic lifetime (classes - in particular linkedlist) but your documentation doesn't specify ownership. You should make that abundantly clear in your hotdocs
- Using both halves of a hashmap (negative and positive type ids) is a cool trick!
- "if not batch // removing item when batch is ongoing is forbidden" - you need some error handling here, this kind of code is too fragile
- presentation of code is a bit muddled and weakly documented. I would prefer to see the public API distinct and clear from the private functions; and in general the code needs more comments
- Installation instructions not clear. Are you not using the wurst dependency manager? Maybe you should?

This has a lot of potential, but my view is that the larger and more feature-full the system, so too do things like modularity, documentation quality, and maintainability matter.

3 stars from me, but very improveable
 

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
@Cokemonkey11
First things first: thanks for your feedback, appreciated.
In general you want a better documentation. I haven't noticed any code changes per se that you would like to see.

- Ingredient list does not have static size of 6, the size is arbitrary. See the 'test' methods again. This system is highly modular and basically compatible with anything hive has ever produced granted it works with 1.28+ patch. The arbitrary ingredient allows for support of such features as backpack.
- There is no smallvec for wurst or at least I'm not aware of it.
HashMap is basically a Table wrapper and HashList has a very week implementation in wurst. My vJass VectorT is much more suitable for the job, but does not have wurst equivalent.
Wurst documentation does not specify ownership for many container-related public methods either - i.e. it does not destroy the object being manipulated.
The only method that is an exception from that is private, and I handle the release process internally.

- Installation is exactly the same as with wurstStdLib2:
wurstExtLib is just another dependency you link to your project via WurstSetup -> Advanced. Maintainability becomes trivial, you update wurstExtLib the as you do wurstStdLib2.

3/5 seems harsh ;X
Documentation is probably not clear enough for wurst user to quickly get a firm grasp over this powerful library. On the other hand, I expect wurst users to know about installation and such because, frankly, most of wurst users are software engineers or programmers.
Will work on it.
 

Cokemonkey11

Code Reviewer
Level 29
Joined
May 9, 2006
Messages
3,516
> In general you want a better documentation. I haven't noticed any code changes per se that you would like to see.

That's right, yes. It's possible that after I see more meaningful documentation I might arrive at some code comments, but right now I think docs is your #1 priority

More complex = more docs needed

> Ingredient list does not have static size of 6, the size is arbitrary. See the 'test' methods again. This system is highly modular and basically compatible with anything hive has ever produced granted it works with 1.28+ patch. The arbitrary ingredient allows for support of such features as backpack.

It's a good point - I did think about pseudo 7th item but didn't keep in mind backpack. I feel somewhat more positive aboud the LinkedList<> centric API now.

I wonder if it makes sense to talk about these structures as Iter<T>. Can wurst describe interfaces over both classes and tuple-types? I'm not sure, but I am sure this is probably overengineering to talk about.

> There is no smallvec for wurst or at least I'm not aware of it.

Given the last point, I don't think it matters - but you could just define a tuple-type, like so:

JASS:
tuple smallInventory(item i0, item i1, item i2, item i3, item i4, item i5, item pseudo)

These have lower overhead than LinkedList<T>

> Wurst documentation does not specify ownership for many container-related public methods either - i.e. it does not destroy the object being manipulated.

Do you agree though that ownership is important? I think so.

And what about functions that return a new, owned class instance? It's useful to say "returns a new list of item recipes. The list is not otherwise referenced and must be cleaned up." something like that

> wurstExtLib is just another dependency you link to your project via WurstSetup

Cool! But in that case, maybe I could suggest splitting out ItemRecipe from the rest of your dependencies?

Actually I'm not so dead-set on that. The current dependency manager is a bit cumbersome - maybe in the future it would be good to split them, but not so important now.

> 3/5 seems harsh ;X

I'm a harsh guy :) but based on your responses so far I'm pretty sure I will raise my rating once I see the "executive summary" level documentation and a little bit of cleanup.
 
Level 1
Joined
Apr 8, 2020
Messages
110
@Bannar Can we get static methods for assemble and assembleEx, in which the system will attempt to create any recipe based on given ingredients - whether these are items in normal inventory or custom one?

In other words, can we get like a generic assemble function that does not pertain to a recipe? I want to assemble items with ease on any event, not just on pickup or spell casts.

Without such methods, I thought of creating a stack of my item recipes and then calling assembleEx for each itemRecipe in a loop. However, such a solution is not very elegant, especially if you have a lot of recipes.
 
Last edited:

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
@SinisterLuffy just bumping so that you know your comment has been noted. Since the launch of Reforged I haven't been working with WE, really. Compatibility with existing WE version would have to checked first, then status of custom functions that I'd added - see if some aren't deprecated: implemented by Blizzard internally and exposed via public API.
 
Hello, I've been having fun using this system in Wurst so far, I just got a few nitpick :

ItemRecipe class contains a "permanent" attribute, this attribute is used to determine whether or not, an item crafted through itemRecipe, can be "disassembled" and refund the ingredients used to assemble the item. I find the term "permanent" confusing as it is a term used in Item Object definition for the Classification field, I actually don't know what's the purpose of classification field other than sorting items in the Editor. Maybe name it "disassemblable" instead?

Same feedback with the "perishable" attribute in the RecipeIngredient class, already used in the Item Object definition, but in the ingredient context, it defines whether or not, the item is consumed upon assemble, maybe rename it to "refundable"?

Last remark is about the ability casted to trigger assemble, I encountered the need to have multiple abilities be used to craft the very same item, if I read correctly, ItemRecipe can hold only 1 ability Id, wouldn't it be possible to use a LinkedList<int> abilityIds to store multiple abilities?
 
Top