Welcome, Guest! Login | Register

Using the Persistence Engine with Half-Life [Print this Article]
Posted by: Persuter
Date posted: May 12 2004
User Rating: N/A
Number of views: 94736
Number of comments: 9
Description: This article describes first line-by-line how to create a program that uses the Persistence Engine, and then suggests some ways to use it in a Half-Life mod.
(You can find Percy's Persistence Engine here.)

Introduction

The Persistence Engine is basically used to save and load information that you want kept across multiple Half-Life server runs. This could be best lap times, car modifications, and the like for a racing game like Half-Life Rally. It might be weapons, armor, items, and money for a role-playing game like Master Sword. It might be total kills and frag/death ratios for Metamod plugins focusing on statistics. There are numerous other possible applications. Again, any time you need to keep information between two runs of a server, or between a player logging in twice, or if you need to associate information with a particular person, the Persistence Engine is the way to do it.

But HOW to do it? In this article, I will develop a small client program that will save and load some information as we pass through a while loop. You'll be able to see each function used as it should be, in context. I'll explain what I'm doing at each piece of code, so you can understand what's going on, and I'll show the entire program, from top to bottom. (Although it should be pointed out that I will NOT be doing this within the context of a Half-Life mod. I'll talk about that later in the article.) This is a very rough and ugly program, that is only to be used as a test of the library and server.

