Post tutorial RSS Where is Poppy - Your First Custom Entity - Part 2

In this, the second part in the “Where is Poppy” modding tutorial series I take you through the steps for creating your own custom mesh loading entity.

Posted by on - Intermediate Server Side Coding

Where is Poppy
Your First Custom Entity


You can find the accompanying PDF here.

The Original Tutorial can be read here.

You can Download the Project Source Code and Releases through Github.


This is Part 2

Due to the monstrous size of this tutorial and the styling applied I had to split it into two parts because of Moddb's 100000 Character Limit.

You can Read the First Part here

You can read the complete tutorial over at Sourcemodding.

Animation

You might wonder why we want to include animation with something that is supposed to be an inanimate static object? There may be situations for instance where we use this loader for a plant or a bush which could include a sway animation to mimic movement in the wind.

Let’s add to our header file an integer to hold the id for what animation our currently loaded model should be playing. I add it to the end of our other unsigned short for ease of use and reuse of code.

unsigned short m_iCollisionMode, m_iSequence;

Next let’s add to the KeyValue Function so that it reads in the correct value for the animation sequence.

if (FStrEq(pkvd->szKeyName, "animate"))
{
 m_iSequence = atoi(pkvd->szValue);
 pkvd->fHandled = TRUE;
}

The whole function should now look like this:

void CStaticMesh::KeyValue(KeyValueData *pkvd)
{
 if (FStrEq(pkvd->szKeyName, "animate"))
 {
  m_iSequence = atoi(pkvd->szValue);
  pkvd->fHandled = TRUE;
 }
 else if (FStrEq(pkvd->szKeyName, "collisionmode"))
 {
  m_iCollisionMode = atoi(pkvd->szValue);
  pkvd->fHandled = TRUE;
 }
 else if (FStrEq(pkvd->szKeyName, "bbmins"))
 {
  UTIL_StringToVector(mins, pkvd->szValue);
  pkvd->fHandled = TRUE;
 }
 else if (FStrEq(pkvd->szKeyName, "bbmaxs"))
 {
  UTIL_StringToVector(maxs, pkvd->szValue);
  pkvd->fHandled = TRUE;
 } 
 else
  CBaseEntity::KeyValue(pkvd);
}

We must then set the sequence based on what the user input through hammer. We do that in the Spawn function near the end. We must add the following:

pev->sequence = m_iSequence;

Another small change we should make while we are in the Spawn function is to change this:

ExtractBbox(0, mins, maxs);

To:

ExtractBbox(m_iSequence, mins, maxs);

This makes sure that the correct sequence Collision box will be used if the user selects to use that collision mode.

We then need to add the following to our Animate function so that the animation can play.

pev->frame > 255 ? pev->frame = 0 : pev->frame++;


For those unfamiliar with the above line it is known as a ternary operation, basically an inline if else statement. Its written like this simply as an arguably easier alternative which is a little faster to write.

The equivalent as an if else would be:

if (pev->frame > 255){
 pev->frame = 0;
}
else{
 pev->frame++
}

Basically we are just making sure that the frames are incrementing with each update and if we reach the max of 255 reset to zero and start incrementing again.

pev->frame controls the individual frames in a sequence.

We must change our FGD from:

@PointClass color(0 255 0) size(-20 -20 -20, 20 20 20) studio() = wip_StaticMesh : "wip_StaticMesh"
[
model(studio) : "Model"
 bbmins(string) : "Collision Volume Mins" : "-16 -16 -16"
 bbmaxs(string) : "Collision Volume Maxs" : "16 16 16"
 spawnflags(flags) =
     [
         1: "Solid?" : 1
         2: "Debug Bounding Box?" : 0
     ]
   collisionmode(choices) : "Collision Mode" : 2 = 
 [
  0: "None"
  1: "Manual Inputs"
  2: "Sequence Based"
 ]
]

To:

@PointClass color(0 255 0) size(-20 -20 -20, 20 20 20) studio() = wip_StaticMesh : "wip_StaticMesh"
[
 sequence(integer) : "Animation Sequence (Editor)" : 0 : "Sequence to display in Jackhammer. This does not affect gameplay."
 animate(integer) : "Animation Sequence (Game)" : 0 : "Setting an in game Animation Sequence for the selected model"
 model(studio) : "Model"
 bbmins(string) : "Collision Volume Mins" : "-16 -16 -16"
 bbmaxs(string) : "Collision Volume Maxs" : "16 16 16"
 spawnflags(flags) =
     [
         1: "Solid?" : 1
         2: "Debug Bounding Box?" : 0
     ]
     collisionmode(choices) : "Collision Mode" : 2 = 
 [
  0: "None"
  1: "Manual Inputs"
  2: "Sequence Based"
 ]
]

