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

TrainSystem

Level 29
Joined
Mar 10, 2009
Messages
5,016
TrainSystem v1.3

This system is used in my map and works quite well, I want to test it here...

What the hell is this?
- Auto trains a unit one at a time
- This is more dynamic than GUI AI editor as it doesnt recognize priorities, it will train random units
- If the unit is not yet available or needs food/gold/lumber, it will not count the limit
- Limits the number of units trained by a particular structure

JASS:
/*
=====Train System v1.3
=====By Mckill2009

This is a system in which your building will automatically train a unit, this is good for
AI's or computer players...

REQUIRED NATIVE ABOVE MAP SCRIPT:
    native UnitAlive takes unit u returns boolean

API:
    static method register takes unit source, integer limit returns nothing
        - first thing to do
        - registers your source unit that will automatically train units
        - sets the initial limit to train units
    
    static method addTrain takes unit source, integer trainedID returns nothing
        - second thing to do
        - source - the unit that will be training
        - trainedID - the unittypeID of the unit being trained
        - the more trainedID you register, the higher the probability it will be trained in the list
        
    static method setLimit takes unit source, integer limit returns nothing
        - sets/adds the limit of your units being trained
        - the limit must be greater than the limit from initial setup (from register)
    
    static method removeTrain takes unit source returns nothing
        - removes the source from the list or stops the auto train
    
    static method clearAll takes nothing returns nothing
        - removes all units from the list
        - destroys the ID's and starts a fresh one

CREDITS:
- Table by Bribe
- RegisterPlayerUnitEvent by Magtheridon96

OTHER CREDITS:
Magtheridon96 (for suggestions)
Troll-Brain (for tests)
*/

library TrainSystem uses Table, RegisterPlayerUnitEvent

struct TrainSystem
    unit source
    
    static constant integer maxInstance = 8190
    static constant integer childID = 1000
    static integer instance = 0
    static integer array instanceAr
    static timer t = CreateTimer()
    static real interval = 1.0
    static Table lim //limit of units trained by a particular structure
    static Table chk //checks the counter of the trained unit
    static Table tra //trained unit reduce counter
    static TableArray random
    
    private static method looper takes nothing returns nothing
        local thistype this
        local integer looper = 0
        local integer i
        local integer ID
        local integer ran
        local integer array ar
        loop
            set looper = looper + 1
            set this = instanceAr[looper]
            set ID = GetHandleId(.source)
            if UnitAlive(.source) and chk.has(ID) then
                if (lim[ID]) > (chk[ID]) and chk.boolean[ID] then
                    set i = 0
                    loop
                        set i = i + 1
                        set ar[i] = random[i][ID]
                        exitwhen i==random[childID][ID] //maxINDEX
                    endloop
                    set ran = GetRandomInt(1,random[childID][ID])
                    if IssueImmediateOrderById(.source, ar[ran]) then
                        set chk.boolean[ID] = false
                    endif
                endif            
            else
                call chk.remove(ID)
                set .source = null
                call .destroy()
                set instanceAr[looper] = instanceAr[instance]
                set instanceAr[instance] = this
                set looper = looper - 1
                set instance = instance - 1    
                if instance==0 then
                    call PauseTimer(t)
                endif
            endif
            exitwhen looper==instance
        endloop
    endmethod

    private static method deathEvent takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local unit source //the source that trains the dying unit
        local integer ID = GetHandleId(u)
        local integer sourceID        
        if u==tra.unit[ID] then
            set source = chk.unit[ID]
            set sourceID = GetHandleId(source)
            set chk[sourceID] = chk[sourceID] - 1
            set chk.boolean[sourceID] = true
            call tra.remove(ID)
            set source = null
        endif    
        set u = null
    endmethod   
    
    private static method trainCancel takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local unit trained = GetTrainedUnit()
        local integer ID = GetHandleId(u)
        if not chk.boolean[ID] then
            set chk.boolean[ID] = true
            set chk[ID] = chk[ID] - 1
        endif
        set u = null
        set trained = null
    endmethod  
    
    private static method trainEnd takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local unit trained = GetTrainedUnit()
        local integer ID = GetHandleId(u)
        local integer trID = GetHandleId(trained)
        if chk.has(ID) then
            set chk[ID] = chk[ID] + 1
            set chk.boolean[ID] = true
            set chk.unit[trID] = u
            set tra.unit[trID] = trained
        endif
        set u = null
        set trained = null
    endmethod  
    
    private static method onInit takes nothing returns nothing
        call RegisterPlayerUnitEvent(EVENT_PLAYER_UNIT_TRAIN_CANCEL, function thistype.trainCancel)
        call RegisterPlayerUnitEvent(EVENT_PLAYER_UNIT_TRAIN_FINISH, function thistype.trainEnd)
        call RegisterPlayerUnitEvent(EVENT_PLAYER_UNIT_DEATH, function thistype.deathEvent)
        set lim = Table.create()
        set chk = Table.create()
        set tra = Table.create()
        set random = TableArray[0x2000]
    endmethod
    
    //API's==============================
    static method register takes unit source, integer limit returns nothing
        local integer ID = GetHandleId(source)
        local thistype this
        if chk.has(ID) then
            debug call BJDebugMsg("register ERROR: "+GetUnitName(source)+" is already registered!")
        else
            if instance==maxInstance then
                debug call BJDebugMsg("register ERROR: Unable to allocate more than "+I2S(maxInstance)+" instance.")
                call DestroyTimer(t)
            else
                set this = allocate()        
                set .source = source
                set lim[ID] = limit
                set chk[ID] = 0 //checks the limit
                set chk.boolean[ID] = true //cheks if registered or not
                if instance==0 then
                    set t = CreateTimer()
                    call TimerStart(t,interval,true,function thistype.looper)
                endif
                set instance = instance + 1
                set instanceAr[instance] = this
            endif    
        endif
    endmethod
    
    static method addTrain takes unit source, integer trainedID returns nothing
        local integer ID = GetHandleId(source)
        local integer index
        if chk.has(ID) then
            set random[childID][ID] = random[childID][ID]+1 //maxINDEX
            set index = random[childID][ID]
            set random[index][ID] = trainedID //so that all trainedID will be saved in a unique index
        else
            debug call BJDebugMsg("addTrain ERROR: Please register "+GetUnitName(source)+" first.")
        endif
    endmethod
    
    static method setLimit takes unit source, integer limit returns nothing
        local integer ID = GetHandleId(source)
        if chk.has(ID) then
            if (limit) > (lim[ID]) then
                set lim[ID] = limit
            else
                debug call BJDebugMsg("setLimit ERROR: Limit must be greater than registered value!")
            endif
        else
            debug call BJDebugMsg("addLimit ERROR: "+GetUnitName(source)+" is not registered!")
        endif
    endmethod
    
    static method removeTrain takes unit source returns nothing
        local integer ID = GetHandleId(source)
        if chk.has(ID) then
            call lim.remove(ID)
            call chk.remove(ID)
            set random[childID][ID] = 0
        else
            debug call BJDebugMsg("removeTrain ERROR: "+GetUnitName(source)+" is not registered!")
        endif
    endmethod
    
    static method clear takes nothing returns nothing
        call PauseTimer(t)
        call DestroyTimer(t)
        call random.destroy()
        call lim.destroy()
        call chk.destroy()
        call tra.destroy()
        set instance = 0
        set lim = Table.create()
        set chk = Table.create()
        set tra = Table.create()
        set random = TableArray[0x2000]
    endmethod    
