Click here to Skip to main content
Click here to Skip to main content

Adding high score capability to MS Solitaire

By , 25 Jul 2007
Rate this:
Please Sign up or sign in to vote.

Screenshot - solitairehighscore.gif

Contents

Preface

This showdown was a long time a comin'. We met maybe 7 or 8 years ago. His name was Score. Solitaire Score. He was mocking me with his lack of high scoring mechanism. I tried to pull Spy++ on him, but that just made him laugh: "You rookie," he said, "What in the hell is wrong with ya?"

"I've got no time to fight with you," said I, "I have work to do."

"C'mon back when you're not so wet behind the ears. We'll step outside and sort our business out," he said.

Since then, we met on occasions. He always winked at me, "Well boy, we ain't getting any younger..." Then one day I decided that's it; it's me or him. I cleaned my WinDbg and put it in its holster. I was hoping we wouldn't be using bare hands for this. We met at high noon, as was set. We stood for only a moment and then drew. I fired a few breakpoints at him, just to see what he was going to do. He moved right and then left, but the last one got him and he suddenly froze. I could see terror in his data segment. He tried to throw confusing assembly commands at me, but I was too focused on the target. Suddenly, it was there. I could see it. I almost cried out in surprise. His hidden address was revealed for all to see.

I rejoiced too soon. He had one more trick left up his sleeve. While we were fighting in debug, he kept his address in his left sleeve. Once we moved to normal run, he would change it to the right. I called his bluff and his address fell to the ground. He knew that he'd been beat. He looked at me with hatred in his error handling and said, "It only took you 2 days, you son of a bitch!"

Introduction

Ok. That was a nice story. Based on true events, too. Once I realized I could not "steal" the score with a spy, I became intrigued. I was also wondering why there wasn't a built-in Solitaire high score mechanism. I found no answer here. However, people keep their Solitaire score anyway and in the most bizarre ways, for example, here. Obviously, once you "know" what the score is right now, you can manage a high score list. I can think of at least two answers to that last question. You can find those in the Conclusions section.

Finding the score

Honestly, when I started I was sure that this part was going to be the most time-consuming. As it turned out, that wasn't the case, but still, when you start something like that you're not sure when it will finish and how long it is going to take. I started running Solitaire with WinDbg. Notice these lines:

xecutable search path is: 
ModLoad: 01000000 01010000   sol.exe 
ModLoad: 7c900000 7c9b0000   ntdll.dll
ModLoad: 7c800000 7c8f5000   C:\WINXP\system32\kernel32.dll
ModLoad: 77c10000 77c68000   C:\WINXP\system32\msvcrt.dll
ModLoad: 77dd0000 77e6b000   C:\WINXP\system32\ADVAPI32.dll
ModLoad: 77e70000 77f01000   C:\WINXP\system32\RPCRT4.dll
ModLoad: 77f10000 77f57000   C:\WINXP\system32\GDI32.dll
ModLoad: 7e410000 7e4a0000   C:\WINXP\system32\USER32.dll
ModLoad: 6fc10000 6fc6b000   C:\WINXP\system32\CARDS.dll
ModLoad: 7c9c0000 7d1d5000   C:\WINXP\system32\SHELL32.dll
ModLoad: 77f60000 77fd6000   C:\WINXP\system32\SHLWAPI.dll
ModLoad: 773d0000 774d3000   C:\WINXP\WinSxS\
                                 x86_Microsoft.Windows.Common-Controls_

So now we know the virtual address space of our process. We can safely assume that the address spaces of the DLLs do not contain the variable that holds the score. We can therefore concentrate our efforts between address 0x01000000 and address 0x01010000.

Now we look for a place where the score is changing. Well, when we set Solitaire options to Vegas-Cumulative, the score changes on every deal action. We need to find the exact line of code where this happens. This is not so easy. What I did was add breakpoints along the way in hopes that I would catch the deal action before the score actually changed. You can see the score change if you keep your eye on Solitaire at the same time.

Once I found that breakpoint, 0x010019ac, I followed it until I saw where the score was changing. At first, I stepped over every call command to see which call changed the score. Then I stepped into that call the next time. In the end I got here:

Address    OpCode/Params   Decoded instruction
------------------------------------------------------------
010030a1   014830          add dword ptr [eax+30h],
                               ecx ds:0023:000bc2f0=ffffff30

