Split PathUtil from Util, add more xmldoc

This commit is contained in:
Kurt 2025-06-08 16:33:31 -05:00
parent 5b70ca0397
commit ccfa58e5f1
32 changed files with 247 additions and 101 deletions

View File

@ -119,7 +119,7 @@ private static int GetSlotCountForBox(int boxSlotCount, int box, int total)
private static string GetFolderName(SaveFile sav, int box, BoxExportFolderNaming mode)
{
var boxName = sav is IBoxDetailNameRead r ? r.GetBoxName(box) : BoxDetailNameExtensions.GetDefaultBoxName(box);
boxName = Util.CleanFileName(boxName);
boxName = PathUtil.CleanFileName(boxName);
return mode switch
{
BoxExportFolderNaming.BoxName => boxName,
@ -132,7 +132,7 @@ private static string GetFolderName(SaveFile sav, int box, BoxExportFolderNaming
private static string GetFileName(PKM pk, BoxExportIndexPrefix mode, IFileNamer<PKM> namer, int box, int slot, int boxSlotCount)
{
var slotName = GetInnerName(namer, pk);
var fileName = Util.CleanFileName(slotName);
var fileName = PathUtil.CleanFileName(slotName);
var prefix = GetPrefix(mode, box, slot, boxSlotCount);
return $"{prefix}{fileName}.{pk.Extension}";
@ -152,7 +152,7 @@ private static string GetInnerName(IFileNamer<PKM> namer, PKM pk)
try
{
var slotName = namer.GetName(pk);
return Util.CleanFileName(slotName);
return PathUtil.CleanFileName(slotName);
}
catch { return "Name Error"; }
}

View File

@ -210,7 +210,7 @@ public IReadOnlyList<ComboItem> GetLocationList(GameVersion version, EntityConte
return MetGen2;
IReadOnlyList<ComboItem> result;
if (egg && version < W && context.Generation() >= 5)
if (egg && version < W && context is not (EntityContext.Gen3 or EntityContext.Gen4))
result = MetGen4;
else
result = GetLocationListInternal(version, context);

View File

@ -13,21 +13,26 @@ namespace PKHeX.Core;
public sealed class EvolutionTree : EvolutionNetwork
{
public const int MaxEvolutions = 3;
public static readonly EvolutionTree Evolves1 = GetViaSpecies (PersonalTable.Y, EvolutionSet.GetArray(GetReader("g1", "g1"u8)));
public static readonly EvolutionTree Evolves2 = GetViaSpecies (PersonalTable.C, EvolutionSet.GetArray(GetReader("g2", "g2"u8)));
public static readonly EvolutionTree Evolves3 = GetViaSpecies (PersonalTable.RS, EvolutionSet.GetArray(GetReader("g3", "g3"u8)));
public static readonly EvolutionTree Evolves4 = GetViaSpecies (PersonalTable.DP, EvolutionSet.GetArray(GetReader("g4", "g4"u8)));
public static readonly EvolutionTree Evolves5 = GetViaSpecies (PersonalTable.BW, EvolutionSet.GetArray(GetReader("g5", "g5"u8)));
public static readonly EvolutionTree Evolves6 = GetViaSpecies (PersonalTable.AO, EvolutionSet.GetArray(GetReader("g6", "g6"u8)));
public static readonly EvolutionTree Evolves7 = GetViaPersonal(PersonalTable.USUM, EvolutionSet.GetArray(GetReader("uu", "uu"u8)));
public static readonly EvolutionTree Evolves7b = GetViaPersonal(PersonalTable.GG, EvolutionSet.GetArray(GetReader("gg", "gg"u8)));
public static readonly EvolutionTree Evolves8 = GetViaPersonal(PersonalTable.SWSH, EvolutionSet.GetArray(GetReader("ss", "ss"u8)));
public static readonly EvolutionTree Evolves8a = GetViaPersonal(PersonalTable.LA, EvolutionSet.GetArray(GetReader("la", "la"u8), 0));
public static readonly EvolutionTree Evolves8b = GetViaPersonal(PersonalTable.BDSP, EvolutionSet.GetArray(GetReader("bs", "bs"u8)));
public static readonly EvolutionTree Evolves9 = GetViaPersonal(PersonalTable.SV, EvolutionSet.GetArray(GetReader("sv", "sv"u8)));
public static readonly EvolutionTree Evolves1 = GetViaSpecies (PersonalTable.Y, Get("g1", "g1"u8));
public static readonly EvolutionTree Evolves2 = GetViaSpecies (PersonalTable.C, Get("g2", "g2"u8));
public static readonly EvolutionTree Evolves3 = GetViaSpecies (PersonalTable.RS, Get("g3", "g3"u8));
public static readonly EvolutionTree Evolves4 = GetViaSpecies (PersonalTable.DP, Get("g4", "g4"u8));
public static readonly EvolutionTree Evolves5 = GetViaSpecies (PersonalTable.BW, Get("g5", "g5"u8));
public static readonly EvolutionTree Evolves6 = GetViaSpecies (PersonalTable.AO, Get("g6", "g6"u8));
public static readonly EvolutionTree Evolves7 = GetViaPersonal(PersonalTable.USUM, Get("uu", "uu"u8));
public static readonly EvolutionTree Evolves7b = GetViaPersonal(PersonalTable.GG, Get("gg", "gg"u8));
public static readonly EvolutionTree Evolves8 = GetViaPersonal(PersonalTable.SWSH, Get("ss", "ss"u8));
public static readonly EvolutionTree Evolves8a = GetViaPersonal(PersonalTable.LA, Get("la", "la"u8, 0));
public static readonly EvolutionTree Evolves8b = GetViaPersonal(PersonalTable.BDSP, Get("bs", "bs"u8));
public static readonly EvolutionTree Evolves9 = GetViaPersonal(PersonalTable.SV, Get("sv", "sv"u8));
private static EvolutionMethod[][] Get([ConstantExpected] string resource, [Length(2, 2)] ReadOnlySpan<byte> identifier, [ConstantExpected] byte levelUp = 1)
{
var data = Util.GetBinaryResource($"evos_{resource}.pkl");
var bla = BinLinkerAccessor16.Get(data, identifier);
return EvolutionSet.GetArray(bla, levelUp);
}
private static ReadOnlySpan<byte> GetResource([ConstantExpected] string resource) => Util.GetBinaryResource($"evos_{resource}.pkl");
private static BinLinkerAccessor16 GetReader([ConstantExpected] string resource, [Length(2, 2)] ReadOnlySpan<byte> identifier) => BinLinkerAccessor16.Get(GetResource(resource), identifier);
private EvolutionTree(IEvolutionForward forward, IEvolutionReverse reverse) : base(forward, reverse) { }
private static EvolutionTree GetViaSpecies(IPersonalTable t, EvolutionMethod[][] entries)

View File

@ -7,7 +7,7 @@ namespace PKHeX.Core;
/// Stores parsed data about how a move was learned.
/// </summary>
/// <param name="Info">Info about the game it was learned in.</param>
/// <param name="EvoStage">Evolution stage index within the <see cref="MoveLearnInfo.Environment"/> evolution list it existed in.</param>
/// <param name="EvoStage">Evolution stage index within the <see cref="EntityContext"/> evolution list it existed in.</param>
/// <param name="Generation">Rough indicator of generation the <see cref="MoveLearnInfo.Environment"/> was.</param>
/// <param name="Expect">Optional value used when the move is not legal, to indicate that another move ID should have been in that move slot instead.</param>
public readonly record struct MoveResult(MoveLearnInfo Info, byte EvoStage = 0, byte Generation = 0, ushort Expect = 0)
@ -15,7 +15,8 @@ namespace PKHeX.Core;
public bool IsParsed => this != default;
public bool Valid => Info.Method.IsValid();
internal MoveResult(LearnMethod method, LearnEnvironment game = 0) : this(new MoveLearnInfo(method, game), Generation: game.GetGeneration()) { }
internal MoveResult(LearnMethod method, LearnEnvironment game) : this(new MoveLearnInfo(method, game), Generation: game.GetGeneration()) { }
private MoveResult(LearnMethod method) : this(new MoveLearnInfo(method, LearnEnvironment.None)) { }
public string Summary(ISpeciesForm current, EvolutionHistory history)
{
@ -66,6 +67,7 @@ private EvoCriteria GetDetail(EvolutionHistory history)
public static readonly MoveResult Duplicate = new(LearnMethod.Duplicate);
public static readonly MoveResult EmptyInvalid = new(LearnMethod.EmptyInvalid);
public static readonly MoveResult Sketch = new(LearnMethod.Sketch);
public static MoveResult Unobtainable(ushort expect) => new(LearnMethod.UnobtainableExpect) { Expect = expect };
public static MoveResult Unobtainable() => new(LearnMethod.Unobtainable);

View File

@ -7,9 +7,6 @@ namespace PKHeX.Core;
/// </summary>
internal static class LearnVerifier
{
private static readonly MoveResult Duplicate = new(LearnMethod.Duplicate);
private static readonly MoveResult EmptyInvalid = new(LearnMethod.EmptyInvalid);
public static void Verify(Span<MoveResult> result, PKM pk, IEncounterTemplate enc, EvolutionHistory history)
{
// Clear any existing parse results.
@ -46,7 +43,7 @@ private static void Finalize(Span<MoveResult> result, ReadOnlySpan<ushort> curre
// Can't have empty first move.
if (current[0] == 0)
result[0] = EmptyInvalid;
result[0] = MoveResult.EmptyInvalid;
}
private static void VerifyNoEmptyDuplicates(Span<MoveResult> result, ReadOnlySpan<ushort> current)
@ -80,7 +77,7 @@ private static void FlagDuplicateMovesAfterIndex(Span<MoveResult> result, ReadOn
{
if (current[i] != move)
continue;
result[index] = Duplicate;
result[index] = MoveResult.Duplicate;
return;
}
}
@ -91,7 +88,7 @@ private static void FlagEmptySlotsBeforeIndex(Span<MoveResult> result, ReadOnlyS
{
if (current[i] != 0)
return;
result[i] = EmptyInvalid;
result[i] = MoveResult.EmptyInvalid;
}
}

View File

@ -114,9 +114,6 @@ public static void ChangeFormArgument(this IFormArgument f, ushort species, byte
public static uint GetFormArgumentMax(ushort species, byte form, EntityContext context)
{
var gen = context.Generation();
if (gen <= 5)
return 0;
return species switch
{
(int)Furfrou when form != 0 => 5,

View File

@ -17,9 +17,12 @@ public abstract class PKM : ISpeciesForm, ITrainerID32, IGeneration, IShiny, ILa
public abstract int SIZE_STORED { get; }
public string Extension => GetType().Name.ToLowerInvariant();
public abstract PersonalInfo PersonalInfo { get; }
/// <summary>
/// Bytes in the data structure that are unused, either as alignment padding, or were reserved and never used.
/// </summary>
public virtual ReadOnlySpan<ushort> ExtraBytes => [];
// Internal Attributes set on creation
public readonly byte[] Data; // Raw Storage
protected PKM(byte[] data) => Data = data;

View File

@ -45,9 +45,22 @@ public static string[] GetFormList(ushort species, IReadOnlyList<string> types,
};
}
/// <summary>
/// Determines whether Mega Pokémon forms exist in the specified <see cref="EntityContext"/>.
/// </summary>
private static bool IsMegaContext(this EntityContext context) => context is Gen6 or Gen7 or Gen7b;
/// <summary>
/// Used to indicate that the form list is a single form, so no name is specified.
/// </summary>
private static readonly string[] EMPTY = [string.Empty];
/// <summary>
/// Lets Go, Pikachu! &amp; Eevee! Starter form name.
/// </summary>
/// <remarks>
/// Different from the "Partner Cap" form.
/// </remarks>
private const string Starter = nameof(Starter);
private static string[] GetFormsGen1(ushort species, IReadOnlyList<string> types, IReadOnlyList<string> forms, EntityContext context)

View File

@ -13,6 +13,13 @@ public interface IRibbonSetAffixed
public static class AffixedRibbon
{
/// <summary>
/// Value present when no ribbon is affixed.
/// </summary>
public const sbyte None = -1;
/// <summary>
/// Represents the maximum allowable value for an affixed ribbon index.
/// </summary>
public const sbyte Max = (sbyte)RibbonIndex.MAX_COUNT - 1;
}

View File

@ -46,17 +46,19 @@ public int MaxCount
}
}
/// <summary>
/// Gets a list of all ribbons available for the entity and their state.
/// </summary>
public static List<RibbonInfo> GetRibbonInfo(PKM pk)
{
// Get a list of all Ribbon Attributes in the PKM
var riblist = new List<RibbonInfo>();
var names = ReflectUtil.GetPropertiesStartWithPrefix(pk.GetType(), PropertyPrefix);
foreach (var name in names)
{
object? RibbonValue = ReflectUtil.GetValue(pk, name);
if (RibbonValue is bool b)
var value = ReflectUtil.GetValue(pk, name);
if (value is bool b)
riblist.Add(new RibbonInfo(name, b));
else if (RibbonValue is byte x)
else if (value is byte x)
riblist.Add(new RibbonInfo(name, x));
}
return riblist;

View File

@ -102,7 +102,7 @@ private static string GetFileName(string path, string bak)
var fileName = Path.GetFileName(path);
// Trim off existing backup name if present
var bakName = Util.CleanFileName(bak);
var bakName = PathUtil.CleanFileName(bak);
if (fileName.EndsWith(bakName, StringComparison.Ordinal))
fileName = fileName[..^bakName.Length];
@ -159,7 +159,7 @@ private static string TrimNames(string fileName, ReadOnlySpan<string> extensions
public string GetBackupFileName(string destDir)
{
return Path.Combine(destDir, Util.CleanFileName(BAKName));
return Path.Combine(destDir, PathUtil.CleanFileName(BAKName));
}
private void SetAsBlank()

View File

@ -38,12 +38,12 @@ public static int DumpBoxes(this SaveFile sav, string path, bool boxFolders = fa
if (boxFolders)
{
string boxName = sav is IBoxDetailName bn ? bn.GetBoxName(box) : BoxDetailNameExtensions.GetDefaultBoxName(box);
boxName = Util.CleanFileName(boxName);
boxName = PathUtil.CleanFileName(boxName);
boxFolder = Path.Combine(path, boxName);
Directory.CreateDirectory(boxFolder);
}
var fileName = Util.CleanFileName(pk.FileName);
var fileName = PathUtil.CleanFileName(pk.FileName);
var fn = Path.Combine(boxFolder, fileName);
if (File.Exists(fn))
continue;
@ -76,7 +76,7 @@ public static int DumpBox(this SaveFile sav, string path, int currentBox)
if (pk.Species == 0 || !pk.Valid || box != currentBox)
continue;
var fileName = Path.Combine(path, Util.CleanFileName(pk.FileName));
var fileName = Path.Combine(path, PathUtil.CleanFileName(pk.FileName));
if (File.Exists(fileName))
continue;

View File

@ -128,15 +128,18 @@ public static ComboItem[] GetVariedCBListBall(ReadOnlySpan<string> itemNames, Re
for (int i = 0; i < ballItemID.Length; i++)
list[i] = new ComboItem(itemNames[ballItemID[i]], ballIndex[i]);
// 3 Balls are preferentially first, sort Master Ball with the rest Alphabetically.
// First 3 Balls (Poke, Great, Ultra) are preferentially first, sort Master Ball with the rest Alphabetically.
list.AsSpan(3).Sort(Comparer);
return list;
}
/// <summary>
/// Comparer for <see cref="ComboItem"/> based on the <see cref="ComboItem.Text"/> property.
/// </summary>
private static readonly FunctorComparer<ComboItem> Comparer =
new((a, b) => string.CompareOrdinal(a.Text, b.Text));
private sealed class FunctorComparer<T>(Comparison<T> Comparison) : IComparer<T>
private sealed class FunctorComparer<T>(Comparison<T> Comparison) : IComparer<T> where T : notnull
{
public int Compare(T? x, T? y)
{

View File

@ -2,6 +2,9 @@
namespace PKHeX.Core;
/// <summary>
/// Logic for handling date and time values, including validation and conversion to/from seconds elapsed since a specific epoch.
/// </summary>
public static class DateUtil
{
/// <summary>
@ -102,12 +105,5 @@ public static DateOnly GetRandomDateWithin(DateOnly start, DateOnly end, Random
/// <summary>
/// Checks if the given time components represent a valid time.
/// </summary>
/// <param name="receivedHour"></param>
/// <param name="receivedMinute"></param>
/// <param name="receivedSecond"></param>
/// <returns></returns>
public static bool IsValidTime(byte receivedHour, byte receivedMinute, byte receivedSecond)
{
return receivedHour < 24u && receivedMinute < 60u && receivedSecond < 60u;
}
public static bool IsValidTime(byte hour, byte minute, byte second) => hour < 24u && minute < 60u && second < 60u;
}

View File

@ -86,6 +86,22 @@ public static long GetFileSize(string path)
catch { return -1; } // Bad File / Locked
}
/// <summary>
/// Safely iterates over the elements of the specified <see cref="IEnumerable{T}"/>, handling exceptions during enumeration.
/// </summary>
/// <remarks>
/// This method ensures that exceptions thrown during enumeration do not terminate the iteration prematurely.
/// Instead, it logs the exception (if a <paramref name="log"/> action is provided) and continues iterating until the specified <paramref name="failOut"/> limit is reached.
/// If the limit is exceeded, the iteration stops.
/// </remarks>
/// <typeparam name="T">The type of elements in the source collection.</typeparam>
/// <param name="source">The source collection to iterate over. Cannot be <see langword="null"/>.</param>
/// <param name="failOut">
/// The maximum number of consecutive exceptions allowed before the iteration is terminated.
/// Must be greater than or equal to 0.
/// </param>
/// <param name="log">An optional action to log or handle exceptions that occur during enumeration. If <see langword="null"/>, exceptions are ignored.</param>
/// <returns>An <see cref="IEnumerable{T}"/> that yields elements from the source collection, skipping over elements that cause exceptions.</returns>
public static IEnumerable<T> IterateSafe<T>(this IEnumerable<T> source, int failOut = 10, Action<Exception>? log = null)
{
using var enumerator = source.GetEnumerator();
@ -306,7 +322,7 @@ public static string GetPKMTempFileName(PKM pk, bool encrypt)
string fn = pk.FileNameWithoutExtension;
string filename = fn + (encrypt ? $".ek{pk.Format}" : $".{pk.Extension}");
return Path.Combine(Path.GetTempPath(), Util.CleanFileName(filename));
return Path.Combine(Path.GetTempPath(), PathUtil.CleanFileName(filename));
}
/// <summary>
@ -342,8 +358,15 @@ public static string GetPKMTempFileName(PKM pk, bool encrypt)
/// <param name="Count">Count of objects</param>
public sealed record ConcatenatedEntitySet(Memory<byte> Data, int Count)
{
/// <summary>
/// Size of each Entity in bytes.
/// </summary>
public int SlotSize => Data.Length / Count;
/// <summary>
/// Retrieves a specific slot of data from the concatenated set.
/// </summary>
/// <param name="index">Slot index to retrieve.</param>
public Span<byte> GetSlot(int index)
{
var size = SlotSize;

View File

@ -19,7 +19,8 @@ public static bool GetFlag(ReadOnlySpan<byte> arr, int offset, int bitIndex)
return ((arr[offset] >> bitIndex) & 1) != 0;
}
public static bool GetFlag(ReadOnlySpan<byte> arr, int index) => GetFlag(arr, index >> 3, index);
/// <inheritdoc cref="GetFlag(ReadOnlySpan{byte}, int, int)"/>
public static bool GetFlag(ReadOnlySpan<byte> arr, int bitIndex) => GetFlag(arr, bitIndex >> 3, bitIndex);
/// <summary>
/// Sets the requested <see cref="bitIndex"/> value to the byte at <see cref="offset"/>.
@ -36,8 +37,20 @@ public static void SetFlag(Span<byte> arr, int offset, int bitIndex, bool value)
arr[offset] = (byte)newValue;
}
/// <summary>
/// Sets or clears a specific bit in the provided byte span at the specified index.
/// </summary>
/// <remarks>This method modifies the byte span in place.
/// Ensure that the span has sufficient capacity to accommodate the specified bit index.
/// </remarks>
/// <param name="arr">The span of bytes where the bit will be set or cleared.</param>
/// <param name="index">The zero-based index of the bit to modify. Must be within the bounds of the span.</param>
/// <param name="value"><see langword="true"/> to set the bit to 1; <see langword="false"/> to clear the bit to 0.</param>
public static void SetFlag(Span<byte> arr, int index, bool value) => SetFlag(arr, index >> 3, index, value);
/// <inheritdoc cref="GetBitFlagArray(ReadOnlySpan{byte}, Span{bool})"/>
/// <param name="data">The byte array containing the bit flags.</param>
/// <param name="count">The number of bits to read from the byte array.</param>
public static bool[] GetBitFlagArray(ReadOnlySpan<byte> data, int count)
{
var result = new bool[count];
@ -45,14 +58,33 @@ public static bool[] GetBitFlagArray(ReadOnlySpan<byte> data, int count)
return result;
}
/// <summary>
/// Converts a byte array into a boolean array, where each bit in the byte array corresponds to a boolean value in the result.
/// </summary>
/// <param name="data">The byte array containing the bit flags.</param>
/// <param name="result">>The span to write the boolean values into.</param>
public static void GetBitFlagArray(ReadOnlySpan<byte> data, Span<bool> result)
{
for (int i = 0; i < result.Length; i++)
result[i] = (data[i >> 3] & (1 << (i & 7))) != 0;
}
/// <inheritdoc cref="GetBitFlagArray(ReadOnlySpan{byte}, Span{bool})"/>
public static bool[] GetBitFlagArray(ReadOnlySpan<byte> data) => GetBitFlagArray(data, data.Length << 3);
/// <summary>
/// Sets the bit flags in the specified byte array based on the provided boolean values.
/// </summary>
/// <remarks>
/// The method modifies the <paramref name="data"/> span in place.
/// Each boolean value in <paramref name="value"/> corresponds to a bit in <paramref name="data"/>, with the first boolean value affecting the least significant bit of the first byte.
/// Ensure that <paramref name="data"/> has enough bytes to store all bits from <paramref name="value"/>; otherwise, an <see cref="IndexOutOfRangeException"/> may occur.
/// </remarks>
/// <param name="data">A <see cref="Span{T}"/> of bytes where the bit flags will be set.</param>
/// <param name="value">
/// A <see cref="ReadOnlySpan{T}"/> of boolean values representing the bit flags to set.
/// Each <see langword="true"/> value sets the corresponding bit to 1, and each <see langword="false"/> value sets it to 0.
/// </param>
public static void SetBitFlagArray(Span<byte> data, ReadOnlySpan<bool> value)
{
for (int i = 0; i < value.Length; i++)

View File

@ -42,16 +42,13 @@ private static string[] DumpStrings(Type t)
return result;
}
/// <summary>
/// Gets the current localization in a static class containing language-specific strings
/// </summary>
/// <param name="t"></param>
/// <inheritdoc cref="GetLocalization(Type, ReadOnlySpan{string})"/>
public static string[] GetLocalization(Type t) => DumpStrings(t);
/// <summary>
/// Gets the current localization in a static class containing language-specific strings
/// </summary>
/// <param name="t"></param>
/// <param name="t">Type of the static class containing the desired strings.</param>
/// <param name="existingLines">Existing localization lines (if provided)</param>
public static string[] GetLocalization(Type t, ReadOnlySpan<string> existingLines)
{

View File

@ -5,8 +5,21 @@
namespace PKHeX.Core;
/// <summary>
/// Logic for fetching data from the internet, such as text files or JSON data.
/// </summary>
public static class NetUtil
{
/// <summary>
/// Retrieves the content of the specified URL as a string.
/// </summary>
/// <remarks>
/// This method attempts to fetch the content of the specified URL and return it as a string.
/// If the URL is inaccessible or an error occurs during the operation, the method returns <see langword="null"/>.
/// The caller should handle the possibility of a <see langword="null"/> return value.
/// </remarks>
/// <param name="url">The <see cref="Uri"/> of the resource to retrieve.</param>
/// <returns>A string containing the content of the resource, or <see langword="null"/> if the resource could not be retrieved or an error occurred.</returns>
public static string? GetStringFromURL(Uri url)
{
try
@ -26,13 +39,14 @@ public static class NetUtil
}
}
// The GitHub API will fail if no user agent is provided. Use a hardcoded one to avoid issues.
private const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36";
private static Stream? GetStreamFromURL(Uri url)
{
// The GitHub API will fail if no user agent is provided
using var client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(3);
const string agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36";
client.DefaultRequestHeaders.Add("User-Agent", agent);
client.DefaultRequestHeaders.Add("User-Agent", UserAgent);
var response = client.GetAsync(url).Result;
return response.IsSuccessStatusCode ? response.Content.ReadAsStream() : null;
}

View File

@ -3,10 +3,16 @@
namespace PKHeX.Core;
public static partial class Util
/// <summary>
/// Logic for sanitizing file names and paths.
/// </summary>
/// <remarks>
/// Converting raw data to file names, trusting the data can lead to filesystem issues with invalid characters.
/// </remarks>
public static class PathUtil
{
/// <summary>
/// Cleans the local <see cref="fileName"/> by removing any invalid filename characters.
/// Cleans the <see cref="fileName"/> by removing any invalid filename characters.
/// </summary>
/// <returns>New string without any invalid characters.</returns>
public static string CleanFileName(string fileName)
@ -28,6 +34,11 @@ public static string CleanFileName(ReadOnlySpan<char> fileName)
return new string(result[..ctr]);
}
/// <summary>
/// Wish this were a ReadOnlySpan&lt;char&gt; instead of a char[].
/// </summary>
private static readonly char[] InvalidFileNameChars = Path.GetInvalidFileNameChars();
/// <summary>
/// Removes any invalid filename characters from the input string.
/// </summary>
@ -36,7 +47,7 @@ public static string CleanFileName(ReadOnlySpan<char> fileName)
/// <returns>Length of the cleaned string</returns>
private static int GetCleanFileName(ReadOnlySpan<char> input, Span<char> output)
{
ReadOnlySpan<char> invalid = Path.GetInvalidFileNameChars();
ReadOnlySpan<char> invalid = InvalidFileNameChars;
int ctr = 0;
foreach (var c in input)
{

View File

@ -7,7 +7,25 @@ public static partial class Util
/// <inheritdoc cref="Random.Shared"/>
public static Random Rand => Random.Shared;
/// <inheritdoc cref="Rand32(Random)"/>
/// <remarks>Uses <see cref="Random.Shared"/> to generate the random number.</remarks>
public static uint Rand32() => Rand32(Rand);
/// <summary>
/// Generates a random 32-bit unsigned integer.
/// </summary>
/// <param name="rnd">The <see cref="Random"/> instance used to generate the random number.</param>
/// <returns>A random 32-bit unsigned integer.</returns>
public static uint Rand32(this Random rnd) => (uint)rnd.NextInt64();
/// <summary>
/// Generates a 64-bit unsigned random number by combining two 32-bit random values.
/// </summary>
/// <remarks>
/// This method extends the <see cref="Random"/> class to provide a 64-bit random number by combining the results of two 32-bit random number generations.
/// The lower 32 bits are derived from one call to <c>Rand32</c>, and the upper 32 bits are derived from another call.
/// </remarks>
/// <param name="rnd">The <see cref="Random"/> instance used to generate the random values.</param>
/// <returns>A 64-bit unsigned integer representing the combined random value.</returns>
public static ulong Rand64(this Random rnd) => rnd.Rand32() | ((ulong)rnd.Rand32() << 32);
}

View File

@ -178,6 +178,19 @@ public static string GetStringResource(string name)
return result;
}
/// <summary>
/// Splits the specified <see cref="ReadOnlySpan{T}"/> of characters into an array of strings, using newline characters ('\n') as delimiters.
/// </summary>
/// <remarks>
/// This method is optimized for performance and avoids unnecessary allocations by working directly with spans.
/// It is suitable for scenarios where splitting large text data into lines is required.
/// </remarks>
/// <param name="s">The span of characters to split. Can include '\n' and '\r\n' as line breaks.</param>
/// <returns>
/// An array of strings, where each element represents a line of text from the input span.
/// Lines ending with a carriage return ('\r') will have the '\r' removed.
/// Returns an empty array if the input span is empty.
/// </returns>
private static string[] FastSplit(ReadOnlySpan<char> s)
{
// Get Count

View File

@ -90,20 +90,15 @@ public static void LoadHexBytesTo(ReadOnlySpan<char> str, Span<byte> dest, int t
private static byte DecodeTuple(char _0, char _1)
{
byte result;
if (char.IsAsciiDigit(_0))
result = (byte)((_0 - '0') << 4);
else if (char.IsAsciiHexDigitUpper(_0))
result = (byte)((_0 - 'A' + 10) << 4);
else
throw new ArgumentOutOfRangeException(nameof(_0));
return (byte)(DecodeChar(_0) << 4 | DecodeChar(_1));
if (char.IsAsciiDigit(_1))
result |= (byte)(_1 - '0');
else if (char.IsAsciiHexDigitUpper(_1))
result |= (byte)(_1 - 'A' + 10);
else
throw new ArgumentOutOfRangeException(nameof(_1));
return result;
static int DecodeChar(char x)
{
if (char.IsAsciiDigit(x))
return (byte)(x - '0');
if (char.IsAsciiHexDigitUpper(x))
return (byte)(x - 'A' + 10);
throw new ArgumentOutOfRangeException(nameof(_0));
}
}
}

View File

@ -69,17 +69,17 @@ public static uint GetHexValue(ReadOnlySpan<char> value)
if (char.IsAsciiDigit(c))
{
result <<= 4;
result += (uint)(c - '0');
result |= (uint)(c - '0');
}
else if (char.IsAsciiHexDigitUpper(c))
{
result <<= 4;
result += (uint)(c - 'A' + 10);
result |= (uint)(c - 'A' + 10);
}
else if (char.IsAsciiHexDigitLower(c))
{
result <<= 4;
result += (uint)(c - 'a' + 10);
result |= (uint)(c - 'a' + 10);
}
}
return result;
@ -101,29 +101,31 @@ public static ulong GetHexValue64(ReadOnlySpan<char> value)
if (char.IsAsciiDigit(c))
{
result <<= 4;
result += (uint)(c - '0');
result |= (uint)(c - '0');
}
else if (char.IsAsciiHexDigitUpper(c))
{
result <<= 4;
result += (uint)(c - 'A' + 10);
result |= (uint)(c - 'A' + 10);
}
else if (char.IsAsciiHexDigitLower(c))
{
result <<= 4;
result += (uint)(c - 'a' + 10);
result |= (uint)(c - 'a' + 10);
}
}
return result;
}
/// <summary>
/// Parses a variable length hex string (non-spaced, bytes in order).
/// </summary>
/// <inheritdoc cref="GetBytesFromHexString(ReadOnlySpan{char}, Span{byte})"/>
public static byte[] GetBytesFromHexString(ReadOnlySpan<char> input)
=> Convert.FromHexString(input);
/// <inheritdoc cref="GetBytesFromHexString(ReadOnlySpan{char})"/>
/// <summary>
/// Parses a variable length hex string (non-spaced, bytes in order).
/// </summary>
/// <param name="input">Hex string to parse</param>
/// <param name="result">Buffer to write the result to</param>
public static void GetBytesFromHexString(ReadOnlySpan<char> input, Span<byte> result)
=> Convert.FromHexString(input, result, out _, out _);
@ -162,10 +164,7 @@ public static int GetOnlyHex(ReadOnlySpan<char> str, Span<char> result)
return ctr;
}
/// <summary>
/// Returns a new string with each word converted to its appropriate title case.
/// </summary>
/// <param name="span">Input string to modify</param>
/// <inheritdoc cref="ToTitleCase(ReadOnlySpan{char}, Span{char})"/>
public static string ToTitleCase(ReadOnlySpan<char> span)
{
if (span.IsEmpty)
@ -176,7 +175,14 @@ public static string ToTitleCase(ReadOnlySpan<char> span)
return new string(result);
}
/// <inheritdoc cref="ToTitleCase(ReadOnlySpan{char})"/>
/// <summary>
/// Returns a new string with each word converted to its appropriate title case.
/// </summary>
/// <remarks>
/// Assumes that words are separated by whitespace characters. Duplicate whitespace are not skipped.
/// </remarks>
/// <param name="span">Input string to modify</param>
/// <param name="result">>Buffer to write the result to</param>
public static void ToTitleCase(ReadOnlySpan<char> span, Span<char> result)
{
// Add the first character as uppercase, then add each successive character as lowercase.

View File

@ -29,6 +29,12 @@ public sealed class ValueTypeTypeConverter : ExpandableObjectConverter
}
}
/// <summary>
/// Used for converting a <see cref="uint"/> to an uppercase hex string and back.
/// </summary>
/// <remarks>
/// When converting from a string, it accepts both "0x" prefixed and non-prefixed hex strings, with case insensitivity.
/// </remarks>
public sealed class TypeConverterU32 : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
@ -60,6 +66,12 @@ public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destina
}
}
/// <summary>
/// Used for converting a <see cref="ulong"/> to an uppercase hex string and back.
/// </summary>
/// <remarks>
/// When converting from a string, it accepts both "0x" prefixed and non-prefixed hex strings, with case insensitivity.
/// </remarks>
public sealed class TypeConverterU64 : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)

View File

@ -958,7 +958,7 @@ public bool ExportBackup()
return false;
}
var suggestion = Util.CleanFileName(SAV.Metadata.BAKName);
var suggestion = PathUtil.CleanFileName(SAV.Metadata.BAKName);
using var sfd = new SaveFileDialog();
sfd.FileName = suggestion;
if (sfd.ShowDialog() != DialogResult.OK)

View File

@ -901,7 +901,7 @@ private static string GetProgramTitle(SaveFile sav)
return title + $"[{version}]";
if (!sav.State.Exportable) // Blank save file
return title + $"{sav.Metadata.FileName} [{sav.OT} ({version})]";
return title + Path.GetFileNameWithoutExtension(Util.CleanFileName(sav.Metadata.BAKName)); // more descriptive
return title + Path.GetFileNameWithoutExtension(PathUtil.CleanFileName(sav.Metadata.BAKName)); // more descriptive
}
private static bool TryBackupExportCheck(SaveFile sav, string path)

View File

@ -241,7 +241,7 @@ private void ClickSet(object sender, EventArgs e)
PKM pk = PKME_Tabs.PreparePKM();
Directory.CreateDirectory(DatabasePath);
string path = Path.Combine(DatabasePath, Util.CleanFileName(pk.FileName));
string path = Path.Combine(DatabasePath, PathUtil.CleanFileName(pk.FileName));
if (File.Exists(path))
{
@ -510,7 +510,7 @@ private void Menu_Export_Click(object sender, EventArgs e)
Directory.CreateDirectory(path);
foreach (var pk in Results.Select(z => z.Entity))
File.WriteAllBytes(Path.Combine(path, Util.CleanFileName(pk.FileName)), pk.DecryptedPartyData);
File.WriteAllBytes(Path.Combine(path, PathUtil.CleanFileName(pk.FileName)), pk.DecryptedPartyData);
}
private void Menu_Import_Click(object sender, EventArgs e)

View File

@ -292,7 +292,7 @@ private void Menu_Export_Click(object sender, EventArgs e)
foreach (var gift in Results.OfType<DataMysteryGift>()) // WC3 have no data
{
var fileName = Util.CleanFileName(gift.FileName);
var fileName = PathUtil.CleanFileName(gift.FileName);
var path = Path.Combine(folder, fileName);
var data = gift.Write();
File.WriteAllBytes(path, data);

View File

@ -419,7 +419,7 @@ private void B_Export_Click(object sender, EventArgs e)
tr = "Trainer";
using var sfd = new SaveFileDialog();
sfd.Filter = "Secret Base Data|*.sb6";
sfd.FileName = $"{sb.BaseLocation:D2} - {Util.CleanFileName(tr)}.sb6";
sfd.FileName = $"{sb.BaseLocation:D2} - {PathUtil.CleanFileName(tr)}.sb6";
if (sfd.ShowDialog() != DialogResult.OK)
return;

View File

@ -207,7 +207,7 @@ private void B_ExportGoFiles_Click(object sender, EventArgs e)
var folder = fbd.SelectedPath;
foreach (var gpk in gofiles)
File.WriteAllBytes(Path.Combine(folder, Util.CleanFileName(gpk.FileName)), gpk.Data);
File.WriteAllBytes(Path.Combine(folder, PathUtil.CleanFileName(gpk.FileName)), gpk.Data);
WinFormsUtil.Alert($"Dumped {gofiles.Length} files to {folder}");
}

View File

@ -493,7 +493,7 @@ private async void BoxSlot_MouseDown(object? sender, MouseEventArgs e)
// Create Temp File to Drag
wc_slot = index;
Cursor.Current = Cursors.Hand;
string newfile = Path.Combine(Path.GetTempPath(), Util.CleanFileName(gift.FileName));
string newfile = Path.Combine(Path.GetTempPath(), PathUtil.CleanFileName(gift.FileName));
try
{
File.WriteAllBytes(newfile, gift.Write());

View File

@ -336,7 +336,7 @@ public static bool SavePKMDialog(PKM pk)
using var sfd = new SaveFileDialog();
sfd.Filter = genericFilter;
sfd.DefaultExt = pkx;
sfd.FileName = Util.CleanFileName(pk.FileName);
sfd.FileName = PathUtil.CleanFileName(pk.FileName);
if (sfd.ShowDialog() != DialogResult.OK)
return false;
@ -422,7 +422,7 @@ public static bool ExportMGDialog(DataMysteryGift gift)
{
using var sfd = new SaveFileDialog();
sfd.Filter = GetMysterGiftFilter(gift.Context);
sfd.FileName = Util.CleanFileName(gift.FileName);
sfd.FileName = PathUtil.CleanFileName(gift.FileName);
if (sfd.ShowDialog() != DialogResult.OK)
return false;