We use the following to set a sequence that we can visualize in the editor, for some reason this does not work for actually setting the value we would expect to see in game and that’s why we have a separate animate key. I believe that the word “sequence” is reserved much like the keys “mins” and “maxs”. I could not for the life of me get them to work.

sequence(integer) : "Animation Sequence (editor)" : 0 : "Sequence to display in Jackhammer. This does not affect gameplay."

And finally we use the following to set what sequence will actually be used in game

animate(integer) : "Animation Sequence (Game)" : 0 : "Setting an in game Animation Sequence for the selected model"

Save the FGD, compile the code, restart Hammer and you should see 2 new entries in the properties of our entity.



The Editor Entry changes it for the editor and the Game Entry changes it in game so be sure to set the in game entry correctly if you intend to use animations.

Try the editor version and watch as your model plays the different animations you switch to. I highlight this option as an easy way to preview what sequence you play and its totally optional, rip it out if you don’t need it.

If you load tree.mdl into JHLMV note that you have two animations to choose from. A sequence number is given to be used as an ID/Index, this is the number you provide in hammer to select a Sequence.



This current implementation does not provide for people who do not want to play animations. So let’s prepare a Flag and a condition in the code to cater for this.

Change the following to our header:

bool m_bDebugBB = false;

To:

bool m_bDebugBB = false, m_bAnimate = false;

And add the following after that:
I explain why I skip 3 below.

#define WIP_ANIMATE  4

Next in our CPP change the following in our Spawn Function:

pev->sequence = m_iSequence;

To:

// Check if the User wants to animate
if (FBitSet(pev->spawnflags, WIP_ANIMATE))
{
 m_bAnimate = true;
pev->sequence = m_iSequence;
}

Then in our Animate function change:

pev->frame > 255 ? pev->frame = 0 : pev->frame++;

To:

if (m_bAnimate){
 pev->frame > 255 ? pev->frame = 0 : pev->frame++;
}

Again this is cheaper to check a Boolean in the update loop as opposed to checking the state of the Flag which involves calling further functions.

Add the following to our flags section in the FGD

4: "Animate?" : 1

Note: that I skip the number 3 here and in the header which seems to be a feature/bug/limit in how flags work or are sent between the FGD and the game or between the Map and the game. Flags are stored as a power of 2. So the series to set them goes like this 1, 2, 4, 8, 16, 32, 64, 128, 256 etc.

All the spawn flags together should now look like this:

spawnflags(flags) =
[
     1: "Solid?" : 1
     2: "Debug Bounding Box?" : 0
     4: "Animate?" : 1
]

Save the FGD, Compile the code, restart hammer and check out our new flag in the entities properties flags tab.



Test your changes with the flag enabled and observe the inanimate model within our seen.

Let’s also add a speed variable to our entity to control how fast the model’s sequence plays back in game.

Add the following float to our header

float m_flAnimationSpeed = 0.0f;

Then add it to our KeyValue Function to read in its data. We use atof() not atoi()

if (FStrEq(pkvd->szKeyName, "animationspeed"))
{
 m_flAnimationSpeed = atof(pkvd->szValue);
 pkvd->fHandled = TRUE;
}

Let’s add it to our animate function where it will be used to set the animation speed. Note: 1.0 = normal speed, Greater than 1.0 = faster animation, we will use a negative number to change the direction of the animation, which involves a little extra coding.

Let’s change:

if (m_bAnimate){
 pev->frame > 255 ? pev->frame = 0 : pev->frame++
}

To:

if (m_bAnimate){
 if (m_flAnimationSpeed >= 0.0){
  pev->frame > 255 ? pev->frame = 0 : pev->frame += m_flAnimationSpeed;
 }
 else{
  pev->frame < 0 ? pev->frame = 255 : pev->frame += m_flAnimationSpeed;
 }
}

Basically what I am doing here is checking if we are animating, then I am checking our speed variable for what was input in the map, If it’s a positive number, we increment using the speed as the increment value If it’s a negative number, we decrement using the speed as the decrement value effectively reversing the animation.