This is where we add the value -- in this case, -208 -- from address 0x000bc2f0 to what we have in register ecx, which was loaded with -52 (ffffffcc) from this address 0x007fc60 on the previous line. Success!

The score is moving

Now we need to write a program that can access that address and get the score. Before we get all chirpy, I will save you the trouble and say that the score location changes depending on the way the process is started. It is different when launching Solitaire by double-clicking on the EXE or double-clicking on a shortcut to the EXE. Two different shortcuts to the same EXE -- for example, different descriptions -- yield a different score address. We can also guess that trying to find the address on different computers, versions of Windows, versions of Solitaire, etc. will result in the score residing at a different address. We'd be right to guess so; I've tried it. Now it is evident that we'll need to scan for the address, but how and where?

The most important thing to remember is that we can tell what the score is. We'd like it to be a unique value so that it's easy to look for. We also need to know where to look. The where is easy. Going back to WinDbg, we can see while the process is running that the data segment starts at address 0x000a0000 and goes on for a while. We can see some sections with data, lots of strings, but the highest address I found the score at was 0x000bc2f0. For precautionary measures, we'll scan in this range: 0x000a000 and 0x000bffff.

Now we have to decide on the method of reading the process memory. I don't like forcing decisions. It leads to mistakes, mistakes lead to confusion, confusion leads to fear and fear leads to the dark side.

I chose the ReadProcessMemory and WriteProcessMemory functions because they present a simple and fast-to-implement solution. I also chose it because I had to decide when the action of reading the score should occur. I decided to let the user do that. Like I said, I don't like forcing decisions. The other options may be hooks or trying to load a DLL to the process memory space and running a thread to read the addresses you want. Here is why you shouldn't use the ReadProcessMemory and WriteProcessMemory functions: The old new thing.

We are going to use them anyway. That article describes a security issue of two processes not having the same permissions. We'll be willing to accept that if we have a multi-user disorder, it means we have problems. Here is a piece of code that scans the data segment looking for the value of -104, which I found to be quite unique in the data segment and easy to achieve. I'll explain more about this later.

#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#pragma warning (disable:4312)

int _tmain(int argc, _TCHAR* argv[])
{
    long pid;
    HANDLE hProcess;
    HWND hWnd = FindWindow(TEXT("Solitaire"), NULL);
    if (hWnd != NULL)
    {
        GetWindowThreadProcessId(hWnd, (LPDWORD)&pid);
        hProcess = OpenProcess(PROCESS_VM_OPERATION|
            PROCESS_VM_READ|
            PROCESS_VM_WRITE|
            PROCESS_QUERY_INFORMATION|
            PROCESS_TERMINATE, FALSE, pid);
        if (hProcess != NULL)
        {
            //long lAddr = 0x000bc2f0;
            long lPtr = 0x000a0000;
            long lAddrEnd = 0x000Bffff;
            long lVal  = 0;
            DWORD dwBytesTx = 0;
            while (lPtr < lAddrEnd)
            {
                ReadProcessMemory(hProcess, 
                    (void*)lPtr, &lVal, sizeof(long), &dwBytesTx);
                if (-104  == lVal)
                {
                    printf("at address %d found value -104\n", lPtr);
                    lVal = 1;
                    dwBytesTx = 0;
                    WriteProcessMemory(hProcess, 
                        (void*)lPtr, &lVal, sizeof(long), &dwBytesTx);
                    // send message to the solitiare window to 
                    // refresh the score view
                    SendMessage(hWnd, WM_SIZE, SIZE_MINIMIZED, 0);
                    ReadProcessMemory(hProcess, 
                        (void*)lPtr, &lVal, sizeof(long), &dwBytesTx);
                    printf("at address %d the value is %d\n", lPtr, lVal);
                    break;
                }
                lPtr+=4;
            }
            CloseHandle(hProcess);
        }
    }
    getchar();
    return 0;
}

Designing the application

Now that we know how to get the score, we need to decide our goals for the new application:

  1. Start Solitaire from within the application in a consistent way, meaning the same way every time.
  2. Find the address of the score and keep it.
  3. Manage a high score list.
  4. Read the score.
  5. Load a score, so you can resume play from where you left off.

