引入 AxiNSApi 以及 补完Switch自动打包

This commit is contained in:
sin365 2025-04-08 10:36:07 +08:00
parent 279483c14f
commit 6111319ad4
6 changed files with 293 additions and 57 deletions
AxibugEmuOnline.Client/Assets

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 611bc182f939ea147a72b08613e2d2ba guid: 164952f99969ca942b4761b200d7e381
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}

View File

@ -3,6 +3,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using UnityEditor; using UnityEditor;
@ -82,7 +83,7 @@ namespace AxibugEmuOnline.Editors
{ {
BuildReport report = BuildPipeline.BuildPlayer(options); BuildReport report = BuildPipeline.BuildPlayer(options);
} }
catch(Exception ex) catch (Exception ex)
{ {
Debug.LogError($"[AxibugNSPTools] Unity Build NSP 错误:{ex.ToString()}"); Debug.LogError($"[AxibugNSPTools] Unity Build NSP 错误:{ex.ToString()}");
return; return;
@ -147,13 +148,13 @@ namespace AxibugEmuOnline.Editors
#region #region
CleanDirectory(Path.Combine(nspParentDir, "repacker_extract")); CleanDirectory(Path.Combine(nspParentDir, "repacker_extract"));
CleanDirectory(Path.Combine(Path.GetTempPath(), "NCA")); CleanDirectory(Path.Combine(Path.GetTempPath(), "NCA"));
CleanDirectory(Path.Combine(WorkRoot, "hacpack_backup")); CleanDirectory(Path.Combine(nspParentDir, "hacpack_backup"));
#endregion #endregion
EditorUtility.DisplayProgressBar("AxibugNSPTools", $"解包NSP文件", 0.2f); EditorUtility.DisplayProgressBar("AxibugNSPTools", $"解包NSP文件", 0.2f);
#region NSP文件 #region NSP文件
string extractPath = Path.Combine(nspParentDir, "repacker_extract"); string extractPath = Path.Combine(nspParentDir, "repacker_extract");
ExecuteCommand($"{tools["authoringTool"]} extract -o \"{extractPath}\" \"{nspFilePath}\""); ExecuteCommand($"{tools["authoringTool"]} extract -o \"{extractPath}\" \"{nspFilePath}\"", nspParentDir);
string controlPath = null; string controlPath = null;
string programPath = null; string programPath = null;
@ -167,34 +168,43 @@ namespace AxibugEmuOnline.Editors
#region NCA/NSP #region NCA/NSP
string tmpPath = Path.Combine(Path.GetTempPath(), "NCA"); string tmpPath = Path.Combine(Path.GetTempPath(), "NCA");
EditorUtility.DisplayProgressBar("AxibugNSPTools", $"ÖØ½¨NCA", 0.6f); EditorUtility.DisplayProgressBar("AxibugNSPTools", $"重建 Program NCA", 0.3f);
string programNCA = BuildProgramNCA(tmpPath, titleID, programPath); string programNCA = BuildProgramNCA(tmpPath, titleID, programPath, nspParentDir);
EditorUtility.DisplayProgressBar("AxibugNSPTools", $"ÖØ½¨NCA", 0.7f); EditorUtility.DisplayProgressBar("AxibugNSPTools", $"重建 Control NCA", 0.4f);
string controlNCA = BuildControlNCA(tmpPath, titleID, controlPath); string controlNCA = BuildControlNCA(tmpPath, titleID, controlPath, nspParentDir);
EditorUtility.DisplayProgressBar("AxibugNSPTools", $"ÖØ½¨NCA", 0.8f); EditorUtility.DisplayProgressBar("AxibugNSPTools", $"重建 Meta NCA", 0.5f);
BuildMetaNCA(tmpPath, titleID, programNCA, controlNCA); BuildMetaNCA(tmpPath, titleID, programNCA, controlNCA, nspParentDir);
EditorUtility.DisplayProgressBar("AxibugNSPTools", $"重建NSP", 0.6f);
string outputNSP = BuildFinalNSP(nspFilePath, nspParentDir, tmpPath, titleID, nspParentDir);
EditorUtility.DisplayProgressBar("AxibugNSPTools", $"重建NSP", 0.9f); EditorUtility.DisplayProgressBar("AxibugNSPTools", $"重建NSP", 0.9f);
string outputNSP = BuildFinalNSP(nspFilePath, nspParentDir, tmpPath, titleID);
EditorUtility.DisplayProgressBar("AxibugNSPTools", $"ÖØ½¨NSP", 1f);
Debug.Log($"[AxibugNSPTools]Repacking completed: {outputNSP}"); Debug.Log($"[AxibugNSPTools]Repacking completed: {outputNSP}");
EditorUtility.ClearProgressBar();
#endregion #endregion
EditorUtility.DisplayProgressBar("AxibugNSPTools", $"清理临时目录", 1);
#region
CleanDirectory(Path.Combine(nspParentDir, "repacker_extract"));
CleanDirectory(Path.Combine(Path.GetTempPath(), "NCA"));
CleanDirectory(Path.Combine(nspParentDir, "hacpack_backup"));
#endregion
System.Diagnostics.Process.Start("explorer", "/select,\"" + outputNSP.Trim() + "\"");
EditorUtility.ClearProgressBar();
} }
#region #region
static string GetUserInput() static string GetUserInput()
{ {
Console.Write("Enter the NSP filepath: "); Console.Write("Enter the NSP filepath: ");
return Console.ReadLine(); return Console.ReadLine();
} }
static string ExtractTitleID(string path) static string ExtractTitleID(string path)
{ {
var match = Regex.Match(path, @"0100[\dA-Fa-f]{12}"); var match = Regex.Match(path, @"0100[\dA-Fa-f]{12}");
return match.Success ? match.Value : null; return match.Success ? match.Value : null;
} }
static void CleanDirectory(string path) static void CleanDirectory(string path)
{ {
if (Directory.Exists(path)) if (Directory.Exists(path))
@ -215,8 +225,9 @@ namespace AxibugEmuOnline.Editors
} }
} }
static string ExecuteCommand(string command) static string ExecuteCommand(string command, string workdir)
{ {
Debug.Log($"调用cmd=>{command}");
var process = new System.Diagnostics.Process() var process = new System.Diagnostics.Process()
{ {
StartInfo = new System.Diagnostics.ProcessStartInfo StartInfo = new System.Diagnostics.ProcessStartInfo
@ -228,7 +239,8 @@ namespace AxibugEmuOnline.Editors
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true, CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8, // 明确指定编码 StandardOutputEncoding = Encoding.UTF8, // 明确指定编码
StandardErrorEncoding = Encoding.UTF8 StandardErrorEncoding = Encoding.UTF8,
WorkingDirectory = workdir
} }
}; };
@ -275,36 +287,36 @@ namespace AxibugEmuOnline.Editors
#endregion #endregion
#region NCA构建逻辑 #region NCA构建逻辑
static string BuildProgramNCA(string tmpPath, string titleID, string programDir) static string BuildProgramNCA(string tmpPath, string titleID, string programDir, string workdir)
{ {
string args = $"-k \"{prodKeysPath}\" -o \"{tmpPath}\" --titleid {titleID} " + string args = $"-k \"{prodKeysPath}\" -o \"{tmpPath}\" --titleid {titleID} " +
$"--type nca --ncatype program --exefsdir \"{programDir}/fs0\" " + $"--type nca --ncatype program --exefsdir \"{programDir}/fs0\" " +
$"--romfsdir \"{programDir}/fs1\" --logodir \"{programDir}/fs2\""; $"--romfsdir \"{programDir}/fs1\" --logodir \"{programDir}/fs2\"";
string output = ExecuteCommand($"{tools["hacPack"]} {args}"); string output = ExecuteCommand($"{tools["hacPack"]} {args}", workdir);
return ParseNCAOutput(output, "Program"); return ParseNCAOutput(output, "Program");
} }
static string BuildControlNCA(string tmpPath, string titleID, string controlDir) static string BuildControlNCA(string tmpPath, string titleID, string controlDir, string workdir)
{ {
string args = $"-k \"{prodKeysPath}\" -o \"{tmpPath}\" --titleid {titleID} " + string args = $"-k \"{prodKeysPath}\" -o \"{tmpPath}\" --titleid {titleID} " +
$"--type nca --ncatype control --romfsdir \"{controlDir}/fs0\""; $"--type nca --ncatype control --romfsdir \"{controlDir}/fs0\"";
string output = ExecuteCommand($"{tools["hacPack"]} {args}"); string output = ExecuteCommand($"{tools["hacPack"]} {args}", workdir);
return ParseNCAOutput(output, "Control"); return ParseNCAOutput(output, "Control");
} }
static void BuildMetaNCA(string tmpPath, string titleID, string programNCA, string controlNCA) static void BuildMetaNCA(string tmpPath, string titleID, string programNCA, string controlNCA, string workdir)
{ {
string args = $"-k \"{prodKeysPath}\" -o \"{tmpPath}\" --titleid {titleID} " + string args = $"-k \"{prodKeysPath}\" -o \"{tmpPath}\" --titleid {titleID} " +
$"--type nca --ncatype meta --titletype application " + $"--type nca --ncatype meta --titletype application " +
$"--programnca \"{programNCA}\" --controlnca \"{controlNCA}\""; $"--programnca \"{programNCA}\" --controlnca \"{controlNCA}\"";
ExecuteCommand($"{tools["hacPack"]} {args}"); ExecuteCommand($"{tools["hacPack"]} {args}", workdir);
} }
static string BuildFinalNSP(string origPath, string parentDir, string tmpPath, string titleID) static string BuildFinalNSP(string origPath, string parentDir, string tmpPath, string titleID, string workdir)
{ {
string outputPath = origPath.Replace(".nsp", "_repacked.nsp"); string outputPath = origPath.Replace(".nsp", "_repacked.nsp");
if (File.Exists(outputPath)) File.Delete(outputPath); if (File.Exists(outputPath)) File.Delete(outputPath);
@ -312,7 +324,7 @@ namespace AxibugEmuOnline.Editors
string args = $"-k \"{prodKeysPath}\" -o \"{parentDir}\" --titleid {titleID} " + string args = $"-k \"{prodKeysPath}\" -o \"{parentDir}\" --titleid {titleID} " +
$"--type nsp --ncadir \"{tmpPath}\""; $"--type nsp --ncadir \"{tmpPath}\"";
ExecuteCommand($"{tools["hacPack"]} {args}"); ExecuteCommand($"{tools["hacPack"]} {args}", workdir);
File.Move(Path.Combine(parentDir, $"{titleID}.nsp"), outputPath); File.Move(Path.Combine(parentDir, $"{titleID}.nsp"), outputPath);
return outputPath; return outputPath;
} }
@ -321,7 +333,9 @@ namespace AxibugEmuOnline.Editors
{ {
var line = output.Split('\n') var line = output.Split('\n')
.FirstOrDefault(l => l.Contains($"Created {type} NCA:")); .FirstOrDefault(l => l.Contains($"Created {type} NCA:"));
return line?.Split(':').Last().Trim(); //return line?.Split(':').Last().Trim();
return line?.Substring(line.IndexOf("NCA:") + "NCA:".Length).Trim();
} }
#endregion #endregion
} }

