Someone had to do it! In my continuing quest to become a Perl guru, let me tell you about embedding Perl in your Quake 2 DLL. I've implemented the calls to Perl in what I hope is a simple API - use it or modify it as you prefer. Note: This is not a tutorial on Perl! Get the "Learning Perl" (http://www.ora.com/catalog/lperl2/index.html) and "Programming Perl" (http://www.ora.com/catalog/pperl2/index.html) books from O'Reilly and Associates. These are THE books on Perl. At least basic familiarity with programming Perl is assumed for the rest of this tutorial. First, you'll need Perl installed on your system. If using Windows, you must download Gurusamy Sarathy's binary version of Perl for Win32 at ftp://ftp.digital.com/pub/plan/perl/CPAN/ports/win32/Standard/x86. ActiveState's build of Perl for Win32 is great, but embedding Perl with their library is vastly different from Unix builds, so we'll use Sarathy's version for maximum portability. Okay, now that you have Perl installed, let's look at the code for implementing this Perl API. // // cch_perlq2.c // // Adding a perl interpreter to Quake 2 #include // from the Perl distribution #include // ditto static PerlInterpreter *myPerl; void InitPerl() { char *argv[] = { "", "perlq2.pl" }; int argc = 2; myPerl = perl_alloc(); perl_construct(myPerl); perl_parse(myPerl, NULL, argc, argv, NULL); perl_run(myPerl); } I32 PerlEval(char *string) { return perl_eval_sv(newSVpv(string, 0), G_DISCARD); } int GetPerlInt(char *string) { return SvIV(perl_get_sv(string, FALSE)); } float GetPerlFloat(char *string) { return SvNV(perl_get_sv(string, FALSE)); } char *GetPerlString(char *string) { STRLEN length; return SvPV(perl_get_sv(string, FALSE), length); } void FreePerl() { perl_destruct(myPerl); perl_free(myPerl); } Actually, most of this code is program independent, so knock yourself out imbedding Perl into all of your programs. :) If the specifics of those functions look overwhelming, don't worry too much about them. Mostly they refer to stuff in the Perl library that you'll never have to bother with because you'll be using these nifty little API functions instead. Let's go over those functions. InitPerl() sets everything up for us. perl_alloc and perl_construct ready the Perl interpreter. perl_parse will parse the file 'perlq2.pl' which should be located in the current working directory. You should put any Perl functions or other Perl code you want initiated at startup in this file. perl_run actually runs the code so that, at this point, your Perl interpreter should be fully initialized. This should be added to the InitGame() function in g_save.c, I prefer toward the end of the function at about line 190 (+'s indicate lines added). InitClientResp (&game.clients[i]); } globals.num_edicts = game.maxclients+1; + + // CCH: Initialize perl interpreter + InitPerl(); } PerlEval() is where you will be feeding Perl code to the interpreter. Just call PerlEval() with any string containing Perl code and the interpreter will evaluate it. That's right, any Perl code should work. As the perlembed manpage/documentation says, "Your string can be as long as you wish; it can contain multiple statements; it can employ 'use', 'require', and 'do' to include external Perl files." The world is your oyster. Perl. Get it? Anyway... The next three GetPerl*() functions are intended to provide an easy interface for retrieving scalar values from the Perl interpreter. If you need the integer equivalent of the scalar value '$time', just use GetPerlInt("time"). Need that as a float, you say? GetPerlFloat("time"). Never mind, you're going to print it out instead? GetPerlString("time"). Time, time, time, see what's become of me... PerlFree() deallocates the Perl interpreter and frees up the memory. It's always good to clean up after yourself. This should be added to the ShutdownGame() function in g_main.c, at about line 80. gi.FreeTags (TAG_LEVEL); gi.FreeTags (TAG_GAME); + + // CCH: Free the perl interpreter + FreePerl(); } Finally, to use all these functions, you'll have to add the following to g_local.h. +// CCH: PerlQ2 includes & functions +#include // from the Perl distribution + +void InitPerl(); +I32 PerlEval(char *string); +int GetPerlInt(char *string); +float GetPerlFloat(char *string); +char *GetPerlString(char *string); +void FreePerl(); + "handy.h" is included to resolve the I32 reference that PerlEval() returns. You'll also need to set your compiling options so that the Perl library, perl.lib, is included in your project and the include files can be found. In MSVC 5.0, this entails going to Tools->Options, selecting the Directories tab, and adding your Perl CORE directory (probably "C:\Perl\Lib\CORE") to the Include and Library directories. You should also open your Project, go to Project->Settings, select "All Configurations" in the "Settings For:" field, select the Link tab, then add 'perl.lib' to the Object/library modules. Hopefully, those of you using other compilers can figure out your own implementation, but I recommend reading the perlembed manpage/documentation if you run into trouble. Okay, we know all the details now, let's try an example. Here's a perlq2.pl to put in your Quake 2 directory. # # perlq2.pl # sub CapAndColor { my($string) = @_; $string =~ s/\b\w/pack("c",ord(uc($&))|128)/eg; return($string); } This file contains one Perl function called CapAndColor(). It takes a string as its first argument, capitalizes the first letter of every word and sets that letter to print in green text in Quake 2. It then returns this new string. Now, we need a way to call this Perl code. After adding all the appropriate code from above for setting up the Perl interpreter, we add a new command to g_cmds.c. +/* +================= +Cmd_JAPH_f +CCH: function for JAPH command +================= +*/ +void Cmd_JAPH_f (edict_t *ent) +{ + char *japh = "just another perl hacker"; + char perlCommand[64]; + + sprintf(perlCommand, "$return = &CapAndColor('%s');", japh); + PerlEval(perlCommand); + gi.centerprintf(ent, "%s\n", GetPerlString("return")); +} This function simply sprintf's our japh variable and the Perl code we want to call into a temporary perlCommand buffer. Notice how we have '$return' receive the return value from &CapAndColor(); we could call &CapAndColor() directly, but we'd have no way to get to the result! After having PerlEval() evaluate our command, we then use GetPerlString() to retrieve the result of the &CapAndColor() call and centerprint it to the player. One last tidbit we need is a way to call this command function, so we add the following to ClientCommand() in g_cmds.c. Cmd_Kill_f (ent); else if (Q_stricmp (cmd, "putaway") == 0) Cmd_PutAway_f (ent); + + // CCH: japh command + else if (Q_stricmp (cmd, "japh") == 0) + Cmd_JAPH_f (ent); + else if (Q_stricmp (cmd, "wave") == 0) Cmd_Wave_f (ent); else if (Q_stricmp (cmd, "gameversion") == 0) Now, you should be able to compile this up, load quake2 with your new gamex86.dll, and type 'cmd japh' at the console and see 'Just Another Perl Hacker' centerprinted on your screen with J, A, P, and H in green, the rest of the text in white. For you paranoid types and Intel lawyers (I repeat myself?), this is not 'hacker' in the media sense. Just thought I'd mention that for Randal's sake. Anyway, I think this is a pretty simple but fairly powerful API to Perl (it seems like it's hard NOT to do something powerful with Perl). If anyone finds this useful and would like to see me work more on it (like, oh, adding ERROR HANDLING, maybe, figuring out what PerlEval() is returning...), drop me a line so I'll know there's interest. Speaking of error handling, at this point, there isn't any, so try running your Perl scripts from the command line first if you're having problems. Also, get Perl running with a simple script (like the above example) first before you go wild with your 2000 lines of Perl code, okay? Full source, patch file, and DLL at http://www.jump.net/~dctank. Chris Hilton chilton@scci-ad.com