mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
feat: Add account privacy settings endpoint
- Implemented new endpoints for retrieving account privacy settings. - Added fields for privacy preferences such as profile visibility, online status, and activity visibility. feat: Extend play menu tags functionality - Updated PlayMenuTagsRequest to include TrendingTags. - Modified SetPlayMenuTags to handle trending tags. - Enhanced logging to include trending tags. fix: Increase maximum archive size for room zip imports - Updated MaxArchiveBytes from 12GB to 25GB in RoomZipImportController. feat: Improve CDN image handling - Added local image path resolution for room images. - Implemented lazy loading of room image aliases from JSON file. feat: Enhance match player presence resolution - Updated ResolveDataBlobForPresenceAsync to consider sub-room IDs and private instance status. feat: Extend RoomEntity with new properties - Added AllowNewUsers, MinLevel, MaxPlayerCalculationMode, LoadScreensJson, PromoImagesJson, and PromoExternalContentJson fields. feat: Update database schema for new room properties - Added new columns to the Rooms table for AllowNewUsers, MinLevel, MaxPlayerCalculationMode, LoadScreensJson, PromoImagesJson, and PromoExternalContentJson. feat: Implement platform notifications controller - Created PlatformNotificationsController for managing user notification preferences. - Added endpoints for retrieving and setting notification preferences and categories.
This commit is contained in:
+334
-200
@@ -49,6 +49,7 @@ public class Mod : MelonMod
|
||||
private static readonly Dictionary<string, Type> ResolvedTypeCache = new();
|
||||
private readonly HashSet<string> _diagnosticPatchLabels = new();
|
||||
private bool _networkPatchesRegistered;
|
||||
private bool _httpDiagnosticsRegistered;
|
||||
private bool _joinTracePatchesRegistered;
|
||||
private bool _quitTraceCoreRegistered;
|
||||
private bool _quitTraceGameComplete;
|
||||
@@ -86,6 +87,9 @@ public class Mod : MelonMod
|
||||
// is a UnityEngine.KeyCode name; BackQuote is the `~` key.
|
||||
public static bool EnableDebugConsole = false;
|
||||
public static string DebugConsoleToggleKey = "BackQuote";
|
||||
public static string[] DevCommands = Array.Empty<string>();
|
||||
public static bool DiagnoseDevMenu = false;
|
||||
public static bool EnableBuiltInDevDebugTools = false;
|
||||
|
||||
// ── Desktop Screen Sharing gadget FPS ──
|
||||
// RecRoom.Tools.Productivity.DesktopScreenSharingDisplay broadcasts
|
||||
@@ -110,29 +114,6 @@ public class Mod : MelonMod
|
||||
public static int DesktopScreenShareResolution = 0; // horizontal px
|
||||
public static int DesktopScreenShareQuality = 0; // JPEG quality
|
||||
|
||||
// One-shot dev-UI diagnostic (no behaviour change). When true,
|
||||
// ~5s after load the mod logs whether the watch's native developer
|
||||
// menu content actually exists in this build (HomeScreenFlow.
|
||||
// devMenuPanel / devButtonPrefab present vs NULL), the dev-flow list
|
||||
// counts, and whether RecRoom.Debugging.DebugConsole is in the scene
|
||||
// + whether its static Execute() command path works. This tells us
|
||||
// stripped-vs-gated so we know whether to un-gate the game's own dev
|
||||
// menu or build a custom watch entry. Output goes to the MelonLoader
|
||||
// console + dorknet-diagnostics.log.
|
||||
public static bool DiagnoseDevMenu = false;
|
||||
|
||||
// Inject a "Dev Console" button into the watch's native dev-menu
|
||||
// panel (HomeScreenFlow.devMenuPanel) by cloning the game's own
|
||||
// devButtonPrefab. The panel/prefab exist in this build (just gated
|
||||
// off), so we instantiate + activate + label, then wire its click to
|
||||
// run console commands. Native Button3D → works in VR and Screen Mode.
|
||||
public static bool EnableDevWatchButton = false;
|
||||
// Preset dev commands shown as watch buttons (when EnableDevWatchButton
|
||||
// is on). Each entry is "Label=command"; clicking runs
|
||||
// DebugConsole.Execute(command). Edit to match this build's actual
|
||||
// DebugConsoleCommandConfig command names/args.
|
||||
public static string[] DevCommands = { "Help=help", "Fly=fly", "NoClip=noclip" };
|
||||
|
||||
// Raise RRO quest party size past the baked 4. The cap lives in each
|
||||
// quest GameConfigurationAsset.TeamConfigurations[].MaxTeamSize
|
||||
// ScriptableObject (baked in the client bundle; no server lever). The
|
||||
@@ -186,116 +167,10 @@ public class Mod : MelonMod
|
||||
"NotifyServerCertificate",
|
||||
prefix: nameof(TlsPatches.NotifyServerCertificate_Prefix));
|
||||
}
|
||||
if (Cfg.EnableDebugConsole || Cfg.EnableDevWatchButton)
|
||||
Log.Msg($"[debugconsole] armed: toggleKey={Cfg.DebugConsoleToggleKey}, commands={DebugConsolePatches.FormatConfiguredCommandsForLog()}");
|
||||
// Image CDN signature verification. Server responses still include
|
||||
// Content-Signature, but the official RSA public key is baked into
|
||||
// GameAssembly and the matching private key is not available. This
|
||||
// method is the verifier in the 2023 build (JFDPIJFDHAD.HOCPMNEKIHP);
|
||||
// skipping it lets signed local CDN/image bytes load.
|
||||
TryPatchByName("JFDPIJFDHAD",
|
||||
"HOCPMNEKIHP",
|
||||
args: new[]
|
||||
{
|
||||
typeof(string),
|
||||
typeof(Il2CppStructArray<byte>),
|
||||
typeof(Il2CppStructArray<byte>),
|
||||
typeof(Il2CppStructArray<byte>),
|
||||
typeof(Il2CppStructArray<byte>)
|
||||
},
|
||||
prefix: nameof(ImageSignaturePatches.VerifyImageSignature_Prefix));
|
||||
RegisterQuitTracePatches(logMisses: true);
|
||||
RegisterAntiTamperCallbackPatches(logMisses: true);
|
||||
RegisterToxModPatches(logMisses: true);
|
||||
PatchFileHashCheckerCallback();
|
||||
// Post-save blob-name refresh. The 2020 watch's
|
||||
// RoomPersistenceManager.RoomDataBlobName is only ever set by
|
||||
// /goto-driven flows — neither the SubscriptionUpdateRoom push
|
||||
// nor SubscriptionUpdatePresence pushes the server fires after
|
||||
// save propagate into this field. So the PostSaveReloading
|
||||
// sub-state's MasterReloadRoomAsync deserializes the cached
|
||||
// bytes from the OLD blob name (the one /goto returned at boot),
|
||||
// wiping any MakerPen object the user just placed even though
|
||||
// the new blob is already on S3.
|
||||
//
|
||||
// Verified live: after a dorm save with object placement, server
|
||||
// pushes SubscriptionUpdateRoom + PresenceUpdate both carrying
|
||||
// dataBlob="dorm_p<id>_v(N+1).dat", but the watch's
|
||||
// set_RoomDataBlobName trace immediately after the save shows
|
||||
// old="v(N)" new="v(N)" — no propagation, no fresh download,
|
||||
// no respawn. Leave+return triggers /goto which DOES update
|
||||
// RoomDataBlobName, so the fix is to do the same write
|
||||
// ourselves: when entering the SavingRoomSuccess state on the
|
||||
// RoomPersistenceManager, read the just-pushed
|
||||
// Rooms.LocalRoomScene.DataBlobName and force-set
|
||||
// __instance.RoomDataBlobName to it. That fires the real
|
||||
// setter → OnRoomDataBlobNameChanged → DownloadRoomDataBlobAsync
|
||||
// for the new blob → PostSaveReloading picks up the fresh
|
||||
// bytes and respawns the MakerPen objects.
|
||||
TryPatchByName("RecRoom.Persistence.RoomPersistenceManager",
|
||||
"set_RoomDataBlobName",
|
||||
args: new[] { typeof(string) },
|
||||
prefix: nameof(SavePatches.SetRoomDataBlobName_Prefix));
|
||||
// Intercept the SubscriptionUpdateRoom push BEFORE the watch's
|
||||
// (cachedRoomId, cachedSubRoomId) early-out gate drops it. We
|
||||
// stash the just-arrived Scenes[0].DataBlobName into a static
|
||||
// so the set_RoomDataBlobName hijack can use it — otherwise
|
||||
// LocalRoomDetails / LocalRoomScene never get the new blob
|
||||
// name on the second-or-later push for the same (room, sub)
|
||||
// tuple (= every post-save push in a dorm).
|
||||
TryPatchByName("RecNet.Rooms",
|
||||
"OnSubscriptionUpdateRoom",
|
||||
prefix: nameof(SavePatches.OnSubscriptionUpdateRoom_Prefix));
|
||||
// Force AccountExtensions.CanLocalPlayerChat to return true. The
|
||||
// 2020 watch's room-chat send path (PlayerEmotes.SendChatEmote
|
||||
// Coroutine d__53 offset 040) gates on this — if false, the
|
||||
// coroutine bails before raising the Photon RPC, so the sender
|
||||
// doesn't even see their own message. The check ANDs three
|
||||
// signals: LocalAccount.TreatAsJunior (ObscuredBool at offset
|
||||
// 0x40), a PlatformManager flag, and a ModerationManager
|
||||
// singleton field. At least one returns the wrong value against
|
||||
// our server. Force-allow — single-tenant private server, text
|
||||
// moderation isn't a concern.
|
||||
TryPatchByName("RecNet.AccountExtensions",
|
||||
"CanLocalPlayerChat",
|
||||
args: Type.EmptyTypes,
|
||||
postfix: nameof(ChatPatches.CanLocalPlayerChat_Postfix));
|
||||
// PlayerEmotes.RemoveInvalidCharactersFromMessage walks the chat
|
||||
// font asset's character table via TMP_FontAsset.HasCharacters
|
||||
// to strip glyphs the font can't render. On 2020 Rec Room the
|
||||
// chat font field can be null at the moment this fires (font
|
||||
// asset bundle hasn't loaded yet?) and HasCharacters NREs deep
|
||||
// inside Unity's TMP — which propagates out through the chat
|
||||
// send callback and aborts the RPC. The watch's own ISIL has
|
||||
// "skipping invalid character check" log paths for missing
|
||||
// assets but doesn't actually short-circuit on the null field
|
||||
// we hit. Skip the method entirely; chat lines will just keep
|
||||
// any glyphs the font can't render (most common: emoji).
|
||||
TryPatchByName("RecRoom.Players.PlayerEmotes",
|
||||
"RemoveInvalidCharactersFromMessage",
|
||||
prefix: nameof(ChatPatches.RemoveInvalidCharactersFromMessage_Prefix));
|
||||
// Trace hooks so we can see exactly where chat dies. Send path:
|
||||
// KeyboardInputField.submit → RoomChatMenu.SendEmoteMessage(string)
|
||||
// → PlayerEmotes.SendChatEmote(msg, true)
|
||||
// → SendChatEmoteCoroutine d__53 → PurifyString HTTP call
|
||||
// → b__0 callback (error, cleanVersion) — fires Photon RPC
|
||||
// AND local-echoes via ProcessNewChatMessageReceived
|
||||
// → on each receiver (including self): RpcChatEmote(msg) →
|
||||
// ProcessNewChatMessageReceived → ReceiveRoomChat appends to
|
||||
// the chat log buffer.
|
||||
TryPatchByName("RecRoom.Players.PlayerEmotes",
|
||||
"SendChatEmote",
|
||||
args: new[] { typeof(string), typeof(bool) },
|
||||
prefix: nameof(ChatPatches.SendChatEmote_Prefix));
|
||||
TryPatchByName("RecRoom.Players.PlayerEmotes",
|
||||
"ProcessNewChatMessageReceived",
|
||||
args: new[] { typeof(string), typeof(bool) },
|
||||
prefix: nameof(ChatPatches.ProcessNewChatMessageReceived_Prefix));
|
||||
TryPatchByName("RecRoom.Players.PlayerEmotes",
|
||||
"RpcChatEmote",
|
||||
args: new[] { typeof(string) },
|
||||
prefix: nameof(ChatPatches.RpcChatEmote_Prefix));
|
||||
|
||||
// Desktop Screen Sharing FPS override. Only register when a target
|
||||
// is set; the postfix on the gadget's Awake rewrites the baked
|
||||
// refresh-frequency field (and, if enabled, raises the Photon
|
||||
@@ -309,11 +184,6 @@ public class Mod : MelonMod
|
||||
$"(raisePhotonRate={Cfg.DesktopScreenShareRaisePhotonRate})");
|
||||
}
|
||||
|
||||
TryPatchByName("RecRoom.Tools.MakerPenVisuals",
|
||||
"set_LaserPointerEnabled",
|
||||
args: new[] { typeof(bool) },
|
||||
prefix: nameof(MakerPenGiftPreviewPatches.LaserPointerEnabled_Prefix));
|
||||
|
||||
// RRO quest party-size bump. Prefix the runtime config-apply path so
|
||||
// we can read the config's Name (scope to the allowlisted quests) and
|
||||
// bump GameConfigurationData.TeamConfigurations[].TeamSize before the
|
||||
@@ -338,9 +208,30 @@ public class Mod : MelonMod
|
||||
|
||||
RegisterStudioTracePatches();
|
||||
|
||||
if (Cfg.EnableBuiltInDevDebugTools)
|
||||
RegisterBuiltInDevDebugTools();
|
||||
|
||||
Log.Msg("=== Client patches registered ===");
|
||||
}
|
||||
|
||||
private void RegisterBuiltInDevDebugTools()
|
||||
{
|
||||
Log.Msg("=== Registering built-in Dev Debug Tools hooks ===");
|
||||
TryPatchByName("RRUI.Data.GlobalModelController+HideIfNotDeveloperImpl",
|
||||
"DGFIJKPCBLK", args: Type.EmptyTypes,
|
||||
prefix: nameof(BuiltInDevDebugToolsPatches.KeepVisible_Prefix));
|
||||
TryPatchByName("RRUI.Data.GlobalModelController+HideIfNotDeveloperAndNotDebugBuildImpl",
|
||||
"DGFIJKPCBLK", args: Type.EmptyTypes,
|
||||
prefix: nameof(BuiltInDevDebugToolsPatches.KeepVisible_Prefix));
|
||||
TryPatchByName("RRUI.Data.GlobalModelController+HideIfNotDebugBuildImpl",
|
||||
"DGFIJKPCBLK", args: Type.EmptyTypes,
|
||||
prefix: nameof(BuiltInDevDebugToolsPatches.KeepVisible_Prefix));
|
||||
TryPatchByName("RRUI.Data.DevDebugBuildInformationModel",
|
||||
"get_BuildInfoFormatted", args: Type.EmptyTypes,
|
||||
postfix: nameof(BuiltInDevDebugToolsPatches.BuildInfoFormatted_Postfix));
|
||||
Log.Msg("=== Built-in Dev Debug Tools hooks registered ===");
|
||||
}
|
||||
|
||||
// ── Room-join + watch-button trace ────────────────────────────────
|
||||
// Traces the full "press the Dorm Room button → join the room" chain
|
||||
// so a stuck join (e.g. the watch's Dorm Room button doing nothing)
|
||||
@@ -631,12 +522,6 @@ public class Mod : MelonMod
|
||||
new[] { typeof(string), typeof(UriKind) },
|
||||
prefix: nameof(UriPatches.UriStringCtor_Prefix));
|
||||
complete &= TryPatchHttpRequestUriConstructors();
|
||||
TryPatchByName("BestHTTP.HTTPRequest", "set_Uri",
|
||||
args: new[] { typeof(Uri) },
|
||||
prefix: nameof(UriPatches.HttpRequestSetUri_Prefix));
|
||||
TryPatchByName("BestHTTP.HTTPRequest", "PrepareUri",
|
||||
args: new[] { typeof(Uri) },
|
||||
prefix: nameof(UriPatches.HttpRequestPrepareUri_Prefix));
|
||||
complete &= TryPatchByName("BestHTTP.HTTPRequest", "Send",
|
||||
prefix: nameof(UriPatches.HttpRequestSend_Prefix));
|
||||
complete &= TryPatchHttpManagerStringSendRequestOverloads();
|
||||
@@ -644,9 +529,90 @@ public class Mod : MelonMod
|
||||
args: new[] { ResolveType("BestHTTP.HTTPRequest") ?? typeof(object) },
|
||||
prefix: nameof(UriPatches.HttpManagerSendRequestObject_Prefix));
|
||||
|
||||
RegisterHttpDiagnostics();
|
||||
_networkPatchesRegistered = complete;
|
||||
}
|
||||
|
||||
private void RegisterHttpDiagnostics()
|
||||
{
|
||||
if (_httpDiagnosticsRegistered) return;
|
||||
|
||||
var complete = true;
|
||||
complete &= TryPatchBndRequestBuilderDiagnostics();
|
||||
complete &= TryPatchDiagnosticAllOverloads("BestHTTP.HTTPRequest", "CallCallback",
|
||||
postfix: nameof(HttpTracePatches.CallCallback_Postfix),
|
||||
finalizer: nameof(HttpTracePatches.CallCallback_Finalizer));
|
||||
complete &= TryPatchByName("BestHTTP.HTTPRequest", "AddHeader",
|
||||
args: new[] { typeof(string), typeof(string) },
|
||||
prefix: nameof(HttpTracePatches.HeaderSet_Prefix));
|
||||
complete &= TryPatchByName("BestHTTP.HTTPRequest", "SetHeader",
|
||||
args: new[] { typeof(string), typeof(string) },
|
||||
prefix: nameof(HttpTracePatches.HeaderSet_Prefix));
|
||||
|
||||
_httpDiagnosticsRegistered = complete;
|
||||
}
|
||||
|
||||
private bool TryPatchBndRequestBuilderDiagnostics()
|
||||
{
|
||||
const string label = "BNDIAONDFFF.request-builder";
|
||||
if (_diagnosticPatchLabels.Contains(label))
|
||||
return true;
|
||||
|
||||
var type = ResolveType("BNDIAONDFFF");
|
||||
if (type is null)
|
||||
{
|
||||
Log.Warning("[patch-miss] BNDIAONDFFF request-builder: type not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
var prefix = new HarmonyMethod(GetPatchMethod(nameof(HttpTracePatches.RecNetRequestBuilderCtor_Prefix)));
|
||||
var postfix = new HarmonyMethod(GetPatchMethod(nameof(HttpTracePatches.RecNetRequestBuilderCtor_Postfix)));
|
||||
var patched = 0;
|
||||
|
||||
foreach (var ctor in type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
|
||||
{
|
||||
var parameters = ctor.GetParameters();
|
||||
if (parameters.Length != 3 || parameters[2].ParameterType != typeof(string)) continue;
|
||||
|
||||
try
|
||||
{
|
||||
HarmonyInstance.Patch(ctor, prefix: prefix, postfix: postfix);
|
||||
patched++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning($"[patch-fail] BNDIAONDFFF..ctor({FormatParameterTypes(parameters)}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var method in AccessTools.GetDeclaredMethods(type))
|
||||
{
|
||||
if (method.Name != "FILJELOLBKK") continue;
|
||||
var parameters = method.GetParameters();
|
||||
if (parameters.Length != 3 || parameters[2].ParameterType != typeof(string)) continue;
|
||||
|
||||
try
|
||||
{
|
||||
HarmonyInstance.Patch(method, prefix: prefix, postfix: postfix);
|
||||
patched++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning($"[patch-fail] BNDIAONDFFF.FILJELOLBKK({FormatParameterTypes(parameters)}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (patched == 0)
|
||||
{
|
||||
Log.Warning("[patch-miss] BNDIAONDFFF request-builder overloads");
|
||||
return false;
|
||||
}
|
||||
|
||||
_diagnosticPatchLabels.Add(label);
|
||||
Log.Msg($"[patch-ok] BNDIAONDFFF request-builder x{patched}");
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryPatchHttpRequestUriConstructors()
|
||||
{
|
||||
var type = ResolveType("BestHTTP.HTTPRequest");
|
||||
@@ -753,10 +719,10 @@ public class Mod : MelonMod
|
||||
RegisterToxModPatches(logMisses: false);
|
||||
}
|
||||
|
||||
if (Cfg.EnableDebugConsole || Cfg.EnableDevWatchButton)
|
||||
if (Cfg.EnableDebugConsole)
|
||||
DebugConsolePatches.PollToggleKey();
|
||||
|
||||
if (Cfg.DiagnoseDevMenu || Cfg.EnableDevWatchButton)
|
||||
if (Cfg.DiagnoseDevMenu)
|
||||
DevMenuProbe.Tick();
|
||||
}
|
||||
|
||||
@@ -783,13 +749,22 @@ public class Mod : MelonMod
|
||||
if (TryGetConfigValue(r, "ServerHost", out var v)) Cfg.ServerHost = v.GetString() ?? Cfg.ServerHost;
|
||||
if (TryGetConfigValue(r, "EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean();
|
||||
if (TryGetConfigValue(r, "TraceRegistrationDialog", out v)) Cfg.TraceRegistrationDialog = v.GetBoolean();
|
||||
if (TryGetConfigValue(r, "EnableDebugConsole", out v)) Cfg.EnableDebugConsole = v.GetBoolean();
|
||||
if (TryGetConfigValue(r, "DebugConsoleToggleKey", out v)) Cfg.DebugConsoleToggleKey = v.GetString() ?? Cfg.DebugConsoleToggleKey;
|
||||
if (TryGetConfigValue(r, "EnableDebugConsole", out v)) Cfg.EnableDebugConsole = v.GetBoolean();
|
||||
if (TryGetConfigValue(r, "DebugConsoleToggleKey", out v)) Cfg.DebugConsoleToggleKey = v.GetString() ?? Cfg.DebugConsoleToggleKey;
|
||||
if (TryGetConfigValue(r, "DiagnoseDevMenu", out v)) Cfg.DiagnoseDevMenu = v.GetBoolean();
|
||||
if (TryGetConfigValue(r, "EnableBuiltInDevDebugTools", out v)) Cfg.EnableBuiltInDevDebugTools = v.GetBoolean();
|
||||
if (TryGetConfigValue(r, "DevCommands", out v) && v.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
var commands = new List<string>();
|
||||
foreach (var e in v.EnumerateArray())
|
||||
if (e.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(e.GetString()))
|
||||
commands.Add(e.GetString()!);
|
||||
Cfg.DevCommands = commands.ToArray();
|
||||
}
|
||||
if (TryGetConfigValue(r, "DesktopScreenShareFps", out v)) Cfg.DesktopScreenShareFps = (float)v.GetDouble();
|
||||
if (TryGetConfigValue(r, "DesktopScreenShareRaisePhotonRate", out v)) Cfg.DesktopScreenShareRaisePhotonRate = v.GetBoolean();
|
||||
if (TryGetConfigValue(r, "DesktopScreenShareResolution", out v)) Cfg.DesktopScreenShareResolution = v.GetInt32();
|
||||
if (TryGetConfigValue(r, "DesktopScreenShareQuality", out v)) Cfg.DesktopScreenShareQuality = v.GetInt32();
|
||||
if (TryGetConfigValue(r, "DiagnoseDevMenu", out v)) Cfg.DiagnoseDevMenu = v.GetBoolean();
|
||||
if (TryGetConfigValue(r, "QuestMaxTeamSize", out v)) Cfg.QuestMaxTeamSize = v.GetInt32();
|
||||
if (TryGetConfigValue(r, "QuestMaxTeamSizeRooms", out v) && v.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
@@ -799,17 +774,6 @@ public class Mod : MelonMod
|
||||
rooms.Add(e.GetString()!);
|
||||
Cfg.QuestMaxTeamSizeRooms = rooms.ToArray(); // explicit (incl. empty = none)
|
||||
}
|
||||
if (TryGetConfigValue(r, "EnableDevWatchButton", out v)) Cfg.EnableDevWatchButton = v.GetBoolean();
|
||||
if (TryGetConfigValue(r, "DevCommands", out v) && v.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
var list = new List<string>();
|
||||
foreach (var e in v.EnumerateArray())
|
||||
{
|
||||
var s = e.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(s)) list.Add(s!);
|
||||
}
|
||||
if (list.Count > 0) Cfg.DevCommands = list.ToArray();
|
||||
}
|
||||
Log.Msg($"[config] loaded from {path}: ServerHost={Cfg.ServerHost}, " +
|
||||
$"EnableTlsTrustBypass={Cfg.EnableTlsTrustBypass}");
|
||||
}
|
||||
@@ -1063,7 +1027,7 @@ public class Mod : MelonMod
|
||||
{
|
||||
// Look in all three patch holder classes — small enough that a
|
||||
// linear scan is cheaper than per-class lookups.
|
||||
foreach (var holder in new[] { typeof(UriPatches), typeof(HttpTracePatches), typeof(AuthPatches), typeof(AntiCheatPatches), typeof(AntiTamperPatches), typeof(FileHashCheckerPatches), typeof(ToxModPatches), typeof(TlsPatches), typeof(ImageSignaturePatches), typeof(QuitTracePatches), typeof(DiagnosticPatches), typeof(RoomLoadDiagnostics), typeof(SavePatches), typeof(ChatPatches), typeof(JoinPatches), typeof(RegistrationPatches), typeof(DebugConsolePatches), typeof(ScreenSharePatches), typeof(MakerPenGiftPreviewPatches), typeof(QuestTeamSize) })
|
||||
foreach (var holder in new[] { typeof(UriPatches), typeof(HttpTracePatches), typeof(AuthPatches), typeof(AntiCheatPatches), typeof(AntiTamperPatches), typeof(FileHashCheckerPatches), typeof(ToxModPatches), typeof(TlsPatches), typeof(QuitTracePatches), typeof(DiagnosticPatches), typeof(RoomLoadDiagnostics), typeof(SavePatches), typeof(ChatPatches), typeof(JoinPatches), typeof(RegistrationPatches), typeof(BuiltInDevDebugToolsPatches), typeof(DebugConsolePatches), typeof(ScreenSharePatches), typeof(MakerPenGiftPreviewPatches), typeof(QuestTeamSize) })
|
||||
{
|
||||
var m = holder.GetMethod(name, BindingFlags.Public | BindingFlags.Static);
|
||||
if (m is not null) return m;
|
||||
@@ -1085,17 +1049,6 @@ public class Mod : MelonMod
|
||||
}
|
||||
|
||||
var complete = true;
|
||||
complete &= TryPatchDiagnosticByName("SessionManager", "TryApplicationQuit",
|
||||
prefix: nameof(QuitTracePatches.SessionManagerTryApplicationQuit_Prefix),
|
||||
logMiss: logMisses);
|
||||
complete &= TryPatchDiagnosticByName("SessionManager", "TryApplicationQuit",
|
||||
args: new[] { typeof(int) },
|
||||
prefix: nameof(QuitTracePatches.SessionManagerTryApplicationQuitInt_Prefix),
|
||||
logMiss: logMisses);
|
||||
complete &= TryPatchDiagnosticByName("SessionManager", "FatalApplicationQuit",
|
||||
args: new[] { typeof(int), typeof(string) },
|
||||
prefix: nameof(QuitTracePatches.SessionManagerFatalApplicationQuit_Prefix),
|
||||
logMiss: logMisses);
|
||||
complete &= TryPatchDiagnosticByName("OPEOLPDLEBO", "GDNAOLMIDLM",
|
||||
prefix: nameof(QuitTracePatches.ApplicationWantsToQuit_Prefix),
|
||||
logMiss: logMisses);
|
||||
@@ -1145,17 +1098,6 @@ public class Mod : MelonMod
|
||||
logMiss: logMisses);
|
||||
}
|
||||
|
||||
TryPatchDiagnosticByName("SessionManager", "TryApplicationQuit",
|
||||
prefix: nameof(DiagnosticPatches.SessionManagerTryApplicationQuit_Prefix),
|
||||
logMiss: logMisses);
|
||||
TryPatchDiagnosticByName("SessionManager", "TryApplicationQuit",
|
||||
args: new[] { typeof(int) },
|
||||
prefix: nameof(DiagnosticPatches.SessionManagerTryApplicationQuitInt_Prefix),
|
||||
logMiss: logMisses);
|
||||
TryPatchDiagnosticByName("SessionManager", "FatalApplicationQuit",
|
||||
args: new[] { typeof(int), typeof(string) },
|
||||
prefix: nameof(DiagnosticPatches.SessionManagerFatalApplicationQuit_Prefix),
|
||||
logMiss: logMisses);
|
||||
RegisterDeepRoomDiagnostics(logMisses);
|
||||
// SteamManager.Awake + SteamPlatformManager.*LoginInitialize were
|
||||
// pure-logging diagnostics that fired during Unity's very first
|
||||
@@ -1732,6 +1674,31 @@ internal static class RoomLoadDiagnostics
|
||||
}
|
||||
|
||||
// ── Debug console access ───────────────────────────────────────────────
|
||||
// Built-in watch Dev Debug Tools hooks. The 2023 RRUI page already exists,
|
||||
// but retail rows are hidden by GlobalModelController hide gates. We keep
|
||||
// those rows visible and append the debug-console command table to the
|
||||
// built-in Build Information text so the watch page reflects the real client
|
||||
// command config.
|
||||
internal static class BuiltInDevDebugToolsPatches
|
||||
{
|
||||
public static bool KeepVisible_Prefix() => false;
|
||||
|
||||
public static void BuildInfoFormatted_Postfix(ref string __result)
|
||||
{
|
||||
try
|
||||
{
|
||||
var commands = DebugConsolePatches.BuildCommandReferenceText("Debug Console Commands", maxCommands: 60);
|
||||
__result = string.IsNullOrWhiteSpace(__result)
|
||||
? commands
|
||||
: __result.TrimEnd() + "\n\n" + commands;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Mod.Log.Warning($"[devtools] failed to append command list: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The 2020 client ships RecRoom.Debugging.DebugConsole — a real in-game
|
||||
// dev console (text input + a DebugConsoleCommandConfig command table:
|
||||
// SetTimeScale, Fly, Teleport, GoToRoom, KillAllEnemies, DebugCompleteDaily,
|
||||
@@ -1766,6 +1733,7 @@ internal static class DebugConsolePatches
|
||||
private static Type? _consoleType;
|
||||
private static MethodInfo? _toggleMethod; // ShowInputField(bool) / MCNBJFECHLJ(bool)
|
||||
private static MethodInfo? _setConsoleText; // DebugConsole.SetConsoleText(string)
|
||||
private static List<ConfiguredCommand>? _commandCache;
|
||||
|
||||
// Harmony prefix returning false → skips the original CheatManager
|
||||
// detector, so a tripped heuristic never runs its punish/drop path.
|
||||
@@ -1773,7 +1741,7 @@ internal static class DebugConsolePatches
|
||||
|
||||
public static string FormatConfiguredCommandsForLog()
|
||||
{
|
||||
var commands = GetConfiguredCommands();
|
||||
var commands = GetAvailableCommands();
|
||||
if (commands.Count == 0) return "<none>";
|
||||
|
||||
var parts = new List<string>();
|
||||
@@ -1971,27 +1939,49 @@ internal static class DebugConsolePatches
|
||||
|
||||
private static string BuildConfiguredCommandText()
|
||||
{
|
||||
var commands = GetConfiguredCommands();
|
||||
return BuildCommandReferenceText("DorkNet debug commands", maxCommands: 80) + "\n\nType a command and press Enter.";
|
||||
}
|
||||
|
||||
public static string BuildCommandReferenceText(string header, int maxCommands = int.MaxValue)
|
||||
{
|
||||
var commands = GetAvailableCommands();
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("DorkNet debug commands");
|
||||
sb.AppendLine(header);
|
||||
sb.AppendLine();
|
||||
if (commands.Count == 0)
|
||||
{
|
||||
sb.AppendLine("No commands configured in DevCommands.");
|
||||
sb.AppendLine("No DebugConsoleCommandConfig commands found.");
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < commands.Count; i++)
|
||||
var visible = Math.Min(commands.Count, maxCommands);
|
||||
for (var i = 0; i < visible; i++)
|
||||
sb.AppendLine($"{i + 1}. {commands[i].Label}: {commands[i].Command}");
|
||||
if (visible < commands.Count)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"+ {commands.Count - visible} more commands available through the debug console.");
|
||||
}
|
||||
}
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Type a command and press Enter.");
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
private static List<ConfiguredCommand> GetConfiguredCommands()
|
||||
internal static List<ConfiguredCommand> GetAvailableCommands()
|
||||
{
|
||||
if (_commandCache is not null) return _commandCache;
|
||||
|
||||
var commands = new List<ConfiguredCommand>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
AddConfiguredCommands(commands, seen);
|
||||
AddRuntimeCommands(commands, seen);
|
||||
commands.Sort((a, b) => string.Compare(a.Label, b.Label, StringComparison.OrdinalIgnoreCase));
|
||||
_commandCache = commands;
|
||||
Mod.Log.Msg($"[debugconsole] command table loaded: {commands.Count} command(s)");
|
||||
return commands;
|
||||
}
|
||||
|
||||
private static void AddConfiguredCommands(List<ConfiguredCommand> commands, HashSet<string> seen)
|
||||
{
|
||||
foreach (var entry in Mod.Cfg.DevCommands)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(entry)) continue;
|
||||
@@ -2000,12 +1990,117 @@ internal static class DebugConsolePatches
|
||||
var command = split >= 0 ? entry[(split + 1)..].Trim() : entry.Trim();
|
||||
if (string.IsNullOrWhiteSpace(command)) continue;
|
||||
if (string.IsNullOrWhiteSpace(label)) label = command;
|
||||
commands.Add(new ConfiguredCommand(label, command));
|
||||
AddCommand(commands, seen, label, command);
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
private readonly struct ConfiguredCommand
|
||||
private static void AddRuntimeCommands(List<ConfiguredCommand> commands, HashSet<string> seen)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cfgType = Mod.ResolveType("RecRoom.Debugging.DebugConsoleCommandConfig");
|
||||
if (cfgType is null)
|
||||
{
|
||||
Mod.Log.Warning("[debugconsole] DebugConsoleCommandConfig type not found");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var method in cfgType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static))
|
||||
{
|
||||
if (method.GetParameters().Length != 0) continue;
|
||||
if (!cfgType.IsAssignableFrom(method.ReturnType)) continue;
|
||||
try { AddCommandConfig(method.Invoke(null, Array.Empty<object>()), commands, seen); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
var il2cppType = ToIl2CppType(cfgType);
|
||||
var resources = Mod.ResolveType("UnityEngine.Resources");
|
||||
if (il2cppType is not null && resources is not null)
|
||||
{
|
||||
var all = OneArgMethod(resources, "FindObjectsOfTypeAll", il2cppType)?.Invoke(null, new[] { il2cppType });
|
||||
var count = ArrLen(all);
|
||||
for (var i = 0; i < count; i++)
|
||||
AddCommandConfig(ArrItem(all, i), commands, seen);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Mod.Log.Warning($"[debugconsole] runtime command scan failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddCommandConfig(object? config, List<ConfiguredCommand> commands, HashSet<string> seen)
|
||||
{
|
||||
if (config is null) return;
|
||||
var metas = GetMemberValue(config, "Metas");
|
||||
var count = ArrLen(metas);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var meta = ArrItem(metas, i);
|
||||
if (meta is null) continue;
|
||||
var methodName = ReadStringMember(meta, "MethodName");
|
||||
if (string.IsNullOrWhiteSpace(methodName)) continue;
|
||||
var declaringType = ReadStringMember(meta, "DeclaringType");
|
||||
var label = string.IsNullOrWhiteSpace(declaringType)
|
||||
? methodName
|
||||
: $"{ShortTypeName(declaringType)}.{methodName}";
|
||||
AddCommand(commands, seen, label, methodName);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddCommand(List<ConfiguredCommand> commands, HashSet<string> seen, string label, string command)
|
||||
{
|
||||
label = CleanLabel(label);
|
||||
command = (command ?? string.Empty).Trim();
|
||||
if (command.Length == 0 || !seen.Add(command)) return;
|
||||
commands.Add(new ConfiguredCommand(label, command));
|
||||
}
|
||||
|
||||
private static object? GetMemberValue(object instance, string name)
|
||||
{
|
||||
var type = instance.GetType();
|
||||
var prop = type.GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (prop is not null && prop.CanRead && prop.GetIndexParameters().Length == 0)
|
||||
{
|
||||
try { return prop.GetValue(instance); } catch { }
|
||||
}
|
||||
|
||||
var field = type.GetField(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (field is not null)
|
||||
{
|
||||
try { return field.GetValue(instance); } catch { }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string ReadStringMember(object instance, string name)
|
||||
{
|
||||
return GetMemberValue(instance, name)?.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
private static string CleanLabel(string label)
|
||||
{
|
||||
label = (label ?? string.Empty).Trim();
|
||||
if (label.Length == 0) return "Command";
|
||||
|
||||
var cleaned = new char[label.Length];
|
||||
for (var i = 0; i < label.Length; i++)
|
||||
{
|
||||
var ch = label[i];
|
||||
cleaned[i] = char.IsLetterOrDigit(ch) || ch is '.' or '_' or '-' or ' ' ? ch : '_';
|
||||
}
|
||||
|
||||
return new string(cleaned);
|
||||
}
|
||||
|
||||
private static string ShortTypeName(string typeName)
|
||||
{
|
||||
var idx = typeName.LastIndexOf('.');
|
||||
return idx >= 0 && idx < typeName.Length - 1 ? typeName[(idx + 1)..] : typeName;
|
||||
}
|
||||
|
||||
internal readonly struct ConfiguredCommand
|
||||
{
|
||||
public ConfiguredCommand(string label, string command)
|
||||
{
|
||||
@@ -2677,6 +2772,8 @@ internal static class HttpTracePatches
|
||||
private static int _failureLogCount;
|
||||
private static int _responseTupleLogCount;
|
||||
private static int _builderSendLogCount;
|
||||
private static int _callbackLogCount;
|
||||
private static int _uriUnreadDumpCount;
|
||||
|
||||
public static void RecNetRequestSend_Prefix(object __instance, MethodBase __originalMethod)
|
||||
{
|
||||
@@ -2827,6 +2924,7 @@ internal static class HttpTracePatches
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ShouldLogCallback(__instance)) return;
|
||||
DiagnosticPatches.Write(
|
||||
$"[http-callback] EXIT {__originalMethod.DeclaringType?.FullName}.{__originalMethod.Name} " +
|
||||
$"{DescribeRequestAndResponse(__instance)}");
|
||||
@@ -2837,6 +2935,27 @@ internal static class HttpTracePatches
|
||||
}
|
||||
}
|
||||
|
||||
public static Exception? CallCallback_Finalizer(Exception? __exception, object __instance, MethodBase __originalMethod)
|
||||
{
|
||||
if (__exception is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
DiagnosticPatches.Write(
|
||||
$"[http-callback] EXCEPTION {__originalMethod.DeclaringType?.FullName}.{__originalMethod.Name} " +
|
||||
$"{DescribeRequestAndResponse(__instance)} " +
|
||||
$"{RoomLoadDiagnostics.FormatException(__exception, __exception)} " +
|
||||
$"state={DumpObjectFields(__instance, 32, 900)}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogFailure("callback-finalizer", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return __exception;
|
||||
}
|
||||
|
||||
public static void TraceUrl(string? url, string source)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return;
|
||||
@@ -2855,14 +2974,18 @@ internal static class HttpTracePatches
|
||||
try
|
||||
{
|
||||
var id = NextRequestId();
|
||||
var uri = TryReadRequestUri(request)?.ToString() ?? "<uri unread>";
|
||||
var uriValue = TryReadRequestUri(request);
|
||||
var uri = uriValue?.ToString() ?? "<uri unread>";
|
||||
var method = TryReadHttpMethod(request);
|
||||
var headers = TryReadHeaders(request);
|
||||
var body = TryReadBodyPreview(request);
|
||||
var fallback = uriValue is null && System.Threading.Interlocked.Increment(ref _uriUnreadDumpCount) <= 40
|
||||
? $" requestFields={DumpObjectFields(request, 24, 260)}"
|
||||
: "";
|
||||
|
||||
DiagnosticPatches.Write(
|
||||
$"[http-trace] #{id} {source} method={method} uri={SanitizeInline(uri)} " +
|
||||
$"headers={headers} body={body}");
|
||||
$"headers={headers} body={body}{fallback}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -3019,6 +3142,14 @@ internal static class HttpTracePatches
|
||||
return "<body unread>";
|
||||
}
|
||||
|
||||
private static bool ShouldLogCallback(object request)
|
||||
{
|
||||
var response = TryReadResponseObject(request);
|
||||
var status = response is null ? null : TryReadStatusCode(response);
|
||||
if (status >= 400) return true;
|
||||
return System.Threading.Interlocked.Increment(ref _callbackLogCount) <= 80;
|
||||
}
|
||||
|
||||
private static string DescribeRequestAndResponse(object request)
|
||||
{
|
||||
var uri = TryReadRequestUri(request)?.ToString() ?? "<uri unread>";
|
||||
@@ -3064,6 +3195,17 @@ internal static class HttpTracePatches
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int? TryReadStatusCode(object response)
|
||||
{
|
||||
if (TryReadMemberValue(response, "StatusCode", out var value))
|
||||
{
|
||||
try { return Convert.ToInt32(value); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string DescribeResponseObject(object response)
|
||||
{
|
||||
var known = DescribeKnownMembers(response,
|
||||
@@ -3806,14 +3948,6 @@ internal static class TlsPatches
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ImageSignaturePatches
|
||||
{
|
||||
public static bool VerifyImageSignature_Prefix()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class QuitTracePatches
|
||||
{
|
||||
public static bool ApplicationQuit_Prefix()
|
||||
|
||||
Reference in New Issue
Block a user