Since this is for fun, I added the following software requests:

  1. The application doesn't need a main window, so we'll run it as a tray icon application.
  2. I'd like it to be a small and quick application.
  3. I'd like it to not use DLLs that are not already in the current system, like all sorts of MFCs. That means linking statically to MFC.
  4. I'll try to separate the modules into static libraries, so they can be used in other projects easily. Boy, are we going to pay for that.
  5. The application will save its data in the Registry.
  6. The application will save its data encrypted. Since it's in the Registry, we don't want smarty-pants users to meddle with our data.
  7. I like C#-style events; they're very easy to use. I think we'll need to use events for such things as when we have a new high score.
  8. If there is a code already doing something I want and I can use it, I will.

When we break it down into modules, we will need the following:

  1. A high score module. Add, remove and so on.
  2. An events module.
  3. A process-handling module. We'll need to find Solitaire by name, since this is what we know about it. We'll need to read and write to memory.
  4. Registry with encryption module.
  5. A UI module. We'll need a list to show high scores. We'll need an about window with a link, etc.
  6. We'll need an application that runs in the tray and we'll need it to only run once.

Events

For the events module, I used an already existing code. Please see the References section. This code allows you to define C#-like events. Events are always useful. They make you separate code that doesn't belong together. The only problem I had with this event implementation was that I couldn't define an event without parameters.

High scores

The Highscore module is composed of these classes:

  • CAppSetting: A template class that represents a setting you can save and load. I could not resist calling a class by that name. App - setting. Funny...
  • CHighscoreEntry: A high score entry consisting of name, score and time.
  • CHighscoreManager: The managing high scores class.

Processes

CProcessQuery is the class that does all of the process-related actions, i.e. all of the read, write and find by name. I started with the approach from this article: How to get handle to any running process by its name. This approach reads information regarding processes from the Registry. The problem was that the line of code that actually read the process information caused the application's memory to reach 20Mb.

An application that uses that much memory can no longer be considered small, so I decided to go the PSAPI way recommended by Microsoft here: Enumerating All Processes. It can be argued that the functions of CProcessQuery can be all static since it does not save information about a specific process. Let's call it the first step in moving from the Win32 API to the OOP way. Every time we're asked to perform an action on Solitaire, we look for it again. This way, we don't mind if Solitaire is stopped, as long as it is started through our application.

Registry

There is plenty of Registry code lurking around. I used the CAESEncRegKey Registry encrypted access. For more information about this class, please see the References section.

UI and application

Since we've (I've) decided on a tray application, a lot of the operations are performed by the application class and not the hidden window. This is the code I used:

  • CWinAppEx: One instance application. I added a little change to what happens when another instance is started: an event is raised. Handling the event will show a pop-up balloon on the tray icon. Oh, sweet events.
  • CTrayNotifyIcon (or NTray): An implementation of a tray icon.
  • CSortListCtrl: A combination of two list controls. One is a sorted list and the other adds text color and icons. I used it a while back when I started writing a logger. The logger was never finished, but the view was awesome.
  • CLabel: Used for the score window title.
  • CHyperLink: Used for the about window link.

For more information about the classes I used, please see the References section. Another class worth mentioning is the CSettingsManager class that handles all of the settings operations like reading and writing.

Building the application

The application was written and built in Microsoft Visual C++ 2005. I can't guarantee (and I highly doubt) that it will compile on any other version. You never know, though, until you try.

The easy way to build the application would be to download the solution archive and the compiled libraries. See the links appearing at the top of this article. Extract the archives next to each other, i.e. use extract here. Open the solution and build. If you want, you can download and build Crypto++ including the CPP files by yourself. You will need to specify the path to where the Crypto++ library outputs files. Because I chose to create LIB files and link statically, I ran into a few linking problems. Most of them, if not all, however, miraculously disappeared when I added #include "Stdafx.h" to AESHelper.h. It took a while to find. You should have no problem building the application in release or debug.

Using the application

Solitaire Highscore comes with an attached help file. You can download the application release version with the help file from the link provided at the top of this article. I will not repeat the whole thing, due to the fact that I don't want to lose my fingerprints. In short, on the first run, SolitaireHighscore changes Solitaire's settings to Vegas-Cumulative. It starts Solitaire hidden, sends it a deal (F2) message that causes the score to reduce to -104 and scans Solitaire's memory to find the score address.

