rott/kf_sources/AcediaCore/Classes/UnflectApi.uc
2026-07-14 20:27:09 +07:00

432 lines
18 KiB
Ucode

/**
* 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 <https://www.gnu.org/licenses/>.
*/
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<string> 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.")
}