1.0 = normal forward animation speed, default
>1.0 = faster than normal speed, dependant on what you input
0.0 = No animation
0.0 = slower animation speed
<= -1 = reversed animation and a lower negative value gives faster reverse playback

You could argue against my animate flag here (and use the animation speed condition instead) since a value of 0.0 for the animation speed means the model wont animate either but I wanted to also show you that flag id’s were powers of 2.

Add our latest addition to the FGD:

animationspeed(string) : "Animation Speed" : "1.0"

Restart hammer, check the new property and test out both positive and negative values.

Scaling our Mesh

The last major addition I want to bring to our entity is the ability to scale our mesh.

This does not work out of the box for meshes in GoldSrc as the support was only built in for sprites. However, we can make a small change to our client project which would enable pev->scale for meshes.

Locate in the hl_cdll project the file StudioModelRenderer.cpp.

In the function:

CStudioModelRenderer::StudioSetUpTransform (int trivial_accept)

Towards the bottom lets add the following:

if (m_pCurrentEntity->curstate.scale != 0 && m_pCurrentEntity->curstate.scale != 1.0)
{
 for (int i = 0; i < 3; i++)
 {
  for (int j = 0; j < 3; j++)
  {
   (*m_protationmatrix)[i][j] *= m_pCurrentEntity->curstate.scale;
  }
 }
}

Basically this checks for a change in the models scale. The scale setting by default is 0 since it was unused prior to this. If its zero technically we shouldn’t see it so let’s not modify it if its zero.

A scale of 1 would also mean no change and the model would be its original scale baked into the mdl file.

Anything between those numbers require that the rotation matrix be modified by multiplying each value in the matrix by the scale input by the user through the hammer level.

Let’s tie it all together by returning to wip_static_mesh.h

Add the following float:

float m_fModelScale = 1.0f;

Then we must also consider that we have to modify the collision boxes. We scale them by the same amount we scale the visible mesh.

Change the following:

if (FBitSet(pev->spawnflags, WIP_IS_SOLID))
{
 pev->solid = SOLID_BBOX;
 
  if (m_iCollisionMode == 1)
 {
  UTIL_SetSize(pev, mins, maxs);
 }
 else if (m_iCollisionMode == 2)
 {
  ExtractBbox(m_iSequence, mins, maxs);
  UTIL_SetSize(pev, mins, maxs);
 }
 
 if (FBitSet(pev->spawnflags, WIP_DEBUG_BB)){
  m_bDebugBB = true;
 }
}

To:

if (FBitSet(pev->spawnflags, WIP_IS_SOLID))
{
 pev->solid = SOLID_BBOX;
 
  if (m_iCollisionMode == 1)
 {
  mins = mins * m_fModelScale;
  maxs = maxs * m_fModelScale;
 
  UTIL_SetSize(pev, mins, maxs);
 }
 else if (m_iCollisionMode == 2)
 {
  ExtractBbox(m_iSequence, mins, maxs);
 
  mins = mins * m_fModelScale;
  maxs = maxs * m_fModelScale;
 
  UTIL_SetSize(pev, mins, maxs);
 }
 
 if (FBitSet(pev->spawnflags, WIP_DEBUG_BB)){
  m_bDebugBB = true;
 }
}
 
pev->scale = m_fModelScale;

We then modify KeyValue to include a check for the scale.

Add the following:

else if (FStrEq(pkvd->szKeyName, "modelscale"))
{
 m_fModelScale = atof(pkvd->szValue);
 pkvd->fHandled = TRUE;
}

Finally let’s add to the FGD

modelscale(string) : "Model Scale (Game)" : "1.0" : "Set the Model Scale (0.0 - 1.0)"
scale(string) : "Model Scale (Editor)" : "1.0" : "Set the Model Scale (0.0 - 1.0)"

Again the word “scale” seems to be reserved and won’t pass values to the game engine so we use “scale” for the editor and “modelscale” for ingame scale changes.

Save the FGD, Compile the Code, make scale changes in Hammer and load the game to see a scaled Mesh. If you enable the bounding box visualizer and set the collision mode to sequence you can see the collision box scale perfectly with your model.

Here is a xen tree scaled by 0.2:

Error Prevention

I noticed during development of this entity two scenarios one of which could be hard to detect and debug.

