432 lines
18 KiB
Ucode
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.")
|
|
} |