System PUI - Perfect Unit Indexing

Cohadar

master of fugue
New version: v6.2 (fully backwards compatible)
Changes:
* You can now get unit from an index GetIndexUnit(index) -> unit
* Added PUI module that does the same stuff as PUI textmacro

JASS:

//===========================================================================
//  PUI -- Perfect Unit Indexing by Cohadar -- v6.2
//===========================================================================
//
//  PURPOUSE:
//       * Extending UnitUserData()
//       * This is basically perfect hashing algorithm for units
//
//  HOW TO USE:
//       * You have two functions at your disposal: 
//           GetUnitIndex(unit) -> index
//           GetIndexUnit(index) -> unit
//
//       * Put you custom unit properties in struct that extends arrays.
//         Use unit index as id of that struct.
//         There is an example of this in demo map.
//         
//  PROS:
//       * Unit indexes are assigned only to units that actually need them.
//       * Unit index will be automatically recycled when unit is removed from the game.
//       * Automatically detects null unit handles and removed unit handles (in debug mode)
//
//  CONS:
//       * This system needs exclusive access to UnitUserData
//
//  DETAILS:
//       * Uses internal vJass struct index allocation/deallocation algorithm.
//       * Periodically checks and recycles unit indexes
//
//  THANKS TO:
//       * Vexorian - for his help with PUI textmacro
//       * Builder Bob - for testing and bugfinding
//       * Joker(Div) - bugfinding
//
//  HOW TO IMPORT:
//       * Just create a trigger named PUI
//       * convert it to text and replace the whole trigger text with this one
//
//===========================================================================


library PUI initializer Init

//===========================================================================
globals    
    // maximum number of indexed units on your map at a single moment of time
    private constant integer MAX_INDEXES = 1024  // up to 8192
    
    // period of recycling, 32 indexes per second
    private constant real PERIOD = 0.03125   
    
    // current check index
    private integer C = 0 
endglobals

//===========================================================================
// Using internal struct algorithm for index allocation/dealocation
// This way I don't have to write messy code of my own
//===========================================================================
private struct UnitIndex
    unit u
    
    //----------------------------------------------------------
    static method create takes unit whichUnit returns UnitIndex
        local UnitIndex index 
        
        // check for null unit handle
        debug if whichUnit == null then
        debug     call BJDebugMsg("|c00FF0000ERROR: PUI - Index requested for null unit")
        debug     return 0
        debug endif
        
        set index = UnitIndex.allocate()
        set index.u = whichUnit
        call SetUnitUserData(whichUnit, index)
        
        // check for removed unit handle
        debug if GetUnitUserData(whichUnit) == 0 then
        debug     call BJDebugMsg("|c00FFCC00WARNING: PUI - Bad unit handle")
        debug     return 0
        debug endif        
        
        return index
    endmethod
    
    //----------------------------------------------------------
    static method Recycler takes nothing returns boolean
        local UnitIndex index = C
        set C = C + 1
        if C == MAX_INDEXES then
            set C = 0
        endif
        if index.u != null then
            if (GetUnitUserData(index.u) == 0) then
                set index.u = null
                call index.destroy()
            endif
        endif
        return false
    endmethod
endstruct

//===========================================================================
//  Returns index of some unit, if unit has no index it gets a new one.
//===========================================================================
function GetUnitIndex takes unit whichUnit returns integer
    local UnitIndex index = GetUnitUserData(whichUnit)
    if index == 0 then
        set index = UnitIndex.create(whichUnit)
    endif
    return index
endfunction

//===========================================================================
//  Return unit that has specified index, or null in no such unit exists.
//===========================================================================
function GetIndexUnit takes integer index returns unit
    local UnitIndex i = index
    if i.u != null then
        if GetUnitUserData(i.u) == index then
            return i.u
        endif
    endif
    return null
endfunction

//===========================================================================
private function Init takes nothing returns nothing
    local trigger trig = CreateTrigger()
    call TriggerRegisterTimerEvent(trig, PERIOD, true)
    call TriggerAddCondition(trig, Condition(function UnitIndex.Recycler))
