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

Resource System

This bundle is marked as awaiting update. A staff member has requested changes to it before it can be approved.
This system was designed to allow for a much greater flexibility and wider variety of resources in Wc3 maps. It allows you to create almost any number of resources, make any destructable a resource, and give almost anything a cost!


Features:

  • The system can handle up to 738 different resources simultaneously, and can also easily be adapted to allow for different players to use different resources.
  • Essentially any destructable can be marked as a resource, any unit as a harvester, and any unit or structure as a drop-off point.
  • The system includes a multiboard representation of the resources, though it is technically possible to link the in-game resources to custom ones.
  • ANY order or ability can be assigned a resource cost, so you can easily make spells or item abilities cost resources.
  • The test map includes an example on how to spawn corpses from critters and make them harvestable. You can also make harvestable farms, AoE-style. Another possible use would be to spawn harvestable derelicts from destroyed structures.

Limitations:
  • Resource destructables cannot have the "tree" classification (unfortunately).
  • Custom repair costs are currently not supported - I recommend that you make repairs free if you want to use this system.
  • You will need to register all resource costs manually, and also type out the costs in the tooltips of each unit.

Setup:
  • See "Resource Setup" code section for commented implementation details. Some basic knowledge of how raw unit IDs work is required.
  • I have gotten criticism before over adding custom models to my test maps for embellshment - so just to clarify, you do NOT need to include any of the custom imports in order for the system to work. You will however need to add the custom spells and configure their respective IDs.

Code:

JASS:
library ResourceLibrary

    //-------------------------------------------//
    //---------CUSTOM RESOURCE SYSTEM------------//
    //------------------BY-----------------------//
    //--------------FINGOLFIN--------------------//
    //-------------------------------------------//

    globals
     
        //DEFAULT MESSAGE STRINGS
        private constant string MSG_ERROR               = "|cffff0000ERROR: "
        private constant string MSG_DEFAULT_NOTENOUGH   = "|cffdaa520Not enough "

        //SYSTEM FLAGS
        private constant boolean USE_INDEXER            = false
     
        //HASHTABLE KEYS
        private constant integer RESOURCE_KEY           = StringHash("Resources")
        private constant integer HARVESTER_KEY          = StringHash("Harvester")
        private constant integer SPAWNER_KEY            = StringHash("Spawner")
        private constant integer CONSTRUCTION_KEY       = StringHash("Construction")
     
        //SYSTEM CONSTANTS
        private constant integer MAX_RESOURCES          = 100
        private constant integer ORDERID_CANCEL         = 851976
        private constant integer DROP_ABIL              = 'drop'
        private constant string DROP_ORDER              = "curse"

        private constant real INTERVAL                  = 0.33
        private constant real SEARCH_RADIUS             = 10000
     
     
        private group ENUM_GROUP                        = CreateGroup()
        private rect RECT                               = Rect(-1, -1, 1, 1)
     
        private hashtable Hashtbl                       = InitHashtable()
     
        //PLAYER RESOURCES
        integer array PlayerResource[11][MAX_RESOURCES]
        boolean array IsConstructing

    endglobals
 
    function IsUnitConstructing takes unit whichunit returns boolean
        static if USE_INDEXER then
            if IsConstructing[GetUnitUserData(whichunit)] != null then
                return IsConstructing[GetUnitUserData(whichunit)]
            endif
            return false
        else
            if HaveSavedBoolean(Hashtbl, CONSTRUCTION_KEY, GetHandleId(whichunit)) then
                return LoadBoolean(Hashtbl, CONSTRUCTION_KEY, GetHandleId(whichunit))
            endif
            return false
        endif
    endfunction
 
    function RegisterCost takes integer unitid, Resource whichresource, integer amount returns nothing
        call SaveInteger(Hashtbl, unitid, whichresource, amount)
    endfunction
 
    function GetUnitBuildTime takes integer unitid returns real
        return 60.
    endfunction
 
    function GetCost takes integer unitid, Resource whichresource returns integer
        //WORKS FOR UNITS, BUILDINGS, TECH ITEMS... ANYTHING, REALLY!
        if HaveSavedInteger(Hashtbl, unitid, whichresource) then
            return LoadInteger(Hashtbl, unitid, whichresource)
        endif
        return 0
    endfunction

    struct Resource
     
        readonly string             name
        readonly string             icon
        readonly string             color
        readonly string             anim
        readonly string             animtag
        readonly string             msg_not_enough      = MSG_DEFAULT_NOTENOUGH

        readonly integer            orderid
        readonly integer            abilid              = 0
     
        readonly integer array      max_carried[11]
        readonly integer array      harvest_rate[11]
     
        readonly thistype           next                = 0
        readonly thistype           prev                = 0

        boolean                     useHitAnim          = true
     
        readonly static integer     count               = 0
        private static thistype     enum                = 0
        private static real         min_x               = 0
        private static real         min_y               = 0
        private static real         min_dist            = 0
        private static unit         min_unit            = null
        private static destructable min_dest            = null
        private static player       min_player          = Player(PLAYER_NEUTRAL_PASSIVE)
        private static boolean      useEnum             = false
        private static boolean      doRefund            = true
        private static group        cancelGroup
        private static timer        cancelTimer
     
        private static sound        snd_error
     
        static method operator first takes nothing returns thistype
            return thistype(0).next
        endmethod
     
        static method operator last takes nothing returns thistype
            return thistype(0).prev
        endmethod

        static method create takes string name, string icon, string anim, string animtag, string color, integer harvestrate, integer maxcarried, integer abilid, string ordername returns thistype
            local thistype this = thistype.allocate()
            local integer i = 0
         
            //ADD TO RESOURCE LIST
            set last.next = this
            set prev = last
            set thistype(0).prev = this
            set count = count + 1
         
            //SETUP MEMBERS
            set .name = name
            set .icon = icon
            set .color = color
            set .anim = anim
            set .animtag = animtag
            set .abilid = abilid
            set .orderid = OrderId(ordername)
         
            loop
                exitwhen i > 11
                set .max_carried[i] = maxcarried
                set .harvest_rate[i] = harvestrate
                set i = i+1
            endloop

            return this
        endmethod
     
        static method get takes destructable d returns thistype
            if HaveSavedInteger(Hashtbl, RESOURCE_KEY, GetDestructableTypeId(d)) then
                return LoadInteger(Hashtbl, RESOURCE_KEY, GetDestructableTypeId(d))
            endif
            return 0
        endmethod

        method registerSource takes integer typeid returns nothing
            call SaveInteger(Hashtbl, RESOURCE_KEY, typeid, this)
        endmethod

        method isDestructableSource takes destructable d returns boolean
            return LoadInteger(Hashtbl, RESOURCE_KEY, GetDestructableTypeId(d)) == this
        endmethod

        method registerDropoffAbil takes integer abilid returns nothing
            call SaveInteger(Hashtbl, RESOURCE_KEY, this, abilid)
        endmethod

        method isUnitDropoff takes unit u returns boolean
            if HaveSavedInteger(Hashtbl, RESOURCE_KEY, this) then
                return GetUnitAbilityLevel(u, LoadInteger(Hashtbl, RESOURCE_KEY, this)) > 0
            endif
            return false
        endmethod
     
        method setMaxCarriedForPlayer takes player p, integer value returns nothing
            set max_carried[GetPlayerId(p)] = value
        endmethod
     
        method setHarvestRateForPlayer takes player p, integer value returns nothing
            set harvest_rate[GetPlayerId(p)] = value
        endmethod
     
        method setInsufficientResourceMsg takes string msg returns nothing
            set .msg_not_enough = msg
        endmethod

        private static method enumClosestDropoff takes nothing returns boolean
            local thistype this = .enum
            local unit u = GetFilterUnit()
            local real dx = GetUnitX(u)-.min_x
            local real dy = GetUnitY(u)-.min_y
            local real dsq = dx*dx+dy*dy

            if .isUnitDropoff(u) and GetOwningPlayer(u) == .min_player then
                if dsq < .min_dist or .min_dist == 0 then
                    set .min_dist = dsq
                    set .min_unit = u
                endif
            endif
            set u = null
            return false
        endmethod

        method getClosestDropoff takes real x, real y, player p returns unit
            local group g = CreateGroup()

            set .enum = this
            set .min_unit = null
            set .min_x = x
            set .min_y = y
            set .min_dist = 0
            set .min_player = p

            call GroupEnumUnitsInRange(g, x, y, SEARCH_RADIUS, Filter(function thistype.enumClosestDropoff))

            call DestroyGroup(g)
            set g = null

            return .min_unit
        endmethod
     
        private static method enumClosestDestructable takes nothing returns nothing
            local destructable d = GetEnumDestructable()
            local real dsq = 0
            local real dx = 0
            local real dy = 0
         
            if GetDestructableLife(d) > 0 and (((not .useEnum) and Resource.get(d) != 0) or (.useEnum and enum.isDestructableSource(d))) then
                set dx = GetDestructableX(d)-.min_x
                set dy = GetDestructableY(d)-.min_y
                set dsq = dx * dx + dy * dy
                if dsq < .min_dist or .min_dist == 0 then
                    set .min_dist = dsq
                    set .min_dest = d
                endif
            endif
         
            set d = null
        endmethod
     
        method getClosestSource takes real x, real y, real radius returns destructable
            call SetRect(RECT, x-radius, y-radius, x+radius, y+radius)
         
            set .enum = this
            set .useEnum = true
            set .min_x = x
            set .min_y = y
            set .min_dist = 0
            set .min_dest = null
         
            call EnumDestructablesInRect(RECT, null, function thistype.enumClosestDestructable)
         
            return .min_dest
        endmethod
     
        static method getClosestDestructable takes real x, real y, real radius returns destructable
            call SetRect(RECT, x-radius, y-radius, x+radius, y+radius)
         
            set .useEnum = false
            set .min_x = x
            set .min_y = y
            set .min_dist = 0
            set .min_dest = null
         
            call EnumDestructablesInRect(RECT, null, function thistype.enumClosestDestructable)
         
            return .min_dest
        endmethod
     
        private static method enumCancelProd takes nothing returns nothing
            set thistype.doRefund = false
            if IssueImmediateOrderById(GetEnumUnit(), ORDERID_CANCEL) == false then
                set thistype.doRefund = true
            endif

            call GroupRemoveUnit(thistype.cancelGroup, GetEnumUnit())
        endmethod
     
        private static method cancelProd takes nothing returns nothing
            call ForGroup(thistype.cancelGroup, function thistype.enumCancelProd)
        endmethod
     
        static method unitCancelProd takes unit whichunit returns nothing
            if IsUnitType(whichunit, UNIT_TYPE_STRUCTURE) then
                call GroupAddUnit(thistype.cancelGroup, whichunit)
                call TimerStart(thistype.cancelTimer, 0, false, function thistype.cancelProd)
            else
                call PauseUnit(whichunit, true)
                call IssueImmediateOrder(whichunit, "stop")
                call PauseUnit(whichunit, false)
            endif
        endmethod
     
        static method enforceCost takes unit producer, integer objectid, boolean apply returns boolean
            local thistype this = .first
            local thistype array res_list
            local integer array cost_list
            local integer playerid = GetPlayerId(GetOwningPlayer(producer))
            local integer cost = 0
            local integer n = 0
         
            loop
                exitwhen this == 0
                set cost = GetCost(objectid, this)
                if cost > 0 then
                    if PlayerResource[playerid][this] >= cost then
                        set res_list[n] = this
                        set cost_list[n] = cost
                        set n = n+1
                    else
                        call .unitCancelProd(producer)
                        call DisplayTextToPlayer(Player(playerid), 0, 0, .msg_not_enough+.name+"!")
                        call StartSound(.snd_error)
                        return false
                    endif
                endif
                set this = .next
            endloop
         
            if apply then
                //ONCE WE KNOW WE CAN AFFORD THE UNIT, WE SUBSTRACT THE RESORUCES
                loop
                    set n = n-1
                    set PlayerResource[playerid][res_list[n]] = PlayerResource[playerid][res_list[n]] - cost_list[n]
                    exitwhen n == 0
                endloop
            endif
            return true
        endmethod
     
        static method refundCost takes integer objectid, integer playerid returns nothing
            local thistype this = .first
            local integer cost = 0
         
            loop
                exitwhen this == 0
                set cost = GetCost(objectid, this)
                if cost > 0 then
                    set PlayerResource[playerid][this] = PlayerResource[playerid][this]+cost
                endif
                set this = .next
            endloop
        endmethod
     
        private static method onOrder takes nothing returns boolean
            local integer orderid   = GetIssuedOrderId()
            local unit producer     = GetTriggerUnit()
            //WE DON'T WANT TO APPLY THE COST OF STRUCTURES UNTIL THEY ARE PLACED
            //HOWEVER, WE NEED TO APPLY COST TO STRUCTURES BEING UPGRADED!
            local boolean applyCost = not((IsUnitType(producer, UNIT_TYPE_STRUCTURE) == false) and IsUnitIdType(orderid, UNIT_TYPE_STRUCTURE))
         
            if orderid != ORDERID_CANCEL and orderid != OrderId("move") and orderid != OrderId("smart") and orderid != OrderId("stop") then
                call .enforceCost(producer, orderid, applyCost)
            endif
         
            set producer = null
            return false
        endmethod
     
        private static method onBuildingFinished takes nothing returns boolean
            static if USE_INDEXER then
                set IsConstructing[GetUnitUserData(GetConstructedStructure())] = false
            else
                call SaveBoolean(Hashtbl, CONSTRUCTION_KEY, GetHandleId(GetConstructedStructure()), false)
            endif
            return false
        endmethod
     
        private static method onBuildingConstructed takes nothing returns boolean
            local unit constructed = GetTriggerUnit()
         
            if thistype.enforceCost(constructed, GetUnitTypeId(constructed), true) == false then
                call RemoveUnit(constructed)
                set thistype.doRefund = true
            endif
         
            static if USE_INDEXER then
                set IsConstructing[GetUnitUserData(constructed)] = true
            else
                call SaveBoolean(Hashtbl, CONSTRUCTION_KEY, GetHandleId(constructed), false)
            endif
         
            set constructed = null
            return false
        endmethod
     
        private static method onUnitCanceled takes nothing returns boolean
            if thistype.doRefund then
                call .refundCost(GetTrainedUnitType(), GetPlayerId(GetTriggerPlayer()))
            else
                set thistype.doRefund = true
            endif
         
            return false
        endmethod
     
        private static method onResearchCanceled takes nothing returns boolean
            if thistype.doRefund then
                call .refundCost(GetResearched(), GetPlayerId(GetTriggerPlayer()))
            else
                set thistype.doRefund = true
            endif
         
            return false
        endmethod
     
        private static method onUpgradeCanceled takes nothing returns boolean
            //WE ALWAYS REFUND CANCELED UPGRADES!
            call .refundCost(GetUnitTypeId(GetTriggerUnit()), GetPlayerId(GetTriggerPlayer()))
            set thistype.doRefund = true
            return false
        endmethod
     
        private static method onBuildingCanceled takes nothing returns boolean
            //WE ALWAYS REFUND CANCELED STRUCTURES!
            call .refundCost(GetUnitTypeId(GetCancelledStructure()), GetPlayerId(GetTriggerPlayer()))
            set thistype.doRefund = true
            return false
        endmethod
     
        private static method onInit takes nothing returns nothing
            local integer i = 0
            local trigger t = CreateTrigger()
         
            set thistype.cancelGroup = CreateGroup()
            set thistype.cancelTimer = CreateTimer()
         
            //REGISTER WHEN A UNIT STARTS SOME KIND OF ACTIVITY
            loop
                exitwhen i == bj_MAX_PLAYER_SLOTS
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_ISSUED_ORDER, null)
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, null)
                set i = i+1
            endloop
         
            call TriggerAddCondition(t, Condition(function thistype.onOrder))
         
            //UNIT CANCEL
            set i = 0
            set t = CreateTrigger()
         
            loop
                exitwhen i == bj_MAX_PLAYER_SLOTS
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_TRAIN_CANCEL, null)
                set i = i+1
            endloop
         
            call TriggerAddCondition(t, Condition(function thistype.onUnitCanceled))
         
            //BUILDING START
            set i = 0
            set t = CreateTrigger()
         
            loop
                exitwhen i == bj_MAX_PLAYER_SLOTS
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_CONSTRUCT_START, null)
                set i = i+1
            endloop
         
            call TriggerAddCondition(t, Condition(function thistype.onBuildingConstructed))
         
            //BUILDING START
            set i = 0
            set t = CreateTrigger()
         
            loop
                exitwhen i == bj_MAX_PLAYER_SLOTS
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_CONSTRUCT_FINISH, null)
                set i = i+1
            endloop
         
            call TriggerAddCondition(t, Condition(function thistype.onBuildingFinished))
         
            //BUILDING CANCEL
            set i = 0
            set t = CreateTrigger()
         
            loop
                exitwhen i == bj_MAX_PLAYER_SLOTS
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_CONSTRUCT_CANCEL, null)
                set i = i+1
            endloop
         
            call TriggerAddCondition(t, Condition(function thistype.onBuildingCanceled))
         
            //UPGRADE CANCEL
            set i = 0
            set t = CreateTrigger()
         
            loop
                exitwhen i == bj_MAX_PLAYER_SLOTS
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_UPGRADE_CANCEL, null)
                set i = i+1
            endloop
         
            call TriggerAddCondition(t, Condition(function thistype.onUpgradeCanceled))
         
            //RESEARCH CANCEL
            set i = 0
            set t = CreateTrigger()
         
            loop
                exitwhen i == bj_MAX_PLAYER_SLOTS
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_RESEARCH_CANCEL, null)
                set i = i+1
            endloop
         
            call TriggerAddCondition(t, Condition(function thistype.onResearchCanceled))
         
            set .snd_error = CreateSound("Sound\\Interface\\Error.wav", false, false, false, 0, 0, "DefaultEAXON")
            call SetSoundVolume(.snd_error, 127 )
        endmethod

    endstruct

    struct Harvester
 
        static integer  count           = 0
        private boolean inlist          = false
 
        thistype        next            = 0
        thistype        prev            = 0

        unit            u
        Resource        target_type     = 0
        Resource        carried_type    = 0
        integer         carried_amount  = 0
        destructable    target          = null

        private static timer cooldown
        private static thistype array harvesters //FOR INDEXER VERSION
     
        static method operator first takes nothing returns thistype
            return thistype(0).next
        endmethod
     
        static method operator last takes nothing returns thistype
            return thistype(0).prev
        endmethod
     
        method listRemove takes nothing returns nothing
            if not .inlist then
                return
            endif
            set .inlist = false
            set .prev.next = next
            set .next.prev = prev
            set .count = .count - 1
         
            set .target_type = 0
         
            if .count == 0 then
                call PauseTimer(.cooldown)
            endif
        endmethod

        static method create takes unit u returns thistype
            local thistype this = thistype.allocate()
         
            set .u = u
         
            static if USE_INDEXER then
                set .harvesters[GetUnitUserData(u)] = this
            else
                call SaveInteger(Hashtbl, HARVESTER_KEY, GetHandleId(u), this)
            endif
         
            return this
        endmethod

        static method get takes unit u returns thistype
            static if USE_INDEXER then
                if .harvesters[GetUnitUserData(u)] != 0 then
                    return .harvesters[GetUnitUserData(u)]
                endif
            else
                if HaveSavedInteger(Hashtbl, HARVESTER_KEY, GetHandleId(u)) then
                    return LoadInteger(Hashtbl, HARVESTER_KEY, GetHandleId(u))
                endif
            endif
            return 0
        endmethod
     
        static method isUnitHarvester takes unit u returns boolean
            local Resource r = Resource.first
         
            loop
                exitwhen r == 0
                if GetUnitAbilityLevel(u, r.abilid) > 0 then
                    return true
                endif
                set r = r.next
            endloop
            return false
        endmethod
     
        static method remove takes unit u returns boolean
            local thistype this = thistype.get(u)
            static if USE_INDEXER then
                if this != 0 then
                    call .listRemove()
                    call .destroy()
                    call RemoveSavedInteger(Hashtbl, HARVESTER_KEY, GetHandleId(u))
                    return true
                endif
            else
                if this != 0 then
                    call .listRemove()
                    call .destroy()
                    set .harvesters[GetUnitUserData(u)] = 0
                    return true
                endif
            endif
            return false
        endmethod
     
        method returnResource takes nothing returns nothing
            call .listRemove()
            if not IssueTargetOrder(.u, DROP_ORDER, .carried_type.getClosestDropoff(GetUnitX(.u), GetUnitY(.u), GetOwningPlayer(.u))) then
                debug call BJDebugMsg(MSG_ERROR+"Failed to cast drop ability!")
            endif
        endmethod
     
        method setCarried takes Resource setTo returns nothing
            call AddUnitAnimationProperties(.u, .carried_type.animtag, false)
            call AddUnitAnimationProperties(.u, setTo.animtag, true)
            set .carried_type = setTo
            set .carried_amount = 0
        endmethod
     
        private method addTextTag takes nothing returns nothing
            local texttag tt = CreateTextTag()
            local real vel = 4.5 / 128
            local real xvel = vel * Cos(45 * bj_DEGTORAD)
            local real yvel = vel * Sin(45 * bj_DEGTORAD)
         
            call SetTextTagText(tt, .carried_type.color+"+"+I2S(.carried_amount), 0.023)
            call SetTextTagPos(tt, GetUnitX(.u), GetUnitY(.u), 100)
            call SetTextTagColor(tt, 255, 215, 255, 255)
            call SetTextTagVelocity(tt, xvel, yvel)
            call SetTextTagLifespan(tt, 1)
            call SetTextTagFadepoint(tt, 0)
            call SetTextTagPermanent(tt, false)
         
            set tt = null
        endmethod

        static method filterHarvester takes nothing returns boolean
            if .isUnitHarvester(GetFilterUnit()) then
                call thistype.create(GetFilterUnit())
            endif
            return false
        endmethod

        private static method registerInRect takes rect r returns nothing
            call GroupEnumUnitsInRect(ENUM_GROUP, r, Filter(function thistype.filterHarvester))
        endmethod
     
        private static method registerAll takes nothing returns nothing
            call DestroyTimer(GetExpiredTimer())
            call thistype.registerInRect(bj_mapInitialPlayableArea)
        endmethod
     
        private static method onUpdate takes nothing returns nothing
            local thistype this = .first
            local thistype temp
         
            //THIS IS NECESSARY IN CASE THE SPELL IS ON COOLDOWN

            loop
                exitwhen this == 0
                set temp = .next
                 
                if .target == null or GetDestructableLife(.target) < 0.4 then
                    //TRY TO FIND ANOTHER SOURCE IF OURS IS INVALID
                    if .target_type != 0 then
                        set .target = .target_type.getClosestSource(GetUnitX(.u), GetUnitY(.u), 1000)
                    elseif .carried_type != 0 then
                        set .target = .carried_type.getClosestSource(GetUnitX(.u), GetUnitY(.u), 1000)
                    endif
                endif
             
                if .target == null then
                    //IF WE STILL COULDN'T FIND ANY, MIGHT ASWELL GO HOME
                    if .carried_amount > 0 then
                        call .returnResource()
                    else
                        call IssueImmediateOrder(.u, "stop")
                        //WE NO LONGER NEED TO CHECK THIS UNIT:
                        call .listRemove()
                    endif
                else
                    if .target_type != 0 then
                        call IssueTargetOrderById(.u, .target_type.orderid, .target)
                    else
                        debug call BJDebugMsg(MSG_ERROR+"Target type undefined!")
                    endif
                endif

                set this = temp
            endloop
        endmethod
     
        method listAdd takes nothing returns nothing
            if .inlist or this==0 then
                return
            endif
            set .inlist = true
            set .last.next = this
            set .prev = .last
            set thistype(0).prev = this
            set .next = thistype(0)
            set .count = .count + 1
         
            if .count == 1 then
                call TimerStart(.cooldown, INTERVAL, true, function thistype.onUpdate)
            endif
        endmethod

        private static method onHarvest takes nothing returns boolean
            local thistype this = thistype.get(GetTriggerUnit())
            local integer p = GetPlayerId(GetOwningPlayer(GetTriggerUnit()))
            local destructable targ = GetSpellTargetDestructable()
            local Resource r
         
            if this == 0 or targ == null then
                //INVALID HARVESTER OR TARGET!
                return false
            endif
         
            set r = Resource.get(targ)
         
            if r != 0 and GetSpellAbilityId() == r.abilid then
                if .carried_type != r or .carried_amount == 0 then
                    call .setCarried(r)
                endif
             
                set .target = targ
             
                if .carried_amount >= .carried_type.max_carried then
                    set .carried_amount = .carried_type.max_carried
                    call SetUnitAnimation(.u, "stand"+" "+.carried_type.animtag)
                    call .returnResource()
                    set targ = null
                    return false
                else
                    call SetUnitAnimation(.u, .carried_type.anim+" "+.carried_type.animtag)
                    if not .inlist then
                        set .target_type = r
                        call .listAdd()
                    endif
                endif
             
                if GetWidgetLife(targ) < .carried_type.harvest_rate then
                    set .carried_amount = R2I(.carried_amount + GetWidgetLife(targ))
                else
                    set .carried_amount = .carried_amount + .carried_type.harvest_rate
                    if r.useHitAnim then
                        call SetDestructableAnimation(.target, "stand hit")
                        call QueueDestructableAnimation(.target, "stand")
                    endif
                endif
             
                call SetWidgetLife(.target, GetWidgetLife(.target)-.carried_type.harvest_rate)
             
                if GetWidgetLife(.target) < 0.4 and .carried_amount < .carried_type.max_carried then
                    set .target = .carried_type.getClosestSource(GetUnitX(.u), GetUnitY(.u), SEARCH_RADIUS)
                endif
            endif
         
            set targ = null
            return false
        endmethod

        private static method onOrder takes nothing returns boolean
            local thistype this = thistype.get(GetTriggerUnit())
            local destructable d = GetOrderTargetDestructable()
            local unit u = GetOrderTargetUnit()
            local integer orderid = GetIssuedOrderId()
            local Resource r
         
            if this == 0 then
                set u = null
                set d = null
                return false
            endif
         
            if not(orderid == OrderId("smart") or orderid == OrderId("attack")) then
                set r = Resource.first
             
                loop
                    exitwhen r == 0
                 
                    if orderid == r.orderid then
                        set u = null
                        set d = null
                        return false
                    endif
                 
                    set r = r.next
                endloop
                //ORDER IS NOT RESOURCE RELATED - REMOVE FROM HARVESTER LOOP!
                call .listRemove()
                set u = null
                set d = null
                return false
            endif
         
            //TARGET IS UNIT
            if u != null then
                if .carried_type.isUnitDropoff(u) and GetOwningPlayer(u) == GetOwningPlayer(.u) then
                    if IsUnitConstructing(u) == false then
                        //THE TARGET IS A DROPOFF, DROP RESOURCES
                        call IssueTargetOrder(.u, DROP_ORDER, u)
                    endif
                endif

                set u = null
                set d = null
                return false
            endif
         
            //TARGET IS DESTRUCTABLE
            if d == null then
                //THIS FIXES A BUG CAUSED BY FOG OF WAR, WHERE A SMART ORDER WOULD RETURN NO DESTRUCTABLE.
                set d = Resource.getClosestDestructable(GetOrderPointX(), GetOrderPointY(), 80)
            endif
         
            if d != null then
                set r = Resource.get(d)
                //IF WE FOUND A DESTRUCTABLE, BUT IT IS NOT A RESOURCE, WE CAN TERMINATE THE FUNCTION.
                if r == 0 then
                    set u = null
                    set d = null
                    return false
                endif
             
                if GetUnitAbilityLevel(.u, r.abilid) == 0 or IssueTargetOrderById(.u, r.orderid, d) == false then
                    //WE NEED TO STOP UNITS FROM INSTINCTIVELY ATTACKING RESOURCES.
                    call PauseUnit(.u, true)
                    call IssueImmediateOrder(.u, "stop")
                    call PauseUnit(.u, false)
                    call SetUnitAnimation(.u, "stand")
                    call IssueTargetOrder(.u, "move", d)
                 
                    if GetUnitAbilityLevel(.u, r.abilid) == 0 then
                        //call BJDebugMsg(MSG_ERROR+"Failed to target resource - Unit does not have required harvest ability.")
                    else
                        //call BJDebugMsg(MSG_ERROR+"Failed to target resource - Ability is on cooldown or cannot target destructable.")
                    endif
                endif
             
                if GetUnitAbilityLevel(.u, r.abilid) > 0 then
                    //IF TARGET IS A RESOURCE, ADD THE HARVESTER TO THE LOOP AGAIN:
                    set .target = d
                    set .target_type = r
                    call .listAdd()
                else
                    call .listRemove()
                endif
            else
                call .listRemove()
            endif
         
            set u = null
            set d = null

            return false
        endmethod

        private static method onDrop takes nothing returns boolean
            local thistype this = thistype.get(GetTriggerUnit())
            local integer p = GetPlayerId(GetOwningPlayer(GetTriggerUnit()))
            local unit target = GetSpellTargetUnit()
         
            if this == 0 or .carried_type == 0 or (GetOwningPlayer(GetTriggerUnit()) !=  GetOwningPlayer(target)) then
                set target = null
                return false
            elseif GetSpellAbilityId() != DROP_ABIL or .carried_type.isUnitDropoff(target) == false or IsUnitConstructing(target) then
                //NO NEED TO CONTINUE IF TARGET UNIT IS NOT A DROPOFF
                set target = null
                return false
            endif
         
            if .carried_amount > 0 then
                //DROP RESOURCES IF WE HAVE ANY
                set PlayerResource[.carried_type] = PlayerResource[.carried_type] + .carried_amount
                call .addTextTag()
                set .carried_amount = 0
                call AddUnitAnimationProperties(.u, .carried_type.animtag, false)
            endif
     
            if .target == null or GetDestructableLife(.target) <= 0 then
                //TRY TO FIND A NEW SOURCE IF THE OLD ONE IS GONE
                set .target = .carried_type.getClosestSource(GetUnitX(.u), GetUnitY(.u), 1000)
            endif
         
            if .target != null then
                call IssueTargetOrderById(.u, .carried_type.orderid, .target)
                set .target_type = .carried_type
                call .listAdd()
            endif
         
            set target = null
            return false
        endmethod
     
        private static method onEnterMap takes nothing returns boolean
            if .isUnitHarvester(GetTriggerUnit()) then
                call .create(GetTriggerUnit())
            endif
            return false
        endmethod
     
        private static method onDeath takes nothing returns boolean
            local thistype this = thistype.get(GetTriggerUnit())
            if this != 0 then
                call .remove(GetTriggerUnit())
            endif
            return false
        endmethod

        private static method onInit takes nothing returns nothing
            local trigger t = CreateTrigger()
            local region r = CreateRegion()
            local integer i = 0
         
            set .cooldown = CreateTimer()
         
            //SOME TRIGGERS WE NEED:
         
            //REGISTER ORDERS
            loop
                exitwhen i == bj_MAX_PLAYER_SLOTS
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER, null)
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, null)
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_SPELL_CHANNEL, null)
                set i = i+1
            endloop
     
            call TriggerAddCondition(t, Condition(function thistype.onOrder))
         
            //REGISTER HARVEST SPELL
            set i = 0
            set t = CreateTrigger()
         
            loop
                exitwhen i == bj_MAX_PLAYER_SLOTS
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
                set i = i+1
            endloop
         
            call TriggerAddCondition(t, Condition(function thistype.onHarvest))
         
            //REGISTER DROPOFF SPELL
            //I WAS NOT SURE IF ONE CONDITION RETURNING FALSE CAUSE THE GAME TO SKIP THE NEXT ONE, SO I SPLIT THIS INTO TWO TRIGGERS.
            set i = 0
            set t = CreateTrigger()
         
            loop
                exitwhen i == bj_MAX_PLAYER_SLOTS
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
                set i = i+1
            endloop
         
            call TriggerAddCondition(t, Condition(function thistype.onDrop))
         
            //REGISTER HARVESTER DEATH
            set i = 0
            set t = CreateTrigger()
         
            loop
                exitwhen i == bj_MAX_PLAYER_SLOTS
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_DEATH, null)
                set i = i+1
            endloop
         
            call TriggerAddCondition(t, Condition(function thistype.onDeath))
         
            //REGISTER HARVESTER ENTER MAP
            set t = CreateTrigger()
            call RegionAddRect(r, bj_mapInitialPlayableArea)
            call TriggerRegisterEnterRegion(t, r, null)
            call TriggerAddCondition(t, Condition( function thistype.onEnterMap))
         
            //REGISTER ALL EXISTING HARVESTERS:
            call TimerStart(CreateTimer(), 0, false, function thistype.registerAll)
         
            set t = null
            set r = null
        endmethod

    endstruct

