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 f53553f..01fbafe 100644
--- a/sources/BaseAPI/Global.uc
+++ b/sources/BaseAPI/Global.uc
@@ -35,6 +35,8 @@ 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;
var public MathAPI math;
@@ -75,14 +77,16 @@ 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'));
ref = RefAPI(memory.Allocate(class'RefAPI'));
box = BoxAPI(memory.Allocate(class'BoxAPI'));
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/Gameplay/KF1Frontend/Health/KF1_HealthComponent.uc b/sources/Gameplay/KF1Frontend/Health/KF1_HealthComponent.uc
index 1684437..d290ad0 100644
--- a/sources/Gameplay/KF1Frontend/Health/KF1_HealthComponent.uc
+++ b/sources/Gameplay/KF1Frontend/Health/KF1_HealthComponent.uc
@@ -67,7 +67,7 @@ private final function TryReplaceDamageTypes()
return;
}
triedToReplaceDamageTypes = true;
- if (!class'SideEffects'.default.allowReplacingDamageTypes)
+ if (!class'ServerSideEffects'.default.allowReplacingDamageTypes)
{
_.logger.Auto(warnReplacingDamageTypesForbidden);
return;
diff --git a/sources/KFImplementation/Core/KF1_SideEffectAPI.uc b/sources/KFImplementation/Core/KF1_SideEffectAPI.uc
deleted file mode 100644
index 6bd78d2..0000000
--- a/sources/KFImplementation/Core/KF1_SideEffectAPI.uc
+++ /dev/null
@@ -1,115 +0,0 @@
-/**
- * Standard implementation for simple API for managing a list of
- * `SideEffect` info objects: can add, remove, return all and by package.
- * Copyright 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 KF1_SideEffectAPI extends SideEffectAPI;
-
-var private array activeSideEffects;
-
-public function array GetAll()
-{
- local int i;
-
- for (i = 0; i < activeSideEffects.length; i += 1) {
- activeSideEffects[i].NewRef();
- }
- return activeSideEffects;
-}
-
-public function SideEffect GetClass(class sideEffectClass)
-{
- local int i;
-
- if (sideEffectClass == none) {
- return none;
- }
- for (i = 0; i < activeSideEffects.length; i += 1)
- {
- if (activeSideEffects[i].class == sideEffectClass)
- {
- activeSideEffects[i].NewRef();
- return activeSideEffects[i];
- }
- }
- return none;
-}
-
-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 (nextPackage.Compare(packageName, SCASE_INSENSITIVE))
- {
- activeSideEffects[i].NewRef();
- result[result.length] = activeSideEffects[i];
- }
- _.memory.Free(nextPackage);
- }
- return result;
-}
-
-public function bool Add(SideEffect newSideEffect)
-{
- local int i;
-
- if (newSideEffect == none) {
- return false;
- }
- for (i = 0; i < activeSideEffects.length; i += 1)
- {
- if (activeSideEffects[i].class == newSideEffect.class) {
- return false;
- }
- }
- newSideEffect.NewRef();
- activeSideEffects[activeSideEffects.length] = newSideEffect;
- return true;
-}
-
-public function bool RemoveClass(
- class sideEffectClass)
-{
- local int i;
-
- if (sideEffectClass == none) {
- return false;
- }
- for (i = 0; i < activeSideEffects.length; i += 1)
- {
- if (activeSideEffects[i].class == sideEffectClass)
- {
- _.memory.Free(activeSideEffects[i]);
- activeSideEffects.Remove(i, 1);
- return true;
- }
- }
- return false;
-}
-
-defaultproperties
-{
-}
\ No newline at end of file
diff --git a/sources/LevelAPI/SideEffects.uc b/sources/KFImplementation/Server/ServerSideEffects.uc
similarity index 99%
rename from sources/LevelAPI/SideEffects.uc
rename to sources/KFImplementation/Server/ServerSideEffects.uc
index 8ad922a..7f56037 100644
--- a/sources/LevelAPI/SideEffects.uc
+++ b/sources/KFImplementation/Server/ServerSideEffects.uc
@@ -19,7 +19,7 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class SideEffects extends AcediaObject
+class ServerSideEffects extends AcediaObject
dependson(BroadcastAPI)
abstract
config(AcediaSystem);
diff --git a/sources/KFImplementation/Server/Unreal/BroadcastsAPI/BroadcastEventsObserver.uc b/sources/KFImplementation/Server/Unreal/BroadcastsAPI/BroadcastEventsObserver.uc
index 0f6f0ef..1a4e116 100644
--- a/sources/KFImplementation/Server/Unreal/BroadcastsAPI/BroadcastEventsObserver.uc
+++ b/sources/KFImplementation/Server/Unreal/BroadcastsAPI/BroadcastEventsObserver.uc
@@ -207,7 +207,7 @@ var private Broadcast_OnHandleTextFor_Signal onHandleTextFor;
public final function Initialize(ServerUnrealService service)
{
usedInjectionLevel =
- class'SideEffects'.default.broadcastHandlerInjectionLevel;
+ class'ServerSideEffects'.default.broadcastHandlerInjectionLevel;
if (usedInjectionLevel == BHIJ_Root) {
Disable('Tick');
}
diff --git a/sources/KFImplementation/Server/Unreal/BroadcastsAPI/BroadcastSideEffect.uc b/sources/KFImplementation/Server/Unreal/BroadcastsAPI/BroadcastSideEffect.uc
deleted file mode 100644
index 0a136d7..0000000
--- a/sources/KFImplementation/Server/Unreal/BroadcastsAPI/BroadcastSideEffect.uc
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * Object representing a side effect introduced into the game/server.
- * Side effects in Acedia refer to changes that aren't a part of mod's main
- * functionality, but rather something necessary to make that functionality
- * possible that might also affect how other mods work.
- * This is a simple data container that is meant to describe relevant
- * changes to the human user.
- * Copyright 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 BroadcastSideEffect extends SideEffect
- dependson(BroadcastAPI);
-
-public final function Initialize(BroadcastAPI.InjectionLevel usedInjectionLevel)
-{
- sideEffectName =
- _.text.FromString("AcediaCore's `BroadcastHandler` injected");
- sideEffectDescription =
- _.text.FromString("Handling text and localized messages between server"
- @ "and clients requires AcediaCore to add its own `BroadcastHandler`"
- @ "into their linked list."
- @ "This is normal, since `BroadcastHandler` class was designed to allow"
- @ "mods to do that, however, for full functionality Acedia requires to"
- @ "inject it as the very first element (`BHIJ_Root` level injection),"
- @ "since some of the events become otherwise inaccessible."
- @ "This can result in incompatibility with other mods that are trying"
- @ "to do the same."
- @ "For that reason AcediaCore can also inject its `BroadcastHandler` as"
- @ "`BHIJ_Registered`.");
- sideEffectPackage = _.text.FromString("AcediaCore");
- sideEffectSource = _.text.FromString("UnrealAPI");
- if (usedInjectionLevel == BHIJ_Root)
- {
- sideEffectStatus =
- _.text.FromFormattedString("{$TextPositive BHIJ_Root}");
- }
- else if (usedInjectionLevel == BHIJ_Registered)
- {
- sideEffectStatus =
- _.text.FromFormattedString("{$TextNetutral BHIJ_Registered}");
- }
- else
- {
- sideEffectStatus =
- _.text.FromFormattedString("{$TextNegative BHIJ_None (???)}");
- }
-}
-
-defaultproperties
-{
-}
\ No newline at end of file
diff --git a/sources/KFImplementation/Server/Unreal/BroadcastsAPI/KF1_BroadcastAPI.uc b/sources/KFImplementation/Server/Unreal/BroadcastsAPI/KF1_BroadcastAPI.uc
index 0dd5feb..3b3da3f 100644
--- a/sources/KFImplementation/Server/Unreal/BroadcastsAPI/KF1_BroadcastAPI.uc
+++ b/sources/KFImplementation/Server/Unreal/BroadcastsAPI/KF1_BroadcastAPI.uc
@@ -1,6 +1,6 @@
/**
* Acedia's default implementation for `BroadcastAPI`.
- * Copyright 2021-2022 Anton Tarasenko
+ * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -23,7 +23,6 @@ class KF1_BroadcastAPI extends BroadcastAPI;
// wasting resources/spamming errors in the log about our inability to do so
var private bool triedToInjectBroadcastHandler;
-var private LoggerAPI.Definition infoInjectedBroadcastEventsObserver;
var private LoggerAPI.Definition errBroadcasthandlerForbidden;
var private LoggerAPI.Definition errBroadcasthandlerUnknown;
@@ -94,7 +93,7 @@ public function Broadcast_OnHandleLocalizedFor_Slot OnHandleLocalizedFor(
/**
* Method that attempts to inject Acedia's `BroadcastEventObserver`, while
- * respecting settings inside `class'SideEffects'`.
+ * respecting settings inside `class'ServerSideEffects'`.
*
* @param service Reference to `ServerUnrealService` to exchange signal and
* slots classes with.
@@ -102,27 +101,19 @@ public function Broadcast_OnHandleLocalizedFor_Slot OnHandleLocalizedFor(
protected final function TryInjectBroadcastHandler(ServerUnrealService service)
{
local InjectionLevel usedLevel;
- local BroadcastSideEffect sideEffect;
local BroadcastEventsObserver broadcastObserver;
if (triedToInjectBroadcasthandler) {
return;
}
triedToInjectBroadcasthandler = true;
- usedLevel = class'SideEffects'.default.broadcastHandlerInjectionLevel;
+ usedLevel = class'ServerSideEffects'.default.broadcastHandlerInjectionLevel;
broadcastObserver = BroadcastEventsObserver(_server.unreal.broadcasts.Add(
class'BroadcastEventsObserver', usedLevel));
if (broadcastObserver != none)
{
broadcastObserver.Initialize(service);
- sideEffect =
- BroadcastSideEffect(_.memory.Allocate(class'BroadcastSideEffect'));
- sideEffect.Initialize(usedLevel);
- _server.sideEffects.Add(sideEffect);
- _.memory.Free(sideEffect);
- _.logger
- .Auto(infoInjectedBroadcastEventsObserver)
- .Arg(InjectionLevelIntoText(usedLevel));
+ AddSideEffect();
return;
}
// We are here if we have failed
@@ -137,6 +128,35 @@ protected final function TryInjectBroadcastHandler(ServerUnrealService service)
}
}
+private final static function AddSideEffect() {
+ local InjectionLevel usedLevel;
+ local Text sideEffectStatus;
+ local SideEffect newSideEffect;
+
+ usedLevel = class'ServerSideEffects'.default.broadcastHandlerInjectionLevel;
+ if (usedLevel == BHIJ_Root) {
+ sideEffectStatus = __().text.FromFormattedString("{$TextPositive BHIJ_Root}");
+ } else if (usedLevel == BHIJ_Registered) {
+ sideEffectStatus = __().text.FromFormattedString("{$TextNeutral BHIJ_Registered}");
+ } else {
+ sideEffectStatus = __().text.FromFormattedString("{$TextNegative BHIJ_None (???)}");
+ }
+ newSideEffect = __().sideEffects.Add(
+ P("AcediaCore's `BroadcastHandler` injected"),
+ P("Handling text and localized messages between server and clients requires AcediaCore to"
+ @ "add its own `BroadcastHandler` into their linked list. This is normal, since"
+ @ "`BroadcastHandler` class was designed to allow mods to do that, however, for full"
+ @ "functionality Acedia requires to inject it as the very first element"
+ @ "(`BHIJ_Root` level injection), since some of the events become otherwise"
+ @ "inaccessible. This can result in incompatibility with other mods that are trying to"
+ @ "do the same. For that reason AcediaCore can also inject its `BroadcastHandler` as"
+ @ "`BHIJ_Registered`."),
+ P("AcediaCore"),
+ P("UnrealAPI"),
+ sideEffectStatus);
+ __().memory.Free(newSideEffect);
+}
+
private final function Text InjectionLevelIntoText(
InjectionLevel injectionLevel)
{
@@ -257,7 +277,6 @@ public function bool IsAdded(class BHClassToFind)
defaultproperties
{
- infoInjectedBroadcastEventsObserver = (l=LOG_Info,m="Injected AcediaCore's `BroadcastEventsObserver` with level `%1`.")
- errBroadcastHandlerForbidden = (l=LOG_Error,m="Injected AcediaCore's `BroadcastEventsObserver` is required, but forbidden by AcediaCore's settings: in file \"AcediaSystem.ini\", section [AcediaCore.SideEffects], variable `broadcastHandlerInjectionLevel`.")
- errBroadcastHandlerUnknown = (l=LOG_Error,m="Injected AcediaCore's `BroadcastEventsObserver` failed to be injected with level `%1` for unknown reason.")
+ errBroadcastHandlerForbidden = (l=LOG_Error,m="Injected AcediaCore's `BroadcastEventsObserver` is required, but forbidden by AcediaCore's settings: in file \"AcediaSystem.ini\", section [AcediaCore.SideEffects], variable `broadcastHandlerInjectionLevel`.")
+ errBroadcastHandlerUnknown = (l=LOG_Error,m="Injected AcediaCore's `BroadcastEventsObserver` failed to be injected with level `%1` for unknown reason.")
}
\ No newline at end of file
diff --git a/sources/KFImplementation/Server/Unreal/GameRulesAPI/GameRulesSideEffect.uc b/sources/KFImplementation/Server/Unreal/GameRulesAPI/GameRulesSideEffect.uc
deleted file mode 100644
index da70011..0000000
--- a/sources/KFImplementation/Server/Unreal/GameRulesAPI/GameRulesSideEffect.uc
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Object representing a side effect introduced into the game/server.
- * Side effects in Acedia refer to changes that aren't a part of mod's main
- * functionality, but rather something necessary to make that functionality
- * possible that might also affect how other mods work.
- * This is a simple data container that is meant to describe relevant
- * changes to the human user.
- * Copyright 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 GameRulesSideEffect extends SideEffect;
-
-public final function Initialize()
-{
- sideEffectName =
- _.text.FromString("AcediaCore's `AcediaGameRules` added");
- sideEffectDescription =
- _.text.FromString("`GameRule`s is one of the main ways to get notified"
- @ "about various gameplay-related events in Unreal Engine."
- @ "Of course AcediaCore would require handling some of those events,"
- @ "depending on how it's used.");
- sideEffectPackage = _.text.FromString("AcediaCore");
- sideEffectSource = _.text.FromString("UnrealAPI");
- sideEffectStatus = _.text.FromFormattedString("{$TextPositive active}");
-}
-
-defaultproperties
-{
-}
\ No newline at end of file
diff --git a/sources/KFImplementation/Server/Unreal/GameRulesAPI/KF1_GameRulesAPI.uc b/sources/KFImplementation/Server/Unreal/GameRulesAPI/KF1_GameRulesAPI.uc
index 6cab0b4..8f00da5 100644
--- a/sources/KFImplementation/Server/Unreal/GameRulesAPI/KF1_GameRulesAPI.uc
+++ b/sources/KFImplementation/Server/Unreal/GameRulesAPI/KF1_GameRulesAPI.uc
@@ -1,6 +1,6 @@
/**
* Acedia's default implementation for `GameRulesAPI` API.
- * Copyright 2021-2022 Anton Tarasenko
+ * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -23,7 +23,6 @@ class KF1_GameRulesAPI extends GameRulesAPI;
// wasting resources/spamming errors in the log about our inability to do so
var private bool triedToInjectGameRules;
-var private LoggerAPI.Definition infoAddedGameRules;
var private LoggerAPI.Definition errGameRulesForbidden;
var private LoggerAPI.Definition errGameRulesUnknown;
@@ -132,21 +131,20 @@ public function GameRules_OnScoreKill_Slot OnScoreKill(
/**
* Method that attempts to inject Acedia's `AcediaGameRules`, while
- * respecting settings inside `class'SideEffects'`.
+ * respecting settings inside `class'ServerSideEffects'`.
*
* @param service Reference to `ServerUnrealService` to exchange signal and
* slots classes with.
*/
protected function TryAddingGameRules(ServerUnrealService service)
{
- local AcediaGameRules gameRules;
- local GameRulesSideEffect sideEffect;
+ local AcediaGameRules gameRules;
if (triedToInjectGameRules) {
return;
}
triedToInjectGameRules = true;
- if (!class'SideEffects'.default.allowAddingGameRules)
+ if (!class'ServerSideEffects'.default.allowAddingGameRules)
{
_.logger.Auto(errGameRulesForbidden);
return;
@@ -155,18 +153,27 @@ protected function TryAddingGameRules(ServerUnrealService service)
if (gameRules != none)
{
gameRules.Initialize(service);
- sideEffect =
- GameRulesSideEffect(_.memory.Allocate(class'GameRulesSideEffect'));
- sideEffect.Initialize();
- _server.sideEffects.Add(sideEffect);
- _.memory.Free(sideEffect);
- _.logger.Auto(infoAddedGameRules);
+ AddSideEffect();
}
else {
_.logger.Auto(errGameRulesUnknown);
}
}
+private final static function AddSideEffect() {
+ local SideEffect newSideEffect;
+
+ newSideEffect = __().sideEffects.Add(
+ P("AcediaCore's `AcediaGameRules` added"),
+ P("`GameRule`s is one of the main ways to get notified about various gameplay-related"
+ @ "events in Unreal Engine. Of course AcediaCore would require handling some of those"
+ @ "events, depending on how it's used."),
+ P("AcediaCore"),
+ P("UnrealAPI"),
+ F("{$TextPositive active}"));
+ __().memory.Free(newSideEffect);
+}
+
public function GameRules Add(class newRulesClass)
{
local GameRules newGameRules;
@@ -236,7 +243,6 @@ public function bool AreAdded(class rulesClassToCheck)
defaultproperties
{
- infoAddedGameRules = (l=LOG_Info,m="Added AcediaCore's `AcediaGameRules`.")
- errGameRulesForbidden = (l=LOG_Error,m="Adding AcediaCore's `AcediaGameRules` is required, but forbidden by AcediaCore's settings: in file \"AcediaSystem.ini\", section [AcediaCore.SideEffects], variable `allowAddingGameRules`.")
- errGameRulesUnknown = (l=LOG_Error,m="Adding AcediaCore's `AcediaGameRules` failed to be injected with level for unknown reason.")
+ errGameRulesForbidden = (l=LOG_Error,m="Adding AcediaCore's `AcediaGameRules` is required, but forbidden by AcediaCore's settings: in file \"AcediaSystem.ini\", section [AcediaCore.SideEffects], variable `allowAddingGameRules`.")
+ errGameRulesUnknown = (l=LOG_Error,m="Adding AcediaCore's `AcediaGameRules` failed to be injected with level for unknown reason.")
}
\ No newline at end of file
diff --git a/sources/LevelAPI/API/SideEffects/SideEffect.uc b/sources/LevelAPI/API/SideEffects/SideEffect.uc
deleted file mode 100644
index 928e7e4..0000000
--- a/sources/LevelAPI/API/SideEffects/SideEffect.uc
+++ /dev/null
@@ -1,164 +0,0 @@
-/**
- * Object representing a side effect introduced into the game/server.
- * Side effects in Acedia refer to changes that aren't a part of mod's main
- * functionality, but rather something necessary to make that functionality
- * possible that might also affect how other mods work.
- * This is a simple data container that is meant to describe relevant
- * changes to the human user.
- * Copyright 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 SideEffect extends AcediaObject
- abstract;
-
-/**
- * # Side effects
- *
- * In Acedia "side effect" refers to changes that aren't a part of mod's
- * main functionality, but rather something necessary to make that
- * functionality possible that might also affect how other mods work. Their
- * purpose is to help developers and server admins figure out what changes were
- * performed by Acedia or mods based on it.
- * What needs to be considered a side effect is loosely defined and they
- * are simply a tool to inform others that something they might not have
- * expected has happened, that can possibly break other (their) mods.
- * AcediaCore, for example, tried to leave a minimal footprint, avoiding
- * making any changes to the game classes unless requested, but it still has to
- * do some changes (adding `GameRules`, replacing damage types for some zeds,
- * etc.) and `SideEffect`s can be used to document these changes. They can be
- * used in a similar way for AcediaFixes - a package that is only meant for
- * fixing bugs, but inevitably has to make a lot of under the hood changes to
- * achieve that.
- * On the other hand gameplay mods like Futility 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.
- *
- * ## Implementing your own `SideEffect`s
- *
- * Simply make a non-abstract child class based on `SideEffect`, create its
- * instance filled with necessary data and register it in `SideEffectAPI` once
- * side effect is introduced. If you revert introduced side effect, you should
- * remove registered object through the same API.
- * Each class of the `SideEffect` is supposed to represent a particular
- * side effect introduced into the game engine. Whether side effect is active
- * is decided by whether it is currently registered in `SideEffectAPI`.
- *
- * NOTE: `SideEffect` should not have any logic and should serve as
- * an immutable data container.
- */
-
-var protected Text sideEffectName;
-var protected Text sideEffectDescription;
-var protected Text sideEffectPackage;
-var protected Text sideEffectSource;
-var protected Text sideEffectStatus;
-
-/**
- * Returns name (short description) of the caller `SideEffect`. User need to
- * be able to tell what this `SideEffect` is generally about from the glance at
- * this name.
- *
- * Guideline is for this value to not exceed `80` characters, but this is not
- * enforced.
- *
- * Must not be `none`.
- *
- * @return Name (short description) of the caller `SideEffect`.
- * Guaranteed to not be `none`.
- */
-public function Text GetName()
-{
- if (sideEffectName != none) {
- return sideEffectName.Copy();
- }
- return none;
-}
-
-/**
- * Returns description of the caller `SideEffect`. This should describe what
- * was done and why relevant change was necessary.
- *
- * Must not be `none`.
- *
- * @return Description of the caller `SideEffect`.
- * Guaranteed to not be `none`.
- */
-public function Text GetDescription()
-{
- if (sideEffectDescription != none) {
- return sideEffectDescription.Copy();
- }
- return none;
-}
-
-/**
- * Returns name of the package ("*.u" file) that introduced this change.
- *
- * Note that if package "A" actually performed the change because another
- * package "B" requested certain functionality, it is still package "A" that is
- * responsible for the side effect.
- *
- * Must not be `none`.
- *
- * @return Name of the package ("*.u" file) that introduced this change.
- * Guaranteed to not be `none`.
- */
-public function Text GetPackage()
-{
- if (sideEffectPackage != none) {
- return sideEffectPackage.Copy();
- }
- return none;
-}
-
-/**
- * What part of package caused this change. For huge packages can be used to
- * further specify what introduced the change.
- *
- * Returned value can be `none` (e.g. when it is unnecessary for small
- * packages).
- *
- * @return Name (short description) of the part of the package that caused
- * caller `SideEffect`.
- */
-public function Text GetSource()
-{
- if (sideEffectSource != none) {
- return sideEffectSource.Copy();
- }
- return none;
-}
-
-/**
- * Status of the caller `SideEffect`. Some side effects can be introduced in
- * several different ways - this value needs to help distinguish between them.
- *
- * @return Status of the caller `SideEffect`.
- */
-public function Text GetStatus()
-{
- if (sideEffectStatus != none) {
- return sideEffectStatus.Copy();
- }
- return none;
-}
-
-defaultproperties
-{
-}
\ No newline at end of file
diff --git a/sources/LevelAPI/API/SideEffects/SideEffectAPI.uc b/sources/LevelAPI/API/SideEffects/SideEffectAPI.uc
deleted file mode 100644
index ed4ff2e..0000000
--- a/sources/LevelAPI/API/SideEffects/SideEffectAPI.uc
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * Base class for simple API for managing a list of `SideEffect` info
- * objects: can add, remove, return all and by package.
- * Copyright 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 SideEffectAPI extends AcediaObject
- abstract;
-
-/**
- * Returns all so far registered `SideEffect`s.
- *
- * @return Array of all registered `SideEffect`s.
- */
-public function array GetAll();
-
-/**
- * Returns active `SideEffect` instance of the specified class
- * `sideEffectClass`.
- *
- * @param sideEffectClass Class of side effect to return active instance of.
- * @return Active `SideEffect` instance of the specified class
- * `sideEffectClass`.
- * `none` if either `sideEffectClass` is `none` or side effect of such
- * class is not currently active.
- */
-public function SideEffect GetClass(class sideEffectClass);
-
-/**
- * Returns all so far registered `SideEffect`s from a package `packageName`
- * (case insensitive).
- *
- * @param packageName Name of the package, in `SideEffect`s from which we are
- * interested. Must be not `none`.
- * @return Array of all registered `SideEffect`s from a package `packageName`.
- * If `none`, returns an empty array.
- */
-public function array GetFromPackage(BaseText packageName);
-
-/**
- * Registers a new `SideEffect` object as active.
- *
- * @param newSideEffect Instance of some `SideEffect` class to register as
- * active side effect. Must not be `none`.
- * @return `true` if new side effect was added and `false` otherwise.
- */
-public function bool Add(SideEffect newSideEffect);
-
-/**
- * Removes `SideEffect` of the specified sub-class from the list of active
- * side effects.
- *
- * @param sideEffectClass Class of the side effect to remove.
- * @return `true` if some side effect was removed as a result of this operation
- * and `false` otherwise (even if there was no side effect of specified
- * class to begin with).
- */
-public function bool RemoveClass(class sideEffectClass);
-
-defaultproperties
-{
-}
\ No newline at end of file
diff --git a/sources/LevelAPI/AcediaAdapter.uc b/sources/LevelAPI/AcediaAdapter.uc
index 1b3ae94..e37bd1b 100644
--- a/sources/LevelAPI/AcediaAdapter.uc
+++ b/sources/LevelAPI/AcediaAdapter.uc
@@ -35,9 +35,8 @@ class AcediaAdapter extends AcediaObject
* specify desired `AcediaAdapter` before loading server/client core.
*/
-var public const class sideEffectAPIClass;
-var public const class timeAPIClass;
-var public const class dbAPIClass;
+var public const class timeAPIClass;
+var public const class dbAPIClass;
defaultproperties
{
diff --git a/sources/LevelAPI/CoreGlobal.uc b/sources/LevelAPI/CoreGlobal.uc
index 4b84aa9..fe43ef4 100644
--- a/sources/LevelAPI/CoreGlobal.uc
+++ b/sources/LevelAPI/CoreGlobal.uc
@@ -87,11 +87,9 @@ protected function Initialize()
.ArgClass(self.class);
return;
}
- api = class'Global'.static.GetInstance().memory;
- sideEffects =
- SideEffectAPI(api.Allocate(adapterClass.default.sideEffectAPIClass));
- time = TimeAPI(api.Allocate(adapterClass.default.timeAPIClass));
- db = DBAPI(api.Allocate(adapterClass.default.dbAPIClass));
+ api = class'Global'.static.GetInstance().memory;
+ time = TimeAPI(api.Allocate(adapterClass.default.timeAPIClass));
+ db = DBAPI(api.Allocate(adapterClass.default.dbAPIClass));
}
/**
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();