Beta Quest Mod
Project Status Finished
Project Duration 6 months
Software Used Visual Studio
Languages Used C++, C#, C, Assembly, Python

There came a point where I decided I should put my accumulated knowledge about Twilight Princess to the test and create a Modification for it. I chose the HD version since it is by far the nicest looking release of the game. The result is Twilight Princess HD - Beta Quest:

Beta Quest randomizes all the loading zones in Twilight Princess. The generator is driven by a seed which is calculated based off the name the player gives Link before starting a new file. With randomized loading zones it becomes a real challenge to progress in the game. You have to perform tricks and often glitches to leave an area again since you potentially arrive with no items to progress normally.

If you find an useful loading zone, it becomes a game of memorizing the chain that lead to it and later on when you possess a required item to advance you need to recall it. Topped off with Bingo cards that give every player a random set of objectives to complete and the ability to race against each other make Beta Quest a very addictive and fun way to play.


Patching Game Files

Alongside new features I also wanted to modify a few of the game maps for Beta Quest. I quickly wrote my own tool that can parse, edit and expand .dzr files - a format Nintendo uses to store map and entity information. Thus I was able to change the collisions of a game area for example or include new actors while removing others.


Getting these changes into the game was an initial obstacle since the Wii U is a closed platform and games are not easily modifiable like on PC. Uploading the modded game in its entirety was not an option for obvious reasons and letting users patch in the changes themselves is way too complicated of a process. Finally many people own the game on disc and can not modify the files.

The best solution ended up being to hook into a Wii U system function that manages file system access as shown by the following code snippet:

//FS defines
#define FS_STATUS_OK                    0
#define FS_RET_UNSUPPORTED_CMD          0x0400
#define FS_RET_NO_ERROR                 0x0000
#define FS_MOUNT_SOURCE_SIZE            0x300
#define FS_MAX_MOUNTPATH_SIZE           128
#define FS_SOURCETYPE_EXTERNAL          0

DECL(int, FSOpenFile, void *pClient, void *pCmd, const char *path, const char *mode, int *fd, int errHandling)
{
	std::string newPath = path;

	//Load modded file from SD if a match is found
	if (g_fileRedirectActive)
	{
		int status = 0;
		char mountSrc[FS_MOUNT_SOURCE_SIZE];
		char mountPath[FS_MAX_MOUNTPATH_SIZE];

		if (strstr(path, "/content/") != 0)
		{
			std::string localSDPath = newPath.substr(newPath.find("content"));
			localSDPath = "/vol/external01/wiiu/apps/TP_BetaQuest/" + localSDPath;

			//Mount SD card
			if ((status = FSGetMountSource(pClient, pCmd, FS_SOURCETYPE_EXTERNAL, &mountSrc, FS_RET_NO_ERROR)) == FS_STATUS_OK)
			{
				if ((status = FSMount(pClient, pCmd, &mountSrc, mountPath, FS_MAX_MOUNTPATH_SIZE, FS_RET_UNSUPPORTED_CMD)) == FS_STATUS_OK)
				{
					FSStat stats;
					if ((status = real_FSGetStat(pClient, pCmd, localSDPath.c_str(), &stats, -1)) == FS_STATUS_OK) //Get file stats. FS_STATUS_OK means the file was found on SD
					{
						log_printf("FSOpenFile: File %s found in MOD!", localSDPath.c_str());
						newPath = localSDPath;
					}
				}
				else
				{
					log_printf("FSMount failed %d\n", status);
				}
			}
			else
			{
				log_printf("FSGetMountSource failed %d\n", status);
			}
		}
	}

	//Open the normal or modded file
	return real_FSOpenFile(pClient, pCmd, newPath.c_str(), mode, fd, errHandling);
}

By doing the FSOpenFile hook I'm able to mount an external SD card and re-direct file access from the disc or game installation folder to a directory on the SD card instead if the game attempts to open a file that I shipped a modified copy with.

This made modding significantly easier while allowing Beta Quest to retain a small overall size and be simple and fast to install.


Example: Adding Map Waypoints