endlibrary

JASS:
library ResourceSetup initializer Init requires ResourceLibrary

    /*
    HOWTO:
 
    //GET THE NAME OF YOUR RESOURCE
 
        set udg_myString = MyResource.name
 
    //Example:
 
        set udg_woodName = Wood.name
 
    //GET THE ICON OF YOUR RESOURCE
 
        set udg_myString = MyResource.icon
 
    //Example:
 
        set udg_woodIcon = Wood.icon
 
    //GET THE AMOUNT OF RESOURCE ACCUMULATED BY PlAYER

        set udg_player_resource = PlayerResource[playerid][resource]
 
    //Example:
 
        set udg_player1_Wood = PlayerResource[0][Wood]
        set udg_player2_Gold = PlayerResource[1][Gold]
   
    //Note that player id:s start from 0 in JASS!
    //This means Player 1 is 0, Player 2 is 1, etc.
 
    */
 

    globals
     
        //Here you can specify the names of your resources.
        //Just type "resource", space, and the name you want it to have. You can have up to 20 resources,
        //If you need more, you can change the "MAX_RESOURCES" constant in the resource library up to a
        //maximum of 738 resources.
      
        Resource Wood
        Resource Food
        Resource Gold
        Resource Iron
        Resource Stone
     
    endglobals
 
    private function Init takes nothing returns nothing
 
        //This is where you create the actual resource. 
        //"harvest" is the raw ability ID of your harvest ability. I recommend that you simply copy the ability used in the test map. 
        //"ordername", in this case "cripple", is the order used to cast the "harvest" ability.
     
        ///static method create takes string name, string icon, string anim, string animtag, string color, integer harvestrate, integer maxcarried, integer harvest, string ordername returns resource
     
        set Wood = Resource.create("Wood", "ReplaceableTextures\\CommandButtons\\BTNBundleOfLumber.blp", "work", "lumber", "|cff006400", 2, 10, 'harv', "cripple")
        set Gold = Resource.create("Gold", "ReplaceableTextures\\CommandButtons\\BTNChestOfGold.blp", "work", "gold", "|cffffd700", 2, 10, 'harv', "cripple")
        set Stone = Resource.create("Stone", "ReplaceableTextures\\CommandButtons\\BTNGolemThunderclap.blp", "work", "gold", "|cffa0a0a0", 2, 10, 'harv', "cripple")
        set Food = Resource.create("Food", "ReplaceableTextures\\CommandButtons\\BTNMonsterLure.blp", "work", "gold", "|cffff4500", 2, 10, 'harv', "cripple")
     
        //To make a unit into a dropoff point, you add a special ability to it. You can register a dropoff ability for each individual resource, or use the same, as you please.
        //If you don't want the dropoff ability to be seen, just use one which doesn't shown an icon, such as "sphere", "channel", or "item armor bonus".
        //This is how you registrer dropoff abilities:
     
        call Wood.registerDropoffAbil('doff')
        call Gold.registerDropoffAbil('doff')
        call Stone.registerDropoffAbil('doff')
        call Food.registerDropoffAbil('doff')
     
        //This is how you register what destructables constitute your resource.
        //The only requirement is that they are destructables, and that they don't use the "tree" classification.
     
        call Wood.registerSource('tre0')
        call Stone.registerSource('ston')
        call Gold.registerSource('gold')
        call Food.registerSource('shee')
        call Food.registerSource('stag')
     
        //The below line disables hit animations (such as tree shake) for a certain resource.
        set Food.useHitAnim = false
     
        //Finally, you might want to add your custom resource costs to your units and buildings. This is quite tedious work,
        //but you will instead not have to bother with adding costs in the object editor. This is how you register your costs:
     
        //takes integer unitid, resource whichresource, integer amount
        //Peasant
        call RegisterCost('hpea', Gold, 50)
        call RegisterCost('hpea', Food, 50)
     
        //Footman
        call RegisterCost('hfoo', Gold, 75)
        call RegisterCost('hfoo', Food, 50)
     
        //Rifleman
        call RegisterCost('hrif', Gold, 25)
        call RegisterCost('hrif', Food, 50)
        call RegisterCost('hrif', Wood, 25)
     
        //Barracks
        call RegisterCost('hbar', Gold, 100)
        call RegisterCost('hbar', Wood, 220)
        call RegisterCost('hbar', Stone, 50)
     
        //Keep
        call RegisterCost('hkee', Gold, 180)
        call RegisterCost('hkee', Wood, 280)
        call RegisterCost('hkee', Stone, 65)
     
        //Town Hall
        call RegisterCost('htow', Gold, 120)
        call RegisterCost('htow', Wood, 240)
        call RegisterCost('htow', Stone, 50)
     
        //Town Hall
        call RegisterCost('hbla', Gold, 80)
        call RegisterCost('hbla', Wood, 180)
        call RegisterCost('hbla', Stone, 35)
     
        //Lumber Mill
        call RegisterCost('hlum', Gold, 50)
        call RegisterCost('hlum', Wood, 120)
     
        //Scout Tower
        call RegisterCost('hwtw', Gold, 50)
        call RegisterCost('hwtw', Wood, 100)
        call RegisterCost('hwtw', Stone, 50)
     
        //Guard Tower
        call RegisterCost('hgtw', Gold, 25)
        call RegisterCost('hgtw', Wood, 50)
        call RegisterCost('hgtw', Stone, 25)
     
        //Farm
        call RegisterCost('hhou', Gold, 35)
        call RegisterCost('hhou', Wood, 90)
     
        //Defend
        call RegisterCost('Rhde', Gold, 100)
        call RegisterCost('Rhde', Wood, 150)
     
        //Long Rifles
        call RegisterCost('Rhri', Gold, 150)
        call RegisterCost('Rhri', Wood, 75)
     
     
        //Setup starting resources
        set PlayerResource[0][Food] = 200
        set PlayerResource[0][Gold] = 300
        set PlayerResource[0][Stone] = 100
        set PlayerResource[0][Wood] = 300
     
    endfunction