endfunction

endlibrary

//***************************************************************************
//
//  PUI TEXTMACROS
// 
//***************************************************************************

//===========================================================================
//  Allowed PUI_PROPERTY TYPES are: unit, integer, real, boolean, string
//  Do NOT put handles that need to be destroyed here (timer, trigger, ...)
//  Instead put them in a struct and use PUI textmacro
//===========================================================================
//! textmacro PUI_PROPERTY takes VISIBILITY, TYPE, NAME, DEFAULT
$VISIBILITY$ struct $NAME$
    private static unit   array pui_unit
    private static $TYPE$ array pui_data
    
    //-----------------------------------------------------------------------
    //  Returns default value when first time used
    //-----------------------------------------------------------------------
    static method operator[] takes unit whichUnit returns $TYPE$
        local integer pui = GetUnitIndex(whichUnit)
        if .pui_unit[pui] != whichUnit then
            set .pui_unit[pui] = whichUnit
            set .pui_data[pui] = $DEFAULT$
        endif
        return .pui_data[pui]
    endmethod
    
    //-----------------------------------------------------------------------
    static method operator[]= takes unit whichUnit, $TYPE$ whichData returns nothing
        local integer pui = GetUnitIndex(whichUnit)
        set .pui_unit[pui] = whichUnit
        set .pui_data[pui] = whichData
    endmethod
endstruct
//! endtextmacro

//===========================================================================
//  Never destroy PUI structs directly.
//  Use .release() instead, will call .destroy()
//  The best option is never to release or destroy PUI structs manually
//    because they will be recycled automatically with unit handle.
//===========================================================================
//! textmacro PUI
    private static unit    array pui_unit
    private static integer array pui_data
    private static integer array pui_id
    
    //-----------------------------------------------------------------------
    //  Returns zero if no struct is attached to unit
    //-----------------------------------------------------------------------
    static method operator[] takes unit whichUnit returns integer
        local integer pui = GetUnitIndex(whichUnit)
        if .pui_data[pui] != 0 then
            if .pui_unit[pui] != whichUnit then
                // recycled handle detected
                call .destroy(.pui_data[pui])
                set .pui_unit[pui] = null
                set .pui_data[pui] = 0            
            endif
        endif
        return .pui_data[pui]
    endmethod
    
    //-----------------------------------------------------------------------
    //  This will overwrite already attached struct if any
    //-----------------------------------------------------------------------
    static method operator[]= takes unit whichUnit, integer whichData returns nothing
        local integer pui = GetUnitIndex(whichUnit)
        if .pui_data[pui] != 0 then
            call .destroy(.pui_data[pui])
        endif
        set .pui_unit[pui] = whichUnit
        set .pui_data[pui] = whichData
        set .pui_id[whichData] = pui
    endmethod

    //-----------------------------------------------------------------------
    //  If you do not call release struct will be destroyed when unit handle gets recycled
    //-----------------------------------------------------------------------
    method release takes nothing returns nothing
        local integer pui= .pui_id[integer(this)]
        call .destroy()
        set .pui_unit[pui] = null
        set .pui_data[pui] = 0
    endmethod
//! endtextmacro

//***************************************************************************
//
//  PUI MODULE  (does the same stuff as PUI textmacro)
// 
//***************************************************************************

