mirror of
https://github.com/rvtr/unlaunch-installer_dev.git
synced 2026-01-26 13:43:08 -05:00
Parse the provided loaded background images and reconstruct a new GIF file on the fly to ensure they don't contain any extension that might not be supported by unalunch. The generated GIF file will have a global color table, and single section, being the image descriptor. It also won't have any looping block.
555 lines
18 KiB
C++
555 lines
18 KiB
C++
#include "message.h"
|
|
#include "sha1digest.h"
|
|
#include "storage.h"
|
|
#include "tonccpy.h"
|
|
#include "unlaunch.h"
|
|
|
|
#include <nds/sha1.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <cstdio>
|
|
#include <unistd.h>
|
|
#include <memory>
|
|
#include <format>
|
|
|
|
static char unlaunchInstallerBuffer[0x30000];
|
|
static char ogUnlaunchInstallerBuffer[0x30000];
|
|
static const char* hnaaTmdPath = "nand:/title/00030017/484e4141/content/title.tmd";
|
|
static const char* hnaaBackupTmdPath = "nand:/title/00030017/484e4141/content/title.tmd.bak";
|
|
|
|
UNLAUNCH_VERSION installerVersion{INVALID};
|
|
size_t unlaunchInstallerSize{};
|
|
|
|
constexpr std::array knownUnlaunchHashes{
|
|
/*"9e6a8d95062533dfc422362f99ff3e24e7de9920"_sha1, // v0.8: blacklisted, doesn't like this install method*/
|
|
/*"fb0d0ffebda67b786f608bf5cbcb2efee6ab42bb"_sha1, // v0.9: blacklisted, doesn't like this install method*/
|
|
/*"d710ff585e321082b33456dd4e0568200c9adcc7"_sha1, // v1.0: blacklisted, doesn't like this install method*/
|
|
/*"4f3e455e0a752d35a219a3ff10ba14a6c98bff13"_sha1, // v1.1: blacklisted, doesn't like this install method*/
|
|
/*"25db1a47ba84748f911d9f4357bfc417533121c7"_sha1, // v1.2: blacklisted, doesn't like this install method*/
|
|
/*"068f1d56da02bb4f93fb76d7874e14010c7e7a3d"_sha1, // v1.3: blacklisted, doesn't like this install method*/
|
|
/*"43197370de74d302ef7c4420059c3ca7d50c4f3d"_sha1, // v1.4: blacklisted, doesn't like this install method*/
|
|
/*"0525b28cc59b6f7fc00ad592aebadd7257bf7efb"_sha1, // v1.5: blacklisted, doesn't like this install method*/
|
|
/*"9470a51fde188235052b119f6bfabf6689cb2343"_sha1, // v1.6: blacklisted, doesn't like this install method*/
|
|
/*"672c11eb535b97b0d32ff580d314a2ad6411d5fe"_sha1, // v1.7: blacklisted, doesn't like this install method*/
|
|
/*"b76c2b1722e769c6c0b4b3d4bc73250e41993229"_sha1, // v1.8: blacklisted, the HNAA patch is only done for 2.0 */
|
|
/*"f3eb41cba136a3477523155f8b05df14917c55f4"_sha1, // v1.9: blacklisted, the HNAA patch is only done for 2.0 */
|
|
"15f4a36251d1408d71114019b2825fe8f5b4c8cc"_sha1, // v2.0
|
|
};
|
|
|
|
constexpr std::array gifOffsets{
|
|
/* std::make_pair(0x48d4, 0x8540),*/ /* 1.8 */
|
|
/* std::make_pair(0x48c8, 0x8534),*/ /* 1.9 */
|
|
std::make_pair(0x48f0, 0x855c), /* 2.0 */
|
|
};
|
|
|
|
constexpr std::array blockAllPatchesOffset{
|
|
/* 0xae74, */ /* 1.9 */
|
|
0xae91, /* 2.0 */
|
|
};
|
|
|
|
static bool writeUnlaunchToHNAAFolder();
|
|
|
|
bool isValidUnlaunchInstallerSize(size_t size)
|
|
{
|
|
return size == 163320 /*1.8*/ || size == 196088 /*1.9, 2.0*/;
|
|
}
|
|
|
|
static bool removeHnaaLauncher()
|
|
{
|
|
auto* errString = [] -> const char* {
|
|
if(fileExists(hnaaTmdPath)) {
|
|
if(!toggleFileReadOnly(hnaaTmdPath, false))
|
|
{
|
|
return "\x1B[31mError:\x1B[33m Failed to mark unlaunch's title.tmd as writable\nLeaving as is\n";
|
|
}
|
|
if(!removeIfExists(hnaaTmdPath))
|
|
{
|
|
return "\x1B[31mError:\x1B[33m Failed to delete ulnaunch's title.tmd\n";
|
|
}
|
|
}
|
|
if(!removeIfExists("nand:/title/00030017/484e4141/content"))
|
|
{
|
|
return "\x1B[31mError:\x1B[33m Failed to delete ulnaunch's content folder\n";
|
|
}
|
|
if(!removeIfExists("nand:/title/00030017/484e4141"))
|
|
{
|
|
return "\x1B[31mError:\x1B[33m Failed to delete ulnaunch's 484e4141 folder\n";
|
|
}
|
|
return nullptr;
|
|
}();
|
|
if(errString)
|
|
{
|
|
messageBox(errString);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool restoreMainTmd(const consoleInfo& info, bool removeHNAABackup)
|
|
{
|
|
std::shared_ptr<FILE> launcherTmdSptr{fopen(info.launcherTmdPath.data(), "r+b"), fclose};
|
|
if(!launcherTmdSptr)
|
|
{
|
|
messageBox("\x1B[31mError:\x1B[33m Failed to open default launcher's title.tmd\n");
|
|
return false;
|
|
}
|
|
FILE* launcherTmd = launcherTmdSptr.get();
|
|
|
|
// If the tmd is patched, assume the HNAA backup is already set in place.
|
|
if(info.tmdPatched) {
|
|
fseek(launcherTmd, 0x190, SEEK_SET);
|
|
// Set back the title.tmd's title id from GNXX to HNXX
|
|
char c = 0x48;
|
|
fwrite(&c, 1, 1, launcherTmd);
|
|
fflush(launcherTmd);
|
|
} else if(!info.tmdGood || info.tmdInvalid) {
|
|
// The tmd isn't good, it either has the wrong size, or the hash didn't match
|
|
// and it wasn't patched with the new method
|
|
// Install the hnaa backup if not found and then truncate the tmd to 520b
|
|
// before restoring it
|
|
if(!info.UnlaunchHNAAtmdFound && !removeHNAABackup)
|
|
{
|
|
auto choiceString = [&]{
|
|
if(installerVersion != INVALID)
|
|
return "Unlaunch was installed with the\n"
|
|
"legacy method.\n"
|
|
"Before uninstalling it, a\n"
|
|
"failsafe installation will be\n"
|
|
"created.\n"
|
|
"Proceed?";
|
|
return "Unlaunch was installed with the\n"
|
|
"legacy method\n"
|
|
"But a failsafe installation\n"
|
|
"cannot be created since no valid\n"
|
|
"unlaunch installer was provided.\n"
|
|
"Proceed anyways?";
|
|
}();
|
|
if(choiceBox(choiceString) == NO)
|
|
{
|
|
return false;
|
|
}
|
|
if(installerVersion != INVALID)
|
|
{
|
|
if(!writeUnlaunchToHNAAFolder())
|
|
{
|
|
if(choiceBox("Failsafe installation couldn't\n"
|
|
"be copmleted.\n"
|
|
"Proceed anyways?") == NO)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (ftruncate(fileno(launcherTmd), 520) != 0) {
|
|
messageBox("\x1B[31mError:\x1B[33m Failed to remove unlaunch\n");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Sha1Digest digest;
|
|
calculateFileSha1(launcherTmd, &digest);
|
|
|
|
// the tmd still doesn't match, write a known good one
|
|
if(digest != info.recoveryTmdDataSha) {
|
|
fseek(launcherTmd, 0, SEEK_SET);
|
|
auto written = fwrite(info.recoveryTmdData.data(), info.recoveryTmdData.size(), 1, launcherTmd);
|
|
if(written != 1) {
|
|
messageBox("\x1B[31mError:\x1B[33m Failed to remove unlaunch\n");
|
|
return false;
|
|
}
|
|
}
|
|
if(removeHNAABackup && info.UnlaunchHNAAtmdFound)
|
|
{
|
|
return removeHnaaLauncher();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool patchMainTmd(const char* path)
|
|
{
|
|
FILE* launcherTmd = fopen(path, "r+b");
|
|
if(!launcherTmd)
|
|
{
|
|
messageBox("\x1B[31mError:\x1B[33m Failed to open default launcher's title.tmd\n");
|
|
return false;
|
|
}
|
|
// Patches the title.tmd's title id from HNXX to GNXX
|
|
fseek(launcherTmd, 0x190, SEEK_SET);
|
|
char c;
|
|
fread(&c, 1, 1, launcherTmd);
|
|
//if byte is not already set, it's clean
|
|
if(c == 0x48)
|
|
{
|
|
fseek(launcherTmd, -1, SEEK_CUR);
|
|
c = 0x47;
|
|
fwrite(&c, 1, 1, launcherTmd);
|
|
}
|
|
else if(c != 0x47)
|
|
{
|
|
messageBox("\x1B[31mError:\x1B[33m Default launcher's title.tmd was tamprered with, aborting\n");
|
|
fclose(launcherTmd);
|
|
return false;
|
|
}
|
|
|
|
fclose(launcherTmd);
|
|
return true;
|
|
}
|
|
|
|
static bool restoreProtoTmd(const char* path)
|
|
{
|
|
if (!fileExists(hnaaBackupTmdPath))
|
|
{
|
|
messageBox("\x1B[31mError:\x1B[33m No original tmd found!\nCan't uninstall unlaunch.\n");
|
|
return false;
|
|
}
|
|
removeIfExists(path);
|
|
rename(hnaaBackupTmdPath, path);
|
|
toggleFileReadOnly(path, false);
|
|
return true;
|
|
}
|
|
|
|
bool uninstallUnlaunch(const consoleInfo& info, bool removeHNAABackup)
|
|
{
|
|
// TODO: handle retailLauncherTmdPresentAndToBePatched = false on retail consoles
|
|
if (info.isRetail) {
|
|
if(!toggleFileReadOnly(info.launcherTmdPath.data(), false))
|
|
{
|
|
messageBox(std::format("\x1B[31mError:\x1B[33m Failed to make {} writable\n", info.launcherTmdPath).data());
|
|
return false;
|
|
}
|
|
if(!toggleFileReadOnly(info.launcherAppPath.data(), false))
|
|
{
|
|
messageBox(std::format("\x1B[31mError:\x1B[33m Failed to make {} writable\n", info.launcherAppPath).data());
|
|
return false;
|
|
}
|
|
if (!restoreMainTmd(info, removeHNAABackup))
|
|
{
|
|
return false;
|
|
}
|
|
} else {
|
|
if (!toggleFileReadOnly(hnaaTmdPath, false) || !restoreProtoTmd(hnaaTmdPath))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool writeUnlaunchTmd(const char* path)
|
|
{
|
|
static constexpr auto unlaunchShaOffset = 0x4000;
|
|
Sha1Digest expectedDigest, actualDigest;
|
|
swiSHA1Calc(expectedDigest.data(), unlaunchInstallerBuffer + unlaunchShaOffset,
|
|
(unlaunchInstallerSize + 520) - unlaunchShaOffset);
|
|
if(calculateFileSha1PathOffset(path, actualDigest.data(), unlaunchShaOffset) && expectedDigest == actualDigest)
|
|
{
|
|
// the tmd hasn't changed, no need to do anything
|
|
return true;
|
|
}
|
|
|
|
FILE* targetTmd = fopen(path, "wb");
|
|
if (!targetTmd)
|
|
{
|
|
messageBox("\x1B[31mError:\x1B[33m Failed to open target unlaunch tmd\n");
|
|
return false;
|
|
}
|
|
|
|
if(!writeToFile(targetTmd, unlaunchInstallerBuffer, unlaunchInstallerSize + 520))
|
|
{
|
|
fclose(targetTmd);
|
|
removeIfExists(path);
|
|
messageBox("\x1B[31mError:\x1B[33m Failed write unlaunch to tmd\n");
|
|
return false;
|
|
}
|
|
|
|
fclose(targetTmd);
|
|
|
|
if(!calculateFileSha1PathOffset(path, actualDigest.data(), unlaunchShaOffset) || expectedDigest != actualDigest)
|
|
{
|
|
removeIfExists(path);
|
|
messageBox("\x1B[31mError:\x1B[33m Unlaunch tmd was not properly written\n");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool writeUnlaunchToHNAAFolder()
|
|
{
|
|
//Create HNAA launcher folder
|
|
if (!safeCreateDir("nand:/title/00030017")
|
|
|| !safeCreateDir("nand:/title/00030017/484e4141")
|
|
|| !safeCreateDir("nand:/title/00030017/484e4141/content")) {
|
|
return false;
|
|
}
|
|
|
|
// We have to remove write protect otherwise reinstalling will fail.
|
|
if (fileExists(hnaaTmdPath) && !toggleFileReadOnly(hnaaTmdPath, false)) {
|
|
messageBox("\x1B[31mError:\x1B[33m Can't remove launcher tmd write protect\n");
|
|
return false;
|
|
}
|
|
if (!writeUnlaunchTmd(hnaaTmdPath))
|
|
{
|
|
removeHnaaLauncher();
|
|
return false;
|
|
}
|
|
|
|
//Mark the tmd as readonly
|
|
if(!toggleFileReadOnly(hnaaTmdPath, true))
|
|
{
|
|
messageBox("\x1B[31mError:\x1B[33m Failed to mark unlaunch's title.tmd as read only\n");
|
|
removeHnaaLauncher();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool installUnlaunchRetailConsole(const consoleInfo& info)
|
|
{
|
|
if(!writeUnlaunchToHNAAFolder())
|
|
return false;
|
|
|
|
//Finally patch the default launcher tmd to be invalid
|
|
//If there isn't a title.tmd matching the language region in the hwinfo
|
|
// nothing else has to be done, could be a language patch, or a dev system, the user will know what they have done
|
|
if (!info.tmdFound)
|
|
return true;
|
|
|
|
// Set tmd as writable in case unlaunch was already installed through the old method
|
|
if(!toggleFileReadOnly(info.launcherTmdPath.data(), false) || !patchMainTmd(info.launcherTmdPath.data()))
|
|
{
|
|
removeHnaaLauncher();
|
|
return false;
|
|
}
|
|
if (!toggleFileReadOnly(info.launcherTmdPath.data(), true) || !toggleFileReadOnly(info.launcherAppPath.data(), true))
|
|
{
|
|
messageBox("\x1B[31mError:\x1B[33m Failed to mark default launcher's title.tmd\nas read only, install might be unstable\n");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool installUnlaunchProtoConsole(void)
|
|
{
|
|
if(choiceBox("Your DSi has a non-standard\nregion.\n"
|
|
"\x1B[31mInstalling unlaunch may be\n"
|
|
"unsafe.\x1B[33m"
|
|
"\nCancelling is recommended!"
|
|
"\n\nContinue anyways?") == NO)
|
|
return false;
|
|
|
|
// Prototypes DSis are always HNAA. We can't use code that will nuke their launcher.
|
|
|
|
// Also some justification for adding proto support: they're really common.
|
|
// "Real" protos (X3, X4, etc) are hard to find but there are tons of release
|
|
// version DSis that are running prototype firmware.
|
|
// Likely factory rejects that never had production firmware flashed.
|
|
|
|
// We have to remove write protect otherwise reinstalling will fail.
|
|
if (fileExists(hnaaTmdPath) && !toggleFileReadOnly(hnaaTmdPath, false)) {
|
|
messageBox("\x1B[31mError:\x1B[33m Can't remove launcher tmd write protect\n");
|
|
return false;
|
|
}
|
|
bool hnaaBackupExists = fileExists(hnaaBackupTmdPath);
|
|
// Back up the TMD since we'll be writing to it directly.
|
|
if (!hnaaBackupExists)
|
|
{
|
|
rename(hnaaTmdPath, hnaaBackupTmdPath);
|
|
// Mark backup tmd as readonly, just to be sure
|
|
toggleFileReadOnly("nand:/title/00030017/484e4141/content/title.tmd.bak", true);
|
|
}
|
|
|
|
if(!writeUnlaunchTmd(hnaaTmdPath))
|
|
{
|
|
copyFile("nand:/title/00030017/484e4141/content/title.tmd.bak", hnaaTmdPath);
|
|
return false;
|
|
}
|
|
|
|
// Mark the tmd as readonly
|
|
if (!toggleFileReadOnly(hnaaTmdPath, true))
|
|
{
|
|
// There is nothing that can be done at this point.
|
|
messageBox("\x1B[31mError:\x1B[33m Failed to mark tmd as read only\n");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool readUnlaunchInstaller(std::string_view path)
|
|
{
|
|
FILE* unlaunchInstaller = fopen(path.data(), "rb");
|
|
if (!unlaunchInstaller)
|
|
{
|
|
messageBox("\x1B[31mError:\x1B[33m Failed to open unlaunch installer\n");
|
|
return false;
|
|
}
|
|
|
|
unlaunchInstallerSize = getFileSize(unlaunchInstaller);
|
|
if(!isValidUnlaunchInstallerSize(unlaunchInstallerSize))
|
|
{
|
|
messageBox("\x1B[31mError:\x1B[33m Unlaunch installer wrong file size\n");
|
|
return false;
|
|
}
|
|
|
|
int toRead = unlaunchInstallerSize;
|
|
auto* buff = unlaunchInstallerBuffer;
|
|
// Pad the installer with 520 bytes, those being the size of a valid tmd
|
|
buff += 520;
|
|
|
|
size_t n = 0;
|
|
while (toRead != 0 && (n = fread(buff, sizeof(uint8_t), toRead, unlaunchInstaller)) > 0)
|
|
{
|
|
toRead -= n;
|
|
buff += n;
|
|
}
|
|
if (toRead != 0 || ferror(unlaunchInstaller))
|
|
{
|
|
fclose(unlaunchInstaller);
|
|
messageBox("\x1B[31mError:\x1B[33m Failed read unlaunch installer\n");
|
|
return false;
|
|
}
|
|
|
|
fclose(unlaunchInstaller);
|
|
return true;
|
|
}
|
|
|
|
static bool verifyUnlaunchInstaller(void)
|
|
{
|
|
Sha1Digest digest;
|
|
swiSHA1Calc(digest.data(), unlaunchInstallerBuffer + 520, unlaunchInstallerSize);
|
|
auto it = std::ranges::find(knownUnlaunchHashes, digest);
|
|
if(it == knownUnlaunchHashes.end())
|
|
{
|
|
messageBox("\x1B[31mError:\x1B[33m Provided unlaunch installer has an unknown hash\n");
|
|
return false;
|
|
}
|
|
auto idx = std::distance(knownUnlaunchHashes.begin(), it);
|
|
installerVersion = static_cast<UNLAUNCH_VERSION>(idx);
|
|
return true;
|
|
}
|
|
|
|
static bool patchCustomBackground(std::span<uint8_t> customBackground)
|
|
{
|
|
auto size = customBackground.size();
|
|
if(size == 0)
|
|
{
|
|
return true;
|
|
}
|
|
if(size < 7 || size > MAX_GIF_SIZE)
|
|
{
|
|
messageBox("\x1B[31mError:\x1B[33m Gif file too big.\n");
|
|
return false;
|
|
}
|
|
|
|
const u32 gifSignatureStart = 0x38464947;
|
|
const u32 gifSignatureEnd = 0x3B000044;
|
|
|
|
auto [gifOffsetStart, gifOffsetEnd] = gifOffsets[installerVersion];
|
|
|
|
auto* gifStart = reinterpret_cast<uint32_t*>((unlaunchInstallerBuffer + 520) + gifOffsetStart);
|
|
auto* gifEnd = reinterpret_cast<uint32_t*>((unlaunchInstallerBuffer + 520) + gifOffsetEnd);
|
|
|
|
if(*gifStart != gifSignatureStart || *gifEnd != gifSignatureEnd)
|
|
{
|
|
messageBox("\x1B[31mError:\x1B[33m Gif offsets not matching.\n");
|
|
return false;
|
|
}
|
|
|
|
|
|
std::memcpy(gifStart, customBackground.data(), size);
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool applyBinaryPatch(const char* path)
|
|
{
|
|
static constexpr auto lzssCompressedBinaryOffset = 0x8580;
|
|
static constexpr auto lzssCompressedBinarySize = 0x67FD;
|
|
auto* patch = fopen(path, "rb");
|
|
if(!patch)
|
|
{
|
|
messageBox("\x1B[31mError:\x1B[33m Failed to open the patch.\n");
|
|
return false;
|
|
}
|
|
auto patchSize = getFileSize(patch);
|
|
if(patchSize > lzssCompressedBinarySize)
|
|
{
|
|
messageBox("\x1B[31mError:\x1B[33m Patch is too big.\n");
|
|
fclose(patch);
|
|
return false;
|
|
}
|
|
if (fread((unlaunchInstallerBuffer + 520) + lzssCompressedBinaryOffset, 1, patchSize, patch) != patchSize)
|
|
{
|
|
messageBox("\x1B[31mError:\x1B[33m Failed to read patch.\n");
|
|
fclose(patch);
|
|
return false;
|
|
}
|
|
fclose(patch);
|
|
return true;
|
|
}
|
|
|
|
static bool patchUnlaunchInstaller(bool disableAllPatches, const char* splashSoundBinaryPatchPath, std::span<uint8_t> customBackground)
|
|
{
|
|
tonccpy(unlaunchInstallerBuffer, ogUnlaunchInstallerBuffer, sizeof(unlaunchInstallerBuffer));
|
|
if (splashSoundBinaryPatchPath)
|
|
{
|
|
iprintf("Applying splash and sound patch\n");
|
|
if(!applyBinaryPatch(splashSoundBinaryPatchPath))
|
|
{
|
|
return false;
|
|
}
|
|
} else {
|
|
if(disableAllPatches) {
|
|
// change launcher TID from ANH to SAN so that unlaunch doesn't realize it's booting the launcher
|
|
auto patchOffset = blockAllPatchesOffset[installerVersion];
|
|
const char newID[]{'S','A','N'};
|
|
memcpy((unlaunchInstallerBuffer + 520) + patchOffset, newID, 3);
|
|
}
|
|
iprintf("Applying HNAA patch\n");
|
|
if(!applyBinaryPatch("nitro:/force-hnaa-patch.bin"))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
if(!patchCustomBackground(customBackground))
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
UNLAUNCH_VERSION loadUnlaunchInstaller(std::string_view path)
|
|
{
|
|
if(readUnlaunchInstaller(path) && verifyUnlaunchInstaller())
|
|
{
|
|
tonccpy(ogUnlaunchInstallerBuffer, unlaunchInstallerBuffer, sizeof(unlaunchInstallerBuffer));
|
|
return installerVersion;
|
|
}
|
|
return INVALID;
|
|
}
|
|
|
|
std::array unlaunchVersionStrings{
|
|
"v2.0",
|
|
"INVALID",
|
|
};
|
|
|
|
static_assert(unlaunchVersionStrings.size() == (INVALID + 1));
|
|
|
|
const char* getUnlaunchVersionString(UNLAUNCH_VERSION version)
|
|
{
|
|
return unlaunchVersionStrings[version];
|
|
}
|
|
|
|
bool installUnlaunch(const consoleInfo& info, bool disableAllPatches, const char* splashSoundBinaryPatchPath, std::span<uint8_t> customBackground)
|
|
{
|
|
if (installerVersion == INVALID || !patchUnlaunchInstaller(disableAllPatches, splashSoundBinaryPatchPath, customBackground))
|
|
return false;
|
|
|
|
// Treat protos differently
|
|
if (!info.isRetail)
|
|
{
|
|
return installUnlaunchProtoConsole();
|
|
}
|
|
// Do things normally for production units
|
|
return installUnlaunchRetailConsole(info);
|
|
}
|