(By the way: This tutorial and this engine are really for coders who are pretty familiar with C++ and MSVC. The function calls aren't hard but you have to be reasonably certain to understand what including header files does, or what some standard library call does. You also need to at least not be frightened away by STL, because I use in the library.)

Test Program

We start, as any good program starts, with some PRAGMAS.

 CODE (C++) 
#pragma warning (disable:4786)


This turns off the annoying warning telling me that my type names are longer than 255 characters (don't get scared, this happens because of some STL default templatizing stuff that you can't see, there aren't actually 256 character type names). Next we get a system include:

 CODE (C++) 
#include <winsock2.h>


This includes the Winsock functions. We will only need it for starting up and cleaning up, the Persistence Engine takes care of everything else. It can't call WSAStartup and WSACleanup (you'll see them later), however, because in some programs the user might want to call them at a different time (i.e., they're doing their own networking stuff), the Engine can't call them.

 CODE (C++) 
#include "persistence.h"


This is the header file from the software download.

 CODE (C++) 
int main( int argc, char* argv[] )
{
    WSADATA wsadata;
    WSAStartup( MAKEWORD( 2, 2 ), &wsadata );


OK, if the first (or, God forbid, second) line confused you, this is too advanced for you.

The third and fourth line start Winsock. Don't even worry about what it means -- it's REALLY stupid. Just close your eyes and say, "When I need to do Winsock networking, I need to put these lines first."

 CODE (C++) 
    if( StartClient( argv[1], atoi( argv[2] ) ) != 0 )
        return 1;


The important function to look at here is StartClient. We assume the first argument to the program is a dotted IP address, e.g., 192.168.1.1 in char* format. This is the server that you want to connect to. We also assume that the second argument is an integer, e.g., 10000, which is used as the port to connect to. StartClient simply connects to the server and exchanges an encryption handshake. If StartClient returns 0, everything's fine, and we drop through. If StartClient returns anything else, something's gone wrong, so we return 1 (that tells the OS there's been an error).

 CODE (C++) 
char option[255];
    char name[255];
    char pass[255];
    char data[255];


This initializes 4 255 character arrays. If... you know, if this confuses you, again, you are in the wrong tutorial. :)

 CODE (C++) 
    scanf( "%s %s %s %s", option, name, pass, data );


This reads in four strings, separated by spaces or returns, into option, name, pass, and data, respectively.

 CODE (C++) 
    while( string( option ) != string( "q" ) )
    {


This checks that option is not "q". If option is "q" we want to exit the program. Otherwise we loop through.

 CODE (C++) 
        while( HasValidatedData() )
        {
            LoginWithData ld = GetValidatedData();


OK, we come to our first real data-oriented library functions! The Persistence Engine client keeps a queue of data requests that have been validated (their name and password check out) and returned by the server. When HasValidatedData() returns true, you can call GetValidatedData() to get one of those requests. Thus, while I have validated data in the queue to pick up, I run through this loop, get it, and do the following with it:

 CODE (C++) 
if( ld.second.first )
                printf( "%s -- %s\n", ld.first.first.c_str(), (char*)(ld.second.first) );
            else
                printf( "Error: %i\n", ld.second.second );
            ReturnDataMemory( ld.second );
        }


OK, I print out the data that I received back and I return the data memory that I "borrowed" with GetValidatedData(). Now, I have to explain the pair class that I rely so heavily on. You are probably seeing all these "first"s and "second"s and becoming confused. All the pair class does is take two types, and create a class with two objects, one of each type, one called first and one called second. Thus, in persistence.h, we have the following code:

*** WARNING TO COPY-PASTERS -- THIS CODE ISN'T PART OF THE MAIN SOURCE ***

 CODE (C++) 
// Name -- password
typedef pair<string,string> LoginInfo;

// Data pointer -- Data size
typedef pair<void*,int> Data;

// Login info -- Associated Data
typedef pair<LoginInfo,Data> LoginWithData;


We see that we have three classes defined as pairs. (typedef creates an "alias" for a class type, so whenever I type LoginInfo, it's like I'm really typing pair<string,string>.) LoginInfo consists of a natural pair -- the login name and the password. Thus, I pair those together. Similarly, the Data object I am returning is a pair of a void* pointer and an int -- the data pointer and the size. For those who are wondering why I'm returning this, it's to facilitate saving data structures, which I will talk about later.

Finally, we then wish to pair a login name and password with a particular bit of data, so we create a final pair called LoginWithData. Now again, these are TYPES, it's important to understand. Now, if I have an object b of type LoginWithData, I can call b.first and get an object of type LoginInfo, or I can call b.second and get an object of type Data. So hopefully you see this is a simple way to pair up two related objects of differing types. Even better, we get constructors, so we can type LoginInfo( name, pass ), where name and pass are strings (we'll do something similar in a moment), and create an object with those two objects as first and second.

So now you should be able to read the code posted earlier. ld.first.first is an ANSI string corresponding to the name of the person whose data this is. c_str() is the function that changes an ANSI string to a char* so printf can read it. Then, ld.second.first is the data pointer of the person. We cast the data pointer directly to a char* and read it -- note that if you use this to save data structures, this will not be possible. Finally, ReturnDataMemory takes a Data object and deletes the memory. Obviously after you use this, you can't use the Data object anymore. It's important to think of these three functions as a triple -- if HasValidatedData returns true, you use GetValidatedData, do what you need to with the data, and then call ReturnDataMemory to get rid of it.

What we've done with the above loop is to check if there's data in the queue, get it, print the name and data out to the screen, and trash the data memory. Of course, in a Half-Life mod, what you would want to do is find the player with the appropriate memory and copy the data to him or her, and then trash the memory.

The check, by the way, checks if the data pointer returned is NULL. If the data pointer IS NULL, then an error has occurred. An error code is passed in the data size, ld.second.second. Here I just print it out, in real life you would probably want to compare it to the values in persistence.h. The two errors are if the login asked for doesn't exist, and if the wrong password was provided. Note that the first might not even be an error -- you might check to see if a name is taken by trying to request data for the name with some random password, and if you get back that the login doesn't exist, it's open.

 CODE (C++) 
if( string( option ) == string( "v" ) )
            RequestData( LoginInfo( string( name ), string( pass ) ) );


OK, now we start with the user interaction. If option is "v", we want to request the server to send us the data associated with the input name and password. Note how our use of the pair object and a typedef has given us a nice, well-behaved data object. Note that LoginInfo is a CONSTRUCTOR -- we are implicitly creating an object and passing it into RequestData. (Because name and pass are char*, we have to change them to strings, so we are also using string constructors on them.) Note that we don't use the data variable -- you still have to type it in. I told you this was a rough testing program.

RequestData is another Persistence Engine function. It tells the Persistence Engine client to ask the server to send it the data related to the login. This will later cause HasValidatedData() to return true. In a Half-Life mod, you might request a name and password from a player, then call RequestData with them.

 CODE (C++) 
else
            SaveData( LoginWithData( LoginInfo( string( name ), string( pass ) ), Data( data, strlen( data ) ) ) );


OK, this is a lot of parentheses because I'm using a lot of constructors. Let's take it left to right. SaveData is a function, not a constructor, from the Persistence Engine again. It simply takes in a LoginWithData and saves the data under that name and password. Note that the data formerly under that name is erased, even if it was under a different password, and this SAVES whatever you pass in to the password. With this program you'll be able to test the server directly -- get familiar with how it works. AS for the constructors and variables, you see we have a LoginWithData constructor, followed by a LoginInfo constructor that is exactly the same as in the RequestData call. We then create a Data object using the data variable as the pointer and strlen( data ) as the size. This allows us to save strings. If you want to save structs, look towards the end of this article.

 CODE (C++) 
scanf( "%s %s %s %s", option, name, pass, data );
    }


This reads in the variables for the next pass through the loop, and ends the loop. So what this loop does is check to see if option is q. If it isn't, it checks to see if we have any validated data, and if so, prints it to screen. We then check what option is, and call RequestData or SaveData on name, pass, and data accordingly. Basically, this will allow you to save and request data to a server and see what data you get back. Note that if you call RequestData, you will have to type in another command before you get back the validated data. This is because I want you to get used to this operating asynchronously. You don't want to call RequestData and then wait around for HasValidatedData to return true -- call RequestData and then leave. If this is critical data, like hit points, don't let the player enter the game until you receive the data back (it won't be long), or let them play and update their data when it comes back.

Finally, we end the program with:

 CODE (C++) 
    EndClient();

    WSACleanup();

    return 0;
}


