Latest Blogs

A Quick Guide to MAG Squad Leadership

I picked up MAG (Massive Action Game) for the PS3 a few weeks ago, and I love it. It's a terrible looking game, with graphics reminiscent of UT2004. The controls can be awkward, and driving vehicles harsh.

So why do I love MAG? The chain-of-command system is the best I've ever seen. There are three positions of command: Squad Leader, Platoon Leader, and Officer In Charge. Squad Leader is the lowest level of command; one only needs to reach level 15 to become a squad leader. That's no picnic, however, and it takes a couple of days minimum. After Squad Leader is Platoon Leader, and you attain that position only by winning at least 30 games as Squad Leader. Lastly, there is Officer In Charge, akin to a Commander or General. This requires hundreds of successful games as Squad Leader or Platoon Leader.

Unlike Battlefield 2, where anyone can be the Commander, MAG requires skill to lead. Now that you know a little bit about it, I'm gonna get into my favorite position: Squad Leader.

As Squad Leader, you are responsible for yourself and seven other men in your squad. You direct your men to objectives by setting FRAGOs (Fragmentary Orders) using the control map. Your squadmates receive double points for anything they do on the objective you've set, so there is a good reason for your men to refrain from subordination. However, if you set the wrong FRAGO or, even worse, don't even set a FRAGO, your squadmates will cry "mutiny!" quicker than a fox out a hole. So for my sake and yours, here are a few tips to good Squad Leadership:

