14. July 2020

Quake Downloads

We have it…

(Intelligent) Weapon Switching

This tutorial will allow each client that plays your mod to have their own customizable weapon switching. Quake2 had locked weapon precedences, but back in the days of the original Quake each player could choose their own precedences. So in Quake2 the BFG was the “best” gun, then the rail gun, then hyperblaster, then rocket launcher, etc. But in Quake you could choose your own order.

1. Quake3 Weapon Switching ?

So what cool weapon switching features did Quake3 bring us? None; sadly. In Quake3 you have two options: auto switch, and manual switch. Autoswitch means that any time you pick up a weapon you will automatically use it. Even if you’ve got the BFG in hands and you pick up a shot gun you’ll be forced to draw the shot gun. Talk about lame… And manual switch is even worse! In manual mode it never forces you to draw a gun. So you have to manually switch to the guns as you pick them up.

Neither of the methods in Quake3 is acceptable to the hard core gamer. So we’re going to do something about it 🙂

This tutorial will show you how to implement Quake style weapon switching which is fully customizable on a per user basis. Each user will have a cvar “cg_weaponOrder” that controls their weapon preferences. Let’s get started then…

We’ll be modifying the following files:

 

  • cg_weapons.c
  • cg_main.c
  • cg_local.h
  • cg_event.c
  • g_items.c
  • g_main.c
  • bg_public.h

    2. Define some Global Variables and Function Prototypes

    First open cg_local.h and find the “cg_main.c” definitions:

         // 
         // cg_main.c 
         // 
         const char *CG_ConfigString( int index ); 
         const char *CG_Argv( int arg ); 
    
         void QDECL CG_Printf( const char *msg, ... ); 
         void QDECL CG_Error( const char *msg, ... ); 
    
         void CG_StartMusic( void ); 
    
         void CG_UpdateCvars( void ); 
    
         int CG_CrosshairPlayer( void ); 
         int CG_LastAttacker( void ); 
    
         //WarZone 
         #define NUM_WEAPS 9 
         extern  int cg_weaponsCount; 
         extern  int weaponOrder[NUM_WEAPS]; 
         extern  int weaponRawOrder[NUM_WEAPS]; 
         int RateWeapon (int weapon); 
         int NextWeapon (int curr); 
         int PrevWeapon (int curr);
    

    Save and close cg_local.h, we’re done with that file.

    3. Define a ‘cvar’

    Now open cg_main.c and find the cvar definitions:

         vmCvar_t cg_deferPlayers; 
         vmCvar_t cg_drawTeamOverlay; 
         vmCvar_t cg_teamOverlayUserinfo; 
    
         vmCvar_t cg_weaponOrder; //WarZone 
         int cg_weaponsCount = -1; //WarZone
    

    Now find the cvar table:

          { &cg_buildScript, "com_buildScript", "0", 0 }, // force loading of all possible
         data amd error on failures 
          { &cg_paused, "cl_paused", "0", CVAR_ROM }, 
          { &cg_blood, "com_blood", "1", CVAR_ARCHIVE }, 
          { &cg_syncronousClients, "g_syncronousClients", "0", 0 }, // communicated by systeminfo 
          { &cg_weaponOrder, "cg_weaponOrder", "1/2/3/4/5/6/7/8/9", CVAR_ARCHIVE }, //WarZone
    

    Note: you might need to add a comma at the end of the cg_syncronousClients definition (I put that comma in cyan but its hardly obvious).

    4. Define our Utility Functions

    Now we’re going to add the bulk of my new code. The new code will live right below the cvar table:

          { &cg_buildScript, "com_buildScript", "0", 0 }, // force loading of all possible data amd error on failures
    
          { &cg_paused, "cl_paused", "0", CVAR_ROM }, 
          { &cg_blood, "com_blood", "1", CVAR_ARCHIVE }, 
          { &cg_syncronousClients, "g_syncronousClients", "0", 0 }, // communicated by systeminfo
    
          { &cg_weaponOrder, "cg_weaponOrder", "1/2/3/4/5/6/7/8/9", CVAR_ARCHIVE }, //WarZone
    
         }; 
    
         int  cvarTableSize = sizeof( cvarTable ) / sizeof( cvarTable[0] ); 
    
         //<WarZone> 
         int weaponOrder[NUM_WEAPS]; 
         int weaponRawOrder[NUM_WEAPS]; 
    
         int NextWeapon (int curr) 
         { 
           int i; 
           int w = -1; 
    
           for (i = 0; i < NUM_WEAPS; i++) 
           { 
             if (weaponRawOrder[i] == curr) 
             { 
               w = i; 
               break; 
             } 
           } 
    
           if (w == -1) 
             return curr; //shouldn't happen 
    
           return weaponRawOrder[(w + 1) % NUM_WEAPS]; 
         } 
    
         int PrevWeapon (int curr) 
         { 
           int i; 
           int w = -1; 
    
           for (i = 0; i < NUM_WEAPS; i++) 
           { 
             if (weaponRawOrder[i] == curr) 
             { 
               w = i; 
               break; 
             } 
           } 
    
           if (w == -1) 
             return curr; //shouldn't happen 
    
           return weaponRawOrder[w - 1 >= 0 ? w - 1 : NUM_WEAPS - 1]; 
         } 
    
         int RateWeapon (int weapon) 
         { 
           weapon--; 
    
           if (weapon > 8 || weapon < 0) 
             return 0; //bad weapon 
    
           return weaponOrder[weapon]; 
         } 
    
         int contains(int *list, int size, int number) 
         { 
           int i; 
    
           for (i = 0; i < size; i++) 
             if (list[i] == number) return 1; 
    
           return 0; 
         } 
    
         void UpdateWeaponOrder (void) 
         { 
           char *order = cg_weaponOrder.string; 
           char weapon[3]; 
           int i, start; 
           int tempOrder[NUM_WEAPS]; 
           int weapUsed[NUM_WEAPS]; 
           int temp; 
    
           weapon[1] = '\0'; 
           memset(tempOrder, 0, sizeof(tempOrder)); 
           memset(weapUsed, 0, sizeof(weapUsed)); 
    
           i = 0; 
           while (order != NULL && *order != '\0' && i < NUM_WEAPS) 
           { 
             weapon[0] = *order; 
             order++; 
    
             if (*order != '\\' && *order != '/') 
             { 
               weapon[1] = *order; 
               weapon[2] = '\0'; 
               order++; 
             } else { 
               weapon[1] = '\0'; 
             } 
    
             if (*order != '\0') 
               order++; 
    
             temp = atoi( weapon ); 
             if (temp < 1 || temp > NUM_WEAPS) 
             { 
               CG_Printf( "Error: %i is out of range. Ignoring..\n", temp ); 
             } 
             else if ( contains( tempOrder, sizeof(tempOrder)/sizeof(tempOrder[0]), temp ) )
    
             { 
               CG_Printf( "Error: %s (%i) already in list. Ignoring..\n",
                 (BG_FindItemForWeapon( temp ))->pickup_name, temp );
    
             } else { 
               tempOrder[i] = temp; 
               weapUsed[temp - 1] = 1; 
               i++; 
             } 
           } 
    
           //error checking.. 
           start = 0; 
           for (i = 0; i < NUM_WEAPS; i++) 
           { 
             if (weapUsed[i]) 
               continue; 
             CG_Printf( "Error: %s (%i) not in list. Adding it to front of the list..\n",
               (BG_FindItemForWeapon( i + 1 ))->pickup_name, i + 1 );
    
             weaponRawOrder[start++] = i + 1; 
           } 
           //build the raw order list 
           for (i = start; i < NUM_WEAPS; i++) 
             weaponRawOrder[i] = tempOrder[i - start]; 
    
           //built the remaping table 
           for (i = 0; i < NUM_WEAPS; i++) 
             weaponOrder[weaponRawOrder[i] - 1] = i + 1; 
    
         } 
         //</WarZone>
    

    Ok now let’s talk about what that code does for a sec. We just added NextWeapon(), PrevWeapon(), RateWeapon(), contains(), and UpdateWeaponOrder(). UpdateWeaponOrder() will be called everytime a player modifies their weapon order. NextWeapon() and PrevWeapon() are used to cycle through the players’ weapon precendeces. RateWeapon() returns the “rating” of a weapon which is used to determine if a new weapon is better than the one the player is already carrying. contains() is a simple helper function which returns true if a list contains a certain value.

    5. Respond to Client Weapon Order Changes

    Now find CG_UpdateCvars():

         void CG_UpdateCvars( void ) { 
          int   i; 
          cvarTable_t *cv; 
    
          for ( i = 0, cv = cvarTable ; i < cvarTableSize ; i++, cv++ ) { 
           trap_Cvar_Update( cv->vmCvar ); 
          } 
    
          // check for modications here 
    
          // If team overlay is on, ask for updates from the server.  If its off, 
          // let the server know so we don't receive it 
          if ( drawTeamOverlayModificationCount != cg_drawTeamOverlay.modificationCount ) {
    
           drawTeamOverlayModificationCount = cg_drawTeamOverlay.modificationCount; 
    
           if ( cg_drawTeamOverlay.integer > 0 ) { 
            trap_Cvar_Set( "teamoverlay", "1" ); 
           } else { 
            trap_Cvar_Set( "teamoverlay", "0" ); 
           } 
          } 
    
           //WarZone 
           if ( cg_weaponsCount != cg_weaponOrder.modificationCount ) 
           { 
             UpdateWeaponOrder(); 
             cg_weaponsCount = cg_weaponOrder.modificationCount; 
           } 
         }
    

    That code will call UpdateWeaponOrder() whenever the cvar “cg_weaponOrder” is modified.Save and close cg_main.c, we’re done with that file.

    Next open cg_weapons.c and find the function CG_DrawWeaponSelect(). You can either comment out, or delete the old function. Replace it with this function instead:

         void CG_DrawWeaponSelect( void ) { 
          int  i; 
          int  bits; 
          int  count; 
          int  weap; 
          int  x, y, w; 
          char *name; 
          float *color; 
    
          // don't display if dead 
          if ( cg.predictedPlayerState.stats[STAT_HEALTH] <= 0 ) { 
            return; 
          } 
    
          color = CG_FadeColor( cg.weaponSelectTime, WEAPON_SELECT_TIME ); 
          if ( !color ) { 
            return; 
          } 
          trap_R_SetColor( color ); 
    
          // showing weapon select clears pickup item display, but not the blend blob 
          cg.itemPickupTime = 0; 
    
          // count the number of weapons owned 
          bits = cg.snap->ps.stats[ STAT_WEAPONS ]; 
          count = 0; 
          for ( i = 1 ; i < NUM_WEAPS ; i++ ) { //WarZone 
            if ( bits & ( 1 << i ) ) { 
              count++; 
            } 
          } 
    
          x = 320 - count * 20; 
          y = 380; 
    
          weap = weaponRawOrder[NUM_WEAPS - 1]; //WarZone -- select last weapon 
          for ( i = 0 ; i < NUM_WEAPS ; i++ ) { //WarZone 
            weap = NextWeapon( weap ); 
    
            if ( !( bits & ( 1 << weap ) ) ) { 
              continue; 
            } 
    
            CG_RegisterWeapon( weap ); 
    
            // draw weapon icon 
            CG_DrawPic( x, y, 32, 32, cg_weapons[weap].weaponIcon ); 
    
            // draw selection marker 
            if ( weap == cg.weaponSelect ) { 
              CG_DrawPic( x-4, y-4, 40, 40, cgs.media.selectShader ); 
            } 
    
            // no ammo cross on top 
            if ( !cg.snap->ps.ammo[ weap ] ) { 
              CG_DrawPic( x, y, 32, 32, cgs.media.noammoShader ); 
            } 
    
            x += 40; 
          } 
    
          // draw the selected name 
          if ( cg_weapons[ cg.weaponSelect ].item ) { 
            name = cg_weapons[ cg.weaponSelect ].item->pickup_name; 
            if ( name ) { 
              w = CG_DrawStrlen( name ) * BIGCHAR_WIDTH; 
              x = ( SCREEN_WIDTH - w ) / 2; 
              CG_DrawBigStringColor(x, y - 22, name, color); 
            } 
          } 
    
          trap_R_SetColor( NULL ); 
         }
    

    This new function will display the weapons’ icons in the player’s customized weapon order — very cool : )

    6. Replace CG_NextWeapon_f() and CG_PrevWeapon_f

    Now scroll down a bit and replace the function CG_NextWeapon_f() with this function:

         void CG_NextWeapon_f( void ) { 
          int  i; 
          int  original; 
    
          if ( !cg.snap ) { 
           return; 
          } 
          if ( cg.snap->ps.pm_flags & PMF_FOLLOW ) { 
           return; 
          } 
    
          cg.weaponSelectTime = cg.time; 
          original = cg.weaponSelect; 
    
           for ( i = 0 ; i < NUM_WEAPS ; i++ ) { //WarZone 
           cg.weaponSelect = NextWeapon( cg.weaponSelect ); //WarZone 
    
           if ( cg.weaponSelect == WP_GAUNTLET ) { 
            continue;  // never cycle to gauntlet 
           } 
           if ( CG_WeaponSelectable( cg.weaponSelect ) ) { 
            break; 
           } 
          } 
         }
    

    This function taps into the NextWeapon() function we added to cg_main.c to select the next weapon from the user’s customized weapon order.Scroll down again and replace CG_PrevWeapon_f() with this function:

         void CG_PrevWeapon_f( void ) { 
          int  i; 
          int  original; 
    
          if ( !cg.snap ) { 
           return; 
          } 
          if ( cg.snap->ps.pm_flags & PMF_FOLLOW ) { 
           return; 
          } 
    
          cg.weaponSelectTime = cg.time; 
          original = cg.weaponSelect; 
    
          for ( i = 0 ; i < NUM_WEAPS ; i++ ) { //WarZone 
           cg.weaponSelect = PrevWeapon( cg.weaponSelect ); //WarZone 
    
           if ( cg.weaponSelect == WP_GAUNTLET ) { 
            continue;  // never cycle to gauntlet 
           } 
           if ( CG_WeaponSelectable( cg.weaponSelect ) ) { 
            break; 
           } 
          } 
         }
    

    This function taps into the PrevWeapon() function we added to cg_main.c to select the weapon before the current weapon (not to be confused with “select the last weapon I was using” functionality) from the user’s customized weapon order.

    7. Replace CG_OutOfAmmoChange()

    Keep scrolling down and replace CG_OutOfAmmoChange() with this function:

         void CG_OutOfAmmoChange( void ) { 
           int  i; 
           int  weap; 
    
           cg.weaponSelectTime = cg.time; 
           weap = weaponRawOrder[NUM_WEAPS - 1]; //WarZone -- pick the best weapon they have 
    
           for ( i = 0 ; i < NUM_WEAPS ; i++, weap = PrevWeapon( weap )) { 
             if ( CG_WeaponSelectable( weap ) ) { 
               if (weap != WP_GAUNTLET) 
               { 
                 cg.weaponSelect = weap; 
                 return; 
               } 
             } 
           } 
    
           cg.weaponSelect = WP_GAUNTLET; 
         } 
    

    This causes the best weapon the player has (accoring to their customized weapon order) to be selected. If no suitable weapon is found, the Gauntlet will be selected.Save and close cg_weapons.c, we’re done with that file.

    8. Change CG_ItemPickup

    Next open cg_event.c and find the function CG_ItemPickup() and replace it with this one:

         static void CG_ItemPickup( int itemNum, int isnewitem ) { //WarZone 
           cg.itemPickup = itemNum; 
           cg.itemPickupTime = cg.time; 
           cg.itemPickupBlendTime = cg.time; 
           // see if it should be the grabbed weapon 
           if ( bg_itemlist[itemNum].giType == IT_WEAPON && isnewitem ) //WarZone 
           { 
             // select it immediately 
             if ( cg_autoswitch.integer && bg_itemlist[itemNum].giTag != WP_MACHINEGUN ) { 
               if (RateWeapon( bg_itemlist[itemNum].giTag) > RateWeapon( cg.weaponSelect )) //WarZone 
               { 
                 cg.weaponSelectTime = cg.time; 
                 cg.weaponSelect = bg_itemlist[itemNum].giTag; 
               } 
             } 
           } 
         }
    

    If you were paying attention just then, you’d have noticed that I changed the function header for CG_ItemPickup() to include a new parameter “isnewitem”. This is the part of the code where this modification switches from client side only to include server side changes. The reasoning behind this is that the client module has no effective way of deciding if the player has just collected an item, or it the player has had one for a while. There are methods of bypassing the server code, but none of them worked effectively (it is possible to create an “oldweapons” variable and compare the current set against the old set, but this method breaks when the player dies and looses all of his weapons at once).

    9. Add a New Item Pickup Event

    Now find the CG_EntityEvent() function and scroll down to the EV_ITEM_PICKUP event:

           case EV_ITEM_PICKUP2: 
           DEBUGNAME("EV_ITEM_PICKUP2"); 
             es->number = es->otherEntityNum; //this is a bit of a hack.. but it works GRREAT! 
           case EV_ITEM_PICKUP: 
           DEBUGNAME("EV_ITEM_PICKUP"); 
           { 
            gitem_t *item; 
            int  index; 
            int   isnewitem; //WarZone 
    
            index = es->eventParm;  // player predicted 
            isnewitem = es->otherEntityNum2; //WarZone 
    
            if ( index < 1 || index >= bg_numItems ) { 
             break; 
            } 
            item = &bg_itemlist[ index ]; 
    
            // powerups and team items will have a separate global sound, this one 
            // will be played at prediction time 
            if ( item->giType == IT_POWERUP || item->giType == IT_TEAM) { 
              trap_S_StartSound (NULL, es->number, CHAN_AUTO, trap_S_RegisterSound( "sound/items/n_health.wav" ) ); 
            } else { 
              trap_S_StartSound (NULL, es->number, CHAN_AUTO, trap_S_RegisterSound( item->pickup_sound ) ); 
            } 
    
            // show icon and name on status bar 
            if ( es->number == cg.snap->ps.clientNum ) { 
              CG_ItemPickup( index, isnewitem ); //WarZone 
            } 
           } 
           break;
    

    Here again is the reference of new server code. The es->otherEntityNum2 must be set server side so that the client code will know whether the item being picked up is a new item or not. The new “EV_ITEM_PICKUP2” event will be explained further down.

    10. Another Item Pickup Modification

    Next scroll down to the EV_GLOBAL_ITEM_PICKUP event and make the following change:

          case EV_GLOBAL_ITEM_PICKUP: 
           DEBUGNAME("EV_GLOBAL_ITEM_PICKUP"); 
           { 
            gitem_t *item; 
            int  index; 
    
            index = es->eventParm;  // player predicted 
    
            if ( index < 1 || index >= bg_numItems ) { 
              break; 
            } 
            item = &bg_itemlist[ index ]; 
            // powerup pickups are global 
            trap_S_StartSound (NULL, cg.snap->ps.clientNum, CHAN_AUTO, trap_S_RegisterSound( item->pickup_sound ) ); 
    
            // show icon and name on status bar 
            if ( es->number == cg.snap->ps.clientNum ) { 
              CG_ItemPickup( index, 1 ); //WarZone 
            } 
           } 
           break;
    

    That’s it for cg_event.c, save and close.

    11. Change Touch_Item() Behaviour

    Finally open g_items.c (in the /game code) and find the Touch_Item() function. We’re only going to be making a few small changes:

         void Touch_Item (gentity_t *ent, gentity_t *other, trace_t *trace) { 
          int   respawn; 
          int   had = 1; //WarZone 
    

          case IT_WEAPON: 
             //WarZone 
             if ( other->client->ps.stats[STAT_WEAPONS] & (1 << ent->item->giTag) ) 
               had = 1; 
             else 
               had = 0; 
           respawn = Pickup_Weapon(ent, other); 
           break; 
    

    Now rip out the if ( other->client->pers.predictItemPickup) bit so that it looks like this :

          if ( 0 ) { //WarZone 
            //do nothing... 
          } else { 
             //WarZone 
             gentity_t *event; 
    
             event = G_TempEntity(ent->s.origin, EV_ITEM_PICKUP2); //WarZone
             event->s.eventParm = ent->s.modelindex; 
             event->s.otherEntityNum = other->s.number; 
             event->s.otherEntityNum2 = !had; //WarZone -- used to tell cgame if its a new weapon 
             event->r.svFlags |= SVF_BROADCAST; //broadcast it to everyone
    
    //         G_AddEvent( other, EV_ITEM_PICKUP, ent->s.modelindex ); //kill this line 
          } 
         ... 
         }
    

    Note: The “…” lines mean “some code in between” — do not put the “…” lines in your code.If you were paying attention just then you realized that we just made a drastic change to the item pickup logic. The reasoning behind this is that (first and foremost) predicted events can only have one parameter; the second reason to this is that the predictable event structure isn’t very reliable. Have you ever been playing Q3 and run over a weapon, but not hear the pick up sound? If so that is because the predictable events structure is getting flooded and the EV_ITEM_PICKUP event is getting lost.

    Now the EV_ITEM_PICKUP events will be sent via an external temp entity to ensure that these vital event messages are not lost in the heat of combat.

    Alright save and close g_items.c, done with that file.

    12. Make the Server Show that it Supports Weapons Ordering

    Now for the last and final addition! Open g_main.c (also in /game) and scroll down to the end of the cvar table:

          { &g_inactivity, "g_inactivity", "0", 0, 0, qtrue }, 
          { &g_debugMove, "g_debugMove", "0", 0, 0, qfalse }, 
          { &g_debugDamage, "g_debugDamage", "0", 0, 0, qfalse }, 
          { &g_debugAlloc, "g_debugAlloc", "0", 0, 0, qfalse }, 
          { &g_motd, "g_motd", "", 0, 0, qfalse }, 
          { &g_blood, "com_blood", "1", 0, 0, qfalse }, 
    
          { &g_podiumDist, "g_podiumDist", "80", 0, 0, qfalse }, 
          { &g_podiumDrop, "g_podiumDrop", "70", 0, 0, qfalse }, 
    
          { &g_allowVote, "g_allowVote", "1", 0, 0, qfalse }, //WarZone -- make sure there is a comma after the line above
          { NULL, "g_supportsWeaponOrder", "1", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse } //WarZone
    

    This additional cvar allows players to see which servers support the cg_weaponOrder variable. This is important because the new client code will not be compatible with standard Q3A servers. It would be very easy to make a filter for GameSpy (et al) to filter out servers that don’t have g_supportsWeaponOrder set to 1.Save and close g_main.c, we’re almost done…

    We almost forgot to define EV_ITEM_PICKUP2! Whoops.. so, open up bg_public.h and make this quick addition:

          EV_POWERUP_QUAD, 
          EV_POWERUP_BATTLESUIT, 
          EV_POWERUP_REGEN, 
    
          EV_GIB_PLAYER,   // gib a previously living player 
    
          EV_DEBUG_LINE, 
          EV_TAUNT, //make sure there is a comma here! 
          EV_ITEM_PICKUP2,   // WarZone -- used to create a temp entity to send the pickup message 
    
         } entity_event_t;
    
    Save and close bg_public.h, we're ALL DONE! 
    

    13. End Notes.

    You’ll need to do a recompile on the cgame and game code to see the changes in action. Now I’ll explain how the new cg_weaponOrder variable works. The default value for it is “1/2/3/4/5/6/7/8/9” which tells the game the order that you want the weapons to switch in. The default order will yield a result similar in concept to the Quake2 weapon switching code (if newWeapon > myWeapon then switchWeapons): if you’re holding a shotgun (3) and pick up a rocket launcher (5), the rocket launcher will be selected. If you’re holding a rocket launcher (5) and pick up a grenade launcher (4) the rocket launcher will remain selected.Each weapon has a number assigned to it that corresponds to the key press required to activate that weapon:

  • 1 = Gauntlet
  • 2 = Machine Gun
  • 3 = Shot Gun
  • 4 = Grenade Launcher
  • 5 = Rocket Launcher
  • 6 = Lightning Gun
  • 7 = Rail Gun
  • 8 = Plasma Gun
  • 9 = BFG 10KMy personal preference for the weapon order is “1/2/3/4/6/8/5/7/9” which is very similar to Quake2’s weapon order.If your mod is going to need more than 9 weapons, you’ll need to change the NUM_WEAPS #define we added to cg_local.h to the appropriate value. The cg_weaponOrder string needs to be formated as follows:
         xx\xx\xx 
         xx/xx/xx 
         x/xx\xx 
         xx\x/xx 
         x/x\x 
         x/x/x 
         x\x\x 
    

    Meaning the string can contain up to two consectutive integer digits followed by a seperator which must be either a forward slash or a back slash (/ or \). Duplicate or invalid entries are ignored and missing entries are inserted at the begining of the list. So the string “2/2/3/3/4/5/6/7/8/8/8/9” will yield the same end result as “1/2/3/4/5/6/7/8/9” because the missing “1” is inserted at the front of the list and the duplicate values are ignored.If you have any questions or comments please email me here.

    This code is Copyright (c) 2000 by Mark “WarZone” Smeltzer. You may freely use this code as long a credit for its existance is given to the author.

 

Ads