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!"

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.

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:

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:

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!

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.

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.

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.

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.

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.

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.

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.

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.

Computer voodoo - just kidding
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.

Thanks for that interesting article!
I play solitaire once in a while, and one thing I really wish I could do is to replay the same game after I lose it. (To try different strategies, and see if the alternatives allow to win it)
Would it be possible using your technique to get a game "number", and later force Solitaire to reuse that same "number"?

Hi.
At first glance it seems that what your suggesting is much more complicated :
a. If there is a game "number" (like in Freecell), it will be harder to find, since we don't know it's value. I am not sure there is one, though.
b. It matters when the card order is determined whereas you can always read/write the score. You probably can't change the card order before the deal operation starts (as a programmer, I would change it there ).

I can think of 2 ways you can do it:
1. Inject a dll to the process memory and run a thread that checks a change on the card deck (array) used for dealing or on the function where the deal is performed.
2. Disregard the whole details thing and just read and write the entire memory chunk that you need (save and load state). Something around the guidelines of this article: http://www.codeproject.com/cpp/transactions.asp

I believe the reason there is no score record in the original is simply because each game is different so each possible time is different making the time scores non comparable. Didn't stop the in minesweeper though.

Philosophy: The art of never getting beyond the concept of life.Religion: Morality taking credit for the work of luck.

Thanks. If you ever played Solitaire the Vegas-Cumulative way it's about accumulating points, it is played without time; hence the name.;) But for me, the more reasons why it wasn't done, and the most far fetched, the better.