mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 15:08:47 -07:00
feat: Implement adjustable room capacity and quest team size
- Added MaxCapacity property to RoomEntity and updated related controllers to utilize it. - Modified RoomsController to set MaxPlayers based on room's MaxCapacity. - Introduced QuestTeamSize class to dynamically adjust quest party sizes beyond the default limit. - Updated DevMenu to include options for debugging and managing room settings. - Created UI elements for adjusting MaxCapacity in the admin interface. - Added configuration file for DevMenu with default settings.
This commit is contained in:
@@ -64,29 +64,4 @@
|
|||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!--
|
|
||||||
Dev-tools only: typed references to the per-install Il2Cpp interop
|
|
||||||
assemblies MelonLoader generates (Assembly-CSharp / UnityEngine.* /
|
|
||||||
TextMeshPro). Used by the native watch "Dev Console" injection
|
|
||||||
(DevWatchButton.cs), which needs real types (Button3D.OnClick,
|
|
||||||
Object.Instantiate, TextMeshProUGUI, UnityAction) that pure reflection
|
|
||||||
couldn't drive in this build. These DLLs are game-derived and are NOT
|
|
||||||
redistributed — they're only referenced at build time and resolved at
|
|
||||||
runtime from the player's own install. Private=false so they're never
|
|
||||||
copied into the mod output. Override the dir with -p:Il2CppAsmDir=… .
|
|
||||||
-->
|
|
||||||
<PropertyGroup>
|
|
||||||
<Il2CppAsmDir Condition="'$(Il2CppAsmDir)' == ''">C:\Users\Alexa\Downloads\DorkRoom-2020.12\MelonLoader\Il2CppAssemblies</Il2CppAsmDir>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup Condition="Exists('$(Il2CppAsmDir)')">
|
|
||||||
<Reference Include="Assembly-CSharp"><HintPath>$(Il2CppAsmDir)\Assembly-CSharp.dll</HintPath><Private>false</Private></Reference>
|
|
||||||
<Reference Include="Il2Cppmscorlib"><HintPath>$(Il2CppAsmDir)\Il2Cppmscorlib.dll</HintPath><Private>false</Private></Reference>
|
|
||||||
<Reference Include="UnityEngine.CoreModule"><HintPath>$(Il2CppAsmDir)\UnityEngine.CoreModule.dll</HintPath><Private>false</Private></Reference>
|
|
||||||
<Reference Include="UnityEngine.UI"><HintPath>$(Il2CppAsmDir)\UnityEngine.UI.dll</HintPath><Private>false</Private></Reference>
|
|
||||||
<Reference Include="UnityEngine.InputLegacyModule"><HintPath>$(Il2CppAsmDir)\UnityEngine.InputLegacyModule.dll</HintPath><Private>false</Private></Reference>
|
|
||||||
<Reference Include="Unity.TextMeshPro"><HintPath>$(Il2CppAsmDir)\Unity.TextMeshPro.dll</HintPath><Private>false</Private></Reference>
|
|
||||||
<Reference Include="Il2CppRecRoom.Promises.Runtime"><HintPath>$(Il2CppAsmDir)\Il2CppRecRoom.Promises.Runtime.dll</HintPath><Private>false</Private></Reference>
|
|
||||||
<Reference Include="Il2CppRecRoom.Datastructures.Runtime"><HintPath>$(Il2CppAsmDir)\Il2CppRecRoom.Datastructures.Runtime.dll</HintPath><Private>false</Private></Reference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+16
-14
@@ -133,6 +133,15 @@ public class Mod : MelonMod
|
|||||||
// DebugConsole.Execute(command). Edit to match this build's actual
|
// DebugConsole.Execute(command). Edit to match this build's actual
|
||||||
// DebugConsoleCommandConfig command names/args.
|
// DebugConsoleCommandConfig command names/args.
|
||||||
public static string[] DevCommands = { "Help=help", "Fly=fly", "NoClip=noclip" };
|
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
|
||||||
|
// quest scoreboard + party HUD build rows dynamically, so they
|
||||||
|
// auto-scale once the team cap is raised. 0 = off (leave at 4).
|
||||||
|
// Only single-team, quest-named configs are touched, so PvP team
|
||||||
|
// sizes (paintball/laser tag) are never changed. See QuestTeamSize.
|
||||||
|
public static int QuestMaxTeamSize = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnInitializeMelon()
|
public override void OnInitializeMelon()
|
||||||
@@ -331,17 +340,6 @@ public class Mod : MelonMod
|
|||||||
|
|
||||||
RegisterJoinTracePatches();
|
RegisterJoinTracePatches();
|
||||||
|
|
||||||
// Debug console: skip CheatManager's tamper detectors so the
|
|
||||||
// console's movement/time commands don't trip a dorm-drop. The
|
|
||||||
// console UI toggle itself is driven from OnUpdate (hotkey) and
|
|
||||||
// resolved lazily — only the anti-cheat skip needs a patch.
|
|
||||||
if (Cfg.EnableDebugConsole)
|
|
||||||
{
|
|
||||||
foreach (var m in new[] { "OnTimeCheatDetected", "OnObscuredTypeCheatDetected", "OnHeightCheatDetected", "OnAdvancedMovementCheatDetected" })
|
|
||||||
TryPatchByName("CheatManager", m, prefix: nameof(DebugConsolePatches.SuppressCheatDetected_Prefix));
|
|
||||||
Log.Msg($"[debugconsole] enabled — CheatManager detectors silenced; press '{Cfg.DebugConsoleToggleKey}' to toggle the console.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Desktop Screen Sharing FPS override. Only register when a target
|
// Desktop Screen Sharing FPS override. Only register when a target
|
||||||
// is set; the postfix on the gadget's Awake rewrites the baked
|
// is set; the postfix on the gadget's Awake rewrites the baked
|
||||||
// refresh-frequency field (and, if enabled, raises the Photon
|
// refresh-frequency field (and, if enabled, raises the Photon
|
||||||
@@ -556,9 +554,12 @@ public class Mod : MelonMod
|
|||||||
_firstUpdateLogged = true;
|
_firstUpdateLogged = true;
|
||||||
Log.Msg("[lifecycle] first OnUpdate tick (Unity frame loop is live)");
|
Log.Msg("[lifecycle] first OnUpdate tick (Unity frame loop is live)");
|
||||||
}
|
}
|
||||||
if (Cfg.EnableDebugConsole) DebugConsolePatches.PollToggleKey();
|
|
||||||
if (Cfg.DiagnoseDevMenu) DevMenuProbe.Tick();
|
// Quest party-size bump runs independent of the diagnostic flow below
|
||||||
if (Cfg.EnableDevWatchButton) DevWatchButton.Tick();
|
// (quest GameConfigurationAssets load when the quest room loads, so we
|
||||||
|
// re-apply periodically). Gated behind QuestMaxTeamSize > 0.
|
||||||
|
if (Cfg.QuestMaxTeamSize > 0) QuestTeamSize.Tick();
|
||||||
|
|
||||||
if (_diagnosticGameComplete) return;
|
if (_diagnosticGameComplete) return;
|
||||||
if (_diagnosticRetryFrame++ > 3600) return;
|
if (_diagnosticRetryFrame++ > 3600) return;
|
||||||
if ((_diagnosticRetryFrame % 60) != 0) return;
|
if ((_diagnosticRetryFrame % 60) != 0) return;
|
||||||
@@ -613,6 +614,7 @@ public class Mod : MelonMod
|
|||||||
if (r.TryGetProperty("DesktopScreenShareResolution", out v)) Cfg.DesktopScreenShareResolution = v.GetInt32();
|
if (r.TryGetProperty("DesktopScreenShareResolution", out v)) Cfg.DesktopScreenShareResolution = v.GetInt32();
|
||||||
if (r.TryGetProperty("DesktopScreenShareQuality", out v)) Cfg.DesktopScreenShareQuality = v.GetInt32();
|
if (r.TryGetProperty("DesktopScreenShareQuality", out v)) Cfg.DesktopScreenShareQuality = v.GetInt32();
|
||||||
if (r.TryGetProperty("DiagnoseDevMenu", out v)) Cfg.DiagnoseDevMenu = v.GetBoolean();
|
if (r.TryGetProperty("DiagnoseDevMenu", out v)) Cfg.DiagnoseDevMenu = v.GetBoolean();
|
||||||
|
if (r.TryGetProperty("QuestMaxTeamSize", out v)) Cfg.QuestMaxTeamSize = v.GetInt32();
|
||||||
if (r.TryGetProperty("EnableDevWatchButton", out v)) Cfg.EnableDevWatchButton = v.GetBoolean();
|
if (r.TryGetProperty("EnableDevWatchButton", out v)) Cfg.EnableDevWatchButton = v.GetBoolean();
|
||||||
if (r.TryGetProperty("DevCommands", out v) && v.ValueKind == JsonValueKind.Array)
|
if (r.TryGetProperty("DevCommands", out v) && v.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
// Raise RRO quest party size past the baked 4.
|
||||||
|
//
|
||||||
|
// The cap is NOT in quest-specific code and NOT server-driven — it lives in
|
||||||
|
// each quest's GameConfigurationAsset.TeamConfigurations[].MaxTeamSize, a
|
||||||
|
// ScriptableObject baked into the client asset bundle. The quest scoreboard
|
||||||
|
// (GameScoreboardBodyView instantiates rows into a List) and the party HUD are
|
||||||
|
// fully dynamic, so they auto-scale once the team cap is raised.
|
||||||
|
//
|
||||||
|
// Implemented with reflection (this project references only MelonLoader /
|
||||||
|
// Harmony / Il2CppInterop.Runtime — NOT the game's interop assemblies), the
|
||||||
|
// same way DevMenuProbe drives the game. Quest configs load when the quest
|
||||||
|
// room loads, so we poll from Mod.OnUpdate rather than running once, and only
|
||||||
|
// touch single-team, quest-named configs so PvP team sizes (paintball/laser
|
||||||
|
// tag, 2+ teams) are never changed. Every config is logged once so we can
|
||||||
|
// confirm targeting (these MaxTeamSize values aren't in the static dump).
|
||||||
|
// Gated behind Cfg.QuestMaxTeamSize.
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using Il2CppInterop.Runtime;
|
||||||
|
|
||||||
|
namespace DorkNet.ClientMod;
|
||||||
|
|
||||||
|
internal static class QuestTeamSize
|
||||||
|
{
|
||||||
|
private const BindingFlags BF = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||||
|
private static int _frame;
|
||||||
|
private static readonly HashSet<string> _logged = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
public static void Tick()
|
||||||
|
{
|
||||||
|
_frame++;
|
||||||
|
if (_frame < 120 || (_frame % 120) != 0) return; // settle, then ~every 2s
|
||||||
|
try { Apply(Mod.Cfg.QuestMaxTeamSize); }
|
||||||
|
catch (Exception ex) { Mod.Log.Warning($"[questsize] failed: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Apply(int target)
|
||||||
|
{
|
||||||
|
if (target <= 0) return;
|
||||||
|
var assetType = Mod.ResolveType("RecRoom.Core.GameManagement.GameConfigurationAsset");
|
||||||
|
if (assetType is null) return;
|
||||||
|
|
||||||
|
// Invoke Il2CppType.From via reflection so the Il2CppSystem.Type
|
||||||
|
// return value stays boxed as object — calling it directly would
|
||||||
|
// need a compile reference to Il2Cppmscorlib, which we don't carry.
|
||||||
|
var il2 = ToIl2CppType(assetType);
|
||||||
|
if (il2 is null) return;
|
||||||
|
|
||||||
|
var resources = Mod.ResolveType("UnityEngine.Resources");
|
||||||
|
var findAll = OneArgStatic(resources, "FindObjectsOfTypeAll", il2);
|
||||||
|
var all = findAll?.Invoke(null, new[] { il2 });
|
||||||
|
int n = ArrLen(all);
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
var asset = ArrItem(all, i);
|
||||||
|
if (asset is null) continue;
|
||||||
|
asset = EnsureConcrete(asset, assetType);
|
||||||
|
|
||||||
|
var name = ReadName(asset);
|
||||||
|
var teams = GetMember(asset, "TeamConfigurations");
|
||||||
|
if (teams is null) continue;
|
||||||
|
int tn = ArrLen(teams);
|
||||||
|
|
||||||
|
if (_logged.Add(name))
|
||||||
|
{
|
||||||
|
var sizes = new List<string>(tn);
|
||||||
|
for (int t = 0; t < tn; t++) sizes.Add(ReadInt(ArrItem(teams, t), "MaxTeamSize").ToString());
|
||||||
|
Mod.Log.Msg($"[questsize] config '{name}' teams=[{string.Join(",", sizes)}] (n={tn})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quests are SINGLE-TEAM co-op. That's the reliable signal: PvP
|
||||||
|
// modes have 2+ teams (Paintball [4,4], StuntRunner [1,1,1,1])
|
||||||
|
// and Sandbox has many — only quests (the generic "Quest" config
|
||||||
|
// AND each per-quest config like "The Rise Of JumboTron") are
|
||||||
|
// n==1. Name-matching missed the per-quest configs, so we key off
|
||||||
|
// team count instead and leave every 2+-team mode untouched.
|
||||||
|
if (tn != 1) continue;
|
||||||
|
|
||||||
|
var changed = false;
|
||||||
|
for (int t = 0; t < tn; t++)
|
||||||
|
{
|
||||||
|
var box = ArrItem(teams, t);
|
||||||
|
if (box is null || ReadInt(box, "MaxTeamSize") >= target) continue;
|
||||||
|
if (WriteInt(box, "MaxTeamSize", target) && ArrSet(teams, t, box)) changed = true;
|
||||||
|
}
|
||||||
|
if (changed) Mod.Log.Msg($"[questsize] bumped '{name}' → MaxTeamSize {target}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindObjectsOfTypeAll returns base-typed elements; downcast to the
|
||||||
|
// concrete interop type so reflection sees TeamConfigurations/Name.
|
||||||
|
private static object EnsureConcrete(object obj, Type target)
|
||||||
|
{
|
||||||
|
if (target.IsInstanceOfType(obj)) return obj;
|
||||||
|
var bt = obj.GetType();
|
||||||
|
while (bt != null && bt.Name != "Il2CppObjectBase") bt = bt.BaseType;
|
||||||
|
var cast = bt?.GetMethod("Cast", BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
if (cast != null)
|
||||||
|
{
|
||||||
|
try { return cast.MakeGenericMethod(target).Invoke(obj, null) ?? obj; }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadName(object asset)
|
||||||
|
{
|
||||||
|
if (GetMember(asset, "Name") is string n && n.Length > 0) return n;
|
||||||
|
if (GetMember(asset, "name") is string un && un.Length > 0) return un;
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? GetMember(object o, string name)
|
||||||
|
{
|
||||||
|
var t = o.GetType();
|
||||||
|
var p = t.GetProperty(name, BF);
|
||||||
|
if (p != null && p.CanRead && p.GetIndexParameters().Length == 0)
|
||||||
|
{ try { return p.GetValue(o); } catch { } }
|
||||||
|
var f = t.GetField(name, BF);
|
||||||
|
if (f != null) { try { return f.GetValue(o); } catch { } }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadInt(object? o, string name)
|
||||||
|
{
|
||||||
|
if (o is null) return 0;
|
||||||
|
var v = GetMember(o, name);
|
||||||
|
if (v is int n) return n;
|
||||||
|
return v != null && int.TryParse(v.ToString(), out var p) ? p : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool WriteInt(object o, string name, int val)
|
||||||
|
{
|
||||||
|
var t = o.GetType();
|
||||||
|
var p = t.GetProperty(name, BF);
|
||||||
|
if (p != null && p.CanWrite) { try { p.SetValue(o, val); return true; } catch { } }
|
||||||
|
var f = t.GetField(name, BF);
|
||||||
|
if (f != null) { try { f.SetValue(o, val); return true; } catch { } }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ArrLen(object? arr)
|
||||||
|
{
|
||||||
|
if (arr is null) return 0;
|
||||||
|
if (arr is Array a) return a.Length;
|
||||||
|
var lp = arr.GetType().GetProperty("Length") ?? arr.GetType().GetProperty("Count");
|
||||||
|
return (lp?.GetValue(arr) as int?) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? ArrItem(object? arr, int i)
|
||||||
|
{
|
||||||
|
if (arr is null) return null;
|
||||||
|
if (arr is Array a) return a.GetValue(i);
|
||||||
|
var gi = arr.GetType().GetMethod("get_Item", new[] { typeof(int) });
|
||||||
|
return gi?.Invoke(arr, new object[] { i });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ArrSet(object arr, int i, object val)
|
||||||
|
{
|
||||||
|
var si = arr.GetType().GetMethod("set_Item", new[] { typeof(int), val.GetType() })
|
||||||
|
?? arr.GetType().GetMethod("set_Item");
|
||||||
|
if (si is null) return false;
|
||||||
|
try { si.Invoke(arr, new object[] { i, val }); return true; } catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? ToIl2CppType(Type managed)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var t = typeof(Il2CppType);
|
||||||
|
var from = t.GetMethod("From", new[] { typeof(Type), typeof(bool) })
|
||||||
|
?? t.GetMethod("From", new[] { typeof(Type) });
|
||||||
|
if (from is null) return null;
|
||||||
|
var args = from.GetParameters().Length == 2
|
||||||
|
? new object[] { managed, true } : new object[] { managed };
|
||||||
|
return from.Invoke(null, args);
|
||||||
|
}
|
||||||
|
catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodInfo? OneArgStatic(Type? t, string name, object arg)
|
||||||
|
{
|
||||||
|
if (t is null) return null;
|
||||||
|
foreach (var m in t.GetMethods(BindingFlags.Public | BindingFlags.Static))
|
||||||
|
{
|
||||||
|
if (m.Name != name) continue;
|
||||||
|
var ps = m.GetParameters();
|
||||||
|
if (ps.Length == 1 && ps[0].ParameterType.IsInstanceOfType(arg)) return m;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
using HarmonyLib;
|
||||||
|
using Il2CppRecRoom.Debugging;
|
||||||
|
using MelonLoader;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
using UnityEngine;
|
||||||
|
using UObject = UnityEngine.Object;
|
||||||
|
|
||||||
|
[assembly: MelonInfo(typeof(DorkNet.DevMenu.DevMenuMod), "DorkNet DevMenu", "1.0.0", "Dork Squad")]
|
||||||
|
[assembly: MelonGame(null, null)]
|
||||||
|
|
||||||
|
namespace DorkNet.DevMenu;
|
||||||
|
|
||||||
|
public class DevMenuMod : MelonMod
|
||||||
|
{
|
||||||
|
internal static MelonLogger.Instance Log => Instance!.LoggerInstance;
|
||||||
|
internal static DevMenuMod? Instance;
|
||||||
|
internal HarmonyLib.Harmony PatchHarmony => HarmonyInstance;
|
||||||
|
private static readonly Dictionary<string, Type> ResolvedTypeCache = new();
|
||||||
|
|
||||||
|
public static class Cfg
|
||||||
|
{
|
||||||
|
public static bool EnableDebugConsole = false;
|
||||||
|
public static string DebugConsoleToggleKey = "BackQuote";
|
||||||
|
public static bool DiagnoseDevMenu = false;
|
||||||
|
public static bool EnableDevWatchButton = false;
|
||||||
|
public static string[] DevCommands = { "Help=help", "Fly=fly", "NoClip=noclip" };
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnInitializeMelon()
|
||||||
|
{
|
||||||
|
Instance = this;
|
||||||
|
Log.Msg("=== DorkNet DevMenu loading ===");
|
||||||
|
LoadConfig();
|
||||||
|
|
||||||
|
if (Cfg.EnableDebugConsole)
|
||||||
|
{
|
||||||
|
foreach (var m in new[] { "OnTimeCheatDetected", "OnObscuredTypeCheatDetected", "OnHeightCheatDetected", "OnAdvancedMovementCheatDetected" })
|
||||||
|
TryPatchByName("CheatManager", m, prefix: nameof(DebugConsolePatches.SuppressCheatDetected_Prefix));
|
||||||
|
Log.Msg($"[debugconsole] enabled; press '{Cfg.DebugConsoleToggleKey}' to toggle the console.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnUpdate()
|
||||||
|
{
|
||||||
|
if (Cfg.EnableDebugConsole) DebugConsolePatches.PollToggleKey();
|
||||||
|
if (Cfg.DiagnoseDevMenu) DevMenuProbe.Tick();
|
||||||
|
if (Cfg.EnableDevWatchButton) DevWatchButton.Tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LoadConfig()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var modDll = Assembly.GetExecutingAssembly().Location;
|
||||||
|
var modsDir = Path.GetDirectoryName(modDll) ?? string.Empty;
|
||||||
|
var gameDir = Path.GetDirectoryName(modsDir) ?? string.Empty;
|
||||||
|
var userData = Path.Combine(gameDir, "MelonLoader", "UserData");
|
||||||
|
Directory.CreateDirectory(userData);
|
||||||
|
|
||||||
|
var devPath = Path.Combine(userData, "dorknet-devmenu.json");
|
||||||
|
var legacyPath = Path.Combine(userData, "dorknet-clientmod.json");
|
||||||
|
var path = File.Exists(devPath) ? devPath : legacyPath;
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
Log.Msg($"[config] no dev config found; all dev menu features disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||||
|
var r = doc.RootElement;
|
||||||
|
if (r.TryGetProperty("EnableDebugConsole", out var v)) Cfg.EnableDebugConsole = v.GetBoolean();
|
||||||
|
if (r.TryGetProperty("DebugConsoleToggleKey", out v)) Cfg.DebugConsoleToggleKey = v.GetString() ?? Cfg.DebugConsoleToggleKey;
|
||||||
|
if (r.TryGetProperty("DiagnoseDevMenu", out v)) Cfg.DiagnoseDevMenu = v.GetBoolean();
|
||||||
|
if (r.TryGetProperty("EnableDevWatchButton", out v)) Cfg.EnableDevWatchButton = v.GetBoolean();
|
||||||
|
if (r.TryGetProperty("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 {Path.GetFileName(path)}: debugConsole={Cfg.EnableDebugConsole}, devWatchButton={Cfg.EnableDevWatchButton}, diagnose={Cfg.DiagnoseDevMenu}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning($"[config] load failed, using disabled defaults: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryPatchByName(string typeName, string methodName, Type[]? args = null, string? prefix = null, string? postfix = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var type = ResolveType(typeName);
|
||||||
|
if (type is null)
|
||||||
|
{
|
||||||
|
Log.Warning($"[patch-miss] {typeName}.{methodName}: type not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var method = args is null
|
||||||
|
? AccessTools.Method(type, methodName)
|
||||||
|
: AccessTools.Method(type, methodName, args);
|
||||||
|
if (method is null)
|
||||||
|
{
|
||||||
|
Log.Warning($"[patch-miss] {typeName}.{methodName}: method not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HarmonyMethod? pre = prefix is null ? null : new HarmonyMethod(GetPatchMethod(prefix));
|
||||||
|
HarmonyMethod? post = postfix is null ? null : new HarmonyMethod(GetPatchMethod(postfix));
|
||||||
|
Instance!.PatchHarmony.Patch(method, pre, post);
|
||||||
|
Log.Msg($"[patch-ok] {typeName}.{methodName}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning($"[patch-fail] {typeName}.{methodName}: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodInfo GetPatchMethod(string name)
|
||||||
|
{
|
||||||
|
foreach (var holder in new[] { typeof(DebugConsolePatches) })
|
||||||
|
{
|
||||||
|
var m = holder.GetMethod(name, BindingFlags.Public | BindingFlags.Static);
|
||||||
|
if (m is not null) return m;
|
||||||
|
}
|
||||||
|
throw new MissingMethodException($"patch method '{name}' not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Type? ResolveType(string requested)
|
||||||
|
{
|
||||||
|
if (ResolvedTypeCache.TryGetValue(requested, out var cached)) return cached;
|
||||||
|
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||||
|
{
|
||||||
|
Type? found = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
found = asm.GetType(requested, throwOnError: false)
|
||||||
|
?? asm.GetType("Il2Cpp" + requested, throwOnError: false);
|
||||||
|
if (found is null)
|
||||||
|
{
|
||||||
|
foreach (var t in asm.GetTypes())
|
||||||
|
{
|
||||||
|
if (t.FullName == requested ||
|
||||||
|
t.Name == requested ||
|
||||||
|
t.FullName?.EndsWith("." + requested, StringComparison.Ordinal) == true ||
|
||||||
|
t.FullName?.EndsWith(".Il2Cpp" + requested, StringComparison.Ordinal) == true)
|
||||||
|
{
|
||||||
|
found = t;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
if (found is null) continue;
|
||||||
|
ResolvedTypeCache[requested] = found;
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class DebugConsolePatches
|
||||||
|
{
|
||||||
|
private static bool _consoleVisible;
|
||||||
|
private static bool _warned;
|
||||||
|
private static KeyCode _toggleKey = KeyCode.BackQuote;
|
||||||
|
private static bool _keyParsed;
|
||||||
|
private static MethodInfo? _toggleMethod;
|
||||||
|
|
||||||
|
public static bool SuppressCheatDetected_Prefix() => false;
|
||||||
|
|
||||||
|
public static void PollToggleKey()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_keyParsed)
|
||||||
|
{
|
||||||
|
_keyParsed = true;
|
||||||
|
if (!Enum.TryParse(DevMenuMod.Cfg.DebugConsoleToggleKey, ignoreCase: true, out _toggleKey))
|
||||||
|
{
|
||||||
|
_toggleKey = KeyCode.BackQuote;
|
||||||
|
DevMenuMod.Log.Warning($"[debugconsole] invalid key '{DevMenuMod.Cfg.DebugConsoleToggleKey}', defaulting to BackQuote");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Input.GetKeyDown(_toggleKey))
|
||||||
|
{
|
||||||
|
Toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!_warned)
|
||||||
|
{
|
||||||
|
_warned = true;
|
||||||
|
DevMenuMod.Log.Warning($"[debugconsole] poll failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Toggle()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var console = UObject.FindObjectOfType<DebugConsole>();
|
||||||
|
if (console == null)
|
||||||
|
{
|
||||||
|
DevMenuMod.Log.Warning("[debugconsole] no live DebugConsole in scene");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_toggleMethod ??= AccessTools.Method(typeof(DebugConsole), "ShowInputField", new[] { typeof(bool) })
|
||||||
|
?? AccessTools.Method(typeof(DebugConsole), "MCNBJFECHLJ", new[] { typeof(bool) })
|
||||||
|
?? FindToggleByShape(typeof(DebugConsole));
|
||||||
|
if (_toggleMethod is null)
|
||||||
|
{
|
||||||
|
DevMenuMod.Log.Warning("[debugconsole] show/hide method not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_consoleVisible = !_consoleVisible;
|
||||||
|
_toggleMethod.Invoke(console, new object[] { _consoleVisible });
|
||||||
|
DevMenuMod.Log.Msg($"[debugconsole] {(_consoleVisible ? "opened" : "closed")}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
DevMenuMod.Log.Warning($"[debugconsole] toggle failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodInfo? FindToggleByShape(Type t)
|
||||||
|
{
|
||||||
|
MethodInfo? found = null;
|
||||||
|
foreach (var m in t.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
|
||||||
|
{
|
||||||
|
if (m.ReturnType != typeof(void)) continue;
|
||||||
|
var ps = m.GetParameters();
|
||||||
|
if (ps.Length != 1 || ps[0].ParameterType != typeof(bool)) continue;
|
||||||
|
if (m.Name.StartsWith("set_", StringComparison.Ordinal)) continue;
|
||||||
|
if (m.IsDefined(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), false)) continue;
|
||||||
|
if (found is not null) return null;
|
||||||
|
found = m;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class DevMenuProbe
|
||||||
|
{
|
||||||
|
private static int _frame;
|
||||||
|
private static bool _done;
|
||||||
|
|
||||||
|
public static void Tick()
|
||||||
|
{
|
||||||
|
if (_done) return;
|
||||||
|
_frame++;
|
||||||
|
if (_frame < 180) return;
|
||||||
|
_done = true;
|
||||||
|
|
||||||
|
var homeType = DevMenuMod.ResolveType("AGUI.StackedUI.HomeScreenFlow") ?? DevMenuMod.ResolveType("HomeScreenFlow");
|
||||||
|
var consoleType = DevMenuMod.ResolveType("RecRoom.Debugging.DebugConsole") ?? typeof(DebugConsole);
|
||||||
|
DevMenuMod.Log.Msg("=== [devmenu-probe] runtime dev-menu state ===");
|
||||||
|
DevMenuMod.Log.Msg($"[devmenu-probe] HomeScreenFlow type: {(homeType is null ? "NOT FOUND" : homeType.FullName)}");
|
||||||
|
DevMenuMod.Log.Msg($"[devmenu-probe] DebugConsole type: {(consoleType is null ? "NOT FOUND" : consoleType.FullName)}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var console = UObject.FindObjectOfType<DebugConsole>();
|
||||||
|
DevMenuMod.Log.Msg($"[devmenu-probe] DebugConsole live instance: {(console == null ? "null" : "FOUND")}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
DevMenuMod.Log.Warning($"[devmenu-probe] console instance probe failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
DevMenuMod.Log.Msg("=== [devmenu-probe] done ===");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ using Il2CppTMPro;
|
|||||||
using UnityEngine.Events;
|
using UnityEngine.Events;
|
||||||
using UObject = UnityEngine.Object;
|
using UObject = UnityEngine.Object;
|
||||||
|
|
||||||
namespace DorkNet.ClientMod;
|
namespace DorkNet.DevMenu;
|
||||||
|
|
||||||
internal static class DevWatchButton
|
internal static class DevWatchButton
|
||||||
{
|
{
|
||||||
@@ -32,9 +32,9 @@ internal static class DevWatchButton
|
|||||||
var home = UObject.FindObjectOfType<HomeScreenFlow>();
|
var home = UObject.FindObjectOfType<HomeScreenFlow>();
|
||||||
if (home == null && _frame < 3600) return; // wait for the watch
|
if (home == null && _frame < 3600) return; // wait for the watch
|
||||||
_done = true;
|
_done = true;
|
||||||
if (home == null) { Mod.Log.Warning("[devbtn] HomeScreenFlow never appeared"); return; }
|
if (home == null) { DevMenuMod.Log.Warning("[devbtn] HomeScreenFlow never appeared"); return; }
|
||||||
try { Inject(home); }
|
try { Inject(home); }
|
||||||
catch (Exception ex) { Mod.Log.Warning($"[devbtn] inject failed: {ex}"); }
|
catch (Exception ex) { DevMenuMod.Log.Warning($"[devbtn] inject failed: {ex}"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Inject(HomeScreenFlow home)
|
private static void Inject(HomeScreenFlow home)
|
||||||
@@ -42,7 +42,7 @@ internal static class DevWatchButton
|
|||||||
var buttons = home.GetComponentsInChildren<Button3D>(true);
|
var buttons = home.GetComponentsInChildren<Button3D>(true);
|
||||||
if (buttons == null || buttons.Length == 0)
|
if (buttons == null || buttons.Length == 0)
|
||||||
{
|
{
|
||||||
Mod.Log.Warning("[devbtn] no Button3D found under HomeScreenFlow");
|
DevMenuMod.Log.Warning("[devbtn] no Button3D found under HomeScreenFlow");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ internal static class DevWatchButton
|
|||||||
if (buttons[i].GetComponentInChildren<TextMeshProUGUI>(true) != null) { src = buttons[i]; break; }
|
if (buttons[i].GetComponentInChildren<TextMeshProUGUI>(true) != null) { src = buttons[i]; break; }
|
||||||
bool labeled = src != null;
|
bool labeled = src != null;
|
||||||
if (src == null) src = buttons[0]; // fall back: inject anyway, just no text
|
if (src == null) src = buttons[0]; // fall back: inject anyway, just no text
|
||||||
Mod.Log.Msg($"[devbtn] {buttons.Length} watch buttons; clone source='{src.name}' labeled={labeled}");
|
DevMenuMod.Log.Msg($"[devbtn] {buttons.Length} watch buttons; clone source='{src.name}' labeled={labeled}");
|
||||||
|
|
||||||
int n = 0;
|
int n = 0;
|
||||||
foreach (var (label, cmd) in ParsePresets())
|
foreach (var (label, cmd) in ParsePresets())
|
||||||
@@ -72,7 +72,7 @@ internal static class DevWatchButton
|
|||||||
_clones.Add(clone);
|
_clones.Add(clone);
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
Mod.Log.Msg($"[devbtn] injected {n} dev command button(s) into the watch bar — open the watch to use them");
|
DevMenuMod.Log.Msg($"[devbtn] injected {n} dev command button(s) into the watch bar — open the watch to use them");
|
||||||
DumpCommands();
|
DumpCommands();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,34 +84,34 @@ internal static class DevWatchButton
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var cfgs = UObject.FindObjectsOfTypeAll(Il2CppType.Of<DebugConsoleCommandConfig>());
|
var cfgs = UObject.FindObjectsOfTypeAll(Il2CppType.Of<DebugConsoleCommandConfig>());
|
||||||
Mod.Log.Msg($"[devcmds] {cfgs.Length} DebugConsoleCommandConfig asset(s) loaded");
|
DevMenuMod.Log.Msg($"[devcmds] {cfgs.Length} DebugConsoleCommandConfig asset(s) loaded");
|
||||||
for (int c = 0; c < cfgs.Length; c++)
|
for (int c = 0; c < cfgs.Length; c++)
|
||||||
{
|
{
|
||||||
var cfg = cfgs[c].TryCast<DebugConsoleCommandConfig>();
|
var cfg = cfgs[c].TryCast<DebugConsoleCommandConfig>();
|
||||||
var metas = cfg?.Metas;
|
var metas = cfg?.Metas;
|
||||||
if (metas == null) continue;
|
if (metas == null) continue;
|
||||||
Mod.Log.Msg($"[devcmds] {metas.Count} command(s):");
|
DevMenuMod.Log.Msg($"[devcmds] {metas.Count} command(s):");
|
||||||
for (int i = 0; i < metas.Count; i++)
|
for (int i = 0; i < metas.Count; i++)
|
||||||
{
|
{
|
||||||
var m = metas[i];
|
var m = metas[i];
|
||||||
Mod.Log.Msg($" {m.MethodName} ({m.DeclaringType})");
|
DevMenuMod.Log.Msg($" {m.MethodName} ({m.DeclaringType})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex) { Mod.Log.Warning($"[devcmds] failed: {ex.Message}"); }
|
catch (Exception ex) { DevMenuMod.Log.Warning($"[devcmds] failed: {ex.Message}"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Run(string label, string command)
|
private static void Run(string label, string command)
|
||||||
{
|
{
|
||||||
Mod.Log.Msg($"[devbtn] '{label}' → DebugConsole.Execute(\"{command}\")");
|
DevMenuMod.Log.Msg($"[devbtn] '{label}' -> DebugConsole.Execute(\"{command}\")");
|
||||||
try { DebugConsole.Execute(command); }
|
try { DebugConsole.Execute(command); }
|
||||||
catch (Exception ex) { Mod.Log.Warning($"[devbtn] Execute('{command}') failed: {ex.Message}"); }
|
catch (Exception ex) { DevMenuMod.Log.Warning($"[devbtn] Execute('{command}') failed: {ex.Message}"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Presets come from Cfg.DevCommands as "Label=command" strings.
|
// Presets come from Cfg.DevCommands as "Label=command" strings.
|
||||||
private static System.Collections.Generic.IEnumerable<(string label, string cmd)> ParsePresets()
|
private static System.Collections.Generic.IEnumerable<(string label, string cmd)> ParsePresets()
|
||||||
{
|
{
|
||||||
foreach (var entry in Mod.Cfg.DevCommands)
|
foreach (var entry in DevMenuMod.Cfg.DevCommands)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(entry)) continue;
|
if (string.IsNullOrWhiteSpace(entry)) continue;
|
||||||
var eq = entry.IndexOf('=');
|
var eq = entry.IndexOf('=');
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>DorkNet.DevMenu</RootNamespace>
|
||||||
|
<AssemblyName>DorkNet.DevMenu</AssemblyName>
|
||||||
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
|
||||||
|
<NoWarn>$(NoWarn);CS8602;CS8604;CS0436</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<MelonLoaderVersion Condition="'$(MelonLoaderVersion)' == ''">v0.6.6</MelonLoaderVersion>
|
||||||
|
<MelonLoaderCacheRoot>$(MSBuildThisFileDirectory)..\DorkNet.ClientMod\build\melonloader</MelonLoaderCacheRoot>
|
||||||
|
<MelonLoaderZip>$(MelonLoaderCacheRoot)\MelonLoader.x64.$(MelonLoaderVersion).zip</MelonLoaderZip>
|
||||||
|
<MelonLoaderDir Condition="'$(MelonLoaderDir)' == ''">$(MelonLoaderCacheRoot)\MelonLoader</MelonLoaderDir>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="EnsureMelonLoaderReferences"
|
||||||
|
BeforeTargets="ResolveAssemblyReferences"
|
||||||
|
Condition="!Exists('$(MelonLoaderDir)\net6\MelonLoader.dll')">
|
||||||
|
<MakeDir Directories="$(MelonLoaderCacheRoot)" />
|
||||||
|
<DownloadFile
|
||||||
|
SourceUrl="https://github.com/LavaGang/MelonLoader/releases/download/$(MelonLoaderVersion)/MelonLoader.x64.zip"
|
||||||
|
DestinationFolder="$(MelonLoaderCacheRoot)"
|
||||||
|
DestinationFileName="MelonLoader.x64.$(MelonLoaderVersion).zip"
|
||||||
|
SkipUnchangedFiles="true" />
|
||||||
|
<Unzip SourceFiles="$(MelonLoaderZip)"
|
||||||
|
DestinationFolder="$(MelonLoaderCacheRoot)"
|
||||||
|
OverwriteReadOnlyFiles="true" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="MelonLoader">
|
||||||
|
<HintPath>$(MelonLoaderDir)\net6\MelonLoader.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="0Harmony">
|
||||||
|
<HintPath>$(MelonLoaderDir)\net6\0Harmony.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Il2CppInterop.Runtime">
|
||||||
|
<HintPath>$(MelonLoaderDir)\net6\Il2CppInterop.Runtime.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Il2CppInterop.Common">
|
||||||
|
<HintPath>$(MelonLoaderDir)\net6\Il2CppInterop.Common.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<Il2CppAsmDir Condition="'$(Il2CppAsmDir)' == ''">C:\Users\Alexa\Downloads\DorkRoom-2020.12\MelonLoader\Il2CppAssemblies</Il2CppAsmDir>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup Condition="Exists('$(Il2CppAsmDir)')">
|
||||||
|
<Reference Include="Assembly-CSharp"><HintPath>$(Il2CppAsmDir)\Assembly-CSharp.dll</HintPath><Private>false</Private></Reference>
|
||||||
|
<Reference Include="Il2Cppmscorlib"><HintPath>$(Il2CppAsmDir)\Il2Cppmscorlib.dll</HintPath><Private>false</Private></Reference>
|
||||||
|
<Reference Include="UnityEngine.CoreModule"><HintPath>$(Il2CppAsmDir)\UnityEngine.CoreModule.dll</HintPath><Private>false</Private></Reference>
|
||||||
|
<Reference Include="UnityEngine.UI"><HintPath>$(Il2CppAsmDir)\UnityEngine.UI.dll</HintPath><Private>false</Private></Reference>
|
||||||
|
<Reference Include="UnityEngine.InputLegacyModule"><HintPath>$(Il2CppAsmDir)\UnityEngine.InputLegacyModule.dll</HintPath><Private>false</Private></Reference>
|
||||||
|
<Reference Include="Unity.TextMeshPro"><HintPath>$(Il2CppAsmDir)\Unity.TextMeshPro.dll</HintPath><Private>false</Private></Reference>
|
||||||
|
<Reference Include="Il2CppRecRoom.Promises.Runtime"><HintPath>$(Il2CppAsmDir)\Il2CppRecRoom.Promises.Runtime.dll</HintPath><Private>false</Private></Reference>
|
||||||
|
<Reference Include="Il2CppRecRoom.Datastructures.Runtime"><HintPath>$(Il2CppAsmDir)\Il2CppRecRoom.Datastructures.Runtime.dll</HintPath><Private>false</Private></Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"EnableDebugConsole": false,
|
||||||
|
"DebugConsoleToggleKey": "BackQuote",
|
||||||
|
"DiagnoseDevMenu": false,
|
||||||
|
"EnableDevWatchButton": false,
|
||||||
|
"DevCommands": [
|
||||||
|
"Help=help",
|
||||||
|
"Fly=fly",
|
||||||
|
"NoClip=noclip"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1200,7 +1200,7 @@ public class RoomsController(
|
|||||||
Name = "Home",
|
Name = "Home",
|
||||||
IsSandbox = false,
|
IsSandbox = false,
|
||||||
DataBlobName = dataBlobName,
|
DataBlobName = dataBlobName,
|
||||||
MaxPlayers = 8,
|
MaxPlayers = room.MaxCapacity,
|
||||||
CanMatchmakeInto = true,
|
CanMatchmakeInto = true,
|
||||||
// Use the room's actual UpdatedAt so the watch's
|
// Use the room's actual UpdatedAt so the watch's
|
||||||
// "is my local cache stale?" check sees an accurate
|
// "is my local cache stale?" check sees an accurate
|
||||||
|
|||||||
@@ -903,7 +903,8 @@ public class AdminController(
|
|||||||
string? TagsCsv,
|
string? TagsCsv,
|
||||||
double? HotScore,
|
double? HotScore,
|
||||||
int? State,
|
int? State,
|
||||||
string? ImageName);
|
string? ImageName,
|
||||||
|
int? MaxCapacity);
|
||||||
|
|
||||||
/// <summary>POST <c>api/admin/v1/rooms/{id}/props</c> — partial
|
/// <summary>POST <c>api/admin/v1/rooms/{id}/props</c> — partial
|
||||||
/// update of room properties. Only the non-null fields in the
|
/// update of room properties. Only the non-null fields in the
|
||||||
@@ -926,6 +927,13 @@ public class AdminController(
|
|||||||
if (body.HotScore is double h) { room.HotScore = h; changes.Add($"hot={h}"); }
|
if (body.HotScore is double h) { room.HotScore = h; changes.Add($"hot={h}"); }
|
||||||
if (body.State is int st) { room.State = st; changes.Add($"state={st}"); }
|
if (body.State is int st) { room.State = st; changes.Add($"state={st}"); }
|
||||||
if (body.ImageName is string img) { room.ImageName = img; changes.Add($"image={img}"); }
|
if (body.ImageName is string img) { room.ImageName = img; changes.Add($"image={img}"); }
|
||||||
|
if (body.MaxCapacity is int cap)
|
||||||
|
{
|
||||||
|
// Clamp to a sane range. Note: advertised cap only on the 2020.12
|
||||||
|
// client today (no Photon hard-enforce) — see RoomEntity.MaxCapacity.
|
||||||
|
room.MaxCapacity = Math.Clamp(cap, 1, 80);
|
||||||
|
changes.Add($"maxCapacity={room.MaxCapacity}");
|
||||||
|
}
|
||||||
|
|
||||||
if (changes.Count == 0) return Ok(new { room.Id, unchanged = true });
|
if (changes.Count == 0) return Ok(new { room.Id, unchanged = true });
|
||||||
|
|
||||||
@@ -989,6 +997,7 @@ public class AdminController(
|
|||||||
room.SupportsWalkVR,
|
room.SupportsWalkVR,
|
||||||
room.SupportsTeleportVR,
|
room.SupportsTeleportVR,
|
||||||
room.AllowsJuniors,
|
room.AllowsJuniors,
|
||||||
|
room.MaxCapacity,
|
||||||
room.DisableMicAutoMute,
|
room.DisableMicAutoMute,
|
||||||
room.RoomWarningMask,
|
room.RoomWarningMask,
|
||||||
room.CustomRoomWarning,
|
room.CustomRoomWarning,
|
||||||
|
|||||||
@@ -1026,6 +1026,7 @@ public class GoToController(
|
|||||||
// baked-in DormRoom mapping for unknown names.
|
// baked-in DormRoom mapping for unknown names.
|
||||||
long roomId;
|
long roomId;
|
||||||
string locationId;
|
string locationId;
|
||||||
|
var maxCapacity = 8; // admin-adjustable per-room cap; 8 = default/fallback
|
||||||
if (roomName.Equals("DormRoom", StringComparison.OrdinalIgnoreCase))
|
if (roomName.Equals("DormRoom", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// Each player has their own dorm row (created at signup,
|
// Each player has their own dorm row (created at signup,
|
||||||
@@ -1038,6 +1039,7 @@ public class GoToController(
|
|||||||
var personal = await rooms.EnsurePersonalDormAsync(callerPid);
|
var personal = await rooms.EnsurePersonalDormAsync(callerPid);
|
||||||
roomId = personal.Id;
|
roomId = personal.Id;
|
||||||
locationId = personal.LocationReplicationId;
|
locationId = personal.LocationReplicationId;
|
||||||
|
maxCapacity = personal.MaxCapacity;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1055,6 +1057,7 @@ public class GoToController(
|
|||||||
{
|
{
|
||||||
roomId = room.Id;
|
roomId = room.Id;
|
||||||
locationId = room.LocationReplicationId;
|
locationId = room.LocationReplicationId;
|
||||||
|
maxCapacity = room.MaxCapacity;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1103,7 +1106,7 @@ public class GoToController(
|
|||||||
PhotonRegionId = photonRegion,
|
PhotonRegionId = photonRegion,
|
||||||
PhotonRoomId = photonRoom,
|
PhotonRoomId = photonRoom,
|
||||||
Name = dormInstanceName,
|
Name = dormInstanceName,
|
||||||
MaxCapacity = 8,
|
MaxCapacity = maxCapacity,
|
||||||
IsFull = false,
|
IsFull = false,
|
||||||
IsPrivate = false,
|
IsPrivate = false,
|
||||||
IsInProgress = true,
|
IsInProgress = true,
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ public class MatchPlayerController(
|
|||||||
location: location,
|
location: location,
|
||||||
dataBlob: dataBlob,
|
dataBlob: dataBlob,
|
||||||
photonRegion: photonRegion,
|
photonRegion: photonRegion,
|
||||||
maxCapacity: 8,
|
maxCapacity: personal.MaxCapacity,
|
||||||
photonRoomId: photonRoomId);
|
photonRoomId: photonRoomId);
|
||||||
|
|
||||||
return new RoomInstanceDto
|
return new RoomInstanceDto
|
||||||
@@ -363,7 +363,7 @@ public class MatchPlayerController(
|
|||||||
PhotonRegionId = photonRegion,
|
PhotonRegionId = photonRegion,
|
||||||
PhotonRoomId = photonRoomId,
|
PhotonRoomId = photonRoomId,
|
||||||
Name = dormInstanceName,
|
Name = dormInstanceName,
|
||||||
MaxCapacity = 8,
|
MaxCapacity = personal.MaxCapacity,
|
||||||
IsFull = false,
|
IsFull = false,
|
||||||
IsPrivate = true,
|
IsPrivate = true,
|
||||||
IsInProgress = true,
|
IsInProgress = true,
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ public class RoomEntity
|
|||||||
public bool SupportsWalkVR { get; set; } = true;
|
public bool SupportsWalkVR { get; set; } = true;
|
||||||
public bool SupportsTeleportVR { get; set; } = true;
|
public bool SupportsTeleportVR { get; set; } = true;
|
||||||
public bool AllowsJuniors { get; set; } = true;
|
public bool AllowsJuniors { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Max players the room advertises per instance — flows into the
|
||||||
|
/// matchmaking RoomInstance.MaxCapacity and the v4/details synthesized
|
||||||
|
/// Scenes[0].MaxPlayers. Admin-adjustable. NOTE: the 2020.12 client
|
||||||
|
/// doesn't hard-enforce this (it never sets Photon RoomOptions.MaxPlayers),
|
||||||
|
/// so today it's the advertised/intended cap; true enforcement needs a
|
||||||
|
/// ClientMod Photon patch (tracked as a follow-up).</summary>
|
||||||
|
public int MaxCapacity { get; set; } = 8;
|
||||||
|
|
||||||
public int RoomWarningMask { get; set; } = 0;
|
public int RoomWarningMask { get; set; } = 0;
|
||||||
|
|
||||||
[MaxLength(512)]
|
[MaxLength(512)]
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ public static class DatabaseBootstrap
|
|||||||
await EnsureServerSettingsColumnsAsync(db);
|
await EnsureServerSettingsColumnsAsync(db);
|
||||||
await EnsureInventorySchemaAsync(db);
|
await EnsureInventorySchemaAsync(db);
|
||||||
await EnsureLeaderboardSchemaAsync(db);
|
await EnsureLeaderboardSchemaAsync(db);
|
||||||
|
await EnsureRoomColumnsAsync(db);
|
||||||
|
|
||||||
// Coach system account at Player.Id=1. The RR-Original room seeder
|
// Coach system account at Player.Id=1. The RR-Original room seeder
|
||||||
// below assigns CreatorPlayerId=1 to every canonical room
|
// below assigns CreatorPlayerId=1 to every canonical room
|
||||||
@@ -239,6 +240,30 @@ public static class DatabaseBootstrap
|
|||||||
await db.Database.ExecuteSqlRawAsync(sql);
|
await db.Database.ExecuteSqlRawAsync(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Add post-Initial columns to the Rooms table. Like the other
|
||||||
|
/// Ensure*Columns helpers, this exists because the Postgres path is
|
||||||
|
/// EnsureCreated-only (no migration replay), so a new entity property
|
||||||
|
/// won't appear on an existing DB without an idempotent ALTER.</summary>
|
||||||
|
private static async Task EnsureRoomColumnsAsync(DorkNetDbContext db)
|
||||||
|
{
|
||||||
|
var provider = db.Database.ProviderName ?? string.Empty;
|
||||||
|
if (provider.Contains("Npgsql", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await db.Database.ExecuteSqlRawAsync(
|
||||||
|
@"ALTER TABLE ""Rooms"" ADD COLUMN IF NOT EXISTS ""MaxCapacity"" integer NOT NULL DEFAULT 8;");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// SQLite has no ADD COLUMN IF NOT EXISTS; ignore the duplicate-
|
||||||
|
// column error when it already exists.
|
||||||
|
await db.Database.ExecuteSqlRawAsync(
|
||||||
|
@"ALTER TABLE ""Rooms"" ADD COLUMN ""MaxCapacity"" INTEGER NOT NULL DEFAULT 8;");
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task EnsureServerSettingsColumnsAsync(DorkNetDbContext db)
|
private static async Task EnsureServerSettingsColumnsAsync(DorkNetDbContext db)
|
||||||
{
|
{
|
||||||
var provider = db.Database.ProviderName ?? string.Empty;
|
var provider = db.Database.ProviderName ?? string.Empty;
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ export interface RoomDetail {
|
|||||||
supportsWalkVR: boolean;
|
supportsWalkVR: boolean;
|
||||||
supportsTeleportVR: boolean;
|
supportsTeleportVR: boolean;
|
||||||
allowsJuniors: boolean;
|
allowsJuniors: boolean;
|
||||||
|
maxCapacity: number;
|
||||||
disableMicAutoMute: boolean;
|
disableMicAutoMute: boolean;
|
||||||
roomWarningMask: number;
|
roomWarningMask: number;
|
||||||
customRoomWarning: string;
|
customRoomWarning: string;
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ function GeneralTab({ room, onSaved }: { room: RoomDetail; onSaved: () => void }
|
|||||||
const [tagsCsv, setTagsCsv] = useState(room.tagsCsv);
|
const [tagsCsv, setTagsCsv] = useState(room.tagsCsv);
|
||||||
const [hotScore, setHotScore] = useState(String(room.hotScore));
|
const [hotScore, setHotScore] = useState(String(room.hotScore));
|
||||||
const [imageName, setImageName] = useState(room.imageName);
|
const [imageName, setImageName] = useState(room.imageName);
|
||||||
|
const [maxCapacity, setMaxCapacity] = useState(String(room.maxCapacity ?? 8));
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
// Re-sync local state if a refresh brings new server values in.
|
// Re-sync local state if a refresh brings new server values in.
|
||||||
@@ -176,12 +177,14 @@ function GeneralTab({ room, onSaved }: { room: RoomDetail; onSaved: () => void }
|
|||||||
setTagsCsv(room.tagsCsv);
|
setTagsCsv(room.tagsCsv);
|
||||||
setHotScore(String(room.hotScore));
|
setHotScore(String(room.hotScore));
|
||||||
setImageName(room.imageName);
|
setImageName(room.imageName);
|
||||||
|
setMaxCapacity(String(room.maxCapacity ?? 8));
|
||||||
}, [room.id, room.updatedAt]);
|
}, [room.id, room.updatedAt]);
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const hot = Number(hotScore);
|
const hot = Number(hotScore);
|
||||||
|
const cap = parseInt(maxCapacity, 10);
|
||||||
await api(`/rooms/${room.id}/props`, {
|
await api(`/rooms/${room.id}/props`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
@@ -191,6 +194,7 @@ function GeneralTab({ room, onSaved }: { room: RoomDetail; onSaved: () => void }
|
|||||||
TagsCsv: tagsCsv,
|
TagsCsv: tagsCsv,
|
||||||
HotScore: Number.isFinite(hot) ? hot : undefined,
|
HotScore: Number.isFinite(hot) ? hot : undefined,
|
||||||
ImageName: imageName,
|
ImageName: imageName,
|
||||||
|
MaxCapacity: Number.isFinite(cap) ? cap : undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
toast.push('Saved', 'success');
|
toast.push('Saved', 'success');
|
||||||
@@ -236,6 +240,20 @@ function GeneralTab({ room, onSaved }: { room: RoomDetail; onSaved: () => void }
|
|||||||
className="input tabular-nums"
|
className="input tabular-nums"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="flex flex-col gap-1">
|
||||||
|
<span className="label">Max players (capacity)</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={80}
|
||||||
|
value={maxCapacity}
|
||||||
|
onChange={e => setMaxCapacity(e.target.value)}
|
||||||
|
className="input tabular-nums"
|
||||||
|
/>
|
||||||
|
<span className="text-[11px] text-ink-500">
|
||||||
|
Advertised cap on the 2020 client (not hard-enforced yet). 1–80.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
<label className="flex flex-col gap-1">
|
<label className="flex flex-col gap-1">
|
||||||
<span className="label">Tags (comma-separated)</span>
|
<span className="label">Tags (comma-separated)</span>
|
||||||
<input
|
<input
|
||||||
|
|||||||
Reference in New Issue
Block a user