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