foone,
@foone@digipres.club avatar

bad news: after extensive digging into how Wheel of Fortune (2010, Wii) loads resource files, it seems that there's no way to get it to load patch files from the memory card.

It seems to only look for them on the DVD

foone,
@foone@digipres.club avatar

so it adds the / directory of the DVD to the searcxh path, then bundles (where the bundles are), then scripts (where the scripts are), then /Audio (where the audio files are), then it runs into a problem

foone,
@foone@digipres.club avatar

it queries the current language and tries to add /Audio/$ThatLanguage to the search path. It fails because that folder doesn't exist.

So then it falls back to adding /Audio\English, which also doesn't exist. But since it doesn't check that the add succeeds, it continues loading.

foone,
@foone@digipres.club avatar

Then it adds /Video (where the videos are) and then /Media

There is no /Media on the disc, so I don't know what was supposed to be in there.

foone,
@foone@digipres.club avatar

Finally it checks the language (again!) and adds /Speech/$ThatLanguage to the search path.

Since getLanguage always returns "American", this'll try to add /Speech/American to the search path, and then fail because that folder doesn't exist.
Fortunately the fallback it does try, /Speech\English, does exist.

foone,
@foone@digipres.club avatar

it's not my job to offer code reviews of 13 year old games but this seems less than efficient.
what if they want scripts to vary from language to language? or, hey, some of these movies have voiceovers in them... why not localize those?

foone,
@foone@digipres.club avatar

The answer for scripts at least is that they implemented PackageAppInterface::getLanguage in their scripting engine, so each script can handle their own localization, rather than having per-localization scripts.

foone,
@foone@digipres.club avatar

Maybe they do the same thing with movies. The movies are probably getting played by the scripting engine, so they could just switch what filenames they call for different languages

foone,
@foone@digipres.club avatar

not that it matters in this case: Wheel of Fortune is a US-only game so the only language it exists in is English

foone,
@foone@digipres.club avatar

but this same engine is used in a bunch of games, many of which presumably had non-english versions.

foone,
@foone@digipres.club avatar

You wanna know the weirdest thing this game does with regards to loading bundles?

So, it tries to mount 4 bundles at startup:

The system resources bundle (filename unknown), "SharedAssets.bdg", "Wheel.bdg", and "Patch.bdg".

Now, the disc only contains SharedAssets.bdg, and Wheel.bdg. Patch.bdg is missing, but that's okay, apparently.

But the system resources bundle is vital to having the game boot up. It's not in any file visible in the filesystem. So where is it?

foone,
@foone@digipres.club avatar

SURPRISE! It's a ROM Bundle!

Wait this is a game shipped on an optical disc, it doesn't have any ROMs?

foone,
@foone@digipres.club avatar

well, ROM bundle just means that instead of a filename to mount, you give it a memory pointer + length.
And it parses the bundle out of RAM.

So... I guess it manually loads the bundle from somewhere else? NOPE!

foone,
@foone@digipres.club avatar

there's no code in the game to load the memory location it loads the system bundle out of

foone,
@foone@digipres.club avatar

they embedded it into the executable. That bundle's memory is loaded by the OS loader.

foone,
@foone@digipres.club avatar

and this quickly gets really confusing because one of the first things the game loads is "[SHARED]\Localization\SharedLoc.loc.prd", which you'd think is in the SharedBundle, and it is!

foone,
@foone@digipres.club avatar

except that one isn't used, because it's also in the "ROM bundle" and that one overrides it.

foone,
@foone@digipres.club avatar

which frankly is an underrated anti-reversing technique. Have multiple copies of important subfiles in separate datafiles and have complex precedence rules to determine which one gets loaded, then ignore all of them for another copy you embedded into the binary

foone,
@foone@digipres.club avatar

anyway I don't think they were intentionally trying to make this hard to reverse engineer (if they were, they failed so hard), but it does mean that technically to automatically know what resources will be loaded by the game, you have to solve the halting problem

foone,
@foone@digipres.club avatar

since you can't just look at the bundle files on disc and interpret what data is in them, as the existence of ROM bundles mean you have to ask the question "will this executable ever call BundleManager::mountBundle(BundleManager*,uchar*,int)?" and that's not possible to automatically know. FORTUNATELY I'm doing this manually and the behavior is deterministic.

foone,
@foone@digipres.club avatar

anyway, this is only vaguely related, but there needs to be a tool that helps tie dynamic debugging with static debugging.

