From ced0f1dd991049e7120545237c36ea819fe04ba0 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Fri, 7 Apr 2023 04:05:07 +0700 Subject: [PATCH] Add `UnflectApi` --- config/AcediaSystem.ini | 2 +- sources/BaseAPI/API/SideEffects/SideEffect.uc | 277 ++++++++++++ .../BaseAPI/API/SideEffects/SideEffectAPI.uc | 194 +++++++++ .../API/Unflect/FunctionReplacement.uc | 76 ++++ .../API/Unflect/Tests/MockInitialClass.uc | 30 ++ .../API/Unflect/Tests/MockReplacerClass.uc | 33 ++ .../BaseAPI/API/Unflect/Tests/TEST_Unflect.uc | 87 ++++ sources/BaseAPI/API/Unflect/TypeCast.uc | 29 ++ sources/BaseAPI/API/Unflect/UClass.uc | 49 +++ sources/BaseAPI/API/Unflect/UClassCast.uc | 30 ++ sources/BaseAPI/API/Unflect/UField.uc | 28 ++ sources/BaseAPI/API/Unflect/UFieldCast.uc | 30 ++ sources/BaseAPI/API/Unflect/UFunction.uc | 35 ++ sources/BaseAPI/API/Unflect/UFunctionCast.uc | 30 ++ sources/BaseAPI/API/Unflect/UProperty.uc | 41 ++ sources/BaseAPI/API/Unflect/UPropertyCast.uc | 30 ++ sources/BaseAPI/API/Unflect/UState.uc | 30 ++ sources/BaseAPI/API/Unflect/UStateCast.uc | 30 ++ sources/BaseAPI/API/Unflect/UStruct.uc | 48 ++ sources/BaseAPI/API/Unflect/UStructCast.uc | 30 ++ sources/BaseAPI/API/Unflect/UTextBuffer.uc | 28 ++ sources/BaseAPI/API/Unflect/Unflect.uc | 32 ++ sources/BaseAPI/API/Unflect/UnflectApi.uc | 409 ++++++++++++++++++ sources/BaseAPI/Global.uc | 4 +- sources/ClientAPI/ClientAcediaAdapter.uc | 1 - sources/Manifest.uc | 1 + sources/ServerAPI/ServerAcediaAdapter.uc | 1 - sources/ServerAPI/ServerGlobal.uc | 2 +- 28 files changed, 1612 insertions(+), 5 deletions(-) create mode 100644 sources/BaseAPI/API/SideEffects/SideEffect.uc create mode 100644 sources/BaseAPI/API/SideEffects/SideEffectAPI.uc create mode 100644 sources/BaseAPI/API/Unflect/FunctionReplacement.uc create mode 100644 sources/BaseAPI/API/Unflect/Tests/MockInitialClass.uc create mode 100644 sources/BaseAPI/API/Unflect/Tests/MockReplacerClass.uc create mode 100644 sources/BaseAPI/API/Unflect/Tests/TEST_Unflect.uc create mode 100644 sources/BaseAPI/API/Unflect/TypeCast.uc create mode 100644 sources/BaseAPI/API/Unflect/UClass.uc create mode 100644 sources/BaseAPI/API/Unflect/UClassCast.uc create mode 100644 sources/BaseAPI/API/Unflect/UField.uc create mode 100644 sources/BaseAPI/API/Unflect/UFieldCast.uc create mode 100644 sources/BaseAPI/API/Unflect/UFunction.uc create mode 100644 sources/BaseAPI/API/Unflect/UFunctionCast.uc create mode 100644 sources/BaseAPI/API/Unflect/UProperty.uc create mode 100644 sources/BaseAPI/API/Unflect/UPropertyCast.uc create mode 100644 sources/BaseAPI/API/Unflect/UState.uc create mode 100644 sources/BaseAPI/API/Unflect/UStateCast.uc create mode 100644 sources/BaseAPI/API/Unflect/UStruct.uc create mode 100644 sources/BaseAPI/API/Unflect/UStructCast.uc create mode 100644 sources/BaseAPI/API/Unflect/UTextBuffer.uc create mode 100644 sources/BaseAPI/API/Unflect/Unflect.uc create mode 100644 sources/BaseAPI/API/Unflect/UnflectApi.uc diff --git a/config/AcediaSystem.ini b/config/AcediaSystem.ini index a42cf4e..1b50fda 100644 --- a/config/AcediaSystem.ini +++ b/config/AcediaSystem.ini @@ -3,7 +3,7 @@ [AcediaCore.AcediaEnvironment] debugMode=false -[AcediaCore.SideEffects] +[AcediaCore.ServerSideEffects] ; Acedia requires adding its own `GameRules` to listen to many different ; game events. ; It's normal for a mod to add its own game rules: game rules are diff --git a/sources/BaseAPI/API/SideEffects/SideEffect.uc b/sources/BaseAPI/API/SideEffects/SideEffect.uc new file mode 100644 index 0000000..6d5ede7 --- /dev/null +++ b/sources/BaseAPI/API/SideEffects/SideEffect.uc @@ -0,0 +1,277 @@ +/** + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2022-2023 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class SideEffect extends AcediaObject; + +//! Defines the concept of "side effects" in the context of the Acedia and its derivative mods. +//! +//! Side effects are changes that are not part of the mod's main functionality, but rather something +//! necessary to enable that functionality, while also possibly affecting how other mods work. +//! Documenting these side effects helps developers and server admins understand changes performed +//! by Acedia or mods based on it, and anticipate any potential conflicts or issues that may arise. +//! +//! It should be noted that what constitutes a side effect is loosely defined, and it is simply +//! a tool to inform others that something unexpected has happened, possibly breaking other mods. +//! AcediaCore aims to leave a minimal footprint, but still needs to make some changes +//! (e.g., adding GameRules, patching code of some functions), and [`SideEffects`] can be used to +//! document them. +//! Similarly, [`SideEffect`]s can be used to document changes made by AcediaFixes, a package meant +//! only for fixing bugs that inevitably needs to make many under-the-hood changes to achieve +//! that goal. +//! +//! On the other hand gameplay mods like Futility or Ire can make a lot of changes, but they can all +//! be just expected part of its direct functionality: we expect feature that shares dosh of leavers +//! to alter players' dosh values, so this is not a side effect. +//! Such mods are likely not going to have to specify any side effects whatsoever. + +var private Text name; +var private Text description; +var private Text package; +var private Text source; +var private Text status; +var private bool initialized; + +protected function Finalizer() { + _.memory.Free(name); + _.memory.Free(description); + _.memory.Free(package); + _.memory.Free(source); + _.memory.Free(status); + name = none; + description = none; + package = none; + source = none; + status = none; + initialized = false; +} + +/// Checks whether caller [`SideEffect`] was initialized. +/// +/// Initialization must happen directly after creation and only initialized instances should +/// ever be used. +public final function bool IsInitialized() { + return initialized; +} + +/// This function is used to set the initial values of the [`SideEffect`] object properties when it +/// is first created. +/// +/// All arguments must be not `none`. +/// +/// Returns `true` if the initialization was successful, `false` otherwise (including the case where +/// the [`SideEffect`] object has already been initialized). +public final function bool Initialize( + BaseText sideEffectName, + BaseText sideEffectDescription, + BaseText sideEffectPackage, + BaseText sideEffectSource, + BaseText sideEffectStatus +) { + if (initialized) return false; + if (sideEffectName == none) return false; + if (sideEffectDescription == none) return false; + if (sideEffectPackage == none) return false; + if (sideEffectSource == none) return false; + if (sideEffectStatus == none) return false; + + name = sideEffectName.Copy(); + description = sideEffectDescription.Copy(); + package = sideEffectPackage.Copy(); + source = sideEffectSource.Copy(); + status = sideEffectStatus.Copy(); + initialized = true; + return true; +} + +/// This function is used to set the initial values of the [`SideEffect`] object properties when it +/// is first created. +/// +/// Returns `true` if the initialization was successful, `false` otherwise (including the case where +/// the [`SideEffect`] object has already been initialized). +public final function bool Initialize_S( + string sideEffectName, + string sideEffectDescription, + string sideEffectPackage, + string sideEffectSource, + string sideEffectStatus +) { + name = _.text.FromString(sideEffectName); + description = _.text.FromString(sideEffectDescription); + package = _.text.FromString(sideEffectPackage); + source = _.text.FromString(sideEffectSource); + status = _.text.FromString(sideEffectStatus); + initialized = true; + return true; +} + +/// Returns a brief summary that conveys the purpose of the caller [SideEffect] to the user in +/// a clear and concise manner. +/// +/// While there is no hard limit on the length of this value, it is recommended to keep it under 80 +/// characters for readability. +/// +/// Returned value for initialized [`SideEffect`] is guaranteed to not be `none`, as it is +/// a required property of the [`SideEffect`] object. +public final function Text GetName() { + if (initialized) { + return name.Copy(); + } + return none; +} + +/// Returns a brief summary that conveys the purpose of the caller [SideEffect] to the user in +/// a clear and concise manner. +/// +/// While there is no hard limit on the length of this value, it is recommended to keep it under 80 +/// characters for readability. +public final function string GetName_S() { + if (initialized && name != none) { + return name.ToString(); + } + return ""; +} + +/// Returns the detailed description of the caller [`SideEffect`], which describes what was done +/// and why the relevant change was necessary. +/// +/// Returned value for initialized [`SideEffect`] is guaranteed to not be `none`, as it is +/// a required property of the [`SideEffect`] object. +public final function Text GetDescription() { + if (initialized) { + return description.Copy(); + } + return none; +} + +/// Returns the detailed description of the caller [`SideEffect`], which describes what was done +/// and why the relevant change was necessary. +public final function string GetDescription_S() { + if (initialized && description != none) { + return description.ToString(); + } + return ""; +} + +/// Returns the name of the package ("*.u" file) that introduced the changes +/// represented by the caller `SideEffect`. +/// +/// It should be noted that even if a different package requested the functionality that led to +/// the changes being made, the package responsible for the side effect is the one that performed +/// the changes. +/// +/// Returned value for initialized [`SideEffect`] is guaranteed to not be `none`, as it is +/// a required property of the [`SideEffect`] object. +public final function Text GetPackage() { + if (initialized) { + return package.Copy(); + } + return none; +} + +/// Returns the name of the package ("*.u" file) that introduced the changes +/// represented by the caller `SideEffect`. +/// +/// It should be noted that even if a different package requested the functionality that led to +/// the changes being made, the package responsible for the side effect is the one that performed +/// the changes. +public final function string GetPackage_S() { + if (initialized && package != none) { + return package.ToString(); + } + return ""; +} + +/// The origin of this change within the package is specified, and for larger packages, additional +/// details can be provided to clarify the cause of the change. +/// +/// Returned value for initialized [`SideEffect`] is guaranteed to not be `none`, as it is +/// a required property of the [`SideEffect`] object. +public final function Text GetSource() { + if (initialized) { + return source.Copy(); + } + return none; +} + +/// The origin of this change within the package is specified, and for larger packages, additional +/// details can be provided to clarify the cause of the change. +public final function string GetSource_S() { + if (initialized && source != none) { + return source.ToString(); + } + return ""; +} + +/// The status of the caller [`SideEffect`], that is used to differentiate between different ways +/// that a side effect may have been introduced, allowing for better tracking and management of +/// the effect. +/// +/// Returned value for initialized [`SideEffect`] is guaranteed to not be `none`, as it is +/// a required property of the [`SideEffect`] object. +public final function Text GetStatus() { + if (initialized) { + return status.Copy(); + } + return none; +} + +/// The status of the caller [`SideEffect`], that is used to differentiate between different ways +/// that a side effect may have been introduced, allowing for better tracking and management of +/// the effect. +public final function string GetStatus_S() { + if (initialized && status != none) { + return status.ToString(); + } + return ""; +} + +public function bool IsEqual(Object other) { + local SideEffect otherSideEffect; + + if (self == other) return true; + otherSideEffect = SideEffect(other); + if (otherSideEffect == none) return false; + if (!otherSideEffect.initialized) return false; + if (GetHashCode() != otherSideEffect.GetHashCode()) return false; + if (!name.Compare(otherSideEffect.name,, SFORM_SENSITIVE)) return false; + if (!package.Compare(otherSideEffect.package,, SFORM_SENSITIVE)) return false; + if (!source.Compare(otherSideEffect.source,, SFORM_SENSITIVE)) return false; + if (!status.Compare(otherSideEffect.status,, SFORM_SENSITIVE)) return false; + if (!description.Compare(otherSideEffect.description,, SFORM_SENSITIVE)) return false; + + return true; +} + +protected function int CalculateHashCode() { + local int result; + + if (initialized) { + result = name.GetHashCode(); + result = CombineHash(result, description.GetHashCode()); + result = CombineHash(result, package.GetHashCode()); + result = CombineHash(result, source.GetHashCode()); + result = CombineHash(result, status.GetHashCode()); + return result; + } +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/SideEffects/SideEffectAPI.uc b/sources/BaseAPI/API/SideEffects/SideEffectAPI.uc new file mode 100644 index 0000000..7e58b91 --- /dev/null +++ b/sources/BaseAPI/API/SideEffects/SideEffectAPI.uc @@ -0,0 +1,194 @@ +/** + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2022-2023 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class SideEffectAPI extends AcediaObject; + +var private array activeSideEffects; + +/// Returns an array containing all SideEffect objects that have been registered up to this point. +/// +/// The order of the elements in the array is not guaranteed. +public function array GetAll() { + local int i; + + for (i = 0; i < activeSideEffects.length; i += 1) { + activeSideEffects[i].NewRef(); + } + return activeSideEffects; +} + +/// Returns all registered [`SideEffects`] that are associated with the specified package name +/// (case-insensitive). +public function array GetFromPackage(BaseText packageName) { + local int i; + local Text nextPackage; + local array result; + + if (packageName == none) { + return result; + } + for (i = 0; i < activeSideEffects.length; i += 1) { + nextPackage = activeSideEffects[i].GetPackage(); + if (packageName.Compare(nextPackage, SCASE_INSENSITIVE)) { + activeSideEffects[i].NewRef(); + result[result.length] = activeSideEffects[i]; + } + _.memory.Free(nextPackage); + } + return result; +} + +/// Adds a new side effect to the list of active side effects. +/// +/// This method will fail if any of its arguments are `none` or a side effect with that exact +/// contents was already added. +public function SideEffect Add( + BaseText sideEffectName, + BaseText sideEffectDescription, + BaseText sideEffectPackage, + BaseText sideEffectSource, + BaseText sideEffectStatus +) { + local bool initialized; + local SideEffect newSideEffect; + + newSideEffect = SideEffect(_.memory.Allocate(class'SideEffect')); + initialized = newSideEffect.Initialize( + sideEffectName, + sideEffectDescription, + sideEffectPackage, + sideEffectSource, + sideEffectStatus); + if (initialized) { + if (!AddInstance(newSideEffect)) { + _.memory.Free(newSideEffect); + return none; + } + } else { + _.memory.Free(newSideEffect); + return none; + } + return newSideEffect; +} + +/// Adds a new side effect to the list of active side effects. +/// +/// This method will fail if a side effect with that exact contents was already added. +public function SideEffect Add_S( + string sideEffectName, + string sideEffectDescription, + string sideEffectPackage, + string sideEffectSource, + string sideEffectStatus +) { + local bool initialized; + local SideEffect newSideEffect; + + newSideEffect = SideEffect(_.memory.Allocate(class'SideEffect')); + initialized = newSideEffect.Initialize_S( + sideEffectName, + sideEffectDescription, + sideEffectPackage, + sideEffectSource, + sideEffectStatus); + if (initialized) { + if (!AddInstance(newSideEffect)) { + _.memory.Free(newSideEffect); + return none; + } + } else { + return none; + } + return newSideEffect; +} + +/// Adds a new side effect to the list of active side effects. +/// +/// This method will fail if its argument is `none`, non-initialized or a side effect with that +/// exact contents was already added. +public function bool AddInstance(SideEffect newSideEffect) { + local int i; + + if (newSideEffect == none) return false; + if (!newSideEffect.IsInitialized()) return false; + + for (i = 0; i < activeSideEffects.length; i += 1) { + if (activeSideEffects[i].IsEqual(newSideEffect)) { + return false; + } + } + newSideEffect.NewRef(); + activeSideEffects[activeSideEffects.length] = newSideEffect; + LogAddingSideEffectChange(newSideEffect, true); + return true; +} + +/// Removes a side effect from the list of active side effects. +/// +/// This method will fail if its argument is `none`, non-initialized or a side effect with its +/// contents isn't in the records. +public function bool RemoveInstance(SideEffect inactiveSideEffect) { + local int i; + local bool foundInstance; + + if (inactiveSideEffect == none) { + return false; + } + for (i = 0; i < activeSideEffects.length; i += 1) { + if (activeSideEffects[i].IsEqual(inactiveSideEffect)) { + LogAddingSideEffectChange(activeSideEffects[i], false); + _.memory.Free(activeSideEffects[i]); + activeSideEffects.Remove(i, 1); + foundInstance = true; + } + } + return foundInstance; +} + +private function LogAddingSideEffectChange(SideEffect effect, bool added) { + local MutableText builder; + local Text sideEffectData; + + if (effect == none) { + return; + } + builder = _.text.Empty(); + if (added) { + builder.Append(P("NEW SIDE EFFECT: ")); + } else { + builder.Append(P("REMOVED SIDE EFFECT: ")); + } + sideEffectData = effect.GetName(); + builder.Append(sideEffectData); + _.memory.Free(sideEffectData); + sideEffectData = effect.GetStatus(); + if (sideEffectData != none) { + builder.Append(P(" {")); + builder.Append(sideEffectData); + _.memory.Free(sideEffectData); + builder.Append(P("}")); + } + _.logger.Info(builder); + builder.FreeSelf(); +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/FunctionReplacement.uc b/sources/BaseAPI/API/Unflect/FunctionReplacement.uc new file mode 100644 index 0000000..68a6d7e --- /dev/null +++ b/sources/BaseAPI/API/Unflect/FunctionReplacement.uc @@ -0,0 +1,76 @@ +/** + * Config class for storing map lists. + * Copyright 2023 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class FunctionReplacement extends AcediaObject; + +var private Text replaced; +var private Text replacer; +var private SideEffect effect; + +protected function Finalizer() { + _.memory.Free(replaced); + _.memory.Free(replacer); + _.memory.Free(effect); + replaced = none; + replacer = none; + effect = none; +} + +public static final function FunctionReplacement Make( + BaseText oldFunction, + BaseText newFunction, + SideEffect sideEffect +) { + local FunctionReplacement newReplacement; + + if (oldFunction == none) return none; + if (newFunction == none) return none; + if (sideEffect == none) return none; + + newReplacement = FunctionReplacement(__().memory.Allocate(class'FunctionReplacement')); + newReplacement.replaced = oldFunction.Copy(); + newReplacement.replacer = newFunction.Copy(); + sideEffect.NewRef(); + newReplacement.effect = sideEffect; + return newReplacement; +} + +public final function Text GetReplacedFunctionName() { + return replaced.Copy(); +} + +public final function string GetReplacedFunctionName_S() { + return replaced.ToString(); +} + +public final function Text GetReplacerFunctionName() { + return replacer.Copy(); +} + +public final function string GetReplacerFunctionName_S() { + return replacer.ToString(); +} + +public final function SideEffect GetSideEffect() { + effect.NewRef(); + return effect; +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/Tests/MockInitialClass.uc b/sources/BaseAPI/API/Unflect/Tests/MockInitialClass.uc new file mode 100644 index 0000000..35f2dcd --- /dev/null +++ b/sources/BaseAPI/API/Unflect/Tests/MockInitialClass.uc @@ -0,0 +1,30 @@ +/** + * Config class for storing map lists. + * Copyright 2023 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class MockInitialClass extends AcediaObject; + +var public int counter; + +public final function int DoIt() { + counter += 1; + return counter; +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/Tests/MockReplacerClass.uc b/sources/BaseAPI/API/Unflect/Tests/MockReplacerClass.uc new file mode 100644 index 0000000..0bbb9fb --- /dev/null +++ b/sources/BaseAPI/API/Unflect/Tests/MockReplacerClass.uc @@ -0,0 +1,33 @@ +/** + * Config class for storing map lists. + * Copyright 2023 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class MockReplacerClass extends MockInitialClass; + +public final function int DoIt2() { + counter += 2; + return -counter; +} + +public final function int DoIt3() { + counter -= 1; + return 7; +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/Tests/TEST_Unflect.uc b/sources/BaseAPI/API/Unflect/Tests/TEST_Unflect.uc new file mode 100644 index 0000000..0d0e674 --- /dev/null +++ b/sources/BaseAPI/API/Unflect/Tests/TEST_Unflect.uc @@ -0,0 +1,87 @@ +/** + * Set of tests for `Command` class. + * Copyright 2021 - 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class TEST_Unflect extends TestCase + abstract; + +protected static function TESTS() { + local MockInitialClass obj; + + obj = MockInitialClass(__().memory.Allocate(class'MockInitialClass')); + Context("Replacing functions with `UnflectApi`"); + Test_InitialReplacement(obj); + Test_SecondReplacement(obj); + Test_ReplacementWithSelf(obj); + Test_RevertingReplacement(obj); +} + +protected static function Test_InitialReplacement(MockInitialClass obj) { + Issue("Functions aren't being replaced correctly the first time."); + TEST_ExpectTrue(__().unflect.ReplaceFunction_S( + "AcediaCore.MockInitialClass.DoIt", + "AcediaCore.MockReplacerClass.DoIt2", + "testing")); + TEST_ExpectTrue(obj.DoIt() == -2); + TEST_ExpectTrue(obj.counter == 2); + TEST_ExpectTrue(obj.DoIt() == -4); + TEST_ExpectTrue(obj.counter == 4); +} + +protected static function Test_SecondReplacement(MockInitialClass obj) { + Issue("Functions aren't being replaced correctly in case they were already replaced."); + TEST_ExpectTrue(__().unflect.ReplaceFunction_S( + "AcediaCore.MockInitialClass.DoIt", + "AcediaCore.MockReplacerClass.DoIt3", + "testing")); + TEST_ExpectTrue(obj.DoIt() == 7); + TEST_ExpectTrue(obj.counter == 3); + TEST_ExpectTrue(obj.DoIt() == 7); + TEST_ExpectTrue(obj.counter == 2); +} + +protected static function Test_ReplacementWithSelf(MockInitialClass obj) { + Issue("Attempting to replacing function with itself makes unexpected change."); + TEST_ExpectFalse(__().unflect.ReplaceFunction_S( + "AcediaCore.MockInitialClass.DoIt", + "AcediaCore.MockInitialClass.DoIt", + "testing")); + TEST_ExpectTrue(obj.DoIt() == 7); + TEST_ExpectTrue(obj.counter == 1); +} + +protected static function Test_RevertingReplacement(MockInitialClass obj) { + Issue("Reverting replaced function doesn't work."); + TEST_ExpectTrue(__().unflect.RevertFunction_S("AcediaCore.MockInitialClass.DoIt")); + TEST_ExpectTrue(obj.DoIt() == 2); + TEST_ExpectTrue(obj.counter == 2); + TEST_ExpectTrue(obj.DoIt() == 3); + TEST_ExpectTrue(obj.counter == 3); + + Issue("Reverting already reverted function ends in success."); + TEST_ExpectFalse(__().unflect.RevertFunction_S("AcediaCore.MockInitialClass.DoIt")); + + Issue("Reverting already reverted function leads to unexpected results."); + TEST_ExpectTrue(obj.DoIt() == 4); + TEST_ExpectTrue(obj.counter == 4); +} + +defaultproperties { + caseName = "Function replacement" + caseGroup = "Unflect" +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/TypeCast.uc b/sources/BaseAPI/API/Unflect/TypeCast.uc new file mode 100644 index 0000000..2d1e9cc --- /dev/null +++ b/sources/BaseAPI/API/Unflect/TypeCast.uc @@ -0,0 +1,29 @@ +/** + * One of the original Unflect files. + * Copyright 2022-2023 EliotVU + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class TypeCast extends Object; + +var Object nativeType; + +final function NativeCast(Object type) { + nativeType = type; +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/UClass.uc b/sources/BaseAPI/API/Unflect/UClass.uc new file mode 100644 index 0000000..d6bd843 --- /dev/null +++ b/sources/BaseAPI/API/Unflect/UClass.uc @@ -0,0 +1,49 @@ +/** + * One of the original Unflect files. + * Copyright 2022-2023 EliotVU + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class UClass extends UState within Package; + +var int classFlags; +var int classUnique; +var Guid classGuid; +var UClass classWithin; +var name classConfigName; + +var array classReps; + +var array netFields; + +var array dependencies; + +var array packageImports; +var array defaults; +var array hideCategories; +var array dependentOn; + +var string defaultPropText; + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/UClassCast.uc b/sources/BaseAPI/API/Unflect/UClassCast.uc new file mode 100644 index 0000000..71949ed --- /dev/null +++ b/sources/BaseAPI/API/Unflect/UClassCast.uc @@ -0,0 +1,30 @@ +/** + * One of the original Unflect files. + * Copyright 2022-2023 EliotVU + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class UClassCast extends Object; + +var UClass nativeType; + +final function UClass Cast(Class type) { + super(TypeCast).NativeCast(type); + return nativeType; +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/UField.uc b/sources/BaseAPI/API/Unflect/UField.uc new file mode 100644 index 0000000..56a316e --- /dev/null +++ b/sources/BaseAPI/API/Unflect/UField.uc @@ -0,0 +1,28 @@ +/** + * One of the original Unflect files. + * Copyright 2022-2023 EliotVU + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class UField extends Object + abstract; + +var UField superField; +var UField next; +var UField hashNext; + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/UFieldCast.uc b/sources/BaseAPI/API/Unflect/UFieldCast.uc new file mode 100644 index 0000000..3bbfb0f --- /dev/null +++ b/sources/BaseAPI/API/Unflect/UFieldCast.uc @@ -0,0 +1,30 @@ +/** + * One of the original Unflect files. + * Copyright 2022-2023 EliotVU + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class UFieldCast extends Object; + +var UField nativeType; + +final function UField Cast(Field type) { + super(TypeCast).NativeCast(type); + return nativeType; +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/UFunction.uc b/sources/BaseAPI/API/Unflect/UFunction.uc new file mode 100644 index 0000000..b84d9ee --- /dev/null +++ b/sources/BaseAPI/API/Unflect/UFunction.uc @@ -0,0 +1,35 @@ +/** + * One of the original Unflect files. + * Copyright 2022-2023 EliotVU + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class UFunction extends UStruct within UState + dependson(Unflect); + +var byte functionMD5Digest[16]; + +var int functionFlags; +var Unflect.Int16 nativeIndex; +var Unflect.Int16 repOffset; +var byte operPrecedence; + +var byte numParms; +var Unflect.Int16 parmsSize; +var Unflect.Int16 returnValueOffset; + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/UFunctionCast.uc b/sources/BaseAPI/API/Unflect/UFunctionCast.uc new file mode 100644 index 0000000..8c18bec --- /dev/null +++ b/sources/BaseAPI/API/Unflect/UFunctionCast.uc @@ -0,0 +1,30 @@ +/** + * One of the original Unflect files. + * Copyright 2022-2023 EliotVU + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class UFunctionCast extends Object; + +var UFunction nativeType; + +final function UFunction Cast(Function type) { + super(TypeCast).NativeCast(type); + return nativeType; +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/UProperty.uc b/sources/BaseAPI/API/Unflect/UProperty.uc new file mode 100644 index 0000000..4b54f3d --- /dev/null +++ b/sources/BaseAPI/API/Unflect/UProperty.uc @@ -0,0 +1,41 @@ +/** + * One of the original Unflect files. + * Copyright 2022-2023 EliotVU + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class UProperty extends UField within UField + abstract; + +var int arrayDim; +var int elementSize; +var int propertyFlags; +var name category; + +var byte repOffset[2]; +var byte repIndex[2]; + +var transient int offset; +var transient UProperty propertyLinkNext; +var transient UProperty configLinkNext; +var transient UProperty constructorLinkNext; +var transient UProperty nextRef; +var transient UProperty repOwner; + +var string commentString; + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/UPropertyCast.uc b/sources/BaseAPI/API/Unflect/UPropertyCast.uc new file mode 100644 index 0000000..e2665de --- /dev/null +++ b/sources/BaseAPI/API/Unflect/UPropertyCast.uc @@ -0,0 +1,30 @@ +/** + * One of the original Unflect files. + * Copyright 2022-2023 EliotVU + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class UPropertyCast extends Object; + +var UProperty nativeType; + +final function UProperty Cast(Property type) { + super(TypeCast).NativeCast(type); + return nativeType; +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/UState.uc b/sources/BaseAPI/API/Unflect/UState.uc new file mode 100644 index 0000000..5d3a7d6 --- /dev/null +++ b/sources/BaseAPI/API/Unflect/UState.uc @@ -0,0 +1,30 @@ +/** + * One of the original Unflect files. + * Copyright 2022-2023 EliotVU + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class UState extends UStruct + dependson(Unflect); + +var Unflect.Int64 probeMask; +var Unflect.Int64 ignoreMask; +var int stateFlags; +var Unflect.Int16 labelTableOffset; +var UField vfHash[256]; + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/UStateCast.uc b/sources/BaseAPI/API/Unflect/UStateCast.uc new file mode 100644 index 0000000..0ad3c31 --- /dev/null +++ b/sources/BaseAPI/API/Unflect/UStateCast.uc @@ -0,0 +1,30 @@ +/** + * One of the original Unflect files. + * Copyright 2022-2023 EliotVU + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class UStateCast extends Object; + +var UState nativeType; + +final function UState Cast(State type) { + super(TypeCast).NativeCast(type); + return nativeType; +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/UStruct.uc b/sources/BaseAPI/API/Unflect/UStruct.uc new file mode 100644 index 0000000..7870eb1 --- /dev/null +++ b/sources/BaseAPI/API/Unflect/UStruct.uc @@ -0,0 +1,48 @@ +/** + * One of the original Unflect files. + * Copyright 2022-2023 EliotVU + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class UStruct extends UField; + +var UTextBuffer scriptText; +var UTextBuffer cppText; +var UField children; +var int propertiesSize; +var name friendlyName; +var array script; + +var int textPos; +var int line; +var struct EStructFlags { + var bool native; + var bool export; + var bool long; + var bool init; + var bool unused1; + var bool unused2; + var bool unused3; + var bool unused4; +} StructFlags; + +var Property refLink; +var Property propertyLink; +var Property configLink; +var Property constructorLink; + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/UStructCast.uc b/sources/BaseAPI/API/Unflect/UStructCast.uc new file mode 100644 index 0000000..7ede2d3 --- /dev/null +++ b/sources/BaseAPI/API/Unflect/UStructCast.uc @@ -0,0 +1,30 @@ +/** + * One of the original Unflect files. + * Copyright 2022-2023 EliotVU + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class UStructCast extends Object; + +var UStruct nativeType; + +final function UStruct Cast(/*Core.Struct*/ Object type) { + super(TypeCast).NativeCast(type); + return nativeType; +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/UTextBuffer.uc b/sources/BaseAPI/API/Unflect/UTextBuffer.uc new file mode 100644 index 0000000..a0f8015 --- /dev/null +++ b/sources/BaseAPI/API/Unflect/UTextBuffer.uc @@ -0,0 +1,28 @@ +/** + * One of the original Unflect files. + * Copyright 2022-2023 EliotVU + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class UTextBuffer extends Object; + +var private native const pointer outputDeviceVtbl; + +var int pos, top; +var string text; + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/Unflect.uc b/sources/BaseAPI/API/Unflect/Unflect.uc new file mode 100644 index 0000000..dc6a827 --- /dev/null +++ b/sources/BaseAPI/API/Unflect/Unflect.uc @@ -0,0 +1,32 @@ +/** + * One of the original Unflect files. + * Copyright 2022-2023 EliotVU + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class Unflect extends Object + abstract; + +struct Int16 { + var byte h, l; +}; + +struct Int64 { + var int h, l; +}; + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Unflect/UnflectApi.uc b/sources/BaseAPI/API/Unflect/UnflectApi.uc new file mode 100644 index 0000000..a41e791 --- /dev/null +++ b/sources/BaseAPI/API/Unflect/UnflectApi.uc @@ -0,0 +1,409 @@ +/** + * Config class for storing map lists. + * Copyright 2020 bibibi + * 2020-2023 Shtoyan + * 2023 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class UnflectApi extends AcediaObject; + +//! This API offers advanced reflection capabilities for Unreal Script. +//! +//! Currently, the API supports the ability to replace the code of existing functions with +//! custom code. +//! This can greatly simplify the process of bug fixing and hooking into game events. + +/// A variable responsible for replacing function code in real-time. +/// This variable is used to support dynamic function replacement/patching and event interception +/// at runtime. +var private UFunctionCast functionCaster; + +/// Maps lower case function name (specifies by the full path "package.class.functionName") +/// to a `FunctionRule` that completely describes how it was replaced +var private HashTable completedReplacements; +/// Maps lower case function name (specifies by the full path "package.class.functionName") +/// to the `ByteArrayBox` with that function's original code. +var private HashTable originalScriptCodes; + +var private LoggerAPI.Definition warnSameFunction; +var private LoggerAPI.Definition warnOverridingReplacement, errFailedToFindFunction; +var private LoggerAPI.Definition errReplacementWithoutSources, errCannotCreateReplacementRule; + +protected function Constructor() { + functionCaster = new class'UFunctionCast'; + completedReplacements = _.collections.EmptyHashTable(); + originalScriptCodes = _.collections.EmptyHashTable(); +} + +protected function Finalizer() { + _.memory.Free(completedReplacements); + _.memory.Free(originalScriptCodes); + completedReplacements = none; + originalScriptCodes = none; + functionCaster = none; +} + +/// Reverts the replacement of the function's code, restoring its original behavior. +/// +/// The function to be reverted should be specified using its full path, in the format +/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash"). +/// +/// It's worth noting that several function replacements do not stack. +/// Even if [`ReplaceFunction()`] was called multiple times in a row to replace the same function, +/// this method will cancel all the changes at once. +/// +/// This method returns true if the specified function was previously replaced and has now been +/// successfully reverted. +/// +/// # Errors +/// +/// If the specified function cannot be found (but [`functionName`] isn't `none`), or if +/// UnflectApi has not yet replaced it with any other function, this method will log an error. +public final function bool RevertFunction(BaseText functionName) { + local bool result; + local FunctionReplacement storedReplacement; + local ByteArrayBox storedSources; + local Text functionNameLowerCase; + local UFunction functionInstance; + local SideEffect sideEffect; + + if (functionName == none) { + return false; + } + functionNameLowerCase = functionName.LowerCopy(); + storedReplacement = FunctionReplacement(completedReplacements.GetItem(functionNameLowerCase)); + if (storedReplacement != none) { + storedSources = ByteArrayBox(originalScriptCodes.GetItem(functionNameLowerCase)); + if (storedSources == none) { + _.logger.Auto(errReplacementWithoutSources).Arg(functionNameLowerCase.Copy()); + } else { + functionInstance = FindFunction(functionNameLowerCase); + if (functionInstance != none) { + functionInstance.script = storedSources.Get(); + completedReplacements.RemoveItem(functionNameLowerCase); + sideEffect = storedReplacement.GetSideEffect(); + _.sideEffects.RemoveInstance(sideEffect); + result = true; + } else { + _.logger.Auto(errFailedToFindFunction).Arg(functionNameLowerCase.Copy()); + } + } + } + _.memory.Free4(storedReplacement, functionNameLowerCase, storedSources, sideEffect); + return result; +} + +/// Reverts the replacement of the function's code, restoring its original behavior. +/// +/// The function to be reverted should be specified using its full path, in the format +/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash"). +/// +/// It's worth noting that several function replacements do not stack. +/// Even if [`ReplaceFunction()`] was called multiple times in a row to replace the same function, +/// this method will cancel all the changes at once. +/// +/// This method returns true if the specified function was previously replaced and has now been +/// successfully reverted. +/// +/// # Errors +/// +/// If the specified function cannot be found (but [`functionName`] isn't `none`), or if +/// UnflectApi has not yet replaced it with any other function, this method will log an error. +public final function bool RevertFunction_S(string functionName) { + local bool result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(functionName); + result = RevertFunction(wrapper); + _.memory.Free(wrapper); + return result; +} + +/// Determines whether the specified function has been replaced by UnflectApi. +/// +/// The function to be checked should be specified using its full path, in the format +/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash"). +/// +/// If the function has been replaced, this method will return `true`; +/// otherwise, it will return `false`. +public final function bool IsFunctionReplaced(BaseText functionName) { + local bool result; + local Text functionNameLowerCase; + + if (functionName == none) { + return false; + } + functionNameLowerCase = functionName.LowerCopy(); + result = completedReplacements.HasKey(functionNameLowerCase); + _.memory.Free(functionNameLowerCase); + return result; +} + +/// Determines whether the specified function has been replaced by UnflectApi. +/// +/// The function to be checked should be specified using its full path, in the format +/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash"). +/// +/// If the function has been replaced, this method will return `true`; +/// otherwise, it will return `false`. +public final function bool IsFunctionReplaced_S(string functionName) { + local bool result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(functionName); + result = IsFunctionReplaced(wrapper); + _.memory.Free(wrapper); + return result; +} + +/// Replaces one function with another by modifying its script code in real-time. +/// +/// The reason for replacement must be specified and will serve as a human-readable explanation +/// for a [SideEffect] associated with the replacement. +/// +/// If you need to replace a function in a class, follow these steps: +/// +/// 1. Create a new class that extends the class in which the function you want to replace is +/// located. +/// 2. Declare that function in the created class. +/// 3. **DO NOT** change the function declaration and argument types/amount. +/// 4. **DO NOT** create new local variables, as this can cause random crashes. +/// If you need additional variables, make them global and access them using the +/// `class'myNewClass'.default.myNewVariable` syntax. +/// 5. If you want to call or override parent code, make sure to always specify the desired parent +/// class name. +/// For example, use `super(TargetClass).PostBeginPlay()` instead of `super.PostBeginPlay()`. +/// This will prevent runaway loop crashes. +/// 6. Make your edits to the function's code, and then call the replacement function: +/// ```unrealscript +/// _.unflect.ReplaceFunction( +/// "package.class.targetFunction", +/// "myNewPackage.myNewClass.newFunction"); +/// ``` +/// +/// Following these steps will help ensure that your code changes are compatible with the rest of +/// the codebase and do not cause unexpected crashes. +/// +/// # Errors +/// +/// This method can log error messages in cases where: +/// +/// * The specified function(s) cannot be found. +/// * An attempt is made to replace a function with itself. +/// * An attempt is made to replace a function that has already been replaced. +public final function bool ReplaceFunction( + BaseText oldFunction, + BaseText newFunction, + BaseText replacementReason +) { + local bool result; + local Text oldFunctionLowerCase, newFunctionLowerCase; + + if (oldFunction == none) return false; + if (newFunction == none) return false; + + oldFunctionLowerCase = oldFunction.LowerCopy(); + newFunctionLowerCase = newFunction.LowerCopy(); + result = _replaceFunction(oldFunctionLowerCase, newFunctionLowerCase); + if (result) { + RecordNewReplacement(oldFunctionLowerCase, newFunctionLowerCase, replacementReason); + } + _.memory.Free2(oldFunctionLowerCase, newFunctionLowerCase); + return result; +} + +/// Replaces one function with another by modifying its script code in real-time. +/// +/// The reason for replacement must be specified and will serve as a human-readable explanation +/// for a [SideEffect] associated with the replacement. +/// +/// If you need to replace a function in a class, follow these steps: +/// +/// 1. Create a new class that extends the class in which the function you want to replace is +/// located. +/// 2. Declare that function in the created class. +/// 3. **DO NOT** change the function declaration and argument types/amount. +/// 4. **DO NOT** create new local variables, as this can cause random crashes. +/// If you need additional variables, make them global and access them using the +/// `class'myNewClass'.default.myNewVariable` syntax. +/// 5. If you want to call or override parent code, make sure to always specify the desired parent +/// class name. +/// For example, use `super(TargetClass).PostBeginPlay()` instead of `super.PostBeginPlay()`. +/// This will prevent runaway loop crashes. +/// 6. Make your edits to the function's code, and then call the replacement function: +/// ```unrealscript +/// _.unflect.ReplaceFunction( +/// "package.class.targetFunction", +/// "myNewPackage.myNewClass.newFunction"); +/// ``` +/// +/// Following these steps will help ensure that your code changes are compatible with the rest of +/// the codebase and do not cause unexpected crashes. +/// +/// # Errors +/// +/// This method can log error messages in cases where: +/// +/// * The specified function(s) cannot be found. +/// * An attempt is made to replace a function with itself. +/// * An attempt is made to replace a function that has already been replaced. +public final function bool ReplaceFunction_S( + string oldFunction, + string newFunction, + string replacementReason +) { + local Text oldWrapper, newWrapper, reasonWrapper; + local bool result; + + oldWrapper = _.text.FromString(oldFunction); + newWrapper = _.text.FromString(newFunction); + reasonWrapper = _.text.FromString(replacementReason); + result = ReplaceFunction(oldWrapper, newWrapper, reasonWrapper); + _.memory.Free3(oldWrapper, newWrapper, reasonWrapper); + return result; +} + +// Does actual work for function replacement. +// Arguments are assumed to be not `none` and in lower case. +private final function bool _replaceFunction(Text oldFunctionLowerCase, Text newFunctionLowerCase) { + local ByteArrayBox initialCode; + local UFunction replace, with; + + replace = FindFunction(oldFunctionLowerCase); + if (replace == none) { + _.logger.Auto(errFailedToFindFunction).Arg(oldFunctionLowerCase.Copy()); + return false; + } + with = FindFunction(newFunctionLowerCase); + if (with == none) { + _.logger.Auto(errFailedToFindFunction).Arg(newFunctionLowerCase.Copy()); + return false; + } + if (replace == with) { + _.logger.Auto(warnSameFunction).Arg(oldFunctionLowerCase.Copy()); + return false; + } + // Remember old code, if haven't done so yet. + // Since we attempt it on each replacement, the first recorded `script` value will be + // the initial code. + if (!originalScriptCodes.HasKey(oldFunctionLowerCase)) { + initialCode = _.box.ByteArray(replace.script); + originalScriptCodes.SetItem(oldFunctionLowerCase, initialCode); + _.memory.Free(initialCode); + } + replace.script = with.script; + return true; +} + +// Arguments assumed to be not `none` and in lower case. +private final function UFunction FindFunction(Text functionNameLowerCase) { + local string stringName; + + stringName = functionNameLowerCase.ToString(); + // We need to make sure functions are loaded before performing the replacement. + DynamicLoadObject(GetClassName(stringName), class'class', true); + return functionCaster.Cast(function(FindObject(stringName, class'Function'))); +} + +// Arguments are assumed to be not `none`. +// `oldFunctionLowerCase` and `newFunctionLowerCase` are assumed to be in lower case. +private final function RecordNewReplacement( + Text oldFunctionLowerCase, + Text newFunctionLowerCase, + BaseText replacementReason +) { + local SideEffect oldSideEffect, newSideEffect; + local FunctionReplacement oldRule, newRule; + + // Remove old `FunctionReplacement`, if there is any + oldRule = FunctionReplacement(completedReplacements.GetItem(oldFunctionLowerCase)); + if (oldRule != none) { + _.logger + .Auto(warnOverridingReplacement) + .Arg(oldFunctionLowerCase.Copy()) + .Arg(oldRule.GetReplacerFunctionName()) + .Arg(newFunctionLowerCase.Copy()); + oldSideEffect = oldRule.GetSideEffect(); + _.sideEffects.RemoveInstance(oldSideEffect); + _.memory.Free2(oldRule, oldSideEffect); + } + // Create replacement instance + newSideEffect = MakeSideEffect(oldFunctionLowerCase, newFunctionLowerCase, replacementReason); + newRule = class'FunctionReplacement'.static + .Make(oldFunctionLowerCase, newFunctionLowerCase, newSideEffect); + completedReplacements.SetItem(oldFunctionLowerCase, newRule); + if (newRule == none) { + _.logger + .Auto(errCannotCreateReplacementRule) + .Arg(oldFunctionLowerCase.Copy()) + .Arg(newFunctionLowerCase.Copy()); + } +} + +// Arguments are assumed to be not `none`. +// `oldFunctionLowerCase` and `newFunctionLowerCase` are assumed to be in lower case. +private final function SideEffect MakeSideEffect( + Text oldFunctionLowerCase, + Text newFunctionLowerCase, + BaseText replacementReason +) { + local SideEffect sideEffect; + local MutableText status; + + // Add side effect + status = oldFunctionLowerCase.MutableCopy(); + status.Append(P(" -> ")); + status.Append(newFunctionLowerCase); + sideEffect = _.sideEffects.Add( + P("Changed function's code"), + replacementReason, + P("AcediaCore"), + P("UnflectAPI"), + status + ); + _.memory.Free(status); + return sideEffect; +} + +// Get the "package + dot + class" string for DynamicLoadObject() +private final static function string GetClassName(string input) { + local array parts; + + // create an array + Split(input, ".", parts); + + // state functions + if (parts.length < 3) { + return ""; + } + if (parts.length == 4) { + ReplaceText(input, "." $ parts[2], ""); + ReplaceText(input, "." $ parts[3], ""); + } else { + ReplaceText(input, "." $ parts[2], ""); + } + return input; +} + + +defaultproperties { + warnOverridingReplacement = (l=LOG_Error,m="Attempt to replace a function `%1` with function `%3` after it has already been replaced with `%2`.") + warnSameFunction = (l=LOG_Error,m="Attempt to replace a function `%1` with itself.") + errFailedToFindFunction = (l=LOG_Error,m="`UnflectApi` has failed to find function `%1`.") + errReplacementWithoutSources = (l=LOG_Error,m="Cannot restore function `%1` - its initial source code wasn't preserved. This most likely means that it wasn't yet replaced.") + errCannotCreateReplacementRule = (l=LOG_Error,m="`Cannot create new rule for replacing function `%1` with `%2` even though code was successfully replaces. This should happen, please report this.") +} \ No newline at end of file diff --git a/sources/BaseAPI/Global.uc b/sources/BaseAPI/Global.uc index 4163f34..01fbafe 100644 --- a/sources/BaseAPI/Global.uc +++ b/sources/BaseAPI/Global.uc @@ -35,6 +35,7 @@ class Global extends Object; // For getting instance of [`Global`] from any object. var protected Global myself; +var public UnflectApi unflect; var public SideEffectAPI sideEffects; var public RefAPI ref; var public BoxAPI box; @@ -76,7 +77,7 @@ public final static function Global GetInstance() { protected function Initialize() { // Special case that we cannot spawn with memory API since it obviously // does not exist yet! - memory = new class'MemoryAPI'; + memory = new class'MemoryAPI'; memory._constructor(); // `TextAPI` and `CollectionsAPI` need to be loaded before `LoggerAPI` sideEffects = SideEffectAPI(memory.Allocate(class'SideEffectAPI')); @@ -85,6 +86,7 @@ protected function Initialize() { text = TextAPI(memory.Allocate(class'TextAPI')); math = MathAPI(memory.Allocate(class'MathAPI')); collections = CollectionsAPI(memory.Allocate(class'CollectionsAPI')); + unflect = UnflectAPI(memory.Allocate(class'UnflectAPI')); json = JsonAPI(memory.Allocate(class'JsonAPI')); logger = LoggerAPI(memory.Allocate(class'LoggerAPI')); color = ColorAPI(memory.Allocate(class'ColorAPI')); diff --git a/sources/ClientAPI/ClientAcediaAdapter.uc b/sources/ClientAPI/ClientAcediaAdapter.uc index f01b15a..95d5be1 100644 --- a/sources/ClientAPI/ClientAcediaAdapter.uc +++ b/sources/ClientAPI/ClientAcediaAdapter.uc @@ -27,7 +27,6 @@ var public const class clientInteractionAPIClass; defaultproperties { - sideEffectAPIClass = class'KF1_SideEffectAPI' timeAPIClass = class'KF1_TimeAPI' dbAPIClass = class'DBAPI' clientUnrealAPIClass = class'KF1_ClientUnrealAPI' diff --git a/sources/Manifest.uc b/sources/Manifest.uc index 17dae9a..ce9bd9c 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -59,4 +59,5 @@ defaultproperties testCases(30) = class'TEST_AcediaConfig' testCases(31) = class'TEST_UTF8EncoderDecoder' testCases(32) = class'TEST_AvariceStreamReader' + testCases(33) = class'TEST_Unflect' } \ No newline at end of file diff --git a/sources/ServerAPI/ServerAcediaAdapter.uc b/sources/ServerAPI/ServerAcediaAdapter.uc index 4bcac4a..7498f37 100644 --- a/sources/ServerAPI/ServerAcediaAdapter.uc +++ b/sources/ServerAPI/ServerAcediaAdapter.uc @@ -30,7 +30,6 @@ var public const class serverMutatorAPIClass; defaultproperties { - sideEffectAPIClass = class'KF1_SideEffectAPI' timeAPIClass = class'KF1_TimeAPI' dbAPIClass = class'DBAPI' serverUnrealAPIClass = class'KF1_ServerUnrealAPI' diff --git a/sources/ServerAPI/ServerGlobal.uc b/sources/ServerAPI/ServerGlobal.uc index 67fefcd..45dc38c 100644 --- a/sources/ServerAPI/ServerGlobal.uc +++ b/sources/ServerAPI/ServerGlobal.uc @@ -83,7 +83,7 @@ public final function bool ConnectServerLevelCore() return false; } Initialize(); - if (class'SideEffects'.default.allowHookingIntoMutate) + if (class'ServerSideEffects'.default.allowHookingIntoMutate) { _ = class'Global'.static.GetInstance(); class'InfoQueryHandler'.static.StaticConstructor();