uTAS
Project Status Finished
Project Duration 5 months
Software Used Visual Studio
Languages Used C++, C#, C, Assembly, Lua

uTAS is a software suite for the Nintendo Wii U entirely realized with the use of Homebrew. It allows to create TAS (Tool Assisted Speedruns) directly on a console for the first time ever. There have been devices for other consoles to playback an existing TAS that was made with an emulator on PC, but not to create one from scratch.


Excerpt from Wikipedia: "A tool-assisted speedrun is a set sequence of controller inputs used to perform a task in a video game. The input sequence is usually created by emulating the game and using tools such as slow motion, frame-by-frame advance, memory watch, and save states to create an extremely precise series of inputs. The idea is not to make gameplay easier for players, but rather to produce a demonstration of gameplay that would be practically impossible for a human. Tool-assisted speedruns often feature gameplay that would otherwise be impossible or prohibitively difficult to perform in real time. Producers of tool-assisted speedruns do not compete with "unassisted" speedrunners of video games; on the contrary, collaborative efforts between the two groups often take place."

Without the aforementioned features creating a good TAS is next to impossible. Ultimately realizing nearly all of the bullet points on an actual console instead of an emulator took several months of intense work and research. The result is uTAS:


The following features are supported as of the initial release:

  • Input recording and playback
  • TAS files are stored on the SD card on a per game and region basis
  • Management of projects via a virtual file system
  • Stable ingame freezing and frame advance
  • TAS Input window including turbo buttons and shortcuts
  • Data Display which can be customized to monitor selected memory addresses in real time
  • Mini Savestates
  • Fast playback ability by turning off most of the GPU rendering to boost the game speed
  • Robust desync protection
  • Full lua scripting support to frame advance, savestate, read/write to game memory and take control of the gamepad in a script environment


About Wii U Homebrew

The currently available toolchains to create Homebrew for the Wii U are extremely powerful. An exploit in the webbrowser allows you to launch the Homebrew Launcher. The Homebrew Launcher has the ability to load apps in the form of ELF executables compiled by devkitPro to the PowerPC architecture.


After starting an app the HBL will hook itself into the system application loader calling the main entry point of your app if the user selects a game to start from the console home screen. Since your app code is stored in a special unused memory region of the console and the main entry point is called after the console wiped the main RAM in preparation for the game to be launched, you gain the ability to launch your own threads that will run alongside the game.

The entire console API and all its system libraries are available to the Homebrew code allowing you to use network sockets to communicate with external devices, accessing storage media such as the SD card and rendering things using the native GPU functions.

uTAS Demonstration

The following Twitch.tv clip demonstrates a boss fight in The Legend of Zelda: Twilight Princess HD on the Wii U that's being vastly speed-up with the help of uTAS and TAS only glitches:

The full TAS of this particular testing session can be found here.


What follows is a short presentation on how to create a TAS with uTAS:

How it all Works

The core pillars of uTAS are:

Function Hooks

Function hooks were paramount in realizing uTAS. Due the advanced state of Wii U Homebrew it is actually possible to hook into system as well as game functions and execute your own code before and after calling the "legit" function. Or perhaps not even calling the real function at all. Manipulation of the input and output parameters before returning control to the actual game code are also possible this way.

The following code snippet illustrates how a basic function hook into the VPADRead function was achieved - the function a game has to call to get the current input data from the gamepad. To record and playback inputs for uTAS hooking into this function was essential:


//Buffer that will store our 7 new PPC instructions needed to call the proper real system function later
unsigned int realFunctionBuffer[7];

//Function pointer to our buffer so the real function remains accessible in our program code
int(*real_VPADRead)(int channel, void *buffer, unsigned int bufferSize, int *error) = &realFunctionBuffer[0];

//Our new VPAD Read function implementation (will get called whenever the game polls the Wii U Gamepad)
int my_VPADRead(int channel, void *buffer, unsigned int bufferSize, int *error)
{
	log_printf("Hey the game wants to poll the gamepad");
	return real_VPADRead(channel, buffer, bufferSize, error); //Reads the actual inputs from the real system function and returns it to the caller because we are nice
}		

unsigned int realFuncAddr = 0xFFFFFFFF; //Would actually be the address of the Wii U VPADRead function from the vpad.rpl library
unsigned int restoreInstruction = 0; //Will store a copy of the first instruction from the real function we need to replace. Used to restore the function later if desired

