Body Harvest for the Nintendo 64 is generally regarded as being fun to play, but a bit of a pig to look at, is there anything that can be done to improve that?
A couple of years back there was a big hullabaloo around disabling Anti-aliasing on N64 games – removing some of that characteristic blur, and BH certainly has blur. But before changing video interface settings, there’s actually a bigger problem we can solve. In its low-resolution mode the N64 outputs a video image of 320×240 pixels, but a lot of games don’t even manage to render this. During gameplay Body Harvest renders at 304×230 and then scales that image up to 320×240 for display, which results in a much blurrier output, what if it were to render at the full output resolution?
At 0x7984 in the NTSC ROM is a function that is called before gameplay starts to set the render resolution:
If we change the arguments to the two function calls here to 0x140 and 0xF0 respectively, then the game renders the full output resolution and does no scaling, resulting in this:
That’s already a noticeable improvement! Presumably the lower resolution was chosen as a performance enhancement, but it’s a shame it was, the game looks a lot better without any scaling going on.
Can we do more? At 0x10218 in the ROM is this function which sets two Video Interface Special Features, Gamma to off, and Dither to on.
If we switch the Dither on flag (0x40) for the off flag (0x80):
That makes a big difference, the textures are a lot clearer. Seeing the game run with dither off is a mixed bag though, everything looks quite grainy, like a film, I’m not sure I prefer it.
Now the Video Mode the game is currently running in is LAN1, where the A stands for Anti-aliasing, in theory we should be able to switch to LPN1, where the P stands for Point-sampling, to achieve even further visual clarity. However changing the call to osViSetMode to LPN1 doesn’t seem to make any visual difference at all, even on real hardware. It might be that some other value needs changing.
Additionally, as BH doesn’t make use of the Expansion Pak it should be possible to re-point the framebuffers there and switch to a higher resolution using one of the modes such as HPF1 or HPN1. However when I switch to one of those modes, set the resolutionn to 640×480 and point the (four times larger) framebuffers at some empty RAM, the results are quite odd. Every second frame seems to have what looks something like the Z-buffer drawn half over it, and each vertical half of the frame seems to be drawn over the top of each other as a single frame. I’m not sure exactly what is going on here.
That’s a custom donut car, in game and working just fine.
I also found the real car settings, not the fake ones on the car select screen, so now you can make a car that accelerates so fast it takes off and flies right out of the track… err, good times…
Fixed up support for 8-bit pngs, as not many programs let you write 4-bit palettes. GIMP can, but it doesn’t make it obvious. Custom textures are now easy as.
Well it took a little longer than I might have expected, but here’s a custom-made car model inserted into the game, and running on a real N64. Such quality.
Thought I’d take a break from the tracks and look at the cars again, which are a lot less complex. I’m now at the point where I can export a car with correct textures to an .obj file. Doing the reverse and putting a modified car back into the game is now just a matter of implementing it (unless there’s something I don’t know I don’t know… which is quite possible).
I’ll make a first release once importing a car is possible.
Sometimes opening a file in a hex editor and staring at it is enough to begin working out patterns and possible meaning, other times I can see structure but the meaning is totally beyond comprehension.
Overlaying this data onto already understood data in a visual form can sometimes make its meaning clear. For example, each track collision section has an array of “Triangle” structs which describe the collision mesh. Each Triangle is comprised of 3 indices into the vertex array, and then 4 Bytes of unknown data. The last of these four Bytes is always a low value (<6), often the same value for most of the Triangles in a section, but I have no idea what-so-ever as to its meaning.
So as an experiment, I render the collision mesh and use this value to select a colour for each Triangle from an array of pre-defined colours:
Black Forest with the ground type colour coded
Aha! These different colours match exactly with the different parts of the track, this value must be the collision’s type, which could be road, dirt, grass etc. I still don’t know where these types are defined but this is a good start.
Moving on, each collision vertex is defined as 3 floats for position, 4 Bytes for lighting color (R8G8B8A8), and two shorts whose purpose is unknown. The first short is sometimes 0xFFFF, and otherwise has similar values to the second short which seems to start at 0 for the first vertex and gradually increments by 1 for later vertices.
If I colour the mesh differently depending on whether or not this vertex has a 0xFFFF value, this is the result:
Sydney with the unknown vertex properties == 0xFFFF highlighted
The collision sections with a 0xFFFF value are the sections where the different variations of the track diverge from each other.
If I instead alter the meshes colour depending on the value of the second short I get this:
Kyoto with the collision polygons coloured by the vertex unknown2 value
I’m still not sure what this data is, but now I can see the overall pattern. Perhaps the increasing values are how the game determines if you are going the wrong way around the track, or each cars placing. Doing this colouring using the first shorts values shows that excepting the 0xFFFF values, these are forming a similar – but less segmented – pattern in reverse.
Kyoto with the collision polygons coloured by the vertex unknown1 value
A bunch of N64 games have dummied functions left in which used to send strings out to a console on the debugging hardware. Now they typically do nothing, but some games still call them a lot. I hacked together a little something to pull strings out of PJ64 when the function is called. It’s not pretty, I borrowed code from wherever I could find it and it doesn’t even work 100% but it does print out some strings from some games. Some are missed because multiple calls to the function in a row happen too fast to be seen. I started implementing support for a version of the function found in NHL Breakaway ’98 but it’s not in working form in the code here.
PJ64 must be running in Interpreter mode for this to work.
It probably won’t work unmodified on anyone else’s system as it’s using a hardcoded address.
A much better option would be to write this into an emulator 🙂 Maybe some day.
// N64 Debug Console.cpp : main project file.
#include <windows.h>
#include <stdio.h>
#include <Psapi.h>
#include <iostream>
#include <string>
#include <Tlhelp32.h>
using namespace std;
#define PROCESS_NAME "Project64.exe"
#define MAX_PROCESS 100
#define BASE_ADDRESS 0x52ea0000
HANDLE pPJ64Process;
int MAX_MSG_LENGTH = 100;
enum FormatType {typeD, typeF, typeS};
enum FunctionType {bHarvest, nHL};
void GetProcess( char *pName )
{
int pCount = 0;
PROCESSENTRY32 pEntry = { 0 };
pEntry.dwSize = sizeof(PROCESSENTRY32);
HANDLE pSnapShot = CreateToolhelp32Snapshot( TH32CS_SNAPPROCESS, 0 ); //Create an 32SnapShot.
if(Process32First( pSnapShot, &pEntry ))
{
while( Process32Next( pSnapShot, &pEntry ) && pCount < MAX_PROCESS-1 ) //Loop through all processes.
{
HANDLE hProcess = OpenProcess( PROCESS_ALL_ACCESS,FALSE, pEntry.th32ProcessID ); //Open the current Process.
if(!strcmpi( pEntry.szExeFile, PROCESS_NAME ) ) //Check if the current process is the PROCESS_NAME
{
pPJ64Process = hProcess;
cout << "Found PJ64" << "\n";
pCount++;
}
}
}
CloseHandle( pSnapShot ); //Close the SnapShot.
}
void endianReverse(byte *bytes)
{
byte temp;
byte temp2;
int i = 0;
while (i <= (MAX_MSG_LENGTH - 4))
{
temp = bytes[i];
temp2 = bytes[i+1];
bytes[i] = bytes[i+3];
bytes[i+1] = bytes[i+2];
bytes[i+2] = temp2;
bytes[i+3] = temp;
i+=4;
}
}
int findFormatters(byte *rawMsg, FormatType *types)
{
int numFormatters = 0;
int i = 0;
while (rawMsg[i] != 0)
{
if ((char)rawMsg[i] == '%')
{
while ((char)rawMsg[i] != 'd' && (char)rawMsg[i] != 'x' && (char)rawMsg[i] != 'f' && (char)rawMsg[i] != 's')
{
//if (rawMsg[i] == 0) break;
i++;
}
if ((char)rawMsg[i] == 'd' || (char)rawMsg[i] == 'x') types[numFormatters] = typeD;
else if ((char)rawMsg[i] == 'f') types[numFormatters] = typeF;
else if ((char)rawMsg[i] == 's') types[numFormatters] = typeS;
numFormatters++;
}
i++;
}
return numFormatters;
}
byte* getSubString(byte* subString, int pointer)
{
int offset = pointer & 0x00FFFFFF;
ReadProcessMemory(pPJ64Process,(void*)(BASE_ADDRESS + offset),subString,MAX_MSG_LENGTH,0);
return subString;
}
void messageFormat(byte *rawMsg, byte *param1, byte *param2, byte *param3, int p1, int p2, int p3)
{
int i = 0;
byte *subString = new byte[MAX_MSG_LENGTH];
FormatType *formatTypes = new FormatType[3];
int numFormatters = findFormatters(rawMsg, formatTypes);
//Should only pass in paramXs, then create pXs in here
if (numFormatters == 0)
{
printf((char*)rawMsg);
}
else if (numFormatters == 1)
{
if (formatTypes[0] == typeD)
{
printf((char*)rawMsg, p1);
}
else if (formatTypes[0] == typeF)
{
printf((char*)rawMsg, (float)*param1);
}
else if (formatTypes[0] == typeS)
{
subString = getSubString(subString, p1);
endianReverse(subString);
printf((char*)rawMsg, (char*)subString);
}
else
{
cout << "unknown % format (1)" << "\n";
}
}
else if (numFormatters == 2)
{
if (formatTypes[0] == typeD && formatTypes[1] == typeD)
{
printf((char*)rawMsg, p1, p2);
}
else if (formatTypes[0] == typeD && formatTypes[1] == typeS)
{
subString = getSubString(subString, p2);
endianReverse(subString);
printf((char*)rawMsg, p1, (char*)subString);
}
else if (formatTypes[0] == typeD && formatTypes[1] == typeF)
{
printf((char*)rawMsg, p1, (float)*param2);
}
else if (formatTypes[0] == typeF && formatTypes[1] == typeF)
{
printf((char*)rawMsg, (float)*param1, (float)*param2);
}
else
{
cout << "unknown % format (2): " << (char*)rawMsg << "\n";
}
}
else
{
if (formatTypes[0] == typeD && formatTypes[1] == typeD && formatTypes[2] == typeD)
{
//printf((char*)rawMsg, (int)*param1, (int)*param2, (int)*param3);
printf((char*)rawMsg, p1, p2, p3);
}
else if (formatTypes[0] == typeF && formatTypes[1] == typeF && formatTypes[2] == typeF)
{
printf((char*)rawMsg, (float)*param1, (float)*param2, (float)*param3);
}
else if (formatTypes[0] == typeD && formatTypes[1] == typeD && formatTypes[2] == typeS)
{
subString = getSubString(subString, p3);
endianReverse(subString);
printf((char*)rawMsg, p1, p2, (char*)subString);
}
else
{
cout << "unknown % format (3)" << "\n";
}
}
//cout << endl;
}
DWORD GetModuleBase(HANDLE hProc, string &sModuleName)
{
HMODULE *hModules;
char szBuf[50];
DWORD cModules;
DWORD dwBase = 0;
//------
EnumProcessModules(hProc, hModules, 0, &cModules);
hModules = new HMODULE[cModules/sizeof(HMODULE)];
if(EnumProcessModules(hProc, hModules, cModules/sizeof(HMODULE), &cModules)) {
for(int i = 0; i < cModules/sizeof(HMODULE); i++) {
if(GetModuleBaseName(hProc, hModules[i], szBuf, sizeof(szBuf))) {
if(sModuleName.compare(szBuf) == 0) {
dwBase = (DWORD)hModules[i];
break;
}
}
}
}
delete[] hModules;
return dwBase;
}
int main()
{
string sConsoleMessage = "";
//DWORD baseAddress = (DWORD)keke;
DWORD baseAddress = 0x52ea0000;
DWORD address;
//DWORD address = baseAddress + 0x6f620; // World Driver
DWORD readAddress;
byte *rawMsg = new byte[MAX_MSG_LENGTH];
byte *param1 = new byte[4];
byte *param2 = new byte[4];
byte *param3 = new byte[4];
byte newData[] = {0x00,0x80,0x0c,0x3c,
0x2c,0xf6,0x87,0xad,
0x28,0xf6,0x86,0xad,
0x20,0xf6,0x84,0xad,
0x24,0xf6,0x85,0xad,
0x08,0x00,0xe0,0x03,
0x08,0x00,0xbd,0x27};
byte newData2[] = {0x08,0x00,0xe0,0x03,0x00,0x00,0x00,0x00};
int p1;
int p2;
int p3;
int msgPointer = 0;
int oldMsgPointer = 1;
bool foundFunction = false;
FunctionType funcType = bHarvest;
int *memBlock = new int[16];
GetProcess( PROCESS_NAME );
//DWORD jiggle = GetModuleBase(pPJ64Process, string("heheh"));
if(!pPJ64Process)
{
cout <<"Could not get handle!\n"; cin.get(); } else { // Find print function int matching = 0; int x = 0; while (!foundFunction) { ReadProcessMemory(pPJ64Process,(void*)(baseAddress + x),memBlock,(sizeof(int) * 16),0); if (memBlock[0] == 0x27bdfff8) matching++; if (memBlock[1] == 0xafa40008) matching++; if (memBlock[2] == 0xafa5000c) matching++; if (memBlock[3] == 0xafa60010) matching++; if (memBlock[4] == 0xafa70014) matching++; if (memBlock[5] == 0x03e00008) matching++; if (memBlock[6] == 0x27bd0008) matching++; if (memBlock[7] == 0x27bdfff8) matching++; if (memBlock[8] == 0xafa40008) matching++; if (memBlock[9] == 0xafa5000c) matching++; if (memBlock[10] == 0xafa60010) matching++; if (memBlock[11] == 0xafa70014) matching++; if (memBlock[12] == 0x03e00008) matching++; if (memBlock[13] == 0x27bd0008) matching++; if (memBlock[14] == 0x00000000) matching++; if (memBlock[15] == 0x00000000) matching++; if (matching == 16) { foundFunction = true; } else { matching = 0; x += 4; } if (x == 0x00400000) break; } if (!foundFunction) { matching = 0; x = 0; } while (!foundFunction) // Look for NHL type function { ReadProcessMemory(pPJ64Process,(void*)(baseAddress + x),memBlock,(sizeof(int) * 16),0); if (memBlock[0] == 0x27bdff98) matching++; if (memBlock[1] == 0x27bd0068) matching++; if (memBlock[2] == 0x03e00008) matching++; if (memBlock[3] == 0x00000000) matching++; if (matching == 4) { foundFunction = true; funcType = nHL; } else { matching = 0; x += 4; } if (x == 0x00400000) break; } if (foundFunction) { int y = x; if (funcType == nHL) { y -= 0x1c; } if (y & 0x00008000) y+=0x10000; y += 8; newData[0] = (y >> 16) & 0xFF;
newData[0xd] = (y >> 8) & 0xFF;
newData[0xc] = y & 0xFF;
newData[0x11] = ((y + 4) >> 8) & 0xFF;
newData[0x10] = (y + 4) & 0xFF;
newData[0x9] = ((y + 8) >> 8) & 0xFF;
newData[0x8] = (y + 8) & 0xFF;
newData[0x5] = ((y + 12) >> 8) & 0xFF;
newData[0x4] = (y + 12) & 0xFF;
cout << "Function found at: " << hex << x << endl;
cout << "Function type: " << funcType << endl;
if (funcType == bHarvest)
{
WriteProcessMemory(pPJ64Process,(void*)(baseAddress + x + 0x1c),&newData,28,NULL);
WriteProcessMemory(pPJ64Process,(void*)(baseAddress + x),&newData2,8,NULL);
WriteProcessMemory(pPJ64Process,(void*)(baseAddress + x + 0x30),&newData2,8,NULL);
}
else if (funcType == nHL)
{
WriteProcessMemory(pPJ64Process,(void*)(baseAddress + x),&newData,28,NULL);
}
address = baseAddress + x + 0x8;
}
else
{
cout << "Didn't find function to hook";
}
while(1)
{
if (funcType == bHarvest)
{
ReadProcessMemory(pPJ64Process,(void*)address,&msgPointer,sizeof(msgPointer),0);
}
else if (funcType == nHL)
{
ReadProcessMemory(pPJ64Process,(void*)(address + 4),&msgPointer,sizeof(msgPointer),0);
}
if ((msgPointer & 0xff000000) == 0x80000000)
{
msgPointer = msgPointer & 0x0FFFFFFF;
if (msgPointer != oldMsgPointer) // New Message
{
//cout << hex << msgPointer << "\n";
readAddress = msgPointer + baseAddress;
ReadProcessMemory(pPJ64Process,(void*)readAddress,rawMsg,MAX_MSG_LENGTH,0);
if (funcType == bHarvest)
{
ReadProcessMemory(pPJ64Process,(void*)(address + 4),param1,4,0);
ReadProcessMemory(pPJ64Process,(void*)(address + 4),&p1,sizeof(int),0);
}
else if (funcType == nHL)
{
ReadProcessMemory(pPJ64Process,(void*)address,param1,4,0);
ReadProcessMemory(pPJ64Process,(void*)address,&p1,sizeof(int),0);
}
ReadProcessMemory(pPJ64Process,(void*)(address + 8),param2,4,0);
ReadProcessMemory(pPJ64Process,(void*)(address + 12),param3,4,0);
ReadProcessMemory(pPJ64Process,(void*)(address + 4),&p1,sizeof(int),0);
ReadProcessMemory(pPJ64Process,(void*)(address + 8),&p2,sizeof(int),0);
ReadProcessMemory(pPJ64Process,(void*)(address + 12),&p3,sizeof(int),0);
endianReverse(rawMsg);
messageFormat(rawMsg, param1, param2, param3, p1, p2, p3);
oldMsgPointer = msgPointer;
}
}
//Sleep(100);
}
return 0;
}
}