新增 axibug switch repack tool

This commit is contained in:
sin365 2025-03-11 12:00:17 +08:00
parent 374d496d67
commit 5d8645a1dd
17 changed files with 498 additions and 6 deletions

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 611bc182f939ea147a72b08613e2d2ba
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cbe37300d75dbd641be2e6dca83a913c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,262 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System;
using UnityEditor;
using System.IO;
using UnityEngine;
using System.Text;
namespace AxibugEmuOnline.Editors
{
public class AxibugNSPTools : Editor
{
static string WorkRoot = Path.GetFullPath(Path.Combine(Application.dataPath,"AxiProjectTools/AxiNSPack"));
static string switch_keys = Path.GetFullPath(Path.Combine(Application.dataPath, "AxiProjectTools/AxiNSPack/switch_keys"));
static string hacpack_root = Path.GetFullPath(Path.Combine(Application.dataPath, "AxiProjectTools/AxiNSPack/hacpack"));
static Dictionary<string, string> tools = new Dictionary<string, string>();
static string prodKeysPath;
[MenuItem("Axibug移植工具/Switch/AxibugNSPTools/RepackNSP")]
static void RepackNSP()
{
if (!CheckEnvironmentVariable())
return;
string path = EditorUtility.OpenFilePanel(
title: "选择 .nsp 文件",
directory: Path.Combine(Application.dataPath,".."), // 默认路径为项目 Assets 目录
extension: "nsp" // 限制文件类型为 .nsp
);
if (string.IsNullOrEmpty(path))
return;
RepackNSP(path);
}
static bool CheckEnvironmentVariable()
{
// 获取环境变量(需要添加环境变量检查)
string sdkRoot = Environment.GetEnvironmentVariable("NINTENDO_SDK_ROOT");
if (string.IsNullOrEmpty(sdkRoot))
{
Debug.LogError($"[AxibugNSPTools]请先正确配置环境变量:NINTENDO_SDK_ROOT,(若已配置则保证配置后彻底重启Unity Hub和Unity)");
return false;
}
#region prod.keys文件路径
prodKeysPath = Path.Combine(
switch_keys,
"prod.keys"
);
if (!File.Exists(prodKeysPath))
{
Debug.LogError($"[AxibugNSPTools]未找到 prod.keys! 请先准备文件path:{prodKeysPath}");
return false;
}
#endregion
return true;
}
static void RepackNSP(string nspFile)
{
#region
// 获取环境变量(需要添加环境变量检查)
string sdkRoot = Environment.GetEnvironmentVariable("NINTENDO_SDK_ROOT");
tools["authoringTool"] = Path.Combine(sdkRoot, "Tools/CommandLineTools/AuthoringTool/AuthoringTool.exe");
tools["hacPack"] = hacpack_root;
#endregion
#region NSP文件路径
string nspFilePath = nspFile;
string nspFileName = Path.GetFileName(nspFilePath);
string nspParentDir = Path.GetDirectoryName(nspFilePath);
#endregion
#region Title ID
string titleID = ExtractTitleID(nspFilePath) ?? ManualTitleIDInput();
Debug.Log($"[AxibugNSPTools]Using Title ID: {titleID}");
#endregion
#region
CleanDirectory(Path.Combine(nspParentDir, "repacker_extract"));
CleanDirectory(Path.Combine(Path.GetTempPath(), "NCA"));
CleanDirectory(Path.Combine(WorkRoot, "hacpack_backup"));
#endregion
#region NSP文件
string extractPath = Path.Combine(nspParentDir, "repacker_extract");
ExecuteCommand($"{tools["authoringTool"]} extract -o \"{extractPath}\" \"{nspFilePath}\"");
var (controlPath, programPath) = FindNACPAndNPDPaths(extractPath);
if (controlPath == null || programPath == null)
{
Debug.LogError("[AxibugNSPTools] Critical directory structure not found!");
return;
}
#endregion
#region NCA/NSP
string tmpPath = Path.Combine(Path.GetTempPath(), "NCA");
string programNCA = BuildProgramNCA(tmpPath, titleID, programPath);
string controlNCA = BuildControlNCA(tmpPath, titleID, controlPath);
BuildMetaNCA(tmpPath, titleID, programNCA, controlNCA);
string outputNSP = BuildFinalNSP(nspFilePath, nspParentDir, tmpPath, titleID);
Debug.Log($"[AxibugNSPTools]Repacking completed: {outputNSP}");
#endregion
}
#region
static string GetUserInput()
{
Console.Write("Enter the NSP filepath: ");
return Console.ReadLine();
}
static string ExtractTitleID(string path)
{
var match = Regex.Match(path, @"0100[\dA-Fa-f]{12}");
return match.Success ? match.Value : null;
}
static string ManualTitleIDInput()
{
Console.Write("Enter Title ID manually: ");
return Console.ReadLine().Trim();
}
static void CleanDirectory(string path)
{
if (Directory.Exists(path))
{
Directory.Delete(path, true);
while (Directory.Exists(path)) ; // 等待删除完成
}
}
static (string, string) FindNACPAndNPDPaths(string basePath)
{
foreach (var dir in Directory.GetDirectories(basePath))
{
if (File.Exists(Path.Combine(dir, "fs0/control.nacp")))
return (dir, null);
if (File.Exists(Path.Combine(dir, "fs0/main.npdm")))
return (null, dir);
}
return (null, null);
}
static string ExecuteCommand(string command)
{
var process = new System.Diagnostics.Process()
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/C {command}",
RedirectStandardOutput = true,
RedirectStandardError = true, // 增加错误流重定向
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8, // 明确指定编码
StandardErrorEncoding = Encoding.UTF8
}
};
var outputBuilder = new StringBuilder();
var errorBuilder = new StringBuilder();
// 使用事件处理程序捕获实时输出
process.OutputDataReceived += (sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
outputBuilder.AppendLine(args.Data);
Debug.Log($"[AxibugNSPTools]{args.Data}");
}
};
process.ErrorDataReceived += (sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
errorBuilder.AppendLine(args.Data);
Debug.LogError($"[AxibugNSPTools]{args.Data}");
}
};
process.Start();
// 开始异步读取输出
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// 等待进程退出(此时流已关闭)
process.WaitForExit();
// 将错误信息附加到主输出
if (errorBuilder.Length > 0)
{
outputBuilder.AppendLine("\nError Output:");
outputBuilder.Append(errorBuilder);
}
return outputBuilder.ToString();
}
#endregion
#region NCA构建逻辑
static string BuildProgramNCA(string tmpPath, string titleID, string programDir)
{
string args = $"-k \"{prodKeysPath}\" -o \"{tmpPath}\" --titleid {titleID} " +
$"--type nca --ncatype program --exefsdir \"{programDir}/fs0\" " +
$"--romfsdir \"{programDir}/fs1\" --logodir \"{programDir}/fs2\"";
string output = ExecuteCommand($"{tools["hacPack"]} {args}");
return ParseNCAOutput(output, "Program");
}
static string BuildControlNCA(string tmpPath, string titleID, string controlDir)
{
string args = $"-k \"{prodKeysPath}\" -o \"{tmpPath}\" --titleid {titleID} " +
$"--type nca --ncatype control --romfsdir \"{controlDir}/fs0\"";
string output = ExecuteCommand($"{tools["hacPack"]} {args}");
return ParseNCAOutput(output, "Control");
}
static void BuildMetaNCA(string tmpPath, string titleID, string programNCA, string controlNCA)
{
string args = $"-k \"{prodKeysPath}\" -o \"{tmpPath}\" --titleid {titleID} " +
$"--type nca --ncatype meta --titletype application " +
$"--programnca \"{programNCA}\" --controlnca \"{controlNCA}\"";
ExecuteCommand($"{tools["hacPack"]} {args}");
}
static string BuildFinalNSP(string origPath, string parentDir, string tmpPath, string titleID)
{
string outputPath = origPath.Replace(".nsp", "_repacked.nsp");
if (File.Exists(outputPath)) File.Delete(outputPath);
string args = $"-k \"{prodKeysPath}\" -o \"{parentDir}\" --titleid {titleID} " +
$"--type nsp --ncadir \"{tmpPath}\"";
ExecuteCommand($"{tools["hacPack"]} {args}");
File.Move(Path.Combine(parentDir, $"{titleID}.nsp"), outputPath);
return outputPath;
}
static string ParseNCAOutput(string output, string type)
{
var line = output.Split('\n')
.FirstOrDefault(l => l.Contains($"Created {type} NCA:"));
return line?.Split(':').Last().Trim();
}
#endregion
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 55aa3f0466c30bc4683cdbdc4dd75940

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d90c85ddb14ad7e4e9a6242ba135da0b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b31e2ae7250c09548a777d4dcdfe2d1f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7be57cd4293e9dc4297ea9b83fe08b18
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e1252f6d74d67ee48af0a0342aecc981
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="utf-8"?>
<Application>
<Title>
<Language>AmericanEnglish</Language>
<Name>Homebrew Menu</Name>
<Publisher>Yellows8</Publisher>
</Title>
<Title>
<Language>BritishEnglish</Language>
<Name>Homebrew Menu</Name>
<Publisher>Yellows8</Publisher>
</Title>
<Title>
<Language>Japanese</Language>
<Name>Homebrew Menu</Name>
<Publisher>Yellows8</Publisher>
</Title>
<Title>
<Language>French</Language>
<Name>Homebrew Menu</Name>
<Publisher>Yellows8</Publisher>
</Title>
<Title>
<Language>German</Language>
<Name>Homebrew Menu</Name>
<Publisher>Yellows8</Publisher>
</Title>
<Title>
<Language>LatinAmericanSpanish</Language>
<Name>Homebrew Menu</Name>
<Publisher>Yellows8</Publisher>
</Title>
<Title>
<Language>Spanish</Language>
<Name>Homebrew Menu</Name>
<Publisher>Yellows8</Publisher>
</Title>
<Title>
<Language>Italian</Language>
<Name>Homebrew Menu</Name>
<Publisher>Yellows8</Publisher>
</Title>
<Title>
<Language>Dutch</Language>
<Name>Homebrew Menu</Name>
<Publisher>Yellows8</Publisher>
</Title>
<Title>
<Language>CanadianFrench</Language>
<Name>Homebrew Menu</Name>
<Publisher>Yellows8</Publisher>
</Title>
<Title>
<Language>Portuguese</Language>
<Name>Homebrew Menu</Name>
<Publisher>Yellows8</Publisher>
</Title>
<Title>
<Language>Russian</Language>
<Name>Homebrew Menu</Name>
<Publisher>Yellows8</Publisher>
</Title>
<Isbn/>
<StartupUserAccount>Required</StartupUserAccount>
<UserAccountSwitchLock>Disable</UserAccountSwitchLock>
<ParentalControl>None</ParentalControl>
<SupportedLanguage>AmericanEnglish</SupportedLanguage>
<SupportedLanguage>BritishEnglish</SupportedLanguage>
<SupportedLanguage>Japanese</SupportedLanguage>
<SupportedLanguage>French</SupportedLanguage>
<SupportedLanguage>German</SupportedLanguage>
<SupportedLanguage>LatinAmericanSpanish</SupportedLanguage>
<SupportedLanguage>Spanish</SupportedLanguage>
<SupportedLanguage>Italian</SupportedLanguage>
<SupportedLanguage>Dutch</SupportedLanguage>
<SupportedLanguage>CanadianFrench</SupportedLanguage>
<SupportedLanguage>Russian</SupportedLanguage>
<Screenshot>Allow</Screenshot>
<VideoCapture>Disable</VideoCapture>
<PresenceGroupId>0x0104444444441001</PresenceGroupId>
<DisplayVersion>2.0</DisplayVersion>
<Rating>
<Organization>CERO</Organization>
<Age>12</Age>
</Rating>
<Rating>
<Organization>ESRB</Organization>
<Age>10</Age>
</Rating>
<Rating>
<Organization>USK</Organization>
<Age>12</Age>
</Rating>
<Rating>
<Organization>PEGI</Organization>
<Age>12</Age>
</Rating>
<Rating>
<Organization>PEGIPortugal</Organization>
<Age>12</Age>
</Rating>
<Rating>
<Organization>PEGIBBFC</Organization>
<Age>12</Age>
</Rating>
<Rating>
<Organization>Russian</Organization>
<Age>12</Age>
</Rating>
<Rating>
<Organization>ACB</Organization>
<Age>13</Age>
</Rating>
<Rating>
<Organization>OFLC</Organization>
<Age>13</Age>
</Rating>
<DataLossConfirmation>Required</DataLossConfirmation>
<PlayLogPolicy>All</PlayLogPolicy>
<SaveDataOwnerId>0x0104444444441001</SaveDataOwnerId>
<UserAccountSaveDataSize>0x0000000003e00000</UserAccountSaveDataSize>
<UserAccountSaveDataJournalSize>0x0000000000180000</UserAccountSaveDataJournalSize>
<DeviceSaveDataSize>0x0000000000000000</DeviceSaveDataSize>
<DeviceSaveDataJournalSize>0x0000000000000000</DeviceSaveDataJournalSize>
<BcatDeliveryCacheStorageSize>0x0000000000000000</BcatDeliveryCacheStorageSize>
<ApplicationErrorCodeCategory/>
<AddOnContentBaseId>0x0104444444442001</AddOnContentBaseId>
<LogoType>Nintendo</LogoType>
<LocalCommunicationId>0x0104444444441001</LocalCommunicationId>
<LogoHandling>Auto</LogoHandling>
<SeedForPseudoDeviceId>0x0000000000000000</SeedForPseudoDeviceId>
<BcatPassphrase/>
<AddOnContentRegistrationType>AllOnLaunch</AddOnContentRegistrationType>
<UserAccountSaveDataSizeMax>0x0000000000000000</UserAccountSaveDataSizeMax>
<UserAccountSaveDataJournalSizeMax>0x0000000000000000</UserAccountSaveDataJournalSizeMax>
<DeviceSaveDataSizeMax>0x0000000000000000</DeviceSaveDataSizeMax>
<DeviceSaveDataJournalSizeMax>0x0000000000000000</DeviceSaveDataJournalSizeMax>
<TemporaryStorageSize>0x0000000000000000</TemporaryStorageSize>
<CacheStorageSize>0x0000000000000000</CacheStorageSize>
<CacheStorageJournalSize>0x0000000000000000</CacheStorageJournalSize>
<CacheStorageDataAndJournalSizeMax>0x0000000000000000</CacheStorageDataAndJournalSizeMax>
<CacheStorageIndexMax>0x0000000000000000</CacheStorageIndexMax>
<Hdcp>None</Hdcp>
<CrashReport>Deny</CrashReport>
<RuntimeAddOnContentInstall>Deny</RuntimeAddOnContentInstall>
<PlayLogQueryableApplicationId>0x0000000000000000</PlayLogQueryableApplicationId>
<PlayLogQueryCapability>None</PlayLogQueryCapability>
<Repair>None</Repair>
<Attribute>None</Attribute>
<ProgramIndex>0</ProgramIndex>
<RequiredNetworkServiceLicenseOnLaunch>None</RequiredNetworkServiceLicenseOnLaunch>
</Application>

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 42c1295c31de3a948825b9e8e9e8184f
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7b1b3ff7954facb409d3ba6f9840f762
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 409c6e8e5ead0ac4991ea6c243e407dd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 08bd0c8a53daacb4ea23b14dde156354
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -29,7 +29,7 @@ public class AxiProjectTools : EditorWindow
}
}
[MenuItem("Axibug移植工具/[1]UGUI组件")]
[MenuItem("Axibug移植工具/ToLowVersionUnity/[1]UGUI组件")]
public static void Part1()
{
GoTAxiProjectToolsSence();
@ -132,7 +132,7 @@ public class AxiProjectTools : EditorWindow
#endif
}
[MenuItem("Axibug移植工具/[2]")]
[MenuItem("Axibug移植工具/ToLowVersionUnity/[2]")]
public static void Part2()
{
if (UnityEngine.Windows.Directory.Exists(outCsDir))
@ -161,7 +161,7 @@ public class AxiProjectTools : EditorWindow
Debug.Log("<Color=#FFF333>处理完毕 [2]生成中间脚本代码</color>");
}
[MenuItem("Axibug移植工具/[3]")]
[MenuItem("Axibug移植工具/ToLowVersionUnity/[3]")]
public static void Part3()
{
AxiPrefabCache cache = AssetDatabase.LoadAssetAtPath<AxiPrefabCache>(cachecfgPath);
@ -205,7 +205,7 @@ public class AxiProjectTools : EditorWindow
}
[MenuItem("Axibug移植工具/[4]")]
[MenuItem("Axibug移植工具/ToLowVersionUnity/[4]")]
public static void Part4()
{
AxiPrefabCache cache = AssetDatabase.LoadAssetAtPath<AxiPrefabCache>(cachecfgPath);
@ -259,7 +259,7 @@ public class AxiProjectTools : EditorWindow
}
[MenuItem("Axibug移植工具/[5]UnPack所有嵌套预制体和场景中的预制体")]
[MenuItem("Axibug移植工具/ToLowVersionUnity/[5]UnPack所有嵌套预制体和场景中的预制体")]
public static void UnpackPrefabs()
{
@ -350,7 +350,7 @@ public class AxiProjectTools : EditorWindow
}
[MenuItem("Axibug移植工具/[6]Sprite")]
[MenuItem("Axibug移植工具/ToLowVersionUnity/[6]Sprite")]
public static void FixMultipleMaterialSprites()
{
string[] guids = AssetDatabase.FindAssets("t:sprite");