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

[vJASS] Item Resource Cost

Level 19
Joined
Mar 18, 2012
Messages
1,716

Item Resource Cost


Retrieves and saves information about
gold and lumber resource costs
as well as charges for item type ids.



Obligatory RequirementsOptional Requirements



Be aware that the Inventory - Sell Item Return Rate
of the Gameplay Constant is not taken into account for resource cost calculation.


Item Resource Costs

JASS:
library ItemResourceCost uses Table, WorldBounds optional RegisterPlayerUnitEvent
//===========================================================
// Version 1.1
// Retrieves and saves information about 
// gold and lumber resource costs
// as well as charges for item type ids.
//
// Be aware that the Inventory - Sell Item Return Rate
// of the Gameplay Constant is not taken into account.
//===========================================================
//
//  Credits to:
//      - Bribe for Table
//      - Nestharus for WorldBounds, GetItemCost API and system design.
//      - Magtheridon96 for RegisterPlayerUnitEvent 
//
//===========================================================
//
//  Table                    -  hiveworkshop.com/forums/jass-resources-412/snippet-new-table-188084/
//  WorldBounds              -  github.com/nestharus/JASS/tree/master/jass/Systems/WorldBounds
//  RegisterPlayerUnitEvent  -  hiveworkshop.com/forums/jass-resources-412/snippet-registerplayerunitevent-203338/ 
//
//===========================================================
//
//  API:
//       function GetItemTypeIdGoldCost takes integer itemId returns integer
//       function GetItemTypeIdLumberCost takes integer itemId returns integer
//       function GetItemTypeIdCharges takes integer itemId returns integer
//
//       function GetItemGoldCost takes item whichItem returns integer
//           -   Gets total item gold cost considering the item charges
//       function GetItemLumberCost takes item whichItem returns integer
//           -   Gets total item wood cost considering the item charges
//
//===========================================================
//
//  100% backwards compatible to GetItemCost
//      - github.com/nestharus/JASS/blob/master/jass/Systems/Costs%20-%20Unit%20%26%20Item/item.j 
//
//===========================================================
// User settings.
//===========================================================

    globals                     // Considered as neutral in you map.
        private constant player NEUTRAL_PLAYER = Player(15)
    endglobals
    
    // Boolean "prev" should return the previous setup of your indexer,
    // so the system can properly restore its state once the shop unit is created.
    private function ToogleUnitIndexer takes boolean enable returns boolean
        local boolean prev = true// UnitIndexer.enabled
        // set UnitIndexer.enabled = enable
        return prev
    endfunction
    
    // The unit type id doesn't really matter. Choose whatever you like.
    //! textmacro ITEM_RESOURCE_COST_SHOP_UNIT 
                                                          // For best results choose a corner where no other actions takes place.
            set shop = CreateUnit(NEUTRAL_PLAYER, 'nvlw', WorldBounds.maxX, WorldBounds.maxY, 0)
    //! endtextmacro 

