mirror of
https://github.com/BepInEx/BepInEx.git
synced 2026-10-20 01:46:13 +00:00
Restructure trampoline and detour code
This commit is contained in:
4 files changed
+153
-120
No files matched your search
@@ -83,7 +83,7 @@
|
||||
<Compile Include="IL2CPPChainloader.cs" />
|
||||
<Compile Include="Preloader.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Hook\TrampolineGenerator.cs" />
|
||||
<Compile Include="Hook\DetourGenerator.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BepInEx.Core\BepInEx.Core.csproj">
|
||||
@@ -103,6 +103,8 @@
|
||||
<None Include="app.config" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<ItemGroup>
|
||||
<Folder Include="UnityEngine\" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
+55
-81
@@ -3,27 +3,12 @@ using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using BepInEx.Logging;
|
||||
using Iced.Intel;
|
||||
using MonoMod.RuntimeDetour;
|
||||
|
||||
namespace BepInEx.IL2CPP
|
||||
{
|
||||
public static class TrampolineGenerator
|
||||
public static class DetourGenerator
|
||||
{
|
||||
public static IntPtr Generate(IntPtr originalFunctionPtr, IntPtr patchedFunctionPtr, out int trampolineLength)
|
||||
{
|
||||
var trampolineAlloc = DetourHelper.Native.MemAlloc(80);
|
||||
DetourHelper.Native.MakeWritable(originalFunctionPtr, 32);
|
||||
DetourHelper.Native.MakeWritable(trampolineAlloc, 80);
|
||||
|
||||
trampolineLength = Generate(originalFunctionPtr, patchedFunctionPtr, trampolineAlloc, IntPtr.Size == 8 ? 64 : 32);
|
||||
|
||||
DetourHelper.Native.MakeExecutable(trampolineAlloc, (uint)trampolineLength);
|
||||
DetourHelper.Native.MakeExecutable(originalFunctionPtr, 32);
|
||||
|
||||
return trampolineAlloc;
|
||||
}
|
||||
|
||||
private static void Disassemble(ManualLogSource logSource, IntPtr memoryPtr, int size)
|
||||
public static void Disassemble(ManualLogSource logSource, IntPtr memoryPtr, int size)
|
||||
{
|
||||
byte[] data = new byte[size];
|
||||
Marshal.Copy(memoryPtr, data, 0, size);
|
||||
@@ -52,56 +37,44 @@ namespace BepInEx.IL2CPP
|
||||
}
|
||||
}
|
||||
|
||||
public static IntPtr Generate(ManualLogSource logSource, IntPtr originalFunctionPtr, IntPtr patchedFunctionPtr, out int trampolineLength)
|
||||
public static int GetDetourLength(Architecture arch)
|
||||
=> arch == Architecture.X64 ? 14 : 5;
|
||||
|
||||
/// <summary>
|
||||
/// Writes a detour on <see cref="functionPtr"/> to redirect to <see cref="detourPtr"/>.
|
||||
/// </summary>
|
||||
/// <param name="functionPtr">The pointer to the function to apply the detour to.</param>
|
||||
/// <param name="detourPtr">The pointer to the function to redirect to.</param>
|
||||
/// <param name="architecture">The architecture of the current platform.</param>
|
||||
/// <param name="minimumLength">The minimum amount of length that the detour should consume. If the generated redirect is smaller than this, the remaining space is padded with NOPs.</param>
|
||||
public static void ApplyDetour(IntPtr functionPtr, IntPtr detourPtr, Architecture architecture, int minimumLength = 0)
|
||||
{
|
||||
logSource.LogDebug($"DoHook 0x{originalFunctionPtr.ToString("X")} -> 0x{patchedFunctionPtr.ToString("X")}");
|
||||
byte[] jmp = GenerateAbsoluteJump(detourPtr, functionPtr, architecture);
|
||||
|
||||
var trampolineAlloc = DetourHelper.Native.MemAlloc(80);
|
||||
DetourHelper.Native.MakeWritable(originalFunctionPtr, 32);
|
||||
DetourHelper.Native.MakeWritable(trampolineAlloc, 80);
|
||||
Marshal.Copy(jmp, 0, functionPtr, jmp.Length);
|
||||
|
||||
logSource.LogDebug($"Trampoline allocation: 0x{trampolineAlloc.ToString("X")}");
|
||||
|
||||
|
||||
logSource.LogDebug("Original (32) asm");
|
||||
|
||||
|
||||
Disassemble(logSource, originalFunctionPtr, 32);
|
||||
|
||||
|
||||
trampolineLength = Generate(originalFunctionPtr, patchedFunctionPtr, trampolineAlloc, IntPtr.Size == 8 ? 64 : 32);
|
||||
|
||||
|
||||
logSource.LogDebug("Modified (32) asm");
|
||||
|
||||
Disassemble(logSource, originalFunctionPtr, 32);
|
||||
|
||||
logSource.LogDebug($"Trampoline ({trampolineLength}) asm");
|
||||
|
||||
Disassemble(logSource, trampolineAlloc, trampolineLength);
|
||||
|
||||
DetourHelper.Native.MakeExecutable(trampolineAlloc, 80);
|
||||
DetourHelper.Native.MakeExecutable(originalFunctionPtr, 32);
|
||||
|
||||
return trampolineAlloc;
|
||||
// Fill remaining space with NOP instructions
|
||||
for (int i = jmp.Length; i < minimumLength; i++)
|
||||
Marshal.WriteByte(functionPtr + i, 0x90);
|
||||
}
|
||||
|
||||
public static int Generate(IntPtr originalFunctionPtr, IntPtr patchedFunctionPtr, IntPtr trampolineFunctionPtr, int bitness)
|
||||
/// <summary>
|
||||
/// Reads assembly from <see cref="functionPtr"/> (at least <see cref="minimumTrampolineLength"/> bytes), and writes it to <see cref="trampolinePtr"/> plus a jmp to continue execution.
|
||||
/// </summary>
|
||||
/// <param name="instructionBuffer">The buffer to copy assembly from.</param>
|
||||
/// <param name="functionPtr">The pointer to the function to copy assembly from.</param>
|
||||
/// <param name="trampolinePtr">The pointer to write the trampoline assembly to.</param>
|
||||
/// <param name="arch">The architecture of the current platform.</param>
|
||||
/// <param name="minimumTrampolineLength">Copies at least this many bytes of assembly from <see cref="functionPtr"/>.</param>
|
||||
/// <param name="trampolineLength">Returns the total length of the trampoline, in bytes.</param>
|
||||
/// <param name="jmpLength">Returns the length of the jmp at the end of the trampoline, in bytes.</param>
|
||||
public static void CreateTrampolineFromFunction(byte[] instructionBuffer, IntPtr functionPtr, IntPtr trampolinePtr, int minimumTrampolineLength, Architecture arch, out int trampolineLength, out int jmpLength)
|
||||
{
|
||||
byte[] instructionBuffer = new byte[80];
|
||||
|
||||
Marshal.Copy(originalFunctionPtr, instructionBuffer, 0, 80);
|
||||
|
||||
// Decode original function up until we go past the needed bytes to write the jump to patchedFunctionPtr
|
||||
|
||||
var generatedJmp = GenerateAbsoluteJump(patchedFunctionPtr, originalFunctionPtr, bitness == 64);
|
||||
|
||||
|
||||
uint requiredBytes = (uint)generatedJmp.Length;
|
||||
|
||||
var codeReader = new ByteArrayCodeReader(instructionBuffer);
|
||||
var decoder = Decoder.Create(bitness, codeReader);
|
||||
decoder.IP = (ulong)originalFunctionPtr.ToInt64();
|
||||
var decoder = Decoder.Create(arch == Architecture.X64 ? 64 : 32, codeReader);
|
||||
decoder.IP = (ulong)functionPtr.ToInt64();
|
||||
|
||||
uint totalBytes = 0;
|
||||
var origInstructions = new InstructionList();
|
||||
@@ -112,7 +85,7 @@ namespace BepInEx.IL2CPP
|
||||
totalBytes += (uint)instr.Length;
|
||||
if (instr.Code == Code.INVALID)
|
||||
throw new Exception("Found garbage");
|
||||
if (totalBytes >= requiredBytes)
|
||||
if (totalBytes >= minimumTrampolineLength)
|
||||
break;
|
||||
|
||||
switch (instr.FlowControl)
|
||||
@@ -125,21 +98,24 @@ namespace BepInEx.IL2CPP
|
||||
{
|
||||
var target = instr.NearBranchTarget;
|
||||
}
|
||||
goto default;
|
||||
|
||||
break;
|
||||
//goto default;
|
||||
case FlowControl.Interrupt:// eg. int n
|
||||
break;
|
||||
|
||||
case FlowControl.IndirectBranch:// eg. jmp reg/mem
|
||||
case FlowControl.ConditionalBranch:// eg. je, jno, etc
|
||||
case FlowControl.Return:// eg. ret
|
||||
case FlowControl.Call:// eg. call method
|
||||
case FlowControl.IndirectCall:// eg. call reg/mem
|
||||
case FlowControl.Interrupt:// eg. int n
|
||||
case FlowControl.XbeginXabortXend:
|
||||
case FlowControl.Exception:// eg. ud0
|
||||
default:
|
||||
throw new Exception("Not supported by this simple example - " + instr.FlowControl);
|
||||
}
|
||||
}
|
||||
if (totalBytes < requiredBytes)
|
||||
if (totalBytes < minimumTrampolineLength)
|
||||
throw new Exception("Not enough bytes!");
|
||||
|
||||
if (origInstructions.Count == 0)
|
||||
@@ -150,51 +126,49 @@ namespace BepInEx.IL2CPP
|
||||
|
||||
if (lastInstr.FlowControl != FlowControl.Return)
|
||||
{
|
||||
if (bitness == 64)
|
||||
Instruction detourInstruction;
|
||||
|
||||
if (arch == Architecture.X64)
|
||||
{
|
||||
origInstructions.Add(Instruction.CreateBranch(Code.Jmp_rel32_64, lastInstr.NextIP));
|
||||
detourInstruction = Instruction.CreateBranch(Code.Jmp_rel32_64, lastInstr.NextIP);
|
||||
}
|
||||
else
|
||||
{
|
||||
origInstructions.Add(Instruction.CreateBranch(Code.Jmp_rel32_32, lastInstr.NextIP));
|
||||
detourInstruction = Instruction.CreateBranch(Code.Jmp_rel32_32, lastInstr.NextIP);
|
||||
}
|
||||
|
||||
origInstructions.Add(detourInstruction);
|
||||
}
|
||||
|
||||
|
||||
// Generate trampoline from instruction list
|
||||
|
||||
var codeWriter = new CodeWriterImpl();
|
||||
ulong relocatedBaseAddress = (ulong)trampolineFunctionPtr;
|
||||
ulong relocatedBaseAddress = (ulong)trampolinePtr;
|
||||
var block = new InstructionBlock(codeWriter, origInstructions, relocatedBaseAddress);
|
||||
|
||||
bool success = BlockEncoder.TryEncode(decoder.Bitness, block, out var errorMessage, out var result);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new Exception(errorMessage);
|
||||
}
|
||||
|
||||
// Write generated trampoline
|
||||
|
||||
|
||||
var newCode = codeWriter.ToArray();
|
||||
Marshal.Copy(newCode, 0, trampolineFunctionPtr, newCode.Length);
|
||||
Marshal.Copy(newCode, 0, trampolinePtr, newCode.Length);
|
||||
|
||||
|
||||
// Overwrite the start of trampolineFunctionPtr with a jump to patchedFunctionPtr
|
||||
|
||||
Marshal.Copy(generatedJmp, 0, originalFunctionPtr, (int)requiredBytes);
|
||||
|
||||
// Fill overwritten instructions with NOP
|
||||
for (int i = (int)requiredBytes; i < totalBytes; i++)
|
||||
Marshal.WriteByte(originalFunctionPtr + i, 0x90);
|
||||
|
||||
return newCode.Length;
|
||||
jmpLength = newCode.Length - (int)totalBytes;
|
||||
trampolineLength = newCode.Length;
|
||||
}
|
||||
|
||||
private static byte[] GenerateAbsoluteJump(IntPtr address, IntPtr currentAddress, bool x64)
|
||||
public static byte[] GenerateAbsoluteJump(IntPtr targetAddress, IntPtr currentAddress, Architecture arch)
|
||||
{
|
||||
byte[] jmpBytes;
|
||||
|
||||
if (x64)
|
||||
if (arch == Architecture.X64)
|
||||
{
|
||||
jmpBytes = new byte[]
|
||||
{
|
||||
@@ -202,7 +176,7 @@ namespace BepInEx.IL2CPP
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // Absolute destination address
|
||||
};
|
||||
|
||||
Array.Copy(BitConverter.GetBytes(address.ToInt64()), 0, jmpBytes, 6, 8);
|
||||
Array.Copy(BitConverter.GetBytes(targetAddress.ToInt64()), 0, jmpBytes, 6, 8);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -212,7 +186,7 @@ namespace BepInEx.IL2CPP
|
||||
0x00, 0x00, 0x00, 0x00 // Relative destination address
|
||||
};
|
||||
|
||||
Array.Copy(BitConverter.GetBytes(address.ToInt32() - (currentAddress.ToInt32() + 5)), 0, jmpBytes, 1, 4);
|
||||
Array.Copy(BitConverter.GetBytes(targetAddress.ToInt32() - (currentAddress.ToInt32() + 5)), 0, jmpBytes, 1, 4);
|
||||
}
|
||||
|
||||
return jmpBytes;
|
||||
@@ -15,24 +15,25 @@ namespace BepInEx.IL2CPP.Hook
|
||||
public bool IsApplied { get; protected set; }
|
||||
|
||||
|
||||
public IntPtr OriginalFuncPtr { get; protected set; }
|
||||
public IntPtr DetourFuncPtr { get; protected set; }
|
||||
public IntPtr OriginalFunctionPtr { get; protected set; }
|
||||
public IntPtr DetourFunctionPtr { get; protected set; }
|
||||
|
||||
|
||||
public IntPtr TrampolinePtr { get; protected set; } = IntPtr.Zero;
|
||||
public int TrampolineSize { get; protected set; } = 0;
|
||||
protected int TrampolineJmpSize { get; set; } = 0;
|
||||
|
||||
protected MethodInfo TrampolineMethod { get; set; }
|
||||
|
||||
|
||||
public FastNativeDetour(IntPtr originalFuncPtr, IntPtr detourFuncPtr)
|
||||
public FastNativeDetour(IntPtr originalFunctionPtr, IntPtr detourFunctionPtr)
|
||||
{
|
||||
OriginalFuncPtr = originalFuncPtr;
|
||||
DetourFuncPtr = detourFuncPtr;
|
||||
OriginalFunctionPtr = originalFunctionPtr;
|
||||
DetourFunctionPtr = detourFunctionPtr;
|
||||
|
||||
// TODO: This may not be safe during undo if the method is smaller than 20 bytes
|
||||
BackupBytes = new byte[20];
|
||||
Marshal.Copy(originalFuncPtr, BackupBytes, 0, 20);
|
||||
Marshal.Copy(originalFunctionPtr, BackupBytes, 0, 20);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,21 +48,83 @@ namespace BepInEx.IL2CPP.Hook
|
||||
if (IsApplied)
|
||||
return;
|
||||
|
||||
int trampolineLength;
|
||||
|
||||
if (debuggerLogSource == null)
|
||||
TrampolinePtr = TrampolineGenerator.Generate(OriginalFuncPtr, DetourFuncPtr, out trampolineLength);
|
||||
else
|
||||
TrampolinePtr = TrampolineGenerator.Generate(debuggerLogSource, OriginalFuncPtr, DetourFuncPtr, out trampolineLength);
|
||||
DetourHelper.Native.MakeWritable(OriginalFunctionPtr, 32);
|
||||
|
||||
TrampolineSize = trampolineLength;
|
||||
|
||||
if (debuggerLogSource != null)
|
||||
{
|
||||
debuggerLogSource.LogDebug($"Detouring 0x{OriginalFunctionPtr.ToString("X")} -> 0x{OriginalFunctionPtr.ToString("X")}");
|
||||
|
||||
|
||||
debuggerLogSource.LogDebug("Original (32) asm");
|
||||
|
||||
DetourGenerator.Disassemble(debuggerLogSource, OriginalFunctionPtr, 32);
|
||||
}
|
||||
|
||||
var arch = IntPtr.Size == 8 ? Architecture.X64 : Architecture.X86;
|
||||
|
||||
GenerateTrampolineInner(out int trampolineLength, out int jmpLength);
|
||||
|
||||
debuggerLogSource?.LogDebug($"Cleared function length: {trampolineLength} - {jmpLength} = {trampolineLength - jmpLength}");
|
||||
|
||||
debuggerLogSource?.LogDebug($"Trampoline allocation: 0x{TrampolinePtr.ToString("X")}");
|
||||
|
||||
DetourGenerator.ApplyDetour(OriginalFunctionPtr, DetourFunctionPtr, arch, trampolineLength - jmpLength);
|
||||
|
||||
|
||||
if (debuggerLogSource != null)
|
||||
{
|
||||
debuggerLogSource.LogDebug("Modified (32) asm");
|
||||
|
||||
DetourGenerator.Disassemble(debuggerLogSource, OriginalFunctionPtr, 32);
|
||||
|
||||
debuggerLogSource.LogDebug($"Trampoline ({trampolineLength}) asm");
|
||||
|
||||
DetourGenerator.Disassemble(debuggerLogSource, TrampolinePtr, trampolineLength);
|
||||
}
|
||||
|
||||
DetourHelper.Native.MakeExecutable(OriginalFunctionPtr, 32);
|
||||
|
||||
IsApplied = true;
|
||||
}
|
||||
|
||||
|
||||
private void GenerateTrampolineInner(out int trampolineLength, out int jmpLength)
|
||||
{
|
||||
if (TrampolinePtr != IntPtr.Zero)
|
||||
{
|
||||
trampolineLength = TrampolineSize;
|
||||
jmpLength = TrampolineJmpSize;
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] instructionBuffer = new byte[32];
|
||||
Marshal.Copy(OriginalFunctionPtr, instructionBuffer, 0, 32);
|
||||
|
||||
var trampolineAlloc = DetourHelper.Native.MemAlloc(80);
|
||||
|
||||
DetourHelper.Native.MakeWritable(trampolineAlloc, 80);
|
||||
|
||||
var arch = IntPtr.Size == 8 ? Architecture.X64 : Architecture.X86;
|
||||
|
||||
DetourGenerator.CreateTrampolineFromFunction(instructionBuffer, OriginalFunctionPtr, trampolineAlloc,
|
||||
DetourGenerator.GetDetourLength(arch), arch, out trampolineLength, out jmpLength);
|
||||
|
||||
DetourHelper.Native.MakeExecutable(trampolineAlloc, 80);
|
||||
|
||||
TrampolinePtr = trampolineAlloc;
|
||||
TrampolineSize = trampolineLength;
|
||||
TrampolineJmpSize = jmpLength;
|
||||
}
|
||||
|
||||
|
||||
public void Undo()
|
||||
{
|
||||
Marshal.Copy(BackupBytes, 0, OriginalFuncPtr, BackupBytes.Length);
|
||||
if (!IsApplied)
|
||||
return;
|
||||
|
||||
Marshal.Copy(BackupBytes, 0, OriginalFunctionPtr, BackupBytes.Length);
|
||||
|
||||
DetourHelper.Native.MemFree(TrampolinePtr);
|
||||
|
||||
@@ -80,6 +143,12 @@ namespace BepInEx.IL2CPP.Hook
|
||||
{
|
||||
if (TrampolineMethod == null)
|
||||
{
|
||||
// Generate trampoline without applying the detour
|
||||
GenerateTrampolineInner(out _, out _);
|
||||
|
||||
if (TrampolinePtr == IntPtr.Zero)
|
||||
throw new InvalidOperationException("Trampoline pointer is not available");
|
||||
|
||||
TrampolineMethod = DetourHelper.GenerateNativeProxy(TrampolinePtr, signature);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using BepInEx.Bootstrap;
|
||||
using BepInEx.IL2CPP.Hook;
|
||||
using BepInEx.Logging;
|
||||
using BepInEx.Preloader.Core;
|
||||
using BepInEx.Preloader.Core.Logging;
|
||||
using MonoMod.RuntimeDetour;
|
||||
using UnhollowerBaseLib.Runtime;
|
||||
using UnhollowerRuntimeLib;
|
||||
using UnityEngine;
|
||||
@@ -41,21 +41,7 @@ namespace BepInEx.IL2CPP
|
||||
|
||||
private static RuntimeInvokeDetour originalInvoke;
|
||||
|
||||
private static ManualLogSource unhollowerLogSource = Logger.CreateLogSource("Unhollower");
|
||||
|
||||
private class DetourHandler : UnhollowerRuntimeLib.ManagedDetour
|
||||
{
|
||||
public T Detour<T>(IntPtr from, T to) where T : Delegate
|
||||
{
|
||||
var detour = new NativeDetour(from, to, new NativeDetourConfig { ManualApply = true });
|
||||
|
||||
var trampoline = detour.GenerateTrampoline<T>();
|
||||
|
||||
detour.Apply();
|
||||
|
||||
return trampoline;
|
||||
}
|
||||
}
|
||||
private static readonly ManualLogSource unhollowerLogSource = Logger.CreateLogSource("Unhollower");
|
||||
|
||||
|
||||
public override unsafe void Initialize(string gameExePath = null)
|
||||
@@ -72,9 +58,11 @@ namespace BepInEx.IL2CPP
|
||||
{
|
||||
IntPtr originalFunc = new IntPtr(*(void**)ptr);
|
||||
|
||||
var trampolinePtr = TrampolineGenerator.Generate(unhollowerLogSource, originalFunc, patchedFunctionPtr, out _);
|
||||
var detour = new FastNativeDetour(originalFunc, patchedFunctionPtr);
|
||||
|
||||
detour.Apply();
|
||||
|
||||
*(void**)ptr = (void*)trampolinePtr;
|
||||
*(void**)ptr = (void*)detour.TrampolinePtr;
|
||||
};
|
||||
|
||||
var gameAssemblyModule = Process.GetCurrentProcess().Modules.Cast<ProcessModule>().First(x => x.ModuleName.Contains("GameAssembly"));
|
||||
@@ -84,14 +72,14 @@ namespace BepInEx.IL2CPP
|
||||
|
||||
PreloaderLogger.Log.LogDebug($"Runtime invoke pointer: 0x{functionPtr.ToInt64():X}");
|
||||
|
||||
var invokeDetour = new NativeDetour(functionPtr, Marshal.GetFunctionPointerForDelegate(new RuntimeInvokeDetour(OnInvokeMethod)));
|
||||
var invokeDetour = new FastNativeDetour(functionPtr, Marshal.GetFunctionPointerForDelegate(new RuntimeInvokeDetour(OnInvokeMethod)));
|
||||
|
||||
invokeDetour.Apply(unhollowerLogSource);
|
||||
|
||||
originalInvoke = invokeDetour.GenerateTrampoline<RuntimeInvokeDetour>();
|
||||
|
||||
//invokeDetour.Apply(unhollowerLogSource);
|
||||
invokeDetour.Apply();
|
||||
}
|
||||
|
||||
|
||||
private static bool HasSet = false;
|
||||
|
||||
private static HashSet<string> recordedNames = new HashSet<string>();
|
||||
@@ -108,11 +96,11 @@ namespace BepInEx.IL2CPP
|
||||
{
|
||||
try
|
||||
{
|
||||
UnityEngine.Application.s_LogCallbackHandler = new Action<string, string, LogType>(UnityLogCallback);
|
||||
Application.s_LogCallbackHandler = new Action<string, string, LogType>(UnityLogCallback);
|
||||
|
||||
UnityLogSource.LogMessage($"callback set - {methodName}");
|
||||
|
||||
UnityEngine.Application.CallLogCallback("test from OnInvokeMethod", "", LogType.Log, true);
|
||||
Application.CallLogCallback("test from OnInvokeMethod", "", LogType.Log, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user