void PatchFunction()
{
	unsigned int *space = &realFunctionBuffer[0];

	log_printf("Patching function VPADRead...");

	unsigned int physical = (unsigned int)OSEffectiveToPhysical((void*)realFuncAddr); //Gets the physical hardware address of the real function
	if (!physical) 
	{
		log_printf("Error. Something is wrong with the physical address!");
		return;
	}

	//Adds a new DBAT (Data Block Address Translation) entry with the help of a Kernel function Homebrew gained access to
	//For safety reasons the Wii U does not map its system library address region by default to avoid tampering
	bat_table_t my_dbat_table;
	KernelSetDBATsForDynamicFunction(&my_dbat_table, physical);

	//Copy first instruction from real function to our buffer since we will replace it soon and also safety safe it for restoration ability
	*space = *(unsigned int*)(physical);
	space++;

	restoreInstruction = *(unsigned int*)(physical);

	//Write our 6 other PPC instructions that perform a jump to realFuncAddr + 4
	/*
	stw     r3,-32(r1)
	lis     r3, realFuncAddr+4@h
	ori     r3,r3, realFuncAddr+4@l
	mtctr   r3
	lwz     r3,-32(r1)
	bctr
	*/

	*space = 0x9061FFE0; //stw r3,-32(r1)...the calling function could be using r3 for an input parameter, so safe it first
	space++;
	*space = 0x3C600000 | (((realFuncAddr + 4) >> 16) & 0x0000FFFF); //lis r3, realFuncAddr+4@h
	space++;
	*space = 0x60630000 | ((realFuncAddr + 4) & 0x0000ffff); //ori r3, r3, realFuncAddr+4@l...r3 now contains the address to the real system function + 4 (=its second instruction)
	space++;
	*space = 0x7C6903A6; //mtctr r3...move r3 to the count register
	space++;
	*space = 0x8061FFE0; //lwz r3,-32(r1)...restore input parameter we saved earlier
	space++;
	*space = 0x4E800420; //bctr...branch to count register = realFuncAddr + 4
	space++;

	unsigned int instrCount = 7;
	unsigned int flushLength = 4 * instrCount; //4 bytes per instruction

	DCFlushRange((void*)(space - instrCount), flushLength); //Immediately flush from cache to actual RAM
	ICInvalidateRange((unsigned char*)(space - instrCount), flushLength); //Invalidate the cache blocks

	//Builds a branch instruction to my_VPADRead we copy over the first instruction of the real function
	unsigned int replaceInstr = 0x48000002 | ((unsigned int)(&my_VPADRead) & 0x03fffffc);
	*(unsigned int*)(physical) = replaceInstr;

	ICInvalidateRange((void*)(realFuncAddr), 4); //Important we invalidate the used cache blocks because we restore the old DBAT table next and don't want invalid addresses sitting there

	//Restore DBAT table
	KernelRestoreDBATs(&my_dbat_table);
	
	log_print("Done with patching!");
}

After the hook we have the following setup:

  • Calls to the actual VPADRead function by the game will immediately get re-routed to my_VPADRead which receives the same input parameters. We can do whatever we want in my_VPADRead
  • If we want to call the real system function simply manipulating inputs and outputs or adding additional code we can do so by calling real_VPADRead
  • real_VPADRead points to our realFunctionBuffer. It executes the first instruction of the real function that we copied over, then jumps into the vanilla system function to its second instruction past our planted re-route instruction for calls by the game. Beyond that it works like a normal function call
  • Reverting to the original system function if we decide we no longer need the hook can be simply achieved by copying the restoreInstruction back to the first instruction of the system function to get rid of our re-route

Virtual File System

The question where to store the generated input data and savestates was one I had to solve early on. Initially I was considering storing the content entirely off console by constantly streaming it through the network, but this proved to be unwieldy and not a very good idea in case of unexpected connection failure.

So I opted to store the data on the SD card the user has inserted into his console. Since the files have to be accessible and manageable externally I had to create a semi 'virtual file system' with its own set of commands. A folder would be created on the SD card carrying the game id as its name which would be mapped as the root folder after boot.


The connected PC app is able to retrieve a file and folder list, navigate the tree structure, move, delete and copy files. Commands are packaged up and sent to a listening server socket on the console which translates them into system calls appropriate for the Wii U file system.

The following snippet running on the Wii U is a small excerpt from the command handler showing how the console reads the contents of a folder the desktop application requests and returns the found entries. The second case in the snippet is triggered when the user requests the properties of a TAS movie file which are retrieved from the file header:

//Note: These definitions normally reside in fs_defs.h
//FS defines
#define FS_STATUS_OK                    0
#define FS_RET_NO_ERROR                 0x0000
#define FS_RET_ALL_ERROR                (unsigned int)(-1)
#define FS_STAT_FLAG_IS_DIRECTORY       0x80000000
#define FS_MAX_ENTNAME_SIZE             256 //Max length of a file/folder name

