diff --git a/PKHeX.Core/Editing/Bulk/BatchEditing.cs b/PKHeX.Core/Editing/Bulk/BatchEditing.cs index d4148ae60..53f138a21 100644 --- a/PKHeX.Core/Editing/Bulk/BatchEditing.cs +++ b/PKHeX.Core/Editing/Bulk/BatchEditing.cs @@ -176,7 +176,7 @@ public static bool TryGetPropertyType(string propertyName, [NotNullWhen(true)] o { if (CustomProperties.Contains(propertyName)) { - result ="Custom"; + result = "Custom"; return true; } @@ -344,27 +344,28 @@ public static bool IsFilterMatch(IEnumerable filters, object /// Object to modify. /// Filters which must be satisfied prior to any modifications being made. /// Modifications to perform on the . + /// Custom modifier delegate. /// Result of the attempted modification. - public static bool TryModify(PKM pk, IEnumerable filters, IEnumerable modifications) - { - var result = TryModifyPKM(pk, filters, modifications); - return result == ModifyResult.Modified; - } + public static bool TryModify(PKM pk, IEnumerable filters, IEnumerable modifications, Func? modifier = null) + => TryModifyPKM(pk, filters, modifications, modifier) is ModifyResult.Modified; /// - /// Tries to modify the . + /// Tries to modify the using instructions and a custom modifier delegate. /// - /// Command Filter + /// Object to modify. /// Filters which must be satisfied prior to any modifications being made. /// Modifications to perform on the . + /// Custom modifier delegate. /// Result of the attempted modification. - internal static ModifyResult TryModifyPKM(PKM pk, IEnumerable filters, IEnumerable modifications) + internal static ModifyResult TryModifyPKM(PKM pk, IEnumerable filters, IEnumerable modifications, Func? modifier = null) { if (!pk.ChecksumValid || pk.Species == 0) return ModifyResult.Skipped; var info = new BatchInfo(pk); var props = GetProps(pk); + + // Check if any filter requires us to exclude this from modification scope. foreach (var cmd in filters) { try @@ -372,7 +373,6 @@ internal static ModifyResult TryModifyPKM(PKM pk, IEnumerable if (!IsFilterMatch(cmd, info, props)) return ModifyResult.Filtered; } - // Swallow any error because this can be malformed user input. catch (Exception ex) { Debug.WriteLine(MsgBEModifyFailCompare + " " + ex.Message, cmd.PropertyName, cmd.PropertyValue); @@ -380,8 +380,26 @@ internal static ModifyResult TryModifyPKM(PKM pk, IEnumerable } } + // Run all modifications, and track if any modifications were made or if any errors occurred. var error = false; var result = ModifyResult.Skipped; + + // If a compiled modifier is provided, execute it. If it returns false, skip further modifications. + if (modifier is { } func) + { + try + { + if (!func(pk)) + return ModifyResult.Skipped; + result = ModifyResult.Modified; + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); + return ModifyResult.Error; + } + } + foreach (var cmd in modifications) { try @@ -392,7 +410,6 @@ internal static ModifyResult TryModifyPKM(PKM pk, IEnumerable else if (tmp != ModifyResult.Skipped) result = tmp; } - // Swallow any error because this can be malformed user input. catch (Exception ex) { Debug.WriteLine(MsgBEModifyFail + " " + ex.Message, cmd.PropertyName, cmd.PropertyValue); @@ -437,24 +454,19 @@ private static ModifyResult SetPKMProperty(StringInstruction cmd, BatchInfo info if (cmd.Operation != InstructionOperation.Set) return ApplyNumericOperation(pk, cmd, pi, props); - object val; - if (cmd.Random) - val = cmd.RandomValue; - else if (cmd.PropertyValue.StartsWith(CONST_POINTER) && props.TryGetValue(cmd.PropertyValue.AsSpan(1), out var opi)) - val = opi.GetValue(pk) ?? throw new NullReferenceException(); - else - val = cmd.PropertyValue; + if (!TryResolveOperandValue(cmd, pk, props, out var value)) + return ModifyResult.Error; - ReflectUtil.SetValue(pi, pk, val); + ReflectUtil.SetValue(pi, pk, value); return ModifyResult.Modified; } - private static ModifyResult ApplyNumericOperation(PKM pk, StringInstruction cmd, PropertyInfo pi, Dictionary.AlternateLookup> props) + private static ModifyResult ApplyNumericOperation(T pk, StringInstruction cmd, PropertyInfo pi, Dictionary.AlternateLookup> props) where T : notnull { if (!pi.CanRead) return ModifyResult.Error; - if (!TryGetNumericType(pi.PropertyType, out var numericType, out var isNullable)) + if (!TryGetNumericType(pi.PropertyType, out var numericType)) return ModifyResult.Error; var currentValue = pi.GetValue(pk); @@ -464,15 +476,14 @@ private static ModifyResult ApplyNumericOperation(PKM pk, StringInstruction cmd, if (!TryResolveOperandValue(cmd, pk, props, out var operandValue)) return ModifyResult.Error; - if (!TryApplyNumericOperation(numericType, currentValue, operandValue, cmd.Operation, out var result)) + if (!TryApplyNumericOperation(numericType, currentValue, operandValue, cmd.Operation, out var value)) return ModifyResult.Error; - var valueToSet = isNullable ? Activator.CreateInstance(pi.PropertyType, result) ?? result : result; - pi.SetValue(pk, valueToSet); + ReflectUtil.SetValue(pi, pk, value); return ModifyResult.Modified; } - private static bool TryResolveOperandValue(StringInstruction cmd, PKM pk, Dictionary.AlternateLookup> props, [NotNullWhen(true)] out object? value) + private static bool TryResolveOperandValue(StringInstruction cmd, T pk, Dictionary.AlternateLookup> props, [NotNullWhen(true)] out object? value) { if (cmd.Random) { @@ -483,44 +494,76 @@ private static bool TryResolveOperandValue(StringInstruction cmd, PKM pk, Dictio var propertyValue = cmd.PropertyValue; if (propertyValue.StartsWith(CONST_POINTER) && props.TryGetValue(propertyValue.AsSpan(1), out var opi)) { - value = opi.GetValue(pk) ?? throw new NullReferenceException(); - return true; + value = opi.GetValue(pk); + return value is not null; } value = propertyValue; return true; } - private static bool TryGetNumericType(Type type, out Type numericType, out bool isNullable) + private static bool TryGetNumericType(Type type, out Type numericType) { numericType = Nullable.GetUnderlyingType(type) ?? type; - isNullable = numericType != type; + // isNullable = numericType != type; return numericType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(INumber<>)); } private static bool TryApplyNumericOperation(Type numericType, object currentValue, object operandValue, InstructionOperation operation, [NotNullWhen(true)] out object? result) { - var methodName = operation is InstructionOperation.BitwiseAnd or InstructionOperation.BitwiseOr or InstructionOperation.BitwiseXor - or InstructionOperation.BitwiseShiftLeft or InstructionOperation.BitwiseShiftRight - ? nameof(TryApplyBinaryIntegerOperationCore) - : nameof(TryApplyNumericOperationCore); + result = null; + if (numericType == typeof(byte)) + return ApplyBinaryInteger(currentValue, operandValue, operation, out result); + if (numericType == typeof(sbyte)) + return ApplyBinaryInteger(currentValue, operandValue, operation, out result); + if (numericType == typeof(short)) + return ApplyBinaryInteger(currentValue, operandValue, operation, out result); + if (numericType == typeof(ushort)) + return ApplyBinaryInteger(currentValue, operandValue, operation, out result); + if (numericType == typeof(int)) + return ApplyBinaryInteger(currentValue, operandValue, operation, out result); + if (numericType == typeof(uint)) + return ApplyBinaryInteger(currentValue, operandValue, operation, out result); + if (numericType == typeof(long)) + return ApplyBinaryInteger(currentValue, operandValue, operation, out result); + if (numericType == typeof(ulong)) + return ApplyBinaryInteger(currentValue, operandValue, operation, out result); + if (numericType == typeof(nint)) + return ApplyBinaryInteger(currentValue, operandValue, operation, out result); + if (numericType == typeof(nuint)) + return ApplyBinaryInteger(currentValue, operandValue, operation, out result); + if (numericType == typeof(BigInteger)) + return ApplyBinaryInteger(currentValue, operandValue, operation, out result); + if (numericType == typeof(float)) + return ApplyNumeric(currentValue, operandValue, operation, out result); + if (numericType == typeof(double)) + return ApplyNumeric(currentValue, operandValue, operation, out result); + if (numericType == typeof(decimal)) + return ApplyNumeric(currentValue, operandValue, operation, out result); + return false; + } - if (methodName == nameof(TryApplyBinaryIntegerOperationCore) && !IsBinaryIntegerType(numericType)) + private static bool ApplyNumeric(object currentValue, object operandValue, InstructionOperation operation, [NotNullWhen(true)] out object? result) + where T : INumber + { + if (IsBitwiseOperation(operation)) { result = null; return false; } - var method = typeof(BatchEditing).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static); - if (method is null) - { - result = null; - return false; - } - var generic = method.MakeGenericMethod(numericType); - var parameters = new[] { currentValue, operandValue, operation, null }; - var success = (bool)(generic.Invoke(null, parameters) ?? false); - result = parameters[3]!; + var success = TryApplyNumericOperationCore(currentValue, operandValue, operation, out var typed); + result = typed; + return success; + } + + private static bool ApplyBinaryInteger(object currentValue, object operandValue, InstructionOperation operation, [NotNullWhen(true)] out object? result) + where T : IBinaryInteger + { + var success = IsBitwiseOperation(operation) + ? TryApplyBinaryIntegerOperationCore(currentValue, operandValue, operation, out var typed) + : TryApplyNumericOperationCore(currentValue, operandValue, operation, out typed); + result = typed; return success; } @@ -592,8 +635,9 @@ private static bool TryApplyBinaryIntegerOperationCore(object currentValue, o } } - private static bool IsBinaryIntegerType(Type numericType) - => numericType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IBinaryInteger<>)); + private static bool IsBitwiseOperation(InstructionOperation operation) + => operation is InstructionOperation.BitwiseAnd or InstructionOperation.BitwiseOr or InstructionOperation.BitwiseXor + or InstructionOperation.BitwiseShiftLeft or InstructionOperation.BitwiseShiftRight; private static bool TryConvertNumeric(object value, [NotNullWhen(true)] out T? result) where T : INumber { diff --git a/PKHeX.Core/Editing/Bulk/BatchEditor.cs b/PKHeX.Core/Editing/Bulk/BatchEditor.cs index 9bae7b88a..963045734 100644 --- a/PKHeX.Core/Editing/Bulk/BatchEditor.cs +++ b/PKHeX.Core/Editing/Bulk/BatchEditor.cs @@ -15,13 +15,14 @@ public sealed class BatchEditor private int Failed { get; set; } /// - /// Tries to modify the . + /// Tries to modify the using instructions and a custom modifier delegate. /// /// Object to modify. /// Filters which must be satisfied prior to any modifications being made. /// Modifications to perform on the . + /// Custom modifier delegate. /// Result of the attempted modification. - public bool Process(PKM pk, IEnumerable filters, IEnumerable modifications) + public bool Process(PKM pk, IEnumerable filters, IEnumerable modifications, Func? modifier = null) { if (pk.Species == 0) return false; @@ -33,13 +34,12 @@ public bool Process(PKM pk, IEnumerable filters, IEnumerable< return false; } - var result = BatchEditing.TryModifyPKM(pk, filters, modifications); + var result = BatchEditing.TryModifyPKM(pk, filters, modifications, modifier); if (result != ModifyResult.Skipped) Iterated++; if (result.HasFlag(ModifyResult.Error)) { Failed++; - // Still need to fix checksum if another modification was successful. result &= ~ModifyResult.Error; } if (result != ModifyResult.Modified) @@ -73,15 +73,16 @@ public string GetEditorResults(IReadOnlyCollection sets) /// /// Batch instruction line(s) /// Entities to modify + /// Custom modifier delegate. /// Editor object if follow-up modifications are desired. - public static BatchEditor Execute(ReadOnlySpan lines, IEnumerable data) + public static BatchEditor Execute(ReadOnlySpan lines, IEnumerable data, Func? modifier = null) { var editor = new BatchEditor(); var sets = StringInstructionSet.GetBatchSets(lines); foreach (var pk in data) { foreach (var set in sets) - editor.Process(pk, set.Filters, set.Instructions); + editor.Process(pk, set.Filters, set.Instructions, modifier); } return editor; diff --git a/Tests/PKHeX.Core.Tests/PKM/BatchInstructionTests.cs b/Tests/PKHeX.Core.Tests/PKM/BatchInstructionTests.cs index b1cb2dfc7..45daf682a 100644 --- a/Tests/PKHeX.Core.Tests/PKM/BatchInstructionTests.cs +++ b/Tests/PKHeX.Core.Tests/PKM/BatchInstructionTests.cs @@ -1,4 +1,3 @@ -using System; using FluentAssertions; using Xunit; namespace PKHeX.Core.Tests; @@ -51,7 +50,7 @@ public void ApplyNumericOperation(string instruction, InstructionOperation opera { StringInstruction.TryParseInstruction(instruction, out var entry).Should().BeTrue(); entry.Should().NotBeNull(); - entry!.Operation.Should().Be(operation); + entry.Operation.Should().Be(operation); var pk = CreateTestPK7(initialValue); var modified = BatchEditing.TryModify(pk, [], [entry]); @@ -70,4 +69,75 @@ private static PK7 CreateTestPK7(uint exp) pk.RefreshChecksum(); return pk; } + + [Fact] + public void ProcessDelegateReturnsTrueWhenModified() + { + var pk = CreateTestPK7(100); + var editor = new BatchEditor(); + + bool modified = editor.Process(pk, [], [], static p => + { + p.EXP = 200; + return true; + }); + + modified.Should().BeTrue(); + } + + [Fact] + public void ProcessDelegateUpdatesExpWhenModified() + { + var pk = CreateTestPK7(100); + var editor = new BatchEditor(); + + _ = editor.Process(pk, [], [], static p => + { + p.EXP = 200; + return true; + }); + + pk.EXP.Should().Be(200u); + } + + [Fact] + public void ProcessInstructionsAndDelegateUpdatesExp() + { + var pk = CreateTestPK7(100); + var editor = new BatchEditor(); + + _ = editor.Process(pk, [], [], static p => + { + p.EXP = 200; + return true; + }); + + pk.EXP.Should().Be(200u); + } + + [Fact] + public void ProcessInstructionsAndDelegateSkipsWhenDelegateReturnsFalse() + { + var pk = CreateTestPK7(100); + var editor = new BatchEditor(); + StringInstruction.TryParseInstruction(".EXP=200", out var instruction).Should().BeTrue(); + instruction.Should().NotBeNull(); + + bool modified = editor.Process(pk, [], [instruction], static _ => false); + + modified.Should().BeFalse(); + } + + [Fact] + public void ProcessInstructionsAndDelegatePreservesExpWhenDelegateReturnsFalse() + { + var pk = CreateTestPK7(100); + var editor = new BatchEditor(); + StringInstruction.TryParseInstruction(".EXP=200", out var instruction).Should().BeTrue(); + instruction.Should().NotBeNull(); + + _ = editor.Process(pk, [], [instruction], static _ => false); + + pk.EXP.Should().Be(100u); + } } diff --git a/Tests/PKHeX.Core.Tests/Simulator/ShowdownSetTests.cs b/Tests/PKHeX.Core.Tests/Simulator/ShowdownSetTests.cs index cc2c98061..56736d30c 100644 --- a/Tests/PKHeX.Core.Tests/Simulator/ShowdownSetTests.cs +++ b/Tests/PKHeX.Core.Tests/Simulator/ShowdownSetTests.cs @@ -469,7 +469,7 @@ public void SimulatorParseEVsOutOfOrder(string text, int hp, int atk, int def, i success.Should().BeTrue("Parsing should succeed"); set.Should().NotBeNull(); - var evs = set!.EVs; + var evs = set.EVs; evs[0].Should().Be(hp, "HP EV should match"); evs[1].Should().Be(atk, "Atk EV should match"); evs[2].Should().Be(def, "Def EV should match"); @@ -488,7 +488,7 @@ public void SimulatorParseIVsOutOfOrder(string text, int hp, int atk, int def, i success.Should().BeTrue("Parsing should succeed"); set.Should().NotBeNull(); - var ivs = set!.IVs; + var ivs = set.IVs; ivs[0].Should().Be(hp, "HP IV should match"); ivs[1].Should().Be(atk, "Atk IV should match"); ivs[2].Should().Be(def, "Def IV should match");