yikes,
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
using System.Collections.Generic;
|
||||
using VanadiumGuard.Protocol;
|
||||
|
||||
namespace VanadiumGuard.Server.Policy
|
||||
{
|
||||
/// <summary>
|
||||
/// What each detection is worth, and whether it may ever act on its own.
|
||||
/// <para>
|
||||
/// This table is the whole reason the redesign exists. In the old build every signal was
|
||||
/// worth the same thing - immediate death - so a Cheat Engine window and someone having
|
||||
/// a file named injector.exe in Downloads had identical consequences. Here a signal has
|
||||
/// a weight, and only a handful of signals are trusted enough to end a session by
|
||||
/// themselves.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class DetectionCatalog
|
||||
{
|
||||
public sealed class Entry
|
||||
{
|
||||
/// <summary>Score contribution at 100 confidence. Scores accumulate toward 100.</summary>
|
||||
public double Weight { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this code alone, at high confidence, justifies removing the player.
|
||||
/// Reserved for direct evidence of code modification inside the game process.
|
||||
/// </summary>
|
||||
public bool Decisive { get; init; }
|
||||
|
||||
/// <summary>Human-readable, used in review tooling and audit logs.</summary>
|
||||
public string Description { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<DetectionCode, Entry> Entries = new()
|
||||
{
|
||||
// Modules. Loading a known cheat framework into the process is strong, but the
|
||||
// name match is still a name match, so it is not decisive on its own.
|
||||
[DetectionCode.BlacklistedModuleLoaded] = new Entry
|
||||
{
|
||||
Weight = 55, Decisive = false, Description = "Known cheat or hooking library mapped into the game",
|
||||
},
|
||||
[DetectionCode.ModuleUnbackedByDisk] = new Entry
|
||||
{
|
||||
Weight = 45, Decisive = false, Description = "Module present without a loader notification (manual map)",
|
||||
},
|
||||
[DetectionCode.ModuleFromVolatilePath] = new Entry
|
||||
{
|
||||
Weight = 8, Decisive = false, Description = "Module loaded from a temp or downloads directory",
|
||||
},
|
||||
[DetectionCode.ModuleFromUntrustedPath] = new Entry
|
||||
{
|
||||
Weight = 5, Decisive = false, Description = "Module loaded from outside the trusted roots",
|
||||
},
|
||||
|
||||
// External environment. Weak by construction: what is open on someone's second
|
||||
// monitor is not evidence of what is happening inside the game.
|
||||
[DetectionCode.BlacklistedProcess] = new Entry
|
||||
{
|
||||
Weight = 12, Decisive = false, Description = "A listed tool is running on the machine",
|
||||
},
|
||||
[DetectionCode.BlacklistedWindow] = new Entry
|
||||
{
|
||||
Weight = 10, Decisive = false, Description = "A listed tool has a visible window",
|
||||
},
|
||||
[DetectionCode.DebuggerAttached] = new Entry
|
||||
{
|
||||
Weight = 60, Decisive = false, Description = "A debugger is attached to the game process",
|
||||
},
|
||||
|
||||
// Code integrity. This is the evidence worth acting on: something modified
|
||||
// executable memory inside the process, which nothing legitimate does here.
|
||||
[DetectionCode.NtExportPrologueModified] = new Entry
|
||||
{
|
||||
Weight = 70, Decisive = false, Description = "Syscall stub prologue rewritten as a detour",
|
||||
},
|
||||
[DetectionCode.GameCodeModified] = new Entry
|
||||
{
|
||||
Weight = 85, Decisive = true, Description = "Game code section modified in memory",
|
||||
},
|
||||
[DetectionCode.Il2CppMethodHooked] = new Entry
|
||||
{
|
||||
Weight = 90, Decisive = true, Description = "Inline hook on a watched gameplay method",
|
||||
},
|
||||
|
||||
// Managed integrity.
|
||||
[DetectionCode.ForeignHarmonyPatch] = new Entry
|
||||
{
|
||||
Weight = 6, Decisive = false, Description = "Harmony patch from an unrecognised mod",
|
||||
},
|
||||
[DetectionCode.EnforcementMethodPatched] = new Entry
|
||||
{
|
||||
Weight = 95, Decisive = true, Description = "The guard or process exit path was patched",
|
||||
},
|
||||
|
||||
// Guard integrity, raised server-side. Tampering with the guard is treated as
|
||||
// seriously as tampering with the game, because it is done for the same reason.
|
||||
[DetectionCode.ChallengeFailed] = new Entry
|
||||
{
|
||||
Weight = 90, Decisive = true, Description = "Memory challenge answered with the wrong hash",
|
||||
},
|
||||
[DetectionCode.ChallengeUnreadable] = new Entry
|
||||
{
|
||||
Weight = 20, Decisive = false, Description = "Client could not read a challenged region",
|
||||
},
|
||||
[DetectionCode.GuardSilent] = new Entry
|
||||
{
|
||||
Weight = 50, Decisive = false, Description = "Guard stopped heartbeating while the match was live",
|
||||
},
|
||||
[DetectionCode.SignatureInvalid] = new Entry
|
||||
{
|
||||
Weight = 70, Decisive = false, Description = "Request signature did not verify",
|
||||
},
|
||||
[DetectionCode.SequenceReplay] = new Entry
|
||||
{
|
||||
Weight = 70, Decisive = false, Description = "Replayed or reordered request sequence",
|
||||
},
|
||||
[DetectionCode.PolicyRefused] = new Entry
|
||||
{
|
||||
Weight = 15, Decisive = false, Description = "Client refused to apply the issued policy",
|
||||
},
|
||||
|
||||
// Hardware bans. These are recorded for the audit trail, not to move a score:
|
||||
// a machine ban is a decision that was already made by a person, and running it
|
||||
// back through the scoring path would let a threshold second-guess them.
|
||||
[DetectionCode.MachineBanned] = new Entry
|
||||
{
|
||||
Weight = 0, Decisive = false, Description = "Session opened from a banned machine",
|
||||
},
|
||||
[DetectionCode.MachineLinkedToBan] = new Entry
|
||||
{
|
||||
Weight = 0, Decisive = false, Description = "Machine shares hardware with a banned machine",
|
||||
},
|
||||
[DetectionCode.MachineUnidentifiable] = new Entry
|
||||
{
|
||||
Weight = 0, Decisive = false, Description = "Client could not produce a machine identity",
|
||||
},
|
||||
|
||||
// Device attestation. A session that proved it held a device key and then stopped
|
||||
// signing with it is claiming to be somewhere it is not, which is the one thing
|
||||
// the HMAC alone can never catch.
|
||||
[DetectionCode.AttestationInvalid] = new Entry
|
||||
{
|
||||
Weight = 70, Decisive = false, Description = "Request not signed by the bound device key",
|
||||
},
|
||||
[DetectionCode.AttestationMissing] = new Entry
|
||||
{
|
||||
Weight = 0, Decisive = false, Description = "Session opened without a hardware-backed key",
|
||||
},
|
||||
};
|
||||
|
||||
public static Entry For(DetectionCode code)
|
||||
{
|
||||
return Entries.TryGetValue(code, out Entry? entry)
|
||||
? entry
|
||||
: new Entry { Weight = 0, Decisive = false, Description = "Unknown detection code" };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using VanadiumGuard.Protocol;
|
||||
using VanadiumGuard.Server.Sessions;
|
||||
|
||||
namespace VanadiumGuard.Server.Policy
|
||||
{
|
||||
/// <summary>
|
||||
/// Turns detections into a verdict. This is the only place in the system where a
|
||||
/// decision about a player is made, and it runs on the server where the player cannot
|
||||
/// reach it.
|
||||
/// </summary>
|
||||
public sealed class PolicyEngine
|
||||
{
|
||||
/// <summary>Score at which the session is ended.</summary>
|
||||
public const int DisconnectThreshold = 80;
|
||||
|
||||
/// <summary>Score at which the player is told something is wrong but keeps playing.</summary>
|
||||
public const int WarnThreshold = 40;
|
||||
|
||||
private readonly ILogger<PolicyEngine> _logger;
|
||||
|
||||
public PolicyEngine(ILogger<PolicyEngine> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a batch against the session and returns what the client should do.
|
||||
/// </summary>
|
||||
public Verdict Evaluate(GuardSession session, IReadOnlyList<Detection> detections)
|
||||
{
|
||||
bool decisive = false;
|
||||
|
||||
foreach (Detection detection in detections)
|
||||
{
|
||||
DetectionCatalog.Entry entry = DetectionCatalog.For(detection.Code);
|
||||
|
||||
if (entry.Weight <= 0)
|
||||
{
|
||||
_logger.LogDebug("Ignoring unweighted code {Code} from {Session}", detection.Code, session.SessionId);
|
||||
continue;
|
||||
}
|
||||
|
||||
int confidence = Math.Clamp(detection.Confidence, 0, 100);
|
||||
|
||||
// Confidence scales the weight. A detector rolled out at MaxConfidence 0
|
||||
// contributes exactly nothing, which is what makes a safe rollout possible.
|
||||
double score = entry.Weight * (confidence / 100.0);
|
||||
session.Record(detection, score);
|
||||
|
||||
// Decisive codes still have to arrive at high confidence. A weak reading of
|
||||
// a serious signal is evidence, not a conviction.
|
||||
if (entry.Decisive && confidence >= 85)
|
||||
{
|
||||
decisive = true;
|
||||
_logger.LogWarning(
|
||||
"Decisive detection {Code} subject={Subject} conf={Confidence} player={Player} session={Session}",
|
||||
detection.Code, detection.Subject, confidence, session.PlayerId, session.SessionId);
|
||||
}
|
||||
}
|
||||
|
||||
return VerdictFor(session, decisive);
|
||||
}
|
||||
|
||||
/// <summary>Re-derives a verdict without adding anything, for heartbeats and status.</summary>
|
||||
public Verdict Current(GuardSession session)
|
||||
{
|
||||
return VerdictFor(session, decisive: false);
|
||||
}
|
||||
|
||||
private Verdict VerdictFor(GuardSession session, bool decisive)
|
||||
{
|
||||
// A forced verdict outranks the score. A hardware ban is not a risk judgement
|
||||
// and must not be re-derived from one.
|
||||
Verdict? forced = session.ForcedVerdict;
|
||||
if (forced != null)
|
||||
return forced;
|
||||
|
||||
if (session.Terminated)
|
||||
{
|
||||
return new Verdict
|
||||
{
|
||||
Action = VerdictAction.Disconnect,
|
||||
ReasonCode = "VG-TERM",
|
||||
PlayerMessage = "This session was ended by an anti-cheat check.",
|
||||
};
|
||||
}
|
||||
|
||||
int score = session.RiskScore();
|
||||
|
||||
if (decisive || score >= DisconnectThreshold)
|
||||
{
|
||||
session.MarkTerminated();
|
||||
|
||||
_logger.LogWarning(
|
||||
"Disconnecting player={Player} session={Session} score={Score} decisive={Decisive}",
|
||||
session.PlayerId, session.SessionId, score, decisive);
|
||||
|
||||
return new Verdict
|
||||
{
|
||||
Action = VerdictAction.Disconnect,
|
||||
ReasonCode = "VG-" + score.ToString("D3"),
|
||||
PlayerMessage =
|
||||
"You have been removed from this session by an anti-cheat check. " +
|
||||
"If you believe this is wrong, contact support and quote this code.",
|
||||
|
||||
// A short grace period so the client can flush the batch that triggered
|
||||
// this. Without it the most important evidence is the evidence lost.
|
||||
DelayMs = 2000,
|
||||
};
|
||||
}
|
||||
|
||||
if (score >= WarnThreshold)
|
||||
{
|
||||
return new Verdict
|
||||
{
|
||||
Action = VerdictAction.Warn,
|
||||
ReasonCode = "VG-W" + score.ToString("D3"),
|
||||
PlayerMessage =
|
||||
"Close any debugging or memory tools before continuing. " +
|
||||
"Leaving them open may end your session.",
|
||||
};
|
||||
}
|
||||
|
||||
return new Verdict { Action = VerdictAction.Continue };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises a detection the server itself observed, e.g. a bad signature or a guard
|
||||
/// that went quiet. These never come from the client, which is exactly why they are
|
||||
/// worth something.
|
||||
/// </summary>
|
||||
public void RecordServerSide(GuardSession session, DetectionCode code, string subject, int confidence,
|
||||
IDictionary<string, string>? evidence = null)
|
||||
{
|
||||
var detection = new Detection
|
||||
{
|
||||
Code = code,
|
||||
Severity = Severity.High,
|
||||
Confidence = confidence,
|
||||
Subject = subject,
|
||||
Detector = "server",
|
||||
FirstObservedUtc = DateTimeOffset.UtcNow,
|
||||
LastObservedUtc = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
if (evidence != null)
|
||||
{
|
||||
foreach (KeyValuePair<string, string> pair in evidence)
|
||||
detection.Evidence[pair.Key] = pair.Value;
|
||||
}
|
||||
|
||||
DetectionCatalog.Entry entry = DetectionCatalog.For(code);
|
||||
session.Record(detection, entry.Weight * (confidence / 100.0));
|
||||
|
||||
_logger.LogWarning(
|
||||
"Server-side detection {Code} subject={Subject} player={Player}",
|
||||
code, subject, session.PlayerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using VanadiumGuard.Protocol;
|
||||
|
||||
namespace VanadiumGuard.Server.Policy
|
||||
{
|
||||
/// <summary>
|
||||
/// Serves the policy handed to clients at session open, reloading it from disk when the
|
||||
/// file changes so rules can be adjusted without a deploy.
|
||||
/// </summary>
|
||||
public sealed class PolicyProvider
|
||||
{
|
||||
private readonly ILogger<PolicyProvider> _logger;
|
||||
private readonly string _path;
|
||||
private GuardPolicy _policy;
|
||||
private DateTime _loadedStamp;
|
||||
|
||||
public PolicyProvider(ILogger<PolicyProvider> logger, string policyDirectory)
|
||||
{
|
||||
_logger = logger;
|
||||
_path = Path.Combine(policyDirectory, "policy.json");
|
||||
_policy = Load();
|
||||
}
|
||||
|
||||
public GuardPolicy Current
|
||||
{
|
||||
get
|
||||
{
|
||||
ReloadIfChanged();
|
||||
return _policy;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReloadIfChanged()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_path))
|
||||
return;
|
||||
|
||||
DateTime stamp = File.GetLastWriteTimeUtc(_path);
|
||||
if (stamp <= _loadedStamp)
|
||||
return;
|
||||
|
||||
_policy = Load();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Keep serving the last good policy. A malformed edit must not take the
|
||||
// whole fleet offline.
|
||||
_logger.LogError(ex, "Policy reload failed; continuing with the previous policy");
|
||||
}
|
||||
}
|
||||
|
||||
private GuardPolicy Load()
|
||||
{
|
||||
if (!File.Exists(_path))
|
||||
{
|
||||
_logger.LogWarning("No policy at {Path}; writing the built-in default", _path);
|
||||
|
||||
GuardPolicy fallback = Default();
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_path)!);
|
||||
File.WriteAllText(_path, JsonSerializer.Serialize(
|
||||
fallback, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not write the default policy to {Path}", _path);
|
||||
}
|
||||
|
||||
_loadedStamp = DateTime.UtcNow;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
GuardPolicy? loaded = JsonSerializer.Deserialize<GuardPolicy>(
|
||||
File.ReadAllText(_path),
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
if (loaded == null)
|
||||
throw new InvalidOperationException("policy.json deserialised to null");
|
||||
|
||||
_loadedStamp = File.GetLastWriteTimeUtc(_path);
|
||||
_logger.LogInformation("Loaded policy version {Version}", loaded.PolicyVersion);
|
||||
return loaded;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Policy at {Path} is invalid; using the built-in default", _path);
|
||||
_loadedStamp = DateTime.UtcNow;
|
||||
return Default();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The shipped starting point.
|
||||
/// <para>
|
||||
/// Note what is missing relative to the old hard-coded lists. There is no filesystem
|
||||
/// scanner, so nothing looks at what a player keeps in Documents or Downloads; that
|
||||
/// was a large privacy cost for a signal that only ever proved someone had once
|
||||
/// downloaded a tool. Generic process names are gone. Reverse-engineering tools that
|
||||
/// developers use every day are present but weighted low, because owning IDA is not
|
||||
/// evidence of cheating in a match.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static GuardPolicy Default()
|
||||
{
|
||||
return new GuardPolicy
|
||||
{
|
||||
PolicyVersion = "2024.09.05-1",
|
||||
ReportIntervalMs = 5000,
|
||||
HeartbeatIntervalMs = 15000,
|
||||
|
||||
Detectors = new[]
|
||||
{
|
||||
new DetectorSettings { Name = "module", Enabled = true, MaxConfidence = 100 },
|
||||
new DetectorSettings { Name = "process", Enabled = true, MaxConfidence = 60 },
|
||||
new DetectorSettings { Name = "window", Enabled = true, MaxConfidence = 60 },
|
||||
new DetectorSettings { Name = "debugger", Enabled = true, MaxConfidence = 100 },
|
||||
new DetectorSettings { Name = "nativehook", Enabled = true, MaxConfidence = 100 },
|
||||
|
||||
// Starts in observe-only mode. Every player with a second mod installed
|
||||
// trips this, so it reports for a while and earns its confidence later.
|
||||
new DetectorSettings { Name = "managedpatch", Enabled = true, MaxConfidence = 0 },
|
||||
|
||||
new DetectorSettings { Name = "il2cpp", Enabled = true, MaxConfidence = 100 },
|
||||
},
|
||||
|
||||
ModuleAllowPrefixes = new[]
|
||||
{
|
||||
"vanadiumguard", "melonloader", "0harmony", "il2cpp", "unityplayer", "unityengine",
|
||||
"gameassembly", "baselib", "unitycrashhandler", "recroom", "easyanticheat",
|
||||
"system.", "microsoft.", "mono", "netstandard", "mscor", "clr", "coreclr",
|
||||
"hostfxr", "hostpolicy", "ucrtbase", "vcruntime", "msvcp", "msvcr",
|
||||
"ntdll", "kernel32", "kernelbase", "user32", "gdi32", "win32u", "advapi32",
|
||||
"sechost", "rpcrt4", "combase", "ole32", "oleaut32", "shell32", "shlwapi",
|
||||
"psapi", "winhttp", "wininet", "crypt32", "bcrypt", "ncrypt", "schannel",
|
||||
"iphlpapi", "ws2_32", "mswsock", "dnsapi", "setupapi", "cfgmgr32", "devobj",
|
||||
"powrprof", "secur32", "sspicli", "userenv", "profapi", "winmm", "dsound",
|
||||
"xaudio", "x3daudio", "xinput", "dinput", "openal", "physx", "steam",
|
||||
"nvapi", "nvcuda", "nvoglv", "cudart", "amdxc", "atiadl", "igd", "dxgi",
|
||||
"d3d", "d3dcompiler", "dcomp", "dwmapi", "uxtheme", "propsys", "version",
|
||||
"dbghelp", "dbgcore", "symsrv", "textinputframework", "coreuicomponents",
|
||||
"coremessaging", "windows.storage", "wintypes", "apphelp", "avrt", "wtsapi32",
|
||||
},
|
||||
|
||||
TrustedPathPrefixes = new[]
|
||||
{
|
||||
"\\windows\\system32\\",
|
||||
"\\windows\\winsxs\\",
|
||||
"\\program files\\",
|
||||
"\\program files (x86)\\",
|
||||
"\\steam\\steamapps\\",
|
||||
"\\dotnet\\",
|
||||
},
|
||||
|
||||
ModuleDenyList = new[]
|
||||
{
|
||||
"minhook", "easyhook", "blackbone", "detours", "polyhook",
|
||||
"scyllahide", "scyllahook",
|
||||
"frida-agent", "frida-gadget",
|
||||
"cheatengine", "ceserver", "vehdebug", "speedhack",
|
||||
"unityexplorer", "monoinjector", "melonsampler",
|
||||
"vmthook", "internalhack", "externalhack",
|
||||
},
|
||||
|
||||
// Exact image names only. Anything that is also a normal program name has
|
||||
// been left off entirely rather than matched loosely.
|
||||
ProcessDenyList = new[]
|
||||
{
|
||||
"cheatengine-x86_64.exe", "cheatengine-i386.exe",
|
||||
"cheatengine-x86_64-SSE4-AVX2.exe", "ceserver.exe",
|
||||
"x64dbg.exe", "x32dbg.exe", "ollydbg.exe", "immunitydebugger.exe",
|
||||
"windbg.exe", "windbgx.exe",
|
||||
"ida.exe", "ida64.exe", "idaq.exe", "idaq64.exe",
|
||||
"ghidraRun.exe",
|
||||
"dnSpy.exe", "dnSpy-x86.exe", "dnSpyEx.exe", "ILSpy.exe", "dotPeek64.exe",
|
||||
"scylla.exe", "scylla_x64.exe", "ScyllaHide.exe",
|
||||
"frida.exe", "frida-server.exe", "frida-trace.exe",
|
||||
"artmoney.exe", "squalr.exe",
|
||||
"HTTPDebuggerUI.exe", "HTTPDebuggerSvc.exe",
|
||||
"extremeinjector.exe", "processhacker.exe", "SystemInformer.exe",
|
||||
},
|
||||
|
||||
// A class alone never counts; the detector requires a title match to treat
|
||||
// one of these as more than informational.
|
||||
WindowClassDenyList = new[]
|
||||
{
|
||||
"TFormCheatEngine", "TMemoryBrowser", "Qt5152QWindowIcon", "OLLYDBG",
|
||||
"ID Window", "WinDbgFrameClass",
|
||||
},
|
||||
|
||||
WindowTitleDenyList = new[]
|
||||
{
|
||||
"Cheat Engine", "Memory Viewer", "Add Address Manually",
|
||||
"x64dbg", "x32dbg", "OllyDbg", "Immunity Debugger",
|
||||
"IDA -", "Ghidra:", "dnSpy", "ILSpy",
|
||||
"HTTP Debugger", "Extreme Injector",
|
||||
},
|
||||
|
||||
WatchedExports = new[]
|
||||
{
|
||||
new WatchedExport { Module = "ntdll.dll", Export = "NtProtectVirtualMemory" },
|
||||
new WatchedExport { Module = "ntdll.dll", Export = "NtWriteVirtualMemory" },
|
||||
new WatchedExport { Module = "ntdll.dll", Export = "NtReadVirtualMemory" },
|
||||
new WatchedExport { Module = "ntdll.dll", Export = "NtCreateThreadEx" },
|
||||
new WatchedExport { Module = "ntdll.dll", Export = "NtOpenProcess" },
|
||||
new WatchedExport { Module = "ntdll.dll", Export = "LdrLoadDll" },
|
||||
},
|
||||
|
||||
// Replace these with the real signatures for your build. They are examples,
|
||||
// and a target that does not resolve is skipped rather than treated as a fault.
|
||||
Il2CppTargets = new[]
|
||||
{
|
||||
new Il2CppTarget { TypeName = "UnityEngine.Camera", MethodName = "set_fieldOfView", ParameterCount = 1 },
|
||||
},
|
||||
|
||||
AllowedPatchOwners = new[]
|
||||
{
|
||||
"vanadiumguard", "vanadium.patch", "melonloader",
|
||||
},
|
||||
|
||||
ChallengeableModules = new[]
|
||||
{
|
||||
"GameAssembly.dll", "UnityPlayer.dll", "VanadiumGuard.dll",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user