They both relate to loading the mesh itself.

If the model you are trying to load does not exist, the game will throw a precaching error and tell you which model is missing.



That’s useful enough in this case because you know what model is missing so you can simply locate and fix the issue.

The other issue causes a crash with no error and occurs if you create an instance of our entity but do not apply a model file to it.

To avoid this crash, I have made a small null.mdl model which says no model loaded and I use the defaults parameter in the FGD to set this model.

That way when you set the model in hammer you will see this null.mdl instead of a solid box.

model(studio) : "Model" : "models/null.mdl" : "Set a Mesh to load into the Game"

I placed this custom model in my Mods model folder.

Its looks like this:



Without our custom null.mdl set it would look like this:



And would cause a crash.

To avoid the crash let’s make a small change to the following code:

Change:

PRECACHE_MODEL((char *)STRING(pev->model));
SET_MODEL(ENT(pev), STRING(pev->model));

To:

if (pev->model != 0){
 PRECACHE_MODEL((char *)STRING(pev->model));
 SET_MODEL(ENT(pev), STRING(pev->model));
}
else{
 ALERT(at_console, "[wip_staticMesh] Error, Model Failed to load!\n");
 ALERT(at_console, "[wip_staticMesh] Setting model/null.mdl in its place!\n");
 PRECACHE_MODEL("models/null.mdl");
 SET_MODEL(ENT(pev), "models/null.mdl");
}

So now even if the user removes the defaults from the entity to force the error we will always load the null.mdl (provided it hasn’t been removed) to visualize that a model is missing.

Final Reference Code

I have also made some changes to the header file to account for the possibility that some variable might be used and haven’t yet been set to a value. To do this is simply assign each to a default value.

Header

/******************************
Where is Poppy
29.8.2016
 
wip_static_mesh.h
 
Static Mesh Loader for the mod
Where is Poppy
 
*******************************/
 
#ifndef WIP_STATIC_MESH_H
#define WIP_STATIC_MESH_H
 
#include "extdll.h"  // Required for KeyValueData
#include "util.h"  // Required Consts & Macros
#include "cbase.h"  // Required for CPointEntity
 
class CStaticMesh : public CBaseAnimating
{
private:
 void Spawn(void);
 void EXPORT Animate(void);
 void KeyValue(KeyValueData *pkvd);
 
 Vector mins = { 0, 0, 0 },
     maxs = { 0, 0, 0 };
 
 unsigned short m_iCollisionMode = 0,
       m_iSequence = 0;
 
 bool m_bDebugBB = false,
   m_bAnimate = false;
 
 float m_flAnimationSpeed = 1.0f,
    m_fModelScale = 1.0f;
 
 #define WIP_IS_SOLID 1
 #define WIP_DEBUG_BB 2
 #define WIP_ANIMATE  4
};
 
#endif

Source CPP

/******************************
Where is Poppy
February, 2017
 
wip_static_mesh.cpp
 
Static Mesh Loader for the mod
Where is Poppy
 
*******************************/
 
#include "wip_static_mesh.h"
 
// Need to Link our class to the name (wip_StaticMesh) that hammer will read from the FGD.
// This will be linked directly to the level as well so that the engine can link to it.
LINK_ENTITY_TO_CLASS(wip_StaticMesh, CStaticMesh);
 