//Note: This definition normally resides in tas.h
#pragma pack(push, 1)
typedef struct //File header for a WUM (Wii U Movie) file, a custom file format for uTAS
{
	u8 filetype[4];			// Unique Identifier (always "WUM"0x01) ; 0x01 is the initial WUM version
	u64 titleID;			// The Game ID

	u8 countWUMFiles;		// Count of WUM files in this recording (each file is roughly 25 MB big)
	u8 controllerID;		// Controller Type used
	u8 reserved[2];			// Reserved for future use

	u32 frameCount;			// Number of frames in the recording
	u32 framesLoadCount;		// Number of loading frames in the recording (variable)
	u32 numRerecords;		// Number of rerecords/'cuts' of this TAS
	u8 author[32];			// Author's name (encoded in ASCII)

	u64 recordingStartTime; 	// ticks since year 2000 that recording started

	u8 reserved2[60];		// Make header settings 128 bytes, reserved for new options

	u64 md5Table[240];		//120 MD5 verification entries possible; 1920 bytes ; each md5 entry has to handle at least 2 MB

} WUMHeader;
static_assert(sizeof(WUMHeader) == 2048, "WUMHeader should be 2048 bytes");
#pragma pack(pop)

#define CHECK_ERROR(cond) if (cond) { bss->line = __LINE__; goto error; }

//FS vars
static void *pClient = NULL;
static void *pCmd = NULL;

static char basePath[32];
static char titlePath[64];