endstruct

endlibrary

  • DEMO
    • Events
      • Time - Elapsed game time is 0.00 seconds
    • Conditions
    • Actions
      • Set U = Barracks 0035 <gen>
      • Custom script: call TrainSystem.register(udg_U, 5)
      • Custom script: call TrainSystem.addTrain(udg_U, 'hfoo')
      • Custom script: call TrainSystem.addTrain(udg_U, 'hrif')
      • Set U = Barracks 0036 <gen>
      • Custom script: call TrainSystem.register(udg_U, 5)
      • Custom script: call TrainSystem.addTrain(udg_U, 'hfoo')
      • Custom script: call TrainSystem.addTrain(udg_U, 'hrif')
      • Set U = Arcane Sanctum 0039 <gen>
      • Custom script: call TrainSystem.register(udg_U, 5)
      • Custom script: call TrainSystem.addTrain(udg_U, 'hmpr')
      • Custom script: call TrainSystem.addTrain(udg_U, 'hsor')
      • Set U = Arcane Sanctum 0040 <gen>
      • Custom script: call TrainSystem.register(udg_U, 5)
      • Custom script: call TrainSystem.addTrain(udg_U, 'hmpr')
      • Custom script: call TrainSystem.addTrain(udg_U, 'hsor')
      • Set U = Gryphon Aviary 0037 <gen>
      • Custom script: call TrainSystem.register(udg_U, 5)
      • Custom script: call TrainSystem.addTrain(udg_U, 'hgry')
      • Custom script: call TrainSystem.addTrain(udg_U, 'hdhw')
      • Set U = Gryphon Aviary 0038 <gen>
      • Custom script: call TrainSystem.register(udg_U, 5)
      • Custom script: call TrainSystem.addTrain(udg_U, 'hgry')
      • Custom script: call TrainSystem.addTrain(udg_U, 'hdhw')

v1.3
- Added 3 more API's
- Fixed a big bug that death event wont subtract correctly
v1.1
- Added 1 API to remove structure from auto train
 

Attachments

  • TrainSystem.w3x
    47.7 KB · Views: 123
Last edited:
I like trains

edit
Oh wait, that's not what this system is about ;_;

edit
You don't need the native declaration because Bribe confirmed that UnitAlive returns the same thing as (not IsUnitType(u, UNIT_TYPE_DEAD)).

edit
In the deathEvent function, it would be best to cache the handle ids of b and u.

