foone Profile picture
Jun 12 286 tweets 60 min read
OKAY SKIFREE
This is a game originally from 1991, developed by Chris Pirih, and included on one of the Windows Entertainment Packs.
There's a modern 32bit version by the original developer, on the official site:
ski.ihoc.net Image
it's windows only and it's not got the source available.
which is a shame. crossplatform skifree, skifree mods/improvements would be cool, right?
I've also got the original 16bit EXE and some intermediate versions. but the 32bit version is much easier to analyze, so I'm gonna focus on that one.
It's all in one big ol' EXE. and by "big" I mean it's 116 kilobytes.
So there's no image files or anything. Image
So let's run wrestool on the EXE and see what we've got.
So, a ton of bitmaps... Image
and some icons. Image
So, icons. We've got this icon, which is available in three variants. 2 color, 16 colors, and 16 colors again. I think it's just 16 colors (but windows standard) and 16 colors (but custom).

This is the original icon, designed by Chris Pirih. Image
Then there's the other icon, which was designed by someone at Microsoft. Apparently they didn't like the original icon, and had someone design a fancier icon before it got into the entertainment pack.
same as before: 2 color, 16 color, 16 color but fancy. Image
then there's 8 files that weren't able to be decoded by wrestool. I think they're supposed to be icons? but they don't work. I'll leave them alone for now.
Then we've got 89 bitmaps which make up the rest of the game's graphics. Image
and despite this being the 32bit version, you can tell that these were designed for 16-color displays. They're manually dithered. Image
OKAY SO now it's time to look for code.
We've got an entry point (which'd be WinMain) and then 9 groupings of functions, based on their addresses. 401 through 409. Image
401 has 19 functions
402 has 25 functions
403 has 11 functions
404 has 16 functions
405 has 15 functions
406 has 23 functions
407 has 22 functions
408 has 11 functions
409 has 5 functions
so we have a total of 147 functions in the EXE.
that's a bunch.
but it's doable to reverse engineer these one by one, right?
and reimplement them? giving us a source-available clone of SkiFree?
at first that'd just give us a win32 version of skifree, which... we have.
but then the use of the win32 api could be replaced with SDL or similar, creating an identical but cross-platform skifree.
so we can figure out a lot of things by doing basic puzzle-solving logic.
like let's look at FUN_00405a40.
it sets a bunch of global variables, then calls a function, and if the result is 0, it calls another function with a string, which is basically "can't load bitmaps" Image
so clearly FUN_00405ab0 has something to do with loading bitmaps, and FUN_00404950 displays messages.
and if we check FUN_00404950, it calls some other function to get a caption, then calls MessageBoxA, a win32 API to display a message box, if you couldn't guess. Image
SO let's rename this. FUN_00404950 is DisplayMessageBox.
FUN_00401cf0 is a little more complicated.
But if we look into this, it seems that DAT_0040c61c is a global variable which is an array of pointers to strings, indexed by param_1. Image
if the entry is filled (non-NULL), it simply returns that string.
if it's not filled, it uses LoadStringA to go get it, then allocates some memory for it, puts it in the slot, and then returns it.
so this is just a cache around LoadStringA
LoadStringA pulls strings out of the specified EXE/module (it could be a DLL or something).

I'm gonna guess that it's actually pulling them out of this EXE, since there's no DLLs or anything else to load from.
The module instance it's using is DAT_0040c61c.
Let's see where that's set.
So it's read from 4 places and written from one. Time to check out where it's written. Image
so it's set to param1 of FUN_004052d0, which is a big call that does a bunch of things. Who calls this? Image
It's called in only one place, by FUN_004047e0, which calls it with... its own param1.
UGH. Let's see how calls this! Image
It's called in one place, by the entry function, which passes pHVar5, which is the result of GetModuleHandleA(NULL)

That returns the current module instance. Image
so, rewind the investigation stack.
DAT_0040c61c, the data we were looking at, it's a global variable pointing to the HMODULE handle to the current EXE.
So now we can rewind back to that string-caching function, and name (and type) the DAT_0040c61c data. It's now SkifreeEXE, and we can see it passed to LoadStringA. Image
We can also name DAT_0040c674, which is our array of strings.
It's clearly an array of strings.