static int TASMain(struct pygecko_bss_t *bss, int clientfd)
{
	int ret;
	unsigned char buffer[0x401]

	u64 titleID = OSGetTitleID();

	strcpy(basePath, "/vol/external01/wiiu/TAS");
	sprintf(titlePath, "%s/%llX", basePath, titleID);

	while (1)
	{
		OSSleepTicks(MILLISECS_TO_TICKS(11)); //90 FPS refresh

		ret = checkbyteTAS(bss, clientfd); //Attempts to read a single byte from the socket but returns if no message is in the queue
		if (ret < 0)
			continue;

		switch (ret) {

			//FS Functions
			case 0xF0: //Read Folder Content
			{
				int status = -1;
				int handle = 0;
				char finalPath[255];

				//Wait out async tasks before doing any new FS commands on the SD card
				WaitForAsyncTask();

				//Receive path length
				ret = recvwaitTAS(bss, clientfd, buffer, 1); //Blocks until message has been received fully
				CHECK_ERROR(ret < 0);

				u8 pathLength = buffer[0];
				if (pathLength > 0)
				{
					//Receive path string
					ret = recvwaitTAS(bss, clientfd, buffer, pathLength);
					CHECK_ERROR(ret < 0);

					const char* checkPath = (const char*)&buffer[0];
					sprintf(finalPath, "%s/%s", titlePath, checkPath);
				}
				else //Dump Base Title Path
				{
					strcpy(finalPath, titlePath);
				}

				log_printf("Open and read Path: %s", finalPath);

				if ((status = FSOpenDir(pClient, pCmd, finalPath, &handle, FS_RET_ALL_ERROR)) == FS_STATUS_OK)
				{
					int countFolders = 0;
					int countFiles = 0;

					int folderPos = 0;
					int filePos = 0;

					char fileBuffer[8192];
					char folderBuffer[8192];

					FSDirEntry dir_entry;
					while (FSReadDir(pClient, pCmd, handle, &dir_entry, FS_RET_ALL_ERROR) == FS_STATUS_OK)
					{
						if ((dir_entry.stat.flag & FS_STAT_FLAG_IS_DIRECTORY) == FS_STAT_FLAG_IS_DIRECTORY) //Folder
						{
							if (!strcmp(dir_entry.name, "_temp")) //Exclude temp movie folder (hidden)
								continue;

							u8 length = strlen(dir_entry.name) + 1;

							folderBuffer[folderPos] = length;
							folderPos += 1;

							memcpy(&folderBuffer[folderPos], dir_entry.name, length);
							folderPos += length;

							countFolders++;
						}
						else //Files
						{
							if (strstr(dir_entry.name, ".wum.") == 0 && strstr(dir_entry.name, ".sav.") == 0) //Only grab main movie and state files (exclude .1, .2 chunk movie files too)
							{
								u8 length = strlen(dir_entry.name) + 1;

								fileBuffer[filePos] = length;
								filePos += 1;

								memcpy(&fileBuffer[filePos], dir_entry.name, length);
								filePos += length;

								countFiles++;
							}
						}
					}

					if ((status = FSCloseDir(pClient, pCmd, handle, FS_RET_NO_ERROR)) < FS_STATUS_OK)
					{
						log_printf("Error while closing dir");

						ret = sendbyteTAS(bss, clientfd, (u8)status); //Send error code to connected PC app
						CHECK_ERROR(ret < 0);
					}
					else
					{
						//Send status OK
						ret = sendbyteTAS(bss, clientfd, 0);
						CHECK_ERROR(ret < 0);

						//Send folder count
						ret = sendbyteTAS(bss, clientfd, (u8)countFolders);
						CHECK_ERROR(ret < 0);

						//Stream directory list
						if (countFolders > 0)
						{
							folderPos = 0;
							for (int n = 0; n < countFolders; n++)
							{
								//Len of directory name
								u8 length = folderBuffer[folderPos];
								folderPos += 1;

								ret = sendbyteTAS(bss, clientfd, length);
								CHECK_ERROR(ret < 0);

								//Directory name
								memcpy(&buffer[0], &folderBuffer[folderPos], length);

								ret = sendwaitTAS(bss, clientfd, buffer, length);
								CHECK_ERROR(ret < 0);

								folderPos += length;
							}
						}

						//Send file count
						ret = sendbyteTAS(bss, clientfd, (u8)countFiles);
						CHECK_ERROR(ret < 0);

						//Stream file list
						if (countFiles > 0)
						{
							filePos = 0;
							for (int n = 0; n < countFiles; n++)
							{
								u8 length = fileBuffer[filePos];
								filePos += 1;

								ret = sendbyteTAS(bss, clientfd, length);
								CHECK_ERROR(ret < 0);

								memcpy(&buffer[0], &fileBuffer[filePos], length);

								ret = sendwaitTAS(bss, clientfd, buffer, length);
								CHECK_ERROR(ret < 0);

								filePos += length;
							}
						}
					}
				}
				else
				{
					log_printf("Error opening dir!");

					ret = sendbyteTAS(bss, clientfd, (u8)status);
					CHECK_ERROR(ret < 0);
				}

				break;
			}	
			case 0xF4: //Get WUM Header Info
			{
				int status = -1;
				int handle = 0;
				char finalPath[255];

				WaitForAsyncTask();

				//Receive file path length
				ret = recvwaitTAS(bss, clientfd, buffer, 1);
				CHECK_ERROR(ret < 0);

				u8 pathLength = buffer[0];
				if (pathLength > 0)
				{
					//Receive .wum file path string
					ret = recvwaitTAS(bss, clientfd, buffer, pathLength);
					CHECK_ERROR(ret < 0);

					const char* checkPath = (const char*)&buffer[0];
					sprintf(finalPath, "%s/%s", titlePath, checkPath);

					//Load Movie file (.wum)
					if ((status = FSOpenFile(pClient, pCmd, finalPath, "r", &handle, -1)) == FS_STATUS_OK)
					{
						//Read WUM Header
						WUMHeader* header = (WUMHeader*)memalign(64, sizeof(WUMHeader));
						memset(header, 0, sizeof(WUMHeader));

						status = -1;

						if ((status = FSReadFile(pClient, pCmd, header, sizeof(WUMHeader), 1, handle, 0, -1)) >= FS_STATUS_OK)
						{
							status = 0; //OK

							//Status
							ret = sendbyteTAS(bss, clientfd, (u8)status);
							CHECK_ERROR(ret < 0);

							//Title ID
							ret = sendwaitTAS(bss, clientfd, &header->titleID, 8);
							CHECK_ERROR(ret < 0);

							//Controller ID
							ret = sendwaitTAS(bss, clientfd, &header->controllerID, 1);
							CHECK_ERROR(ret < 0);

							//Frame Count
							ret = sendwaitTAS(bss, clientfd, &header->frameCount, 4);
							CHECK_ERROR(ret < 0);

							//Load Count
							ret = sendwaitTAS(bss, clientfd, &header->framesLoadCount, 4);
							CHECK_ERROR(ret < 0);

							//Re-record Count
							ret = sendwaitTAS(bss, clientfd, &header->numRerecords, 4);
							CHECK_ERROR(ret < 0);

							//Author Name
							ret = sendwaitTAS(bss, clientfd, &header->author[0], 32);
							CHECK_ERROR(ret < 0);

							//Recording Start Time
							ret = sendwaitTAS(bss, clientfd, &header->recordingStartTime, 8);
							CHECK_ERROR(ret < 0);

							free(header);
						}

						FSCloseFile(pClient, pCmd, handle, -1);
					}
				}

				if (status != 0) //Send error code as final message if operation failed
				{
					ret = sendbyteTAS(bss, clientfd, (u8)status);
					CHECK_ERROR(ret < 0);
				}

				break;
			}
			default:
				log_printf("Ret is: %i ; N/A!", ret);
				CHECK_ERROR(ret == 0); //Connection was terminated normally
				break;
		}
	}
}


