Posted by: Persuter
Date posted: May 12 2004 User Rating: N/A | Number of views: 92789 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.
| | #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:
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.
This is the header file from the software download.
| | 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."
| | 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).
| | 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. :)
| | 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.
| | 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.
| | 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:
| | 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 ***
| | typedef pair<string,string> LoginInfo;
typedef pair<void*,int> 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.
| | 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.
| | 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.
| | 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:
| | 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.
| | 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:
| | 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.
| | LoginWithData ld = GetValidatedData(); if( ld.second.second ) { pPlayer = GET_PLAYER_WITH_NICK( ld.first ); 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.
| | 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. |
|
User Comments
Showing comments 1-9
This would have been helpful a few months ago... before we developed our
own persistance engine. Argh. Never mind, good one anyway.. |
|
What Booga said :D, nice tho |
|
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)
|
|
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. |
|
is it possible to use something other than nick name to log in? could you use the player steamid or something? |
|
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.
|
|
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.
|
297 Approved Articless
6 Pending Articles
3940 Registered Members
0 People Online (5 guests)
|
|