endlibrary

JASS:
library ResourceMultiBoard initializer Init requires ResourceLibrary

    globals
        constant real UPDATE_INTERVAL = 0.5
        multiboard array ResourceDisplay
    endglobals
 
    private function Update takes nothing returns nothing
        local integer i = 0
        local Resource r
        local multiboarditem mb
     
        loop
            exitwhen i > 11
            set r = Resource.first
            loop
                exitwhen r == 0
                set mb = MultiboardGetItem(ResourceDisplay[i], r-1, 2)
                call MultiboardSetItemValue(mb, I2S(PlayerResource[i][r]))
                call MultiboardReleaseItem(mb)
                set r = r.next
            endloop
            set i = i+1
        endloop
     
        set mb = null
    endfunction
 
    private function Setup takes nothing returns nothing
        local integer i = 0
        local Resource r
        local multiboarditem mb
        local timer t = CreateTimer()
     
        call DestroyTimer(GetExpiredTimer())
     
        loop
            exitwhen i > 11
         
            set ResourceDisplay[i] = CreateMultiboard()
            call MultiboardSetRowCount(ResourceDisplay[i], Resource.count)
            call MultiboardSetColumnCount(ResourceDisplay[i], 3)
            call MultiboardSetTitleText(ResourceDisplay[i], "Resources:")
         
            if i == 0 then
                call MultiboardDisplay(ResourceDisplay[i], true)
            else
                call MultiboardDisplay(ResourceDisplay[i], false)
            endif
         
            set r = Resource.first
            loop
                exitwhen r == 0
                set mb = MultiboardGetItem(ResourceDisplay[i], r-1, 0)
                call MultiboardSetItemStyle(mb, false, true )
                call MultiboardSetItemIcon(mb, r.icon)
                call MultiboardSetItemWidth( mb, 0.015)
                call MultiboardReleaseItem(mb)
                set r = r.next
            endloop
         
            set r = Resource.first
            loop
                exitwhen r == 0
                set mb = MultiboardGetItem(ResourceDisplay[i], r-1, 1)
                call MultiboardSetItemStyle(mb, true, false)
                call MultiboardSetItemValue(mb, r.name+":")
                call MultiboardSetItemWidth(mb, 0.045)
                call MultiboardReleaseItem(mb)
                set r = r.next
            endloop
         
            set r = Resource.first
            loop
                exitwhen r == 0
                set mb = MultiboardGetItem(ResourceDisplay[i], r-1, 2)
                call MultiboardSetItemStyle(mb, true, false)
                call MultiboardSetItemValue(mb, I2S(PlayerResource[i][r]))
                call MultiboardSetItemWidth(mb, 0.03)
                call MultiboardReleaseItem(mb)
                set r = r.next
            endloop
         
            set i = i+1
        endloop
     
        call MultiboardDisplay(ResourceDisplay[0], true)
        call TimerStart(t, UPDATE_INTERVAL, true, function Update)
     
        set mb = null
        set t = null
     
    endfunction
 
    private function Init takes nothing returns nothing
        local timer t = CreateTimer()
        call TimerStart(t, 0.5, false, function Setup)
        set t = null
    endfunction

