Blog Info: I’ve made a habit of switching topics often on this blog. This might frustrate some of my readers, but believe me, it’s much healthier than constantly revising one project over and over. Besides, since none of these projects are real money-makers, we might as well build as broad a tool-set as possible. So in this guide, we’ll learn a bit about Win32, one of the oldest stable windowing toolkits in existence.
Back-story: Everyone has their MMO of choice. I play Eternal Lands because it’s very, very slow-paced, so I have no chance of getting hooked on it. It’s also quite balanced, and has beautiful scenery and a believable set of systems. You could really tell a story using that world. That said, some minor in-game features really bother me. For example, when you are gathering (“harvesting”) vegetables and minerals, you often unearth some nasty surprise that causes your character to pause. Since harvesting is meant to be the kind of thing you minimize the game for, this means that you’ll be constantly bringing the game back just to check if you’ve been interrupted.
Goal: Figure out a way to notify players when they stop harvesting. Perhaps a pop-up notification like Google Talk provides?
A note about Linux: Despite the fact that Eternal Lands is actually easier to compile under Linux, I’ll be developing this plugin under Windows. GTK fans should have enough knowledge to be able to port this example to Linux. My reason for doing this is simple: on my machine, Eternal Lands runs better on Windows. I’m all for equality, but I’m not going to handicap myself for the sake of fairness.
Getting the Source
Eternal Lands is rather unique in that the client is open-source. I find this rather trusting of the developers, although presumably they did it so that they could get features patched in by the user community. This makes our project a lot easier, so let’s grab the source right away. You’ll need Tortoise CVS. (Note that Tortoise CVS works fine on Windows Vista, as long as you don’t try to perform a CVS Checkout in a protected directory like C:\windows.) The source is located at the project’s BerliOS repository. To access it, create a folder called “EL Client” somewhere on your system. Open the folder, right-click, and choose “CVS Checkout…”. Copy and paste the CVS Root from the source page into the corresponding checkout field; it’ll look something like this:
:pserver:anonymous@cvs.elc.berlios.de:/cvsroot/elc
Then, enter elc for the module name. You can now click Ok:
The entire project contains lots of files, so you’ll have to wait a while for them all to download. While you’re at it, make sure you’ve downloaded and installed the Windows client of Eternal Lands from the main site. (You might consider the Down Them All plugin for Firefox, as the Eternal Lands site will occasionally yahtzee your connection.) This will make running the .exe you compile much easier.
Theory, Approach, and Not Getting Banned
While our downloads chug along their merry way, let’s talk design. First of all, what’s our end goal? That’s easy: “When the client shows the message You have stopped harvesting we should show a popup near the system tray if the Eternal Lands client is minimized.”
Next question: is this legal? Modification of the client code is fine, since the license is very liberal about the source. Actually using it to connect to the game requires that we subscribe to the terms and conditions of Eternal Lands. The only one that applies to us is:
[No] macroing, automating, scripting, exploiting bugs...
However, I don’t think we’re in violation of this. We’re not macroing, and the only thing we are automating or scripting is the use of Alt+Tab. I don’t think Entropy would insist that we constantly check up on our harvesting progress, especially since shell scripts in Linux to check the message logs have been received no hostility on the forums. In fact, this notification actually maximizes the ratio of player-controlled time to time wasted standing still.
Well, it’s nice that we won’t get banned for doing this, but how will we actually program it? There are two approaches I can see: either modify the game to display the window itself, or create a separate program that intercepts packets sent to Eternal Lands and determines if any of these packets represent the You have stopped harvesting message. There are pros and cons to each approach:
Modifying the Game Client
Pros
- You won’t lose any speed. Checking each message and responding only for a few specific ones will not affect performance.
- Modifying the code itself is easy; you won’t have to play with firewalls or packet interceptors.
Cons
- You’ll have to be able to compile the client. For some games (especially cross-platform ones like Eternal Lands) this can be a show-stopper; it’s difficult!
- You’ll need to re-apply your fix every time a new game client is released. This should be easy, but major releases (especially ones that use new libraries) might bring up the issue of compiling again.
Intercepting Packets
Pros
- No modification to the client is necessary. You can easily start the client with or without this feature enabled.
- When a new client is released, your code might continue to work! In fact, it will only fail if the connection protocol or information protocol changes, which is something the developers will probably avoid.
- You can develop in your favorite IDE; you don’t have to compile their code first. Consider: let’s say that we’re running a Windows game in Wine; compiling the client could be a nightmare!
- The result is slightly easier to port than modifying the code.
Cons
- The concept is more difficult; we’re dealing with packet nonsense instead of strings.
- If your code isn’t fast, it might cause your game to lag, since you’re dealing with all packets, not just the rare console message.
Since this is our first tutorial on Eternal Lands, we’ll modify the client code directly. In our next lesson, we’ll create a packet interception program, and polish up the interface so it looks real shiny-like.
Compiling the Code
The Official Eternal Lands Client Development Site has instructions for compiling the client using Dev C++. I followed these instructions and they worked perfectly. If this link is ever broken, you’ll have to figure it out on your own, or you can try compiling the client on Linux and re-working this tutorial from there.
You might want to remove debugging symbols from the compiled executable; this makes it about 77% smaller. To do that, add the line strip –s $(EXE) to the end of your $(EXE) target. In other words, in Makefile.win, find the following:
$(EXE): main.o $(TMP_LIB)
$(LINK) $(CFLAGS) $< -L. -lelc $(LDFLAGS) -o $(EXE)
..and replace it with:
$(EXE): main.o $(TMP_LIB)
$(LINK) $(CFLAGS) $< -L. -lelc $(LDFLAGS) -o $(EXE)
strip -s $(EXE)
Although the original 11.1MB isn’t too big, the resultant 2.56 MB is small enough to email to a friend.
To run the program, you should follow their instructions for running a local copy. In particular:
C:\Dev-Cpp\dll and C:\Dev-Cpp\lib (respectively) to Release, over-writing the files there.Wow! I don’t know about you, but for me, simply re-compiling the client caused it to run about 30% faster! This is particularly astounding, since I wouldn’t expect a naïve build to optimize better than the developer’s build. Someone should try compiling this thing in Visual Studio with PGO and seeing if we can make it even faster.
Adding a Window at Startup
The next logical step is to create a simple window as a proof-of-concept, and show it when Eternal Lands starts up. We’ll be using pure Win32 C to code this.
In Dev C++, click “File->New->Source File” and choose “Yes” when asked if you want to add this file to the current project. Then, immediately “save” the file as taskbaralerts.h, with a type of “Header Files”. Do the same for another file, taskbaralerts.c, of type “C source file”. These will contain the bulk of our implementation; it’s a very good idea to keep as much code as possible out of the official EL source files.
Enter the following into taskbaralerts.h:
#ifndef TASKBARALERTS_H
#define TASKBARALERTS_H
//Includes
#include <stdio.h>
#include <windows.h>
//Function prototypes
LRESULT CALLBACK AlertsWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
void initAlertsWindow(HINSTANCE hInst, char *windowClassName);
#endif //TASKBARALERTS_H
The #ifndef/#define/#endif triplet is known as an Include Guard, and it prevents us from defining multiple versions of “initAlertsWindow”. Now, any file that wants to use these functions can simply include taskbaralerts.h and everything will run smoothly.
Of course, we need to actually implement these somewhere. Add this to taskbaralerts.c:
#include "taskbaralerts.h"
//Global variable
HWND alertsWindow = NULL;
HWND hStatic = NULL;
BOOL windowVisible = FALSE;
HINSTANCE myHInst = NULL;
HBRUSH bkgrdBrush = NULL;
//Message-handling callback
LRESULT CALLBACK AlertsWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_CREATE:
{
hStatic = CreateWindowEx(WS_EX_LEFT, “Static”, “”,
WS_CHILD | WS_VISIBLE,
5, 10, 180, 50,
hwnd, NULL, myHInst, NULL);
SetWindowText(hStatic, “EL Notification Area”);
break;
}
case WM_LBUTTONDOWN:
{
//Hide our window
windowVisible = FALSE;
ShowWindow(hwnd, SW_HIDE);
break;
}
case WM_CLOSE:
{
//Over-ride: Hide our window
windowVisible = FALSE;
ShowWindow(hwnd, SW_HIDE);
break;
}
case WM_DESTROY:
{
//Close the window for good.
PostQuitMessage(0);
break;
}
default:
return DefWindowProc(hwnd, msg, wParam, lParam);
}
return 0;
}
void initAlertsWindow(HINSTANCE hInst, char *windowClassName)
{
//Declare c-style variables
RECT r;
WNDCLASSEX wc;
//Save
myHInst = hInst;
//Necessary?
if (alertsWindow!=NULL) {
return;
}
//Make a brush
bkgrdBrush = CreateSolidBrush(RGB(0, 0, 0));
//Set a window class’s parameters
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = 0;
wc.lpfnWndProc = AlertsWndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInst;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = bkgrdBrush;
wc.lpszMenuName = NULL;
wc.lpszClassName = windowClassName;
wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
if(!RegisterClassEx(&wc)) {
alertsWindow = NULL;
return;
}
//Get the desktop
GetWindowRect(GetDesktopWindow(), &r);
//Create a handle to the window
alertsWindow = CreateWindowEx(
WS_EX_TOPMOST | WS_EX_NOACTIVATE,
windowClassName,
“Eternal Lands Notification”,
WS_CAPTION | WS_SYSMENU, //Simple
r.right-200, r.bottom-100, 200, 100,
NULL, NULL, hInst, NULL
);
if (alertsWindow==NULL) {
return;
}
//TEMP: Show the window
windowVisible = TRUE;
ShowWindow(alertsWindow, SW_SHOW);
UpdateWindow(alertsWindow);
//TEMP end
}
That’s a lot to digest. Let’s start at initAlertsWindow(), which the main game should call when it first loads. First, we check if alertsWindow is not NULL, which could happen if, say, the game restarts itself by re-running main(). Next, we fill out the parameters of our WNDCLASSEX struct. (The MSDN has very good documentation (except, occasionally, when it’s totally wrong!) on its Win32 API, so I’ll be linking to that quite a bit.) The parameter of interest is lpfnWndProc, which points to the function which handles messages for this window. We set it to AlertsWndProc. This means that every time a message is dispatched to our alerts window, the AlertsWndProc function handles it.
Now, we try to register this window class. The function call might fail if, say, Eternal Lands has already created a window class with this name. At any stage along the way, if a function call fails, we simply return. Now we are ready to create the window. However, since we want to position it on the lower-right corner of the screen, we’ll need to know how big the desktop is. So, we call GetWindowRect(GetDesktopWindow(), &r), which stores the desktop’s dimensions in r. Now, we can call CreateWindowEx. Parameters of interest include:
- The dwExStyle is used to describe what type of window we want. WS_EX_TOPMOST means that we want our window to appear above all other non-topmost windows, and WS_EX_NOACTIVATE means that we want our new window to show, but not to take keyboard focus or have a section on the taskbar. This makes sense; if we are typing in Microsoft Word when we stop harvesting, we want the notification to appear above Word, but we don’t want it to take the focus away from word.
- The dwStyle parameter describes what our window looks like. We give it a WS_CAPTION, and add a WS_SYSMENU so that the user can “close” the window if he wants to. Nothing fancy here.
- The four paraemeters x, y, nWidth, and nHeight position our window on the lower-right corner of the screen. You can fiddle with it if you want to display it above the taskbar; it doesn’t really bother me.
That’s all there is to creating the window, and showing it is easy enough. The AlertsWndProc function simply switches through the variety of message types we can receive, returning “0” if it sees nothing of interest (which tells Windows to handle the message itself). In the WM_CREATE message, we add a static sub-window (called a “Label” in other languages) so that we can potentially display multiple messages in the future. The WM_LBUTTONDOWN message tells our window to hide itself; this lets the user simply click on the notification window to make it go away. We also intercept the WM_CLOSE message, because we don’t want the “X” button to actually close the window. WM_DESTROY functions normally for destroying a window, since closing Eternal Lands will also close all of its windows. (Don’t worry, this code won’t spawn any hidden processes that you’ll have to Ctrl+Alt+Delete).
There’s a few things left to do before we actually run this:
- In Makefile.win, find the line that begins with COBJS= and add taskbaralerts.o in between tabs.o and text.o. If you were paying attention you’d notice that although we include taskbaralerts.h in several places, nothing links to taskbaralerts.c! This addition ensures that the linker can find your functions.
- In main.c, add #include “taskbaralerts.h” at the top of the file. If you want, you can add an #ifdef check so that the client will still compile under Linux, but it’s not really necessary. Now, find the line init_stuff(); and add initAlertsWindow(hInst, “EL_Taskalert”); after it. This will initialize our taskbar alert code right before Eternal Lands starts rendering the main game.
- Unfortunately, hInst isn’t defined. Find the line char *win_command_line; at the top of the file, and add HINSTANCE hInst; after it. Then, find the function APIENTRY WinMain and add hInst = hInstance; after int argc; —that was easy, right?
So, shall we run this code? Compile it, and copy el.exe into the Release directory. Run it, and you’ll see our window pop up about halfway through the boot sequence. If you’re in full-screen mode, this will be very obvious.
Intercepting Messages
Delete the lines we wrapped with the “TEMP…TEMP end” comments so that our window won’t show. It’s time to start fiddling with EL’s messages. In Dev C++ click “Search->Find In Files”. This allows us to search every file in our project for some text, and is indispensable when programming in C. Let’s try to find a message —any message— and go from there. What’s the most unique word in Eternal Lands? How about “radon”? Type it in and click “Ok”. The highlighted result looks particularly promising; double-click and you’ll be brought to the code that contains this —yep, it’s a big array of message strings:
The name of the array is temp_event_string; scan the file a bit and you’ll find that this gets sprintf’d into search_str, replacing the %s with username_str. Well… that’s a tangential bit of useful information. Do a “Find In Files…” on username_str and you’ll see that it’s defined in interface.h. So, if we include interface.h, we can access our player’s name. Getting back to the problem at hand, do a “Find in Files…” on catch_counters_text (the name of the function which accesses search_str), and you’ll see that text.c calls this in filter_or_ignore_text(). We just completed a bottom-up search; if you did a top-down search starting from the TCP layer, you’d end up at the same place. Go to the top of the function, under #endif // NEW_SOUND, and put in a call to check_taskbar_alerts(text_to_add); —making sure to #include “taskbaralerts.h” somewhere at the top of the file. Now, switch back to taskbaralerts.h and add a function prototype:
void check_taskbar_alerts(char * text);
In taskbaralerts.c, add #include “interface.h” at the top of the file so we can access our player’s name. Now, before we go any further, we have a small problem. You see, the mess you receive isn’t exactly equal to “You stopped harvesting.” There’s a character at the beginning which specifies the color of the string. We could simply strcmp on “\x7FYou stopped harvesting.”, but this breaks our code if the color of this message changes. What we want is a function named “stringcontains”, which we’ll write next. (Of course, if a player now PMs you with the message You stopped harvesting, this will also set off your alarm. However, I think it’s better to react to this case then try to prevent it.) Add the following code at the top of taskbaralerts.c:
BOOL strcontains(char* src, char* pattern)
{
//Init some tracking variables
int i;
int len;
int patIndex = 0;
int patLen;
//Loop
len = strlen(src);
patLen = strlen(pattern);
for (i=0; i<len; i++) {
//Have we reached a non-match?
if (src[i]!=pattern[patIndex]) {
patIndex = 0;
}
//Continue tracking our pattern
if (src[i]==pattern[patIndex]) {
if (patIndex==patLen-1) {
return TRUE;
} else {
patIndex++;
}
}
}
return FALSE;
}
This function is simple enough; whenever it matches the first letter of the pattern with the current index in the string, it simply assumes it has a match, and resets to zero if that assumption is broken. This will fail if you’re looking for ABAC in the string ABABAC, so you can re-write it later (when we test for usernames) if your username is something funky like that.
Now, let’s hook this function up:
//Bring up our window?
void check_taskbar_alerts(char * text)
{
//Can we track this at all?
if (alertsWindow==NULL) {
return;
} else if (windowVisible==TRUE) {
windowVisible = FALSE;
ShowWindow(alertsWindow, SW_HIDE);
}
//Have we stopped harvesting?
if (strcontains(text, “You stopped harvesting”)==TRUE) {
//Prompt the user
SetWindowText(hStatic, “You stopped harvesting.”);
windowVisible = TRUE;
ShowWindow(alertsWindow, SW_SHOW);
UpdateWindow(alertsWindow);
}
}
Again, simplicity rules the day. This function checks each message as it is received. If the alertsWindow is still visible, that means the user forgot to close it, and we do that for them. Since users are prompted when they begin harvesting, this ensures that the window won’t give any false positives. We then use our new strcontains() method to check if our message looks something like “You stopped harvesting”. If so, we change the text of the window and make it visible. Compile the project and run it. It works! —but there’s one obvious flaw.
Only Displaying the Notification if We’re Not Viewing Eternal Lands
That’s right —the notification appears whenever we stop harvesting, even if we’re currently watching the Eternal Lands window. This is annoying as it grabs us out of full-screen mode. So what should we do? Well, the Eternal Lands window’s title bar contains the caption (Userame on main) Eternal Lands. The “main” changes if you switch servers, so let’s just use our strcontains function to search for the user’s name and the phrase Eternal Lands, and not show the window if both are present. Change our check_taskbar_alerts function to read:
//Bring up our window?
void check_taskbar_alerts(char * text)
{
//Can we track this at all?
if (alertsWindow==NULL) {
return;
} else if (windowVisible==TRUE) {
windowVisible = FALSE;
ShowWindow(alertsWindow, SW_HIDE);
}
//Have we stopped harvesting?
if (strcontains(text, “You stopped harvesting”)==TRUE) {
HWND foreWnd = GetForegroundWindow();
char wndTitle[200];
GetWindowText(foreWnd, wndTitle, 200);
//Does the window title contain the user’s name and the words “Eternal Lands”?
if ((strcontains(wndTitle, “Eternal Lands”)==TRUE) && (strcontains(wndTitle, username_str)==TRUE)) {
//We’re in the game! Do nothing
} else {
//Prompt the user
SetWindowText(hStatic, “You stopped harvesting.”);
windowVisible = TRUE;
ShowWindow(alertsWindow, SW_SHOW);
UpdateWindow(alertsWindow);
}
}
}
Try it out, and you’ll see that everything works just fine.
Possible Improvements
There’s a lot of potential for improvement, since we did such a hasty job designing our plugin. Here’s some particularly sore points; add your own and fix it up!
- If we stop harvesting, the message displays properly. However, it will disappear if another message occurs before we maximize the game client. Moreover, if we see the message and then open the client and tell our character to walk towards a storage (a common response to a full inventory) the message will remain until another message preempts it. Both of these glitches imply that we were wrong to assume that we should hide the alert window when a non-harvesting message appears; rather, we should hide it when the client gains focus again.
- It would be nice to have an icon in the status bar that our window is attached to.
- We should probably give our EL notification box a more EL-ish feel. We could do this with some simple GDI programming.
- We should be able to track more notifications, and display nice pictures related to their content. Like… if we find gold, etc. And, we could have our system tray icon flash bright red if we die (or green if we find a rare stone!).
- In fact, all of our suggestions are edging towards a general re-design of the plugin we made. But in that case, I’d rather not program it in pure Win32 C. If you’re of a different mind, by all means implement these changes. Me, I’ll be brushing up on .NET, Java, and Python. See you next week!





Pingback: Making Games the Hardest Way Possible
Pingback: [EL-2] Eternal Lands Tray Notifier – Part 1 « Making Games the Hardest Way Possible