189c0c9c72
* Implement Modding Support * Executables: Rewrite to use contiguous mem and Spans * Reorder ExeFs, Npdm, ControlData and SaveData calls After discussion with gdkchan, it was decided it's best to call LoadExeFs after all other loads are done as it starts the guest process. * Build RomFs manually instead of Layering FS Layered FS approach has considerable latency when building the final romfs. So, we manually replace files in a single romfs instance. * Add RomFs modding via storage file * Fix and cleanup MemPatch * Add dynamically loaded NRO patching * Support exefs file replacement * Rewrite ModLoader to use mods-search architecture * Disable PPTC when exefs patches are detected Disable PPTC on exefs replacements too * Rewrite ModLoader, again * Increased maintainability and matches Atmosphere closely * Creates base mods structure if it doesn't exist * Add Exefs partition replacement * IPSwitch: Fix nsobid parsing * Move mod logs to new LogClass * Allow custom suffixes to title dirs again * Address nits * Add a per-App "Open Mods Directory" context menu item Creates the path if not present. * Normalize tooltips verbiage * Use LocalStorage and remove unused namespaces
262 lines
7.7 KiB
C#
262 lines
7.7 KiB
C#
using Ryujinx.Common.Logging;
|
|
using System;
|
|
using System.IO;
|
|
using System.Text;
|
|
|
|
namespace Ryujinx.HLE.Loaders.Mods
|
|
{
|
|
class IPSwitchPatcher
|
|
{
|
|
readonly StreamReader _reader;
|
|
public string BuildId { get; }
|
|
|
|
const string BidHeader = "@nsobid-";
|
|
|
|
public IPSwitchPatcher(StreamReader reader)
|
|
{
|
|
string header = reader.ReadLine();
|
|
if (header == null || !header.StartsWith(BidHeader))
|
|
{
|
|
Logger.PrintError(LogClass.ModLoader, "IPSwitch: Malformed PCHTXT file. Skipping...");
|
|
return;
|
|
}
|
|
|
|
_reader = reader;
|
|
BuildId = header.Substring(BidHeader.Length).TrimEnd().TrimEnd('0');
|
|
}
|
|
|
|
private enum Token
|
|
{
|
|
Normal,
|
|
String,
|
|
EscapeChar,
|
|
Comment
|
|
}
|
|
|
|
// Uncomments line and unescapes C style strings within
|
|
private static string PreprocessLine(string line)
|
|
{
|
|
StringBuilder str = new StringBuilder();
|
|
Token state = Token.Normal;
|
|
|
|
for (int i = 0; i < line.Length; ++i)
|
|
{
|
|
char c = line[i];
|
|
char la = i + 1 != line.Length ? line[i + 1] : '\0';
|
|
|
|
switch (state)
|
|
{
|
|
case Token.Normal:
|
|
state = c == '"' ? Token.String :
|
|
c == '/' && la == '/' ? Token.Comment :
|
|
c == '/' && la != '/' ? Token.Comment : // Ignore error and stop parsing
|
|
Token.Normal;
|
|
break;
|
|
case Token.String:
|
|
state = c switch
|
|
{
|
|
'"' => Token.Normal,
|
|
'\\' => Token.EscapeChar,
|
|
_ => Token.String
|
|
};
|
|
break;
|
|
case Token.EscapeChar:
|
|
state = Token.String;
|
|
c = c switch
|
|
{
|
|
'a' => '\a',
|
|
'b' => '\b',
|
|
'f' => '\f',
|
|
'n' => '\n',
|
|
'r' => '\r',
|
|
't' => '\t',
|
|
'v' => '\v',
|
|
'\\' => '\\',
|
|
_ => '?'
|
|
};
|
|
break;
|
|
}
|
|
|
|
if (state == Token.Comment) break;
|
|
|
|
if (state < Token.EscapeChar)
|
|
{
|
|
str.Append(c);
|
|
}
|
|
}
|
|
|
|
return str.ToString().Trim();
|
|
}
|
|
|
|
static int ParseHexByte(byte c)
|
|
{
|
|
if (c >= '0' && c <= '9')
|
|
{
|
|
return c - '0';
|
|
}
|
|
else if (c >= 'A' && c <= 'F')
|
|
{
|
|
return c - 'A' + 10;
|
|
}
|
|
else if (c >= 'a' && c <= 'f')
|
|
{
|
|
return c - 'a' + 10;
|
|
}
|
|
else
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Big Endian
|
|
static byte[] Hex2ByteArrayBE(string hexstr)
|
|
{
|
|
if ((hexstr.Length & 1) == 1) return null;
|
|
|
|
byte[] bytes = new byte[hexstr.Length >> 1];
|
|
|
|
for (int i = 0; i < hexstr.Length; i += 2)
|
|
{
|
|
int high = ParseHexByte((byte)hexstr[i]);
|
|
int low = ParseHexByte((byte)hexstr[i + 1]);
|
|
bytes[i >> 1] = (byte)((high << 4) | low);
|
|
}
|
|
|
|
return bytes;
|
|
}
|
|
|
|
// Auto base discovery
|
|
private static bool ParseInt(string str, out int value)
|
|
{
|
|
if (str[0] == '0' && (str[1] == 'x' || str[1] == 'X'))
|
|
{
|
|
return Int32.TryParse(str.Substring(2), System.Globalization.NumberStyles.HexNumber, null, out value);
|
|
}
|
|
else
|
|
{
|
|
return Int32.TryParse(str, System.Globalization.NumberStyles.Integer, null, out value);
|
|
}
|
|
}
|
|
|
|
private MemPatch Parse()
|
|
{
|
|
if (_reader == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
MemPatch patches = new MemPatch();
|
|
|
|
bool enabled = true;
|
|
bool printValues = false;
|
|
int offset_shift = 0;
|
|
|
|
string line;
|
|
int lineNum = 0;
|
|
|
|
static void Print(string s) => Logger.PrintInfo(LogClass.ModLoader, $"IPSwitch: {s}");
|
|
|
|
void ParseWarn() => Logger.PrintWarning(LogClass.ModLoader, $"IPSwitch: Parse error at line {lineNum} for bid={BuildId}");
|
|
|
|
while ((line = _reader.ReadLine()) != null)
|
|
{
|
|
line = PreprocessLine(line);
|
|
lineNum += 1;
|
|
|
|
if (line.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
else if (line.StartsWith('#'))
|
|
{
|
|
Print(line);
|
|
}
|
|
else if (line.StartsWith("@stop"))
|
|
{
|
|
break;
|
|
}
|
|
else if (line.StartsWith("@enabled"))
|
|
{
|
|
enabled = true;
|
|
}
|
|
else if (line.StartsWith("@disabled"))
|
|
{
|
|
enabled = false;
|
|
}
|
|
else if (line.StartsWith("@flag"))
|
|
{
|
|
var tokens = line.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (tokens.Length < 2)
|
|
{
|
|
ParseWarn();
|
|
continue;
|
|
}
|
|
|
|
if (tokens[1] == "offset_shift")
|
|
{
|
|
if (tokens.Length != 3 || !ParseInt(tokens[2], out offset_shift))
|
|
{
|
|
ParseWarn();
|
|
continue;
|
|
}
|
|
}
|
|
else if (tokens[1] == "print_values")
|
|
{
|
|
printValues = true;
|
|
}
|
|
}
|
|
else if (line.StartsWith('@'))
|
|
{
|
|
// Ignore
|
|
}
|
|
else
|
|
{
|
|
if (!enabled)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var tokens = line.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (tokens.Length < 2)
|
|
{
|
|
ParseWarn();
|
|
continue;
|
|
}
|
|
|
|
if (!Int32.TryParse(tokens[0], System.Globalization.NumberStyles.HexNumber, null, out int offset))
|
|
{
|
|
ParseWarn();
|
|
continue;
|
|
}
|
|
|
|
offset += offset_shift;
|
|
|
|
if (printValues)
|
|
{
|
|
Print($"print_values 0x{offset:x} <= {tokens[1]}");
|
|
}
|
|
|
|
if (tokens[1][0] == '"')
|
|
{
|
|
var patch = Encoding.ASCII.GetBytes(tokens[1].Trim('"'));
|
|
patches.Add((uint)offset, patch);
|
|
}
|
|
else
|
|
{
|
|
var patch = Hex2ByteArrayBE(tokens[1]);
|
|
patches.Add((uint)offset, patch);
|
|
}
|
|
}
|
|
}
|
|
|
|
return patches;
|
|
}
|
|
|
|
public void AddPatches(MemPatch patches)
|
|
{
|
|
patches.AddFrom(Parse());
|
|
}
|
|
}
|
|
} |