endlibrary
Contents

Resource System (Map)

I will test this later on, but I suppose there must be something unique with this that warrants another attempt at a custom resource system.

Until then, I will ask: What does this resource offer compared to some Custom Resource bundles already posted here? I want to have some frame of reference for dealing with these.
 
Last edited:
I actually wasn't aware of any other resource systems until i started searching, though it seems like this one is similar, and has some featuers which this one doesn't (more multiboard features, resources with no need for drop-off, harvested amount different from damaged amount, units or items as resources). However, i noticed some drawbacks it has over this one:
  • Mine supports animation tags for carrying resources, which his strangely doesn't seem to do.
  • His system works by hooking events to any kind of construction, training, selling, etc. and then cancel that event - while mine works by simply assigning a cost to an order (training and constructions are essentially orders in Wc3, with the unit's raw ID as order id) - and then canceling that order if the player can't "afford" it. This way, my system is a lot more compact than his and as a side effect allows for virtually any order to be assigned a cost.
  • My system uses dropoff abilities to define dropoff points, which in my opinion is more intuitive to configure and also slightly more efficient.
  • There is this weird bug thing in Warcraft where targeting destructibles inside the fog of war would not properly return the destructible as the order's target. My system compensates for this by searching for the destructable using a rect. I don't think his does this.
  • My system has no external dependencies (though a unit indexer is optional).
  • The visual appearance of my resource gathering is almost indistinghishable from that of vanilla Wc3 - namely, the harvest ability shows up as channeling while harvesting, "return resource" shows as channeling while returning it, and animation tags are properly assigned. This is not the case with his system.

This one is a resource system, but only supports a single custom resource on top of the existing ones. This one is an absolute mess and has many weird limitations which this system doesn't have. Overall, i think compactness and ease of use are the benefits of my system.
 
I did not test it yet, but judging by the setup demo is there a requirement to setup lumber and gold resources to make the system work? or is that just for showcase?

You can use any resource names you want - wood, gold, etc. are just examples. Actually, you don't really need anything from the "setup" part at all, you can register things yourself as you want. Basically, what you need is a globals block where you define your resource globals, and an initialization function of some sorts which creates the resources and assigns costs. The "ResourceSetup" part just shows how the different methods are used.

This system seems easier to introduce in to existing maps and configure it than the other ones available on Hive.

Though how would you go about adding a trading system so you can give other players resources.

Do you mean like a tribute system? Transferring resources is done like this:

JASS:
// Let's say that we have GUI integer globals ReceivingPlayerId and SendingPlayerId
// As well as a real global TributeAmount

set PlayerResource[udg_ReceivingPlayerId][Wood] = PlayerResource[udg_ReceivingPlayerId][Wood] + udg_TributeAmount
set PlayerResource[udg_SendingPlayerId][Wood] = PlayerResource[udg_SendingPlayerId][Wood] - udg_TributeAmount

Of course, on top if this you might need some sort of dialog system or hero menu.
 

System Review:


Notes:

  • This system is a self-contained library, with additional functionalities based on the whims of the user. With that said, usage of other libraries, as optional ones, would be beneficial for the user, since certain things handled by the system that it should not be involved with are taken care of, if these libraries exist. With that in mind, a possible collision might arise when the system overrides the inner mechanics of a UnitIndexer used by the end-user, or cause the total hashtable count to exceed 255, which isn't likely to happen, but must not be downplayed if such a case occurs.

    Thus, I would recommend introducing these optional libraries (these are not the only ones you can add, though): [System] UnitDex - Unit Indexer, [Snippet] New Table.

  • In LIBRARY_ResourceLibrary, a certain function has the same name as the ai native GetUnitBuildTime. I would suggest adding Ex to it, just in case such a native is used by the end-user.

  • A certain minor lag-phase occurs if you issue multiple units to "harvest" a targeted destructable. This is due to repeated calls to EnumDestructablesInRect, which could be the probable cause. This only affects low-end computers, but optimization concerning this is recommended (I wonder what solution to this will the update offer).

  • On structs Resource and Harvester, the initialization process is of type struct-initialization. In Vexorian's vJASS compiler, this has a lower priority over module initializers. This could be potentially troublesome, as the events by which the system runs upon might be also found in other libraries, which, due to them having potentially been initialized first (module), might affect the way the system works. This is only reserved to listener functions.

    Syntax of module initializers:
    JASS:
    module M
        private static method onInit takes nothing returns nothing
        endmethod
    endmodule
    struct S
        implement M
    endstruct

  • In Resource::onInit and Harvester::onInit, some lines are unnecessarily verbose. These particular lines follow this paradigm:

    JASS:
    loop
        exitwhen i == bj_MAX_PLAYER_SLOTS
        call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
        set i = i+1
    endloop

    In that case above, you can just use the BJ-function equivalent of those lines, saving you some time, and grants a little bit more of readability. This is only reserved to such loops that invoke TriggerRegisterPlayerUnitEvent once and only once.
There are some more cases which might cause this system to bug, but I fear that I cannot disclose it in this review. So, I will just mention them on the messages hereafter.

Status:

  • Awaiting Update
 
Uhm.... thank you for your review MyPad, though let me be straight... I've never encountered anything as infuriatingly dogmatic and arbitrary on this site as the spell reviewing process. So, brace for a huge rant.


25352934.gif

First of all, this:

Thus, I would recommend introducing these optional libraries (these are not the only ones you can add, though): [System] UnitDex - Unit Indexer, [Snippet] New Table.

The last system i uploaded was sent to "substandard" partly on the basis that it used UnitDex, which apparently was considered nonstandard and unreliable. Now, i'm getting pushback for NOT bundling UnitDex with it, despite a unit indexer being an optional feature to be enbled by the user if needed. Some trivia: I have also previously gotten crap over using a timer interval of 0.03 instead of 0.03125 (no, I'm not kidding).