For Beta Quest I figured out the format Nintendo uses to store map waypoints. Therefore I was able to create my own paths. Paths are for instance used to allow Midna to jump ahead and drag Wolf Link with her.

To make a remote area in the Kakariko Village more easily accessible in Beta Quest I added a new trigger onto a platform which plays a custom dialog that I added to the game's language files. Upon completing the dialog a new Midna path would be activated that allowed the player to jump up the entire way to the remote area.


New waypoints (cycled through in red):


The new Midna jump in action:

Spawning Actors in Real Time

To make modding even more straightforward then having to edit and re-pack files after every change - something that can get annoying fast if you just want to move an object around - I came up with a trick. By simply calling the native game functions that I found through reverse engineering the executable it became possible to spawn actors in real time directly from my Homebrew code.

The following excerpt is from the current version of Beta Quest and illustrates how helper functions that I wrote were used to dynamically spawn several new loading zones and entities in the game on the fly:

//These definitions normally reside in game_defs.h
typedef struct //Generic Actor memory entry
{
	unsigned int params;

	float xPos;
	float yPos;
	float zPos;

	unsigned short xRot;
	unsigned short yRot;

	unsigned short flag;
	signed short enemy_id;

	unsigned char flags[9];
	unsigned char room_id;

	unsigned char padding[2];
}fopAcM_prm_class;

typedef struct //Actor property template (objects)
{
	unsigned char name[8];
	unsigned int params;

	float xPos;
	float yPos;
	float zPos;

	unsigned short xRot;
	unsigned short yRot;

	unsigned short flag;
	signed short enemy_id;
}stage_ACTR_data_class;

typedef struct //Scalable Actor property template (mostly triggers)
{
	unsigned char name[8];
	unsigned int params;

	float xPos;
	float yPos;
	float zPos;

	unsigned short unk1;
	unsigned short yRot;
	unsigned short unk2;
	unsigned short unk3;

	unsigned char scaleX;
	unsigned char scaleY;
	unsigned char scaleZ;
	unsigned char padding;
}stage_SCOB_data_class;

//Game function to create new entry in the dynamic actor table and returns ptr to it
DECL(fopAcM_prm_class*, TP_fopAcM_CreateAppend, void)
{
	fopAcM_prm_class* actorMemoryPtr = real_TP_fopAcM_CreateAppend();
	return actorMemoryPtr;
}

//Game function that takes an actor template and the ptr to a corresponding actor memory table entry and spawns the actor in the world after
DECL(void, TP_Stage_ActorCreate, void* actorTemplate, fopAcM_prm_class* actorMemory)
{
	real_TP_Stage_ActorCreate(actorTemplate, actorMemory);
}

//Game function to check whether a switch in the savefile has been turned on
DECL(int, TP_Save_isSwitch, unsigned int* gameInfoPtr, int flagID, int roomID)
{
	int isSwitchedOn = real_TP_Save_isSwitch(gameInfoPtr, flagID, roomID);
	return isSwitchedOn;
}

