mirror of
https://github.com/djdevin/recnet-plugin.git
synced 2026-09-08 14:41:30 -07:00
Suppress DUID mismatch check resulting in create account hang (#3)
* test duuid mismatch failures * add variables to trigger or suppress DUID mismatch * update docs * turn on by default
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
// Test tool: persist a genuinely corrupt STORED device id on this machine, matching the friend's
|
||||
// condition (stored id truncated, current id healthy).
|
||||
//
|
||||
// We can't hand-craft the stored value: it lives in PlayerPrefs under an obfuscated key, encoded as a
|
||||
// CodeStage ObscuredString, and both the key and the encode method are renamed per game build. So we
|
||||
// let the game write it: WriteDUIDs() stores ObscuredString(SystemInfo.deviceUniqueIdentifier) under
|
||||
// the right key. We temporarily spoof deviceUniqueIdentifier to a truncated value around that one
|
||||
// call, so the game encrypts+stores a bad id with its own (unknown-to-us) key. Afterwards the spoof
|
||||
// is off, so the current id reads healthy again -> stored != current -> real mismatch on next launch.
|
||||
[HarmonyPatch]
|
||||
public static class CorruptDUIDPatch
|
||||
{
|
||||
// Only true for the duration of the WriteDUIDs() call below, so the SystemInfo getter is spoofed
|
||||
// exactly there and nowhere else.
|
||||
private static bool _spoofActive;
|
||||
private static string _spoofValue = "";
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(SystemInfo), "get_deviceUniqueIdentifier")]
|
||||
private static bool DeviceIdGetterPrefix(ref string __result)
|
||||
{
|
||||
if (!_spoofActive)
|
||||
return true;
|
||||
__result = _spoofValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns true if the corruption was written (so the caller marks it done and won't repeat).
|
||||
public static bool CorruptStored(GameObject cheatMgrGo)
|
||||
{
|
||||
var cm = cheatMgrGo.GetComponent<CheatManager>() ?? cheatMgrGo.GetComponentInChildren<CheatManager>();
|
||||
if (cm == null)
|
||||
{
|
||||
Plugin.Log.LogError("[CORRUPT] could not find CheatManager component; nothing written");
|
||||
return false;
|
||||
}
|
||||
|
||||
var real = SystemInfo.deviceUniqueIdentifier; // spoof off -> real id
|
||||
var bad = real is { Length: >= 7 } ? real.Substring(0, 7) : "badduid";
|
||||
|
||||
_spoofValue = bad;
|
||||
_spoofActive = true;
|
||||
try
|
||||
{
|
||||
cm.WriteDUIDs(); // encodes+stores ObscuredString(bad) under the real key
|
||||
}
|
||||
finally
|
||||
{
|
||||
_spoofActive = false;
|
||||
}
|
||||
|
||||
Plugin.Log.LogWarning($"[CORRUPT] wrote truncated stored DUID = \"{bad}\" (real id = \"{real}\"). " +
|
||||
"Set 'Corrupt Stored DUID' back to false and relaunch to drive the real mismatch path.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Undo: overwrite the stored value with the real id by calling WriteDUIDs with the spoof off.
|
||||
public static bool RestoreStored(GameObject cheatMgrGo)
|
||||
{
|
||||
var cm = cheatMgrGo.GetComponent<CheatManager>() ?? cheatMgrGo.GetComponentInChildren<CheatManager>();
|
||||
if (cm == null)
|
||||
{
|
||||
Plugin.Log.LogError("[CORRUPT] could not find CheatManager component; nothing restored");
|
||||
return false;
|
||||
}
|
||||
|
||||
cm.WriteDUIDs(); // spoof off -> stores ObscuredString(real deviceUniqueIdentifier)
|
||||
Plugin.Log.LogWarning($"[CORRUPT] restored stored DUID to real id = \"{SystemInfo.deviceUniqueIdentifier}\". " +
|
||||
"Set 'Restore Stored DUID' back to false.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using HarmonyLib;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
// Controls CheatManager.CheckForDUIDMismatch, which returns true when the machine's stored device id
|
||||
// differs from the freshly-derived one. A true result sends the client down the migration path that
|
||||
// POSTs PlayerReporting/v1/deviceId and then stalls on Create Account.
|
||||
//
|
||||
// Three modes, chosen by config:
|
||||
// Simulate = true -> force TRUE (fake a mismatch to reproduce the hang without a corrupt value)
|
||||
// Suppress = true -> force FALSE (the workaround fix: never migrate, never hang)
|
||||
// both false -> pass through, let the REAL check run against the actual stored value
|
||||
// (needed to observe a genuinely corrupt stored id, e.g. after Corrupt Stored DUID)
|
||||
//
|
||||
// Patch the concrete CheatManager method, NOT the abstract PGECJHKNIEN interface, or the prefix
|
||||
// never runs.
|
||||
[HarmonyPatch]
|
||||
public static class DUIDMismatchPatch
|
||||
{
|
||||
private const string SimulatedStoredDeviceId = "491e8b9";
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(CheatManager), "CheckForDUIDMismatch")]
|
||||
private static bool Prefix(ref string ALOMDLLNIMD, ref bool __result)
|
||||
{
|
||||
if (Plugin.SimulateDUIDMismatch.Value)
|
||||
{
|
||||
ALOMDLLNIMD = SimulatedStoredDeviceId;
|
||||
__result = true;
|
||||
Plugin.Log.LogWarning($"[DUID] simulating mismatch, stored id = {SimulatedStoredDeviceId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Plugin.SuppressDUIDMismatch.Value)
|
||||
{
|
||||
ALOMDLLNIMD = string.Empty;
|
||||
__result = false;
|
||||
Plugin.Log.LogInfo("[DUID] mismatch check forced to false (suppressed)");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pass through to the real check.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Collections.Generic;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
// Diagnostic only. Two jobs:
|
||||
// 1. Show where the stored device id lives, by logging PlayerPrefs reads/writes.
|
||||
// 2. Show how far the DUID migration branch gets, by logging CheatManager's other DUID methods.
|
||||
// If WriteDUIDs() never fires after the deviceId POST, the flow stalls before it.
|
||||
[HarmonyPatch]
|
||||
public static class DUIDProbePatch
|
||||
{
|
||||
// PlayerPrefs.GetString is called constantly, so log each key only once — except device/DUID
|
||||
// keys, which we always log so we can watch them change across the migration.
|
||||
private static readonly HashSet<string> SeenKeys = new();
|
||||
|
||||
private static bool IsInteresting(string key) =>
|
||||
key != null && (key.Contains("DUID") || key.Contains("Duid") || key.Contains("duid")
|
||||
|| key.Contains("Device") || key.Contains("device")
|
||||
|| key.Contains("Anon") || key.Contains("anon"));
|
||||
|
||||
private static void Note(string op, string key, string value)
|
||||
{
|
||||
if (IsInteresting(key))
|
||||
Plugin.Log.LogWarning($"[DUID-PROBE] {op} {key} = \"{value}\"");
|
||||
else if (SeenKeys.Add($"{op}:{key}"))
|
||||
Plugin.Log.LogInfo($"[DUID-PROBE] {op} {key} = \"{value}\"");
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(PlayerPrefs), nameof(PlayerPrefs.GetString), [typeof(string)])]
|
||||
private static void GetStringPostfix(string key, string __result) => Note("get", key, __result);
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(PlayerPrefs), nameof(PlayerPrefs.GetString), [typeof(string), typeof(string)])]
|
||||
private static void GetStringDefaultPostfix(string key, string __result) => Note("get", key, __result);
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(PlayerPrefs), nameof(PlayerPrefs.SetString))]
|
||||
private static void SetStringPrefix(string key, string value) => Note("SET", key, value);
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(PlayerPrefs), nameof(PlayerPrefs.DeleteKey))]
|
||||
private static void DeleteKeyPrefix(string key) => Note("DEL", key, "<deleted>");
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(CheatManager), "WriteDUIDs")]
|
||||
private static void WriteDUIDsPrefix() => Plugin.Log.LogWarning("[DUID-PROBE] WriteDUIDs() called");
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(CheatManager), "WriteDUIDs")]
|
||||
private static void WriteDUIDsPostfix() => Plugin.Log.LogWarning("[DUID-PROBE] WriteDUIDs() returned");
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(CheatManager), "ClearDUIDs")]
|
||||
private static void ClearDUIDsPrefix() => Plugin.Log.LogWarning("[DUID-PROBE] ClearDUIDs() called");
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using BestHTTP;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime;
|
||||
using Il2CppInterop.Runtime.InteropTypes.Arrays;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
// Experiment harness for the Create Account hang.
|
||||
//
|
||||
// On a device-id mismatch the client POSTs PlayerReporting/v1/deviceId, the server answers
|
||||
// 200 {"success":true}, and then the client stops: CheatManager.WriteDUIDs() is never called, so the
|
||||
// new id is never persisted and the flow never reaches create_account. That means the client can't
|
||||
// proceed on what it got back.
|
||||
//
|
||||
// This rewrites that one response body before the game sees it, so response shapes can be tried
|
||||
// without redeploying the server. WriteDUIDs() appearing in the log (see DUIDProbePatch) is the
|
||||
// pass signal: it means the client accepted the response and resumed the migration.
|
||||
[HarmonyPatch]
|
||||
public static class DeviceIdResponsePatch
|
||||
{
|
||||
private const string Endpoint = "/PlayerReporting/v1/deviceId";
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(HTTPManager), "SendRequest", [typeof(HTTPRequest)])]
|
||||
private static void Prefix(HTTPRequest request)
|
||||
{
|
||||
if (!request.Uri.AbsoluteUri.Contains(Endpoint, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
var original = request.Callback;
|
||||
|
||||
// Whether the game attached a completion callback at all. If this logs False, the client is
|
||||
// not waiting on this request through the callback API and the "stuck on the response" model
|
||||
// is wrong -- that would be worth knowing before chasing response shapes any further.
|
||||
Plugin.Log.LogWarning($"[DEVICEID] request seen; game callback attached = {original != null}");
|
||||
|
||||
var body = Plugin.DeviceIdResponseOverride.Value;
|
||||
if (string.IsNullOrEmpty(body))
|
||||
return;
|
||||
|
||||
var status = Plugin.DeviceIdResponseStatus.Value;
|
||||
|
||||
request.Callback = DelegateSupport.ConvertDelegate<OnRequestFinishedDelegate>(
|
||||
(Action<HTTPRequest, HTTPResponse>)((req, resp) =>
|
||||
{
|
||||
if (resp != null)
|
||||
{
|
||||
// Set both: DataAsText is computed from Data but cached in dataAsText once read,
|
||||
// and our own HTTP logger may already have read it.
|
||||
resp.Data = new Il2CppStructArray<byte>(Encoding.UTF8.GetBytes(body));
|
||||
resp.dataAsText = body;
|
||||
resp.StatusCode = status;
|
||||
Plugin.Log.LogWarning($"[DEVICEID] response overridden -> {status} {body}");
|
||||
}
|
||||
|
||||
original?.Invoke(req, resp);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public class SendRequestPatch
|
||||
}
|
||||
|
||||
// Cap logged bodies so a large response/request doesn't flood the log.
|
||||
private const int MaxLoggedBodyLength = 1000;
|
||||
private const int MaxLoggedBodyLength = 10000;
|
||||
|
||||
private static string Truncate(string s)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user