With that said, usage of other libraries, as optional ones, would be beneficial for the user, since certain things handled by the system that it should not be involved with are taken care of, if these libraries exist. With that in mind, a possible collision might arise when the system overrides the inner mechanics of a UnitIndexer used by the end-user, or cause the total hashtable count to exceed 255, which isn't likely to happen, but must not be downplayed if such a case occurs.

This is such a massive non-issue. No, the system does not override the inner mechanics of any unit indexer (what is that even supposed to mean?), the whole point of an indexer is to generate an index-friendly ID for each unit for array storage (hence inherently no limitations or collisions with other systems). This system does not modify unit custom data in any way. As for Tables, this system uses both the parent and child keys to store certain data, Table is just for associating a single key with a value. I will not rewrite this whole system (which is already well utilizing a single hashtable) just in the remote and incredibly unlikely event that someone would have more than 256 hashtables in their map.

In LIBRARY_ResourceLibrary, a certain function has the same name as the ai native GetUnitBuildTime. I would suggest adding Ex to it, just in case such a native is used by the end-user.

This is legit criticism, mainly because that function is not actually utilized at all in my system and is likely just legacy from an older version.

A certain minor lag-phase occurs if you issue multiple units to "harvest" a targeted destructable. This is due to repeated calls to EnumDestructablesInRect, which could be the probable cause. This only affects low-end computers, but optimization concerning this is recommended (I wonder what solution to this will the update offer).

