mgba icon indicating copy to clipboard operation
mgba copied to clipboard

CIA Forwarder for individual roms using mGBA rather than VC Injection

Open seraphtc101 opened this issue 8 years ago • 28 comments

POSTED HERE AT ENDRIFTS REQUEST:

I've been looking into the benefits of VC injection for GBA backups rather than using emulators. Obviously it works better for some games than others (for example, SMA4 or the Pokemon titles requiring a lot of messing around with hex patches etc, and even then not functioning properly). I have an N3DS, and the framerate in mGBA is just fine (as is sound in the latest nightly), so the only real benefit from injection is having shortcuts directly to games on my home screen - which is the only real feature that I care about.

Is there anyway to create a .cia file that could call mGBA to start and stop with a specific rom? (ie, the cia is executed and starts the cia version of mGBA with a call that loads a specific ROM, and then when the emulation is stopped mGBA auto closes).

Alternatively, how tricky would it be to build a cut down 'wrapper' version of mGBA that is either hardcoded (or uses a config entry) to start a ROM at launch, and then close when the emulation finishes? I appreciate that this would require multiple mGBA installs (one for each game) but mGBA is pretty small, and I have a 128GB SDXC with plenty of room to spare.

I'm looking for ways to get those last few tricky games onto my home screen without using injection, as mGBA seems to provide much better compatibility in some cases (and more options are good!).

I'm not necessarily looking for anyone to do this for me - I'd just really appreciate some pointers. I have done some limited development in the past (for personal use rather than professionally) so I may be able to have a crack at this if I know where to start.

(On the other hand, if anyone more experienced wanted to weigh in with the code I'd be happy to test).

Original mGBA Forum Link: https://forums.mgba.io/showthread.php?tid=2878

seraphtc101 avatar Apr 27 '17 17:04 seraphtc101

Hello, just want to ask if this feature is still on the list? Thanks...

noctis90210 avatar Dec 13 '18 00:12 noctis90210

It's still planned, yes.

endrift avatar Dec 13 '18 00:12 endrift

Oh thanks. hope it will be implemented soon.

Even theres a work around regarding 128kb save files on some games, specially pokemon. (If youre familiar with the NSUI app by Asdolo ni GBAtemp) Still... Save transfer/import/export on this GBA VC workaround is still pain in the ass... One wrong move and save will be gone.

Another one, just a suggestion as opposed to the OP of this issue, he says its ok if in every cia contains the engine of the mGBA, but i think its better if the cia contains only the banner ofcourse and also file redirection to a specific rom and mGBA engine. Like the one on nds-bootstrap and nds forwarder (if you're familiar on those two)... The nds forwarder (cia) contains redirection to where a certain rom is located on the sd card and also the nds-bootstrap engine. Just a suggestion, but still its in your hands to whats the possibe and easy way...

Anyway, thanks and have a nice day...

noctis90210 avatar Dec 13 '18 18:12 noctis90210

Yup, that's what the current (incomplete) implementation does.

endrift avatar Dec 13 '18 18:12 endrift

Thanks for 0.7 Cant wait to see this feature on 0.8

noctis90210 avatar Feb 03 '19 01:02 noctis90210

@endrift hello, just want to know if mGBA forwarder for 3DS is still possible in future? :-) thanks for the new 0.7.3

noctis90210 avatar Sep 25 '19 22:09 noctis90210

Has this been implimented.

yergerb avatar Jan 05 '21 08:01 yergerb

Hello, I am really thankful for this project and I was also hoping for an implementation, but I guess there is currently not enough interest for this. I really would like to have a CIA version for some games like Yoshi's Universal Gravitation (only mGBA supports the gyro-sensor) So I tried some small changes myself. I changed the GUI loading code so the path to a ROM file is directly loaded when starting mGBA and it works perfectly so far. I also changed the CMake file to use other banner and icon files and to randomize the title id.

I created a fork, so if someone is interested in the changes can try them on their own: https://github.com/TobiasBielefeld/mgba You need to put your banner/icon files and the path to your ROM in the folder "res/3ds_custom_data/" which I created. Then build mGBA using e.g. Docker and the cia will be created in "build-3ds/install/usr/local/cia/". I used the New Ultimate Injector by Asdolo (https://gbatemp.net/threads/discussion-new-super-ultimate-injector-nsui.500376/) to create the banner and icon files (you can export them in the project tab, make sure to use the .bnr and .icn formats).

My approach is not the best one, since every CIA contains a copy of mGBA and the creation is not really user friendly. However it works until there is an official way :)

TobiasBielefeld avatar Feb 06 '21 17:02 TobiasBielefeld

The biggest issue that's been holding me up with this is actually title IDs--there isn't really a great way to generate per-game title IDs without keeping track of which games are already installed or having a chance of colliding with other titles, as far as I know. I might be able to create a custom title ID namespace, but I kind of have doubts that would work, and might get Nintendo very upset if they notice them installed.

endrift avatar Feb 06 '21 22:02 endrift

It works perfectly so far.

Nice to see thats possible now, thanks for making it possible. @TobiasBielefeld

I created a fork, so if someone is interested in the changes can try them on their own: https://github.com/TobiasBielefeld/mgba

I think it really needs its own fork cuz the implementation on how it works is a bit different (cia forwarder) compared to the actual emulator.