Responses are passed back to the PC application which interprets them and performs updates on its visual presentation of the files and folders. The view is specifically geared towards TASing and will only show files with extensions that indicate them clearly belonging to uTAS.

The PC App

Inactive uTAS PC App


Active App after connecting to Wii U


The control panel of the entire operation. Maintains several threads and socket based connections to the Wii U to, for one, retrieve memory values for the Data Display which can be easily manipulated in real time by editing a config file:

//maximum of 20 entries allowed
//allowed value types: byte, short, int, float, stringX (X indicating number of chars to read)
//allowed display types: dec, hex

Name="Speed:"
Address=0x10681E40 + 0x5C
ValueType=float

Name="Facing:"
Address=0x10681E40 + 0x16
ValueType=short
DisplayAs=dec

Name="Link X:"
Address=0x10681E40 + 0x0
ValueType=float

Name="Stage:"
Address=0x1064CDE8
ValueType=string8

Name="Room ID:"
Address=0x106813D4
ValueType=byte
DisplayAs=dec

Name="Spawn ID:"
Address=0x1064CDF1
ValueType=byte
DisplayAs=hex

Name="State:"
Address=0x1062915B
ValueType=string1


This C# snippet from the PC app handles transferring the watch list (read out from the config file) to the Wii U so it can start 'watching' the desired memory addresses:

using System;
using System.Net.Sockets;
using System.Threading;

struct watchListEntry
{
    public bool usePtr;
    public UInt32 address;
    public UInt32 offset;
    public byte type;
    public byte dataSize;
    public byte viewMode;
};

private void dataThread(object client_obj)
{
    NetworkStream dataStream = (NetworkStream)client_obj;
    EndianBinaryReader reader = new EndianBinaryReader(dataStream);
    EndianBinaryWriter writer = new EndianBinaryWriter(dataStream);

    List<watchListEntry> watchList = new List<watchListEntry>();

    /*
    .....
    */

    //Send Watch List over to Wii U
    if (watchList.Count > 0)
    {
        //Calculate total byte count
        int totalBytes = 0;

        foreach (watchListEntry entry in watchList)
        {
            totalBytes += 1; //(bool) is ptr?
            totalBytes += 4; //(uint) normal address or ptr

            if (entry.usePtr)
                totalBytes += 4; //(uint) offset

            totalBytes += 1; //(byte) dataSize
        }

        Byte[] buffer = new Byte[totalBytes];

        //Write Watch List into buffer
        UInt32 index = 0;
        foreach (watchListEntry entry in watchList)
        {
            if (entry.usePtr)
            {
                //is Ptr = true
                buffer[index] = 1;
                index += 1;

                //Ptr Address
                Buffer.BlockCopy(BitConverter.GetBytes(ByteSwap.Swap(entry.address)), 0, buffer, index, 4); //Swap bytes since Wii U uses big endian
                index += 4;

                //Offset
                Buffer.BlockCopy(BitConverter.GetBytes(ByteSwap.Swap(entry.offset)), 0, buffer, index, 4);
                index += 4;
            }
            else
            {
                //is Ptr = false
                buffer[index] = 0;
                index += 1;

                //Normal Address
                Buffer.BlockCopy(BitConverter.GetBytes(ByteSwap.Swap(entry.address)), 0, buffer, index, 4);
                index += 4;
            }

            //Data Size
            buffer[index] = entry.dataSize;
            index += 1;
        }

        //Send total byte count to Wii U first, then the Watch List
        writer.Write(totalBytes);
        writer.Write(buffer, 0, totalBytes);
    }
    else
    {
        int noData = 0;
        writer.Write(noData); //Send empty message
    }
}


Furthermore the desktop application manages updates to its virtual file system representation and pushes input data and user commands to the console. On top of that it can run Lua scripts on the side and react Windows wide to the use of hotkeys if desired.

Lua Support

uTAS ships with a robust Lua integration that allows to invoke all the essential functionality from a script environment which can be very useful in creating input templates for example.


The available functions:

private void RegisterLuaFunctions()
{
    //Basic Functions
    System.Reflection.MethodBase cancelScript = typeof(LuaHandler).GetMethod("LUA_cancelScript");
    state.RegisterFunction("CancelScript", cancelScript);

    System.Reflection.MethodBase msgBox = typeof(LuaHandler).GetMethod("LUA_msgBox");
    state.RegisterFunction("MsgBox", msgBox);

    System.Reflection.MethodBase frameAdvance = typeof(LuaHandler).GetMethod("LUA_frameAdvance");
    state.RegisterFunction("FrameAdvance", frameAdvance);

    System.Reflection.MethodBase getFrameCount = typeof(LuaHandler).GetMethod("LUA_getFrameCount");
    state.RegisterFunction("GetFrameCount", getFrameCount);

    //Memory Read
    System.Reflection.MethodBase read8 = typeof(LuaHandler).GetMethod("LUA_read8");
    state.RegisterFunction("ReadValue8", read8);

    System.Reflection.MethodBase read16 = typeof(LuaHandler).GetMethod("LUA_read16");
    state.RegisterFunction("ReadValue16", read16);

    System.Reflection.MethodBase read32 = typeof(LuaHandler).GetMethod("LUA_read32");
    state.RegisterFunction("ReadValue32", read32);

    System.Reflection.MethodBase readFloat = typeof(LuaHandler).GetMethod("LUA_readFloat");
    state.RegisterFunction("ReadValueFloat", readFloat);

    System.Reflection.MethodBase readString = typeof(LuaHandler).GetMethod("LUA_readString");
    state.RegisterFunction("ReadValueString", readString);

    //Memory Write
    System.Reflection.MethodBase write8 = typeof(LuaHandler).GetMethod("LUA_write8");
    state.RegisterFunction("WriteValue8", write8);

    System.Reflection.MethodBase write16 = typeof(LuaHandler).GetMethod("LUA_write16");
    state.RegisterFunction("WriteValue16", write16);

    System.Reflection.MethodBase write32 = typeof(LuaHandler).GetMethod("LUA_write32");
    state.RegisterFunction("WriteValue32", write32);

    System.Reflection.MethodBase writeFloat = typeof(LuaHandler).GetMethod("LUA_writeFloat");
    state.RegisterFunction("WriteValueFloat", writeFloat);

    System.Reflection.MethodBase writeString = typeof(LuaHandler).GetMethod("LUA_writeString");
    state.RegisterFunction("WriteValueString", writeString);

    //Input Manipulation
    System.Reflection.MethodBase pressButton = typeof(LuaHandler).GetMethod("LUA_pressButton");
    state.RegisterFunction("PressButton", pressButton);

    System.Reflection.MethodBase releaseButton = typeof(LuaHandler).GetMethod("LUA_releaseButton");
    state.RegisterFunction("ReleaseButton", releaseButton);

    System.Reflection.MethodBase pressMainX = typeof(LuaHandler).GetMethod("LUA_setMainStickX");
    state.RegisterFunction("SetMainStickX", pressMainX);

    System.Reflection.MethodBase pressMainY = typeof(LuaHandler).GetMethod("LUA_setMainStickY");
    state.RegisterFunction("SetMainStickY", pressMainY);

    System.Reflection.MethodBase pressCX = typeof(LuaHandler).GetMethod("LUA_setCStickX");
    state.RegisterFunction("SetCStickX", pressCX);

    System.Reflection.MethodBase pressCY = typeof(LuaHandler).GetMethod("LUA_setCStickY");
    state.RegisterFunction("SetCStickY", pressCY);

    //Savestate
    System.Reflection.MethodBase saveState = typeof(LuaHandler).GetMethod("LUA_saveState");
    state.RegisterFunction("SaveState", saveState);

    System.Reflection.MethodBase loadState = typeof(LuaHandler).GetMethod("LUA_loadState");
    state.RegisterFunction("LoadState", loadState);
}


HelloWorld.lua

startCounter = 0

function onScriptStart() --called on script launch

	startCounter = GetFrameCount()	

	FrameAdvance() --advances a frame

end

function onScriptCancel()

	MsgBox("I will sleep now")

end

function onScriptUpdate() --called every frame

	mainFunc()
	
end


function onLoadStateSuccess()

end

function onLoadStateFail()

end


function round(num, idp)

  local mult = 10^(idp or 0)
  return math.floor(num * mult + 0.5) / mult

end

function mainFunc()
	
  local counter = GetFrameCount()

  if counter >= startCounter + 6 then
	
  	local secs = counter / 30
	secs = round(secs, 1)

	MsgBox(string.format("Hello World! We are %.1f seconds into the TAS!", secs))

	CancelScript() --ends the script
  end
	
  FrameAdvance()

end

This example Lua file ships with the uTAS v1.0 release.

Security: Serial Number Verification

I knew that with uTAS the door could be wide opened to potential abuse for example by passing off a TAS playback as a real run performed by a human. Since it would occur on a legit console in one segment it would be almost impossible to prove whether or not someone cheated through analysing the video and audio footage alone.