//Helper function to spawn a loading zone in the indicated location
void SpawnLoadingZone(char roomID, float xPos, float yPos, float zPos, unsigned short yRot, char scaleX, char scaleY, char scaleZ, char autoWalkDirection, bool isAirTrigger, char exitID)
{
	stage_SCOB_data_class loadingZone;

	char type = 0xFF; //Ground trigger by default

	if (isAirTrigger)
		type = 0x00;

	sprintf((char*)loadingZone.name, "scnChg"); //SceneChange = Loading Zone

	/*
	Params for scnChg:
	FF = Generic identifier/padding
	XX = Direction of the auto walk (00=Backwards ; 01=Right ; 02=Left ; 03=Forward)
	XX = Type of loading zone (00=Air ; FF=Ground)
	XX = Exit ID (only valid for the active room. Under the ID the room stores information like "area to load", "fade type" and "fade duration")										
	*/
	loadingZone.params = (0xFF << 24) + (autoWalkDirection << 16) + (type << 8) + exitID;
	loadingZone.xPos = xPos;
	loadingZone.yPos = yPos;
	loadingZone.zPos = zPos;
	loadingZone.unk1 = 0x0FFF;
	loadingZone.yRot = yRot;
	loadingZone.unk2 = 0x0FFF;
	loadingZone.unk3 = 0xFFFF;
	loadingZone.scaleX = scaleX;
	loadingZone.scaleY = scaleY;
	loadingZone.scaleZ = scaleZ;
	loadingZone.padding = 0xFF;

	//Reserve an actor slot for the zone
	fopAcM_prm_class* actorMemoryPtr = real_TP_fopAcM_CreateAppend();

	actorMemoryPtr->params = loadingZone.params;

	actorMemoryPtr->xPos = loadingZone.xPos;
	actorMemoryPtr->yPos = loadingZone.yPos;
	actorMemoryPtr->zPos = loadingZone.zPos;

	actorMemoryPtr->xRot = loadingZone.unk1;
	actorMemoryPtr->yRot = loadingZone.yRot;
	actorMemoryPtr->flag = loadingZone.unk2;
	actorMemoryPtr->enemy_id = loadingZone.unk3;

	actorMemoryPtr->flags[0] = loadingZone.scaleX;
	actorMemoryPtr->flags[1] = loadingZone.scaleY;
	actorMemoryPtr->flags[2] = loadingZone.scaleZ;

	actorMemoryPtr->room_id = roomID;

	//Spawn the zone in the game
	real_TP_Stage_ActorCreate(&loadingZone, actorMemoryPtr);
}

//Helper function to spawn an actor/object in the game world
void SpawnActor(char roomID, const char *name, unsigned int params, float xPos, float yPos, float zPos, unsigned short xRot, unsigned short yRot, unsigned short flag, signed short enemy_id)
{
	stage_ACTR_data_class actor;

	sprintf((char*)actor.name, "%s", name); //Actor class/name
	actor.params = params;
	actor.xPos = xPos;
	actor.yPos = yPos;
	actor.zPos = zPos;
	actor.xRot = xRot;
	actor.yRot = yRot;
	actor.flag = flag;
	actor.enemy_id = enemy_id;

	fopAcM_prm_class* actorMemoryPtr = real_TP_fopAcM_CreateAppend();

	actorMemoryPtr->params = actor.params;

	actorMemoryPtr->xPos = actor.xPos;
	actorMemoryPtr->yPos = actor.yPos;
	actorMemoryPtr->zPos = actor.zPos;

	actorMemoryPtr->xRot = actor.xRot;
	actorMemoryPtr->yRot = actor.yRot;

	actorMemoryPtr->flag = actor.flag;
	actorMemoryPtr->enemy_id = actor.enemy_id;
	actorMemoryPtr->room_id = roomID;

	real_TP_Stage_ActorCreate(&actor, actorMemoryPtr);
}