//===========================================================================
//  Never destroy PUI structs directly.
//  Use .release() instead, will call .destroy()
//  The best option is never to release or destroy PUI structs manually
//    because they will be recycled automatically with unit handle.
//===========================================================================
module PUI
    private static unit    array pui_unit
    private static integer array pui_data
    private static integer array pui_id
    
    //-----------------------------------------------------------------------
    //  Returns zero if no struct is attached to unit
    //-----------------------------------------------------------------------
    static method operator[] takes unit whichUnit returns integer
        local integer pui = GetUnitIndex(whichUnit)
        if .pui_data[pui] != 0 then
            if .pui_unit[pui] != whichUnit then
                // recycled handle detected
                call .destroy(.pui_data[pui])
                set .pui_unit[pui] = null
                set .pui_data[pui] = 0            
            endif
        endif
        return .pui_data[pui]
    endmethod
    
    //-----------------------------------------------------------------------
    //  This will overwrite already attached struct if any
    //-----------------------------------------------------------------------
    static method operator[]= takes unit whichUnit, integer whichData returns nothing
        local integer pui = GetUnitIndex(whichUnit)
        if .pui_data[pui] != 0 then
            call .destroy(.pui_data[pui])
        endif
        set .pui_unit[pui] = whichUnit
        set .pui_data[pui] = whichData
        set .pui_id[whichData] = pui
    endmethod

    //-----------------------------------------------------------------------
    //  If you do not call release struct will be destroyed when unit handle gets recycled
    //-----------------------------------------------------------------------
    method release takes nothing returns nothing
        local integer pui= .pui_id[integer(this)]
        call .destroy()
        set .pui_unit[pui] = null
        set .pui_data[pui] = 0
    endmethod
endmodule


JASS:

//==============================================================================
//  PUI -- Perfect Unit Indexing by Cohadar -- v5.3
//==============================================================================
//
//  PURPOUSE:
//       * Extending UnitUserData()
//       * This is basically perfect hashing algorithm for units
//
//  HOW TO USE:
//       * You have only one function at your disposal GetUnitIndex(unit)
//         It will return a unique index for each unit in range 1..8190
//
//       * What you will do with that index is all up to you
//         Of course using global arrays is the most obvious choice
//         Advanced jassers will think of a couple of more clever ones ofc.
//
//       * There are also 2 textmacros available at the end of library code
//         They can be used for easier attaching to units
//         PUI for structs 
//         PUI_PROPERTY for unit, integer, real, boolean and string variables
//
//  PROS: 
//       * You can use any number of systems that previously could not work together
//         because they all required exclusive access of UnitUserData()
//
//       * You can also use this to attach spell data structs to casters
//
//       * There are no SetUnitIndex() or ClearUnitIndex() functions here
//         Each unit gets assigned one index that cannot be changed
//         That index will be automatically recycled when unit is removed from the game.
//
//  CONS:
//       * This system uses UnitUserData() itself
//         That means that if you want to use PUI you must recode 
//         any other system that uses UnitUserData() to use GetUnitIndex() instead
//
//       * If you use UnitIndex for arrays of non-native types (timers, effects and similar)
//         you must check if timer on that index already exists before you create a new one.
//         This can happen if GetUnitIndex() assigns a recycled index (index of some dead and removed unit)
//         to the newly created unit for which you intended to use timer for
//
//       * All in all this is not a sys for newbies, it gives great power,
//         but it requires knowledge and carefull handling
//
//  DETAILS:
//       * System is using unit array to keep track of all units with an index.
//         Array is periodically checked for removed units,
//         when removed unit is found, index is recycled.
//         Indexes have "decay time" to prevent bugs
//         caused by attaching to "Can't Raise, Does not decay" type units,
//         or by using RemoveUnit() function
//
//  SYSTEM COMMANDS: (debug mode only, red player only)
//
//       * type -pui to display indexes of currently selected units
//       * type -puistats to display some stats
//       * type -puitest to verify that all indexes are valid and unique
//
//  THANKS TO:
//       * Vexorian - for his help with PUI textmacro
//       * Builder Bob - for testing and bugfinding
//       * Joker(Div) - bugfinding
//
//  HOW TO IMPORT:
//       * Just create a trigger named PUI
//       * convert it to text and replace the whole trigger text with this one
//
//==============================================================================


library PUI initializer Init