Therefore I decided to do a limited release at the start and release my tool to trustworthy people in the community only. To enforce it came up with a serial verification system.

This snippet is responsible for verifying the unique console id which I use as the serial against a list of allowed ids:

//Lib Curl
static u32 curlDataPos = 0;

//Curl callback function writing received data into a destination buffer
static size_t curlWriteBuffer(void *buffer, size_t size, size_t count, char *receiveStream)
{
	int totalSize = size * count;
	memcpy(receiveStream + curlDataPos, buffer, totalSize);

	curlDataPos += totalSize;
	return totalSize;
}

static int VerifyConsole()
{
	int outputStatus = -1;

	//Init Curl Libs
	InitCurlFunctionPointers();
	if (NSSLInit() != 0)
	{
		log_printf("No SSL available!");
		return 0;
	}

	int sslContext = NSSLCreateContext();
	int pkiVal1 = 0;
	int pkiVal2 = 0;

        //Add PKI Group Certificates
	if (NSSLAddServerPKIGroups(sslContext, 3, &pkiVal1, &pkiVal2) != 0)
	{
		log_printf("No Group Certificates available!");

		NSSLDestroyContext(sslContext);
		NSSLFinish();
		return 0;
	}

	//Init Curl
	n_curl_global_init(CURL_GLOBAL_ALL);
	CURL* curl = n_curl_easy_init();
	if (!curl)
	{
		log_printf("Curl handle could not be created!");

		NSSLDestroyContext(sslContext);
		NSSLFinish();
		return 0;
	}

	char* keyBuffer = (char*)malloc(10000); //10 KB should be plenty
	memset(keyBuffer, 0, 10000);

	//Set SSL Context and PKI Group Count
	n_curl_easy_setopt(curl, 210, sslContext);
	n_curl_easy_setopt(curl, 211, 3);

	//Set Options
	n_curl_easy_setopt(curl, CURLoption::CURLOPT_URL, "https://pastebin.com/raw/XXXX"); //Try unlisted Pastebin verification file first
	n_curl_easy_setopt(curl, CURLoption::CURLOPT_HEADER, false);
	n_curl_easy_setopt(curl, CURLoption::CURLOPT_FOLLOWLOCATION, false);
	n_curl_easy_setopt(curl, CURLoption::CURLOPT_ENCODING, "");
	n_curl_easy_setopt(curl, CURLoption::CURLOPT_USERAGENT, "uTAS");
	n_curl_easy_setopt(curl, CURLoption::CURLOPT_AUTOREFERER, false);
	n_curl_easy_setopt(curl, CURLoption::CURLOPT_CONNECTTIMEOUT, 5);
	n_curl_easy_setopt(curl, CURLoption::CURLOPT_TIMEOUT, 10);
	n_curl_easy_setopt(curl, CURLoption::CURLOPT_MAXREDIRS, 3);
	n_curl_easy_setopt(curl, CURLoption::CURLOPT_WRITEFUNCTION, curlWriteBuffer);
	n_curl_easy_setopt(curl, CURLoption::CURLOPT_WRITEDATA, keyBuffer);

	log_printf("Perform Curl");

	curlDataPos = 0;
	CURLcode response = n_curl_easy_perform(curl);
	if (response == 0)
	{
		//log_printf("Received total data from pastebin: %i", curlDataPos);

		if (!strstr(keyBuffer, "Authentication List uTAS:"))
		{
			curlDataPos = 0;
		}
		else //List is correct
		{
			outputStatus = 1;
		}
	}
	else
	{
		log_printf("Curl returned error: %i", response);
		curlDataPos = 0;
	}

	if (curlDataPos == 0)
	{
		//Try Dropbox as fallback if pastebin is down/displays heavy load site
		n_curl_easy_setopt(curl, CURLoption::CURLOPT_URL, "https://dl.dropboxusercontent.com/s/XXXXXX");

		memset(keyBuffer, 0, 10000);
		response = n_curl_easy_perform(curl);
		if (response == 0)
		{
			//log_printf("Received total data from dropbox: %i", curlDataPos);

			if (!strstr(keyBuffer, "Authentication List uTAS:"))
			{
				log_printf("Both pastebin and dropbox are invalid lists, critical!");
				outputStatus = 0;
			}
			else //List is correct
			{
				outputStatus = 1;
			}
		}
		else
		{
			log_printf("Both pastebin and dropbox returned errors, critical!");
			outputStatus = 0;
		}
	}

	if (outputStatus == 1) //Key List received, compare with console ID
	{
		outputStatus = -1;

		char* consoleKey = (char*)malloc(36);
		memset(consoleKey, 0, 36);

		sprintf(consoleKey, "%08X-%08X-%08X-%08X", g_consoleID[0], g_consoleID[1], g_consoleID[2], g_consoleID[3]);

		//log_printf("Console key: %s", consoleKey);

		const char delimiters[] = "\r\n";
		char* running = keyBuffer;
		char* token;

		token = strsep(&running, delimiters);
		while (token != NULL)
		{
			if (strlen(token) > 34)
			{
				if (!strcmp(token, consoleKey))
				{
					//log_printf("match found with %s!!", token);

					outputStatus = 1;
					break;
				}
			}
			token = strsep(&running, delimiters);
		}
		free(consoleKey);
	}

	//Cleanup
	free(keyBuffer);
	n_curl_easy_cleanup(curl);

	NSSLDestroyContext(sslContext);
	NSSLFinish();

	return outputStatus;
}