//Game function. Called after the area data has been loaded and creates all the room actors. Add dynamically modded content here
DECL(void, TP_Room_ReLoader, void* ptr, u32* dStage_dt_c, int roomID)
{
	real_TP_Room_ReLoader(ptr, dStage_dt_c, roomID); //Call the real function first which spawns all the normal actors, then we spawn ours

	//Grab current map info stuff
	const unsigned char *currentStagePtr = (const unsigned char *)0x1064CDE8;
	unsigned char currentStage[8];

	const unsigned char *currentLayerPtr = (const unsigned char *)0x1062915B;
	unsigned char currentLayer[2];

	memcpy(currentStage, currentStagePtr, 8);
	memcpy(currentLayer, currentLayerPtr, 2);

	//Spawn special exit zones in City/Lakebed/Palace
	if (!strcmp((const char*)currentStage, "D_MN07") && roomID == 0) //City Exit Zone
		SpawnLoadingZone(roomID, -16.655250, -200.00, 11700.000, 0, 20, 30, 25, 0xFF, false, 0x01);
	else if (!strcmp((const char*)currentStage, "D_MN07") && roomID == 16) //City Ooccoo Shop Exit Zone
		SpawnLoadingZone(roomID, 0, 0, 650, 0, 25, 20, 15, 0xFF, false, 0x00);
	else if (!strcmp((const char*)currentStage, "D_MN01") && roomID == 0) //Lakebed Exit Zone
		SpawnLoadingZone(roomID, -10, 650, 25000, 0, 62, 51, 25, 0xFF, false, 0x00);
	else if (!strcmp((const char*)currentStage, "D_MN08") && roomID == 0) //PoT Exit Zone
		SpawnLoadingZone(roomID, 2700, 146, 14870, 0, 20, 10, 5, 0xFF, false, 0x05);

	/*
	Params for E_ym (normal Twilight bug)
	XX = Bug ID/Switch to turn on when killed
	FF = Generic identifier/padding
	FF = ^
	XX = Bug type (00 - normal bug; 01 - can stick to walls; 03 - flying by default; 04 - can dig)
	*/

	/*
	Params for E_ymt (invisible flying bug. Needs a corresponding normal bug that doesn't fly by default. If present the normal bug can transform into a flying one if player closes in)
	00 = Generic identifier/padding
	00 = ^
	00 = ^
	XX = Flag ID of base E_ym (needs to match the flag id of a normal bug)
	*/

	/*	
	Params for Drop (tear of light)
	00 = Generic identifier/padding
	00 = ^
	XX = Bug this tear belongs to (Bug ID/Switch that makes the tear visible once turned on)
	XX = Tear ID (used to display tears on the minimap)
	*/

	//Spawn additional Twilight Bugs in Eldin Twilight 
	if (!strcmp((const char*)currentStage, "F_SP109") && roomID == 0 && !strcmp((const char*)currentLayer, "e")) //Kakariko while Twilight
	{
		//Sanctuary Top
		SpawnActor(roomID, "E_ym", 0x01FFFF00, 1220, 1270, 5650, 0xFF00, 0, 0xFFFF, 0xFFFF);
		SpawnActor(roomID, "Drop", 0x00000101, 1350, 1400, 5100, 0, 0, 0, 0xFFFF);

		//Hotspring Water
		SpawnActor(roomID, "E_ym", 0x02FFFF03, -5194, 3033, -2061, 0xFF03, 0, 0xFFFF, 0xFFFF);
		SpawnActor(roomID, "Drop", 0x00000202, -5194, 2933, -2061, 0, 0, 0, 0xFFFF);

		//Top Tower
		SpawnActor(roomID, "E_ym", 0x03FFFF00, -2100, 3830, -6200, 0xFF03, 0, 0xFF01, 0xFFFF);
		SpawnActor(roomID, "E_ymt", 0x00000001, -2650, 1477, 2071, 0, 0, 0, 0xFFFF);
		SpawnActor(roomID, "Drop", 0x00000303, -1500, 3930, -6400, 0, 0, 0, 0xFFFF);

		//Spawn the tears in Kakariko Spring as well if the respective bug has been killed
		if (real_TP_Save_isSwitch((unsigned int*)0x10647b48, 0x07, 0xFF) == 1) //0x10647b48 = gameInfoPtr, 0x07 = Switch ID of the Twilight bug
			SpawnActor(roomID, "Drop", 0x00000707, 1268, 0, 11918, 0, 0, 0, 0xFFFF);

		if (real_TP_Save_isSwitch((unsigned int*)0x10647b48, 0x08, 0xFF) == 1)
			SpawnActor(roomID, "Drop", 0x00000808, 376, 0, 11963, 0, 0, 0, 0xFFFF);

		if (real_TP_Save_isSwitch((unsigned int*)0x10647b48, 0x0C, 0xFF) == 1)
			SpawnActor(roomID, "Drop", 0x00000C0C, 188, 0, 11740, 0, 0, 0, 0xFFFF);
	}
	else if (!strcmp((const char*)currentStage, "F_SP110") && roomID == 0 && !strcmp((const char*)currentLayer, "e")) //Death Mountain Entrance
	{
		//DM Entrance Bug
		SpawnActor(roomID, "E_ym", 0x07FFFF01, -2400, -4800, 26610, 0xFF03, 0, 0xFFFF, 0xFFFF);
		SpawnActor(roomID, "Drop", 0x00000707, -2400, -5100, 27000, 0, 0, 0, 0xFFFF);
	}
	else if (!strcmp((const char*)currentStage, "F_SP110") && roomID == 3 && !strcmp((const char*)currentLayer, "e")) //Death Mountain Main Area
	{
		//Front Shortcut Bug
		SpawnActor(roomID, "E_ym", 0x08FFFF00, -300, -870, -4180, 0xFF00, 0, 0xFFFF, 0xFFFF);
		SpawnActor(roomID, "Drop", 0x00000808, 1100, -600, -4560, 0, 0, 0, 0xFFFF);

		//Back Shortcut Bug
		SpawnActor(roomID, "E_ym", 0x0CFFFF00, -3700, 100, -4570, 0xFF00, 0, 0xFFFF, 0xFFFF);
		SpawnActor(roomID, "Drop", 0x00000C0C, -3700, 200, -4570, 0, 0, 0, 0xFFFF);
	}


	//Spawn Bomb Bag and Horse Call chest in Link's house for intro choice (tboxB0 = treasure chest)
	if (!strcmp((const char*)currentStage, "R_SP01") && roomID == 4)
	{
		SpawnActor(roomID, "tboxB0", 0xFF2FF3C0, -100.0f, 0.0f, -200.0f, 0, 0, 0x84FF, 0xFFFF);
		SpawnActor(roomID, "tboxB0", 0xFF1FF3C0, 100.0f, 0.0f, -200.0f, 0, 0, 0x50FF, 0xFFFF);

		const unsigned char *bombBagOwned = (const unsigned char *)0x10647BF3;
		const unsigned char *horseCallOwned = (const unsigned char *)0x10647BF9;

		if (*bombBagOwned == 0xFF && *horseCallOwned == 0xFF) //Bomb Bag and Horsecall not owned, assume the chests are still closed and watch out for the player opening one
		{
			g_checkIntroItemReload = 1;
		}
	}
}