I guess it could be possible to cache the destructible search result per frame. I will see what i can do.

On structs Resource and Harvester, the initialization process is of type struct-initialization. In Vexorian's vJASS compiler, this has a lower priority over module initializers. This could be potentially troublesome, as the events by which the system runs upon might be also found in other libraries, which, due to them having potentially been initialized first (module), might affect the way the system works. This is only reserved to listener functions.

Considering that only the events are registered in onInit (resources are to be registered in library Init), i really don't see how this could cause issues with anything. If you can actually present me with a case where this could cause a failure, I'll change it.

In Resource::onInit and Harvester::onInit, some lines are unnecessarily verbose. These particular lines follow this paradigm:

JASS:
loop
    exitwhen i == bj_MAX_PLAYER_SLOTS
    call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
    set i = i+1
endloop

Though i'm generally in favor of increased readability, i think this is a matter of personal preference more than anything else. In my case, i suppose i just don't like the red highlight which TESH gives BJ functions, since it kinda reminds me of error highlighting.

There are some more cases which might cause this system to bug, but I fear that I cannot disclose it in this review. So, I will just mention them on the messages hereafter.

Please do.
 
Ah, I see why you haven't incorporated some of these helper libraries (the optional ones).

Perhaps I haven't read the system extensively enough, so I'll agree with the custom user data not being an issue.