And in the trainEnd function, it would be best to cache the handle id of the trained unit.

edit
Also, 0x30000 is too large a TableArray size for what you're doing here ;P
0x2000 == 8192 and it too is very large :p
Go for 0x2000 or less
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
I didnt know that but this I dont think so coz I tested the CANCEL and ESC already and the
the chk[ID] seems to be OK...I've added a testmap...

Maybe the bug was fixed, but i don't think so, the test is simple just display a text message on CANCEL event and try to cancel a training unit by clicking on its icon, clicking the red cancel button and finally by pressing esc.

Now, that wouldn't be a problem for computers, it's just an issue about real human players.
But if what i say it's true (most likely) then you should really say it in your documentation, because it's such an obscure bug.
I know you don't recommend that for real human players though.

EDIT : Or it's just about when you have several units in a same training queue, i don't really remember.
 
Level 29
Joined
Mar 10, 2009
Messages
5,016
@Mags
well noted for handle cache and size of array (although nothing wrong with it)...

@Troll- Brain
I did a msg check after set chk[ID] = chk[ID] + 1 and it keeps on
displaying the same number when I cancel/esc...
anyway I may put condition that it can only add computer players if that bug
you say still exist...
I forgot to add an API to unregister the unit...and thanks for the name suggestion...
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
Well, i've never said it and it's off-topic but plz stop to abuse "..." it's really hard to get your feelings.
Usually we use "..." as "etc", or as a sarcasm, or get bored, but i can't believe that you're always sarcastic/bored.

Anyway i'm quite sure of what i said, there are three ways to remove an unit which is being trained (i mean in game as a player).
I will check them and share a test map and a replay since you keep saying it's fine.
Again the bug could be fixed now but that's very unlikely.

EDIT : Ok it's either that the bug was silently fixed in some patch or the bug was nothing but me, the event always fire except when the structure is killed/removed, this was tested with the patch 1.26a.
If one day i have to make an other test before the patch of the dead (end of the return bug) i will test it just for the sake of it.
 

Attachments

  • cancel event test.w3g
    3 KB · Views: 90
  • cancel event.w3m
    12.8 KB · Views: 79
Last edited:
Level 29
Joined
Mar 10, 2009
Messages
5,016
first thanks and sorry for the '...', it's just my...signature or I'm used to it and for me it doesnt mean
etc, but just like 'nothing' or no harm in putting that :)...

the event always fire except when the structure is killed/removed, this was tested with the patch 1.26a.
that's logical my friend coz how can it fire if there's nothing to cancel/esc?, but
if the unit being trained is removed during training (if it can be removed), maybe
that's another story, gotta test it...
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
that's logical my friend coz how can it fire if there's nothing to cancel/esc?, but
if the unit being trained is removed during training (if it can be removed), maybe
that's another story, gotta test it...

I've tried to abuse the return bug to get the unit which is being trained (typecast integer to unit), but the unit doesn't seem to be created until the finish train event fire like Bribe said.
Or at least it doesn't follow the usual handle id and when the unit is officially created its id is changed, but meh that doesn't seem credible.

And it's not logical at all, i'm talking about removing, killing the structure which is training the unit.
For example the event spell abort event fire when the caster is killed during the cast (and probably also removed).
 
Level 29
Joined
Mar 10, 2009
Messages
5,016
...but the unit doesn't seem to be created until the finish train event fire like Bribe said.
cancel btw does not get the Trained unit's ID but the trigger unit, I forget to remove the 'trained'
since I just cnp the code from 'trainEnd'...

JASS:
private static method trainCancel takes nothing returns nothing
        local unit u = GetTriggerUnit()
        //local unit trained = GetTrainedUnit()
        local integer ID = GetHandleId(u)
        if not chk.boolean[ID] then
            set chk.boolean[ID] = true
            set chk[ID] = chk[ID] - 1
        endif
        set u = null
        //set trained = null
endmethod
And it's not logical at all, i'm talking about removing, killing the structure which is training the unit.
For example the event spell abort event fire when the caster is killed during the cast (and probably also removed).
what difference does it make?, coz the structure/unit will be removed from the loop, therefore the instance is recycled...
but IDK if the game will crash when the trained unit dies if the structure is removed/killed so he encounters a null instance
by this set b = chk.unit[ID]...
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
No you don't get it, i basically mean that the cancel event should fire when the structure die, i know you handle this case.
However you probably don't handle the case when the structure is removed of the game with RemoveUnit.
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
I'm just saying that the event EVENT_PLAYER_UNIT_TRAIN_CANCEL should fire on structure dead if there were units being trained, but it's not up to you.

About RemoveUnit, actually it will leak struct instances if a structure which had units being trained is removed of the game without kill it first.
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
No, but i don't know if we can call it a bug (about how the event fire).
And yes i obviously know that you can check for the death event, as mentionned earlier.
But ofc it's a bug from this library about the use of RemoveUnit, you've just confused me because you're talking about unit death but it has nothing to do with RemoveUnit. (lack of partial quote)
 
Top