Dynamically spawned Twilight Bugs and Tears of Light:


The Intro:

Link's house - the starting point of every Beta Quest file. With the help of the code snippet above I added several chests containing useful items into the scene as well as a dozen puppies creating a cosy atmosphere.

Reverse Engineering and Hooking of Game Functions

In Twilight Princess each game map can have several states (internally called 'layers'). During area load one specific layer is chosen depending on story progress. Layers affect many things about a map, but most importantly which actors are present.

To fix some potential softlocks in Beta Quest if you complete major story events in an unintentional order it became necessary to change the function that determines the layer a map should have at any given point. First of all I had to find, identify and figure out the function in the game's executable.

Understanding PowerPC assembly was key in gaining the required knowledge about getLayerNo():



After reversing the input parameters and understanding how the function determines the layer, I was then able to write my function hook:

DECL(int, TP_getLayerNo, const char* stageName, int roomID, int layerOverride)
{
	int result = real_TP_getLayerNo(stageName, roomID, layerOverride);
	u8 layerOverrideByte = (u8)layerOverride;

	if (layerOverrideByte == 0xFF)
	{
		if (!strcmp(stageName, "F_SP108") && result != 0x0E && (roomID == 3 || roomID == 4)) //Coro area in South Faron Woods
		{
			const char *lanternOwned = (const char*)0x10647BE5;
			if (*lanternOwned != 72) //Lantern not owned
			{
				return 0x01;
			}
		}
		else if (!strcmp(stageName, "F_SP103") && roomID == 0) //Ordon Village
		{
			const char *rodOwned = (const char *)0x10647BF8;
			if (*rodOwned != 74 && *rodOwned != 92) //Rod not owned nor upgraded
			{
				return 0x00;
			}
		}
		else if (!strcmp(stageName, "R_SP01") && roomID == 2) //Shield house in Ordon
		{
			return 0x01;
		}
		else if (!strcmp(stageName, "R_SP01") && roomID == 5) //Sword house in Ordon
		{
			return 0x00;
		}
	}

	return result;
}


When the real function returns the appropriate layer number I simply run my own checks against a list of game flags and modify the return value if needed before passing it back to the calling game function.