Considering that only the events are registered in onInit (resources are to be registered in library Init), i really don't see how this could cause issues with anything.

This is a very rare case, but suppose some trigger, which was initialized first via module initializer, was registered to an onOrder event. Upon invocation of the event, it may do something with the unit that would deem it invalid.

Once the trigger in the system listens to that event, it would be working with a unit which may not be capable of doing these things. That is why I suggested a module initializer over a struct initializer.

The last system i uploaded was sent to "substandard" partly on the basis that it used UnitDex, which apparently was considered nonstandard and unreliable. Now, i'm getting pushback for NOT bundling UnitDex with it, despite a unit indexer being an optional feature to be enbled by the user if needed.

You can indicate through documentation that the end-user can choose any unit indexer, and the system only works with the presence of such a system, should USE_INDEXER be true.

Mind if I'll ask for the link for that? I'd like to correct that (if in Spells)

As for Tables, this system uses both the parent and child keys to store certain data, Table is just for associating a single key with a value.

Mmmh, you might have a point there. In the place of my previous suggestion on this, you can add a disclaimer about the hashtable count.
 
All right, i will add some extra documentation then. Regarding the onInit part, i'm still not convinced that this is an issue. Unless you explicitly state library dependencies, libraries will also be initialized in an arbitrary order, so there is no guarantee that other systems won't fiddle with the unit state. If you wanted to make it watertight in that regard, you'd have to add sanity checks in each trigger event. I just think it looks neater to initialize triggers in the scope of the struct they belong to, rather than having it all in one big clusterfuck at the bottom. This is just my personal preference, though.


Here is the system in question:

Realistic Airplane System

To be fair, there were a couple of valid points besides using UnitDex, though those have been since long fixed. Would be great if you could take a look at it.
 
Hmm... inspecting the code further (this resource), I found out the following:

Comments/Bug Reports:
  • <cond> == false, can be simplified into: not <cond>. There are a lot of these within the main library.

  • In Harvester::get, the case where no value supposedly exists in a specified index will not happen, in a way such that the returned value will be the same as that of the default 0 anyway.

  • In Resource::onBuildingConstructed, when the building gets removed, the building's user data should also be cleared (this is only the logical process of the removal of a unit, not a recommended course of action). The system assumes that this will never be the case, and will proceed, regardless of whether the building's user data exists or not. The same reasoning would apply with the handle id.

  • Mostly a tip, you can make functions that are used as triggerconditions return nothing. This is possible!

  • In the first globals block, there is a variable integer array PlayerResource[11][MAX_RESOURCES]. Any reason as to why the number 11 was chosen? (A magic number)

  • There is an odd bug, where a building that is still undergoing construction can be used as a return resource spot.

  • In Harvester, there is a static array member harvesters. This gets generated regardless of the USE_INDEXER flag being true or not.

  • In Harvester::remove(unit u), the logic of accessing the stored data is mixed. If USE_INDEXER is true, the method will instead invoke a hashtable call, and vice versa.
Unless you explicitly state library dependencies, libraries will also be initialized in an arbitrary order, so there is no guarantee that other systems won't fiddle with the unit state.

That is correct, and something that would be enforced in a system, should it have to use external dependencies.

If you wanted to make it watertight in that regard, you'd have to add sanity checks in each trigger event.

That is what I desire when looking at systems in the spell section, making sure that no edge-cases can possibly jeopardize the system, or if it cannot be helped, minimize its' impacts.

I think I might have not covered some more edge-cases, and I haven't figured out how to optimize the system in the case of multiple units being ordered to target a "resource" at the same time.

Realistic Airplane System

I'll check out the link and see what I could find.
 
Something like this, in Jass:

JASS:
local integer playerId = GetPlayerId(GetTriggerPlayer())

if PlayerResource[playerId][Stone] >= 100 then
    set PlayerResource[playerId][Stone] = PlayerResource[playerId][Stone] - 100
    set PlayerResource[playerId][Gold] = PlayerResource[playerId][Gold] + 100
else
    // Do something to prompt the player that he doesn't have enough stone
endif

Though i suppose the easiest way is to register a cost to the ability ID of your trade ability. There is a function called RegisterCost, and although it was primarily meant to assign cost to units, it automatically works for spells since both spells and construction/training are just orders.

Put this in some initialization function, you can for instance set it somewhere in the Resource Setup trigger with all the other initialization stuff:

JASS:
call RegisterCost('a001', Gold, 100)

The benefit of this is that the player gets a message automatically when they can't afford the transaction. However, knowing when to give the gold is harder... you'd have an event when the spell is cast, but to know whether it passes or not, you'd probably still need to make the same check as above.
 
Level 6
Joined
Aug 2, 2009
Messages
128
Ok, so like this?:
  • Events
  • Unit - A unit starts casting an ability
  • Conditions
  • (Ability-type of (casting ability)) Equal to Sell Stone
  • Actions
  • Custom Script - local integer playerId = GetPlayerId(GetTriggerPlayer())
  • Custom Script - if PlayerResource[playerId][Stone] >= 50 then
  • Custom Script - set PlayerResource[playerId][Stone] = PlayerResource[playerId][Stone] - 50
  • Player - add 35 to (Owner of (casting unit)) Current gold
  • Custom Script - else
  • Sound - play error
  • Game - Display to (Owner of (casting unit)) the text: Not enough stone!
 
Level 1
Joined
Oct 12, 2019
Messages
3
I had a problem with buying items ... The fact is that the system does not use a custom resource when buying items. Can this be solved somehow?
 
Level 39
Joined
Feb 27, 2007
Messages
4,994
Not in the regular ‘hover over object to buy’ interface. They can’t be made to show up there. You can detect item/unit buy events and try to subtract the appropriate resource (or refund purchase if insufficient) by reading from some precached array/hashtable database.
 
Level 1
Joined
Apr 12, 2019
Messages
1
Guys, sorry, but there is some erros happening with my map when I try using this wonderful script, I will post them here! Sorry for been soo dumb, I think it is some very simple.
 

Attachments

  • Error 1 World Edit.jpg
    Error 1 World Edit.jpg
    248 KB · Views: 100
  • Error 2.jpg
    Error 2.jpg
    292.7 KB · Views: 85
  • Error 3.png
    Error 3.png
    159.6 KB · Views: 120
Level 18
Joined
Jan 1, 2018
Messages
728
Any plans on updating this system? I've noticed some issues trying the test map, the most critical being that when I spam the train button fast enough, peasants will be added to the training queue even when I don't have enough resources (and I get free resources when I cancel them again).
 
Level 13
Joined
Jul 2, 2015
Messages
872
Cant seem to get this to function properly, I imported the triggers, abilities, destructibles and then set the unit ideas to them (with DropoffAll being A03Z for ex)

But I still cant seem to get it to work, I right click stone, the peasant starts attacking it instead of harvesting, attacking/killing a sheep to get food results in the sheep dying and a corpse spawning, but the corpse is playing the stand anim's and I cant harvest it.
Here's an screenshot, I wanted to try to use the resources already provided in the system to get a hang of it.
Screenshot (4106).png
 
Top