static int start_TASThread(int argc, void *argv)
{
	int sockfd = -1, clientfd = -1, ret = 0, len;
	int optval = 1;
	int handshakeAttempts = 0;

	struct sockaddr_in addr;
	struct pygecko_bss_t *bss = (pygecko_bss_t*)argv;

	int status = -1;
	log_printf("TAS Thread Started!");

	while (1)
	{
		addr.sin_family = AF_INET;
		addr.sin_port = 7340; //TAS Tool uses port 7340
		addr.sin_addr.s_addr = 0; //Use Wii U IP

		sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //Open a handle to socket
		CHECK_ERROR(sockfd == -1);

		setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof optval); //Ensures we can bind the port @Wii U IP in every case even when a connection just died

		ret = bind(sockfd, (sockaddr*)&addr, 16);
		CHECK_ERROR(ret < 0);

		ret = listen(sockfd, 20); //Listens for incoming connections from the TAS Tool
		CHECK_ERROR(ret < 0);

		log_printf("Ready for a connection to the TAS Tool...");

		len = 16;
		clientfd = accept(sockfd, (sockaddr*)&addr, &len); //Block the thread until a client connects
		CHECK_ERROR(clientfd == -1);

		log_printf("TAS Tool connected!");

		handshakeAttempts = 0;
		while (handshakeAttempts < 5000) //Wait 5 seconds for the handshake signal, else abort
		{
			ret = checkbyteTAS(bss, clientfd);
			if (ret == 0xFF) //0xFF is the handshake signal
				break;

			OSSleepTicks(MILLISECS_TO_TICKS(1));
			handshakeAttempts++;
		}

		if (handshakeAttempts >= 5000)
			goto error;

		status = VerifyConsole();
		if (status == -1) //Console failed verification, send consoleID to TAS Tool to prompt the user to verify himself
		{
			log_printf("Verification failed, ask user to authenticate");

			ret = sendbyteTAS(bss, clientfd, 0);
			CHECK_ERROR(ret < 0);

			ret = sendwaitTAS(bss, clientfd, &g_consoleID[0], 4);
			CHECK_ERROR(ret < 0);

			ret = sendwaitTAS(bss, clientfd, &g_consoleID[1], 4);
			CHECK_ERROR(ret < 0);

			ret = sendwaitTAS(bss, clientfd, &g_consoleID[2], 4);
			CHECK_ERROR(ret < 0);

			ret = sendwaitTAS(bss, clientfd, &g_consoleID[3], 4);
			goto error;
		}
		else if (status == 0) //Servers are not reachable
		{
			log_printf("Internet/Server error, inform user");

			ret = sendbyteTAS(bss, clientfd, 1);
			goto error;
		}
		else if (status == 1) //Console passed verification
		{
			log_printf("Console passed verification, unlock TAS Tool");

			ret = sendbyteTAS(bss, clientfd, 2);
			CHECK_ERROR(ret < 0);
		}

		TASToolConnected = 1;

		ret = TASMain(bss, clientfd); //Run main loop of TAS thread
		log_printf("TAS connection has closed!");

		TASToolConnected = 0;

		if (clientfd != -1)
			socketclose(clientfd);
		if (sockfd != -1)
			socketclose(sockfd);

		bss->error = ret;
		clientfd = -1;

		continue;

	error:

		if (clientfd != -1)
			socketclose(clientfd);
		if (sockfd != -1)
			socketclose(sockfd);
		bss->error = ret;

		clientfd = -1;
		sockfd = -1;
	}
	return 0;
}


The unique console id is read out from the kernel by uTAS on boot, hashed and finally verified with a pastebin/dropbox document to confirm if the id has been granted access. In the case it hasn't the hashed id is presented to the user with a warning and to move on the id needs to be submitted and activated.