You need to put your banner/icon files and the path to your ROM in the folder "res/3ds_custom_data/" which I created. Then build mGBA using e.g. Docker and the cia will be created in "build-3ds/install/usr/local/cia/".

Hope you can build a GUI for it since not all people are familiar compiling github projects. Hoping a collaboration between @TobiasBielefeld and @endrift so that when theres mGBA update, the CIA forwarder fork will also updated.

My approach is not the best one, since every CIA contains a copy of mGBA

I think thats fine as long as it works

The biggest issue that's been holding me up with this is actually title IDs--there isn't really a great way to generate per-game title IDs without keeping track of which games are already installed or having a chance of colliding with other titles.

I think its the users responsibility to what ID should he/she choose... at the same time we can also put a warning to avoid colliding title IDs thats why i think its not that big deal. Anyway thanks @endrift for continous development of this project.

noctis90210 avatar Feb 07 '21 00:02 noctis90210

Yea, I installed only like 5 titles using my method and already got the message "This software needs updates to start" twice, because the ID collided... I also thought about making a GUI, but creating the correct icon and banner on my own is also not that trivial. I may take a look at other CIA creators and how they handle the problems in the near future :)

TobiasBielefeld avatar Feb 07 '21 09:02 TobiasBielefeld

This would be a very welcome feature. I'm not sure if it is applicable in this case in terms of the crashing TitleIDs, but for the PSVita, using AdrBubbleBooter to create PSP shortcuts through the PSP emulator Adrenaline, it allows us to generate TitleIDs based on the PSP game's ID (we can choose between this and it being on the format PSPEMUxxxx, xxxx being the number of games installed). I am not sure if such IDs exist for GB/C/A games though. I also have no clue if there are any requirements on the 3DS side for proper TitleIDs, and if this would even work

Snomannen-kalle avatar May 22 '21 12:05 Snomannen-kalle

has it been released??

EpicXboss250 avatar Feb 25 '22 14:02 EpicXboss250

I'm excited for this, it would be a lot easier to manage saves this way than it would be when you compile .gba to .cia

teddblue avatar Mar 22 '22 18:03 teddblue

The biggest issue that's been holding me up with this is actually title IDs--there isn't really a great way to generate per-game title IDs without keeping track of which games are already installed or having a chance of colliding with other titles, as far as I know. I might be able to create a custom title ID namespace, but I kind of have doubts that would work, and might get Nintendo very upset if they notice them installed.

I'm really sorry if I have no idea what I'm talking about, but since nds forwarders can use this method, there is probably a way around title id's. I'll look into how nds forwarders do it just for my own curiosity, but if I find something useful I will let you know

teddblue avatar Mar 31 '22 01:03 teddblue

No news about this noe? TT_TT

PauloDaniel1993 avatar May 22 '22 14:05 PauloDaniel1993

NSUI (New Super Ultimate Injector for 3DS) is able to create mGBA forwarders. However I think it uses an older release or fork of mGBA (by retroarch). It's out of date and compatibility isn't the best. image

Note: you can only create mGBA forwarders after a gba bios has been set under Tools -> Options -> Bios

NSUI is written in C#, I've attached a debugger and had a look at how these *.cia forwarders are being generated. In order to generate the .cia NSUI will create the following temporary working directory: image here's a zip of this working directory: working_directory.zip

Maybe it's possible to update or replace the template .cia or base.cia that's being used to initialize the working directory. Though I'm not very familliar with 3DS homebrew.

Finally, here's the code used by NSUI to create the .cia file.

ExportCIA()
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Media;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
using DamienG.Security.Cryptography;
using New_Super_Ultimate_Injector_for_3DS.CTR_Graphics;
using New_Super_Ultimate_Injector_for_3DS.Properties;