EndClient() is a Persistence Engine function. You should view it as a partner to StartClient. StartClient connects to a server and begins a thread of execution that communicates with it. EndClient breaks the connection and ends the thread. Calling StartClient twice in a row without calling EndClient, or vice versa, will probably crash your program. WSACleanup(), again, is just a Winsock function that ends the networking and cleans up any resources. Finally, return 0 ends main.

OK, I'll now give you a short run of the program on my computer, connecting to a server, so you can see how it works. I'll put the computer's printed statements in bold. Everything else is my input.

 QUOTE  
s Percy Pass Hello
v Percy Pass -
s Percy Pass Goodbye
Percy -- Hello
v Percy Pass -
s Percy Pass2 Hello
Percy -- Goodbye
v Percy Pass -
v Percy Pass2 -
Error: -1
v Persuter Pass - 
Percy -- Hello
s Persuter Pass Hello
Error: 0
v Persuter Pass -
s Percy Pass Foo
Persuter -- Hello
q end end end


OK, from top to bottom. I first save the string "Hello" to the login Percy/Pass. I then request the data for the login Percy/Pass (note that I put a hyphen afterwards because I have to put something in the data string, you can put anything you want here, it doesn't matter). Note that I don't get an immediate response from the computer. I then tell it to save Goodbye to the login Percy/Pass. I finally get a response back from the computer showing me that I got the data "Hello" under the name "Percy", which is what I expected. I then again check the login Percy/Pass, which I expect now to be Goodbye, and if you look down two lines, you will see it is. I then save the data "Hello" to the login Percy/Pass2. I expect this to delete the information under Percy/Pass as well.

To check, I request the data under the login Percy/Pass. Looking down two lines, you see I get an error with the code -1. If you'll check persistence.h, you'll see that that's the error code for wrong password. I then check the login Percy/Pass2. Again looking down two lines, you see that I get back "Percy -- Hello", which is what I expected.

I then check the login Persuter/Pass. I get back "Error: 0", which is what I expect, because that's the error code for no login by that name found. I save "Hello" to Persuter/Pass, check the login Persuter/Pass, save something random to Percy/Pass (just to pass the time) and see that indeed I get back "Persuter -- Hello". I then type in q followed by three strings to end the program.

In a Half-Life Mod

OK, so that's a test program for you to use. But of course, many of you are reading this article to try to learn how to use this engine in your own Half-Life mod. Thus, let's look at that. I'm not going to provide any real code here, but I'll show you how you might go about it. Let's say you have a structure for your players that looks something like this:

 CODE (C++) 
struct playerdata
{
    int level;
    float besttime;
    char name[20];
    char pass[20];
};


Here you can see we store an integer, a floating-point number, and a 20-character name and password. Now, something to note about these -- you can store anything as long as it's a value type or an array of a value type. You CANNOT store pointers to things. Please don't put a CBaseEntity* pointer in here and come asking why you're getting memory errors.

OK, so how can we use this? When a person logs in, we put them in some sort of spectator mode. At this point, they have to send a "pass" command with their password as the argument to the server. We take that password and call RequestData with their nick and password. Each server frame, probably in the Think function of the game rules object, we check HasValidatedData. Once it returns true, and GetValidatedData returns a LoginWithData corresponding to the player, we assign the data as follows, assuming pPlayer is a CBasePlayer* struct, and pPlayer->data is a playerdata struct.

 CODE (C++) 
LoginWithData ld = GetValidatedData();
if( ld.second.second )
{
    pPlayer = GET_PLAYER_WITH_NICK( ld.first ); // <--- you'd write this function yourself
    pPlayer->data = *((playerdata*)(ld.second.second));
    ReturnDataMemory( ld.second );
}


OK, so what happens here? We get the validated data. If it's not an error, we get the player with the nick in the returned data. Again, GET_PLAYER_WITH_NICK is just a placeholder, you'd have to find the player with the right nick. The next is a bit tricky, but it's not hard. We pretend that ld.second.second is a pointer to a playerdata struct by casting it to it, and then dereferencing it. Because the information in ld.second.second will be set up as a playerdata struct, this will work fine. We then assign this data to the struct in pPlayer, and trash the memory used. (Note that because we're trashing the memory, you can't just have pPlayer hold the data pointer.

Now, let's say that player tells the server he wants to save, through some mechanism. Then you make sure his playerdata structure is up-to-date, and do the following, assuming that pPlayer is the player who wants to save, name is his name, and pass is the password he wants.

 CODE (C++) 
LoginInfo login( name, pass );
Data data( (void*)(&(pPlayer->data)), sizeof( playerdata ) );
SaveData( LoginWithData( login, data ) );


OK, we set up login like usual. (I'm assuming name and pass are strings, not char*, here.) We then set up the Data by finding the pointer to pPlayer->data, which is of type playerdata*, and casting it to void*. We also find the size of the playerdata struct, and put that in as the data length. Hopefully now you see why we go through all this rigamarole with the Data object -- we want to be able to save data just like this. We finally call SaveData with a LoginWithData object made up of login and data.

All working systems have two things, a "policy" and a "mechanism". The policy is made up of the rules that make the system run. The mechanism is how the system actually runs. It's similar to the difference between saying that all cars on the road should drive less than 65 and saying that they all run on internal combustion engines. The first is a policy, the second is a mechanism. In this case, the Persistence Engine is a mechanism. It is your job to come up with policies. For example, you may want to make sure that a person has the capability to load data before he can save data. If you're using this as a nickserv, for example, you need to make sure that you validate people before you allow them to save new passwords, that sort of thing. I've tried to make the Engine very general, so you need to think about how you can use it to create persistence -- it's not quite plug-and-play. It's just a tool.

Where Do We Go From Here?

So, I strongly encourage everyone not to simply shove the Engine in its present form into your mod -- that would be a bad idea. There are a number of things it doesn't implement, like making sure malicious people can't save data to the server. You can read about these in the TODO.txt kept in the software download. Basically, however, I don't plan to change the interface from the above, just the inner workings. Thus, I hope people will start testing this out and fooling around with it with an eye to putting it in their mod. I'll be adding some administration tools in the future. Thanks for reading.

Rate This Article
This article has not yet been rated.

You have to register to rate this article.
User Comments Showing comments 1-9

Posted By: Lord Booga on May 12 2004 at 03:43:50
This would have been helpful a few months ago... before we developed our own persistance engine. Argh. Never mind, good one anyway..

Posted By: Cruentus on May 12 2004 at 16:28:43
What Booga said :D, nice tho

Posted By: Bulk on Jun 13 2004 at 15:29:09
Good

Posted By: Put Put Cars on Jun 28 2004 at 14:34:58
:)

Posted By: IdeaMan on Sep 16 2004 at 01:00:31
Turns out I need 3 things in order to use this in my mod:
: Set a "World-Readable" flag on saved data so that certain data can be read by anonymous clients.
: Allow Logins but map to the same data. ( I want to give each server it's own login/pw combo or same login,diff pw)
: Save to a MySQL database (or Postgresql, or some other free database engine)

btw, I'm having a dickens of a time getting the .lib file to link in with what is basically a copy-past (+edits) of what you have above. I think it's because the .lib was built on vc6, and I'm using vc7 (.NET)

Posted By: Persuter on Sep 16 2004 at 01:10:16
Probably. It's rather annoying that MSVC's lib files are very specific. Probably need to switch this over to a DLL. I've been letting this project lag, I'll go ahead and work on it a bit more.

Posted By: Lord_Draco on Sep 20 2004 at 07:16:08
is it possible to use something other than nick name to log in? could you use the player steamid or something?

Posted By: Persuter on Sep 20 2004 at 22:35:48
Uh... yes. You do realize there's no Half-Life specific code in here anywhere, right? Any two strings can serve as a name and password.

Posted By: Lord_Draco on Sep 21 2004 at 08:28:47
i know, i was just didn't know if you could use the steam id. Thanks, and nice tutorial


You must register to post a comment. If you have already registered, you must login.

Latest Articles
3rd person View in Multiplayer
Half-Life 2 | Coding | Client Side Tutorials
How to enable it in HL2DM

By: cct | Nov 13 2006

Making a Camera
Half-Life 2 | Level Design
This camera is good for when you join a map, it gives you a view of the map before you join a team

By: slackiller | Mar 05 2006

Making a camera , Part 2
Half-Life 2 | Level Design
these cameras are working monitors that turn on when a button is pushed.

By: slackiller | Mar 04 2006

Storing weapons on ladder
Half-Life 2 | Coding | Snippets
like Raven Sheild or BF2

By: British_Bomber | Dec 24 2005

Implementation of a string lookup table
Half-Life 2 | Coding | Snippets
A string lookup table is a set of functions that is used to convert strings to pre-defined values

By: deathz0rz | Nov 13 2005


Latest Comments
knock knock
General | News
By: MIFUNE | Dec 31 2017
 
knock knock
General | News
By: omega | Dec 22 2016
 
knock knock
General | News
By: MIFUNE | Oct 10 2015
 
New HL HUD Message System
Half-Life | Coding | Shared Tutorials
By: chbrules | Dec 31 2011
 
knock knock
General | News
By: Whistler | Nov 05 2011
 
Particle Engine tutorial part 4
Half-Life | Coding | Client Side Tutorials
By: darkPhoenix | Feb 18 2010
 
Particle Engine tutorial part 2
Half-Life | Coding | Client Side Tutorials
By: darkPhoenix | Feb 11 2010
 
Particle Engine tutorial part 3
Half-Life | Coding | Client Side Tutorials
By: darkPhoenix | Feb 11 2010
 
Game Movement Series #2: Analog Jumping and Floating
Half-Life 2 | Coding | Shared Tutorials
By: mars3554 | Oct 26 2009
 
Particle Engine tutorial part 5
Half-Life | Coding | Client Side Tutorials
By: Deadpool | Aug 02 2009
 

Site Info
297 Approved Articless
8 Pending Articles
3940 Registered Members
0 People Online (3 guests)
About - Credits - Contact Us

Wavelength version: 3.0.0.9
Valid XHTML 1.0! Valid CSS!