General Tips

  1. You NEED a microphone -- it's impossible to rally your men without speaking to them.
  2. Know your squadmates' names -- more specifically, know how to pronounce them. This will allow you to address your men, and allow your men to recognize they are being addressed. On a similar note, if you can, try to make your name something easy to pronounce. Because you're in a squad of 8, a name like "Zach" or "Tommy" is not likely to be repeated and will help your squadmates address you.
  3. Have a plan -- think of a plan. Any plan will do. Then tell your squadmates your plan, so they feel confident in your ability to lead, and also feel as if they are accomplishing a critical task in the big picture whenever they complete a FRAGO you have set. Which leads me to my next point:
  4. Set a FRAGO point -- soldiers without objectives play CoD. Get a FRAGO point up to direct your squadmates to the next part of your plan. Remember, they are Fragmentary Orders--you should not set the FRAGO on a primary objective. Rather, set it on something smaller, such as a bunker or the AA battery. (This rule doesn't apply to the Sabotage game mode, as the only objectives are primary objectives)
  5. Provide concise, useful intel -- if there are three men inside control point B, tell your squadmates. If there is a sniper guarding the left side of the objective, let your squadmates know. But if you got killed by some goddamn douchebag with a knife, keep it to yourself. This goes for your squadmates, too. Of course, don't tell them to shut up; just tell them you want to be updated about enemies in the battlefield.
  6. If possible, have a friend play -- in a world of skeptic and Rambo-esque FPS gamers, the best way to lead is by example. Bring along a friend who will communicate and work with you. Once you show your mettle, your unruly squadmates will follow in your footsteps. This tactic also works very well if your squad leader is uninvolved.

Sabotage

Because there are only primary objectives in Sabotage, leading is a different kind of game. Relatedly, if you're a platoon leader, you're useless unless you're beside your men. You can't utilize any command abilities, and the only benefits you provide are your passive abilities (such as shorter disarm times and longer bleedout times), which require you to be in the vicinity of your teammates.

There is only one rule:

  1. Lead with your microphone -- without FRAGOs, it's all up to your voice to keep your men motivated and on task. Let's hope you have a smoking voice.

Acquisition & Domination

These game modes are much more complex than Sabotage, and require careful planning and wicked execution.

Defense

  1. Set the FRAGO on the bunker -- yes, those burnoff towers are important, and so is that gate, but those things will be impossible to defend if your squadmates spawn 400m from them. Keep your bunker online; you can easily repair everything else.
  2. If you lose your bunker, set the FRAGO on the gates -- though the gates will probably be blown up in another 20 seconds, it gives your squad precious time to regroup, resuscitate, and recharge.

Offense

  1. Keep your APC in tip-top shape -- because the APC is a mobile spawn point and brings your squadmates closer to battle, it is a very important asset. In Domination, drive the APC to a safe position and leave it there. In Acquisition, drive the APC to the escort vehicle, blowing up gates as you go, but keep an engineer at hand to repair the APC whenever you stop.

I hope these tips help you lead your squadmates to victory again and again. Sure, there will be squads that don't listen, don't talk, and don't follow orders, but don't worry...

Because whenever squads get you down, Mrs. Brown,
And FRAGOs seem hard or tough,
And squadmates are stupid, obnoxious, or daft,
And you feel that you've had quite enough

Just remember that you're playing in a shooter that's evolving
And involving 300 men an hour
That's respawning around 8 men a second, so it's reckoned
With guns that are the source of firepower

And so on and so forth. I shall update this guide whenever I feel a new tip makes sense.

Killing a Python Sub-interpreter That Uses Threads

I was having a tough time figuring out a solution to ending sub-interpreters in Viper when that sub-interpreter had multiple threads. Just recently, though, I found a solution that works for me:

// The current tstate must be the original tstate when we call PyEval_ThreadsInitialized
PyThreadState_Swap(m_pThreadState);

/* If the plug-in has enabled threads (by using any sort of threading),
* we must kill the threads before destroying the interpreter */
if (PyEval_ThreadsInitialized())
{
// Loop through all threads in the current interpreter and raise SystemExit
PyThreadState *curstate = m_pInterpState->tstate_head;
while (curstate != NULL && curstate->interp == m_pInterpState)
{
PyThreadState_Swap(curstate);

PyThreadState_SetAsyncExc(curstate->thread_id, _PyExc_SystemExit);
curstate = curstate->next;

PyThreadState_Swap(NULL);
}

/* This is very much frowned upon in Py_EndInterpreter, but this little
* hack makes Python happy. I don't know the consequences of this line,
* but until I do, I won't feel guilty >_> */
m_pInterpState->tstate_head = m_pThreadState;
}

PyThreadState_Swap(m_pThreadState);
Py_EndInterpreter(m_pThreadState);

m_pInterpState is the PyInterpreterState of the sub-interpreter it's destroying, and m_pThreadState is the original PyThreadState returned by Py_NewInterpreter. I think you can tell by my comments that you should take caution when using this code. However, the code works perfectly: the threads die and the interpreter gets killed, all without a single runtime error.

All About Viper, Part 2: The Technical

In my previous post, I told you all about the story around Viper. Now I'm going to tell you about the story inside Viper!

How Viper Embeds Python

Figuring out how to embed CPython has been an arduous task. There is little documentation on embedding, and few examples (Blender is a bad example, because it merely runs scripts -- it doesn't need to keep track of any details of the scripts). I did my best with those docs, the API reference, and the CPython source code. Oh, I can't forget the fabulous Mattie Casper, who helped me a lot with figuring out how to link CPython on both Windows and Linux.

I don't know if any of my issues have been rectified in CPython3000, but if not, the CPython community needs to do some real work before it becomes a viable solution for embedding.

Linking

Because Viper is not the main program that is run, the default search path for dynamically linked libraries does not include the directory where Viper is executed from. It only includes the srcds/bin/ directory. As most game server providers do not allow access to this directory, Viper needs some way to load CPython from a specified directory.

Windows

On Windows, Viper makes use of delay-loading. Delay-loading DLLs is a simplified way of saying MSVC turns all import references into calls to an import loading function. For example, it would take:

PyInt_FromLong(4);

And turn it into:

__imp_LoadFromDelayLoadedDLL("PyInt_From_Long")(4);

This works out great, except when you need an instance managed by the DLL. For instance, Viper needs Py_None and PyExc_TypeError. MSVC can't handle the loading of that data, so you must do it manually.

Luckily, Py_None is:

#define Py_None &_Py_NoneStruct

This made it possible to do the following:

#undef Py_None
PyObject *Py_None = (PyObject *)GetProcAddress(python25_DLL, "Py_NoneStruct");

Thus, I didn't have to modify Viper's code at all. I wasn't so fortunate with other data. For instance, PyExc_TypeError is defined as a PyObject, and not a pointer to a PyObject. This made it impossible to just #undef it and redefine it. I had to declare my own variable and update all references to &PyExc_TypeError with my new variable:

PyObject *_PyExc_TypeError = (PyObject *)GetProcAddress(python25_DLL, "PyExc_TypeError");

Linux

On Linux, Viper uses the RPATH (runtime linking path) of the ELF format. The RPATH allows you to set an absolute path at compile time where the runtime linker will check for dynamically linked libraries.

For the most part, this works great. I don't need to change any code to use both data and functions from Python. However, there is one pitfall...

The RPATH used in Viper is this:

$ORIGIN/viper/lib/plat-linux2

$ORIGIN expands to the directory where viper.ext.so resides. Unfortunately, some game server providers (e.g., GameServers.com) use the server IP and port in the path to the server, meaning $ORIGIN might expand to:

/home/user/133.7.90.01:27015/srcds/cstrike/addons/sourcemod/extensions/

This causes problems, because RPATH accepts multiple paths separated by a colon. But because the runtime linker has no way to differentiate between a colon in a path and a colon separating paths, it just assumes all colons separate paths. So with our previous example, the runtime linker ends up searching for python25.so in both the /home/user/133.7.90.01 and 27015/srcds/cstrike/addons/sourcemod/extensions paths -- neither of which exist.

Currently, the only solution to this problem is to ask the game server provider for access to the srcds/bin/ directory, and manually place python25.so in it.

Plug-in Management

Separation of Contexts

A fun part of the Viper development process, and still a large zit in the face of widespread CPython embedding, is the separation of plug-in contexts. Though it's impossible to completely separate plug-ins, because low-level functions such as os.close can conflict, CPython does provide sub-interpreters which allow for as much independence as manageable. Each sub-interpreter has its own copies of the __main__, __builtin__, and sys modules, as well as new sys.path and sys.modules lists.

There are a few downfalls to using sub-interpreters, but most of them are only applicable in a multi-threaded environment, of which Viper is not. The major downfall to Viper, then, is a minor jump in complexity: every new sub-interpreter introduces a new thread state (not an actual thread), which must be switched to when calling functions from a plug-in.

When a plug-in calls a Viper function, the correct thread state is already set, but when the Source Engine calls a Viper function (say, because a client executed a console command), the wrong thread state may be set, so Viper must manually set the thread state. Fortunately, this is very easy to implement and manage:

// In CPlugin ctor
m_pThreadState = Py_NewInterpreter();

// Meanwhile...
PyObject *CPluginFunction::Execute(PyObject *args, PyObject *keywds)
{
PyThreadState *_save = PyThreadState_Get();
PyThreadState_Swap(m_pPlugin->m_pThreadState);
PyObject *result = PyObject_Call(m_pFunc, args, keywds);
PyThreadState_Swap(_save);
return result;
}

Viper saves the current thread state in _save, then resinstates it at the end of the function.

Identifying a Plug-in From a Function Call

A tougher and even more enjoyable problem was figuring out what plug-in called a function in Viper. I tried many things -- first, I tried setting a global variable inside the Python plug-in context with a pointer to the CPlugin, but then it was impossible to determine the plug-in if it called a Viper function from an imported module.

Next, I tried to store a pointer to the CPlugin in a dictionary managed by the thread state, but if any new thread states were created (e.g., because twisted created a new thread) there would be no way to determine the plug-in.

Finally, I figured out that every thread state stored a pointer to the sub-interpreter that owned it. Viper now stores a pointer to the sub-interpreter with the CPlugin, and maintains a list of all the sub-interpreters associated connected to their plug-ins. When a Viper function is called from within a plug-in, Viper grabs the current thread state, retrieves the sub-interpreter from that thread state, and finally finds it in the sub-interp list:

CPlugin::CPlugin(char const *file)
{
// ...
m_pThreadState = Py_NewInterpreter();
// ...
m_pInterpState = m_pThreadState->interp;
}

CPlugin *CPluginManager::GetPluginOfInterpreterState(PyInterpreterState *interp)
{
SourceHook::List<CPlugin *>::iterator iter;
for (iter=m_list.begin(); iter!=m_list.end(); iter++)
{
if ((*iter)->GetInterpState() == interp)
return (*iter);
}

return NULL;
}

// Elsewhere...
IViperPlugin *pPlugin;
PyThreadState *tstate = PyThreadState_Get();
pPlugin = g_VPlugins.GetPluginOfInterpreterState(tstate->interp);

Plug-in Metadata

Like SourceMod, Viper provides a way for plug-ins to supply some simple metadata -- title, author, description, version, and url. Unlike SourceMod/SourcePawn, there is no way in Python to know the global variables inside a plug-in before runtime, so Viper first executes the plug-in, then retrieves a dict variable named "myinfo" from the plug-in's global scope:

# In the plug-in
myinfo = {
'name': "Hello World!",
'author': "theY4Kman",
'description': "Salutations, Celestial Rock",
'version': "1.0"
}

To retrieve the fields:

void CPlugin::UpdateInfo()
{
#define RETRIEVE_INFO_FIELD(field)\
{ \
PyObject *_obj; \
if ((_obj = PyDict_GetItemString(myinfo, #field)) != NULL) \
{ \
PyObject *str = PyObject_Str(_obj); \
m_info.field = sm_strdup(PyString_AsString(str)); \
Py_DECREF(str); \
} \
}

PyObject *myinfo = PyDict_GetItemString((PyObject*)m_pPluginDict, "myinfo");
if (myinfo == NULL || !PyDict_Check(myinfo))
return;

RETRIEVE_INFO_FIELD(name);
RETRIEVE_INFO_FIELD(author);
RETRIEVE_INFO_FIELD(description);
RETRIEVE_INFO_FIELD(version);
RETRIEVE_INFO_FIELD(url);

#undef RETRIEVE_INFO_FIELD
}

Forwards

A forward is essentially a list of callback functions and the function prototype that callbacks need to adhere to. When an event occurs, the owner of the forward can fire it, executing each callback in succession. Depending on the configuration of the forward, a callback function may return a "stop" signal, telling the forward to cease execution. For example, SourceMod uses this kind of forward when a BanClient is called, so that a plug-in may cancel a ban.

Viper allows for the creation of two fundamental types of forwards:

  • Named forwards: Viper manages these, and any plug-in may add a callback to the forward's list if it knows the name of the forward.
  • Anonymous forwards: the owning plug-in has complete control over what functions are added and removed from the callback list.

In both cases, the owner plug-in controls when the forward is fired. Forwards also have many different ways to process the return values of callbacks. The following options affect what myforward.fire() returns:

  • ET_Ignore: always returns None, and does nothing when a callback returns; useful for notifications, such as OnClientDisconnected.
  • ET_Event: returns the highest integer returned by a callback
  • ET_LowEvent: returns the lowest integer returned by a callback
  • ET_Hook: returns the highest integer returned by a callback, but will stop executing callbacks if one returns the stop signal. Used in forwards such as BanClient, so that one plug-in can cancel a ban and stop the execution of other callbacks.

These options may work for some cases, but many times you need to process every return value of the callbacks. For this, Viper allows you to pass a function that will be called every time a callback returns. This function will be passed the return value of the callback function. The value that the processor function returns is substituted for the callback's return value when it is processed according to the options above.

A named Viper forward looks like this:

from sourcemod import *

def process_return(rvalue):
return rvalue

myforward = forwards.create('backwardforward', process_return, forwards.ET_Hook, int, str)

def callback(number, string):
print "%d: %s" % (number, string)
if number == 1:
return Plugin_Stop
else:
return Plugin_Continue

forwards.register('backwardforward', callback)

myforward.fire(1, 'the loneliest number')
# "1: the loneliest number" is printed, Plugin_Stop is returned, execution of callbacks ends,
# and myforward.fire() returns Plugin_Stop (which is an int of 2)

An anonymous forward:

from sourcemod import *

myforward = forwards.create('', None, forwards.ET_Ignore, int, str)

def callback(number, string):
print "%d: %s" % (number, string)
if number == 1:
return Plugin_Stop
else:
return Plugin_Continue

myforward.add_function( callback)

myforward.fire(1, 'the loneliest number')
# "1: the loneliest number" is printed, Plugin_Stop is returned, and myforward.fire() returns None

print len(myforward) # 1
myforward.remove_function(callback)
print len(myforward) # 0

The Client Object

Clients have tons of data associated with them -- death count, IP address, Steam ID, name, average latency, health, et cetera ad infinitum. Faced with writing getters for all of these properties, I made use of the extra data allowed by Python to be passed to the getter function:

enum clients__client__getsets_t
{
CLIENT_GETSET_ABS_ANGLES = 0,
CLIENT_GETSET_ABS_ORIGIN,
CLIENT_GETSET_ALIVE,
// ...
CLIENT_GETSET_MODEL,
CLIENT_GETSET_WEAPON,
};

static PyObject *
clients__Client__getter_valid_ingame_modsupport(clients__Client *self,
clients__client__getsets_t type)
{
// ... Check client is valid ...
switch (type)
{
case CLIENT_GETSET_ABS_ANGLES:
return CreatePyVector(pInfo->GetAbsAngles());
case CLIENT_GETSET_ABS_ORIGIN:
return CreatePyVector(pInfo->GetAbsOrigin());
case CLIENT_GETSET_ALIVE:
return PyBool_FromLong(!pInfo->IsDead());
// ...
case CLIENT_GETSET_MODEL:
return PyString_FromString(pInfo->GetModelName());
case CLIENT_GETSET_WEAPON:
return PyString_FromString(pInfo->GetWeaponName());
}
Py_RETURN_NONE;
}

static PyGetSetDef clients__Client__getsets[] = {
{"abs_angles", (getter)clients__Client__getter_valid_ingame_modsupport, NULL,
"The client's angles vector.\n"
"@throw ViperError: Invalid client, client not in-game, or no mod support.",
(void *)CLIENT_GETSET_ABS_ANGLES},
// ...
{NULL},
};

Essentially, I defined an enum with the available client properties, created a function that checks for the appropriate conditions (e.g., making sure the client is connected, not a bot, etc), and finally uses a switch to return the correct data. I'm sure that solution was obvious to the rest of you, but I copied and pasted and edited a getter function for all of those properties before I figured it out.

How Viper is Documented

Since Viper Mark II, documentation has been a considerate concern of mine. After all, good documentation can be the difference between "vital tool" and "useless crap." I wanted a tool that made it easy to add and edit functions and modules, and also had support for user comments. I wanted something like PHP's docs, which are amazing (and without those amazing docs, PHP would be a cluttered pile of excrement). With no luck whatsoever, I eventually settled on just having alright documentation.

Sphinx is the documentation tool used by CPython. Essentially, Sphinx translates specialized reStructuredText into HTML, and provides pretty CSS and a JavaScript search page. It doesn't have many extras, but what it does provide works very well.

You can view Viper's docs online here. On the left side of each page is a Show Source link that displays the reStructuredText source of the current page.

In the future, I want to write a documentation tool using Django that allows for user comments. I recently started using Markdown (this post is written using Markdown), and I'm impressed, so I'll probably use Markdown for the new docs.

How Viper is Built

On Windows, building Viper is pretty simple. I use Visual Studio to manage the project -- I punch in the settings I want in the configuration window (GUI) and hit Build.

For Linux, I used a Makefile for the longest time. I took the sample Makefile for extensions from SourceMod and changed it to what Viper needed. It worked fine, but, like any Makefile, it was ugly and hacky.

Recently, I switched Viper's build system (on Linux only, thus far) to SCons. SCons is a build system that uses Python for its configuration files. It allows you to specify possible actions by calling functions in the configuration file, then, on the command-line, lets you run those actions, just like a Makefile:

extension = env.SharedLibrary(target="$BUILD_DIR/viper", source=SOURCE)
env.Default(extension)
env.Alias("extension", extension)

install = env.Install(SRCDS_BASE + "/cstrike/addons/sourcemod/extensions/auto.1.ep1/", extension)
env.Alias("install", install)

With that SConstruct (the configuration file for SCons), one can run these commands from bash:

scons extension # builds the extension
scons install # builds the extension and moves it to a directory
scons extension install # builds the extension and moves it to a directory

The env.SharedLibrary function instructs SCons to build a shared library (.dll on Windows or .so on Linux), and the env.Install function instructs SCons to move a file from one place to another. The env.Alias function names an action, so it can be called from the command-line. Finally, the env.Default function sets the default action to run if no action is passed to scons on the command-line.

SCons is wonderful because it's written in Python, which enables me to do some pretty cool things. For example, when I call env.Install, I pass extension, which is the env.SharedLibrary action that builds viper.ext.so. This informs SCons that the "install" action is dependent upon building the shared library, and that it should copy the output of the env.SharedLibrary to the folder. It greatly simplifies building, and allows for less hacky code! Plus, it also supports creating archives, checking code out from a repository and compiling, and much, much more!

I know what you're thinking! You're thinking, "it's too good to be true." Well, good sir or madam, you are unfortunately correct. SCons still has its fair share of flaws.

SCons has you create an Environment object (that's what env is in the example above), which is used when building. By default, it's filled with useful variables, such as CXX, CPP, and SHLIBSUFFIX, that control what tools SCons executes, and how it executes them. That's all well and good, except that the provided values to most of these env vars are usually completely useless, in the best case. In the worst case, they cause tons of commotion, and you don't even know what env var is causing it.

For example, I was building Viper one time when I noticed -fPIC was among the options (-fPIC instructs the compiler to create position independent code, which is useless most of the time). I never wrote that flag in, so I spent hours determining the source. Finally, I discovered it: SCons was putting SHCCFLAGS on my build line to gcc, which contained "-fPIC" and nothing but "-fPIC". The easy fix was to set it to blank. After that, everything was fine.

SCons also has an odd way of placing object files. It makes you address files as if they existed under the build directory. So if you wanted to build sdk/smsdk_ext.cpp, you'd tell SCons to build build/debug/sdk/smsdk_ext.cpp. Maybe it helps 90% of the people using SCons, but I think it only helps 1% and annoys the hell out of everyone else. Luckily, Python gives me a quick fix:

_SOURCE = [... 'sdk/smsdk_ext.cpp', ...]
SOURCE = [env["BUILD_DIR"] + "/" + target for target in _SOURCE]

My last real complaint is with command-line variables. SCons provides a few different ways to extract data from the command-line. One of them is with EnumVariable. The goal of an EnumVariable is to map the string value of a variable entered by a user to a value specified in the configuration file.

env_var_ENGINE = EnumVariable(
"ENGINE", "The Source engine version to compile against",
None, ("original", "orangebox", "left4dead"),
map={
"original": SE_EPISODEONE,
"orangebox": SE_ORANGEBOX,
"left4dead": SE_LEFT4DEAD
},
# ignorecase=1 converts the user's supplied value to lowercase
ignorecase=1
)

The above example would allow the user to run:

scons ENGINE=original

Then SCons would translate the user's value ("original") into the mapped value (SE_EPISODEONE). Somewhere along the line, though, a SCons developer decided to use the following code to retrieve the value of an EnumVariable:

converter(env.subst("$ENGINE"))

env.subst returns the value of the passed variable. But because $ENGINE is an EnumVariable, env.subst() effectively becomes converter(). That's bad news bears, because the inner converter() call yields SE_EPISODEONE, but the outer call, *converter(SE_EPISODEONE), gives an error, because it is not a mapped value.

It's very stupid. My workaround is to provide the actual value of SE_EPISODEONE to the list of possible user entered values:

env_var_ENGINE = EnumVariable(
"ENGINE", "The Source engine version to compile against",
None, ("original", "orangebox", "left4dead",
str(SE_EPISODEONE), str(SE_ORANGEBOX), str(SE_LEFT4DEAD)),
map={
"original": SE_EPISODEONE,
"orangebox": SE_ORANGEBOX,
"left4dead": SE_LEFT4DEAD
},
ignorecase=1
)

If you'd like, you can check out Viper's full SConstruct file at github.

All About Viper, Part 1: The Story

Beginnings

Years ago, when I had just begun to pick up heavier languages from PHP and JavaScript, I discovered SourceMod (SM) -- a third-party extension to the game engine used by Half-Life 2, named the Source Engine. (Actually, I started learning EventScripts first, but I was saved when I found SourceMod.) Though still in beta stage, SourceMod provided a rewarding learning environment for an aspiring programmer. I wrote a few plug-ins and marveled at what I was capable of. I really enjoyed working with the Source Engine and SourceMod, despite the oddities and quirks of each.

The SM community was amazing then, as it still is now -- they'll show you how to fix your plug-in, troubleshoot your server installation, or release useful tools for free, like Basic-Master's syntax highlighting code editor, Pawn Studio, or Nican's web-based API viewer. As I learned more and more about programming and SourceMod, I befriended many people in the community, including the dev team (except for sawce; you can keep him).

During the summer of '07, pRED of the dev team jokingly instructed me to drop Python into SourceMod, because I was such an avid fan of Python. Because yaks don't understand sarcasm, I took pRED's words literally.

SMPython was built from the ground up using loose bits of C++ gathered from past experiences with Java and PHP, and the SM source code and wiki. I coded it on Windows, because I'd never used Linux before. I figured out how to delay-load the Python library (more on that later) and run Python scripts in-game. The code was held together by toothpicks and mud, but I was proud of it. After all, I was just an immature 15-year-old. I'd never done anything of real value.

Name Change

I announced SMPython to #sourcemod. Everyone thought it was cool [and/or laughed at me], but they didn't like the name. After a few weeks, they convinced me to change the name. But what name would fit the project? What's catchy?

Then I had an epiphany. A cool guy named Viper hung out in #sourcemod. He runs SteamFriends and is involved in many Half-Life 2 modding community events and functions. I thought "hey, Viper is a snake, just like Python." I really liked the name. Plus, if I changed SMPython to Viper, the cool guy Viper would be highlighted many times as the project Viper was discussed on #sourcemod.

And so, I changed SMPython to Viper.

Middles

I was having lots of trouble with all the hacky code I wrote, so I decided to take a hiatus from development. I went on with my life, learning many new concepts and skills on and off the computer. When I finally came back to SMPython a good year later, I was a less-immature 16-year-old, and I was disgusted with the code my 15-year-old self had crapped out.

I scrapped the entire codebase and started anew. My new code was a lot better. It had structure and meaning. I spent hours thinking over and discussing the design of Viper with people from the SourceMod community. It was also written completely for Linux.

Before my hiatus with Viper Mark 1, I'd decided to install and boot into Linux to compile Viper. What I found was an awesome experience. Sure, I had some troubles at the beginning, but I figured them all out -- first with Google, then by reading programs' source code. I loved the power of customization given to me by the grand open-source project of GNU/Linux. Anyways, Viper code trouble ensued, and I went into hiatus.

Writing Viper in Linux forced many changes. For one, loading Python was greatly simplified, but introduced a couple problems (more on this later). I also had to write a Makefile to build the project, unlike with Visual Studio, where the build process was automated -- the development program I used on Windows.

I completed around 80% of what I wanted to complete before releasing Viper. But then the unexpected happened: I lost motivation. I made a final git push into the Viper repository before booting over to Windows 7 and never looking back to Linux.

Currents

In late December of '09, I decided to release what everything I had on Viper. It turned out I only had the Linux build for the Orange Box version of the Source Engine -- the engine used on Team Fortress 2, Portal, and Half-Life 2: Episode 2.

I finished the documentation of Viper's API, wrote up a quick explanation as to why I was releasing Viper so poorly, and posted it on the AlliedModders forums. I am still working to get Viper ready for a Windows release.

Profiles in History

Profiles in History

The following excerpts are transcripts of the knockoff movie Night of the Day of the Dawn of the Son of the Bride of the Return of the Revenge of the Terror of the Attack of the Evil, Mutant, Alien, Flesh-eating, Hellbound, Zombified Living Dead Part 2 - In Shocking 2-D

 

George Bernard Shaw
George Bernard ShawGeorge Bernard Shaw was at a dinner party one evening and became involved in a conversation with a young lady. He said to the young lady, "Ma'am, if someone paid you one million dollars to sleep with them, would you?" And she said, "Uh, yes; yes, that's a lot of money. Of course I would." George Bernard Shaw smiled and said, "Well, that's interesting. If someone paid you five dollars to sleep with them, would you?" And the young lady said, "Five dollars? That's ridiculous. That's no money at all. Of course not. What do you think I am?" George Bernard Shaw smiled again and said, "We've already established what you are. Now we're just trying to establish a price."

 

Calvin Coolidge
Calvin CoolidgeCalvin Coolidge was one of the presidents of the United States, and was referred to by many friends as "Quiet Cal," because he never really spoke too much. There was a socialite—a young lady—who knew Calvin Coolidge and who thought that she could get him to speak. She made a bet with her friends that she could get Calvin Coolidge to say more than two words. She threw a party and invited Calvin Coolidge. He was there. He was standing in a corner and she was with her friends. She looked at him and smiled and waved; and he smiled back. She walked up to him and said, "I'll be quite frank with you. I made a bet with my friends that I could you to say more than two words." Calvin Coolidge looked at her and said, "You lose," and walked away.

 

Edgar Allen Poe
Edgar Allen PoeEdgar Allen Poe was at a dinner party and became engaged in a conversation with a woman. The conversation took a turn for the worse and very soon it became an argument. Finally, the lady, very angry, said to Edgar Allen Poe, "Mr. Poe, if you were my husband, I'd poison your coffee." Edgar Allen Poe, without missing a beat, said, "If you were my wife, I'd drink it."

 


* All photos and profile texts taken directly from the film *

1 2 3 4 5 Next »