namespace New_Super_Ultimate_Injector_for_3DS.GBA
{
	// Token: 0x02000176 RID: 374
	public class frmGBAInject : frmBaseProject
	{
		public static PlatformCommonUtils.ExportCIAResult ExportCIA(GBAProjectProperties projectProperties, string pathCIA)
		{
			PlatformCommonUtils.ExportCIAResult result;
			try
			{
				FileInfo fileInfo = new FileInfo(pathCIA);
				string workPath = Utils.PrepareAndGetTempFolder();
				DirectoryInfo workPathDirectoryInfo = new DirectoryInfo(workPath);
				GBAInjectionMethod injectionMethod = projectProperties.InjectionMethod;
				bool autoSaveOnExit;
				bool disableSaveDataBackups;
				if (injectionMethod != GBAInjectionMethod.METHOD_VIRTUAL_CONSOLE)
				{
					if (injectionMethod - GBAInjectionMethod.METHOD_RETROARCH_MGBA_FORWARDER > 1)
					{
						throw new Exception("Not implemented.");
					}
					autoSaveOnExit = projectProperties.ContentOptions.AutomaticSaveAndLoad;
					disableSaveDataBackups = true;
				}
				else
				{
					autoSaveOnExit = false;
					disableSaveDataBackups = true;
				}
				byte[] iconBinary;
				if (!projectProperties.UsingImportedIconBinary)
				{
					iconBinary = PlatformCommonUtils.CreateIconBinary(projectProperties.CIALongName, projectProperties.CIAShortName, projectProperties.CIAPublisher, PlatformCommonUtils.CreateIcon24FromBitmap(projectProperties.IconInnerBitmap, projectProperties.IconFrameStyle, projectProperties.FitIconByHeight, projectProperties.Platform), PlatformCommonUtils.CreateIcon48FromBitmap(projectProperties.IconInnerBitmap, projectProperties.IconFrameStyle, projectProperties.FitIconByHeight, projectProperties.Platform), autoSaveOnExit, !projectProperties.HideFromActivityLog, disableSaveDataBackups, false);
				}
				else
				{
					iconBinary = PlatformCommonUtils.ApplyFlags(projectProperties.ImportedIconBinary, autoSaveOnExit, !projectProperties.HideFromActivityLog, disableSaveDataBackups, false);
				}
				byte[] bannerBinary;
				if (!projectProperties.UsingImportedBannerBinary)
				{
					bannerBinary = frmGBAInject.CreateBannerBinary(projectProperties);
				}
				else
				{
					bannerBinary = projectProperties.ImportedBannerBinary;
				}
				if (Utils.GetPlatform() == Utils.Platform.X64)
				{
					File.WriteAllBytes(workPath + "\\ctrtool.exe", Resources.tool_ctrtool);
					File.WriteAllBytes(workPath + "\\makerom.exe", Resources.tool_makerom);
				}
				else
				{
					File.WriteAllBytes(workPath + "\\ctrtool.exe", Resources.tool_ctrtool32);
					File.WriteAllBytes(workPath + "\\makerom.exe", Resources.tool_makerom32);
				}
				File.WriteAllBytes(workPath + "\\3dstool.exe", Resources.tool_3dstool);
				switch (injectionMethod)
				{
				case GBAInjectionMethod.METHOD_VIRTUAL_CONSOLE:
					File.WriteAllBytes(workPath + "\\base.cia", Resources.base_cia_gba_vc);
					break;
				case GBAInjectionMethod.METHOD_RETROARCH_MGBA_FORWARDER:
					File.WriteAllBytes(workPath + "\\base.cia", Resources.base_cia_gba_mgba);
					break;
				case GBAInjectionMethod.METHOD_RETROARCH_GPSP_FORWARDER:
					File.WriteAllBytes(workPath + "\\base.cia", Resources.base_cia_gba_gpsp);
					break;
				}
				Directory.CreateDirectory(workPath + "\\extracted");
				Process process = Process.Start(new ProcessStartInfo(workPath + "\\ctrtool.exe")
				{
					CreateNoWindow = true,
					WorkingDirectory = workPath,
					UseShellExecute = false,
					RedirectStandardError = true,
					Arguments = "--content=DecryptedApp \"base.cia\""
				});
				process.WaitForExit();
				string error = process.StandardError.ReadToEnd();
				int ExitCode = process.ExitCode;
				process.Close();
				File.Move(workPathDirectoryInfo.GetFiles("DecryptedApp.0000.*")[0].FullName, workPath + "\\extracted\\DecryptedPartition0.bin");
				process = Process.Start(new ProcessStartInfo(workPath + "\\3dstool.exe")
				{
					CreateNoWindow = true,
					WorkingDirectory = workPath + "\\extracted",
					UseShellExecute = false,
					RedirectStandardError = true,
					Arguments = "-xtf cxi DecryptedPartition0.bin --header HeaderNCCH0.bin --exh DecryptedExHeader.bin --exefs DecryptedExeFS.bin --romfs DecryptedRomFS.bin --logo LogoLZ.bin --plain PlainRGN.bin"
				});
				process.WaitForExit();
				error = process.StandardError.ReadToEnd();
				ExitCode = process.ExitCode;
				process.Close();
				File.Delete(workPath + "\\base.cia");
				File.Delete(workPath + "\\extracted\\DecryptedPartition0.bin");
				process = Process.Start(new ProcessStartInfo(workPath + "\\3dstool.exe")
				{
					CreateNoWindow = true,
					WorkingDirectory = workPath + "\\extracted",
					UseShellExecute = false,
					RedirectStandardError = true,
					Arguments = "-xutf exefs DecryptedExeFS.bin --exefs-dir ExtractedExeFS --header HeaderExeFS.bin"
				});
				process.WaitForExit();
				error = process.StandardError.ReadToEnd();
				ExitCode = process.ExitCode;
				process.Close();
				process = Process.Start(new ProcessStartInfo(workPath + "\\3dstool.exe")
				{
					CreateNoWindow = true,
					WorkingDirectory = workPath + "\\extracted",
					UseShellExecute = false,
					RedirectStandardError = true,
					Arguments = "-xtf romfs DecryptedRomFS.bin --romfs-dir ExtractedRomFS"
				});
				process.WaitForExit();
				error = process.StandardError.ReadToEnd();
				ExitCode = process.ExitCode;
				process.Close();
				byte[] romBinary = new byte[projectProperties.ROMFileRaw.Length];
				Array.Copy(projectProperties.ROMFileRaw, romBinary, projectProperties.ROMFileRaw.Length);
				if (projectProperties.UsingIPSPatch)
				{
					foreach (byte[] patch in projectProperties.IPSPatches)
					{
						File.WriteAllBytes(workPath + "\\uips.exe", Resources.tool_uips);
						File.WriteAllBytes(workPath + "\\rom.bin", romBinary);
						File.WriteAllBytes(workPath + "\\patch.ips", patch);
						process = Process.Start(new ProcessStartInfo(workPath + "\\uips.exe")
						{
							CreateNoWindow = true,
							WorkingDirectory = workPath,
							UseShellExecute = false,
							RedirectStandardError = true,
							Arguments = "a \"patch.ips\" \"rom.bin\""
						});
						error = "";
						while (error == "")
						{
							error = process.StandardError.ReadToEnd();
							Application.DoEvents();
						}
						ExitCode = process.ExitCode;
						process.Close();
						if (ExitCode != 0)
						{
							MessageBox.Show(string.Format(strings.error_ips_patching, error), constants.app_name, MessageBoxButtons.OK, MessageBoxIcon.Hand);
						}
						romBinary = File.ReadAllBytes(workPath + "\\rom.bin");
						File.Delete(workPath + "\\rom.bin");
						File.Delete(workPath + "\\uips.exe");
					}
				}
				if (projectProperties.ContentOptions.ApplySleepPatch || projectProperties.ContentOptions.ApplyRebootPatch)
				{
					File.WriteAllBytes(workPath + "\\sleephack.exe", Resources.tool_gba_custom_sleephack);
					File.WriteAllBytes(workPath + "\\libstdc++-6.dll", Resources.dll_libstdcpp_6);
					File.WriteAllBytes(workPath + "\\libgcc_s_dw2-1.dll", Resources.dll_libgcc_s_dw2_1);
					File.WriteAllBytes(workPath + "\\libwinpthread-1.dll", Resources.dll_libwinpthread_1);
					byte[] sleepPatch = Resources.sleep_reboot_patch_gba;
					if (!projectProperties.ContentOptions.ApplySleepPatch)
					{
						sleepPatch = Utils.ReplaceByOffset(sleepPatch, frmGBAInject.GBA_NOP_PATCH, frmGBAInject.DO_SLEEP_PATCH_NOP_OFFSET);
					}
					if (!projectProperties.ContentOptions.ApplyRebootPatch)
					{
						sleepPatch = Utils.ReplaceByOffset(sleepPatch, frmGBAInject.GBA_NOP_PATCH, frmGBAInject.DO_REBOOT_PATCH_NOP_OFFSET);
					}
					sleepPatch[frmGBAInject.SLEEP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_A_OFFSET] = (projectProperties.ContentOptions.SleepButtonCombo.UsingA ? 1 : 0);
					sleepPatch[frmGBAInject.SLEEP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_B_OFFSET] = (projectProperties.ContentOptions.SleepButtonCombo.UsingB ? 2 : 0);
					sleepPatch[frmGBAInject.SLEEP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_SELECT_OFFSET] = (projectProperties.ContentOptions.SleepButtonCombo.UsingSelect ? 4 : 0);
					sleepPatch[frmGBAInject.SLEEP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_START_OFFSET] = (projectProperties.ContentOptions.SleepButtonCombo.UsingStart ? 8 : 0);
					sleepPatch[frmGBAInject.SLEEP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_RIGHT_OFFSET] = (projectProperties.ContentOptions.SleepButtonCombo.UsingRight ? 16 : 0);
					sleepPatch[frmGBAInject.SLEEP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_LEFT_OFFSET] = (projectProperties.ContentOptions.SleepButtonCombo.UsingLeft ? 32 : 0);
					sleepPatch[frmGBAInject.SLEEP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_UP_OFFSET] = (projectProperties.ContentOptions.SleepButtonCombo.UsingUp ? 64 : 0);
					sleepPatch[frmGBAInject.SLEEP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_DOWN_OFFSET] = (projectProperties.ContentOptions.SleepButtonCombo.UsingDown ? 128 : 0);
					sleepPatch[frmGBAInject.SLEEP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_L_OFFSET] = (projectProperties.ContentOptions.SleepButtonCombo.UsingL ? 1 : 0);
					sleepPatch[frmGBAInject.SLEEP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_L_OFFSET + 1] = ((projectProperties.ContentOptions.SleepButtonCombo.UsingL && sleepPatch[frmGBAInject.SLEEP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_L_OFFSET + 1] == 64) ? 76 : 64);
					sleepPatch[frmGBAInject.SLEEP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_R_OFFSET] = (projectProperties.ContentOptions.SleepButtonCombo.UsingR ? 2 : 0);
					sleepPatch[frmGBAInject.SLEEP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_R_OFFSET + 1] = ((projectProperties.ContentOptions.SleepButtonCombo.UsingR && sleepPatch[frmGBAInject.SLEEP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_R_OFFSET + 1] == 64) ? 76 : 64);
					sleepPatch[frmGBAInject.WAKE_UP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_A_OFFSET] = (projectProperties.ContentOptions.WakeUpButtonCombo.UsingA ? 1 : 0);
					sleepPatch[frmGBAInject.WAKE_UP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_B_OFFSET] = (projectProperties.ContentOptions.WakeUpButtonCombo.UsingB ? 2 : 0);
					sleepPatch[frmGBAInject.WAKE_UP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_SELECT_OFFSET] = (projectProperties.ContentOptions.WakeUpButtonCombo.UsingSelect ? 4 : 0);
					sleepPatch[frmGBAInject.WAKE_UP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_START_OFFSET] = (projectProperties.ContentOptions.WakeUpButtonCombo.UsingStart ? 8 : 0);
					sleepPatch[frmGBAInject.WAKE_UP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_RIGHT_OFFSET] = (projectProperties.ContentOptions.WakeUpButtonCombo.UsingRight ? 16 : 0);
					sleepPatch[frmGBAInject.WAKE_UP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_LEFT_OFFSET] = (projectProperties.ContentOptions.WakeUpButtonCombo.UsingLeft ? 32 : 0);
					sleepPatch[frmGBAInject.WAKE_UP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_UP_OFFSET] = (projectProperties.ContentOptions.WakeUpButtonCombo.UsingUp ? 64 : 0);
					sleepPatch[frmGBAInject.WAKE_UP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_DOWN_OFFSET] = (projectProperties.ContentOptions.WakeUpButtonCombo.UsingDown ? 128 : 0);
					sleepPatch[frmGBAInject.WAKE_UP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_L_OFFSET] = (projectProperties.ContentOptions.WakeUpButtonCombo.UsingL ? 1 : 0);
					sleepPatch[frmGBAInject.WAKE_UP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_L_OFFSET + 1] = ((projectProperties.ContentOptions.WakeUpButtonCombo.UsingL && sleepPatch[frmGBAInject.WAKE_UP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_L_OFFSET + 1] == 128) ? 140 : 128);
					sleepPatch[frmGBAInject.WAKE_UP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_R_OFFSET] = (projectProperties.ContentOptions.WakeUpButtonCombo.UsingR ? 2 : 0);
					sleepPatch[frmGBAInject.WAKE_UP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_R_OFFSET + 1] = ((projectProperties.ContentOptions.WakeUpButtonCombo.UsingR && sleepPatch[frmGBAInject.WAKE_UP_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_R_OFFSET + 1] == 128) ? 140 : 128);
					sleepPatch[frmGBAInject.REBOOT_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_A_OFFSET] = (projectProperties.ContentOptions.RebootButtonCombo.UsingA ? 1 : 0);
					sleepPatch[frmGBAInject.REBOOT_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_B_OFFSET] = (projectProperties.ContentOptions.RebootButtonCombo.UsingB ? 2 : 0);
					sleepPatch[frmGBAInject.REBOOT_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_SELECT_OFFSET] = (projectProperties.ContentOptions.RebootButtonCombo.UsingSelect ? 4 : 0);
					sleepPatch[frmGBAInject.REBOOT_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_START_OFFSET] = (projectProperties.ContentOptions.RebootButtonCombo.UsingStart ? 8 : 0);
					sleepPatch[frmGBAInject.REBOOT_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_RIGHT_OFFSET] = (projectProperties.ContentOptions.RebootButtonCombo.UsingRight ? 16 : 0);
					sleepPatch[frmGBAInject.REBOOT_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_LEFT_OFFSET] = (projectProperties.ContentOptions.RebootButtonCombo.UsingLeft ? 32 : 0);
					sleepPatch[frmGBAInject.REBOOT_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_UP_OFFSET] = (projectProperties.ContentOptions.RebootButtonCombo.UsingUp ? 64 : 0);
					sleepPatch[frmGBAInject.REBOOT_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_DOWN_OFFSET] = (projectProperties.ContentOptions.RebootButtonCombo.UsingDown ? 128 : 0);
					sleepPatch[frmGBAInject.REBOOT_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_L_OFFSET] = (projectProperties.ContentOptions.RebootButtonCombo.UsingL ? 1 : 0);
					sleepPatch[frmGBAInject.REBOOT_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_L_OFFSET + 1] = ((projectProperties.ContentOptions.RebootButtonCombo.UsingL && sleepPatch[frmGBAInject.REBOOT_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_L_OFFSET + 1] == 64) ? 76 : 64);
					sleepPatch[frmGBAInject.REBOOT_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_R_OFFSET] = (projectProperties.ContentOptions.RebootButtonCombo.UsingR ? 2 : 0);
					sleepPatch[frmGBAInject.REBOOT_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_R_OFFSET + 1] = ((projectProperties.ContentOptions.RebootButtonCombo.UsingR && sleepPatch[frmGBAInject.REBOOT_PATCH_BUTTON_COMBO_OFFSET + frmGBAInject.BUTTON_MASK_R_OFFSET + 1] == 64) ? 76 : 64);
					File.WriteAllBytes(workPath + "\\patch.bin", sleepPatch);
					File.WriteAllBytes(workPath + "\\rom.gba", romBinary);
					ProcessStartInfo ProcessInfo = new ProcessStartInfo(workPath + "\\sleephack.exe");
					ProcessInfo.CreateNoWindow = true;
					ProcessInfo.WorkingDirectory = workPath;
					ProcessInfo.UseShellExecute = false;
					ProcessInfo.RedirectStandardError = true;
					ProcessInfo.Arguments = "rom.gba rom_patched.gba " + romBinary.Length.ToString();
					ProcessInfo.RedirectStandardOutput = true;
					process = Process.Start(ProcessInfo);
					process.WaitForExit();
					string patchOutput = process.StandardOutput.ReadToEnd();
					error = process.StandardError.ReadToEnd();
					ExitCode = process.ExitCode;
					process.Close();
					ProcessInfo.RedirectStandardOutput = false;
					File.Delete(workPath + "\\rom_patched.gba");
					int patchCount = Utils.StringCount(patchOutput, "Patched an interrupt installer!");
					int patchOffset = Utils.FindFreeSpace(romBinary, (uint)((long)(romBinary.Length - sleepPatch.Length) - (long)((ulong)frmGBAInject.SLEEP_REBOOT_PATCH_ACTIVATION_SIZE * (ulong)((long)patchCount)) >> 4) << 4, (uint)((long)sleepPatch.Length + (long)((ulong)frmGBAInject.SLEEP_REBOOT_PATCH_ACTIVATION_SIZE * (ulong)((long)patchCount)) + (16L - ((long)sleepPatch.Length + (long)((ulong)frmGBAInject.SLEEP_REBOOT_PATCH_ACTIVATION_SIZE * (ulong)((long)patchCount))) % 16L) % 16L));
					if (patchOffset == -1)
					{
						patchOffset = romBinary.Length;
					}
					process = Process.Start(new ProcessStartInfo(workPath + "\\sleephack.exe")
					{
						CreateNoWindow = true,
						WorkingDirectory = workPath,
						UseShellExecute = false,
						RedirectStandardError = true,
						Arguments = "rom.gba rom_patched.gba " + patchOffset.ToString()
					});
					process.WaitForExit();
					error = process.StandardError.ReadToEnd();
					ExitCode = process.ExitCode;
					process.Close();
					romBinary = File.ReadAllBytes(workPath + "\\rom_patched.gba");
					File.Delete(workPath + "\\rom.gba");
					File.Delete(workPath + "\\rom_patched.gba");
					File.Delete(workPath + "\\libwinpthread-1.dll");
					File.Delete(workPath + "\\libgcc_s_dw2-1.dll");
					File.Delete(workPath + "\\libstdc++-6.dll");
					File.Delete(workPath + "\\sleephack.exe");
					File.Delete(workPath + "\\patch.bin");
				}
				cart_info_gba cart_info = frmGBAInject.GetGBACartInfo(romBinary);
				if (injectionMethod != GBAInjectionMethod.METHOD_VIRTUAL_CONSOLE)
				{
					if (injectionMethod - GBAInjectionMethod.METHOD_RETROARCH_MGBA_FORWARDER <= 1)
					{
						File.WriteAllBytes(workPath + "\\extracted\\ExtractedRomFS\\rom" + Path.GetExtension(projectProperties.ROMFilePath).ToLower(), romBinary);
					}
				}
				else
				{
					byte[] codeBin = new byte[(Utils.NextNearestPowerOfTwo(cart_info.rom_size) >> 20 << 20) + 864U];
					byte[] footer = frmGBAInject.GetVirtualConsoleGBAFooter(cart_info, 254 - projectProperties.ContentOptions.ScreenGhostingMode, projectProperties.ContentOptions.ScreenColorConfig, frmGBAInject.GetSaveTypeFlagByIndex((int)projectProperties.SaveTypeOverride, cart_info.saveType, cart_info.has_rtc));
					if (footer == null && projectProperties.UsingIPSPatch)
					{
						throw new Exception("The IPS patch brokes the Virtual Console compatibility. Please select another injection method or disable IPS patch.");
					}
					codeBin = Utils.ReplaceByOffset(codeBin, romBinary, 0);
					codeBin = Utils.ReplaceByOffset(codeBin, footer, codeBin.Length - 864);
					File.Delete(workPath + "\\extracted\\ExtractedExeFS\\code.bin");
					File.WriteAllBytes(workPath + "\\extracted\\ExtractedExeFS\\code.bin", codeBin);
				}
				if (injectionMethod - GBAInjectionMethod.METHOD_RETROARCH_MGBA_FORWARDER <= 1)
				{
					File.Copy(PlatformCommonUtils.ApplicationSettings.GBABios, workPath + "\\extracted\\ExtractedRomFS\\gba_bios.bin");
				}
				string internalName = "gba_" + projectProperties.CIATitleID.ToLower() + "_" + PlatformCommonUtils.GetSafeGameName(projectProperties.CIAShortName);
				if (injectionMethod - GBAInjectionMethod.METHOD_RETROARCH_MGBA_FORWARDER <= 1)
				{
					if (projectProperties.ContentOptions.BottomScreenImage != null)
					{
						projectProperties.ContentOptions.BottomScreenImage.Save(workPath + "\\bottom.bmp", ImageFormat.Bmp);
						File.WriteAllBytes(workPath + "\\magick.exe", Resources.tool_magick);
						process = Process.Start(new ProcessStartInfo(workPath + "\\magick.exe")
						{
							CreateNoWindow = true,
							WorkingDirectory = workPath,
							UseShellExecute = false,
							RedirectStandardError = true,
							Arguments = "bottom.bmp -separate -reverse -channel RGB -combine -rotate 90 bottom.rgb"
						});
						process.WaitForExit();
						error = process.StandardError.ReadToEnd();
						ExitCode = process.ExitCode;
						process.Close();
						File.Move(workPath + "\\bottom.rgb", workPath + "\\extracted\\ExtractedRomFS\\bottom.bin");
					}
					string configINI = frmBaseProject.GetRetroArchConfig(projectProperties.ContentOptions, internalName, frmGBAInject.PIXEL_PERFECT_RESOLUTION, Platform.PLATFORM_GBA, false);
					if (injectionMethod == GBAInjectionMethod.METHOD_RETROARCH_GPSP_FORWARDER)
					{
						configINI = configINI.Replace("video_frame_delay = \"0\"", "video_frame_delay = \"1\"");
					}
					File.WriteAllText(workPath + "\\extracted\\ExtractedRomFS\\retroarch.cfg", configINI);
					File.WriteAllText(workPath + "\\extracted\\ExtractedRomFS\\internal_name.txt", internalName);
					File.WriteAllText(workPath + "\\extracted\\ExtractedRomFS\\rom_path.txt", "romfs:/rom" + Path.GetExtension(projectProperties.ROMFilePath).ToLower());
				}
				List<string> thisCoreOptions = new List<string>();
				if (injectionMethod != GBAInjectionMethod.METHOD_RETROARCH_MGBA_FORWARDER)
				{
					if (injectionMethod == GBAInjectionMethod.METHOD_RETROARCH_GPSP_FORWARDER)
					{
						thisCoreOptions = Enumerable.ToList<string>(frmGBAInject.CoreOptionsGPSP);
						string coreOptions = frmBaseProject.GetCoreOptions(projectProperties.ContentOptions.CoreOptions, thisCoreOptions);
						File.WriteAllText(workPath + "\\extracted\\ExtractedRomFS\\retroarch-core-options.cfg", coreOptions);
					}
				}
				else
				{
					thisCoreOptions = Enumerable.ToList<string>(frmGBAInject.CoreOptionsMGBA);
					string coreOptions = frmBaseProject.GetCoreOptions(projectProperties.ContentOptions.CoreOptions, thisCoreOptions);
					File.WriteAllText(workPath + "\\extracted\\ExtractedRomFS\\retroarch-core-options.cfg", coreOptions);
				}
				File.Delete(workPath + "\\extracted\\ExtractedExeFS\\icon.icn");
				File.WriteAllBytes(workPath + "\\extracted\\ExtractedExeFS\\icon.icn", iconBinary);
				File.Delete(workPath + "\\extracted\\ExtractedExeFS\\banner.bnr");
				File.WriteAllBytes(workPath + "\\extracted\\ExtractedExeFS\\banner.bnr", bannerBinary);
				byte[] titleID = Utils.StringToByteArray("0" + projectProperties.CIATitleID);
				byte[] productCode = Encoding.ASCII.GetBytes(constants.product_code_prefix_gba + projectProperties.CIAProductCode);
				titleID = new byte[]
				{
					titleID[2],
					titleID[1],
					titleID[0]
				};
				byte[] exh = File.ReadAllBytes(workPath + "\\extracted\\DecryptedExHeader.bin");
				exh = Utils.ReplaceByOffset(exh, titleID, 457);
				byte[] array = exh;
				int num = 459;
				array[num] &= 15;
				exh = Utils.ReplaceByOffset(exh, titleID, 513);
				byte[] array2 = exh;
				int num2 = 515;
				array2[num2] &= 15;
				exh = Utils.ReplaceByOffset(exh, titleID, 1537);
				byte[] array3 = exh;
				int num3 = 1539;
				array3[num3] &= 15;
				if (injectionMethod == GBAInjectionMethod.METHOD_VIRTUAL_CONSOLE && cart_info.saveType == cart_info_gba.SaveType.SOME_FLASH_1M)
				{
					exh[449] = 4;
					exh[450] = 4;
				}
				File.WriteAllBytes(workPath + "\\extracted\\DecryptedExHeader.bin", exh);
				exh = File.ReadAllBytes(workPath + "\\extracted\\HeaderNCCH0.bin");
				exh = Utils.ReplaceByOffset(exh, titleID, 265);
				byte[] array4 = exh;
				int num4 = 267;
				array4[num4] &= 15;
				exh = Utils.ReplaceByOffset(exh, titleID, 281);
				byte[] array5 = exh;
				int num5 = 283;
				array5[num5] &= 15;
				exh = Utils.ReplaceByOffset(exh, productCode, 336);
				File.WriteAllBytes(workPath + "\\extracted\\HeaderNCCH0.bin", exh);
				process = Process.Start(new ProcessStartInfo(workPath + "\\3dstool.exe")
				{
					CreateNoWindow = true,
					WorkingDirectory = workPath + "\\extracted",
					UseShellExecute = false,
					RedirectStandardError = true,
					Arguments = "-ctf romfs CustomRomFS.bin --romfs-dir ExtractedRomFS"
				});
				process.WaitForExit();
				error = process.StandardError.ReadToEnd();
				ExitCode = process.ExitCode;
				process.Close();
				process = Process.Start(new ProcessStartInfo(workPath + "\\3dstool.exe")
				{
					CreateNoWindow = true,
					WorkingDirectory = workPath + "\\extracted",
					UseShellExecute = false,
					RedirectStandardError = true,
					Arguments = "-ctf exefs CustomExeFS.bin --exefs-dir ExtractedExeFS --header HeaderExeFS.bin"
				});
				process.WaitForExit();
				error = process.StandardError.ReadToEnd();
				ExitCode = process.ExitCode;
				process.Close();
				process = Process.Start(new ProcessStartInfo(workPath + "\\3dstool.exe")
				{
					CreateNoWindow = true,
					WorkingDirectory = workPath + "\\extracted",
					UseShellExecute = false,
					RedirectStandardError = true,
					Arguments = "-ctf cxi CustomPartition0.bin --header HeaderNCCH0.bin --exh DecryptedExHeader.bin --exefs CustomExeFS.bin --romfs CustomRomFS.bin --logo LogoLZ.bin --plain PlainRGN.bin"
				});
				process.WaitForExit();
				error = process.StandardError.ReadToEnd();
				ExitCode = process.ExitCode;
				process.Close();
				if (File.Exists(fileInfo.FullName))
				{
					File.Delete(fileInfo.FullName);
				}
				process = Process.Start(new ProcessStartInfo(workPath + "\\makerom.exe")
				{
					CreateNoWindow = true,
					WorkingDirectory = workPath + "\\extracted",
					UseShellExecute = false,
					RedirectStandardError = true,
					Arguments = string.Concat(new object[]
					{
						"-f cia -minor ",
						projectProperties.CIAMinorVersion,
						" -micro ",
						projectProperties.CIAMicroVersion,
						" -content CustomPartition0.bin:0:0x00 -o \"",
						fileInfo.FullName,
						"\""
					})
				});
				process.WaitForExit();
				error = process.StandardError.ReadToEnd();
				ExitCode = process.ExitCode;
				process.Close();
				try
				{
					Directory.Delete(workPath, true);
				}
				catch
				{
				}
				result = PlatformCommonUtils.ExportCIAResult.RESULT_OK;
			}
			catch
			{
				result = PlatformCommonUtils.ExportCIAResult.RESULT_ERROR_DURING_PROCESS;
			}
			return result;
		}
	}
}

Wynadorn avatar Aug 18 '22 13:08 Wynadorn

Its been a few years since this issue was opened; just wanted to check on progress? The NSUI packaged mGBA forwarder is outdated.

HeyItsJono avatar Oct 19 '22 23:10 HeyItsJono

Building off of @TobiasBielefeld's earlier work I've created a fork that includes a python script to easily generate mGBA 3DS Forwarder .cia files, and also updates the base version of mGBA used in these forwarders to the latest release version to date (0.10.0). Anyone interested can find it here.

HeyItsJono avatar Oct 21 '22 00:10 HeyItsJono

Maybe it's possible to update or replace the template .cia or base.cia that's being used to initialize the working directory. Though I'm not very familliar with 3DS homebrew.

On copying the base.cia file that is used from the resources of NSUI and unpacking it, its ExtractedRomFs simply has a dummy file that says "hi". I am not at all experienced in C#, so it is not easy to figure out (for me at least) where the files containing the embedded emulators are.

Delta18-Git avatar Oct 24 '22 09:10 Delta18-Git

I realise this is not the appropriate forum for this discussion and I apologise but on extracting the code.bin file from the ExeFS as well, I checked it out in a hex editor, and this is clearly a RetroArch core from the strings I can see.

image

However, I am not 100% sure if this the right file as I could see some error logs as seen in the image were visible.