foone,
@foone@digipres.club avatar

like, use the dynamic debugging traces to do things like annotate functions with records of how often they get called from different sources

foone,
@foone@digipres.club avatar

or click a function and see what arguments it has been called with in traces

foone,
@foone@digipres.club avatar

sadly the wii balance board support classes in the engine are just leftovers from the engine's use in other games, and not proof that the developers of Wheel of Fortune (2010, Wii) were far ahead of their time in avant-garde gaming

foone,
@foone@digipres.club avatar

although given my reverse engineering work, it's possible that I may one day reverse this wrong

foone,
@foone@digipres.club avatar

man I'm used to weird padding in structures and strange gaps, but 14 kilobytes? Why?

foone,
@foone@digipres.club avatar

the struct WiiControllerPack has, at offset 1480, an array of 4 KPADStatus structures.
Each of those is 250 bytes. So this array is over by byte 2440.
So where does the next member start? offset 16888.

foone,
@foone@digipres.club avatar

that's 14,448 bytes of nothin'.

or maybe, given that it's left out of the debugging info, 3612 separate vtable pointers!

(this is a joke. it can't possibly be that)

foone,
@foone@digipres.club avatar

wait. the revolution SDK defines KPADStatus about how I'd expect but then it defines KPADUnifiedWpadStatus which is a union of various things. I wonder if my debugging info is eliding some weird union nonsense going on which explains why everything is so big

foone,
@foone@digipres.club avatar

no these all seem to be smaller

what the fuck

foone,
@foone@digipres.club avatar

this is the worst thing about reverse engineering.
you pretty much never can assume "well something weird happened randomly"

foone,
@foone@digipres.club avatar

no, there is A Reason why the compiler did this, and you are going to have to find out what it was

foone,
@foone@digipres.club avatar

me "well at least I have a bunch of types from the debug files, so I can finally figure out the types of a bunch of these untyped globals! goodbye, undefined1 PrimDrawer::bPrimBuffer[6144], let's see what you really are!"

Dumped DWARF debugging info: "unsigned char PrimDrawer::bPrimBuffer[6144]"

WELL THAT DIDN'T HELP MUCH

foone,
@foone@digipres.club avatar

I'm pretty sure this game had two programmers who each compiled 50% of the game's code.

Programmer A had their compiler set to output every bit of debug information it could, short of just shipping the .cpp files

Programmer B had their compiler set to aggressively optimize and never output any debugging info

at the end of the development, the object files from these two developers were linked together, then that binary was shipped to the customer

Randomfrequency,

@foone ah yes, the two genders

aeva,
@aeva@mastodon.gamedev.place avatar

@foone I have a build script in a p4 shelf for the game I'm working on that opts into debugging for a few key cold paths that I'm actively working on. Said modified build script also has a comment in it that says something to the effect of // !!!!!!!!!!!! FOR THE LOVE OF GOD DO NOT APPROVE THIS CODE REVIEW !!!!!!!!!!!!!!

foone,
@foone@digipres.club avatar

seriously. I have complete info telling me that void WorldObject::updateVisRes(const WClipFrust &, CCheckResults); was defined on line 335 of E:\Build\WheelJeopardy\World/WorldObjectBase.h

what's a WClipFrust? that information is not in the debugging info. Go fuck yourself.

foone,
@foone@digipres.club avatar

I think the only thing this compiler has ever inlined is vector.push_back()

foone,
@foone@digipres.club avatar

which is great because it really aids clarity of reverse engineering that every time they call vector.push_back() there's 20 lines of support code for it

foone,
@foone@digipres.club avatar

just after I say this, guess what I find, plain as day, in the disassembly?

MVectorBase<WStaticMesh*,WStaticMesh*,4>::push_back((MVectorBase<WStaticMesh*,WStaticMesh*,4> *)&this->static_meshes,local_20);

OH SO YOU ONLY SOMETIMES INLINE IT, DO YOU?

foone,
@foone@digipres.club avatar

it's inlined earlier in this same function

what the fuck

foone,
@foone@digipres.club avatar

either they REALLY wanted to specify that this class initializes all the floats to 340282349999999991754788743781432688640, or they accidentally included a little-endian NaN when they meant a big-endian NaN.

foone,
@foone@digipres.club avatar

I'm not really sure how you accidently code a floating point number to have the wrong endianness.

foone,
@foone@digipres.club avatar

yeah after some dynamic testing, I think the reason this particular WStaticMesh and WSector and WPortal and WOccluder is extra cursed, with missing debug info and incorrect-invalid-floats?

It's never called. Why would it be? This is a game that basically takes place in one big room, plus a menu. Why would your map need to be split up into sectors?

foone,
@foone@digipres.club avatar

it probably got left in because

  1. codewarrior is terrible at dead code elimination (and all other optimizations)
  2. they have an advanced scripting engine, making dead code elimination even harder
foone,
@foone@digipres.club avatar

fs_QuequeRemove

THIRTEEN YEAR OLD TYPO SPOTTED

foone,
@foone@digipres.club avatar

bPrevIngoreNunchuck

COME ON NOW

foone,
@foone@digipres.club avatar

there is not much gore in Wheel of Fortune.

they meant Ignore

foone,
@foone@digipres.club avatar

ptTargWorld

a pointer to the Target, in World coordinates?

or the best fun park in all of Qo'noS?

foone,
@foone@digipres.club avatar

arg. how is WTrackCam perfectly documented in the debug data, but the surrounding class of WCinematicCamera is not? it's not even mentioned once, not even in arguments! and it's definitely used, I found it by dynamic tracing.

foone,
@foone@digipres.club avatar

oh because it's only created by the fucking scripting language!

foone,
@foone@digipres.club avatar

MY KINGDOM FOR A DAMN SPIGOT SCRIPTING ENGINE DISASSEMBLER

foone,
@foone@digipres.club avatar

(I'm going to have to write it. Well, finish writing it)

foone,
@foone@digipres.club avatar

the nice thing about having a scripting language is sometimes I'll see that they bind a method called "setSkippable" to ID 116 and in callMember they switch on the id and case 116 reads:

this->camera->field08 = getArgument(1);

GEE I WONDER WHAT FIELD08 MIGHT BE

foone,
@foone@digipres.club avatar

this is like 90% of what I've been doing for the last three months.

I'm tracking down lots of obvious clues scattered in a million places to the silliest murder mystery

foone,
@foone@digipres.club avatar

BAD PROGRAMMER.
in ResourceLoadPacket::finalize, they have two for() loops nested in each other, each of which declares their variable as "i".

Because it's a separate scope, this means that these are two separate variables, so it works, but it's NAUGHTY

foone,
@foone@digipres.club avatar

if (this->nVersion == 262) {
if (this != (PropertyBook *)0x0) {
// do some stuff with this
}
}

someone got their checks in the wrong order. and their compiler didn't notice.

foone,
@foone@digipres.club avatar

love to spend a while looking through the code to try and understand how a variable is used, then I check the trace logs and the answer is "it isn't"

foone,
@foone@digipres.club avatar

PFuncSymbol takes a final boolean parameter. What does it do? We don't really need to know, because every time PFuncSymbol is called, it's false.

foone,
@foone@digipres.club avatar

DW_OP_regx 0xffff?
the variable is stored in numbered register 65535? something's gone fucky.

foone,
@foone@digipres.club avatar

pPVar2 = stack[-1]

NO NEGATIVE INDICES

THIS IS C++

foone,
@foone@digipres.club avatar

basically the stack is defined as an array of pointers to PVariable*

But they cheat, and say they have one less than they allocated, and treat the first entry as a count of how many items are in the array.

And because that's too sensible, they store the pointer to the second entry in the array, so they have to go to negative indices to get the size back when they need to expand it

foone,
@foone@digipres.club avatar

this is how some versions of new[] work in C++. but that's a compiler, not A GAME OF WHEEL OF FORTUNE FOR THE FUCKING WII

foone,
@foone@digipres.club avatar

this is C++. they could have just defined their stack as a class with two members, a size and an array.

or, you know, just used an appropriate STL template

foone,
@foone@digipres.club avatar

BUT NO, THE PROGRAMMERS HAD TO BE CLEVER

foone,
@foone@digipres.club avatar

so there's 25 functions here which have the exact same bytes in their body. You'd think this would be a prime candidate for link-time function-merging but I suspect there's a C++ standard reason why they can't do that:

the pointer to two different functions should probably be different

foone,
@foone@digipres.club avatar

they all are "pop the top value off the VM stack and static-cast it to a different pointer".

And since types are mostly a compile-time thing, the contents of these functions end up being exactly the same. Every function is effectively just returning a void*, but the compiler enforces using them differently

foone,
@foone@digipres.club avatar

I will never understand how and why CodeWarrior debugs the arguments to functions the way it does.
It'll be all:
This is a function!
It's called Foobar!
It has 5 parameters!
Parameter 1 is an int named nSlots!
Parameter 2 is a char* named szScript!
Parameter 3 is a bool named bReload!
Parameter 5 is a PVariable* named pv!

foone,
@foone@digipres.club avatar

and it's not like parameter 4 goes unused. It's clearly used in the function, I can see it used.

SO WHY DIDN'T YOU TELL ME WHAT IT WAS CALLED?

foone,
@foone@digipres.club avatar

line 1038 of E:\Build\WheelJeopardy\Parser/PVMachine.cpp somehow declares two pointers named op0 and op1, which is fine, except the debug info says they are both stored in r27. At the same time.

how.

foone,
@foone@digipres.club avatar

you've heard of the Single Instruction Computer? Well make way for the Single Register Computer!
Everything is stored in R27

foone,
@foone@digipres.club avatar

artists depiction of a programmer trying to instrument tracing into the game's virtual machine

foone,
@foone@digipres.club avatar

I ran the game for 10 seconds. 4 seconds of blank screen, 6 seconds of the "make sure you put on the Wii Strap" screen.

78 megabytes of logs as it executed 233,615 instructions in the VM

foone,
@foone@digipres.club avatar

there are only two major problems in reverse engineering:

  1. not enough data
  2. too much data
foone,
@foone@digipres.club avatar

and that's with only one VM! This game supports multiple concurrent VMs executing at once, because of course it does.

and who doesn't want to debug concurrent C++ implemented by a bunch of game developers for a licensed tie-in-game?

foone,
@foone@digipres.club avatar

guess I just have to write a log parser for dolphin to extract out execution traces from multiple megabytes of logs. this is a reasonable thing to do

foone,
@foone@digipres.club avatar

yeah that seems reasonable

foone,
@foone@digipres.club avatar

I may have figured out why function parameters sometimes get elided:

I think it is ignoring const pointers.

foone,
@foone@digipres.club avatar

NOPE! found a function with a const char* and it's included.

whatta hell

foone,
@foone@digipres.club avatar

but it calls a different function with the same arguments, and the const char* is elided from THAT one.

foone,
@foone@digipres.club avatar

I'm gonna have to reverse engineer codewarrior next and see if there's any rand() calls in it

foone,
@foone@digipres.club avatar

I can find event A: a new VM is created
event B: a script is loaded into the VM
event C: a script is executed on the VM

except while the VM pointers for A and C match, they don't at all match B. I'm apparently loading scripts into VMs that were never created. and never loading scripts into VMs that I'm executing

foone,
@foone@digipres.club avatar

OH IT'S GOT BASTARD CACHING

it loads the script, creates a VM for it, then does it again. This time it doesn't need to load the script, because it's already loaded, but it DOES create a new VM. So the second VM never had the script loaded, it just grabbed it from the cache. Then it runs on the second VM

foone,
@foone@digipres.club avatar

no it's mostly got "foone forgot the difference between a pointer and a value" bastards.
Fixed. Next, the stack. Oh god, the stack.

foone,
@foone@digipres.club avatar

I've got to dump a stack in a "language" that's not turing complete and has no loops.

this'll be fun

foone,
@foone@digipres.club avatar

never underestimate the power of loop unrolling and pointless logging

foone,
@foone@digipres.club avatar

help I ran the game for 45 seconds and I now have 2.8 gigabytes of logs

tr7zw,

@foone issue closed: expected behaviour, just buy a bigger drive.

foone,
@foone@digipres.club avatar

@tr7zw I recently did a project where I scanned every Wii game for misplaced files, do you really think I have a file storage shortage problem? :)

tr7zw,

@foone Was more a play on the "works on my machine" trope.

  • All
  • Subscribed
  • Moderated
  • Favorites
  • random
  • Durango
  • DreamBathrooms
  • thenastyranch
  • magazineikmin
  • osvaldo12
  • khanakhh
  • Youngstown
  • mdbf
  • slotface
  • rosin
  • everett
  • ngwrru68w68
  • kavyap
  • InstantRegret
  • JUstTest
  • GTA5RPClips
  • cubers
  • ethstaker
  • normalnudes
  • tacticalgear
  • cisconetworking
  • tester
  • Leos
  • modclub
  • megavids
  • provamag3
  • anitta
  • lostlight
  • All magazines