//==============================================================================
globals    
    //-----------------------------------------------
    private constant real INDEX_DECAY_TIME = 5.  // seconds
    
    //-----------------------------------------------    
    private constant real PERIOD = 0.03125   // 32 fps
        
    //-----------------------------------------------
    private constant integer DECAY_TICKS = R2I(INDEX_DECAY_TIME/PERIOD)
    
    //-----------------------------------------------
    private integer maxIndex = 0
    private integer usedIndexCount = 0
    private integer checker  = 0
    private integer array Indexz
    private unit    array Unitz
    
    //-----------------------------------------------
    private integer freeIndexCount = 0
    private integer array Freez

    //-----------------------------------------------
    private integer decayIndexCount = 0
    private integer decayer  = 0
    private integer array Decayz
    private integer array TimeEndz
    
    private integer tick = 0
endglobals

//==============================================================================
private function PeriodicRecycler takes nothing returns boolean
    set tick = tick + 1
    
    // unit recycler
    if usedIndexCount > 0 then
        set checker = checker + 1
        if checker > usedIndexCount then
            set checker = 1
        endif
        if (GetUnitUserData(Unitz[checker]) == 0) then
            set decayIndexCount = decayIndexCount + 1
            set Decayz[decayIndexCount] = Indexz[checker]
            set TimeEndz[decayIndexCount] = tick + DECAY_TICKS
            
            set Indexz[checker] = Indexz[usedIndexCount]
            set Unitz[checker] = Unitz[usedIndexCount]
            set usedIndexCount = usedIndexCount - 1
        endif
    endif

    // index recycler
    if decayIndexCount > 0 then
        set decayer = decayer + 1
        if decayer > decayIndexCount then
            set decayer = 1
        endif
        if TimeEndz[decayer] <= tick then
            set freeIndexCount = freeIndexCount + 1
            set Freez[freeIndexCount] = Decayz[decayer]

            set Decayz[decayer] = Decayz[decayIndexCount]
            set TimeEndz[decayer] = TimeEndz[decayIndexCount]          
            set decayIndexCount = decayIndexCount - 1
        endif    
    endif
	
    // for debugging 
    //if ModuloInteger(tick, 8) == 0 then
    //    call ClearTextMessages()
    //    call BJDebugMsg("Used/Free/Decaying: " + I2S(usedIndexCount) + "/" + I2S(freeIndexCount) + "/" + I2S(decayIndexCount))
    //endif
    
    return false
endfunction

//==============================================================================
//  Main and only function exported by this library
//==============================================================================
function GetUnitIndex takes unit whichUnit returns integer
    local integer index
    
    debug if whichUnit == null then
    debug   call BJDebugMsg("|c00FF0000ERROR: PUI - Index requested for null unit")
    debug   return 0
    debug endif
    
    set index = GetUnitUserData(whichUnit)

    if index == 0 then
        set usedIndexCount = usedIndexCount + 1

        if freeIndexCount > 0 then
            set Indexz[usedIndexCount] = Freez[freeIndexCount]
            set freeIndexCount = freeIndexCount - 1
        else
            set maxIndex = maxIndex + 1
            set Indexz[usedIndexCount] = maxIndex
        endif
        
        set Unitz[usedIndexCount] = whichUnit
        call SetUnitUserData(whichUnit, Indexz[usedIndexCount])
        set index = GetUnitUserData(whichUnit)
       
        // this happens when requesting unit index for removed unit
        debug if index == 0 then
        debug     call BJDebugMsg("|c00FFCC00WARNING: PUI - Bad unit handle")
        debug endif
    endif
    
    return index
endfunction

//==============================================================================
private function DisplayStats takes nothing returns nothing
    call BJDebugMsg("=============================================")    
    call BJDebugMsg("Biggest index ever = " + I2S(maxIndex))    
    call BJDebugMsg("Indexes in use = " + I2S(usedIndexCount))
    call BJDebugMsg("Decaying indexes = " + I2S(decayIndexCount))
    call BJDebugMsg("Released indexes = " + I2S(freeIndexCount))
    call BJDebugMsg("=============================================")    
endfunction

//===========================================================================
private function DisplaySelectedEnum takes nothing returns nothing
    call BJDebugMsg( "PUI(" + ( GetUnitName(GetEnumUnit()) + ( ") = " + I2S(GetUnitUserData(GetEnumUnit())) ) ) )
endfunction

//===========================================================================
private function DisplaySelected takes nothing returns nothing
    local group g = CreateGroup()
    call SyncSelections()
    call GroupEnumUnitsSelected(g, Player(0), null)
    call ForGroup(g, function DisplaySelectedEnum)
    call DestroyGroup(g)
    set  g = null
endfunction


//==============================================================================
globals
    private integer testCounter = 0
    private integer array TestCountz
endglobals

//==============================================================================
private function Test takes nothing returns nothing
    local integer i
    set testCounter = testCounter + 1
    
    set i = 1
    loop
        exitwhen i > usedIndexCount
        // happens when you abuse UnitUserData outside PUI
        if GetUnitTypeId(Unitz<i>) != 0 and GetUnitUserData(Unitz<i>) != Indexz<i> then
            call BJDebugMsg(&quot;|c00FF0000ERROR: PUI - Invalid index detected&quot;)
            return
        endif
        // if this error happens it means PUI is bugged
        if TestCountz[Indexz<i>] != testCounter then
            set TestCountz[Indexz<i>] = testCounter
        else
            call BJDebugMsg(&quot;|c00FF0000ERROR: PUI - Double index detected&quot;)
            return
        endif
        set i = i + 1
    endloop
    
    call BJDebugMsg(&quot;|c0000ff00OK: PUI - All indexes were valid and unique&quot;)
endfunction

//==============================================================================
private function Init takes nothing returns nothing
    local trigger trig 
    
    set trig = CreateTrigger()
    call TriggerRegisterTimerEvent( trig, PERIOD, true )
    call TriggerAddCondition( trig, Condition(function PeriodicRecycler) )

    debug set trig = CreateTrigger()
    debug call TriggerRegisterPlayerChatEvent( trig, Player(0), &quot;-pui&quot;, true )
    debug call TriggerAddAction( trig, function DisplaySelected )      
    
    debug set trig = CreateTrigger()
    debug call TriggerRegisterPlayerChatEvent( trig, Player(0), &quot;-puistats&quot;, true )
    debug call TriggerAddAction( trig, function DisplayStats )
    
    debug set trig = CreateTrigger()
    debug call TriggerRegisterPlayerChatEvent( trig, Player(0), &quot;-puitest&quot;, true )
    debug call TriggerAddAction( trig, function Test )    
endfunction

endlibrary


//===========================================================================
//  Allowed PUI_PROPERTY TYPES are: unit, integer, real, boolean, string
//  Do NOT put handles that need to be destroyed here (timer, trigger, ...)
//  Instead put them in a struct and use PUI textmacro
//===========================================================================
//! textmacro PUI_PROPERTY takes VISIBILITY, TYPE, NAME, DEFAULT
$VISIBILITY$ struct $NAME$
    private static unit   array pui_unit
    private static $TYPE$ array pui_data
    
    //-----------------------------------------------------------------------
    //  Returns default value when first time used
    //-----------------------------------------------------------------------
    static method operator[] takes unit whichUnit returns $TYPE$
        local integer pui = GetUnitIndex(whichUnit)
        if .pui_unit[pui] != whichUnit then
            set .pui_unit[pui] = whichUnit
            set .pui_data[pui] = $DEFAULT$
        endif
        return .pui_data[pui]
    endmethod
    
    //-----------------------------------------------------------------------
    static method operator[]= takes unit whichUnit, $TYPE$ whichData returns nothing
        local integer pui = GetUnitIndex(whichUnit)
        set .pui_unit[pui] = whichUnit
        set .pui_data[pui] = whichData
    endmethod
endstruct
//! endtextmacro

//===========================================================================
//  Never destroy PUI structs directly.
//  Use .release() instead, will call .destroy()
//===========================================================================
//! textmacro PUI
    private static unit    array pui_unit
    private static integer array pui_data
    private static integer array pui_id
    
    //-----------------------------------------------------------------------
    //  Returns zero if no struct is attached to unit
    //-----------------------------------------------------------------------
    static method operator[] takes unit whichUnit returns integer
        local integer pui = GetUnitIndex(whichUnit)
        if .pui_data[pui] != 0 then
            if .pui_unit[pui] != whichUnit then
                // recycled handle detected
                call .destroy(.pui_data[pui])
                set .pui_unit[pui] = null
                set .pui_data[pui] = 0            
            endif
        endif
        return .pui_data[pui]
    endmethod
    
    //-----------------------------------------------------------------------
    //  This will overwrite already attached struct if any
    //-----------------------------------------------------------------------
    static method operator[]= takes unit whichUnit, integer whichData returns nothing
        local integer pui = GetUnitIndex(whichUnit)
        if .pui_data[pui] != 0 then
            call .destroy(.pui_data[pui])
        endif
        set .pui_unit[pui] = whichUnit
        set .pui_data[pui] = whichData
        set .pui_id[whichData] = pui
    endmethod

    //-----------------------------------------------------------------------
    //  If you do not call release struct will be destroyed when unit handle gets recycled
    //-----------------------------------------------------------------------
    method release takes nothing returns nothing
        local integer pui= .pui_id[integer(this)]
        call .destroy()
        set .pui_unit[pui] = null
        set .pui_data[pui] = 0
    endmethod
//! endtextmacro
</i></i></i></i></i>
 

Attachments

Why are those two functions, DisplayTopIndex() and RecycleIndex(), returning boolean values?
 

Cohadar

master of fugue
Because they are Conditions, not Actions.
(Standard trick in advanced vJass coding to increase speed of things)
 
Okay, and shouldn't you null the trigger variable "trig" in the Init function?

Off-Topic:
In you signature in wc3campaigns, DD v1.0 -link leads to PUI thread. :p
 

Cohadar

master of fugue
Yes this is for spellmaking, I already posted 2 spells that use it.
Soon will be 3.
 

hell_knight

Playing WoW
Does that give every single unit an unique custom value ? I am assuming GetUnitIndex(unit) is getting their custom value or something?

Kind of confused. Because if it is I could use this and make my stuff mui
 

Rheias

New Helper (I got over 2000 posts)
Cohadar, I'm not exactly sure I understand how this system works... Do I set the custom value normally, and simply I get it by getting the unit index, or did I miss something?

Thanks for sharing this. :)
 

Cohadar

master of fugue
System sets indexes for you, do NOT set anything yourself, just use GetUnitIndex() instead of GetUnitUserData()
 

Rheias

New Helper (I got over 2000 posts)
It is obivious that I'm missing something, if the system sets indexes for me, how exactly am I supposed to attach a struct to a unit? Ususally what I would have done is:

call SetUnitUserData(whichUnit,whichStruct)
...
set whichStruct = GetUnitUserData(whichUnit)

How would I do that using your system?
 

Cohadar

master of fugue
It is obivious that I'm missing something, if the system sets indexes for me, how exactly am I supposed to attach a struct to a unit? Ususally what I would have done is:

call SetUnitUserData(whichUnit,whichStruct)
...
set whichStruct = GetUnitUserData(whichUnit)

How would I do that using your system?



By looking at the example map or any other spell I posted?
 

hell_knight

Playing WoW
IN GUI custom values is GetUnitUserData(udg_unit) , so I can convert to JASS and replace that with GetUnitIndex(udg_unit)
That correct? Will anything schrew up if I do this?
 

phyrex1an

Staff Member and irregular helper
Staff member
IN GUI custom values is GetUnitUserData(udg_unit) , so I can convert to JASS and replace that with GetUnitIndex(udg_unit)
That correct? Will anything schrew up if I do this?
That will work as long as you doesn't use (eg, change) the custom value of the unit yourself somewhere else in the script. That's not a 'limitation' of GUI but rather of the system itself (and honestly, why use the custom value directly if you are using this?)
 

Rheias

New Helper (I got over 2000 posts)
Alright, I took a look, I think I get it, but from this part.