///////////////////////////////
// Spawn(void)
//
// The Spawn function handles the creation and intialization of our entitty
// It is the second function to run in this Class
////////////////////////////////
void CStaticMesh::Spawn(void)
{
 // Precache and Load the model
 if (pev->model != 0){
  PRECACHE_MODEL((char *)STRING(pev->model));
  SET_MODEL(ENT(pev), STRING(pev->model));
 }
 // If the Model doesnt exist, print an error and set a default null.mdl as the model
 else{
  ALERT(at_console, "[wip_staticMesh] Error, Model Failed to load!\n");
  ALERT(at_console, "[wip_staticMesh] Setting model/null.mdl in its place!\n");
  PRECACHE_MODEL("models/null.mdl");
  SET_MODEL(ENT(pev), "models/null.mdl");
 }
 
 // Check if the Solid Flag is set and if so be sure to set it 
 // solid with appropriate Collisions
 // If not we also do not set a Collision Box because for a static mesh 
 // there is no reason to do so..
 if (FBitSet(pev->spawnflags, WIP_IS_SOLID))
 {
  // Set Model solid
  pev->solid = SOLID_BBOX;
 
  // Check the collision mode 0 = None, 1 = Manual, 2 = Sequence based
   if (m_iCollisionMode == 1)
  {
   // Scale the collision box 
   mins = mins * m_fModelScale;
   maxs = maxs * m_fModelScale;
 
   // Set Collision box Size
   UTIL_SetSize(pev, mins, maxs);
  }
  else if (m_iCollisionMode == 2)
  {
   // Grab Bounding box size from current sequence
   ExtractBbox(m_iSequence, mins, maxs);
 
   // Sacle the collision box
   mins = mins * m_fModelScale;
   maxs = maxs * m_fModelScale;
 
   // Set Collision box Size
   UTIL_SetSize(pev, mins, maxs);
  }
 
  // Check if the bounding box Visualizer flag is set
  if (FBitSet(pev->spawnflags, WIP_DEBUG_BB)){
   m_bDebugBB = true; // If so set this value to true, we using it in Animate()
  }
 }
 
 // Set the visible meshes scale
 pev->scale = m_fModelScale;
 
 // Check if the User wants to animate
 if (FBitSet(pev->spawnflags, WIP_ANIMATE))
 {
  m_bAnimate = true; // Used in Animate()
  pev->sequence = m_iSequence; // Set the animation based on what the user set in the level
 }
 
 // Set our update/think function to Animate()
 SetThink(&CStaticMesh::Animate);
 // Set when in the future to update next
 pev->nextthink = gpGlobals->time + 0.01;
}
 
///////////////////////////////
// KeyValue(KeyValueData *pkvd)
//
// The KeyValue function imports values set by our level editor in our map
// These Keys are created in our FGD
// We set local variables to the values that the map returns when requested
// It is the first function to run in this Class
////////////////////////////////
void CStaticMesh::KeyValue(KeyValueData *pkvd)
{
 // Grab the speed our animation plays at
 // 0.0 here also stops the animation
 // A netagive value plays the animation in reverse
 // A higher value speeds up the animation
 if (FStrEq(pkvd->szKeyName, "animationspeed"))
 {
  m_flAnimationSpeed = atof(pkvd->szValue);
  pkvd->fHandled = TRUE;
 }
 // In-Game version of editor only variable "sequence"
 // Set an integer to what sequence you want this model to play ingame
 else if (FStrEq(pkvd->szKeyName, "animate"))
 {
  m_iSequence = atoi(pkvd->szValue);
  pkvd->fHandled = TRUE;
 }
 // Set a mode for Collision
 // 0 = No Collision
 // 1 = Manual Mins & Maxs
 // 2 = Sequence Based Collision
 else if (FStrEq(pkvd->szKeyName, "collisionmode"))
 {
  m_iCollisionMode = atoi(pkvd->szValue);
  pkvd->fHandled = TRUE;
 }
 // Minimum Bounding box position
 else if (FStrEq(pkvd->szKeyName, "bbmins"))
 {
  UTIL_StringToVector(mins, pkvd->szValue);
  pkvd->fHandled = TRUE;
 }
 // Maximum Bounding box position
 else if (FStrEq(pkvd->szKeyName, "bbmaxs"))
 {
  UTIL_StringToVector(maxs, pkvd->szValue);
  pkvd->fHandled = TRUE;
 }
 // Set the scale of our model and collision boxes
 // In-Game version of the editor only "scale" keyword
 else if (FStrEq(pkvd->szKeyName, "modelscale"))
 {
  m_fModelScale = atof(pkvd->szValue);
  pkvd->fHandled = TRUE;
 }
 // defaults
 else
  CBaseEntity::KeyValue(pkvd);
}
 
 
///////////////////////////////
// Animate(void)
//
// The Animate function is basically the Update function of this Entitiy
// You add thinks here that you want to change on a frame by frame basis
// Things like animations
// Position changes
// Interactive code
////////////////////////////////
void CStaticMesh::Animate(void)
{
 // Set when in the future to next run the animate function
 pev->nextthink = gpGlobals->time + 0.01;
 
 // If animation is allowed
 if (m_bAnimate)
 {
  if (m_flAnimationSpeed >= 0.0)
  {
   // Ternary condition to update the models normal animation + any extra speed the user adds 
   pev->frame > 255 ? pev->frame = 0 : pev->frame += m_flAnimationSpeed;
  }
  else
  {
   // Ternary condition to update the models reverse animation + any extra speed the user adds
   pev->frame < 0 ? pev->frame = 255 : pev->frame += m_flAnimationSpeed;
  }
 }
 // Visualize the Collision Volume around the model
 if (m_bDebugBB){
  UTIL_RenderBBox(pev->origin, pev->mins, pev->maxs, 1, 0, 255, 0);
 }
}