View File

@ -1,5 +1,7 @@
#if UNITY_SWITCH #if UNITY_SWITCH
using nn.fs; using nn.fs;
using System.Security.Cryptography;
#endif #endif
public class AxiNSIO public class AxiNSIO
@ -253,18 +255,194 @@ public class AxiNSIO
#if !UNITY_SWITCH #if !UNITY_SWITCH
return false; return false;
#else #else
//TODO
#if UNITY_SWITCH && !UNITY_EDITOR
// This next line prevents the user from quitting the game while saving.
// This is required for Nintendo Switch Guideline 0080
UnityEngine.Switch.Notification.EnterExitRequestHandlingSection();
#endif #endif
}
public bool DeletePathDir(string filename) if (CheckPathNotFound(filename))
return false;
nn.Result result;
result = nn.fs.File.Delete(filename);
if (result.IsSuccess() == false)
{
UnityEngine.Debug.LogError($"nn.fs.File.Delete ʧ°Ü {filename} : result=>{result.GetErrorInfo()}");
return false;
}
result = nn.fs.FileSystem.Commit(save_name);
if (!result.IsSuccess())
{
UnityEngine.Debug.LogError($"FileSystem.Commit({save_name}) ʧ°Ü: " + result.GetErrorInfo());
return false;
}
return true;
#if UNITY_SWITCH && !UNITY_EDITOR
// End preventing the user from quitting the game while saving.
UnityEngine.Switch.Notification.LeaveExitRequestHandlingSection();
#endif
#endif
}
public bool DeletePathDir(string filename)
{ {
#if !UNITY_SWITCH #if !UNITY_SWITCH
return false; return false;
#else #else
//TODO
#if UNITY_SWITCH && !UNITY_EDITOR
// This next line prevents the user from quitting the game while saving.
// This is required for Nintendo Switch Guideline 0080
UnityEngine.Switch.Notification.EnterExitRequestHandlingSection();
#endif #endif
}
bool EnsureParentDirectory(string filePath, bool bAutoCreateDir = true) if (CheckPathNotFound(filename))
return false;
nn.Result result;
result = nn.fs.Directory.Delete(filename);
if (result.IsSuccess() == false)
{
UnityEngine.Debug.LogError($"nn.fs.File.Delete ʧ°Ü {filename} : result=>{result.GetErrorInfo()}");
return false;
}
result = nn.fs.FileSystem.Commit(save_name);
if (!result.IsSuccess())
{
UnityEngine.Debug.LogError($"FileSystem.Commit({save_name}) ʧ°Ü: " + result.GetErrorInfo());
return false;
}
return true;
#if UNITY_SWITCH && !UNITY_EDITOR
// End preventing the user from quitting the game while saving.
UnityEngine.Switch.Notification.LeaveExitRequestHandlingSection();
#endif
#endif
}
/// <summary>
/// µÝ¹éɾ³ýĿ¼
/// </summary>
/// <param name="filename"></param>
/// <returns></returns>
public bool DeleteRecursivelyPathDir(string filename)
{
#if !UNITY_SWITCH
return false;
#else
#if UNITY_SWITCH && !UNITY_EDITOR
// This next line prevents the user from quitting the game while saving.
// This is required for Nintendo Switch Guideline 0080
UnityEngine.Switch.Notification.EnterExitRequestHandlingSection();
#endif
if (CheckPathNotFound(filename))
return false;
nn.Result result;
result = nn.fs.Directory.DeleteRecursively(filename);
if (result.IsSuccess() == false)
{
UnityEngine.Debug.LogError($"nn.fs.File.DeleteRecursively ʧ°Ü {filename} : result=>{result.GetErrorInfo()}");
return false;
}
result = nn.fs.FileSystem.Commit(save_name);
if (!result.IsSuccess())
{
UnityEngine.Debug.LogError($"FileSystem.Commit({save_name}) ʧ°Ü: " + result.GetErrorInfo());
return false;
}
return true;
#if UNITY_SWITCH && !UNITY_EDITOR
// End preventing the user from quitting the game while saving.
UnityEngine.Switch.Notification.LeaveExitRequestHandlingSection();
#endif
#endif
}
/// <summary>
/// µÝ¹éɾ³ýÇé¿ö
/// </summary>
/// <param name="filename"></param>
/// <returns></returns>
public bool CleanRecursivelyPathDir(string filename)
{
#if !UNITY_SWITCH
return false;
#else
#if UNITY_SWITCH && !UNITY_EDITOR
// This next line prevents the user from quitting the game while saving.
// This is required for Nintendo Switch Guideline 0080
UnityEngine.Switch.Notification.EnterExitRequestHandlingSection();
#endif
if (CheckPathNotFound(filename))
return false;
nn.Result result;
result = nn.fs.Directory.CleanRecursively(filename);
if (result.IsSuccess() == false)
{
UnityEngine.Debug.LogError($"nn.fs.File.DeleteRecursively ʧ°Ü {filename} : result=>{result.GetErrorInfo()}");
return false;
}
result = nn.fs.FileSystem.Commit(save_name);
if (!result.IsSuccess())
{
UnityEngine.Debug.LogError($"FileSystem.Commit({save_name}) ʧ°Ü: " + result.GetErrorInfo());
return false;
}
return true;
#if UNITY_SWITCH && !UNITY_EDITOR
// End preventing the user from quitting the game while saving.
UnityEngine.Switch.Notification.LeaveExitRequestHandlingSection();
#endif
#endif
}
public bool RenameDir(string oldpath,string newpath)
{
#if !UNITY_SWITCH
return false;
#else
#if UNITY_SWITCH && !UNITY_EDITOR
// This next line prevents the user from quitting the game while saving.
// This is required for Nintendo Switch Guideline 0080
UnityEngine.Switch.Notification.EnterExitRequestHandlingSection();
#endif
if (CheckPathNotFound(oldpath))
return false;
nn.Result result;
result = nn.fs.Directory.Rename(oldpath, newpath);
if (result.IsSuccess() == false)
{
UnityEngine.Debug.LogError($"nn.fs.File.Rename ʧ°Ü {oldpath} to {newpath} : result=>{result.GetErrorInfo()}");
return false;
}
result = nn.fs.FileSystem.Commit(save_name);
if (!result.IsSuccess())
{
UnityEngine.Debug.LogError($"FileSystem.Commit({save_name}) ʧ°Ü: " + result.GetErrorInfo());
return false;
}
return true;
#if UNITY_SWITCH && !UNITY_EDITOR
// End preventing the user from quitting the game while saving.
UnityEngine.Switch.Notification.LeaveExitRequestHandlingSection();
#endif
#endif
}
bool EnsureParentDirectory(string filePath, bool bAutoCreateDir = true)
{ {
#if !UNITY_SWITCH #if !UNITY_SWITCH
return false; return false;

View File

@ -5,10 +5,12 @@ public class AxiNSMount
{ {
static bool bInMount = false; static bool bInMount = false;
internal static string m_SaveMountName; internal static string m_SaveMountName;
static bool bInMountForDebug = false; static bool bInSdCardMount = false;
internal static string m_SaveMountForDebugName; internal static string m_SdCardMountName;
static bool bInSdCardDebugMount = false;
internal static string m_SdCardDebugMountName;
public bool SaveIsMount => bInMount; public bool SaveIsMount => bInMount;
public string SaveMountName public string SaveMountName
{ {
get get
@ -47,14 +49,14 @@ public class AxiNSMount
bInMount = true; bInMount = true;
return true; return true;
} }
#endif #endif
public bool MountSDForDebug(string mountName = "sd") public bool MountSDForDebug(string mountName = "dbgsd")
{ {
#if !UNITY_SWITCH #if !UNITY_SWITCH
return false; return false;
#else #else
if (bInMountForDebug) if (bInSdCardDebugMount)
return true; return true;
nn.Result result; nn.Result result;
result = nn.fs.SdCard.MountForDebug(mountName); result = nn.fs.SdCard.MountForDebug(mountName);
@ -65,35 +67,31 @@ public class AxiNSMount
return false; return false;
} }
UnityEngine.Debug.Log($"nn_fs_MountSdCardForDebug->挂载{mountName}:/ 成功 "); UnityEngine.Debug.Log($"nn_fs_MountSdCardForDebug->挂载{mountName}:/ 成功 ");
m_SaveMountForDebugName = mountName; m_SdCardDebugMountName = mountName;
bInMountForDebug = true; bInSdCardDebugMount = true;
return true; return true;
#endif #endif
} }
public bool MountSD(string mountName = "sd") public bool MountSD(string mountName = "sd")
{ {
#if !UNITY_SWITCH #if !UNITY_SWITCH
return false; return false;
#else #else
if (bInMountForDebug) if (bInSdCardMount)
return true; return true;
nn.Result result; nn.Result result;
result = nn.fs.SdCard.Mount(mountName); result = AxiNSSDCard.Mount(mountName);
//result.abortUnlessSuccess();
if (!result.IsSuccess()) if (!result.IsSuccess())
{ {
UnityEngine.Debug.LogError($"nn_fs_MountSdCard->挂载{mountName}:/ 失败: " + result.ToString()); UnityEngine.Debug.LogError($"nn_fs_MountSdCard->挂载{mountName}:/ 失败: " + result.ToString());
return false; return false;
} }
UnityEngine.Debug.Log($"nn_fs_MountSdCard->挂载{mountName}:/ 成功 "); UnityEngine.Debug.Log($"nn_fs_MountSdCard->挂载{mountName}:/ 成功 ");
m_SaveMountForDebugName = mountName; m_SdCardMountName = mountName;
bInMountForDebug = true; bInSdCardMount = true;
return true; return true;
#endif #endif
} }
public void UnmountSave() public void UnmountSave()
{ {
#if UNITY_SWITCH #if UNITY_SWITCH
@ -107,18 +105,30 @@ public class AxiNSMount
bInMount = false; bInMount = false;
#endif #endif
} }
public void UnmountSDCardForDebug()
public void UnmountSaveForDebug()
{ {
#if UNITY_SWITCH #if UNITY_SWITCH
if (!bInMountForDebug) if (!bInSdCardDebugMount)
{ {
UnityEngine.Debug.LogError($"{m_SaveMountForDebugName}:/ 没有被挂载,无需卸载"); UnityEngine.Debug.LogError($"{m_SdCardDebugMountName}:/ 没有被挂载,无需卸载");
return; return;
} }
nn.fs.FileSystem.Unmount(m_SaveMountForDebugName); nn.fs.FileSystem.Unmount(m_SdCardDebugMountName);
UnityEngine.Debug.LogError($"UnmountSaveForDebufa->已卸载{m_SaveMountForDebugName}:/ "); UnityEngine.Debug.LogError($"UnmountSDCardForDebug->已卸载{m_SdCardDebugMountName}:/ ");
bInMountForDebug = false; bInSdCardDebugMount = false;
#endif #endif
} }
public void UnmountSDCard()
{
#if UNITY_SWITCH
if (!bInSdCardMount)
{
UnityEngine.Debug.LogError($"{m_SdCardMountName}:/ 没有被挂载,无需卸载");
return;
}
nn.fs.FileSystem.Unmount(m_SdCardMountName);
UnityEngine.Debug.LogError($"UnmountSDCard->已卸载{m_SdCardMountName}:/ ");
bInSdCardMount = false;
#endif
}
} }

View File

@ -0,0 +1,23 @@
#if UNITY_SWITCH
using nn.account;
#endif
public class AxiNSSDCard
{
#if UNITY_SWITCH
#if DEVELOPMENT_BUILD || NN_FS_SD_CARD_FOR_DEBUG_ENABLE
[DllImport(Nn.DllName,
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "nn_fs_MountSdCard")]
public static extern nn.Result Mount(string name);
#else
public static nn.Result Mount(string name)
{
return new nn.Result();
}
#endif
#endif
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 21fa04ba4da10d74aafd65dd138478b7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: