/** * 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() { _drop(); _.memory.Free(completedReplacements); _.memory.Free(originalScriptCodes); completedReplacements = none; originalScriptCodes = none; functionCaster = none; } public final function _drop() { local UFunction nextFunctionInstance; local Text nextFunctionName; local HashTableIterator iter; local ByteArrayBox nextSources; // Drop is called when Acedia is shutting down, so releasing references isn't necessary iter = HashTableIterator(completedReplacements.Iterate()); while (!iter.HasFinished()) { nextFunctionInstance = none; nextFunctionName = Text(iter.GetKey()); nextSources = ByteArrayBox(originalScriptCodes.GetItem(nextFunctionName)); if (nextSources != none ) { nextFunctionInstance = FindFunction(nextFunctionName); } if (nextFunctionInstance != none) { nextFunctionInstance.script = nextSources.Get(); } iter.Next(); } } /// 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.") }