//===========================================================
// ItemResourceCost code. Make changes carefully.
//===========================================================
    
    globals 
        private Table costs
        private Table charges
        private Table guardian
        private unit shop
        private integer tempId
    endglobals

    // Maximum safety which can be provided.
    private function FindBoughtItem takes nothing returns nothing
        local item temp = GetEnumItem()
        if GetItemTypeId(temp) == tempId and not guardian.boolean.has(GetHandleId(temp)) then
            set charges[tempId] = GetItemCharges(temp)// Get default charges.
            set tempId = 0
            call RemoveItem(temp)
        endif
        set temp = null
    endfunction
    
    private function FlagItems takes nothing returns nothing
        set guardian.boolean[GetHandleId(GetEnumItem())] = true
    endfunction
    
    private function GetItemData takes integer id returns nothing
        // Save previous resource state.
        local integer gold = GetPlayerState(NEUTRAL_PLAYER, PLAYER_STATE_RESOURCE_GOLD)
        local integer wood = GetPlayerState(NEUTRAL_PLAYER, PLAYER_STATE_RESOURCE_LUMBER)
        local real x = GetUnitX(shop)// This point should be constant,
        local real y = GetUnitY(shop)// but who knows ...
        local rect temp = Rect(x - 1088, y - 1088, x + 1088, y + 1088)// Pathing checks don't go above 1024.
        
        // If a registered trigger action function 
        // of event trigger EVENT_PLAYER_UNIT_ISSUED_ORDER 
        // creates a new item of the same type id as "id" ...
        // And that item pops up within 1088 units of the shop ...
        // While not having RegisterPlayerUnitEvent in your map ... 
        // Then seriously ... I can't help you.
        static if LIBRARY_RegisterPlayerUnitEvent then
            local trigger trig = GetPlayerUnitEventTrigger(EVENT_PLAYER_UNIT_ISSUED_ORDER)
            local boolean flag = IsTriggerEnabled(trig)// To properly restore the trigger state.
        endif
        
        // Prepare the for shopping.
        call SetPlayerState(NEUTRAL_PLAYER, PLAYER_STATE_RESOURCE_GOLD, 1000000)
        call SetPlayerState(NEUTRAL_PLAYER, PLAYER_STATE_RESOURCE_LUMBER, 1000000)
        call AddItemToStock(shop, id, 1, 1)
        set tempId = id
        
        static if LIBRARY_RegisterPlayerUnitEvent then
            call DisableTrigger(trig)
        endif
        call EnumItemsInRect(temp, null, function FlagItems)
        call IssueNeutralImmediateOrderById(NEUTRAL_PLAYER, shop, id)
        call EnumItemsInRect(temp, null, function FindBoughtItem)
        static if LIBRARY_RegisterPlayerUnitEvent then
            if flag then
                call EnableTrigger(trig)
            endif
            set trig = null
        endif        
        call RemoveItemFromStock(shop, id)
        call guardian.flush()

        // Get gold and lumber costs.
        set costs[id] = 1000000 - GetPlayerState(NEUTRAL_PLAYER, PLAYER_STATE_RESOURCE_GOLD)
        set costs[-id] = 1000000 - GetPlayerState(NEUTRAL_PLAYER, PLAYER_STATE_RESOURCE_LUMBER)
        
        // Restore the resource state as it was before.
        call SetPlayerState(NEUTRAL_PLAYER, PLAYER_STATE_RESOURCE_GOLD, gold)
        call SetPlayerState(NEUTRAL_PLAYER, PLAYER_STATE_RESOURCE_LUMBER, wood)
        
        call RemoveRect(temp)
        set temp = null
    endfunction
    
    function GetItemTypeIdGoldCost takes integer id returns integer
        if not costs.has(id) then
            call GetItemData(id)
        endif
        return costs[id]
    endfunction
    
    function GetItemTypeIdLumberCost takes integer id returns integer
        if not costs.has(id) then
            call GetItemData(id)
        endif
        return costs[-id]
    endfunction
    
    function GetItemTypeIdCharges takes integer id returns integer
        if not charges.has(id) then
            call GetItemData(id)
        endif
        return charges[id]
    endfunction
    
    function GetItemGoldCost takes item whichItem returns integer
        local integer id = GetItemTypeId(whichItem)
        local real count = GetItemTypeIdCharges(id)
        if count > 0 then
            return R2I(costs[id]*(GetItemCharges(whichItem)/count))
        endif
        return costs[id]
    endfunction
    
    function GetItemLumberCost takes item whichItem returns integer
        local integer id = GetItemTypeId(whichItem)
        local real count = GetItemTypeIdCharges(id)
        if count > 0 then
            return R2I(costs[-id]*(GetItemCharges(whichItem)/count))
        endif
        return costs[-id]
    endfunction  
    
    // Make sure it works for everyone and every map.
    private module Inits 
        private static method onInit takes nothing returns nothing
            call thistype.init()
        endmethod
    endmodule
    private struct I extends array
        private static method init takes nothing returns nothing
            local boolean prev = ToogleUnitIndexer(false)
            //! runtextmacro ITEM_RESOURCE_COST_SHOP_UNIT()
            call ToogleUnitIndexer(prev)
            
            debug if GetUnitTypeId(shop) == 0 then
                debug call BJDebugMsg("|cffffa500Error in library ItemResourceCost:\n    |cff99b4d1--> [ Invalid unit type id for shop. Check your user settings! ]|r")
            debug endif
            
            call UnitAddAbility(shop, 'Aloc')
            call UnitAddAbility(shop, 'Asid')// Can shop.
            call SetUnitPropWindow(shop, 0)// Can't move.
            call ShowUnit(shop, false)// Can still shop. Performance friendly.
            set costs = Table.create()
            set charges = Table.create()
            set guardian = Table.create()
        endmethod
        implement Inits
    endstruct
endlibrary

// For backwards compatibility.
// You can safely delete the following code
// if not required in your map.
library_once GetItemCost uses ItemResourceCost 
    function GetItemTypeIdWoodCost takes integer id returns integer
        return GetItemTypeIdLumberCost(id) 
    endfunction
    function GetItemWoodCost takes item i returns integer
        return GetItemLumberCost(i)
    endfunction