Delta18-Git avatar Oct 24 '22 09:10 Delta18-Git

How to make a forwarder from a dev version of mGBA:

You will need the following tools:

  • makerom
  • ctrtool
  • 3dstool
  • A text editor
  • A hex editor

Steps:

  1. Download and extract the CIA from the dev build 7z
  2. Run the command ctrtool -q --contents=cxi mgba.cia on the command line
  3. Take note of the output file; it should be of the form cxi.0000.XXXXXXXX
  4. Run the command 3dstool -xvf cxi.0000.XXXXXXXX --exh exheader.bin --header header.bin --exefs exefs.bin, replacing the XXXXXXXX as appropriate
  5. Make up a 3-byte title ID that's unique and keep track of it. I'll be referring to it as TT TT TT in the following steps.
  6. Open header.bin in a hex editor and replace the bytes 1E 1A 00 at 0x111 and 0x191 with the title ID TT TT TT chosen previously, and replace the bytes 020007 at 0x18D with 03 00 05
  7. Open exheader.bin in a hex editor and replace the bytes 1E 1A 00 at 0x1C9, 0x201, and 0x301 with the title ID TT TT TT chosen previously
  8. Make a new directory, and place the ROM you want the forwarder to use in it
  9. Create a new file in that directory called filename and put the filename of the ROM in it (including the .gba, .gb, .gbc or whatever extension)
  10. Run the command 3dstool -cvf romfs.bin -t romfs --romfs-dir DIRECTORY, replacing DIRECTORY with the path to the directory you created before
  11. Run the command 3dstool -cvf new.cxi -t cxi --exh exheader.bin --header header.bin --exefs exefs.bin --romfs romfs.bin
  12. Run the command makerom -f cia -o forwarder.cia -content new.cxi:0:0
  13. The resulting file forwarder.cia is the new mGBA forwarder

I'm working on a way to streamline this so it's built into the mGBA desktop release, as well as replacing the icon and banner, but for right now this works.

endrift avatar Oct 24 '22 10:10 endrift

Forwarders are now implemented in the dev build, though the 3DS one is not yet tested on Windows. Further, it will stay grayed out if you leave it at the (default) "stable build" option due to the stable builds not including the changes needed for the forwarder to be build from them. Leaving this open until the Windows version is tested, but it should work.

endrift avatar Nov 07 '22 06:11 endrift

Just tried this out on windows and it worked great! it's so nice having emulator features for gba games. If possible, it would be nice to have the option to select my own banner.bin files, so I can use the 3d virtual console-like ones generated by the new super ultimate injector for the perfect gba experience

lilliancf avatar Jan 06 '23 01:01 lilliancf

This feature would be so cool to have just like nds forwarder

Skallua avatar Dec 24 '23 06:12 Skallua

@endrift is this project dead or are there plans to implement this still?

roddyaleixo avatar Feb 25 '24 19:02 roddyaleixo

It's already implemented in dev builds, it just needs more testing before I determine it's production ready.

endrift avatar Feb 25 '24 20:02 endrift