The user gets a notice of the scan result. The user will have to start Solitaire from Solitaire Highscore. This option is the default and can be changed in the settings of Solitaire Highscore. The default action of Solitaire Highscore is to check for a high score. Therefore double-clicking on the tray icon will check if Solitaire is running and if it is, read the address that was found on the first run. The list of 10 high scores and the rest of the application settings are saved in the Registry.

TEASER: When the high scores window is showing -- or one of its other windows, like message boxes, etc. -- click CTRL+W and watch for changes in Solitaire behaviour.

Conclusions

So, as I promised, here are probably some of the reasons why Solitaire wasn't shipped with a built-in high score mechanism:

  • Solitaire is more about winning the current hand than accumulating scores.
  • Not all Solitaire options increase or decrease the score; you can play for time.
  • It took me about 10 days to write this little application, not including the 2 days it took to find the address of the score and the rest of the debugging process. I imagine that when Solitaire was written, it probably could have been done in 3-4 days. They already had the score, the registry access and they didn't need to hack into their own process. Having said that, imagine the following conversation:

    MS team leader
    : Hey guys, we're launching the 3.11 in a week. I hope everything is ready.

    Solitaire programmer
    : I need about 3-4 more days for the Solitaire high score stuff.

    MS team leader
    : WHAT?!?!?!?
  • It could also be because no one gives a damn.

Another conclusion is: If you need to find the address of a variable that you assume is in the data segment and you can guarantee a unique value of the variable, it is best to write a program that scans the whole DS once than debug the assembly.

To-do

Here are a few ideas I foresee will never come to pass, unless I am fiercely encouraged:

  • Keep track of best-timed games.
  • Create a website that keeps high scores sent from Solitaire Highscore. If anyone wants to take up this gauntlet, I am more than willing to help.
  • Change the settings mechanism to a generic one.
  • Let the user choose what the default action is, i.e. what happens when double-clicking the tray icon.

Disclaimer

As I am a law-abiding member of The Code Project, I hereby declare that:

I've left all the copyrights of the code I used where I found them. I haven't changed the copyrights at all. Some code was changed in the making of this application, but I promise it wasn't hurt. You can use the code presented here in any way you like, as long as it's not intended against me. If you liked the application or the article, well, sweet. If not, I didn't write any of it; it was a distant relative of mine that I don't know so well. He asked to sign my name onto this article, me being a member and all, and I reluctantly agreed. Moreover, please do not claim this application as being your creation; it isn't nice.

References

  • [NTray] - A tray icon implementation.
  • [Events] - Emulating C# delegates in Standard C++.
  • [CAESEncRegKey] - An AES Encrypting Registry Class.
  • [CWinAppEx] - Limiting an application to a single Instance, the MFC way.
  • [HyperLink] - Hyperlink control.
  • [List Control 1, List Control 2] - List controls.
  • [Crypto++] - Crypto++ Library is a free C++ class library of cryptographic schemes.
  • [CLabel] - A label control that is part of a set of tools.

History

  • 24/07/2007 - Initial posting.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Asa Meltzer
Software Developer
Israel Israel
Software designer and programmer.
Programming languages:
MFC, C++, Java , C#, VB and sometimes C and assembly.

Comments and Discussions

 
GeneralNice Work PinmemberAmro Fawzy31-Jul-07 1:49 
AnswerRe: Nice Work [modified] PinmemberAsa Meltzer31-Jul-07 3:30 
Computer voodoo - just kidding Laugh | :laugh:
I am not sure how detailed an explanation you wish.
It is just highly unlikely that someone declared that variable in one of the dlls MS Solitaire is using. Even if they did, it would be a global variable, so its address would be in MS Solitaire process data segment. Since I found the address by debugging, I didn't really rely on the assumption. I relied on the fact that the score had to read and written. I was looking for the instruction where it happens. Cool | :cool:
 
Asa Meltzer

GeneralWish list Pinmemberjflarvoire30-Jul-07 21:10 
AnswerRe: Wish list PinmemberAsa Meltzer31-Jul-07 1:14 
Generalpossible but irrelavant answer PinmemberTClarke27-Jul-07 0:07 
GeneralRe: possible but irrelavant answer PinmemberAsa Meltzer27-Jul-07 4:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140421.2 | Last Updated 25 Jul 2007
Article Copyright 2007 by Asa Meltzer
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid