• 🏆 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] PlayerUtils

System Code
JASS:
library PlayerUtils
/**************************************************************
*
*   v1.2.9 by TriggerHappy
*
*   This library provides a struct which caches data about players
*   as well as provides functionality for manipulating player colors.
*
*   Constants
*   ------------------
*
*       force FORCE_PLAYING - Player group of everyone who is playing.
*
*   Struct API
*   -------------------
*     struct User
*
*       static method fromIndex takes integer i returns User
*       static method fromLocal takes nothing returns User
*       static method fromPlaying takes integer id returns User
*
*       static method operator []    takes integer id returns User
*       static method operator count takes nothing returns integer
*
*       method operator name         takes nothing returns string
*       method operator name=        takes string name returns nothing
*       method operator color        takes nothing returns playercolor
*       method operator color=       takes playercolor c returns nothing
*       method operator defaultColor takes nothing returns playercolor
*       method operator hex          takes nothing returns string
*       method operator nameColored  takes nothing returns string
*
*       method toPlayer takes nothing returns player
*       method colorUnits takes playercolor c returns nothing
*
*       readonly string originalName
*       readonly boolean isPlaying
*       readonly static player Local
*       readonly static integer LocalId
*       readonly static integer AmountPlaying
*       readonly static playercolor array Color
*       readonly static player array PlayingPlayer
*
**************************************************************/

    globals
        // automatically change unit colors when changing player color
        private constant boolean AUTO_COLOR_UNITS = true
   
        // use an array for name / color lookups (instead of function calls)
        private constant boolean ARRAY_LOOKUP     = false
   
        // this only applies if ARRAY_LOOKUP is true
        private constant boolean HOOK_SAFETY      = false // disable for speed, but only use the struct to change name/color safely
   
        constant force FORCE_PLAYING = CreateForce()
   
        private string array Name
        private string array Hex
        private string array OriginalHex
        private playercolor array CurrentColor
    endglobals

    private keyword PlayerUtilsInit

    struct User extends array
     
        static constant integer NULL = bj_MAX_PLAYER_SLOTS
       
        readonly player handle
        readonly integer id
        readonly thistype next
        readonly thistype prev

        readonly string originalName
        readonly boolean isPlaying
   
        readonly static thistype first
        readonly static thistype last
        readonly static player Local
        readonly static integer LocalId
        readonly static integer AmountPlaying = 0
        readonly static playercolor array Color

        static if not (LIBRARY_GroupUtils) then
            readonly static group ENUM_GROUP = CreateGroup()
        endif

        private static thistype array PlayingPlayer
        private static integer array PlayingPlayerIndex
   
        // similar to Player(#)
        static method fromIndex takes integer i returns thistype
            return thistype(i)
        endmethod
   
        // similar to GetLocalPlayer
        static method fromLocal takes nothing returns thistype
            return thistype(thistype.LocalId)
        endmethod
   
        // access active players array
        static method fromPlaying takes integer index returns thistype
            return PlayingPlayer[index]
        endmethod
   
        static method operator [] takes player p returns thistype
            return thistype(GetPlayerId(p))
        endmethod
   
        method toPlayer takes nothing returns player
            return this.handle
        endmethod
     
        method operator name takes nothing returns string
            static if (ARRAY_LOOKUP) then
                return Name[this]
            else
                return GetPlayerName(this.handle)
            endif
        endmethod
   
        method operator name= takes string newName returns nothing
            call SetPlayerName(this.handle, newName)
            static if (ARRAY_LOOKUP) then
                static if not (HOOK_SAFETY) then
                    set Name[this] = newName
                endif
            endif
        endmethod
   
        method operator color takes nothing returns playercolor
            static if (ARRAY_LOOKUP) then
                return CurrentColor[this]
            else
                return GetPlayerColor(this.handle)
            endif
        endmethod
   
        method operator hex takes nothing returns string
            return OriginalHex[GetHandleId(this.color)]
        endmethod
   
        method operator color= takes playercolor c returns nothing
            call SetPlayerColor(this.handle, c)
       
            static if (ARRAY_LOOKUP) then
                set CurrentColor[this] = c
                static if not (HOOK_SAFETY) then
                    static if (AUTO_COLOR_UNITS) then
                        call this.colorUnits(color)
                    endif
                endif
            endif
        endmethod
   
        method operator defaultColor takes nothing returns playercolor
            return Color[this]
        endmethod
   
        method operator nameColored takes nothing returns string
            return hex + this.name + "|r"
        endmethod
   
        method colorUnits takes playercolor c returns nothing
            local unit u
       
            call GroupEnumUnitsOfPlayer(ENUM_GROUP, this.handle, null)
       
            loop
                set u = FirstOfGroup(ENUM_GROUP)
                exitwhen u == null
                call SetUnitColor(u, c)
                call GroupRemoveUnit(ENUM_GROUP, u)
            endloop
        endmethod
   
        static method onLeave takes nothing returns boolean
            local thistype p  = thistype[GetTriggerPlayer()]
            local integer i   = .PlayingPlayerIndex[p.id]
       
            // clean up
            call ForceRemovePlayer(FORCE_PLAYING, p.toPlayer())
       
            // recycle index
            set .AmountPlaying = .AmountPlaying - 1
            set .PlayingPlayerIndex[i] = .PlayingPlayerIndex[.AmountPlaying]
            set .PlayingPlayer[i] = .PlayingPlayer[.AmountPlaying]
           
            if (.AmountPlaying == 1) then
                set p.prev.next = User.NULL
                set p.next.prev = User.NULL
            else
                set p.prev.next = p.next
                set p.next.prev = p.prev
            endif

            set .last = .PlayingPlayer[.AmountPlaying]
           
            set p.isPlaying = false
       
            return false
        endmethod
   
        implement PlayerUtilsInit
   
    endstruct

    private module PlayerUtilsInit
        private static method onInit takes nothing returns nothing
            local trigger t = CreateTrigger()
            local integer i = 0
            local thistype p
       
            set thistype.Local   = GetLocalPlayer()
            set thistype.LocalId = GetPlayerId(thistype.Local)
       
            set OriginalHex[0]  = "|cffff0303"
            set OriginalHex[1]  = "|cff0042ff"
            set OriginalHex[2]  = "|cff1ce6b9"
            set OriginalHex[3]  = "|cff540081"
            set OriginalHex[4]  = "|cfffffc01"
            set OriginalHex[5]  = "|cfffe8a0e"
            set OriginalHex[6]  = "|cff20c000"
            set OriginalHex[7]  = "|cffe55bb0"
            set OriginalHex[8]  = "|cff959697"
            set OriginalHex[9]  = "|cff7ebff1"
            set OriginalHex[10] = "|cff106246"
            set OriginalHex[11] = "|cff4e2a04"
            
            if (bj_MAX_PLAYERS > 12) then
                set OriginalHex[12] = "|cff9B0000"
                set OriginalHex[13] = "|cff0000C3"
                set OriginalHex[14] = "|cff00EAFF"
                set OriginalHex[15] = "|cffBE00FE"
                set OriginalHex[16] = "|cffEBCD87"
                set OriginalHex[17] = "|cffF8A48B"
                set OriginalHex[18] = "|cffBFFF80"
                set OriginalHex[19] = "|cffDCB9EB"
                set OriginalHex[20] = "|cff282828"
                set OriginalHex[21] = "|cffEBF0FF"
                set OriginalHex[22] = "|cff00781E"
                set OriginalHex[23] = "|cffA46F33"
            endif
         
            set thistype.first = User.NULL

            loop
                exitwhen i == bj_MAX_PLAYERS

                set p         = User(i)
                set p.handle  = Player(i)
                set p.id      = i
           
                set thistype.Color[i] = GetPlayerColor(p.handle)
                set CurrentColor[i] = thistype.Color[i]
             
                if (GetPlayerController(p.handle) == MAP_CONTROL_USER and GetPlayerSlotState(p.handle) == PLAYER_SLOT_STATE_PLAYING) then

                    set .PlayingPlayer[AmountPlaying] = p
                    set .PlayingPlayerIndex[i] = .AmountPlaying
                   
                   set .last = i
                   
                    if (.first == User.NULL) then
                        set .first = i
                        set User(i).next = User.NULL
                        set User(i).prev = User.NULL
                    else
                        set User(i).prev = PlayingPlayer[AmountPlaying-1].id
                        set PlayingPlayer[AmountPlaying-1].next = User(i)
                        set User(i).next = User.NULL
                    endif

                    set p.isPlaying = true
               
                    call TriggerRegisterPlayerEvent(t, p.handle, EVENT_PLAYER_LEAVE)
                    call ForceAddPlayer(FORCE_PLAYING, p.handle)
               
                    set Hex = OriginalHex[GetHandleId(thistype.Color[i])]
               
                    set .AmountPlaying = .AmountPlaying + 1

                endif
           
                set Name = GetPlayerName(p.handle)
                set p.originalName=Name
           
                set i = i + 1
            endloop
       
            call TriggerAddCondition(t, Filter(function thistype.onLeave))
        endmethod
    endmodule

    //===========================================================================


    static if (ARRAY_LOOKUP) then
        static if (HOOK_SAFETY) then
            private function SetPlayerNameHook takes player whichPlayer, string name returns nothing
                set Name[GetPlayerId(whichPlayer)] = name
            endfunction
       
            private function SetPlayerColorHook takes player whichPlayer, playercolor color returns nothing
                local User p = User[whichPlayer]
           
                set Hex = OriginalHex[GetHandleId(color)]
                set CurrentColor = color
           
                static if (AUTO_COLOR_UNITS) then
                    call p.colorUnits(color)
                endif
            endfunction
       
            hook SetPlayerName SetPlayerNameHook
            hook SetPlayerColor SetPlayerColorHook
        endif 
    endif

endlibrary


Example
JASS:
scope PlayerUtilsExample initializer Init

    private function OnGameStart takes nothing returns nothing
        local User p = User.first
 
        loop // only loop through players that are playing
            exitwhen p == User.NULL
 
            set p.name = "Pvt. " + p.originalName // change name
            set p.color = PLAYER_COLOR_GREEN // change color
 
            // show original name and new one
            call DisplayTextToPlayer(User.Local, 0, 0, p.originalName + " was promoted to " + p.nameColored)
 
            // compare player id's instead of handles (should be faster)
            if (User.fromLocal().id == p.id) then
                call PanCameraToTimed(128, 128, 0)
            endif
 
            set p = p.next
        endloop
    endfunction

    //===========================================================================
    private function Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerRegisterTimerEvent(t, 0, false)
        call TriggerAddAction(t, function OnGameStart)
    endfunction

endscope
 
Last edited:
Looks pretty useful. I think the biggest benefit is being able to loop through only the players that concern you.

  • I would remove the hooks completely. You don't need to store the player color or the player name. Just have it as:
    JASS:
            method operator name takes nothing returns string
                return GetPlayerName(this.handle, name)
            endmethod
    
            method operator name= takes string newName returns nothing
                call SetPlayerName(this.handle, name)
            endmethod
    
            method operator color takes nothing returns playercolor
                return GetPlayerColor(this.handle)
            endmethod
    
            method operator color= takes playercolor c returns nothing
                call SetPlayerColor(this.handle, c)
    
                static if (AUTO_COLOR_UNITS) then
                    call this.colorUnits(color)
                endif 
            endmethod
    
            method operator hex takes nothing returns string
                return OriginalHex[GetHandleId(this.color)]
            endmethod
    I don't think people will use any of those functions too often, so you don't need to cache them.
  • I think it might be nice to have separate forces to ease looping through types of players. For example, it would be useful to be able to loop through users, computers, and observers separately. Or all of them together!
  • Most of the properties and functions are self explanatory, but fromIndex/fromPlaying/fromLocal could use a short description in the documentation.
 
Level 7
Joined
Mar 10, 2013
Messages
366
Personally, i don't see why anyone would use User.fromIndex(i) over just User(i). Other than that, i guess it has its uses. One thing though: you should also save each player's original name, since this is useful information in a lot of cases.
If you're looking for a verbose more cleaner syntax over a short one, that's just another option. Personal preference, just that.
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
Is there a loop for player slots that were playing at start and if not wouldn't that be something worth including?

In a perfect world all user related data should be deallocated when a player leaves, but we don't live in such a world. And being able to optimize from 24 to lets say 3 players that were active from start is much better than not having that possibility available.
 
Last edited:
If it's needed to remember the playing players at map start, it probably should be handled in the map itself, using the PlayingPlayer. Seems a bit a specific issue, to be needed to loop over already leaved players, at any time. Or would the amount of initial players maybe be enough?

Not sure that it's good if all player related data would be deallocated - or actually, what is exactly meant by it? As we should keep it mind some things the user wants to stay on map, still being shared to the world, or other players in what ever form.
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
If it's needed to remember the playing players at map start, it probably should be handled in the map itself, using the PlayingPlayer. Seems a bit a specific issue, to be needed to loop over already leaved players, at any time. Or would the amount of initial players maybe be enough?

Not sure that it's good if all player related data would be deallocated - or actually, what is exactly meant by it? As we should keep it mind some things the user wants to stay on map, still being shared to the world, or other players in what ever form.

I don't think it's that specifc to be honest, many systems are configured to remove stuff based on "All Player Slots" and it would be a way to coordinate what you use in setup IsPlaying with OncePlayed at a later point.

For example the Cooldown Manager that I'm working on at the moment needs to iterate through all potential used player slots to remove any potentially allocated data. This might be considered sub-optimization but none the less:

JASS:
        static method purgeFromOption takes SpellOption option returns nothing
            local integer pid = 0
            loop
                exitwhen pid > bj_MAX_PLAYERS
                set this = pid
                if thistype(pid).table.timer.has(option) then
                    call cleanup(thistype(pid).table.timer[option])
                endif
                set pid = pid + 1
            endloop
        endmethod

// -->

 
        static method purgeFromOption takes SpellOption option returns nothing
            local User p = User.firstSlot
            loop
                exitwhen p = User.NULL // We skip such player slots that were empty from start
                //
                if thistype(p.id).table.timer.has(option) then
                    call cleanup(thistype(p.id).table.timer[option])
                endif
                //
                set p = p.nextSlot
            endloop
        endmethod

Its also a really simple thing to add with minimal impact.
 
Last edited:
I think I understand, but look, even he adds the array/force what ever for initial players, it wouldn't even be best solution for showed problem.

Instead of

1. loop: 1 - MAX_PLAYERS

.. you want..

2. loop: 1 - InitialPlayingPlayers

But the actual solution is:

3. loop: 1 - CurrentlyPlayingPlayers

..or at least an updated number. The #2 is still only some vague optimzation, and not really a proper tequnique. Imagine 8 or more players leave the game, so then the InitialPlayingPlayers also doesn't help very much, as it still make many not wanted checks. A system itself should be aware of itself, to know which players are still registered, needed for loop..

But imo, sure it can be added, I'm just questioning the need.

EDIT:

I meant of course, not really "CurrentlyPlayingPlayers", but the players lastly were considered by the system as currently still registered/needed/active.
 
Last edited:
Top