endlibrary

In this context the Object Data Extractor should
be mentioned as tool for reading object editor data fields.
For public resources impractical, as it must be integrated into a specific map file
and therefore a preprocessor made for the map makers.
 

Attachments

  • GetItemCost (2).w3x
    29.8 KB · Views: 65
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
I moved out from the Spell Section, because there are approved
systems equal to this one ... however flawed and less efficient.

I recommend using Item Resource Cost for best and bug-free results.

Reasons:
  • Due to temporary saving items in a table, it will not remove the wrong item of equal type id.
  • Restores neutral player resource properly.
  • Backwards compatible to Nestharus GetItemCost.
  • Due to disabling the player unit event trigger, the chance for creating a non flagged item of
    equal type id within the trigger action function is very small.

Local rect temp.
If you wish I can use a static rect instead. My assumption is that
rects are very very cheap handles and fast in allocation and deallocation.

Trigger action function.
Maybe the item enumeration could run faster via filter function than action function,
but marginal and this is not proven by benchmarks.
However it would require two static boolexpr variables.

Unit Indexers.
The shop is a unit, which must not fire an index event.
The solution presented here seems to be appropriate to enable/disable
any unit indexer there is and restore the previous setup, which
could be "disabled".
JASS:
    // Boolean "prev" should return the previous setup of your indexer,
    // so the system can properly restore its state once the shop unit is created.
    private function ToogleUnitIndexer takes boolean enable returns boolean
        local boolean prev = true// UnitIndexer.enabled
        // set UnitIndexer.enabled = enable
        return prev
    endfunction
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
why exactly does the unit have to be unindexed?
Memory management, force of habit, beauty in code ... there are
many reasons and no reason depending what type of coder you are.

Why must any unit remain unindexed? Why must any unit be indexed? :)
In my opinion indexers are good for cleanup and centralising
the unit enter world event to one trigger evaluation.
Everything else is theory crafting about performance without evidence
from a really submitted playable map. Others will strongly disagree.

Not that this one function call onInit is expensive, but for completeness
it shall remain there. I could also inline Nestharus's UnitIndexer with a static if,
assuming everyone uses his system. I know you'll not like that one.

Unit indexers are frequently used. Just think of damage systems.
The shop doesn't fight, in best cases does nothing else than
working for this system during a game season.
So why reference it in a unit array and give it an index?
Not to mention that onIndex evenatually kicks of x other threads.
For example registers to a damage trigger and stores the index in a list
of units for that damage trigger to referesh after x units died.

Maybe setting up a private constant global which would be configurable for that?
When it comes to prices in Warcraft there is a difference in buying and selling items.
You pay full price on buying, but get % price on sell. Merchants are ass....
If you change the game constants you must also change the setting in the library.
You'll need a double amount of API to distinguish between buy and sell...

I think it's good as it is now. It gives you values as set in the object editor.
Users may re-calculate the returned costs to their needs.
 
Level 23
Joined
Apr 16, 2012
Messages
4,041
You are putting a per-map burden on mappers to suplement a line or more of code to disable their unit indexer of choice, which may not even support it just because you want to not index the unit for no reason. My opinion on this is that unit indexer shall index all units unless it is specifically required to not index the unit for some reason. I do not see any reason to not let indexer index the unit here.

Fair enough on the price, since I can multiply it in my script if I want the sell price and it is map-based.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
In a worst case scenario it's a one time DoNothing() function call.

On the other side users have granted controll over indexing the shop,
without touching the actual system code. It also shows right away that the
library is unit indexer ( setting ) independant.

My reasons for aware not indexing the shop.
  • onIndex kicks of multiple threads and registers units to other systems this shop unit doesn't belong to.
  • The purpose of the shop unit is limited to this library and shall not be abused anywhere else.
  • It should also not be accessable from anywhere else. GetUnitByIndex(index) ( Except unit enumeration ... )
  • Running an indexing process doesn't have a positive effect on the shop, but potentially
    destabilize Item Resource Cost by other ( maybe not perfectly refined code ).
    Not all map makers are master coders.
 
It does not seem to work with charged items, because of charges[id].
I quickly printed some info and everything runs but the private function FindBoughtItem does not run.
So the item is not found and default charges is never set. Or do I miss something? Here is the map attached. I added items in middle to test with charges.
(works with default charges, but not when used some)

Would need an answer, you can just PM me or post here. Gy for now.
 

Attachments

  • GetItemCost (2).w3x
    30.3 KB · Views: 42
Last edited:
Top