set ret.PUI[GetUnitUserData(victim)] = ret

I looked and I couldn't find any member in your struct called PUI. Further more, I tried to create a new struct and using PUI in it and it syntaxed me, so I'm a bit lost.
 

Cohadar

master of fugue
Ok I extracted the core of PUI usage so you can see it.
I will also write this:
JASS:

SetTimerStructA ~ static method create
GetTimerStructA ~ static method Get
ClearTimerStructA ~ method onDestroy

Using PUI is inherently more complicated than using ABC simply because unit attaching is complicated than timer or trigger attaching,
with units you have to check for previous attachments to make stuff MUI.

I tried to make it simpler but all simplifications would include use of textmacros and besides if you are going to write spells with unit attaching you better know how to work with structs.
So there is no easy way here, simply learn it and use it.


JASS:

scope YourSpell

//===========================================================================
private struct Data
    unit whichUnit
    private static Data array PUI  //&lt;---------

    //-------------------------------------------------------------------------  
    static method create takes unit whichUnit returns Data
        local Data ret = Data.allocate()
        set ret.whichUnit = whichUnit
        set Data.PUI[GetUnitIndex(whichUnit)] = ret // create unit-&gt;Data link
        return ret
    endmethod
    
    //-------------------------------------------------------------------------  
    static method Get takes unit whichUnit returns Data
        local integer index
    
        set index = GetUnitIndex(whichUnit) // PUI
        if Data.PUI[index] == 0 then
            debug call BJDebugMsg(&quot;|c00FFCC00&quot;+&quot;Data struct created on first use for unit: &quot; + GetUnitName(whichUnit))
            return Data.create(whichUnit)
        endif
        
        return Data.PUI[index]
    endmethod    

    //-------------------------------------------------------------------------  
    method onDestroy takes nothing returns nothing
        set Data.PUI[GetUnitIndex(.whichUnit)] = 0 // break unit-&gt;Data link
    endmethod    
endstruct

endscope
 
General chit-chat
Help Users
  • No one is chatting at the moment.
  • Ghan Ghan:
    We did!
  • Ghan Ghan:
    The old shoutbox wasn't supported anymore. We updated xenForo, so it had to be replaced.
  • jonas jonas:
    let's see if everyone finds it... the nice thing about the shoutbox was I could check on it even while logged out, but the existence of this one is hidden when you're not logged in
  • Ghan Ghan:
    We can fix that.
  • Ghan Ghan:
    Chat should show on the sidebar when not logged in now.
  • Ghan Ghan:
    (You'll still need to log in to post messages)
  • Ghan Ghan:
    Test!
  • tom_mai78101 tom_mai78101:
    I must be in a test server.
  • tom_mai78101 tom_mai78101:
    Nice, Twitter tweets embedding now works
  • Wizard Wizard:
    Yup.
  • Ghan Ghan:
    Excellent.
  • Ghan Ghan:
    @tom_mai78101 Hello there.
  • Ghan Ghan:
    Tagging works in the chat too.
  • tom_mai78101 tom_mai78101:
    @Ghan Missed it.
  • Wizard Wizard:
    Still fixing things here and there. Added widgets to the portal, will make it match the ones here on the forum index tomorrow.
  • Ghan Ghan:
    The venerable World Editor Tutorials site has been converted to HTTPS at last.
  • jonas jonas:
    cool
  • jonas jonas:
    and I can even edit my messages, nice
  • seph ir oth seph ir oth:
    GENERAL CHIT CHAT, YOU ARE A BOLD ONE
  • Ghan Ghan:
    Hello there
  • The Helper The Helper:
    this new chatbox is great and the forum software update is great too
    +1
  • The Helper The Helper:
    upgrade has fixed forum registration spam problem
  • tom_mai78101 tom_mai78101:
    Something tells me we might be able to customize the chatbox a bit, considering that there's a gap under every message.
  • Wizard Wizard:
    Going to deploy a fix soon, just had to take some time for myself this weekend.

    Members online

    Affiliates

    Hive Workshop
    Top