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();