FGD

In case you hadn’t noticed FGDs support a default value as well as the option to include a short description of what the function does. For Example

// Key(type)  : Name    : Default Value   : Description of function
 
model(studio) : "Model" : "path/to/model" : "Select a Model to Load"
 
sequence(integer) : "Animation Sequence (editor)" : 0 : "Sequence to display in Jackhammer. This does not affect gameplay."

I cleaned up the FGD to include descriptions which can be shown in the help section of the Entity in Hammer.

// Where is Poppy Forge Game Data
// Cathal McNally
// sourcemodding@gmail.com
// www.sourcemodding.com
 
// 29.8.2016
 
// wip_StaticMesh
 
@PointClass color(0 255 0) size(-20 -20 -20, 20 20 20) studio() = wip_StaticMesh : "wip_StaticMesh"
[
 animate(integer) : "Animation Sequence (Game)" : 0 : "Setting an in game Animation Sequence for the selected model"
 sequence(integer) : "Animation Sequence (Editor)" : 0 : "Sequence to display in Jackhammer. This does not affect gameplay."
 animationspeed(string) : "Animation Speed" : "1.0" : "Set the Speed of your animation. 1.0 = normal, 0.0 - 1.0 is slower, greater than 1 is faster and less or equal to -1 reverses the animation"
 modelscale(string) : "Model Scale (Game)" : "1.0" : "Set the Model Scale (0.0 - 1.0)"
 scale(string) : "Model Scale (Editor)" : "1.0" : "Set the Model Scale (0.0 - 1.0)"
 model(studio) : "Model" : "models/null.mdl" : "Set a Mesh to load into the Game"
 bbmins(string) : "Collision Volume Mins" : "-16 -16 -16" : "Set the Minuimum Collision position for our Manually set Bounding Volume" 
 bbmaxs(string) : "Collision Volume Maxs" : "16 16 16" : "Set the Maximum Collision position for our Manually set Bounding Volume"
 spawnflags(flags) =
    [
        1: "Solid?" : 1 : "Enable Collisions?"
        2: "Debug Bounding Box?" : 0 : "Show a visual representation of the bounding box?"
        4: "Animate?" : 1 : "Animate the Model?"
    ]
    collisionmode(choices) : "Collision Mode" : 2 = 
 [
  0: "None" : "No Collisions"
  1: "Manual Inputs" : "Enter Manual Min and Max values for a Custom Bounding Volume?"
  2: "Sequence Based" : "Take the Bounding volume from the selected Animation Sequence?"
 ]
]

Click on Help in our object properties. The help tips in hammer for our entity looks like this:

Further Reading

Special Thanks

Sam Vanheer aka Solokiller, for his insight into engine features and his ever eager nature to help me.
Elias Ringhauge aka eliasr, for his tutorial on the collision system on GoldSrc and for taking the time to help me understand it better, especially the visualizer for the Collision box.

I hope this tutorial helps you get to grips with coding your own entities in GoldSrc. If you find any issues or if you know of anything this document should include please feel free to send a mail onto me concerning it.

The support thread for this tutorial can be found over on the forums.

You can download the Project Source Code, Assets and Release through Github

Comments
eliasr
eliasr

Thanks for the thanks, but also for your contribution.
I'm in the middle of making a new teleporter, and your tutorial will be a great help to me.

Reply Good karma Bad karma+2 votes
eliasr
eliasr

Update on "A very technical explanation regarding the engine"
Use Archive.ph

The forum post has been deleted, most likely due to the issue described here: Reddit.com

Reply Good karma Bad karma+1 vote
sourcemodding Author
sourcemodding

Thank you. I suspect it was removed for that reason as well. such a shame but thankfull for the archived snapshot :)

Reply Good karma+1 vote
Post a comment
Sign in or join with:

Only registered members can share their thoughts. So come on! Join the community today (totally free - or sign in with your social account on the right) and join in the conversation.