But how many? UINT goes up to 4 billion. There definitely aren't 4 billion strings in this program.
wrestool didn't seem to be able to extract the strings. Let's try other tools.
I've got PE Explorer in my WinXP VM. Let's aim that at the EXE Image
It shows the EXE has two string tables. The first one has entries 1-15, and the second one has 16-17. ImageImage
I don't really know how two string tables work, so I'm gonna assume these are just merged somehow.
we've got strings 1-17, so let's set the size to 18 (because there's a 0 that's never used)
also we can check our guesses.
if we go back to our DisplayMessageBox function, we can see that FUN_00401cf0 was called with the parameter of "1". And string table 1 has entry 1 set to... "SkiFree". That's a reasonable caption for a message box. Image
SO DAT_0040c674 becomes StringCache, and the type is set to char** (so we didn't need to know how many entries anyway)
apply that to our FUN_00401cf0, and now it makes more sense. instead of a global variable + a parameter times 4, it's just an index into an array.

speaking of which, FUN_00401cf0 becomes GetCachedString() Image
So now DisplayMessageBox is looking much more sensible. We're calling functions with names, and we have a name.
uType=0x30 looks a bit weird, though. Image
Let's check the equates... MB_ICONWARNING or MB_ICONEXCLAMATION looks good. Image
See? That's absolutely reasonable code now. I named the parameter "message".
The only thing that's a bit ugly is the fact we're passing just "1" to GetCachedString. That should probably be a constant like STR_PROGRAM_CAPTION, which we can define later. Image
ONE FUNCTION DOWN, 146 TO GO!
but hey, just like a logic puzzle, we can use our newly discovered knowledge to see what else we can learn.

We know what GetCachedString does, and we know what DisplayMessageBox does.
Let's see what calls them.
see, we can just do Find References to GetCachedString Image
it's called from 35 places! that's a lot of strings. Image
Here's one that looks FUN (no pun intended): FUN_00401d70. It's a bunch of math and then a call to wsprintfA to format a string.
It's calling GetCachedString with the value 0xb, or 11.
The string table says 11 is "%2u:%2.2u:%2.2u.%2.2u"

So, number:number:number.number Image
Let's go back into SkiFree for a sec... Image
OH HELLO. Look at that top line. NUMBER:NUMBER:NUMBER.NUMBER.

So hours, minutes, seconds, hundredths of a second? Image
so I bet these numbers would make more sense if they weren't in hexadecimal. Let's turn them into decimals. Image
oh look.
60. That makes sense. It's base-60 time. THANKS BABYLONIANS.
Let's name these variables Image
Doesn't that make a lot more sense? Image
OKAY so I learned of the Equates Table.
now I can define my own constants like STR_TIME_FORMAT and set them to special values like 0xB (aka 11) Image
so the function can now look like this! Image
so now we can go back to looking at GetCachedString calls
So, FUN_00405760 has two calls to GetCachedString.
It also does some stuff with setting global variables, and setting windows text... I think this sets the window title based on some data.
BUT FIRST, set equates. Image
we already knew 1, and 2... it turns out to be "Ski Paused ... Press F3 to continue"

Also we can look at strings in Ghidra, I learned that just now Image
now with equates, this looks to make more sense.
We are checking some global variable, and if it's set to something other than 0, we set the window title to the Paused version, or we set it to the not-paused version. Image
So now we can name the function, we can rename DAT_0040c6d0 to IsPaused and give it a type of BOOL.

Let's look at the first parameter to SetWindowTextA, a global called DAT_0040c6c8 Image
So SetWindowTextA takes a HWND for the window to set.
So this is probably a global handle to the SkiFree window. Let's confirm with some checking as to where it's referenced. Image
hey look.
Over in FUN_004052d0, it's set to the result of the CreateWindowExA call.
So yeah. This is the main window handle.
And we can also see a there's a secondary window, titled SkiStatus. Image
So we've got two global HWNDs, Window_SkiMain and Window_SkiStatus. Image
okay so let's look around at the rest of this function.
nHeight, the height of the window, is set to DAT_0040c74c. That's a global, let's go see where it's set. Image
OKAY so it's set over in FUN_004052d0, based on GetDeviceCaps. Image
set the equates and we can see that it's calling the HORZRES and VERTRES.
Which get the width in pixels and height in pixels of the screen.

(Ignore the & and | stuff, it's doing something weird with the data variable width) Image
SO we name and type those variables, and now the screen-size creation logic starts to make sense.
It sets the width to to the screen width, but if the screen height is smaller than the width, the width is set to the height. Image
Then we set the X position to the width-screen width, divided by 2.

in other words, center the window.
Scroll up a bit, and let's look at the registration of the window classes.
We can see they register SkiMain and SkiStatus, and also set the WndProc to LAB_00405800 and LAB_004068d0

So we know those are the WndProc functions for each class! Image
so yeah, LAB_00405800 gets named and typed as the SkiMainWndProc.
It wasn't defined as a function before, so we're now up to 149 functions once we do the SkiStatusWndProc Image
the WndProc is a windows thing which is the message handler for a window. As messages like "the user moved the mouse" or "you need to draw the window" are sent to your function, the WndProc handles them.
the SkiStatusWndProc is much shorter. Let's apply some equates. Image
wait can you not apply equates to switch statements?
ok weird. Whatever. For now I added some comments.
It only handles WM_CREATE, WM_DESTROY, and WM_PAINT.
(and technically WM_SIZE but that just jumps to the end of WM_CREATE which gets handled defaultly. it's very spaghetti code) Image
so FUN_00406970 gets called at WM_PAINT.
So let's look into that function, given that it's probably just "draw the status window"
I've named it PaintStatusWindow.
It's got a lot of very repeative code, but the general gist seems to be that we get a string, call a function, get a string, call a function, rinse, lather, repeat, then call a final function. Image
Apply some equates and we can see it's getting lengths and presumably formatting strings? we need to investigate FUN_00401e20 Image
fortunately it's very simple. It just calls TextOutA and then sets some value based on a global constant.
I have my suspicions about what that's about Image
Yup. Cross-referencing with the definition of TextOutA, we can see that the variable that's being modified is the Y position we're drawing at.
So this string takes a pointer to the Y position to draw at, then automatically increments it by a set amount. Image
so if you call it multiple times, instead of drawing at the same place, it automatically moves down the screen. Fancy.
so we can guess that DAT_0040c668 is a "line height" variable. It'll either be set to some hardcoded constant or set based on some text metrics, I bet... let's see.
hey look FUN_00406a70 sets it based on the output of GetTextMetrics! Image
applying some naming we can see it's based on the .tmHeight value of the metrics of the DC DAT_0040c6cc, which is a global based on the param passed in. Image
btw FUN_00406a70 has shown up before:
it's the function called in response to WM_CREATE in the SkiStatusWndProc
I named it SkiStatusWindowCreate

It looks very similar to the PaintStatusWindow function we looked at, but it's calling a different function. Is this a different draw text? Image
Nope! FUN_00406c50 is calling GetTextExtentPoint32A.
That computes the length a bit of text would appear as if it was rendered.
And look at if statement: if a parameter is less than a value, set it to a value?
That's a max() function. We're finding the longest string, in pixels Image
There we go. Image
but yeah. with some equates and naming these variables knowing what they do, we can see that this basically is just checking all the strings it needs to render to figure out how wide they are, then setting some variables based on that. Image
so, back to SkiStatusWndProc.
We can figure out that the value that is called with FrameRect in the WM_PAINT function is also the one set by the WM_CREATE handling code, but also we have one final function to look into:
FUN_00406c80, called on WM_DESTROY.
it does the obvious things like unselecting the font and releasing the DC, but there's one more function call here that's interesting, which has a weird variable name: s_V:\hack\ski32\ski2.c_0040c090? Image
that variable is set to "V:\\hack\\ski32\\ski2.c", that's why it's named like that.
and what the heck is 0x1123?
So we check it and, oh look, it formats a string based on those parameters, than calls another function...
What's that format string? "%s line %u"

OH HEY. This is the results of an assert macro! Image
the assert, in the original source, looks something like this:

assert(param_1 == WindowSkiStatus);
but because it's a macro, it gets expanded into something like this:
if(!(param_1 == WindowSkiStatus) ShowAssertFailure(__FILE__, __LINE__);
which got further expanded into:

if(!(param_1 == WindowSkiStatus) ShowAssertFailure("V:\\hack\\ski32\\ski2.c", 4387);
to tell the programmer that the assertion failed on that line in that file.
well, now we know how many lines the code has! and that it's written in C, in a file named "ski2.c"
bit of naming. Next, let's look into FUN_00401270 Image
It's similar to the previous MessageBox-calling function but it also checks the return value. Image
So a bit of equates and naming and this makes more sense.
It's showing a message, but it's not just informative. It's showing an OK/Cancel dialog, and if the user clicks Cancel, it calls DestroyWindow on the main SkiFree window, closing the program. Image
So now that we know what the AssertionFailure function is, how many other places is it potentially called?
111 places! Image
but now we can look through all of those calls and see what values are passed to them.

It turns out 4387, the one we saw here, is the latest line we see an assert for.
And all of them are calling for ski2.c
which tells us:
1. this game is entirely (or mostly) defined in one C file
2. it's around 4400 lines long
okay so let's find another opening to attack the code from.
A good place is Imports: What win32 APIs does this program call? Image
Imports are great because we can look up all these functions and know what arguments they have, and use that to figure out what data and methods are.

Let's pick... LoadBitmapA. We know this game has 89 bitmaps, it must load them somewhere Image
It turns out LoadBitmap is only ever called from one location: FUN_00405ea0.
This is just a tiny wrapper around the existing LoadBitmap, which automatically passes the SkifreeEXE module handle.

It's also decompiled incorrectly. Image
Look at the definition for LoadBitmapA. It takes in an instance and a string to the bitmap, but it returns an HBITMAP.
But wait, this wrapper returns void! How does that work? Image
It doesn't. Ghidra fucked up.
So if we look at the raw disasembly, we have some mov/and/push that set up the arguments, then it calls a pointer to LoadBitmapA, then it just returns. No mention of a return value here! Is this really loading a value and throwing it away? Image
NOPE.
so this function is defined as __fastcall.
That's the calling convention, which tells the compiler how you call a function, where parameters go, where return values are stored, and various other things.
and fastcall works similar to cdecl calling convention, which stores return values (at least integers and memory addresses) in the EAX register.
so if you have a function A that calls function B, and then returns the return value of function B, like:

int functionA(){
int ret = functionB();
return ret;
}
FunctionA needs to:
1. get the return value stored in EAX
2. store it in its own return value. which is EAX
so it's "get EAX, save it to EAX"
so "mov EAX, EAX"

That is a operation you don't need to do.
So there's no need to do it. So the compiler doesn't.
And Ghidra didn't realize this. Since there was no code handling the return value, it assumed there was no return value.
SO let's fix our wrapper function to return a value, and see if that prompts Ghidra to realize what's happening
I set the return type to HBITMAP instead of void, and ghidra goes "OH HOLY SHIT WAIT A MINUTE HERE" and suddenly there's a variable Image
and a bit of naming and now this makes more sense. Image
okay so now where is our LoadSkifreeBitmap function called?
It turns out only two places, inside a function at FUN_00405ab0
So here's the first one.
We call LoadSkifreeBitmap with some value, check if it's NULL, then we call GetObjectA on it. That does... something. something that involves 24 bytes. Image
And this is where we should cheat and use THE POWER OF COMPILERS to help decompile things.

Yes.
So GetObjectA takes a pointer to one of these types. A BITMAP or DIBSECTION or a word or an EXLOGPEN or a LOGPEN or a LOGBRUSH or a LOGFONT. Which one is it? Image
well the previous argument is a size, in bytes.
so we could look into the definition of things like BITMAP and start counting bytes... BUT I'M LAZY AND I CAN'T REMEMBER NUMBERS Image
but I have Visual Studio 2022 Image
So let's make a new C++ Console program. Image
It's not loading the text editor.
this is going to limit my ability to write code Image
oh is this the update that adds the ability to EDIT TEXT FILES? Image
whatever. I've got sublime text.

ANYWAY WHERE WAS I? oh yes using this to help us. Image
So let's just do this. We include the windows header and then call sizeof on BITMAP. Now we have the compiler tell us how bit a BITMAP is. Image
the answer is apparently "32" Image
well, whatever thing the SkiFree code was calling was 24 bytes. So it must not be a BITMAP (probably)

Let's try DIBSECTION ?
okay it's definitely not a DIBSECTION Image
BUT WAIT A MINUTE HERE
remember the first post in this thread? and how many bits did it say this version was?

32! Image
I am on a 64bit windows.
with a 64bit compiler.
let's retarget this compiler to make 32bit EXEs and run it again, with sizeof(BITMAP) Image
TADA.WAV Image
so it's indeed getting the info on a BITMAP. we can now go back to ghidra and stuff that information in.
and there we go.
now the following lines make sense. It's getting the bitmap and then doing some comparisons on the width and height! Image
so it seems to be doing some pretty basic comparisons.
it's finding the biggest width and height, and it's summing up the heights of the 32-pixel-and-smaller bitmaps.
renaming and looking at the surrounding code, we can guess at why it's doing this.
After adding up all the heights of the small (meaning 32 pixels wide or smaller) bitmaps, it creates a new bitmap that is 32 pixels wide, and as tall as all of them. Image
it's gonna cache them all into one tall strip, isn't it?
probably.
But here's an interesting thing:
CreateBitmap is called with width 32, height (the sum of all the small icons), but planes/bits both set to 1. .
A black & white image.

Now why would Skifree need a black and white copy of all the bitmaps? Image
anyway continuing down the function, we see the big icons are getting their own cache.
this one is set to max_width, and the sum of the big icons. Image
but it gets the same "let's make a B&W copy too" treatment.
so here in the loop that is actually loading the images, we've got all this weird ppHVar + offsets stuff.
This is the tell tale sign of a struct/class. Unfortunately we don't yet know how where the member variables are. Image
but we've got an "Auto Create Structure" option. Maybe that'll help? Image
and there we go! it assigned them names based on numbers and offsets. Image
So let's rename it to "Sprite" since that's what this seems to be. And we can guess what these fields are based on how they're used. Image
Then we've got code like this. It checks the width and sets a field based on it. So this is the source DC for this particular sprite, in either the "big bitmaps" cache or the "small bitmaps" cache. Image
So remember how there's two copies of the sprites?
well, here's the trick.
It draws the sprite using the ROP (raster operation Code) of SRCCOPY, which just copies all the bits as-is.
That goes to the regular sprite cache.
Then it copies again, using NOTSRCCOPY Image
NOTSRCCOPY inverts all the pixels.
so what happens if you copy this sprite using SRCCOPY to a B&W bitmap? Image
You get this. Image
and with NOTSRCOPY, you get this. Image
so we're basically making up a copy of all the sprites, but as black & white silhouettes
and the reason is TRANSPARENCY

specifically, we have none.
bitblt, the function that draws sprites, doesn't really do transparency (at least in the version we're using here). it copies rectangles of pixels, and the only options are how it merges them with the destination
but skifree has to have transparency, right? I mean, look at me standing in front of a pole here. Image
if there really wasn't any transparency, it would look like this. Image
Let's put a pin in FUN_00405ab0 (name it LoadSpriteCache) and go look for the answer in other calls to BitBlt.
okay so it's in a big loop and such so I can't easily show how it works in code. So I'm gonna fake it.
the answer is SRCAND and SRCPAINT.
SRCAND only sets pixels if they're set in both the image being drawn and the image we're drawing over.
SRCPAINT only sets pixels if they're set in either the drawn or overwritten image.
So how does this work?
Here's our background before we start trying to draw the player over it. Image
and we've got this mask image. Image
So if we just drew it with SRCCOPY, we'd get this. This doesn't help, so we don't do that. Image
arg I'm doing this all wrong. There's a couple ways to do this and I think they're doing it a different way. OKAY PRETEND THINGS IN A BIT.
so don't look too closely at the bitwise operations, because I'm halfway through it and realizing they did it differently.

BUT BASICALLY they use an AND here so that we end up only modifying the destination image where the mask is black, resulting in this. Image
this is kinda like transparency, but it only works for all- black sprites.
Not terribly useful unless you're trying to emulate a game & watch.
But then we have our regular image, which you'll recall has no transparency. it's actually white there. Image
It's drawn on using the SRCPAINT operation, which does an OR.
This means it only draws where the destination image is black, basically. So it ends up ignoring any pixels in the destination image that are white or other colors. Image
so instead of doing a transparent blit, it does two pixel-operation blits with a regular image and a mask image. By combining them together in this way, you get an imitation of transparency.
why is it done this way, instead of just having a transparent blit?
well... I'm not super sure, but I think it is because of hardware acceleration.
this sort of two-step blit would be very easy to make hardware to optimize. it's very parallel and it involves simple bitwise operations for each drawing event.
it's also potentially easier for a CPU to to accelerate, since you can do many pixels at once by using bitwise operations. there's no branches, just math operations.
anyway I have COMPLETELY bungled how this exactly works in this case, but the basic theory is right. Just don't look to closely at the and/or math.
here's an example for how it's done that doesn't get the math completely wrong:
parallel.vub.ac.be/education/modu…
okay, next function.
FUN_00401b80. It's got some cached strings, so let's set up equates. Image
much better. Image
okay lets look at SkiMainWndProc.
Still can't figure out how to use equates on a switch statement, so comments it is. Image
FUN_004060b0 is called by WM_PAINT, so that's simple. Image
and that function is also simple: it starts painting, fills the screen with something (I'm guessing white!), and then calls another function. Image
and DAT_0040c69c is set in the CreateWindows function and set to (HBRUSH)GetStockObject(WHITE_BRUSH);

So yeah. White.
So, that function it calls.
It checks some things, then seems to iterate through a linked list of Things.
I'm guessing Sprites/Actors.
Then finally it calls something at the end. Image
I named the function PaintScreenInner, and set set up a structure called Actor to store these actors, but only know some fields so far. Image
I've got slightly cross-eyed Image
so the important thing to do when you run into a dead end is to back out and try attacking it somewhere else. eventually you can make your way back here, knowing more
yes, reverse engineering is a metroidvania
so let's go back to SkiMainWndProc. We've got a function called on WM_KEYDOWN and WM_CHAR. Image
That's two functions, I mean.
So let's look at the WM_CHAR one first, as it's likely to be simpler. Image
ARG STILL CAN'T SET EQUATES ON SWITCH STATEMENTS
so, comments.
It's doing things based on the following keys: x/y/f/r/t/X/Y Image
so X/Y/x/y slide you over by one pixel, f toggles fast mode, and I don't know what r and t do.
but we can at least label the F key as what it does.
so let's rewind to WndProc and check out the WM_KEYDOWN handler
it's also a switch statement, which as established I HATE WITH FURY Image
it hides the window on ESC.

Ask your parents what a "boss key" is. Image
okay so F3 calls SetWindowTitle. Which is weird. That makes no sense. Image
so SetWindowTitle doesn't do what I thought it does.
It actually toggles if the game is paused.
still working on it, just not doing anything interesting.
This isn't the first time I've hacked on SkiFree, btw.
Here's a blog about the time I figured out it has hidden sound code:
foone.wordpress.com/2017/06/20/unc…
fun fact: SetTimer is called with a value of 40, and that's in milliseconds.

So SkiFree tries to run at 25 FPS.
Here's a fun function, FUN_00406cda.
It multiples a global variable, then adds something to it, then returns some of the upper bits. Anyone guess why? Image
if you google those numbers it shows up in a stackoverflow question asking about MSVC++ implementation of rand()! Image
there's some suspicious looking code.
it calls random(666) and does one if the result isn't 0, and something else if it is.

does it summon EL DIABLO!? Image
so FUN_004066d0 is called when you click, and it first checks if _DAT_0040c72c is set to NULL, and if not, it does a bunch of stuff. Image
and if it is NULL, it initializes global variables... like it's starting the game over. Image
and you know when the game will start over if you click?

after you've been eaten by the yeti! Image
so I bet _DAT_0040c72c is the actor for the player.
and this bit here, where there's all this checking on uVar2, based on field32.
These numbers are suspicious: 18, 20, 21, 19, and 13. Image
If we look at our extracted images, hey... this looks like some of the frames that play when you're flipping in mid-air. which advance when you click. Image
so field32_0x44 is probably the animation frame
so, two snippets from OnKeyDown.
playerActor->field_0x46 is decreased by 8 on pressing Left, and increased when you press Right... I think this is your facing direction. ImageImage
oh and I figured out what the "t" key does: when it's pressed, it immediately triggers an update, as if 25 milliseconds have passed.
So by holding it down you can speed the game up.
huh, I think this game actually DOES track your position in X,Y,Z. It makes sense, I guess, but I figured it was just a trick
Yep.
FUN_00402be0 handles adjusting positions based on movement. If your next height is still above 0, you're accelerated downward by 1, until you reach 0 again. Image
So there's AddStylePoints, which really should be called AdjustStylePoints. it just changes the players total style points. But the real use for this is to look at all 13 places it's called. Image
So the amounts adjusted are:
-64, (-1, 2, 4, 8), 1, -16, 1, 100, 16, -32, 1000, 6, 20, 20, 3.
which are all very interesting numbers. so we can try doing things in SkiFree and seeing how many points we get, and search them in the code to find out which part of the code that is.
so if I have 2000 points and I run into a tree, my points are now 1968. So we lost 32 points Image
and in FUN_00403a00 there's a bit 139 lines in where the points are adjusted by -32 points and Sound1 is played. Image
So we can name -32 as POINTS_CRASH_INTO_TREE and rename Sound1 into SoundCrash
so most of the style points are in one function, at FUN_00403a00, which seems to take too Actors and figure out various things based on their types.
So I think it's an actor-collision check
so now I need to figure out what the actor types are.
0 seems to be the player, but there's also ones as high as 17. They don't seem to be directly connected to sprites so I can't look them up based on images.
it does seem to be only 17, as this function sets the type and it throws an assert if you set it to <0 or > 17. Image
but hey, see how param_2 is passed to SetAnimationFrame?
well one call into it passes a type of 3 and animation frame of 33. Image
image 33 is the dog.
So I bet type 3 is a dog! Image
but we need to find a way to force the creation of actors of different types, so we can see what they are.
and we know how the code creates dogs, so why don't we modify it to make things that aren't dogs?

CAN WE BE TRUSTED WITH SUCH POWER?
so here's the code that creates the DOG. see the hex digits to the left of the assembly? Image
so here's that bit in the EXE.
B9 03 00 00 00 is the
MOV ??, 3.

LET'S CHANGE. Well, we only know one other values for ACTOR_TYPE, 0, which is the player.
Let's make TWO PLAYERS! Image
oh shut up. there's no virus in the EXE I just made! Image
okay, nope, there's a dog. so it's not dogs. Image
so let's try something else. Remember that create-actor function?
Why don't we HIJACK IT?
But I think there's an easier way to do this. Lemme switch tools.
OllyDbg, which I happen to have set up in my XP vm. Image
Well, this isn't working very well. I finally got it to log calls to ActorType, but I'm not getting many. I don't think this is the main CreateActor call. I'm only getting occasional 0, 4, and 3 types. Image
But see there's another CreateActor function that looks nearly identical. I don't know the difference yet, but let's breakpoint this one instead. Image
That's better. A whole bunch here.

I think this one might be for immobile actors, and the other one is for mobile actors? Image
SO NOW THAT WE HAVE IT
let's cheat.
so we're making actors with types 17, 14, 13, 12, 14, 11, right?

what if they were all 17?
Here's the code that creates the actor. EDI is where the actor_type ends up, but we need some room to insert our own code. You see the two TEST/GZ/TEST/JGE pairs? Those are the assertions. We know those won't trigger, so let's just... Image
select and fill with NOPs. Image
now we have eight bytes of free space. Image
now we just need to assemble something into that space. Let's set EDI to 11 (hex, so 17). Image
That used up 5 bytes, so we've still got 3 bytes free. Image
UNFORTUNATELLY this will immediately break. Image
because it's not "if ASSERTIONFAILED, then jump to error code"
it's "if NOT ASSERTIONFAILED, then jump PAST error code"
we actually need to NOP out all of this code. Image
well this all looks pretty normal Image
BUT THIS IS MISLEADING

check it out. I can't hit anything.
I'm just slightly slowed down by them. I think type 17 is some kind of actor type that just slows you down a little.

And everything looks normal because the animation frame is still being set normally.
okay so I'm naming this function CreateStaticActor, and I've noticed this bit here. Two of them are created at some position, with animation sprite 55 and 56. Image
and according to the resources, 55 and 56 are these flags. Image
SO LET'S TEST and set them to something else.
like changing that 0x37 (55) into a 0x44 (68) Image
Hey look! it works. Image
so, logging calls to the CreateStaticActor function, we get the following: lots of type=17, with different frames. Image
which match up to these. Image
head down a bit more, and we finally get some different actors!
we've got type 15 with the frame 47, type 13 with frames 49,50,51, and a type 14 with frame 46. Image
type 13 turns out to be these three.

So, let's call 13 ACTORTYPE_TREE Image
So after some more logging, I've got 11-17 filled out. Image
and for the CreateActor (the active version), it seems to do some kind of weirdness which means I can't map the frames up.
But I can tell you that 3 is the snowboarder, because it keeps happening while I'm sitting there watching the debugger.
making progress. Image
still can't find 7,8,9, and there seems to be two yetis. Image
yetii?
yetis? yetipodes?
So as part of the creating of actors, we've got this function, which is mostly sensible. The top bit sets up a SLALOM, we can ignore that. After that, it generates a random number in the 0-999 range. Image
the math here is a little weird because of how the returns work.
And that last one is nasty.
but there's a 1/20 chance it spawns a SNEAKYTREE
9/20 chance it spawns a TREE
4/20 chance it's a LUMP
1/20 chance it's a LUMPYSNOW
4/20 chance it's a ROCK
1/50 chance it's a SKIJUMP
1/50 chance it's a SLOWSKIER
1/100 chance it's a DOG
so FUN_00402850 is named GetAnimationFrameFromActorType and I'm trying to make sense of it. It has weird math. Image
so case 3 is actually ACTORTYPE_ROCK, and it picks either 46 or 45.
1/4th of the time it's 46, the other 3/4ths it's 45.
So we have 3 rocks for every stump Image
case 4 is the ACTORTYPE_LUMP
1/3rd of the time it's 48, 2/3rds of the time it's 47.

So two small lumps for every big lump. Image
5 is just animation frame 52, the skijump.

which I should add to VGAPride as the SkiFree Pride Flag Image
the rest of the function is the trees.
1/8th of the time, it's the dead tree (50).
1/8th of the time, it's the big tree (51)
6/8ths of the time, it's the regular tree (49) Image
okay I've decided that type 17 isn't type "flag", it's "decoration"

the main reason for this is that the piss is type 17
you can't piss a flag.
This is some interesting code.
So, it spawns a Snowboarder, but this code isn't handling a snowboarder... it's handling a skilift.

See how it rolls 1 out of 1000 and if it passes, it creates a snowboarder? Image
That's the ski-lift!
Yes, it's a rare occurrence, but skilifts sometimes spawn snowboarders. Image
it checks that the animation frame is 65 (although it's written as 39, for reasons I don't understand yet) and if the skiboarder is spawned, it switches the frame to 66. The one without the snowboarder. Image
So this math looks like it should be something I recognize.

But I'm not figuring it out. It's definitely checking to make sure param_3 and param_4 aren't the same, to avoid a divide by zero. Image
BUT I have been working on this for too many hours today and I am out of brain juice. so I'm shelving it for now.
BUT HEY if you enjoyed this long hopefully slightly educational thread, and want to see it continue, feel free to send me a dollar or two on my ko-fi:
ko-fi.com/fooneturing
or sign up to send monthly donations on my patreon:
patreon.com/foone
you owe me at least 5$ if you read this thread coming from the orange hellsite, that's all I'll say.
20$ if you commented on the site about how this should have been a blog or a livestream.

• • •

Missing some Tweet in this thread? You can try to force a refresh
 

Keep Current with foone

foone Profile picture

Stay in touch and get notified when new unrolls are available from this author!

Read all threads

This Thread may be Removed Anytime!

PDF

Twitter may remove this content at anytime! Save it as PDF for later use!

Try unrolling a thread yourself!

how to unroll video
  1. Follow @ThreadReaderApp to mention us!

  2. From a Twitter thread mention us with a keyword "unroll"
@threadreaderapp unroll

Practice here first or read more on our help page!

More from @Foone

Sep 28
help this pride flag is being gatekept Image
at least it's not a paywalled pride flag
pridewalled
Read 46 tweets
Sep 27
check out my new phone Image
fuck on screen keyboards Image
oh it's got a web browser! that's handy. I didn't realize it was this advanced Image
Read 24 tweets
Sep 27
The Legend of Zelda: The Space-Filling Curve
COME ON Image
Read 5 tweets
Sep 26
Check out this drawer I saw at a junk store.

It's a case. as in "upper" and "lower"
specifically, this is a California job case, it's a tray for different movable type casts.
They're different sizes because you need different amounts of each letter when writing english text.
You pick up the pieces of metal from each little box and move them into a composing stick, in reverse order
(commons.wikimedia.org/wiki/File:Hand…)
Read 10 tweets
Sep 26
So I got a 10$ used keyboard today and it turns out it's a kind I vaguely heard existed but had never actually seen in person.
So it uses cherry-mx keycaps, but what keyswitch is this?
Answer: NONE! There's no keyswitches. This is a rubber-dome-over-membrane keyboard.
Normally rubber-dome-over-membrane keyboards use keycaps with integrated sliders, but this one doesn't, so it can be cherry-mount.
So the sliders look like this:
Read 21 tweets
Sep 26
Fun fact: by the original design of "Safeway", modern Safeway is Unsafe.
The point of the original "Safe"way was that it didn't let you buy on credit.
The idea was that grocery stores were selling food and supplies to farmers on credit, and if the harvest didn't come in like they hoped, they were fucked and in a ton of debt.
Safeway was therefore "safe" because you had to pay cash, up front. You can't go into debt to pay for your groceries.
Read 49 tweets

Did Thread Reader help you today?

Support us! We are indie developers!


This site is made by just two indie developers on a laptop doing marketing, support and development! Read more about the story.

Become a Premium Member ($3/month or $30/year) and get exclusive features!

Become Premium

Don't want to be a Premium member but still want to support us?

Make a small donation by buying us coffee ($5) or help with server cost ($10)

Donate via Paypal

Or Donate anonymously using crypto!

Ethereum

0xfe58350B80634f60Fa6Dc149a72b4DFbc17D341E copy

Bitcoin

3ATGMxNzCUFzxpMCHL5sWSt4DVtS8UqXpi copy

Thank you for your support!

Follow Us on Twitter!

:(